diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 00000000..fabbe104
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,2 @@
+src/vector/modernizr.js
+src/component-index.js
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 00000000..c181384f
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+    extends: ["./node_modules/matrix-react-sdk/.eslintrc.js"],
+}
diff --git a/.gitignore b/.gitignore
index 2e34b6c1..c28df64c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,8 @@
 /node_modules
 /packages/
 /webapp
+/.npmrc
 .DS_Store
 npm-debug.log
 electron/dist
+electron/pub
diff --git a/.travis.yml b/.travis.yml
index 1fbc4dcc..af738bb4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,4 +3,5 @@ node_js:
     - 6   # node v6, to match jenkins
 install:
     - npm install
+    - (cd node_modules/matrix-js-sdk && npm install)
     - (cd node_modules/matrix-react-sdk && npm run build)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 12ebad99..ee745baa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,168 @@
-hanges in [0.8.4](https://github.com/vector-im/vector-web/releases/tag/v0.8.4) (2016-11-04)
+Changes in [0.9.6](https://github.com/vector-im/riot-web/releases/tag/v0.9.6) (2017-01-16)
+==========================================================================================
+[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.6-rc.1...v0.9.6)
+
+ * Update to matrix-js-sdk 0.9.6 for video calling fix
+
+Changes in [0.9.6-rc.1](https://github.com/vector-im/riot-web/releases/tag/v0.9.6-rc.1) (2017-01-13)
+====================================================================================================
+[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.5...v0.9.6-rc.1)
+
+ * Build the js-sdk in the CI script
+   [\#2920](https://github.com/vector-im/riot-web/pull/2920)
+ * Hopefully fix Windows shortcuts
+   [\#2917](https://github.com/vector-im/riot-web/pull/2917)
+ * Update README now the js-sdk has a transpile step
+   [\#2921](https://github.com/vector-im/riot-web/pull/2921)
+ * Use the role for 'toggle dev tools'
+   [\#2915](https://github.com/vector-im/riot-web/pull/2915)
+ * Enable screen sharing easter-egg in desktop app
+   [\#2909](https://github.com/vector-im/riot-web/pull/2909)
+ * make electron send email validation URLs with a nextlink of riot.im
+   [\#2808](https://github.com/vector-im/riot-web/pull/2808)
+ * add Debian Stretch install steps to readme
+   [\#2809](https://github.com/vector-im/riot-web/pull/2809)
+ * Update desktop build instructions fixes #2792
+   [\#2793](https://github.com/vector-im/riot-web/pull/2793)
+ * CSS for the delete threepid button
+   [\#2784](https://github.com/vector-im/riot-web/pull/2784)
+
+Changes in [0.9.5](https://github.com/vector-im/riot-web/releases/tag/v0.9.5) (2016-12-24)
+==========================================================================================
+[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.4...v0.9.5)
+
+ * make electron send email validation URLs with a nextlink of riot.im rather than file:///
+ * add gnu-tar to debian electron build deps
+ * fix win32 shortcut in start menu
+
+Changes in [0.9.4](https://github.com/vector-im/riot-web/releases/tag/v0.9.4) (2016-12-22)
+==========================================================================================
+[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.3...v0.9.4)
+
+ * Update to libolm 2.1.0. This should help resolve a problem with browser
+   sessions being logged out ([\#2726](https://github.com/vector-im/riot-web/issues/2726)).
+
+Changes in [0.9.3](https://github.com/vector-im/riot-web/releases/tag/v0.9.3) (2016-12-22)
+==========================================================================================
+[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.2...v0.9.3)
+
+ * (from matrix-react-sdk) Fix regression where the date separator would be displayed
+   at the wrong time of day.
+ * README.md: fix GFMD for nativefier
+   [\#2755](https://github.com/vector-im/riot-web/pull/2755)
+
+Changes in [0.9.2](https://github.com/vector-im/riot-web/releases/tag/v0.9.2) (2016-12-16)
+==========================================================================================
+[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.1...v0.9.2)
+
+ * Remove the client side filtering from the room dir
+   [\#2750](https://github.com/vector-im/riot-web/pull/2750)
+ * Configure olm memory size
+   [\#2745](https://github.com/vector-im/riot-web/pull/2745)
+ * Support room dir 3rd party network filtering
+   [\#2747](https://github.com/vector-im/riot-web/pull/2747)
+
+Changes in [0.9.1](https://github.com/vector-im/riot-web/releases/tag/v0.9.1) (2016-12-09)
+==========================================================================================
+[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.1-rc.2...v0.9.1)
+
+ * Update README to say how to build the desktop app
+   [\#2732](https://github.com/vector-im/riot-web/pull/2732)
+ * Add signing ID in release_config.yaml
+   [\#2731](https://github.com/vector-im/riot-web/pull/2731)
+ * Makeover!
+   [\#2722](https://github.com/vector-im/riot-web/pull/2722)
+ * Fix broken tests
+   [\#2730](https://github.com/vector-im/riot-web/pull/2730)
+ * Make the 'loading' tests work in isolation
+   [\#2727](https://github.com/vector-im/riot-web/pull/2727)
+ * Use a PNG for the icon on non-Windows
+   [\#2708](https://github.com/vector-im/riot-web/pull/2708)
+ * Add missing brackets to call to toUpperCase
+   [\#2703](https://github.com/vector-im/riot-web/pull/2703)
+
+Changes in [0.9.1-rc.2](https://github.com/vector-im/riot-web/releases/tag/v0.9.1-rc.2) (2016-12-06)
+====================================================================================================
+[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.1-rc.1...v0.9.1-rc.2)
+
+ * Fix clicking on notifications
+   [\#2700](https://github.com/vector-im/riot-web/pull/2700)
+ * Desktop app: Only show window when ready
+   [\#2697](https://github.com/vector-im/riot-web/pull/2697)
+
+Changes in [0.9.1-rc.1](https://github.com/vector-im/riot-web/releases/tag/v0.9.1-rc.1) (2016-12-05)
+====================================================================================================
+[Full Changelog](https://github.com/vector-im/riot-web/compare/v0.9.0...v0.9.1-rc.1)
+
+ * Final bits to prepare electron distribtion:
+   [\#2653](https://github.com/vector-im/riot-web/pull/2653)
+ * Update name & repo to reflect renamed repository
+   [\#2692](https://github.com/vector-im/riot-web/pull/2692)
+ * Document cross_origin_renderer_url
+   [\#2680](https://github.com/vector-im/riot-web/pull/2680)
+ * Add css for the iframes for e2e attachments
+   [\#2659](https://github.com/vector-im/riot-web/pull/2659)
+ * Fix config location in some more places
+   [\#2670](https://github.com/vector-im/riot-web/pull/2670)
+ * CSS updates for s/block/blacklist for e2e
+   [\#2662](https://github.com/vector-im/riot-web/pull/2662)
+ * Update to electron 1.4.8
+   [\#2660](https://github.com/vector-im/riot-web/pull/2660)
+ * Add electron config
+   [\#2644](https://github.com/vector-im/riot-web/pull/2644)
+ * Move getDefaultDeviceName into the Platforms
+   [\#2643](https://github.com/vector-im/riot-web/pull/2643)
+ * Add Freenode & Mozilla domains
+   [\#2641](https://github.com/vector-im/riot-web/pull/2641)
+ * Include config.sample.json in dist tarball
+   [\#2614](https://github.com/vector-im/riot-web/pull/2614)
+
+Changes in [0.9.0](https://github.com/vector-im/vector-web/releases/tag/v0.9.0) (2016-11-19)
+============================================================================================
+[Full Changelog](https://github.com/vector-im/vector-web/compare/v0.8.4...v0.9.0)
+
+ * Add a cachebuster to /version
+   [\#2596](https://github.com/vector-im/vector-web/pull/2596)
+ * Add a 'View decrypted source' button
+   [\#2587](https://github.com/vector-im/vector-web/pull/2587)
+ * Fix changelog dialog to  read new version format
+   [\#2577](https://github.com/vector-im/vector-web/pull/2577)
+ * Build all of the vector dir in the build process
+   [\#2558](https://github.com/vector-im/vector-web/pull/2558)
+ * Support for get_app_version
+   [\#2553](https://github.com/vector-im/vector-web/pull/2553)
+ * Add CSS for mlist truncation
+   [\#2565](https://github.com/vector-im/vector-web/pull/2565)
+ * Add menu option for `external_url` if present
+   [\#2560](https://github.com/vector-im/vector-web/pull/2560)
+ * Make auto-update configureable
+   [\#2555](https://github.com/vector-im/vector-web/pull/2555)
+ * Missed files electron windows fixes
+   [\#2556](https://github.com/vector-im/vector-web/pull/2556)
+ * Add some CSS for  scalar error popup
+   [\#2554](https://github.com/vector-im/vector-web/pull/2554)
+ * Catch unhandled errors in the electron process
+   [\#2552](https://github.com/vector-im/vector-web/pull/2552)
+ * Slight grab-bag of fixes for electron on Windows
+   [\#2551](https://github.com/vector-im/vector-web/pull/2551)
+ * Electron app (take 3)
+   [\#2535](https://github.com/vector-im/vector-web/pull/2535)
+ * Use webpack-dev-server instead of http-server
+   [\#2542](https://github.com/vector-im/vector-web/pull/2542)
+ * Better support no-config when loading from file
+   [\#2541](https://github.com/vector-im/vector-web/pull/2541)
+ * Fix loading with no config from HTTP
+   [\#2540](https://github.com/vector-im/vector-web/pull/2540)
+ * Move 'new version' support into Platform
+   [\#2532](https://github.com/vector-im/vector-web/pull/2532)
+ * Add Notification support to the Web Platform
+   [\#2533](https://github.com/vector-im/vector-web/pull/2533)
+ * Use the defaults if given a blank config file
+   [\#2534](https://github.com/vector-im/vector-web/pull/2534)
+ * Implement Platforms
+   [\#2531](https://github.com/vector-im/vector-web/pull/2531)
+
+Changes in [0.8.4](https://github.com/vector-im/vector-web/releases/tag/v0.8.4) (2016-11-04)
 ============================================================================================
 [Full Changelog](https://github.com/vector-im/vector-web/compare/v0.8.4-rc.2...v0.8.4)
 
diff --git a/README.md b/README.md
index 9d1fb48c..fd6acfd7 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ https://riot.im/develop for those who like living dangerously.
 To host your own copy of Riot, the quickest bet is to use a pre-built
 released version of Riot:
 
-1. Download the latest version from https://github.com/vector-im/vector-web/releases
+1. Download the latest version from https://github.com/vector-im/riot-web/releases
 1. Untar the tarball on your web server
 1. Move (or symlink) the vector-x.x.x directory to an appropriate name
 1. If desired, copy `config.sample.json` to `config.json` and edit it
@@ -25,6 +25,14 @@ Note that Chrome does not allow microphone or webcam access for sites served
 over http (except localhost), so for working VoIP you will need to serve Riot
 over https.
 
+### Installation Steps for Debian Stretch
+1. Add the repository to your sources.list using either of the following two options:
+  - Directly to sources.list: `echo "deb https://riot.im/packages/debian/ stretch main" | sudo tee -a /etc/apt/sources.list`
+  - As a separate entry in sources.list.d: `echo "deb https://riot.im/packages/debian/ stretch main" | sudo tee /etc/apt/sources.list.d/riot.list`
+2. Add the gpg signing key for the riot repository: `curl -s https://riot.im/packages/debian/repo-key.asc | sudo apt-key add -`
+3. Update your package lists: `sudo apt-get update`
+4. Install Riot: `sudo apt-get install riot-web`
+
 Important Security Note
 =======================
 
@@ -36,7 +44,7 @@ access to Riot (or other apps) due to sharing the same domain.
 
 We have put some coarse mitigations into place to try to protect against this
 situation, but it's still not good practice to do it in the first place.  See
-https://github.com/vector-im/vector-web/issues/1977 for more details.
+https://github.com/vector-im/riot-web/issues/1977 for more details.
 
 Building From Source
 ====================
@@ -45,13 +53,17 @@ Riot is a modular webapp built with modern ES6 and requires a npm build system
 to build.
 
 1. Install or update `node.js` so that your `npm` is at least at version `2.0.0`
-1. Clone the repo: `git clone https://github.com/vector-im/vector-web.git`
-1. Switch to the vector-web directory: `cd vector-web`
+1. Clone the repo: `git clone https://github.com/vector-im/riot-web.git`
+1. Switch to the riot-web directory: `cd riot-web`
 1. Install the prerequisites: `npm install`
 1. If you are using the `develop` branch of vector-web, you will probably need
-   to rebuild one of the dependencies, due to
-   https://github.com/npm/npm/issues/3055: `(cd node_modules/matrix-react-sdk
-   && npm install)`
+   to rebuild some of the dependencies, due to
+   https://github.com/npm/npm/issues/3055:
+
+   ```
+   (cd node_modules/matrix-js-sdk && npm install)
+   (cd node_modules/matrix-react-sdk && npm install)
+   ```
 1. Configure the app by copying `config.sample.json` to `config.json` and
    modifying it (see below for details)
 1. `npm run dist` to build a tarball to deploy. Untaring this file will give
@@ -59,16 +71,16 @@ to build.
    web server.
 
 Note that `npm run dist` is not supported on Windows, so Windows users can run `npm
-run build`, which will build all the necessary files into the `vector`
-directory. The version of Vector will not appear in Settings without
-using the dist script. You can then mount the vector directory on your
+run build`, which will build all the necessary files into the `webapp`
+directory. The version of Riot will not appear in Settings without
+using the dist script. You can then mount the `webapp` directory on your
 webserver to actually serve up the app, which is entirely static content.
 
 config.json
 ===========
 
-You can configure the app by copying `vector/config.sample.json` to
-`vector/config.json` and customising it:
+You can configure the app by copying `config.sample.json` to
+`config.json` and customising it:
 
 1. `default_hs_url` is the default home server url.
 1. `default_is_url` is the default identity server url (this is the server used
@@ -81,55 +93,73 @@ You can configure the app by copying `vector/config.sample.json` to
    and https://vector.im.  In future identity servers will be decentralised.
 1. `integrations_ui_url`: URL to the web interface for the integrations server.
 1. `integrations_rest_url`: URL to the REST interface for the integrations server.
-1. `roomDirectory`: config for the public room directory. This section encodes behaviour
-   on the room directory screen for filtering the list by server / network type and joining
-   third party networks. This config section will disappear once APIs are available to
-   get this information for home servers. This section is optional.
+1. `roomDirectory`: config for the public room directory. This section is optional.
 1. `roomDirectory.servers`: List of other Home Servers' directories to include in the drop
    down list. Optional.
-1. `roomDirectory.serverConfig`: Config for each server in `roomDirectory.servers`. Optional.
-1. `roomDirectory.serverConfig.<server_name>.networks`: List of networks (named
-   in `roomDirectory.networks`) to include for this server. Optional.
-1. `roomDirectory.networks`: config for each network type. Optional.
-1. `roomDirectory.<network_type>.name`: Human-readable name for the network. Required.
-1. `roomDirectory.<network_type>.protocol`: Protocol as given by the server in
-   `/_matrix/client/unstable/thirdparty/protocols` response. Required to be able to join
-   this type of third party network.
-1. `roomDirectory.<network_type>.domain`: Domain as given by the server in
-   `/_matrix/client/unstable/thirdparty/protocols` response, if present. Required to be
-   able to join this type of third party network, if present in `thirdparty/protocols`.
-1. `roomDirectory.<network_type>.portalRoomPattern`: Regular expression matching aliases
-   for portal rooms to locations on this network. Required.
-1. `roomDirectory.<network_type>.icon`: URL to an icon to be displayed for this network. Required.
-1. `roomDirectory.<network_type>.example`: Textual example of a location on this network,
-   eg. '#channel' for an IRC network. Optional.
-1. `roomDirectory.<network_type>.nativePattern`: Regular expression that matches a
-   valid location on this network. This is used as a hint to the user to indicate
-   when a valid location has been entered so it's not necessary for this to be
-   exactly correct. Optional.
+1. `update_base_url` (electron app only): HTTPS URL to a web server to download
+   updates from. This should be the path to the directory containing `macos`
+   and `win32` (for update packages, not installer packages).
+1. `cross_origin_renderer_url`: URL to a static HTML page hosting code to help display
+   encrypted file attachments. This MUST be hosted on a completely separate domain to
+   anything else since it is used to isolate the privileges of file attachments to this
+   domain. Default: `usercontent.riot.im`. This needs to contain v1.html from
+   https://github.com/matrix-org/usercontent/blob/master/v1.html
 
 Running as a Desktop app
 ========================
 
-In future we'll do an official distribution of Riot as an desktop app.  Meanwhile,
-there are a few options:
+Riot can also be run as a desktop app, wrapped in electron. You can download a
+pre-built version from https://riot.im/desktop.html or, if you prefer,
+built it yourself.
 
-@asdf:matrix.org points out that you can use nativefier and it just works(tm):
+To run as a desktop app:
+
+1. Follow the instructions in 'Building From Source' above
+2. Install electron and run it:
+
+   ```
+   npm install electron
+   node_modules/.bin/electron .
+   ```
+
+To build packages, use electron-builder. This is configured to output:
+ * dmg + zip for macOS
+ * exe + nupkg for Windows
+ * deb for Linux
+But this can be customised by editing the `build` section of package.json
+as per https://github.com/electron-userland/electron-builder/wiki/Options
+
+See https://github.com/electron-userland/electron-builder/wiki/Multi-Platform-Build
+for dependencies required for building packages for various platforms.
+
+The only platform that can build packages for all three platforms is macOS:
+```
+brew install wine --without-x11
+brew install mono
+brew install gnu-tar
+npm install
+npm run build:electron
+```
+
+For other packages, use electron-builder manually. For example, to build a package
+for 64 bit Linux:
+
+ 1. Follow the instructions in 'Building From Source' above
+ 2. `node_modules/.bin/build -l --x64`
+
+All electron packages go into `electron/dist/`
+
+Many thanks to @aviraldg for the initial work on the electron integration.
+
+Other options for running as a desktop app:
+ * https://github.com/krisak/vector-electron-desktop
+ * @asdf:matrix.org points out that you can use nativefier and it just works(tm)
 
 ```
 sudo npm install nativefier -g
 nativefier https://riot.im/app/
 ```
 
-krisa has a dedicated electron project at
-https://github.com/krisak/vector-electron-desktop (although you should swap out
-the 'vector' folder for the latest vector tarball you want to run.  Get a
-tarball from https://github.com/vector-im/vector-web/releases or build your own
-- see Building From Source above).
-
-There's also a (much) older electron distribution at https://github.com/stevenhammerton/vector-desktop
-
-
 Development
 ===========
 
@@ -149,13 +179,13 @@ the `component-index.js` for the app (used in future for skinning)
 development on Riot forcing `matrix-react-sdk` to move fast at the expense of
 maintaining a clear abstraction between the two.**  Hacking on Riot inevitably
 means hacking equally on `matrix-react-sdk`, and there are bits of
-`matrix-react-sdk` behaviour incorrectly residing in the `vector-web` project
+`matrix-react-sdk` behaviour incorrectly residing in the `riot-web` project
 (e.g. matrix-react-sdk specific CSS), and a bunch of Riot specific behaviour
 in the `matrix-react-sdk` (grep for `vector` / `riot`).  This separation problem will be
 solved asap once development on Riot (and thus matrix-react-sdk) has
 stabilised.  Until then, the two projects should basically be considered as a
 single unit.  In particular, `matrix-react-sdk` issues are currently filed
-against `vector-web` in github.
+against `riot-web` in github.
 
 Please note that Riot is intended to run correctly without access to the public
 internet.  So please don't depend on resources (JS libs, CSS, images, fonts)
@@ -190,8 +220,8 @@ Then similarly with `matrix-react-sdk`:
 
 Finally, build and start Riot itself:
 
-1. `git clone git@github.com:vector-im/vector-web.git`
-1. `cd vector-web`
+1. `git clone git@github.com:vector-im/riot-web.git`
+1. `cd riot-web`
 1. `git checkout develop`
 1. `npm install`
 1. `rm -r node_modules/matrix-js-sdk; ln -s ../../matrix-js-sdk node_modules/`
@@ -215,10 +245,10 @@ Finally, build and start Riot itself:
    disables caching, so do NOT use it in production.
 1. Open http://127.0.0.1:8080/ in your browser to see your newly built Riot.
 
-When you make changes to `matrix-react-sdk`, you will need to run `npm run
-build` in the relevant directory. You can do this automatically by instead
-running `npm start` in the directory, to start a development builder which
-will watch for changes to the files and rebuild automatically.
+When you make changes to `matrix-react-sdk` or `matrix-js-sdk`, you will need
+to run `npm run build` in the relevant directory. You can do this automatically
+by instead running `npm start` in the directory, to start a development builder
+which will watch for changes to the files and rebuild automatically.
 
 If you add or remove any components from the Riot skin, you will need to rebuild
 the skin's index by running, `npm run reskindex`.
diff --git a/config.sample.json b/config.sample.json
index 49303769..a65646ac 100644
--- a/config.sample.json
+++ b/config.sample.json
@@ -4,64 +4,11 @@
     "brand": "Riot",
     "integrations_ui_url": "https://scalar.vector.im/",
     "integrations_rest_url": "https://scalar.vector.im/api",
+    "bug_report_endpoint_url": "https://vector.im/bugs",
     "enableLabs": true,
     "roomDirectory": {
         "servers": [
             "matrix.org"
-        ],
-        "serverConfig": {
-            "matrix.org": {
-                "networks": [
-                    "_matrix",
-                    "gitter",
-                    "irc:freenode",
-                    "irc:mozilla",
-                    "irc:snoonet",
-                    "irc:oftc"
-                ]
-            }
-        },
-        "networks": {
-            "gitter": {
-                "protocol": "gitter",
-                "portalRoomPattern": "#gitter_.*:matrix.org",
-                "name": "Gitter",
-                "icon": "//gitter.im/favicon.ico",
-                "example": "org/community",
-                "nativePattern": "[^\\s]+/[^\\s]+$"
-            },
-            "irc:freenode": {
-                "portalRoomPattern": "#freenode_.*:matrix.org",
-                "name": "Freenode",
-                "icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX",
-                "example": "#channel",
-                "nativePattern": "^#[^\\s]+$"
-            },
-            "irc:mozilla": {
-                "portalRoomPattern": "#mozilla_.*:matrix.org",
-                "name": "Mozilla",
-                "icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX",
-                "example": "#channel",
-                "nativePattern": "^#[^\\s]+$"
-            },
-            "irc:snoonet": {
-                "protocol": "irc",
-                "domain": "ipv6-irc.snoonet.org",
-                "portalRoomPattern": "#_snoonet_.*:matrix.org",
-                "name": "Snoonet",
-                "icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX",
-                "example": "#channel",
-                "nativePattern": "^#[^\\s]+$"
-            },
-            "irc:oftc": {
-                "protocol": "irc",
-                "domain": "irc.oftc.net",
-                "portalRoomPattern": "#_oftc_.*:matrix.org",
-                "name": "OFTC",
-                "icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX",
-                "example": "#channel",
-                "nativePattern": "^#[^\\s]+$"
-            }
-        }
+        ]
     }
 }
diff --git a/docs/theming.md b/docs/theming.md
new file mode 100644
index 00000000..c6373e52
--- /dev/null
+++ b/docs/theming.md
@@ -0,0 +1,25 @@
+Theming Riot
+============
+
+Themes are a very basic way of providing simple alternative look & feels to the
+riot-web app via CSS & custom imagery.
+
+They are *NOT* co be confused with 'skins', which describe apps which sit on top
+of matrix-react-sdk - e.g. in theory Riot itself is a react-sdk skin.
+As of Jan 2017, skins are not fully supported; riot is the only available skin.
+
+To define a theme for Riot:
+
+ 1. Pick a name, e.g. `teal`. at time of writing we have `light` and `dark`.
+ 2. Fork `src/skins/vector/css/themes/dark.scss` to be teal.scss
+ 3. Fork `src/skins/vector/css/themes/_base.scss` to be _teal.scss
+ 4. Override variables in _teal.scss as desired. You may wish to delete ones
+    which don't differ from _base.scss, to make it clear which are being
+    overridden. If every single colour is being changed (as per _dark.scss)
+    then you might as well keep them all.
+ 5. Add the theme to the list of entrypoints in webpack.config.js
+ 6. Add the theme to the list of themes in matrix-react-sdk's UserSettings.js
+ 7. Sit back and admire your handywork.
+
+In future, the assets for a theme will probably be gathered together into a
+single directory tree.
diff --git a/electron/build/icon.icns b/electron/build/icon.icns
index 55c03d96..d74e97e7 100644
Binary files a/electron/build/icon.icns and b/electron/build/icon.icns differ
diff --git a/electron/build/icon.ico b/electron/build/icon.ico
index 8b681ffb..8f8ff94e 100644
Binary files a/electron/build/icon.ico and b/electron/build/icon.ico differ
diff --git a/electron/build/icons/128x128.png b/electron/build/icons/128x128.png
new file mode 100644
index 00000000..54149b1a
Binary files /dev/null and b/electron/build/icons/128x128.png differ
diff --git a/electron/build/icons/16x16.png b/electron/build/icons/16x16.png
new file mode 100644
index 00000000..def4ec5e
Binary files /dev/null and b/electron/build/icons/16x16.png differ
diff --git a/electron/build/icons/24x24.png b/electron/build/icons/24x24.png
new file mode 100644
index 00000000..6fe144ea
Binary files /dev/null and b/electron/build/icons/24x24.png differ
diff --git a/electron/build/icons/256x256.png b/electron/build/icons/256x256.png
new file mode 100644
index 00000000..563e6d5e
Binary files /dev/null and b/electron/build/icons/256x256.png differ
diff --git a/electron/build/icons/48x48.png b/electron/build/icons/48x48.png
new file mode 100644
index 00000000..9cd225d4
Binary files /dev/null and b/electron/build/icons/48x48.png differ
diff --git a/electron/build/icons/512x512.png b/electron/build/icons/512x512.png
new file mode 100644
index 00000000..328a723c
Binary files /dev/null and b/electron/build/icons/512x512.png differ
diff --git a/electron/build/icons/64x64.png b/electron/build/icons/64x64.png
new file mode 100644
index 00000000..20a7a6cf
Binary files /dev/null and b/electron/build/icons/64x64.png differ
diff --git a/electron/build/icons/96x96.png b/electron/build/icons/96x96.png
new file mode 100644
index 00000000..eb48c2c9
Binary files /dev/null and b/electron/build/icons/96x96.png differ
diff --git a/electron/img/riot.png b/electron/img/riot.png
new file mode 100644
index 00000000..fe13aa99
Binary files /dev/null and b/electron/img/riot.png differ
diff --git a/electron/riot.im/README b/electron/riot.im/README
new file mode 100644
index 00000000..09c21874
--- /dev/null
+++ b/electron/riot.im/README
@@ -0,0 +1,4 @@
+This directory contains the config file for the official riot.im distribution
+of Riot Desktop. You probably do not want to build with this config unless
+you're building the official riot.im distribution, or you'll find your builds
+will replace themselves with the riot.im build.
diff --git a/electron/riot.im/config.json b/electron/riot.im/config.json
new file mode 100644
index 00000000..e129e5bf
--- /dev/null
+++ b/electron/riot.im/config.json
@@ -0,0 +1,72 @@
+{
+    "update_base_url": "https://riot.im/download/desktop/update/",
+    "default_hs_url": "https://matrix.org",
+    "default_is_url": "https://vector.im",
+    "brand": "Riot",
+    "integrations_ui_url": "https://scalar.vector.im/",
+    "integrations_rest_url": "https://scalar.vector.im/api",
+    "enableLabs": true,
+    "roomDirectory": {
+        "servers": [
+            "matrix.org"
+        ],
+        "serverConfig": {
+            "matrix.org": {
+                "networks": [
+                    "_matrix",
+                    "gitter",
+                    "irc:freenode",
+                    "irc:mozilla",
+                    "irc:snoonet",
+                    "irc:oftc"
+                ]
+            }
+        },
+        "networks": {
+            "gitter": {
+                "protocol": "gitter",
+                "portalRoomPattern": "#gitter_.*:matrix.org",
+                "name": "Gitter",
+                "icon": "https://gitter.im/favicon.ico",
+                "example": "org/community",
+                "nativePattern": "[^\\s]+/[^\\s]+$"
+            },
+            "irc:freenode": {
+                "protocol": "irc",
+                "domain": "chat.freenode.net",
+                "portalRoomPattern": "#freenode_.*:matrix.org",
+                "name": "Freenode",
+                "icon": "https://matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX",
+                "example": "#channel",
+                "nativePattern": "^#[^\\s]+$"
+            },
+            "irc:mozilla": {
+                "protocol": "irc",
+                "domain": "chat.freenode.net",
+                "portalRoomPattern": "#mozilla_.*:matrix.org",
+                "name": "Mozilla",
+                "icon": "https://matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX",
+                "example": "#channel",
+                "nativePattern": "^#[^\\s]+$"
+            },
+            "irc:snoonet": {
+                "protocol": "irc",
+                "domain": "ipv6-irc.snoonet.org",
+                "portalRoomPattern": "#_snoonet_.*:matrix.org",
+                "name": "Snoonet",
+                "icon": "https://matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX",
+                "example": "#channel",
+                "nativePattern": "^#[^\\s]+$"
+            },
+            "irc:oftc": {
+                "protocol": "irc",
+                "domain": "irc.oftc.net",
+                "portalRoomPattern": "#_oftc_.*:matrix.org",
+                "name": "OFTC",
+                "icon": "https://matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX",
+                "example": "#channel",
+                "nativePattern": "^#[^\\s]+$"
+            }
+        }
+    }
+}
diff --git a/electron/src/electron-main.js b/electron/src/electron-main.js
index 18758eee..33b44ce9 100644
--- a/electron/src/electron-main.js
+++ b/electron/src/electron-main.js
@@ -26,11 +26,13 @@ if (check_squirrel_hooks()) return;
 const electron = require('electron');
 const url = require('url');
 
+const tray = require('./tray');
+
 const VectorMenu = require('./vectormenu');
 
 let vectorConfig = {};
 try {
-    vectorConfig = require('../../vector/config.json');
+    vectorConfig = require('../../webapp/config.json');
 } catch (e) {
     // it would be nice to check the error code here and bail if the config
     // is unparseable, but we get MODULE_NOT_FOUND in the case of a missing
@@ -101,9 +103,9 @@ function pollForUpdates() {
     }
 }
 
-function startAutoUpdate(update_url) {
-    if (update_url.slice(-1) !== '/') {
-        update_url = update_url + '/';
+function startAutoUpdate(update_base_url) {
+    if (update_base_url.slice(-1) !== '/') {
+        update_base_url = update_base_url + '/';
     }
     try {
         // For reasons best known to Squirrel, the way it checks for updates
@@ -112,9 +114,18 @@ function startAutoUpdate(update_url) {
         // 204 No Content. On windows it takes a base path and looks for
         // files under that path.
         if (process.platform == 'darwin') {
-            electron.autoUpdater.setFeedURL(update_url);
+            // include the current version in the URL we hit. Electron doesn't add
+            // it anywhere (apart from the User-Agent) so it's up to us. We could
+            // (and previously did) just use the User-Agent, but this doesn't
+            // rely on NSURLConnection setting the User-Agent to what we expect,
+            // and also acts as a convenient cache-buster to ensure that when the
+            // app updates it always gets a fresh value to avoid update-looping.
+            electron.autoUpdater.setFeedURL(
+                update_base_url +
+                'macos/?localVersion=' + encodeURIComponent(electron.app.getVersion())
+            );
         } else if (process.platform == 'win32') {
-            electron.autoUpdater.setFeedURL(update_url + 'win32/');
+            electron.autoUpdater.setFeedURL(update_base_url + 'win32/' + process.arch + '/');
         } else {
             // Squirrel / electron only supports auto-update on these two platforms.
             // I'm not even going to try to guess which feed style they'd use if they
@@ -148,26 +159,56 @@ process.on('uncaughtException', function (error) {
 
 electron.ipcMain.on('install_update', installUpdate);
 
+electron.app.commandLine.appendSwitch('--enable-usermedia-screen-capturing');
+
+const shouldQuit = electron.app.makeSingleInstance((commandLine, workingDirectory) => {
+    // Someone tried to run a second instance, we should focus our window.
+    if (mainWindow) {
+        if (!mainWindow.isVisible()) mainWindow.show();
+        if (mainWindow.isMinimized()) mainWindow.restore();
+        mainWindow.focus();
+    }
+});
+
+if (shouldQuit) {
+    electron.app.quit()
+}
+
 electron.app.on('ready', () => {
-    if (vectorConfig.update_url) {
-        console.log("Starting auto update with URL: " + vectorConfig.update_url);
-        startAutoUpdate(vectorConfig.update_url);
+    if (vectorConfig.update_base_url) {
+        console.log("Starting auto update with base URL: " + vectorConfig.update_base_url);
+        startAutoUpdate(vectorConfig.update_base_url);
     } else {
-        console.log("No update_url is defined: auto update is disabled");
+        console.log("No update_base_url is defined: auto update is disabled");
     }
 
+    const icon_path = `${__dirname}/../img/riot.` + (
+        process.platform == 'win32' ? 'ico' : 'png'
+    );
+
     mainWindow = new electron.BrowserWindow({
-        icon: `${__dirname}/../img/riot.ico`,
+        icon: icon_path,
         width: 1024, height: 768,
+        show: false,
+        autoHideMenuBar: true,
     });
     mainWindow.loadURL(`file://${__dirname}/../../webapp/index.html`);
     electron.Menu.setApplicationMenu(VectorMenu);
 
+    // Create trayIcon icon
+    tray.create(mainWindow, {
+        icon_path: icon_path,
+        brand: vectorConfig.brand || 'Riot'
+    });
+
+    mainWindow.once('ready-to-show', () => {
+        mainWindow.show();
+    });
     mainWindow.on('closed', () => {
         mainWindow = null;
     });
     mainWindow.on('close', (e) => {
-        if (process.platform == 'darwin' && !appQuitting) {
+        if (!appQuitting && (tray.hasTray() || process.platform == 'darwin')) {
             // On Mac, closing the window just hides it
             // (this is generally how single-window Mac apps
             // behave, eg. Mail.app)
@@ -198,3 +239,9 @@ electron.app.on('activate', () => {
 electron.app.on('before-quit', () => {
     appQuitting = true;
 });
+
+// Set the App User Model ID to match what the squirrel
+// installer uses for the shortcut icon.
+// This makes notifications work on windows 8.1 (and is
+// a noop on other platforms).
+electron.app.setAppUserModelId('com.squirrel.riot-web.Riot');
diff --git a/electron/src/squirrelhooks.js b/electron/src/squirrelhooks.js
index 10fb8d9e..15ed670f 100644
--- a/electron/src/squirrelhooks.js
+++ b/electron/src/squirrelhooks.js
@@ -1,9 +1,30 @@
+/*
+Copyright 2017 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
 const path = require('path');
 const spawn = require('child_process').spawn;
 const app = require('electron').app;
 
 function run_update_exe(args, done) {
+    // Invokes Squirrel's Update.exe which will do things for us like create shortcuts
+    // Note that there's an Update.exe in the app-x.x.x directory and one in the parent
+    // directory: we need to run the one in the parent directory, because it discovers
+    // information about the app by inspecting the directory it's run from.
     const updateExe = path.resolve(path.dirname(process.execPath), '..', 'Update.exe');
+    console.log('Spawning `%s` with args `%s`', updateExe, args);
     spawn(updateExe, args, {
       detached: true
     }).on('close', done);
diff --git a/electron/src/tray.js b/electron/src/tray.js
new file mode 100644
index 00000000..2ccdf40c
--- /dev/null
+++ b/electron/src/tray.js
@@ -0,0 +1,67 @@
+/*
+Copyright 2017 Karl Glatz <karl@glatz.biz>
+Copyright 2017 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+const path = require('path');
+const electron = require('electron');
+
+const app = electron.app;
+const Tray = electron.Tray;
+const MenuItem = electron.MenuItem;
+
+let trayIcon = null;
+
+exports.hasTray = function hasTray() {
+    return (trayIcon !== null);
+}
+
+exports.create = function (win, config) {
+    // no trays on darwin
+    if (process.platform === 'darwin' || trayIcon) {
+        return;
+    }
+
+    const toggleWin = function () {
+        if (win.isVisible() && !win.isMinimized()) {
+            win.hide();
+        } else {
+            if (win.isMinimized()) win.restore();
+            if (!win.isVisible()) win.show();
+            win.focus();
+        }
+    };
+
+    const contextMenu = electron.Menu.buildFromTemplate([
+        {
+            label: 'Show/Hide ' + config.brand,
+            click: toggleWin
+        },
+        {
+            type: 'separator'
+        },
+        {
+            label: 'Quit',
+            click: function () {
+                app.quit();
+            }
+        }
+    ]);
+
+    trayIcon = new Tray(config.icon_path);
+    trayIcon.setToolTip(config.brand);
+    trayIcon.setContextMenu(contextMenu);
+    trayIcon.on('click', toggleWin);
+};
diff --git a/electron/src/vectormenu.js b/electron/src/vectormenu.js
index f4d55c15..70ed3ac3 100644
--- a/electron/src/vectormenu.js
+++ b/electron/src/vectormenu.js
@@ -72,11 +72,7 @@ const template = [
                 role: 'togglefullscreen'
             },
             {
-                label: 'Toggle Developer Tools',
-                accelerator: process.platform == 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
-                click: function(item, focusedWindow) {
-                    if (focusedWindow) focusedWindow.toggleDevTools();
-                }
+                role: 'toggledevtools'
             }
         ]
     },
diff --git a/karma.conf.js b/karma.conf.js
index 24742169..901832c7 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -29,12 +29,22 @@ module.exports = function (config) {
         files: [
             'node_modules/babel-polyfill/browser.js',
             testFile,
-            {pattern: 'vector/img/*', watched: false, included: false, served: true, nocache: false},
+
+            // make the images available via our httpd. They will be avaliable
+            // below http://localhost:[PORT]/base/. See also `proxies` which
+            // defines alternative URLs for them.
+            //
+            // This isn't required by any of the tests, but it stops karma
+            // logging warnings when it serves a 404 for them.
+            {
+                pattern: 'src/skins/vector/img/*',
+                watched: false, included: false, served: true, nocache: false,
+            },
         ],
 
-        // redirect img links to the karma server
         proxies: {
-            "/img/": "/base/vector/img/",
+            // redirect img links to the karma server. See above.
+            "/img/": "/base/src/skins/vector/img/",
         },
 
         // preprocess matching files before serving them to the browser
@@ -86,6 +96,12 @@ module.exports = function (config) {
 
         webpack: {
             module: {
+                preLoaders: [
+                    // use the source-map-loader for javascript. This means
+                    // that we have a better chance of seeing line numbers from
+                    // the pre-babeled source.
+                    { test: /\.js$/, loader: "source-map-loader" },
+                ],
                 loaders: [
                     { test: /\.json$/, loader: "json" },
                     {
diff --git a/package.json b/package.json
index 173b9e1d..915056e6 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,13 @@
 {
-  "name": "vector-web",
+  "name": "riot-web",
   "productName": "Riot",
   "main": "electron/src/electron-main.js",
-  "version": "0.8.4",
+  "version": "0.9.6",
   "description": "A feature-rich client for Matrix.org",
   "author": "Vector Creations Ltd.",
   "repository": {
     "type": "git",
-    "url": "https://github.com/vector-im/vector-web"
+    "url": "https://github.com/vector-im/riot-web"
   },
   "license": "Apache-2.0",
   "files": [
@@ -27,27 +27,23 @@
   "matrix-react-parent": "matrix-react-sdk",
   "scripts": {
     "reskindex": "reskindex -h src/header",
-    "build:res": "cpx \"{src/skins/vector/fonts,src/skins/vector/img}/**\" webapp/ && cpx \"{res/media,res/vector-icons}/**\" webapp/",
-    "build:config": "cpx config.json webapp/",
-    "build:emojione": "cpx \"node_modules/emojione/assets/svg/*\" webapp/emojione/svg/",
+    "build:res": "node scripts/copy-res.js",
     "build:modernizr": "modernizr -c .modernizr.json -d src/vector/modernizr.js",
-    "build:css": "mkdirp build && catw \"src/skins/vector/css/**/*.css\" -o build/components.css --no-watch",
     "build:compile": "babel --source-maps -d lib src",
     "build:bundle": "NODE_ENV=production webpack -p --progress",
     "build:bundle:dev": "webpack --optimize-occurence-order --progress",
-    "build:electron": "build -lwm",
-    "build": "node scripts/babelcheck.js && npm run build:res && npm run build:config && npm run build:emojione && npm run build:css && npm run build:bundle",
-    "build:dev": "node scripts/babelcheck.js && npm run build:res && npm run build:config && npm run build:emojione && npm run build:css && npm run build:bundle:dev",
+    "build:electron": "npm run clean && npm run build && build -wml --ia32 --x64",
+    "build": "node scripts/babelcheck.js && npm run build:res && npm run build:bundle",
+    "build:dev": "node scripts/babelcheck.js && npm run build:res && npm run build:bundle:dev",
     "dist": "scripts/package.sh",
-    "start:res": "parallelshell \"cpx -w \\\"{src/skins/vector/fonts,src/skins/vector/img}/**\\\" webapp/\" \"cpx -w \\\"{res/media,res/vector-icons}/**\\\" webapp/\"",
-    "start:config": "cpx -w config.json webapp/",
-    "start:emojione": "cpx \"node_modules/emojione/assets/svg/*\" webapp/emojione/svg/ -w",
-    "start:js": "webpack-dev-server -w --progress",
+    "start:res": "node scripts/copy-res.js -w",
+    "start:js": "webpack-dev-server --output-filename=bundles/_dev_/[name].js --output-chunk-file=bundles/_dev_/[name].js -w --progress",
     "start:js:prod": "NODE_ENV=production webpack-dev-server -w --progress",
-    "start:skins:css": "mkdirp build && catw \"src/skins/vector/css/**/*.css\" -o build/components.css",
-    "start": "node scripts/babelcheck.js && parallelshell \"npm run start:emojione\" \"npm run start:res\" \"npm run start:config\" \"npm run start:js\" \"npm run start:skins:css\"",
-    "start:prod": "parallelshell \"npm run start:emojione\" \"npm run start:js:prod\" \"npm run start:skins:css\"",
-    "clean": "rimraf build lib webapp",
+    "start": "node scripts/babelcheck.js && parallelshell \"npm run start:res\" \"npm run start:js\"",
+    "start:prod": "parallelshell \"npm run start:res\" \"npm run start:js:prod\"",
+    "lint": "eslint src/",
+    "lintall": "eslint src/ test/",
+    "clean": "rimraf lib webapp electron/dist",
     "prepublish": "npm run build:compile",
     "test": "karma start --single-run=true --autoWatch=false --browsers PhantomJS --colors=false",
     "test:multi": "karma start"
@@ -70,16 +66,17 @@
     "matrix-react-sdk": "matrix-org/matrix-react-sdk#develop",
     "modernizr": "^3.1.0",
     "q": "^1.4.1",
-    "react": "^15.2.1",
+    "react": "^15.4.0",
     "react-dnd": "^2.1.4",
     "react-dnd-html5-backend": "^2.1.2",
-    "react-dom": "^15.2.1",
+    "react-dom": "^15.4.0",
     "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
     "sanitize-html": "^1.11.1",
     "ua-parser-js": "^0.7.10",
     "url": "^0.11.0"
   },
   "devDependencies": {
+    "autoprefixer": "^6.6.0",
     "babel-cli": "^6.5.2",
     "babel-core": "^6.14.0",
     "babel-eslint": "^6.1.0",
@@ -94,11 +91,16 @@
     "babel-preset-es2017": "^6.16.0",
     "babel-preset-react": "^6.16.0",
     "babel-preset-stage-2": "^6.17.0",
-    "catw": "^1.0.1",
+    "chokidar": "^1.6.1",
     "cpx": "^1.3.2",
     "css-raw-loader": "^0.1.1",
-    "electron-builder": "^7.23.2",
+    "electron-builder": "^11.2.4",
+    "electron-builder-squirrel-windows": "^11.2.1",
     "emojione": "^2.2.3",
+    "eslint": "^3.14.0",
+    "eslint-config-google": "^0.7.1",
+    "eslint-plugin-flowtype": "^2.30.0",
+    "eslint-plugin-react": "^6.9.0",
     "expect": "^1.16.0",
     "fs-extra": "^0.30.0",
     "html-webpack-plugin": "^2.24.0",
@@ -111,37 +113,52 @@
     "karma-phantomjs-launcher": "^1.0.0",
     "karma-sourcemap-loader": "^0.3.7",
     "karma-webpack": "^1.7.0",
+    "minimist": "^1.2.0",
     "mkdirp": "^0.5.1",
     "mocha": "^2.4.5",
     "parallelshell": "^1.2.0",
     "phantomjs-prebuilt": "^2.1.7",
-    "react-addons-perf": "^15.0",
-    "react-addons-test-utils": "^15.0.1",
+    "postcss-extend": "^1.0.5",
+    "postcss-import": "^9.0.0",
+    "postcss-loader": "^1.2.2",
+    "postcss-mixins": "^5.4.1",
+    "postcss-nested": "^1.0.0",
+    "postcss-scss": "^0.4.0",
+    "postcss-simple-vars": "^3.0.0",
+    "postcss-strip-inline-comments": "^0.1.5",
+    "react-addons-perf": "^15.4.0",
+    "react-addons-test-utils": "^15.4.0",
     "rimraf": "^2.4.3",
     "source-map-loader": "^0.1.5",
     "webpack": "^1.12.14",
     "webpack-dev-server": "^1.16.2"
   },
   "optionalDependencies": {
-    "olm": "https://matrix.org/packages/npm/olm/olm-2.0.0.tgz"
+    "olm": "https://matrix.org/packages/npm/olm/olm-2.1.0.tgz"
   },
   "build": {
     "appId": "im.riot.app",
     "category": "Network",
-    "electronVersion": "1.4.2",
+    "electronVersion": "1.4.14",
     "//asar=false": "https://github.com/electron-userland/electron-builder/issues/675",
     "asar": false,
     "dereference": true,
     "//files": "We bundle everything, so we only need to include webapp/",
     "files": [
-      "!**/*",
       "electron/src/**",
       "electron/img/**",
       "webapp/**",
       "package.json"
     ],
-    "squirrelWindows": {
-      "iconUrl": "https://riot.im/favicon.ico"
+    "linux": {
+      "target": "deb",
+      "maintainer": "support@riot.im",
+      "desktop": {
+        "StartupWMClass": "riot-web"
+      }
+    },
+    "win": {
+      "target": "squirrel"
     }
   },
   "directories": {
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 00000000..5305d9ed
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,13 @@
+module.exports = {
+    plugins: [
+        require("postcss-import")(),
+        require("autoprefixer")(),
+        require("postcss-simple-vars")(),
+        require("postcss-extend")(),
+        require("postcss-nested")(),
+        require("postcss-mixins")(),
+        require("postcss-strip-inline-comments")(),
+    ],
+    "parser": "postcss-scss",
+    "local-plugins": true,
+};
diff --git a/release_config.yaml b/release_config.yaml
new file mode 100644
index 00000000..2be2a0de
--- /dev/null
+++ b/release_config.yaml
@@ -0,0 +1 @@
+signing_id: packages@riot.im
diff --git a/res/vector-icons/android-chrome-144x144.png b/res/vector-icons/android-chrome-144x144.png
index 2a83f521..30c8cf18 100644
Binary files a/res/vector-icons/android-chrome-144x144.png and b/res/vector-icons/android-chrome-144x144.png differ
diff --git a/res/vector-icons/android-chrome-192x192.png b/res/vector-icons/android-chrome-192x192.png
index 5794bdb0..560e20ad 100644
Binary files a/res/vector-icons/android-chrome-192x192.png and b/res/vector-icons/android-chrome-192x192.png differ
diff --git a/res/vector-icons/android-chrome-36x36.png b/res/vector-icons/android-chrome-36x36.png
index 17b6fe83..ee41772f 100644
Binary files a/res/vector-icons/android-chrome-36x36.png and b/res/vector-icons/android-chrome-36x36.png differ
diff --git a/res/vector-icons/android-chrome-48x48.png b/res/vector-icons/android-chrome-48x48.png
index 7435fbd8..9cd225d4 100644
Binary files a/res/vector-icons/android-chrome-48x48.png and b/res/vector-icons/android-chrome-48x48.png differ
diff --git a/res/vector-icons/android-chrome-72x72.png b/res/vector-icons/android-chrome-72x72.png
index 9fdbbefb..d9de0c57 100644
Binary files a/res/vector-icons/android-chrome-72x72.png and b/res/vector-icons/android-chrome-72x72.png differ
diff --git a/res/vector-icons/android-chrome-96x96.png b/res/vector-icons/android-chrome-96x96.png
index b9d43269..eb48c2c9 100644
Binary files a/res/vector-icons/android-chrome-96x96.png and b/res/vector-icons/android-chrome-96x96.png differ
diff --git a/res/vector-icons/apple-touch-icon-114x114.png b/res/vector-icons/apple-touch-icon-114x114.png
index 0f0ba555..e07fa437 100644
Binary files a/res/vector-icons/apple-touch-icon-114x114.png and b/res/vector-icons/apple-touch-icon-114x114.png differ
diff --git a/res/vector-icons/apple-touch-icon-120x120.png b/res/vector-icons/apple-touch-icon-120x120.png
index bd524795..82650913 100644
Binary files a/res/vector-icons/apple-touch-icon-120x120.png and b/res/vector-icons/apple-touch-icon-120x120.png differ
diff --git a/res/vector-icons/apple-touch-icon-144x144.png b/res/vector-icons/apple-touch-icon-144x144.png
index 2a83f521..30c8cf18 100644
Binary files a/res/vector-icons/apple-touch-icon-144x144.png and b/res/vector-icons/apple-touch-icon-144x144.png differ
diff --git a/res/vector-icons/apple-touch-icon-152x152.png b/res/vector-icons/apple-touch-icon-152x152.png
index 04ed2d47..5bc63546 100644
Binary files a/res/vector-icons/apple-touch-icon-152x152.png and b/res/vector-icons/apple-touch-icon-152x152.png differ
diff --git a/res/vector-icons/apple-touch-icon-180x180.png b/res/vector-icons/apple-touch-icon-180x180.png
index fe13aa99..85e9f8ca 100644
Binary files a/res/vector-icons/apple-touch-icon-180x180.png and b/res/vector-icons/apple-touch-icon-180x180.png differ
diff --git a/res/vector-icons/apple-touch-icon-57x57.png b/res/vector-icons/apple-touch-icon-57x57.png
index 3d7055c5..253c3db7 100644
Binary files a/res/vector-icons/apple-touch-icon-57x57.png and b/res/vector-icons/apple-touch-icon-57x57.png differ
diff --git a/res/vector-icons/apple-touch-icon-60x60.png b/res/vector-icons/apple-touch-icon-60x60.png
index 0dc6bcb5..192a3467 100644
Binary files a/res/vector-icons/apple-touch-icon-60x60.png and b/res/vector-icons/apple-touch-icon-60x60.png differ
diff --git a/res/vector-icons/apple-touch-icon-72x72.png b/res/vector-icons/apple-touch-icon-72x72.png
index 9fdbbefb..d9de0c57 100644
Binary files a/res/vector-icons/apple-touch-icon-72x72.png and b/res/vector-icons/apple-touch-icon-72x72.png differ
diff --git a/res/vector-icons/apple-touch-icon-76x76.png b/res/vector-icons/apple-touch-icon-76x76.png
index 5724b6b8..b9471645 100644
Binary files a/res/vector-icons/apple-touch-icon-76x76.png and b/res/vector-icons/apple-touch-icon-76x76.png differ
diff --git a/res/vector-icons/apple-touch-icon-precomposed.png b/res/vector-icons/apple-touch-icon-precomposed.png
index fe13aa99..85e9f8ca 100644
Binary files a/res/vector-icons/apple-touch-icon-precomposed.png and b/res/vector-icons/apple-touch-icon-precomposed.png differ
diff --git a/res/vector-icons/apple-touch-icon.png b/res/vector-icons/apple-touch-icon.png
index fe13aa99..85e9f8ca 100644
Binary files a/res/vector-icons/apple-touch-icon.png and b/res/vector-icons/apple-touch-icon.png differ
diff --git a/res/vector-icons/favicon-16x16.png b/res/vector-icons/favicon-16x16.png
index cc7e01e3..def4ec5e 100644
Binary files a/res/vector-icons/favicon-16x16.png and b/res/vector-icons/favicon-16x16.png differ
diff --git a/res/vector-icons/favicon-32x32.png b/res/vector-icons/favicon-32x32.png
index a0089a4e..c999923a 100644
Binary files a/res/vector-icons/favicon-32x32.png and b/res/vector-icons/favicon-32x32.png differ
diff --git a/res/vector-icons/favicon-96x96.png b/res/vector-icons/favicon-96x96.png
index b9d43269..eb48c2c9 100644
Binary files a/res/vector-icons/favicon-96x96.png and b/res/vector-icons/favicon-96x96.png differ
diff --git a/res/vector-icons/favicon.ico b/res/vector-icons/favicon.ico
index 1b40e174..8f8ff94e 100644
Binary files a/res/vector-icons/favicon.ico and b/res/vector-icons/favicon.ico differ
diff --git a/res/vector-icons/mstile-144x144.png b/res/vector-icons/mstile-144x144.png
index 4e130472..30c8cf18 100644
Binary files a/res/vector-icons/mstile-144x144.png and b/res/vector-icons/mstile-144x144.png differ
diff --git a/res/vector-icons/mstile-150x150.png b/res/vector-icons/mstile-150x150.png
index 7222a40c..5b8fca8a 100644
Binary files a/res/vector-icons/mstile-150x150.png and b/res/vector-icons/mstile-150x150.png differ
diff --git a/res/vector-icons/mstile-310x150.png b/res/vector-icons/mstile-310x150.png
index af5ec1b3..d809f00a 100644
Binary files a/res/vector-icons/mstile-310x150.png and b/res/vector-icons/mstile-310x150.png differ
diff --git a/res/vector-icons/mstile-310x310.png b/res/vector-icons/mstile-310x310.png
index f5fecd42..ec62f8ee 100644
Binary files a/res/vector-icons/mstile-310x310.png and b/res/vector-icons/mstile-310x310.png differ
diff --git a/res/vector-icons/mstile-70x70.png b/res/vector-icons/mstile-70x70.png
index d45b51fb..820a909e 100644
Binary files a/res/vector-icons/mstile-70x70.png and b/res/vector-icons/mstile-70x70.png differ
diff --git a/scripts/copy-res.js b/scripts/copy-res.js
new file mode 100755
index 00000000..826d9a96
--- /dev/null
+++ b/scripts/copy-res.js
@@ -0,0 +1,81 @@
+#!/usr/bin/env node
+
+// copies the resources into the webapp directory.
+//
+
+// cpx includes globbed parts of the filename in the destination, but excludes
+// common parents. Hence, "res/{a,b}/**": the output will be "dest/a/..." and
+// "dest/b/...".
+const COPY_LIST = [
+    ["res/{media,vector-icons}/**", "webapp"],
+    ["src/skins/vector/{fonts,img}/**", "webapp"],
+    ["node_modules/emojione/assets/svg/*", "webapp/emojione/svg/"],
+    ["./config.json", "webapp", {directwatch: 1}],
+];
+
+const parseArgs = require('minimist');
+const Cpx = require('cpx');
+const chokidar = require('chokidar');
+
+const argv = parseArgs(
+    process.argv.slice(2), {}
+);
+
+var watch = argv.w;
+var verbose = argv.v;
+
+function errCheck(err) {
+    if (err) {
+        console.error(err.message);
+        process.exit(1);
+    }
+}
+
+function next(i, err) {
+    errCheck(err);
+
+    if (i >= COPY_LIST.length) {
+        return;
+    }
+
+    const ent = COPY_LIST[i];
+    const source = ent[0];
+    const dest = ent[1];
+    const opts = ent[2] || {};
+
+    const cpx = new Cpx.Cpx(source, dest);
+
+    if (verbose) {
+        cpx.on("copy", (event) => {
+            console.log(`Copied: ${event.srcPath} --> ${event.dstPath}`);
+        });
+        cpx.on("remove", (event) => {
+            console.log(`Removed: ${event.path}`);
+        });
+    }
+
+    const cb = (err) => {next(i+1, err)};
+
+    if (watch) {
+        if (opts.directwatch) {
+            // cpx -w creates a watcher for the parent of any files specified,
+            // which in the case of config.json is '.', which inevitably takes
+            // ages to crawl. So we create our own watcher on the files
+            // instead.
+            const copy = () => {cpx.copy(errCheck)};
+            chokidar.watch(source)
+                .on('add', copy)
+                .on('change', copy)
+                .on('ready', cb)
+                .on('error', errCheck);
+        } else {
+            cpx.on('watch-ready', cb);
+            cpx.on("watch-error", cb);
+            cpx.watch();
+        }
+    } else {
+        cpx.copy(cb);
+    }
+}
+
+next(0);
diff --git a/scripts/deploy.py b/scripts/deploy.py
new file mode 100755
index 00000000..c96b46e8
--- /dev/null
+++ b/scripts/deploy.py
@@ -0,0 +1,183 @@
+#!/usr/bin/env python
+#
+# download and unpack a riot-web tarball.
+#
+# Allows `bundles` to be extracted to a common directory, and a link to
+# config.json to be added.
+
+from __future__ import print_function
+
+import argparse
+import os
+import os.path
+import subprocess
+import sys
+import tarfile
+
+try:
+    # python3
+    from urllib.request import urlretrieve
+except ImportError:
+    # python2
+    from urllib import urlretrieve
+
+class DeployException(Exception):
+    pass
+
+def create_relative_symlink(linkname, target):
+    relpath = os.path.relpath(target, os.path.dirname(linkname))
+    print ("Symlink %s -> %s" % (linkname, relpath))
+    os.symlink(relpath, linkname)
+
+
+def move_bundles(source, dest):
+    """Move the contents of the 'bundles' directory to a common dir
+
+    We check that we will not be overwriting anything before we proceed.
+
+    Args:
+        source (str): path to 'bundles' within the extracted tarball
+        dest (str): target common directory
+    """
+
+    if not os.path.isdir(dest):
+        os.mkdir(dest)
+
+    # build a map from source to destination, checking for non-existence as we go.
+    renames = {}
+    for f in os.listdir(source):
+        dst = os.path.join(dest, f)
+        if os.path.exists(dst):
+            raise DeployException(
+                "Not deploying. The bundle includes '%s' which we have previously deployed."
+                % f
+            )
+        renames[os.path.join(source, f)] = dst
+
+    for (src, dst) in renames.iteritems():
+        print ("Move %s -> %s" % (src, dst))
+        os.rename(src, dst)
+
+class Deployer:
+    def __init__(self):
+        self.packages_path = "."
+        self.bundles_path = None
+        self.should_clean = False
+        self.config_location = None
+        self.verify_signature = True
+
+    def deploy(self, tarball, extract_path):
+        """Download a tarball if necessary, and unpack it
+
+        Returns:
+            (str) the path to the unpacked deployment
+        """
+        print("Deploying %s to %s" % (tarball, extract_path))
+
+        name_str = os.path.basename(tarball).replace(".tar.gz", "")
+        extracted_dir = os.path.join(extract_path, name_str)
+        if os.path.exists(extracted_dir):
+            raise DeployException('Cannot unpack %s: %s already exists' % (
+                tarball, extracted_dir))
+
+        downloaded = False
+        if tarball.startswith("http://") or tarball.startswith("https://"):
+            tarball = self.download_and_verify(tarball)
+            print("Downloaded file: %s" % tarball)
+            downloaded = True
+
+        try:
+            with tarfile.open(tarball) as tar:
+                tar.extractall(extract_path)
+        finally:
+            if self.should_clean and downloaded:
+                os.remove(tarball)
+
+        print ("Extracted into: %s" % extracted_dir)
+
+        if self.config_location:
+            create_relative_symlink(
+                target=self.config_location,
+                linkname=os.path.join(extracted_dir, 'config.json')
+            )
+
+        if self.bundles_path:
+            extracted_bundles = os.path.join(extracted_dir, 'bundles')
+            move_bundles(source=extracted_bundles, dest=self.bundles_path)
+
+            # replace the (hopefully now empty) extracted_bundles dir with a
+            # symlink to the common dir.
+            os.rmdir(extracted_bundles)
+            create_relative_symlink(
+                target=self.bundles_path,
+                linkname=extracted_bundles,
+            )
+        return extracted_dir
+
+    def download_and_verify(self, url):
+        tarball = self.download_file(url)
+
+        if self.verify_signature:
+            sigfile = self.download_file(url + ".asc")
+            subprocess.check_call(["gpg", "--verify", sigfile, tarball])
+
+        return tarball
+
+    def download_file(self, url):
+        if not os.path.isdir(self.packages_path):
+            os.mkdir(self.packages_path)
+        local_filename = os.path.join(self.packages_path,
+                                      url.split('/')[-1])
+        sys.stdout.write("Downloading %s -> %s..." % (url, local_filename))
+        sys.stdout.flush()
+        urlretrieve(url, local_filename)
+        print ("Done")
+        return local_filename
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser("Deploy a Riot build on a web server.")
+    parser.add_argument(
+        "-p", "--packages-dir", default="./packages", help=(
+            "The directory to download the tarball into. (Default: '%(default)s')"
+        )
+    )
+    parser.add_argument(
+        "-e", "--extract-path", default="./deploys", help=(
+            "The location to extract .tar.gz files to. (Default: '%(default)s')"
+        )
+    )
+    parser.add_argument(
+        "-b", "--bundles-dir", nargs='?', default="./bundles", help=(
+            "A directory to move the contents of the 'bundles' directory to. A \
+            symlink to the bundles directory will also be written inside the \
+            extracted tarball. Example: './bundles'. \
+            (Default: '%(default)s')"
+        )
+    )
+    parser.add_argument(
+        "-c", "--clean", action="store_true", default=False, help=(
+            "Remove .tar.gz files after they have been downloaded and extracted. \
+            (Default: %(default)s)"
+        )
+    )
+    parser.add_argument(
+        "--config", nargs='?', default='./config.json', help=(
+            "Write a symlink at config.json in the extracted tarball to this \
+            location. (Default: '%(default)s')"
+        )
+    )
+    parser.add_argument(
+        "tarball", help=(
+            "filename of tarball, or URL to download."
+        ),
+    )
+
+    args = parser.parse_args()
+
+    deployer = Deployer()
+    deployer.packages_path = args.packages_dir
+    deployer.bundles_path = args.bundles_dir
+    deployer.should_clean = args.clean
+    deployer.config_location = args.config
+
+    deployer.deploy(args.tarball, args.extract_path)
diff --git a/scripts/electron-package.sh b/scripts/electron-package.sh
new file mode 100755
index 00000000..87e353f7
--- /dev/null
+++ b/scripts/electron-package.sh
@@ -0,0 +1,130 @@
+#!/bin/bash
+
+set -e
+
+usage() {
+    echo "Usage: $0 -v <version> -c <config file> [-n]"
+    echo
+    echo "version: commit-ish to check out and build"
+    echo "config file: a path to a json config file to"
+    echo "ship with the build. In addition, update_base_url:"
+    echo "from this file is used to set up auto-update."
+    echo "-n: build with no config file."
+    echo
+    echo "Values may also be passed as environment variables"
+}
+
+conffile=
+version=
+skipcfg=0
+while getopts "c:v:n" opt; do
+    case $opt in
+        c)
+            conffile=$OPTARG
+            ;;
+        v)
+            version=$OPTARG
+            ;;
+        n)
+            skipcfg=1
+            ;;
+        \?)
+            echo "Invalid option: -$OPTARG" >&2
+            usage
+            exit
+            ;;
+    esac
+done
+
+if [ -z "$version" ]; then
+    echo "No version supplied"
+    usage
+    exit
+fi
+
+if [ -z "$conffile" ] && [ "$skipcfg" = 0 ]; then
+    echo "No config file given. Use -c to supply a config file or"
+    echo "-n to build with no config file (and no auto update)."
+    exit
+fi
+
+if [ -n "$conffile" ]; then
+    update_base_url=`jq -r .update_base_url $conffile`
+
+    if [ -z "$update_base_url" ]; then
+        echo "No update URL supplied. Use update_base_url: null if you really"
+        echo "want a build with no auto-update."
+        usage
+        exit
+    fi
+    # Make sure the base URL ends in a slash if it doesn't already
+    update_base_url=`echo $update_base_url | sed -e 's#\([^\/]\)$#\1\/#'`
+fi
+
+if [ ! -f package.json ]; then
+    echo "No package.json found. This script must be run from"
+    echo "the vector-web directory."
+    exit
+fi
+
+echo "Building $version using Update base URL $update_base_url"
+
+projdir=`pwd`
+builddir=`mktemp -d 2>/dev/null || mktemp -d -t 'buildtmp'`
+pushd "$builddir"
+
+git clone "$projdir" .
+git checkout "$version"
+
+# Figure out what version we're building
+vername=`jq -r .version package.json`
+
+if [ -n "$conffile" ]; then
+    popd
+    cp "$conffile" "$builddir/"
+    pushd "$builddir"
+fi
+
+npm install
+npm run build:electron
+
+popd
+
+distdir="$builddir/electron/dist"
+pubdir="$projdir/electron/pub"
+rm -r "$pubdir" || true
+mkdir -p "$pubdir"
+
+# Install packages: what the user downloads the first time,
+# (DMGs for mac, exe installer for windows)
+mkdir -p "$pubdir/install/macos"
+cp $distdir/mac/*.dmg "$pubdir/install/macos/"
+
+mkdir -p "$pubdir/install/win32/ia32/"
+cp $distdir/win-ia32/*.exe "$pubdir/install/win32/ia32/"
+
+mkdir -p "$pubdir/install/win32/x64/"
+cp $distdir/win/*.exe "$pubdir/install/win32/x64/"
+
+# Packages for auto-update
+mkdir -p "$pubdir/update/macos"
+cp $distdir/mac/*.zip "$pubdir/update/macos/"
+echo "$vername" > "$pubdir/update/macos/latest"
+
+mkdir -p "$pubdir/update/win32/ia32/"
+cp $distdir/win-ia32/*.nupkg "$pubdir/update/win32/ia32/"
+cp $distdir/win-ia32/RELEASES "$pubdir/update/win32/ia32/"
+
+mkdir -p "$pubdir/update/win32/x64/"
+cp $distdir/win/*.nupkg "$pubdir/update/win32/x64/"
+cp $distdir/win/RELEASES "$pubdir/update/win32/x64/"
+
+# Move the debs to the main project dir's dist folder
+rm -r "$projdir/electron/dist" || true
+mkdir -p "$projdir/electron/dist"
+cp $distdir/*.deb "$projdir/electron/dist/"
+
+rm -rf "$builddir"
+
+echo "Riot Desktop is ready to go in $pubdir: this directory can be hosted on your web server."
+echo "deb archives are in electron/dist/ - these should be added into your debian repository"
diff --git a/scripts/jenkins.sh b/scripts/jenkins.sh
index be8d8dee..5ccc1991 100755
--- a/scripts/jenkins.sh
+++ b/scripts/jenkins.sh
@@ -19,12 +19,16 @@ tar -C olm -xz < olm/olm-*.tgz
 rm -r node_modules/olm
 cp -r olm/package node_modules/olm
 
-# we may be using a dev branch of react-sdk, in which case we need to build it
+# we may be using dev branches of js-sdk and react-sdk, in which case we need to build them
+(cd node_modules/matrix-js-sdk && npm run build)
 (cd node_modules/matrix-react-sdk && npm run build)
 
 # run the mocha tests
 npm run test
 
+# run eslint
+npm run lintall -- -f checkstyle -o eslint.xml || true
+
 rm dist/vector-*.tar.gz || true # rm previous artifacts without failing if it doesn't exist
 
  # node_modules deps from 'npm install' don't have a .git dir so can't
diff --git a/scripts/make-icons.sh b/scripts/make-icons.sh
new file mode 100755
index 00000000..c77064ab
--- /dev/null
+++ b/scripts/make-icons.sh
@@ -0,0 +1,93 @@
+#!/bin/bash
+
+if [ $# != 1 ]
+then
+    echo "Usage: $0 <svg file>"
+    exit
+fi
+
+set -e
+set -x
+
+tmpdir=`mktemp -d 2>/dev/null || mktemp -d -t 'icontmp'`
+
+for i in 1024 512 310 256 192 180 152 150 144 128 120 114 96 76 72 70 64 60 57 48 36 32 24 16
+do
+    #convert -background none -density 1000 -resize $i -extent $i -gravity center "$1" "$tmpdir/$i.png"
+
+    # Above is the imagemagick command to render an svg to png. Unfortunately, its support for SVGs
+    # with CSS isn't very good (with rsvg and even moreso the built in renderer) so we use cairosvg.
+    # This can be installed with:
+    #    pip install cairosvg==1.0.22 # Version 2 doesn't support python 2
+    #    pip install tinycss
+    #    pip install cssselect # These are necessary for CSS support
+    # You'll also need xmlstarlet from your favourite package manager
+    #
+    # Cairosvg doesn't suport rendering at a specific size (https://github.com/Kozea/CairoSVG/issues/83#issuecomment-215720176)
+    # so we have to 'resize the svg' first (add width and height attributes to the svg element) to make it render at the
+    # size we need.
+    # XXX: This will break if the svg already has width and height attributes
+    cp "$1" "$tmpdir/tmp.svg"
+    xmlstarlet ed -N x="http://www.w3.org/2000/svg" --insert "/x:svg" --type attr -n width -v $i "$tmpdir/tmp.svg" > "$tmpdir/tmp2.svg"
+    xmlstarlet ed -N x="http://www.w3.org/2000/svg" --insert "/x:svg" --type attr -n height -v $i "$tmpdir/tmp2.svg" > "$tmpdir/tmp3.svg"
+    cairosvg -f png -o "$tmpdir/$i.png"  "$tmpdir/tmp3.svg"
+    rm "$tmpdir/tmp.svg" "$tmpdir/tmp2.svg" "$tmpdir/tmp3.svg"
+done
+
+# one more for the non-square mstile
+cp "$1" "$tmpdir/tmp.svg"
+xmlstarlet ed -N x="http://www.w3.org/2000/svg" --insert "/x:svg" --type attr -n width -v 310 "$tmpdir/tmp.svg" > "$tmpdir/tmp2.svg"
+xmlstarlet ed -N x="http://www.w3.org/2000/svg" --insert "/x:svg" --type attr -n height -v 150 "$tmpdir/tmp2.svg" > "$tmpdir/tmp3.svg"
+cairosvg -f png -o "$tmpdir/310x150.png"  "$tmpdir/tmp3.svg"
+rm "$tmpdir/tmp.svg" "$tmpdir/tmp2.svg" "$tmpdir/tmp3.svg"
+
+mkdir "$tmpdir/Riot.iconset"
+cp "$tmpdir/16.png" "$tmpdir/Riot.iconset/icon_16x16.png"
+cp "$tmpdir/32.png" "$tmpdir/Riot.iconset/icon_16x16@2x.png"
+cp "$tmpdir/32.png" "$tmpdir/Riot.iconset/icon_32x32.png"
+cp "$tmpdir/64.png" "$tmpdir/Riot.iconset/icon_32x32@2x.png"
+cp "$tmpdir/128.png" "$tmpdir/Riot.iconset/icon_128x128.png"
+cp "$tmpdir/256.png" "$tmpdir/Riot.iconset/icon_128x128@2x.png"
+cp "$tmpdir/256.png" "$tmpdir/Riot.iconset/icon_256x256.png"
+cp "$tmpdir/512.png" "$tmpdir/Riot.iconset/icon_256x256@2x.png"
+cp "$tmpdir/512.png" "$tmpdir/Riot.iconset/icon_512x512.png"
+cp "$tmpdir/1024.png" "$tmpdir/Riot.iconset/icon_512x512@2x.png"
+iconutil -c icns -o electron/build/icon.icns "$tmpdir/Riot.iconset"
+
+cp "$tmpdir/36.png" "res/vector-icons/android-chrome-36x36.png"
+cp "$tmpdir/48.png" "res/vector-icons/android-chrome-48x48.png"
+cp "$tmpdir/72.png" "res/vector-icons/android-chrome-72x72.png"
+cp "$tmpdir/96.png" "res/vector-icons/android-chrome-96x96.png"
+cp "$tmpdir/144.png" "res/vector-icons/android-chrome-144x144.png"
+cp "$tmpdir/192.png" "res/vector-icons/android-chrome-192x192.png"
+cp "$tmpdir/180.png" "res/vector-icons/apple-touch-icon.png"
+cp "$tmpdir/180.png" "res/vector-icons/apple-touch-icon-precomposed.png"
+cp "$tmpdir/57.png" "res/vector-icons/apple-touch-icon-57x57.png"
+cp "$tmpdir/60.png" "res/vector-icons/apple-touch-icon-60x60.png"
+cp "$tmpdir/72.png" "res/vector-icons/apple-touch-icon-72x72.png"
+cp "$tmpdir/76.png" "res/vector-icons/apple-touch-icon-76x76.png"
+cp "$tmpdir/114.png" "res/vector-icons/apple-touch-icon-114x114.png"
+cp "$tmpdir/120.png" "res/vector-icons/apple-touch-icon-120x120.png"
+cp "$tmpdir/144.png" "res/vector-icons/apple-touch-icon-144x144.png"
+cp "$tmpdir/152.png" "res/vector-icons/apple-touch-icon-152x152.png"
+cp "$tmpdir/180.png" "res/vector-icons/apple-touch-icon-180x180.png"
+cp "$tmpdir/16.png" "res/vector-icons/favicon-16x16.png"
+cp "$tmpdir/32.png" "res/vector-icons/favicon-32x32.png"
+cp "$tmpdir/96.png" "res/vector-icons/favicon-96x96.png"
+cp "$tmpdir/70.png" "res/vector-icons/mstile-70x70.png"
+cp "$tmpdir/144.png" "res/vector-icons/mstile-144x144.png"
+cp "$tmpdir/150.png" "res/vector-icons/mstile-150x150.png"
+cp "$tmpdir/310.png" "res/vector-icons/mstile-310x310.png"
+cp "$tmpdir/310x150.png" "res/vector-icons/mstile-310x150.png"
+
+convert "$tmpdir/16.png" "$tmpdir/32.png" "$tmpdir/64.png" "$tmpdir/128.png"  "$tmpdir/256.png" "res/vector-icons/favicon.ico"
+
+cp "res/vector-icons/favicon.ico" "electron/build/icon.ico"
+
+# https://github.com/electron-userland/electron-builder/blob/3f97b86993d4ea5172e562b182230a194de0f621/src/targets/LinuxTargetHelper.ts#L127
+for i in 24 96 16 48 64 128 256 512
+do
+    cp "$tmpdir/$i.png" "electron/build/icons/${i}x${i}.png"
+done
+
+rm -r "$tmpdir"
diff --git a/scripts/package.sh b/scripts/package.sh
index 05227e1b..5c1fdd5e 100755
--- a/scripts/package.sh
+++ b/scripts/package.sh
@@ -15,6 +15,11 @@ fi
 
 npm run clean
 npm run build$dev
+
+# include the sample config in the tarball. Arguably this should be done by
+# `npm run build`, but it's just too painful.
+cp config.sample.json webapp/
+
 mkdir -p dist
 cp -r webapp vector-$version
 echo $version > vector-$version/version
diff --git a/scripts/redeploy.py b/scripts/redeploy.py
index 6e6e64aa..598f6c52 100755
--- a/scripts/redeploy.py
+++ b/scripts/redeploy.py
@@ -1,26 +1,30 @@
 #!/usr/bin/env python
+#
+# auto-deploy script for https://riot.im/develop
+#
+# Listens for HTTP hits. When it gets one, downloads the artifact from jenkins
+# and deploys it as the new version.
+#
+# Requires the following python packages:
+#
+#   - requests
+#   - flask
+#
 from __future__ import print_function
 import json, requests, tarfile, argparse, os, errno
+import time
 from urlparse import urljoin
+
 from flask import Flask, jsonify, request, abort
+
+from deploy import Deployer, DeployException
+
 app = Flask(__name__)
 
-arg_jenkins_url, arg_extract_path, arg_should_clean, arg_symlink, arg_config_location = (
-    None, None, None, None, None
-)
-
-def download_file(url):
-    local_filename = url.split('/')[-1]
-    r = requests.get(url, stream=True)
-    with open(local_filename, 'wb') as f:
-        for chunk in r.iter_content(chunk_size=1024): 
-            if chunk: # filter out keep-alive new chunks
-                f.write(chunk)
-    return local_filename
-
-def untar_to(tarball, dest):
-    with tarfile.open(tarball) as tar:
-        tar.extractall(dest)
+arg_jenkins_url = None
+deployer = None
+arg_extract_path = None
+arg_symlink = None
 
 def create_symlink(source, linkname):
     try:
@@ -57,6 +61,9 @@ def on_receive_jenkins_poke():
         abort(400, "Missing or bad build number")
         return
 
+    return fetch_jenkins_build(job_name, build_num)
+
+def fetch_jenkins_build(job_name, build_num):
     artifact_url = urljoin(
         arg_jenkins_url, "job/%s/%s/api/json" % (job_name, build_num)
     )
@@ -106,28 +113,45 @@ def on_receive_jenkins_poke():
         arg_jenkins_url, "job/%s/%s/artifact/%s" % (job_name, build_num, tar_gz_path)
     )
 
-    print("Retrieving .tar.gz file: %s" % tar_gz_url)
-    filename = download_file(tar_gz_url)
-    print("Downloaded file: %s" % filename)
-    name_str = filename.replace(".tar.gz", "")
-    untar_to(filename, arg_extract_path)
-
-    extracted_dir = os.path.join(arg_extract_path, name_str)
-
-    if arg_should_clean:
-        os.remove(filename)
+    # we extract into a directory based on the build number. This avoids the
+    # problem of multiple builds building the same git version and thus having
+    # the same tarball name. That would lead to two potential problems:
+    #   (a) sometimes jenkins serves corrupted artifacts; we would replace
+    #       a good deploy with a bad one
+    #   (b) we'll be overwriting the live deployment, which means people might
+    #       see half-written files.
+    build_dir = os.path.join(arg_extract_path, "%s-#%s" % (job_name, build_num))
+    try:
+        extracted_dir = deploy_tarball(tar_gz_url, build_dir)
+    except DeployException as e:
+        abort(400, e.message)
 
     create_symlink(source=extracted_dir, linkname=arg_symlink)
 
-    if arg_config_location:
-        create_symlink(source=arg_config_location, linkname=os.path.join(extracted_dir, 'config.json'))
-
     return jsonify({})
 
+def deploy_tarball(tar_gz_url, build_dir):
+    """Download a tarball from jenkins and unpack it
+
+    Returns:
+        (str) the path to the unpacked deployment
+    """
+    if os.path.exists(build_dir):
+        raise DeployException(
+            "Not deploying. We have previously deployed this build."
+        )
+    os.mkdir(build_dir)
+
+    # we rely on the fact that flask only serves one request at a time to
+    # ensure that we do not overwrite a tarball from a concurrent request.
+
+    return deployer.deploy(tar_gz_url, build_dir)
+
+
 if __name__ == "__main__":
     parser = argparse.ArgumentParser("Runs a Vector redeployment server.")
     parser.add_argument(
-        "-j", "--jenkins", dest="jenkins", default="http://matrix.org/jenkins/", help=(
+        "-j", "--jenkins", dest="jenkins", default="https://matrix.org/jenkins/", help=(
             "The base URL of the Jenkins web server. This will be hit to get the\
             built artifacts (the .gz file) for redeploying."
         )
@@ -142,6 +166,13 @@ if __name__ == "__main__":
             "The location to extract .tar.gz files to."
         )
     )
+    parser.add_argument(
+        "-b", "--bundles-dir", dest="bundles_dir", help=(
+            "A directory to move the contents of the 'bundles' directory to. A \
+            symlink to the bundles directory will also be written inside the \
+            extracted tarball. Example: './bundles'."
+        )
+    )
     parser.add_argument(
         "-c", "--clean", dest="clean", action="store_true", default=False, help=(
             "Remove .tar.gz files after they have been downloaded and extracted."
@@ -160,18 +191,47 @@ if __name__ == "__main__":
             To this location."
         )
     )
+    parser.add_argument(
+        "--test", dest="tarball_uri", help=(
+            "Don't start an HTTP listener. Instead download a build from Jenkins \
+            immediately."
+        ),
+    )
+
     args = parser.parse_args()
     if args.jenkins.endswith("/"): # important for urljoin
         arg_jenkins_url = args.jenkins
     else:
         arg_jenkins_url = args.jenkins + "/"
     arg_extract_path = args.extract
-    arg_should_clean = args.clean
     arg_symlink = args.symlink
-    arg_config_location = args.config
-    print(
-        "Listening on port %s. Extracting to %s%s. Symlinking to %s. Jenkins URL: %s. Config location: %s" %
-        (args.port, arg_extract_path,
-            " (clean after)" if arg_should_clean else "", arg_symlink, arg_jenkins_url, arg_config_location)
-    )
-    app.run(host="0.0.0.0", port=args.port, debug=True)
+
+    if not os.path.isdir(arg_extract_path):
+        os.mkdir(arg_extract_path)
+
+    deployer = Deployer()
+    deployer.bundles_path = args.bundles_dir
+    deployer.should_clean = args.clean
+    deployer.config_location = args.config
+
+    # we don't pgp-sign jenkins artifacts; instead we rely on HTTPS access to
+    # the jenkins server (and the jenkins server not being compromised and/or
+    # github not serving it compromised source). If that's not good enough for
+    # you, don't use riot.im/develop.
+    deployer.verify_signature = False
+
+    if args.tarball_uri is not None:
+        build_dir = os.path.join(arg_extract_path, "test-%i" % (time.time()))
+        deploy_tarball(args.tarball_uri, build_dir)
+    else:
+        print(
+            "Listening on port %s. Extracting to %s%s. Symlinking to %s. Jenkins URL: %s. Config location: %s" %
+            (args.port,
+             arg_extract_path,
+             " (clean after)" if deployer.should_clean else "",
+             arg_symlink,
+             arg_jenkins_url,
+             deployer.config_location,
+            )
+        )
+        app.run(host="0.0.0.0", port=args.port, debug=True)
diff --git a/src/component-index.js b/src/component-index.js
index 068ab061..4e6eefb6 100644
--- a/src/component-index.js
+++ b/src/component-index.js
@@ -27,62 +27,64 @@ limitations under the License.
 module.exports.components = require('matrix-react-sdk/lib/component-index').components;
 
 import structures$BottomLeftMenu from './components/structures/BottomLeftMenu';
-module.exports.components['structures.BottomLeftMenu'] = structures$BottomLeftMenu;
+structures$BottomLeftMenu && (module.exports.components['structures.BottomLeftMenu'] = structures$BottomLeftMenu);
 import structures$CompatibilityPage from './components/structures/CompatibilityPage';
-module.exports.components['structures.CompatibilityPage'] = structures$CompatibilityPage;
+structures$CompatibilityPage && (module.exports.components['structures.CompatibilityPage'] = structures$CompatibilityPage);
 import structures$LeftPanel from './components/structures/LeftPanel';
-module.exports.components['structures.LeftPanel'] = structures$LeftPanel;
+structures$LeftPanel && (module.exports.components['structures.LeftPanel'] = structures$LeftPanel);
 import structures$RightPanel from './components/structures/RightPanel';
-module.exports.components['structures.RightPanel'] = structures$RightPanel;
+structures$RightPanel && (module.exports.components['structures.RightPanel'] = structures$RightPanel);
 import structures$HomePage from './components/structures/HomePage';
-module.exports.components['structures.HomePage'] = structures$HomePage;
+structures$HomePage && (module.exports.components['structures.HomePage'] = structures$HomePage);
 import structures$RoomDirectory from './components/structures/RoomDirectory';
-module.exports.components['structures.RoomDirectory'] = structures$RoomDirectory;
+structures$RoomDirectory && (module.exports.components['structures.RoomDirectory'] = structures$RoomDirectory);
 import structures$RoomSubList from './components/structures/RoomSubList';
-module.exports.components['structures.RoomSubList'] = structures$RoomSubList;
+structures$RoomSubList && (module.exports.components['structures.RoomSubList'] = structures$RoomSubList);
 import structures$SearchBox from './components/structures/SearchBox';
-module.exports.components['structures.SearchBox'] = structures$SearchBox;
+structures$SearchBox && (module.exports.components['structures.SearchBox'] = structures$SearchBox);
 import structures$ViewSource from './components/structures/ViewSource';
-module.exports.components['structures.ViewSource'] = structures$ViewSource;
+structures$ViewSource && (module.exports.components['structures.ViewSource'] = structures$ViewSource);
 import views$context_menus$MessageContextMenu from './components/views/context_menus/MessageContextMenu';
-module.exports.components['views.context_menus.MessageContextMenu'] = views$context_menus$MessageContextMenu;
+views$context_menus$MessageContextMenu && (module.exports.components['views.context_menus.MessageContextMenu'] = views$context_menus$MessageContextMenu);
 import views$context_menus$NotificationStateContextMenu from './components/views/context_menus/NotificationStateContextMenu';
-module.exports.components['views.context_menus.NotificationStateContextMenu'] = views$context_menus$NotificationStateContextMenu;
+views$context_menus$NotificationStateContextMenu && (module.exports.components['views.context_menus.NotificationStateContextMenu'] = views$context_menus$NotificationStateContextMenu);
 import views$context_menus$RoomTagContextMenu from './components/views/context_menus/RoomTagContextMenu';
-module.exports.components['views.context_menus.RoomTagContextMenu'] = views$context_menus$RoomTagContextMenu;
+views$context_menus$RoomTagContextMenu && (module.exports.components['views.context_menus.RoomTagContextMenu'] = views$context_menus$RoomTagContextMenu);
+import views$dialogs$BugReportDialog from './components/views/dialogs/BugReportDialog';
+views$dialogs$BugReportDialog && (module.exports.components['views.dialogs.BugReportDialog'] = views$dialogs$BugReportDialog);
 import views$dialogs$ChangelogDialog from './components/views/dialogs/ChangelogDialog';
-module.exports.components['views.dialogs.ChangelogDialog'] = views$dialogs$ChangelogDialog;
+views$dialogs$ChangelogDialog && (module.exports.components['views.dialogs.ChangelogDialog'] = views$dialogs$ChangelogDialog);
 import views$directory$NetworkDropdown from './components/views/directory/NetworkDropdown';
-module.exports.components['views.directory.NetworkDropdown'] = views$directory$NetworkDropdown;
+views$directory$NetworkDropdown && (module.exports.components['views.directory.NetworkDropdown'] = views$directory$NetworkDropdown);
 import views$elements$ImageView from './components/views/elements/ImageView';
-module.exports.components['views.elements.ImageView'] = views$elements$ImageView;
+views$elements$ImageView && (module.exports.components['views.elements.ImageView'] = views$elements$ImageView);
 import views$elements$Spinner from './components/views/elements/Spinner';
-module.exports.components['views.elements.Spinner'] = views$elements$Spinner;
+views$elements$Spinner && (module.exports.components['views.elements.Spinner'] = views$elements$Spinner);
 import views$globals$GuestWarningBar from './components/views/globals/GuestWarningBar';
-module.exports.components['views.globals.GuestWarningBar'] = views$globals$GuestWarningBar;
+views$globals$GuestWarningBar && (module.exports.components['views.globals.GuestWarningBar'] = views$globals$GuestWarningBar);
 import views$globals$MatrixToolbar from './components/views/globals/MatrixToolbar';
-module.exports.components['views.globals.MatrixToolbar'] = views$globals$MatrixToolbar;
+views$globals$MatrixToolbar && (module.exports.components['views.globals.MatrixToolbar'] = views$globals$MatrixToolbar);
 import views$globals$NewVersionBar from './components/views/globals/NewVersionBar';
-module.exports.components['views.globals.NewVersionBar'] = views$globals$NewVersionBar;
+views$globals$NewVersionBar && (module.exports.components['views.globals.NewVersionBar'] = views$globals$NewVersionBar);
 import views$login$VectorCustomServerDialog from './components/views/login/VectorCustomServerDialog';
-module.exports.components['views.login.VectorCustomServerDialog'] = views$login$VectorCustomServerDialog;
+views$login$VectorCustomServerDialog && (module.exports.components['views.login.VectorCustomServerDialog'] = views$login$VectorCustomServerDialog);
 import views$login$VectorLoginFooter from './components/views/login/VectorLoginFooter';
-module.exports.components['views.login.VectorLoginFooter'] = views$login$VectorLoginFooter;
+views$login$VectorLoginFooter && (module.exports.components['views.login.VectorLoginFooter'] = views$login$VectorLoginFooter);
 import views$login$VectorLoginHeader from './components/views/login/VectorLoginHeader';
-module.exports.components['views.login.VectorLoginHeader'] = views$login$VectorLoginHeader;
+views$login$VectorLoginHeader && (module.exports.components['views.login.VectorLoginHeader'] = views$login$VectorLoginHeader);
 import views$messages$DateSeparator from './components/views/messages/DateSeparator';
-module.exports.components['views.messages.DateSeparator'] = views$messages$DateSeparator;
+views$messages$DateSeparator && (module.exports.components['views.messages.DateSeparator'] = views$messages$DateSeparator);
 import views$messages$MessageTimestamp from './components/views/messages/MessageTimestamp';
-module.exports.components['views.messages.MessageTimestamp'] = views$messages$MessageTimestamp;
+views$messages$MessageTimestamp && (module.exports.components['views.messages.MessageTimestamp'] = views$messages$MessageTimestamp);
 import views$rooms$DNDRoomTile from './components/views/rooms/DNDRoomTile';
-module.exports.components['views.rooms.DNDRoomTile'] = views$rooms$DNDRoomTile;
+views$rooms$DNDRoomTile && (module.exports.components['views.rooms.DNDRoomTile'] = views$rooms$DNDRoomTile);
 import views$rooms$RoomDropTarget from './components/views/rooms/RoomDropTarget';
-module.exports.components['views.rooms.RoomDropTarget'] = views$rooms$RoomDropTarget;
+views$rooms$RoomDropTarget && (module.exports.components['views.rooms.RoomDropTarget'] = views$rooms$RoomDropTarget);
 import views$rooms$RoomTooltip from './components/views/rooms/RoomTooltip';
-module.exports.components['views.rooms.RoomTooltip'] = views$rooms$RoomTooltip;
+views$rooms$RoomTooltip && (module.exports.components['views.rooms.RoomTooltip'] = views$rooms$RoomTooltip);
 import views$rooms$SearchBar from './components/views/rooms/SearchBar';
-module.exports.components['views.rooms.SearchBar'] = views$rooms$SearchBar;
+views$rooms$SearchBar && (module.exports.components['views.rooms.SearchBar'] = views$rooms$SearchBar);
 import views$settings$IntegrationsManager from './components/views/settings/IntegrationsManager';
-module.exports.components['views.settings.IntegrationsManager'] = views$settings$IntegrationsManager;
+views$settings$IntegrationsManager && (module.exports.components['views.settings.IntegrationsManager'] = views$settings$IntegrationsManager);
 import views$settings$Notifications from './components/views/settings/Notifications';
-module.exports.components['views.settings.Notifications'] = views$settings$Notifications;
+views$settings$Notifications && (module.exports.components['views.settings.Notifications'] = views$settings$Notifications);
diff --git a/src/components/structures/CompatibilityPage.js b/src/components/structures/CompatibilityPage.js
index dd139e65..bae33803 100644
--- a/src/components/structures/CompatibilityPage.js
+++ b/src/components/structures/CompatibilityPage.js
@@ -45,10 +45,8 @@ module.exports = React.createClass({
                 available or experimental in your current browser.
                 </p>
                 <p>
-                Please install <a href="https://www.google.com/chrome">Chrome</a> or
-                <a href="https://getfirefox.com">Firefox</a> for the best experience.
-                <a href="http://apple.com/safari">Safari</a> and
-                <a href="http://opera.com">Opera</a> work too.
+                Please install <a href="https://www.google.com/chrome">Chrome</a> or <a href="https://getfirefox.com">Firefox</a> for
+                the best experience. <a href="http://apple.com/safari">Safari</a> and <a href="http://opera.com">Opera</a> work too.
                 </p>
                 <p>
                 With your current browser, the look and feel of the application may
diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js
index e369a26e..5fb0324c 100644
--- a/src/components/structures/RoomDirectory.js
+++ b/src/components/structures/RoomDirectory.js
@@ -31,6 +31,8 @@ var linkifyMatrix = require('matrix-react-sdk/lib/linkify-matrix');
 var sanitizeHtml = require('sanitize-html');
 var q = require('q');
 
+import {instanceForInstanceId, protocolNameForInstanceId} from '../../utils/DirectoryUtils';
+
 linkifyMatrix(linkify);
 
 module.exports = React.createClass({
@@ -42,9 +44,7 @@ module.exports = React.createClass({
 
     getDefaultProps: function() {
         return {
-            config: {
-                networks: [],
-            },
+            config: {},
         }
     },
 
@@ -52,36 +52,26 @@ module.exports = React.createClass({
         return {
             publicRooms: [],
             loading: true,
-            network: null,
+            protocolsLoading: true,
+            instanceId: null,
+            includeAll: false,
             roomServer: null,
             filterString: null,
         }
     },
 
     componentWillMount: function() {
-        // precompile Regexps
-        this.portalRoomPatterns = {};
-        this.nativePatterns = {};
-        if (this.props.config.networks) {
-            for (const network of Object.keys(this.props.config.networks)) {
-                const network_info = this.props.config.networks[network];
-                if (network_info.portalRoomPattern) {
-                    this.portalRoomPatterns[network] = new RegExp(network_info.portalRoomPattern);
-                }
-                if (network_info.nativePattern) {
-                    this.nativePatterns[network] = new RegExp(network_info.nativePattern);
-                }
-            }
-        }
-
         this.nextBatch = null;
         this.filterTimeout = null;
         this.scrollPanel = null;
         this.protocols = null;
 
+        this.setState({protocolsLoading: true});
         MatrixClientPeg.get().getThirdpartyProtocols().done((response) => {
             this.protocols = response;
+            this.setState({protocolsLoading: false});
         }, (err) => {
+            this.setState({protocolsLoading: false});
             if (MatrixClientPeg.get().isGuest()) {
                 // Guests currently aren't allowed to use this API, so
                 // ignore this as otherwise this error is literally the
@@ -131,6 +121,11 @@ module.exports = React.createClass({
         if (my_server != MatrixClientPeg.getHomeServerName()) {
             opts.server = my_server;
         }
+        if (this.state.instanceId) {
+            opts.third_party_instance_id = this.state.instanceId;
+        } else if (this.state.includeAll) {
+            opts.include_all_networks = true;
+        }
         if (this.nextBatch) opts.since = this.nextBatch;
         if (my_filter_string) opts.filter = { generic_search_term: my_filter_string } ;
         return MatrixClientPeg.get().publicRooms(opts).then((data) => {
@@ -231,7 +226,7 @@ module.exports = React.createClass({
         }
     },
 
-    onOptionChange: function(server, network) {
+    onOptionChange: function(server, instanceId, includeAll) {
         // clear next batch so we don't try to load more rooms
         this.nextBatch = null;
         this.setState({
@@ -240,7 +235,8 @@ module.exports = React.createClass({
             // to clear the list anyway.
             publicRooms: [],
             roomServer: server,
-            network: network,
+            instanceId: instanceId,
+            includeAll: includeAll,
         }, this.refreshRoomList);
         // We also refresh the room list each time even though this
         // filtering is client-side. It hopefully won't be client side
@@ -271,7 +267,7 @@ module.exports = React.createClass({
         this.filterTimeout = setTimeout(() => {
             this.filterTimeout = null;
             this.refreshRoomList();
-        }, 300);
+        }, 700);
     },
 
     onFilterClear: function() {
@@ -286,14 +282,19 @@ module.exports = React.createClass({
     },
 
     onJoinClick: function(alias) {
-        // If we're on the 'Matrix' network (or all networks),
-        // just show that rooms alias
-        if (this.state.network == null || this.state.network == '_matrix') {
+        // If we don't have a particular instance id selected, just show that rooms alias
+        if (!this.state.instanceId) {
+            // If the user specified an alias without a domain, add on whichever server is selected
+            // in the dropdown
+            if (alias.indexOf(':') == -1) {
+                alias = alias + ':' + this.state.roomServer;
+            }
             this.showRoomAlias(alias);
         } else {
-            // This is a 3rd party protocol. Let's see if we
-            // can join it
-            const fields = this._getFieldsForThirdPartyLocation(alias, this.state.network);
+            // This is a 3rd party protocol. Let's see if we can join it
+            const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
+            const instance = instanceForInstanceId(this.protocols, this.state.instanceId);
+            const fields = protocolName ? this._getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance) : null;
             if (!fields) {
                 const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
                 Modal.createDialog(ErrorDialog, {
@@ -302,8 +303,7 @@ module.exports = React.createClass({
                 });
                 return;
             }
-            const protocol = this._protocolForThirdPartyNetwork(this.state.network);
-            MatrixClientPeg.get().getThirdpartyLocation(protocol, fields).done((resp) => {
+            MatrixClientPeg.get().getThirdpartyLocation(protocolName, fields).done((resp) => {
                 if (resp.length > 0 && resp[0].alias) {
                     this.showRoomAlias(resp[0].alias);
                 } else {
@@ -372,13 +372,7 @@ module.exports = React.createClass({
 
         if (!this.state.publicRooms) return [];
 
-        var rooms = this.state.publicRooms.filter((a) => {
-            if (this.state.network) {
-                if (!this._isRoomInNetwork(a, this.state.roomServer, this.state.network)) return false;
-            }
-
-            return true;
-        });
+        var rooms = this.state.publicRooms;
         var rows = [];
         var self = this;
         var guestRead, guestJoin, perms;
@@ -440,119 +434,46 @@ module.exports = React.createClass({
         this.scrollPanel = element;
     },
 
-    /**
-     * Terrible temporary function that guess what network a public room
-     * entry is in, until synapse is able to tell us
-     */
-    _isRoomInNetwork: function(room, server, network) {
-        // We carve rooms into two categories here. 'portal' rooms are
-        // rooms created by a user joining a bridge 'portal' alias to
-        // participate in that room or a foreign network. A room is a
-        // portal room if it has exactly one alias and that alias matches
-        // a pattern defined in the config. Its network is the key
-        // of the pattern that it matches.
-        // All other rooms are considered 'native matrix' rooms, and
-        // go into the special '_matrix' network.
-
-        let roomNetwork = '_matrix';
-        if (room.aliases && room.aliases.length == 1) {
-            if (this.props.config.serverConfig && this.props.config.serverConfig[server] && this.props.config.serverConfig[server].networks) {
-                for (const n of this.props.config.serverConfig[server].networks) {
-                    const pat = this.portalRoomPatterns[n];
-                    if (pat && pat.test(room.aliases[0])) {
-                        roomNetwork = n;
-                    }
-                }
-            }
-        }
-        return roomNetwork == network;
-    },
-
-    _stringLooksLikeId: function(s, network) {
+    _stringLooksLikeId: function(s, field_type) {
         let pat = /^#[^\s]+:[^\s]/;
-        if (
-            network && network != '_matrix' &&
-            this.nativePatterns[network]
-        ) {
-            pat = this.nativePatterns[network];
+        if (field_type && field_type.regexp) {
+            pat = new RegExp(field_type.regexp);
         }
 
         return pat.test(s);
     },
 
-    _protocolForThirdPartyNetwork: function(network) {
-        if (
-            this.props.config.networks &&
-            this.props.config.networks[network] &&
-            this.props.config.networks[network].protocol
-        ) {
-            return this.props.config.networks[network].protocol;
-        }
-    },
-
-    _getFieldsForThirdPartyLocation: function(user_input, network) {
-        if (!this.props.config.networks || !this.props.config.networks[network]) return null;
-
-        const network_info = this.props.config.networks[network];
-        if (!network_info.protocol) return null;
-
-        if (!this.protocols) return null;
-
-        let matched_instance;
-        // Try to find which instance in the 'protocols' response
-        // matches this network. We look for a matching protocol
-        // and the existence of a 'domain' field and if present,
-        // its value.
-        if (
-            this.protocols[network_info.protocol] &&
-            this.protocols[network_info.protocol].instances &&
-            this.protocols[network_info.protocol].instances.length == 1
-        ) {
-            const the_instance = this.protocols[network_info.protocol].instances[0];
-            // If there's only one instance in this protocol, use it
-            // as long as it has no domain (which we assume to mean it's
-            // there is only one possible instance).
-            if (
-                (
-                    the_instance.fields.domain === undefined &&
-                    network_info.domain === undefined
-                ) ||
-                (
-                    the_instance.fields.domain !== undefined &&
-                    the_instance.fields.domain == network_info.domain
-                )
-            ) {
-                matched_instance = the_instance;
-            }
-        } else if (network_info.domain) {
-            // otherwise, we look for one with a matching domain.
-            for (const this_instance of this.protocols[network_info.protocol].instances) {
-                if (this_instance.fields.domain == network_info.domain) {
-                    matched_instance = this_instance;
-                }
-            }
-        }
-
-        if (matched_instance === undefined) return null;
-
-        // now make an object with the fields specified by that protocol. We
+    _getFieldsForThirdPartyLocation: function(userInput, protocol, instance) {
+        // make an object with the fields specified by that protocol. We
         // require that the values of all but the last field come from the
         // instance. The last is the user input.
-        const required_fields = this.protocols[network_info.protocol].location_fields;
+        const requiredFields = protocol.location_fields;
+        if (!requiredFields) return null;
         const fields = {};
-        for (let i = 0; i < required_fields.length - 1; ++i) {
-            const this_field = required_fields[i];
-            if (matched_instance.fields[this_field] === undefined) return null;
-            fields[this_field] = matched_instance.fields[this_field];
+        for (let i = 0; i < requiredFields.length - 1; ++i) {
+            const thisField = requiredFields[i];
+            if (instance.fields[thisField] === undefined) return null;
+            fields[thisField] = instance.fields[thisField];
         }
-        fields[required_fields[required_fields.length - 1]] = user_input;
+        fields[requiredFields[requiredFields.length - 1]] = userInput;
         return fields;
     },
 
     render: function() {
+        const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
+        const Loader = sdk.getComponent("elements.Spinner");
+
+        if (this.state.protocolsLoading) {
+            return (
+                <div className="mx_RoomDirectory">
+                    <SimpleRoomHeader title="Directory" />
+                    <Loader />
+                </div>
+            );
+        }
+
         let content;
         if (this.state.loading) {
-            const Loader = sdk.getComponent("elements.Spinner");
             content = <div className="mx_RoomDirectory">
                 <Loader />
             </div>;
@@ -583,26 +504,35 @@ module.exports = React.createClass({
             </ScrollPanel>;
         }
 
-        let placeholder = 'Search for a room';
-        if (this.state.network === null || this.state.network === '_matrix') {
-            placeholder = '#example:' + this.state.roomServer;
-        } else if (
-            this.props.config.networks &&
-            this.props.config.networks[this.state.network] &&
-            this.props.config.networks[this.state.network].example &&
-            this._getFieldsForThirdPartyLocation(this.state.filterString, this.state.network)
+        const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
+        let instance_expected_field_type;
+        if (
+            protocolName &&
+            this.protocols &&
+            this.protocols[protocolName] &&
+            this.protocols[protocolName].location_fields.length > 0 &&
+            this.protocols[protocolName].field_types
         ) {
-            placeholder = this.props.config.networks[this.state.network].example;
+            const last_field = this.protocols[protocolName].location_fields.slice(-1)[0];
+            instance_expected_field_type = this.protocols[protocolName].field_types[last_field];
         }
 
-        let showJoinButton = this._stringLooksLikeId(this.state.filterString, this.state.network);
-        if (this.state.network && this.state.network != '_matrix') {
-            if (this._getFieldsForThirdPartyLocation(this.state.filterString, this.state.network) === null) {
+
+        let placeholder = 'Search for a room';
+        if (!this.state.instanceId) {
+            placeholder = '#example:' + this.state.roomServer;
+        } else if (instance_expected_field_type) {
+            placeholder = instance_expected_field_type.placeholder;
+        }
+
+        let showJoinButton = this._stringLooksLikeId(this.state.filterString, instance_expected_field_type);
+        if (protocolName) {
+            const instance = instanceForInstanceId(this.protocols, this.state.instanceId);
+            if (this._getFieldsForThirdPartyLocation(this.state.filterString, this.protocols[protocolName], instance) === null) {
                 showJoinButton = false;
             }
         }
 
-        const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
         const NetworkDropdown = sdk.getComponent('directory.NetworkDropdown');
         const DirectorySearchBox = sdk.getComponent('elements.DirectorySearchBox');
         return (
@@ -615,7 +545,7 @@ module.exports = React.createClass({
                             onChange={this.onFilterChange} onClear={this.onFilterClear} onJoinClick={this.onJoinClick}
                             placeholder={placeholder} showJoinButton={showJoinButton}
                         />
-                        <NetworkDropdown config={this.props.config} onOptionChange={this.onOptionChange} />
+                        <NetworkDropdown config={this.props.config} protocols={this.protocols} onOptionChange={this.onOptionChange} />
                     </div>
                     {content}
                 </div>
diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js
index 1dbc5319..a0e198ce 100644
--- a/src/components/structures/ViewSource.js
+++ b/src/components/structures/ViewSource.js
@@ -22,7 +22,8 @@ module.exports = React.createClass({
     displayName: 'ViewSource',
 
     propTypes: {
-        onFinished: React.PropTypes.func.isRequired
+        content: React.PropTypes.object.isRequired,
+        onFinished: React.PropTypes.func.isRequired,
     },
 
     componentDidMount: function() {
@@ -45,10 +46,9 @@ module.exports = React.createClass({
         return (
             <div className="mx_ViewSource">
                 <pre>
-                    {JSON.stringify(this.props.mxEvent.event, null, 2)}
+                    {JSON.stringify(this.props.content, null, 2)}
                 </pre>
             </div>
         );
     }
 });
-
diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js
index 7786b9bd..e3640f77 100644
--- a/src/components/views/context_menus/MessageContextMenu.js
+++ b/src/components/views/context_menus/MessageContextMenu.js
@@ -47,7 +47,16 @@ module.exports = React.createClass({
     onViewSourceClick: function() {
         var ViewSource = sdk.getComponent('structures.ViewSource');
         Modal.createDialog(ViewSource, {
-            mxEvent: this.props.mxEvent
+            content: this.props.mxEvent.event,
+        }, 'mx_Dialog_viewsource');
+        if (this.props.onFinished) this.props.onFinished();
+    },
+
+    onViewClearSourceClick: function() {
+        const ViewSource = sdk.getComponent('structures.ViewSource');
+        Modal.createDialog(ViewSource, {
+            // FIXME: _clearEvent is private
+            content: this.props.mxEvent._clearEvent,
         }, 'mx_Dialog_viewsource');
         if (this.props.onFinished) this.props.onFinished();
     },
@@ -97,6 +106,7 @@ module.exports = React.createClass({
         var eventStatus = this.props.mxEvent.status;
         var resendButton;
         var viewSourceButton;
+        var viewClearSourceButton;
         var redactButton;
         var cancelButton;
         var permalinkButton;
@@ -133,6 +143,14 @@ module.exports = React.createClass({
             </div>
         );
 
+        if (this.props.mxEvent.getType() !== this.props.mxEvent.getWireType()) {
+            viewClearSourceButton = (
+                <div className="mx_MessageContextMenu_field" onClick={this.onViewClearSourceClick}>
+                    View Decrypted Source
+                </div>
+            );
+        }
+
         if (this.props.eventTileOps) {
             if (this.props.eventTileOps.isWidgetHidden()) {
                 unhidePreviewButton = (
@@ -174,6 +192,7 @@ module.exports = React.createClass({
                 {redactButton}
                 {cancelButton}
                 {viewSourceButton}
+                {viewClearSourceButton}
                 {unhidePreviewButton}
                 {permalinkButton}
                 {UserSettingsStore.isFeatureEnabled('rich_text_editor') ? quoteButton : null}
diff --git a/src/components/views/context_menus/NotificationStateContextMenu.js b/src/components/views/context_menus/NotificationStateContextMenu.js
index 243275db..d4b40d17 100644
--- a/src/components/views/context_menus/NotificationStateContextMenu.js
+++ b/src/components/views/context_menus/NotificationStateContextMenu.js
@@ -120,22 +120,22 @@ module.exports = React.createClass({
                 </div>
                 <div className={ alertMeClasses } onClick={this._onClickAlertMe} >
                     <img className="mx_NotificationStateContextMenu_activeIcon" src="img/notif-active.svg" width="12" height="12" />
-                    <img className="mx_NotificationStateContextMenu_icon" src="img/icon-context-mute-off-copy.svg" width="16" height="12" />
+                    <img className="mx_NotificationStateContextMenu_icon mx_filterFlipColor" src="img/icon-context-mute-off-copy.svg" width="16" height="12" />
                     All messages (loud)
                 </div>
                 <div className={ allNotifsClasses } onClick={this._onClickAllNotifs} >
                     <img className="mx_NotificationStateContextMenu_activeIcon" src="img/notif-active.svg" width="12" height="12" />
-                    <img className="mx_NotificationStateContextMenu_icon" src="img/icon-context-mute-off.svg" width="16" height="12" />
+                    <img className="mx_NotificationStateContextMenu_icon mx_filterFlipColor" src="img/icon-context-mute-off.svg" width="16" height="12" />
                     All messages
                 </div>
                 <div className={ mentionsClasses } onClick={this._onClickMentions} >
                     <img className="mx_NotificationStateContextMenu_activeIcon" src="img/notif-active.svg" width="12" height="12" />
-                    <img className="mx_NotificationStateContextMenu_icon" src="img/icon-context-mute-mentions.svg" width="16" height="12" />
+                    <img className="mx_NotificationStateContextMenu_icon mx_filterFlipColor" src="img/icon-context-mute-mentions.svg" width="16" height="12" />
                     Mentions only
                 </div>
                 <div className={ muteNotifsClasses } onClick={this._onClickMute} >
                     <img className="mx_NotificationStateContextMenu_activeIcon" src="img/notif-active.svg" width="12" height="12" />
-                    <img className="mx_NotificationStateContextMenu_icon" src="img/icon-context-mute.svg" width="16" height="12" />
+                    <img className="mx_NotificationStateContextMenu_icon mx_filterFlipColor" src="img/icon-context-mute.svg" width="16" height="12" />
                     Mute
                 </div>
             </div>
diff --git a/src/components/views/dialogs/BugReportDialog.js b/src/components/views/dialogs/BugReportDialog.js
new file mode 100644
index 00000000..dcc0850e
--- /dev/null
+++ b/src/components/views/dialogs/BugReportDialog.js
@@ -0,0 +1,127 @@
+/*
+Copyright 2017 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+import sdk from 'matrix-react-sdk';
+import rageshake from '../../../vector/rageshake';
+
+export default class BugReportDialog extends React.Component {
+    constructor(props, context) {
+        super(props, context);
+        this.state = {
+            sendLogs: true,
+            busy: false,
+            err: null,
+            text: "",
+        };
+        this._onSubmit = this._onSubmit.bind(this);
+        this._onCancel = this._onCancel.bind(this);
+        this._onTextChange = this._onTextChange.bind(this);
+        this._onSendLogsChange = this._onSendLogsChange.bind(this);
+    }
+
+    _onCancel(ev) {
+        this.props.onFinished(false);
+    }
+
+    _onSubmit(ev) {
+        const sendLogs = this.state.sendLogs;
+        const userText = this.state.text;
+        if (!sendLogs && userText.trim().length === 0) {
+            this.setState({
+                err: "Please describe the bug and/or send logs.",
+            });
+            return;
+        }
+        this.setState({ busy: true, err: null });
+        rageshake.sendBugReport(userText, sendLogs).then(() => {
+            this.setState({ busy: false });
+            this.props.onFinished(false);
+        }, (err) => {
+            this.setState({ busy: false, err: `Failed: ${err.message}` });
+        });
+    }
+
+    _onTextChange(ev) {
+        this.setState({ text: ev.target.value });
+    }
+
+    _onSendLogsChange(ev) {
+        this.setState({ sendLogs: ev.target.checked });
+    }
+
+    render() {
+        const Loader = sdk.getComponent("elements.Spinner");
+
+        let error = null;
+        if (this.state.err) {
+            error = <div className="error">
+                {this.state.err}
+            </div>;
+        }
+
+        const okLabel = this.state.busy ? <Loader /> : 'Send';
+
+        let cancelButton = null;
+        if (!this.state.busy) {
+            cancelButton = <button onClick={this._onCancel}>
+                Cancel
+            </button>;
+        }
+
+        return (
+            <div className="mx_BugReportDialog">
+                <div className="mx_Dialog_title">
+                    Report a bug
+                </div>
+                <div className="mx_Dialog_content">
+                    <p>Please describe the bug. What did you do?
+                    What did you expect to happen?
+                    What actually happened?</p>
+                    <textarea
+                        className="mx_BugReportDialog_input"
+                        rows={5}
+                        onChange={this._onTextChange}
+                        value={this.state.text}
+                        placeholder="Describe your problem here."
+                    />
+                    <p>In order to diagnose problems, logs from this client will be sent with
+                    this bug report.
+                    If you would prefer to only send the text above, please untick:</p>
+                    <input type="checkbox" checked={this.state.sendLogs}
+                        onChange={this._onSendLogsChange} id="mx_BugReportDialog_logs"/>
+                    <label htmlFor="mx_BugReportDialog_logs">Send logs</label>
+                    {error}
+                </div>
+                <div className="mx_Dialog_buttons">
+                    <button
+                        className="mx_Dialog_primary danger"
+                        onClick={this._onSubmit}
+                        autoFocus={true}
+                    >
+                        {okLabel}
+                    </button>
+
+                    {cancelButton}
+                </div>
+            </div>
+        );
+    }
+}
+
+BugReportDialog.propTypes = {
+    onFinished: React.PropTypes.func.isRequired,
+};
diff --git a/src/components/views/dialogs/ChangelogDialog.js b/src/components/views/dialogs/ChangelogDialog.js
index 6bc6de60..d14d8dc2 100644
--- a/src/components/views/dialogs/ChangelogDialog.js
+++ b/src/components/views/dialogs/ChangelogDialog.js
@@ -31,9 +31,10 @@ export default class ChangelogDialog extends React.Component {
         const version = this.props.newVersion.split('-');
         const version2 = this.props.version.split('-');
         if(version == null || version2 == null) return;
+        // parse versions of form: [vectorversion]-react-[react-sdk-version]-js-[js-sdk-version]
         for(let i=0; i<REPOS.length; i++) {
-            const oldVersion = version2[2*i+1];
-            const newVersion = version[2*i+1];
+            const oldVersion = version2[2*i];
+            const newVersion = version[2*i];
             request(`https://api.github.com/repos/${REPOS[i]}/compare/${oldVersion}...${newVersion}`, (a, b, body) => {
                 if(body == null) return;
                 this.setState({[REPOS[i]]: JSON.parse(body).commits});
diff --git a/src/components/views/directory/NetworkDropdown.js b/src/components/views/directory/NetworkDropdown.js
index eb60c4a5..4ce094bc 100644
--- a/src/components/views/directory/NetworkDropdown.js
+++ b/src/components/views/directory/NetworkDropdown.js
@@ -16,6 +16,9 @@ limitations under the License.
 
 import React from 'react';
 import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
+import {instanceForInstanceId} from '../../../utils/DirectoryUtils';
+
+const DEFAULT_ICON_URL = "img/network-matrix.svg";
 
 export default class NetworkDropdown extends React.Component {
     constructor(props) {
@@ -35,20 +38,11 @@ export default class NetworkDropdown extends React.Component {
         this.inputTextBox = null;
 
         const server = MatrixClientPeg.getHomeServerName();
-        let defaultNetwork = null;
-        if (
-            this.props.config.serverConfig &&
-            this.props.config.serverConfig[server] &&
-            this.props.config.serverConfig[server].networks &&
-            this.props.config.serverConfig[server].networks.indexOf('_matrix') > -1
-        ) {
-            defaultNetwork = '_matrix';
-        }
-
         this.state = {
             expanded: false,
             selectedServer: server,
-            selectedNetwork: defaultNetwork,
+            selectedInstance: null,
+            includeAllNetworks: false,
         };
     }
 
@@ -58,7 +52,7 @@ export default class NetworkDropdown extends React.Component {
         document.addEventListener('click', this.onDocumentClick, false);
 
         // fire this now so the defaults can be set up
-        this.props.onOptionChange(this.state.selectedServer, this.state.selectedNetwork);
+        this.props.onOptionChange(this.state.selectedServer, this.state.selectedInstance, this.state.includeAllNetworks);
     }
 
     componentWillUnmount() {
@@ -98,13 +92,14 @@ export default class NetworkDropdown extends React.Component {
         ev.preventDefault();
     }
 
-    onMenuOptionClick(server, network, ev) {
+    onMenuOptionClick(server, instance, includeAll) {
         this.setState({
             expanded: false,
             selectedServer: server,
-            selectedNetwork: network,
+            selectedInstanceId: instance ? instance.instance_id : null,
+            includeAll: includeAll,
         });
-        this.props.onOptionChange(server, network);
+        this.props.onOptionChange(server, instance ? instance.instance_id : null, includeAll);
     }
 
     onInputKeyUp(e) {
@@ -144,11 +139,22 @@ export default class NetworkDropdown extends React.Component {
             servers.unshift(MatrixClientPeg.getHomeServerName());
         }
 
+        // For our own HS, we can use the instance_ids given in the third party protocols
+        // response to get the server to filter the room list by network for us.
+        // We can't get thirdparty protocols for remote server yet though, so for those
+        // we can only show the default room list.
         for (const server of servers) {
-            options.push(this._makeMenuOption(server, null));
-            if (this.props.config.serverConfig && this.props.config.serverConfig[server] && this.props.config.serverConfig[server].networks) {
-                for (const network of this.props.config.serverConfig[server].networks) {
-                    options.push(this._makeMenuOption(server, network));
+            options.push(this._makeMenuOption(server, null, true));
+            if (server == MatrixClientPeg.getHomeServerName()) {
+                options.push(this._makeMenuOption(server, null, false));
+                if (this.props.protocols) {
+                    for (const proto of Object.keys(this.props.protocols)) {
+                        if (!this.props.protocols[proto].instances) continue;
+                        for (const instance of this.props.protocols[proto].instances) {
+                            if (!instance.instance_id) continue;
+                            options.push(this._makeMenuOption(server, instance, false));
+                        }
+                    }
                 }
             }
         }
@@ -156,50 +162,36 @@ export default class NetworkDropdown extends React.Component {
         return options;
     }
 
-    _makeMenuOption(server, network, wire_onclick) {
-        if (wire_onclick === undefined) wire_onclick = true;
+    _makeMenuOption(server, instance, includeAll, handleClicks) {
+        if (handleClicks === undefined) handleClicks = true;
+
         let icon;
         let name;
         let span_class;
+        let key;
 
-        if (network === null) {
+        if (!instance && includeAll) {
+            key = server;
             name = server;
             span_class = 'mx_NetworkDropdown_menu_all';
-        } else if (network == '_matrix') {
+        } else if (!instance) {
+            key = server + '_all';
             name = 'Matrix';
-            icon = <img src="img/network-matrix.svg" width="16" height="16" />;
+            icon = <img src="img/network-matrix.svg" />;
             span_class = 'mx_NetworkDropdown_menu_network';
         } else {
-            if (this.props.config.networks[network] === undefined) {
-                throw new Error(network + ' network missing from config');
-            }
-            if (this.props.config.networks[network].name) {
-                name = this.props.config.networks[network].name;
-            } else {
-                name = network;
-            }
-            if (this.props.config.networks[network].icon) {
-                // omit height here so if people define a non-square logo in the config, it
-                // will keep the aspect when it scales
-                icon = <img src={this.props.config.networks[network].icon} width="16" />;
-            } else {
-                icon = <img src={iconPath} width="16" height="16" />;
-            }
-
+            key = server + '_inst_' + instance.instance_id;
+            icon = <img src={instance.icon || DEFAULT_ICON_URL} />;
+            name = instance.desc;
             span_class = 'mx_NetworkDropdown_menu_network';
         }
 
-        const click_handler = wire_onclick ? this.onMenuOptionClick.bind(this, server, network) : null;
-
-        let key = server;
-        if (network !== null) {
-            key += '_' + network;
-        }
+        const click_handler = handleClicks ? this.onMenuOptionClick.bind(this, server, instance, includeAll) : null;
 
         return <div key={key} className="mx_NetworkDropdown_networkoption" onClick={click_handler}>
             {icon}
-            <span className={span_class}>{name}</span>
-        </div>;
+            <span className="mx_NetworkDropdown_menu_network">{name}</span>
+        </div>
     }
 
     render() {
@@ -216,8 +208,9 @@ export default class NetworkDropdown extends React.Component {
                 placeholder="matrix.org" // 'matrix.org' as an example of an HS name
             />
         } else {
+            const instance = instanceForInstanceId(this.props.protocols, this.state.selectedInstanceId);
             current_value = this._makeMenuOption(
-                this.state.selectedServer, this.state.selectedNetwork, false
+                this.state.selectedServer, instance, this.state.includeAll, false
             );
         }
 
@@ -233,12 +226,12 @@ export default class NetworkDropdown extends React.Component {
 
 NetworkDropdown.propTypes = {
     onOptionChange: React.PropTypes.func.isRequired,
+    protocols: React.PropTypes.object,
+    // The room directory config. May have a 'servers' key that is a list of server names to include in the dropdown
     config: React.PropTypes.object,
 };
 
 NetworkDropdown.defaultProps = {
-    config: {
-        networks: [],
-    }
+    protocols: {},
+    config: {},
 };
-
diff --git a/src/components/views/globals/NewVersionBar.js b/src/components/views/globals/NewVersionBar.js
index 36d6bc71..3e03be72 100644
--- a/src/components/views/globals/NewVersionBar.js
+++ b/src/components/views/globals/NewVersionBar.js
@@ -23,11 +23,11 @@ import PlatformPeg from 'matrix-react-sdk/lib/PlatformPeg';
 
 /**
  * Check a version string is compatible with the Changelog
- * dialog
+ * dialog ([vectorversion]-react-[react-sdk-version]-js-[js-sdk-version])
  */
 function checkVersion(ver) {
     const parts = ver.split('-');
-    return parts[0] == 'vector' && parts[2] == 'react' && parts[4] == 'js';
+    return parts.length == 5 && parts[1] == 'react' && parts[3] == 'js';
 }
 
 export default React.createClass({
diff --git a/src/components/views/login/VectorLoginHeader.js b/src/components/views/login/VectorLoginHeader.js
index 32867230..daa2b09c 100644
--- a/src/components/views/login/VectorLoginHeader.js
+++ b/src/components/views/login/VectorLoginHeader.js
@@ -23,11 +23,14 @@ module.exports = React.createClass({
     statics: {
         replaces: 'LoginHeader',
     },
+    propTypes: {
+        icon: React.PropTypes.string,
+    },
 
     render: function() {
         return (
             <div className="mx_Login_logo">
-                <img src="img/logo.png" width="195" height="195" alt="Riot"/>
+                <img src={this.props.icon || "img/logo.png"} alt="Riot"/>
             </div>
         );
     }
diff --git a/src/skins/vector/css/common.css b/src/skins/vector/css/_common.scss
similarity index 81%
rename from src/skins/vector/css/common.css
rename to src/skins/vector/css/_common.scss
index bb00bbd8..bf31bea6 100644
--- a/src/skins/vector/css/common.css
+++ b/src/skins/vector/css/_common.scss
@@ -29,7 +29,8 @@ body {
        Arial here. */
     font-family: 'Open Sans', Arial, Helvetica, Sans-Serif;
     font-size: 15px;
-    color: #454545;
+    background-color: $primary-bg-color;
+    color: $primary-fg-color;
     border: 0px;
     margin: 0px;
     /* This should render the fonts the same accross browsers */
@@ -41,7 +42,7 @@ div.error {
 }
 
 h2 {
-    color: #454545;
+    color: $primary-fg-color;
     font-weight: 400;
     font-size: 18px;
     margin-top: 16px;
@@ -51,15 +52,20 @@ h2 {
 a:hover,
 a:link,
 a:visited {
-    color: #76cfa6;
+    color: $accent-color;
+}
+
+input[type=text], input[type=password], textarea {
+    background-color: transparent;
+    color: $primary-fg-color;
 }
 
 input[type=text].error, input[type=password].error {
-    border: 1px solid red;
+    border: 1px solid $warning-color;
 }
 
 input[type=text]:focus, textarea:focus {
-    border: 1px solid #76CFA6;
+    border: 1px solid $accent-color;
     outline: none;
     box-shadow: none;
 }
@@ -77,10 +83,7 @@ textarea {
 /* applied to side-panels and messagepanel when in RoomSettings */
 .mx_fadable {
     opacity: 1;
-    -webkit-transition: opacity 0.2s ease-in-out;
-    -moz-transition: opacity 0.2s ease-in-out;
-    -ms-transition: opacity 0.2s ease-in-out;
-    -o-transition: opacity 0.2s ease-in-out;
+    transition: opacity 0.2s ease-in-out;
 }
 
 /* XXX: critical hack to GeminiScrollbar to allow them to work in FF 42 and Chrome 48.
@@ -122,14 +125,8 @@ textarea {
     width: 100%;
     height: 100%;
 
-    display: -webkit-box;
-    display: -moz-box;
-    display: -ms-flexbox;
-    display: -webkit-flex;
     display: flex;
-    -webkit-align-items: center;
     align-items: center;
-    -webkit-justify-content: center;
     justify-content: center;
 }
 
@@ -148,8 +145,8 @@ textarea {
 }
 
 .mx_Dialog {
-    background-color: #fff;
-    color: #747474;
+    background-color: $primary-bg-color;
+    color: $light-fg-color;
     z-index: 4010;
     font-weight: 300;
     font-size: 15px;
@@ -168,13 +165,13 @@ textarea {
     left: 0;
     width: 100%;
     height: 100%;
-    background-color: #e9e9e9;
+    background-color: $dialog-background-bg-color;
     opacity: 0.8;
 }
 
 .mx_Dialog_lightbox .mx_Dialog_background {
     opacity: 0.85;
-    background-color: #000;
+    background-color: $lightbox-background-bg-color;
 }
 
 .mx_Dialog_lightbox .mx_Dialog {
@@ -190,7 +187,7 @@ textarea {
 .mx_Dialog_content {
     margin: 24px 58px 68px 0;
     font-size: 14px;
-    color: #4a4a4a;
+    color: $primary-fg-color;
     word-wrap: break-word;
 }
 
@@ -202,7 +199,7 @@ textarea {
     border: 0px;
     height: 36px;
     border-radius: 40px;
-    border: solid 1px #76cfa6;
+    border: solid 1px $accent-color;
     font-weight: 600;
     font-size: 14px;
     font-family: 'Open Sans', Arial, Helvetica, Sans-Serif;
@@ -212,26 +209,26 @@ textarea {
     padding-right: 1.5em;
     outline: none;
     cursor: pointer;
-    color: #76cfa6;
-    background-color: #fff;
+    color: $accent-color;
+    background-color: $primary-bg-color;
 
     /* align images in buttons (eg spinners) */
     vertical-align: middle;
 }
 
 .mx_Dialog button.mx_Dialog_primary, .mx_Dialog  input[type="submit"].mx_Dialog_primary {
-    color: #fff;
-    background-color: #76cfa6;
+    color: $accent-fg-color;
+    background-color: $accent-color;
 }
 
 .mx_Dialog button.danger, .mx_Dialog input[type="submit"].danger {
-    background-color: #ff0064;
-    border: solid 1px #ff0064;
+    background-color: $warning-color;
+    border: solid 1px $warning-color;
 }
 
 .mx_Dialog button:disabled, .mx_Dialog input[type="submit"]:disabled {
-    background-color: #777777;
-    border: solid 1px #777777;
+    background-color: $light-fg-color;
+    border: solid 1px $light-fg-color;
     opacity: 0.7;
 }
 
@@ -241,11 +238,11 @@ textarea {
     font-weight: bold;
     font-size: 22px;
     line-height: 1.4;
-    color: #454545;
+    color: $primary-fg-color;
 }
 
 .mx_Dialog_title.danger {
-    color: #ff0064;
+    color: $warning-color;
 }
 
 .mx_TextInputDialog_label {
@@ -256,10 +253,10 @@ textarea {
 .mx_TextInputDialog_input {
     font-size: 15px;
     border-radius: 3px;
-    border: 1px solid #f0f0f0;
+    border: 1px solid $input-border-color;
     padding: 9px;
-    color: #454545;
-    background-color: #fff;
+    color: $primary-fg-color;
+    background-color: $primary-bg-color;
 }
 
 .mx_emojione {
@@ -268,19 +265,19 @@ textarea {
 }
 
 ::-moz-selection {
-    background-color: #76CFA6;
-    color: white;
+    background-color: $accent-color;
+    color: $selection-fg-color;
 }
 
 ::selection {
-    background-color: #76CFA6;
-    color: white;
+    background-color: $accent-color;
+    color: $selection-fg-color;
 }
 
 /** green button with rounded corners */
 .mx_textButton {
-    color: #fff;
-    background-color: #76cfa6;
+    color: $accent-fg-color;
+    background-color: $accent-color;
     border-radius: 17px;
     text-align: center;
     padding-left: 1em;
diff --git a/src/skins/vector/css/_components.scss b/src/skins/vector/css/_components.scss
new file mode 100644
index 00000000..323c22e0
--- /dev/null
+++ b/src/skins/vector/css/_components.scss
@@ -0,0 +1,75 @@
+// autogenerated by rethemendex.sh
+@import "./_common.scss";
+@import "./matrix-react-sdk/structures/_ContextualMenu.scss";
+@import "./matrix-react-sdk/structures/_CreateRoom.scss";
+@import "./matrix-react-sdk/structures/_FilePanel.scss";
+@import "./matrix-react-sdk/structures/_MatrixChat.scss";
+@import "./matrix-react-sdk/structures/_NotificationPanel.scss";
+@import "./matrix-react-sdk/structures/_RoomStatusBar.scss";
+@import "./matrix-react-sdk/structures/_RoomView.scss";
+@import "./matrix-react-sdk/structures/_SearchBox.scss";
+@import "./matrix-react-sdk/structures/_UploadBar.scss";
+@import "./matrix-react-sdk/structures/_UserSettings.scss";
+@import "./matrix-react-sdk/structures/login/_Login.scss";
+@import "./matrix-react-sdk/views/avatars/_BaseAvatar.scss";
+@import "./matrix-react-sdk/views/dialogs/_BugReportDialog.scss";
+@import "./matrix-react-sdk/views/dialogs/_ChatInviteDialog.scss";
+@import "./matrix-react-sdk/views/dialogs/_EncryptedEventDialog.scss";
+@import "./matrix-react-sdk/views/dialogs/_SetDisplayNameDialog.scss";
+@import "./matrix-react-sdk/views/dialogs/_UnknownDeviceDialog.scss";
+@import "./matrix-react-sdk/views/elements/_AddressSelector.scss";
+@import "./matrix-react-sdk/views/elements/_AddressTile.scss";
+@import "./matrix-react-sdk/views/elements/_DirectorySearchBox.scss";
+@import "./matrix-react-sdk/views/elements/_MemberEventListSummary.scss";
+@import "./matrix-react-sdk/views/elements/_ProgressBar.scss";
+@import "./matrix-react-sdk/views/elements/_RichText.scss";
+@import "./matrix-react-sdk/views/login/_ServerConfig.scss";
+@import "./matrix-react-sdk/views/messages/_MImageBody.scss";
+@import "./matrix-react-sdk/views/messages/_MNoticeBody.scss";
+@import "./matrix-react-sdk/views/messages/_MTextBody.scss";
+@import "./matrix-react-sdk/views/messages/_TextualEvent.scss";
+@import "./matrix-react-sdk/views/messages/_UnknownBody.scss";
+@import "./matrix-react-sdk/views/rooms/_Autocomplete.scss";
+@import "./matrix-react-sdk/views/rooms/_EntityTile.scss";
+@import "./matrix-react-sdk/views/rooms/_EventTile.scss";
+@import "./matrix-react-sdk/views/rooms/_LinkPreviewWidget.scss";
+@import "./matrix-react-sdk/views/rooms/_MemberDeviceInfo.scss";
+@import "./matrix-react-sdk/views/rooms/_MemberInfo.scss";
+@import "./matrix-react-sdk/views/rooms/_MemberList.scss";
+@import "./matrix-react-sdk/views/rooms/_MessageComposer.scss";
+@import "./matrix-react-sdk/views/rooms/_PresenceLabel.scss";
+@import "./matrix-react-sdk/views/rooms/_RoomHeader.scss";
+@import "./matrix-react-sdk/views/rooms/_RoomList.scss";
+@import "./matrix-react-sdk/views/rooms/_RoomPreviewBar.scss";
+@import "./matrix-react-sdk/views/rooms/_RoomSettings.scss";
+@import "./matrix-react-sdk/views/rooms/_RoomTile.scss";
+@import "./matrix-react-sdk/views/rooms/_SearchableEntityList.scss";
+@import "./matrix-react-sdk/views/rooms/_TabCompleteBar.scss";
+@import "./matrix-react-sdk/views/rooms/_TopUnreadMessagesBar.scss";
+@import "./matrix-react-sdk/views/settings/_DevicesPanel.scss";
+@import "./matrix-react-sdk/views/settings/_IntegrationsManager.scss";
+@import "./matrix-react-sdk/views/voip/_CallView.scss";
+@import "./matrix-react-sdk/views/voip/_IncomingCallbox.scss";
+@import "./matrix-react-sdk/views/voip/_VideoView.scss";
+@import "./vector-web/_fonts.scss";
+@import "./vector-web/structures/_CompatibilityPage.scss";
+@import "./vector-web/structures/_LeftPanel.scss";
+@import "./vector-web/structures/_RightPanel.scss";
+@import "./vector-web/structures/_RoomDirectory.scss";
+@import "./vector-web/structures/_RoomSubList.scss";
+@import "./vector-web/structures/_ViewSource.scss";
+@import "./vector-web/views/context_menus/_MessageContextMenu.scss";
+@import "./vector-web/views/context_menus/_NotificationStateContextMenu.scss";
+@import "./vector-web/views/context_menus/_RoomTagContextMenu.scss";
+@import "./vector-web/views/dialogs/_ChangelogDialog.scss";
+@import "./vector-web/views/directory/_NetworkDropdown.scss";
+@import "./vector-web/views/elements/_ImageView.scss";
+@import "./vector-web/views/elements/_Spinner.scss";
+@import "./vector-web/views/globals/_GuestWarningBar.scss";
+@import "./vector-web/views/globals/_MatrixToolbar.scss";
+@import "./vector-web/views/messages/_MessageTimestamp.scss";
+@import "./vector-web/views/messages/_SenderProfile.scss";
+@import "./vector-web/views/rooms/_RoomDropTarget.scss";
+@import "./vector-web/views/rooms/_RoomTooltip.scss";
+@import "./vector-web/views/rooms/_SearchBar.scss";
+@import "./vector-web/views/settings/_Notifications.scss";
diff --git a/src/skins/vector/css/matrix-react-sdk/structures/ContextualMenu.css b/src/skins/vector/css/matrix-react-sdk/structures/_ContextualMenu.scss
similarity index 87%
rename from src/skins/vector/css/matrix-react-sdk/structures/ContextualMenu.css
rename to src/skins/vector/css/matrix-react-sdk/structures/_ContextualMenu.scss
index d317363d..d3e73a9a 100644
--- a/src/skins/vector/css/matrix-react-sdk/structures/ContextualMenu.css
+++ b/src/skins/vector/css/matrix-react-sdk/structures/_ContextualMenu.scss
@@ -30,10 +30,10 @@ limitations under the License.
 }
 
 .mx_ContextualMenu {
-    border: solid 1px rgba(187, 187, 187, 0.5);
+    border: solid 1px $menu-border-color;
     border-radius: 4px;
-    background-color: #f6f6f6;
-    color: #4a4a4a;
+    background-color: $menu-bg-color;
+    color: $primary-fg-color;
     position: absolute;
     padding: 6px;
 	font-size: 14px;
@@ -51,7 +51,7 @@ limitations under the License.
     width: 0;
     height: 0;
     border-top: 8px solid transparent;
-    border-left: 8px solid rgba(187, 187, 187, 0.5);
+    border-left: 8px solid $menu-border-color;
     border-bottom: 8px solid transparent;
 }
 
@@ -60,7 +60,7 @@ limitations under the License.
     width: 0;
     height: 0;
     border-top: 7px solid transparent;
-    border-left: 7px solid #f6f6f6;
+    border-left: 7px solid $menu-bg-color;
     border-bottom: 7px solid transparent;
     position:absolute;
     top: -7px;
@@ -78,7 +78,7 @@ limitations under the License.
     width: 0;
     height: 0;
     border-top: 8px solid transparent;
-    border-right: 8px solid rgba(187, 187, 187, 0.5);
+    border-right: 8px solid $menu-border-color;
     border-bottom: 8px solid transparent;
 }
 
@@ -87,7 +87,7 @@ limitations under the License.
     width: 0;
     height: 0;
     border-top: 7px solid transparent;
-    border-right: 7px solid #f6f6f6;
+    border-right: 7px solid $menu-bg-color;
     border-bottom: 7px solid transparent;
     position:absolute;
     top: -7px;
diff --git a/src/skins/vector/css/matrix-react-sdk/structures/CreateRoom.css b/src/skins/vector/css/matrix-react-sdk/structures/_CreateRoom.scss
similarity index 91%
rename from src/skins/vector/css/matrix-react-sdk/structures/CreateRoom.css
rename to src/skins/vector/css/matrix-react-sdk/structures/_CreateRoom.scss
index 88804409..2be19352 100644
--- a/src/skins/vector/css/matrix-react-sdk/structures/CreateRoom.css
+++ b/src/skins/vector/css/matrix-react-sdk/structures/_CreateRoom.scss
@@ -18,13 +18,13 @@ limitations under the License.
     width: 960px;
     margin-left: auto;
     margin-right: auto;
-    color: #4a4a4a;
+    color: $primary-fg-color;
 }
 
 .mx_CreateRoom input,
 .mx_CreateRoom textarea  {
     border-radius: 3px;
-    border: 1px solid #c7c7c7;
+    border: 1px solid $strong-input-border-color;
     font-weight: 300;
     font-size: 13px;
     padding: 9px;
diff --git a/src/skins/vector/css/matrix-react-sdk/structures/FilePanel.css b/src/skins/vector/css/matrix-react-sdk/structures/_FilePanel.scss
similarity index 90%
rename from src/skins/vector/css/matrix-react-sdk/structures/FilePanel.css
rename to src/skins/vector/css/matrix-react-sdk/structures/_FilePanel.scss
index fca53b1d..872085b6 100644
--- a/src/skins/vector/css/matrix-react-sdk/structures/FilePanel.css
+++ b/src/skins/vector/css/matrix-react-sdk/structures/_FilePanel.scss
@@ -15,13 +15,8 @@ limitations under the License.
 */
 
 .mx_FilePanel {
-    -webkit-box-ordinal-group: 2;
-    -moz-box-ordinal-group: 2;
-    -ms-flex-order: 2;
-    -webkit-order: 2;
     order: 2;
 
-    -webkit-flex: 1 1 0;
     flex: 1 1 0;
 
     width: 100%;
@@ -58,12 +53,12 @@ limitations under the License.
 .mx_FilePanel .mx_EventTile .mx_MImageBody_download {
     display: flex;
     font-size: 14px;
-    color: #acacac;
+    color: $event-timestamp-color;
 }
 
 .mx_FilePanel .mx_EventTile .mx_MImageBody_downloadLink {
     flex: 1 1 auto;
-    color: #747474;
+    color: $light-fg-color;
 }
 
 .mx_FilePanel .mx_EventTile .mx_MImageBody_size {
@@ -90,7 +85,7 @@ limitations under the License.
     padding: 0px;
     font-size: 11px;
     opacity: 1.0;
-    color: #acacac;
+    color: $event-timestamp-color;
 }
 
 .mx_FilePanel .mx_EventTile .mx_MessageTimestamp {
@@ -100,7 +95,7 @@ limitations under the License.
     position: initial;
     font-size: 11px;
     opacity: 1.0;
-    color: #acacac;
+    color: $event-timestamp-color;
 }
 
 /* Overrides for the wrappers around the body tile */
@@ -111,7 +106,7 @@ limitations under the License.
 }
 
 .mx_FilePanel .mx_EventTile:hover .mx_EventTile_line {
-    background-color: #fff;
+    background-color: $primary-bg-color;
 }
 
 .mx_FilePanel .mx_EventTile_selected .mx_EventTile_line {
diff --git a/src/skins/vector/css/matrix-react-sdk/structures/MatrixChat.css b/src/skins/vector/css/matrix-react-sdk/structures/_MatrixChat.scss
similarity index 65%
rename from src/skins/vector/css/matrix-react-sdk/structures/MatrixChat.css
rename to src/skins/vector/css/matrix-react-sdk/structures/_MatrixChat.scss
index f1cc7d4a..05a39ea7 100644
--- a/src/skins/vector/css/matrix-react-sdk/structures/MatrixChat.css
+++ b/src/skins/vector/css/matrix-react-sdk/structures/_MatrixChat.scss
@@ -27,34 +27,21 @@ limitations under the License.
 }
 
 .mx_MatrixChat_wrapper {
-    display: -webkit-box;
-    display: -moz-box;
-    display: -ms-flexbox;
-    display: -webkit-flex;
     display: flex;
 
     flex-direction: column;
-    -webkit-flex-direction: column;
 
     width: 100%;
     height: 100%;
 }
 
 .mx_MatrixToolbar {
-    -webkit-box-ordinal-group: 1;
-    -moz-box-ordinal-group: 1;
-    -ms-flex-order: 1;
-    -webkit-order: 1;
     order: 1;
 
     height: 40px;
 }
 
 .mx_GuestWarningBar {
-    -webkit-box-ordinal-group: 1;
-    -moz-box-ordinal-group: 1;
-    -ms-flex-order: 1;
-    -webkit-order: 1;
     order: 1;
 
     height: 40px;
@@ -68,52 +55,32 @@ limitations under the License.
     width: 100%;
     height: 100%;
 
-    display: -webkit-box;
-    display: -moz-box;
-    display: -ms-flexbox;
-    display: -webkit-flex;
     display: flex;
 
-    -webkit-box-ordinal-group: 2;
-    -moz-box-ordinal-group: 2;
-    -ms-flex-order: 2;
-    -webkit-order: 2;
     order: 2;
 
-    -webkit-flex: 1;
     flex: 1;
 }
 
 .mx_MatrixChat .mx_LeftPanel {
-    -webkit-box-ordinal-group: 1;
-    -moz-box-ordinal-group: 1;
-    -ms-flex-order: 1;
-    -webkit-order: 1;
     order: 1;
 
-    background-color: #eaf5f0;
+    background-color: $secondary-accent-color;
 
-    -webkit-flex: 0 0 235px;
     flex: 0 0 235px;
 }
 
 .mx_MatrixChat .mx_LeftPanel.collapsed {
-    -webkit-flex: 0 0 60px;
     flex: 0 0 60px;
 }
 
 .mx_MatrixChat .mx_MatrixChat_middlePanel {
-    -webkit-box-ordinal-group: 2;
-    -moz-box-ordinal-group: 2;
-    -ms-flex-order: 2;
-    -webkit-order: 2;
     order: 2;
 
     padding-left: 20px;
     padding-right: 22px;
-    background-color: #fff;
+    background-color: $primary-bg-color;
 
-    -webkit-flex: 1;
     flex: 1;
 
     /* Experimental fix for https://github.com/vector-im/vector-web/issues/947
@@ -132,25 +99,15 @@ limitations under the License.
      * point, but instead we fudge it and make the middlePanel
      * flex itself.
      */
-    display: -webkit-box;
-    display: -moz-box;
-    display: -ms-flexbox;
-    display: -webkit-flex;
     display: flex;
 }
 
 .mx_MatrixChat .mx_RightPanel {
-    -webkit-box-ordinal-group: 3;
-    -moz-box-ordinal-group: 3;
-    -ms-flex-order: 3;
-    -webkit-order: 3;
     order: 3;
 
-    -webkit-flex: 0 0 235px;
     flex: 0 0 235px;
 }
 
 .mx_MatrixChat .mx_RightPanel.collapsed {
-    -webkit-flex: 0 0 122px;
     flex: 0 0 122px;
 }
diff --git a/src/skins/vector/css/matrix-react-sdk/structures/NotificationPanel.css b/src/skins/vector/css/matrix-react-sdk/structures/_NotificationPanel.scss
similarity index 91%
rename from src/skins/vector/css/matrix-react-sdk/structures/NotificationPanel.css
rename to src/skins/vector/css/matrix-react-sdk/structures/_NotificationPanel.scss
index 83b0a033..ef75678d 100644
--- a/src/skins/vector/css/matrix-react-sdk/structures/NotificationPanel.css
+++ b/src/skins/vector/css/matrix-react-sdk/structures/_NotificationPanel.scss
@@ -15,13 +15,8 @@ limitations under the License.
 */
 
 .mx_NotificationPanel {
-    -webkit-box-ordinal-group: 2;
-    -moz-box-ordinal-group: 2;
-    -ms-flex-order: 2;
-    -webkit-order: 2;
     order: 2;
 
-    -webkit-flex: 1 1 0;
     flex: 1 1 0;
 
     width: 100%;
@@ -51,7 +46,7 @@ limitations under the License.
 }
 
 .mx_NotificationPanel .mx_EventTile_roomName a {
-    color: #4a4a4a;
+    color: $primary-fg-color;
 }
 
 .mx_NotificationPanel .mx_EventTile_avatar {
@@ -61,8 +56,7 @@ limitations under the License.
 
 .mx_NotificationPanel .mx_EventTile .mx_SenderProfile, 
 .mx_NotificationPanel .mx_EventTile .mx_MessageTimestamp {
-    color: #000;
-    opacity: 0.3;
+    color: $primary-fg-color;
     font-size: 12px;
     display: inline;
     padding-left: 0px;
@@ -94,7 +88,7 @@ limitations under the License.
 }
 
 .mx_NotificationPanel .mx_EventTile:hover .mx_EventTile_line {
-    background-color: #fff;
+    background-color: $primary-bg-color;
 }
 
 .mx_NotificationPanel .mx_EventTile_selected .mx_EventTile_line {
diff --git a/src/skins/vector/css/matrix-react-sdk/structures/RoomStatusBar.css b/src/skins/vector/css/matrix-react-sdk/structures/_RoomStatusBar.scss
similarity index 73%
rename from src/skins/vector/css/matrix-react-sdk/structures/RoomStatusBar.css
rename to src/skins/vector/css/matrix-react-sdk/structures/_RoomStatusBar.scss
index ef0b69c4..d124f9c6 100644
--- a/src/skins/vector/css/matrix-react-sdk/structures/RoomStatusBar.css
+++ b/src/skins/vector/css/matrix-react-sdk/structures/_RoomStatusBar.scss
@@ -21,10 +21,10 @@ limitations under the License.
 
 /* position the indicator in the same place horizontally as .mx_EventTile_avatar. */
 .mx_RoomStatusBar_indicator {
-    padding-left: 18px;
+    padding-left: 17px;
     padding-right: 12px;
     margin-left: -73px;
-    margin-top: 13px;
+    margin-top: 8px;
     float: left;
     width: 24px;
     text-align: center;
@@ -36,16 +36,16 @@ limitations under the License.
 }
 
 .mx_RoomStatusBar_placeholderIndicator span {
-    color: #4a4a4a;
+    color: $primary-fg-color;
     opacity: 0.5;
     position: relative;
     top: -4px;
-/*    
+/*
     animation-duration: 1s;
     animation-name: bounce;
     animation-direction: alternate;
     animation-iteration-count: infinite;
-*/    
+*/
 }
 
 .mx_RoomStatusBar_placeholderIndicator span:nth-child(1) {
@@ -70,13 +70,43 @@ limitations under the License.
     }
 }
 
+.mx_RoomStatusBar_typingIndicatorAvatars {
+    width: 52px;
+    text-align: left;
+}
+
+.mx_RoomStatusBar_typingIndicatorAvatars .mx_BaseAvatar_image {
+    margin-right: -12px;
+    border: 1px solid $primary-bg-color;
+}
+
+.mx_RoomStatusBar_typingIndicatorAvatars .mx_BaseAvatar_initial {
+    padding-left: 1px;
+    padding-top: 1px;
+}
+
+.mx_RoomStatusBar_typingIndicatorRemaining {
+    display: inline-block;
+    color: #acacac;
+    background-color: #ddd;
+    border: 1px solid $primary-bg-color;
+    border-radius: 40px;
+    width: 24px;
+    height: 24px;
+    line-height: 24px;
+    font-size: 0.8em;
+    vertical-align: top;
+    text-align: center;
+    position: absolute;
+}
+
 .mx_RoomStatusBar_scrollDownIndicator {
     cursor: pointer;
 }
 
 .mx_RoomStatusBar_unreadMessagesBar {
     padding-top: 10px;
-    color: #ff0064;
+    color: $warning-color;
     cursor: pointer;
 }
 
@@ -93,29 +123,29 @@ limitations under the License.
 }
 
 .mx_RoomStatusBar_connectionLostBar_title {
-    color: #ff0064;
+    color: $warning-color;
 }
 
 .mx_RoomStatusBar_connectionLostBar_desc {
-    color: #454545;
+    color: $primary-fg-color;
     font-size: 13px;
     opacity: 0.5;
 }
 
 .mx_RoomStatusBar_resend_link {
-    color: #454545 ! important;
+    color: $primary-fg-color ! important;
     text-decoration: underline ! important;
     cursor: pointer;
 }
 
 .mx_RoomStatusBar_tabCompleteBar {
     padding-top: 10px;
-    color: #4a4a4a;
+    color: $primary-fg-color;
 }
 
 .mx_RoomStatusBar_typingBar {
     padding-top: 10px;
-    color: #4a4a4a;
+    color: $primary-fg-color;
     opacity: 0.5;
     overflow-y: hidden;
     display: block;
@@ -123,19 +153,16 @@ limitations under the License.
 
 .mx_RoomStatusBar_tabCompleteWrapper {
     display: flex;
-    display: -webkit-flex;
     height: 26px;
 }
 
 .mx_RoomStatusBar_tabCompleteWrapper .mx_TabCompleteBar {
     flex: 1 1 auto;
-    -webkit-flex: 1 1 auto;
 }
 
 .mx_RoomStatusBar_tabCompleteEol {
     flex: 0 0 auto;
-    -webkit-flex: 0 0 auto;
-    color: #76CFA6;
+    color: $accent-color;
 }
 
 .mx_RoomStatusBar_tabCompleteEol object {
diff --git a/src/skins/vector/css/matrix-react-sdk/structures/RoomView.css b/src/skins/vector/css/matrix-react-sdk/structures/_RoomView.scss
similarity index 67%
rename from src/skins/vector/css/matrix-react-sdk/structures/RoomView.css
rename to src/skins/vector/css/matrix-react-sdk/structures/_RoomView.scss
index c3f7ceed..55771f79 100644
--- a/src/skins/vector/css/matrix-react-sdk/structures/RoomView.css
+++ b/src/skins/vector/css/matrix-react-sdk/structures/_RoomView.scss
@@ -18,25 +18,15 @@ limitations under the License.
     word-wrap: break-word;
     position: relative;
 
-    display: -webkit-box;
-    display: -moz-box;
-    display: -ms-flexbox;
-    display: -webkit-flex;
     display: flex;
     width: 100%;
 
     flex-direction: column;
-    -webkit-flex-direction: column;
 }
 
 .mx_RoomView .mx_RoomHeader {
-    -webkit-box-ordinal-group: 1;
-    -moz-box-ordinal-group: 1;
-    -ms-flex-order: 1;
-    -webkit-order: 1;
     order: 1;
 
-    -webkit-flex: 0 0 70px;
     flex: 0 0 70px;
 }
 
@@ -53,14 +43,10 @@ limitations under the License.
     padding-right: 12px;
     margin-left: -12px;
 
-    -webkit-border-top-left-radius: 10px;
-    -webkit-border-top-right-radius: 10px;
-    -moz-border-radius-topleft: 10px;
-    -moz-border-radius-topright: 10px;
     border-top-left-radius: 10px;
     border-top-right-radius: 10px;
 
-    background-color: rgba(255, 255, 255, 0.9);
+    background-color: $droptarget-bg-color;
     border: 2px #e1dddd solid;
     border-bottom: none;
     position: absolute;
@@ -77,10 +63,6 @@ limitations under the License.
 }
 
 .mx_RoomView_auxPanel {
-    -webkit-box-ordinal-group: 2;
-    -moz-box-ordinal-group: 2;
-    -ms-flex-order: 2;
-    -webkit-order: 2;
     order: 2;
 
     min-width: 0px;
@@ -89,28 +71,18 @@ limitations under the License.
     margin: auto;
 
     overflow: auto;
-    border-bottom: 1px solid #e5e5e5;
+    border-bottom: 1px solid $primary-hairline-color;
 
-    -webkit-flex: 0 0 auto;
     flex: 0 0 auto;
 }
 
 .mx_RoomView_topUnreadMessagesBar {
-    -webkit-box-ordinal-group: 3;
-    -moz-box-ordinal-group: 3;
-    -ms-flex-order: 3;
-    -webkit-order: 3;
     order: 3;
 }
 
 .mx_RoomView_messagePanel {
-    -webkit-box-ordinal-group: 4;
-    -moz-box-ordinal-group: 4;
-    -ms-flex-order: 4;
-    -webkit-order: 4;
     order: 4;
 
-    -webkit-flex: 1 1 0;
     flex: 1 1 0;
 
     width: 100%;
@@ -124,22 +96,15 @@ limitations under the License.
 
     min-height: 100%;
 
-    display: -webkit-box;
-    display: -moz-box;
-    display: -ms-flexbox;
-    display: -webkit-flex;
     display: flex;
 
     flex-direction: column;
-    -webkit-flex-direction: column;
 
     justify-content: flex-end;
-    -webkit-justify-content: flex-end;
 }
 
 .mx_RoomView_searchResultsPanel .mx_RoomView_messageListWrapper {
     justify-content: flex-start;
-    -webkit-justify-content: flex-start;
 }
 
 .mx_RoomView_MessageList {
@@ -158,14 +123,10 @@ limitations under the License.
     margin-bottom: 8px;
     margin-left: 63px;
     padding-bottom: 6px;
-    border-bottom: 1px solid #e5e5e5;
+    border-bottom: 1px solid $primary-hairline-color;
 }
 
 .mx_RoomView_invitePrompt {
-    -webkit-box-ordinal-group: 2;
-    -moz-box-ordinal-group: 2;
-    -ms-flex-order: 2;
-    -webkit-order: 2;
     order: 2;
 
     min-width: 0px;
@@ -185,23 +146,32 @@ li.mx_RoomView_myReadMarker_container {
 }
 
 hr.mx_RoomView_myReadMarker {
-    border-top: solid 1px #76cfa6;
-    border-bottom: solid 1px #76cfa6;
+    border-top: solid 1px $accent-color;
+    border-bottom: solid 1px $accent-color;
     margin-top: 0px;
     position: relative;
     top: 5px;
 }
 
 .mx_RoomView_statusArea {
-    -webkit-box-ordinal-group: 5;
-    -moz-box-ordinal-group: 5;
-    -ms-flex-order: 5;
-    -webkit-order: 5;
     order: 5;
 
     width: 100%;
-    -webkit-flex: 0 0 auto;
     flex: 0 0 auto;
+
+    max-height: 0px;
+    background-color: $primary-bg-color;
+    z-index: 1000;
+    overflow: hidden;
+
+    -webkit-transition: all .2s ease-out;
+    -moz-transition: all .2s ease-out;
+    -ms-transition: all .2s ease-out;
+    -o-transition: all .2s ease-out;
+}
+
+.mx_RoomView_statusArea_expanded {
+    max-height: 100px;
 }
 
 .mx_RoomView_statusAreaBox {
@@ -212,16 +182,16 @@ hr.mx_RoomView_myReadMarker {
 
 .mx_RoomView_statusAreaBox_line {
     margin-left: 65px;
-    border-top: 1px solid #e5e5e5;
+    border-top: 1px solid $primary-hairline-color;
     height: 1px;
 }
 
 .mx_RoomView_callStatusBar .mx_UploadBar_uploadProgressInner {
-    background-color: #fff;
+    background-color: $primary-bg-color;
 }
 
 .mx_RoomView_callStatusBar .mx_UploadBar_uploadFilename {
-    color: #fff;
+    color: $accent-fg-color;
     opacity: 1.0;
 }
 
@@ -234,8 +204,8 @@ hr.mx_RoomView_myReadMarker {
 }
 
 .mx_RoomView_inCall .mx_RoomView_statusAreaBox {
-    background-color: #76CFA6;
-    color: #fff;
+    background-color: $accent-color;
+    color: $accent-fg-color;
     position: relative;
 }
 
@@ -257,14 +227,9 @@ hr.mx_RoomView_myReadMarker {
 }
 
 .mx_RoomView .mx_MessageComposer {
-    -webkit-box-ordinal-group: 6;
-    -moz-box-ordinal-group: 6;
-    -ms-flex-order: 6;
-    -webkit-order: 6;
     order: 6;
 
     width: 100%;
-    -webkit-flex: 0 0 auto;
     flex: 0 0 auto;
     margin-right: 2px;
 }
@@ -272,13 +237,13 @@ hr.mx_RoomView_myReadMarker {
 .mx_RoomView_ongoingConfCallNotification {
     width: 100%;
     text-align: center;
-    background-color: #ff0064;
-    color: #fff;
+    background-color: $warning-color;
+    color: $accent-fg-color;
     font-weight: bold;
     padding: 6px 0;
     cursor: pointer;
 }
 
 .mx_RoomView_ongoingConfCallNotification a {
-    color: #fff ! important;
+    color: $accent-fg-color ! important;
 }
\ No newline at end of file
diff --git a/src/skins/vector/css/matrix-react-sdk/structures/SearchBox.css b/src/skins/vector/css/matrix-react-sdk/structures/_SearchBox.scss
similarity index 94%
rename from src/skins/vector/css/matrix-react-sdk/structures/SearchBox.css
rename to src/skins/vector/css/matrix-react-sdk/structures/_SearchBox.scss
index 0b536259..0f34f056 100644
--- a/src/skins/vector/css/matrix-react-sdk/structures/SearchBox.css
+++ b/src/skins/vector/css/matrix-react-sdk/structures/_SearchBox.scss
@@ -22,7 +22,6 @@ limitations under the License.
     padding-bottom: 22px;
 
     display: flex;
-    display: -webkit-flex;
 }
 
 .mx_SearchBox_searchButton {
@@ -38,7 +37,6 @@ limitations under the License.
 
 .mx_SearchBox_search {
     flex: 1 1 auto;
-    -webkit-flex: 1 1 auto;
     width: 0px;
     font-family: 'Open Sans', Arial, Helvetica, Sans-Serif;
     font-size: 12px;
@@ -46,7 +44,6 @@ limitations under the License.
     height: 24px;
     border: 0px ! important;
     /* border-bottom: 1px solid rgba(0, 0, 0, 0.1) ! important; */
-    background-color: transparent;
     border: 0px;
 }
 
diff --git a/src/skins/vector/css/matrix-react-sdk/structures/UploadBar.css b/src/skins/vector/css/matrix-react-sdk/structures/_UploadBar.scss
similarity index 93%
rename from src/skins/vector/css/matrix-react-sdk/structures/UploadBar.css
rename to src/skins/vector/css/matrix-react-sdk/structures/_UploadBar.scss
index b489e132..d76c8166 100644
--- a/src/skins/vector/css/matrix-react-sdk/structures/UploadBar.css
+++ b/src/skins/vector/css/matrix-react-sdk/structures/_UploadBar.scss
@@ -26,7 +26,7 @@ limitations under the License.
 }
 
 .mx_UploadBar_uploadProgressInner {
-    background-color: #76cfa6;
+    background-color: $accent-color;
     height: 5px;
 }
 
@@ -34,7 +34,7 @@ limitations under the License.
     margin-top: 5px;
     margin-left: 65px;
     opacity: 0.5;
-    color: #4a4a4a;
+    color: $primary-fg-color;
 }
 
 .mx_UploadBar_uploadIcon {
@@ -57,5 +57,5 @@ limitations under the License.
     float: right;
     margin-top: 5px;
     margin-right: 30px;
-    color: #76cfa6;
+    color: $accent-color;
 }
diff --git a/src/skins/vector/css/matrix-react-sdk/structures/UserSettings.css b/src/skins/vector/css/matrix-react-sdk/structures/_UserSettings.scss
similarity index 84%
rename from src/skins/vector/css/matrix-react-sdk/structures/UserSettings.css
rename to src/skins/vector/css/matrix-react-sdk/structures/_UserSettings.scss
index f48a88a9..5c1b4c5c 100644
--- a/src/skins/vector/css/matrix-react-sdk/structures/UserSettings.css
+++ b/src/skins/vector/css/matrix-react-sdk/structures/_UserSettings.scss
@@ -20,34 +20,19 @@ limitations under the License.
     margin-left: auto;
     margin-right: auto;
 
-    display: -webkit-box;
-    display: -moz-box;
-    display: -ms-flexbox;
-    display: -webkit-flex;
     display: flex;
     flex-direction: column;
-    -webkit-flex-direction: column;
 }
 
 .mx_UserSettings .mx_RoomHeader {
-    -webkit-box-ordinal-group: 1;
-    -moz-box-ordinal-group: 1;
-    -ms-flex-order: 1;
-    -webkit-order: 1;
     order: 1;
 
-    -webkit-flex: 0 0 70px;
     flex: 0 0 70px;
 }
 
 .mx_UserSettings_body {
-    -webkit-box-ordinal-group: 2;
-    -moz-box-ordinal-group: 2;
-    -ms-flex-order: 2;
-    -webkit-order: 2;
     order: 2;
 
-    -webkit-flex: 1 1 0;
     flex: 1 1 0;
 
     margin-top: -20px;
@@ -58,7 +43,7 @@ limitations under the License.
     clear: both;
     margin-left: 63px;
     text-transform: uppercase;
-    color: #3d3b39;
+    color: $h3-color;
     font-weight: 600;
     font-size: 13px;
     margin-top: 26px;
@@ -84,8 +69,8 @@ limitations under the License.
     border-radius: 36px;
     font-weight: 400;
     font-size: 16px;
-    color: #fff;
-    background-color: #76cfa6;
+    color: $accent-fg-color;
+    background-color: $accent-color;
     width: auto;
     margin: auto;
     padding: 7px;
@@ -95,7 +80,7 @@ limitations under the License.
 }
 
 .mx_UserSettings_button.danger {
-    background-color: #ff0064;
+    background-color: $warning-color;
 }
 
 .mx_UserSettings_section {
@@ -166,15 +151,15 @@ limitations under the License.
 {
     display: inline-block;
     border: 0px;
-    border-bottom: 1px solid rgba(151, 151, 151, 0.5);
+    border-bottom: 1px solid $input-underline-color;
     padding: 0px;
     width: 240px;
-    color: rgba(74, 74, 74, 0.9);
+    color: $input-fg-color;
     font-family: 'Open Sans', Helvetica, Arial, Sans-Serif;
     font-size: 16px;
 }
 
-.mx_UserSettings_addThreepid {
+.mx_UserSettings_threepidButton {
     display: table-cell;
     padding-left: 0.5em;
     position: relative;
diff --git a/src/skins/vector/css/matrix-react-sdk/structures/login/Login.css b/src/skins/vector/css/matrix-react-sdk/structures/login/_Login.scss
similarity index 84%
rename from src/skins/vector/css/matrix-react-sdk/structures/login/Login.css
rename to src/skins/vector/css/matrix-react-sdk/structures/login/_Login.scss
index 0f610b25..39c65e5d 100644
--- a/src/skins/vector/css/matrix-react-sdk/structures/login/Login.css
+++ b/src/skins/vector/css/matrix-react-sdk/structures/login/_Login.scss
@@ -18,21 +18,15 @@ limitations under the License.
     width: 100%;
     height: 100%;
 
-    display: -webkit-box;
-    display: -moz-box;
-    display: -ms-flexbox;
-    display: -webkit-flex;
     display: flex;
-    -webkit-align-items: center;
     align-items: center;
-    -webkit-justify-content: center;
     justify-content: center;
 
     overflow: auto;
 }
 
 .mx_Login h2 {
-    color: #4a4a4a;
+    color: $primary-fg-color;
     font-weight: 300;
     margin-top: 32px;
     margin-bottom: 20px;
@@ -48,12 +42,24 @@ limitations under the License.
 
 .mx_Login_logo {
     text-align: center;
+    height: 195px;
+}
+
+.mx_Login_logo img {
+    height: 100%
+}
+
+.mx_Login_support {
+    text-align: center;
+    font-size: 13px;
+    margin-top: 0px;
+    opacity: 0.7;
 }
 
 .mx_Login_field {
-    width: 100%;
+    width: 280px;
     border-radius: 3px;
-    border: 1px solid #c7c7c7;
+    border: 1px solid $strong-input-border-color;
     font-weight: 300;
     font-size: 13px;
     padding: 9px;
@@ -75,9 +81,9 @@ limitations under the License.
     border-radius: 40px;
     height: 40px;
     border: 0px;
-    background-color: #76cfa6;
+    background-color: $accent-color;
     font-size: 15px;
-    color: #fff;
+    color: $accent-fg-color;
 }
 
 .mx_Login_label {
@@ -99,7 +105,7 @@ limitations under the License.
 }
 
 .mx_Login_create:link {
-    color: #4a4a4a;
+    color: $primary-fg-color;
 }
 
 .mx_Login_links {
@@ -112,7 +118,7 @@ limitations under the License.
 }
 
 .mx_Login_links a:link {
-   color: #4a4a4a;
+   color: $primary-fg-color;
 }
 
 .mx_Login_prompt {
@@ -127,7 +133,7 @@ limitations under the License.
 }
 
 .mx_Login_forgot:link {
-    color: #4a4a4a;
+    color: $primary-fg-color;
 }
 
 .mx_Login_loader {
@@ -147,7 +153,7 @@ limitations under the License.
 }
 
 .mx_Login_error {
-    color: #ff2020;
+    color: $warning-color;
     font-weight: bold;
     text-align: center;
 /*
diff --git a/src/skins/vector/css/matrix-react-sdk/views/avatars/BaseAvatar.css b/src/skins/vector/css/matrix-react-sdk/views/avatars/_BaseAvatar.scss
similarity index 93%
rename from src/skins/vector/css/matrix-react-sdk/views/avatars/BaseAvatar.css
rename to src/skins/vector/css/matrix-react-sdk/views/avatars/_BaseAvatar.scss
index 901a2959..f23b929b 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/avatars/BaseAvatar.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/avatars/_BaseAvatar.scss
@@ -21,7 +21,7 @@ limitations under the License.
 .mx_BaseAvatar_initial {
     position: absolute;
     z-index: 1;
-    color: #fff;
+    color: $avatar-initial-color;
     text-align: center;
     speak: none;
     pointer-events: none;
@@ -31,4 +31,5 @@ limitations under the License.
 .mx_BaseAvatar_image {
     border-radius: 40px;
     vertical-align: top;
+    background-color: #fff;
 }
diff --git a/src/skins/vector/css/matrix-react-sdk/views/dialogs/_BugReportDialog.scss b/src/skins/vector/css/matrix-react-sdk/views/dialogs/_BugReportDialog.scss
new file mode 100644
index 00000000..0f47e974
--- /dev/null
+++ b/src/skins/vector/css/matrix-react-sdk/views/dialogs/_BugReportDialog.scss
@@ -0,0 +1,20 @@
+/*
+Copyright 2017 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_BugReportDialog_input {
+    width: 100%;
+    box-sizing: border-box;
+}
diff --git a/src/skins/vector/css/matrix-react-sdk/views/dialogs/ChatInviteDialog.css b/src/skins/vector/css/matrix-react-sdk/views/dialogs/_ChatInviteDialog.scss
similarity index 89%
rename from src/skins/vector/css/matrix-react-sdk/views/dialogs/ChatInviteDialog.css
rename to src/skins/vector/css/matrix-react-sdk/views/dialogs/_ChatInviteDialog.scss
index aa1dced8..4a214b51 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/dialogs/ChatInviteDialog.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/dialogs/_ChatInviteDialog.scss
@@ -36,7 +36,7 @@ limitations under the License.
 
 .mx_ChatInviteDialog_inputContainer {
     border-radius: 3px;
-    border: solid 1px #f0f0f0;
+    border: solid 1px $input-border-color;
     line-height: 36px;
     padding-left: 4px;
     padding-right: 4px;
@@ -49,7 +49,7 @@ limitations under the License.
 
 .mx_ChatInviteDialog_error {
     position: absolute;
-    color: #ff0064;
+    color: $warning-color;
     line-height: 36px;
 }
 
@@ -63,3 +63,9 @@ limitations under the License.
 .mx_ChatInviteDialog_cancel object {
     pointer-events: none;
 }
+
+.mx_ChatInviteDialog_addressSelectHeader {
+    font-weight: bold;
+    line-height: 150%;
+    text-indent: 4px;
+}
diff --git a/src/skins/vector/css/matrix-react-sdk/views/dialogs/EncryptedEventDialog.css b/src/skins/vector/css/matrix-react-sdk/views/dialogs/_EncryptedEventDialog.scss
similarity index 90%
rename from src/skins/vector/css/matrix-react-sdk/views/dialogs/EncryptedEventDialog.css
rename to src/skins/vector/css/matrix-react-sdk/views/dialogs/_EncryptedEventDialog.scss
index 5e72d7f8..cbc0195c 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/dialogs/EncryptedEventDialog.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/dialogs/_EncryptedEventDialog.scss
@@ -24,7 +24,7 @@ limitations under the License.
     border: 0px;
     height: 36px;
     border-radius: 40px;
-    border: solid 1px #76cfa6;
+    border: solid 1px $accent-color;
     font-weight: 600;
     font-size: 14px;
     font-family: 'Open Sans', Arial, Helvetica, Sans-Serif;
@@ -34,6 +34,6 @@ limitations under the License.
     padding-right: 1.5em;
     outline: none;
     cursor: pointer;
-    color: #76cfa6;
-    background-color: #fff;
+    color: $accent-color;
+    background-color: $primary-bg-color;
 }
\ No newline at end of file
diff --git a/src/skins/vector/css/matrix-react-sdk/views/dialogs/SetDisplayNameDialog.css b/src/skins/vector/css/matrix-react-sdk/views/dialogs/_SetDisplayNameDialog.scss
similarity index 85%
rename from src/skins/vector/css/matrix-react-sdk/views/dialogs/SetDisplayNameDialog.css
rename to src/skins/vector/css/matrix-react-sdk/views/dialogs/_SetDisplayNameDialog.scss
index 68409a80..2f0750ad 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/dialogs/SetDisplayNameDialog.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/dialogs/_SetDisplayNameDialog.scss
@@ -16,9 +16,9 @@ limitations under the License.
 
 .mx_SetDisplayNameDialog_input {
     border-radius: 3px;
-    border: 1px solid #f0f0f0;
+    border: 1px solid $input-border-color;
     padding: 9px;
-    color: #454545;
-    background-color: #fff;
+    color: $primary-fg-color;
+    background-color: $primary-bg-color;
     font-size: 15px;
 }
\ No newline at end of file
diff --git a/src/skins/vector/css/matrix-react-sdk/views/dialogs/_UnknownDeviceDialog.scss b/src/skins/vector/css/matrix-react-sdk/views/dialogs/_UnknownDeviceDialog.scss
new file mode 100644
index 00000000..3a9b64d1
--- /dev/null
+++ b/src/skins/vector/css/matrix-react-sdk/views/dialogs/_UnknownDeviceDialog.scss
@@ -0,0 +1,46 @@
+/*
+Copyright 2016 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_UnknownDeviceDialog .mx_MemberDeviceInfo {
+    float: right;
+    clear: both;
+    padding: 0px;
+    padding-top: 8px;
+}
+
+.mx_UnknownDeviceDialog .mx_MemberDeviceInfo_textButton {
+    border: 0px;
+    height: 24px;
+    border-radius: 40px;
+    border: solid 1px $accent-color;
+    font-weight: 600;
+    font-size: 13px;
+    font-family: 'Open Sans', Arial, Helvetica, Sans-Serif;
+    margin-left: 0px;
+    margin-right: 8px;
+    padding-left: 0.5em;
+    padding-right: 0.5em;
+    width: 70px;
+    outline: none;
+    cursor: pointer;
+    color: $accent-color;
+    background-color: $primary-bg-color;
+}
+
+.mx_UnknownDeviceDialog .mx_UnknownDeviceDialog_deviceList li {
+    height: 40px;
+    border-bottom: 1px solid $primary-hairline-color;
+}
\ No newline at end of file
diff --git a/src/skins/vector/css/matrix-react-sdk/views/elements/AddressSelector.css b/src/skins/vector/css/matrix-react-sdk/views/elements/_AddressSelector.scss
similarity index 76%
rename from src/skins/vector/css/matrix-react-sdk/views/elements/AddressSelector.css
rename to src/skins/vector/css/matrix-react-sdk/views/elements/_AddressSelector.scss
index aa0f8c6a..9871a7e8 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/elements/AddressSelector.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/elements/_AddressSelector.scss
@@ -16,13 +16,13 @@ limitations under the License.
 
 .mx_AddressSelector {
     position: absolute;
-    background-color: #fff;
+    background-color: $primary-bg-color;
     width: 485px;
     max-height: 116px;
     overflow-y: auto;
 	border-radius: 3px;
-	background-color: #fff;
-	border: solid 1px #76cfa6;
+	background-color: $primary-bg-color;
+	border: solid 1px $accent-color;
     cursor: pointer;
 }
 
@@ -31,15 +31,15 @@ limitations under the License.
 }
 
 .mx_AddressSelector_addressListElement .mx_AddressTile {
-    background-color: #fff;
-    border: solid 1px #fff;
+    background-color: $primary-bg-color;
+    border: solid 1px $primary-bg-color;
 }
 
 .mx_AddressSelector_addressListElement.mx_AddressSelector_selected {
-     background-color: #eaf5f0; /* selected colour */
+    background-color: $selected-color;
 }
 
 .mx_AddressSelector_addressListElement.mx_AddressSelector_selected .mx_AddressTile {
-    background-color: #eaf5f0; /* selected colour */
-    border: solid 1px #eaf5f0; /* selected colour */
+    background-color: $selected-color;
+    border: solid 1px $selected-color;
 }
diff --git a/src/skins/vector/css/matrix-react-sdk/views/elements/AddressTile.css b/src/skins/vector/css/matrix-react-sdk/views/elements/_AddressTile.scss
similarity index 94%
rename from src/skins/vector/css/matrix-react-sdk/views/elements/AddressTile.css
rename to src/skins/vector/css/matrix-react-sdk/views/elements/_AddressTile.scss
index 76c0e603..0ecfb17c 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/elements/AddressTile.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/elements/_AddressTile.scss
@@ -18,9 +18,9 @@ limitations under the License.
     display: inline-block;
     border-radius: 3px;
     background-color: rgba(74, 73, 74, 0.1);
-    border: solid 1px #f0f0f0;
+    border: solid 1px $input-border-color;
     line-height: 26px;
-    color: #454545;
+    color: $primary-fg-color;
     font-size: 14px;
     font-weight: normal;
     margin-right: 4px;
@@ -28,8 +28,8 @@ limitations under the License.
 
 .mx_AddressTile.mx_AddressTile_error {
     background-color: rgba(255, 0, 100, 0.1);
-    color: #ff0064;
-    border-color: #ff0064;
+    color: $warning-color;
+    border-color: $warning-color;
 }
 
 .mx_AddressTile_network {
@@ -106,7 +106,7 @@ limitations under the License.
 }
 
 .mx_AddressTile_email.mx_AddressTile_justified {
-    width: 380px; /* name + id width */
+    width: 200px; /* same as id width */
     overflow: hidden;
     white-space: nowrap;
     text-overflow: ellipsis;
diff --git a/src/skins/vector/css/matrix-react-sdk/views/elements/DirectorySearchBox.css b/src/skins/vector/css/matrix-react-sdk/views/elements/_DirectorySearchBox.scss
similarity index 87%
rename from src/skins/vector/css/matrix-react-sdk/views/elements/DirectorySearchBox.css
rename to src/skins/vector/css/matrix-react-sdk/views/elements/_DirectorySearchBox.scss
index 3a0922bc..94a92b23 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/elements/DirectorySearchBox.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/elements/_DirectorySearchBox.scss
@@ -17,19 +17,17 @@ limitations under the License.
 .mx_DirectorySearchBox {
     position: relative;
     border-radius: 3px;
-    border: 1px solid #c7c7c7;
+    border: 1px solid $strong-input-border-color;
 }
 
 .mx_DirectorySearchBox_container {
     display: flex;
-    display: -webkit-flex;
     padding-left: 9px;
     padding-right: 9px;
 }
 
 .mx_DirectorySearchBox_input {
     flex-grow: 1;
-    -webkit-flex-grow: 1;
     border: 0;
     padding: 0;
     font-weight: 300;
@@ -44,9 +42,9 @@ input[type=text].mx_DirectorySearchBox_input:focus {
     padding: 3px;
     padding-left: 10px;
     padding-right: 10px;
-    background-color: #efefef;
+    background-color: $plinth-bg-color;
     border-radius: 3px;
-    background-image: url('img/icon-return.svg');
+    background-image: url('../../img/icon-return.svg');
     background-position: 8px 70%;
     background-repeat: no-repeat;
     text-indent: 18px;
@@ -63,7 +61,7 @@ input[type=text].mx_DirectorySearchBox_input:focus {
 .mx_DirectorySearchBox_clear {
     display: inline-block;
     vertical-align: middle;
-    background: url('img/icon_context_delete.svg');
+    background: url('../../img/icon_context_delete.svg');
     background-position: 0 50%;
     background-repeat: no-repeat;
     width: 15px;
diff --git a/src/skins/vector/css/matrix-react-sdk/views/elements/MemberEventListSummary.css b/src/skins/vector/css/matrix-react-sdk/views/elements/_MemberEventListSummary.scss
similarity index 97%
rename from src/skins/vector/css/matrix-react-sdk/views/elements/MemberEventListSummary.css
rename to src/skins/vector/css/matrix-react-sdk/views/elements/_MemberEventListSummary.scss
index b8197805..7d1b059b 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/elements/MemberEventListSummary.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/elements/_MemberEventListSummary.scss
@@ -31,6 +31,6 @@ limitations under the License.
 }
 
 .mx_MemberEventListSummary_toggle {
-    color:#76cfa6;
+    color:$accent-color;
     cursor:pointer;
 }
diff --git a/src/skins/vector/css/matrix-react-sdk/views/elements/ProgressBar.css b/src/skins/vector/css/matrix-react-sdk/views/elements/_ProgressBar.scss
similarity index 88%
rename from src/skins/vector/css/matrix-react-sdk/views/elements/ProgressBar.css
rename to src/skins/vector/css/matrix-react-sdk/views/elements/_ProgressBar.scss
index 7b5e0c74..a3fee232 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/elements/ProgressBar.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/elements/_ProgressBar.scss
@@ -16,10 +16,10 @@ limitations under the License.
 
 .mx_ProgressBar {
     height: 5px;
-    border: 1px solid black;
+    border: 1px solid $progressbar-color;
 }
 
 .mx_ProgressBar_fill {
     height: 100%;
-    background-color: #000;
+    background-color: $progressbar-color;
 }
diff --git a/src/skins/vector/css/matrix-react-sdk/views/elements/RichText.css b/src/skins/vector/css/matrix-react-sdk/views/elements/_RichText.scss
similarity index 84%
rename from src/skins/vector/css/matrix-react-sdk/views/elements/RichText.css
rename to src/skins/vector/css/matrix-react-sdk/views/elements/_RichText.scss
index f0b3c046..72a4c0b7 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/elements/RichText.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/elements/_RichText.scss
@@ -1,14 +1,14 @@
 .mx_UserPill {
     color: white;
-    background-color: #76cfa6;
+    background-color: $accent-color;
     padding: 2px 8px;
     border-radius: 16px;
 }
 
 .mx_RoomPill {
     background-color: white;
-    color: #76cfa6;
-    border: 1px solid #76cfa6;
+    color: $accent-color;
+    border: 1px solid $accent-color;
     padding: 2px 8px;
     border-radius: 16px;
 }
diff --git a/src/skins/vector/css/matrix-react-sdk/views/login/ServerConfig.css b/src/skins/vector/css/matrix-react-sdk/views/login/_ServerConfig.scss
similarity index 96%
rename from src/skins/vector/css/matrix-react-sdk/views/login/ServerConfig.css
rename to src/skins/vector/css/matrix-react-sdk/views/login/_ServerConfig.scss
index 75cd4170..1ad195de 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/login/ServerConfig.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/login/_ServerConfig.scss
@@ -27,5 +27,5 @@ limitations under the License.
     opacity: 0.8;
     font-size: 13px;
     font-weight: 300;
-    color: #4a4a4a;
+    color: $primary-fg-color;
 }
\ No newline at end of file
diff --git a/src/skins/vector/css/matrix-react-sdk/views/messages/MImageBody.css b/src/skins/vector/css/matrix-react-sdk/views/messages/_MImageBody.scss
similarity index 59%
rename from src/skins/vector/css/matrix-react-sdk/views/messages/MImageBody.css
rename to src/skins/vector/css/matrix-react-sdk/views/messages/_MImageBody.scss
index 1134f2f6..83ae0616 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/messages/MImageBody.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/messages/_MImageBody.scss
@@ -22,18 +22,18 @@ limitations under the License.
 .mx_MImageBody_thumbnail {
     max-width: 100%;
 /*
-    background-color: #fff;
-    border: 2px solid #fff;
+    background-color: $primary-bg-color;
+    border: 2px solid $primary-bg-color;
     border-radius: 1px;
 */
 }
 
 .mx_MImageBody_download {
-    color: #76cfa6;
+    color: $accent-color;
 }
 
 .mx_MImageBody_download a {
-    color: #76cfa6;
+    color: $accent-color;
     text-decoration: none;
 }
 
@@ -44,3 +44,17 @@ limitations under the License.
     vertical-align: middle;
     pointer-events: none;
 }
+
+/* Remove the border and padding for iframes for download links. */
+.mx_MImageBody_download iframe {
+    margin: 0px;
+    padding: 0px;
+    border: none;
+    width: 100%;
+    /* Set the height of the iframe to be 1 line of text.
+     * Iframes don't automatically size themselves to fit their content.
+     * So either we have to fix the height of the iframe using CSS or
+     * use javascript's cross-origin postMessage API to communicate how
+     * big the content of the iframe is. */
+    height: 1.5em;
+}
diff --git a/src/skins/vector/css/matrix-react-sdk/views/messages/MNoticeBody.css b/src/skins/vector/css/matrix-react-sdk/views/messages/_MNoticeBody.scss
similarity index 100%
rename from src/skins/vector/css/matrix-react-sdk/views/messages/MNoticeBody.css
rename to src/skins/vector/css/matrix-react-sdk/views/messages/_MNoticeBody.scss
diff --git a/src/skins/vector/css/matrix-react-sdk/views/messages/MTextBody.css b/src/skins/vector/css/matrix-react-sdk/views/messages/_MTextBody.scss
similarity index 100%
rename from src/skins/vector/css/matrix-react-sdk/views/messages/MTextBody.css
rename to src/skins/vector/css/matrix-react-sdk/views/messages/_MTextBody.scss
diff --git a/src/skins/vector/css/matrix-react-sdk/views/messages/TextualEvent.css b/src/skins/vector/css/matrix-react-sdk/views/messages/_TextualEvent.scss
similarity index 100%
rename from src/skins/vector/css/matrix-react-sdk/views/messages/TextualEvent.css
rename to src/skins/vector/css/matrix-react-sdk/views/messages/_TextualEvent.scss
diff --git a/src/skins/vector/css/matrix-react-sdk/views/messages/UnknownBody.css b/src/skins/vector/css/matrix-react-sdk/views/messages/_UnknownBody.scss
similarity index 100%
rename from src/skins/vector/css/matrix-react-sdk/views/messages/UnknownBody.css
rename to src/skins/vector/css/matrix-react-sdk/views/messages/_UnknownBody.scss
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/Autocomplete.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/_Autocomplete.scss
similarity index 83%
rename from src/skins/vector/css/matrix-react-sdk/views/rooms/Autocomplete.css
rename to src/skins/vector/css/matrix-react-sdk/views/rooms/_Autocomplete.scss
index 6d611b5e..062dd0ba 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/Autocomplete.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_Autocomplete.scss
@@ -3,8 +3,8 @@
     bottom: 0;
     z-index: 1000;
     width: 100%;
-    border: 1px solid #e5e5e5;
-    background: white;
+    border: 1px solid $primary-hairline-color;
+    background: $primary-bg-color;
     border-bottom: none;
     border-radius: 4px 4px 0 0;
     max-height: 50vh;
@@ -12,7 +12,7 @@
 }
 
 .mx_Autocomplete_ProviderSection {
-    border-bottom: 1px solid #e5e5e5;
+    border-bottom: 1px solid $primary-hairline-color;
 }
 
 .mx_Autocomplete_Completion_container_pill {
@@ -28,7 +28,7 @@
     user-select: none;
     cursor: pointer;
     align-items: center;
-    color: #4a4a4a;
+    color: $primary-fg-color;
 }
 
 .mx_Autocomplete_Completion_block * {
@@ -42,7 +42,7 @@
     user-select: none;
     cursor: pointer;
     align-items: center;
-    color: #4a4a4a;
+    color: $primary-fg-color;
 }
 
 .mx_Autocomplete_Completion_pill * {
@@ -57,13 +57,13 @@
 }
 
 .mx_Autocomplete_Completion.selected {
-    background: #f6f6f6;
+    background: $menu-bg-color;
     outline: none;
 }
 
 .mx_Autocomplete_provider_name {
     margin: 12px;
-    color: #454545;
+    color: $primary-fg-color;
     font-weight: 400;
     opacity: 0.4;
 }
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/EntityTile.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/_EntityTile.scss
similarity index 96%
rename from src/skins/vector/css/matrix-react-sdk/views/rooms/EntityTile.css
rename to src/skins/vector/css/matrix-react-sdk/views/rooms/_EntityTile.scss
index e52ece77..3f360e79 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/EntityTile.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_EntityTile.scss
@@ -17,7 +17,7 @@ limitations under the License.
 .mx_EntityTile {
     display: table-row;
     position: relative;
-    color: #454545;
+    color: $primary-fg-color;
     cursor: pointer;
 }
 
@@ -77,12 +77,12 @@ limitations under the License.
 
 .mx_EntityTile_ellipsis .mx_EntityTile_name {
     font-style: italic;
-    font-color: #454545;
+    color: $primary-fg-color;
 }
 
 .mx_EntityTile_invitePlaceholder .mx_EntityTile_name {
     font-style: italic;
-    font-color: #454545;
+    color: $primary-fg-color;
 }
 
 .mx_EntityTile_unavailable .mx_EntityTile_avatar,
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/EventTile.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/_EventTile.scss
similarity index 86%
rename from src/skins/vector/css/matrix-react-sdk/views/rooms/EventTile.css
rename to src/skins/vector/css/matrix-react-sdk/views/rooms/_EventTile.scss
index fd3f486b..29170a83 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/EventTile.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_EventTile.scss
@@ -44,7 +44,7 @@ limitations under the License.
 }
 
 .mx_EventTile .mx_SenderProfile {
-    color: #454545;
+    color: $primary-fg-color;
     opacity: 0.5;
     font-size: 14px;
     display: block; /* anti-zalgo, with overflow hidden */
@@ -61,7 +61,7 @@ limitations under the License.
     display: block;
     visibility: hidden;
     white-space: nowrap;
-    color: #acacac;
+    color: $event-timestamp-color;
     font-size: 11px;
     left: 8px;
     position: absolute;
@@ -93,20 +93,20 @@ limitations under the License.
  * TODO: ultimately we probably want some transition on here.
  */
 .mx_EventTile_selected .mx_EventTile_line {
-    border-left: #76cfa6 5px solid;
+    border-left: $accent-color 5px solid;
     padding-left: 60px;
-    background-color: #f7f7f7;
+    background-color: $event-selected-color;
 }
 
 .mx_EventTile:hover .mx_EventTile_line,
 .mx_EventTile.menu .mx_EventTile_line
 {
-    background-color: #f7f7f7;
+    background-color: $event-selected-color;
 }
 
 .mx_EventTile_searchHighlight {
-    background-color: #76cfa6;
-    color: #fff;
+    background-color: $accent-color;
+    color: $accent-fg-color;
     border-radius: 5px;
     padding-left: 2px;
     padding-right: 2px;
@@ -114,26 +114,26 @@ limitations under the License.
 }
 
 .mx_EventTile_searchHighlight a {
-    background-color: #76cfa6;
-    color: #fff;
+    background-color: $accent-color;
+    color: $accent-fg-color;
 }
 
 .mx_EventTile_encrypting {
-    color: #abddbc ! important;
+    color: $event-encrypting-color ! important;
 }
 
 .mx_EventTile_sending {
-    color: #ddd;
+    color: $event-sending-color;
 }
 
 .mx_EventTile_notSent {
-    color: #f44;
+    color: $event-notsent-color;
 }
 
 .mx_EventTile_highlight,
 .mx_EventTile_highlight .markdown-body
  {
-    color: #FF0064;
+    color: $warning-color;
 }
 
 .mx_EventTile_contextual {
@@ -146,7 +146,12 @@ limitations under the License.
     z-index: 1;
     position: relative;
     width: 90px;
-    height: 1px; /* Hack to stop the height of this pushing the messages apart. Replaces marigin-top: -6px. This interacts better with a read marker being in between. Content overflows. */
+
+    /* Hack to stop the height of this pushing the messages apart.
+       Replaces margin-top: -6px. This interacts better with a read
+       marker being in between. Content overflows. */
+    height: 1px;
+
     margin-right: 10px;
 }
 
@@ -172,6 +177,9 @@ limitations under the License.
     cursor: pointer;
     top: 6px;
     right: 6px;
+    width: 19px;
+    height: 19px;
+    background-image: url($edit-button-url);
 }
 
 .mx_EventTile:hover .mx_EventTile_editButton,
@@ -204,7 +212,7 @@ limitations under the License.
 }
 
 .mx_EventTile_readAvatarRemainder {
-    color: #acacac;
+    color: $event-timestamp-color;
     font-size: 11px;
     position: absolute;
 }
@@ -244,10 +252,10 @@ limitations under the License.
 }
 
 .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line {
-    border-left: #76cfa5 5px solid;
+    border-left: $e2e-verified-color 5px solid;
 }
 .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line {
-    border-left: #e8bf37 5px solid;
+    border-left: $e2e-unverified-color 5px solid;
 }
 
 .mx_EventTile:hover.mx_EventTile_verified .mx_MessageTimestamp,
@@ -286,6 +294,10 @@ limitations under the License.
     overflow-y: visible;
 }
 
+.mx_EventTile_content .markdown-body code {
+    background-color: #f8f8f8;
+}
+
 .mx_EventTile_content .markdown-body h1,
 .mx_EventTile_content .markdown-body h2,
 .mx_EventTile_content .markdown-body h3,
@@ -297,7 +309,7 @@ limitations under the License.
 }
 
 .mx_EventTile_content .markdown-body a {
-    color: #76cfa6;
+    color: $accent-color;
 }
 
 .mx_EventTile_content .markdown-body .hljs {
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/LinkPreviewWidget.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/_LinkPreviewWidget.scss
similarity index 89%
rename from src/skins/vector/css/matrix-react-sdk/views/rooms/LinkPreviewWidget.css
rename to src/skins/vector/css/matrix-react-sdk/views/rooms/_LinkPreviewWidget.scss
index 2e2d9f80..33f283e0 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/LinkPreviewWidget.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_LinkPreviewWidget.scss
@@ -18,14 +18,12 @@ limitations under the License.
     margin-top: 15px;
     margin-right: 15px;
     margin-bottom: 15px;
-    display: -webkit-flex;
     display: flex;
-    border-left: 4px solid #ddd;
-    color: #888;
+    border-left: 4px solid $preview-widget-bar-color;
+    color: $preview-widget-fg-color;
 }
 
 .mx_LinkPreviewWidget_image {
-    -webkit-flex: 0 0 100px;
     flex: 0 0 100px;
     margin-left: 15px;
     text-align: center;
@@ -34,7 +32,6 @@ limitations under the License.
 
 .mx_LinkPreviewWidget_caption {
     margin-left: 15px;
-    -webkit-flex: 1 1 auto;
     flex: 1 1 auto;
 }
 
@@ -56,7 +53,6 @@ limitations under the License.
 .mx_LinkPreviewWidget_cancel {
     visibility: hidden;
     cursor: pointer;
-    -webkit-flex: 0 0 40px;
     flex: 0 0 40px;
 }
 
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/MemberDeviceInfo.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/_MemberDeviceInfo.scss
similarity index 68%
rename from src/skins/vector/css/matrix-react-sdk/views/rooms/MemberDeviceInfo.css
rename to src/skins/vector/css/matrix-react-sdk/views/rooms/_MemberDeviceInfo.scss
index 44cba9d1..f1584d4e 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/MemberDeviceInfo.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_MemberDeviceInfo.scss
@@ -20,8 +20,8 @@ limitations under the License.
 
 
 .mx_MemberDeviceInfo_textButton {
-    color: #fff;
-    background-color: #76cfa6;
+    color: $accent-fg-color;
+    background-color: $accent-color;
     border-radius: 17px;
     text-align: center;
     padding-left: 1em;
@@ -44,26 +44,35 @@ limitations under the License.
     border-bottom: 1px solid rgba(0,0,0,0.1);
 }
 
-.mx_MemberDeviceInfo_block,
-.mx_MemberDeviceInfo_unblock {
+.mx_MemberDeviceInfo_blacklist,
+.mx_MemberDeviceInfo_unblacklist {
     float: right;
 }
 
+/* "Unblacklist" is too long for a regular button: make it wider and
+   reduce the padding. */
+.mx_EncryptedEventDialog .mx_MemberDeviceInfo_blacklist,
+.mx_EncryptedEventDialog .mx_MemberDeviceInfo_unblacklist {
+    width: 8em;
+    padding-left: 1em;
+    padding-right: 1em;
+}
+
 .mx_MemberDeviceInfo div.mx_MemberDeviceInfo_verified,
 .mx_MemberDeviceInfo div.mx_MemberDeviceInfo_unverified,
-.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_blocked {
+.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_blacklisted {
     float: right;
     padding-left: 1em;
 }
 
 .mx_MemberDeviceInfo div.mx_MemberDeviceInfo_verified {
-    color: #76cfa5;
+    color: $e2e-verified-color;
 }
 
 .mx_MemberDeviceInfo div.mx_MemberDeviceInfo_unverified {
-    color: #e8bf37;
+    color: $e2e-unverified-color;
 }
 
-.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_blocked {
-    color: #ba6363;
+.mx_MemberDeviceInfo div.mx_MemberDeviceInfo_blacklisted {
+    color: $e2e-warning-color;
 }
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/MemberInfo.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/_MemberInfo.scss
similarity index 94%
rename from src/skins/vector/css/matrix-react-sdk/views/rooms/MemberInfo.css
rename to src/skins/vector/css/matrix-react-sdk/views/rooms/_MemberInfo.scss
index 40b1524c..d6fb5a19 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/MemberInfo.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_MemberInfo.scss
@@ -61,7 +61,7 @@ limitations under the License.
 
 .mx_MemberInfo h3 {
     text-transform: uppercase;
-    color: #3d3b39;
+    color: $h3-color;
     font-weight: 600;
     font-size: 13px;
     margin-top: 16px;
@@ -69,10 +69,9 @@ limitations under the License.
 }
 
 .mx_MemberInfo_profileField {
-    font-color: #999999;
     font-size: 13px;
     position: relative;
-    background-color: #fff;
+    background-color: $primary-bg-color;
 }
 
 .mx_MemberInfo_buttons {
@@ -82,7 +81,7 @@ limitations under the License.
 .mx_MemberInfo_field {
     cursor: pointer;
     font-size: 13px;
-    color: #76cfa6;
+    color: $accent-color;
     margin-left: 8px;
     line-height: 23px;
 }
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/MemberList.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/_MemberList.scss
similarity index 82%
rename from src/skins/vector/css/matrix-react-sdk/views/rooms/MemberList.css
rename to src/skins/vector/css/matrix-react-sdk/views/rooms/_MemberList.scss
index 7d383606..d87bced4 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/MemberList.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_MemberList.scss
@@ -20,22 +20,15 @@ limitations under the License.
     margin-top: 12px;
     margin-right: 20px;
 
-    -webkit-flex: 1;
     flex: 1;
 
-    display: -webkit-box;
-    display: -moz-box;
-    display: -ms-flexbox;
-    display: -webkit-flex;
     display: flex;
 
     flex-direction: column;
-    -webkit-flex-direction: column;
 }
 
 .mx_MemberList .mx_Spinner {
     flex: 0;
-    -webkit-flex: 0;
 }
 
 .mx_MemberList_chevron {
@@ -48,17 +41,16 @@ limitations under the License.
     overflow-y: auto;
 
     order: 1;
-    -webkit-flex: 1 1 0;
     flex: 1 1 0px;
 }
 
 .mx_MemberList_query {
     font-family: 'Open Sans', Arial, Helvetica, Sans-Serif;
     border-radius: 3px;
-    border: 1px solid #f0f0f0;
+    border: 1px solid $input-border-color;
     padding: 9px;
-    color: #454545;
-    background-color: #fff;
+    color: $primary-fg-color;
+    background-color: $primary-bg-color;
     margin-left: 3px;
     font-size: 14px;
     margin-bottom: 8px;
@@ -66,13 +58,13 @@ limitations under the License.
 }
 
 .mx_MemberList_query::-moz-placeholder {
-    color: #454545;
+    color: $primary-fg-color;
     opacity: 0.5;
     font-size: 14px;
 }
 
 .mx_MemberList_query::-webkit-input-placeholder {
-    color: #454545;
+    color: $primary-fg-color;
     opacity: 0.5;
     font-size: 14px;
 }
@@ -80,7 +72,6 @@ limitations under the License.
 .mx_MemberList_joined {
     order: 2;
     flex: 1 0 0;
-    -webkit-flex: 1 0 0;
 
     overflow-y: auto;
 }
@@ -89,14 +80,13 @@ limitations under the License.
 .mx_MemberList_invited {
     order: 3;
     flex: 0 0 100px;
-    -webkit-flex: 0 0 100px;
     overflow-y: auto;
 }
 */
 
 .mx_MemberList_invited h2 {
     text-transform: uppercase;
-    color: #3d3b39;
+    color: $h3-color;
     font-weight: 600;
     font-size: 13px;
     padding-left: 3px;
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/MessageComposer.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/_MessageComposer.scss
similarity index 91%
rename from src/skins/vector/css/matrix-react-sdk/views/rooms/MessageComposer.css
rename to src/skins/vector/css/matrix-react-sdk/views/rooms/_MessageComposer.scss
index 842f8db6..525cc1f6 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/MessageComposer.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_MessageComposer.scss
@@ -18,7 +18,7 @@ limitations under the License.
     max-width: 960px;
     vertical-align: middle;
     margin: auto;
-    border-top: 1px solid #e5e5e5;
+    border-top: 1px solid $primary-hairline-color;
     position: relative;
 }
 
@@ -57,7 +57,7 @@ limitations under the License.
     height: 60px;
     text-align: center;
     font-style: italic;
-    color: #888;
+    color: $greyed-fg-color;
 }
 
 .mx_MessageComposer_input_wrapper {
@@ -90,10 +90,10 @@ limitations under the License.
 }
 
 .mx_MessageComposer_input blockquote {
-    color: rgb(119, 119, 119);
+    color: $blockquote-fg-color;
     margin: 0 0 16px;
     padding: 0 15px;
-    border-left: 4px solid rgb(221, 221, 221);
+    border-left: 4px solid $blockquote-bar-color;
 }
 
 .mx_MessageComposer_input textarea {
@@ -105,11 +105,9 @@ limitations under the License.
     border: 0px;
     resize: none;
     outline: none;
-    -webkit-box-shadow: none;
-    -moz-box-shadow: none;
     box-shadow: none;
-    color: #454545;
-    background-color: #fff;
+    color: $primary-fg-color;
+    background-color: $primary-bg-color;
     font-size: 14px;
     max-height: 120px;
     overflow: auto;
@@ -120,11 +118,11 @@ limitations under the License.
 /* hack for FF as vertical alignment of custom placeholder text is broken */
 .mx_MessageComposer_input textarea::-moz-placeholder {
     line-height: 100%;
-    color: #76cfa6;
+    color: $accent-color;
     opacity: 1.0;
 }
 .mx_MessageComposer_input textarea::-webkit-input-placeholder {
-    color: #76cfa6;
+    color: $accent-color;
 }
 
 .mx_MessageComposer_upload,
@@ -153,7 +151,7 @@ limitations under the License.
 
 .mx_MessageComposer_formatbar_wrapper {
     width: 100%;
-    background-color: #f7f7f7;
+    background-color: $menu-bg-color;
     box-shadow: inset 0 1px 0 0 rgba(0, 0, 0, 0.08);
 }
 
@@ -170,7 +168,7 @@ limitations under the License.
     flex-direction: row;
     align-items: center;
     font-size: 10px;
-    color: #888;
+    color: $greyed-fg-color;
 }
 
 .mx_MessageComposer_formatbar * {
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/PresenceLabel.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/_PresenceLabel.scss
similarity index 100%
rename from src/skins/vector/css/matrix-react-sdk/views/rooms/PresenceLabel.css
rename to src/skins/vector/css/matrix-react-sdk/views/rooms/_PresenceLabel.scss
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomHeader.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomHeader.scss
similarity index 77%
rename from src/skins/vector/css/matrix-react-sdk/views/rooms/RoomHeader.css
rename to src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomHeader.scss
index 056fa879..4affc994 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomHeader.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomHeader.scss
@@ -16,7 +16,6 @@ limitations under the License.
 
 /* add 20px to the height of the header when editing */
 .mx_RoomHeader_editing {
-    -webit-flex: 0 0 93px ! important;
     flex: 0 0 93px ! important;
 }
 
@@ -24,63 +23,36 @@ limitations under the License.
     max-width: 960px;
     margin: auto;
     height: 70px;
-
-    -webkit-align-items: center;
     align-items: center;
-
-    display: -webkit-box;
-    display: -moz-box;
-    display: -ms-flexbox;
-    display: -webkit-flex;
     display: flex;
 }
 
 .mx_RoomHeader_leftRow {
     margin-left: -2px;
-
-    -webkit-box-ordinal-group: 1;
-    -moz-box-ordinal-group: 1;
-    -ms-flex-order: 1;
-    -webkit-order: 1;
     order: 1;
-
-    -webkit-flex: 1;
     flex: 1;
 }
 
 .mx_RoomHeader_spinner {
     height: 36px;
-
-    -webkit-box-ordinal-group: 2;
-    -moz-box-ordinal-group: 2;
-    -ms-flex-order: 2;
-    -webkit-order: 2;
     order: 2;
-
     padding-left: 12px;
     padding-right: 12px;
 }
 
 .mx_RoomHeader_textButton {
     height: 36px;
-    background-color: #76cfa6;
+    background-color: $accent-color;
     border-radius: 36px;
     margin-right: 8px;
-    color: #fff;
+    color: $accent-fg-color;
     line-height: 34px;
     margin-top: -2px;
     text-align: center;
-
-    -webkit-box-ordinal-group: 2;
-    -moz-box-ordinal-group: 2;
-    -ms-flex-order: 2;
-    -webkit-order: 2;
     order: 2;
-
     cursor: pointer;
 
 /*
-    -webkit-flex: 0 0 90px;
     flex: 0 0 90px;
 */
     padding-left: 12px;
@@ -88,27 +60,17 @@ limitations under the License.
 }
 
 .mx_RoomHeader_cancelButton {
-    -webkit-box-ordinal-group: 2;
-    -moz-box-ordinal-group: 2;
-    -ms-flex-order: 2;
-    -webkit-order: 2;
     order: 2;
-
     cursor: pointer;
-
     padding-left: 12px;
     padding-right: 12px;
 }
 
 .mx_RoomHeader_rightRow {
     margin-top: 4px;
-    background-color: #fff;
+    background-color: $primary-bg-color;
     display: flex;
     align-items: center;
-    -webkit-box-ordinal-group: 3;
-    -moz-box-ordinal-group: 3;
-    -ms-flex-order: 3;
-    -webkit-order: 3;
     order: 3;
 }
 
@@ -120,7 +82,7 @@ limitations under the License.
 
 .mx_RoomHeader_simpleHeader {
     line-height: 70px;
-    color: #454545;
+    color: $primary-fg-color;
     font-size: 22px;
     font-weight: bold;
     overflow: hidden;
@@ -138,7 +100,7 @@ limitations under the License.
     width: 100%;
     height: 31px;
     overflow: hidden;
-    color: #454545;
+    color: $primary-fg-color;
     font-weight: bold;
     font-size: 22px;
     padding-left: 19px;
@@ -153,7 +115,7 @@ limitations under the License.
 }
 
 .mx_RoomHeader_settingsHint {
-    color: #a2a2a2 ! important;
+    color: $settings-grey-fg-color ! important;
 }
 
 .mx_RoomHeader_searchStatus {
@@ -174,21 +136,21 @@ limitations under the License.
 }
 
 .mx_RoomHeader_name:hover div:not(.mx_RoomHeader_editable) {
-    color: #76cfa6;
+    color: $accent-color;
 }
 
 .mx_RoomHeader_placeholder {
-    color: #a2a2a2 ! important;
+    color: $settings-grey-fg-color ! important;
 }
 
 .mx_RoomHeader_editable {
-    border-bottom: 1px solid #c7c7c7 ! important;
+    border-bottom: 1px solid $strong-input-border-color ! important;
     min-width: 150px;
     cursor: text;
 }
 
 .mx_RoomHeader_editable:focus {
-    border-bottom: 1px solid #76CFA6 ! important;
+    border-bottom: 1px solid $accent-color ! important;
     outline: none;
     box-shadow: none;
 }
@@ -197,7 +159,7 @@ limitations under the License.
     vertical-align: bottom;
     float: left;
     max-height: 42px;
-    color: #A2A2A2;
+    color: $settings-grey-fg-color;
     font-weight: 300;
     font-size: 13px;
     margin-left: 19px;
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomList.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomList.scss
similarity index 100%
rename from src/skins/vector/css/matrix-react-sdk/views/rooms/RoomList.css
rename to src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomList.scss
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomPreviewBar.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomPreviewBar.scss
similarity index 80%
rename from src/skins/vector/css/matrix-react-sdk/views/rooms/RoomPreviewBar.css
rename to src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomPreviewBar.scss
index d4ba4997..91aa82f3 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomPreviewBar.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomPreviewBar.scss
@@ -18,38 +18,30 @@ limitations under the License.
     text-align: center;
     height: 176px;
 
-    background-color: #f7f7f7;
-
-    -webkit-align-items: center;
     align-items: center;
-
     flex-direction: column;
-    -webkit-flex-direction: column;
-
     justify-content: center;
-    -webkit-justify-content: center;
-
-    display: -webkit-box;
-    display: -moz-box;
-    display: -ms-flexbox;
-    display: -webkit-flex;
     display: flex;
+
+    /* For welcome pages */
+    background-color: #f7f7f7;
+    -webkit-align-items: center;
 }
 
 .mx_RoomPreviewBar_wrapper {
 }
 
 .mx_RoomPreviewBar_invite_text {
-    color: #454545;
+    color: $primary-fg-color;
 }
 
 .mx_RoomPreviewBar_join_text {
-    color: #ff0064;
+    color: $warning-color;
 }
 
 .mx_RoomPreviewBar_preview_text {
     margin-top: 25px;
-    color: #a4a4a4;
+    color: $settings-grey-fg-color;
 }
 
 .mx_RoomPreviewBar_join_text a {
@@ -58,9 +50,7 @@ limitations under the License.
 }
 
 .mx_RoomPreviewBar_warning {
-    display: -webkit-flex;
     display: flex;
-    -webkit-align-items: center;
     align-items: center;
     padding: 8px;
 }
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomSettings.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomSettings.scss
similarity index 92%
rename from src/skins/vector/css/matrix-react-sdk/views/rooms/RoomSettings.css
rename to src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomSettings.scss
index 6a3b1ac8..ef115f6e 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomSettings.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomSettings.scss
@@ -24,10 +24,10 @@ limitations under the License.
 .mx_RoomSettings_integrationsButton_error {
     position: relative;
     height: 36px;
-    background-color: #76cfa6;
+    background-color: $accent-color;
     border-radius: 36px;
     margin-right: 8px;
-    color: #fff;
+    color: $accent-fg-color;
     line-height: 34px;
     text-align: center;
     float: right;
@@ -47,8 +47,8 @@ limitations under the License.
     font-size: 10pt;
     line-height: 1.5em;
     border-radius: 5px;
-    background-color: #000;
-    color: #fff;
+    background-color: $accent-color;
+    color: $accent-fg-color;
 }
 
 .mx_RoomSettings_e2eIcon {
@@ -81,7 +81,7 @@ limitations under the License.
 
 .mx_RoomSettings h3 {
     text-transform: uppercase;
-    color: #3d3b39;
+    color: $h3-color;
     font-weight: 600;
     font-size: 13px;
     margin-top: 36px;
@@ -174,7 +174,7 @@ limitations under the License.
 }
 
 .mx_RoomSettings_warning {
-    color: #ff0064;
+    color: $warning-color;
     font-weight: bold;
     margin-top: 8px;
     margin-bottom: 8px;
@@ -182,13 +182,13 @@ limitations under the License.
 
 .mx_RoomSettings_editable {
     border: 0px;
-    border-bottom: 1px solid #c7c7c7;
+    border-bottom: 1px solid $strong-input-border-color;
     padding: 0px;
     min-width: 240px;
 }
 
 .mx_RoomSettings_editable:focus {
-    border-bottom: 1px solid #76CFA6;
+    border-bottom: 1px solid $accent-color;
     outline: none;
     box-shadow: none;
 }
@@ -205,7 +205,7 @@ limitations under the License.
 }
 
 .mx_RoomSettings_aliasPlaceholder {
-    color: #a2a2a2;
+    color: $settings-grey-fg-color;
 }
 
 .mx_RoomSettings_buttons {
@@ -220,8 +220,8 @@ limitations under the License.
     border-radius: 36px;
     font-weight: 400;
     font-size: 15px;
-    color: #fff;
-    background-color: #76cfa6;
+    color: $accent-fg-color;
+    background-color: $accent-color;
     width: auto;
     margin: auto;
     padding: 6px;
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomTile.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomTile.scss
similarity index 91%
rename from src/skins/vector/css/matrix-react-sdk/views/rooms/RoomTile.css
rename to src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomTile.scss
index 2822d82e..5ca4ac17 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/RoomTile.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_RoomTile.scss
@@ -65,7 +65,7 @@ limitations under the License.
     position: absolute;
     content: "";
     border-radius: 40px;
-    background-image: url("img/icons_ellipsis.svg");
+    background-image: url("../../img/icons_ellipsis.svg");
     background-size: 25px;
     width: 24px;
     height: 24px;
@@ -78,7 +78,7 @@ limitations under the License.
     position: absolute;
     content: "";
     border-radius: 40px;
-    background: #4A4A4A;
+    background: $primary-fg-color;
     bottom: 0;
     width: 24px;
     height: 24px;
@@ -103,7 +103,7 @@ limitations under the License.
     padding-right: 6px;
     padding-top: 2px;
     padding-bottom: 3px;
-    color: rgba(69, 69, 69, 0.8);
+    color: $roomtile-name-color;
     white-space: nowrap;
     overflow: hidden;
     text-overflow: ellipsis;
@@ -142,7 +142,7 @@ limitations under the License.
     width: 0;
     height: 0;
     margin-left: 5px;
-    border-top: 5px solid #ff0064;
+    border-top: 5px solid $warning-color;
     border-right: 7px solid transparent;
 }
 
@@ -154,7 +154,7 @@ limitations under the License.
     right: 8px; /*gutter */
     top: 9px;
     border-radius: 8px;
-    color: #fff;
+    color: $accent-fg-color;
     font-weight: 600;
     font-size: 10px;
     text-align: center;
@@ -171,15 +171,15 @@ limitations under the License.
 
 .mx_RoomTile.mx_RoomTile_noBadges .mx_RoomTile_badge.mx_RoomTile_badgeButton,
 .mx_RoomTile.mx_RoomTile_notificationStateMenu.mx_RoomTile_noBadges .mx_RoomTile_badge {
-    background-color: rgb(214, 214, 214);
+    background-color: $neutral-badge-color;
 }
 
 .mx_RoomTile_unreadNotify .mx_RoomTile_badge {
-    background-color: #76cfa6;
+    background-color: $accent-color;
 }
 
 .mx_RoomTile_highlight .mx_RoomTile_badge {
-    background-color: #ff0064;
+    background-color: $warning-color;
 }
 
 .mx_RoomTile_unread, .mx_RoomTile_highlight {
@@ -187,7 +187,7 @@ limitations under the License.
 }
 
 .mx_RoomTile_selected {
-    background-color: rgba(255, 255, 255, 0.8);
+    background-color: $roomtile-selected-bg-color;
 }
 
 .mx_RoomTile .mx_RoomTile_name.mx_RoomTile_badgeShown {
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/SearchableEntityList.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/_SearchableEntityList.scss
similarity index 84%
rename from src/skins/vector/css/matrix-react-sdk/views/rooms/SearchableEntityList.css
rename to src/skins/vector/css/matrix-react-sdk/views/rooms/_SearchableEntityList.scss
index 76d9e216..6116dd92 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/SearchableEntityList.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_SearchableEntityList.scss
@@ -16,19 +16,17 @@ limitations under the License.
 
 .mx_SearchableEntityList {
     display: flex;
-    display: -webkit-flex;
 
     flex-direction: column;
-    -webkit-flex-direction: column;
 }
 
 .mx_SearchableEntityList_query {
     font-family: 'Open Sans', Arial, Helvetica, Sans-Serif;
     border-radius: 3px;
-    border: 1px solid #f0f0f0;
+    border: 1px solid $input-border-color;
     padding: 9px;
-    color: #454545;
-    background-color: #fff;
+    color: $primary-fg-color;
+    background-color: $primary-bg-color;
     margin-left: 3px;
     font-size: 15px;
     margin-bottom: 8px;
@@ -36,20 +34,19 @@ limitations under the License.
 }
 
 .mx_SearchableEntityList_query::-moz-placeholder {
-    color: #454545;
+    color: $primary-fg-color;
     opacity: 0.5;
     font-size: 12px;
 }
 
 .mx_SearchableEntityList_query::-webkit-input-placeholder {
-    color: #454545;
+    color: $primary-fg-color;
     opacity: 0.5;
     font-size: 12px;
 }
 
 .mx_SearchableEntityList_listWrapper {
     flex: 1;
-    -webkit-flex: 1;
 
     overflow-y: auto;
 }
@@ -67,14 +64,13 @@ limitations under the License.
 .mx_SearchableEntityList_hrWrapper {
     width: 100%;
     flex: 0 0 auto;
-    -webkit-flex: 0 0 auto;
 }
 
 .mx_SearchableEntityList hr {
     height: 1px;
     border: 0px;
-    color: #e1dddd;
-    background-color: #e1dddd;
+    color: $primary-fg-color;
+    background-color: $primary-fg-color;
     margin-right: 15px;
     margin-top: 11px;
     margin-bottom: 11px;
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/TabCompleteBar.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/_TabCompleteBar.scss
similarity index 92%
rename from src/skins/vector/css/matrix-react-sdk/views/rooms/TabCompleteBar.css
rename to src/skins/vector/css/matrix-react-sdk/views/rooms/_TabCompleteBar.scss
index 1f8a3450..5dcbd21a 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/TabCompleteBar.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_TabCompleteBar.scss
@@ -27,7 +27,7 @@ limitations under the License.
 
 .mx_TabCompleteBar_command {
     margin-right: 8px;
-    background-color: #76CFA6;
+    background-color: $accent-color;
     padding-left: 8px;
     padding-right: 8px;
     padding-top: 2px;
@@ -41,7 +41,7 @@ limitations under the License.
 .mx_TabCompleteBar_command .mx_TabCompleteBar_text {
     opacity: 1.0;
     vertical-align: initial;
-    color: #fff;
+    color: $accent-fg-color;
 }
 
 .mx_TabCompleteBar_item img {
@@ -50,7 +50,7 @@ limitations under the License.
 }
 
 .mx_TabCompleteBar_text {
-    color: #4a4a4a;
+    color: $primary-fg-color;
     vertical-align: middle;
     opacity: 0.5;
 }
diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/TopUnreadMessagesBar.css b/src/skins/vector/css/matrix-react-sdk/views/rooms/_TopUnreadMessagesBar.scss
similarity index 95%
rename from src/skins/vector/css/matrix-react-sdk/views/rooms/TopUnreadMessagesBar.css
rename to src/skins/vector/css/matrix-react-sdk/views/rooms/_TopUnreadMessagesBar.scss
index 77184d42..7bbafcbe 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/rooms/TopUnreadMessagesBar.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_TopUnreadMessagesBar.scss
@@ -19,7 +19,7 @@ limitations under the License.
     max-width: 960px;
     padding-top: 5px;
     padding-bottom: 5px;
-    border-bottom: 1px solid #e5e5e5;
+    border-bottom: 1px solid $primary-hairline-color;
 }
 
 .mx_TopUnreadMessagesBar_scrollUp {
diff --git a/src/skins/vector/css/matrix-react-sdk/views/settings/DevicesPanel.css b/src/skins/vector/css/matrix-react-sdk/views/settings/_DevicesPanel.scss
similarity index 100%
rename from src/skins/vector/css/matrix-react-sdk/views/settings/DevicesPanel.css
rename to src/skins/vector/css/matrix-react-sdk/views/settings/_DevicesPanel.scss
diff --git a/src/skins/vector/css/matrix-react-sdk/views/settings/IntegrationsManager.css b/src/skins/vector/css/matrix-react-sdk/views/settings/_IntegrationsManager.scss
similarity index 95%
rename from src/skins/vector/css/matrix-react-sdk/views/settings/IntegrationsManager.css
rename to src/skins/vector/css/matrix-react-sdk/views/settings/_IntegrationsManager.scss
index 93ee0e20..13fc9b22 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/settings/IntegrationsManager.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/settings/_IntegrationsManager.scss
@@ -24,7 +24,7 @@ limitations under the License.
 }
 
 .mx_IntegrationsManager iframe {
-    background-color: #fff;
+    background-color: $primary-bg-color;
     border: 0px;
     width: 100%;
     height: 100%;
diff --git a/src/skins/vector/css/matrix-react-sdk/views/voip/CallView.css b/src/skins/vector/css/matrix-react-sdk/views/voip/_CallView.scss
similarity index 91%
rename from src/skins/vector/css/matrix-react-sdk/views/voip/CallView.css
rename to src/skins/vector/css/matrix-react-sdk/views/voip/_CallView.scss
index 8051b4d0..deb89a83 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/voip/CallView.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/voip/_CallView.scss
@@ -15,8 +15,8 @@ limitations under the License.
 */
 
 .mx_CallView_voice {
-    background-color: #76cfa6;
-    color: #fff;
+    background-color: $accent-color;
+    color: $accent-fg-color;
     cursor: pointer;
     text-align: center;
     padding: 6px;
diff --git a/src/skins/vector/css/matrix-react-sdk/views/voip/IncomingCallbox.css b/src/skins/vector/css/matrix-react-sdk/views/voip/_IncomingCallbox.scss
similarity index 90%
rename from src/skins/vector/css/matrix-react-sdk/views/voip/IncomingCallbox.css
rename to src/skins/vector/css/matrix-react-sdk/views/voip/_IncomingCallbox.scss
index 5cf62091..64eac25d 100644
--- a/src/skins/vector/css/matrix-react-sdk/views/voip/IncomingCallbox.css
+++ b/src/skins/vector/css/matrix-react-sdk/views/voip/_IncomingCallbox.scss
@@ -18,7 +18,7 @@ limitations under the License.
     text-align: center;
     border: 1px solid #a4a4a4;
     border-radius: 8px;
-    background-color: #fff;
+    background-color: $primary-bg-color;
     position: fixed;
     z-index: 1000;
     padding: 6px;
@@ -41,14 +41,12 @@ limitations under the License.
 
 .mx_IncomingCallBox_buttons {
     display: flex;
-    display: -webkit-flex;
 }
 
 .mx_IncomingCallBox_buttons_cell {
     vertical-align: middle;
     padding: 6px;
     flex: 1;
-    -webkit-flex: 1;
 }
 
 .mx_IncomingCallBox_buttons_decline,
@@ -58,14 +56,14 @@ limitations under the License.
     height: 36px;
     line-height: 36px;
     border-radius: 36px;
-    color: #fff;
+    color: $accent-fg-color;
     margin: auto;
 }
 
 .mx_IncomingCallBox_buttons_decline {
-    background-color: #f48080;
+    background-color: $voip-decline-color;
 }
 
 .mx_IncomingCallBox_buttons_accept {
-    background-color: #80f480;
+    background-color: $voip-accept-color;
 }
diff --git a/src/skins/vector/css/matrix-react-sdk/views/voip/VideoView.css b/src/skins/vector/css/matrix-react-sdk/views/voip/_VideoView.scss
similarity index 100%
rename from src/skins/vector/css/matrix-react-sdk/views/voip/VideoView.css
rename to src/skins/vector/css/matrix-react-sdk/views/voip/_VideoView.scss
diff --git a/src/skins/vector/css/rethemendex.sh b/src/skins/vector/css/rethemendex.sh
new file mode 100755
index 00000000..a7d9a657
--- /dev/null
+++ b/src/skins/vector/css/rethemendex.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+cd `dirname $0`
+
+{
+    echo "// autogenerated by rethemendex.sh"
+
+    find . \! \( -path ./themes -prune \) -iname _\*.scss |
+        fgrep -v _components.scss | LC_ALL=C sort |
+        while read i; do
+            echo "@import \"$i\";"
+        done
+} > _components.scss
diff --git a/src/skins/vector/css/themes/_base.scss b/src/skins/vector/css/themes/_base.scss
new file mode 100644
index 00000000..db921034
--- /dev/null
+++ b/src/skins/vector/css/themes/_base.scss
@@ -0,0 +1,106 @@
+
+// typical text (dark-on-white in light skin)
+$primary-fg-color: #454545;
+$primary-bg-color: #ffffff;
+
+// used for dialog box text
+$light-fg-color: #747474;
+
+// used for focusing form controls
+$focus-bg-color: #dddddd;
+
+// button UI (white-on-green in light skin)
+$accent-fg-color: #ffffff;
+$accent-color: #76CFA6;
+
+$selection-fg-color: $primary-bg-color;
+
+// red warning colour
+$warning-color: #ff0064;
+
+// left-panel style muted accent color
+$secondary-accent-color: #eaf5f0;
+
+// used by RoomDirectory permissions
+$plinth-bg-color: $secondary-accent-color;
+
+// used by RoomDropTarget
+$droptarget-bg-color: rgba(255,255,255,0.5);
+
+// used by AddressSelector
+$selected-color: #eaf5f0;
+
+// selected for hoverover & selected event tiles
+$event-selected-color: #f7f7f7;
+
+// used for the hairline dividers in RoomView
+$primary-hairline-color: #e5e5e5;
+
+// used for the border of input text fields
+$input-border-color: #f0f0f0;
+
+// apart from login forms, which have stronger border
+$strong-input-border-color: #c7c7c7;
+
+// used for UserSettings EditableText
+$input-underline-color: rgba(151, 151, 151, 0.5);
+$input-fg-color: rgba(74, 74, 74, 0.9);
+
+// context menus
+$menu-border-color: rgba(187, 187, 187, 0.5);
+$menu-bg-color: #f6f6f6;
+
+$avatar-initial-color: #ffffff;
+
+$h3-color: #3d3b39;
+
+$dialog-background-bg-color: #e9e9e9;
+$lightbox-background-bg-color: #000;
+
+$greyed-fg-color: #888;
+
+$neutral-badge-color: #dbdbdb;
+
+$preview-widget-bar-color: #ddd;
+$preview-widget-fg-color: $greyed-fg-color;
+
+$blockquote-bar-color: #ddd;
+$blockquote-fg-color: #777;
+
+$settings-grey-fg-color: #a2a2a2;
+
+$voip-decline-color: #f48080;
+$voip-accept-color: #80f480;
+
+// ********************
+
+$roomtile-name-color: rgba(69, 69, 69, 0.8);
+$roomtile-selected-bg-color: rgba(255, 255, 255, 0.8);
+
+$roomsublist-label-fg-color: $h3-color;
+$roomsublist-label-bg-color: #d3efe1;
+
+// ********************
+
+// event tile lifecycle
+$event-encrypting-color: #abddbc;
+$event-sending-color: #ddd;
+$event-notsent-color: #f44;
+
+// event timestamp
+$event-timestamp-color: #acacac;
+
+$edit-button-url: "../../img/icon_context_message.svg";
+
+// e2e
+$e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color
+$e2e-unverified-color: #e8bf37;
+$e2e-warning-color: #ba6363;
+
+/*** ImageView ***/
+$lightbox-bg-color: #454545;
+$lightbox-fg-color: #ffffff;
+$lightbox-border-color: #ffffff;
+
+// unused?
+$progressbar-color: #000;
\ No newline at end of file
diff --git a/src/skins/vector/css/themes/_dark.scss b/src/skins/vector/css/themes/_dark.scss
new file mode 100644
index 00000000..cf6d6eff
--- /dev/null
+++ b/src/skins/vector/css/themes/_dark.scss
@@ -0,0 +1,126 @@
+
+// typical text (dark-on-white in light skin)
+$primary-fg-color: #dddddd;
+$primary-bg-color: #2d2d2d;
+
+// used for focusing form controls
+$focus-bg-color: #101010;
+
+// used for dialog box text
+$light-fg-color: #747474;
+
+// button UI (white-on-green in light skin)
+$accent-fg-color: $primary-bg-color;
+$accent-color: #76CFA6;
+
+$selection-fg-color: $primary-bg-color;
+
+// red warning colour
+$warning-color: #ff0064;
+
+// left-panel style muted accent color
+$secondary-accent-color: $primary-bg-color;
+
+// used by RoomDirectory permissions
+$plinth-bg-color: #474747;
+
+// used by RoomDropTarget
+$droptarget-bg-color: rgba(45,45,45,0.5);
+
+// used by AddressSelector
+$selected-color: #eaf5f0;
+
+// selected for hoverover & selected event tiles
+$event-selected-color: #353535;
+
+// used for the hairline dividers in RoomView
+$primary-hairline-color: #474747;
+
+// used for the border of input text fields
+$input-border-color: #3a3a3a;
+
+// apart from login forms, which have stronger border
+$strong-input-border-color: #656565;
+
+// used for UserSettings EditableText
+$input-underline-color: $primary-fg-color;
+$input-fg-color: $primary-fg-color;
+
+// context menus
+$menu-border-color: rgba(187, 187, 187, 0.5);
+$menu-bg-color: #373737;
+
+$avatar-initial-color: #2d2d2d;
+
+$h3-color: $primary-fg-color;
+
+$dialog-background-bg-color: #000;
+$lightbox-background-bg-color: #000;
+
+$greyed-fg-color: #888;
+
+$neutral-badge-color: #888;
+
+$preview-widget-bar-color: $menu-bg-color;
+$preview-widget-fg-color: $greyed-fg-color;
+
+$blockquote-bar-color: #ddd;
+$blockquote-fg-color: #777;
+
+$settings-grey-fg-color: #a2a2a2;
+
+$voip-decline-color: #f48080;
+$voip-accept-color: #80f480;
+
+// ********************
+
+$roomtile-name-color: rgba(186, 186, 186, 0.8);
+$roomtile-selected-bg-color: rgba(255, 255, 255, 0.05);
+
+$roomsublist-label-fg-color: $h3-color;
+$roomsublist-label-bg-color: #454545;
+
+// ********************
+
+// event tile lifecycle
+$event-encrypting-color: rgba(171, 221, 188, 0.4);
+$event-sending-color: #ddd;
+$event-notsent-color: #f44;
+
+// event timestamp
+$event-timestamp-color: #acacac;
+
+$edit-button-url: "../../img/icon_context_message_dark.svg";
+
+// e2e
+$e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color
+$e2e-unverified-color: #e8bf37;
+$e2e-warning-color: #ba6363;
+
+/*** ImageView ***/
+$lightbox-bg-color: #454545;
+$lightbox-fg-color: #ffffff;
+$lightbox-border-color: #ffffff;
+
+// unused?
+$progressbar-color: #000;
+
+// Nasty hacks to apply a filter to arbitrary monochrome artwork to make it
+// better match the theme.  Typically applied to dark grey 'off' buttons or
+// light grey 'on' buttons.
+.mx_filterFlipColor {
+    filter: invert();
+}
+
+.gm-scrollbar .thumb {
+    filter: invert();
+}
+
+.mx_EventTile_content .markdown-body pre,
+.mx_EventTile_content .markdown-body code {
+    filter: invert();
+}
+
+.mx_EventTile_content .markdown-body pre code {
+    filter: none;
+}
\ No newline at end of file
diff --git a/src/skins/vector/css/themes/dark.scss b/src/skins/vector/css/themes/dark.scss
new file mode 100644
index 00000000..5a37d036
--- /dev/null
+++ b/src/skins/vector/css/themes/dark.scss
@@ -0,0 +1,3 @@
+@import "_base.scss";
+@import "_dark.scss";
+@import "../_components.scss";
diff --git a/src/skins/vector/css/themes/light.scss b/src/skins/vector/css/themes/light.scss
new file mode 100644
index 00000000..ea0f93d5
--- /dev/null
+++ b/src/skins/vector/css/themes/light.scss
@@ -0,0 +1,2 @@
+@import "_base.scss";
+@import "../_components.scss";
\ No newline at end of file
diff --git a/src/skins/vector/css/vector-web/fonts.css b/src/skins/vector/css/vector-web/_fonts.scss
similarity index 57%
rename from src/skins/vector/css/vector-web/fonts.css
rename to src/skins/vector/css/vector-web/_fonts.scss
index 719eeebc..52ac95b5 100644
--- a/src/skins/vector/css/vector-web/fonts.css
+++ b/src/skins/vector/css/vector-web/_fonts.scss
@@ -3,44 +3,46 @@
  * Includes extended Latin, Greek, Cyrillic and Vietnamese character sets
  */
 
+/* the 'src' links are relative to the bundle.css, which is in a subdirectory.
+ */
 @font-face {
     font-family: 'Open Sans';
-    src: url('fonts/Open_Sans/OpenSans-Regular.ttf') format('truetype');
+    src: url('../../fonts/Open_Sans/OpenSans-Regular.ttf') format('truetype');
     font-weight: 400;
     font-style: normal;
 }
 
 @font-face {
     font-family: 'Open Sans';
-    src: url('fonts/Open_Sans/OpenSans-Italic.ttf') format('truetype');
+    src: url('../../fonts/Open_Sans/OpenSans-Italic.ttf') format('truetype');
     font-weight: 400;
     font-style: italic;
 }
 
 @font-face {
     font-family: 'Open Sans';
-    src: url('fonts/Open_Sans/OpenSans-Semibold.ttf') format('truetype');
+    src: url('../../fonts/Open_Sans/OpenSans-Semibold.ttf') format('truetype');
     font-weight: 600;
     font-style: normal;
 }
 
 @font-face {
     font-family: 'Open Sans';
-    src: url('fonts/Open_Sans/OpenSans-SemiboldItalic.ttf') format('truetype');
+    src: url('../../fonts/Open_Sans/OpenSans-SemiboldItalic.ttf') format('truetype');
     font-weight: 600;
     font-style: italic;
 }
 
 @font-face {
     font-family: 'Open Sans';
-    src: url('fonts/Open_Sans/OpenSans-Bold.ttf') format('truetype');
+    src: url('../../fonts/Open_Sans/OpenSans-Bold.ttf') format('truetype');
     font-weight: 700;
     font-style: normal;
 }
 
 @font-face {
     font-family: 'Open Sans';
-    src: url('fonts/Open_Sans/OpenSans-BoldItalic.ttf') format('truetype');
+    src: url('../../fonts/Open_Sans/OpenSans-BoldItalic.ttf') format('truetype');
     font-weight: 700;
     font-style: italic;
 }
@@ -52,14 +54,14 @@
 
 @font-face {
     font-family: 'Fira Mono';
-    src: url('fonts/Fira_Mono/FiraMono-Regular.ttf') format('truetype');
+    src: url('../../fonts/Fira_Mono/FiraMono-Regular.ttf') format('truetype');
     font-weight: 400;
     font-style: normal;
 }
 
 @font-face {
     font-family: 'Fira Mono';
-    src: url('fonts/Fira_Mono/FiraMono-Bold.ttf') format('truetype');
+    src: url('../../fonts/Fira_Mono/FiraMono-Bold.ttf') format('truetype');
     font-weight: 700;
     font-style: normal;
 }
diff --git a/src/skins/vector/css/vector-web/structures/CompatibilityPage.css b/src/skins/vector/css/vector-web/structures/_CompatibilityPage.scss
similarity index 100%
rename from src/skins/vector/css/vector-web/structures/CompatibilityPage.css
rename to src/skins/vector/css/vector-web/structures/_CompatibilityPage.scss
diff --git a/src/skins/vector/css/vector-web/structures/LeftPanel.css b/src/skins/vector/css/vector-web/structures/_LeftPanel.scss
similarity index 85%
rename from src/skins/vector/css/vector-web/structures/LeftPanel.css
rename to src/skins/vector/css/vector-web/structures/_LeftPanel.scss
index 35d3e22b..d3bbce1b 100644
--- a/src/skins/vector/css/vector-web/structures/LeftPanel.css
+++ b/src/skins/vector/css/vector-web/structures/_LeftPanel.scss
@@ -17,13 +17,8 @@ limitations under the License.
 .mx_LeftPanel {
     position: relative;
 
-    display: -webkit-box;
-    display: -moz-box;
-    display: -ms-flexbox;
-    display: -webkit-flex;
     display: flex;
     flex-direction: column;
-    -webkit-flex-direction: column;
 }
 
 .mx_LeftPanel_hideButton {
@@ -39,13 +34,8 @@ limitations under the License.
 }
 
 .mx_LeftPanel .mx_RoomList_scrollbar {
-    -webkit-box-ordinal-group: 1;
-    -moz-box-ordinal-group: 1;
-    -ms-flex-order: 1;
-    -webkit-order: 1;
     order: 1;
 
-    -webkit-flex: 1 1 0;
     flex: 1 1 0;
 
     overflow-y: auto;
@@ -57,16 +47,11 @@ limitations under the License.
 }
 
 .mx_LeftPanel .mx_BottomLeftMenu {
-    -webkit-box-ordinal-group: 3;
-    -moz-box-ordinal-group: 3;
-    -ms-flex-order: 3;
-    -webkit-order: 3;
     order: 3;
 
     border-top: 1px solid rgba(118, 207, 166, 0.2);
     margin-left: 16px; /* gutter */
     margin-right: 16px; /* gutter */
-    -webkit-flex: 0 0 60px;
     flex: 0 0 60px;
     z-index: 1;
 }
diff --git a/src/skins/vector/css/vector-web/structures/RightPanel.css b/src/skins/vector/css/vector-web/structures/_RightPanel.scss
similarity index 74%
rename from src/skins/vector/css/vector-web/structures/RightPanel.css
rename to src/skins/vector/css/vector-web/structures/_RightPanel.scss
index 5da4c4ab..91034e63 100644
--- a/src/skins/vector/css/vector-web/structures/RightPanel.css
+++ b/src/skins/vector/css/vector-web/structures/_RightPanel.scss
@@ -17,26 +17,16 @@ limitations under the License.
 .mx_RightPanel {
     position: relative;
 
-    display: -webkit-box;
-    display: -moz-box;
-    display: -ms-flexbox;
-    display: -webkit-flex;
     display: flex;
     flex-direction: column;
-    -webkit-flex-direction: column;
 }
 
 .mx_RightPanel_header {
-    -webkit-box-ordinal-group: 1;
-    -moz-box-ordinal-group: 1;
-    -ms-flex-order: 1;
-    -webkit-order: 1;
     order: 1;
 
-    border-bottom: 1px solid #e5e5e5;
+    border-bottom: 1px solid $primary-hairline-color;
     margin-right: 20px;
 
-    -webkit-flex: 0 0 70px;
     flex: 0 0 70px;
 }
 
@@ -45,7 +35,7 @@ limitations under the License.
 .mx_RightPanel_headerButtonGroup {
     margin-top: 6px;
     float: left;
-    background-color: #fff;
+    background-color: $primary-bg-color;
     margin-left: 0px;
 }
 
@@ -68,12 +58,13 @@ limitations under the License.
     width: 25px;
     height: 5px;
     border-radius: 5px;
-    background-color: rgba(118, 207, 166, 0.2);
+    background-color: $accent-color;
+    opacity: 0.2;
 }
 
 .mx_RightPanel_headerButton_badge {
     font-size: 11px;
-    color: #76cfa6;
+    color: $accent-color;
     font-weight: bold;
     padding-bottom: 2px;
 }
@@ -81,33 +72,23 @@ limitations under the License.
 .mx_RightPanel .mx_MemberList,
 .mx_RightPanel .mx_MemberInfo,
 .mx_RightPanel_blank {
-    -webkit-box-ordinal-group: 2;
-    -moz-box-ordinal-group: 2;
-    -ms-flex-order: 2;
-    -webkit-order: 2;
     order: 2;
     flex: 1 1 0;
-    -webkit-flex: 1 1 0;
 }
 
 .mx_RightPanel_footer {
-    -webkit-box-ordinal-group: 3;
-    -moz-box-ordinal-group: 3;
-    -ms-flex-order: 3;
-    -webkit-order: 3;
     order: 3;
 
-    border-top: 1px solid #e5e5e5;
+    border-top: 1px solid $primary-hairline-color;
     margin-right: 20px;
 
-    -webkit-flex: 0 0 60px;
     flex: 0 0 60px;
 }
 
 .mx_RightPanel_footer .mx_RightPanel_invite {
     line-height: 35px;
     font-size: 14px;
-	color: #4A4A4A;
+	color: $primary-fg-color;
     padding-top: 13px;
     padding-left: 5px;
     cursor: pointer;
diff --git a/src/skins/vector/css/vector-web/structures/RoomDirectory.css b/src/skins/vector/css/vector-web/structures/_RoomDirectory.scss
similarity index 82%
rename from src/skins/vector/css/vector-web/structures/RoomDirectory.css
rename to src/skins/vector/css/vector-web/structures/_RoomDirectory.scss
index 563b1772..6e508ec7 100644
--- a/src/skins/vector/css/vector-web/structures/RoomDirectory.css
+++ b/src/skins/vector/css/vector-web/structures/_RoomDirectory.scss
@@ -20,35 +20,23 @@ limitations under the License.
     margin-left: auto;
     margin-right: auto;
     margin-bottom: 12px;
-    color: #4a4a4a;
+    color: $primary-fg-color;
 
-    display: -webkit-box;
-    display: -moz-box;
-    display: -ms-flexbox;
-    display: -webkit-flex;
     display: flex;
 
     flex-direction: column;
-    -webkit-flex-direction: column;
 }
 
 .mx_RoomDirectory_list {
-    -webkit-flex: 1;
     flex: 1;
 
-    display: -webkit-box;
-    display: -moz-box;
-    display: -ms-flexbox;
-    display: -webkit-flex;
     display: flex;
 
     flex-direction: column;
-    -webkit-flex-direction: column;
 }
 
 .mx_RoomDirectory_list .mx_RoomView_messageListWrapper {
     justify-content: flex-start;
-    -webkit-justify-content: flex-start;
 }
 
 .mx_RoomDirectory_listheader {
@@ -72,13 +60,12 @@ limitations under the License.
 
 .mx_RoomDirectory_tableWrapper {
     overflow-y: auto;
-    -webkit-flex: 1 1 0;
     flex: 1 1 0;
 }
 
 .mx_RoomDirectory_table {
     font-size: 14px;
-    color: #4a4a4a;
+    color: $primary-fg-color;
     width: 100%;
     text-align: left;
     table-layout: fixed;
@@ -110,11 +97,11 @@ limitations under the License.
     padding-right: 5px;
     height: 15px;
     border-radius: 11px;
-    background-color: #eaf5f0;
+    background-color: $plinth-bg-color;
     text-transform: uppercase;
     font-weight: 600;
     font-size: 11px;
-    color: #61c295;
+    color: $accent-color;
 }
 
 .mx_RoomDirectory_topic {
@@ -123,7 +110,7 @@ limitations under the License.
 
 .mx_RoomDirectory_alias {
     font-size: 12px;
-    color: #b3b3b3;
+    color: $settings-grey-fg-color;
 }
 
 .mx_RoomDirectory_roomMemberCount {
diff --git a/src/skins/vector/css/vector-web/structures/RoomSubList.css b/src/skins/vector/css/vector-web/structures/_RoomSubList.scss
similarity index 87%
rename from src/skins/vector/css/vector-web/structures/RoomSubList.css
rename to src/skins/vector/css/vector-web/structures/_RoomSubList.scss
index e6e81aef..0d56d6c3 100644
--- a/src/skins/vector/css/vector-web/structures/RoomSubList.css
+++ b/src/skins/vector/css/vector-web/structures/_RoomSubList.scss
@@ -29,7 +29,7 @@ limitations under the License.
 .mx_RoomSubList_label {
     position: relative;
     text-transform: uppercase;
-    color: #3d3b39;
+    color: $roomsublist-label-fg-color;
     font-weight: 600;
     font-size: 12px;
     width: 203px; /* padding + width = LHS Panel width */
@@ -39,8 +39,8 @@ limitations under the License.
     padding-top: 6px;
     padding-bottom: 6px;
     cursor: pointer;
-    background-color: #d3efe1;
-    border-top: solid 2px #eaf5f0;
+    background-color: $roomsublist-label-bg-color;
+    border-top: solid 2px $secondary-accent-color;
 }
 
 .mx_RoomSubList_label.mx_RoomSubList_fixed {
@@ -63,7 +63,7 @@ limitations under the License.
     display: inline-block;
     font-size: 12px;
     font-weight: normal;
-    color: #76cfa6;
+    color: $accent-color;
     padding-left: 5px;
     text-transform: none;
 }
@@ -80,14 +80,14 @@ limitations under the License.
     right: 8px; /*gutter */
     top: 7px;
     border-radius: 8px;
-    color: #fff;
+    color: $accent-fg-color;
     font-weight: 600;
     font-size: 10px;
     text-align: center;
     padding-top: 1px;
     padding-left: 4px;
     padding-right: 4px;
-    background-color: #76cfa6;
+    background-color: $accent-color;
 }
 
 /*
@@ -97,7 +97,7 @@ limitations under the License.
 */
 
 .mx_RoomSubList_badgeHighlight {
-    background-color: #ff0064;
+    background-color: $warning-color;
 }
 
 /* This is the bottom of the speech bubble */
@@ -108,7 +108,7 @@ limitations under the License.
     width: 0;
     height: 0;
     margin-left: 5px;
-    border-top: 5px solid #ff0064;
+    border-top: 5px solid $warning-color;
     border-right: 7px solid transparent;
 }
 
@@ -129,7 +129,7 @@ limitations under the License.
     height: 0;
     border-left: 5px solid transparent;
     border-right: 5px solid transparent;
-    border-top: 6px solid #76cfa6;
+    border-top: 6px solid $accent-color;
 }
 
 .mx_RoomSubList_chevronUp {
@@ -137,14 +137,14 @@ limitations under the License.
     height: 0;
     border-left: 5px solid transparent;
     border-right: 5px solid transparent;
-    border-bottom: 6px solid #76cfa6;
+    border-bottom: 6px solid $accent-color;
 }
 
 .mx_RoomSubList_chevronRight {
     width: 0;
     height: 0;
     border-top: 5px solid transparent;
-    border-left: 6px solid #76cfa6;
+    border-left: 6px solid $accent-color;
     border-bottom: 5px solid transparent;
 }
 
@@ -165,7 +165,7 @@ limitations under the License.
 .mx_RoomSubList_line {
     display: inline-block;
     width: 159px;
-    border-top: dotted 2px #76cfa6;
+    border-top: dotted 2px $accent-color;
     vertical-align: middle;
 }
 
@@ -179,7 +179,7 @@ limitations under the License.
     font-size: 10px;
     font-weight: 600;
     text-align: left;
-    color: #76cfa6;
+    color: $accent-color;
     padding-left: 7px;
     padding-right: 7px;
     padding-left: 7px;
@@ -198,20 +198,20 @@ limitations under the License.
     right: 8px; /*gutter */
     top: -2px;
     border-radius: 8px;
-    border: solid 1px #76cfa6;
-    color: #fff;
+    border: solid 1px $accent-color;
+    color: $accent-fg-color;
     font-weight: 600;
     font-size: 10px;
     text-align: center;
     padding-top: 1px;
     padding-left: 3px;
     padding-right: 3px;
-    background-color: #fff;
+    background-color: $primary-bg-color;
     vertical-align: middle;
 }
 
 .mx_RoomSubList_moreBadge.mx_RoomSubList_moreBadgeNotify {
-    background-color: #76cfa6;
+    background-color: $accent-color;
     border: 0;
     padding-top: 3px;
     padding-left: 4px;
@@ -219,7 +219,7 @@ limitations under the License.
 }
 
 .mx_RoomSubList_moreBadge.mx_RoomSubList_moreBadgeHighlight {
-    background-color: #ff0064;
+    background-color: $warning-color;
     border: 0;
     padding-top: 3px;
     padding-left: 4px;
diff --git a/src/skins/vector/css/vector-web/structures/ViewSource.css b/src/skins/vector/css/vector-web/structures/_ViewSource.scss
similarity index 100%
rename from src/skins/vector/css/vector-web/structures/ViewSource.css
rename to src/skins/vector/css/vector-web/structures/_ViewSource.scss
diff --git a/src/skins/vector/css/vector-web/views/context_menus/MessageContextMenu.css b/src/skins/vector/css/vector-web/views/context_menus/_MessageContextMenu.scss
similarity index 100%
rename from src/skins/vector/css/vector-web/views/context_menus/MessageContextMenu.css
rename to src/skins/vector/css/vector-web/views/context_menus/_MessageContextMenu.scss
diff --git a/src/skins/vector/css/vector-web/views/context_menus/NotificationStateContextMenu.css b/src/skins/vector/css/vector-web/views/context_menus/_NotificationStateContextMenu.scss
similarity index 100%
rename from src/skins/vector/css/vector-web/views/context_menus/NotificationStateContextMenu.css
rename to src/skins/vector/css/vector-web/views/context_menus/_NotificationStateContextMenu.scss
diff --git a/src/skins/vector/css/vector-web/views/context_menus/RoomTagContextMenu.css b/src/skins/vector/css/vector-web/views/context_menus/_RoomTagContextMenu.scss
similarity index 96%
rename from src/skins/vector/css/vector-web/views/context_menus/RoomTagContextMenu.css
rename to src/skins/vector/css/vector-web/views/context_menus/_RoomTagContextMenu.scss
index 947fd480..16a3ab79 100644
--- a/src/skins/vector/css/vector-web/views/context_menus/RoomTagContextMenu.css
+++ b/src/skins/vector/css/vector-web/views/context_menus/_RoomTagContextMenu.scss
@@ -31,7 +31,7 @@ limitations under the License.
 
 .mx_RoomTagContextMenu_field:last-child {
     padding-bottom: 4px;
-    color: #ff0064;
+    color: $warning-color;
 }
 
 .mx_RoomTagContextMenu_field.mx_RoomTagContextMenu_fieldSet {
@@ -70,8 +70,7 @@ limitations under the License.
     border-right-style: none;
     border-top-style: solid;
     border-top-width: 1px;
-    border-color: #bbbbbb;
-    opacity: 0.4;
+    border-color: $menu-border-color;
 }
 
 .mx_RoomTagContextMenu_fieldSet .mx_RoomTagContextMenu_icon {
diff --git a/src/skins/vector/css/vector-web/views/dialogs/ChangelogDialog.css b/src/skins/vector/css/vector-web/views/dialogs/_ChangelogDialog.scss
similarity index 100%
rename from src/skins/vector/css/vector-web/views/dialogs/ChangelogDialog.css
rename to src/skins/vector/css/vector-web/views/dialogs/_ChangelogDialog.scss
diff --git a/src/skins/vector/css/vector-web/views/directory/NetworkDropdown.css b/src/skins/vector/css/vector-web/views/directory/_NetworkDropdown.scss
similarity index 87%
rename from src/skins/vector/css/vector-web/views/directory/NetworkDropdown.css
rename to src/skins/vector/css/vector-web/views/directory/_NetworkDropdown.scss
index 8b4c4459..84aa896a 100644
--- a/src/skins/vector/css/vector-web/views/directory/NetworkDropdown.css
+++ b/src/skins/vector/css/vector-web/views/directory/_NetworkDropdown.scss
@@ -21,14 +21,14 @@ limitations under the License.
 .mx_NetworkDropdown_input {
     position: relative;
     border-radius: 3px;
-    border: 1px solid #c7c7c7;
+    border: 1px solid $strong-input-border-color;
     font-weight: 300;
     font-size: 13px;
     user-select: none;
 }
 
 .mx_NetworkDropdown_arrow {
-    border-color: #4a4a4a transparent transparent;
+    border-color: $primary-fg-color transparent transparent;
     border-style: solid;
     border-width: 5px 5px 0;
     display: block;
@@ -67,12 +67,12 @@ input.mx_NetworkDropdown_networkoption, input.mx_NetworkDropdown_networkoption:f
     margin: 0;
     padding: 0px;
     border-radius: 3px;
-    border: 1px solid #76cfa6;
-    background-color: white;
+    border: 1px solid $accent-color;
+    background-color: $primary-bg-color;
 }
 
 .mx_NetworkDropdown_menu .mx_NetworkDropdown_networkoption:hover {
-    background-color: #ddd;
+    background-color: $focus-bg-color;
 }
 
 .mx_NetworkDropdown_menu_network {
diff --git a/src/skins/vector/css/vector-web/views/elements/ImageView.css b/src/skins/vector/css/vector-web/views/elements/_ImageView.scss
similarity index 78%
rename from src/skins/vector/css/vector-web/views/elements/ImageView.css
rename to src/skins/vector/css/vector-web/views/elements/_ImageView.scss
index 03223f25..8ed0698a 100644
--- a/src/skins/vector/css/vector-web/views/elements/ImageView.css
+++ b/src/skins/vector/css/vector-web/views/elements/_ImageView.scss
@@ -19,39 +19,27 @@ limitations under the License.
  */
 
 .mx_ImageView {
-    display: -webkit-flex;
     display: flex;
     width: 100%;
     height: 100%;
-    -webkit-align-items: center;
     align-items: center;
 }
 
 .mx_ImageView_lhs {
-    -webkit-box-ordinal-group: 1;
     order: 1;
-    -webkit-flex: 1;
     flex: 1 1 10%;
     min-width: 60px;
-    /*
-    background-color: #080;
-    height: 20px;
-    */
+    // background-color: #080;
+    // height: 20px;
 }
 
 .mx_ImageView_content {
-    -webkit-box-ordinal-group: 2;
     order: 2;
     /* min-width hack needed for FF */
     min-width: 0px;
     height: 90%;
-    -webkit-flex: 15;
     flex: 15 15 0;
-
-    display: -webkit-flex;
     display: flex;
-    -webkit-align-items: center;
-    -webkit-justify-content: center;
     align-items: center;
     justify-content: center;
 }
@@ -62,7 +50,7 @@ limitations under the License.
     max-height: 100%;
     /* object-fit hack needed for Chrome due to Chrome not re-laying-out until you refresh */
     object-fit: contain;
-    /* background-image: url('img/trans.png'); */
+    /* background-image: url('../../img/trans.png'); */
     pointer-events: all;
 }
 
@@ -78,16 +66,13 @@ limitations under the License.
 .mx_ImageView_label {
     text-align: left;
     display: flex;
-    display: -webkit-flex;
     justify-content: center;
-    -webkit-justify-content: center;
     flex-direction: column;
-    -webkit-flex-direction: column;
     padding-left: 30px;
     padding-right: 30px;
     min-height: 100%;
     max-width: 240px;
-    color: #fff;
+    color: $lightbox-fg-color;
 }
 
 .mx_ImageView_cancel {
@@ -114,10 +99,10 @@ limitations under the License.
     margin-top: 24px;
     margin-bottom: 6px;
     border-radius: 5px;
-    background-color: #454545;
+    background-color: $lightbox-bg-color;
     font-size: 14px;
     padding: 9px;
-    border: 1px solid #fff;
+    border: 1px solid $lightbox-border-color;
 }
 
 .mx_ImageView_size {
@@ -125,7 +110,7 @@ limitations under the License.
 }
 
 .mx_ImageView_link {
-    color: #fff ! important;
+    color: $lightbox-fg-color ! important;
     text-decoration: none ! important;
 }
 
@@ -141,13 +126,9 @@ limitations under the License.
 }
 
 .mx_ImageView_rhs {
-    -webkit-box-ordinal-group: 3;
     order: 3;
-    -webkit-flex: 1;
     flex: 1 1 10%;
     min-width: 300px;
-    /*
-    background-color: #800;
-    height: 20px;
-    */
+    // background-color: #800;
+    // height: 20px;
 }
diff --git a/src/skins/vector/css/vector-web/views/elements/Spinner.css b/src/skins/vector/css/vector-web/views/elements/_Spinner.scss
similarity index 86%
rename from src/skins/vector/css/vector-web/views/elements/Spinner.css
rename to src/skins/vector/css/vector-web/views/elements/_Spinner.scss
index 3831cc4c..aea57379 100644
--- a/src/skins/vector/css/vector-web/views/elements/Spinner.css
+++ b/src/skins/vector/css/vector-web/views/elements/_Spinner.scss
@@ -15,16 +15,12 @@ limitations under the License.
 */
 
 .mx_Spinner {
-    display: -webkit-flex;
     display: flex;
-    -webkit-align-items: center;
-    -webkit-justify-content: center;
     align-items: center;
     justify-content: center;
     width: 100%;
     height: 100%;
     flex: 1;
-    -webkit-flex: 1;
 }
 
 .mx_MatrixChat_middlePanel .mx_Spinner {
diff --git a/src/skins/vector/css/vector-web/views/globals/GuestWarningBar.css b/src/skins/vector/css/vector-web/views/globals/_GuestWarningBar.scss
similarity index 79%
rename from src/skins/vector/css/vector-web/views/globals/GuestWarningBar.css
rename to src/skins/vector/css/vector-web/views/globals/_GuestWarningBar.scss
index 717d75af..f5bdbaf9 100644
--- a/src/skins/vector/css/vector-web/views/globals/GuestWarningBar.css
+++ b/src/skins/vector/css/vector-web/views/globals/_GuestWarningBar.scss
@@ -15,15 +15,10 @@ limitations under the License.
 */
 
 .mx_GuestWarningBar {
-    background-color: #76cfa6;
-    color: #fff;
+    background-color: $accent-color;
+    color: $accent-fg-color;
 
-    display: -webkit-box;
-    display: -moz-box;
-    display: -ms-flexbox;
-    display: -webkit-flex;
     display: flex;
-    -webkit-align-items: center;
     align-items: center;
 }
 
@@ -34,7 +29,7 @@ limitations under the License.
 }
 
 .mx_GuestWarningBar a {
-    color: #fff ! important;
+    color: $accent-fg-color ! important;
     text-decoration: underline ! important;
     cursor: pointer;
 }
diff --git a/src/skins/vector/css/vector-web/views/globals/MatrixToolbar.css b/src/skins/vector/css/vector-web/views/globals/_MatrixToolbar.scss
similarity index 82%
rename from src/skins/vector/css/vector-web/views/globals/MatrixToolbar.css
rename to src/skins/vector/css/vector-web/views/globals/_MatrixToolbar.scss
index 4e214e11..5a0b23ae 100644
--- a/src/skins/vector/css/vector-web/views/globals/MatrixToolbar.css
+++ b/src/skins/vector/css/vector-web/views/globals/_MatrixToolbar.scss
@@ -15,15 +15,10 @@ limitations under the License.
 */
 
 .mx_MatrixToolbar {
-    background-color: #76cfa6;
-    color: #fff;
+    background-color: $accent-color;
+    color: $accent-fg-color;
 
-    display: -webkit-box;
-    display: -moz-box;
-    display: -ms-flexbox;
-    display: -webkit-flex;
     display: flex;
-    -webkit-align-items: center;
     align-items: center;
 }
 
@@ -34,13 +29,12 @@ limitations under the License.
 }
 
 .mx_MatrixToolbar_content {
-    -webkit-flex: 1;
     flex: 1;
 }
 
 .mx_MatrixToolbar_link
 {
-    color: #fff ! important;
+    color: $accent-fg-color ! important;
     text-decoration: underline ! important;
     cursor: pointer;
 }
diff --git a/src/skins/vector/css/vector-web/views/messages/MessageTimestamp.css b/src/skins/vector/css/vector-web/views/messages/_MessageTimestamp.scss
similarity index 100%
rename from src/skins/vector/css/vector-web/views/messages/MessageTimestamp.css
rename to src/skins/vector/css/vector-web/views/messages/_MessageTimestamp.scss
diff --git a/src/skins/vector/css/vector-web/views/messages/SenderProfile.css b/src/skins/vector/css/vector-web/views/messages/_SenderProfile.scss
similarity index 100%
rename from src/skins/vector/css/vector-web/views/messages/SenderProfile.css
rename to src/skins/vector/css/vector-web/views/messages/_SenderProfile.scss
diff --git a/src/skins/vector/css/vector-web/views/rooms/RoomDropTarget.css b/src/skins/vector/css/vector-web/views/rooms/_RoomDropTarget.scss
similarity index 89%
rename from src/skins/vector/css/vector-web/views/rooms/RoomDropTarget.css
rename to src/skins/vector/css/vector-web/views/rooms/_RoomDropTarget.scss
index 003215af..e91658e8 100644
--- a/src/skins/vector/css/vector-web/views/rooms/RoomDropTarget.css
+++ b/src/skins/vector/css/vector-web/views/rooms/_RoomDropTarget.scss
@@ -22,9 +22,9 @@ limitations under the License.
     margin-bottom: 7px;
     padding-top: 5px;
     padding-bottom: 5px;
-    border: 1px dashed #76cfa6;
-    color: #454545;
-    background-color: rgba(255,255,255,0.5);
+    border: 1px dashed $accent-color;
+    color: $primary-fg-color;
+    background-color: $droptarget-bg-color;
     border-radius: 4px;
 }
 
@@ -39,7 +39,7 @@ limitations under the License.
 }
 
 .mx_RoomDropTarget_avatar {
-    background-color: #fff;
+    background-color: $primary-bg-color;
     border-radius: 24px;
     width: 24px;
     height: 24px;
diff --git a/src/skins/vector/css/vector-web/views/rooms/RoomTooltip.css b/src/skins/vector/css/vector-web/views/rooms/_RoomTooltip.scss
similarity index 82%
rename from src/skins/vector/css/vector-web/views/rooms/RoomTooltip.css
rename to src/skins/vector/css/vector-web/views/rooms/_RoomTooltip.scss
index 192fa77b..5469a9e6 100644
--- a/src/skins/vector/css/vector-web/views/rooms/RoomTooltip.css
+++ b/src/skins/vector/css/vector-web/views/rooms/_RoomTooltip.scss
@@ -21,16 +21,16 @@ limitations under the License.
     width: 0;
     height: 0;
     border-top: 8px solid transparent;
-    border-right: 8px solid rgba(187, 187, 187, 0.5);
+    border-right: 8px solid $menu-border-color;
     border-bottom: 8px solid transparent;
 }
 
-.mx_RoomTooltip_chevron:after{
+.mx_RoomTooltip_chevron:after {
     content:'';
     width: 0;
     height: 0;
     border-top: 7px solid transparent;
-    border-right: 7px solid #fff;
+    border-right: 7px solid $primary-bg-color;
     border-bottom: 7px solid transparent;
     position:absolute;
     top: -7px;
@@ -40,14 +40,14 @@ limitations under the License.
 .mx_RoomTooltip {
     display: none;
     position: fixed;
-    border: 1px solid rgba(187, 187, 187, 0.5);
+    border: 1px solid $menu-border-color;
     border-radius: 5px;
-    background-color: #fff;
+    background-color: $primary-bg-color;
     z-index: 2000;
     padding: 5px;
     pointer-events: none;
     line-height: 14px;
     font-size: 13px;
-    color: rgba(0, 0, 0, 0.7);
+    color: $primary-fg-color;
 }
 
diff --git a/src/skins/vector/css/vector-web/views/rooms/SearchBar.css b/src/skins/vector/css/vector-web/views/rooms/_SearchBar.scss
similarity index 85%
rename from src/skins/vector/css/vector-web/views/rooms/SearchBar.css
rename to src/skins/vector/css/vector-web/views/rooms/_SearchBar.scss
index 7ec1a17a..079ea16c 100644
--- a/src/skins/vector/css/vector-web/views/rooms/SearchBar.css
+++ b/src/skins/vector/css/vector-web/views/rooms/_SearchBar.scss
@@ -18,21 +18,18 @@ limitations under the License.
     padding-top: 5px;
     padding-bottom: 5px;
     display: flex;
-    display: -webkit-flex;
     align-items: center;
-    -webkit-align-items: center;
 }
 
 .mx_SearchBar_input {
     display: inline block;
     border-radius: 3px 0px 0px 3px;
-    border: 1px solid #f0f0f0;
+    border: 1px solid $input-border-color;
     font-size: 15px;
     padding: 9px;
     padding-left: 11px;
     width: auto;
     flex: 1 1 0;
-    -webkit-flex: 1 1 0;
 }
 
 .mx_SearchBar_searchButton {
@@ -41,7 +38,7 @@ limitations under the License.
     width: 37px;
     height: 37px;
     border-radius: 0px 3px 3px 0px;
-    background-color: #76CFA6;
+    background-color: $accent-color;
 }
 
 @keyframes pulsate {
@@ -61,8 +58,8 @@ limitations under the License.
     border-radius: 36px;
     font-weight: 400;
     font-size: 15px;
-    color: #fff;
-    background-color: #76cfa6;
+    color: $accent-fg-color;
+    background-color: $accent-color;
     width: auto;
     margin: auto;
     margin-left: 7px;
@@ -74,9 +71,9 @@ limitations under the License.
 }
 
 .mx_SearchBar_unselected {
-    background-color: #fff;
-    color: #76CFA6;
-    border: #76CFA6 1px solid;
+    background-color: $primary-bg-color;
+    color: $accent-color;
+    border: $accent-color 1px solid;
 }
 
 .mx_SearchBar_cancel {
diff --git a/src/skins/vector/css/vector-web/views/settings/Notifications.css b/src/skins/vector/css/vector-web/views/settings/_Notifications.scss
similarity index 98%
rename from src/skins/vector/css/vector-web/views/settings/Notifications.css
rename to src/skins/vector/css/vector-web/views/settings/_Notifications.scss
index 7a93f3f7..4c88e449 100644
--- a/src/skins/vector/css/vector-web/views/settings/Notifications.css
+++ b/src/skins/vector/css/vector-web/views/settings/_Notifications.scss
@@ -58,7 +58,7 @@ limitations under the License.
 
 .mx_UserNotifSettings_keywords {
     cursor: pointer;
-    color: #76cfa6;
+    color: $accent-color;
 }
 
 .mx_UserSettings_devicesTable td {
diff --git a/src/skins/vector/img/icon-call.svg b/src/skins/vector/img/icon-call.svg
index 2d96b145..0ca5c29e 100644
--- a/src/skins/vector/img/icon-call.svg
+++ b/src/skins/vector/img/icon-call.svg
@@ -2,7 +2,7 @@
 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
 <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="35" height="35" viewBox="0, 0, 35, 35">
   <g id="Symbols">
-    <path d="M17.5,35 C27.165,35 35,27.165 35,17.5 C35,7.835 27.165,0 17.5,0 C7.835,0 0,7.835 0,17.5 C0,27.165 7.835,35 17.5,35 z" fill="#EAF5F0" id="Oval-109"/>
+    <path d="M17.5,35 C27.165,35 35,27.165 35,17.5 C35,7.835 27.165,0 17.5,0 C7.835,0 0,7.835 0,17.5 C0,27.165 7.835,35 17.5,35 z" fill="#76CFA6" opacity="0.15" id="Oval-109"/>
     <path d="M23,28 C21.195,28 16.807,25.624 13.122,20.524 C9.675,15.755 8,12.309 8,9.99 C8,8.164 9.225,7.293 9.883,6.825 L10.045,6.708 C10.773,6.173 11.903,6 12.337,6 C13.097,6 13.417,6.458 13.611,6.857 C13.776,7.195 15.141,10.212 15.28,10.587 C15.492,11.164 15.422,12.005 14.766,12.488 L14.65,12.571 C14.324,12.804 13.718,13.236 13.634,13.761 C13.594,14.016 13.677,14.283 13.889,14.577 C14.946,16.043 18.32,20.346 18.928,20.931 C19.405,21.389 20.009,21.455 20.42,21.098 C20.846,20.729 21.035,20.511 21.037,20.508 L21.081,20.465 C21.116,20.434 21.449,20.162 21.992,20.162 C22.385,20.162 22.783,20.302 23.178,20.576 C24.201,21.287 26.51,22.876 26.51,22.876 L26.547,22.906 C26.842,23.166 27.269,23.917 26.772,24.893 C26.256,25.907 24.655,28 23,28 L23,28 z" fill-opacity="0" stroke="#76CFA6" stroke-width="1" id="path-1"/>
   </g>
 </svg>
diff --git a/src/skins/vector/img/icon_context_message_dark.svg b/src/skins/vector/img/icon_context_message_dark.svg
new file mode 100644
index 00000000..b4336cc3
--- /dev/null
+++ b/src/skins/vector/img/icon_context_message_dark.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="19px" height="19px" viewBox="0 0 19 19" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: sketchtool 39.1 (31720) - http://www.bohemiancoding.com/sketch -->
+    <title>ED5D3E59-2561-4AC1-9B43-82FBC51767FC</title>
+    <desc>Created with sketchtool.</desc>
+    <defs></defs>
+    <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="icon_context">
+            <g>
+                <path d="M9.5,19 C14.7467051,19 19,14.7467051 19,9.5 C19,4.25329488 14.7467051,0 9.5,0 C4.25329488,0 0,4.25329488 0,9.5 C0,14.7467051 4.25329488,19 9.5,19 Z" id="Oval-69" fill="#000" opacity="0.2"></path>
+                <path d="M4.5,9.50063771 C4.5,9.13148623 4.59887838,8.85242947 4.7966381,8.66345907 C4.99439782,8.47448867 5.28224377,8.38000488 5.66018457,8.38000488 C6.0249414,8.38000488 6.3072941,8.47668596 6.50725115,8.67005103 C6.70720821,8.86341609 6.80718523,9.14027555 6.80718523,9.50063771 C6.80718523,9.84781589 6.70610956,10.1213794 6.50395517,10.3213365 C6.30180079,10.5212935 6.02054674,10.6212705 5.66018457,10.6212705 C5.29103309,10.6212705 5.00538444,10.5234908 4.80323006,10.3279284 C4.60107568,10.132366 4.5,9.85660521 4.5,9.50063771 L4.5,9.50063771 Z M8.3431114,9.50063771 C8.3431114,9.13148623 8.44198978,8.85242947 8.63974951,8.66345907 C8.83750923,8.47448867 9.12755247,8.38000488 9.50988794,8.38000488 C9.87464476,8.38000488 10.1569975,8.47668596 10.3569545,8.67005103 C10.5569116,8.86341609 10.6568886,9.14027555 10.6568886,9.50063771 C10.6568886,9.84781589 10.5558129,10.1213794 10.3536585,10.3213365 C10.1515042,10.5212935 9.8702501,10.6212705 9.50988794,10.6212705 C9.13634179,10.6212705 8.84849585,10.5234908 8.64634146,10.3279284 C8.44418708,10.132366 8.3431114,9.85660521 8.3431114,9.50063771 L8.3431114,9.50063771 Z M12.1928148,9.50063771 C12.1928148,9.13148623 12.2916931,8.85242947 12.4894529,8.66345907 C12.6872126,8.47448867 12.9750585,8.38000488 13.3529993,8.38000488 C13.7177562,8.38000488 14.0001089,8.47668596 14.2000659,8.67005103 C14.400023,8.86341609 14.5,9.14027555 14.5,9.50063771 C14.5,9.84781589 14.3989243,10.1213794 14.1967699,10.3213365 C13.9946156,10.5212935 13.7133615,10.6212705 13.3529993,10.6212705 C12.9838479,10.6212705 12.6981992,10.5234908 12.4960448,10.3279284 C12.2938904,10.132366 12.1928148,9.85660521 12.1928148,9.50063771 L12.1928148,9.50063771 Z" id="…" fill="#FFF" opacity="0.6"></path>
+            </g>
+        </g>
+    </g>
+</svg>
diff --git a/src/skins/vector/img/icons-close-button.svg b/src/skins/vector/img/icons-close-button.svg
index f17940f5..f960d73a 100644
--- a/src/skins/vector/img/icons-close-button.svg
+++ b/src/skins/vector/img/icons-close-button.svg
@@ -7,7 +7,7 @@
     <g id="1:1-chat" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
         <g id="Chat-People-2-Invite-modal-(similar-to-chat-group-5)" transform="translate(-909.000000, -263.000000)">
             <g id="icons_close" transform="translate(909.000000, 263.000000)">
-                <path d="M17.5,35 C27.1649831,35 35,27.1649831 35,17.5 C35,7.83501688 27.1649831,0 17.5,0 C7.83501688,0 0,7.83501688 0,17.5 C0,27.1649831 7.83501688,35 17.5,35 Z" id="Oval-1-Copy-7" fill="#EAF5F0"></path>
+                <path d="M17.5,35 C27.1649831,35 35,27.1649831 35,17.5 C35,7.83501688 27.1649831,0 17.5,0 C7.83501688,0 0,7.83501688 0,17.5 C0,27.1649831 7.83501688,35 17.5,35 Z" id="Oval-1-Copy-7" fill="#76CFA6" opacity="0.15"></path>
                 <polyline id="icon_close" fill="#76CFA6" opacity="0.9" transform="translate(17.468897, 17.470577) rotate(-315.000000) translate(-17.468897, -17.470577) " points="18.2115394 5.97057742 16.674774 5.97057742 16.674774 16.7275762 5.9688975 16.7275762 5.9688975 18.2642903 16.674774 18.2642903 16.674774 28.9705774 18.2115394 28.9705774 18.2115394 18.2642903 28.9688975 18.2642903 28.9688975 16.7275762 18.2115394 16.7275762 18.2115394 5.97057742"></polyline>
             </g>
         </g>
diff --git a/src/skins/vector/img/icons-search.svg b/src/skins/vector/img/icons-search.svg
index 4f5002ab..d85709e6 100644
--- a/src/skins/vector/img/icons-search.svg
+++ b/src/skins/vector/img/icons-search.svg
@@ -2,7 +2,7 @@
 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
 <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="35" height="35" viewBox="0, 0, 35, 35">
   <g id="Symbols">
-    <path d="M17.5,35 C27.165,35 35,27.165 35,17.5 C35,7.835 27.165,0 17.5,0 C7.835,0 0,7.835 0,17.5 C0,27.165 7.835,35 17.5,35 z" fill="#EAF5F0" id="Oval-1-Copy-7"/>
+    <path d="M17.5,35 C27.165,35 35,27.165 35,17.5 C35,7.835 27.165,0 17.5,0 C7.835,0 0,7.835 0,17.5 C0,27.165 7.835,35 17.5,35 z" fill="#76CFA6" opacity="0.15" id="Oval-1-Copy-7"/>
     <path d="M22.4,15.4 C22.4,19.266 19.266,22.4 15.4,22.4 C11.534,22.4 8.4,19.266 8.4,15.4 C8.4,11.534 11.534,8.4 15.4,8.4 C19.266,8.4 22.4,11.534 22.4,15.4 z" fill-opacity="0" stroke="#76CFA6" stroke-width="1" stroke-linecap="round" id="path-1" opacity="0.7"/>
     <path d="M20.3,20.3 L25.25,25.25" fill-opacity="0" stroke="#76CFA6" stroke-width="1" stroke-linecap="round" id="Line" opacity="0.7"/>
   </g>
diff --git a/src/skins/vector/img/icons-upload.svg b/src/skins/vector/img/icons-upload.svg
index 9074fcf9..b0101e87 100644
--- a/src/skins/vector/img/icons-upload.svg
+++ b/src/skins/vector/img/icons-upload.svg
@@ -2,7 +2,7 @@
 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
 <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="35" height="35" viewBox="0, 0, 35, 35">
   <g id="Symbols">
-    <path d="M17.5,35 C27.165,35 35,27.165 35,17.5 C35,7.835 27.165,0 17.5,0 C7.835,0 0,7.835 0,17.5 C0,27.165 7.835,35 17.5,35 z" fill="#EAF5F0" id="Oval-109-Copy"/>
+    <path d="M17.5,35 C27.165,35 35,27.165 35,17.5 C35,7.835 27.165,0 17.5,0 C7.835,0 0,7.835 0,17.5 C0,27.165 7.835,35 17.5,35 z" fill="#76CFA6" opacity="0.15" id="Oval-109-Copy"/>
     <g id="file">
       <path d="M10,10.01 C10,7.795 11.782,6 14.004,6 L18.402,6 C18.402,6 25,12.492 25,12.492 L25,24.006 C25,26.212 23.206,28 21,28 L14,28 C11.791,28 10,26.2 10,23.99 L10,10.01 z" fill-opacity="0" stroke="#76CFA6" stroke-width="1" id="path-1"/>
       <path d="M25,13 L20.157,13 C18.966,13 18,12.034 18,10.843 L18,6" fill-opacity="0" stroke="#76CFA6" stroke-width="1" id="path-3"/>
diff --git a/src/skins/vector/img/icons-video.svg b/src/skins/vector/img/icons-video.svg
index a35df49b..d367f496 100644
--- a/src/skins/vector/img/icons-video.svg
+++ b/src/skins/vector/img/icons-video.svg
@@ -7,7 +7,7 @@
     <g id="Extra-icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
         <g id="Extra-icons-sheet" transform="translate(-542.000000, -366.000000)">
             <g id="icons_video" transform="translate(542.000000, 366.000000)">
-                <path d="M17.5,35 C27.1649831,35 35,27.1649831 35,17.5 C35,7.83501688 27.1649831,0 17.5,0 C7.83501688,0 0,7.83501688 0,17.5 C0,27.1649831 7.83501688,35 17.5,35 Z" id="Oval-109-Copy-2" fill="#EAF5F0"></path>
+                <path d="M17.5,35 C27.1649831,35 35,27.1649831 35,17.5 C35,7.83501688 27.1649831,0 17.5,0 C7.83501688,0 0,7.83501688 0,17.5 C0,27.1649831 7.83501688,35 17.5,35 Z" id="Oval-109-Copy-2" fill="#76CFA6" opacity="0.15"></path>
                 <g transform="translate(9.000000, 11.500000)" id="Rectangle-20-+-Path-16" stroke="#76CFA6">
                     <g transform="scale(1.0, 0.8)">
                         <rect id="Rectangle-20" x="0" y="0" width="13" height="17" rx="4"></rect>
diff --git a/src/skins/vector/img/logos/riot-logo-1.svg b/src/skins/vector/img/logos/riot-logo-1.svg
new file mode 100644
index 00000000..6b79dd9e
--- /dev/null
+++ b/src/skins/vector/img/logos/riot-logo-1.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 174.99 143.93"><defs><style>.cls-1{fill:#764d80;}.cls-2{fill:#7dc8a2;}.cls-3{fill:#afdbc5;}</style></defs><title>Asset 4</title><g id="Layer_2" data-name="Layer 2"><g id="Design"><path class="cls-1" d="M146.53,51.12h25.61a2.57,2.57,0,0,0,0-5.14H146.53a2.57,2.57,0,0,0,0,5.14Z"/><path class="cls-1" d="M146.53,5.14h7.66v7.79l-.05,0-9,6.32a2.57,2.57,0,0,0,2.95,4.21l7.23-5.06a10.24,10.24,0,0,0,19.42-4.59V2.64s0,0,0-.07A2.57,2.57,0,0,0,172.14,0H146.53a2.57,2.57,0,0,0,0,5.14Zm23,8.71a5.12,5.12,0,1,1-10.23,0V5.14h10.23Z"/><path class="cls-1" d="M172.14,118.28a2.57,2.57,0,0,0-2.57,2.57v7.6h-23A2.57,2.57,0,0,0,144,131h0a2.57,2.57,0,0,0,2.57,2.57h23v7.78a2.57,2.57,0,0,0,5.14,0V131h0V120.85A2.57,2.57,0,0,0,172.14,118.28Z"/><path class="cls-1" d="M159.34,72.63A15.65,15.65,0,1,0,175,88.28,15.67,15.67,0,0,0,159.34,72.63Zm0,26.16a10.51,10.51,0,1,1,10.51-10.51A10.52,10.52,0,0,1,159.34,98.79Z"/><path class="cls-2" d="M62.33,0H21.22A20.56,20.56,0,0,0,.66,20.57v102.8a20.56,20.56,0,0,0,41.12,0V102.79H62.33A51.38,51.38,0,0,0,62.33,0Z"/><path class="cls-3" d="M21.22,138.81A15.45,15.45,0,0,1,5.79,123.37V20.57A15.45,15.45,0,0,1,21.09,5.14H62.33a46.26,46.26,0,0,1,0,92.51H36.65v25.71A15.45,15.45,0,0,1,21.22,138.81Z"/><path class="cls-2" d="M21.22,133.68a10.32,10.32,0,0,1-10.3-10.3V20.57A10.33,10.33,0,0,1,21,10.27H62.33a41.13,41.13,0,0,1,0,82.26H31.52v30.84A10.32,10.32,0,0,1,21.22,133.68Z"/><path class="cls-3" d="M21.22,128.55A5.18,5.18,0,0,1,16,123.37V20.57a5.2,5.2,0,0,1,5-5.17H62.33a36,36,0,0,1,0,72H26.39v36A5.18,5.18,0,0,1,21.22,128.55Z"/><path class="cls-2" d="M21.27,82.28H62.33a30.87,30.87,0,0,0,0-61.75H21.22Z"/><path class="cls-3" d="M26.39,77.15l0-51.49h36a25.75,25.75,0,0,1,0,51.49Z"/><path class="cls-2" d="M31.51,72l0-41.23H62.33a20.62,20.62,0,0,1,0,41.23Z"/><path class="cls-3" d="M36.64,66.9l0-31H62.33a15.49,15.49,0,0,1,0,31Z"/><path class="cls-2" d="M41.76,61.77l0-20.72H62.33a10.36,10.36,0,0,1,0,20.72Z"/><path class="cls-3" d="M46.89,56.64V46.18H62.33a5.23,5.23,0,0,1,0,10.47Z"/><circle class="cls-1" cx="20.56" cy="20.57" r="20.56" transform="translate(-8.08 15.51) rotate(-35)"/><path class="cls-1" d="M109.34,111.57a20.56,20.56,0,1,1-33.69,23.59l-30.79-44A20.56,20.56,0,0,1,78.55,67.58Z"/></g></g></svg>
\ No newline at end of file
diff --git a/src/skins/vector/img/logos/riot-logo-2.svg b/src/skins/vector/img/logos/riot-logo-2.svg
new file mode 100644
index 00000000..96e0bbb1
--- /dev/null
+++ b/src/skins/vector/img/logos/riot-logo-2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 174.36 143.93"><defs><style>.cls-1{fill:#764d80;}.cls-2{fill:#f69e98;}</style></defs><title>Asset 2</title><g id="Layer_2" data-name="Layer 2"><g id="Design"><path class="cls-1" d="M61.67,0H20.56A20.56,20.56,0,0,0,0,20.57v102.8a20.56,20.56,0,0,0,41.12,0V102.79H61.67A51.38,51.38,0,0,0,61.67,0Z"/><path class="cls-2" d="M20.56,139.54A16.19,16.19,0,0,1,4.39,123.37V20.57a16.21,16.21,0,0,1,16-16.17H61.67a47,47,0,0,1,0,94H36.73v25A16.19,16.19,0,0,1,20.56,139.54ZM20.39,5.9A14.7,14.7,0,0,0,5.89,20.57v102.8a14.67,14.67,0,0,0,29.34,0V96.9H61.67a45.49,45.49,0,0,0,0-91H20.39Z"/><path class="cls-2" d="M20.56,134.4a11,11,0,0,1-11-11V20.57a11.09,11.09,0,0,1,10.79-11H61.67a41.85,41.85,0,0,1,0,83.7H31.59v30.11A11,11,0,0,1,20.56,134.4ZM20.3,11A9.57,9.57,0,0,0,11,20.57v102.8a9.53,9.53,0,1,0,19.05,0V91.75H61.67a40.35,40.35,0,0,0,0-80.7H20.3Z"/><path class="cls-2" d="M20.56,129.25a5.89,5.89,0,0,1-5.88-5.88V20.57a5.94,5.94,0,0,1,5.61-5.88H61.67a36.71,36.71,0,0,1,0,73.42H26.44v35.25A5.89,5.89,0,0,1,20.56,129.25ZM20.31,16.19a4.42,4.42,0,0,0-4.13,4.38v102.8a4.38,4.38,0,0,0,8.77,0V86.61H61.67a35.21,35.21,0,0,0,0-70.42Z"/><path class="cls-2" d="M61.67,83H19.81V19.83H61.67a31.57,31.57,0,0,1,0,63.14Zm-40.36-1.5H61.67a30.07,30.07,0,0,0,0-60.14H21.32Z"/><path class="cls-2" d="M61.67,77.83H25V25H61.67a26.43,26.43,0,0,1,0,52.85Zm-35.22-1.5H61.67a24.93,24.93,0,0,0,0-49.85H26.46Z"/><path class="cls-2" d="M61.67,72.69H30.1V30.12H61.67a21.29,21.29,0,0,1,0,42.57ZM31.6,71.19H61.67a19.79,19.79,0,0,0,0-39.57H31.6Z"/><path class="cls-2" d="M61.67,67.55H35.24V35.26H61.67a16.14,16.14,0,0,1,0,32.29ZM36.74,66H61.67a14.64,14.64,0,0,0,0-29.29H36.74Z"/><path class="cls-2" d="M61.67,62.41H40.38v-22H61.67a11,11,0,0,1,0,22Zm-19.79-1.5H61.67a9.5,9.5,0,0,0,0-19H41.88Z"/><path class="cls-2" d="M61.67,57.26H45.52V45.54H61.67a5.86,5.86,0,0,1,0,11.72ZM47,55.76H61.67a4.36,4.36,0,0,0,0-8.72H47Z"/><path class="cls-2" d="M61.18,52.16H51.89a.75.75,0,1,1,0-1.5h9.29a.75.75,0,0,1,0,1.5Z"/><path class="cls-2" d="M20.56,124.12a.75.75,0,0,1-.75-.75v-36a.75.75,0,0,1,1.5,0v36A.75.75,0,0,1,20.56,124.12Z"/><circle class="cls-2" cx="20.59" cy="20.57" r="20.56" transform="translate(-8.07 15.53) rotate(-35)"/><path class="cls-2" d="M109.36,111.56a20.56,20.56,0,1,1-33.69,23.59l-30.79-44A20.56,20.56,0,0,1,78.58,67.57Z"/><path class="cls-2" d="M145.91,51.12h25.61a2.57,2.57,0,0,0,0-5.14H145.91a2.57,2.57,0,1,0,0,5.14Z"/><path class="cls-2" d="M145.91,5.14h7.66v7.79l-.05,0-9,6.32a2.57,2.57,0,0,0,2.95,4.21l7.23-5.06a10.24,10.24,0,0,0,19.42-4.59V2.64s0,0,0-.07A2.57,2.57,0,0,0,171.51,0H145.91a2.57,2.57,0,1,0,0,5.14Zm23,8.71a5.12,5.12,0,1,1-10.23,0V5.14h10.23Z"/><path class="cls-2" d="M171.51,118.28a2.57,2.57,0,0,0-2.57,2.57v7.6h-23a2.57,2.57,0,0,0-2.57,2.57h0a2.57,2.57,0,0,0,2.57,2.57h23v7.78a2.57,2.57,0,0,0,5.14,0V131h0V120.85A2.57,2.57,0,0,0,171.51,118.28Z"/><path class="cls-2" d="M158.71,72.63a15.65,15.65,0,1,0,15.65,15.65A15.67,15.67,0,0,0,158.71,72.63Zm0,26.16a10.51,10.51,0,1,1,10.51-10.51A10.52,10.52,0,0,1,158.71,98.79Z"/></g></g></svg>
\ No newline at end of file
diff --git a/src/skins/vector/img/logos/riot-logo-3.svg b/src/skins/vector/img/logos/riot-logo-3.svg
new file mode 100644
index 00000000..985b9c9f
--- /dev/null
+++ b/src/skins/vector/img/logos/riot-logo-3.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 174.99 143.93"><defs><style>.cls-1{fill:#fac79e;}.cls-2{fill:#e45e5d;}.cls-3{fill:#f8a05f;}</style></defs><title>Asset 5</title><g id="Layer_2" data-name="Layer 2"><g id="Design"><path class="cls-1" d="M146.53,51.12h25.61a2.57,2.57,0,0,0,0-5.14H146.53a2.57,2.57,0,0,0,0,5.14Z"/><path class="cls-1" d="M146.53,5.14h7.66v7.79l-.05,0-9,6.32a2.57,2.57,0,0,0,2.95,4.21l7.23-5.06a10.24,10.24,0,0,0,19.42-4.59V2.64s0,0,0-.07A2.57,2.57,0,0,0,172.14,0H146.53a2.57,2.57,0,0,0,0,5.14Zm23,8.71a5.12,5.12,0,1,1-10.23,0V5.14h10.23Z"/><path class="cls-1" d="M172.14,118.28a2.57,2.57,0,0,0-2.57,2.57v7.6h-23A2.57,2.57,0,0,0,144,131h0a2.57,2.57,0,0,0,2.57,2.57h23v7.78a2.57,2.57,0,0,0,5.14,0V131h0V120.85A2.57,2.57,0,0,0,172.14,118.28Z"/><path class="cls-1" d="M159.34,72.63A15.65,15.65,0,1,0,175,88.28,15.67,15.67,0,0,0,159.34,72.63Zm0,26.16a10.51,10.51,0,1,1,10.51-10.51A10.52,10.52,0,0,1,159.34,98.79Z"/><path class="cls-2" d="M61.67,0H20.56A20.56,20.56,0,0,0,0,20.57v102.8a20.56,20.56,0,0,0,41.12,0V102.79H61.67A51.38,51.38,0,0,0,61.67,0Z"/><polygon class="cls-3" points="47.03 0.01 40.32 0.01 0 48.06 0 56.05 47.03 0.01"/><polygon class="cls-3" points="60.45 0.01 53.74 0.01 0 64.05 0 72.04 60.45 0.01"/><path class="cls-3" d="M72.83,1.24a51,51,0,0,0-5.9-1L0,80v8Z"/><polygon class="cls-3" points="33.61 0.01 26.9 0.01 0 32.06 0 40.06 33.61 0.01"/><path class="cls-3" d="M83.27,4.78a51,51,0,0,0-5-2L0,96v8Z"/><path class="cls-3" d="M41.12,102.78h.18l64.78-77.2a51.55,51.55,0,0,0-2.94-4.49L5.55,137.38a20.62,20.62,0,0,0,3.95,3.29L41.12,103Z"/><path class="cls-3" d="M112.22,42.23l-50.8,60.54h.25a51.55,51.55,0,0,0,6.85-.47L113,49.3A51.32,51.32,0,0,0,112.22,42.23Z"/><path class="cls-3" d="M40.71,127.46l-12.59,15A20.58,20.58,0,0,0,40.71,127.46Z"/><path class="cls-3" d="M110,68.88a51,51,0,0,0,2.67-11.18L76.73,100.52a51.1,51.1,0,0,0,10.6-4.64Z"/><path class="cls-3" d="M92.22,10.1a51.58,51.58,0,0,0-4.3-2.87L0,112v8Z"/><path class="cls-3" d="M99.83,17q-1.73-1.92-3.64-3.66L.42,127.5a20.41,20.41,0,0,0,2,5.58Z"/><path class="cls-3" d="M41.12,119v-8l-26.82,32a20.53,20.53,0,0,0,5.9,1Z"/><path class="cls-3" d="M110.71,36a51,51,0,0,0-2.09-5.5L48,102.78h6.71Z"/><circle class="cls-1" cx="20.59" cy="20.57" r="20.56" transform="translate(-8.07 15.53) rotate(-35)"/><path class="cls-1" d="M109.36,111.56a20.56,20.56,0,1,1-33.69,23.59l-30.79-44A20.56,20.56,0,0,1,78.58,67.57Z"/></g></g></svg>
\ No newline at end of file
diff --git a/src/skins/vector/img/logos/riot-logo-4.svg b/src/skins/vector/img/logos/riot-logo-4.svg
new file mode 100644
index 00000000..24a7ddab
--- /dev/null
+++ b/src/skins/vector/img/logos/riot-logo-4.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 175.64 143.93"><defs><style>.cls-1{fill:#e45e5d;}.cls-2{fill:#c7bdcd;}.cls-3{fill:#f69e98;}</style></defs><title>Asset 3</title><g id="Layer_2" data-name="Layer 2"><g id="Design"><path class="cls-1" d="M61.67,0H20.56A20.56,20.56,0,0,0,0,20.57v102.8a20.56,20.56,0,0,0,41.12,0V102.79H61.67A51.38,51.38,0,0,0,61.67,0Z"/><path class="cls-2" d="M147.19,51.12h25.61a2.57,2.57,0,1,0,0-5.14H147.19a2.57,2.57,0,0,0,0,5.14Z"/><path class="cls-2" d="M147.19,5.14h7.66v7.79l-.05,0-9,6.32a2.57,2.57,0,0,0,2.95,4.21l7.23-5.06a10.24,10.24,0,0,0,19.42-4.59V2.64s0,0,0-.07A2.57,2.57,0,0,0,172.79,0H147.19a2.57,2.57,0,0,0,0,5.14Zm23,8.71a5.12,5.12,0,1,1-10.23,0V5.14h10.23Z"/><path class="cls-2" d="M172.79,118.28a2.57,2.57,0,0,0-2.57,2.57v7.6h-23a2.57,2.57,0,0,0-2.57,2.57h0a2.57,2.57,0,0,0,2.57,2.57h23v7.78a2.57,2.57,0,0,0,5.14,0V131h0V120.85A2.57,2.57,0,0,0,172.79,118.28Z"/><path class="cls-2" d="M160,72.63a15.65,15.65,0,1,0,15.65,15.65A15.67,15.67,0,0,0,160,72.63Zm0,26.16A10.51,10.51,0,1,1,170.5,88.28,10.52,10.52,0,0,1,160,98.79Z"/><path class="cls-3" d="M2.77,133.64A20.62,20.62,0,0,0,7,138.77H34.14a20.62,20.62,0,0,0,4.21-5.13Z"/><path class="cls-3" d="M0,92.52v5.14H84a51.48,51.48,0,0,0,8.41-5.14Z"/><rect class="cls-3" y="102.8" width="41.12" height="5.13"/><rect class="cls-3" y="113.08" width="41.12" height="5.14"/><path class="cls-3" d="M.68,15.41H98.3a51.8,51.8,0,0,0-5.86-5.14H2.78A20.41,20.41,0,0,0,.68,15.41Z"/><path class="cls-3" d="M0,123.36H0a20.53,20.53,0,0,0,.67,5.12H40.45a20.53,20.53,0,0,0,.67-5.12H0Z"/><path class="cls-3" d="M0,56.53H112.8c.17-1.69.26-3.4.26-5.13H0Z"/><path class="cls-3" d="M0,82.24v5.14H98.32a51.79,51.79,0,0,0,4.43-5.14Z"/><path class="cls-3" d="M0,20.57V25.7H106.14a51.58,51.58,0,0,0-3.39-5.14Z"/><path class="cls-3" d="M20.81,0h-.25A20.46,20.46,0,0,0,7,5.14H84A51.07,51.07,0,0,0,61.67,0Z"/><path class="cls-3" d="M0,30.85V36H110.69a51,51,0,0,0-1.94-5.14Z"/><path class="cls-3" d="M0,72v5.13H106.14A51.24,51.24,0,0,0,108.75,72Z"/><path class="cls-3" d="M0,61.68v5.14H110.69A50.92,50.92,0,0,0,112,61.68Z"/><path class="cls-3" d="M0,41.22v5.14H112.8a51.15,51.15,0,0,0-.77-5.14Z"/><circle class="cls-2" cx="20.59" cy="20.57" r="20.56" transform="translate(-8.07 15.53) rotate(-35)"/><path class="cls-2" d="M109.36,111.56a20.56,20.56,0,0,1-33.69,23.59l-30.79-44A20.56,20.56,0,0,1,78.58,67.57Z"/></g></g></svg>
\ No newline at end of file
diff --git a/src/skins/vector/img/logos/riot-logo-5.svg b/src/skins/vector/img/logos/riot-logo-5.svg
new file mode 100644
index 00000000..6a2c61df
--- /dev/null
+++ b/src/skins/vector/img/logos/riot-logo-5.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 174.99 143.93"><defs><style>.cls-1{fill:#764d80;}.cls-2{fill:#afdbc5;}</style></defs><title>Asset 1</title><g id="Layer_2" data-name="Layer 2"><g id="Design"><path class="cls-1" d="M61.67,0H20.56A20.56,20.56,0,0,0,0,20.57v102.8a20.56,20.56,0,0,0,41.12,0V102.79H61.67A51.38,51.38,0,0,0,61.67,0Z"/><circle class="cls-2" cx="20.59" cy="20.57" r="20.56" transform="matrix(0.82, -0.57, 0.57, 0.82, -8.07, 15.53)"/><path class="cls-2" d="M109.36,111.56a20.56,20.56,0,1,1-33.69,23.59l-30.79-44A20.56,20.56,0,0,1,78.58,67.57Z"/><path class="cls-2" d="M146.53,51.12h25.61a2.57,2.57,0,0,0,0-5.14H146.53a2.57,2.57,0,0,0,0,5.14Z"/><path class="cls-2" d="M146.53,5.14h7.66v7.79l-.05,0-9,6.32a2.57,2.57,0,1,0,2.95,4.21l7.23-5.06a10.24,10.24,0,0,0,19.42-4.59V2.64s0,0,0-.07A2.57,2.57,0,0,0,172.14,0H146.53a2.57,2.57,0,0,0,0,5.14Zm23,8.71a5.12,5.12,0,1,1-10.23,0V5.14h10.23Z"/><path class="cls-2" d="M172.14,118.28a2.57,2.57,0,0,0-2.57,2.57v7.6h-23A2.57,2.57,0,0,0,144,131h0a2.57,2.57,0,0,0,2.57,2.57h23v7.78a2.57,2.57,0,0,0,5.14,0V131h0V120.85A2.57,2.57,0,0,0,172.14,118.28Z"/><path class="cls-2" d="M159.34,72.63A15.65,15.65,0,1,0,175,88.28,15.67,15.67,0,0,0,159.34,72.63Zm0,26.16a10.51,10.51,0,1,1,10.51-10.51A10.52,10.52,0,0,1,159.34,98.79Z"/></g></g></svg>
\ No newline at end of file
diff --git a/src/skins/vector/img/logos/riot-logo-bw.svg b/src/skins/vector/img/logos/riot-logo-bw.svg
new file mode 100644
index 00000000..cb6d77d6
--- /dev/null
+++ b/src/skins/vector/img/logos/riot-logo-bw.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 174.99 143.93"><defs><style>.cls-1{fill:#fff;}</style></defs><title>Asset 6</title><g id="Layer_2" data-name="Layer 2"><g id="Design"><path d="M146.53,51.12h25.61a2.57,2.57,0,0,0,0-5.14H146.53a2.57,2.57,0,0,0,0,5.14Z"/><path d="M146.53,5.14h7.66v7.79l-.05,0-9,6.32a2.57,2.57,0,0,0,2.95,4.21l7.23-5.06a10.24,10.24,0,0,0,19.42-4.59V2.64s0,0,0-.07A2.57,2.57,0,0,0,172.14,0H146.53a2.57,2.57,0,0,0,0,5.14Zm23,8.71a5.12,5.12,0,1,1-10.23,0V5.14h10.23Z"/><path d="M172.14,118.28a2.57,2.57,0,0,0-2.57,2.57v7.6h-23A2.57,2.57,0,0,0,144,131h0a2.57,2.57,0,0,0,2.57,2.57h23v7.78a2.57,2.57,0,0,0,5.14,0V131h0V120.85A2.57,2.57,0,0,0,172.14,118.28Z"/><path d="M159.34,72.63A15.65,15.65,0,1,0,175,88.28,15.67,15.67,0,0,0,159.34,72.63Zm0,26.16a10.51,10.51,0,1,1,10.51-10.51A10.52,10.52,0,0,1,159.34,98.79Z"/><path d="M62.33,0H21.22A20.56,20.56,0,0,0,.66,20.57v102.8a20.56,20.56,0,0,0,41.12,0V102.79H62.33A51.38,51.38,0,0,0,62.33,0Z"/><path class="cls-1" d="M21.22,138.81A15.45,15.45,0,0,1,5.79,123.37V20.57A15.45,15.45,0,0,1,21.09,5.14H62.33a46.26,46.26,0,0,1,0,92.51H36.65v25.71A15.45,15.45,0,0,1,21.22,138.81Z"/><path d="M21.22,133.68a10.32,10.32,0,0,1-10.3-10.3V20.57A10.33,10.33,0,0,1,21,10.27H62.33a41.13,41.13,0,0,1,0,82.26H31.52v30.84A10.32,10.32,0,0,1,21.22,133.68Z"/><path class="cls-1" d="M21.22,128.55A5.18,5.18,0,0,1,16,123.37V20.57a5.2,5.2,0,0,1,5-5.17H62.33a36,36,0,0,1,0,72H26.39v36A5.18,5.18,0,0,1,21.22,128.55Z"/><path d="M21.27,82.28H62.33a30.87,30.87,0,0,0,0-61.75H21.22Z"/><path class="cls-1" d="M26.39,77.15l0-51.49h36a25.75,25.75,0,0,1,0,51.49Z"/><path d="M31.51,72l0-41.23H62.33a20.62,20.62,0,0,1,0,41.23Z"/><path class="cls-1" d="M36.64,66.9l0-31H62.33a15.49,15.49,0,0,1,0,31Z"/><path d="M41.76,61.77l0-20.72H62.33a10.36,10.36,0,0,1,0,20.72Z"/><path class="cls-1" d="M46.89,56.64V46.18H62.33a5.23,5.23,0,0,1,0,10.47Z"/><circle cx="20.56" cy="20.57" r="20.56" transform="translate(-8.08 15.51) rotate(-35)"/><path d="M109.34,111.57a20.56,20.56,0,1,1-33.69,23.59l-30.79-44A20.56,20.56,0,0,1,78.55,67.58Z"/></g></g></svg>
\ No newline at end of file
diff --git a/src/skins/vector/img/logos/riot-logo.svg b/src/skins/vector/img/logos/riot-logo.svg
new file mode 100644
index 00000000..cbfaa625
--- /dev/null
+++ b/src/skins/vector/img/logos/riot-logo.svg
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:#7DC8A2;}
+	.st1{fill:#AFDBC5;}
+	.st2{fill:#764D80;}
+</style>
+<g>
+	<path class="st0" d="M212.8,38.8h-91.2c-0.2,0-0.4,0-0.6,0c-25.3,0-45.9,20.5-45.9,45.9v229.5c0,25.3,20.5,45.9,45.9,45.9
+		s45.9-20.5,45.9-45.9v-45.9h45.9c63.2,0,114.7-51.5,114.7-114.7S276,38.8,212.8,38.8z"/>
+	<path class="st1" d="M121,348.6c-19,0-34.4-15.5-34.4-34.4V84.7c0-18.9,15.3-34.3,34.2-34.4c0.2,0,0.5,0,0.7,0h91.3
+		c56.9,0,103.3,46.3,103.3,103.3s-46.3,103.3-103.3,103.3h-57.3v57.4C155.4,333.2,140,348.6,121,348.6z"/>
+	<path class="st0" d="M121,337.2c-12.7,0-23-10.3-23-23V84.7c0-12.5,10.1-22.7,22.5-23c0.2,0,0.5,0,0.7,0l91.5,0
+		c50.6,0,91.8,41.2,91.8,91.8s-41.2,91.8-91.8,91.8H144v68.8C144,326.9,133.7,337.2,121,337.2z"/>
+	<path class="st1" d="M121,325.7c-6.4,0-11.6-5.2-11.6-11.6V84.7c0-6.2,5-11.3,11.1-11.5l0.5,0l91.8,0c44.3,0,80.4,36.1,80.4,80.4
+		s-36,80.4-80.4,80.4h-80.2v80.3C132.6,320.5,127.4,325.7,121,325.7z"/>
+	<path class="st0" d="M121.1,222.4h91.7c38,0,68.9-30.9,68.9-68.9s-30.9-68.9-68.9-68.9H121L121.1,222.4z"/>
+	<path class="st1" d="M132.5,211l-0.1-114.9h80.3c31.7,0,57.5,25.8,57.5,57.5S244.5,211,212.8,211H132.5z"/>
+	<path class="st0" d="M144,199.6l-0.1-92h68.9c25.4,0,46,20.6,46,46s-20.6,46-46,46H144z"/>
+	<path class="st1" d="M155.4,188.1l-0.1-69.1h57.4c19.1,0,34.6,15.5,34.6,34.6s-15.5,34.6-34.6,34.6H155.4z"/>
+	<path class="st0" d="M166.9,176.7l0-46.3h45.9c12.8,0,23.1,10.4,23.1,23.1s-10.4,23.1-23.1,23.1H166.9z"/>
+	<path class="st1" d="M178.3,165.2l0-23.4h34.5c6.4,0,11.7,5.2,11.7,11.7s-5.2,11.7-11.7,11.7H178.3z"/>
+	<g>
+		
+			<ellipse transform="matrix(0.8192 -0.5736 0.5736 0.8192 -26.9642 83.8822)" class="st2" cx="119.5" cy="84.7" rx="45.9" ry="45.9"/>
+		<path class="st2" d="M317.7,287.8c14.5,20.8,9.5,49.4-11.3,63.9c-20.8,14.5-49.4,9.5-63.9-11.3l-68.7-98.2
+			c-14.5-20.8-9.5-49.4,11.3-63.9c20.8-14.5,49.4-9.5,63.9,11.3L317.7,287.8z"/>
+	</g>
+</g>
+</svg>
diff --git a/src/utils/DirectoryUtils.js b/src/utils/DirectoryUtils.js
new file mode 100644
index 00000000..72e44681
--- /dev/null
+++ b/src/utils/DirectoryUtils.js
@@ -0,0 +1,23 @@
+// Find a protocol 'instance' with a given instance_id
+// in the supplied protocols dict
+export function instanceForInstanceId(protocols, instance_id) {
+    if (!instance_id) return null;
+    for (const proto of Object.keys(protocols)) {
+        if (!protocols[proto].instances && protocols[proto].instances instanceof Array) continue;
+        for (const instance of protocols[proto].instances) {
+            if (instance.instance_id == instance_id) return instance;
+        }
+    }
+}
+
+// given an instance_id, return the name of the protocol for
+// that instance ID in the supplied protocols dict
+export function protocolNameForInstanceId(protocols, instance_id) {
+    if (!instance_id) return null;
+    for (const proto of Object.keys(protocols)) {
+        if (!protocols[proto].instances && protocols[proto].instances instanceof Array) continue;
+        for (const instance of protocols[proto].instances) {
+            if (instance.instance_id == instance_id) return proto;
+        }
+    }
+}
diff --git a/src/vector/index.html b/src/vector/index.html
index 73cdd2df..3ab667cc 100644
--- a/src/vector/index.html
+++ b/src/vector/index.html
@@ -20,14 +20,24 @@
     <meta name="msapplication-TileImage" content="vector-icons/mstile-144x144.png">
     <meta name="msapplication-config" content="vector-icons/browserconfig.xml">
     <meta name="theme-color" content="#ffffff">
-    <% for(var i=0; i<htmlWebpackPlugin.files.css.length; i++) {%>
-       <link href="<%= htmlWebpackPlugin.files.css[i] %>" rel="stylesheet">
-    <% } %>
+    <% for (var i=0; i < htmlWebpackPlugin.files.css.length; i++) {
+        var file = htmlWebpackPlugin.files.css[i];
+        var match = file.match(/^bundles\/.*?\/theme-(.*)\.css$/);
+        if (match) {
+            var title = match[1].charAt(0).toUpperCase() + match[1].slice(1);
+            var light = match[1] == 'light';
+        %>
+           <link rel="<%= light ? '' : 'alternate ' %>stylesheet" title="<%= title %>"
+               href="<%= file %>">
+        <% } else { %>
+           <link rel="stylesheet" href="<%= file %>">
+        <% }
+    } %>
   </head>
   <body style="height: 100%;">
     <section id="matrixchat" style="height: 100%;"></section>
     <noscript>Sorry, Riot requires JavaScript to be enabled.</noscript>
-    <% for(var i=0; i<htmlWebpackPlugin.files.js.length; i++) {%>
+    <% for (var i=0; i < htmlWebpackPlugin.files.js.length; i++) {%>
        <script src="<%= htmlWebpackPlugin.files.js[i] %>"></script>
     <% } %>
     <img src="img/warning.svg" width="24" height="23" style="visibility: hidden; position: absolute; top: 0px; left: 0px;"/>
diff --git a/src/vector/index.js b/src/vector/index.js
index 9c9d48fa..d7072d09 100644
--- a/src/vector/index.js
+++ b/src/vector/index.js
@@ -28,14 +28,22 @@ limitations under the License.
 // https://babeljs.io/docs/plugins/transform-runtime/
 require('babel-polyfill');
 
-// CSS requires: just putting them here for now as CSS is going to be
-// refactored "soon" anyway
-require('../../build/components.css');
+// Require common CSS here; this will make webpack process it into bundle.css.
+// Our own CSS (which is themed) is imported via separate webpack entry points
+// in webpack.config.js
 require('gemini-scrollbar/gemini-scrollbar.css');
 require('gfm.css/gfm.css');
 require('highlight.js/styles/github.css');
 require('draft-js/dist/Draft.css');
 
+const rageshake = require("./rageshake");
+rageshake.init().then(() => {
+    console.log("Initialised rageshake: See https://bugs.chromium.org/p/chromium/issues/detail?id=583193 to fix line numbers on Chrome.");
+    rageshake.cleanup();
+}, (err) => {
+    console.error("Failed to initialise rageshake: " + err);
+});
+
 
  // add React and ReactPerf to the global namespace, to make them easier to
  // access via the console
@@ -54,7 +62,6 @@ var UpdateChecker = require("./updater");
 var q = require('q');
 var request = require('browser-request');
 
-import UAParser from 'ua-parser-js';
 import url from 'url';
 
 import {parseQs, parseQsFromFragment} from './url_utils';
@@ -120,6 +127,8 @@ var lastLoadedScreen = null;
 // so a web page can update the URL bar appropriately.
 var onNewScreen = function(screen) {
     console.log("newscreen "+screen);
+    // just remember the most recent screen while we are loading, so that the
+    // user doesn't see the URL bar doing a dance
     if (!loaded) {
         lastLoadedScreen = screen;
     } else {
@@ -135,40 +144,23 @@ var onNewScreen = function(screen) {
 // click back to the client having registered.
 // It's up to us to recognise if we're loaded with
 // this URL and tell MatrixClient to resume registration.
+//
+// If we're in electron, we should never pass through a file:// URL otherwise
+// the identity server will try to 302 the browser to it, which breaks horribly.
+// so in that instance, hardcode to use riot.im/app for now instead.
 var makeRegistrationUrl = function() {
-    return window.location.protocol + '//' +
-           window.location.host +
-           window.location.pathname +
-           '#/register';
-}
-
-
-function getDefaultDeviceDisplayName() {
-    // strip query-string and fragment from uri
-    let u = url.parse(window.location.href);
-    u.search = "";
-    u.hash = "";
-    let app_name = u.format();
-
-    let ua = new UAParser();
-    return app_name + " via " + ua.getBrowser().name +
-        " on " + ua.getOS().name;
+    if (window.location.protocol === "file:") {
+        return 'https://riot.im/app/#/register';
+    }
+    else {
+        return window.location.protocol + '//' +
+               window.location.host +
+               window.location.pathname +
+               '#/register';
+    }
 }
 
 window.addEventListener('hashchange', onHashChange);
-window.onload = function() {
-    console.log("window.onload");
-    if (!validBrowser) {
-        return;
-    }
-    UpdateChecker.start();
-    routeUrl(window.location);
-    loaded = true;
-    if (lastLoadedScreen) {
-        onNewScreen(lastLoadedScreen);
-        lastLoadedScreen = null;
-    }
-}
 
 function getConfig() {
     let deferred = q.defer();
@@ -249,6 +241,7 @@ async function loadApp() {
     let configError;
     try {
         configJson = await getConfig();
+        rageshake.setBugReportEndpoint(configJson.bug_report_endpoint_url);
     } catch (e) {
         configError = e;
     }
@@ -259,6 +252,8 @@ async function loadApp() {
             Unable to load config file: please refresh the page to try again.
         </div>, document.getElementById('matrixchat'));
     } else if (validBrowser) {
+        UpdateChecker.start();
+
         var MatrixChat = sdk.getComponent('structures.MatrixChat');
 
         window.matrixChat = ReactDOM.render(
@@ -271,10 +266,19 @@ async function loadApp() {
                 startingFragmentQueryParams={fragparts.params}
                 enableGuest={true}
                 onLoadCompleted={onLoadCompleted}
-                defaultDeviceDisplayName={getDefaultDeviceDisplayName()}
+                defaultDeviceDisplayName={PlatformPeg.get().getDefaultDeviceDisplayName()}
             />,
             document.getElementById('matrixchat')
         );
+
+        routeUrl(window.location);
+
+        // we didn't propagate screen changes to the URL bar while we were loading; do it now.
+        loaded = true;
+        if (lastLoadedScreen) {
+            onNewScreen(lastLoadedScreen);
+            lastLoadedScreen = null;
+        }
     }
     else {
         console.error("Browser is missing required features.");
@@ -285,7 +289,6 @@ async function loadApp() {
                 validBrowser = true;
                 console.log("User accepts the compatibility risks.");
                 loadApp();
-                window.onload(); // still do the same code paths for compatible clients
             }} />,
             document.getElementById('matrixchat')
         );
diff --git a/src/vector/olm-loader.js b/src/vector/olm-loader.js
new file mode 100644
index 00000000..a62d05b4
--- /dev/null
+++ b/src/vector/olm-loader.js
@@ -0,0 +1,41 @@
+/*
+Copyright 2016 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+/* a very thin shim for loading olm.js: just sets the global OLM_OPTIONS and
+ * requires the actual olm.js library.
+ *
+ * olm.js reads global.OLM_OPTIONS and defines global.Olm. The latter is fine for us,
+ * but we need to prepare the former.
+ *
+ * We can't use webpack's definePlugin to do this, because we tell webpack not
+ * to parse olm.js. We also can't put this code in index.js, because olm and
+ * index.js are loaded in parallel, and we need to make sure OLM_OPTIONS is set
+ * before olm.js is loaded.
+ */
+
+/* total_memory must be a power of two, and at least twice the stack.
+ *
+ * We don't need a lot of stack, but we do need about 128K of heap to encrypt a
+ * 64K event (enough to store the ciphertext and the plaintext, bearing in mind
+ * that the plaintext can only be 48K because base64). We also have about 36K
+ * of statics. So let's have 256K of memory.
+ */
+global.OLM_OPTIONS = {
+    TOTAL_STACK: 64*1024,
+    TOTAL_MEMORY: 256*1024,
+};
+
+require('olm/olm.js');
diff --git a/src/vector/platform/ElectronPlatform.js b/src/vector/platform/ElectronPlatform.js
index c7455a71..c10f2f83 100644
--- a/src/vector/platform/ElectronPlatform.js
+++ b/src/vector/platform/ElectronPlatform.js
@@ -35,8 +35,30 @@ function onUpdateDownloaded(ev, releaseNotes, ver, date, updateURL) {
     });
 }
 
+function platformFriendlyName() {
+    console.log(window.process);
+    switch (window.process.platform) {
+        case 'darwin':
+            return 'macOS';
+        case 'freebsd':
+            return 'FreeBSD';
+        case 'openbsd':
+            return 'OpenBSD';
+        case 'sunos':
+            return 'SunOS';
+        case 'win32':
+            return 'Windows';
+        default:
+            // Sorry, Linux users: you get lumped into here,
+            // but only because Linux's capitalisation is
+            // normal. We do care about you.
+            return window.process.platform[0].toUpperCase() + window.process.platform.slice(1);
+    }
+}
+
 export default class ElectronPlatform extends VectorBasePlatform {
     setNotificationCount(count: number) {
+        if (this.notificationCount === count) return;
         super.setNotificationCount(count);
         // this sometimes throws because electron is made of fail:
         // https://github.com/electron/electron/issues/7351
@@ -58,7 +80,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
         return true;
     }
 
-    displayNotification(title: string, msg: string, avatarUrl: string): Notification {
+    displayNotification(title: string, msg: string, avatarUrl: string, room: Object): Notification {
         // Notifications in Electron use the HTML5 notification API
         const notification = new global.Notification(
             title,
@@ -76,6 +98,10 @@ export default class ElectronPlatform extends VectorBasePlatform {
                 room_id: room.roomId
             });
             global.focus();
+            const currentWin = electron.remote.getCurrentWindow();
+            currentWin.show();
+            currentWin.restore();
+            currentWin.focus();
         };
 
         return notification;
@@ -101,4 +127,16 @@ export default class ElectronPlatform extends VectorBasePlatform {
         // it should exit.
         electron.ipcRenderer.send('install_update');
     }
+
+    getDefaultDeviceDisplayName() {
+        return "Riot Desktop on " + platformFriendlyName();
+    }
+
+    screenCaptureErrorString() {
+        return null;
+    }
+
+    requestNotificationPermission() : Promise {
+        return q('granted');
+    }
 }
diff --git a/src/vector/platform/VectorBasePlatform.js b/src/vector/platform/VectorBasePlatform.js
index d2ed8c34..5240f3f5 100644
--- a/src/vector/platform/VectorBasePlatform.js
+++ b/src/vector/platform/VectorBasePlatform.js
@@ -39,4 +39,12 @@ export default class VectorBasePlatform extends BasePlatform {
      */
     installUpdate() {
     }
+
+    /**
+     * Get a sensible default display name for the
+     * device Vector is running on
+     */
+    getDefaultDeviceDisplayName() {
+        return "Unknown device";
+    }
 }
diff --git a/src/vector/platform/WebPlatform.js b/src/vector/platform/WebPlatform.js
index b5459aec..bf417513 100644
--- a/src/vector/platform/WebPlatform.js
+++ b/src/vector/platform/WebPlatform.js
@@ -23,6 +23,9 @@ import request from 'browser-request';
 import dis from 'matrix-react-sdk/lib/dispatcher.js';
 import q from 'q';
 
+import url from 'url';
+import UAParser from 'ua-parser-js';
+
 export default class WebPlatform extends VectorBasePlatform {
     constructor() {
         super();
@@ -57,11 +60,13 @@ export default class WebPlatform extends VectorBasePlatform {
     }
 
     setNotificationCount(count: number) {
+        if (this.notificationCount === count) return;
         super.setNotificationCount(count);
         this._updateFavicon();
     }
 
     setErrorStatus(errorDidOccur: boolean) {
+        if (this.errorDidOccur === errorDidOccur) return;
         super.setErrorStatus(errorDidOccur);
         this._updateFavicon();
     }
@@ -100,7 +105,7 @@ export default class WebPlatform extends VectorBasePlatform {
         return defer.promise;
     }
 
-    displayNotification(title: string, msg: string, avatarUrl: string) {
+    displayNotification(title: string, msg: string, avatarUrl: string, room: Object) {
         const notification = new global.Notification(
             title,
             {
@@ -117,6 +122,7 @@ export default class WebPlatform extends VectorBasePlatform {
                 room_id: room.roomId
             });
             global.focus();
+            notification.close();
         };
 
         // Chrome only dismisses notifications after 20s, which
@@ -128,8 +134,18 @@ export default class WebPlatform extends VectorBasePlatform {
 
     _getVersion() {
         const deferred = q.defer();
+
+        // We add a cachebuster to the request to make sure that we know about
+        // the most recent version on the origin server. That might not
+        // actually be the version we'd get on a reload (particularly in the
+        // presence of intermediate caching proxies), but still: we're trying
+        // to tell the user that there is a new version.
         request(
-            { method: "GET", url: "version" },
+            {
+                method: "GET",
+                url: "version",
+                qs: { cachebuster: Date.now() },
+            },
             (err, response, body) => {
                 if (err || response.status < 200 || response.status >= 300) {
                     if (err == null) err = { status: response.status };
@@ -170,4 +186,24 @@ export default class WebPlatform extends VectorBasePlatform {
     installUpdate() {
         window.location.reload();
     }
+
+    getDefaultDeviceDisplayName() {
+        // strip query-string and fragment from uri
+        let u = url.parse(window.location.href);
+        u.search = "";
+        u.hash = "";
+        let app_name = u.format();
+
+        let ua = new UAParser();
+        return app_name + " via " + ua.getBrowser().name +
+            " on " + ua.getOS().name;
+    }
+
+    screenCaptureErrorString() {
+        // it won't work at all if you're not on HTTPS so whine whine whine
+        if (!global.window || global.window.location.protocol !== "https:") {
+            return "You need to be using HTTPS to place a screen-sharing call.";
+        }
+        return null;
+    }
 }
diff --git a/src/vector/rageshake.js b/src/vector/rageshake.js
new file mode 100644
index 00000000..61e29a31
--- /dev/null
+++ b/src/vector/rageshake.js
@@ -0,0 +1,501 @@
+/*
+Copyright 2017 OpenMarket Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import PlatformPeg from 'matrix-react-sdk/lib/PlatformPeg';
+import request from "browser-request";
+import q from "q";
+
+// This module contains all the code needed to log the console, persist it to
+// disk and submit bug reports. Rationale is as follows:
+//  - Monkey-patching the console is preferable to having a log library because
+//    we can catch logs by other libraries more easily, without having to all
+//    depend on the same log framework / pass the logger around.
+//  - We use IndexedDB to persists logs because it has generous disk space
+//    limits compared to local storage. IndexedDB does not work in incognito
+//    mode, in which case this module will not be able to write logs to disk.
+//    However, the logs will still be stored in-memory, so can still be
+//    submitted in a bug report should the user wish to: we can also store more
+//    logs in-memory than in local storage, which does work in incognito mode.
+//    We also need to handle the case where there are 2+ tabs. Each JS runtime
+//    generates a random string which serves as the "ID" for that tab/session.
+//    These IDs are stored along with the log lines.
+//  - Bug reports are sent as a POST over HTTPS: it purposefully does not use
+//    Matrix as bug reports may be made when Matrix is not responsive (which may
+//    be the cause of the bug). We send the most recent N MB of UTF-8 log data,
+//    starting with the most recent, which we know because the "ID"s are
+//    actually timestamps. We then purge the remaining logs. We also do this
+//    purge on startup to prevent logs from accumulating.
+
+const FLUSH_RATE_MS = 30 * 1000;
+
+// A class which monkey-patches the global console and stores log lines.
+class ConsoleLogger {
+    constructor() {
+        this.logs = "";
+    }
+
+    monkeyPatch(consoleObj) {
+        // Monkey-patch console logging
+        const consoleFunctionsToLevels = {
+            log: "I",
+            info: "I",
+            warn: "W",
+            error: "E",
+        };
+        Object.keys(consoleFunctionsToLevels).forEach((fnName) => {
+            const level = consoleFunctionsToLevels[fnName];
+            let originalFn = consoleObj[fnName].bind(consoleObj);
+            consoleObj[fnName] = (...args) => {
+                this.log(level, ...args);
+                originalFn(...args);
+            }
+        });
+    }
+
+    log(level, ...args) {
+        // We don't know what locale the user may be running so use ISO strings
+        const ts = new Date().toISOString();
+        // Some browsers support string formatting which we're not doing here
+        // so the lines are a little more ugly but easy to implement / quick to
+        // run.
+        // Example line:
+        // 2017-01-18T11:23:53.214Z W Failed to set badge count
+        const line = `${ts} ${level} ${args.join(' ')}\n`;
+        // Using + really is the quickest way in JS
+        // http://jsperf.com/concat-vs-plus-vs-join
+        this.logs += line;
+    }
+
+    /**
+     * Retrieve log lines to flush to disk.
+     * @param {boolean} keepLogs True to not delete logs after flushing.
+     * @return {string} \n delimited log lines to flush.
+     */
+    flush(keepLogs) {
+        // The ConsoleLogger doesn't care how these end up on disk, it just
+        // flushes them to the caller.
+        if (keepLogs) {
+            return this.logs;
+        }
+        const logsToFlush = this.logs;
+        this.logs = "";
+        return logsToFlush;
+    }
+}
+
+// A class which stores log lines in an IndexedDB instance.
+class IndexedDBLogStore {
+    constructor(indexedDB, logger) {
+        this.indexedDB = indexedDB;
+        this.logger = logger;
+        this.id = "instance-" + Math.random() + Date.now();
+        this.index = 0;
+        this.db = null;
+        this.flushPromise = null;
+        // set if flush() is called whilst one is ongoing
+        this.flushAgainPromise = null;
+    }
+
+    /**
+     * @return {Promise} Resolves when the store is ready.
+     */
+    connect() {
+        let req = this.indexedDB.open("logs");
+        return q.Promise((resolve, reject) => {
+            req.onsuccess = (event) => {
+                this.db = event.target.result;
+                // Periodically flush logs to local storage / indexeddb
+                setInterval(this.flush.bind(this), FLUSH_RATE_MS);
+                resolve();
+            };
+
+            req.onerror = (event) => {
+                const err = (
+                    "Failed to open log database: " + event.target.errorCode
+                );
+                console.error(err);
+                reject(new Error(err));
+            };
+
+            // First time: Setup the object store
+            req.onupgradeneeded = (event) => {
+                const db = event.target.result;
+                const logObjStore = db.createObjectStore("logs", {
+                    keyPath: ["id", "index"]
+                });
+                // Keys in the database look like: [ "instance-148938490", 0 ]
+                // Later on we need to query everything based on an instance id.
+                // In order to do this, we need to set up indexes "id".
+                logObjStore.createIndex("id", "id", { unique: false });
+
+                logObjStore.add(
+                    this._generateLogEntry(
+                        new Date() + " ::: Log database was created."
+                    )
+                );
+
+                const lastModifiedStore = db.createObjectStore("logslastmod", {
+                    keyPath: "id",
+                });
+                lastModifiedStore.add(this._generateLastModifiedTime());
+            }
+        });
+    }
+
+    /**
+     * Flush logs to disk.
+     *
+     * There are guards to protect against race conditions in order to ensure
+     * that all previous flushes have completed before the most recent flush.
+     * Consider without guards:
+     *  - A calls flush() periodically.
+     *  - B calls flush() and wants to send logs immediately afterwards.
+     *  - If B doesn't wait for A's flush to complete, B will be missing the
+     *    contents of A's flush.
+     * To protect against this, we set 'flushPromise' when a flush is ongoing.
+     * Subsequent calls to flush() during this period will chain another flush,
+     * then keep returning that same chained flush.
+     *
+     * This guarantees that we will always eventually do a flush when flush() is
+     * called.
+     *
+     * @return {Promise} Resolved when the logs have been flushed.
+     */
+    flush() {
+        // check if a flush() operation is ongoing
+        if (this.flushPromise && this.flushPromise.isPending()) {
+            if (this.flushAgainPromise && this.flushAgainPromise.isPending()) {
+                // this is the 3rd+ time we've called flush() : return the same
+                // promise.
+                return this.flushAgainPromise;
+            }
+            // queue up a flush to occur immediately after the pending one
+            // completes.
+            this.flushAgainPromise = this.flushPromise.then(() => {
+                return this.flush();
+            });
+            return this.flushAgainPromise;
+        }
+        // there is no flush promise or there was but it has finished, so do
+        // a brand new one, destroying the chain which may have been built up.
+        this.flushPromise = q.Promise((resolve, reject) => {
+            if (!this.db) {
+                // not connected yet or user rejected access for us to r/w to
+                // the db.
+                reject(new Error("No connected database"));
+                return;
+            }
+            const lines = this.logger.flush();
+            if (lines.length === 0) {
+                resolve();
+                return;
+            }
+            let txn = this.db.transaction(["logs", "logslastmod"], "readwrite");
+            let objStore = txn.objectStore("logs");
+            objStore.add(this._generateLogEntry(lines));
+            let lastModStore = txn.objectStore("logslastmod");
+            lastModStore.put(this._generateLastModifiedTime());
+            txn.oncomplete = (event) => {
+                resolve();
+            };
+            txn.onerror = (event) => {
+                console.error(
+                    "Failed to flush logs : ", event
+                );
+                reject(
+                    new Error("Failed to write logs: " + event.target.errorCode)
+                );
+            }
+        });
+        return this.flushPromise;
+    }
+
+    /**
+     * Consume the most recent logs and return them. Older logs which are not
+     * returned are deleted at the same time, so this can be called at startup
+     * to do house-keeping to keep the logs from growing too large.
+     *
+     * @return {Promise<Object[]>} Resolves to an array of objects. The array is
+     * sorted in time (oldest first) based on when the log file was created (the
+     * log ID). The objects have said log ID in an "id" field and "lines" which
+     * is a big string with all the new-line delimited logs.
+     */
+    async consume() {
+        const MAX_LOG_SIZE = 1024 * 1024 * 50; // 50 MB
+        const db = this.db;
+
+        // Returns: a string representing the concatenated logs for this ID.
+        function fetchLogs(id) {
+            const o = db.transaction("logs", "readonly").objectStore("logs");
+            return selectQuery(o.index("id"), IDBKeyRange.only(id),
+            (cursor) => {
+                return {
+                    lines: cursor.value.lines,
+                    index: cursor.value.index,
+                }
+            }).then((linesArray) => {
+                // We have been storing logs periodically, so string them all
+                // together *in order of index* now
+                linesArray.sort((a, b) => {
+                    return a.index - b.index;
+                })
+                return linesArray.map((l) => l.lines).join("");
+            });
+        }
+
+        // Returns: A sorted array of log IDs. (newest first)
+        function fetchLogIds() {
+            // To gather all the log IDs, query for all records in logslastmod.
+            const o = db.transaction("logslastmod", "readonly").objectStore(
+                "logslastmod"
+            );
+            return selectQuery(o, undefined, (cursor) => {
+                return {
+                    id: cursor.value.id,
+                    ts: cursor.value.ts,
+                };
+            }).then((res) => {
+                // Sort IDs by timestamp (newest first)
+                return res.sort((a, b) => {
+                    return b.ts - a.ts;
+                }).map((a) => a.id);
+            });
+        }
+
+        function deleteLogs(id) {
+            return q.Promise((resolve, reject) => {
+                const txn = db.transaction(
+                    ["logs", "logslastmod"], "readwrite"
+                );
+                const o = txn.objectStore("logs");
+                // only load the key path, not the data which may be huge
+                const query = o.index("id").openKeyCursor(IDBKeyRange.only(id));
+                query.onsuccess = (event) => {
+                    const cursor = event.target.result;
+                    if (!cursor) {
+                        return;
+                    }
+                    o.delete(cursor.primaryKey);
+                    cursor.continue();
+                }
+                txn.oncomplete = () => {
+                    resolve();
+                };
+                txn.onerror = (event) => {
+                    reject(
+                        new Error(
+                            "Failed to delete logs for " +
+                            `'${id}' : ${event.target.errorCode}`
+                        )
+                    );
+                };
+                // delete last modified entries
+                const lastModStore = txn.objectStore("logslastmod");
+                lastModStore.delete(id);
+            });
+        }
+
+        let allLogIds = await fetchLogIds();
+        let removeLogIds = [];
+        let logs = [];
+        let size = 0;
+        for (let i = 0; i < allLogIds.length; i++) {
+            let lines = await fetchLogs(allLogIds[i]);
+            logs.push({
+                lines: lines,
+                id: allLogIds[i],
+            });
+            size += lines.length;
+            if (size > MAX_LOG_SIZE) {
+                // the remaining log IDs should be removed. If we go out of
+                // bounds this is just []
+                removeLogIds = allLogIds.slice(i + 1);
+                break;
+            }
+        }
+        if (removeLogIds.length > 0) {
+            console.log("Removing logs: ", removeLogIds);
+            // Don't await this because it's non-fatal if we can't clean up
+            // logs.
+            Promise.all(removeLogIds.map((id) => deleteLogs(id))).then(() => {
+                console.log(`Removed ${removeLogIds.length} old logs.`);
+            }, (err) => {
+                console.error(err);
+            })
+        }
+        return logs;
+    }
+
+    _generateLogEntry(lines) {
+        return {
+            id: this.id,
+            lines: lines,
+            index: this.index++
+        };
+    }
+
+    _generateLastModifiedTime() {
+        return {
+            id: this.id,
+            ts: Date.now(),
+        };
+    }
+}
+
+/**
+ * Helper method to collect results from a Cursor and promiseify it.
+ * @param {ObjectStore|Index} store The store to perform openCursor on.
+ * @param {IDBKeyRange=} keyRange Optional key range to apply on the cursor.
+ * @param {Function} resultMapper A function which is repeatedly called with a
+ * Cursor.
+ * Return the data you want to keep.
+ * @return {Promise<T[]>} Resolves to an array of whatever you returned from
+ * resultMapper.
+ */
+function selectQuery(store, keyRange, resultMapper) {
+    const query = store.openCursor(keyRange);
+    return q.Promise((resolve, reject) => {
+        let results = [];
+        query.onerror = (event) => {
+            reject(new Error("Query failed: " + event.target.errorCode));
+        };
+        // collect results
+        query.onsuccess = (event) => {
+            const cursor = event.target.result;
+            if (!cursor) {
+                resolve(results);
+                return; // end of results
+            }
+            results.push(resultMapper(cursor));
+            cursor.continue();
+        }
+    });
+}
+
+
+let store = null;
+let logger = null;
+let initPromise = null;
+let bugReportEndpoint = null;
+module.exports = {
+
+    /**
+     * Configure rage shaking support for sending bug reports.
+     * Modifies globals.
+     * @return {Promise} Resolves when set up.
+     */
+    init: function() {
+        if (initPromise) {
+            return initPromise;
+        }
+        logger = new ConsoleLogger();
+        logger.monkeyPatch(window.console);
+        if (window.indexedDB) {
+            store = new IndexedDBLogStore(window.indexedDB, logger);
+            initPromise = store.connect();
+            return initPromise;
+        }
+        initPromise = Promise.resolve();
+        return initPromise;
+    },
+
+    /**
+     * Clean up old logs.
+     * @return Promise Resolves if cleaned logs.
+     */
+    cleanup: async function() {
+        if (!store) {
+            return;
+        }
+        await store.consume();
+    },
+
+    setBugReportEndpoint: function(url) {
+        bugReportEndpoint = url;
+    },
+
+    /**
+     * Send a bug report.
+     * @param {string} userText Any additional user input.
+     * @param {boolean} sendLogs True to send logs
+     * @return {Promise} Resolved when the bug report is sent.
+     */
+    sendBugReport: async function(userText, sendLogs) {
+        if (!logger) {
+            throw new Error(
+                "No console logger, did you forget to call init()?"
+            );
+        }
+        if (!bugReportEndpoint) {
+            throw new Error("No bug report endpoint has been set.");
+        }
+
+        let version = "UNKNOWN";
+        try {
+            version = await PlatformPeg.get().getAppVersion();
+        }
+        catch (err) {} // PlatformPeg already logs this.
+
+        let userAgent = "UNKNOWN";
+        if (window.navigator && window.navigator.userAgent) {
+            userAgent = window.navigator.userAgent;
+        }
+
+        // If in incognito mode, store is null, but we still want bug report
+        // sending to work going off the in-memory console logs.
+        console.log("Sending bug report.");
+        let logs = [];
+        if (sendLogs) {
+            if (store) {
+                // flush most recent logs
+                await store.flush();
+                logs = await store.consume();
+            }
+            else {
+                logs.push({
+                    lines: logger.flush(true),
+                    id: "-",
+                });
+            }
+        }
+
+        await q.Promise((resolve, reject) => {
+            request({
+                method: "POST",
+                url: bugReportEndpoint,
+                body: {
+                    logs: logs,
+                    text: (
+                        userText || "User did not supply any additional text."
+                    ),
+                    version: version,
+                    user_agent: userAgent,
+                },
+                json: true,
+            }, (err, res) => {
+                if (err) {
+                    reject(err);
+                    return;
+                }
+                if (res.status < 200 || res.status >= 400) {
+                    reject(new Error(`HTTP ${res.status}`));
+                    return;
+                }
+                resolve();
+            })
+        });
+    }
+};
diff --git a/test/.eslintrc.js b/test/.eslintrc.js
new file mode 100644
index 00000000..4cc4659d
--- /dev/null
+++ b/test/.eslintrc.js
@@ -0,0 +1,5 @@
+module.exports = {
+    env: {
+        mocha: true,
+    },
+}
diff --git a/test/app-tests/joining.js b/test/app-tests/joining.js
index 989effa3..442b67c2 100644
--- a/test/app-tests/joining.js
+++ b/test/app-tests/joining.js
@@ -77,6 +77,7 @@ describe('joining a room', function () {
             httpBackend.when('POST', '/filter').respond(200, { filter_id: 'fid' });
             httpBackend.when('GET', '/sync').respond(200, {});
             httpBackend.when('POST', '/publicRooms').respond(200, {chunk: []});
+            httpBackend.when('GET', '/thirdparty/protocols').respond(200, {});
             httpBackend.when('GET', '/directory/room/'+encodeURIComponent(ROOM_ALIAS)).respond(200, { room_id: ROOM_ID });
 
             // start with a logged-in client
@@ -132,6 +133,12 @@ describe('joining a room', function () {
                 httpBackend.when('POST', '/join/'+encodeURIComponent(ROOM_ALIAS))
                     .respond(200, {room_id: ROOM_ID});
                 return httpBackend.flush();
+            }).then(() => {
+                // wait for the join request to be made
+                return q.delay(1);
+            }).then(() => {
+                // flush it through
+                return httpBackend.flush();
             }).then(() => {
                 httpBackend.verifyNoOutstandingExpectation();
 
diff --git a/test/app-tests/loading.js b/test/app-tests/loading.js
index 892ba852..4504d00b 100644
--- a/test/app-tests/loading.js
+++ b/test/app-tests/loading.js
@@ -16,6 +16,8 @@ limitations under the License.
 
 /* loading.js: test the myriad paths we have for loading the application */
 
+import 'skin-sdk';
+
 import React from 'react';
 import ReactDOM from 'react-dom';
 import ReactTestUtils from 'react-addons-test-utils';
@@ -92,13 +94,13 @@ describe('loading:', function () {
         loadCompletePromise = loadCompleteDefer.promise;
 
         function onNewScreen(screen) {
-            console.log("newscreen "+screen);
+            console.log(Date.now() + " newscreen "+screen);
             if (!appLoaded) {
                 lastLoadedScreen = screen;
             } else {
                 var hash = '#/' + screen;
                 windowLocation.hash = hash;
-                console.log("browser URI now "+ windowLocation);
+                console.log(Date.now() + " browser URI now "+ windowLocation);
             }
         }
 
@@ -120,22 +122,22 @@ describe('loading:', function () {
         );
 
         function routeUrl(location, matrixChat) {
-            console.log("Routing URL "+location);
+            console.log(Date.now() + " Routing URL "+location);
             var fragparts = parseQsFromFragment(location);
             matrixChat.showScreen(fragparts.location.substring(1),
                                   fragparts.params);
         }
 
         // pause for a cycle, then simulate the window.onload handler
-        q.delay(0).then(() => {
-            console.log("simulating window.onload");
+        window.setTimeout(() => {
+            console.log(Date.now() + " simulating window.onload");
             routeUrl(windowLocation, matrixChat);
             appLoaded = true;
             if (lastLoadedScreen) {
                 onNewScreen(lastLoadedScreen);
                 lastLoadedScreen = null;
             }
-        }).done();
+        }, 0);
     }
 
     describe("Clean load with no stored credentials:", function() {
@@ -152,6 +154,9 @@ describe('loading:', function () {
                 }).respond(403, "Guest access is disabled");
 
                 return httpBackend.flush();
+            }).then(() => {
+                // Wait for another trip around the event loop for the UI to update
+                return q.delay(1);
             }).then(() => {
                 // we expect a single <Login> component
                 ReactTestUtils.findRenderedComponentWithType(
@@ -175,6 +180,9 @@ describe('loading:', function () {
                 }).respond(403, "Guest access is disabled");
 
                 return httpBackend.flush();
+            }).then(() => {
+                // Wait for another trip around the event loop for the UI to update
+                return q.delay(1);
             }).then(() => {
                 // we expect a single <Login> component
                 let login = ReactTestUtils.findRenderedComponentWithType(
@@ -189,6 +197,9 @@ describe('loading:', function () {
                 });
                 login.onPasswordLogin("user", "pass")
                 return httpBackend.flush();
+            }).then(() => {
+                // Wait for another trip around the event loop for the UI to update
+                return q.delay(1);
             }).then(() => {
                 // we expect a spinner
                 ReactTestUtils.findRenderedComponentWithType(
@@ -200,9 +211,9 @@ describe('loading:', function () {
                 return httpBackend.flush();
             }).then(() => {
                 // once the sync completes, we should have a room view
+                return awaitRoomView(matrixChat);
+            }).then(() => {
                 httpBackend.verifyNoOutstandingExpectation();
-                ReactTestUtils.findRenderedComponentWithType(
-                    matrixChat, sdk.getComponent('structures.RoomView'));
                 expect(windowLocation.hash).toEqual("#/room/!room:id");
 
                 // and the localstorage should have been updated
@@ -229,10 +240,8 @@ describe('loading:', function () {
 
             loadApp();
 
-            q.delay(1).then(() => {
-                // we expect a spinner
-                assertAtSyncingSpinner(matrixChat);
-
+            return awaitSyncingSpinner(matrixChat).then(() => {
+                // we got a sync spinner - let the sync complete
                 return httpBackend.flush();
             }).then(() => {
                 // once the sync completes, we should have a directory
@@ -252,16 +261,14 @@ describe('loading:', function () {
                 uriFragment: "#/room/!room:id",
             });
 
-            q.delay(1).then(() => {
-                // we expect a spinner
-                assertAtSyncingSpinner(matrixChat);
-
+            return awaitSyncingSpinner(matrixChat).then(() => {
+                // we got a sync spinner - let the sync complete
                 return httpBackend.flush();
             }).then(() => {
                 // once the sync completes, we should have a room view
+                return awaitRoomView(matrixChat);
+            }).then(() => {
                 httpBackend.verifyNoOutstandingExpectation();
-                ReactTestUtils.findRenderedComponentWithType(
-                    matrixChat, sdk.getComponent('structures.RoomView'));
                 expect(windowLocation.hash).toEqual("#/room/!room:id");
             }).done(done, done);
 
@@ -286,9 +293,9 @@ describe('loading:', function () {
 
                 return httpBackend.flush();
             }).then(() => {
-                // now we should have a spinner with a logout link
-                assertAtSyncingSpinner(matrixChat);
-
+                return awaitSyncingSpinner(matrixChat);
+            }).then(() => {
+                // we got a sync spinner - let the sync complete
                 httpBackend.when('GET', '/sync').respond(200, {});
                 return httpBackend.flush();
             }).then(() => {
@@ -321,9 +328,8 @@ describe('loading:', function () {
 
                 return httpBackend.flush();
             }).then(() => {
-                // now we should have a spinner with a logout link
-                assertAtSyncingSpinner(matrixChat);
-
+                return awaitSyncingSpinner(matrixChat);
+            }).then(() => {
                 httpBackend.when('GET', '/sync').check(function(req) {
                     expect(req.path).toMatch(new RegExp("^https://homeserver/"));
                 }).respond(200, {});
@@ -357,16 +363,15 @@ describe('loading:', function () {
 
                 return httpBackend.flush();
             }).then(() => {
-                // now we should have a spinner with a logout link
-                assertAtSyncingSpinner(matrixChat);
-
+                return awaitSyncingSpinner(matrixChat);
+            }).then(() => {
                 httpBackend.when('GET', '/sync').respond(200, {});
                 return httpBackend.flush();
             }).then(() => {
                 // once the sync completes, we should have a room view
+                return awaitRoomView(matrixChat);
+            }).then(() => {
                 httpBackend.verifyNoOutstandingExpectation();
-                ReactTestUtils.findRenderedComponentWithType(
-                    matrixChat, sdk.getComponent('structures.RoomView'));
                 expect(windowLocation.hash).toEqual("#/room/!room:id");
             }).done(done, done);
         });
@@ -422,6 +427,32 @@ function assertAtLoadingSpinner(matrixChat) {
 
 // we've got login creds, and are waiting for the sync to finish.
 // the page includes a logout link.
+function awaitSyncingSpinner(matrixChat, retryLimit, retryCount) {
+    if (retryLimit === undefined) {
+        retryLimit = 5;
+    }
+    if (retryCount === undefined) {
+        retryCount = 0;
+    }
+
+    if (matrixChat.state.loading) {
+        console.log(Date.now() + " Awaiting sync spinner: still loading.");
+        if (retryCount >= retryLimit) {
+            throw new Error("MatrixChat still not loaded after " +
+                            retryCount + " tries");
+        }
+        return q.delay(0).then(() => {
+            return awaitSyncingSpinner(matrixChat, retryLimit, retryCount + 1);
+        });
+    }
+
+    console.log(Date.now() + " Awaiting sync spinner: load complete.");
+
+    // state looks good, check the rendered output
+    assertAtSyncingSpinner(matrixChat);
+    return q();
+}
+
 function assertAtSyncingSpinner(matrixChat) {
     var domComponent = ReactDOM.findDOMNode(matrixChat);
     expect(domComponent.className).toEqual("mx_MatrixChat_splash");
@@ -432,3 +463,30 @@ function assertAtSyncingSpinner(matrixChat) {
         matrixChat, 'a');
     expect(logoutLink.text).toEqual("Logout");
 }
+
+function awaitRoomView(matrixChat, retryLimit, retryCount) {
+    if (retryLimit === undefined) {
+        retryLimit = 5;
+    }
+    if (retryCount === undefined) {
+        retryCount = 0;
+    }
+
+    if (!matrixChat.state.ready) {
+        console.log(Date.now() + " Awaiting room view: not ready yet.");
+        if (retryCount >= retryLimit) {
+            throw new Error("MatrixChat still not ready after " +
+                            retryCount + " tries");
+        }
+        return q.delay(0).then(() => {
+            return awaitRoomView(matrixChat, retryLimit, retryCount + 1);
+        });
+    }
+
+    console.log(Date.now() + " Awaiting room view: now ready.");
+
+    // state looks good, check the rendered output
+    ReactTestUtils.findRenderedComponentWithType(
+        matrixChat, sdk.getComponent('structures.RoomView'));
+    return q();
+}
diff --git a/webpack.config.js b/webpack.config.js
index 4f350dd4..136e0af7 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -14,7 +14,11 @@ module.exports = {
         // (we should probably make js-sdk load it asynchronously at some
         // point, so that it doesn't block the pageload, but that is a separate
         // problem)
-        "olm": "olm/olm.js",
+        "olm": "./src/vector/olm-loader.js",
+
+        // CSS themes
+        "theme-light": "./src/skins/vector/css/themes/light.scss",
+        "theme-dark": "./src/skins/vector/css/themes/dark.scss"
     },
     module: {
         preLoaders: [
@@ -23,8 +27,25 @@ module.exports = {
         loaders: [
             { test: /\.json$/, loader: "json" },
             { test: /\.js$/, loader: "babel", include: path.resolve('./src') },
-            // css-raw-loader loads CSS but doesn't try to treat url()s as require()s
-            { test: /\.css$/, loader: ExtractTextPlugin.extract("css-raw-loader") },
+            {
+                test: /\.scss$/,
+
+                // 1. postcss-loader turns the SCSS into normal CSS.
+                // 2. css-raw-loader turns the CSS into a javascript module
+                //    whose default export is a string containing the CSS.
+                //    (css-raw-loader is similar to css-loader, but the latter
+                //    would also drag in the imgs and fonts that our CSS refers to
+                //    as webpack inputs.)
+                // 3. ExtractTextPlugin turns that string into a separate asset.
+                loader: ExtractTextPlugin.extract(
+                    "css-raw-loader!postcss-loader?config=postcss.config.js"
+                ),
+            },
+            {
+                // this works similarly to the scss case, without postcss.
+                test: /\.css$/,
+                loader: ExtractTextPlugin.extract("css-raw-loader"),
+            },
         ],
         noParse: [
             // don't parse the languages within highlight.js. They cause stack
@@ -40,7 +61,17 @@ module.exports = {
     },
     output: {
         path: path.join(__dirname, "webapp"),
-        filename: "[name].[chunkhash].js",
+
+        // the generated js (and CSS, from the ExtractTextPlugin) are put in a
+        // unique subdirectory for the build. There will only be one such
+        // 'bundle' directory in the generated tarball; however, hosting
+        // servers can collect 'bundles' from multiple versions into one
+        // directory and symlink it into place - this allows users who loaded
+        // an older version of the application to continue to access webpack
+        // chunks even after the app is redeployed.
+        //
+        filename: "bundles/[hash]/[name].js",
+        chunkFilename: "bundles/[hash]/[name].js",
         devtoolModuleFilenameTemplate: function(info) {
             // Reading input source maps gives only relative paths here for
             // everything. Until I figure out how to fix this, this is a
@@ -57,7 +88,8 @@ module.exports = {
         alias: {
             // alias any requires to the react module to the one in our path, otherwise
             // we tend to get the react source included twice when using npm link.
-            react: path.resolve('./node_modules/react'),
+            "react": path.resolve('./node_modules/react'),
+            "react-dom": path.resolve('./node_modules/react-dom'),
             "react-addons-perf": path.resolve('./node_modules/react-addons-perf'),
 
             // same goes for js-sdk
@@ -78,7 +110,7 @@ module.exports = {
         }),
 
         new ExtractTextPlugin(
-            "[name].[contenthash].css",
+            "bundles/[hash]/[name].css",
             {
                 allChunks: true
             }