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>Vault dial verification | Flow pattern</title>
<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=IBM+Plex+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<!--
Flow login pattern: Vault dial (https://flow.donta.io/screens/login/vault-dial)
Two-step verification as a bank vault. Every digit of the one-time code spins
the combination dial to that number; when all six are in, the code submits
itself, the bolts retract one by one and the door swings open onto warm light.
A wrong code shakes the dial and blinks the red lamp. Under the art it's one
real input with autocomplete="one-time-code", so SMS autofill, paste ("123 456",
"123-456") and password managers all work. The vault is decorative (aria-hidden).
Demo: any 6 digits verify; 000000 shows the error.
-->
<style>
:root {
--wall-1: #0e1114;
--wall-2: #1a1f25;
--steel-1: #2b3239;
--steel-2: #5d6873;
--steel-3: #a7b2bc;
--brass-1: #8a6a2a;
--brass-2: #c9a24a;
--brass-3: #f0d88f;
--warm: #ffbe62;
--ink: #eef1f4;
--muted: #98a3ad;
--err: #ff7a6b;
--ok: #7ee3a8;
--mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
--sans: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
* { box-sizing: border-box; }
html, body { margin: 0; min-height: 100%; }
body {
min-height: 100vh;
font-family: var(--sans);
color: var(--ink);
background:
radial-gradient(60% 70% at 32% 50%, rgba(255, 190, 98, 0.07), transparent 70%),
repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.018) 0 1px, transparent 1px 120px),
repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.018) 0 1px, transparent 1px 120px),
linear-gradient(160deg, var(--wall-2), var(--wall-1) 70%);
overflow-x: hidden;
}
.scene {
min-height: 100vh;
max-width: 1240px;
margin: 0 auto;
padding: 32px clamp(20px, 5vw, 64px);
display: grid;
grid-template-columns: minmax(0, 1.05fr) minmax(340px, 0.95fr);
gap: clamp(24px, 5vw, 72px);
align-items: center;
}
/* ── Vault ─────────────────────────────────── */
.vault { position: relative; width: min(560px, 100%); aspect-ratio: 1; justify-self: center; perspective: 1600px; }
.vault > * { position: absolute; inset: 0; }
.frame svg, .door svg { width: 100%; height: 100%; display: block; overflow: visible; }
.interior {
inset: 7%;
border-radius: 50%;
background:
radial-gradient(circle at 50% 46%, #fff1c9 0%, #ffcf7a 18%, #e08a2e 42%, #6b3410 72%, #2a1406 100%);
box-shadow: inset 0 0 60px rgba(0, 0, 0, 0.75);
display: grid; place-items: center start; padding-left: 16%;
opacity: 0; transition: opacity 0.6s ease 0.3s;
}
.interior span {
font: 700 clamp(22px, 3.4vw, 34px)/1 var(--mono); color: #3a1b05; letter-spacing: 0.02em;
text-shadow: 0 1px 0 rgba(255, 240, 200, 0.6);
translate: 0 10px; opacity: 0; transition: opacity 0.5s 1.1s, translate 0.6s 1.1s cubic-bezier(.3,1.4,.5,1);
}
.door { inset: 6%; transform-origin: 100% 50%; transition: transform 1.3s cubic-bezier(.55,.05,.25,1) 0.75s; z-index: 2; }
.vault.open .interior { opacity: 1; }
.vault.open .interior span { opacity: 1; translate: 0 0; }
.vault.open .door { transform: rotateY(-72deg); }
.dial { transform-box: view-box; transform-origin: 300px 300px; transition: transform 0.65s cubic-bezier(.25,.9,.3,1.15); }
.wheel { transform-box: view-box; transform-origin: 300px 300px; transform: rotate(45deg); transition: transform 1s cubic-bezier(.4,0,.2,1); }
.vault.pending .wheel { transform: rotate(80deg); transition-duration: 0.7s; }
.vault.unlocked .wheel { transform: rotate(-135deg); }
.bolt { transition: transform 0.35s cubic-bezier(.5,0,.3,1); }
.vault.unlocked .bolt { transform: translate(var(--bx), var(--by)); }
.vault.shake .dial-wrap { animation: dial-shake 0.45s ease; }
.dial-wrap { transform-box: view-box; transform-origin: 300px 300px; }
@keyframes dial-shake { 20%, 60% { transform: rotate(-6deg); } 40%, 80% { transform: rotate(6deg); } }
.lamp { fill: #3a2a1a; transition: fill 0.2s; }
.lamp-glow { opacity: 0; transition: opacity 0.2s; }
.vault.pending .lamp { fill: #ffb347; animation: pulse 0.6s ease-in-out infinite alternate; }
.vault.pending .lamp-glow { opacity: 0.6; fill: #ffb347; }
.vault.error .lamp { fill: #ff4d3d; animation: blink 0.35s steps(1) 4; }
.vault.error .lamp-glow { opacity: 0.8; fill: #ff4d3d; animation: blink 0.35s steps(1) 4; }
.vault.unlocked .lamp { fill: #49e08b; }
.vault.unlocked .lamp-glow { opacity: 0.8; fill: #49e08b; }
@keyframes pulse { to { opacity: 0.5; } }
@keyframes blink { 50% { opacity: 0.15; } }
/* ── Panel ─────────────────────────────────── */
.panel {
position: relative;
width: min(420px, 100%);
justify-self: center;
padding: 34px 32px 26px;
border-radius: 18px;
background: linear-gradient(170deg, #20262d, #14181d 70%);
border: 1px solid rgba(240, 216, 143, 0.28);
box-shadow: 0 30px 70px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.07), inset 0 0 0 6px rgba(0, 0, 0, 0.18);
display: grid;
gap: 18px;
}
.screw { position: absolute; width: 10px; height: 10px; border-radius: 50%;
background: radial-gradient(circle at 35% 35%, var(--brass-3), var(--brass-1)); box-shadow: 0 1px 1px rgba(0,0,0,.6); }
.screw::after { content: ""; position: absolute; left: 2px; right: 2px; top: 4px; height: 1.5px; background: rgba(60, 40, 10, 0.8); transform: rotate(35deg); }
.screw.tl { top: 12px; left: 12px; } .screw.tr { top: 12px; right: 12px; } .screw.bl { bottom: 12px; left: 12px; } .screw.br { bottom: 12px; right: 12px; }
.plate {
justify-self: start; margin: 0; padding: 6px 12px; border-radius: 6px;
background: linear-gradient(180deg, var(--brass-3), var(--brass-2) 60%, var(--brass-1));
color: #2b1d05; font: 700 12px/1 var(--mono); letter-spacing: 0.14em;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5), 0 2px 6px rgba(0, 0, 0, 0.4);
}
h1 { margin: -4px 0 0; font: 700 clamp(26px, 2.8vw, 32px)/1.1 var(--mono); letter-spacing: -0.02em; }
.sub { margin: -6px 0 0; color: var(--muted); font-size: 15px; line-height: 1.55; text-wrap: pretty; }
.sub b { color: var(--ink); font-weight: 600; white-space: nowrap; }
.code { position: relative; }
.code input {
position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; border: 0; padding: 0;
font-size: 16px; color: transparent; caret-color: transparent; background: transparent; z-index: 2; cursor: text;
}
.wheels { display: grid; grid-template-columns: repeat(3, 1fr) 14px repeat(3, 1fr); gap: 8px; align-items: center; }
.wheels i { height: 2px; background: rgba(240, 216, 143, 0.35); border-radius: 2px; }
.slot {
position: relative; height: 68px; border-radius: 10px; overflow: hidden;
background: linear-gradient(180deg, #07090b 0%, #1d2329 18%, #2a3138 50%, #1d2329 82%, #07090b 100%);
border: 1.5px solid rgba(240, 216, 143, 0.32);
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.8), 0 1px 0 rgba(255, 255, 255, 0.05);
display: grid; place-items: center;
transition: border-color 0.15s, box-shadow 0.15s;
}
.slot b {
font: 700 32px/1 var(--mono); color: var(--brass-3);
text-shadow: 0 1px 0 #000, 0 0 12px rgba(240, 216, 143, 0.25);
translate: 0 0;
}
.slot b.roll { animation: roll 0.32s cubic-bezier(.3,1.5,.5,1); }
@keyframes roll { 0% { translate: 0 -34px; opacity: 0; } 100% { translate: 0 0; opacity: 1; } }
.code.focused .slot.current { border-color: var(--brass-3); box-shadow: inset 0 2px 6px rgba(0,0,0,.8), 0 0 0 3px rgba(240, 216, 143, 0.25); }
.code.focused .slot.current::after { content: ""; position: absolute; bottom: 14px; width: 18px; height: 3px; border-radius: 2px; background: var(--brass-3); animation: caret 1s steps(1) infinite; }
@keyframes caret { 50% { opacity: 0; } }
.code.error .slot { border-color: var(--err); }
.code.ok .slot { border-color: var(--ok); }
.code.ok .slot b { color: #c9f5da; }
.btn {
min-height: 52px; border: 0; border-radius: 12px; cursor: pointer;
background: linear-gradient(180deg, var(--brass-3), var(--brass-2) 55%, #a8832f);
color: #241703; font: 700 16px/1 var(--mono); letter-spacing: 0.02em;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55), inset 0 -2px 0 rgba(80, 50, 0, 0.35), 0 10px 24px rgba(201, 162, 74, 0.2);
}
.btn:hover { filter: brightness(1.06); }
.btn:disabled { cursor: progress; opacity: 0.8; }
.btn:focus-visible, .link:focus-visible { outline: 3px solid var(--brass-3); outline-offset: 2px; }
.msg { min-height: 22px; margin: -4px 0 0; font-size: 14px; line-height: 1.45; font-weight: 500; }
.msg.err { color: var(--err); }
.msg.ok { color: var(--ok); }
.row { display: flex; justify-content: space-between; align-items: center; gap: 12px; font-size: 14px; color: var(--muted); border-top: 1px solid rgba(255, 255, 255, 0.08); padding-top: 6px; }
.link { border: 0; background: none; min-height: 44px; padding: 0 4px; color: var(--brass-3); font: 600 14px/1 var(--mono); cursor: pointer; text-decoration: underline; text-underline-offset: 4px; }
.link:disabled { color: var(--muted); text-decoration: none; cursor: default; font-weight: 500; }
.hint { margin: -6px 0 0; font-size: 12.5px; color: #7d8892; line-height: 1.5; }
@media (max-width: 860px) {
.scene { grid-template-columns: 1fr; gap: 20px; padding: 20px 18px 36px; align-items: start; }
.vault { width: min(320px, 82vw); }
.slot { height: 58px; }
.slot b { font-size: 26px; }
.panel { padding: 30px 22px 22px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation: none !important; transition: none !important; }
}
</style>
</head>
<body>
<main class="scene">
<div class="vault" id="vault" aria-hidden="true">
<!-- frame set into the wall -->
<div class="frame">
<svg viewBox="0 0 600 600">
<defs>
<radialGradient id="frameG" cx="0.5" cy="0.5" r="0.5">
<stop offset="0.8" stop-color="#0a0c0f" />
<stop offset="0.88" stop-color="#3b434c" />
<stop offset="0.94" stop-color="#7f8b96" />
<stop offset="1" stop-color="#23292f" />
</radialGradient>
</defs>
<circle cx="300" cy="300" r="298" fill="url(#frameG)" />
<circle cx="300" cy="300" r="298" fill="none" stroke="rgba(0,0,0,.6)" stroke-width="2" />
<g fill="#05070a">
<rect x="292" y="10" width="16" height="28" rx="3" /><rect x="292" y="562" width="16" height="28" rx="3" />
<rect x="10" y="292" width="28" height="16" rx="3" /><rect x="562" y="292" width="28" height="16" rx="3" />
<rect x="88" y="88" width="16" height="28" rx="3" transform="rotate(-45 96 102)" /><rect x="496" y="88" width="16" height="28" rx="3" transform="rotate(45 504 102)" />
<rect x="88" y="484" width="16" height="28" rx="3" transform="rotate(45 96 498)" /><rect x="496" y="484" width="16" height="28" rx="3" transform="rotate(-45 504 498)" />
</g>
</svg>
</div>
<div class="interior"><span>You're in</span></div>
<div class="door">
<svg viewBox="0 0 600 600">
<defs>
<radialGradient id="doorG" cx="0.42" cy="0.38" r="0.75">
<stop offset="0" stop-color="#c4cdd5" />
<stop offset="0.45" stop-color="#7d8994" />
<stop offset="0.85" stop-color="#3d464f" />
<stop offset="1" stop-color="#262c32" />
</radialGradient>
<radialGradient id="brassG" cx="0.4" cy="0.35" r="0.8">
<stop offset="0" stop-color="#fbe7a6" />
<stop offset="0.5" stop-color="#c9a24a" />
<stop offset="1" stop-color="#6e5220" />
</radialGradient>
<radialGradient id="faceG" cx="0.45" cy="0.4" r="0.7">
<stop offset="0" stop-color="#2e353c" />
<stop offset="1" stop-color="#0d1013" />
</radialGradient>
<linearGradient id="boltG" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#5d6873" /><stop offset="0.5" stop-color="#d6dde3" /><stop offset="1" stop-color="#5d6873" />
</linearGradient>
<radialGradient id="rivetG" cx="0.35" cy="0.3" r="0.7">
<stop offset="0" stop-color="#e3e8ec" /><stop offset="1" stop-color="#4b545d" />
</radialGradient>
</defs>
<!-- bolts (slide inward when unlocked) -->
<g>
<rect class="bolt" style="--bx:0px;--by:34px" x="288" y="-6" width="24" height="56" rx="4" fill="url(#boltG)" />
<rect class="bolt" style="--bx:0px;--by:-34px" x="288" y="550" width="24" height="56" rx="4" fill="url(#boltG)" />
<rect class="bolt" style="--bx:34px;--by:0px" x="-6" y="288" width="56" height="24" rx="4" fill="url(#boltG)" />
<rect class="bolt" style="--bx:-34px;--by:0px" x="550" y="288" width="56" height="24" rx="4" fill="url(#boltG)" />
<g transform="rotate(45 300 300)">
<rect class="bolt" style="--bx:0px;--by:34px" x="288" y="-6" width="24" height="56" rx="4" fill="url(#boltG)" />
<rect class="bolt" style="--bx:0px;--by:-34px" x="288" y="550" width="24" height="56" rx="4" fill="url(#boltG)" />
<rect class="bolt" style="--bx:34px;--by:0px" x="-6" y="288" width="56" height="24" rx="4" fill="url(#boltG)" />
<rect class="bolt" style="--bx:-34px;--by:0px" x="550" y="288" width="56" height="24" rx="4" fill="url(#boltG)" />
</g>
</g>
<!-- door slab -->
<circle cx="300" cy="300" r="272" fill="url(#doorG)" />
<g fill="none" stroke="#fff" stroke-opacity="0.045">
<circle cx="300" cy="300" r="250" /><circle cx="300" cy="300" r="236" /><circle cx="300" cy="300" r="222" /><circle cx="300" cy="300" r="208" />
<circle cx="300" cy="300" r="194" /><circle cx="300" cy="300" r="180" /><circle cx="300" cy="300" r="166" /><circle cx="300" cy="300" r="152" />
</g>
<circle cx="300" cy="300" r="272" fill="none" stroke="#1a1f24" stroke-width="6" />
<circle cx="300" cy="300" r="258" fill="none" stroke="rgba(255,255,255,.12)" stroke-width="2" />
<!-- rivets -->
<g fill="url(#rivetG)" id="rivets"></g>
<!-- brand plate + lamp -->
<rect x="236" y="74" width="128" height="30" rx="5" fill="url(#brassG)" />
<text x="300" y="95" text-anchor="middle" style="font-family: var(--mono)" font-weight="700" font-size="14" letter-spacing="3" fill="#2b1d05">VAULT 07</text>
<circle class="lamp-glow" cx="300" cy="134" r="16" filter="blur(6px)" />
<circle cx="300" cy="134" r="9" fill="#111" />
<circle class="lamp" cx="300" cy="134" r="6.5" />
<!-- spoked handle wheel -->
<g class="wheel">
<g stroke="#9aa6b1" stroke-width="12" stroke-linecap="round">
<line x1="300" y1="300" x2="300" y2="122" /><line x1="300" y1="300" x2="478" y2="300" />
<line x1="300" y1="300" x2="300" y2="478" /><line x1="300" y1="300" x2="122" y2="300" />
</g>
<g stroke="#e3e8ec" stroke-width="3" stroke-linecap="round" opacity="0.5">
<line x1="296" y1="292" x2="296" y2="126" /><line x1="306" y1="296" x2="474" y2="296" />
</g>
<g fill="url(#brassG)" stroke="#5c4418" stroke-width="2">
<circle cx="300" cy="114" r="16" /><circle cx="486" cy="300" r="16" /><circle cx="300" cy="486" r="16" /><circle cx="114" cy="300" r="16" />
</g>
</g>
<!-- combination dial -->
<g class="dial-wrap">
<circle cx="300" cy="300" r="112" fill="#0b0d10" opacity="0.5" transform="translate(0 6)" />
<circle cx="300" cy="300" r="110" fill="url(#brassG)" />
<circle cx="300" cy="300" r="98" fill="#4a3714" />
<g class="dial" id="dial">
<circle cx="300" cy="300" r="95" fill="url(#faceG)" />
<g id="ticks" stroke="#d8c48a"></g>
<g id="nums" fill="#f0d88f" style="font-family: var(--mono)" font-size="20" font-weight="700" text-anchor="middle"></g>
<circle cx="300" cy="300" r="40" fill="url(#brassG)" />
<circle cx="300" cy="300" r="30" fill="#1a1f24" />
<g stroke="#8a6a2a" stroke-width="4" stroke-linecap="round">
<line x1="300" y1="276" x2="300" y2="324" />
</g>
</g>
<!-- fixed indicator -->
<path d="M300 196 L290 178 H310Z" fill="#ff7a4a" stroke="#3a1b05" stroke-width="1.5" />
</g>
</svg>
</div>
</div>
<form class="panel" id="form" novalidate>
<span class="screw tl"></span><span class="screw tr"></span><span class="screw bl"></span><span class="screw br"></span>
<p class="plate">Two-step check</p>
<h1>Enter your code</h1>
<p class="sub">We texted a code to <b>(949) ••• ••42</b>. Each digit you type turns the dial.</p>
<div class="code" id="code">
<label for="otp" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap">6-digit code</label>
<input id="otp" name="otp" inputmode="numeric" autocomplete="one-time-code" maxlength="6" aria-describedby="msg">
<div class="wheels" aria-hidden="true">
<span class="slot"><b></b></span><span class="slot"><b></b></span><span class="slot"><b></b></span>
<i></i>
<span class="slot"><b></b></span><span class="slot"><b></b></span><span class="slot"><b></b></span>
</div>
</div>
<button class="btn" id="verify" type="submit">Unlock</button>
<p class="msg" id="msg" role="status" aria-live="polite"></p>
<div class="row">
<span>Didn't get it?</span>
<button class="link" id="resend" type="button" disabled>Resend in 30s</button>
</div>
<p class="hint">Demo: any 6 digits unlock; 000000 shows the error. Try pasting "482 913".</p>
</form>
</main>
<script>
(() => {
// Build dial ticks, numbers and rivets.
const NS = "http://www.w3.org/2000/svg";
const ticks = document.getElementById("ticks");
for (let i = 0; i < 50; i++) {
const a = (i / 50) * Math.PI * 2;
const long = i % 5 === 0;
const r1 = long ? 70 : 78, r2 = 90;
const l = document.createElementNS(NS, "line");
l.setAttribute("x1", 300 + Math.sin(a) * r1); l.setAttribute("y1", 300 - Math.cos(a) * r1);
l.setAttribute("x2", 300 + Math.sin(a) * r2); l.setAttribute("y2", 300 - Math.cos(a) * r2);
l.setAttribute("stroke-width", long ? 3 : 1.4);
l.setAttribute("opacity", long ? 1 : 0.6);
ticks.append(l);
}
const nums = document.getElementById("nums");
for (let d = 0; d < 10; d++) {
const a = (d / 10) * Math.PI * 2;
const t = document.createElementNS(NS, "text");
t.setAttribute("x", 300 + Math.sin(a) * 56); t.setAttribute("y", 300 - Math.cos(a) * 56 + 7);
t.setAttribute("transform", `rotate(${d * 36} ${300 + Math.sin(a) * 56} ${300 - Math.cos(a) * 56})`);
t.textContent = d; nums.append(t);
}
const rivets = document.getElementById("rivets");
for (let i = 0; i < 24; i++) {
const a = (i / 24) * Math.PI * 2 + Math.PI / 24;
const c = document.createElementNS(NS, "circle");
c.setAttribute("cx", 300 + Math.sin(a) * 244); c.setAttribute("cy", 300 - Math.cos(a) * 244); c.setAttribute("r", 6);
rivets.append(c);
}
const vault = document.getElementById("vault");
const dial = document.getElementById("dial");
const wrap = document.getElementById("code");
const input = document.getElementById("otp");
const slots = [...wrap.querySelectorAll(".slot")];
const form = document.getElementById("form");
const verify = document.getElementById("verify");
const msg = document.getElementById("msg");
const resend = document.getElementById("resend");
const bolts = [...document.querySelectorAll(".bolt")];
const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
let angle = 0, prev = "", lastSubmitted = "";
// Turn the dial to digit d, alternating direction like a real combination lock.
function spinTo(d, i) {
const target = -d * 36;
const dir = i % 2 === 0 ? -1 : 1;
let a = target;
while ((a - angle) * dir < 180) a += 360 * dir;
while ((a - angle) * dir > 540) a -= 360 * dir;
angle = a;
dial.style.transform = `rotate(${angle}deg)`;
}
function render() {
// Be liberal in what we accept: keep only digits ("123 456", "123-456" both work).
const v = input.value.replace(/\D/g, "").slice(0, 6);
if (v !== input.value) input.value = v;
slots.forEach((s, i) => {
const b = s.querySelector("b");
const ch = v[i] || "";
if (ch !== b.textContent) {
b.textContent = ch;
if (ch && !reduced) { b.classList.remove("roll"); void b.offsetWidth; b.classList.add("roll"); }
}
s.classList.toggle("current", i === Math.min(v.length, 5) && v.length < 6);
});
if (v.length > prev.length) spinTo(Number(v[v.length - 1]), v.length - 1);
prev = v;
wrap.classList.remove("error");
vault.classList.remove("error");
if (v.length === 6 && v !== lastSubmitted) { lastSubmitted = v; form.requestSubmit(); }
if (v.length < 6) lastSubmitted = "";
}
input.addEventListener("input", () => { if (!vault.classList.contains("unlocked")) { msg.className = "msg"; msg.textContent = ""; } render(); });
input.addEventListener("focus", () => { wrap.classList.add("focused"); render(); });
input.addEventListener("blur", () => wrap.classList.remove("focused"));
input.addEventListener("keyup", () => input.setSelectionRange(input.value.length, input.value.length));
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
form.addEventListener("submit", async (e) => {
e.preventDefault();
if (vault.classList.contains("unlocked")) return;
const v = input.value;
if (v.length < 6) {
msg.className = "msg err"; msg.textContent = `Enter all 6 digits. ${6 - v.length} to go.`;
input.focus(); return;
}
verify.disabled = true; verify.textContent = "Checking…";
vault.classList.add("pending");
await wait(800); // your real verification request goes here
vault.classList.remove("pending");
verify.disabled = false; verify.textContent = "Unlock";
if (v === "000000") {
wrap.classList.add("error");
vault.classList.remove("shake", "error"); void vault.offsetWidth; vault.classList.add("shake", "error");
msg.className = "msg err";
msg.textContent = "That code didn't open it. Check the newest text, or send a new code.";
input.focus(); input.select();
return;
}
wrap.classList.add("ok");
msg.className = "msg ok"; msg.textContent = "Verified. You're signed in.";
verify.textContent = "Unlocked"; verify.disabled = true;
// Bolts retract one by one, then the door swings open.
bolts.forEach((b, i) => { b.style.transitionDelay = reduced ? "0s" : `${i * 70}ms`; });
vault.classList.add("unlocked");
await wait(reduced ? 0 : 200);
vault.classList.add("open");
});
// Resend: a visible countdown instead of a hidden link.
let t;
function countdown(sec) {
clearInterval(t); resend.disabled = true;
const tick = () => {
resend.textContent = sec > 0 ? `Resend in ${sec}s` : "Send a new code";
if (sec-- <= 0) { resend.disabled = false; clearInterval(t); }
};
tick(); t = setInterval(tick, 1000);
}
resend.addEventListener("click", () => {
msg.className = "msg ok"; msg.textContent = "New code sent.";
input.value = ""; prev = ""; render(); input.focus(); countdown(30);
});
countdown(30);
render();
})();
</script>
</body>
</html>