merge m-c to fx-team
authorTim Taubert <tim.taubert@gmx.de>
Sat, 28 Jan 2012 09:10:04 +0100
changeset 86806 71aeeb6a2547cb8cd4735d73c90b366974258407
parent 86780 8a59519e137ec6ed458b54834c544d3f4d7262c0 (current diff)
parent 86805 a6e8ef1ef92635c2710cd529c3852588678ea579 (diff)
child 86807 447c2d1181390b52b196dda6477da750342d9718
push id805
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 18:17:35 +0000
treeherdermozilla-aurora@6fb3bf232436 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone12.0a1
merge m-c to fx-team
browser/components/thumbnails/test/browser_thumbnails_cache.js
--- a/accessible/tests/mochitest/events/test_scroll.xul
+++ b/accessible/tests/mochitest/events/test_scroll.xul
@@ -10,16 +10,18 @@
 
 <?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
                  type="text/css"?>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
   <script type="application/javascript"
           src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+  <script type="application/javascript"
+          src="chrome://browser/content/utilityOverlay.js"/>
 
   <script type="application/javascript"
           src="../common.js" />
   <script type="application/javascript"
           src="../role.js" />
   <script type="application/javascript"
           src="../states.js" />
   <script type="application/javascript"
--- a/accessible/tests/mochitest/name/nsRootAcc_wnd.xul
+++ b/accessible/tests/mochitest/name/nsRootAcc_wnd.xul
@@ -5,16 +5,19 @@
 <?xml-stylesheet href="chrome://browser/content/browser.css"
                  type="text/css"?>
 <!-- SeaMonkey tabbrowser -->
 <?xml-stylesheet href="chrome://navigator/content/navigator.css"
                  type="text/css"?>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
+  <script type="application/javascript"
+          src="chrome://browser/content/utilityOverlay.js"/>
+
   <script type="application/javascript">
   <![CDATA[
     var gOpenerWnd = window.opener.wrappedJSObject;
 
     function ok(aCond, aMsg) {
       gOpenerWnd.SimpleTest.ok(aCond, aMsg);
     }
 
--- a/accessible/tests/mochitest/relations/test_tabbrowser.xul
+++ b/accessible/tests/mochitest/relations/test_tabbrowser.xul
@@ -10,16 +10,18 @@
 <?xml-stylesheet href="chrome://navigator/content/navigator.css"
                  type="text/css"?>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         title="Accessible XUL tabbrowser relation tests">
 
   <script type="application/javascript"
           src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+  <script type="application/javascript"
+          src="chrome://browser/content/utilityOverlay.js"/>
 
   <script type="application/javascript"
           src="../common.js" />
   <script type="application/javascript"
           src="../role.js" />
   <script type="application/javascript"
           src="../relations.js" />
   <script type="application/javascript"
--- a/accessible/tests/mochitest/tree/test_tabbrowser.xul
+++ b/accessible/tests/mochitest/tree/test_tabbrowser.xul
@@ -11,16 +11,18 @@
 <?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
                  type="text/css"?>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         title="Accessible XUL tabbrowser hierarchy tests">
 
   <script type="application/javascript"
           src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+  <script type="application/javascript"
+          src="chrome://browser/content/utilityOverlay.js"/>
 
   <script type="application/javascript"
           src="../common.js" />
   <script type="application/javascript"
           src="../role.js" />
   <script type="application/javascript"
           src="../events.js" />
 
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1105,10 +1105,16 @@ pref("devtools.editor.component", "orion
 // preference is a string so that localizers can alter it.
 pref("browser.menu.showCharacterEncoding", "chrome://browser/locale/browser.properties");
 
 // Allow using tab-modal prompts when possible.
 pref("prompts.tab_modal.enabled", true);
 // Whether the Panorama should animate going in/out of tabs
 pref("browser.panorama.animate_zoom", true);
 
+// Defines the url to be used for new tabs.
+pref("browser.newtab.url", "about:blank");
+
+// Toggles the content of 'about:newtab'. Shows the grid when enabled.
+pref("browser.newtabpage.enabled", false);
+
 // Enable the DOM full-screen API.
 pref("full-screen-api.enabled", true);
--- a/browser/base/content/browser-fullZoom.js
+++ b/browser/base/content/browser-fullZoom.js
@@ -220,17 +220,17 @@ var FullZoom = {
    * @param aBrowser
    *        (optional) browser object displaying the document
    */
   onLocationChange: function FullZoom_onLocationChange(aURI, aIsTabSwitch, aBrowser) {
     if (!aURI || (aIsTabSwitch && !this.siteSpecific))
       return;
 
     // Avoid the cps roundtrip and apply the default/global pref.
-    if (aURI.spec == "about:blank") {
+    if (isBlankPageURL(aURI.spec)) {
       this._applyPrefToSetting(undefined, aBrowser);
       return;
     }
 
     let browser = aBrowser || gBrowser.selectedBrowser;
 
     // Media documents should always start at 1, and are not affected by prefs.
     if (!aIsTabSwitch && browser.contentDocument.mozSyntheticDocument) {
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -1007,17 +1007,17 @@ var PlacesStarButton = {
     this._itemIds = [];
 
     if (this._pendingStmt) {
       this._pendingStmt.cancel();
       delete this._pendingStmt;
     }
 
     // We can load about:blank before the actual page, but there is no point in handling that page.
-    if (this._uri.spec == "about:blank") {
+    if (isBlankPageURL(this._uri.spec)) {
       return;
     }
 
     this._pendingStmt = PlacesUtils.asyncGetBookmarkIds(this._uri, function (aItemIds, aURI) {
       // Safety check that the bookmarked URI equals the tracked one.
       if (!aURI.equals(this._uri)) {
         Components.utils.reportError("PlacesStarButton did not receive current URI");
         return;
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -183,16 +183,17 @@ XPCOMUtils.defineLazyGetter(this, "Inspe
 XPCOMUtils.defineLazyGetter(this, "Tilt", function() {
   let tmp = {};
   Cu.import("resource:///modules/devtools/Tilt.jsm", tmp);
   return new tmp.Tilt(window);
 });
 
 let gInitialPages = [
   "about:blank",
+  "about:newtab",
   "about:privatebrowsing",
   "about:sessionrestore"
 ];
 
 #include browser-fullZoom.js
 #include browser-places.js
 #include browser-tabPreviews.js
 #include browser-tabview.js
@@ -2199,17 +2200,17 @@ function openLocation() {
     if (win) {
       // If there's an open browser window, it should handle this command
       win.focus()
       win.openLocation();
     }
     else {
       // If there are no open browser windows, open a new one
       win = window.openDialog("chrome://browser/content/", "_blank",
-                              "chrome,all,dialog=no", "about:blank");
+                              "chrome,all,dialog=no", BROWSER_NEW_TAB_URL);
       win.addEventListener("load", openLocationCallback, false);
     }
     return;
   }
 #endif
   openDialog("chrome://browser/content/openLocation.xul", "_blank",
              "chrome,modal,titlebar", window);
 }
@@ -2217,17 +2218,17 @@ function openLocation() {
 function openLocationCallback()
 {
   // make sure the DOM is ready
   setTimeout(function() { this.openLocation(); }, 0);
 }
 
 function BrowserOpenTab()
 {
-  openUILinkIn("about:blank", "tab");
+  openUILinkIn(BROWSER_NEW_TAB_URL, "tab");
 }
 
 /* Called from the openLocation dialog. This allows that dialog to instruct
    its opener to open a new window and then step completely out of the way.
    Anything less byzantine is causing horrible crashes, rather believably,
    though oddly only on Linux. */
 function delayedOpenWindow(chrome, flags, href, postData)
 {
@@ -2552,17 +2553,17 @@ function URLBarSetURI(aURI) {
 
     // Replace initial page URIs with an empty string
     // only if there's no opener (bug 370555).
     if (gInitialPages.indexOf(uri.spec) != -1)
       value = content.opener ? uri.spec : "";
     else
       value = losslessDecodeURI(uri);
 
-    valid = (uri.spec != "about:blank");
+    valid = !isBlankPageURL(uri.spec);
   }
 
   gURLBar.value = value;
   gURLBar.valueIsTyped = !valid;
   SetPageProxyState(valid ? "valid" : "invalid");
 }
 
 function losslessDecodeURI(aURI) {
@@ -2869,17 +2870,17 @@ function BrowserOnClick(event) {
  * and is presented with about:blocked.  The "Get me out of here!"
  * button should take the user to the default start page so that even
  * when their own homepage is infected, we can get them somewhere safe.
  */
 function getMeOutOfHere() {
   // Get the start page from the *default* pref branch, not the user's
   var prefs = Cc["@mozilla.org/preferences-service;1"]
              .getService(Ci.nsIPrefService).getDefaultBranch(null);
-  var url = "about:blank";
+  var url = BROWSER_NEW_TAB_URL;
   try {
     url = prefs.getComplexValue("browser.startup.homepage",
                                 Ci.nsIPrefLocalizedString).data;
     // If url is a pipe-delimited set of pages, just take the first one.
     if (url.indexOf("|") != -1)
       url = url.split("|")[0];
   } catch(e) {
     Components.utils.reportError("Couldn't get homepage pref: " + e);
@@ -5187,17 +5188,17 @@ nsBrowserAccess.prototype = {
         aWhere = gPrefService.getIntPref("browser.link.open_newwindow.override.external");
       else
         aWhere = gPrefService.getIntPref("browser.link.open_newwindow");
     }
     switch (aWhere) {
       case Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW :
         // FIXME: Bug 408379. So how come this doesn't send the
         // referrer like the other loads do?
-        var url = aURI ? aURI.spec : "about:blank";
+        var url = aURI ? aURI.spec : BROWSER_NEW_TAB_URL;
         // Pass all params to openDialog to ensure that "url" isn't passed through
         // loadOneOrMoreURIs, which splits based on "|"
         newWindow = openDialog(getBrowserURL(), "_blank", "all,dialog=no", url, null, null, null);
         break;
       case Ci.nsIBrowserDOMWindow.OPEN_NEWTAB :
         let win, needToFocusWin;
 
         // try the current window.  if we're in a popup, fall back on the most recent browser window
@@ -5220,17 +5221,17 @@ nsBrowserAccess.prototype = {
           win.focus();
           newWindow = win.content;
           break;
         }
 
         let loadInBackground = gPrefService.getBoolPref("browser.tabs.loadDivertedInBackground");
         let referrer = aOpener ? makeURI(aOpener.location.href) : null;
 
-        let tab = win.gBrowser.loadOneTab(aURI ? aURI.spec : "about:blank", {
+        let tab = win.gBrowser.loadOneTab(aURI ? aURI.spec : BROWSER_NEW_TAB_URL, {
                                           referrerURI: referrer,
                                           fromExternal: isExternal,
                                           inBackground: loadInBackground});
         let browser = win.gBrowser.getBrowserForTab(tab);
 
         newWindow = browser.contentWindow;
         if (needToFocusWin || (!loadInBackground && isExternal))
           newWindow.focus();
@@ -7769,19 +7770,21 @@ function undoCloseWindow(aIndex) {
 }
 
 /*
  * Determines if a tab is "empty", usually used in the context of determining
  * if it's ok to close the tab.
  */
 function isTabEmpty(aTab) {
   let browser = aTab.linkedBrowser;
+  let uri = browser.currentURI.spec;
+  let body = browser.contentDocument.body;
   return browser.sessionHistory.count < 2 &&
-         browser.currentURI.spec == "about:blank" &&
-         !browser.contentDocument.body.hasChildNodes() &&
+         isBlankPageURL(uri) &&
+         (!body || !body.hasChildNodes()) &&
          !aTab.hasAttribute("busy");
 }
 
 #ifdef MOZ_SERVICES_SYNC
 function BrowserOpenSyncTabs() {
   switchToTabHavingURI("about:sync-tabs", true);
 }
 #endif
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -386,17 +386,17 @@
     </panel>
 
     <!-- Bookmarks and history tooltip -->
     <tooltip id="bhTooltip"/>
 
     <panel id="customizeToolbarSheetPopup"
            noautohide="true">
       <iframe id="customizeToolbarSheetIFrame"
-              style="&dialog.style;"
+              style="&dialog.dimensions;"
               hidden="true"/>
     </panel>
 
     <tooltip id="tabbrowser-tab-tooltip" onpopupshowing="gBrowser.createTooltip(event);"/>
 
     <tooltip id="back-button-tooltip">
       <label class="tooltip-label" value="&backButton.tooltip;"/>
 #ifdef XP_MACOSX
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/batch.js
@@ -0,0 +1,76 @@
+#ifdef 0
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#endif
+
+/**
+ * This class makes it easy to wait until a batch of callbacks has finished.
+ *
+ * Example:
+ *
+ * let batch = new Batch(function () alert("finished"));
+ * let pop = batch.pop.bind(batch);
+ *
+ * for (let i = 0; i < 5; i++) {
+ *   batch.push();
+ *   setTimeout(pop, i * 1000);
+ * }
+ *
+ * batch.close();
+ */
+function Batch(aCallback) {
+  this._callback = aCallback;
+}
+
+Batch.prototype = {
+  /**
+   * The number of batch entries.
+   */
+  _count: 0,
+
+  /**
+   * Whether this batch is closed.
+   */
+  _closed: false,
+
+  /**
+   * Increases the number of batch entries by one.
+   */
+  push: function Batch_push() {
+    if (!this._closed)
+      this._count++;
+  },
+
+  /**
+   * Decreases the number of batch entries by one.
+   */
+  pop: function Batch_pop() {
+    if (this._count)
+      this._count--;
+
+    if (this._closed)
+      this._check();
+  },
+
+  /**
+   * Closes the batch so that no new entries can be added.
+   */
+  close: function Batch_close() {
+    if (this._closed)
+      return;
+
+    this._closed = true;
+    this._check();
+  },
+
+  /**
+   * Checks if the batch has finished.
+   */
+  _check: function Batch_check() {
+    if (this._count == 0 && this._callback) {
+      this._callback();
+      this._callback = null;
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/cells.js
@@ -0,0 +1,137 @@
+#ifdef 0
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#endif
+
+/**
+ * This class manages a cell's DOM node (not the actually cell content, a site).
+ * It's mostly read-only, i.e. all manipulation of both position and content
+ * aren't handled here.
+ */
+function Cell(aGrid, aNode) {
+  this._grid = aGrid;
+  this._node = aNode;
+  this._node._newtabCell = this;
+
+  // Register drag-and-drop event handlers.
+  ["DragEnter", "DragOver", "DragExit", "Drop"].forEach(function (aType) {
+    let method = "on" + aType;
+    this[method] = this[method].bind(this);
+    this._node.addEventListener(aType.toLowerCase(), this[method], false);
+  }, this);
+}
+
+Cell.prototype = {
+  /**
+   *
+   */
+  _grid: null,
+
+  /**
+   * The cell's DOM node.
+   */
+  get node() this._node,
+
+  /**
+   * The cell's offset in the grid.
+   */
+  get index() {
+    let index = this._grid.cells.indexOf(this);
+
+    // Cache this value, overwrite the getter.
+    Object.defineProperty(this, "index", {value: index, enumerable: true});
+
+    return index;
+  },
+
+  /**
+   * The previous cell in the grid.
+   */
+  get previousSibling() {
+    let prev = this.node.previousElementSibling;
+    prev = prev && prev._newtabCell;
+
+    // Cache this value, overwrite the getter.
+    Object.defineProperty(this, "previousSibling", {value: prev, enumerable: true});
+
+    return prev;
+  },
+
+  /**
+   * The next cell in the grid.
+   */
+  get nextSibling() {
+    let next = this.node.nextElementSibling;
+    next = next && next._newtabCell;
+
+    // Cache this value, overwrite the getter.
+    Object.defineProperty(this, "nextSibling", {value: next, enumerable: true});
+
+    return next;
+  },
+
+  /**
+   * The site contained in the cell, if any.
+   */
+  get site() {
+    let firstChild = this.node.firstElementChild;
+    return firstChild && firstChild._newtabSite;
+  },
+
+  /**
+   * Checks whether the cell contains a pinned site.
+   * @return Whether the cell contains a pinned site.
+   */
+  containsPinnedSite: function Cell_containsPinnedSite() {
+    let site = this.site;
+    return site && site.isPinned();
+  },
+
+  /**
+   * Checks whether the cell contains a site (is empty).
+   * @return Whether the cell is empty.
+   */
+  isEmpty: function Cell_isEmpty() {
+    return !this.site;
+  },
+
+  /**
+   * Event handler for the 'dragenter' event.
+   * @param aEvent The dragenter event.
+   */
+  onDragEnter: function Cell_onDragEnter(aEvent) {
+    if (gDrag.isValid(aEvent)) {
+      aEvent.preventDefault();
+      gDrop.enter(this, aEvent);
+    }
+  },
+
+  /**
+   * Event handler for the 'dragover' event.
+   * @param aEvent The dragover event.
+   */
+  onDragOver: function Cell_onDragOver(aEvent) {
+    if (gDrag.isValid(aEvent))
+      aEvent.preventDefault();
+  },
+
+  /**
+   * Event handler for the 'dragexit' event.
+   * @param aEvent The dragexit event.
+   */
+  onDragExit: function Cell_onDragExit(aEvent) {
+    gDrop.exit(this, aEvent);
+  },
+
+  /**
+   * Event handler for the 'drop' event.
+   * @param aEvent The drop event.
+   */
+  onDrop: function Cell_onDrop(aEvent) {
+    if (gDrag.isValid(aEvent)) {
+      aEvent.preventDefault();
+      gDrop.drop(this, aEvent);
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/drag.js
@@ -0,0 +1,140 @@
+#ifdef 0
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#endif
+
+/**
+ * This singleton implements site dragging functionality.
+ */
+let gDrag = {
+  /**
+   * The site offset to the drag start point.
+   */
+  _offsetX: null,
+  _offsetY: null,
+
+  /**
+   * The site that is dragged.
+   */
+  _draggedSite: null,
+  get draggedSite() this._draggedSite,
+
+  /**
+   * The cell width/height at the point the drag started.
+   */
+  _cellWidth: null,
+  _cellHeight: null,
+  get cellWidth() this._cellWidth,
+  get cellHeight() this._cellHeight,
+
+  /**
+   * Start a new drag operation.
+   * @param aSite The site that's being dragged.
+   * @param aEvent The 'dragstart' event.
+   */
+  start: function Drag_start(aSite, aEvent) {
+    this._draggedSite = aSite;
+
+    // Prevent moz-transform for left, top.
+    aSite.node.setAttribute("dragged", "true");
+
+    // Make sure the dragged site is floating above the grid.
+    aSite.node.setAttribute("ontop", "true");
+
+    this._setDragData(aSite, aEvent);
+
+    // Store the cursor offset.
+    let node = aSite.node;
+    let rect = node.getBoundingClientRect();
+    this._offsetX = aEvent.clientX - rect.left;
+    this._offsetY = aEvent.clientY - rect.top;
+
+    // Store the cell dimensions.
+    let cellNode = aSite.cell.node;
+    this._cellWidth = cellNode.offsetWidth;
+    this._cellHeight = cellNode.offsetHeight;
+
+    gTransformation.freezeSitePosition(aSite);
+  },
+
+  /**
+   * Handles the 'drag' event.
+   * @param aSite The site that's being dragged.
+   * @param aEvent The 'drag' event.
+   */
+  drag: function Drag_drag(aSite, aEvent) {
+    // Get the viewport size.
+    let {clientWidth, clientHeight} = document.documentElement;
+
+    // We'll want a padding of 5px.
+    let border = 5;
+
+    // Enforce minimum constraints to keep the drag image inside the window.
+    let left = Math.max(scrollX + aEvent.clientX - this._offsetX, border);
+    let top = Math.max(scrollY + aEvent.clientY - this._offsetY, border);
+
+    // Enforce maximum constraints to keep the drag image inside the window.
+    left = Math.min(left, scrollX + clientWidth - this.cellWidth - border);
+    top = Math.min(top, scrollY + clientHeight - this.cellHeight - border);
+
+    // Update the drag image's position.
+    gTransformation.setSitePosition(aSite, {left: left, top: top});
+  },
+
+  /**
+   * Ends the current drag operation.
+   * @param aSite The site that's being dragged.
+   * @param aEvent The 'dragend' event.
+   */
+  end: function Drag_end(aSite, aEvent) {
+    aSite.node.removeAttribute("dragged");
+
+    // Slide the dragged site back into its cell (may be the old or the new cell).
+    gTransformation.slideSiteTo(aSite, aSite.cell, {
+      unfreeze: true,
+      callback: function () aSite.node.removeAttribute("ontop")
+    });
+
+    this._draggedSite = null;
+  },
+
+  /**
+   * Checks whether we're responsible for a given drag event.
+   * @param aEvent The drag event to check.
+   * @return Whether we should handle this drag and drop operation.
+   */
+  isValid: function Drag_isValid(aEvent) {
+    let dt = aEvent.dataTransfer;
+    return dt && dt.types.contains("text/x-moz-url");
+  },
+
+  /**
+   * Initializes the drag data for the current drag operation.
+   * @param aSite The site that's being dragged.
+   * @param aEvent The 'dragstart' event.
+   */
+  _setDragData: function Drag_setDragData(aSite, aEvent) {
+    let {url, title} = aSite;
+
+    let dt = aEvent.dataTransfer;
+    dt.mozCursor = "default";
+    dt.effectAllowed = "move";
+    dt.setData("text/plain", url);
+    dt.setData("text/uri-list", url);
+    dt.setData("text/x-moz-url", url + "\n" + title);
+    dt.setData("text/html", "<a href=\"" + url + "\">" + url + "</a>");
+
+    // Create and use an empty drag element. We don't want to use the default
+    // drag image with its default opacity.
+    let dragElement = document.createElementNS(HTML_NAMESPACE, "div");
+    dragElement.classList.add("drag-element");
+    let body = document.getElementById("body");
+    body.appendChild(dragElement);
+    dt.setDragImage(dragElement, 0, 0);
+
+    // After the 'dragstart' event has been processed we can remove the
+    // temporary drag element from the DOM.
+    setTimeout(function () body.removeChild(dragElement), 0);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/drop.js
@@ -0,0 +1,147 @@
+#ifdef 0
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#endif
+
+// A little delay that prevents the grid from being too sensitive when dragging
+// sites around.
+const DELAY_REARRANGE_MS = 100;
+
+/**
+ * This singleton implements site dropping functionality.
+ */
+let gDrop = {
+  /**
+   * The last drop target.
+   */
+  _lastDropTarget: null,
+
+  /**
+   * Handles the 'dragenter' event.
+   * @param aCell The drop target cell.
+   */
+  enter: function Drop_enter(aCell) {
+    this._delayedRearrange(aCell);
+  },
+
+  /**
+   * Handles the 'dragexit' event.
+   * @param aCell The drop target cell.
+   * @param aEvent The 'dragexit' event.
+   */
+  exit: function Drop_exit(aCell, aEvent) {
+    if (aEvent.dataTransfer && !aEvent.dataTransfer.mozUserCancelled) {
+      this._delayedRearrange();
+    } else {
+      // The drag operation has been cancelled.
+      this._cancelDelayedArrange();
+      this._rearrange();
+    }
+  },
+
+  /**
+   * Handles the 'drop' event.
+   * @param aCell The drop target cell.
+   * @param aEvent The 'dragexit' event.
+   * @param aCallback The callback to call when the drop is finished.
+   */
+  drop: function Drop_drop(aCell, aEvent, aCallback) {
+    // The cell that is the drop target could contain a pinned site. We need
+    // to find out where that site has gone and re-pin it there.
+    if (aCell.containsPinnedSite())
+      this._repinSitesAfterDrop(aCell);
+
+    // Pin the dragged or insert the new site.
+    this._pinDraggedSite(aCell, aEvent);
+
+    this._cancelDelayedArrange();
+
+    // Update the grid and move all sites to their new places.
+    gUpdater.updateGrid(aCallback);
+  },
+
+  /**
+   * Re-pins all pinned sites in their (new) positions.
+   * @param aCell The drop target cell.
+   */
+  _repinSitesAfterDrop: function Drop_repinSitesAfterDrop(aCell) {
+    let sites = gDropPreview.rearrange(aCell);
+
+    // Filter out pinned sites.
+    let pinnedSites = sites.filter(function (aSite) {
+      return aSite && aSite.isPinned();
+    });
+
+    // Re-pin all shifted pinned cells.
+    pinnedSites.forEach(function (aSite) aSite.pin(sites.indexOf(aSite)), this);
+  },
+
+  /**
+   * Pins the dragged site in its new place.
+   * @param aCell The drop target cell.
+   * @param aEvent The 'dragexit' event.
+   */
+  _pinDraggedSite: function Drop_pinDraggedSite(aCell, aEvent) {
+    let index = aCell.index;
+    let draggedSite = gDrag.draggedSite;
+
+    if (draggedSite) {
+      // Pin the dragged site at its new place.
+      if (aCell != draggedSite.cell)
+        draggedSite.pin(index);
+    } else {
+      // A new link was dragged onto the grid. Create it by pinning its URL.
+      let dt = aEvent.dataTransfer;
+      let [url, title] = dt.getData("text/x-moz-url").split(/[\r\n]+/);
+      gPinnedLinks.pin({url: url, title: title}, index);
+    }
+  },
+
+  /**
+   * Time a rearrange with a little delay.
+   * @param aCell The drop target cell.
+   */
+  _delayedRearrange: function Drop_delayedRearrange(aCell) {
+    // The last drop target didn't change so there's no need to re-arrange.
+    if (this._lastDropTarget == aCell)
+      return;
+
+    let self = this;
+
+    function callback() {
+      self._rearrangeTimeout = null;
+      self._rearrange(aCell);
+    }
+
+    this._cancelDelayedArrange();
+    this._rearrangeTimeout = setTimeout(callback, DELAY_REARRANGE_MS);
+
+    // Store the last drop target.
+    this._lastDropTarget = aCell;
+  },
+
+  /**
+   * Cancels a timed rearrange, if any.
+   */
+  _cancelDelayedArrange: function Drop_cancelDelayedArrange() {
+    if (this._rearrangeTimeout) {
+      clearTimeout(this._rearrangeTimeout);
+      this._rearrangeTimeout = null;
+    }
+  },
+
+  /**
+   * Rearrange all sites in the grid depending on the current drop target.
+   * @param aCell The drop target cell.
+   */
+  _rearrange: function Drop_rearrange(aCell) {
+    let sites = gGrid.sites;
+
+    // We need to rearrange the grid only if there's a current drop target.
+    if (aCell)
+      sites = gDropPreview.rearrange(aCell);
+
+    gTransformation.rearrangeSites(sites, {unfreeze: !aCell});
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/dropPreview.js
@@ -0,0 +1,222 @@
+#ifdef 0
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#endif
+
+/**
+ * This singleton provides the ability to re-arrange the current grid to
+ * indicate the transformation that results from dropping a cell at a certain
+ * position.
+ */
+let gDropPreview = {
+  /**
+   * Rearranges the sites currently contained in the grid when a site would be
+   * dropped onto the given cell.
+   * @param aCell The drop target cell.
+   * @return The re-arranged array of sites.
+   */
+  rearrange: function DropPreview_rearrange(aCell) {
+    let sites = gGrid.sites;
+
+    // Insert the dragged site into the current grid.
+    this._insertDraggedSite(sites, aCell);
+
+    // After the new site has been inserted we need to correct the positions
+    // of all pinned tabs that have been moved around.
+    this._repositionPinnedSites(sites, aCell);
+
+    return sites;
+  },
+
+  /**
+   * Inserts the currently dragged site into the given array of sites.
+   * @param aSites The array of sites to insert into.
+   * @param aCell The drop target cell.
+   */
+  _insertDraggedSite: function DropPreview_insertDraggedSite(aSites, aCell) {
+    let dropIndex = aCell.index;
+    let draggedSite = gDrag.draggedSite;
+
+    // We're currently dragging a site.
+    if (draggedSite) {
+      let dragCell = draggedSite.cell;
+      let dragIndex = dragCell.index;
+
+      // Move the dragged site into its new position.
+      if (dragIndex != dropIndex) {
+        aSites.splice(dragIndex, 1);
+        aSites.splice(dropIndex, 0, draggedSite);
+      }
+    // We're handling an external drag item.
+    } else {
+      aSites.splice(dropIndex, 0, null);
+    }
+  },
+
+  /**
+   * Correct the position of all pinned sites that might have been moved to
+   * different positions after the dragged site has been inserted.
+   * @param aSites The array of sites containing the dragged site.
+   * @param aCell The drop target cell.
+   */
+  _repositionPinnedSites:
+    function DropPreview_repositionPinnedSites(aSites, aCell) {
+
+    // Collect all pinned sites.
+    let pinnedSites = this._filterPinnedSites(aSites, aCell);
+
+    // Correct pinned site positions.
+    pinnedSites.forEach(function (aSite) {
+      aSites[aSites.indexOf(aSite)] = aSites[aSite.cell.index];
+      aSites[aSite.cell.index] = aSite;
+    }, this);
+
+    // There might be a pinned cell that got pushed out of the grid, try to
+    // sneak it in by removing a lower-priority cell.
+    if (this._hasOverflowedPinnedSite(aSites, aCell))
+      this._repositionOverflowedPinnedSite(aSites, aCell);
+  },
+
+  /**
+   * Filter pinned sites out of the grid that are still on their old positions
+   * and have not moved.
+   * @param aSites The array of sites to filter.
+   * @param aCell The drop target cell.
+   * @return The filtered array of sites.
+   */
+  _filterPinnedSites: function DropPreview_filterPinnedSites(aSites, aCell) {
+    let draggedSite = gDrag.draggedSite;
+
+    // When dropping on a cell that contains a pinned site make sure that all
+    // pinned cells surrounding the drop target are moved as well.
+    let range = this._getPinnedRange(aCell);
+
+    return aSites.filter(function (aSite, aIndex) {
+      // The site must be valid, pinned and not the dragged site.
+      if (!aSite || aSite == draggedSite || !aSite.isPinned())
+        return false;
+
+      let index = aSite.cell.index;
+
+      // If it's not in the 'pinned range' it's a valid pinned site.
+      return (index > range.end || index < range.start);
+    });
+  },
+
+  /**
+   * Determines the range of pinned sites surrounding the drop target cell.
+   * @param aCell The drop target cell.
+   * @return The range of pinned cells.
+   */
+  _getPinnedRange: function DropPreview_getPinnedRange(aCell) {
+    let dropIndex = aCell.index;
+    let range = {start: dropIndex, end: dropIndex};
+
+    // We need a pinned range only when dropping on a pinned site.
+    if (aCell.containsPinnedSite()) {
+      let links = gPinnedLinks.links;
+
+      // Find all previous siblings of the drop target that are pinned as well.
+      while (range.start && links[range.start - 1])
+        range.start--;
+
+      let maxEnd = links.length - 1;
+
+      // Find all next siblings of the drop target that are pinned as well.
+      while (range.end < maxEnd && links[range.end + 1])
+        range.end++;
+    }
+
+    return range;
+  },
+
+  /**
+   * Checks if the given array of sites contains a pinned site that has
+   * been pushed out of the grid.
+   * @param aSites The array of sites to check.
+   * @param aCell The drop target cell.
+   * @return Whether there is an overflowed pinned cell.
+   */
+  _hasOverflowedPinnedSite:
+    function DropPreview_hasOverflowedPinnedSite(aSites, aCell) {
+
+    // If the drop target isn't pinned there's no way a pinned site has been
+    // pushed out of the grid so we can just exit here.
+    if (!aCell.containsPinnedSite())
+      return false;
+
+    let cells = gGrid.cells;
+
+    // No cells have been pushed out of the grid, nothing to do here.
+    if (aSites.length <= cells.length)
+      return false;
+
+    let overflowedSite = aSites[cells.length];
+
+    // Nothing to do if the site that got pushed out of the grid is not pinned.
+    return (overflowedSite && overflowedSite.isPinned());
+  },
+
+  /**
+   * We have a overflowed pinned site that we need to re-position so that it's
+   * visible again. We try to find a lower-priority cell (empty or containing
+   * an unpinned site) that we can move it to.
+   * @param aSites The array of sites.
+   * @param aCell The drop target cell.
+   */
+  _repositionOverflowedPinnedSite:
+    function DropPreview_repositionOverflowedPinnedSite(aSites, aCell) {
+
+    // Try to find a lower-priority cell (empty or containing an unpinned site).
+    let index = this._indexOfLowerPrioritySite(aSites, aCell);
+
+    if (index > -1) {
+      let cells = gGrid.cells;
+      let dropIndex = aCell.index;
+
+      // Move all pinned cells to their new positions to let the overflowed
+      // site fit into the grid.
+      for (let i = index + 1, lastPosition = index; i < aSites.length; i++) {
+        if (i != dropIndex) {
+          aSites[lastPosition] = aSites[i];
+          lastPosition = i;
+        }
+      }
+
+      // Finally, remove the overflowed site from its previous position.
+      aSites.splice(cells.length, 1);
+    }
+  },
+
+  /**
+   * Finds the index of the last cell that is empty or contains an unpinned
+   * site. These are considered to be of a lower priority.
+   * @param aSites The array of sites.
+   * @param aCell The drop target cell.
+   * @return The cell's index.
+   */
+  _indexOfLowerPrioritySite:
+    function DropPreview_indexOfLowerPrioritySite(aSites, aCell) {
+
+    let cells = gGrid.cells;
+    let dropIndex = aCell.index;
+
+    // Search (beginning with the last site in the grid) for a site that is
+    // empty or unpinned (an thus lower-priority) and can be pushed out of the
+    // grid instead of the pinned site.
+    for (let i = cells.length - 1; i >= 0; i--) {
+      // The cell that is our drop target is not a good choice.
+      if (i == dropIndex)
+        continue;
+
+      let site = aSites[i];
+
+      // We can use the cell only if it's empty or the site is un-pinned.
+      if (!site || !site.isPinned())
+        return i;
+    }
+
+    return -1;
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/dropTargetShim.js
@@ -0,0 +1,178 @@
+#ifdef 0
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#endif
+
+/**
+ * This singleton provides a custom drop target detection. We need this because
+ * the default DnD target detection relies on the cursor's position. We want
+ * to pick a drop target based on the dragged site's position.
+ */
+let gDropTargetShim = {
+  /**
+   * Cache for the position of all cells, cleaned after drag finished.
+   */
+  _cellPositions: null,
+
+  /**
+   * The last drop target that was hovered.
+   */
+  _lastDropTarget: null,
+
+  /**
+   * Initializes the drop target shim.
+   */
+  init: function DropTargetShim_init() {
+    let node = gGrid.node;
+
+    this._dragover = this._dragover.bind(this);
+
+    // Add drag event handlers.
+    node.addEventListener("dragstart", this._start.bind(this), true);
+    // XXX bug 505521 - Don't listen for drag, it's useless at the moment.
+    //node.addEventListener("drag", this._drag.bind(this), false);
+    node.addEventListener("dragend", this._end.bind(this), true);
+  },
+
+  /**
+   * Handles the 'dragstart' event.
+   * @param aEvent The 'dragstart' event.
+   */
+  _start: function DropTargetShim_start(aEvent) {
+    gGrid.lock();
+
+    // XXX bug 505521 - Listen for dragover on the document.
+    document.documentElement.addEventListener("dragover", this._dragover, false);
+  },
+
+  /**
+   * Handles the 'drag' event and determines the current drop target.
+   * @param aEvent The 'drag' event.
+   */
+  _drag: function DropTargetShim_drag(aEvent) {
+    // Let's see if we find a drop target.
+    let target = this._findDropTarget(aEvent);
+
+    if (target == this._lastDropTarget) {
+      // XXX bug 505521 - Don't fire dragover for now (causes recursion).
+      /*if (target)
+        // The last drop target is valid and didn't change.
+        this._dispatchEvent(aEvent, "dragover", target);*/
+    } else {
+      if (this._lastDropTarget)
+        // We left the last drop target.
+        this._dispatchEvent(aEvent, "dragexit", this._lastDropTarget);
+
+      if (target)
+        // We're now hovering a (new) drop target.
+        this._dispatchEvent(aEvent, "dragenter", target);
+
+      if (this._lastDropTarget)
+        // We left the last drop target.
+        this._dispatchEvent(aEvent, "dragleave", this._lastDropTarget);
+
+      this._lastDropTarget = target;
+    }
+  },
+
+  /**
+   * Handles the 'dragover' event as long as bug 505521 isn't fixed to get
+   * current mouse cursor coordinates while dragging.
+   * @param aEvent The 'dragover' event.
+   */
+  _dragover: function DropTargetShim_dragover(aEvent) {
+    let sourceNode = aEvent.dataTransfer.mozSourceNode;
+    gDrag.drag(sourceNode._newtabSite, aEvent);
+
+    this._drag(aEvent);
+  },
+
+  /**
+   * Handles the 'dragend' event.
+   * @param aEvent The 'dragend' event.
+   */
+  _end: function DropTargetShim_end(aEvent) {
+    // Make sure to determine the current drop target in case the dragenter
+    // event hasn't been fired.
+    this._drag(aEvent);
+
+    if (this._lastDropTarget) {
+      if (aEvent.dataTransfer.mozUserCancelled) {
+        // The drag operation was cancelled.
+        this._dispatchEvent(aEvent, "dragexit", this._lastDropTarget);
+        this._dispatchEvent(aEvent, "dragleave", this._lastDropTarget);
+      } else {
+        // A site was successfully dropped.
+        this._dispatchEvent(aEvent, "drop", this._lastDropTarget);
+      }
+
+      // Clean up.
+      this._lastDropTarget = null;
+      this._cellPositions = null;
+    }
+
+    gGrid.unlock();
+
+    // XXX bug 505521 - Remove the document's dragover listener.
+    document.documentElement.removeEventListener("dragover", this._dragover, false);
+  },
+
+  /**
+   * Determines the current drop target by matching the dragged site's position
+   * against all cells in the grid.
+   * @return The currently hovered drop target or null.
+   */
+  _findDropTarget: function DropTargetShim_findDropTarget() {
+    // These are the minimum intersection values - we want to use the cell if
+    // the site is >= 50% hovering its position.
+    let minWidth = gDrag.cellWidth / 2;
+    let minHeight = gDrag.cellHeight / 2;
+
+    let cellPositions = this._getCellPositions();
+    let rect = gTransformation.getNodePosition(gDrag.draggedSite.node);
+
+    // Compare each cell's position to the dragged site's position.
+    for (let i = 0; i < cellPositions.length; i++) {
+      let inter = rect.intersect(cellPositions[i].rect);
+
+      // If the intersection is big enough we found a drop target.
+      if (inter.width >= minWidth && inter.height >= minHeight)
+        return cellPositions[i].cell;
+    }
+
+    // No drop target found.
+    return null;
+  },
+
+  /**
+   * Gets the positions of all cell nodes.
+   * @return The (cached) cell positions.
+   */
+  _getCellPositions: function DropTargetShim_getCellPositions() {
+    if (this._cellPositions)
+      return this._cellPositions;
+
+    return this._cellPositions = gGrid.cells.map(function (cell) {
+      return {cell: cell, rect: gTransformation.getNodePosition(cell.node)};
+    });
+  },
+
+  /**
+   * Dispatches a custom DragEvent on the given target node.
+   * @param aEvent The source event.
+   * @param aType The event type.
+   * @param aTarget The target node that receives the event.
+   */
+  _dispatchEvent:
+    function DropTargetShim_dispatchEvent(aEvent, aType, aTarget) {
+
+    let node = aTarget.node;
+    let event = document.createEvent("DragEvents");
+
+    event.initDragEvent(aType, true, true, window, 0, 0, 0, 0, 0, false, false,
+                        false, false, 0, node, aEvent.dataTransfer);
+
+    node.dispatchEvent(event);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/grid.js
@@ -0,0 +1,132 @@
+#ifdef 0
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#endif
+
+/**
+ * This singleton represents the grid that contains all sites.
+ */
+let gGrid = {
+  /**
+   * The DOM node of the grid.
+   */
+  _node: null,
+  get node() this._node,
+
+  /**
+   * The cached DOM fragment for sites.
+   */
+  _siteFragment: null,
+
+  /**
+   * All cells contained in the grid.
+   */
+  get cells() {
+    let children = this.node.querySelectorAll("li");
+    let cells = [new Cell(this, child) for each (child in children)];
+
+    // Replace the getter with our cached value.
+    Object.defineProperty(this, "cells", {value: cells, enumerable: true});
+
+    return cells;
+  },
+
+  /**
+   * All sites contained in the grid's cells. Sites may be empty.
+   */
+  get sites() [cell.site for each (cell in this.cells)],
+
+  /**
+   * Initializes the grid.
+   * @param aSelector The query selector of the grid.
+   */
+  init: function Grid_init(aSelector) {
+    this._node = document.querySelector(aSelector);
+    this._createSiteFragment();
+    this._draw();
+  },
+
+  /**
+   * Creates a new site in the grid.
+   * @param aLink The new site's link.
+   * @param aCell The cell that will contain the new site.
+   * @return The newly created site.
+   */
+  createSite: function Grid_createSite(aLink, aCell) {
+    let node = aCell.node;
+    node.appendChild(this._siteFragment.cloneNode(true));
+    return new Site(node.firstElementChild, aLink);
+  },
+
+  /**
+   * Refreshes the grid and re-creates all sites.
+   */
+  refresh: function Grid_refresh() {
+    // Remove all sites.
+    this.cells.forEach(function (cell) {
+      let node = cell.node;
+      let child = node.firstElementChild;
+
+      if (child)
+        node.removeChild(child);
+    }, this);
+
+    // Draw the grid again.
+    this._draw();
+  },
+
+  /**
+   * Locks the grid to block all pointer events.
+   */
+  lock: function Grid_lock() {
+    this.node.setAttribute("locked", "true");
+  },
+
+  /**
+   * Unlocks the grid to allow all pointer events.
+   */
+  unlock: function Grid_unlock() {
+    this.node.removeAttribute("locked");
+  },
+
+  /**
+   * Creates the DOM fragment that is re-used when creating sites.
+   */
+  _createSiteFragment: function Grid_createSiteFragment() {
+    let site = document.createElementNS(HTML_NAMESPACE, "a");
+    site.classList.add("site");
+    site.setAttribute("draggable", "true");
+
+    // Create the site's inner HTML code.
+    site.innerHTML =
+      '<img class="site-img" width="' + THUMB_WIDTH +'" ' +
+      ' height="' + THUMB_HEIGHT + '" alt=""/>' +
+      '<span class="site-title"/>' +
+      '<span class="site-strip">' +
+      '  <input class="button strip-button strip-button-pin" type="button"' +
+      '   tabindex="-1" title="' + newTabString("pin") + '"/>' +
+      '  <input class="button strip-button strip-button-block" type="button"' +
+      '   tabindex="-1" title="' + newTabString("block") + '"/>' +
+      '</span>';
+
+    this._siteFragment = document.createDocumentFragment();
+    this._siteFragment.appendChild(site);
+  },
+
+  /**
+   * Draws the grid, creates all sites and puts them into their cells.
+   */
+  _draw: function Grid_draw() {
+    let cells = this.cells;
+
+    // Put sites into the cells.
+    let links = gLinks.getLinks();
+    let length = Math.min(links.length, cells.length);
+
+    for (let i = 0; i < length; i++) {
+      if (links[i])
+        this.createSite(links[i], cells[i]);
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/newTab.css
@@ -0,0 +1,173 @@
+:root {
+  -moz-appearance: none;
+}
+
+#scrollbox:not([page-disabled]) {
+  overflow: auto;
+}
+
+#body {
+  position: relative;
+  margin: 0;
+  min-width: 675px;
+  -moz-user-select: none;
+}
+
+.button {
+  cursor: pointer;
+}
+
+/* TOOLBAR */
+#toolbar {
+  position: absolute;
+}
+
+#toolbar[page-disabled] {
+  position: fixed;
+}
+
+#toolbar:-moz-locale-dir(rtl) {
+  left: 8px;
+  right: auto;
+}
+
+.toolbar-button {
+  position: absolute;
+  cursor: pointer;
+  -moz-transition: opacity 200ms ease-out;
+}
+
+#toolbar-button-show,
+#toolbar-button-reset {
+  opacity: 0;
+  pointer-events: none;
+}
+
+#toolbar-button-reset[modified],
+#toolbar-button-show[page-disabled] {
+  opacity: 1;
+  pointer-events: auto;
+}
+
+#toolbar-button-hide[page-disabled],
+#toolbar-button-reset[page-disabled] {
+  opacity: 0;
+  pointer-events: none;
+}
+
+/* GRID */
+#grid {
+  width: 637px;
+  height: 411px;
+  overflow: hidden;
+  list-style-type: none;
+  -moz-transition: opacity 200ms ease-out;
+}
+
+#grid[page-disabled] {
+  opacity: 0;
+}
+
+#grid[page-disabled],
+#grid[locked] {
+  pointer-events: none;
+}
+
+/* CELLS */
+.cell {
+  float: left;
+  width: 201px;
+  height: 127px;
+  margin-bottom: 15px;
+  -moz-margin-end: 16px;
+}
+
+.cell:-moz-locale-dir(rtl) {
+  float: right;
+}
+
+.cell:nth-child(3n+3) {
+  -moz-margin-end: 0;
+}
+
+/* SITES */
+.site {
+  display: block;
+  position: relative;
+  width: 201px;
+  height: 127px;
+}
+
+.site[frozen] {
+  position: absolute;
+  pointer-events: none;
+}
+
+.site[ontop] {
+  z-index: 10;
+}
+
+/* SITE IMAGE */
+.site-img {
+  display: block;
+  opacity: 0.75;
+  -moz-transition: opacity 200ms ease-out;
+}
+
+.site:hover > .site-img,
+.site[ontop] > .site-img,
+.site:-moz-focusring > .site-img {
+  opacity: 1;
+}
+
+.site-img[loading] {
+  display: none;
+}
+
+/* SITE TITLE */
+.site-title {
+  position: absolute;
+  left: 0;
+  bottom: 0;
+  overflow: hidden;
+}
+
+/* SITE STRIP */
+.site-strip {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 195px;
+  height: 17px;
+  overflow: hidden;
+  opacity: 0;
+  -moz-transition: opacity 200ms ease-out;
+}
+
+.site:hover:not([frozen]) > .site-strip {
+  opacity: 1;
+}
+
+.strip-button-pin,
+.strip-button-block:-moz-locale-dir(rtl) {
+  float: left;
+}
+
+.strip-button-block,
+.strip-button-pin:-moz-locale-dir(rtl) {
+  float: right;
+}
+
+/* DRAG & DROP */
+
+/*
+ * This is just a temporary drag element used for dataTransfer.setDragImage()
+ * so that we can use custom drag images and elements. It needs an opacity of
+ * 0.01 so that the core code detects that it's in fact a visible element.
+ */
+.drag-element {
+  width: 1px;
+  height: 1px;
+  background-color: #fff;
+  opacity: 0.01;
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/newTab.js
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+let Cu = Components.utils;
+let Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource:///modules/PageThumbs.jsm");
+Cu.import("resource:///modules/NewTabUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Rect",
+  "resource://gre/modules/Geometry.jsm");
+
+let {
+  links: gLinks,
+  allPages: gAllPages,
+  pinnedLinks: gPinnedLinks,
+  blockedLinks: gBlockedLinks
+} = NewTabUtils;
+
+XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() {
+  return Services.strings.
+    createBundle("chrome://browser/locale/newTab.properties");
+});
+
+function newTabString(name) gStringBundle.GetStringFromName('newtab.' + name);
+
+const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
+const THUMB_WIDTH = 201;
+const THUMB_HEIGHT = 127;
+
+#include batch.js
+#include transformations.js
+#include page.js
+#include toolbar.js
+#include grid.js
+#include cells.js
+#include sites.js
+#include drag.js
+#include drop.js
+#include dropTargetShim.js
+#include dropPreview.js
+#include updater.js
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/newTab.xul
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://browser/content/newtab/newTab.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/newtab/newTab.css" type="text/css"?>
+
+<!DOCTYPE window [
+  <!ENTITY % newTabDTD SYSTEM "chrome://browser/locale/newTab.dtd">
+  %newTabDTD;
+]>
+
+<xul:window xmlns="http://www.w3.org/1999/xhtml"
+            xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+            disablefastfind="true" title="&newtab.pageTitle;">
+  <xul:vbox id="scrollbox" flex="1" title=" ">
+    <body id="body">
+      <div id="toolbar">
+        <input class="button toolbar-button" id="toolbar-button-show"
+               type="button" title="&newtab.show;"/>
+        <input class="button toolbar-button" id="toolbar-button-hide"
+               type="button" title="&newtab.hide;"/>
+        <input class="button toolbar-button" id="toolbar-button-reset"
+               type="button" title="&newtab.reset;"/>
+      </div>
+
+      <ul id="grid">
+        <li class="cell"/><li class="cell"/><li class="cell"/>
+        <li class="cell"/><li class="cell"/><li class="cell"/>
+        <li class="cell"/><li class="cell"/><li class="cell"/>
+      </ul>
+
+      <xul:script type="text/javascript;version=1.8" src="chrome://browser/content/newtab/newTab.js"/>
+      <xul:script type="text/javascript;version=1.8">
+        gPage.init("#toolbar", "#grid");
+      </xul:script>
+    </body>
+  </xul:vbox>
+</xul:window>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/page.js
@@ -0,0 +1,173 @@
+#ifdef 0
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#endif
+
+/**
+ * This singleton represents the whole 'New Tab Page' and takes care of
+ * initializing all its components.
+ */
+let gPage = {
+  /**
+   * Initializes the page.
+   * @param aToolbarSelector The query selector for the page toolbar.
+   * @param aGridSelector The query selector for the grid.
+   */
+  init: function Page_init(aToolbarSelector, aGridSelector) {
+    gToolbar.init(aToolbarSelector);
+    this._gridSelector = aGridSelector;
+
+    // Add ourselves to the list of pages to receive notifications.
+    gAllPages.register(this);
+
+    // Listen for 'unload' to unregister this page.
+    function unload() gAllPages.unregister(self);
+    addEventListener("unload", unload, false);
+
+    // Check if the new tab feature is enabled.
+    if (gAllPages.enabled)
+      this._init();
+    else
+      this._updateAttributes(false);
+  },
+
+  /**
+   * Listens for notifications specific to this page.
+   */
+  observe: function Page_observe() {
+    let enabled = gAllPages.enabled;
+    this._updateAttributes(enabled);
+
+    // Initialize the whole page if we haven't done that, yet.
+    if (enabled)
+      this._init();
+  },
+
+  /**
+   * Updates the whole page and the grid when the storage has changed.
+   */
+  update: function Page_update() {
+    this.updateModifiedFlag();
+    gGrid.refresh();
+  },
+
+  /**
+   * Checks if the page is modified and sets the CSS class accordingly
+   */
+  updateModifiedFlag: function Page_updateModifiedFlag() {
+    let node = document.getElementById("toolbar-button-reset");
+    let modified = this._isModified();
+
+    if (modified)
+      node.setAttribute("modified", "true");
+    else
+      node.removeAttribute("modified");
+
+    this._updateTabIndices(gAllPages.enabled, modified);
+  },
+
+  /**
+   * Internally initializes the page. This runs only when/if the feature
+   * is/gets enabled.
+   */
+  _init: function Page_init() {
+    if (this._initialized)
+      return;
+
+    this._initialized = true;
+
+    gLinks.populateCache(function () {
+      // Check if the grid is modified.
+      this.updateModifiedFlag();
+
+      // Initialize and render the grid.
+      gGrid.init(this._gridSelector);
+
+      // Initialize the drop target shim.
+      gDropTargetShim.init();
+
+      // Workaround to prevent a delay on MacOSX due to a slow drop animation.
+      let doc = document.documentElement;
+      doc.addEventListener("dragover", this.onDragOver, false);
+      doc.addEventListener("drop", this.onDrop, false);
+    }.bind(this));
+  },
+
+  /**
+   * Updates the 'page-disabled' attributes of the respective DOM nodes.
+   * @param aValue Whether to set or remove attributes.
+   */
+  _updateAttributes: function Page_updateAttributes(aValue) {
+    let nodes = document.querySelectorAll("#grid, #scrollbox, #toolbar, .toolbar-button");
+
+    // Set the nodes' states.
+    for (let i = 0; i < nodes.length; i++) {
+      let node = nodes[i];
+      if (aValue)
+        node.removeAttribute("page-disabled");
+      else
+        node.setAttribute("page-disabled", "true");
+    }
+
+    this._updateTabIndices(aValue, this._isModified());
+  },
+
+  /**
+   * Checks whether the page is modified.
+   * @return Whether the page is modified or not.
+   */
+  _isModified: function Page_isModified() {
+    // The page is considered modified only if sites have been removed.
+    return !gBlockedLinks.isEmpty();
+  },
+
+  /**
+   * Updates the tab indices of focusable elements.
+   * @param aEnabled Whether the page is currently enabled.
+   * @param aModified Whether the page is currently modified.
+   */
+  _updateTabIndices: function Page_updateTabIndices(aEnabled, aModified) {
+    function setFocusable(aNode, aFocusable) {
+      if (aFocusable)
+        aNode.removeAttribute("tabindex");
+      else
+        aNode.setAttribute("tabindex", "-1");
+    }
+
+    // Sites and the 'hide' button are always focusable when the grid is shown.
+    let nodes = document.querySelectorAll(".site, #toolbar-button-hide");
+    for (let i = 0; i < nodes.length; i++)
+      setFocusable(nodes[i], aEnabled);
+
+    // The 'show' button is focusable when the grid is hidden.
+    let btnShow = document.getElementById("toolbar-button-show");
+    setFocusable(btnShow, !aEnabled);
+
+    // The 'reset' button is focusable when the grid is shown and modified.
+    let btnReset = document.getElementById("toolbar-button-reset");
+    setFocusable(btnReset, aEnabled && aModified);
+  },
+
+  /**
+   * Handles the 'dragover' event. Workaround to prevent a delay on MacOSX
+   * due to a slow drop animation.
+   * @param aEvent The 'dragover' event.
+   */
+  onDragOver: function Page_onDragOver(aEvent) {
+    if (gDrag.isValid(aEvent))
+      aEvent.preventDefault();
+  },
+
+  /**
+   * Handles the 'drop' event. Workaround to prevent a delay on MacOSX due to
+   * a slow drop animation.
+   * @param aEvent The 'drop' event.
+   */
+  onDrop: function Page_onDrop(aEvent) {
+    if (gDrag.isValid(aEvent)) {
+      aEvent.preventDefault();
+      aEvent.stopPropagation();
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/sites.js
@@ -0,0 +1,209 @@
+#ifdef 0
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#endif
+
+/**
+ * This class represents a site that is contained in a cell and can be pinned,
+ * moved around or deleted.
+ */
+function Site(aNode, aLink) {
+  this._node = aNode;
+  this._node._newtabSite = this;
+
+  this._link = aLink;
+
+  this._render();
+  this._addEventHandlers();
+}
+
+Site.prototype = {
+  /**
+   * The site's DOM node.
+   */
+  get node() this._node,
+
+  /**
+   * The site's link.
+   */
+  get link() this._link,
+
+  /**
+   * The url of the site's link.
+   */
+  get url() this.link.url,
+
+  /**
+   * The title of the site's link.
+   */
+  get title() this.link.title,
+
+  /**
+   * The site's parent cell.
+   */
+  get cell() {
+    let parentNode = this.node.parentNode;
+    return parentNode && parentNode._newtabCell;
+  },
+
+  /**
+   * Pins the site on its current or a given index.
+   * @param aIndex The pinned index (optional).
+   */
+  pin: function Site_pin(aIndex) {
+    if (typeof aIndex == "undefined")
+      aIndex = this.cell.index;
+
+    this._updateAttributes(true);
+    gPinnedLinks.pin(this._link, aIndex);
+  },
+
+  /**
+   * Unpins the site and calls the given callback when done.
+   * @param aCallback The callback to be called when finished.
+   */
+  unpin: function Site_unpin(aCallback) {
+    if (this.isPinned()) {
+      this._updateAttributes(false);
+      gPinnedLinks.unpin(this._link);
+      gUpdater.updateGrid(aCallback);
+    }
+  },
+
+  /**
+   * Checks whether this site is pinned.
+   * @return Whether this site is pinned.
+   */
+  isPinned: function Site_isPinned() {
+    return gPinnedLinks.isPinned(this._link);
+  },
+
+  /**
+   * Blocks the site (removes it from the grid) and calls the given callback
+   * when done.
+   * @param aCallback The callback to be called when finished.
+   */
+  block: function Site_block(aCallback) {
+    gBlockedLinks.block(this._link);
+    gUpdater.updateGrid(aCallback);
+    gPage.updateModifiedFlag();
+  },
+
+  /**
+   * Gets the DOM node specified by the given query selector.
+   * @param aSelector The query selector.
+   * @return The DOM node we found.
+   */
+  _querySelector: function Site_querySelector(aSelector) {
+    return this.node.querySelector(aSelector);
+  },
+
+  /**
+   * Updates attributes for all nodes which status depends on this site being
+   * pinned or unpinned.
+   * @param aPinned Whether this site is now pinned or unpinned.
+   */
+  _updateAttributes: function (aPinned) {
+    let buttonPin = this._querySelector(".strip-button-pin");
+
+    if (aPinned) {
+      this.node.setAttribute("pinned", true);
+      buttonPin.setAttribute("title", newTabString("unpin"));
+    } else {
+      this.node.removeAttribute("pinned");
+      buttonPin.setAttribute("title", newTabString("pin"));
+    }
+  },
+
+  /**
+   * Renders the site's data (fills the HTML fragment).
+   */
+  _render: function Site_render() {
+    let title = this.title || this.url;
+    this.node.setAttribute("title", title);
+    this.node.setAttribute("href", this.url);
+    this._querySelector(".site-title").textContent = title;
+
+    if (this.isPinned())
+      this._updateAttributes(true);
+
+    this._renderThumbnail();
+  },
+
+  /**
+   * Renders the site's thumbnail.
+   */
+  _renderThumbnail: function Site_renderThumbnail() {
+    let img = this._querySelector(".site-img")
+    img.setAttribute("alt", this.title || this.url);
+    img.setAttribute("loading", "true");
+
+    // Wait until the image has loaded.
+    img.addEventListener("load", function onLoad() {
+      img.removeEventListener("load", onLoad, false);
+      img.removeAttribute("loading");
+    }, false);
+
+    // Set the thumbnail url.
+    img.setAttribute("src", PageThumbs.getThumbnailURL(this.url));
+  },
+
+  /**
+   * Adds event handlers for the site and its buttons.
+   */
+  _addEventHandlers: function Site_addEventHandlers() {
+    // Register drag-and-drop event handlers.
+    ["DragStart", /*"Drag",*/ "DragEnd"].forEach(function (aType) {
+      let method = "_on" + aType;
+      this[method] = this[method].bind(this);
+      this._node.addEventListener(aType.toLowerCase(), this[method], false);
+    }, this);
+
+    let self = this;
+
+    function pin(aEvent) {
+      if (aEvent)
+        aEvent.preventDefault();
+
+      if (self.isPinned())
+        self.unpin();
+      else
+        self.pin();
+    }
+
+    function block(aEvent) {
+      if (aEvent)
+        aEvent.preventDefault();
+
+      self.block();
+    }
+
+    this._querySelector(".strip-button-pin").addEventListener("click", pin, false);
+    this._querySelector(".strip-button-block").addEventListener("click", block, false);
+  },
+
+  /**
+   * Event handler for the 'dragstart' event.
+   * @param aEvent The drag event.
+   */
+  _onDragStart: function Site_onDragStart(aEvent) {
+    gDrag.start(this, aEvent);
+  },
+
+  /**
+   * Event handler for the 'drag' event.
+   * @param aEvent The drag event.
+  */
+  _onDrag: function Site_onDrag(aEvent) {
+    gDrag.drag(this, aEvent);
+  },
+
+  /**
+   * Event handler for the 'dragend' event.
+   * @param aEvent The drag event.
+   */
+  _onDragEnd: function Site_onDragEnd(aEvent) {
+    gDrag.end(this, aEvent);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/toolbar.js
@@ -0,0 +1,87 @@
+#ifdef 0
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#endif
+
+/**
+ * This singleton represents the page's toolbar that allows to enable/disable
+ * the 'New Tab Page' feature and to reset the whole page.
+ */
+let gToolbar = {
+  /**
+   * Initializes the toolbar.
+   * @param aSelector The query selector of the toolbar.
+   */
+  init: function Toolbar_init(aSelector) {
+    this._node = document.querySelector(aSelector);
+    let buttons = this._node.querySelectorAll("input");
+
+    // Listen for 'click' events on the toolbar buttons.
+    ["show", "hide", "reset"].forEach(function (aType, aIndex) {
+      let self = this;
+      let button = buttons[aIndex];
+      let handler = function () self[aType]();
+
+      button.addEventListener("click", handler, false);
+
+#ifdef XP_MACOSX
+      // Per default buttons lose focus after being clicked on Mac OS X.
+      // So when the URL bar has focus and a toolbar button is clicked the
+      // URL bar regains focus and the history pops up. We need to prevent
+      // that by explicitly removing its focus.
+      button.addEventListener("mousedown", function () {
+        window.focus();
+      }, false);
+#endif
+    }, this);
+  },
+
+  /**
+   * Enables the 'New Tab Page' feature.
+   */
+  show: function Toolbar_show() {
+    this._passButtonFocus("show", "hide");
+    gAllPages.enabled = true;
+  },
+
+  /**
+   * Disables the 'New Tab Page' feature.
+   */
+  hide: function Toolbar_hide() {
+    this._passButtonFocus("hide", "show");
+    gAllPages.enabled = false;
+  },
+
+  /**
+   * Resets the whole page and forces it to re-render its content.
+   * @param aCallback The callback to call when the page has been reset.
+   */
+  reset: function Toolbar_reset(aCallback) {
+    this._passButtonFocus("reset", "hide");
+    let node = gGrid.node;
+
+    // animate the page reset
+    gTransformation.fadeNodeOut(node, function () {
+      NewTabUtils.reset();
+
+      gLinks.populateCache(function () {
+        gAllPages.update();
+
+        // Without the setTimeout() we have a strange flicker.
+        setTimeout(function () gTransformation.fadeNodeIn(node, aCallback));
+      }, true);
+    });
+  },
+
+  /**
+   * Passes the focus from the current button to the next.
+   * @param aCurrent The button that currently has focus.
+   * @param aNext The button that is focused next.
+   */
+  _passButtonFocus: function Toolbar_passButtonFocus(aCurrent, aNext) {
+    if (document.querySelector("#toolbar-button-" + aCurrent + ":-moz-focusring"))
+      document.getElementById("toolbar-button-" + aNext).focus();
+  }
+};
+
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/transformations.js
@@ -0,0 +1,226 @@
+#ifdef 0
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#endif
+
+/**
+ * This singleton allows to transform the grid by repositioning a site's node
+ * in the DOM and by showing or hiding the node. It additionally provides
+ * convenience methods to work with a site's DOM node.
+ */
+let gTransformation = {
+  /**
+   * Gets a DOM node's position.
+   * @param aNode The DOM node.
+   * @return A Rect instance with the position.
+   */
+  getNodePosition: function Transformation_getNodePosition(aNode) {
+    let {left, top, width, height} = aNode.getBoundingClientRect();
+    return new Rect(left + scrollX, top + scrollY, width, height);
+  },
+
+  /**
+   * Fades a given node from zero to full opacity.
+   * @param aNode The node to fade.
+   * @param aCallback The callback to call when finished.
+   */
+  fadeNodeIn: function Transformation_fadeNodeIn(aNode, aCallback) {
+    this._setNodeOpacity(aNode, 1, function () {
+      // Clear the style property.
+      aNode.style.opacity = "";
+
+      if (aCallback)
+        aCallback();
+    });
+  },
+
+  /**
+   * Fades a given node from full to zero opacity.
+   * @param aNode The node to fade.
+   * @param aCallback The callback to call when finished.
+   */
+  fadeNodeOut: function Transformation_fadeNodeOut(aNode, aCallback) {
+    this._setNodeOpacity(aNode, 0, aCallback);
+  },
+
+  /**
+   * Fades a given site from zero to full opacity.
+   * @param aSite The site to fade.
+   * @param aCallback The callback to call when finished.
+   */
+  showSite: function Transformation_showSite(aSite, aCallback) {
+    this.fadeNodeIn(aSite.node, aCallback);
+  },
+
+  /**
+   * Fades a given site from full to zero opacity.
+   * @param aSite The site to fade.
+   * @param aCallback The callback to call when finished.
+   */
+  hideSite: function Transformation_hideSite(aSite, aCallback) {
+    this.fadeNodeOut(aSite.node, aCallback);
+  },
+
+  /**
+   * Allows to set a site's position.
+   * @param aSite The site to re-position.
+   * @param aPosition The desired position for the given site.
+   */
+  setSitePosition: function Transformation_setSitePosition(aSite, aPosition) {
+    let style = aSite.node.style;
+    let {top, left} = aPosition;
+
+    style.top = top + "px";
+    style.left = left + "px";
+  },
+
+  /**
+   * Freezes a site in its current position by positioning it absolute.
+   * @param aSite The site to freeze.
+   */
+  freezeSitePosition: function Transformation_freezeSitePosition(aSite) {
+    aSite.node.setAttribute("frozen", "true");
+    this.setSitePosition(aSite, this.getNodePosition(aSite.node));
+  },
+
+  /**
+   * Unfreezes a site by removing its absolute positioning.
+   * @param aSite The site to unfreeze.
+   */
+  unfreezeSitePosition: function Transformation_unfreezeSitePosition(aSite) {
+    let style = aSite.node.style;
+    style.left = style.top = "";
+    aSite.node.removeAttribute("frozen");
+  },
+
+  /**
+   * Slides the given site to the target node's position.
+   * @param aSite The site to move.
+   * @param aTarget The slide target.
+   * @param aOptions Set of options (see below).
+   *        unfreeze - unfreeze the site after sliding
+   *        callback - the callback to call when finished
+   */
+  slideSiteTo: function Transformation_slideSiteTo(aSite, aTarget, aOptions) {
+    let currentPosition = this.getNodePosition(aSite.node);
+    let targetPosition = this.getNodePosition(aTarget.node)
+    let callback = aOptions && aOptions.callback;
+
+    let self = this;
+
+    function finish() {
+      if (aOptions && aOptions.unfreeze)
+        self.unfreezeSitePosition(aSite);
+
+      if (callback)
+        callback();
+    }
+
+    // Nothing to do here if the positions already match.
+    if (currentPosition.equals(targetPosition)) {
+      finish();
+    } else {
+      this.setSitePosition(aSite, targetPosition);
+      this._whenTransitionEnded(aSite.node, finish);
+    }
+  },
+
+  /**
+   * Rearranges a given array of sites and moves them to their new positions or
+   * fades in/out new/removed sites.
+   * @param aSites An array of sites to rearrange.
+   * @param aOptions Set of options (see below).
+   *        unfreeze - unfreeze the site after rearranging
+   *        callback - the callback to call when finished
+   */
+  rearrangeSites: function Transformation_rearrangeSites(aSites, aOptions) {
+    let batch;
+    let cells = gGrid.cells;
+    let callback = aOptions && aOptions.callback;
+    let unfreeze = aOptions && aOptions.unfreeze;
+
+    if (callback) {
+      batch = new Batch(callback);
+      callback = function () batch.pop();
+    }
+
+    aSites.forEach(function (aSite, aIndex) {
+      // Do not re-arrange empty cells or the dragged site.
+      if (!aSite || aSite == gDrag.draggedSite)
+        return;
+
+      if (batch)
+        batch.push();
+
+      if (!cells[aIndex])
+        // The site disappeared from the grid, hide it.
+        this.hideSite(aSite, callback);
+      else if (this._getNodeOpacity(aSite.node) != 1)
+        // The site disappeared before but is now back, show it.
+        this.showSite(aSite, callback);
+      else
+        // The site's position has changed, move it around.
+        this._moveSite(aSite, aIndex, {unfreeze: unfreeze, callback: callback});
+    }, this);
+
+    if (batch)
+      batch.close();
+  },
+
+  /**
+   * Listens for the 'transitionend' event on a given node and calls the given
+   * callback.
+   * @param aNode The node that is transitioned.
+   * @param aCallback The callback to call when finished.
+   */
+  _whenTransitionEnded:
+    function Transformation_whenTransitionEnded(aNode, aCallback) {
+
+    aNode.addEventListener("transitionend", function onEnd() {
+      aNode.removeEventListener("transitionend", onEnd, false);
+      aCallback();
+    }, false);
+  },
+
+  /**
+   * Gets a given node's opacity value.
+   * @param aNode The node to get the opacity value from.
+   * @return The node's opacity value.
+   */
+  _getNodeOpacity: function Transformation_getNodeOpacity(aNode) {
+    let cstyle = window.getComputedStyle(aNode, null);
+    return cstyle.getPropertyValue("opacity");
+  },
+
+  /**
+   * Sets a given node's opacity.
+   * @param aNode The node to set the opacity value for.
+   * @param aOpacity The opacity value to set.
+   * @param aCallback The callback to call when finished.
+   */
+  _setNodeOpacity:
+    function Transformation_setNodeOpacity(aNode, aOpacity, aCallback) {
+
+    if (this._getNodeOpacity(aNode) == aOpacity) {
+      if (aCallback)
+        aCallback();
+    } else {
+      if (aCallback)
+        this._whenTransitionEnded(aNode, aCallback);
+
+      aNode.style.opacity = aOpacity;
+    }
+  },
+
+  /**
+   * Moves a site to the cell with the given index.
+   * @param aSite The site to move.
+   * @param aIndex The target cell's index.
+   * @param aOptions Options that are directly passed to slideSiteTo().
+   */
+  _moveSite: function Transformation_moveSite(aSite, aIndex, aOptions) {
+    this.freezeSitePosition(aSite);
+    this.slideSiteTo(aSite, gGrid.cells[aIndex], aOptions);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/newtab/updater.js
@@ -0,0 +1,182 @@
+#ifdef 0
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#endif
+
+/**
+ * This singleton provides functionality to update the current grid to a new
+ * set of pinned and blocked sites. It adds, moves and removes sites.
+ */
+let gUpdater = {
+  /**
+   * Updates the current grid according to its pinned and blocked sites.
+   * This removes old, moves existing and creates new sites to fill gaps.
+   * @param aCallback The callback to call when finished.
+   */
+  updateGrid: function Updater_updateGrid(aCallback) {
+    let links = gLinks.getLinks().slice(0, gGrid.cells.length);
+
+    // Find all sites that remain in the grid.
+    let sites = this._findRemainingSites(links);
+
+    let self = this;
+
+    // Remove sites that are no longer in the grid.
+    this._removeLegacySites(sites, function () {
+      // Freeze all site positions so that we can move their DOM nodes around
+      // without any visual impact.
+      self._freezeSitePositions(sites);
+
+      // Move the sites' DOM nodes to their new position in the DOM. This will
+      // have no visual effect as all the sites have been frozen and will
+      // remain in their current position.
+      self._moveSiteNodes(sites);
+
+      // Now it's time to animate the sites actually moving to their new
+      // positions.
+      self._rearrangeSites(sites, function () {
+        // Try to fill empty cells and finish.
+        self._fillEmptyCells(links, aCallback);
+
+        // Update other pages that might be open to keep them synced.
+        gAllPages.update(gPage);
+      });
+    });
+  },
+
+  /**
+   * Takes an array of links and tries to correlate them to sites contained in
+   * the current grid. If no corresponding site can be found (i.e. the link is
+   * new and a site will be created) then just set it to null.
+   * @param aLinks The array of links to find sites for.
+   * @return Array of sites mapped to the given links (can contain null values).
+   */
+  _findRemainingSites: function Updater_findRemainingSites(aLinks) {
+    let map = {};
+
+    // Create a map to easily retrieve the site for a given URL.
+    gGrid.sites.forEach(function (aSite) {
+      if (aSite)
+        map[aSite.url] = aSite;
+    });
+
+    // Map each link to its corresponding site, if any.
+    return aLinks.map(function (aLink) {
+      return aLink && (aLink.url in map) && map[aLink.url];
+    });
+  },
+
+  /**
+   * Freezes the given sites' positions.
+   * @param aSites The array of sites to freeze.
+   */
+  _freezeSitePositions: function Updater_freezeSitePositions(aSites) {
+    aSites.forEach(function (aSite) {
+      if (aSite)
+        gTransformation.freezeSitePosition(aSite);
+    });
+  },
+
+  /**
+   * Moves the given sites' DOM nodes to their new positions.
+   * @param aSites The array of sites to move.
+   */
+  _moveSiteNodes: function Updater_moveSiteNodes(aSites) {
+    let cells = gGrid.cells;
+
+    // Truncate the given array of sites to not have more sites than cells.
+    // This can happen when the user drags a bookmark (or any other new kind
+    // of link) onto the grid.
+    let sites = aSites.slice(0, cells.length);
+
+    sites.forEach(function (aSite, aIndex) {
+      let cell = cells[aIndex];
+      let cellSite = cell.site;
+
+      // The site's position didn't change.
+      if (!aSite || cellSite != aSite) {
+        let cellNode = cell.node;
+
+        // Empty the cell if necessary.
+        if (cellSite)
+          cellNode.removeChild(cellSite.node);
+
+        // Put the new site in place, if any.
+        if (aSite)
+          cellNode.appendChild(aSite.node);
+      }
+    }, this);
+  },
+
+  /**
+   * Rearranges the given sites and slides them to their new positions.
+   * @param aSites The array of sites to re-arrange.
+   * @param aCallback The callback to call when finished.
+   */
+  _rearrangeSites: function Updater_rearrangeSites(aSites, aCallback) {
+    let options = {callback: aCallback, unfreeze: true};
+    gTransformation.rearrangeSites(aSites, options);
+  },
+
+  /**
+   * Removes all sites from the grid that are not in the given links array or
+   * exceed the grid.
+   * @param aSites The array of sites remaining in the grid.
+   * @param aCallback The callback to call when finished.
+   */
+  _removeLegacySites: function Updater_removeLegacySites(aSites, aCallback) {
+    let batch = new Batch(aCallback);
+
+    // Delete sites that were removed from the grid.
+    gGrid.sites.forEach(function (aSite) {
+      // The site must be valid and not in the current grid.
+      if (!aSite || aSites.indexOf(aSite) != -1)
+        return;
+
+      batch.push();
+
+      // Fade out the to-be-removed site.
+      gTransformation.hideSite(aSite, function () {
+        let node = aSite.node;
+
+        // Remove the site from the DOM.
+        node.parentNode.removeChild(node);
+        batch.pop();
+      });
+    });
+
+    batch.close();
+  },
+
+  /**
+   * Tries to fill empty cells with new links if available.
+   * @param aLinks The array of links.
+   * @param aCallback The callback to call when finished.
+   */
+  _fillEmptyCells: function Updater_fillEmptyCells(aLinks, aCallback) {
+    let {cells, sites} = gGrid;
+    let batch = new Batch(aCallback);
+
+    // Find empty cells and fill them.
+    sites.forEach(function (aSite, aIndex) {
+      if (aSite || !aLinks[aIndex])
+        return;
+
+      batch.push();
+
+      // Create the new site and fade it in.
+      let site = gGrid.createSite(aLinks[aIndex], cells[aIndex]);
+
+      // Set the site's initial opacity to zero.
+      site.node.style.opacity = 0;
+
+      // Without the setTimeout() the node would just appear instead of fade in.
+      setTimeout(function () {
+        gTransformation.showSite(site, function () batch.pop());
+      }, 0);
+    });
+
+    batch.close();
+  }
+};
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -627,17 +627,17 @@
                     !(this.mBrowser.docShell.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE))
                   this.mBrowser.mIconURL = null;
 
                 let autocomplete = this.mTabBrowser._placesAutocomplete;
                 if (this.mBrowser.registeredOpenURI) {
                   autocomplete.unregisterOpenPage(this.mBrowser.registeredOpenURI);
                   delete this.mBrowser.registeredOpenURI;
                 }
-                if (aLocation.spec != "about:blank") {
+                if (!isBlankPageURL(aLocation.spec)) {
                   autocomplete.registerOpenPage(aLocation);
                   this.mBrowser.registeredOpenURI = aLocation;
                 }
               }
 
               if (!this.mBlank) {
                 this._callProgressListeners("onLocationChange",
                                             [aWebProgress, aRequest, aLocation,
@@ -1060,17 +1060,17 @@
               if (browser.currentURI.spec) {
                 try {
                   title = this.mURIFixup.createExposableURI(browser.currentURI).spec;
                 } catch(ex) {
                   title = browser.currentURI.spec;
                 }
               }
 
-              if (title && title != "about:blank") {
+              if (title && !isBlankPageURL(title)) {
                 // At this point, we now have a URI.
                 // Let's try to unescape it using a character set
                 // in case the URI is not ASCII.
                 try {
                   var characterSet = browser.contentDocument.characterSet;
                   const textToSubURI = Components.classes["@mozilla.org/intl/texttosuburi;1"]
                                                  .getService(Components.interfaces.nsITextToSubURI);
                   title = textToSubURI.unEscapeNonAsciiURI(characterSet, title);
@@ -1584,17 +1584,17 @@
                 return null;
 
               newTab = true;
             }
 
             aTab.closing = true;
             this._removingTabs.push(aTab);
             if (newTab)
-              this.addTab("about:blank", {skipAnimation: true});
+              this.addTab(BROWSER_NEW_TAB_URL, {skipAnimation: true});
             else
               this.tabContainer.updateVisibility();
 
             // We're committed to closing the tab now.
             // Dispatch a notification.
             // We dispatch it before any teardown so that event listeners can
             // inspect the tab that's about to close.
             var evt = document.createEvent("UIEvent");
--- a/browser/base/content/test/Makefile.in
+++ b/browser/base/content/test/Makefile.in
@@ -35,16 +35,20 @@
 # ***** END LICENSE BLOCK *****
 
 DEPTH		= ../../../..
 topsrcdir	= @top_srcdir@
 srcdir		= @srcdir@
 VPATH		= @srcdir@
 relativesrcdir  = browser/base/content/test
 
+DIRS += \
+		newtab \
+		$(NULL)
+
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _TEST_FILES = \
 		test_feed_discovery.html \
 		feed_discovery.html \
 		test_bug395533.html \
 		bug395533-data.txt \
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/newtab/Makefile.in
@@ -0,0 +1,27 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH		= ../../../../..
+topsrcdir	= @top_srcdir@
+srcdir		= @srcdir@
+VPATH		= @srcdir@
+relativesrcdir  = browser/base/content/test/newtab
+
+include $(DEPTH)/config/autoconf.mk
+include $(topsrcdir)/config/rules.mk
+
+_BROWSER_FILES = \
+	browser_newtab_block.js \
+	browser_newtab_disable.js \
+	browser_newtab_drag_drop.js \
+	browser_newtab_drop_preview.js \
+	browser_newtab_private_browsing.js \
+	browser_newtab_reset.js \
+	browser_newtab_tabsync.js \
+	browser_newtab_unpin.js \
+	head.js \
+	$(NULL)
+
+libs::	$(_BROWSER_FILES)
+	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_block.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * These tests make sure that blocking/removing sites from the grid works
+ * as expected. Pinned tabs should not be moved. Gaps will be re-filled
+ * if more sites are available.
+ */
+function runTests() {
+  // we remove sites and expect the gaps to be filled as long as there still
+  // are some sites available
+  setLinks("0,1,2,3,4,5,6,7,8,9");
+  setPinnedLinks("");
+
+  yield addNewTabPageTab();
+  checkGrid("0,1,2,3,4,5,6,7,8");
+
+  yield blockCell(cells[4]);
+  checkGrid("0,1,2,3,5,6,7,8,9");
+
+  yield blockCell(cells[4]);
+  checkGrid("0,1,2,3,6,7,8,9,");
+
+  yield blockCell(cells[4]);
+  checkGrid("0,1,2,3,7,8,9,,");
+
+  // we removed a pinned site
+  reset();
+  setLinks("0,1,2,3,4,5,6,7,8");
+  setPinnedLinks(",1");
+
+  yield addNewTabPageTab();
+  checkGrid("0,1p,2,3,4,5,6,7,8");
+
+  yield blockCell(cells[1]);
+  checkGrid("0,2,3,4,5,6,7,8,");
+
+  // we remove the last site on the grid (which is pinned) and expect the gap
+  // to be re-filled and the new site to be unpinned
+  reset();
+  setLinks("0,1,2,3,4,5,6,7,8,9");
+  setPinnedLinks(",,,,,,,,8");
+
+  yield addNewTabPageTab();
+  checkGrid("0,1,2,3,4,5,6,7,8p");
+
+  yield blockCell(cells[8]);
+  checkGrid("0,1,2,3,4,5,6,7,9");
+
+  // we remove the first site on the grid with the last one pinned. all cells
+  // but the last one should shift to the left and a new site fades in
+  reset();
+  setLinks("0,1,2,3,4,5,6,7,8,9");
+  setPinnedLinks(",,,,,,,,8");
+
+  yield addNewTabPageTab();
+  checkGrid("0,1,2,3,4,5,6,7,8p");
+
+  yield blockCell(cells[0]);
+  checkGrid("1,2,3,4,5,6,7,9,8p");
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_disable.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * These tests make sure that the 'New Tab Page' feature can be disabled if the
+ * decides not to use it.
+ */
+function runTests() {
+  // create a new tab page and hide it.
+  setLinks("0,1,2,3,4,5,6,7,8");
+  setPinnedLinks("");
+
+  yield addNewTabPageTab();
+  let gridNode = cw.gGrid.node;
+
+  ok(!gridNode.hasAttribute("page-disabled"), "page is not disabled");
+
+  cw.gToolbar.hide();
+  ok(gridNode.hasAttribute("page-disabled"), "page is disabled");
+
+  let oldGridNode = cw.gGrid.node;
+
+  // create a second new tage page and make sure it's disabled. enable it
+  // again and check if the former page gets enabled as well.
+  yield addNewTabPageTab();
+  ok(gridNode.hasAttribute("page-disabled"), "page is disabled");
+
+  // check that no sites have been rendered
+  is(0, cw.document.querySelectorAll(".site").length, "no sites have been rendered");
+
+  cw.gToolbar.show();
+  ok(!gridNode.hasAttribute("page-disabled"), "page is not disabled");
+  ok(!oldGridNode.hasAttribute("page-disabled"), "old page is not disabled");
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_drag_drop.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * These tests make sure that dragging and dropping sites works as expected.
+ * Sites contained in the grid need to shift around to indicate the result
+ * of the drag-and-drop operation. If the grid is full and we're dragging
+ * a new site into it another one gets pushed out.
+ */
+function runTests() {
+  // test a simple drag-and-drop scenario
+  setLinks("0,1,2,3,4,5,6,7,8");
+  setPinnedLinks("");
+
+  yield addNewTabPageTab();
+  checkGrid("0,1,2,3,4,5,6,7,8");
+
+  yield simulateDrop(cells[1], cells[0]);
+  checkGrid("1,0p,2,3,4,5,6,7,8");
+
+  // drag a cell to its current cell and make sure it's not pinned afterwards
+  setLinks("0,1,2,3,4,5,6,7,8");
+  setPinnedLinks("");
+
+  yield addNewTabPageTab();
+  checkGrid("0,1,2,3,4,5,6,7,8");
+
+  yield simulateDrop(cells[0], cells[0]);
+  checkGrid("0,1,2,3,4,5,6,7,8");
+
+  // ensure that pinned pages aren't moved if that's not necessary
+  setLinks("0,1,2,3,4,5,6,7,8");
+  setPinnedLinks(",1,2");
+
+  yield addNewTabPageTab();
+  checkGrid("0,1p,2p,3,4,5,6,7,8");
+
+  yield simulateDrop(cells[3], cells[0]);
+  checkGrid("3,1p,2p,0p,4,5,6,7,8");
+
+  // pinned sites should always be moved around as blocks. if a pinned site is
+  // moved around, neighboring pinned are affected as well
+  setLinks("0,1,2,3,4,5,6,7,8");
+  setPinnedLinks("0,1");
+
+  yield addNewTabPageTab();
+  checkGrid("0p,1p,2,3,4,5,6,7,8");
+
+  yield simulateDrop(cells[0], cells[2]);
+  checkGrid("2p,0p,1p,3,4,5,6,7,8");
+
+  // pinned sites should not be pushed out of the grid (unless there are only
+  // pinned ones left on the grid)
+  setLinks("0,1,2,3,4,5,6,7,8");
+  setPinnedLinks(",,,,,,,7,8");
+
+  yield addNewTabPageTab();
+  checkGrid("0,1,2,3,4,5,6,7p,8p");
+
+  yield simulateDrop(cells[8], cells[2]);
+  checkGrid("0,1,3,4,5,6,7p,8p,2p");
+
+  // make sure that pinned sites are re-positioned correctly
+  setLinks("0,1,2,3,4,5,6,7,8");
+  setPinnedLinks("0,1,2,,,5");
+
+  yield addNewTabPageTab();
+  checkGrid("0p,1p,2p,3,4,5p,6,7,8");
+
+  yield simulateDrop(cells[4], cells[0]);
+  checkGrid("3,1p,2p,4,0p,5p,6,7,8");
+
+  // drag a new site onto the very first cell
+  setLinks("0,1,2,3,4,5,6,7,8");
+  setPinnedLinks(",,,,,,,7,8");
+
+  yield addNewTabPageTab();
+  checkGrid("0,1,2,3,4,5,6,7p,8p");
+
+  yield simulateDrop(cells[0]);
+  checkGrid("99p,0,1,2,3,4,5,7p,8p");
+
+  // drag a new site onto the grid and make sure that pinned cells don't get
+  // pushed out
+  setLinks("0,1,2,3,4,5,6,7,8");
+  setPinnedLinks(",,,,,,,7,8");
+
+  yield addNewTabPageTab();
+  checkGrid("0,1,2,3,4,5,6,7p,8p");
+
+  yield simulateDrop(cells[7]);
+  checkGrid("0,1,2,3,4,5,7p,99p,8p");
+
+  // drag a new site beneath a pinned cell and make sure the pinned cell is
+  // not moved
+  setLinks("0,1,2,3,4,5,6,7,8");
+  setPinnedLinks(",,,,,,,,8");
+
+  yield addNewTabPageTab();
+  checkGrid("0,1,2,3,4,5,6,7,8p");
+
+  yield simulateDrop(cells[7]);
+  checkGrid("0,1,2,3,4,5,6,99p,8p");
+
+  // drag a new site onto a block of pinned sites and make sure they're shifted
+  // around accordingly
+  setLinks("0,1,2,3,4,5,6,7,8");
+  setPinnedLinks("0,1,2,,,,,,");
+
+  yield addNewTabPageTab();
+  checkGrid("0p,1p,2p");
+
+  yield simulateDrop(cells[1]);
+  checkGrid("0p,99p,1p,2p,3,4,5,6,7");
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_drop_preview.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * These tests ensure that the drop preview correctly arranges sites when
+ * dragging them around.
+ */
+function runTests() {
+  // the first three sites are pinned - make sure they're re-arranged correctly
+  setLinks("0,1,2,3,4,5,6,7,8");
+  setPinnedLinks("0,1,2,,,5");
+
+  yield addNewTabPageTab();
+  checkGrid("0p,1p,2p,3,4,5p,6,7,8");
+
+  cw.gDrag._draggedSite = cells[0].site;
+  let sites = cw.gDropPreview.rearrange(cells[4]);
+  cw.gDrag._draggedSite = null;
+
+  checkGrid("3,1p,2p,4,0p,5p,6,7,8", sites);
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_private_browsing.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * These tests ensure that all changes made to the new tab page in private
+ * browsing mode are discarded after switching back to normal mode again.
+ * The private browsing mode should start with the current grid shown in normal
+ * mode.
+ */
+let pb = Cc["@mozilla.org/privatebrowsing;1"]
+         .getService(Ci.nsIPrivateBrowsingService);
+
+function runTests() {
+  // prepare the grid
+  setLinks("0,1,2,3,4,5,6,7,8,9");
+  ok(!pb.privateBrowsingEnabled, "private browsing is disabled");
+
+  yield addNewTabPageTab();
+  pinCell(cells[0]);
+  checkGrid("0p,1,2,3,4,5,6,7,8");
+
+  // enter private browsing mode
+  yield togglePrivateBrowsing();
+  ok(pb.privateBrowsingEnabled, "private browsing is enabled");
+
+  yield addNewTabPageTab();
+  checkGrid("0p,1,2,3,4,5,6,7,8");
+
+  // modify the grid while we're in pb mode
+  yield blockCell(cells[1]);
+  checkGrid("0p,2,3,4,5,6,7,8");
+
+  yield unpinCell(cells[0]);
+  checkGrid("0,2,3,4,5,6,7,8");
+
+  // exit private browsing mode
+  yield togglePrivateBrowsing();
+  ok(!pb.privateBrowsingEnabled, "private browsing is disabled");
+
+  // check that the grid is the same as before entering pb mode
+  yield addNewTabPageTab();
+  checkGrid("0p,1,2,3,4,5,6,7,8");
+}
+
+function togglePrivateBrowsing() {
+  let topic = "private-browsing-transition-complete";
+
+  Services.obs.addObserver(function observe() {
+    Services.obs.removeObserver(observe, topic);
+    executeSoon(TestRunner.next);
+  }, topic, false);
+
+  pb.privateBrowsingEnabled = !pb.privateBrowsingEnabled;
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_reset.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * These tests make sure that resetting the 'New Tage Page' works as expected.
+ */
+function runTests() {
+  // create a new tab page and check its modified state after blocking a site
+  setLinks("0,1,2,3,4,5,6,7,8");
+  setPinnedLinks("");
+
+  yield addNewTabPageTab();
+  let resetButton = cw.document.getElementById("toolbar-button-reset");
+
+  checkGrid("0,1,2,3,4,5,6,7,8");
+  ok(!resetButton.hasAttribute("modified"), "page is not modified");
+
+  yield blockCell(cells[4]);
+  checkGrid("0,1,2,3,5,6,7,8,");
+  ok(resetButton.hasAttribute("modified"), "page is modified");
+
+  yield cw.gToolbar.reset(TestRunner.next);
+  checkGrid("0,1,2,3,4,5,6,7,8");
+  ok(!resetButton.hasAttribute("modified"), "page is not modified");
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_tabsync.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * These tests make sure that all changes that are made to a specific
+ * 'New Tab Page' are synchronized with all other open 'New Tab Pages'
+ * automatically. All about:newtab pages should always be in the same
+ * state.
+ */
+function runTests() {
+  setLinks("0,1,2,3,4,5,6,7,8,9");
+  setPinnedLinks(",1");
+
+  yield addNewTabPageTab();
+  checkGrid("0,1p,2,3,4,5,6,7,8");
+
+  let resetButton = cw.document.getElementById("toolbar-button-reset");
+  ok(!resetButton.hasAttribute("modified"), "page is not modified");
+
+  let oldCw = cw;
+  let oldResetButton = resetButton;
+
+  // create the new tab page
+  yield addNewTabPageTab();
+  checkGrid("0,1p,2,3,4,5,6,7,8");
+
+  resetButton = cw.document.getElementById("toolbar-button-reset");
+  ok(!resetButton.hasAttribute("modified"), "page is not modified");
+
+  // unpin a cell
+  yield unpinCell(cells[1]);
+  checkGrid("0,1,2,3,4,5,6,7,8");
+  checkGrid("0,1,2,3,4,5,6,7,8", oldCw.gGrid.sites);
+
+  // remove a cell
+  yield blockCell(cells[1]);
+  checkGrid("0,2,3,4,5,6,7,8,9");
+  checkGrid("0,2,3,4,5,6,7,8,9", oldCw.gGrid.sites);
+  ok(resetButton.hasAttribute("modified"), "page is modified");
+  ok(oldResetButton.hasAttribute("modified"), "page is modified");
+
+  // insert a new cell by dragging
+  yield simulateDrop(cells[1]);
+  checkGrid("0,99p,2,3,4,5,6,7,8");
+  checkGrid("0,99p,2,3,4,5,6,7,8", oldCw.gGrid.sites);
+
+  // drag a cell around
+  yield simulateDrop(cells[1], cells[2]);
+  checkGrid("0,2p,99p,3,4,5,6,7,8");
+  checkGrid("0,2p,99p,3,4,5,6,7,8", oldCw.gGrid.sites);
+
+  // reset the new tab page
+  yield cw.gToolbar.reset(TestRunner.next);
+  checkGrid("0,1,2,3,4,5,6,7,8");
+  checkGrid("0,1,2,3,4,5,6,7,8", oldCw.gGrid.sites);
+  ok(!resetButton.hasAttribute("modified"), "page is not modified");
+  ok(!oldResetButton.hasAttribute("modified"), "page is not modified");
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_unpin.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * These tests make sure that when a site gets unpinned it is either moved to
+ * its actual place in the grid or removed in case it's not on the grid anymore.
+ */
+function runTests() {
+  // we have a pinned link that didn't change its position since it was pinned.
+  // nothing should happend when we unpin it.
+  setLinks("0,1,2,3,4,5,6,7,8");
+  setPinnedLinks(",1");
+
+  yield addNewTabPageTab();
+  checkGrid("0,1p,2,3,4,5,6,7,8");
+
+  yield unpinCell(cells[1]);
+  checkGrid("0,1,2,3,4,5,6,7,8");
+
+  // we have a pinned link that is not anymore in the list of the most-visited
+  // links. this should disappear, the remaining links adjust their positions
+  // and a new link will appear at the end of the grid.
+  setLinks("0,1,2,3,4,5,6,7,8");
+  setPinnedLinks(",99");
+
+  yield addNewTabPageTab();
+  checkGrid("0,99p,1,2,3,4,5,6,7");
+
+  yield unpinCell(cells[1]);
+  checkGrid("0,1,2,3,4,5,6,7,8");
+
+  // we have a pinned link that changed its position since it was pinned. it
+  // should be moved to its new position after being unpinned.
+  setLinks("0,1,2,3,4,5,6,7");
+  setPinnedLinks(",1,,,,,,,0");
+
+  yield addNewTabPageTab();
+  checkGrid("2,1p,3,4,5,6,7,,0p");
+
+  yield unpinCell(cells[1]);
+  checkGrid("1,2,3,4,5,6,7,,0p");
+
+  yield unpinCell(cells[8]);
+  checkGrid("0,1,2,3,4,5,6,7,");
+
+  // we have pinned link that changed its position since it was pinned. the
+  // link will disappear from the grid because it's now a much lower priority
+  setLinks("0,1,2,3,4,5,6,7,8,9");
+  setPinnedLinks("9");
+
+  yield addNewTabPageTab();
+  checkGrid("9p,0,1,2,3,4,5,6,7");
+
+  yield unpinCell(cells[0]);
+  checkGrid("0,1,2,3,4,5,6,7,8");
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/newtab/head.js
@@ -0,0 +1,262 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const PREF_NEWTAB_ENABLED = "browser.newtabpage.enabled";
+
+Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, true);
+
+Cu.import("resource:///modules/NewTabUtils.jsm");
+
+registerCleanupFunction(function () {
+  reset();
+
+  while (gBrowser.tabs.length > 1)
+    gBrowser.removeTab(gBrowser.tabs[1]);
+
+  Services.prefs.clearUserPref(PREF_NEWTAB_ENABLED);
+});
+
+/**
+ * Global variables that are accessed by tests.
+ */
+let cw;
+let cells;
+
+/**
+ * We'll want to restore the original links provider later.
+ */
+let originalProvider = NewTabUtils.links._provider;
+
+/**
+ * Provide the default test function to start our test runner.
+ */
+function test() {
+  TestRunner.run();
+}
+
+/**
+ * The test runner that controls the execution flow of our tests.
+ */
+let TestRunner = {
+  /**
+   * Starts the test runner.
+   */
+  run: function () {
+    waitForExplicitFinish();
+
+    this._iter = runTests();
+    this.next();
+  },
+
+  /**
+   * Runs the next available test or finishes if there's no test left.
+   */
+  next: function () {
+    try {
+      TestRunner._iter.next();
+    } catch (e if e instanceof StopIteration) {
+      finish();
+    }
+  }
+};
+
+/**
+ * Allows to provide a list of links that is used to construct the grid.
+ * @param aLinksPattern the pattern (see below)
+ *
+ * Example: setLinks("1,2,3")
+ * Result: [{url: "about:blank#1", title: "site#1"},
+ *          {url: "about:blank#2", title: "site#2"}
+ *          {url: "about:blank#3", title: "site#3"}]
+ */
+function setLinks(aLinksPattern) {
+  let links = aLinksPattern.split(/\s*,\s*/).map(function (id) {
+    return {url: "about:blank#" + id, title: "site#" + id};
+  });
+
+  NewTabUtils.links._provider = {getLinks: function (c) c(links)};
+  NewTabUtils.links._links = links;
+}
+
+/**
+ * Allows to specify the list of pinned links (that have a fixed position in
+ * the grid.
+ * @param aLinksPattern the pattern (see below)
+ *
+ * Example: setPinnedLinks("3,,1")
+ * Result: 'about:blank#3' is pinned in the first cell. 'about:blank#1' is
+ *         pinned in the third cell.
+ */
+function setPinnedLinks(aLinksPattern) {
+  let pinnedLinks = [];
+
+  aLinksPattern.split(/\s*,\s*/).forEach(function (id, index) {
+    let link;
+
+    if (id)
+      link = {url: "about:blank#" + id, title: "site#" + id};
+
+    pinnedLinks[index] = link;
+  });
+
+  // Inject the list of pinned links to work with.
+  NewTabUtils.pinnedLinks._links = pinnedLinks;
+}
+
+/**
+ * Resets the lists of blocked and pinned links and clears the storage.
+ */
+function reset() {
+  NewTabUtils.reset();
+
+  // Restore the old provider to prevent memory leaks.
+  NewTabUtils.links._provider = originalProvider;
+}
+
+/**
+ * Creates a new tab containing 'about:newtab'.
+ */
+function addNewTabPageTab() {
+  let tab = gBrowser.selectedTab = gBrowser.addTab("about:newtab");
+  let browser = tab.linkedBrowser;
+
+  // Wait for the new tab page to be loaded.
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+
+    cw = browser.contentWindow;
+
+    if (NewTabUtils.allPages.enabled) {
+      cells = cw.gGrid.cells;
+
+      // Continue when the link cache has been populated.
+      NewTabUtils.links.populateCache(TestRunner.next);
+    } else {
+      TestRunner.next();
+    }
+
+  }, true);
+}
+
+/**
+ * Compares the current grid arrangement with the given pattern.
+ * @param the pattern (see below)
+ * @param the array of sites to compare with (optional)
+ *
+ * Example: checkGrid("3p,2,,1p")
+ * Result: We expect the first cell to contain the pinned site 'about:blank#3'.
+ *         The second cell contains 'about:blank#2'. The third cell is empty.
+ *         The fourth cell contains the pinned site 'about:blank#4'.
+ */
+function checkGrid(aSitesPattern, aSites) {
+  let valid = true;
+
+  aSites = aSites || cw.gGrid.sites;
+
+  aSitesPattern.split(/\s*,\s*/).forEach(function (id, index) {
+    let site = aSites[index];
+    let match = id.match(/^\d+/);
+
+    // We expect the cell to be empty.
+    if (!match) {
+      if (site) {
+        valid = false;
+        ok(false, "expected cell#" + index + " to be empty");
+      }
+
+      return;
+    }
+
+    // We expect the cell to contain a site.
+    if (!site) {
+      valid = false;
+      ok(false, "didn't expect cell#" + index + " to be empty");
+
+      return;
+    }
+
+    let num = match[0];
+
+    // Check the site's url.
+    if (site.url != "about:blank#" + num) {
+      valid = false;
+      is(site.url, "about:blank#" + num, "cell#" + index + " has the wrong url");
+    }
+
+    let shouldBePinned = /p$/.test(id);
+    let cellContainsPinned = site.isPinned();
+    let cssClassPinned = site.node && site.node.hasAttribute("pinned");
+
+    // Check if the site should be and is pinned.
+    if (shouldBePinned) {
+      if (!cellContainsPinned) {
+        valid = false;
+        ok(false, "expected cell#" + index + " to be pinned");
+      } else if (!cssClassPinned) {
+        valid = false;
+        ok(false, "expected cell#" + index + " to have css class 'pinned'");
+      }
+    } else {
+      if (cellContainsPinned) {
+        valid = false;
+        ok(false, "didn't expect cell#" + index + " to be pinned");
+      } else if (cssClassPinned) {
+        valid = false;
+        ok(false, "didn't expect cell#" + index + " to have css class 'pinned'");
+      }
+    }
+  });
+
+  // If every test passed, say so.
+  if (valid)
+    ok(true, "grid status = " + aSitesPattern);
+}
+
+/**
+ * Blocks the given cell's site from the grid.
+ * @param aCell the cell that contains the site to block
+ */
+function blockCell(aCell) {
+  aCell.site.block(function () executeSoon(TestRunner.next));
+}
+
+/**
+ * Pins a given cell's site on a given position.
+ * @param aCell the cell that contains the site to pin
+ * @param aIndex the index the defines where the site should be pinned
+ */
+function pinCell(aCell, aIndex) {
+  aCell.site.pin(aIndex);
+}
+
+/**
+ * Unpins the given cell's site.
+ * @param aCell the cell that contains the site to unpin
+ */
+function unpinCell(aCell) {
+  aCell.site.unpin(function () executeSoon(TestRunner.next));
+}
+
+/**
+ * Simulates a drop and drop operation.
+ * @param aDropTarget the cell that is the drop target
+ * @param aDragSource the cell that contains the dragged site (optional)
+ */
+function simulateDrop(aDropTarget, aDragSource) {
+  let event = {
+    dataTransfer: {
+      mozUserCancelled: false,
+      setData: function () null,
+      setDragImage: function () null,
+      getData: function () "about:blank#99\nblank"
+    }
+  };
+
+  if (aDragSource)
+    cw.gDrag.start(aDragSource.site, event);
+
+  cw.gDrop.drop(aDropTarget, event, function () executeSoon(TestRunner.next));
+
+  if (aDragSource)
+    cw.gDrag.end(aDragSource.site);
+}
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -36,21 +36,33 @@
 # 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 *****
 
 // Services = object with smart getters for common XPCOM services
 Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "BROWSER_NEW_TAB_URL", function () {
+  return Services.prefs.getCharPref("browser.newtab.url") || "about:blank";
+});
 
 var TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
 
 var gBidiUI = false;
 
+/**
+ * Determines whether the given url is considered a special URL for new tabs.
+ */
+function isBlankPageURL(aURL) {
+  return aURL == "about:blank" || aURL == BROWSER_NEW_TAB_URL;
+}
+
 function getBrowserURL()
 {
   return "chrome://browser/content/browser.xul";
 }
 
 function getTopWin(skipPopups) {
   // If this is called in a browser window, use that window regardless of
   // whether it's the frontmost window, since commands can be executed in
@@ -294,17 +306,17 @@ function openLinkIn(url, where, params) 
   // resulted in a new frontmost window (e.g. "javascript:window.open('');").
   var fm = Components.classes["@mozilla.org/focus-manager;1"].
              getService(Components.interfaces.nsIFocusManager);
   if (window == fm.activeWindow)
     w.content.focus();
   else
     w.gBrowser.selectedBrowser.focus();
 
-  if (!loadInBackground && url == "about:blank")
+  if (!loadInBackground && isBlankPageURL(url))
     w.focusAndSelectUrlBar();
 }
 
 // Used as an onclick handler for UI elements with link-like behavior.
 // e.g. onclick="checkForMiddleClick(this, event);"
 function checkForMiddleClick(node, event) {
   // We should be using the disabled property here instead of the attribute,
   // but some elements that this function is used with don't support it (e.g.
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -26,16 +26,19 @@ browser.jar:
         content/browser/aboutRobots-icon.png          (content/aboutRobots-icon.png)
         content/browser/aboutRobots-widget-left.png   (content/aboutRobots-widget-left.png)
 *       content/browser/browser.css                   (content/browser.css)
 *       content/browser/browser.js                    (content/browser.js)
 *       content/browser/browser.xul                   (content/browser.xul)
 *       content/browser/browser-tabPreviews.xml       (content/browser-tabPreviews.xml)
 *       content/browser/content.js                    (content/content.js)
 *       content/browser/fullscreen-video.xhtml        (content/fullscreen-video.xhtml)
+*       content/browser/newtab/newTab.xul             (content/newtab/newTab.xul)
+*       content/browser/newtab/newTab.js              (content/newtab/newTab.js)
+        content/browser/newtab/newTab.css             (content/newtab/newTab.css)
 *       content/browser/pageinfo/pageInfo.xul         (content/pageinfo/pageInfo.xul)
 *       content/browser/pageinfo/pageInfo.js          (content/pageinfo/pageInfo.js)
 *       content/browser/pageinfo/pageInfo.css         (content/pageinfo/pageInfo.css)
 *       content/browser/pageinfo/pageInfo.xml         (content/pageinfo/pageInfo.xml)
 *       content/browser/pageinfo/feeds.js             (content/pageinfo/feeds.js)
 *       content/browser/pageinfo/feeds.xml            (content/pageinfo/feeds.xml)
 *       content/browser/pageinfo/permissions.js       (content/pageinfo/permissions.js)
 *       content/browser/pageinfo/security.js          (content/pageinfo/security.js)
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -100,16 +100,18 @@ static RedirEntry kRedirMap[] = {
   { "sync-progress", "chrome://browser/content/syncProgress.xhtml",
     nsIAboutModule::ALLOW_SCRIPT },
   { "sync-tabs", "chrome://browser/content/aboutSyncTabs.xul",
     nsIAboutModule::ALLOW_SCRIPT },
 #endif
   { "home", "chrome://browser/content/aboutHome.xhtml",
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::ALLOW_SCRIPT },
+  { "newtab", "chrome://browser/content/newtab/newTab.xul",
+    nsIAboutModule::ALLOW_SCRIPT },
   { "permissions", "chrome://browser/content/preferences/aboutPermissions.xul",
     nsIAboutModule::ALLOW_SCRIPT },
 };
 static const int kRedirTotal = NS_ARRAY_LENGTH(kRedirMap);
 
 static nsCAutoString
 GetAboutModuleName(nsIURI *aURI)
 {
--- a/browser/components/build/nsModule.cpp
+++ b/browser/components/build/nsModule.cpp
@@ -138,16 +138,17 @@ static const mozilla::Module::ContractID
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "rights", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "robots", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "sessionrestore", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
 #ifdef MOZ_SERVICES_SYNC
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "sync-tabs", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "sync-progress", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
 #endif
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "home", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
+    { NS_ABOUT_MODULE_CONTRACTID_PREFIX "newtab", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "permissions", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
 #if defined(XP_WIN) && !defined(__MINGW32__)
     { NS_BROWSERPROFILEMIGRATOR_CONTRACTID_PREFIX "ie", &kNS_WINIEPROFILEMIGRATOR_CID },
 #elif defined(XP_MACOSX)
     { NS_SHELLSERVICE_CONTRACTID, &kNS_SHELLSERVICE_CID },
     { NS_BROWSERPROFILEMIGRATOR_CONTRACTID_PREFIX "safari", &kNS_SAFARIPROFILEMIGRATOR_CID },
 #endif
     { NS_PRIVATE_BROWSING_SERVICE_CONTRACTID, &kNS_PRIVATE_BROWSING_SERVICE_WRAPPER_CID },
--- a/browser/components/thumbnails/Makefile.in
+++ b/browser/components/thumbnails/Makefile.in
@@ -13,16 +13,15 @@ EXTRA_COMPONENTS = \
 	BrowserPageThumbs.manifest \
 	PageThumbsProtocol.js \
 	$(NULL)
 
 EXTRA_PP_JS_MODULES = \
 	PageThumbs.jsm \
 	$(NULL)
 
-# FIXME Bug 721422 - Re-enable tests and make them work with URI_DANGEROUS_TO_LOAD
-#ifdef ENABLE_TESTS
-#	DIRS += test
-#endif
+ifdef ENABLE_TESTS
+	DIRS += test
+endif
 
 include $(topsrcdir)/config/rules.mk
 
 XPIDL_FLAGS += -I$(topsrcdir)/browser/components/
--- a/browser/components/thumbnails/test/Makefile.in
+++ b/browser/components/thumbnails/test/Makefile.in
@@ -7,15 +7,14 @@ topsrcdir	= @top_srcdir@
 srcdir		= @srcdir@
 VPATH		= @srcdir@
 relativesrcdir  = browser/components/thumbnails/test
 
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _BROWSER_FILES = \
-	browser_thumbnails_cache.js \
 	browser_thumbnails_capture.js \
 	head.js \
 	$(NULL)
 
 libs::	$(_BROWSER_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
deleted file mode 100644
--- a/browser/components/thumbnails/test/browser_thumbnails_cache.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/**
- * These tests ensure that saving a thumbnail to the cache works. They also
- * retrieve the thumbnail and display it using an <img> element to compare
- * its pixel colors.
- */
-function runTests() {
-  // Create a new tab with a red background.
-  yield addTab("data:text/html,<body bgcolor=ff0000></body>");
-  let cw = gBrowser.selectedTab.linkedBrowser.contentWindow;
-
-  // Capture a thumbnail for the tab.
-  let canvas = PageThumbs.capture(cw);
-
-  // Store the tab into the thumbnail cache.
-  yield PageThumbs.store("key", canvas, next);
-
-  let {width, height} = canvas;
-  let thumb = PageThumbs.getThumbnailURL("key", width, height);
-
-  // Create a new tab with an image displaying the previously stored thumbnail.
-  yield addTab("data:text/html,<img src='" + thumb + "'/>" + 
-               "<canvas width=" + width + " height=" + height + "/>");
-
-  cw = gBrowser.selectedTab.linkedBrowser.contentWindow;
-  let [img, canvas] = cw.document.querySelectorAll("img, canvas");
-
-  // Draw the image to a canvas and compare the pixel color values.
-  let ctx = canvas.getContext("2d");
-  ctx.drawImage(img, 0, 0, width, height);
-  checkCanvasColor(ctx, 255, 0, 0, "we have a red image and canvas");
-}
--- a/browser/components/thumbnails/test/browser_thumbnails_capture.js
+++ b/browser/components/thumbnails/test/browser_thumbnails_capture.js
@@ -1,38 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
- * These tests ensure that capturing a site's screenshot to a canvas actually
- * works.
+ * These tests ensure that capturing a sites's thumbnail, saving it and
+ * retrieving it from the cache works.
  */
 function runTests() {
   // Create a tab with a red background.
   yield addTab("data:text/html,<body bgcolor=ff0000></body>");
-  checkCurrentThumbnailColor(255, 0, 0, "we have a red thumbnail");
+  yield captureAndCheckColor(255, 0, 0, "we have a red thumbnail");
 
   // Load a page with a green background.
   yield navigateTo("data:text/html,<body bgcolor=00ff00></body>");
-  checkCurrentThumbnailColor(0, 255, 0, "we have a green thumbnail");
+  yield captureAndCheckColor(0, 255, 0, "we have a green thumbnail");
 
   // Load a page with a blue background.
   yield navigateTo("data:text/html,<body bgcolor=0000ff></body>");
-  checkCurrentThumbnailColor(0, 0, 255, "we have a blue thumbnail");
+  yield captureAndCheckColor(0, 0, 255, "we have a blue thumbnail");
 }
-
-/**
- * Captures a thumbnail of the currently selected tab and checks the color of
- * the resulting canvas.
- * @param aRed The red component's intensity.
- * @param aGreen The green component's intensity.
- * @param aBlue The blue component's intensity.
- * @param aMessage The info message to print when checking the pixel color.
- */
-function checkCurrentThumbnailColor(aRed, aGreen, aBlue, aMessage) {
-  let tab = gBrowser.selectedTab;
-  let cw = tab.linkedBrowser.contentWindow;
-
-  let canvas = PageThumbs.capture(cw);
-  let ctx = canvas.getContext("2d");
-
-  checkCanvasColor(ctx, aRed, aGreen, aBlue, aMessage);
-}
--- a/browser/components/thumbnails/test/head.js
+++ b/browser/components/thumbnails/test/head.js
@@ -3,16 +3,18 @@
 
 Cu.import("resource:///modules/PageThumbs.jsm");
 
 registerCleanupFunction(function () {
   while (gBrowser.tabs.length > 1)
     gBrowser.removeTab(gBrowser.tabs[1]);
 });
 
+let cachedXULDocument;
+
 /**
  * Provide the default test function to start our test runner.
  */
 function test() {
   TestRunner.run();
 }
 
 /**
@@ -49,39 +51,105 @@ function next() {
 }
 
 /**
  * Creates a new tab with the given URI.
  * @param aURI The URI that's loaded in the tab.
  */
 function addTab(aURI) {
   let tab = gBrowser.selectedTab = gBrowser.addTab(aURI);
-  whenBrowserLoaded(tab.linkedBrowser);
+  whenLoaded(tab.linkedBrowser);
 }
 
 /**
  * Loads a new URI into the currently selected tab.
  * @param aURI The URI to load.
  */
 function navigateTo(aURI) {
   let browser = gBrowser.selectedTab.linkedBrowser;
-  whenBrowserLoaded(browser);
+  whenLoaded(browser);
   browser.loadURI(aURI);
 }
 
 /**
- * Continues the current test execution when a load event for the given browser
- * has been received
- * @param aBrowser The browser to listen on.
+ * Continues the current test execution when a load event for the given element
+ * has been received.
+ * @param aElement The DOM element to listen on.
+ * @param aCallback The function to call when the load event was dispatched.
+ */
+function whenLoaded(aElement, aCallback) {
+  aElement.addEventListener("load", function onLoad() {
+    aElement.removeEventListener("load", onLoad, true);
+    executeSoon(aCallback || next);
+  }, true);
+}
+
+/**
+ * Captures a screenshot for the currently selected tab, stores it in the cache,
+ * retrieves it from the cache and compares pixel color values.
+ * @param aRed The red component's intensity.
+ * @param aGreen The green component's intensity.
+ * @param aBlue The blue component's intensity.
+ * @param aMessage The info message to print when comparing the pixel color.
  */
-function whenBrowserLoaded(aBrowser) {
-  aBrowser.addEventListener("load", function onLoad() {
-    aBrowser.removeEventListener("load", onLoad, true);
-    executeSoon(next);
-  }, true);
+function captureAndCheckColor(aRed, aGreen, aBlue, aMessage) {
+  let window = gBrowser.selectedTab.linkedBrowser.contentWindow;
+
+  let key = Date.now();
+  let data = PageThumbs.capture(window);
+
+  // Store the thumbnail in the cache.
+  PageThumbs.store(key, data, function () {
+    let width = 100, height = 100;
+    let thumb = PageThumbs.getThumbnailURL(key, width, height);
+
+    getXULDocument(function (aDocument) {
+      let htmlns = "http://www.w3.org/1999/xhtml";
+      let img = aDocument.createElementNS(htmlns, "img");
+      img.setAttribute("src", thumb);
+
+      whenLoaded(img, function () {
+        let canvas = aDocument.createElementNS(htmlns, "canvas");
+        canvas.setAttribute("width", width);
+        canvas.setAttribute("height", height);
+
+        // Draw the image to a canvas and compare the pixel color values.
+        let ctx = canvas.getContext("2d");
+        ctx.drawImage(img, 0, 0, width, height);
+        checkCanvasColor(ctx, aRed, aGreen, aBlue, aMessage);
+
+        next();
+      });
+    });
+  });
+}
+
+/**
+ * Passes a XUL document (created if necessary) to the given callback.
+ * @param aCallback The function to be called when the XUL document has been
+ *                  created. The first argument will be the document.
+ */
+function getXULDocument(aCallback) {
+  let hiddenWindow = Services.appShell.hiddenDOMWindow;
+  let doc = cachedXULDocument || hiddenWindow.document;
+
+  if (doc instanceof XULDocument) {
+    aCallback(cachedXULDocument = doc);
+    return;
+  }
+
+  let iframe = doc.createElement("iframe");
+  iframe.setAttribute("src", "chrome://global/content/mozilla.xhtml");
+
+  iframe.addEventListener("DOMContentLoaded", function onLoad() {
+    iframe.removeEventListener("DOMContentLoaded", onLoad, false);
+    aCallback(cachedXULDocument = iframe.contentDocument);
+  }, false);
+
+  doc.body.appendChild(iframe);
 }
 
 /**
  * Checks the top-left pixel of a given canvas' 2d context for a given color.
  * @param aContext The 2D context of a canvas.
  * @param aRed The red component's intensity.
  * @param aGreen The green component's intensity.
  * @param aBlue The blue component's intensity.
--- a/browser/devtools/shared/LayoutHelpers.jsm
+++ b/browser/devtools/shared/LayoutHelpers.jsm
@@ -114,32 +114,82 @@ LayoutHelpers = {
 
       frameWin = frameWin.parent;
     }
 
     return rect;
   },
 
   /**
+   * Compute the absolute position and the dimensions of a node, relativalely
+   * to the root window.
+   *
+   * @param nsIDOMNode aNode
+   *        a DOM element to get the bounds for
+   * @param nsIWindow aContentWindow
+   *        the content window holding the node
+   */
+  getRect: function LH_getRect(aNode, aContentWindow) {
+    let frameWin = aNode.ownerDocument.defaultView;
+    let clientRect = aNode.getBoundingClientRect();
+
+    // Go up in the tree of frames to determine the correct rectangle.
+    // clientRect is read-only, we need to be able to change properties.
+    rect = {top: clientRect.top + aContentWindow.pageYOffset,
+            left: clientRect.left + aContentWindow.pageXOffset,
+            width: clientRect.width,
+            height: clientRect.height};
+
+    // We iterate through all the parent windows.
+    while (true) {
+
+      // Are we in the top-level window?
+      if (frameWin.parent === frameWin || !frameWin.frameElement) {
+        break;
+      }
+
+      // We are in an iframe.
+      // We take into account the parent iframe position and its
+      // offset (borders and padding).
+      let frameRect = frameWin.frameElement.getBoundingClientRect();
+
+      let [offsetTop, offsetLeft] =
+        this.getIframeContentOffset(frameWin.frameElement);
+
+      rect.top += frameRect.top + offsetTop;
+      rect.left += frameRect.left + offsetLeft;
+
+      frameWin = frameWin.parent;
+    }
+
+    return rect;
+  },
+
+  /**
    * Returns iframe content offset (iframe border + padding).
    * Note: this function shouldn't need to exist, had the platform provided a
    * suitable API for determining the offset between the iframe's content and
    * its bounding client rect. Bug 626359 should provide us with such an API.
    *
    * @param aIframe
    *        The iframe.
    * @returns array [offsetTop, offsetLeft]
    *          offsetTop is the distance from the top of the iframe and the
    *            top of the content document.
    *          offsetLeft is the distance from the left of the iframe and the
    *            left of the content document.
    */
   getIframeContentOffset: function LH_getIframeContentOffset(aIframe) {
     let style = aIframe.contentWindow.getComputedStyle(aIframe, null);
 
+    // In some cases, the computed style is null
+    if (!style) {
+      return [0, 0];
+    }
+
     let paddingTop = parseInt(style.getPropertyValue("padding-top"));
     let paddingLeft = parseInt(style.getPropertyValue("padding-left"));
 
     let borderTop = parseInt(style.getPropertyValue("border-top-width"));
     let borderLeft = parseInt(style.getPropertyValue("border-left-width"));
 
     return [borderTop + paddingTop, borderLeft + paddingLeft];
   },
--- a/browser/devtools/tilt/Tilt.jsm
+++ b/browser/devtools/tilt/Tilt.jsm
@@ -31,36 +31,54 @@
  * 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 LGPL or the GPL. 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 *****/
-
-/*global Components, Services, TiltGL, TiltUtils, TiltVisualizer */
 "use strict";
 
 const Cu = Components.utils;
 
 // Tilt notifications dispatched through the nsIObserverService.
 const TILT_NOTIFICATIONS = {
 
-  // Fires when Tilt completes the initialization.
+  // Fires when Tilt starts the initialization.
+  INITIALIZING: "tilt-initializing",
+
+  // Fires immediately after initialization is complete.
+  // (when the canvas overlay is visible and the 3D mesh is completely created)
   INITIALIZED: "tilt-initialized",
 
-  // Fires when Tilt is destroyed.
+  // Fires immediately before the destruction is started.
+  DESTROYING: "tilt-destroying",
+
+  // Fires immediately before the destruction is finished.
+  // (just before the canvas overlay is removed from its parent node)
+  BEFORE_DESTROYED: "tilt-before-destroyed",
+
+  // Fires when Tilt is completely destroyed.
   DESTROYED: "tilt-destroyed",
 
   // Fires when Tilt is shown (after a tab-switch).
   SHOWN: "tilt-shown",
 
   // Fires when Tilt is hidden (after a tab-switch).
-  HIDDEN: "tilt-hidden"
+  HIDDEN: "tilt-hidden",
+
+  // Fires once Tilt highlights an element in the page.
+  HIGHLIGHTING: "tilt-highlighting",
+
+  // Fires once Tilt stops highlighting any element.
+  UNHIGHLIGHTING: "tilt-unhighlighting",
+
+  // Fires when a node is removed from the 3D mesh.
+  NODE_REMOVED: "tilt-node-removed"
 };
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/devtools/TiltGL.jsm");
 Cu.import("resource:///modules/devtools/TiltUtils.jsm");
 Cu.import("resource:///modules/devtools/TiltVisualizer.jsm");
 
 let EXPORTED_SYMBOLS = ["Tilt"];
@@ -101,29 +119,30 @@ Tilt.prototype = {
     // if the visualizer for the current tab is already open, destroy it now
     if (this.visualizers[id]) {
       this.destroy(id, true);
       return;
     }
 
     // create a visualizer instance for the current tab
     this.visualizers[id] = new TiltVisualizer({
-      parentNode: this.chromeWindow.gBrowser.selectedBrowser.parentNode,
+      chromeWindow: this.chromeWindow,
       contentWindow: this.chromeWindow.gBrowser.selectedBrowser.contentWindow,
+      parentNode: this.chromeWindow.gBrowser.selectedBrowser.parentNode,
       requestAnimationFrame: this.chromeWindow.mozRequestAnimationFrame,
-      inspectorUI: this.chromeWindow.InspectorUI
+      notifications: this.NOTIFICATIONS
     });
 
     // make sure the visualizer object was initialized properly
     if (!this.visualizers[id].isInitialized()) {
       this.destroy(id);
       return;
     }
 
-    Services.obs.notifyObservers(null, TILT_NOTIFICATIONS.INITIALIZED, null);
+    Services.obs.notifyObservers(null, TILT_NOTIFICATIONS.INITIALIZING, null);
   },
 
   /**
    * Destroys a specific instance of the visualizer.
    *
    * @param {String} aId
    *                 the identifier of the instance in the visualizers array
    * @param {Boolean} aAnimateFlag
@@ -152,33 +171,34 @@ Tilt.prototype = {
       if (!aAnimateFlag) {
         finalize.call(this, aId);
         return;
       }
 
       let controller = this.visualizers[aId].controller;
       let presenter = this.visualizers[aId].presenter;
 
-      TiltUtils.setDocumentZoom(presenter.transforms.zoom);
+      let content = presenter.contentWindow;
+      let pageXOffset = content.pageXOffset * presenter.transforms.zoom;
+      let pageYOffset = content.pageYOffset * presenter.transforms.zoom;
 
-      let content = presenter.contentWindow;
-      let pageXOffset = content.pageXOffset * TiltUtils.getDocumentZoom();
-      let pageYOffset = content.pageYOffset * TiltUtils.getDocumentZoom();
+      Services.obs.notifyObservers(null, TILT_NOTIFICATIONS.DESTROYING, null);
+      TiltUtils.setDocumentZoom(this.chromeWindow, presenter.transforms.zoom);
 
       controller.removeEventListeners();
       controller.arcball.reset([-pageXOffset, -pageYOffset]);
       presenter.executeDestruction(finalize.bind(this, aId));
     }
   },
 
   /**
    * Handles any supplementary post-initialization work, done immediately
-   * after a TILT_NOTIFICATIONS.INITIALIZED notification.
+   * after a TILT_NOTIFICATIONS.INITIALIZING notification.
    */
-  _whenInitialized: function T__whenInitialized()
+  _whenInitializing: function T__whenInitializing()
   {
     this._whenShown();
   },
 
   /**
    * Handles any supplementary post-destruction work, done immediately
    * after a TILT_NOTIFICATIONS.DESTROYED notification.
    */
@@ -245,17 +265,17 @@ Tilt.prototype = {
     // load the preferences from the devtools.tilt branch
     TiltVisualizer.Prefs.load();
 
     // hide the button in the Inspector toolbar if Tilt is not enabled
     this.tiltButton.hidden = !this.enabled;
 
     // add the necessary observers to handle specific notifications
     Services.obs.addObserver(
-      this._whenInitialized.bind(this), TILT_NOTIFICATIONS.INITIALIZED, false);
+      this._whenInitializing.bind(this), TILT_NOTIFICATIONS.INITIALIZING, false);
     Services.obs.addObserver(
       this._whenDestroyed.bind(this), TILT_NOTIFICATIONS.DESTROYED, false);
     Services.obs.addObserver(
       this._whenShown.bind(this), TILT_NOTIFICATIONS.SHOWN, false);
     Services.obs.addObserver(
       this._whenHidden.bind(this), TILT_NOTIFICATIONS.HIDDEN, false);
     Services.obs.addObserver(function(aSubject, aTopic, aWinId) {
       this.destroy(aWinId); }.bind(this),
@@ -279,17 +299,17 @@ Tilt.prototype = {
       this.highlighterContainer.style.display = "";
     }.bind(this);
 
     Services.obs.addObserver(onOpened,
       this.chromeWindow.InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
     Services.obs.addObserver(onClosed,
       this.chromeWindow.InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false);
     Services.obs.addObserver(onOpened,
-      TILT_NOTIFICATIONS.INITIALIZED, false);
+      TILT_NOTIFICATIONS.INITIALIZING, false);
     Services.obs.addObserver(onClosed,
       TILT_NOTIFICATIONS.DESTROYED, false);
 
 
     this._setupFinished = true;
   },
 
   /**
--- a/browser/devtools/tilt/TiltGL.jsm
+++ b/browser/devtools/tilt/TiltGL.jsm
@@ -31,18 +31,16 @@
  * 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 LGPL or the GPL. 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 *****/
-
-/*global Components, Services, TiltMath, TiltUtils, mat4 */
 "use strict";
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 const WEBGL_CONTEXT_NAME = "experimental-webgl";
 
--- a/browser/devtools/tilt/TiltMath.jsm
+++ b/browser/devtools/tilt/TiltMath.jsm
@@ -31,18 +31,16 @@
  * 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 LGPL or the GPL. 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 *****/
-
-/*global Components, TiltUtils */
 "use strict";
 
 const Cu = Components.utils;
 
 Cu.import("resource:///modules/devtools/TiltUtils.jsm");
 
 let EXPORTED_SYMBOLS =
   ["EPSILON", "TiltMath", "vec3", "mat3", "mat4", "quat4"];
--- a/browser/devtools/tilt/TiltUtils.jsm
+++ b/browser/devtools/tilt/TiltUtils.jsm
@@ -31,26 +31,25 @@
  * 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 LGPL or the GPL. 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 *****/
-
-/*global Components, Services, XPCOMUtils */
 "use strict";
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
 
 let EXPORTED_SYMBOLS = ["TiltUtils"];
 
 /**
  * Module containing various helper functions used throughout Tilt.
  */
 let TiltUtils = {};
 
@@ -398,118 +397,16 @@ TiltUtils.DOM = {
   {
     return {
       width: aContentWindow.innerWidth + aContentWindow.scrollMaxX,
       height: aContentWindow.innerHeight + aContentWindow.scrollMaxY
     };
   },
 
   /**
-   * Returns the absolute x, y, width and height coordinates of a node, or null
-   * if the passed node is not an ELEMENT_NODE.
-   *
-   * @param {Element} aNode
-   *                  the node which coordinates need to be calculated
-   * @param {Window} aContentWindow
-   *                 optional, the window content holding the the document
-   *
-   * @return {Object} an object containing the top, left, width, height coords
-   */
-  getNodeCoordinates: function TUD_getNodeCoordinates(aNode, aContentWindow) {
-    // make sure the contentWindow parameter is a valid object
-    aContentWindow = aContentWindow || {};
-
-    if (aNode.nodeType !== 1) { // Node.ELEMENT_NODE
-      return null;
-    }
-
-    let rect = {
-      top: 0,
-      left: 0,
-      width: 0,
-      height: 0
-    };
-
-    // the preferred way of getting the bounding client rectangle
-    let clientRect = aNode.getBoundingClientRect();
-    rect.top = clientRect.top + aContentWindow.pageYOffset;
-    rect.left = clientRect.left + aContentWindow.pageXOffset;
-    rect.width = clientRect.width;
-    rect.height = clientRect.height;
-
-    // compute the iframe position and its offset if necessary
-    let frameRect = this.getFrameOffset(
-      aNode.ownerDocument.defaultView.frameElement, aContentWindow);
-
-    if (frameRect) {
-      rect.top += frameRect.top;
-      rect.left += frameRect.left;
-    }
-
-    return rect;
-  },
-
-  /**
-   * Retuns the parent iframe position and its offset (borders and padding),
-   * or null if the passed frame is not valid.
-   *
-   * @param {Element} aNode
-   *                  the iframe which offset need to be calculated
-   * @param {Window} aContentWindow
-   *                 optional, the window content holding the the document
-   *
-   * @return {Object} an object containing the top and left coords
-   */
-  getFrameOffset: (function() {
-    let cache = {};
-
-    return function TUD_getFrameOffset(aFrame, aContentWindow) {
-      // make sure the contentWindow parameter is a valid object
-      aContentWindow = aContentWindow || {};
-
-      if (!aFrame) {
-        return null;
-      }
-
-      let id = TiltUtils.getWindowId(aFrame.contentWindow) + "," +
-        aContentWindow.pageXOffset || 0 + "," +
-        aContentWindow.pageYOffset || 0;
-
-      // check the cache to see if this iframe offset wasn't calculated already
-      if (cache[id] !== undefined) {
-        return cache[id];
-      }
-
-      let offset = {
-        top: 0,
-        left: 0
-      };
-
-      // take the parent iframe bounding rect position into account
-      let frameRect = aFrame.getBoundingClientRect();
-      offset.top = frameRect.top;
-      offset.left = frameRect.left;
-
-      // compute the iframe content offset (iframe border + padding)
-      // bug #626359
-      let style = aFrame.contentWindow.getComputedStyle(aFrame, null);
-      if (style) {
-        offset.top +=
-          parseInt(style.getPropertyValue("padding-top")) +
-          parseInt(style.getPropertyValue("border-top-width"));
-        offset.left +=
-          parseInt(style.getPropertyValue("padding-left")) +
-          parseInt(style.getPropertyValue("border-left-width"));
-      }
-
-      return (cache[id] = offset);
-    };
-  }()),
-
-  /**
    * Traverses a document object model & calculates useful info for each node.
    *
    * @param {Window} aContentWindow
    *                 the window content holding the document
    * @param {Object} aProperties
    *                 optional, an object containing the following properties:
    *        {Object} invisibleElements
    *                 elements which should be ignored
@@ -544,17 +441,17 @@ TiltUtils.DOM = {
 
         // skip some nodes to avoid visualization meshes that are too bloated
         let name = node.localName;
         if (!name || aInvisibleElements[name]) {
           continue;
         }
 
         // get the x, y, width and height coordinates of the node
-        let coord = this.getNodeCoordinates(node, aContentWindow);
+        let coord = LayoutHelpers.getRect(node, aContentWindow);
         if (!coord) {
           continue;
         }
 
         // the maximum size slices the traversal where needed
         if (coord.left > aMaxX || coord.top > aMaxY) {
           continue;
         }
@@ -627,28 +524,16 @@ TiltUtils.destroyObject = function TU_de
   for (let i in aScope) {
     if (aScope.hasOwnProperty(i)) {
       delete aScope[i];
     }
   }
 };
 
 /**
- * Gets the most recent browser window.
- *
- * @return {Window} the window
- */
-TiltUtils.getBrowserWindow = function TU_getBrowserWindow()
-{
-  return Cc["@mozilla.org/appshell/window-mediator;1"]
-    .getService(Ci.nsIWindowMediator)
-    .getMostRecentWindow("navigator:browser");
-};
-
-/**
  * Retrieve the unique ID of a window object.
  *
  * @param {Window} aWindow
  *                 the window to get the ID from
  *
  * @return {Number} the window ID
  */
 TiltUtils.getWindowId = function TU_getWindowId(aWindow)
@@ -660,42 +545,48 @@ TiltUtils.getWindowId = function TU_getW
   return aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                 .getInterface(Ci.nsIDOMWindowUtils)
                 .currentInnerWindowID;
 };
 
 /**
  * Gets the markup document viewer zoom for the currently selected browser.
  *
+ * @param {Window} aChromeWindow
+ *                 the top-level browser window
+ *
  * @return {Number} the zoom ammount
  */
-TiltUtils.getDocumentZoom = function TU_getDocumentZoom() {
-  return TiltUtils.getBrowserWindow()
-                  .gBrowser.selectedBrowser.markupDocumentViewer.fullZoom;
+TiltUtils.getDocumentZoom = function TU_getDocumentZoom(aChromeWindow) {
+  return aChromeWindow.gBrowser.selectedBrowser.markupDocumentViewer.fullZoom;
 };
 
 /**
  * Sets the markup document viewer zoom for the currently selected browser.
  *
+ * @param {Window} aChromeWindow
+ *                 the top-level browser window
+ *
  * @param {Number} the zoom ammount
  */
-TiltUtils.setDocumentZoom = function TU_getDocumentZoom(aZoom) {
-  TiltUtils.getBrowserWindow()
-           .gBrowser.selectedBrowser.markupDocumentViewer.fullZoom = aZoom;
+TiltUtils.setDocumentZoom = function TU_setDocumentZoom(aChromeWindow, aZoom) {
+  aChromeWindow.gBrowser.selectedBrowser.markupDocumentViewer.fullZoom = aZoom;
 };
 
 /**
  * Performs a garbage collection.
+ *
+ * @param {Window} aChromeWindow
+ *                 the top-level browser window
  */
-TiltUtils.gc = function TU_gc()
+TiltUtils.gc = function TU_gc(aChromeWindow)
 {
-  TiltUtils.getBrowserWindow()
-           .QueryInterface(Ci.nsIInterfaceRequestor)
-           .getInterface(Ci.nsIDOMWindowUtils)
-           .garbageCollect();
+  aChromeWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIDOMWindowUtils)
+               .garbageCollect();
 };
 
 /**
  * Clears the cache and sets all the variables to null.
  */
 TiltUtils.clearCache = function TU_clearCache()
 {
   TiltUtils.DOM.parentNode = null;
--- a/browser/devtools/tilt/TiltVisualizer.jsm
+++ b/browser/devtools/tilt/TiltVisualizer.jsm
@@ -31,19 +31,16 @@
  * 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 LGPL or the GPL. 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 *****/
-
-/*global Components, ChromeWorker */
-/*global TiltGL, TiltMath, EPSILON, vec3, mat4, quat4, TiltUtils */
 "use strict";
 
 const Cu = Components.utils;
 const Ci = Components.interfaces;
 
 const ELEMENT_MIN_SIZE = 4;
 const INVISIBLE_ELEMENTS = {
   "head": true,
@@ -73,55 +70,63 @@ const ARCBALL_ZOOM_STEP = 0.1;
 const ARCBALL_ZOOM_MIN = -3000;
 const ARCBALL_ZOOM_MAX = 500;
 const ARCBALL_RESET_FACTOR = 0.9;
 const ARCBALL_RESET_INTERVAL = 1000 / 60;
 
 const TILT_CRAFTER = "resource:///modules/devtools/TiltWorkerCrafter.js";
 const TILT_PICKER = "resource:///modules/devtools/TiltWorkerPicker.js";
 
+Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/devtools/TiltGL.jsm");
 Cu.import("resource:///modules/devtools/TiltMath.jsm");
 Cu.import("resource:///modules/devtools/TiltUtils.jsm");
 Cu.import("resource:///modules/devtools/TiltVisualizerStyle.jsm");
 
 let EXPORTED_SYMBOLS = ["TiltVisualizer"];
 
 /**
  * Initializes the visualization presenter and controller.
  *
  * @param {Object} aProperties
  *                 an object containing the following properties:
- *       {Element} parentNode: the parent node to hold the visualization
+ *        {Window} chromeWindow: a reference to the top level window
  *        {Window} contentWindow: the content window holding the visualized doc
+ *       {Element} parentNode: the parent node to hold the visualization
  *      {Function} requestAnimationFrame: responsible with scheduling loops
- *   {InspectorUI} inspectorUI: necessary instance of the InspectorUI
+ *        {Object} notifications: necessary notifications for Tilt
  *      {Function} onError: optional, function called if initialization failed
  *      {Function} onLoad: optional, function called if initialization worked
  */
 function TiltVisualizer(aProperties)
 {
   // make sure the properties parameter is a valid object
   aProperties = aProperties || {};
 
   /**
+   * Save a reference to the top-level window.
+   */
+  this.chromeWindow = aProperties.chromeWindow;
+
+  /**
    * The canvas element used for rendering the visualization.
    */
   this.canvas = TiltUtils.DOM.initCanvas(aProperties.parentNode, {
     focusable: true,
     append: true
   });
 
   /**
    * Visualization logic and drawing loop.
    */
   this.presenter = new TiltVisualizer.Presenter(this.canvas,
+    aProperties.chromeWindow,
     aProperties.contentWindow,
     aProperties.requestAnimationFrame,
-    aProperties.inspectorUI,
+    aProperties.notifications,
     aProperties.onError || null,
     aProperties.onLoad || null);
 
   /**
    * Visualization mouse and keyboard controller.
    */
   this.controller = new TiltVisualizer.Controller(this.canvas, this.presenter);
 }
@@ -155,46 +160,66 @@ TiltVisualizer.prototype = {
   cleanup: function TV_cleanup()
   {
     if (this.controller) {
       TiltUtils.destroyObject(this.controller);
     }
     if (this.presenter) {
       TiltUtils.destroyObject(this.presenter);
     }
+
+    let chromeWindow = this.chromeWindow;
+
     TiltUtils.destroyObject(this);
     TiltUtils.clearCache();
-    TiltUtils.gc();
+    TiltUtils.gc(chromeWindow);
   }
 };
 
 /**
  * This object manages the visualization logic and drawing loop.
  *
  * @param {HTMLCanvasElement} aCanvas
  *                            the canvas element used for rendering
- * @param {Object} aContentWindow
+ * @param {Window} aChromeWindow
+ *                 a reference to the top-level window
+ * @param {Window} aContentWindow
  *                 the content window holding the document to be visualized
  * @param {Function} aRequestAnimationFrame
  *                   function responsible with scheduling loop frames
- * @param {InspectorUI} aInspectorUI
- *                      necessary instance of the InspectorUI
+ * @param {Object} aNotifications
+ *                 necessary notifications for Tilt
  * @param {Function} onError
  *                   function called if initialization failed
  * @param {Function} onLoad
  *                   function called if initialization worked
  */
 TiltVisualizer.Presenter = function TV_Presenter(
-  aCanvas, aContentWindow, aRequestAnimationFrame, aInspectorUI,
+  aCanvas, aChromeWindow, aContentWindow, aRequestAnimationFrame, aNotifications,
   onError, onLoad)
 {
+  /**
+   * A canvas overlay used for drawing the visualization.
+   */
   this.canvas = aCanvas;
+
+  /**
+   * Save a reference to the top-level window, to access InspectorUI or Tilt.
+   */
+  this.chromeWindow = aChromeWindow;
+
+  /**
+   * The content window generating the visualization
+   */
   this.contentWindow = aContentWindow;
-  this.inspectorUI = aInspectorUI;
-  this.tiltUI = aInspectorUI.chromeWin.Tilt;
+
+  /**
+   * Shortcut for accessing notifications strings.
+   */
+  this.NOTIFICATIONS = aNotifications;
 
   /**
    * Create the renderer, containing useful functions for easy drawing.
    */
   this.renderer = new TiltGL.Renderer(aCanvas, onError, onLoad);
 
   /**
    * A custom shader used for drawing the visualization mesh.
@@ -220,27 +245,28 @@ TiltVisualizer.Presenter = function TV_P
     v3: vec3.create()
   };
 
   /**
    * Scene transformations, exposing offset, translation and rotation.
    * Modified by events in the controller through delegate functions.
    */
   this.transforms = {
-    zoom: TiltUtils.getDocumentZoom(),
+    zoom: TiltUtils.getDocumentZoom(aChromeWindow),
     offset: vec3.create(),      // mesh offset, aligned to the viewport center
     translation: vec3.create(), // scene translation, on the [x, y, z] axis
     rotation: quat4.create()    // scene rotation, expressed as a quaternion
   };
 
   /**
    * Variables holding information about the initial and current node selected.
    */
+  this._currentSelection = -1; // the selected node index
   this._initialSelection = false; // true if an initial selection was made
-  this._currentSelection = -1; // the selected node index
+  this._initialMeshConfiguration = false; // true if the 3D mesh was configured
 
   /**
    * Variable specifying if the scene should be redrawn.
    * This should happen usually when the visualization is translated/rotated.
    */
   this.redraw = true;
 
   /**
@@ -290,38 +316,22 @@ TiltVisualizer.Presenter = function TV_P
     aRequestAnimationFrame(loop);
 
     // only redraw if we really have to
     if (this.redraw) {
       this.redraw = false;
       this.drawVisualization();
     }
 
-    // call the attached ondraw event handler if specified (by the controller)
+    // call the attached ondraw function and handle all keyframe notifications
     if ("function" === typeof this.ondraw) {
       this.ondraw(this.frames);
     }
 
-    if (!TiltVisualizer.Prefs.introTransition && !this.isExecutingDestruction) {
-      this.frames = INTRO_TRANSITION_DURATION;
-    }
-    if (!TiltVisualizer.Prefs.outroTransition && this.isExecutingDestruction) {
-      this.frames = OUTRO_TRANSITION_DURATION;
-    }
-
-    if ("function" === typeof this.onInitializationFinished &&
-        this.frames === INTRO_TRANSITION_DURATION &&
-       !this.isExecutingDestruction) {
-      this.onInitializationFinished();
-    }
-    if ("function" === typeof this.onDestructionFinished &&
-        this.frames === OUTRO_TRANSITION_DURATION &&
-        this.isExecutingDestruction) {
-      this.onDestructionFinished();
-    }
+    this.handleKeyframeNotifications();
   }.bind(this);
 
   setup();
   loop();
 };
 
 TiltVisualizer.Presenter.prototype = {
 
@@ -490,17 +500,17 @@ TiltVisualizer.Presenter.prototype = {
 
   /**
    * Create the combined mesh representing the document visualization by
    * traversing the document & adding a stack for each node that is drawable.
    *
    * @param {Object} aData
    *                 object containing the necessary mesh verts, texcoord etc.
    */
-  setupMesh: function TVP_setupMesh(aData) /*global TiltVisualizerStyle */
+  setupMesh: function TVP_setupMesh(aData)
   {
     let renderer = this.renderer;
 
     // destroy any previously created mesh
     TiltUtils.destroyObject(this.meshStacks);
     TiltUtils.destroyObject(this.meshWireframe);
 
     // if the renderer was destroyed, don't continue setup
@@ -525,23 +535,23 @@ TiltVisualizer.Presenter.prototype = {
     this.meshWireframe = {
       vertices: this.meshStacks.vertices,
       indices: new renderer.IndexBuffer(aData.wireframeIndices)
     };
 
     // if there's no initial selection made, highlight the required node
     if (!this._initialSelection) {
       this._initialSelection = true;
-      this.highlightNode(this.inspectorUI.selection);
+      this.highlightNode(this.chromeWindow.InspectorUI.selection);
     }
 
     if (!this._initialMeshConfiguration) {
       this._initialMeshConfiguration = true;
 
-      let zoom = TiltUtils.getDocumentZoom();
+      let zoom = this.transforms.zoom;
       let width = Math.min(aData.meshWidth * zoom, renderer.width);
       let height = Math.min(aData.meshHeight * zoom, renderer.height);
 
       // set the necessary mesh offsets
       this.transforms.offset[0] = -width * 0.5;
       this.transforms.offset[1] = -height * 0.5;
 
       // make sure the canvas is opaque now that the initialization is finished
@@ -605,17 +615,17 @@ TiltVisualizer.Presenter.prototype = {
     this.contentWindow.addEventListener("resize", this.onResize, false);
   },
 
   /**
    * Called when the content window of the current browser is resized.
    */
   onResize: function TVP_onResize(e)
   {
-    let zoom = TiltUtils.getDocumentZoom();
+    let zoom = TiltUtils.getDocumentZoom(this.chromeWindow);
     let width = e.target.innerWidth * zoom;
     let height = e.target.innerHeight * zoom;
 
     // handle aspect ratio changes to update the projection matrix
     this.renderer.width = width;
     this.renderer.height = height;
 
     this.redraw = true;
@@ -624,20 +634,16 @@ TiltVisualizer.Presenter.prototype = {
   /**
    * Highlights a specific node.
    *
    * @param {Element} aNode
    *                  the html node to be highlighted
    */
   highlightNode: function TVP_highlightNode(aNode)
   {
-    if (!aNode) {
-      return;
-    }
-
     this.highlightNodeFor(this.traverseData.nodes.indexOf(aNode));
   },
 
   /**
    * Picks a stacked dom node at the x and y screen coordinates and highlights
    * the selected node in the mesh.
    *
    * @param {Number} x
@@ -696,20 +702,23 @@ TiltVisualizer.Presenter.prototype = {
   highlightNodeFor: function TVP_highlightNodeFor(aNodeIndex)
   {
     this.redraw = true;
 
     // if the node was already selected, don't do anything
     if (this._currentSelection === aNodeIndex) {
       return;
     }
+
     // if an invalid or nonexisted node is specified, disable the highlight
     if (aNodeIndex < 0) {
       this._currentSelection = -1;
       this.highlight.disabled = true;
+
+      Services.obs.notifyObservers(null, this.NOTIFICATIONS.UNHIGHLIGHTING, null);
       return;
     }
 
     let highlight = this.highlight;
     let info = this.traverseData.info[aNodeIndex];
     let node = this.traverseData.nodes[aNodeIndex];
     let style = TiltVisualizerStyle.nodes;
 
@@ -725,18 +734,22 @@ TiltVisualizer.Presenter.prototype = {
     let z = info.depth;
 
     vec3.set([x,     y,     z * STACK_THICKNESS], highlight.v0);
     vec3.set([x + w, y,     z * STACK_THICKNESS], highlight.v1);
     vec3.set([x + w, y + h, z * STACK_THICKNESS], highlight.v2);
     vec3.set([x,     y + h, z * STACK_THICKNESS], highlight.v3);
 
     this._currentSelection = aNodeIndex;
-    this.inspectorUI.inspectNode(node, this.contentWindow.innerHeight < y ||
-                                       this.contentWindow.pageYOffset > 0);
+
+    this.chromeWindow.InspectorUI.inspectNode(node,
+      this.contentWindow.innerHeight < y ||
+      this.contentWindow.pageYOffset > 0);
+
+    Services.obs.notifyObservers(null, this.NOTIFICATIONS.HIGHLIGHTING, null);
   },
 
   /**
    * Deletes a node from the visualization mesh.
    *
    * @param {Number} aNodeIndex
    *                 the index of the node in the this.traverseData array;
    *                 if not specified, it will default to the current selection
@@ -753,16 +766,19 @@ TiltVisualizer.Presenter.prototype = {
 
     for (let i = 0, k = 36 * aNodeIndex; i < 36; i++) {
       meshData.vertices[i + k] = 0;
     }
 
     this.meshStacks.vertices = new renderer.VertexBuffer(meshData.vertices, 3);
     this.highlight.disabled = true;
     this.redraw = true;
+
+    Services.obs.notifyObservers(null,
+      this.NOTIFICATIONS.NODE_REMOVED, null);
   },
 
   /**
    * Picks a stacked dom node at the x and y screen coordinates and issues
    * a callback function with the found intersection.
    *
    * @param {Number} x
    *                 the current horizontal coordinate of the mouse
@@ -792,17 +808,17 @@ TiltVisualizer.Presenter.prototype = {
         }
       } else {
         if ("function" === typeof aProperties.onfail) {
           aProperties.onfail();
         }
       }
     }, false);
 
-    let zoom = TiltUtils.getDocumentZoom();
+    let zoom = TiltUtils.getDocumentZoom(this.chromeWindow);
     let width = this.renderer.width * zoom;
     let height = this.renderer.height * zoom;
     let mesh = this.meshStacks;
     x *= zoom;
     y *= zoom;
 
     // create a ray following the mouse direction from the near clipping plane
     // to the far clipping plane, to check for intersections with the mesh,
@@ -862,48 +878,85 @@ TiltVisualizer.Presenter.prototype = {
         transforms.rotation[3] !== w) {
 
       quat4.set(aQuaternion, transforms.rotation);
       this.redraw = true;
     }
   },
 
   /**
-   * Checks if this object was initialized properly.
-   *
-   * @return {Boolean} true if the object was initialized properly
+   * Handles notifications at specific frame counts.
    */
-  isInitialized: function TVP_isInitialized()
+  handleKeyframeNotifications: function TV_handleKeyframeNotifications()
   {
-    return this.renderer && this.renderer.context;
+    if (!TiltVisualizer.Prefs.introTransition && !this.isExecutingDestruction) {
+      this.frames = INTRO_TRANSITION_DURATION;
+    }
+    if (!TiltVisualizer.Prefs.outroTransition && this.isExecutingDestruction) {
+      this.frames = OUTRO_TRANSITION_DURATION;
+    }
+
+    if (this.frames === INTRO_TRANSITION_DURATION &&
+       !this.isExecutingDestruction) {
+
+      Services.obs.notifyObservers(null, this.NOTIFICATIONS.INITIALIZED, null);
+
+      if ("function" === typeof this.onInitializationFinished) {
+        this.onInitializationFinished();
+      }
+    }
+
+    if (this.frames === OUTRO_TRANSITION_DURATION &&
+        this.isExecutingDestruction) {
+
+      Services.obs.notifyObservers(null, this.NOTIFICATIONS.BEFORE_DESTROYED, null);
+
+      if ("function" === typeof this.onDestructionFinished) {
+        this.onDestructionFinished();
+      }
+    }
   },
 
   /**
-   * Starts executing a destruction animation and executes a callback function
+   * Starts executing the destruction sequence and issues a callback function
    * when finished.
    *
    * @param {Function} aCallback
    *                   the destruction finished callback
    */
   executeDestruction: function TV_executeDestruction(aCallback)
   {
     if (!this.isExecutingDestruction) {
       this.isExecutingDestruction = true;
       this.onDestructionFinished = aCallback;
 
+      // if we execute the destruction after the initialization finishes,
+      // proceed normally; otherwise, skip everything and immediately issue
+      // the callback
+
       if (this.frames > OUTRO_TRANSITION_DURATION) {
         this.frames = 0;
         this.redraw = true;
       } else {
         aCallback();
       }
     }
   },
 
   /**
+   * Checks if this object was initialized properly.
+   *
+   * @return {Boolean} true if the object was initialized properly
+   */
+  isInitialized: function TVP_isInitialized()
+  {
+    return this.renderer && this.renderer.context;
+  },
+
+  /**
    * Function called when this object is destroyed.
    */
   finalize: function TVP_finalize()
   {
     TiltUtils.destroyObject(this.visualizationProgram);
     TiltUtils.destroyObject(this.texture);
 
     if (this.meshStacks) {
@@ -930,36 +983,44 @@ TiltVisualizer.Presenter.prototype = {
  *
  * @param {HTMLCanvasElement} aCanvas
  *                            the visualization canvas element
  * @param {TiltVisualizer.Presenter} aPresenter
  *                                   the presenter instance to control
  */
 TiltVisualizer.Controller = function TV_Controller(aCanvas, aPresenter)
 {
+  /**
+   * A canvas overlay on which mouse and keyboard event listeners are attached.
+   */
   this.canvas = aCanvas;
+
+  /**
+   * Save a reference to the presenter to modify its model-view transforms.
+   */
   this.presenter = aPresenter;
 
   /**
    * The initial controller dimensions and offset, in pixels.
    */
-  this.left = aPresenter.contentWindow.pageXOffset || 0;
-  this.top = aPresenter.contentWindow.pageYOffset || 0;
+  this.zoom = aPresenter.transforms.zoom;
+  this.left = (aPresenter.contentWindow.pageXOffset || 0) * this.zoom;
+  this.top = (aPresenter.contentWindow.pageYOffset || 0) * this.zoom;
   this.width = aCanvas.width;
   this.height = aCanvas.height;
 
-  this.left *= TiltUtils.getDocumentZoom();
-  this.top *= TiltUtils.getDocumentZoom();
-
   /**
    * Arcball used to control the visualization using the mouse.
    */
-  this.arcball = new TiltVisualizer.Arcball(this.width, this.height, 0,
-    [this.width + this.left < aPresenter.maxTextureSize ? -this.left : 0,
-     this.height + this.top < aPresenter.maxTextureSize ? -this.top : 0]);
+  this.arcball = new TiltVisualizer.Arcball(
+    this.presenter.chromeWindow, this.width, this.height, 0,
+    [
+      this.width + this.left < aPresenter.maxTextureSize ? -this.left : 0,
+      this.height + this.top < aPresenter.maxTextureSize ? -this.top : 0
+    ]);
 
   /**
    * Object containing the rotation quaternion and the translation amount.
    */
   this.coordinates = null;
 
   // bind the owner object to the necessary functions
   TiltUtils.bindObjectFunc(this, "update");
@@ -970,17 +1031,17 @@ TiltVisualizer.Controller = function TV_
 
   // attach this controller's update function to the presenter ondraw event
   aPresenter.ondraw = this.update;
 };
 
 TiltVisualizer.Controller.prototype = {
 
   /**
-   * Adds all added events listeners required by this controller.
+   * Adds events listeners required by this controller.
    */
   addEventListeners: function TVC_addEventListeners()
   {
     let canvas = this.canvas;
     let presenter = this.presenter;
 
     // bind commonly used mouse and keyboard events with the controller
     canvas.addEventListener("mousedown", this.onMouseDown, false);
@@ -1151,19 +1212,20 @@ TiltVisualizer.Controller.prototype = {
   },
 
   /**
    * Called when a key is released.
    */
   onKeyUp: function TVC_onKeyUp(e)
   {
     let code = e.keyCode || e.which;
+    let tilt = this.presenter.chromeWindow.Tilt;
 
     if (code === e.DOM_VK_ESCAPE) {
-      this.presenter.tiltUI.destroy(this.presenter.tiltUI.currentWindowId, 1);
+      tilt.destroy(tilt.currentWindowId, true);
       return;
     }
     if (code === e.DOM_VK_X) {
       this.presenter.deleteNode();
     }
 
     if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
       e.preventDefault();
@@ -1179,17 +1241,17 @@ TiltVisualizer.Controller.prototype = {
     this.arcball.cancelKeyEvents();
   },
 
   /**
    * Called when the content window of the current browser is resized.
    */
   onResize: function TVC_onResize(e)
   {
-    let zoom = TiltUtils.getDocumentZoom();
+    let zoom = TiltUtils.getDocumentZoom(this.presenter.chromeWindow);
     let width = e.target.innerWidth * zoom;
     let height = e.target.innerHeight * zoom;
 
     this.arcball.resize(width, height);
   },
 
   /**
    * Checks if this object was initialized properly.
@@ -1214,31 +1276,38 @@ TiltVisualizer.Controller.prototype = {
   }
 };
 
 /**
  * This is a general purpose 3D rotation controller described by Ken Shoemake
  * in the Graphics Interface ’92 Proceedings. It features good behavior
  * easy implementation, cheap execution.
  *
+ * @param {Window} aChromeWindow
+ *                 a reference to the top-level window
  * @param {Number} aWidth
  *                 the width of canvas
  * @param {Number} aHeight
  *                 the height of canvas
  * @param {Number} aRadius
  *                 optional, the radius of the arcball
  * @param {Array} aInitialTrans
  *                optional, initial vector translation
  * @param {Array} aInitialRot
  *                optional, initial quaternion rotation
  */
 TiltVisualizer.Arcball = function TV_Arcball(
-  aWidth, aHeight, aRadius, aInitialTrans, aInitialRot)
+  aChromeWindow, aWidth, aHeight, aRadius, aInitialTrans, aInitialRot)
 {
   /**
+   * Save a reference to the top-level window to set/remove intervals.
+   */
+  this.chromeWindow = aChromeWindow;
+
+  /**
    * Values retaining the current horizontal and vertical mouse coordinates.
    */
   this._mousePress = vec3.create();
   this._mouseRelease = vec3.create();
   this._mouseMove = vec3.create();
   this._mouseLerp = vec3.create();
   this._mouseButton = -1;
 
@@ -1683,38 +1752,37 @@ TiltVisualizer.Arcball.prototype = {
    */
   reset: function TVA_reset(aFinalTranslation, aFinalRotation)
   {
     if ("function" === typeof this.onResetStart) {
       this.onResetStart();
       this.onResetStart = null;
     }
 
+    let func = this._nextResetIntervalStep.bind(this);
+
     this.cancelMouseEvents();
     this.cancelKeyEvents();
     this._cancelResetInterval();
 
-    let window = TiltUtils.getBrowserWindow();
-    let func = this._nextResetIntervalStep.bind(this);
-
     this._save();
     this._resetFinalTranslation = vec3.create(aFinalTranslation);
     this._resetFinalRotation = quat4.create(aFinalRotation);
-    this._resetInterval = window.setInterval(func, ARCBALL_RESET_INTERVAL);
+    this._resetInterval =
+      this.chromeWindow.setInterval(func, ARCBALL_RESET_INTERVAL);
   },
 
   /**
    * Cancels the current arcball reset animation if there is one.
    */
   _cancelResetInterval: function TVA__cancelResetInterval()
   {
     if (this._resetInterval) {
-      let window = TiltUtils.getBrowserWindow();
+      this.chromeWindow.clearInterval(this._resetInterval);
 
-      window.clearInterval(this._resetInterval);
       this._resetInterval = null;
       this._save();
 
       if ("function" === typeof this.onResetFinish) {
         this.onResetFinish();
         this.onResetFinish = null;
       }
     }
@@ -1753,18 +1821,18 @@ TiltVisualizer.Arcball.prototype = {
 
       this._cancelResetInterval();
     }
   },
 
   /**
    * Loads the keys to control this arcball.
    */
-  _loadKeys: function TVA__loadKeys() {
-
+  _loadKeys: function TVA__loadKeys()
+  {
     this.rotateKeys = {
       "up": Ci.nsIDOMKeyEvent["DOM_VK_W"],
       "down": Ci.nsIDOMKeyEvent["DOM_VK_S"],
       "left": Ci.nsIDOMKeyEvent["DOM_VK_A"],
       "right": Ci.nsIDOMKeyEvent["DOM_VK_D"],
     };
     this.panKeys = {
       "up": Ci.nsIDOMKeyEvent["DOM_VK_UP"],
--- a/browser/devtools/tilt/TiltVisualizerStyle.jsm
+++ b/browser/devtools/tilt/TiltVisualizerStyle.jsm
@@ -31,18 +31,16 @@
  * 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 LGPL or the GPL. 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 *****/
-
-/*global Components, TiltMath */
 "use strict";
 
 const Cu = Components.utils;
 
 Cu.import("resource:///modules/devtools/TiltMath.jsm");
 
 let EXPORTED_SYMBOLS = ["TiltVisualizerStyle"];
 let rgba = TiltMath.hex2rgba;
--- a/browser/devtools/tilt/TiltWorkerCrafter.js
+++ b/browser/devtools/tilt/TiltWorkerCrafter.js
@@ -31,18 +31,16 @@
  * 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 LGPL or the GPL. 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 *****/
-
-/*global self*/
 "use strict";
 
 const SIXTEEN_OVER_255 = 16 / 255;
 const ONE_OVER_255 = 1 / 255;
 
 /**
  * Given the initialization data (thickness, sizes and information about
  * each DOM node) this worker sends back the arrays representing
--- a/browser/devtools/tilt/TiltWorkerPicker.js
+++ b/browser/devtools/tilt/TiltWorkerPicker.js
@@ -31,18 +31,16 @@
  * 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 LGPL or the GPL. 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 *****/
-
-/*global self*/
 "use strict";
 
 /**
  * This worker handles picking, given a set of vertices and a ray (calculates
  * the intersection points and offers back information about the closest hit).
  *
  * Used in the TiltVisualization.Presenter object.
  */
--- a/browser/devtools/tilt/test/Makefile.in
+++ b/browser/devtools/tilt/test/Makefile.in
@@ -42,16 +42,17 @@ VPATH			= @srcdir@
 relativesrcdir 	= browser/devtools/tilt/test
 
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _BROWSER_TEST_FILES = \
 	head.js \
 	browser_tilt_01_lazy_getter.js \
+	browser_tilt_02_notifications-seq.js \
 	browser_tilt_02_notifications.js \
 	browser_tilt_03_tab_switch.js \
 	browser_tilt_04_initialization-key.js \
 	browser_tilt_04_initialization.js \
 	browser_tilt_05_destruction-esc.js \
 	browser_tilt_05_destruction-url.js \
 	browser_tilt_05_destruction.js \
 	browser_tilt_arcball-reset-typeahead.js \
--- a/browser/devtools/tilt/test/browser_tilt_01_lazy_getter.js
+++ b/browser/devtools/tilt/test/browser_tilt_01_lazy_getter.js
@@ -1,12 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, Tilt */
 "use strict";
 
 function test() {
   ok(Tilt,
     "The Tilt object wasn't got correctly via defineLazyGetter.");
   is(Tilt.chromeWindow, window,
     "The top-level window wasn't saved correctly");
   ok(Tilt.visualizers,
new file mode 100644
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_02_notifications-seq.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let tabEvents = "";
+
+function test() {
+  if (!isTiltEnabled()) {
+    info("Skipping notifications test because Tilt isn't enabled.");
+    return;
+  }
+  if (!isWebGLSupported()) {
+    info("Skipping notifications test because WebGL isn't supported.");
+    return;
+  }
+
+  requestLongerTimeout(10);
+  waitForExplicitFinish();
+
+  createTab(function() {
+    Services.obs.addObserver(cleanup, DESTROYED, false);
+
+    Services.obs.addObserver(obs_INITIALIZING, INITIALIZING, false);
+    Services.obs.addObserver(obs_INITIALIZED, INITIALIZED, false);
+    Services.obs.addObserver(obs_DESTROYING, DESTROYING, false);
+    Services.obs.addObserver(obs_BEFORE_DESTROYED, BEFORE_DESTROYED, false);
+    Services.obs.addObserver(obs_DESTROYED, DESTROYED, false);
+
+    info("Starting up the Tilt notifications test.");
+    createTilt({});
+  });
+}
+
+function obs_INITIALIZING() {
+  info("Handling the INITIALIZING notification.");
+  tabEvents += "INITIALIZING;";
+}
+
+function obs_INITIALIZED() {
+  info("Handling the INITIALIZED notification.");
+  tabEvents += "INITIALIZED;";
+
+  Tilt.destroy(Tilt.currentWindowId, true);
+}
+
+function obs_DESTROYING() {
+  info("Handling the DESTROYING( notification.");
+  tabEvents += "DESTROYING;";
+}
+
+function obs_BEFORE_DESTROYED() {
+  info("Handling the BEFORE_DESTROYED notification.");
+  tabEvents += "BEFORE_DESTROYED;";
+}
+
+function obs_DESTROYED() {
+  info("Handling the DESTROYED notification.");
+  tabEvents += "DESTROYED;";
+}
+
+function cleanup() {
+  info("Cleaning up the notifications test.");
+
+  is(tabEvents, "INITIALIZING;INITIALIZED;DESTROYING;BEFORE_DESTROYED;DESTROYED;",
+    "The notifications weren't fired in the correct order.");
+
+  Services.obs.removeObserver(cleanup, DESTROYED);
+
+  Services.obs.removeObserver(obs_INITIALIZING, INITIALIZING, false);
+  Services.obs.removeObserver(obs_INITIALIZED, INITIALIZED, false);
+  Services.obs.removeObserver(obs_DESTROYING, DESTROYING, false);
+  Services.obs.removeObserver(obs_BEFORE_DESTROYED, BEFORE_DESTROYED, false);
+  Services.obs.removeObserver(obs_DESTROYED, DESTROYED, false);
+
+  gBrowser.removeCurrentTab();
+  finish();
+}
--- a/browser/devtools/tilt/test/browser_tilt_02_notifications.js
+++ b/browser/devtools/tilt/test/browser_tilt_02_notifications.js
@@ -1,96 +1,105 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, info, waitForExplicitFinish, finish, executeSoon, gBrowser */
-/*global isTiltEnabled, isWebGLSupported, createTab, createTilt, Tilt */
-/*global Services, TILT_INITIALIZED, TILT_DESTROYED, TILT_SHOWN, TILT_HIDDEN */
 "use strict";
 
 let tab0, tab1;
 let testStep = -1;
 let tabEvents = "";
 
 function test() {
   if (!isTiltEnabled()) {
     info("Skipping notifications test because Tilt isn't enabled.");
     return;
   }
   if (!isWebGLSupported()) {
     info("Skipping notifications test because WebGL isn't supported.");
     return;
   }
 
+  requestLongerTimeout(10);
   waitForExplicitFinish();
 
   gBrowser.tabContainer.addEventListener("TabSelect", tabSelect, false);
   createNewTab();
 }
 
 function createNewTab() {
   tab0 = gBrowser.selectedTab;
 
   tab1 = createTab(function() {
-    Services.obs.addObserver(cleanup, TILT_DESTROYED, false);
-    Services.obs.addObserver(tab_TILT_INITIALIZED, TILT_INITIALIZED, false);
-    Services.obs.addObserver(tab_TILT_DESTROYED, TILT_DESTROYED, false);
-    Services.obs.addObserver(tab_TILT_SHOWN, TILT_SHOWN, false);
-    Services.obs.addObserver(tab_TILT_HIDDEN, TILT_HIDDEN, false);
+    Services.obs.addObserver(cleanup, DESTROYED, false);
 
+    Services.obs.addObserver(tab_INITIALIZING, INITIALIZING, false);
+    Services.obs.addObserver(tab_DESTROYING, DESTROYING, false);
+    Services.obs.addObserver(tab_SHOWN, SHOWN, false);
+    Services.obs.addObserver(tab_HIDDEN, HIDDEN, false);
+
+    info("Starting up the Tilt notifications test.");
     createTilt({
       onTiltOpen: function()
       {
         testStep = 0;
         tabSelect();
       }
     });
   });
 }
 
-function tab_TILT_INITIALIZED() {
-  tabEvents += "ti;";
+function tab_INITIALIZING() {
+  info("Handling the INITIALIZING notification.");
+  tabEvents += "INITIALIZING;";
 }
 
-function tab_TILT_DESTROYED() {
-  tabEvents += "td;";
+function tab_DESTROYING() {
+  info("Handling the DESTROYING notification.");
+  tabEvents += "DESTROYING;";
 }
 
-function tab_TILT_SHOWN() {
-  tabEvents += "ts;";
+function tab_SHOWN() {
+  info("Handling the SHOWN notification.");
+  tabEvents += "SHOWN;";
 }
 
-function tab_TILT_HIDDEN() {
-  tabEvents += "th;";
+function tab_HIDDEN() {
+  info("Handling the HIDDEN notification.");
+  tabEvents += "HIDDEN;";
 }
 
 let testSteps = [
   function step0() {
+    info("Selecting tab0.");
     gBrowser.selectedTab = tab0;
   },
   function step1() {
+    info("Selecting tab1.");
     gBrowser.selectedTab = tab1;
   },
   function step2() {
-    Tilt.destroy(Tilt.currentWindowId);
+    info("Killing it.");
+    Tilt.destroy(Tilt.currentWindowId, true);
   }
 ];
 
 function cleanup() {
-  is(tabEvents, "ti;th;ts;td;",
+  info("Cleaning up the notifications test.");
+
+  is(tabEvents, "INITIALIZING;HIDDEN;SHOWN;DESTROYING;",
     "The notifications weren't fired in the correct order.");
 
   tab0 = null;
   tab1 = null;
 
-  Services.obs.removeObserver(cleanup, TILT_DESTROYED);
-  Services.obs.removeObserver(tab_TILT_INITIALIZED, TILT_INITIALIZED, false);
-  Services.obs.removeObserver(tab_TILT_DESTROYED, TILT_DESTROYED, false);
-  Services.obs.removeObserver(tab_TILT_SHOWN, TILT_SHOWN, false);
-  Services.obs.removeObserver(tab_TILT_HIDDEN, TILT_HIDDEN, false);
+  Services.obs.removeObserver(cleanup, DESTROYED);
+
+  Services.obs.removeObserver(tab_INITIALIZING, INITIALIZING, false);
+  Services.obs.removeObserver(tab_DESTROYING, DESTROYING, false);
+  Services.obs.removeObserver(tab_SHOWN, SHOWN, false);
+  Services.obs.removeObserver(tab_HIDDEN, HIDDEN, false);
 
   gBrowser.tabContainer.removeEventListener("TabSelect", tabSelect, false);
   gBrowser.removeCurrentTab();
   finish();
 }
 
 function tabSelect() {
   if (testStep !== -1) {
--- a/browser/devtools/tilt/test/browser_tilt_03_tab_switch.js
+++ b/browser/devtools/tilt/test/browser_tilt_03_tab_switch.js
@@ -1,13 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, info, waitForExplicitFinish, finish, executeSoon, gBrowser */
-/*global isTiltEnabled, isWebGLSupported, createTab, createTilt, Tilt */
 "use strict";
 
 let tab0, tab1, tab2;
 let testStep = -1;
 
 function test() {
   if (!isTiltEnabled()) {
     info("Skipping tab switch test because Tilt isn't enabled.");
--- a/browser/devtools/tilt/test/browser_tilt_04_initialization-key.js
+++ b/browser/devtools/tilt/test/browser_tilt_04_initialization-key.js
@@ -1,15 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, info, waitForExplicitFinish, finish, executeSoon, gBrowser */
-/*global isTiltEnabled, isWebGLSupported, createTab, createTilt */
-/*global Services, EventUtils, Tilt, TiltUtils, TiltVisualizer, InspectorUI */
-/*global Ci, TILT_INITIALIZED, TILT_DESTROYED, INSPECTOR_OPENED */
 "use strict";
 
 let id;
 let tiltKey;
 let eventType;
 
 function test() {
   if (!isTiltEnabled()) {
@@ -40,33 +35,33 @@ function onInspectorOpen() {
   Services.obs.removeObserver(onInspectorOpen, INSPECTOR_OPENED);
 
   executeSoon(function() {
     is(Tilt.visualizers[id], null,
       "A instance of the visualizer shouldn't be initialized yet.");
 
     info("Pressing the accesskey should open Tilt.");
 
-    Services.obs.addObserver(onTiltOpen, TILT_INITIALIZED, false);
+    Services.obs.addObserver(onTiltOpen, INITIALIZING, false);
     EventUtils.synthesizeKey(tiltKey, eventType);
   });
 }
 
 function onTiltOpen() {
-  Services.obs.removeObserver(onTiltOpen, TILT_INITIALIZED);
+  Services.obs.removeObserver(onTiltOpen, INITIALIZING);
 
   executeSoon(function() {
     ok(Tilt.visualizers[id] instanceof TiltVisualizer,
       "A new instance of the visualizer wasn't created properly.");
     ok(Tilt.visualizers[id].isInitialized(),
       "The new instance of the visualizer wasn't initialized properly.");
 
     info("Pressing the accesskey again should close Tilt.");
 
-    Services.obs.addObserver(onTiltClose, TILT_DESTROYED, false);
+    Services.obs.addObserver(onTiltClose, DESTROYED, false);
     EventUtils.synthesizeKey(tiltKey, eventType);
   });
 }
 
 function onTiltClose() {
   is(Tilt.visualizers[id], null,
     "The current instance of the visualizer wasn't destroyed properly.");
 
--- a/browser/devtools/tilt/test/browser_tilt_04_initialization.js
+++ b/browser/devtools/tilt/test/browser_tilt_04_initialization.js
@@ -1,14 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, info, waitForExplicitFinish, finish, gBrowser */
-/*global isTiltEnabled, isWebGLSupported, createTab, createTilt */
-/*global Tilt, TiltUtils, TiltVisualizer */
 "use strict";
 
 function test() {
   if (!isTiltEnabled()) {
     info("Skipping initialization test because Tilt isn't enabled.");
     return;
   }
   if (!isWebGLSupported()) {
--- a/browser/devtools/tilt/test/browser_tilt_05_destruction-esc.js
+++ b/browser/devtools/tilt/test/browser_tilt_05_destruction-esc.js
@@ -1,14 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, info, waitForExplicitFinish, finish, gBrowser */
-/*global isTiltEnabled, isWebGLSupported, createTab, createTilt */
-/*global Services, EventUtils, Tilt, TiltUtils, InspectorUI, TILT_DESTROYED */
 "use strict";
 
 function test() {
   if (!isTiltEnabled()) {
     info("Skipping destruction test because Tilt isn't enabled.");
     return;
   }
   if (!isWebGLSupported()) {
@@ -17,25 +13,25 @@ function test() {
   }
 
   waitForExplicitFinish();
 
   createTab(function() {
     createTilt({
       onTiltOpen: function()
       {
-        Services.obs.addObserver(cleanup, TILT_DESTROYED, false);
+        Services.obs.addObserver(cleanup, DESTROYED, false);
         EventUtils.sendKey("ESCAPE");
       }
     });
   });
 }
 
 function cleanup() {
   let id = TiltUtils.getWindowId(gBrowser.selectedBrowser.contentWindow);
 
   is(Tilt.visualizers[id], null,
     "The current instance of the visualizer wasn't destroyed properly.");
 
-  Services.obs.removeObserver(cleanup, TILT_DESTROYED);
+  Services.obs.removeObserver(cleanup, DESTROYED);
   gBrowser.removeCurrentTab();
   finish();
 }
--- a/browser/devtools/tilt/test/browser_tilt_05_destruction-url.js
+++ b/browser/devtools/tilt/test/browser_tilt_05_destruction-url.js
@@ -1,14 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, info, waitForExplicitFinish, finish, gBrowser */
-/*global isTiltEnabled, isWebGLSupported, createTab, createTilt */
-/*global Services, EventUtils, Tilt, TiltUtils, InspectorUI, TILT_DESTROYED */
 "use strict";
 
 function test() {
   if (!isTiltEnabled()) {
     info("Skipping destruction test because Tilt isn't enabled.");
     return;
   }
   if (!isWebGLSupported()) {
@@ -17,25 +13,25 @@ function test() {
   }
 
   waitForExplicitFinish();
 
   createTab(function() {
     createTilt({
       onTiltOpen: function()
       {
-        Services.obs.addObserver(cleanup, TILT_DESTROYED, false);
+        Services.obs.addObserver(cleanup, DESTROYED, false);
         window.content.location = "about:mozilla";
       }
     });
   });
 }
 
 function cleanup() {
   let id = TiltUtils.getWindowId(gBrowser.selectedBrowser.contentWindow);
 
   is(Tilt.visualizers[id], null,
     "The current instance of the visualizer wasn't destroyed properly.");
 
-  Services.obs.removeObserver(cleanup, TILT_DESTROYED);
+  Services.obs.removeObserver(cleanup, DESTROYED);
   gBrowser.removeCurrentTab();
   finish();
 }
--- a/browser/devtools/tilt/test/browser_tilt_05_destruction.js
+++ b/browser/devtools/tilt/test/browser_tilt_05_destruction.js
@@ -1,14 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, info, waitForExplicitFinish, finish, gBrowser */
-/*global isTiltEnabled, isWebGLSupported, createTab, createTilt */
-/*global Services, Tilt, TiltUtils, InspectorUI, TILT_DESTROYED */
 "use strict";
 
 function test() {
   if (!isTiltEnabled()) {
     info("Skipping destruction test because Tilt isn't enabled.");
     return;
   }
   if (!isWebGLSupported()) {
@@ -17,25 +13,25 @@ function test() {
   }
 
   waitForExplicitFinish();
 
   createTab(function() {
     createTilt({
       onTiltOpen: function()
       {
-        Services.obs.addObserver(cleanup, TILT_DESTROYED, false);
+        Services.obs.addObserver(cleanup, DESTROYED, false);
         InspectorUI.closeInspectorUI();
       }
     });
   });
 }
 
 function cleanup() {
   let id = TiltUtils.getWindowId(gBrowser.selectedBrowser.contentWindow);
 
   is(Tilt.visualizers[id], null,
     "The current instance of the visualizer wasn't destroyed properly.");
 
-  Services.obs.removeObserver(cleanup, TILT_DESTROYED);
+  Services.obs.removeObserver(cleanup, DESTROYED);
   gBrowser.removeCurrentTab();
   finish();
 }
--- a/browser/devtools/tilt/test/browser_tilt_arcball-reset-typeahead.js
+++ b/browser/devtools/tilt/test/browser_tilt_arcball-reset-typeahead.js
@@ -1,14 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, info, isApproxVec, waitForExplicitFinish, executeSoon, finish */
-/*global isTiltEnabled, isWebGLSupported, createTab, createTilt */
-/*global Services, EventUtils, InspectorUI, TiltVisualizer, TILT_DESTROYED */
 "use strict";
 
 function test() {
   if (!isTiltEnabled()) {
     info("Skipping part of the arcball test because Tilt isn't enabled.");
     return;
   }
   if (!isWebGLSupported()) {
@@ -25,50 +21,50 @@ function test() {
       onTiltOpen: function(instance)
       {
         performTest(instance.presenter.canvas,
                     instance.controller.arcball, function() {
 
           info("Killing arcball reset test.");
 
           Services.prefs.setBoolPref("accessibility.typeaheadfind", false);
-          Services.obs.addObserver(cleanup, TILT_DESTROYED, false);
+          Services.obs.addObserver(cleanup, DESTROYED, false);
           InspectorUI.closeInspectorUI();
         });
       }
     });
   });
 }
 
 function performTest(canvas, arcball, callback) {
   is(document.activeElement, canvas,
     "The visualizer canvas should be focused when performing this test.");
 
 
   info("Starting arcball reset test.");
 
   // start translating and rotating sometime at random
 
-  executeSoon(function() {
+  window.setTimeout(function() {
     info("Synthesizing key down events.");
 
-    EventUtils.synthesizeKey("VK_W", { type: "keydown" });
-    EventUtils.synthesizeKey("VK_LEFT", { type: "keydown" });
+    EventUtils.synthesizeKey("VK_S", { type: "keydown" });     // add a little
+    EventUtils.synthesizeKey("VK_RIGHT", { type: "keydown" }); // diversity
 
     // wait for some arcball translations and rotations to happen
 
-    executeSoon(function() {
+    window.setTimeout(function() {
       info("Synthesizing key up events.");
 
-      EventUtils.synthesizeKey("VK_W", { type: "keyup" });
-      EventUtils.synthesizeKey("VK_LEFT", { type: "keyup" });
+      EventUtils.synthesizeKey("VK_S", { type: "keyup" });
+      EventUtils.synthesizeKey("VK_RIGHT", { type: "keyup" });
 
       // ok, transformations finished, we can now try to reset the model view
 
-      executeSoon(function() {
+      window.setTimeout(function() {
         info("Synthesizing arcball reset key press.");
 
         arcball.onResetStart = function() {
           info("Starting arcball reset animation.");
         };
 
         arcball.onResetFinish = function() {
           ok(isApproxVec(arcball._lastRot, [0, 0, 0, 1]),
@@ -93,20 +89,21 @@ function performTest(canvas, arcball, ca
           ok(isApproxVec([arcball._zoomAmount], [0]),
             "The arcball _zoomAmount field wasn't reset correctly.");
 
           info("Finishing arcball reset test.");
           callback();
         };
 
         EventUtils.synthesizeKey("VK_R", { type: "keydown" });
-      });
-    });
-  });
+
+      }, Math.random() * 1000); // leave enough time for transforms to happen
+    }, Math.random() * 1000);
+  }, Math.random() * 1000);
 }
 
-function cleanup() { /*global gBrowser */
+function cleanup() {
   info("Cleaning up arcball reset test.");
 
-  Services.obs.removeObserver(cleanup, TILT_DESTROYED);
+  Services.obs.removeObserver(cleanup, DESTROYED);
   gBrowser.removeCurrentTab();
   finish();
 }
--- a/browser/devtools/tilt/test/browser_tilt_arcball-reset.js
+++ b/browser/devtools/tilt/test/browser_tilt_arcball-reset.js
@@ -1,14 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, info, isApproxVec, waitForExplicitFinish, executeSoon, finish */
-/*global isTiltEnabled, isWebGLSupported, createTab, createTilt */
-/*global Services, EventUtils, InspectorUI, TiltVisualizer, TILT_DESTROYED */
 "use strict";
 
 function test() {
   if (!isTiltEnabled()) {
     info("Skipping part of the arcball test because Tilt isn't enabled.");
     return;
   }
   if (!isWebGLSupported()) {
@@ -23,50 +19,50 @@ function test() {
     createTilt({
       onTiltOpen: function(instance)
       {
         performTest(instance.presenter.canvas,
                     instance.controller.arcball, function() {
 
           info("Killing arcball reset test.");
 
-          Services.obs.addObserver(cleanup, TILT_DESTROYED, false);
+          Services.obs.addObserver(cleanup, DESTROYED, false);
           InspectorUI.closeInspectorUI();
         });
       }
     });
   });
 }
 
 function performTest(canvas, arcball, callback) {
   is(document.activeElement, canvas,
     "The visualizer canvas should be focused when performing this test.");
 
 
   info("Starting arcball reset test.");
 
   // start translating and rotating sometime at random
 
-  executeSoon(function() {
+  window.setTimeout(function() {
     info("Synthesizing key down events.");
 
     EventUtils.synthesizeKey("VK_W", { type: "keydown" });
     EventUtils.synthesizeKey("VK_LEFT", { type: "keydown" });
 
     // wait for some arcball translations and rotations to happen
 
-    executeSoon(function() {
+    window.setTimeout(function() {
       info("Synthesizing key up events.");
 
       EventUtils.synthesizeKey("VK_W", { type: "keyup" });
       EventUtils.synthesizeKey("VK_LEFT", { type: "keyup" });
 
       // ok, transformations finished, we can now try to reset the model view
 
-      executeSoon(function() {
+      window.setTimeout(function() {
         info("Synthesizing arcball reset key press.");
 
         arcball.onResetStart = function() {
           info("Starting arcball reset animation.");
         };
 
         arcball.onResetFinish = function() {
           ok(isApproxVec(arcball._lastRot, [0, 0, 0, 1]),
@@ -91,20 +87,21 @@ function performTest(canvas, arcball, ca
           ok(isApproxVec([arcball._zoomAmount], [0]),
             "The arcball _zoomAmount field wasn't reset correctly.");
 
           info("Finishing arcball reset test.");
           callback();
         };
 
         EventUtils.synthesizeKey("VK_R", { type: "keydown" });
-      });
-    });
-  });
+
+      }, Math.random() * 1000); // leave enough time for transforms to happen
+    }, Math.random() * 1000);
+  }, Math.random() * 1000);
 }
 
-function cleanup() { /*global gBrowser */
+function cleanup() {
   info("Cleaning up arcball reset test.");
 
-  Services.obs.removeObserver(cleanup, TILT_DESTROYED);
+  Services.obs.removeObserver(cleanup, DESTROYED);
   gBrowser.removeCurrentTab();
   finish();
 }
--- a/browser/devtools/tilt/test/browser_tilt_arcball.js
+++ b/browser/devtools/tilt/test/browser_tilt_arcball.js
@@ -1,13 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, info, isApprox, isApproxVec, vec3, quat4 */
-/*global TiltVisualizer */
 "use strict";
 
 function cloneUpdate(update) {
   return {
     rotation: quat4.create(update.rotation),
     translation: vec3.create(update.translation)
   };
 }
@@ -23,37 +20,37 @@ function isExpectedUpdate(update1, updat
                                           JSON.stringify(update2) + " instead.");
       return false;
     }
   }
   return true;
 }
 
 function test() {
-  let arcball1 = new TiltVisualizer.Arcball(123, 456);
+  let arcball1 = new TiltVisualizer.Arcball(window, 123, 456);
 
   is(arcball1.width, 123,
     "The first arcball width wasn't set correctly.");
   is(arcball1.height, 456,
     "The first arcball height wasn't set correctly.");
   is(arcball1.radius, 123,
     "The first arcball radius wasn't implicitly set correctly.");
 
 
-  let arcball2 = new TiltVisualizer.Arcball(987, 654);
+  let arcball2 = new TiltVisualizer.Arcball(window, 987, 654);
 
   is(arcball2.width, 987,
     "The second arcball width wasn't set correctly.");
   is(arcball2.height, 654,
     "The second arcball height wasn't set correctly.");
   is(arcball2.radius, 654,
     "The second arcball radius wasn't implicitly set correctly.");
 
 
-  let arcball3 = new TiltVisualizer.Arcball(512, 512);
+  let arcball3 = new TiltVisualizer.Arcball(window, 512, 512);
 
   let sphereVec = vec3.create();
   arcball3.pointToSphere(123, 456, 256, 512, 512, sphereVec);
 
   ok(isApproxVec(sphereVec, [-0.009765625, 0.390625, 0.9204980731010437]),
     "The pointToSphere() function didn't map the coordinates correctly.");
 
   let stack1 = [];
--- a/browser/devtools/tilt/test/browser_tilt_controller.js
+++ b/browser/devtools/tilt/test/browser_tilt_controller.js
@@ -1,14 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, info, waitForExplicitFinish, finish, executeSoon, gBrowser */
-/*global isEqualVec, isTiltEnabled, isWebGLSupported, createTab, createTilt */
-/*global Services, EventUtils, vec3, mat4, quat4 */
 "use strict";
 
 function test() {
   if (!isTiltEnabled()) {
     info("Skipping controller test because Tilt isn't enabled.");
     return;
   }
   if (!isWebGLSupported()) {
--- a/browser/devtools/tilt/test/browser_tilt_gl01.js
+++ b/browser/devtools/tilt/test/browser_tilt_gl01.js
@@ -1,13 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, info, isApproxVec, isWebGLSupported, createCanvas, TiltGL */
-/*global WebGLRenderingContext, WebGLProgram */
 "use strict";
 
 let isWebGLAvailable;
 
 function onWebGLFail() {
   isWebGLAvailable = false;
 }
 
--- a/browser/devtools/tilt/test/browser_tilt_gl02.js
+++ b/browser/devtools/tilt/test/browser_tilt_gl02.js
@@ -1,12 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, info, isApproxVec, isWebGLSupported, createCanvas, TiltGL */
 "use strict";
 
 let isWebGLAvailable;
 
 function onWebGLFail() {
   isWebGLAvailable = false;
 }
 
--- a/browser/devtools/tilt/test/browser_tilt_gl03.js
+++ b/browser/devtools/tilt/test/browser_tilt_gl03.js
@@ -1,12 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, info, isApproxVec, isWebGLSupported, createCanvas, TiltGL */
 "use strict";
 
 let isWebGLAvailable;
 
 function onWebGLFail() {
   isWebGLAvailable = false;
 }
 
--- a/browser/devtools/tilt/test/browser_tilt_gl04.js
+++ b/browser/devtools/tilt/test/browser_tilt_gl04.js
@@ -1,12 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, info, isApproxVec, isWebGLSupported, createCanvas, TiltGL */
 "use strict";
 
 let isWebGLAvailable;
 
 function onWebGLFail() {
   isWebGLAvailable = false;
 }
 
--- a/browser/devtools/tilt/test/browser_tilt_gl05.js
+++ b/browser/devtools/tilt/test/browser_tilt_gl05.js
@@ -1,12 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, info, isWebGLSupported, createCanvas, TiltGL */
 "use strict";
 
 let isWebGLAvailable;
 
 function onWebGLFail() {
   isWebGLAvailable = false;
 }
 
--- a/browser/devtools/tilt/test/browser_tilt_gl06.js
+++ b/browser/devtools/tilt/test/browser_tilt_gl06.js
@@ -1,12 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, info, isWebGLSupported, createCanvas, TiltGL */
 "use strict";
 
 let isWebGLAvailable;
 
 function onWebGLFail() {
   isWebGLAvailable = false;
 }
 
--- a/browser/devtools/tilt/test/browser_tilt_gl07.js
+++ b/browser/devtools/tilt/test/browser_tilt_gl07.js
@@ -1,12 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, isnot, info, isWebGLSupported, createCanvas, TiltGL */
 "use strict";
 
 let isWebGLAvailable;
 
 function onWebGLFail() {
   isWebGLAvailable = false;
 }
 
--- a/browser/devtools/tilt/test/browser_tilt_gl08.js
+++ b/browser/devtools/tilt/test/browser_tilt_gl08.js
@@ -1,12 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, isnot, info, isWebGLSupported, createCanvas, TiltGL */
 "use strict";
 
 let isWebGLAvailable;
 
 function onWebGLFail() {
   isWebGLAvailable = false;
 }
 
--- a/browser/devtools/tilt/test/browser_tilt_math01.js
+++ b/browser/devtools/tilt/test/browser_tilt_math01.js
@@ -1,12 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, isApprox, isApproxVec, TiltMath */
 "use strict";
 
 function test() {
   ok(isApprox(TiltMath.radians(30), 0.523598775),
     "The radians() function didn't calculate the value correctly.");
 
   ok(isApprox(TiltMath.degrees(0.5), 28.64788975),
     "The degrees() function didn't calculate the value correctly.");
--- a/browser/devtools/tilt/test/browser_tilt_math02.js
+++ b/browser/devtools/tilt/test/browser_tilt_math02.js
@@ -1,12 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, isApproxVec, vec3, mat4 */
 "use strict";
 
 function test() {
   let v1 = vec3.create();
 
   ok(v1, "Should have created a vector with vec3.create().");
   is(v1.length, 3, "A vec3 should have 3 elements.");
 
--- a/browser/devtools/tilt/test/browser_tilt_math03.js
+++ b/browser/devtools/tilt/test/browser_tilt_math03.js
@@ -1,12 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, isApproxVec, mat3 */
 "use strict";
 
 function test() {
   let m1 = mat3.create();
 
   ok(m1, "Should have created a matrix with mat3.create().");
   is(m1.length, 9, "A mat3 should have 9 elements.");
 
--- a/browser/devtools/tilt/test/browser_tilt_math04.js
+++ b/browser/devtools/tilt/test/browser_tilt_math04.js
@@ -1,12 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, isApprox, isApproxVec, mat4 */
 "use strict";
 
 function test() {
   let m1 = mat4.create();
 
   ok(m1, "Should have created a matrix with mat4.create().");
   is(m1.length, 16, "A mat4 should have 16 elements.");
 
--- a/browser/devtools/tilt/test/browser_tilt_math05.js
+++ b/browser/devtools/tilt/test/browser_tilt_math05.js
@@ -1,12 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, isApproxVec, mat4 */
 "use strict";
 
 function test() {
   let m1 = mat4.create([
     1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
 
   let m2 = mat4.create([
     0, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15, 4, 8, 12, 16]);
--- a/browser/devtools/tilt/test/browser_tilt_math06.js
+++ b/browser/devtools/tilt/test/browser_tilt_math06.js
@@ -1,12 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, isApprox, isApproxVec, quat4 */
 "use strict";
 
 function test() {
   let q1 = quat4.create();
 
   ok(q1, "Should have created a quaternion with quat4.create().");
   is(q1.length, 4, "A quat4 should have 4 elements.");
 
--- a/browser/devtools/tilt/test/browser_tilt_math07.js
+++ b/browser/devtools/tilt/test/browser_tilt_math07.js
@@ -1,12 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, isApproxVec, quat4 */
 "use strict";
 
 function test() {
   let q1 = quat4.create([1, 2, 3, 4]);
   let q2 = quat4.create([5, 6, 7, 8]);
 
   quat4.multiply(q1, q2);
   ok(isApproxVec(q1, [24, 48, 48, -6]),
--- a/browser/devtools/tilt/test/browser_tilt_picking.js
+++ b/browser/devtools/tilt/test/browser_tilt_picking.js
@@ -1,14 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, info, waitForExplicitFinish, finish, gBrowser */
-/*global isTiltEnabled, isWebGLSupported, createTab, createTilt */
-/*global Services, InspectorUI, TILT_DESTROYED */
 "use strict";
 
 function test() {
   if (!isTiltEnabled()) {
     info("Skipping picking test because Tilt isn't enabled.");
     return;
   }
   if (!isWebGLSupported()) {
@@ -30,23 +26,23 @@ function test() {
           presenter.pickNode(canvas.width / 2, canvas.height / 2, {
             onpick: function(data)
             {
               ok(data.index > 0,
                 "Simply picking a node didn't work properly.");
               ok(!presenter.highlight.disabled,
                 "After only picking a node, it shouldn't be highlighted.");
 
-              Services.obs.addObserver(cleanup, TILT_DESTROYED, false);
+              Services.obs.addObserver(cleanup, DESTROYED, false);
               InspectorUI.closeInspectorUI();
             }
           });
         };
       }
     });
   });
 }
 
 function cleanup() {
-  Services.obs.removeObserver(cleanup, TILT_DESTROYED);
+  Services.obs.removeObserver(cleanup, DESTROYED);
   gBrowser.removeCurrentTab();
   finish();
 }
--- a/browser/devtools/tilt/test/browser_tilt_picking_delete.js
+++ b/browser/devtools/tilt/test/browser_tilt_picking_delete.js
@@ -1,15 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
 
-/*global ok, is, info, waitForExplicitFinish, finish, gBrowser */
-/*global isTiltEnabled, isWebGLSupported, createTab, createTilt */
-/*global Services, InspectorUI, TILT_DESTROYED */
-"use strict";
+let presenter;
 
 function test() {
   if (!isTiltEnabled()) {
     info("Skipping picking delete test because Tilt isn't enabled.");
     return;
   }
   if (!isWebGLSupported()) {
     info("Skipping picking delete test because WebGL isn't supported.");
@@ -17,51 +15,55 @@ function test() {
   }
 
   waitForExplicitFinish();
 
   createTab(function() {
     createTilt({
       onTiltOpen: function(instance)
       {
-        let presenter = instance.presenter;
-        let canvas = presenter.canvas;
+        presenter = instance.presenter;
+        Services.obs.addObserver(whenNodeRemoved, NODE_REMOVED, false);
 
         presenter.onSetupMesh = function() {
-
-          presenter.highlightNodeAt(canvas.width / 2, canvas.height / 2, {
+          presenter.highlightNodeAt(presenter.canvas.width / 2,
+                                    presenter.canvas.height / 2, {
             onpick: function()
             {
               ok(presenter._currentSelection > 0,
                 "Highlighting a node didn't work properly.");
               ok(!presenter.highlight.disabled,
                 "After highlighting a node, it should be highlighted. D'oh.");
 
               presenter.deleteNode();
-
-              ok(presenter._currentSelection > 0,
-                "Deleting a node shouldn't change the current selection.");
-              ok(presenter.highlight.disabled,
-                "After deleting a node, it shouldn't be highlighted.");
-
-              let nodeIndex = presenter._currentSelection;
-              let meshData = presenter.meshData;
-
-              for (let i = 0, k = 36 * nodeIndex; i < 36; i++) {
-                is(meshData.vertices[i + k], 0,
-                  "The stack vertices weren't degenerated properly.");
-              }
-
-              Services.obs.addObserver(cleanup, TILT_DESTROYED, false);
-              InspectorUI.closeInspectorUI();
             }
           });
         };
       }
     });
   });
 }
 
+function whenNodeRemoved() {
+  ok(presenter._currentSelection > 0,
+    "Deleting a node shouldn't change the current selection.");
+  ok(presenter.highlight.disabled,
+    "After deleting a node, it shouldn't be highlighted.");
+
+  let nodeIndex = presenter._currentSelection;
+  let meshData = presenter.meshData;
+
+  for (let i = 0, k = 36 * nodeIndex; i < 36; i++) {
+    is(meshData.vertices[i + k], 0,
+      "The stack vertices weren't degenerated properly.");
+  }
+
+  executeSoon(function() {
+    Services.obs.addObserver(cleanup, DESTROYED, false);
+    InspectorUI.closeInspectorUI();
+  });
+}
+
 function cleanup() {
-  Services.obs.removeObserver(cleanup, TILT_DESTROYED);
+  Services.obs.removeObserver(cleanup, DESTROYED);
   gBrowser.removeCurrentTab();
   finish();
 }
--- a/browser/devtools/tilt/test/browser_tilt_picking_highlight01.js
+++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight01.js
@@ -1,15 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
 
-/*global ok, is, info, waitForExplicitFinish, finish, gBrowser */
-/*global isTiltEnabled, isWebGLSupported, createTab, createTilt */
-/*global Services, InspectorUI, TILT_DESTROYED */
-"use strict";
+let presenter;
 
 function test() {
   if (!isTiltEnabled()) {
     info("Skipping highlight test because Tilt isn't enabled.");
     return;
   }
   if (!isWebGLSupported()) {
     info("Skipping highlight test because WebGL isn't supported.");
@@ -17,34 +15,53 @@ function test() {
   }
 
   waitForExplicitFinish();
 
   createTab(function() {
     createTilt({
       onTiltOpen: function(instance)
       {
-        let presenter = instance.presenter;
+        presenter = instance.presenter;
+        Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false);
 
         presenter.onSetupMesh = function() {
           let contentDocument = presenter.contentWindow.document;
-          let body = contentDocument.getElementsByTagName("body")[0];
-
-          presenter.highlightNode(body);
+          let div = contentDocument.getElementById("first-law");
 
-          ok(presenter._currentSelection > 0,
-            "Highlighting a node didn't work properly.");
-          ok(!presenter.highlight.disabled,
-            "After highlighting a node, it should be highlighted. D'oh.");
-
-          Services.obs.addObserver(cleanup, TILT_DESTROYED, false);
-          InspectorUI.closeInspectorUI();
+          presenter.highlightNode(div);
         };
       }
     });
   });
 }
 
+function whenHighlighting() {
+  ok(presenter._currentSelection > 0,
+    "Highlighting a node didn't work properly.");
+  ok(!presenter.highlight.disabled,
+    "After highlighting a node, it should be highlighted. D'oh.");
+
+  executeSoon(function() {
+    Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false);
+    presenter.highlightNode(null);
+  });
+}
+
+function whenUnhighlighting() {
+  ok(presenter._currentSelection < 0,
+    "Unhighlighting a should remove the current selection.");
+  ok(presenter.highlight.disabled,
+    "After unhighlighting a node, it shouldn't be highlighted anymore. D'oh.");
+
+  executeSoon(function() {
+    Services.obs.addObserver(cleanup, DESTROYED, false);
+    InspectorUI.closeInspectorUI();
+  });
+}
+
 function cleanup() {
-  Services.obs.removeObserver(cleanup, TILT_DESTROYED);
+  Services.obs.removeObserver(whenHighlighting, HIGHLIGHTING);
+  Services.obs.removeObserver(whenUnhighlighting, UNHIGHLIGHTING);
+  Services.obs.removeObserver(cleanup, DESTROYED);
   gBrowser.removeCurrentTab();
   finish();
 }
--- a/browser/devtools/tilt/test/browser_tilt_picking_highlight02.js
+++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight02.js
@@ -1,15 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
 
-/*global ok, is, info, waitForExplicitFinish, finish, gBrowser */
-/*global isTiltEnabled, isWebGLSupported, createTab, createTilt */
-/*global Services, InspectorUI, TILT_DESTROYED */
-"use strict";
+let presenter;
 
 function test() {
   if (!isTiltEnabled()) {
     info("Skipping highlight test because Tilt isn't enabled.");
     return;
   }
   if (!isWebGLSupported()) {
     info("Skipping highlight test because WebGL isn't supported.");
@@ -17,36 +15,51 @@ function test() {
   }
 
   waitForExplicitFinish();
 
   createTab(function() {
     createTilt({
       onTiltOpen: function(instance)
       {
-        let presenter = instance.presenter;
-        let canvas = presenter.canvas;
+        presenter = instance.presenter;
+        Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false);
 
         presenter.onSetupMesh = function() {
-
-          presenter.highlightNodeAt(canvas.width / 2, canvas.height / 2, {
-            onpick: function()
-            {
-              ok(presenter._currentSelection > 0,
-                "Highlighting a node didn't work properly.");
-              ok(!presenter.highlight.disabled,
-                "After highlighting a node, it should be highlighted. D'oh.");
-
-              Services.obs.addObserver(cleanup, TILT_DESTROYED, false);
-              InspectorUI.closeInspectorUI();
-            }
-          });
+          presenter.highlightNodeAt(presenter.canvas.width / 2,
+                                    presenter.canvas.height / 2);
         };
       }
     });
   });
 }
 
+function whenHighlighting() {
+  ok(presenter._currentSelection > 0,
+    "Highlighting a node didn't work properly.");
+  ok(!presenter.highlight.disabled,
+    "After highlighting a node, it should be highlighted. D'oh.");
+
+  executeSoon(function() {
+    Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false);
+    presenter.highlightNodeAt(-1, -1);
+  });
+}
+
+function whenUnhighlighting() {
+  ok(presenter._currentSelection < 0,
+    "Unhighlighting a should remove the current selection.");
+  ok(presenter.highlight.disabled,
+    "After unhighlighting a node, it shouldn't be highlighted anymore. D'oh.");
+
+  executeSoon(function() {
+    Services.obs.addObserver(cleanup, DESTROYED, false);
+    InspectorUI.closeInspectorUI();
+  });
+}
+
 function cleanup() {
-  Services.obs.removeObserver(cleanup, TILT_DESTROYED);
+  Services.obs.removeObserver(whenHighlighting, HIGHLIGHTING);
+  Services.obs.removeObserver(whenUnhighlighting, UNHIGHLIGHTING);
+  Services.obs.removeObserver(cleanup, DESTROYED);
   gBrowser.removeCurrentTab();
   finish();
 }
--- a/browser/devtools/tilt/test/browser_tilt_picking_highlight03.js
+++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight03.js
@@ -1,15 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
 
-/*global ok, is, info, waitForExplicitFinish, finish, gBrowser */
-/*global isTiltEnabled, isWebGLSupported, createTab, createTilt */
-/*global Services, InspectorUI, TILT_DESTROYED */
-"use strict";
+let presenter;
 
 function test() {
   if (!isTiltEnabled()) {
     info("Skipping highlight test because Tilt isn't enabled.");
     return;
   }
   if (!isWebGLSupported()) {
     info("Skipping highlight test because WebGL isn't supported.");
@@ -17,31 +15,50 @@ function test() {
   }
 
   waitForExplicitFinish();
 
   createTab(function() {
     createTilt({
       onTiltOpen: function(instance)
       {
-        let presenter = instance.presenter;
+        presenter = instance.presenter;
+        Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false);
 
         presenter.onSetupMesh = function() {
-          presenter.highlightNodeFor(1);
-
-          ok(presenter._currentSelection > 0,
-            "Highlighting a node didn't work properly.");
-          ok(!presenter.highlight.disabled,
-            "After highlighting a node, it should be highlighted. D'oh.");
-
-          Services.obs.addObserver(cleanup, TILT_DESTROYED, false);
-          InspectorUI.closeInspectorUI();
+          presenter.highlightNodeFor(5); // 1 = html, 2 = body, 3 = first div
         };
       }
     });
   });
 }
 
+function whenHighlighting() {
+  ok(presenter._currentSelection > 0,
+    "Highlighting a node didn't work properly.");
+  ok(!presenter.highlight.disabled,
+    "After highlighting a node, it should be highlighted. D'oh.");
+
+  executeSoon(function() {
+    Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false);
+    presenter.highlightNodeFor(-1);
+  });
+}
+
+function whenUnhighlighting() {
+  ok(presenter._currentSelection < 0,
+    "Unhighlighting a should remove the current selection.");
+  ok(presenter.highlight.disabled,
+    "After unhighlighting a node, it shouldn't be highlighted anymore. D'oh.");
+
+  executeSoon(function() {
+    Services.obs.addObserver(cleanup, DESTROYED, false);
+    InspectorUI.closeInspectorUI();
+  });
+}
+
 function cleanup() {
-  Services.obs.removeObserver(cleanup, TILT_DESTROYED);
+  Services.obs.removeObserver(whenHighlighting, HIGHLIGHTING);
+  Services.obs.removeObserver(whenUnhighlighting, UNHIGHLIGHTING);
+  Services.obs.removeObserver(cleanup, DESTROYED);
   gBrowser.removeCurrentTab();
   finish();
 }
--- a/browser/devtools/tilt/test/browser_tilt_utils01.js
+++ b/browser/devtools/tilt/test/browser_tilt_utils01.js
@@ -1,12 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, TiltUtils */
 "use strict";
 
 function test() {
   let prefs = TiltUtils.Preferences;
   ok(prefs, "The TiltUtils.Preferences wasn't found.");
 
   prefs.create("test-pref-bool", "boolean", true);
   prefs.create("test-pref-int", "integer", 42);
--- a/browser/devtools/tilt/test/browser_tilt_utils02.js
+++ b/browser/devtools/tilt/test/browser_tilt_utils02.js
@@ -1,12 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, TiltUtils */
 "use strict";
 
 function test() {
   let l10 = TiltUtils.L10n;
   ok(l10, "The TiltUtils.L10n wasn't found.");
 
 
   ok(l10.stringBundle,
--- a/browser/devtools/tilt/test/browser_tilt_utils03.js
+++ b/browser/devtools/tilt/test/browser_tilt_utils03.js
@@ -1,12 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, TiltUtils */
 "use strict";
 
 function test() {
   let dom = TiltUtils.DOM;
 
   is(dom.parentNode, null,
     "The parent node should not be initially set.");
 
--- a/browser/devtools/tilt/test/browser_tilt_utils04.js
+++ b/browser/devtools/tilt/test/browser_tilt_utils04.js
@@ -1,12 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, gBrowser, TiltUtils */
 "use strict";
 
 function test() {
   let dom = TiltUtils.DOM;
   ok(dom, "The TiltUtils.DOM wasn't found.");
 
 
   is(dom.initCanvas(), null,
--- a/browser/devtools/tilt/test/browser_tilt_utils05.js
+++ b/browser/devtools/tilt/test/browser_tilt_utils05.js
@@ -1,13 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
 
-/*global ok, is, waitForExplicitFinish, finish, gBrowser, TiltUtils */
-"use strict";
+Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
 
 function init(callback) {
   let iframe = gBrowser.ownerDocument.createElement("iframe");
 
   iframe.addEventListener("load", function onLoad() {
     iframe.removeEventListener("load", onLoad, true);
     callback(iframe);
 
@@ -50,25 +50,25 @@ function test() {
 
     is(cwDimensions.width - iframe.contentWindow.scrollMaxX,
       iframe.contentWindow.innerWidth,
       "The content window width wasn't calculated correctly.");
     is(cwDimensions.height - iframe.contentWindow.scrollMaxY,
       iframe.contentWindow.innerHeight,
       "The content window height wasn't calculated correctly.");
 
-
-    let nodeCoordinates = dom.getNodeCoordinates(
+    let nodeCoordinates = LayoutHelpers.getRect(
       iframe.contentDocument.getElementById("test-div"), iframe.contentWindow);
 
-    let frameOffset = dom.getFrameOffset(iframe);
+    let frameOffset = LayoutHelpers.getIframeContentOffset(iframe);
+    let frameRect = iframe.getBoundingClientRect();
 
-    is(nodeCoordinates.top, frameOffset.top + 98,
+    is(nodeCoordinates.top, frameRect.top + frameOffset[0] + 98,
       "The node coordinates top value wasn't calculated correctly.");
-    is(nodeCoordinates.left, frameOffset.left + 76,
+    is(nodeCoordinates.left, frameRect.left + frameOffset[1] + 76,
       "The node coordinates left value wasn't calculated correctly.");
     is(nodeCoordinates.width, 123,
       "The node coordinates width value wasn't calculated correctly.");
     is(nodeCoordinates.height, 456,
       "The node coordinates height value wasn't calculated correctly.");
 
 
     let store = dom.traverse(iframe.contentWindow);
--- a/browser/devtools/tilt/test/browser_tilt_utils06.js
+++ b/browser/devtools/tilt/test/browser_tilt_utils06.js
@@ -1,12 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, isnot, gBrowser, TiltUtils */
 "use strict";
 
 let someObject = {
   a: 1,
   func: function()
   {
     this.b = 2;
   }
@@ -38,13 +36,9 @@ function test() {
     "The finalize function wasn't called when an object was destroyed.");
 
 
   TiltUtils.destroyObject(someObject);
   is(typeof someObject.a, "undefined",
     "Not all members of the destroyed object were deleted.");
   is(typeof someObject.func, "undefined",
     "Not all function members of the destroyed object were deleted.");
-
-
-  is(TiltUtils.getBrowserWindow(), window,
-    "The getBrowserWindow() function didn't return the correct window.");
 }
--- a/browser/devtools/tilt/test/browser_tilt_visualizer.js
+++ b/browser/devtools/tilt/test/browser_tilt_visualizer.js
@@ -1,31 +1,29 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, info, isApproxVec, isTiltEnabled, isWebGLSupported, gBrowser*/
-/*global TiltVisualizer */
 "use strict";
 
 function test() {
   if (!isTiltEnabled()) {
     info("Skipping notifications test because Tilt isn't enabled.");
     return;
   }
   if (!isWebGLSupported()) {
     info("Skipping visualizer test because WebGL isn't supported.");
     return;
   }
 
   let webGLError = false;
   let webGLLoad = false;
 
   let visualizer = new TiltVisualizer({
+    chromeWindow: window,
+    contentWindow: gBrowser.selectedBrowser.contentWindow,
     parentNode: gBrowser.selectedBrowser.parentNode,
-    contentWindow: gBrowser.selectedBrowser.contentWindow,
     requestAnimationFrame: window.mozRequestAnimationFrame,
     inspectorUI: window.InspectorUI,
 
     onError: function onWebGLError()
     {
       webGLError = true;
     },
 
--- a/browser/devtools/tilt/test/browser_tilt_zoom.js
+++ b/browser/devtools/tilt/test/browser_tilt_zoom.js
@@ -1,30 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global ok, is, info, waitForExplicitFinish, finish, executeSoon, gBrowser */
-/*global isApprox, isTiltEnabled, isWebGLSupported, createTab, createTilt */
-/*global Services, EventUtils, TiltUtils, InspectorUI, TILT_DESTROYED */
 "use strict";
 
 const ZOOM = 2;
 const RESIZE = 50;
 
-function setZoom(value) {
-  gBrowser.selectedBrowser.markupDocumentViewer.fullZoom = value;
-}
+function test() {
+  let random = Math.random() * 10;
 
-function getZoom() {
-  return gBrowser.selectedBrowser.markupDocumentViewer.fullZoom;
-}
-
-function test() {
-  TiltUtils.setDocumentZoom(Math.random());
-  is(getZoom(), TiltUtils.getDocumentZoom(),
+  TiltUtils.setDocumentZoom(window, random);
+  ok(isApprox(TiltUtils.getDocumentZoom(window), random),
     "The getDocumentZoom utility function didn't return the expected results.");
 
 
   if (!isTiltEnabled()) {
     info("Skipping controller test because Tilt isn't enabled.");
     return;
   }
   if (!isWebGLSupported()) {
@@ -33,67 +23,67 @@ function test() {
   }
 
   waitForExplicitFinish();
 
   createTab(function() {
     createTilt({
       onInspectorOpen: function()
       {
-        setZoom(ZOOM);
+        TiltUtils.setDocumentZoom(window, ZOOM);
       },
       onTiltOpen: function(instance)
       {
         ok(isApprox(instance.presenter.transforms.zoom, ZOOM),
           "The presenter transforms zoom wasn't initially set correctly.");
 
         let contentWindow = gBrowser.selectedBrowser.contentWindow;
         let initialWidth = contentWindow.innerWidth;
         let initialHeight = contentWindow.innerHeight;
 
         let renderer = instance.presenter.renderer;
         let arcball = instance.controller.arcball;
 
         ok(isApprox(contentWindow.innerWidth * ZOOM, renderer.width, 1),
-          "The renderer width wasn't set correctly.");
+          "The renderer width wasn't set correctly before the resize.");
         ok(isApprox(contentWindow.innerHeight * ZOOM, renderer.height, 1),
-          "The renderer height wasn't set correctly.");
+          "The renderer height wasn't set correctly before the resize.");
 
         ok(isApprox(contentWindow.innerWidth * ZOOM, arcball.width, 1),
-          "The arcball width wasn't set correctly.");
+          "The arcball width wasn't set correctly before the resize.");
         ok(isApprox(contentWindow.innerHeight * ZOOM, arcball.height, 1),
-          "The arcball height wasn't set correctly.");
+          "The arcball height wasn't set correctly before the resize.");
 
 
         window.resizeBy(-RESIZE * ZOOM, -RESIZE * ZOOM);
 
         executeSoon(function() {
           ok(isApprox(contentWindow.innerWidth + RESIZE, initialWidth, 1),
-            "The content window width wasn't set correctly.");
+            "The content window width wasn't set correctly after the resize.");
           ok(isApprox(contentWindow.innerHeight + RESIZE, initialHeight, 1),
-            "The content window height wasn't set correctly.");
+            "The content window height wasn't set correctly after the resize.");
 
           ok(isApprox(contentWindow.innerWidth * ZOOM, renderer.width, 1),
-            "The renderer width wasn't set correctly.");
+            "The renderer width wasn't set correctly after the resize.");
           ok(isApprox(contentWindow.innerHeight * ZOOM, renderer.height, 1),
-            "The renderer height wasn't set correctly.");
+            "The renderer height wasn't set correctly after the resize.");
 
           ok(isApprox(contentWindow.innerWidth * ZOOM, arcball.width, 1),
-            "The arcball width wasn't set correctly.");
+            "The arcball width wasn't set correctly after the resize.");
           ok(isApprox(contentWindow.innerHeight * ZOOM, arcball.height, 1),
-            "The arcball height wasn't set correctly.");
+            "The arcball height wasn't set correctly after the resize.");
 
 
           window.resizeBy(RESIZE * ZOOM, RESIZE * ZOOM);
 
-          Services.obs.addObserver(cleanup, TILT_DESTROYED, false);
+          Services.obs.addObserver(cleanup, DESTROYED, false);
           InspectorUI.closeInspectorUI();
         });
       },
     });
   });
 }
 
 function cleanup() {
-  Services.obs.removeObserver(cleanup, TILT_DESTROYED);
+  Services.obs.removeObserver(cleanup, DESTROYED);
   gBrowser.removeCurrentTab();
   finish();
 }
--- a/browser/devtools/tilt/test/head.js
+++ b/browser/devtools/tilt/test/head.js
@@ -1,13 +1,10 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/*global Services, Components, gBrowser, executeSoon, info */
-/*global InspectorUI, Tilt, TiltGL, EPSILON */
 "use strict";
 
 let tempScope = {};
 Components.utils.import("resource:///modules/devtools/TiltGL.jsm", tempScope);
 Components.utils.import("resource:///modules/devtools/TiltMath.jsm", tempScope);
 Components.utils.import("resource:///modules/devtools/TiltUtils.jsm", tempScope);
 Components.utils.import("resource:///modules/devtools/TiltVisualizer.jsm", tempScope);
 let TiltGL = tempScope.TiltGL;
@@ -23,38 +20,44 @@ let TiltVisualizer = tempScope.TiltVisua
 
 const DEFAULT_HTML = "data:text/html," +
   "<DOCTYPE html>" +
   "<html>" +
     "<head>" +
       "<title>Three Laws</title>" +
     "</head>" +
     "<body>" +
-      "<div>" +
-        "A robot may not injure a human being or, through inaction, allow a" +
+      "<div id='first-law'>" +
+        "A robot may not injure a human being or, through inaction, allow a " +
         "human being to come to harm." +
       "</div>" +
       "<div>" +
-        "A robot must obey the orders given to it by human beings, except" +
+        "A robot must obey the orders given to it by human beings, except " +
         "where such orders would conflict with the First Law." +
       "</div>" +
       "<div>" +
-        "A robot must protect its own existence as long as such protection" +
+        "A robot must protect its own existence as long as such protection " +
         "does not conflict with the First or Second Laws." +
       "</div>" +
     "<body>" +
   "</html>";
 
 const INSPECTOR_OPENED = InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED;
 const INSPECTOR_CLOSED = InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED;
 
-const TILT_INITIALIZED = Tilt.NOTIFICATIONS.INITIALIZED;
-const TILT_DESTROYED = Tilt.NOTIFICATIONS.DESTROYED;
-const TILT_SHOWN = Tilt.NOTIFICATIONS.SHOWN;
-const TILT_HIDDEN = Tilt.NOTIFICATIONS.HIDDEN;
+const INITIALIZING = Tilt.NOTIFICATIONS.INITIALIZING;
+const INITIALIZED = Tilt.NOTIFICATIONS.INITIALIZED;
+const DESTROYING = Tilt.NOTIFICATIONS.DESTROYING;
+const BEFORE_DESTROYED = Tilt.NOTIFICATIONS.BEFORE_DESTROYED;
+const DESTROYED = Tilt.NOTIFICATIONS.DESTROYED;
+const SHOWN = Tilt.NOTIFICATIONS.SHOWN;
+const HIDDEN = Tilt.NOTIFICATIONS.HIDDEN;
+const HIGHLIGHTING = Tilt.NOTIFICATIONS.HIGHLIGHTING;
+const UNHIGHLIGHTING = Tilt.NOTIFICATIONS.UNHIGHLIGHTING;
+const NODE_REMOVED = Tilt.NOTIFICATIONS.NODE_REMOVED;
 
 const TILT_ENABLED = Services.prefs.getBoolPref("devtools.tilt.enabled");
 const INSP_ENABLED = Services.prefs.getBoolPref("devtools.inspector.enabled");
 
 
 function isTiltEnabled() {
   return TILT_ENABLED && INSP_ENABLED;
 }
@@ -117,47 +120,49 @@ function createTab(callback, location) {
   }, true);
 
   gBrowser.selectedBrowser.contentWindow.location = location || DEFAULT_HTML;
   return tab;
 }
 
 
 function createTilt(callbacks, close) {
+  Services.prefs.setBoolPref("webgl.verbose", true);
+
   Services.obs.addObserver(onInspectorOpen, INSPECTOR_OPENED, false);
   InspectorUI.toggleInspectorUI();
 
   function onInspectorOpen() {
     Services.obs.removeObserver(onInspectorOpen, INSPECTOR_OPENED);
 
     executeSoon(function() {
       if ("function" === typeof callbacks.onInspectorOpen) {
         callbacks.onInspectorOpen();
       }
-      Services.obs.addObserver(onTiltOpen, TILT_INITIALIZED, false);
+      Services.obs.addObserver(onTiltOpen, INITIALIZING, false);
       Tilt.initialize();
     });
   }
 
   function onTiltOpen() {
-    Services.obs.removeObserver(onTiltOpen, TILT_INITIALIZED);
+    Services.obs.removeObserver(onTiltOpen, INITIALIZING);
 
     executeSoon(function() {
       if ("function" === typeof callbacks.onTiltOpen) {
         callbacks.onTiltOpen(Tilt.visualizers[Tilt.currentWindowId]);
       }
       if (close) {
-        Services.obs.addObserver(onTiltClose, TILT_DESTROYED, false);
+        Services.obs.addObserver(onTiltClose, DESTROYED, false);
         Tilt.destroy(Tilt.currentWindowId);
       }
     });
   }
 
   function onTiltClose() {
-    Services.obs.removeObserver(onTiltClose, TILT_DESTROYED);
+    Services.obs.removeObserver(onTiltClose, DESTROYED);
 
     executeSoon(function() {
       if ("function" === typeof callbacks.onTiltClose) {
         callbacks.onTiltClose();
       }
       if (close) {
         Services.obs.addObserver(onInspectorClose, INSPECTOR_CLOSED, false);
         InspectorUI.closeInspectorUI();
--- a/browser/devtools/webconsole/GcliCommands.jsm
+++ b/browser/devtools/webconsole/GcliCommands.jsm
@@ -72,30 +72,39 @@ gcli.addCommand({
 });
 
 /**
  * 'console clear' command
  */
 gcli.addCommand({
   name: "console clear",
   description: gcli.lookup("consoleclearDesc"),
-  exec: function(args, context) {
+  exec: function Command_consoleClear(args, context) {
+    let window = context.environment.chromeDocument.defaultView;
     let hud = HUDService.getHudReferenceById(context.environment.hudId);
-    hud.gcliterm.clearOutput();
+
+    // Use a timeout so we also clear the reporting of the clear command
+    let threadManager = Components.classes["@mozilla.org/thread-manager;1"]
+        .getService(Components.interfaces.nsIThreadManager);
+    threadManager.mainThread.dispatch({
+      run: function() {
+        hud.gcliterm.clearOutput();
+      }
+    }, Components.interfaces.nsIThread.DISPATCH_NORMAL);
   }
 });
 
 
 /**
  * 'console close' command
  */
 gcli.addCommand({
   name: "console close",
   description: gcli.lookup("consolecloseDesc"),
-  exec: function(args, context) {
+  exec: function Command_consoleClose(args, context) {
     let tab = HUDService.getHudReferenceById(context.environment.hudId).tab;
     HUDService.deactivateHUDForContext(tab);
   }
 });
 
 /**
  * 'inspect' command
  */
@@ -107,13 +116,12 @@ gcli.addCommand({
     {
       name: "node",
       type: "node",
       description: gcli.lookup("inspectNodeDesc"),
       manual: gcli.lookup("inspectNodeManual")
     }
   ],
   exec: function Command_inspect(args, context) {
-    let hud = HUDService.getHudReferenceById(context.environment.hudId);
-    let InspectorUI = hud.gcliterm.document.defaultView.InspectorUI;
-    InspectorUI.openInspectorUI(args.node);
+    let document = context.environment.chromeDocument;
+    document.defaultView.InspectorUI.openInspectorUI(args.node);
   }
 });
--- a/browser/devtools/webconsole/HUDService.jsm
+++ b/browser/devtools/webconsole/HUDService.jsm
@@ -2062,17 +2062,26 @@ HUD_SERVICE.prototype =
    *        The message reported by the console service.
    * @return void
    */
   logConsoleAPIMessage: function HS_logConsoleAPIMessage(aHUDId, aMessage)
   {
     // Pipe the message to createMessageNode().
     let hud = HUDService.hudReferences[aHUDId];
     function formatResult(x) {
-      return (typeof(x) == "string") ? x : hud.jsterm.formatResult(x);
+      if (typeof(x) == "string") {
+        return x;
+      }
+      if (hud.gcliterm) {
+        return hud.gcliterm.formatResult(x);
+      }
+      if (hud.jsterm) {
+        return hud.jsterm.formatResult(x);
+      }
+      return x;
     }
 
     let body = null;
     let clipboardText = null;
     let sourceURL = null;
     let sourceLine = 0;
     let level = aMessage.level;
     let args = aMessage.arguments;
@@ -4694,17 +4703,17 @@ function JSTermHelper(aJSTerm)
     aJSTerm.helperEvaluated = true;
     let propPanel = aJSTerm.openPropertyPanel(null, unwrap(aObject));
     propPanel.panel.setAttribute("hudId", aJSTerm.hudId);
   };
 
   aJSTerm.sandbox.inspectrules = function JSTH_inspectrules(aNode)
   {
     aJSTerm.helperEvaluated = true;
-    let doc = aJSTerm.parentNode.ownerDocument;
+    let doc = aJSTerm.inputNode.ownerDocument;
     let win = doc.defaultView;
     let panel = createElement(doc, "panel", {
       label: "CSS Rules",
       titlebar: "normal",
       noautofocus: "true",
       noautohide: "true",
       close: "true",
       width: 350,
@@ -4808,16 +4817,17 @@ function JSTerm(aContext, aParentNode, a
 {
   // set the context, attach the UI by appending to aParentNode
 
   this.application = appName();
   this.context = aContext;
   this.parentNode = aParentNode;
   this.mixins = aMixin;
   this.console = aConsole;
+  this.document = aParentNode.ownerDocument
 
   this.setTimeout = aParentNode.ownerDocument.defaultView.setTimeout;
 
   let node = aParentNode;
   while (!node.hasAttribute("id")) {
     node = node.parentNode;
   }
   this.hudId = node.getAttribute("id");
@@ -5013,17 +5023,17 @@ JSTerm.prototype = {
           }
           catch (ex) {
             self.console.error(ex);
           }
         }
       });
     }
 
-    let doc = self.parentNode.ownerDocument;
+    let doc = self.document;
     let parent = doc.getElementById("mainPopupSet");
     let title = (aEvalString
         ? HUDService.getFormatStr("jsPropertyInspectTitle", [aEvalString])
         : HUDService.getStr("jsPropertyTitle"));
 
     propPanel = new PropertyPanel(parent, doc, title, aOutputObject, buttons);
     propPanel.linkNode = aAnchor;
 
@@ -5042,17 +5052,17 @@ JSTerm.prototype = {
    *        String that was evaluated to get the aOutputObject.
    * @param object aResultObject
    *        The evaluation result object.
    * @param object aOutputString
    *        The output string to be written to the outputNode.
    */
   writeOutputJS: function JST_writeOutputJS(aEvalString, aOutputObject, aOutputString)
   {
-    let node = ConsoleUtils.createMessageNode(this.parentNode.ownerDocument,
+    let node = ConsoleUtils.createMessageNode(this.document,
                                               CATEGORY_OUTPUT,
                                               SEVERITY_LOG,
                                               aOutputString,
                                               this.hudId);
 
     let linkNode = node.querySelector(".webconsole-msg-body");
 
     linkNode.classList.add("hud-clickable");
@@ -5091,17 +5101,17 @@ JSTerm.prototype = {
    * @param number aCategory
    *        The category of message: one of the CATEGORY_ constants.
    * @param number aSeverity
    *        The severity of message: one of the SEVERITY_ constants.
    * @returns void
    */
   writeOutput: function JST_writeOutput(aOutputMessage, aCategory, aSeverity)
   {
-    let node = ConsoleUtils.createMessageNode(this.parentNode.ownerDocument,
+    let node = ConsoleUtils.createMessageNode(this.document,
                                               aCategory, aSeverity,
                                               aOutputMessage, this.hudId);
 
     ConsoleUtils.outputMessageNode(node, this.hudId);
   },
 
   /**
    * Format the jsterm execution result based on its type.
@@ -6814,16 +6824,17 @@ let commandExports = undefined;
  */
 function GcliTerm(aContentWindow, aHudId, aDocument, aConsole, aHintNode, aConsoleWrap)
 {
   this.context = Cu.getWeakReference(aContentWindow);
   this.hudId = aHudId;
   this.document = aDocument;
   this.console = aConsole;
   this.hintNode = aHintNode;
+  this._window = this.context.get().QueryInterface(Ci.nsIDOMWindow);
 
   this.createUI();
   this.createSandbox();
 
   this.show = this.show.bind(this);
   this.hide = this.hide.bind(this);
 
   // Allow GCLI:Inputter to decide how and when to open a scratchpad window
@@ -6836,17 +6847,21 @@ function GcliTerm(aContentWindow, aHudId
       aValue = aValue.replace(/^\s*{\s*/, '');
       ScratchpadManager.openScratchpad({ text: aValue });
       return true;
     },
     linkText: stringBundle.GetStringFromName('scratchpad.linkText')
   };
 
   this.opts = {
-    environment: { hudId: this.hudId },
+    environment: {
+      hudId: this.hudId,
+      chromeDocument: this.document,
+      contentDocument: aContentWindow.document
+    },
     chromeDocument: this.document,
     contentDocument: aContentWindow.document,
     jsEnvironment: {
       globalObject: unwrap(aContentWindow),
       evalFunction: this.evalInSandbox.bind(this)
     },
     inputElement: this.inputNode,
     completeElement: this.completeNode,
@@ -6906,16 +6921,17 @@ GcliTerm.prototype = {
     delete this.opts.contentDocument;
     delete this.opts.jsEnvironment;
     delete this.opts.gcliTerm;
 
     delete this.context;
     delete this.document;
     delete this.console;
     delete this.hintNode;
+    delete this._window;
 
     delete this.sandbox;
     delete this.element
     delete this.inputStack
     delete this.completeNode
     delete this.inputNode
   },
 
@@ -6965,24 +6981,69 @@ GcliTerm.prototype = {
   onCommandOutput: function Gcli_onCommandOutput(aEvent)
   {
     // When we can update the history of the console, then we should stop
     // filtering incomplete reports.
     if (!aEvent.output.completed) {
       return;
     }
 
-    this.writeOutput(aEvent.output.typed, { category: CATEGORY_INPUT });
-
-    if (aEvent.output.output == null) {
+    this.writeOutput(aEvent.output.typed, CATEGORY_INPUT);
+
+    // This is an experiment to see how much people yell when we stop reporting
+    // undefined replies.
+    if (aEvent.output.output === undefined) {
       return;
     }
 
     let output = aEvent.output.output;
-    if (aEvent.output.command.returnType == "html" && typeof output == "string") {
+    let declaredType = aEvent.output.command.returnType || "";
+
+    if (declaredType == "object") {
+      let actualType = typeof output;
+      if (output === null) {
+        output = "null";
+      }
+      else if (actualType == "string") {
+        output = "\"" + output + "\"";
+      }
+      else if (actualType == "object" || actualType == "function") {
+        let formatOpts = [ nameObject(output) ];
+        output = stringBundle.formatStringFromName('gcliterm.instanceLabel',
+                formatOpts, formatOpts.length);
+        let linkNode = this.document.createElementNS(HTML_NS, 'html:span');
+        linkNode.appendChild(this.document.createTextNode(output));
+        linkNode.classList.add("hud-clickable");
+        linkNode.setAttribute("aria-haspopup", "true");
+
+        // Make the object bring up the property panel.
+        linkNode.addEventListener("mousedown", function(aEv) {
+          this._startX = aEv.clientX;
+          this._startY = aEv.clientY;
+        }.bind(this), false);
+
+        linkNode.addEventListener("click", function(aEv) {
+          if (aEv.detail != 1 || aEv.button != 0 ||
+              (this._startX != aEv.clientX && this._startY != aEv.clientY)) {
+            return;
+          }
+
+          if (!this._panelOpen) {
+            let propPanel = this.openPropertyPanel(aEvent.output.typed, aEvent.output.output, this);
+            propPanel.panel.setAttribute("hudId", this.hudId);
+            this._panelOpen = true;
+          }
+        }.bind(this), false);
+
+        output = linkNode;
+      }
+      // else if (actualType == number/boolean/undefined) do nothing
+    }
+
+    if (declaredType == "html" && typeof output == "string") {
       output = this.document.createRange().createContextualFragment(
           '<div xmlns="' + HTML_NS + '" xmlns:xul="' + XUL_NS + '">' +
           output + '</div>');
     }
 
     // See https://github.com/mozilla/domtemplate/blob/master/README.md
     // for docs on the template() function
     let element = this.document.createRange().createContextualFragment(
@@ -7012,61 +7073,101 @@ GcliTerm.prototype = {
     ConsoleUtils.outputMessageNode(element, this.hudId);
   },
 
   /**
    * Setup the eval sandbox, should be called whenever we are attached.
    */
   createSandbox: function Gcli_createSandbox()
   {
-    let win = this.context.get().QueryInterface(Ci.nsIDOMWindow);
-
     // create a JS Sandbox out of this.context
-    this.sandbox = new Cu.Sandbox(win, {
-      sandboxPrototype: win,
+    this.sandbox = new Cu.Sandbox(this._window, {
+      sandboxPrototype: this._window,
       wantXrays: false
     });
     this.sandbox.console = this.console;
+
+    JSTermHelper(this);
   },
 
   /**
    * Evaluates a string in the sandbox.
    *
    * @param string aString
    *        String to evaluate in the sandbox
    * @return The result of the evaluation
    */
   evalInSandbox: function Gcli_evalInSandbox(aString)
   {
-    return Cu.evalInSandbox(aString, this.sandbox, "1.8", "Web Console", 1);
+    let window = unwrap(this.sandbox.window);
+    let temp$ = null;
+    let temp$$ = null;
+
+    // We prefer to execute the page-provided implementations for the $() and
+    // $$() functions.
+    if (typeof window.$ == "function") {
+      temp$ = this.sandbox.$;
+      delete this.sandbox.$;
+    }
+    if (typeof window.$$ == "function") {
+      temp$$ = this.sandbox.$$;
+      delete this.sandbox.$$;
+    }
+
+    let result = Cu.evalInSandbox(aString, this.sandbox, "1.8", "Web Console", 1);
+
+    if (temp$) {
+      this.sandbox.$ = temp$;
+    }
+    if (temp$$) {
+      this.sandbox.$$ = temp$$;
+    }
+
+    return result;
   },
 
   /**
    * Writes a message to the HUD that originates from the interactive
    * JavaScript console.
    *
    * @param string aOutputMessage
    *        The message to display.
    * @param number aCategory
    *        One of the CATEGORY_ constants.
    * @param number aSeverity
    *        One of the SEVERITY_ constants.
    */
-  writeOutput: function Gcli_writeOutput(aOutputMessage, aOptions)
+  writeOutput: function Gcli_writeOutput(aOutputMessage, aCategory, aSeverity, aOptions)
   {
     aOptions = aOptions || {};
 
     let node = ConsoleUtils.createMessageNode(
                     this.document,
-                    aOptions.category || CATEGORY_OUTPUT,
-                    aOptions.severity || SEVERITY_LOG,
+                    aCategory || CATEGORY_OUTPUT,
+                    aSeverity || SEVERITY_LOG,
                     aOutputMessage,
                     this.hudId,
                     aOptions.sourceUrl || undefined,
                     aOptions.sourceLine || undefined,
                     aOptions.clipboardText || undefined);
 
     ConsoleUtils.outputMessageNode(node, this.hudId);
   },
 
   clearOutput: JSTerm.prototype.clearOutput,
+  openPropertyPanel: JSTerm.prototype.openPropertyPanel,
+
+  formatResult: JSTerm.prototype.formatResult,
+  getResultType: JSTerm.prototype.getResultType,
+  formatString: JSTerm.prototype.formatString,
 };
 
+/**
+ * A fancy version of toString()
+ */
+function nameObject(aObj) {
+  if (aObj.constructor && aObj.constructor.name) {
+    return aObj.constructor.name;
+  }
+  // If that fails, use Objects toString which sometimes gives something
+  // better than 'Object', and at least defaults to Object if nothing better
+  return Object.prototype.toString.call(aObj).slice(8, -1);
+}
--- a/browser/devtools/webconsole/gcli.jsm
+++ b/browser/devtools/webconsole/gcli.jsm
@@ -195,16 +195,21 @@ var console = {};
    * Object.toString gives: "[object ?????]"; we want the "?????".
    *
    * @param {object} aObj
    *        The object from which to extract the constructor name
    * @return {string}
    *        The constructor name
    */
   function getCtorName(aObj) {
+    if (aObj.constructor && aObj.constructor.name) {
+      return aObj.constructor.name;
+    }
+    // If that fails, use Objects toString which sometimes gives something
+    // better than 'Object', and at least defaults to Object if nothing better
     return Object.prototype.toString.call(aObj).slice(8, -1);
   }
 
   /**
    * A single line stringification of an object designed for use by humans
    *
    * @param {any} aThing
    *        The object to be stringified
@@ -865,16 +870,20 @@ function Command(commandSpec) {
 
   // Track if the user is trying to mix default params and param groups.
   // All the non-grouped parameters must come before all the param groups
   // because non-grouped parameters can be assigned positionally, so their
   // index is important. We don't want 'holes' in the order caused by
   // parameter groups.
   var usingGroups = false;
 
+  if (this.returnType == null) {
+    this.returnType = 'string';
+  }
+
   // In theory this could easily be made recursive, so param groups could
   // contain nested param groups. Current thinking is that the added
   // complexity for the UI probably isn't worth it, so this implementation
   // prevents nesting.
   paramSpecs.forEach(function(spec) {
     if (!spec.group) {
       if (usingGroups) {
         console.error('Parameters can\'t come after param groups.' +
@@ -3015,16 +3024,25 @@ JavascriptType.prototype.stringify = fun
  * matches - this the number of matches that we aim for
  */
 JavascriptType.MAX_COMPLETION_MATCHES = 10;
 
 JavascriptType.prototype.parse = function(arg) {
   var typed = arg.text;
   var scope = globalObject;
 
+  // Just accept numbers
+  if (!isNaN(parseFloat(typed)) && isFinite(typed)) {
+    return new Conversion(typed, arg);
+  }
+  // Just accept constants like true/false/null/etc
+  if (typed.trim().match(/(null|undefined|NaN|Infinity|true|false)/)) {
+    return new Conversion(typed, arg);
+  }
+
   // Analyze the input text and find the beginning of the last part that
   // should be completed.
   var beginning = this._findCompletionBeginning(typed);
 
   // There was an error analyzing the string.
   if (beginning.err) {
     return new Conversion(typed, arg, Status.ERROR, beginning.err);
   }
@@ -3598,25 +3616,18 @@ NodeType.prototype.name = 'node';
 define('gcli/host', ['require', 'exports', 'module' ], function(require, exports, module) {
 
 
 /**
  * Helper to turn a node background it's complementary color for 1 second.
  * There is likely a better way to do this, but this will do for now.
  */
 exports.flashNode = function(node, color) {
-  if (!node.__gcliHighlighting) {
-    node.__gcliHighlighting = true;
-    var original = node.style.background;
-    node.style.background = color;
-    setTimeout(function() {
-      node.style.background = original;
-      delete node.__gcliHighlighting;
-    }, 1000);
-  }
+  // We avoid changing the DOM under firefox developer tools so this is a no-op
+  // In future we will use the multi-highlighter implemented in bug 653545.
 };
 
 
 });
 /*
  * Copyright 2009-2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE.txt or:
  * http://opensource.org/licenses/BSD-3-Clause
@@ -3986,44 +3997,22 @@ var evalCommandSpec = {
   name: '{',
   params: [
     {
       name: 'javascript',
       type: 'javascript',
       description: ''
     }
   ],
-  returnType: 'html',
+  returnType: 'object',
   description: { key: 'cliEvalJavascript' },
   exec: function(args, context) {
-    // &#x2192; is right arrow. We use explicit entities to ensure XML validity
-    var resultPrefix = '<em>{ ' + args.javascript + ' }</em> &#x2192; ';
-    try {
-      var result = customEval(args.javascript);
-
-      if (result === null) {
-        return resultPrefix + 'null.';
-      }
-
-      if (result === undefined) {
-        return resultPrefix + 'undefined.';
-      }
-
-      if (typeof result === 'function') {
-        // &#160; is &nbsp;
-        return resultPrefix +
-            (result + '').replace(/\n/g, '<br>').replace(/ /g, '&#160;');
-      }
-
-      return resultPrefix + result;
-    }
-    catch (ex) {
-      return resultPrefix + 'Exception: ' + ex.message;
-    }
-  }
+    return customEval(args.javascript);
+  },
+  evalRegexp: /^\s*{\s*/
 };
 
 
 /**
  * This is a special assignment to reflect the command itself.
  */
 function CommandAssignment() {
   this.param = new canon.Parameter({
@@ -4197,46 +4186,47 @@ Requisition.prototype._onAssignmentChang
     return;
   }
 
   this._structuralChangeInProgress = true;
 
   // Refactor? See bug 660765
   // Do preceding arguments need to have dummy values applied so we don't
   // get a hole in the command line?
+  var i;
   if (ev.assignment.param.isPositionalAllowed()) {
-    for (var i = 0; i < ev.assignment.paramIndex; i++) {
+    for (i = 0; i < ev.assignment.paramIndex; i++) {
       var assignment = this.getAssignment(i);
       if (assignment.param.isPositionalAllowed()) {
         if (assignment.ensureVisibleArgument()) {
           this._args.push(assignment.getArg());
         }
       }
     }
   }
 
   // Remember where we found the first match
   var index = MORE_THAN_THE_MOST_ARGS_POSSIBLE;
-  for (var i = 0; i < this._args.length; i++) {
+  for (i = 0; i < this._args.length; i++) {
     if (this._args[i].assignment === ev.assignment) {
       if (i < index) {
         index = i;
       }
       this._args.splice(i, 1);
       i--;
     }
   }
 
   if (index === MORE_THAN_THE_MOST_ARGS_POSSIBLE) {
     this._args.push(ev.assignment.getArg());
   }
   else {
     // Is there a way to do this that doesn't involve a loop?
     var newArgs = ev.conversion.arg.getArgs();
-    for (var i = 0; i < newArgs.length; i++) {
+    for (i = 0; i < newArgs.length; i++) {
       this._args.splice(index + i, 0, newArgs[i]);
     }
   }
   this._structuralChangeInProgress = false;
 
   this.inputChange();
 };
 
@@ -4269,24 +4259,24 @@ Requisition.prototype._onCommandAssignme
  * But we also need named access ...
  * @return The found assignment, or undefined, if no match was found
  */
 Requisition.prototype.getAssignment = function(nameOrNumber) {
   var name = (typeof nameOrNumber === 'string') ?
     nameOrNumber :
     Object.keys(this._assignments)[nameOrNumber];
   return this._assignments[name] || undefined;
-},
+};
 
 /**
  * Where parameter name == assignment names - they are the same
  */
 Requisition.prototype.getParameterNames = function() {
   return Object.keys(this._assignments);
-},
+};
 
 /**
  * A *shallow* clone of the assignments.
  * This is useful for systems that wish to go over all the assignments
  * finding values one way or another and wish to trim an array as they go.
  */
 Requisition.prototype.cloneAssignments = function() {
   return Object.keys(this._assignments).map(function(name) {
@@ -4409,24 +4399,25 @@ Requisition.prototype.toCanonicalString 
 Requisition.prototype.createInputArgTrace = function() {
   if (!this._args) {
     throw new Error('createInputMap requires a command line. See source.');
     // If this is a problem then we can fake command line input using
     // something like the code in #toCanonicalString().
   }
 
   var args = [];
+  var i;
   this._args.forEach(function(arg) {
-    for (var i = 0; i < arg.prefix.length; i++) {
+    for (i = 0; i < arg.prefix.length; i++) {
       args.push({ arg: arg, char: arg.prefix[i], part: 'prefix' });
     }
-    for (var i = 0; i < arg.text.length; i++) {
+    for (i = 0; i < arg.text.length; i++) {
       args.push({ arg: arg, char: arg.text[i], part: 'text' });
     }
-    for (var i = 0; i < arg.suffix.length; i++) {
+    for (i = 0; i < arg.suffix.length; i++) {
       args.push({ arg: arg, char: arg.suffix[i], part: 'suffix' });
     }
   });
 
   return args;
 };
 
 /**
@@ -4577,37 +4568,45 @@ Requisition.prototype.exec = function(in
     command = this.commandAssignment.getValue();
     args = this.getArgsObject();
   }
 
   if (!command) {
     return false;
   }
 
+  // Display JavaScript input without the initial { or closing }
+  var typed = this.toString();
+  if (evalCommandSpec.evalRegexp.test(typed)) {
+    typed = typed.replace(evalCommandSpec.evalRegexp, '');
+    // Bug 717763: What if the JavaScript naturally ends with a }?
+    typed = typed.replace(/\s*}\s*$/, '');
+  }
+
   var outputObject = {
     command: command,
     args: args,
-    typed: this.toString(),
+    typed: typed,
     canonical: this.toCanonicalString(),
     completed: false,
     start: new Date()
   };
 
   this.commandOutputManager.sendCommandOutput(outputObject);
 
-  var onComplete = (function(output, error) {
+  var onComplete = function(output, error) {
     if (visible) {
       outputObject.end = new Date();
       outputObject.duration = outputObject.end.getTime() - outputObject.start.getTime();
       outputObject.error = error;
       outputObject.output = output;
       outputObject.completed = true;
       this.commandOutputManager.sendCommandOutput(outputObject);
     }
-  }).bind(this);
+  }.bind(this);
 
   try {
     var context = new ExecutionContext(this);
     var reply = command.exec(args, context);
 
     if (reply != null && reply.isPromise) {
       reply.then(
         function(data) { onComplete(data, false); },
@@ -4616,16 +4615,17 @@ Requisition.prototype.exec = function(in
       // Add progress to our promise and add a handler for it here
       // See bug 659300
     }
     else {
       onComplete(reply, false);
     }
   }
   catch (ex) {
+    console.error(ex);
     onComplete(ex, true);
   }
 
   this.clear();
   return true;
 };
 
 /**
@@ -4770,16 +4770,17 @@ Requisition.prototype._tokenize = functi
   var blockDepth = 0; // For JS with nested {}
 
   // This is just a state machine. We're going through the string char by char
   // The 'mode' is one of the 'In' states. As we go, we're adding Arguments
   // to the 'args' array.
 
   while (true) {
     var c = typed[i];
+    var str;
     switch (mode) {
       case In.WHITESPACE:
         if (c === '\'') {
           prefix = typed.substring(start, i + 1);
           mode = In.SINGLE_Q;
           start = i + 1;
         }
         else if (c === '"') {
@@ -4802,52 +4803,52 @@ Requisition.prototype._tokenize = functi
           start = i;
         }
         break;
 
       case In.SIMPLE:
         // There is an edge case of xx'xx which we are assuming to
         // be a single parameter (and same with ")
         if (c === ' ') {
-          var str = unescape2(typed.substring(start, i));
+          str = unescape2(typed.substring(start, i));
           args.push(new Argument(str, prefix, ''));
           mode = In.WHITESPACE;
           start = i;
           prefix = '';
         }
         break;
 
       case In.SINGLE_Q:
         if (c === '\'') {
-          var str = unescape2(typed.substring(start, i));
+          str = unescape2(typed.substring(start, i));
           args.push(new Argument(str, prefix, c));
           mode = In.WHITESPACE;
           start = i + 1;
           prefix = '';
         }
         break;
 
       case In.DOUBLE_Q:
         if (c === '"') {
-          var str = unescape2(typed.substring(start, i));
+          str = unescape2(typed.substring(start, i));
           args.push(new Argument(str, prefix, c));
           mode = In.WHITESPACE;
           start = i + 1;
           prefix = '';
         }
         break;
 
       case In.SCRIPT:
         if (c === '{') {
           blockDepth++;
         }
         else if (c === '}') {
           blockDepth--;
           if (blockDepth === 0) {
-            var str = unescape2(typed.substring(start, i));
+            str = unescape2(typed.substring(start, i));
             args.push(new ScriptArgument(str, prefix, c));
             mode = In.WHITESPACE;
             start = i + 1;
             prefix = '';
           }
         }
         break;
     }
@@ -4866,21 +4867,21 @@ Requisition.prototype._tokenize = functi
             args.push(new Argument('', extra, ''));
           }
           else {
             lastArg.suffix += extra;
           }
         }
       }
       else if (mode === In.SCRIPT) {
-        var str = unescape2(typed.substring(start, i + 1));
+        str = unescape2(typed.substring(start, i + 1));
         args.push(new ScriptArgument(str, prefix, ''));
       }
       else {
-        var str = unescape2(typed.substring(start, i + 1));
+        str = unescape2(typed.substring(start, i + 1));
         args.push(new Argument(str, prefix, ''));
       }
       break;
     }
   }
 
   return args;
 };
@@ -4903,26 +4904,26 @@ function isSimple(typed) {
 /**
  * Looks in the canon for a command extension that matches what has been
  * typed at the command line.
  */
 Requisition.prototype._split = function(args) {
   // Handle the special case of the user typing { javascript(); }
   // We use the hidden 'eval' command directly rather than shift()ing one of
   // the parameters, and parse()ing it.
+  var conversion;
   if (args[0] instanceof ScriptArgument) {
     // Special case: if the user enters { console.log('foo'); } then we need to
     // use the hidden 'eval' command
-    var conversion = new Conversion(evalCommand, new Argument());
+    conversion = new Conversion(evalCommand, new Argument());
     this.commandAssignment.setConversion(conversion);
     return;
   }
 
   var argsUsed = 1;
-  var conversion;
 
   while (argsUsed <= args.length) {
     var arg = (argsUsed === 1) ?
       args[0] :
       new MergedArgument(args, 0, argsUsed);
     conversion = this.commandAssignment.param.type.parse(arg);
 
     // We only want to carry on if this command is a parent command,
@@ -6539,16 +6540,17 @@ argFetch.ArgFetcher = ArgFetcher;
  * http://opensource.org/licenses/BSD-3-Clause
  */
 
 define('gcli/ui/field', ['require', 'exports', 'module' , 'gcli/util', 'gcli/l10n', 'gcli/argument', 'gcli/types', 'gcli/types/basic', 'gcli/types/javascript', 'gcli/ui/menu'], function(require, exports, module) {
 
 
 var dom = require('gcli/util').dom;
 var createEvent = require('gcli/util').createEvent;
+var KeyEvent = require('gcli/util').event.KeyEvent;
 var l10n = require('gcli/l10n');
 
 var Argument = require('gcli/argument').Argument;
 var TrueNamedArgument = require('gcli/argument').TrueNamedArgument;
 var FalseNamedArgument = require('gcli/argument').FalseNamedArgument;
 var ArrayArgument = require('gcli/argument').ArrayArgument;
 
 var Status = require('gcli/types').Status;
@@ -6567,26 +6569,31 @@ var JavascriptType = require('gcli/types
 var Menu = require('gcli/ui/menu').Menu;
 
 
 /**
  * A Field is a way to get input for a single parameter.
  * This class is designed to be inherited from. It's important that all
  * subclasses have a similar constructor signature because they are created
  * via getField(...)
- * @param document The document we use in calling createElement
  * @param type The type to use in conversions
- * @param named Is this parameter named? That is to say, are positional
- * arguments disallowed, if true, then we need to provide updates to the
- * command line that explicitly name the parameter in use (e.g. --verbose, or
- * --name Fred rather than just true or Fred)
- * @param name If this parameter is named, what name should we use
- * @param requ The requisition that we're attached to
- */
-function Field(document, type, named, name, requ) {
+ * @param options A set of properties to help fields configure themselves:
+ * - document: The document we use in calling createElement
+ * - named: Is this parameter named? That is to say, are positional
+ *         arguments disallowed, if true, then we need to provide updates to
+ *         the command line that explicitly name the parameter in use
+ *         (e.g. --verbose, or --name Fred rather than just true or Fred)
+ * - name: If this parameter is named, what name should we use
+ * - requisition: The requisition that we're attached to
+ * - required: Boolean to indicate if this is a mandatory field
+ */
+function Field(type, options) {
+  this.type = type;
+  this.document = options.document;
+  this.requisition = options.requisition;
 }
 
 /**
  * Subclasses should assign their element with the DOM node that gets added
  * to the 'form'. It doesn't have to be an input node, just something that
  * contains it.
  */
 Field.prototype.element = undefined;
@@ -6633,20 +6640,24 @@ Field.prototype.setMessage = function(me
     dom.setInnerHtml(this.messageElement, message);
   }
 };
 
 /**
  * Method to be called by subclasses when their input changes, which allows us
  * to properly pass on the fieldChanged event.
  */
-Field.prototype.onInputChange = function() {
+Field.prototype.onInputChange = function(ev) {
   var conversion = this.getConversion();
   this.fieldChanged({ conversion: conversion });
   this.setMessage(conversion.message);
+
+  if (ev.keyCode === KeyEvent.DOM_VK_RETURN) {
+    this.requisition.exec();
+  }
 };
 
 /**
  * 'static/abstract' method to allow implementations of Field to lay a claim
  * to a type. This allows claims of various strength to be weighted up.
  * See the Field.*MATCH values.
  */
 Field.claim = function() {
@@ -6710,18 +6721,17 @@ exports.addField = addField;
 exports.removeField = removeField;
 exports.getField = getField;
 
 
 /**
  * A field that allows editing of strings
  */
 function StringField(type, options) {
-  this.document = options.document;
-  this.type = type;
+  Field.call(this, type, options);
   this.arg = new Argument();
 
   this.element = dom.createElement(this.document, 'input');
   this.element.type = 'text';
   this.element.classList.add('gcli-field');
 
   this.onInputChange = this.onInputChange.bind(this);
   this.element.addEventListener('keyup', this.onInputChange, false);
@@ -6758,18 +6768,17 @@ StringField.claim = function(type) {
 exports.StringField = StringField;
 addField(StringField);
 
 
 /**
  * A field that allows editing of numbers using an [input type=number] field
  */
 function NumberField(type, options) {
-  this.document = options.document;
-  this.type = type;
+  Field.call(this, type, options);
   this.arg = new Argument();
 
   this.element = dom.createElement(this.document, 'input');
   this.element.type = 'number';
   if (this.type.max) {
     this.element.max = this.type.max;
   }
   if (this.type.min) {
@@ -6813,18 +6822,18 @@ NumberField.prototype.getConversion = fu
 exports.NumberField = NumberField;
 addField(NumberField);
 
 
 /**
  * A field that uses a checkbox to toggle a boolean field
  */
 function BooleanField(type, options) {
-  this.document = options.document;
-  this.type = type;
+  Field.call(this, type, options);
+
   this.name = options.name;
   this.named = options.named;
 
   this.element = dom.createElement(this.document, 'input');
   this.element.type = 'checkbox';
   this.element.id = 'gcliForm' + this.name;
 
   this.onInputChange = this.onInputChange.bind(this);
@@ -6871,18 +6880,18 @@ addField(BooleanField);
  * <li>value: This is the (probably non-string) value, known as a value by the
  *   assignment
  * <li>optValue: This is the text value as known by the DOM option element, as
  *   in &lt;option value=???%gt...
  * <li>optText: This is the contents of the DOM option element.
  * </ul>
  */
 function SelectionField(type, options) {
-  this.document = options.document;
-  this.type = type;
+  Field.call(this, type, options);
+
   this.items = [];
 
   this.element = dom.createElement(this.document, 'select');
   this.element.classList.add('gcli-field');
   this._addOption({
     name: l10n.lookupFormat('fieldSelectionSelect', [ options.name ])
   });
   var lookup = this.type.getLookup();
@@ -6939,19 +6948,17 @@ SelectionField.prototype._addOption = fu
 exports.SelectionField = SelectionField;
 addField(SelectionField);
 
 
 /**
  * A field that allows editing of javascript
  */
 function JavascriptField(type, options) {
-  this.document = options.document;
-  this.type = type;
-  this.requ = options.requisition;
+  Field.call(this, type, options);
 
   this.onInputChange = this.onInputChange.bind(this);
   this.arg = new Argument('', '{ ', ' }');
 
   this.element = dom.createElement(this.document, 'div');
 
   this.input = dom.createElement(this.document, 'input');
   this.input.type = 'text';
@@ -7045,20 +7052,18 @@ exports.JavascriptField = JavascriptFiel
 addField(JavascriptField);
 
 
 /**
  * A field that works with deferred types by delaying resolution until that
  * last possible time
  */
 function DeferredField(type, options) {
-  this.document = options.document;
-  this.type = type;
+  Field.call(this, type, options);
   this.options = options;
-  this.requisition = options.requisition;
   this.requisition.assignmentChange.add(this.update, this);
 
   this.element = dom.createElement(this.document, 'div');
   this.update();
 
   this.fieldChanged = createEvent('DeferredField.fieldChanged');
 }
 
@@ -7106,18 +7111,18 @@ exports.DeferredField = DeferredField;
 addField(DeferredField);
 
 
 /**
  * For use with deferred types that do not yet have anything to resolve to.
  * BlankFields are not for general use.
  */
 function BlankField(type, options) {
-  this.document = options.document;
-  this.type = type;
+  Field.call(this, type, options);
+
   this.element = dom.createElement(this.document, 'div');
 
   this.fieldChanged = createEvent('BlankField.fieldChanged');
 }
 
 BlankField.prototype = Object.create(Field.prototype);
 
 BlankField.claim = function(type) {
@@ -7134,20 +7139,18 @@ exports.BlankField = BlankField;
 addField(BlankField);
 
 
 /**
  * Adds add/delete buttons to a normal field allowing there to be many values
  * given for a parameter.
  */
 function ArrayField(type, options) {
-  this.document = options.document;
-  this.type = type;
+  Field.call(this, type, options);
   this.options = options;
-  this.requ = options.requisition;
 
   this._onAdd = this._onAdd.bind(this);
   this.members = [];
 
   // <div class=gcliArrayParent save="${element}">
   this.element = dom.createElement(this.document, 'div');
   this.element.classList.add('gcli-array-parent');
 
--- a/browser/devtools/webconsole/test/Makefile.in
+++ b/browser/devtools/webconsole/test/Makefile.in
@@ -138,16 +138,18 @@ include $(topsrcdir)/config/rules.mk
 	browser_webconsole_bug_663443_panel_title.js \
 	browser_webconsole_bug_660806_history_nav.js \
 	browser_webconsole_bug_651501_document_body_autocomplete.js \
 	browser_webconsole_bug_653531_highlighter_console_helper.js \
 	browser_webconsole_bug_659907_console_dir.js \
 	browser_webconsole_bug_678816.js \
 	browser_webconsole_bug_664131_console_group.js \
 	browser_webconsole_bug_704295.js \
+	browser_gcli_commands.js \
+	browser_gcli_helpers.js \
 	browser_gcli_inspect.js \
 	browser_gcli_integrate.js \
 	browser_gcli_require.js \
 	browser_gcli_web.js \
 	browser_webconsole_bug_658368_time_methods.js \
 	browser_webconsole_bug_622303_persistent_filters.js \
 	browser_webconsole_window_zombie.js \
 	browser_cached_messages.js \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_gcli_commands.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// For more information on GCLI see:
+// - https://github.com/mozilla/gcli/blob/master/docs/index.md
+// - https://wiki.mozilla.org/DevTools/Features/GCLI
+
+Components.utils.import("resource:///modules/gcli.jsm");
+
+let hud;
+let gcliterm;
+
+registerCleanupFunction(function() {
+  gcliterm = undefined;
+  hud = undefined;
+  Services.prefs.clearUserPref("devtools.gcli.enable");
+});
+
+function test() {
+  Services.prefs.setBoolPref("devtools.gcli.enable", true);
+  addTab("http://example.com/browser/browser/devtools/webconsole/test/browser_gcli_inspect.html");
+  browser.addEventListener("DOMContentLoaded", onLoad, false);
+}
+
+function onLoad() {
+  browser.removeEventListener("DOMContentLoaded", onLoad, false);
+
+  openConsole();
+
+  hud = HUDService.getHudByWindow(content);
+  gcliterm = hud.gcliterm;
+
+  testEcho();
+
+  // gcli._internal.console.error("Command Tests Completed");
+}
+
+function testEcho() {
+  let nodes = exec("echo message");
+  is(nodes.length, 2, "after echo");
+  is(nodes[0].textContent, "echo message", "output 0");
+  is(nodes[1].textContent.trim(), "message", "output 1");
+
+  testConsoleClear();
+}
+
+function testConsoleClear() {
+  let nodes = exec("console clear");
+  is(nodes.length, 1, "after console clear 1");
+
+  executeSoon(function() {
+    let nodes = hud.outputNode.querySelectorAll("richlistitem");
+    is(nodes.length, 0, "after console clear 2");
+
+    testConsoleClose();
+  });
+}
+
+function testConsoleClose() {
+  ok(hud.hudId in HUDService.hudReferences, "console open");
+
+  exec("console close");
+
+  ok(!(hud.hudId in HUDService.hudReferences), "console closed");
+
+  finishTest();
+}
+
+function exec(command) {
+  gcliterm.clearOutput();
+  let nodes = hud.outputNode.querySelectorAll("richlistitem");
+  is(nodes.length, 0, "setup - " + command);
+
+  gcliterm.opts.console.inputter.setInput(command);
+  gcliterm.opts.requisition.exec();
+
+  return hud.outputNode.querySelectorAll("richlistitem");
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_gcli_helpers.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// For more information on GCLI see:
+// - https://github.com/mozilla/gcli/blob/master/docs/index.md
+// - https://wiki.mozilla.org/DevTools/Features/GCLI
+
+Components.utils.import("resource:///modules/gcli.jsm");
+
+let hud;
+let gcliterm;
+
+registerCleanupFunction(function() {
+  gcliterm = undefined;
+  hud = undefined;
+  Services.prefs.clearUserPref("devtools.gcli.enable");
+});
+
+function test() {
+  Services.prefs.setBoolPref("devtools.gcli.enable", true);
+  addTab("http://example.com/browser/browser/devtools/webconsole/test//test-console.html");
+  browser.addEventListener("DOMContentLoaded", onLoad, false);
+}
+
+function onLoad() {
+  browser.removeEventListener("DOMContentLoaded", onLoad, false);
+
+  openConsole();
+  hud = HUDService.getHudByWindow(content);
+  gcliterm = hud.gcliterm;
+
+  testHelpers();
+  testScripts();
+
+  closeConsole();
+  finishTest();
+
+  // gcli._internal.console.error("Command Tests Completed");
+}
+
+function testScripts() {
+  check("{ 'id=' + $('header').getAttribute('id')", '"id=header"');
+  check("{ headerQuery = $$('h1')", "Instance of NodeList");
+  check("{ 'length=' + headerQuery.length", '"length=1"');
+
+  check("{ xpathQuery = $x('.//*', document.body);", 'Instance of Array');
+  check("{ 'headerFound='  + (xpathQuery[0] == headerQuery[0])", '"headerFound=true"');
+
+  check("{ 'keysResult=' + (keys({b:1})[0] == 'b')", '"keysResult=true"');
+  check("{ 'valuesResult=' + (values({b:1})[0] == 1)", '"valuesResult=true"');
+
+  check("{ [] instanceof Array", "true");
+  check("{ ({}) instanceof Object", "true");
+  check("{ document", "Instance of HTMLDocument");
+  check("{ null", "null");
+  check("{ undefined", undefined);
+
+  check("{ for (var x=0; x<9; x++) x;", "8");
+}
+
+function check(command, reply) {
+  gcliterm.clearOutput();
+  gcliterm.opts.console.inputter.setInput(command);
+  gcliterm.opts.requisition.exec();
+
+  let labels = hud.outputNode.querySelectorAll(".webconsole-msg-output");
+  if (reply === undefined) {
+    is(labels.length, 0, "results count for: " + command);
+  }
+  else {
+    is(labels.length, 1, "results count for: " + command);
+    is(labels[0].textContent.trim(), reply, "message for: " + command);
+  }
+
+  gcliterm.opts.console.inputter.setInput("");
+}
+
+function testHelpers() {
+  gcliterm.clearOutput();
+  gcliterm.opts.console.inputter.setInput("{ pprint({b:2, a:1})");
+  gcliterm.opts.requisition.exec();
+  // Doesn't conform to check() format
+  let label = hud.outputNode.querySelector(".webconsole-msg-output");
+  is(label.textContent.trim(), "a: 1\n  b: 2", "pprint() worked");
+
+  // no gcliterm.clearOutput() here as we clear the output using the clear() fn.
+  gcliterm.opts.console.inputter.setInput("{ clear()");
+  gcliterm.opts.requisition.exec();
+  ok(!hud.outputNode.querySelector(".hud-group"), "clear() worked");
+
+  // check that pprint(window) and keys(window) don't throw, bug 608358
+  gcliterm.clearOutput();
+  gcliterm.opts.console.inputter.setInput("{ pprint(window)");
+  gcliterm.opts.requisition.exec();
+  let labels = hud.outputNode.querySelectorAll(".webconsole-msg-output");
+  is(labels.length, 1, "one line of output for pprint(window)");
+
+  gcliterm.clearOutput();
+  gcliterm.opts.console.inputter.setInput("{ keys(window)");
+  gcliterm.opts.requisition.exec();
+  labels = hud.outputNode.querySelectorAll(".webconsole-msg-output");
+  is(labels.length, 1, "one line of output for keys(window)");
+
+  gcliterm.clearOutput();
+  gcliterm.opts.console.inputter.setInput("{ pprint('hi')");
+  gcliterm.opts.requisition.exec();
+  // Doesn't conform to check() format, bug 614561
+  label = hud.outputNode.querySelector(".webconsole-msg-output");
+  is(label.textContent.trim(), '0: "h"\n  1: "i"', 'pprint("hi") worked');
+
+  // Causes a memory leak. FIXME Bug 717892
+  /*
+  // check that pprint(function) shows function source, bug 618344
+  gcliterm.clearOutput();
+  gcliterm.opts.console.inputter.setInput("{ pprint(print)");
+  gcliterm.opts.requisition.exec();
+  label = hud.outputNode.querySelector(".webconsole-msg-output");
+  isnot(label.textContent.indexOf("SEVERITY_LOG"), -1, "pprint(function) shows function source");
+  */
+
+  gcliterm.clearOutput();
+}
--- a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
@@ -146,16 +146,21 @@ webConsolePositionWindow=Window
 # the correct direction.
 webConsoleWindowTitleAndURL=Web Console - %S
 
 # LOCALIZATION NOTE (scratchpad.linkText):
 # The text used in the right hand side of the web console command line when
 # Javascript is being entered, to indicate how to jump into scratchpad mode
 scratchpad.linkText=Shift+RETURN - Open in Scratchpad
 
+# LOCALIZATION NOTE (gcliterm.instanceLabel): The console displays
+# objects using their type (from the constructor function) in this descriptive
+# string
+gcliterm.instanceLabel=Instance of %S
+
 # LOCALIZATION NOTE (Autocomplete.label):
 # The autocomplete popup panel label/title.
 Autocomplete.label=Autocomplete popup
 
 # LOCALIZATION NOTE (stacktrace.anonymousFunction):
 # This string is used to display JavaScript functions that have no given name -
 # they are said to be anonymous. See stacktrace.outputMessage.
 stacktrace.anonymousFunction=<anonymous>
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/newTab.dtd
@@ -0,0 +1,6 @@
+<!-- These strings are used in the about:newtab page -->
+<!ENTITY newtab.pageTitle "New Tab">
+
+<!ENTITY newtab.show "Show the New Tab Page">
+<!ENTITY newtab.hide "Hide the New Tab Page">
+<!ENTITY newtab.reset "Reset the New Tab Page">
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/newTab.properties
@@ -0,0 +1,3 @@
+newtab.pin=Pin this site at its current position
+newtab.unpin=Unpin this site
+newtab.block=Remove this site
--- a/browser/locales/jar.mn
+++ b/browser/locales/jar.mn
@@ -23,16 +23,18 @@
     locale/browser/devtools/scratchpad.properties     (%chrome/browser/devtools/scratchpad.properties)
     locale/browser/devtools/scratchpad.dtd            (%chrome/browser/devtools/scratchpad.dtd)
     locale/browser/devtools/styleeditor.properties    (%chrome/browser/devtools/styleeditor.properties)
     locale/browser/devtools/styleeditor.dtd           (%chrome/browser/devtools/styleeditor.dtd)
     locale/browser/devtools/styleinspector.properties (%chrome/browser/devtools/styleinspector.properties)
     locale/browser/devtools/styleinspector.dtd        (%chrome/browser/devtools/styleinspector.dtd)
     locale/browser/devtools/webConsole.dtd            (%chrome/browser/devtools/webConsole.dtd)
     locale/browser/devtools/sourceeditor.properties   (%chrome/browser/devtools/sourceeditor.properties)
+    locale/browser/newTab.dtd                      (%chrome/browser/newTab.dtd)
+    locale/browser/newTab.properties               (%chrome/browser/newTab.properties)
     locale/browser/openLocation.dtd                (%chrome/browser/openLocation.dtd)
     locale/browser/openLocation.properties         (%chrome/browser/openLocation.properties)
 *   locale/browser/pageInfo.dtd                    (%chrome/browser/pageInfo.dtd)
     locale/browser/pageInfo.properties             (%chrome/browser/pageInfo.properties)
     locale/browser/quitDialog.properties           (%chrome/browser/quitDialog.properties)
 *   locale/browser/safeMode.dtd                    (%chrome/browser/safeMode.dtd)
     locale/browser/sanitize.dtd                    (%chrome/browser/sanitize.dtd)
     locale/browser/search.properties               (%chrome/browser/search.properties)
--- a/browser/modules/Makefile.in
+++ b/browser/modules/Makefile.in
@@ -46,16 +46,17 @@ include $(topsrcdir)/config/config.mk
 
 ifdef ENABLE_TESTS
 DIRS += test
 endif
 
 EXTRA_JS_MODULES = \
 	openLocationLastURL.jsm \
 	NetworkPrioritizer.jsm \
+	NewTabUtils.jsm \
 	offlineAppCache.jsm \
 	TelemetryTimestamps.jsm \
 	$(NULL)
 
 ifeq ($(MOZ_WIDGET_TOOLKIT),windows) 
 EXTRA_JS_MODULES += \
 	WindowsPreviewPerTab.jsm \
 	WindowsJumpLists.jsm \
new file mode 100644
--- /dev/null
+++ b/browser/modules/NewTabUtils.jsm
@@ -0,0 +1,568 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+let EXPORTED_SYMBOLS = ["NewTabUtils"];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+  "resource://gre/modules/PlacesUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gPrivateBrowsing",
+  "@mozilla.org/privatebrowsing;1", "nsIPrivateBrowsingService");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Dict", "resource://gre/modules/Dict.jsm");
+
+// The preference that tells whether this feature is enabled.
+const PREF_NEWTAB_ENABLED = "browser.newtabpage.enabled";
+
+// The maximum number of results we want to retrieve from history.
+const HISTORY_RESULTS_LIMIT = 100;
+
+/**
+ * Singleton that provides storage functionality.
+ */
+let Storage = {
+  /**
+   * The dom storage instance used to persist data belonging to the New Tab Page.
+   */
+  get domStorage() {
+    let uri = Services.io.newURI("about:newtab", null, null);
+    let principal = Services.scriptSecurityManager.getCodebasePrincipal(uri);
+
+    let sm = Services.domStorageManager;
+    let storage = sm.getLocalStorageForPrincipal(principal, "");
+
+    // Cache this value, overwrite the getter.
+    let descriptor = {value: storage, enumerable: true};
+    Object.defineProperty(this, "domStorage", descriptor);
+
+    return storage;
+  },
+
+  /**
+   * The current storage used to persist New Tab Page data. If we're currently
+   * in private browsing mode this will return a PrivateBrowsingStorage
+   * instance.
+   */
+  get currentStorage() {
+    let storage = this.domStorage;
+
+    // Check if we're starting in private browsing mode.
+    if (gPrivateBrowsing.privateBrowsingEnabled)
+      storage = new PrivateBrowsingStorage(storage);
+
+    // Register an observer to listen for private browsing mode changes.
+    Services.obs.addObserver(this, "private-browsing", true);
+
+    // Cache this value, overwrite the getter.
+    let descriptor = {value: storage, enumerable: true, writable: true};
+    Object.defineProperty(this, "currentStorage", descriptor);
+
+    return storage;
+  },
+
+  /**
+   * Gets the value for a given key from the storage.
+   * @param aKey The storage key (a string).
+   * @param aDefault A default value if the key doesn't exist.
+   * @return The value for the given key.
+   */
+  get: function Storage_get(aKey, aDefault) {
+    let value;
+
+    try {
+      value = JSON.parse(this.currentStorage.getItem(aKey));
+    } catch (e) {}
+
+    return value || aDefault;
+  },
+
+  /**
+   * Sets the storage value for a given key.
+   * @param aKey The storage key (a string).
+   * @param aValue The value to set.
+   */
+  set: function Storage_set(aKey, aValue) {
+    this.currentStorage.setItem(aKey, JSON.stringify(aValue));
+  },
+
+  /**
+   * Clears the storage and removes all values.
+   */
+  clear: function Storage_clear() {
+    this.currentStorage.clear();
+  },
+
+  /**
+   * Implements the nsIObserver interface to get notified about private
+   * browsing mode changes.
+   */
+  observe: function Storage_observe(aSubject, aTopic, aData) {
+    if (aData == "enter") {
+      // When switching to private browsing mode we keep the current state
+      // of the grid and provide a volatile storage for it that is
+      // discarded upon leaving private browsing.
+      this.currentStorage = new PrivateBrowsingStorage(this.domStorage);
+    } else {
+      // Reset to normal DOM storage.
+      this.currentStorage = this.domStorage;
+
+      // When switching back from private browsing we need to reset the
+      // grid and re-read its values from the underlying storage. We don't
+      // want any data from private browsing to show up.
+      PinnedLinks.resetCache();
+      BlockedLinks.resetCache();
+
+      Pages.update();
+    }
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference])
+};
+
+/**
+ * This class implements a temporary storage used while the user is in private
+ * browsing mode. It is discarded when leaving pb mode.
+ */
+function PrivateBrowsingStorage(aStorage) {
+  this._data = new Dict();
+
+  for (let i = 0; i < aStorage.length; i++) {
+    let key = aStorage.key(i);
+    this._data.set(key, aStorage.getItem(key));
+  }
+}
+
+PrivateBrowsingStorage.prototype = {
+  /**
+   * The data store.
+   */
+  _data: null,
+
+  /**
+   * Gets the value for a given key from the storage.
+   * @param aKey The storage key.
+   * @param aDefault A default value if the key doesn't exist.
+   * @return The value for the given key.
+   */
+  getItem: function PrivateBrowsingStorage_getItem(aKey) {
+    return this._data.get(aKey);
+  },
+
+  /**
+   * Sets the storage value for a given key.
+   * @param aKey The storage key.
+   * @param aValue The value to set.
+   */
+  setItem: function PrivateBrowsingStorage_setItem(aKey, aValue) {
+    this._data.set(aKey, aValue);
+  },
+
+  /**
+   * Clears the storage and removes all values.
+   */
+  clear: function PrivateBrowsingStorage_clear() {
+    this._data.listkeys().forEach(function (akey) {
+      this._data.del(aKey);
+    }, this);
+  }
+};
+
+/**
+ * Singleton that serves as a registry for all open 'New Tab Page's.
+ */
+let AllPages = {
+  /**
+   * The array containing all active pages.
+   */
+  _pages: [],
+
+  /**
+   * Tells whether we already added a preference observer.
+   */
+  _observing: false,
+
+  /**
+   * Cached value that tells whether the New Tab Page feature is enabled.
+   */
+  _enabled: null,
+
+  /**
+   * Adds a page to the internal list of pages.
+   * @param aPage The page to register.
+   */
+  register: function AllPages_register(aPage) {
+    this._pages.push(aPage);
+
+    // Add the preference observer if we haven't already.
+    if (!this._observing) {
+      this._observing = true;
+      Services.prefs.addObserver(PREF_NEWTAB_ENABLED, this, true);
+    }
+  },
+
+  /**
+   * Removes a page from the internal list of pages.
+   * @param aPage The page to unregister.
+   */
+  unregister: function AllPages_unregister(aPage) {
+    let index = this._pages.indexOf(aPage);
+    this._pages.splice(index, 1);
+  },
+
+  /**
+   * Returns whether the 'New Tab Page' is enabled.
+   */
+  get enabled() {
+    if (this._enabled === null)
+      this._enabled = Services.prefs.getBoolPref(PREF_NEWTAB_ENABLED);
+
+    return this._enabled;
+  },
+
+  /**
+   * Enables or disables the 'New Tab Page' feature.
+   */
+  set enabled(aEnabled) {
+    if (this.enabled != aEnabled)
+      Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, !!aEnabled);
+  },
+
+  /**
+   * Updates all currently active pages but the given one.
+   * @param aExceptPage The page to exclude from updating.
+   */
+  update: function AllPages_update(aExceptPage) {
+    this._pages.forEach(function (aPage) {
+      if (aExceptPage != aPage)
+        aPage.update();
+    });
+  },
+
+  /**
+   * Implements the nsIObserver interface to get notified when the preference
+   * value changes.
+   */
+  observe: function AllPages_observe() {
+    // Clear the cached value.
+    this._enabled = null;
+
+    let args = Array.slice(arguments);
+
+    this._pages.forEach(function (aPage) {
+      aPage.observe.apply(aPage, args);
+    }, this);
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference])
+};
+
+/**
+ * Singleton that keeps track of all pinned links and their positions in the
+ * grid.
+ */
+let PinnedLinks = {
+  /**
+   * The cached list of pinned links.
+   */
+  _links: null,
+
+  /**
+   * The array of pinned links.
+   */
+  get links() {
+    if (!this._links)
+      this._links = Storage.get("pinnedLinks", []);
+
+    return this._links;
+  },
+
+  /**
+   * Pins a link at the given position.
+   * @param aLink The link to pin.
+   * @param aIndex The grid index to pin the cell at.
+   */
+  pin: function PinnedLinks_pin(aLink, aIndex) {
+    // Clear the link's old position, if any.
+    this.unpin(aLink);
+
+    this.links[aIndex] = aLink;
+    Storage.set("pinnedLinks", this.links);
+  },
+
+  /**
+   * Unpins a given link.
+   * @param aLink The link to unpin.
+   */
+  unpin: function PinnedLinks_unpin(aLink) {
+    let index = this._indexOfLink(aLink);
+    if (index != -1) {
+      this.links[index] = null;
+      Storage.set("pinnedLinks", this.links);
+    }
+  },
+
+  /**
+   * Checks whether a given link is pinned.
+   * @params aLink The link to check.
+   * @return whether The link is pinned.
+   */
+  isPinned: function PinnedLinks_isPinned(aLink) {
+    return this._indexOfLink(aLink) != -1;
+  },
+
+  /**
+   * Resets the links cache.
+   */
+  resetCache: function PinnedLinks_resetCache() {
+    this._links = null;
+  },
+
+  /**
+   * Finds the index of a given link in the list of pinned links.
+   * @param aLink The link to find an index for.
+   * @return The link's index.
+   */
+  _indexOfLink: function PinnedLinks_indexOfLink(aLink) {
+    for (let i = 0; i < this.links.length; i++) {
+      let link = this.links[i];
+      if (link && link.url == aLink.url)
+        return i;
+    }
+
+    // The given link is unpinned.
+    return -1;
+  }
+};
+
+/**
+ * Singleton that keeps track of all blocked links in the grid.
+ */
+let BlockedLinks = {
+  /**
+   * The cached list of blocked links.
+   */
+  _links: null,
+
+  /**
+   * The list of blocked links.
+   */
+  get links() {
+    if (!this._links)
+      this._links = Storage.get("blockedLinks", {});
+
+    return this._links;
+  },
+
+  /**
+   * Blocks a given link.
+   * @param aLink The link to block.
+   */
+  block: function BlockedLinks_block(aLink) {
+    this.links[aLink.url] = 1;
+
+    // Make sure we unpin blocked links.
+    PinnedLinks.unpin(aLink);
+
+    Storage.set("blockedLinks", this.links);
+  },
+
+  /**
+   * Returns whether a given link is blocked.
+   * @param aLink The link to check.
+   */
+  isBlocked: function BlockedLinks_isBlocked(aLink) {
+    return (aLink.url in this.links);
+  },
+
+  /**
+   * Checks whether the list of blocked links is empty.
+   * @return Whether the list is empty.
+   */
+  isEmpty: function BlockedLinks_isEmpty() {
+    return Object.keys(this.links).length == 0;
+  },
+
+  /**
+   * Resets the links cache.
+   */
+  resetCache: function BlockedLinks_resetCache() {
+    this._links = null;
+  }
+};
+
+/**
+ * Singleton that serves as the default link provider for the grid. It queries
+ * the history to retrieve the most frequently visited sites.
+ */
+let PlacesProvider = {
+  /**
+   * Gets the current set of links delivered by this provider.
+   * @param aCallback The function that the array of links is passed to.
+   */
+  getLinks: function PlacesProvider_getLinks(aCallback) {
+    let options = PlacesUtils.history.getNewQueryOptions();
+    options.maxResults = HISTORY_RESULTS_LIMIT;
+
+    // Sort by frecency, descending.
+    options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_DESCENDING
+
+    // We don't want source redirects for this query.
+    options.redirectsMode = Ci.nsINavHistoryQueryOptions.REDIRECTS_MODE_TARGET;
+
+    let links = [];
+
+    let callback = {
+      handleResult: function (aResultSet) {
+        let row;
+
+        while (row = aResultSet.getNextRow()) {
+          let url = row.getResultByIndex(1);
+          let title = row.getResultByIndex(2);
+          links.push({url: url, title: title});
+        }
+      },
+
+      handleError: function (aError) {
+        // Should we somehow handle this error?
+        aCallback([]);
+      },
+
+      handleCompletion: function (aReason) {
+        aCallback(links);
+      }
+    };
+
+    // Execute the query.
+    let query = PlacesUtils.history.getNewQuery();
+    let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase);
+    db.asyncExecuteLegacyQueries([query], 1, options, callback);
+  }
+};
+
+/**
+ * Singleton that provides access to all links contained in the grid (including
+ * the ones that don't fit on the grid). A link is a plain object with title
+ * and url properties.
+ *
+ * Example:
+ *
+ * {url: "http://www.mozilla.org/", title: "Mozilla"}
+ */
+let Links = {
+  /**
+   * The links cache.
+   */
+  _links: null,
+
+  /**
+   * The default provider for links.
+   */
+  _provider: PlacesProvider,
+
+  /**
+   * List of callbacks waiting for the cache to be populated.
+   */
+  _populateCallbacks: [],
+
+  /**
+   * Populates the cache with fresh links from the current provider.
+   * @param aCallback The callback to call when finished (optional).
+   * @param aForce When true, populates the cache even when it's already filled.
+   */
+  populateCache: function Links_populateCache(aCallback, aForce) {
+    let callbacks = this._populateCallbacks;
+
+    // Enqueue the current callback.
+    callbacks.push(aCallback);
+
+    // There was a callback waiting already, thus the cache has not yet been
+    // populated.
+    if (callbacks.length > 1)
+      return;
+
+    function executeCallbacks() {
+      while (callbacks.length) {
+        let callback = callbacks.shift();
+        if (callback) {
+          try {
+            callback();
+          } catch (e) {
+            // We want to proceed even if a callback fails.
+          }
+        }
+      }
+    }
+
+    if (this._links && !aForce) {
+      executeCallbacks();
+    } else {
+      this._provider.getLinks(function (aLinks) {
+        this._links = aLinks;
+        executeCallbacks();
+      }.bind(this));
+    }
+  },
+
+  /**
+   * Gets the current set of links contained in the grid.
+   * @return The links in the grid.
+   */
+  getLinks: function Links_getLinks() {
+    let pinnedLinks = Array.slice(PinnedLinks.links);
+
+    // Filter blocked and pinned links.
+    let links = this._links.filter(function (link) {
+      return !BlockedLinks.isBlocked(link) && !PinnedLinks.isPinned(link);
+    });
+
+    // Try to fill the gaps between pinned links.
+    for (let i = 0; i < pinnedLinks.length && links.length; i++)
+      if (!pinnedLinks[i])
+        pinnedLinks[i] = links.shift();
+
+    // Append the remaining links if any.
+    if (links.length)
+      pinnedLinks = pinnedLinks.concat(links);
+
+    return pinnedLinks;
+  },
+
+  /**
+   * Resets the links cache.
+   */
+  resetCache: function Links_resetCache() {
+    this._links = [];
+  }
+};
+
+/**
+ * Singleton that provides the public API of this JSM.
+ */
+let NewTabUtils = {
+  /**
+   * Resets the NewTabUtils module, its links and its storage.
+   */
+  reset: function NewTabUtils_reset() {
+    Storage.clear();
+    Links.resetCache();
+    PinnedLinks.resetCache();
+    BlockedLinks.resetCache();
+  },
+
+  allPages: AllPages,
+  links: Links,
+  pinnedLinks: PinnedLinks,
+  blockedLinks: BlockedLinks
+};
--- a/browser/themes/gnomestripe/devtools/gcli.css
+++ b/browser/themes/gnomestripe/devtools/gcli.css
@@ -105,16 +105,17 @@
   display: none;
 }
 
 .gcliterm-msg-body {
   margin-top: 0;
   margin-bottom: 3px;
   -moz-margin-start: 3px;
   -moz-margin-end: 6px;
+  list-style-image: none;
 }
 
 /* Extract from display.css, we only want these 2 rules */
 
 .gcli-out-shortcut {
   border: 1px solid #999;
   border-radius: 3px;
   padding: 0 4px;
--- a/browser/themes/gnomestripe/devtools/webconsole.css
+++ b/browser/themes/gnomestripe/devtools/webconsole.css
@@ -65,16 +65,21 @@
 
 .webconsole-timestamp {
   color: GrayText;
   margin-top: 0;
   margin-bottom: 0;
   font: 12px "DejaVu Sans Mono", monospace;
 }
 
+.hud-msg-node {
+  list-style-image: url(chrome://browser/skin/devtools/webconsole.png);
+  -moz-image-region: rect(0, 1px, 0, 0);
+}
+
 .webconsole-msg-icon {
   margin: 3px 4px;
   width: 8px;
   height: 8px;
 }
 
 .hud-clickable {
   cursor: pointer;
@@ -82,18 +87,16 @@
 }
 
 .webconsole-msg-body {
   margin-top: 0;
   margin-bottom: 3px;
   -moz-margin-start: 3px;
   -moz-margin-end: 6px;
   white-space: pre-wrap;
-  list-style-image: url(chrome://browser/skin/devtools/webconsole.png);
-  -moz-image-region: rect(0, 1px, 0, 0);
   font: 12px "DejaVu Sans Mono", monospace;
 }
 
 .webconsole-msg-body-piece {
   margin: 0;
 }
 
 .webconsole-msg-url {
--- a/browser/themes/gnomestripe/jar.mn
+++ b/browser/themes/gnomestripe/jar.mn
@@ -40,16 +40,19 @@ browser.jar:
   skin/classic/browser/feeds/feedIcon.png             (feeds/feedIcon.png)
   skin/classic/browser/feeds/feedIcon16.png           (feeds/feedIcon16.png)
   skin/classic/browser/feeds/videoFeedIcon.png        (feeds/feedIcon.png)
   skin/classic/browser/feeds/videoFeedIcon16.png      (feeds/feedIcon16.png)
   skin/classic/browser/feeds/audioFeedIcon.png        (feeds/feedIcon.png)
   skin/classic/browser/feeds/audioFeedIcon16.png      (feeds/feedIcon16.png)
   skin/classic/browser/feeds/subscribe.css            (feeds/subscribe.css)
   skin/classic/browser/feeds/subscribe-ui.css         (feeds/subscribe-ui.css)
+  skin/classic/browser/newtab/newTab.css              (newtab/newTab.css)
+  skin/classic/browser/newtab/strip.png               (newtab/strip.png)
+  skin/classic/browser/newtab/toolbar.png             (newtab/toolbar.png)
   skin/classic/browser/places/bookmarksMenu.png       (places/bookmarksMenu.png)
   skin/classic/browser/places/bookmarksToolbar.png    (places/bookmarksToolbar.png)
   skin/classic/browser/places/calendar.png            (places/calendar.png)
 * skin/classic/browser/places/editBookmarkOverlay.css (places/editBookmarkOverlay.css)
   skin/classic/browser/places/livemark-item.png       (places/livemark-item.png)
   skin/classic/browser/places/pageStarred.png         (places/pageStarred.png)
   skin/classic/browser/places/starred48.png           (places/starred48.png)
   skin/classic/browser/places/unstarred48.png         (places/unstarred48.png)
new file mode 100644
--- /dev/null
+++ b/browser/themes/gnomestripe/newtab/newTab.css
@@ -0,0 +1,142 @@
+#scrollbox {
+  padding-bottom: 18px;
+  background-color: #fff;
+}
+
+#body {
+  padding-top: 106px;
+  font-family: sans-serif;
+}
+
+.button {
+  padding: 0;
+  border: 0 none;
+}
+
+/* TOOLBAR */
+#toolbar {
+  top: 8px;
+  right: 8px;
+  width: 13px;
+  height: 30px;
+  padding: 0;
+  margin: 0;
+}
+
+.toolbar-button {
+  background: transparent url(chrome://browser/skin/newtab/toolbar.png);
+}
+
+#toolbar-button-show {
+  width: 11px;
+  height: 11px;
+  background-position: -10px 0;
+}
+
+#toolbar-button-show:hover {
+  background-position: -10px -12px;
+}
+
+#toolbar-button-show:active {
+  background-position: -10px -24px;
+}
+
+#toolbar-button-hide {
+  width: 10px;
+  height: 10px;
+}
+
+#toolbar-button-hide:hover {
+  background-position: 0 -12px;
+}
+
+#toolbar-button-hide:active {
+  background-position: 0 -24px;
+}
+
+#toolbar-button-reset {
+  top: 17px;
+  width: 11px;
+  height: 12px;
+}
+
+#toolbar-button-reset {
+  background-position: -21px 0;
+}
+
+#toolbar-button-reset:hover {
+  background-position: -21px -12px;
+}
+
+#toolbar-button-reset:active {
+  background-position: -21px -24px;
+}
+
+/* GRID */
+#grid {
+  padding: 1px;
+  margin: 0 auto;
+}
+
+/* SITES */
+.site {
+  background-color: #ececec;
+  -moz-transition: 200ms ease-out;
+  -moz-transition-property: top, left, box-shadow, opacity;
+}
+
+.site[dragged] {
+  -moz-transition-property: box-shadow;
+}
+
+.site[ontop] {
+  box-shadow: 0 1px 4px #000;
+  outline: none;
+}
+
+/* SITE TITLE */
+.site-title {
+  height: 2.4em;
+  width: 189px;
+  padding: 0 6px;
+  background-color: rgba(0,0,0,0.5);
+  border: solid transparent;
+  border-width: 6px 0;
+  color: #fff;
+  text-decoration: none;
+  line-height: 1.2em;
+  font-weight: 700;
+}
+
+/* SITE STRIP */
+.site-strip {
+  padding: 4px 3px;
+  background-color: rgba(0,0,0,0.5);
+}
+
+.strip-button {
+  width: 17px;
+  height: 17px;
+  background: transparent url(chrome://browser/skin/newtab/strip.png);
+}
+
+.strip-button-pin:hover {
+  background-position: 0 -17px;
+}
+
+.strip-button-pin:active,
+.site[pinned] .strip-button-pin {
+  background-position: 0 -34px;
+}
+
+.strip-button-block {
+  background-position: -17px 0;
+}
+
+.strip-button-block:hover {
+  background-position: -17px -17px;
+}
+
+.strip-button-block:active {
+  background-position: -17px -34px;
+}
--- a/browser/themes/pinstripe/devtools/gcli.css
+++ b/browser/themes/pinstripe/devtools/gcli.css
@@ -105,16 +105,17 @@
   display: none;
 }
 
 .gcliterm-msg-body {
   margin-top: 0;
   margin-bottom: 3px;
   -moz-margin-start: 3px;
   -moz-margin-end: 6px;
+  list-style-image: none;
 }
 
 /* Extract from display.css, we only want these 2 rules */
 
 .gcli-out-shortcut {
   border: 1px solid #999;
   border-radius: 3px;
   padding: 0 4px;
--- a/browser/themes/pinstripe/devtools/webconsole.css
+++ b/browser/themes/pinstripe/devtools/webconsole.css
@@ -68,16 +68,21 @@
 
 .webconsole-timestamp {
   color: GrayText;
   margin-top: 0;
   margin-bottom: 0;
   font: 11px Menlo, Monaco, monospace;
 }
 
+.hud-msg-node {
+  list-style-image: url(chrome://browser/skin/devtools/webconsole.png);
+  -moz-image-region: rect(0, 1px, 0, 0);
+}
+
 .webconsole-msg-icon {
   margin: 3px 4px;
   width: 8px;
   height: 8px;
 }
 
 .hud-clickable {
   cursor: pointer;
@@ -85,18 +90,16 @@
 }
 
 .webconsole-msg-body {
   margin-top: 0;
   margin-bottom: 3px;
   -moz-margin-start: 3px;
   -moz-margin-end: 6px;
   white-space: pre-wrap;
-  list-style-image: url(chrome://browser/skin/devtools/webconsole.png);
-  -moz-image-region: rect(0, 1px, 0, 0);
   font: 11px Menlo, Monaco, monospace;
 }
 
 .webconsole-msg-body-piece {
   margin: 0;
 }
 
 .webconsole-msg-url {
--- a/browser/themes/pinstripe/jar.mn
+++ b/browser/themes/pinstripe/jar.mn
@@ -50,16 +50,19 @@ browser.jar:
   skin/classic/browser/feeds/subscribe.css                  (feeds/subscribe.css)
   skin/classic/browser/feeds/subscribe-ui.css               (feeds/subscribe-ui.css)
   skin/classic/browser/feeds/feedIcon.png                   (feeds/feedIcon.png)
   skin/classic/browser/feeds/feedIcon16.png                 (feeds/feedIcon16.png)
   skin/classic/browser/feeds/videoFeedIcon.png              (feeds/feedIcon.png)
   skin/classic/browser/feeds/videoFeedIcon16.png            (feeds/feedIcon16.png)
   skin/classic/browser/feeds/audioFeedIcon.png              (feeds/feedIcon.png)
   skin/classic/browser/feeds/audioFeedIcon16.png            (feeds/feedIcon16.png)
+  skin/classic/browser/newtab/newTab.css                    (newtab/newTab.css)
+  skin/classic/browser/newtab/strip.png                     (newtab/strip.png)
+  skin/classic/browser/newtab/toolbar.png                   (newtab/toolbar.png)
   skin/classic/browser/setDesktopBackground.css
   skin/classic/browser/inspector.css
   skin/classic/browser/monitor.png
   skin/classic/browser/monitor_16-10.png
   skin/classic/browser/places/allBookmarks.png              (places/allBookmarks.png)
 * skin/classic/browser/places/places.css                    (places/places.css)
 * skin/classic/browser/places/organizer.css                 (places/organizer.css)
   skin/classic/browser/places/query.png                     (places/query.png)
new file mode 100644
--- /dev/null
+++ b/browser/themes/pinstripe/newtab/newTab.css
@@ -0,0 +1,141 @@
+#scrollbox {
+  padding-bottom: 18px;
+  background-color: #fff;
+}
+
+#body {
+  padding-top: 106px;
+  font-family: sans-serif;
+}
+
+.button {
+  padding: 0;
+  border: 0 none;
+}
+
+/* TOOLBAR */
+#toolbar {
+  top: 8px;
+  right: 8px;
+  width: 13px;
+  height: 30px;
+  padding: 0;
+  margin: 0;
+}
+
+.toolbar-button {
+  background: transparent url(chrome://browser/skin/newtab/toolbar.png);
+}
+
+#toolbar-button-show {
+  width: 11px;
+  height: 11px;
+  background-position: -10px 0;
+}
+
+#toolbar-button-show:hover {
+  background-position: -10px -12px;
+}
+
+#toolbar-button-show:active {
+  background-position: -10px -24px;
+}
+
+#toolbar-button-hide {
+  width: 10px;
+  height: 10px;
+}
+
+#toolbar-button-hide:hover {
+  background-position: 0 -12px;
+}
+
+#toolbar-button-hide:active {
+  background-position: 0 -24px;
+}
+
+#toolbar-button-reset {
+  top: 17px;
+  width: 11px;
+  height: 12px;
+}
+
+#toolbar-button-reset {
+  background-position: -21px 0;
+}
+
+#toolbar-button-reset:hover {
+  background-position: -21px -12px;
+}
+
+#toolbar-button-reset:active {
+  background-position: -21px -24px;
+}
+
+/* GRID */
+#grid {
+  padding: 1px;
+  margin: 0 auto;
+}
+
+/* SITES */
+.site {
+  background-color: #ececec;
+  -moz-transition: 200ms ease-out;
+  -moz-transition-property: top, left, box-shadow, opacity;
+}
+
+.site[dragged] {
+  -moz-transition-property: box-shadow;
+}
+
+.site[ontop] {
+  box-shadow: 0 1px 4px #000;
+}
+
+/* SITE TITLE */
+.site-title {
+  height: 2.4em;
+  width: 189px;
+  padding: 0 6px;
+  background-color: rgba(0,0,0,0.5);
+  border: solid transparent;
+  border-width: 6px 0;
+  color: #fff;
+  text-decoration: none;
+  line-height: 1.2em;
+  font-weight: 700;
+}
+
+/* SITE STRIP */
+.site-strip {
+  padding: 4px 3px;
+  background-color: rgba(0,0,0,0.5);
+}
+
+.strip-button {
+  width: 17px;
+  height: 17px;
+  background: transparent url(chrome://browser/skin/newtab/strip.png);
+}
+
+.strip-button-pin:hover {
+  background-position: 0 -17px;
+}
+
+.strip-button-pin:active,
+.site[pinned] .strip-button-pin {
+  background-position: 0 -34px;
+}
+
+.strip-button-block {
+  background-position: -17px 0;
+}
+
+.strip-button-block:hover {
+  background-position: -17px -17px;
+}
+
+.strip-button-block:active {
+  background-position: -17px -34px;
+}
--- a/browser/themes/winstripe/devtools/gcli.css
+++ b/browser/themes/winstripe/devtools/gcli.css
@@ -105,16 +105,17 @@
   display: none;
 }
 
 .gcliterm-msg-body {
   margin-top: 0;
   margin-bottom: 3px;
   -moz-margin-start: 3px;
   -moz-margin-end: 6px;
+  list-style-image: none;
 }
 
 /* Extract from display.css, we only want these 2 rules */
 
 .gcli-out-shortcut {
   border: 1px solid #999;
   border-radius: 3px;
   padding: 0 4px;
--- a/browser/themes/winstripe/devtools/webconsole.css
+++ b/browser/themes/winstripe/devtools/webconsole.css
@@ -64,16 +64,21 @@
 
 .webconsole-timestamp {
   color: GrayText;
   margin-top: 0;
   margin-bottom: 0;
   font: 12px Consolas, Lucida Console, monospace;
 }
 
+.hud-msg-node {
+  list-style-image: url(chrome://browser/skin/devtools/webconsole.png);
+  -moz-image-region: rect(0, 1px, 0, 0);
+}
+
 .webconsole-msg-icon {
   margin: 3px 4px;
   width: 8px;
   height: 8px;
 }
 
 .hud-clickable {
   cursor: pointer;
@@ -81,18 +86,16 @@
 }
 
 .webconsole-msg-body {
   margin-top: 0;
   margin-bottom: 3px;
   -moz-margin-start: 3px;
   -moz-margin-end: 6px;
   white-space: pre-wrap;
-  list-style-image: url(chrome://browser/skin/devtools/webconsole.png);
-  -moz-image-region: rect(0, 1px, 0, 0);
   font: 12px Consolas, Lucida Console, monospace;
 }
 
 .webconsole-msg-body-piece {
   margin: 0;
 }
 
 .webconsole-msg-url {
--- a/browser/themes/winstripe/jar.mn
+++ b/browser/themes/winstripe/jar.mn
@@ -52,16 +52,19 @@ browser.jar:
         skin/classic/browser/feeds/feedIcon.png                      (feeds/feedIcon.png)
         skin/classic/browser/feeds/feedIcon16.png                    (feeds/feedIcon16.png)
         skin/classic/browser/feeds/audioFeedIcon.png                 (feeds/feedIcon.png)
         skin/classic/browser/feeds/audioFeedIcon16.png               (feeds/feedIcon16.png)
         skin/classic/browser/feeds/videoFeedIcon.png                 (feeds/feedIcon.png)
         skin/classic/browser/feeds/videoFeedIcon16.png               (feeds/feedIcon16.png)
         skin/classic/browser/feeds/subscribe.css                     (feeds/subscribe.css)
         skin/classic/browser/feeds/subscribe-ui.css                  (feeds/subscribe-ui.css)
+        skin/classic/browser/newtab/newTab.css                       (newtab/newTab.css)
+        skin/classic/browser/newtab/strip.png                        (newtab/strip.png)
+        skin/classic/browser/newtab/toolbar.png                      (newtab/toolbar.png)
         skin/classic/browser/inspector.css
         skin/classic/browser/places/places.css                       (places/places.css)
 *       skin/classic/browser/places/organizer.css                    (places/organizer.css)
         skin/classic/browser/places/bookmark.png                     (places/bookmark.png)
         skin/classic/browser/places/editBookmark.png                 (places/editBookmark.png)
         skin/classic/browser/places/query.png                        (places/query.png)
         skin/classic/browser/places/bookmarksMenu.png                (places/bookmarksMenu.png)
         skin/classic/browser/places/bookmarksToolbar.png             (places/bookmarksToolbar.png)
@@ -211,16 +214,19 @@ browser.jar:
         skin/classic/aero/browser/feeds/feedIcon.png                 (feeds/feedIcon-aero.png)
         skin/classic/aero/browser/feeds/feedIcon16.png               (feeds/feedIcon16-aero.png)
         skin/classic/aero/browser/feeds/audioFeedIcon.png            (feeds/feedIcon-aero.png)
         skin/classic/aero/browser/feeds/audioFeedIcon16.png          (feeds/feedIcon16-aero.png)
         skin/classic/aero/browser/feeds/videoFeedIcon.png            (feeds/feedIcon-aero.png)
         skin/classic/aero/browser/feeds/videoFeedIcon16.png          (feeds/feedIcon16-aero.png)
         skin/classic/aero/browser/feeds/subscribe.css                (feeds/subscribe.css)
         skin/classic/aero/browser/feeds/subscribe-ui.css             (feeds/subscribe-ui.css)
+        skin/classic/aero/browser/newtab/newTab.css                  (newtab/newTab.css)
+        skin/classic/aero/browser/newtab/strip.png                   (newtab/strip.png)
+        skin/classic/aero/browser/newtab/toolbar.png                 (newtab/toolbar.png)
         skin/classic/aero/browser/inspector.css
 *       skin/classic/aero/browser/places/places.css                  (places/places-aero.css)
 *       skin/classic/aero/browser/places/organizer.css               (places/organizer-aero.css)
         skin/classic/aero/browser/places/bookmark.png                (places/bookmark.png)
         skin/classic/aero/browser/places/editBookmark.png            (places/editBookmark.png)
         skin/classic/aero/browser/places/query.png                   (places/query-aero.png)
         skin/classic/aero/browser/places/bookmarksMenu.png           (places/bookmarksMenu-aero.png)
         skin/classic/aero/browser/places/bookmarksToolbar.png        (places/bookmarksToolbar-aero.png)
new file mode 100644
--- /dev/null
+++ b/browser/themes/winstripe/newtab/newTab.css
@@ -0,0 +1,142 @@
+#scrollbox {
+  padding-bottom: 18px;
+  background-color: #fff;
+}
+
+#body {
+  padding-top: 106px;
+  font-family: sans-serif;
+}
+
+.button {
+  padding: 0;
+  border: 0 none;
+}
+
+/* TOOLBAR */
+#toolbar {
+  top: 8px;
+  right: 8px;
+  width: 13px;
+  height: 30px;
+  padding: 0;
+  margin: 0;
+}
+
+.toolbar-button {
+  background: transparent url(chrome://browser/skin/newtab/toolbar.png);
+}
+
+#toolbar-button-show {
+  width: 11px;
+  height: 11px;
+  background-position: -10px 0;
+}
+
+#toolbar-button-show:hover {
+  background-position: -10px -12px;
+}
+
+#toolbar-button-show:active {
+  background-position: -10px -24px;
+}
+
+#toolbar-button-hide {
+  width: 10px;
+  height: 10px;
+}
+
+#toolbar-button-hide:hover {
+  background-position: 0 -12px;
+}
+
+#toolbar-button-hide:active {
+  background-position: 0 -24px;
+}
+
+#toolbar-button-reset {
+  top: 17px;
+  width: 11px;
+  height: 12px;
+}
+
+#toolbar-button-reset {
+  background-position: -21px 0;
+}
+
+#toolbar-button-reset:hover {
+  background-position: -21px -12px;
+}
+
+#toolbar-button-reset:active {
+  background-position: -21px -24px;
+}
+
+/* GRID */
+#grid {
+  padding: 1px;
+  margin: 0 auto;
+}
+
+/* SITES */
+.site {
+  background-color: #ececec;
+  -moz-transition: 200ms ease-out;
+  -moz-transition-property: top, left, box-shadow, opacity;
+}
+
+.site[dragged] {
+  -moz-transition-property: box-shadow;
+}
+
+.site[ontop] {
+  box-shadow: 0 1px 4px #000;
+  outline: none;
+}
+
+/* SITE TITLE */
+.site-title {
+  height: 2.4em;
+  width: 189px;
+  padding: 0 6px;
+  background-color: rgba(0,0,0,0.5);
+  border: solid transparent;
+  border-width: 6px 0;
+  color: #fff;
+  text-decoration: none;
+  line-height: 1.2em;
+  font-weight: 700;
+}
+
+/* SITE STRIP */
+.site-strip {
+  padding: 4px 3px;
+  background-color: rgba(0,0,0,0.5);
+}
+
+.strip-button {
+  width: 17px;
+  height: 17px;
+  background: transparent url(chrome://browser/skin/newtab/strip.png);
+}
+
+.strip-button-pin:hover {
+  background-position: 0 -17px;
+}
+
+.strip-button-pin:active,
+.site[pinned] .strip-button-pin {
+  background-position: 0 -34px;
+}
+
+.strip-button-block {
+  background-position: -17px 0;
+}
+
+.strip-button-block:hover {
+  background-position: -17px -17px;
+}
+
+.strip-button-block:active {
+  background-position: -17px -34px;
+}
--- a/toolkit/content/Services.jsm
+++ b/toolkit/content/Services.jsm
@@ -72,20 +72,22 @@ let initTable = [
   ["eTLD", "@mozilla.org/network/effective-tld-service;1", "nsIEffectiveTLDService"],
   ["io", "@mozilla.org/network/io-service;1", "nsIIOService2"],
   ["locale", "@mozilla.org/intl/nslocaleservice;1", "nsILocaleService"],
   ["logins", "@mozilla.org/login-manager;1", "nsILoginManager"],
   ["obs", "@mozilla.org/observer-service;1", "nsIObserverService"],
   ["perms", "@mozilla.org/permissionmanager;1", "nsIPermissionManager"],
   ["prompt", "@mozilla.org/embedcomp/prompt-service;1", "nsIPromptService"],
   ["scriptloader", "@mozilla.org/moz/jssubscript-loader;1", "mozIJSSubScriptLoader"],
+  ["scriptSecurityManager", "@mozilla.org/scriptsecuritymanager;1", "nsIScriptSecurityManager"],
 #ifdef MOZ_TOOLKIT_SEARCH
   ["search", "@mozilla.org/browser/search-service;1", "nsIBrowserSearchService"],
 #endif
   ["storage", "@mozilla.org/storage/service;1", "mozIStorageService"],
+  ["domStorageManager", "@mozilla.org/dom/storagemanager;1", "nsIDOMStorageManager"],
   ["strings", "@mozilla.org/intl/stringbundle;1", "nsIStringBundleService"],
   ["telemetry", "@mozilla.org/base/telemetry;1", "nsITelemetry"],
   ["tm", "@mozilla.org/thread-manager;1", "nsIThreadManager"],
   ["urlFormatter", "@mozilla.org/toolkit/URLFormatterService;1", "nsIURLFormatter"],
   ["vc", "@mozilla.org/xpcom/version-comparator;1", "nsIVersionComparator"],
   ["wm", "@mozilla.org/appshell/window-mediator;1", "nsIWindowMediator"],
   ["ww", "@mozilla.org/embedcomp/window-watcher;1", "nsIWindowWatcher"],
   ["startup", "@mozilla.org/toolkit/app-startup;1", "nsIAppStartup"],
--- a/toolkit/content/customizeToolbar.xul
+++ b/toolkit/content/customizeToolbar.xul
@@ -47,17 +47,17 @@
 <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
 <?xml-stylesheet href="chrome://global/content/customizeToolbar.css" type="text/css"?>
 <?xml-stylesheet href="chrome://global/skin/customizeToolbar.css" type="text/css"?>
 
 <window id="CustomizeToolbarWindow"
         title="&dialog.title;"
         onload="onLoad();"
         onunload="onUnload();"
-        style="&dialog.style;"
+        style="&dialog.dimensions;"
         persist="width height"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
 <script type="application/javascript" src="chrome://global/content/customizeToolbar.js"/>
 
 <stringbundle id="stringBundle" src="chrome://global/locale/customizeToolbar.properties"/>
 
 <keyset id="CustomizeToolbarKeyset">
--- a/toolkit/content/widgets/videocontrols.xml
+++ b/toolkit/content/widgets/videocontrols.xml
@@ -934,17 +934,17 @@
                     this.video.muted = !this.video.muted;
 
                     // We'll handle style changes in the event listener for
                     // the "volumechange" event, same as if content script was
                     // controlling volume.
                 },
 
                 isVideoInFullScreen : function () {
-                    return document.mozFullScreenElement != null;
+                    return document.mozFullScreenElement == this.video;
                 },
 
                 toggleFullscreen : function () {
                     this.isVideoInFullScreen() ?
                         document.mozCancelFullScreen() :
                         this.video.mozRequestFullScreen();
                 },
 
--- a/toolkit/locales/en-US/chrome/global/customizeToolbar.dtd
+++ b/toolkit/locales/en-US/chrome/global/customizeToolbar.dtd
@@ -1,10 +1,10 @@
 <!ENTITY dialog.title             "Customize Toolbar">
-<!ENTITY dialog.style             "width: 86ch; height: 36em;">
+<!ENTITY dialog.dimensions        "width: 92ch; height: 36em;">
 <!ENTITY instructions.description "You can add or remove items by dragging to or from the toolbars.">
 <!ENTITY show.label               "Show:">
 <!ENTITY iconsAndText.label       "Icons and Text">
 <!ENTITY icons.label              "Icons">
 <!ENTITY text.label               "Text">
 <!ENTITY useSmallIcons.label      "Use Small Icons">
 <!ENTITY restoreDefaultSet.label  "Restore Default Set">
 <!ENTITY addNewToolbar.label      "Add New Toolbar">
index f26dfba3777bf3480ce05a3280a9d93376fda3ed..ffbc3d5ae4627d9beb7c4e8c6ef80542e7e1414c
GIT binary patch
literal 733
zc$@*<0wVp1P)<h;3K|Lk000e1NJLTq001BW000mO1ONa4-3hhQ0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!e@R3^R5(wKQr}C{Q4~L8`_eL~udZa(
zw|%$RVAF?wJoF$T5)vzsxgRPu-H)$Bm^I9)o8UHWLiy07U@u|p-S>!~haiZcs4TUK
zN%fHUw2kH;a60#X{f<DW&Ic~{bI!fzo_o#*;9sV%bv|za-2uPu#j#o!=mCtltYUWn
z3_N7m9ngGmWUU7adv{AGK&ba=aoPz*)7~5w%Dn()oMBi|zyU^nzzB{NMfHNE<NWtK
zn62eE6<h?7$3<K$M{4t!`#EvtB7has4BxW7?I7?2Lbq!>qk6#(vg|ESVg~PF3M3wv
zFhwl8J!KDol_f|$NUcp{rk1Bdb)YMnD0_C3ymZ9|hoNox7EUp+<G^lk_GAp&p^5U9
z!Vow;Tu$Qb8z0pmy!PPi*Q8|_0u+%z_<R!=aFU^sfHPe97-Thz^miMy$=-YfT=f4#
z;LJzP`YF4OA|iS0;D#ZH&}!xh%(RP;=Nd(*YX({bDpaQHt;7tpy6CMY@)|i8sSYc_
z+-S0ybBQAAT*!3$$C;@q4^IBCrh9g@dYQ?BOke4E+Wh|s48N{}u+&nY#<>kQy{=!q
ziu3jK<KrMugubxj!Ga3y3mV_?u$o1jS-(+DVd96<d4V<Y(z&fD-ma!Mf+Qr?L`P{5
zV+>joRaP2Qr6-|6Qyu#<BKxQ@G)9om^nDt^IC&K|#x7?$fl-FW7&+TYbi3FXtr%i>
z6y~xZAu;JJg;g)u?l)H^aCBA4#ffPNLb>?cD2`T=MH8b#9OLi&nv~!Ba9s6*{iPdY
z>!FiD2t4#_Ql?#?HTWTfu~$6@WWk#Bhjc99O!iCOhOC(FOYf8?*$=MY`h^(+ky7qW
P00000NkvXXu0mjfwAEM1
index c51d25c33cd842ccd7de207fa847b823c08d84ee..894480761d0742015ffcfcc2d28bc10e88cbe4a9
GIT binary patch
literal 619
zc$@)i0+juUP)<h;3K|Lk000e1NJLTq001EX000~a1^@s6?*P^u0006qNkl<Zc-rlj
z-Ah_w9LIY(%OX$I#i(R0!Lb?Dx^NKDMO_RAyO8Q;C2T~^g#{f-ds`}!_A(fFw?Dv3
z{|hg@QP5j2L=Y8JP*D*t-1j%019GT!@fe4;gb)5+4(Iut=R7`-U9DD2b)F<1r$mQ8
z3JLCk)t3;;<?<?ht0f^M5{Y$O_XF{;8WOU=4LCvmqOI4HOeUuag~A7q$J75%5+v{m
z{cCVcpr&u@4`j31ebj$eDwT_1FgS)<cPAxefh%w%qmUT(dc7;TT+V@>t5hmon9XL9
zSE45ti_KQ6)pOK-flFX-M<Fo?W^_8;Mk<w}{&_eY{<C;_BnIpCx)1qxaCjR`Lepq8
z&M@E{Tc|Tut96ScA>%+U@<@y$c7TJsU=q&}pF!A*SPO>qdi@gho6Y7a@-ZHXNyO6M
zU=lAW&gb*{Y(T+iG|p13R;x+W=aR@|GP}t4?t)27LcnIRSk`5UKp^mu`jJS)hJ1qS
zb<mB|=`@MhZi7jTf(7svXeCJ+Ef$L>=<oP^zR$d`L(6Ww3bve1Cq4bQcI1T#M!*mj
z=R5qG+$0!}$G1gMT%<aWL@yH=2XDZ8x7%$+e_KYmS%pHOHL6FW(M^NFu)@}Z(6P;m
zP3yAD<@$<iHVNfEVKSNK?RNWyMx!w!e@$W?-?n8Dyc(K`FA9l1paauv1_pSy?R!Q1
ze*c={0P6<NKtB*3{3vH4uT(h76(a13|NFmp|CWU6{04~YFpk{n7DWI6002ovPDHLk
FV1jWc9RdIV
index 767edd3089c159e8b6e3a83d32cc1259bfc22bc2..944098ca19a4f63331ae852606c3907285e7e203
GIT binary patch
literal 269
zc%17D@N?(olHy`uVBq!ia0vp^G9b*s1|*Ak?@s|zS3F%DLn>~)oo2{&C_sc|ZBbJ^
zd;X+m78e1QjRMM9x8@wKev<O?arLI7VbMD#Iq~xR`O`R8FU9oy=S6>-<7NIF_#C!j
z*0PoT-`yuY-^0D6yh-((^Mz)`_e?M6CdfXwo?PDe#jim2xipB8_u*7RK!trpqJhR9
z`8N_gT<_V>^RTskZ&qh(KJ@#ba<k*ZzYiJ@F8EMaa2}*^M>0s^9U}>md;EDI<*cs@
zWo}p0PfV|wvlM7kMw6Jyj>-2p!z5NX9-RBd?Ly@={(mp>9|>1!@A!0CZPs?6{}?=7
L{an^LB{Ts5-H&uP
index daa9ca6e4caba8df1d13f3a4a4cdf92e63574230..11e2731df64f9dd00623f263150d8a651ee1b1bc
GIT binary patch
literal 448
zc$@*i0YCnUP)<h;3K|Lk000e1NJLTq000~S000~a1^@s6at+^<0004pNkl<Zc-rmO
z%}c^y90%}Tuu)B96&6G|iY^{HL_!1uFF~iEF2RO1$OILN-W0@v9zuj(=qUSF93FQO
z{{#^Pck!@`2peMGK?e_6>Yj}b4nFWa&z^@bJkR#qX8QfUtEPeO1QNdfZy;rwX0hFF
zM@prV5B*uE1N~@cp<o!sTfJUC#@M`*fdRDB-!kyp>2&h7S}llsb4~=ZXs2LM7Cy1f
ziAtriVQn+%z~>?Iy<YF5*=(NYa=9fdm{j27FP`9~+wB%`zMOTwBmy5skb<JF>*;E>
zx?`QMoxuAsWZ=EkYVla>nKbZjT#BMZu+}?iU~*LDa``qC3W->EU?(vC7d$XYp6BD3
ze+qlBW@k%d!=MPnBuPrpG<|`&hp-8Yb|0GSpUAQtXIb_NbHcC<UQ(}xI7n61D93S8
z+&h6Ftbm8)i+^d+G))u);Ra)8un+4%kuGh4Mx!C1bVV74;n6>WEm(G{c>95aFzkZQ
q>CY1dtH8j5jSukjpT_w>SA7AyaD`eO0!@Gb0000<MNUMnLSTZ}pUTbv
index b6dc700e68555400caa2015139ce25d285bd8cc6..f159627631c536f0def829f45f3715f198db1a5e
GIT binary patch
literal 562
zc$@(;0?qx2P)<h;3K|Lk000e1NJLTq001EX000~a1^@s6?*P^u0005`Nkl<Zc-rlk
zJxe1|6o#+Sh#MmkWI<RVjX%IDorNIz0T%fK5!ogKZVD?QA0%W#Cf_6>O~68ljj(Lv
zk5MaO3$bt}*1j(sG6WevP!0$lcye>kJ-N?e?ky(fI|3r4$85}lJz#-DkX3*li(c`Y
zOfs45_j<kS@p$~G1%33=56sbMG`brM241JrQG-5u=?A7T91bV#cH7g2UiyKt`~Ci;
z)oOX#&`UosHJs0+*=%~+&`ZDJN4MMc>h-#-4ZYJQcu^e3akYWhZ-SSkR;#(%!0R``
z^Ql&=t~T)cO*9$}&$ew>8+iRDa7nyMrQ&J>uipg1E0@czHt^0iQ7V;OWoDYNbGh6d
zk`KjVQ4RX&r5~7lB9XW(6bkpRT_m0(e~j8dxX!xQ_M(qo`hkf92b_Ql@Hc>e;4+m;
zT~U<J=bynm*ICE<7xuakpCf*=B44(^HrNSZ7aV}IbUJ;5N6N=Jg>&Y)E_J&v?6obv
zxcJQi_yJbI8u%H&I@kpJ;c)mglgZpdJZ*q;=D99)>w$aGLm$2L<Hh1lAm+dl*Z_Z`
z(dc<LoBhvQ&XIHGWqpbDZ`{|&<Xw$K<S`z{VzG0}vTitXF8PSxz-KohAVOeSgug*u
z#<=7|@7V9de+?pXzQ1IQNM1E4TmXwQV*Z8W36?vi;x!bAB>(^b07*qoM6N<$f|;uc
A!vFvP
index c1908cd7aad34b06ca5e7b8800b33d1f2c88f06c..96d2ed75140584a92940e65b4f5d1f7a60a133c3
GIT binary patch
literal 563
zc$@(<0?hr1P)<h;3K|Lk000e1NJLTq001oj000~a1^@s6^*lC`0005{Nkl<Zc-rlj
z%}N4c6ox0uC^JGN2nyVE0j=D%2*L|!;SCtkMxk7^3j9HF#PJ^$H$scTHbJ!MVyacp
zB3d+K>)tm`4N@(fg3h7?56AC(=bP_=IRhI1@kim)%XmzK9gqSCAgz!C@!Jt!BN>au
zx}8qvb~qfqXoY_Cq%Wn927|$4uh(;0t(IEoM^E}vI@j;_N6lu_(GET7OKG#)?T#9a
zhNB&N(wEW|%*&`=uRGeICw<?f+wHbftyXRA(DOr@gKk-ttsQu}(j4+irDAIbo~|_K
zrCcuC+JUDlU8~g`(==`Ez|)n+kUOPP$<_`$U1@|<EEa9;z;j%>P$<~S8Iv|Mnam@K
z&-r{_E%c)&eJP!dMx)ocT<*#12f}&dL-CAn;XeDk-a$Wl(wEW^V1W~G0WK%x3S7tI
z@f(U|v)NCu&VBZ=-@_dj;%AAku_!+_z$Vz5kZrIB&Ju~l9bP40;|E-`&VA|I_Hf6h
z_(jCmm;v)(353D&gsgydu<Q5xPm{^y{ku3b*Q|42`c^#L5f(pq!WVz6e*|I*%z-s<
z7z_r_)9LgBzjkJ>S(p7e_K%(S5Ip!>;sJSm$DvT@Je5k_F>@{TfGey(njm~&K?L_f
zRdPh?zOT+{2#N<}4p$`yq^>$BI0I%S`;7lEj92mUjv&D^yS)Ga002ovPDHLkV1jJ=
B4P5{L
index d50b7d75a695379ebe0499fd288635dba0b76cc7..5b76e2fa45d8481ca0a81e524c4a20508adfc284
GIT binary patch
literal 324
zc$@)50lWT*P)<h;3K|Lk000e1NJLTq001EX000~a1^@s6?*P^u0003CNkl<Zc-rmO
zKZ}B30LSs;&f%bj#)6=RgQj{74ZQ(d3Wqql3aTOc52a4s)lF`_FlSeB7sSzi4-^QG
z1sBK9Md6FjqtEbBL4Ap$DD^T#hWHcW$&HRseBW2aJ}rdeI8GPqelafa5HjF|wwRaM
z2m$Ee{1I(LI94SfB)|_^-(nzMZQHH~L9oZMGx|o$cnm}?48sHFJG4lOfhb|TLi2ku
z5EYDPkD?$v&zoRAy%*(iXqwh_T~|QHNih(4)Icig>$)zkeiV;^c)LPMsD@gmX%2Dz
z>lXcGg^H+Y7{&l=K1D=?48fxjccMo|$Pml2+WZfg4Ef-H%4Ntq@u%kTmLdKNk$M-z
WIy6#<EqF%&0000<MNUMnLSTa1(TaZn
--- a/toolkit/themes/pinstripe/global/media/videocontrols.css
+++ b/toolkit/themes/pinstripe/global/media/videocontrols.css
@@ -164,17 +164,17 @@
   background: url(chrome://global/skin/media/scrubberThumbWide.png) no-repeat center;
 }
 
 .timeLabel {
   color: rgba(255,255,255,.75);
   font-size: 10px;
   font-family: Helvetica, sans-serif;
   text-shadow: rgba(0,0,0,.3) 0 1px;
-  padding-top: 4px;
+  padding-top: 7px;
 }
 
 .statusOverlay {
   -moz-box-align: center;
   -moz-box-pack: center;
   background-color: rgba(0,0,0,.55);
 }
 
index f26dfba3777bf3480ce05a3280a9d93376fda3ed..ffbc3d5ae4627d9beb7c4e8c6ef80542e7e1414c
GIT binary patch
literal 733
zc$@*<0wVp1P)<h;3K|Lk000e1NJLTq001BW000mO1ONa4-3hhQ0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!e@R3^R5(wKQr}C{Q4~L8`_eL~udZa(
zw|%$RVAF?wJoF$T5)vzsxgRPu-H)$Bm^I9)o8UHWLiy07U@u|p-S>!~haiZcs4TUK
zN%fHUw2kH;a60#X{f<DW&Ic~{bI!fzo_o#*;9sV%bv|za-2uPu#j#o!=mCtltYUWn
z3_N7m9ngGmWUU7adv{AGK&ba=aoPz*)7~5w%Dn()oMBi|zyU^nzzB{NMfHNE<NWtK
zn62eE6<h?7$3<K$M{4t!`#EvtB7has4BxW7?I7?2Lbq!>qk6#(vg|ESVg~PF3M3wv
zFhwl8J!KDol_f|$NUcp{rk1Bdb)YMnD0_C3ymZ9|hoNox7EUp+<G^lk_GAp&p^5U9
z!Vow;Tu$Qb8z0pmy!PPi*Q8|_0u+%z_<R!=aFU^sfHPe97-Thz^miMy$=-YfT=f4#
z;LJzP`YF4OA|iS0;D#ZH&}!xh%(RP;=Nd(*YX({bDpaQHt;7tpy6CMY@)|i8sSYc_
z+-S0ybBQAAT*!3$$C;@q4^IBCrh9g@dYQ?BOke4E+Wh|s48N{}u+&nY#<>kQy{=!q
ziu3jK<KrMugubxj!Ga3y3mV_?u$o1jS-(+DVd96<d4V<Y(z&fD-ma!Mf+Qr?L`P{5
zV+>joRaP2Qr6-|6Qyu#<BKxQ@G)9om^nDt^IC&K|#x7?$fl-FW7&+TYbi3FXtr%i>
z6y~xZAu;JJg;g)u?l)H^aCBA4#ffPNLb>?cD2`T=MH8b#9OLi&nv~!Ba9s6*{iPdY
z>!FiD2t4#_Ql?#?HTWTfu~$6@WWk#Bhjc99O!iCOhOC(FOYf8?*$=MY`h^(+ky7qW
P00000NkvXXu0mjfwAEM1
index c51d25c33cd842ccd7de207fa847b823c08d84ee..894480761d0742015ffcfcc2d28bc10e88cbe4a9
GIT binary patch
literal 619
zc$@)i0+juUP)<h;3K|Lk000e1NJLTq001EX000~a1^@s6?*P^u0006qNkl<Zc-rlj
z-Ah_w9LIY(%OX$I#i(R0!Lb?Dx^NKDMO_RAyO8Q;C2T~^g#{f-ds`}!_A(fFw?Dv3
z{|hg@QP5j2L=Y8JP*D*t-1j%019GT!@fe4;gb)5+4(Iut=R7`-U9DD2b)F<1r$mQ8
z3JLCk)t3;;<?<?ht0f^M5{Y$O_XF{;8WOU=4LCvmqOI4HOeUuag~A7q$J75%5+v{m
z{cCVcpr&u@4`j31ebj$eDwT_1FgS)<cPAxefh%w%qmUT(dc7;TT+V@>t5hmon9XL9
zSE45ti_KQ6)pOK-flFX-M<Fo?W^_8;Mk<w}{&_eY{<C;_BnIpCx)1qxaCjR`Lepq8
z&M@E{Tc|Tut96ScA>%+U@<@y$c7TJsU=q&}pF!A*SPO>qdi@gho6Y7a@-ZHXNyO6M
zU=lAW&gb*{Y(T+iG|p13R;x+W=aR@|GP}t4?t)27LcnIRSk`5UKp^mu`jJS)hJ1qS
zb<mB|=`@MhZi7jTf(7svXeCJ+Ef$L>=<oP^zR$d`L(6Ww3bve1Cq4bQcI1T#M!*mj
z=R5qG+$0!}$G1gMT%<aWL@yH=2XDZ8x7%$+e_KYmS%pHOHL6FW(M^NFu)@}Z(6P;m
zP3yAD<@$<iHVNfEVKSNK?RNWyMx!w!e@$W?-?n8Dyc(K`FA9l1paauv1_pSy?R!Q1
ze*c={0P6<NKtB*3{3vH4uT(h76(a13|NFmp|CWU6{04~YFpk{n7DWI6002ovPDHLk
FV1jWc9RdIV
index 767edd3089c159e8b6e3a83d32cc1259bfc22bc2..944098ca19a4f63331ae852606c3907285e7e203
GIT binary patch
literal 269
zc%17D@N?(olHy`uVBq!ia0vp^G9b*s1|*Ak?@s|zS3F%DLn>~)oo2{&C_sc|ZBbJ^
zd;X+m78e1QjRMM9x8@wKev<O?arLI7VbMD#Iq~xR`O`R8FU9oy=S6>-<7NIF_#C!j
z*0PoT-`yuY-^0D6yh-((^Mz)`_e?M6CdfXwo?PDe#jim2xipB8_u*7RK!trpqJhR9
z`8N_gT<_V>^RTskZ&qh(KJ@#ba<k*ZzYiJ@F8EMaa2}*^M>0s^9U}>md;EDI<*cs@
zWo}p0PfV|wvlM7kMw6Jyj>-2p!z5NX9-RBd?Ly@={(mp>9|>1!@A!0CZPs?6{}?=7
L{an^LB{Ts5-H&uP
index daa9ca6e4caba8df1d13f3a4a4cdf92e63574230..11e2731df64f9dd00623f263150d8a651ee1b1bc
GIT binary patch
literal 448
zc$@*i0YCnUP)<h;3K|Lk000e1NJLTq000~S000~a1^@s6at+^<0004pNkl<Zc-rmO
z%}c^y90%}Tuu)B96&6G|iY^{HL_!1uFF~iEF2RO1$OILN-W0@v9zuj(=qUSF93FQO
z{{#^Pck!@`2peMGK?e_6>Yj}b4nFWa&z^@bJkR#qX8QfUtEPeO1QNdfZy;rwX0hFF
zM@prV5B*uE1N~@cp<o!sTfJUC#@M`*fdRDB-!kyp>2&h7S}llsb4~=ZXs2LM7Cy1f
ziAtriVQn+%z~>?Iy<YF5*=(NYa=9fdm{j27FP`9~+wB%`zMOTwBmy5skb<JF>*;E>
zx?`QMoxuAsWZ=EkYVla>nKbZjT#BMZu+}?iU~*LDa``qC3W->EU?(vC7d$XYp6BD3
ze+qlBW@k%d!=MPnBuPrpG<|`&hp-8Yb|0GSpUAQtXIb_NbHcC<UQ(}xI7n61D93S8
z+&h6Ftbm8)i+^d+G))u);Ra)8un+4%kuGh4Mx!C1bVV74;n6>WEm(G{c>95aFzkZQ
q>CY1dtH8j5jSukjpT_w>SA7AyaD`eO0!@Gb0000<MNUMnLSTZ}pUTbv
index b6dc700e68555400caa2015139ce25d285bd8cc6..f159627631c536f0def829f45f3715f198db1a5e
GIT binary patch
literal 562
zc$@(;0?qx2P)<h;3K|Lk000e1NJLTq001EX000~a1^@s6?*P^u0005`Nkl<Zc-rlk
zJxe1|6o#+Sh#MmkWI<RVjX%IDorNIz0T%fK5!ogKZVD?QA0%W#Cf_6>O~68ljj(Lv
zk5MaO3$bt}*1j(sG6WevP!0$lcye>kJ-N?e?ky(fI|3r4$85}lJz#-DkX3*li(c`Y
zOfs45_j<kS@p$~G1%33=56sbMG`brM241JrQG-5u=?A7T91bV#cH7g2UiyKt`~Ci;
z)oOX#&`UosHJs0+*=%~+&`ZDJN4MMc>h-#-4ZYJQcu^e3akYWhZ-SSkR;#(%!0R``
z^Ql&=t~T)cO*9$}&$ew>8+iRDa7nyMrQ&J>uipg1E0@czHt^0iQ7V;OWoDYNbGh6d
zk`KjVQ4RX&r5~7lB9XW(6bkpRT_m0(e~j8dxX!xQ_M(qo`hkf92b_Ql@Hc>e;4+m;
zT~U<J=bynm*ICE<7xuakpCf*=B44(^HrNSZ7aV}IbUJ;5N6N=Jg>&Y)E_J&v?6obv
zxcJQi_yJbI8u%H&I@kpJ;c)mglgZpdJZ*q;=D99)>w$aGLm$2L<Hh1lAm+dl*Z_Z`
z(dc<LoBhvQ&XIHGWqpbDZ`{|&<Xw$K<S`z{VzG0}vTitXF8PSxz-KohAVOeSgug*u
z#<=7|@7V9de+?pXzQ1IQNM1E4TmXwQV*Z8W36?vi;x!bAB>(^b07*qoM6N<$f|;uc
A!vFvP
index c1908cd7aad34b06ca5e7b8800b33d1f2c88f06c..96d2ed75140584a92940e65b4f5d1f7a60a133c3
GIT binary patch
literal 563
zc$@(<0?hr1P)<h;3K|Lk000e1NJLTq001oj000~a1^@s6^*lC`0005{Nkl<Zc-rlj
z%}N4c6ox0uC^JGN2nyVE0j=D%2*L|!;SCtkMxk7^3j9HF#PJ^$H$scTHbJ!MVyacp
zB3d+K>)tm`4N@(fg3h7?56AC(=bP_=IRhI1@kim)%XmzK9gqSCAgz!C@!Jt!BN>au
zx}8qvb~qfqXoY_Cq%Wn927|$4uh(;0t(IEoM^E}vI@j;_N6lu_(GET7OKG#)?T#9a
zhNB&N(wEW|%*&`=uRGeICw<?f+wHbftyXRA(DOr@gKk-ttsQu}(j4+irDAIbo~|_K
zrCcuC+JUDlU8~g`(==`Ez|)n+kUOPP$<_`$U1@|<EEa9;z;j%>P$<~S8Iv|Mnam@K
z&-r{_E%c)&eJP!dMx)ocT<*#12f}&dL-CAn;XeDk-a$Wl(wEW^V1W~G0WK%x3S7tI
z@f(U|v)NCu&VBZ=-@_dj;%AAku_!+_z$Vz5kZrIB&Ju~l9bP40;|E-`&VA|I_Hf6h
z_(jCmm;v)(353D&gsgydu<Q5xPm{^y{ku3b*Q|42`c^#L5f(pq!WVz6e*|I*%z-s<
z7z_r_)9LgBzjkJ>S(p7e_K%(S5Ip!>;sJSm$DvT@Je5k_F>@{TfGey(njm~&K?L_f
zRdPh?zOT+{2#N<}4p$`yq^>$BI0I%S`;7lEj92mUjv&D^yS)Ga002ovPDHLkV1jJ=
B4P5{L
index d50b7d75a695379ebe0499fd288635dba0b76cc7..5b76e2fa45d8481ca0a81e524c4a20508adfc284
GIT binary patch
literal 324
zc$@)50lWT*P)<h;3K|Lk000e1NJLTq001EX000~a1^@s6?*P^u0003CNkl<Zc-rmO
zKZ}B30LSs;&f%bj#)6=RgQj{74ZQ(d3Wqql3aTOc52a4s)lF`_FlSeB7sSzi4-^QG
z1sBK9Md6FjqtEbBL4Ap$DD^T#hWHcW$&HRseBW2aJ}rdeI8GPqelafa5HjF|wwRaM
z2m$Ee{1I(LI94SfB)|_^-(nzMZQHH~L9oZMGx|o$cnm}?48sHFJG4lOfhb|TLi2ku
z5EYDPkD?$v&zoRAy%*(iXqwh_T~|QHNih(4)Icig>$)zkeiV;^c)LPMsD@gmX%2Dz
z>lXcGg^H+Y7{&l=K1D=?48fxjccMo|$Pml2+WZfg4Ef-H%4Ntq@u%kTmLdKNk$M-z
WIy6#<EqF%&0000<MNUMnLSTa1(TaZn
--- a/toolkit/themes/winstripe/global/media/videocontrols.css
+++ b/toolkit/themes/winstripe/global/media/videocontrols.css
@@ -173,17 +173,17 @@
   background: url(chrome://global/skin/media/scrubberThumbWide.png) no-repeat center;
 }
 
 .timeLabel {
   color: rgba(255,255,255,.75);
   font-size: 10px;
   font-family: Arial, sans-serif;
   text-shadow: rgba(0,0,0,.3) 0 1px;
-  padding-top: 2px;
+  padding-top: 5px;
 }
 
 .statusOverlay {
   -moz-box-align: center;
   -moz-box-pack: center;
   background-color: rgba(0,0,0,.55);
 }