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>Lamp pull login | Flow pattern</title>
<!--
Flow login pattern: Lamp pull (https://flow.donta.io/screens/login/lamp-pull)
Pull the cord (drag, click, or Enter) to switch the lamp on. The form is a real
form from the start: focusing a field or typing also turns the light on, so the
interaction never stands between someone and their account.
Demo: any email and a password of 8+ characters signs in; the password "wrongpass" shows the error state.
-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@1,9..144,400;1,9..144,600&display=swap" rel="stylesheet">
<style>
:root {
--ink: #f3ede2; --muted: #a89f90; --line: #3b342b; --card: #16130f;
--accent: #f4c35a; --accent-ink: #1d1606; --err: #ff8f6b; --ok: #9be3b8;
--serif: "Fraunces", "Iowan Old Style", "Palatino Linotype", Palatino, 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 {
min-height: 100vh; background: #0c0a08; color: var(--ink);
display: grid; place-items: center; padding: 24px; overflow: hidden; position: relative;
}
/* Room: wallpaper and floor that only show where the lamp lights them. */
.room { position: fixed; inset: 0; pointer-events: none; --lx: 33%; --ly: 34%; }
.wall {
position: absolute; inset: 0 0 24% 0;
background:
repeating-linear-gradient(90deg, rgba(255, 214, 150, 0.05) 0 1px, transparent 1px 22px),
repeating-linear-gradient(90deg, rgba(255, 214, 150, 0.035) 0 9px, transparent 9px 44px),
#120f0c;
-webkit-mask: radial-gradient(60vmax 52vmax at var(--lx) var(--ly), #000 0%, rgba(0,0,0,.45) 45%, transparent 75%);
mask: radial-gradient(60vmax 52vmax at var(--lx) var(--ly), #000 0%, rgba(0,0,0,.45) 45%, transparent 75%);
opacity: calc(var(--on) * 1); transition: opacity .6s ease;
}
.floor {
position: absolute; left: 0; right: 0; bottom: 0; height: 24%;
background: linear-gradient(#0e0c0a, #090807);
border-top: 1px solid rgba(255, 220, 160, calc(0.05 + var(--on) * 0.08));
}
.pool {
position: absolute; left: var(--lx); bottom: 9%; width: 46vmax; height: 11vmax; translate: -50% 0;
background: radial-gradient(closest-side, rgba(255, 196, 104, 0.30), rgba(255, 180, 90, 0.08) 60%, transparent);
opacity: var(--on); transition: opacity .6s ease;
}
body:has(.is-on) { --on: 1; }
body { --on: 0; }
.scene {
position: relative; z-index: 1; width: min(1080px, 100%);
display: grid; grid-template-columns: 1.15fr 1fr; align-items: end; gap: 32px;
}
/* Lamp */
.lamp-wrap { --h: min(620px, 74vh); position: relative; height: var(--h); display: grid; place-items: end center; }
.lamp { height: var(--h); width: calc(var(--h) * 220 / 360); overflow: visible; }
.glow {
position: absolute; left: 50%; top: 14%; width: calc(var(--h) * 1.3); height: calc(var(--h) * 1.1); translate: -50% 0;
background: radial-gradient(closest-side, rgba(255, 205, 110, 0.26), rgba(255, 190, 90, 0.07) 55%, transparent 75%);
opacity: var(--on); transition: opacity 0.5s ease; pointer-events: none;
}
.cone { opacity: calc(var(--on) * 0.8); transition: opacity 0.45s ease; }
.bulb { fill: color-mix(in srgb, #ffe2a0 calc(var(--on) * 100%), #4a4338); transition: fill 0.25s ease; }
.shade-in { fill: color-mix(in srgb, #ffcf7a calc(var(--on) * 60%), #221e19); transition: fill .35s ease; }
.motes { position: absolute; inset: 0; pointer-events: none; opacity: calc(var(--on) * 0.75); transition: opacity .8s ease; }
.motes i {
position: absolute; width: 3px; height: 3px; border-radius: 50%; background: #ffe6b0;
box-shadow: 0 0 6px rgba(255, 220, 150, .9); animation: drift 9s ease-in-out infinite alternate;
}
@keyframes drift { 0% { translate: 0 0; opacity: .2; } 50% { opacity: 1; } 100% { translate: 18px -60px; opacity: .3; } }
.cord {
position: absolute; left: calc(50% + var(--h) * 0.1); top: calc(var(--h) * 0.255); width: 44px; height: calc(var(--h) * 0.3);
display: grid; justify-items: center; align-content: start; background: none; border: 0; padding: 0; cursor: grab; touch-action: none;
}
.cord:active { cursor: grabbing; }
.cord .string { width: 2px; height: calc(var(--h) * 0.16); background: linear-gradient(#5d554a, #9c917f); transition: height 0.35s cubic-bezier(.3,1.6,.5,1); }
.cord .knob { width: 13px; height: 20px; border-radius: 7px; background: radial-gradient(circle at 35% 30%, #ffe3a1, var(--accent) 60%, #b98a2a); box-shadow: 0 0 calc(var(--on) * 16px) rgba(255, 200, 100, 0.85); }
.cord:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: 8px; }
.cord-hint {
position: absolute; left: calc(50% + var(--h) * 0.1 + 34px); top: calc(var(--h) * 0.4);
font: italic 17px/1.2 var(--serif); color: #cbbfa9; white-space: nowrap; display: flex; align-items: center; gap: 6px;
opacity: calc(1 - var(--on)); transition: opacity .3s;
}
.cord-hint svg { width: 28px; height: 18px; }
/* Form */
.card {
position: relative; width: min(380px, 100%); justify-self: center; margin-bottom: 6vh;
background: linear-gradient(160deg, rgba(34, 29, 23, 0.94), rgba(20, 17, 13, 0.96));
border: 1px solid var(--line); border-radius: 22px; padding: 32px 30px 26px;
display: grid; gap: 16px;
/* Dim but readable when off; never disabled. */
filter: brightness(calc(0.62 + var(--on) * 0.38));
box-shadow: -40px 10px 90px -50px rgba(255, 190, 90, calc(var(--on) * 0.55)), 0 30px 60px rgba(0, 0, 0, 0.45);
transition: filter 0.5s ease, box-shadow 0.6s ease;
}
.eyebrow { margin: 0; font-size: 13px; color: var(--muted); letter-spacing: 0.02em; }
h1 { margin: -6px 0 0; font: italic 600 38px/1.05 var(--serif); letter-spacing: -0.01em; color: #fff4e0; }
.sub { margin: -6px 0 6px; color: var(--muted); font-size: 15px; }
label { display: grid; gap: 7px; font-size: 14px; font-weight: 600; color: #e6dccb; }
input {
min-height: 48px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--line);
background: #0f0d0a; color: var(--ink); font: inherit; font-size: 16px;
}
input:focus { outline: 3px solid color-mix(in srgb, var(--accent) 40%, transparent); border-color: var(--accent); }
input[aria-invalid="true"] { border-color: var(--err); }
.row { display: flex; justify-content: space-between; align-items: center; font-size: 14px; }
a { color: var(--accent); }
.btn {
min-height: 50px; border: 0; border-radius: 12px; color: var(--accent-ink);
background: linear-gradient(#f8cf6e, #eab448); box-shadow: inset 0 1px 0 rgba(255,255,255,.4), 0 8px 24px rgba(240, 180, 70, calc(var(--on) * .25));
font-family: inherit; font-weight: 700; font-size: 16px; cursor: pointer;
}
.btn[aria-busy="true"] { opacity: 0.7; cursor: progress; }
.msg { min-height: 20px; margin: 0; font-size: 14px; }
.msg.err { color: var(--err); }
.msg.ok { color: var(--ok); }
.hint { font-size: 12px; color: var(--muted); margin: 0; }
@media (max-width: 760px) {
body { place-items: start center; overflow: auto; }
.scene { grid-template-columns: 1fr; gap: 0; }
.lamp-wrap { --h: 300px; }
.card { margin-bottom: 0; margin-top: -10px; }
.room { --lx: 50%; --ly: 18%; }
.floor, .pool { display: none; }
}
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } .motes i { animation: none; } }
</style>
</head>
<body>
<div class="room" aria-hidden="true"><div class="wall"></div><div class="floor"></div><div class="pool"></div></div>
<main class="scene" id="scene">
<div class="lamp-wrap">
<div class="glow" aria-hidden="true"></div>
<svg class="lamp" viewBox="0 0 220 360" aria-hidden="true">
<defs>
<linearGradient id="cone" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ffd98a" stop-opacity="0.6" />
<stop offset="0.7" stop-color="#ffcf7a" stop-opacity="0.12" />
<stop offset="1" stop-color="#ffcf7a" stop-opacity="0" />
</linearGradient>
<linearGradient id="shade" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#2b2620" /><stop offset="0.55" stop-color="#3a332a" /><stop offset="1" stop-color="#211d18" />
</linearGradient>
<linearGradient id="pole" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#1f1b17" /><stop offset="0.5" stop-color="#4a4136" /><stop offset="1" stop-color="#1f1b17" />
</linearGradient>
</defs>
<polygon class="cone" points="66,96 154,96 232,362 -12,362" fill="url(#cone)" />
<ellipse class="shade-in" cx="110" cy="95" rx="49" ry="7" />
<path d="M60 96 L82 50 H138 L160 96 Z" fill="url(#shade)" stroke="#4d453a" stroke-width="1.5" />
<path d="M82 50 H138" stroke="#6a5f50" stroke-width="2" />
<ellipse class="bulb" cx="110" cy="97" rx="20" ry="5" />
<rect x="107.5" y="99" width="5" height="246" fill="url(#pole)" />
<ellipse cx="110" cy="350" rx="40" ry="7" fill="#26211b" />
<ellipse cx="110" cy="347" rx="40" ry="6" fill="#332c24" />
</svg>
<div class="motes" aria-hidden="true">
<i style="left:44%;top:42%;animation-delay:-1s"></i><i style="left:52%;top:55%;animation-delay:-3s"></i>
<i style="left:38%;top:66%;animation-delay:-5s"></i><i style="left:58%;top:72%;animation-delay:-2s"></i>
<i style="left:47%;top:80%;animation-delay:-6s"></i><i style="left:62%;top:60%;animation-delay:-4s"></i>
<i style="left:35%;top:52%;animation-delay:-7s"></i>
</div>
<button class="cord" id="cord" type="button" aria-pressed="false" aria-label="Turn the light on">
<span class="string" id="string"></span>
<span class="knob"></span>
</button>
<span class="cord-hint" aria-hidden="true">
<svg viewBox="0 0 28 18" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><path d="M27 4 C18 2, 9 6, 3 12" /><path d="M3 6 L3 12 L9 12" /></svg>
pull the cord
</span>
</div>
<form class="card" id="form" novalidate>
<p class="eyebrow">Good evening</p>
<h1>Welcome back</h1>
<p class="sub">Sign in to pick up where you left off.</p>
<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" style="justify-content:flex-end"><a href="#">Forgot password?</a></div>
<button class="btn" id="submit" type="submit">Sign in</button>
<p class="msg" id="msg" role="status" aria-live="polite"></p>
<p class="hint">Demo: any email, 8+ character password. Use "wrongpass" to see the error.</p>
</form>
</main>
<script>
(() => {
const scene = document.getElementById("scene");
const cord = document.getElementById("cord");
const string = document.getElementById("string");
const form = document.getElementById("form");
const email = document.getElementById("email");
const password = document.getElementById("password");
const submit = document.getElementById("submit");
const msg = document.getElementById("msg");
function setOn(on) {
scene.classList.toggle("is-on", on);
cord.setAttribute("aria-pressed", String(on));
cord.setAttribute("aria-label", on ? "Turn the light off" : "Turn the light on");
}
const toggle = () => setOn(!scene.classList.contains("is-on"));
// Pull: drag the knob down past 30px and release. Click / Enter / Space also work.
let startY = null, pulled = 0, dragged = false;
cord.addEventListener("pointerdown", (e) => { startY = e.clientY; dragged = false; cord.setPointerCapture(e.pointerId); string.style.transition = "none"; });
cord.addEventListener("pointermove", (e) => {
if (startY === null) return;
pulled = Math.max(0, Math.min(60, e.clientY - startY));
if (pulled > 4) dragged = true;
string.style.height = 70 + pulled + "px";
});
cord.addEventListener("pointerup", () => {
if (startY === null) return;
string.style.transition = "";
string.style.height = "";
if (!dragged || pulled > 30) toggle();
startY = null; pulled = 0;
});
cord.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(); } });
// Never gate the form: any interaction with it turns the light on.
form.addEventListener("focusin", () => setOn(true));
form.addEventListener("submit", async (e) => {
e.preventDefault();
msg.className = "msg"; msg.textContent = "";
email.removeAttribute("aria-invalid"); password.removeAttribute("aria-invalid");
const v = email.value.trim();
if (!/^\S+@\S+\.\S+$/.test(v)) { email.setAttribute("aria-invalid", "true"); msg.className = "msg err"; msg.textContent = "Enter your email address, like name@example.com."; email.focus(); return; }
if (password.value.length < 8) { password.setAttribute("aria-invalid", "true"); msg.className = "msg err"; msg.textContent = "Your password is at least 8 characters."; password.focus(); return; }
submit.setAttribute("aria-busy", "true"); submit.disabled = true; submit.textContent = "Signing in…";
await new Promise((r) => setTimeout(r, 700));
submit.removeAttribute("aria-busy"); submit.disabled = false; submit.textContent = "Sign in";
if (password.value === "wrongpass") {
password.setAttribute("aria-invalid", "true"); msg.className = "msg err";
msg.textContent = "That password doesn't match this email. Try again or reset it.";
password.select(); return;
}
msg.className = "msg ok"; msg.textContent = "Signed in. Welcome back.";
});
})();
</script>
</body>
</html>