diff --git a/src/PasswordReset.js b/src/PasswordReset.js
new file mode 100644
index 0000000000000000000000000000000000000000..1029b07b709a0214788abb971be9b366e3822fce
--- /dev/null
+++ b/src/PasswordReset.js
@@ -0,0 +1,104 @@
+/*
+Copyright 2015, 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.
+*/
+
+var Matrix = require("matrix-js-sdk");
+
+/**
+ * Allows a user to reset their password on a homeserver.
+ *
+ * This involves getting an email token from the identity server to "prove" that
+ * the client owns the given email address, which is then passed to the password
+ * API on the homeserver in question with the new password.
+ */
+class PasswordReset {
+
+    /**
+     * Configure the endpoints for password resetting.
+     * @param {string} homeserverUrl The URL to the HS which has the account to reset.
+     * @param {string} identityUrl The URL to the IS which has linked the email -> mxid mapping.
+     */
+    constructor(homeserverUrl, identityUrl) {
+        this.client = Matrix.createClient({
+            baseUrl: homeserverUrl,
+            idBaseUrl: identityUrl
+        });
+        this.clientSecret = generateClientSecret();
+        this.identityServerDomain = identityUrl.split("://")[1];
+    }
+
+    /**
+     * Attempt to reset the user's password. This will trigger a side-effect of
+     * sending an email to the provided email address.
+     * @param {string} emailAddress The email address
+     * @param {string} newPassword The new password for the account.
+     * @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked().
+     */
+    resetPassword(emailAddress, newPassword) {
+        this.password = newPassword;
+        return this.client.requestEmailToken(emailAddress, this.clientSecret, 1).then((res) => {
+            this.sessionId = res.sid;
+            return res;
+        }, function(err) {
+            if (err.httpStatus) {
+                err.message = err.message + ` (Status ${err.httpStatus})`;
+            }
+            throw err;
+        });
+    }
+
+    /**
+     * Checks if the email link has been clicked by attempting to change the password
+     * for the mxid linked to the email.
+     * @return {Promise} Resolves if the password was reset. Rejects with an object
+     * with a "message" property which contains a human-readable message detailing why
+     * the reset failed, e.g. "There is no mapped matrix user ID for the given email address".
+     */
+    checkEmailLinkClicked() {
+        return this.client.setPassword({
+            type: "m.login.email.identity",
+            threepid_creds: {
+                sid: this.sessionId,
+                client_secret: this.clientSecret,
+                id_server: this.identityServerDomain
+            }
+        }, this.password).catch(function(err) {
+            if (err.httpStatus === 401) {
+                err.message = "Failed to verify email address: make sure you clicked the link in the email";
+            }
+            else if (err.httpStatus === 404) {
+                err.message = "Your email address does not appear to be associated with a Matrix ID on this Homeserver.";
+            }
+            else if (err.httpStatus) {
+                err.message += ` (Status ${err.httpStatus})`;
+            }
+            throw err;
+        });
+    }
+}
+
+// from Angular SDK
+function generateClientSecret() {
+    var ret = "";
+    var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+
+    for (var i = 0; i < 32; i++) {
+        ret += chars.charAt(Math.floor(Math.random() * chars.length));
+    }
+
+    return ret;
+}
+
+module.exports = PasswordReset;
diff --git a/src/SlashCommands.js b/src/SlashCommands.js
index 363560f0c6073cd7e273debd20bb9704cb5ee3e7..1dd7ecb08fecbaf6fa663eb61da7cde7e0e94cb7 100644
--- a/src/SlashCommands.js
+++ b/src/SlashCommands.js
@@ -43,11 +43,27 @@ var commands = {
         return reject("Usage: /nick <display_name>");
     },
 
-    // Takes an #rrggbb colourcode and retints the UI (just for debugging)
+    // Changes the colorscheme of your current room
     tint: function(room_id, args) {
-        Tinter.tint(args);
-        return success();
-    },
+
+        if (args) {
+            var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/);
+            if (matches) {
+                Tinter.tint(matches[1], matches[4]);
+                var colorScheme = {}
+                colorScheme.primary_color = matches[1];
+                if (matches[4]) {
+                    colorScheme.secondary_color = matches[4];
+                }
+                return success(
+                    MatrixClientPeg.get().setRoomAccountData(
+                        room_id, "org.matrix.room.color_scheme", colorScheme
+                    )                    
+                );
+            }
+        }
+        return reject("Usage: /tint <primaryColor> [<secondaryColor>]");
+   },
 
     encrypt: function(room_id, args) {
         if (args == "on") {
diff --git a/src/Tinter.js b/src/Tinter.js
index 7245a5825b11a8a753491864cf0e1fcb2ba98275..3e7949b65dd5d7c0f64f8ba9c2762f04e942cea4 100644
--- a/src/Tinter.js
+++ b/src/Tinter.js
@@ -127,6 +127,11 @@ module.exports = {
             cached = true;
         }
 
+        if (!primaryColor) {
+            primaryColor = "#76CFA6"; // Vector green
+            secondaryColor = "#EAF5F0"; // Vector light green
+        }
+
         if (!secondaryColor) {
             var x = 0.16; // average weighting factor calculated from vector green & light green
             var rgb = hexToRgb(primaryColor);
@@ -146,6 +151,13 @@ module.exports = {
             tertiaryColor = rgbToHex(rgb1);
         }
 
+        if (colors[0] === primaryColor &&
+            colors[1] === secondaryColor &&
+            colors[2] === tertiaryColor)
+        {
+            return;
+        }
+
         colors = [primaryColor, secondaryColor, tertiaryColor];
 
         // go through manually fixing up the stylesheets.
diff --git a/src/UserActivity.js b/src/UserActivity.js
index 3048ad4454792f9bc3f4d0fc5438bd4dee867399..8b136c0bcc1d6796184d6607a992d6cebf5ac1da 100644
--- a/src/UserActivity.js
+++ b/src/UserActivity.js
@@ -31,6 +31,11 @@ class UserActivity {
     start() {
         document.onmousemove = this._onUserActivity.bind(this);
         document.onkeypress = this._onUserActivity.bind(this);
+        // can't use document.scroll here because that's only the document
+        // itself being scrolled. Need to use addEventListener's useCapture.
+        // also this needs to be the wheel event, not scroll, as scroll is
+        // fired when the view scrolls down for a new message.
+        window.addEventListener('wheel', this._onUserActivity.bind(this), true);
         this.lastActivityAtTs = new Date().getTime();
         this.lastDispatchAtTs = 0;
     }
@@ -41,10 +46,11 @@ class UserActivity {
     stop() {
         document.onmousemove = undefined;
         document.onkeypress = undefined;
+        window.removeEventListener('wheel', this._onUserActivity.bind(this), true);
     }
 
     _onUserActivity(event) {
-        if (event.screenX) {
+        if (event.screenX && event.type == "mousemove") {
             if (event.screenX === this.lastScreenX &&
                 event.screenY === this.lastScreenY)
             {
diff --git a/src/component-index.js b/src/component-index.js
index cef1b093a4b7d04b636ad4a6f63a8cce23ca8ae5..9fe15adfc62b6cbc8d9c03dfa40e4ad73bf5adb0 100644
--- a/src/component-index.js
+++ b/src/component-index.js
@@ -23,6 +23,7 @@ limitations under the License.
 
 module.exports.components = {};
 module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom');
+module.exports.components['structures.login.ForgotPassword'] = require('./components/structures/login/ForgotPassword');
 module.exports.components['structures.login.Login'] = require('./components/structures/login/Login');
 module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration');
 module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration');
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index 320dad09b3df8c8db194ac1489a62909594099b7..e5af2a86b550686c5d953e0fc6538210caf5bf78 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -30,6 +30,7 @@ var Registration = require("./login/Registration");
 var PostRegistration = require("./login/PostRegistration");
 
 var Modal = require("../../Modal");
+var Tinter = require("../../Tinter");
 var sdk = require('../../index');
 var MatrixTools = require('../../MatrixTools');
 var linkifyMatrix = require("../../linkify-matrix");
@@ -233,6 +234,13 @@ module.exports = React.createClass({
                 });
                 this.notifyNewScreen('register');
                 break;
+            case 'start_password_recovery':
+                if (this.state.logged_in) return;
+                this.replaceState({
+                    screen: 'forgot_password'
+                });
+                this.notifyNewScreen('forgot_password');
+                break;
             case 'token_login':
                 if (this.state.logged_in) return;
 
@@ -411,7 +419,16 @@ module.exports = React.createClass({
             if (room) {
                 var theAlias = MatrixTools.getCanonicalAliasForRoom(room);
                 if (theAlias) presentedId = theAlias;
+
+                var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme");
+                var color_scheme = {};
+                if (color_scheme_event) {
+                    color_scheme = color_scheme_event.getContent();
+                    // XXX: we should validate the event
+                }                
+                Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
             }
+
             this.notifyNewScreen('room/'+presentedId);
             newState.ready = true;
         }
@@ -559,6 +576,11 @@ module.exports = React.createClass({
                 action: 'token_login',
                 params: params
             });
+        } else if (screen == 'forgot_password') {
+            dis.dispatch({
+                action: 'start_password_recovery',
+                params: params
+            });
         } else if (screen == 'new') {
             dis.dispatch({
                 action: 'view_create_room',
@@ -668,6 +690,10 @@ module.exports = React.createClass({
         this.showScreen("login");
     },
 
+    onForgotPasswordClick: function() {
+        this.showScreen("forgot_password");
+    },
+
     onRegistered: function(credentials) {
         this.onLoggedIn(credentials);
         // do post-registration stuff
@@ -706,6 +732,7 @@ module.exports = React.createClass({
         var CreateRoom = sdk.getComponent('structures.CreateRoom');
         var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
         var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
+        var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
 
         // needs to be before normal PageTypes as you are logged in technically
         if (this.state.screen == 'post_registration') {
@@ -801,13 +828,21 @@ module.exports = React.createClass({
                     onLoggedIn={this.onRegistered}
                     onLoginClick={this.onLoginClick} />
             );
+        } else if (this.state.screen == 'forgot_password') {
+            return (
+                <ForgotPassword
+                    homeserverUrl={this.props.config.default_hs_url}
+                    identityServerUrl={this.props.config.default_is_url}
+                    onComplete={this.onLoginClick} />
+            );
         } else {
             return (
                 <Login
                     onLoggedIn={this.onLoggedIn}
                     onRegisterClick={this.onRegisterClick}
                     homeserverUrl={this.props.config.default_hs_url}
-                    identityServerUrl={this.props.config.default_is_url} />
+                    identityServerUrl={this.props.config.default_is_url}
+                    onForgotPasswordClick={this.onForgotPasswordClick} />
             );
         }
     }
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 7a729a4436c86266b9b18ccb243d53dd19ba4d06..ddde9c2645b00e6b0b9f139ef52b89e871eb4999 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -37,9 +37,11 @@ var TabComplete = require("../../TabComplete");
 var MemberEntry = require("../../TabCompleteEntries").MemberEntry;
 var Resend = require("../../Resend");
 var dis = require("../../dispatcher");
+var Tinter = require("../../Tinter");
 
 var PAGINATE_SIZE = 20;
 var INITIAL_SIZE = 20;
+var SEND_READ_RECEIPT_DELAY = 2000;
 
 var DEBUG_SCROLL = false;
 
@@ -74,7 +76,9 @@ module.exports = React.createClass({
             syncState: MatrixClientPeg.get().getSyncState(),
             hasUnsentMessages: this._hasUnsentMessages(room),
             callState: null,
-            guestsCanJoin: false
+            guestsCanJoin: false,
+            readMarkerEventId: room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId),
+            readMarkerGhostEventId: undefined
         }
     },
 
@@ -82,6 +86,7 @@ module.exports = React.createClass({
         this.dispatcherRef = dis.register(this.onAction);
         MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
         MatrixClientPeg.get().on("Room.name", this.onRoomName);
+        MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData);
         MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
         MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
         MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
@@ -152,6 +157,7 @@ module.exports = React.createClass({
         if (MatrixClientPeg.get()) {
             MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
             MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
+            MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData);
             MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
             MatrixClientPeg.get().removeListener("RoomMember.typing", this.onRoomMemberTyping);
             MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
@@ -159,6 +165,8 @@ module.exports = React.createClass({
         }
 
         window.removeEventListener('resize', this.onResize);        
+
+        Tinter.tint(); // reset colourscheme
     },
 
     onAction: function(payload) {
@@ -272,9 +280,58 @@ module.exports = React.createClass({
         }
     },
 
+    updateTint: function() {
+        var room = MatrixClientPeg.get().getRoom(this.props.roomId);
+        if (!room) return;
+
+        var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme");
+        var color_scheme = {};
+        if (color_scheme_event) {
+            color_scheme = color_scheme_event.getContent();
+            // XXX: we should validate the event
+        }                
+        Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
+    },
+
+    onRoomAccountData: function(room, event) {
+        if (room.roomId == this.props.roomId) {
+            if (event.getType === "org.matrix.room.color_scheme") {
+                var color_scheme = event.getContent();
+                // XXX: we should validate the event
+                Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
+            }
+        }
+    },
+
     onRoomReceipt: function(receiptEvent, room) {
         if (room.roomId == this.props.roomId) {
-            this.forceUpdate();
+            var readMarkerEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId);
+            var readMarkerGhostEventId = this.state.readMarkerGhostEventId;
+            if (this.state.readMarkerEventId !== undefined && this.state.readMarkerEventId != readMarkerEventId) {
+                readMarkerGhostEventId = this.state.readMarkerEventId;
+            }
+
+
+            // if the event after the one referenced in the read receipt if sent by us, do nothing since
+            // this is a temporary period before the synthesized receipt for our own message arrives
+            var readMarkerGhostEventIndex;
+            for (var i = 0; i < room.timeline.length; ++i) {
+                if (room.timeline[i].getId() == readMarkerGhostEventId) {
+                    readMarkerGhostEventIndex = i;
+                    break;
+                }
+            }
+            if (readMarkerGhostEventIndex + 1 < room.timeline.length) {
+                var nextEvent = room.timeline[readMarkerGhostEventIndex + 1];
+                if (nextEvent.sender && nextEvent.sender.userId == MatrixClientPeg.get().credentials.userId) {
+                    readMarkerGhostEventId = undefined;
+                }
+            }
+
+            this.setState({
+                readMarkerEventId: readMarkerEventId,
+                readMarkerGhostEventId: readMarkerGhostEventId,
+            });
         }
     },
 
@@ -374,6 +431,8 @@ module.exports = React.createClass({
 
         this.scrollToBottom();
         this.sendReadReceipt();
+
+        this.updateTint();
     },
 
     componentDidUpdate: function() {
@@ -695,10 +754,10 @@ module.exports = React.createClass({
 
         var EventTile = sdk.getComponent('rooms.EventTile');
 
-
         var prevEvent = null; // the last event we showed
-        var readReceiptEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId);
         var startIdx = Math.max(0, this.state.room.timeline.length - this.state.messageCap);
+        var readMarkerIndex;
+        var ghostIndex;
         for (var i = startIdx; i < this.state.room.timeline.length; i++) {
             var mxEv = this.state.room.timeline[i];
 
@@ -712,6 +771,25 @@ module.exports = React.createClass({
                 }
             }
 
+            // now we've decided whether or not to show this message,
+            // add the read up to marker if appropriate
+            // doing this here means we implicitly do not show the marker
+            // if it's at the bottom
+            // NB. it would be better to decide where the read marker was going
+            // when the state changed rather than here in the render method, but
+            // this is where we decide what messages we show so it's the only
+            // place we know whether we're at the bottom or not.
+            var self = this;
+            var mxEvSender = mxEv.sender ? mxEv.sender.userId : null;
+            if (prevEvent && prevEvent.getId() == this.state.readMarkerEventId && mxEvSender != MatrixClientPeg.get().credentials.userId) {
+                var hr;
+                hr = (<hr className="mx_RoomView_myReadMarker" style={{opacity: 1, width: '99%'}} ref={function(n) {
+                    self.readMarkerNode = n;
+                }} />);
+                readMarkerIndex = ret.length;
+                ret.push(<li key="_readupto" className="mx_RoomView_myReadMarker_container">{hr}</li>);
+            }
+
             // is this a continuation of the previous message?
             var continuation = false;
             if (prevEvent !== null) {
@@ -748,13 +826,29 @@ module.exports = React.createClass({
                 </li>
             );
 
-            if (eventId == readReceiptEventId) {
-                ret.push(<hr className="mx_RoomView_myReadMarker" />);
+            // A read up to marker has died and returned as a ghost!
+            // Lives in the dom as the ghost of the previous one while it fades away
+            if (eventId == this.state.readMarkerGhostEventId) {
+                ghostIndex = ret.length;
             }
 
             prevEvent = mxEv;
         }
 
+        // splice the read marker ghost in now that we know whether the read receipt
+        // is the last element or not, because we only decide as we're going along.
+        if (readMarkerIndex === undefined && ghostIndex && ghostIndex <= ret.length) {
+            var hr;
+            hr = (<hr className="mx_RoomView_myReadMarker" style={{opacity: 1, width: '85%'}} ref={function(n) {
+                Velocity(n, {opacity: '0', width: '10%'}, {duration: 400, easing: 'easeInSine', delay: 1000, complete: function() {
+                    self.setState({readMarkerGhostEventId: undefined});
+                }});
+            }} />);
+            ret.splice(ghostIndex, 0, (
+                <li key="_readuptoghost" className="mx_RoomView_myReadMarker_container">{hr}</li>
+            ));
+        }
+
         return ret;
     },
 
@@ -825,6 +919,14 @@ module.exports = React.createClass({
             );
         }
 
+        if (newVals.color_scheme) {
+            deferreds.push(
+                MatrixClientPeg.get().setRoomAccountData(
+                    this.state.room.roomId, "org.matrix.room.color_scheme", newVals.color_scheme
+                )
+            );
+        }
+
         deferreds.push(
             MatrixClientPeg.get().setGuestAccess(this.state.room.roomId, {
                 allowRead: newVals.guest_read,
@@ -923,11 +1025,13 @@ module.exports = React.createClass({
             history_visibility: this.refs.room_settings.getHistoryVisibility(),
             power_levels: this.refs.room_settings.getPowerLevels(),
             guest_join: this.refs.room_settings.canGuestsJoin(),
-            guest_read: this.refs.room_settings.canGuestsRead()
+            guest_read: this.refs.room_settings.canGuestsRead(),
+            color_scheme: this.refs.room_settings.getColorScheme(),
         });
     },
 
     onCancelClick: function() {
+        this.updateTint();
         this.setState({editingRoomSettings: false});
     },
 
diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js
new file mode 100644
index 0000000000000000000000000000000000000000..dcf6a7c28e00ca747d59e8bfe1c73d402f2793d5
--- /dev/null
+++ b/src/components/structures/login/ForgotPassword.js
@@ -0,0 +1,199 @@
+/*
+Copyright 2015, 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.
+*/
+
+'use strict';
+
+var React = require('react');
+var sdk = require('../../../index');
+var Modal = require("../../../Modal");
+var MatrixClientPeg = require('../../../MatrixClientPeg');
+
+var PasswordReset = require("../../../PasswordReset");
+
+module.exports = React.createClass({
+    displayName: 'ForgotPassword',
+
+    propTypes: {
+        homeserverUrl: React.PropTypes.string,
+        identityServerUrl: React.PropTypes.string,
+        onComplete: React.PropTypes.func.isRequired
+    },
+
+    getInitialState: function() {
+        return {
+            enteredHomeserverUrl: this.props.homeserverUrl,
+            enteredIdentityServerUrl: this.props.identityServerUrl,
+            progress: null
+        };
+    },
+
+    submitPasswordReset: function(hsUrl, identityUrl, email, password) {
+        this.setState({
+            progress: "sending_email"
+        });
+        this.reset = new PasswordReset(hsUrl, identityUrl);
+        this.reset.resetPassword(email, password).done(() => {
+            this.setState({
+                progress: "sent_email"
+            });
+        }, (err) => {
+            this.showErrorDialog("Failed to send email: " + err.message);
+            this.setState({
+                progress: null
+            });
+        })
+    },
+
+    onVerify: function(ev) {
+        ev.preventDefault();
+        if (!this.reset) {
+            console.error("onVerify called before submitPasswordReset!");
+            return;
+        }
+        this.reset.checkEmailLinkClicked().done((res) => {
+            this.setState({ progress: "complete" });
+        }, (err) => {
+            this.showErrorDialog(err.message);
+        })
+    },
+
+    onSubmitForm: function(ev) {
+        ev.preventDefault();
+
+        if (!this.state.email) {
+            this.showErrorDialog("The email address linked to your account must be entered.");
+        }
+        else if (!this.state.password || !this.state.password2) {
+            this.showErrorDialog("A new password must be entered.");
+        }
+        else if (this.state.password !== this.state.password2) {
+            this.showErrorDialog("New passwords must match each other.");
+        }
+        else {
+            this.submitPasswordReset(
+                this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl,
+                this.state.email, this.state.password
+            );
+        }
+    },
+
+    onInputChanged: function(stateKey, ev) {
+        this.setState({
+            [stateKey]: ev.target.value
+        });
+    },
+
+    onHsUrlChanged: function(newHsUrl) {
+        this.setState({
+            enteredHomeserverUrl: newHsUrl
+        });
+    },
+
+    onIsUrlChanged: function(newIsUrl) {
+        this.setState({
+            enteredIdentityServerUrl: newIsUrl
+        });
+    },
+
+    showErrorDialog: function(body, title) {
+        var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+        Modal.createDialog(ErrorDialog, {
+            title: title,
+            description: body
+        });
+    },
+
+    render: function() {
+        var LoginHeader = sdk.getComponent("login.LoginHeader");
+        var LoginFooter = sdk.getComponent("login.LoginFooter");
+        var ServerConfig = sdk.getComponent("login.ServerConfig");
+        var Spinner = sdk.getComponent("elements.Spinner");
+
+        var resetPasswordJsx;
+
+        if (this.state.progress === "sending_email") {
+            resetPasswordJsx = <Spinner />
+        }
+        else if (this.state.progress === "sent_email") {
+            resetPasswordJsx = (
+                <div>
+                    An email has been sent to {this.state.email}. Once you&#39;ve followed
+                    the link it contains, click below.
+                    <br />
+                    <input className="mx_Login_submit" type="button" onClick={this.onVerify}
+                        value="I have verified my email address" />
+                </div>
+            );
+        }
+        else if (this.state.progress === "complete") {
+            resetPasswordJsx = (
+                <div>
+                    <p>Your password has been reset.</p>
+                    <p>You have been logged out of all devices and will no longer receive push notifications.
+                    To re-enable notifications, re-log in on each device.</p>
+                    <input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
+                        value="Return to login screen" />
+                </div>
+            );
+        }
+        else {
+            resetPasswordJsx = (
+            <div>
+                To reset your password, enter the email address linked to your account:
+                <br />
+                <div>
+                    <form onSubmit={this.onSubmitForm}>
+                        <input className="mx_Login_field" ref="user" type="text"
+                            value={this.state.email}
+                            onChange={this.onInputChanged.bind(this, "email")}
+                            placeholder="Email address" autoFocus />
+                        <br />
+                        <input className="mx_Login_field" ref="pass" type="password"
+                            value={this.state.password}
+                            onChange={this.onInputChanged.bind(this, "password")}
+                            placeholder="New password" />
+                        <br />
+                        <input className="mx_Login_field" ref="pass" type="password"
+                            value={this.state.password2}
+                            onChange={this.onInputChanged.bind(this, "password2")}
+                            placeholder="Confirm your new password" />
+                        <br />
+                        <input className="mx_Login_submit" type="submit" value="Send Reset Email" />
+                    </form>
+                    <ServerConfig ref="serverConfig"
+                        withToggleButton={true}
+                        defaultHsUrl={this.props.homeserverUrl}
+                        defaultIsUrl={this.props.identityServerUrl}
+                        onHsUrlChanged={this.onHsUrlChanged}
+                        onIsUrlChanged={this.onIsUrlChanged}
+                        delayTimeMs={0}/>
+                    <LoginFooter />
+                </div>
+            </div>
+            );
+        }
+
+
+        return (
+            <div className="mx_Login">
+                <div className="mx_Login_box">
+                    <LoginHeader />
+                    {resetPasswordJsx}
+                </div>
+            </div>
+        );
+    }
+});
diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js
index b7d2d762a472913021681c6fe5fad52ca932f514..b853b8fd95cc8da920b07a18626e530814221e4b 100644
--- a/src/components/structures/login/Login.js
+++ b/src/components/structures/login/Login.js
@@ -33,7 +33,9 @@ module.exports = React.createClass({displayName: 'Login',
         homeserverUrl: React.PropTypes.string,
         identityServerUrl: React.PropTypes.string,
         // login shouldn't know or care how registration is done.
-        onRegisterClick: React.PropTypes.func.isRequired
+        onRegisterClick: React.PropTypes.func.isRequired,
+        // login shouldn't care how password recovery is done.
+        onForgotPasswordClick: React.PropTypes.func
     },
 
     getDefaultProps: function() {
@@ -138,7 +140,9 @@ module.exports = React.createClass({displayName: 'Login',
         switch (step) {
             case 'm.login.password':
                 return (
-                    <PasswordLogin onSubmit={this.onPasswordLogin} />
+                    <PasswordLogin
+                        onSubmit={this.onPasswordLogin}
+                        onForgotPasswordClick={this.props.onForgotPasswordClick} />
                 );
             case 'm.login.cas':
                 return (
diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js
index 3367ac32573e6258a0689042159f8e6ae2c4c901..a8751da1a78b94330fd1006797dd919039bdc062 100644
--- a/src/components/views/login/PasswordLogin.js
+++ b/src/components/views/login/PasswordLogin.js
@@ -22,7 +22,8 @@ var ReactDOM = require('react-dom');
  */
 module.exports = React.createClass({displayName: 'PasswordLogin',
     propTypes: {
-        onSubmit: React.PropTypes.func.isRequired // fn(username, password)
+        onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
+        onForgotPasswordClick: React.PropTypes.func // fn()
     },
 
     getInitialState: function() {
@@ -46,6 +47,16 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
     },
 
     render: function() {
+        var forgotPasswordJsx;
+
+        if (this.props.onForgotPasswordClick) {
+            forgotPasswordJsx = (
+                <a className="mx_Login_forgot" onClick={this.props.onForgotPasswordClick} href="#">
+                    Forgot your password?
+                </a>
+            );
+        }
+
         return (
             <div>
                 <form onSubmit={this.onSubmitForm}>
@@ -57,6 +68,7 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
                     value={this.state.password} onChange={this.onPasswordChanged}
                     placeholder="Password" />
                 <br />
+                {forgotPasswordJsx}
                 <input className="mx_Login_submit" type="submit" value="Log in" />
                 </form>
             </div>
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js
index 8b5435e46a1255d861d003c4b318b1fb7188221d..cdfbc0bfc8729568a6a257bb59e7931742130aae 100644
--- a/src/components/views/rooms/RoomHeader.js
+++ b/src/components/views/rooms/RoomHeader.js
@@ -116,7 +116,7 @@ module.exports = React.createClass({
                 }
 
                 name =
-                    <div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
+                    <div className="mx_RoomHeader_name">
                         <div className="mx_RoomHeader_nametext" title={ this.props.room.name }>{ this.props.room.name }</div>
                         { searchStatus }
                         <div className="mx_RoomHeader_settingsButton" title="Settings">
@@ -151,7 +151,7 @@ module.exports = React.createClass({
 
             header =
                 <div className="mx_RoomHeader_wrapper">
-                    <div className="mx_RoomHeader_leftRow">
+                    <div className="mx_RoomHeader_leftRow" onClick={this.props.onSettingsClick}>
                         <div className="mx_RoomHeader_avatar">
                             { roomAvatar }
                         </div>
diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js
index 9e07385d65548505c67e0fa9a73d24cb2f6b8cc0..c5e37521c5b0c4cbe0be3a957679c5b9124393ed 100644
--- a/src/components/views/rooms/RoomSettings.js
+++ b/src/components/views/rooms/RoomSettings.js
@@ -16,8 +16,23 @@ limitations under the License.
 
 var React = require('react');
 var MatrixClientPeg = require('../../../MatrixClientPeg');
+var Tinter = require('../../../Tinter');
 var sdk = require('../../../index');
 
+var room_colors = [
+    // magic room default values courtesy of Ribot
+    ["#76cfa6", "#eaf5f0"],
+    ["#81bddb", "#eaf1f4"],
+    ["#bd79cb", "#f3eaf5"],
+    ["#c65d94", "#f5eaef"],
+    ["#e55e5e", "#f5eaea"],
+    ["#eca46f", "#f5eeea"],
+    ["#dad658", "#f5f4ea"],
+    ["#80c553", "#eef5ea"],
+    ["#bb814e", "#eee8e3"],
+    ["#595959", "#ececec"],
+];
+
 module.exports = React.createClass({
     displayName: 'RoomSettings',
 
@@ -26,8 +41,37 @@ module.exports = React.createClass({
     },
 
     getInitialState: function() {
+        // work out the initial color index
+        var room_color_index = undefined;
+        var color_scheme_event = this.props.room.getAccountData("org.matrix.room.color_scheme");
+        if (color_scheme_event) {
+            var color_scheme = color_scheme_event.getContent();
+            if (color_scheme.primary_color) color_scheme.primary_color = color_scheme.primary_color.toLowerCase();
+            if (color_scheme.secondary_color) color_scheme.secondary_color = color_scheme.secondary_color.toLowerCase();
+            // XXX: we should validate these values
+            for (var i = 0; i < room_colors.length; i++) {
+                var room_color = room_colors[i];
+                if (room_color[0] === color_scheme.primary_color &&
+                    room_color[1] === color_scheme.secondary_color)
+                {
+                    room_color_index = i;
+                    break;
+                }
+            }
+            if (room_color_index === undefined) {
+                // append the unrecognised colours to our palette
+                room_color_index = room_colors.length;
+                room_colors[room_color_index] = [ color_scheme.primary_color, color_scheme.secondary_color ];
+            }
+        }
+        else {
+            room_color_index = 0;
+        }
+
         return {
-            power_levels_changed: false
+            power_levels_changed: false,
+            color_scheme_changed: false,
+            color_scheme_index: room_color_index,
         };
     },
 
@@ -78,6 +122,25 @@ module.exports = React.createClass({
         });
     },
 
+    getColorScheme: function() {
+        if (!this.state.color_scheme_changed) return undefined;
+
+        return {
+            primary_color: room_colors[this.state.color_scheme_index][0],
+            secondary_color: room_colors[this.state.color_scheme_index][1],            
+        };
+    },
+
+    onColorSchemeChanged: function(index) {
+        // preview what the user just changed the scheme to.
+        Tinter.tint(room_colors[index][0], room_colors[index][1]);
+
+        this.setState({
+            color_scheme_changed: true,
+            color_scheme_index: index,
+        });
+    },
+
     render: function() {
         var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
 
@@ -151,16 +214,80 @@ module.exports = React.createClass({
         }
         var can_set_room_avatar = current_user_level >= room_avatar_level;
 
+        var self = this;
+
+        var room_colors_section =
+            <div>
+                <h3>Room Colour</h3>
+                <div className="mx_RoomSettings_roomColors">
+                    {room_colors.map(function(room_color, i) {
+                        var selected;
+                        if (i === self.state.color_scheme_index) {
+                            selected =
+                                <div className="mx_RoomSettings_roomColor_selected">
+                                    <img src="img/tick.svg" width="17" height="14" alt="./"/>
+                                </div>
+                        }
+                        var boundClick = self.onColorSchemeChanged.bind(self, i)
+                        return (
+                            <div className="mx_RoomSettings_roomColor"
+                                  key={ "room_color_" + i }
+                                  style={{ backgroundColor: room_color[1] }}
+                                  onClick={ boundClick }>
+                                { selected }
+                                <div className="mx_RoomSettings_roomColorPrimary" style={{ backgroundColor: room_color[0] }}></div>
+                            </div>
+                        );
+                    })}
+                </div>
+            </div>;
+
         var change_avatar;
         if (can_set_room_avatar) {
-            change_avatar = <div>
-                <h3>Room Icon</h3>
-                <ChangeAvatar room={this.props.room} />
-            </div>;
+            change_avatar =
+                <div>
+                    <h3>Room Icon</h3>
+                    <ChangeAvatar room={this.props.room} />
+                </div>;
         }
 
         var banned = this.props.room.getMembersWithMembership("ban");
 
+        var events_levels_section;
+        if (events_levels.length) {
+            events_levels_section = 
+                <div>
+                    <h3>Event levels</h3>
+                    <div className="mx_RoomSettings_eventLevels mx_RoomSettings_settings">
+                        {Object.keys(events_levels).map(function(event_type, i) {
+                            return (
+                                <div key={event_type}>
+                                    <label htmlFor={"mx_RoomSettings_event_"+i}>{event_type}</label>
+                                    <input type="text" defaultValue={events_levels[event_type]} size="3" id={"mx_RoomSettings_event_"+i} disabled/>
+                                </div>
+                            );
+                        })}
+                    </div>
+                </div>;
+        }
+
+        var banned_users_section;
+        if (banned.length) {
+            banned_users_section =
+                <div>
+                    <h3>Banned users</h3>
+                    <div className="mx_RoomSettings_banned">
+                        {banned.map(function(member, i) {
+                            return (
+                                <div key={i}>
+                                    {member.userId}
+                                </div>
+                            );
+                        })}
+                    </div>
+                </div>;
+        }
+
         return (
             <div className="mx_RoomSettings">
                 <textarea className="mx_RoomSettings_description" placeholder="Topic" defaultValue={topic} ref="topic"/> <br/>
@@ -174,10 +301,13 @@ module.exports = React.createClass({
                     <input type="checkbox" ref="guests_join" defaultChecked={guest_access === "can_join"}/>
                     Allow guests to join this room
                 </label> <br/>
-                <label className="mx_RoomSettings_encrypt"><input type="checkbox" /> Encrypt room</label> <br/>
+                <label className="mx_RoomSettings_encrypt"><input type="checkbox" /> Encrypt room</label>
+
+                { room_colors_section }
+
 
                 <h3>Power levels</h3>
-                <div className="mx_RoomSettings_power_levels mx_RoomSettings_settings">
+                <div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
                     <div>
                         <label htmlFor="mx_RoomSettings_ban_level">Ban level</label>
                         <input type="text" defaultValue={ban_level} size="3" ref="ban" id="mx_RoomSettings_ban_level"
@@ -217,7 +347,7 @@ module.exports = React.createClass({
                 </div>
 
                 <h3>User levels</h3>
-                <div className="mx_RoomSettings_user_levels mx_RoomSettings_settings">
+                <div className="mx_RoomSettings_userLevels mx_RoomSettings_settings">
                     {Object.keys(user_levels).map(function(user, i) {
                         return (
                             <div key={user}>
@@ -228,29 +358,9 @@ module.exports = React.createClass({
                     })}
                 </div>
 
-                <h3>Event levels</h3>
-                <div className="mx_RoomSettings_event_lvels mx_RoomSettings_settings">
-                    {Object.keys(events_levels).map(function(event_type, i) {
-                        return (
-                            <div key={event_type}>
-                                <label htmlFor={"mx_RoomSettings_event_"+i}>{event_type}</label>
-                                <input type="text" defaultValue={events_levels[event_type]} size="3" id={"mx_RoomSettings_event_"+i} disabled/>
-                            </div>
-                        );
-                    })}
-                </div>
-
-                <h3>Banned users</h3>
-                <div className="mx_RoomSettings_banned">
-                    {banned.map(function(member, i) {
-                        return (
-                            <div key={i}>
-                                {member.userId}
-                            </div>
-                        );
-                    })}
-                </div>
-                {change_avatar}
+                { events_levels_section }
+                { banned_users_section }
+                { change_avatar }
             </div>
         );
     }
diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js
index f024f53a9a7ac8078b6ea6bd8bd605577599a9a9..f5ec6a0467e9fa9ebc00e2625c27ee5ace44eb0d 100644
--- a/src/components/views/settings/ChangeAvatar.js
+++ b/src/components/views/settings/ChangeAvatar.js
@@ -111,13 +111,14 @@ module.exports = React.createClass({
         // Having just set an avatar we just display that since it will take a little
         // time to propagate through to the RoomAvatar.
         if (this.props.room && !this.avatarSet) {
-            avatarImg = <RoomAvatar room={this.props.room} width='320' height='240' resizeMethod='scale' />;
+            avatarImg = <RoomAvatar room={this.props.room} width='240' height='240' resizeMethod='crop' />;
         } else {
             var style = {
-                maxWidth: 320,
+                maxWidth: 240,
                 maxHeight: 240,
+                objectFit: 'cover',
             };
-            avatarImg = <img src={this.state.avatarUrl} style={style} />;
+            avatarImg = <img className="mx_RoomAvatar" src={this.state.avatarUrl} style={style} />;
         }
 
         var uploadSection;