One self-contained HTML file, no libraries. Your AI gets it from Flow's get_pattern tool.
Show the code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Elevator login | Flow pattern</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Marcellus&display=swap">
<!--
Flow login pattern: Elevator (https://flow.donta.io/screens/login/elevator)
An art-deco elevator lobby. The sign-in form is a brass-framed plaque on the
lobby wall and its submit is the call button: it lights the moment you press
it, the floor needle sweeps up while you're signed in, then a soft chime and
the doors slide open on a warm car that welcomes you by name. On a wrong
password the needle drops back, a red lamp lights and the message says what
to do. The form works from the first frame; the elevator only answers it.
Demo: any email and an 8+ character password signs in; "wrongpass" shows the error.
-->
<style>
:root {
--wall: #0b231e; --wall-2: #12332b; --lacquer: #070f0d;
--brass: #d4ad5f; --brass-hi: #f3d998; --brass-lo: #8a6a2c;
--cream: #f1e6c8; --muted: #b9ae92; --line: #3d5a50;
--err: #ff8f70; --ok: #b9e6c4; --lamp: #ffcf6b;
--deco: "Marcellus", "Didot", "Bodoni 72", "Iowan Old Style", Georgia, serif;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}
* { box-sizing: border-box; }
html, body { margin: 0; min-height: 100%; }
body {
position: relative; min-height: 100vh; color: var(--cream); overflow-x: hidden;
background:
radial-gradient(ellipse 60% 45% at 32% 20%, rgba(243, 217, 152, 0.10), transparent 70%),
repeating-linear-gradient(90deg, transparent 0 46px, rgba(0,0,0,.18) 46px 48px, rgba(255,255,255,.025) 48px 50px),
linear-gradient(180deg, var(--wall-2), var(--wall) 70%, #06120f);
}
/* marble floor */
body::after {
content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: max(15vh, 110px); z-index: -1;
background:
linear-gradient(180deg, rgba(212,173,95,.5), rgba(212,173,95,0) 3px),
radial-gradient(ellipse 22% 70% at 30% 0%, rgba(212,173,95,.16), transparent 70%),
linear-gradient(115deg, transparent 40%, rgba(255,255,255,.05) 45%, transparent 52%),
linear-gradient(180deg, #101614, #040605);
}
.lobby {
min-height: 100vh; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
align-items: center; justify-items: center; gap: clamp(24px, 5vw, 80px);
padding: clamp(24px, 5vw, 64px) clamp(16px, 5vw, 72px) 18vh;
max-width: 1200px; margin: 0 auto;
}
/* ── Elevator ─────────────────────────────── */
.shaft { width: min(360px, 82vw); display: grid; justify-items: center; position: relative; }
.sconce { position: absolute; top: 36%; width: 14px; height: 54px; border-radius: 7px 7px 2px 2px; background: linear-gradient(var(--brass-hi), var(--brass-lo)); box-shadow: 0 -30px 60px 20px rgba(255, 207, 107, 0.16); }
.sconce.l { left: -38px; } .sconce.r { right: -38px; }
.dial-wrap { width: 72%; position: relative; z-index: 2; margin-bottom: -2px; }
.dial { width: 100%; display: block; overflow: visible; }
.needle { transform-box: view-box; transform-origin: 130px 128px; rotate: -80deg; transition: rotate 1.1s cubic-bezier(.45,.05,.25,1); }
.lamps { position: absolute; left: -16px; right: -16px; bottom: 2px; display: flex; justify-content: space-between; pointer-events: none; }
.lamps i { width: 11px; height: 11px; border-radius: 50%; background: #3b2a1c; box-shadow: inset 0 1px 2px rgba(0,0,0,.6); transition: background-color .2s, box-shadow .2s; }
.up.on { background: var(--lamp); box-shadow: 0 0 10px 2px rgba(255,207,107,.8); }
.stop.on { background: #ff5a4a; box-shadow: 0 0 10px 2px rgba(255,90,74,.8); }
.frame {
width: 100%; position: relative; padding: 14px; border-radius: 4px;
background: linear-gradient(135deg, var(--brass-hi), var(--brass) 30%, var(--brass-lo) 70%, var(--brass));
box-shadow: 0 30px 60px rgba(0,0,0,.55), inset 0 0 0 1px rgba(255,255,255,.25);
}
.frame::before { display: none; /* the dial is the crown */
content: ""; position: absolute; left: 50%; top: -28px; translate: -50% 0; width: 46%; height: 30px;
background: linear-gradient(135deg, var(--brass-hi), var(--brass-lo));
clip-path: polygon(0 100%, 8% 100%, 8% 66%, 20% 66%, 20% 33%, 34% 33%, 34% 0, 66% 0, 66% 33%, 80% 33%, 80% 66%, 92% 66%, 92% 100%, 100% 100%);
}
.doors { position: relative; aspect-ratio: 3 / 4; overflow: hidden; background: #1c140c; }
.car {
position: absolute; inset: 0; display: grid; place-content: center; justify-items: center; gap: 14px; text-align: center; padding: 22px;
background:
radial-gradient(ellipse 80% 40% at 50% 0%, rgba(255, 240, 200, .95), rgba(255, 220, 150, 0) 70%),
repeating-linear-gradient(90deg, rgba(90, 45, 15, .0) 0 38px, rgba(90, 45, 15, .22) 38px 40px),
linear-gradient(180deg, #f2cf8f, #c98a45 70%, #8d5426);
color: #2a1608;
}
.car::before { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 16%; background: linear-gradient(180deg, #5c3416, #3a1f0c); box-shadow: inset 0 2px 0 rgba(255,220,160,.35); }
.car > * { position: relative; z-index: 1; }
.car::after { content: ""; position: absolute; left: 10%; right: 10%; top: 70%; height: 6px; border-radius: 3px; background: linear-gradient(var(--brass-hi), var(--brass-lo)); box-shadow: 0 3px 6px rgba(60,30,10,.35); }
.car .floor { font: 14px/1 var(--deco); letter-spacing: .3em; text-transform: uppercase; opacity: .7; }
.car h2 { margin: 0; font: 400 clamp(24px, 3.2vw, 32px)/1.1 var(--deco); }
.car a {
position: relative; z-index: 1; display: inline-grid; place-items: center; min-height: 44px; padding: 0 20px; border-radius: 999px;
background: #2a1608; color: var(--brass-hi); text-decoration: none; font-weight: 600; font-size: 15px; letter-spacing: .02em;
}
.car a:focus-visible { outline: 3px solid #2a1608; outline-offset: 3px; }
.door {
position: absolute; top: 0; bottom: 0; width: 50%; z-index: 1;
background:
linear-gradient(90deg, rgba(0,0,0,.25), transparent 8%, transparent 92%, rgba(0,0,0,.35)),
linear-gradient(100deg, transparent 30%, rgba(255,245,215,.28) 38%, transparent 46%),
linear-gradient(160deg, #e9c983, #b98c41 40%, #8a6428 75%, #b38a45);
transition: translate 1.2s cubic-bezier(.65,0,.25,1);
}
.door svg { position: absolute; inset: 8% 10%; width: 80%; height: 84%; }
.door.l { left: 0; border-right: 1px solid #5b4218; }
.door.r { right: 0; border-left: 1px solid #f5dfa6; }
.is-open .door.l { translate: -101% 0; }
.is-open .door.r { translate: 101% 0; }
.car[aria-hidden="true"] a { visibility: hidden; }
/* ── Plaque form ──────────────────────────── */
.plaque {
width: min(400px, 100%); position: relative; padding: 34px 32px 28px;
background: linear-gradient(180deg, #0d2923, #08201b);
border: 1px solid var(--brass); outline: 1px solid rgba(212,173,95,.45); outline-offset: 6px;
box-shadow: 0 30px 70px rgba(0,0,0,.5);
display: grid; gap: 16px;
}
.plaque::before, .plaque::after { /* deco corner fans */
content: ""; position: absolute; width: 46px; height: 46px; pointer-events: none;
background: repeating-conic-gradient(from 0deg at 0 0, var(--brass) 0 3deg, transparent 3deg 12deg);
-webkit-mask: radial-gradient(circle at 0 0, #000 70%, transparent 71%); mask: radial-gradient(circle at 0 0, #000 70%, transparent 71%);
opacity: .7;
}
.plaque::before { left: 0; top: 0; }
.plaque::after { right: 0; bottom: 0; rotate: 180deg; }
.eyebrow { margin: 0; font: 13px/1 var(--deco); letter-spacing: .32em; text-transform: uppercase; color: var(--brass); text-align: center; }
h1 { margin: 0; text-align: center; font: 400 clamp(32px, 4vw, 40px)/1.05 var(--deco); letter-spacing: .01em; color: var(--cream); }
.rule { display: flex; align-items: center; gap: 10px; width: 70%; margin: -2px auto 2px; }
.rule::before, .rule::after { content: ""; flex: 1; height: 1px; background: linear-gradient(90deg, transparent, var(--brass)); }
.rule::after { background: linear-gradient(90deg, var(--brass), transparent); }
.rule i { width: 8px; height: 8px; rotate: 45deg; border: 1px solid var(--brass); background: rgba(212,173,95,.35); }
label { display: grid; gap: 7px; font-size: 13px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
input {
width: 100%; min-height: 48px; padding: 0 14px; border-radius: 2px; border: 1px solid var(--line);
background: #061512; color: var(--cream); font: inherit; font-size: 16px; letter-spacing: normal; text-transform: none;
}
input:focus { outline: 2px solid var(--brass); outline-offset: 2px; border-color: var(--brass); }
input[aria-invalid="true"] { border-color: var(--err); }
.row { display: flex; justify-content: flex-end; font-size: 14px; margin-top: -4px; }
.row a { color: var(--brass-hi); }
.call {
justify-self: stretch; min-height: 54px; border-radius: 999px; border: 0; cursor: pointer;
display: flex; align-items: center; justify-content: center; gap: 14px;
background: linear-gradient(180deg, var(--brass-hi), var(--brass) 45%, var(--brass-lo));
color: #1f1606; font-weight: 600; font-size: 16px; line-height: 1; font-family: inherit; letter-spacing: .06em; text-transform: uppercase;
box-shadow: 0 2px 0 #5c4519, 0 8px 18px rgba(0,0,0,.4), inset 0 1px 0 rgba(255,255,255,.5);
transition: translate .08s, box-shadow .08s;
}
.call:active { translate: 0 2px; box-shadow: 0 0 0 #5c4519, 0 4px 10px rgba(0,0,0,.4), inset 0 1px 0 rgba(255,255,255,.5); }
.call .bulb {
width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center;
background: radial-gradient(circle at 40% 35%, #5a4422, #2b1e0b); color: #8a6a2c; font-size: 11px;
box-shadow: inset 0 1px 3px rgba(0,0,0,.6), 0 0 0 2px #6b5121; transition: background .15s, color .15s, box-shadow .15s;
}
.call.lit .bulb { background: radial-gradient(circle at 40% 35%, #fff4cf, var(--lamp) 55%, #e09a2a); color: #6b3d00; box-shadow: 0 0 0 2px #6b5121, 0 0 18px 4px rgba(255, 207, 107, .85); }
.call:disabled { cursor: progress; }
.call:focus-visible { outline: 2px solid var(--cream); outline-offset: 3px; }
.msg { min-height: 20px; margin: 0; font-size: 14px; text-align: center; }
.msg.err { color: var(--err); } .msg.ok { color: var(--ok); }
.msg a { color: var(--brass-hi); }
.hint { margin: 0; font-size: 12px; color: #8f8670; text-align: center; }
@media (max-width: 760px) {
.lobby { grid-template-columns: 1fr; padding-top: 52px; gap: 40px; }
.shaft { width: min(260px, 70vw); }
.sconce { display: none; }
}
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none !important; animation: none !important; } }
</style>
</head>
<body>
<main class="lobby" id="lobby">
<section class="shaft" aria-label="Elevator">
<span class="sconce l" aria-hidden="true"></span><span class="sconce r" aria-hidden="true"></span>
<div class="dial-wrap" aria-hidden="true">
<svg class="dial" viewBox="0 0 260 140">
<defs>
<linearGradient id="dialFace" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1b130a"/><stop offset="1" stop-color="#0d0905"/></linearGradient>
<linearGradient id="dialRim" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#f3d998"/><stop offset=".5" stop-color="#b98c41"/><stop offset="1" stop-color="#f0d38c"/></linearGradient>
</defs>
<path d="M6 132 A124 124 0 0 1 254 132 Z" fill="url(#dialRim)"/>
<path d="M16 132 A114 114 0 0 1 244 132 Z" fill="url(#dialFace)"/>
<g id="ticks" stroke="#d4ad5f" stroke-width="2" stroke-linecap="round"></g>
<g id="nums" fill="#f1e6c8" font-family="Marcellus, Didot, Georgia, serif" font-size="13" text-anchor="middle"></g>
<g class="needle" id="needle">
<path d="M130 128 L127 50 L130 34 L133 50 Z" fill="#f3d998"/>
<circle cx="130" cy="128" r="9" fill="url(#dialRim)"/>
</g>
</svg>
<div class="lamps"><i class="up" id="lampUp"></i><i class="stop" id="lampStop"></i></div>
</div>
<div class="frame">
<div class="doors" id="doors">
<div class="car" id="car" aria-hidden="true">
<span class="floor" id="floorText">Floor 9</span>
<h2 id="welcome">Welcome back</h2>
<a href="#" id="go">Go to dashboard</a>
</div>
<div class="door l" aria-hidden="true"><svg viewBox="0 0 100 300" preserveAspectRatio="none"><use href="#inlay"/></svg></div>
<div class="door r" aria-hidden="true"><svg viewBox="0 0 100 300" preserveAspectRatio="none" style="transform:scaleX(-1)"><use href="#inlay"/></svg></div>
</div>
</div>
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<symbol id="inlay" viewBox="0 0 100 300">
<g fill="none" stroke="#5e441a" stroke-width="1.2" opacity=".75">
<rect x="4" y="4" width="92" height="292"/>
<rect x="12" y="12" width="76" height="276"/>
<path d="M100 150 L12 60 M100 150 L12 100 M100 150 L12 150 M100 150 L12 200 M100 150 L12 240"/>
<path d="M100 150 L60 12 M100 150 L60 288"/>
<path d="M12 30 L50 30 L50 12 M12 270 L50 270 L50 288"/>
</g>
<path d="M100 120 A30 30 0 0 0 100 180" fill="#f3d998" opacity=".35"/>
</symbol>
</svg>
</section>
<form class="plaque" id="form" novalidate>
<p class="eyebrow">The Grand Lobby</p>
<h1>Welcome back</h1>
<div class="rule" aria-hidden="true"><i></i></div>
<label>Email
<input id="email" name="email" type="email" autocomplete="username" inputmode="email" required>
</label>
<label>Password
<input id="password" name="password" type="password" autocomplete="current-password" required minlength="8">
</label>
<div class="row"><a href="#">Forgot password?</a></div>
<button class="call" id="submit" type="submit"><span class="bulb" aria-hidden="true">▲</span><span id="callLabel">Sign in</span></button>
<p class="msg" id="msg" role="status" aria-live="polite"></p>
<p class="hint">Demo: any email, 8+ character password. "wrongpass" shows the error.</p>
</form>
</main>
<script>
(() => {
const FLOORS = ["L", "2", "3", "4", "5", "6", "7", "8", "9"];
const ticks = document.getElementById("ticks");
const nums = document.getElementById("nums");
const NS = "http://www.w3.org/2000/svg";
FLOORS.forEach((f, i) => {
const a = (-80 + (160 / (FLOORS.length - 1)) * i) * Math.PI / 180;
const sx = 130 + Math.sin(a) * 98, sy = 128 - Math.cos(a) * 98;
const ex = 130 + Math.sin(a) * 108, ey = 128 - Math.cos(a) * 108;
const t = document.createElementNS(NS, "line");
t.setAttribute("x1", sx); t.setAttribute("y1", sy); t.setAttribute("x2", ex); t.setAttribute("y2", ey);
ticks.append(t);
const n = document.createElementNS(NS, "text");
n.setAttribute("x", 130 + Math.sin(a) * 82); n.setAttribute("y", 132 - Math.cos(a) * 82);
n.textContent = f; nums.append(n);
});
const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
const needle = document.getElementById("needle");
const lampUp = document.getElementById("lampUp");
const lampStop = document.getElementById("lampStop");
const doors = document.getElementById("doors");
const car = document.getElementById("car");
const form = document.getElementById("form");
const email = document.getElementById("email");
const password = document.getElementById("password");
const btn = document.getElementById("submit");
const callLabel = document.getElementById("callLabel");
const msg = document.getElementById("msg");
const setNeedle = (deg) => { needle.style.rotate = deg + "deg"; };
function chime() {
try {
const ac = new (window.AudioContext || window.webkitAudioContext)();
[[659.25, 0], [523.25, 0.32]].forEach(([f, t]) => {
const o = ac.createOscillator(), g = ac.createGain();
o.type = "sine"; o.frequency.value = f;
g.gain.setValueAtTime(0.0001, ac.currentTime + t);
g.gain.exponentialRampToValueAtTime(0.07, ac.currentTime + t + 0.02);
g.gain.exponentialRampToValueAtTime(0.0001, ac.currentTime + t + 1.1);
o.connect(g).connect(ac.destination); o.start(ac.currentTime + t); o.stop(ac.currentTime + t + 1.2);
});
} catch {}
}
function reset() {
doors.classList.remove("is-open"); car.setAttribute("aria-hidden", "true");
lampStop.classList.remove("on"); lampUp.classList.remove("on"); btn.classList.remove("lit");
}
function fail(input, text) {
input.focus();
input.setAttribute("aria-invalid", "true");
msg.className = "msg err"; msg.textContent = text;
}
form.addEventListener("submit", async (e) => {
e.preventDefault();
if (btn.disabled) return;
reset();
msg.className = "msg"; msg.textContent = "";
email.removeAttribute("aria-invalid"); password.removeAttribute("aria-invalid");
if (!/^\S+@\S+\.\S+$/.test(email.value.trim())) return fail(email, "Enter your email address, like name@example.com.");
if (password.value.length < 8) return fail(password, "Your password is at least 8 characters.");
// Answer immediately: the call button lights and the car starts up.
btn.classList.add("lit"); lampUp.classList.add("on");
btn.disabled = true; btn.setAttribute("aria-busy", "true"); callLabel.textContent = "Calling…";
setNeedle(80);
await new Promise((r) => setTimeout(r, 1200)); // your real request goes here
btn.disabled = false; btn.removeAttribute("aria-busy"); callLabel.textContent = "Sign in";
lampUp.classList.remove("on");
if (password.value === "wrongpass") {
setNeedle(-80); btn.classList.remove("lit"); lampStop.classList.add("on");
return fail(password, "That password doesn't match this email. Try again or reset it.");
}
const raw = email.value.split("@")[0].split(/[._-]+/)[0] || "";
const name = raw ? raw[0].toUpperCase() + raw.slice(1) : "";
document.getElementById("welcome").textContent = name ? `Welcome back, ${name}` : "Welcome back";
msg.className = "msg ok"; msg.textContent = `Signed in. Welcome back${name ? ", " + name : ""}.`;
if (!reduced) chime();
doors.classList.add("is-open"); car.removeAttribute("aria-hidden");
setTimeout(() => document.getElementById("go").focus({ preventScroll: true }), reduced ? 0 : 900);
});
})();
</script>
</body>
</html>