diff --git a/.gitignore b/.gitignore
index ff44ccd0c0be5f6e7288f1282a2f0a853f06dc72..5c48f0e0b97b6d82bf93880858cd51d2db92a22b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@
 /npm-debug.log
 /resources/static/build
 /resources/static/production
+.DS_Store
\ No newline at end of file
diff --git a/ChangeLog b/ChangeLog
index e30219c1821e21c9a0f791e3a317adf012ec93a2..8e07fa26f09ef9b8313c0e2d0816dc00689b0c30 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,23 @@
-train-2012.06.22: (in progress)
-  *
+train-2012.07.06: (in progress)
+
+train-2012.06.22:
+  * browserid.org now redirects to login.persona.org, all URLs are updated: #1743
+  * Websites can now provide their name and logo (requires SSL) to be displayed in the dialog: #1098, #1761
+  * A user is now sent back to the site they were visiting after verification (requires .watch() API): #385
+  * Fix .watch() API under IE8: #1637
+  * For dev and ephemeral deployments, move to awsbox, and new URLs: #1394, #1046, #1741
+  * Fix the scroll bar appearing on the main site's index page if it is not needed: #1693
+  * Clear the password if the user types a password then changes the email address: #1540
+  * New watch API now requires invocation with proper context (navigator.id.foo, not var foo = navigator.id.foo)
+  * Router fixes: #1713
+  * Serve fonts locally, don't pull resources from google: #1695
+  * Optimize images: #1747
+  * Fix flashes when verifying an email address: #1734
+  * Unit test added which runs jshint: #1731
+  * Fix submit occurring when selecting an email address in Firefox from the auto-complete list: #1780
+  * For KPI data, round timestamp to nearest 10 minutes, making correlation improbable: #1732
+  * Code cleanup: #1701, #1703, #1000, #1248, #1759, #1733, #1792
+  * Breaking API change: Persona now returns pubkey from generateKeypair to IdPs as a string
 
 train-2012.06.08:
   * rebrand from 'browserid' to 'persona': (including regressions #1711 #1706 #1716 #1719)
diff --git a/bin/router b/bin/router
index 56db15569691be00b3498ca77ddffe1171575f22..dc409f51267387c05c08a9f0f6c540854cf6c25c 100755
--- a/bin/router
+++ b/bin/router
@@ -49,7 +49,10 @@ if (!config.get('browserid_url')) {
 
 // #1 - Setup health check / heartbeat middleware.
 // This is in front of logging on purpose.  see issue #537
-heartbeat.setup(app);
+var browserid_url = urlparse(config.get('browserid_url')).validate().normalize().originOnly();
+heartbeat.setup(app, {
+  dependencies: [browserid_url]
+});
 
 // #2 - logging!  all requests other than __heartbeat__ are logged
 app.use(express.logger({
@@ -119,7 +122,6 @@ wsapi.setup({
 }, app);
 
 // Forward all leftover requests to browserid
-var browserid_url = urlparse(config.get('browserid_url')).validate().normalize().originOnly();
 app.use(function(req, res, next) {
   forward(
     browserid_url+req.url, req, res,
diff --git a/config/aws.json b/config/aws.json
index 418d8fc46340525c56893f7452c79cb9d41b1c94..95eb55fdfae619519adb14a3a8040cd717dcc507 100644
--- a/config/aws.json
+++ b/config/aws.json
@@ -21,5 +21,7 @@
   },
   "proxy": { "bind_to": { "port": 10006 } },
   "router": { "bind_to": { "port": 8080 } },
-  "kpi_backend_db_url" : "https://kpiggybank.hacksign.in/wsapi/interaction_data"
+  "kpi_backend_db_url" : "https://kpiggybank.hacksign.in/wsapi/interaction_data",
+  // whether to show the development menu.
+  "enable_development_menu": true
 }
diff --git a/config/local.json b/config/local.json
index 6d346f58d91652f91b4eff0e375a3941ec55d7a2..0aec32890f91b63109148e09f7bf85cd7877df87 100644
--- a/config/local.json
+++ b/config/local.json
@@ -12,5 +12,8 @@
   "express_log_format": "dev_bid",
   "email_to_console": true,
   "env": "local",
-  "kpi_backend_sample_rate": 1.0
+  "kpi_backend_sample_rate": 1.0,
+
+  // whether to show the development menu.
+  "enable_development_menu": true
 }
diff --git a/config/production.json b/config/production.json
index cf9b1968d99e7548030a83e60c4978f78352c1d2..13cbcaf2ed977a1d1dc597c93a2d988335666677 100644
--- a/config/production.json
+++ b/config/production.json
@@ -50,7 +50,10 @@
   "dbwriter_url": "http://127.0.0.1:62900",
   "browserid": { "bind_to": { "port": 62700 } },
   "browserid_url": "http://127.0.0.1:62700",
-  "router": { "bind_to": { "port": 63300 } }
+  "router": { "bind_to": { "port": 63300 } },
+
+  // set to true to enable the development menu.
+  "enable_development_menu": false
 
   // http_proxy should be overridded per env
   //"http_proxy": {
diff --git a/example/primary/provision.html b/example/primary/provision.html
index 94395c345c921b38b0949f727cd28b1f9f03cda7..c8b7cd583c4c38008d9df3181fa2ee3539bbce92 100644
--- a/example/primary/provision.html
+++ b/example/primary/provision.html
@@ -33,7 +33,7 @@
           $.ajax({
             url: '/api/cert_key',
             data: JSON.stringify({
-              pubkey: pubkey,
+              pubkey: JSON.parse(pubkey),
               duration: cert_duration
             }),
             type: 'POST',
diff --git a/lib/browserid/views.js b/lib/browserid/views.js
index 15fcadb077e7c06f9d9525bb52398d988239b053..53fcf01427448db8ed61dd22d00e6de3f6f796f5 100644
--- a/lib/browserid/views.js
+++ b/lib/browserid/views.js
@@ -38,6 +38,9 @@ function renderCachableView(req, res, template, options) {
   res.setHeader('Date', new Date().toUTCString());
   res.setHeader('Vary', 'Accept-Encoding,Accept-Language');
   res.setHeader('Content-Type', 'text/html; charset=utf8');
+
+  options.enable_development_menu = config.get('enable_development_menu');
+
   res.render(template, options);
 }
 
@@ -136,7 +139,12 @@ exports.setup = function(app) {
 
   app.get("/forgot", function(req, res) {
     // !cachable!  email embedded in DOM
-    res.render('forgot.ejs', {title: 'Forgot Password', fullpage: false, email: req.query.email});
+    res.render('forgot.ejs', {
+      title: 'Forgot Password',
+      fullpage: false,
+      email: req.query.email,
+      enable_development_menu: config.get('enable_development_menu')
+    });
   });
 
   app.get("/signin", function(req, res) {
@@ -157,7 +165,12 @@ exports.setup = function(app) {
 
   app.get("/verify_email_address", function(req, res) {
     // !cachable!  token is embedded in DOM
-    res.render('verify_email_address.ejs', {title: 'Complete Registration', fullpage: true, token: req.query.token});
+    res.render('verify_email_address.ejs', {
+      title: 'Complete Registration',
+      fullpage: true,
+      token: req.query.token,
+      enable_development_menu: config.get('enable_development_menu')
+    });
   });
 
   app.get("/add_email_address", function(req,res) {
@@ -168,7 +181,7 @@ exports.setup = function(app) {
   if ([ 'https://login.persona.org', 'https://login.anosrep.org' ].indexOf(config.get('public_url')) === -1) {
     // serve test.ejs to /test or /test/ or /test/index.html
     app.get(/^\/test\/(?:index.html)?$/, function (req, res) {
-      res.render('test.ejs', {title: 'BrowserID QUnit Test', layout: false});
+      res.render('test.ejs', {title: 'Mozilla Persona QUnit Test', layout: false});
     });
   } else {
     // this is stage or production, explicitly disable all resources under /test
diff --git a/lib/configuration.js b/lib/configuration.js
index 2858160e4e2b9acc26a0ab3eacf640a4ba606adc..0c5b68dce08d4963c117b79ced4bf9663e2d4c09 100644
--- a/lib/configuration.js
+++ b/lib/configuration.js
@@ -219,6 +219,10 @@ var conf = module.exports = convict({
   declaration_of_support_timeout_ms: {
     doc: "The amount of time we wait for a server to respond with a declaration of support, before concluding that they are not a primary.  Only relevant when our local proxy is in use, not in production or staging",
     format: 'integer = 15000'
+  },
+  enable_development_menu: {
+    doc: "Whether or not the development menu can be accessed",
+    format: 'boolean = false'
   }
 });
 
diff --git a/lib/heartbeat.js b/lib/heartbeat.js
index 666e77767ac78f4fb8fdb74355a42a4bf870dadc..e5e309fd4c990366e7cb198bb549744e179ea550 100644
--- a/lib/heartbeat.js
+++ b/lib/heartbeat.js
@@ -4,20 +4,74 @@
 
 const
 urlparse = require('urlparse'),
-logger = require('./logging.js').logger;
+logger = require('./logging.js').logger,
+url = require('url');
 
 // the path that heartbeats live at
 exports.path = '/__heartbeat__';
 
+const checkTimeout = 5000;
+
 // a helper function to set up a heartbeat check
-exports.setup = function(app, cb) {
+exports.setup = function(app, options, cb) {
+  var dependencies = [];
+
+  if (typeof options == 'function') {
+    cb = options;
+  } else if (options && options.dependencies) {
+    dependencies = options.dependencies;
+  }
+  var count = dependencies.length;
+
   app.use(function(req, res, next) {
-    if (req.method === 'GET' && req.path === exports.path) {
-      function ok(yeah) {
-        res.writeHead(yeah ? 200 : 500);
-        res.write(yeah ? 'ok' : 'not ok');
-        res.end();
+    if (req.method !== 'GET' || req.path !== exports.path) {
+      return next();
+    }
+
+    var checked = 0;
+    var query = url.parse(req.url, true).query;
+    var deep = typeof query.deep != 'undefined';
+    var notOk = [];
+
+    // callback for checking a dependency
+    function checkCB (num) {
+      return function (err, isOk) {
+        checked++;
+        if (err) {
+          notOk.push(dependencies[num] + ': '+ err);
+        }
+
+        // if all dependencies have been checked
+        if (checked == count) {
+          if (notOk.length === 0) {
+            try {
+              if (cb) cb(ok);
+              else ok(true);
+            } catch(e) {
+              logger.error("Exception caught in heartbeat handler: " + e.toString());
+              ok(false, e);
+            }
+          } else {
+            logger.warn("heartbeat failed due to dependencies - " + notOk.join(', '));
+            ok(false, '\n' + notOk.join('\n') + '\n');
+          }
+        }
+      };
+    }
+
+    function ok(yeah, msg) {
+      res.writeHead(yeah ? 200 : 500);
+      res.write(yeah ? 'ok' : 'bad');
+      if (msg) res.write(msg);
+      res.end();
+    }
+
+    // check all dependencies if deep
+    if (deep && count) {
+      for (var i = 0; i < count; i++) {
+        check(dependencies[i] + exports.path, checkCB(i));
       }
+    } else {
       try {
         if (cb) cb(ok);
         else ok(true);
@@ -25,29 +79,39 @@ exports.setup = function(app, cb) {
         logger.error("Exception caught in heartbeat handler: " + e.toString());
         ok(false);
       }
-    } else {
-      return next();
     }
   });
 };
 
+
 // a function to check the heartbeat of a remote server
-exports.check = function(url, cb) {
+var check = exports.check = function(url, cb) {
   if (typeof url === 'string') url = urlparse(url).normalize().validate();
   else if (typeof url !== 'object') throw "url string or object required as argumnet to heartbeat.check";
   if (!url.port) url.port = (url.scheme === 'http') ? 80 : 443;
 
   var shortname = url.host + ':' + url.port;
 
-  require(url.scheme).get({
+  var timeoutHandle = setTimeout(function() {
+    req.abort();
+  }, checkTimeout);
+
+  var req = require(url.scheme).get({
     host: url.host,
     port: url.port,
     path: exports.path
   }, function (res) {
-    if (res.statusCode === 200) cb(true);
-    else logger.error("non-200 response from " + shortname + ".  fatal! (" + res.statusCode + ")");
-  }, function (e) {
-    logger.error("can't communicate with " + shortname + ".  fatal: " + e);
-    cb(false);
+    clearTimeout(timeoutHandle);
+    if (res.statusCode === 200) cb(null, true);
+    else {
+      logger.warn("heartbeat failure: non-200 response from " + shortname + ".  fatal! (" +
+                  res.statusCode + ")");
+      cb("response code " + res.statusCode);
+    }
+  });
+  req.on('error', function (e) {
+    clearTimeout(timeoutHandle);
+    logger.warn("heartbeat failure: can't communicate with " + shortname + ".  fatal: " + e);
+    cb(e ? e : "unknown error");
   });
 };
diff --git a/lib/static_resources.js b/lib/static_resources.js
index 9c0b1e951dd482c52c874ce01f7b83971ac7e54f..b994c3842d6b8abe2f939c53caf349150c4e521c 100644
--- a/lib/static_resources.js
+++ b/lib/static_resources.js
@@ -68,7 +68,8 @@ var browserid_js = und.flatten([
     '/pages/forgot.js',
     '/pages/manage_account.js',
     '/pages/signin.js',
-    '/pages/signup.js'
+    '/pages/signup.js',
+    '/pages/about.js'
   ]
 ]);
 
diff --git a/package.json b/package.json
index 7801ed885964e53f924355ddae3bb3d909007649..eecc9a09648dccf0957d9ccd54744eefca6de0ed 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,8 @@
     "devDependencies": {
         "vows": "0.5.13",
         "awsbox": "0.2.12",
-        "irc": "0.3.3"
+        "irc": "0.3.3",
+        "jshint": "0.7.1"
     },
     "scripts": {
         "postinstall": "./scripts/generate_ephemeral_keys.sh",
diff --git a/resources/static/communication_iframe/start.js b/resources/static/communication_iframe/start.js
index 57ce36dcaca52ba1d78fc3db41208e89371444ec..fa26648aef2caed72eeb18c9a81b2259cee266c5 100644
--- a/resources/static/communication_iframe/start.js
+++ b/resources/static/communication_iframe/start.js
@@ -9,6 +9,11 @@
       user = bid.User,
       storage = bid.Storage;
 
+  // Initialize all localstorage values to default values.  Neccesary for
+  // proper sync of IE8 localStorage across multiple simultaneous
+  // browser sessions.
+  storage.setDefaultValues();
+
   network.init();
 
   var chan = Channel.build({
diff --git a/resources/static/css/common.css b/resources/static/css/common.css
index c12bdae416fdfb37a4ca365559162111d5418ab9..11f1f79ee52db1129100d736c9765cecb79de34d 100644
--- a/resources/static/css/common.css
+++ b/resources/static/css/common.css
@@ -128,6 +128,10 @@ input[type=password]:disabled {
      * issue #1311 */
     -webkit-text-fill-color: #4f4f4f;
     opacity: 1;
+    /* Remove the box-shadow and border-color that come with a focused input
+     * field */
+    box-shadow: none;
+    border-color: #b2b2b2;
 }
 
 input[type=radio],
@@ -359,7 +363,12 @@ footer .help {
 
 #wait, #delay, #error {
   background-color: #dadee1;
-  background-image: url("/i/grain.png"), -moz-linear-gradient(top, #dadee1, #c7ccd0);
+  background-image: url("/i/grain.png"), -webkit-gradient(linear, left top, left bottom, from(#dadee1), to(#c7ccd0));
+  background-image: url("/i/grain.png"), -webkit-linear-gradient(top, #dadee1, #c7ccd0);
+  background-image: url("/i/grain.png"),    -moz-linear-gradient(top, #dadee1, #c7ccd0);
+  background-image: url("/i/grain.png"),     -ms-linear-gradient(top, #dadee1, #c7ccd0);
+  background-image: url("/i/grain.png"),      -o-linear-gradient(top, #dadee1, #c7ccd0);
+  background-image: url("/i/grain.png"),         linear-gradient(top, #dadee1, #c7ccd0);
 }
 
 #wait, #delay {
diff --git a/resources/static/css/m.css b/resources/static/css/m.css
index 8325d9e0bf567610fb120ad1941dc9c7cc4640fc..50943635fe29a7aa2366788155d2265970643603 100644
--- a/resources/static/css/m.css
+++ b/resources/static/css/m.css
@@ -45,7 +45,7 @@
 /*
  * 620 catches most mobile devices in landscape mode.  The purpose of this is
  * to make sure the right hand nav menu does not drop partially below the
- * persona logo.
+ * persona logo. This also adjusts the boxes on the "How It Works" page.
  */
 @media screen and (max-width: 620px) {
   header ul {
@@ -54,6 +54,51 @@
     display: block;
     text-align: center;
   }
+
+  .blurb.half {
+    width: 100%;
+    float: none;
+    min-height: 0 !important;
+  }
+  .blurb.half.first {
+    margin-right: 0;
+  }
+
+  .blurb {
+    display: -webkit-box;
+    display: box;
+    -webkit-box-orient: vertical;
+    box-orient: vertical;
+  }
+  .blurb .info {
+    -webkit-box-ordinal-group: 2;
+    -moz-box-ordinal-group: 2;
+    -ms-box-ordinal-group: 2;
+    box-ordinal-group: 2;
+  }
+  .blurb h1{
+    font-size: 20px;
+  }
+  .blurb.flexible .graphic {
+    margin: 0 0 30px;
+  }
+  .blurb .first {
+    padding-right: 0;
+  }
+  .blurb .info, .blurb .graphic {
+    float: none;
+    width: 100%;
+  }
+
+  h2.title {
+    font-size: 32px;
+    padding-bottom: 15px;
+  }
+
+  .privacy{
+    margin: 60px 0 30px;
+    padding-bottom: 30px;
+  }
 }
 
 /*
@@ -78,11 +123,6 @@
     padding: 10px;
   }
 
-  #about {
-    padding: 10px;
-    border-radius: 5px;
-  }
-
   .headline-main {
     font-size: 37px;
     text-align: center;
@@ -159,39 +199,6 @@
     list-style-position: inside;
   }
 
-  #about .video,
-  #about .video img {
-    width: 300px;
-    height: auto;
-  }
-
-  .row {
-    padding: 20px 20px 0;
-    margin: 0;
-  }
-
-  .row div {
-    width: auto;
-    height: auto;
-    vertical-align: inherit;
-    display: block;
-    padding: 20px 0;
-  }
-
-  .row img {
-    float: none;
-    width: 260px;
-    height: auto;
-  }
-
-  .row p {
-    float: none;
-    display: block;
-    width: 260px;
-    text-indent: -33px;
-    padding-left: 33px;
-  }
-
   #signUpFormWrap {
     margin: 122px 10px 122px;
   }
diff --git a/resources/static/css/style.css b/resources/static/css/style.css
index 35bfa524ab66efd353488ddd596061022bb3fc77..6ac4fb9fd4c9c7f5c8eac819133263d8ed2883eb 100644
--- a/resources/static/css/style.css
+++ b/resources/static/css/style.css
@@ -106,15 +106,6 @@ body {
   padding: 50px 0;
 }
 
-#about {
-  color: #444;
-  text-shadow: 1px 1px 0 rgba(255,255,255,0.5);
-  padding: 50px 75px;
-  background-color: #fff;
-  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
-  border-radius: 5px;
-}
-
 h1 {
   margin-bottom: 35px;
 }
@@ -123,65 +114,6 @@ h1 {
   font-weight: 300;
 }
 
-.row {
-  margin: 25px 0 0 0;
-  padding: 0 0 25px 0;
-  position: relative;
-  border-bottom: 1px solid #eee;
-}
-
-
-
-
-.row:last-child {
-  padding-bottom: 0;
-  border-bottom: none;
-}
-
-.row div {
-  height: 140px;
-  width: 500px;
-  padding: 0 0 0 20px;
-  display: table-cell;
-  vertical-align: middle;
-}
-
-.row p {
-  width: 380px;
-  text-shadow: 1px 1px 0 rgba(255,255,255,0.5);
-  float: left;
-}
-
-.row img {
-  float: left;
-}
-
-.row button, .row .button {
-  float: right;
-  display: inline-block;
-}
-
-div.steps {
-  width: 24px;
-  height: 24px;
-  vertical-align: bottom;
-  margin-right: 10px;
-  background-image: url('/i/count.png');
-  float: left;
-}
-
-.one .steps {
-  background-position: left 0;
-}
-
-.two .steps {
-  background-position: left -24px;
-}
-
-.three .steps {
-  background-position: left -48px;
-}
-
 
 #legal {
   padding: 75px 125px;
@@ -515,30 +447,10 @@ button.delete:active {
   padding: 0;
 }
 
-#signUpForm .red {
-  color: red;
-}
-
 #signUpForm .submit {
   height: 28px;
 }
 
-#signUpForm .error {
-  margin-top: 20px;
-  color: red;
-  background-color: rgba(255,0,0,0.25);
-  -ms-filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3fff0000,endColorstr=#3fff0000);
-  filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3fff0000,endColorstr=#3fff0000);
-  zoom: 1;
-  padding: 5px;
-  line-height: 16px;
-
-  -webkit-border-radius: 3px;
-     -moz-border-radius: 3px;
-       -o-border-radius: 3px;
-          border-radius: 3px;
-}
-
 #signUpForm > .siteinfo {
   margin-bottom: 10px;
 }
@@ -679,3 +591,141 @@ footer ul li:first-child a:hover {
   display: block;
 }
 
+/*  How It Works
+ ***************/
+
+ h2.title {
+  font-size: 48px;
+  font-weight: normal;
+  color: #fff;
+  text-shadow: 0 1px rgba(0, 0, 0, 0.5);
+  text-align: center;
+  letter-spacing: -2px;
+  padding-bottom: 30px;
+  margin: 0;
+}
+
+.blurb, a.developers {
+  -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.13);
+  -moz-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.13);
+  -ms-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.13);
+  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.13);
+  background: #63727d;
+  background: rgba(13, 28, 41, 0.1);
+  font-size: 14px;
+  color: #fff;
+}
+
+.blurb {
+  zoom: 1;
+  margin-top: 30px;
+  padding: 30px;
+  text-align: left;
+  line-height: 1.5;
+}
+.blurb:before, .blurb:after {
+  content: "";
+  display: table;
+}
+.blurb:after {
+  clear: both;
+}
+
+.blurb h1, .blurb p, .blurb a, a.developers{
+  text-shadow: 0 1px rgba(0, 0, 0, 0.5);
+  font-weight: normal;
+}
+
+.blurb img{
+  max-width: 100%;
+  vertical-align: bottom;
+}
+
+.blurb a {
+  color: #fff;
+  border-bottom: 1px dotted #fff;
+  font-weight: normal;
+}
+.blurb a:hover {
+  color: #53b7fb;
+}
+.blurb.half {
+  width: 48%;
+  float: left;
+}
+.blurb.half.first {
+  margin-right: 4%;
+}
+.blurb .info, .blurb .graphic {
+  width: 50%;
+  float: left;
+}
+.blurb .first {
+  padding-right: 30px;
+}
+.blurb .graphic {
+  text-align: center;
+}
+.blurb h1 {
+  font-size: 32px;
+  font-weight: normal;
+  letter-spacing: -1px;
+  line-height: 1.1;
+  margin-bottom: 20px;
+}
+.blurb p {
+  margin-bottom: 1em;
+}
+.blurb p:last-of-type {
+  margin-bottom: 0;
+}
+
+.privacy {
+  -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.1);
+  -moz-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.1);
+  -ms-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.1);
+  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.1);
+  zoom: 1;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+  padding-bottom: 60px;
+  margin: 100px 0 60px;
+}
+.privacy:before, .privacy:after {
+  content: "";
+  display: table;
+}
+.privacy:after {
+  clear: both;
+}
+
+a.developers {
+  -webkit-transition: all 300ms ease;
+  -moz-transition: all 300ms ease;
+  -ms-transition: all 300ms ease;
+  transition: all 300ms ease;
+  display: block;
+  padding: 13px 15px;
+  line-height: 1.4;
+  text-align: center;
+}
+a.developers:hover {
+  background: #3b4e5c;
+  background: rgba(13, 28, 41, 0.2);
+}
+a.developers img {
+  margin: 0 5px -7px 0;
+}
+a.developers span {
+  color: #53b7fb;
+  font-weight: bold;
+  margin-right: 10px;
+  display: inline-block;
+}
+
+article.flexible {
+  padding-bottom: 0;
+}
+article.flexible .info {
+  margin-bottom: 30px;
+}
+
diff --git a/resources/static/dialog/controllers/actions.js b/resources/static/dialog/controllers/actions.js
index 80435e5220f6bafe3e5856ef3aeb358bc715a098..3a95c89155d98f12c71b9f8eb11231f0a1f82bfe 100644
--- a/resources/static/dialog/controllers/actions.js
+++ b/resources/static/dialog/controllers/actions.js
@@ -28,7 +28,6 @@ BrowserID.Modules.Actions = (function() {
     }
 
     mediator.publish("service", { name: name });
-    bid.resize();
 
     return module;
   }
diff --git a/resources/static/dialog/controllers/add_email.js b/resources/static/dialog/controllers/add_email.js
index c9a8c638ca85ddc7f7566fb1c80e8dd90fa85d89..da642e0d2ced87d3cdf769b0d2389e0b2ad46e9a 100644
--- a/resources/static/dialog/controllers/add_email.js
+++ b/resources/static/dialog/controllers/add_email.js
@@ -7,17 +7,38 @@ BrowserID.Modules.AddEmail = (function() {
   "use strict";
 
   var bid = BrowserID,
+      dom = bid.DOM,
       helpers = bid.Helpers,
       dialogHelpers = helpers.Dialog,
       errors = bid.Errors,
       complete = helpers.complete,
-      tooltip = bid.Tooltip;
+      tooltip = bid.Tooltip,
+      hints = ["addressInfo"],
+      ANIMATION_TIME = 250;
+
+  function hideHint(selector) {
+    $("." + selector).hide();
+  }
+
+  function showHint(selector, callback) {
+    _.each(hints, function(className) {
+      if (className !== selector) {
+        hideHint(className);
+      }
+    });
+
+    $("." + selector).fadeIn(ANIMATION_TIME, function() {
+      dom.fireEvent(window, "resize");
+      complete(callback);
+    });
+  }
 
   function addEmail(callback) {
     var email = helpers.getAndValidateEmail("#newEmail"),
         self=this;
 
     if (email) {
+      showHint("addressInfo");
       dialogHelpers.addEmail.call(self, email, callback);
     }
     else {
@@ -39,6 +60,7 @@ BrowserID.Modules.AddEmail = (function() {
           });
 
       self.renderDialog("add_email", templateData);
+      hideHint("addressInfo");
 
       self.click("#cancel", cancelAddEmail);
       Module.sc.start.call(self, options);
diff --git a/resources/static/dialog/controllers/authenticate.js b/resources/static/dialog/controllers/authenticate.js
index ac413aa99a82cb3fd6573580dee2a04d4e83e99c..c316e55a5e5822825c2ad2bb69f1b32a6bf106fa 100644
--- a/resources/static/dialog/controllers/authenticate.js
+++ b/resources/static/dialog/controllers/authenticate.js
@@ -43,6 +43,7 @@ BrowserID.Modules.Authenticate = (function() {
 
     if (!email) return;
 
+    dom.setAttr('#email', 'disabled', 'disabled');
     if(info && info.type) {
       onAddressInfo(info);
     }
@@ -54,6 +55,7 @@ BrowserID.Modules.Authenticate = (function() {
 
     function onAddressInfo(info) {
       addressInfo = info;
+      dom.removeAttr('#email', 'disabled');
 
       if(info.type === "primary") {
         self.close("primary_user", info, info);
diff --git a/resources/static/dialog/controllers/dialog.js b/resources/static/dialog/controllers/dialog.js
index eff93813905bb9617cb979b6286d5b9a89c01b6f..1e14ca80d2f1354555169b977c9986431cbbe8a8 100644
--- a/resources/static/dialog/controllers/dialog.js
+++ b/resources/static/dialog/controllers/dialog.js
@@ -177,6 +177,11 @@ BrowserID.Modules.Dialog = (function() {
           // that come from other domains, only allow absolute paths from the
           // origin.
           params.siteLogo = fixupAbsolutePath(origin_url, paramsFromRP.siteLogo);
+          // To avoid mixed content errors, only allow siteLogos to be served
+          // from https RPs
+          if (URLParse(origin_url).scheme !== "https") {
+            throw "only https sites can specify a siteLogo";
+          }
         }
 
         if (paramsFromRP.siteName) {
diff --git a/resources/static/dialog/css/m.css b/resources/static/dialog/css/m.css
index 2a757809b174111c8050bdb28fcc7a5a5cd8d9cf..ff2ee544b1ecc1f17c11781afb4dfa69e4e2313b 100644
--- a/resources/static/dialog/css/m.css
+++ b/resources/static/dialog/css/m.css
@@ -159,14 +159,6 @@
     line-height: 40px;
   }
 
-  #error {
-    position: static;
-  }
-
-  #error .vertical, #delay .vertical, #wait .vertical {
-    height: 250px;
-  }
-
   #error .vertical {
     width: auto;
   }
diff --git a/resources/static/dialog/resources/screen_size_hacks.js b/resources/static/dialog/resources/screen_size_hacks.js
index 33685cb756f36123246e781af5600ff6b586e4c8..c304ca0e4cd3cac9d242d47d07a78f3993e78e30 100644
--- a/resources/static/dialog/resources/screen_size_hacks.js
+++ b/resources/static/dialog/resources/screen_size_hacks.js
@@ -106,6 +106,8 @@
         contentHeight = Math.max(100, contentHeight, formHeight);
         contentEl.css("min-height", contentHeight + "px");
 
+        // Remove the explicit static position we added to let this go back to
+        // the position specified in CSS.
         $("section,#signIn").css("position", "");
 
         favIconHeight = $("#favicon").outerHeight();
diff --git a/resources/static/dialog/views/add_email.ejs b/resources/static/dialog/views/add_email.ejs
index 5af78c55bba4147120393579d08484e2be5ebbb2..9e183be4c20c66d881def28e9617382b19731f1a 100644
--- a/resources/static/dialog/views/add_email.ejs
+++ b/resources/static/dialog/views/add_email.ejs
@@ -27,6 +27,10 @@
                 <%= gettext('That address is already added to your account!') %>
               </div>
           </li>
+
+          <li id="hint_section" class="addressInfo">
+              <%= gettext("Please hold on while we get information about your email provider.") %>
+          </li>
       </ul>
 
       <div class="submit cf">
diff --git a/resources/static/dialog/views/error.ejs b/resources/static/dialog/views/error.ejs
index 0b35cd680dc17fcb47e25adc6284ae7cce111b46..2a0ee0066700746b6c73e31818f4353ee1156976 100644
--- a/resources/static/dialog/views/error.ejs
+++ b/resources/static/dialog/views/error.ejs
@@ -14,6 +14,10 @@
       <%= gettext("Persona requires cookies") %>
     </h2>
     <%= format(gettext("Please close this window, <a %s>enable cookies</a> and try again"), [" target='_blank' href='http://support.mozilla.org/en-US/kb/Websites%20say%20cookies%20are%20blocked'"]) %>
+  <% } else if(typeof title === "string") { %>
+    <h2>
+      <span class="emphasis"><%= title %></span>
+    </h2>
   <% } else { %>
     <h2 id="defaultError">
       <%= gettext("We are very sorry.") %><span class="emphasis"> <%= gettext("There has been an error!") %></span>
diff --git a/resources/static/fonts/LICENSE.txt b/resources/static/fonts/LICENSE.txt
new file mode 100644
index 0000000000000000000000000000000000000000..0b365b09f4a75a94f6e9baa26b7283ce7c873d0d
--- /dev/null
+++ b/resources/static/fonts/LICENSE.txt
@@ -0,0 +1,203 @@
+Fonts obtained from Google's Web Font service at: http://www.google.com/webfonts
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/resources/static/i/developers-link.png b/resources/static/i/developers-link.png
new file mode 100644
index 0000000000000000000000000000000000000000..e730af8df49fcf06234eb9361ec5b894870fd922
Binary files /dev/null and b/resources/static/i/developers-link.png differ
diff --git a/resources/static/i/flexible-graphic.png b/resources/static/i/flexible-graphic.png
new file mode 100644
index 0000000000000000000000000000000000000000..c94d2cc1e3e86106a155785b26f6b2c151a19a11
Binary files /dev/null and b/resources/static/i/flexible-graphic.png differ
diff --git a/resources/static/i/one-password-graphic.png b/resources/static/i/one-password-graphic.png
new file mode 100644
index 0000000000000000000000000000000000000000..380579a0ed0599b8fc206d1331817d175202b5ab
Binary files /dev/null and b/resources/static/i/one-password-graphic.png differ
diff --git a/resources/static/pages/about.js b/resources/static/pages/about.js
new file mode 100644
index 0000000000000000000000000000000000000000..9741062539480bfb5b450d9eac79235427d75c5e
--- /dev/null
+++ b/resources/static/pages/about.js
@@ -0,0 +1,45 @@
+/*globals BrowserID:true, $:true*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+BrowserID.about = (function() {
+  "use strict";
+
+  var bid = BrowserID;
+
+  function resize() {
+    // Get tallest blurb
+    var tallestBlurb = 0
+
+    $('.half').each(function(index) {
+      var $this = $(this);
+
+      if (index == 0) {
+        tallestBlurb = $this.height();
+      } else {
+
+        if ($this.height() < tallestBlurb) {
+          $this.css('min-height', tallestBlurb);
+        } else {
+          $('.half.first').css('min-height', $this.height());
+        }
+
+      }
+    });
+  }
+
+  var Module = bid.Modules.PageModule.extend({
+    start: function(options) {
+      var self=this;
+
+      Module.sc.start.call(self, options);
+      resize();
+
+      // The half heights can change every time there is a window resize.
+      self.bind(window, "resize", resize);
+    }
+  });
+
+  return Module;
+}());
diff --git a/resources/static/pages/signin.js b/resources/static/pages/signin.js
index 8a487f82fff0b6a0cbb9914ac7fb311e698a7b87..edc58501195871df8b021adfdcd325f87ba6357d 100644
--- a/resources/static/pages/signin.js
+++ b/resources/static/pages/signin.js
@@ -48,7 +48,9 @@ BrowserID.signIn = (function() {
         email = helpers.getAndValidateEmail("#email");
 
     if(email) {
+      dom.setAttr('#email', 'disabled', 'disabled');
       user.addressInfo(email, function(info) {
+        dom.removeAttr('#email', 'disabled');
         addressInfo = info;
 
         if(info.type === "secondary") {
diff --git a/resources/static/pages/signup.js b/resources/static/pages/signup.js
index 3fef51badbfe646ad8f2d9ec523afff178dfd896..d7d4049ba49380eddc94045312655fd0685d1c5f 100644
--- a/resources/static/pages/signup.js
+++ b/resources/static/pages/signup.js
@@ -99,15 +99,17 @@ BrowserID.signUp = (function() {
           self = this;
 
       if (email) {
-
+        dom.setAttr('#email', 'disabled', 'disabled');
         user.isEmailRegistered(email, function(isRegistered) {
           if(isRegistered) {
+            dom.removeAttr('#email', 'disabled');
             $('#registeredEmail').html(email);
             showNotice(".alreadyRegistered");
             oncomplete && oncomplete(false);
           }
           else {
             user.addressInfo(email, function(info) {
+              dom.removeAttr('#email', 'disabled');
               if(info.type === "primary") {
                 createPrimaryUser.call(self, info, oncomplete);
               }
diff --git a/resources/static/pages/start.js b/resources/static/pages/start.js
index f6bb1b258897ed2588cd7c148d8445065db60d09..62f92b774bed66ebba614798c1d1303912b51670 100644
--- a/resources/static/pages/start.js
+++ b/resources/static/pages/start.js
@@ -90,7 +90,7 @@ $(function() {
     // footer remains at the bottom of the screen.
     var paddingTop = 0, paddingBottom = 0;
 
-    if(paddingAddedToMinHeight()) {
+    if (paddingAddedToMinHeight()) {
       paddingTop = parseInt($("#content").css("padding-top") || 0, 10);
       paddingBottom = parseInt($("#content").css("padding-bottom") || 0, 10);
     }
@@ -107,7 +107,7 @@ $(function() {
   moduleManager.register("development", Development);
   moduleManager.start("development");
 
-  if(shouldCheckCookies(path)) {
+  if (shouldCheckCookies(path)) {
     // do a cookie check on every page except the main page.
     moduleManager.register("cookie_check", CookieCheck);
     moduleManager.start("cookie_check", { ready: start });
@@ -120,7 +120,7 @@ $(function() {
   function start(status) {
     // If cookies are disabled, do not run any of the page specific code and
     // instead just show the error message.
-    if(!status) return;
+    if (!status) return;
 
 
     if (!path || path === "/") {
@@ -144,13 +144,17 @@ $(function() {
         verifyFunction: "verifyEmail"
       });
     }
-    else if(path === "/verify_email_address") {
+    else if (path === "/verify_email_address") {
       var module = bid.verifySecondaryAddress.create();
       module.start({
         token: token,
         verifyFunction: "verifyUser"
       });
     }
+    else if (path === "/about") {
+      var module = bid.about.create();
+      module.start({});
+    }
     else {
       // Instead of throwing a hard error here, adding a message to the console
       // to let developers know something is up.
diff --git a/resources/static/pages/verify_secondary_address.js b/resources/static/pages/verify_secondary_address.js
index df4561ec37b6707e3401afafd4f7c07225ba283a..5b2f7f20051e8bd9c3ce71c181ac75c2cc6c4c69 100644
--- a/resources/static/pages/verify_secondary_address.js
+++ b/resources/static/pages/verify_secondary_address.js
@@ -19,7 +19,6 @@ BrowserID.verifySecondaryAddress = (function() {
       tooltip = bid.Tooltip,
       token,
       sc,
-      needsPassword,
       mustAuth,
       verifyFunction,
       doc = document,
@@ -29,11 +28,6 @@ BrowserID.verifySecondaryAddress = (function() {
       redirectTo,
       redirectTimeout;  // set in config if available, use REDIRECT_SECONDS otw.
 
-  function showError(el, oncomplete) {
-    dom.hide(".hint,#signUpForm");
-    $(el).fadeIn(ANIMATION_TIME, oncomplete);
-  }
-
   function showRegistrationInfo(info) {
     dom.setInner("#email", info.email);
 
@@ -55,38 +49,37 @@ BrowserID.verifySecondaryAddress = (function() {
 
   function submit(oncomplete) {
     var pass = dom.getInner("#password") || undefined,
-        vpass = dom.getInner("#vpassword") || undefined,
-        inputValid = (!needsPassword ||
-                    validation.passwordAndValidationPassword(pass, vpass))
-             && (!mustAuth ||
-                    validation.password(pass));
+        inputValid = !mustAuth || validation.password(pass);
 
     if (inputValid) {
       user[verifyFunction](token, pass, function(info) {
         dom.addClass("body", "complete");
 
-        var verified = info.valid,
-            selector = verified ? "#congrats" : "#cannotcomplete";
-
-        pageHelpers.replaceFormWithNotice(selector, function() {
-          if (redirectTo && verified) {
-
-            // set the loggedIn status for the site.  This allows us to get
-            // a silent assertion without relying on the dialog to set the
-            // loggedIn status for the domain.  This is useful when the user
-            // closes the dialog OR if redirection happens before the dialog
-            // has had a chance to finish its business.
-            storage.setLoggedIn(URLParse(redirectTo).originOnly(), email);
-
-            setTimeout(function() {
-              doc.location.href = redirectTo;
+        var verified = info.valid;
+
+        if (verified) {
+          pageHelpers.replaceFormWithNotice("#congrats", function() {
+            if (redirectTo) {
+              // set the loggedIn status for the site.  This allows us to get
+              // a silent assertion without relying on the dialog to set the
+              // loggedIn status for the domain.  This is useful when the user
+              // closes the dialog OR if redirection happens before the dialog
+              // has had a chance to finish its business.
+              storage.setLoggedIn(URLParse(redirectTo).originOnly(), email);
+
+              setTimeout(function() {
+                doc.location.href = redirectTo;
+                complete(oncomplete, verified);
+              }, redirectTimeout);
+            }
+            else {
               complete(oncomplete, verified);
-            }, redirectTimeout);
-          }
-          else {
-            complete(oncomplete, verified);
-          }
-        });
+            }
+          });
+        }
+        else {
+          pageHelpers.showFailure(errors.cannotComplete, info, oncomplete);
+        }
       }, function(info) {
         if (info.network && info.network.status === 401) {
           tooltip.showTooltip("#cannot_authenticate");
@@ -103,36 +96,30 @@ BrowserID.verifySecondaryAddress = (function() {
   }
 
   function startVerification(oncomplete) {
+    var self=this;
     user.tokenInfo(token, function(info) {
       if (info) {
         redirectTo = info.returnTo;
         email = info.email;
         showRegistrationInfo(info);
 
-        needsPassword = info.needs_password;
         mustAuth = info.must_auth;
-
-        if (needsPassword) {
-          // This is a fix for legacy users who started the user creation
-          // process without setting their password in the dialog.  If the user
-          // needs a password, they must set it now.  Once all legacy users are
-          // verified or their links invalidated, this flow can be removed.
-          dom.addClass("body", "enter_password");
-          dom.addClass("body", "enter_verify_password");
-          complete(oncomplete, true);
-        }
-        else if (mustAuth) {
-          // These are users who have set their passwords inside of the dialog.
+        if (mustAuth) {
+          // These are users who are authenticating in a different browser or
+          // session than the initiator.
           dom.addClass("body", "enter_password");
           complete(oncomplete, true);
         }
         else {
-          // These are users who do not have to set their passwords at all.
+          // Easy case where user is in same browser and same session, just
+          // verify and be done with it all!
           submit(oncomplete);
         }
       }
       else {
-        showError("#cannotconfirm");
+        // renderError is used directly instead of pageHelpers.showFailure
+        // because showFailure hides the title in the extended info.
+        self.renderError("error", errors.cannotConfirm);
         complete(oncomplete, false);
       }
     }, pageHelpers.getFailure(errors.getTokenInfo, oncomplete));
@@ -140,7 +127,8 @@ BrowserID.verifySecondaryAddress = (function() {
 
   var Module = bid.Modules.PageModule.extend({
     start: function(options) {
-      this.checkRequired(options, "token", "verifyFunction");
+      var self=this;
+      self.checkRequired(options, "token", "verifyFunction");
 
       token = options.token;
       verifyFunction = options.verifyFunction;
@@ -151,9 +139,9 @@ BrowserID.verifySecondaryAddress = (function() {
         redirectTimeout = REDIRECT_SECONDS * 1000;
       }
 
-      startVerification(options.ready);
+      startVerification.call(self, options.ready);
 
-      sc.start.call(this, options);
+      sc.start.call(self, options);
     },
 
     submit: submit
diff --git a/resources/static/shared/error-display.js b/resources/static/shared/error-display.js
index c607eac8f9c37b1a0ecaf07af2b7e50a47699338..7777364d2242006aca12f7c98c08d099466c586e 100644
--- a/resources/static/shared/error-display.js
+++ b/resources/static/shared/error-display.js
@@ -14,7 +14,12 @@ BrowserID.ErrorDisplay = (function() {
     /**
      * XXX What a big steaming pile, use CSS animations for this!
      */
-    $("#moreInfo").slideDown();
+    $("#moreInfo").slideDown(function() {
+      // The expanded info may be partially obscured on mobile devices in
+      // landscape mode.  Force the screen size hacks to account for the new
+      // expanded size.
+      dom.fireEvent(window, "resize");
+    });
     $("#openMoreInfo").css({visibility: "hidden"});
   }
 
diff --git a/resources/static/shared/error-messages.js b/resources/static/shared/error-messages.js
index 2ce74cfde884cc3adb5a4103cd12e30794f5be89..f761aabf63ff14e628987e485123a9a311698476 100644
--- a/resources/static/shared/error-messages.js
+++ b/resources/static/shared/error-messages.js
@@ -35,6 +35,14 @@ BrowserID.Errors = (function(){
       title: "Cancelling User Account"
     },
 
+    cannotConfirm: {
+      title: gettext("There was a problem with your signup link. Has this address already been registered?")
+    },
+
+    cannotComplete: {
+      title: gettext("Error encountered trying to complete registration.")
+    },
+
     checkAuthentication: {
       title: "Checking Authentication"
     },
@@ -48,7 +56,7 @@ BrowserID.Errors = (function(){
     },
 
     cookiesDisabled: {
-      title: gettext("BrowserID requires cookies"),
+      title: gettext("Persona requires cookies"),
       message: format(gettext("Please close this window, <a %s>enable cookies</a> and try again"), [" target='_blank' href='" + enableCookiesURL + "'"])
 
     },
diff --git a/resources/static/shared/modules/interaction_data.js b/resources/static/shared/modules/interaction_data.js
index f9de4013f329732d62db7fe36bfe7c5423408999..e07e04de42e27fea588e31ad5ddd805040ba694b 100644
--- a/resources/static/shared/modules/interaction_data.js
+++ b/resources/static/shared/modules/interaction_data.js
@@ -23,6 +23,8 @@
 //    listen for events via the mediator?
 
 BrowserID.Modules.InteractionData = (function() {
+  "use strict";
+
   var bid = BrowserID,
       model = bid.Models.InteractionData,
       network = bid.Network,
@@ -98,10 +100,16 @@ BrowserID.Modules.InteractionData = (function() {
       return;
     }
 
+    // server_time is sent in milliseconds. The promise to users and data
+    // safety is the timestamp would be at a 10 minute resolution.  Round to the
+    // nearest 10 minute mark.
+    var TEN_MINS_IN_MS = 10 * 60 * 1000,
+        roundedServerTime = Math.round(result.server_time / TEN_MINS_IN_MS) * TEN_MINS_IN_MS;
+
     var currentData = {
       event_stream: self.initialEventStream,
       sample_rate: sampleRate,
-      timestamp: result.server_time,
+      timestamp: roundedServerTime,
       local_timestamp: self.startTime.toString(),
       lang: dom.getAttr('html', 'lang') || null,
     };
diff --git a/resources/static/shared/modules/page_module.js b/resources/static/shared/modules/page_module.js
index 54b570841d4f0b3bb250039939ec26e0f173c1f2..36aa78c7c29a72aae98d2d6552b08100467c54b2 100644
--- a/resources/static/shared/modules/page_module.js
+++ b/resources/static/shared/modules/page_module.js
@@ -15,16 +15,6 @@ BrowserID.Modules.PageModule = (function() {
       cancelEvent = helpers.cancelEvent,
       mediator = bid.Mediator;
 
-   function onKeypress(event) {
-    if (event.which === 13) {
-      // IE8 does not trigger the submit event when hitting enter. Submit the
-      // form if the key press was an enter and prevent the default action so
-      // the form is not submitted twice.
-      event.preventDefault();
-      this.submit();
-    }
-   }
-
    function onSubmit() {
      if (!dom.hasClass("body", "submit_disabled") && this.validate()) {
        this.submit();
@@ -69,7 +59,6 @@ BrowserID.Modules.PageModule = (function() {
       self.options = options || {};
 
       self.bind("form", "submit", cancelEvent(onSubmit));
-      self.bind("input", "keypress", onKeypress);
     },
 
     stop: function() {
diff --git a/resources/static/shared/provisioning.js b/resources/static/shared/provisioning.js
index 177ad14a68e0b076456d601968817301cfdccdd8..fa91170c2ea3941fa2c59b5b1b53ff748125f2d7 100644
--- a/resources/static/shared/provisioning.js
+++ b/resources/static/shared/provisioning.js
@@ -72,7 +72,7 @@ BrowserID.Provisioning = (function() {
       trans.delayReturn(true);
       jwcrypto.generateKeypair({algorithm: "DS", keysize: BrowserID.KEY_LENGTH}, function(err, kp) {
         keypair = kp;
-        trans.complete(keypair.publicKey.toSimpleObject());
+        trans.complete(keypair.publicKey.serialize());
       });
     });
 
diff --git a/resources/static/shared/screens.js b/resources/static/shared/screens.js
index 0ce5cd13392214a46ad7c22c839945c284b854cd..e45daaea63ce87b41b4759c3497852602b78fbe3 100644
--- a/resources/static/shared/screens.js
+++ b/resources/static/shared/screens.js
@@ -15,11 +15,13 @@ BrowserID.Screens = (function() {
       show: function(template, vars) {
         renderer.render(target + " .contents", template, vars);
         dom.addClass(BODY, className);
+        dom.fireEvent(window, "resize");
         this.visible = true;
       },
 
       hide: function() {
         dom.removeClass(BODY, className);
+        dom.fireEvent(window, "resize");
         this.visible = false;
       }
     }
diff --git a/resources/static/shared/storage.js b/resources/static/shared/storage.js
index c4f7b0f741b4b9dbc450396f3c90147f882edd39..b195e760191606371425e2f2f218eb2a6ad0dcc6 100644
--- a/resources/static/shared/storage.js
+++ b/resources/static/shared/storage.js
@@ -37,23 +37,40 @@ BrowserID.Storage = (function() {
     if (window.console && console.error) console.error(msg);
   }
 
-  function prepareDeps() {
-    if (!jwcrypto) {
-      jwcrypto = require("./jwcrypto");
-    }
-  }
-
   function storeEmails(emails) {
     storage.emails = JSON.stringify(emails);
   }
 
   function clear() {
     storage.removeItem("emails");
-    storage.removeItem("tempKeypair");
     storage.removeItem("siteInfo");
     storage.removeItem("managePage");
   }
 
+  // initialize all localStorage values to default if they are unset.
+  // this function is only neccesary on IE8 where there are localStorage
+  // synchronization issues between different browsing contexts, however
+  // it's intended to avoid IE8 specific bugs from being introduced.
+  // see issue #1637
+  function setDefaultValues() {
+    _.each({
+      emailToUserID: {},
+      emails: {},
+      interaction_data: {},
+      loggedIn: {},
+      main_site: {},
+      managePage: {},
+      returnTo: null,
+      siteInfo: {},
+      stagedOnBehalfOf: null,
+      usersComputer: {}
+    }, function(defaultVal, key) {
+      if (!storage[key]) {
+        storage[key] = JSON.stringify(defaultVal);
+      }
+    });
+  }
+
   function getEmails() {
     try {
       var emails = JSON.parse(storage.emails || "{}");
@@ -567,6 +584,13 @@ BrowserID.Storage = (function() {
      */
     clear: clear,
     setReturnTo: setReturnTo,
-    getReturnTo: getReturnTo
+    getReturnTo: getReturnTo,
+    /**
+     * Set all used storage values to default if they are unset.  This function
+     * is required for proper localStorage sync between different browsing contexts,
+     * see issue #1637 for full details.
+     * @method setDefaultValues
+     */
+    setDefaultValues: setDefaultValues
   };
 }());
diff --git a/resources/static/test/cases/controllers/dialog.js b/resources/static/test/cases/controllers/dialog.js
index 088e182caa3aa417a81206efc52305fa10c0ae32..1f0f7ffd1ac497d3a62b4922394a24d062f20e94 100644
--- a/resources/static/test/cases/controllers/dialog.js
+++ b/resources/static/test/cases/controllers/dialog.js
@@ -501,7 +501,26 @@
     });
   });
 
-  asyncTest("get with absolute path - allowed URL but it must be properly escaped", function() {
+  asyncTest("get with absolute path and http RP - not allowed", function() {
+    createController({
+      ready: function() {
+        mediator.subscribe("start", function(msg, info) {
+          ok(false, "start should not have been called");
+        });
+
+        var siteLogo = '/i/card.png';
+        var retval = controller.get(HTTP_TEST_DOMAIN, {
+          siteLogo: siteLogo
+        });
+
+        equal(retval, "only https sites can specify a siteLogo", "expected error");
+        testErrorVisible();
+        start();
+      }
+    });
+  });
+
+  asyncTest("get with absolute path and https RP - allowed URL but is properly escaped", function() {
     createController({
       ready: function() {
         var startInfo;
@@ -510,12 +529,12 @@
         });
 
         var siteLogo = '/i/card.png" onerror="alert(\'xss\')" <script>alert(\'more xss\')</script>';
-        var retval = controller.get(HTTP_TEST_DOMAIN, {
+        var retval = controller.get(HTTPS_TEST_DOMAIN, {
           siteLogo: siteLogo
         });
 
         testHelpers.testObjectValuesEqual(startInfo, {
-          siteLogo: encodeURI(HTTP_TEST_DOMAIN + siteLogo)
+          siteLogo: encodeURI(HTTPS_TEST_DOMAIN + siteLogo)
         });
         equal(typeof retval, "undefined", "no error expected");
         testErrorNotVisible();
diff --git a/resources/static/test/cases/pages/about.js b/resources/static/test/cases/pages/about.js
new file mode 100644
index 0000000000000000000000000000000000000000..298642ce39fc66e35c73f04e9a6be107b781ebdd
--- /dev/null
+++ b/resources/static/test/cases/pages/about.js
@@ -0,0 +1,33 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global test: true, start: true, module: true, ok: true, equal: true, BrowserID:true */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+(function() {
+  "use strict";
+
+  var bid = BrowserID,
+      testHelpers = bid.TestHelpers,
+      controller;
+
+  module("pages/about", {
+    setup: function() {
+      testHelpers.setup();
+      bid.Renderer.render("#page_head", "site/about", {});
+    },
+    teardown: function() {
+      testHelpers.teardown();
+    }
+  });
+
+  function createController(options, callback) {
+    controller = BrowserID.about.create();
+    controller.start(options);
+  }
+
+  test("start - no errors", function() {
+    createController({});
+    ok(controller, "controller created");
+  });
+
+}());
diff --git a/resources/static/test/cases/pages/verify_secondary_address.js b/resources/static/test/cases/pages/verify_secondary_address.js
index 0d74afe90af7dc39c853bc3f8860a11adfbac517..ac4ddaa8b832c09fe5a03cd19c8a49a29fdcd37e 100644
--- a/resources/static/test/cases/pages/verify_secondary_address.js
+++ b/resources/static/test/cases/pages/verify_secondary_address.js
@@ -13,6 +13,7 @@
       dom = bid.DOM,
       testHelpers = bid.TestHelpers,
       testHasClass = testHelpers.testHasClass,
+      testVisible = testHelpers.testVisible,
       validToken = true,
       controller,
       config = {
@@ -56,7 +57,7 @@
   }
 
   function testCannotConfirm() {
-    ok($("#cannotconfirm").is(":visible"), "cannot confirm box is visible");
+    testHelpers.testErrorVisible();
   }
 
   test("start with missing token", function() {
@@ -70,22 +71,21 @@
     equal(error, "missing config option: token", "correct error thrown");
   });
 
-  asyncTest("no password: start with good token and site", function() {
+  asyncTest("valid token, no password necessary - verify user and show site info", function() {
     var returnTo = "https://test.domain/path";
     storage.setReturnTo(returnTo);
 
     createController(config, function() {
-      testEmail();
-      ok($(".siteinfo").is(":visible"), "siteinfo is visible when we say what it is");
-      equal($(".website:nth(0)").text(), returnTo, "website is updated");
+      testVisible("#congrats");
       testHasClass("body", "complete");
+      equal($(".website").text(), returnTo, "website is updated");
       equal(doc.location.href, returnTo, "redirection occurred to correct URL");
       equal(storage.getLoggedIn("https://test.domain"), "testuser@testuser.com", "logged in status set");
       start();
     });
   });
 
-  asyncTest("no password: start with good token and nosite", function() {
+  asyncTest("valid token, no password necessary, no saved site info - verify user but do not show site info", function() {
     createController(config, function() {
       testEmail();
       equal($(".siteinfo").is(":visible"), false, "siteinfo is not visible without having it");
@@ -94,7 +94,7 @@
     });
   });
 
-  asyncTest("no password: start with bad token", function() {
+  asyncTest("invalid token - show cannot confirm error", function() {
     xhr.useResult("invalid");
 
     createController(config, function() {
@@ -103,7 +103,7 @@
     });
   });
 
-  asyncTest("no password: start with emailForVerficationToken XHR failure", function() {
+  asyncTest("valid token with xhr error - show error screen", function() {
     xhr.useResult("ajaxError");
     createController(config, function() {
       testHelpers.testErrorVisible();
@@ -156,88 +156,4 @@
     });
   });
 
-  asyncTest("must set password, successful login", function() {
-    xhr.useResult("needsPassword");
-    createController(config, function() {
-      xhr.useResult("valid");
-
-      $("#password").val("password");
-      $("#vpassword").val("password");
-
-      testHasClass("body", "enter_password");
-      testHasClass("body", "enter_verify_password");
-
-      controller.submit(function(status) {
-        equal(status, true, "correct status");
-        testHasClass("body", "complete");
-        start();
-      });
-    });
-  });
-
-  asyncTest("must set password, too short a password", function() {
-    xhr.useResult("needsPassword");
-    createController(config, function() {
-      xhr.useResult("valid");
-
-      $("#password").val("pass");
-      $("#vpassword").val("pass");
-
-      controller.submit(function(status) {
-        equal(status, false, "correct status");
-        testHelpers.testTooltipVisible();
-        start();
-      });
-    });
-  });
-
-  asyncTest("must set password, too long a password", function() {
-    xhr.useResult("needsPassword");
-    createController(config, function() {
-      xhr.useResult("valid");
-
-      var pass = testHelpers.generateString(81);
-      $("#password").val(pass);
-      $("#vpassword").val(pass);
-
-      controller.submit(function(status) {
-        equal(status, false, "correct status");
-        testHelpers.testTooltipVisible();
-        start();
-      });
-    });
-  });
-
-  asyncTest("must set password, missing verification password", function() {
-    xhr.useResult("needsPassword");
-    createController(config, function() {
-      xhr.useResult("valid");
-
-      $("#password").val("password");
-      $("#vpassword").val("");
-
-      controller.submit(function(status) {
-        equal(status, false, "correct status");
-        testHelpers.testTooltipVisible();
-        start();
-      });
-    });
-  });
-
-  asyncTest("must set password, mismatched passwords", function() {
-    xhr.useResult("needsPassword");
-    createController(config, function() {
-      xhr.useResult("valid");
-
-      $("#password").val("password");
-      $("#vpassword").val("password1");
-
-      controller.submit(function(status) {
-        equal(status, false, "correct status");
-        testHelpers.testTooltipVisible();
-        start();
-      });
-    });
-  });
-
 }());
diff --git a/resources/static/test/cases/shared/modules/interaction_data.js b/resources/static/test/cases/shared/modules/interaction_data.js
index f2b6b675d0ae32888178c1e63639f8f270c3e7dd..35940a19c9bd04e043467b3a9ea3d1df6a5c8e2b 100644
--- a/resources/static/test/cases/shared/modules/interaction_data.js
+++ b/resources/static/test/cases/shared/modules/interaction_data.js
@@ -235,4 +235,15 @@
     });
   });
 
+  asyncTest("timestamp rounded to 10 minute intervals", function() {
+    var TEN_MINS_IN_MS = 10 * 60 * 1000;
+    createController();
+    network.withContext(function() {
+      var timestamp = controller.getCurrent().timestamp;
+      ok(timestamp, "a timestamp has been passed: " + timestamp);
+      equal(timestamp % TEN_MINS_IN_MS, 0, "timestamp has been rounded to a 10 minute interval");
+      start();
+    });
+  });
+
 }());
diff --git a/resources/static/test/cases/shared/modules/page_module.js b/resources/static/test/cases/shared/modules/page_module.js
index 0544d20f0e97f9dd9e4bce4a2e6fdc2e6350ba32..ebd90fdba6b1f573a564ed409aca4196a781926a 100644
--- a/resources/static/test/cases/shared/modules/page_module.js
+++ b/resources/static/test/cases/shared/modules/page_module.js
@@ -200,33 +200,5 @@
     equal(submitCalled, true, "submit permitted to complete");
   });
 
-  test("form is submitted once 'enter keypress' event", function() {
-    createController();
-    controller.renderDialog("test_template_with_input", {
-      title: "Test title",
-      message: "Test message"
-    });
-
-    controller.start();
-
-
-    var submitCalled = 0;
-    controller.submit = function() {
-      submitCalled++;
-    };
-
-    // synthesize the entire series of key* events so we replicate the behavior
-    // of keyboard interaction.
-    var e = jQuery.Event("keydown", { keyCode: 13, which: 13 });
-    $("#templateInput").trigger(e);
-
-    var e = jQuery.Event("keyup", { keyCode: 13, which: 13 });
-    $("#templateInput").trigger(e);
-
-    var e = jQuery.Event("keypress", { keyCode: 13, which: 13 });
-    $("#templateInput").trigger(e);
-
-    equal(submitCalled, 1, "submit called a single time");
-  });
 }());
 
diff --git a/resources/static/test/testHelpers/helpers.js b/resources/static/test/testHelpers/helpers.js
index 0a7130ef5fc565d31ee50761f104cad7d28043d2..ed90066d39a1fdf4438117acbea6bcf7ce7f2823 100644
--- a/resources/static/test/testHelpers/helpers.js
+++ b/resources/static/test/testHelpers/helpers.js
@@ -221,8 +221,7 @@ BrowserID.TestHelpers = (function() {
     },
 
     testHasClass: function(selector, className, msg) {
-      ok($(selector).hasClass(className),
-          selector + " has className " + className + " - " + msg);
+      ok($(selector).hasClass(className), msg || selector + " has className: " + className);
     },
 
     testUndefined: function(toTest, msg) {
@@ -231,6 +230,10 @@ BrowserID.TestHelpers = (function() {
 
     testNotUndefined: function(toTest, msg) {
       notEqual(typeof toTest, "undefined", msg || "object is defined");
+    },
+
+    testVisible: function(selector, msg) {
+      ok($(selector).is(":visible"), msg || selector + " should be visible");
     }
 
   };
diff --git a/resources/views/about.ejs b/resources/views/about.ejs
index a7eb8dea4e292cea91e2080126587a176305d933..dfdeba036ae1e32c4470c9530e9b96b6a31d2792 100644
--- a/resources/views/about.ejs
+++ b/resources/views/about.ejs
@@ -3,38 +3,46 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <div id="content" class="display_always">
-    <div id="about">
-
-        <h1 class="center">
-          <strong>Persona</strong> is an <strong>easier</strong> way to sign in
-        </h1>
-
-
-        <div class="row one cf">
-            <img src="/i/tutorial_1.png">
-            <div>
-                <div class="steps"></div> On your favorite website that supports Persona, Click the &lsquo;Sign In&rsquo; button
-            </div>
-        </div>
-
-        <div class="row two cf">
-            <img src="/i/tutorial_2.png">
-            <div>
-                <div class="steps"></div> Select your preferred Email
-            </div>
-
-        </div>
-
-        <div class="row three cf">
-            <img src="/i/tutorial_3.png">
-            <div>
-                <div class="steps"></div> No passwords necessary, you're done!
-            </div>
-        </div>
-
-        <div class="row cf center">
-            If you need some help with Persona, head over to our <a href="https://support.mozilla.com/en-US/kb/what-browserid-and-how-does-it-work" target="_blank">support center</a>.
-        </div>
-    </div>
+    <div class="about">
+        <section class="simple-signon">
+            <h2 class="title">Simplified sign-on.</h2>
+            <article class="blurb">
+                <div class="info first">
+                    <h1>Persona replaces multiple passwords</h1>
+                    <p>Sites such as <a href="http://crossword.thetimes.co.uk/">The Times Crossword</a>, <a href="http://openphoto.net/">OpenPhoto</a> and <a href="https://www.voo.st/">Voost</a> use Persona instead of usernames to sign you in.</p><p>This means you only need one password to sign in to many sites.</p>
+                </div>
+
+                <div class="graphic">
+                    <img src="i/one-password-graphic.png" alt="One password to rule them all.">
+                </div>
+            </article>
+
+            <article class="blurb flexible">
+                <div class="graphic first">
+                    <img src="i/flexible-graphic.png" alt="Use multiple email addresses">
+                </div>
+
+                <div class="info">
+                    <h1>Persona is flexible</h1>
+                    <p>Within Persona, your identity is your email address. You can use as many email addresses as you want, but you still only need one password.</p>
+                </div>
+            </article>
+        </section>
+
+        <section class="privacy">
+            <h2 class="title">Real privacy.</h2>
+
+            <article class="blurb half first" style="min-height: 195px; ">
+                <h1>Persona is proudly non-profit for you</h1>
+                <p>Persona is developed by Mozilla, a not-for-profit company trusted throughout the Web community. Our goal is to create technologies that balance an open Web platform with people’s privacy.</p>
+            </article>
+            <article class="blurb half">
+                <h1>Persona preserves your privacy</h1>
+                <p>Persona does not track your activity around the Web. It creates a wall between signing you in and what you do once you’re there. The history of what sites you visit is stored only on your own computer.</p>
+            </article>
+        </section>
+
+        <a href="https://developer.mozilla.org/en/BrowserID/Quick_Setup" class="developers"><img src="i/developers-link.png" alt="Persona for developers"><span>Implement Persona on your site </span>Developer guides and API documentation</a>
+    </div><!-- #dashboard -->
 </div>
 
diff --git a/resources/views/add_email_address.ejs b/resources/views/add_email_address.ejs
index 9a4128aeee67fac5702ae45de569cd952d41fe4a..f56a6caf70bff61116465caf50941f13d20b2bec 100644
--- a/resources/views/add_email_address.ejs
+++ b/resources/views/add_email_address.ejs
@@ -2,15 +2,12 @@
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
-<div id="hAlign" class="display_always">
+<div id="hAlign">
     <div id="vAlign">
-        <ul class="notifications">
-            <li class="notification error" id="cannotconfirm"><%= gettext('Error encountered while attempting to confirm your address. Have you previously verified this address?') %></li>
-            <li class="notification error" id="cannotcomplete"><%= gettext('Error encountered trying to complete registration.') %></li>
-        </ul>
-
-        <form id="signUpForm" class="cf">
-            <p class="hint siteinfo"><%= gettext('Finish signing into:') %> <strong><span class="website"></span></strong></p>
+        <form id="signUpForm" class="cf password_entry">
+            <p class="hint siteinfo">
+              <%= gettext('Finish signing into:') %> <strong class="website"></strong>
+            </p>
 
             <h1><%= gettext('Email Verification') %></h1>
 
@@ -19,7 +16,8 @@
                     <label for="email"><%= gettext('Email Address') %></label>
                     <input class="youraddress" id="email" placeholder="<%= gettext('Your Email') %>" type="email" value="" disabled="disabled" maxlength="254" />
                 </li>
-                <li class="password_entry">
+
+                <li>
                     <label for="password"><%= gettext('Password') %></label>
                     <input id="password" placeholder="<%= gettext('Your Password') %>" type="password" autofocus maxlength=80 />
 
@@ -27,35 +25,16 @@
                       <%= gettext('Password is required.') %>
                     </div>
 
-                    <div class="tooltip" id="password_length" for="password">
-                      <%= gettext('Password must be between 8 and 80 characters long.') %>
-                    </div>
-
                     <div id="cannot_authenticate" class="tooltip" for="password">
                       <%= gettext('The account cannot be verified with this username and password.') %>
                     </div>
                 </li>
-
-                <li class="password_entry" id="verify_password">
-                    <label for="vpassword"><%= gettext('Verify Password') %></label>
-                    <input id="vpassword" placeholder="<%= gettext('Repeat Password') %>" type="password" maxlength="80">
-
-                    <div id="vpassword_required" class="tooltip" for="vpassword">
-                      <%= gettext('Verification password is required.') %>
-                    </div>
-
-                    <div class="tooltip" id="passwords_no_match" for="vpassword">
-                      <%= gettext ('Passwords do not match.') %>
-                    </div>
-
-                </li>
             </ul>
 
             <div class="submit cf password_entry">
                 <button><%= gettext('finish') %></button>
             </div>
 
-
         </form>
 
         <div id="congrats">
diff --git a/resources/views/dialog_layout.ejs b/resources/views/dialog_layout.ejs
index 6ca9a9ade30ed647c0d717a819ed21a21009c58c..4b12722611e3681ead1acbc0eb7e202eff51bad5 100644
--- a/resources/views/dialog_layout.ejs
+++ b/resources/views/dialog_layout.ejs
@@ -20,7 +20,9 @@
 </head>
   <body class="waiting">
       <header id="header">
-         <a href="#" id="showDevelopment">&nbsp;</a>
+         <% if (useJavascript !== false && enable_development_menu) { %>
+           <a href="#" id="showDevelopment">&nbsp;</a>
+         <% } %>
          <h1><a class="home" target="_blank" href="/">Mozilla Persona Home</a></h1>
       </header>
 
diff --git a/resources/views/layout.ejs b/resources/views/layout.ejs
index 91e873f19e3636e98ae79402aad503636d46afcd..d01aa64f819fb1fa741e229cd64231fe48afc183 100644
--- a/resources/views/layout.ejs
+++ b/resources/views/layout.ejs
@@ -18,7 +18,9 @@
   <title><%= format(gettext("Mozilla Persona: %s"), [title]) %></title>
 </head>
 <body class="loading">
-<a href="#" id="showDevelopment">&nbsp;</a>
+<% if (enable_development_menu) { %>
+  <a href="#" id="showDevelopment">&nbsp;</a>
+<% } %>
 <div id="errorBackground"></div>
 
 <div id="wrapper">
diff --git a/resources/views/test.ejs b/resources/views/test.ejs
index 7f05abc2301be6417ad5444f2932d358047a7d19..da1332fe11b80a0fe7d2547c457495081ea4b377 100644
--- a/resources/views/test.ejs
+++ b/resources/views/test.ejs
@@ -143,6 +143,7 @@
     <script src="/pages/manage_account.js"></script>
     <script src="/pages/signin.js"></script>
     <script src="/pages/signup.js"></script>
+    <script src="/pages/about.js"></script>
 
     <script src="testHelpers/helpers.js"></script>
 
@@ -179,6 +180,7 @@
     <script src="cases/pages/signin.js"></script>
     <script src="cases/pages/signup.js"></script>
     <script src="cases/pages/manage_account.js"></script>
+    <script src="cases/pages/about.js"></script>
 
     <script src="cases/resources/internal_api.js"></script>
     <script src="cases/resources/helpers.js"></script>
diff --git a/resources/views/verify_email_address.ejs b/resources/views/verify_email_address.ejs
index d033b9417e2b39158d875138bb6ec24aca1a0286..118c70df793807bfb6586e04eb955624061bbe5a 100644
--- a/resources/views/verify_email_address.ejs
+++ b/resources/views/verify_email_address.ejs
@@ -2,15 +2,13 @@
       License, v. 2.0. If a copy of the MPL was not distributed with this
       file, You can obtain one at http://mozilla.org/MPL/2.0/. */ %>
 
-<div id="hAlign" class="display_always">
+<div id="hAlign">
     <div id="vAlign">
-        <ul class="notifications">
-            <li class="notification error" id="cannotconfirm"><%= gettext('There was a problem with your signup link. Has this address already been registered?') %></li>
-            <li class="notification error" id="cannotcomplete"><%= gettext('Error encountered trying to complete registration.') %></li>
-        </ul>
+        <form id="signUpForm" class="cf password_entry">
+            <p class="hint siteinfo">
+              <%= gettext('Finish signing into:') %> <strong class="website"></strong>
+            </p>
 
-        <form id="signUpForm" class="cf">
-            <p class="hint siteinfo"><%= gettext('Finish signing into:') %> <strong><span class="website"></span></strong></p>
             <h1><%= gettext('Last step!') %></h1>
 
             <ul class="inputs">
@@ -19,7 +17,7 @@
                     <input class="youraddress" id="email" placeholder="<%= gettext('Your Email') %>" type="email" value="" disabled="disabled" maxlength="254" />
                 </li>
 
-                <li class="password_entry">
+                <li>
                     <label for="password"><%= gettext('Password') %></label>
                     <input id="password" placeholder="<%= gettext('Your Password') %>" type="password" autofocus maxlength=80 />
 
@@ -27,28 +25,10 @@
                       <%= gettext('Password is required.') %>
                     </div>
 
-                    <div class="tooltip" id="password_length" for="password">
-                      <%= gettext('Password must be between 8 and 80 characters long.') %>
-                    </div>
-
                     <div id="cannot_authenticate" class="tooltip" for="password">
                       <%= gettext('The account cannot be verified with this username and password.') %>
                     </div>
                 </li>
-
-                <li class="password_entry" id="verify_password">
-                    <label for="vpassword"><%= gettext('Verify Password') %></label>
-                    <input id="vpassword" placeholder="<%= gettext('Repeat Password') %>" type="password" maxlength="80">
-
-                    <div id="vpassword_required" class="tooltip" for="vpassword">
-                      <%= gettext('Verification password is required.') %>
-                    </div>
-
-                    <div class="tooltip" id="passwords_no_match" for="vpassword">
-                      <%= gettext ('Passwords do not match.') %>
-                    </div>
-
-                </li>
             </ul>
 
             <div class="submit cf password_entry">
diff --git a/scripts/browserid.spec b/scripts/browserid.spec
index 5429472cb4112474e621c0da403e096cd0e73664..6bb044ea89e314de69c2ad4b6b31dcbdb7bfdcd2 100644
--- a/scripts/browserid.spec
+++ b/scripts/browserid.spec
@@ -1,7 +1,7 @@
 %define _rootdir /opt/browserid
 
 Name:          browserid-server
-Version:       0.2012.06.22
+Version:       0.2012.07.06
 Release:       1%{?dist}_%{svnrev}
 Summary:       BrowserID server
 Packager:      Pete Fritchman <petef@mozilla.com>
diff --git a/scripts/run_locally.js b/scripts/run_locally.js
index 2a0b9bca58e6d832abee7774b5fa4f1696f58f9a..346fa7dda7dff3152af234e9f9fef5261e0d01fd 100755
--- a/scripts/run_locally.js
+++ b/scripts/run_locally.js
@@ -112,11 +112,19 @@ function runDaemon(daemon, cb) {
   });
 };
 
+// start all daemons except the router in parallel
 var daemonNames = Object.keys(daemonsToRun);
-function runNextDaemon() {
-  if (daemonNames.length) runDaemon(daemonNames.shift(), runNextDaemon);
-}
-runNextDaemon();
+daemonNames.splice(daemonNames.indexOf('router'), 1);
+
+var numDaemonsRun = 0;
+daemonNames.forEach(function(dn) {
+  runDaemon(dn, function() {
+    if (++numDaemonsRun === daemonNames.length) {
+      // after all daemons are up and running, start the router
+      runDaemon('router', function() { });
+    }
+  });
+});
 
 process.on('SIGINT', function () {
   console.log('\nSIGINT recieved! trying to shut down gracefully...');
diff --git a/tests/conformance-test.js b/tests/conformance-test.js
old mode 100644
new mode 100755
diff --git a/tests/data/lib.jshintrc b/tests/data/lib.jshintrc
new file mode 100644
index 0000000000000000000000000000000000000000..fa8c0257f97f0e09d30c121ae4930ac6bad0394d
--- /dev/null
+++ b/tests/data/lib.jshintrc
@@ -0,0 +1,8 @@
+{
+  "undef": true,
+  "node": true,
+  "es5": true,
+  "esnext": true,
+  "strict": false,
+  "sub": true
+}
diff --git a/tests/heartbeat-test.js b/tests/heartbeat-test.js
new file mode 100755
index 0000000000000000000000000000000000000000..815f97120003b062da83fe9d898acc0fc16ff471
--- /dev/null
+++ b/tests/heartbeat-test.js
@@ -0,0 +1,122 @@
+#!/usr/bin/env node
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+require('./lib/test_env.js');
+
+const assert =
+require('assert'),
+vows = require('vows'),
+start_stop = require('./lib/start-stop.js'),
+wsapi = require('./lib/wsapi.js'),
+db = require('../lib/db.js'),
+config = require('../lib/configuration.js'),
+bcrypt = require('bcrypt'),
+http = require('http');
+
+var suite = vows.describe('heartbeat');
+
+// disable vows (often flakey?) async error behavior
+suite.options.error = false;
+
+start_stop.addStartupBatches(suite);
+
+// test deep and shallow heartbeats work for all processes
+[ 10004, 10002, 10003, 10004, 10007 ].forEach(function(port) {
+  [ true, false ].forEach(function(shallow) {
+    var testName = "shallow heartbeat check for 127.0.0.1:" + port;
+    suite.addBatch({
+      testName: {
+        topic: function() {
+          var self = this;
+
+          var req = http.get({
+            host: '127.0.0.1',
+            port: port,
+            path: '/__heartbeat__' + ( shallow ? "" : "?deep=true")
+          }, function(res) {
+            self.callback(null, res.statusCode);
+            req.abort();
+          }).on('error', function(e) {
+            self.callback(e, null);
+            req.abort();
+          });
+        },
+        "works":     function(err, code) {
+          assert.strictEqual(err, null);
+          assert.equal(code, 200);
+        }
+      }
+    });
+  });
+});
+
+// now let's SIGSTOP the browserid process and verify that the router's
+// deep heartbeat fails within 11s
+suite.addBatch({
+  "stopping the browserid process": {
+    topic: function() {
+      process.kill(parseInt(process.env['BROWSERID_PID'], 10), 'SIGSTOP');      
+      this.callback();
+    },
+    "then doing a deep __heartbeat__ on router": {
+      topic: function() {
+        var self = this;
+        var start = new Date();
+        var req = http.get({
+          host: '127.0.0.1',
+          port: 10002,
+          path: '/__heartbeat__?deep=true'
+        }, function(res) {
+          self.callback(null, res.statusCode, start);
+          req.abort();
+        }).on('error', function(e) {
+          self.callback(e, null);
+          req.abort();
+        });
+      },
+      "fails": function(e, code, start) {
+        assert.ok(!e);
+        assert.strictEqual(500, code);
+      },
+      "takes about 5s": function(e, code, start) {
+        assert.ok(!e);
+        var elapsedMS = new Date() - start;
+        assert.ok(3000 < elapsedMS < 7000);
+      },
+      "but upon SIGCONT": {
+        topic: function(e, code) {
+          process.kill(parseInt(process.env['BROWSERID_PID'], 10), 'SIGCONT');      
+          this.callback();
+        },
+        "a deep heartbeat": {
+          topic: function() {
+            var self = this;
+            var req = http.get(
+              { host: '127.0.0.1', port: 10002, path: '/__heartbeat__?deep=true'},
+              function(res) {
+                self.callback(null, res.statusCode);
+                req.abort();
+              }).on('error', function(e) {
+                self.callback(e, null);
+                req.abort();
+              });
+          },
+          "works": function(err, code) {
+            assert.ok(!err);
+            assert.strictEqual(200, code);
+          }
+        }
+      }
+    }
+  }
+});
+
+
+start_stop.addShutdownBatches(suite);
+
+// run or export the suite.
+if (process.argv[1] === __filename) suite.run();
+else suite.export(module);
diff --git a/tests/jshint-test.js b/tests/jshint-test.js
new file mode 100755
index 0000000000000000000000000000000000000000..e0971e0887ef6a66ae046cb7ed413554eb4e98c0
--- /dev/null
+++ b/tests/jshint-test.js
@@ -0,0 +1,46 @@
+#!/usr/bin/env node
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+require('./lib/test_env.js');
+
+// add lib/ to the require path
+
+const
+assert = require('assert'),
+vows = require('vows'),
+fs = require('fs'),
+path = require('path'),
+exec = require('child_process').exec;
+
+var suite = vows.describe('jshint');
+var jshintPath = '../node_modules/jshint/bin/hint';
+
+// disable vows (often flakey?) async error behavior
+suite.options.error = false;
+
+suite.addBatch({
+  "run jshint on the lib directory": {
+    topic: function () {
+      var cmd = jshintPath + ' --config ./data/lib.jshintrc ../lib/ | grep "not defined"';
+      var child = exec(cmd, {cwd: path.resolve(__dirname)}, this.callback);
+    },
+    "jshint is found and runs" : function (error, stdout, stderr) {
+      // NOTE: until we clean up jshint errors and agree on what options,
+      // we only verify that the program was found and runs, but not that
+      // it is completely clean and error free in jshint's opinion.
+      assert.ok(!error || error.toString().indexOf('No such') === -1);
+    },
+    "no globals are created or referenced" : function (error, stdout, stderr) {
+      var errors = stdout.split("\n").length - 1;
+      assert.strictEqual(errors, 0);
+    }
+  }
+});
+
+
+// run or export the suite.
+if (process.argv[1] === __filename) suite.run();
+else suite.export(module);
diff --git a/tests/lib/start-stop.js b/tests/lib/start-stop.js
index b73aa2ea8f4951a1c615788e750259ab93bd7ece..742fb032745d0607052ff7b19de83157dff95e8a 100644
--- a/tests/lib/start-stop.js
+++ b/tests/lib/start-stop.js
@@ -46,10 +46,13 @@ function setupProc(proc) {
         }
       }
       var tokenRegex = new RegExp('token=([A-Za-z0-9]+)$', 'm');
+      var pidRegex = new RegExp('^spawned (\\w+) \\(.*\\) with pid ([0-9]+)$');
 
       if (!sentReady && /^router.*127\.0\.0\.1:10002$/.test(x)) {
         exports.browserid.emit('ready');
         sentReady = true;
+      } else if (!sentReady && (m = pidRegex.exec(x))) {
+        process.env[m[1].toUpperCase() + "_PID"] = m[2]; 
       } else if (m = tokenRegex.exec(x)) {
         if (!(/forwarding request:/.test(x))) {
           tokenStack.push(m[1]);
diff --git a/tests/simple-stage-user-utf8-password.js b/tests/simple-stage-user-utf8-password.js
old mode 100644
new mode 100755