diff --git a/resources/static/communication_iframe/iframe.js b/resources/static/communication_iframe/iframe.js
index 57e7080cd69f5425faec910c4901ba54dc2e3a25..da4e50dfba37a28ed08b6d09d99b658ee0e148c1 100644
--- a/resources/static/communication_iframe/iframe.js
+++ b/resources/static/communication_iframe/iframe.js
@@ -41,19 +41,19 @@ window.console = window.console || {
 
 steal
   .plugins('jquery')
-  .then('../dialog/resources/jschannel',
-        '../dialog/resources/base64',
-        '../dialog/resources/underscore-min',
+  .then('../lib/jschannel',
+        '../lib/base64',
+        '../lib/underscore-min',
         '../dialog/resources/channel',
-        '../dialog/resources/browserid',
-        '../dialog/resources/storage',
-        '../dialog/resources/tooltip',
-        '../dialog/resources/validation',
-        '../dialog/resources/browserid-extensions',
-        '../dialog/resources/network',
-        '../dialog/resources/user',
-        '../dialog/resources/error-messages',
-        '../dialog/resources/wait-messages',
+        '../shared/browserid',
+        '../shared/storage',
+        '../shared/tooltip',
+        '../shared/validation',
+        '../shared/browserid-extensions',
+        '../shared/network',
+        '../shared/user',
+        '../shared/error-messages',
+        '../shared/wait-messages',
         afterResourceLoad);
 
 function afterResourceLoad() {
diff --git a/resources/static/css/style.css b/resources/static/css/style.css
index fc2e755689ed39f6d5776fcdf5a37eebac25768f..c5101f1c54b4fe9f18f0b988ffe1cca1fda3e836 100644
--- a/resources/static/css/style.css
+++ b/resources/static/css/style.css
@@ -116,6 +116,10 @@ hr {
   background-color: rgba(0,0,0,.6);
 }
 
+#error {
+  display: none;
+}
+
 #error > div {
   background-color: #fff;
   position: absolute;
@@ -128,7 +132,7 @@ hr {
 }
 
 #error ul, #error li {
-    list-style-type: none; 
+    list-style-type: none;
 }
 
 #wait strong, #error strong {
diff --git a/resources/static/dialog/controllers/authenticate_controller.js b/resources/static/dialog/controllers/authenticate_controller.js
index f064f0ca5782fa0310a8d9997d4d9beb0ecef915..9fd77e4b69580d6688b42dd8641e3d3bb272bea7 100644
--- a/resources/static/dialog/controllers/authenticate_controller.js
+++ b/resources/static/dialog/controllers/authenticate_controller.js
@@ -43,10 +43,11 @@
       errors = bid.Errors,
       validation = bid.Validation,
       tooltip = bid.Tooltip,
+      dom = bid.DOM,
       lastEmail = "";
 
   function getEmail() {
-    return $("#email").val().trim();
+    return dom.getInner("#email").trim();
   }
 
   function checkEmail(el, event) {
@@ -89,18 +90,18 @@
 
   function authenticate(el, event) {
     var email = getEmail(),
-        pass = $("#password").val(),
+        pass = dom.getInner("#password"),
         self = this;
 
     cancelEvent(event);
 
     if (!validation.emailAndPassword(email, pass)) return;
 
-    user.authenticate(email, pass, 
+    user.authenticate(email, pass,
       function onComplete(authenticated) {
         if (authenticated) {
           self.close("authenticated", {
-            email: email 
+            email: email
           });
         } else {
           bid.Tooltip.showTooltip("#cannot_authenticate");
@@ -149,7 +150,7 @@
     self.publish("enter_password");
     self.submit = authenticate;
     animateSwap(".start:visible,.newuser:visible,.forgot:visible", ".returning", function() {
-      $("#password").focus();  
+      dom.getElements("#password")[0].focus();
     });
   }
 
@@ -157,7 +158,7 @@
     cancelEvent(event);
 
     this.submit = resetPassword;
-    $("#email").attr("disabled", "disabled");
+    dom.setAttr("#email", "disabled", "disabled");
 
     animateSwap(".start:visible,.newuser:visible,.returning:visible", ".forgot");
   }
@@ -165,8 +166,8 @@
   function cancelForgotPassword(el, event) {
     cancelEvent(event);
 
-    $("#email").removeAttr("disabled");
-    enterPasswordState.call(this); 
+    dom.removeAttr("#email", "disabled");
+    enterPasswordState.call(this);
   }
 
   function createUserState(el, event) {
@@ -185,7 +186,7 @@
       options = options || {};
 
       this._super(el, {
-        bodyTemplate: "authenticate.ejs",
+        bodyTemplate: "authenticate",
         bodyVars: {
           sitename: user.getHostname(),
           email: options.email || ""
@@ -193,7 +194,7 @@
       });
 
       this.submit = checkEmail;
-      // If we already have an email address, check if it is valid, if so, show 
+      // If we already have an email address, check if it is valid, if so, show
       // password.
       if (options.email) this.submit();
     },
diff --git a/resources/static/dialog/controllers/checkregistration_controller.js b/resources/static/dialog/controllers/checkregistration_controller.js
index 06558c8be8272ece176f0e4ce70293b7d9fb1289..b19fa0be985cd6dbf330eed4b456fae39e5fb7a1 100644
--- a/resources/static/dialog/controllers/checkregistration_controller.js
+++ b/resources/static/dialog/controllers/checkregistration_controller.js
@@ -1,5 +1,5 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */                                             
-/*global BrowserID: true, PageController: true */ 
+/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*global BrowserID: true, PageController: true */
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  *
@@ -43,7 +43,7 @@
     init: function(el, options) {
       var me=this;
       me._super(el, {
-        waitTemplate: "confirmemail.ejs",
+        waitTemplate: "confirmemail",
         waitVars: {
           email: options.email
         }
diff --git a/resources/static/dialog/controllers/dialog_controller.js b/resources/static/dialog/controllers/dialog_controller.js
index c2d3562fe66d8c0d458b0f8d46051e1538979028..7e0af554a29e7489762926c6030f748596e598cf 100644
--- a/resources/static/dialog/controllers/dialog_controller.js
+++ b/resources/static/dialog/controllers/dialog_controller.js
@@ -1,5 +1,5 @@
-/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */                                             
-/*global setupChannel:true, BrowserID: true, PageController: true, OpenAjax: true */ 
+/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*global setupChannel:true, BrowserID: true, PageController: true, OpenAjax: true */
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  *
@@ -45,11 +45,12 @@
   var bid = BrowserID,
       user = bid.User,
       errors = bid.Errors,
+      dom = bid.DOM,
       offline = false,
       win = window,
       subscriptions = [],
       hub = OpenAjax.hub;
-      
+
   function subscribe(message, cb) {
      subscriptions.push(hub.subscribe(message, cb));
   }
@@ -74,7 +75,7 @@
           win.setupChannel(self);
           self.stateMachine();
         } catch (e) {
-          self.renderError("error.ejs", {
+          self.renderError("error", {
             action: errors.relaySetup
           });
         }
@@ -84,12 +85,12 @@
         var subscription;
 
         while(subscription = subscriptions.pop()) {
-          hub.unsubscribe(subscription);  
+          hub.unsubscribe(subscription);
         }
 
         this._super();
       },
-        
+
       getVerifiedEmail: function(origin_url, onsuccess, onerror) {
         var self=this;
         self.onsuccess = onsuccess;
@@ -101,11 +102,11 @@
         }
 
         user.setOrigin(origin_url);
-        $("#sitename").text(user.getHostname());
+        dom.setInner("#sitename", user.getHostname());
 
         self.doCheckAuth();
 
-        $(win).bind("unload", function() {
+        dom.bindEvent(win, "unload", function() {
           bid.Storage.setStagedOnBehalfOf("");
           self.doCancel();
         });
@@ -113,7 +114,7 @@
 
 
       stateMachine: function() {
-        var self=this, 
+        var self=this,
             el = this.element;
 
         subscribe("offline", function(msg, info) {
@@ -135,7 +136,7 @@
 
         subscribe("authenticated", function(msg, info) {
           //self.doEmailSelected(info.email);
-          // XXX benadida, lloyd - swap these two if you want to experiment with 
+          // XXX benadida, lloyd - swap these two if you want to experiment with
           // generating assertions directly from signin.
           self.syncEmails();
         });
@@ -184,13 +185,13 @@
       },
 
       doOffline: function() {
-        this.renderError("offline.ejs", {});
+        this.renderError("offline", {});
         offline = true;
       },
 
       doXHRError: function(info) {
         if (!offline) {
-          this.renderError("error.ejs", $.extend({
+          this.renderError("error", $.extend({
             action: errors.xhrError
           }, info));
         }
@@ -217,8 +218,8 @@
 
       doPickEmail: function() {
         this.element.pickemail({
-          // XXX ideal is to get rid of this and have a User function 
-          // that takes care of getting email addresses AND the last used email 
+          // XXX ideal is to get rid of this and have a User function
+          // that takes care of getting email addresses AND the last used email
           // for this site.
           origin: user.getHostname()
         });
@@ -230,7 +231,7 @@
 
       doForgotPassword: function(email) {
         this.element.forgotpassword({
-          email: email  
+          email: email
         });
       },
 
@@ -255,8 +256,8 @@
 
       doAssertionGenerated: function(assertion) {
         var self=this;
-        // Clear onerror before the call to onsuccess - the code to onsuccess 
-        // calls window.close, which would trigger the onerror callback if we 
+        // Clear onerror before the call to onsuccess - the code to onsuccess
+        // calls window.close, which would trigger the onerror callback if we
         // tried this afterwards.
         self.onerror = null;
         self.onsuccess(assertion);
@@ -269,20 +270,20 @@
 
       syncEmails: function() {
         var self = this;
-        user.syncEmails(self.doPickEmail.bind(self), 
+        user.syncEmails(self.doPickEmail.bind(self),
           self.getErrorDialog(errors.signIn));
       },
 
       doCheckAuth: function() {
         var self=this;
-        user.checkAuthenticationAndSync(function onSuccess() {}, 
+        user.checkAuthenticationAndSync(function onSuccess() {},
           function onComplete(authenticated) {
             if (authenticated) {
               self.doPickEmail();
             } else {
               self.doAuthenticate();
             }
-          }, 
+          },
           self.getErrorDialog(errors.checkAuthentication));
     }
 
diff --git a/resources/static/dialog/controllers/page_controller.js b/resources/static/dialog/controllers/page_controller.js
index 70113e8ad0d3165f32734415893fd530125303a6..ed4a4fdaaf4e5f4866863574c417aece5f62012e 100644
--- a/resources/static/dialog/controllers/page_controller.js
+++ b/resources/static/dialog/controllers/page_controller.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*/
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
@@ -37,7 +37,10 @@
 (function() {
 "use strict";
 
-  var ANIMATION_TIME = 250;
+  var ANIMATION_TIME = 250,
+      bid = BrowserID,
+      dom = bid.DOM,
+      screens = bid.Screens;
 
 
   $.Controller.extend("PageController", {
@@ -67,53 +70,40 @@
       }
 
       // XXX move all of these, bleck.
-      $("form").bind("submit", me.onSubmit.bind(me));
-      $("#cancel").click(me.onCancel.bind(me));
-      $("#back").click(me.onBack.bind(me));
-      $("#thisIsNotMe").click(me.close.bind(me, "notme"));
+      dom.bindEvent("form", "submit", me.onSubmit.bind(me));
+      dom.bindEvent("#thisIsNotMe", "click", me.close.bind(me, "notme"));
     },
 
     destroy: function() {
-      $("form").unbind("submit");
-      $("input").unbind("keyup");
-      $("#cancel").unbind("click");
-      $("#back").unbind("click");
-      $("#thisIsNotMe").unbind("click");
+      dom.unbindEvent("form", "submit");
+      dom.unbindEvent("input", "keyup");
+      dom.unbindEvent("#thisIsNotMe", "click");
 
-      $("body").removeClass("waiting");
+      dom.removeClass("body", "waiting");
 
       this._super();
     },
 
-    renderTemplates: function(target, body, body_vars) {
-      if (body) {
-        var bodyHtml = $.View("//dialog/views/" + body, body_vars);
-        target = $(target + " .contents");
-        target.html(bodyHtml).find("input").eq(0).focus(); 
-      }
-    },
-
     renderDialog: function(body, body_vars) {
-      this.renderTemplates("#formWrap", body, body_vars);
-      $("body").removeClass("error").removeClass("waiting").addClass("form");
+      screens.form(body, body_vars);
       $("#wait, #error").stop().fadeOut(ANIMATION_TIME);
+      dom.focus("input:visible:eq(0)");
     },
 
     renderWait: function(body, body_vars) {
-      this.renderTemplates("#wait", body, body_vars);
-      $("body").removeClass("error").removeClass("form").addClass("waiting").css('opacity', 1);
+      screens.wait(body, body_vars);
+      $("body").css('opacity', 1);
       $("#wait").stop().hide().fadeIn(ANIMATION_TIME);
     },
 
     renderError: function(body, body_vars) {
-      this.renderTemplates("#error", body, body_vars);
-      $("body").removeClass("waiting").removeClass("form").addClass("error");
+      screens.error(body, body_vars);
       $("#error").stop().css('opacity', 1).hide().fadeIn(ANIMATION_TIME);
 
       /**
-       * What a big steaming pile, use CSS animations for this!
+       * TODO What a big steaming pile, use CSS animations for this!
        */
-      $("#openMoreInfo").click(function(event) {
+      dom.bindEvent("#openMoreInfo", "click", function(event) {
         event.preventDefault();
 
         $("#moreInfo").slideDown();
@@ -140,9 +130,9 @@
     },
 
     doWait: function(info) {
-      this.renderWait("wait.ejs", info);
+      this.renderWait("wait", info);
 
-      $("body").addClass("waiting");
+      dom.addClass("body", "waiting");
     },
 
     close: function(message, data) {
@@ -155,28 +145,16 @@
     /**
      * Get a curried function to an error dialog.
      * @method getErrorDialog
-     * @method {object} action - info to use for the error dialog.  Should have 
+     * @method {object} action - info to use for the error dialog.  Should have
      * two fields, message, description.
      */
     getErrorDialog: function(action) {
       var self=this;
       return function(lowLevelInfo) {
-        self.renderError("error.ejs", $.extend({
+        self.renderError("error", $.extend({
           action: action
         }, lowLevelInfo));
       }
-    },
-
-    onCancel: function(event) {
-      event.preventDefault();
-      event.stopPropagation();
-      this.close("cancel");
-    },
-
-    onBack: function(event) {
-      event.preventDefault();
-      event.stopPropagation();
-      this.close("start");
     }
   });
 
diff --git a/resources/static/dialog/controllers/pickemail_controller.js b/resources/static/dialog/controllers/pickemail_controller.js
index eda8675067f2cc817c7529d2451532bfaf7d1b78..01cea06fa081d06c60431a99c3fbfcc52031075d 100644
--- a/resources/static/dialog/controllers/pickemail_controller.js
+++ b/resources/static/dialog/controllers/pickemail_controller.js
@@ -1,5 +1,5 @@
-/*jshint brgwser:true, jQuery: true, forin: true, laxbreak:true */                                             
-/*global _: true, BrowserID: true, PageController: true */ 
+/*jshint brgwser:true, jQuery: true, forin: true, laxbreak:true */
+/*global _: true, BrowserID: true, PageController: true */
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  *
@@ -42,6 +42,7 @@
       user = bid.User,
       errors = bid.Errors,
       storage = bid.Storage,
+      dom = bid.DOM,
       body = $("body"),
       animationComplete = body.innerWidth() < 640,
       assertion;
@@ -104,7 +105,7 @@
   }
 
   function getAssertion(email) {
-    // Kick of the assertion fetching/keypair generating while we are showing 
+    // Kick of the assertion fetching/keypair generating while we are showing
     // the animation, hopefully this minimizes the delay the user notices.
     var self=this;
     user.getAssertion(email, function(assert) {
@@ -122,7 +123,7 @@
            animationComplete = true;
            tryClose.call(self);
          });
-      }); 
+      });
     }
     else {
       tryClose.call(self);
@@ -133,7 +134,7 @@
   function signIn(element, event) {
     cancelEvent(event);
     var self=this,
-        email = $("input[type=radio]:checked").val();
+        email = dom.getInner("input[type=radio]:checked");
 
     var valid = checkEmail.call(self, email);
     if (valid) {
@@ -145,7 +146,7 @@
   }
 
   function addEmail(element, event) {
-    var email = $("#newEmail").val(),
+    var email = dom.getInner("#newEmail"),
         self=this;
 
     cancelEvent(event);
@@ -180,21 +181,21 @@
     init: function(el, options) {
       var origin = user.getOrigin();
       this._super(el, {
-        bodyTemplate: "pickemail.ejs",
+        bodyTemplate: "pickemail",
         bodyVars: {
           identities: user.getStoredEmailKeypairs(),
-          // XXX ideal is to get rid of this and have a User function 
-          // that takes care of getting email addresses AND the last used email 
+          // XXX ideal is to get rid of this and have a User function
+          // that takes care of getting email addresses AND the last used email
           // for this site.
           siteemail: storage.site.get(origin, "email"),
           remember: storage.site.get(origin, "remember") || false
         }
       });
 
-      $("body").css("opacity", "1");
+      body.css("opacity", "1");
 
-      if($("#selectEmail input[type=radio]:visible").length === 0) {
-        // If there is only one email address, the radio button is never shown, 
+      if(dom.getElements("#selectEmail input[type=radio]:visible").length === 0) {
+        // If there is only one email address, the radio button is never shown,
         // instead focus the sign in button so that the user can click enter.
         // issue #412
         $("#signInButton").focus();
diff --git a/resources/static/dialog/dialog.js b/resources/static/dialog/dialog.js
index 3651dacb8524211c0e7dbfb553e797d58b7070fa..319fa4cca385ae40e0e92c371e2207215072fdaa 100644
--- a/resources/static/dialog/dialog.js
+++ b/resources/static/dialog/dialog.js
@@ -39,26 +39,34 @@ window.console = window.console || {
   log: function() {}
 };
 
-steal.plugins(
-	'jquery/controller',			// a widget factory
-	'jquery/controller/subscribe',	// subscribe to OpenAjax.hub
-	'jquery/view/ejs',				// client side templates
-	'jquery/controller/view')		// lookup views with the controller's name
+steal
+  .plugins(
+              'jquery/controller',			// a widget factory
+              'jquery/controller/subscribe')	// subscribe to OpenAjax.hub
 
-	.resources('jschannel',
-               'base64',
-               'underscore-min',
-               'channel',
-               'browserid',
-               'storage',
-               'tooltip',
-               'validation',
-               'browser-support',
-               'browserid-extensions',
-               'network',
-               'user',
-               'error-messages',
-               'wait-messages')					// 3rd party script's (like jQueryUI), in resources folder
+	.resources(
+               'channel')
+  .then(
+               '../lib/jschannel',
+               '../lib/base64',
+               '../lib/underscore-min',
+               '../lib/ejs',
+               '../shared/browserid',
+               '../lib/dom-jquery',
+
+               '../shared/storage',
+               '../shared/templates',
+               '../shared/renderer',
+               '../shared/error-display',
+               '../shared/screens',
+               '../shared/tooltip',
+               '../shared/validation',
+               '../shared/browser-support',
+               '../shared/browserid-extensions',
+               '../shared/network',
+               '../shared/user',
+               '../shared/error-messages',
+               '../shared/wait-messages')
 
 	.controllers('page',
                'dialog',
@@ -66,16 +74,8 @@ steal.plugins(
                'checkregistration',
                'pickemail')					// loads files in controllers folder
 
-	.views('authenticate.ejs',
-           'confirmemail.ejs',
-           'pickemail.ejs',
-           'wait.ejs',
-           'error.ejs',
-           'offline.ejs'
-          ).
-
-          then(function() {
-            $(function() {
-              $('body').dialog().show();
-            });
-          });						// adds views to be added to build
+  .then(function() {
+    $(function() {
+      $('body').dialog().show();
+    });
+  });						// adds views to be added to build
diff --git a/resources/static/dialog/resources/error-display.js b/resources/static/dialog/resources/error-display.js
deleted file mode 100644
index c70452133340fc07413b71d79ab6fa6bd8004060..0000000000000000000000000000000000000000
--- a/resources/static/dialog/resources/error-display.js
+++ /dev/null
@@ -1,30 +0,0 @@
-BrowserID.ErrorDisplay = (function() {
-  "use strict";
-
-  function render(target, template, data) {
-      template = $(template).html();
-      _.templateSettings = {
-          interpolate : /\{\{(.+?)\}\}/g,
-          evaluate : /\{\%(.+?)\%\}/g
-      };
-      var display = $(_.template(template, data)).appendTo(target);
-
-      /**
-       * What a big steaming pile, use CSS animations for this!
-       */
-      display.find("#openMoreInfo").click(function(event) {
-        event.preventDefault();
-
-        display.find("#moreInfo").slideDown();
-        display.find("#openMoreInfo").css({visibility: "hidden"});
-      });
-    
-      return display;
-  }
-
-  return {
-    render: render
-  };
-
-}());
-
diff --git a/resources/static/dialog/views/error.ejs b/resources/static/dialog/views/error.ejs
index 537d2471f5a80b3305428035e80b950569a20ba9..6c5e9bcbd7c0557004a4a96e8ea138807e907c47 100644
--- a/resources/static/dialog/views/error.ejs
+++ b/resources/static/dialog/views/error.ejs
@@ -2,7 +2,11 @@
   <h2>We are very sorry, there has been an error!</h2>
 
   <p>
+  <% if (typeof dialog !== "undefined" && dialog !== false) { %>
+    To retry, you will have to reload the page and try again.  More info can be found by opening the expanded info below.
+  <% } else { %>
     To retry, you will have to close BrowserID and try again.  More info can be found by opening the expanded info below.
+  <% } %>
   </p>
 
   <a href="#" id="openMoreInfo">See more info</a>
@@ -15,7 +19,7 @@
         <% if (action.message) { %>
           <p>
             <%= action.message %>
-          </p>  
+          </p>
         <% } %>
       </li>
     <% } %>
@@ -26,13 +30,13 @@
         <strong id="network">Network Info:</strong> <%= network.type %>: <%= network.url %>
 
         <p>
-          <strong>Response Code - </strong> <%= network.textStatus %> 
+          <strong>Response Code - </strong> <%= network.textStatus %>
         </p>
 
         <% if (network.errorThrown) { %>
           <p>
-            <strong>Error Type:</strong> <%= network.errorThrown %> 
-          </p>  
+            <strong>Error Type:</strong> <%= network.errorThrown %>
+          </p>
         <% } %>
 
       </li>
diff --git a/resources/static/dialog/views/testBodyTemplate.ejs b/resources/static/dialog/views/testBodyTemplate.ejs
index 77a06e46bbf5acedeb0fbb140b6f289c40389a8b..cb93998d20f58f089634384355be0c21ab09a8c5 100644
--- a/resources/static/dialog/views/testBodyTemplate.ejs
+++ b/resources/static/dialog/views/testBodyTemplate.ejs
@@ -1,2 +1,2 @@
-<input type="text" value="" />
+<input id="templateInput" type="text" value="" />
 
diff --git a/resources/static/dialog/views/tooltip.ejs b/resources/static/dialog/views/tooltip.ejs
new file mode 100644
index 0000000000000000000000000000000000000000..191af143ed1935fc67ff917b5ef03a264e0d936a
--- /dev/null
+++ b/resources/static/dialog/views/tooltip.ejs
@@ -0,0 +1,4 @@
+<div class="tooltip">
+  <%= contents %>
+</div>
+
diff --git a/resources/static/dialog/resources/base64.js b/resources/static/lib/base64.js
similarity index 100%
rename from resources/static/dialog/resources/base64.js
rename to resources/static/lib/base64.js
diff --git a/resources/static/lib/dom-jquery.js b/resources/static/lib/dom-jquery.js
new file mode 100644
index 0000000000000000000000000000000000000000..889d165bf42ce1080d6326e6559431ba148a0d18
--- /dev/null
+++ b/resources/static/lib/dom-jquery.js
@@ -0,0 +1,310 @@
+/**
+* Written by Shane Tomlinson - original source at:
+* https://github.com/stomlinson/AFrame-JS/blob/master/src/adapters/jquery.js
+* which is licensed under the Creative Commons Attribution 3.0 License.
+*
+* A DOM Manipulation adapter for jQuery.
+* @class BrowserID.DOM
+* @static
+*/
+BrowserID.DOM = ( function() {
+    var jQuery = typeof( window ) !== 'undefined' && window.jQuery;
+    var DOM = {
+        /**
+        * Get a set of elements that match the selector
+        * @method getElements
+        * @param {selector || element} selector - if a string, a selector to search for.
+        * @return {array} array of elements
+        */
+        getElements: function( selector ) {
+            return jQuery( selector );
+        },
+
+        /**
+        * Get a set of descendent elements that match the selector
+        * @method getDescendentElements
+        * @param {string} selector - The selector to search for.
+        * @param {element} root - root node to search from
+        * @return {array} array of elements
+        */
+        getDescendentElements: function( selector, root ) {
+            return jQuery( root ).find( selector );
+        },
+
+        /**
+        * Get a set of descendent elements that match the selector, include the root node if it
+        *   matches the selector
+        * @method getElementsIncludeRoot
+        * @param {string} selector - The selector to search for.
+        * @param {element} root - root node to search from
+        * @return {array} array of elements
+        */
+        getElementsIncludeRoot: function( selector, root ) {
+            root = jQuery( root );
+            var set = root.find( selector );
+            if( root.is( selector ) ) {
+                set = root.add( set );
+            }
+            return set;
+        },
+
+        /**
+        * Get the children for an element
+        * @method getChildren
+        * @param {selector || element} selector - element to get children for
+        * @return {array} an array of children
+        */
+        getChildren: function( selector ) {
+            return jQuery( selector ).children();
+        },
+
+        /**
+        * Get the nth child element
+        * @method getNthChild
+        * @param {selector || element} selector - element to get children for
+        * @param {number} index - index of the child to get
+        * @return {element} the nth child if it exists.
+        */
+        getNthChild: function( selector, index ) {
+            return jQuery( selector ).children()[ index ];
+        },
+
+        /**
+        * Iterate over a set of elements
+        * @method forEach
+        * @param {Elements} elements - elements to iterate over
+        * @param {function} callback - callback to call.  Callback called with: callback( element, index );
+        * @param {context} context - context to callback in
+        */
+        forEach: function( elements, callback, context ) {
+            jQuery( elements ).each( function( index, element ) {
+                callback.call( context, element, index );
+            } );
+        },
+
+        /**
+        * Remove an element
+        * @method removeElement
+        * @param {selector || element} selector - element to remove
+        */
+        removeElement: function( selector ) {
+            jQuery( selector ).remove();
+        },
+
+        /**
+        * Bind to an elements DOM Event
+        * @method bindEvent
+        * @param {selector || element} element to bind on
+        * @param {string} eventName - name of event
+        * @param {function} callback - callback to call
+        */
+        bindEvent: function( element, eventName, callback ) {
+            return jQuery( element ).bind( eventName, callback );
+        },
+
+        /**
+        * Unbind an already bound DOM Event from an element.
+        * @method unbindEvent
+        * @param {selector || element} element to unbind from
+        * @param {string} eventName - name of event
+        * @param {function} callback - callback
+        */
+        unbindEvent: function( element, eventName, callback ) {
+            return jQuery( element ).unbind( eventName, callback );
+        },
+
+        /**
+        * Fire a DOM event on an element
+        * @method fireEvent
+        * @param {selector || element} element
+        * @param {string} type - event to fire
+        */
+        fireEvent: function( element, type ) {
+            return jQuery( element ).trigger( type );
+        },
+
+        /**
+        * Set the inner value of an element, including input elements
+        * @method setInner
+        * @param {selector || element} element - element to set
+        * @param {string} value - value to set
+        */
+        setInner: function( element, value ) {
+            var target = jQuery( element );
+            if( isValBased( target ) ) {
+                target.val( value );
+            }
+            else {
+                target.html( value );
+            }
+        },
+
+        /**
+        * Get the inner value of an element, including input elements
+        * @method getInner
+        * @param {selector || element} element
+        * @return {string} inner value of the element
+        */
+        getInner: function( element ) {
+            var target = jQuery( element );
+            var retval = '';
+
+            if( isValBased( target ) ) {
+                retval = target.val();
+            }
+            else {
+                retval = target.html();
+            }
+            return retval;
+        },
+
+        /**
+        * Set an element's attribute.
+        * @method setAttr
+        * @param {selector || element} element
+        * @param {string} attrName - the attribute name
+        * @param {string} value - value to set
+        */
+        setAttr: function( element, attrName, value ) {
+            jQuery( element ).attr( attrName, value );
+        },
+
+        /**
+        * Get an element's attribute.
+        * @method getAttr
+        * @param {selector || element} element
+        * @param {string} attrName - the attribute name
+        * @return {string} attribute's value
+        */
+        getAttr: function( element, attrName ) {
+            return jQuery( element ).attr( attrName );
+        },
+
+        /**
+        * Check if an element has an attribute
+        * @method hasAttr
+        * @param {selector || element} element
+        * @param {string} attrName - the attribute name
+        * @return {boolean} true if the element has the attribute, false otw.
+        */
+        hasAttr: function( element, attrName ) {
+            var val = jQuery( element )[ 0 ].getAttribute( attrName );
+            return val !== null;
+        },
+
+        /**
+        * Remove an attribute from an element.
+        * @method removeAttr
+        * @param {selector || element} element
+        * @param {string} attrName - the attribute to remove
+        */
+        removeAttr: function( element, attrName ) {
+            return jQuery( element ).removeAttr( attrName );
+        },
+
+        /**
+        * Add a class to an element
+        * @method addClass
+        * @param {selector || element} element
+        * @param {string} className
+        */
+        addClass: function( element, className ) {
+            jQuery( element ).addClass( className );
+        },
+
+        /**
+        * Remove a class from an element
+        * @method removeClass
+        * @param {selector || element} element
+        * @param {string} className
+        */
+        removeClass: function( element, className ) {
+            jQuery( element ).removeClass( className );
+        },
+
+        /**
+        * Check if an element has a class
+        * @method hasClass
+        * @param {selector || element} element
+        * @param {string} className
+        * @return {boolean} true if element has class, false otw.
+        */
+        hasClass: function( element, className ) {
+            return jQuery( element ).hasClass( className );
+        },
+
+        /**
+        * Create an element
+        * @method createElement
+        * @param {string} type - element type
+        * @param {string} html (optional) - inner HTML
+        * @return {element} created element
+        */
+        createElement: function( type, html ) {
+            var element = jQuery( '<' + type + '/>' );
+            if( html ) {
+                BrowserID.DOM.setInner( element, html );
+            }
+            return element;
+        },
+
+        /**
+        * Append an element as the last child of another element
+        * @method appendTo
+        * @param {selector || element} elementToInsert
+        * @param {selector || element} elementToAppendTo
+        */
+        appendTo: function( elementToInsert, elementToAppendTo ) {
+            var el = jQuery(elementToInsert );
+            el.appendTo( jQuery( elementToAppendTo ) );
+            return el;
+        },
+
+        /**
+        * Insert an element before another element
+        * @method insertBefore
+        * @param {selector || element} elementToInsert
+        * @param {selector || element} elementToInsertBefore
+        */
+        insertBefore: function( elementToInsert, elementToInsertBefore ) {
+            jQuery( elementToInsert ).insertBefore( elementToInsertBefore );
+        },
+
+        /**
+        * Insert as the nth child of an element
+        * @method insertAsNthChild
+        * @param {selector || element} elementToInsert
+        * @param {selector || element} parent
+        * @param {number} index
+        */
+        insertAsNthChild: function( elementToInsert, parent, index ) {
+            var children = jQuery( parent ).children();
+            if( index === children.length ) {
+                elementToInsert.appendTo( parent );
+            }
+            else {
+                var insertBefore = children.eq( index );
+                elementToInsert.insertBefore( insertBefore );
+            }
+
+        },
+
+        /**
+         * Focus an element
+         * @method focus
+         * @param {selelector || element} elementToFocus
+         */
+        focus: function( elementToFocus ) {
+          jQuery( elementToFocus ).focus();
+        }
+
+
+    };
+
+    function isValBased( target ) {
+        return target.is( 'input' ) || target.is( 'textarea' );
+    }
+
+    return DOM;
+
+}() );
diff --git a/resources/static/lib/ejs.js b/resources/static/lib/ejs.js
new file mode 100644
index 0000000000000000000000000000000000000000..49d95252400213f7c1f2105344541485ebcb50aa
--- /dev/null
+++ b/resources/static/lib/ejs.js
@@ -0,0 +1,505 @@
+(function(){
+    
+
+var rsplit = function(string, regex) {
+	var result = regex.exec(string),retArr = new Array(), first_idx, last_idx, first_bit;
+	while (result != null)
+	{
+		first_idx = result.index; last_idx = regex.lastIndex;
+		if ((first_idx) != 0)
+		{
+			first_bit = string.substring(0,first_idx);
+			retArr.push(string.substring(0,first_idx));
+			string = string.slice(first_idx);
+		}		
+		retArr.push(result[0]);
+		string = string.slice(result[0].length);
+		result = regex.exec(string);	
+	}
+	if (! string == '')
+	{
+		retArr.push(string);
+	}
+	return retArr;
+},
+chop =  function(string){
+    return string.substr(0, string.length - 1);
+},
+extend = function(d, s){
+    for(var n in s){
+        if(s.hasOwnProperty(n))  d[n] = s[n]
+    }
+}
+
+
+EJS = function( options ){
+	options = typeof options == "string" ? {view: options} : options
+    this.set_options(options);
+	if(options.precompiled){
+		this.template = {};
+		this.template.process = options.precompiled;
+		EJS.update(this.name, this);
+		return;
+	}
+    if(options.element)
+	{
+		if(typeof options.element == 'string'){
+			var name = options.element
+			options.element = document.getElementById(  options.element )
+			if(options.element == null) throw name+'does not exist!'
+		}
+		if(options.element.value){
+			this.text = options.element.value
+		}else{
+			this.text = options.element.innerHTML
+		}
+		this.name = options.element.id
+		this.type = '['
+	}else if(options.url){
+        options.url = EJS.endExt(options.url, this.extMatch);
+		this.name = this.name ? this.name : options.url;
+        var url = options.url
+        //options.view = options.absolute_url || options.view || options.;
+		var template = EJS.get(this.name /*url*/, this.cache);
+		if (template) return template;
+	    if (template == EJS.INVALID_PATH) return null;
+        try{
+            this.text = EJS.request( url+(this.cache ? '' : '?'+Math.random() ));
+        }catch(e){}
+
+		if(this.text == null){
+            throw( {type: 'EJS', message: 'There is no template at '+url}  );
+		}
+		//this.name = url;
+	}
+	var template = new EJS.Compiler(this.text, this.type);
+
+	template.compile(options, this.name);
+
+	
+	EJS.update(this.name, this);
+	this.template = template;
+};
+/* @Prototype*/
+EJS.prototype = {
+	/**
+	 * Renders an object with extra view helpers attached to the view.
+	 * @param {Object} object data to be rendered
+	 * @param {Object} extra_helpers an object with additonal view helpers
+	 * @return {String} returns the result of the string
+	 */
+    render : function(object, extra_helpers){
+        object = object || {};
+        this._extra_helpers = extra_helpers;
+		var v = new EJS.Helpers(object, extra_helpers || {});
+		return this.template.process.call(object, object,v);
+	},
+    update : function(element, options){
+        if(typeof element == 'string'){
+			element = document.getElementById(element)
+		}
+		if(options == null){
+			_template = this;
+			return function(object){
+				EJS.prototype.update.call(_template, element, object)
+			}
+		}
+		if(typeof options == 'string'){
+			params = {}
+			params.url = options
+			_template = this;
+			params.onComplete = function(request){
+				var object = eval( request.responseText )
+				EJS.prototype.update.call(_template, element, object)
+			}
+			EJS.ajax_request(params)
+		}else
+		{
+			element.innerHTML = this.render(options)
+		}
+    },
+	out : function(){
+		return this.template.out;
+	},
+    /**
+     * Sets options on this view to be rendered with.
+     * @param {Object} options
+     */
+	set_options : function(options){
+        this.type = options.type || EJS.type;
+		this.cache = options.cache != null ? options.cache : EJS.cache;
+		this.text = options.text || null;
+		this.name =  options.name || null;
+		this.ext = options.ext || EJS.ext;
+		this.extMatch = new RegExp(this.ext.replace(/\./, '\.'));
+	}
+};
+EJS.endExt = function(path, match){
+	if(!path) return null;
+	match.lastIndex = 0
+	return path+ (match.test(path) ? '' : this.ext )
+}
+
+
+
+
+/* @Static*/
+EJS.Scanner = function(source, left, right) {
+	
+    extend(this,
+        {left_delimiter: 	left +'%',
+         right_delimiter: 	'%'+right,
+         double_left: 		left+'%%',
+         double_right:  	'%%'+right,
+         left_equal: 		left+'%=',
+         left_comment: 	left+'%#'})
+
+	this.SplitRegexp = left=='[' ? /(\[%%)|(%%\])|(\[%=)|(\[%#)|(\[%)|(%\]\n)|(%\])|(\n)/ : new RegExp('('+this.double_left+')|(%%'+this.double_right+')|('+this.left_equal+')|('+this.left_comment+')|('+this.left_delimiter+')|('+this.right_delimiter+'\n)|('+this.right_delimiter+')|(\n)') ;
+	
+	this.source = source;
+	this.stag = null;
+	this.lines = 0;
+};
+
+EJS.Scanner.to_text = function(input){
+	if(input == null || input === undefined)
+        return '';
+    if(input instanceof Date)
+		return input.toDateString();
+	if(input.toString) 
+        return input.toString();
+	return '';
+};
+
+EJS.Scanner.prototype = {
+  scan: function(block) {
+     scanline = this.scanline;
+	 regex = this.SplitRegexp;
+	 if (! this.source == '')
+	 {
+	 	 var source_split = rsplit(this.source, /\n/);
+	 	 for(var i=0; i<source_split.length; i++) {
+		 	 var item = source_split[i];
+			 this.scanline(item, regex, block);
+		 }
+	 }
+  },
+  scanline: function(line, regex, block) {
+	 this.lines++;
+	 var line_split = rsplit(line, regex);
+ 	 for(var i=0; i<line_split.length; i++) {
+	   var token = line_split[i];
+       if (token != null) {
+		   	try{
+	         	block(token, this);
+		 	}catch(e){
+				throw {type: 'EJS.Scanner', line: this.lines};
+			}
+       }
+	 }
+  }
+};
+
+
+EJS.Buffer = function(pre_cmd, post_cmd) {
+	this.line = new Array();
+	this.script = "";
+	this.pre_cmd = pre_cmd;
+	this.post_cmd = post_cmd;
+	for (var i=0; i<this.pre_cmd.length; i++)
+	{
+		this.push(pre_cmd[i]);
+	}
+};
+EJS.Buffer.prototype = {
+	
+  push: function(cmd) {
+	this.line.push(cmd);
+  },
+
+  cr: function() {
+	this.script = this.script + this.line.join('; ');
+	this.line = new Array();
+	this.script = this.script + "\n";
+  },
+
+  close: function() {
+	if (this.line.length > 0)
+	{
+		for (var i=0; i<this.post_cmd.length; i++){
+			this.push(pre_cmd[i]);
+		}
+		this.script = this.script + this.line.join('; ');
+		line = null;
+	}
+  }
+ 	
+};
+
+
+EJS.Compiler = function(source, left) {
+    this.pre_cmd = ['var ___ViewO = [];'];
+	this.post_cmd = new Array();
+	this.source = ' ';	
+	if (source != null)
+	{
+		if (typeof source == 'string')
+		{
+		    source = source.replace(/\r\n/g, "\n");
+            source = source.replace(/\r/g,   "\n");
+			this.source = source;
+		}else if (source.innerHTML){
+			this.source = source.innerHTML;
+		} 
+		if (typeof this.source != 'string'){
+			this.source = "";
+		}
+	}
+	left = left || '<';
+	var right = '>';
+	switch(left) {
+		case '[':
+			right = ']';
+			break;
+		case '<':
+			break;
+		default:
+			throw left+' is not a supported deliminator';
+			break;
+	}
+	this.scanner = new EJS.Scanner(this.source, left, right);
+	this.out = '';
+};
+EJS.Compiler.prototype = {
+  compile: function(options, name) {
+  	options = options || {};
+	this.out = '';
+	var put_cmd = "___ViewO.push(";
+	var insert_cmd = put_cmd;
+	var buff = new EJS.Buffer(this.pre_cmd, this.post_cmd);		
+	var content = '';
+	var clean = function(content)
+	{
+	    content = content.replace(/\\/g, '\\\\');
+        content = content.replace(/\n/g, '\\n');
+        content = content.replace(/"/g,  '\\"');
+        return content;
+	};
+	this.scanner.scan(function(token, scanner) {
+		if (scanner.stag == null)
+		{
+			switch(token) {
+				case '\n':
+					content = content + "\n";
+					buff.push(put_cmd + '"' + clean(content) + '");');
+					buff.cr();
+					content = '';
+					break;
+				case scanner.left_delimiter:
+				case scanner.left_equal:
+				case scanner.left_comment:
+					scanner.stag = token;
+					if (content.length > 0)
+					{
+						buff.push(put_cmd + '"' + clean(content) + '")');
+					}
+					content = '';
+					break;
+				case scanner.double_left:
+					content = content + scanner.left_delimiter;
+					break;
+				default:
+					content = content + token;
+					break;
+			}
+		}
+		else {
+			switch(token) {
+				case scanner.right_delimiter:
+					switch(scanner.stag) {
+						case scanner.left_delimiter:
+							if (content[content.length - 1] == '\n')
+							{
+								content = chop(content);
+								buff.push(content);
+								buff.cr();
+							}
+							else {
+								buff.push(content);
+							}
+							break;
+						case scanner.left_equal:
+							buff.push(insert_cmd + "(EJS.Scanner.to_text(" + content + ")))");
+							break;
+					}
+					scanner.stag = null;
+					content = '';
+					break;
+				case scanner.double_right:
+					content = content + scanner.right_delimiter;
+					break;
+				default:
+					content = content + token;
+					break;
+			}
+		}
+	});
+	if (content.length > 0)
+	{
+		// Chould be content.dump in Ruby
+		buff.push(put_cmd + '"' + clean(content) + '")');
+	}
+	buff.close();
+	this.out = buff.script + ";";
+	var to_be_evaled = '/*'+name+'*/this.process = function(_CONTEXT,_VIEW) { try { with(_VIEW) { with (_CONTEXT) {'+this.out+" return ___ViewO.join('');}}}catch(e){e.lineNumber=null;throw e;}};";
+	
+	try{
+		eval(to_be_evaled);
+	}catch(e){
+		if(typeof JSLINT != 'undefined'){
+			JSLINT(this.out);
+			for(var i = 0; i < JSLINT.errors.length; i++){
+				var error = JSLINT.errors[i];
+				if(error.reason != "Unnecessary semicolon."){
+					error.line++;
+					var e = new Error();
+					e.lineNumber = error.line;
+					e.message = error.reason;
+					if(options.view)
+						e.fileName = options.view;
+					throw e;
+				}
+			}
+		}else{
+			throw e;
+		}
+	}
+  }
+};
+
+
+//type, cache, folder
+/**
+ * Sets default options for all views
+ * @param {Object} options Set view with the following options
+ * <table class="options">
+				<tbody><tr><th>Option</th><th>Default</th><th>Description</th></tr>
+				<tr>
+					<td>type</td>
+					<td>'<'</td>
+					<td>type of magic tags.  Options are '&lt;' or '['
+					</td>
+				</tr>
+				<tr>
+					<td>cache</td>
+					<td>true in production mode, false in other modes</td>
+					<td>true to cache template.
+					</td>
+				</tr>
+	</tbody></table>
+ * 
+ */
+EJS.config = function(options){
+	EJS.cache = options.cache != null ? options.cache : EJS.cache;
+	EJS.type = options.type != null ? options.type : EJS.type;
+	EJS.ext = options.ext != null ? options.ext : EJS.ext;
+	
+	var templates_directory = EJS.templates_directory || {}; //nice and private container
+	EJS.templates_directory = templates_directory;
+	EJS.get = function(path, cache){
+		if(cache == false) return null;
+		if(templates_directory[path]) return templates_directory[path];
+  		return null;
+	};
+	
+	EJS.update = function(path, template) { 
+		if(path == null) return;
+		templates_directory[path] = template ;
+	};
+	
+	EJS.INVALID_PATH =  -1;
+};
+EJS.config( {cache: true, type: '<', ext: '.ejs' } );
+
+
+
+/**
+ * @constructor
+ * By adding functions to EJS.Helpers.prototype, those functions will be available in the 
+ * views.
+ * @init Creates a view helper.  This function is called internally.  You should never call it.
+ * @param {Object} data The data passed to the view.  Helpers have access to it through this._data
+ */
+EJS.Helpers = function(data, extras){
+	this._data = data;
+    this._extras = extras;
+    extend(this, extras );
+};
+/* @prototype*/
+EJS.Helpers.prototype = {
+    /**
+     * Renders a new view.  If data is passed in, uses that to render the view.
+     * @param {Object} options standard options passed to a new view.
+     * @param {optional:Object} data
+     * @return {String}
+     */
+	view: function(options, data, helpers){
+        if(!helpers) helpers = this._extras
+		if(!data) data = this._data;
+		return new EJS(options).render(data, helpers);
+	},
+    /**
+     * For a given value, tries to create a human representation.
+     * @param {Object} input the value being converted.
+     * @param {Object} null_text what text should be present if input == null or undefined, defaults to ''
+     * @return {String} 
+     */
+	to_text: function(input, null_text) {
+	    if(input == null || input === undefined) return null_text || '';
+	    if(input instanceof Date) return input.toDateString();
+		if(input.toString) return input.toString().replace(/\n/g, '<br />').replace(/''/g, "'");
+		return '';
+	}
+};
+    EJS.newRequest = function(){
+	   var factories = [function() { return new ActiveXObject("Msxml2.XMLHTTP"); },function() { return new XMLHttpRequest(); },function() { return new ActiveXObject("Microsoft.XMLHTTP"); }];
+	   for(var i = 0; i < factories.length; i++) {
+	        try {
+	            var request = factories[i]();
+	            if (request != null)  return request;
+	        }
+	        catch(e) { continue;}
+	   }
+	}
+	
+	EJS.request = function(path){
+	   var request = new EJS.newRequest()
+	   request.open("GET", path, false);
+	   
+	   try{request.send(null);}
+	   catch(e){return null;}
+	   
+	   if ( request.status == 404 || request.status == 2 ||(request.status == 0 && request.responseText == '') ) return null;
+	   
+	   return request.responseText
+	}
+	EJS.ajax_request = function(params){
+		params.method = ( params.method ? params.method : 'GET')
+		
+		var request = new EJS.newRequest();
+		request.onreadystatechange = function(){
+			if(request.readyState == 4){
+				if(request.status == 200){
+					params.onComplete(request)
+				}else
+				{
+					params.onComplete(request)
+				}
+			}
+		}
+		request.open(params.method, params.url)
+		request.send(null)
+	}
+
+
+})();
\ No newline at end of file
diff --git a/resources/static/js/highlight.js b/resources/static/lib/highlight.js
similarity index 100%
rename from resources/static/js/highlight.js
rename to resources/static/lib/highlight.js
diff --git a/resources/static/js/html5shim.js b/resources/static/lib/html5shim.js
similarity index 100%
rename from resources/static/js/html5shim.js
rename to resources/static/lib/html5shim.js
diff --git a/resources/static/js/jquery-1.6.2.min.js b/resources/static/lib/jquery-1.6.2.min.js
similarity index 100%
rename from resources/static/js/jquery-1.6.2.min.js
rename to resources/static/lib/jquery-1.6.2.min.js
diff --git a/resources/static/dialog/resources/jschannel.js b/resources/static/lib/jschannel.js
similarity index 100%
rename from resources/static/dialog/resources/jschannel.js
rename to resources/static/lib/jschannel.js
diff --git a/resources/static/js/json2.js b/resources/static/lib/json2.js
similarity index 100%
rename from resources/static/js/json2.js
rename to resources/static/lib/json2.js
diff --git a/resources/static/lib/module.js b/resources/static/lib/module.js
new file mode 100644
index 0000000000000000000000000000000000000000..e9b06f406e1d26da0af6918a7dadacc72324659b
--- /dev/null
+++ b/resources/static/lib/module.js
@@ -0,0 +1,96 @@
+/**
+* Author Shane Tomlinson
+* Original source can be found at:
+* https://github.com/stomlinson/appcore/blob/master/js/module.js
+* Licences under Mozilla Tri-License
+*/
+BrowserID.module = (function() {
+  "use strict";
+
+  var registration = {},
+      created = {},
+      running = {};
+
+  function register(service, module, config) {
+    if (!module) {
+      throw "module constructor missing for " + service;
+    }
+
+    registration[service] = {
+      constructor: module,
+      config: config
+    };
+  }
+
+  function getRegistration(service) {
+    return registration[service];
+  }
+
+  function getModule(service) {
+    return registration[service].constructor;
+  }
+
+  function reset() {
+    registration = {};
+    running = {};
+    created = {};
+  }
+
+  function start(service, data) {
+    if (running[service]) {
+      throw "module already running for " + service;
+    }
+
+    var module = created[service];
+
+    if (!module) {
+      var registration = getRegistration(service);
+      if (registration) {
+        var constr = registration.constructor,
+            config = registration.config;
+
+        module = new constr();
+        created[service] = module;
+        module.init(config);
+      }
+      else {
+        throw "module not registered for " + service;
+      }
+    }
+
+    module.start(data);
+    running[service] = module;
+
+    return module;
+  }
+
+  function stop(service) {
+    var module = running[service];
+
+    if (module) {
+      module.stop();
+      delete running[service];
+    }
+    else {
+      throw "module not started for " + service;
+    }
+  }
+
+  function stopAll() {
+    for(var key in running) {
+      var module = running[key];
+      module.stop();
+      delete running[key];
+    }
+  }
+
+
+  return {
+    register: register,
+    getModule: getModule,
+    reset: reset,
+    start: start,
+    stop: stop,
+    stopAll: stopAll
+  };
+}());
diff --git a/resources/static/dialog/resources/underscore-min.js b/resources/static/lib/underscore-min.js
similarity index 100%
rename from resources/static/dialog/resources/underscore-min.js
rename to resources/static/lib/underscore-min.js
diff --git a/resources/static/js/pages/add_email_address.js b/resources/static/pages/add_email_address.js
similarity index 100%
rename from resources/static/js/pages/add_email_address.js
rename to resources/static/pages/add_email_address.js
diff --git a/resources/static/js/browserid.js b/resources/static/pages/browserid.js
similarity index 100%
rename from resources/static/js/browserid.js
rename to resources/static/pages/browserid.js
diff --git a/resources/static/js/pages/forgot.js b/resources/static/pages/forgot.js
similarity index 100%
rename from resources/static/js/pages/forgot.js
rename to resources/static/pages/forgot.js
diff --git a/resources/static/js/pages/index.js b/resources/static/pages/index.js
similarity index 100%
rename from resources/static/js/pages/index.js
rename to resources/static/pages/index.js
diff --git a/resources/static/js/pages/manage_account.js b/resources/static/pages/manage_account.js
similarity index 100%
rename from resources/static/js/pages/manage_account.js
rename to resources/static/pages/manage_account.js
diff --git a/resources/static/js/page_helpers.js b/resources/static/pages/page_helpers.js
similarity index 96%
rename from resources/static/js/page_helpers.js
rename to resources/static/pages/page_helpers.js
index f53a0844ab5ebaaae2582c3604c3488a709430e5..11e8546b3066616115652d344a83e81c075bd605 100644
--- a/resources/static/js/page_helpers.js
+++ b/resources/static/pages/page_helpers.js
@@ -52,7 +52,7 @@ BrowserID.PageHelpers = (function() {
   }
 
   function prefillEmail() {
-    // If the user tried to sign in on the sign up page with an existing email, 
+    // If the user tried to sign in on the sign up page with an existing email,
     // place that email in the email field, then focus the password.
     var el = $("#email"),
         email = locStorage.signInEmail;
@@ -64,7 +64,7 @@ BrowserID.PageHelpers = (function() {
 
     el.keyup(onEmailKeyUp);
   }
-  
+
   function clearStoredEmail() {
     locStorage.removeItem("signInEmail");
   }
@@ -86,8 +86,8 @@ BrowserID.PageHelpers = (function() {
 
   function getFailure(error) {
     return function onFailure(info) {
-      info = $.extend(info, { action: error });
-      errorDisplay.render("#error", "#templateError", info);
+      info = $.extend(info, { action: error, dialog: false });
+      bid.Screens.error("error", info);
       $("#errorBackground").fadeIn();
       $("#error").fadeIn();
     }
diff --git a/resources/static/js/pages/signin.js b/resources/static/pages/signin.js
similarity index 100%
rename from resources/static/js/pages/signin.js
rename to resources/static/pages/signin.js
diff --git a/resources/static/js/pages/signup.js b/resources/static/pages/signup.js
similarity index 100%
rename from resources/static/js/pages/signup.js
rename to resources/static/pages/signup.js
diff --git a/resources/static/js/pages/verify_email_address.js b/resources/static/pages/verify_email_address.js
similarity index 100%
rename from resources/static/js/pages/verify_email_address.js
rename to resources/static/pages/verify_email_address.js
diff --git a/resources/static/dialog/resources/browser-support.js b/resources/static/shared/browser-support.js
similarity index 100%
rename from resources/static/dialog/resources/browser-support.js
rename to resources/static/shared/browser-support.js
diff --git a/resources/static/dialog/resources/browserid-extensions.js b/resources/static/shared/browserid-extensions.js
similarity index 100%
rename from resources/static/dialog/resources/browserid-extensions.js
rename to resources/static/shared/browserid-extensions.js
diff --git a/resources/static/dialog/resources/browserid.js b/resources/static/shared/browserid.js
similarity index 100%
rename from resources/static/dialog/resources/browserid.js
rename to resources/static/shared/browserid.js
diff --git a/resources/static/shared/error-display.js b/resources/static/shared/error-display.js
new file mode 100644
index 0000000000000000000000000000000000000000..c26c830ecc222cac38b5e77f191973bc474e4d9a
--- /dev/null
+++ b/resources/static/shared/error-display.js
@@ -0,0 +1,33 @@
+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();
+    $("#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/dialog/resources/error-messages.js b/resources/static/shared/error-messages.js
similarity index 100%
rename from resources/static/dialog/resources/error-messages.js
rename to resources/static/shared/error-messages.js
diff --git a/resources/static/dialog/resources/network.js b/resources/static/shared/network.js
similarity index 100%
rename from resources/static/dialog/resources/network.js
rename to resources/static/shared/network.js
diff --git a/resources/static/shared/renderer.js b/resources/static/shared/renderer.js
new file mode 100644
index 0000000000000000000000000000000000000000..60785aeed49368feb0ba525f9eab87979c9820db
--- /dev/null
+++ b/resources/static/shared/renderer.js
@@ -0,0 +1,78 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global BrowserID: true, _: true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+BrowserID.Renderer = (function() {
+  "use strict";
+
+  var bid = BrowserID,
+      dom = bid.DOM;
+
+  function getTemplateHTML(body, vars) {
+    var config,
+        templateText = bid.Templates[body];
+
+    if(templateText) {
+      config = {
+        text: templateText
+      };
+    }
+    else {
+      // TODO - be able to set the directory
+      config = {
+        url: "/dialog/views/" + body + ".ejs"
+      };
+    }
+
+    var html = new EJS(config).render(vars);
+    return html;
+  }
+
+  function render(target, body, vars) {
+    var html = getTemplateHTML(body, vars);
+    return dom.setInner(target, html);
+  }
+
+  function append(target, body, vars) {
+    var html = getTemplateHTML(body, vars);
+    return dom.appendTo(html, target);
+  }
+
+  return {
+    render: render,
+    append: append
+  }
+}());
+
diff --git a/resources/static/shared/screens.js b/resources/static/shared/screens.js
new file mode 100644
index 0000000000000000000000000000000000000000..97338199fdcf84ec5a516ee01aec9e8f41519bb7
--- /dev/null
+++ b/resources/static/shared/screens.js
@@ -0,0 +1,40 @@
+BrowserID.Screens = (function() {
+  "use strict";
+
+  var bid = BrowserID,
+      dom = BrowserID.DOM,
+      renderer = bid.Renderer;
+
+  function render(target, body, vars) {
+    renderer.render(target + " .contents", body, vars);
+  }
+
+  function form(template, vars) {
+    render("#formWrap", template, vars);
+    dom.removeClass("body", "error");
+    dom.removeClass("body", "waiting");
+    dom.addClass("body", "form");
+  }
+
+  function wait(template, vars) {
+    render("#wait", template, vars);
+    dom.removeClass("body", "error");
+    dom.removeClass("body", "form");
+    dom.addClass("body", "waiting");
+  }
+
+  function error(template, vars) {
+    render("#error", template, vars);
+    dom.removeClass("body", "waiting");
+    dom.removeClass("body", "form");
+    dom.addClass("body", "error");
+
+    bid.ErrorDisplay.start("#error");
+  }
+
+  return {
+    form: form,
+    wait: wait,
+    error: error
+  };
+}());
diff --git a/resources/static/dialog/resources/storage.js b/resources/static/shared/storage.js
similarity index 100%
rename from resources/static/dialog/resources/storage.js
rename to resources/static/shared/storage.js
diff --git a/resources/static/shared/templates.js b/resources/static/shared/templates.js
new file mode 100644
index 0000000000000000000000000000000000000000..58a7358fe20bab4f753cb49f5b015b877506ec89
--- /dev/null
+++ b/resources/static/shared/templates.js
@@ -0,0 +1,2 @@
+BrowserID.Templates = {};
+
diff --git a/resources/static/dialog/resources/tooltip.js b/resources/static/shared/tooltip.js
similarity index 90%
rename from resources/static/dialog/resources/tooltip.js
rename to resources/static/shared/tooltip.js
index 602602b4a31a97763bd23e9bf26406cd41a3d9f9..8800b6ceafe47aed1e30d334685c369b37af45c6 100644
--- a/resources/static/dialog/resources/tooltip.js
+++ b/resources/static/shared/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 */
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
@@ -40,17 +40,17 @@ BrowserID.Tooltip = (function() {
 
   var ANIMATION_TIME = 250,
       TOOLTIP_DISPLAY = 2000,
-      READ_WPM = 200;
+      READ_WPM = 200,
+      bid = BrowserID,
+      dom = bid.DOM,
+      renderer = bid.Renderer;
 
   function createTooltip(el) {
       var contents = el.html();
-      var template = $("#templateTooltip").html();
-      _.templateSettings = {
-          interpolate : /\{\{(.+?)\}\}/g
-      };
-      var tooltip = $(_.template(template, {
+
+      var tooltip = renderer.append("body", "tooltip", {
         contents: contents
-      }));
+      });
 
       return tooltip;
   }
@@ -79,11 +79,11 @@ BrowserID.Tooltip = (function() {
   }
 
   function createAndShowRelatedTooltip(el, relatedTo, complete) {
-      // This means create a copy of the tooltip element and position it in 
-      // relation to an element.  Right now we are putting the tooltip directly 
-      // above the element.  Once the tooltip is no longer needed, remove it 
+      // This means create a copy of the tooltip element and position it in
+      // relation to an element.  Right now we are putting the tooltip directly
+      // above the element.  Once the tooltip is no longer needed, remove it
       // from the DOM.
-      var tooltip = createTooltip(el).appendTo("body");
+      var tooltip = createTooltip(el);
 
       var target = $("#" + relatedTo);
       positionTooltip(tooltip, target);
@@ -101,7 +101,7 @@ BrowserID.Tooltip = (function() {
     el = $(el);
     var messageFor = el.attr("for");
 
-    // First, see if we are "for" another element, if we are, create a copy of 
+    // First, see if we are "for" another element, if we are, create a copy of
     // the tooltip to attach to the element.
     if(messageFor) {
       createAndShowRelatedTooltip(el, messageFor, complete);
diff --git a/resources/static/dialog/resources/user.js b/resources/static/shared/user.js
similarity index 100%
rename from resources/static/dialog/resources/user.js
rename to resources/static/shared/user.js
diff --git a/resources/static/dialog/resources/validation.js b/resources/static/shared/validation.js
similarity index 100%
rename from resources/static/dialog/resources/validation.js
rename to resources/static/shared/validation.js
diff --git a/resources/static/dialog/resources/wait-messages.js b/resources/static/shared/wait-messages.js
similarity index 100%
rename from resources/static/dialog/resources/wait-messages.js
rename to resources/static/shared/wait-messages.js
diff --git a/resources/static/dialog/funcunit.html b/resources/static/test/funcunit.html
similarity index 100%
rename from resources/static/dialog/funcunit.html
rename to resources/static/test/funcunit.html
diff --git a/resources/static/test/qunit.html b/resources/static/test/qunit.html
index 3e2ae0292f2d5da9e8b6826bd23d7c8af49032ff..05fcf8726607782e8873bf41e7c9943cebc7aabd 100644
--- a/resources/static/test/qunit.html
+++ b/resources/static/test/qunit.html
@@ -58,23 +58,13 @@
     </div>
 
     <ul class="notifications">
-      <li class="notification emailsent">Email Sent</li> 
-      <li class="notification doh">doh</li> 
+      <li class="notification emailsent">Email Sent</li>
+      <li class="notification doh">doh</li>
     </ul>
 
-    <ul id="emailList"> 
+    <ul id="emailList">
     </ul>
 
-    <script type="text/html" id="templateError">
-      <div class="message">{{ action.title }}</div>
-    </script>
-
-    <script type="text/html" id="templateTooltip">
-      <div class="tooltip" id="createdTooltip">
-        {{ contents }}
-      </div>
-    </script>
-
     <script type="text/html" id="templateUser">
       <li>{{email}}</li>
     </script>
diff --git a/resources/static/test/qunit/controllers/authenticate_controller_unit_test.js b/resources/static/test/qunit/controllers/authenticate_controller_unit_test.js
index 5358636ef39ca95d239ae1f9f3a703f9f66ec90f..a09787facf69e5dfa1ab91476919533c12e9e94e 100644
--- a/resources/static/test/qunit/controllers/authenticate_controller_unit_test.js
+++ b/resources/static/test/qunit/controllers/authenticate_controller_unit_test.js
@@ -34,10 +34,10 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery").then("/test/qunit/mocks/xhr", "/dialog/resources/network", "/dialog/controllers/page_controller", "/dialog/controllers/authenticate_controller", function() {
+steal.then("/dialog/controllers/page_controller", "/dialog/controllers/authenticate_controller", function() {
   "use strict";
 
-  var controller, 
+  var controller,
       el = $("body"),
       bid = BrowserID,
       storage = bid.Storage,
@@ -80,11 +80,11 @@ steal.plugins("jquery").then("/test/qunit/mocks/xhr", "/dialog/resources/network
         } catch(e) {
           // may already be destroyed from close inside of the controller.
         }
-      }    
+      }
       reset();
       storage.clear();
       network.setXHR($);
-    } 
+    }
   });
 
   test("setting email address prefills address field", function() {
diff --git a/resources/static/test/qunit/controllers/dialog_controller_unit_test.js b/resources/static/test/qunit/controllers/dialog_controller_unit_test.js
index bdfbe742ba55fc94d725627de349f31f05ddf01b..fa602bbc490831ace4460a91c039473173ebe83a 100644
--- a/resources/static/test/qunit/controllers/dialog_controller_unit_test.js
+++ b/resources/static/test/qunit/controllers/dialog_controller_unit_test.js
@@ -53,8 +53,8 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
   function initController() {
     controller = el.dialog({
       window: {
-        setupChannel: function() { 
-          if (channelError) throw "Channel error";  
+        setupChannel: function() {
+          if (channelError) throw "Channel error";
         }
       }
     }).controller();
@@ -69,7 +69,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", "/dialog/con
     teardown: function() {
       controller.destroy();
       reset();
-    } 
+    }
   });
 
   test("initialization with channel error", function() {
diff --git a/resources/static/test/qunit/controllers/page_controller_unit_test.js b/resources/static/test/qunit/controllers/page_controller_unit_test.js
index 299b8ed000444c38d3d8e3cf13ad80ad89f6b6a4..8d898fc49c23c4af2d5a91ec91f522f185cbc339 100644
--- a/resources/static/test/qunit/controllers/page_controller_unit_test.js
+++ b/resources/static/test/qunit/controllers/page_controller_unit_test.js
@@ -38,13 +38,14 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
   "use strict";
 
   var controller, el,
-      bodyTemplate = "testBodyTemplate.ejs",
-      waitTemplate = "wait.ejs";
+      bodyTemplate = "testBodyTemplate",
+      waitTemplate = "wait";
 
   function reset() {
     el = $("#controller_head");
     el.find("#formWrap .contents").html("");
     el.find("#wait .contents").html("");
+    $("#error").html("<div class='contents'></div>");
     el.find("#error .contents").html("");
   }
 
@@ -56,7 +57,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
     teardown: function() {
       controller.destroy();
       reset();
-    } 
+    }
   });
 
   test("page controller with no template causes no side effects", function() {
@@ -130,8 +131,8 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
         message: "Test message"
       }
     }).controller();
-   
-    controller.renderError("wait.ejs", {
+
+    controller.renderError("wait", {
       title: "error title",
       message: "error message"
     });
@@ -149,8 +150,8 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
         message: "Test message"
       }
     }).controller();
-   
-    controller.renderError("error.ejs", {
+
+    controller.renderError("error", {
       action: {
         title: "expanded action info",
         message: "expanded message"
@@ -180,7 +181,7 @@ steal.plugins("jquery").then("/dialog/controllers/page_controller", function() {
         message: "Test message"
       }
     }).controller();
-   
+
     // This is the medium level info.
     var func = controller.getErrorDialog({
       title: "medium level info error title",
diff --git a/resources/static/test/qunit/mocks/templates.js b/resources/static/test/qunit/mocks/templates.js
new file mode 100644
index 0000000000000000000000000000000000000000..d1d8ee165ad8db26d208bcec33fe93ef65be5c56
--- /dev/null
+++ b/resources/static/test/qunit/mocks/templates.js
@@ -0,0 +1,4 @@
+BrowserID.Templates = {
+  inMemoryTemplate: "<div id='templateInput'></div>"
+};
+
diff --git a/resources/static/test/qunit/pages/add_email_address_test.js b/resources/static/test/qunit/pages/add_email_address_test.js
index 93d1863ed3eb89e23eefa38b1bbacc215ae9582a..6bbca4c28cb2a1ee7b30b04fab931e7de7c16b6a 100644
--- a/resources/static/test/qunit/pages/add_email_address_test.js
+++ b/resources/static/test/qunit/pages/add_email_address_test.js
@@ -34,7 +34,7 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery").then("/dialog/resources/network", "/js/pages/add_email_address", function() {
+steal.then("/pages/add_email_address", function() {
   "use strict";
 
   var bid = BrowserID,
@@ -42,17 +42,19 @@ steal.plugins("jquery").then("/dialog/resources/network", "/js/pages/add_email_a
       storage = bid.Storage,
       xhr = bid.Mocks.xhr,
       validToken = true;
-  
+
   module("pages/add_email_address", {
     setup: function() {
-      network.setXHR(xhr);  
+      network.setXHR(xhr);
       xhr.useResult("valid");
-      $(".error").stop().hide();
+      $(".error").removeClass("error");
+      $("#error").stop().hide();
       $(".website").text("");
     },
     teardown: function() {
-      network.setXHR($);  
-      $(".error").stop().hide();
+      network.setXHR($);
+      $(".error").removeClass("error");
+      $("#error").stop().hide();
       $(".website").text("");
     }
   });
@@ -61,7 +63,7 @@ steal.plugins("jquery").then("/dialog/resources/network", "/js/pages/add_email_a
     storage.setStagedOnBehalfOf("browserid.org");
 
     bid.addEmailAddress("token");
-    
+
     setTimeout(function() {
       equal($("#email").text(), "testuser@testuser.com", "email set");
       ok($(".siteinfo").is(":visible"), "siteinfo is visible when we say what it is");
@@ -73,7 +75,7 @@ steal.plugins("jquery").then("/dialog/resources/network", "/js/pages/add_email_a
 
   test("addEmailAddress with good token and nosite", function() {
     bid.addEmailAddress("token");
-    
+
     setTimeout(function() {
       equal($("#email").text(), "testuser@testuser.com", "email set");
       equal($(".siteinfo").is(":visible"), false, "siteinfo is not visible without having it");
diff --git a/resources/static/test/qunit/js/browserid_unit_test.js b/resources/static/test/qunit/pages/browserid_unit_test.js
similarity index 93%
rename from resources/static/test/qunit/js/browserid_unit_test.js
rename to resources/static/test/qunit/pages/browserid_unit_test.js
index 840039df59b43dd36e016f6c23636bddcc784d55..7a5184845529e94fb662d0fb88aee81ddd04e2e1 100644
--- a/resources/static/test/qunit/js/browserid_unit_test.js
+++ b/resources/static/test/qunit/pages/browserid_unit_test.js
@@ -34,11 +34,11 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery", "funcunit/qunit").then("/js/page_helpers", "/js/browserid", function() {
+steal.then("/pages/page_helpers", "/pages/browserid", function() {
   "use strict";
 
-  module("/js/browserid");
-  
+  module("/pages/browserid");
+
 
 });
 
diff --git a/resources/static/test/qunit/pages/forgot_unit_test.js b/resources/static/test/qunit/pages/forgot_unit_test.js
index 2dbbca592ba47822b965e2e7621417b30b9717d7..ab17c1d0ea4bce88db65c1f957942bad42dc54a3 100644
--- a/resources/static/test/qunit/pages/forgot_unit_test.js
+++ b/resources/static/test/qunit/pages/forgot_unit_test.js
@@ -34,7 +34,7 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery").then("/dialog/resources/network", "/dialog/resources/user", "/js/pages/forgot", function() {
+steal.then("/pages/forgot", function() {
   "use strict";
 
   var bid = BrowserID,
@@ -46,13 +46,15 @@ steal.plugins("jquery").then("/dialog/resources/network", "/dialog/resources/use
   module("pages/forgot", {
     setup: function() {
       network.setXHR(xhr);
-      $(".error").stop().hide();
+      $(".error").removeClass("error");
+      $("#error").stop().hide();
       xhr.useResult("valid");
       bid.forgot();
     },
     teardown: function() {
       network.setXHR($);
-      $(".error").stop().hide();
+      $(".error").removeClass("error");
+      $("#error").stop().hide();
       $(".website").text("");
       bid.forgot.reset();
     }
@@ -110,7 +112,7 @@ steal.plugins("jquery").then("/dialog/resources/network", "/dialog/resources/use
     $("#email").val("testuser@testuser.com");
 
     testEmailNotSent(function() {
-      equal($("#error").is(":visible"), true, "error is visible");  
+      equal($("#error").is(":visible"), true, "error is visible");
       start();
     });
   });
diff --git a/resources/static/test/qunit/pages/manage_account_unit_test.js b/resources/static/test/qunit/pages/manage_account_unit_test.js
index c981153d2974acab134053d479a2282f25845463..8ac855e0c04bc2ab19d928bd687a7e886513695c 100644
--- a/resources/static/test/qunit/pages/manage_account_unit_test.js
+++ b/resources/static/test/qunit/pages/manage_account_unit_test.js
@@ -34,7 +34,7 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery").then("/dialog/resources/network", "/js/pages/manage_account", function() {
+steal.then("/pages/manage_account", function() {
   "use strict";
 
   var bid = BrowserID,
@@ -53,17 +53,19 @@ steal.plugins("jquery").then("/dialog/resources/network", "/js/pages/manage_acco
 
   module("pages/manage_account", {
     setup: function() {
-      network.setXHR(xhr);  
+      network.setXHR(xhr);
       xhr.useResult("valid");
       user.setOrigin(TEST_ORIGIN);
       $("#emailList").empty();
+      $(".error").removeClass("error");
       $("#error").hide();
       mocks.document.location = "";
       storage.clear();
     },
     teardown: function() {
-      network.setXHR($);  
+      network.setXHR($);
       $("#emailList").empty();
+      $(".error").removeClass("error");
       $("#error").hide();
     }
   });
@@ -74,7 +76,7 @@ steal.plugins("jquery").then("/dialog/resources/network", "/js/pages/manage_acco
     bid.manageAccount(mocks);
 
     setTimeout(function() {
-      equal($("#emailList").children().length, 0, "no children have been added"); 
+      equal($("#emailList").children().length, 0, "no children have been added");
       start();
     }, TEST_DELAY);
 
@@ -85,7 +87,7 @@ steal.plugins("jquery").then("/dialog/resources/network", "/js/pages/manage_acco
     bid.manageAccount(mocks);
 
     setTimeout(function() {
-      equal($("#emailList").children().length, 1, "there has been one child added"); 
+      equal($("#emailList").children().length, 1, "there has been one child added");
       start();
     }, TEST_DELAY);
 
@@ -98,7 +100,7 @@ steal.plugins("jquery").then("/dialog/resources/network", "/js/pages/manage_acco
     bid.manageAccount(mocks);
 
     setTimeout(function() {
-      equal($("#error").is(":visible"), true, "error message is visible on XHR error"); 
+      equal($("#error").is(":visible"), true, "error message is visible on XHR error");
       start();
     }, ERROR_DELAY);
 
@@ -117,7 +119,7 @@ steal.plugins("jquery").then("/dialog/resources/network", "/js/pages/manage_acco
       bid.manageAccount.removeEmail("testuser@testuser.com");
 
       setTimeout(function() {
-        equal($("#emailList").children().length, 1, "after removing an email, only one remains"); 
+        equal($("#emailList").children().length, 1, "after removing an email, only one remains");
         start();
       }, TEST_DELAY);
     }, TEST_DELAY);
@@ -136,7 +138,7 @@ steal.plugins("jquery").then("/dialog/resources/network", "/js/pages/manage_acco
       bid.manageAccount.removeEmail("testuser@testuser.com");
 
       setTimeout(function() {
-        equal($("#error").is(":visible"), true, "error message is visible on XHR error"); 
+        equal($("#error").is(":visible"), true, "error message is visible on XHR error");
         start();
       }, ERROR_DELAY);
     }, TEST_DELAY);
@@ -170,7 +172,7 @@ steal.plugins("jquery").then("/dialog/resources/network", "/js/pages/manage_acco
       bid.manageAccount.removeEmail("testuser@testuser.com");
 
       setTimeout(function() {
-        equal($("#error").is(":visible"), true, "error message is visible on XHR error"); 
+        equal($("#error").is(":visible"), true, "error message is visible on XHR error");
         start();
       }, ERROR_DELAY);
     }, TEST_DELAY);
@@ -202,7 +204,7 @@ steal.plugins("jquery").then("/dialog/resources/network", "/js/pages/manage_acco
       bid.manageAccount.cancelAccount();
 
       setTimeout(function() {
-        equal($("#error").is(":visible"), true, "error message is visible on XHR error"); 
+        equal($("#error").is(":visible"), true, "error message is visible on XHR error");
         start();
       }, ERROR_DELAY);
     }, TEST_DELAY);
diff --git a/resources/static/test/qunit/js/page_helpers_unit_test.js b/resources/static/test/qunit/pages/page_helpers_unit_test.js
similarity index 95%
rename from resources/static/test/qunit/js/page_helpers_unit_test.js
rename to resources/static/test/qunit/pages/page_helpers_unit_test.js
index 9f076596ea5b4691651f1e9fa7705923b556fc02..813b669be051331496b7649c75eeb7798be1f75a 100644
--- a/resources/static/test/qunit/js/page_helpers_unit_test.js
+++ b/resources/static/test/qunit/pages/page_helpers_unit_test.js
@@ -34,13 +34,13 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid", function() {
+steal.then(function() {
   "use strict";
 
   var pageHelpers = BrowserID.PageHelpers;
 
-  module("/js/page_helpers");
-  
+  module("pages/page_helpers");
+
 
   test("setStoredEmail/getStoredEmail/setupEmail prefills the email address", function() {
     $("#email").val("");
@@ -58,7 +58,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid", fu
     pageHelpers.setStoredEmail("testuser@testuser.co");
     pageHelpers.setupEmail();
 
-    // The fake jQuery event does not actually cause the letter to be added, we 
+    // The fake jQuery event does not actually cause the letter to be added, we
     // have to do that manually.
     $("#email").val("testuser@testuser.com");
 
diff --git a/resources/static/test/qunit/pages/signin_unit_test.js b/resources/static/test/qunit/pages/signin_unit_test.js
index ad6738d65ec8df41717e0c25135f7371673c713b..8f001abb41696a43bf89e1d18c680f475b2a9b79 100644
--- a/resources/static/test/qunit/pages/signin_unit_test.js
+++ b/resources/static/test/qunit/pages/signin_unit_test.js
@@ -34,7 +34,7 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery").then("/test/qunit/mocks/xhr", "/dialog/resources/network", "/dialog/resources/user", "/js/pages/signin", function() {
+steal.then("/pages/signin", function() {
   "use strict";
 
   var bid = BrowserID,
@@ -49,6 +49,7 @@ steal.plugins("jquery").then("/test/qunit/mocks/xhr", "/dialog/resources/network
   module("pages/signin", {
     setup: function() {
       network.setXHR(xhr);
+      $(".error").removeClass("error");
       $("#error").stop().hide();
       xhr.useResult("valid");
       docMock.location = "signin";
@@ -56,6 +57,7 @@ steal.plugins("jquery").then("/test/qunit/mocks/xhr", "/dialog/resources/network
     },
     teardown: function() {
       network.setXHR($);
+      $(".error").removeClass("error");
       $("#error").stop().hide();
       $("#error .message").remove();
       bid.signIn.reset();
@@ -118,7 +120,7 @@ steal.plugins("jquery").then("/test/qunit/mocks/xhr", "/dialog/resources/network
 
     testUserNotSignedIn(function() {
       setTimeout(function() {
-        equal($("#error").is(":visible"), true, "error is visible");  
+        equal($("#error").is(":visible"), true, "error is visible");
         start();
       }, 500);
     });
diff --git a/resources/static/test/qunit/pages/signup_unit_test.js b/resources/static/test/qunit/pages/signup_unit_test.js
index 3644704e374b6ec0103cbcac5e93dd7c4af44017..e6aae6e93447bdde34ae4139adcfd95dc3d79050 100644
--- a/resources/static/test/qunit/pages/signup_unit_test.js
+++ b/resources/static/test/qunit/pages/signup_unit_test.js
@@ -34,7 +34,7 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery").then("/test/qunit/mocks/xhr", "/dialog/resources/network", "/dialog/resources/user", "/js/pages/signup", function() {
+steal.then("/pages/signup", function() {
   "use strict";
 
   var bid = BrowserID,
@@ -47,6 +47,7 @@ steal.plugins("jquery").then("/test/qunit/mocks/xhr", "/dialog/resources/network
   module("pages/signup", {
     setup: function() {
       network.setXHR(xhr);
+      $(".error").removeClass("error");
       $("#error").stop().hide();
       $(".notification").stop().hide();
       xhr.useResult("valid");
@@ -55,6 +56,7 @@ steal.plugins("jquery").then("/test/qunit/mocks/xhr", "/dialog/resources/network
     },
     teardown: function() {
       network.setXHR($);
+      $(".error").removeClass("error");
       $("#error").stop().hide();
       $(".notification").stop().hide();
       $("#error .message").remove();
diff --git a/resources/static/test/qunit/pages/verify_email_address_test.js b/resources/static/test/qunit/pages/verify_email_address_test.js
index 879e8cf0a3d45d27f580bb74b3e78f3d69d5cee1..e1d273a9a63db1ad87c3778d42837a27ea65aa97 100644
--- a/resources/static/test/qunit/pages/verify_email_address_test.js
+++ b/resources/static/test/qunit/pages/verify_email_address_test.js
@@ -34,7 +34,7 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery").then("/dialog/resources/network", "/js/pages/verify_email_address", function() {
+steal.then("/pages/verify_email_address", function() {
   "use strict";
 
   var bid = BrowserID,
@@ -42,17 +42,21 @@ steal.plugins("jquery").then("/dialog/resources/network", "/js/pages/verify_emai
       storage = bid.Storage,
       xhr = bid.Mocks.xhr,
       validToken = true;
-  
+
   module("pages/verify_email_address", {
     setup: function() {
-      network.setXHR(xhr);  
+      network.setXHR(xhr);
       xhr.useResult("valid");
-      $("#error,.error").stop().hide();
+      $("body").removeClass("error");
+      $(".error").removeClass("error");
+      $("#error").stop().hide();
       $(".website").text("");
     },
     teardown: function() {
-      network.setXHR($);  
-      $("#error,.error").stop().hide();
+      network.setXHR($);
+      $("body").removeClass("error");
+      $(".error").removeClass("error");
+      $("#error").stop().hide();
       $(".website").text("");
     }
   });
@@ -61,7 +65,7 @@ steal.plugins("jquery").then("/dialog/resources/network", "/js/pages/verify_emai
     storage.setStagedOnBehalfOf("browserid.org");
 
     bid.verifyEmailAddress("token");
-    
+
     setTimeout(function() {
       equal($("#email").val(), "testuser@testuser.com", "email set");
       ok($(".siteinfo").is(":visible"), "siteinfo is visible when we say what it is");
@@ -77,7 +81,7 @@ steal.plugins("jquery").then("/dialog/resources/network", "/js/pages/verify_emai
 
     bid.verifyEmailAddress("token");
 
-    
+
     setTimeout(function() {
       equal($("#email").val(), "testuser@testuser.com", "email set");
       equal($(".siteinfo").is(":visible"), false, "siteinfo is not visible without having it");
diff --git a/resources/static/test/qunit/qunit.js b/resources/static/test/qunit/qunit.js
index c3722bed4afa027eb93cb038aabae29b696d60c5..c4a5bce555966e6a61efa100e2518693aa8314c3 100644
--- a/resources/static/test/qunit/qunit.js
+++ b/resources/static/test/qunit/qunit.js
@@ -1,47 +1,50 @@
-steal("/dialog/resources/browserid.js",
-      "/test/qunit/mocks/mocks.js",
-      "/test/qunit/mocks/xhr.js",
-      "/dialog/resources/browser-support.js",
-      "/dialog/resources/error-messages.js",
-      "/dialog/resources/error-display.js",
-      "/dialog/resources/storage.js",
-      "/dialog/resources/tooltip.js",
-      "/dialog/resources/validation.js",
-      "/dialog/resources/underscore-min.js"
-      )
-  .plugins(
-    "jquery", 
-    "jquery/controller",
-    "jquery/controller/subscribe",
-    "jquery/controller/view",
-    "jquery/view/ejs",
-    "funcunit/qunit")
-	.views('testBodyTemplate.ejs',
-         'wait.ejs',
-         'pickemail.ejs',
-         'offline.ejs',
-         'error.ejs')
-  .then("js/browserid_unit_test")
-  .then("js/page_helpers_unit_test")
-  .then("include_unit_test")
-  .then("relay/relay_unit_test")
-  .then("pages/add_email_address_test")
-  .then("pages/verify_email_address_test")
-  .then("pages/forgot_unit_test")
-  .then("pages/signin_unit_test")
-  .then("pages/signup_unit_test")
-  .then("pages/manage_account_unit_test")
-  .then("resources/tooltip_unit_test")
-  .then("resources/error-display_unit_test")
-  .then("resources/channel_unit_test")
-  .then("resources/browser-support_unit_test")
-  .then("resources/validation_unit_test")
-  .then("resources/storage_unit_test")
-  .then("resources/network_unit_test")
-  .then("resources/user_unit_test")
-  .then("controllers/page_controller_unit_test")
-  .then("controllers/pickemail_controller_unit_test")
-  .then("controllers/dialog_controller_unit_test")
-  .then("controllers/checkregistration_controller_unit_test")
-  .then("controllers/authenticate_controller_unit_test")
+steal.plugins(
+      "jquery",
+      "jquery/controller",
+      "jquery/controller/subscribe",
+      "funcunit/qunit")
+  .then(
+      "/lib/underscore-min",
+      "/lib/ejs",
+      "/shared/browserid",
+      "/lib/dom-jquery",
+      "mocks/mocks",
+      "mocks/xhr",
+      "/shared/renderer",
+      "/shared/screens",
+      "/shared/browser-support",
+      "/shared/error-messages",
+      "/shared/error-display",
+      "/shared/storage",
+      "/shared/tooltip",
+      "/shared/network",
+      "/shared/user",
+      "/shared/validation",
+
+      "pages/browserid_unit_test",
+      "pages/page_helpers_unit_test",
+
+      "include_unit_test",
+      "relay/relay_unit_test",
+      "pages/add_email_address_test",
+      "pages/verify_email_address_test",
+      "pages/forgot_unit_test",
+      "pages/signin_unit_test",
+      "pages/signup_unit_test",
+      "pages/manage_account_unit_test",
+      "shared/renderer_unit_test",
+      "shared/screens_unit_test",
+      "shared/tooltip_unit_test",
+      "shared/error-display_unit_test",
+      "shared/browser-support_unit_test",
+      "shared/validation_unit_test",
+      "shared/storage_unit_test",
+      "shared/network_unit_test",
+      "shared/user_unit_test",
+      "resources/channel_unit_test",
+      "controllers/page_controller_unit_test",
+      "controllers/pickemail_controller_unit_test",
+      "controllers/dialog_controller_unit_test",
+      "controllers/checkregistration_controller_unit_test",
+      "controllers/authenticate_controller_unit_test");
 
diff --git a/resources/static/test/qunit/relay/relay_unit_test.js b/resources/static/test/qunit/relay/relay_unit_test.js
index ae7e6dc6109e84fd1d7e6957d0e208a14218212f..566c5a90fc9dee8a9662ffe2c27d1b97e60338c6 100644
--- a/resources/static/test/qunit/relay/relay_unit_test.js
+++ b/resources/static/test/qunit/relay/relay_unit_test.js
@@ -34,7 +34,7 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery").then("/dialog/resources/jschannel", "/relay/relay", function() {
+steal.plugins("jquery").then("/lib/jschannel", "/relay/relay", function() {
   "use strict";
 
   var winMock = {},
@@ -44,7 +44,7 @@ steal.plugins("jquery").then("/dialog/resources/jschannel", "/relay/relay", func
     build: function(options) {
       this.options = options;
 
-      channelMock.bindMessage = channelMock.cb = channelMock.status = 
+      channelMock.bindMessage = channelMock.cb = channelMock.status =
         channelMock.errorCode = channelMock.verboseError = undefined;
 
       return {
@@ -59,7 +59,7 @@ steal.plugins("jquery").then("/dialog/resources/jschannel", "/relay/relay", func
     receiveGetVerifiedEmail: function() {
       // cb is the mock callback that is passed to Channel.bind
       channelMock.cb({
-        origin: "Origin", 
+        origin: "Origin",
         delayReturn: function() {},
         complete: function(status) {
           channelMock.status = status;
@@ -84,13 +84,13 @@ steal.plugins("jquery").then("/dialog/resources/jschannel", "/relay/relay", func
     teardown: function() {
       relay.init({
         window: window.parent,
-        channel: Channel 
+        channel: Channel
       });
     }
   });
 
   test("Can open the relay, happy case", function() {
-    relay.open(); 
+    relay.open();
 
     /**
      * Check to make sure channel build is correct
diff --git a/resources/static/test/qunit/resources/channel_unit_test.js b/resources/static/test/qunit/resources/channel_unit_test.js
index e5c26c079329da29b87651dc9999661af4965adc..94c2df40addc07c35e08a4c5371f7bd23c6e7d95 100644
--- a/resources/static/test/qunit/resources/channel_unit_test.js
+++ b/resources/static/test/qunit/resources/channel_unit_test.js
@@ -34,7 +34,7 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/channel", function() {
+steal.then("/dialog/resources/channel", function() {
   var channel = BrowserID.Channel;
 
   var navMock = {
@@ -83,7 +83,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/channel", func
   test("window.setupChannel exists for legacy uses", function() {
     ok(typeof window.setupChannel, "function", "window.setupChannel exists for legacy uses");
   });
-  
+
   test("IFRAME channel with assertion", function() {
     channel.init({
       window: winMock,
@@ -138,7 +138,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/channel", func
 
   test("IFRAME channel with error on open", function() {
     var winMockWithoutRelay = $.extend(true, {}, winMock);
-    delete winMockWithoutRelay.opener.frames.browserid_relay_1234; 
+    delete winMockWithoutRelay.opener.frames.browserid_relay_1234;
 
     channel.init({
       window: winMockWithoutRelay,
diff --git a/resources/static/test/qunit/resources/browser-support_unit_test.js b/resources/static/test/qunit/shared/browser-support_unit_test.js
similarity index 97%
rename from resources/static/test/qunit/resources/browser-support_unit_test.js
rename to resources/static/test/qunit/shared/browser-support_unit_test.js
index a26857f01eef422ad03c0cc49fbb4d9257957cef..bbba6db904bc9b406e7eec0168e5c26865052923 100644
--- a/resources/static/test/qunit/resources/browser-support_unit_test.js
+++ b/resources/static/test/qunit/shared/browser-support_unit_test.js
@@ -34,7 +34,7 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery", "funcunit/qunit").then(function() {
+steal.then(function() {
   "use strict";
 
   var bid = BrowserID,
@@ -42,7 +42,7 @@ steal.plugins("jquery", "funcunit/qunit").then(function() {
       stubWindow,
       stubNavigator;
 
-  module("browser-support", {
+  module("shared/browser-support", {
     setup: function() {
       // Hard coded goodness for testing purposes
       stubNavigator = {
@@ -61,7 +61,7 @@ steal.plugins("jquery", "funcunit/qunit").then(function() {
     teardown: function() {
     }
   });
-  
+
   test("browser without localStorage", function() {
     delete stubWindow.localStorage;
 
diff --git a/resources/static/test/qunit/resources/error-display_unit_test.js b/resources/static/test/qunit/shared/error-display_unit_test.js
similarity index 78%
rename from resources/static/test/qunit/resources/error-display_unit_test.js
rename to resources/static/test/qunit/shared/error-display_unit_test.js
index 842ba40d82ad0adfa4b01f08baf578427c84dac7..232641dae56762422810b0dadc9fbdf0eaa15b72 100644
--- a/resources/static/test/qunit/resources/error-display_unit_test.js
+++ b/resources/static/test/qunit/shared/error-display_unit_test.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 */
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
@@ -34,27 +34,34 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/error-display", function() {
+steal.then(function() {
   "use strict";
 
   var bid = BrowserID,
       errorDisplay = bid.ErrorDisplay;
 
-  module("/resources/error-display", {
+  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();
     }
   });
 
-  test("can show an error", function() {
-    var target = $("#error .contents");
-    target.empty();
+  test("can initialize and open the error display", function() {
+    $("#error").show();
+    bid.ErrorDisplay.start("#error");
+    bid.ErrorDisplay.open();
 
-    errorDisplay.render(target, "#templateError", { action: { title: "Error Message" } });
+    setTimeout(function() {
+      ok($("#moreInfo").is(":visible"), "expanded info is visible");
+      start();
+    }, 100);
 
-    ok(target.html(), "Error has some contents");
+    stop();
   });
 
 
+
 });
diff --git a/resources/static/test/qunit/resources/network_unit_test.js b/resources/static/test/qunit/shared/network_unit_test.js
similarity index 98%
rename from resources/static/test/qunit/resources/network_unit_test.js
rename to resources/static/test/qunit/shared/network_unit_test.js
index cfd90f41d51b242263c573db20395d800c739f98..525ce1c011f59d41dc76f346440b0807e81bea23 100644
--- a/resources/static/test/qunit/resources/network_unit_test.js
+++ b/resources/static/test/qunit/shared/network_unit_test.js
@@ -34,7 +34,7 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", function() {
+steal.then(function() {
   "use strict";
 
   var testName,
@@ -53,8 +53,8 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
   }
 
   function notificationCheck(cb) {
-    // Take the original arguments, take off the function.  Add any additional 
-    // arguments that were passed in, and then tack on the onSuccess and 
+    // Take the original arguments, take off the function.  Add any additional
+    // arguments that were passed in, and then tack on the onSuccess and
     // onFailure to the end.  Then call the callback.
     var args = Array.prototype.slice.call(arguments, 1);
 
@@ -82,11 +82,11 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
   }
 
   function failureCheck(cb) {
-    // Take the original arguments, take off the function.  Add any additional 
-    // arguments that were passed in, and then tack on the onSuccess and 
+    // Take the original arguments, take off the function.  Add any additional
+    // arguments that were passed in, and then tack on the onSuccess and
     // onFailure to the end.  Then call the callback.
     var args = Array.prototype.slice.call(arguments, 1);
-    
+
     args.push(function onSuccess(authenticated) {
       ok(false, "XHR failure should never pass");
       wrappedStart();
@@ -108,7 +108,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
 
   var network = BrowserID.Network;
 
-  module("/resources/network", {
+  module("shared/network", {
     setup: function() {
       network.setXHR(xhr);
       xhr.useResult("valid");
@@ -187,8 +187,8 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     xhr.useResult("ajaxError");
     xhr.setContextInfo("authenticated", false);
 
-    // Do not convert this to failureCheck, we do this manually because 
-    // checkAuth does not make an XHR request.  Since it does not make an XHR 
+    // Do not convert this to failureCheck, we do this manually because
+    // checkAuth does not make an XHR request.  Since it does not make an XHR
     // request, we do not test whether the app is notified of an XHR failure
     network.checkAuth(function onSuccess() {
       ok(true, "checkAuth does not make an ajax call, all good");
@@ -227,7 +227,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
   wrappedAsyncTest("complete_email_addition valid", function() {
     network.completeEmailRegistration("goodtoken", function onSuccess(proven) {
       equal(proven, true, "good token proved");
-      wrappedStart(); 
+      wrappedStart();
     }, function onFailure() {
       wrappedStart();
     });
@@ -239,7 +239,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     xhr.useResult("invalid");
     network.completeEmailRegistration("badtoken", function onSuccess(proven) {
       equal(proven, false, "bad token could not be proved");
-      wrappedStart(); 
+      wrappedStart();
     }, function onFailure() {
       wrappedStart();
     });
@@ -647,8 +647,8 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/network", func
     network.serverTime(function onSuccess(time) {
       var diff = Math.abs((new Date()) - time);
       equal(1245 < diff && diff < 1255, true, "server time and local time should be less than 100ms different (is " + diff + "ms different)");
-      // XXX by stomlinson - I think this is an incorrect test.  The time returned here is the 
-      // time as it is on the server, which could be more than 100ms off of 
+      // XXX by stomlinson - I think this is an incorrect test.  The time returned here is the
+      // time as it is on the server, which could be more than 100ms off of
       // what the local machine says it is.
       //equal(Math.abs(diff) < 100, true, "server time and local time should be less than 100ms different (is " + diff + "ms different)");
       wrappedStart();
diff --git a/resources/static/test/qunit/shared/renderer_unit_test.js b/resources/static/test/qunit/shared/renderer_unit_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..f33e59cc09288ec3282cdd8ce998f40fdc642e45
--- /dev/null
+++ b/resources/static/test/qunit/shared/renderer_unit_test.js
@@ -0,0 +1,82 @@
+/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*globals BrowserID: true, _:true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla bid.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+steal.then("/test/qunit/mocks/templates", "/shared/renderer", function() {
+  "use strict";
+
+  var bid = BrowserID,
+      renderer = bid.Renderer;
+
+  module("shared/renderer", {
+    setup: function() {
+
+    },
+
+    teardown: function() {
+
+    }
+  });
+
+  test("render template loaded using XHR", function() {
+    $("#formWrap .contents").empty();
+    $("#templateInput").remove();
+
+    renderer.render("#formWrap .contents", "testBodyTemplate");
+
+    ok($("#templateInput").length, "template written when loaded using XHR");
+  });
+
+  test("render template from memory", function() {
+    $("#formWrap .contents").empty();
+    $("#templateInput").remove();
+
+    renderer.render("#formWrap .contents", "inMemoryTemplate");
+
+    ok($("#templateInput").length, "template written when loaded from memory");
+  });
+
+  test("append template to element", function() {
+    $("#formWrap .contents").empty();
+    $("#templateInput").remove();
+
+    renderer.append("#formWrap", "inMemoryTemplate");
+
+    ok($("#formWrap > #templateInput").length && $("#formWrap > .contents"), "template appended to element instead of overwriting it");
+
+  });
+});
+
+
diff --git a/resources/static/test/qunit/shared/screens_unit_test.js b/resources/static/test/qunit/shared/screens_unit_test.js
new file mode 100644
index 0000000000000000000000000000000000000000..70df97ee4f90ff20d77797b89ccf6afbea15e779
--- /dev/null
+++ b/resources/static/test/qunit/shared/screens_unit_test.js
@@ -0,0 +1,86 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global steal: true, test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID: true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+steal.then(function() {
+  "use strict";
+
+  var bid = BrowserID,
+      screens = bid.Screens,
+      el;
+
+  module("shared/screens", {
+    setup: function() {
+
+    },
+
+    teardown: function() {
+      el.empty();
+    }
+  });
+
+  test("form", function() {
+    el = $("#formWrap .contents");
+    el.empty();
+    screens.form("testBodyTemplate");
+
+    ok($("#templateInput").length, "the template has been written");
+    equal($("body").hasClass("error"), false, "error class taken off of body");
+    equal($("body").hasClass("waiting"), false, "waiting class taken off of body");
+    equal($("body").hasClass("form"), true, "form class added to body");
+  });
+
+  test("wait", function() {
+    var el = $("#wait .contents");
+    el.empty();
+    screens.wait("testBodyTemplate");
+
+    ok($("#templateInput").length, "the template has been written");
+    equal($("body").hasClass("error"), false, "error class taken off of body");
+    equal($("body").hasClass("form"), false, "form class taken off of body");
+    equal($("body").hasClass("waiting"), true, "waiting class added to body");
+  });
+
+  test("error", function() {
+    var el = $("#error .contents");
+    el.empty();
+    screens.error("testBodyTemplate");
+
+    ok($("#templateInput").length, "the template has been written");
+    equal($("body").hasClass("waiting"), false, "waiting class taken off of body");
+    equal($("body").hasClass("form"), false, "form class taken off of body");
+    equal($("body").hasClass("error"), true, "error class added to body");
+  });
+});
diff --git a/resources/static/test/qunit/resources/storage_unit_test.js b/resources/static/test/qunit/shared/storage_unit_test.js
similarity index 97%
rename from resources/static/test/qunit/resources/storage_unit_test.js
rename to resources/static/test/qunit/shared/storage_unit_test.js
index 40e17f5a7e872b4d05368b2b6e49cfc72f170a6b..0a62353804f20e440338dc00df61691d5cd75dcc 100644
--- a/resources/static/test/qunit/resources/storage_unit_test.js
+++ b/resources/static/test/qunit/shared/storage_unit_test.js
@@ -34,10 +34,10 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/storage", function() {
+steal.then(function() {
   var storage = BrowserID.Storage;
 
-  module("storage", {
+  module("shared/storage", {
     setup: function() {
       storage.clear();
     },
@@ -141,9 +141,9 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/storage", func
     try {
       storage.site.set("www.testsite.com", "email", "testuser@testuser.com");
     } catch(e) {
-      error = e; 
+      error = e;
     }
-    
+
     equal(error.toString(), "unknown email address", "An unknown email address was added");
   });
 
diff --git a/resources/static/test/qunit/resources/tooltip_unit_test.js b/resources/static/test/qunit/shared/tooltip_unit_test.js
similarity index 87%
rename from resources/static/test/qunit/resources/tooltip_unit_test.js
rename to resources/static/test/qunit/shared/tooltip_unit_test.js
index ab12693190b8de498bdc9a7f9e3ef2aae0d5c77b..3dde6b3d3cef4d66069524f68549c9fc3ac442c5 100644
--- a/resources/static/test/qunit/resources/tooltip_unit_test.js
+++ b/resources/static/test/qunit/shared/tooltip_unit_test.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 */
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
@@ -34,13 +34,13 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/tooltip", function() {
+steal.then(function() {
   "use strict";
 
   var bid = BrowserID,
       tooltip = bid.Tooltip
 
-  module("/resources/tooltip", {
+  module("shared/tooltip", {
     setup: function() {
     },
     teardown: function() {
@@ -59,11 +59,6 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/tooltip", func
       start();
     });
 
-    var el = $("#createdTooltip");
-    equal(el.length, 1, "one tooltip created");
-    var contents = el.html() || "";
-    equal(contents.indexOf("contents") === -1, true, "contents have been replaced");
-
     stop();
   });
 
diff --git a/resources/static/test/qunit/resources/user_unit_test.js b/resources/static/test/qunit/shared/user_unit_test.js
similarity index 95%
rename from resources/static/test/qunit/resources/user_unit_test.js
rename to resources/static/test/qunit/shared/user_unit_test.js
index 5efbb5dbb52adfba5fe0391c0cbbd99fb5fb316e..66d46bd73b19713b4103d6cfcaa2c0df65c5ee11 100644
--- a/resources/static/test/qunit/resources/user_unit_test.js
+++ b/resources/static/test/qunit/shared/user_unit_test.js
@@ -38,7 +38,7 @@ var jwk = require("./jwk");
 var jwt = require("./jwt");
 var jwcert = require("./jwcert");
 
-steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", function() {
+steal.then(function() {
   var lib = BrowserID.User,
       storage = BrowserID.Storage,
       xhr = BrowserID.Mocks.xhr,
@@ -56,7 +56,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
 
     // Decode the assertion to a bundle.
     var bundle = JSON.parse(window.atob(assertion));
-    
+
     // Make sure both parts of the bundle exist
     ok(bundle.certificates && bundle.certificates.length, "we have an array like object for the certificates");
     equal(typeof bundle.assertion, "string");
@@ -70,9 +70,9 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     equal(tok.audience, testOrigin, "correct audience");
     var expires = tok.expires.getTime();
     ok(typeof expires === "number" && !isNaN(expires), "expiration date is valid");
-  
+
     var nowPlus2Mins = new Date().getTime() + (2 * 60 * 1000);
-    // expiration date must be within 5 seconds of 2 minutes from now - see 
+    // expiration date must be within 5 seconds of 2 minutes from now - see
     // issue 433 (https://github.com/mozilla/browserid/issues/433)
     ok(((nowPlus2Mins - 5000) < expires) && (expires < (nowPlus2Mins + 5000)), "expiration date must be within 5 seconds of 2 minutes from now");
 
@@ -86,7 +86,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     */
   }
 
-  module("resources/user", {
+  module("shared/user", {
     setup: function() {
       BrowserID.Network.setXHR(xhr);
       xhr.useResult("valid");
@@ -115,7 +115,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     lib.setOrigin(origin);
 
     var hostname = lib.getHostname();
-    equal(hostname, "testorigin.com", "getHostname returns only the hostname"); 
+    equal(hostname, "testorigin.com", "getHostname returns only the hostname");
   });
 
   test("getStoredEmailKeypairs", function() {
@@ -144,9 +144,9 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     lib.clearStoredEmailKeypairs();
     var identities = lib.getStoredEmailKeypairs();
     var count = 0;
-    for(var key in identities) { 
+    for(var key in identities) {
       if(identities.hasOwnProperty(key)) {
-        count++; 
+        count++;
       }
     }
 
@@ -180,7 +180,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       ok(false, "xhr failure should never succeed");
       start();
     }, function() {
-      ok(true, "xhr failure should always be a failure"); 
+      ok(true, "xhr failure should always be a failure");
       start();
     });
 
@@ -243,7 +243,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       start();
     }, function() {
       ok(storage.getStagedOnBehalfOf(), "staged on behalf of is not cleared on XHR failure");
-      ok(true, "xhr failure should always be a failure"); 
+      ok(true, "xhr failure should always be a failure");
       start();
     });
 
@@ -254,7 +254,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     storage.setStagedOnBehalfOf(testOrigin);
 
     lib.verifyUser("token", "password", function onSuccess(info) {
-      
+
       ok(info.valid, "token was valid");
       equal(info.email, "testuser@testuser.com", "email part of info");
       equal(info.origin, testOrigin, "origin in info");
@@ -270,7 +270,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     xhr.useResult("invalid");
 
     lib.verifyUser("token", "password", function onSuccess(info) {
-      
+
       equal(info.valid, false, "bad token calls onSuccess with a false validity");
 
       start();
@@ -287,10 +287,10 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       ok(false, "xhr failure should never succeed");
       start();
     }, function() {
-      ok(true, "xhr failure should always be a failure"); 
+      ok(true, "xhr failure should always be a failure");
       start();
     });
-      
+
     stop();
   });
 
@@ -310,7 +310,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       equal(status.success, true, "password reset for known user");
       start();
     }, function() {
-      ok(false, "onFailure should not be called"); 
+      ok(false, "onFailure should not be called");
       start();
     });
 
@@ -323,7 +323,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       equal(status.reason, "invalid_user", "invalid_user is the reason");
       start();
     }, function() {
-      ok(false, "onFailure should not be called"); 
+      ok(false, "onFailure should not be called");
       start();
     });
 
@@ -337,7 +337,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       equal(status.reason, "throttle", "password reset was throttled");
       start();
     }, function() {
-      ok(false, "onFailure should not be called"); 
+      ok(false, "onFailure should not be called");
       start();
     });
 
@@ -350,7 +350,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       ok(false, "xhr failure should never succeed");
       start();
     }, function() {
-      ok(true, "xhr failure should always be a failure"); 
+      ok(true, "xhr failure should always be a failure");
       start();
     });
 
@@ -387,7 +387,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       ok(false, "xhr failure should never succeed");
       start();
     }, function() {
-      ok(true, "xhr failure should always be a failure"); 
+      ok(true, "xhr failure should always be a failure");
       start();
     });
 
@@ -426,7 +426,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       ok(false, "xhr failure should never succeed");
       start();
     }, function() {
-      ok(true, "xhr failure should always be a failure"); 
+      ok(true, "xhr failure should always be a failure");
       start();
     });
 
@@ -471,10 +471,10 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     lib.checkAuthenticationAndSync(function onSuccess() {
     }, function onComplete() {
       ok(false, "xhr failure should never succeed");
-      
+
       start();
     }, function onFailure() {
-      ok(true, "xhr failure should always be a failure"); 
+      ok(true, "xhr failure should always be a failure");
       start();
     });
 
@@ -512,7 +512,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       ok(false, "xhr failure should never succeed");
       start();
     }, function() {
-      ok(true, "xhr failure should always be a failure"); 
+      ok(true, "xhr failure should always be a failure");
       start();
     });
 
@@ -526,7 +526,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       var identities = lib.getStoredEmailKeypairs();
       equal(false, "testemail@testemail.com" in identities, "Our new email is not added until confirmation.");
 
-      equal(storage.getStagedOnBehalfOf(), lib.getHostname(), "initiatingOrigin is stored"); 
+      equal(storage.getStagedOnBehalfOf(), lib.getHostname(), "initiatingOrigin is stored");
 
       start();
     }, failure("addEmail failure"));
@@ -543,7 +543,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       var identities = lib.getStoredEmailKeypairs();
       equal(false, "testemail@testemail.com" in identities, "Our new email is not added until confirmation.");
 
-      equal(typeof storage.getStagedOnBehalfOf(), "undefined", "initiatingOrigin is not stored"); 
+      equal(typeof storage.getStagedOnBehalfOf(), "undefined", "initiatingOrigin is not stored");
 
       start();
     }, failure("addEmail failure"));
@@ -557,7 +557,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       ok(false, "xhr failure should never succeed");
       start();
     }, function() {
-      ok(true, "xhr failure should always be a failure"); 
+      ok(true, "xhr failure should always be a failure");
       start();
     });
 
@@ -617,7 +617,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       start();
     }, function() {
       ok(storage.getStagedOnBehalfOf(), "staged on behalf of is cleared when validation completes");
-      ok(true, "xhr failure should always be a failure"); 
+      ok(true, "xhr failure should always be a failure");
       start();
     });
 
@@ -628,7 +628,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
   test("verifyEmail with a good token", function() {
     storage.setStagedOnBehalfOf(testOrigin);
     lib.verifyEmail("token", function onSuccess(info) {
-      
+
       ok(info.valid, "token was valid");
       equal(info.email, "testuser@testuser.com", "email part of info");
       equal(info.origin, testOrigin, "origin in info");
@@ -644,7 +644,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     xhr.useResult("invalid");
 
     lib.verifyEmail("token", function onSuccess(info) {
-      
+
       equal(info.valid, false, "bad token calls onSuccess with a false validity");
 
       start();
@@ -661,10 +661,10 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       ok(false, "xhr failure should never succeed");
       start();
     }, function() {
-      ok(true, "xhr failure should always be a failure"); 
+      ok(true, "xhr failure should always be a failure");
       start();
     });
-      
+
     stop();
   });
 
@@ -692,7 +692,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       var identity = lib.getStoredEmailKeypair("testemail@testemail.com");
       equal(typeof identity, "undefined", "Invalid email is not synced");
 
-      start();      
+      start();
     });
 
     stop();
@@ -704,7 +704,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       ok(false, "xhr failure should never succeed");
       start();
     }, function() {
-      ok(true, "xhr failure should always be a failure"); 
+      ok(true, "xhr failure should always be a failure");
       start();
     });
 
@@ -744,7 +744,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       ok(false, "xhr failure should never succeed");
       start();
     }, function() {
-      ok(true, "xhr failure should always be a failure"); 
+      ok(true, "xhr failure should always be a failure");
       start();
     });
 
@@ -771,7 +771,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       var identities = lib.getStoredEmailKeypairs();
       ok("testuser@testuser.com" in identities, "Our new email is added");
       equal(_.size(identities), 1, "there is one identity");
-      start(); 
+      start();
     }, failure("identity sync failure"));
 
     stop();
@@ -841,7 +841,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       ok(false, "xhr failure should never succeed");
       start();
     }, function() {
-      ok(true, "xhr failure should always be a failure"); 
+      ok(true, "xhr failure should always be a failure");
       start();
     });
 
@@ -880,7 +880,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
         start();
       });
     }, failure("getAssertion failure"));
-    
+
     stop();
   });
 
@@ -892,7 +892,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       ok(false, "xhr failure should never succeed");
       start();
     }, function() {
-      ok(true, "xhr failure should always be a failure"); 
+      ok(true, "xhr failure should always be a failure");
       start();
     });
 
@@ -927,7 +927,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
           ok(false, "xhr failure should never succeed");
           start();
         }, function() {
-          ok(true, "xhr failure should always be a failure"); 
+          ok(true, "xhr failure should always be a failure");
           start();
         });
 
@@ -954,7 +954,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
       ok(false, "xhr failure should never succeed");
       start();
     }, function() {
-      ok(true, "xhr failure should always be a failure"); 
+      ok(true, "xhr failure should always be a failure");
       start();
     });
 
@@ -1002,7 +1002,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     lib.syncEmailKeypair("testuser@testuser.com", function() {
       storage.site.set(testOrigin, "remember", false);
       storage.site.set(testOrigin, "email", "testuser@testuser.com");
-      // invalidate the email so that we force a fresh key certification with 
+      // invalidate the email so that we force a fresh key certification with
       // the server
       storage.invalidateEmail("testuser@testuser.com");
 
@@ -1023,7 +1023,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     lib.syncEmailKeypair("testuser@testuser.com", function() {
       storage.site.set(testOrigin, "remember", true);
       storage.site.set(testOrigin, "email", "testuser@testuser.com");
-      // invalidate the email so that we force a fresh key certification with 
+      // invalidate the email so that we force a fresh key certification with
       // the server
       storage.invalidateEmail("testuser@testuser.com");
 
@@ -1044,7 +1044,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/user", functio
     lib.syncEmailKeypair("testuser@testuser.com", function() {
       storage.site.set(testOrigin, "remember", true);
       storage.site.set(testOrigin, "email", "testuser@testuser.com");
-      // invalidate the email so that we force a fresh key certification with 
+      // invalidate the email so that we force a fresh key certification with
       // the server
       storage.invalidateEmail("testuser@testuser.com");
 
diff --git a/resources/static/test/qunit/resources/validation_unit_test.js b/resources/static/test/qunit/shared/validation_unit_test.js
similarity index 98%
rename from resources/static/test/qunit/resources/validation_unit_test.js
rename to resources/static/test/qunit/shared/validation_unit_test.js
index 7b7c532a331a55f06f09522d5a9d07dbc098a463..8db0dd50af046acc4eb25399dd9ccad880ea948c 100644
--- a/resources/static/test/qunit/resources/validation_unit_test.js
+++ b/resources/static/test/qunit/shared/validation_unit_test.js
@@ -34,7 +34,7 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
-steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid", function() {
+steal.then(function() {
   "use strict";
 
   var bid = BrowserID,
@@ -46,7 +46,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid", fu
     tooltipShown = true;
   }
 
-  module("resources/validation", {
+  module("shared/validation", {
     setup: function() {
       origShowTooltip = bid.Tooltip.showTooltip;
       bid.Tooltip.showTooltip = showTooltip;
@@ -57,7 +57,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid", fu
       bid.Tooltip.showTooltip = origShowTooltip;
     }
   });
-  
+
   test("email address x@y.z is valid", function() {
     ok(bid.verifyEmail("x@y.z"), "x@y.z is valid");
   });
@@ -87,7 +87,7 @@ steal.plugins("jquery", "funcunit/qunit").then("/dialog/resources/browserid", fu
   });
 
 
-  
+
   test("email with valid email", function() {
     var valid = validation.email("testuser@testuser.com");
 
diff --git a/resources/views/dialog_layout.ejs b/resources/views/dialog_layout.ejs
index a2b6ce692a36d56f5d004f41ce92ce8c40f4b6c2..b362ae940b23b6454609895f47ba989bdb28c06f 100644
--- a/resources/views/dialog_layout.ejs
+++ b/resources/views/dialog_layout.ejs
@@ -2,7 +2,7 @@
 <html>
 <head>
   <meta charset="utf-8">
-  <meta name="viewport" content="initial-scale=1.0; maximum-scale=1.0; width=device-width;">  
+  <meta name="viewport" content="initial-scale=1.0; maximum-scale=1.0; width=device-width;">
   <!--[if lt IE 9]>
     <script type="text/javascript" src="/js/html5shim.js"></script>
   <![endif]-->
@@ -46,11 +46,6 @@
       </div>
 
       <% if (useJavascript !== false) { %>
-          <script type="text/html" id="templateTooltip">
-            <div class="tooltip">
-              {{ contents }}
-            </div>
-          </script>
           <script type="text/javascript" src="/vepbundle"></script>
           <script type="text/javascript" src="steal/steal<%= production ? '.production' : '' %>.js?dialog"></script>
       <% } %>
diff --git a/resources/views/layout.ejs b/resources/views/layout.ejs
index 02906973378a8d10cf49d9790d68ecf5270f0699..1caa4fe8bd3e3a44353b44e1ab4cb901f2772090 100644
--- a/resources/views/layout.ejs
+++ b/resources/views/layout.ejs
@@ -10,32 +10,39 @@
   <script src="/vepbundle" type="text/javascript"></script>
   <% if (production) { %>
     <link rel="stylesheet" type="text/css" href="/css/browserid.min.css">
-    <script src="/js/lib.min.js" type="text/javascript"></script>
+    <script src="/pages/lib.min.js" type="text/javascript"></script>
   <% } else { %>
     <link rel="stylesheet" href="/css/style.css" type="text/css" media="screen">
     <link rel="stylesheet" href="/css/m.css" type="text/css" media="screen">
 
-    <script src="/js/jquery-1.6.2.min.js" type="text/javascript"></script>
-    <script src="/js/json2.js" type="text/javascript"></script>
-    <script src="/dialog/resources/underscore-min.js" type="text/javascript"></script>
-    <script src="/dialog/resources/browserid-extensions.js" type="text/javascript"></script>
-    <script src="/dialog/resources/browserid.js" type="text/javascript"></script>
-    <script src="/dialog/resources/error-display.js" type="text/javascript"></script>
-    <script src="/dialog/resources/error-messages.js" type="text/javascript"></script>
-    <script src="/js/page_helpers.js" type="text/javascript"></script>
-    <script src="/js/browserid.js" type="text/javascript"></script>
-    <script src="/js/pages/index.js" type="text/javascript"></script>
-    <script src="/dialog/resources/storage.js" type="text/javascript"></script>
-    <script src="/dialog/resources/network.js" type="text/javascript"></script>
-    <script src="/dialog/resources/user.js" type="text/javascript"></script>
-    <script src="/dialog/resources/tooltip.js" type="text/javascript"></script>
-    <script src="/dialog/resources/validation.js" type="text/javascript"></script>
-    <script src="/js/pages/add_email_address.js" type="text/javascript"></script>
-    <script src="/js/pages/verify_email_address.js" type="text/javascript"></script>
-    <script src="/js/pages/forgot.js" type="text/javascript"></script>
-    <script src="/js/pages/manage_account.js" type="text/javascript"></script>
-    <script src="/js/pages/signin.js" type="text/javascript"></script>
-    <script src="/js/pages/signup.js" type="text/javascript"></script>
+    <script src="/lib/jquery-1.6.2.min.js" type="text/javascript"></script>
+    <script src="/lib/json2.js" type="text/javascript"></script>
+    <script src="/lib/underscore-min.js" type="text/javascript"></script>
+    <script src="/lib/ejs.js" type="text/javascript"></script>
+    <script src="/shared/browserid-extensions.js" type="text/javascript"></script>
+    <script src="/shared/browserid.js" type="text/javascript"></script>
+    <script src="/lib/dom-jquery.js" type="text/javascript"></script>
+
+    <script src="/shared/templates.js" type="text/javascript"></script>
+    <script src="/shared/renderer.js" type="text/javascript"></script>
+    <script src="/shared/error-display.js" type="text/javascript"></script>
+    <script src="/shared/screens.js" type="text/javascript"></script>
+    <script src="/shared/error-messages.js" type="text/javascript"></script>
+    <script src="/shared/storage.js" type="text/javascript"></script>
+    <script src="/shared/network.js" type="text/javascript"></script>
+    <script src="/shared/user.js" type="text/javascript"></script>
+    <script src="/shared/tooltip.js" type="text/javascript"></script>
+    <script src="/shared/validation.js" type="text/javascript"></script>
+
+    <script src="/pages/page_helpers.js" type="text/javascript"></script>
+    <script src="/pages/browserid.js" type="text/javascript"></script>
+    <script src="/pages/index.js" type="text/javascript"></script>
+    <script src="/pages/add_email_address.js" type="text/javascript"></script>
+    <script src="/pages/verify_email_address.js" type="text/javascript"></script>
+    <script src="/pages/forgot.js" type="text/javascript"></script>
+    <script src="/pages/manage_account.js" type="text/javascript"></script>
+    <script src="/pages/signin.js" type="text/javascript"></script>
+    <script src="/pages/signup.js" type="text/javascript"></script>
   <% } %>
   <title>BrowserID: <%- title %></title>
 </head>
@@ -59,7 +66,7 @@
         </ul>
     </header>
 
-    <div id="error"></div>
+    <div id="error"><div class="contents"></div></div>
 
     <%- body %>
 
@@ -75,58 +82,5 @@
 
 </div>
 
-<script type="text/html" id="templateTooltip">
-  <div class="tooltip">
-    {{ contents }}
-  </div>
-</script>
-
-<script type="text/html" id="templateError">
-  <div>
-
-  <h2>We are very sorry, there has been an error!</h2>
-
-  <p>
-    To retry, you will have to reload the page and try again.  More info can be found by opening the expanded info below.
-  </p>
-
-  <a href="#" id="openMoreInfo">See more info</a>
-
-  <ul id="moreInfo">
-    {% if (typeof action !== "undefined") { %}
-      <li>
-        <strong id="action">Action: </strong>{{ action.title }}
-
-        {% if (action.message) { %}
-          <p>
-            {{ action.message }}
-          </p>  
-        {% } %}
-      </li>
-    {% } %}
-
-    {% if (typeof network !== "undefined") { %}
-      <li>
-
-        <strong id="network">Network Info:</strong> {{ network.type }}: {{ network.url }}
-
-        <p>
-          <strong>Response Code - </strong> {{ network.textStatus }} 
-        </p>
-
-        {% if (network.errorThrown) { %}
-          <p>
-            <strong>Error Type:</strong> {{ network.errorThrown }}
-          </p>  
-        {% } %}
-
-      </li>
-
-    {% } %}
-
-  </ul>
-  </div>
-</script>
-
 </body>
 </html>
diff --git a/resources/views/relay.ejs b/resources/views/relay.ejs
index 1fc38b014dd59ab903ef989de9604ece89eb63c2..648ae21f67d7ff0a71d0fa7f6f91802af179367d 100644
--- a/resources/views/relay.ejs
+++ b/resources/views/relay.ejs
@@ -1,21 +1,21 @@
 <!doctype html>
 <html>
 <head>
-  <meta charset="utf-8"> 
+  <meta charset="utf-8">
   <title>Browser ID</title>
 </head>
   <body>
       Relay iframe.  Woohoo!
       <% if (production) { %>
-        <script type='text/javascript' 
+        <script type='text/javascript'
           src='https://browserid.org/relay/production.js'></script>
 
       <% } else { %>
-        <script type='text/javascript' 
-          src='https://browserid.org/dialog/resources/jschannel.js'></script>
-        <script type='text/javascript' 
-          src='https://browserid.org/dialog/resources/browserid.js'></script>
-        <script type='text/javascript' 
+        <script type='text/javascript'
+          src='https://browserid.org/lib/jschannel.js'></script>
+        <script type='text/javascript'
+          src='https://browserid.org/shared/browserid.js'></script>
+        <script type='text/javascript'
           src='https://browserid.org/relay/relay.js'></script>
       <% } %>
 
diff --git a/scripts/compress.sh b/scripts/compress.sh
index 46bff6cdc77fb196ed73a3fea0bb772f1e319608..530dc9c3ff1d60efc1eba02c3fc374f55aecee6a 100755
--- a/scripts/compress.sh
+++ b/scripts/compress.sh
@@ -28,8 +28,18 @@ echo ''
 echo '****Building dialog HTML, CSS, and JS****'
 echo ''
 
+## This creates a combined templates file which is copied into
+## resources/templates.js and included into the minified bundle.
+
+cd dialog/views
+../../../../scripts/create_templates.js
+cd ../../
+cp shared/templates.js shared/templates.js.orig
+cp dialog/views/templates.js shared/templates.js
+
 steal/js dialog/scripts/build.js
 
+
 cd communication_iframe
 $UGLIFY < production.js > production.min.js
 mv production.min.js production.js
@@ -43,7 +53,7 @@ cat popup.css m.css > production.css
 $JAVA -jar $YUI_LOCATION production.css -o production.min.css
 
 cd ../../relay
-cat ../dialog/resources/jschannel.js ../dialog/resources/browserid.js relay.js > production.js
+cat ../lib/jschannel.js ../shared/browserid.js relay.js > production.js
 $UGLIFY < production.js > production.min.js
 mv production.min.js production.js
 
@@ -52,9 +62,9 @@ echo ''
 echo '****Building BrowserID.org HTML, CSS, and JS****'
 echo ''
 
-cd ../js
+cd ../pages
 # re-minimize everything together
-cat jquery-1.6.2.min.js json2.js ../dialog/resources/browserid.js ../dialog/resources/error-display.js ../dialog/resources/error-messages.js page_helpers.js browserid.js ../dialog/resources/underscore-min.js ../dialog/resources/browserid-extensions.js ../dialog/resources/storage.js ../dialog/resources/network.js ../dialog/resources/user.js ../dialog/resources/tooltip.js ../dialog/resources/validation.js pages/index.js pages/add_email_address.js pages/verify_email_address.js pages/manage_account.js pages/signin.js pages/signup.js pages/forgot.js > lib.js
+cat ../lib/jquery-1.6.2.min.js ../lib/json2.js ../lib/underscore-min.js ../lib/ejs.js ../shared/browserid-extensions.js ../shared/browserid.js ../lib/dom-jquery.js ../shared/templates.js ../shared/renderer.js ../shared/error-display.js ../shared/screens.js ../shared/error-messages.js ../shared/storage.js ../shared/network.js ../shared/user.js ../shared/tooltip.js ../shared/validation.js page_helpers.js browserid.js index.js add_email_address.js verify_email_address.js forgot.js manage_account.js signin.js signup.js > lib.js
 $UGLIFY < lib.js > lib.min.js
 
 cd ../css
diff --git a/scripts/create_templates.js b/scripts/create_templates.js
new file mode 100755
index 0000000000000000000000000000000000000000..4595d2333d13da4e7cf88c3a84af3c9b94eb71fa
--- /dev/null
+++ b/scripts/create_templates.js
@@ -0,0 +1,24 @@
+#!/usr/bin/env node
+
+const fs = require("fs");
+
+var dir = process.env.TEMPLATE_DIR || process.cwd();
+console.log(dir);
+
+var templates = {};
+
+
+fs.readdir(dir, function(err, fileNames) {
+  for(var index = 0, max = fileNames.length; index < max; index++) {
+    var fileName = fileNames[index];
+    if(fileName.match(/\.ejs$/)) {
+      var templateName = fileName.replace(/\.ejs/, '');
+      templates[templateName] = fs.readFileSync(dir + "/" + fileName, "utf8")
+    }
+  }
+
+  var templateData = "BrowserID.Templates =" + JSON.stringify(templates) + ";";
+
+  fs.writeFileSync(dir + "/templates.js", templateData, "utf8");
+});
+