Open demo

CSS code

HTML
<main class="ap">
  <header class="ap-top">
    <a class="ap-logo" href="#"><svg width="28" height="28" viewBox="0 0 28 28" aria-hidden="true"><rect width="28" height="28" rx="9" fill="#1e6b3a"/><path d="M14 22v-8m0 0c0-4 3-6 7-6 0 4-3 6-7 6zm0 2c0-3-2.5-5-6-5 0 3 2.5 5 6 5z" fill="#dcf0d3" stroke="#dcf0d3" stroke-width="1.5" stroke-linejoin="round"/></svg>Sprout</a>
    <p>Tested by 640 beta gardeners</p>
  </header>
  <div class="ap-grid">
    <section aria-labelledby="ap-title">
      <p class="ap-chip">iPhone and Android · Spring 2027</p>
      <h1 class="ap-title" id="ap-title">Never guess when to water again.</h1>
      <p class="ap-lead">Sprout learns each plant's light, pot and season, then tells you what it needs today. Snap a leaf to catch pests and yellowing before they spread. Free, with an optional Plus plan.</p>
      <ul class="ap-badges" aria-label="Launch platforms">
        <li class="ap-badge"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><rect x="6" y="2" width="12" height="20" rx="3"/><path d="M11 18h2"/></svg><span><small>Coming soon to the</small>App Store</span></li>
        <li class="ap-badge"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><rect x="6" y="2" width="12" height="20" rx="3"/><path d="M11 18h2"/></svg><span><small>Coming soon to</small>Google Play</span></li>
      </ul>
      <form class="ap-form" data-signup="ap-done" action="#" method="post">
        <label for="ap-email">Email me on launch day</label>
        <div class="ap-field">
          <input id="ap-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required aria-describedby="ap-err">
          <button type="submit">Remind me</button>
        </div>
        <p class="ap-err" id="ap-err" data-error aria-live="polite"></p>
      </form>
      <div class="ap-done" id="ap-done" tabindex="-1" hidden><p><strong>Reminder set.</strong> You will get one email with both store links on launch day.</p></div>
    </section>
    <div class="ap-phone-wrap">
      <div class="ap-phone" role="img" aria-label="Sprout app preview: today's care list for a Monstera, a fiddle-leaf fig and a Calathea">
        <div class="ap-screen">
          <span class="ap-notch"></span>
          <p class="ap-hi">Good morning, Maya</p>
          <p class="ap-h">Today · 3 tasks</p>
          <div class="ap-task"><span class="ap-pot"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3s6 6.5 6 11a6 6 0 0 1-12 0c0-4.5 6-11 6-11z"/></svg></span><span><b>Monstera</b><small>Water 250 ml</small></span><span class="ap-tick on"></span></div>
          <div class="ap-task"><span class="ap-pot"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1.5 1.5M17.5 17.5 19 19M5 19l1.5-1.5M17.5 6.5 19 5"/></svg></span><span><b>Fiddle-leaf fig</b><small>Turn towards the window</small></span><span class="ap-tick"></span></div>
          <div class="ap-task"><span class="ap-pot"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 14c2 0 2-2 4-2s2 2 4 2 2-2 4-2 2 2 4 2M4 19c2 0 2-2 4-2s2 2 4 2 2-2 4-2 2 2 4 2M12 3v5"/></svg></span><span><b>Calathea</b><small>Mist the leaves</small></span><span class="ap-tick"></span></div>
          <div class="ap-stat"><small>Watering this week</small><b>1.2 litres, all on time</b><div class="ap-bars"><i style="height:40%"></i><i style="height:70%"></i><i style="height:30%"></i><i style="height:90%"></i><i style="height:55%"></i><i style="height:75%"></i><i style="height:45%"></i></div></div>
          <div class="ap-tabbar"><i></i><i></i><i></i><i></i></div>
        </div>
      </div>
    </div>
  </div>
</main>
CSS
/* App pre-launch page: pitch, text-only "coming soon" store badges (no store logos), a launch-day email form and a CSS phone mock-up. */
.ap{--ap-bg:#eef5e9;--ap-ink:#13261b;--ap-muted:#435a4a;--ap-accent:#1e6b3a;--ap-accent-hover:#175530;--ap-line:#7d9483;--ap-err:#a52a1f;
  position:relative;line-height:1.5;min-height:100vh;min-height:100dvh;display:flex;flex-direction:column;padding:26px clamp(20px,5vw,56px);overflow:hidden;
  color:var(--ap-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  background:radial-gradient(40% 55% at 82% 50%,#cfe8c2 0,transparent 70%),var(--ap-bg)}
.ap [hidden]{display:none!important}
.ap a{color:inherit}
.ap a:focus-visible,.ap button:focus-visible,.ap input:focus-visible{outline:2px solid var(--ap-accent);outline-offset:3px}
.ap-top,.ap-grid{width:100%;max-width:1120px;margin:0 auto}
.ap-top{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
.ap-logo{display:inline-flex;align-items:center;gap:9px;font-weight:800;font-size:1.15rem;letter-spacing:-.02em;text-decoration:none}
.ap-top p{margin:0;font-size:.88rem;color:var(--ap-muted)}
.ap-grid{flex:1;display:grid;grid-template-columns:minmax(0,7fr) minmax(0,5fr);gap:40px;align-items:center;padding:36px 0}
.ap-chip{display:inline-flex;align-items:center;gap:8px;margin:0 0 18px;padding:6px 12px;font-size:.8rem;font-weight:700;color:var(--ap-accent);background:#fff;border-radius:999px;box-shadow:0 2px 10px -4px rgba(19,38,27,.2)}
.ap-chip::before{content:"";width:7px;height:7px;border-radius:50%;background:#22a352}
.ap-title{margin:0 0 16px;font-size:clamp(2.3rem,5.4vw,3.9rem);font-weight:800;line-height:1.02;letter-spacing:-.045em}
.ap-lead{max-width:500px;margin:0 0 26px;font-size:1.06rem;line-height:1.6;color:var(--ap-muted)}
.ap-badges{display:flex;flex-wrap:wrap;gap:10px;margin:0 0 26px;padding:0;list-style:none}
.ap-badge{display:inline-flex;align-items:center;gap:10px;padding:9px 18px 9px 14px;color:#fff;background:var(--ap-ink);border-radius:12px}
.ap-badge span{display:flex;flex-direction:column;font-size:1.02rem;font-weight:700;line-height:1.15}
.ap-badge small{font-size:.66rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:#cfe0d3}
.ap-form{max-width:460px}
.ap-form label{display:block;margin:0 0 8px;font-size:.92rem;font-weight:700}
.ap-field{display:flex;gap:8px}
.ap-field input{flex:1;min-width:0;height:50px;padding:0 16px;font:inherit;color:var(--ap-ink);background:#fff;border:1px solid var(--ap-line);border-radius:12px}
.ap-field input::placeholder{color:#62786a}
.ap-field input[aria-invalid="true"]{border-color:var(--ap-err);box-shadow:0 0 0 1px var(--ap-err)}
.ap-field button{flex:none;height:50px;padding:0 22px;font:inherit;font-weight:700;color:#fff;background:var(--ap-accent);border:0;border-radius:12px;cursor:pointer;transition:background-color .2s}
.ap-field button:hover{background:var(--ap-accent-hover)}
.ap-err{min-height:1.4em;margin:8px 0 0;font-size:.88rem;color:var(--ap-err)}
.ap-done{max-width:460px;padding:16px 18px;background:#fff;border:1px solid #b9d6b3;border-radius:14px;line-height:1.5}
.ap-done:focus{outline:none}
.ap-done p{margin:0}
.ap-phone-wrap{display:grid;place-items:center}
.ap-phone{width:270px;padding:10px;background:#13261b;border-radius:44px;box-shadow:0 40px 70px -30px rgba(19,38,27,.55),inset 0 0 0 2px #2c4434;transform:rotate(4deg)}
.ap-screen{position:relative;display:flex;flex-direction:column;gap:9px;height:520px;padding:44px 16px 16px;overflow:hidden;background:#f7faf5;border-radius:35px}
.ap-notch{position:absolute;top:10px;left:50%;width:84px;height:24px;background:#13261b;border-radius:999px;transform:translateX(-50%)}
.ap-hi{margin:0;font-size:.72rem;color:#4f6656}
.ap-h{margin:0 0 4px;font-size:1.2rem;font-weight:800;letter-spacing:-.02em}
.ap-task{display:flex;align-items:center;gap:10px;padding:10px;background:#fff;border-radius:16px;box-shadow:0 1px 3px rgba(19,38,27,.1)}
.ap-task b{display:block;font-size:.8rem}
.ap-task small{display:block;font-size:.68rem;color:#4f6656}
.ap-pot{flex:none;display:grid;place-items:center;width:34px;height:34px;border-radius:11px;background:#dcf0d3;color:#1e6b3a}
.ap-tick{flex:none;width:20px;height:20px;margin-left:auto;border:2px solid #9bb3a2;border-radius:50%}
.ap-tick.on{background:#1e6b3a url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 12.5 4.5 4.5L19 7.5'/%3E%3C/svg%3E") center/11px no-repeat;border-color:#1e6b3a}
.ap-stat{margin-top:4px;padding:12px;color:#fff;background:#1e6b3a;border-radius:16px}
.ap-stat small{font-size:.66rem;color:#d3ead8}
.ap-stat b{display:block;margin:2px 0 8px;font-size:.95rem}
.ap-bars{display:flex;align-items:flex-end;gap:5px;height:44px}
.ap-bars i{flex:1;background:#8fd19e;border-radius:4px 4px 2px 2px}
.ap-tabbar{display:flex;justify-content:space-around;margin-top:auto;padding:10px 0 0;border-top:1px solid #e1eadd}
.ap-tabbar i{width:20px;height:20px;border-radius:6px;background:#c7d8cb}
.ap-tabbar i:first-child{background:#1e6b3a}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
@media (max-width:991.98px){.ap-grid{grid-template-columns:1fr}.ap-phone{transform:none}}
@media (max-width:480px){.ap-phone{width:240px}.ap-screen{height:460px}}
@media (max-width:575.98px){.ap-field{flex-direction:column}.ap-field input{flex:none}.ap-field button{width:100%}}
@media (prefers-reduced-motion:reduce){.ap *{transition:none}}
JavaScript
// Sign-up form, front end only: it checks the address and shows the confirmation.
// Nothing is sent. Point the form's action at your mailing-list provider, or post it with fetch() below.
document.querySelectorAll('form[data-signup]').forEach(function (form) {
  var input = form.querySelector('input[type="email"]');
  var error = form.querySelector('[data-error]');
  var done = document.getElementById(form.getAttribute('data-signup'));
  form.noValidate = true; // our message replaces the browser bubble; without JS, native validation still works
  function clear() { error.textContent = ''; input.removeAttribute('aria-invalid'); }
  input.addEventListener('input', clear);
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var value = input.value.trim();
    if (!value || !input.checkValidity()) {
      error.textContent = value ? 'Check the address: it needs an @ and a domain, like [email protected].' : 'Enter your email address.';
      input.setAttribute('aria-invalid', 'true');
      input.focus();
      return;
    }
    clear();
    form.hidden = true;
    done.hidden = false;
    done.focus();
  });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<main class="ap d-flex flex-column overflow-hidden">
  <header class="ap-wide w-100 mx-auto d-flex flex-wrap align-items-center justify-content-between gap-3">
    <a class="ap-logo d-inline-flex align-items-center gap-2 fw-bolder" href="#"><svg width="28" height="28" viewBox="0 0 28 28" aria-hidden="true"><rect width="28" height="28" rx="9" fill="#1e6b3a"/><path d="M14 22v-8m0 0c0-4 3-6 7-6 0 4-3 6-7 6zm0 2c0-3-2.5-5-6-5 0 3 2.5 5 6 5z" fill="#dcf0d3" stroke="#dcf0d3" stroke-width="1.5" stroke-linejoin="round"/></svg>Sprout</a>
    <p class="small text-body-secondary m-0">Tested by 640 beta gardeners</p>
  </header>
  <div class="ap-wide w-100 mx-auto flex-grow-1 d-flex flex-column justify-content-center py-4">
  <div class="row align-items-center gx-4 gy-4">
    <section class="col-lg-7" aria-labelledby="ap-title">
      <p class="ap-chip badge rounded-pill d-inline-flex align-items-center gap-2 mb-3">iPhone and Android · Spring 2027</p>
      <h1 class="ap-title fw-bolder mb-3" id="ap-title">Never guess when to water again.</h1>
      <p class="ap-lead text-body-secondary mb-4">Sprout learns each plant's light, pot and season, then tells you what it needs today. Snap a leaf to catch pests and yellowing before they spread. Free, with an optional Plus plan.</p>
      <ul class="list-unstyled d-flex flex-wrap gap-2 mb-4" aria-label="Launch platforms">
        <li class="ap-badge d-inline-flex align-items-center gap-2 text-white"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><rect x="6" y="2" width="12" height="20" rx="3"/><path d="M11 18h2"/></svg><span class="d-flex flex-column fw-bold"><small class="fw-semibold text-uppercase">Coming soon to the</small>App Store</span></li>
        <li class="ap-badge d-inline-flex align-items-center gap-2 text-white"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><rect x="6" y="2" width="12" height="20" rx="3"/><path d="M11 18h2"/></svg><span class="d-flex flex-column fw-bold"><small class="fw-semibold text-uppercase">Coming soon to</small>Google Play</span></li>
      </ul>
      <form class="ap-form" data-signup="ap-done" action="#" method="post">
        <label class="form-label fw-bold" for="ap-email">Email me on launch day</label>
        <div class="d-flex flex-column flex-sm-row gap-2">
          <input class="form-control" id="ap-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required aria-describedby="ap-err">
          <button class="btn btn-ap flex-shrink-0" type="submit">Remind me</button>
        </div>
        <p class="ap-err small mt-2 mb-0" id="ap-err" data-error aria-live="polite"></p>
      </form>
      <div class="ap-done bg-white px-3 py-3" id="ap-done" tabindex="-1" hidden><p class="m-0"><strong>Reminder set.</strong> You will get one email with both store links on launch day.</p></div>
    </section>
    <div class="ap-phone-wrap col-lg-5">
      <div class="ap-phone" role="img" aria-label="Sprout app preview: today's care list for a Monstera, a fiddle-leaf fig and a Calathea">
        <div class="ap-screen">
          <span class="ap-notch"></span>
          <p class="ap-hi">Good morning, Maya</p>
          <p class="ap-h">Today · 3 tasks</p>
          <div class="ap-task"><span class="ap-pot"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3s6 6.5 6 11a6 6 0 0 1-12 0c0-4.5 6-11 6-11z"/></svg></span><span><b>Monstera</b><small>Water 250 ml</small></span><span class="ap-tick on"></span></div>
          <div class="ap-task"><span class="ap-pot"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1.5 1.5M17.5 17.5 19 19M5 19l1.5-1.5M17.5 6.5 19 5"/></svg></span><span><b>Fiddle-leaf fig</b><small>Turn towards the window</small></span><span class="ap-tick"></span></div>
          <div class="ap-task"><span class="ap-pot"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 14c2 0 2-2 4-2s2 2 4 2 2-2 4-2 2 2 4 2M4 19c2 0 2-2 4-2s2 2 4 2 2-2 4-2 2 2 4 2M12 3v5"/></svg></span><span><b>Calathea</b><small>Mist the leaves</small></span><span class="ap-tick"></span></div>
          <div class="ap-stat"><small>Watering this week</small><b>1.2 litres, all on time</b><div class="ap-bars"><i style="height:40%"></i><i style="height:70%"></i><i style="height:30%"></i><i style="height:90%"></i><i style="height:55%"></i><i style="height:75%"></i><i style="height:45%"></i></div></div>
          <div class="ap-tabbar"><i></i><i></i><i></i><i></i></div>
        </div>
      </div>
    </div>
  </div>
  </div>
</main>
CSS
/* Bootstrap grid, .badge, .form-label, .form-control and .btn; the phone mock-up is plain CSS (Bootstrap has no equivalent).
   Store badges are text only: no store logos. */
.ap{--ap-accent:#1e6b3a;--ap-accent-hover:#175530;--ap-line:#7d9483;--ap-err:#a52a1f;
  --bs-body-bg:#eef5e9;--bs-body-color:#13261b;--bs-secondary-color:#435a4a;
  --bs-body-font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  min-height:100dvh;padding:26px clamp(20px,5vw,56px);color:var(--bs-body-color);font-family:var(--bs-body-font-family);
  background:radial-gradient(40% 55% at 82% 50%,#cfe8c2 0,transparent 70%),var(--bs-body-bg)}
.ap a{color:inherit}
.ap a:focus-visible{outline:2px solid var(--ap-accent);outline-offset:3px;border-radius:2px}
.ap-wide{max-width:1120px}
.ap-logo{font-size:1.15rem;letter-spacing:-.02em;text-decoration:none}
.ap-chip{--bs-badge-font-size:.8rem;--bs-badge-padding-x:12px;--bs-badge-padding-y:6px;color:var(--ap-accent);background:#fff;box-shadow:0 2px 10px -4px rgba(19,38,27,.2)}
.ap-chip::before{content:"";width:7px;height:7px;border-radius:50%;background:#22a352}
.ap-title{font-size:clamp(2.3rem,5.4vw,3.9rem);line-height:1.02;letter-spacing:-.045em}
.ap-lead{max-width:500px;font-size:1.06rem;line-height:1.6}
.ap-badge{padding:9px 18px 9px 14px;background:var(--bs-body-color);border-radius:12px}
.ap-badge span{font-size:1.02rem;line-height:1.15}
.ap-badge small{font-size:.66rem;letter-spacing:.04em;color:#cfe0d3}
.ap-form,.ap-done{max-width:460px}
.ap .form-label{font-size:.92rem}
.ap .form-control{height:50px;padding:0 16px;border-color:var(--ap-line);border-radius:12px}
.ap .form-control::placeholder{color:#62786a}
.ap .form-control:focus{border-color:var(--ap-line);box-shadow:none;outline:2px solid var(--ap-accent);outline-offset:3px}
.ap .form-control[aria-invalid="true"]{border-color:var(--ap-err);box-shadow:0 0 0 1px var(--ap-err)}
.ap .btn-ap{--bs-btn-bg:var(--ap-accent);--bs-btn-border-color:var(--ap-accent);--bs-btn-color:#fff;--bs-btn-font-weight:700;--bs-btn-border-radius:12px;--bs-btn-padding-x:22px;
  --bs-btn-hover-bg:var(--ap-accent-hover);--bs-btn-hover-border-color:var(--ap-accent-hover);--bs-btn-hover-color:#fff;
  --bs-btn-active-bg:var(--ap-accent-hover);--bs-btn-active-border-color:var(--ap-accent-hover);--bs-btn-active-color:#fff;
  --bs-btn-focus-box-shadow:0 0 0 3px var(--bs-body-bg),0 0 0 5px var(--ap-accent);height:50px}
.ap-err{min-height:1.4em;color:var(--ap-err)}
.ap-done{border:1px solid #b9d6b3;border-radius:14px}
.ap-done:focus{outline:none}
.ap-phone-wrap{display:grid;place-items:center}
.ap-phone{width:270px;padding:10px;background:#13261b;border-radius:44px;box-shadow:0 40px 70px -30px rgba(19,38,27,.55),inset 0 0 0 2px #2c4434;transform:rotate(4deg)}
.ap-screen{position:relative;display:flex;flex-direction:column;gap:9px;height:520px;padding:44px 16px 16px;overflow:hidden;background:#f7faf5;border-radius:35px}
.ap-notch{position:absolute;top:10px;left:50%;width:84px;height:24px;background:#13261b;border-radius:999px;transform:translateX(-50%)}
.ap-hi{margin:0;font-size:.72rem;color:#4f6656}
.ap-h{margin:0 0 4px;font-size:1.2rem;font-weight:800;letter-spacing:-.02em}
.ap-task{display:flex;align-items:center;gap:10px;padding:10px;background:#fff;border-radius:16px;box-shadow:0 1px 3px rgba(19,38,27,.1)}
.ap-task b{display:block;font-size:.8rem}
.ap-task small{display:block;font-size:.68rem;color:#4f6656}
.ap-pot{flex:none;display:grid;place-items:center;width:34px;height:34px;border-radius:11px;background:#dcf0d3;color:#1e6b3a}
.ap-tick{flex:none;width:20px;height:20px;margin-left:auto;border:2px solid #9bb3a2;border-radius:50%}
.ap-tick.on{background:#1e6b3a url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 12.5 4.5 4.5L19 7.5'/%3E%3C/svg%3E") center/11px no-repeat;border-color:#1e6b3a}
.ap-stat{margin-top:4px;padding:12px;color:#fff;background:#1e6b3a;border-radius:16px}
.ap-stat small{font-size:.66rem;color:#d3ead8}
.ap-stat b{display:block;margin:2px 0 8px;font-size:.95rem}
.ap-bars{display:flex;align-items:flex-end;gap:5px;height:44px}
.ap-bars i{flex:1;background:#8fd19e;border-radius:4px 4px 2px 2px}
.ap-tabbar{display:flex;justify-content:space-around;margin-top:auto;padding:10px 0 0;border-top:1px solid #e1eadd}
.ap-tabbar i{width:20px;height:20px;border-radius:6px;background:#c7d8cb}
.ap-tabbar i:first-child{background:#1e6b3a}
@media (max-width:991.98px){.ap-phone{transform:none}}
@media (max-width:480px){.ap-phone{width:240px}.ap-screen{height:460px}}
@media (prefers-reduced-motion:reduce){.ap .btn,.ap .form-control{transition:none}}
JavaScript
// Sign-up form, front end only: it checks the address and shows the confirmation.
// Nothing is sent. Point the form's action at your mailing-list provider, or post it with fetch() below.
document.querySelectorAll('form[data-signup]').forEach(function (form) {
  var input = form.querySelector('input[type="email"]');
  var error = form.querySelector('[data-error]');
  var done = document.getElementById(form.getAttribute('data-signup'));
  form.noValidate = true; // our message replaces the browser bubble; without JS, native validation still works
  function clear() { error.textContent = ''; input.removeAttribute('aria-invalid'); }
  input.addEventListener('input', clear);
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var value = input.value.trim();
    if (!value || !input.checkValidity()) {
      error.textContent = value ? 'Check the address: it needs an @ and a domain, like [email protected].' : 'Enter your email address.';
      input.setAttribute('aria-invalid', 'true');
      input.focus();
      return;
    }
    clear();
    form.hidden = true;
    done.hidden = false;
    done.focus();
  });
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<main class="relative flex min-h-dvh flex-col overflow-hidden bg-[radial-gradient(40%_55%_at_82%_50%,#cfe8c2_0,transparent_70%)] bg-ap-bg px-[clamp(20px,5vw,56px)] py-[26px] font-[-apple-system,BlinkMacSystemFont,'Segoe_UI',Roboto,Helvetica,Arial,sans-serif] text-ap-ink">
  <header class="mx-auto flex w-full max-w-[1120px] flex-wrap items-center justify-between gap-3">
    <a class="inline-flex items-center gap-[9px] text-[1.15rem] font-extrabold tracking-[-.02em] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-ap-accent" href="#"><svg width="28" height="28" viewBox="0 0 28 28" aria-hidden="true"><rect width="28" height="28" rx="9" fill="#1e6b3a"/><path d="M14 22v-8m0 0c0-4 3-6 7-6 0 4-3 6-7 6zm0 2c0-3-2.5-5-6-5 0 3 2.5 5 6 5z" fill="#dcf0d3" stroke="#dcf0d3" stroke-width="1.5" stroke-linejoin="round"/></svg>Sprout</a>
    <p class="text-[.88rem] text-ap-muted">Tested by 640 beta gardeners</p>
  </header>
  <div class="mx-auto grid w-full max-w-[1120px] flex-1 grid-cols-1 items-center gap-10 py-9 min-[992px]:grid-cols-[minmax(0,7fr)_minmax(0,5fr)]">
    <section aria-labelledby="ap-title">
      <p class="mb-[18px] inline-flex items-center gap-2 rounded-full bg-white px-3 py-1.5 text-[.8rem] font-bold text-ap-accent shadow-[0_2px_10px_-4px_rgba(19,38,27,.2)] before:size-[7px] before:rounded-full before:bg-[#22a352]">iPhone and Android · Spring 2027</p>
      <h1 class="mb-4 text-[clamp(2.3rem,5.4vw,3.9rem)] leading-[1.02] font-extrabold tracking-[-.045em]" id="ap-title">Never guess when to water again.</h1>
      <p class="mb-[26px] max-w-[500px] text-[1.06rem] leading-[1.6] text-ap-muted">Sprout learns each plant's light, pot and season, then tells you what it needs today. Snap a leaf to catch pests and yellowing before they spread. Free, with an optional Plus plan.</p>
      <ul class="mb-[26px] flex flex-wrap gap-2.5" aria-label="Launch platforms">
        <li class="inline-flex items-center gap-2.5 rounded-xl bg-ap-ink py-[9px] pr-[18px] pl-3.5 text-white"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><rect x="6" y="2" width="12" height="20" rx="3"/><path d="M11 18h2"/></svg><span class="flex flex-col text-[1.02rem] leading-[1.15] font-bold"><small class="text-[.66rem] font-semibold tracking-[.04em] text-[#cfe0d3] uppercase">Coming soon to the</small>App Store</span></li>
        <li class="inline-flex items-center gap-2.5 rounded-xl bg-ap-ink py-[9px] pr-[18px] pl-3.5 text-white"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><rect x="6" y="2" width="12" height="20" rx="3"/><path d="M11 18h2"/></svg><span class="flex flex-col text-[1.02rem] leading-[1.15] font-bold"><small class="text-[.66rem] font-semibold tracking-[.04em] text-[#cfe0d3] uppercase">Coming soon to</small>Google Play</span></li>
      </ul>
      <form class="max-w-[460px]" data-signup="ap-done" action="#" method="post">
        <label class="mb-2 block text-[.92rem] font-bold" for="ap-email">Email me on launch day</label>
        <div class="flex flex-col gap-2 min-[576px]:flex-row">
          <input class="h-[50px] min-w-0 flex-none min-[576px]:flex-1 rounded-xl border border-ap-line bg-white px-4 text-ap-ink placeholder:text-[#62786a] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-ap-accent aria-invalid:border-ap-err aria-invalid:shadow-[0_0_0_1px_var(--color-ap-err)]" id="ap-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required aria-describedby="ap-err">
          <button class="h-[50px] flex-none cursor-pointer rounded-xl bg-ap-accent px-[22px] font-bold text-white transition-colors duration-200 hover:bg-ap-accent-hover focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-ap-accent motion-reduce:transition-none" type="submit">Remind me</button>
        </div>
        <p class="mt-2 min-h-[1.4em] text-[.88rem] text-ap-err" id="ap-err" data-error aria-live="polite"></p>
      </form>
      <div class="max-w-[460px] rounded-[14px] border border-[#b9d6b3] bg-white px-[18px] py-4 leading-normal focus:outline-none" id="ap-done" tabindex="-1" hidden><p><strong>Reminder set.</strong> You will get one email with both store links on launch day.</p></div>
    </section>
    <div class="grid place-items-center">
      <div class="w-[240px] rounded-[44px] bg-ap-ink p-2.5 shadow-[0_40px_70px_-30px_rgba(19,38,27,.55),inset_0_0_0_2px_#2c4434] min-[481px]:w-[270px] min-[992px]:rotate-4" role="img" aria-label="Sprout app preview: today's care list for a Monstera, a fiddle-leaf fig and a Calathea">
        <div class="relative flex h-[460px] flex-col gap-[9px] overflow-hidden rounded-[35px] bg-[#f7faf5] px-4 pt-11 pb-4 min-[481px]:h-[520px]">
          <span class="absolute top-2.5 left-1/2 h-6 w-[84px] -translate-x-1/2 rounded-full bg-ap-ink"></span>
          <p class="text-[.72rem] text-ap-soft">Good morning, Maya</p>
          <p class="mb-1 text-[1.2rem] font-extrabold tracking-[-.02em]">Today · 3 tasks</p>
          <div class="flex items-center gap-2.5 rounded-2xl bg-white p-2.5 shadow-[0_1px_3px_rgba(19,38,27,.1)]"><span class="grid size-[34px] flex-none place-items-center rounded-[11px] bg-[#dcf0d3] text-ap-accent"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3s6 6.5 6 11a6 6 0 0 1-12 0c0-4.5 6-11 6-11z"/></svg></span><span><b class="block text-[.8rem]">Monstera</b><small class="block text-[.68rem] text-ap-soft">Water 250 ml</small></span><span class="ml-auto grid size-5 flex-none place-items-center rounded-full border-2 border-ap-accent bg-ap-accent text-white"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span></div>
          <div class="flex items-center gap-2.5 rounded-2xl bg-white p-2.5 shadow-[0_1px_3px_rgba(19,38,27,.1)]"><span class="grid size-[34px] flex-none place-items-center rounded-[11px] bg-[#dcf0d3] text-ap-accent"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1.5 1.5M17.5 17.5 19 19M5 19l1.5-1.5M17.5 6.5 19 5"/></svg></span><span><b class="block text-[.8rem]">Fiddle-leaf fig</b><small class="block text-[.68rem] text-ap-soft">Turn towards the window</small></span><span class="ml-auto size-5 flex-none rounded-full border-2 border-[#9bb3a2]"></span></div>
          <div class="flex items-center gap-2.5 rounded-2xl bg-white p-2.5 shadow-[0_1px_3px_rgba(19,38,27,.1)]"><span class="grid size-[34px] flex-none place-items-center rounded-[11px] bg-[#dcf0d3] text-ap-accent"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 14c2 0 2-2 4-2s2 2 4 2 2-2 4-2 2 2 4 2M4 19c2 0 2-2 4-2s2 2 4 2 2-2 4-2 2 2 4 2M12 3v5"/></svg></span><span><b class="block text-[.8rem]">Calathea</b><small class="block text-[.68rem] text-ap-soft">Mist the leaves</small></span><span class="ml-auto size-5 flex-none rounded-full border-2 border-[#9bb3a2]"></span></div>
          <div class="mt-1 rounded-2xl bg-ap-accent p-3 text-white"><small class="text-[.66rem] text-[#d3ead8]">Watering this week</small><b class="mt-0.5 mb-2 block text-[.95rem]">1.2 litres, all on time</b><div class="flex h-11 items-end gap-[5px] *:flex-1 *:rounded-t-[4px] *:rounded-b-[2px] *:bg-[#8fd19e]"><i class="h-[40%]"></i><i class="h-[70%]"></i><i class="h-[30%]"></i><i class="h-[90%]"></i><i class="h-[55%]"></i><i class="h-[75%]"></i><i class="h-[45%]"></i></div></div>
          <div class="mt-auto flex justify-around border-t border-[#e1eadd] pt-2.5 *:size-5 *:rounded-md *:bg-[#c7d8cb] *:first:bg-ap-accent"><i></i><i></i><i></i><i></i></div>
        </div>
      </div>
    </div>
  </div>
</main>
CSS
/* Store badges are text only: no store logos. */
@theme {
  --color-ap-bg: #eef5e9;
  --color-ap-ink: #13261b;
  --color-ap-muted: #435a4a;
  --color-ap-soft: #4f6656;
  --color-ap-accent: #1e6b3a;
  --color-ap-accent-hover: #175530;
  --color-ap-line: #7d9483;
  --color-ap-err: #a52a1f;
}
JavaScript
// Sign-up form, front end only: it checks the address and shows the confirmation.
// Nothing is sent. Point the form's action at your mailing-list provider, or post it with fetch() below.
document.querySelectorAll('form[data-signup]').forEach(function (form) {
  var input = form.querySelector('input[type="email"]');
  var error = form.querySelector('[data-error]');
  var done = document.getElementById(form.getAttribute('data-signup'));
  form.noValidate = true; // our message replaces the browser bubble; without JS, native validation still works
  function clear() { error.textContent = ''; input.removeAttribute('aria-invalid'); }
  input.addEventListener('input', clear);
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var value = input.value.trim();
    if (!value || !input.checkValidity()) {
      error.textContent = value ? 'Check the address: it needs an @ and a domain, like [email protected].' : 'Enter your email address.';
      input.setAttribute('aria-invalid', 'true');
      input.focus();
      return;
    }
    clear();
    form.hidden = true;
    done.hidden = false;
    done.focus();
  });
});

About this set

A pre-launch page for a mobile app, with a phone mock-up beside the pitch. The demo is a plant-care app: a platform and season chip, a headline, a paragraph on what the app does, two dark store badges that read Coming soon to the App Store and Coming soon to Google Play, and a form to get one email on launch day. The badges are text with a generic phone icon rather than the stores’ own logos, so there is nothing to license and nothing to swap out later except the words; once the app is live, turn each one into a link. On the right, a tilted phone drawn entirely in HTML and CSS shows the app’s Today screen: three care tasks with icons and tick circles, a weekly watering chart and a tab bar. Because it is markup and not a screenshot, you can change the copy in the mock-up as the product changes. The phone is exposed to screen readers as a single image with a description. The grid stacks below 992 pixels with the phone under the text, and the form validates in the browser and confirms without sending anything.

What’s included

  • Text-only Coming soon store badges, no brand logos
  • Phone mock-up built in HTML and CSS, not an image
  • Mock-up exposed as one role="img" with a description
  • Launch-day reminder form with validation and confirmation
  • Tilt is removed when the layout stacks

Accessibility

The store badges are a list named Launch platforms, with their icons hidden. The phone mock-up has role="img" and an aria-label that describes the screen, so its decorative contents are not read piece by piece. The email field has a visible label, an aria-live error linked with aria-describedby and aria-invalid when wrong, and focus moves to the confirmation. Links, the field and the button show a 2px green focus outline; transitions are removed under prefers-reduced-motion.

Customise it

Change --ap-bg, --ap-ink and --ap-accent on .ap (plain CSS and Bootstrap) or the --color-ap-* theme colours in Tailwind. The phone size is the width on .ap-phone and the height on .ap-screen; edit the tasks and chart bars directly in the markup.