about summary refs log tree commit diff
path: root/app/javascript
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript')
-rw-r--r--app/javascript/fonts/montserrat/Montserrat-Medium.ttfbin0 -> 192488 bytes
-rw-r--r--app/javascript/images/cloud2.pngbin0 -> 4973 bytes
-rw-r--r--app/javascript/images/cloud3.pngbin0 -> 5860 bytes
-rw-r--r--app/javascript/images/cloud4.pngbin0 -> 5273 bytes
-rw-r--r--app/javascript/images/elephant-fren.pngbin0 -> 40859 bytes
-rw-r--r--app/javascript/images/logo.svg2
-rw-r--r--app/javascript/mastodon/actions/bundles.js25
-rw-r--r--app/javascript/mastodon/actions/notifications.js4
-rw-r--r--app/javascript/mastodon/actions/store.js7
-rw-r--r--app/javascript/mastodon/actions/timelines.js10
-rw-r--r--app/javascript/mastodon/components/column_header.js2
-rw-r--r--app/javascript/mastodon/components/dropdown_menu.js19
-rw-r--r--app/javascript/mastodon/components/permalink.js4
-rw-r--r--app/javascript/mastodon/containers/mastodon.js5
-rw-r--r--app/javascript/mastodon/containers/timeline_container.js39
-rw-r--r--app/javascript/mastodon/emoji.js82
-rw-r--r--app/javascript/mastodon/features/account_timeline/index.js4
-rw-r--r--app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js3
-rw-r--r--app/javascript/mastodon/features/compose/index.js20
-rw-r--r--app/javascript/mastodon/features/notifications/index.js8
-rw-r--r--app/javascript/mastodon/features/report/containers/status_check_box_container.js4
-rw-r--r--app/javascript/mastodon/features/standalone/public_timeline/index.js76
-rw-r--r--app/javascript/mastodon/features/ui/components/bundle.js101
-rw-r--r--app/javascript/mastodon/features/ui/components/bundle_column_error.js44
-rw-r--r--app/javascript/mastodon/features/ui/components/bundle_modal_error.js53
-rw-r--r--app/javascript/mastodon/features/ui/components/column_loading.js19
-rw-r--r--app/javascript/mastodon/features/ui/components/columns_area.js74
-rw-r--r--app/javascript/mastodon/features/ui/components/image_loader.js17
-rw-r--r--app/javascript/mastodon/features/ui/components/media_modal.js19
-rw-r--r--app/javascript/mastodon/features/ui/components/modal_loading.js20
-rw-r--r--app/javascript/mastodon/features/ui/components/modal_root.js55
-rw-r--r--app/javascript/mastodon/features/ui/components/onboarding_modal.js44
-rw-r--r--app/javascript/mastodon/features/ui/components/report_modal.js4
-rw-r--r--app/javascript/mastodon/features/ui/components/tabs_bar.js26
-rw-r--r--app/javascript/mastodon/features/ui/containers/bundle_container.js19
-rw-r--r--app/javascript/mastodon/features/ui/containers/status_list_container.js6
-rw-r--r--app/javascript/mastodon/features/ui/index.js94
-rw-r--r--app/javascript/mastodon/features/ui/util/get_rect_from_entry.js21
-rw-r--r--app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js9
-rw-r--r--app/javascript/mastodon/features/ui/util/react_router_helpers.js57
-rw-r--r--app/javascript/mastodon/locales/ar.json7
-rw-r--r--app/javascript/mastodon/locales/bg.json7
-rw-r--r--app/javascript/mastodon/locales/ca.json7
-rw-r--r--app/javascript/mastodon/locales/de.json7
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json63
-rw-r--r--app/javascript/mastodon/locales/en.json7
-rw-r--r--app/javascript/mastodon/locales/eo.json7
-rw-r--r--app/javascript/mastodon/locales/es.json7
-rw-r--r--app/javascript/mastodon/locales/fa.json7
-rw-r--r--app/javascript/mastodon/locales/fi.json7
-rw-r--r--app/javascript/mastodon/locales/fr.json63
-rw-r--r--app/javascript/mastodon/locales/he.json7
-rw-r--r--app/javascript/mastodon/locales/hr.json7
-rw-r--r--app/javascript/mastodon/locales/hu.json7
-rw-r--r--app/javascript/mastodon/locales/id.json7
-rw-r--r--app/javascript/mastodon/locales/io.json7
-rw-r--r--app/javascript/mastodon/locales/it.json7
-rw-r--r--app/javascript/mastodon/locales/ja.json7
-rw-r--r--app/javascript/mastodon/locales/ko.json181
-rw-r--r--app/javascript/mastodon/locales/nl.json17
-rw-r--r--app/javascript/mastodon/locales/no.json7
-rw-r--r--app/javascript/mastodon/locales/oc.json15
-rw-r--r--app/javascript/mastodon/locales/pl.json9
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json9
-rw-r--r--app/javascript/mastodon/locales/pt.json7
-rw-r--r--app/javascript/mastodon/locales/ru.json7
-rw-r--r--app/javascript/mastodon/locales/th.json7
-rw-r--r--app/javascript/mastodon/locales/tr.json7
-rw-r--r--app/javascript/mastodon/locales/uk.json7
-rw-r--r--app/javascript/mastodon/locales/whitelist_ko.json2
-rw-r--r--app/javascript/mastodon/locales/whitelist_zh-HK.json1
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json9
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json9
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json9
-rw-r--r--app/javascript/mastodon/main.js12
-rw-r--r--app/javascript/mastodon/reducers/accounts.js6
-rw-r--r--app/javascript/mastodon/reducers/accounts_counters.js8
-rw-r--r--app/javascript/mastodon/reducers/alerts.js6
-rw-r--r--app/javascript/mastodon/reducers/cards.js6
-rw-r--r--app/javascript/mastodon/reducers/compose.js31
-rw-r--r--app/javascript/mastodon/reducers/contexts.js18
-rw-r--r--app/javascript/mastodon/reducers/index.js22
-rw-r--r--app/javascript/mastodon/reducers/media_attachments.js4
-rw-r--r--app/javascript/mastodon/reducers/meta.js4
-rw-r--r--app/javascript/mastodon/reducers/notifications.js14
-rw-r--r--app/javascript/mastodon/reducers/relationships.js6
-rw-r--r--app/javascript/mastodon/reducers/reports.js16
-rw-r--r--app/javascript/mastodon/reducers/search.js16
-rw-r--r--app/javascript/mastodon/reducers/settings.js30
-rw-r--r--app/javascript/mastodon/reducers/status_lists.js10
-rw-r--r--app/javascript/mastodon/reducers/statuses.js6
-rw-r--r--app/javascript/mastodon/reducers/timelines.js24
-rw-r--r--app/javascript/mastodon/reducers/user_lists.js32
-rw-r--r--app/javascript/mastodon/selectors/index.js6
-rw-r--r--app/javascript/packs/common.js5
-rw-r--r--app/javascript/packs/public.js18
-rw-r--r--app/javascript/styles/about.scss448
-rw-r--r--app/javascript/styles/basics.scss24
-rw-r--r--app/javascript/styles/boost.scss19
-rw-r--r--app/javascript/styles/components.scss121
-rw-r--r--app/javascript/styles/containers.scss48
-rw-r--r--app/javascript/styles/fonts/montserrat.scss8
-rw-r--r--app/javascript/styles/forms.scss127
-rw-r--r--app/javascript/styles/stream_entries.scss12
104 files changed, 2069 insertions, 575 deletions
diff --git a/app/javascript/fonts/montserrat/Montserrat-Medium.ttf b/app/javascript/fonts/montserrat/Montserrat-Medium.ttf
new file mode 100644
index 000000000..88d70b89c
--- /dev/null
+++ b/app/javascript/fonts/montserrat/Montserrat-Medium.ttf
Binary files differdiff --git a/app/javascript/images/cloud2.png b/app/javascript/images/cloud2.png
new file mode 100644
index 000000000..f325ca6de
--- /dev/null
+++ b/app/javascript/images/cloud2.png
Binary files differdiff --git a/app/javascript/images/cloud3.png b/app/javascript/images/cloud3.png
new file mode 100644
index 000000000..ab194d0b8
--- /dev/null
+++ b/app/javascript/images/cloud3.png
Binary files differdiff --git a/app/javascript/images/cloud4.png b/app/javascript/images/cloud4.png
new file mode 100644
index 000000000..98323f5a2
--- /dev/null
+++ b/app/javascript/images/cloud4.png
Binary files differdiff --git a/app/javascript/images/elephant-fren.png b/app/javascript/images/elephant-fren.png
new file mode 100644
index 000000000..3b64edf08
--- /dev/null
+++ b/app/javascript/images/elephant-fren.png
Binary files differdiff --git a/app/javascript/images/logo.svg b/app/javascript/images/logo.svg
index c233db842..16cb3a944 100644
--- a/app/javascript/images/logo.svg
+++ b/app/javascript/images/logo.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" height="1000" width="1000"><path d="M500 0a500 500 0 0 0-353.553 146.447 500 500 0 1 0 707.106 707.106A500 500 0 0 0 500 0zm-.059 280.05h107.12c-19.071 13.424-26.187 51.016-27.12 73.843V562.05c0 44.32-35.68 80-80 80s-80-35.68-80-80v-202c0-44.32 35.68-80 80-80zm-.441 52c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zm-279.059 7.9c44.32 0 80 35.68 80 80v206.157c.933 22.827 8.049 60.42 27.12 73.842H220.44c-44.32 0-80-35.68-80-80v-200c0-44.32 35.68-80 80-80zm559.12 0c44.32 0 80 35.68 80 80v200c0 44.32-35.68 80-80 80H672.44c19.071-13.424 26.187-51.016 27.12-73.843V419.95c0-44.32 35.68-80 80-80zM220 392c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm560 0c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm-280.5 40.05c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zM220 491.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zM499.5 532c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zM220 591.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28z" fill="#189efc"/></svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" height="1000" width="1000"><path d="M500 0a500 500 0 0 0-353.553 146.447 500 500 0 1 0 707.106 707.106A500 500 0 0 0 500 0zm-.059 280.05h107.12c-19.071 13.424-26.187 51.016-27.12 73.843V562.05c0 44.32-35.68 80-80 80s-80-35.68-80-80v-202c0-44.32 35.68-80 80-80zm-.441 52c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zm-279.059 7.9c44.32 0 80 35.68 80 80v206.157c.933 22.827 8.049 60.42 27.12 73.842H220.44c-44.32 0-80-35.68-80-80v-200c0-44.32 35.68-80 80-80zm559.12 0c44.32 0 80 35.68 80 80v200c0 44.32-35.68 80-80 80H672.44c19.071-13.424 26.187-51.016 27.12-73.843V419.95c0-44.32 35.68-80 80-80zM220 392c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm560 0c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zm-280.5 40.05c-15.464 0-28 12.537-28 28 0 15.465 12.536 28 28 28s28-12.535 28-28c0-15.463-12.536-28-28-28zM220 491.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zM499.5 532c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28zM220 591.95c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28zm560 0c-15.464 0-28 12.535-28 28 0 15.463 12.536 28 28 28s28-12.537 28-28c0-15.465-12.536-28-28-28z" fill="#fff"/></svg>
diff --git a/app/javascript/mastodon/actions/bundles.js b/app/javascript/mastodon/actions/bundles.js
new file mode 100644
index 000000000..ecc9c8f7d
--- /dev/null
+++ b/app/javascript/mastodon/actions/bundles.js
@@ -0,0 +1,25 @@
+export const BUNDLE_FETCH_REQUEST = 'BUNDLE_FETCH_REQUEST';
+export const BUNDLE_FETCH_SUCCESS = 'BUNDLE_FETCH_SUCCESS';
+export const BUNDLE_FETCH_FAIL = 'BUNDLE_FETCH_FAIL';
+
+export function fetchBundleRequest(skipLoading) {
+  return {
+    type: BUNDLE_FETCH_REQUEST,
+    skipLoading,
+  };
+}
+
+export function fetchBundleSuccess(skipLoading) {
+  return {
+    type: BUNDLE_FETCH_SUCCESS,
+    skipLoading,
+  };
+}
+
+export function fetchBundleFail(error, skipLoading) {
+  return {
+    type: BUNDLE_FETCH_FAIL,
+    error,
+    skipLoading,
+  };
+}
diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js
index cda636139..c7d248122 100644
--- a/app/javascript/mastodon/actions/notifications.js
+++ b/app/javascript/mastodon/actions/notifications.js
@@ -1,5 +1,5 @@
 import api, { getLinks } from '../api';
-import Immutable from 'immutable';
+import { List as ImmutableList } from 'immutable';
 import IntlMessageFormat from 'intl-messageformat';
 import { fetchRelationships } from './accounts';
 import { defineMessages } from 'react-intl';
@@ -124,7 +124,7 @@ export function refreshNotificationsFail(error, skipLoading) {
 
 export function expandNotifications() {
   return (dispatch, getState) => {
-    const items  = getState().getIn(['notifications', 'items'], Immutable.List());
+    const items  = getState().getIn(['notifications', 'items'], ImmutableList());
 
     if (getState().getIn(['notifications', 'isLoading']) || items.size === 0) {
       return;
diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js
index 601cea001..0597d265e 100644
--- a/app/javascript/mastodon/actions/store.js
+++ b/app/javascript/mastodon/actions/store.js
@@ -1,10 +1,11 @@
-import Immutable from 'immutable';
+import { Iterable, fromJS } from 'immutable';
 
 export const STORE_HYDRATE = 'STORE_HYDRATE';
+export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
 
 const convertState = rawState =>
-  Immutable.fromJS(rawState, (k, v) =>
-    Immutable.Iterable.isIndexed(v) ? v.toList() : v.toMap().mapKeys(x =>
+  fromJS(rawState, (k, v) =>
+    Iterable.isIndexed(v) ? v.toList() : v.toMap().mapKeys(x =>
       Number.isNaN(x * 1) ? x : x * 1));
 
 export function hydrateStore(rawState) {
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index cb4410eba..dd14cb1cd 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -1,5 +1,5 @@
 import api, { getLinks } from '../api';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
 export const TIMELINE_UPDATE  = 'TIMELINE_UPDATE';
 export const TIMELINE_DELETE  = 'TIMELINE_DELETE';
@@ -66,13 +66,13 @@ export function refreshTimelineRequest(timeline, skipLoading) {
 
 export function refreshTimeline(timelineId, path, params = {}) {
   return function (dispatch, getState) {
-    const timeline = getState().getIn(['timelines', timelineId], Immutable.Map());
+    const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
 
     if (timeline.get('isLoading') || timeline.get('online')) {
       return;
     }
 
-    const ids      = timeline.get('items', Immutable.List());
+    const ids      = timeline.get('items', ImmutableList());
     const newestId = ids.size > 0 ? ids.first() : null;
 
     let skipLoading = timeline.get('loaded');
@@ -111,8 +111,8 @@ export function refreshTimelineFail(timeline, error, skipLoading) {
 
 export function expandTimeline(timelineId, path, params = {}) {
   return (dispatch, getState) => {
-    const timeline = getState().getIn(['timelines', timelineId], Immutable.Map());
-    const ids      = timeline.get('items', Immutable.List());
+    const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
+    const ids      = timeline.get('items', ImmutableList());
 
     if (timeline.get('isLoading') || ids.size === 0) {
       return;
diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js
index 027d01767..e9f041be6 100644
--- a/app/javascript/mastodon/components/column_header.js
+++ b/app/javascript/mastodon/components/column_header.js
@@ -10,7 +10,7 @@ export default class ColumnHeader extends React.PureComponent {
   };
 
   static propTypes = {
-    title: PropTypes.string.isRequired,
+    title: PropTypes.node.isRequired,
     icon: PropTypes.string.isRequired,
     active: PropTypes.bool,
     multiColumn: PropTypes.bool,
diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js
index 12e1b44fa..98323b069 100644
--- a/app/javascript/mastodon/components/dropdown_menu.js
+++ b/app/javascript/mastodon/components/dropdown_menu.js
@@ -14,6 +14,7 @@ export default class DropdownMenu extends React.PureComponent {
     size: PropTypes.number.isRequired,
     direction: PropTypes.string,
     ariaLabel: PropTypes.string,
+    disabled: PropTypes.bool,
   };
 
   static defaultProps = {
@@ -68,9 +69,19 @@ export default class DropdownMenu extends React.PureComponent {
   }
 
   render () {
-    const { icon, items, size, direction, ariaLabel } = this.props;
-    const { expanded } = this.state;
+    const { icon, items, size, direction, ariaLabel, disabled } = this.props;
+    const { expanded }   = this.state;
     const directionClass = (direction === 'left') ? 'dropdown__left' : 'dropdown__right';
+    const iconStyle      = { fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` };
+    const iconClassname  = `fa fa-fw fa-${icon} dropdown__icon`;
+
+    if (disabled) {
+      return (
+        <div className='icon-button disabled' style={iconStyle} aria-label={ariaLabel}>
+          <i className={iconClassname} aria-hidden />
+        </div>
+      );
+    }
 
     const dropdownItems = expanded && (
       <ul className='dropdown__content-list'>
@@ -80,8 +91,8 @@ export default class DropdownMenu extends React.PureComponent {
 
     return (
       <Dropdown ref={this.setRef} onShow={this.handleShow} onHide={this.handleHide}>
-        <DropdownTrigger className='icon-button' style={{ fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }} aria-label={ariaLabel}>
-          <i className={`fa fa-fw fa-${icon} dropdown__icon`}  aria-hidden />
+        <DropdownTrigger className='icon-button' style={iconStyle} aria-label={ariaLabel}>
+          <i className={iconClassname} aria-hidden />
         </DropdownTrigger>
 
         <DropdownContent className={directionClass}>
diff --git a/app/javascript/mastodon/components/permalink.js b/app/javascript/mastodon/components/permalink.js
index 0b7d0a65a..d726d37a2 100644
--- a/app/javascript/mastodon/components/permalink.js
+++ b/app/javascript/mastodon/components/permalink.js
@@ -15,7 +15,7 @@ export default class Permalink extends React.PureComponent {
   };
 
   handleClick = (e) => {
-    if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
+    if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
       e.preventDefault();
       this.context.router.history.push(this.props.to);
     }
@@ -25,7 +25,7 @@ export default class Permalink extends React.PureComponent {
     const { href, children, className, ...other } = this.props;
 
     return (
-      <a href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
+      <a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
         {children}
       </a>
     );
diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js
index 398f7d243..8287375c4 100644
--- a/app/javascript/mastodon/containers/mastodon.js
+++ b/app/javascript/mastodon/containers/mastodon.js
@@ -22,14 +22,15 @@ import { getLocale } from '../locales';
 const { localeData, messages } = getLocale();
 addLocaleData(localeData);
 
-const store = configureStore();
+export const store = configureStore();
 const initialState = JSON.parse(document.getElementById('initial-state').textContent);
 try {
   initialState.local_settings = JSON.parse(localStorage.getItem('mastodon-settings'));
 } catch (e) {
   initialState.local_settings = {};
 }
-store.dispatch(hydrateStore(initialState));
+const hydrateAction = hydrateStore(initialState);
+store.dispatch(hydrateAction);
 
 export default class Mastodon extends React.PureComponent {
 
diff --git a/app/javascript/mastodon/containers/timeline_container.js b/app/javascript/mastodon/containers/timeline_container.js
new file mode 100644
index 000000000..6b545ef09
--- /dev/null
+++ b/app/javascript/mastodon/containers/timeline_container.js
@@ -0,0 +1,39 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import PropTypes from 'prop-types';
+import configureStore from '../store/configureStore';
+import { hydrateStore } from '../actions/store';
+import { IntlProvider, addLocaleData } from 'react-intl';
+import { getLocale } from '../locales';
+import PublicTimeline from '../features/standalone/public_timeline';
+
+const { localeData, messages } = getLocale();
+addLocaleData(localeData);
+
+const store = configureStore();
+const initialStateContainer = document.getElementById('initial-state');
+
+if (initialStateContainer !== null) {
+  const initialState = JSON.parse(initialStateContainer.textContent);
+  store.dispatch(hydrateStore(initialState));
+}
+
+export default class TimelineContainer extends React.PureComponent {
+
+  static propTypes = {
+    locale: PropTypes.string.isRequired,
+  };
+
+  render () {
+    const { locale } = this.props;
+
+    return (
+      <IntlProvider locale={locale} messages={messages}>
+        <Provider store={store}>
+          <PublicTimeline />
+        </Provider>
+      </IntlProvider>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/emoji.js b/app/javascript/mastodon/emoji.js
index 01d01fb72..7043d5f3a 100644
--- a/app/javascript/mastodon/emoji.js
+++ b/app/javascript/mastodon/emoji.js
@@ -1,35 +1,55 @@
 import emojione from 'emojione';
-
-const toImage = str => shortnameToImage(unicodeToImage(str));
-
-const unicodeToImage = str => {
-  const mappedUnicode = emojione.mapUnicodeToShort();
-
-  return str.replace(emojione.regUnicode, unicodeChar => {
-    if (typeof unicodeChar === 'undefined' || unicodeChar === '' || !(unicodeChar in emojione.jsEscapeMap)) {
-      return unicodeChar;
+import Trie from 'substring-trie';
+
+const mappedUnicode = emojione.mapUnicodeToShort();
+const trie = new Trie(Object.keys(emojione.jsEscapeMap));
+
+function emojify(str) {
+  // This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.)
+  // and replacing valid shortnames like :smile: and :wink: as well as unicode strings
+  // that _aren't_ within tags with an <img> version.
+  // The goal is to be the same as an emojione.regShortNames/regUnicode replacement, but faster.
+  let i = -1;
+  let insideTag = false;
+  let insideShortname = false;
+  let shortnameStartIndex = -1;
+  let match;
+  while (++i < str.length) {
+    const char = str.charAt(i);
+    if (insideShortname && char === ':') {
+      const shortname = str.substring(shortnameStartIndex, i + 1);
+      if (shortname in emojione.emojioneList) {
+        const unicode = emojione.emojioneList[shortname].unicode[emojione.emojioneList[shortname].unicode.length - 1];
+        const alt = emojione.convert(unicode.toUpperCase());
+        const replacement = `<img draggable="false" class="emojione" alt="${alt}" title="${shortname}" src="/emoji/${unicode}.svg" />`;
+        str = str.substring(0, shortnameStartIndex) + replacement + str.substring(i + 1);
+        i += (replacement.length - shortname.length - 1); // jump ahead the length we've added to the string
+      } else {
+        i--; // stray colon, try again
+      }
+      insideShortname = false;
+    } else if (insideTag && char === '>') {
+      insideTag = false;
+    } else if (char === '<') {
+      insideTag = true;
+      insideShortname = false;
+    } else if (!insideTag && char === ':') {
+      insideShortname = true;
+      shortnameStartIndex = i;
+    } else if (!insideTag && (match = trie.search(str.substring(i)))) {
+      const unicodeStr = match;
+      if (unicodeStr in emojione.jsEscapeMap) {
+        const unicode  = emojione.jsEscapeMap[unicodeStr];
+        const short    = mappedUnicode[unicode];
+        const filename = emojione.emojioneList[short].fname;
+        const alt      = emojione.convert(unicode.toUpperCase());
+        const replacement =  `<img draggable="false" class="emojione" alt="${alt}" title="${short}" src="/emoji/${filename}.svg" />`;
+        str = str.substring(0, i) + replacement + str.substring(i + unicodeStr.length);
+        i += (replacement.length - unicodeStr.length); // jump ahead the length we've added to the string
+      }
     }
-
-    const unicode  = emojione.jsEscapeMap[unicodeChar];
-    const short    = mappedUnicode[unicode];
-    const filename = emojione.emojioneList[short].fname;
-    const alt      = emojione.convert(unicode.toUpperCase());
-
-    return `<img draggable="false" class="emojione" alt="${alt}" title="${short}" src="/emoji/${filename}.svg" />`;
-  });
-};
-
-const shortnameToImage = str => str.replace(emojione.regShortNames, shortname => {
-  if (typeof shortname === 'undefined' || shortname === '' || !(shortname in emojione.emojioneList)) {
-    return shortname;
   }
+  return str;
+}
 
-  const unicode = emojione.emojioneList[shortname].unicode[emojione.emojioneList[shortname].unicode.length - 1];
-  const alt     = emojione.convert(unicode.toUpperCase());
-
-  return `<img draggable="false" class="emojione" alt="${alt}" title="${shortname}" src="/emoji/${unicode}.svg" />`;
-});
-
-export default function emojify(text) {
-  return toImage(text);
-};
+export default emojify;
diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js
index 955d0000e..3c8b63114 100644
--- a/app/javascript/mastodon/features/account_timeline/index.js
+++ b/app/javascript/mastodon/features/account_timeline/index.js
@@ -9,11 +9,11 @@ import LoadingIndicator from '../../components/loading_indicator';
 import Column from '../ui/components/column';
 import HeaderContainer from './containers/header_container';
 import ColumnBackButton from '../../components/column_back_button';
-import Immutable from 'immutable';
+import { List as ImmutableList } from 'immutable';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 
 const mapStateToProps = (state, props) => ({
-  statusIds: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'items'], Immutable.List()),
+  statusIds: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'items'], ImmutableList()),
   isLoading: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'isLoading']),
   hasMore: !!state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'next']),
   me: state.getIn(['meta', 'me']),
diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
index c83dbb63e..83c66a5d5 100644
--- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
@@ -2,6 +2,7 @@ import React from 'react';
 import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
 import PropTypes from 'prop-types';
 import { defineMessages, injectIntl } from 'react-intl';
+import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
 
 const messages = defineMessages({
   emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
@@ -50,7 +51,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
     this.setState({ active: true });
     if (!EmojiPicker) {
       this.setState({ loading: true });
-      import(/* webpackChunkName: "emojione_picker" */ 'emojione-picker').then(TheEmojiPicker => {
+      EmojiPickerAsync().then(TheEmojiPicker => {
         EmojiPicker = TheEmojiPicker.default;
         this.setState({ loading: false });
       }).catch(() => {
diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js
index 3ec205f2c..69bead689 100644
--- a/app/javascript/mastodon/features/compose/index.js
+++ b/app/javascript/mastodon/features/compose/index.js
@@ -2,6 +2,7 @@ import React from 'react';
 import ComposeFormContainer from './containers/compose_form_container';
 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 '../../actions/compose';
 import { openModal } from '../../actions/modal';
@@ -15,6 +16,8 @@ import SearchResultsContainer from './containers/search_results_container';
 
 const messages = defineMessages({
   start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
+  home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
+  notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
   public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
   community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
   settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
@@ -22,6 +25,7 @@ const messages = defineMessages({
 });
 
 const mapStateToProps = state => ({
+  columns: state.getIn(['settings', 'columns']),
   showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
 });
 
@@ -31,6 +35,7 @@ export default class Compose extends React.PureComponent {
 
   static propTypes = {
     dispatch: PropTypes.func.isRequired,
+    columns: ImmutablePropTypes.list.isRequired,
     multiColumn: PropTypes.bool,
     showSearch: PropTypes.bool,
     intl: PropTypes.object.isRequired,
@@ -60,11 +65,22 @@ export default class Compose extends React.PureComponent {
     let header = '';
 
     if (multiColumn) {
+      const { columns } = this.props;
       header = (
         <div className='drawer__header'>
           <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)}><i role='img' aria-label={intl.formatMessage(messages.start)} className='fa fa-fw fa-asterisk' /></Link>
-          <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)}><i role='img' aria-label={intl.formatMessage(messages.community)} className='fa fa-fw fa-users' /></Link>
-          <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)}><i role='img' aria-label={intl.formatMessage(messages.public)} className='fa fa-fw fa-globe' /></Link>
+          {!columns.some(column => column.get('id') === 'HOME') && (
+            <Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)}><i role='img' className='fa fa-fw fa-home' aria-label={intl.formatMessage(messages.home_timeline)} /></Link>
+          )}
+          {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && (
+            <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)}><i role='img' className='fa fa-fw fa-bell' aria-label={intl.formatMessage(messages.notifications)} /></Link>
+          )}
+          {!columns.some(column => column.get('id') === 'COMMUNITY') && (
+            <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)}><i role='img' aria-label={intl.formatMessage(messages.community)} className='fa fa-fw fa-users' /></Link>
+          )}
+          {!columns.some(column => column.get('id') === 'PUBLIC') && (
+            <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)}><i role='img' aria-label={intl.formatMessage(messages.public)} className='fa fa-fw fa-globe' /></Link>
+          )}
           <a onClick={this.openSettings} role='button' tabIndex='0' className='drawer__tab' title={intl.formatMessage(messages.settings)}><i role='img' aria-label={intl.formatMessage(messages.settings)} className='fa fa-fw fa-cogs' /></a>
           <a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)}><i role='img' aria-label={intl.formatMessage(messages.logout)} className='fa fa-fw fa-sign-out' /></a>
         </div>
diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js
index d4e1555b2..fb8f0d3cc 100644
--- a/app/javascript/mastodon/features/notifications/index.js
+++ b/app/javascript/mastodon/features/notifications/index.js
@@ -11,7 +11,7 @@ import { ScrollContainer } from 'react-router-scroll';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import ColumnSettingsContainer from './containers/column_settings_container';
 import { createSelector } from 'reselect';
-import Immutable from 'immutable';
+import { List as ImmutableList } from 'immutable';
 import LoadMore from '../../components/load_more';
 import { debounce } from 'lodash';
 
@@ -20,7 +20,7 @@ const messages = defineMessages({
 });
 
 const getNotifications = createSelector([
-  state => Immutable.List(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()),
+  state => ImmutableList(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()),
   state => state.getIn(['notifications', 'items']),
 ], (excludedTypes, notifications) => notifications.filterNot(item => excludedTypes.includes(item.get('type'))));
 
@@ -122,7 +122,7 @@ export default class Notifications extends React.PureComponent {
     let unread         = '';
     let scrollContainer = '';
 
-    if (!isLoading && notifications.size > 0 && hasMore) {
+    if (!isLoading && hasMore) {
       loadMore = <LoadMore onClick={this.handleLoadMore} />;
     }
 
@@ -132,7 +132,7 @@ export default class Notifications extends React.PureComponent {
 
     if (isLoading && this.scrollableArea) {
       scrollableArea = this.scrollableArea;
-    } else if (notifications.size > 0) {
+    } else if (notifications.size > 0 || hasMore) {
       scrollableArea = (
         <div className='scrollable' onScroll={this.handleScroll} ref={this.setRef}>
           {unread}
diff --git a/app/javascript/mastodon/features/report/containers/status_check_box_container.js b/app/javascript/mastodon/features/report/containers/status_check_box_container.js
index 8997718a2..48cd0319b 100644
--- a/app/javascript/mastodon/features/report/containers/status_check_box_container.js
+++ b/app/javascript/mastodon/features/report/containers/status_check_box_container.js
@@ -1,11 +1,11 @@
 import { connect } from 'react-redux';
 import StatusCheckBox from '../components/status_check_box';
 import { toggleStatusReport } from '../../../actions/reports';
-import Immutable from 'immutable';
+import { Set as ImmutableSet } from 'immutable';
 
 const mapStateToProps = (state, { id }) => ({
   status: state.getIn(['statuses', id]),
-  checked: state.getIn(['reports', 'new', 'status_ids'], Immutable.Set()).includes(id),
+  checked: state.getIn(['reports', 'new', 'status_ids'], ImmutableSet()).includes(id),
 });
 
 const mapDispatchToProps = (dispatch, { id }) => ({
diff --git a/app/javascript/mastodon/features/standalone/public_timeline/index.js b/app/javascript/mastodon/features/standalone/public_timeline/index.js
new file mode 100644
index 000000000..de4b5320a
--- /dev/null
+++ b/app/javascript/mastodon/features/standalone/public_timeline/index.js
@@ -0,0 +1,76 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import StatusListContainer from '../../ui/containers/status_list_container';
+import {
+  refreshPublicTimeline,
+  expandPublicTimeline,
+} from '../../../actions/timelines';
+import Column from '../../../components/column';
+import ColumnHeader from '../../../components/column_header';
+import { defineMessages, injectIntl } from 'react-intl';
+
+const messages = defineMessages({
+  title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' },
+});
+
+@connect()
+@injectIntl
+export default class PublicTimeline extends React.PureComponent {
+
+  static propTypes = {
+    dispatch: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleHeaderClick = () => {
+    this.column.scrollTop();
+  }
+
+  setRef = c => {
+    this.column = c;
+  }
+
+  componentDidMount () {
+    const { dispatch } = this.props;
+
+    dispatch(refreshPublicTimeline());
+
+    this.polling = setInterval(() => {
+      dispatch(refreshPublicTimeline());
+    }, 3000);
+  }
+
+  componentWillUnmount () {
+    if (typeof this.polling !== 'undefined') {
+      clearInterval(this.polling);
+      this.polling = null;
+    }
+  }
+
+  handleLoadMore = () => {
+    this.props.dispatch(expandPublicTimeline());
+  }
+
+  render () {
+    const { intl } = this.props;
+
+    return (
+      <Column ref={this.setRef}>
+        <ColumnHeader
+          icon='globe'
+          title={intl.formatMessage(messages.title)}
+          onClick={this.handleHeaderClick}
+        />
+
+        <StatusListContainer
+          timelineId='public'
+          loadMore={this.handleLoadMore}
+          scrollKey='standalone_public_timeline'
+          trackScroll={false}
+        />
+      </Column>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/ui/components/bundle.js b/app/javascript/mastodon/features/ui/components/bundle.js
new file mode 100644
index 000000000..72798f690
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/bundle.js
@@ -0,0 +1,101 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const emptyComponent = () => null;
+const noop = () => { };
+
+class Bundle extends React.Component {
+
+  static propTypes = {
+    fetchComponent: PropTypes.func.isRequired,
+    loading: PropTypes.func,
+    error: PropTypes.func,
+    children: PropTypes.func.isRequired,
+    renderDelay: PropTypes.number,
+    onFetch: PropTypes.func,
+    onFetchSuccess: PropTypes.func,
+    onFetchFail: PropTypes.func,
+  }
+
+  static defaultProps = {
+    loading: emptyComponent,
+    error: emptyComponent,
+    renderDelay: 0,
+    onFetch: noop,
+    onFetchSuccess: noop,
+    onFetchFail: noop,
+  }
+
+  static cache = {}
+
+  state = {
+    mod: undefined,
+    forceRender: false,
+  }
+
+  componentWillMount() {
+    this.load(this.props);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.fetchComponent !== this.props.fetchComponent) {
+      this.load(nextProps);
+    }
+  }
+
+  componentWillUnmount () {
+    if (this.timeout) {
+      clearTimeout(this.timeout);
+    }
+  }
+
+  load = (props) => {
+    const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props;
+
+    this.setState({ mod: undefined });
+    onFetch();
+
+    if (renderDelay !== 0) {
+      this.timestamp = new Date();
+      this.timeout = setTimeout(() => this.setState({ forceRender: true }), renderDelay);
+    }
+
+    if (Bundle.cache[fetchComponent.name]) {
+      const mod = Bundle.cache[fetchComponent.name];
+
+      this.setState({ mod: mod.default });
+      onFetchSuccess();
+      return Promise.resolve();
+    }
+
+    return fetchComponent()
+      .then((mod) => {
+        Bundle.cache[fetchComponent.name] = mod;
+        this.setState({ mod: mod.default });
+        onFetchSuccess();
+      })
+      .catch((error) => {
+        this.setState({ mod: null });
+        onFetchFail(error);
+      });
+  }
+
+  render() {
+    const { loading: Loading, error: Error, children, renderDelay } = this.props;
+    const { mod, forceRender } = this.state;
+    const elapsed = this.timestamp ? (new Date() - this.timestamp) : renderDelay;
+
+    if (mod === undefined) {
+      return (elapsed >= renderDelay || forceRender) ? <Loading /> : null;
+    }
+
+    if (mod === null) {
+      return <Error onRetry={this.load} />;
+    }
+
+    return children(mod);
+  }
+
+}
+
+export default Bundle;
diff --git a/app/javascript/mastodon/features/ui/components/bundle_column_error.js b/app/javascript/mastodon/features/ui/components/bundle_column_error.js
new file mode 100644
index 000000000..cd124746a
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/bundle_column_error.js
@@ -0,0 +1,44 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl } from 'react-intl';
+
+import Column from './column';
+import ColumnHeader from './column_header';
+import ColumnBackButtonSlim from '../../../components/column_back_button_slim';
+import IconButton from '../../../components/icon_button';
+
+const messages = defineMessages({
+  title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' },
+  body: { id: 'bundle_column_error.body', defaultMessage: 'Something went wrong while loading this component.' },
+  retry: { id: 'bundle_column_error.retry', defaultMessage: 'Try again' },
+});
+
+class BundleColumnError extends React.Component {
+
+  static propTypes = {
+    onRetry: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  }
+
+  handleRetry = () => {
+    this.props.onRetry();
+  }
+
+  render () {
+    const { intl: { formatMessage } } = this.props;
+
+    return (
+      <Column>
+        <ColumnHeader icon='exclamation-circle' type={formatMessage(messages.title)} />
+        <ColumnBackButtonSlim />
+        <div className='error-column'>
+          <IconButton title={formatMessage(messages.retry)} icon='refresh' onClick={this.handleRetry} size={64} />
+          {formatMessage(messages.body)}
+        </div>
+      </Column>
+    );
+  }
+
+}
+
+export default injectIntl(BundleColumnError);
diff --git a/app/javascript/mastodon/features/ui/components/bundle_modal_error.js b/app/javascript/mastodon/features/ui/components/bundle_modal_error.js
new file mode 100644
index 000000000..928bfe1f7
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/bundle_modal_error.js
@@ -0,0 +1,53 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { defineMessages, injectIntl } from 'react-intl';
+
+import IconButton from '../../../components/icon_button';
+
+const messages = defineMessages({
+  error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this component.' },
+  retry: { id: 'bundle_modal_error.retry', defaultMessage: 'Try again' },
+  close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' },
+});
+
+class BundleModalError extends React.Component {
+
+  static propTypes = {
+    onRetry: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  }
+
+  handleRetry = () => {
+    this.props.onRetry();
+  }
+
+  render () {
+    const { onClose, intl: { formatMessage } } = this.props;
+
+    // Keep the markup in sync with <ModalLoading />
+    // (make sure they have the same dimensions)
+    return (
+      <div className='modal-root__modal error-modal'>
+        <div className='error-modal__body'>
+          <IconButton title={formatMessage(messages.retry)} icon='refresh' onClick={this.handleRetry} size={64} />
+          {formatMessage(messages.error)}
+        </div>
+
+        <div className='error-modal__footer'>
+          <div>
+            <button
+              onClick={onClose}
+              className='error-modal__nav onboarding-modal__skip'
+            >
+              {formatMessage(messages.close)}
+            </button>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+export default injectIntl(BundleModalError);
diff --git a/app/javascript/mastodon/features/ui/components/column_loading.js b/app/javascript/mastodon/features/ui/components/column_loading.js
new file mode 100644
index 000000000..7ecfaf77a
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/column_loading.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import Column from '../../../components/column';
+import ColumnHeader from '../../../components/column_header';
+
+const ColumnLoading = ({ title = '', icon = ' ' }) => (
+  <Column>
+    <ColumnHeader icon={icon} title={title} multiColumn={false} />
+    <div className='scrollable' />
+  </Column>
+);
+
+ColumnLoading.propTypes = {
+  title: PropTypes.node,
+  icon: PropTypes.string,
+};
+
+export default ColumnLoading;
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index 3c3e9425d..cbc185a7d 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -2,14 +2,14 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ImmutablePureComponent from 'react-immutable-pure-component';
-import ReactSwipeable from 'react-swipeable';
-import HomeTimeline from '../../home_timeline';
-import Notifications from '../../notifications';
-import PublicTimeline from '../../public_timeline';
-import CommunityTimeline from '../../community_timeline';
-import HashtagTimeline from '../../hashtag_timeline';
-import Compose from '../../compose';
-import { getPreviousLink, getNextLink } from './tabs_bar';
+
+import ReactSwipeableViews from 'react-swipeable-views';
+import { links, getIndex, getLink } from './tabs_bar';
+
+import BundleContainer from '../containers/bundle_container';
+import ColumnLoading from './column_loading';
+import BundleColumnError from './bundle_column_error';
+import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline } from '../../ui/util/async-components';
 
 const componentMap = {
   'COMPOSE': Compose,
@@ -32,39 +32,61 @@ export default class ColumnsArea extends ImmutablePureComponent {
     children: PropTypes.node,
   };
 
-  handleRightSwipe = () => {
-    const previousLink = getPreviousLink(this.context.router.history.location.pathname);
+  handleSwipe = (index) => {
+    window.requestAnimationFrame(() => {
+      window.requestAnimationFrame(() => {
+        this.context.router.history.push(getLink(index));
+      });
+    });
+  }
 
-    if (previousLink) {
-      this.context.router.history.push(previousLink);
-    }
+  renderView = (link, index) => {
+    const columnIndex = getIndex(this.context.router.history.location.pathname);
+    const title = link.props.children[1] && React.cloneElement(link.props.children[1]);
+    const icon = (link.props.children[0] || link.props.children).props.className.split(' ')[2].split('-')[1];
+
+    const view = (index === columnIndex) ?
+      React.cloneElement(this.props.children) :
+      <ColumnLoading title={title} icon={icon} />;
+
+    return (
+      <div className='columns-area' key={index}>
+        {view}
+      </div>
+    );
   }
 
-  handleLeftSwipe = () => {
-    const previousLink = getNextLink(this.context.router.history.location.pathname);
+  renderLoading = () => {
+    return <ColumnLoading />;
+  }
 
-    if (previousLink) {
-      this.context.router.history.push(previousLink);
-    }
-  };
+  renderError = (props) => {
+    return <BundleColumnError {...props} />;
+  }
 
   render () {
     const { columns, children, singleColumn } = this.props;
 
+    const columnIndex = getIndex(this.context.router.history.location.pathname);
+
     if (singleColumn) {
-      return (
-        <ReactSwipeable onSwipedLeft={this.handleLeftSwipe} onSwipedRight={this.handleRightSwipe} className='columns-area'>
-          {children}
-        </ReactSwipeable>
-      );
+      return columnIndex !== -1 ? (
+        <ReactSwipeableViews index={columnIndex} onChangeIndex={this.handleSwipe} animateTransitions={false} style={{ height: '100%' }}>
+          {links.map(this.renderView)}
+        </ReactSwipeableViews>
+      ) : <div className='columns-area'>{children}</div>;
     }
 
     return (
       <div className='columns-area'>
         {columns.map(column => {
-          const SpecificComponent = componentMap[column.get('id')];
           const params = column.get('params', null) === null ? null : column.get('params').toJS();
-          return <SpecificComponent key={column.get('uuid')} columnId={column.get('uuid')} params={params} multiColumn />;
+
+          return (
+            <BundleContainer key={column.get('uuid')} fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading} error={this.renderError}>
+              {SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn />}
+            </BundleContainer>
+          );
         })}
 
         {React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))}
diff --git a/app/javascript/mastodon/features/ui/components/image_loader.js b/app/javascript/mastodon/features/ui/components/image_loader.js
index 52c3a898b..aad594380 100644
--- a/app/javascript/mastodon/features/ui/components/image_loader.js
+++ b/app/javascript/mastodon/features/ui/components/image_loader.js
@@ -8,12 +8,14 @@ export default class ImageLoader extends React.PureComponent {
     alt: PropTypes.string,
     src: PropTypes.string.isRequired,
     previewSrc: PropTypes.string.isRequired,
-    width: PropTypes.number.isRequired,
-    height: PropTypes.number.isRequired,
+    width: PropTypes.number,
+    height: PropTypes.number,
   }
 
   static defaultProps = {
     alt: '',
+    width: null,
+    height: null,
   };
 
   state = {
@@ -46,8 +48,8 @@ export default class ImageLoader extends React.PureComponent {
     this.setState({ loading: true, error: false });
     Promise.all([
       this.loadPreviewCanvas(props),
-      this.loadOriginalImage(props),
-    ])
+      this.hasSize() && this.loadOriginalImage(props),
+    ].filter(Boolean))
       .then(() => {
         this.setState({ loading: false, error: false });
         this.clearPreviewCanvas();
@@ -106,6 +108,11 @@ export default class ImageLoader extends React.PureComponent {
     this.removers = [];
   }
 
+  hasSize () {
+    const { width, height } = this.props;
+    return typeof width === 'number' && typeof height === 'number';
+  }
+
   setCanvasRef = c => {
     this.canvas = c;
   }
@@ -116,6 +123,7 @@ export default class ImageLoader extends React.PureComponent {
 
     const className = classNames('image-loader', {
       'image-loader--loading': loading,
+      'image-loader--amorphous': !this.hasSize(),
     });
 
     return (
@@ -125,6 +133,7 @@ export default class ImageLoader extends React.PureComponent {
           width={width}
           height={height}
           ref={this.setCanvasRef}
+          style={{ opacity: loading ? 1 : 0 }}
         />
 
         {!loading && (
diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js
index 8bb81ca01..d869fffa6 100644
--- a/app/javascript/mastodon/features/ui/components/media_modal.js
+++ b/app/javascript/mastodon/features/ui/components/media_modal.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import ReactSwipeable from 'react-swipeable';
+import ReactSwipeableViews from 'react-swipeable-views';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import ExtendedVideoPlayer from '../../../components/extended_video_player';
@@ -26,12 +26,16 @@ export default class MediaModal extends ImmutablePureComponent {
     index: null,
   };
 
+  handleSwipe = (index) => {
+    this.setState({ index: (index) % this.props.media.size });
+  }
+
   handleNextClick = () => {
     this.setState({ index: (this.getIndex() + 1) % this.props.media.size });
   }
 
   handlePrevClick = () => {
-    this.setState({ index: (this.getIndex() - 1) % this.props.media.size });
+    this.setState({ index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size });
   }
 
   handleKeyUp = (e) => {
@@ -74,7 +78,12 @@ export default class MediaModal extends ImmutablePureComponent {
     }
 
     if (attachment.get('type') === 'image') {
-      content = <ImageLoader previewSrc={attachment.get('preview_url')} src={url} width={attachment.getIn(['meta', 'original', 'width'])} height={attachment.getIn(['meta', 'original', 'height'])} />;
+      content = media.map((image) => {
+        const width  = image.getIn(['meta', 'original', 'width']) || null;
+        const height = image.getIn(['meta', 'original', 'height']) || null;
+
+        return <ImageLoader previewSrc={image.get('preview_url')} src={image.get('url')} width={width} height={height} key={image.get('preview_url')} />;
+      }).toArray();
     } else if (attachment.get('type') === 'gifv') {
       content = <ExtendedVideoPlayer src={url} muted controls={false} />;
     }
@@ -85,9 +94,9 @@ export default class MediaModal extends ImmutablePureComponent {
 
         <div className='media-modal__content'>
           <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} />
-          <ReactSwipeable onSwipedRight={this.handlePrevClick} onSwipedLeft={this.handleNextClick}>
+          <ReactSwipeableViews onChangeIndex={this.handleSwipe} index={index} animateHeight>
             {content}
-          </ReactSwipeable>
+          </ReactSwipeableViews>
         </div>
 
         {rightNav}
diff --git a/app/javascript/mastodon/features/ui/components/modal_loading.js b/app/javascript/mastodon/features/ui/components/modal_loading.js
new file mode 100644
index 000000000..f403ca4c9
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/modal_loading.js
@@ -0,0 +1,20 @@
+import React from 'react';
+
+import LoadingIndicator from '../../../components/loading_indicator';
+
+// Keep the markup in sync with <BundleModalError />
+// (make sure they have the same dimensions)
+const ModalLoading = () => (
+  <div className='modal-root__modal error-modal'>
+    <div className='error-modal__body'>
+      <LoadingIndicator />
+    </div>
+    <div className='error-modal__footer'>
+      <div>
+        <button className='error-modal__nav onboarding-modal__skip' />
+      </div>
+    </div>
+  </div>
+);
+
+export default ModalLoading;
diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js
index 3777c1bf6..de4f44ce6 100644
--- a/app/javascript/mastodon/features/ui/components/modal_root.js
+++ b/app/javascript/mastodon/features/ui/components/modal_root.js
@@ -1,14 +1,19 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import MediaModal from './media_modal';
-import OnboardingModal from './onboarding_modal';
-import VideoModal from './video_modal';
-import BoostModal from './boost_modal';
-import ConfirmationModal from './confirmation_modal';
-import ReportModal from './report_modal';
-import SettingsContainer from '../../../../glitch/containers/settings';
 import TransitionMotion from 'react-motion/lib/TransitionMotion';
 import spring from 'react-motion/lib/spring';
+import BundleContainer from '../containers/bundle_container';
+import BundleModalError from './bundle_modal_error';
+import ModalLoading from './modal_loading';
+import {
+  MediaModal,
+  OnboardingModal,
+  VideoModal,
+  BoostModal,
+  ConfirmationModal,
+  ReportModal,
+  SettingsModal,
+} from '../../../features/ui/util/async-components';
 
 const MODAL_COMPONENTS = {
   'MEDIA': MediaModal,
@@ -17,7 +22,7 @@ const MODAL_COMPONENTS = {
   'BOOST': BoostModal,
   'CONFIRM': ConfirmationModal,
   'REPORT': ReportModal,
-  'SETTINGS': SettingsContainer,
+  'SETTINGS': SettingsModal,
 };
 
 export default class ModalRoot extends React.PureComponent {
@@ -51,6 +56,22 @@ export default class ModalRoot extends React.PureComponent {
     return { opacity: spring(0), scale: spring(0.98) };
   }
 
+  renderModal = (SpecificComponent) => {
+    const { props, onClose } = this.props;
+
+    return <SpecificComponent {...props} onClose={onClose} />;
+  }
+
+  renderLoading = () => {
+    return <ModalLoading />;
+  }
+
+  renderError = (props) => {
+    const { onClose } = this.props;
+
+    return <BundleModalError {...props} onClose={onClose} />;
+  }
+
   render () {
     const { type, props, onClose } = this.props;
     const visible = !!type;
@@ -72,18 +93,14 @@ export default class ModalRoot extends React.PureComponent {
       >
         {interpolatedStyles =>
           <div className='modal-root'>
-            {interpolatedStyles.map(({ key, data: { type, props }, style }) => {
-              const SpecificComponent = MODAL_COMPONENTS[type];
-
-              return (
-                <div key={key} style={{ pointerEvents: visible ? 'auto' : 'none' }}>
-                  <div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity }} onClick={onClose} />
-                  <div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
-                    <SpecificComponent {...props} onClose={onClose} />
-                  </div>
+            {interpolatedStyles.map(({ key, data: { type }, style }) => (
+              <div key={key} style={{ pointerEvents: visible ? 'auto' : 'none' }}>
+                <div role='presentation' className='modal-root__overlay' style={{ opacity: style.opacity }} onClick={onClose} />
+                <div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}>
+                  <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading} error={this.renderError} renderDelay={200}>{this.renderModal}</BundleContainer>
                 </div>
-              );
-            })}
+              </div>
+            ))}
           </div>
         }
       </TransitionMotion>
diff --git a/app/javascript/mastodon/features/ui/components/onboarding_modal.js b/app/javascript/mastodon/features/ui/components/onboarding_modal.js
index dab5e47ea..1b1cb00da 100644
--- a/app/javascript/mastodon/features/ui/components/onboarding_modal.js
+++ b/app/javascript/mastodon/features/ui/components/onboarding_modal.js
@@ -3,16 +3,14 @@ import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import ReactSwipeable from 'react-swipeable';
+import ReactSwipeableViews from 'react-swipeable-views';
 import classNames from 'classnames';
 import Permalink from '../../../components/permalink';
-import TransitionMotion from 'react-motion/lib/TransitionMotion';
-import spring from 'react-motion/lib/spring';
 import ComposeForm from '../../compose/components/compose_form';
 import Search from '../../compose/components/search';
 import NavigationBar from '../../compose/components/navigation_bar';
 import ColumnHeader from './column_header';
-import Immutable from 'immutable';
+import { List as ImmutableList } from 'immutable';
 
 const noop = () => { };
 
@@ -50,7 +48,7 @@ const PageTwo = ({ me }) => (
       </div>
       <ComposeForm
         text='Awoo! #introductions'
-        suggestions={Immutable.List()}
+        suggestions={ImmutableList()}
         mentionedDomains={[]}
         spoiler={false}
         onChange={noop}
@@ -227,6 +225,10 @@ export default class OnboardingModal extends React.PureComponent {
     }));
   }
 
+  handleSwipe = (index) => {
+    this.setState({ currentIndex: index });
+  }
+
   handleKeyUp = ({ key }) => {
     switch (key) {
     case 'ArrowLeft':
@@ -263,30 +265,18 @@ export default class OnboardingModal extends React.PureComponent {
       </button>
     );
 
-    const styles = pages.map((data, i) => ({
-      key: `page-${i}`,
-      data,
-      style: {
-        opacity: spring(i === currentIndex ? 1 : 0),
-      },
-    }));
-
     return (
       <div className='modal-root__modal onboarding-modal'>
-        <TransitionMotion styles={styles}>
-          {interpolatedStyles => (
-            <ReactSwipeable onSwipedRight={this.handlePrev} onSwipedLeft={this.handleNext} className='onboarding-modal__pager'>
-              {interpolatedStyles.map(({ key, data, style }, i) => {
-                const className = classNames('onboarding-modal__page__wrapper', {
-                  'onboarding-modal__page__wrapper--active': i === currentIndex,
-                });
-                return (
-                  <div key={key} style={style} className={className}>{data}</div>
-                );
-              })}
-            </ReactSwipeable>
-          )}
-        </TransitionMotion>
+        <ReactSwipeableViews index={currentIndex} onChangeIndex={this.handleSwipe} className='onboarding-modal__pager'>
+          {pages.map((page, i) => {
+            const className = classNames('onboarding-modal__page__wrapper', {
+              'onboarding-modal__page__wrapper--active': i === currentIndex,
+            });
+            return (
+              <div key={i} className={className}>{page}</div>
+            );
+          })}
+        </ReactSwipeableViews>
 
         <div className='onboarding-modal__paginator'>
           <div>
diff --git a/app/javascript/mastodon/features/ui/components/report_modal.js b/app/javascript/mastodon/features/ui/components/report_modal.js
index c989d2c9b..b5dfa422e 100644
--- a/app/javascript/mastodon/features/ui/components/report_modal.js
+++ b/app/javascript/mastodon/features/ui/components/report_modal.js
@@ -7,7 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import { makeGetAccount } from '../../../selectors';
 import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
 import StatusCheckBox from '../../report/containers/status_check_box_container';
-import Immutable from 'immutable';
+import { OrderedSet } from 'immutable';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import Button from '../../../components/button';
 
@@ -26,7 +26,7 @@ const makeMapStateToProps = () => {
       isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
       account: getAccount(state, accountId),
       comment: state.getIn(['reports', 'new', 'comment']),
-      statusIds: Immutable.OrderedSet(state.getIn(['timelines', `account:${accountId}`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])),
+      statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])),
     };
   };
 
diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js
index c2e6c88b5..b4153ff45 100644
--- a/app/javascript/mastodon/features/ui/components/tabs_bar.js
+++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js
@@ -2,7 +2,7 @@ import React from 'react';
 import NavLink from 'react-router-dom/NavLink';
 import { FormattedMessage } from 'react-intl';
 
-const links = [
+export const links = [
   <NavLink className='tabs-bar__link primary' activeClassName='active' to='/statuses/new'><i className='fa fa-fw fa-pencil' /><FormattedMessage id='tabs_bar.compose' defaultMessage='Compose' /></NavLink>,
   <NavLink className='tabs-bar__link primary' activeClassName='active' to='/timelines/home'><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
   <NavLink className='tabs-bar__link primary' activeClassName='active' to='/notifications'><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
@@ -13,25 +13,13 @@ const links = [
   <NavLink className='tabs-bar__link primary' activeClassName='active' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started'><i className='fa fa-fw fa-asterisk' /></NavLink>,
 ];
 
-export function getPreviousLink (path) {
-  const index = links.findIndex(link => link.props.to === path);
-
-  if (index > 0) {
-    return links[index - 1].props.to;
-  }
-
-  return null;
-};
-
-export function getNextLink (path) {
-  const index = links.findIndex(link => link.props.to === path);
-
-  if (index !== -1 && index < links.length - 1) {
-    return links[index + 1].props.to;
-  }
+export function getIndex (path) {
+  return links.findIndex(link => link.props.to === path);
+}
 
-  return null;
-};
+export function getLink (index) {
+  return links[index].props.to;
+}
 
 export default class TabsBar extends React.Component {
 
diff --git a/app/javascript/mastodon/features/ui/containers/bundle_container.js b/app/javascript/mastodon/features/ui/containers/bundle_container.js
new file mode 100644
index 000000000..7e3f0c3a6
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/containers/bundle_container.js
@@ -0,0 +1,19 @@
+import { connect } from 'react-redux';
+
+import Bundle from '../components/bundle';
+
+import { fetchBundleRequest, fetchBundleSuccess, fetchBundleFail } from '../../../actions/bundles';
+
+const mapDispatchToProps = dispatch => ({
+  onFetch () {
+    dispatch(fetchBundleRequest());
+  },
+  onFetchSuccess () {
+    dispatch(fetchBundleSuccess());
+  },
+  onFetchFail (error) {
+    dispatch(fetchBundleFail(error));
+  },
+});
+
+export default connect(null, mapDispatchToProps)(Bundle);
diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js
index 45ad6209b..1b2e1056a 100644
--- a/app/javascript/mastodon/features/ui/containers/status_list_container.js
+++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js
@@ -1,13 +1,13 @@
 import { connect } from 'react-redux';
 import StatusList from '../../../components/status_list';
 import { scrollTopTimeline } from '../../../actions/timelines';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 import { createSelector } from 'reselect';
 import { debounce } from 'lodash';
 
 const makeGetStatusIds = () => createSelector([
-  (state, { type }) => state.getIn(['settings', type], Immutable.Map()),
-  (state, { type }) => state.getIn(['timelines', type, 'items'], Immutable.List()),
+  (state, { type }) => state.getIn(['settings', type], ImmutableMap()),
+  (state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()),
   (state)           => state.get('statuses'),
   (state)           => state.getIn(['meta', 'me']),
 ], (columnSettings, statusIds, statuses, me) => statusIds.filter(id => {
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 824963a53..5a0398eb4 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -1,6 +1,5 @@
 import React from 'react';
-import Switch from 'react-router-dom/Switch';
-import Route from 'react-router-dom/Route';
+import classNames from 'classnames';
 import Redirect from 'react-router-dom/Redirect';
 import NotificationsContainer from './containers/notifications_container';
 import PropTypes from 'prop-types';
@@ -13,66 +12,37 @@ import { debounce } from 'lodash';
 import { uploadCompose } from '../../actions/compose';
 import { refreshHomeTimeline } from '../../actions/timelines';
 import { refreshNotifications } from '../../actions/notifications';
+import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
 import UploadArea from './components/upload_area';
 import ColumnsAreaContainer from './containers/columns_area_container';
-import Status from '../../features/status';
-import GettingStarted from '../../features/getting_started';
-import PublicTimeline from '../../features/public_timeline';
-import CommunityTimeline from '../../features/community_timeline';
-import AccountTimeline from '../../features/account_timeline';
-import AccountGallery from '../../features/account_gallery';
-import HomeTimeline from '../../features/home_timeline';
-import Compose from '../../features/compose';
-import Followers from '../../features/followers';
-import Following from '../../features/following';
-import Reblogs from '../../features/reblogs';
-import Favourites from '../../features/favourites';
-import HashtagTimeline from '../../features/hashtag_timeline';
-import Notifications from '../../features/notifications';
-import FollowRequests from '../../features/follow_requests';
-import GenericNotFound from '../../features/generic_not_found';
-import FavouritedStatuses from '../../features/favourited_statuses';
-import Blocks from '../../features/blocks';
-import Mutes from '../../features/mutes';
-
-// Small wrapper to pass multiColumn to the route components
-const WrappedSwitch = ({ multiColumn, children }) => (
-  <Switch>
-    {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))}
-  </Switch>
-);
-
-WrappedSwitch.propTypes = {
-  multiColumn: PropTypes.bool,
-  children: PropTypes.node,
-};
-
-// Small Wraper to extract the params from the route and pass
-// them to the rendered component, together with the content to
-// be rendered inside (the children)
-class WrappedRoute extends React.Component {
-
-  static propTypes = {
-    component: PropTypes.func.isRequired,
-    content: PropTypes.node,
-    multiColumn: PropTypes.bool,
-  }
-
-  renderComponent = ({ match: { params } }) => {
-    const { component: Component, content, multiColumn } = this.props;
-
-    return <Component params={params} multiColumn={multiColumn}>{content}</Component>;
-  }
-
-  render () {
-    const { component: Component, content, ...rest } = this.props;
-
-    return <Route {...rest} render={this.renderComponent} />;
-  }
-
-}
+import {
+  Compose,
+  Status,
+  GettingStarted,
+  PublicTimeline,
+  CommunityTimeline,
+  AccountTimeline,
+  AccountGallery,
+  HomeTimeline,
+  Followers,
+  Following,
+  Reblogs,
+  Favourites,
+  HashtagTimeline,
+  Notifications,
+  FollowRequests,
+  GenericNotFound,
+  FavouritedStatuses,
+  Blocks,
+  Mutes,
+} from './util/async-components';
+
+// Dummy import, to make sure that <Status /> ends up in the application bundle.
+// Without this it ends up in ~8 very commonly used bundles.
+import '../../../glitch/components/status';
 
 const mapStateToProps = state => ({
+  systemFontUi: state.getIn(['meta', 'system_font_ui']),
   layout: state.getIn(['local_settings', 'layout']),
   isWide: state.getIn(['local_settings', 'stretch']),
 });
@@ -85,6 +55,7 @@ export default class UI extends React.PureComponent {
     children: PropTypes.node,
     layout: PropTypes.string,
     isWide: PropTypes.bool,
+    systemFontUi: PropTypes.bool,
   };
 
   state = {
@@ -194,8 +165,13 @@ export default class UI extends React.PureComponent {
       }
     };
 
+    const className = classNames('ui', columnsClass(layout), {
+      'wide': isWide,
+      'system-font': this.props.systemFontUi,
+    });
+
     return (
-      <div className={'ui ' + columnsClass(layout) + (isWide ? ' wide' : '')} ref={this.setRef}>
+      <div className={className} ref={this.setRef}>
         <TabsBar />
         <ColumnsAreaContainer singleColumn={isMobile(width, layout)}>
           <WrappedSwitch>
diff --git a/app/javascript/mastodon/features/ui/util/get_rect_from_entry.js b/app/javascript/mastodon/features/ui/util/get_rect_from_entry.js
new file mode 100644
index 000000000..c266cd7dc
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/util/get_rect_from_entry.js
@@ -0,0 +1,21 @@
+
+// Get the bounding client rect from an IntersectionObserver entry.
+// This is to work around a bug in Chrome: https://crbug.com/737228
+
+let hasBoundingRectBug;
+
+function getRectFromEntry(entry) {
+  if (typeof hasBoundingRectBug !== 'boolean') {
+    const boundingRect = entry.target.getBoundingClientRect();
+    const observerRect = entry.boundingClientRect;
+    hasBoundingRectBug = boundingRect.height !== observerRect.height ||
+      boundingRect.top !== observerRect.top ||
+      boundingRect.width !== observerRect.width ||
+      boundingRect.bottom !== observerRect.bottom ||
+      boundingRect.left !== observerRect.left ||
+      boundingRect.right !== observerRect.right;
+  }
+  return hasBoundingRectBug ? entry.target.getBoundingClientRect() : entry.boundingClientRect;
+}
+
+export default getRectFromEntry;
diff --git a/app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js b/app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js
index 0e959f9ae..2b24c6583 100644
--- a/app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js
+++ b/app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js
@@ -37,9 +37,18 @@ class IntersectionObserverWrapper {
     }
   }
 
+  unobserve (id, node) {
+    if (this.observer) {
+      delete this.callbacks[id];
+      this.observer.unobserve(node);
+    }
+  }
+
   disconnect () {
     if (this.observer) {
+      this.callbacks = {};
       this.observer.disconnect();
+      this.observer = null;
     }
   }
 
diff --git a/app/javascript/mastodon/features/ui/util/react_router_helpers.js b/app/javascript/mastodon/features/ui/util/react_router_helpers.js
new file mode 100644
index 000000000..ede578e56
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/util/react_router_helpers.js
@@ -0,0 +1,57 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Switch from 'react-router-dom/Switch';
+import Route from 'react-router-dom/Route';
+
+import ColumnLoading from '../components/column_loading';
+import BundleColumnError from '../components/bundle_column_error';
+import BundleContainer from '../containers/bundle_container';
+
+// Small wrapper to pass multiColumn to the route components
+export const WrappedSwitch = ({ multiColumn, children }) => (
+  <Switch>
+    {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))}
+  </Switch>
+);
+
+WrappedSwitch.propTypes = {
+  multiColumn: PropTypes.bool,
+  children: PropTypes.node,
+};
+
+// Small Wraper to extract the params from the route and pass
+// them to the rendered component, together with the content to
+// be rendered inside (the children)
+export class WrappedRoute extends React.Component {
+
+  static propTypes = {
+    component: PropTypes.func.isRequired,
+    content: PropTypes.node,
+    multiColumn: PropTypes.bool,
+  }
+
+  renderComponent = ({ match }) => {
+    const { component, content, multiColumn } = this.props;
+
+    return (
+      <BundleContainer fetchComponent={component} loading={this.renderLoading} error={this.renderError}>
+        {Component => <Component params={match.params} multiColumn={multiColumn}>{content}</Component>}
+      </BundleContainer>
+    );
+  }
+
+  renderLoading = () => {
+    return <ColumnLoading />;
+  }
+
+  renderError = (props) => {
+    return <BundleColumnError {...props} />;
+  }
+
+  render () {
+    const { component: Component, content, ...rest } = this.props;
+
+    return <Route {...rest} render={this.renderComponent} />;
+  }
+
+}
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index c13bc73d3..6992e7e0f 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -18,6 +18,12 @@
   "account.unfollow": "إلغاء المتابعة",
   "account.unmute": "إلغاء الكتم عن @{name}",
   "boost_modal.combo": "يمكنك ضغط {combo} لتخطّي هذه في المرّة القادمة",
+  "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",
   "column.blocks": "الحسابات المحجوبة",
   "column.community": "الخيط العام المحلي",
   "column.favourites": "المفضلة",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "لا تقم بإدراجه على الخيوط العامة",
   "privacy.unlisted.short": "غير مدرج",
   "reply_indicator.cancel": "إلغاء",
-  "report.heading": "تقرير جديد",
   "report.placeholder": "تعليقات إضافية",
   "report.submit": "إرسال",
   "report.target": "إبلاغ",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index 3b6f228c6..7a56e1446 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Не следвай",
   "account.unmute": "Unmute @{name}",
   "boost_modal.combo": "You can press {combo} to skip this next time",
+  "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",
   "column.blocks": "Blocked users",
   "column.community": "Local timeline",
   "column.favourites": "Favourites",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Do not show in public timelines",
   "privacy.unlisted.short": "Unlisted",
   "reply_indicator.cancel": "Отказ",
-  "report.heading": "New report",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
   "report.target": "Reporting",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 8e8c95d56..b2673915a 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Deixar de seguir",
   "account.unmute": "Treure silenci de @{name}",
   "boost_modal.combo": "Pots premer {combo} per saltar-te això el proper cop",
+  "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",
   "column.blocks": "Usuaris bloquejats",
   "column.community": "Línia de temps local",
   "column.favourites": "Favorits",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "No publicar en línies de temps públiques",
   "privacy.unlisted.short": "No llistat",
   "reply_indicator.cancel": "Cancel·lar",
-  "report.heading": "Nou informe",
   "report.placeholder": "Comentaris addicionals",
   "report.submit": "Enviar",
   "report.target": "Informes",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 55499c0a3..4b62403c3 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Entfolgen",
   "account.unmute": "@{name} nicht mehr stummschalten",
   "boost_modal.combo": "Du kannst {combo} drücken, um dies beim nächsten Mal zu überspringen",
+  "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",
   "column.blocks": "Blockierte Benutzer",
   "column.community": "Lokale Zeitleiste",
   "column.favourites": "Favoriten",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Nicht in öffentlichen Zeitleisten anzeigen",
   "privacy.unlisted.short": "Nicht gelistet",
   "reply_indicator.cancel": "Abbrechen",
-  "report.heading": "Neue Meldung",
   "report.placeholder": "Zusätzliche Kommentare",
   "report.submit": "Absenden",
   "report.target": "Melden",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index a5ff686a0..36d82ec1a 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -644,6 +644,14 @@
         "id": "getting_started.heading"
       },
       {
+        "defaultMessage": "Home",
+        "id": "tabs_bar.home"
+      },
+      {
+        "defaultMessage": "Notifications",
+        "id": "tabs_bar.notifications"
+      },
+      {
         "defaultMessage": "Federated timeline",
         "id": "navigation_bar.public_timeline"
       },
@@ -959,27 +967,6 @@
   {
     "descriptors": [
       {
-        "defaultMessage": "New report",
-        "id": "report.heading"
-      },
-      {
-        "defaultMessage": "Additional comments",
-        "id": "report.placeholder"
-      },
-      {
-        "defaultMessage": "Submit",
-        "id": "report.submit"
-      },
-      {
-        "defaultMessage": "Reporting",
-        "id": "report.target"
-      }
-    ],
-    "path": "app/javascript/mastodon/features/report/index.json"
-  },
-  {
-    "descriptors": [
-      {
         "defaultMessage": "Delete",
         "id": "status.delete"
       },
@@ -1039,6 +1026,40 @@
   {
     "descriptors": [
       {
+        "defaultMessage": "Network error",
+        "id": "bundle_column_error.title"
+      },
+      {
+        "defaultMessage": "Something went wrong while loading this component.",
+        "id": "bundle_column_error.body"
+      },
+      {
+        "defaultMessage": "Try again",
+        "id": "bundle_column_error.retry"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/ui/components/bundle_column_error.json"
+  },
+  {
+    "descriptors": [
+      {
+        "defaultMessage": "Something went wrong while loading this component.",
+        "id": "bundle_modal_error.message"
+      },
+      {
+        "defaultMessage": "Try again",
+        "id": "bundle_modal_error.retry"
+      },
+      {
+        "defaultMessage": "Close",
+        "id": "bundle_modal_error.close"
+      }
+    ],
+    "path": "app/javascript/mastodon/features/ui/components/bundle_modal_error.json"
+  },
+  {
+    "descriptors": [
+      {
         "defaultMessage": "Cancel",
         "id": "confirmation_modal.cancel"
       }
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index b286ed2d3..cf29e38da 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Unfollow",
   "account.unmute": "Unmute @{name}",
   "boost_modal.combo": "You can press {combo} to skip this next time",
+  "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",
   "column.blocks": "Blocked users",
   "column.community": "Local timeline",
   "column.favourites": "Favourites",
@@ -141,7 +147,6 @@
   "privacy.unlisted.long": "Do not post to public timelines",
   "privacy.unlisted.short": "Unlisted",
   "reply_indicator.cancel": "Cancel",
-  "report.heading": "Report {target}",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
   "report.target": "Reporting {target}",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 330fe831d..2648a6840 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Malsekvi",
   "account.unmute": "Unmute @{name}",
   "boost_modal.combo": "You can press {combo} to skip this next time",
+  "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",
   "column.blocks": "Blocked users",
   "column.community": "Loka tempolinio",
   "column.favourites": "Favourites",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Do not show in public timelines",
   "privacy.unlisted.short": "Unlisted",
   "reply_indicator.cancel": "Rezigni",
-  "report.heading": "New report",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
   "report.target": "Reporting",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 6469aa6f2..c42930380 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Dejar de seguir",
   "account.unmute": "Unmute @{name}",
   "boost_modal.combo": "You can press {combo} to skip this next time",
+  "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",
   "column.blocks": "Usuarios bloqueados",
   "column.community": "Historia local",
   "column.favourites": "Favoritos",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "No mostrar en la historia federada",
   "privacy.unlisted.short": "Sin federar",
   "reply_indicator.cancel": "Cancelar",
-  "report.heading": "New report",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
   "report.target": "Reporting",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index 3835caab1..c9f1888b5 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -18,6 +18,12 @@
   "account.unfollow": "پایان پیگیری",
   "account.unmute": "باصدا کردن @{name}",
   "boost_modal.combo": "دکمهٔ {combo} را بزنید تا دیگر این را نبینید",
+  "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",
   "column.blocks": "کاربران مسدودشده",
   "column.community": "نوشته‌های محلی",
   "column.favourites": "پسندیده‌ها",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "عمومی، ولی فهرست نکن",
   "privacy.unlisted.short": "فهرست‌نشده",
   "reply_indicator.cancel": "لغو",
-  "report.heading": "گزارش تازه",
   "report.placeholder": "توضیح اضافه",
   "report.submit": "بفرست",
   "report.target": "گزارش‌دادن",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index dae911799..b836d2f5d 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Lopeta seuraaminen",
   "account.unmute": "Unmute @{name}",
   "boost_modal.combo": "You can press {combo} to skip this next time",
+  "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",
   "column.blocks": "Blocked users",
   "column.community": "Paikallinen aikajana",
   "column.favourites": "Favourites",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Do not show in public timelines",
   "privacy.unlisted.short": "Unlisted",
   "reply_indicator.cancel": "Peruuta",
-  "report.heading": "New report",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
   "report.target": "Reporting",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index 1a69235c8..eaa01638c 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -7,7 +7,7 @@
   "account.followers": "Abonné⋅e⋅s",
   "account.follows": "Abonnements",
   "account.follows_you": "Vous suit",
-  "account.media": "Media",
+  "account.media": "Média",
   "account.mention": "Mentionner",
   "account.mute": "Masquer",
   "account.posts": "Statuts",
@@ -18,6 +18,12 @@
   "account.unfollow": "Ne plus suivre",
   "account.unmute": "Ne plus masquer",
   "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour pouvoir passer ceci, la prochaine fois",
+  "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",
   "column.blocks": "Comptes bloqués",
   "column.community": "Fil public local",
   "column.favourites": "Favoris",
@@ -31,10 +37,10 @@
   "column_header.unpin": "Retirer",
   "column_subheading.navigation": "Navigation",
   "column_subheading.settings": "Paramètres",
-  "compose_form.lock_disclaimer": "Votre compte n'est pas {locked}. Tout le monde peut vous suivre et voir vos pouets restreints.",
+  "compose_form.lock_disclaimer": "Votre compte n’est pas {locked}. Tout le monde peut vous suivre et voir vos pouets restreints.",
   "compose_form.lock_disclaimer.lock": "verrouillé",
-  "compose_form.placeholder": "Qu’avez-vous en tête ?",
-  "compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodon. Si {domains} {domainsCount, plural, one {n’est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n’y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d’une autre manière à d’autres personnes imprévues.",
+  "compose_form.placeholder": "Qu’avez-vous en tête ?",
+  "compose_form.privacy_disclaimer": "Votre statut privé va être transmis aux personnes mentionnées sur {domains}. Avez-vous confiance en {domainsCount, plural, one {ce serveur} other {ces serveurs}} pour ne pas divulguer votre statut ? Les statuts privés ne fonctionnent que sur les instances de Mastodon. Si {domains} {domainsCount, plural, one {n’est pas une instance de Mastodon} other {ne sont pas des instances de Mastodon}}, il n’y aura aucune indication que votre statut est privé, et il pourrait être partagé ou rendu visible d’une autre manière à d’autres personnes imprévues.",
   "compose_form.publish": "Pouet ",
   "compose_form.publish_loud": "{publish}!",
   "compose_form.sensitive": "Marquer le média comme délicat",
@@ -42,13 +48,13 @@
   "compose_form.spoiler_placeholder": "Avertissement",
   "confirmation_modal.cancel": "Annuler",
   "confirmations.block.confirm": "Bloquer",
-  "confirmations.block.message": "Confirmez vous le blocage de {name} ?",
+  "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.message": "Confirmez vous la suppression de ce pouet ?",
   "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 silenciations ciblés sont suffisants et préférables.",
+  "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 silenciations ciblés sont suffisants et préférables.",
   "confirmations.mute.confirm": "Silencer",
-  "confirmations.mute.message": "Confirmez vous la silenciation {name} ?",
+  "confirmations.mute.message": "Confirmez vous la silenciation {name} ?",
   "emoji_button.activity": "Activités",
   "emoji_button.flags": "Drapeaux",
   "emoji_button.food": "Boire et manger",
@@ -59,20 +65,20 @@
   "emoji_button.search": "Recherche…",
   "emoji_button.symbols": "Symboles",
   "emoji_button.travel": "Lieux et voyages",
-  "empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir !",
+  "empty_column.community": "Le fil public local est vide. Écrivez-donc quelque chose pour le remplir !",
   "empty_column.hashtag": "Il n’y a encore aucun contenu relatif à ce hashtag",
-  "empty_column.home": "Vous ne suivez encore personne. Visitez {public} ou bien utilisez la recherche pour vous connecter à d’autres utilisateurs⋅trices.",
+  "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.inactivity": "Votre accueil est vide. Si vous ne vous êtes pas connecté⋅e depuis un moment, il se remplira automatiquement très bientôt.",
   "empty_column.home.public_timeline": "le fil public",
-  "empty_column.notifications": "Vous n’avez pas encore de notification. Interagissez avec d’autres utilisateurs⋅trices pour débuter la conversation.",
-  "empty_column.public": "Il n’y a rien ici ! Écrivez quelque chose publiquement, ou bien suivez manuellement des utilisateurs⋅trices d’autres instances pour remplir le fil public.",
+  "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": "Autoriser",
   "follow_request.reject": "Rejeter",
   "getting_started.appsshort": "Applications",
   "getting_started.faq": "FAQ",
   "getting_started.heading": "Pour commencer",
   "getting_started.open_source_notice": "Mastodon est un logiciel libre. Vous pouvez contribuer et envoyer vos commentaires et rapports de bogues via {github} sur GitHub.",
-  "getting_started.userguide": "Guide d'utilisation",
+  "getting_started.userguide": "Guide d’utilisation",
   "home.column_settings.advanced": "Avancé",
   "home.column_settings.basic": "Basique",
   "home.column_settings.filter_regex": "Filtrer avec une expression rationnelle",
@@ -93,37 +99,37 @@
   "navigation_bar.mutes": "Comptes silencés",
   "navigation_bar.preferences": "Préférences",
   "navigation_bar.public_timeline": "Fil public global",
-  "notification.favourite": "{name} a ajouté à ses favoris :",
+  "notification.favourite": "{name} a ajouté à ses favoris :",
   "notification.follow": "{name} vous suit.",
-  "notification.mention": "{name} vous a mentionné⋅e :",
-  "notification.reblog": "{name} a partagé votre statut :",
+  "notification.mention": "{name} vous a mentionné⋅e :",
+  "notification.reblog": "{name} a partagé votre statut :",
   "notifications.clear": "Nettoyer",
-  "notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?",
+  "notifications.clear_confirmation": "Voulez-vous vraiment supprimer toutes vos notifications ?",
   "notifications.column_settings.alert": "Notifications locales",
-  "notifications.column_settings.favourite": "Favoris :",
-  "notifications.column_settings.follow": "Nouveaux⋅elles abonn⋅é⋅s :",
-  "notifications.column_settings.mention": "Mentions :",
-  "notifications.column_settings.reblog": "Partages :",
+  "notifications.column_settings.favourite": "Favoris :",
+  "notifications.column_settings.follow": "Nouveaux⋅elles abonn⋅é⋅s :",
+  "notifications.column_settings.mention": "Mentions :",
+  "notifications.column_settings.reblog": "Partages :",
   "notifications.column_settings.show": "Afficher dans la colonne",
   "notifications.column_settings.sound": "Émettre un son",
   "onboarding.done": "Effectué",
   "onboarding.next": "Suivant",
-  "onboarding.page_five.public_timelines": "Le fil public global affiche les posts de tou⋅te⋅s les utilisateurs⋅trices suivi⋅es par les membres de {domain}. Le fil public local est identique mais se limite aux utilisateurs⋅trices de {domain}.",
-  "onboarding.page_four.home": "L’Accueil affiche les posts de tou⋅te⋅s les utilisateurs⋅trices que vous suivez",
+  "onboarding.page_five.public_timelines": "Le fil public global affiche les posts de tou⋅te⋅s les utilisateur⋅ice⋅s suivi⋅es par les membres de {domain}. Le fil public local est identique mais se limite aux utilisateur⋅ice⋅s de {domain}.",
+  "onboarding.page_four.home": "L’Accueil affiche les posts de tou⋅te⋅s les utilisateur⋅ice⋅s que vous suivez",
   "onboarding.page_four.notifications": "Les Notifications vous informent lorsque quelqu’un interagit avec vous",
   "onboarding.page_one.federation": "Mastodon est un réseau social qui appartient à tou⋅te⋅s.",
-  "onboarding.page_one.handle": "Vous êtes sur {domain}, une des nombreuses instances indépendantes de Mastodon. Votre nom d’utilisateur⋅trice complet est {handle}",
-  "onboarding.page_one.welcome": "Bienvenue sur Mastodon !",
+  "onboarding.page_one.handle": "Vous êtes sur {domain}, une des nombreuses instances indépendantes de Mastodon. Votre nom d’utilisateur⋅ice complet est {handle}",
+  "onboarding.page_one.welcome": "Bienvenue sur Mastodon !",
   "onboarding.page_six.admin": "L’administrateur⋅trice de votre instance est {admin}",
   "onboarding.page_six.almost_done": "Nous y sommes presque…",
   "onboarding.page_six.appetoot": "Bon Appetoot!",
   "onboarding.page_six.apps_available": "De nombreuses {apps} sont disponibles pour iOS, Android et autres. Et maintenant… Bon Appetoot!",
   "onboarding.page_six.github": "Mastodon est un logiciel libre, gratuit et open-source. Vous pouvez rapporter des bogues, suggérer des fonctionnalités, ou contribuer à son développement sur {github}.",
   "onboarding.page_six.guidelines": "règles de la communauté",
-  "onboarding.page_six.read_guidelines": "S’il vous plaît, n’oubliez pas de lire les {guidelines} !",
+  "onboarding.page_six.read_guidelines": "S’il vous plaît, n’oubliez pas de lire les {guidelines} !",
   "onboarding.page_six.various_app": "applications mobiles",
   "onboarding.page_three.profile": "Modifiez votre profil pour changer votre avatar, votre description ainsi que votre nom. Vous y trouverez également d’autres préférences.",
-  "onboarding.page_three.search": "Utilisez la barre de recherche pour trouver des utilisateurs⋅trices et regarder des hashtags tels que {illustration} et {introductions}. Pour trouver quelqu’un qui n’est pas sur cette instance, utilisez son nom d’utilisateur⋅trice complet.",
+  "onboarding.page_three.search": "Utilisez la barre de recherche pour trouver des utilisateur⋅ice⋅s et regarder des hashtags tels que {illustration} et {introductions}. Pour trouver quelqu’un qui n’est pas sur cette instance, utilisez son nom d’utilisateur⋅ice complet.",
   "onboarding.page_two.compose": "Écrivez depuis la colonne de composition. Vous pouvez ajouter des images, changer les réglages de confidentialité, et ajouter des avertissements de contenu (Content Warning) grâce aux icônes en dessous.",
   "onboarding.skip": "Passer",
   "privacy.change": "Ajuster la confidentialité du message",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Ne pas afficher dans les fils publics",
   "privacy.unlisted.short": "Non-listé",
   "reply_indicator.cancel": "Annuler",
-  "report.heading": "Nouveau signalement",
   "report.placeholder": "Commentaires additionnels",
   "report.submit": "Envoyer",
   "report.target": "Signalement",
@@ -151,7 +156,7 @@
   "status.mute_conversation": "Masquer la conversation",
   "status.open": "Déplier ce statut",
   "status.reblog": "Partager",
-  "status.reblogged_by": "{name} a partagé :",
+  "status.reblogged_by": "{name} a partagé :",
   "status.reply": "Répondre",
   "status.replyAll": "Répondre au fil",
   "status.report": "Signaler @{name}",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index db3d00394..98c7ea021 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -18,6 +18,12 @@
   "account.unfollow": "הפסקת מעקב",
   "account.unmute": "הפסקת השתקת @{name}",
   "boost_modal.combo": "ניתן להקיש {combo} כדי לדלג בפעם הבאה",
+  "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",
   "column.blocks": "חסימות",
   "column.community": "ציר זמן מקומי",
   "column.favourites": "חיבובים",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "לא יופיע בפידים הציבוריים המשותפים",
   "privacy.unlisted.short": "לא לפיד הכללי",
   "reply_indicator.cancel": "ביטול",
-  "report.heading": "דווח חדש",
   "report.placeholder": "הערות נוספות",
   "report.submit": "שליחה",
   "report.target": "דיווח",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index f85eb8a3f..fdf5c11c0 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Prestani slijediti",
   "account.unmute": "Poništi utišavanje @{name}",
   "boost_modal.combo": "Možeš pritisnuti {combo} kako bi ovo preskočio sljedeći put",
+  "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",
   "column.blocks": "Blokirani korisnici",
   "column.community": "Lokalni timeline",
   "column.favourites": "Favoriti",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Ne prikazuj u javnim timelineovima",
   "privacy.unlisted.short": "Unlisted",
   "reply_indicator.cancel": "Otkaži",
-  "report.heading": "Nova prijava",
   "report.placeholder": "Dodatni komentari",
   "report.submit": "Pošalji",
   "report.target": "Prijavljivanje",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 350410c4b..baf762c8d 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Követés abbahagyása",
   "account.unmute": "Unmute @{name}",
   "boost_modal.combo": "You can press {combo} to skip this next time",
+  "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",
   "column.blocks": "Blocked users",
   "column.community": "Local timeline",
   "column.favourites": "Favourites",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Do not show in public timelines",
   "privacy.unlisted.short": "Unlisted",
   "reply_indicator.cancel": "Mégsem",
-  "report.heading": "New report",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
   "report.target": "Reporting",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index 6e9bc5ba9..6f6d688e9 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Berhenti mengikuti",
   "account.unmute": "Berhenti membisukan @{name}",
   "boost_modal.combo": "Anda dapat menekan {combo} untuk melewati ini",
+  "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",
   "column.blocks": "Pengguna diblokir",
   "column.community": "Linimasa Lokal",
   "column.favourites": "Favorit",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Tidak ditampilkan di linimasa publik",
   "privacy.unlisted.short": "Tak Terdaftar",
   "reply_indicator.cancel": "Batal",
-  "report.heading": "Laporan baru",
   "report.placeholder": "Komentar tambahan",
   "report.submit": "Kirim",
   "report.target": "Melaporkan",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index 005dd4f56..25e0adc8a 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Ne plus sequar",
   "account.unmute": "Ne plus celar @{name}",
   "boost_modal.combo": "Tu povas presar sur {combo} por omisar co en la venonta foyo",
+  "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",
   "column.blocks": "Blokusita uzeri",
   "column.community": "Lokala tempolineo",
   "column.favourites": "Favorati",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Ne montrar en publika tempolinei",
   "privacy.unlisted.short": "Ne enlistigota",
   "reply_indicator.cancel": "Nihiligar",
-  "report.heading": "Nova denunco",
   "report.placeholder": "Plusa komenti",
   "report.submit": "Sendar",
   "report.target": "Denuncante",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 4a5b218e8..4881b0f08 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Non seguire",
   "account.unmute": "Non silenziare @{name}",
   "boost_modal.combo": "Puoi premere {combo} per saltare questo passaggio la prossima volta",
+  "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",
   "column.blocks": "Utenti bloccati",
   "column.community": "Timeline locale",
   "column.favourites": "Apprezzati",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Non mostrare sulla timeline pubblica",
   "privacy.unlisted.short": "Non elencato",
   "reply_indicator.cancel": "Annulla",
-  "report.heading": "Nuova segnalazione",
   "report.placeholder": "Commenti aggiuntivi",
   "report.submit": "Invia",
   "report.target": "Invio la segnalazione",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index cb8074b5d..f62072852 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -18,6 +18,12 @@
   "account.unfollow": "フォロー解除",
   "account.unmute": "ミュート解除",
   "boost_modal.combo": "次からは{combo}を押せば、これをスキップできます。",
+  "bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。",
+  "bundle_column_error.retry": "再試行",
+  "bundle_column_error.title": "ネットワークエラー",
+  "bundle_modal_error.close": "閉じる",
+  "bundle_modal_error.message": "コンポーネントの読み込み中に問題が発生しました。",
+  "bundle_modal_error.retry": "再試行",
   "column.blocks": "ブロックしたユーザー",
   "column.community": "ローカルタイムライン",
   "column.favourites": "お気に入り",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "公開TLで表示しない",
   "privacy.unlisted.short": "未収載",
   "reply_indicator.cancel": "キャンセル",
-  "report.heading": "新規通報",
   "report.placeholder": "コメント",
   "report.submit": "通報する",
   "report.target": "問題のユーザー",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
new file mode 100644
index 000000000..5e1aaac85
--- /dev/null
+++ b/app/javascript/mastodon/locales/ko.json
@@ -0,0 +1,181 @@
+{
+  "account.block": "차단",
+  "account.block_domain": "{domain} 전체를 숨김",
+  "account.disclaimer": "이 사용자는 다른 인스턴스에 소속되어 있으므로, 수치가 정확하지 않을 수도 있습니다.",
+  "account.edit_profile": "프로필 편집",
+  "account.follow": "팔로우",
+  "account.followers": "팔로워",
+  "account.follows": "팔로우",
+  "account.follows_you": "날 팔로우합니다",
+  "account.media": "미디어",
+  "account.mention": "답장",
+  "account.mute": "뮤트",
+  "account.posts": "포스트",
+  "account.report": "신고",
+  "account.requested": "승인 대기 중",
+  "account.unblock": "차단 해제",
+  "account.unblock_domain": "{domain} 숨김 해제",
+  "account.unfollow": "팔로우 해제",
+  "account.unmute": "뮤트 해제",
+  "boost_modal.combo": "다음부터 {combo}를 누르면 이 과정을 건너뛸 수 있습니다.",
+  "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",
+  "column.blocks": "차단 중인 사용자",
+  "column.community": "로컬 타임라인",
+  "column.favourites": "즐겨찾기",
+  "column.follow_requests": "팔로우 요청",
+  "column.home": "홈",
+  "column.mutes": "뮤트 중인 사용자",
+  "column.notifications": "알림",
+  "column.public": "연합 타임라인",
+  "column_back_button.label": "돌아가기",
+  "column_header.pin": "고정하기",
+  "column_header.unpin": "고정 해제",
+  "column_subheading.navigation": "내비게이션",
+  "column_subheading.settings": "설정",
+  "compose_form.lock_disclaimer": "이 계정은 {locked}로 설정 되어 있지 않습니다. 누구나 이 계정을 팔로우 할 수 있으며, 팔로워 공개의 포스팅을 볼 수 있습니다.",
+  "compose_form.lock_disclaimer.lock": "비공개",
+  "compose_form.placeholder": "지금 무엇을 하고 있나요?",
+  "compose_form.privacy_disclaimer": "이 계정의 비공개 포스트는 멘션된 사용자가 소속된 {domains}으로 전송됩니다. {domainsCount, plural, one {이 서버를} other {이 서버들을}} 신뢰할 수 있습니까? 포스팅의 프라이버시 보호는 Mastodon 서버에서만 유효합니다. {domains}가 Mastodon 인스턴스가 아닐 경우, 이 투고가 사적인 것으로 취급되지 않은 채 부스트 되거나 원하지 않는 사용자에게 보여질 가능성이 있습니다.",
+  "compose_form.publish": "Toot",
+  "compose_form.publish_loud": "{publish}!",
+  "compose_form.sensitive": "이 미디어를 민감한 미디어로 취급",
+  "compose_form.spoiler": "텍스트 숨기기",
+  "compose_form.spoiler_placeholder": "경고",
+  "confirmation_modal.cancel": "취소",
+  "confirmations.block.confirm": "차단",
+  "confirmations.block.message": "정말로 {name}를 차단하시겠습니까?",
+  "confirmations.delete.confirm": "삭제",
+  "confirmations.delete.message": "정말로 삭제하시겠습니까?",
+  "confirmations.domain_block.confirm": "도메인 전체를 숨김",
+  "confirmations.domain_block.message": "정말로 {domain} 전체를 숨기시겠습니까? 대부분의 경우 개별 차단이나 뮤트로 충분합니다.",
+  "confirmations.mute.confirm": "뮤트",
+  "confirmations.mute.message": "정말로 {name}를 뮤트하시겠습니까?",
+  "emoji_button.activity": "활동",
+  "emoji_button.flags": "국기",
+  "emoji_button.food": "음식",
+  "emoji_button.label": "emoji를 추가",
+  "emoji_button.nature": "자연",
+  "emoji_button.objects": "물건",
+  "emoji_button.people": "사람들",
+  "emoji_button.search": "검색...",
+  "emoji_button.symbols": "기호",
+  "emoji_button.travel": "여행과 장소",
+  "empty_column.community": "로컬 타임라인에 아무 것도 없습니다. 아무거나 적어 보세요!",
+  "empty_column.hashtag": "이 해시태그는 아직 사용되지 않았습니다.",
+  "empty_column.home": "아직 아무도 팔로우 하고 있지 않습니다. {public}를 보러 가거나, 검색하여 다른 사용자를 찾아 보세요.",
+  "empty_column.home.inactivity": "홈 피드에 아무 것도 없습니다. 한동안 활동하지 않은 경우 곧 원래대로 돌아올 것입니다.",
+  "empty_column.home.public_timeline": "연합 타임라인",
+  "empty_column.notifications": "아직 알림이 없습니다. 다른 사람과 대화를 시작해 보세요!",
+  "empty_column.public": "여기엔 아직 아무 것도 없습니다! 공개적으로 무언가 포스팅하거나, 다른 인스턴스 유저를 팔로우 해서 가득 채워보세요!",
+  "follow_request.authorize": "허가",
+  "follow_request.reject": "거부",
+  "getting_started.appsshort": "어플리케이션",
+  "getting_started.faq": "자주 있는 질문",
+  "getting_started.heading": "시작",
+  "getting_started.open_source_notice": "Mastodon은 오픈 소스 소프트웨어입니다. 누구나 GitHub({github})에서 개발에 참여하거나, 문제를 보고할 수 있습니다.",
+  "getting_started.userguide": "사용자 가이드",
+  "home.column_settings.advanced": "고급 사용자용",
+  "home.column_settings.basic": "기본 설정",
+  "home.column_settings.filter_regex": "정규 표현식으로 필터링",
+  "home.column_settings.show_reblogs": "부스트 표시",
+  "home.column_settings.show_replies": "답글 표시",
+  "home.settings": "컬럼 설정",
+  "lightbox.close": "닫기",
+  "loading_indicator.label": "불러오는 중...",
+  "media_gallery.toggle_visible": "표시 전환",
+  "missing_indicator.label": "찾을 수 없습니다",
+  "navigation_bar.blocks": "차단한 사용자",
+  "navigation_bar.community_timeline": "로컬 타임라인",
+  "navigation_bar.edit_profile": "프로필 편집",
+  "navigation_bar.favourites": "즐겨찾기",
+  "navigation_bar.follow_requests": "팔로우 요청",
+  "navigation_bar.info": "이 인스턴스에 대해서",
+  "navigation_bar.logout": "로그아웃",
+  "navigation_bar.mutes": "뮤트 중인 사용자",
+  "navigation_bar.preferences": "사용자 설정",
+  "navigation_bar.public_timeline": "연합 타임라인",
+  "notification.favourite": "{name}님이 즐겨찾기 했습니다",
+  "notification.follow": "{name}님이 나를 팔로우 했습니다",
+  "notification.mention": "{name}님이 답글을 보냈습니다",
+  "notification.reblog": "{name}님이 부스트 했습니다",
+  "notifications.clear": "알림 지우기",
+  "notifications.clear_confirmation": "정말로 알림을 삭제하시겠습니까?",
+  "notifications.column_settings.alert": "데스크탑 알림",
+  "notifications.column_settings.favourite": "즐겨찾기",
+  "notifications.column_settings.follow": "새 팔로워",
+  "notifications.column_settings.mention": "답글",
+  "notifications.column_settings.reblog": "부스트",
+  "notifications.column_settings.show": "컬럼에 표시",
+  "notifications.column_settings.sound": "효과음 재생",
+  "onboarding.done": "완료",
+  "onboarding.next": "다음",
+  "onboarding.page_five.public_timelines": "연합 타임라인에서는 {domain}의 사람들이 팔로우 중인 Mastodon 전체 인스턴스의 공개 포스트를 표시합니다. 로컬 타임라인에서는 {domain} 만의 공개 포스트를 표시합니다.",
+  "onboarding.page_four.home": "홈 타임라인에서는 내가 팔로우 중인 사람들의 포스트를 표시합니다.",
+  "onboarding.page_four.notifications": "알림에서는 다른 사람들과의 연결을 표시합니다.",
+  "onboarding.page_one.federation": "Mastodon은 누구나 참가할 수 있는 SNS입니다.",
+  "onboarding.page_one.handle": "여러분은 지금 수많은 Mastodon 인스턴스 중 하나인 {domain}에 있습니다. 당신의 유저 이름은 {handle} 입니다.",
+  "onboarding.page_one.welcome": "Mastodon에 어서 오세요!",
+  "onboarding.page_six.admin": "이 인스턴스의 관리자는 {admin}입니다.",
+  "onboarding.page_six.almost_done": "이상입니다.",
+  "onboarding.page_six.appetoot": "Bon Appetoot!",
+  "onboarding.page_six.apps_available": "iOS、Android 또는 다른 플랫폼에서 사용할 수 있는 {apps}이 있습니다.",
+  "onboarding.page_six.github": "Mastodon는 오픈 소스 소프트웨어입니다. 버그 보고나 기능 추가 요청, 기여는 {github}에서 할 수 있습니다.",
+  "onboarding.page_six.guidelines": "커뮤니티 가이드라인",
+  "onboarding.page_six.read_guidelines": "{guidelines}을 확인하는 것을 잊지 마세요.",
+  "onboarding.page_six.various_app": "다양한 모바일 어플리케이션",
+  "onboarding.page_three.profile": "[프로필 편집] 에서 자기 소개나 이름을 변경할 수 있습니다. 또한 다른 설정도 변경할 수 있습니다.",
+  "onboarding.page_three.search": "검색 바에서 {illustration} 나 {introductions} 와 같이 특정 해시태그가 달린 포스트를 보거나, 사용자를 찾을 수 있습니다.",
+  "onboarding.page_two.compose": "이 폼에서 포스팅 할 수 있습니다. 이미지나 공개 범위 설정, 스포일러 경고 설정은 아래 아이콘으로 설정할 수 있습니다.",
+  "onboarding.skip": "건너뛰기",
+  "privacy.change": "포스트의 프라이버시 설정을 변경",
+  "privacy.direct.long": "멘션한 사용자에게만 공개",
+  "privacy.direct.short": "다이렉트",
+  "privacy.private.long": "팔로워에게만 공개",
+  "privacy.private.short": "비공개",
+  "privacy.public.long": "공개 타임라인에 표시",
+  "privacy.public.short": "공개",
+  "privacy.unlisted.long": "공개 타임라인에 표시하지 않음",
+  "privacy.unlisted.short": "Unlisted",
+  "reply_indicator.cancel": "취소",
+  "report.placeholder": "코멘트",
+  "report.submit": "신고하기",
+  "report.target": "문제가 된 사용자",
+  "search.placeholder": "검색",
+  "search_results.total": "{count, number}건의 결과",
+  "status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다",
+  "status.delete": "삭제",
+  "status.favourite": "즐겨찾기",
+  "status.load_more": "더 보기",
+  "status.media_hidden": "미디어 숨겨짐",
+  "status.mention": "답장",
+  "status.mute_conversation": "이 대화를 뮤트",
+  "status.open": "상세 정보 표시",
+  "status.reblog": "부스트",
+  "status.reblogged_by": "{name}님이 부스트 했습니다",
+  "status.reply": "답장",
+  "status.replyAll": "전원에게 답장",
+  "status.report": "신고",
+  "status.sensitive_toggle": "클릭해서 표시하기",
+  "status.sensitive_warning": "민감한 미디어",
+  "status.show_less": "숨기기",
+  "status.show_more": "더 보기",
+  "status.unmute_conversation": "이 대화의 뮤트 해제하기",
+  "tabs_bar.compose": "포스트",
+  "tabs_bar.federated_timeline": "연합",
+  "tabs_bar.home": "홈",
+  "tabs_bar.local_timeline": "로컬",
+  "tabs_bar.notifications": "알림",
+  "upload_area.title": "드래그 & 드롭으로 업로드",
+  "upload_button.label": "미디어 추가",
+  "upload_form.undo": "재시도",
+  "upload_progress.label": "업로드 중...",
+  "video_player.expand": "동영상 자세히 보기",
+  "video_player.toggle_sound": "소리 토글하기",
+  "video_player.toggle_visible": "표시 전환",
+  "video_player.video_error": "동영상 재생에 실패했습니다"
+}
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 38ca6518a..479d157f3 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -10,7 +10,7 @@
   "account.media": "Media",
   "account.mention": "Vermeld @{name}",
   "account.mute": "Negeer @{name}",
-  "account.posts": "Berichten",
+  "account.posts": "Toots",
   "account.report": "Rapporteer @{name}",
   "account.requested": "Wacht op goedkeuring",
   "account.unblock": "Deblokkeer @{name}",
@@ -18,11 +18,17 @@
   "account.unfollow": "Ontvolgen",
   "account.unmute": "@{name} niet meer negeren",
   "boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan",
+  "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",
   "column.blocks": "Geblokkeerde gebruikers",
   "column.community": "Lokale tijdlijn",
   "column.favourites": "Favorieten",
   "column.follow_requests": "Volgverzoeken",
-  "column.home": "Jouw tijdlijn",
+  "column.home": "Start",
   "column.mutes": "Genegeerde gebruikers",
   "column.notifications": "Meldingen",
   "column.public": "Globale tijdlijn",
@@ -62,7 +68,7 @@
   "empty_column.community": "De lokale tijdlijn is leeg. Toot iets in het openbaar om de bal aan het rollen te krijgen!",
   "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.inactivity": "Jouw tijdlijn is leeg. Wanneer je een tijdje inactief bent geweest wordt deze snel opnieuw aangemaakt.",
+  "empty_column.home.inactivity": "Deze tijdlijn is leeg. Wanneer je een tijdje inactief bent geweest wordt deze snel opnieuw aangemaakt.",
   "empty_column.home.public_timeline": "de globale tijdlijn",
   "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.",
@@ -109,7 +115,7 @@
   "onboarding.done": "Klaar",
   "onboarding.next": "Volgende",
   "onboarding.page_five.public_timelines": "De lokale tijdlijn toont openbare toots van iedereen op {domain}. De globale tijdlijn toont openbare toots van iedereen die door gebruikers van {domain} worden gevolgd, dus ook mensen van andere Mastodon-servers. Dit zijn de openbare tijdlijnen en vormen een uitstekende manier om nieuwe mensen te ontdekken.",
-  "onboarding.page_four.home": "Jouw tijdlijn laat toots zien van mensen die jij volgt.",
+  "onboarding.page_four.home": "Deze tijdlijn laat toots zien van mensen die jij volgt.",
   "onboarding.page_four.notifications": "De kolom met meldingen toont alle interacties die je met andere Mastodon-gebruikers hebt.",
   "onboarding.page_one.federation": "Mastodon is een netwerk van onafhankelijke servers die samen een groot sociaal netwerk vormen.",
   "onboarding.page_one.handle": "Je bevindt je nu op {domain}, dus is jouw volledige Mastodon-adres {handle}",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Niet op openbare tijdlijnen tonen",
   "privacy.unlisted.short": "Minder openbaar",
   "reply_indicator.cancel": "Annuleren",
-  "report.heading": "Rapporteren",
   "report.placeholder": "Extra opmerkingen",
   "report.submit": "Verzenden",
   "report.target": "Rapporteren van",
@@ -162,7 +167,7 @@
   "status.unmute_conversation": "Conversatie niet meer negeren",
   "tabs_bar.compose": "Schrijven",
   "tabs_bar.federated_timeline": "Globaal",
-  "tabs_bar.home": "Jouw tijdlijn",
+  "tabs_bar.home": "Start",
   "tabs_bar.local_timeline": "Lokaal",
   "tabs_bar.notifications": "Meldingen",
   "upload_area.title": "Hierin slepen om te uploaden",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index a3c956279..4bbf14938 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Avfølg",
   "account.unmute": "Avdemp @{name}",
   "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.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",
   "column.blocks": "Blokkerte brukere",
   "column.community": "Lokal tidslinje",
   "column.favourites": "Likt",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Ikke vis i offentlige tidslinjer",
   "privacy.unlisted.short": "Uoppført",
   "reply_indicator.cancel": "Avbryt",
-  "report.heading": "Ny rapport",
   "report.placeholder": "Tilleggskommentarer",
   "report.submit": "Send inn",
   "report.target": "Rapporterer",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index a2a82ae9f..2c119ef41 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Quitar de sègre",
   "account.unmute": "Quitar de rescondre @{name}",
   "boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven",
+  "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",
   "column.blocks": "Personas blocadas",
   "column.community": "Flux d’actualitat public local",
   "column.favourites": "Favorits",
@@ -34,7 +40,7 @@
   "compose_form.lock_disclaimer": "Vòstre compte es pas {locked}. Tot lo mond pòt vos sègre e veire los estatuts reservats als seguidors.",
   "compose_form.lock_disclaimer.lock": "clavat",
   "compose_form.placeholder": "A de qué pensatz ?",
-  "compose_form.privacy_disclaimer": "Vòstre estatut privat serà enviat a las personas mencionadas sus {domains}. Vos fisatz d’aqueste{domainsCount, plural, one { servidor} other {s servidors}} per divulgar pas vòstre estatut ? Los estatuts privats foncionan pas que sus las instàncias a Mastodons. Se {domains} {domainsCount, plural, one {es pas una instància a Mastodon} other {son pas d'instàncias a Mastodon}}, i aurà pas d’indicacion disent que vòstre estatut es privat e poirà èsser partejat o èsser visible a de mond pas prevists",
+  "compose_form.privacy_disclaimer": "Vòstre estatut privat serà enviat a las personas mencionadas sus {domains}. Vos fisatz d’aqueste {domainsCount, plural, one { servidor} other {s servidors}} per divulgar pas vòstre estatut ? Los estatuts privats foncionan pas que sus las instàncias de Mastodon. Se {domains} {domainsCount, plural, one {es pas una instància a Mastodon} other {son pas d'instàncias a Mastodon}}, i aurà pas d’indicacion disent que vòstre estatut es privat e poirà èsser partejat o èsser visible a de mond pas prevists",
   "compose_form.publish": "Tut",
   "compose_form.publish_loud": "{publish} !",
   "compose_form.sensitive": "Marcar lo mèdia coma sensible",
@@ -51,7 +57,7 @@
   "confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?",
   "emoji_button.activity": "Activitat",
   "emoji_button.flags": "Drapèus",
-  "emoji_button.food": "Manjar e beure",
+  "emoji_button.food": "Beure e manjar",
   "emoji_button.label": "Inserir un emoji",
   "emoji_button.nature": "Natura",
   "emoji_button.objects": "Objèctes",
@@ -136,10 +142,9 @@
   "privacy.unlisted.long": "Mostrar pas dins los fluxes publics",
   "privacy.unlisted.short": "Pas-listat",
   "reply_indicator.cancel": "Anullar",
-  "report.heading": "Nòu senhalament",
   "report.placeholder": "Comentaris addicionals",
-  "report.submit": "Mandat",
-  "report.target": "Senhalament",
+  "report.submit": "Mandar",
+  "report.target": "Senhalar {target}",
   "search.placeholder": "Recercar",
   "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}",
   "status.cannot_reblog": "Aqueste estatut pòt pas èsser partejat",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index bf425501f..ac63ec40f 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -6,7 +6,7 @@
   "account.follow": "Obserwuj",
   "account.followers": "Obserwujący",
   "account.follows": "Obserwacje",
-  "account.follows_you": "Obserwują cię",
+  "account.follows_you": "Obserwuje cię",
   "account.media": "Media",
   "account.mention": "Wspomnij o @{name}",
   "account.mute": "Wycisz @{name}",
@@ -18,6 +18,12 @@
   "account.unfollow": "Przestań obserwować",
   "account.unmute": "Cofnij wyciszenie @{name}",
   "boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem",
+  "bundle_column_error.body": "Coś poszło nie tak podczas ładowania tego składnika.",
+  "bundle_column_error.retry": "Spróbuj ponownie",
+  "bundle_column_error.title": "Błąd sieci",
+  "bundle_modal_error.close": "Zamknij",
+  "bundle_modal_error.message": "Coś poszło nie tak podczas ładowania tego składnika.",
+  "bundle_modal_error.retry": "Spróbuj ponownie",
   "column.blocks": "Zablokowani użytkownicy",
   "column.community": "Lokalna oś czasu",
   "column.favourites": "Ulubione",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Niewidoczne na publicznych osiach czasu",
   "privacy.unlisted.short": "Niewidoczne",
   "reply_indicator.cancel": "Anuluj",
-  "report.heading": "Zgłoś {target}",
   "report.placeholder": "Dodatkowe komentarze",
   "report.submit": "Wyślij",
   "report.target": "Zgłaszanie {target}",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 5e5834a0e..b199a39ce 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Deixar de seguir",
   "account.unmute": "Não silenciar @{name}",
   "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",
   "column.blocks": "Utilizadores Bloqueados",
   "column.community": "Local",
   "column.favourites": "Favoritos",
@@ -72,7 +78,6 @@
   "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.support": "{faq} • {userguide} • {apps}",
   "getting_started.userguide": "User Guide",
   "home.column_settings.advanced": "Avançado",
   "home.column_settings.basic": "Básico",
@@ -107,7 +112,6 @@
   "notifications.column_settings.reblog": "Partilhas:",
   "notifications.column_settings.show": "Mostrar nas colunas",
   "notifications.column_settings.sound": "Reproduzir som",
-  "notifications.settings": "Parâmetros da listagem de Notificações",
   "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.",
@@ -138,7 +142,6 @@
   "privacy.unlisted.long": "Não publicar nos feeds públicos",
   "privacy.unlisted.short": "Não listar",
   "reply_indicator.cancel": "Cancelar",
-  "report.heading": "Nova denúncia",
   "report.placeholder": "Comentários adicionais",
   "report.submit": "Enviar",
   "report.target": "Denunciar",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index 4ebfe0c60..b199a39ce 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Deixar de seguir",
   "account.unmute": "Não silenciar @{name}",
   "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",
   "column.blocks": "Utilizadores Bloqueados",
   "column.community": "Local",
   "column.favourites": "Favoritos",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Não publicar nos feeds públicos",
   "privacy.unlisted.short": "Não listar",
   "reply_indicator.cancel": "Cancelar",
-  "report.heading": "Nova denúncia",
   "report.placeholder": "Comentários adicionais",
   "report.submit": "Enviar",
   "report.target": "Denunciar",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index f561f0151..f9f48a48d 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Отписаться",
   "account.unmute": "Снять глушение",
   "boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз",
+  "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",
   "column.blocks": "Список блокировки",
   "column.community": "Локальная лента",
   "column.favourites": "Понравившееся",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Не показывать в лентах",
   "privacy.unlisted.short": "Скрытый",
   "reply_indicator.cancel": "Отмена",
-  "report.heading": "Новая жалоба",
   "report.placeholder": "Комментарий",
   "report.submit": "Отправить",
   "report.target": "Жалуемся на",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 608d911e9..8a39beacb 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Unfollow",
   "account.unmute": "Unmute @{name}",
   "boost_modal.combo": "You can press {combo} to skip this next time",
+  "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",
   "column.blocks": "Blocked users",
   "column.community": "Local timeline",
   "column.favourites": "Favourites",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Do not post to public timelines",
   "privacy.unlisted.short": "Unlisted",
   "reply_indicator.cancel": "Cancel",
-  "report.heading": "New report",
   "report.placeholder": "Additional comments",
   "report.submit": "Submit",
   "report.target": "Reporting",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index 7512971f5..203e4a09e 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Takipten vazgeç",
   "account.unmute": "Sesi aç @{name}",
   "boost_modal.combo": "Bir dahaki sefere {combo} tuşuna basabilirsiniz",
+  "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",
   "column.blocks": "Engellenen kullanıcılar",
   "column.community": "Yerel zaman tüneli",
   "column.favourites": "Favoriler",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Herkese açık zaman tüneline gönderme",
   "privacy.unlisted.short": "Listelenmemiş",
   "reply_indicator.cancel": "İptal",
-  "report.heading": "Yeni rapor",
   "report.placeholder": "Ek yorumlar",
   "report.submit": "Gönder",
   "report.target": "Raporlama",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index a117c854b..c0f4a8dbb 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -18,6 +18,12 @@
   "account.unfollow": "Відписатися",
   "account.unmute": "Зняти глушення",
   "boost_modal.combo": "Ви можете натиснути {combo}, щоб пропустити це наступного разу",
+  "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",
   "column.blocks": "Заблоковані користувачі",
   "column.community": "Локальна стрічка",
   "column.favourites": "Вподобане",
@@ -136,7 +142,6 @@
   "privacy.unlisted.long": "Не показувати у публічних стрічках",
   "privacy.unlisted.short": "Прихований",
   "reply_indicator.cancel": "Відмінити",
-  "report.heading": "Нова скарга",
   "report.placeholder": "Додаткові коментарі",
   "report.submit": "Відправити",
   "report.target": "Скаржимося на",
diff --git a/app/javascript/mastodon/locales/whitelist_ko.json b/app/javascript/mastodon/locales/whitelist_ko.json
new file mode 100644
index 000000000..0d4f101c7
--- /dev/null
+++ b/app/javascript/mastodon/locales/whitelist_ko.json
@@ -0,0 +1,2 @@
+[
+]
diff --git a/app/javascript/mastodon/locales/whitelist_zh-HK.json b/app/javascript/mastodon/locales/whitelist_zh-HK.json
index d5fddc3bc..0d4f101c7 100644
--- a/app/javascript/mastodon/locales/whitelist_zh-HK.json
+++ b/app/javascript/mastodon/locales/whitelist_zh-HK.json
@@ -1,3 +1,2 @@
 [
-  "getting_started.support"
 ]
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index 68648f2dd..998e1c8da 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -18,6 +18,12 @@
   "account.unfollow": "取消关注",
   "account.unmute": "取消 @{name} 的静音",
   "boost_modal.combo": "如你想在下次路过时显示,请按{combo},",
+  "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",
   "column.blocks": "屏蔽用户",
   "column.community": "本站时间轴",
   "column.favourites": "赞过的嘟文",
@@ -72,7 +78,6 @@
   "getting_started.faq": "FAQ",
   "getting_started.heading": "开始使用",
   "getting_started.open_source_notice": "Mastodon 是一个开放源码的软件。你可以在官方 GitHub ({github}) 贡献或者回报问题。",
-  "getting_started.support": "{faq} • {userguide} • {apps}",
   "getting_started.userguide": "User Guide",
   "home.column_settings.advanced": "高端",
   "home.column_settings.basic": "基本",
@@ -107,7 +112,6 @@
   "notifications.column_settings.reblog": "你的嘟文被转嘟:",
   "notifications.column_settings.show": "在通知栏显示",
   "notifications.column_settings.sound": "播放音效",
-  "notifications.settings": "字段设置",
   "onboarding.done": "出发!",
   "onboarding.next": "下一步",
   "onboarding.page_five.public_timelines": "本站时间轴显示来自 {domain} 的所有人的公共嘟文。 跨站公共时间轴显示 {domain} 上的各位关注的来自所有Mastodon服务器实例上的人发表的公共嘟文。这些就是寻人好去处的公共时间轴啦。",
@@ -138,7 +142,6 @@
   "privacy.unlisted.long": "公开,但不在公共时间轴显示",
   "privacy.unlisted.short": "公开",
   "reply_indicator.cancel": "取消",
-  "report.heading": "举报",
   "report.placeholder": "额外消息",
   "report.submit": "提交",
   "report.target": "Reporting",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index a9844a625..1079d5429 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -18,6 +18,12 @@
   "account.unfollow": "取消關注",
   "account.unmute": "取消 @{name} 的靜音",
   "boost_modal.combo": "如你想在下次路過這顯示,請按{combo},",
+  "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",
   "column.blocks": "封鎖用戶",
   "column.community": "本站時間軸",
   "column.favourites": "喜歡的文章",
@@ -72,7 +78,6 @@
   "getting_started.faq": "常見問題",
   "getting_started.heading": "開始使用",
   "getting_started.open_source_notice": "Mastodon(萬象)是一個開放源碼的軟件。你可以在官方 GitHub ({github}) 貢獻或者回報問題。",
-  "getting_started.support": "{faq} • {userguide} • {apps}",
   "getting_started.userguide": "使用指南",
   "home.column_settings.advanced": "進階",
   "home.column_settings.basic": "基本",
@@ -107,7 +112,6 @@
   "notifications.column_settings.reblog": "轉推你的文章:",
   "notifications.column_settings.show": "在通知欄顯示",
   "notifications.column_settings.sound": "播放音效",
-  "notifications.settings": "欄位設定",
   "onboarding.done": "開始使用",
   "onboarding.next": "繼續",
   "onboarding.page_five.public_timelines": "「本站時間軸」顯示在 {domain} 各用戶的公開文章。「跨站時間軸」顯示在 {domain} 各人關注的所有用戶(包括其他服務站)的公開文章。這些都是「公共時間軸」,是認識新朋友的好地方。",
@@ -138,7 +142,6 @@
   "privacy.unlisted.long": "公開,但不在公共時間軸顯示",
   "privacy.unlisted.short": "公開",
   "reply_indicator.cancel": "取消",
-  "report.heading": "舉報",
   "report.placeholder": "額外訊息",
   "report.submit": "提交",
   "report.target": "舉報",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 5497becf0..6240b8879 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -18,6 +18,12 @@
   "account.unfollow": "取消關注",
   "account.unmute": "不再消音 @{name}",
   "boost_modal.combo": "下次你可以按 {combo} 來跳過",
+  "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",
   "column.blocks": "封鎖的使用者",
   "column.community": "本地時間軸",
   "column.favourites": "最愛",
@@ -72,7 +78,6 @@
   "getting_started.faq": "FAQ",
   "getting_started.heading": "馬上開始",
   "getting_started.open_source_notice": "Mastodon 是開源軟體。你可以在 GitHub {github} 上做出貢獻或是回報問題。",
-  "getting_started.support": "{faq} • {userguide} • {apps}",
   "getting_started.userguide": "使用者指南",
   "home.column_settings.advanced": "進階",
   "home.column_settings.basic": "基本",
@@ -107,7 +112,6 @@
   "notifications.column_settings.reblog": "轉推:",
   "notifications.column_settings.show": "顯示在欄位中",
   "notifications.column_settings.sound": "播放音效",
-  "notifications.settings": "欄位設定",
   "onboarding.done": "完成",
   "onboarding.next": "下一步",
   "onboarding.page_five.public_timelines": "本地時間軸顯示 {domain} 上所有人的公開貼文。聯盟時間軸顯示 {domain} 上所有人關注的公開貼文。這就是公開時間軸,發現新朋友的好地方。",
@@ -138,7 +142,6 @@
   "privacy.unlisted.long": "不要貼到公開時間軸",
   "privacy.unlisted.short": "不列出來",
   "reply_indicator.cancel": "取消",
-  "report.heading": "新的通報",
   "report.placeholder": "更多訊息",
   "report.submit": "送出",
   "report.target": "通報中",
diff --git a/app/javascript/mastodon/main.js b/app/javascript/mastodon/main.js
index f14b6a825..90c2c5da2 100644
--- a/app/javascript/mastodon/main.js
+++ b/app/javascript/mastodon/main.js
@@ -1,9 +1,5 @@
 const perf = require('./performance');
 
-// import default stylesheet with variables
-require('font-awesome/css/font-awesome.css');
-require('mastodon-application-style');
-
 function onDomContentLoaded(callback) {
   if (document.readyState !== 'loading') {
     callback();
@@ -20,6 +16,14 @@ function main() {
 
   require.context('../images/', true);
 
+  if (window.history && history.replaceState) {
+    const { pathname, search, hash } = window.location;
+    const path = pathname + search + hash;
+    if (!(/^\/web[$/]/).test(path)) {
+      history.replaceState(null, document.title, `/web${path}`);
+    }
+  }
+
   onDomContentLoaded(() => {
     const mountNode = document.getElementById('mastodon');
     const props = JSON.parse(mountNode.getAttribute('data-props'));
diff --git a/app/javascript/mastodon/reducers/accounts.js b/app/javascript/mastodon/reducers/accounts.js
index 7b7074317..4d7c3adc9 100644
--- a/app/javascript/mastodon/reducers/accounts.js
+++ b/app/javascript/mastodon/reducers/accounts.js
@@ -44,7 +44,7 @@ import {
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
 } from '../actions/favourites';
 import { STORE_HYDRATE } from '../actions/store';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, fromJS } from 'immutable';
 
 const normalizeAccount = (state, account) => {
   account = { ...account };
@@ -53,7 +53,7 @@ const normalizeAccount = (state, account) => {
   delete account.following_count;
   delete account.statuses_count;
 
-  return state.set(account.id, Immutable.fromJS(account));
+  return state.set(account.id, fromJS(account));
 };
 
 const normalizeAccounts = (state, accounts) => {
@@ -82,7 +82,7 @@ const normalizeAccountsFromStatuses = (state, statuses) => {
   return state;
 };
 
-const initialState = Immutable.Map();
+const initialState = ImmutableMap();
 
 export default function accounts(state = initialState, action) {
   switch(action.type) {
diff --git a/app/javascript/mastodon/reducers/accounts_counters.js b/app/javascript/mastodon/reducers/accounts_counters.js
index eb8a4f83d..4423e1b50 100644
--- a/app/javascript/mastodon/reducers/accounts_counters.js
+++ b/app/javascript/mastodon/reducers/accounts_counters.js
@@ -46,9 +46,9 @@ import {
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
 } from '../actions/favourites';
 import { STORE_HYDRATE } from '../actions/store';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, fromJS } from 'immutable';
 
-const normalizeAccount = (state, account) => state.set(account.id, Immutable.fromJS({
+const normalizeAccount = (state, account) => state.set(account.id, fromJS({
   followers_count: account.followers_count,
   following_count: account.following_count,
   statuses_count: account.statuses_count,
@@ -80,12 +80,12 @@ const normalizeAccountsFromStatuses = (state, statuses) => {
   return state;
 };
 
-const initialState = Immutable.Map();
+const initialState = ImmutableMap();
 
 export default function accountsCounters(state = initialState, action) {
   switch(action.type) {
   case STORE_HYDRATE:
-    return state.merge(action.state.get('accounts').map(item => Immutable.fromJS({
+    return state.merge(action.state.get('accounts').map(item => fromJS({
       followers_count: item.get('followers_count'),
       following_count: item.get('following_count'),
       statuses_count: item.get('statuses_count'),
diff --git a/app/javascript/mastodon/reducers/alerts.js b/app/javascript/mastodon/reducers/alerts.js
index aaea9775f..089d920c3 100644
--- a/app/javascript/mastodon/reducers/alerts.js
+++ b/app/javascript/mastodon/reducers/alerts.js
@@ -3,14 +3,14 @@ import {
   ALERT_DISMISS,
   ALERT_CLEAR,
 } from '../actions/alerts';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
-const initialState = Immutable.List([]);
+const initialState = ImmutableList([]);
 
 export default function alerts(state = initialState, action) {
   switch(action.type) {
   case ALERT_SHOW:
-    return state.push(Immutable.Map({
+    return state.push(ImmutableMap({
       key: state.size > 0 ? state.last().get('key') + 1 : 0,
       title: action.title,
       message: action.message,
diff --git a/app/javascript/mastodon/reducers/cards.js b/app/javascript/mastodon/reducers/cards.js
index 3c9395011..4d86b0d7e 100644
--- a/app/javascript/mastodon/reducers/cards.js
+++ b/app/javascript/mastodon/reducers/cards.js
@@ -1,13 +1,13 @@
 import { STATUS_CARD_FETCH_SUCCESS } from '../actions/cards';
 
-import Immutable from 'immutable';
+import { Map as ImmutableMap, fromJS } from 'immutable';
 
-const initialState = Immutable.Map();
+const initialState = ImmutableMap();
 
 export default function cards(state = initialState, action) {
   switch(action.type) {
   case STATUS_CARD_FETCH_SUCCESS:
-    return state.set(action.id, Immutable.fromJS(action.card));
+    return state.set(action.id, fromJS(action.card));
   default:
     return state;
   }
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 21d801f2a..6ac7b4b4a 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -25,12 +25,12 @@ import {
 } from '../actions/compose';
 import { TIMELINE_DELETE } from '../actions/timelines';
 import { STORE_HYDRATE } from '../actions/store';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
 import uuid from '../uuid';
 
-const initialState = Immutable.Map({
+const initialState = ImmutableMap({
   mounted: false,
-  advanced_options: Immutable.Map({
+  advanced_options: ImmutableMap({
     do_not_federate: false,
   }),
   sensitive: false,
@@ -44,20 +44,21 @@ const initialState = Immutable.Map({
   is_submitting: false,
   is_uploading: false,
   progress: 0,
-  media_attachments: Immutable.List(),
+  media_attachments: ImmutableList(),
   suggestion_token: null,
-  suggestions: Immutable.List(),
+  suggestions: ImmutableList(),
   me: null,
-  default_advanced_options: Immutable.Map({
+  default_advanced_options: ImmutableMap({
     do_not_federate: false,
   }),
   default_privacy: 'public',
+  default_sensitive: false,
   resetFileKey: Math.floor((Math.random() * 0x10000)),
   idempotencyKey: null,
 });
 
 function statusToTextMentions(state, status) {
-  let set = Immutable.OrderedSet([]);
+  let set = ImmutableOrderedSet([]);
   let me  = state.get('me');
 
   if (status.getIn(['account', 'id']) !== me) {
@@ -83,6 +84,8 @@ function clearAll(state) {
 };
 
 function appendMedia(state, media) {
+  const prevSize = state.get('media_attachments').size;
+
   return state.withMutations(map => {
     map.update('media_attachments', list => list.push(media));
     map.set('is_uploading', false);
@@ -90,6 +93,10 @@ function appendMedia(state, media) {
     map.update('text', oldText => `${oldText.trim()} ${media.get('text_url')}`);
     map.set('focusDate', new Date());
     map.set('idempotencyKey', uuid());
+
+    if (prevSize === 0 && state.get('default_sensitive')) {
+      map.set('sensitive', true);
+    }
   });
 };
 
@@ -112,7 +119,7 @@ const insertSuggestion = (state, position, token, completion) => {
   return state.withMutations(map => {
     map.update('text', oldText => `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`);
     map.set('suggestion_token', null);
-    map.update('suggestions', Immutable.List(), list => list.clear());
+    map.update('suggestions', ImmutableList(), list => list.clear());
     map.set('focusDate', new Date());
     map.set('idempotencyKey', uuid());
   });
@@ -180,7 +187,7 @@ export default function compose(state = initialState, action) {
       map.set('in_reply_to', action.status.get('id'));
       map.set('text', statusToTextMentions(state, action.status));
       map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
-      map.set('advanced_options', new Immutable.Map({
+      map.set('advanced_options', new ImmutableMap({
         do_not_federate: /👁\ufe0f?<\/p>$/.test(action.status.get('content')),
       }));
       map.set('focusDate', new Date());
@@ -216,7 +223,7 @@ export default function compose(state = initialState, action) {
       map.set('is_uploading', true);
     });
   case COMPOSE_UPLOAD_SUCCESS:
-    return appendMedia(state, Immutable.fromJS(action.media));
+    return appendMedia(state, fromJS(action.media));
   case COMPOSE_UPLOAD_FAIL:
     return state.set('is_uploading', false);
   case COMPOSE_UPLOAD_UNDO:
@@ -229,9 +236,9 @@ export default function compose(state = initialState, action) {
       .set('focusDate', new Date())
       .set('idempotencyKey', uuid());
   case COMPOSE_SUGGESTIONS_CLEAR:
-    return state.update('suggestions', Immutable.List(), list => list.clear()).set('suggestion_token', null);
+    return state.update('suggestions', ImmutableList(), list => list.clear()).set('suggestion_token', null);
   case COMPOSE_SUGGESTIONS_READY:
-    return state.set('suggestions', Immutable.List(action.accounts.map(item => item.id))).set('suggestion_token', action.token);
+    return state.set('suggestions', ImmutableList(action.accounts.map(item => item.id))).set('suggestion_token', action.token);
   case COMPOSE_SUGGESTION_SELECT:
     return insertSuggestion(state, action.position, action.token, action.completion);
   case TIMELINE_DELETE:
diff --git a/app/javascript/mastodon/reducers/contexts.js b/app/javascript/mastodon/reducers/contexts.js
index 8a24f5f7a..9bfc09aa7 100644
--- a/app/javascript/mastodon/reducers/contexts.js
+++ b/app/javascript/mastodon/reducers/contexts.js
@@ -1,10 +1,10 @@
 import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses';
 import { TIMELINE_DELETE } from '../actions/timelines';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
 
-const initialState = Immutable.Map({
-  ancestors: Immutable.Map(),
-  descendants: Immutable.Map(),
+const initialState = ImmutableMap({
+  ancestors: ImmutableMap(),
+  descendants: ImmutableMap(),
 });
 
 const normalizeContext = (state, id, ancestors, descendants) => {
@@ -18,12 +18,12 @@ const normalizeContext = (state, id, ancestors, descendants) => {
 };
 
 const deleteFromContexts = (state, id) => {
-  state.getIn(['descendants', id], Immutable.List()).forEach(descendantId => {
-    state = state.updateIn(['ancestors', descendantId], Immutable.List(), list => list.filterNot(itemId => itemId === id));
+  state.getIn(['descendants', id], ImmutableList()).forEach(descendantId => {
+    state = state.updateIn(['ancestors', descendantId], ImmutableList(), list => list.filterNot(itemId => itemId === id));
   });
 
-  state.getIn(['ancestors', id], Immutable.List()).forEach(ancestorId => {
-    state = state.updateIn(['descendants', ancestorId], Immutable.List(), list => list.filterNot(itemId => itemId === id));
+  state.getIn(['ancestors', id], ImmutableList()).forEach(ancestorId => {
+    state = state.updateIn(['descendants', ancestorId], ImmutableList(), list => list.filterNot(itemId => itemId === id));
   });
 
   state = state.deleteIn(['descendants', id]).deleteIn(['ancestors', id]);
@@ -34,7 +34,7 @@ const deleteFromContexts = (state, id) => {
 export default function contexts(state = initialState, action) {
   switch(action.type) {
   case CONTEXT_FETCH_SUCCESS:
-    return normalizeContext(state, action.id, Immutable.fromJS(action.ancestors), Immutable.fromJS(action.descendants));
+    return normalizeContext(state, action.id, fromJS(action.ancestors), fromJS(action.descendants));
   case TIMELINE_DELETE:
     return deleteFromContexts(state, action.id);
   default:
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index a3f5cb58e..35f30f601 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -1,7 +1,6 @@
 import { combineReducers } from 'redux-immutable';
 import timelines from './timelines';
 import meta from './meta';
-import compose from './compose';
 import alerts from './alerts';
 import { loadingBarReducer } from 'react-redux-loading-bar';
 import modal from './modal';
@@ -9,21 +8,21 @@ import user_lists from './user_lists';
 import accounts from './accounts';
 import accounts_counters from './accounts_counters';
 import statuses from './statuses';
-import media_attachments from './media_attachments';
 import relationships from './relationships';
-import search from './search';
-import notifications from './notifications';
 import settings from './settings';
 import local_settings from '../../glitch/reducers/local_settings';
 import status_lists from './status_lists';
 import cards from './cards';
 import reports from './reports';
 import contexts from './contexts';
+import compose from './compose';
+import search from './search';
+import media_attachments from './media_attachments';
+import notifications from './notifications';
 
-export default combineReducers({
+const reducers = {
   timelines,
   meta,
-  compose,
   alerts,
   loadingBar: loadingBarReducer,
   modal,
@@ -31,14 +30,17 @@ export default combineReducers({
   status_lists,
   accounts,
   accounts_counters,
-  media_attachments,
   statuses,
   relationships,
-  search,
-  notifications,
   settings,
   local_settings,
   cards,
   reports,
   contexts,
-});
+  compose,
+  search,
+  media_attachments,
+  notifications,
+};
+
+export default combineReducers(reducers);
diff --git a/app/javascript/mastodon/reducers/media_attachments.js b/app/javascript/mastodon/reducers/media_attachments.js
index 85bea4f0b..24119f628 100644
--- a/app/javascript/mastodon/reducers/media_attachments.js
+++ b/app/javascript/mastodon/reducers/media_attachments.js
@@ -1,7 +1,7 @@
 import { STORE_HYDRATE } from '../actions/store';
-import Immutable from 'immutable';
+import { Map as ImmutableMap } from 'immutable';
 
-const initialState = Immutable.Map({
+const initialState = ImmutableMap({
   accept_content_types: [],
 });
 
diff --git a/app/javascript/mastodon/reducers/meta.js b/app/javascript/mastodon/reducers/meta.js
index 1551228ec..119ef9d8f 100644
--- a/app/javascript/mastodon/reducers/meta.js
+++ b/app/javascript/mastodon/reducers/meta.js
@@ -1,7 +1,7 @@
 import { STORE_HYDRATE } from '../actions/store';
-import Immutable from 'immutable';
+import { Map as ImmutableMap } from 'immutable';
 
-const initialState = Immutable.Map({
+const initialState = ImmutableMap({
   streaming_api_base_url: null,
   access_token: null,
   me: null,
diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js
index 0c1cf5b0f..0063d24e4 100644
--- a/app/javascript/mastodon/reducers/notifications.js
+++ b/app/javascript/mastodon/reducers/notifications.js
@@ -11,10 +11,10 @@ import {
 } from '../actions/notifications';
 import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts';
 import { TIMELINE_DELETE } from '../actions/timelines';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
-const initialState = Immutable.Map({
-  items: Immutable.List(),
+const initialState = ImmutableMap({
+  items: ImmutableList(),
   next: null,
   top: true,
   unread: 0,
@@ -22,7 +22,7 @@ const initialState = Immutable.Map({
   isLoading: true,
 });
 
-const notificationToMap = notification => Immutable.Map({
+const notificationToMap = notification => ImmutableMap({
   id: notification.id,
   type: notification.type,
   account: notification.account.id,
@@ -46,7 +46,7 @@ const normalizeNotification = (state, notification) => {
 };
 
 const normalizeNotifications = (state, notifications, next) => {
-  let items    = Immutable.List();
+  let items    = ImmutableList();
   const loaded = state.get('loaded');
 
   notifications.forEach((n, i) => {
@@ -64,7 +64,7 @@ const normalizeNotifications = (state, notifications, next) => {
 };
 
 const appendNormalizedNotifications = (state, notifications, next) => {
-  let items = Immutable.List();
+  let items = ImmutableList();
 
   notifications.forEach((n, i) => {
     items = items.set(i, notificationToMap(n));
@@ -110,7 +110,7 @@ export default function notifications(state = initialState, action) {
   case ACCOUNT_BLOCK_SUCCESS:
     return filterNotifications(state, action.relationship);
   case NOTIFICATIONS_CLEAR:
-    return state.set('items', Immutable.List()).set('next', null);
+    return state.set('items', ImmutableList()).set('next', null);
   case TIMELINE_DELETE:
     return deleteByStatus(state, action.id);
   default:
diff --git a/app/javascript/mastodon/reducers/relationships.js b/app/javascript/mastodon/reducers/relationships.js
index b6607860c..c7b04a668 100644
--- a/app/javascript/mastodon/reducers/relationships.js
+++ b/app/javascript/mastodon/reducers/relationships.js
@@ -11,9 +11,9 @@ import {
   DOMAIN_BLOCK_SUCCESS,
   DOMAIN_UNBLOCK_SUCCESS,
 } from '../actions/domain_blocks';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, fromJS } from 'immutable';
 
-const normalizeRelationship = (state, relationship) => state.set(relationship.id, Immutable.fromJS(relationship));
+const normalizeRelationship = (state, relationship) => state.set(relationship.id, fromJS(relationship));
 
 const normalizeRelationships = (state, relationships) => {
   relationships.forEach(relationship => {
@@ -23,7 +23,7 @@ const normalizeRelationships = (state, relationships) => {
   return state;
 };
 
-const initialState = Immutable.Map();
+const initialState = ImmutableMap();
 
 export default function relationships(state = initialState, action) {
   switch(action.type) {
diff --git a/app/javascript/mastodon/reducers/reports.js b/app/javascript/mastodon/reducers/reports.js
index ad35eaa05..283c5b6f5 100644
--- a/app/javascript/mastodon/reducers/reports.js
+++ b/app/javascript/mastodon/reducers/reports.js
@@ -7,13 +7,13 @@ import {
   REPORT_STATUS_TOGGLE,
   REPORT_COMMENT_CHANGE,
 } from '../actions/reports';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable';
 
-const initialState = Immutable.Map({
-  new: Immutable.Map({
+const initialState = ImmutableMap({
+  new: ImmutableMap({
     isSubmitting: false,
     account_id: null,
-    status_ids: Immutable.Set(),
+    status_ids: ImmutableSet(),
     comment: '',
   }),
 });
@@ -26,14 +26,14 @@ export default function reports(state = initialState, action) {
       map.setIn(['new', 'account_id'], action.account.get('id'));
 
       if (state.getIn(['new', 'account_id']) !== action.account.get('id')) {
-        map.setIn(['new', 'status_ids'], action.status ? Immutable.Set([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : Immutable.Set());
+        map.setIn(['new', 'status_ids'], action.status ? ImmutableSet([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : ImmutableSet());
         map.setIn(['new', 'comment'], '');
       } else {
-        map.updateIn(['new', 'status_ids'], Immutable.Set(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id'))));
+        map.updateIn(['new', 'status_ids'], ImmutableSet(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id'))));
       }
     });
   case REPORT_STATUS_TOGGLE:
-    return state.updateIn(['new', 'status_ids'], Immutable.Set(), set => {
+    return state.updateIn(['new', 'status_ids'], ImmutableSet(), set => {
       if (action.checked) {
         return set.add(action.statusId);
       }
@@ -50,7 +50,7 @@ export default function reports(state = initialState, action) {
   case REPORT_SUBMIT_SUCCESS:
     return state.withMutations(map => {
       map.setIn(['new', 'account_id'], null);
-      map.setIn(['new', 'status_ids'], Immutable.Set());
+      map.setIn(['new', 'status_ids'], ImmutableSet());
       map.setIn(['new', 'comment'], '');
       map.setIn(['new', 'isSubmitting'], false);
     });
diff --git a/app/javascript/mastodon/reducers/search.js b/app/javascript/mastodon/reducers/search.js
index 0a3adac05..08d90e4e8 100644
--- a/app/javascript/mastodon/reducers/search.js
+++ b/app/javascript/mastodon/reducers/search.js
@@ -5,13 +5,13 @@ import {
   SEARCH_SHOW,
 } from '../actions/search';
 import { COMPOSE_MENTION, COMPOSE_REPLY } from '../actions/compose';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
-const initialState = Immutable.Map({
+const initialState = ImmutableMap({
   value: '',
   submitted: false,
   hidden: false,
-  results: Immutable.Map(),
+  results: ImmutableMap(),
 });
 
 export default function search(state = initialState, action) {
@@ -21,7 +21,7 @@ export default function search(state = initialState, action) {
   case SEARCH_CLEAR:
     return state.withMutations(map => {
       map.set('value', '');
-      map.set('results', Immutable.Map());
+      map.set('results', ImmutableMap());
       map.set('submitted', false);
       map.set('hidden', false);
     });
@@ -31,10 +31,10 @@ export default function search(state = initialState, action) {
   case COMPOSE_MENTION:
     return state.set('hidden', true);
   case SEARCH_FETCH_SUCCESS:
-    return state.set('results', Immutable.Map({
-      accounts: Immutable.List(action.results.accounts.map(item => item.id)),
-      statuses: Immutable.List(action.results.statuses.map(item => item.id)),
-      hashtags: Immutable.List(action.results.hashtags),
+    return state.set('results', ImmutableMap({
+      accounts: ImmutableList(action.results.accounts.map(item => item.id)),
+      statuses: ImmutableList(action.results.statuses.map(item => item.id)),
+      hashtags: ImmutableList(action.results.hashtags),
     })).set('submitted', true);
   default:
     return state;
diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js
index 9a15a1fe3..1bdee7356 100644
--- a/app/javascript/mastodon/reducers/settings.js
+++ b/app/javascript/mastodon/reducers/settings.js
@@ -1,40 +1,40 @@
 import { SETTING_CHANGE } from '../actions/settings';
 import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE } from '../actions/columns';
 import { STORE_HYDRATE } from '../actions/store';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, fromJS } from 'immutable';
 import uuid from '../uuid';
 
-const initialState = Immutable.Map({
+const initialState = ImmutableMap({
   onboarded: false,
   layout: 'auto',
 
-  home: Immutable.Map({
-    shows: Immutable.Map({
+  home: ImmutableMap({
+    shows: ImmutableMap({
       reblog: true,
       reply: true,
     }),
 
-    regex: Immutable.Map({
+    regex: ImmutableMap({
       body: '',
     }),
   }),
 
-  notifications: Immutable.Map({
-    alerts: Immutable.Map({
+  notifications: ImmutableMap({
+    alerts: ImmutableMap({
       follow: true,
       favourite: true,
       reblog: true,
       mention: true,
     }),
 
-    shows: Immutable.Map({
+    shows: ImmutableMap({
       follow: true,
       favourite: true,
       reblog: true,
       mention: true,
     }),
 
-    sounds: Immutable.Map({
+    sounds: ImmutableMap({
       follow: true,
       favourite: true,
       reblog: true,
@@ -42,20 +42,20 @@ const initialState = Immutable.Map({
     }),
   }),
 
-  community: Immutable.Map({
-    regex: Immutable.Map({
+  community: ImmutableMap({
+    regex: ImmutableMap({
       body: '',
     }),
   }),
 
-  public: Immutable.Map({
-    regex: Immutable.Map({
+  public: ImmutableMap({
+    regex: ImmutableMap({
       body: '',
     }),
   }),
 });
 
-const defaultColumns = Immutable.fromJS([
+const defaultColumns = fromJS([
   { id: 'COMPOSE', uuid: uuid(), params: {} },
   { id: 'HOME', uuid: uuid(), params: {} },
   { id: 'NOTIFICATIONS', uuid: uuid(), params: {} },
@@ -83,7 +83,7 @@ export default function settings(state = initialState, action) {
   case SETTING_CHANGE:
     return state.setIn(action.key, action.value);
   case COLUMN_ADD:
-    return state.update('columns', list => list.push(Immutable.fromJS({ id: action.id, uuid: uuid(), params: action.params })));
+    return state.update('columns', list => list.push(fromJS({ id: action.id, uuid: uuid(), params: action.params })));
   case COLUMN_REMOVE:
     return state.update('columns', list => list.filterNot(item => item.get('uuid') === action.uuid));
   case COLUMN_MOVE:
diff --git a/app/javascript/mastodon/reducers/status_lists.js b/app/javascript/mastodon/reducers/status_lists.js
index 7d00f6d30..bbc973302 100644
--- a/app/javascript/mastodon/reducers/status_lists.js
+++ b/app/javascript/mastodon/reducers/status_lists.js
@@ -2,13 +2,13 @@ import {
   FAVOURITED_STATUSES_FETCH_SUCCESS,
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
 } from '../actions/favourites';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
-const initialState = Immutable.Map({
-  favourites: Immutable.Map({
+const initialState = ImmutableMap({
+  favourites: ImmutableMap({
     next: null,
     loaded: false,
-    items: Immutable.List(),
+    items: ImmutableList(),
   }),
 });
 
@@ -16,7 +16,7 @@ const normalizeList = (state, listType, statuses, next) => {
   return state.update(listType, listMap => listMap.withMutations(map => {
     map.set('next', next);
     map.set('loaded', true);
-    map.set('items', Immutable.List(statuses.map(item => item.id)));
+    map.set('items', ImmutableList(statuses.map(item => item.id)));
   }));
 };
 
diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js
index 691135ff7..b1b1d0988 100644
--- a/app/javascript/mastodon/reducers/statuses.js
+++ b/app/javascript/mastodon/reducers/statuses.js
@@ -33,7 +33,7 @@ import {
   FAVOURITED_STATUSES_EXPAND_SUCCESS,
 } from '../actions/favourites';
 import { SEARCH_FETCH_SUCCESS } from '../actions/search';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, fromJS } from 'immutable';
 
 const normalizeStatus = (state, status) => {
   if (!status) {
@@ -51,7 +51,7 @@ const normalizeStatus = (state, status) => {
   const searchContent = [status.spoiler_text, status.content].join(' ').replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n');
   normalStatus.search_index = new DOMParser().parseFromString(searchContent, 'text/html').documentElement.textContent;
 
-  return state.update(status.id, Immutable.Map(), map => map.mergeDeep(Immutable.fromJS(normalStatus)));
+  return state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus)));
 };
 
 const normalizeStatuses = (state, statuses) => {
@@ -82,7 +82,7 @@ const filterStatuses = (state, relationship) => {
   return state;
 };
 
-const initialState = Immutable.Map();
+const initialState = ImmutableMap();
 
 export default function statuses(state = initialState, action) {
   switch(action.type) {
diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js
index 1b738a16a..065e89f96 100644
--- a/app/javascript/mastodon/reducers/timelines.js
+++ b/app/javascript/mastodon/reducers/timelines.js
@@ -15,25 +15,25 @@ import {
   ACCOUNT_BLOCK_SUCCESS,
   ACCOUNT_MUTE_SUCCESS,
 } from '../actions/accounts';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
 
-const initialState = Immutable.Map();
+const initialState = ImmutableMap();
 
-const initialTimeline = Immutable.Map({
+const initialTimeline = ImmutableMap({
   unread: 0,
   online: false,
   top: true,
   loaded: false,
   isLoading: false,
   next: false,
-  items: Immutable.List(),
+  items: ImmutableList(),
 });
 
 const normalizeTimeline = (state, timeline, statuses, next) => {
-  const ids       = Immutable.List(statuses.map(status => status.get('id')));
+  const ids       = ImmutableList(statuses.map(status => status.get('id')));
   const wasLoaded = state.getIn([timeline, 'loaded']);
   const hadNext   = state.getIn([timeline, 'next']);
-  const oldIds    = state.getIn([timeline, 'items'], Immutable.List());
+  const oldIds    = state.getIn([timeline, 'items'], ImmutableList());
 
   return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
     mMap.set('loaded', true);
@@ -44,8 +44,8 @@ const normalizeTimeline = (state, timeline, statuses, next) => {
 };
 
 const appendNormalizedTimeline = (state, timeline, statuses, next) => {
-  const ids    = Immutable.List(statuses.map(status => status.get('id')));
-  const oldIds = state.getIn([timeline, 'items'], Immutable.List());
+  const ids    = ImmutableList(statuses.map(status => status.get('id')));
+  const oldIds = state.getIn([timeline, 'items'], ImmutableList());
 
   return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
     mMap.set('isLoading', false);
@@ -56,7 +56,7 @@ const appendNormalizedTimeline = (state, timeline, statuses, next) => {
 
 const updateTimeline = (state, timeline, status, references) => {
   const top        = state.getIn([timeline, 'top']);
-  const ids        = state.getIn([timeline, 'items'], Immutable.List());
+  const ids        = state.getIn([timeline, 'items'], ImmutableList());
   const includesId = ids.includes(status.get('id'));
   const unread     = state.getIn([timeline, 'unread'], 0);
 
@@ -124,11 +124,11 @@ export default function timelines(state = initialState, action) {
   case TIMELINE_EXPAND_FAIL:
     return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false));
   case TIMELINE_REFRESH_SUCCESS:
-    return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses), action.next);
+    return normalizeTimeline(state, action.timeline, fromJS(action.statuses), action.next);
   case TIMELINE_EXPAND_SUCCESS:
-    return appendNormalizedTimeline(state, action.timeline, Immutable.fromJS(action.statuses), action.next);
+    return appendNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next);
   case TIMELINE_UPDATE:
-    return updateTimeline(state, action.timeline, Immutable.fromJS(action.status), action.references);
+    return updateTimeline(state, action.timeline, fromJS(action.status), action.references);
   case TIMELINE_DELETE:
     return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
   case ACCOUNT_BLOCK_SUCCESS:
diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js
index 83bf1be1b..8db18c5dc 100644
--- a/app/javascript/mastodon/reducers/user_lists.js
+++ b/app/javascript/mastodon/reducers/user_lists.js
@@ -20,22 +20,22 @@ import {
   MUTES_FETCH_SUCCESS,
   MUTES_EXPAND_SUCCESS,
 } from '../actions/mutes';
-import Immutable from 'immutable';
+import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
-const initialState = Immutable.Map({
-  followers: Immutable.Map(),
-  following: Immutable.Map(),
-  reblogged_by: Immutable.Map(),
-  favourited_by: Immutable.Map(),
-  follow_requests: Immutable.Map(),
-  blocks: Immutable.Map(),
-  mutes: Immutable.Map(),
+const initialState = ImmutableMap({
+  followers: ImmutableMap(),
+  following: ImmutableMap(),
+  reblogged_by: ImmutableMap(),
+  favourited_by: ImmutableMap(),
+  follow_requests: ImmutableMap(),
+  blocks: ImmutableMap(),
+  mutes: ImmutableMap(),
 });
 
 const normalizeList = (state, type, id, accounts, next) => {
-  return state.setIn([type, id], Immutable.Map({
+  return state.setIn([type, id], ImmutableMap({
     next,
-    items: Immutable.List(accounts.map(item => item.id)),
+    items: ImmutableList(accounts.map(item => item.id)),
   }));
 };
 
@@ -56,22 +56,22 @@ export default function userLists(state = initialState, action) {
   case FOLLOWING_EXPAND_SUCCESS:
     return appendToList(state, 'following', action.id, action.accounts, action.next);
   case REBLOGS_FETCH_SUCCESS:
-    return state.setIn(['reblogged_by', action.id], Immutable.List(action.accounts.map(item => item.id)));
+    return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
   case FAVOURITES_FETCH_SUCCESS:
-    return state.setIn(['favourited_by', action.id], Immutable.List(action.accounts.map(item => item.id)));
+    return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
   case FOLLOW_REQUESTS_FETCH_SUCCESS:
-    return state.setIn(['follow_requests', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next);
+    return state.setIn(['follow_requests', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next);
   case FOLLOW_REQUESTS_EXPAND_SUCCESS:
     return state.updateIn(['follow_requests', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next);
   case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
   case FOLLOW_REQUEST_REJECT_SUCCESS:
     return state.updateIn(['follow_requests', 'items'], list => list.filterNot(item => item === action.id));
   case BLOCKS_FETCH_SUCCESS:
-    return state.setIn(['blocks', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
+    return state.setIn(['blocks', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
   case BLOCKS_EXPAND_SUCCESS:
     return state.updateIn(['blocks', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
   case MUTES_FETCH_SUCCESS:
-    return state.setIn(['mutes', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
+    return state.setIn(['mutes', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
   case MUTES_EXPAND_SUCCESS:
     return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
   default:
diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js
index 07d9a2629..d26d1b727 100644
--- a/app/javascript/mastodon/selectors/index.js
+++ b/app/javascript/mastodon/selectors/index.js
@@ -1,5 +1,5 @@
 import { createSelector } from 'reselect';
-import Immutable from 'immutable';
+import { List as ImmutableList } from 'immutable';
 
 const getAccountBase         = (state, id) => state.getIn(['accounts', id], null);
 const getAccountCounters     = (state, id) => state.getIn(['accounts_counters', id], null);
@@ -73,10 +73,10 @@ export const makeGetNotification = () => {
 };
 
 export const getAccountGallery = createSelector([
-  (state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], Immutable.List()),
+  (state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], ImmutableList()),
   state       => state.get('statuses'),
 ], (statusIds, statuses) => {
-  let medias = Immutable.List();
+  let medias = ImmutableList();
 
   statusIds.forEach(statusId => {
     const status = statuses.get(statusId);
diff --git a/app/javascript/packs/common.js b/app/javascript/packs/common.js
index 9d63d8f98..a0cb91ae4 100644
--- a/app/javascript/packs/common.js
+++ b/app/javascript/packs/common.js
@@ -1,2 +1,7 @@
 import { start } from 'rails-ujs';
+
+// import default stylesheet with variables
+require('font-awesome/css/font-awesome.css');
+require('mastodon-application-style');
+
 start();
diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
index 8a3ae0b3c..06cc1b53a 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -5,6 +5,9 @@ import emojify from '../mastodon/emoji';
 import { getLocale } from '../mastodon/locales';
 import loadPolyfills from '../mastodon/load_polyfills';
 import { processBio } from '../glitch/util/bio_metadata';
+import TimelineContainer from '../mastodon/containers/timeline_container';
+import React from 'react';
+import ReactDOM from 'react-dom';
 
 require.context('../images/', true);
 
@@ -37,6 +40,13 @@ function loaded() {
     const datetime = new Date(content.getAttribute('datetime'));
     content.textContent = relativeFormat.format(datetime);;
   });
+
+  const mountNode = document.getElementById('mastodon-timeline');
+
+  if (mountNode !== null) {
+    const props = JSON.parse(mountNode.getAttribute('data-props'));
+    ReactDOM.render(<TimelineContainer {...props} />, mountNode);
+  }
 }
 
 function main() {
@@ -54,8 +64,12 @@ function main() {
     }
   });
 
-  delegate(document, '.media-spoiler', 'click', ({ target }) => {
-    target.style.display = 'none';
+  delegate(document, '.activity-stream .media-spoiler-wrapper .media-spoiler', 'click', function() {
+    this.parentNode.classList.add('media-spoiler-wrapper__visible');
+  });
+
+  delegate(document, '.activity-stream .media-spoiler-wrapper .spoiler-button', 'click', function() {
+    this.parentNode.classList.remove('media-spoiler-wrapper__visible');
   });
 
   delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
diff --git a/app/javascript/styles/about.scss b/app/javascript/styles/about.scss
index 7145d0092..5716163be 100644
--- a/app/javascript/styles/about.scss
+++ b/app/javascript/styles/about.scss
@@ -116,10 +116,6 @@
     .wrapper {
       padding: 20px;
     }
-
-    .features-list {
-      display: block;
-    }
   }
 }
 
@@ -299,80 +295,438 @@
   }
 }
 
-.features-list {
+.features-list__row {
   display: flex;
-  margin-bottom: 20px;
+  padding: 10px 0;
+  justify-content: space-between;
+
+  &:first-child {
+    padding-top: 0;
+  }
 
-  .features-list__column {
-    flex: 1 1 0;
+  .visual {
+    flex: 0 0 auto;
+    display: flex;
+    align-items: center;
+    margin-left: 15px;
 
-    ul {
-      list-style: none;
+    .fa {
+      display: block;
+      color: $ui-primary-color;
+      font-size: 48px;
     }
+  }
 
-    li {
-      margin: 0;
+  .text {
+    font-size: 16px;
+    line-height: 30px;
+    color: lighten($ui-base-color, 26%);
+
+    h6 {
+      font-weight: 500;
+      color: $ui-primary-color;
     }
   }
 }
 
-.screenshot-with-signup {
-  display: flex;
-  margin-bottom: 20px;
-
-  .mascot {
-    flex: 1 1 auto;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    flex-direction: column;
+.landing-page {
+  .header-wrapper {
+    padding-top: 15px;
+    background: $ui-base-color;
+    background: linear-gradient(150deg, lighten($ui-base-color, 8%), $ui-base-color);
+    position: relative;
 
-    img {
-      display: block;
+    .mascot-container {
+      max-width: 800px;
       margin: 0 auto;
-      max-width: 100%;
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      height: 100%;
+    }
+
+    .mascot {
+      position: absolute;
+      bottom: -14px;
+      width: auto;
       height: auto;
+      left: 60px;
+      z-index: 3;
+    }
+  }
+
+  p,
+  li {
+    font: inherit;
+    font-weight: inherit;
+    margin-bottom: 0;
+  }
+
+  .header {
+    line-height: 30px;
+    overflow: hidden;
+
+    .container {
+      display: flex;
+      justify-content: space-between;
+    }
+
+    .hero {
+      margin-top: 50px;
+      align-items: center;
+      position: relative;
+
+      .floats {
+        position: absolute;
+        width: 100%;
+        height: 100%;
+        top: 0;
+        left: 0;
+
+        img {
+          position: absolute;
+          transition: all 0.1s linear;
+          animation-name: floating;
+          animation-duration: 1.7s;
+          animation-iteration-count: infinite;
+          animation-direction: alternate;
+          animation-timing-function: linear;
+          z-index: 2;
+        }
+
+        .float-1 {
+          height: 170px;
+          right: -120px;
+          bottom: 0;
+        }
+
+        .float-2 {
+          height: 100px;
+          right: 210px;
+          bottom: 0;
+          animation-delay: 0.2s;
+        }
+
+        .float-3 {
+          height: 140px;
+          right: 110px;
+          top: -30px;
+          animation-delay: 0.1s;
+        }
+      }
+
+      .simple_form,
+      .closed-registrations-message {
+        background: darken($ui-base-color, 4%);
+        width: 280px;
+        padding: 15px 20px;
+        border-radius: 4px 4px 0 0;
+        line-height: initial;
+        position: relative;
+        z-index: 4;
+
+        .actions {
+          margin-bottom: 0;
+
+          button,
+          .button,
+          .block-button {
+            margin-bottom: 0;
+          }
+        }
+      }
+
+      .heading {
+        position: relative;
+        z-index: 4;
+        padding-bottom: 150px;
+      }
+
+      .closed-registrations-message {
+        min-height: 330px;
+        display: flex;
+        flex-direction: column;
+        justify-content: space-between;
+      }
+    }
+
+    ul {
+      list-style: none;
+      margin: 0;
+
+      li {
+        display: inline-block;
+        vertical-align: bottom;
+        margin: 0;
+
+        &:first-child a {
+          padding-left: 0;
+        }
+
+        &:last-child a {
+          padding-right: 0;
+        }
+      }
+    }
+
+    .links {
+      position: relative;
+      z-index: 4;
+
+      a {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        color: $ui-primary-color;
+        text-decoration: none;
+        padding: 12px 16px;
+        line-height: 32px;
+        font-family: 'mastodon-font-display', sans-serif;
+        font-weight: 500;
+        font-size: 14px;
+
+        &:hover {
+          color: $ui-secondary-color;
+        }
+      }
+
+      .brand {
+        a {
+          padding-left: 0;
+          color: $white;
+        }
+
+        img {
+          width: 32px;
+          height: 32px;
+          margin-right: 10px;
+        }
+      }
+    }
+  }
+
+  .container {
+    width: 100%;
+    box-sizing: border-box;
+    max-width: 800px;
+    margin: 0 auto;
+  }
+
+  .wrapper {
+    max-width: 800px;
+    margin: 0 auto;
+    padding: 0;
+  }
+
+  .learn-more-cta {
+    background: darken($ui-base-color, 4%);
+    padding: 50px 0;
+  }
+
+  h3 {
+    font-family: 'mastodon-font-display', sans-serif;
+    font-size: 16px;
+    line-height: 24px;
+    font-weight: 500;
+    margin-bottom: 20px;
+    color: $ui-primary-color;
+  }
+
+  p {
+    font-size: 16px;
+    line-height: 30px;
+    color: lighten($ui-base-color, 26%);
+  }
+
+  .features {
+    padding: 50px 0;
+
+    .container {
+      display: flex;
     }
   }
 
-  .simple_form,
-  .closed-registrations-message {
-    width: 300px;
+  #mastodon-timeline {
+    -webkit-overflow-scrolling: touch;
+    -ms-overflow-style: -ms-autohiding-scrollbar;
+    font-family: 'mastodon-font-sans-serif', sans-serif;
+    font-size: 13px;
+    line-height: 18px;
+    font-weight: 400;
+    color: $primary-text-color;
+    width: 330px;
+    margin-right: 30px;
     flex: 0 0 auto;
-    background: rgba(darken($ui-base-color, 7%), 0.5);
-    padding: 14px;
-    border-radius: 4px;
-    box-shadow: 0 0 15px rgba($base-shadow-color, 0.4);
+    background: $ui-base-color;
+    overflow: hidden;
+    box-shadow: 0 0 6px rgba($black, 0.1);
 
-    .actions {
-      margin-bottom: 0;
+    .column {
+      padding: 0;
+      border-radius: 4px;
+      overflow: hidden;
+      height: 100%;
     }
 
-    .info {
-      text-align: center;
+    .scrollable {
+      height: 400px;
+    }
+
+    p {
+      font-size: inherit;
+      line-height: inherit;
+      font-weight: inherit;
+      color: $primary-text-color;
 
       a {
         color: $ui-secondary-color;
+        text-decoration: none;
       }
     }
   }
 
-  @media screen and (max-width: 625px) {
-    .mascot {
+  .about-mastodon {
+    max-width: 675px;
+
+    p {
+      margin-bottom: 20px;
+    }
+
+    .features-list {
+      margin-top: 20px;
+    }
+  }
+
+  em {
+    display: inline;
+    margin: 0;
+    padding: 0;
+    font-weight: 500;
+    background: transparent;
+    font-family: inherit;
+    font-size: inherit;
+    line-height: inherit;
+    color: $ui-primary-color;
+  }
+
+  h1 {
+    font-family: 'mastodon-font-display', sans-serif;
+    font-size: 26px;
+    line-height: 30px;
+    margin-bottom: 0;
+    font-weight: 500;
+    color: $ui-secondary-color;
+
+    small {
+      font-family: 'mastodon-font-sans-serif', sans-serif;
+      display: block;
+      font-size: 18px;
+      font-weight: 400;
+      color: lighten($ui-base-color, 26%);
+    }
+  }
+
+  .footer-links {
+    padding-bottom: 50px;
+    text-align: right;
+    color: lighten($ui-base-color, 26%);
+
+    p {
+      font-size: 14px;
+    }
+
+    a {
+      color: inherit;
+      text-decoration: underline;
+    }
+  }
+
+  @media screen and (max-width: 800px) {
+    .container {
+      padding: 0 20px;
+    }
+
+    .header-wrapper .mascot {
+      left: 20px;
+    }
+  }
+
+  @media screen and (max-width: 689px) {
+    .header-wrapper .mascot {
       display: none;
     }
+  }
 
-    .simple_form,
-    .closed-registrations-message {
-      flex: auto;
+  @media screen and (max-width: 675px) {
+    .header-wrapper {
+      padding-top: 0;
+    }
+
+    .header .container,
+    .features .container {
+      display: block;
+    }
+
+    .links {
+      padding-top: 15px;
+      background: darken($ui-base-color, 4%);
+    }
+
+    .header {
+      padding-top: 0;
+
+      .hero {
+        margin-top: 30px;
+        padding: 0;
+
+        .heading {
+          padding-bottom: 20px;
+        }
+      }
+
+      .floats {
+        display: none;
+      }
+
+      .heading,
+      .nav {
+        text-align: center;
+      }
+
+      .heading h1 {
+        padding: 30px 0;
+      }
+
+      .hero {
+        .simple_form,
+        .closed-registrations-message {
+          background: darken($ui-base-color, 8%);
+          width: 100%;
+          border-radius: 0;
+          box-sizing: border-box;
+        }
+      }
+    }
+
+    #mastodon-timeline {
+      height: 70vh;
+      width: 100%;
+      margin-bottom: 50px;
     }
   }
 }
 
-.closed-registrations-message {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  text-align: center;
+@keyframes floating {
+  from {
+    transform: translate(0, 0);
+  }
+
+  65% {
+    transform: translate(0, 4px);
+  }
+
+  to {
+    transform: translate(0, -0);
+  }
 }
diff --git a/app/javascript/styles/basics.scss b/app/javascript/styles/basics.scss
index 70a5be367..182ea36a4 100644
--- a/app/javascript/styles/basics.scss
+++ b/app/javascript/styles/basics.scss
@@ -1,6 +1,6 @@
 body {
   font-family: 'mastodon-font-sans-serif', sans-serif;
-  background: $ui-base-color url('../images/background-photo.jpg');
+  background: $ui-base-color;
   background-size: cover;
   background-attachment: fixed;
   font-size: 13px;
@@ -11,6 +11,8 @@ body {
   text-rendering: optimizelegibility;
   font-feature-settings: "kern";
   text-size-adjust: none;
+  -webkit-tap-highlight-color: rgba(0,0,0,0);
+  -webkit-tap-highlight-color: transparent;
 
   &.app-body {
     position: fixed;
@@ -20,6 +22,11 @@ body {
     background: $ui-base-color;
   }
 
+  &.about-body {
+    background: darken($ui-base-color, 8%);
+    padding-bottom: 0;
+  }
+
   &.embed {
     background: transparent;
     margin: 0;
@@ -61,3 +68,18 @@ button {
   align-items: center;
   justify-content: center;
 }
+
+.system-font {
+  // system-ui => standard property (Chrome/Android WebView 56+, Opera 43+, Safari 11+)
+  // -apple-system => Safari <11 specific
+  // BlinkMacSystemFont => Chrome <56 on macOS specific
+  // Segoe UI => Windows 7/8/10
+  // Oxygen => KDE
+  // Ubuntu => Unity/Ubuntu
+  // Cantarell => GNOME
+  // Fira Sans => Firefox OS
+  // Droid Sans => Older Androids (<4.0)
+  // Helvetica Neue => Older macOS <10.11
+  // mastodon-font-sans-serif => web-font (Roboto) fallback and newer Androids (>=4.0)
+  font-family: system-ui, -apple-system,BlinkMacSystemFont, "Segoe UI","Oxygen", "Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",mastodon-font-sans-serif, sans-serif;
+}
diff --git a/app/javascript/styles/boost.scss b/app/javascript/styles/boost.scss
index 9cad7a4f5..e44df2ea4 100644
--- a/app/javascript/styles/boost.scss
+++ b/app/javascript/styles/boost.scss
@@ -1,12 +1,15 @@
-@function url-friendly-colour($colour) {
-  @return '%23' + str-slice('#{$colour}', 2, -1)
+@function hex-color($color) {
+  @if type-of($color) == 'color' {
+    $color: str-slice(ie-hex-str($color), 4);
+  }
+  @return '%23' + unquote($color)
 }
 
 button.icon-button i.fa-retweet {
-  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{url-friendly-colour(lighten($ui-base-color, 26%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{url-friendly-colour($ui-highlight-color)}' stroke-width='0'/></svg>");
+  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color(lighten($ui-base-color, 26%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color($ui-highlight-color)}' stroke-width='0'/></svg>");
 
   &:hover {
-    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{url-friendly-colour(lighten($ui-base-color, 33%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{url-friendly-colour($ui-highlight-color)}' stroke-width='0'/></svg>");
+    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color(lighten($ui-base-color, 33%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color($ui-highlight-color)}' stroke-width='0'/></svg>");
   }
 }
 
@@ -23,3 +26,11 @@ button.icon-button.disabled i.fa-retweet {
     background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{lighten($ui-base-color, 16%)}' stroke-width='0'/></svg>");
   }
 }
+
+//  Mastodon gave us this one, but I'm not sure if it's better.  - @kibi@glitch.social
+
+/*
+button.icon-button.disabled i.fa-retweet {
+  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color(lighten($ui-base-color, 13%))}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color($ui-highlight-color)}' stroke-width='0'/></svg>");
+}
+*/
diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss
index 49d3c9873..6cca3666a 100644
--- a/app/javascript/styles/components.scss
+++ b/app/javascript/styles/components.scss
@@ -42,8 +42,38 @@
     cursor: default;
   }
 
+  &.button-alternative {
+    font-size: 16px;
+    line-height: 36px;
+    height: auto;
+    color: $ui-base-color;
+    background: $ui-primary-color;
+    text-transform: none;
+    padding: 4px 16px;
+
+    &:active,
+    &:focus,
+    &:hover {
+      background-color: lighten($ui-primary-color, 4%);
+    }
+  }
+
   &.button-secondary {
-    //
+    font-size: 16px;
+    line-height: 36px;
+    height: auto;
+    color: $ui-primary-color;
+    text-transform: none;
+    background: transparent;
+    padding: 3px 15px;
+    border: 1px solid $ui-primary-color;
+
+    &:active,
+    &:focus,
+    &:hover {
+      border-color: lighten($ui-primary-color, 4%);
+      color: lighten($ui-primary-color, 4%);
+    }
   }
 
   &.button--block {
@@ -1327,6 +1357,20 @@
     height: 100%;
     background-image: none;
   }
+
+  &.image-loader--amorphous {
+    position: static;
+
+    .image-loader__preview-canvas {
+      display: none;
+    }
+
+    .image-loader__img {
+      position: static;
+      width: auto;
+      height: auto;
+    }
+  }
 }
 
 .navigation-bar {
@@ -1463,6 +1507,23 @@
   .columns-area {
     padding: 0;
   }
+
+  .react-swipeable-view-container .columns-area {
+    height: calc(100% - 20px) !important;
+  }
+}
+
+.react-swipeable-view-container {
+  &,
+  .columns-area,
+  .drawer,
+  .column {
+    height: 100%;
+  }
+}
+
+.react-swipeable-view-container > * {
+  height: 100%;
 }
 
 .column {
@@ -1510,8 +1571,7 @@
 .drawer__tab {
   display: block;
   flex: 1 1 auto;
-  padding: 15px;
-  padding-bottom: 13px;
+  padding: 15px 5px 13px;
   color: $ui-primary-color;
   text-decoration: none;
   text-align: center;
@@ -2537,7 +2597,8 @@ button.icon-button.active i.fa-retweet {
   vertical-align: middle;
 }
 
-.empty-column-indicator {
+.empty-column-indicator,
+.error-column {
   color: lighten($ui-base-color, 20%);
   background: $ui-base-color;
   text-align: center;
@@ -2563,6 +2624,10 @@ button.icon-button.active i.fa-retweet {
   }
 }
 
+.error-column {
+  flex-direction: column;
+}
+
 @keyframes pulse {
   0% {
     opacity: 1;
@@ -3206,7 +3271,7 @@ button.icon-button.active i.fa-retweet {
   video {
     max-width: 80vw;
     max-height: 80vh;
-    width: auto;
+    width: 100%;
     height: auto;
   }
 
@@ -3214,6 +3279,7 @@ button.icon-button.active i.fa-retweet {
   canvas {
     display: block;
     background: url('../images/void.png') repeat;
+    object-fit: contain;
   }
 }
 
@@ -3224,7 +3290,8 @@ button.icon-button.active i.fa-retweet {
   z-index: 100;
 }
 
-.onboarding-modal {
+.onboarding-modal,
+.error-modal {
   background: $ui-secondary-color;
   color: $ui-base-color;
   border-radius: 8px;
@@ -3238,6 +3305,26 @@ button.icon-button.active i.fa-retweet {
   width: 80vw;
   max-width: 520px;
   max-height: 420px;
+
+  .react-swipeable-view-container > div {
+    width: 100%;
+    height: 100%;
+    box-sizing: border-box;
+    padding: 25px;
+    display: none;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    display: flex;
+    user-select: text;
+  }
+}
+
+.error-modal__body {
+  height: 80vh;
+  width: 80vw;
+  max-width: 520px;
+  max-height: 420px;
   position: relative;
 
   & > div {
@@ -3258,6 +3345,14 @@ button.icon-button.active i.fa-retweet {
   }
 }
 
+.error-modal__body {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  text-align: center;
+}
+
 @media screen and (max-width: 550px) {
   .onboarding-modal {
     width: 100%;
@@ -3274,7 +3369,8 @@ button.icon-button.active i.fa-retweet {
   }
 }
 
-.onboarding-modal__paginator {
+.onboarding-modal__paginator,
+.error-modal__footer {
   flex: 0 0 auto;
   background: darken($ui-secondary-color, 8%);
   display: flex;
@@ -3284,7 +3380,8 @@ button.icon-button.active i.fa-retweet {
     min-width: 33px;
   }
 
-  .onboarding-modal__nav {
+  .onboarding-modal__nav,
+  .error-modal__nav {
     color: darken($ui-secondary-color, 34%);
     background-color: transparent;
     border: 0;
@@ -3307,6 +3404,10 @@ button.icon-button.active i.fa-retweet {
   }
 }
 
+.error-modal__footer {
+  justify-content: center;
+}
+
 .onboarding-modal__dots {
   flex: 1 1 auto;
   display: flex;
@@ -3676,6 +3777,7 @@ button.icon-button.active i.fa-retweet {
 
 .report-modal__statuses {
   min-height: 20vh;
+  max-height: 40vh;
   overflow-y: auto;
   overflow-x: hidden;
 }
@@ -3831,8 +3933,7 @@ button.icon-button.active i.fa-retweet {
 
 .media-gallery__item-thumbnail {
   cursor: zoom-in;
-  display: flex;
-  align-items: center;
+  display: block;
   text-decoration: none;
   width: 100%;
   height: 100%;
diff --git a/app/javascript/styles/containers.scss b/app/javascript/styles/containers.scss
index 68f73e0c0..44d4c1118 100644
--- a/app/javascript/styles/containers.scss
+++ b/app/javascript/styles/containers.scss
@@ -10,52 +10,36 @@
 }
 
 .logo-container {
-  max-width: 400px;
   margin: 100px auto;
-  margin-bottom: 0;
-  cursor: default;
+  margin-bottom: 50px;
 
   @media screen and (max-width: 360px) {
     margin: 30px auto;
   }
 
   h1 {
-    display: block;
-    text-align: center;
-    color: $primary-text-color;
-    font-size: 48px;
-    font-weight: 500;
+    display: flex;
+    justify-content: center;
+    align-items: center;
 
     img {
-      display: block;
-      margin: 20px auto;
-      width: 180px;
-      height: 180px;
+      width: 32px;
+      height: 32px;
+      margin-right: 10px;
     }
 
     a {
-      color: inherit;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      color: $primary-text-color;
       text-decoration: none;
       outline: 0;
-
-      img {
-        opacity: 0.8;
-        transition: opacity 0.8s ease;
-      }
-
-      &:hover {
-        img {
-          opacity: 1;
-          transition-duration: 0.2s;
-        }
-      }
-    }
-
-    small {
-      display: block;
-      font-size: 12px;
-      font-weight: 400;
-      font-family: 'mastodon-font-monospace', monospace;
+      padding: 12px 16px;
+      line-height: 32px;
+      font-family: 'mastodon-font-display', sans-serif;
+      font-weight: 500;
+      font-size: 14px;
     }
   }
 }
diff --git a/app/javascript/styles/fonts/montserrat.scss b/app/javascript/styles/fonts/montserrat.scss
index e4012ab02..206f1865e 100644
--- a/app/javascript/styles/fonts/montserrat.scss
+++ b/app/javascript/styles/fonts/montserrat.scss
@@ -7,3 +7,11 @@
   font-weight: 400;
   font-style: normal;
 }
+
+@font-face {
+  font-family: 'mastodon-font-display';
+  src: local('Montserrat'),
+    url('../fonts/montserrat/Montserrat-Medium.ttf') format('truetype');
+  font-weight: 500;
+  font-style: normal;
+}
diff --git a/app/javascript/styles/forms.scss b/app/javascript/styles/forms.scss
index 7a181f36b..e1de36d55 100644
--- a/app/javascript/styles/forms.scss
+++ b/app/javascript/styles/forms.scss
@@ -24,6 +24,20 @@ code {
 
   p.hint {
     margin-bottom: 15px;
+    color: lighten($ui-base-color, 32%);
+
+    &.subtle-hint {
+      text-align: center;
+      font-size: 12px;
+      line-height: 18px;
+      margin-top: 15px;
+      margin-bottom: 0;
+      color: lighten($ui-base-color, 26%);
+
+      a {
+        color: $ui-primary-color;
+      }
+    }
   }
 
   strong {
@@ -43,10 +57,7 @@ code {
     }
   }
 
-  .input.file,
-  .input.select,
-  .input.radio_buttons,
-  .input.check_boxes {
+  .input.with_label {
     padding: 15px 0;
     margin-bottom: 0;
 
@@ -57,6 +68,44 @@ code {
       display: block;
       padding-top: 5px;
     }
+
+    &.boolean {
+      padding: initial;
+      margin-bottom: initial;
+
+      .label_input > label {
+        font-family: inherit;
+        font-size: 14px;
+        color: $primary-text-color;
+        display: block;
+        width: auto;
+      }
+
+      label.checkbox {
+        position: relative;
+        padding-left: 25px;
+        flex: 1 1 auto;
+      }
+    }
+  }
+
+  .input.with_block_label {
+    & > label {
+      font-family: inherit;
+      font-size: 16px;
+      color: $primary-text-color;
+      display: block;
+      padding-top: 5px;
+    }
+
+    .hint {
+      margin-bottom: 15px;
+    }
+
+    li {
+      float: left;
+      width: 50%;
+    }
   }
 
   .fields-group {
@@ -92,7 +141,7 @@ code {
     input[type=checkbox] {
       position: absolute;
       left: 0;
-      top: 1px;
+      top: 5px;
       margin: 0;
     }
 
@@ -102,6 +151,29 @@ code {
     }
   }
 
+  .check_boxes {
+    .checkbox {
+      label {
+        font-family: inherit;
+        font-size: 14px;
+        color: $primary-text-color;
+        display: block;
+        width: auto;
+        position: relative;
+        padding-top: 5px;
+        padding-left: 25px;
+        flex: 1 1 auto;
+      }
+
+      input[type=checkbox] {
+        position: absolute;
+        left: 0;
+        top: 5px;
+        margin: 0;
+      }
+    }
+  }
+
   input[type=text],
   input[type=number],
   input[type=email],
@@ -197,8 +269,6 @@ code {
 
     &:active,
     &:focus {
-      position: relative;
-      top: 1px;
       background-color: darken($ui-highlight-color, 5%);
     }
 
@@ -219,6 +289,27 @@ code {
   select {
     font-size: 16px;
   }
+
+  .input-with-append {
+    position: relative;
+
+    .input input {
+      padding-right: 127px;
+    }
+
+    .append {
+      position: absolute;
+      right: 0;
+      top: 0;
+      padding: 7px 4px;
+      padding-bottom: 9px;
+      font-size: 16px;
+      color: lighten($ui-base-color, 26%);
+      font-family: inherit;
+      pointer-events: none;
+      cursor: default;
+    }
+  }
 }
 
 .flash-message {
@@ -240,7 +331,7 @@ code {
   text-align: center;
 
   a {
-    color: $primary-text-color;
+    color: $ui-primary-color;
     text-decoration: none;
 
     &:hover {
@@ -357,21 +448,11 @@ code {
   }
 }
 
-.user_filtered_languages {
-  & > label {
-    font-family: inherit;
-    font-size: 16px;
-    color: $primary-text-color;
-    display: block;
-    padding-top: 5px;
-  }
-
-  .hint {
-    margin-bottom: 15px;
-  }
+.post-follow-actions {
+  text-align: center;
+  color: $ui-primary-color;
 
-  li {
-    float: left;
-    width: 50%;
+  div {
+    margin-bottom: 4px;
   }
 }
diff --git a/app/javascript/styles/stream_entries.scss b/app/javascript/styles/stream_entries.scss
index 8ecf31fd5..a9111d7c9 100644
--- a/app/javascript/styles/stream_entries.scss
+++ b/app/javascript/styles/stream_entries.scss
@@ -326,6 +326,18 @@
     }
   }
 
+  .media-spoiler-wrapper {
+    &.media-spoiler-wrapper__visible {
+      .media-spoiler {
+        display: none;
+      }
+
+      .spoiler-button {
+        display: block;
+      }
+    }
+  }
+
   .pre-header {
     padding: 14px 0;
     padding-left: (48px + 14px * 2);