diff --git a/.awsbox.json b/.awsbox.json
index 3f0ea469b9b1502e682f2f78eed9b5a0d654879f..81feaa18fb0d95f4811a93e3e43c1a81af405cad 100644
--- a/.awsbox.json
+++ b/.awsbox.json
@@ -5,7 +5,8 @@
     "bin/dbwriter",
     "bin/keysigner",
     "bin/verifier",
-    "bin/browserid"
+    "bin/browserid",
+    "bin/static"
   ],
   "env": {
     "CONFIG_FILES": "$HOME/code/config/production.json,$HOME/code/config/aws.json,$HOME/config.json"
diff --git a/ChangeLog b/ChangeLog
index 8e07fa26f09ef9b8313c0e2d0816dc00689b0c30..a36cfb8104fb573403df2b92b89a3ba4610fff1c 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,4 +1,25 @@
-train-2012.07.06: (in progress)
+train-2012.07.20: (in progress)
+  * 
+
+train-2012.07.06:
+  * refinement of all user facing language: #1889, #1905, #1675, #1923, #1925
+  * requiredEmail feature removed: #1760
+  * A new /about page #1768
+  * watch() parameter - loggedInEmail renamed to loggedInUser #1805
+  * persona ToS / PP now displayed in dialog: #1240
+  * fix cases where redirection to website after verification would fail: #1860
+  * clean up console messages: #1518
+  * load_gen cleanup: #1278
+  * user interface improvements: #1777, #1661, #1433, #1548, #1774, #1721, #1826, #868, #1517, #1093, #1892, #1928
+  * updates to "key performance indicator": #1667, #1730
+  * test improvments: #1794, #1875, #1883
+  * code cleanup: #1778, #1756, #1748, #1849, #1852
+  * font licenses added to source tree: #1820
+  * load time performance improvements: #1793, #1851, #1861
+  * improvments to email provider API ("primary"): #1502
+  * security improvments - better random numbers: #1788
+  * Fix Persona on Windows Metro: #1867
+  * Fix dialog layout when rendered in a native webview on iOS: #1517
 
 train-2012.06.22:
   * browserid.org now redirects to login.persona.org, all URLs are updated: #1743
diff --git a/README.md b/README.md
index 4149335887dc9bb6e7b221fe950b722ffd181f17..fbf2a687b763b88df249b06553e2b658dba07377 100644
--- a/README.md
+++ b/README.md
@@ -2,11 +2,11 @@
    - 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/. -->
 
-Here lives the [BrowserID][] implementation.  BrowserID is an implementation of the
-[verified email protocol][VEP].
+Here lives the [Persona][] login implementation. This is an implementation of the
+[BrowserID protocol][].
 
-  [BrowserID]:https://login.persona.org
-  [VEP]:https://wiki.mozilla.org/Labs/Identity/VerifiedEmailProtocol
+  [Persona]: https://browserid.org
+  [BrowserID protocol]: https://github.com/mozilla/id-specs
 
 This repository contains several distinct things related to BrowserID:
 
@@ -16,33 +16,58 @@ This repository contains several distinct things related to BrowserID:
   * **the login.persona.org website** - the templates, css, and javascript that make up the visible part of login.persona.org
   * **the javascript/HTML dialog & include library** - this is include.js and the code that it includes, the bit that someone using browserid will include.
 
-## Dependencies
+## Getting Started
 
-Here's the software you'll need installed:
+If you want to work on the core BrowserID service, follow these instructions:
 
-* node.js (>= 0.6.2): http://nodejs.org/
+### Install Dependencies
+
+BrowserID needs the following dependencies before it can run:
+
+* node.js (>= 0.6.2): http://nodejs.org
 * npm: http://npmjs.org/ (or bundled with node in 0.6.3+)
 * libgmp3
 * git
 * g++
 
-## Getting started:
+For detailed instructions for your particular operating system, check out the `SETUP` docs in the `docs/` folder.
+
+### Configure Git
+
+The BrowserID team uses Git and GitHub for all of our collaboration, code hosting, and bug tracking. If you want to help out with core development, you'll need to sign up for a GitHub account and configure Git:
+
+1. Sign up for a GitHub account at https://github.com/
+2. Learn how to configure Git at http://help.github.com/articles/set-up-git
+3. Learn how to fork and clone a repository at https://help.github.com/articles/fork-a-repo
+
+If you'd like to use SSH keys instead of a password when you authenticate with GitHub, refer to https://help.github.com/articles/generating-ssh-keys
+
+If you'd like to contribute code back to us, please do so using a GitHub Pull Request, as we follow the "Fork and Pull" collaborative development model. You can learn about pull requests at https://help.github.com/articles/using-pull-requests
+
+### Running BrowserID Locally
+
+To run the BrowserID service locally, you must first:
+
+1. Clone the repository to your local machine.
+2. Run `npm install` from the root of your local clone.
+
+You can then start the BrowserID suite of services by running `npm start` from the root of your local clone.
+
+When you run `npm start`, it will print several URLs to your terminal. You can test that everything is working by visiting the URL for the `example` (RP) site. Look for a line like this in the terminal: `example (10361): running on http://127.0.0.1:10001`.
 
-1. install node and npm
-3. run `npm install` to install 3rd party libraries and generate keys
-3. run `npm start` to start the servers locally
-4. visit the demo application ('rp') in your web browser (url output on the console at runtime)
+You can stop the services by typing Control-C in the terminal.
 
-You can stop the servers with a Cntl-C in the terminal.
+### Staying Up to Date
 
-## Staying up to date:
+To stay up to date with BrowserID:
 
-1. `rm -Rf var node_modules`
-2. `npm install`
+1. Use `git pull` to retrieve new changes.
+2. Delete both the `var` and `node_modules` folders in the root of your local clone.
+3. Run `npm install` from the root of your local clone.
 
 ## Testing
 
-### Local Testing
+### Local testing:
 Unit tests can be run by invoking `npm test` at the top level.  At present,
 there are three classes of unit tests to be run:
 
@@ -53,7 +78,7 @@ there are three classes of unit tests to be run:
 You can control which tests are run using the `WHAT_TESTS` env var, see
 `scripts/test` for details.
 
-### Continuous Integration Testing
+### Continuous Integration Testing:
 Integration tests are done with [Travis-CI][]. It is recommended that you setup [Travis-CI][] for your BrowserID fork so that tests are automatically run when you push changes. This will give the BrowserID team confidence that your changes both function correctly and do not cause regressions in other parts of the code.  Configuration files are already included in the repo but some setup is necessary.
 
 1. Sign in to [GitHub][]
@@ -71,7 +96,7 @@ Integration tests are done with [Travis-CI][]. It is recommended that you setup
   [Travis-CI]: http://travis-ci.org
   [GitHub]: https://github.com
 
-## Development model
+## Development Model
 
 **branching & release model** - You'll notice some funky branching conventions, like the default branch is named `dev` rather than `master` as you might expect.  We're using gitflow: the approach is described in a [blog post](http://lloyd.io/applying-gitflow).
 
diff --git a/bin/browserid b/bin/browserid
index 3e88bd93f1a0194bf0c28d5504d7cec0dfa883dd..225526ab27a818edbc3f7545e0f216b088c5573f 100755
--- a/bin/browserid
+++ b/bin/browserid
@@ -25,8 +25,7 @@ heartbeat = require('../lib/heartbeat.js'),
 metrics = require('../lib/metrics.js'),
 logger = require('../lib/logging.js').logger,
 forward = require('../lib/http_forward').forward,
-shutdown = require('../lib/shutdown'),
-views = require('../lib/browserid/views.js');
+shutdown = require('../lib/shutdown');
 
 var app = undefined;
 
@@ -39,7 +38,13 @@ logger.info("browserid server starting up");
 
 // #1 - Setup health check / heartbeat middleware.
 // This is in front of logging on purpose.  see issue #537
-heartbeat.setup(app);
+heartbeat.setup(app, function(cb) {
+  // ping the database to verify we're really healthy.
+  db.ping(function(e) {
+    if (e) logger.error("database ping error: " + e);
+    cb(!e);
+  });
+});
 
 // #2 - logging!  all requests other than __heartbeat__ are logged
 app.use(express.logger({
@@ -99,15 +104,6 @@ app.use(function(req, resp, next) {
   return next();
 });
 
-var static_root = path.join(__dirname, "..", "resources", "static");
-
-app.use(cachify.setup(assets(config.get('supported_languages')),
-        {
-          prefix: config.get('cachify_prefix'),
-          production: config.get('use_minified_resources'),
-          root: static_root
-        }));
-
 // #7 - perform response substitution to support local/dev/beta environments
 // (specifically, this replaces URLs in responses, e.g. https://login.persona.org
 //  with https://login.anosrep.org)
@@ -118,40 +114,13 @@ wsapi.setup({
   forward_writes: urlparse(config.get('dbwriter_url')).validate().normalize().originOnly()
 }, app);
 
-// #9 - handle views for dynamicish content
-views.setup(app);
-
-// #10 if the BROWSERID_FAKE_VERIFICATION env var is defined, we'll include
+// #9 if the BROWSERID_FAKE_VERIFICATION env var is defined, we'll include
 // fake_verification.js.  This is used during testing only and should
 // never be included in a production deployment
 if (process.env['BROWSERID_FAKE_VERIFICATION']) {
   require('../lib/browserid/fake_verification.js').addVerificationWSAPI(app);
 }
 
-// if nothing else has caught this request, serve static files, but ensure
-// that proper vary headers are installed to prevent unwanted caching
-app.use(function(req, res, next) {
-  res.setHeader('Vary', 'Accept-Encoding,Accept-Language');
-  next();
-});
-
-// add 'Access-Control-Allow-Origin' headers to static resources that will be served
-// from the CDN.  We explicitly allow resources served from public_url to access these.
-app.use(function(req, res, next) {
-  res.on('header', function() {
-    res.setHeader("Access-Control-Allow-Origin", config.get('public_url'));
-  });
-  next();
-});
-
-// if we're not serving minified resources (local dev), then we should add
-// .ejs to the mime table so it's properly substituted.  issue #1875
-if (!config.get('use_minified_resources')) {
-  express.static.mime.types['ejs'] = 'text/html';
-}
-
-app.use(express.static(static_root));
-
 // open the databse
 db.open(config.get('database'), function (error) {
   if (error) {
@@ -180,9 +149,9 @@ db.open(config.get('database'), function (error) {
             logger.error("error creating test users - bcrypt encrypt pass: " + err);
             process.exit(1);
           }
-          var want = parseInt(process.env['CREATE_TEST_USERS']);
+          var want = parseInt(process.env['CREATE_TEST_USERS'], 10);
           var have = 0;
-          for (i = 1; i <= want; i++) {
+          for (var i = 1; i <= want; i++) {
             db.addTestUser(i + "@loadtest.domain", hash, function(err, email) {
               if (++have == want) {
                 logger.warn("created " + want + " test users");
diff --git a/bin/dbwriter b/bin/dbwriter
index c629863e2241f62b8d94e08342cd66697caffe70..f50e577de773aebaf3592c25c5a8aabb3c5cdca1 100755
--- a/bin/dbwriter
+++ b/bin/dbwriter
@@ -8,13 +8,13 @@ const
 fs = require('fs'),
 path = require('path'),
 url = require('url'),
-http = require('http');
+http = require('http'),
 urlparse = require('urlparse'),
 express = require('express'),
 wsapi = require('../lib/wsapi.js'),
-httputils = require('../lib/httputils.js');
+httputils = require('../lib/httputils.js'),
 secrets = require('../lib/secrets.js'),
-db = require('../lib/db.js');
+db = require('../lib/db.js'),
 config = require('../lib/configuration.js'),
 heartbeat = require('../lib/heartbeat.js'),
 metrics = require('../lib/metrics.js'),
@@ -49,7 +49,7 @@ app.use(express.logger({
 
 var statsd_config = config.get('statsd');
 if (statsd_config && statsd_config.enabled) {
-  logger_statsd = require("connect-logger-statsd");
+  var logger_statsd = require("connect-logger-statsd");
   app.use(logger_statsd({
     host: statsd_config.hostname || "localhost",
     port: statsd_config.port || 8125,
@@ -93,7 +93,7 @@ wsapi.setup({
 
 function doShutdown(readyForShutdownCB) {
   require('../lib/bcrypt.js').shutdown();
-  db.close(readyForShutdownCB)
+  db.close(readyForShutdownCB);
 }
 
 // open the databse
diff --git a/bin/keysigner b/bin/keysigner
index 714a4fed322de04c1971f966c1f94755ec1c4784..1d981b0c5bc2203a46673e15eef3f423920402de 100755
--- a/bin/keysigner
+++ b/bin/keysigner
@@ -42,7 +42,7 @@ app.use(express.logger({
 
 var statsd_config = config.get('statsd');
 if (statsd_config && statsd_config.enabled) {
-  logger_statsd = require("connect-logger-statsd");
+  var logger_statsd = require("connect-logger-statsd");
   app.use(logger_statsd({
     host: statsd_config.hostname || "localhost",
     port: statsd_config.port || 8125,
diff --git a/bin/router b/bin/router
index dc409f51267387c05c08a9f0f6c540854cf6c25c..1d333c310362f4d09ab8dd9b2873711b02a85f0d 100755
--- a/bin/router
+++ b/bin/router
@@ -8,7 +8,7 @@ const
 fs = require('fs'),
 path = require('path'),
 url = require('url'),
-http = require('http');
+http = require('http'),
 urlparse = require('urlparse'),
 express = require('express');
 
@@ -48,10 +48,12 @@ if (!config.get('browserid_url')) {
 // order in which middleware will be invoked as requests are processed.
 
 // #1 - Setup health check / heartbeat middleware.
+// Depends on positive health checks from browserid and static processes
 // This is in front of logging on purpose.  see issue #537
 var browserid_url = urlparse(config.get('browserid_url')).validate().normalize().originOnly();
+var static_url = urlparse(config.get('static_url')).validate().normalize().originOnly();
 heartbeat.setup(app, {
-  dependencies: [browserid_url]
+  dependencies: [browserid_url, static_url]
 });
 
 // #2 - logging!  all requests other than __heartbeat__ are logged
@@ -70,7 +72,7 @@ app.use(express.limit("10kb"));
 
 var statsd_config = config.get('statsd');
 if (statsd_config && statsd_config.enabled) {
-  logger_statsd = require("connect-logger-statsd");
+  var logger_statsd = require("connect-logger-statsd");
   app.use(logger_statsd({
     host: statsd_config.hostname || "localhost",
     port: statsd_config.port || 8125,
@@ -115,16 +117,37 @@ if (config.get('verifier_url')) {
   });
 }
 
-// handle /wsapi writes
-wsapi.setup({
-  router_mode: true,
-  forward_writes: urlparse(config.get('dbwriter_url')).validate().normalize().originOnly()
-}, app);
+// #10 if the BROWSERID_FAKE_VERIFICATION env var is defined, we'll include
+// fake_verification.js.  This is used during testing only and should
+// never be included in a production deployment
+if (process.env['BROWSERID_FAKE_VERIFICATION']) {
+  app.use(function(req, res, next) {
+    if (url.parse(req.url).pathname == '/wsapi/fake_verification') {
+      forward(
+        browserid_url+req.url, req, res,
+        function(err) {
+          if (err) {
+            logger.error("error forwarding request:", err);
+          }
+        });
+    } else {
+      return next();
+    }
+  });
+}
+
+// handle /wsapi reads/writes
+var dbwriter_url = urlparse(config.get('dbwriter_url')).validate().normalize().originOnly();
+
+wsapi.routeSetup(app, {
+  read_url: browserid_url,
+  write_url: dbwriter_url
+});
 
-// Forward all leftover requests to browserid
+//catch-all
 app.use(function(req, res, next) {
   forward(
-    browserid_url+req.url, req, res,
+    static_url+req.url, req, res,
     function(err) {
       if (err) {
         logger.error("error forwarding request:", err);
diff --git a/bin/static b/bin/static
new file mode 100755
index 0000000000000000000000000000000000000000..47af95441d6858387a0bfa09918fef5c124bdc52
--- /dev/null
+++ b/bin/static
@@ -0,0 +1,106 @@
+#!/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/. */
+
+const
+fs = require('fs'),
+path = require('path'),
+url = require('url'),
+http = require('http'),
+urlparse = require('urlparse'),
+express = require('express');
+
+const
+assets = require('../lib/static_resources').all,
+cachify = require('connect-cachify'),
+i18n = require('../lib/i18n.js'),
+wsapi = require('../lib/wsapi.js'),
+httputils = require('../lib/httputils.js'),
+secrets = require('../lib/secrets.js'),
+db = require('../lib/db.js'),
+config = require('../lib/configuration.js'),
+heartbeat = require('../lib/heartbeat.js'),
+metrics = require('../lib/metrics.js'),
+logger = require('../lib/logging.js').logger,
+forward = require('../lib/http_forward').forward,
+shutdown = require('../lib/shutdown'),
+views = require('../lib/static/views.js');
+
+var app = undefined;
+
+app = express.createServer();
+
+logger.info("static starting up");
+
+// Setup health check / heartbeat middleware.
+// This is in front of logging on purpose.  see issue #537
+heartbeat.setup(app);
+
+// logging!  all requests other than __heartbeat__ are logged
+app.use(express.logger({
+  format: config.get('express_log_format'),
+  stream: {
+    write: function(x) {
+      logger.info(typeof x === 'string' ? x.trim() : x);
+    }
+  }
+}));
+
+// #2.1 - localization
+app.use(i18n.abide({
+  supported_languages: config.get('supported_languages'),
+  default_lang: config.get('default_lang'),
+  debug_lang: config.get('debug_lang'),
+  locale_directory: config.get('locale_directory'),
+  disable_locale_check: config.get('disable_locale_check')
+}));
+
+var statsd_config = config.get('statsd');
+if (statsd_config && statsd_config.enabled) {
+  logger_statsd = require("connect-logger-statsd");
+  app.use(logger_statsd({
+    host: statsd_config.hostname || "localhost",
+    port: statsd_config.port || 8125,
+    prefix: statsd_config.prefix || "browserid.static."
+  }));
+}
+// #4 - prevent framing of everything.  content underneath that needs to be
+// framed must explicitly remove the x-frame-options
+app.use(function(req, resp, next) {
+  resp.setHeader('x-frame-options', 'DENY');
+  next();
+});
+
+var static_root = path.join(__dirname, "..", "resources", "static");
+
+// #7 - perform response substitution to support local/dev/beta environments
+// (specifically, this replaces URLs in responses, e.g. https://browserid.org
+//  with https://diresworb.org)
+config.performSubstitution(app);
+
+// #9 - handle views for dynamicish content
+views.setup(app);
+
+app.use(cachify.setup(assets(config.get('supported_languages')),
+        {
+          prefix: config.get('cachify_prefix'),
+          production: config.get('use_minified_resources'),
+          root: static_root,
+        }));
+
+
+// if nothing else has caught this request, serve static files, but ensure
+// that proper vary headers are installed to prevent unwanted caching
+app.use(function(req, res, next) {
+  res.setHeader('Vary', 'Accept-Encoding,Accept-Language');
+  next();
+});
+
+app.use(express.static(static_root));
+
+var bindTo = config.get('bind_to');
+app.listen(bindTo.port, bindTo.host, function() {
+  logger.info("running on http://" + app.address().address + ":" + app.address().port);
+});
diff --git a/bin/verifier b/bin/verifier
index d58415422bb02a7aabdfca68a478a66bf73a4b29..5596047ee2b6b86f6edc65a65361a4ec08f3685f 100755
--- a/bin/verifier
+++ b/bin/verifier
@@ -41,7 +41,7 @@ app.use(express.limit("10kb"));
 
 var statsd_config = config.get('statsd');
 if (statsd_config && statsd_config.enabled) {
-  logger_statsd = require("connect-logger-statsd");
+  var logger_statsd = require("connect-logger-statsd");
   app.use(logger_statsd({
     host: statsd_config.hostname || "localhost",
     port: statsd_config.port || 8125,
@@ -73,7 +73,7 @@ try {
 }
 
 function doVerification(req, resp, next) {
-  req.body = req.body || {}
+  req.body = req.body || {};
 
   var assertion = (req.query && req.query.assertion) ? req.query.assertion : req. body.assertion;
   var audience = (req.query && req.query.audience) ? req.query.audience : req.body.audience;
@@ -124,7 +124,7 @@ function doVerification(req, resp, next) {
       });
     }
   });
-};
+}
 
 app.post('/verify', doVerification);
 app.post('/', doVerification);
diff --git a/config/l10n-prod.json b/config/l10n-prod.json
index 48885e9dd8420516a0e717af4c776603d8da7ea0..f05c4c968ea6d4544463b02005067d09811332c0 100644
--- a/config/l10n-prod.json
+++ b/config/l10n-prod.json
@@ -1,8 +1,9 @@
 {
-"supported_languages": [
-    "ca", "cs", "cy", "da", "de", "el", "en-US", "eo", "es", "et",
-    "eu", "fr", "fy", "ga", "gd", "gl", "he", "hr", "hu", "id",
-    "it", "ja", "ko", "lij", "nb-NO", "nl", "pa", "pl", "pt-BR", "rm",
-    "ru", "sk", "sl", "sq", "sr", "sv", "tr", "zh-CN", "zh-TW"
+  "supported_languages": [
+    "af", "bg", "ca", "cs", "cy", "da", "de", "el", "en-US", "eo",
+    "es", "et", "eu", "fi", "fr", "fy", "ga", "gd", "gl", "he",
+    "hr", "hu", "id", "it", "ja", "ko", "lij", "lt", "nb-NO", "nl",
+    "pa", "pl", "pt-BR", "rm", "ro", "ru", "sk", "sl", "son", "sq",
+    "sr", "sv", "tr", "uk", "zh-CN", "zh-TW"
   ]
 }
diff --git a/config/local.json b/config/local.json
index 0aec32890f91b63109148e09f7bf85cd7877df87..ba653f4feb79cb3112b0265960ce72bd667204d4 100644
--- a/config/local.json
+++ b/config/local.json
@@ -4,6 +4,7 @@
   "dbwriter": { "bind_to": { "port": 10004 } },
   "proxy": { "bind_to": { "port": 10006 } },
   "browserid": { "bind_to": { "port": 10007 } },
+  "static": { "bind_to": { "port": 10010 } },
   "router": { "bind_to": { "port": 10002 } },
   "use_minified_resources": false,
   "database": {
diff --git a/config/production.json b/config/production.json
index 13cbcaf2ed977a1d1dc597c93a2d988335666677..61ca34a2a499aa4d2bd10256951c9e2092498665 100644
--- a/config/production.json
+++ b/config/production.json
@@ -50,6 +50,8 @@
   "dbwriter_url": "http://127.0.0.1:62900",
   "browserid": { "bind_to": { "port": 62700 } },
   "browserid_url": "http://127.0.0.1:62700",
+  "static": { "bind_to": { "port": 63400 } },
+  "static_url": "http://127.0.0.1:63400",
   "router": { "bind_to": { "port": 63300 } },
 
   // set to true to enable the development menu.
diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md
index b4425f6553fb6c22bff20c39a27401ff5eec5c19..0884334dd5b8d87388bc8b3cce17a3dda0d0a3e1 100644
--- a/docs/DEPLOYMENT.md
+++ b/docs/DEPLOYMENT.md
@@ -75,7 +75,7 @@ Let's get started:
   2. install git if required: `sudo apt-get install git-core`
   3. become user git: `sudo su -s /bin/bash git`
   4. hop into your home directory: `cd`
-  5. install gitolite: [This.](http://sitaramc.github.com/gitolite/nonroot.html)
+  5. install gitolite: [This.](https://github.com/sitaramc/gitolite)
   6. add a browserid repo: [This.](http://sitaramc.github.com/gitolite/add.html)
 
 At this point you've morphed your servers into git servers.  Go ahead and
diff --git a/docs/ORGANIZATION.md b/docs/ORGANIZATION.md
index 785f00b36671f4f0921fdcf4132f5a5474971146..fbb4aca2a4b16ccbe6a6aeaa4bc2ea3cd61411a5 100644
--- a/docs/ORGANIZATION.md
+++ b/docs/ORGANIZATION.md
@@ -43,4 +43,4 @@ and they share the following directory structure:
 
 * `tests/` - Tests written using [vows](http://vowsjs.org)
 
- * Run via `scripts/run_all_tests.sh`
+ * Run via `scripts/test`
diff --git a/docs/SETUP_UBUNTU.md b/docs/SETUP_UBUNTU.md
new file mode 100644
index 0000000000000000000000000000000000000000..7f13367da2184c61fc6124dc56403428363d7283
--- /dev/null
+++ b/docs/SETUP_UBUNTU.md
@@ -0,0 +1,9 @@
+Installing Dependencies on Ubuntu
+---------------------------------
+
+Run the following to install necessary dependencies:
+
+    sudo apt-add-repository ppa:chris-lea/node.js
+    sudo apt-get update
+    sudo apt-get install python-software-properties
+    sudo apt-get install nodejs npm git-core libgmp3-dev g++
diff --git a/docs/TESTING.md b/docs/TESTING.md
index 789397a7f08ad3c5bd167ee72c1bdc401b85e4b6..e052624a3fdda0df31ef32317101ebcb242ff0a8 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -2,15 +2,61 @@
    - 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/. -->
 
-Developer tests should be run before committing code. There are two test suites.
+Developer tests should be run before committing code. There are two test interfaces:
 
   - `npm test`
 
   - Load http://localhost:10002/test/index.html into a world wide web browser
 
-Note that for mysql, you will need to grant `browserid` privileges to create tables.
-You can then run the mysql suite with, e.g., 
+## Web Interface
 
-    NODE_ENV=test_mysql MYSQL_USER=browserid MYSQL_PASSWORD=browserid npm test
+The test URL (`localhost:10002/test`) takes an optional `filter`
+argument that can be used to restrict the test suite to one module.
+For example, to run only the `shared/xhr` tests, visit:
 
-  
+```
+http://localhost:10002/test/?filter=shared/xhr
+```
+
+The filter matches substrings, so you can also filter by `shared` to
+get `shared/xhr`, `shared/user`, etc.
+
+Test module names are listed on the web page on the left-hand side.
+
+## Shell Interface
+
+### MySQL
+
+Running tests with `npm test` will use a json database by default.  To
+test using MySQL, you will need to grant `browserid` privileges to
+create tables.  You can then run the mysql suite with, e.g.,
+
+```bash
+NODE_ENV=test_mysql MYSQL_USER=browserid MYSQL_PASSWORD=browserid npm test
+```
+
+### Test Suites
+
+There are two test suites:
+
+- `back`
+- `front`
+
+By default the test runner will run them all. You can limit it to one
+suite by setting `WHAT_TESTS` in your environment.  For example:
+
+```bash
+WHAT_TESTS=front npm test
+```
+
+The front-end tests are run via PhantomJS.
+
+### Filtering
+
+As in the web tests, you can tell the runner to run only tests whose
+modules match a given name.  Specify this in your environment with
+`FRONTEND_TEST_FILTER`.  For example:
+
+```bash
+WHAT_TESTS=front FRONTEND_TEST_FILTER=shared/user npm test
+```
diff --git a/lib/configuration.js b/lib/configuration.js
index 1d0bd7a30883b49af16ba955e5068e698a53a367..ef4d97f42e2609563a792783337bbc04f2697663 100644
--- a/lib/configuration.js
+++ b/lib/configuration.js
@@ -210,6 +210,10 @@ var conf = module.exports = convict({
     format: 'string?',
     env: 'BROWSERID_URL'
   },
+  static_url: {
+    format: 'string?',
+    env: 'STATIC_URL'
+  },
   process_type: 'string',
   email_to_console: 'boolean = false',
   declaration_of_support_timeout_ms: {
diff --git a/lib/db/json.js b/lib/db/json.js
index 376a2a791cae9a968949a202772df5c2193d552f..0bcfbf8a1fa9bcc5fdb8fab56c40705ff1b01e23 100644
--- a/lib/db/json.js
+++ b/lib/db/json.js
@@ -16,6 +16,13 @@ logger = require('../logging.js').logger,
 configuration = require('../configuration.js'),
 temp = require('temp');
 
+// existsSync moved from path in 0.6.x to fs in 0.8.x
+if (typeof fs.existsSync === 'function') {
+  var existsSync = fs.existsSync;
+} else {
+  var existsSync = path.existsSync;
+}
+
 // a little alias for stringify
 const ESC = JSON.stringify;
 
@@ -59,7 +66,7 @@ function flush() {
 
 function sync() {
   // the database not existing yet just means its empty, don't log an error
-  if (path.existsSync(dbPath)) {
+  if (existsSync(dbPath)) {
     try {
       db = JSON.parse(fs.readFileSync(dbPath));
 
diff --git a/lib/email.js b/lib/email.js
index 5afe2fe293f1066bc774eef99f52146ae7502c3e..f87e03e404bee549e7431cc0372d3b43af8f4cd3 100644
--- a/lib/email.js
+++ b/lib/email.js
@@ -28,7 +28,7 @@ if (smtp_params && smtp_params.host) {
   }
 }
 
-const TEMPLATE_PATH = path.join(__dirname, "browserid", "email_templates");
+const TEMPLATE_PATH = path.join(__dirname, "static", "email_templates");
 
 // the underbar decorator to allow getext to extract strings 
 function _(str) { return str; }
diff --git a/lib/i18n.js b/lib/i18n.js
index 7b2d214d96838f943392f4c7646419f91c35660b..a17cb8678f8044224cb5558697320e573e1a5c22 100644
--- a/lib/i18n.js
+++ b/lib/i18n.js
@@ -21,6 +21,13 @@ var logger = require('./logging.js').logger,
     util = require('util'),
     fs = require('fs');
 
+// existsSync moved from path in 0.6.x to fs in 0.8.x
+if (typeof fs.existsSync === 'function') {
+  var existsSync = fs.existsSync;
+} else {
+  var existsSync = path.existsSync;
+}
+
 const BIDI_RTL_LANGS = ['ar', 'db-LB', 'fa', 'he'];
 
 var mo_cache = {};
@@ -66,8 +73,8 @@ exports.abide = function (options) {
         default_locale = localeFrom(options.default_lang);
 
     mo_cache[l] = {
-      mo_exists: path.existsSync(mo_file_path(l)),
-      json_exists: path.existsSync(json_file_path(l)),
+      mo_exists: existsSync(mo_file_path(l)),
+      json_exists: existsSync(json_file_path(l)),
       gt: null
     };
     if (l !== debug_locale) {
@@ -244,15 +251,17 @@ exports.languageFrom = function (locale) {
  * of values for positional replacement.
  *
  * Named Example:
- * format("%(salutation)s %(place)s", {salutation: "Hello", place: "World"}, true);
+ * format("%(salutation)s %(place)s", {salutation: "Hello", place: "World"});
  * Positional Example:
  * format("%s %s", ["Hello", "World"]);
  */
 var format = exports.format = function (fmt, obj, named) {
-  if (! fmt) return "";
-  if (named) {
-    return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])});
-  } else {
+  if (!fmt) return "";
+  if (Array.isArray(obj) || named === false) {
     return fmt.replace(/%s/g, function(match){return String(obj.shift())});
+  } else if (typeof obj === 'object' || named === true) {
+    return fmt.replace(/%\(\s*([^)]+)\s*\)/g, function(m, v){
+      return String(obj[v]);
+    });
   }
 };
diff --git a/lib/logging.js b/lib/logging.js
index 058d161869a7e3113ef506304a6f8b81e16e8c0e..a67a01190cb2ca7f311f9718f4a9b453abb38b7a 100644
--- a/lib/logging.js
+++ b/lib/logging.js
@@ -20,12 +20,19 @@ configuration = require("./configuration"),
 path = require('path'),
 fs = require('fs');
 
+// existsSync moved from path in 0.6.x to fs in 0.8.x
+if (typeof fs.existsSync === 'function') {
+  var existsSync = fs.existsSync;
+} else {
+  var existsSync = path.existsSync;
+}
+
 // go through the configuration and determine log location
 var log_path = path.join(configuration.get('var_path'), 'log');
 
 // simple inline function for creation of dirs
 function mkdir_p(p) {
-  if (!path.existsSync(p)) {
+  if (!existsSync(p)) {
     mkdir_p(path.dirname(p));
     fs.mkdirSync(p, "0755");
   }
diff --git a/lib/metrics.js b/lib/metrics.js
index 48134aa5f4bd1feee13b39e23a90cd9e1a330fc5..c5e2cd2b5105b8b9fa2700f7e1c781b4cb2e8434 100644
--- a/lib/metrics.js
+++ b/lib/metrics.js
@@ -24,6 +24,13 @@ path = require('path'),
 fs = require('fs'),
 urlparse = require('urlparse');
 
+// existsSync moved from path in 0.6.x to fs in 0.8.x
+if (typeof fs.existsSync === 'function') {
+  var existsSync = fs.existsSync;
+} else {
+  var existsSync = path.existsSync;
+}
+
 // go through the configuration and determine log location
 // for now we only log to one place
 // FIXME: separate logs depending on purpose?
@@ -33,7 +40,7 @@ var LOGGER;
 
 // simple inline function for creation of dirs
 function mkdir_p(p) {
-  if (!path.existsSync(p)) {
+  if (!existsSync(p)) {
     mkdir_p(path.dirname(p));
     fs.mkdirSync(p, "0755");
   }
diff --git a/lib/primary.js b/lib/primary.js
index ad422cbf1aeb0f6fb472c3eaec22075b17f42f10..37c3fc809ff4aeafc3cbba888b8d548c8ba538c4 100644
--- a/lib/primary.js
+++ b/lib/primary.js
@@ -275,6 +275,10 @@ exports.verifyAssertion = function(assertion, cb) {
   jwcrypto.cert.verifyBundle(assertion, now, getRoot, function(err, certParamsArray, payload, assertionParams) {
     if (err) return cb(err);
 
+    // for now, to be extra safe, we don't allow cert chains
+    if (certParamsArray.length > 1)
+      return cb("certificate chaining is not yet allowed");
+    
     // audience must be browserid itself
     var want = urlparse(config.get('public_url')).originOnly();
     var got = urlparse(assertionParams.audience).originOnly();
diff --git a/lib/browserid/email_templates/add.ejs b/lib/static/email_templates/add.ejs
similarity index 100%
rename from lib/browserid/email_templates/add.ejs
rename to lib/static/email_templates/add.ejs
diff --git a/lib/browserid/email_templates/new.ejs b/lib/static/email_templates/new.ejs
similarity index 100%
rename from lib/browserid/email_templates/new.ejs
rename to lib/static/email_templates/new.ejs
diff --git a/lib/browserid/email_templates/reset.ejs b/lib/static/email_templates/reset.ejs
similarity index 72%
rename from lib/browserid/email_templates/reset.ejs
rename to lib/static/email_templates/reset.ejs
index 0b1845507c71c0f4b8b980e680e09f4427cbe53e..68734aa0239b405e352272e0b7827d3c3dd72b80 100644
--- a/lib/browserid/email_templates/reset.ejs
+++ b/lib/static/email_templates/reset.ejs
@@ -1,4 +1,4 @@
-<%= gettext('Forgot your password for Persona?  It happens to the best of us.') %>
+<%= gettext('Forgot your password for Persona? It happens to the best of us.') %>
 
 <%= gettext('Click to reset your password:') %>
 <%= link %>
diff --git a/lib/browserid/views.js b/lib/static/views.js
similarity index 99%
rename from lib/browserid/views.js
rename to lib/static/views.js
index b26361e2c6a04ff4e381cafe12b62c80132764b1..65a5d64eb9b8d3f599f76563fc5103fd5ba96b38 100644
--- a/lib/browserid/views.js
+++ b/lib/static/views.js
@@ -15,6 +15,8 @@ httputils = require('../httputils.js'),
 etagify = require('etagify'),
 secrets = require('../secrets');
 
+require("jwcrypto/lib/algs/rs");
+
 // all templated content, redirects, and renames are handled here.
 // anything that is not an api, and not static
 const
diff --git a/lib/static_resources.js b/lib/static_resources.js
index 7b14d260da15c063e0260419c81c2420b4c03eb8..9503c19d6daac0db3cf2638992e5daf9338e2fed 100644
--- a/lib/static_resources.js
+++ b/lib/static_resources.js
@@ -43,7 +43,6 @@ var common_js = [
   '/common/js/enable_cookies_url.js',
   '/common/js/wait-messages.js',
   '/common/js/error-messages.js',
-  '/common/js/error-display.js',
   '/common/js/storage.js',
   '/common/js/xhr_transport.js',
   '/common/js/xhr.js',
@@ -54,7 +53,8 @@ var common_js = [
   '/common/js/modules/xhr_delay.js',
   '/common/js/modules/xhr_disable_form.js',
   '/common/js/modules/cookie_check.js',
-  '/common/js/modules/development.js'
+  '/common/js/modules/development.js',
+  '/common/js/modules/extended-info.js'
 ];
 
 var browserid_min_js = '/production/:locale/browserid.js';
@@ -157,6 +157,10 @@ exports.resources = {
   ],
   '/production/relay.js': [
     '/relay/relay.js'
+  ],
+  '/production/authenticate_with_primary.js': [
+    '/common/js/lib/winchan.js',
+    '/auth_with_idp/main.js'
   ]
 };
 exports.resources[dialog_min_js] = dialog_js;
diff --git a/lib/verifier/certassertion.js b/lib/verifier/certassertion.js
index a8342f3160b2737490c3b0312e5830f4777611f8..1222df636fc7124438e6ab80727a7c0abeefa5a4 100644
--- a/lib/verifier/certassertion.js
+++ b/lib/verifier/certassertion.js
@@ -119,6 +119,10 @@ function verify(assertion, audience, successCB, errorCB) {
     }, function(err, certParamsArray, payload, assertionParams) {
       if (err) return errorCB(err);
 
+      // for now, to be extra safe, we don't allow cert chains
+      if (certParamsArray.length > 1)
+        return errorCB("certificate chaining is not yet allowed");
+      
       // audience must match!
       var err = compareAudiences(assertionParams.audience, audience)
       if (err) {
diff --git a/lib/wsapi.js b/lib/wsapi.js
index e95a6ec4f144d13d0b981d6e3f7a79e0e87eb09b..b9c5b50f7f384ec1f2fe5336cc165b27c4441585 100644
--- a/lib/wsapi.js
+++ b/lib/wsapi.js
@@ -50,6 +50,8 @@ if (config.get('public_url').indexOf('https://login.persona.org') !== 0) {
   COOKIE_KEY += "_" + hash.digest('hex').slice(0, 6);
 }
 
+const WSAPI_PREFIX = '/wsapi/';
+
 logger.info('session cookie name is: ' + COOKIE_KEY);
 
 function clearAuthenticatedUser(session) {
@@ -75,7 +77,7 @@ function bcryptPassword(password, cb) {
     statsd.timing('bcrypt.encrypt_time', reqTime);
     cb.apply(null, arguments);
   });
-};
+}
 
 function authenticateSession(session, uid, level, duration_ms) {
   if (['assertion', 'password'].indexOf(level) === -1)
@@ -116,6 +118,30 @@ function databaseDown(res, err) {
   httputils.serviceUnavailable(res, "database unavailable");
 }
 
+function operationFromURL (path) {
+  var purl = url.parse(path);
+  return purl.pathname.substr(0, WSAPI_PREFIX.length) === WSAPI_PREFIX &&
+          purl.pathname.substr(WSAPI_PREFIX.length) || null;
+}
+
+var APIs;
+function allAPIs () {
+  if (APIs) return APIs;
+
+  APIs = {};
+
+  fs.readdirSync(path.join(__dirname, 'wsapi')).forEach(function (f) {
+    // skip files that don't have a .js suffix or start with a dot
+    if (f.length <= 3 || f.substr(-3) !== '.js' || f.substr(0,1) === '.') return;
+    var operation = f.substr(0, f.length - 3);
+
+    var api = require(path.join(__dirname, 'wsapi', f));
+    APIs[operation] = api;
+  });
+
+  return APIs;
+}
+
 // common functions exported, for use by different api calls
 exports.clearAuthenticatedUser = clearAuthenticatedUser;
 exports.isAuthed = isAuthed;
@@ -127,10 +153,6 @@ exports.langContext = langContext;
 exports.databaseDown = databaseDown;
 
 exports.setup = function(options, app) {
-  const WSAPI_PREFIX = '/wsapi/';
-
-  // all operations that are being forwarded
-  var forwardedOperations = [];
 
   // If externally we're serving content over SSL we can enable things
   // like strict transport security and change the way cookies are set
@@ -162,8 +184,7 @@ exports.setup = function(options, app) {
     // by layers higher up based on cache control headers.
     // the fallout is that all code that interacts with sessions
     // should be under /wsapi
-    if (purl.pathname.substr(0, WSAPI_PREFIX.length) === WSAPI_PREFIX)
-    {
+    if (purl.pathname.substr(0, WSAPI_PREFIX.length) === WSAPI_PREFIX) {
       // explicitly disallow caching on all /wsapi calls (issue #294)
       resp.setHeader('Cache-Control', 'no-cache, max-age=0');
 
@@ -189,45 +210,31 @@ exports.setup = function(options, app) {
           return httputils.badRequest(resp, "no such api");
       }
 
-      // if this request is to be forwarded, we will not perform request validation,
-      // cookie parsing, nor body parsing - leaving that up to the process we're forwarding
-      // to.
-      if (-1 !== forwardedOperations.indexOf(operation)) {
-        // queue up the body here on and forward a single unchunked request onto the
-        // writer
-        return bodyParser(req, resp, function() {
-          next();
-        });
-      } else if (options.router_mode) {
-        // skip wsapi request, let browserid middleware handle forwards
-        return next();
-      } else {
-        // this is not a forwarded operation, perform full parsing and validation
-        return cookieParser(req, resp, function() {
-          bodyParser(req, resp, function() {
-            cookieSessionMiddleware(req, resp, function() {
-              // only on POSTs
-              if (req.method === "POST") {
-
-                if (req.session === undefined || typeof req.session.csrf !== 'string') { // there must be a session
-                  logger.warn("POST calls to /wsapi require a cookie to be sent, this user may have cookies disabled");
-                  return httputils.forbidden(resp, "no cookie");
-                }
-
-                // and the token must match what is sent in the post body
-                else if (!req.body || !req.session || !req.session.csrf || req.body.csrf != req.session.csrf) {
-                  // if any of these things are false, then we'll block the request
-                  var b = req.body ? req.body.csrf : "<none>";
-                  var s = req.session ? req.session.csrf : "<none>";
-                  logger.warn("CSRF validation failure, token mismatch. got:" + b + " want:" + s);
-                  return httputils.badRequest(resp, "CSRF violation");
-                }
+      // perform full parsing and validation
+      return cookieParser(req, resp, function() {
+        bodyParser(req, resp, function() {
+          cookieSessionMiddleware(req, resp, function() {
+            // only on POSTs
+            if (req.method === "POST") {
+
+              if (req.session === undefined || typeof req.session.csrf !== 'string') { // there must be a session
+                logger.warn("POST calls to /wsapi require a cookie to be sent, this user may have cookies disabled");
+                return httputils.forbidden(resp, "no cookie");
+              }
+
+              // and the token must match what is sent in the post body
+              else if (!req.body || !req.session || !req.session.csrf || req.body.csrf != req.session.csrf) {
+                // if any of these things are false, then we'll block the request
+                var b = req.body ? req.body.csrf : "<none>";
+                var s = req.session ? req.session.csrf : "<none>";
+                logger.warn("CSRF validation failure, token mismatch. got:" + b + " want:" + s);
+                return httputils.badRequest(resp, "CSRF violation");
               }
-              return next();
-            });
+            }
+            return next();
           });
         });
-      }
+      });
     } else {
       return next();
     }
@@ -243,54 +250,25 @@ exports.setup = function(options, app) {
     if (op.args) {
       str += " - " + op.args.join(", ");
     }
+    if (op.internal) str += ' - internal';
     str += ")";
     logger.debug(str);
   }
 
-  fs.readdirSync(path.join(__dirname, 'wsapi')).forEach(function (f) {
-    // skip files that don't have a .js suffix or start with a dot
-    if (f.length <= 3 || f.substr(-3) !== '.js' || f.substr(0,1) === '.') return;
-    var operation = f.substr(0, f.length - 3);
-
+  var all = allAPIs();
+  Object.keys(all).forEach(function (operation) {
     try {
-      var api = require(path.join(__dirname, 'wsapi', f));
+      var api = all[operation];
 
-      // don't register read apis if we are configured as a writer,
+      // - don't register read apis if we are configured as a writer,
       // with the exception of ping which tests database connection health.
-      if (options.only_write_apis && !api.writes_db &&
-          operation != 'ping') return;
+      // - don't register write apis if we are not configured as a writer
+      if ((options.only_write_apis && !api.writes_db && operation != 'ping') ||
+          (!options.only_write_apis && api.writes_db))
+            return;
 
       wsapis[operation] = api;
 
-      // forward writes if options.forward_writes is defined
-      if (options.forward_writes && wsapis[operation].writes_db &&
-          !wsapis[operation].disallow_forward)
-      {
-        forwardedOperations.push(operation);
-        var forward_url = options.forward_writes + "/wsapi/" + operation;
-        wsapis[operation].process = function(req, res) {
-          forward(forward_url, req, res, function(err) {
-            if (err) {
-              logger.error("error forwarding '"+ operation +
-                           "' request to '" + options.forward_writes + ":" + err);
-              httputils.serverError(res, "internal request forwarding error");
-            }
-          });
-        };
-
-        // XXX: disable validation on forwarded requests
-        // (we cannot perform this validation because we don't parse cookies
-        // nor post bodies on forwarded requests)
-        //
-        // at some point we'll want to improve our cookie parser and
-        // fully validate forwarded requests both at the intermediate
-        // hop (webhead) AND final destination (secure webhead)
-
-        delete api.args; // deleting args will cause arg validation to be skipped
-
-        api.authed = false; // authed=false will prevent us from checking auth status
-      }
-
       // set up the argument validator
       if (api.args) {
         if (!Array.isArray(api.args)) throw "exports.args must be an array of strings";
@@ -309,30 +287,15 @@ exports.setup = function(options, app) {
   // debug output - all supported apis
   logger.debug("WSAPIs:");
   Object.keys(wsapis).forEach(function(api) {
-    if (options.forward_writes && wsapis[api].writes_db) return;
     describeOperation(api, wsapis[api]);
   });
 
-  if (options.forward_writes) {
-    logger.debug("forwarded WSAPIs (to " + options.forward_writes + "):");
-    Object.keys(wsapis).forEach(function(api) {
-      if (wsapis[api].writes_db) {
-        describeOperation(api, wsapis[api]);
-      }
-    });
-  }
-
   app.use(function(req, resp, next) {
     var purl = url.parse(req.url);
 
     if (purl.pathname.substr(0, WSAPI_PREFIX.length) === WSAPI_PREFIX) {
       const operation = purl.pathname.substr(WSAPI_PREFIX.length);
 
-      if (options.router_mode && -1 === forwardedOperations.indexOf(operation)) {
-        // skip wsapi request, let browserid middleware handle forwards
-        return next();
-      }
-
       // the fake_verification wsapi is implemented elsewhere.
       if (operation == 'fake_verification') return next();
 
@@ -359,3 +322,46 @@ exports.setup = function(options, app) {
     }
   });
 };
+
+
+exports.routeSetup = function (app, options) {
+  var wsapis = allAPIs();
+
+  app.use(function(req, resp, next) {
+    var operation = operationFromURL(req.url);
+
+    // not a WSAPI request
+    if (!operation) return next();
+
+    var api = wsapis[operation];
+
+    // check to see if the api is known here, before spending more time with
+    // the request.
+    if (!wsapis.hasOwnProperty(operation) ||
+        api.method.toLowerCase() !== req.method.toLowerCase()) {
+      // if the fake verification api is enabled (for load testing),
+      // then let this request fall through
+      if (operation !== 'fake_verification' || !process.env['BROWSERID_FAKE_VERIFICATION'])
+        return httputils.badRequest(resp, "no such api");
+    }
+
+    if (api.internal) {
+        return httputils.notFound(resp);
+    }
+
+    var destination_url = api.writes_db ? options.write_url + "/wsapi/" + operation
+                                        : options.read_url + req.url;
+
+    var cb = function() {
+      forward(
+        destination_url, req, resp,
+        function(err) {
+          if (err) {
+            logger.error("error forwarding request:", err);
+          }
+        });
+    };
+    return express.bodyParser()(req, resp, cb);
+
+  });
+};
diff --git a/lib/wsapi/create_account_with_assertion.js b/lib/wsapi/create_account_with_assertion.js
index 13f96d395fb6b56c7f4fa85adfcbc38472ea48cf..3fdd3f3608037056ab8326767946d58a91ef182e 100644
--- a/lib/wsapi/create_account_with_assertion.js
+++ b/lib/wsapi/create_account_with_assertion.js
@@ -12,7 +12,7 @@ logger = require('../logging.js').logger;
 exports.method = 'post';
 exports.writes_db = true;
 exports.authed = false;
-exports.disallow_forward = true;
+exports.internal = true;
 exports.args = ['assertion'];
 exports.i18n = false;
 
diff --git a/package.json b/package.json
index 24a3bdf1e613fdced528e5c488f7be92f699ae34..925c8fd9691036bc04feaa7670e0b025d78183c7 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
         "ejs": "0.4.3",
         "etagify": "0.0.2",
         "express": "2.5.0",
-        "iconv": "1.1.3",
+        "iconv": "1.2.1",
         "mustache": "0.3.1-dev",
         "jwcrypto": "0.3.2",
         "mysql": "0.9.5",
diff --git a/resources/static/auth_with_idp/main.js b/resources/static/auth_with_idp/main.js
index d0ee9697a5a33baa76ef2347b2ba98baff13352b..0e759ca4906d124c2fca95397d22e5ad5bfeef79 100644
--- a/resources/static/auth_with_idp/main.js
+++ b/resources/static/auth_with_idp/main.js
@@ -5,7 +5,13 @@
 var wc = WinChan.onOpen(function(origin, args, cb) {
   if (window.location.hash === '#complete') cb();
   else {
+    var fullURL = args;
+
+    // store information in window.name to indicate that
+    // we redirect here
+    window.name = 'auth_with_primary';
+
     wc.detach();
-    window.location = args;
+    window.location = fullURL;
   }
 });
diff --git a/resources/static/authentication_api.js b/resources/static/authentication_api.js
index ca9b16c49990977dac2d828dacfdf2a9ea669571..8e2a0e98b775cb4167c2f41cfc406e9f1f283013 100644
--- a/resources/static/authentication_api.js
+++ b/resources/static/authentication_api.js
@@ -33,11 +33,17 @@
     };
 
     navigator.id.completeAuthentication = function(cb) {
-      window.location = 'https://login.persona.org/sign_in#AUTH_RETURN';
+      if (window.name == 'auth_with_primary')
+        window.location = 'https://login.persona.org/authenticate_with_primary#complete';
+      else
+        window.location = 'https://login.persona.org/sign_in#AUTH_RETURN';
     };
 
     navigator.id.raiseAuthenticationFailure = function(reason) {
-      window.location = 'https://login.persona.org/sign_in#AUTH_RETURN_CANCEL';
+      if (window.name == 'auth_with_primary')
+        window.location = 'https://login.persona.org/authenticate_with_primary#complete';
+      else
+        window.location = 'https://login.persona.org/sign_in#AUTH_RETURN_CANCEL';
     };
 
     navigator.id._primaryAPIIsShimmed = true;
diff --git a/resources/static/common/css/style.css b/resources/static/common/css/style.css
index d3dc7e2de54963c9510ff84be55a9b55218482e5..ab5a2a960f12cafd4b5d13115bbb83558ca2e6cb 100644
--- a/resources/static/common/css/style.css
+++ b/resources/static/common/css/style.css
@@ -160,6 +160,7 @@ input[type=checkbox] {
 button,
 .button {
     font-size: 14px;
+    font-weight: bold;
     line-height: 14px;
     /* The difference between top and bottom padding is to make up for the tiny
      * offset that browsers use to display lowercase letters.
@@ -168,12 +169,12 @@ button,
     float: right;
     border: 0;
     color: #fff;
-    text-shadow: 0 1px 0 #888, 0 0 2px rgba(255,255,255,.2);
+    text-shadow: 0 1px rgba(0,0,0,0.5);
     cursor: pointer;
     white-space: nowrap;
 
     border-radius: 3px;
-    border-bottom: 1px solid #888;
+    box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.3), 0 1px 0 rgba(0, 0, 0, 0.2);
 
     background-color: #4eb5e5;
     background-image: -webkit-gradient(linear, left top, left bottom, from(#4eb5e5), to(#3196cf));
@@ -188,13 +189,15 @@ button:hover,
 button:focus,
 .button:hover,
 .button:focus {
-    background-color: #43a6e2;
-    background-image: -webkit-gradient(linear, left top, left bottom, from(#43a6e2), to(#277ac1));
-    background-image: -webkit-linear-gradient(top, #43a6e2, #277ac1);
-    background-image:    -moz-linear-gradient(top, #43a6e2, #277ac1);
-    background-image:      -ms-linear-gradient(top, #43a6e2, #277ac1);
-    background-image:       -o-linear-gradient(top, #43a6e2, #277ac1);
-    background-image:          linear-gradient(top, #43a6e2, #277ac1);
+    box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.3), 0 1px 0 rgba(0, 0, 0, 0.2), 0 2px 0 rgba(0, 0, 0, 0.1);
+
+    background-color: #4aafe5;
+    background-image: -webkit-gradient(linear, left top, left bottom, from(#4aafe5), to(#2c89c8));
+    background-image: -webkit-linear-gradient(top, #4aafe5, #2c89c8);
+    background-image:    -moz-linear-gradient(top, #4aafe5, #2c89c8);
+    background-image:      -ms-linear-gradient(top, #4aafe5, #2c89c8);
+    background-image:       -o-linear-gradient(top, #4aafe5, #2c89c8);
+    background-image:          linear-gradient(top, #4aafe5, #2c89c8);
 }
 
 button:focus,
@@ -213,8 +216,8 @@ button:active,
     background-image:       -o-linear-gradient(top, #184a73, #276084);
     background-image:          linear-gradient(top, #184a73, #276084);
     color: #97b6ca;
-    text-shadow: 0 1px #143348;
-    box-shadow: 0 0 5px #003763 inset;
+    text-shadow: 0 1px rgba(0,0,0,0.4);
+    box-shadow: inset 0 2px 1px rgba(0,0,0,0.3);
 }
 
 button::-moz-focus-inner, .button::-moz-focus-inner {
@@ -237,19 +240,29 @@ button::-moz-focus-inner, .button::-moz-focus-inner {
     background-position: center right, center;
 }
 
-
 .submit button:hover,
 .submit button:focus,
 .submit .button:hover,
 .submit .button:focus {
-    background-color: #43a6e2;
-    background-image: url("/common/i/button-arrow.png");
-    background-image: url("/common/i/button-arrow.png"), -webkit-gradient(linear, left top, left bottom, from(#43a6e2), to(#277ac1));
-    background-image: url("/common/i/button-arrow.png"), -webkit-linear-gradient(top, #43a6e2, #277ac1);
-    background-image: url("/common/i/button-arrow.png"),    -moz-linear-gradient(top, #43a6e2, #277ac1);
-    background-image: url("/common/i/button-arrow.png"),     -ms-linear-gradient(top, #43a6e2, #277ac1);
-    background-image: url("/common/i/button-arrow.png"),      -o-linear-gradient(top, #43a6e2, #277ac1);
-    background-image: url("/common/i/button-arrow.png"),         linear-gradient(top, #43a6e2, #277ac1);
+    background-color: #4aafe5;
+    background-image: url("/common/i/button-arrow-hover.png");
+    background-image: url("/common/i/button-arrow-hover.png"), -webkit-gradient(linear, left top, left bottom, from(#4aafe5), to(#2c89c8));
+    background-image: url("/common/i/button-arrow-hover.png"), -webkit-linear-gradient(top, #4aafe5, #2c89c8);
+    background-image: url("/common/i/button-arrow-hover.png"),    -moz-linear-gradient(top, #4aafe5, #2c89c8);
+    background-image: url("/common/i/button-arrow-hover.png"),     -ms-linear-gradient(top, #4aafe5, #2c89c8);
+    background-image: url("/common/i/button-arrow-hover.png"),      -o-linear-gradient(top, #4aafe5, #2c89c8);
+    background-image: url("/common/i/button-arrow-hover.png"),         linear-gradient(top, #4aafe5, #2c89c8);
+}
+
+.submit button:active,
+.submit .button:active {
+    background-color: #184a73;
+    background-image: url("/common/i/button-arrow-active.png"), -webkit-gradient(linear, left top, left bottom, from(#184a73), to(#276084));
+    background-image: url("/common/i/button-arrow-active.png"), -webkit-linear-gradient(top, #184a73, #276084);
+    background-image: url("/common/i/button-arrow-active.png"),    -moz-linear-gradient(top, #184a73, #276084);
+    background-image: url("/common/i/button-arrow-active.png"),      -ms-linear-gradient(top, #184a73, #276084);
+    background-image: url("/common/i/button-arrow-active.png"),       -o-linear-gradient(top, #184a73, #276084);
+    background-image: url("/common/i/button-arrow-active.png"),          linear-gradient(top, #184a73, #276084);
 }
 
 button[disabled], .submit_disabled button, .submit_disabled .button,
@@ -285,18 +298,38 @@ button.negative:hover,
 button.negative:focus,
 .button.negative:hover,
 .button.negative:focus {
-    background-color: #ad1804;
-    background-image: -webkit-gradient(linear, left top, left bottom, from(#ad1804), to(#911403));
-    background-image: -webkit-linear-gradient(top, #ad1804, #911403);
-    background-image:    -moz-linear-gradient(top, #ad1804, #911403);
-    background-image:     -ms-linear-gradient(top, #ad1804, #911403);
-    background-image:      -o-linear-gradient(top, #ad1804, #911403);
-    background-image:         linear-gradient(top, #ad1804, #911403);
+    background-color: #e3653f;
+    background-image: -webkit-gradient(linear, left top, left bottom, from(#e3653f), to(#c01c03));
+    background-image: -webkit-linear-gradient(top, #e3653f, #c01c03);
+    background-image:    -moz-linear-gradient(top, #e3653f, #c01c03);
+    background-image:     -ms-linear-gradient(top, #e3653f, #c01c03);
+    background-image:      -o-linear-gradient(top, #e3653f, #c01c03);
+    background-image:         linear-gradient(top, #e3653f, #c01c03);
 }
 
 button.negative:active,
 .button.negative:active {
     box-shadow: 0 0 5px #333 inset;
+    color: #cfa391;
+
+    background-color: #83311e;
+    background-image: -webkit-gradient(linear, left top, left bottom, from(#83311e), to(#670d01));
+    background-image: -webkit-linear-gradient(top, #83311e, #670d01);
+    background-image:    -moz-linear-gradient(top, #83311e, #670d01);
+    background-image:     -ms-linear-gradient(top, #83311e, #670d01);
+    background-image:      -o-linear-gradient(top, #83311e, #670d01);
+    background-image:         linear-gradient(top, #83311e, #670d01);
+}
+
+button.loading, input[type="submit"].loading, .submit button.loading, .submit .button.loading{
+    background-color: #4eb5e5;
+    background-image: url("/common/i/button-loader.gif"), -webkit-gradient(linear, left top, left bottom, from(#4eb5e5), to(#3196cf));
+    background-image: url("/common/i/button-loader.gif"), -webkit-linear-gradient(top, #4eb5e5, #3196cf);
+    background-image: url("/common/i/button-loader.gif"),    -moz-linear-gradient(top, #4eb5e5, #3196cf);
+    background-image: url("/common/i/button-loader.gif"),      -ms-linear-gradient(top, #4eb5e5, #3196cf);
+    background-image: url("/common/i/button-loader.gif"),       -o-linear-gradient(top, #4eb5e5, #3196cf);
+    background-image: url("/common/i/button-loader.gif"),          linear-gradient(top, #4eb5e5, #3196cf);
+    background-position: 95% center;
 }
 
 .tospp {
@@ -432,12 +465,12 @@ footer .help {
     margin: 0 auto;
 }
 
-#openMoreInfo {
+.openMoreInfo {
     display: block;
     margin-top: 15px;
 }
 
-#moreInfo {
+.moreInfo {
     display: none;
     color: #999;
 }
diff --git a/resources/static/common/fonts/LICENSE.txt b/resources/static/common/fonts/LICENSE.txt
index 0b365b09f4a75a94f6e9baa26b7283ce7c873d0d..2c227077890c8c6b4c4edd557015c1ba8f760d18 100644
--- a/resources/static/common/fonts/LICENSE.txt
+++ b/resources/static/common/fonts/LICENSE.txt
@@ -1,203 +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.
+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/common/fonts/OpenSans-Bold.woff b/resources/static/common/fonts/OpenSans-Bold.woff
index a8b5bc261e2d20d18c8fcda6f65bb7c52cae3804..1945cad7f6aa9cfaa76da9fea9370b9f0be34766 100644
Binary files a/resources/static/common/fonts/OpenSans-Bold.woff and b/resources/static/common/fonts/OpenSans-Bold.woff differ
diff --git a/resources/static/common/fonts/OpenSans-BoldItalic.woff b/resources/static/common/fonts/OpenSans-BoldItalic.woff
index 6a1833811e187bf6174c4b06516905100f4d5c1f..4b1cc51926c330e4dff95bc0d9d2b8137db63f7f 100644
Binary files a/resources/static/common/fonts/OpenSans-BoldItalic.woff and b/resources/static/common/fonts/OpenSans-BoldItalic.woff differ
diff --git a/resources/static/common/fonts/OpenSans-Italic.woff b/resources/static/common/fonts/OpenSans-Italic.woff
index 039a294953ae18f154a91ca7407cbe9d12f37cb3..26a934f032633b9af62f41753f7d9060b93c1d3f 100644
Binary files a/resources/static/common/fonts/OpenSans-Italic.woff and b/resources/static/common/fonts/OpenSans-Italic.woff differ
diff --git a/resources/static/common/fonts/OpenSans-Light.woff b/resources/static/common/fonts/OpenSans-Light.woff
index 8025ba6cb5cbd7c1b444b27b4331efa4eec454f5..1c885eba3b4d80fa707dbb44049f42456e510e09 100644
Binary files a/resources/static/common/fonts/OpenSans-Light.woff and b/resources/static/common/fonts/OpenSans-Light.woff differ
diff --git a/resources/static/common/fonts/OpenSans-LightItalic.woff b/resources/static/common/fonts/OpenSans-LightItalic.woff
index c1e29dc534e750ffaa6c641c0f9ab369765daf63..a7465fec1d27d52dc4d85105dcbbdae3a85ed2c2 100644
Binary files a/resources/static/common/fonts/OpenSans-LightItalic.woff and b/resources/static/common/fonts/OpenSans-LightItalic.woff differ
diff --git a/resources/static/common/fonts/OpenSans-Regular.woff b/resources/static/common/fonts/OpenSans-Regular.woff
index 58e6cb38180fc8389b73569724935e791021ab94..feaed88cf2835a72c9533311ab167d5388110715 100644
Binary files a/resources/static/common/fonts/OpenSans-Regular.woff and b/resources/static/common/fonts/OpenSans-Regular.woff differ
diff --git a/resources/static/common/i/button-arrow-active.png b/resources/static/common/i/button-arrow-active.png
new file mode 100644
index 0000000000000000000000000000000000000000..6d8b4b1ebee6f2c6e86609b68187b1bc7e8a6aee
Binary files /dev/null and b/resources/static/common/i/button-arrow-active.png differ
diff --git a/resources/static/common/i/button-arrow-hover.png b/resources/static/common/i/button-arrow-hover.png
new file mode 100644
index 0000000000000000000000000000000000000000..0f16a66797ce4ce0e21223d4d8a873c4064164ed
Binary files /dev/null and b/resources/static/common/i/button-arrow-hover.png differ
diff --git a/resources/static/common/i/button-loader.gif b/resources/static/common/i/button-loader.gif
new file mode 100644
index 0000000000000000000000000000000000000000..48b61ac1b684c487b5e270bd75e6166ff1193f29
Binary files /dev/null and b/resources/static/common/i/button-loader.gif differ
diff --git a/resources/static/common/js/error-display.js b/resources/static/common/js/error-display.js
deleted file mode 100644
index 7777364d2242006aca12f7c98c08d099466c586e..0000000000000000000000000000000000000000
--- a/resources/static/common/js/error-display.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/* 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.ErrorDisplay = (function() {
-  "use strict";
-
-  var bid = BrowserID,
-      dom = bid.DOM;
-
-  function open(event) {
-    event && event.preventDefault();
-
-    /**
-     * XXX What a big steaming pile, use CSS animations for this!
-     */
-    $("#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"});
-  }
-
-  function init(target) {
-    dom.bindEvent("#openMoreInfo", "click", open);
-    return dom.getElements(target);
-  }
-
-  function stop() {
-    dom.unbindEvent("#openMoreInfo", "click", open);
-  }
-
-  return {
-    start: init,
-    stop: stop,
-    open: open
-  };
-
-}());
-
diff --git a/resources/static/common/js/gettext.js b/resources/static/common/js/gettext.js
index 2b24477c5a468f3f8ac97ebcabb135da4f3c94d1..0ee2f7e36b7416d41beed987b842b676ff392bb1 100644
--- a/resources/static/common/js/gettext.js
+++ b/resources/static/common/js/gettext.js
@@ -20,14 +20,16 @@
         },
         // See lib/i18n.js format docs
         format: function (fmt, obj, named) {
-          if (! fmt) return "";
-          if (! fmt.replace) {
+          if (!fmt) return "";
+          if (!fmt.replace) {
             return fmt;
           }
-          if (named) {
-            return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])});
-          } else {
+          if (_.isArray(obj) || named === false) {
             return fmt.replace(/%s/g, function(match){return String(obj.shift())});
+          } else if (_.isObject(obj) || named === true) {
+            return fmt.replace(/%\(\s*([^)]+)\s*\)/g, function(m, v){
+              return String(obj[v]);
+            });
           }
         }
       };
@@ -40,5 +42,4 @@
   var gt = new Gettext(params);
   window.gettext = gt.gettext.bind(gt);
   window.format = gt.format.bind(gt);
-
 }());
diff --git a/resources/static/common/js/modules/cookie_check.js b/resources/static/common/js/modules/cookie_check.js
index a1f361dd5ded9fea8d531c1944e3842d78eb462d..c3057a503d79a9dd9076f04668e0f8414670fe9c 100644
--- a/resources/static/common/js/modules/cookie_check.js
+++ b/resources/static/common/js/modules/cookie_check.js
@@ -1,4 +1,4 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*jshint browser:true, jquery: true, forin: true, laxbreak:true */
 /*global 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
diff --git a/resources/static/common/js/modules/development.js b/resources/static/common/js/modules/development.js
index 5c676960ac8b580463109857f3a80691321d9bd9..4511c38511b86c78d41cdbe420b37b2091129081 100644
--- a/resources/static/common/js/modules/development.js
+++ b/resources/static/common/js/modules/development.js
@@ -1,4 +1,4 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*jshint browser:true, jquery: true, forin: true, laxbreak:true */
 /*global 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
diff --git a/resources/static/common/js/modules/extended-info.js b/resources/static/common/js/modules/extended-info.js
new file mode 100644
index 0000000000000000000000000000000000000000..0e6a9c2dd65ad62639d290486661239ce1ddcf2f
--- /dev/null
+++ b/resources/static/common/js/modules/extended-info.js
@@ -0,0 +1,46 @@
+/* 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.Modules.ExtendedInfo = (function() {
+  "use strict";
+
+  var bid = BrowserID,
+      dom = bid.DOM,
+      complete = bid.Helpers.complete;
+
+
+  var Module = bid.Modules.PageModule.extend({
+    start: function(config) {
+      var self=this;
+
+      self.checkRequired(config, "target");
+      self.target = config.target;
+
+      var openerEl = self.openerEl = $(".openMoreInfo", self.target);
+      self.click(openerEl, self.open);
+
+      Module.sc.start.call(self, config);
+    },
+
+    open: function(oncomplete) {
+      var self = this,
+          extendedInfoEl = $(".moreInfo", self.target);
+
+      /**
+       * XXX What a big steaming pile, use CSS animations for this!
+       */
+      $(extendedInfoEl).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");
+        complete(oncomplete);
+      });
+      $(self.openerEl).css({visibility: "hidden"});
+    }
+  });
+
+  return Module;
+}());
+
diff --git a/resources/static/common/js/modules/interaction_data.js b/resources/static/common/js/modules/interaction_data.js
index 8f36514dac7ddc5f6f13dccd4d4e6a593f105ef5..181db90d64aedef85291e5ad943019de8a4133ed 100644
--- a/resources/static/common/js/modules/interaction_data.js
+++ b/resources/static/common/js/modules/interaction_data.js
@@ -28,6 +28,7 @@ BrowserID.Modules.InteractionData = (function() {
   var bid = BrowserID,
       model = bid.Models.InteractionData,
       network = bid.Network,
+      storage = bid.Storage,
       complete = bid.Helpers.complete,
       dom = bid.DOM,
       sc;
@@ -98,13 +99,64 @@ BrowserID.Modules.InteractionData = (function() {
     if (self.sessionContextHandled) return;
     self.sessionContextHandled = true;
 
+    publishPreviousSession.call(self, result);
+  }
+
+  function publishPreviousSession(result) {
     // Publish any outstanding data.  Unless this is a continuation, previous
     // session data must be published independently of whether the current
     // dialog session is allowed to sample data. This is because the original
     // dialog session has already decided whether to collect data.
+    //
+    // beginSampling must happen afterwards, since we need to send and
+    // then scrub out the previous sessions data.
 
-    model.stageCurrent();
-    publishStored.call(self);
+    var self = this;
+
+    function onComplete() {
+      model.stageCurrent();
+      publishStored.call(self);
+      beginSampling.call(self, result);
+    }
+
+    // if we were orphaned last time, but user is now authenticated,
+    // lets see if their action end in success, and if so,
+    // remove the orphaned flag
+    //
+    // actions:
+    // - user_staged => is authenticated?
+    // - email_staged => email count is higher?
+    //
+    // See https://github.com/mozilla/browserid/issues/1827
+    var current = model.getCurrent();
+    if (current && current.orphaned) {
+      var events = current.event_stream || [];
+      if (hasEvent(events, MediatorToKPINameTable.user_staged)) {
+        network.checkAuth(function(auth) {
+          if (!!auth) {
+            current.orphaned = false;
+            model.setCurrent(current);
+          }
+          complete(onComplete);
+        });
+      } else if (hasEvent(events, MediatorToKPINameTable.email_staged)) {
+        if ((storage.getEmailCount() || 0) > (current.number_emails || 0)) {
+          current.orphaned = false;
+          model.setCurrent(current);
+        }
+        complete(onComplete);
+      } else {
+        // oh well, an orphan it is
+        complete(onComplete);
+      }
+    } else {
+      // not an orphan, move along
+      complete(onComplete);
+    }
+  }
+
+  function beginSampling(result) {
+    var self = this;
 
     // set the sample rate as defined by the server.  It's a value
     // between 0..1, integer or float, and it specifies the percentage
@@ -152,6 +204,19 @@ BrowserID.Modules.InteractionData = (function() {
     self.initialEventStream = null;
 
     self.samplesBeingStored = true;
+    
+  }
+
+  function indexOfEvent(eventStream, eventName) {
+    for(var event, i = 0; event = eventStream[i]; ++i) {
+      if(event[0] === eventName) return i;
+    }
+
+    return -1;
+  }
+
+  function hasEvent(eventStream, eventName) {
+    return indexOfEvent(eventStream, eventName) !== -1;
   }
 
   function onKPIData(msg, result) {
diff --git a/resources/static/common/js/modules/page_module.js b/resources/static/common/js/modules/page_module.js
index 2afa4634e9e26725e67933e5c2ab0cd20148162e..abf39c1262c38b91138a77f7ce03c842d53aaace 100644
--- a/resources/static/common/js/modules/page_module.js
+++ b/resources/static/common/js/modules/page_module.js
@@ -1,4 +1,4 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*jshint browser:true, jquery: true, forin: true, laxbreak:true */
 /*global 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
@@ -132,15 +132,9 @@ BrowserID.Modules.PageModule = (function() {
     renderWait: showScreen.curry(screens.wait),
     hideWait: hideScreen.curry(screens.wait),
 
-    renderError: function(template, data, oncomplete) {
-      screens.error.show(template, data);
-
-      bid.ErrorDisplay.start();
-
-      oncomplete && oncomplete(false);
-    },
-
+    renderError: showScreen.curry(screens.error),
     hideError: hideScreen.curry(screens.error),
+
     renderDelay: showScreen.curry(screens.delay),
     hideDelay: hideScreen.curry(screens.delay),
 
diff --git a/resources/static/common/js/network.js b/resources/static/common/js/network.js
index d423f12d32fa4ee7694d5730643c95e17bf64b68..26397bc3ea7beb72971b3b55455628da763a44b4 100644
--- a/resources/static/common/js/network.js
+++ b/resources/static/common/js/network.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global 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
diff --git a/resources/static/common/js/provisioning.js b/resources/static/common/js/provisioning.js
index fa91170c2ea3941fa2c59b5b1b53ff748125f2d7..53859547130ee17a4033c1ae060e879ebbbbf704 100644
--- a/resources/static/common/js/provisioning.js
+++ b/resources/static/common/js/provisioning.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global 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
diff --git a/resources/static/common/js/renderer.js b/resources/static/common/js/renderer.js
index 8cbfb216a5c541d1ac62782efc3564abea3df593..4f74c0c414b83e8af803fdc1be5f61bbdcfd174d 100644
--- a/resources/static/common/js/renderer.js
+++ b/resources/static/common/js/renderer.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global 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
diff --git a/resources/static/common/js/screens.js b/resources/static/common/js/screens.js
index e45daaea63ce87b41b4759c3497852602b78fbe3..d852bb9826cbf61dbc32519f5a9da10a0bd07c7b 100644
--- a/resources/static/common/js/screens.js
+++ b/resources/static/common/js/screens.js
@@ -13,21 +13,43 @@ BrowserID.Screens = (function() {
   function Screen(target, className) {
     return {
       show: function(template, vars) {
+        var self=this;
+
         renderer.render(target + " .contents", template, vars);
         dom.addClass(BODY, className);
         dom.fireEvent(window, "resize");
-        this.visible = true;
+
+        // extendedInfo takes care of info that is on a screen but hidden by
+        // default.  When the user clicks the "open extended info" button, it
+        // is displayed to them.
+
+        if (self.extendedInfo) {
+          // sometimes a screen is overwritten and never hidden.  When this
+          // happens, old extendedInfos need to be torn down.
+          self.extendedInfo.stop();
+        }
+        self.extendedInfo = bid.Modules.ExtendedInfo.create();
+        self.extendedInfo.start({ target: target });
+
+        self.visible = true;
       },
 
       hide: function() {
+        var self=this;
+
         dom.removeClass(BODY, className);
         dom.fireEvent(window, "resize");
-        this.visible = false;
+
+        if (self.extendedInfo) {
+          self.extendedInfo.stop();
+          self.extendedInfo = null;
+        }
+
+        self.visible = false;
       }
     }
   }
 
-
   return {
     form: new Screen("#formWrap", "form"),
     wait: new Screen("#wait", "waiting"),
diff --git a/resources/static/common/js/state_machine.js b/resources/static/common/js/state_machine.js
index c6dcafdcc6db6f2f1c4a8e58754d0f859b02bcca..12a4c4a1b32e4a81f5b49550f282608293b7039b 100644
--- a/resources/static/common/js/state_machine.js
+++ b/resources/static/common/js/state_machine.js
@@ -1,4 +1,4 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*jshint browser:true, jquery: true, forin: true, laxbreak:true */
 /*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
diff --git a/resources/static/common/js/tooltip.js b/resources/static/common/js/tooltip.js
index 912595e49333cf629f26b5b9f412d28accaf3b47..e50d8bc89ffb416a694599b9b165bc9ca04dafa3 100644
--- a/resources/static/common/js/tooltip.js
+++ b/resources/static/common/js/tooltip.js
@@ -1,4 +1,4 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*jshint browser:true, jquery: true, forin: true, laxbreak:true */
 /*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
diff --git a/resources/static/common/js/user.js b/resources/static/common/js/user.js
index be52ee35266f6b908a78941a889ba4200239336c..b1be1696d9d8cddb49f66ba00eaa51f88060a168 100644
--- a/resources/static/common/js/user.js
+++ b/resources/static/common/js/user.js
@@ -965,8 +965,14 @@ BrowserID.User = (function() {
      */
     syncEmailKeypair: function(email, onComplete, onFailure) {
       prepareDeps();
-      jwcrypto.generateKeypair({algorithm: "DS", keysize: bid.KEY_LENGTH}, function(err, keypair) {
-        certifyEmailKeypair(email, keypair, onComplete, onFailure);
+      // jwcrypto depends on a random seed being set to generate a keypair.
+      // The seed is set with a call to network.withContext.  Ensure the
+      // random seed is set before continuing or else the seed may not be set,
+      // the key never created, and the onComplete callback never called.
+      network.withContext(function() {
+        jwcrypto.generateKeypair({algorithm: "DS", keysize: bid.KEY_LENGTH}, function(err, keypair) {
+          certifyEmailKeypair(email, keypair, onComplete, onFailure);
+        });
       });
     },
 
diff --git a/resources/static/common/js/xhr.js b/resources/static/common/js/xhr.js
index 2d86c6fde9fa421161c56f142bad9562482830d9..007d9c31dd5b058d578bbbb84e69de284d66ec9a 100644
--- a/resources/static/common/js/xhr.js
+++ b/resources/static/common/js/xhr.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global 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
diff --git a/resources/static/common/js/xhr_transport.js b/resources/static/common/js/xhr_transport.js
index 61e639646b40145ae723c2c06fbb0a05eaa21f2f..6366d032d44e93abee200f759a288482c796662d 100644
--- a/resources/static/common/js/xhr_transport.js
+++ b/resources/static/common/js/xhr_transport.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global 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
diff --git a/resources/static/dialog/js/misc/helpers.js b/resources/static/dialog/js/misc/helpers.js
index 23831652fef2a4e8c5807a1f89b2d7b354faf4da..6a0cae05bdfa6e35aa536e44c11f813eca400f13 100644
--- a/resources/static/dialog/js/misc/helpers.js
+++ b/resources/static/dialog/js/misc/helpers.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global 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
@@ -20,7 +20,10 @@
         doAnimation = $("#signIn").length && bodyWidth > 640;
 
     if (doAnimation) {
-      var endWidth = bodyWidth - 10;
+      /**
+       * Force the arrow to slide all the way off the screen.
+       */
+      var endWidth = bodyWidth + $(".arrowContainer").outerWidth();
 
       body.addClass("completing");
       /**
diff --git a/resources/static/dialog/js/misc/internal_api.js b/resources/static/dialog/js/misc/internal_api.js
index 3f0ceb8be5077f47826ece6a1b64ccb6a7e00feb..5a5e644c0dbd2cd6c1038c7ede4c8f937d25cf1f 100644
--- a/resources/static/dialog/js/misc/internal_api.js
+++ b/resources/static/dialog/js/misc/internal_api.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global 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
diff --git a/resources/static/dialog/js/modules/actions.js b/resources/static/dialog/js/modules/actions.js
index d778612926b9bf767ff915901ce709ea3de474b4..aec8e96a38ca1abfa54e69e683aa2bd7f2134c99 100644
--- a/resources/static/dialog/js/modules/actions.js
+++ b/resources/static/dialog/js/modules/actions.js
@@ -37,7 +37,8 @@ BrowserID.Modules.Actions = (function() {
       verifier: verifier,
       verificationMessage: message,
       password: password,
-      siteName: options.siteName
+      siteName: options.siteName,
+      email: options.email
     });
     controller.startCheck();
   }
diff --git a/resources/static/dialog/js/modules/add_email.js b/resources/static/dialog/js/modules/add_email.js
index 07f6242caf302bed2cb3aaa49ee7416522ffb811..c5fd53a0f26333989fe63813fc190195308fbd6f 100644
--- a/resources/static/dialog/js/modules/add_email.js
+++ b/resources/static/dialog/js/modules/add_email.js
@@ -1,4 +1,4 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*jshint browser:true, jquery: true, forin: true, laxbreak:true */
 /*global _: true, BrowserID: true, PageController: true, gettext: 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
@@ -40,7 +40,10 @@ BrowserID.Modules.AddEmail = (function() {
 
     if (email) {
       showHint("addressInfo");
-      dialogHelpers.addEmail.call(self, email, callback);
+      dialogHelpers.addEmail.call(self, email, function removeHint(status) {
+        hideHint("addressInfo");
+        complete(callback, status);
+      });
     }
     else {
       complete(callback, false);
diff --git a/resources/static/dialog/js/modules/check_registration.js b/resources/static/dialog/js/modules/check_registration.js
index 784298ac2e6e584b289e869a35028a5077381065..29094a1dcfd4bc7749ed50db4616f3d4baaa46c3 100644
--- a/resources/static/dialog/js/modules/check_registration.js
+++ b/resources/static/dialog/js/modules/check_registration.js
@@ -1,4 +1,4 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*jshint browser:true, jquery: true, forin: true, laxbreak:true */
 /*global BrowserID: true, PageController: 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
@@ -15,6 +15,8 @@ BrowserID.Modules.CheckRegistration = (function() {
     start: function(options) {
       var self=this;
       options = options || {};
+
+      self.checkRequired(options, "email", "siteName");
       var templateData = {
         email: options.email,
         required: options.required,
diff --git a/resources/static/dialog/js/modules/dialog.js b/resources/static/dialog/js/modules/dialog.js
index 8a4169fa8e5a3467885c6740109beb6b7ff1a9bc..7ed80ec7d026164860778e7cee7f979b557f9a18 100644
--- a/resources/static/dialog/js/modules/dialog.js
+++ b/resources/static/dialog/js/modules/dialog.js
@@ -1,4 +1,4 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*jshint browser:true, jquery: true, forin: true, laxbreak:true */
 /*global 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
diff --git a/resources/static/dialog/js/modules/generate_assertion.js b/resources/static/dialog/js/modules/generate_assertion.js
index eef1b2d0d3ccef38dddb34f94355064ee49c3bf3..d0045eee2f8987a60aeb79a2fc5e82c540042faa 100644
--- a/resources/static/dialog/js/modules/generate_assertion.js
+++ b/resources/static/dialog/js/modules/generate_assertion.js
@@ -1,4 +1,4 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*jshint browser:true, jquery: true, forin: true, laxbreak:true */
 /*global _: true, BrowserID: true, PageController: 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
diff --git a/resources/static/dialog/js/modules/is_this_your_computer.js b/resources/static/dialog/js/modules/is_this_your_computer.js
index 41fb15f36a029eb378485b316fa0bf4c54c78deb..b070364ee5bbc7f635f762af0898f790d69bd538 100644
--- a/resources/static/dialog/js/modules/is_this_your_computer.js
+++ b/resources/static/dialog/js/modules/is_this_your_computer.js
@@ -1,4 +1,4 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*jshint browser:true, jquery: true, forin: true, laxbreak:true */
 /*global BrowserID:true, PageController: 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
diff --git a/resources/static/dialog/js/modules/pick_email.js b/resources/static/dialog/js/modules/pick_email.js
index 8b35cab2348cd3fc9b33dc101e6e21012c6879cd..ecd77cbcb94c2160710fe787645a6488e1e777e7 100644
--- a/resources/static/dialog/js/modules/pick_email.js
+++ b/resources/static/dialog/js/modules/pick_email.js
@@ -1,4 +1,4 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*jshint browser:true, jquery: true, forin: true, laxbreak:true */
 /*global _: true, BrowserID: true, PageController: 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
diff --git a/resources/static/dialog/js/modules/primary_user_provisioned.js b/resources/static/dialog/js/modules/primary_user_provisioned.js
index 892448a5071df9acd2d0e21f5f5cb317d397a43b..1543f4b494005be8bdfb3fa9257dec465e58e76b 100644
--- a/resources/static/dialog/js/modules/primary_user_provisioned.js
+++ b/resources/static/dialog/js/modules/primary_user_provisioned.js
@@ -1,4 +1,4 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*jshint browser:true, jquery: true, forin: true, laxbreak:true */
 /*global BrowserID:true, PageController: 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
diff --git a/resources/static/dialog/js/modules/provision_primary_user.js b/resources/static/dialog/js/modules/provision_primary_user.js
index 4e1c9e186a70e900eb697d631c8d8dddef8a57bd..2df0b623d824a809543e1a33f359683ae8427b4b 100644
--- a/resources/static/dialog/js/modules/provision_primary_user.js
+++ b/resources/static/dialog/js/modules/provision_primary_user.js
@@ -1,4 +1,4 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*jshint browser:true, jquery: true, forin: true, laxbreak:true */
 /*global BrowserID:true, PageController: 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
diff --git a/resources/static/dialog/js/modules/required_email.js b/resources/static/dialog/js/modules/required_email.js
index 037621924d000b0b2c5925f1ad34b22efd58f550..5653913acbdcfed2d12e8c7b68941dc1bff1e5ee 100644
--- a/resources/static/dialog/js/modules/required_email.js
+++ b/resources/static/dialog/js/modules/required_email.js
@@ -1,4 +1,4 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*jshint browser:true, jquery: true, forin: true, laxbreak:true */
 /*global _: true, BrowserID: true, PageController: 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
diff --git a/resources/static/dialog/js/modules/rp_info.js b/resources/static/dialog/js/modules/rp_info.js
index 55d91d276edf7e861841dc1c3243920fd44f536b..9fab0c7b295dbdc8160d72f63285e3fe0fd79b15 100644
--- a/resources/static/dialog/js/modules/rp_info.js
+++ b/resources/static/dialog/js/modules/rp_info.js
@@ -1,4 +1,4 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*jshint browser:true, jquery: true, forin: true, laxbreak:true */
 /*global _: true, BrowserID: true, PageController: 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
diff --git a/resources/static/dialog/js/modules/verify_primary_user.js b/resources/static/dialog/js/modules/verify_primary_user.js
index c7cd90b83ce8a769d0985722fd582cab65b733e7..423175e569febcb726f721640b5060a1c7f156ba 100644
--- a/resources/static/dialog/js/modules/verify_primary_user.js
+++ b/resources/static/dialog/js/modules/verify_primary_user.js
@@ -1,4 +1,4 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*jshint browser:true, jquery: true, forin: true, laxbreak:true */
 /*global _: true, BrowserID: true, PageController: 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
diff --git a/resources/static/dialog/views/authenticate.ejs b/resources/static/dialog/views/authenticate.ejs
index 8fac82a6afa7507939539cc50064b14b4dfe0ad5..b56209b7af1e1d67b0c1ab08343c99908b6f0c5c 100644
--- a/resources/static/dialog/views/authenticate.ejs
+++ b/resources/static/dialog/views/authenticate.ejs
@@ -12,7 +12,7 @@
 
           <li>
               <label for="email"><%= gettext('To sign in with Persona, please enter your email address.') %></label>
-              <input id="email" type="email" autocapitalize="off" autocorrect="off" value="<%= email %>" maxlength="254" tabindex="1" placeholder="<%= gettext('enter email address') %>"/>
+              <input id="email" type="email" autocapitalize="off" autocorrect="off" value="<%= email %>" maxlength="254" placeholder="<%= gettext('enter email address') %>"/>
 
               <div id="email_format" class="tooltip" for="email">
                 <%= gettext('This field must be an email address.') %>
@@ -36,7 +36,7 @@
 
               <label for="password" class="hidden"><%= gettext('Password') %></label>
 
-              <input id="password" type="password" maxlength="80" tabindex="2" placeholder="<%= gettext('password') %>" />
+              <input id="password" type="password" maxlength="80" placeholder="<%= gettext('password') %>" />
 
               <div id="password_required" class="tooltip" for="password">
                 <%= gettext('The password field is required.') %>
@@ -57,12 +57,10 @@
 
 
       <p class="submit tospp">
-         <%= format(
-              gettext('By proceeding, you agree to %s\'s <a %s>Terms</a> and <a %s>Privacy Policy</a>.'),
-                   [ "Persona",
-                     ' href="https://login.persona.org/tos" target="_new"',
-                     ' href="https://login.persona.org/privacy" target="_new"',
-                   ]) %>
+         <%= format(gettext("By proceeding, you agree to %(site)'s <a %(terms)>Terms</a> and <a %(privacy)>Privacy Policy</a>."),
+                    { site: "Persona",
+                      terms: 'href="https://login.persona.org/tos" target="_new"',
+                      privacy: 'href="https://login.persona.org/privacy" target="_new"' }) %>
       </p>
 
   </div>
diff --git a/resources/static/dialog/views/confirm_email.ejs b/resources/static/dialog/views/confirm_email.ejs
index 380b051ef47b8db3368807163de78aaa99174a3f..a796667bc6c500bd03bee8a8ede2f2926e40412c 100644
--- a/resources/static/dialog/views/confirm_email.ejs
+++ b/resources/static/dialog/views/confirm_email.ejs
@@ -9,6 +9,6 @@
     </p>
 
     <p>
-      <%= format(gettext('Click the link in the confirmation email.  You\'ll then immediately be signed in to %s.'), ["<strong>" + siteName + "</strong>"]) %>
+      <%= format(gettext('Click the link in the confirmation email. You\'ll then immediately be signed in to %s.'), ["<strong>" + siteName + "</strong>"]) %>
     </p>
 
diff --git a/resources/static/dialog/views/error.ejs b/resources/static/dialog/views/error.ejs
index 2a0ee0066700746b6c73e31818f4353ee1156976..bdb9146febadd11e7491a847b87a043e5d8745bf 100644
--- a/resources/static/dialog/views/error.ejs
+++ b/resources/static/dialog/views/error.ejs
@@ -35,11 +35,11 @@
   <% } %>
 
   <% if(typeof action !== "undefined" || typeof network !== "undefined") { %>
-    <a href="#" id="openMoreInfo">
+    <a href="#" class="openMoreInfo">
       <%= gettext("See more info") %>
     </a>
 
-    <ul id="moreInfo">
+    <ul class="moreInfo">
       <% if (typeof action !== "undefined") { %>
         <li>
           <strong id="action">Action: </strong><%= action.title %>
diff --git a/resources/static/dialog/views/required_email.ejs b/resources/static/dialog/views/required_email.ejs
index 973048b7b76c981361cecc74d161dfbc7897ae14..0bb323a27b3e7478b621349e3b0375a6e69226ee 100644
--- a/resources/static/dialog/views/required_email.ejs
+++ b/resources/static/dialog/views/required_email.ejs
@@ -63,12 +63,10 @@
           </p>
           <% if (personaTOSPP) { %>
             <p class="tospp">
-               <%= format(
-                    gettext('By proceeding, you agree to %s\'s <a %s>Terms</a> and <a %s>Privacy Policy</a>.'),
-                         [ "Persona",
-                           ' href="https://login.persona.org/tos" target="_new"',
-                           ' href="https://login.persona.org/privacy" target="_new"',
-                         ]) %>
+               <%= format(gettext("By proceeding, you agree to %(site)'s <a %(terms)>Terms</a> and <a %(privacy)>Privacy Policy</a>."),
+                          { site: "Persona",
+                            terms: 'href="https://login.persona.org/tos" target="_new"',
+                            privacy: 'href="https://login.persona.org/privacy" target="_new"' }) %>
             </p>
           <% } %>
       </div>
diff --git a/resources/static/dialog/views/rp_info.ejs b/resources/static/dialog/views/rp_info.ejs
index 724a3e1925dfbf9a1c5d58d6e8bfc67278d31c44..4ae1e5a072a978a33d7676d706c272d0d75c83d0 100644
--- a/resources/static/dialog/views/rp_info.ejs
+++ b/resources/static/dialog/views/rp_info.ejs
@@ -19,10 +19,12 @@
 
 <% if(privacyPolicy && termsOfService) { %>
   <p id="rptospp" class="tospp">
-    <%= format(gettext("By proceeding, you agree to %s\'s <a %s>Terms</a> and <a %s>Privacy Policy</a>."),
-    [ siteName || hostname,
-     ' href="' + termsOfService + '" id="rp_tos" target="_blank"',
-     ' href="' + privacyPolicy + '" id="rp_pp" target="_blank"'
-     ]) %>
+    <%= format(gettext("By proceeding, you agree to %(site)'s <a %(terms)>Terms</a> and <a %(privacy)>Privacy Policy</a>."),
+               {
+                 terms: 'href="' + termsOfService + '" id="rp_tos" target="_blank"',
+                 privacy: 'href="' + privacyPolicy + '" id="rp_pp" target="_blank"',
+                 site: siteName || hostname
+               })
+    %>
    </p>
 <% } %>
diff --git a/resources/static/dialog/views/set_password.ejs b/resources/static/dialog/views/set_password.ejs
index 87bc118de6dc64a0b61be3a84e7295bdb2b0631c..d9bb25f53333b51cfee3ea111b14468208797f98 100644
--- a/resources/static/dialog/views/set_password.ejs
+++ b/resources/static/dialog/views/set_password.ejs
@@ -19,7 +19,7 @@
               <% if (password_reset) { %>
                 <label for="password"><%= format(gettext("Create a new password to use with %s."), ["Persona"]) %></label>
               <% } else { %>
-                <label for="password"><%= format(gettext("%s"), ["<strong>" + email + "</strong>"]) %></label>
+                <label for="password"><strong><%= email %></strong></label>
               <% } %>
 
               <input id="password" type="password" maxlength="80" placeholder="<%= gettext("create password") %>"/>
@@ -70,12 +70,10 @@
 
       <% if (personaTOSPP) { %>
         <p id="persona_tospp" class="submit tospp">
-           <%= format(
-              gettext('By proceeding, you agree to %s\'s <a %s>Terms</a> and <a %s>Privacy Policy</a>.'),
-                   [ "Persona",
-                     ' href="https://login.persona.org/tos" target="_new"',
-                     ' href="https://login.persona.org/privacy" target="_new"',
-                   ]) %>
+            <%= format(gettext("By proceeding, you agree to %(site)'s <a %(terms)>Terms</a> and <a %(privacy)>Privacy Policy</a>."),
+                       { site: "Persona",
+                         terms: 'href="https://login.persona.org/tos" target="_new"',
+                         privacy: 'href="https://login.persona.org/privacy" target="_new"' }) %>
         </p>
       <% } %>
   </div>
diff --git a/resources/static/dialog/views/verify_primary_user.ejs b/resources/static/dialog/views/verify_primary_user.ejs
index d186d200fba22a1067e2f469d5cd712ed409c848..695924ac81a007098eea328f7f365730cf6de3d4 100644
--- a/resources/static/dialog/views/verify_primary_user.ejs
+++ b/resources/static/dialog/views/verify_primary_user.ejs
@@ -8,10 +8,12 @@
   </h3>
 
   <p>
-    <%= format(gettext("Persona lets you use your %s account to sign into sites like %s."), [idpName, siteName]) %>
+    <%= format(gettext("Persona lets you use your %(emailProvider) account to sign into sites like %(aWebsite)."),
+               { emailProvider: idpName, aWebsite: siteName }) %>
   </p>
   <p>
-    <%= format(gettext("Once you verify your account there, you will be signed in to %s."), [siteName]) %>
+    <%= format(gettext("Once you verify your account there, you will be signed in to %(aWebsite)."),
+               {aWebsite : siteName}) %>
   </p>
     <p class="submit cf buttonrow">
         <button id="verifyWithPrimary"><%= format(gettext("sign in with %s"), [idpName]) %></button>
@@ -20,12 +22,10 @@
 
     <% if (personaTOSPP) { %>
       <p id="persona_tospp" class="submit tospp">
-         <%= format(
-            gettext('By proceeding, you agree to %s\'s <a %s>Terms</a> and <a %s>Privacy Policy</a>.'),
-                 [ "Persona",
-                   ' href="https://login.persona.org/tos" target="_new"',
-                   ' href="https://login.persona.org/privacy" target="_new"',
-                 ]) %>
+         <%= format(gettext("By proceeding, you agree to %(site)'s <a %(terms)>Terms</a> and <a %(privacy)>Privacy Policy</a>."),
+                    { site: "Persona",
+                      terms: 'href="https://login.persona.org/tos" target="_new"',
+                      privacy: 'href="https://login.persona.org/privacy" target="_new"' }) %>
       </p>
     <% } %>
 
diff --git a/resources/static/favicon.ico b/resources/static/favicon.ico
index 57f1ad65a590cf905efd550c87a07b2223d59ccf..9e7a5e61104414fd88f01d2897eab8c6e3def5b7 100644
Binary files a/resources/static/favicon.ico and b/resources/static/favicon.ico differ
diff --git a/resources/static/pages/css/m.css b/resources/static/pages/css/m.css
index 50943635fe29a7aa2366788155d2265970643603..084d2f397e46f7944880c2e9cf5c34efd06a8c06 100644
--- a/resources/static/pages/css/m.css
+++ b/resources/static/pages/css/m.css
@@ -182,7 +182,6 @@
 
   .tour .button {
     font-size: 18px;
-    padding: 5px 15px;
   }
 
   #hint,
diff --git a/resources/static/pages/css/style.css b/resources/static/pages/css/style.css
index d73c65a9f1fbfe26c3f45fbffef015accc062cf3..b496480eeca4569031a4adbe603719ad93e96194 100644
--- a/resources/static/pages/css/style.css
+++ b/resources/static/pages/css/style.css
@@ -366,7 +366,8 @@ button.delete:active {
   border: 1px solid;
   border-radius: 7px;
   border-color: #68b8e8 #5da8dc #2f597b #5aa4d9;
-  padding: 5px 25px;
+  display:inline-block;
+  padding: 11px 25px;
   background-image: -webkit-linear-gradient(top, #42a5e1, #2970aa);
   background-image:    -moz-linear-gradient(top, #42a5e1, #2970aa);
   background-image:     -ms-linear-gradient(top, #42a5e1, #2970aa);
@@ -428,6 +429,16 @@ button.delete:active {
           border-radius: 5px;
 }
 
+#signUpForm h1 {
+  margin-bottom: 20px;
+}
+
+#signUpForm h2 {
+  margin-bottom: 20px;
+  font-weight: 300;
+  font-size: 22px;
+}
+
 #signUpForm a {
   color: #6dc7ff;
   text-shadow: 0 1px 0 #888;
@@ -489,7 +500,6 @@ button.delete:active {
 .notifications > .notification {
   border-radius: 3px;
   display: none;
-  text-align: center;
 }
 
 .notifications .notification.error {
@@ -497,6 +507,10 @@ button.delete:active {
   background-color: rgba(255,0,0,0.25);
 }
 
+.notification p {
+  margin-top: 8px;
+}
+
 
 #wrapper > header {
   font-weight: bold;
@@ -621,18 +635,12 @@ footer ul li:first-child a:hover {
 }
 
 .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;
+  overflow: hidden;
+  display: block;
 }
 
 .blurb h1, .blurb p, .blurb a, a.developers{
diff --git a/resources/static/pages/js/page_helpers.js b/resources/static/pages/js/page_helpers.js
index 47389799d5ef8393901c8132a2b9751a28b5a6c7..22df8dbbcc5ba89174aa34936d374531f38eafe0 100644
--- a/resources/static/pages/js/page_helpers.js
+++ b/resources/static/pages/js/page_helpers.js
@@ -13,7 +13,6 @@ BrowserID.PageHelpers = (function() {
       user = bid.User,
       helpers = bid.Helpers,
       dom = bid.DOM,
-      errorDisplay = bid.ErrorDisplay,
       ANIMATION_SPEED = 250,
       origStoredEmail;
 
@@ -63,7 +62,6 @@ BrowserID.PageHelpers = (function() {
   function showFailure(error, info, callback) {
     info = $.extend(info || {}, { action: error, dialog: false });
     bid.Screens.error.show("error", info);
-    errorDisplay.start();
     callback && callback(false);
   }
 
@@ -136,17 +134,12 @@ BrowserID.PageHelpers = (function() {
       throw "cannot verify with primary without an email address and URL"
     }
 
-    var url = helpers.toURL(baseURL, {
-        email: email,
-        return_to: "https://login.persona.org/authenticate_with_primary#complete"
-    });
-
     winchan.open({
       url: "https://login.persona.org/authenticate_with_primary",
       // This is the relay that will be used when the IdP redirects to sign_in_complete
       relay_url: "https://login.persona.org/relay",
       window_features: "width=700,height=375",
-      params: url
+      params: helpers.toURL(baseURL, {email: email})
     }, function(error, result) {
       // We have to force a reset of the primary caches because the user's
       // authentication status may be incorrect.
diff --git a/resources/static/test/cases/common/js/browser-support.js b/resources/static/test/cases/common/js/browser-support.js
index 5acce790f6855f07596d3b3ced3aaf31e9f89497..53c55c97151c4c4100c1953816e659e62fadefca 100644
--- a/resources/static/test/cases/common/js/browser-support.js
+++ b/resources/static/test/cases/common/js/browser-support.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: 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
diff --git a/resources/static/test/cases/common/js/class.js b/resources/static/test/cases/common/js/class.js
index bf9306ac693ccdcca2be44b528dfec6f9d19997c..1180ae6ca049e2530d9bb875ef969c9ccd87ebce 100644
--- a/resources/static/test/cases/common/js/class.js
+++ b/resources/static/test/cases/common/js/class.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: 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
diff --git a/resources/static/test/cases/common/js/command.js b/resources/static/test/cases/common/js/command.js
index 0fd003bb0f9f79143a4e813b1b5ae4b1004f6859..ebe06d3ca909a3a8002fa9646677e019c319d2a8 100644
--- a/resources/static/test/cases/common/js/command.js
+++ b/resources/static/test/cases/common/js/command.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: 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
diff --git a/resources/static/test/cases/common/js/enable_cookies_url.js b/resources/static/test/cases/common/js/enable_cookies_url.js
index fb48744f1b53ec23a9a084d8949949c5653318c7..d6256521dfd87becded3fd77eb49c1e98221a7ad 100644
--- a/resources/static/test/cases/common/js/enable_cookies_url.js
+++ b/resources/static/test/cases/common/js/enable_cookies_url.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: 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
diff --git a/resources/static/test/cases/common/js/error-display.js b/resources/static/test/cases/common/js/error-display.js
deleted file mode 100644
index a3903093820de3fa84ba236b6a47d1dc31130607..0000000000000000000000000000000000000000
--- a/resources/static/test/cases/common/js/error-display.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
-/*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/. */
-(function() {
-  "use strict";
-
-  var bid = BrowserID,
-      errorDisplay = bid.ErrorDisplay;
-
-  module("shared/error-display", {
-    setup: function() {
-        $("#error").html("<div class='contents'><a href='#' id='openMoreInfo'>Open</a><div id='moreInfo' style='display:none'>Expanded Info</div></div>");
-    },
-    teardown: function() {
-      $("#error").hide();
-    }
-  });
-
-  asyncTest("can initialize and open the error display", function() {
-    $("#error").show();
-    bid.ErrorDisplay.start("#error");
-    bid.ErrorDisplay.open();
-
-    setTimeout(function() {
-      ok($("#moreInfo").is(":visible"), "expanded info is visible");
-      start();
-    }, 100);
-  });
-
-
-
-}());
diff --git a/resources/static/test/cases/common/js/helpers.js b/resources/static/test/cases/common/js/helpers.js
index 263cad7e298134de8a481f9c94c3e34102a56d60..8d9c97ebd3e2302334c1dac627539d7088bb3d16 100644
--- a/resources/static/test/cases/common/js/helpers.js
+++ b/resources/static/test/cases/common/js/helpers.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/common/js/history.js b/resources/static/test/cases/common/js/history.js
index 7d29e4d33a94a6eb9ee5293637b393cee725c332..394ccc23acbfe7b527435cc262e791070c8cecbe 100644
--- a/resources/static/test/cases/common/js/history.js
+++ b/resources/static/test/cases/common/js/history.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: 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
diff --git a/resources/static/test/cases/common/js/models/interaction_data.js b/resources/static/test/cases/common/js/models/interaction_data.js
index 8f229c141f738196db302ba9e43c11edf46f30bf..b09f8a2ad008612109786aaa3aafb76cfe8fed63 100644
--- a/resources/static/test/cases/common/js/models/interaction_data.js
+++ b/resources/static/test/cases/common/js/models/interaction_data.js
@@ -1,5 +1,5 @@
 
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/common/js/modules/cookie_check.js b/resources/static/test/cases/common/js/modules/cookie_check.js
index b6358ba50fe1571fec66ee894aa40d8cacd4a6d3..593f8a72b41c96f0adef1419d267387cfeeaebb2 100644
--- a/resources/static/test/cases/common/js/modules/cookie_check.js
+++ b/resources/static/test/cases/common/js/modules/cookie_check.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/common/js/modules/extended-info.js b/resources/static/test/cases/common/js/modules/extended-info.js
new file mode 100644
index 0000000000000000000000000000000000000000..8173ca40df51eadc06a73082be1bbed064f2ca91
--- /dev/null
+++ b/resources/static/test/cases/common/js/modules/extended-info.js
@@ -0,0 +1,33 @@
+/*jshint browser:true, jquery: true, forin: true, laxbreak:true */
+/*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/. */
+
+(function() {
+  "use strict";
+
+  var bid = BrowserID,
+      ExtendedInfo = bid.Modules.ExtendedInfo;
+
+  module("common/js/modules/extended-info", {
+    setup: function() {
+        $("#error").html("<div class='contents'><a href='#' class='openMoreInfo'>Open</a><div class='moreInfo' style='display:none'>Expanded Info</div></div>");
+    },
+    teardown: function() {
+      $("#error").hide();
+    }
+  });
+
+  asyncTest("can initialize and open the extended info", function openExtendedInfo() {
+    $("#error").show();
+    var errorDisplay = ExtendedInfo.create();
+    errorDisplay.start({ target: "#error" });
+    errorDisplay.open(function() {
+      ok($("#error .moreInfo").is(":visible"), "expanded info is visible");
+      start();
+    });
+  });
+
+
+}());
diff --git a/resources/static/test/cases/common/js/modules/interaction_data.js b/resources/static/test/cases/common/js/modules/interaction_data.js
index 434631b9b747fe58b7cd8b979325edef22ee1112..eadf72fa0db55e4fa480598c97a47f340104f2ff 100644
--- a/resources/static/test/cases/common/js/modules/interaction_data.js
+++ b/resources/static/test/cases/common/js/modules/interaction_data.js
@@ -1,5 +1,5 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
-/*global test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */
+/*jshint browser: true, forin: true, laxbreak: true */
+/*global test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true, asyncTest: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/. */
@@ -9,6 +9,7 @@
   var bid = BrowserID,
       testHelpers = bid.TestHelpers,
       network = bid.Network,
+      storage = bid.Storage,
       model = bid.Models.InteractionData,
       xhr = bid.Mocks.xhr,
       mediator = bid.Mediator,
@@ -36,18 +37,20 @@
     controller = BrowserID.Modules.InteractionData.create();
     controller.start(config);
 
-    controller.setNameTable({
-      before_session_context: null,
-      after_session_context: null,
-      session1_before_session_context: null,
-      session1_after_session_context: null,
-      session2_before_session_context: null,
-      session2_after_session_context: null,
-      initial_string_name: "translated_name",
-      initial_function_name: function(msg, data) {
-        return "function_translation." + msg;
-      }
-    });
+    if (setKPINameTable) {
+      controller.setNameTable({
+        before_session_context: null,
+        after_session_context: null,
+        session1_before_session_context: null,
+        session1_after_session_context: null,
+        session2_before_session_context: null,
+        session2_after_session_context: null,
+        initial_string_name: "translated_name",
+        initial_function_name: function(msg, data) {
+          return "function_translation." + msg;
+        }
+      });
+    }
 
   }
 
@@ -69,7 +72,7 @@
     // simulate data stored for last session
     model.push({ timestamp: new Date().getTime() });
 
-    createController();
+    createController(true);
 
     controller.addEvent("before_session_context");
 
@@ -122,7 +125,7 @@
   });
 
   asyncTest("samplingEnabled set to false - no data collection occurs", function() {
-    createController({ samplingEnabled: false });
+    createController(true, { samplingEnabled: false });
 
     // the initial with_context will send off any stored data, there should be
     // no stored data.
@@ -140,7 +143,7 @@
   });
 
   asyncTest("continue: true, data collection permitted on previous session - continue appending data to previous session", function() {
-    createController();
+    createController(true);
 
     controller.addEvent("session1_before_session_context");
     network.withContext(function() {
@@ -150,7 +153,7 @@
       // re-get session context.
       controller = null;
       network.clearContext();
-      createController({ continuation: true });
+      createController(true, { continuation: true });
 
       controller.addEvent("session2_before_session_context");
       network.withContext(function() {
@@ -257,4 +260,124 @@
     });
   });
 
+  asyncTest("kpi orphans are adopted if user.staged and user is signed in", function() {
+    // 1. user.user_staged
+    // 2. dialog is orphaned
+    // 3. user comes back, authenticated
+    // 4. the orphan found a good home
+    createController(false);
+    network.withContext(function() {
+      // user is staged
+      controller.addEvent("user_staged");
+      // dialog all done, its orphaned, oh noes! think of the kids!
+      mediator.publish("kpi_data", {
+        orphaned: true
+      });
+      network.clearContext();
+
+
+      // new page
+      createController(false);
+      // make user authenticated
+      xhr.setContextInfo("auth_level", "password");
+      network.withContext(function() {
+        var request = xhr.getLastRequest('/wsapi/interaction_data');
+        var data = JSON.parse(request.data).data[0];
+        equal(data.orphaned, false, "orphaned is not sent");
+        start();
+      });
+    });
+  });
+
+  asyncTest("kpi orphans are NOT adopted if NOT user.staged and user is signed in", function() {
+    // 1. user was not staged
+    // 2. dialog is orphaned
+    // 3. user comes back, authenticated
+    // 4. but he wasn't staged, so dont adopt
+    createController(false);
+    network.withContext(function() {
+      // dialog all done, its orphaned, oh noes! think of the kids!
+      mediator.publish("kpi_data", {
+        orphaned: true
+      });
+      network.clearContext();
+
+
+      // new page
+      createController(false);
+      // make user authenticated
+      xhr.setContextInfo("auth_level", "password");
+      network.withContext(function() {
+        var request = xhr.getLastRequest('/wsapi/interaction_data');
+        var data = JSON.parse(request.data).data[0];
+        equal(data.orphaned, true, "orphaned is sent");
+        start();
+      });
+    });
+  });
+
+    asyncTest("kpi orphans are adopted if add_email and email count increased", function() {
+    // 1. email_staged
+    // 2. dialog is orphaned
+    // 3. email is verified
+    // 4. user comes back, authenticated
+    // 5. the orphan found a good home
+    createController(false);
+    network.withContext(function() {
+      // email is staged
+      controller.addEvent("email_staged");
+      // dialog all done, its orphaned, oh noes! think of the kids!
+      mediator.publish("kpi_data", {
+        orphaned: true,
+        number_emails: storage.getEmailCount() || 0
+      });
+      network.clearContext();
+
+      // email is verified
+      storage.addSecondaryEmail("testuser@testuser.org");
+
+      // new page
+      createController(false);
+      // make user authenticated
+      xhr.setContextInfo("auth_level", "password");
+      network.withContext(function() {
+        var request = xhr.getLastRequest('/wsapi/interaction_data');
+        var data = JSON.parse(request.data).data[0];
+        equal(data.orphaned, false, "orphaned is not sent");
+        start();
+      });
+    });
+  });
+
+  asyncTest("kpi orphans are NOT adopted if add_email but email count is same", function() {
+    // 1. email staged
+    // 2. dialog is orphaned
+    // 3. user comes back, authenticated
+    // 4. but no new email, so oprhan is true
+    createController(false);
+    network.withContext(function() {
+      // user is staged
+      controller.addEvent("email_staged");
+      // dialog all done, its orphaned, oh noes! think of the kids!
+      mediator.publish("kpi_data", {
+        orphaned: true,
+        number_emails: storage.getEmailCount() || 0
+      });
+      network.clearContext();
+
+      // user never confirms
+
+      // new page
+      createController(false);
+      // make user authenticated
+      xhr.setContextInfo("auth_level", "password");
+      network.withContext(function() {
+        var request = xhr.getLastRequest('/wsapi/interaction_data');
+        var data = JSON.parse(request.data).data[0];
+        equal(data.orphaned, true, "orphaned is sent");
+        start();
+      });
+    });
+  });
+
 }());
diff --git a/resources/static/test/cases/common/js/modules/page_module.js b/resources/static/test/cases/common/js/modules/page_module.js
index ebd90fdba6b1f573a564ed409aca4196a781926a..da03cc638b5142d7ef25d6bec2b95f9e4866b5b4 100644
--- a/resources/static/test/cases/common/js/modules/page_module.js
+++ b/resources/static/test/cases/common/js/modules/page_module.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/common/js/modules/xhr_delay.js b/resources/static/test/cases/common/js/modules/xhr_delay.js
index 29585e1dbc3501a6a1903cd25ace5bac62f6982e..81e7c06d7b6ad3412c489a14f8b00cd88266248a 100644
--- a/resources/static/test/cases/common/js/modules/xhr_delay.js
+++ b/resources/static/test/cases/common/js/modules/xhr_delay.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/common/js/modules/xhr_disable_form.js b/resources/static/test/cases/common/js/modules/xhr_disable_form.js
index 73ac45e8d51c2ecfea0852824d5d8d8b427df450..71efa49c4dde6ce1e93a87604b0460caf398e704 100644
--- a/resources/static/test/cases/common/js/modules/xhr_disable_form.js
+++ b/resources/static/test/cases/common/js/modules/xhr_disable_form.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/common/js/network.js b/resources/static/test/cases/common/js/network.js
index 4ce7680923dd3d0f03d8c57f771d69d461ae7e53..5fac936df6c34fa014b5c2e2cf59d2b36de319d9 100644
--- a/resources/static/test/cases/common/js/network.js
+++ b/resources/static/test/cases/common/js/network.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global asyncTest: true, start: true, stop: 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
diff --git a/resources/static/test/cases/common/js/renderer.js b/resources/static/test/cases/common/js/renderer.js
index ab6f605d9a8a00ce841d8bb9ea618bef7ff18b3c..f3895f8665466b124c85ccc0680c2abd6d49c3d5 100644
--- a/resources/static/test/cases/common/js/renderer.js
+++ b/resources/static/test/cases/common/js/renderer.js
@@ -1,4 +1,4 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*jshint browser:true, jquery: true, forin: true, laxbreak:true */
 /*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
diff --git a/resources/static/test/cases/common/js/screens.js b/resources/static/test/cases/common/js/screens.js
index 98f75978889c33ddbae761a25fb628dfa71c67b9..c989d5cfb43c6df7f1220f74f2e6962eac618f46 100644
--- a/resources/static/test/cases/common/js/screens.js
+++ b/resources/static/test/cases/common/js/screens.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/common/js/state_machine.js b/resources/static/test/cases/common/js/state_machine.js
index 93f5eb58fe26fba3d811ae1f490c9299b0dee7f6..60b5982ccb4e8f76f363914e25c18400b9747d0c 100644
--- a/resources/static/test/cases/common/js/state_machine.js
+++ b/resources/static/test/cases/common/js/state_machine.js
@@ -1,4 +1,4 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*jshint browser:true, jquery: true, forin: true, laxbreak:true */
 /*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
diff --git a/resources/static/test/cases/common/js/storage.js b/resources/static/test/cases/common/js/storage.js
index 2b7fd3d0789b6c3d171d816a3a6500c0efd7c50a..a347507f8e6bc74677a978db6af86c2c878fbace 100644
--- a/resources/static/test/cases/common/js/storage.js
+++ b/resources/static/test/cases/common/js/storage.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/common/js/tooltip.js b/resources/static/test/cases/common/js/tooltip.js
index 16605f018947b1d5d1c80d5eba7176bab9ab24c8..b078911203a99b71d29d57439647c2c9ff8fb2ab 100644
--- a/resources/static/test/cases/common/js/tooltip.js
+++ b/resources/static/test/cases/common/js/tooltip.js
@@ -1,4 +1,4 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*jshint browser:true, jquery: true, forin: true, laxbreak:true */
 /*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
diff --git a/resources/static/test/cases/common/js/user.js b/resources/static/test/cases/common/js/user.js
index 271fa30d0f502bba33494653bb99f41297f3c2c4..8713e750ffe2d67d86d74d2de0362bb43db82a2d 100644
--- a/resources/static/test/cases/common/js/user.js
+++ b/resources/static/test/cases/common/js/user.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, module: true, ok: true, equal: true, strictEqual: 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
@@ -175,15 +175,14 @@ var jwcrypto = require("./lib/jwcrypto");
 
   asyncTest("createPrimaryUser with primary, user verified with primary - expect 'primary.verified'", function() {
     xhr.useResult("primary");
-    provisioning.setStatus(provisioning.AUTHENTICATED, function() {
-      lib.createPrimaryUser({email: "unregistered@testuser.com"}, function(status) {
-        equal(status, "primary.verified", "primary user is already verified, correct status");
-        network.checkAuth(function(authenticated) {
-          equal(authenticated, "assertion", "after provisioning user, user should be automatically authenticated to Persona");
-          start();
-        });
-      }, testHelpers.unexpectedXHRFailure);
-    });
+    provisioning.setStatus(provisioning.AUTHENTICATED);
+    lib.createPrimaryUser({email: "unregistered@testuser.com"}, function(status) {
+      equal(status, "primary.verified", "primary user is already verified, correct status");
+      network.checkAuth(function(authenticated) {
+        equal(authenticated, "assertion", "after provisioning user, user should be automatically authenticated to Persona");
+        start();
+      });
+    }, testHelpers.unexpectedXHRFailure);
   });
 
   asyncTest("createPrimaryUser with primary, user must authenticate with primary - expect 'primary.verify'", function() {
diff --git a/resources/static/test/cases/common/js/validation.js b/resources/static/test/cases/common/js/validation.js
index 4308cd83d6f320bee6ce5d4fc31a5fb6ff06a735..2ca280e8df228b9dec34dd96c91fdc40de5bf3e9 100644
--- a/resources/static/test/cases/common/js/validation.js
+++ b/resources/static/test/cases/common/js/validation.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/common/js/xhr.js b/resources/static/test/cases/common/js/xhr.js
index 2edd1f7dd3a2de1d8186f2e8037e77f8070adde9..b1a06464dfd01bf1ada65238323e5407f9153a92 100644
--- a/resources/static/test/cases/common/js/xhr.js
+++ b/resources/static/test/cases/common/js/xhr.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global asyncTest: true, start: true, stop: 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
diff --git a/resources/static/test/cases/dialog/js/misc/helpers.js b/resources/static/test/cases/dialog/js/misc/helpers.js
index 7bd85e9961a480f12562fdf27784707f64259d9f..edc1fa98a8a5417630559085b2218cc2ee9a66fc 100644
--- a/resources/static/test/cases/dialog/js/misc/helpers.js
+++ b/resources/static/test/cases/dialog/js/misc/helpers.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/dialog/js/misc/internal_api.js b/resources/static/test/cases/dialog/js/misc/internal_api.js
index 5e3ebfe4c4a561e22b3a110c65ec4b57ad9f812b..f8a1de54a084e0879acca63400417ab5196a2c95 100644
--- a/resources/static/test/cases/dialog/js/misc/internal_api.js
+++ b/resources/static/test/cases/dialog/js/misc/internal_api.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: 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
diff --git a/resources/static/test/cases/dialog/js/misc/state.js b/resources/static/test/cases/dialog/js/misc/state.js
index 3d384a0eb72c216073c080033acbfdf4d148d6aa..d6535638fd817d6385d7950077102dfbffd5fb72 100644
--- a/resources/static/test/cases/dialog/js/misc/state.js
+++ b/resources/static/test/cases/dialog/js/misc/state.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/dialog/js/modules/actions.js b/resources/static/test/cases/dialog/js/modules/actions.js
index 27c54f781f818759fa4527d1c1c0910675b5bcee..3f98fc81b8d968879c1c34738a00b4eab5500111 100644
--- a/resources/static/test/cases/dialog/js/modules/actions.js
+++ b/resources/static/test/cases/dialog/js/modules/actions.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
@@ -75,12 +75,12 @@
   });
 
   asyncTest("doConfirmUser - start the check_registration service", function() {
-    testActionStartsModule("doConfirmUser", {email: TEST_EMAIL},
+    testActionStartsModule("doConfirmUser", {email: TEST_EMAIL, siteName: "Unit Test Site"},
       "check_registration");
   });
 
   asyncTest("doConfirmEmail - start the check_registration service", function() {
-    testActionStartsModule("doConfirmEmail", {email: TEST_EMAIL},
+    testActionStartsModule("doConfirmEmail", {email: TEST_EMAIL, siteName: "Unit Test Site"},
       "check_registration");
   });
 
diff --git a/resources/static/test/cases/dialog/js/modules/add_email.js b/resources/static/test/cases/dialog/js/modules/add_email.js
index 43f19e385b3091fcdd8411ee30f0c6891f38a123..7e7000ca556cc3953ef6ec898a7c96a37c2dc8df 100644
--- a/resources/static/test/cases/dialog/js/modules/add_email.js
+++ b/resources/static/test/cases/dialog/js/modules/add_email.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/dialog/js/modules/authenticate.js b/resources/static/test/cases/dialog/js/modules/authenticate.js
index 6827fce0d83f54e2cb3f554eba3c990c8e055ce1..7952811e9e066da8631b160d9780d0b68453f112 100644
--- a/resources/static/test/cases/dialog/js/modules/authenticate.js
+++ b/resources/static/test/cases/dialog/js/modules/authenticate.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/dialog/js/modules/check_registration.js b/resources/static/test/cases/dialog/js/modules/check_registration.js
index 03a01bf0a23fed05da0e3de64fab2eb43843e9eb..82da23a350875070a5a3b925341c19fa0d5e3f29 100644
--- a/resources/static/test/cases/dialog/js/modules/check_registration.js
+++ b/resources/static/test/cases/dialog/js/modules/check_registration.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
@@ -19,7 +19,8 @@
       email: "registered@testuser.com",
       verifier: verifier,
       verificationMessage: message,
-      required: required
+      required: required,
+      siteName: "Unit Test Site"
     });
   }
 
diff --git a/resources/static/test/cases/dialog/js/modules/dialog.js b/resources/static/test/cases/dialog/js/modules/dialog.js
index 2519af8c14c3c13e0c77ae4b66b80fcd156036fc..87ea235e58aaf7a7b8738569c5bcefd3f477dd09 100644
--- a/resources/static/test/cases/dialog/js/modules/dialog.js
+++ b/resources/static/test/cases/dialog/js/modules/dialog.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/dialog/js/modules/forgot_password.js b/resources/static/test/cases/dialog/js/modules/forgot_password.js
index d81d8b1de331c895aab433830b56b639d8744411..4d782af44d9c8f7d004bc09fdfe2a487301455b2 100644
--- a/resources/static/test/cases/dialog/js/modules/forgot_password.js
+++ b/resources/static/test/cases/dialog/js/modules/forgot_password.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/dialog/js/modules/generate_assertion.js b/resources/static/test/cases/dialog/js/modules/generate_assertion.js
index 436cffe82c87df0ec7a1c26ef8de935ddfdb5d62..04201f05e33a12103334a3275476e3c45f5329c0 100644
--- a/resources/static/test/cases/dialog/js/modules/generate_assertion.js
+++ b/resources/static/test/cases/dialog/js/modules/generate_assertion.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global asyncTest: true, test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/dialog/js/modules/is_this_your_computer.js b/resources/static/test/cases/dialog/js/modules/is_this_your_computer.js
index 9e4945fd0873ab92be0ff0480d6cb757163aa6ff..cc31ae506656633f24b9b961339642b5c7862cb0 100644
--- a/resources/static/test/cases/dialog/js/modules/is_this_your_computer.js
+++ b/resources/static/test/cases/dialog/js/modules/is_this_your_computer.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/dialog/js/modules/pick_email.js b/resources/static/test/cases/dialog/js/modules/pick_email.js
index 555d2f4631c63ae0c9949339feab0e389c180e2c..ff7b28eb56aaf4720d933daecc8a868563313476 100644
--- a/resources/static/test/cases/dialog/js/modules/pick_email.js
+++ b/resources/static/test/cases/dialog/js/modules/pick_email.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/dialog/js/modules/primary_user_provisioned.js b/resources/static/test/cases/dialog/js/modules/primary_user_provisioned.js
index fb228c05c1ed1a9a2e25648e2cc88547fc53989c..e17d3e4c5a0791b57677c963f924a42b88cf4b3f 100644
--- a/resources/static/test/cases/dialog/js/modules/primary_user_provisioned.js
+++ b/resources/static/test/cases/dialog/js/modules/primary_user_provisioned.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/dialog/js/modules/provision_primary_user.js b/resources/static/test/cases/dialog/js/modules/provision_primary_user.js
index 18ecf1133902e243fea8da47bc1901c7935b864a..d5c540849802c80b10796e8efd7677491cb51645 100644
--- a/resources/static/test/cases/dialog/js/modules/provision_primary_user.js
+++ b/resources/static/test/cases/dialog/js/modules/provision_primary_user.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
@@ -10,6 +10,7 @@
       bid = BrowserID,
       storage = bid.Storage,
       user = bid.User,
+      network = bid.Network,
       register = bid.TestHelpers.register,
       xhr = bid.Mocks.xhr,
       mediator = bid.Mediator,
@@ -56,6 +57,7 @@
 
   asyncTest("create controller with all fields specified, user authenticated with primary - expected user provisioned", function() {
     provisioning.setStatus(provisioning.AUTHENTICATED);
+    xhr.useResult("primary");
 
     mediator.subscribe("primary_user_provisioned", function(msg, info) {
       ok(info.assertion, "assertion available");
@@ -72,6 +74,7 @@
 
   asyncTest("create controller with all fields specified, user not authenticated with primary - expected user must authenticate", function() {
     provisioning.setStatus(provisioning.NOT_AUTHENTICATED);
+    xhr.useResult("primary");
 
     mediator.subscribe("primary_user_unauthenticated", function(msg, info) {
       equal(info.auth_url, "https://auth_url", "primary information fetched");
@@ -86,8 +89,8 @@
   });
 
   asyncTest("create controller with missing auth/prov, user authenticated with primary - expected to request provisioning info from backend, user provisioned", function() {
-    xhr.useResult("primary");
     provisioning.setStatus(provisioning.AUTHENTICATED);
+    xhr.useResult("primary");
 
     mediator.subscribe("primary_user_provisioned", function(msg, info) {
       equal(info.email, "unregistered@testuser.com", "user is provisioned after requesting info from backend");
diff --git a/resources/static/test/cases/dialog/js/modules/required_email.js b/resources/static/test/cases/dialog/js/modules/required_email.js
index 97800512f5c0df71e88c4529c20d7fc3e71b3658..2c42d0c1ed1df24e263d1d68c32d93f0a0bc7384 100644
--- a/resources/static/test/cases/dialog/js/modules/required_email.js
+++ b/resources/static/test/cases/dialog/js/modules/required_email.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global asyncTest: true, test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/dialog/js/modules/rp_info.js b/resources/static/test/cases/dialog/js/modules/rp_info.js
index c50a3127c4253f84ef0663a2e1836b3db32ecfb5..a73fe472c680de4d0ce16af7700835e9901e0f93 100644
--- a/resources/static/test/cases/dialog/js/modules/rp_info.js
+++ b/resources/static/test/cases/dialog/js/modules/rp_info.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/dialog/js/modules/set_password.js b/resources/static/test/cases/dialog/js/modules/set_password.js
index 0d64302f07c16ac0042558e60dd26840fdc97db3..529ef0d1c18aa7b3c44e7d60c91cd1fe18460396 100644
--- a/resources/static/test/cases/dialog/js/modules/set_password.js
+++ b/resources/static/test/cases/dialog/js/modules/set_password.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/dialog/js/modules/verify_primary_user.js b/resources/static/test/cases/dialog/js/modules/verify_primary_user.js
index 08083a9fe9d3d0ef7908a0d18c1e958a8bed00e9..cce8e0c1932acffa2e02dd1ec52aadb9af8ea81a 100644
--- a/resources/static/test/cases/dialog/js/modules/verify_primary_user.js
+++ b/resources/static/test/cases/dialog/js/modules/verify_primary_user.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global asyncTest: true, test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/include.js b/resources/static/test/cases/include.js
index 0cadc3e3576f2241abf572d26c291b005f5b2993..35cfd9894fd6822df4dc2d2f32a3895634ca5d8f 100644
--- a/resources/static/test/cases/include.js
+++ b/resources/static/test/cases/include.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global test: true, start: true, stop: 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
diff --git a/resources/static/test/cases/pages/js/about.js b/resources/static/test/cases/pages/js/about.js
index 298642ce39fc66e35c73f04e9a6be107b781ebdd..e00fbf5674a9dd6efc337b1005649df49891ebcd 100644
--- a/resources/static/test/cases/pages/js/about.js
+++ b/resources/static/test/cases/pages/js/about.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: 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
diff --git a/resources/static/test/cases/pages/js/browserid.js b/resources/static/test/cases/pages/js/browserid.js
index 0232715a3d8e1b9c1c3775636e0e82cba4694ee5..661382d958c69eda4ce6d686eeaa2ca770d32198 100644
--- a/resources/static/test/cases/pages/js/browserid.js
+++ b/resources/static/test/cases/pages/js/browserid.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: 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
diff --git a/resources/static/test/cases/pages/js/forgot.js b/resources/static/test/cases/pages/js/forgot.js
index d1b1466193204d8a576cd6fc0a99bdc23ab57b39..120026480de3836ae0791345d500eda702970d84 100644
--- a/resources/static/test/cases/pages/js/forgot.js
+++ b/resources/static/test/cases/pages/js/forgot.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: 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
diff --git a/resources/static/test/cases/pages/js/manage_account.js b/resources/static/test/cases/pages/js/manage_account.js
index c40870aac35ff7e2c0fc16a078a7106e91f47f5d..d4fc4b1f8412f55b978e458133608e92389f24f8 100644
--- a/resources/static/test/cases/pages/js/manage_account.js
+++ b/resources/static/test/cases/pages/js/manage_account.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: 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
diff --git a/resources/static/test/cases/pages/js/page_helpers.js b/resources/static/test/cases/pages/js/page_helpers.js
index 5df6b052f6f8d79a3539b7d453da436bdc361824..40b8b22f17bd5c27681ec8c4fe1f586c438e4279 100644
--- a/resources/static/test/cases/pages/js/page_helpers.js
+++ b/resources/static/test/cases/pages/js/page_helpers.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: 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
@@ -15,7 +15,7 @@
       xhr = bid.Mocks.xhr,
       errors = bid.Errors;
 
-  module("pages/page_helpers", {
+  module("pages/js/page_helpers", {
     setup: function() {
       testHelpers.setup();
       winMock = new WindowMock();
@@ -154,12 +154,12 @@
       // We have to make sure the error screen itself is visible and that the
       // extra info is hidden so when we click on the extra info it opens.
       $("#error").show();
-      $("#moreInfo").hide();
-      $("#openMoreInfo").trigger("click");
+      $("#error .moreInfo").hide();
+      $("#error .openMoreInfo").trigger("click");
 
       // Add a bit of delay to wait for the animation
       setTimeout(function() {
-        equal($("#moreInfo").is(":visible"), true, "extra info is visible after click");
+        equal($("#error .moreInfo").is(":visible"), true, "extra info is visible after click");
         start();
       }, 100);
 
diff --git a/resources/static/test/cases/pages/js/signin.js b/resources/static/test/cases/pages/js/signin.js
index 79baa83d22529f7981ad6f6d499ce7cec383d999..4927cb5e602a668eeb4ba1b524a12a280e511928 100644
--- a/resources/static/test/cases/pages/js/signin.js
+++ b/resources/static/test/cases/pages/js/signin.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: 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
diff --git a/resources/static/test/cases/pages/js/signup.js b/resources/static/test/cases/pages/js/signup.js
index 92c2cd00573462efdfb27549a73f4711ab56fc7f..7db9cae261cf2840ab49f59b2d1e31eeadf896c4 100644
--- a/resources/static/test/cases/pages/js/signup.js
+++ b/resources/static/test/cases/pages/js/signup.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: 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
diff --git a/resources/static/test/cases/pages/js/verify_secondary_address.js b/resources/static/test/cases/pages/js/verify_secondary_address.js
index ac4ddaa8b832c09fe5a03cd19c8a49a29fdcd37e..f15227bc0934937982552abb941804c3fe69463f 100644
--- a/resources/static/test/cases/pages/js/verify_secondary_address.js
+++ b/resources/static/test/cases/pages/js/verify_secondary_address.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: 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
diff --git a/resources/static/test/mocks/cachify.js b/resources/static/test/mocks/cachify.js
index 7f95a7aa46a82b816d4c07b6967136c6fb22d342..6b1abe0c7827fe1ba21c787a5671a50ff5f38dcb 100644
--- a/resources/static/test/mocks/cachify.js
+++ b/resources/static/test/mocks/cachify.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: 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/. */
diff --git a/resources/static/test/mocks/mocks.js b/resources/static/test/mocks/mocks.js
index 21d167af5df37e07262b87c6331a3258ff24bb37..73a115855dac2d6e115489ded66c1662b29c5794 100644
--- a/resources/static/test/mocks/mocks.js
+++ b/resources/static/test/mocks/mocks.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global 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
diff --git a/resources/static/test/mocks/provisioning.js b/resources/static/test/mocks/provisioning.js
index cdee24b1306b946483456f8fc2fad4e6ca16de3a..13907e4d7b981c59658f65268b32cec9f8cdfe8c 100644
--- a/resources/static/test/mocks/provisioning.js
+++ b/resources/static/test/mocks/provisioning.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global 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
@@ -16,12 +16,26 @@ BrowserID.Mocks.Provisioning = (function() {
 
   function Provisioning(info, onsuccess, onfailure) {
     if(status === Provisioning.AUTHENTICATED) {
-      onsuccess(keypair, cert);
+      if (!keypair) {
+        // JWCrypto relies on there being a random seed.  The random seed is
+        // gotten whenever network.withContext is called.  Since this is
+        // supposed to mock the IdP provisioning step which will not call
+        // network.withContext, add a random seed to ensure that we can get our
+        // keypair.
+        jwcrypto.addEntropy("H+ZgKuhjVckv/H4i0Qvj/JGJEGDVOXSIS5RCOjY9/Bo=");
+        jwcrypto.generateKeypair({algorithm: "DS", keysize: 256}, function(err, kp) {
+          keypair = kp;
+          if (onsuccess) onsuccess(keypair, cert);
+        });
+      }
+      else {
+        if (onsuccess) onsuccess(keypair, cert);
+      }
     }
     else onfailure(failure);
   }
 
-  Provisioning.setStatus = function(newStatus, cb) {
+  Provisioning.setStatus = function(newStatus) {
     failure = null;
 
     status = newStatus;
@@ -31,14 +45,6 @@ BrowserID.Mocks.Provisioning = (function() {
         code: "primaryError",
         msg: "user is not authenticated as target user"
       };
-      if (cb) cb();
-    }
-    else if(newStatus === Provisioning.AUTHENTICATED) {
-      if (!keypair)
-        jwcrypto.generateKeypair({algorithm: "DS", keysize: 256}, function(err, kp) {
-          keypair = kp;
-          if (cb) cb();
-        });
     }
   };
 
diff --git a/resources/static/test/mocks/winchan.js b/resources/static/test/mocks/winchan.js
index 43ecf8dc51c519fdf173298db27290ec23ac2d78..cab49e566953dcf7c2b3f214afef4ea41710bc83 100644
--- a/resources/static/test/mocks/winchan.js
+++ b/resources/static/test/mocks/winchan.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global 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
diff --git a/resources/static/test/mocks/window.js b/resources/static/test/mocks/window.js
index a633ba0d20180139f7bcca5d6230f0d30379c1aa..e41e909e232e8d478792b234fb1fa628550dc5cb 100644
--- a/resources/static/test/mocks/window.js
+++ b/resources/static/test/mocks/window.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global 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
diff --git a/resources/static/test/mocks/xhr.js b/resources/static/test/mocks/xhr.js
index e2d56028acabded23da7beaeea1bfa7726cfc26b..95f6b1d192ce8d0bb9ddd7063a796daa51b0af63 100644
--- a/resources/static/test/mocks/xhr.js
+++ b/resources/static/test/mocks/xhr.js
@@ -1,4 +1,4 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
+/*jshint browser: true, forin: true, laxbreak: true */
 /*global start: true, stop: 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
diff --git a/resources/views/about.ejs b/resources/views/about.ejs
index 4ea433948b01e4d7b87b5c5957fad13fcda54322..3c41de497f35575cacf7ae8c847da2991dde5b22 100644
--- a/resources/views/about.ejs
+++ b/resources/views/about.ejs
@@ -9,7 +9,7 @@
             <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>
+                    <p>Sites such as <a href="http://crossword.thetimes.co.uk/" target="_blank">The Times Crossword</a>, <a href="http://current.openphoto.me/" target="_blank">OpenPhoto</a> and <a href="https://www.voo.st/" target="_blank">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">
@@ -42,7 +42,7 @@
             </article>
         </section>
 
-        <a href="https://developer.mozilla.org/en/BrowserID/Quick_Setup" class="developers"><img src="<%- cachify('/pages/i/developers-link.png') %>" alt="Persona for developers"><span>Implement Persona on your site </span>Developer guides and API documentation</a>
+        <a href="https://developer.mozilla.org/en/BrowserID/Quick_Setup" class="developers" target="_blank"><img src="<%- cachify('/pages/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/authenticate_with_primary.ejs b/resources/views/authenticate_with_primary.ejs
index 8039a33763f86aa160a92570dbd9ea31f43c90c9..45f8dab1d09c80d294440ce144536cd12cac2259 100644
--- a/resources/views/authenticate_with_primary.ejs
+++ b/resources/views/authenticate_with_primary.ejs
@@ -6,7 +6,6 @@
 <head>
   <meta charset="utf-8">
   <title>Browser ID</title>
-  <script type='text/javascript' src='https://static.login.persona.org/lib/winchan.js'></script>
-  <script type='text/javascript' src='https://static.login.persona.org/auth_with_idp/main.js'></script>
+  <%- cachify_js('/production/authenticate_with_primary.js') %>
 </head>
 </html>
diff --git a/resources/views/cookies_disabled.ejs b/resources/views/cookies_disabled.ejs
index 1116dcf8443dca8c34a42b062275470737ea6eaf..3add09ffd46fe16d711f0e430aeca27071f8a133 100644
--- a/resources/views/cookies_disabled.ejs
+++ b/resources/views/cookies_disabled.ejs
@@ -10,7 +10,7 @@
           </h2>
 
           <p>
-            <%- 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'"]) %>
+            <%- format(gettext("Please close this window, <a %s>enable cookies</a> and try again"), [" target='_blank' href='http://support.mozilla.org/kb/Websites%20say%20cookies%20are%20blocked'"]) %>
           </p>
         </div>
       </div>
diff --git a/resources/views/dialog_layout.ejs b/resources/views/dialog_layout.ejs
index 5eb03bdb09f21243b7c674d4099eee8486ffd2e7..6e567ccba473db1379e4d8c96d75c99a5292d420 100644
--- a/resources/views/dialog_layout.ejs
+++ b/resources/views/dialog_layout.ejs
@@ -6,11 +6,11 @@
 <html LANG="<%= lang %>" dir="<%= lang_dir %>">
 <head>
   <meta charset="utf-8">
-  <meta name="viewport" content="initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,width=device-width" />
+  <meta name="viewport" content="initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0" />
   <meta name="format-detection" content="email=no" />
 
   <!--[if lt IE 9]>
-    <script src="/common/js/lib/html5shim.js"></script>
+    <%- cachify_js('/production/html5shim.js') %>
   <![endif]-->
   <%- cachify_css('/production/dialog.css') %>
   <!--[if lt IE 9]>
@@ -31,8 +31,7 @@
       </div>
 
       <footer>
-<%- format(gettext('<strong>Persona.</strong> Simplified sign-in, built by a non-profit.  <a %s>Learn more&rarr;</a>'), [" href='/about' target='_blank'"]) %>
-
+<%- format(gettext('<strong>Persona.</strong> Simplified sign-in, built by a non-profit. <a %s>Learn more&rarr;</a>'), [" href='/about' target='_blank'"]) %>
       </footer>
 
 
diff --git a/resources/views/forgot.ejs b/resources/views/forgot.ejs
index 87b0d8f7b4f853feeb6ffd4cdbd1f64ef3d7ad71..705d14ead5e6cb0f4c5c666d1db39106c74d08f8 100644
--- a/resources/views/forgot.ejs
+++ b/resources/views/forgot.ejs
@@ -9,17 +9,14 @@
             <h1>Forgot Password</h1>
             <div class="notifications">
                 <div class="notification emailsent">
-                  <p>
-                    We sent a confirmation email to <strong id="sentToEmail"></strong>.
-                  </p>
+                  <h2>Confirm your email address</h2>
 
                   <p>
-                    To finish resetting your password just click the verify link we sent to your email address.
+                    Check your email at <strong id="sentToEmail"></strong>.
                   </p>
 
-                  <br />
                   <p>
-                    If this is a mistake, just ignore the sent email and <a href="/signup" class="action cancelVerify" id="back">use another email address</a>.
+                    Click the link in the confirmation email. Your password will then be reset.
                   </p>
                 </div>
             </div>
@@ -65,7 +62,7 @@
 
                 <li>
                     <label for="vpassword">Verify Password</label>
-                    <input id="vpassword" placeholder="Repeat Password" type="password" maxlength="80">
+                    <input id="vpassword" placeholder="Verify Password" type="password" maxlength="80">
 
                     <div id="password_required" class="tooltip" for="vpassword">
                       Verification password is required.
diff --git a/resources/views/signup.ejs b/resources/views/signup.ejs
index 81ec178b4280f3f1394062c80d9574f04e00dad5..8440816f1aa7113fd49c490e01818ccdd7c69d54 100644
--- a/resources/views/signup.ejs
+++ b/resources/views/signup.ejs
@@ -15,17 +15,14 @@
                 </li>
 
                 <li class="notification emailsent">
-                  <p>
-                    We sent a confirmation email to <strong id="sentToEmail"></strong>.
-                  </p>
+                  <h2>Confirm your email address</h2>
 
                   <p>
-                    To finish signing up just click the verify link we sent to your email address.
+                    Check your email at <strong id="sentToEmail"></strong>.
                   </p>
 
-                  <br />
                   <p>
-                    If this is a mistake, just ignore the sent email and <a href="/signup" class="action cancelVerify" id="back">use another email address</a>.
+                    Click the link in the confirmation email. You'll then immediately be signed into Persona.
                   </p>
                 </li>
 
@@ -68,7 +65,7 @@
 
                 <li class="password_entry">
                     <label for="vpassword">Verify Password</label>
-                    <input id="vpassword" placeholder="Repeat Password" type="password" maxlength="80">
+                    <input id="vpassword" placeholder="Verify Password" type="password" maxlength="80">
 
                     <div id="password_required" class="tooltip" for="vpassword">
                       Verification password is required.
@@ -106,7 +103,7 @@
                     sign in with your provider.  A new window will be opened.
                   </p>
 
-                  <p>
+                  <p class="submit">
                     <button id="authWithPrimary" tabindex="1">Verify</button>
                   </p>
                 </li>
diff --git a/resources/views/test.ejs b/resources/views/test.ejs
index 546fb8a9ceb76c3c53f1ca64432426f89bcf9be3..dccc4233b69becc8ce6d80bc8928dfd284f19853 100644
--- a/resources/views/test.ejs
+++ b/resources/views/test.ejs
@@ -100,7 +100,6 @@
     <script src="/common/js/enable_cookies_url.js"></script>
     <script src="/common/js/wait-messages.js"></script>
     <script src="/common/js/error-messages.js"></script>
-    <script src="/common/js/error-display.js"></script>
     <script src="/common/js/storage.js"></script>
     <script src="/common/js/xhr.js"></script>
     <script src="/common/js/network.js"></script>
@@ -118,6 +117,7 @@
     <script src="/common/js/modules/xhr_disable_form.js"></script>
     <script src="/common/js/modules/cookie_check.js"></script>
     <script src="/common/js/modules/interaction_data.js"></script>
+    <script src="/common/js/modules/extended-info.js"></script>
 
     <script src="/dialog/js/misc/internal_api.js"></script>
     <script src="/dialog/js/misc/helpers.js"></script>
@@ -154,7 +154,6 @@
     <script src="cases/common/js/renderer.js"></script>
     <script src="cases/common/js/screens.js"></script>
     <script src="cases/common/js/tooltip.js"></script>
-    <script src="cases/common/js/error-display.js"></script>
     <script src="cases/common/js/browser-support.js"></script>
     <script src="cases/common/js/enable_cookies_url.js"></script>
     <script src="cases/common/js/validation.js"></script>
@@ -173,6 +172,7 @@
     <script src="cases/common/js/modules/xhr_disable_form.js"></script>
     <script src="cases/common/js/modules/cookie_check.js"></script>
     <script src="cases/common/js/modules/interaction_data.js"></script>
+    <script src="cases/common/js/modules/extended-info.js"></script>
 
     <script src="cases/pages/js/browserid.js"></script>
     <script src="cases/pages/js/page_helpers.js"></script>
diff --git a/scripts/assign_issues.js b/scripts/assign_issues.js
index 6ac941032f7a34f075449db10cd0c2fc92d2c0ad..01a4da318986fbc248d37e1c9f434cd3118de19e 100755
--- a/scripts/assign_issues.js
+++ b/scripts/assign_issues.js
@@ -8,6 +8,8 @@ const https = require('https');
 
 // people to get issues, and the issues that were assigned to them
 var people = {
+  'jedp': [],
+  'seanmonstar': [],
   'ozten': [],
   'lloyd': [],
   'shane-tomlinson': [],
diff --git a/scripts/browserid.spec b/scripts/browserid.spec
index 6bb044ea89e314de69c2ad4b6b31dcbdb7bfdcd2..879bf3490f42dcbaeb683374c9838c8786e03adc 100644
--- a/scripts/browserid.spec
+++ b/scripts/browserid.spec
@@ -1,7 +1,7 @@
 %define _rootdir /opt/browserid
 
 Name:          browserid-server
-Version:       0.2012.07.06
+Version:       0.2012.07.20
 Release:       1%{?dist}_%{svnrev}
 Summary:       BrowserID server
 Packager:      Pete Fritchman <petef@mozilla.com>
diff --git a/scripts/phantomrunner.js b/scripts/phantomrunner.js
index c0edf122ebc9ad92bb8172eb1a5553cac5343e2d..d06732bc8011d53add437f31000aabb99f60f856 100644
--- a/scripts/phantomrunner.js
+++ b/scripts/phantomrunner.js
@@ -78,8 +78,8 @@ page.open(phantom.args[0], function(status){
 
                         var failingItems = node.querySelectorAll(".fail");
                         var failingItemsCount = failingItems.length;
-                        for(var i = 0; i < failingItemsCount; i++) {
-                          var failingItem = failingItems.item(i);
+                        for(var j = 0; j < failingItemsCount; j++) {
+                          var failingItem = failingItems.item(j);
                           console.log("   - " + failingItem.innerText);
                         }
                     }
diff --git a/scripts/run_locally.js b/scripts/run_locally.js
index 346fa7dda7dff3152af234e9f9fef5261e0d01fd..f034d8279d9c2e57c3d09b2154a715232d65fbf7 100755
--- a/scripts/run_locally.js
+++ b/scripts/run_locally.js
@@ -30,6 +30,7 @@ var daemonsToRun = {
   },
   proxy: { },
   browserid: { },
+  static: { },
   router: { }
 };
 
@@ -55,6 +56,7 @@ process.env['BROWSERID_URL'] = 'http://' + HOST + ":10007";
 process.env['VERIFIER_URL'] = 'http://' + HOST + ":10000/verify";
 process.env['KEYSIGNER_URL'] = 'http://' + HOST + ":10003";
 process.env['ROUTER_URL'] = 'http://' + HOST + ":10002";
+process.env['STATIC_URL'] = 'http://' + HOST + ":10010";
 
 process.env['PUBLIC_URL'] = process.env['ROUTER_URL'];
 
diff --git a/scripts/serve_example.js b/scripts/serve_example.js
index 11e806a6a51c1f7c5b280b31b91575cfc583f390..29d59e536fe6c48a4992e26f01474e5d9bce2695 100755
--- a/scripts/serve_example.js
+++ b/scripts/serve_example.js
@@ -16,8 +16,8 @@ var exampleServer = express.createServer();
 
 exampleServer.use(express.logger({ format: 'dev' }));
 
-if (process.env['BROWSERID_URL']) {
-  var burl = urlparse(process.env['BROWSERID_URL']).validate().normalize().originOnly().toString();
+if (process.env['PUBLIC_URL']) {
+  var burl = urlparse(process.env['PUBLIC_URL']).validate().normalize().originOnly().toString();
   console.log('using browserid server at ' + burl);
 
   exampleServer.use(postprocess(function(req, buffer) {
diff --git a/scripts/serve_example_primary.js b/scripts/serve_example_primary.js
index f0ef97001ddbffd738177f3aa3823c8937a3fbc2..974c4e88b3158c64ed0ea1b63a2ca9d057baafd4 100755
--- a/scripts/serve_example_primary.js
+++ b/scripts/serve_example_primary.js
@@ -41,8 +41,8 @@ exampleServer.use(function(req, res, next) {
 
 exampleServer.use(express.logger({ format: 'dev' }));
 
-if (process.env['BROWSERID_URL']) {
-  var burl = urlparse(process.env['BROWSERID_URL']).validate().normalize().originOnly().toString();
+if (process.env['PUBLIC_URL']) {
+  var burl = urlparse(process.env['PUBLIC_URL']).validate().normalize().originOnly().toString();
   console.log('using browserid server at ' + burl);
 
   exampleServer.use(postprocess(function(req, buffer) {
diff --git a/scripts/test b/scripts/test
index 9b6069a6eb2f0ad7187e961f14dd5144352444ae..5ed56ae46ea30267cf024104e78ec7c1ef0c51f9 100755
--- a/scripts/test
+++ b/scripts/test
@@ -22,6 +22,9 @@ if (!process.env['WHAT_TESTS']) {
 if (whatTests[0] == 'all') whatTests = [ 'back_mysql', 'back', 'front' ];
 
 var ec = 0;
+var frontend_test_filter = process.env['FRONTEND_TEST_FILTER'] ?
+                           ' (filter: ' + process.env['FRONTEND_TEST_FILTER'] + ')' :
+                           '';
 function run() {
   if (!whatTests.length) process.exit(ec);
 
@@ -29,7 +32,7 @@ function run() {
 
   const availConf = {
     front: {
-      what: "Front end unit tests under PhantomJS",
+      what: "Front end unit tests under PhantomJS" + frontend_test_filter,
       node_env: 'test_json',
       script: 'test_frontend'
     },
diff --git a/scripts/test_frontend b/scripts/test_frontend
index 879af173a0d8caf7d0cf3a4c37df6f9fc311b893..19f2c66a133f80b8185c73105bc8e80bb56175c5 100755
--- a/scripts/test_frontend
+++ b/scripts/test_frontend
@@ -35,8 +35,10 @@ start_stop.addStartupBatches(suite);
 suite.addBatch({
   "frontend unit tests": {
     topic: function() {
+      var filter = process.env['FRONTEND_TEST_FILTER'] ?
+                   '?filter=' + process.env['FRONTEND_TEST_FILTER'] : '';
       var kid = spawn('phantomjs', [ path.join(__dirname, 'phantomrunner.js'),
-                                     'http://127.0.0.1:10002/test' ]);
+                                     'http://127.0.0.1:10002/test/'+filter ]);
       kid.stdout.on('data', function(d) { process.stdout.write(d); });
       kid.stderr.on('data', function(d) { process.stderr.write(d); });
       kid.on('exit', this.callback);
diff --git a/tests/auth-with-assertion-test.js b/tests/auth-with-assertion-test.js
index 94a0e9ff1592254241627f123add37974b0077e0..d82156e74b885c7b8749423c5ef051c56b6a4b43 100755
--- a/tests/auth-with-assertion-test.js
+++ b/tests/auth-with-assertion-test.js
@@ -15,7 +15,8 @@ db = require('../lib/db.js'),
 config = require('../lib/configuration.js'),
 http = require('http'),
 querystring = require('querystring'),
-primary = require('./lib/primary.js');
+primary = require('./lib/primary.js'),
+jwcrypto = require('jwcrypto');
 
 var suite = vows.describe('auth-with-assertion');
 
@@ -26,7 +27,9 @@ start_stop.addStartupBatches(suite);
 
 const TEST_DOMAIN = 'example.domain',
       TEST_EMAIL = 'testuser@' + TEST_DOMAIN,
-      TEST_ORIGIN = 'http://127.0.0.1:10002';
+      TEST_ORIGIN = 'http://127.0.0.1:10002',
+      OTHER_EMAIL = 'otheruser@' + TEST_DOMAIN;
+
 
 // here we go!  let's authenticate with an assertion from
 // a primary.
@@ -72,6 +75,59 @@ suite.addBatch({
   }
 });
 
+// now let's generate an assertion using this user
+suite.addBatch({
+  "generating a new intermediate keypair and then an assertion": {
+    topic: function() {
+      var expirationDate = new Date(new Date().getTime() + (2 * 60 * 1000));
+      var self = this;
+
+      jwcrypto.generateKeypair(
+        {algorithm: "DS", keysize: 256},
+        function(err, innerKeypair) {
+          
+          // sign this innerkeypair with the key from g_cert (g_keypair)
+          jwcrypto.cert.sign(
+            innerKeypair.publicKey, {email: OTHER_EMAIL},
+            {issuedAt: new Date(), expiresAt: expirationDate},
+            {}, primaryUser._keyPair.secretKey,
+            function(err, innerCert) {
+
+              jwcrypto.assertion.sign(
+                {},
+                {audience: TEST_ORIGIN, expiresAt: expirationDate},
+                innerKeypair.secretKey, function(err, signedObject) {
+                  if (err) return cb(err);
+
+                  var fullAssertion = jwcrypto.cert.bundle(
+                    [primaryUser._cert, innerCert], signedObject);
+
+                  self.callback(null, fullAssertion);
+                });
+              
+            });
+        });
+    },
+    "succeeds": function(err, assertion) {
+      assert.isString(assertion);
+    },
+    "and logging in with the assertion fails": {
+      topic: function(err, assertion)  {
+        wsapi.post('/wsapi/auth_with_assertion', {
+          assertion: assertion,
+          ephemeral: true
+        }).call(this);
+      },
+      "fails": function(err, r) {
+        var resp = JSON.parse(r.body);
+        assert.isObject(resp);
+        assert.isFalse(resp.success);
+        assert.equal(resp.reason, "certificate chaining is not yet allowed");
+      }
+    }
+  }
+});
+
 start_stop.addShutdownBatches(suite);
 
 // run or export the suite.
diff --git a/tests/heartbeat-test.js b/tests/heartbeat-test.js
index 815f97120003b062da83fe9d898acc0fc16ff471..6dd5341b4b586f303cc43069fdfbb5046ff8a311 100755
--- a/tests/heartbeat-test.js
+++ b/tests/heartbeat-test.js
@@ -114,6 +114,66 @@ suite.addBatch({
   }
 });
 
+// now let's SIGSTOP the static process and verify that the router's
+// deep heartbeat fails within 11s
+suite.addBatch({
+  "stopping the static process": {
+    topic: function() {
+      process.kill(parseInt(process.env['STATIC_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['STATIC_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);
 
diff --git a/tests/internal-wsapi-test.js b/tests/internal-wsapi-test.js
new file mode 100644
index 0000000000000000000000000000000000000000..59d04ae2dad6f84e5c89a72d187c12f529e7eac5
--- /dev/null
+++ b/tests/internal-wsapi-test.js
@@ -0,0 +1,35 @@
+#!/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');
+
+var suite = vows.describe('internal-wsapi');
+
+// disable vows (often flakey?) async error behavior
+suite.options.error = false;
+
+start_stop.addStartupBatches(suite);
+
+suite.addBatch({
+  "requesting to create an account with an assertion": {
+    topic: wsapi.post('/wsapi/create_account_with_assertion', { }),
+    "returns a 404": function(err, r) {
+      assert.strictEqual(r.code, 404);
+    }
+  }
+});
+
+start_stop.addShutdownBatches(suite);
+
+// run or export the suite.
+if (process.argv[1] === __filename) suite.run();
+else suite.export(module);
diff --git a/tests/stalled-mysql-test.js b/tests/stalled-mysql-test.js
index 988bb2f9fffb35c61ab5bed427ef78599eaee826..5bf27ce9716093340a626e60f44b48c0bb902c5d 100755
--- a/tests/stalled-mysql-test.js
+++ b/tests/stalled-mysql-test.js
@@ -388,8 +388,8 @@ suite.addBatch({
         assertion: g_assertion
       }).call(this);
     },
-    "fails with 503": function(err, r) {
-      assert.strictEqual(r.code, 503);
+    "fails with 404": function(err, r) {
+      assert.strictEqual(r.code, 404);
     }
   }
 });
diff --git a/tests/static-resource-test.js b/tests/static-resource-test.js
index 66a83a9ba88fb4ef3428474f635e8acfc5ac9142..76307a47acec4dcc7023601ac53bdff5055bbc8c 100755
--- a/tests/static-resource-test.js
+++ b/tests/static-resource-test.js
@@ -23,7 +23,15 @@ suite.addBatch({
       var res = resources.resources;
       assert.ok(files['/production/dialog.css'].length >= 3);
       // Get ride of non-localized asset bundles
-      ['/production/communication_iframe.js', '/production/include.js', '/production/dialog.css', '/production/browserid.css', '/production/ie8_main.css', '/production/ie8_dialog.css', '/production/relay.js', '/production/html5shim.js'].forEach(
+      ['/production/communication_iframe.js',
+       '/production/include.js',
+       '/production/dialog.css',
+       '/production/browserid.css',
+       '/production/ie8_main.css',
+       '/production/ie8_dialog.css',
+       '/production/relay.js',
+       '/production/html5shim.js',
+       '/production/authenticate_with_primary.js'].forEach(
         function (nonLocaleAsset) {
           delete res[nonLocaleAsset];
           delete files[nonLocaleAsset];
diff --git a/tests/verifier-test.js b/tests/verifier-test.js
index c3f708acc268dd61e9c7b05de0d7f702a76fc6f2..f6225c04884d2901f92168bc454c64379ea62178 100755
--- a/tests/verifier-test.js
+++ b/tests/verifier-test.js
@@ -966,6 +966,59 @@ suite.addBatch({
   }
 });
 
+const OTHER_EMAIL = 'otheremail@example.com';
+
+// check that chained certs do not work
+suite.addBatch({
+  "generating an assertion with chained certs": {
+    topic: function() {
+      // primaryCert generated
+      // newClientKeypair generated
+      var expirationDate = new Date(new Date().getTime() + (2 * 60 * 1000));
+      var self = this;
+
+      jwcrypto.generateKeypair(
+        {algorithm: "DS", keysize: 256},
+        function(err, innerKeypair) {
+
+          // sign this innerkeypair with the key from g_cert (g_keypair)
+          jwcrypto.cert.sign(
+            innerKeypair.publicKey, {email: OTHER_EMAIL},
+            {issuedAt: new Date(), expiresAt: expirationDate},
+            {}, g_keypair.secretKey,
+            function(err, innerCert) {
+              jwcrypto.assertion.sign({}, {audience: TEST_ORIGIN, expiresAt: expirationDate},
+                                      innerKeypair.secretKey, function(err, assertion) {
+                                        if (err) return self.callback(err);
+                                        
+                                        var b = jwcrypto.cert.bundle([g_cert, innerCert],
+                                                                     assertion);
+                                        self.callback(null, b);
+                                      });
+            });
+          
+        });
+    },
+    "yields a good looking assertion": function (err, assertion) {
+      assert.isString(assertion);
+      assert.equal(assertion.length > 0, true);
+    },
+    "will cause the verifier": {
+      topic: function(err, assertion) {
+        wsapi.post('/verify', {
+          audience: TEST_ORIGIN,
+          assertion: assertion
+        }).call(this);
+      },
+      "to fail": function (err, r) {
+        var resp = JSON.parse(r.body);
+        assert.strictEqual(resp.status, 'failure');
+        assert.strictEqual(resp.reason, "certificate chaining is not yet allowed");
+      }
+    }
+  }
+});
+
 start_stop.addShutdownBatches(suite);
 
 // run or export the suite.