diff --git a/PRIVACY.md b/PRIVACY.md
index 4b0acc3e1d7f94088bd39eed00089221475e5c67..89afb36c09e8bed26f517e67e91ffc6ff4ef8e6b 100644
--- a/PRIVACY.md
+++ b/PRIVACY.md
@@ -19,8 +19,6 @@ In this Privacy Notice, we explain what data may be accessible to Mozilla or oth
 - You can learn more by looking at the code itself. [Janus SFU](https://github.com/mozilla/janus-plugin-sfu), [Reticulum](https://github.com/mozilla/reticulum), [Hubs](https://github.com/mozilla/hubs), [Hubs-Ops](https://github.com/mozilla/hubs-ops)
 </details>
 
-<p/>
-
 <details open>
   <summary>
     <strong>Mozilla receives technical and interaction data to improve performance and stability.</strong>
diff --git a/package.json b/package.json
index 16cb3510f746fba6b10af1bff7ca8dcc65f22709..f51fa7aed32571f8297cd66b3ab61a66f6cec781 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,7 @@
     "aframe-extras": "https://github.com/MozillaReality/aframe-extras#feature/precompute-nav-mesh",
     "aframe-input-mapping-component": "https://github.com/mozillareality/aframe-input-mapping-component#hubs/master",
     "aframe-physics-extras": "^0.1.3",
-    "aframe-physics-system": "^3.1.1",
+    "aframe-physics-system": "github:infinitelee/aframe-physics-system#hubs/master",
     "aframe-rounded": "^1.0.3",
     "aframe-slice9-component": "^1.0.0",
     "aframe-teleport-controls": "https://github.com/mozillareality/aframe-teleport-controls#hubs/master",
diff --git a/scripts/default.env b/scripts/default.env
index 3d6556c0fa826b118d94dc8bc88a9f2555c5a520..72a793ee38019250e54724aaf8dddb14d457dbd2 100644
--- a/scripts/default.env
+++ b/scripts/default.env
@@ -1,7 +1,7 @@
 # This origin trial token is used to enable WebVR and Gamepad Extensions on Chrome 62+
 # You can find more information about getting your own origin trial token here: https://github.com/GoogleChrome/OriginTrials/blob/gh-pages/developer-guide.md
 ORIGIN_TRIAL_TOKEN="AgN/JtqSF6qpD3OZk8KgM5/UYqUUrwc166cOQSRCqvU+TIpHWdiwBUWH5V1K/jJkdtBrO4Q5I0XSGm16uB/Y4QQAAABVeyJvcmlnaW4iOiJodHRwczovL2h1YnMubW96aWxsYS5jb206NDQzIiwiZmVhdHVyZSI6IldlYlZSMS4xTTYyIiwiZXhwaXJ5IjoxNTI4MjQ1ODI1fQ=="
-ORIGIN_TRIAL_EXPIRES="2018-05-15"
+ORIGIN_TRIAL_EXPIRES="2018-06-05"
 JANUS_SERVER="wss://prod-janus.reticulum.io"
 DEV_RETICULUM_SERVER="dev.reticulum.io"
 ASSET_BUNDLE_SERVER="https://asset-bundles-prod.reticulum.io"
diff --git a/src/assets/stylesheets/index.scss b/src/assets/stylesheets/index.scss
index 7aa0079ce7bf648480fe507525fff489cc5b3d46..014ca0a7a7f7f32e20db86d9a4725da7beb5d011 100644
--- a/src/assets/stylesheets/index.scss
+++ b/src/assets/stylesheets/index.scss
@@ -45,7 +45,7 @@ body {
   position: fixed;
   top: 0;
   left: 0;
-  opacity: 0.66;
+  opacity: 0.45;
   min-width: 100%;
   min-height: 100%;
   z-index: 1;
diff --git a/src/assets/stylesheets/info-dialog.scss b/src/assets/stylesheets/info-dialog.scss
index ae46af24e2d332f746c67b498e85bc7633af51a0..47b06a43623b109a9fa27060d83f7da44889e59c 100644
--- a/src/assets/stylesheets/info-dialog.scss
+++ b/src/assets/stylesheets/info-dialog.scss
@@ -3,8 +3,7 @@
   height: 100%;
   top: 0;
   left: 0;
-  position: absolute;
-  pointer-events: none;
+  position: fixed;
   color: white;
   z-index: 2;
 }
@@ -54,6 +53,17 @@
 	a { color: white }
       }
 
+      &__links {
+        display: flex;
+        justify-content: center;
+        margin: 16px 0;
+        a {
+          margin: 0 12px;
+          color: $light-text;
+        }
+      }
+
+
       &__close {
 	position: absolute;
 	left: 12px;
diff --git a/src/components/character-controller.js b/src/components/character-controller.js
index 26dc12a8cd1e36eefe4d4ab65faee4c559576f09..174148d9c0109bde06e52f4cd68a89c798331208 100644
--- a/src/components/character-controller.js
+++ b/src/components/character-controller.js
@@ -75,7 +75,7 @@ AFRAME.registerComponent("character-controller", {
   },
 
   handleTeleport: function(event) {
-    this.setPositionOnNavMesh(event.detail.oldPosition, this.el.object3D);
+    this.setPositionOnNavMesh(event.detail.oldPosition, event.detail.newPosition, this.el.object3D, true);
   },
 
   tick: (function() {
@@ -142,19 +142,23 @@ AFRAME.registerComponent("character-controller", {
       this.pendingSnapRotationMatrix.identity(); // Revert to identity
 
       if (this.velocity.lengthSq() > EPS) {
-        this.setPositionOnNavMesh(startPos, root);
+        this.setPositionOnNavMesh(startPos, root.position, root);
       }
     };
   })(),
 
-  setPositionOnNavMesh: function(position, object3D) {
+  setPositionOnNavMesh: function(startPosition, endPosition, object3D, resetPosition = false) {
     const nav = this.el.sceneEl.systems.nav;
     if (nav.navMesh) {
-      if (!this.navGroup) {
-        this.navGroup = nav.getGroup(position);
+      if (!this.navGroup || resetPosition) {
+        this.navGroup = nav.getGroup(endPosition);
       }
-      this.navNode = this.navNode || nav.getNode(position, this.navGroup);
-      this.navNode = nav.clampStep(position, object3D.position, this.navGroup, this.navNode, object3D.position);
+
+      if (!this.navNode || resetPosition) {
+        this.navNode = nav.getNode(endPosition, this.navGroup) || this.navNode;
+      }
+
+      this.navNode = nav.clampStep(startPosition, endPosition, this.navGroup, this.navNode, object3D.position);
     }
   },
 
diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js
index 7b2c16f77ee185d432581bea38806558e7448dce..debcd20ca005437c5edaf68ac4cf2f2a577b671f 100644
--- a/src/components/cursor-controller.js
+++ b/src/components/cursor-controller.js
@@ -42,26 +42,22 @@ AFRAME.registerComponent("cursor-controller", {
 
     this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered });
 
-    const functionNames = [
-      "_handlePointerDown",
-      "_handlePointerMove",
-      "_handlePointerUp",
-      "_handleMouseDown",
-      "_handleMouseMove",
-      "_handleMouseUp",
-      "_handleWheel",
-      "_handleEnterVR",
-      "_handleExitVR",
-      "_handlePrimaryDown",
-      "_handlePrimaryUp",
-      "_handleModelLoaded",
-      "_handleCursorLoaded",
-      "_handleControllerConnected",
-      "_handleControllerDisconnected"
-    ];
-    functionNames.forEach(name => {
-      this[name] = this[name].bind(this);
-    });
+    this._handleTouchStart = this._handleTouchStart.bind(this);
+    this._handleSingleTouchStart = this._handleSingleTouchStart.bind(this);
+    this._handleTouchMove = this._handleTouchMove.bind(this);
+    this._handleTouchEnd = this._handleTouchEnd.bind(this);
+    this._handleMouseDown = this._handleMouseDown.bind(this);
+    this._handleMouseMove = this._handleMouseMove.bind(this);
+    this._handleMouseUp = this._handleMouseUp.bind(this);
+    this._handleWheel = this._handleWheel.bind(this);
+    this._handleEnterVR = this._handleEnterVR.bind(this);
+    this._handleExitVR = this._handleExitVR.bind(this);
+    this._handlePrimaryDown = this._handlePrimaryDown.bind(this);
+    this._handlePrimaryUp = this._handlePrimaryUp.bind(this);
+    this._handleModelLoaded = this._handleModelLoaded.bind(this);
+    this._handleCursorLoaded = this._handleCursorLoaded.bind(this);
+    this._handleControllerConnected = this._handleControllerConnected.bind(this);
+    this._handleControllerDisconnected = this._handleControllerDisconnected.bind(this);
 
     this.data.cursor.addEventListener("loaded", this._handleCursorLoaded);
   },
@@ -81,10 +77,10 @@ AFRAME.registerComponent("cursor-controller", {
   },
 
   play: function() {
-    document.addEventListener("pointerdown", this._handlePointerDown);
-    document.addEventListener("pointermove", this._handlePointerMove);
-    document.addEventListener("pointerup", this._handlePointerUp);
-    document.addEventListener("pointercancel", this._handlePointerUp);
+    document.addEventListener("touchstart", this._handleTouchStart);
+    document.addEventListener("touchmove", this._handleTouchMove);
+    document.addEventListener("touchend", this._handleTouchEnd);
+    document.addEventListener("touchcancel", this._handleTouchEnd);
     document.addEventListener("mousedown", this._handleMouseDown);
     document.addEventListener("mousemove", this._handleMouseMove);
     document.addEventListener("mouseup", this._handleMouseUp);
@@ -97,6 +93,8 @@ AFRAME.registerComponent("cursor-controller", {
     this.data.playerRig.addEventListener(this.data.primaryUp, this._handlePrimaryUp);
     this.data.playerRig.addEventListener(this.data.grabEvent, this._handlePrimaryDown);
     this.data.playerRig.addEventListener(this.data.releaseEvent, this._handlePrimaryUp);
+    this.data.playerRig.addEventListener("gamepadbuttondown", this._handlePrimaryDown);
+    this.data.playerRig.addEventListener("gamepadbuttonup", this._handlePrimaryUp);
     this.data.playerRig.addEventListener("model-loaded", this._handleModelLoaded);
 
     this.el.sceneEl.addEventListener("controllerconnected", this._handleControllerConnected);
@@ -104,10 +102,10 @@ AFRAME.registerComponent("cursor-controller", {
   },
 
   pause: function() {
-    document.removeEventListener("pointerdown", this._handlePointerDown);
-    document.removeEventListener("pointermove", this._handlePointerMove);
-    document.removeEventListener("pointerup", this._handlePointerUp);
-    document.removeEventListener("pointercancel", this._handlePointerUp);
+    document.removeEventListener("touchstart", this._handleTouchStart);
+    document.removeEventListener("touchmove", this._handleTouchMove);
+    document.removeEventListener("touchend", this._handleTouchEnd);
+    document.removeEventListener("touchcancel", this._handleTouchEnd);
     document.removeEventListener("mousedown", this._handleMouseDown);
     document.removeEventListener("mousemove", this._handleMouseMove);
     document.removeEventListener("mouseup", this._handleMouseUp);
@@ -120,6 +118,8 @@ AFRAME.registerComponent("cursor-controller", {
     this.data.playerRig.removeEventListener(this.data.primaryUp, this._handlePrimaryUp);
     this.data.playerRig.removeEventListener(this.data.grabEvent, this._handlePrimaryDown);
     this.data.playerRig.removeEventListener(this.data.releaseEvent, this._handlePrimaryUp);
+    this.data.playerRig.removeEventListener("gamepadbuttondown", this._handlePrimaryDown);
+    this.data.playerRig.removeEventListener("gamepadbuttonup", this._handlePrimaryUp);
     this.data.playerRig.removeEventListener("model-loaded", this._handleModelLoaded);
 
     this.el.sceneEl.removeEventListener("controllerconnected", this._handleControllerConnected);
@@ -229,7 +229,9 @@ AFRAME.registerComponent("cursor-controller", {
   },
 
   _setLookControlsEnabled(enabled) {
-    window.LookControlsToggle.toggle(enabled, this);
+    if (window.LookControlsToggle) {
+      window.LookControlsToggle.toggle(enabled, this);
+    }
   },
 
   _startTeleport: function() {
@@ -250,28 +252,33 @@ AFRAME.registerComponent("cursor-controller", {
     this._setCursorVisibility(true);
   },
 
-  _handlePointerDown: function(e) {
-    if (!this.isMobile || this.hasPointingDevice || this.activeTouch || e.clientY / window.innerHeight >= 0.8) return;
+  _handleTouchStart: function(e) {
+    if (!this.isMobile || this.hasPointingDevice) {
+      return;
+    }
 
-    this.activeTouch = e;
+    for (let i = 0; i < e.touches.length; i++) {
+      this._handleSingleTouchStart(e.touches[i]);
+    }
+  },
+
+  _handleSingleTouchStart: function(touch) {
+    if (this.activeTouch || touch.clientY / window.innerHeight >= 0.8) return;
 
     // Update the ray and cursor positions
     const raycasterComp = this.el.components.raycaster;
     const raycaster = raycasterComp.raycaster;
     const camera = this.data.camera.components.camera.camera;
     const cursor = this.data.cursor;
-    this.mousePos.set(
-      this.activeTouch.clientX / window.innerWidth * 2 - 1,
-      -(this.activeTouch.clientY / window.innerHeight) * 2 + 1
-    );
+    this.mousePos.set(touch.clientX / window.innerWidth * 2 - 1, -(touch.clientY / window.innerHeight) * 2 + 1);
     raycaster.setFromCamera(this.mousePos, camera);
     this.el.setAttribute("raycaster", { origin: raycaster.ray.origin, direction: raycaster.ray.direction });
     raycasterComp.checkIntersections();
     const intersections = raycasterComp.intersections;
     if (intersections.length === 0 || intersections[0].distance >= this.data.maxDistance) {
-      this.activeTouch = null;
       return;
     }
+    this.activeTouch = touch;
     cursor.object3D.position.copy(intersections[0].point);
     // Cursor position must be synced to physics before constraint is created
     cursor.components["static-body"].syncToPhysics();
@@ -280,20 +287,28 @@ AFRAME.registerComponent("cursor-controller", {
     cursor.emit("cursor-grab", {});
   },
 
-  _handlePointerMove: function(e) {
+  _handleTouchMove: function(e) {
     if (!this.isMobile || this.hasPointingDevice) return;
 
-    if (
-      (!this.activeTouch && e.clientY / window.innerHeight < 0.8) ||
-      (this.activeTouch && e.pointerId === this.activeTouch.pointerId)
-    ) {
-      this.mousePos.set(e.clientX / window.innerWidth * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1);
-      return;
+    for (let i = 0; i < e.touches.length; i++) {
+      const touch = e.touches[i];
+      if (
+        (!this.activeTouch && touch.clientY / window.innerHeight < 0.8) ||
+        (this.activeTouch && touch.identifier === this.activeTouch.identifier)
+      ) {
+        this.mousePos.set(touch.clientX / window.innerWidth * 2 - 1, -(touch.clientY / window.innerHeight) * 2 + 1);
+        return;
+      }
     }
   },
 
-  _handlePointerUp: function(e) {
-    if (!this.isMobile || this.hasPointingDevice || !this.activeTouch || e.pointerId !== this.activeTouch.pointerId) {
+  _handleTouchEnd: function(e) {
+    if (
+      !this.isMobile ||
+      this.hasPointingDevice ||
+      !this.activeTouch ||
+      Array.prototype.some.call(e.touches, touch => touch.identifier === this.activeTouch.identifier)
+    ) {
       return;
     }
 
@@ -343,10 +358,8 @@ AFRAME.registerComponent("cursor-controller", {
   },
 
   _handleEnterVR: function() {
-    if (AFRAME.utils.device.checkHeadsetConnected()) {
-      this.inVR = true;
-      this._updateController();
-    }
+    this.inVR = true;
+    this._updateController();
   },
 
   _handleExitVR: function() {
@@ -382,7 +395,7 @@ AFRAME.registerComponent("cursor-controller", {
   },
 
   _handleCursorLoaded: function() {
-    this.data.cursor.object3DMap.mesh.renderOrder = window.RENDER_ORDER.CURSOR;
+    this.data.cursor.object3DMap.mesh.renderOrder = window.APP.RENDER_ORDER.CURSOR;
   },
 
   _handleControllerConnected: function(e) {
@@ -413,7 +426,7 @@ AFRAME.registerComponent("cursor-controller", {
   _updateController: function() {
     this.hasPointingDevice = this.controllerQueue.length > 0 && this.inVR;
 
-    this._setCursorVisibility(this.hasPointingDevice);
+    this._setCursorVisibility(this.hasPointingDevice || this.isMobile);
 
     if (this.hasPointingDevice) {
       const controllerData = this.controllerQueue[0];
@@ -423,12 +436,5 @@ AFRAME.registerComponent("cursor-controller", {
     } else {
       this.controller = null;
     }
-  },
-
-  some: function(a, fn) {
-    for (let i = 0; i < a.length; a++) {
-      if (fn(a[i])) return true;
-    }
-    return false;
   }
 });
diff --git a/src/components/in-world-hud.js b/src/components/in-world-hud.js
index 30a912ef91f05fc7c651b3c1a4c52d7312661065..7633414b67165b9feaed769a194ca94c9bc6efee 100644
--- a/src/components/in-world-hud.js
+++ b/src/components/in-world-hud.js
@@ -8,10 +8,11 @@ AFRAME.registerComponent("in-world-hud", {
     this.freeze = this.el.querySelector(".freeze");
     this.bubble = this.el.querySelector(".bubble");
     this.background = this.el.querySelector(".bg");
-    this.mic.object3DMap.mesh.renderOrder = window.RENDER_ORDER.HUD;
-    this.freeze.object3DMap.mesh.renderOrder = window.RENDER_ORDER.HUD;
-    this.bubble.object3DMap.mesh.renderOrder = window.RENDER_ORDER.HUD;
-    this.background.object3DMap.mesh.renderORder = window.RENDER_ORDER.HUD_BACKGROUND;
+    const renderOrder = window.APP.RENDER_ORDER;
+    this.mic.object3DMap.mesh.renderOrder = renderOrder.HUD;
+    this.freeze.object3DMap.mesh.renderOrder = renderOrder.HUD;
+    this.bubble.object3DMap.mesh.renderOrder = renderOrder.HUD;
+    this.background.object3DMap.mesh.renderORder = renderOrder.HUD_BACKGROUND;
 
     this.updateButtonStates = () => {
       this.mic.setAttribute("icon-button", "active", this.el.sceneEl.is("muted"));
diff --git a/src/components/stats-plus.js b/src/components/stats-plus.js
index 2c4c148001c65cb2523bfa564024615a402392a8..6229b5f33057c7a89691c97eb4d5b77e99d7cb7d 100644
--- a/src/components/stats-plus.js
+++ b/src/components/stats-plus.js
@@ -123,8 +123,8 @@ AFRAME.registerComponent("stats-plus", {
     this.el.setAttribute(this.name, false);
   },
   remove() {
-    this.el.sceneEl.removeListener("enter-vr", this.hide);
-    this.el.sceneEl.removeListener("exit-vr", this.show);
+    this.el.sceneEl.removeEventListener("enter-vr", this.hide);
+    this.el.sceneEl.removeEventListener("exit-vr", this.show);
 
     if (this.statsEl) {
       this.statsEl.parentNode.removeChild(this.statsEl);
diff --git a/src/hub.html b/src/hub.html
index 4d0036bccbe187da62310652bc1998498f2805ab..c8771f3137f4d9c1c45b71579cd911eb101e2575 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -38,19 +38,19 @@
     >
 
         <a-assets>
-            <img id="tooltip"  src="./assets/hud/tooltip.9.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" >
+            <img id="tooltip" crossorigin="anonymous" src="./assets/hud/tooltip.9.png">
+            <img id="mute-off" crossorigin="anonymous" src="./assets/hud/mute_off.png">
+            <img id="mute-off-hover" crossorigin="anonymous" src="./assets/hud/mute_off-hover.png">
+            <img id="mute-on" crossorigin="anonymous" src="./assets/hud/mute_on.png">
+            <img id="mute-on-hover" crossorigin="anonymous" src="./assets/hud/mute_on-hover.png">
+            <img id="bubble-off" crossorigin="anonymous" src="./assets/hud/bubble_off.png">
+            <img id="bubble-off-hover" crossorigin="anonymous" src="./assets/hud/bubble_off-hover.png">
+            <img id="bubble-on" crossorigin="anonymous" src="./assets/hud/bubble_on.png">
+            <img id="bubble-on-hover" crossorigin="anonymous" src="./assets/hud/bubble_on-hover.png">
+            <img id="freeze-off" crossorigin="anonymous" src="./assets/hud/freeze_off.png">
+            <img id="freeze-off-hover" crossorigin="anonymous" src="./assets/hud/freeze_off-hover.png">
+            <img id="freeze-on" crossorigin="anonymous" src="./assets/hud/freeze_on.png">
+            <img id="freeze-on-hover" crossorigin="anonymous" 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>
@@ -69,7 +69,7 @@
             <a-asset-item id="quack" src="./assets/sfx/quack.mp3" response-type="arraybuffer" preload="auto"></a-asset-item>
             <a-asset-item id="specialquack" src="./assets/sfx/specialquack.mp3" response-type="arraybuffer" preload="auto"></a-asset-item>
 
-            <img id="water-normal-map" src="./assets/waternormals.jpg">
+            <img id="water-normal-map" crossorigin="anonymous" src="./assets/waternormals.jpg">
 
             <!-- Templates -->
 
@@ -166,7 +166,7 @@
             <template id="interactable-template">
                 <a-entity
                     gltf-model-plus="src: #interactable-duck; inflate: true;"
-                    class="interactable" 
+                    class="interactable"
                     super-networked-interactable="counter: #counter; mass: 1;"
                     body="type: dynamic; shape: none; mass: 1;"
                     grabbable
@@ -194,8 +194,8 @@
         <a-entity
             id="cursor-controller"
             cursor-controller="
-                cursor: #cursor; 
-                camera: #player-camera; 
+                cursor: #cursor;
+                camera: #player-camera;
                 playerRig: #player-rig;
                 physicalHandSelector: #player-right-controller;
                 gazeTeleportControls: #gaze-teleport;"
@@ -218,7 +218,7 @@
             segments-height="9"
             segments-width="9"
             event-repeater="events: raycaster-intersection, raycaster-intersection-cleared; eventSource: #cursor-controller"
-        ></a-sphere> 
+        ></a-sphere>
 
         <!-- Player Rig -->
         <a-entity
@@ -259,9 +259,9 @@
                     id="gaze-teleport"
                     position = "0.15 0 0"
                     teleport-controls="
-                    cameraRig: #player-rig; 
-                    teleportOrigin: #player-camera; 
-                    button: cursor-teleport_; 
+                    cameraRig: #player-rig;
+                    teleportOrigin: #player-camera;
+                    button: cursor-teleport_;
                     collisionEntities: [nav-mesh];
                     drawIncrementally: true;
                     incrementalDrawMs: 600;
@@ -276,9 +276,9 @@
                 hand-controls2="left"
                 tracked-controls
                 teleport-controls="
-                    cameraRig: #player-rig; 
-                    teleportOrigin: #player-camera; 
-                    button: cursor-teleport_; 
+                    cameraRig: #player-rig;
+                    teleportOrigin: #player-camera;
+                    button: cursor-teleport_;
                     collisionEntities: [nav-mesh];
                     drawIncrementally: true;
                     incrementalDrawMs: 600;
@@ -297,9 +297,9 @@
                 hand-controls2="right"
                 tracked-controls
                 teleport-controls="
-                    cameraRig: #player-rig; 
-                    teleportOrigin: #player-camera; 
-                    button: cursor-teleport_; 
+                    cameraRig: #player-rig;
+                    teleportOrigin: #player-camera;
+                    button: cursor-teleport_;
                     collisionEntities: [nav-mesh];
                     drawIncrementally: true;
                     incrementalDrawMs: 600;
@@ -346,8 +346,8 @@
         </a-entity>
 
         <!-- Environment -->
-        <a-entity 
-            id="environment-root" 
+        <a-entity
+            id="environment-root"
             nav-mesh-helper
             static-body="shape: none;"
         ></a-entity>
diff --git a/src/hub.js b/src/hub.js
index e7b05cc5824738a4d6df438ef03af02bce7bf3d2..aea22f113ba248bcb05f76f3613c760f56aef4ab 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -17,7 +17,7 @@ import "aframe-billboard-component";
 import "aframe-rounded";
 import "webrtc-adapter";
 import "aframe-slice9-component";
-import "./utils/ios-audio-context-fix";
+import "./utils/audio-context-fix";
 
 import trackpad_dpad4 from "./behaviours/trackpad-dpad4";
 import joystick_dpad4 from "./behaviours/joystick-dpad4";
@@ -86,6 +86,11 @@ if (qs.quality) {
 } else {
   window.APP.quality = isMobile ? "low" : "high";
 }
+window.APP.RENDER_ORDER = {
+  HUD_BACKGROUND: 1,
+  HUD: 2,
+  CURSOR: 3
+};
 
 import "aframe-physics-system";
 import "aframe-physics-extras";
@@ -115,12 +120,6 @@ import PinchToMove from "./utils/pinch-to-move.js";
 import LookControlsToggle from "./utils/look-controls-toggle.js";
 import PointerLookControls from "./utils/pointer-look-controls.js";
 
-window.RENDER_ORDER = {
-  HUD_BACKGROUND: 1,
-  HUD: 2,
-  CURSOR: 3
-};
-
 function qsTruthy(param) {
   const val = qs[param];
   // if the param exists but is not set (e.g. "?foo&bar"), its value is null.
@@ -205,10 +204,16 @@ const onReady = async () => {
     if (NAF.connection.adapter && NAF.connection.adapter.localMediaStream) {
       NAF.connection.adapter.localMediaStream.getTracks().forEach(t => t.stop());
     }
-    hubChannel.disconnect();
+    if (hubChannel) {
+      hubChannel.disconnect();
+    }
     const scene = document.querySelector("a-scene");
-    scene.renderer.animate(null); // Stop animation loop, TODO A-Frame should do this
-    document.body.removeChild(scene);
+    if (scene) {
+      if (scene.renderer) {
+        scene.renderer.animate(null); // Stop animation loop, TODO A-Frame should do this
+      }
+      document.body.removeChild(scene);
+    }
   };
 
   const enterScene = async (mediaStream, enterInVR, janusRoomId) => {
diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js
index 63353a4118b5c60b046c0b6c40fe202ff20ff05c..df85b425646d1b9a0eae4f216ba3c00cae6dfdc8 100644
--- a/src/react-components/home-root.js
+++ b/src/react-components/home-root.js
@@ -30,9 +30,36 @@ class HomeRoot extends Component {
   componentDidMount() {
     this.loadEnvironments();
     this.setState({ dialogType: this.props.dialogType });
-    document.querySelector("#background-video").playbackRate = 0.75;
+    this.loadHomeVideo();
   }
 
+  loadHomeVideo = () => {
+    const videoEl = document.querySelector("#background-video");
+    function initVideo() {
+      videoEl.playbackRate = 0.75;
+      videoEl.play();
+      function toggleVideo() {
+        // Play the video if the window/tab is visible.
+        if (!("hasFocus" in document)) {
+          return;
+        }
+        if (document.hasFocus()) {
+          videoEl.play();
+        } else {
+          videoEl.pause();
+        }
+      }
+      document.addEventListener("visibilitychange", toggleVideo);
+      window.addEventListener("focus", toggleVideo);
+      window.addEventListener("blur", toggleVideo);
+    }
+    if (videoEl.readyState >= videoEl.HAVE_FUTURE_DATA) {
+      initVideo();
+    } else {
+      videoEl.addEventListener("canplay", initVideo);
+    }
+  };
+
   showDialog = dialogType => {
     return e => {
       e.preventDefault();
@@ -181,7 +208,7 @@ class HomeRoot extends Component {
               </div>
             </div>
           </div>
-          <video playsInline autoPlay muted loop className="background-video" id="background-video">
+          <video playsInline muted loop className="background-video" id="background-video">
             <source src={homeVideoWebM} type="video/webm" />
             <source src={homeVideoMp4} type="video/mp4" />
           </video>
diff --git a/src/react-components/info-dialog.js b/src/react-components/info-dialog.js
index ab7bdda1c22098e1991afd98ff23ffdeb63df83e..6e27e39c91262cf7e23a2597d00793f5f172f34b 100644
--- a/src/react-components/info-dialog.js
+++ b/src/react-components/info-dialog.js
@@ -27,6 +27,28 @@ class InfoDialog extends Component {
 
     const loc = document.location;
     this.shareLink = `${loc.protocol}//${loc.host}${loc.pathname}`;
+    this.onKeyDown = this.onKeyDown.bind(this);
+    this.onContainerClicked = this.onContainerClicked.bind(this);
+  }
+
+  componentDidMount() {
+    window.addEventListener("keydown", this.onKeyDown);
+  }
+
+  componentWillUnmount() {
+    window.removeEventListener("keydown", this.onKeyDown);
+  }
+
+  onKeyDown(e) {
+    if (e.key === "Escape") {
+      this.props.onCloseDialog();
+    }
+  }
+
+  onContainerClicked(e) {
+    if (e.currentTarget === e.target) {
+      this.props.onCloseDialog();
+    }
   }
 
   shareLinkClicked = () => {
@@ -211,6 +233,18 @@ class InfoDialog extends Component {
             <p>
               The <b>Bubble Toggle</b> hides avatars that enter your personal space.
             </p>
+            <p className="dialog__box__contents__links">
+              <a target="_blank" rel="noopener noreferrer" href="https://github.com/mozilla/hubs/blob/master/TERMS.md">
+                <FormattedMessage id="profile.terms_of_use" />
+              </a>
+              <a
+                target="_blank"
+                rel="noopener noreferrer"
+                href="https://github.com/mozilla/hubs/blob/master/PRIVACY.md"
+              >
+                <FormattedMessage id="profile.privacy_notice" />
+              </a>
+            </p>
           </div>
         );
         break;
@@ -223,7 +257,7 @@ class InfoDialog extends Component {
 
     return (
       <div className="dialog-overlay">
-        <div className={dialogClasses}>
+        <div className={dialogClasses} onClick={this.onContainerClicked}>
           <div className="dialog__box">
             <div className="dialog__box__contents">
               <button className="dialog__box__contents__close" onClick={this.props.onCloseDialog}>
diff --git a/src/storage/store.js b/src/storage/store.js
index ce44da77198fca2ba570068ffdfa544a5f16732a..23f3168198f11a6824d0f1a4bdd63d67a0ef5961 100644
--- a/src/storage/store.js
+++ b/src/storage/store.js
@@ -1,5 +1,5 @@
 import { Validator } from "jsonschema";
-import { merge } from "lodash";
+import merge from "lodash/merge";
 
 const LOCAL_STORE_KEY = "___hubs_store";
 const STORE_STATE_CACHE_KEY = Symbol();
diff --git a/src/utils/audio-context-fix.js b/src/utils/audio-context-fix.js
new file mode 100644
index 0000000000000000000000000000000000000000..1d3df3b08b23d216823980d924989ada5564f51f
--- /dev/null
+++ b/src/utils/audio-context-fix.js
@@ -0,0 +1,22 @@
+/**
+ * Chrome and Safari will start Audio contexts in a "suspended" state.
+ * A user interaction (touch/mouse event) is needed in order to resume the AudioContext.
+ */
+
+document.addEventListener("DOMContentLoaded", () => {
+  const ctx = THREE.AudioContext.getContext();
+
+  function resume() {
+    ctx.resume();
+
+    setTimeout(function() {
+      if (ctx.state === "running") {
+        document.body.removeEventListener("touchend", resume, false);
+        document.body.removeEventListener("mouseup", resume, false);
+      }
+    }, 0);
+  }
+
+  document.body.addEventListener("touchend", resume, false);
+  document.body.addEventListener("mouseup", resume, false);
+});
diff --git a/src/utils/ios-audio-context-fix.js b/src/utils/ios-audio-context-fix.js
deleted file mode 100644
index ce474194f663ae81040f8fdadcdb5a5130792a37..0000000000000000000000000000000000000000
--- a/src/utils/ios-audio-context-fix.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Mobile Safari will start Audio contexts in a "suspended" state.
- * A user interaction (touch event) is needed in order to resume the AudioContext.
- */
-const iDevices = /\biPhone.*Mobile|\biPod|\biPad|AppleCoreMedia/;
-
-if (iDevices.test(navigator.userAgent)) {
-  document.addEventListener("DOMContentLoaded", () => {
-    const ctx = THREE.AudioContext.getContext();
-
-    function resume() {
-      ctx.resume();
-
-      setTimeout(function() {
-        if (ctx.state === "running") {
-          document.body.removeEventListener("touchend", resume, false);
-        }
-      }, 0);
-    }
-
-    document.body.addEventListener("touchend", resume, false);
-  });
-}
diff --git a/src/utils/pinch-to-move.js b/src/utils/pinch-to-move.js
index 093fc9ac72d5633bbbd6b51b2a025acb79de9776..af474d5c43bb25fca8ecc8ac1e162a7d96bcd5e2 100644
--- a/src/utils/pinch-to-move.js
+++ b/src/utils/pinch-to-move.js
@@ -20,26 +20,28 @@ export default class PinchToMove {
     }
 
     this.el.emit("move", { axis: [0, this.dir * this.decayingSpeed] });
-    this.decayingSpeed *= 0.93;
+    this.decayingSpeed *= 0.95;
   }
 
   onPinch(e) {
     const dist = e.detail.distance * this.speed;
     this.decayingSpeed = dist;
     this.dir = -1;
+    this.el.emit("move", { axis: [0, this.dir * dist] });
 
-    if (!this.interval) {
-      this.interval = window.setInterval(this.decay, 20);
-    }
+    //    if (!this.interval) {
+    //      this.interval = window.setInterval(this.decay, 20);
+    //    }
   }
 
   onSpread(e) {
     const dist = e.detail.distance * this.speed;
     this.decayingSpeed = dist;
     this.dir = 1;
+    this.el.emit("move", { axis: [0, this.dir * dist] });
 
-    if (!this.interval) {
-      this.interval = window.setInterval(this.decay, 20);
-    }
+    //   if (!this.interval) {
+    //     this.interval = window.setInterval(this.decay, 20);
+    //   }
   }
 }
diff --git a/src/utils/pinch.js b/src/utils/pinch.js
index cbb2ba5036307adc8dd918897fe622380834445d..df28f571b163045ab9657a0ed4f14ed989d63fc2 100644
--- a/src/utils/pinch.js
+++ b/src/utils/pinch.js
@@ -2,49 +2,62 @@ export default class Pinch {
   constructor(el) {
     this.el = el;
     this.prevDiff = -1;
-    this.evCache = [];
+    this.touchCache = [];
+    this.usedTouch = { identifier: -1 };
 
-    this.onPointerMove = this.onPointerMove.bind(this);
-    this.onPointerDown = this.onPointerDown.bind(this);
-    this.onPointerUp = this.onPointerUp.bind(this);
-    this.removeEvent = this.removeEvent.bind(this);
+    this.onTouchMove = this.onTouchMove.bind(this);
+    this.onTouchStart = this.onTouchStart.bind(this);
+    this.onTouchEnd = this.onTouchEnd.bind(this);
+    this.removeTouch = this.removeTouch.bind(this);
+    this.addTouch = this.addTouch.bind(this);
 
-    document.addEventListener("pointermove", this.onPointerMove);
-    document.addEventListener("pointerdown", this.onPointerDown);
-    document.addEventListener("pointerup", this.onPointerUp);
-    document.addEventListener("pointercancel", this.onPointerUp);
-    document.addEventListener("touch-used-by-cursor", this.onPointerUp);
+    document.addEventListener("touchmove", this.onTouchMove);
+    document.addEventListener("touchstart", this.onTouchStart);
+    document.addEventListener("touchend", this.onTouchEnd);
+    document.addEventListener("touchcancel", this.onTouchEnd);
+    document.addEventListener("touch-used-by-cursor", ev => {
+      const touch = ev.detail;
+      this.removeTouch(touch);
+      this.usedTouch = touch;
+    });
   }
 
-  onPointerUp = ev => {
-    this.removeEvent(ev);
-    if (this.evCache.length < 2) {
-      window.LookControlsToggle.toggle(true, this);
-      this.prevDiff = -1;
+  onTouchEnd = ev => {
+    for (let i = 0; i < ev.changedTouches.length; i++) {
+      const touch = ev.changedTouches[i];
+      if (touch.identifier === this.usedTouch.identifier) {
+        this.usedTouch = { identifier: -1 };
+      }
+      this.removeTouch(touch);
     }
   };
 
-  onPointerDown = ev => {
-    if (ev.isUsedByCursor || ev.clientY / window.innerHeight >= 0.8) {
-      return;
+  onTouchStart = ev => {
+    for (let i = 0; i < ev.touches.length; i++) {
+      const touch = ev.touches[i];
+      if (touch.identifier === this.usedTouch.identifier || touch.clientY / window.innerHeight >= 0.8) {
+        continue;
+      }
+      this.addTouch(touch);
     }
-    this.evCache.push(ev);
   };
 
-  onPointerMove = ev => {
-    const cache = this.evCache;
-
-    for (var i = 0; i < cache.length; i++) {
-      if (ev.pointerId === cache[i].pointerId) {
-        cache[i] = ev;
-        break;
+  onTouchMove = ev => {
+    const cache = this.touchCache;
+    for (let i = 0; i < ev.touches.length; i++) {
+      const touch = ev.touches[i];
+      if (touch.identifier !== this.usedTouch.identifier) {
+        this.updateTouch(touch);
       }
     }
 
     if (cache.length !== 2) {
+      this.prevDiff = -1;
       return;
     }
-    window.LookControlsToggle.toggle(false, this);
+    if (window.LookControlsToggle) {
+      window.LookControlsToggle.toggle(false, this);
+    }
 
     const diff = Pinch.distance(cache[0].clientX, cache[0].clientY, cache[1].clientX, cache[1].clientY);
 
@@ -59,13 +72,38 @@ export default class Pinch {
     this.prevDiff = diff;
   };
 
-  removeEvent = ev => {
-    for (let i = 0; i < this.evCache.length; i++) {
-      if (this.evCache[i].pointerId == ev.pointerId) {
-        this.evCache.splice(i, 1);
+  removeTouch = touch => {
+    for (let i = 0; i < this.touchCache.length; i++) {
+      if (this.touchCache[i].identifier === touch.identifier) {
+        this.touchCache.splice(i, 1);
         break;
       }
     }
+    if (this.touchCache.length < 2) {
+      if (window.LookControlsToggle) {
+        window.LookControlsToggle.toggle(true, this);
+      }
+      this.prevDiff = -1;
+    }
+  };
+
+  addTouch = touch => {
+    for (let i = 0; i < this.touchCache.length; i++) {
+      if (this.touchCache[i].identifier === touch.identifier) {
+        return;
+      }
+    }
+
+    this.touchCache.push(touch);
+  };
+
+  updateTouch = touch => {
+    for (let i = 0; i < this.touchCache.length; i++) {
+      if (this.touchCache[i].identifier === touch.identifier) {
+        this.touchCache[i] = touch;
+        return;
+      }
+    }
   };
 
   static distance = (x1, y1, x2, y2) => {
diff --git a/src/utils/pointer-look-controls.js b/src/utils/pointer-look-controls.js
index 49485c8a1273fdb90f05966c85be39de26d954c4..78b23bbc689f7f229734ec6bda1d9ca9a9efdded 100644
--- a/src/utils/pointer-look-controls.js
+++ b/src/utils/pointer-look-controls.js
@@ -4,17 +4,28 @@ export default class PointerLookControls {
     this.xSpeed = 0.005;
     this.ySpeed = 0.003;
     this.lookControlsEl = lookControlsEl;
-    this.onPointerDown = this.onPointerDown.bind(this);
-    this.onPointerMove = this.onPointerMove.bind(this);
-    this.onPointerUp = this.onPointerUp.bind(this);
+    this.onTouchStart = this.onTouchStart.bind(this);
+    this.onTouchMove = this.onTouchMove.bind(this);
+    this.onTouchEnd = this.onTouchEnd.bind(this);
     this.getLookControls = this.getLookControls.bind(this);
-    this.removeEvent = this.removeEvent.bind(this);
-    document.addEventListener("touch-used-by-cursor", this.onPointerUp);
+    this.removeTouch = this.removeTouch.bind(this);
+    this.usedTouch = { identifier: -1 };
+    document.addEventListener("touch-used-by-cursor", ev => {
+      const touch = ev.detail;
+      this.removeTouch(touch);
+      this.usedTouch = touch;
+    });
+
     this.start = this.start.bind(this);
     this.stop = this.stop.bind(this);
 
     this.getLookControls();
     this.cache = [];
+
+    document.addEventListener("touchstart", this.onTouchStart);
+    document.addEventListener("touchmove", this.onTouchMove);
+    document.addEventListener("touchend", this.onTouchEnd);
+    document.addEventListener("touchcancel", this.onTouchEnd);
   }
 
   getLookControls() {
@@ -24,64 +35,74 @@ export default class PointerLookControls {
   }
 
   start() {
-    document.addEventListener("pointerdown", this.onPointerDown);
-    document.addEventListener("pointermove", this.onPointerMove);
-    document.addEventListener("pointerup", this.onPointerUp);
-    document.addEventListener("pointercancel", this.onPointerUp);
     if (!this.lookControls) {
       this.getLookControls();
     }
+    this.enabled = true;
   }
 
   stop() {
-    document.removeEventListener("pointerdown", this.onPointerDown);
-    document.removeEventListener("pointermove", this.onPointerMove);
-    document.removeEventListener("pointerup", this.onPointerUp);
-    document.removeEventListener("pointercancel", this.onPointerUp);
-    this.cache = [];
+    this.enabled = false;
   }
 
-  onPointerDown(ev) {
-    if (ev.isUsedByCursor || ev.clientY / window.innerHeight >= 0.8) {
-      return;
+  onTouchStart(ev) {
+    for (let i = 0; i < ev.touches.length; i++) {
+      let touch = ev.touches[i];
+      if (touch.identifier === this.usedTouch.identifier || touch.clientY / window.innerHeight >= 0.8) {
+        continue;
+      }
     }
-    this.cache.push(ev);
   }
 
-  onPointerMove(ev) {
+  onTouchMove(ev) {
     const cache = this.cache;
-    if (ev.isUsedByCursor || ev.clientY / window.innerHeight >= 0.8) {
-      return;
-    }
+    this.foo = !!this.foo ? this.foo + 1 : 1;
+    for (let i = 0; i < ev.touches.length; i++) {
+      let touch = ev.touches[i];
 
-    let cachedEv = null;
-    for (var i = 0; i < cache.length; i++) {
-      if (ev.pointerId === cache[i].pointerId) {
-        cachedEv = cache[i];
-        cache[i] = ev;
-        break;
+      if (touch.identifier === this.usedTouch.identifier || touch.clientY / window.innerHeight >= 0.8) {
+        continue;
       }
-    }
-    if (!cachedEv) {
-      return;
-    }
 
-    const dX = ev.clientX - cachedEv.clientX;
-    const dY = ev.clientY - cachedEv.clientY;
+      let cachedTouch = null;
+      for (var j = 0; j < cache.length; j++) {
+        if (touch.identifier === cache[j].identifier) {
+          cachedTouch = cache[j];
+          cache[j] = touch;
+          break;
+        }
+      }
+      if (!cachedTouch) {
+        this.cache.push(touch);
+        continue;
+      }
+
+      if (!this.enabled) {
+        continue;
+      }
+      const dX = touch.clientX - cachedTouch.clientX;
+      const dY = touch.clientY - cachedTouch.clientY;
 
-    this.yawObject.rotation.y -= dX * this.xSpeed;
-    this.pitchObject.rotation.x -= dY * this.ySpeed;
-    this.pitchObject.rotation.x = Math.max(-PI_2, Math.min(PI_2, this.pitchObject.rotation.x));
+      this.yawObject.rotation.y -= dX * this.xSpeed;
+      this.pitchObject.rotation.x -= dY * this.ySpeed;
+      this.pitchObject.rotation.x = Math.max(-PI_2, Math.min(PI_2, this.pitchObject.rotation.x));
+    }
   }
 
-  onPointerUp(ev) {
-    this.removeEvent(ev);
+  onTouchEnd(ev) {
+    for (let i = 0; i < ev.changedTouches.length; i++) {
+      const touch = ev.changedTouches[i];
+      this.removeTouch(touch);
+      if (touch.identifier === this.usedTouch.identifier) {
+        this.usedTouch = { identifier: -1 };
+      }
+    }
   }
 
-  removeEvent(ev) {
+  removeTouch(touch) {
     const cache = this.cache;
     for (let i = 0; i < cache.length; i++) {
-      if (cache[i].pointerId == ev.pointerId) {
+      if (cache[i].identifier == touch.identifier) {
         cache.splice(i, 1);
         break;
       }
diff --git a/yarn.lock b/yarn.lock
index e02de0e3740390d0cded16bbe216c82af9372883..2d452cee67db4e1d5e48710397da6d770bb12b10 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -174,9 +174,9 @@ aframe-physics-extras@^0.1.3:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/aframe-physics-extras/-/aframe-physics-extras-0.1.3.tgz#803e2164fb96c0a80f2d1a81458f3277f262b130"
 
-aframe-physics-system@^3.1.1:
+"aframe-physics-system@github:infinitelee/aframe-physics-system#hubs/master":
   version "3.1.1"
-  resolved "https://registry.yarnpkg.com/aframe-physics-system/-/aframe-physics-system-3.1.1.tgz#3e6c48f8ce63a1d356a7e302fed51c7b5ad23d22"
+  resolved "https://codeload.github.com/infinitelee/aframe-physics-system/tar.gz/80a722ddc9496e4fc867fb3662f61b389d0fd4ca"
   dependencies:
     browserify "^14.3.0"
     budo "^10.0.3"