diff --git a/src/assets/stylesheets/index.scss b/src/assets/stylesheets/index.scss
index 014ca0a7a7f7f32e20db86d9a4725da7beb5d011..84e67fb075f83ea1d303cfbce7e4ba7b0d4ea8b9 100644
--- a/src/assets/stylesheets/index.scss
+++ b/src/assets/stylesheets/index.scss
@@ -2,6 +2,8 @@
 @import 'hub-create';
 @import 'info-dialog';
 
+$title-width: 350px;
+
 * {
   box-sizing: border-box;
 }
@@ -53,20 +55,24 @@ body {
 
 .header-content {
   padding: 1.5em 2.5em 1.5em 2.5em;
-  background-color: rgba(0, 0, 0, 0.85);
+  background-color: $darkest-transparent;
   min-height: 90px;
   height: 90px;
   display: flex;
-  border-bottom: 2px solid #242424;
+  border-bottom: 1px solid $darkest-grey;
 
   &__title {
-    flex: 10;
     display: flex;
+    width: $title-width;
 
     @media (max-width: 768px) {
       justify-content: center;
     }
 
+    @media (max-width: 1024px) {
+      flex: 1 1 350px;
+    }
+
     &__name {
       width: 200px;
     }
@@ -77,12 +83,32 @@ body {
     }
   }
 
+  &__entry-code {
+    @media (max-width: 1024px) {
+      display: none;
+    }
+
+    flex: 10;
+    text-align: center;
+    display: flex;
+    flex-direction: row;
+    justify-content: center;
+    align-items: center;
+
+    &__link {
+      color: white;
+      text-decoration-color: $light-grey;
+    }
+  }
+
   &__experiment {
     text-align: right;
     flex: 1 1 350px;
+    width: 350px;
     color: $grey-text;
     font-size: 1.0em;
     font-weight: lighter;
+    white-space: nowrap;
 
     @media (max-width: 768px) {
       display: none;
@@ -117,6 +143,26 @@ body {
   }
 }
 
+.header-subtitle {
+  @media (min-width: 1024px) {
+    display: none;
+  }
+
+  padding: 8px;
+  background-color: $darkest-transparent;
+  text-align: center;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  border-bottom: 2px solid $darkest-grey;
+
+  &__link {
+    color: white;
+    text-decoration-color: $light-grey;
+  }
+}
+
 .hero-content {
   flex: 10;
   min-height: 740px;
@@ -208,7 +254,7 @@ body {
 
 .footer-content {
   padding: 1em 2.25em 1em 2.25em;
-  background-color: rgba(0, 0, 0, 0.85);
+  background-color: $darkest-transparent;
   min-height: 80px;
   display: flex;
   border-top: 2px solid #242424;
diff --git a/src/assets/stylesheets/link.scss b/src/assets/stylesheets/link.scss
index 63da92c9709fa72367461af1006d85de03e4873a..b94325c5039062d7848a4d254df9d27b88a7ceb4 100644
--- a/src/assets/stylesheets/link.scss
+++ b/src/assets/stylesheets/link.scss
@@ -108,6 +108,7 @@ a {
   display: grid;
   grid-template-columns: 1fr 1fr 1fr;
   grid-template-rows: 1fr 1fr 1fr 1fr;
+  text-align: center;
 }
 
 :local(.keypad-button) {
@@ -116,8 +117,8 @@ a {
   font-family: sans-serif;
   border: 4px $light-grey solid;
   border-radius: 128px;
-  min-width: 88px;
-  min-height: 88px;
+  min-width: 80px;
+  min-height: 80px;
   cursor: pointer;
   line-height: 68px;
   margin: 8px;
@@ -170,7 +171,7 @@ a {
   margin: 0;
   font-size: 64pt;
   border: 0;
-  width: 225px;
+  width: 295px;
   letter-spacing: 0.08em;
   text-align: center;
 }
diff --git a/src/assets/stylesheets/shared.scss b/src/assets/stylesheets/shared.scss
index 18944a4080dd096f4f6d917e61d3834cb05ab04a..e54447f58f853687b1f3f3d2be6d136d95df01c1 100644
--- a/src/assets/stylesheets/shared.scss
+++ b/src/assets/stylesheets/shared.scss
@@ -1,11 +1,12 @@
 $dark-transparent: rgba(0, 0, 0, 0.4);
 $darker-transparent: rgba(0, 0, 0, 0.6);
-$darkest-transparent: rgba(0, 0, 0, 0.95);
+$darkest-transparent: rgba(0, 0, 0, 0.9);
 $grey-text: rgba(192, 192, 192, 1.0);
 $light-text: rgba(240, 240, 240, 1.0);
 $light-grey: lightgrey;
 $dark-grey: rgba(128, 128, 128, 1.0);
 $darker-grey: rgba(64, 64, 64, 1.0);
+$darkest-grey: rgba(32, 32, 32, 1.0);
 
 %default-font {
   font-family: 'Zilla Slab', sans-serif;
diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json
index c6ef31c5c944ea85568fb6fe1cded6c47e0341b7..0201cf0fca4a81f60217ab97679f6f2e7316fc43 100644
--- a/src/assets/translations.data.json
+++ b/src/assets/translations.data.json
@@ -13,6 +13,7 @@
     "entry.device-medium": "Device",
     "entry.device-subtitle-desktop": "Standalone Headset or Phone",
     "entry.device-subtitle-mobile": "Mobile Headset or PC",
+    "entry.device-subtitle-vr": "Phone or PC",
     "entry.cardboard": "Enter on Google Cardboard",
     "entry.daydream-prefix": "Enter on ",
     "entry.daydream-medium": "Daydream",
@@ -59,10 +60,11 @@
     "home.made_with_love": "made with 🦆 by ",
     "home.environment_author_by": " by ",
     "home.dialog.close": "CLOSE",
+    "home.have_entry_code": "Have a link code?",
     "mailing_list.privacy_label": "I'm okay with Mozilla handling my info as explained in",
     "mailing_list.privacy_link": "this Privacy Notice",
     "link.in_your_browser": "In your device's browser, go to:",
-    "link.enter_code": "Then, enter this code:",
+    "link.enter_code": "Then, enter this link code:",
     "link.do_not_close": "Keep this dialog open to use this code.",
     "link.link_page_header": "Enter your code:",
     "link.dont_have_a_code": "Don't have a code?",
diff --git a/src/hub.js b/src/hub.js
index f85a4980f3dd50dbda64fc368627d77fe42c0d1a..8bd8e2a29f9dbe957b0c20cd2798c160fbe7ede0 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -69,6 +69,7 @@ import UIRoot from "./react-components/ui-root";
 import HubChannel from "./utils/hub-channel";
 import LinkChannel from "./utils/link-channel";
 import { connectToReticulum } from "./utils/phoenix-utils";
+import { disableiOSZoom } from "./utils/disable-ios-zoom";
 
 import "./systems/personal-space-bubble";
 import "./systems/app-mode";
@@ -133,6 +134,8 @@ if (!isBotMode) {
   registerTelemetry();
 }
 
+disableiOSZoom();
+
 AFRAME.registerInputBehaviour("trackpad_dpad4", trackpad_dpad4);
 AFRAME.registerInputBehaviour("joystick_dpad4", joystick_dpad4);
 AFRAME.registerInputBehaviour("msft_mr_axis_with_deadzone", msft_mr_axis_with_deadzone);
diff --git a/src/link.html b/src/link.html
index 8f44654c0562b9ce2afd5b3bb95f482fffc213ab..954061491faa3b257adedf87df06a26cfc58f79b 100644
--- a/src/link.html
+++ b/src/link.html
@@ -7,6 +7,7 @@
     <link rel="shortcut icon" type="image/png" href="/favicon.ico"/>
     <title>Enter Code | Hubs by Mozilla</title>
     <link href="https://fonts.googleapis.com/css?family=Zilla+Slab:300,300i,400,400i,700" rel="stylesheet">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
 </head>
 
 <body>
diff --git a/src/react-components/entry-buttons.js b/src/react-components/entry-buttons.js
index 90dd40cfd23724418228cc7bb3b46fe45dc8e66f..9e8302b3c7bbf9bd44edbaf6538100f11abf3fb8 100644
--- a/src/react-components/entry-buttons.js
+++ b/src/react-components/entry-buttons.js
@@ -38,7 +38,8 @@ EntryButton.propTypes = {
   iconSrc: PropTypes.string,
   prefixMessageId: PropTypes.string,
   mediumMessageId: PropTypes.string,
-  subtitle: PropTypes.string
+  subtitle: PropTypes.string,
+  isInHMD: PropTypes.bool
 };
 
 export const TwoDEntryButton = props => {
@@ -91,9 +92,12 @@ export const DeviceEntryButton = props => {
     ...props,
     iconSrc: DeviceEntryImg,
     prefixMessageId: mobiledetect.mobile() ? "entry.device-prefix-mobile" : "entry.device-prefix-desktop",
-    mediumMessageId: "entry.device-medium",
-    subtitle: mobiledetect.mobile() ? "entry.device-subtitle-mobile" : "entry.device-subtitle-desktop"
+    mediumMessageId: "entry.device-medium"
   };
 
+  entryButtonProps.subtitle = entryButtonProps.isInHMD
+    ? "entry.device-subtitle-vr"
+    : mobiledetect.mobile() ? "entry.device-subtitle-mobile" : "entry.device-subtitle-desktop";
+
   return <EntryButton {...entryButtonProps} />;
 };
diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js
index df85b425646d1b9a0eae4f216ba3c00cae6dfdc8..7c9a502446fd8587bf063d68977cfc644cec91e4 100644
--- a/src/react-components/home-root.js
+++ b/src/react-components/home-root.js
@@ -99,6 +99,11 @@ class HomeRoot extends Component {
                 <img className="header-content__title__name" src="../assets/images/logo.svg" />
                 <div className="header-content__title__preview">preview</div>
               </div>
+              <div className="header-content__entry-code">
+                <a className="header-content__entry-code__link" href="/link" rel="nofollow">
+                  <FormattedMessage id="home.have_entry_code" />
+                </a>
+              </div>
               <div className="header-content__experiment">
                 <div className="header-content__experiment__container">
                   <img src="../assets/images/webvr_cube.svg" className="header-content__experiment__icon" />
@@ -132,6 +137,13 @@ class HomeRoot extends Component {
                 </div>
               </div>
             </div>
+            <div className="header-subtitle">
+              <div>
+                <a className="header-subtitle__link" href="/link" rel="nofollow">
+                  <FormattedMessage id="home.have_entry_code" />
+                </a>
+              </div>
+            </div>
             <div className="hero-content">
               <div className="hero-content__attribution">
                 Medieval Fantasy Book by{" "}
diff --git a/src/react-components/link-root.js b/src/react-components/link-root.js
index 1ab8aa47829ad5181f3796b72502d3f587241b55..66ab987b6493a0cb99ab0742eb966d074ffe5ad5 100644
--- a/src/react-components/link-root.js
+++ b/src/react-components/link-root.js
@@ -6,10 +6,12 @@ import en from "react-intl/locale-data/en";
 import { lang, messages } from "../utils/i18n";
 import classNames from "classnames";
 import styles from "../assets/stylesheets/link.scss";
+import { disableiOSZoom } from "../utils/disable-ios-zoom";
 
 const MAX_DIGITS = 4;
 
 addLocaleData([...en]);
+disableiOSZoom();
 
 class LinkRoot extends Component {
   static propTypes = {
@@ -78,12 +80,17 @@ class LinkRoot extends Component {
         }
       })
       .catch(e => {
-        console.error(e);
         this.setState({ failedAtLeastOnce: true, enteredDigits: "" });
+
+        if (!(e instanceof Error && (e.message === "in_use" || e.message === "failed"))) {
+          throw e;
+        }
       });
   };
 
   render() {
+    // Note we use type "tel" for the input due to https://bugzilla.mozilla.org/show_bug.cgi?id=1005603
+
     return (
       <IntlProvider locale={lang} messages={messages}>
         <div className={styles.link}>
@@ -106,7 +113,8 @@ class LinkRoot extends Component {
               <div className={styles.enteredDigits}>
                 <input
                   className={styles.digitInput}
-                  type="number"
+                  type="tel"
+                  pattern="[0-9]*"
                   value={this.state.enteredDigits}
                   onChange={ev => {
                     this.setState({ enteredDigits: ev.target.value });
diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js
index 803c28acded3354c4065be0fa380424e0b7c2237..cb69c63436f8d9d72479fc10e9ce541d5fde2ceb 100644
--- a/src/react-components/ui-root.js
+++ b/src/react-components/ui-root.js
@@ -612,7 +612,9 @@ class UIRoot extends Component {
       this.state.entryStep === ENTRY_STEPS.start ? (
         <div className="entry-panel">
           <div className="entry-panel__button-container">
-            <TwoDEntryButton onClick={this.enter2D} />
+            {this.props.availableVREntryTypes.screen !== VR_DEVICE_AVAILABILITY.no && (
+              <TwoDEntryButton onClick={this.enter2D} />
+            )}
             {this.props.availableVREntryTypes.generic !== VR_DEVICE_AVAILABILITY.no && (
               <GenericEntryButton onClick={this.enterVR} />
             )}
@@ -626,7 +628,7 @@ class UIRoot extends Component {
                 }
               />
             )}
-            <DeviceEntryButton onClick={this.attemptLink} />
+            <DeviceEntryButton onClick={this.attemptLink} isInHMD={this.props.availableVREntryTypes.isInHMD} />
             {this.props.availableVREntryTypes.cardboard !== VR_DEVICE_AVAILABILITY.no && (
               <div className="entry-panel__secondary" onClick={this.enterVR}>
                 <FormattedMessage id="entry.cardboard" />
diff --git a/src/utils/disable-ios-zoom.js b/src/utils/disable-ios-zoom.js
new file mode 100644
index 0000000000000000000000000000000000000000..c2d17104a6ae4b9b8c99221fe5bec8a8f79cc909
--- /dev/null
+++ b/src/utils/disable-ios-zoom.js
@@ -0,0 +1,24 @@
+import MobileDetect from "mobile-detect";
+const mobiledetect = new MobileDetect(navigator.userAgent);
+
+export function disableiOSZoom() {
+  if (!mobiledetect.is("iPhone") && !mobiledetect.is("iPad")) return;
+
+  let lastTouchAtMs = 0;
+
+  document.addEventListener("touchmove", ev => {
+    if (ev.scale === 1) return;
+
+    ev.preventDefault();
+  });
+
+  document.addEventListener("touchend", ev => {
+    const now = new Date().getTime();
+    const isDoubleTouch = now - lastTouchAtMs <= 300;
+    lastTouchAtMs = now;
+
+    if (isDoubleTouch) {
+      ev.preventDefault();
+    }
+  });
+}
diff --git a/src/utils/vr-caps-detect.js b/src/utils/vr-caps-detect.js
index 79c425da26428b227daa71890b443f913640abc2..3ab8f4370638e49ccea8389f99e14bdac2c1db43 100644
--- a/src/utils/vr-caps-detect.js
+++ b/src/utils/vr-caps-detect.js
@@ -35,15 +35,19 @@ const GENERIC_ENTRY_TYPE_DEVICE_BLACKLIST = [/cardboard/i];
 // Once in a compatible browser, we should assume it will work (if it doesn't, it's because they don't have the headset,
 // haven't installed the software, our guess about their phone was wrong, etc.)
 //
-// At the time of this writing there are three VR "entry types" that will be validated by this method:
+// At the time of this writing there are the following VR "entry types" that will be validated by this method:
 //
+// - screen: Enter "on-screen" in 2D
 // - generic: Generic WebVR (platform/OS agnostic indicator if a general 'Enter VR' option should be presented.)
 // - daydream: Google Daydream
 // - gearvr: Oculus GearVR
+// - cardboard: Google Cardboard
 //
+// This function also detects if the user is already in a headset, and returns the isInHMD key to be `true` if so.
 export async function getAvailableVREntryTypes() {
   const isSamsungBrowser = browser.name === "chrome" && navigator.userAgent.match(/SamsungBrowser/);
   const isOculusBrowser = navigator.userAgent.match(/Oculus/);
+  const isInHMD = isOculusBrowser;
 
   // This needs to be kept up-to-date with the latest browsers that can support VR and Hubs.
   // Checking for navigator.getVRDisplays always passes b/c of polyfill.
@@ -51,6 +55,7 @@ export async function getAvailableVREntryTypes() {
 
   const isDaydreamCapableBrowser = !!(isWebVRCapableBrowser && browser.name === "chrome" && !isSamsungBrowser);
 
+  const screen = isInHMD ? VR_DEVICE_AVAILABILITY.no : VR_DEVICE_AVAILABILITY.yes;
   let generic = mobiledetect.mobile() ? VR_DEVICE_AVAILABILITY.no : VR_DEVICE_AVAILABILITY.maybe;
   let cardboard = VR_DEVICE_AVAILABILITY.no;
 
@@ -65,7 +70,8 @@ export async function getAvailableVREntryTypes() {
 
   // For daydream detection, we first check if they are on an Android compatible device, and assume they
   // may support daydream *unless* this browser has WebVR capabilities, in which case we can do better.
-  let daydream = isMaybeDaydreamCompatibleDevice() ? VR_DEVICE_AVAILABILITY.maybe : VR_DEVICE_AVAILABILITY.no;
+  let daydream =
+    isMaybeDaydreamCompatibleDevice() && !isInHMD ? VR_DEVICE_AVAILABILITY.maybe : VR_DEVICE_AVAILABILITY.no;
 
   if (isWebVRCapableBrowser) {
     const displays = await navigator.getVRDisplays();
@@ -94,5 +100,5 @@ export async function getAvailableVREntryTypes() {
     }
   }
 
-  return { generic, gearvr, daydream, cardboard };
+  return { screen, generic, gearvr, daydream, cardboard, isInHMD };
 }