diff --git a/package.json b/package.json
index 525603b4ea4b139f4d45c7de964a48d45e68745a..89c2b86a1b3fdc077a6e1c02a6f06cd7c26efbcd 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
     "aframe-teleport-controls": "^0.3.1",
     "aframe-xr": "github:brianpeiris/aframe-xr#3162aed",
     "classnames": "^2.2.5",
+    "copy-to-clipboard": "^3.0.8",
     "detect-browser": "^2.1.0",
     "event-target-shim": "^3.0.1",
     "form-urlencoded": "^2.0.4",
diff --git a/src/assets/stylesheets/2d-hud.css b/src/assets/stylesheets/2d-hud.css
index a50436114181d18997248dce77a3cc1d9500363f..024daffb0b6463f58a70d5e7d2ef9565d840de91 100644
--- a/src/assets/stylesheets/2d-hud.css
+++ b/src/assets/stylesheets/2d-hud.css
@@ -38,7 +38,7 @@
   display: flex;
   align-items: center;
   justify-content: center;
-  z-index: 10;
+  z-index: 1;
 }
 
 :local(.panel) {
diff --git a/src/assets/stylesheets/footer.scss b/src/assets/stylesheets/footer.scss
new file mode 100644
index 0000000000000000000000000000000000000000..0e2e957677e8c5ebde65ab04c0ab7736890e2c41
--- /dev/null
+++ b/src/assets/stylesheets/footer.scss
@@ -0,0 +1,125 @@
+@import 'shared';
+
+:local(.container) {
+  position: absolute;
+  width: 100%;
+  bottom: 0;
+  font-size: 1.3em;
+  display: flex;
+  flex-direction: column;
+  pointer-events: auto;
+  // Position above virtual gamepad controls on mobile
+  z-index: 1;
+
+}
+:local(.floatingButton) {
+  display: flex;
+  justify-content: center;
+}
+:local(.header), :local(.menu-header) {
+  display: flex;
+}
+:local(.header) {
+  border-bottom: 1px solid rgba(32, 32, 32, 0.65);
+
+  @media (max-width: 768px) {
+    border-bottom: none;
+  }
+}
+:local(.menu-header) {
+  background-color: transparent;
+  border-bottom: 1px solid rgba(32, 32, 32, 0.65);
+
+  @media (min-width: 768px) {
+    display: none;
+  }
+}
+:local(.header) {
+  background-color: rgba(0, 0, 0, 0.65);
+
+  @media (max-width: 768px) {
+    background-color: transparent;
+  }
+
+  :local(.hub-info) {
+    @media (max-width: 768px) {
+      display: none;
+    }
+  }
+
+  :local(.hub-stats) {
+    @media (max-width: 768px) {
+      display: none;
+    }
+  }
+}
+
+:local(.hub-info) {
+  flex: 1;
+  margin: 16px 24px;
+  display: flex;
+  align-items: center;
+  @media (max-width: 768px) {
+    margin: 16px 8px;
+    font-size: 0.9em;
+  }
+}
+:local(.hub-stats) {
+  text-align: right;
+  margin: 16px 24px;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  @media (min-width: 768px) {
+    flex: 1;
+  }
+  @media (max-width: 768px) {
+    margin: 16px 8px;
+  }
+  :local(.hub-participant-count) {
+    margin: 0 12px;
+  }
+}
+
+:local(.menu) {
+  padding: 5px 0;
+  background-color: rgba(0, 0, 0, 0.85);
+  display: flex;
+  flex-direction: column;
+}
+:local(.menu-buttons) {
+  margin: 0 auto;
+}
+:local(.menu-button) {
+  @extend %default-font;
+  margin: 16px 0;
+  padding: 0;
+  display: block;
+  background: none;
+  border: none;
+  color: white;
+  cursor: pointer;
+  font-size: 0.8em;
+
+  @media (max-width: 768px) {
+    flex: 1;
+    align-self: center;
+  }
+  :local(.menu-button__icon) {
+    background: black;
+    width: 40px !important;
+    height: 40px;
+    border: 3px solid white;
+    border-radius: 40px;
+    display: inline-block;
+    font-size: 22px;
+    vertical-align: sub;
+    line-height: 42px;
+    svg {
+      margin-left: 0px;
+    }
+  }
+  :local(.menu-button__text) {
+    margin-left: 16px;
+  }
+}
diff --git a/src/assets/stylesheets/hub.scss b/src/assets/stylesheets/hub.scss
index c10db8dd196ca6b3dce999605e002893cacd05e6..b7d6a928ceb38e36282373ca184bbef631705025 100644
--- a/src/assets/stylesheets/hub.scss
+++ b/src/assets/stylesheets/hub.scss
@@ -6,20 +6,16 @@
 @import 'profile';
 @import 'entry';
 @import 'audio';
+@import 'info-dialog';
 
 .a-enter-vr {
   display: none;
 }
 
-.rs-base {
-  top: auto;
-  bottom: 20px;
-}
-
 .a-canvas.a-grab-cursor:hover {
-	cursor: none;
+  cursor: none;
 }
 
 .a-canvas.a-grab-cursor:active {
-	cursor: none;
+  cursor: none;
 }
diff --git a/src/assets/stylesheets/index.scss b/src/assets/stylesheets/index.scss
index b09114616dd142d6272899ea88ca70564511f741..c7e9b5099b4c16841f4cb61a3f7deddb185f25bc 100644
--- a/src/assets/stylesheets/index.scss
+++ b/src/assets/stylesheets/index.scss
@@ -1,5 +1,6 @@
 @import 'shared';
 @import 'hub-create';
+@import 'info-dialog';
 
 * {
   box-sizing: border-box;
@@ -226,110 +227,3 @@ 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/stylesheets/info-dialog.scss b/src/assets/stylesheets/info-dialog.scss
new file mode 100644
index 0000000000000000000000000000000000000000..8312010fc7602c17b5b1221ee423e910f7cdb8cc
--- /dev/null
+++ b/src/assets/stylesheets/info-dialog.scss
@@ -0,0 +1,150 @@
+.dialog-overlay {
+  width: 100%;
+  height: 100%;
+  top: 0;
+  left: 0;
+  position: absolute;
+  pointer-events: none;
+  color: white;
+  z-index: 2;
+}
+
+.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;
+      }
+
+      &__body {
+	margin: 40px;
+	margin-bottom: 0px;
+	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;
+      }
+    }
+  }
+}
+
+.invite-form {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  text-align: center;
+  margin: 0;
+
+  &__buttons {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+  }
+
+  &__link {
+    display: flex;
+    flex-direction: row;
+  }
+
+  &__link_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%;
+  }
+
+  &__action-button {
+    @extend %bottom-button;
+    margin-left: 6px;
+    margin-right: 6px;
+    appearance: none;
+    width: 128px;
+    text-align: center;
+    -moz-appearance: none;
+    -webkit-appearance: none;
+  }
+}
+
+.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;
+  }
+}
+
+
+
diff --git a/src/assets/stylesheets/profile.scss b/src/assets/stylesheets/profile.scss
index 95f2caa2629d34e3f99fbab242f5cbf28b498038..d392c99abaeb15bfd047ccce1effa7ca501b5d43 100644
--- a/src/assets/stylesheets/profile.scss
+++ b/src/assets/stylesheets/profile.scss
@@ -16,6 +16,10 @@
     margin: 1em 0;
   }
 
+  &__form {
+    height: 100%;
+  }
+
   &__box {
     border-radius: 8px;
     display: flex;
@@ -27,7 +31,9 @@
     width: 60vw;
     min-width: 300px;
     max-width: 700px;
-    height: 500px;
+    min-height: 300px;
+    max-height: 1000px;
+    height: 90%;
 
     &--darkened {
       background-color: $darkest-transparent;
diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json
index 6d29a6306cfb0aa0438a456099ab93f986704fab..5a1ad32fa33a1dce374dedcf9694d01d4793d044 100644
--- a/src/assets/translations.data.json
+++ b/src/assets/translations.data.json
@@ -13,9 +13,8 @@
     "entry.daydream-via-chrome": "Using Google Chrome",
     "entry.enable-screen-sharing": "Share my desktop",
     "profile.save": "SAVE",
-    "profile.display_name.label": "Display name:",
     "profile.display_name.validation_warning": "Alphanumerics and hyphens. At least 3 characters, no more than 32",
-    "profile.header": "Your identity",
+    "profile.header": "Your display name:",
     "profile.terms.prefix": "I confirm that I am over the age of 13 and agree to the",
     "profile.terms.privacy": "privacy policy",
     "profile.terms.conjunction": "and",
@@ -33,6 +32,7 @@
     "audio.granted-subtitle": "You can still mute yourself in-game",
     "audio.granted-next": "NEXT",
     "exit.subtitle.exited": "Your session has ended. Refresh your browser to start a new one.",
+    "exit.subtitle.closed": "This room is no longer available.",
     "exit.subtitle.full": "This room is full, please try again later.",
     "exit.subtitle.connect_error": "Unable to connect to this room, please try again later.",
     "autoexit.title": "Auto-ending session in ",
diff --git a/src/components/stats-plus.css b/src/components/stats-plus.css
index fc4417bf506b5a5916c286b72aeaf86189790732..9fc5a1908dbc72abadc6a67c0479e3cfe8d01db3 100644
--- a/src/components/stats-plus.css
+++ b/src/components/stats-plus.css
@@ -11,16 +11,18 @@
 }
 
 :global(.rs-fps-counter) {
+  font-family: monospace;
   cursor: pointer;
   position: absolute;
-  bottom: 0;
-  left: 0;
-  padding: 8px;
+  top: 0;
+  right: 0;
+  padding: 8px 12px;
   color: #aaa;
   font-size: 10px;
 }
 
-:global(.rs-mobile) {
-  bottom: auto;
-  top: 0;
-}
\ No newline at end of file
+:global(.rs-base) {
+  right: 10px;
+  left: auto;
+  top: 10px;
+}
diff --git a/src/components/stats-plus.js b/src/components/stats-plus.js
index c64100eeafa0e069a10ff2b005181a2dc446435e..2c4c148001c65cb2523bfa564024615a402392a8 100644
--- a/src/components/stats-plus.js
+++ b/src/components/stats-plus.js
@@ -53,6 +53,7 @@ AFRAME.registerComponent("stats-plus", {
     this.fpsEl.classList.add("rs-fps-counter");
     document.body.appendChild(this.fpsEl);
     this.lastFpsUpdate = performance.now();
+    this.lastFps = 0;
     this.frameCount = 0;
 
     if (scene.isMobile) {
@@ -88,8 +89,11 @@ AFRAME.registerComponent("stats-plus", {
 
       // 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";
+        const fps = Math.round(this.frameCount / ((now - this.lastFpsUpdate) / 1000));
+        if (fps !== this.lastFps) {
+          this.fpsEl.innerHTML = Math.round(fps) + " FPS";
+          this.lastFps = fps;
+        }
         this.lastFpsUpdate = now;
         this.frameCount = 0;
       }
diff --git a/src/hub.js b/src/hub.js
index d7ab7351a4f4b998637a84efc24e8405a46400f9..3cd164eb07a952ee61380bfc96190f84fc3ae83a 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -204,9 +204,7 @@ const onReady = async () => {
       serverURL: process.env.JANUS_SERVER
     });
 
-    if (!qsTruthy("no_stats")) {
-      scene.setAttribute("stats-plus", false);
-    }
+    scene.setAttribute("stats-plus", false);
 
     if (isMobile || qsTruthy("mobile")) {
       playerRig.setAttribute("virtual-gamepad-controls", {});
@@ -248,6 +246,19 @@ const onReady = async () => {
         hubChannel.sendEntryEvent().then(() => {
           store.update({ lastEnteredAt: moment().toJSON() });
         });
+        remountUI({ occupantCount: NAF.connection.adapter.publisher.initialOccupants.length + 1 });
+      });
+
+      document.body.addEventListener("clientConnected", () => {
+        remountUI({
+          occupantCount: Object.keys(NAF.connection.adapter.occupants).length + 1
+        });
+      });
+
+      document.body.addEventListener("clientDisconnected", () => {
+        remountUI({
+          occupantCount: Object.keys(NAF.connection.adapter.occupants).length + 1
+        });
       });
 
       scene.components["networked-scene"].connect().catch(connectError => {
@@ -313,10 +324,10 @@ const onReady = async () => {
   console.log(`Hub ID: ${hubId}`);
 
   const socketProtocol = document.location.protocol === "https:" ? "wss:" : "ws:";
-  const socketPort = qs.phx_port || (process.env.NODE_ENV === "production" ? document.location.port : 443);
-  const socketHost =
-    qs.phx_host ||
-    (process.env.NODE_ENV === "production" ? document.location.hostname : process.env.DEV_RETICULUM_SERVER);
+  const [retHost, retPort] = (process.env.DEV_RETICULUM_SERVER || "").split(":");
+  const isProd = process.env.NODE_ENV === "production";
+  const socketPort = qs.phx_port || (isProd ? document.location.port : retPort) || "443";
+  const socketHost = qs.phx_host || (isProd ? document.location.hostname : retHost) || "";
   const socketUrl = `${socketProtocol}//${socketHost}${socketPort ? `:${socketPort}` : ""}/socket`;
   console.log(`Phoenix Channel URL: ${socketUrl}`);
 
@@ -331,7 +342,7 @@ const onReady = async () => {
       const hub = data.hubs[0];
       const defaultSpaceTopic = hub.topics[0];
       const gltfBundleUrl = defaultSpaceTopic.assets.find(a => a.asset_type === "gltf_bundle").src;
-      remountUI({ janusRoomId: defaultSpaceTopic.janus_room_id });
+      remountUI({ janusRoomId: defaultSpaceTopic.janus_room_id, hubName: hub.name });
       initialEnvironmentEl.setAttribute("gltf-bundle", `src: ${gltfBundleUrl}`);
       hubChannel.setPhoenixChannel(channel);
     })
diff --git a/src/react-components/footer.js b/src/react-components/footer.js
new file mode 100644
index 0000000000000000000000000000000000000000..3fa68d53f1655ba09f6c42d744c312281ad306fe
--- /dev/null
+++ b/src/react-components/footer.js
@@ -0,0 +1,74 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import FontAwesomeIcon from "@fortawesome/react-fontawesome";
+import faUsers from "@fortawesome/fontawesome-free-solid/faUsers";
+import faEllipsisH from "@fortawesome/fontawesome-free-solid/faEllipsisH";
+import faShareAlt from "@fortawesome/fontawesome-free-solid/faShareAlt";
+import faExclamation from "@fortawesome/fontawesome-free-solid/faExclamation";
+import faTimes from "@fortawesome/fontawesome-free-solid/faTimes";
+
+import styles from "../assets/stylesheets/footer.scss";
+
+export default class Footer extends Component {
+  static propTypes = {
+    hubName: PropTypes.string,
+    occupantCount: PropTypes.number,
+    onClickInvite: PropTypes.func,
+    onClickReport: PropTypes.func
+  };
+  state = {
+    menuVisible: false
+  };
+  render() {
+    const menuVisible = this.state.menuVisible;
+    return (
+      <div className={styles.container}>
+        <div className={styles.header}>
+          <div className={styles.hubInfo}>
+            <span>{this.props.hubName}</span>
+          </div>
+          <button className={styles.menuButton} onClick={() => this.setState({ menuVisible: !menuVisible })}>
+            <i className={styles.menuButtonIcon}>
+              <FontAwesomeIcon icon={menuVisible ? faTimes : faEllipsisH} />
+            </i>
+          </button>
+          <div className={styles.hubStats}>
+            <FontAwesomeIcon icon={faUsers} />
+            <span className={styles.hubParticipantCount}>{this.props.occupantCount || "-"}</span>
+          </div>
+        </div>
+        {menuVisible && (
+          <div className={styles.menu}>
+            <div className={styles.menuHeader}>
+              <div className={styles.hubInfo}>
+                <span>{this.props.hubName}</span>
+              </div>
+              <div className={styles.hubStats}>
+                <FontAwesomeIcon icon={faUsers} />
+                <span className={styles.hubParticipantCount}>{this.props.occupantCount || "-"}</span>
+              </div>
+            </div>
+            <div className={styles.menuButtons}>
+              <button className={styles.menuButton} onClick={this.props.onClickInvite}>
+                <i className={styles.menuButtonIcon}>
+                  <FontAwesomeIcon icon={faShareAlt} />
+                </i>
+                <span className={styles.menuButtonText}>
+                  <strong>Invite</strong> Others
+                </span>
+              </button>
+              <button className={styles.menuButton} onClick={this.props.onClickReport}>
+                <i className={styles.menuButtonIcon}>
+                  <FontAwesomeIcon icon={faExclamation} />
+                </i>
+                <span className={styles.menuButtonText}>
+                  <strong>Report</strong> an Issue
+                </span>
+              </button>
+            </div>
+          </div>
+        )}
+      </div>
+    );
+  }
+}
diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js
index 88d697a2d15ffc1ebfb4528f0e38f6de7134656e..b95b9d58a0bbe692c44943c860b8784b65737e19 100644
--- a/src/react-components/home-root.js
+++ b/src/react-components/home-root.js
@@ -4,9 +4,9 @@ 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";
+import InfoDialog from "./info-dialog.js";
 
 const navigatorLang = (navigator.languages && navigator.languages[0]) || navigator.language || navigator.userLanguage;
 
@@ -50,32 +50,6 @@ class HomeRoot extends Component {
     };
   };
 
-  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 = [];
 
@@ -92,93 +66,11 @@ 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 });
+    const mainContentClassNames = classNames({
+      "main-content": true,
+      "main-content--noninteractive": !!this.state.dialogType
+    });
+    const dialogTypes = InfoDialog.dialogTypes;
 
     return (
       <IntlProvider locale={lang} messages={messages}>
@@ -242,7 +134,7 @@ class HomeRoot extends Component {
                     className="footer-content__links__link"
                     rel="noopener noreferrer"
                     href="#"
-                    onClick={this.showDialog("slack")}
+                    onClick={this.showDialog(dialogTypes.slack)}
                   >
                     <FormattedMessage id="home.join_us" />
                   </a>
@@ -250,7 +142,7 @@ class HomeRoot extends Component {
                     className="footer-content__links__link"
                     rel="noopener noreferrer"
                     href="#"
-                    onClick={this.showDialog("updates")}
+                    onClick={this.showDialog(dialogTypes.updates)}
                   >
                     <FormattedMessage id="home.get_updates" />
                   </a>
@@ -258,7 +150,7 @@ class HomeRoot extends Component {
                     className="footer-content__links__link"
                     rel="noopener noreferrer"
                     href="#"
-                    onClick={this.showDialog("report")}
+                    onClick={this.showDialog(dialogTypes.report)}
                   >
                     <FormattedMessage id="home.report_issue" />
                   </a>
@@ -284,20 +176,11 @@ class HomeRoot extends Component {
             <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>
+            <InfoDialog
+              dialogType={this.state.dialogType}
+              onCloseDialog={() => this.setState({ dialogType: null })}
+              onSubmittedEmail={() => this.setState({ dialogType: dialogTypes.email_submitted })}
+            />
           )}
         </div>
       </IntlProvider>
diff --git a/src/react-components/hub-create-panel.js b/src/react-components/hub-create-panel.js
index 6e689653bcc4129547ec780cacbe220cb9f954da..0b74d55b2698d51f6688e23e02e871177ecea436 100644
--- a/src/react-components/hub-create-panel.js
+++ b/src/react-components/hub-create-panel.js
@@ -7,7 +7,7 @@ import faAngleLeft from "@fortawesome/fontawesome-free-solid/faAngleLeft";
 import faAngleRight from "@fortawesome/fontawesome-free-solid/faAngleRight";
 import FontAwesomeIcon from "@fortawesome/react-fontawesome";
 
-import deafault_scene_preview_thumbnail from "../assets/images/default_thumbnail.png";
+import default_scene_preview_thumbnail from "../assets/images/default_thumbnail.png";
 
 const HUB_NAME_PATTERN = "^[A-Za-z0-9-'\":!@#$%^&*(),.?~ ]{4,64}$";
 
@@ -21,15 +21,34 @@ class HubCreatePanel extends Component {
     super(props);
 
     this.state = {
+      ready: false,
       name: generateHubName(),
       environmentIndex: Math.floor(Math.random() * props.environments.length),
-
       // HACK: expand on small screens by default to ensure scene selection possible.
       // Eventually this could/should be done via media queries.
       expanded: window.innerWidth < 420
     };
+
+    // Optimisticly preload all environment thumbnails
+    (async () => {
+      const environmentThumbnails = props.environments.map((_, i) => this._getEnvironmentThumbnail(i));
+      await Promise.all(
+        environmentThumbnails.map(environmentThumbnail => this._preloadImage(environmentThumbnail.srcset))
+      );
+      this.setState({ ready: true });
+    })();
   }
 
+  _getEnvironmentThumbnail = environmentIndex => {
+    const environment = this.props.environments[environmentIndex];
+    const meta = environment.meta || {};
+    return (
+      (meta.images || []).find(i => i.type === "preview-thumbnail") || {
+        srcset: default_scene_preview_thumbnail
+      }
+    );
+  };
+
   createHub = async e => {
     e.preventDefault();
     const environment = this.props.environments[this.state.environmentIndex];
@@ -64,21 +83,28 @@ class HubCreatePanel extends Component {
     return new RegExp(HUB_NAME_PATTERN).test(this.state.name) && new RegExp(hubAlphaPattern).test(this.state.name);
   };
 
-  setToEnvironmentOffset = offset => {
+  _preloadImage = async srcset => {
+    const img = new Image();
+    const imgLoad = new Promise(resolve => img.addEventListener("load", resolve));
+    img.srcset = srcset;
+    await imgLoad;
+  };
+
+  setToEnvironmentOffset = async offset => {
     const numEnvs = this.props.environments.length;
 
-    this.setState(state => ({
-      environmentIndex: ((state.environmentIndex + offset) % this.props.environments.length + numEnvs) % numEnvs
-    }));
+    const environmentIndex = ((this.state.environmentIndex + offset) % numEnvs + numEnvs) % numEnvs;
+    const environmentThumbnail = this._getEnvironmentThumbnail(environmentIndex);
+    await this._preloadImage(environmentThumbnail.srcset);
+
+    this.setState({ environmentIndex });
   };
 
-  setToNextEnvironment = e => {
-    e.preventDefault();
+  setToNextEnvironment = () => {
     this.setToEnvironmentOffset(1);
   };
 
-  setToPreviousEnvironment = e => {
-    e.preventDefault();
+  setToPreviousEnvironment = () => {
     this.setToEnvironmentOffset(-1);
   };
 
@@ -90,6 +116,7 @@ class HubCreatePanel extends Component {
   };
 
   render() {
+    if (!this.state.ready) return null;
     const { formatMessage } = this.props.intl;
 
     if (this.props.environments.length == 0) {
@@ -101,9 +128,7 @@ class HubCreatePanel extends Component {
 
     const environmentTitle = meta.title || environment.name;
     const environmentAuthor = (meta.authors || [])[0];
-    const environmentThumbnail = (meta.images || []).find(i => i.type === "preview-thumbnail") || {
-      srcset: deafault_scene_preview_thumbnail
-    };
+    const environmentThumbnail = this._getEnvironmentThumbnail(this.state.environmentIndex);
 
     const formNameClassNames = classNames("create-panel__form__name", {
       "create-panel__form__name--expanded": this.state.expanded
@@ -120,17 +145,16 @@ class HubCreatePanel extends Component {
           <div className="create-panel__form">
             <div
               className="create-panel__form__left-container"
-              onClick={e => {
-                e.preventDefault();
-
+              onClick={async () => {
                 if (this.state.expanded) {
                   this.shuffle();
                 } else {
+                  await this._preloadImage(this._getEnvironmentThumbnail(this.state.environmentIndex).srcset);
                   this.setState({ expanded: true });
                 }
               }}
             >
-              <button className="create-panel__form__rotate-button">
+              <button type="button" tabIndex="3" className="create-panel__form__rotate-button">
                 {this.state.expanded ? (
                   <img src="../assets/images/dice_icon.svg" />
                 ) : (
@@ -138,8 +162,8 @@ class HubCreatePanel extends Component {
                 )}
               </button>
             </div>
-            <div className="create-panel__form__right-container" onClick={this.createHub}>
-              <button className="create-panel__form__submit-button">
+            <div className="create-panel__form__right-container">
+              <button type="submit" tabIndex="5" className="create-panel__form__submit-button">
                 {this.isHubNameValid() ? (
                   <img src="../assets/images/hub_create_button_enabled.svg" />
                 ) : (
@@ -184,6 +208,8 @@ class HubCreatePanel extends Component {
                   <div className="create-panel__form__environment__picker__controls">
                     <button
                       className="create-panel__form__environment__picker__controls__prev"
+                      type="button"
+                      tabIndex="1"
                       onClick={this.setToPreviousEnvironment}
                     >
                       <FontAwesomeIcon icon={faAngleLeft} />
@@ -191,6 +217,8 @@ class HubCreatePanel extends Component {
 
                     <button
                       className="create-panel__form__environment__picker__controls__next"
+                      type="button"
+                      tabIndex="2"
                       onClick={this.setToNextEnvironment}
                     >
                       <FontAwesomeIcon icon={faAngleRight} />
@@ -200,6 +228,7 @@ class HubCreatePanel extends Component {
               </div>
             )}
             <input
+              tabIndex="4"
               className={formNameClassNames}
               value={this.state.name}
               onChange={e => this.setState({ name: e.target.value })}
diff --git a/src/react-components/info-dialog.js b/src/react-components/info-dialog.js
new file mode 100644
index 0000000000000000000000000000000000000000..20032dcd92490c381d3eddc3a3b3218707029cd1
--- /dev/null
+++ b/src/react-components/info-dialog.js
@@ -0,0 +1,207 @@
+import React, { Component } from "react";
+import copy from "copy-to-clipboard";
+import PropTypes from "prop-types";
+import { FormattedMessage } from "react-intl";
+import formurlencoded from "form-urlencoded";
+
+// TODO i18n
+
+class InfoDialog extends Component {
+  static dialogTypes = {
+    slack: Symbol("slack"),
+    email_submitted: Symbol("email_submitted"),
+    invite: Symbol("invite"),
+    updates: Symbol("updates"),
+    report: Symbol("report")
+  };
+  static propTypes = {
+    dialogType: PropTypes.oneOf(Object.values(InfoDialog.dialogTypes)),
+    onCloseDialog: PropTypes.func,
+    onSubmittedEmail: PropTypes.func
+  };
+
+  constructor(props) {
+    super(props);
+
+    const loc = document.location;
+    this.shareLink = `${loc.protocol}//${loc.host}${loc.pathname}`;
+  }
+
+  shareLinkClicked = () => {
+    navigator.share({
+      title: document.title,
+      url: this.shareLink
+    });
+  };
+
+  copyLinkClicked = () => {
+    copy(this.shareLink);
+    this.setState({ copyLinkButtonText: "Copied!" });
+  };
+
+  state = {
+    mailingListEmail: "",
+    mailingListPrivacy: false,
+    copyLinkButtonText: "Copy"
+  };
+
+  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.props.onSubmittedEmail);
+  };
+
+  render() {
+    if (!this.props.dialogType) {
+      return <div />;
+    }
+
+    let dialogTitle = null;
+    let dialogBody = null;
+
+    switch (this.props.dialogType) {
+      // TODO i18n, FormattedMessage doesn't play nicely with links
+      case InfoDialog.dialogTypes.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 InfoDialog.dialogTypes.email_submitted:
+        dialogTitle = "";
+        dialogBody = "Great! Please check your e-mail to confirm your subscription.";
+        break;
+      case InfoDialog.dialogTypes.invite:
+        dialogTitle = "Invite Others";
+        dialogBody = (
+          <div>
+            <div>Just share the link to have others join you.</div>
+            <div className="invite-form">
+              <input
+                type="text"
+                readOnly
+                onFocus={e => e.target.select()}
+                value={this.shareLink}
+                className="invite-form__link_field"
+              />
+              <div className="invite-form__buttons">
+                {navigator.share && (
+                  <button className="invite-form__action-button" onClick={this.shareLinkClicked}>
+                    <span>Share</span>
+                  </button>
+                )}
+                <button className="invite-form__action-button" onClick={this.copyLinkClicked}>
+                  <span>{this.state.copyLinkButtonText}</span>
+                </button>
+              </div>
+            </div>
+          </div>
+        );
+        break;
+      case InfoDialog.dialogTypes.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 InfoDialog.dialogTypes.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;
+    }
+
+    return (
+      <div className="dialog-overlay">
+        <div className="dialog">
+          <div className="dialog__box">
+            <div className="dialog__box__contents">
+              <button className="dialog__box__contents__close" onClick={this.props.onCloseDialog}>
+                <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>
+    );
+  }
+}
+
+export default InfoDialog;
diff --git a/src/react-components/profile-entry-panel.js b/src/react-components/profile-entry-panel.js
index 2732f3ca9bd173cc3390bd1af05316b8f6614747..d38ac55e33c5294ccf262db97fbd2b11c5a20b32 100644
--- a/src/react-components/profile-entry-panel.js
+++ b/src/react-components/profile-entry-panel.js
@@ -72,16 +72,14 @@ class ProfileEntryPanel extends Component {
 
     return (
       <div className="profile-entry">
-        <form onSubmit={this.saveStateAndFinish}>
+        <form onSubmit={this.saveStateAndFinish} className="profile-entry__form">
           <div className="profile-entry__box profile-entry__box--darkened">
-            <div className="profile-entry__subtitle">
+            <label htmlFor="#profile-entry-display-name" className="profile-entry__subtitle">
               <FormattedMessage id="profile.header" />
-            </div>
+            </label>
             <label>
-              <span className="profile-entry__display-name-label">
-                <FormattedMessage id="profile.display_name.label" />
-              </span>
               <input
+                id="profile-entry-display-name"
                 className="profile-entry__form-field-text"
                 value={this.state.display_name}
                 onChange={e => this.setState({ display_name: e.target.value })}
diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js
index 512cb62c14750d01c8cd9cbbff7c3bc2b8b9cc6d..16b0b8944bf866f6d82c757d77bed73f8459e956 100644
--- a/src/react-components/ui-root.js
+++ b/src/react-components/ui-root.js
@@ -12,7 +12,9 @@ import AutoExitWarning from "./auto-exit-warning";
 import { TwoDEntryButton, GenericEntryButton, GearVREntryButton, DaydreamEntryButton } from "./entry-buttons.js";
 import { ProfileInfoHeader } from "./profile-info-header.js";
 import ProfileEntryPanel from "./profile-entry-panel";
+import InfoDialog from "./info-dialog.js";
 import TwoDHUD from "./2d-hud";
+import Footer from "./footer";
 
 const mobiledetect = new MobileDetect(navigator.userAgent);
 
@@ -65,12 +67,15 @@ class UIRoot extends Component {
     availableVREntryTypes: PropTypes.object,
     initialEnvironmentLoaded: PropTypes.bool,
     janusRoomId: PropTypes.number,
-    roomUnavailableReason: PropTypes.string
+    roomUnavailableReason: PropTypes.string,
+    hubName: PropTypes.string,
+    occupantCount: PropTypes.number
   };
 
   state = {
     entryStep: ENTRY_STEPS.start,
     enterInVR: false,
+    infoDialogType: null,
 
     shareScreen: false,
     requestedScreen: false,
@@ -760,7 +765,7 @@ class UIRoot extends Component {
       "ui-dialog--darkened": this.state.entryStep !== ENTRY_STEPS.finished
     });
 
-    const dialogBoxClassNames = classNames("ui-interactive", "ui-dialog-box");
+    const dialogBoxClassNames = classNames({ "ui-interactive": !this.state.infoDialogType, "ui-dialog-box": true });
 
     const dialogBoxContentsClassNames = classNames({
       "ui-dialog-box-contents": true,
@@ -770,6 +775,11 @@ class UIRoot extends Component {
     return (
       <IntlProvider locale={lang} messages={messages}>
         <div className="ui">
+          <InfoDialog
+            dialogType={this.state.infoDialogType}
+            onCloseDialog={() => this.setState({ infoDialogType: null })}
+          />
+
           <div className={dialogClassNames}>
             {(this.state.entryStep !== ENTRY_STEPS.finished || this.isWaitingForAutoExit()) && (
               <div className={dialogBoxClassNames}>
@@ -786,7 +796,15 @@ class UIRoot extends Component {
             )}
           </div>
           {this.state.entryStep === ENTRY_STEPS.finished ? (
-            <TwoDHUD muted={this.state.muted} onToggleMute={this.toggleMute} />
+            <div>
+              <TwoDHUD muted={this.state.muted} onToggleMute={this.toggleMute} />
+              <Footer
+                hubName={this.props.hubName}
+                occupantCount={this.props.occupantCount}
+                onClickInvite={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.invite })}
+                onClickReport={() => this.setState({ infoDialogType: InfoDialog.dialogTypes.report })}
+              />
+            </div>
           ) : null}
         </div>
       </IntlProvider>
diff --git a/webpack.config.js b/webpack.config.js
index 55bae82132e8d8611ba8b01ffcddf1ac49109c4b..3a70b848d2f763d1ff8b37d8a698fc682f7a54d4 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -141,7 +141,9 @@ const config = {
               loader: "css-loader",
               options: {
                 name: "[path][name]-[hash].[ext]",
-                minimize: process.env.NODE_ENV === "production"
+                minimize: process.env.NODE_ENV === "production",
+                localIdentName: "[name]__[local]__[hash:base64:5]",
+                camelCase: true
               }
             },
             "sass-loader"
@@ -156,7 +158,9 @@ const config = {
             loader: "css-loader",
             options: {
               name: "[path][name]-[hash].[ext]",
-              minimize: process.env.NODE_ENV === "production"
+              minimize: process.env.NODE_ENV === "production",
+              localIdentName: "[name]__[local]__[hash:base64:5]",
+              camelCase: true
             }
           }
         })
diff --git a/yarn.lock b/yarn.lock
index d42ec6a46cdd0953f94855b7cea8a59c3bc3e2ad..63079940bac14f9025e01f590ec2de5b7eca9710 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2170,6 +2170,12 @@ copy-descriptor@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
 
+copy-to-clipboard@^3.0.8:
+  version "3.0.8"
+  resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz#f4e82f4a8830dce4666b7eb8ded0c9bcc313aba9"
+  dependencies:
+    toggle-selection "^1.0.3"
+
 core-js@^1.0.0:
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
@@ -7913,6 +7919,10 @@ to-regex@^3.0.1:
     extend-shallow "^2.0.1"
     regex-not "^1.0.0"
 
+toggle-selection@^1.0.3:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
+
 toposort@^1.0.0:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.6.tgz#c31748e55d210effc00fdcdc7d6e68d7d7bb9cec"