about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/about_controller.rb5
-rw-r--r--app/controllers/accounts_controller.rb1
-rw-r--r--app/controllers/admin/base_controller.rb7
-rw-r--r--app/controllers/api/v1/accounts/search_controller.rb9
-rw-r--r--app/controllers/api/v1/accounts_controller.rb2
-rw-r--r--app/controllers/api/v1/lists/accounts_controller.rb18
-rw-r--r--app/controllers/application_controller.rb82
-rw-r--r--app/controllers/auth/confirmations_controller.rb7
-rw-r--r--app/controllers/auth/passwords_controller.rb5
-rw-r--r--app/controllers/auth/registrations_controller.rb5
-rw-r--r--app/controllers/auth/sessions_controller.rb5
-rw-r--r--app/controllers/authorize_follows_controller.rb5
-rw-r--r--app/controllers/follower_accounts_controller.rb4
-rw-r--r--app/controllers/following_accounts_controller.rb4
-rw-r--r--app/controllers/home_controller.rb5
-rw-r--r--app/controllers/invites_controller.rb5
-rw-r--r--app/controllers/remote_follow_controller.rb5
-rw-r--r--app/controllers/settings/applications_controller.rb4
-rw-r--r--app/controllers/settings/base_controller.rb12
-rw-r--r--app/controllers/settings/deletes_controller.rb6
-rw-r--r--app/controllers/settings/exports_controller.rb6
-rw-r--r--app/controllers/settings/follower_domains_controller.rb6
-rw-r--r--app/controllers/settings/imports_controller.rb5
-rw-r--r--app/controllers/settings/keyword_mutes_controller.rb5
-rw-r--r--app/controllers/settings/notifications_controller.rb6
-rw-r--r--app/controllers/settings/preferences_controller.rb9
-rw-r--r--app/controllers/settings/profiles_controller.rb5
-rw-r--r--app/controllers/settings/sessions_controller.rb1
-rw-r--r--app/controllers/settings/two_factor_authentications_controller.rb5
-rw-r--r--app/controllers/shares_controller.rb5
-rw-r--r--app/controllers/statuses_controller.rb2
-rw-r--r--app/controllers/stream_entries_controller.rb1
-rw-r--r--app/controllers/tags_controller.rb1
-rw-r--r--app/controllers/well_known/webfinger_controller.rb19
-rw-r--r--app/helpers/admin/filter_helper.rb2
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/routing_helper.rb2
-rw-r--r--app/javascript/core/admin.js (renamed from app/javascript/packs/admin.js)2
-rw-r--r--app/javascript/core/common.js8
-rw-r--r--app/javascript/core/embed.js23
-rw-r--r--app/javascript/core/public.js25
-rw-r--r--app/javascript/core/settings.js43
-rw-r--r--app/javascript/core/theme.yml16
-rw-r--r--app/javascript/flavours/glitch/actions/accounts.js (renamed from app/javascript/themes/glitch/actions/accounts.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/alerts.js (renamed from app/javascript/themes/glitch/actions/alerts.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/blocks.js (renamed from app/javascript/themes/glitch/actions/blocks.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/bundles.js (renamed from app/javascript/themes/glitch/actions/bundles.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/cards.js (renamed from app/javascript/themes/glitch/actions/cards.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/columns.js (renamed from app/javascript/themes/glitch/actions/columns.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/compose.js (renamed from app/javascript/themes/glitch/actions/compose.js)4
-rw-r--r--app/javascript/flavours/glitch/actions/domain_blocks.js (renamed from app/javascript/themes/glitch/actions/domain_blocks.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/emojis.js (renamed from app/javascript/themes/glitch/actions/emojis.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/favourites.js (renamed from app/javascript/themes/glitch/actions/favourites.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/height_cache.js (renamed from app/javascript/themes/glitch/actions/height_cache.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/interactions.js (renamed from app/javascript/themes/glitch/actions/interactions.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/local_settings.js (renamed from app/javascript/themes/glitch/actions/local_settings.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/modal.js (renamed from app/javascript/themes/glitch/actions/modal.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/mutes.js (renamed from app/javascript/themes/glitch/actions/mutes.js)4
-rw-r--r--app/javascript/flavours/glitch/actions/notifications.js (renamed from app/javascript/themes/glitch/actions/notifications.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/onboarding.js (renamed from app/javascript/themes/glitch/actions/onboarding.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/pin_statuses.js (renamed from app/javascript/themes/glitch/actions/pin_statuses.js)4
-rw-r--r--app/javascript/flavours/glitch/actions/push_notifications.js (renamed from app/javascript/themes/glitch/actions/push_notifications.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/reports.js (renamed from app/javascript/themes/glitch/actions/reports.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/search.js (renamed from app/javascript/themes/glitch/actions/search.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/settings.js (renamed from app/javascript/themes/glitch/actions/settings.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/statuses.js (renamed from app/javascript/themes/glitch/actions/statuses.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/store.js (renamed from app/javascript/themes/glitch/actions/store.js)0
-rw-r--r--app/javascript/flavours/glitch/actions/streaming.js (renamed from app/javascript/themes/glitch/actions/streaming.js)2
-rw-r--r--app/javascript/flavours/glitch/actions/timelines.js (renamed from app/javascript/themes/glitch/actions/timelines.js)2
-rw-r--r--app/javascript/flavours/glitch/components/account.js (renamed from app/javascript/themes/glitch/components/account.js)6
-rw-r--r--app/javascript/flavours/glitch/components/attachment_list.js (renamed from app/javascript/themes/glitch/components/attachment_list.js)0
-rw-r--r--app/javascript/flavours/glitch/components/autosuggest_emoji.js (renamed from app/javascript/themes/glitch/components/autosuggest_emoji.js)2
-rw-r--r--app/javascript/flavours/glitch/components/autosuggest_textarea.js (renamed from app/javascript/themes/glitch/components/autosuggest_textarea.js)4
-rw-r--r--app/javascript/flavours/glitch/components/avatar.js (renamed from app/javascript/themes/glitch/components/avatar.js)0
-rw-r--r--app/javascript/flavours/glitch/components/avatar_overlay.js (renamed from app/javascript/themes/glitch/components/avatar_overlay.js)0
-rw-r--r--app/javascript/flavours/glitch/components/button.js (renamed from app/javascript/themes/glitch/components/button.js)0
-rw-r--r--app/javascript/flavours/glitch/components/collapsable.js (renamed from app/javascript/themes/glitch/components/collapsable.js)2
-rw-r--r--app/javascript/flavours/glitch/components/column.js (renamed from app/javascript/themes/glitch/components/column.js)2
-rw-r--r--app/javascript/flavours/glitch/components/column_back_button.js (renamed from app/javascript/themes/glitch/components/column_back_button.js)0
-rw-r--r--app/javascript/flavours/glitch/components/column_back_button_slim.js (renamed from app/javascript/themes/glitch/components/column_back_button_slim.js)0
-rw-r--r--app/javascript/flavours/glitch/components/column_header.js (renamed from app/javascript/themes/glitch/components/column_header.js)3
-rw-r--r--app/javascript/flavours/glitch/components/display_name.js (renamed from app/javascript/themes/glitch/components/display_name.js)0
-rw-r--r--app/javascript/flavours/glitch/components/dropdown_menu.js (renamed from app/javascript/themes/glitch/components/dropdown_menu.js)2
-rw-r--r--app/javascript/flavours/glitch/components/extended_video_player.js (renamed from app/javascript/themes/glitch/components/extended_video_player.js)0
-rw-r--r--app/javascript/flavours/glitch/components/icon_button.js (renamed from app/javascript/themes/glitch/components/icon_button.js)2
-rw-r--r--app/javascript/flavours/glitch/components/intersection_observer_article.js (renamed from app/javascript/themes/glitch/components/intersection_observer_article.js)4
-rw-r--r--app/javascript/flavours/glitch/components/load_more.js (renamed from app/javascript/themes/glitch/components/load_more.js)0
-rw-r--r--app/javascript/flavours/glitch/components/loading_indicator.js (renamed from app/javascript/themes/glitch/components/loading_indicator.js)0
-rw-r--r--app/javascript/flavours/glitch/components/media_gallery.js (renamed from app/javascript/themes/glitch/components/media_gallery.js)4
-rw-r--r--app/javascript/flavours/glitch/components/missing_indicator.js (renamed from app/javascript/themes/glitch/components/missing_indicator.js)0
-rw-r--r--app/javascript/flavours/glitch/components/notification_purge_buttons.js (renamed from app/javascript/themes/glitch/components/notification_purge_buttons.js)0
-rw-r--r--app/javascript/flavours/glitch/components/permalink.js (renamed from app/javascript/themes/glitch/components/permalink.js)0
-rw-r--r--app/javascript/flavours/glitch/components/relative_timestamp.js (renamed from app/javascript/themes/glitch/components/relative_timestamp.js)0
-rw-r--r--app/javascript/flavours/glitch/components/scrollable_list.js (renamed from app/javascript/themes/glitch/components/scrollable_list.js)6
-rw-r--r--app/javascript/flavours/glitch/components/setting_text.js (renamed from app/javascript/themes/glitch/components/setting_text.js)0
-rw-r--r--app/javascript/flavours/glitch/components/status.js (renamed from app/javascript/themes/glitch/components/status.js)4
-rw-r--r--app/javascript/flavours/glitch/components/status_action_bar.js (renamed from app/javascript/themes/glitch/components/status_action_bar.js)7
-rw-r--r--app/javascript/flavours/glitch/components/status_content.js (renamed from app/javascript/themes/glitch/components/status_content.js)2
-rw-r--r--app/javascript/flavours/glitch/components/status_header.js (renamed from app/javascript/themes/glitch/components/status_header.js)0
-rw-r--r--app/javascript/flavours/glitch/components/status_list.js (renamed from app/javascript/themes/glitch/components/status_list.js)2
-rw-r--r--app/javascript/flavours/glitch/components/status_prepend.js (renamed from app/javascript/themes/glitch/components/status_prepend.js)0
-rw-r--r--app/javascript/flavours/glitch/components/status_visibility_icon.js (renamed from app/javascript/themes/glitch/components/status_visibility_icon.js)0
-rw-r--r--app/javascript/flavours/glitch/containers/account_container.js (renamed from app/javascript/themes/glitch/containers/account_container.js)12
-rw-r--r--app/javascript/flavours/glitch/containers/card_container.js (renamed from app/javascript/themes/glitch/containers/card_container.js)2
-rw-r--r--app/javascript/flavours/glitch/containers/compose_container.js (renamed from app/javascript/themes/glitch/containers/compose_container.js)8
-rw-r--r--app/javascript/flavours/glitch/containers/dropdown_menu_container.js (renamed from app/javascript/themes/glitch/containers/dropdown_menu_container.js)6
-rw-r--r--app/javascript/flavours/glitch/containers/intersection_observer_article_container.js (renamed from app/javascript/themes/glitch/containers/intersection_observer_article_container.js)4
-rw-r--r--app/javascript/flavours/glitch/containers/mastodon.js (renamed from app/javascript/themes/glitch/containers/mastodon.js)14
-rw-r--r--app/javascript/flavours/glitch/containers/media_gallery_container.js (renamed from app/javascript/themes/glitch/containers/media_gallery_container.js)2
-rw-r--r--app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js (renamed from app/javascript/themes/glitch/containers/notification_purge_buttons_container.js)6
-rw-r--r--app/javascript/flavours/glitch/containers/status_container.js (renamed from app/javascript/themes/glitch/containers/status_container.js)20
-rw-r--r--app/javascript/flavours/glitch/containers/timeline_container.js (renamed from app/javascript/themes/glitch/containers/timeline_container.js)10
-rw-r--r--app/javascript/flavours/glitch/containers/video_container.js (renamed from app/javascript/themes/glitch/containers/video_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/account/components/action_bar.js (renamed from app/javascript/themes/glitch/features/account/components/action_bar.js)9
-rw-r--r--app/javascript/flavours/glitch/features/account/components/header.js (renamed from app/javascript/themes/glitch/features/account/components/header.js)10
-rw-r--r--app/javascript/flavours/glitch/features/account_gallery/components/media_item.js (renamed from app/javascript/themes/glitch/features/account_gallery/components/media_item.js)2
-rw-r--r--app/javascript/flavours/glitch/features/account_gallery/index.js (renamed from app/javascript/themes/glitch/features/account_gallery/index.js)16
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/components/header.js (renamed from app/javascript/themes/glitch/features/account_timeline/components/header.js)6
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js (renamed from app/javascript/themes/glitch/features/account_timeline/containers/header_container.js)18
-rw-r--r--app/javascript/flavours/glitch/features/account_timeline/index.js (renamed from app/javascript/themes/glitch/features/account_timeline/index.js)4
-rw-r--r--app/javascript/flavours/glitch/features/blocks/index.js (renamed from app/javascript/themes/glitch/features/blocks/index.js)10
-rw-r--r--app/javascript/flavours/glitch/features/community_timeline/components/column_settings.js (renamed from app/javascript/themes/glitch/features/community_timeline/components/column_settings.js)2
-rw-r--r--app/javascript/flavours/glitch/features/community_timeline/containers/column_settings_container.js (renamed from app/javascript/themes/glitch/features/community_timeline/containers/column_settings_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/community_timeline/index.js (renamed from app/javascript/themes/glitch/features/community_timeline/index.js)12
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/advanced_options.js (renamed from app/javascript/themes/glitch/features/compose/components/advanced_options.js)0
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/advanced_options_toggle.js (renamed from app/javascript/themes/glitch/features/compose/components/advanced_options_toggle.js)0
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/attach_options.js (renamed from app/javascript/themes/glitch/features/compose/components/attach_options.js)4
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/autosuggest_account.js (renamed from app/javascript/themes/glitch/features/compose/components/autosuggest_account.js)4
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/character_counter.js (renamed from app/javascript/themes/glitch/features/compose/components/character_counter.js)0
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/compose_form.js (renamed from app/javascript/themes/glitch/features/compose/components/compose_form.js)12
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/dropdown.js (renamed from app/javascript/themes/glitch/features/compose/components/dropdown.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.js (renamed from app/javascript/themes/glitch/features/compose/components/emoji_picker_dropdown.js)4
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/navigation_bar.js (renamed from app/javascript/themes/glitch/features/compose/components/navigation_bar.js)6
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js (renamed from app/javascript/themes/glitch/features/compose/components/privacy_dropdown.js)4
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/reply_indicator.js (renamed from app/javascript/themes/glitch/features/compose/components/reply_indicator.js)6
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/search.js (renamed from app/javascript/themes/glitch/features/compose/components/search.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/search_results.js (renamed from app/javascript/themes/glitch/features/compose/components/search_results.js)4
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/text_icon_button.js (renamed from app/javascript/themes/glitch/features/compose/components/text_icon_button.js)0
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/upload.js (renamed from app/javascript/themes/glitch/features/compose/components/upload.js)4
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/upload_button.js (renamed from app/javascript/themes/glitch/features/compose/components/upload_button.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/upload_form.js (renamed from app/javascript/themes/glitch/features/compose/components/upload_form.js)0
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/upload_progress.js (renamed from app/javascript/themes/glitch/features/compose/components/upload_progress.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/warning.js (renamed from app/javascript/themes/glitch/features/compose/components/warning.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/advanced_options_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/advanced_options_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/autosuggest_account_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/autosuggest_account_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/compose_form_container.js)4
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/emoji_picker_dropdown_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/emoji_picker_dropdown_container.js)4
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/navigation_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/navigation_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/privacy_dropdown_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/privacy_dropdown_container.js)6
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/reply_indicator_container.js)4
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/search_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/search_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/search_results_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/search_results_container.js)0
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/sensitive_button_container.js)6
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/spoiler_button_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/spoiler_button_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/upload_button_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/upload_button_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/upload_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/upload_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/upload_form_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/upload_form_container.js)0
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/upload_progress_container.js)0
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/warning_container.js (renamed from app/javascript/themes/glitch/features/compose/containers/warning_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/compose/index.js (renamed from app/javascript/themes/glitch/features/compose/index.js)10
-rw-r--r--app/javascript/flavours/glitch/features/direct_timeline/containers/column_settings_container.js (renamed from app/javascript/themes/glitch/features/direct_timeline/containers/column_settings_container.js)4
-rw-r--r--app/javascript/flavours/glitch/features/direct_timeline/index.js (renamed from app/javascript/themes/glitch/features/direct_timeline/index.js)12
-rw-r--r--app/javascript/flavours/glitch/features/favourited_statuses/index.js (renamed from app/javascript/themes/glitch/features/favourited_statuses/index.js)10
-rw-r--r--app/javascript/flavours/glitch/features/favourites/index.js (renamed from app/javascript/themes/glitch/features/favourites/index.js)10
-rw-r--r--app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.js (renamed from app/javascript/themes/glitch/features/follow_requests/components/account_authorize.js)8
-rw-r--r--app/javascript/flavours/glitch/features/follow_requests/containers/account_authorize_container.js (renamed from app/javascript/themes/glitch/features/follow_requests/containers/account_authorize_container.js)4
-rw-r--r--app/javascript/flavours/glitch/features/follow_requests/index.js (renamed from app/javascript/themes/glitch/features/follow_requests/index.js)8
-rw-r--r--app/javascript/flavours/glitch/features/followers/index.js (renamed from app/javascript/themes/glitch/features/followers/index.js)14
-rw-r--r--app/javascript/flavours/glitch/features/following/index.js (renamed from app/javascript/themes/glitch/features/following/index.js)14
-rw-r--r--app/javascript/flavours/glitch/features/generic_not_found/index.js11
-rw-r--r--app/javascript/flavours/glitch/features/getting_started/index.js (renamed from app/javascript/themes/glitch/features/getting_started/index.js)10
-rw-r--r--app/javascript/flavours/glitch/features/hashtag_timeline/index.js (renamed from app/javascript/themes/glitch/features/hashtag_timeline/index.js)12
-rw-r--r--app/javascript/flavours/glitch/features/home_timeline/components/column_settings.js (renamed from app/javascript/themes/glitch/features/home_timeline/components/column_settings.js)8
-rw-r--r--app/javascript/flavours/glitch/features/home_timeline/containers/column_settings_container.js (renamed from app/javascript/themes/glitch/features/home_timeline/containers/column_settings_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/home_timeline/index.js (renamed from app/javascript/themes/glitch/features/home_timeline/index.js)10
-rw-r--r--app/javascript/flavours/glitch/features/local_settings/index.js (renamed from app/javascript/themes/glitch/features/local_settings/index.js)7
-rw-r--r--app/javascript/flavours/glitch/features/local_settings/navigation/index.js (renamed from app/javascript/themes/glitch/features/local_settings/navigation/index.js)3
-rw-r--r--app/javascript/flavours/glitch/features/local_settings/navigation/item/index.js (renamed from app/javascript/themes/glitch/features/local_settings/navigation/item/index.js)3
-rw-r--r--app/javascript/flavours/glitch/features/local_settings/page/index.js (renamed from app/javascript/themes/glitch/features/local_settings/page/index.js)3
-rw-r--r--app/javascript/flavours/glitch/features/local_settings/page/item/index.js (renamed from app/javascript/themes/glitch/features/local_settings/page/item/index.js)3
-rw-r--r--app/javascript/flavours/glitch/features/mutes/index.js (renamed from app/javascript/themes/glitch/features/mutes/index.js)10
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/clear_column_button.js (renamed from app/javascript/themes/glitch/features/notifications/components/clear_column_button.js)0
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/column_settings.js (renamed from app/javascript/themes/glitch/features/notifications/components/column_settings.js)0
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/follow.js (renamed from app/javascript/themes/glitch/features/notifications/components/follow.js)4
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/notification.js (renamed from app/javascript/themes/glitch/features/notifications/components/notification.js)2
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/overlay.js (renamed from app/javascript/themes/glitch/features/notifications/components/overlay.js)0
-rw-r--r--app/javascript/flavours/glitch/features/notifications/components/setting_toggle.js (renamed from app/javascript/themes/glitch/features/notifications/components/setting_toggle.js)0
-rw-r--r--app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js (renamed from app/javascript/themes/glitch/features/notifications/containers/column_settings_container.js)8
-rw-r--r--app/javascript/flavours/glitch/features/notifications/containers/notification_container.js (renamed from app/javascript/themes/glitch/features/notifications/containers/notification_container.js)4
-rw-r--r--app/javascript/flavours/glitch/features/notifications/containers/overlay_container.js (renamed from app/javascript/themes/glitch/features/notifications/containers/overlay_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/notifications/index.js (renamed from app/javascript/themes/glitch/features/notifications/index.js)10
-rw-r--r--app/javascript/flavours/glitch/features/pinned_statuses/index.js (renamed from app/javascript/themes/glitch/features/pinned_statuses/index.js)8
-rw-r--r--app/javascript/flavours/glitch/features/public_timeline/containers/column_settings_container.js (renamed from app/javascript/themes/glitch/features/public_timeline/containers/column_settings_container.js)4
-rw-r--r--app/javascript/flavours/glitch/features/public_timeline/index.js (renamed from app/javascript/themes/glitch/features/public_timeline/index.js)12
-rw-r--r--app/javascript/flavours/glitch/features/reblogs/index.js (renamed from app/javascript/themes/glitch/features/reblogs/index.js)10
-rw-r--r--app/javascript/flavours/glitch/features/report/components/status_check_box.js (renamed from app/javascript/themes/glitch/features/report/components/status_check_box.js)0
-rw-r--r--app/javascript/flavours/glitch/features/report/containers/status_check_box_container.js (renamed from app/javascript/themes/glitch/features/report/containers/status_check_box_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/standalone/compose/index.js20
-rw-r--r--app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js (renamed from app/javascript/themes/glitch/features/standalone/hashtag_timeline/index.js)8
-rw-r--r--app/javascript/flavours/glitch/features/standalone/public_timeline/index.js (renamed from app/javascript/themes/glitch/features/standalone/public_timeline/index.js)8
-rw-r--r--app/javascript/flavours/glitch/features/status/components/action_bar.js (renamed from app/javascript/themes/glitch/features/status/components/action_bar.js)6
-rw-r--r--app/javascript/flavours/glitch/features/status/components/card.js (renamed from app/javascript/themes/glitch/features/status/components/card.js)0
-rw-r--r--app/javascript/flavours/glitch/features/status/components/detailed_status.js (renamed from app/javascript/themes/glitch/features/status/components/detailed_status.js)14
-rw-r--r--app/javascript/flavours/glitch/features/status/containers/card_container.js (renamed from app/javascript/themes/glitch/features/status/containers/card_container.js)0
-rw-r--r--app/javascript/flavours/glitch/features/status/index.js (renamed from app/javascript/themes/glitch/features/status/index.js)26
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/actions_modal.js (renamed from app/javascript/themes/glitch/features/ui/components/actions_modal.js)10
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/boost_modal.js (renamed from app/javascript/themes/glitch/features/ui/components/boost_modal.js)10
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/bundle.js (renamed from app/javascript/themes/glitch/features/ui/components/bundle.js)0
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/bundle_column_error.js (renamed from app/javascript/themes/glitch/features/ui/components/bundle_column_error.js)4
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/bundle_modal_error.js (renamed from app/javascript/themes/glitch/features/ui/components/bundle_modal_error.js)2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/column.js (renamed from app/javascript/themes/glitch/features/ui/components/column.js)4
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/column_header.js (renamed from app/javascript/themes/glitch/features/ui/components/column_header.js)0
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/column_link.js (renamed from app/javascript/themes/glitch/features/ui/components/column_link.js)0
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/column_loading.js (renamed from app/javascript/themes/glitch/features/ui/components/column_loading.js)4
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/column_subheading.js (renamed from app/javascript/themes/glitch/features/ui/components/column_subheading.js)0
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/columns_area.js (renamed from app/javascript/themes/glitch/features/ui/components/columns_area.js)4
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/confirmation_modal.js (renamed from app/javascript/themes/glitch/features/ui/components/confirmation_modal.js)2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/doodle_modal.js (renamed from app/javascript/themes/glitch/features/ui/components/doodle_modal.js)6
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/drawer_loading.js (renamed from app/javascript/themes/glitch/features/ui/components/drawer_loading.js)0
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/embed_modal.js (renamed from app/javascript/themes/glitch/features/ui/components/embed_modal.js)0
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/image_loader.js (renamed from app/javascript/themes/glitch/features/ui/components/image_loader.js)0
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/media_modal.js (renamed from app/javascript/themes/glitch/features/ui/components/media_modal.js)4
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/modal_loading.js (renamed from app/javascript/themes/glitch/features/ui/components/modal_loading.js)2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/modal_root.js (renamed from app/javascript/themes/glitch/features/ui/components/modal_root.js)2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/mute_modal.js (renamed from app/javascript/themes/glitch/features/ui/components/mute_modal.js)8
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/onboarding_modal.js (renamed from app/javascript/themes/glitch/features/ui/components/onboarding_modal.js)10
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/report_modal.js (renamed from app/javascript/themes/glitch/features/ui/components/report_modal.js)10
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/tabs_bar.js (renamed from app/javascript/themes/glitch/features/ui/components/tabs_bar.js)2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/upload_area.js (renamed from app/javascript/themes/glitch/features/ui/components/upload_area.js)2
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/video_modal.js (renamed from app/javascript/themes/glitch/features/ui/components/video_modal.js)2
-rw-r--r--app/javascript/flavours/glitch/features/ui/containers/bundle_container.js (renamed from app/javascript/themes/glitch/features/ui/containers/bundle_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/ui/containers/columns_area_container.js (renamed from app/javascript/themes/glitch/features/ui/containers/columns_area_container.js)0
-rw-r--r--app/javascript/flavours/glitch/features/ui/containers/loading_bar_container.js (renamed from app/javascript/themes/glitch/features/ui/containers/loading_bar_container.js)0
-rw-r--r--app/javascript/flavours/glitch/features/ui/containers/modal_container.js (renamed from app/javascript/themes/glitch/features/ui/containers/modal_container.js)2
-rw-r--r--app/javascript/flavours/glitch/features/ui/containers/notifications_container.js (renamed from app/javascript/themes/glitch/features/ui/containers/notifications_container.js)4
-rw-r--r--app/javascript/flavours/glitch/features/ui/containers/status_list_container.js (renamed from app/javascript/themes/glitch/features/ui/containers/status_list_container.js)10
-rw-r--r--app/javascript/flavours/glitch/features/ui/index.js (renamed from app/javascript/themes/glitch/features/ui/index.js)16
-rw-r--r--app/javascript/flavours/glitch/features/video/index.js (renamed from app/javascript/themes/glitch/features/video/index.js)2
-rw-r--r--app/javascript/flavours/glitch/middleware/errors.js (renamed from app/javascript/themes/glitch/middleware/errors.js)2
-rw-r--r--app/javascript/flavours/glitch/middleware/loading_bar.js (renamed from app/javascript/themes/glitch/middleware/loading_bar.js)0
-rw-r--r--app/javascript/flavours/glitch/middleware/sounds.js (renamed from app/javascript/themes/glitch/middleware/sounds.js)0
-rw-r--r--app/javascript/flavours/glitch/packs/about.js22
-rw-r--r--app/javascript/flavours/glitch/packs/common.js1
-rw-r--r--app/javascript/flavours/glitch/packs/home.js7
-rw-r--r--app/javascript/flavours/glitch/packs/public.js75
-rw-r--r--app/javascript/flavours/glitch/packs/share.js22
-rw-r--r--app/javascript/flavours/glitch/reducers/accounts.js (renamed from app/javascript/themes/glitch/reducers/accounts.js)24
-rw-r--r--app/javascript/flavours/glitch/reducers/accounts_counters.js (renamed from app/javascript/themes/glitch/reducers/accounts_counters.js)22
-rw-r--r--app/javascript/flavours/glitch/reducers/alerts.js (renamed from app/javascript/themes/glitch/reducers/alerts.js)2
-rw-r--r--app/javascript/flavours/glitch/reducers/cards.js (renamed from app/javascript/themes/glitch/reducers/cards.js)2
-rw-r--r--app/javascript/flavours/glitch/reducers/compose.js (renamed from app/javascript/themes/glitch/reducers/compose.js)10
-rw-r--r--app/javascript/flavours/glitch/reducers/contexts.js (renamed from app/javascript/themes/glitch/reducers/contexts.js)4
-rw-r--r--app/javascript/flavours/glitch/reducers/custom_emojis.js (renamed from app/javascript/themes/glitch/reducers/custom_emojis.js)6
-rw-r--r--app/javascript/flavours/glitch/reducers/height_cache.js (renamed from app/javascript/themes/glitch/reducers/height_cache.js)2
-rw-r--r--app/javascript/flavours/glitch/reducers/index.js (renamed from app/javascript/themes/glitch/reducers/index.js)0
-rw-r--r--app/javascript/flavours/glitch/reducers/local_settings.js (renamed from app/javascript/themes/glitch/reducers/local_settings.js)4
-rw-r--r--app/javascript/flavours/glitch/reducers/media_attachments.js (renamed from app/javascript/themes/glitch/reducers/media_attachments.js)2
-rw-r--r--app/javascript/flavours/glitch/reducers/meta.js (renamed from app/javascript/themes/glitch/reducers/meta.js)2
-rw-r--r--app/javascript/flavours/glitch/reducers/modal.js (renamed from app/javascript/themes/glitch/reducers/modal.js)2
-rw-r--r--app/javascript/flavours/glitch/reducers/mutes.js (renamed from app/javascript/themes/glitch/reducers/mutes.js)2
-rw-r--r--app/javascript/flavours/glitch/reducers/notifications.js (renamed from app/javascript/themes/glitch/reducers/notifications.js)6
-rw-r--r--app/javascript/flavours/glitch/reducers/push_notifications.js (renamed from app/javascript/themes/glitch/reducers/push_notifications.js)4
-rw-r--r--app/javascript/flavours/glitch/reducers/relationships.js (renamed from app/javascript/themes/glitch/reducers/relationships.js)4
-rw-r--r--app/javascript/flavours/glitch/reducers/reports.js (renamed from app/javascript/themes/glitch/reducers/reports.js)2
-rw-r--r--app/javascript/flavours/glitch/reducers/search.js (renamed from app/javascript/themes/glitch/reducers/search.js)4
-rw-r--r--app/javascript/flavours/glitch/reducers/settings.js (renamed from app/javascript/themes/glitch/reducers/settings.js)11
-rw-r--r--app/javascript/flavours/glitch/reducers/status_lists.js (renamed from app/javascript/themes/glitch/reducers/status_lists.js)6
-rw-r--r--app/javascript/flavours/glitch/reducers/statuses.js (renamed from app/javascript/themes/glitch/reducers/statuses.js)18
-rw-r--r--app/javascript/flavours/glitch/reducers/timelines.js (renamed from app/javascript/themes/glitch/reducers/timelines.js)4
-rw-r--r--app/javascript/flavours/glitch/reducers/user_lists.js (renamed from app/javascript/themes/glitch/reducers/user_lists.js)8
-rw-r--r--app/javascript/flavours/glitch/selectors/index.js (renamed from app/javascript/themes/glitch/selectors/index.js)0
-rw-r--r--app/javascript/flavours/glitch/service_worker/entry.js (renamed from app/javascript/themes/glitch/service_worker/entry.js)0
-rw-r--r--app/javascript/flavours/glitch/service_worker/web_push_notifications.js (renamed from app/javascript/themes/glitch/service_worker/web_push_notifications.js)0
-rw-r--r--app/javascript/flavours/glitch/store/configureStore.js (renamed from app/javascript/themes/glitch/store/configureStore.js)0
-rw-r--r--app/javascript/flavours/glitch/styles/_mixins.scss (renamed from app/javascript/themes/glitch/styles/_mixins.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/about.scss (renamed from app/javascript/themes/glitch/styles/about.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/accounts.scss (renamed from app/javascript/themes/glitch/styles/accounts.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/admin.scss (renamed from app/javascript/themes/glitch/styles/admin.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/basics.scss (renamed from app/javascript/themes/glitch/styles/basics.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/compact_header.scss (renamed from app/javascript/themes/glitch/styles/compact_header.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/components/boost.scss (renamed from app/javascript/themes/glitch/styles/boost.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/components/doodle.scss (renamed from app/javascript/themes/glitch/styles/doodle.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/components/emoji_picker.scss (renamed from app/javascript/themes/glitch/styles/emoji_picker.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/components/index.scss (renamed from app/javascript/themes/glitch/styles/components.scss)4
-rw-r--r--app/javascript/flavours/glitch/styles/components/local_settings.scss81
-rw-r--r--app/javascript/flavours/glitch/styles/containers.scss (renamed from app/javascript/themes/glitch/styles/containers.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/footer.scss (renamed from app/javascript/themes/glitch/styles/footer.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/forms.scss (renamed from app/javascript/themes/glitch/styles/forms.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/index.scss (renamed from app/javascript/themes/glitch/styles/index.scss)3
-rw-r--r--app/javascript/flavours/glitch/styles/landing_strip.scss (renamed from app/javascript/themes/glitch/styles/landing_strip.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/lists.scss (renamed from app/javascript/themes/glitch/styles/lists.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/reset.scss (renamed from app/javascript/themes/glitch/styles/reset.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/rtl.scss (renamed from app/javascript/themes/glitch/styles/rtl.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/stream_entries.scss (renamed from app/javascript/themes/glitch/styles/stream_entries.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/tables.scss (renamed from app/javascript/themes/glitch/styles/tables.scss)0
-rw-r--r--app/javascript/flavours/glitch/styles/variables.scss (renamed from app/javascript/themes/glitch/styles/variables.scss)0
-rw-r--r--app/javascript/flavours/glitch/theme.yml33
-rw-r--r--app/javascript/flavours/glitch/util/api.js (renamed from app/javascript/themes/glitch/util/api.js)0
-rw-r--r--app/javascript/flavours/glitch/util/async-components.js115
-rw-r--r--app/javascript/flavours/glitch/util/base_polyfills.js (renamed from app/javascript/themes/glitch/util/base_polyfills.js)0
-rw-r--r--app/javascript/flavours/glitch/util/bio_metadata.js (renamed from app/javascript/themes/glitch/util/bio_metadata.js)0
-rw-r--r--app/javascript/flavours/glitch/util/counter.js (renamed from app/javascript/themes/glitch/util/counter.js)0
-rw-r--r--app/javascript/flavours/glitch/util/emoji/emoji_compressed.js (renamed from app/javascript/themes/glitch/util/emoji/emoji_compressed.js)0
-rw-r--r--app/javascript/flavours/glitch/util/emoji/emoji_map.json (renamed from app/javascript/themes/glitch/util/emoji/emoji_map.json)0
-rw-r--r--app/javascript/flavours/glitch/util/emoji/emoji_mart_data_light.js (renamed from app/javascript/themes/glitch/util/emoji/emoji_mart_data_light.js)0
-rw-r--r--app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js (renamed from app/javascript/themes/glitch/util/emoji/emoji_mart_search_light.js)0
-rw-r--r--app/javascript/flavours/glitch/util/emoji/emoji_picker.js (renamed from app/javascript/themes/glitch/util/emoji/emoji_picker.js)0
-rw-r--r--app/javascript/flavours/glitch/util/emoji/emoji_unicode_mapping_light.js (renamed from app/javascript/themes/glitch/util/emoji/emoji_unicode_mapping_light.js)0
-rw-r--r--app/javascript/flavours/glitch/util/emoji/emoji_utils.js (renamed from app/javascript/themes/glitch/util/emoji/emoji_utils.js)0
-rw-r--r--app/javascript/flavours/glitch/util/emoji/index.js (renamed from app/javascript/themes/glitch/util/emoji/index.js)2
-rw-r--r--app/javascript/flavours/glitch/util/emoji/unicode_to_filename.js (renamed from app/javascript/themes/glitch/util/emoji/unicode_to_filename.js)0
-rw-r--r--app/javascript/flavours/glitch/util/emoji/unicode_to_unified_name.js (renamed from app/javascript/themes/glitch/util/emoji/unicode_to_unified_name.js)0
-rw-r--r--app/javascript/flavours/glitch/util/extra_polyfills.js (renamed from app/javascript/themes/glitch/util/extra_polyfills.js)0
-rw-r--r--app/javascript/flavours/glitch/util/fullscreen.js (renamed from app/javascript/themes/glitch/util/fullscreen.js)0
-rw-r--r--app/javascript/flavours/glitch/util/get_rect_from_entry.js (renamed from app/javascript/themes/glitch/util/get_rect_from_entry.js)0
-rw-r--r--app/javascript/flavours/glitch/util/initial_state.js (renamed from app/javascript/themes/glitch/util/initial_state.js)0
-rw-r--r--app/javascript/flavours/glitch/util/intersection_observer_wrapper.js (renamed from app/javascript/themes/glitch/util/intersection_observer_wrapper.js)0
-rw-r--r--app/javascript/flavours/glitch/util/is_mobile.js (renamed from app/javascript/themes/glitch/util/is_mobile.js)0
-rw-r--r--app/javascript/flavours/glitch/util/link_header.js (renamed from app/javascript/themes/glitch/util/link_header.js)0
-rw-r--r--app/javascript/flavours/glitch/util/load_polyfills.js (renamed from app/javascript/themes/glitch/util/load_polyfills.js)0
-rw-r--r--app/javascript/flavours/glitch/util/main.js (renamed from app/javascript/themes/glitch/util/main.js)2
-rw-r--r--app/javascript/flavours/glitch/util/optional_motion.js (renamed from app/javascript/themes/glitch/util/optional_motion.js)2
-rw-r--r--app/javascript/flavours/glitch/util/performance.js (renamed from app/javascript/themes/glitch/util/performance.js)0
-rw-r--r--app/javascript/flavours/glitch/util/react_router_helpers.js (renamed from app/javascript/themes/glitch/util/react_router_helpers.js)6
-rw-r--r--app/javascript/flavours/glitch/util/ready.js (renamed from app/javascript/themes/glitch/util/ready.js)0
-rw-r--r--app/javascript/flavours/glitch/util/reduced_motion.js (renamed from app/javascript/themes/glitch/util/reduced_motion.js)0
-rw-r--r--app/javascript/flavours/glitch/util/rtl.js (renamed from app/javascript/themes/glitch/util/rtl.js)0
-rw-r--r--app/javascript/flavours/glitch/util/schedule_idle_task.js (renamed from app/javascript/themes/glitch/util/schedule_idle_task.js)0
-rw-r--r--app/javascript/flavours/glitch/util/scroll.js (renamed from app/javascript/themes/glitch/util/scroll.js)0
-rw-r--r--app/javascript/flavours/glitch/util/stream.js (renamed from app/javascript/themes/glitch/util/stream.js)0
-rw-r--r--app/javascript/flavours/glitch/util/url_regex.js (renamed from app/javascript/themes/glitch/util/url_regex.js)0
-rw-r--r--app/javascript/flavours/glitch/util/uuid.js (renamed from app/javascript/themes/glitch/util/uuid.js)0
-rw-r--r--app/javascript/flavours/glitch/util/web_push_subscription.js (renamed from app/javascript/themes/glitch/util/web_push_subscription.js)4
-rw-r--r--app/javascript/flavours/vanilla/theme.yml33
-rw-r--r--app/javascript/fonts/premillenium/MSSansSerif.ttfbin0 -> 626300 bytes
-rw-r--r--app/javascript/glitch/locales/en.json2
-rw-r--r--app/javascript/images/icon_about.pngbin0 -> 497 bytes
-rw-r--r--app/javascript/images/icon_blocks.pngbin0 -> 356 bytes
-rw-r--r--app/javascript/images/icon_home.pngbin0 -> 328 bytes
-rw-r--r--app/javascript/images/icon_likes.pngbin0 -> 326 bytes
-rw-r--r--app/javascript/images/icon_local.pngbin0 -> 599 bytes
-rw-r--r--app/javascript/images/icon_logout.pngbin0 -> 383 bytes
-rw-r--r--app/javascript/images/icon_mutes.pngbin0 -> 411 bytes
-rw-r--r--app/javascript/images/icon_pin.pngbin0 -> 337 bytes
-rw-r--r--app/javascript/images/icon_public.pngbin0 -> 688 bytes
-rw-r--r--app/javascript/images/icon_settings.pngbin0 -> 639 bytes
-rw-r--r--app/javascript/images/start.pngbin0 -> 263 bytes
-rw-r--r--app/javascript/locales/index.js9
-rw-r--r--app/javascript/mastodon/actions/lists.js289
-rw-r--r--app/javascript/mastodon/components/account.js12
-rw-r--r--app/javascript/mastodon/components/autosuggest_textarea.js1
-rw-r--r--app/javascript/mastodon/features/account/components/action_bar.js5
-rw-r--r--app/javascript/mastodon/features/account_timeline/containers/header_container.js2
-rw-r--r--app/javascript/mastodon/features/getting_started/index.js4
-rw-r--r--app/javascript/mastodon/features/list_editor/components/account.js77
-rw-r--r--app/javascript/mastodon/features/list_editor/components/search.js75
-rw-r--r--app/javascript/mastodon/features/list_editor/index.js80
-rw-r--r--app/javascript/mastodon/features/list_timeline/index.js72
-rw-r--r--app/javascript/mastodon/features/lists/components/new_list_form.js78
-rw-r--r--app/javascript/mastodon/features/lists/index.js76
-rw-r--r--app/javascript/mastodon/features/status/components/card.js2
-rw-r--r--app/javascript/mastodon/features/ui/components/modal_root.js2
-rw-r--r--app/javascript/mastodon/features/ui/index.js2
-rw-r--r--app/javascript/mastodon/features/ui/util/async-components.js8
-rw-r--r--app/javascript/mastodon/locales/ar.json56
-rw-r--r--app/javascript/mastodon/locales/bg.json12
-rw-r--r--app/javascript/mastodon/locales/ca.json52
-rw-r--r--app/javascript/mastodon/locales/de.json54
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json68
-rw-r--r--app/javascript/mastodon/locales/en.json12
-rw-r--r--app/javascript/mastodon/locales/eo.json12
-rw-r--r--app/javascript/mastodon/locales/es.json82
-rw-r--r--app/javascript/mastodon/locales/fa.json12
-rw-r--r--app/javascript/mastodon/locales/fi.json12
-rw-r--r--app/javascript/mastodon/locales/fr.json44
-rw-r--r--app/javascript/mastodon/locales/he.json156
-rw-r--r--app/javascript/mastodon/locales/hr.json12
-rw-r--r--app/javascript/mastodon/locales/hu.json12
-rw-r--r--app/javascript/mastodon/locales/id.json12
-rw-r--r--app/javascript/mastodon/locales/index.js10
-rw-r--r--app/javascript/mastodon/locales/io.json12
-rw-r--r--app/javascript/mastodon/locales/it.json12
-rw-r--r--app/javascript/mastodon/locales/ja.json30
-rw-r--r--app/javascript/mastodon/locales/ko.json12
-rw-r--r--app/javascript/mastodon/locales/nl.json68
-rw-r--r--app/javascript/mastodon/locales/no.json84
-rw-r--r--app/javascript/mastodon/locales/oc.json58
-rw-r--r--app/javascript/mastodon/locales/pl.json12
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json38
-rw-r--r--app/javascript/mastodon/locales/pt.json248
-rw-r--r--app/javascript/mastodon/locales/ru.json12
-rw-r--r--app/javascript/mastodon/locales/sv.json12
-rw-r--r--app/javascript/mastodon/locales/th.json12
-rw-r--r--app/javascript/mastodon/locales/tr.json12
-rw-r--r--app/javascript/mastodon/locales/uk.json12
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json52
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json12
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json12
-rw-r--r--app/javascript/mastodon/reducers/accounts.js6
-rw-r--r--app/javascript/mastodon/reducers/accounts_counters.js6
-rw-r--r--app/javascript/mastodon/reducers/index.js2
-rw-r--r--app/javascript/mastodon/reducers/list_editor.js89
-rw-r--r--app/javascript/mastodon/reducers/lists.js24
-rw-r--r--app/javascript/mastodon/reducers/settings.js7
-rw-r--r--app/javascript/packs/about.js8
-rw-r--r--app/javascript/packs/application.js11
-rw-r--r--app/javascript/packs/common.js7
-rw-r--r--app/javascript/packs/public.js88
-rw-r--r--app/javascript/packs/share.js8
-rw-r--r--app/javascript/skins/vanilla/win95.scss1
-rw-r--r--app/javascript/styles/common.scss5
-rw-r--r--app/javascript/styles/mastodon/about.scss2
-rw-r--r--app/javascript/styles/mastodon/accounts.scss6
-rw-r--r--app/javascript/styles/mastodon/admin.scss18
-rw-r--r--app/javascript/styles/mastodon/components.scss163
-rw-r--r--app/javascript/styles/mastodon/emoji_picker.scss5
-rw-r--r--app/javascript/styles/mastodon/forms.scss30
-rw-r--r--app/javascript/styles/mastodon/landing_strip.scss12
-rw-r--r--app/javascript/styles/mastodon/stream_entries.scss12
-rw-r--r--app/javascript/styles/mastodon/tables.scss6
-rw-r--r--app/javascript/styles/mastodon/variables.scss3
-rw-r--r--app/javascript/styles/win95.scss1655
-rw-r--r--app/javascript/themes/glitch/features/generic_not_found/index.js11
-rw-r--r--app/javascript/themes/glitch/features/local_settings/navigation/item/style.scss27
-rw-r--r--app/javascript/themes/glitch/features/local_settings/navigation/style.scss10
-rw-r--r--app/javascript/themes/glitch/features/local_settings/page/item/style.scss7
-rw-r--r--app/javascript/themes/glitch/features/local_settings/page/style.scss9
-rw-r--r--app/javascript/themes/glitch/features/local_settings/style.scss34
-rw-r--r--app/javascript/themes/glitch/features/standalone/compose/index.js20
-rw-r--r--app/javascript/themes/glitch/index.js14
-rw-r--r--app/javascript/themes/glitch/styles/reset copy.scss91
-rw-r--r--app/javascript/themes/glitch/theme.yml18
-rw-r--r--app/javascript/themes/glitch/util/async-components.js115
m---------app/javascript/themes/mastodon-go0
-rw-r--r--app/javascript/themes/vanilla/theme.yml18
-rw-r--r--app/lib/activitypub/activity.rb4
-rw-r--r--app/lib/extractor.rb2
-rw-r--r--app/lib/formatter.rb2
-rw-r--r--app/lib/ostatus/activity/base.rb2
-rw-r--r--app/lib/ostatus/atom_serializer.rb2
-rw-r--r--app/lib/provider_discovery.rb2
-rw-r--r--app/lib/request.rb2
-rw-r--r--app/lib/themes.rb43
-rw-r--r--app/lib/user_settings_decorator.rb13
-rw-r--r--app/mailers/notification_mailer.rb2
-rw-r--r--app/mailers/user_mailer.rb6
-rw-r--r--app/models/account.rb68
-rw-r--r--app/models/list.rb19
-rw-r--r--app/models/list_account.rb2
-rw-r--r--app/models/preview_card.rb1
-rw-r--r--app/models/remote_follow.rb4
-rw-r--r--app/models/session_activation.rb12
-rw-r--r--app/models/user.rb2
-rw-r--r--app/presenters/account_relationships_presenter.rb2
-rw-r--r--app/presenters/status_relationships_presenter.rb2
-rw-r--r--app/serializers/rest/preview_card_serializer.rb2
-rw-r--r--app/serializers/rest/relationship_serializer.rb20
-rw-r--r--app/serializers/webfinger_serializer.rb26
-rw-r--r--app/services/account_search_service.rb22
-rw-r--r--app/services/activitypub/process_collection_service.rb2
-rw-r--r--app/services/authorize_follow_service.rb2
-rw-r--r--app/services/fetch_link_card_service.rb9
-rw-r--r--app/services/post_status_service.rb2
-rw-r--r--app/services/process_feed_service.rb2
-rw-r--r--app/services/remove_status_service.rb2
-rw-r--r--app/services/search_service.rb2
-rw-r--r--app/services/suspend_account_service.rb2
-rw-r--r--app/views/about/more.html.haml1
-rw-r--r--app/views/about/show.html.haml1
-rw-r--r--app/views/admin/reports/show.html.haml3
-rw-r--r--app/views/admin/statuses/index.html.haml3
-rw-r--r--app/views/home/index.html.haml6
-rw-r--r--app/views/layouts/_theme.html.haml13
-rw-r--r--app/views/layouts/admin.html.haml3
-rwxr-xr-xapp/views/layouts/application.html.haml10
-rw-r--r--app/views/layouts/auth.html.haml3
-rw-r--r--app/views/layouts/embedded.html.haml8
-rw-r--r--app/views/layouts/error.html.haml4
-rw-r--r--app/views/layouts/modal.html.haml3
-rw-r--r--app/views/layouts/public.html.haml3
-rw-r--r--app/views/settings/preferences/show.html.haml5
-rw-r--r--app/views/shares/show.html.haml1
-rw-r--r--app/views/tags/show.html.haml1
-rw-r--r--app/views/well_known/webfinger/show.json.rabl18
-rw-r--r--app/views/well_known/webfinger/show.xml.ruby4
-rw-r--r--app/workers/scheduler/feed_cleanup_scheduler.rb40
486 files changed, 5181 insertions, 1677 deletions
diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb
index 47690e81e..8785df14e 100644
--- a/app/controllers/about_controller.rb
+++ b/app/controllers/about_controller.rb
@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 class AboutController < ApplicationController
+  before_action :set_pack
   before_action :set_body_classes
   before_action :set_instance_presenter, only: [:show, :more, :terms]
 
@@ -21,6 +22,10 @@ class AboutController < ApplicationController
 
   helper_method :new_user
 
+  def set_pack
+    use_pack action_name == 'show' ? 'about' : 'common'
+  end
+
   def set_instance_presenter
     @instance_presenter = InstancePresenter.new
   end
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index 75915b337..309cb65da 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -7,6 +7,7 @@ class AccountsController < ApplicationController
   def show
     respond_to do |format|
       format.html do
+        use_pack 'public'
         @pinned_statuses = []
 
         if current_account && @account.blocking?(current_account)
diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb
index 7fb69d578..fc299f74c 100644
--- a/app/controllers/admin/base_controller.rb
+++ b/app/controllers/admin/base_controller.rb
@@ -5,8 +5,13 @@ module Admin
     include Authorization
     include AccountableConcern
 
+    layout 'admin'
+
     before_action :require_staff!
+    before_action :set_pack
 
-    layout 'admin'
+    def set_pack
+      use_pack 'admin'
+    end
   end
 end
diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb
index 2a5cac547..11e647c3c 100644
--- a/app/controllers/api/v1/accounts/search_controller.rb
+++ b/app/controllers/api/v1/accounts/search_controller.rb
@@ -17,12 +17,13 @@ class Api::V1::Accounts::SearchController < Api::BaseController
     AccountSearchService.new.call(
       params[:q],
       limit_param(DEFAULT_ACCOUNTS_LIMIT),
-      resolving_search?,
-      current_account
+      current_account,
+      resolve: truthy_param?(:resolve),
+      following: truthy_param?(:following)
     )
   end
 
-  def resolving_search?
-    params[:resolve] == 'true'
+  def truthy_param?(key)
+    params[key] == 'true'
   end
 end
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index b1a2ed573..4e73e9e8b 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -51,7 +51,7 @@ class Api::V1::AccountsController < Api::BaseController
     @account = Account.find(params[:id])
   end
 
-  def relationships(options = {})
+  def relationships(**options)
     AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
   end
 end
diff --git a/app/controllers/api/v1/lists/accounts_controller.rb b/app/controllers/api/v1/lists/accounts_controller.rb
index 40c485e8d..c29c73b3e 100644
--- a/app/controllers/api/v1/lists/accounts_controller.rb
+++ b/app/controllers/api/v1/lists/accounts_controller.rb
@@ -10,7 +10,7 @@ class Api::V1::Lists::AccountsController < Api::BaseController
   after_action :insert_pagination_headers, only: :show
 
   def show
-    @accounts = @list.accounts.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
+    @accounts = load_accounts
     render json: @accounts, each_serializer: REST::AccountSerializer
   end
 
@@ -35,6 +35,14 @@ class Api::V1::Lists::AccountsController < Api::BaseController
     @list = List.where(account: current_account).find(params[:list_id])
   end
 
+  def load_accounts
+    if unlimited?
+      @list.accounts.all
+    else
+      @list.accounts.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
+    end
+  end
+
   def list_accounts
     Account.find(account_ids)
   end
@@ -52,12 +60,16 @@ class Api::V1::Lists::AccountsController < Api::BaseController
   end
 
   def next_path
+    return if unlimited?
+
     if records_continue?
       api_v1_list_accounts_url pagination_params(max_id: pagination_max_id)
     end
   end
 
   def prev_path
+    return if unlimited?
+
     unless @accounts.empty?
       api_v1_list_accounts_url pagination_params(since_id: pagination_since_id)
     end
@@ -78,4 +90,8 @@ class Api::V1::Lists::AccountsController < Api::BaseController
   def pagination_params(core_params)
     params.permit(:limit).merge(core_params)
   end
+
+  def unlimited?
+    params[:limit] == '0'
+  end
 end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index f5dbe837e..c6d148c8c 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -12,8 +12,8 @@ class ApplicationController < ActionController::Base
 
   helper_method :current_account
   helper_method :current_session
-  helper_method :current_theme
-  helper_method :theme_data
+  helper_method :current_flavour
+  helper_method :current_skin
   helper_method :single_user_mode?
 
   rescue_from ActionController::RoutingError, with: :not_found
@@ -54,6 +54,73 @@ class ApplicationController < ActionController::Base
     new_user_session_path
   end
 
+  def pack(data, pack_name, skin = 'default')
+    return nil unless pack?(data, pack_name)
+    pack_data = {
+      common: pack_name == 'common' ? nil : resolve_pack(data['name'] ? Themes.instance.flavour(current_flavour) : Themes.instance.core, 'common', skin),
+      flavour: data['name'],
+      pack: pack_name,
+      preload: nil,
+      skin: nil,
+    }
+    if data['pack'][pack_name].is_a?(Hash)
+      pack_data[:common] = nil if data['pack'][pack_name]['use_common'] == false
+      pack_data[:pack] = nil unless data['pack'][pack_name]['filename']
+      if data['pack'][pack_name]['preload']
+        pack_data[:preload] = [data['pack'][pack_name]['preload']] if data['pack'][pack_name]['preload'].is_a?(String)
+        pack_data[:preload] = data['pack'][pack_name]['preload'] if data['pack'][pack_name]['preload'].is_a?(Array)
+      end
+      if skin != 'default' && data['skin'][skin]
+        pack_data[:skin] = skin if data['skin'][skin].include?(pack_name)
+      else  #  default skin
+        pack_data[:skin] = 'default' if data['pack'][pack_name]['stylesheet']
+      end
+    end
+    pack_data
+  end
+
+  def pack?(data, pack_name)
+    if data['pack'].is_a?(Hash) && data['pack'].key?(pack_name)
+      return true if data['pack'][pack_name].is_a?(String) || data['pack'][pack_name].is_a?(Hash)
+    end
+    false
+  end
+
+  def nil_pack(data, pack_name, skin = 'default')
+    {
+      common: pack_name == 'common' ? nil : resolve_pack(data['name'] ? Themes.instance.flavour(current_flavour) : Themes.instance.core, 'common', skin),
+      flavour: data['name'],
+      pack: nil,
+      preload: nil,
+      skin: nil,
+    }
+  end
+
+  def resolve_pack(data, pack_name, skin = 'default')
+    result = pack(data, pack_name, skin)
+    unless result
+      if data['name'] && data.key?('fallback')
+        if data['fallback'].nil?
+          return nil_pack(data, pack_name, skin)
+        elsif data['fallback'].is_a?(String) && Themes.instance.flavour(data['fallback'])
+          return resolve_pack(Themes.instance.flavour(data['fallback']), pack_name)
+        elsif data['fallback'].is_a?(Array)
+          data['fallback'].each do |fallback|
+            return resolve_pack(Themes.instance.flavour(fallback), pack_name) if Themes.instance.flavour(fallback)
+          end
+        end
+        return nil_pack(data, pack_name, skin)
+      end
+      return data.key?('name') && data['name'] != Setting.default_settings['flavour'] ? resolve_pack(Themes.instance.flavour(Setting.default_settings['flavour']), pack_name) : nil_pack(data, pack_name, skin)
+    end
+    result
+  end
+
+  def use_pack(pack_name)
+    @core = resolve_pack(Themes.instance.core, pack_name)
+    @theme = resolve_pack(Themes.instance.flavour(current_flavour), pack_name, current_skin)
+  end
+
   protected
 
   def forbidden
@@ -84,13 +151,14 @@ class ApplicationController < ActionController::Base
     @current_session ||= SessionActivation.find_by(session_id: cookies.signed['_session_id'])
   end
 
-  def current_theme
-    return Setting.default_settings['theme'] unless Themes.instance.names.include? current_user&.setting_theme
-    current_user.setting_theme
+  def current_flavour
+    return Setting.default_settings['flavour'] unless Themes.instance.flavours.include? current_user&.setting_flavour
+    current_user.setting_flavour
   end
 
-  def theme_data
-    Themes.instance.get(current_theme)
+  def current_skin
+    return 'default' unless Themes.instance.skins_for(current_flavour).include? current_user&.setting_skin
+    current_user.setting_skin
   end
 
   def cache_collection(raw, klass)
diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb
index d5e8e58ed..5ffa1c9a3 100644
--- a/app/controllers/auth/confirmations_controller.rb
+++ b/app/controllers/auth/confirmations_controller.rb
@@ -2,10 +2,17 @@
 
 class Auth::ConfirmationsController < Devise::ConfirmationsController
   layout 'auth'
+  before_action :set_pack
 
   def show
     super do |user|
       BootstrapTimelineWorker.perform_async(user.account_id) if user.errors.empty?
     end
   end
+  
+  private
+
+  def set_pack
+    use_pack 'auth'
+  end
 end
diff --git a/app/controllers/auth/passwords_controller.rb b/app/controllers/auth/passwords_controller.rb
index 171b997dc..e0400aa3d 100644
--- a/app/controllers/auth/passwords_controller.rb
+++ b/app/controllers/auth/passwords_controller.rb
@@ -2,6 +2,7 @@
 
 class Auth::PasswordsController < Devise::PasswordsController
   before_action :check_validity_of_reset_password_token, only: :edit
+  before_action :set_pack
 
   layout 'auth'
 
@@ -17,4 +18,8 @@ class Auth::PasswordsController < Devise::PasswordsController
   def reset_password_token_is_valid?
     resource_class.with_reset_password_token(params[:reset_password_token]).present?
   end
+
+  def set_pack
+    use_pack 'auth'
+  end
 end
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index da0b6512f..f4247fd95 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -5,6 +5,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
 
   before_action :check_enabled_registrations, only: [:new, :create]
   before_action :configure_sign_up_params, only: [:create]
+  before_action :set_pack
   before_action :set_sessions, only: [:edit, :update]
   before_action :set_instance_presenter, only: [:new, :create, :update]
 
@@ -55,6 +56,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
 
   private
 
+  def set_pack
+    use_pack %w(edit update).include?(action_name) ? 'admin' : 'auth'
+  end
+
   def set_instance_presenter
     @instance_presenter = InstancePresenter.new
   end
diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb
index a5acb6c36..72d544102 100644
--- a/app/controllers/auth/sessions_controller.rb
+++ b/app/controllers/auth/sessions_controller.rb
@@ -9,6 +9,7 @@ class Auth::SessionsController < Devise::SessionsController
   skip_before_action :check_suspension, only: [:destroy]
   prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
   before_action :set_instance_presenter, only: [:new]
+  before_action :set_pack
 
   def create
     super do |resource|
@@ -85,6 +86,10 @@ class Auth::SessionsController < Devise::SessionsController
 
   private
 
+  def set_pack
+    use_pack 'auth'
+  end
+
   def set_instance_presenter
     @instance_presenter = InstancePresenter.new
   end
diff --git a/app/controllers/authorize_follows_controller.rb b/app/controllers/authorize_follows_controller.rb
index 78b564183..2d29bd379 100644
--- a/app/controllers/authorize_follows_controller.rb
+++ b/app/controllers/authorize_follows_controller.rb
@@ -4,6 +4,7 @@ class AuthorizeFollowsController < ApplicationController
   layout 'modal'
 
   before_action :authenticate_user!
+  before_action :set_pack
 
   def show
     @account = located_account || render(:error)
@@ -23,6 +24,10 @@ class AuthorizeFollowsController < ApplicationController
 
   private
 
+  def set_pack
+    use_pack 'modal'
+  end
+
   def follow_attempt
     FollowService.new.call(current_account, acct_without_prefix)
   end
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index 399e79665..080cbde11 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -7,7 +7,9 @@ class FollowerAccountsController < ApplicationController
     @follows = Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
 
     respond_to do |format|
-      format.html
+      format.html do
+        use_pack 'public'
+      end
 
       format.json do
         render json: collection_presenter,
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index 1e73d4bd4..74e83ad81 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -7,7 +7,9 @@ class FollowingAccountsController < ApplicationController
     @follows = Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
 
     respond_to do |format|
-      format.html
+      format.html do
+        use_pack 'public'
+      end
 
       format.json do
         render json: collection_presenter,
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 21dde20ce..7437a647e 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -2,6 +2,7 @@
 
 class HomeController < ApplicationController
   before_action :authenticate_user!
+  before_action :set_pack
   before_action :set_initial_state_json
 
   def index
@@ -37,6 +38,10 @@ class HomeController < ApplicationController
     redirect_to(default_redirect_path)
   end
 
+  def set_pack
+    use_pack 'home'
+  end
+
   def set_initial_state_json
     serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
     @initial_state_json   = serializable_resource.to_json
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 38d6c8d73..189e4072e 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -6,6 +6,7 @@ class InvitesController < ApplicationController
   layout 'admin'
 
   before_action :authenticate_user!
+  before_action :set_pack
 
   def index
     authorize :invite, :create?
@@ -37,6 +38,10 @@ class InvitesController < ApplicationController
 
   private
 
+  def set_pack
+    use_pack 'settings'
+  end
+
   def resource_params
     params.require(:invite).permit(:max_uses, :expires_in)
   end
diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb
index 48b026aa5..e6f379886 100644
--- a/app/controllers/remote_follow_controller.rb
+++ b/app/controllers/remote_follow_controller.rb
@@ -4,6 +4,7 @@ class RemoteFollowController < ApplicationController
   layout 'modal'
 
   before_action :set_account
+  before_action :set_pack
   before_action :gone, if: :suspended_account?
 
   def new
@@ -31,6 +32,10 @@ class RemoteFollowController < ApplicationController
     { acct: session[:remote_follow] }
   end
 
+  def set_pack
+    use_pack 'modal'
+  end
+
   def set_account
     @account = Account.find_local!(params[:account_username])
   end
diff --git a/app/controllers/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb
index 8fc9a0fa9..35a6f7f9e 100644
--- a/app/controllers/settings/applications_controller.rb
+++ b/app/controllers/settings/applications_controller.rb
@@ -1,9 +1,7 @@
 # frozen_string_literal: true
 
-class Settings::ApplicationsController < ApplicationController
-  layout 'admin'
+class Settings::ApplicationsController < Settings::BaseController
 
-  before_action :authenticate_user!
   before_action :set_application, only: [:show, :update, :destroy, :regenerate]
   before_action :prepare_scopes, only: [:create, :update]
 
diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb
new file mode 100644
index 000000000..7322d461b
--- /dev/null
+++ b/app/controllers/settings/base_controller.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class Settings::BaseController < ApplicationController
+  layout 'admin'
+
+  before_action :authenticate_user!
+  before_action :set_pack
+
+  def set_pack
+    use_pack 'settings'
+  end
+end
diff --git a/app/controllers/settings/deletes_controller.rb b/app/controllers/settings/deletes_controller.rb
index 80002b995..4c1121471 100644
--- a/app/controllers/settings/deletes_controller.rb
+++ b/app/controllers/settings/deletes_controller.rb
@@ -1,10 +1,8 @@
 # frozen_string_literal: true
 
-class Settings::DeletesController < ApplicationController
-  layout 'admin'
+class Settings::DeletesController < Settings::BaseController
 
-  before_action :check_enabled_deletion
-  before_action :authenticate_user!
+  prepend_before_action :check_enabled_deletion
 
   def show
     @confirmation = Form::DeleteConfirmation.new
diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb
index ae62f00c1..9c03ece86 100644
--- a/app/controllers/settings/exports_controller.rb
+++ b/app/controllers/settings/exports_controller.rb
@@ -1,10 +1,6 @@
 # frozen_string_literal: true
 
-class Settings::ExportsController < ApplicationController
-  layout 'admin'
-
-  before_action :authenticate_user!
-
+class Settings::ExportsController < Settings::BaseController
   def show
     @export = Export.new(current_account)
   end
diff --git a/app/controllers/settings/follower_domains_controller.rb b/app/controllers/settings/follower_domains_controller.rb
index 9968504e5..141b2270d 100644
--- a/app/controllers/settings/follower_domains_controller.rb
+++ b/app/controllers/settings/follower_domains_controller.rb
@@ -2,11 +2,7 @@
 
 require 'sidekiq-bulk'
 
-class Settings::FollowerDomainsController < ApplicationController
-  layout 'admin'
-
-  before_action :authenticate_user!
-
+class Settings::FollowerDomainsController < Settings::BaseController
   def show
     @account = current_account
     @domains = current_account.followers.reorder('MIN(follows.id) DESC').group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10)
diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb
index 0db13d1ca..dbd136ebe 100644
--- a/app/controllers/settings/imports_controller.rb
+++ b/app/controllers/settings/imports_controller.rb
@@ -1,9 +1,6 @@
 # frozen_string_literal: true
 
-class Settings::ImportsController < ApplicationController
-  layout 'admin'
-
-  before_action :authenticate_user!
+class Settings::ImportsController < Settings::BaseController
   before_action :set_account
 
   def show
diff --git a/app/controllers/settings/keyword_mutes_controller.rb b/app/controllers/settings/keyword_mutes_controller.rb
index f79e1b320..699b8a3ef 100644
--- a/app/controllers/settings/keyword_mutes_controller.rb
+++ b/app/controllers/settings/keyword_mutes_controller.rb
@@ -1,9 +1,6 @@
 # frozen_string_literal: true
 
-class Settings::KeywordMutesController < ApplicationController
-  layout 'admin'
-
-  before_action :authenticate_user!
+class Settings::KeywordMutesController < Settings::BaseController
   before_action :load_keyword_mute, only: [:edit, :update, :destroy]
 
   def index
diff --git a/app/controllers/settings/notifications_controller.rb b/app/controllers/settings/notifications_controller.rb
index ce2530c54..6286e3ebf 100644
--- a/app/controllers/settings/notifications_controller.rb
+++ b/app/controllers/settings/notifications_controller.rb
@@ -1,10 +1,6 @@
 # frozen_string_literal: true
 
-class Settings::NotificationsController < ApplicationController
-  layout 'admin'
-
-  before_action :authenticate_user!
-
+class Settings::NotificationsController < Settings::BaseController
   def show; end
 
   def update
diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index 069026715..277f0f657 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -1,10 +1,6 @@
 # frozen_string_literal: true
 
-class Settings::PreferencesController < ApplicationController
-  layout 'admin'
-
-  before_action :authenticate_user!
-
+class Settings::PreferencesController < Settings::BaseController
   def show; end
 
   def update
@@ -42,7 +38,8 @@ class Settings::PreferencesController < ApplicationController
       :setting_reduce_motion,
       :setting_system_font_ui,
       :setting_noindex,
-      :setting_theme,
+      :setting_flavour,
+      :setting_skin,
       notification_emails: %i(follow follow_request reblog favourite mention digest),
       interactions: %i(must_be_follower must_be_following)
     )
diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb
index 28f78a4fb..dadc3d911 100644
--- a/app/controllers/settings/profiles_controller.rb
+++ b/app/controllers/settings/profiles_controller.rb
@@ -1,11 +1,8 @@
 # frozen_string_literal: true
 
-class Settings::ProfilesController < ApplicationController
+class Settings::ProfilesController < Settings::BaseController
   include ObfuscateFilename
 
-  layout 'admin'
-
-  before_action :authenticate_user!
   before_action :set_account
 
   obfuscate_filename [:account, :avatar]
diff --git a/app/controllers/settings/sessions_controller.rb b/app/controllers/settings/sessions_controller.rb
index 0da1b027b..780ea64b4 100644
--- a/app/controllers/settings/sessions_controller.rb
+++ b/app/controllers/settings/sessions_controller.rb
@@ -1,5 +1,6 @@
 # frozen_string_literal: true
 
+#  Intentionally does not inherit from BaseController
 class Settings::SessionsController < ApplicationController
   before_action :set_session, only: :destroy
 
diff --git a/app/controllers/settings/two_factor_authentications_controller.rb b/app/controllers/settings/two_factor_authentications_controller.rb
index 863cc7351..8c7737e9d 100644
--- a/app/controllers/settings/two_factor_authentications_controller.rb
+++ b/app/controllers/settings/two_factor_authentications_controller.rb
@@ -1,10 +1,7 @@
 # frozen_string_literal: true
 
 module Settings
-  class TwoFactorAuthenticationsController < ApplicationController
-    layout 'admin'
-
-    before_action :authenticate_user!
+  class TwoFactorAuthenticationsController < BaseController
     before_action :verify_otp_required, only: [:create]
 
     def show
diff --git a/app/controllers/shares_controller.rb b/app/controllers/shares_controller.rb
index 994742c3d..81d279c8b 100644
--- a/app/controllers/shares_controller.rb
+++ b/app/controllers/shares_controller.rb
@@ -4,6 +4,7 @@ class SharesController < ApplicationController
   layout 'modal'
 
   before_action :authenticate_user!
+  before_action :set_pack
   before_action :set_body_classes
 
   def show
@@ -24,6 +25,10 @@ class SharesController < ApplicationController
     }
   end
 
+  def set_pack
+    use_pack 'share'
+  end
+
   def set_body_classes
     @body_classes = 'compose-standalone'
   end
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index e8a360fb5..84c9e7685 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -14,6 +14,7 @@ class StatusesController < ApplicationController
   def show
     respond_to do |format|
       format.html do
+        use_pack 'public'
         @ancestors   = @status.reply? ? cache_collection(@status.ancestors(current_account), Status) : []
         @descendants = cache_collection(@status.descendants(current_account), Status)
 
@@ -37,6 +38,7 @@ class StatusesController < ApplicationController
   end
 
   def embed
+    use_pack 'embed'
     response.headers['X-Frame-Options'] = 'ALLOWALL'
     render 'stream_entries/embed', layout: 'embedded'
   end
diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb
index 5f61e2182..b597ba4bb 100644
--- a/app/controllers/stream_entries_controller.rb
+++ b/app/controllers/stream_entries_controller.rb
@@ -14,6 +14,7 @@ class StreamEntriesController < ApplicationController
   def show
     respond_to do |format|
       format.html do
+        use_pack 'public'
         @ancestors   = @stream_entry.activity.reply? ? cache_collection(@stream_entry.activity.ancestors(current_account), Status) : []
         @descendants = cache_collection(@stream_entry.activity.descendants(current_account), Status)
       end
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index 9f3090e37..5d11a8139 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -9,6 +9,7 @@ class TagsController < ApplicationController
 
     respond_to do |format|
       format.html do
+        use_pack 'about'
         serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
         @initial_state_json   = serializable_resource.to_json
       end
diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb
index 1c27b2b18..5cc606808 100644
--- a/app/controllers/well_known/webfinger_controller.rb
+++ b/app/controllers/well_known/webfinger_controller.rb
@@ -6,12 +6,10 @@ module WellKnown
 
     def show
       @account = Account.find_local!(username_from_resource)
-      @canonical_account_uri = @account.to_webfinger_s
-      @magic_key = pem_to_magic_key(@account.keypair.public_key)
 
       respond_to do |format|
         format.any(:json, :html) do
-          render formats: :json, content_type: 'application/jrd+json'
+          render json: @account, serializer: WebfingerSerializer, content_type: 'application/jrd+json'
         end
 
         format.xml do
@@ -35,21 +33,6 @@ module WellKnown
       WebfingerResource.new(resource_user).username
     end
 
-    def pem_to_magic_key(public_key)
-      modulus, exponent = [public_key.n, public_key.e].map do |component|
-        result = []
-
-        until component.zero?
-          result << [component % 256].pack('C')
-          component >>= 8
-        end
-
-        result.reverse.join
-      end
-
-      (['RSA'] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.')
-    end
-
     def resource_param
       params.require(:resource)
     end
diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb
index 73250cbf5..9443934b3 100644
--- a/app/helpers/admin/filter_helper.rb
+++ b/app/helpers/admin/filter_helper.rb
@@ -13,7 +13,7 @@ module Admin::FilterHelper
     link_to text, new_url, class: filter_link_class(new_class)
   end
 
-  def table_link_to(icon, text, path, options = {})
+  def table_link_to(icon, text, path, **options)
     link_to safe_join([fa_icon(icon), text]), path, options.merge(class: 'table-action-link')
   end
 
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 7dfab1df1..8ed5c8bda 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -5,7 +5,7 @@ module ApplicationHelper
     current_page?(path) ? 'active' : ''
   end
 
-  def active_link_to(label, path, options = {})
+  def active_link_to(label, path, **options)
     link_to label, path, options.merge(class: active_nav_class(path))
   end
 
diff --git a/app/helpers/routing_helper.rb b/app/helpers/routing_helper.rb
index f4693358c..11894a895 100644
--- a/app/helpers/routing_helper.rb
+++ b/app/helpers/routing_helper.rb
@@ -11,7 +11,7 @@ module RoutingHelper
     end
   end
 
-  def full_asset_url(source, options = {})
+  def full_asset_url(source, **options)
     source = ActionController::Base.helpers.asset_url(source, options) unless use_storage?
 
     URI.join(root_url, source).to_s
diff --git a/app/javascript/packs/admin.js b/app/javascript/core/admin.js
index 993827db5..c0bd09bdd 100644
--- a/app/javascript/packs/admin.js
+++ b/app/javascript/core/admin.js
@@ -1,3 +1,5 @@
+//  This file will be loaded on admin pages, regardless of theme.
+
 import { delegate } from 'rails-ujs';
 
 function handleDeleteStatus(event) {
diff --git a/app/javascript/core/common.js b/app/javascript/core/common.js
new file mode 100644
index 000000000..a7073ef0e
--- /dev/null
+++ b/app/javascript/core/common.js
@@ -0,0 +1,8 @@
+//  This file will be loaded on all pages, regardless of theme.
+
+import { start } from 'rails-ujs';
+import 'font-awesome/css/font-awesome.css';
+
+require.context('../images/', true);
+
+start();
diff --git a/app/javascript/core/embed.js b/app/javascript/core/embed.js
new file mode 100644
index 000000000..6146e6592
--- /dev/null
+++ b/app/javascript/core/embed.js
@@ -0,0 +1,23 @@
+//  This file will be loaded on embed pages, regardless of theme.
+
+window.addEventListener('message', e => {
+  const data = e.data || {};
+
+  if (!window.parent || data.type !== 'setHeight') {
+    return;
+  }
+
+  function setEmbedHeight () {
+    window.parent.postMessage({
+      type: 'setHeight',
+      id: data.id,
+      height: document.getElementsByTagName('html')[0].scrollHeight,
+    }, '*');
+  };
+
+  if (['interactive', 'complete'].includes(document.readyState)) {
+    setEmbedHeight();
+  } else {
+    document.addEventListener('DOMContentLoaded', setEmbedHeight);
+  }
+});
diff --git a/app/javascript/core/public.js b/app/javascript/core/public.js
new file mode 100644
index 000000000..47c34a259
--- /dev/null
+++ b/app/javascript/core/public.js
@@ -0,0 +1,25 @@
+//  This file will be loaded on public pages, regardless of theme.
+
+const { delegate } = require('rails-ujs');
+
+delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
+  if (button !== 0) {
+    return true;
+  }
+  window.location.href = target.href;
+  return false;
+});
+
+delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => {
+  const contentEl = target.parentNode.parentNode.querySelector('.e-content');
+
+  if (contentEl.style.display === 'block') {
+    contentEl.style.display = 'none';
+    target.parentNode.style.marginBottom = 0;
+  } else {
+    contentEl.style.display = 'block';
+    target.parentNode.style.marginBottom = null;
+  }
+
+  return false;
+});
diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js
new file mode 100644
index 000000000..ada5fba2b
--- /dev/null
+++ b/app/javascript/core/settings.js
@@ -0,0 +1,43 @@
+//  This file will be loaded on settings pages, regardless of theme.
+
+const { length } = require('stringz');
+const { delegate } = require('rails-ujs');
+
+import { processBio } from 'flavours/glitch/util/bio_metadata';
+
+delegate(document, '.account_display_name', 'input', ({ target }) => {
+  const nameCounter = document.querySelector('.name-counter');
+
+  if (nameCounter) {
+    nameCounter.textContent = 30 - length(target.value);
+  }
+});
+
+delegate(document, '.account_note', 'input', ({ target }) => {
+  const noteCounter = document.querySelector('.note-counter');
+
+  if (noteCounter) {
+    const noteWithoutMetadata = processBio(target.value).text;
+    noteCounter.textContent = 500 - length(noteWithoutMetadata);
+  }
+});
+
+delegate(document, '#account_avatar', 'change', ({ target }) => {
+  const avatar = document.querySelector('.card.compact .avatar img');
+  const [file] = target.files || [];
+  const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
+
+  avatar.src = url;
+});
+
+delegate(document, '#account_header', 'change', ({ target }) => {
+  const header = document.querySelector('.card.compact');
+  const [file] = target.files || [];
+  const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc;
+
+  header.style.backgroundImage = `url(${url})`;
+});
+
+delegate(document, '#user_setting_flavour, #user_setting_skin', 'change', ({ target }) => {
+  target.form.submit();
+});
diff --git a/app/javascript/core/theme.yml b/app/javascript/core/theme.yml
new file mode 100644
index 000000000..0dc05a149
--- /dev/null
+++ b/app/javascript/core/theme.yml
@@ -0,0 +1,16 @@
+#  These packs will be loaded on every appropriate page, regardless of
+#  theme.
+pack:
+  about:
+  admin: admin.js
+  auth:
+  common:
+    filename: common.js
+    stylesheet: true
+  embed: embed.js
+  error:
+  home:
+  modal:
+  public: public.js
+  settings: settings.js
+  share:
diff --git a/app/javascript/themes/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js
index f1a8c5471..8ab92f9e7 100644
--- a/app/javascript/themes/glitch/actions/accounts.js
+++ b/app/javascript/flavours/glitch/actions/accounts.js
@@ -1,4 +1,4 @@
-import api, { getLinks } from 'themes/glitch/util/api';
+import api, { getLinks } from 'flavours/glitch/util/api';
 
 export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
 export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
diff --git a/app/javascript/themes/glitch/actions/alerts.js b/app/javascript/flavours/glitch/actions/alerts.js
index f37fdeeb6..f37fdeeb6 100644
--- a/app/javascript/themes/glitch/actions/alerts.js
+++ b/app/javascript/flavours/glitch/actions/alerts.js
diff --git a/app/javascript/themes/glitch/actions/blocks.js b/app/javascript/flavours/glitch/actions/blocks.js
index 6ba9460f0..fe44ca19a 100644
--- a/app/javascript/themes/glitch/actions/blocks.js
+++ b/app/javascript/flavours/glitch/actions/blocks.js
@@ -1,4 +1,4 @@
-import api, { getLinks } from 'themes/glitch/util/api';
+import api, { getLinks } from 'flavours/glitch/util/api';
 import { fetchRelationships } from './accounts';
 
 export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
diff --git a/app/javascript/themes/glitch/actions/bundles.js b/app/javascript/flavours/glitch/actions/bundles.js
index ecc9c8f7d..ecc9c8f7d 100644
--- a/app/javascript/themes/glitch/actions/bundles.js
+++ b/app/javascript/flavours/glitch/actions/bundles.js
diff --git a/app/javascript/themes/glitch/actions/cards.js b/app/javascript/flavours/glitch/actions/cards.js
index 2a1bc369a..c897daf58 100644
--- a/app/javascript/themes/glitch/actions/cards.js
+++ b/app/javascript/flavours/glitch/actions/cards.js
@@ -1,4 +1,4 @@
-import api from 'themes/glitch/util/api';
+import api from 'flavours/glitch/util/api';
 
 export const STATUS_CARD_FETCH_REQUEST = 'STATUS_CARD_FETCH_REQUEST';
 export const STATUS_CARD_FETCH_SUCCESS = 'STATUS_CARD_FETCH_SUCCESS';
diff --git a/app/javascript/themes/glitch/actions/columns.js b/app/javascript/flavours/glitch/actions/columns.js
index bcb0cdf98..bcb0cdf98 100644
--- a/app/javascript/themes/glitch/actions/columns.js
+++ b/app/javascript/flavours/glitch/actions/columns.js
diff --git a/app/javascript/themes/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js
index 07c469477..32746f27b 100644
--- a/app/javascript/themes/glitch/actions/compose.js
+++ b/app/javascript/flavours/glitch/actions/compose.js
@@ -1,6 +1,6 @@
-import api from 'themes/glitch/util/api';
+import api from 'flavours/glitch/util/api';
 import { throttle } from 'lodash';
-import { search as emojiSearch } from 'themes/glitch/util/emoji/emoji_mart_search_light';
+import { search as emojiSearch } from 'flavours/glitch/util/emoji/emoji_mart_search_light';
 import { useEmoji } from './emojis';
 
 import {
diff --git a/app/javascript/themes/glitch/actions/domain_blocks.js b/app/javascript/flavours/glitch/actions/domain_blocks.js
index 0a880394a..8506df91c 100644
--- a/app/javascript/themes/glitch/actions/domain_blocks.js
+++ b/app/javascript/flavours/glitch/actions/domain_blocks.js
@@ -1,4 +1,4 @@
-import api, { getLinks } from 'themes/glitch/util/api';
+import api, { getLinks } from 'flavours/glitch/util/api';
 
 export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST';
 export const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS';
diff --git a/app/javascript/themes/glitch/actions/emojis.js b/app/javascript/flavours/glitch/actions/emojis.js
index 7cd9d4b7b..7cd9d4b7b 100644
--- a/app/javascript/themes/glitch/actions/emojis.js
+++ b/app/javascript/flavours/glitch/actions/emojis.js
diff --git a/app/javascript/themes/glitch/actions/favourites.js b/app/javascript/flavours/glitch/actions/favourites.js
index e9b3559af..decdcee4f 100644
--- a/app/javascript/themes/glitch/actions/favourites.js
+++ b/app/javascript/flavours/glitch/actions/favourites.js
@@ -1,4 +1,4 @@
-import api, { getLinks } from 'themes/glitch/util/api';
+import api, { getLinks } from 'flavours/glitch/util/api';
 
 export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST';
 export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS';
diff --git a/app/javascript/themes/glitch/actions/height_cache.js b/app/javascript/flavours/glitch/actions/height_cache.js
index 4c752993f..4c752993f 100644
--- a/app/javascript/themes/glitch/actions/height_cache.js
+++ b/app/javascript/flavours/glitch/actions/height_cache.js
diff --git a/app/javascript/themes/glitch/actions/interactions.js b/app/javascript/flavours/glitch/actions/interactions.js
index d61a7ba2a..ceeb2773b 100644
--- a/app/javascript/themes/glitch/actions/interactions.js
+++ b/app/javascript/flavours/glitch/actions/interactions.js
@@ -1,4 +1,4 @@
-import api from 'themes/glitch/util/api';
+import api from 'flavours/glitch/util/api';
 
 export const REBLOG_REQUEST = 'REBLOG_REQUEST';
 export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
diff --git a/app/javascript/themes/glitch/actions/local_settings.js b/app/javascript/flavours/glitch/actions/local_settings.js
index 28660a4e8..28660a4e8 100644
--- a/app/javascript/themes/glitch/actions/local_settings.js
+++ b/app/javascript/flavours/glitch/actions/local_settings.js
diff --git a/app/javascript/themes/glitch/actions/modal.js b/app/javascript/flavours/glitch/actions/modal.js
index 80e15c28e..80e15c28e 100644
--- a/app/javascript/themes/glitch/actions/modal.js
+++ b/app/javascript/flavours/glitch/actions/modal.js
diff --git a/app/javascript/themes/glitch/actions/mutes.js b/app/javascript/flavours/glitch/actions/mutes.js
index bb19e8657..e06130533 100644
--- a/app/javascript/themes/glitch/actions/mutes.js
+++ b/app/javascript/flavours/glitch/actions/mutes.js
@@ -1,6 +1,6 @@
-import api, { getLinks } from 'themes/glitch/util/api';
+import api, { getLinks } from 'flavours/glitch/util/api';
 import { fetchRelationships } from './accounts';
-import { openModal } from 'themes/glitch/actions/modal';
+import { openModal } from 'flavours/glitch/actions/modal';
 
 export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
 export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
diff --git a/app/javascript/themes/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js
index fbf06f7c4..9b9ebf86d 100644
--- a/app/javascript/themes/glitch/actions/notifications.js
+++ b/app/javascript/flavours/glitch/actions/notifications.js
@@ -1,4 +1,4 @@
-import api, { getLinks } from 'themes/glitch/util/api';
+import api, { getLinks } from 'flavours/glitch/util/api';
 import { List as ImmutableList } from 'immutable';
 import IntlMessageFormat from 'intl-messageformat';
 import { fetchRelationships } from './accounts';
diff --git a/app/javascript/themes/glitch/actions/onboarding.js b/app/javascript/flavours/glitch/actions/onboarding.js
index a161c50ef..a161c50ef 100644
--- a/app/javascript/themes/glitch/actions/onboarding.js
+++ b/app/javascript/flavours/glitch/actions/onboarding.js
diff --git a/app/javascript/themes/glitch/actions/pin_statuses.js b/app/javascript/flavours/glitch/actions/pin_statuses.js
index b3e064e58..d3d1a154f 100644
--- a/app/javascript/themes/glitch/actions/pin_statuses.js
+++ b/app/javascript/flavours/glitch/actions/pin_statuses.js
@@ -1,10 +1,10 @@
-import api from 'themes/glitch/util/api';
+import api from 'flavours/glitch/util/api';
 
 export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
 export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
 export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';
 
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 
 export function fetchPinnedStatuses() {
   return (dispatch, getState) => {
diff --git a/app/javascript/themes/glitch/actions/push_notifications.js b/app/javascript/flavours/glitch/actions/push_notifications.js
index 55661d2b0..55661d2b0 100644
--- a/app/javascript/themes/glitch/actions/push_notifications.js
+++ b/app/javascript/flavours/glitch/actions/push_notifications.js
diff --git a/app/javascript/themes/glitch/actions/reports.js b/app/javascript/flavours/glitch/actions/reports.js
index 93f9085b2..ad4fd18a9 100644
--- a/app/javascript/themes/glitch/actions/reports.js
+++ b/app/javascript/flavours/glitch/actions/reports.js
@@ -1,4 +1,4 @@
-import api from 'themes/glitch/util/api';
+import api from 'flavours/glitch/util/api';
 import { openModal, closeModal } from './modal';
 
 export const REPORT_INIT   = 'REPORT_INIT';
diff --git a/app/javascript/themes/glitch/actions/search.js b/app/javascript/flavours/glitch/actions/search.js
index 414e4755e..e86bd848e 100644
--- a/app/javascript/themes/glitch/actions/search.js
+++ b/app/javascript/flavours/glitch/actions/search.js
@@ -1,4 +1,4 @@
-import api from 'themes/glitch/util/api';
+import api from 'flavours/glitch/util/api';
 
 export const SEARCH_CHANGE = 'SEARCH_CHANGE';
 export const SEARCH_CLEAR  = 'SEARCH_CLEAR';
diff --git a/app/javascript/themes/glitch/actions/settings.js b/app/javascript/flavours/glitch/actions/settings.js
index 79adca18c..79adca18c 100644
--- a/app/javascript/themes/glitch/actions/settings.js
+++ b/app/javascript/flavours/glitch/actions/settings.js
diff --git a/app/javascript/themes/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js
index 702f4e9b6..8b49083ac 100644
--- a/app/javascript/themes/glitch/actions/statuses.js
+++ b/app/javascript/flavours/glitch/actions/statuses.js
@@ -1,4 +1,4 @@
-import api from 'themes/glitch/util/api';
+import api from 'flavours/glitch/util/api';
 
 import { deleteFromTimelines } from './timelines';
 import { fetchStatusCard } from './cards';
diff --git a/app/javascript/themes/glitch/actions/store.js b/app/javascript/flavours/glitch/actions/store.js
index a1db0fdd5..a1db0fdd5 100644
--- a/app/javascript/themes/glitch/actions/store.js
+++ b/app/javascript/flavours/glitch/actions/store.js
diff --git a/app/javascript/themes/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js
index ccf6c27d8..595eefa41 100644
--- a/app/javascript/themes/glitch/actions/streaming.js
+++ b/app/javascript/flavours/glitch/actions/streaming.js
@@ -1,4 +1,4 @@
-import { connectStream } from 'themes/glitch/util/stream';
+import { connectStream } from 'flavours/glitch/util/stream';
 import {
   updateTimeline,
   deleteFromTimelines,
diff --git a/app/javascript/themes/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js
index 5ce14fbe9..3fabf3885 100644
--- a/app/javascript/themes/glitch/actions/timelines.js
+++ b/app/javascript/flavours/glitch/actions/timelines.js
@@ -1,4 +1,4 @@
-import api, { getLinks } from 'themes/glitch/util/api';
+import api, { getLinks } from 'flavours/glitch/util/api';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
 export const TIMELINE_UPDATE  = 'TIMELINE_UPDATE';
diff --git a/app/javascript/themes/glitch/components/account.js b/app/javascript/flavours/glitch/components/account.js
index d0ff77050..ac07051db 100644
--- a/app/javascript/themes/glitch/components/account.js
+++ b/app/javascript/flavours/glitch/components/account.js
@@ -7,7 +7,7 @@ import Permalink from './permalink';
 import IconButton from './icon_button';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   follow: { id: 'account.follow', defaultMessage: 'Follow' },
@@ -81,7 +81,7 @@ export default class Account extends ImmutablePureComponent {
         buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
       } else if (muting) {
         let hidingNotificationsButton;
-        if (muting.get('notifications')) {
+        if (account.getIn(['relationship', 'muting_notifications'])) {
           hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />;
         } else {
           hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username')  })} onClick={this.handleMuteNotifications} />;
@@ -93,7 +93,7 @@ export default class Account extends ImmutablePureComponent {
           </div>
         );
       } else {
-        buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following ? true : false} />;
+        buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
       }
     }
 
diff --git a/app/javascript/themes/glitch/components/attachment_list.js b/app/javascript/flavours/glitch/components/attachment_list.js
index b3d00b335..b3d00b335 100644
--- a/app/javascript/themes/glitch/components/attachment_list.js
+++ b/app/javascript/flavours/glitch/components/attachment_list.js
diff --git a/app/javascript/themes/glitch/components/autosuggest_emoji.js b/app/javascript/flavours/glitch/components/autosuggest_emoji.js
index 3c6f915e4..79e113d9c 100644
--- a/app/javascript/themes/glitch/components/autosuggest_emoji.js
+++ b/app/javascript/flavours/glitch/components/autosuggest_emoji.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import unicodeMapping from 'themes/glitch/util/emoji/emoji_unicode_mapping_light';
+import unicodeMapping from 'flavours/glitch/util/emoji/emoji_unicode_mapping_light';
 
 const assetHost = process.env.CDN_HOST || '';
 
diff --git a/app/javascript/themes/glitch/components/autosuggest_textarea.js b/app/javascript/flavours/glitch/components/autosuggest_textarea.js
index fa93847a2..551528e5a 100644
--- a/app/javascript/themes/glitch/components/autosuggest_textarea.js
+++ b/app/javascript/flavours/glitch/components/autosuggest_textarea.js
@@ -1,9 +1,9 @@
 import React from 'react';
-import AutosuggestAccountContainer from 'themes/glitch/features/compose/containers/autosuggest_account_container';
+import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container';
 import AutosuggestEmoji from './autosuggest_emoji';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import { isRtl } from 'themes/glitch/util/rtl';
+import { isRtl } from 'flavours/glitch/util/rtl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import Textarea from 'react-textarea-autosize';
 import classNames from 'classnames';
diff --git a/app/javascript/themes/glitch/components/avatar.js b/app/javascript/flavours/glitch/components/avatar.js
index dd155f059..dd155f059 100644
--- a/app/javascript/themes/glitch/components/avatar.js
+++ b/app/javascript/flavours/glitch/components/avatar.js
diff --git a/app/javascript/themes/glitch/components/avatar_overlay.js b/app/javascript/flavours/glitch/components/avatar_overlay.js
index 2ecf9fa44..2ecf9fa44 100644
--- a/app/javascript/themes/glitch/components/avatar_overlay.js
+++ b/app/javascript/flavours/glitch/components/avatar_overlay.js
diff --git a/app/javascript/themes/glitch/components/button.js b/app/javascript/flavours/glitch/components/button.js
index 16868010c..16868010c 100644
--- a/app/javascript/themes/glitch/components/button.js
+++ b/app/javascript/flavours/glitch/components/button.js
diff --git a/app/javascript/themes/glitch/components/collapsable.js b/app/javascript/flavours/glitch/components/collapsable.js
index 8bc0a54f4..fe125a729 100644
--- a/app/javascript/themes/glitch/components/collapsable.js
+++ b/app/javascript/flavours/glitch/components/collapsable.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import Motion from 'themes/glitch/util/optional_motion';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import PropTypes from 'prop-types';
 
diff --git a/app/javascript/themes/glitch/components/column.js b/app/javascript/flavours/glitch/components/column.js
index adeba9cc1..57c4c7a40 100644
--- a/app/javascript/themes/glitch/components/column.js
+++ b/app/javascript/flavours/glitch/components/column.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import detectPassiveEvents from 'detect-passive-events';
-import { scrollTop } from 'themes/glitch/util/scroll';
+import { scrollTop } from 'flavours/glitch/util/scroll';
 
 export default class Column extends React.PureComponent {
 
diff --git a/app/javascript/themes/glitch/components/column_back_button.js b/app/javascript/flavours/glitch/components/column_back_button.js
index 50c3bf11f..50c3bf11f 100644
--- a/app/javascript/themes/glitch/components/column_back_button.js
+++ b/app/javascript/flavours/glitch/components/column_back_button.js
diff --git a/app/javascript/themes/glitch/components/column_back_button_slim.js b/app/javascript/flavours/glitch/components/column_back_button_slim.js
index 2cdf1b25b..2cdf1b25b 100644
--- a/app/javascript/themes/glitch/components/column_back_button_slim.js
+++ b/app/javascript/flavours/glitch/components/column_back_button_slim.js
diff --git a/app/javascript/themes/glitch/components/column_header.js b/app/javascript/flavours/glitch/components/column_header.js
index e601082c8..ae90b6f81 100644
--- a/app/javascript/themes/glitch/components/column_header.js
+++ b/app/javascript/flavours/glitch/components/column_header.js
@@ -4,8 +4,7 @@ import classNames from 'classnames';
 import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 
-// Glitch imports
-import NotificationPurgeButtonsContainer from 'themes/glitch/containers/notification_purge_buttons_container';
+import NotificationPurgeButtonsContainer from 'flavours/glitch/containers/notification_purge_buttons_container';
 
 const messages = defineMessages({
   show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
diff --git a/app/javascript/themes/glitch/components/display_name.js b/app/javascript/flavours/glitch/components/display_name.js
index 2cf84f8f4..2cf84f8f4 100644
--- a/app/javascript/themes/glitch/components/display_name.js
+++ b/app/javascript/flavours/glitch/components/display_name.js
diff --git a/app/javascript/themes/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js
index d30dc2aaf..d4a886a8b 100644
--- a/app/javascript/themes/glitch/components/dropdown_menu.js
+++ b/app/javascript/flavours/glitch/components/dropdown_menu.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import IconButton from './icon_button';
 import Overlay from 'react-overlays/lib/Overlay';
-import Motion from 'themes/glitch/util/optional_motion';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import detectPassiveEvents from 'detect-passive-events';
 
diff --git a/app/javascript/themes/glitch/components/extended_video_player.js b/app/javascript/flavours/glitch/components/extended_video_player.js
index f8bd067e8..f8bd067e8 100644
--- a/app/javascript/themes/glitch/components/extended_video_player.js
+++ b/app/javascript/flavours/glitch/components/extended_video_player.js
diff --git a/app/javascript/themes/glitch/components/icon_button.js b/app/javascript/flavours/glitch/components/icon_button.js
index 31cdf4703..13b91e8a1 100644
--- a/app/javascript/themes/glitch/components/icon_button.js
+++ b/app/javascript/flavours/glitch/components/icon_button.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import Motion from 'themes/glitch/util/optional_motion';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
diff --git a/app/javascript/themes/glitch/components/intersection_observer_article.js b/app/javascript/flavours/glitch/components/intersection_observer_article.js
index f0139ac75..8b06f9a8c 100644
--- a/app/javascript/themes/glitch/components/intersection_observer_article.js
+++ b/app/javascript/flavours/glitch/components/intersection_observer_article.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import scheduleIdleTask from 'themes/glitch/util/schedule_idle_task';
-import getRectFromEntry from 'themes/glitch/util/get_rect_from_entry';
+import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task';
+import getRectFromEntry from 'flavours/glitch/util/get_rect_from_entry';
 import { is } from 'immutable';
 
 // Diff these props in the "rendered" state
diff --git a/app/javascript/themes/glitch/components/load_more.js b/app/javascript/flavours/glitch/components/load_more.js
index c4c8c94a2..c4c8c94a2 100644
--- a/app/javascript/themes/glitch/components/load_more.js
+++ b/app/javascript/flavours/glitch/components/load_more.js
diff --git a/app/javascript/themes/glitch/components/loading_indicator.js b/app/javascript/flavours/glitch/components/loading_indicator.js
index d6a5adb6f..d6a5adb6f 100644
--- a/app/javascript/themes/glitch/components/loading_indicator.js
+++ b/app/javascript/flavours/glitch/components/loading_indicator.js
diff --git a/app/javascript/themes/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js
index b6b40c585..d2e80de49 100644
--- a/app/javascript/themes/glitch/components/media_gallery.js
+++ b/app/javascript/flavours/glitch/components/media_gallery.js
@@ -4,9 +4,9 @@ import PropTypes from 'prop-types';
 import { is } from 'immutable';
 import IconButton from './icon_button';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { isIOS } from 'themes/glitch/util/is_mobile';
+import { isIOS } from 'flavours/glitch/util/is_mobile';
 import classNames from 'classnames';
-import { autoPlayGif } from 'themes/glitch/util/initial_state';
+import { autoPlayGif } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
diff --git a/app/javascript/themes/glitch/components/missing_indicator.js b/app/javascript/flavours/glitch/components/missing_indicator.js
index 87df7f61c..87df7f61c 100644
--- a/app/javascript/themes/glitch/components/missing_indicator.js
+++ b/app/javascript/flavours/glitch/components/missing_indicator.js
diff --git a/app/javascript/themes/glitch/components/notification_purge_buttons.js b/app/javascript/flavours/glitch/components/notification_purge_buttons.js
index e0c1543b0..e0c1543b0 100644
--- a/app/javascript/themes/glitch/components/notification_purge_buttons.js
+++ b/app/javascript/flavours/glitch/components/notification_purge_buttons.js
diff --git a/app/javascript/themes/glitch/components/permalink.js b/app/javascript/flavours/glitch/components/permalink.js
index d726d37a2..d726d37a2 100644
--- a/app/javascript/themes/glitch/components/permalink.js
+++ b/app/javascript/flavours/glitch/components/permalink.js
diff --git a/app/javascript/themes/glitch/components/relative_timestamp.js b/app/javascript/flavours/glitch/components/relative_timestamp.js
index 51588e78c..51588e78c 100644
--- a/app/javascript/themes/glitch/components/relative_timestamp.js
+++ b/app/javascript/flavours/glitch/components/relative_timestamp.js
diff --git a/app/javascript/themes/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js
index ccdcd7c85..8b1e3c93d 100644
--- a/app/javascript/themes/glitch/components/scrollable_list.js
+++ b/app/javascript/flavours/glitch/components/scrollable_list.js
@@ -1,13 +1,13 @@
 import React, { PureComponent } from 'react';
 import { ScrollContainer } from 'react-router-scroll-4';
 import PropTypes from 'prop-types';
-import IntersectionObserverArticleContainer from 'themes/glitch/containers/intersection_observer_article_container';
+import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container';
 import LoadMore from './load_more';
-import IntersectionObserverWrapper from 'themes/glitch/util/intersection_observer_wrapper';
+import IntersectionObserverWrapper from 'flavours/glitch/util/intersection_observer_wrapper';
 import { throttle } from 'lodash';
 import { List as ImmutableList } from 'immutable';
 import classNames from 'classnames';
-import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'themes/glitch/util/fullscreen';
+import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen';
 
 export default class ScrollableList extends PureComponent {
 
diff --git a/app/javascript/themes/glitch/components/setting_text.js b/app/javascript/flavours/glitch/components/setting_text.js
index a6dde4c0f..a6dde4c0f 100644
--- a/app/javascript/themes/glitch/components/setting_text.js
+++ b/app/javascript/flavours/glitch/components/setting_text.js
diff --git a/app/javascript/themes/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js
index 9288bcafa..b0d9e3757 100644
--- a/app/javascript/themes/glitch/components/status.js
+++ b/app/javascript/flavours/glitch/components/status.js
@@ -6,9 +6,9 @@ import StatusHeader from './status_header';
 import StatusContent from './status_content';
 import StatusActionBar from './status_action_bar';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import { MediaGallery, Video } from 'themes/glitch/util/async-components';
+import { MediaGallery, Video } from 'flavours/glitch/util/async-components';
 import { HotKeys } from 'react-hotkeys';
-import NotificationOverlayContainer from 'themes/glitch/features/notifications/containers/overlay_container';
+import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container';
 import classNames from 'classnames';
 
 // We use the component (and not the container) since we do not want
diff --git a/app/javascript/themes/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js
index 9d615ed7c..5a06782be 100644
--- a/app/javascript/themes/glitch/components/status_action_bar.js
+++ b/app/javascript/flavours/glitch/components/status_action_bar.js
@@ -1,14 +1,11 @@
-//  THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !!
-//  SEE INSTEAD : glitch/components/status/action_bar
-
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import IconButton from './icon_button';
-import DropdownMenuContainer from 'themes/glitch/containers/dropdown_menu_container';
+import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 import RelativeTimestamp from './relative_timestamp';
 
 const messages = defineMessages({
diff --git a/app/javascript/themes/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js
index 3eba6eaa0..0c40e62cc 100644
--- a/app/javascript/themes/glitch/components/status_content.js
+++ b/app/javascript/flavours/glitch/components/status_content.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import { isRtl } from 'themes/glitch/util/rtl';
+import { isRtl } from 'flavours/glitch/util/rtl';
 import { FormattedMessage } from 'react-intl';
 import Permalink from './permalink';
 import classnames from 'classnames';
diff --git a/app/javascript/themes/glitch/components/status_header.js b/app/javascript/flavours/glitch/components/status_header.js
index bfa996cd5..bfa996cd5 100644
--- a/app/javascript/themes/glitch/components/status_header.js
+++ b/app/javascript/flavours/glitch/components/status_header.js
diff --git a/app/javascript/themes/glitch/components/status_list.js b/app/javascript/flavours/glitch/components/status_list.js
index ddb1354c6..f190ba6ab 100644
--- a/app/javascript/themes/glitch/components/status_list.js
+++ b/app/javascript/flavours/glitch/components/status_list.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import StatusContainer from 'themes/glitch/containers/status_container';
+import StatusContainer from 'flavours/glitch/containers/status_container';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import ScrollableList from './scrollable_list';
 
diff --git a/app/javascript/themes/glitch/components/status_prepend.js b/app/javascript/flavours/glitch/components/status_prepend.js
index bd2559e46..bd2559e46 100644
--- a/app/javascript/themes/glitch/components/status_prepend.js
+++ b/app/javascript/flavours/glitch/components/status_prepend.js
diff --git a/app/javascript/themes/glitch/components/status_visibility_icon.js b/app/javascript/flavours/glitch/components/status_visibility_icon.js
index 017b69cbb..017b69cbb 100644
--- a/app/javascript/themes/glitch/components/status_visibility_icon.js
+++ b/app/javascript/flavours/glitch/components/status_visibility_icon.js
diff --git a/app/javascript/themes/glitch/containers/account_container.js b/app/javascript/flavours/glitch/containers/account_container.js
index c1ce49987..bc84d299b 100644
--- a/app/javascript/themes/glitch/containers/account_container.js
+++ b/app/javascript/flavours/glitch/containers/account_container.js
@@ -1,8 +1,8 @@
 import React from 'react';
 import { connect } from 'react-redux';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { makeGetAccount } from 'themes/glitch/selectors';
-import Account from 'themes/glitch/components/account';
+import { makeGetAccount } from 'flavours/glitch/selectors';
+import Account from 'flavours/glitch/components/account';
 import {
   followAccount,
   unfollowAccount,
@@ -10,10 +10,10 @@ import {
   unblockAccount,
   muteAccount,
   unmuteAccount,
-} from 'themes/glitch/actions/accounts';
-import { openModal } from 'themes/glitch/actions/modal';
-import { initMuteModal } from 'themes/glitch/actions/mutes';
-import { unfollowModal } from 'themes/glitch/util/initial_state';
+} from 'flavours/glitch/actions/accounts';
+import { openModal } from 'flavours/glitch/actions/modal';
+import { initMuteModal } from 'flavours/glitch/actions/mutes';
+import { unfollowModal } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
diff --git a/app/javascript/themes/glitch/containers/card_container.js b/app/javascript/flavours/glitch/containers/card_container.js
index 8285437bb..dec7df522 100644
--- a/app/javascript/themes/glitch/containers/card_container.js
+++ b/app/javascript/flavours/glitch/containers/card_container.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import Card from 'themes/glitch/features/status/components/card';
+import Card from 'flavours/glitch/features/status/components/card';
 import { fromJS } from 'immutable';
 
 export default class CardContainer extends React.PureComponent {
diff --git a/app/javascript/themes/glitch/containers/compose_container.js b/app/javascript/flavours/glitch/containers/compose_container.js
index 82980ee36..60f6a9c9f 100644
--- a/app/javascript/themes/glitch/containers/compose_container.js
+++ b/app/javascript/flavours/glitch/containers/compose_container.js
@@ -1,12 +1,12 @@
 import React from 'react';
 import { Provider } from 'react-redux';
 import PropTypes from 'prop-types';
-import configureStore from 'themes/glitch/store/configureStore';
-import { hydrateStore } from 'themes/glitch/actions/store';
+import configureStore from 'flavours/glitch/store/configureStore';
+import { hydrateStore } from 'flavours/glitch/actions/store';
 import { IntlProvider, addLocaleData } from 'react-intl';
 import { getLocale } from 'mastodon/locales';
-import Compose from 'themes/glitch/features/standalone/compose';
-import initialState from 'themes/glitch/util/initial_state';
+import Compose from 'flavours/glitch/features/standalone/compose';
+import initialState from 'flavours/glitch/util/initial_state';
 
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
diff --git a/app/javascript/themes/glitch/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js
index 15e8da2e3..0b4f72fa1 100644
--- a/app/javascript/themes/glitch/containers/dropdown_menu_container.js
+++ b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js
@@ -1,7 +1,7 @@
-import { openModal, closeModal } from 'themes/glitch/actions/modal';
+import { openModal, closeModal } from 'flavours/glitch/actions/modal';
 import { connect } from 'react-redux';
-import DropdownMenu from 'themes/glitch/components/dropdown_menu';
-import { isUserTouching } from 'themes/glitch/util/is_mobile';
+import DropdownMenu from 'flavours/glitch/components/dropdown_menu';
+import { isUserTouching } from 'flavours/glitch/util/is_mobile';
 
 const mapStateToProps = state => ({
   isModalOpen: state.get('modal').modalType === 'ACTIONS',
diff --git a/app/javascript/themes/glitch/containers/intersection_observer_article_container.js b/app/javascript/flavours/glitch/containers/intersection_observer_article_container.js
index 6ede64738..f2741f2d4 100644
--- a/app/javascript/themes/glitch/containers/intersection_observer_article_container.js
+++ b/app/javascript/flavours/glitch/containers/intersection_observer_article_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
-import IntersectionObserverArticle from 'themes/glitch/components/intersection_observer_article';
-import { setHeight } from 'themes/glitch/actions/height_cache';
+import IntersectionObserverArticle from 'flavours/glitch/components/intersection_observer_article';
+import { setHeight } from 'flavours/glitch/actions/height_cache';
 
 const makeMapStateToProps = (state, props) => ({
   cachedHeight: state.getIn(['height_cache', props.saveHeightKey, props.id]),
diff --git a/app/javascript/themes/glitch/containers/mastodon.js b/app/javascript/flavours/glitch/containers/mastodon.js
index 348470637..1c98cd5f7 100644
--- a/app/javascript/themes/glitch/containers/mastodon.js
+++ b/app/javascript/flavours/glitch/containers/mastodon.js
@@ -1,16 +1,16 @@
 import React from 'react';
 import { Provider } from 'react-redux';
 import PropTypes from 'prop-types';
-import configureStore from 'themes/glitch/store/configureStore';
-import { showOnboardingOnce } from 'themes/glitch/actions/onboarding';
+import configureStore from 'flavours/glitch/store/configureStore';
+import { showOnboardingOnce } from 'flavours/glitch/actions/onboarding';
 import { BrowserRouter, Route } from 'react-router-dom';
 import { ScrollContext } from 'react-router-scroll-4';
-import UI from 'themes/glitch/features/ui';
-import { hydrateStore } from 'themes/glitch/actions/store';
-import { connectUserStream } from 'themes/glitch/actions/streaming';
+import UI from 'flavours/glitch/features/ui';
+import { hydrateStore } from 'flavours/glitch/actions/store';
+import { connectUserStream } from 'flavours/glitch/actions/streaming';
 import { IntlProvider, addLocaleData } from 'react-intl';
-import { getLocale } from 'mastodon/locales';
-import initialState from 'themes/glitch/util/initial_state';
+import { getLocale } from 'locales';
+import initialState from 'flavours/glitch/util/initial_state';
 
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
diff --git a/app/javascript/themes/glitch/containers/media_gallery_container.js b/app/javascript/flavours/glitch/containers/media_gallery_container.js
index 86965f73b..54bfbf453 100644
--- a/app/javascript/themes/glitch/containers/media_gallery_container.js
+++ b/app/javascript/flavours/glitch/containers/media_gallery_container.js
@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { IntlProvider, addLocaleData } from 'react-intl';
 import { getLocale } from 'mastodon/locales';
-import MediaGallery from 'themes/glitch/components/media_gallery';
+import MediaGallery from 'flavours/glitch/components/media_gallery';
 import { fromJS } from 'immutable';
 
 const { localeData, messages } = getLocale();
diff --git a/app/javascript/themes/glitch/containers/notification_purge_buttons_container.js b/app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js
index ee4cb84cd..2570cf4a5 100644
--- a/app/javascript/themes/glitch/containers/notification_purge_buttons_container.js
+++ b/app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js
@@ -3,13 +3,13 @@ import { connect } from 'react-redux';
 import { defineMessages, injectIntl } from 'react-intl';
 
 //  Our imports.
-import NotificationPurgeButtons from 'themes/glitch/components/notification_purge_buttons';
+import NotificationPurgeButtons from 'flavours/glitch/components/notification_purge_buttons';
 import {
   deleteMarkedNotifications,
   enterNotificationClearingMode,
   markAllNotifications,
-} from 'themes/glitch/actions/notifications';
-import { openModal } from 'themes/glitch/actions/modal';
+} from 'flavours/glitch/actions/notifications';
+import { openModal } from 'flavours/glitch/actions/modal';
 
 const messages = defineMessages({
   clearMessage: { id: 'notifications.marked_clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all selected notifications?' },
diff --git a/app/javascript/themes/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js
index 0cee73b9a..b753de7b3 100644
--- a/app/javascript/themes/glitch/containers/status_container.js
+++ b/app/javascript/flavours/glitch/containers/status_container.js
@@ -1,11 +1,11 @@
 import React from 'react';
 import { connect } from 'react-redux';
-import Status from 'themes/glitch/components/status';
-import { makeGetStatus } from 'themes/glitch/selectors';
+import Status from 'flavours/glitch/components/status';
+import { makeGetStatus } from 'flavours/glitch/selectors';
 import {
   replyCompose,
   mentionCompose,
-} from 'themes/glitch/actions/compose';
+} from 'flavours/glitch/actions/compose';
 import {
   reblog,
   favourite,
@@ -13,14 +13,14 @@ import {
   unfavourite,
   pin,
   unpin,
-} from 'themes/glitch/actions/interactions';
-import { blockAccount } from 'themes/glitch/actions/accounts';
-import { muteStatus, unmuteStatus, deleteStatus } from 'themes/glitch/actions/statuses';
-import { initMuteModal } from 'themes/glitch/actions/mutes';
-import { initReport } from 'themes/glitch/actions/reports';
-import { openModal } from 'themes/glitch/actions/modal';
+} from 'flavours/glitch/actions/interactions';
+import { blockAccount } from 'flavours/glitch/actions/accounts';
+import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses';
+import { initMuteModal } from 'flavours/glitch/actions/mutes';
+import { initReport } from 'flavours/glitch/actions/reports';
+import { openModal } from 'flavours/glitch/actions/modal';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { boostModal, deleteModal } from 'themes/glitch/util/initial_state';
+import { boostModal, deleteModal } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
diff --git a/app/javascript/themes/glitch/containers/timeline_container.js b/app/javascript/flavours/glitch/containers/timeline_container.js
index a75f8808d..c5ffe1b63 100644
--- a/app/javascript/themes/glitch/containers/timeline_container.js
+++ b/app/javascript/flavours/glitch/containers/timeline_container.js
@@ -1,13 +1,13 @@
 import React from 'react';
 import { Provider } from 'react-redux';
 import PropTypes from 'prop-types';
-import configureStore from 'themes/glitch/store/configureStore';
-import { hydrateStore } from 'themes/glitch/actions/store';
+import configureStore from 'flavours/glitch/store/configureStore';
+import { hydrateStore } from 'flavours/glitch/actions/store';
 import { IntlProvider, addLocaleData } from 'react-intl';
 import { getLocale } from 'mastodon/locales';
-import PublicTimeline from 'themes/glitch/features/standalone/public_timeline';
-import HashtagTimeline from 'themes/glitch/features/standalone/hashtag_timeline';
-import initialState from 'themes/glitch/util/initial_state';
+import PublicTimeline from 'flavours/glitch/features/standalone/public_timeline';
+import HashtagTimeline from 'flavours/glitch/features/standalone/hashtag_timeline';
+import initialState from 'flavours/glitch/util/initial_state';
 
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
diff --git a/app/javascript/themes/glitch/containers/video_container.js b/app/javascript/flavours/glitch/containers/video_container.js
index 2b0e98666..b206e9a10 100644
--- a/app/javascript/themes/glitch/containers/video_container.js
+++ b/app/javascript/flavours/glitch/containers/video_container.js
@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { IntlProvider, addLocaleData } from 'react-intl';
 import { getLocale } from 'mastodon/locales';
-import Video from 'themes/glitch/features/video';
+import Video from 'flavours/glitch/features/video';
 
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
diff --git a/app/javascript/themes/glitch/features/account/components/action_bar.js b/app/javascript/flavours/glitch/features/account/components/action_bar.js
index 0edd5c848..df8cb3733 100644
--- a/app/javascript/themes/glitch/features/account/components/action_bar.js
+++ b/app/javascript/flavours/glitch/features/account/components/action_bar.js
@@ -1,10 +1,10 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import DropdownMenuContainer from 'themes/glitch/containers/dropdown_menu_container';
+import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
 import { Link } from 'react-router-dom';
 import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl';
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
@@ -63,9 +63,8 @@ export default class ActionBar extends React.PureComponent {
     if (account.get('id') === me) {
       menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
     } else {
-      const following = account.getIn(['relationship', 'following']);
-      if (following) {
-        if (following.get('reblogs')) {
+      if (account.getIn(['relationship', 'following'])) {
+        if (account.getIn(['relationship', 'showing_reblogs'])) {
           menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
         } else {
           menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
diff --git a/app/javascript/themes/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js
index 696bb1991..f5ecaae71 100644
--- a/app/javascript/themes/glitch/features/account/components/header.js
+++ b/app/javascript/flavours/glitch/features/account/components/header.js
@@ -4,12 +4,12 @@ import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
-import Avatar from 'themes/glitch/components/avatar';
-import IconButton from 'themes/glitch/components/icon_button';
+import Avatar from 'flavours/glitch/components/avatar';
+import IconButton from 'flavours/glitch/components/icon_button';
 
-import emojify from 'themes/glitch/util/emoji';
-import { me } from 'themes/glitch/util/initial_state';
-import { processBio } from 'themes/glitch/util/bio_metadata';
+import emojify from 'flavours/glitch/util/emoji';
+import { me } from 'flavours/glitch/util/initial_state';
+import { processBio } from 'flavours/glitch/util/bio_metadata';
 
 const messages = defineMessages({
   unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
diff --git a/app/javascript/themes/glitch/features/account_gallery/components/media_item.js b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js
index 88c9156b5..e52d3b0bb 100644
--- a/app/javascript/themes/glitch/features/account_gallery/components/media_item.js
+++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import Permalink from 'themes/glitch/components/permalink';
+import Permalink from 'flavours/glitch/components/permalink';
 
 export default class MediaItem extends ImmutablePureComponent {
 
diff --git a/app/javascript/themes/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.js
index a21c089da..951e019e3 100644
--- a/app/javascript/themes/glitch/features/account_gallery/index.js
+++ b/app/javascript/flavours/glitch/features/account_gallery/index.js
@@ -2,18 +2,18 @@ import React from 'react';
 import { connect } from 'react-redux';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import { fetchAccount } from 'themes/glitch/actions/accounts';
-import { refreshAccountMediaTimeline, expandAccountMediaTimeline } from 'themes/glitch/actions/timelines';
-import LoadingIndicator from 'themes/glitch/components/loading_indicator';
-import Column from 'themes/glitch/features/ui/components/column';
-import ColumnBackButton from 'themes/glitch/components/column_back_button';
+import { fetchAccount } from 'flavours/glitch/actions/accounts';
+import { refreshAccountMediaTimeline, expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
+import Column from 'flavours/glitch/features/ui/components/column';
+import ColumnBackButton from 'flavours/glitch/components/column_back_button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import { getAccountGallery } from 'themes/glitch/selectors';
+import { getAccountGallery } from 'flavours/glitch/selectors';
 import MediaItem from './components/media_item';
-import HeaderContainer from 'themes/glitch/features/account_timeline/containers/header_container';
+import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container';
 import { FormattedMessage } from 'react-intl';
 import { ScrollContainer } from 'react-router-scroll-4';
-import LoadMore from 'themes/glitch/components/load_more';
+import LoadMore from 'flavours/glitch/components/load_more';
 
 const mapStateToProps = (state, props) => ({
   medias: getAccountGallery(state, props.params.accountId),
diff --git a/app/javascript/themes/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js
index c719a7bcb..4ad677fbe 100644
--- a/app/javascript/themes/glitch/features/account_timeline/components/header.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js
@@ -1,9 +1,9 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import InnerHeader from 'themes/glitch/features/account/components/header';
-import ActionBar from 'themes/glitch/features/account/components/action_bar';
-import MissingIndicator from 'themes/glitch/components/missing_indicator';
+import InnerHeader from 'flavours/glitch/features/account/components/header';
+import ActionBar from 'flavours/glitch/features/account/components/action_bar';
+import MissingIndicator from 'flavours/glitch/components/missing_indicator';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 export default class Header extends ImmutablePureComponent {
diff --git a/app/javascript/themes/glitch/features/account_timeline/containers/header_container.js b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js
index 766b57b56..37ff445b2 100644
--- a/app/javascript/themes/glitch/features/account_timeline/containers/header_container.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import { connect } from 'react-redux';
-import { makeGetAccount } from 'themes/glitch/selectors';
+import { makeGetAccount } from 'flavours/glitch/selectors';
 import Header from '../components/header';
 import {
   followAccount,
@@ -8,14 +8,14 @@ import {
   blockAccount,
   unblockAccount,
   unmuteAccount,
-} from 'themes/glitch/actions/accounts';
-import { mentionCompose } from 'themes/glitch/actions/compose';
-import { initMuteModal } from 'themes/glitch/actions/mutes';
-import { initReport } from 'themes/glitch/actions/reports';
-import { openModal } from 'themes/glitch/actions/modal';
-import { blockDomain, unblockDomain } from 'themes/glitch/actions/domain_blocks';
+} from 'flavours/glitch/actions/accounts';
+import { mentionCompose } from 'flavours/glitch/actions/compose';
+import { initMuteModal } from 'flavours/glitch/actions/mutes';
+import { initReport } from 'flavours/glitch/actions/reports';
+import { openModal } from 'flavours/glitch/actions/modal';
+import { blockDomain, unblockDomain } from 'flavours/glitch/actions/domain_blocks';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { unfollowModal } from 'themes/glitch/util/initial_state';
+import { unfollowModal } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
@@ -68,7 +68,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
   },
 
   onReblogToggle (account) {
-    if (account.getIn(['relationship', 'following', 'reblogs'])) {
+    if (account.getIn(['relationship', 'showing_reblogs'])) {
       dispatch(followAccount(account.get('id'), false));
     } else {
       dispatch(followAccount(account.get('id'), true));
diff --git a/app/javascript/themes/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js
index 81336ef3a..75dba5049 100644
--- a/app/javascript/themes/glitch/features/account_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/account_timeline/index.js
@@ -2,8 +2,8 @@ import React from 'react';
 import { connect } from 'react-redux';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import { fetchAccount } from 'themes/glitch/actions/accounts';
-import { refreshAccountTimeline, expandAccountTimeline } from 'themes/glitch/actions/timelines';
+import { fetchAccount } from 'flavours/glitch/actions/accounts';
+import { refreshAccountTimeline, expandAccountTimeline } from 'flavours/glitch/actions/timelines';
 import StatusList from '../../components/status_list';
 import LoadingIndicator from '../../components/loading_indicator';
 import Column from '../ui/components/column';
diff --git a/app/javascript/themes/glitch/features/blocks/index.js b/app/javascript/flavours/glitch/features/blocks/index.js
index 70630818c..edd448921 100644
--- a/app/javascript/themes/glitch/features/blocks/index.js
+++ b/app/javascript/flavours/glitch/features/blocks/index.js
@@ -2,12 +2,12 @@ import React from 'react';
 import { connect } from 'react-redux';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import LoadingIndicator from 'themes/glitch/components/loading_indicator';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
 import { ScrollContainer } from 'react-router-scroll-4';
-import Column from 'themes/glitch/features/ui/components/column';
-import ColumnBackButtonSlim from 'themes/glitch/components/column_back_button_slim';
-import AccountContainer from 'themes/glitch/containers/account_container';
-import { fetchBlocks, expandBlocks } from 'themes/glitch/actions/blocks';
+import Column from 'flavours/glitch/features/ui/components/column';
+import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim';
+import AccountContainer from 'flavours/glitch/containers/account_container';
+import { fetchBlocks, expandBlocks } from 'flavours/glitch/actions/blocks';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
diff --git a/app/javascript/themes/glitch/features/community_timeline/components/column_settings.js b/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.js
index 988e36308..6dc292ee5 100644
--- a/app/javascript/themes/glitch/features/community_timeline/components/column_settings.js
+++ b/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.js
@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import SettingText from 'themes/glitch/components/setting_text';
+import SettingText from 'flavours/glitch/components/setting_text';
 
 const messages = defineMessages({
   filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' },
diff --git a/app/javascript/themes/glitch/features/community_timeline/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/community_timeline/containers/column_settings_container.js
index cd9c34365..84234a836 100644
--- a/app/javascript/themes/glitch/features/community_timeline/containers/column_settings_container.js
+++ b/app/javascript/flavours/glitch/features/community_timeline/containers/column_settings_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
 import ColumnSettings from '../components/column_settings';
-import { changeSetting } from 'themes/glitch/actions/settings';
+import { changeSetting } from 'flavours/glitch/actions/settings';
 
 const mapStateToProps = state => ({
   settings: state.getIn(['settings', 'community']),
diff --git a/app/javascript/themes/glitch/features/community_timeline/index.js b/app/javascript/flavours/glitch/features/community_timeline/index.js
index 9d255bd01..55355414f 100644
--- a/app/javascript/themes/glitch/features/community_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/community_timeline/index.js
@@ -1,17 +1,17 @@
 import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
-import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container';
-import Column from 'themes/glitch/components/column';
-import ColumnHeader from 'themes/glitch/components/column_header';
+import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
 import {
   refreshCommunityTimeline,
   expandCommunityTimeline,
-} from 'themes/glitch/actions/timelines';
-import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns';
+} from 'flavours/glitch/actions/timelines';
+import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
-import { connectCommunityStream } from 'themes/glitch/actions/streaming';
+import { connectCommunityStream } from 'flavours/glitch/actions/streaming';
 
 const messages = defineMessages({
   title: { id: 'column.community', defaultMessage: 'Local timeline' },
diff --git a/app/javascript/themes/glitch/features/compose/components/advanced_options.js b/app/javascript/flavours/glitch/features/compose/components/advanced_options.js
index 045bad2e5..045bad2e5 100644
--- a/app/javascript/themes/glitch/features/compose/components/advanced_options.js
+++ b/app/javascript/flavours/glitch/features/compose/components/advanced_options.js
diff --git a/app/javascript/themes/glitch/features/compose/components/advanced_options_toggle.js b/app/javascript/flavours/glitch/features/compose/components/advanced_options_toggle.js
index 98b3b6a44..98b3b6a44 100644
--- a/app/javascript/themes/glitch/features/compose/components/advanced_options_toggle.js
+++ b/app/javascript/flavours/glitch/features/compose/components/advanced_options_toggle.js
diff --git a/app/javascript/themes/glitch/features/compose/components/attach_options.js b/app/javascript/flavours/glitch/features/compose/components/attach_options.js
index c396714f3..6c7a1f55f 100644
--- a/app/javascript/themes/glitch/features/compose/components/attach_options.js
+++ b/app/javascript/flavours/glitch/features/compose/components/attach_options.js
@@ -6,10 +6,10 @@ import { injectIntl, defineMessages } from 'react-intl';
 
 //  Our imports  //
 import ComposeDropdown from './dropdown';
-import { uploadCompose } from 'themes/glitch/actions/compose';
+import { uploadCompose } from 'flavours/glitch/actions/compose';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import { openModal } from 'themes/glitch/actions/modal';
+import { openModal } from 'flavours/glitch/actions/modal';
 
 const messages = defineMessages({
   upload :
diff --git a/app/javascript/themes/glitch/features/compose/components/autosuggest_account.js b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.js
index 4a98d89fe..3d474af30 100644
--- a/app/javascript/themes/glitch/features/compose/components/autosuggest_account.js
+++ b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.js
@@ -1,6 +1,6 @@
 import React from 'react';
-import Avatar from 'themes/glitch/components/avatar';
-import DisplayName from 'themes/glitch/components/display_name';
+import Avatar from 'flavours/glitch/components/avatar';
+import DisplayName from 'flavours/glitch/components/display_name';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
diff --git a/app/javascript/themes/glitch/features/compose/components/character_counter.js b/app/javascript/flavours/glitch/features/compose/components/character_counter.js
index 0ecfc9141..0ecfc9141 100644
--- a/app/javascript/themes/glitch/features/compose/components/character_counter.js
+++ b/app/javascript/flavours/glitch/features/compose/components/character_counter.js
diff --git a/app/javascript/themes/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js
index 54b1944a4..67ce935f4 100644
--- a/app/javascript/themes/glitch/features/compose/components/compose_form.js
+++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js
@@ -1,12 +1,12 @@
 import React from 'react';
 import CharacterCounter from './character_counter';
-import Button from 'themes/glitch/components/button';
+import Button from 'flavours/glitch/components/button';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import ReplyIndicatorContainer from '../containers/reply_indicator_container';
-import AutosuggestTextarea from 'themes/glitch/components/autosuggest_textarea';
+import AutosuggestTextarea from 'flavours/glitch/components/autosuggest_textarea';
 import { defineMessages, injectIntl } from 'react-intl';
-import Collapsable from 'themes/glitch/components/collapsable';
+import Collapsable from 'flavours/glitch/components/collapsable';
 import SpoilerButtonContainer from '../containers/spoiler_button_container';
 import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
 import ComposeAdvancedOptionsContainer from '../containers/advanced_options_container';
@@ -14,12 +14,12 @@ import SensitiveButtonContainer from '../containers/sensitive_button_container';
 import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
 import UploadFormContainer from '../containers/upload_form_container';
 import WarningContainer from '../containers/warning_container';
-import { isMobile } from 'themes/glitch/util/is_mobile';
+import { isMobile } from 'flavours/glitch/util/is_mobile';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { length } from 'stringz';
-import { countableText } from 'themes/glitch/util/counter';
+import { countableText } from 'flavours/glitch/util/counter';
 import ComposeAttachOptions from './attach_options';
-import initialState from 'themes/glitch/util/initial_state';
+import initialState from 'flavours/glitch/util/initial_state';
 
 const maxChars = initialState.max_toot_chars;
 
diff --git a/app/javascript/themes/glitch/features/compose/components/dropdown.js b/app/javascript/flavours/glitch/features/compose/components/dropdown.js
index f3d9f094e..1b0000fb7 100644
--- a/app/javascript/themes/glitch/features/compose/components/dropdown.js
+++ b/app/javascript/flavours/glitch/features/compose/components/dropdown.js
@@ -3,7 +3,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 //  Our imports.
-import IconButton from 'themes/glitch/components/icon_button';
+import IconButton from 'flavours/glitch/components/icon_button';
 
 const iconStyle = {
   height     : null,
diff --git a/app/javascript/themes/glitch/features/compose/components/emoji_picker_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.js
index fd59efb85..cf89f91d3 100644
--- a/app/javascript/themes/glitch/features/compose/components/emoji_picker_dropdown.js
+++ b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.js
@@ -1,12 +1,12 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl } from 'react-intl';
-import { EmojiPicker as EmojiPickerAsync } from 'themes/glitch/util/async-components';
+import { EmojiPicker as EmojiPickerAsync } from 'flavours/glitch/util/async-components';
 import Overlay from 'react-overlays/lib/Overlay';
 import classNames from 'classnames';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import detectPassiveEvents from 'detect-passive-events';
-import { buildCustomEmojis } from 'themes/glitch/util/emoji';
+import { buildCustomEmojis } from 'flavours/glitch/util/emoji';
 
 const messages = defineMessages({
   emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
diff --git a/app/javascript/themes/glitch/features/compose/components/navigation_bar.js b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js
index 24a70949b..1b6d74123 100644
--- a/app/javascript/themes/glitch/features/compose/components/navigation_bar.js
+++ b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js
@@ -1,9 +1,9 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import Avatar from 'themes/glitch/components/avatar';
-import IconButton from 'themes/glitch/components/icon_button';
-import Permalink from 'themes/glitch/components/permalink';
+import Avatar from 'flavours/glitch/components/avatar';
+import IconButton from 'flavours/glitch/components/icon_button';
+import Permalink from 'flavours/glitch/components/permalink';
 import { FormattedMessage } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
diff --git a/app/javascript/themes/glitch/features/compose/components/privacy_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js
index 0cd92d174..90f062f8f 100644
--- a/app/javascript/themes/glitch/features/compose/components/privacy_dropdown.js
+++ b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js
@@ -1,9 +1,9 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { injectIntl, defineMessages } from 'react-intl';
-import IconButton from 'themes/glitch/components/icon_button';
+import IconButton from 'flavours/glitch/components/icon_button';
 import Overlay from 'react-overlays/lib/Overlay';
-import Motion from 'themes/glitch/util/optional_motion';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import detectPassiveEvents from 'detect-passive-events';
 import classNames from 'classnames';
diff --git a/app/javascript/themes/glitch/features/compose/components/reply_indicator.js b/app/javascript/flavours/glitch/features/compose/components/reply_indicator.js
index 9a8d10ceb..cb28e51be 100644
--- a/app/javascript/themes/glitch/features/compose/components/reply_indicator.js
+++ b/app/javascript/flavours/glitch/features/compose/components/reply_indicator.js
@@ -1,9 +1,9 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import Avatar from 'themes/glitch/components/avatar';
-import IconButton from 'themes/glitch/components/icon_button';
-import DisplayName from 'themes/glitch/components/display_name';
+import Avatar from 'flavours/glitch/components/avatar';
+import IconButton from 'flavours/glitch/components/icon_button';
+import DisplayName from 'flavours/glitch/components/display_name';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
diff --git a/app/javascript/themes/glitch/features/compose/components/search.js b/app/javascript/flavours/glitch/features/compose/components/search.js
index c3218137f..1ce66b19d 100644
--- a/app/javascript/themes/glitch/features/compose/components/search.js
+++ b/app/javascript/flavours/glitch/features/compose/components/search.js
@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import Overlay from 'react-overlays/lib/Overlay';
-import Motion from 'themes/glitch/util/optional_motion';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 
 const messages = defineMessages({
diff --git a/app/javascript/themes/glitch/features/compose/components/search_results.js b/app/javascript/flavours/glitch/features/compose/components/search_results.js
index 3fdafa5f3..2a4818d4e 100644
--- a/app/javascript/themes/glitch/features/compose/components/search_results.js
+++ b/app/javascript/flavours/glitch/features/compose/components/search_results.js
@@ -1,8 +1,8 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import { FormattedMessage } from 'react-intl';
-import AccountContainer from 'themes/glitch/containers/account_container';
-import StatusContainer from 'themes/glitch/containers/status_container';
+import AccountContainer from 'flavours/glitch/containers/account_container';
+import StatusContainer from 'flavours/glitch/containers/status_container';
 import { Link } from 'react-router-dom';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
diff --git a/app/javascript/themes/glitch/features/compose/components/text_icon_button.js b/app/javascript/flavours/glitch/features/compose/components/text_icon_button.js
index 9c8ffab1f..9c8ffab1f 100644
--- a/app/javascript/themes/glitch/features/compose/components/text_icon_button.js
+++ b/app/javascript/flavours/glitch/features/compose/components/text_icon_button.js
diff --git a/app/javascript/themes/glitch/features/compose/components/upload.js b/app/javascript/flavours/glitch/features/compose/components/upload.js
index ded376ada..a1fc93234 100644
--- a/app/javascript/themes/glitch/features/compose/components/upload.js
+++ b/app/javascript/flavours/glitch/features/compose/components/upload.js
@@ -1,8 +1,8 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import IconButton from 'themes/glitch/components/icon_button';
-import Motion from 'themes/glitch/util/optional_motion';
+import IconButton from 'flavours/glitch/components/icon_button';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { defineMessages, injectIntl } from 'react-intl';
diff --git a/app/javascript/themes/glitch/features/compose/components/upload_button.js b/app/javascript/flavours/glitch/features/compose/components/upload_button.js
index d7742adfe..f06167a2a 100644
--- a/app/javascript/themes/glitch/features/compose/components/upload_button.js
+++ b/app/javascript/flavours/glitch/features/compose/components/upload_button.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import IconButton from 'themes/glitch/components/icon_button';
+import IconButton from 'flavours/glitch/components/icon_button';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl } from 'react-intl';
 import { connect } from 'react-redux';
diff --git a/app/javascript/themes/glitch/features/compose/components/upload_form.js b/app/javascript/flavours/glitch/features/compose/components/upload_form.js
index b7f112205..b7f112205 100644
--- a/app/javascript/themes/glitch/features/compose/components/upload_form.js
+++ b/app/javascript/flavours/glitch/features/compose/components/upload_form.js
diff --git a/app/javascript/themes/glitch/features/compose/components/upload_progress.js b/app/javascript/flavours/glitch/features/compose/components/upload_progress.js
index b923d0a22..2a3b8ceb4 100644
--- a/app/javascript/themes/glitch/features/compose/components/upload_progress.js
+++ b/app/javascript/flavours/glitch/features/compose/components/upload_progress.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import Motion from 'themes/glitch/util/optional_motion';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import { FormattedMessage } from 'react-intl';
 
diff --git a/app/javascript/themes/glitch/features/compose/components/warning.js b/app/javascript/flavours/glitch/features/compose/components/warning.js
index 82df55a31..4962e76c8 100644
--- a/app/javascript/themes/glitch/features/compose/components/warning.js
+++ b/app/javascript/flavours/glitch/features/compose/components/warning.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import Motion from 'themes/glitch/util/optional_motion';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 
 export default class Warning extends React.PureComponent {
diff --git a/app/javascript/themes/glitch/features/compose/containers/advanced_options_container.js b/app/javascript/flavours/glitch/features/compose/containers/advanced_options_container.js
index 9f168942a..da381568b 100644
--- a/app/javascript/themes/glitch/features/compose/containers/advanced_options_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/advanced_options_container.js
@@ -2,7 +2,7 @@
 import { connect } from 'react-redux';
 
 //  Our imports.
-import { toggleComposeAdvancedOption } from 'themes/glitch/actions/compose';
+import { toggleComposeAdvancedOption } from 'flavours/glitch/actions/compose';
 import ComposeAdvancedOptions from '../components/advanced_options';
 
 const mapStateToProps = state => ({
diff --git a/app/javascript/themes/glitch/features/compose/containers/autosuggest_account_container.js b/app/javascript/flavours/glitch/features/compose/containers/autosuggest_account_container.js
index 96eb70c18..0e1c328fe 100644
--- a/app/javascript/themes/glitch/features/compose/containers/autosuggest_account_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/autosuggest_account_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
 import AutosuggestAccount from '../components/autosuggest_account';
-import { makeGetAccount } from 'themes/glitch/selectors';
+import { makeGetAccount } from 'flavours/glitch/selectors';
 
 const makeMapStateToProps = () => {
   const getAccount = makeGetAccount();
diff --git a/app/javascript/themes/glitch/features/compose/containers/compose_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
index 7afa988f1..e2e93e44b 100644
--- a/app/javascript/themes/glitch/features/compose/containers/compose_form_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
 import ComposeForm from '../components/compose_form';
-import { changeComposeVisibility, uploadCompose } from 'themes/glitch/actions/compose';
+import { changeComposeVisibility, uploadCompose } from 'flavours/glitch/actions/compose';
 import {
   changeCompose,
   submitCompose,
@@ -9,7 +9,7 @@ import {
   selectComposeSuggestion,
   changeComposeSpoilerText,
   insertEmojiCompose,
-} from 'themes/glitch/actions/compose';
+} from 'flavours/glitch/actions/compose';
 
 const mapStateToProps = state => ({
   text: state.getIn(['compose', 'text']),
diff --git a/app/javascript/themes/glitch/features/compose/containers/emoji_picker_dropdown_container.js b/app/javascript/flavours/glitch/features/compose/containers/emoji_picker_dropdown_container.js
index 55a13bd65..ba85edd87 100644
--- a/app/javascript/themes/glitch/features/compose/containers/emoji_picker_dropdown_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/emoji_picker_dropdown_container.js
@@ -1,9 +1,9 @@
 import { connect } from 'react-redux';
 import EmojiPickerDropdown from '../components/emoji_picker_dropdown';
-import { changeSetting } from 'themes/glitch/actions/settings';
+import { changeSetting } from 'flavours/glitch/actions/settings';
 import { createSelector } from 'reselect';
 import { Map as ImmutableMap } from 'immutable';
-import { useEmoji } from 'themes/glitch/actions/emojis';
+import { useEmoji } from 'flavours/glitch/actions/emojis';
 
 const perLine = 8;
 const lines   = 2;
diff --git a/app/javascript/themes/glitch/features/compose/containers/navigation_container.js b/app/javascript/flavours/glitch/features/compose/containers/navigation_container.js
index b6d737b46..eb630ffbb 100644
--- a/app/javascript/themes/glitch/features/compose/containers/navigation_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/navigation_container.js
@@ -1,6 +1,6 @@
 import { connect }   from 'react-redux';
 import NavigationBar from '../components/navigation_bar';
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 
 const mapStateToProps = state => {
   return {
diff --git a/app/javascript/themes/glitch/features/compose/containers/privacy_dropdown_container.js b/app/javascript/flavours/glitch/features/compose/containers/privacy_dropdown_container.js
index 9636ceab2..cb94fcc80 100644
--- a/app/javascript/themes/glitch/features/compose/containers/privacy_dropdown_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/privacy_dropdown_container.js
@@ -1,8 +1,8 @@
 import { connect } from 'react-redux';
 import PrivacyDropdown from '../components/privacy_dropdown';
-import { changeComposeVisibility } from 'themes/glitch/actions/compose';
-import { openModal, closeModal } from 'themes/glitch/actions/modal';
-import { isUserTouching } from 'themes/glitch/util/is_mobile';
+import { changeComposeVisibility } from 'flavours/glitch/actions/compose';
+import { openModal, closeModal } from 'flavours/glitch/actions/modal';
+import { isUserTouching } from 'flavours/glitch/util/is_mobile';
 
 const mapStateToProps = state => ({
   isModalOpen: state.get('modal').modalType === 'ACTIONS',
diff --git a/app/javascript/themes/glitch/features/compose/containers/reply_indicator_container.js b/app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js
index 6dcabb3cd..a7c82d135 100644
--- a/app/javascript/themes/glitch/features/compose/containers/reply_indicator_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
-import { cancelReplyCompose } from 'themes/glitch/actions/compose';
-import { makeGetStatus } from 'themes/glitch/selectors';
+import { cancelReplyCompose } from 'flavours/glitch/actions/compose';
+import { makeGetStatus } from 'flavours/glitch/selectors';
 import ReplyIndicator from '../components/reply_indicator';
 
 const makeMapStateToProps = () => {
diff --git a/app/javascript/themes/glitch/features/compose/containers/search_container.js b/app/javascript/flavours/glitch/features/compose/containers/search_container.js
index a450d27e7..8f4bfcf08 100644
--- a/app/javascript/themes/glitch/features/compose/containers/search_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/search_container.js
@@ -4,7 +4,7 @@ import {
   clearSearch,
   submitSearch,
   showSearch,
-} from 'themes/glitch/actions/search';
+} from 'flavours/glitch/actions/search';
 import Search from '../components/search';
 
 const mapStateToProps = state => ({
diff --git a/app/javascript/themes/glitch/features/compose/containers/search_results_container.js b/app/javascript/flavours/glitch/features/compose/containers/search_results_container.js
index 16d95d417..16d95d417 100644
--- a/app/javascript/themes/glitch/features/compose/containers/search_results_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/search_results_container.js
diff --git a/app/javascript/themes/glitch/features/compose/containers/sensitive_button_container.js b/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js
index a710dd104..cf6706c0e 100644
--- a/app/javascript/themes/glitch/features/compose/containers/sensitive_button_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js
@@ -2,9 +2,9 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
-import IconButton from 'themes/glitch/components/icon_button';
-import { changeComposeSensitivity } from 'themes/glitch/actions/compose';
-import Motion from 'themes/glitch/util/optional_motion';
+import IconButton from 'flavours/glitch/components/icon_button';
+import { changeComposeSensitivity } from 'flavours/glitch/actions/compose';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import { injectIntl, defineMessages } from 'react-intl';
 
diff --git a/app/javascript/themes/glitch/features/compose/containers/spoiler_button_container.js b/app/javascript/flavours/glitch/features/compose/containers/spoiler_button_container.js
index 160e71ba9..d7b4246bc 100644
--- a/app/javascript/themes/glitch/features/compose/containers/spoiler_button_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/spoiler_button_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
 import TextIconButton from '../components/text_icon_button';
-import { changeComposeSpoilerness } from 'themes/glitch/actions/compose';
+import { changeComposeSpoilerness } from 'flavours/glitch/actions/compose';
 import { injectIntl, defineMessages } from 'react-intl';
 
 const messages = defineMessages({
diff --git a/app/javascript/themes/glitch/features/compose/containers/upload_button_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_button_container.js
index f332eae1a..4c1cb49e9 100644
--- a/app/javascript/themes/glitch/features/compose/containers/upload_button_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/upload_button_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
 import UploadButton from '../components/upload_button';
-import { uploadCompose } from 'themes/glitch/actions/compose';
+import { uploadCompose } from 'flavours/glitch/actions/compose';
 
 const mapStateToProps = state => ({
   disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
diff --git a/app/javascript/themes/glitch/features/compose/containers/upload_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_container.js
index eea514bf5..368038425 100644
--- a/app/javascript/themes/glitch/features/compose/containers/upload_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/upload_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
 import Upload from '../components/upload';
-import { undoUploadCompose, changeUploadCompose } from 'themes/glitch/actions/compose';
+import { undoUploadCompose, changeUploadCompose } from 'flavours/glitch/actions/compose';
 
 const mapStateToProps = (state, { id }) => ({
   media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
diff --git a/app/javascript/themes/glitch/features/compose/containers/upload_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_form_container.js
index a6798bf51..a6798bf51 100644
--- a/app/javascript/themes/glitch/features/compose/containers/upload_form_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/upload_form_container.js
diff --git a/app/javascript/themes/glitch/features/compose/containers/upload_progress_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js
index 0cfee96da..0cfee96da 100644
--- a/app/javascript/themes/glitch/features/compose/containers/upload_progress_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js
diff --git a/app/javascript/themes/glitch/features/compose/containers/warning_container.js b/app/javascript/flavours/glitch/features/compose/containers/warning_container.js
index 225d6a1dd..f20c75b91 100644
--- a/app/javascript/themes/glitch/features/compose/containers/warning_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/warning_container.js
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
 import Warning from '../components/warning';
 import PropTypes from 'prop-types';
 import { FormattedMessage } from 'react-intl';
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 
 const mapStateToProps = state => ({
   needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
diff --git a/app/javascript/themes/glitch/features/compose/index.js b/app/javascript/flavours/glitch/features/compose/index.js
index 3fcaf416f..02b97ad00 100644
--- a/app/javascript/themes/glitch/features/compose/index.js
+++ b/app/javascript/flavours/glitch/features/compose/index.js
@@ -4,16 +4,16 @@ import NavigationContainer from './containers/navigation_container';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import { connect } from 'react-redux';
-import { mountCompose, unmountCompose } from 'themes/glitch/actions/compose';
-import { openModal } from 'themes/glitch/actions/modal';
-import { changeLocalSetting } from 'themes/glitch/actions/local_settings';
+import { mountCompose, unmountCompose } from 'flavours/glitch/actions/compose';
+import { openModal } from 'flavours/glitch/actions/modal';
+import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
 import { Link } from 'react-router-dom';
 import { injectIntl, defineMessages } from 'react-intl';
 import SearchContainer from './containers/search_container';
-import Motion from 'themes/glitch/util/optional_motion';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import SearchResultsContainer from './containers/search_results_container';
-import { changeComposing } from 'themes/glitch/actions/compose';
+import { changeComposing } from 'flavours/glitch/actions/compose';
 
 const messages = defineMessages({
   start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
diff --git a/app/javascript/themes/glitch/features/direct_timeline/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/direct_timeline/containers/column_settings_container.js
index 2a40c65a5..d3e4b4216 100644
--- a/app/javascript/themes/glitch/features/direct_timeline/containers/column_settings_container.js
+++ b/app/javascript/flavours/glitch/features/direct_timeline/containers/column_settings_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
-import ColumnSettings from 'themes/glitch/features/community_timeline/components/column_settings';
-import { changeSetting } from 'themes/glitch/actions/settings';
+import ColumnSettings from 'flavours/glitch/features/community_timeline/components/column_settings';
+import { changeSetting } from 'flavours/glitch/actions/settings';
 
 const mapStateToProps = state => ({
   settings: state.getIn(['settings', 'direct']),
diff --git a/app/javascript/themes/glitch/features/direct_timeline/index.js b/app/javascript/flavours/glitch/features/direct_timeline/index.js
index 6b29cf94d..81096c0ec 100644
--- a/app/javascript/themes/glitch/features/direct_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/direct_timeline/index.js
@@ -1,17 +1,17 @@
 import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
-import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container';
-import Column from 'themes/glitch/components/column';
-import ColumnHeader from 'themes/glitch/components/column_header';
+import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
 import {
   refreshDirectTimeline,
   expandDirectTimeline,
-} from 'themes/glitch/actions/timelines';
-import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns';
+} from 'flavours/glitch/actions/timelines';
+import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
-import { connectDirectStream } from 'themes/glitch/actions/streaming';
+import { connectDirectStream } from 'flavours/glitch/actions/streaming';
 
 const messages = defineMessages({
   title: { id: 'column.direct', defaultMessage: 'Direct messages' },
diff --git a/app/javascript/themes/glitch/features/favourited_statuses/index.js b/app/javascript/flavours/glitch/features/favourited_statuses/index.js
index 80345e0e2..e20dda718 100644
--- a/app/javascript/themes/glitch/features/favourited_statuses/index.js
+++ b/app/javascript/flavours/glitch/features/favourited_statuses/index.js
@@ -2,11 +2,11 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'themes/glitch/actions/favourites';
-import Column from 'themes/glitch/features/ui/components/column';
-import ColumnHeader from 'themes/glitch/components/column_header';
-import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns';
-import StatusList from 'themes/glitch/components/status_list';
+import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'flavours/glitch/actions/favourites';
+import Column from 'flavours/glitch/features/ui/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
+import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
+import StatusList from 'flavours/glitch/components/status_list';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
diff --git a/app/javascript/themes/glitch/features/favourites/index.js b/app/javascript/flavours/glitch/features/favourites/index.js
index d7b8ac3b1..055a15ccb 100644
--- a/app/javascript/themes/glitch/features/favourites/index.js
+++ b/app/javascript/flavours/glitch/features/favourites/index.js
@@ -2,12 +2,12 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import LoadingIndicator from 'themes/glitch/components/loading_indicator';
-import { fetchFavourites } from 'themes/glitch/actions/interactions';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
+import { fetchFavourites } from 'flavours/glitch/actions/interactions';
 import { ScrollContainer } from 'react-router-scroll-4';
-import AccountContainer from 'themes/glitch/containers/account_container';
-import Column from 'themes/glitch/features/ui/components/column';
-import ColumnBackButton from 'themes/glitch/components/column_back_button';
+import AccountContainer from 'flavours/glitch/containers/account_container';
+import Column from 'flavours/glitch/features/ui/components/column';
+import ColumnBackButton from 'flavours/glitch/components/column_back_button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const mapStateToProps = (state, props) => ({
diff --git a/app/javascript/themes/glitch/features/follow_requests/components/account_authorize.js b/app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.js
index ce386d888..dead0753f 100644
--- a/app/javascript/themes/glitch/features/follow_requests/components/account_authorize.js
+++ b/app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.js
@@ -1,10 +1,10 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import Permalink from 'themes/glitch/components/permalink';
-import Avatar from 'themes/glitch/components/avatar';
-import DisplayName from 'themes/glitch/components/display_name';
-import IconButton from 'themes/glitch/components/icon_button';
+import Permalink from 'flavours/glitch/components/permalink';
+import Avatar from 'flavours/glitch/components/avatar';
+import DisplayName from 'flavours/glitch/components/display_name';
+import IconButton from 'flavours/glitch/components/icon_button';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
diff --git a/app/javascript/themes/glitch/features/follow_requests/containers/account_authorize_container.js b/app/javascript/flavours/glitch/features/follow_requests/containers/account_authorize_container.js
index 78ae77eee..693e98e8c 100644
--- a/app/javascript/themes/glitch/features/follow_requests/containers/account_authorize_container.js
+++ b/app/javascript/flavours/glitch/features/follow_requests/containers/account_authorize_container.js
@@ -1,7 +1,7 @@
 import { connect } from 'react-redux';
-import { makeGetAccount } from 'themes/glitch/selectors';
+import { makeGetAccount } from 'flavours/glitch/selectors';
 import AccountAuthorize from '../components/account_authorize';
-import { authorizeFollowRequest, rejectFollowRequest } from 'themes/glitch/actions/accounts';
+import { authorizeFollowRequest, rejectFollowRequest } from 'flavours/glitch/actions/accounts';
 
 const makeMapStateToProps = () => {
   const getAccount = makeGetAccount();
diff --git a/app/javascript/themes/glitch/features/follow_requests/index.js b/app/javascript/flavours/glitch/features/follow_requests/index.js
index 3f44f518a..04ff3f111 100644
--- a/app/javascript/themes/glitch/features/follow_requests/index.js
+++ b/app/javascript/flavours/glitch/features/follow_requests/index.js
@@ -2,12 +2,12 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import LoadingIndicator from 'themes/glitch/components/loading_indicator';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
 import { ScrollContainer } from 'react-router-scroll-4';
-import Column from 'themes/glitch/features/ui/components/column';
-import ColumnBackButtonSlim from 'themes/glitch/components/column_back_button_slim';
+import Column from 'flavours/glitch/features/ui/components/column';
+import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim';
 import AccountAuthorizeContainer from './containers/account_authorize_container';
-import { fetchFollowRequests, expandFollowRequests } from 'themes/glitch/actions/accounts';
+import { fetchFollowRequests, expandFollowRequests } from 'flavours/glitch/actions/accounts';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
diff --git a/app/javascript/themes/glitch/features/followers/index.js b/app/javascript/flavours/glitch/features/followers/index.js
index d586bf41d..f0ef29ff6 100644
--- a/app/javascript/themes/glitch/features/followers/index.js
+++ b/app/javascript/flavours/glitch/features/followers/index.js
@@ -2,18 +2,18 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import LoadingIndicator from 'themes/glitch/components/loading_indicator';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
 import {
   fetchAccount,
   fetchFollowers,
   expandFollowers,
-} from 'themes/glitch/actions/accounts';
+} from 'flavours/glitch/actions/accounts';
 import { ScrollContainer } from 'react-router-scroll-4';
-import AccountContainer from 'themes/glitch/containers/account_container';
-import Column from 'themes/glitch/features/ui/components/column';
-import HeaderContainer from 'themes/glitch/features/account_timeline/containers/header_container';
-import LoadMore from 'themes/glitch/components/load_more';
-import ColumnBackButton from 'themes/glitch/components/column_back_button';
+import AccountContainer from 'flavours/glitch/containers/account_container';
+import Column from 'flavours/glitch/features/ui/components/column';
+import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container';
+import LoadMore from 'flavours/glitch/components/load_more';
+import ColumnBackButton from 'flavours/glitch/components/column_back_button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const mapStateToProps = (state, props) => ({
diff --git a/app/javascript/themes/glitch/features/following/index.js b/app/javascript/flavours/glitch/features/following/index.js
index c306faf21..f30f7b0d9 100644
--- a/app/javascript/themes/glitch/features/following/index.js
+++ b/app/javascript/flavours/glitch/features/following/index.js
@@ -2,18 +2,18 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import LoadingIndicator from 'themes/glitch/components/loading_indicator';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
 import {
   fetchAccount,
   fetchFollowing,
   expandFollowing,
-} from 'themes/glitch/actions/accounts';
+} from 'flavours/glitch/actions/accounts';
 import { ScrollContainer } from 'react-router-scroll-4';
-import AccountContainer from 'themes/glitch/containers/account_container';
-import Column from 'themes/glitch/features/ui/components/column';
-import HeaderContainer from 'themes/glitch/features/account_timeline/containers/header_container';
-import LoadMore from 'themes/glitch/components/load_more';
-import ColumnBackButton from 'themes/glitch/components/column_back_button';
+import AccountContainer from 'flavours/glitch/containers/account_container';
+import Column from 'flavours/glitch/features/ui/components/column';
+import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container';
+import LoadMore from 'flavours/glitch/components/load_more';
+import ColumnBackButton from 'flavours/glitch/components/column_back_button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const mapStateToProps = (state, props) => ({
diff --git a/app/javascript/flavours/glitch/features/generic_not_found/index.js b/app/javascript/flavours/glitch/features/generic_not_found/index.js
new file mode 100644
index 000000000..d01a1ba47
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/generic_not_found/index.js
@@ -0,0 +1,11 @@
+import React from 'react';
+import Column from 'flavours/glitch/features/ui/components/column';
+import MissingIndicator from 'flavours/glitch/components/missing_indicator';
+
+const GenericNotFound = () => (
+  <Column>
+    <MissingIndicator />
+  </Column>
+);
+
+export default GenericNotFound;
diff --git a/app/javascript/themes/glitch/features/getting_started/index.js b/app/javascript/flavours/glitch/features/getting_started/index.js
index 74b019cf1..2af0fcf48 100644
--- a/app/javascript/themes/glitch/features/getting_started/index.js
+++ b/app/javascript/flavours/glitch/features/getting_started/index.js
@@ -1,14 +1,14 @@
 import React from 'react';
-import Column from 'themes/glitch/features/ui/components/column';
-import ColumnLink from 'themes/glitch/features/ui/components/column_link';
-import ColumnSubheading from 'themes/glitch/features/ui/components/column_subheading';
+import Column from 'flavours/glitch/features/ui/components/column';
+import ColumnLink from 'flavours/glitch/features/ui/components/column_link';
+import ColumnSubheading from 'flavours/glitch/features/ui/components/column_subheading';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import { connect } from 'react-redux';
-import { openModal } from 'themes/glitch/actions/modal';
+import { openModal } from 'flavours/glitch/actions/modal';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
diff --git a/app/javascript/themes/glitch/features/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js
index a878931b3..9f3c9bec7 100644
--- a/app/javascript/themes/glitch/features/hashtag_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js
@@ -1,16 +1,16 @@
 import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
-import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container';
-import Column from 'themes/glitch/components/column';
-import ColumnHeader from 'themes/glitch/components/column_header';
+import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
 import {
   refreshHashtagTimeline,
   expandHashtagTimeline,
-} from 'themes/glitch/actions/timelines';
-import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns';
+} from 'flavours/glitch/actions/timelines';
+import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
 import { FormattedMessage } from 'react-intl';
-import { connectHashtagStream } from 'themes/glitch/actions/streaming';
+import { connectHashtagStream } from 'flavours/glitch/actions/streaming';
 
 const mapStateToProps = (state, props) => ({
   hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0,
diff --git a/app/javascript/themes/glitch/features/home_timeline/components/column_settings.js b/app/javascript/flavours/glitch/features/home_timeline/components/column_settings.js
index 533da6c36..3ff0a1daf 100644
--- a/app/javascript/themes/glitch/features/home_timeline/components/column_settings.js
+++ b/app/javascript/flavours/glitch/features/home_timeline/components/column_settings.js
@@ -2,8 +2,8 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import SettingToggle from 'themes/glitch/features/notifications/components/setting_toggle';
-import SettingText from 'themes/glitch/components/setting_text';
+import SettingToggle from 'flavours/glitch/features/notifications/components/setting_toggle';
+import SettingText from 'flavours/glitch/components/setting_text';
 
 const messages = defineMessages({
   filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' },
@@ -34,6 +34,10 @@ export default class ColumnSettings extends React.PureComponent {
           <SettingToggle prefix='home_timeline' settings={settings} settingKey={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
         </div>
 
+        <div className='column-settings__row'>
+          <SettingToggle prefix='home_timeline' settings={settings} settingKey={['shows', 'direct']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_direct' defaultMessage='Show DMs' />} />
+        </div>
+
         <span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
 
         <div className='column-settings__row'>
diff --git a/app/javascript/themes/glitch/features/home_timeline/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/home_timeline/containers/column_settings_container.js
index a0062f564..19a8e792f 100644
--- a/app/javascript/themes/glitch/features/home_timeline/containers/column_settings_container.js
+++ b/app/javascript/flavours/glitch/features/home_timeline/containers/column_settings_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
 import ColumnSettings from '../components/column_settings';
-import { changeSetting, saveSettings } from 'themes/glitch/actions/settings';
+import { changeSetting, saveSettings } from 'flavours/glitch/actions/settings';
 
 const mapStateToProps = state => ({
   settings: state.getIn(['settings', 'home']),
diff --git a/app/javascript/themes/glitch/features/home_timeline/index.js b/app/javascript/flavours/glitch/features/home_timeline/index.js
index 8a65891cd..2dfec6bbe 100644
--- a/app/javascript/themes/glitch/features/home_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/home_timeline/index.js
@@ -1,11 +1,11 @@
 import React from 'react';
 import { connect } from 'react-redux';
-import { expandHomeTimeline } from 'themes/glitch/actions/timelines';
+import { expandHomeTimeline } from 'flavours/glitch/actions/timelines';
 import PropTypes from 'prop-types';
-import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container';
-import Column from 'themes/glitch/components/column';
-import ColumnHeader from 'themes/glitch/components/column_header';
-import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns';
+import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
+import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
 import { Link } from 'react-router-dom';
diff --git a/app/javascript/themes/glitch/features/local_settings/index.js b/app/javascript/flavours/glitch/features/local_settings/index.js
index 6c5d51413..4e4605ea9 100644
--- a/app/javascript/themes/glitch/features/local_settings/index.js
+++ b/app/javascript/flavours/glitch/features/local_settings/index.js
@@ -7,11 +7,8 @@ import { connect } from 'react-redux';
 //  Our imports
 import LocalSettingsPage from './page';
 import LocalSettingsNavigation from './navigation';
-import { closeModal } from 'themes/glitch/actions/modal';
-import { changeLocalSetting } from 'themes/glitch/actions/local_settings';
-
-//  Stylesheet imports
-import './style.scss';
+import { closeModal } from 'flavours/glitch/actions/modal';
+import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
 
 const mapStateToProps = state => ({
   settings: state.get('local_settings'),
diff --git a/app/javascript/themes/glitch/features/local_settings/navigation/index.js b/app/javascript/flavours/glitch/features/local_settings/navigation/index.js
index fa35e83c7..fc2167c0c 100644
--- a/app/javascript/themes/glitch/features/local_settings/navigation/index.js
+++ b/app/javascript/flavours/glitch/features/local_settings/navigation/index.js
@@ -6,9 +6,6 @@ import { injectIntl, defineMessages } from 'react-intl';
 //  Our imports
 import LocalSettingsNavigationItem from './item';
 
-//  Stylesheet imports
-import './style.scss';
-
 //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 
 const messages = defineMessages({
diff --git a/app/javascript/themes/glitch/features/local_settings/navigation/item/index.js b/app/javascript/flavours/glitch/features/local_settings/navigation/item/index.js
index a352d5fb2..b67d479e7 100644
--- a/app/javascript/themes/glitch/features/local_settings/navigation/item/index.js
+++ b/app/javascript/flavours/glitch/features/local_settings/navigation/item/index.js
@@ -3,9 +3,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
 
-//  Stylesheet imports
-import './style.scss';
-
 //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 
 export default class LocalSettingsPage extends React.PureComponent {
diff --git a/app/javascript/themes/glitch/features/local_settings/page/index.js b/app/javascript/flavours/glitch/features/local_settings/page/index.js
index 498230f7b..62bf410c6 100644
--- a/app/javascript/themes/glitch/features/local_settings/page/index.js
+++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js
@@ -7,9 +7,6 @@ import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
 //  Our imports
 import LocalSettingsPageItem from './item';
 
-//  Stylesheet imports
-import './style.scss';
-
 //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 
 const messages = defineMessages({
diff --git a/app/javascript/themes/glitch/features/local_settings/page/item/index.js b/app/javascript/flavours/glitch/features/local_settings/page/item/index.js
index 37e28c084..66e84dfe1 100644
--- a/app/javascript/themes/glitch/features/local_settings/page/item/index.js
+++ b/app/javascript/flavours/glitch/features/local_settings/page/item/index.js
@@ -3,9 +3,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 
-//  Stylesheet imports
-import './style.scss';
-
 //  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 
 export default class LocalSettingsPageItem extends React.PureComponent {
diff --git a/app/javascript/themes/glitch/features/mutes/index.js b/app/javascript/flavours/glitch/features/mutes/index.js
index 1158b8262..87517eec9 100644
--- a/app/javascript/themes/glitch/features/mutes/index.js
+++ b/app/javascript/flavours/glitch/features/mutes/index.js
@@ -2,12 +2,12 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import LoadingIndicator from 'themes/glitch/components/loading_indicator';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
 import { ScrollContainer } from 'react-router-scroll-4';
-import Column from 'themes/glitch/features/ui/components/column';
-import ColumnBackButtonSlim from 'themes/glitch/components/column_back_button_slim';
-import AccountContainer from 'themes/glitch/containers/account_container';
-import { fetchMutes, expandMutes } from 'themes/glitch/actions/mutes';
+import Column from 'flavours/glitch/features/ui/components/column';
+import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim';
+import AccountContainer from 'flavours/glitch/containers/account_container';
+import { fetchMutes, expandMutes } from 'flavours/glitch/actions/mutes';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
diff --git a/app/javascript/themes/glitch/features/notifications/components/clear_column_button.js b/app/javascript/flavours/glitch/features/notifications/components/clear_column_button.js
index 22a10753f..22a10753f 100644
--- a/app/javascript/themes/glitch/features/notifications/components/clear_column_button.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/clear_column_button.js
diff --git a/app/javascript/themes/glitch/features/notifications/components/column_settings.js b/app/javascript/flavours/glitch/features/notifications/components/column_settings.js
index 88a29d4d3..88a29d4d3 100644
--- a/app/javascript/themes/glitch/features/notifications/components/column_settings.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/column_settings.js
diff --git a/app/javascript/themes/glitch/features/notifications/components/follow.js b/app/javascript/flavours/glitch/features/notifications/components/follow.js
index 96cfe83e6..54506f67c 100644
--- a/app/javascript/themes/glitch/features/notifications/components/follow.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/follow.js
@@ -7,8 +7,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
 import { HotKeys } from 'react-hotkeys';
 
 // Our imports.
-import Permalink from 'themes/glitch/components/permalink';
-import AccountContainer from 'themes/glitch/containers/account_container';
+import Permalink from 'flavours/glitch/components/permalink';
+import AccountContainer from 'flavours/glitch/containers/account_container';
 import NotificationOverlayContainer from '../containers/overlay_container';
 
 export default class NotificationFollow extends ImmutablePureComponent {
diff --git a/app/javascript/themes/glitch/features/notifications/components/notification.js b/app/javascript/flavours/glitch/features/notifications/components/notification.js
index 47f5770bf..cc77426d3 100644
--- a/app/javascript/themes/glitch/features/notifications/components/notification.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/notification.js
@@ -5,7 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 //  Our imports,
-import StatusContainer from 'themes/glitch/containers/status_container';
+import StatusContainer from 'flavours/glitch/containers/status_container';
 import NotificationFollow from './follow';
 
 export default class Notification extends ImmutablePureComponent {
diff --git a/app/javascript/themes/glitch/features/notifications/components/overlay.js b/app/javascript/flavours/glitch/features/notifications/components/overlay.js
index e56f9c628..e56f9c628 100644
--- a/app/javascript/themes/glitch/features/notifications/components/overlay.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/overlay.js
diff --git a/app/javascript/themes/glitch/features/notifications/components/setting_toggle.js b/app/javascript/flavours/glitch/features/notifications/components/setting_toggle.js
index 281359d2a..281359d2a 100644
--- a/app/javascript/themes/glitch/features/notifications/components/setting_toggle.js
+++ b/app/javascript/flavours/glitch/features/notifications/components/setting_toggle.js
diff --git a/app/javascript/themes/glitch/features/notifications/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js
index ddc8495f4..ce502700c 100644
--- a/app/javascript/themes/glitch/features/notifications/containers/column_settings_container.js
+++ b/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js
@@ -1,10 +1,10 @@
 import { connect } from 'react-redux';
 import { defineMessages, injectIntl } from 'react-intl';
 import ColumnSettings from '../components/column_settings';
-import { changeSetting, saveSettings } from 'themes/glitch/actions/settings';
-import { clearNotifications } from 'themes/glitch/actions/notifications';
-import { changeAlerts as changePushNotifications, saveSettings as savePushNotificationSettings } from 'themes/glitch/actions/push_notifications';
-import { openModal } from 'themes/glitch/actions/modal';
+import { changeSetting, saveSettings } from 'flavours/glitch/actions/settings';
+import { clearNotifications } from 'flavours/glitch/actions/notifications';
+import { changeAlerts as changePushNotifications, saveSettings as savePushNotificationSettings } from 'flavours/glitch/actions/push_notifications';
+import { openModal } from 'flavours/glitch/actions/modal';
 
 const messages = defineMessages({
   clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' },
diff --git a/app/javascript/themes/glitch/features/notifications/containers/notification_container.js b/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js
index 033c61e3d..be007f30b 100644
--- a/app/javascript/themes/glitch/features/notifications/containers/notification_container.js
+++ b/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js
@@ -2,9 +2,9 @@
 import { connect } from 'react-redux';
 
 //  Our imports.
-import { makeGetNotification } from 'themes/glitch/selectors';
+import { makeGetNotification } from 'flavours/glitch/selectors';
 import Notification from '../components/notification';
-import { mentionCompose } from 'themes/glitch/actions/compose';
+import { mentionCompose } from 'flavours/glitch/actions/compose';
 
 const makeMapStateToProps = () => {
   const getNotification = makeGetNotification();
diff --git a/app/javascript/themes/glitch/features/notifications/containers/overlay_container.js b/app/javascript/flavours/glitch/features/notifications/containers/overlay_container.js
index 52649cdd7..ee2d19814 100644
--- a/app/javascript/themes/glitch/features/notifications/containers/overlay_container.js
+++ b/app/javascript/flavours/glitch/features/notifications/containers/overlay_container.js
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
 
 //  Our imports.
 import NotificationOverlay from '../components/overlay';
-import { markNotificationForDelete } from 'themes/glitch/actions/notifications';
+import { markNotificationForDelete } from 'flavours/glitch/actions/notifications';
 
 const mapDispatchToProps = dispatch => ({
   onMarkForDelete(id, yes) {
diff --git a/app/javascript/themes/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js
index 1ecde660a..12b0b5b83 100644
--- a/app/javascript/themes/glitch/features/notifications/index.js
+++ b/app/javascript/flavours/glitch/features/notifications/index.js
@@ -2,21 +2,21 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import Column from 'themes/glitch/components/column';
-import ColumnHeader from 'themes/glitch/components/column_header';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
 import {
   enterNotificationClearingMode,
   expandNotifications,
   scrollTopNotifications,
-} from 'themes/glitch/actions/notifications';
-import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns';
+} from 'flavours/glitch/actions/notifications';
+import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
 import NotificationContainer from './containers/notification_container';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
 import { createSelector } from 'reselect';
 import { List as ImmutableList } from 'immutable';
 import { debounce } from 'lodash';
-import ScrollableList from 'themes/glitch/components/scrollable_list';
+import ScrollableList from 'flavours/glitch/components/scrollable_list';
 
 const messages = defineMessages({
   title: { id: 'column.notifications', defaultMessage: 'Notifications' },
diff --git a/app/javascript/themes/glitch/features/pinned_statuses/index.js b/app/javascript/flavours/glitch/features/pinned_statuses/index.js
index 0a3997850..f56d70176 100644
--- a/app/javascript/themes/glitch/features/pinned_statuses/index.js
+++ b/app/javascript/flavours/glitch/features/pinned_statuses/index.js
@@ -2,10 +2,10 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import { fetchPinnedStatuses } from 'themes/glitch/actions/pin_statuses';
-import Column from 'themes/glitch/features/ui/components/column';
-import ColumnBackButtonSlim from 'themes/glitch/components/column_back_button_slim';
-import StatusList from 'themes/glitch/components/status_list';
+import { fetchPinnedStatuses } from 'flavours/glitch/actions/pin_statuses';
+import Column from 'flavours/glitch/features/ui/components/column';
+import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim';
+import StatusList from 'flavours/glitch/components/status_list';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
diff --git a/app/javascript/themes/glitch/features/public_timeline/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/public_timeline/containers/column_settings_container.js
index 0185a7724..b13e20645 100644
--- a/app/javascript/themes/glitch/features/public_timeline/containers/column_settings_container.js
+++ b/app/javascript/flavours/glitch/features/public_timeline/containers/column_settings_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
-import ColumnSettings from 'themes/glitch/features/community_timeline/components/column_settings';
-import { changeSetting } from 'themes/glitch/actions/settings';
+import ColumnSettings from 'flavours/glitch/features/community_timeline/components/column_settings';
+import { changeSetting } from 'flavours/glitch/actions/settings';
 
 const mapStateToProps = state => ({
   settings: state.getIn(['settings', 'public']),
diff --git a/app/javascript/themes/glitch/features/public_timeline/index.js b/app/javascript/flavours/glitch/features/public_timeline/index.js
index f5b3865af..bbdd4612e 100644
--- a/app/javascript/themes/glitch/features/public_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/public_timeline/index.js
@@ -1,17 +1,17 @@
 import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
-import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container';
-import Column from 'themes/glitch/components/column';
-import ColumnHeader from 'themes/glitch/components/column_header';
+import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
 import {
   refreshPublicTimeline,
   expandPublicTimeline,
-} from 'themes/glitch/actions/timelines';
-import { addColumn, removeColumn, moveColumn } from 'themes/glitch/actions/columns';
+} from 'flavours/glitch/actions/timelines';
+import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
-import { connectPublicStream } from 'themes/glitch/actions/streaming';
+import { connectPublicStream } from 'flavours/glitch/actions/streaming';
 
 const messages = defineMessages({
   title: { id: 'column.public', defaultMessage: 'Federated timeline' },
diff --git a/app/javascript/themes/glitch/features/reblogs/index.js b/app/javascript/flavours/glitch/features/reblogs/index.js
index 8723f7c7c..25b792b39 100644
--- a/app/javascript/themes/glitch/features/reblogs/index.js
+++ b/app/javascript/flavours/glitch/features/reblogs/index.js
@@ -2,12 +2,12 @@ import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import LoadingIndicator from 'themes/glitch/components/loading_indicator';
-import { fetchReblogs } from 'themes/glitch/actions/interactions';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
+import { fetchReblogs } from 'flavours/glitch/actions/interactions';
 import { ScrollContainer } from 'react-router-scroll-4';
-import AccountContainer from 'themes/glitch/containers/account_container';
-import Column from 'themes/glitch/features/ui/components/column';
-import ColumnBackButton from 'themes/glitch/components/column_back_button';
+import AccountContainer from 'flavours/glitch/containers/account_container';
+import Column from 'flavours/glitch/features/ui/components/column';
+import ColumnBackButton from 'flavours/glitch/components/column_back_button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const mapStateToProps = (state, props) => ({
diff --git a/app/javascript/themes/glitch/features/report/components/status_check_box.js b/app/javascript/flavours/glitch/features/report/components/status_check_box.js
index cc9232201..cc9232201 100644
--- a/app/javascript/themes/glitch/features/report/components/status_check_box.js
+++ b/app/javascript/flavours/glitch/features/report/components/status_check_box.js
diff --git a/app/javascript/themes/glitch/features/report/containers/status_check_box_container.js b/app/javascript/flavours/glitch/features/report/containers/status_check_box_container.js
index 40d55fb3c..9bfd41ffc 100644
--- a/app/javascript/themes/glitch/features/report/containers/status_check_box_container.js
+++ b/app/javascript/flavours/glitch/features/report/containers/status_check_box_container.js
@@ -1,6 +1,6 @@
 import { connect } from 'react-redux';
 import StatusCheckBox from '../components/status_check_box';
-import { toggleStatusReport } from 'themes/glitch/actions/reports';
+import { toggleStatusReport } from 'flavours/glitch/actions/reports';
 import { Set as ImmutableSet } from 'immutable';
 
 const mapStateToProps = (state, { id }) => ({
diff --git a/app/javascript/flavours/glitch/features/standalone/compose/index.js b/app/javascript/flavours/glitch/features/standalone/compose/index.js
new file mode 100644
index 000000000..b33c21cb5
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/standalone/compose/index.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import ComposeFormContainer from 'flavours/glitch/features/compose/containers/compose_form_container';
+import NotificationsContainer from 'flavours/glitch/features/ui/containers/notifications_container';
+import LoadingBarContainer from 'flavours/glitch/features/ui/containers/loading_bar_container';
+import ModalContainer from 'flavours/glitch/features/ui/containers/modal_container';
+
+export default class Compose extends React.PureComponent {
+
+  render () {
+    return (
+      <div>
+        <ComposeFormContainer />
+        <NotificationsContainer />
+        <ModalContainer />
+        <LoadingBarContainer className='loading-bar' />
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/themes/glitch/features/standalone/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js
index 7c56f264f..0ad2cef80 100644
--- a/app/javascript/themes/glitch/features/standalone/hashtag_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js
@@ -1,13 +1,13 @@
 import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
-import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container';
+import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
 import {
   refreshHashtagTimeline,
   expandHashtagTimeline,
-} from 'themes/glitch/actions/timelines';
-import Column from 'themes/glitch/components/column';
-import ColumnHeader from 'themes/glitch/components/column_header';
+} from 'flavours/glitch/actions/timelines';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
 
 @connect()
 export default class HashtagTimeline extends React.PureComponent {
diff --git a/app/javascript/themes/glitch/features/standalone/public_timeline/index.js b/app/javascript/flavours/glitch/features/standalone/public_timeline/index.js
index b3fb55288..717f6fcaf 100644
--- a/app/javascript/themes/glitch/features/standalone/public_timeline/index.js
+++ b/app/javascript/flavours/glitch/features/standalone/public_timeline/index.js
@@ -1,13 +1,13 @@
 import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
-import StatusListContainer from 'themes/glitch/features/ui/containers/status_list_container';
+import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
 import {
   refreshPublicTimeline,
   expandPublicTimeline,
-} from 'themes/glitch/actions/timelines';
-import Column from 'themes/glitch/components/column';
-import ColumnHeader from 'themes/glitch/components/column_header';
+} from 'flavours/glitch/actions/timelines';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
 import { defineMessages, injectIntl } from 'react-intl';
 
 const messages = defineMessages({
diff --git a/app/javascript/themes/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js
index 6cda988d1..4d660ee3c 100644
--- a/app/javascript/themes/glitch/features/status/components/action_bar.js
+++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js
@@ -1,10 +1,10 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import IconButton from 'themes/glitch/components/icon_button';
+import IconButton from 'flavours/glitch/components/icon_button';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import DropdownMenuContainer from 'themes/glitch/containers/dropdown_menu_container';
+import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
 import { defineMessages, injectIntl } from 'react-intl';
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 
 const messages = defineMessages({
   delete: { id: 'status.delete', defaultMessage: 'Delete' },
diff --git a/app/javascript/themes/glitch/features/status/components/card.js b/app/javascript/flavours/glitch/features/status/components/card.js
index bb83374b9..bb83374b9 100644
--- a/app/javascript/themes/glitch/features/status/components/card.js
+++ b/app/javascript/flavours/glitch/features/status/components/card.js
diff --git a/app/javascript/themes/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
index df78ce4b6..0cb5238b0 100644
--- a/app/javascript/themes/glitch/features/status/components/detailed_status.js
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
@@ -1,17 +1,17 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import Avatar from 'themes/glitch/components/avatar';
-import DisplayName from 'themes/glitch/components/display_name';
-import StatusContent from 'themes/glitch/components/status_content';
-import StatusGallery from 'themes/glitch/components/media_gallery';
-import AttachmentList from 'themes/glitch/components/attachment_list';
+import Avatar from 'flavours/glitch/components/avatar';
+import DisplayName from 'flavours/glitch/components/display_name';
+import StatusContent from 'flavours/glitch/components/status_content';
+import StatusGallery from 'flavours/glitch/components/media_gallery';
+import AttachmentList from 'flavours/glitch/components/attachment_list';
 import { Link } from 'react-router-dom';
 import { FormattedDate, FormattedNumber } from 'react-intl';
 import CardContainer from '../containers/card_container';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import Video from 'themes/glitch/features/video';
-import VisibilityIcon from 'themes/glitch/components/status_visibility_icon';
+import Video from 'flavours/glitch/features/video';
+import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon';
 
 export default class DetailedStatus extends ImmutablePureComponent {
 
diff --git a/app/javascript/themes/glitch/features/status/containers/card_container.js b/app/javascript/flavours/glitch/features/status/containers/card_container.js
index a97404de1..a97404de1 100644
--- a/app/javascript/themes/glitch/features/status/containers/card_container.js
+++ b/app/javascript/flavours/glitch/features/status/containers/card_container.js
diff --git a/app/javascript/themes/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js
index 8561bd4de..93b0fe9d9 100644
--- a/app/javascript/themes/glitch/features/status/index.js
+++ b/app/javascript/flavours/glitch/features/status/index.js
@@ -3,11 +3,11 @@ import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import { fetchStatus } from 'themes/glitch/actions/statuses';
-import MissingIndicator from 'themes/glitch/components/missing_indicator';
+import { fetchStatus } from 'flavours/glitch/actions/statuses';
+import MissingIndicator from 'flavours/glitch/components/missing_indicator';
 import DetailedStatus from './components/detailed_status';
 import ActionBar from './components/action_bar';
-import Column from 'themes/glitch/features/ui/components/column';
+import Column from 'flavours/glitch/features/ui/components/column';
 import {
   favourite,
   unfavourite,
@@ -15,23 +15,23 @@ import {
   unreblog,
   pin,
   unpin,
-} from 'themes/glitch/actions/interactions';
+} from 'flavours/glitch/actions/interactions';
 import {
   replyCompose,
   mentionCompose,
-} from 'themes/glitch/actions/compose';
-import { deleteStatus } from 'themes/glitch/actions/statuses';
-import { initReport } from 'themes/glitch/actions/reports';
-import { makeGetStatus } from 'themes/glitch/selectors';
+} from 'flavours/glitch/actions/compose';
+import { deleteStatus } from 'flavours/glitch/actions/statuses';
+import { initReport } from 'flavours/glitch/actions/reports';
+import { makeGetStatus } from 'flavours/glitch/selectors';
 import { ScrollContainer } from 'react-router-scroll-4';
-import ColumnBackButton from 'themes/glitch/components/column_back_button';
-import StatusContainer from 'themes/glitch/containers/status_container';
-import { openModal } from 'themes/glitch/actions/modal';
+import ColumnBackButton from 'flavours/glitch/components/column_back_button';
+import StatusContainer from 'flavours/glitch/containers/status_container';
+import { openModal } from 'flavours/glitch/actions/modal';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { HotKeys } from 'react-hotkeys';
-import { boostModal, deleteModal } from 'themes/glitch/util/initial_state';
-import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'themes/glitch/util/fullscreen';
+import { boostModal, deleteModal } from 'flavours/glitch/util/initial_state';
+import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen';
 
 const messages = defineMessages({
   deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
diff --git a/app/javascript/themes/glitch/features/ui/components/actions_modal.js b/app/javascript/flavours/glitch/features/ui/components/actions_modal.js
index 7a2b78b63..0873c282f 100644
--- a/app/javascript/themes/glitch/features/ui/components/actions_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/actions_modal.js
@@ -2,11 +2,11 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import StatusContent from 'themes/glitch/components/status_content';
-import Avatar from 'themes/glitch/components/avatar';
-import RelativeTimestamp from 'themes/glitch/components/relative_timestamp';
-import DisplayName from 'themes/glitch/components/display_name';
-import IconButton from 'themes/glitch/components/icon_button';
+import StatusContent from 'flavours/glitch/components/status_content';
+import Avatar from 'flavours/glitch/components/avatar';
+import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
+import DisplayName from 'flavours/glitch/components/display_name';
+import IconButton from 'flavours/glitch/components/icon_button';
 import classNames from 'classnames';
 
 export default class ActionsModal extends ImmutablePureComponent {
diff --git a/app/javascript/themes/glitch/features/ui/components/boost_modal.js b/app/javascript/flavours/glitch/features/ui/components/boost_modal.js
index 49781db10..9652bcb2d 100644
--- a/app/javascript/themes/glitch/features/ui/components/boost_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/boost_modal.js
@@ -2,11 +2,11 @@ import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import Button from 'themes/glitch/components/button';
-import StatusContent from 'themes/glitch/components/status_content';
-import Avatar from 'themes/glitch/components/avatar';
-import RelativeTimestamp from 'themes/glitch/components/relative_timestamp';
-import DisplayName from 'themes/glitch/components/display_name';
+import Button from 'flavours/glitch/components/button';
+import StatusContent from 'flavours/glitch/components/status_content';
+import Avatar from 'flavours/glitch/components/avatar';
+import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
+import DisplayName from 'flavours/glitch/components/display_name';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const messages = defineMessages({
diff --git a/app/javascript/themes/glitch/features/ui/components/bundle.js b/app/javascript/flavours/glitch/features/ui/components/bundle.js
index fc88e0c70..fc88e0c70 100644
--- a/app/javascript/themes/glitch/features/ui/components/bundle.js
+++ b/app/javascript/flavours/glitch/features/ui/components/bundle.js
diff --git a/app/javascript/themes/glitch/features/ui/components/bundle_column_error.js b/app/javascript/flavours/glitch/features/ui/components/bundle_column_error.js
index daedc6299..3e979a250 100644
--- a/app/javascript/themes/glitch/features/ui/components/bundle_column_error.js
+++ b/app/javascript/flavours/glitch/features/ui/components/bundle_column_error.js
@@ -4,8 +4,8 @@ import { defineMessages, injectIntl } from 'react-intl';
 
 import Column from './column';
 import ColumnHeader from './column_header';
-import ColumnBackButtonSlim from 'themes/glitch/components/column_back_button_slim';
-import IconButton from 'themes/glitch/components/icon_button';
+import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim';
+import IconButton from 'flavours/glitch/components/icon_button';
 
 const messages = defineMessages({
   title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' },
diff --git a/app/javascript/themes/glitch/features/ui/components/bundle_modal_error.js b/app/javascript/flavours/glitch/features/ui/components/bundle_modal_error.js
index 8cca32ae9..2c14a1e5c 100644
--- a/app/javascript/themes/glitch/features/ui/components/bundle_modal_error.js
+++ b/app/javascript/flavours/glitch/features/ui/components/bundle_modal_error.js
@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl } from 'react-intl';
 
-import IconButton from 'themes/glitch/components/icon_button';
+import IconButton from 'flavours/glitch/components/icon_button';
 
 const messages = defineMessages({
   error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this component.' },
diff --git a/app/javascript/themes/glitch/features/ui/components/column.js b/app/javascript/flavours/glitch/features/ui/components/column.js
index 73a5bc15e..ab78414e0 100644
--- a/app/javascript/themes/glitch/features/ui/components/column.js
+++ b/app/javascript/flavours/glitch/features/ui/components/column.js
@@ -2,8 +2,8 @@ import React from 'react';
 import ColumnHeader from './column_header';
 import PropTypes from 'prop-types';
 import { debounce } from 'lodash';
-import { scrollTop } from 'themes/glitch/util/scroll';
-import { isMobile } from 'themes/glitch/util/is_mobile';
+import { scrollTop } from 'flavours/glitch/util/scroll';
+import { isMobile } from 'flavours/glitch/util/is_mobile';
 
 export default class Column extends React.PureComponent {
 
diff --git a/app/javascript/themes/glitch/features/ui/components/column_header.js b/app/javascript/flavours/glitch/features/ui/components/column_header.js
index af195ea9c..af195ea9c 100644
--- a/app/javascript/themes/glitch/features/ui/components/column_header.js
+++ b/app/javascript/flavours/glitch/features/ui/components/column_header.js
diff --git a/app/javascript/themes/glitch/features/ui/components/column_link.js b/app/javascript/flavours/glitch/features/ui/components/column_link.js
index b845d1895..b845d1895 100644
--- a/app/javascript/themes/glitch/features/ui/components/column_link.js
+++ b/app/javascript/flavours/glitch/features/ui/components/column_link.js
diff --git a/app/javascript/themes/glitch/features/ui/components/column_loading.js b/app/javascript/flavours/glitch/features/ui/components/column_loading.js
index 75f26218a..ba2d0824e 100644
--- a/app/javascript/themes/glitch/features/ui/components/column_loading.js
+++ b/app/javascript/flavours/glitch/features/ui/components/column_loading.js
@@ -1,8 +1,8 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-import Column from 'themes/glitch/components/column';
-import ColumnHeader from 'themes/glitch/components/column_header';
+import Column from 'flavours/glitch/components/column';
+import ColumnHeader from 'flavours/glitch/components/column_header';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 export default class ColumnLoading extends ImmutablePureComponent {
diff --git a/app/javascript/themes/glitch/features/ui/components/column_subheading.js b/app/javascript/flavours/glitch/features/ui/components/column_subheading.js
index 8160c4aa3..8160c4aa3 100644
--- a/app/javascript/themes/glitch/features/ui/components/column_subheading.js
+++ b/app/javascript/flavours/glitch/features/ui/components/column_subheading.js
diff --git a/app/javascript/themes/glitch/features/ui/components/columns_area.js b/app/javascript/flavours/glitch/features/ui/components/columns_area.js
index 452950363..264f60724 100644
--- a/app/javascript/themes/glitch/features/ui/components/columns_area.js
+++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js
@@ -11,10 +11,10 @@ import BundleContainer from '../containers/bundle_container';
 import ColumnLoading from './column_loading';
 import DrawerLoading from './drawer_loading';
 import BundleColumnError from './bundle_column_error';
-import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses } from 'themes/glitch/util/async-components';
+import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses } from 'flavours/glitch/util/async-components';
 
 import detectPassiveEvents from 'detect-passive-events';
-import { scrollRight } from 'themes/glitch/util/scroll';
+import { scrollRight } from 'flavours/glitch/util/scroll';
 
 const componentMap = {
   'COMPOSE': Compose,
diff --git a/app/javascript/themes/glitch/features/ui/components/confirmation_modal.js b/app/javascript/flavours/glitch/features/ui/components/confirmation_modal.js
index 3d568aec3..d4d1e587e 100644
--- a/app/javascript/themes/glitch/features/ui/components/confirmation_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/confirmation_modal.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import { injectIntl, FormattedMessage } from 'react-intl';
-import Button from 'themes/glitch/components/button';
+import Button from 'flavours/glitch/components/button';
 
 @injectIntl
 export default class ConfirmationModal extends React.PureComponent {
diff --git a/app/javascript/themes/glitch/features/ui/components/doodle_modal.js b/app/javascript/flavours/glitch/features/ui/components/doodle_modal.js
index 819656dbf..9c74451b3 100644
--- a/app/javascript/themes/glitch/features/ui/components/doodle_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/doodle_modal.js
@@ -1,12 +1,12 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import Button from 'themes/glitch/components/button';
+import Button from 'flavours/glitch/components/button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import Atrament from 'atrament'; // the doodling library
 import { connect } from 'react-redux';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import { doodleSet, uploadCompose } from 'themes/glitch/actions/compose';
-import IconButton from 'themes/glitch/components/icon_button';
+import { doodleSet, uploadCompose } from 'flavours/glitch/actions/compose';
+import IconButton from 'flavours/glitch/components/icon_button';
 import { debounce, mapValues } from 'lodash';
 import classNames from 'classnames';
 
diff --git a/app/javascript/themes/glitch/features/ui/components/drawer_loading.js b/app/javascript/flavours/glitch/features/ui/components/drawer_loading.js
index 08b0d2347..08b0d2347 100644
--- a/app/javascript/themes/glitch/features/ui/components/drawer_loading.js
+++ b/app/javascript/flavours/glitch/features/ui/components/drawer_loading.js
diff --git a/app/javascript/themes/glitch/features/ui/components/embed_modal.js b/app/javascript/flavours/glitch/features/ui/components/embed_modal.js
index 1afffb51b..1afffb51b 100644
--- a/app/javascript/themes/glitch/features/ui/components/embed_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/embed_modal.js
diff --git a/app/javascript/themes/glitch/features/ui/components/image_loader.js b/app/javascript/flavours/glitch/features/ui/components/image_loader.js
index aad594380..aad594380 100644
--- a/app/javascript/themes/glitch/features/ui/components/image_loader.js
+++ b/app/javascript/flavours/glitch/features/ui/components/image_loader.js
diff --git a/app/javascript/themes/glitch/features/ui/components/media_modal.js b/app/javascript/flavours/glitch/features/ui/components/media_modal.js
index 1dad972b2..e56147c5b 100644
--- a/app/javascript/themes/glitch/features/ui/components/media_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.js
@@ -2,9 +2,9 @@ import React from 'react';
 import ReactSwipeableViews from 'react-swipeable-views';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import ExtendedVideoPlayer from 'themes/glitch/components/extended_video_player';
+import ExtendedVideoPlayer from 'flavours/glitch/components/extended_video_player';
 import { defineMessages, injectIntl } from 'react-intl';
-import IconButton from 'themes/glitch/components/icon_button';
+import IconButton from 'flavours/glitch/components/icon_button';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import ImageLoader from './image_loader';
 
diff --git a/app/javascript/themes/glitch/features/ui/components/modal_loading.js b/app/javascript/flavours/glitch/features/ui/components/modal_loading.js
index e14d20fbb..b1c322154 100644
--- a/app/javascript/themes/glitch/features/ui/components/modal_loading.js
+++ b/app/javascript/flavours/glitch/features/ui/components/modal_loading.js
@@ -1,6 +1,6 @@
 import React from 'react';
 
-import LoadingIndicator from 'themes/glitch/components/loading_indicator';
+import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
 
 // Keep the markup in sync with <BundleModalError />
 // (make sure they have the same dimensions)
diff --git a/app/javascript/themes/glitch/features/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
index fbe794170..61b239283 100644
--- a/app/javascript/themes/glitch/features/ui/components/modal_root.js
+++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js
@@ -15,7 +15,7 @@ import {
   ReportModal,
   SettingsModal,
   EmbedModal,
-} from 'themes/glitch/util/async-components';
+} from 'flavours/glitch/util/async-components';
 
 const MODAL_COMPONENTS = {
   'MEDIA': () => Promise.resolve({ default: MediaModal }),
diff --git a/app/javascript/themes/glitch/features/ui/components/mute_modal.js b/app/javascript/flavours/glitch/features/ui/components/mute_modal.js
index ffccdc84d..0202b1ab1 100644
--- a/app/javascript/themes/glitch/features/ui/components/mute_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/mute_modal.js
@@ -3,10 +3,10 @@ import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import { injectIntl, FormattedMessage } from 'react-intl';
 import Toggle from 'react-toggle';
-import Button from 'themes/glitch/components/button';
-import { closeModal } from 'themes/glitch/actions/modal';
-import { muteAccount } from 'themes/glitch/actions/accounts';
-import { toggleHideNotifications } from 'themes/glitch/actions/mutes';
+import Button from 'flavours/glitch/components/button';
+import { closeModal } from 'flavours/glitch/actions/modal';
+import { muteAccount } from 'flavours/glitch/actions/accounts';
+import { toggleHideNotifications } from 'flavours/glitch/actions/mutes';
 
 
 const mapStateToProps = state => {
diff --git a/app/javascript/themes/glitch/features/ui/components/onboarding_modal.js b/app/javascript/flavours/glitch/features/ui/components/onboarding_modal.js
index 58875262e..21f1addea 100644
--- a/app/javascript/themes/glitch/features/ui/components/onboarding_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/onboarding_modal.js
@@ -5,16 +5,16 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ReactSwipeableViews from 'react-swipeable-views';
 import classNames from 'classnames';
-import Permalink from 'themes/glitch/components/permalink';
-import ComposeForm from 'themes/glitch/features/compose/components/compose_form';
-import Search from 'themes/glitch/features/compose/components/search';
-import NavigationBar from 'themes/glitch/features/compose/components/navigation_bar';
+import Permalink from 'flavours/glitch/components/permalink';
+import ComposeForm from 'flavours/glitch/features/compose/components/compose_form';
+import Search from 'flavours/glitch/features/compose/components/search';
+import NavigationBar from 'flavours/glitch/features/compose/components/navigation_bar';
 import ColumnHeader from './column_header';
 import {
   List as ImmutableList,
   Map as ImmutableMap,
 } from 'immutable';
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 
 const noop = () => { };
 
diff --git a/app/javascript/themes/glitch/features/ui/components/report_modal.js b/app/javascript/flavours/glitch/features/ui/components/report_modal.js
index e6153948e..b4dc1e3d6 100644
--- a/app/javascript/themes/glitch/features/ui/components/report_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/report_modal.js
@@ -1,15 +1,15 @@
 import React from 'react';
 import { connect } from 'react-redux';
-import { changeReportComment, submitReport } from 'themes/glitch/actions/reports';
-import { refreshAccountTimeline } from 'themes/glitch/actions/timelines';
+import { changeReportComment, submitReport } from 'flavours/glitch/actions/reports';
+import { refreshAccountTimeline } from 'flavours/glitch/actions/timelines';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import { makeGetAccount } from 'themes/glitch/selectors';
+import { makeGetAccount } from 'flavours/glitch/selectors';
 import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
-import StatusCheckBox from 'themes/glitch/features/report/containers/status_check_box_container';
+import StatusCheckBox from 'flavours/glitch/features/report/containers/status_check_box_container';
 import { OrderedSet } from 'immutable';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import Button from 'themes/glitch/components/button';
+import Button from 'flavours/glitch/components/button';
 
 const messages = defineMessages({
   placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' },
diff --git a/app/javascript/themes/glitch/features/ui/components/tabs_bar.js b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js
index ef5deae99..89b455dd8 100644
--- a/app/javascript/themes/glitch/features/ui/components/tabs_bar.js
+++ b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import { NavLink } from 'react-router-dom';
 import { FormattedMessage, injectIntl } from 'react-intl';
 import { debounce } from 'lodash';
-import { isUserTouching } from 'themes/glitch/util/is_mobile';
+import { isUserTouching } from 'flavours/glitch/util/is_mobile';
 
 export const links = [
   <NavLink className='tabs-bar__link primary' to='/statuses/new' data-preview-title-id='tabs_bar.compose' data-preview-icon='pencil' ><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></NavLink>,
diff --git a/app/javascript/themes/glitch/features/ui/components/upload_area.js b/app/javascript/flavours/glitch/features/ui/components/upload_area.js
index 72a450215..cc6b6d171 100644
--- a/app/javascript/themes/glitch/features/ui/components/upload_area.js
+++ b/app/javascript/flavours/glitch/features/ui/components/upload_area.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import Motion from 'themes/glitch/util/optional_motion';
+import Motion from 'flavours/glitch/util/optional_motion';
 import spring from 'react-motion/lib/spring';
 import { FormattedMessage } from 'react-intl';
 
diff --git a/app/javascript/themes/glitch/features/ui/components/video_modal.js b/app/javascript/flavours/glitch/features/ui/components/video_modal.js
index 91168c790..22fa998fb 100644
--- a/app/javascript/themes/glitch/features/ui/components/video_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/video_modal.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import Video from 'themes/glitch/features/video';
+import Video from 'flavours/glitch/features/video';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 export default class VideoModal extends ImmutablePureComponent {
diff --git a/app/javascript/themes/glitch/features/ui/containers/bundle_container.js b/app/javascript/flavours/glitch/features/ui/containers/bundle_container.js
index e6f9afcf7..c9086c9bc 100644
--- a/app/javascript/themes/glitch/features/ui/containers/bundle_container.js
+++ b/app/javascript/flavours/glitch/features/ui/containers/bundle_container.js
@@ -2,7 +2,7 @@ import { connect } from 'react-redux';
 
 import Bundle from '../components/bundle';
 
-import { fetchBundleRequest, fetchBundleSuccess, fetchBundleFail } from 'themes/glitch/actions/bundles';
+import { fetchBundleRequest, fetchBundleSuccess, fetchBundleFail } from 'flavours/glitch/actions/bundles';
 
 const mapDispatchToProps = dispatch => ({
   onFetch () {
diff --git a/app/javascript/themes/glitch/features/ui/containers/columns_area_container.js b/app/javascript/flavours/glitch/features/ui/containers/columns_area_container.js
index 95f95618b..95f95618b 100644
--- a/app/javascript/themes/glitch/features/ui/containers/columns_area_container.js
+++ b/app/javascript/flavours/glitch/features/ui/containers/columns_area_container.js
diff --git a/app/javascript/themes/glitch/features/ui/containers/loading_bar_container.js b/app/javascript/flavours/glitch/features/ui/containers/loading_bar_container.js
index 4bb90fb68..4bb90fb68 100644
--- a/app/javascript/themes/glitch/features/ui/containers/loading_bar_container.js
+++ b/app/javascript/flavours/glitch/features/ui/containers/loading_bar_container.js
diff --git a/app/javascript/themes/glitch/features/ui/containers/modal_container.js b/app/javascript/flavours/glitch/features/ui/containers/modal_container.js
index c26f19886..f074002e4 100644
--- a/app/javascript/themes/glitch/features/ui/containers/modal_container.js
+++ b/app/javascript/flavours/glitch/features/ui/containers/modal_container.js
@@ -1,5 +1,5 @@
 import { connect } from 'react-redux';
-import { closeModal } from 'themes/glitch/actions/modal';
+import { closeModal } from 'flavours/glitch/actions/modal';
 import ModalRoot from '../components/modal_root';
 
 const mapStateToProps = state => ({
diff --git a/app/javascript/themes/glitch/features/ui/containers/notifications_container.js b/app/javascript/flavours/glitch/features/ui/containers/notifications_container.js
index 5bd4017f5..88d482bcf 100644
--- a/app/javascript/themes/glitch/features/ui/containers/notifications_container.js
+++ b/app/javascript/flavours/glitch/features/ui/containers/notifications_container.js
@@ -1,7 +1,7 @@
 import { connect } from 'react-redux';
 import { NotificationStack } from 'react-notification';
-import { dismissAlert } from 'themes/glitch/actions/alerts';
-import { getAlerts } from 'themes/glitch/selectors';
+import { dismissAlert } from 'flavours/glitch/actions/alerts';
+import { getAlerts } from 'flavours/glitch/selectors';
 
 const mapStateToProps = state => ({
   notifications: getAlerts(state),
diff --git a/app/javascript/themes/glitch/features/ui/containers/status_list_container.js b/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js
index 807c82e16..eca85b8e6 100644
--- a/app/javascript/themes/glitch/features/ui/containers/status_list_container.js
+++ b/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js
@@ -1,10 +1,10 @@
 import { connect } from 'react-redux';
-import StatusList from 'themes/glitch/components/status_list';
-import { scrollTopTimeline } from 'themes/glitch/actions/timelines';
+import StatusList from 'flavours/glitch/components/status_list';
+import { scrollTopTimeline } from 'flavours/glitch/actions/timelines';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 import { createSelector } from 'reselect';
 import { debounce } from 'lodash';
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 
 const makeGetStatusIds = () => createSelector([
   (state, { type }) => state.getIn(['settings', type], ImmutableMap()),
@@ -32,6 +32,10 @@ const makeGetStatusIds = () => createSelector([
       showStatus = showStatus && (statusForId.get('in_reply_to_id') === null || statusForId.get('in_reply_to_account_id') === me);
     }
 
+    if (columnSettings.getIn(['shows', 'direct']) === false) {
+      showStatus = showStatus && statusForId.get('visibility') !== 'direct';
+    }
+
     if (showStatus && regex && statusForId.get('account') !== me) {
       const searchIndex = statusForId.get('reblog') ? statuses.getIn([statusForId.get('reblog'), 'search_index']) : statusForId.get('search_index');
       showStatus = !regex.test(searchIndex);
diff --git a/app/javascript/themes/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js
index 3eea63189..4a1982916 100644
--- a/app/javascript/themes/glitch/features/ui/index.js
+++ b/app/javascript/flavours/glitch/features/ui/index.js
@@ -6,13 +6,13 @@ import TabsBar from './components/tabs_bar';
 import ModalContainer from './containers/modal_container';
 import { connect } from 'react-redux';
 import { Redirect, withRouter } from 'react-router-dom';
-import { isMobile } from 'themes/glitch/util/is_mobile';
+import { isMobile } from 'flavours/glitch/util/is_mobile';
 import { debounce } from 'lodash';
-import { uploadCompose, resetCompose } from 'themes/glitch/actions/compose';
-import { refreshHomeTimeline } from 'themes/glitch/actions/timelines';
-import { refreshNotifications } from 'themes/glitch/actions/notifications';
-import { clearHeight } from 'themes/glitch/actions/height_cache';
-import { WrappedSwitch, WrappedRoute } from 'themes/glitch/util/react_router_helpers';
+import { uploadCompose, resetCompose } from 'flavours/glitch/actions/compose';
+import { refreshHomeTimeline } from 'flavours/glitch/actions/timelines';
+import { refreshNotifications } from 'flavours/glitch/actions/notifications';
+import { clearHeight } from 'flavours/glitch/actions/height_cache';
+import { WrappedSwitch, WrappedRoute } from 'flavours/glitch/util/react_router_helpers';
 import UploadArea from './components/upload_area';
 import ColumnsAreaContainer from './containers/columns_area_container';
 import classNames from 'classnames';
@@ -38,9 +38,9 @@ import {
   Blocks,
   Mutes,
   PinnedStatuses,
-} from 'themes/glitch/util/async-components';
+} from 'flavours/glitch/util/async-components';
 import { HotKeys } from 'react-hotkeys';
-import { me } from 'themes/glitch/util/initial_state';
+import { me } from 'flavours/glitch/util/initial_state';
 import { defineMessages, injectIntl } from 'react-intl';
 
 // Dummy import, to make sure that <Status /> ends up in the application bundle.
diff --git a/app/javascript/themes/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js
index 0ecbe37c9..97c1c27fa 100644
--- a/app/javascript/themes/glitch/features/video/index.js
+++ b/app/javascript/flavours/glitch/features/video/index.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import { throttle } from 'lodash';
 import classNames from 'classnames';
-import { isFullscreen, requestFullscreen, exitFullscreen } from 'themes/glitch/util/fullscreen';
+import { isFullscreen, requestFullscreen, exitFullscreen } from 'flavours/glitch/util/fullscreen';
 
 const messages = defineMessages({
   play: { id: 'video.play', defaultMessage: 'Play' },
diff --git a/app/javascript/themes/glitch/middleware/errors.js b/app/javascript/flavours/glitch/middleware/errors.js
index c54e7b0a2..f3dfc8b06 100644
--- a/app/javascript/themes/glitch/middleware/errors.js
+++ b/app/javascript/flavours/glitch/middleware/errors.js
@@ -1,4 +1,4 @@
-import { showAlert } from 'themes/glitch/actions/alerts';
+import { showAlert } from 'flavours/glitch/actions/alerts';
 
 const defaultFailSuffix = 'FAIL';
 
diff --git a/app/javascript/themes/glitch/middleware/loading_bar.js b/app/javascript/flavours/glitch/middleware/loading_bar.js
index a98f1bb2b..a98f1bb2b 100644
--- a/app/javascript/themes/glitch/middleware/loading_bar.js
+++ b/app/javascript/flavours/glitch/middleware/loading_bar.js
diff --git a/app/javascript/themes/glitch/middleware/sounds.js b/app/javascript/flavours/glitch/middleware/sounds.js
index 3d1e3eaba..3d1e3eaba 100644
--- a/app/javascript/themes/glitch/middleware/sounds.js
+++ b/app/javascript/flavours/glitch/middleware/sounds.js
diff --git a/app/javascript/flavours/glitch/packs/about.js b/app/javascript/flavours/glitch/packs/about.js
new file mode 100644
index 000000000..bc0a4887b
--- /dev/null
+++ b/app/javascript/flavours/glitch/packs/about.js
@@ -0,0 +1,22 @@
+import loadPolyfills from 'flavours/glitch/util/load_polyfills';
+
+function loaded() {
+  const TimelineContainer = require('flavours/glitch/containers/timeline_container').default;
+  const React             = require('react');
+  const ReactDOM          = require('react-dom');
+  const mountNode         = document.getElementById('mastodon-timeline');
+
+  if (mountNode !== null) {
+    const props = JSON.parse(mountNode.getAttribute('data-props'));
+    ReactDOM.render(<TimelineContainer {...props} />, mountNode);
+  }
+}
+
+function main() {
+  const ready = require('flavours/glitch/util/ready').default;
+  ready(loaded);
+}
+
+loadPolyfills().then(main).catch(error => {
+  console.error(error);
+});
diff --git a/app/javascript/flavours/glitch/packs/common.js b/app/javascript/flavours/glitch/packs/common.js
new file mode 100644
index 000000000..07445d2b3
--- /dev/null
+++ b/app/javascript/flavours/glitch/packs/common.js
@@ -0,0 +1 @@
+import 'flavours/glitch/styles/index.scss';
diff --git a/app/javascript/flavours/glitch/packs/home.js b/app/javascript/flavours/glitch/packs/home.js
new file mode 100644
index 000000000..b8f7b7d8e
--- /dev/null
+++ b/app/javascript/flavours/glitch/packs/home.js
@@ -0,0 +1,7 @@
+import loadPolyfills from 'flavours/glitch/util/load_polyfills';
+
+loadPolyfills().then(() => {
+  require('flavours/glitch/util/main').default();
+}).catch(e => {
+  console.error(e);
+});
diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js
new file mode 100644
index 000000000..9ea82b53a
--- /dev/null
+++ b/app/javascript/flavours/glitch/packs/public.js
@@ -0,0 +1,75 @@
+import loadPolyfills from 'flavours/glitch/util/load_polyfills';
+import ready from 'flavours/glitch/util/ready';
+
+function main() {
+  const IntlRelativeFormat = require('intl-relativeformat').default;
+  const emojify = require('flavours/glitch/util/emoji').default;
+  const { getLocale } = require('locales');
+  const { localeData } = getLocale();
+  const VideoContainer = require('flavours/glitch/containers/video_container').default;
+  const MediaGalleryContainer = require('flavours/glitch/containers/media_gallery_container').default;
+  const CardContainer = require('flavours/glitch/containers/card_container').default;
+  const React = require('react');
+  const ReactDOM = require('react-dom');
+
+  localeData.forEach(IntlRelativeFormat.__addLocaleData);
+
+  ready(() => {
+    const locale = document.documentElement.lang;
+
+    const dateTimeFormat = new Intl.DateTimeFormat(locale, {
+      year: 'numeric',
+      month: 'long',
+      day: 'numeric',
+      hour: 'numeric',
+      minute: 'numeric',
+    });
+
+    const relativeFormat = new IntlRelativeFormat(locale);
+
+    [].forEach.call(document.querySelectorAll('.emojify'), (content) => {
+      content.innerHTML = emojify(content.innerHTML);
+    });
+
+    [].forEach.call(document.querySelectorAll('time.formatted'), (content) => {
+      const datetime = new Date(content.getAttribute('datetime'));
+      const formattedDate = dateTimeFormat.format(datetime);
+
+      content.title = formattedDate;
+      content.textContent = formattedDate;
+    });
+
+    [].forEach.call(document.querySelectorAll('time.time-ago'), (content) => {
+      const datetime = new Date(content.getAttribute('datetime'));
+
+      content.title = dateTimeFormat.format(datetime);
+      content.textContent = relativeFormat.format(datetime);
+    });
+
+    [].forEach.call(document.querySelectorAll('.logo-button'), (content) => {
+      content.addEventListener('click', (e) => {
+        e.preventDefault();
+        window.open(e.target.href, 'mastodon-intent', 'width=400,height=400,resizable=no,menubar=no,status=no,scrollbars=yes');
+      });
+    });
+
+    [].forEach.call(document.querySelectorAll('[data-component="Video"]'), (content) => {
+      const props = JSON.parse(content.getAttribute('data-props'));
+      ReactDOM.render(<VideoContainer locale={locale} {...props} />, content);
+    });
+
+    [].forEach.call(document.querySelectorAll('[data-component="MediaGallery"]'), (content) => {
+      const props = JSON.parse(content.getAttribute('data-props'));
+      ReactDOM.render(<MediaGalleryContainer locale={locale} {...props} />, content);
+    });
+
+    [].forEach.call(document.querySelectorAll('[data-component="Card"]'), (content) => {
+      const props = JSON.parse(content.getAttribute('data-props'));
+      ReactDOM.render(<CardContainer locale={locale} {...props} />, content);
+    });
+  });
+}
+
+loadPolyfills().then(main).catch(error => {
+  console.error(error);
+});
diff --git a/app/javascript/flavours/glitch/packs/share.js b/app/javascript/flavours/glitch/packs/share.js
new file mode 100644
index 000000000..9f2aa2553
--- /dev/null
+++ b/app/javascript/flavours/glitch/packs/share.js
@@ -0,0 +1,22 @@
+import loadPolyfills from 'flavours/glitch/util/load_polyfills';
+
+function loaded() {
+  const ComposeContainer = require('flavours/glitch/containers/compose_container').default;
+  const React = require('react');
+  const ReactDOM = require('react-dom');
+  const mountNode = document.getElementById('mastodon-compose');
+
+  if (mountNode !== null) {
+    const props = JSON.parse(mountNode.getAttribute('data-props'));
+    ReactDOM.render(<ComposeContainer {...props} />, mountNode);
+  }
+}
+
+function main() {
+  const ready = require('flavours/glitch/util/ready').default;
+  ready(loaded);
+}
+
+loadPolyfills().then(main).catch(error => {
+  console.error(error);
+});
diff --git a/app/javascript/themes/glitch/reducers/accounts.js b/app/javascript/flavours/glitch/reducers/accounts.js
index 0a65d3723..5113a281a 100644
--- a/app/javascript/themes/glitch/reducers/accounts.js
+++ b/app/javascript/flavours/glitch/reducers/accounts.js
@@ -6,16 +6,16 @@ import {
   FOLLOWING_EXPAND_SUCCESS,
   FOLLOW_REQUESTS_FETCH_SUCCESS,
   FOLLOW_REQUESTS_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/accounts';
+} from 'flavours/glitch/actions/accounts';
 import {
   BLOCKS_FETCH_SUCCESS,
   BLOCKS_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/blocks';
+} from 'flavours/glitch/actions/blocks';
 import {
   MUTES_FETCH_SUCCESS,
   MUTES_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/mutes';
-import { COMPOSE_SUGGESTIONS_READY } from 'themes/glitch/actions/compose';
+} from 'flavours/glitch/actions/mutes';
+import { COMPOSE_SUGGESTIONS_READY } from 'flavours/glitch/actions/compose';
 import {
   REBLOG_SUCCESS,
   UNREBLOG_SUCCESS,
@@ -23,28 +23,28 @@ import {
   UNFAVOURITE_SUCCESS,
   REBLOGS_FETCH_SUCCESS,
   FAVOURITES_FETCH_SUCCESS,
-} from 'themes/glitch/actions/interactions';
+} from 'flavours/glitch/actions/interactions';
 import {
   TIMELINE_REFRESH_SUCCESS,
   TIMELINE_UPDATE,
   TIMELINE_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/timelines';
+} from 'flavours/glitch/actions/timelines';
 import {
   STATUS_FETCH_SUCCESS,
   CONTEXT_FETCH_SUCCESS,
-} from 'themes/glitch/actions/statuses';
-import { SEARCH_FETCH_SUCCESS } from 'themes/glitch/actions/search';
+} from 'flavours/glitch/actions/statuses';
+import { SEARCH_FETCH_SUCCESS } from 'flavours/glitch/actions/search';
 import {
   NOTIFICATIONS_UPDATE,
   NOTIFICATIONS_REFRESH_SUCCESS,
   NOTIFICATIONS_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/notifications';
+} from 'flavours/glitch/actions/notifications';
 import {
   FAVOURITED_STATUSES_FETCH_SUCCESS,
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/favourites';
-import { STORE_HYDRATE } from 'themes/glitch/actions/store';
-import emojify from 'themes/glitch/util/emoji';
+} from 'flavours/glitch/actions/favourites';
+import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
+import emojify from 'flavours/glitch/util/emoji';
 import { Map as ImmutableMap, fromJS } from 'immutable';
 import escapeTextContentForBrowser from 'escape-html';
 
diff --git a/app/javascript/themes/glitch/reducers/accounts_counters.js b/app/javascript/flavours/glitch/reducers/accounts_counters.js
index e3728ecd7..a9aebd26f 100644
--- a/app/javascript/themes/glitch/reducers/accounts_counters.js
+++ b/app/javascript/flavours/glitch/reducers/accounts_counters.js
@@ -8,16 +8,16 @@ import {
   FOLLOW_REQUESTS_EXPAND_SUCCESS,
   ACCOUNT_FOLLOW_SUCCESS,
   ACCOUNT_UNFOLLOW_SUCCESS,
-} from 'themes/glitch/actions/accounts';
+} from 'flavours/glitch/actions/accounts';
 import {
   BLOCKS_FETCH_SUCCESS,
   BLOCKS_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/blocks';
+} from 'flavours/glitch/actions/blocks';
 import {
   MUTES_FETCH_SUCCESS,
   MUTES_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/mutes';
-import { COMPOSE_SUGGESTIONS_READY } from 'themes/glitch/actions/compose';
+} from 'flavours/glitch/actions/mutes';
+import { COMPOSE_SUGGESTIONS_READY } from 'flavours/glitch/actions/compose';
 import {
   REBLOG_SUCCESS,
   UNREBLOG_SUCCESS,
@@ -25,27 +25,27 @@ import {
   UNFAVOURITE_SUCCESS,
   REBLOGS_FETCH_SUCCESS,
   FAVOURITES_FETCH_SUCCESS,
-} from 'themes/glitch/actions/interactions';
+} from 'flavours/glitch/actions/interactions';
 import {
   TIMELINE_REFRESH_SUCCESS,
   TIMELINE_UPDATE,
   TIMELINE_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/timelines';
+} from 'flavours/glitch/actions/timelines';
 import {
   STATUS_FETCH_SUCCESS,
   CONTEXT_FETCH_SUCCESS,
-} from 'themes/glitch/actions/statuses';
-import { SEARCH_FETCH_SUCCESS } from 'themes/glitch/actions/search';
+} from 'flavours/glitch/actions/statuses';
+import { SEARCH_FETCH_SUCCESS } from 'flavours/glitch/actions/search';
 import {
   NOTIFICATIONS_UPDATE,
   NOTIFICATIONS_REFRESH_SUCCESS,
   NOTIFICATIONS_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/notifications';
+} from 'flavours/glitch/actions/notifications';
 import {
   FAVOURITED_STATUSES_FETCH_SUCCESS,
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/favourites';
-import { STORE_HYDRATE } from 'themes/glitch/actions/store';
+} from 'flavours/glitch/actions/favourites';
+import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
 import { Map as ImmutableMap, fromJS } from 'immutable';
 
 const normalizeAccount = (state, account) => state.set(account.id, fromJS({
diff --git a/app/javascript/themes/glitch/reducers/alerts.js b/app/javascript/flavours/glitch/reducers/alerts.js
index ad66b63f6..50f8d30f7 100644
--- a/app/javascript/themes/glitch/reducers/alerts.js
+++ b/app/javascript/flavours/glitch/reducers/alerts.js
@@ -2,7 +2,7 @@ import {
   ALERT_SHOW,
   ALERT_DISMISS,
   ALERT_CLEAR,
-} from 'themes/glitch/actions/alerts';
+} from 'flavours/glitch/actions/alerts';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
 const initialState = ImmutableList([]);
diff --git a/app/javascript/themes/glitch/reducers/cards.js b/app/javascript/flavours/glitch/reducers/cards.js
index 35be30444..92ecfd086 100644
--- a/app/javascript/themes/glitch/reducers/cards.js
+++ b/app/javascript/flavours/glitch/reducers/cards.js
@@ -1,4 +1,4 @@
-import { STATUS_CARD_FETCH_SUCCESS } from 'themes/glitch/actions/cards';
+import { STATUS_CARD_FETCH_SUCCESS } from 'flavours/glitch/actions/cards';
 
 import { Map as ImmutableMap, fromJS } from 'immutable';
 
diff --git a/app/javascript/themes/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js
index be359fcb4..aaa36b696 100644
--- a/app/javascript/themes/glitch/reducers/compose.js
+++ b/app/javascript/flavours/glitch/reducers/compose.js
@@ -28,12 +28,12 @@ import {
   COMPOSE_UPLOAD_CHANGE_FAIL,
   COMPOSE_DOODLE_SET,
   COMPOSE_RESET,
-} from 'themes/glitch/actions/compose';
-import { TIMELINE_DELETE } from 'themes/glitch/actions/timelines';
-import { STORE_HYDRATE } from 'themes/glitch/actions/store';
+} from 'flavours/glitch/actions/compose';
+import { TIMELINE_DELETE } from 'flavours/glitch/actions/timelines';
+import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
 import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
-import uuid from 'themes/glitch/util/uuid';
-import { me } from 'themes/glitch/util/initial_state';
+import uuid from 'flavours/glitch/util/uuid';
+import { me } from 'flavours/glitch/util/initial_state';
 
 const initialState = ImmutableMap({
   mounted: false,
diff --git a/app/javascript/themes/glitch/reducers/contexts.js b/app/javascript/flavours/glitch/reducers/contexts.js
index 56c930bd5..53e93a589 100644
--- a/app/javascript/themes/glitch/reducers/contexts.js
+++ b/app/javascript/flavours/glitch/reducers/contexts.js
@@ -1,5 +1,5 @@
-import { CONTEXT_FETCH_SUCCESS } from 'themes/glitch/actions/statuses';
-import { TIMELINE_DELETE, TIMELINE_CONTEXT_UPDATE } from 'themes/glitch/actions/timelines';
+import { CONTEXT_FETCH_SUCCESS } from 'flavours/glitch/actions/statuses';
+import { TIMELINE_DELETE, TIMELINE_CONTEXT_UPDATE } from 'flavours/glitch/actions/timelines';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
 const initialState = ImmutableMap({
diff --git a/app/javascript/themes/glitch/reducers/custom_emojis.js b/app/javascript/flavours/glitch/reducers/custom_emojis.js
index e3f1e0018..592cea8dc 100644
--- a/app/javascript/themes/glitch/reducers/custom_emojis.js
+++ b/app/javascript/flavours/glitch/reducers/custom_emojis.js
@@ -1,7 +1,7 @@
 import { List as ImmutableList } from 'immutable';
-import { STORE_HYDRATE } from 'themes/glitch/actions/store';
-import { search as emojiSearch } from 'themes/glitch/util/emoji/emoji_mart_search_light';
-import { buildCustomEmojis } from 'themes/glitch/util/emoji';
+import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
+import { search as emojiSearch } from 'flavours/glitch/util/emoji/emoji_mart_search_light';
+import { buildCustomEmojis } from 'flavours/glitch/util/emoji';
 
 const initialState = ImmutableList();
 
diff --git a/app/javascript/themes/glitch/reducers/height_cache.js b/app/javascript/flavours/glitch/reducers/height_cache.js
index 93c31b42c..8b05e0b19 100644
--- a/app/javascript/themes/glitch/reducers/height_cache.js
+++ b/app/javascript/flavours/glitch/reducers/height_cache.js
@@ -1,5 +1,5 @@
 import { Map as ImmutableMap } from 'immutable';
-import { HEIGHT_CACHE_SET, HEIGHT_CACHE_CLEAR } from 'themes/glitch/actions/height_cache';
+import { HEIGHT_CACHE_SET, HEIGHT_CACHE_CLEAR } from 'flavours/glitch/actions/height_cache';
 
 const initialState = ImmutableMap();
 
diff --git a/app/javascript/themes/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js
index aa748421a..aa748421a 100644
--- a/app/javascript/themes/glitch/reducers/index.js
+++ b/app/javascript/flavours/glitch/reducers/index.js
diff --git a/app/javascript/themes/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js
index b1ffa047e..69d98741b 100644
--- a/app/javascript/themes/glitch/reducers/local_settings.js
+++ b/app/javascript/flavours/glitch/reducers/local_settings.js
@@ -2,8 +2,8 @@
 import { Map as ImmutableMap } from 'immutable';
 
 //  Our imports.
-import { STORE_HYDRATE } from 'themes/glitch/actions/store';
-import { LOCAL_SETTING_CHANGE } from 'themes/glitch/actions/local_settings';
+import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
+import { LOCAL_SETTING_CHANGE } from 'flavours/glitch/actions/local_settings';
 
 const initialState = ImmutableMap({
   layout    : 'auto',
diff --git a/app/javascript/themes/glitch/reducers/media_attachments.js b/app/javascript/flavours/glitch/reducers/media_attachments.js
index 69a44639c..6e6058576 100644
--- a/app/javascript/themes/glitch/reducers/media_attachments.js
+++ b/app/javascript/flavours/glitch/reducers/media_attachments.js
@@ -1,4 +1,4 @@
-import { STORE_HYDRATE } from 'themes/glitch/actions/store';
+import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
 import { Map as ImmutableMap } from 'immutable';
 
 const initialState = ImmutableMap({
diff --git a/app/javascript/themes/glitch/reducers/meta.js b/app/javascript/flavours/glitch/reducers/meta.js
index 2249f1d78..a98dc436a 100644
--- a/app/javascript/themes/glitch/reducers/meta.js
+++ b/app/javascript/flavours/glitch/reducers/meta.js
@@ -1,4 +1,4 @@
-import { STORE_HYDRATE } from 'themes/glitch/actions/store';
+import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
 import { Map as ImmutableMap } from 'immutable';
 
 const initialState = ImmutableMap({
diff --git a/app/javascript/themes/glitch/reducers/modal.js b/app/javascript/flavours/glitch/reducers/modal.js
index 97fb31203..80bc11dda 100644
--- a/app/javascript/themes/glitch/reducers/modal.js
+++ b/app/javascript/flavours/glitch/reducers/modal.js
@@ -1,4 +1,4 @@
-import { MODAL_OPEN, MODAL_CLOSE } from 'themes/glitch/actions/modal';
+import { MODAL_OPEN, MODAL_CLOSE } from 'flavours/glitch/actions/modal';
 
 const initialState = {
   modalType: null,
diff --git a/app/javascript/themes/glitch/reducers/mutes.js b/app/javascript/flavours/glitch/reducers/mutes.js
index 8fe4ae0c3..8f52a7704 100644
--- a/app/javascript/themes/glitch/reducers/mutes.js
+++ b/app/javascript/flavours/glitch/reducers/mutes.js
@@ -3,7 +3,7 @@ import Immutable from 'immutable';
 import {
   MUTES_INIT_MODAL,
   MUTES_TOGGLE_HIDE_NOTIFICATIONS,
-} from 'themes/glitch/actions/mutes';
+} from 'flavours/glitch/actions/mutes';
 
 const initialState = Immutable.Map({
   new: Immutable.Map({
diff --git a/app/javascript/themes/glitch/reducers/notifications.js b/app/javascript/flavours/glitch/reducers/notifications.js
index c4f505053..fb2b3f549 100644
--- a/app/javascript/themes/glitch/reducers/notifications.js
+++ b/app/javascript/flavours/glitch/reducers/notifications.js
@@ -14,12 +14,12 @@ import {
   NOTIFICATIONS_DELETE_MARKED_FAIL,
   NOTIFICATIONS_ENTER_CLEARING_MODE,
   NOTIFICATIONS_MARK_ALL_FOR_DELETE,
-} from 'themes/glitch/actions/notifications';
+} from 'flavours/glitch/actions/notifications';
 import {
   ACCOUNT_BLOCK_SUCCESS,
   ACCOUNT_MUTE_SUCCESS,
-} from 'themes/glitch/actions/accounts';
-import { TIMELINE_DELETE } from 'themes/glitch/actions/timelines';
+} from 'flavours/glitch/actions/accounts';
+import { TIMELINE_DELETE } from 'flavours/glitch/actions/timelines';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
 const initialState = ImmutableMap({
diff --git a/app/javascript/themes/glitch/reducers/push_notifications.js b/app/javascript/flavours/glitch/reducers/push_notifications.js
index 744e4a0eb..f0a800d23 100644
--- a/app/javascript/themes/glitch/reducers/push_notifications.js
+++ b/app/javascript/flavours/glitch/reducers/push_notifications.js
@@ -1,5 +1,5 @@
-import { STORE_HYDRATE } from 'themes/glitch/actions/store';
-import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, ALERTS_CHANGE } from 'themes/glitch/actions/push_notifications';
+import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
+import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, ALERTS_CHANGE } from 'flavours/glitch/actions/push_notifications';
 import Immutable from 'immutable';
 
 const initialState = Immutable.Map({
diff --git a/app/javascript/themes/glitch/reducers/relationships.js b/app/javascript/flavours/glitch/reducers/relationships.js
index d9135d6da..6303089ac 100644
--- a/app/javascript/themes/glitch/reducers/relationships.js
+++ b/app/javascript/flavours/glitch/reducers/relationships.js
@@ -6,11 +6,11 @@ import {
   ACCOUNT_MUTE_SUCCESS,
   ACCOUNT_UNMUTE_SUCCESS,
   RELATIONSHIPS_FETCH_SUCCESS,
-} from 'themes/glitch/actions/accounts';
+} from 'flavours/glitch/actions/accounts';
 import {
   DOMAIN_BLOCK_SUCCESS,
   DOMAIN_UNBLOCK_SUCCESS,
-} from 'themes/glitch/actions/domain_blocks';
+} from 'flavours/glitch/actions/domain_blocks';
 import { Map as ImmutableMap, fromJS } from 'immutable';
 
 const normalizeRelationship = (state, relationship) => state.set(relationship.id, fromJS(relationship));
diff --git a/app/javascript/themes/glitch/reducers/reports.js b/app/javascript/flavours/glitch/reducers/reports.js
index b714374ea..c18fbcdc6 100644
--- a/app/javascript/themes/glitch/reducers/reports.js
+++ b/app/javascript/flavours/glitch/reducers/reports.js
@@ -6,7 +6,7 @@ import {
   REPORT_CANCEL,
   REPORT_STATUS_TOGGLE,
   REPORT_COMMENT_CHANGE,
-} from 'themes/glitch/actions/reports';
+} from 'flavours/glitch/actions/reports';
 import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable';
 
 const initialState = ImmutableMap({
diff --git a/app/javascript/themes/glitch/reducers/search.js b/app/javascript/flavours/glitch/reducers/search.js
index aec9e2efb..f9bf92098 100644
--- a/app/javascript/themes/glitch/reducers/search.js
+++ b/app/javascript/flavours/glitch/reducers/search.js
@@ -3,8 +3,8 @@ import {
   SEARCH_CLEAR,
   SEARCH_FETCH_SUCCESS,
   SEARCH_SHOW,
-} from 'themes/glitch/actions/search';
-import { COMPOSE_MENTION, COMPOSE_REPLY } from 'themes/glitch/actions/compose';
+} from 'flavours/glitch/actions/search';
+import { COMPOSE_MENTION, COMPOSE_REPLY } from 'flavours/glitch/actions/compose';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
 const initialState = ImmutableMap({
diff --git a/app/javascript/themes/glitch/reducers/settings.js b/app/javascript/flavours/glitch/reducers/settings.js
index c22bbbd8d..6d5d71217 100644
--- a/app/javascript/themes/glitch/reducers/settings.js
+++ b/app/javascript/flavours/glitch/reducers/settings.js
@@ -1,9 +1,9 @@
-import { SETTING_CHANGE, SETTING_SAVE } from 'themes/glitch/actions/settings';
-import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE } from 'themes/glitch/actions/columns';
-import { STORE_HYDRATE } from 'themes/glitch/actions/store';
-import { EMOJI_USE } from 'themes/glitch/actions/emojis';
+import { SETTING_CHANGE, SETTING_SAVE } from 'flavours/glitch/actions/settings';
+import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE } from 'flavours/glitch/actions/columns';
+import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
+import { EMOJI_USE } from 'flavours/glitch/actions/emojis';
 import { Map as ImmutableMap, fromJS } from 'immutable';
-import uuid from 'themes/glitch/util/uuid';
+import uuid from 'flavours/glitch/util/uuid';
 
 const initialState = ImmutableMap({
   saved: true,
@@ -17,6 +17,7 @@ const initialState = ImmutableMap({
     shows: ImmutableMap({
       reblog: true,
       reply: true,
+      direct: true,
     }),
 
     regex: ImmutableMap({
diff --git a/app/javascript/themes/glitch/reducers/status_lists.js b/app/javascript/flavours/glitch/reducers/status_lists.js
index 8dc7d374e..5a3d0db0c 100644
--- a/app/javascript/themes/glitch/reducers/status_lists.js
+++ b/app/javascript/flavours/glitch/reducers/status_lists.js
@@ -1,17 +1,17 @@
 import {
   FAVOURITED_STATUSES_FETCH_SUCCESS,
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/favourites';
+} from 'flavours/glitch/actions/favourites';
 import {
   PINNED_STATUSES_FETCH_SUCCESS,
-} from 'themes/glitch/actions/pin_statuses';
+} from 'flavours/glitch/actions/pin_statuses';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 import {
   FAVOURITE_SUCCESS,
   UNFAVOURITE_SUCCESS,
   PIN_SUCCESS,
   UNPIN_SUCCESS,
-} from 'themes/glitch/actions/interactions';
+} from 'flavours/glitch/actions/interactions';
 
 const initialState = ImmutableMap({
   favourites: ImmutableMap({
diff --git a/app/javascript/themes/glitch/reducers/statuses.js b/app/javascript/flavours/glitch/reducers/statuses.js
index ef8086865..410bc013b 100644
--- a/app/javascript/themes/glitch/reducers/statuses.js
+++ b/app/javascript/flavours/glitch/reducers/statuses.js
@@ -9,37 +9,37 @@ import {
   UNFAVOURITE_SUCCESS,
   PIN_SUCCESS,
   UNPIN_SUCCESS,
-} from 'themes/glitch/actions/interactions';
+} from 'flavours/glitch/actions/interactions';
 import {
   STATUS_FETCH_SUCCESS,
   CONTEXT_FETCH_SUCCESS,
   STATUS_MUTE_SUCCESS,
   STATUS_UNMUTE_SUCCESS,
-} from 'themes/glitch/actions/statuses';
+} from 'flavours/glitch/actions/statuses';
 import {
   TIMELINE_REFRESH_SUCCESS,
   TIMELINE_UPDATE,
   TIMELINE_DELETE,
   TIMELINE_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/timelines';
+} from 'flavours/glitch/actions/timelines';
 import {
   ACCOUNT_BLOCK_SUCCESS,
   ACCOUNT_MUTE_SUCCESS,
-} from 'themes/glitch/actions/accounts';
+} from 'flavours/glitch/actions/accounts';
 import {
   NOTIFICATIONS_UPDATE,
   NOTIFICATIONS_REFRESH_SUCCESS,
   NOTIFICATIONS_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/notifications';
+} from 'flavours/glitch/actions/notifications';
 import {
   FAVOURITED_STATUSES_FETCH_SUCCESS,
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/favourites';
+} from 'flavours/glitch/actions/favourites';
 import {
   PINNED_STATUSES_FETCH_SUCCESS,
-} from 'themes/glitch/actions/pin_statuses';
-import { SEARCH_FETCH_SUCCESS } from 'themes/glitch/actions/search';
-import emojify from 'themes/glitch/util/emoji';
+} from 'flavours/glitch/actions/pin_statuses';
+import { SEARCH_FETCH_SUCCESS } from 'flavours/glitch/actions/search';
+import emojify from 'flavours/glitch/util/emoji';
 import { Map as ImmutableMap, fromJS } from 'immutable';
 import escapeTextContentForBrowser from 'escape-html';
 
diff --git a/app/javascript/themes/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js
index 7f19a1897..679e1601e 100644
--- a/app/javascript/themes/glitch/reducers/timelines.js
+++ b/app/javascript/flavours/glitch/reducers/timelines.js
@@ -10,12 +10,12 @@ import {
   TIMELINE_SCROLL_TOP,
   TIMELINE_CONNECT,
   TIMELINE_DISCONNECT,
-} from 'themes/glitch/actions/timelines';
+} from 'flavours/glitch/actions/timelines';
 import {
   ACCOUNT_BLOCK_SUCCESS,
   ACCOUNT_MUTE_SUCCESS,
   ACCOUNT_UNFOLLOW_SUCCESS,
-} from 'themes/glitch/actions/accounts';
+} from 'flavours/glitch/actions/accounts';
 import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
 
 const initialState = ImmutableMap();
diff --git a/app/javascript/themes/glitch/reducers/user_lists.js b/app/javascript/flavours/glitch/reducers/user_lists.js
index 8c3a7d748..a4df9ec8d 100644
--- a/app/javascript/themes/glitch/reducers/user_lists.js
+++ b/app/javascript/flavours/glitch/reducers/user_lists.js
@@ -7,19 +7,19 @@ import {
   FOLLOW_REQUESTS_EXPAND_SUCCESS,
   FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
   FOLLOW_REQUEST_REJECT_SUCCESS,
-} from 'themes/glitch/actions/accounts';
+} from 'flavours/glitch/actions/accounts';
 import {
   REBLOGS_FETCH_SUCCESS,
   FAVOURITES_FETCH_SUCCESS,
-} from 'themes/glitch/actions/interactions';
+} from 'flavours/glitch/actions/interactions';
 import {
   BLOCKS_FETCH_SUCCESS,
   BLOCKS_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/blocks';
+} from 'flavours/glitch/actions/blocks';
 import {
   MUTES_FETCH_SUCCESS,
   MUTES_EXPAND_SUCCESS,
-} from 'themes/glitch/actions/mutes';
+} from 'flavours/glitch/actions/mutes';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
 const initialState = ImmutableMap({
diff --git a/app/javascript/themes/glitch/selectors/index.js b/app/javascript/flavours/glitch/selectors/index.js
index d26d1b727..d26d1b727 100644
--- a/app/javascript/themes/glitch/selectors/index.js
+++ b/app/javascript/flavours/glitch/selectors/index.js
diff --git a/app/javascript/themes/glitch/service_worker/entry.js b/app/javascript/flavours/glitch/service_worker/entry.js
index eea4cfc3c..eea4cfc3c 100644
--- a/app/javascript/themes/glitch/service_worker/entry.js
+++ b/app/javascript/flavours/glitch/service_worker/entry.js
diff --git a/app/javascript/themes/glitch/service_worker/web_push_notifications.js b/app/javascript/flavours/glitch/service_worker/web_push_notifications.js
index f63cff335..f63cff335 100644
--- a/app/javascript/themes/glitch/service_worker/web_push_notifications.js
+++ b/app/javascript/flavours/glitch/service_worker/web_push_notifications.js
diff --git a/app/javascript/themes/glitch/store/configureStore.js b/app/javascript/flavours/glitch/store/configureStore.js
index 1376d4cba..1376d4cba 100644
--- a/app/javascript/themes/glitch/store/configureStore.js
+++ b/app/javascript/flavours/glitch/store/configureStore.js
diff --git a/app/javascript/themes/glitch/styles/_mixins.scss b/app/javascript/flavours/glitch/styles/_mixins.scss
index 102723e39..102723e39 100644
--- a/app/javascript/themes/glitch/styles/_mixins.scss
+++ b/app/javascript/flavours/glitch/styles/_mixins.scss
diff --git a/app/javascript/themes/glitch/styles/about.scss b/app/javascript/flavours/glitch/styles/about.scss
index 4ec689427..4ec689427 100644
--- a/app/javascript/themes/glitch/styles/about.scss
+++ b/app/javascript/flavours/glitch/styles/about.scss
diff --git a/app/javascript/themes/glitch/styles/accounts.scss b/app/javascript/flavours/glitch/styles/accounts.scss
index 2cf98c642..2cf98c642 100644
--- a/app/javascript/themes/glitch/styles/accounts.scss
+++ b/app/javascript/flavours/glitch/styles/accounts.scss
diff --git a/app/javascript/themes/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss
index 87bc710af..87bc710af 100644
--- a/app/javascript/themes/glitch/styles/admin.scss
+++ b/app/javascript/flavours/glitch/styles/admin.scss
diff --git a/app/javascript/themes/glitch/styles/basics.scss b/app/javascript/flavours/glitch/styles/basics.scss
index b5d77ff63..b5d77ff63 100644
--- a/app/javascript/themes/glitch/styles/basics.scss
+++ b/app/javascript/flavours/glitch/styles/basics.scss
diff --git a/app/javascript/themes/glitch/styles/compact_header.scss b/app/javascript/flavours/glitch/styles/compact_header.scss
index 90d98cc8c..90d98cc8c 100644
--- a/app/javascript/themes/glitch/styles/compact_header.scss
+++ b/app/javascript/flavours/glitch/styles/compact_header.scss
diff --git a/app/javascript/themes/glitch/styles/boost.scss b/app/javascript/flavours/glitch/styles/components/boost.scss
index b07b72f8e..b07b72f8e 100644
--- a/app/javascript/themes/glitch/styles/boost.scss
+++ b/app/javascript/flavours/glitch/styles/components/boost.scss
diff --git a/app/javascript/themes/glitch/styles/doodle.scss b/app/javascript/flavours/glitch/styles/components/doodle.scss
index a4a1cfc84..a4a1cfc84 100644
--- a/app/javascript/themes/glitch/styles/doodle.scss
+++ b/app/javascript/flavours/glitch/styles/components/doodle.scss
diff --git a/app/javascript/themes/glitch/styles/emoji_picker.scss b/app/javascript/flavours/glitch/styles/components/emoji_picker.scss
index 2b46d30fc..2b46d30fc 100644
--- a/app/javascript/themes/glitch/styles/emoji_picker.scss
+++ b/app/javascript/flavours/glitch/styles/components/emoji_picker.scss
diff --git a/app/javascript/themes/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components/index.scss
index ade8df018..7aeab0a6a 100644
--- a/app/javascript/themes/glitch/styles/components.scss
+++ b/app/javascript/flavours/glitch/styles/components/index.scss
@@ -1,5 +1,3 @@
-@import 'variables';
-
 .app-body {
   -webkit-overflow-scrolling: touch;
   -ms-overflow-style: -ms-autohiding-scrollbar;
@@ -4830,3 +4828,5 @@ noscript {
 }
 
 @import 'doodle';
+@import 'emoji_picker';
+@import 'local_settings';
diff --git a/app/javascript/flavours/glitch/styles/components/local_settings.scss b/app/javascript/flavours/glitch/styles/components/local_settings.scss
new file mode 100644
index 000000000..16c8cf003
--- /dev/null
+++ b/app/javascript/flavours/glitch/styles/components/local_settings.scss
@@ -0,0 +1,81 @@
+.glitch.local-settings {
+  position: relative;
+  display: flex;
+  flex-direction: row;
+  background: $ui-secondary-color;
+  color: $ui-base-color;
+  border-radius: 8px;
+  height: 80vh;
+  width: 80vw;
+  max-width: 740px;
+  max-height: 450px;
+  overflow: hidden;
+
+  label {
+    display: block;
+  }
+
+  h1 {
+    font-size: 18px;
+    font-weight: 500;
+    line-height: 24px;
+    margin-bottom: 20px;
+  }
+
+  h2 {
+    font-size: 15px;
+    font-weight: 500;
+    line-height: 20px;
+    margin-top: 20px;
+    margin-bottom: 10px;
+  }
+}
+
+.glitch.local-settings__navigation__item {
+  display: block;
+  padding: 15px 20px;
+  color: inherit;
+  background: $primary-text-color;
+  border-bottom: 1px $ui-primary-color solid;
+  cursor: pointer;
+  text-decoration: none;
+  outline: none;
+  transition: background .3s;
+
+  &:hover {
+    background: $ui-secondary-color;
+  }
+
+  &.active {
+    background: $ui-highlight-color;
+    color: $primary-text-color;
+  }
+
+  &.close, &.close:hover {
+    background: $error-value-color;
+    color: $primary-text-color;
+  }
+}
+
+.glitch.local-settings__navigation {
+  background: $primary-text-color;
+  color: $ui-base-color;
+  width: 200px;
+  font-size: 15px;
+  line-height: 20px;
+  overflow-y: auto;
+}
+
+.glitch.local-settings__page {
+  display: block;
+  flex: auto;
+  padding: 15px 20px 15px 20px;
+  width: 360px;
+  overflow-y: auto;
+}
+
+.glitch.local-settings__page__item {
+  select {
+    margin-bottom: 5px;
+  }
+}
diff --git a/app/javascript/themes/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss
index af2589e23..af2589e23 100644
--- a/app/javascript/themes/glitch/styles/containers.scss
+++ b/app/javascript/flavours/glitch/styles/containers.scss
diff --git a/app/javascript/themes/glitch/styles/footer.scss b/app/javascript/flavours/glitch/styles/footer.scss
index 2d953b34e..2d953b34e 100644
--- a/app/javascript/themes/glitch/styles/footer.scss
+++ b/app/javascript/flavours/glitch/styles/footer.scss
diff --git a/app/javascript/themes/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss
index 61fcf286f..61fcf286f 100644
--- a/app/javascript/themes/glitch/styles/forms.scss
+++ b/app/javascript/flavours/glitch/styles/forms.scss
diff --git a/app/javascript/themes/glitch/styles/index.scss b/app/javascript/flavours/glitch/styles/index.scss
index c962a1f62..a5169f881 100644
--- a/app/javascript/themes/glitch/styles/index.scss
+++ b/app/javascript/flavours/glitch/styles/index.scss
@@ -14,8 +14,7 @@
 @import 'forms';
 @import 'accounts';
 @import 'stream_entries';
-@import 'components';
-@import 'emoji_picker';
+@import 'components/index';
 @import 'about';
 @import 'tables';
 @import 'admin';
diff --git a/app/javascript/themes/glitch/styles/landing_strip.scss b/app/javascript/flavours/glitch/styles/landing_strip.scss
index 0bf9daafd..0bf9daafd 100644
--- a/app/javascript/themes/glitch/styles/landing_strip.scss
+++ b/app/javascript/flavours/glitch/styles/landing_strip.scss
diff --git a/app/javascript/themes/glitch/styles/lists.scss b/app/javascript/flavours/glitch/styles/lists.scss
index 6019cd800..6019cd800 100644
--- a/app/javascript/themes/glitch/styles/lists.scss
+++ b/app/javascript/flavours/glitch/styles/lists.scss
diff --git a/app/javascript/themes/glitch/styles/reset.scss b/app/javascript/flavours/glitch/styles/reset.scss
index cc5ba9d7c..cc5ba9d7c 100644
--- a/app/javascript/themes/glitch/styles/reset.scss
+++ b/app/javascript/flavours/glitch/styles/reset.scss
diff --git a/app/javascript/themes/glitch/styles/rtl.scss b/app/javascript/flavours/glitch/styles/rtl.scss
index 67bfa8a38..67bfa8a38 100644
--- a/app/javascript/themes/glitch/styles/rtl.scss
+++ b/app/javascript/flavours/glitch/styles/rtl.scss
diff --git a/app/javascript/themes/glitch/styles/stream_entries.scss b/app/javascript/flavours/glitch/styles/stream_entries.scss
index 453070b7c..453070b7c 100644
--- a/app/javascript/themes/glitch/styles/stream_entries.scss
+++ b/app/javascript/flavours/glitch/styles/stream_entries.scss
diff --git a/app/javascript/themes/glitch/styles/tables.scss b/app/javascript/flavours/glitch/styles/tables.scss
index ad46f5f9f..ad46f5f9f 100644
--- a/app/javascript/themes/glitch/styles/tables.scss
+++ b/app/javascript/flavours/glitch/styles/tables.scss
diff --git a/app/javascript/themes/glitch/styles/variables.scss b/app/javascript/flavours/glitch/styles/variables.scss
index f42d9c8c5..f42d9c8c5 100644
--- a/app/javascript/themes/glitch/styles/variables.scss
+++ b/app/javascript/flavours/glitch/styles/variables.scss
diff --git a/app/javascript/flavours/glitch/theme.yml b/app/javascript/flavours/glitch/theme.yml
new file mode 100644
index 000000000..fe09fa105
--- /dev/null
+++ b/app/javascript/flavours/glitch/theme.yml
@@ -0,0 +1,33 @@
+#  (REQUIRED) The location of the pack files.
+pack:
+  about: packs/about.js
+  admin:
+  auth:
+  common:
+    filename: packs/common.js
+    stylesheet: true
+  embed: packs/public.js
+  error:
+  home:
+    filename: packs/home.js
+    preload:
+    - flavours/glitch/async/getting_started
+    - flavours/glitch/async/compose
+    - flavours/glitch/async/home_timeline
+    - flavours/glitch/async/notifications
+  modal:
+  public: packs/public.js
+  settings:
+  share: packs/share.js
+
+#  (OPTIONAL) The directory which contains the pack files.
+#  Defaults to the theme directory (`app/javascript/themes/[theme]`),
+#  which should be sufficient for like 99% of use-cases lol.
+
+#      pack_directory: app/javascript/packs
+
+#  (OPTIONAL) By default the theme will fallback to the default theme
+#  if a particular pack is not provided. You can specify different
+#  fallbacks here, or disable fallback behaviours altogether by
+#  specifying a `null` value.
+fallback:
diff --git a/app/javascript/themes/glitch/util/api.js b/app/javascript/flavours/glitch/util/api.js
index ecc703c0a..ecc703c0a 100644
--- a/app/javascript/themes/glitch/util/api.js
+++ b/app/javascript/flavours/glitch/util/api.js
diff --git a/app/javascript/flavours/glitch/util/async-components.js b/app/javascript/flavours/glitch/util/async-components.js
new file mode 100644
index 000000000..b7ac4d3ec
--- /dev/null
+++ b/app/javascript/flavours/glitch/util/async-components.js
@@ -0,0 +1,115 @@
+export function EmojiPicker () {
+  return import(/* webpackChunkName: "flavours/glitch/async/emoji_picker" */'flavours/glitch/util/emoji/emoji_picker');
+}
+
+export function Compose () {
+  return import(/* webpackChunkName: "flavours/glitch/async/compose" */'flavours/glitch/features/compose');
+}
+
+export function Notifications () {
+  return import(/* webpackChunkName: "flavours/glitch/async/notifications" */'flavours/glitch/features/notifications');
+}
+
+export function HomeTimeline () {
+  return import(/* webpackChunkName: "flavours/glitch/async/home_timeline" */'flavours/glitch/features/home_timeline');
+}
+
+export function PublicTimeline () {
+  return import(/* webpackChunkName: "flavours/glitch/async/public_timeline" */'flavours/glitch/features/public_timeline');
+}
+
+export function CommunityTimeline () {
+  return import(/* webpackChunkName: "flavours/glitch/async/community_timeline" */'flavours/glitch/features/community_timeline');
+}
+
+export function HashtagTimeline () {
+  return import(/* webpackChunkName: "flavours/glitch/async/hashtag_timeline" */'flavours/glitch/features/hashtag_timeline');
+}
+
+export function DirectTimeline() {
+  return import(/* webpackChunkName: "flavours/glitch/async/direct_timeline" */'flavours/glitch/features/direct_timeline');
+}
+
+export function Status () {
+  return import(/* webpackChunkName: "flavours/glitch/async/status" */'flavours/glitch/features/status');
+}
+
+export function GettingStarted () {
+  return import(/* webpackChunkName: "flavours/glitch/async/getting_started" */'flavours/glitch/features/getting_started');
+}
+
+export function PinnedStatuses () {
+  return import(/* webpackChunkName: "flavours/glitch/async/pinned_statuses" */'flavours/glitch/features/pinned_statuses');
+}
+
+export function AccountTimeline () {
+  return import(/* webpackChunkName: "flavours/glitch/async/account_timeline" */'flavours/glitch/features/account_timeline');
+}
+
+export function AccountGallery () {
+  return import(/* webpackChunkName: "flavours/glitch/async/account_gallery" */'flavours/glitch/features/account_gallery');
+}
+
+export function Followers () {
+  return import(/* webpackChunkName: "flavours/glitch/async/followers" */'flavours/glitch/features/followers');
+}
+
+export function Following () {
+  return import(/* webpackChunkName: "flavours/glitch/async/following" */'flavours/glitch/features/following');
+}
+
+export function Reblogs () {
+  return import(/* webpackChunkName: "flavours/glitch/async/reblogs" */'flavours/glitch/features/reblogs');
+}
+
+export function Favourites () {
+  return import(/* webpackChunkName: "flavours/glitch/async/favourites" */'flavours/glitch/features/favourites');
+}
+
+export function FollowRequests () {
+  return import(/* webpackChunkName: "flavours/glitch/async/follow_requests" */'flavours/glitch/features/follow_requests');
+}
+
+export function GenericNotFound () {
+  return import(/* webpackChunkName: "flavours/glitch/async/generic_not_found" */'flavours/glitch/features/generic_not_found');
+}
+
+export function FavouritedStatuses () {
+  return import(/* webpackChunkName: "flavours/glitch/async/favourited_statuses" */'flavours/glitch/features/favourited_statuses');
+}
+
+export function Blocks () {
+  return import(/* webpackChunkName: "flavours/glitch/async/blocks" */'flavours/glitch/features/blocks');
+}
+
+export function Mutes () {
+  return import(/* webpackChunkName: "flavours/glitch/async/mutes" */'flavours/glitch/features/mutes');
+}
+
+export function OnboardingModal () {
+  return import(/* webpackChunkName: "flavours/glitch/async/onboarding_modal" */'flavours/glitch/features/ui/components/onboarding_modal');
+}
+
+export function MuteModal () {
+  return import(/* webpackChunkName: "flavours/glitch/async/mute_modal" */'flavours/glitch/features/ui/components/mute_modal');
+}
+
+export function ReportModal () {
+  return import(/* webpackChunkName: "flavours/glitch/async/report_modal" */'flavours/glitch/features/ui/components/report_modal');
+}
+
+export function SettingsModal () {
+  return import(/* webpackChunkName: "flavours/glitch/async/settings_modal" */'flavours/glitch/features/local_settings');
+}
+
+export function MediaGallery () {
+  return import(/* webpackChunkName: "flavours/glitch/async/media_gallery" */'flavours/glitch/components/media_gallery');
+}
+
+export function Video () {
+  return import(/* webpackChunkName: "flavours/glitch/async/video" */'flavours/glitch/features/video');
+}
+
+export function EmbedModal () {
+  return import(/* webpackChunkName: "flavours/glitch/async/embed_modal" */'flavours/glitch/features/ui/components/embed_modal');
+}
diff --git a/app/javascript/themes/glitch/util/base_polyfills.js b/app/javascript/flavours/glitch/util/base_polyfills.js
index 7856b26f9..7856b26f9 100644
--- a/app/javascript/themes/glitch/util/base_polyfills.js
+++ b/app/javascript/flavours/glitch/util/base_polyfills.js
diff --git a/app/javascript/themes/glitch/util/bio_metadata.js b/app/javascript/flavours/glitch/util/bio_metadata.js
index 599ec20e2..599ec20e2 100644
--- a/app/javascript/themes/glitch/util/bio_metadata.js
+++ b/app/javascript/flavours/glitch/util/bio_metadata.js
diff --git a/app/javascript/themes/glitch/util/counter.js b/app/javascript/flavours/glitch/util/counter.js
index 700ba2163..700ba2163 100644
--- a/app/javascript/themes/glitch/util/counter.js
+++ b/app/javascript/flavours/glitch/util/counter.js
diff --git a/app/javascript/themes/glitch/util/emoji/emoji_compressed.js b/app/javascript/flavours/glitch/util/emoji/emoji_compressed.js
index e5b834a74..e5b834a74 100644
--- a/app/javascript/themes/glitch/util/emoji/emoji_compressed.js
+++ b/app/javascript/flavours/glitch/util/emoji/emoji_compressed.js
diff --git a/app/javascript/themes/glitch/util/emoji/emoji_map.json b/app/javascript/flavours/glitch/util/emoji/emoji_map.json
index 13753ba84..13753ba84 100644
--- a/app/javascript/themes/glitch/util/emoji/emoji_map.json
+++ b/app/javascript/flavours/glitch/util/emoji/emoji_map.json
diff --git a/app/javascript/themes/glitch/util/emoji/emoji_mart_data_light.js b/app/javascript/flavours/glitch/util/emoji/emoji_mart_data_light.js
index 45086fc4c..45086fc4c 100644
--- a/app/javascript/themes/glitch/util/emoji/emoji_mart_data_light.js
+++ b/app/javascript/flavours/glitch/util/emoji/emoji_mart_data_light.js
diff --git a/app/javascript/themes/glitch/util/emoji/emoji_mart_search_light.js b/app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js
index 5755bf1c4..5755bf1c4 100644
--- a/app/javascript/themes/glitch/util/emoji/emoji_mart_search_light.js
+++ b/app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js
diff --git a/app/javascript/themes/glitch/util/emoji/emoji_picker.js b/app/javascript/flavours/glitch/util/emoji/emoji_picker.js
index 7e145381e..7e145381e 100644
--- a/app/javascript/themes/glitch/util/emoji/emoji_picker.js
+++ b/app/javascript/flavours/glitch/util/emoji/emoji_picker.js
diff --git a/app/javascript/themes/glitch/util/emoji/emoji_unicode_mapping_light.js b/app/javascript/flavours/glitch/util/emoji/emoji_unicode_mapping_light.js
index 918684c31..918684c31 100644
--- a/app/javascript/themes/glitch/util/emoji/emoji_unicode_mapping_light.js
+++ b/app/javascript/flavours/glitch/util/emoji/emoji_unicode_mapping_light.js
diff --git a/app/javascript/themes/glitch/util/emoji/emoji_utils.js b/app/javascript/flavours/glitch/util/emoji/emoji_utils.js
index dbf725c1f..dbf725c1f 100644
--- a/app/javascript/themes/glitch/util/emoji/emoji_utils.js
+++ b/app/javascript/flavours/glitch/util/emoji/emoji_utils.js
diff --git a/app/javascript/themes/glitch/util/emoji/index.js b/app/javascript/flavours/glitch/util/emoji/index.js
index 8c45a58fe..31c3e14ca 100644
--- a/app/javascript/themes/glitch/util/emoji/index.js
+++ b/app/javascript/flavours/glitch/util/emoji/index.js
@@ -1,4 +1,4 @@
-import { autoPlayGif } from 'themes/glitch/util/initial_state';
+import { autoPlayGif } from 'flavours/glitch/util/initial_state';
 import unicodeMapping from './emoji_unicode_mapping_light';
 import Trie from 'substring-trie';
 
diff --git a/app/javascript/themes/glitch/util/emoji/unicode_to_filename.js b/app/javascript/flavours/glitch/util/emoji/unicode_to_filename.js
index c75c4cd7d..c75c4cd7d 100644
--- a/app/javascript/themes/glitch/util/emoji/unicode_to_filename.js
+++ b/app/javascript/flavours/glitch/util/emoji/unicode_to_filename.js
diff --git a/app/javascript/themes/glitch/util/emoji/unicode_to_unified_name.js b/app/javascript/flavours/glitch/util/emoji/unicode_to_unified_name.js
index 808ac197e..808ac197e 100644
--- a/app/javascript/themes/glitch/util/emoji/unicode_to_unified_name.js
+++ b/app/javascript/flavours/glitch/util/emoji/unicode_to_unified_name.js
diff --git a/app/javascript/themes/glitch/util/extra_polyfills.js b/app/javascript/flavours/glitch/util/extra_polyfills.js
index 3acc55abd..3acc55abd 100644
--- a/app/javascript/themes/glitch/util/extra_polyfills.js
+++ b/app/javascript/flavours/glitch/util/extra_polyfills.js
diff --git a/app/javascript/themes/glitch/util/fullscreen.js b/app/javascript/flavours/glitch/util/fullscreen.js
index cf5d0cf98..cf5d0cf98 100644
--- a/app/javascript/themes/glitch/util/fullscreen.js
+++ b/app/javascript/flavours/glitch/util/fullscreen.js
diff --git a/app/javascript/themes/glitch/util/get_rect_from_entry.js b/app/javascript/flavours/glitch/util/get_rect_from_entry.js
index c266cd7dc..c266cd7dc 100644
--- a/app/javascript/themes/glitch/util/get_rect_from_entry.js
+++ b/app/javascript/flavours/glitch/util/get_rect_from_entry.js
diff --git a/app/javascript/themes/glitch/util/initial_state.js b/app/javascript/flavours/glitch/util/initial_state.js
index ef5d8b0ef..ef5d8b0ef 100644
--- a/app/javascript/themes/glitch/util/initial_state.js
+++ b/app/javascript/flavours/glitch/util/initial_state.js
diff --git a/app/javascript/themes/glitch/util/intersection_observer_wrapper.js b/app/javascript/flavours/glitch/util/intersection_observer_wrapper.js
index 2b24c6583..2b24c6583 100644
--- a/app/javascript/themes/glitch/util/intersection_observer_wrapper.js
+++ b/app/javascript/flavours/glitch/util/intersection_observer_wrapper.js
diff --git a/app/javascript/themes/glitch/util/is_mobile.js b/app/javascript/flavours/glitch/util/is_mobile.js
index 80e8e0a8a..80e8e0a8a 100644
--- a/app/javascript/themes/glitch/util/is_mobile.js
+++ b/app/javascript/flavours/glitch/util/is_mobile.js
diff --git a/app/javascript/themes/glitch/util/link_header.js b/app/javascript/flavours/glitch/util/link_header.js
index a3e7ccf1c..a3e7ccf1c 100644
--- a/app/javascript/themes/glitch/util/link_header.js
+++ b/app/javascript/flavours/glitch/util/link_header.js
diff --git a/app/javascript/themes/glitch/util/load_polyfills.js b/app/javascript/flavours/glitch/util/load_polyfills.js
index 8927b7358..8927b7358 100644
--- a/app/javascript/themes/glitch/util/load_polyfills.js
+++ b/app/javascript/flavours/glitch/util/load_polyfills.js
diff --git a/app/javascript/themes/glitch/util/main.js b/app/javascript/flavours/glitch/util/main.js
index c10a64ded..fe57fa962 100644
--- a/app/javascript/themes/glitch/util/main.js
+++ b/app/javascript/flavours/glitch/util/main.js
@@ -1,5 +1,5 @@
 import * as WebPushSubscription from './web_push_subscription';
-import Mastodon from 'themes/glitch/containers/mastodon';
+import Mastodon from 'flavours/glitch/containers/mastodon';
 import React from 'react';
 import ReactDOM from 'react-dom';
 import ready from './ready';
diff --git a/app/javascript/themes/glitch/util/optional_motion.js b/app/javascript/flavours/glitch/util/optional_motion.js
index b8a57b22f..eecb6634e 100644
--- a/app/javascript/themes/glitch/util/optional_motion.js
+++ b/app/javascript/flavours/glitch/util/optional_motion.js
@@ -1,4 +1,4 @@
-import { reduceMotion } from 'themes/glitch/util/initial_state';
+import { reduceMotion } from 'flavours/glitch/util/initial_state';
 import ReducedMotion from './reduced_motion';
 import Motion from 'react-motion/lib/Motion';
 
diff --git a/app/javascript/themes/glitch/util/performance.js b/app/javascript/flavours/glitch/util/performance.js
index 450a90626..450a90626 100644
--- a/app/javascript/themes/glitch/util/performance.js
+++ b/app/javascript/flavours/glitch/util/performance.js
diff --git a/app/javascript/themes/glitch/util/react_router_helpers.js b/app/javascript/flavours/glitch/util/react_router_helpers.js
index c02fb5247..1dba5e9bb 100644
--- a/app/javascript/themes/glitch/util/react_router_helpers.js
+++ b/app/javascript/flavours/glitch/util/react_router_helpers.js
@@ -2,9 +2,9 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { Switch, Route } from 'react-router-dom';
 
-import ColumnLoading from 'themes/glitch/features/ui/components/column_loading';
-import BundleColumnError from 'themes/glitch/features/ui/components/bundle_column_error';
-import BundleContainer from 'themes/glitch/features/ui/containers/bundle_container';
+import ColumnLoading from 'flavours/glitch/features/ui/components/column_loading';
+import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
+import BundleContainer from 'flavours/glitch/features/ui/containers/bundle_container';
 
 // Small wrapper to pass multiColumn to the route components
 export class WrappedSwitch extends React.PureComponent {
diff --git a/app/javascript/themes/glitch/util/ready.js b/app/javascript/flavours/glitch/util/ready.js
index dd543910b..dd543910b 100644
--- a/app/javascript/themes/glitch/util/ready.js
+++ b/app/javascript/flavours/glitch/util/ready.js
diff --git a/app/javascript/themes/glitch/util/reduced_motion.js b/app/javascript/flavours/glitch/util/reduced_motion.js
index 95519042b..95519042b 100644
--- a/app/javascript/themes/glitch/util/reduced_motion.js
+++ b/app/javascript/flavours/glitch/util/reduced_motion.js
diff --git a/app/javascript/themes/glitch/util/rtl.js b/app/javascript/flavours/glitch/util/rtl.js
index 00870a15d..00870a15d 100644
--- a/app/javascript/themes/glitch/util/rtl.js
+++ b/app/javascript/flavours/glitch/util/rtl.js
diff --git a/app/javascript/themes/glitch/util/schedule_idle_task.js b/app/javascript/flavours/glitch/util/schedule_idle_task.js
index b04d4a8ee..b04d4a8ee 100644
--- a/app/javascript/themes/glitch/util/schedule_idle_task.js
+++ b/app/javascript/flavours/glitch/util/schedule_idle_task.js
diff --git a/app/javascript/themes/glitch/util/scroll.js b/app/javascript/flavours/glitch/util/scroll.js
index 2af07e0fb..2af07e0fb 100644
--- a/app/javascript/themes/glitch/util/scroll.js
+++ b/app/javascript/flavours/glitch/util/scroll.js
diff --git a/app/javascript/themes/glitch/util/stream.js b/app/javascript/flavours/glitch/util/stream.js
index 36c68ffc5..36c68ffc5 100644
--- a/app/javascript/themes/glitch/util/stream.js
+++ b/app/javascript/flavours/glitch/util/stream.js
diff --git a/app/javascript/themes/glitch/util/url_regex.js b/app/javascript/flavours/glitch/util/url_regex.js
index e676d1879..e676d1879 100644
--- a/app/javascript/themes/glitch/util/url_regex.js
+++ b/app/javascript/flavours/glitch/util/url_regex.js
diff --git a/app/javascript/themes/glitch/util/uuid.js b/app/javascript/flavours/glitch/util/uuid.js
index be1899305..be1899305 100644
--- a/app/javascript/themes/glitch/util/uuid.js
+++ b/app/javascript/flavours/glitch/util/uuid.js
diff --git a/app/javascript/themes/glitch/util/web_push_subscription.js b/app/javascript/flavours/glitch/util/web_push_subscription.js
index 70b26105b..de185b6d9 100644
--- a/app/javascript/themes/glitch/util/web_push_subscription.js
+++ b/app/javascript/flavours/glitch/util/web_push_subscription.js
@@ -1,6 +1,6 @@
 import axios from 'axios';
-import { store } from 'themes/glitch/containers/mastodon';
-import { setBrowserSupport, setSubscription, clearSubscription } from 'themes/glitch/actions/push_notifications';
+import { store } from 'flavours/glitch/containers/mastodon';
+import { setBrowserSupport, setSubscription, clearSubscription } from 'flavours/glitch/actions/push_notifications';
 
 // Taken from https://www.npmjs.com/package/web-push
 const urlBase64ToUint8Array = (base64String) => {
diff --git a/app/javascript/flavours/vanilla/theme.yml b/app/javascript/flavours/vanilla/theme.yml
new file mode 100644
index 000000000..67fd9723e
--- /dev/null
+++ b/app/javascript/flavours/vanilla/theme.yml
@@ -0,0 +1,33 @@
+#  (REQUIRED) The location of the pack files inside `pack_directory`.
+pack:
+  about: about.js
+  admin:
+  auth:
+  common:
+    filename: common.js
+    stylesheet: true
+  embed: public.js
+  error:
+  home:
+    filename: application.js
+    preload:
+    - features/getting_started
+    - features/compose
+    - features/home_timeline
+    - features/notifications
+  modal:
+  public: public.js
+  settings:
+  share: share.js
+
+#  (OPTIONAL) The directory which contains the pack files.
+#  Defaults to the theme directory (`app/javascript/themes/[theme]`),
+#  but in the case of the vanilla Mastodon theme the pack files are
+#  somewhere else.
+pack_directory: app/javascript/packs
+
+#  (OPTIONAL) By default the theme will fallback to the default theme
+#  if a particular pack is not provided. You can specify different
+#  fallbacks here, or disable fallback behaviours altogether by
+#  specifying a `null` value.
+fallback:
diff --git a/app/javascript/fonts/premillenium/MSSansSerif.ttf b/app/javascript/fonts/premillenium/MSSansSerif.ttf
new file mode 100644
index 000000000..3afd76ff2
--- /dev/null
+++ b/app/javascript/fonts/premillenium/MSSansSerif.ttf
Binary files differdiff --git a/app/javascript/glitch/locales/en.json b/app/javascript/glitch/locales/en.json
index 69aa29108..0276cb837 100644
--- a/app/javascript/glitch/locales/en.json
+++ b/app/javascript/glitch/locales/en.json
@@ -32,6 +32,8 @@
   "status.collapse": "Collapse",
   "status.uncollapse": "Uncollapse",
 
+  "home.column_settings.show_direct": "Show DMs",
+
   "notification.markForDeletion": "Mark for deletion",
   "notifications.clear": "Clear all my notifications",
   "notifications.marked_clear_confirmation": "Are you sure you want to permanently clear all selected notifications?",
diff --git a/app/javascript/images/icon_about.png b/app/javascript/images/icon_about.png
new file mode 100644
index 000000000..08b76dcd9
--- /dev/null
+++ b/app/javascript/images/icon_about.png
Binary files differdiff --git a/app/javascript/images/icon_blocks.png b/app/javascript/images/icon_blocks.png
new file mode 100644
index 000000000..8b1490875
--- /dev/null
+++ b/app/javascript/images/icon_blocks.png
Binary files differdiff --git a/app/javascript/images/icon_home.png b/app/javascript/images/icon_home.png
new file mode 100644
index 000000000..66ce779c0
--- /dev/null
+++ b/app/javascript/images/icon_home.png
Binary files differdiff --git a/app/javascript/images/icon_likes.png b/app/javascript/images/icon_likes.png
new file mode 100644
index 000000000..17d7a9c59
--- /dev/null
+++ b/app/javascript/images/icon_likes.png
Binary files differdiff --git a/app/javascript/images/icon_local.png b/app/javascript/images/icon_local.png
new file mode 100644
index 000000000..5f82df395
--- /dev/null
+++ b/app/javascript/images/icon_local.png
Binary files differdiff --git a/app/javascript/images/icon_logout.png b/app/javascript/images/icon_logout.png
new file mode 100644
index 000000000..7ff806f58
--- /dev/null
+++ b/app/javascript/images/icon_logout.png
Binary files differdiff --git a/app/javascript/images/icon_mutes.png b/app/javascript/images/icon_mutes.png
new file mode 100644
index 000000000..c2225e966
--- /dev/null
+++ b/app/javascript/images/icon_mutes.png
Binary files differdiff --git a/app/javascript/images/icon_pin.png b/app/javascript/images/icon_pin.png
new file mode 100644
index 000000000..2329d8c54
--- /dev/null
+++ b/app/javascript/images/icon_pin.png
Binary files differdiff --git a/app/javascript/images/icon_public.png b/app/javascript/images/icon_public.png
new file mode 100644
index 000000000..3c09460db
--- /dev/null
+++ b/app/javascript/images/icon_public.png
Binary files differdiff --git a/app/javascript/images/icon_settings.png b/app/javascript/images/icon_settings.png
new file mode 100644
index 000000000..07f5c4519
--- /dev/null
+++ b/app/javascript/images/icon_settings.png
Binary files differdiff --git a/app/javascript/images/start.png b/app/javascript/images/start.png
new file mode 100644
index 000000000..7843455b6
--- /dev/null
+++ b/app/javascript/images/start.png
Binary files differdiff --git a/app/javascript/locales/index.js b/app/javascript/locales/index.js
new file mode 100644
index 000000000..421cb7fab
--- /dev/null
+++ b/app/javascript/locales/index.js
@@ -0,0 +1,9 @@
+let theLocale;
+
+export function setLocale(locale) {
+  theLocale = locale;
+}
+
+export function getLocale() {
+  return theLocale;
+}
diff --git a/app/javascript/mastodon/actions/lists.js b/app/javascript/mastodon/actions/lists.js
index 332e42166..4c8f9b186 100644
--- a/app/javascript/mastodon/actions/lists.js
+++ b/app/javascript/mastodon/actions/lists.js
@@ -4,12 +4,52 @@ export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
 export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
 export const LIST_FETCH_FAIL    = 'LIST_FETCH_FAIL';
 
+export const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST';
+export const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS';
+export const LISTS_FETCH_FAIL    = 'LISTS_FETCH_FAIL';
+
+export const LIST_EDITOR_TITLE_CHANGE = 'LIST_EDITOR_TITLE_CHANGE';
+export const LIST_EDITOR_RESET        = 'LIST_EDITOR_RESET';
+export const LIST_EDITOR_SETUP        = 'LIST_EDITOR_SETUP';
+
+export const LIST_CREATE_REQUEST = 'LIST_CREATE_REQUEST';
+export const LIST_CREATE_SUCCESS = 'LIST_CREATE_SUCCESS';
+export const LIST_CREATE_FAIL    = 'LIST_CREATE_FAIL';
+
+export const LIST_UPDATE_REQUEST = 'LIST_UPDATE_REQUEST';
+export const LIST_UPDATE_SUCCESS = 'LIST_UPDATE_SUCCESS';
+export const LIST_UPDATE_FAIL    = 'LIST_UPDATE_FAIL';
+
+export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST';
+export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS';
+export const LIST_DELETE_FAIL    = 'LIST_DELETE_FAIL';
+
+export const LIST_ACCOUNTS_FETCH_REQUEST = 'LIST_ACCOUNTS_FETCH_REQUEST';
+export const LIST_ACCOUNTS_FETCH_SUCCESS = 'LIST_ACCOUNTS_FETCH_SUCCESS';
+export const LIST_ACCOUNTS_FETCH_FAIL    = 'LIST_ACCOUNTS_FETCH_FAIL';
+
+export const LIST_EDITOR_SUGGESTIONS_CHANGE = 'LIST_EDITOR_SUGGESTIONS_CHANGE';
+export const LIST_EDITOR_SUGGESTIONS_READY  = 'LIST_EDITOR_SUGGESTIONS_READY';
+export const LIST_EDITOR_SUGGESTIONS_CLEAR  = 'LIST_EDITOR_SUGGESTIONS_CLEAR';
+
+export const LIST_EDITOR_ADD_REQUEST = 'LIST_EDITOR_ADD_REQUEST';
+export const LIST_EDITOR_ADD_SUCCESS = 'LIST_EDITOR_ADD_SUCCESS';
+export const LIST_EDITOR_ADD_FAIL    = 'LIST_EDITOR_ADD_FAIL';
+
+export const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST';
+export const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS';
+export const LIST_EDITOR_REMOVE_FAIL    = 'LIST_EDITOR_REMOVE_FAIL';
+
 export const fetchList = id => (dispatch, getState) => {
+  if (getState().getIn(['lists', id])) {
+    return;
+  }
+
   dispatch(fetchListRequest(id));
 
   api(getState).get(`/api/v1/lists/${id}`)
     .then(({ data }) => dispatch(fetchListSuccess(data)))
-    .catch(err => dispatch(fetchListFail(err)));
+    .catch(err => dispatch(fetchListFail(id, err)));
 };
 
 export const fetchListRequest = id => ({
@@ -22,7 +62,252 @@ export const fetchListSuccess = list => ({
   list,
 });
 
-export const fetchListFail = error => ({
+export const fetchListFail = (id, error) => ({
   type: LIST_FETCH_FAIL,
+  id,
+  error,
+});
+
+export const fetchLists = () => (dispatch, getState) => {
+  dispatch(fetchListsRequest());
+
+  api(getState).get('/api/v1/lists')
+    .then(({ data }) => dispatch(fetchListsSuccess(data)))
+    .catch(err => dispatch(fetchListsFail(err)));
+};
+
+export const fetchListsRequest = () => ({
+  type: LISTS_FETCH_REQUEST,
+});
+
+export const fetchListsSuccess = lists => ({
+  type: LISTS_FETCH_SUCCESS,
+  lists,
+});
+
+export const fetchListsFail = error => ({
+  type: LISTS_FETCH_FAIL,
+  error,
+});
+
+export const submitListEditor = shouldReset => (dispatch, getState) => {
+  const listId = getState().getIn(['listEditor', 'listId']);
+  const title  = getState().getIn(['listEditor', 'title']);
+
+  if (listId === null) {
+    dispatch(createList(title, shouldReset));
+  } else {
+    dispatch(updateList(listId, title, shouldReset));
+  }
+};
+
+export const setupListEditor = listId => (dispatch, getState) => {
+  dispatch({
+    type: LIST_EDITOR_SETUP,
+    list: getState().getIn(['lists', listId]),
+  });
+
+  dispatch(fetchListAccounts(listId));
+};
+
+export const changeListEditorTitle = value => ({
+  type: LIST_EDITOR_TITLE_CHANGE,
+  value,
+});
+
+export const createList = (title, shouldReset) => (dispatch, getState) => {
+  dispatch(createListRequest());
+
+  api(getState).post('/api/v1/lists', { title }).then(({ data }) => {
+    dispatch(createListSuccess(data));
+
+    if (shouldReset) {
+      dispatch(resetListEditor());
+    }
+  }).catch(err => dispatch(createListFail(err)));
+};
+
+export const createListRequest = () => ({
+  type: LIST_CREATE_REQUEST,
+});
+
+export const createListSuccess = list => ({
+  type: LIST_CREATE_SUCCESS,
+  list,
+});
+
+export const createListFail = error => ({
+  type: LIST_CREATE_FAIL,
+  error,
+});
+
+export const updateList = (id, title, shouldReset) => (dispatch, getState) => {
+  dispatch(updateListRequest(id));
+
+  api(getState).put(`/api/v1/lists/${id}`, { title }).then(({ data }) => {
+    dispatch(updateListSuccess(data));
+
+    if (shouldReset) {
+      dispatch(resetListEditor());
+    }
+  }).catch(err => dispatch(updateListFail(id, err)));
+};
+
+export const updateListRequest = id => ({
+  type: LIST_UPDATE_REQUEST,
+  id,
+});
+
+export const updateListSuccess = list => ({
+  type: LIST_UPDATE_SUCCESS,
+  list,
+});
+
+export const updateListFail = (id, error) => ({
+  type: LIST_UPDATE_FAIL,
+  id,
+  error,
+});
+
+export const resetListEditor = () => ({
+  type: LIST_EDITOR_RESET,
+});
+
+export const deleteList = id => (dispatch, getState) => {
+  dispatch(deleteListRequest(id));
+
+  api(getState).delete(`/api/v1/lists/${id}`)
+    .then(() => dispatch(deleteListSuccess(id)))
+    .catch(err => dispatch(deleteListFail(id, err)));
+};
+
+export const deleteListRequest = id => ({
+  type: LIST_DELETE_REQUEST,
+  id,
+});
+
+export const deleteListSuccess = id => ({
+  type: LIST_DELETE_SUCCESS,
+  id,
+});
+
+export const deleteListFail = (id, error) => ({
+  type: LIST_DELETE_FAIL,
+  id,
+  error,
+});
+
+export const fetchListAccounts = listId => (dispatch, getState) => {
+  dispatch(fetchListAccountsRequest(listId));
+
+  api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } })
+    .then(({ data }) => dispatch(fetchListAccountsSuccess(listId, data)))
+    .catch(err => dispatch(fetchListAccountsFail(listId, err)));
+};
+
+export const fetchListAccountsRequest = id => ({
+  type: LIST_ACCOUNTS_FETCH_REQUEST,
+  id,
+});
+
+export const fetchListAccountsSuccess = (id, accounts, next) => ({
+  type: LIST_ACCOUNTS_FETCH_SUCCESS,
+  id,
+  accounts,
+  next,
+});
+
+export const fetchListAccountsFail = (id, error) => ({
+  type: LIST_ACCOUNTS_FETCH_FAIL,
+  id,
+  error,
+});
+
+export const fetchListSuggestions = q => (dispatch, getState) => {
+  const params = {
+    q,
+    resolve: false,
+    limit: 4,
+    following: true,
+  };
+
+  api(getState).get('/api/v1/accounts/search', { params })
+    .then(({ data }) => dispatch(fetchListSuggestionsReady(q, data)));
+};
+
+export const fetchListSuggestionsReady = (query, accounts) => ({
+  type: LIST_EDITOR_SUGGESTIONS_READY,
+  query,
+  accounts,
+});
+
+export const clearListSuggestions = () => ({
+  type: LIST_EDITOR_SUGGESTIONS_CLEAR,
+});
+
+export const changeListSuggestions = value => ({
+  type: LIST_EDITOR_SUGGESTIONS_CHANGE,
+  value,
+});
+
+export const addToListEditor = accountId => (dispatch, getState) => {
+  dispatch(addToList(getState().getIn(['listEditor', 'listId']), accountId));
+};
+
+export const addToList = (listId, accountId) => (dispatch, getState) => {
+  dispatch(addToListRequest(listId, accountId));
+
+  api(getState).post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] })
+    .then(() => dispatch(addToListSuccess(listId, accountId)))
+    .catch(err => dispatch(addToListFail(listId, accountId, err)));
+};
+
+export const addToListRequest = (listId, accountId) => ({
+  type: LIST_EDITOR_ADD_REQUEST,
+  listId,
+  accountId,
+});
+
+export const addToListSuccess = (listId, accountId) => ({
+  type: LIST_EDITOR_ADD_SUCCESS,
+  listId,
+  accountId,
+});
+
+export const addToListFail = (listId, accountId, error) => ({
+  type: LIST_EDITOR_ADD_FAIL,
+  listId,
+  accountId,
+  error,
+});
+
+export const removeFromListEditor = accountId => (dispatch, getState) => {
+  dispatch(removeFromList(getState().getIn(['listEditor', 'listId']), accountId));
+};
+
+export const removeFromList = (listId, accountId) => (dispatch, getState) => {
+  dispatch(removeFromListRequest(listId, accountId));
+
+  api(getState).delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } })
+    .then(() => dispatch(removeFromListSuccess(listId, accountId)))
+    .catch(err => dispatch(removeFromListFail(listId, accountId, err)));
+};
+
+export const removeFromListRequest = (listId, accountId) => ({
+  type: LIST_EDITOR_REMOVE_REQUEST,
+  listId,
+  accountId,
+});
+
+export const removeFromListSuccess = (listId, accountId) => ({
+  type: LIST_EDITOR_REMOVE_SUCCESS,
+  listId,
+  accountId,
+});
+
+export const removeFromListFail = (listId, accountId, error) => ({
+  type: LIST_EDITOR_REMOVE_FAIL,
+  listId,
+  accountId,
   error,
 });
diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js
index 1f2d7690f..b0479db4f 100644
--- a/app/javascript/mastodon/components/account.js
+++ b/app/javascript/mastodon/components/account.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { Fragment } from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import Avatar from './avatar';
@@ -81,19 +81,19 @@ export default class Account extends ImmutablePureComponent {
         buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
       } else if (muting) {
         let hidingNotificationsButton;
-        if (muting.get('notifications')) {
+        if (account.getIn(['relationship', 'muting_notifications'])) {
           hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />;
         } else {
           hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username')  })} onClick={this.handleMuteNotifications} />;
         }
         buttons = (
-          <div>
+          <Fragment>
             <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />
             {hidingNotificationsButton}
-          </div>
+          </Fragment>
         );
-      } else {
-        buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following ? true : false} />;
+      } else if (!account.get('moved')) {
+        buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
       }
     }
 
diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js
index 14a8d4c38..6a16e2fc7 100644
--- a/app/javascript/mastodon/components/autosuggest_textarea.js
+++ b/app/javascript/mastodon/components/autosuggest_textarea.js
@@ -209,6 +209,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
             onBlur={this.onBlur}
             onPaste={this.onPaste}
             style={style}
+            aria-autocomplete='list'
           />
         </label>
 
diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js
index 389296c42..cb849fa5d 100644
--- a/app/javascript/mastodon/features/account/components/action_bar.js
+++ b/app/javascript/mastodon/features/account/components/action_bar.js
@@ -63,9 +63,8 @@ export default class ActionBar extends React.PureComponent {
     if (account.get('id') === me) {
       menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
     } else {
-      const following = account.getIn(['relationship', 'following']);
-      if (following) {
-        if (following.get('reblogs')) {
+      if (account.getIn(['relationship', 'following'])) {
+        if (account.getIn(['relationship', 'showing_reblogs'])) {
           menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
         } else {
           menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
index b41eb19d4..b5e0e9a3f 100644
--- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js
+++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js
@@ -68,7 +68,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
   },
 
   onReblogToggle (account) {
-    if (account.getIn(['relationship', 'following', 'reblogs'])) {
+    if (account.getIn(['relationship', 'showing_reblogs'])) {
       dispatch(followAccount(account.get('id'), false));
     } else {
       dispatch(followAccount(account.get('id'), true));
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index df4284207..4b4c02bcc 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -25,6 +25,7 @@ const messages = defineMessages({
   mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
   info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' },
   pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
+  lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
   keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' },
 });
 
@@ -70,6 +71,7 @@ export default class GettingStarted extends ImmutablePureComponent {
     navItems = navItems.concat([
       <ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
       <ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
+      <ColumnLink key='9' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />,
     ]);
 
     if (myAccount.get('locked')) {
@@ -79,7 +81,7 @@ export default class GettingStarted extends ImmutablePureComponent {
     navItems = navItems.concat([
       <ColumnLink key='7' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
       <ColumnLink key='8' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
-      <ColumnLink key='9' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' hideOnMobile />,
+      <ColumnLink key='10' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' hideOnMobile />,
     ]);
 
     return (
diff --git a/app/javascript/mastodon/features/list_editor/components/account.js b/app/javascript/mastodon/features/list_editor/components/account.js
new file mode 100644
index 000000000..c78c58e24
--- /dev/null
+++ b/app/javascript/mastodon/features/list_editor/components/account.js
@@ -0,0 +1,77 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { makeGetAccount } from '../../../selectors';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import Avatar from '../../../components/avatar';
+import DisplayName from '../../../components/display_name';
+import IconButton from '../../../components/icon_button';
+import { defineMessages, injectIntl } from 'react-intl';
+import { removeFromListEditor, addToListEditor } from '../../../actions/lists';
+
+const messages = defineMessages({
+  remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
+  add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
+});
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = (state, { accountId, added }) => ({
+    account: getAccount(state, accountId),
+    added: typeof added === 'undefined' ? state.getIn(['listEditor', 'accounts', 'items']).includes(accountId) : added,
+  });
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { accountId }) => ({
+  onRemove: () => dispatch(removeFromListEditor(accountId)),
+  onAdd: () => dispatch(addToListEditor(accountId)),
+});
+
+@connect(makeMapStateToProps, mapDispatchToProps)
+@injectIntl
+export default class Account extends ImmutablePureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.map.isRequired,
+    intl: PropTypes.object.isRequired,
+    onRemove: PropTypes.func.isRequired,
+    onAdd: PropTypes.func.isRequired,
+    added: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    added: false,
+  };
+
+  render () {
+    const { account, intl, onRemove, onAdd, added } = this.props;
+
+    let button;
+
+    if (added) {
+      button = <IconButton icon='times' title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
+    } else {
+      button = <IconButton icon='plus' title={intl.formatMessage(messages.add)} onClick={onAdd} />;
+    }
+
+    return (
+      <div className='account'>
+        <div className='account__wrapper'>
+          <div className='account__display-name'>
+            <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
+            <DisplayName account={account} />
+          </div>
+
+          <div className='account__relationship'>
+            {button}
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/list_editor/components/search.js b/app/javascript/mastodon/features/list_editor/components/search.js
new file mode 100644
index 000000000..45c4d0f2e
--- /dev/null
+++ b/app/javascript/mastodon/features/list_editor/components/search.js
@@ -0,0 +1,75 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { defineMessages, injectIntl } from 'react-intl';
+import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
+import classNames from 'classnames';
+
+const messages = defineMessages({
+  search: { id: 'lists.search', defaultMessage: 'Search among people you follow' },
+});
+
+const mapStateToProps = state => ({
+  value: state.getIn(['listEditor', 'suggestions', 'value']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onSubmit: value => dispatch(fetchListSuggestions(value)),
+  onClear: () => dispatch(clearListSuggestions()),
+  onChange: value => dispatch(changeListSuggestions(value)),
+});
+
+@connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
+export default class Search extends React.PureComponent {
+
+  static propTypes = {
+    intl: PropTypes.object.isRequired,
+    value: PropTypes.string.isRequired,
+    onChange: PropTypes.func.isRequired,
+    onSubmit: PropTypes.func.isRequired,
+    onClear: PropTypes.func.isRequired,
+  };
+
+  handleChange = e => {
+    this.props.onChange(e.target.value);
+  }
+
+  handleKeyUp = e => {
+    if (e.keyCode === 13) {
+      this.props.onSubmit(this.props.value);
+    }
+  }
+
+  handleClear = () => {
+    this.props.onClear();
+  }
+
+  render () {
+    const { value, intl } = this.props;
+    const hasValue = value.length > 0;
+
+    return (
+      <div className='list-editor__search search'>
+        <label>
+          <span style={{ display: 'none' }}>{intl.formatMessage(messages.search)}</span>
+
+          <input
+            className='search__input'
+            type='text'
+            value={value}
+            onChange={this.handleChange}
+            onKeyUp={this.handleKeyUp}
+            placeholder={intl.formatMessage(messages.search)}
+          />
+        </label>
+
+        <div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
+          <i className={classNames('fa fa-search', { active: !hasValue })} />
+          <i aria-label={intl.formatMessage(messages.search)} className={classNames('fa fa-times-circle', { active: hasValue })} />
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/list_editor/index.js b/app/javascript/mastodon/features/list_editor/index.js
new file mode 100644
index 000000000..a3b60e447
--- /dev/null
+++ b/app/javascript/mastodon/features/list_editor/index.js
@@ -0,0 +1,80 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { injectIntl } from 'react-intl';
+import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
+import Account from './components/account';
+import Search from './components/search';
+import Motion from '../ui/util/optional_motion';
+import spring from 'react-motion/lib/spring';
+
+const mapStateToProps = state => ({
+  title: state.getIn(['listEditor', 'title']),
+  accountIds: state.getIn(['listEditor', 'accounts', 'items']),
+  searchAccountIds: state.getIn(['listEditor', 'suggestions', 'items']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onInitialize: listId => dispatch(setupListEditor(listId)),
+  onClear: () => dispatch(clearListSuggestions()),
+  onReset: () => dispatch(resetListEditor()),
+});
+
+@connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
+export default class ListEditor extends ImmutablePureComponent {
+
+  static propTypes = {
+    listId: PropTypes.string.isRequired,
+    onClose: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+    onInitialize: PropTypes.func.isRequired,
+    onClear: PropTypes.func.isRequired,
+    onReset: PropTypes.func.isRequired,
+    title: PropTypes.string.isRequired,
+    accountIds: ImmutablePropTypes.list.isRequired,
+    searchAccountIds: ImmutablePropTypes.list.isRequired,
+  };
+
+  componentDidMount () {
+    const { onInitialize, listId } = this.props;
+    onInitialize(listId);
+  }
+
+  componentWillUnmount () {
+    const { onReset } = this.props;
+    onReset();
+  }
+
+  render () {
+    const { title, accountIds, searchAccountIds, onClear } = this.props;
+    const showSearch = searchAccountIds.size > 0;
+
+    return (
+      <div className='modal-root__modal list-editor'>
+        <h4>{title}</h4>
+
+        <Search />
+
+        <div className='drawer__pager'>
+          <div className='drawer__inner list-editor__accounts'>
+            {accountIds.map(accountId => <Account key={accountId} accountId={accountId} added />)}
+          </div>
+
+          {showSearch && <div role='button' tabIndex='-1' className='drawer__backdrop' onClick={onClear} />}
+
+          <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
+            {({ x }) =>
+              <div className='drawer__inner backdrop' style={{ transform: x === 0 ? null : `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
+                {searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
+              </div>
+            }
+          </Motion>
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js
index 71f6e36a8..1dcd4de14 100644
--- a/app/javascript/mastodon/features/list_timeline/index.js
+++ b/app/javascript/mastodon/features/list_timeline/index.js
@@ -6,10 +6,18 @@ import StatusListContainer from '../ui/containers/status_list_container';
 import Column from '../../components/column';
 import ColumnHeader from '../../components/column_header';
 import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
-import { FormattedMessage } from 'react-intl';
+import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
 import { connectListStream } from '../../actions/streaming';
 import { refreshListTimeline, expandListTimeline } from '../../actions/timelines';
-import { fetchList } from '../../actions/lists';
+import { fetchList, deleteList } from '../../actions/lists';
+import { openModal } from '../../actions/modal';
+import MissingIndicator from '../../components/missing_indicator';
+import LoadingIndicator from '../../components/loading_indicator';
+
+const messages = defineMessages({
+  deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
+  deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' },
+});
 
 const mapStateToProps = (state, props) => ({
   list: state.getIn(['lists', props.params.id]),
@@ -17,15 +25,21 @@ const mapStateToProps = (state, props) => ({
 });
 
 @connect(mapStateToProps)
+@injectIntl
 export default class ListTimeline extends React.PureComponent {
 
+  static contextTypes = {
+    router: PropTypes.object,
+  };
+
   static propTypes = {
     params: PropTypes.object.isRequired,
     dispatch: PropTypes.func.isRequired,
     columnId: PropTypes.string,
     hasUnread: PropTypes.bool,
     multiColumn: PropTypes.bool,
-    list: ImmutablePropTypes.map,
+    list: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
+    intl: PropTypes.object.isRequired,
   };
 
   handlePin = () => {
@@ -35,6 +49,7 @@ export default class ListTimeline extends React.PureComponent {
       dispatch(removeColumn(columnId));
     } else {
       dispatch(addColumn('LIST', { id: this.props.params.id }));
+      this.context.router.history.push('/');
     }
   }
 
@@ -73,12 +88,49 @@ export default class ListTimeline extends React.PureComponent {
     this.props.dispatch(expandListTimeline(id));
   }
 
+  handleEditClick = () => {
+    this.props.dispatch(openModal('LIST_EDITOR', { listId: this.props.params.id }));
+  }
+
+  handleDeleteClick = () => {
+    const { dispatch, columnId, intl } = this.props;
+    const { id } = this.props.params;
+
+    dispatch(openModal('CONFIRM', {
+      message: intl.formatMessage(messages.deleteMessage),
+      confirm: intl.formatMessage(messages.deleteConfirm),
+      onConfirm: () => {
+        dispatch(deleteList(id));
+
+        if (!!columnId) {
+          dispatch(removeColumn(columnId));
+        } else {
+          this.context.router.history.push('/lists');
+        }
+      },
+    }));
+  }
+
   render () {
     const { hasUnread, columnId, multiColumn, list } = this.props;
     const { id } = this.props.params;
     const pinned = !!columnId;
     const title  = list ? list.get('title') : id;
 
+    if (typeof list === 'undefined') {
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
+    } else if (list === false) {
+      return (
+        <Column>
+          <MissingIndicator />
+        </Column>
+      );
+    }
+
     return (
       <Column ref={this.setRef}>
         <ColumnHeader
@@ -90,7 +142,19 @@ export default class ListTimeline extends React.PureComponent {
           onClick={this.handleHeaderClick}
           pinned={pinned}
           multiColumn={multiColumn}
-        />
+        >
+          <div className='column-header__links'>
+            <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleEditClick}>
+              <i className='fa fa-pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
+            </button>
+
+            <button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleDeleteClick}>
+              <i className='fa fa-trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
+            </button>
+          </div>
+
+          <hr />
+        </ColumnHeader>
 
         <StatusListContainer
           trackScroll={!pinned}
diff --git a/app/javascript/mastodon/features/lists/components/new_list_form.js b/app/javascript/mastodon/features/lists/components/new_list_form.js
new file mode 100644
index 000000000..eed6efc25
--- /dev/null
+++ b/app/javascript/mastodon/features/lists/components/new_list_form.js
@@ -0,0 +1,78 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
+import IconButton from '../../../components/icon_button';
+import { defineMessages, injectIntl } from 'react-intl';
+
+const messages = defineMessages({
+  label: { id: 'lists.new.title_placeholder', defaultMessage: 'New list title' },
+  title: { id: 'lists.new.create', defaultMessage: 'Add list' },
+});
+
+const mapStateToProps = state => ({
+  value: state.getIn(['listEditor', 'title']),
+  disabled: state.getIn(['listEditor', 'isSubmitting']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onChange: value => dispatch(changeListEditorTitle(value)),
+  onSubmit: () => dispatch(submitListEditor(true)),
+});
+
+@connect(mapStateToProps, mapDispatchToProps)
+@injectIntl
+export default class NewListForm extends React.PureComponent {
+
+  static propTypes = {
+    value: PropTypes.string.isRequired,
+    disabled: PropTypes.bool,
+    intl: PropTypes.object.isRequired,
+    onChange: PropTypes.func.isRequired,
+    onSubmit: PropTypes.func.isRequired,
+  };
+
+  handleChange = e => {
+    this.props.onChange(e.target.value);
+  }
+
+  handleSubmit = e => {
+    e.preventDefault();
+    this.props.onSubmit();
+  }
+
+  handleClick = () => {
+    this.props.onSubmit();
+  }
+
+  render () {
+    const { value, disabled, intl } = this.props;
+
+    const label = intl.formatMessage(messages.label);
+    const title = intl.formatMessage(messages.title);
+
+    return (
+      <form className='column-inline-form' onSubmit={this.handleSubmit}>
+        <label>
+          <span style={{ display: 'none' }}>{label}</span>
+
+          <input
+            className='setting-text'
+            value={value}
+            disabled={disabled}
+            onChange={this.handleChange}
+            placeholder={label}
+          />
+        </label>
+
+        <IconButton
+          disabled={disabled}
+          icon='plus'
+          title={title}
+          onClick={this.handleClick}
+        />
+      </form>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/lists/index.js b/app/javascript/mastodon/features/lists/index.js
new file mode 100644
index 000000000..28026c434
--- /dev/null
+++ b/app/javascript/mastodon/features/lists/index.js
@@ -0,0 +1,76 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import LoadingIndicator from '../../components/loading_indicator';
+import Column from '../ui/components/column';
+import ColumnBackButtonSlim from '../../components/column_back_button_slim';
+import { fetchLists } from '../../actions/lists';
+import { defineMessages, injectIntl } from 'react-intl';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import ColumnLink from '../ui/components/column_link';
+import ColumnSubheading from '../ui/components/column_subheading';
+import NewListForm from './components/new_list_form';
+import { createSelector } from 'reselect';
+
+const messages = defineMessages({
+  heading: { id: 'column.lists', defaultMessage: 'Lists' },
+  subheading: { id: 'lists.subheading', defaultMessage: 'Your lists' },
+});
+
+const getOrderedLists = createSelector([state => state.get('lists')], lists => {
+  if (!lists) {
+    return lists;
+  }
+
+  return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
+});
+
+const mapStateToProps = state => ({
+  lists: getOrderedLists(state),
+});
+
+@connect(mapStateToProps)
+@injectIntl
+export default class Lists extends ImmutablePureComponent {
+
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    dispatch: PropTypes.func.isRequired,
+    lists: ImmutablePropTypes.list,
+    intl: PropTypes.object.isRequired,
+  };
+
+  componentWillMount () {
+    this.props.dispatch(fetchLists());
+  }
+
+  render () {
+    const { intl, lists } = this.props;
+
+    if (!lists) {
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
+    }
+
+    return (
+      <Column icon='bars' heading={intl.formatMessage(messages.heading)}>
+        <ColumnBackButtonSlim />
+
+        <NewListForm />
+
+        <div className='scrollable'>
+          <ColumnSubheading text={intl.formatMessage(messages.subheading)} />
+
+          {lists.map(list =>
+            <ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='bars' text={list.get('title')} />
+          )}
+        </div>
+      </Column>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js
index 680bf63ab..d3e322c36 100644
--- a/app/javascript/mastodon/features/status/components/card.js
+++ b/app/javascript/mastodon/features/status/components/card.js
@@ -101,7 +101,7 @@ export default class Card extends React.PureComponent {
         onClick={this.handlePhotoClick}
         role='button'
         tabIndex='0'
-        src={card.get('url')}
+        src={card.get('embed_url')}
         alt={card.get('title')}
         width={card.get('width')}
         height={card.get('height')}
diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js
index 79d86370e..ebbff6b5a 100644
--- a/app/javascript/mastodon/features/ui/components/modal_root.js
+++ b/app/javascript/mastodon/features/ui/components/modal_root.js
@@ -13,6 +13,7 @@ import {
   MuteModal,
   ReportModal,
   EmbedModal,
+  ListEditor,
 } from '../../../features/ui/util/async-components';
 
 const MODAL_COMPONENTS = {
@@ -25,6 +26,7 @@ const MODAL_COMPONENTS = {
   'REPORT': ReportModal,
   'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
   'EMBED': EmbedModal,
+  'LIST_EDITOR': ListEditor,
 };
 
 export default class ModalRoot extends React.PureComponent {
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 361326961..5b0d7246a 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -38,6 +38,7 @@ import {
   Blocks,
   Mutes,
   PinnedStatuses,
+  Lists,
 } from './util/async-components';
 import { HotKeys } from 'react-hotkeys';
 import { me } from '../../initial_state';
@@ -404,6 +405,7 @@ export default class UI extends React.Component {
               <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
               <WrappedRoute path='/blocks' component={Blocks} content={children} />
               <WrappedRoute path='/mutes' component={Mutes} content={children} />
+              <WrappedRoute path='/lists' component={Lists} content={children} />
 
               <WrappedRoute component={GenericNotFound} content={children} />
             </WrappedSwitch>
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index b741f668e..d6586680b 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -30,6 +30,10 @@ export function ListTimeline () {
   return import(/* webpackChunkName: "features/list_timeline" */'../../list_timeline');
 }
 
+export function Lists () {
+  return import(/* webpackChunkName: "features/lists" */'../../lists');
+}
+
 export function Status () {
   return import(/* webpackChunkName: "features/status" */'../../status');
 }
@@ -113,3 +117,7 @@ export function Video () {
 export function EmbedModal () {
   return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal');
 }
+
+export function ListEditor () {
+  return import(/* webpackChunkName: "features/list_editor" */'../../list_editor');
+}
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 596a519bd..a984b38d2 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -7,22 +7,22 @@
   "account.followers": "المتابعون",
   "account.follows": "يتبع",
   "account.follows_you": "يتابعك",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "إخفاء ترقيات @{name}",
   "account.media": "وسائط",
   "account.mention": "أُذكُر @{name}",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "{name} إنتقل إلى :",
   "account.mute": "أكتم @{name}",
-  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.mute_notifications": "كتم إخطارات @{name}",
   "account.posts": "المشاركات",
   "account.report": "أبلغ عن @{name}",
   "account.requested": "في انتظار الموافقة",
   "account.share": "مشاركة @{name}'s profile",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.show_reblogs": "عرض ترقيات @{name}",
   "account.unblock": "إلغاء الحظر عن @{name}",
   "account.unblock_domain": "فك حظر {domain}",
   "account.unfollow": "إلغاء المتابعة",
   "account.unmute": "إلغاء الكتم عن @{name}",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.unmute_notifications": "إلغاء كتم إخطارات @{name}",
   "account.view_full_profile": "عرض الملف الشخصي كاملا",
   "boost_modal.combo": "يمكنك ضغط {combo} لتخطّي هذه في المرّة القادمة",
   "bundle_column_error.body": "لقد وقع هناك خطأ أثناء عملية تحميل هذا العنصر.",
@@ -36,6 +36,7 @@
   "column.favourites": "المفضلة",
   "column.follow_requests": "طلبات المتابعة",
   "column.home": "الرئيسية",
+  "column.lists": "Lists",
   "column.mutes": "الحسابات المكتومة",
   "column.notifications": "الإشعارات",
   "column.pins": "التبويقات المثبتة",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "هل أنت متأكد أنك تريد حجب {name} ؟",
   "confirmations.delete.confirm": "حذف",
   "confirmations.delete.message": "هل أنت متأكد أنك تريد حذف هذا المنشور ؟",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "إخفاء إسم النطاق كاملا",
   "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
   "confirmations.mute.confirm": "أكتم",
@@ -76,7 +79,7 @@
   "emoji_button.food": "الطعام والشراب",
   "emoji_button.label": "أدرج إيموجي",
   "emoji_button.nature": "الطبيعة",
-  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.not_found": "لا إيموجو !! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "أشياء",
   "emoji_button.people": "الناس",
   "emoji_button.recent": "الشائعة الإستخدام",
@@ -84,13 +87,13 @@
   "emoji_button.search_results": "نتائج البحث",
   "emoji_button.symbols": "رموز",
   "emoji_button.travel": "أماكن و أسفار",
-  "empty_column.community": "الخط الزمني المحلي فارغ. اكتب شيئا ما للعامة كبداية.",
+  "empty_column.community": "الخط الزمني المحلي فارغ. أكتب شيئا ما للعامة كبداية !",
   "empty_column.hashtag": "ليس هناك بعدُ أي محتوى ذو علاقة بهذا الوسم.",
   "empty_column.home": "إنك لا تتبع بعد أي شخص إلى حد الآن. زر {public} أو استخدام حقل البحث لكي تبدأ على التعرف على مستخدمين آخرين.",
   "empty_column.home.public_timeline": "الخيط العام",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "هذه القائمة فارغة.",
   "empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.",
-  "empty_column.public": "لا يوجد شيء هنا ! قم بتحرير شيء ما بشكل عام، أو اتبع مستخدمين آخرين في الخوادم المثيلة الأخرى لملء خيط المحادثات العام.",
+  "empty_column.public": "لا يوجد أي شيء هنا ! قم بنشر شيء ما للعامة، أو إتبع مستخدمين آخرين في الخوادم المثيلة الأخرى لملء خيط المحادثات العام",
   "follow_request.authorize": "ترخيص",
   "follow_request.reject": "رفض",
   "getting_started.appsshort": "تطبيقات",
@@ -104,37 +107,46 @@
   "home.column_settings.show_reblogs": "عرض الترقيات",
   "home.column_settings.show_replies": "عرض الردود",
   "home.settings": "إعدادات العمود",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.back": "للعودة",
+  "keyboard_shortcuts.boost": "للترقية",
   "keyboard_shortcuts.column": "to focus a status in one of the columns",
   "keyboard_shortcuts.compose": "to focus the compose textarea",
   "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.down": "للإنتقال إلى أسفل القائمة",
   "keyboard_shortcuts.enter": "to open status",
   "keyboard_shortcuts.favourite": "to favourite",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
   "keyboard_shortcuts.hotkey": "Hotkey",
   "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.mention": "لذِكر الناشر",
+  "keyboard_shortcuts.reply": "للردّ",
   "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.toot": "لتحرير تبويق جديد",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.up": "للإنتقال إلى أعلى القائمة",
   "lightbox.close": "إغلاق",
   "lightbox.next": "التالي",
   "lightbox.previous": "العودة",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "تحميل ...",
   "media_gallery.toggle_visible": "عرض / إخفاء",
   "missing_indicator.label": "تعذر العثور عليه",
-  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "mute_modal.hide_notifications": "هل تود إخفاء الإخطارات القادمة من هذا المستخدم ؟",
   "navigation_bar.blocks": "الحسابات المحجوبة",
   "navigation_bar.community_timeline": "الخيط العام المحلي",
   "navigation_bar.edit_profile": "تعديل الملف الشخصي",
   "navigation_bar.favourites": "المفضلة",
   "navigation_bar.follow_requests": "طلبات المتابعة",
   "navigation_bar.info": "معلومات إضافية",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.keyboard_shortcuts": "إختصارات لوحة المفاتيح",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "خروج",
   "navigation_bar.mutes": "الحسابات المكتومة",
   "navigation_bar.pins": "التبويقات المثبتة",
@@ -186,7 +198,7 @@
   "privacy.unlisted.short": "غير مدرج",
   "relative_time.days": "{number}d",
   "relative_time.hours": "{number}h",
-  "relative_time.just_now": "now",
+  "relative_time.just_now": "الآن",
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "إلغاء",
@@ -199,7 +211,7 @@
   "search_popout.tips.status": "حالة",
   "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
   "search_popout.tips.user": "مستخدِم",
-  "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
+  "search_results.total": "{count, number} {count, plural, one {result} و {results}}",
   "standalone.public_title": "نظرة على ...",
   "status.cannot_reblog": "تعذرت ترقية هذا المنشور",
   "status.delete": "إحذف",
@@ -208,7 +220,7 @@
   "status.load_more": "حمّل المزيد",
   "status.media_hidden": "الصورة مستترة",
   "status.mention": "أذكُر @{name}",
-  "status.more": "More",
+  "status.more": "المزيد",
   "status.mute_conversation": "كتم المحادثة",
   "status.open": "وسع هذه المشاركة",
   "status.pin": "تدبيس على الملف الشخصي",
@@ -229,7 +241,7 @@
   "tabs_bar.home": "الرئيسية",
   "tabs_bar.local_timeline": "المحلي",
   "tabs_bar.notifications": "الإخطارات",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.",
   "upload_area.title": "إسحب ثم أفلت للرفع",
   "upload_button.label": "إضافة وسائط",
   "upload_form.description": "وصف للمعاقين بصريا",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index 82f092648..d20120b11 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favourites",
   "column.follow_requests": "Follow requests",
   "column.home": "Начало",
+  "column.lists": "Lists",
   "column.mutes": "Muted users",
   "column.notifications": "Известия",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Are you sure you want to block {name}?",
   "confirmations.delete.confirm": "Delete",
   "confirmations.delete.message": "Are you sure you want to delete this status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Hide entire domain",
   "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
   "confirmations.mute.confirm": "Mute",
@@ -124,6 +127,14 @@
   "lightbox.close": "Затвори",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Зареждане...",
   "media_gallery.toggle_visible": "Toggle visibility",
   "missing_indicator.label": "Not found",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Follow requests",
   "navigation_bar.info": "Extended information",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Излизане",
   "navigation_bar.mutes": "Muted users",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index baa9432de..bfa931fc0 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -7,7 +7,7 @@
   "account.followers": "Seguidors",
   "account.follows": "Seguint",
   "account.follows_you": "et segueix",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "Amaga els impulsos de @{name}",
   "account.media": "Media",
   "account.mention": "Esmentar @{name}",
   "account.moved_to": "{name} s'ha mogut a:",
@@ -17,7 +17,7 @@
   "account.report": "Informe @{name}",
   "account.requested": "Esperant aprovació. Clic per a cancel·lar la petició de seguiment",
   "account.share": "Compartir el perfil de @{name}",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.show_reblogs": "Mostra els impulsos de @{name}",
   "account.unblock": "Desbloquejar @{name}",
   "account.unblock_domain": "Mostra {domain}",
   "account.unfollow": "Deixar de seguir",
@@ -36,6 +36,7 @@
   "column.favourites": "Favorits",
   "column.follow_requests": "Peticions per seguir-te",
   "column.home": "Inici",
+  "column.lists": "Lists",
   "column.mutes": "Usuaris silenciats",
   "column.notifications": "Notificacions",
   "column.pins": "Toot fixat",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Estàs segur que vols bloquejar {name}?",
   "confirmations.delete.confirm": "Esborrar",
   "confirmations.delete.message": "Estàs segur que vols esborrar aquest estat?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Amagar tot el domini",
   "confirmations.domain_block.message": "Estàs realment, realment segur que vols bloquejar totalment {domain}? En la majoria dels casos bloquejar o silenciar és suficient i preferible.",
   "confirmations.mute.confirm": "Silenciar",
@@ -72,7 +75,7 @@
   "embed.preview": "Aquí tenim quin aspecte tindrá:",
   "emoji_button.activity": "Activitat",
   "emoji_button.custom": "Personalitzat",
-  "emoji_button.flags": "Flags",
+  "emoji_button.flags": "Marques",
   "emoji_button.food": "Menjar i Beure",
   "emoji_button.label": "Inserir emoji",
   "emoji_button.nature": "Natura",
@@ -88,7 +91,7 @@
   "empty_column.hashtag": "Encara no hi ha res amb aquesta etiqueta.",
   "empty_column.home": "Encara no segueixes ningú. Visita {public} o fes cerca per començar i conèixer altres usuaris.",
   "empty_column.home.public_timeline": "la línia de temps pública",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "Encara no hi ha res en aquesta llista.",
   "empty_column.notifications": "Encara no tens notificacions. Interactua amb altres per iniciar la conversa.",
   "empty_column.public": "No hi ha res aquí! Escriu alguna cosa públicament o segueix manualment usuaris d'altres instàncies per omplir-ho",
   "follow_request.authorize": "Autoritzar",
@@ -104,26 +107,34 @@
   "home.column_settings.show_reblogs": "Mostrar impulsos",
   "home.column_settings.show_replies": "Mostrar respostes",
   "home.settings": "Ajustos de columna",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.back": "navegar enrera",
+  "keyboard_shortcuts.boost": "impulsar",
+  "keyboard_shortcuts.column": "per centrar un estat en una de les columnes",
+  "keyboard_shortcuts.compose": "per centrar l'area de composició de text",
   "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.down": "per baixar en la llista",
   "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.favourite": "afavorir",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
-  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.hotkey": "Tecla d'accés directe",
+  "keyboard_shortcuts.legend": "per a mostrar aquesta llegenda",
+  "keyboard_shortcuts.mention": "per esmentar l'autor",
+  "keyboard_shortcuts.reply": "respondre",
+  "keyboard_shortcuts.search": "per centrar la cerca",
+  "keyboard_shortcuts.toot": "per a començar un toot nou de trinca",
+  "keyboard_shortcuts.unfocus": "descentrar l'area de composició de text/cerca",
+  "keyboard_shortcuts.up": "moure amunt en la llista",
   "lightbox.close": "Tancar",
   "lightbox.next": "Següent",
   "lightbox.previous": "Anterior",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Carregant...",
   "media_gallery.toggle_visible": "Alternar visibilitat",
   "missing_indicator.label": "No trobat",
@@ -134,7 +145,8 @@
   "navigation_bar.favourites": "Favorits",
   "navigation_bar.follow_requests": "Sol·licituds de seguiment",
   "navigation_bar.info": "Informació addicional",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.keyboard_shortcuts": "Dreceres de teclat",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Tancar sessió",
   "navigation_bar.mutes": "Usuaris silenciats",
   "navigation_bar.pins": "Toots fixats",
@@ -195,7 +207,7 @@
   "report.target": "Informes",
   "search.placeholder": "Cercar",
   "search_popout.search_format": "Format de cerca avançada",
-  "search_popout.tips.hashtag": "hashtag",
+  "search_popout.tips.hashtag": "etiqueta",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "El text simple retorna coincidències amb els noms de visualització, els noms d'usuari i els hashtags",
   "search_popout.tips.user": "usuari",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 343528899..b6d9e27a7 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -7,22 +7,22 @@
   "account.followers": "Folgende",
   "account.follows": "Folgt",
   "account.follows_you": "Folgt dir",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "Geteilte Beiträge von @{name} verbergen",
   "account.media": "Medien",
   "account.mention": "@{name} erwähnen",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "{name} ist umgezogen auf:",
   "account.mute": "@{name} stummschalten",
-  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.mute_notifications": "Benachrichtigungen von @{name} verbergen",
   "account.posts": "Beiträge",
   "account.report": "@{name} melden",
   "account.requested": "Warte auf Erlaubnis. Klicke zum Abbrechen",
   "account.share": "Profil von @{name} teilen",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.show_reblogs": "Von @{name} geteilte Beiträge anzeigen",
   "account.unblock": "@{name} entblocken",
   "account.unblock_domain": "{domain} wieder anzeigen",
   "account.unfollow": "Entfolgen",
   "account.unmute": "@{name} nicht mehr stummschalten",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.unmute_notifications": "Benachrichtigungen von @{name} einschalten",
   "account.view_full_profile": "Vollständiges Profil anzeigen",
   "boost_modal.combo": "Du kannst {combo} drücken, um dies beim nächsten Mal zu überspringen",
   "bundle_column_error.body": "Etwas ist beim Laden schiefgelaufen.",
@@ -36,6 +36,7 @@
   "column.favourites": "Favoriten",
   "column.follow_requests": "Folgeanfragen",
   "column.home": "Startseite",
+  "column.lists": "Lists",
   "column.mutes": "Stummgeschaltete Profile",
   "column.notifications": "Mitteilungen",
   "column.pins": "Angeheftete Beiträge",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Bist du dir sicher, dass du {name} blockieren möchtest?",
   "confirmations.delete.confirm": "Löschen",
   "confirmations.delete.message": "Bist du dir sicher, dass du diesen Beitrag löschen möchtest?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Die ganze Domain verbergen",
   "confirmations.domain_block.message": "Bist du dir wirklich sicher, dass du die ganze Domain {domain} verbergen willst? In den meisten Fällen reichen ein paar gezielte Blocks aus.",
   "confirmations.mute.confirm": "Stummschalten",
@@ -80,7 +83,7 @@
   "emoji_button.objects": "Gegenstände",
   "emoji_button.people": "Personen",
   "emoji_button.recent": "Häufig benutzt",
-  "emoji_button.search": "Suchen",
+  "emoji_button.search": "Suchen…",
   "emoji_button.search_results": "Suchergebnisse",
   "emoji_button.symbols": "Symbole",
   "emoji_button.travel": "Reisen und Orte",
@@ -88,7 +91,7 @@
   "empty_column.hashtag": "Unter diesem Hashtag gibt es noch nichts.",
   "empty_column.home": "Deine Startseite ist leer! Besuche {public} oder nutze die Suche, um loszulegen und andere Leute zu finden.",
   "empty_column.home.public_timeline": "die öffentliche Zeitleiste",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "Diese Liste ist derzeit leer.",
   "empty_column.notifications": "Du hast noch keine Mitteilungen. Interagiere mit anderen, um ins Gespräch zu kommen.",
   "empty_column.public": "Hier ist nichts zu sehen! Schreibe etwas öffentlich oder folge Profilen von anderen Instanzen, um die Zeitleiste aufzufüllen",
   "follow_request.authorize": "Erlauben",
@@ -104,37 +107,46 @@
   "home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen",
   "home.column_settings.show_replies": "Antworten anzeigen",
   "home.settings": "Spalteneinstellungen",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
+  "keyboard_shortcuts.back": "zurück navigieren",
+  "keyboard_shortcuts.boost": "boosten",
+  "keyboard_shortcuts.column": "einen Status in einer der Spalten fokussieren",
   "keyboard_shortcuts.compose": "to focus the compose textarea",
   "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.down": "sich in der Liste hinunter bewegen",
   "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.favourite": "favorisieren",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
   "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
+  "keyboard_shortcuts.legend": "diese Übersicht anzeigen",
+  "keyboard_shortcuts.mention": "Autor_in erwähnen",
+  "keyboard_shortcuts.reply": "antworten",
+  "keyboard_shortcuts.search": "die Suche fokussieren",
+  "keyboard_shortcuts.toot": "einen neuen Toot beginnen",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.up": "sich in der Liste hinauf bewegen",
   "lightbox.close": "Schließen",
   "lightbox.next": "Weiter",
   "lightbox.previous": "Zurück",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Wird geladen …",
   "media_gallery.toggle_visible": "Sichtbarkeit umschalten",
   "missing_indicator.label": "Nicht gefunden",
-  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "mute_modal.hide_notifications": "Benachrichtigungen von diesem Account verbergen?",
   "navigation_bar.blocks": "Blockierte Profile",
   "navigation_bar.community_timeline": "Lokale Zeitleiste",
   "navigation_bar.edit_profile": "Profil bearbeiten",
   "navigation_bar.favourites": "Favoriten",
   "navigation_bar.follow_requests": "Folgeanfragen",
   "navigation_bar.info": "Über diese Instanz",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.keyboard_shortcuts": "Tastenkombinationen",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Abmelden",
   "navigation_bar.mutes": "Stummgeschaltete Profile",
   "navigation_bar.pins": "Angeheftete Beiträge",
@@ -169,7 +181,7 @@
   "onboarding.page_six.apps_available": "Es gibt verschiedene {apps} für iOS, Android und weitere Plattformen.",
   "onboarding.page_six.github": "Mastodon ist freie, quelloffene Software. Du kannst auf {github} dazu beitragen, Probleme melden und Wünsche äußern.",
   "onboarding.page_six.guidelines": "Richtlinien",
-  "onboarding.page_six.read_guidelines": "Bitte mach dich mit den {guidelines} von {domain} vertraut.",
+  "onboarding.page_six.read_guidelines": "Bitte mach dich mit den {guidelines} von {domain} vertraut!",
   "onboarding.page_six.various_app": "Apps",
   "onboarding.page_three.profile": "Bearbeite dein Profil, um dein Bild, deinen Namen und deine Beschreibung anzupassen. Dort findest du auch weitere Einstellungen.",
   "onboarding.page_three.search": "Benutze die Suchfunktion, um Leute zu finden und mit Hashtags wie {illustration} oder {introductions} nach Beiträgen zu suchen. Um eine Person zu finden, die auf einer anderen Instanz ist, benutze den vollständigen Profilnamen.",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index be751589e..bb82cf5f5 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -858,6 +858,10 @@
         "id": "navigation_bar.pins"
       },
       {
+        "defaultMessage": "Lists",
+        "id": "navigation_bar.lists"
+      },
+      {
         "defaultMessage": "Keyboard shortcuts",
         "id": "navigation_bar.keyboard_shortcuts"
       },
@@ -1011,6 +1015,44 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "Remove from list",
+        "id": "lists.account.remove"
+      },
+      {
+        "defaultMessage": "Add to list",
+        "id": "lists.account.add"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/list_editor/components/account.json"
+  },
+  {
+    "descriptors": [
+      {
+        "defaultMessage": "Search among people you follow",
+        "id": "lists.search"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/list_editor/components/search.json"
+  },
+  {
+    "descriptors": [
+      {
+        "defaultMessage": "Are you sure you want to permanently delete this list?",
+        "id": "confirmations.delete_list.message"
+      },
+      {
+        "defaultMessage": "Delete",
+        "id": "confirmations.delete_list.confirm"
+      },
+      {
+        "defaultMessage": "Edit list",
+        "id": "lists.edit"
+      },
+      {
+        "defaultMessage": "Delete list",
+        "id": "lists.delete"
+      },
+      {
         "defaultMessage": "There is nothing in this list yet.",
         "id": "empty_column.list"
       }
@@ -1020,6 +1062,32 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "New list title",
+        "id": "lists.new.title_placeholder"
+      },
+      {
+        "defaultMessage": "Add list",
+        "id": "lists.new.create"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/lists/components/new_list_form.json"
+  },
+  {
+    "descriptors": [
+      {
+        "defaultMessage": "Lists",
+        "id": "column.lists"
+      },
+      {
+        "defaultMessage": "Your lists",
+        "id": "lists.subheading"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/lists/index.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "Muted users",
         "id": "column.mutes"
       }
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 4e0b838b0..538124904 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favourites",
   "column.follow_requests": "Follow requests",
   "column.home": "Home",
+  "column.lists": "Lists",
   "column.mutes": "Muted users",
   "column.notifications": "Notifications",
   "column.pins": "Pinned toots",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Are you sure you want to block {name}?",
   "confirmations.delete.confirm": "Delete",
   "confirmations.delete.message": "Are you sure you want to delete this status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Hide entire domain",
   "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
   "confirmations.mute.confirm": "Mute",
@@ -124,6 +127,14 @@
   "lightbox.close": "Close",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Loading...",
   "media_gallery.toggle_visible": "Toggle visibility",
   "missing_indicator.label": "Not found",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Follow requests",
   "navigation_bar.info": "About this instance",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Logout",
   "navigation_bar.mutes": "Muted users",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index f9e32ba5e..619c7320a 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favoritoj",
   "column.follow_requests": "Abonpetoj",
   "column.home": "Hejmo",
+  "column.lists": "Lists",
   "column.mutes": "Silentigitaj uzantoj",
   "column.notifications": "Sciigoj",
   "column.pins": "Alpinglitaj pepoj",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Ĉu vi konfirmas la blokadon de {name}?",
   "confirmations.delete.confirm": "Malaperigi",
   "confirmations.delete.message": "Ĉu vi konfirmas la malaperigon de tiun pepon?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Kaŝi la tutan reton",
   "confirmations.domain_block.message": "Ĉu vi vere, vere certas, ke vi volas bloki {domain} tute? Plej ofte, kelkaj celitaj blokadoj aŭ silentigoj estas sufiĉaj kaj preferindaj.",
   "confirmations.mute.confirm": "Silentigi",
@@ -124,6 +127,14 @@
   "lightbox.close": "Fermi",
   "lightbox.next": "Malantaŭa",
   "lightbox.previous": "Antaŭa",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Ŝarganta…",
   "media_gallery.toggle_visible": "Baskuli videblecon",
   "missing_indicator.label": "Ne trovita",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Abonpetoj",
   "navigation_bar.info": "Plia informo",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Elsaluti",
   "navigation_bar.mutes": "Silentigitaj uzantoj",
   "navigation_bar.pins": "Alpinglitaj pepoj",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 38ba291e4..411615744 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -7,22 +7,22 @@
   "account.followers": "Seguidores",
   "account.follows": "Sigue",
   "account.follows_you": "Te sigue",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "Ocultar retoots de @{name}",
   "account.media": "Media",
   "account.mention": "Mencionar a @{name}",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "{name} se ha mudado a:",
   "account.mute": "Silenciar a @{name}",
-  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.mute_notifications": "Silenciar notificaciones de @{name}",
   "account.posts": "Publicaciones",
   "account.report": "Reportar a @{name}",
   "account.requested": "Esperando aprobación",
   "account.share": "Compartir el perfil de @{name}",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.show_reblogs": "Mostrar retoots de @{name}",
   "account.unblock": "Desbloquear a @{name}",
   "account.unblock_domain": "Mostrar a {domain}",
   "account.unfollow": "Dejar de seguir",
   "account.unmute": "Dejar de silenciar a @{name}",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.unmute_notifications": "Dejar de silenciar las notificaciones de @{name}",
   "account.view_full_profile": "Ver perfil completo",
   "boost_modal.combo": "Puedes presionar {combo} para saltear este aviso la próxima vez",
   "bundle_column_error.body": "Algo salió mal al cargar este componente.",
@@ -36,6 +36,7 @@
   "column.favourites": "Favoritos",
   "column.follow_requests": "Solicitudes de seguimiento",
   "column.home": "Inicio",
+  "column.lists": "Lists",
   "column.mutes": "Usuarios silenciados",
   "column.notifications": "Notificaciones",
   "column.pins": "Toot fijado",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "¿Estás seguro de que quieres bloquear a {name}?",
   "confirmations.delete.confirm": "Eliminar",
   "confirmations.delete.message": "¿Estás seguro de que quieres borrar este toot?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Ocultar dominio entero",
   "confirmations.domain_block.message": "¿Seguro de que quieres bloquear al dominio entero? En algunos casos es preferible bloquear o silenciar objetivos determinados.",
   "confirmations.mute.confirm": "Silenciar",
@@ -71,26 +74,26 @@
   "embed.instructions": "Añade este toot a tu sitio web con el siguiente código.",
   "embed.preview": "Así es como se verá:",
   "emoji_button.activity": "Actividad",
-  "emoji_button.custom": "Custom",
+  "emoji_button.custom": "Personalizado",
   "emoji_button.flags": "Marcas",
   "emoji_button.food": "Comida y bebida",
   "emoji_button.label": "Insertar emoji",
   "emoji_button.nature": "Naturaleza",
-  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.not_found": "No hay emojos!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objetos",
   "emoji_button.people": "Gente",
-  "emoji_button.recent": "Frequently used",
+  "emoji_button.recent": "Usados frecuentemente",
   "emoji_button.search": "Buscar…",
-  "emoji_button.search_results": "Search results",
+  "emoji_button.search_results": "Resultados de búsqueda",
   "emoji_button.symbols": "Símbolos",
   "emoji_button.travel": "Viajes y lugares",
   "empty_column.community": "La línea de tiempo local está vacía. ¡Escribe algo para empezar la fiesta!",
   "empty_column.hashtag": "No hay nada en este hashtag aún.",
   "empty_column.home": "No estás siguiendo a nadie aún. Visita {public} o haz búsquedas para empezar y conocer gente nueva.",
   "empty_column.home.public_timeline": "la línea de tiempo pública",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "No hay nada en esta lista aún.",
   "empty_column.notifications": "No tienes ninguna notificación aún. Interactúa con otros para empezar una conversación.",
-  "empty_column.public": "¡No hay nada aquí! Escribe algo públicamente, o sigue usuarios de otras instancias manualmente para llenarlo.",
+  "empty_column.public": "¡No hay nada aquí! Escribe algo públicamente, o sigue usuarios de otras instancias manualmente para llenarlo",
   "follow_request.authorize": "Autorizar",
   "follow_request.reject": "Rechazar",
   "getting_started.appsshort": "Aplicaciones",
@@ -104,37 +107,46 @@
   "home.column_settings.show_reblogs": "Mostrar retoots",
   "home.column_settings.show_replies": "Mostrar respuestas",
   "home.settings": "Ajustes de columna",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.back": "volver atrás",
+  "keyboard_shortcuts.boost": "retootear",
+  "keyboard_shortcuts.column": "enfocar un estado en una de las columnas",
+  "keyboard_shortcuts.compose": "enfocar el área de texto de redacción",
   "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.down": "mover hacia abajo en la lista",
   "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.favourite": "añadir a favoritos",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
-  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.hotkey": "Tecla caliente",
+  "keyboard_shortcuts.legend": "para mostrar esta leyenda",
+  "keyboard_shortcuts.mention": "para mencionar al autor",
+  "keyboard_shortcuts.reply": "para responder",
+  "keyboard_shortcuts.search": "para poner el foco en la búsqueda",
+  "keyboard_shortcuts.toot": "para comenzar un nuevo toot",
+  "keyboard_shortcuts.unfocus": "para retirar el foco de la caja de redacción/búsqueda",
+  "keyboard_shortcuts.up": "para ir hacia arriba en la lista",
   "lightbox.close": "Cerrar",
   "lightbox.next": "Siguiente",
   "lightbox.previous": "Anterior",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Cargando…",
   "media_gallery.toggle_visible": "Cambiar visibilidad",
   "missing_indicator.label": "No encontrado",
-  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "mute_modal.hide_notifications": "Ocultar notificaciones de este usuario?",
   "navigation_bar.blocks": "Usuarios bloqueados",
   "navigation_bar.community_timeline": "Historia local",
   "navigation_bar.edit_profile": "Editar perfil",
   "navigation_bar.favourites": "Favoritos",
   "navigation_bar.follow_requests": "Solicitudes para seguirte",
   "navigation_bar.info": "Información adicional",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.keyboard_shortcuts": "Atajos de teclado",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Cerrar sesión",
   "navigation_bar.mutes": "Usuarios silenciados",
   "navigation_bar.pins": "Toots fijados",
@@ -150,8 +162,8 @@
   "notifications.column_settings.favourite": "Favoritos:",
   "notifications.column_settings.follow": "Nuevos seguidores:",
   "notifications.column_settings.mention": "Menciones:",
-  "notifications.column_settings.push": "Notificaciones push:",
-  "notifications.column_settings.push_meta": "Este dispositivo:",
+  "notifications.column_settings.push": "Notificaciones push",
+  "notifications.column_settings.push_meta": "Este dispositivo",
   "notifications.column_settings.reblog": "Retoots:",
   "notifications.column_settings.show": "Mostrar en columna",
   "notifications.column_settings.sound": "Reproducir sonido",
@@ -194,11 +206,11 @@
   "report.submit": "Publicar",
   "report.target": "Reportando",
   "search.placeholder": "Buscar",
-  "search_popout.search_format": "Advanced search format",
-  "search_popout.tips.hashtag": "hashtag",
+  "search_popout.search_format": "Formato de búsqueda avanzada",
+  "search_popout.tips.hashtag": "etiqueta",
   "search_popout.tips.status": "status",
-  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
-  "search_popout.tips.user": "user",
+  "search_popout.tips.text": "El texto simple devuelve correspondencias de nombre, usuario y hashtag",
+  "search_popout.tips.user": "usuario",
   "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
   "standalone.public_title": "Un pequeño vistazo...",
   "status.cannot_reblog": "Este toot no puede retootearse",
@@ -229,10 +241,10 @@
   "tabs_bar.home": "Inicio",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificaciones",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "ui.beforeunload": "Tu borrador se perderá si sales de Mastodon.",
   "upload_area.title": "Arrastra y suelta para subir",
   "upload_button.label": "Subir multimedia",
-  "upload_form.description": "Describe for the visually impaired",
+  "upload_form.description": "Describir para los usuarios con dificultad visual",
   "upload_form.undo": "Deshacer",
   "upload_progress.label": "Subiendo…",
   "video.close": "Cerrar video",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index dafb52bb5..aa5c21feb 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -36,6 +36,7 @@
   "column.favourites": "پسندیده‌ها",
   "column.follow_requests": "درخواست‌های پیگیری",
   "column.home": "خانه",
+  "column.lists": "Lists",
   "column.mutes": "کاربران بی‌صداشده",
   "column.notifications": "اعلان‌ها",
   "column.pins": "نوشته‌های ثابت",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "آیا واقعاً می‌خواهید {name} را مسدود کنید؟",
   "confirmations.delete.confirm": "پاک کن",
   "confirmations.delete.message": "آیا واقعاً می‌خواهید این نوشته را پاک کنید؟",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "پنهان‌سازی کل دامین",
   "confirmations.domain_block.message": "آیا جدی جدی می‌خواهید کل دامین {domain} را مسدود کنید؟ بیشتر وقت‌ها مسدودکردن یا بی‌صداکردن چند حساب کاربری خاص کافی است و توصیه می‌شود.",
   "confirmations.mute.confirm": "بی‌صدا کن",
@@ -124,6 +127,14 @@
   "lightbox.close": "بستن",
   "lightbox.next": "بعدی",
   "lightbox.previous": "قبلی",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "بارگیری...",
   "media_gallery.toggle_visible": "تغییر پیدایی",
   "missing_indicator.label": "پیدا نشد",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "درخواست‌های پیگیری",
   "navigation_bar.info": "اطلاعات تکمیلی",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "خروج",
   "navigation_bar.mutes": "کاربران بی‌صداشده",
   "navigation_bar.pins": "نوشته‌های ثابت",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index e3739eb68..db9319e2e 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favourites",
   "column.follow_requests": "Follow requests",
   "column.home": "Koti",
+  "column.lists": "Lists",
   "column.mutes": "Muted users",
   "column.notifications": "Ilmoitukset",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Are you sure you want to block {name}?",
   "confirmations.delete.confirm": "Delete",
   "confirmations.delete.message": "Are you sure you want to delete this status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Hide entire domain",
   "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
   "confirmations.mute.confirm": "Mute",
@@ -124,6 +127,14 @@
   "lightbox.close": "Sulje",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Ladataan...",
   "media_gallery.toggle_visible": "Toggle visibility",
   "missing_indicator.label": "Not found",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Follow requests",
   "navigation_bar.info": "Extended information",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Kirjaudu ulos",
   "navigation_bar.mutes": "Muted users",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index e4cabf52c..9b9469bc2 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -7,22 +7,22 @@
   "account.followers": "Abonné⋅e⋅s",
   "account.follows": "Abonnements",
   "account.follows_you": "Vous suit",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "Masquer les partages de @{name}",
   "account.media": "Média",
   "account.mention": "Mentionner",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "{name} a déménagé vers :",
   "account.mute": "Masquer",
-  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.mute_notifications": "Ignorer les notifications de @{name}",
   "account.posts": "Statuts",
   "account.report": "Signaler",
   "account.requested": "Invitation envoyée",
   "account.share": "Partager le profil de @{name}",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.show_reblogs": "Afficher les partages de @{name}",
   "account.unblock": "Débloquer",
   "account.unblock_domain": "Ne plus masquer {domain}",
   "account.unfollow": "Ne plus suivre",
   "account.unmute": "Ne plus masquer",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.unmute_notifications": "Réactiver les notifications de @{name}",
   "account.view_full_profile": "Afficher le profil complet",
   "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois",
   "bundle_column_error.body": "Une erreur s’est produite lors du chargement de ce composant.",
@@ -36,6 +36,7 @@
   "column.favourites": "Favoris",
   "column.follow_requests": "Demandes de suivi",
   "column.home": "Accueil",
+  "column.lists": "Lists",
   "column.mutes": "Comptes masqués",
   "column.notifications": "Notifications",
   "column.pins": "Pouets épinglés",
@@ -52,7 +53,7 @@
   "compose_form.lock_disclaimer": "Votre compte n’est pas {locked}. Tout le monde peut vous suivre et voir vos pouets privés.",
   "compose_form.lock_disclaimer.lock": "verrouillé",
   "compose_form.placeholder": "Qu’avez-vous en tête ?",
-  "compose_form.publish": "Pouet ",
+  "compose_form.publish": "Pouet",
   "compose_form.publish_loud": "{publish} !",
   "compose_form.sensitive": "Marquer le média comme sensible",
   "compose_form.spoiler": "Masquer le texte derrière un avertissement",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Confirmez-vous le blocage de {name} ?",
   "confirmations.delete.confirm": "Supprimer",
   "confirmations.delete.message": "Confirmez-vous la suppression de ce pouet ?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Masquer le domaine entier",
   "confirmations.domain_block.message": "Êtes-vous vraiment, vraiment sûr⋅e de vouloir bloquer {domain} en entier ? Dans la plupart des cas, quelques blocages ou masquages ciblés sont suffisants et préférables.",
   "confirmations.mute.confirm": "Masquer",
@@ -69,14 +72,14 @@
   "confirmations.unfollow.confirm": "Ne plus suivre",
   "confirmations.unfollow.message": "Voulez-vous arrêter de suivre {name} ?",
   "embed.instructions": "Intégrez ce statut à votre site en copiant le code ci-dessous.",
-  "embed.preview": "Il apparaîtra comme cela : ",
+  "embed.preview": "Il apparaîtra comme cela :",
   "emoji_button.activity": "Activités",
   "emoji_button.custom": "Personnalisés",
   "emoji_button.flags": "Drapeaux",
   "emoji_button.food": "Boire et manger",
   "emoji_button.label": "Insérer un émoji",
   "emoji_button.nature": "Nature",
-  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.not_found": "Pas d'emojis !! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objets",
   "emoji_button.people": "Personnages",
   "emoji_button.recent": "Fréquemment utilisés",
@@ -88,7 +91,7 @@
   "empty_column.hashtag": "Il n’y a encore aucun contenu associé à ce hashtag",
   "empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres utilisateur⋅ice⋅s.",
   "empty_column.home.public_timeline": "le fil public",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "Il n'y a rien dans cette liste pour l'instant.",
   "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateur⋅ice⋅s pour débuter la conversation.",
   "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateur⋅ice⋅s d’autres instances pour remplir le fil public.",
   "follow_request.authorize": "Accepter",
@@ -104,12 +107,12 @@
   "home.column_settings.show_reblogs": "Afficher les partages",
   "home.column_settings.show_replies": "Afficher les réponses",
   "home.settings": "Paramètres de la colonne",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.back": "revenir en arrière",
+  "keyboard_shortcuts.boost": "partager",
+  "keyboard_shortcuts.column": "focaliser un statut dans l'une des colonnes",
+  "keyboard_shortcuts.compose": "pour centrer la zone de redaction",
   "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.down": "descendre dans la liste",
   "keyboard_shortcuts.enter": "to open status",
   "keyboard_shortcuts.favourite": "to favourite",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
@@ -124,10 +127,18 @@
   "lightbox.close": "Fermer",
   "lightbox.next": "Suivant",
   "lightbox.previous": "Précédent",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Chargement…",
   "media_gallery.toggle_visible": "Modifier la visibilité",
   "missing_indicator.label": "Non trouvé",
-  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "mute_modal.hide_notifications": "Masquer les notifications de cet utilisateur ?",
   "navigation_bar.blocks": "Comptes bloqués",
   "navigation_bar.community_timeline": "Fil public local",
   "navigation_bar.edit_profile": "Modifier le profil",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Demandes de suivi",
   "navigation_bar.info": "Plus d’informations",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Déconnexion",
   "navigation_bar.mutes": "Comptes masqués",
   "navigation_bar.pins": "Pouets épinglés",
@@ -147,7 +159,7 @@
   "notifications.clear": "Nettoyer",
   "notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?",
   "notifications.column_settings.alert": "Notifications locales",
-  "notifications.column_settings.favourite": "Favoris :",
+  "notifications.column_settings.favourite": "Favoris :",
   "notifications.column_settings.follow": "Nouveaux⋅elles abonné⋅e⋅s :",
   "notifications.column_settings.mention": "Mentions :",
   "notifications.column_settings.push": "Notifications push",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index fdebdf92a..ec1e30dd5 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -1,52 +1,53 @@
 {
   "account.block": "חסימת @{name}",
   "account.block_domain": "להסתיר הכל מהקהילה {domain}",
-  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.disclaimer_full": "המידע להלן עשוי להיות לא עדכני או לא שלם.",
   "account.edit_profile": "עריכת פרופיל",
   "account.follow": "מעקב",
   "account.followers": "עוקבים",
   "account.follows": "נעקבים",
   "account.follows_you": "במעקב אחריך",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "להסתיר הידהודים מאת @{name}",
   "account.media": "מדיה",
   "account.mention": "אזכור של @{name}",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "החשבון {name} הועבר אל:",
   "account.mute": "להשתיק את @{name}",
-  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.mute_notifications": "להסתיר התראות מאת @{name}",
   "account.posts": "הודעות",
   "account.report": "לדווח על @{name}",
   "account.requested": "בהמתנה לאישור",
-  "account.share": "Share @{name}'s profile",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.share": "לשתף את אודות @{name}",
+  "account.show_reblogs": "להראות הדהודים מאת @{name}",
   "account.unblock": "הסרת חסימה מעל @{name}",
   "account.unblock_domain": "הסר חסימה מקהילת {domain}",
   "account.unfollow": "הפסקת מעקב",
   "account.unmute": "הפסקת השתקת @{name}",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
-  "account.view_full_profile": "View full profile",
+  "account.unmute_notifications": "להפסיק הסתרת הודעות מעם @{name}",
+  "account.view_full_profile": "הראה אודות מלאות",
   "boost_modal.combo": "ניתן להקיש {combo} כדי לדלג בפעם הבאה",
-  "bundle_column_error.body": "Something went wrong while loading this component.",
-  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.body": "משהו השתבש בעת הצגת הרכיב הזה.",
+  "bundle_column_error.retry": "לנסות שוב",
   "bundle_column_error.title": "Network error",
-  "bundle_modal_error.close": "Close",
-  "bundle_modal_error.message": "Something went wrong while loading this component.",
-  "bundle_modal_error.retry": "Try again",
+  "bundle_modal_error.close": "לסגור",
+  "bundle_modal_error.message": "משהו השתבש בעת טעינת הרכיב הזה.",
+  "bundle_modal_error.retry": "לנסות שוב",
   "column.blocks": "חסימות",
   "column.community": "ציר זמן מקומי",
   "column.favourites": "חיבובים",
   "column.follow_requests": "בקשות מעקב",
   "column.home": "בבית",
+  "column.lists": "Lists",
   "column.mutes": "השתקות",
   "column.notifications": "התראות",
   "column.pins": "Pinned toot",
   "column.public": "בפרהסיה",
   "column_back_button.label": "חזרה",
-  "column_header.hide_settings": "Hide settings",
-  "column_header.moveLeft_settings": "Move column to the left",
-  "column_header.moveRight_settings": "Move column to the right",
-  "column_header.pin": "Pin",
-  "column_header.show_settings": "Show settings",
-  "column_header.unpin": "Unpin",
+  "column_header.hide_settings": "הסתרת העדפות",
+  "column_header.moveLeft_settings": "הזחת טור לשמאל",
+  "column_header.moveRight_settings": "הזחת טור לימין",
+  "column_header.pin": "קיבוע",
+  "column_header.show_settings": "הצגת העדפות",
+  "column_header.unpin": "שחרור קיבוע",
   "column_subheading.navigation": "ניווט",
   "column_subheading.settings": "אפשרויות",
   "compose_form.lock_disclaimer": "חשבונך אינו {locked}. כל אחד יוכל לעקוב אחריך כדי לקרוא את הודעותיך המיועדות לעוקבים בלבד.",
@@ -62,35 +63,37 @@
   "confirmations.block.message": "לחסום את {name}?",
   "confirmations.delete.confirm": "למחוק",
   "confirmations.delete.message": "למחוק את ההודעה?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "הסתר קהילה שלמה",
   "confirmations.domain_block.message": "באמת באמת לחסום את כל קהילת {domain}? ברב המקרים השתקות נבחרות של מספר משתמשים מסויימים צריכה להספיק.",
   "confirmations.mute.confirm": "להשתיק",
   "confirmations.mute.message": "להשתיק את {name}?",
-  "confirmations.unfollow.confirm": "Unfollow",
-  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
-  "embed.instructions": "Embed this status on your website by copying the code below.",
-  "embed.preview": "Here is what it will look like:",
+  "confirmations.unfollow.confirm": "להפסיק מעקב",
+  "confirmations.unfollow.message": "להפסיק מעקב אחרי {name}?",
+  "embed.instructions": "ניתן להטמיע את ההודעה באתרך ע\"י העתקת הקוד שלהלן.",
+  "embed.preview": "דוגמא כיצד זה יראה:",
   "emoji_button.activity": "פעילות",
-  "emoji_button.custom": "Custom",
+  "emoji_button.custom": "מיוחדים",
   "emoji_button.flags": "דגלים",
   "emoji_button.food": "אוכל ושתיה",
   "emoji_button.label": "הוספת אמוג'י",
   "emoji_button.nature": "טבע",
-  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.not_found": "רגישון לא נמצא!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "חפצים",
   "emoji_button.people": "אנשים",
-  "emoji_button.recent": "Frequently used",
+  "emoji_button.recent": "בשימוש תדיר",
   "emoji_button.search": "חיפוש...",
-  "emoji_button.search_results": "Search results",
+  "emoji_button.search_results": "תוצאות חיפוש",
   "emoji_button.symbols": "סמלים",
   "emoji_button.travel": "טיולים ואתרים",
   "empty_column.community": "טור הסביבה ריק. יש לפרסם משהו כדי שדברים יתרחילו להתגלגל!",
   "empty_column.hashtag": "אין כלום בהאשתג הזה עדיין.",
   "empty_column.home": "אף אחד לא במעקב עדיין. אפשר לבקר ב{public} או להשתמש בחיפוש כדי להתחיל ולהכיר חצוצרנים אחרים.",
   "empty_column.home.public_timeline": "ציר זמן בין-קהילתי",
-  "empty_column.list": "There is nothing in this list yet.",
-  "empty_column.notifications": "אין התראות עדיין. יאללה, הגיע הזמן להתחיל להתערבב!",
-  "empty_column.public": "אין פה כלום! כדי למלא את הטור הזה אפשר לכתוב משהו, או להתחיל לעקוב אחרי אנשים מקהילות אחרות.",
+  "empty_column.list": "אין עדיין מאום ברשימה.",
+  "empty_column.notifications": "אין התראות עדיין. יאללה, הגיע הזמן להתחיל להתערבב.",
+  "empty_column.public": "אין פה כלום! כדי למלא את הטור הזה אפשר לכתוב משהו, או להתחיל לעקוב אחרי אנשים מקהילות אחרות",
   "follow_request.authorize": "קבלה",
   "follow_request.reject": "דחיה",
   "getting_started.appsshort": "יישומונים לניידים",
@@ -104,40 +107,49 @@
   "home.column_settings.show_reblogs": "הצגת הדהודים",
   "home.column_settings.show_replies": "הצגת תגובות",
   "home.settings": "הגדרות טור",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.back": "ניווט חזרה",
+  "keyboard_shortcuts.boost": "להדהד",
+  "keyboard_shortcuts.column": "להתמקד בהודעה באחד מהטורים",
+  "keyboard_shortcuts.compose": "להתמקד בתיבת חיבור ההודעות",
   "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.down": "לנוע במורד הרשימה",
   "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.favourite": "לחבב",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
-  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.hotkey": "מקש קיצור",
+  "keyboard_shortcuts.legend": "להציג את הפירוש",
+  "keyboard_shortcuts.mention": "לאזכר את המחבר(ת)",
+  "keyboard_shortcuts.reply": "לענות",
+  "keyboard_shortcuts.search": "להתמקד בחלון החיפוש",
+  "keyboard_shortcuts.toot": "להתחיל חיצרוץ חדש",
+  "keyboard_shortcuts.unfocus": "לצאת מתיבת חיבור/חיפוש",
+  "keyboard_shortcuts.up": "לנוע במעלה הרשימה",
   "lightbox.close": "סגירה",
-  "lightbox.next": "Next",
-  "lightbox.previous": "Previous",
+  "lightbox.next": "הלאה",
+  "lightbox.previous": "הקודם",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "טוען...",
   "media_gallery.toggle_visible": "נראה\\בלתי נראה",
   "missing_indicator.label": "לא נמצא",
-  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "mute_modal.hide_notifications": "להסתיר הודעות מחשבון זה?",
   "navigation_bar.blocks": "חסימות",
   "navigation_bar.community_timeline": "ציר זמן מקומי",
   "navigation_bar.edit_profile": "עריכת פרופיל",
   "navigation_bar.favourites": "חיבובים",
   "navigation_bar.follow_requests": "בקשות מעקב",
   "navigation_bar.info": "מידע נוסף",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.keyboard_shortcuts": "קיצורי מקלדת",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "יציאה",
   "navigation_bar.mutes": "השתקות",
-  "navigation_bar.pins": "Pinned toots",
+  "navigation_bar.pins": "חיצרוצים מקובעים",
   "navigation_bar.preferences": "העדפות",
   "navigation_bar.public_timeline": "ציר זמן בין-קהילתי",
   "notification.favourite": "חצרוצך חובב על ידי {name}",
@@ -150,8 +162,8 @@
   "notifications.column_settings.favourite": "מחובבים:",
   "notifications.column_settings.follow": "עוקבים חדשים:",
   "notifications.column_settings.mention": "פניות:",
-  "notifications.column_settings.push": "Push notifications",
-  "notifications.column_settings.push_meta": "This device",
+  "notifications.column_settings.push": "הודעות בדחיפה",
+  "notifications.column_settings.push_meta": "מכשיר זה",
   "notifications.column_settings.reblog": "הדהודים:",
   "notifications.column_settings.show": "הצגה בטור",
   "notifications.column_settings.sound": "שמע מופעל",
@@ -160,7 +172,7 @@
   "onboarding.page_five.public_timelines": "ציר הזמן המקומי מראה הודעות פומביות מכל באי קהילת {domain}. ציר הזמן העולמי מראה הודעות פומביות מאת כי מי שבאי קהילת {domain} עוקבים אחריו. אלו צירי הזמן הפומביים, דרך נהדרת לגלות אנשים חדשים.",
   "onboarding.page_four.home": "ציר זמן הבית מראה הודעות מהנעקבים שלך.",
   "onboarding.page_four.notifications": "טור ההתראות מראה כשמישהו מתייחס להודעות שלך.",
-  "onboarding.page_one.federation": "מסטודון היא רשת של שרתים עצמאיים מצורפים ביחד לכדי רשת חברתית אחת גדולה. אנחנו מכנים את השרתים האלו: קהילות",
+  "onboarding.page_one.federation": "מסטודון היא רשת של שרתים עצמאיים מצורפים ביחד לכדי רשת חברתית אחת גדולה. אנחנו מכנים את השרתים האלו קהילות.",
   "onboarding.page_one.handle": "אתם בקהילה {domain}, ולכן מזהה המשתמש המלא שלכם הוא {handle}",
   "onboarding.page_one.welcome": "ברוכים הבאים למסטודון!",
   "onboarding.page_six.admin": "הקהילה מנוהלת בידי {admin}.",
@@ -186,7 +198,7 @@
   "privacy.unlisted.short": "לא לפיד הכללי",
   "relative_time.days": "{number}d",
   "relative_time.hours": "{number}h",
-  "relative_time.just_now": "now",
+  "relative_time.just_now": "כרגע",
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "ביטול",
@@ -194,24 +206,24 @@
   "report.submit": "שליחה",
   "report.target": "דיווח",
   "search.placeholder": "חיפוש",
-  "search_popout.search_format": "Advanced search format",
-  "search_popout.tips.hashtag": "hashtag",
+  "search_popout.search_format": "מבנה חיפוש מתקדם",
+  "search_popout.tips.hashtag": "האשתג",
   "search_popout.tips.status": "status",
-  "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
-  "search_popout.tips.user": "user",
+  "search_popout.tips.text": "טקסט פשוט מחזיר כינויים, שמות משתמש והאשתגים",
+  "search_popout.tips.user": "משתמש(ת)",
   "search_results.total": "{count, number} {count, plural, one {תוצאה} other {תוצאות}}",
-  "standalone.public_title": "A look inside...",
+  "standalone.public_title": "הצצה פנימה...",
   "status.cannot_reblog": "לא ניתן להדהד הודעה זו",
   "status.delete": "מחיקה",
-  "status.embed": "Embed",
+  "status.embed": "הטמעה",
   "status.favourite": "חיבוב",
   "status.load_more": "עוד",
   "status.media_hidden": "מדיה מוסתרת",
   "status.mention": "פניה אל @{name}",
-  "status.more": "More",
+  "status.more": "עוד",
   "status.mute_conversation": "השתקת שיחה",
   "status.open": "הרחבת הודעה",
-  "status.pin": "Pin on profile",
+  "status.pin": "לקבע באודות",
   "status.reblog": "הדהוד",
   "status.reblogged_by": "הודהד על ידי {name}",
   "status.reply": "תגובה",
@@ -219,29 +231,29 @@
   "status.report": "דיווח על @{name}",
   "status.sensitive_toggle": "לחצו כדי לראות",
   "status.sensitive_warning": "תוכן רגיש",
-  "status.share": "Share",
+  "status.share": "שיתוף",
   "status.show_less": "הראה פחות",
   "status.show_more": "הראה יותר",
   "status.unmute_conversation": "הסרת השתקת שיחה",
-  "status.unpin": "Unpin from profile",
+  "status.unpin": "לשחרר מקיבוע באודות",
   "tabs_bar.compose": "חיבור",
   "tabs_bar.federated_timeline": "ציר זמן בין-קהילתי",
   "tabs_bar.home": "בבית",
   "tabs_bar.local_timeline": "ציר זמן מקומי",
   "tabs_bar.notifications": "התראות",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "ui.beforeunload": "הטיוטא תאבד אם תעזבו את מסטודון.",
   "upload_area.title": "ניתן להעלות על ידי Drag & drop",
   "upload_button.label": "הוספת מדיה",
-  "upload_form.description": "Describe for the visually impaired",
+  "upload_form.description": "תיאור לכבדי ראיה",
   "upload_form.undo": "ביטול",
   "upload_progress.label": "עולה...",
-  "video.close": "Close video",
-  "video.exit_fullscreen": "Exit full screen",
-  "video.expand": "Expand video",
+  "video.close": "סגירת וידאו",
+  "video.exit_fullscreen": "יציאה ממסך מלא",
+  "video.expand": "להרחיב וידאו",
   "video.fullscreen": "Full screen",
-  "video.hide": "Hide video",
-  "video.mute": "Mute sound",
+  "video.hide": "להסתיר וידאו",
+  "video.mute": "השתקת צליל",
   "video.pause": "Pause",
-  "video.play": "Play",
-  "video.unmute": "Unmute sound"
+  "video.play": "ניגון",
+  "video.unmute": "החזרת צליל"
 }
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index 497917b9c..c21482670 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favoriti",
   "column.follow_requests": "Zahtjevi za slijeđenje",
   "column.home": "Dom",
+  "column.lists": "Lists",
   "column.mutes": "Utišani korisnici",
   "column.notifications": "Notifikacije",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Želiš li sigurno blokirati {name}?",
   "confirmations.delete.confirm": "Obriši",
   "confirmations.delete.message": "Želiš li stvarno obrisati ovaj status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Sakrij cijelu domenu",
   "confirmations.domain_block.message": "Jesi li zaista, zaista siguran da želiš potpuno blokirati {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
   "confirmations.mute.confirm": "Utišaj",
@@ -124,6 +127,14 @@
   "lightbox.close": "Zatvori",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Učitavam...",
   "media_gallery.toggle_visible": "Preklopi vidljivost",
   "missing_indicator.label": "Nije nađen",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Zahtjevi za slijeđenje",
   "navigation_bar.info": "Više informacija",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Odjavi se",
   "navigation_bar.mutes": "Utišani korisnici",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 6df09aea9..71dd810b6 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favourites",
   "column.follow_requests": "Follow requests",
   "column.home": "Kezdőlap",
+  "column.lists": "Lists",
   "column.mutes": "Muted users",
   "column.notifications": "Értesítések",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Are you sure you want to block {name}?",
   "confirmations.delete.confirm": "Delete",
   "confirmations.delete.message": "Are you sure you want to delete this status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Hide entire domain",
   "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
   "confirmations.mute.confirm": "Mute",
@@ -124,6 +127,14 @@
   "lightbox.close": "Bezárás",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Betöltés...",
   "media_gallery.toggle_visible": "Toggle visibility",
   "missing_indicator.label": "Not found",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Follow requests",
   "navigation_bar.info": "Extended information",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Kijelentkezés",
   "navigation_bar.mutes": "Muted users",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index 66aff71f0..744423e78 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favorit",
   "column.follow_requests": "Permintaan mengikuti",
   "column.home": "Beranda",
+  "column.lists": "Lists",
   "column.mutes": "Pengguna dibisukan",
   "column.notifications": "Notifikasi",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Apa anda yakin ingin memblokir {name}?",
   "confirmations.delete.confirm": "Hapus",
   "confirmations.delete.message": "Apa anda yakin akan menghapus status ini?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Hide entire domain",
   "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
   "confirmations.mute.confirm": "Bisukan",
@@ -124,6 +127,14 @@
   "lightbox.close": "Tutup",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Tunggu sebentar...",
   "media_gallery.toggle_visible": "Tampil/Sembunyikan",
   "missing_indicator.label": "Tidak ditemukan",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Permintaan mengikuti",
   "navigation_bar.info": "Informasi selengkapnya",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Keluar",
   "navigation_bar.mutes": "Pengguna dibisukan",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/index.js b/app/javascript/mastodon/locales/index.js
index 421cb7fab..7e7297561 100644
--- a/app/javascript/mastodon/locales/index.js
+++ b/app/javascript/mastodon/locales/index.js
@@ -1,9 +1 @@
-let theLocale;
-
-export function setLocale(locale) {
-  theLocale = locale;
-}
-
-export function getLocale() {
-  return theLocale;
-}
+export * from 'locales';
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index 673f895fe..b1523e626 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favorati",
   "column.follow_requests": "Demandi di sequado",
   "column.home": "Hemo",
+  "column.lists": "Lists",
   "column.mutes": "Celita uzeri",
   "column.notifications": "Savigi",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Are you sure you want to block {name}?",
   "confirmations.delete.confirm": "Delete",
   "confirmations.delete.message": "Are you sure you want to delete this status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Hide entire domain",
   "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
   "confirmations.mute.confirm": "Mute",
@@ -124,6 +127,14 @@
   "lightbox.close": "Klozar",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Kargante...",
   "media_gallery.toggle_visible": "Chanjar videbleso",
   "missing_indicator.label": "Ne trovita",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Demandi di sequado",
   "navigation_bar.info": "Detaloza informi",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Ekirar",
   "navigation_bar.mutes": "Celita uzeri",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 3d2d47471..9a2d320fd 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -36,6 +36,7 @@
   "column.favourites": "Apprezzati",
   "column.follow_requests": "Richieste di amicizia",
   "column.home": "Home",
+  "column.lists": "Lists",
   "column.mutes": "Utenti silenziati",
   "column.notifications": "Notifiche",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Are you sure you want to block {name}?",
   "confirmations.delete.confirm": "Delete",
   "confirmations.delete.message": "Are you sure you want to delete this status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Hide entire domain",
   "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
   "confirmations.mute.confirm": "Mute",
@@ -124,6 +127,14 @@
   "lightbox.close": "Chiudi",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Carico...",
   "media_gallery.toggle_visible": "Imposta visibilità",
   "missing_indicator.label": "Non trovato",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Richieste di amicizia",
   "navigation_bar.info": "Informazioni estese",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Logout",
   "navigation_bar.mutes": "Utenti silenziati",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 388dd57e8..e015c41c2 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -7,22 +7,22 @@
   "account.followers": "フォロワー",
   "account.follows": "フォロー",
   "account.follows_you": "フォローされています",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "@{name}さんからのブーストを非表示",
   "account.media": "メディア",
   "account.mention": "返信",
   "account.moved_to": "{name}さんは引っ越しました:",
   "account.mute": "ミュート",
-  "account.mute_notifications": "@{name}からの通知を受け取る",
+  "account.mute_notifications": "@{name}さんからの通知を受け取る",
   "account.posts": "投稿",
   "account.report": "通報",
   "account.requested": "承認待ち",
-  "account.share": "@{name} のプロフィールを共有する",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.share": "@{name}さんのプロフィールを共有する",
+  "account.show_reblogs": "@{name}さんからのブーストを表示",
   "account.unblock": "ブロック解除",
   "account.unblock_domain": "{domain}を表示",
   "account.unfollow": "フォロー解除",
   "account.unmute": "ミュート解除",
-  "account.unmute_notifications": "@{name}からの通知を受け取らない",
+  "account.unmute_notifications": "@{name}さんからの通知を受け取らない",
   "account.view_full_profile": "全ての情報を見る",
   "boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。",
   "bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。",
@@ -36,6 +36,7 @@
   "column.favourites": "お気に入り",
   "column.follow_requests": "フォローリクエスト",
   "column.home": "ホーム",
+  "column.lists": "リスト",
   "column.mutes": "ミュートしたユーザー",
   "column.notifications": "通知",
   "column.pins": "固定されたトゥート",
@@ -59,15 +60,17 @@
   "compose_form.spoiler_placeholder": "ここに警告を書いてください",
   "confirmation_modal.cancel": "キャンセル",
   "confirmations.block.confirm": "ブロック",
-  "confirmations.block.message": "本当に{name}をブロックしますか?",
+  "confirmations.block.message": "本当に{name}さんをブロックしますか?",
   "confirmations.delete.confirm": "削除",
   "confirmations.delete.message": "本当に削除しますか?",
+  "confirmations.delete_list.confirm": "削除",
+  "confirmations.delete_list.message": "本当に削除しますか?",
   "confirmations.domain_block.confirm": "ドメイン全体を非表示",
   "confirmations.domain_block.message": "本当に{domain}全体を非表示にしますか? 多くの場合は個別にブロックやミュートするだけで充分であり、また好ましいです。",
   "confirmations.mute.confirm": "ミュート",
-  "confirmations.mute.message": "本当に{name}をミュートしますか?",
+  "confirmations.mute.message": "本当に{name}さんをミュートしますか?",
   "confirmations.unfollow.confirm": "フォロー解除",
-  "confirmations.unfollow.message": "本当に{name}をフォロー解除しますか?",
+  "confirmations.unfollow.message": "本当に{name}さんをフォロー解除しますか?",
   "embed.instructions": "下記のコードをコピーしてウェブサイトに埋め込みます。",
   "embed.preview": "表示例:",
   "emoji_button.activity": "活動",
@@ -112,7 +115,7 @@
   "keyboard_shortcuts.down": "カラム内一つ下に移動",
   "keyboard_shortcuts.enter": "トゥートの詳細を表示",
   "keyboard_shortcuts.favourite": "お気に入り",
-  "keyboard_shortcuts.heading": "キーボードショートカット一覧",
+  "keyboard_shortcuts.heading": "キーボードショートカット",
   "keyboard_shortcuts.hotkey": "ホットキー",
   "keyboard_shortcuts.legend": "この一覧を表示",
   "keyboard_shortcuts.mention": "メンション",
@@ -124,6 +127,14 @@
   "lightbox.close": "閉じる",
   "lightbox.next": "次",
   "lightbox.previous": "前",
+  "lists.account.add": "リストに追加",
+  "lists.account.remove": "リストから外す",
+  "lists.delete": "リストを削除",
+  "lists.edit": "リストを編集",
+  "lists.new.create": "リストを作成",
+  "lists.new.title_placeholder": "新規リスト名",
+  "lists.search": "フォローしている人の中から検索",
+  "lists.subheading": "あなたのリスト",
   "loading_indicator.label": "読み込み中...",
   "media_gallery.toggle_visible": "表示切り替え",
   "missing_indicator.label": "見つかりません",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "フォローリクエスト",
   "navigation_bar.info": "このインスタンスについて",
   "navigation_bar.keyboard_shortcuts": "キーボードショートカット",
+  "navigation_bar.lists": "リスト",
   "navigation_bar.logout": "ログアウト",
   "navigation_bar.mutes": "ミュートしたユーザー",
   "navigation_bar.pins": "固定されたトゥート",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 03028fc31..3f47baa76 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -36,6 +36,7 @@
   "column.favourites": "즐겨찾기",
   "column.follow_requests": "팔로우 요청",
   "column.home": "홈",
+  "column.lists": "Lists",
   "column.mutes": "뮤트 중인 사용자",
   "column.notifications": "알림",
   "column.pins": "고정된 툿",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "정말로 {name}를 차단하시겠습니까?",
   "confirmations.delete.confirm": "삭제",
   "confirmations.delete.message": "정말로 삭제하시겠습니까?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "도메인 전체를 숨김",
   "confirmations.domain_block.message": "정말로 {domain} 전체를 숨기시겠습니까? 대부분의 경우 개별 차단이나 뮤트로 충분합니다.",
   "confirmations.mute.confirm": "뮤트",
@@ -124,6 +127,14 @@
   "lightbox.close": "닫기",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "불러오는 중...",
   "media_gallery.toggle_visible": "표시 전환",
   "missing_indicator.label": "찾을 수 없습니다",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "팔로우 요청",
   "navigation_bar.info": "이 인스턴스에 대해서",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "로그아웃",
   "navigation_bar.mutes": "뮤트 중인 사용자",
   "navigation_bar.pins": "고정된 툿",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 218709899..26e86308d 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -7,22 +7,22 @@
   "account.followers": "Volgers",
   "account.follows": "Volgt",
   "account.follows_you": "Volgt jou",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "Verberg boosts van @{name}",
   "account.media": "Media",
   "account.mention": "Vermeld @{name}",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "{name} is verhuisd naar:",
   "account.mute": "Negeer @{name}",
-  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.mute_notifications": "Negeer meldingen van @{name}",
   "account.posts": "Toots",
   "account.report": "Rapporteer @{name}",
-  "account.requested": "Wacht op goedkeuring. Klik om volgverzoek te annuleren.",
+  "account.requested": "Wacht op goedkeuring. Klik om het volgverzoek te annuleren",
   "account.share": "Profiel van @{name} delen",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.show_reblogs": "Toon boosts van @{name}",
   "account.unblock": "Deblokkeer @{name}",
   "account.unblock_domain": "{domain} niet meer negeren",
   "account.unfollow": "Ontvolgen",
   "account.unmute": "@{name} niet meer negeren",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.unmute_notifications": "@{name} meldingen niet meer negeren",
   "account.view_full_profile": "Volledig profiel tonen",
   "boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan",
   "bundle_column_error.body": "Tijdens het laden van dit onderdeel is er iets fout gegaan.",
@@ -36,6 +36,7 @@
   "column.favourites": "Favorieten",
   "column.follow_requests": "Volgverzoeken",
   "column.home": "Start",
+  "column.lists": "Lists",
   "column.mutes": "Genegeerde gebruikers",
   "column.notifications": "Meldingen",
   "column.pins": "Vastgezette toots",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Weet je het zeker dat je {name} wilt blokkeren?",
   "confirmations.delete.confirm": "Verwijderen",
   "confirmations.delete.message": "Weet je het zeker dat je deze toot wilt verwijderen?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Negeer alles van deze server",
   "confirmations.domain_block.message": "Weet je het echt, echt zeker dat je alles van {domain} wil negeren? In de meeste gevallen is het blokkeren of negeren van een paar specifieke personen voldoende en gewenst.",
   "confirmations.mute.confirm": "Negeren",
@@ -88,9 +91,9 @@
   "empty_column.hashtag": "Er is nog niks te vinden onder deze hashtag.",
   "empty_column.home": "Jij volgt nog niemand. Bezoek {public} of gebruik het zoekvenster om andere mensen te ontmoeten.",
   "empty_column.home.public_timeline": "de globale tijdlijn",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "Er is nog niks in deze lijst.",
   "empty_column.notifications": "Je hebt nog geen meldingen. Heb interactie met andere mensen om het gesprek aan te gaan.",
-  "empty_column.public": "Er is hier helemaal niks! Toot iets in het openbaar of volg mensen van andere Mastodon-servers om het te vullen.",
+  "empty_column.public": "Er is hier helemaal niks! Toot iets in het openbaar of volg mensen van andere servers om het te vullen",
   "follow_request.authorize": "Goedkeuren",
   "follow_request.reject": "Afkeuren",
   "getting_started.appsshort": "Apps",
@@ -99,42 +102,51 @@
   "getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}.",
   "getting_started.userguide": "Gebruikersgids",
   "home.column_settings.advanced": "Geavanceerd",
-  "home.column_settings.basic": "Basic",
+  "home.column_settings.basic": "Basis",
   "home.column_settings.filter_regex": "Wegfilteren met reguliere expressies",
   "home.column_settings.show_reblogs": "Boosts tonen",
   "home.column_settings.show_replies": "Reacties tonen",
   "home.settings": "Kolom-instellingen",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
+  "keyboard_shortcuts.back": "om terug te navigeren",
+  "keyboard_shortcuts.boost": "om te boosten",
+  "keyboard_shortcuts.column": "om te focussen op een status in één van de kolommen",
+  "keyboard_shortcuts.compose": "om te focussen op het toot tekstvak",
   "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.down": "om naar beneden door de lijst te bewegen",
   "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.favourite": "om het te markeren als favoriet",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
-  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.hotkey": "Sneltoets",
+  "keyboard_shortcuts.legend": "om deze legenda weer te geven",
+  "keyboard_shortcuts.mention": "om de auteur te vermelden",
+  "keyboard_shortcuts.reply": "om te antwoorden",
+  "keyboard_shortcuts.search": "om te focussen op zoeken",
+  "keyboard_shortcuts.toot": "om een nieuwe toot te starten",
+  "keyboard_shortcuts.unfocus": "om te ontfocussen van het toot tekstvak/zoeken",
+  "keyboard_shortcuts.up": "om omhoog te bewegen in de lijst",
   "lightbox.close": "Sluiten",
   "lightbox.next": "Volgende",
   "lightbox.previous": "Vorige",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Laden…",
   "media_gallery.toggle_visible": "Media wel/niet tonen",
   "missing_indicator.label": "Niet gevonden",
-  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "mute_modal.hide_notifications": "Verberg meldingen van deze persoon?",
   "navigation_bar.blocks": "Geblokkeerde gebruikers",
   "navigation_bar.community_timeline": "Lokale tijdlijn",
   "navigation_bar.edit_profile": "Profiel bewerken",
   "navigation_bar.favourites": "Favorieten",
   "navigation_bar.follow_requests": "Volgverzoeken",
   "navigation_bar.info": "Uitgebreide informatie",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.keyboard_shortcuts": "Toetsenbord sneltoetsen",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Afmelden",
   "navigation_bar.mutes": "Genegeerde gebruikers",
   "navigation_bar.pins": "Vastgezette toots",
@@ -186,7 +198,7 @@
   "privacy.unlisted.short": "Minder openbaar",
   "relative_time.days": "{number}d",
   "relative_time.hours": "{number}h",
-  "relative_time.just_now": "now",
+  "relative_time.just_now": "nu",
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Annuleren",
@@ -197,7 +209,7 @@
   "search_popout.search_format": "Geavanceerd zoeken",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "toot",
-  "search_popout.tips.text": "Gebruik gewone tekst om te zoeken op weergavenamen, gebruikersnamen en hashtags.",
+  "search_popout.tips.text": "Gebruik gewone tekst om te zoeken op weergavenamen, gebruikersnamen en hashtags",
   "search_popout.tips.user": "gebruiker",
   "search_results.total": "{count, number} {count, plural, one {resultaat} other {resultaten}}",
   "standalone.public_title": "Een kijkje binnenin...",
@@ -229,7 +241,7 @@
   "tabs_bar.home": "Start",
   "tabs_bar.local_timeline": "Lokaal",
   "tabs_bar.notifications": "Meldingen",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "ui.beforeunload": "Je concept zal verloren gaan als je Mastodon verlaat.",
   "upload_area.title": "Hierin slepen om te uploaden",
   "upload_button.label": "Media toevoegen",
   "upload_form.description": "Omschrijf dit voor mensen met een visuele beperking",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index 234589d34..233b6c946 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -1,7 +1,7 @@
 {
   "account.block": "Blokkér @{name}",
   "account.block_domain": "Skjul alt fra {domain}",
-  "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
+  "account.disclaimer_full": "Informasjonen nedenfor kan gi et ufullstendig bilde av brukerens profil.",
   "account.edit_profile": "Rediger profil",
   "account.follow": "Følg",
   "account.followers": "Følgere",
@@ -10,43 +10,44 @@
   "account.hide_reblogs": "Hide boosts from @{name}",
   "account.media": "Media",
   "account.mention": "Nevn @{name}",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "{name} har flyttet til:",
   "account.mute": "Demp @{name}",
-  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.mute_notifications": "Ignorer varsler fra @{name}",
   "account.posts": "Innlegg",
   "account.report": "Rapportér @{name}",
   "account.requested": "Venter på godkjennelse",
-  "account.share": "Share @{name}'s profile",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.share": "Del @{name}s profil",
+  "account.show_reblogs": "Vis boosts fra @{name}",
   "account.unblock": "Avblokker @{name}",
   "account.unblock_domain": "Vis {domain}",
   "account.unfollow": "Avfølg",
   "account.unmute": "Avdemp @{name}",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
-  "account.view_full_profile": "View full profile",
+  "account.unmute_notifications": "Vis varsler fra @{name}",
+  "account.view_full_profile": "Vis full profil",
   "boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang",
-  "bundle_column_error.body": "Something went wrong while loading this component.",
-  "bundle_column_error.retry": "Try again",
+  "bundle_column_error.body": "Noe gikk galt mens denne komponenten lastet.",
+  "bundle_column_error.retry": "Prøv igjen",
   "bundle_column_error.title": "Network error",
-  "bundle_modal_error.close": "Close",
-  "bundle_modal_error.message": "Something went wrong while loading this component.",
-  "bundle_modal_error.retry": "Try again",
+  "bundle_modal_error.close": "Lukk",
+  "bundle_modal_error.message": "Noe gikk galt da denne komponenten lastet.",
+  "bundle_modal_error.retry": "Prøv igjen",
   "column.blocks": "Blokkerte brukere",
   "column.community": "Lokal tidslinje",
   "column.favourites": "Likt",
   "column.follow_requests": "Følgeforespørsler",
   "column.home": "Hjem",
+  "column.lists": "Lists",
   "column.mutes": "Dempede brukere",
   "column.notifications": "Varsler",
   "column.pins": "Pinned toot",
   "column.public": "Felles tidslinje",
   "column_back_button.label": "Tilbake",
-  "column_header.hide_settings": "Hide settings",
-  "column_header.moveLeft_settings": "Move column to the left",
-  "column_header.moveRight_settings": "Move column to the right",
-  "column_header.pin": "Pin",
-  "column_header.show_settings": "Show settings",
-  "column_header.unpin": "Unpin",
+  "column_header.hide_settings": "Gjem  innstillinger",
+  "column_header.moveLeft_settings": "Flytt feltet til venstre",
+  "column_header.moveRight_settings": "Flytt feltet til høyre",
+  "column_header.pin": "Fest",
+  "column_header.show_settings": "Vis innstillinger",
+  "column_header.unpin": "Løsne",
   "column_subheading.navigation": "Navigasjon",
   "column_subheading.settings": "Innstillinger",
   "compose_form.lock_disclaimer": "Din konto er ikke {locked}. Hvem som helst kan følge deg og se dine private poster.",
@@ -62,33 +63,35 @@
   "confirmations.block.message": "Er du sikker på at du vil blokkere {name}?",
   "confirmations.delete.confirm": "Slett",
   "confirmations.delete.message": "Er du sikker på at du vil slette denne statusen?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Skjul alt fra domenet",
   "confirmations.domain_block.message": "Er du sikker på at du vil skjule hele domenet {domain}? I de fleste tilfeller er det bedre med målrettet blokkering eller demping.",
   "confirmations.mute.confirm": "Demp",
   "confirmations.mute.message": "Er du sikker på at du vil dempe {name}?",
-  "confirmations.unfollow.confirm": "Unfollow",
-  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
-  "embed.instructions": "Embed this status on your website by copying the code below.",
-  "embed.preview": "Here is what it will look like:",
+  "confirmations.unfollow.confirm": "Slutt å følge",
+  "confirmations.unfollow.message": "Er du sikker på at du vil slutte å følge {name}?",
+  "embed.instructions": "Kopier koden under for å bygge inn denne statusen på hjemmesiden din.",
+  "embed.preview": "Slik kommer det til å se ut:",
   "emoji_button.activity": "Aktivitet",
   "emoji_button.custom": "Custom",
   "emoji_button.flags": "Flagg",
   "emoji_button.food": "Mat og drikke",
   "emoji_button.label": "Sett inn emoji",
   "emoji_button.nature": "Natur",
-  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.not_found": "Ingen emojojoer!! (╯°□°)╯︵ ┻━┻",
   "emoji_button.objects": "Objekter",
   "emoji_button.people": "Mennesker",
-  "emoji_button.recent": "Frequently used",
+  "emoji_button.recent": "Hyppig brukt",
   "emoji_button.search": "Søk...",
-  "emoji_button.search_results": "Search results",
+  "emoji_button.search_results": "Søkeresultat",
   "emoji_button.symbols": "Symboler",
   "emoji_button.travel": "Reise & steder",
   "empty_column.community": "Den lokale tidslinjen er tom. Skriv noe offentlig for å få snøballen til å rulle!",
   "empty_column.hashtag": "Det er ingenting i denne hashtagen ennå.",
   "empty_column.home": "Du har ikke fulgt noen ennå. Besøk {publlic} eller bruk søk for å komme i gang og møte andre brukere.",
   "empty_column.home.public_timeline": "en offentlig tidslinje",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "Det er ikke noe i denne listen ennå.",
   "empty_column.notifications": "Du har ingen varsler ennå. Kommuniser med andre for å begynne samtalen.",
   "empty_column.public": "Det er ingenting her! Skriv noe offentlig, eller følg brukere manuelt fra andre instanser for å fylle den opp",
   "follow_request.authorize": "Autorisér",
@@ -104,19 +107,19 @@
   "home.column_settings.show_reblogs": "Vis fremhevinger",
   "home.column_settings.show_replies": "Vis svar",
   "home.settings": "Kolonneinnstillinger",
-  "keyboard_shortcuts.back": "to navigate back",
+  "keyboard_shortcuts.back": "for å navigere tilbake",
   "keyboard_shortcuts.boost": "to boost",
   "keyboard_shortcuts.column": "to focus a status in one of the columns",
   "keyboard_shortcuts.compose": "to focus the compose textarea",
   "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
+  "keyboard_shortcuts.down": "for å flytte ned i listen",
   "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
+  "keyboard_shortcuts.favourite": "for å favorittmarkere",
   "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
+  "keyboard_shortcuts.hotkey": "Lyntast",
   "keyboard_shortcuts.legend": "to display this legend",
   "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
+  "keyboard_shortcuts.reply": "for å svare",
   "keyboard_shortcuts.search": "to focus search",
   "keyboard_shortcuts.toot": "to start a brand new toot",
   "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
@@ -124,6 +127,14 @@
   "lightbox.close": "Lukk",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Laster...",
   "media_gallery.toggle_visible": "Veksle synlighet",
   "missing_indicator.label": "Ikke funnet",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Følgeforespørsler",
   "navigation_bar.info": "Utvidet informasjon",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Logg ut",
   "navigation_bar.mutes": "Dempede brukere",
   "navigation_bar.pins": "Pinned toots",
@@ -237,11 +249,11 @@
   "upload_progress.label": "Laster opp...",
   "video.close": "Close video",
   "video.exit_fullscreen": "Exit full screen",
-  "video.expand": "Expand video",
+  "video.expand": "Utvid video",
   "video.fullscreen": "Full screen",
-  "video.hide": "Hide video",
-  "video.mute": "Mute sound",
+  "video.hide": "Skjul video",
+  "video.mute": "Skru av lyd",
   "video.pause": "Pause",
-  "video.play": "Play",
-  "video.unmute": "Unmute sound"
+  "video.play": "Spill av",
+  "video.unmute": "Skru på lyd"
 }
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index 6e68d6810..ec7202ff6 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -7,17 +7,17 @@
   "account.followers": "Seguidors",
   "account.follows": "Abonaments",
   "account.follows_you": "Vos sèc",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "Rescondre los partages de @{name}",
   "account.media": "Mèdias",
   "account.mention": "Mencionar @{name}",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "{name} a mudat los catons a : ",
   "account.mute": "Rescondre @{name}",
   "account.mute_notifications": "Mute notifications from @{name}",
   "account.posts": "Estatuts",
   "account.report": "Senhalar @{name}",
   "account.requested": "Invitacion mandada. Clicatz per anullar.",
   "account.share": "Partejar lo perfil a @{name}",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.show_reblogs": "Mostrar los partages de @{name}",
   "account.unblock": "Desblocar @{name}",
   "account.unblock_domain": "Desblocar {domain}",
   "account.unfollow": "Quitar de sègre",
@@ -36,6 +36,7 @@
   "column.favourites": "Favorits",
   "column.follow_requests": "Demandas d’abonament",
   "column.home": "Acuèlh",
+  "column.lists": "Lists",
   "column.mutes": "Personas rescondudas",
   "column.notifications": "Notificacions",
   "column.pins": "Tuts penjats",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Sètz segur de voler blocar {name} ?",
   "confirmations.delete.confirm": "Escafar",
   "confirmations.delete.message": "Sètz segur de voler escafar l’estatut ?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Amagar tot lo domeni",
   "confirmations.domain_block.message": "Sètz segur segur de voler blocar completament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
   "confirmations.mute.confirm": "Rescondre",
@@ -88,7 +91,7 @@
   "empty_column.hashtag": "I a pas encara de contengut ligat a aqueste hashtag",
   "empty_column.home": "Vòstre flux d’acuèlh es void. Visitatz {public} o utilizatz la recèrca per vos connectar a d’autras personas.",
   "empty_column.home.public_timeline": "lo flux public",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "I a pas res dins la lista pel moment.",
   "empty_column.notifications": "Avètz pas encara de notificacions. Respondètz a qualqu’un per començar una conversacion.",
   "empty_column.public": "I a pas res aquí ! Escrivètz quicòm de public, o seguètz de personas d’autras instàncias per garnir lo flux public.",
   "follow_request.authorize": "Autorizar",
@@ -104,26 +107,34 @@
   "home.column_settings.show_reblogs": "Mostrar los partatges",
   "home.column_settings.show_replies": "Mostrar las responsas",
   "home.settings": "Paramètres de la colomna",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
-  "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
-  "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
-  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
-  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.back": "per anar enrèire",
+  "keyboard_shortcuts.boost": "per partejar",
+  "keyboard_shortcuts.column": "per centrar un estatut a una colomna",
+  "keyboard_shortcuts.compose": "per anar al camp tèxte",
+  "keyboard_shortcuts.description": "Descripcion",
+  "keyboard_shortcuts.down": "per far davalar dins la lista",
+  "keyboard_shortcuts.enter": "per dobrir los estatuts",
+  "keyboard_shortcuts.favourite": "per apondre als favorits",
+  "keyboard_shortcuts.heading": "Acorchis clavièr",
+  "keyboard_shortcuts.hotkey": "Clau",
+  "keyboard_shortcuts.legend": "per mostrar aquesta legenda",
+  "keyboard_shortcuts.mention": "per mencionar l’autor",
+  "keyboard_shortcuts.reply": "per respondre",
+  "keyboard_shortcuts.search": "per anar a la recèrca",
+  "keyboard_shortcuts.toot": "per començar un estatut tot novèl",
+  "keyboard_shortcuts.unfocus": "per quitar lo camp tèxte/de recèrca",
+  "keyboard_shortcuts.up": "per far montar dins la lista",
   "lightbox.close": "Tampar",
   "lightbox.next": "Seguent",
   "lightbox.previous": "Precedent",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Cargament…",
   "media_gallery.toggle_visible": "Modificar la visibilitat",
   "missing_indicator.label": "Pas trobat",
@@ -134,7 +145,8 @@
   "navigation_bar.favourites": "Favorits",
   "navigation_bar.follow_requests": "Demandas d'abonament",
   "navigation_bar.info": "Mai informacions",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.keyboard_shortcuts": "Acorchis clavièr",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Desconnexion",
   "navigation_bar.mutes": "Personas rescondudas",
   "navigation_bar.pins": "Tuts penjats",
@@ -229,7 +241,7 @@
   "tabs_bar.home": "Acuèlh",
   "tabs_bar.local_timeline": "Flux public local",
   "tabs_bar.notifications": "Notificacions",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "ui.beforeunload": "Vòstre brolhon serà perdut se quitatz Mastodon.",
   "upload_area.title": "Lisatz e depausatz per mandar",
   "upload_button.label": "Ajustar un mèdia",
   "upload_form.description": "Descripcion pels mal vesents",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index 907999940..9295ab937 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -36,6 +36,7 @@
   "column.favourites": "Ulubione",
   "column.follow_requests": "Prośby o śledzenie",
   "column.home": "Strona główna",
+  "column.lists": "Listy",
   "column.mutes": "Wyciszeni użytkownicy",
   "column.notifications": "Powiadomienia",
   "column.pins": "Przypięte wpisy",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Czy na pewno chcesz zablokować {name}?",
   "confirmations.delete.confirm": "Usuń",
   "confirmations.delete.message": "Czy na pewno chcesz usunąć ten wpis?",
+  "confirmations.delete_list.confirm": "Usuń",
+  "confirmations.delete_list.message": "Czy na pewno chcesz bezpowrotnie usunąć tą listę?",
   "confirmations.domain_block.confirm": "Ukryj wszysyko z domeny",
   "confirmations.domain_block.message": "Czy na pewno chcesz zablokować całą domenę {domain}? Zwykle lepszym rozwiązaniem jest blokada lub wyciszenie kilku użytkowników.",
   "confirmations.mute.confirm": "Wycisz",
@@ -124,6 +127,14 @@
   "lightbox.close": "Zamknij",
   "lightbox.next": "Następne",
   "lightbox.previous": "Poprzednie",
+  "lists.account.add": "Dodaj do listy",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Usuń listę",
+  "lists.edit": "Edytuj listę",
+  "lists.new.create": "Utwórz listę",
+  "lists.new.title_placeholder": "Wprowadź tytuł listy…",
+  "lists.search": "Szukaj wśród osób które śledzisz",
+  "lists.subheading": "Twoje listy",
   "loading_indicator.label": "Ładowanie…",
   "media_gallery.toggle_visible": "Przełącz widoczność",
   "missing_indicator.label": "Nie znaleziono",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Prośby o śledzenie",
   "navigation_bar.info": "Szczegółowe informacje",
   "navigation_bar.keyboard_shortcuts": "Skróty klawiszowe",
+  "navigation_bar.lists": "Listy",
   "navigation_bar.logout": "Wyloguj",
   "navigation_bar.mutes": "Wyciszeni użytkownicy",
   "navigation_bar.pins": "Przypięte wpisy",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index ba0d2093e..1df27d536 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -7,22 +7,22 @@
   "account.followers": "Seguidores",
   "account.follows": "Segue",
   "account.follows_you": "Segue você",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "Esconder compartilhamentos de @{name}",
   "account.media": "Mídia",
   "account.mention": "Mencionar @{name}",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "{name} se mudou para:",
   "account.mute": "Silenciar @{name}",
-  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.mute_notifications": "Silenciar notificações de @{name}",
   "account.posts": "Posts",
   "account.report": "Denunciar @{name}",
-  "account.requested": "Aguardando aprovação. Clique para cancelar a solicitação.",
+  "account.requested": "Aguardando aprovação. Clique para cancelar a solicitação",
   "account.share": "Compartilhar perfil de @{name}",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.show_reblogs": "Mostra compartilhamentos de @{name}",
   "account.unblock": "Desbloquear @{name}",
   "account.unblock_domain": "Desbloquear {domain}",
   "account.unfollow": "Deixar de seguir",
   "account.unmute": "Não silenciar @{name}",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
+  "account.unmute_notifications": "Retirar silêncio das notificações vindas de @{name}",
   "account.view_full_profile": "Ver perfil completo",
   "boost_modal.combo": "Você pode pressionar {combo} para ignorar este diálogo na próxima vez",
   "bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.",
@@ -36,6 +36,7 @@
   "column.favourites": "Favoritos",
   "column.follow_requests": "Seguidores pendentes",
   "column.home": "Página inicial",
+  "column.lists": "Lists",
   "column.mutes": "Usuários silenciados",
   "column.notifications": "Notificações",
   "column.pins": "Postagens fixadas",
@@ -62,13 +63,15 @@
   "confirmations.block.message": "Você tem certeza de que quer bloquear {name}?",
   "confirmations.delete.confirm": "Excluir",
   "confirmations.delete.message": "Você tem certeza de que quer excluir esta postagem?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Esconder o domínio inteiro",
   "confirmations.domain_block.message": "Você quer mesmo bloquear {domain} inteiro? Na maioria dos casos, silenciar ou bloquear alguns usuários é o suficiente e o recomendado.",
   "confirmations.mute.confirm": "Silenciar",
   "confirmations.mute.message": "Você tem certeza de que quer silenciar {name}?",
   "confirmations.unfollow.confirm": "Deixar de seguir",
   "confirmations.unfollow.message": "Você tem certeza de que quer deixar de seguir {name}?",
-  "embed.instructions": "Incorpore esta postagem em seu site copiando o código abaixo:",
+  "embed.instructions": "Incorpore esta postagem em seu site copiando o código abaixo.",
   "embed.preview": "Aqui está uma previsão de como ficará:",
   "emoji_button.activity": "Atividades",
   "emoji_button.custom": "Customizados",
@@ -85,12 +88,12 @@
   "emoji_button.symbols": "Símbolos",
   "emoji_button.travel": "Viagens & Lugares",
   "empty_column.community": "A timeline local está vazia. Escreva algo publicamente para começar!",
-  "empty_column.hashtag": "Ainda não há qualquer conteúdo com essa hashtag",
+  "empty_column.hashtag": "Ainda não há qualquer conteúdo com essa hashtag.",
   "empty_column.home": "Você ainda não segue usuário algo. Visite a timeline {public} ou use o buscador para procurar e conhecer outros usuários.",
   "empty_column.home.public_timeline": "global",
-  "empty_column.list": "There is nothing in this list yet.",
-  "empty_column.notifications": "Você ainda não possui notificações. Interaja com outros usuários para começar a conversar!",
-  "empty_column.public": "Não há nada aqui! Escreva algo publicamente ou siga manualmente usuários de outras instâncias.",
+  "empty_column.list": "Ainda não há nada nesta lista.",
+  "empty_column.notifications": "Você ainda não possui notificações. Interaja com outros usuários para começar a conversar.",
+  "empty_column.public": "Não há nada aqui! Escreva algo publicamente ou siga manualmente usuários de outras instâncias",
   "follow_request.authorize": "Autorizar",
   "follow_request.reject": "Rejeitar",
   "getting_started.appsshort": "Apps",
@@ -104,8 +107,8 @@
   "home.column_settings.show_reblogs": "Mostrar compartilhamentos",
   "home.column_settings.show_replies": "Mostrar as respostas",
   "home.settings": "Configurações de colunas",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
+  "keyboard_shortcuts.back": "para navegar de volta",
+  "keyboard_shortcuts.boost": "para compartilhar",
   "keyboard_shortcuts.column": "to focus a status in one of the columns",
   "keyboard_shortcuts.compose": "to focus the compose textarea",
   "keyboard_shortcuts.description": "Description",
@@ -124,6 +127,14 @@
   "lightbox.close": "Fechar",
   "lightbox.next": "Próximo",
   "lightbox.previous": "Anterior",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Carregando...",
   "media_gallery.toggle_visible": "Esconder/Mostrar",
   "missing_indicator.label": "Não encontrado",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Seguidores pendentes",
   "navigation_bar.info": "Mais informações",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Sair",
   "navigation_bar.mutes": "Usuários silenciados",
   "navigation_bar.pins": "Postagens fixadas",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index f349bd191..3d3e19571 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -1,143 +1,155 @@
 {
   "account.block": "Bloquear @{name}",
-  "account.block_domain": "Hide everything from {domain}",
+  "account.block_domain": "Esconder tudo do domínio {domain}",
   "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
   "account.edit_profile": "Editar perfil",
   "account.follow": "Seguir",
   "account.followers": "Seguidores",
   "account.follows": "Segue",
   "account.follows_you": "É teu seguidor",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "Esconder partilhas de @{name}",
   "account.media": "Media",
   "account.mention": "Mencionar @{name}",
-  "account.moved_to": "{name} has moved to:",
+  "account.moved_to": "{name} mudou a sua conta para:",
   "account.mute": "Silenciar @{name}",
-  "account.mute_notifications": "Mute notifications from @{name}",
+  "account.mute_notifications": "Silenciar notificações de @{name}",
   "account.posts": "Posts",
   "account.report": "Denunciar @{name}",
   "account.requested": "A aguardar aprovação",
-  "account.share": "Share @{name}'s profile",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.share": "Partilhar o perfil @{name}",
+  "account.show_reblogs": "Mostrar partilhas de @{name}",
   "account.unblock": "Não bloquear @{name}",
   "account.unblock_domain": "Unhide {domain}",
   "account.unfollow": "Deixar de seguir",
   "account.unmute": "Não silenciar @{name}",
-  "account.unmute_notifications": "Unmute notifications from @{name}",
-  "account.view_full_profile": "View full profile",
+  "account.unmute_notifications": "Deixar de silenciar @{name}",
+  "account.view_full_profile": "Ver perfil completo",
   "boost_modal.combo": "Pode clicar {combo} para não voltar a ver",
-  "bundle_column_error.body": "Something went wrong while loading this component.",
-  "bundle_column_error.retry": "Try again",
-  "bundle_column_error.title": "Network error",
-  "bundle_modal_error.close": "Close",
-  "bundle_modal_error.message": "Something went wrong while loading this component.",
-  "bundle_modal_error.retry": "Try again",
+  "bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.",
+  "bundle_column_error.retry": "Tente de novo",
+  "bundle_column_error.title": "Erro de rede",
+  "bundle_modal_error.close": "Fechar",
+  "bundle_modal_error.message": "Algo de errado aconteceu enquanto este componente era carregado.",
+  "bundle_modal_error.retry": "Tente de novo",
   "column.blocks": "Utilizadores Bloqueados",
   "column.community": "Local",
   "column.favourites": "Favoritos",
   "column.follow_requests": "Seguidores Pendentes",
   "column.home": "Home",
+  "column.lists": "Lists",
   "column.mutes": "Utilizadores silenciados",
   "column.notifications": "Notificações",
   "column.pins": "Pinned toot",
   "column.public": "Global",
   "column_back_button.label": "Voltar",
-  "column_header.hide_settings": "Hide settings",
-  "column_header.moveLeft_settings": "Move column to the left",
-  "column_header.moveRight_settings": "Move column to the right",
-  "column_header.pin": "Pin",
-  "column_header.show_settings": "Show settings",
-  "column_header.unpin": "Unpin",
-  "column_subheading.navigation": "Navigation",
-  "column_subheading.settings": "Settings",
-  "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
-  "compose_form.lock_disclaimer.lock": "locked",
+  "column_header.hide_settings": "Esconder preferências",
+  "column_header.moveLeft_settings": "Mover coluna para a esquerda",
+  "column_header.moveRight_settings": "Mover coluna para a direita",
+  "column_header.pin": "Fixar",
+  "column_header.show_settings": "Mostrar preferências",
+  "column_header.unpin": "Remover fixar",
+  "column_subheading.navigation": "Navegação",
+  "column_subheading.settings": "Preferências",
+  "compose_form.lock_disclaimer": "A tua conta não está {locked}. Qualquer pessoa pode seguir-te e ver as publicações direcionadas apenas a seguidores.",
+  "compose_form.lock_disclaimer.lock": "bloqueada",
   "compose_form.placeholder": "Em que estás a pensar?",
   "compose_form.publish": "Publicar",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive": "Marcar media como conteúdo sensível",
   "compose_form.spoiler": "Esconder texto com aviso",
   "compose_form.spoiler_placeholder": "Aviso de conteúdo",
-  "confirmation_modal.cancel": "Cancel",
+  "confirmation_modal.cancel": "Cancelar",
   "confirmations.block.confirm": "Block",
-  "confirmations.block.message": "Are you sure you want to block {name}?",
-  "confirmations.delete.confirm": "Delete",
-  "confirmations.delete.message": "Are you sure you want to delete this status?",
-  "confirmations.domain_block.confirm": "Hide entire domain",
-  "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
-  "confirmations.mute.confirm": "Mute",
-  "confirmations.mute.message": "Are you sure you want to mute {name}?",
-  "confirmations.unfollow.confirm": "Unfollow",
-  "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
-  "embed.instructions": "Embed this status on your website by copying the code below.",
-  "embed.preview": "Here is what it will look like:",
-  "emoji_button.activity": "Activity",
-  "emoji_button.custom": "Custom",
-  "emoji_button.flags": "Flags",
-  "emoji_button.food": "Food & Drink",
+  "confirmations.block.message": "De certeza que queres bloquear {name}?",
+  "confirmations.delete.confirm": "Eliminar",
+  "confirmations.delete.message": "De certeza que queres eliminar esta publicação?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
+  "confirmations.domain_block.confirm": "Esconder tudo deste domínio",
+  "confirmations.domain_block.message": "De certeza que queres bloquear por completo o domínio {domain}? Na maioria dos casos, silenciar ou bloquear alguns utilizadores é o suficiente e o recomendado.",
+  "confirmations.mute.confirm": "Silenciar",
+  "confirmations.mute.message": "De certeza que queres silenciar {name}?",
+  "confirmations.unfollow.confirm": "Deixar de seguir",
+  "confirmations.unfollow.message": "De certeza que queres deixar de seguir {name}?",
+  "embed.instructions": "Publicar este post num outro site copiando o código abaixo.",
+  "embed.preview": "Podes ver aqui como irá ficar:",
+  "emoji_button.activity": "Actividade",
+  "emoji_button.custom": "Especiais",
+  "emoji_button.flags": "Bandeiras",
+  "emoji_button.food": "Comida & Bebida",
   "emoji_button.label": "Inserir Emoji",
-  "emoji_button.nature": "Nature",
-  "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
-  "emoji_button.objects": "Objects",
-  "emoji_button.people": "People",
-  "emoji_button.recent": "Frequently used",
-  "emoji_button.search": "Search...",
-  "emoji_button.search_results": "Search results",
-  "emoji_button.symbols": "Symbols",
-  "emoji_button.travel": "Travel & Places",
-  "empty_column.community": "Ainda não existem conteúdo local para mostrar!",
-  "empty_column.hashtag": "Ainda não existe qualquer conteúdo com essa hashtag",
+  "emoji_button.nature": "Natureza",
+  "emoji_button.not_found": "Não tem emojos!! (╯°□°)╯︵ ┻━┻",
+  "emoji_button.objects": "Objectos",
+  "emoji_button.people": "Pessoas",
+  "emoji_button.recent": "Regularmente utilizados",
+  "emoji_button.search": "Procurar...",
+  "emoji_button.search_results": "Resultados da pesquisa",
+  "emoji_button.symbols": "Símbolos",
+  "emoji_button.travel": "Viagens & Lugares",
+  "empty_column.community": "Ainda não existe conteúdo local para mostrar!",
+  "empty_column.hashtag": "Não foram encontradas publicações com essa hashtag",
   "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.",
   "empty_column.home.public_timeline": "global",
-  "empty_column.list": "There is nothing in this list yet.",
+  "empty_column.list": "Ainda não existem publicações nesta lista.",
   "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.",
   "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.",
   "follow_request.authorize": "Autorizar",
   "follow_request.reject": "Rejeitar",
-  "getting_started.appsshort": "Apps",
+  "getting_started.appsshort": "Aplicações",
   "getting_started.faq": "FAQ",
   "getting_started.heading": "Primeiros passos",
   "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}.",
-  "getting_started.userguide": "User Guide",
+  "getting_started.userguide": "Guia do utilizador",
   "home.column_settings.advanced": "Avançado",
   "home.column_settings.basic": "Básico",
   "home.column_settings.filter_regex": "Filtrar com uma expressão regular",
   "home.column_settings.show_reblogs": "Mostrar as partilhas",
   "home.column_settings.show_replies": "Mostrar as respostas",
   "home.settings": "Parâmetros da listagem",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
-  "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
-  "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
-  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
+  "keyboard_shortcuts.back": "para navegar de volta",
+  "keyboard_shortcuts.boost": "para partilhar",
+  "keyboard_shortcuts.column": "para focar uma publicação numa das colunas",
+  "keyboard_shortcuts.compose": "para focar na área de publicação",
+  "keyboard_shortcuts.description": "Descrição",
+  "keyboard_shortcuts.down": "para mover para baixo na lista",
+  "keyboard_shortcuts.enter": "para expandir uma publicação",
+  "keyboard_shortcuts.favourite": "para adicionar aos favoritos",
+  "keyboard_shortcuts.heading": "Atalhos do teclado",
   "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
-  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.legend": "para mostrar esta legenda",
+  "keyboard_shortcuts.mention": "para mencionar o autor",
+  "keyboard_shortcuts.reply": "para responder",
+  "keyboard_shortcuts.search": "para focar na pesquisa",
+  "keyboard_shortcuts.toot": "para compor um novo post",
+  "keyboard_shortcuts.unfocus": "para remover o foco da área de publicação/pesquisa",
+  "keyboard_shortcuts.up": "para mover para cima na lista",
   "lightbox.close": "Fechar",
-  "lightbox.next": "Next",
-  "lightbox.previous": "Previous",
-  "loading_indicator.label": "Carregando...",
+  "lightbox.next": "Próximo",
+  "lightbox.previous": "Anterior",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
+  "loading_indicator.label": "A carregar...",
   "media_gallery.toggle_visible": "Esconder/Mostrar",
   "missing_indicator.label": "Não encontrado",
-  "mute_modal.hide_notifications": "Hide notifications from this user?",
+  "mute_modal.hide_notifications": "Esconder notificações deste utilizador?",
   "navigation_bar.blocks": "Utilizadores bloqueados",
   "navigation_bar.community_timeline": "Local",
   "navigation_bar.edit_profile": "Editar perfil",
   "navigation_bar.favourites": "Favoritos",
   "navigation_bar.follow_requests": "Seguidores pendentes",
   "navigation_bar.info": "Mais informações",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.keyboard_shortcuts": "Atalhos de teclado",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Sair",
   "navigation_bar.mutes": "Utilizadores silenciados",
-  "navigation_bar.pins": "Pinned toots",
+  "navigation_bar.pins": "Posts fixos",
   "navigation_bar.preferences": "Preferências",
   "navigation_bar.public_timeline": "Global",
   "notification.favourite": "{name} adicionou o teu post aos favoritos",
@@ -150,31 +162,31 @@
   "notifications.column_settings.favourite": "Favoritos:",
   "notifications.column_settings.follow": "Novos seguidores:",
   "notifications.column_settings.mention": "Menções:",
-  "notifications.column_settings.push": "Push notifications",
-  "notifications.column_settings.push_meta": "This device",
+  "notifications.column_settings.push": "Notificações Push",
+  "notifications.column_settings.push_meta": "Este dispositivo",
   "notifications.column_settings.reblog": "Partilhas:",
   "notifications.column_settings.show": "Mostrar nas colunas",
   "notifications.column_settings.sound": "Reproduzir som",
-  "onboarding.done": "Done",
-  "onboarding.next": "Next",
-  "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
-  "onboarding.page_four.home": "The home timeline shows posts from people you follow.",
-  "onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.",
-  "onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
-  "onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}",
-  "onboarding.page_one.welcome": "Welcome to Mastodon!",
-  "onboarding.page_six.admin": "Your instance's admin is {admin}.",
-  "onboarding.page_six.almost_done": "Almost done...",
+  "onboarding.done": "Pronto",
+  "onboarding.next": "Próximo",
+  "onboarding.page_five.public_timelines": "A timeline local mostra as publicações de todos os utilizadores em {domain}. A timeline global mostra as publicações de todas as pessoas que pessoas em {domain} seguem. Estas são as timelines públicas, uma óptima forma de conhecer novas pessoas.",
+  "onboarding.page_four.home": "A timeline home mostra as publicações de pessoas que tu segues.",
+  "onboarding.page_four.notifications": "A coluna de notificações mostra-te quando alguém interage contigo.",
+  "onboarding.page_one.federation": "Mastodon é uma rede de servidores independentes ligados entre si para fazer uma grande rede social. Nós chamamos instâncias a estes servidores.",
+  "onboarding.page_one.handle": "Tu estás em {domain}, por isso o teu endereço completo de utilizador é {handle}",
+  "onboarding.page_one.welcome": "Bem-vindo(a) ao Mastodon!",
+  "onboarding.page_six.admin": "O administrador da tua instância é {admin}.",
+  "onboarding.page_six.almost_done": "Quase pronto...",
   "onboarding.page_six.appetoot": "Bon Appetoot!",
-  "onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.",
-  "onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
-  "onboarding.page_six.guidelines": "community guidelines",
-  "onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!",
-  "onboarding.page_six.various_app": "mobile apps",
-  "onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.",
-  "onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.",
-  "onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.",
-  "onboarding.skip": "Skip",
+  "onboarding.page_six.apps_available": "Existem {apps} disponíveis para iOS, Android e outras plataformas.",
+  "onboarding.page_six.github": "Mastodon é um software gratuito e de código aberto. Podes reportar bugs, solicitar novas funcionalidades e contribuir para o código em {github}.",
+  "onboarding.page_six.guidelines": "termos de utilização da comunidade",
+  "onboarding.page_six.read_guidelines": "Por favor, lê os {guidelines} de {domain}!",
+  "onboarding.page_six.various_app": "aplicações de telemóvel",
+  "onboarding.page_three.profile": "Edita o teu perfil para mudar a tua imagem, biografia e nome. Lá encontrarás também outras preferências que podes personalizar.",
+  "onboarding.page_three.search": "Utiliza a caixa de pesquisa para procurar pessoas ou hashtags, exemplo {illustration} / {introductions}. Para procurar uma pessoa que não está nesta instância, utiliza o endereço completo.",
+  "onboarding.page_two.compose": "Escreve posts na coluna de publicações. Podes publicar imagens, alterar a privacidade e adicionar alertas de conteúdo usando os ícones abaixo da caixa de composição.",
+  "onboarding.skip": "Saltar",
   "privacy.change": "Ajustar a privacidade da mensagem",
   "privacy.direct.long": "Apenas para utilizadores mencionados",
   "privacy.direct.short": "Directo",
@@ -186,7 +198,7 @@
   "privacy.unlisted.short": "Não listar",
   "relative_time.days": "{number}d",
   "relative_time.hours": "{number}h",
-  "relative_time.just_now": "now",
+  "relative_time.just_now": "agora",
   "relative_time.minutes": "{number}m",
   "relative_time.seconds": "{number}s",
   "reply_indicator.cancel": "Cancelar",
@@ -194,54 +206,54 @@
   "report.submit": "Enviar",
   "report.target": "Denunciar",
   "search.placeholder": "Pesquisar",
-  "search_popout.search_format": "Advanced search format",
+  "search_popout.search_format": "Formato avançado de pesquisa",
   "search_popout.tips.hashtag": "hashtag",
   "search_popout.tips.status": "status",
   "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
-  "search_popout.tips.user": "user",
+  "search_popout.tips.user": "utilizador",
   "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
-  "standalone.public_title": "A look inside...",
-  "status.cannot_reblog": "This post cannot be boosted",
+  "standalone.public_title": "Espreitar lá dentro...",
+  "status.cannot_reblog": "Este post não pode ser partilhado",
   "status.delete": "Eliminar",
   "status.embed": "Embed",
   "status.favourite": "Adicionar aos favoritos",
   "status.load_more": "Carregar mais",
   "status.media_hidden": "Media escondida",
   "status.mention": "Mencionar @{name}",
-  "status.more": "More",
-  "status.mute_conversation": "Mute conversation",
+  "status.more": "Mais",
+  "status.mute_conversation": "Silenciar conversa",
   "status.open": "Expandir",
   "status.pin": "Pin on profile",
   "status.reblog": "Partilhar",
   "status.reblogged_by": "{name} partilhou",
   "status.reply": "Responder",
-  "status.replyAll": "Reply to thread",
-  "status.report": "Denúnciar @{name}",
+  "status.replyAll": "Responder à conversa",
+  "status.report": "Denunciar @{name}",
   "status.sensitive_toggle": "Clique para ver",
   "status.sensitive_warning": "Conteúdo sensível",
   "status.share": "Share",
   "status.show_less": "Mostrar menos",
   "status.show_more": "Mostrar mais",
-  "status.unmute_conversation": "Unmute conversation",
-  "status.unpin": "Unpin from profile",
+  "status.unmute_conversation": "Deixar de silenciar esta conversa",
+  "status.unpin": "Não fixar no perfil",
   "tabs_bar.compose": "Criar",
   "tabs_bar.federated_timeline": "Global",
   "tabs_bar.home": "Home",
   "tabs_bar.local_timeline": "Local",
   "tabs_bar.notifications": "Notificações",
-  "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
+  "ui.beforeunload": "O teu rascunho vai ser perdido se abandonares o Mastodon.",
   "upload_area.title": "Arraste e solte para enviar",
   "upload_button.label": "Adicionar media",
-  "upload_form.description": "Describe for the visually impaired",
+  "upload_form.description": "Descrição da imagem para pessoas com dificuldades visuais",
   "upload_form.undo": "Anular",
   "upload_progress.label": "A gravar...",
-  "video.close": "Close video",
-  "video.exit_fullscreen": "Exit full screen",
-  "video.expand": "Expand video",
+  "video.close": "Fechar vídeo",
+  "video.exit_fullscreen": "Sair de full screen",
+  "video.expand": "Expandir vídeo",
   "video.fullscreen": "Full screen",
-  "video.hide": "Hide video",
-  "video.mute": "Mute sound",
-  "video.pause": "Pause",
-  "video.play": "Play",
-  "video.unmute": "Unmute sound"
+  "video.hide": "Esconder vídeo",
+  "video.mute": "Silenciar",
+  "video.pause": "Pausar",
+  "video.play": "Reproduzir",
+  "video.unmute": "Remover de silêncio"
 }
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index df52eca14..0aef2d9df 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -36,6 +36,7 @@
   "column.favourites": "Понравившееся",
   "column.follow_requests": "Запросы на подписку",
   "column.home": "Главная",
+  "column.lists": "Lists",
   "column.mutes": "Список глушения",
   "column.notifications": "Уведомления",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Вы уверены, что хотите заблокировать {name}?",
   "confirmations.delete.confirm": "Удалить",
   "confirmations.delete.message": "Вы уверены, что хотите удалить этот статус?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Блокировать весь домен",
   "confirmations.domain_block.message": "Вы на самом деле уверены, что хотите блокировать весь {domain}? В большинстве случаев нескольких отдельных блокировок или глушений достаточно.",
   "confirmations.mute.confirm": "Заглушить",
@@ -124,6 +127,14 @@
   "lightbox.close": "Закрыть",
   "lightbox.next": "Далее",
   "lightbox.previous": "Назад",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Загрузка...",
   "media_gallery.toggle_visible": "Показать/скрыть",
   "missing_indicator.label": "Не найдено",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Запросы на подписку",
   "navigation_bar.info": "Об узле",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Выйти",
   "navigation_bar.mutes": "Список глушения",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index 5f887ef34..53090452f 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favoriter",
   "column.follow_requests": "Följ förfrågningar",
   "column.home": "Hem",
+  "column.lists": "Lists",
   "column.mutes": "Tystade användare",
   "column.notifications": "Meddelanden",
   "column.pins": "Nålade toots",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Är du säker att du vill blockera {name}?",
   "confirmations.delete.confirm": "Ta bort",
   "confirmations.delete.message": "Är du säker att du vill ta bort denna status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Blockera hela domänen",
   "confirmations.domain_block.message": "Är du verkligen, verkligen säker på att du vill blockera hela {domain}? I de flesta fall är några riktade blockeringar eller nedtystade tillräckligt och föredras.",
   "confirmations.mute.confirm": "Tysta",
@@ -124,6 +127,14 @@
   "lightbox.close": "Stäng",
   "lightbox.next": "Nästa",
   "lightbox.previous": "Tidigare",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Laddar...",
   "media_gallery.toggle_visible": "Växla synlighet",
   "missing_indicator.label": "Hittades inte",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Följförfrågningar",
   "navigation_bar.info": "Om denna instans",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Logga ut",
   "navigation_bar.mutes": "Tystade användare",
   "navigation_bar.pins": "Nålade inlägg (toots)",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 7b2b13202..2f064a193 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favourites",
   "column.follow_requests": "Follow requests",
   "column.home": "Home",
+  "column.lists": "Lists",
   "column.mutes": "Muted users",
   "column.notifications": "Notifications",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Are you sure you want to block {name}?",
   "confirmations.delete.confirm": "Delete",
   "confirmations.delete.message": "Are you sure you want to delete this status?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Hide entire domain",
   "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
   "confirmations.mute.confirm": "Mute",
@@ -124,6 +127,14 @@
   "lightbox.close": "Close",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Loading...",
   "media_gallery.toggle_visible": "Toggle visibility",
   "missing_indicator.label": "Not found",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Follow requests",
   "navigation_bar.info": "About this instance",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Logout",
   "navigation_bar.mutes": "Muted users",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index dd5a96bdb..be8103d1c 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -36,6 +36,7 @@
   "column.favourites": "Favoriler",
   "column.follow_requests": "Takip istekleri",
   "column.home": "Anasayfa",
+  "column.lists": "Lists",
   "column.mutes": "Susturulmuş kullanıcılar",
   "column.notifications": "Bildirimler",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "{name} kullanıcısını engellemek istiyor musunuz?",
   "confirmations.delete.confirm": "Sil",
   "confirmations.delete.message": "Bu gönderiyi silmek istiyor musunuz?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Hide entire domain",
   "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
   "confirmations.mute.confirm": "Sessize al",
@@ -124,6 +127,14 @@
   "lightbox.close": "Kapat",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Yükleniyor...",
   "media_gallery.toggle_visible": "Görünürlüğü değiştir",
   "missing_indicator.label": "Bulunamadı",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Takip istekleri",
   "navigation_bar.info": "Genişletilmiş bilgi",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Çıkış",
   "navigation_bar.mutes": "Sessize alınmış kullanıcılar",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 6206cd65b..273661462 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -36,6 +36,7 @@
   "column.favourites": "Вподобане",
   "column.follow_requests": "Запити на підписку",
   "column.home": "Головна",
+  "column.lists": "Lists",
   "column.mutes": "Заглушені користувачі",
   "column.notifications": "Сповіщення",
   "column.pins": "Pinned toot",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "Ви впевнені, що хочете заблокувати {name}?",
   "confirmations.delete.confirm": "Видалити",
   "confirmations.delete.message": "Ви впевнені, що хочете видалити цей допис?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "Сховати весь домен",
   "confirmations.domain_block.message": "Ви точно, точно впевнені, що хочете заблокувати весь домен {domain}? У більшості випадків для нормальної роботи краще заблокувати/заглушити лише деяких користувачів.",
   "confirmations.mute.confirm": "Заглушити",
@@ -124,6 +127,14 @@
   "lightbox.close": "Закрити",
   "lightbox.next": "Next",
   "lightbox.previous": "Previous",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "Завантаження...",
   "media_gallery.toggle_visible": "Показати/приховати",
   "missing_indicator.label": "Не знайдено",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "Запити на підписку",
   "navigation_bar.info": "Про інстанцію",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "Вийти",
   "navigation_bar.mutes": "Заглушені користувачі",
   "navigation_bar.pins": "Pinned toots",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index c23868cda..cb5607cc5 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -7,7 +7,7 @@
   "account.followers": "关注者",
   "account.follows": "正在关注",
   "account.follows_you": "关注了你",
-  "account.hide_reblogs": "Hide boosts from @{name}",
+  "account.hide_reblogs": "隐藏来自 @{name} 的转嘟",
   "account.media": "媒体",
   "account.mention": "提及 @{name}",
   "account.moved_to": "{name} 已经迁移到:",
@@ -17,7 +17,7 @@
   "account.report": "举报 @{name}",
   "account.requested": "正在等待对方同意。点击以取消发送关注请求",
   "account.share": "分享 @{name} 的个人资料",
-  "account.show_reblogs": "Show boosts from @{name}",
+  "account.show_reblogs": "显示来自 @{name} 的转嘟",
   "account.unblock": "不再屏蔽 @{name}",
   "account.unblock_domain": "不再隐藏来自 {domain} 的内容",
   "account.unfollow": "取消关注",
@@ -36,6 +36,7 @@
   "column.favourites": "收藏过的嘟文",
   "column.follow_requests": "关注请求",
   "column.home": "主页",
+  "column.lists": "列表",
   "column.mutes": "被隐藏的用户",
   "column.notifications": "通知",
   "column.pins": "置顶嘟文",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "想好了,真的要屏蔽 {name}?",
   "confirmations.delete.confirm": "删除",
   "confirmations.delete.message": "想好了,真的要删除这条嘟文?",
+  "confirmations.delete_list.confirm": "删除",
+  "confirmations.delete_list.message": "你确定要永久删除这个列表吗?",
   "confirmations.domain_block.confirm": "隐藏整个网站的内容",
   "confirmations.domain_block.message": "你真的真的确定要隐藏所有来自 {domain} 的内容吗?多数情况下,屏蔽或隐藏几个特定的用户就应该能满足你的需要了。",
   "confirmations.mute.confirm": "隐藏",
@@ -104,26 +107,34 @@
   "home.column_settings.show_reblogs": "显示转嘟",
   "home.column_settings.show_replies": "显示回复",
   "home.settings": "栏目设置",
-  "keyboard_shortcuts.back": "to navigate back",
-  "keyboard_shortcuts.boost": "to boost",
-  "keyboard_shortcuts.column": "to focus a status in one of the columns",
-  "keyboard_shortcuts.compose": "to focus the compose textarea",
-  "keyboard_shortcuts.description": "Description",
-  "keyboard_shortcuts.down": "to move down in the list",
-  "keyboard_shortcuts.enter": "to open status",
-  "keyboard_shortcuts.favourite": "to favourite",
-  "keyboard_shortcuts.heading": "Keyboard Shortcuts",
-  "keyboard_shortcuts.hotkey": "Hotkey",
-  "keyboard_shortcuts.legend": "to display this legend",
-  "keyboard_shortcuts.mention": "to mention author",
-  "keyboard_shortcuts.reply": "to reply",
-  "keyboard_shortcuts.search": "to focus search",
-  "keyboard_shortcuts.toot": "to start a brand new toot",
-  "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
-  "keyboard_shortcuts.up": "to move up in the list",
+  "keyboard_shortcuts.back": "返回上一页",
+  "keyboard_shortcuts.boost": "转嘟",
+  "keyboard_shortcuts.column": "选择第 X 栏中的嘟文",
+  "keyboard_shortcuts.compose": "选择嘟文撰写框",
+  "keyboard_shortcuts.description": "说明",
+  "keyboard_shortcuts.down": "在列表中让光标下移",
+  "keyboard_shortcuts.enter": "展开嘟文",
+  "keyboard_shortcuts.favourite": "收藏嘟文",
+  "keyboard_shortcuts.heading": "快捷键列表",
+  "keyboard_shortcuts.hotkey": "快捷键",
+  "keyboard_shortcuts.legend": "显示此列表",
+  "keyboard_shortcuts.mention": "提及嘟文作者",
+  "keyboard_shortcuts.reply": "回复嘟文",
+  "keyboard_shortcuts.search": "选择搜索框",
+  "keyboard_shortcuts.toot": "发送新嘟文",
+  "keyboard_shortcuts.unfocus": "取消输入",
+  "keyboard_shortcuts.up": "在列表中让光标上移",
   "lightbox.close": "关闭",
   "lightbox.next": "下一步",
   "lightbox.previous": "上一步",
+  "lists.account.add": "添加到列表",
+  "lists.account.remove": "从列表中删除",
+  "lists.delete": "删除列表",
+  "lists.edit": "编辑列表",
+  "lists.new.create": "新建列表",
+  "lists.new.title_placeholder": "新列表的标题",
+  "lists.search": "搜索你关注的人",
+  "lists.subheading": "你的列表",
   "loading_indicator.label": "加载中……",
   "media_gallery.toggle_visible": "切换显示/隐藏",
   "missing_indicator.label": "找不到内容",
@@ -134,7 +145,8 @@
   "navigation_bar.favourites": "收藏的内容",
   "navigation_bar.follow_requests": "关注请求",
   "navigation_bar.info": "关于本站",
-  "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.keyboard_shortcuts": "快捷键列表",
+  "navigation_bar.lists": "列表",
   "navigation_bar.logout": "注销",
   "navigation_bar.mutes": "被隐藏的用户",
   "navigation_bar.pins": "置顶嘟文",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index a1c7aa875..dbb9584c6 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -36,6 +36,7 @@
   "column.favourites": "最愛的文章",
   "column.follow_requests": "關注請求",
   "column.home": "主頁",
+  "column.lists": "Lists",
   "column.mutes": "靜音名單",
   "column.notifications": "通知",
   "column.pins": "置頂文章",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "你確定要封鎖{name}嗎?",
   "confirmations.delete.confirm": "刪除",
   "confirmations.delete.message": "你確定要刪除{name}嗎?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "隱藏整個網站",
   "confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或靜音幾個特定目標就好。",
   "confirmations.mute.confirm": "靜音",
@@ -124,6 +127,14 @@
   "lightbox.close": "關閉",
   "lightbox.next": "繼續",
   "lightbox.previous": "回退",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "載入中...",
   "media_gallery.toggle_visible": "打開或關上",
   "missing_indicator.label": "找不到內容",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "關注請求",
   "navigation_bar.info": "關於本服務站",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "登出",
   "navigation_bar.mutes": "被你靜音的用戶",
   "navigation_bar.pins": "置頂文章",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index b52c5a810..0b05a83cd 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -36,6 +36,7 @@
   "column.favourites": "最愛",
   "column.follow_requests": "關注請求",
   "column.home": "家",
+  "column.lists": "Lists",
   "column.mutes": "消音的使用者",
   "column.notifications": "通知",
   "column.pins": "置頂貼文",
@@ -62,6 +63,8 @@
   "confirmations.block.message": "你確定要封鎖 {name} ?",
   "confirmations.delete.confirm": "刪除",
   "confirmations.delete.message": "你確定要刪除這個狀態?",
+  "confirmations.delete_list.confirm": "Delete",
+  "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
   "confirmations.domain_block.confirm": "隱藏整個網域",
   "confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。",
   "confirmations.mute.confirm": "消音",
@@ -124,6 +127,14 @@
   "lightbox.close": "關閉",
   "lightbox.next": "繼續",
   "lightbox.previous": "回退",
+  "lists.account.add": "Add to list",
+  "lists.account.remove": "Remove from list",
+  "lists.delete": "Delete list",
+  "lists.edit": "Edit list",
+  "lists.new.create": "Add list",
+  "lists.new.title_placeholder": "New list title",
+  "lists.search": "Search among follows",
+  "lists.subheading": "Your lists",
   "loading_indicator.label": "讀取中...",
   "media_gallery.toggle_visible": "切換可見性",
   "missing_indicator.label": "找不到",
@@ -135,6 +146,7 @@
   "navigation_bar.follow_requests": "關注請求",
   "navigation_bar.info": "關於本站",
   "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
+  "navigation_bar.lists": "Lists",
   "navigation_bar.logout": "登出",
   "navigation_bar.mutes": "消音的使用者",
   "navigation_bar.pins": "置頂貼文",
diff --git a/app/javascript/mastodon/reducers/accounts.js b/app/javascript/mastodon/reducers/accounts.js
index 5891d0fdd..f77061dfa 100644
--- a/app/javascript/mastodon/reducers/accounts.js
+++ b/app/javascript/mastodon/reducers/accounts.js
@@ -43,6 +43,10 @@ import {
   FAVOURITED_STATUSES_FETCH_SUCCESS,
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
 } from '../actions/favourites';
+import {
+  LIST_ACCOUNTS_FETCH_SUCCESS,
+  LIST_EDITOR_SUGGESTIONS_READY,
+} from '../actions/lists';
 import { STORE_HYDRATE } from '../actions/store';
 import emojify from '../features/emoji/emoji';
 import { Map as ImmutableMap, fromJS } from 'immutable';
@@ -115,6 +119,8 @@ export default function accounts(state = initialState, action) {
   case BLOCKS_EXPAND_SUCCESS:
   case MUTES_FETCH_SUCCESS:
   case MUTES_EXPAND_SUCCESS:
+  case LIST_ACCOUNTS_FETCH_SUCCESS:
+  case LIST_EDITOR_SUGGESTIONS_READY:
     return action.accounts ? normalizeAccounts(state, action.accounts) : state;
   case NOTIFICATIONS_REFRESH_SUCCESS:
   case NOTIFICATIONS_EXPAND_SUCCESS:
diff --git a/app/javascript/mastodon/reducers/accounts_counters.js b/app/javascript/mastodon/reducers/accounts_counters.js
index 2a78a9f34..a93fa4245 100644
--- a/app/javascript/mastodon/reducers/accounts_counters.js
+++ b/app/javascript/mastodon/reducers/accounts_counters.js
@@ -45,6 +45,10 @@ import {
   FAVOURITED_STATUSES_FETCH_SUCCESS,
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
 } from '../actions/favourites';
+import {
+  LIST_ACCOUNTS_FETCH_SUCCESS,
+  LIST_EDITOR_SUGGESTIONS_READY,
+} from '../actions/lists';
 import { STORE_HYDRATE } from '../actions/store';
 import { Map as ImmutableMap, fromJS } from 'immutable';
 
@@ -106,6 +110,8 @@ export default function accountsCounters(state = initialState, action) {
   case BLOCKS_EXPAND_SUCCESS:
   case MUTES_FETCH_SUCCESS:
   case MUTES_EXPAND_SUCCESS:
+  case LIST_ACCOUNTS_FETCH_SUCCESS:
+  case LIST_EDITOR_SUGGESTIONS_READY:
     return action.accounts ? normalizeAccounts(state, action.accounts) : state;
   case NOTIFICATIONS_REFRESH_SUCCESS:
   case NOTIFICATIONS_EXPAND_SUCCESS:
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index 425a2acdd..a028e989c 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -23,6 +23,7 @@ import notifications from './notifications';
 import height_cache from './height_cache';
 import custom_emojis from './custom_emojis';
 import lists from './lists';
+import listEditor from './list_editor';
 
 const reducers = {
   timelines,
@@ -49,6 +50,7 @@ const reducers = {
   height_cache,
   custom_emojis,
   lists,
+  listEditor,
 };
 
 export default combineReducers(reducers);
diff --git a/app/javascript/mastodon/reducers/list_editor.js b/app/javascript/mastodon/reducers/list_editor.js
new file mode 100644
index 000000000..02a0dabb1
--- /dev/null
+++ b/app/javascript/mastodon/reducers/list_editor.js
@@ -0,0 +1,89 @@
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+import {
+  LIST_CREATE_REQUEST,
+  LIST_CREATE_FAIL,
+  LIST_CREATE_SUCCESS,
+  LIST_UPDATE_REQUEST,
+  LIST_UPDATE_FAIL,
+  LIST_UPDATE_SUCCESS,
+  LIST_EDITOR_RESET,
+  LIST_EDITOR_SETUP,
+  LIST_EDITOR_TITLE_CHANGE,
+  LIST_ACCOUNTS_FETCH_REQUEST,
+  LIST_ACCOUNTS_FETCH_SUCCESS,
+  LIST_ACCOUNTS_FETCH_FAIL,
+  LIST_EDITOR_SUGGESTIONS_READY,
+  LIST_EDITOR_SUGGESTIONS_CLEAR,
+  LIST_EDITOR_SUGGESTIONS_CHANGE,
+  LIST_EDITOR_ADD_SUCCESS,
+  LIST_EDITOR_REMOVE_SUCCESS,
+} from '../actions/lists';
+
+const initialState = ImmutableMap({
+  listId: null,
+  isSubmitting: false,
+  title: '',
+
+  accounts: ImmutableMap({
+    items: ImmutableList(),
+    loaded: false,
+    isLoading: false,
+  }),
+
+  suggestions: ImmutableMap({
+    value: '',
+    items: ImmutableList(),
+  }),
+});
+
+export default function listEditorReducer(state = initialState, action) {
+  switch(action.type) {
+  case LIST_EDITOR_RESET:
+    return initialState;
+  case LIST_EDITOR_SETUP:
+    return state.withMutations(map => {
+      map.set('listId', action.list.get('id'));
+      map.set('title', action.list.get('title'));
+      map.set('isSubmitting', false);
+    });
+  case LIST_EDITOR_TITLE_CHANGE:
+    return state.set('title', action.value);
+  case LIST_CREATE_REQUEST:
+  case LIST_UPDATE_REQUEST:
+    return state.set('isSubmitting', true);
+  case LIST_CREATE_FAIL:
+  case LIST_UPDATE_FAIL:
+    return state.set('isSubmitting', false);
+  case LIST_CREATE_SUCCESS:
+  case LIST_UPDATE_SUCCESS:
+    return state.withMutations(map => {
+      map.set('isSubmitting', false);
+      map.set('listId', action.list.id);
+    });
+  case LIST_ACCOUNTS_FETCH_REQUEST:
+    return state.setIn(['accounts', 'isLoading'], true);
+  case LIST_ACCOUNTS_FETCH_FAIL:
+    return state.setIn(['accounts', 'isLoading'], false);
+  case LIST_ACCOUNTS_FETCH_SUCCESS:
+    return state.update('accounts', accounts => accounts.withMutations(map => {
+      map.set('isLoading', false);
+      map.set('loaded', true);
+      map.set('items', ImmutableList(action.accounts.map(item => item.id)));
+    }));
+  case LIST_EDITOR_SUGGESTIONS_CHANGE:
+    return state.setIn(['suggestions', 'value'], action.value);
+  case LIST_EDITOR_SUGGESTIONS_READY:
+    return state.setIn(['suggestions', 'items'], ImmutableList(action.accounts.map(item => item.id)));
+  case LIST_EDITOR_SUGGESTIONS_CLEAR:
+    return state.update('suggestions', suggestions => suggestions.withMutations(map => {
+      map.set('items', ImmutableList());
+      map.set('value', '');
+    }));
+  case LIST_EDITOR_ADD_SUCCESS:
+    return state.updateIn(['accounts', 'items'], list => list.unshift(action.accountId));
+  case LIST_EDITOR_REMOVE_SUCCESS:
+    return state.updateIn(['accounts', 'items'], list => list.filterNot(item => item === action.accountId));
+  default:
+    return state;
+  }
+};
diff --git a/app/javascript/mastodon/reducers/lists.js b/app/javascript/mastodon/reducers/lists.js
index 3e3908869..f30ffbcbd 100644
--- a/app/javascript/mastodon/reducers/lists.js
+++ b/app/javascript/mastodon/reducers/lists.js
@@ -1,14 +1,36 @@
-import { LIST_FETCH_SUCCESS } from '../actions/lists';
+import {
+  LIST_FETCH_SUCCESS,
+  LIST_FETCH_FAIL,
+  LISTS_FETCH_SUCCESS,
+  LIST_CREATE_SUCCESS,
+  LIST_UPDATE_SUCCESS,
+  LIST_DELETE_SUCCESS,
+} from '../actions/lists';
 import { Map as ImmutableMap, fromJS } from 'immutable';
 
 const initialState = ImmutableMap();
 
 const normalizeList = (state, list) => state.set(list.id, fromJS(list));
 
+const normalizeLists = (state, lists) => {
+  lists.forEach(list => {
+    state = normalizeList(state, list);
+  });
+
+  return state;
+};
+
 export default function lists(state = initialState, action) {
   switch(action.type) {
   case LIST_FETCH_SUCCESS:
+  case LIST_CREATE_SUCCESS:
+  case LIST_UPDATE_SUCCESS:
     return normalizeList(state, action.list);
+  case LISTS_FETCH_SUCCESS:
+    return normalizeLists(state, action.lists);
+  case LIST_DELETE_SUCCESS:
+  case LIST_FETCH_FAIL:
+    return state.set(action.id, false);
   default:
     return state;
   }
diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js
index a9f3f9529..5817cf49b 100644
--- a/app/javascript/mastodon/reducers/settings.js
+++ b/app/javascript/mastodon/reducers/settings.js
@@ -2,6 +2,7 @@ import { SETTING_CHANGE, SETTING_SAVE } from '../actions/settings';
 import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE } from '../actions/columns';
 import { STORE_HYDRATE } from '../actions/store';
 import { EMOJI_USE } from '../actions/emojis';
+import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists';
 import { Map as ImmutableMap, fromJS } from 'immutable';
 import uuid from '../uuid';
 
@@ -84,6 +85,8 @@ const moveColumn = (state, uuid, direction) => {
 
 const updateFrequentEmojis = (state, emoji) => state.update('frequentlyUsedEmojis', ImmutableMap(), map => map.update(emoji.id, 0, count => count + 1)).set('saved', false);
 
+const filterDeadListColumns = (state, listId) => state.update('columns', columns => columns.filterNot(column => column.get('id') === 'LIST' && column.get('params').get('id') === listId));
+
 export default function settings(state = initialState, action) {
   switch(action.type) {
   case STORE_HYDRATE:
@@ -106,6 +109,10 @@ export default function settings(state = initialState, action) {
     return updateFrequentEmojis(state, action.emoji);
   case SETTING_SAVE:
     return state.set('saved', true);
+  case LIST_FETCH_FAIL:
+    return action.error.response.status === 404 ? filterDeadListColumns(state, action.id) : state;
+  case LIST_DELETE_SUCCESS:
+    return filterDeadListColumns(state, action.id);
   default:
     return state;
   }
diff --git a/app/javascript/packs/about.js b/app/javascript/packs/about.js
index 6ce8757dc..63e12da42 100644
--- a/app/javascript/packs/about.js
+++ b/app/javascript/packs/about.js
@@ -1,9 +1,7 @@
-import loadPolyfills from 'themes/glitch/util/load_polyfills';
-
-require.context('../images/', true);
+import loadPolyfills from '../mastodon/load_polyfills';
 
 function loaded() {
-  const TimelineContainer = require('themes/glitch/containers/timeline_container').default;
+  const TimelineContainer = require('../mastodon/containers/timeline_container').default;
   const React             = require('react');
   const ReactDOM          = require('react-dom');
   const mountNode         = document.getElementById('mastodon-timeline');
@@ -15,7 +13,7 @@ function loaded() {
 }
 
 function main() {
-  const ready = require('themes/glitch/util/ready').default;
+  const ready = require('../mastodon/ready').default;
   ready(loaded);
 }
 
diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js
index 21dc78986..116632dea 100644
--- a/app/javascript/packs/application.js
+++ b/app/javascript/packs/application.js
@@ -1,16 +1,5 @@
-//  THIS IS THE `vanilla` THEME PACK FILE!!
-//  IT'S HERE FOR UPSTREAM COMPATIBILITY!!
-//  THE `glitch` PACK FILE IS IN `themes/glitch/index.js`!!
-
 import loadPolyfills from '../mastodon/load_polyfills';
 
-// import default stylesheet with variables
-require('font-awesome/css/font-awesome.css');
-
-import '../styles/application.scss';
-
-require.context('../images/', true);
-
 loadPolyfills().then(() => {
   require('../mastodon/main').default();
 }).catch(e => {
diff --git a/app/javascript/packs/common.js b/app/javascript/packs/common.js
index 96e6f4b16..5d42509c5 100644
--- a/app/javascript/packs/common.js
+++ b/app/javascript/packs/common.js
@@ -1,6 +1 @@
-import { start } from 'rails-ujs';
-import 'font-awesome/css/font-awesome.css';
-
-require.context('../images/', true);
-
-start();
+import 'styles/application.scss';
diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
index 6adacad98..3472af6c1 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -1,33 +1,14 @@
-import loadPolyfills from 'themes/glitch/util/load_polyfills';
-import { processBio } from 'themes/glitch/util/bio_metadata';
-import ready from 'themes/glitch/util/ready';
-
-window.addEventListener('message', e => {
-  const data = e.data || {};
-
-  if (!window.parent || data.type !== 'setHeight') {
-    return;
-  }
-
-  ready(() => {
-    window.parent.postMessage({
-      type: 'setHeight',
-      id: data.id,
-      height: document.getElementsByTagName('html')[0].scrollHeight,
-    }, '*');
-  });
-});
+import loadPolyfills from '../mastodon/load_polyfills';
+import ready from '../mastodon/ready';
 
 function main() {
-  const { length } = require('stringz');
   const IntlRelativeFormat = require('intl-relativeformat').default;
-  const { delegate } = require('rails-ujs');
-  const emojify = require('../themes/glitch/util/emoji').default;
-  const { getLocale } = require('mastodon/locales');
+  const emojify = require('../mastodon/features/emoji/emoji').default;
+  const { getLocale } = require('../mastodon/locales');
   const { localeData } = getLocale();
-  const VideoContainer = require('../themes/glitch/containers/video_container').default;
-  const MediaGalleryContainer = require('../themes/glitch/containers/media_gallery_container').default;
-  const CardContainer = require('../themes/glitch/containers/card_container').default;
+  const VideoContainer = require('../mastodon/containers/video_container').default;
+  const MediaGalleryContainer = require('../mastodon/containers/media_gallery_container').default;
+  const CardContainer = require('../mastodon/containers/card_container').default;
   const React = require('react');
   const ReactDOM = require('react-dom');
 
@@ -87,61 +68,6 @@ function main() {
       ReactDOM.render(<CardContainer locale={locale} {...props} />, content);
     });
   });
-
-  delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
-    if (button !== 0) {
-      return true;
-    }
-    window.location.href = target.href;
-    return false;
-  });
-
-  delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => {
-    const contentEl = target.parentNode.parentNode.querySelector('.e-content');
-
-    if (contentEl.style.display === 'block') {
-      contentEl.style.display = 'none';
-      target.parentNode.style.marginBottom = 0;
-    } else {
-      contentEl.style.display = 'block';
-      target.parentNode.style.marginBottom = null;
-    }
-
-    return false;
-  });
-
-  delegate(document, '.account_display_name', 'input', ({ target }) => {
-    const nameCounter = document.querySelector('.name-counter');
-
-    if (nameCounter) {
-      nameCounter.textContent = 30 - length(target.value);
-    }
-  });
-
-  delegate(document, '.account_note', 'input', ({ target }) => {
-    const noteCounter = document.querySelector('.note-counter');
-
-    if (noteCounter) {
-      const noteWithoutMetadata = processBio(target.value).text;
-      noteCounter.textContent = 500 - length(noteWithoutMetadata);
-    }
-  });
-
-  delegate(document, '#account_avatar', 'change', ({ target }) => {
-    const avatar = document.querySelector('.card.compact .avatar img');
-    const [file] = target.files || [];
-    const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
-
-    avatar.src = url;
-  });
-
-  delegate(document, '#account_header', 'change', ({ target }) => {
-    const header = document.querySelector('.card.compact');
-    const [file] = target.files || [];
-    const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc;
-
-    header.style.backgroundImage = `url(${url})`;
-  });
 }
 
 loadPolyfills().then(main).catch(error => {
diff --git a/app/javascript/packs/share.js b/app/javascript/packs/share.js
index 9cd95bcee..e9580f648 100644
--- a/app/javascript/packs/share.js
+++ b/app/javascript/packs/share.js
@@ -1,9 +1,7 @@
-import loadPolyfills from 'themes/glitch/util/load_polyfills';
-
-require.context('../images/', true);
+import loadPolyfills from '../mastodon/load_polyfills';
 
 function loaded() {
-  const ComposeContainer = require('themes/glitch/containers/compose_container').default;
+  const ComposeContainer = require('../mastodon/containers/compose_container').default;
   const React = require('react');
   const ReactDOM = require('react-dom');
   const mountNode = document.getElementById('mastodon-compose');
@@ -15,7 +13,7 @@ function loaded() {
 }
 
 function main() {
-  const ready = require('themes/glitch/util/ready').default;
+  const ready = require('../mastodon/ready').default;
   ready(loaded);
 }
 
diff --git a/app/javascript/skins/vanilla/win95.scss b/app/javascript/skins/vanilla/win95.scss
new file mode 100644
index 000000000..298f6ee9d
--- /dev/null
+++ b/app/javascript/skins/vanilla/win95.scss
@@ -0,0 +1 @@
+@import 'styles/win95';
diff --git a/app/javascript/styles/common.scss b/app/javascript/styles/common.scss
deleted file mode 100644
index c1772e7ae..000000000
--- a/app/javascript/styles/common.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-// This makes our fonts available everywhere.
-
-@import 'fonts/roboto';
-@import 'fonts/roboto-mono';
-@import 'fonts/montserrat';
diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss
index 358d86eec..e45fc03d3 100644
--- a/app/javascript/styles/mastodon/about.scss
+++ b/app/javascript/styles/mastodon/about.scss
@@ -19,7 +19,7 @@
     display: inline;
     margin: 0;
     padding: 0;
-    font-weight: 500;
+    font-weight: 700;
     background: transparent;
     font-family: inherit;
     font-size: inherit;
diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss
index 23e20a366..9015d04cb 100644
--- a/app/javascript/styles/mastodon/accounts.scss
+++ b/app/javascript/styles/mastodon/accounts.scss
@@ -473,6 +473,12 @@
       strong {
         font-weight: 500;
         color: $ui-base-color;
+
+        @each $lang in $cjk-langs {
+          &:lang(#{$lang}) {
+            font-weight: 700;
+          }
+        }
       }
 
       span {
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index 7f078470e..bddea557b 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -121,6 +121,12 @@
       strong {
         color: $primary-text-color;
         font-weight: 500;
+
+        @each $lang in $cjk-langs {
+          &:lang(#{$lang}) {
+            font-weight: 700;
+          }
+        }
       }
     }
 
@@ -222,6 +228,12 @@
       font-weight: 500;
       text-transform: uppercase;
       font-size: 12px;
+
+      @each $lang in $cjk-langs {
+        &:lang(#{$lang}) {
+          font-weight: 700;
+        }
+      }
     }
 
     a {
@@ -265,6 +277,12 @@
     font-size: 14px;
     line-height: 18px;
     color: $ui-secondary-color;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 
   .account-card {
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 26ff8bf00..17322264e 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -280,6 +280,12 @@
   strong {
     color: darken($ui-secondary-color, 65%);
     font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 
   a {
@@ -440,11 +446,6 @@
     position: absolute;
     right: 5px;
     top: 5px;
-
-    ::-webkit-scrollbar-track:hover,
-    ::-webkit-scrollbar-track:active {
-      background-color: rgba($base-overlay-background, 0.3);
-    }
   }
 }
 
@@ -517,7 +518,10 @@
   font-weight: 400;
   overflow: hidden;
   white-space: pre-wrap;
-  padding-top: 5px;
+
+  &:focus {
+    outline: 0;
+  }
 
   &.status__content--with-spoiler {
     white-space: normal;
@@ -530,7 +534,7 @@
   .emojione {
     width: 20px;
     height: 20px;
-    margin: -5px 0 0;
+    margin: -3px 0 0;
   }
 
   p {
@@ -766,7 +770,7 @@
 .status__action-bar {
   align-items: center;
   display: flex;
-  margin-top: 5px;
+  margin-top: 10px;
 }
 
 .status__action-bar-button {
@@ -799,7 +803,7 @@
     .emojione {
       width: 24px;
       height: 24px;
-      margin: -5px 0 0;
+      margin: -3px 0 0;
     }
   }
 
@@ -965,6 +969,12 @@
 
   strong {
     font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 
   a {
@@ -1066,6 +1076,12 @@
     font-size: 15px;
     font-weight: 500;
     color: $primary-text-color;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 
   abbr {
@@ -2085,7 +2101,7 @@
 .getting-started {
   box-sizing: border-box;
   padding-bottom: 235px;
-  background: url('../images/mastodon-getting-started.png') no-repeat 0 100%;
+  background: url('~images/mastodon-getting-started.png') no-repeat 0 100%;
   flex: 1 0 auto;
 
   p {
@@ -2298,7 +2314,7 @@ button.icon-button.active i.fa-retweet {
   justify-content: center;
 
   & > div {
-    background: url('../images/mastodon-not-found.png') no-repeat center -50px;
+    background: url('~images/mastodon-not-found.png') no-repeat center -50px;
     padding-top: 210px;
     width: 100%;
   }
@@ -2359,6 +2375,10 @@ button.icon-button.active i.fa-retweet {
   margin-left: 0;
 }
 
+.column-header__links .text-btn {
+  margin-right: 10px;
+}
+
 .column-header__button {
   background: lighten($ui-base-color, 4%);
   border: 0;
@@ -2398,6 +2418,14 @@ button.icon-button.active i.fa-retweet {
   &.animating {
     overflow-y: hidden;
   }
+
+  hr {
+    height: 0;
+    background: transparent;
+    border: 0;
+    border-top: 1px solid lighten($ui-base-color, 12%);
+    margin: 10px 0;
+  }
 }
 
 .column-header__collapsible-inner {
@@ -2974,6 +3002,12 @@ button.icon-button.active i.fa-retweet {
     font-weight: 500;
     display: block;
     color: $ui-base-color;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 }
 
@@ -3171,7 +3205,7 @@ button.icon-button.active i.fa-retweet {
   img,
   canvas {
     display: block;
-    background: url('../images/void.png') repeat;
+    background: url('~images/void.png') repeat;
     object-fit: contain;
   }
 
@@ -3408,6 +3442,12 @@ button.icon-button.active i.fa-retweet {
       border-radius: 4px;
       font-size: 14px;
       padding: 3px 6px;
+
+      @each $lang in $cjk-langs {
+        &:lang(#{$lang}) {
+          font-weight: 700;
+        }
+      }
     }
   }
 }
@@ -3418,7 +3458,7 @@ button.icon-button.active i.fa-retweet {
 }
 
 .onboarding-modal__page-one__elephant-friend {
-  background: url('../images/elephant-friend-1.png') no-repeat center center / contain;
+  background: url('~images/elephant-friend-1.png') no-repeat center center / contain;
   width: 155px;
   height: 193px;
   margin-right: 15px;
@@ -3738,6 +3778,12 @@ button.icon-button.active i.fa-retweet {
 
   strong {
     font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 }
 
@@ -4440,3 +4486,94 @@ noscript {
     margin-bottom: 0;
   }
 }
+
+.column-inline-form {
+  padding: 7px 15px;
+  padding-right: 5px;
+  display: flex;
+  justify-content: flex-start;
+  align-items: center;
+  background: lighten($ui-base-color, 4%);
+
+  label {
+    flex: 1 1 auto;
+
+    input {
+      width: 100%;
+      margin-bottom: 6px;
+
+      &:focus {
+        outline: 0;
+      }
+    }
+  }
+
+  .icon-button {
+    flex: 0 0 auto;
+    margin-left: 5px;
+  }
+}
+
+.drawer__backdrop {
+  cursor: pointer;
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba($base-overlay-background, 0.5);
+}
+
+.list-editor {
+  background: $ui-base-color;
+  flex-direction: column;
+  border-radius: 8px;
+  box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
+  width: 380px;
+  overflow: hidden;
+
+  @media screen and (max-width: 420px) {
+    width: 90%;
+  }
+
+  h4 {
+    padding: 15px 0;
+    background: lighten($ui-base-color, 13%);
+    font-weight: 500;
+    font-size: 16px;
+    text-align: center;
+    border-radius: 8px 8px 0 0;
+  }
+
+  .drawer__pager {
+    height: 50vh;
+  }
+
+  .drawer__inner {
+    border-radius: 0 0 8px 8px;
+
+    &.backdrop {
+      width: calc(100% - 60px);
+      box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
+      border-radius: 0 0 0 8px;
+    }
+  }
+
+  &__accounts {
+    overflow-y: auto;
+  }
+
+  .account__display-name {
+    &:hover strong {
+      text-decoration: none;
+    }
+  }
+
+  .account__avatar {
+    cursor: default;
+  }
+
+  .search {
+    margin-bottom: 0;
+  }
+}
diff --git a/app/javascript/styles/mastodon/emoji_picker.scss b/app/javascript/styles/mastodon/emoji_picker.scss
index 2b46d30fc..4161cc045 100644
--- a/app/javascript/styles/mastodon/emoji_picker.scss
+++ b/app/javascript/styles/mastodon/emoji_picker.scss
@@ -95,6 +95,11 @@
   padding: 0 6px 6px;
   background: $simple-background-color;
   will-change: transform;
+
+  &::-webkit-scrollbar-track:hover,
+  &::-webkit-scrollbar-track:active {
+    background-color: rgba($base-overlay-background, 0.3);
+  }
 }
 
 .emoji-mart-search {
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 61fcf286f..2bef53cff 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -56,6 +56,12 @@ code {
 
   strong {
     font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 
   .label_input {
@@ -395,6 +401,12 @@ code {
 
   strong {
     font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 
   @media screen and (max-width: 740px) and (min-width: 441px) {
@@ -430,6 +442,12 @@ code {
   strong {
     color: $ui-secondary-color;
     font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 
   @media screen and (max-width: 740px) and (min-width: 441px) {
@@ -474,6 +492,12 @@ code {
 
     strong {
       font-weight: 500;
+
+      @each $lang in $cjk-langs {
+        &:lang(#{$lang}) {
+          font-weight: 700;
+        }
+      }
     }
   }
 }
@@ -506,6 +530,12 @@ code {
       display: block;
       margin-bottom: 5px;
 
+      @each $lang in $cjk-langs {
+        &:lang(#{$lang}) {
+          font-weight: 700;
+        }
+      }
+
       .fa {
         font-weight: 400;
       }
diff --git a/app/javascript/styles/mastodon/landing_strip.scss b/app/javascript/styles/mastodon/landing_strip.scss
index 2f7c98d54..ffa1e149d 100644
--- a/app/javascript/styles/mastodon/landing_strip.scss
+++ b/app/javascript/styles/mastodon/landing_strip.scss
@@ -12,6 +12,12 @@
   strong,
   a {
     font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 
   a {
@@ -46,6 +52,12 @@
   strong,
   a {
     font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 
   a {
diff --git a/app/javascript/styles/mastodon/stream_entries.scss b/app/javascript/styles/mastodon/stream_entries.scss
index 4f323a378..a35bbee79 100644
--- a/app/javascript/styles/mastodon/stream_entries.scss
+++ b/app/javascript/styles/mastodon/stream_entries.scss
@@ -119,6 +119,12 @@
       strong {
         font-weight: 500;
         color: $ui-base-color;
+
+        @each $lang in $cjk-langs {
+          &:lang(#{$lang}) {
+            font-weight: 700;
+          }
+        }
       }
 
       span {
@@ -170,6 +176,12 @@
         strong {
           font-weight: 500;
           color: $ui-base-color;
+
+          @each $lang in $cjk-langs {
+            &:lang(#{$lang}) {
+              font-weight: 700;
+            }
+          }
         }
 
         span {
diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss
index ad46f5f9f..92870e679 100644
--- a/app/javascript/styles/mastodon/tables.scss
+++ b/app/javascript/styles/mastodon/tables.scss
@@ -40,6 +40,12 @@
 
   strong {
     font-weight: 500;
+
+    @each $lang in $cjk-langs {
+      &:lang(#{$lang}) {
+        font-weight: 700;
+      }
+    }
   }
 
   &.inline-table > tbody > tr:nth-child(odd) > td,
diff --git a/app/javascript/styles/mastodon/variables.scss b/app/javascript/styles/mastodon/variables.scss
index 52c8cd1cf..dcc2857ff 100644
--- a/app/javascript/styles/mastodon/variables.scss
+++ b/app/javascript/styles/mastodon/variables.scss
@@ -27,3 +27,6 @@ $ui-base-lighter-color: lighten($ui-base-color, 26%) !default; // Lighter darkes
 $ui-primary-color: $classic-primary-color !default;            // Lighter
 $ui-secondary-color: $classic-secondary-color !default;        // Lightest
 $ui-highlight-color: $classic-highlight-color !default;        // Vibrant
+
+// Language codes that uses CJK fonts
+$cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW;
diff --git a/app/javascript/styles/win95.scss b/app/javascript/styles/win95.scss
new file mode 100644
index 000000000..d683218b0
--- /dev/null
+++ b/app/javascript/styles/win95.scss
@@ -0,0 +1,1655 @@
+//  win95 theme from cybrespace.
+
+//  Modified by kibi! to use webpack package syntax for urls (eg,
+//  `url(~images/…)`) for easy importing into skins.
+
+$win95-bg: #bfbfbf;
+$win95-dark-grey: #404040;
+$win95-mid-grey: #808080;
+$win95-window-header: #00007f;
+$win95-tooltip-yellow: #ffffcc;
+$win95-blue: blue;
+
+$ui-base-lighter-color: $win95-dark-grey;
+$ui-highlight-color: $win95-window-header;
+
+@mixin win95-border-outset() {
+  border-left: 2px solid #efefef;
+  border-top: 2px solid #efefef;
+  border-right: 2px solid #404040;
+  border-bottom: 2px solid #404040;
+  border-radius:0px;
+}
+
+@mixin win95-outset() {
+  box-shadow: inset -1px -1px 0px #000000,
+              inset 1px 1px 0px #ffffff,
+              inset -2px -2px 0px #808080,
+              inset 2px 2px 0px #dfdfdf;
+  border-radius:0px;
+}
+
+@mixin win95-border-inset() {
+  border-left: 2px solid #404040;
+  border-top: 2px solid #404040;
+  border-right: 2px solid #efefef;
+  border-bottom: 2px solid #efefef;
+  border-radius:0px;
+}
+
+@mixin win95-border-slight-inset() {
+  border-left: 1px solid #404040;
+  border-top: 1px solid #404040;
+  border-right: 1px solid #efefef;
+  border-bottom: 1px solid #efefef;
+  border-radius:0px;
+}
+
+@mixin win95-inset() {
+  box-shadow: inset 1px 1px 0px #000000,
+              inset -1px -1px 0px #ffffff,
+              inset 2px 2px 0px #808080,
+              inset -2px -2px 0px #dfdfdf;
+  border-width:0px;
+  border-radius:0px;
+}
+
+
+@mixin win95-tab() {
+  box-shadow: inset -1px 0px 0px #000000,
+              inset 1px 0px 0px #ffffff,
+              inset 0px 1px 0px #ffffff,
+              inset 0px 2px 0px #dfdfdf,
+              inset -2px 0px 0px #808080,
+              inset 2px 0px 0px #dfdfdf;
+  border-radius:0px;
+  border-top-left-radius: 1px;
+  border-top-right-radius: 1px;
+}
+
+@mixin win95-reset() {
+  box-shadow: unset;
+}
+
+@font-face {
+  font-family:"premillenium";
+  src: url('~fonts/premillenium/MSSansSerif.ttf') format('truetype');
+}
+
+@import 'application';
+
+/* borrowed from cybrespace style: wider columns and full column width images */
+
+@media screen and (min-width: 1300px) {
+  .column {
+    flex-grow: 1 !important;
+    max-width: 400px;
+  }
+
+  .drawer {
+    width: 17%;
+    max-width: 400px;
+    min-width: 330px;
+  }
+}
+
+.media-gallery,
+.video-player {
+  max-height:30vh;
+  height:30vh !important;
+  position:relative;
+  margin-top:20px;
+  margin-left:-68px;
+  width: calc(100% + 80px) !important;
+  max-width: calc(100% + 80px);
+}
+
+.detailed-status .media-gallery,
+.detailed-status .video-player {
+  margin-left:-5px;
+  width: calc(100% + 9px);
+  max-width: calc(100% + 9px);
+}
+
+.video-player video {
+  transform: unset;
+  top: unset;
+}
+
+.detailed-status .media-spoiler,
+.status .media-spoiler {
+  height: 100%!important;
+  vertical-align: middle;
+}
+
+
+/* main win95 style */
+
+body {
+  font-size:13px;
+  font-family: "MS Sans Serif", "premillenium", sans-serif;
+  color:black;
+}
+
+.ui,
+.ui .columns-area,
+body.admin {
+  background: #008080;
+}
+
+.loading-bar {
+  height:5px;
+  background-color: #000080;
+}
+
+.tabs-bar {
+  background: $win95-bg;
+  @include win95-outset()
+  height: 30px;
+}
+
+.tabs-bar__link {
+  color:black;
+  border:2px outset $win95-bg;
+  border-top-width: 1px;
+  border-left-width: 1px;
+  margin:2px;
+  padding:3px;
+}
+
+.tabs-bar__link.active {
+  @include win95-inset();
+  color:black;
+}
+
+.tabs-bar__link:last-child::before {
+  content:"Start";
+  color:black;
+  font-weight:bold;
+  font-size:15px;
+  width:80%;
+  display:block;
+  position:absolute;
+  right:0px;
+}
+
+.tabs-bar__link:last-child {
+  position:relative;
+  flex-basis:60px !important;
+  font-size:0px;
+  color:$win95-bg;
+
+  background-image: url("~images/start.png");
+  background-repeat:no-repeat;
+  background-position:8%;
+  background-clip:padding-box;
+  background-size:auto 50%;
+}
+
+.drawer .drawer__inner {
+  overflow: visible;
+  height:inherit;
+}
+
+.drawer__pager {
+  overflow-y:auto;
+}
+
+.column {
+  max-height:100vh;
+}
+
+.column > .scrollable {
+  background: $win95-bg;
+  @include win95-border-outset()
+  border-top-width:0px;
+}
+
+.column-header__wrapper {
+  color:white;
+  font-weight:bold;
+  background:#7f7f7f;
+}
+
+.column-header {
+  padding:2px;
+  font-size:13px;
+  background:#7f7f7f;
+  @include win95-border-outset()
+  border-bottom-width:0px;
+  color:white;
+  font-weight:bold;
+}
+
+.column-header__wrapper.active {
+  background:$win95-window-header;
+}
+
+.column-header__wrapper.active::before {
+  display:none;
+}
+.column-header.active {
+  box-shadow:unset;
+  background:$win95-window-header;
+}
+
+.column-header.active .column-header__icon {
+  color:white;
+}
+
+.column-header__button {
+  background: $win95-bg;
+  color: black;
+  line-height:0px;
+  font-size:14px;
+  max-height:20px;
+  padding:0px 2px;
+  margin-top:2px;
+  @include win95-outset()
+}
+
+.column-header__button.active, .column-header__button.active:hover {
+  @include win95-inset();
+  background-color:#7f7f7f;
+}
+
+.column-header__back-button {
+  background: $win95-bg;
+  color: black;
+  padding:2px;
+  max-height:20px;
+  margin-top:2px;
+  @include win95-outset()
+  font-size:13px;
+  font-weight:bold;
+}
+
+.column-back-button {
+  background:$win95-bg;
+  color:black;
+  @include win95-outset()
+  padding:2px;
+  font-size:13px;
+  font-weight:bold;
+}
+
+.column-back-button--slim-button {
+  position:absolute;
+  top:-22px;
+  right:4px;
+  max-height:20px;
+  max-width:60px;
+  padding:0px 2px;
+}
+
+.column-back-button__icon {
+  font-size:11px;
+  margin-top:-3px;
+}
+
+.column-header__collapsible {
+  border-left:2px outset $win95-bg;
+  border-right:2px outset $win95-bg;
+}
+
+.column-header__collapsible-inner {
+  background:$win95-bg;
+  color:black;
+}
+
+.column-header__collapsible__extra {
+  color:black;
+}
+
+.column-header__collapsible__extra div[role="group"] {
+  border: 2px groove $win95-bg;
+  border-radius:4px;
+  margin-bottom:8px;
+  padding:4px;
+}
+
+.column-settings__section {
+  color:black;
+  font-weight:bold;
+  font-size:11px;
+  position:relative;
+  top: -12px;
+  left:4px;
+  background-color:$win95-bg;
+  display:inline-block;
+  padding:0px 4px;
+  margin-bottom:0px;
+}
+
+.setting-meta__label, .setting-toggle__label {
+  color:black;
+  font-weight:normal;
+}
+
+.setting-meta__label span:before {
+  content:"(";
+}
+.setting-meta__label span:after {
+  content:")";
+}
+
+.setting-toggle {
+  line-height:13px;
+}
+
+.react-toggle .react-toggle-track {
+  border-radius:0px;
+  background-color:white;
+  @include win95-border-inset();
+
+  width:12px;
+  height:12px;
+}
+
+.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track {
+  background-color:white;
+}
+
+.react-toggle .react-toggle-track-check {
+  left:2px;
+  transition:unset;
+}
+
+.react-toggle .react-toggle-track-check svg path {
+  fill: black;
+}
+
+.react-toggle .react-toggle-track-x {
+  display:none;
+}
+
+.react-toggle .react-toggle-thumb {
+  border-radius:0px;
+  display:none;
+}
+
+.text-btn {
+  background-color:$win95-bg;
+  @include win95-outset()
+  padding:4px;
+}
+
+.text-btn:hover {
+  text-decoration:none;
+  color:black;
+}
+
+.text-btn:active {
+  @include win95-inset();
+}
+
+.setting-text {
+  color:black;
+  background-color:white;
+  @include win95-inset();
+  font-size:13px;
+  padding:2px;
+}
+
+.setting-text:active, .setting-text:focus,
+.setting-text.light:active, .setting-text.light:focus {
+  color:black;
+  border-bottom:2px inset $win95-bg;
+}
+
+.column-header__setting-arrows .column-header__setting-btn {
+  padding:3px 10px;
+}
+
+.column-header__setting-arrows .column-header__setting-btn:last-child {
+  padding:3px 10px;
+}
+
+.missing-indicator {
+  background-color:$win95-bg;
+  color:black;
+  @include win95-outset()
+}
+
+.missing-indicator > div {
+  background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAYAAAA7bUf6AAAACXBIWXMAAC4jAAAuIwF4pT92AAAAF3pUWHRUaXRsZQAACJnLyy9Jyy/NSwEAD5IDblIFOhoAAAAXelRYdEF1dGhvcgAACJlLzijKz0vMAQALmgLoDsFj8gAAAQpJREFUOMuVlD0OwjAMhd2oQl04Axfo0IGBgYELcAY6cqQuSO0ZOEAZGBg6VKg74gwsEaoESRVHjusI8aQqzY8/PbtOEz1qkFSn2YevlaNOpLMJh2DwvixhuXtOa6/LCh51DUMEFkAsgAZD207Doin8mQ562JpRE5CHBAAhmIqD1L8AqzUUUJkxc6kr3AgAJ+NuvIWRdk7WcrKl0AUqcIBBHOiEbpS4m27mIL5Onfg3k0rgggeQuS2sDOGSahKR+glgqaGLgUJs951NN1q9D72cQqQWR9cr3sm9YcEssEuz6eEuZh2bu0aSOhQ1MBezu2O/+TVSvEFII3qLsZWrSA2AAUQIh1HpyP/kC++zjVSMj6ntAAAAAElFTkSuQmCC')
+    no-repeat;
+  background-position:center center;
+}
+
+.empty-column-indicator,
+.error-column {
+  background: $win95-bg;
+  color: black;
+}
+
+.status__wrapper {
+  border: 2px groove $win95-bg;
+  margin:4px;
+}
+
+.status {
+  @include win95-border-slight-inset();
+  background-color:white;
+  margin:4px;
+  padding-bottom:40px;
+  margin-bottom:8px;
+}
+
+.status.status-direct {
+  background-color:$win95-bg;
+}
+
+.status__content {
+  font-size:13px;
+}
+
+.status.light .status__relative-time,
+.status.light .display-name span {
+  color: #7f7f7f;
+}
+
+.status__action-bar {
+  box-sizing:border-box;
+  position:absolute;
+  bottom:-1px;
+  left:-1px;
+  background:$win95-bg;
+  width:calc(100% + 2px);
+  padding-left:10px;
+  padding: 4px 2px;
+  padding-bottom:4px;
+  border-bottom:2px groove $win95-bg;
+  border-top:1px outset $win95-bg;
+  text-align: right;
+}
+
+.status__wrapper .status__action-bar {
+  border-bottom-width:0px;
+}
+
+.status__action-bar-button {
+  float:right;
+}
+
+.status__action-bar-dropdown {
+  margin-left:auto;
+  margin-right:10px;
+
+  .icon-button {
+    min-width:28px;
+  }
+}
+.status.light .status__content a {
+  color:blue;
+}
+
+.focusable:focus {
+  background: $win95-bg;
+  .detailed-status__action-bar {
+    background: $win95-bg;
+  }
+
+  .status, .detailed-status {
+    background: white;
+    outline:2px dotted $win95-mid-grey;
+  }
+}
+
+.dropdown__trigger.icon-button {
+  padding-right:6px;
+}
+
+.detailed-status__action-bar-dropdown .icon-button {
+  min-width:28px;
+}
+
+.detailed-status {
+  background:white;
+  background-clip:padding-box;
+  margin:4px;
+  border: 2px groove $win95-bg;
+  padding:4px;
+}
+
+.detailed-status__display-name {
+  color:#7f7f7f;
+}
+
+.detailed-status__display-name strong {
+  color:black;
+  font-weight:bold;
+}
+.account__avatar,
+.account__avatar-overlay-base,
+.account__header__avatar,
+.account__avatar-overlay-overlay {
+  @include win95-border-slight-inset();
+  clip-path:none;
+  filter: saturate(1.8) brightness(1.1);
+}
+
+.detailed-status__action-bar {
+  background-color:$win95-bg;
+  border:0px;
+  border-bottom:2px groove $win95-bg;
+  margin-bottom:8px;
+  justify-items:left;
+  padding-left:4px;
+}
+.icon-button {
+  background:$win95-bg;
+  @include win95-border-outset()
+  padding:0px 0px 0px 0px;
+  margin-right:4px;
+
+  color:#3f3f3f;
+  &.inverted, &:hover, &.inverted:hover, &:active, &:focus {
+    color:#3f3f3f;
+  }
+}
+
+.icon-button:active {
+  @include win95-border-inset();
+}
+
+.status__action-bar > .icon-button {
+  padding:0px 15px 0px 0px;
+  min-width:25px;
+}
+
+.icon-button.star-icon,
+.icon-button.star-icon:active {
+  background:transparent;
+  border:none;
+}
+
+.icon-button.star-icon.active {
+  color: $gold-star;
+  &:active,  &:hover, &:focus {
+    color: $gold-star;
+  }
+}
+
+.icon-button.star-icon > i {
+  background:$win95-bg;
+  @include win95-border-outset()
+  padding-bottom:3px;
+}
+
+.icon-button.star-icon:active > i {
+  @include win95-border-inset();
+}
+
+.text-icon-button {
+  color:$win95-dark-grey;
+}
+
+.detailed-status__action-bar-dropdown {
+  margin-left:auto;
+  justify-content:right;
+  padding-right:16px;
+}
+
+.detailed-status__button {
+  flex:0 0 auto;
+}
+
+.detailed-status__button .icon-button {
+  padding-left:2px;
+  padding-right:25px;
+}
+
+.status-card {
+  border-radius:0px;
+  background:white;
+  border: 1px solid black;
+  color:black;
+}
+
+.status-card:hover {
+  background-color:white;
+}
+
+.status-card__title {
+  color:blue;
+  text-decoration:underline;
+  font-weight:bold;
+}
+
+.load-more {
+  width:auto;
+  margin:5px auto;
+  background: $win95-bg;
+  @include win95-outset();
+  color:black;
+  padding: 2px 5px;
+
+  &:hover {
+    background: $win95-bg;
+    color:black;
+  }
+}
+
+.status-card__description {
+ color:black;
+}
+
+.account__display-name strong, .status__display-name strong {
+  color:black;
+  font-weight:bold;
+}
+
+.account .account__display-name {
+  color:black;
+}
+
+.account {
+  border-bottom: 2px groove $win95-bg;
+}
+
+.reply-indicator__content .status__content__spoiler-link, .status__content .status__content__spoiler-link {
+  background:$win95-bg;
+  @include win95-outset()
+}
+
+.reply-indicator__content .status__content__spoiler-link:hover, .status__content .status__content__spoiler-link:hover {
+  background:$win95-bg;
+}
+
+.reply-indicator__content .status__content__spoiler-link:active, .status__content .status__content__spoiler-link:active {
+  @include win95-inset();
+}
+
+.reply-indicator__content a, .status__content a {
+  color:blue;
+}
+
+.notification {
+  border: 2px groove $win95-bg;
+  margin:4px;
+}
+
+.notification__message {
+  color:black;
+  font-size:13px;
+}
+
+.notification__display-name {
+  font-weight:bold;
+}
+
+
+.drawer__header {
+  background: $win95-bg;
+  @include win95-border-outset()
+  justify-content:left;
+  margin-bottom:0px;
+  padding-bottom:2px;
+  border-bottom:2px groove $win95-bg;
+}
+
+.drawer__tab {
+  color:black;
+  @include win95-outset()
+  padding:5px;
+  margin:2px;
+  flex: 0 0 auto;
+}
+
+.drawer__tab:first-child::before {
+  content:"Start";
+  color:black;
+  font-weight:bold;
+  font-size:15px;
+  width:80%;
+  display:block;
+  position:absolute;
+  right:0px;
+
+}
+
+.drawer__tab:first-child {
+  position:relative;
+  padding:5px 15px;
+  width:40px;
+  font-size:0px;
+  color:$win95-bg;
+
+  background-image: url("~images/start.png");
+  background-repeat:no-repeat;
+  background-position:8%;
+  background-clip:padding-box;
+  background-size:auto 50%;
+}
+
+.drawer__header a:hover {
+  background-color:transparent;
+}
+
+.drawer__header a:first-child:hover {
+  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAOCAIAAACpTQvdAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAF3pUWHRBdXRob3IAAAiZS84oys9LzAEAC5oC6A7BY/IAAACWSURBVCiRhVJJDsQgDEuqOfRZ7a1P5gbP4uaJaEjTADMWQhHYjlk4p0wLnNdptdF4KvBUDyGzVwc2xO+uKtH+1o0ytEEmqFpuxlvFCGCxKbNIT56QCi2MzaA/2Mz+mERSOeqzJG2RUxkjdTabgPtFoZ1bZxcKvgPcLZVufAyR9Ni8v5dWDzfFx0giC1RvZFv6l35QQ/Mvv39XXgGzQpoAAAAASUVORK5CYII=");
+  background-repeat:no-repeat;
+  background-position:8%;
+  background-clip:padding-box;
+  background-size:auto 50%;
+  transition:unset;
+}
+
+.drawer__tab:first-child {
+
+}
+
+.search {
+  background:$win95-bg;
+  padding-top:2px;
+  padding:2px;
+  border:2px outset $win95-bg;
+  border-top-width:0px;
+  border-bottom: 2px groove $win95-bg;
+  margin-bottom:0px;
+}
+
+.search input {
+  background-color:white;
+  color:black;
+  @include win95-border-slight-inset();
+}
+
+.search__input:focus {
+  background-color:white;
+}
+
+.search-popout {
+  box-shadow: unset;
+  color:black;
+  border-radius:0px;
+  background-color:$win95-tooltip-yellow;
+  border:1px solid black;
+
+  h4 {
+    color:black;
+    text-transform: none;
+    font-weight:bold;
+  }
+}
+
+.search-results__header {
+  background-color: $win95-bg;
+  color:black;
+  border-bottom:2px groove $win95-bg;
+}
+
+.search-results__hashtag {
+  color:blue;
+}
+
+.search-results__section .account:hover,
+.search-results__section .account:hover .account__display-name,
+.search-results__section .account:hover .account__display-name strong,
+.search-results__section .search-results__hashtag:hover {
+  background-color:$win95-window-header;
+  color:white;
+}
+
+.search__icon .fa {
+  color:#808080;
+
+  &.active {
+    opacity:1.0;
+  }
+
+  &:hover {
+    color: #808080;
+  }
+}
+
+.drawer__inner,
+.drawer__inner.darker {
+  background-color:$win95-bg;
+  border: 2px outset $win95-bg;
+  border-top-width:0px;
+}
+
+.navigation-bar {
+  color:black;
+}
+
+.navigation-bar strong {
+  color:black;
+  font-weight:bold;
+}
+
+.autosuggest-textarea__textarea, .spoiler-input__input {
+  border-radius:0px;
+  @include win95-border-slight-inset();
+}
+
+.autosuggest-textarea__textarea {
+  border-bottom:0px;
+}
+
+.compose-form__uploads-wrapper {
+  border-radius:0px;
+  border-bottom:1px inset $win95-bg;
+  border-top-width:0px;
+}
+
+.compose-form__upload-wrapper {
+  border-left:1px inset $win95-bg;
+  border-right:1px inset $win95-bg;
+}
+
+.compose-form__buttons {
+  background-color:$win95-bg;
+  border-radius:0px;
+  box-shadow:unset;
+  border:2px groove $win95-bg;
+  margin-top:4px;
+  padding:4px 8px;
+}
+
+.privacy-dropdown.active
+.privacy-dropdown__value {
+  background: $win95-bg;
+  box-shadow:unset;
+}
+
+.privacy-dropdown__option.active, .privacy-dropdown__option:hover,
+.privacy-dropdown__option.active:hover {
+  background:$win95-window-header;
+}
+
+.privacy-dropdown.active .privacy-dropdown__dropdown {
+  box-shadow:unset;
+  color:black;
+  @include win95-outset()
+  background: $win95-bg;
+}
+
+.privacy-dropdown__option__content {
+  color:black;
+}
+
+.privacy-dropdown__option__content strong {
+  font-weight:bold;
+}
+
+.compose-form__warning::before {
+  content:"Tip:";
+  font-weight:bold;
+  display:block;
+  position:absolute;
+  top:-10px;
+  background-color:$win95-bg;
+  font-size:11px;
+  padding: 0px 5px;
+}
+
+.compose-form__warning {
+  position:relative;
+  box-shadow:unset;
+  border:2px groove $win95-bg;
+  background-color:$win95-bg;
+  color:black;
+}
+
+.compose-form__warning a {
+  color:blue;
+}
+
+.compose-form__warning strong {
+  color:black;
+  text-decoration:underline;
+}
+
+.compose-form__buttons button.active:last-child {
+  @include win95-border-inset();
+  background: #dfdfdf;
+  color:#7f7f7f;
+}
+
+.compose-form__upload-thumbnail {
+  border-radius:0px;
+  border:2px groove $win95-bg;
+  background-color:$win95-bg;
+  padding:2px;
+  box-sizing:border-box;
+}
+
+.compose-form__upload-thumbnail .icon-button {
+  max-width:20px;
+  max-height:20px;
+  line-height:10px !important;
+}
+
+.compose-form__upload-thumbnail .icon-button::before {
+  content:"X";
+  font-size:13px;
+  font-weight:bold;
+  color:black;
+}
+
+.compose-form__upload-thumbnail .icon-button i {
+  display:none;
+}
+
+.emoji-dialog.with-search {
+  box-shadow:unset;
+  border-radius:0px;
+  background-color:$win95-bg;
+  border:1px solid black;
+  box-sizing:content-box;
+
+}
+
+.emoji-dialog .emoji-search {
+  color:black;
+  background-color:white;
+  border-radius:0px;
+  @include win95-inset();
+}
+
+.emoji-dialog .emoji-search-wrapper {
+  border-bottom:2px groove $win95-bg;
+}
+
+.emoji-dialog .emoji-category-title {
+  color:black;
+  font-weight:bold;
+}
+
+.reply-indicator {
+  background-color:$win95-bg;
+  border-radius:3px;
+  border:2px groove $win95-bg;
+}
+
+.button {
+  background-color:$win95-bg;
+  @include win95-outset()
+  border-radius:0px;
+  color:black;
+  font-weight:bold;
+
+  &:hover, &:focus, &:disabled {
+    background-color:$win95-bg;
+  }
+
+  &:active {
+    @include win95-inset();
+  }
+
+  &:disabled {
+    color: #808080;
+    text-shadow: 1px 1px 0px #efefef;
+
+    &:active {
+      @include win95-outset();
+    }
+  }
+
+}
+
+#Getting-started {
+  background-color:$win95-bg;
+  @include win95-inset();
+  border-bottom-width:0px;
+}
+
+#Getting-started::before {
+  content:"Start";
+  color:black;
+  font-weight:bold;
+  font-size:15px;
+  width:80%;
+  text-align:center;
+  display:block;
+  position:absolute;
+  right:2px;
+}
+
+#Getting-started {
+  position:relative;
+  padding:5px 15px;
+  width:60px;
+  font-size:0px;
+  color:$win95-bg;
+
+  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAOCAIAAACpTQvdAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAF3pUWHRBdXRob3IAAAiZS84oys9LzAEAC5oC6A7BY/IAAACWSURBVCiRhVJJDsQgDEuqOfRZ7a1P5gbP4uaJaEjTADMWQhHYjlk4p0wLnNdptdF4KvBUDyGzVwc2xO+uKtH+1o0ytEEmqFpuxlvFCGCxKbNIT56QCi2MzaA/2Mz+mERSOeqzJG2RUxkjdTabgPtFoZ1bZxcKvgPcLZVufAyR9Ni8v5dWDzfFx0giC1RvZFv6l35QQ/Mvv39XXgGzQpoAAAAASUVORK5CYII=");
+  background-repeat:no-repeat;
+  background-position:8%;
+  background-clip:padding-box;
+  background-size:auto 50%;
+}
+
+.column-subheading {
+  background-color:$win95-bg;
+  color:black;
+  font-size:0px;
+  border-bottom: 2px groove $win95-bg;
+  padding:0px;
+  margin:0px;
+}
+
+.column-link {
+  background-color:transparent;
+  background-size:32px 32px;
+  background-repeat:no-repeat;
+  background-position: 36px 50%;
+  color:black;
+  padding-left:40px;
+
+  &:hover {
+    background-color: $win95-window-header;
+    background-size:32px 32px;
+    background-repeat:no-repeat;
+    background-position: 36px 50%;
+    color:white;
+  }
+
+  i {
+    font-size: 0px;
+    width:32px;
+  }
+}
+
+.column-link[href="/web/timelines/public"] {
+  background-image: url("~images/icon_public.png");
+  &:hover { background-image: url("~images/icon_public.png"); }
+}
+.column-link[href="/web/timelines/public/local"] {
+  background-image: url("~images/icon_local.png");
+  &:hover { background-image: url("~images/icon_local.png"); }
+}
+.column-link[href="/web/pinned"] {
+  background-image: url("~images/icon_pin.png");
+  &:hover { background-image: url("~images/icon_pin.png"); }
+}
+.column-link[href="/web/favourites"] {
+  background-image: url("~images/icon_likes.png");
+  &:hover { background-image: url("~images/icon_likes.png"); }
+}
+.column-link[href="/web/blocks"] {
+  background-image: url("~images/icon_blocks.png");
+  &:hover { background-image: url("~images/icon_blocks.png"); }
+}
+.column-link[href="/web/mutes"] {
+  background-image: url("~images/icon_mutes.png");
+  &:hover { background-image: url("~images/icon_mutes.png"); }
+}
+.column-link[href="/settings/preferences"] {
+  background-image: url("~images/icon_settings.png");
+  &:hover { background-image: url("~images/icon_settings.png"); }
+}
+.column-link[href="/about/more"] {
+  background-image: url("~images/icon_about.png");
+  &:hover { background-image: url("~images/icon_about.png"); }
+}
+.column-link[href="/auth/sign_out"] {
+  background-image: url("~images/icon_logout.png");
+  &:hover { background-image: url("~images/icon_logout.png"); }
+}
+
+.getting-started__footer {
+  display:none;
+}
+
+.getting-started__wrapper::before {
+  content:"Mastodon 95";
+  font-weight:bold;
+  font-size:23px;
+  color:white;
+  line-height:30px;
+  padding-left:20px;
+  padding-right:40px;
+
+  left:0px;
+  bottom:-30px;
+  display:block;
+  position:absolute;
+  background-color:#7f7f7f;
+  width:200%;
+  height:30px;
+
+  -ms-transform: rotate(-90deg);
+
+  -webkit-transform: rotate(-90deg);
+  transform: rotate(-90deg);
+  transform-origin:top left;
+}
+
+.getting-started__wrapper {
+  @include win95-border-outset()
+  background-color:$win95-bg;
+}
+
+.account__header {
+  background-color:#7f7f7f;
+}
+
+.account__header .account__header__content {
+  color:white;
+}
+
+.account__action-bar__tab > span {
+  color:black;
+  font-weight:bold;
+}
+
+.account__action-bar__tab strong {
+  color:black;
+}
+
+.account__action-bar {
+  border: unset;
+}
+
+.account__action-bar__tab {
+  border: 1px outset $win95-bg;
+}
+
+.account__action-bar__tab:active {
+  @include win95-inset();
+}
+
+.dropdown--active .dropdown__content > ul,
+.dropdown-menu {
+  background:$win95-tooltip-yellow;
+  border-radius:0px;
+  border:1px solid black;
+  box-shadow:unset;
+}
+
+.dropdown-menu a {
+  background-color:transparent;
+}
+
+.dropdown--active::after {
+  display:none;
+}
+
+.dropdown--active .icon-button {
+  color:black;
+  @include win95-inset();
+}
+
+.dropdown--active .dropdown__content > ul > li > a {
+  background:transparent;
+}
+
+.dropdown--active .dropdown__content > ul > li > a:hover {
+  background:transparent;
+  color:black;
+  text-decoration:underline;
+}
+
+.dropdown__sep,
+.dropdown-menu__separator
+{
+  border-color:#7f7f7f;
+}
+
+.detailed-status__action-bar-dropdown .dropdown--active .dropdown__content.dropdown__left {
+  left:unset;
+}
+
+.dropdown > .icon-button, .detailed-status__button > .icon-button,
+.status__action-bar > .icon-button, .star-icon i {
+    /* i don't know what's going on with the inline
+       styles someone should look at the react code */
+    height: 25px !important;
+    width: 28px !important;
+    box-sizing: border-box;
+}
+
+.status__action-bar-button .fa-floppy-o {
+    padding-top: 2px;
+}
+
+.status__action-bar-dropdown {
+    position: relative;
+    top: -3px;
+}
+
+.detailed-status__action-bar-dropdown .dropdown {
+    position: relative;
+    top: -4px;
+}
+
+.notification .status__action-bar {
+    border-bottom: none;
+}
+
+.notification .status {
+    margin-bottom: 4px;
+}
+
+.status__wrapper .status {
+    margin-bottom: 3px;
+}
+
+.status__wrapper {
+    margin-bottom: 8px;
+}
+
+.icon-button .fa-retweet {
+    position: relative;
+    top: -1px;
+}
+
+.embed-modal, .error-modal, .onboarding-modal,
+.actions-modal, .boost-modal, .confirmation-modal, .report-modal {
+  @include win95-outset()
+  background:$win95-bg;
+}
+
+.actions-modal::before,
+.boost-modal::before,
+.confirmation-modal::before,
+.report-modal::before {
+  content: "Confirmation";
+  display:block;
+  background:$win95-window-header;
+  color:white;
+  font-weight:bold;
+  padding-left:2px;
+}
+
+.boost-modal::before {
+  content: "Boost confirmation";
+}
+
+.boost-modal__action-bar > div > span:before {
+  content: "Tip: ";
+  font-weight:bold;
+}
+
+.boost-modal__action-bar, .confirmation-modal__action-bar, .report-modal__action-bar {
+  background:$win95-bg;
+  margin-top:-15px;
+}
+
+.embed-modal h4, .error-modal h4, .onboarding-modal h4 {
+  background:$win95-window-header;
+  color:white;
+  font-weight:bold;
+  padding:2px;
+  font-size:13px;
+  text-align:left;
+}
+
+.confirmation-modal__action-bar {
+  .confirmation-modal__cancel-button {
+    color:black;
+
+    &:active,
+    &:focus,
+    &:hover {
+      color:black;
+    }
+
+    &:active {
+      @include win95-inset();
+    }
+  }
+}
+
+.embed-modal .embed-modal__container .embed-modal__html,
+.embed-modal .embed-modal__container .embed-modal__html:focus {
+  background:white;
+  color:black;
+  @include win95-inset();
+}
+
+.modal-root__overlay,
+.account__header > div {
+  background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFnpUWHRUaXRsZQAACJnLzU9JzElKBwALgwLXaCRlPwAAABd6VFh0QXV0aG9yAAAImUvOKMrPS8wBAAuaAugOwWPyAAAAEUlEQVQImWNgYGD4z4AE/gMADwMB/414xEUAAAAASUVORK5CYII=');
+}
+
+
+.admin-wrapper::before {
+  position:absolute;
+  top:0px;
+  content:"Control Panel";
+  color:white;
+  background-color:$win95-window-header;
+  font-size:13px;
+  font-weight:bold;
+  width:calc(100%);
+  margin: 2px;
+  display:block;
+  padding:2px;
+  padding-left:22px;
+  box-sizing:border-box;
+}
+
+.admin-wrapper {
+  position:relative;
+  background: $win95-bg;
+  @include win95-outset()
+  width:70vw;
+  height:80vh;
+  margin:10vh auto;
+  color: black;
+  padding-top:24px;
+  flex-direction:column;
+  overflow:hidden;
+}
+
+@media screen and (max-width: 1120px) {
+  .admin-wrapper {
+    width:90vw;
+    height:95vh;
+    margin:2.5vh auto;
+  }
+}
+
+@media screen and (max-width: 740px) {
+  .admin-wrapper {
+    width:100vw;
+    height:95vh;
+    height:calc(100vh - 24px);
+    margin:0px 0px 0px 0px;
+  }
+}
+
+.admin-wrapper .sidebar-wrapper {
+  position:static;
+  height:auto;
+  flex: 0 0 auto;
+  margin:2px;
+}
+
+.admin-wrapper .content-wrapper {
+  flex: 1 1 auto;
+  width:calc(100% - 20px);
+  @include win95-border-outset()
+  position:relative;
+  margin-left:10px;
+  margin-right:10px;
+  margin-bottom:40px;
+  box-sizing:border-box;
+}
+
+.admin-wrapper .content {
+  background-color: $win95-bg;
+  width: 100%;
+  max-width:100%;
+  min-height:100%;
+  box-sizing:border-box;
+  position:relative;
+}
+
+.admin-wrapper .sidebar {
+  position:static;
+  background: $win95-bg;
+  color:black;
+  width: 100%;
+  height:auto;
+  padding-bottom: 20px;
+}
+
+.admin-wrapper .sidebar .logo {
+  position:absolute;
+  top:2px;
+  left:4px;
+  width:18px;
+  height:18px;
+  margin:0px;
+}
+
+.admin-wrapper .sidebar > ul {
+  background: $win95-bg;
+  margin:0px;
+  margin-left:8px;
+  color:black;
+
+  & > li {
+    display:inline-block;
+
+    &#settings,
+    &#admin {
+      padding:2px;
+      border: 0px solid transparent;
+    }
+
+    &#logout {
+      position:absolute;
+      @include win95-outset();
+      right:12px;
+      bottom:10px;
+    }
+
+    &#web {
+      display:inline-block;
+      @include win95-outset();
+      position:absolute;
+      left: 12px;
+      bottom: 10px;
+    }
+
+    & > a {
+      display:inline-block;
+      @include win95-tab();
+      padding:2px 5px;
+      margin:0px;
+      color:black;
+      vertical-align:baseline;
+
+      &.selected {
+        background: $win95-bg;
+        color:black;
+        padding-top: 4px;
+        padding-bottom:4px;
+      }
+
+      &:hover {
+        background: $win95-bg;
+        color:black;
+      }
+    }
+
+    & > ul {
+      width:calc(100% - 20px);
+      background: transparent;
+      position:absolute;
+      left: 10px;
+      top:54px;
+      z-index:3;
+
+      & > li {
+        background: $win95-bg;
+        display: inline-block;
+        vertical-align:baseline;
+
+        & > a {
+          background: $win95-bg;
+          @include win95-tab();
+          color:black;
+          padding:2px 5px;
+          position:relative;
+          z-index:3;
+
+          &.selected {
+            background: $win95-bg;
+            color:black;
+            padding-bottom:4px;
+            padding-top: 4px;
+            padding-right:7px;
+            margin-left:-2px;
+            margin-right:-2px;
+            position:relative;
+            z-index:4;
+
+            &:first-child {
+              margin-left:0px;
+            }
+
+            &:hover {
+              background: transparent;
+              color:black;
+            }
+          }
+
+          &:hover {
+            background: $win95-bg;
+            color:black;
+          }
+        }
+      }
+    }
+  }
+}
+
+@media screen and (max-width: 1520px) {
+  .admin-wrapper .sidebar > ul > li > ul {
+    max-width:1000px;
+  }
+
+  .admin-wrapper .sidebar {
+    padding-bottom: 45px;
+  }
+}
+
+@media screen and (max-width: 600px) {
+  .admin-wrapper .sidebar > ul > li > ul {
+    max-width:500px;
+  }
+
+  .admin-wrapper {
+    .sidebar {
+      padding:0px;
+      padding-bottom: 70px;
+      width: 100%;
+      height: auto;
+    }
+    .content-wrapper {
+      overflow:auto;
+      height:80%;
+      height:calc(100% - 150px);
+    }
+  }
+}
+
+.flash-message {
+  background-color:$win95-tooltip-yellow;
+  color:black;
+  border:1px solid black;
+  border-radius:0px;
+  position:absolute;
+  top:0px;
+  left:0px;
+  width:100%;
+}
+
+.admin-wrapper table {
+  background-color: white;
+  @include win95-border-slight-inset();
+}
+
+.admin-wrapper .content h2,
+.simple_form .input.with_label .label_input > label,
+.admin-wrapper .content h6,
+.admin-wrapper .content > p,
+.admin-wrapper .content .muted-hint,
+.simple_form span.hint,
+.simple_form h4,
+.simple_form .check_boxes .checkbox label,
+.simple_form .input.with_label.boolean .label_input > label,
+.filters .filter-subset a,
+.simple_form .input.radio_buttons .radio label,
+a.table-action-link,
+a.table-action-link:hover,
+.simple_form .input.with_block_label > label,
+.simple_form p.hint {
+  color:black;
+}
+
+.table > tbody > tr:nth-child(2n+1) > td,
+.table > tbody > tr:nth-child(2n+1) > th {
+  background-color:white;
+}
+
+.simple_form input[type=text],
+.simple_form input[type=number],
+.simple_form input[type=email],
+.simple_form input[type=password],
+.simple_form textarea {
+  color:black;
+  background-color:white;
+  @include win95-border-slight-inset();
+
+  &:active, &:focus {
+    background-color:white;
+  }
+}
+
+.simple_form button,
+.simple_form .button,
+.simple_form .block-button
+{
+  background: $win95-bg;
+  @include win95-outset()
+  color:black;
+  font-weight: normal;
+
+  &:hover {
+    background: $win95-bg;
+  }
+}
+
+.simple_form .warning, .table-form .warning
+{
+  background: $win95-tooltip-yellow;
+  color:black;
+  box-shadow: unset;
+  text-shadow:unset;
+  border:1px solid black;
+
+  a {
+    color: blue;
+    text-decoration:underline;
+  }
+}
+
+.simple_form button.negative,
+.simple_form .button.negative,
+.simple_form .block-button.negative
+{
+  background: $win95-bg;
+}
+
+.filters .filter-subset  {
+  border: 2px groove $win95-bg;
+  padding:2px;
+}
+
+.filters .filter-subset a::before {
+  content: "";
+  background-color:white;
+  border-radius:50%;
+  border:2px solid black;
+  border-top-color:#7f7f7f;
+  border-left-color:#7f7f7f;
+  border-bottom-color:#f5f5f5;
+  border-right-color:#f5f5f5;
+  width:12px;
+  height:12px;
+  display:inline-block;
+  vertical-align:middle;
+  margin-right:2px;
+}
+
+.filters .filter-subset a.selected::before {
+  background-color:black;
+  box-shadow: inset 0 0 0 3px white;
+}
+
+.filters .filter-subset a,
+.filters .filter-subset a:hover,
+.filters .filter-subset a.selected {
+  color:black;
+  border-bottom: 0px solid transparent;
+}
+
diff --git a/app/javascript/themes/glitch/features/generic_not_found/index.js b/app/javascript/themes/glitch/features/generic_not_found/index.js
deleted file mode 100644
index ccd2b87b2..000000000
--- a/app/javascript/themes/glitch/features/generic_not_found/index.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react';
-import Column from 'themes/glitch/features/ui/components/column';
-import MissingIndicator from 'themes/glitch/components/missing_indicator';
-
-const GenericNotFound = () => (
-  <Column>
-    <MissingIndicator />
-  </Column>
-);
-
-export default GenericNotFound;
diff --git a/app/javascript/themes/glitch/features/local_settings/navigation/item/style.scss b/app/javascript/themes/glitch/features/local_settings/navigation/item/style.scss
deleted file mode 100644
index 7f7371993..000000000
--- a/app/javascript/themes/glitch/features/local_settings/navigation/item/style.scss
+++ /dev/null
@@ -1,27 +0,0 @@
-@import 'styles/mastodon/variables';
-
-.glitch.local-settings__navigation__item {
-  display: block;
-  padding: 15px 20px;
-  color: inherit;
-  background: $primary-text-color;
-  border-bottom: 1px $ui-primary-color solid;
-  cursor: pointer;
-  text-decoration: none;
-  outline: none;
-  transition: background .3s;
-
-  &:hover {
-    background: $ui-secondary-color;
-  }
-
-  &.active {
-    background: $ui-highlight-color;
-    color: $primary-text-color;
-  }
-
-  &.close, &.close:hover {
-    background: $error-value-color;
-    color: $primary-text-color;
-  }
-}
diff --git a/app/javascript/themes/glitch/features/local_settings/navigation/style.scss b/app/javascript/themes/glitch/features/local_settings/navigation/style.scss
deleted file mode 100644
index 0336f943b..000000000
--- a/app/javascript/themes/glitch/features/local_settings/navigation/style.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-@import 'styles/mastodon/variables';
-
-.glitch.local-settings__navigation {
-  background: $primary-text-color;
-  color: $ui-base-color;
-  width: 200px;
-  font-size: 15px;
-  line-height: 20px;
-  overflow-y: auto;
-}
diff --git a/app/javascript/themes/glitch/features/local_settings/page/item/style.scss b/app/javascript/themes/glitch/features/local_settings/page/item/style.scss
deleted file mode 100644
index b2d8f7185..000000000
--- a/app/javascript/themes/glitch/features/local_settings/page/item/style.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-@import 'styles/mastodon/variables';
-
-.glitch.local-settings__page__item {
-  select {
-    margin-bottom: 5px;
-  }
-}
diff --git a/app/javascript/themes/glitch/features/local_settings/page/style.scss b/app/javascript/themes/glitch/features/local_settings/page/style.scss
deleted file mode 100644
index e9eedcad0..000000000
--- a/app/javascript/themes/glitch/features/local_settings/page/style.scss
+++ /dev/null
@@ -1,9 +0,0 @@
-@import 'styles/mastodon/variables';
-
-.glitch.local-settings__page {
-  display: block;
-  flex: auto;
-  padding: 15px 20px 15px 20px;
-  width: 360px;
-  overflow-y: auto;
-}
diff --git a/app/javascript/themes/glitch/features/local_settings/style.scss b/app/javascript/themes/glitch/features/local_settings/style.scss
deleted file mode 100644
index 765294607..000000000
--- a/app/javascript/themes/glitch/features/local_settings/style.scss
+++ /dev/null
@@ -1,34 +0,0 @@
-@import 'styles/mastodon/variables';
-
-.glitch.local-settings {
-  position: relative;
-  display: flex;
-  flex-direction: row;
-  background: $ui-secondary-color;
-  color: $ui-base-color;
-  border-radius: 8px;
-  height: 80vh;
-  width: 80vw;
-  max-width: 740px;
-  max-height: 450px;
-  overflow: hidden;
-
-  label {
-    display: block;
-  }
-
-  h1 {
-    font-size: 18px;
-    font-weight: 500;
-    line-height: 24px;
-    margin-bottom: 20px;
-  }
-
-  h2 {
-    font-size: 15px;
-    font-weight: 500;
-    line-height: 20px;
-    margin-top: 20px;
-    margin-bottom: 10px;
-  }
-}
diff --git a/app/javascript/themes/glitch/features/standalone/compose/index.js b/app/javascript/themes/glitch/features/standalone/compose/index.js
deleted file mode 100644
index 8a8118178..000000000
--- a/app/javascript/themes/glitch/features/standalone/compose/index.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-import ComposeFormContainer from 'themes/glitch/features/compose/containers/compose_form_container';
-import NotificationsContainer from 'themes/glitch/features/ui/containers/notifications_container';
-import LoadingBarContainer from 'themes/glitch/features/ui/containers/loading_bar_container';
-import ModalContainer from 'themes/glitch/features/ui/containers/modal_container';
-
-export default class Compose extends React.PureComponent {
-
-  render () {
-    return (
-      <div>
-        <ComposeFormContainer />
-        <NotificationsContainer />
-        <ModalContainer />
-        <LoadingBarContainer className='loading-bar' />
-      </div>
-    );
-  }
-
-}
diff --git a/app/javascript/themes/glitch/index.js b/app/javascript/themes/glitch/index.js
deleted file mode 100644
index 407e1f767..000000000
--- a/app/javascript/themes/glitch/index.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import loadPolyfills from './util/load_polyfills';
-
-// import default stylesheet with variables
-require('font-awesome/css/font-awesome.css');
-
-import './styles/index.scss';
-
-require.context('../../images/', true);
-
-loadPolyfills().then(() => {
-  require('./util/main').default();
-}).catch(e => {
-  console.error(e);
-});
diff --git a/app/javascript/themes/glitch/styles/reset copy.scss b/app/javascript/themes/glitch/styles/reset copy.scss
deleted file mode 100644
index cc5ba9d7c..000000000
--- a/app/javascript/themes/glitch/styles/reset copy.scss
+++ /dev/null
@@ -1,91 +0,0 @@
-/* http://meyerweb.com/eric/tools/css/reset/
-   v2.0 | 20110126
-   License: none (public domain)
-*/
-
-html, body, div, span, applet, object, iframe,
-h1, h2, h3, h4, h5, h6, p, blockquote, pre,
-a, abbr, acronym, address, big, cite, code,
-del, dfn, em, img, ins, kbd, q, s, samp,
-small, strike, strong, sub, sup, tt, var,
-b, u, i, center,
-dl, dt, dd, ol, ul, li,
-fieldset, form, label, legend,
-table, caption, tbody, tfoot, thead, tr, th, td,
-article, aside, canvas, details, embed,
-figure, figcaption, footer, header, hgroup,
-menu, nav, output, ruby, section, summary,
-time, mark, audio, video {
-  margin: 0;
-  padding: 0;
-  border: 0;
-  font-size: 100%;
-  font: inherit;
-  vertical-align: baseline;
-}
-
-/* HTML5 display-role reset for older browsers */
-article, aside, details, figcaption, figure,
-footer, header, hgroup, menu, nav, section {
-  display: block;
-}
-
-body {
-  line-height: 1;
-}
-
-ol, ul {
-  list-style: none;
-}
-
-blockquote, q {
-  quotes: none;
-}
-
-blockquote:before, blockquote:after,
-q:before, q:after {
-  content: '';
-  content: none;
-}
-
-table {
-  border-collapse: collapse;
-  border-spacing: 0;
-}
-
-::-webkit-scrollbar {
-  width: 8px;
-  height: 8px;
-}
-
-::-webkit-scrollbar-thumb {
-  background: lighten($ui-base-color, 4%);
-  border: 0px none $base-border-color;
-  border-radius: 50px;
-}
-
-::-webkit-scrollbar-thumb:hover {
-  background: lighten($ui-base-color, 6%);
-}
-
-::-webkit-scrollbar-thumb:active {
-  background: lighten($ui-base-color, 4%);
-}
-
-::-webkit-scrollbar-track {
-  border: 0px none $base-border-color;
-  border-radius: 0;
-  background: rgba($base-overlay-background, 0.1);
-}
-
-::-webkit-scrollbar-track:hover {
-  background: $ui-base-color;
-}
-
-::-webkit-scrollbar-track:active {
-  background: $ui-base-color;
-}
-
-::-webkit-scrollbar-corner {
-  background: transparent;
-}
diff --git a/app/javascript/themes/glitch/theme.yml b/app/javascript/themes/glitch/theme.yml
deleted file mode 100644
index 49fba8f40..000000000
--- a/app/javascript/themes/glitch/theme.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-#  (REQUIRED) The location of the pack file inside `pack_directory`.
-pack: index.js
-
-#  (OPTIONAL) The directory which contains the pack file.
-#  Defaults to the theme directory (`app/javascript/themes/[theme]`),
-#  but in the case of the vanilla Mastodon theme the pack file is
-#  somewhere else.
-#    pack_directory: app/javascript/packs
-
-#  (OPTIONAL) Additional javascript resources to preload, for use with
-#  lazy-loaded components. It is **STRONGLY RECOMMENDED** that you
-#  derive these pathnames from `themes/[your-theme]` to ensure that
-#  they stay unique. (Of course, vanilla doesn't do this ^^;;)
-preload:
-- themes/glitch/async/getting_started
-- themes/glitch/async/compose
-- themes/glitch/async/home_timeline
-- themes/glitch/async/notifications
diff --git a/app/javascript/themes/glitch/util/async-components.js b/app/javascript/themes/glitch/util/async-components.js
deleted file mode 100644
index 91e85fed5..000000000
--- a/app/javascript/themes/glitch/util/async-components.js
+++ /dev/null
@@ -1,115 +0,0 @@
-export function EmojiPicker () {
-  return import(/* webpackChunkName: "themes/glitch/async/emoji_picker" */'themes/glitch/util/emoji/emoji_picker');
-}
-
-export function Compose () {
-  return import(/* webpackChunkName: "themes/glitch/async/compose" */'themes/glitch/features/compose');
-}
-
-export function Notifications () {
-  return import(/* webpackChunkName: "themes/glitch/async/notifications" */'themes/glitch/features/notifications');
-}
-
-export function HomeTimeline () {
-  return import(/* webpackChunkName: "themes/glitch/async/home_timeline" */'themes/glitch/features/home_timeline');
-}
-
-export function PublicTimeline () {
-  return import(/* webpackChunkName: "themes/glitch/async/public_timeline" */'themes/glitch/features/public_timeline');
-}
-
-export function CommunityTimeline () {
-  return import(/* webpackChunkName: "themes/glitch/async/community_timeline" */'themes/glitch/features/community_timeline');
-}
-
-export function HashtagTimeline () {
-  return import(/* webpackChunkName: "themes/glitch/async/hashtag_timeline" */'themes/glitch/features/hashtag_timeline');
-}
-
-export function DirectTimeline() {
-  return import(/* webpackChunkName: "themes/glitch/async/direct_timeline" */'themes/glitch/features/direct_timeline');
-}
-
-export function Status () {
-  return import(/* webpackChunkName: "themes/glitch/async/status" */'themes/glitch/features/status');
-}
-
-export function GettingStarted () {
-  return import(/* webpackChunkName: "themes/glitch/async/getting_started" */'themes/glitch/features/getting_started');
-}
-
-export function PinnedStatuses () {
-  return import(/* webpackChunkName: "themes/glitch/async/pinned_statuses" */'themes/glitch/features/pinned_statuses');
-}
-
-export function AccountTimeline () {
-  return import(/* webpackChunkName: "themes/glitch/async/account_timeline" */'themes/glitch/features/account_timeline');
-}
-
-export function AccountGallery () {
-  return import(/* webpackChunkName: "themes/glitch/async/account_gallery" */'themes/glitch/features/account_gallery');
-}
-
-export function Followers () {
-  return import(/* webpackChunkName: "themes/glitch/async/followers" */'themes/glitch/features/followers');
-}
-
-export function Following () {
-  return import(/* webpackChunkName: "themes/glitch/async/following" */'themes/glitch/features/following');
-}
-
-export function Reblogs () {
-  return import(/* webpackChunkName: "themes/glitch/async/reblogs" */'themes/glitch/features/reblogs');
-}
-
-export function Favourites () {
-  return import(/* webpackChunkName: "themes/glitch/async/favourites" */'themes/glitch/features/favourites');
-}
-
-export function FollowRequests () {
-  return import(/* webpackChunkName: "themes/glitch/async/follow_requests" */'themes/glitch/features/follow_requests');
-}
-
-export function GenericNotFound () {
-  return import(/* webpackChunkName: "themes/glitch/async/generic_not_found" */'themes/glitch/features/generic_not_found');
-}
-
-export function FavouritedStatuses () {
-  return import(/* webpackChunkName: "themes/glitch/async/favourited_statuses" */'themes/glitch/features/favourited_statuses');
-}
-
-export function Blocks () {
-  return import(/* webpackChunkName: "themes/glitch/async/blocks" */'themes/glitch/features/blocks');
-}
-
-export function Mutes () {
-  return import(/* webpackChunkName: "themes/glitch/async/mutes" */'themes/glitch/features/mutes');
-}
-
-export function OnboardingModal () {
-  return import(/* webpackChunkName: "themes/glitch/async/onboarding_modal" */'themes/glitch/features/ui/components/onboarding_modal');
-}
-
-export function MuteModal () {
-  return import(/* webpackChunkName: "themes/glitch/async/mute_modal" */'themes/glitch/features/ui/components/mute_modal');
-}
-
-export function ReportModal () {
-  return import(/* webpackChunkName: "themes/glitch/async/report_modal" */'themes/glitch/features/ui/components/report_modal');
-}
-
-export function SettingsModal () {
-  return import(/* webpackChunkName: "themes/glitch/async/settings_modal" */'themes/glitch/features/local_settings');
-}
-
-export function MediaGallery () {
-  return import(/* webpackChunkName: "themes/glitch/async/media_gallery" */'themes/glitch/components/media_gallery');
-}
-
-export function Video () {
-  return import(/* webpackChunkName: "themes/glitch/async/video" */'themes/glitch/features/video');
-}
-
-export function EmbedModal () {
-  return import(/* webpackChunkName: "themes/glitch/async/embed_modal" */'themes/glitch/features/ui/components/embed_modal');
-}
diff --git a/app/javascript/themes/mastodon-go b/app/javascript/themes/mastodon-go
deleted file mode 160000
-Subproject 74c0293e83dbb49ea4f27eea108526df6216d2a
diff --git a/app/javascript/themes/vanilla/theme.yml b/app/javascript/themes/vanilla/theme.yml
deleted file mode 100644
index 0b262cc82..000000000
--- a/app/javascript/themes/vanilla/theme.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-#  (REQUIRED) The location of the pack file inside `pack_directory`.
-pack: application.js
-
-#  (OPTIONAL) The directory which contains the pack file.
-#  Defaults to the theme directory (`app/javascript/themes/[theme]`),
-#  but in the case of the vanilla Mastodon theme the pack file is
-#  somewhere else.
-pack_directory: app/javascript/packs
-
-#  (OPTIONAL) Additional javascript resources to preload, for use with
-#  lazy-loaded components. It is **STRONGLY RECOMMENDED** that you
-#  derive these pathnames from `themes/[your-theme]` to ensure that
-#  they stay unique. (Of course, vanilla doesn't do this ^^;;)
-preload:
-- features/getting_started
-- features/compose
-- features/home_timeline
-- features/notifications
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index 01144f595..820189d29 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -3,7 +3,7 @@
 class ActivityPub::Activity
   include JsonLdHelper
 
-  def initialize(json, account, options = {})
+  def initialize(json, account, **options)
     @json    = json
     @account = account
     @object  = @json['object']
@@ -15,7 +15,7 @@ class ActivityPub::Activity
   end
 
   class << self
-    def factory(json, account, options = {})
+    def factory(json, account, **options)
       @json = json
       klass&.new(json, account, options)
     end
diff --git a/app/lib/extractor.rb b/app/lib/extractor.rb
index 738ec89a0..479689d60 100644
--- a/app/lib/extractor.rb
+++ b/app/lib/extractor.rb
@@ -32,7 +32,7 @@ module Extractor
     possible_entries
   end
 
-  def extract_hashtags_with_indices(text, _options = {})
+  def extract_hashtags_with_indices(text, **)
     return [] unless text =~ /#/
 
     tags = []
diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb
index 9d8bc52db..f5bf64cc7 100644
--- a/app/lib/formatter.rb
+++ b/app/lib/formatter.rb
@@ -9,7 +9,7 @@ class Formatter
 
   include ActionView::Helpers::TextHelper
 
-  def format(status, options = {})
+  def format(status, **options)
     if status.reblog?
       prepend_reblog = status.reblog.account.acct
       status         = status.proper
diff --git a/app/lib/ostatus/activity/base.rb b/app/lib/ostatus/activity/base.rb
index 8b27b124f..c5933f3ad 100644
--- a/app/lib/ostatus/activity/base.rb
+++ b/app/lib/ostatus/activity/base.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class OStatus::Activity::Base
-  def initialize(xml, account = nil, options = {})
+  def initialize(xml, account = nil, **options)
     @xml     = xml
     @account = account
     @options = options
diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb
index 3ca6c5943..656e45822 100644
--- a/app/lib/ostatus/atom_serializer.rb
+++ b/app/lib/ostatus/atom_serializer.rb
@@ -319,7 +319,7 @@ class OStatus::AtomSerializer
 
   private
 
-  def append_element(parent, name, content = nil, attributes = {})
+  def append_element(parent, name, content = nil, **attributes)
     element = Ox::Element.new(name)
     attributes.each { |k, v| element[k] = sanitize_str(v) }
     element << sanitize_str(content) unless content.nil?
diff --git a/app/lib/provider_discovery.rb b/app/lib/provider_discovery.rb
index 5e02e6806..bcc4ed500 100644
--- a/app/lib/provider_discovery.rb
+++ b/app/lib/provider_discovery.rb
@@ -2,7 +2,7 @@
 
 class ProviderDiscovery < OEmbed::ProviderDiscovery
   class << self
-    def discover_provider(url, options = {})
+    def discover_provider(url, **options)
       res    = Request.new(:get, url).perform
       format = options[:format]
 
diff --git a/app/lib/request.rb b/app/lib/request.rb
index 30ea0e7ee..7671f4ffc 100644
--- a/app/lib/request.rb
+++ b/app/lib/request.rb
@@ -5,7 +5,7 @@ class Request
 
   include RoutingHelper
 
-  def initialize(verb, url, options = {})
+  def initialize(verb, url, **options)
     @verb    = verb
     @url     = Addressable::URI.parse(url).normalize
     @options = options
diff --git a/app/lib/themes.rb b/app/lib/themes.rb
index f7ec22fd2..863326e2d 100644
--- a/app/lib/themes.rb
+++ b/app/lib/themes.rb
@@ -7,22 +7,59 @@ class Themes
   include Singleton
 
   def initialize
+
+    core = YAML.load_file(Rails.root.join('app', 'javascript', 'core', 'theme.yml'))
+    core['pack'] = Hash.new unless core['pack']
+
     result = Hash.new
-    Dir.glob(Rails.root.join('app', 'javascript', 'themes', '*', 'theme.yml')) do |path|
+    Dir.glob(Rails.root.join('app', 'javascript', 'flavours', '*', 'theme.yml')) do |path|
       data = YAML.load_file(path)
       name = File.basename(File.dirname(path))
       if data['pack']
+        data['name'] = name
+        data['skin'] = { 'default' => [] }
         result[name] = data
       end
     end
+
+    Dir.glob(Rails.root.join('app', 'javascript', 'skins', '*', '*')) do |path|
+      ext = File.extname(path)
+      skin = File.basename(path)
+      name = File.basename(File.dirname(path))
+      if result[name]
+        if File.directory?(path)
+          pack = []
+          Dir.glob(File.join(path, '*.{css,scss}')) do |sheet|
+            pack.push(File.basename(sheet, File.extname(sheet)))
+          end
+        elsif ext.match(/^\.s?css$/i)
+          skin = File.basename(path, ext)
+          pack = ['common']
+        end
+        if skin != 'default'
+          result[name]['skin'][skin] = pack
+        end
+      end
+    end
+
+    @core = core
     @conf = result
+
+  end
+
+  def core
+    @core
   end
 
-  def get(name)
+  def flavour(name)
     @conf[name]
   end
 
-  def names
+  def flavours
     @conf.keys
   end
+
+  def skins_for(name)
+    @conf[name]['skin'].keys
+  end
 end
diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb
index d86959c0b..8af384a2d 100644
--- a/app/lib/user_settings_decorator.rb
+++ b/app/lib/user_settings_decorator.rb
@@ -26,7 +26,8 @@ class UserSettingsDecorator
     user.settings['reduce_motion']       = reduce_motion_preference if change?('setting_reduce_motion')
     user.settings['system_font_ui']      = system_font_ui_preference if change?('setting_system_font_ui')
     user.settings['noindex']             = noindex_preference if change?('setting_noindex')
-    user.settings['theme']               = theme_preference if change?('setting_theme')
+    user.settings['flavour']             = flavour_preference if change?('setting_flavour')
+    user.settings['skin']                = skin_preference if change?('setting_skin')
   end
 
   def merged_notification_emails
@@ -73,10 +74,14 @@ class UserSettingsDecorator
     boolean_cast_setting 'setting_noindex'
   end
 
-  def theme_preference
-    settings['setting_theme']
+  def flavour_preference
+    settings['setting_flavour']
   end
-  
+
+  def skin_preference
+    settings['setting_skin']
+  end
+
   def boolean_cast_setting(key)
     settings[key] == '1'
   end
diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb
index d79f26366..fd2b0649a 100644
--- a/app/mailers/notification_mailer.rb
+++ b/app/mailers/notification_mailer.rb
@@ -63,7 +63,7 @@ class NotificationMailer < ApplicationMailer
     end
   end
 
-  def digest(recipient, opts = {})
+  def digest(recipient, **opts)
     @me            = recipient
     @since         = opts[:since] || @me.user.last_emailed_at || @me.user.current_sign_in_at
     @notifications = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since)
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index bdb29ebad..5a062dc25 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -5,7 +5,7 @@ class UserMailer < Devise::Mailer
 
   helper :instance
 
-  def confirmation_instructions(user, token, _opts = {})
+  def confirmation_instructions(user, token, **)
     @resource = user
     @token    = token
     @instance = Rails.configuration.x.local_domain
@@ -17,7 +17,7 @@ class UserMailer < Devise::Mailer
     end
   end
 
-  def reset_password_instructions(user, token, _opts = {})
+  def reset_password_instructions(user, token, **)
     @resource = user
     @token    = token
     @instance = Rails.configuration.x.local_domain
@@ -29,7 +29,7 @@ class UserMailer < Devise::Mailer
     end
   end
 
-  def password_change(user, _opts = {})
+  def password_change(user, **)
     @resource = user
     @instance = Rails.configuration.x.local_domain
 
diff --git a/app/models/account.rb b/app/models/account.rb
index ffd19fa52..48b17bbb8 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -186,6 +186,21 @@ class Account < ApplicationRecord
     @keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
   end
 
+  def magic_key
+    modulus, exponent = [keypair.public_key.n, keypair.public_key.e].map do |component|
+      result = []
+
+      until component.zero?
+        result << [component % 256].pack('C')
+        component >>= 8
+      end
+
+      result.reverse.join
+    end
+
+    (['RSA'] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.')
+  end
+
   def subscription(webhook_url)
     @subscription ||= OStatus2::Subscription.new(remote_url, secret: secret, webhook: webhook_url, hub: hub_url)
   end
@@ -279,23 +294,46 @@ class Account < ApplicationRecord
       find_by_sql([sql, limit])
     end
 
-    def advanced_search_for(terms, account, limit = 10)
+    def advanced_search_for(terms, account, limit = 10, following = false)
       textsearch, query = generate_query_for_search(terms)
 
-      sql = <<-SQL.squish
-        SELECT
-          accounts.*,
-          (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
-        FROM accounts
-        LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)
-        WHERE #{query} @@ #{textsearch}
-          AND accounts.suspended = false
-        GROUP BY accounts.id
-        ORDER BY rank DESC
-        LIMIT ?
-      SQL
-
-      find_by_sql([sql, account.id, account.id, limit])
+      if following
+        sql = <<-SQL.squish
+          WITH first_degree AS (
+            SELECT target_account_id
+            FROM follows
+            WHERE account_id = ?
+          )
+          SELECT
+            accounts.*,
+            (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
+          FROM accounts
+          LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)
+          WHERE accounts.id IN (SELECT * FROM first_degree)
+            AND #{query} @@ #{textsearch}
+            AND accounts.suspended = false
+          GROUP BY accounts.id
+          ORDER BY rank DESC
+          LIMIT ?
+        SQL
+
+        find_by_sql([sql, account.id, account.id, account.id, limit])
+      else
+        sql = <<-SQL.squish
+          SELECT
+            accounts.*,
+            (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
+          FROM accounts
+          LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)
+          WHERE #{query} @@ #{textsearch}
+            AND accounts.suspended = false
+          GROUP BY accounts.id
+          ORDER BY rank DESC
+          LIMIT ?
+        SQL
+
+        find_by_sql([sql, account.id, account.id, limit])
+      end
     end
 
     private
diff --git a/app/models/list.rb b/app/models/list.rb
index 5d7ba0065..910864b26 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -19,4 +19,23 @@ class List < ApplicationRecord
   has_many :accounts, through: :list_accounts
 
   validates :title, presence: true
+
+  before_destroy :clean_feed_manager
+
+  private
+
+  def clean_feed_manager
+    reblog_key       = FeedManager.instance.key(:list, id, 'reblogs')
+    reblogged_id_set = Redis.current.zrange(reblog_key, 0, -1)
+
+    Redis.current.pipelined do
+      Redis.current.del(FeedManager.instance.key(:list, id))
+      Redis.current.del(reblog_key)
+
+      reblogged_id_set.each do |reblogged_id|
+        reblog_set_key = FeedManager.instance.key(:list, id, "reblogs:#{reblogged_id}")
+        Redis.current.del(reblog_set_key)
+      end
+    end
+  end
 end
diff --git a/app/models/list_account.rb b/app/models/list_account.rb
index c08239aa0..253932590 100644
--- a/app/models/list_account.rb
+++ b/app/models/list_account.rb
@@ -14,6 +14,8 @@ class ListAccount < ApplicationRecord
   belongs_to :account, required: true
   belongs_to :follow, required: true
 
+  validates :account_id, uniqueness: { scope: :list_id }
+
   before_validation :set_follow
 
   private
diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb
index e2bf65d94..5baddba8a 100644
--- a/app/models/preview_card.rb
+++ b/app/models/preview_card.rb
@@ -21,6 +21,7 @@
 #  height             :integer          default(0), not null
 #  created_at         :datetime         not null
 #  updated_at         :datetime         not null
+#  embed_url          :string           default(""), not null
 #
 
 class PreviewCard < ApplicationRecord
diff --git a/app/models/remote_follow.rb b/app/models/remote_follow.rb
index c3f867743..070144e2d 100644
--- a/app/models/remote_follow.rb
+++ b/app/models/remote_follow.rb
@@ -7,8 +7,8 @@ class RemoteFollow
 
   validates :acct, presence: true
 
-  def initialize(attrs = {})
-    @acct = attrs[:acct].gsub(/\A@/, '').strip unless attrs[:acct].nil?
+  def initialize(attrs = nil)
+    @acct = attrs[:acct].gsub(/\A@/, '').strip if !attrs.nil? && !attrs[:acct].nil?
   end
 
   def valid?
diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb
index d19489b36..1d4ebca02 100644
--- a/app/models/session_activation.rb
+++ b/app/models/session_activation.rb
@@ -14,16 +14,6 @@
 #  web_push_subscription_id :integer
 #
 
-#  id              :bigint           not null, primary key
-#  user_id         :bigint           not null
-#  session_id      :string           not null
-#  created_at      :datetime         not null
-#  updated_at      :datetime         not null
-#  user_agent      :string           default(""), not null
-#  ip              :inet
-#  access_token_id :bigint
-#
-
 class SessionActivation < ApplicationRecord
   belongs_to :user, inverse_of: :session_activations, required: true
   belongs_to :access_token, class_name: 'Doorkeeper::AccessToken', dependent: :destroy
@@ -53,7 +43,7 @@ class SessionActivation < ApplicationRecord
       id && where(session_id: id).exists?
     end
 
-    def activate(options = {})
+    def activate(**options)
       activation = create!(options)
       purge_old
       activation
diff --git a/app/models/user.rb b/app/models/user.rb
index 578622fdf..29bdcbd67 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -76,7 +76,7 @@ class User < ApplicationRecord
   has_many :session_activations, dependent: :destroy
 
   delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal,
-           :reduce_motion, :system_font_ui, :noindex, :theme,
+           :reduce_motion, :system_font_ui, :noindex, :flavour, :skin,
            to: :settings, prefix: :setting, allow_nil: false
 
   attr_accessor :invite_code
diff --git a/app/presenters/account_relationships_presenter.rb b/app/presenters/account_relationships_presenter.rb
index a30558bac..bf1ba3716 100644
--- a/app/presenters/account_relationships_presenter.rb
+++ b/app/presenters/account_relationships_presenter.rb
@@ -4,7 +4,7 @@ class AccountRelationshipsPresenter
   attr_reader :following, :followed_by, :blocking,
               :muting, :requested, :domain_blocking
 
-  def initialize(account_ids, current_account_id, options = {})
+  def initialize(account_ids, current_account_id, **options)
     @following       = Account.following_map(account_ids, current_account_id).merge(options[:following_map] || {})
     @followed_by     = Account.followed_by_map(account_ids, current_account_id).merge(options[:followed_by_map] || {})
     @blocking        = Account.blocking_map(account_ids, current_account_id).merge(options[:blocking_map] || {})
diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb
index bc3887a44..b04e10e2f 100644
--- a/app/presenters/status_relationships_presenter.rb
+++ b/app/presenters/status_relationships_presenter.rb
@@ -3,7 +3,7 @@
 class StatusRelationshipsPresenter
   attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map
 
-  def initialize(statuses, current_account_id = nil, options = {})
+  def initialize(statuses, current_account_id = nil, **options)
     if current_account_id.nil?
       @reblogs_map    = {}
       @favourites_map = {}
diff --git a/app/serializers/rest/preview_card_serializer.rb b/app/serializers/rest/preview_card_serializer.rb
index 9c460332c..2df9d07a7 100644
--- a/app/serializers/rest/preview_card_serializer.rb
+++ b/app/serializers/rest/preview_card_serializer.rb
@@ -6,7 +6,7 @@ class REST::PreviewCardSerializer < ActiveModel::Serializer
   attributes :url, :title, :description, :type,
              :author_name, :author_url, :provider_name,
              :provider_url, :html, :width, :height,
-             :image
+             :image, :embed_url
 
   def image
     object.image? ? full_asset_url(object.image.url(:original)) : nil
diff --git a/app/serializers/rest/relationship_serializer.rb b/app/serializers/rest/relationship_serializer.rb
index 998727e37..45bfd4d6e 100644
--- a/app/serializers/rest/relationship_serializer.rb
+++ b/app/serializers/rest/relationship_serializer.rb
@@ -1,15 +1,21 @@
 # frozen_string_literal: true
 
 class REST::RelationshipSerializer < ActiveModel::Serializer
-  attributes :id, :following, :followed_by, :blocking,
-             :muting, :requested, :domain_blocking
+  attributes :id, :following, :showing_reblogs, :followed_by, :blocking,
+             :muting, :muting_notifications, :requested, :domain_blocking
 
   def id
     object.id.to_s
   end
 
   def following
-    instance_options[:relationships].following[object.id] || false
+    instance_options[:relationships].following[object.id] ? true : false
+  end
+
+  def showing_reblogs
+    (instance_options[:relationships].following[object.id] || {})[:reblogs] ||
+      (instance_options[:relationships].requested[object.id] || {})[:reblogs] ||
+      false
   end
 
   def followed_by
@@ -21,11 +27,15 @@ class REST::RelationshipSerializer < ActiveModel::Serializer
   end
 
   def muting
-    instance_options[:relationships].muting[object.id] || false
+    instance_options[:relationships].muting[object.id] ? true : false
+  end
+
+  def muting_notifications
+    (instance_options[:relationships].muting[object.id] || {})[:notifications] || false
   end
 
   def requested
-    instance_options[:relationships].requested[object.id] || false
+    instance_options[:relationships].requested[object.id] ? true : false
   end
 
   def domain_blocking
diff --git a/app/serializers/webfinger_serializer.rb b/app/serializers/webfinger_serializer.rb
new file mode 100644
index 000000000..f80d12c02
--- /dev/null
+++ b/app/serializers/webfinger_serializer.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class WebfingerSerializer < ActiveModel::Serializer
+  include RoutingHelper
+
+  attributes :subject, :aliases, :links
+
+  def subject
+    object.to_webfinger_s
+  end
+
+  def aliases
+    [short_account_url(object), account_url(object)]
+  end
+
+  def links
+    [
+      { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) },
+      { rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(object, format: 'atom') },
+      { rel: 'self', type: 'application/activity+json', href: account_url(object) },
+      { rel: 'salmon', href: api_salmon_url(object.id) },
+      { rel: 'magic-public-key', href: "data:application/magic-public-key,#{object.magic_key}" },
+      { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_follow_url}?acct={uri}" },
+    ]
+  end
+end
diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb
index b0c663d02..a289ceac4 100644
--- a/app/services/account_search_service.rb
+++ b/app/services/account_search_service.rb
@@ -1,12 +1,12 @@
 # frozen_string_literal: true
 
 class AccountSearchService < BaseService
-  attr_reader :query, :limit, :resolve, :account
+  attr_reader :query, :limit, :options, :account
 
-  def call(query, limit, resolve = false, account = nil)
-    @query = query
-    @limit = limit
-    @resolve = resolve
+  def call(query, limit, account = nil, options = {})
+    @query   = query
+    @limit   = limit
+    @options = options
     @account = account
 
     search_service_results
@@ -25,7 +25,7 @@ class AccountSearchService < BaseService
   end
 
   def resolving_non_matching_remote_account?
-    resolve && !exact_match && !domain_is_local?
+    options[:resolve] && !exact_match && !domain_is_local?
   end
 
   def search_results_and_exact_match
@@ -58,12 +58,16 @@ class AccountSearchService < BaseService
     @_domain_is_local ||= TagManager.instance.local_domain?(query_domain)
   end
 
+  def search_from
+    options[:following] && account ? account.following : Account
+  end
+
   def exact_match
     @_exact_match ||= begin
       if domain_is_local?
-        Account.find_local(query_username)
+        search_from.find_local(query_username)
       else
-        Account.find_remote(query_username, query_domain)
+        search_from.find_remote(query_username, query_domain)
       end
     end
   end
@@ -79,7 +83,7 @@ class AccountSearchService < BaseService
   end
 
   def advanced_search_results
-    Account.advanced_search_for(terms_for_query, account, limit)
+    Account.advanced_search_for(terms_for_query, account, limit, options[:following])
   end
 
   def simple_search_results
diff --git a/app/services/activitypub/process_collection_service.rb b/app/services/activitypub/process_collection_service.rb
index db4d1b4bc..eb93329e9 100644
--- a/app/services/activitypub/process_collection_service.rb
+++ b/app/services/activitypub/process_collection_service.rb
@@ -3,7 +3,7 @@
 class ActivityPub::ProcessCollectionService < BaseService
   include JsonLdHelper
 
-  def call(body, account, options = {})
+  def call(body, account, **options)
     @account = account
     @json    = Oj.load(body, mode: :strict)
     @options = options
diff --git a/app/services/authorize_follow_service.rb b/app/services/authorize_follow_service.rb
index b1bff8962..f47d488f1 100644
--- a/app/services/authorize_follow_service.rb
+++ b/app/services/authorize_follow_service.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class AuthorizeFollowService < BaseService
-  def call(source_account, target_account, options = {})
+  def call(source_account, target_account, **options)
     if options[:skip_follow_request]
       follow_request = FollowRequest.new(account: source_account, target_account: target_account)
     else
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index 37cf75379..cec96d927 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -74,9 +74,6 @@ class FetchLinkCardService < BaseService
 
     return false unless response.respond_to?(:type)
 
-    # The photo will change the URL. So, to avoid duplication of URLs, PreviewCard needs to be checked again.
-    @card = PreviewCard.find_by(url: response.url) || @card if response.type == 'photo'
-
     @card.type          = response.type
     @card.title         = response.respond_to?(:title)         ? response.title         : ''
     @card.author_name   = response.respond_to?(:author_name)   ? response.author_name   : ''
@@ -90,9 +87,9 @@ class FetchLinkCardService < BaseService
     when 'link'
       @card.image = URI.parse(response.thumbnail_url) if response.respond_to?(:thumbnail_url)
     when 'photo'
-      @card.url    = response.url
-      @card.width  = response.width.presence  || 0
-      @card.height = response.height.presence || 0
+      @card.embed_url = response.url
+      @card.width     = response.width.presence  || 0
+      @card.height    = response.height.presence || 0
     when 'video'
       @card.width  = response.width.presence  || 0
       @card.height = response.height.presence || 0
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index 974c586f2..59531a76c 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -13,7 +13,7 @@ class PostStatusService < BaseService
   # @option [Doorkeeper::Application] :application
   # @option [String] :idempotency Optional idempotency key
   # @return [Status]
-  def call(account, text, in_reply_to = nil, options = {})
+  def call(account, text, in_reply_to = nil, **options)
     if options[:idempotency].present?
       existing_id = redis.get("idempotency:status:#{account.id}:#{options[:idempotency]}")
       return Status.find(existing_id) if existing_id
diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb
index 60eff135e..30a9dd85e 100644
--- a/app/services/process_feed_service.rb
+++ b/app/services/process_feed_service.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class ProcessFeedService < BaseService
-  def call(body, account, options = {})
+  def call(body, account, **options)
     @options = options
 
     xml = Nokogiri::XML(body)
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index 7789bd441..e164c03ab 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -3,7 +3,7 @@
 class RemoveStatusService < BaseService
   include StreamEntryRenderer
 
-  def call(status, options = {})
+  def call(status, **options)
     @payload      = Oj.dump(event: :delete, payload: status.id.to_s)
     @status       = status
     @account      = status.account
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 1ed3f0032..85ad94463 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -10,7 +10,7 @@ class SearchService < BaseService
       if url_query?
         results.merge!(remote_resource_results) unless remote_resource.nil?
       elsif query.present?
-        results[:accounts] = AccountSearchService.new.call(query, limit, resolve, account)
+        results[:accounts] = AccountSearchService.new.call(query, limit, account, resolve: resolve)
         results[:hashtags] = Tag.search_for(query.gsub(/\A#/, ''), limit) unless query.start_with?('@')
       end
     end
diff --git a/app/services/suspend_account_service.rb b/app/services/suspend_account_service.rb
index 5b37ba9ba..958b28cdc 100644
--- a/app/services/suspend_account_service.rb
+++ b/app/services/suspend_account_service.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class SuspendAccountService < BaseService
-  def call(account, options = {})
+  def call(account, **options)
     @account = account
     @options = options
 
diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml
index 7ffa5ecc3..d92362bd7 100644
--- a/app/views/about/more.html.haml
+++ b/app/views/about/more.html.haml
@@ -2,7 +2,6 @@
   = site_hostname
 
 - content_for :header_tags do
-  = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
   = render partial: 'shared/og'
 
 .landing-page
diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml
index 385b0b1dc..4f5b53470 100644
--- a/app/views/about/show.html.haml
+++ b/app/views/about/show.html.haml
@@ -3,7 +3,6 @@
 
 - content_for :header_tags do
   %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
-  = javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous'
   = render partial: 'shared/og'
 
 .landing-page
diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml
index 5747cc274..7dd962bf2 100644
--- a/app/views/admin/reports/show.html.haml
+++ b/app/views/admin/reports/show.html.haml
@@ -1,6 +1,3 @@
-- content_for :header_tags do
-  = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous'
-
 - content_for :page_title do
   = t('admin.reports.report', id: @report.id)
 
diff --git a/app/views/admin/statuses/index.html.haml b/app/views/admin/statuses/index.html.haml
index fe2581527..9747a92cf 100644
--- a/app/views/admin/statuses/index.html.haml
+++ b/app/views/admin/statuses/index.html.haml
@@ -1,6 +1,3 @@
-- content_for :header_tags do
-  = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous'
-
 - content_for :page_title do
   = t('admin.statuses.title')
 
diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml
index 63b3a0c26..e8a81656c 100644
--- a/app/views/home/index.html.haml
+++ b/app/views/home/index.html.haml
@@ -1,13 +1,7 @@
 - content_for :header_tags do
-  - if theme_data['preload']
-    - theme_data['preload'].each do |link|
-      %link{ href: asset_pack_path("#{link}.js"), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
   %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key}
   %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
 
-  = javascript_pack_tag "themes/#{current_theme}", integrity: true, crossorigin: 'anonymous'
-  = stylesheet_pack_tag "themes/#{current_theme}", integrity: true, media: 'all'
-
 .app-holder#mastodon{ data: { props: Oj.dump(default_props) } }
   %noscript
     = image_tag asset_pack_path('logo.svg'), alt: 'Mastodon'
diff --git a/app/views/layouts/_theme.html.haml b/app/views/layouts/_theme.html.haml
new file mode 100644
index 000000000..066d9de42
--- /dev/null
+++ b/app/views/layouts/_theme.html.haml
@@ -0,0 +1,13 @@
+- if theme
+  - if theme[:pack] != 'common' && theme[:common]
+    = render partial: 'layouts/theme', object: theme[:common]
+  - if theme[:pack]
+    = javascript_pack_tag theme[:flavour] ? "flavours/#{theme[:flavour]}/#{theme[:pack]}" : "core/#{theme[:pack]}", integrity: true, crossorigin: 'anonymous'
+    - if theme[:skin]
+      - if !theme[:flavour] || theme[:skin] == 'default'
+        = stylesheet_pack_tag theme[:flavour] ? "flavours/#{theme[:flavour]}/#{theme[:pack]}" : "core/#{theme[:pack]}", integrity: true, media: 'all'
+      - else
+        = stylesheet_pack_tag "skins/#{theme[:flavour]}/#{theme[:skin]}/#{theme[:pack]}"
+    - if theme[:preload]
+      - theme[:preload].each do |link|
+        %link{ href: asset_pack_path("#{link}.js"), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/
diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml
index c98d85f7b..66382db50 100644
--- a/app/views/layouts/admin.html.haml
+++ b/app/views/layouts/admin.html.haml
@@ -1,6 +1,3 @@
-- content_for :header_tags do
-  = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
-
 - content_for :content do
   .admin-wrapper
     .sidebar-wrapper
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 24b74c787..99ae7d90d 100755
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -18,16 +18,16 @@
         = ' - '
       = title
 
-    = stylesheet_pack_tag 'common', media: 'all'
-    = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous'
+    = javascript_pack_tag "locales", integrity: true, crossorigin: 'anonymous'
     = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous'
     = csrf_meta_tags
 
-    - if controller_name != 'home'
-      = stylesheet_pack_tag 'application', integrity: true, media: 'all'
-
     = yield :header_tags
 
+    -#  These must come after :header_tags to ensure our initial state has been defined.
+    = render partial: 'layouts/theme', object: @core
+    = render partial: 'layouts/theme', object: @theme
+
   - body_classes ||= @body_classes || ''
   - body_classes += ' system-font' if current_account&.user&.setting_system_font_ui
 
diff --git a/app/views/layouts/auth.html.haml b/app/views/layouts/auth.html.haml
index d8ac733f9..f4812ac6a 100644
--- a/app/views/layouts/auth.html.haml
+++ b/app/views/layouts/auth.html.haml
@@ -1,6 +1,3 @@
-- content_for :header_tags do
-  = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
-
 - content_for :content do
   .container
     .logo-container
diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml
index 5fc60be17..935670514 100644
--- a/app/views/layouts/embedded.html.haml
+++ b/app/views/layouts/embedded.html.haml
@@ -4,10 +4,8 @@
     %meta{ charset: 'utf-8' }/
     %meta{ name: 'robots', content: 'noindex' }/
 
-    = stylesheet_pack_tag 'common', media: 'all'
-    = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous'
-    = stylesheet_pack_tag 'application', integrity: true, media: 'all'
+    = javascript_pack_tag "locales", integrity: true, crossorigin: 'anonymous'
     = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous'
-    = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
-  %body.embed
+    = render partial: 'layouts/theme', object: @core
+    = render partial: 'layouts/theme', object: @theme
     = yield
diff --git a/app/views/layouts/error.html.haml b/app/views/layouts/error.html.haml
index d0eae4434..9904b8fdd 100644
--- a/app/views/layouts/error.html.haml
+++ b/app/views/layouts/error.html.haml
@@ -5,8 +5,8 @@
     %meta{ charset: 'utf-8' }/
     %title= safe_join([yield(:page_title), Setting.default_settings['site_title']], ' - ')
     %meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/
-    = stylesheet_pack_tag 'common', media: 'all'
-    = stylesheet_pack_tag 'application', integrity: true, media: 'all'
+    = render partial: 'layouts/theme', object: @core
+    = render partial: 'layouts/theme', object: @theme
   %body.error
     .dialog
       %img{ alt: Setting.default_settings['site_title'], src: '/oops.gif' }/
diff --git a/app/views/layouts/modal.html.haml b/app/views/layouts/modal.html.haml
index a819e098d..d3519f032 100644
--- a/app/views/layouts/modal.html.haml
+++ b/app/views/layouts/modal.html.haml
@@ -1,6 +1,3 @@
-- content_for :header_tags do
-  = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
-
 - content_for :content do
   - if user_signed_in?
     .account-header
diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml
index 83e92b938..b3795eaad 100644
--- a/app/views/layouts/public.html.haml
+++ b/app/views/layouts/public.html.haml
@@ -1,6 +1,3 @@
-- content_for :header_tags do
-  = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
-
 - content_for :content do
   .container= yield
   .footer
diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml
index 69e26a7be..9564c0399 100644
--- a/app/views/settings/preferences/show.html.haml
+++ b/app/views/settings/preferences/show.html.haml
@@ -26,8 +26,9 @@
   %h4= t 'preferences.web'
 
   .fields-group
-    - if Themes.instance.names.size > 1
-      = f.input :setting_theme, collection: Themes.instance.names, label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, include_blank: false
+    - if Themes.instance.flavours.size > 1
+      = f.input :setting_flavour, collection: Themes.instance.flavours, label_method: lambda { |flavour| I18n.t("themes.#{flavour}", default: flavour) }, wrapper: :with_label, include_blank: false
+      = f.input :setting_skin, collection: Themes.instance.skins_for(current_flavour), label_method: lambda { |skin| I18n.t("themes.#{current_flavour}.skins.#{skin}", default: skin) }, wrapper: :with_label, include_blank: false
 
     = f.input :setting_unfollow_modal, as: :boolean, wrapper: :with_label
     = f.input :setting_boost_modal, as: :boolean, wrapper: :with_label
diff --git a/app/views/shares/show.html.haml b/app/views/shares/show.html.haml
index 44b6f145f..4c0390c42 100644
--- a/app/views/shares/show.html.haml
+++ b/app/views/shares/show.html.haml
@@ -1,5 +1,4 @@
 - content_for :header_tags do
   %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
-  = javascript_pack_tag 'share', integrity: true, crossorigin: 'anonymous'
 
 #mastodon-compose{ data: { props: Oj.dump(default_props) } }
diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml
index ea8b0faa3..e05fe1c39 100644
--- a/app/views/tags/show.html.haml
+++ b/app/views/tags/show.html.haml
@@ -3,7 +3,6 @@
 
 - content_for :header_tags do
   %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
-  = javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous'
   = render 'og'
 
 .landing-page.tag-page
diff --git a/app/views/well_known/webfinger/show.json.rabl b/app/views/well_known/webfinger/show.json.rabl
deleted file mode 100644
index 762d1860d..000000000
--- a/app/views/well_known/webfinger/show.json.rabl
+++ /dev/null
@@ -1,18 +0,0 @@
-object @account
-
-node(:subject) { @canonical_account_uri }
-
-node(:aliases) do
-  [short_account_url(@account), account_url(@account)]
-end
-
-node(:links) do
-  [
-    { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(@account) },
-    { rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(@account, format: 'atom') },
-    { rel: 'self', type: 'application/activity+json', href: account_url(@account) },
-    { rel: 'salmon', href: api_salmon_url(@account.id) },
-    { rel: 'magic-public-key', href: "data:application/magic-public-key,#{@magic_key}" },
-    { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_follow_url}?acct={uri}" },
-  ]
-end
diff --git a/app/views/well_known/webfinger/show.xml.ruby b/app/views/well_known/webfinger/show.xml.ruby
index b0158b8bd..0c7289d6a 100644
--- a/app/views/well_known/webfinger/show.xml.ruby
+++ b/app/views/well_known/webfinger/show.xml.ruby
@@ -1,13 +1,13 @@
 Nokogiri::XML::Builder.new do |xml|
   xml.XRD(xmlns: 'http://docs.oasis-open.org/ns/xri/xrd-1.0') do
-    xml.Subject @canonical_account_uri
+    xml.Subject @account.to_webfinger_s
     xml.Alias short_account_url(@account)
     xml.Alias account_url(@account)
     xml.Link(rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(@account))
     xml.Link(rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(@account, format: 'atom'))
     xml.Link(rel: 'self', type: 'application/activity+json', href: account_url(@account))
     xml.Link(rel: 'salmon', href: api_salmon_url(@account.id))
-    xml.Link(rel: 'magic-public-key', href: "data:application/magic-public-key,#{@magic_key}")
+    xml.Link(rel: 'magic-public-key', href: "data:application/magic-public-key,#{@account.magic_key}")
     xml.Link(rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_follow_url}?acct={uri}")
   end
 end.to_xml
diff --git a/app/workers/scheduler/feed_cleanup_scheduler.rb b/app/workers/scheduler/feed_cleanup_scheduler.rb
index cfa2d31a4..23fa7672b 100644
--- a/app/workers/scheduler/feed_cleanup_scheduler.rb
+++ b/app/workers/scheduler/feed_cleanup_scheduler.rb
@@ -5,16 +5,30 @@ class Scheduler::FeedCleanupScheduler
   include Sidekiq::Worker
 
   def perform
+    clean_home_feeds!
+    clean_list_feeds!
+  end
+
+  private
+
+  def clean_home_feeds!
+    clean_feeds!(inactive_account_ids, :home)
+  end
+
+  def clean_list_feeds!
+    clean_feeds!(inactive_list_ids, :list)
+  end
+
+  def clean_feeds!(ids, type)
     reblogged_id_sets = {}
-    feedmanager = FeedManager.instance
 
     redis.pipelined do
-      inactive_user_ids.each do |account_id|
-        redis.del(feedmanager.key(:home, account_id))
-        reblog_key = feedmanager.key(:home, account_id, 'reblogs')
+      ids.each do |feed_id|
+        redis.del(feed_manager.key(type, feed_id))
+        reblog_key = feed_manager.key(type, feed_id, 'reblogs')
         # We collect a future for this: we don't block while getting
         # it, but we can iterate over it later.
-        reblogged_id_sets[account_id] = redis.zrange(reblog_key, 0, -1)
+        reblogged_id_sets[feed_id] = redis.zrange(reblog_key, 0, -1)
         redis.del(reblog_key)
       end
     end
@@ -22,19 +36,25 @@ class Scheduler::FeedCleanupScheduler
     # Remove all of the reblog tracking keys we just removed the
     # references to.
     redis.pipelined do
-      reblogged_id_sets.each do |account_id, future|
+      reblogged_id_sets.each do |feed_id, future|
         future.value.each do |reblogged_id|
-          reblog_set_key = feedmanager.key(:home, account_id, "reblogs:#{reblogged_id}")
+          reblog_set_key = feed_manager.key(type, feed_id, "reblogs:#{reblogged_id}")
           redis.del(reblog_set_key)
         end
       end
     end
   end
 
-  private
+  def inactive_account_ids
+    @inactive_account_ids ||= User.confirmed.inactive.pluck(:account_id)
+  end
+
+  def inactive_list_ids
+    List.where(account_id: inactive_account_ids).pluck(:id)
+  end
 
-  def inactive_user_ids
-    @inactive_user_ids ||= User.confirmed.inactive.pluck(:account_id)
+  def feed_manager
+    FeedManager.instance
   end
 
   def redis