diff --git a/package.json b/package.json
index d14d1c7a5166f8cf321335363872fa2da4806c6b..219811f62a5a6226c7d26fe712205e580c8f1858 100644
--- a/package.json
+++ b/package.json
@@ -32,13 +32,14 @@
     "classnames": "^2.2.5",
     "detect-browser": "^2.1.0",
     "event-target-shim": "^3.0.1",
+    "form-urlencoded": "^2.0.4",
     "jsonschema": "^1.2.2",
     "minijanus": "^0.5.0",
     "mobile-detect": "^1.4.1",
     "moment": "^2.22.0",
     "moment-timezone": "^0.5.14",
     "moving-average": "^1.0.0",
-    "naf-janus-adapter": "https://github.com/mozilla/naf-janus-adapter#feature/disconnect",
+    "naf-janus-adapter": "^0.5.2",
     "networked-aframe": "https://github.com/mozillareality/networked-aframe#mr-social-client/master",
     "nipplejs": "^0.6.7",
     "phoenix": "^1.3.0",
diff --git a/src/App.js b/src/App.js
index c542f439f903e0a4426d087cb33d69852b25944d..b814e080d4e1df7ae5f6dbe7649d2548211f4c42 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,7 +1,10 @@
+import Store from "./storage/store";
+
 export class App {
   constructor() {
     this.scene = null;
     this.quality = "low";
+    this.store = new Store();
   }
 
   setQuality(quality) {
diff --git a/src/assets/hud/avatar.jpg b/src/assets/hud/avatar.jpg
deleted file mode 100755
index af8093c601f6dcbc74b431ff7f7aca414cf4dc01..0000000000000000000000000000000000000000
Binary files a/src/assets/hud/avatar.jpg and /dev/null differ
diff --git a/src/assets/hud/avatar.png b/src/assets/hud/avatar.png
deleted file mode 100644
index d604f8f18d51b481bcc0013f30e2c610100b231c..0000000000000000000000000000000000000000
Binary files a/src/assets/hud/avatar.png and /dev/null differ
diff --git a/src/assets/hud/bubble_off-hover.png b/src/assets/hud/bubble_off-hover.png
new file mode 100644
index 0000000000000000000000000000000000000000..efa274eb628b529cbf3959cdb3db9597ead49112
Binary files /dev/null and b/src/assets/hud/bubble_off-hover.png differ
diff --git a/src/assets/hud/bubble_off.png b/src/assets/hud/bubble_off.png
new file mode 100644
index 0000000000000000000000000000000000000000..782f4d1aa025d5d2ae08bbf0fb745989c67b4157
Binary files /dev/null and b/src/assets/hud/bubble_off.png differ
diff --git a/src/assets/hud/bubble_on-hover.png b/src/assets/hud/bubble_on-hover.png
new file mode 100644
index 0000000000000000000000000000000000000000..ddc5a202747eeaf513f1261664e56b294fb51b32
Binary files /dev/null and b/src/assets/hud/bubble_on-hover.png differ
diff --git a/src/assets/hud/bubble_on.png b/src/assets/hud/bubble_on.png
new file mode 100644
index 0000000000000000000000000000000000000000..0a70407d65498848bd50c6ebcb7167c35c0d8a5f
Binary files /dev/null and b/src/assets/hud/bubble_on.png differ
diff --git a/src/assets/hud/freeze_off-hover.png b/src/assets/hud/freeze_off-hover.png
new file mode 100644
index 0000000000000000000000000000000000000000..80af8637d4d75559166529fb53e96c424d0be695
Binary files /dev/null and b/src/assets/hud/freeze_off-hover.png differ
diff --git a/src/assets/hud/freeze_off.png b/src/assets/hud/freeze_off.png
new file mode 100644
index 0000000000000000000000000000000000000000..302c7a23742b0ac94a7ffc1406935bf90a0a9be6
Binary files /dev/null and b/src/assets/hud/freeze_off.png differ
diff --git a/src/assets/hud/freeze_on-hover.png b/src/assets/hud/freeze_on-hover.png
new file mode 100644
index 0000000000000000000000000000000000000000..ce846be44344e1bfa5bb394bc36a1be25599ee67
Binary files /dev/null and b/src/assets/hud/freeze_on-hover.png differ
diff --git a/src/assets/hud/freeze_on.png b/src/assets/hud/freeze_on.png
new file mode 100644
index 0000000000000000000000000000000000000000..edf0da5b9e7f78a00af084cf0429e086289fba01
Binary files /dev/null and b/src/assets/hud/freeze_on.png differ
diff --git a/src/assets/hud/mute_off-hover.png b/src/assets/hud/mute_off-hover.png
new file mode 100644
index 0000000000000000000000000000000000000000..4e064fb57702c605d08211f836e94dcbbf135a89
Binary files /dev/null and b/src/assets/hud/mute_off-hover.png differ
diff --git a/src/assets/hud/mute_off.png b/src/assets/hud/mute_off.png
new file mode 100644
index 0000000000000000000000000000000000000000..9dcce25b132006b1de0228458692659cec6c93f3
Binary files /dev/null and b/src/assets/hud/mute_off.png differ
diff --git a/src/assets/hud/mute_on-hover.png b/src/assets/hud/mute_on-hover.png
new file mode 100644
index 0000000000000000000000000000000000000000..f823c06561115c5208cce101dde4fe3d203d4c18
Binary files /dev/null and b/src/assets/hud/mute_on-hover.png differ
diff --git a/src/assets/hud/mute_on.png b/src/assets/hud/mute_on.png
new file mode 100644
index 0000000000000000000000000000000000000000..e5862312db1c45889cf37613cf3a01dad0566615
Binary files /dev/null and b/src/assets/hud/mute_on.png differ
diff --git a/src/assets/hud/muted.png b/src/assets/hud/muted.png
deleted file mode 100644
index b557fd0195e53a1dc9b4873db65b69afb0b7d1b1..0000000000000000000000000000000000000000
Binary files a/src/assets/hud/muted.png and /dev/null differ
diff --git a/src/assets/hud/unmuted.png b/src/assets/hud/unmuted.png
deleted file mode 100755
index 71f1af21bdd72aaa22ebe740e84a481aa1dc8b3f..0000000000000000000000000000000000000000
Binary files a/src/assets/hud/unmuted.png and /dev/null differ
diff --git a/src/assets/stylesheets/2d-hud.css b/src/assets/stylesheets/2d-hud.css
deleted file mode 100644
index a50436114181d18997248dce77a3cc1d9500363f..0000000000000000000000000000000000000000
--- a/src/assets/stylesheets/2d-hud.css
+++ /dev/null
@@ -1,102 +0,0 @@
-:local(.container) {
-  position: absolute;
-  top: 10px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  height: 80px;
-  width: 100%;
-}
-
-:local(.panel) {
-  display: flex;
-  justify-content: space-around;
-  align-items: center;
-  padding: 10px;
-  background-color: rgba(0, 0, 0, 0.35);
-}
-
-:local(.panel.left) {
-  border-bottom-left-radius: 30px;
-  border-top-left-radius: 30px;
-  padding-left: 20px;
-  padding-right: 50px;
-  margin-right: -40px;
-}
-
-:local(.panel.right) {
-  border-bottom-right-radius: 30px;
-  border-top-right-radius: 30px;
-  padding-right: 20px;
-  padding-left: 50px;
-  margin-left: -40px;
-}
-
-:local(.modeButton) {
-  width: 80px;
-  height: 80px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  z-index: 10;
-}
-
-:local(.panel) {
-  display: flex;
-  justify-content: space-around;
-  align-items: center;
-  padding: 10px;
-  background-color: rgba(0, 0, 0, 0.35);
-}
-
-
-:local(.nametag) {
-  display: flex;
-  justify-content: center;
-  font-size: 32px;
-  font-family: sans-serif;
-  color: white;
-  margin: 0 10px 0 0;
-}
-
-:local(.avatar) {
-  width: 80px;
-  height: 80px;
-  background-size: 100%;
-  background-image: url(../hud/avatar.png);
-}
-
-:local(.mic) {
-  display: flex;
-  width: 32px;
-  height: 32px;
-  -webkit-mask: url(../hud/unmuted.png);
-  -webkit-mask-size: 32px;
-  mask: url(../hud/unmuted.png);
-  mask-size: 32px;
-  background-color: white;
-  cursor: pointer;
-}
-
-:local(.mic:hover) {
-  background-color: cyan;
-}
-
-:local(.mic.muted:hover) {
-  background-color: cyan;
-}
-
-:local(.mic:active) {
-  background-color: red;
-}
-
-:local(.mic.muted) {
-  -webkit-mask: url(../hud/muted.png);
-  -webkit-mask-size: 32px;
-  mask: url(../hud/muted.png);
-  mask-size: 32px;
-}
-
-:local(.mic.muted:active) {
-  background-color: green;
-}
diff --git a/src/assets/stylesheets/2d-hud.scss b/src/assets/stylesheets/2d-hud.scss
new file mode 100644
index 0000000000000000000000000000000000000000..2fe1736009876a36c2dd29d0c0e6de6c709220bf
--- /dev/null
+++ b/src/assets/stylesheets/2d-hud.scss
@@ -0,0 +1,85 @@
+:local(.container) {
+  position: absolute;
+  top: 10px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 80px;
+  width: 100%;
+}
+
+:local(.panel) {
+  display: flex;
+  justify-content: space-around;
+  align-items: center;
+  padding: 5px;
+  background-color: rgba(#4F4F4F, 0.45);
+}
+
+:local(.panel.left) {
+  border-bottom-left-radius: 30px;
+  border-top-left-radius: 30px;
+  padding-left: 5px;
+  padding-right: 45px;
+  margin-right: -40px;
+}
+
+:local(.panel.right) {
+  border-bottom-right-radius: 30px;
+  border-top-right-radius: 30px;
+  padding-right: 5px;
+  padding-left: 45px;
+  margin-left: -40px;
+}
+
+:local(.iconButton) {
+  width: 40px;
+  height: 40px;
+  background-size: 100%;
+  cursor: pointer;
+}
+
+:local(.iconButton.large) {
+  width: 80px;
+  height: 80px;
+  z-index: 10;
+}
+
+:local(.iconButton.mute) {
+  background-image: url(../hud/mute_off.png);
+}
+:local(.iconButton.mute:hover) {
+  background-image: url(../hud/mute_off-hover.png);
+}
+:local(.iconButton.mute.active) {
+  background-image: url(../hud/mute_on.png);
+}
+:local(.iconButton.mute.active:hover) {
+  background-image: url(../hud/mute_on-hover.png);
+}
+
+:local(.iconButton.bubble) {
+  background-image: url(../hud/bubble_off.png);
+}
+:local(.iconButton.bubble:hover) {
+  background-image: url(../hud/bubble_off-hover.png);
+}
+:local(.iconButton.bubble.active) {
+  background-image: url(../hud/bubble_on.png);
+}
+:local(.iconButton.bubble.active:hover) {
+  background-image: url(../hud/bubble_on-hover.png);
+}
+
+:local(.iconButton.freeze) {
+  background-image: url(../hud/freeze_off.png);
+}
+:local(.iconButton.freeze:hover) {
+  background-image: url(../hud/freeze_off-hover.png);
+}
+:local(.iconButton.freeze.active) {
+  background-image: url(../hud/freeze_on.png);
+}
+:local(.iconButton.freeze.active:hover) {
+  background-image: url(../hud/freeze_on-hover.png);
+}
\ No newline at end of file
diff --git a/src/assets/stylesheets/entry.scss b/src/assets/stylesheets/entry.scss
index abed31db312891d5d0d4c425b42310851df21553..7c7d18c840410378a86d9a4b41211777808a60b0 100644
--- a/src/assets/stylesheets/entry.scss
+++ b/src/assets/stylesheets/entry.scss
@@ -51,6 +51,7 @@
   background: none;
   color: white;
   border: none;
+  align-items: center;
   @extend %default-font;
 
   &__icon {
diff --git a/src/assets/stylesheets/index.scss b/src/assets/stylesheets/index.scss
index 77615e9d341f4ca5688ebe6f0e209163f2c884cf..38833657208e9a58a19ef8b10add9e8a5ceaa25d 100644
--- a/src/assets/stylesheets/index.scss
+++ b/src/assets/stylesheets/index.scss
@@ -34,6 +34,10 @@ body {
   display: flex;
   flex-direction: column;
   z-index: 2;
+
+  &--noninteractive {
+    pointer-events: none;
+  }
 }
 
 .background-video {
@@ -49,7 +53,7 @@ body {
 .header-content {
   padding: 1.5em 2.5em 1.5em 2.5em;
   background-color: rgba(0, 0, 0, 0.85);
-  min-height: 90px;
+  height: 90px;
   display: flex;
   border-bottom: 2px solid #242424;
 
@@ -217,3 +221,111 @@ body {
     }
   }
 }
+
+.overlay {
+  width: 100%;
+  height: 100%;
+  top: 0;
+  left: 0;
+  position: absolute;
+  pointer-events: none;
+  color: white;
+  z-index: 2;
+}
+
+.mailing-list-form {
+  display: flex;
+  height: 100%;
+  flex-direction: column;
+  justify-content: center;
+  text-align: center;
+  margin: 0;
+
+  &__first {
+    width: 100%;
+  }
+
+  &__email_field {
+    @extend %rounded-border;
+    @extend %default-font;
+    color: $light-text;
+    font-size: 1.2em;
+    background-color: transparent;
+    line-height: 2.0em;
+    padding-left: 1.25em;
+    padding-right: 1.25em;
+    margin: 0.5em 0;
+    width: 100%;
+  }
+
+  &__submit {
+    @extend %bottom-button;
+    border: 0;
+    margin-top: 16px;
+  }
+
+  &__privacy {
+    margin-top: 10px;
+    font-size: 0.7em;
+  }
+}
+
+.dialog {
+  display: grid;
+  grid-template-columns: 1fr 20px minmax(200px,500px) 20px 1fr;
+  grid-template-rows: 1fr 20px 275px 20px 1fr;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0,0,0,.6);
+
+  &__box {
+    grid-column: 3;
+    grid-row: 3;
+    position: relative;
+    pointer-events: auto;
+
+    &__contents {
+      background-color: rgba(0,0,0,0.8);
+      border-radius: 8px;
+      width: 100%;
+      height: 100%;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      text-align: center;
+      position: relative;
+
+      &__title {
+	@extend %top-title;
+	margin-top: 20px;
+      }
+
+      &__body {
+	margin: 40px;
+	font-size: 1.1em;
+	margin-top: 20px;
+	color: $grey-text;
+	display: flex;
+	flex-direction: column;
+
+	a { color: white }
+      }
+
+      &__close {
+	position: absolute;
+	left: 12px;
+	top: 6px;
+	color: white;
+	font-size: 1.4em;
+
+	background: none;
+	cursor: pointer;
+	border: none;
+      }
+    }
+  }
+}
+
+
+
diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json
index 1b9844c8abf2dc7d9ed7c089f84923faa601fd96..2ad5c1cf95deb131b7b8b4aa02add9f63c11cc7f 100644
--- a/src/assets/translations.data.json
+++ b/src/assets/translations.data.json
@@ -1,6 +1,5 @@
 {
-  "en":
-  {
+  "en": {
     "entry.screen-prefix": "Enter on ",
     "entry.desktop-screen": "Screen",
     "entry.mobile-screen": "Phone",
@@ -33,7 +32,7 @@
     "audio.granted-title": "Mic permissions granted",
     "audio.granted-subtitle": "You can still mute yourself in-game",
     "audio.granted-next": "NEXT",
-    "exit.subtitle": "Your session has ended.",
+    "exit.subtitle": "Your session has ended. Refresh your browser to start a new one.",
     "autoexit.title": "Auto-ending session in ",
     "autoexit.title_units": " seconds",
     "autoexit.subtitle": "You have started another session.",
@@ -45,12 +44,15 @@
     "home.webvr_disclaimer_post": " experiment by ",
     "home.webvr_disclaimer_mr_team": "Mozilla Mixed Reality",
     "home.view_source": "View Source",
-    "home.join_on_slack": "Join us on Slack",
+    "home.join_us": "Join the Conversation",
     "home.report_issue": "Report an Issue",
     "home.get_updates": "Get Updates",
     "home.hero_title": "A new way to get together online.",
     "home.hero_subtitle": "Laugh, play, get stuff done, or just hang out.",
-    "home.made_with_love": "made with ❤️ by ",
-    "home.environment_author_by": " by "
+    "home.made_with_love": "made with 🦆 by ",
+    "home.environment_author_by": " by ",
+    "home.dialog.close": "CLOSE",
+    "mailing_list.privacy_label": "I'm okay with Mozilla handling my info as explained in",
+    "mailing_list.privacy_link": "this Privacy Notice"
   }
 }
diff --git a/src/components/freeze-controller.js b/src/components/freeze-controller.js
new file mode 100644
index 0000000000000000000000000000000000000000..77e66f91f167d0f01ea4acac406c6f32eac83039
--- /dev/null
+++ b/src/components/freeze-controller.js
@@ -0,0 +1,27 @@
+AFRAME.registerComponent("freeze-controller", {
+  schema: {
+    toggleEvent: { type: "string" }
+  },
+
+  init: function() {
+    this.onToggle = this.onToggle.bind(this);
+  },
+
+  play: function() {
+    this.el.addEventListener(this.data.toggleEvent, this.onToggle);
+  },
+
+  pause: function() {
+    this.el.removeEventListener(this.data.toggleEvent, this.onToggle);
+  },
+
+  onToggle: function() {
+    window.APP.store.update({ profile: { has_found_freeze: true } });
+    NAF.connection.adapter.toggleFreeze();
+    if (NAF.connection.adapter.frozen) {
+      this.el.addState("frozen");
+    } else {
+      this.el.removeState("frozen");
+    }
+  }
+});
diff --git a/src/components/hud-controller.js b/src/components/hud-controller.js
index 4ee274f34ae9daa6f24c8e414eee824b845c49db..d2439b3765d0272ae5b113fa174b1738ab213c2b 100644
--- a/src/components/hud-controller.js
+++ b/src/components/hud-controller.js
@@ -15,7 +15,8 @@ AFRAME.registerComponent("hud-controller", {
     offset: { default: 0.7 }, // distance from hud above head,
     lookCutoff: { default: 20 }, // angle at which the hud should be "on",
     animRange: { default: 30 }, // degrees over which to animate the hud into view
-    yawCutoff: { default: 50 } // yaw degrees at wich the hud should reoirent even if the user is looking up
+    yawCutoff: { default: 50 }, // yaw degrees at wich the hud should reoirent even if the user is looking up
+    showTip: { type: "bool" }
   },
   init() {
     this.isYLocked = false;
@@ -33,14 +34,34 @@ AFRAME.registerComponent("hud-controller", {
     const head = this.data.head.object3D;
     const sceneEl = this.el.sceneEl;
 
-    const { offset, lookCutoff, animRange, yawCutoff } = this.data;
+    const { offset, lookCutoff, animRange, yawCutoff, showTip } = this.data;
 
     const pitch = head.rotation.x * THREE.Math.RAD2DEG;
     const yawDif = deltaAngle(head.rotation.y, hud.rotation.y) * THREE.Math.RAD2DEG;
 
-    // Reorient the hud only if the user is looking away from the hud, for right now this arbitrarily means the hud is 1/3 way animated away
+    // animate the hud into place over animRange degrees as the user aproaches the lookCutoff angle
+    let t = 1 - THREE.Math.clamp(lookCutoff - pitch, 0, animRange) / animRange;
+
+    // HUD is locked down while showing tooltip
+    if (showTip) {
+      t = 1;
+    }
+
+    // Once the HUD is in place it should stay in place until you look sufficiently far down
+    if (t === 1) {
+      this.lockedHeadPositionY = head.position.y;
+      this.hudLocked = true;
+    } else if (this.hudLocked && pitch < lookCutoff - animRange / 2) {
+      this.hudLocked = false;
+    }
+
+    if (this.hudLocked) {
+      t = 1;
+    }
+
+    // Reorient the hud only if the user is looking away from the hud, for right now this arbitrarily means the hud is 1/2 way animated away
     // TODO: come up with better huristics for this that maybe account for the user turning away from the hud "too far", also animate the position so that it doesnt just snap.
-    if (yawDif >= yawCutoff || pitch < lookCutoff - animRange / 3) {
+    if (yawDif >= yawCutoff || pitch < lookCutoff - animRange / 2) {
       const lookDir = new THREE.Vector3(0, 0, -1);
       lookDir.applyQuaternion(head.quaternion);
       lookDir.add(head.position);
@@ -48,18 +69,6 @@ AFRAME.registerComponent("hud-controller", {
       hud.position.z = lookDir.z;
       hud.setRotationFromEuler(new THREE.Euler(0, head.rotation.y, 0));
     }
-
-    // animate the hud into place over animRange degrees as the user aproaches the lookCutoff angle
-    const t = 1 - THREE.Math.clamp(lookCutoff - pitch, 0, animRange) / animRange;
-
-    // Lock the hud in place relative to a known head position so it doesn't bob up and down
-    // with the user's head
-    if (!this.isYLocked && t === 1) {
-      this.lockedHeadPositionY = head.position.y;
-    }
-    const EPSILON = 0.001;
-    this.isYLocked = t > 1 - EPSILON;
-
     hud.position.y = (this.isYLocked ? this.lockedHeadPositionY : head.position.y) + offset + (1 - t) * offset;
     hud.rotation.x = (1 - t) * THREE.Math.DEG2RAD * 90;
 
diff --git a/src/components/icon-button.js b/src/components/icon-button.js
new file mode 100644
index 0000000000000000000000000000000000000000..eab80803f4d85ad425a6d5a0c71fc8f6f9ddc333
--- /dev/null
+++ b/src/components/icon-button.js
@@ -0,0 +1,57 @@
+AFRAME.registerComponent("icon-button", {
+  schema: {
+    image: { type: "string" },
+    hoverImage: { type: "string" },
+    activeImage: { type: "string" },
+    activeHoverImage: { type: "string" },
+    active: { type: "boolean" },
+    haptic: { type: "selector" }
+  },
+
+  init() {
+    this.onHover = () => {
+      this.hovering = true;
+      this.updateButtonState();
+      this.emitHapticPulse();
+    };
+    this.onHoverOut = () => {
+      this.hovering = false;
+      this.updateButtonState();
+    };
+    this.onClick = () => {
+      this.emitHapticPulse();
+    };
+  },
+
+  emitHapticPulse() {
+    if (this.data.haptic) {
+      this.data.haptic.emit("haptic_pulse", { intensity: "low" });
+    }
+  },
+
+  play() {
+    this.updateButtonState();
+    this.el.addEventListener("mouseover", this.onHover);
+    this.el.addEventListener("mouseout", this.onHoverOut);
+    this.el.addEventListener("click", this.onClick);
+  },
+
+  pause() {
+    this.el.removeEventListener("mouseover", this.onHover);
+    this.el.removeEventListener("mouseout", this.onHoverOut);
+    this.el.removeEventListener("click", this.onClick);
+  },
+
+  update() {
+    this.updateButtonState();
+  },
+
+  updateButtonState() {
+    const hovering = this.hovering;
+    const active = this.data.active;
+
+    const image = active ? (hovering ? "activeHoverImage" : "activeImage") : hovering ? "hoverImage" : "image";
+
+    this.el.setAttribute("src", this.data[image]);
+  }
+});
diff --git a/src/components/in-world-hud.js b/src/components/in-world-hud.js
index 9e307e498256da344e5e2e3533baf82b09f93b6f..6b6c28bfc2cc97f1a3a776f9c4c12edd867457a3 100644
--- a/src/components/in-world-hud.js
+++ b/src/components/in-world-hud.js
@@ -5,99 +5,41 @@ AFRAME.registerComponent("in-world-hud", {
   },
   init() {
     this.mic = this.el.querySelector(".mic");
+    this.freeze = this.el.querySelector(".freeze");
 
-    const muted = this.el.sceneEl.is("muted");
-    this.mic.setAttribute("src", muted ? "#muted" : "#unmuted");
-
-    this.showCorrectMuteState = () => {
-      const muted = this.el.sceneEl.is("muted");
-      this.mic.setAttribute("src", muted ? "#muted" : "#unmuted");
+    this.updateButtonStates = () => {
+      this.mic.setAttribute("icon-button", "active", this.el.sceneEl.is("muted"));
+      this.freeze.setAttribute("icon-button", "active", this.el.sceneEl.is("frozen"));
     };
+    this.updateButtonStates();
 
     this.onStateChange = evt => {
-      if (evt.detail !== "muted") return;
-      this.showCorrectMuteState();
-    };
-
-    this.onMicHover = () => {
-      this.hoveredOnMic = true;
-      this.data.haptic.emit("haptic_pulse", { intensity: "low" });
-      this.mic.setAttribute("material", "color", "#1DD");
-    };
-
-    this.onMicHoverExit = () => {
-      this.hoveredOnMic = false;
-      this.mic.setAttribute("material", "color", "#FFF");
-      this.showCorrectMuteState();
+      if (!(evt.detail === "muted" || evt.detail === "frozen")) return;
+      this.updateButtonStates();
     };
 
-    this.onMicDown = () => {
-      this.data.haptic.emit("haptic_pulse", { intensity: "medium" });
-      this.el.sceneEl.removeEventListener("micAudio", this.onAudioFrequencyChange);
-      this.mic.setAttribute("material", "color", this.el.sceneEl.is("muted") ? "#0FA" : "#F33");
+    this.onMicClick = () => {
       this.el.emit("action_mute");
-      window.setTimeout(() => {
-        this.mic.setAttribute("material", "color", "#FFF");
-        this.el.sceneEl.addEventListener("micAudio", this.onAudioFrequencyChange);
-      }, 150);
-    };
-
-    this.onClick = () => {
-      if (this.hoveredOnMic) {
-        this.onMicDown();
-      }
     };
 
-    this.onAudioFrequencyChange = e => {
-      if (this.hoveredOnMic) return;
-      const red = 1.0 - e.detail.volume / 10.0;
-      this.mic.object3DMap.mesh.material.color = { r: red, g: 9, b: 9 };
+    this.onFreezeClick = () => {
+      this.el.emit("action_freeze");
     };
-
-    this.el.sceneEl.addEventListener("mediaStream", evt => {
-      this.ms = evt.detail.ms;
-      const ctx = THREE.AudioContext.getContext();
-      const source = ctx.createMediaStreamSource(this.ms);
-      this.analyser = ctx.createAnalyser();
-      this.levels = new Uint8Array(this.analyser.frequencyBinCount);
-      source.connect(this.analyser);
-    });
   },
 
   play() {
-    this.mic.addEventListener("raycaster-intersected", this.onMicHover);
-    this.mic.addEventListener("raycaster-intersected-cleared", this.onMicHoverExit);
-
     this.el.sceneEl.addEventListener("stateadded", this.onStateChange);
     this.el.sceneEl.addEventListener("stateremoved", this.onStateChange);
 
-    this.el.addEventListener("click", this.onClick);
-
-    this.el.sceneEl.addEventListener("micAudio", this.onAudioFrequencyChange);
+    this.mic.addEventListener("click", this.onMicClick);
+    this.freeze.addEventListener("click", this.onFreezeClick);
   },
 
   pause() {
     this.el.sceneEl.removeEventListener("stateadded", this.onStateChange);
     this.el.sceneEl.removeEventListener("stateremoved", this.onStateChange);
 
-    this.el.removeEventListener("click", this.onClick);
-
-    this.el.sceneEl.removeEventListener("micAudio", this.onAudioFrequencyChange);
-  },
-
-  tick: function() {
-    if (!this.analyser) return;
-
-    this.analyser.getByteFrequencyData(this.levels);
-
-    let sum = 0;
-    for (let i = 0; i < this.levels.length; i++) {
-      sum += this.levels[i];
-    }
-    this.volume = sum / this.levels.length;
-    this.el.emit("micAudio", {
-      volume: this.volume,
-      levels: this.levels
-    });
+    this.mic.removeEventListener("click", this.onMicClick);
+    this.freeze.removeEventListener("click", this.onFreezeClick);
   }
 });
diff --git a/src/components/stats-plus.css b/src/components/stats-plus.css
new file mode 100644
index 0000000000000000000000000000000000000000..fc4417bf506b5a5916c286b72aeaf86189790732
--- /dev/null
+++ b/src/components/stats-plus.css
@@ -0,0 +1,26 @@
+:global(.rs-header) {
+  display: flex;
+  justify-content: space-between;
+  border-bottom: 1px rgba(255,255,255,0.1) solid;
+  margin-bottom: 8px;
+}
+
+:global(.rs-collapse-btn) {
+  cursor: pointer;
+  font-size: 12px;
+}
+
+:global(.rs-fps-counter) {
+  cursor: pointer;
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  padding: 8px;
+  color: #aaa;
+  font-size: 10px;
+}
+
+:global(.rs-mobile) {
+  bottom: auto;
+  top: 0;
+}
\ No newline at end of file
diff --git a/src/components/stats-plus.js b/src/components/stats-plus.js
new file mode 100644
index 0000000000000000000000000000000000000000..c64100eeafa0e069a10ff2b005181a2dc446435e
--- /dev/null
+++ b/src/components/stats-plus.js
@@ -0,0 +1,130 @@
+import "./stats-plus.css";
+// Adapted from https://github.com/aframevr/aframe/blob/master/src/components/scene/stats.js
+
+function createStats(scene) {
+  const threeStats = new window.threeStats(scene.renderer);
+  const aframeStats = new window.aframeStats(scene);
+  const plugins = scene.isMobile ? [] : [threeStats, aframeStats];
+  return new window.rStats({
+    css: [], // Our stylesheet is injected from AFrame.
+    values: {
+      fps: { caption: "fps", below: 30 }
+    },
+    groups: [{ caption: "Framerate", values: ["fps", "raf"] }],
+    plugins: plugins
+  });
+}
+
+const HIDDEN_CLASS = "a-hidden";
+
+AFRAME.registerComponent("stats-plus", {
+  // Whether or not the stats panel is expanded.
+  // Shows FPS counter when collapsed.
+  schema: { default: false },
+  init() {
+    this.onExpand = this.onExpand.bind(this);
+    this.onCollapse = this.onCollapse.bind(this);
+    this.onEnterVr = this.onEnterVr.bind(this);
+    this.onExitVr = this.onExitVr.bind(this);
+
+    const scene = this.el.sceneEl;
+    this.stats = createStats(scene);
+    this.statsEl = document.querySelector(".rs-base");
+
+    // Add header to stats panel so we can collapse it
+    const statsHeaderEl = document.createElement("div");
+    statsHeaderEl.classList.add("rs-header");
+
+    const statsTitleEl = document.createElement("h1");
+    statsTitleEl.innerHTML = "Stats";
+    statsHeaderEl.appendChild(statsTitleEl);
+
+    const collapseEl = document.createElement("div");
+    collapseEl.classList.add("rs-collapse-btn");
+    collapseEl.innerHTML = "X";
+    collapseEl.addEventListener("click", this.onCollapse);
+    statsHeaderEl.appendChild(collapseEl);
+
+    this.statsEl.insertBefore(statsHeaderEl, this.statsEl.firstChild);
+
+    // Add fps counter to the page
+    this.fpsEl = document.createElement("div");
+    this.fpsEl.addEventListener("click", this.onExpand);
+    this.fpsEl.classList.add("rs-fps-counter");
+    document.body.appendChild(this.fpsEl);
+    this.lastFpsUpdate = performance.now();
+    this.frameCount = 0;
+
+    if (scene.isMobile) {
+      this.statsEl.classList.add("rs-mobile");
+      this.fpsEl.classList.add("rs-mobile");
+    }
+
+    scene.addEventListener("enter-vr", this.onEnterVr);
+    scene.addEventListener("exit-vr", this.onExitVr);
+  },
+  update(oldData) {
+    if (oldData !== this.data) {
+      if (this.data) {
+        this.statsEl.classList.remove(HIDDEN_CLASS);
+        this.fpsEl.classList.add(HIDDEN_CLASS);
+      } else {
+        this.statsEl.classList.add(HIDDEN_CLASS);
+        this.fpsEl.classList.remove(HIDDEN_CLASS);
+      }
+    }
+  },
+  tick() {
+    if (this.data) {
+      // Update rStats
+      const stats = this.stats;
+      stats("rAF").tick();
+      stats("FPS").frame();
+      stats().update();
+    } else {
+      // Update the fps counter
+      const now = performance.now();
+      this.frameCount++;
+
+      // Update the fps counter text once a second
+      if (now >= this.lastFpsUpdate + 1000) {
+        const fps = this.frameCount / ((now - this.lastFpsUpdate) / 1000);
+        this.fpsEl.innerHTML = Math.round(fps) + " FPS";
+        this.lastFpsUpdate = now;
+        this.frameCount = 0;
+      }
+    }
+  },
+  onEnterVr() {
+    // Hide all stats elements when entering VR on mobile
+    if (this.el.sceneEl.isMobile) {
+      this.statsEl.classList.add(HIDDEN_CLASS);
+      this.fpsEl.classList.add(HIDDEN_CLASS);
+    }
+  },
+  onExitVr() {
+    // Revert to previous state whe exiting VR on mobile
+    if (this.el.sceneEl.isMobile) {
+      if (this.data) {
+        this.statsEl.classList.remove(HIDDEN_CLASS);
+      } else {
+        this.fpsEl.classList.remove(HIDDEN_CLASS);
+      }
+    }
+  },
+  onExpand() {
+    this.el.setAttribute(this.name, true);
+  },
+  onCollapse() {
+    this.el.setAttribute(this.name, false);
+  },
+  remove() {
+    this.el.sceneEl.removeListener("enter-vr", this.hide);
+    this.el.sceneEl.removeListener("exit-vr", this.show);
+
+    if (this.statsEl) {
+      this.statsEl.parentNode.removeChild(this.statsEl);
+      this.fpsEl.parentNode.removeChild(this.fpsEl);
+    }
+  }
+});
diff --git a/src/hub.html b/src/hub.html
index e7ca7655ff31d81da8529db96cfecdf85f5c66a1..a80599f4fd9d6a125eed5c5bb33ab9094915de23 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -22,16 +22,26 @@
         networked-scene="adapter: janus; audio: true; debug: true; connectOnLoad: false;"
         physics
         mute-mic="eventSrc: a-scene; toggleEvents: action_mute"
+        freeze-controller="toggleEvent: action_freeze"
         personal-space-bubble="debug: false;"
 
         app-mode-input-mappings="modes: default, hud; actionSets: default, hud;"
         >
 
         <a-assets>
-            <img id="unmuted"  src="./assets/hud/unmuted.png" >
-            <img id="muted"  src="./assets/hud/muted.png" >
-            <img id="avatar"  src="./assets/hud/avatar.png" >
             <img id="tooltip"  src="./assets/hud/tooltip.png" >
+            <img id="mute-off"  src="./assets/hud/mute_off.png" >
+            <img id="mute-off-hover"  src="./assets/hud/mute_off-hover.png" >
+            <img id="mute-on"  src="./assets/hud/mute_on.png" >
+            <img id="mute-on-hover"  src="./assets/hud/mute_on-hover.png" >
+            <img id="bubble-off"  src="./assets/hud/bubble_off.png" >
+            <img id="bubble-off-hover"  src="./assets/hud/bubble_off-hover.png" >
+            <img id="bubble-on"  src="./assets/hud/bubble_on.png" >
+            <img id="bubble-on-hover"  src="./assets/hud/bubble_on-hover.png" >
+            <img id="freeze-off"  src="./assets/hud/freeze_off.png" >
+            <img id="freeze-off-hover"  src="./assets/hud/freeze_off-hover.png" >
+            <img id="freeze-on"  src="./assets/hud/freeze_on.png" >
+            <img id="freeze-on-hover"  src="./assets/hud/freeze_on-hover.png" >
 
             <a-asset-item id="botdefault" response-type="arraybuffer" src="https://asset-bundles-prod.reticulum.io/bots/BotDefault_Avatar-9f71f8ff22.gltf"></a-asset-item>
             <a-asset-item id="botbobo" response-type="arraybuffer" src="https://asset-bundles-prod.reticulum.io/bots/BotBobo_Avatar-f9740a010b.gltf"></a-asset-item>
@@ -183,10 +193,10 @@
                 vr-mode-toggle-playing__hud-controller
             >
                 <a-entity in-world-hud="haptic:#player-right-controller;raycaster:#player-right-controller;" rotation="30 0 0">
-                    <a-rounded height="0.13" width="0.6" color="#000000" position="-0.3 -0.065 0" radius="0.065" opacity="0.35" class="hud bg"></a-rounded>
-                    <a-image src="#unmuted" scale="0.1 0.1 0.1" position="-0.2 0 0.001" class="hud mic" material="alphaTest:0.1;"></a-image>
-                    <a-image src="#avatar" scale="0.2 0.2 0.2" position="0 0 0.001" class="hud avatar"></a-image>
-                    <a-image src="#unmuted" scale="0.1 0.1 0.1" position="0.2 0 0.001" class="hud mic" material="alphaTest:0.1;"></a-image>
+                    <a-rounded height="0.13" width="0.48" color="#000000" position="-0.24 -0.065 0" radius="0.065" opacity="0.35" class="hud bg"></a-rounded>
+                    <a-image icon-button="image: #mute-off; hoverImage: #mute-off-hover; activeImage: #mute-on; activeHoverImage: #mute-on-hover" scale="0.1 0.1 0.1" position="-0.17 0 0.001" class="hud mic" material="alphaTest:0.1;"></a-image>
+                    <a-image icon-button="image: #freeze-off; hoverImage: #freeze-off-hover; activeImage: #freeze-on; activeHoverImage: #freeze-on-hover" scale="0.2 0.2 0.2" position="0 0 0.001" class="hud freeze"></a-image>
+                    <a-image icon-button="image: #bubble-off; hoverImage: #bubble-off-hover; activeImage: #bubble-on; activeHoverImage: #bubble-on-hover" scale="0.1 0.1 0.1" position="0.17 0 0.001" class="hud mic" material="alphaTest:0.1;"></a-image>
                 </a-entity>
             </a-entity>
 
diff --git a/src/hub.js b/src/hub.js
index 0489cb38e9afe45a50547030d2eba8eeacb1856e..f04c5ae7db83c8396186f1017202b5b4704c5167 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -50,6 +50,9 @@ import "./components/hand-poses";
 import "./components/gltf-model-plus";
 import "./components/gltf-bundle";
 import "./components/hud-controller";
+import "./components/freeze-controller";
+import "./components/icon-button";
+import "./components/stats-plus";
 
 import ReactDOM from "react-dom";
 import React from "react";
@@ -58,12 +61,14 @@ import HubChannel from "./utils/hub-channel";
 
 import "./systems/personal-space-bubble";
 import "./systems/app-mode";
+import "./systems/exit-on-blur";
 
 import "./gltf-component-mappings";
 
 import { App } from "./App";
 
 window.APP = new App();
+const store = window.APP.store;
 
 const qs = queryString.parse(location.search);
 const isMobile = AFRAME.utils.device.isMobile();
@@ -89,7 +94,6 @@ import "./components/nav-mesh-helper";
 import registerNetworkSchemas from "./network-schemas";
 import { inGameActions, config as inputConfig } from "./input-mappings";
 import registerTelemetry from "./telemetry";
-import Store from "./storage/store";
 
 import { generateDefaultProfile, generateRandomName } from "./utils/identity.js";
 import { getAvailableVREntryTypes } from "./utils/vr-caps-detect.js";
@@ -109,7 +113,6 @@ AFRAME.registerInputActivator("pressedmove", PressedMove);
 AFRAME.registerInputActivator("reverseY", ReverseY);
 AFRAME.registerInputMappings(inputConfig, true);
 
-const store = new Store();
 const concurrentLoadDetector = new ConcurrentLoadDetector();
 const hubChannel = new HubChannel(store);
 
@@ -124,6 +127,9 @@ if (!store.state.profile.has_changed_name) {
 }
 
 async function exitScene() {
+  if (NAF.connection.adapter && NAF.connection.adapter.localMediaStream) {
+    NAF.connection.adapter.localMediaStream.getTracks().forEach(t => t.stop());
+  }
   hubChannel.disconnect();
   const scene = document.querySelector("a-scene");
   scene.renderer.animate(null); // Stop animation loop, TODO A-Frame should do this
@@ -136,6 +142,8 @@ function applyProfileFromStore(playerRig) {
     displayName,
     avatarSrc: "#" + (store.state.profile.avatar_id || "botdefault")
   });
+  const hudController = playerRig.querySelector("[hud-controller]");
+  hudController.setAttribute("hud-controller", { showTip: !store.state.profile.has_found_freeze });
   document.querySelector("a-scene").emit("username-changed", { username: displayName });
 }
 
@@ -145,6 +153,8 @@ async function enterScene(mediaStream, enterInVR, janusRoomId) {
   document.querySelector("a-scene canvas").classList.remove("blurred");
   scene.render();
 
+  scene.setAttribute("stats-plus", false);
+
   if (enterInVR) {
     scene.enterVR();
   }
@@ -158,10 +168,6 @@ async function enterScene(mediaStream, enterInVR, janusRoomId) {
     serverURL: process.env.JANUS_SERVER
   });
 
-  if (!qsTruthy("no_stats")) {
-    scene.setAttribute("stats", true);
-  }
-
   if (isMobile || qsTruthy("mobile")) {
     playerRig.setAttribute("virtual-gamepad-controls", {});
   }
diff --git a/src/react-components/2d-hud.js b/src/react-components/2d-hud.js
index 9679bb9c090af49966c6f5cbf03072201c860f00..d02d6deb206275df0a2389d31a6b9354c035de4f 100644
--- a/src/react-components/2d-hud.js
+++ b/src/react-components/2d-hud.js
@@ -2,25 +2,28 @@ import React from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
 
-import styles from "../assets/stylesheets/2d-hud.css";
+import styles from "../assets/stylesheets/2d-hud.scss";
 
-const TwoDHUD = ({ muted, onToggleMute }) => (
+const TwoDHUD = ({ muted, frozen, onToggleMute, onToggleFreeze }) => (
   <div className={styles.container}>
     <div className={cx("ui-interactive", styles.panel, styles.left)}>
-      <div className={cx(styles.mic, { [styles.muted]: muted })} onClick={onToggleMute} />
-    </div>
-    <div className={cx("ui-interactive", styles.modeButton)}>
-      <div className={styles.avatar} />
+      <div className={cx(styles.iconButton, styles.mute, { [styles.active]: muted })} onClick={onToggleMute} />
     </div>
+    <div
+      className={cx("ui-interactive", styles.iconButton, styles.large, styles.freeze, { [styles.active]: frozen })}
+      onClick={onToggleFreeze}
+    />
     <div className={cx("ui-interactive", styles.panel, styles.right)}>
-      <div className={cx(styles.mic, { [styles.muted]: muted })} onClick={onToggleMute} />
+      <div className={cx(styles.iconButton, styles.bubble, { [styles.active]: muted })} onClick={onToggleMute} />
     </div>
   </div>
 );
 
 TwoDHUD.propTypes = {
   muted: PropTypes.bool,
-  onToggleMute: PropTypes.func
+  frozen: PropTypes.bool,
+  onToggleMute: PropTypes.func,
+  onToggleFreeze: PropTypes.func
 };
 
 export default TwoDHUD;
diff --git a/src/react-components/avatar-selector.js b/src/react-components/avatar-selector.js
index 528d5b81558e37f12aea6a0182c4cc08d8971782..6ae6ab6ac91110ae8cc65b9d5b5f2db8d075f911 100644
--- a/src/react-components/avatar-selector.js
+++ b/src/react-components/avatar-selector.js
@@ -39,7 +39,7 @@ class AvatarSelector extends Component {
       // so we need to force it here.
       const currRot = this.animation.parentNode.getAttribute("rotation");
       const currY = currRot.y;
-      const toRot = String.split(this.animation.attributes.to.value, " ");
+      const toRot = this.animation.getAttribute("to").split(" ");
       const toY = toRot[1];
       const step = 360.0 / this.props.avatars.length;
       const brokenlyBigRotation = Math.abs(toY - currY) > 3 * step;
diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js
index af896b643db9e3cf38ac5403aa58e143f28c58ce..d91dfdbed6e726a785cbb8744d35e3dab066085c 100644
--- a/src/react-components/home-root.js
+++ b/src/react-components/home-root.js
@@ -3,6 +3,8 @@ import PropTypes from "prop-types";
 import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl";
 import en from "react-intl/locale-data/en";
 import homeVideo from "../assets/video/home.webm";
+import classNames from "classnames";
+import formurlencoded from "form-urlencoded";
 
 import HubCreatePanel from "./hub-create-panel.js";
 
@@ -28,7 +30,10 @@ class HomeRoot extends Component {
   };
 
   state = {
-    environments: []
+    environments: [],
+    dialogType: null,
+    mailingListEmail: "",
+    mailingListPrivacy: false
   };
 
   componentDidMount() {
@@ -36,6 +41,40 @@ class HomeRoot extends Component {
     document.querySelector("#background-video").playbackRate = 0.5;
   }
 
+  showDialog = dialogType => {
+    return e => {
+      e.preventDefault();
+      e.stopPropagation();
+      this.setState({ dialogType });
+    };
+  };
+
+  closeDialog = () => {
+    this.setState({ dialogType: null });
+  };
+
+  signUpForMailingList = async e => {
+    e.preventDefault();
+    e.stopPropagation();
+    if (!this.state.mailingListPrivacy) return;
+
+    const url = "https://www.mozilla.org/en-US/newsletter/";
+
+    const payload = {
+      email: this.state.mailingListEmail,
+      newsletters: "mixed-reality",
+      privacy: true,
+      fmt: "H",
+      source_url: document.location.href
+    };
+
+    await fetch(url, {
+      body: formurlencoded(payload),
+      method: "POST",
+      headers: { "content-type": "application/x-www-form-urlencoded" }
+    }).then(() => this.setState({ dialogType: "email_submitted" }));
+  };
+
   loadEnvironments = () => {
     const environments = [];
 
@@ -52,10 +91,98 @@ class HomeRoot extends Component {
   };
 
   render() {
+    let dialogTitle = null;
+    let dialogBody = null;
+
+    switch (this.state.dialogType) {
+      // TODO i18n, FormattedMessage doesn't play nicely with links
+      case "slack":
+        dialogTitle = "Get in Touch";
+        dialogBody = (
+          <span>
+            Want to join the conversation?
+            <p />
+            Join us on the{" "}
+            <a href="https://webvr-slack.herokuapp.com/" target="_blank" rel="noopener noreferrer">
+              WebVR Slack
+            </a>{" "}
+            in the #social channel.<br />VR meetups every Friday at noon PST!
+            <p /> Or, tweet at{" "}
+            <a href="https://twitter.com/mozillareality" target="_blank" rel="noopener noreferrer">
+              @mozillareality
+            </a>{" "}
+            on Twitter.
+          </span>
+        );
+        break;
+      case "email_submitted":
+        dialogTitle = "";
+        dialogBody = "Great! Please check your e-mail to confirm your subscription.";
+        break;
+      case "updates":
+        dialogTitle = "";
+        dialogBody = (
+          <span>
+            Sign up to get release notes about new features.
+            <p />
+            <form onSubmit={this.signUpForMailingList}>
+              <div className="mailing-list-form">
+                <input
+                  type="email"
+                  value={this.state.mailingListEmail}
+                  onChange={e => this.setState({ mailingListEmail: e.target.value })}
+                  className="mailing-list-form__email_field"
+                  required
+                  placeholder="Your email here"
+                />
+                <label className="mailing-list-form__privacy">
+                  <input
+                    className="mailing-list-form__privacy_checkbox"
+                    type="checkbox"
+                    required
+                    value={this.state.mailingListPrivacy}
+                    onChange={e => this.setState({ mailingListPrivacy: e.target.checked })}
+                  />
+                  <span className="mailing-list-form__privacy_label">
+                    <FormattedMessage id="mailing_list.privacy_label" />{" "}
+                    <a target="_blank" rel="noopener noreferrer" href="https://www.mozilla.org/en-US/privacy/">
+                      <FormattedMessage id="mailing_list.privacy_link" />
+                    </a>
+                  </span>
+                </label>
+                <input className="mailing-list-form__submit" type="submit" value="Sign Up Now" />
+              </div>
+            </form>
+          </span>
+        );
+        break;
+      case "report":
+        dialogTitle = "Report an Issue";
+        dialogBody = (
+          <span>
+            Need to report a problem?
+            <p />
+            You can file a{" "}
+            <a href="https://github.com/mozilla/mr-social-client/issues" target="_blank" rel="noopener noreferrer">
+              Github Issue
+            </a>{" "}
+            or e-mail us for support at <a href="mailto:hubs@mozilla.com">hubs@mozilla.com</a>.
+            <p />
+            You can also find us in #social on the{" "}
+            <a href="http://webvr-slack.herokuapp.com/" target="_blank" rel="noopener noreferrer">
+              WebVR Slack
+            </a>.
+          </span>
+        );
+        break;
+    }
+
+    const mainContentClassNames = classNames({ "main-content": true, "main-content--noninteractive": !!dialogTitle });
+
     return (
       <IntlProvider locale={lang} messages={messages}>
         <div className="home">
-          <div className="main-content">
+          <div className={mainContentClassNames}>
             <div className="header-content">
               <div className="header-content__title">
                 <img className="header-content__title__name" src="../assets/images/logo.svg" />
@@ -110,13 +237,28 @@ class HomeRoot extends Component {
             <div className="footer-content">
               <div className="footer-content__links">
                 <div className="footer-content__links__top">
-                  <a className="footer-content__links__link" rel="noopener noreferrer" target="_blank" href="#">
-                    <FormattedMessage id="home.join_on_slack" />
+                  <a
+                    className="footer-content__links__link"
+                    rel="noopener noreferrer"
+                    href="#"
+                    onClick={this.showDialog("slack")}
+                  >
+                    <FormattedMessage id="home.join_us" />
                   </a>
-                  <a className="footer-content__links__link" rel="noopener noreferrer" target="_blank" href="#">
+                  <a
+                    className="footer-content__links__link"
+                    rel="noopener noreferrer"
+                    href="#"
+                    onClick={this.showDialog("updates")}
+                  >
                     <FormattedMessage id="home.get_updates" />
                   </a>
-                  <a className="footer-content__links__link" rel="noopener noreferrer" target="_blank" href="#">
+                  <a
+                    className="footer-content__links__link"
+                    rel="noopener noreferrer"
+                    href="#"
+                    onClick={this.showDialog("report")}
+                  >
                     <FormattedMessage id="home.report_issue" />
                   </a>
                 </div>
@@ -130,6 +272,22 @@ class HomeRoot extends Component {
           <video playsInline autoPlay muted loop className="background-video" id="background-video">
             <source src={homeVideo} type="video/webm" />
           </video>
+          {this.state.dialogType && (
+            <div className="overlay">
+              <div className="dialog">
+                <div className="dialog__box">
+                  <div className="dialog__box__contents">
+                    <button className="dialog__box__contents__close" onClick={this.closeDialog}>
+                      <span>🗙</span>
+                    </button>
+                    <div className="dialog__box__contents__title">{dialogTitle}</div>
+                    <div className="dialog__box__contents__body">{dialogBody}</div>
+                    <div className="dialog__box__contents__button-container" />
+                  </div>
+                </div>
+              </div>
+            </div>
+          )}
         </div>
       </IntlProvider>
     );
diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js
index 3e22208dacc6219b59cc9276562c71d43f129f03..7ff3d25a675e484879619f00a26427b90592e26e 100644
--- a/src/react-components/ui-root.js
+++ b/src/react-components/ui-root.js
@@ -90,6 +90,9 @@ class UIRoot extends Component {
     autoExitTimerInterval: null,
     secondsRemainingBeforeAutoExit: Infinity,
 
+    muted: false,
+    frozen: false,
+
     exited: false,
 
     showProfileEntry: false
@@ -107,6 +110,12 @@ class UIRoot extends Component {
     this.props.scene.addEventListener("loaded", this.onSceneLoaded);
     this.props.scene.addEventListener("stateadded", this.onAframeStateChanged);
     this.props.scene.addEventListener("stateremoved", this.onAframeStateChanged);
+    this.props.scene.addEventListener("exit", this.exit);
+  }
+
+  componentWillUnmount() {
+    this.props.scene.removeEventListener("loaded", this.onSceneLoaded);
+    this.props.scene.removeEventListener("exit", this.exit);
   }
 
   componentDidUpdate(prevProps) {
@@ -121,9 +130,9 @@ class UIRoot extends Component {
 
   // TODO: mute state should probably actually just live in react land
   onAframeStateChanged = e => {
-    if (e.detail !== "muted") return;
+    if (!(e.detail === "muted" || e.detail === "frozen")) return;
     this.setState({
-      muted: this.props.scene.is("muted")
+      [e.detail]: this.props.scene.is(e.detail)
     });
   };
 
@@ -131,6 +140,10 @@ class UIRoot extends Component {
     this.props.scene.emit("action_mute");
   };
 
+  toggleFreeze = () => {
+    this.props.scene.emit("action_freeze");
+  };
+
   handleForcedVREntryType = () => {
     if (!this.props.forcedVREntryType) return;
 
@@ -758,7 +771,12 @@ class UIRoot extends Component {
             )}
           </div>
           {this.state.entryStep === ENTRY_STEPS.finished ? (
-            <TwoDHUD muted={this.state.muted} onToggleMute={this.toggleMute} />
+            <TwoDHUD
+              muted={this.state.muted}
+              frozen={this.state.frozen}
+              onToggleMute={this.toggleMute}
+              onToggleFreeze={this.toggleFreeze}
+            />
           ) : null}
         </div>
       </IntlProvider>
diff --git a/src/storage/store.js b/src/storage/store.js
index 4351ebeda99e9f9665fcbbf395858296cd2e56a4..b117e1849bf6a20cc28d770c7ef66112bb8888ba 100644
--- a/src/storage/store.js
+++ b/src/storage/store.js
@@ -19,6 +19,7 @@ export const SCHEMA = {
       properties: {
         has_agreed_to_terms: { type: "boolean" },
         has_changed_name: { type: "boolean" },
+        has_found_freeze: { type: "boolean" },
         display_name: { type: "string", pattern: "^[A-Za-z0-9-]{3,32}$" },
         avatar_id: { type: "string" }
       }
diff --git a/src/systems/exit-on-blur.js b/src/systems/exit-on-blur.js
new file mode 100644
index 0000000000000000000000000000000000000000..e6263a101f5a2fd1f71cd4b122a12431104e2457
--- /dev/null
+++ b/src/systems/exit-on-blur.js
@@ -0,0 +1,30 @@
+AFRAME.registerSystem("exit-on-blur", {
+  init() {
+    this.onBlur = this.onBlur.bind(this);
+    this.onFocus = this.onFocus.bind(this);
+
+    window.addEventListener("blur", this.onBlur);
+    window.addEventListener("focus", this.onFocus);
+
+    this.exitTimeout = null;
+  },
+
+  onBlur() {
+    if (this.el.isMobile) {
+      this.exitTimeout = setTimeout(() => {
+        this.el.dispatchEvent(new CustomEvent("exit"));
+      }, 10 * 1000);
+    }
+  },
+
+  onFocus() {
+    if (this.el.isMobile) {
+      clearTimeout(this.exitTimeout);
+    }
+  },
+
+  remove() {
+    clearTimeout(this.exitTimeout);
+    window.removeEventListener("blur", this.onBlur);
+  }
+});
diff --git a/src/utils/identity.js b/src/utils/identity.js
index db78b027e3e851aa254532f264438e362062576c..4cce7fa3729e376be6d28d57eb3120715bceee8b 100644
--- a/src/utils/identity.js
+++ b/src/utils/identity.js
@@ -103,6 +103,7 @@ export function generateDefaultProfile() {
   return {
     has_agreed_to_terms: false,
     has_changed_name: false,
+    has_found_freeze: false,
     avatar_id: selectRandom(avatarIds)
   };
 }
diff --git a/yarn.lock b/yarn.lock
index 4391edbc296f19f7af084c959e516b67caed2123..15edf71c1aed3e12ac16b63174f0cb2934c8a9d7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -118,7 +118,7 @@ accepts@1.3.3:
     mime-types "~2.1.11"
     negotiator "0.6.1"
 
-accepts@~1.3.4, accepts@~1.3.5:
+accepts@~1.3.4:
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2"
   dependencies:
@@ -2011,18 +2011,14 @@ colormin@^1.0.5:
     css-color-names "0.0.4"
     has "^1.0.1"
 
-colors@*:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.1.tgz#f4a3d302976aaf042356ba1ade3b1a2c62d9d794"
+colors@*, colors@^1.1.2, colors@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
 
 colors@1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
 
-colors@^1.1.2, colors@~1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
-
 combine-source-map@~0.7.1:
   version "0.7.2"
   resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.7.2.tgz#0870312856b307a87cc4ac486f3a9a62aeccc09e"
@@ -2840,17 +2836,7 @@ error@^7.0.2:
     string-template "~0.2.1"
     xtend "~4.0.0"
 
-es-abstract@^1.5.1:
-  version "1.11.0"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.11.0.tgz#cce87d518f0496893b1a30cd8461835535480681"
-  dependencies:
-    es-to-primitive "^1.1.1"
-    function-bind "^1.1.1"
-    has "^1.0.1"
-    is-callable "^1.1.3"
-    is-regex "^1.0.4"
-
-es-abstract@^1.7.0:
+es-abstract@^1.5.1, es-abstract@^1.7.0:
   version "1.10.0"
   resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864"
   dependencies:
@@ -3080,42 +3066,7 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2:
   dependencies:
     homedir-polyfill "^1.0.1"
 
-express@^4.10.7:
-  version "4.16.3"
-  resolved "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53"
-  dependencies:
-    accepts "~1.3.5"
-    array-flatten "1.1.1"
-    body-parser "1.18.2"
-    content-disposition "0.5.2"
-    content-type "~1.0.4"
-    cookie "0.3.1"
-    cookie-signature "1.0.6"
-    debug "2.6.9"
-    depd "~1.1.2"
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    etag "~1.8.1"
-    finalhandler "1.1.1"
-    fresh "0.5.2"
-    merge-descriptors "1.0.1"
-    methods "~1.1.2"
-    on-finished "~2.3.0"
-    parseurl "~1.3.2"
-    path-to-regexp "0.1.7"
-    proxy-addr "~2.0.3"
-    qs "6.5.1"
-    range-parser "~1.2.0"
-    safe-buffer "5.1.1"
-    send "0.16.2"
-    serve-static "1.13.2"
-    setprototypeof "1.1.0"
-    statuses "~1.4.0"
-    type-is "~1.6.16"
-    utils-merge "1.0.1"
-    vary "~1.1.2"
-
-express@^4.16.2:
+express@^4.10.7, express@^4.16.2:
   version "4.16.2"
   resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c"
   dependencies:
@@ -3321,18 +3272,6 @@ finalhandler@1.1.0:
     statuses "~1.3.1"
     unpipe "~1.0.0"
 
-finalhandler@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105"
-  dependencies:
-    debug "2.6.9"
-    encodeurl "~1.0.2"
-    escape-html "~1.0.3"
-    on-finished "~2.3.0"
-    parseurl "~1.3.2"
-    statuses "~1.4.0"
-    unpipe "~1.0.0"
-
 find-cache-dir@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f"
@@ -3420,6 +3359,10 @@ form-data@~2.1.1:
     combined-stream "^1.0.5"
     mime-types "^2.1.12"
 
+form-urlencoded@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/form-urlencoded/-/form-urlencoded-2.0.4.tgz#dbcd590a49ae35d5e9516bbba8567242d0291fe5"
+
 forwarded@~0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@@ -5186,10 +5129,6 @@ minijanus@^0.5.0:
   version "0.5.0"
   resolved "https://registry.yarnpkg.com/minijanus/-/minijanus-0.5.0.tgz#78e1429bb5d83cb3957a538335d2ae901bf614fa"
 
-"minijanus@https://github.com/mozilla/minijanus.js#master":
-  version "0.5.0"
-  resolved "https://github.com/mozilla/minijanus.js#497f4dd80fdb92e247238e638daed14ae6623575"
-
 minimalistic-assert@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
@@ -5373,12 +5312,12 @@ mute-stream@0.0.7:
   version "0.0.7"
   resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
 
-"naf-janus-adapter@https://github.com/mozilla/naf-janus-adapter#feature/disconnect":
-  version "0.4.1"
-  resolved "https://github.com/mozilla/naf-janus-adapter#4a4532014d6489403cf7e451790925ce747f8e41"
+naf-janus-adapter@^0.5.2:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/naf-janus-adapter/-/naf-janus-adapter-0.5.2.tgz#f4a9522c4e0b38fcbfe7c71b668afed67d5e133e"
   dependencies:
     debug "^3.1.0"
-    minijanus "https://github.com/mozilla/minijanus.js#master"
+    minijanus "^0.5.0"
 
 nan@^2.3.0, nan@^2.3.2:
   version "2.9.1"
@@ -6399,7 +6338,7 @@ prop-types@^15.5.4, prop-types@^15.6.0:
     loose-envify "^1.3.1"
     object-assign "^4.1.1"
 
-proxy-addr@~2.0.2, proxy-addr@~2.0.3:
+proxy-addr@~2.0.2:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.3.tgz#355f262505a621646b3130a728eb647e22055341"
   dependencies:
@@ -7167,7 +7106,7 @@ serve-static@1.13.1:
     parseurl "~1.3.2"
     send "0.16.1"
 
-serve-static@1.13.2, serve-static@^1.10.0, serve-static@^1.8.0:
+serve-static@^1.10.0, serve-static@^1.8.0:
   version "1.13.2"
   resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1"
   dependencies:
@@ -7979,7 +7918,7 @@ type-check@~0.3.2:
   dependencies:
     prelude-ls "~1.1.2"
 
-type-is@~1.6.15, type-is@~1.6.16:
+type-is@~1.6.15:
   version "1.6.16"
   resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"
   dependencies: