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>Night owl login | 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=Nunito:wght@500;700;800;900&display=swap" rel="stylesheet">
<!--
Flow login pattern: Night owl (https://flow.donta.io/screens/login/night-owl)
A moonlit forest with an owl keeping watch. Its eyes follow what you type in
the email field, its wings fold over its eyes while you type your password,
one wing lifts to peek when you press Show, it ruffles and frowns on an error
and hoots with happy eyes on success. The owl and forest are decorative
(aria-hidden); the sign-in form is a real, fully usable form from the start.
Demo: any email and an 8+ character password signs in; the password "wrongpass" shows the error state.
-->
<style>
:root {
--night-1: #070a1f;
--night-2: #141244;
--night-3: #2a1d63;
--moon: #f3ecd2;
--amber: #f6c25b;
--amber-ink: #2a1803;
--ink: #eeeaff;
--muted: #b1acdb;
--line: rgba(196, 186, 255, 0.22);
--field: rgba(9, 10, 32, 0.66);
--err: #ff9b85;
--ok: #9be7c4;
--font: "Nunito", ui-rounded, "SF Pro Rounded", "Segoe UI", system-ui, sans-serif;
}
* { box-sizing: border-box; }
html, body { margin: 0; min-height: 100%; }
body {
min-height: 100vh;
font-family: var(--font);
color: var(--ink);
background: radial-gradient(120% 90% at 30% 10%, var(--night-3) 0%, var(--night-2) 45%, var(--night-1) 100%);
overflow-x: hidden;
}
/* ── Forest backdrop ───────────────────────── */
.forest { position: fixed; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.firefly { animation: drift 9s ease-in-out infinite alternate, glow 3.2s ease-in-out infinite; transform-box: fill-box; }
.firefly:nth-of-type(2n) { animation-duration: 11s, 4.1s; animation-delay: -3s, -1s; }
.firefly:nth-of-type(3n) { animation-duration: 13s, 2.7s; animation-delay: -6s, -2s; }
@keyframes drift { 0% { translate: 0 0; } 50% { translate: 18px -14px; } 100% { translate: -12px 10px; } }
@keyframes glow { 0%, 100% { opacity: 0.25; } 50% { opacity: 1; } }
.twinkle { animation: glow 4s ease-in-out infinite; }
.twinkle:nth-of-type(odd) { animation-delay: -2s; }
/* ── Layout ────────────────────────────────── */
.scene {
position: relative;
min-height: 100vh;
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(340px, 0.85fr);
align-items: center;
gap: 24px;
padding: 32px clamp(20px, 5vw, 72px);
max-width: 1280px;
margin: 0 auto;
}
.perch { position: relative; align-self: end; height: min(78vh, 640px); }
.owl-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
/* ── Owl parts ─────────────────────────────── */
.owl { transition: translate 0.3s ease; }
.pupils { transition: transform 0.14s ease-out; }
.lid { transform-box: fill-box; transform-origin: 50% 0; transform: scaleY(0); transition: transform 0.09s ease-in; }
.owl.blink .lid { transform: scaleY(1); }
.happy-eyes { opacity: 0; transition: opacity 0.15s; }
.owl.happy .happy-eyes { opacity: 1; }
.owl.happy .eyes-open { opacity: 0; }
.eyes-open { transition: opacity 0.15s; }
.tuft { transform-box: fill-box; transition: transform 0.3s cubic-bezier(.3,1.5,.5,1); }
.tuft-l { transform-origin: 100% 100%; }
.tuft-r { transform-origin: 0% 100%; }
.owl.cross .tuft-l { transform: rotate(-28deg); }
.owl.cross .tuft-r { transform: rotate(28deg); }
.brow { opacity: 0; transition: opacity 0.2s; }
.owl.cross .brow { opacity: 1; }
.wing { transition: transform 0.5s cubic-bezier(.34,1.4,.5,1); }
.wing-l { transform-origin: 112px 300px; }
.wing-r { transform-origin: 288px 300px; }
.owl.cover .wing-l { transform: rotate(-170deg); }
.owl.cover .wing-r { transform: rotate(170deg); }
.owl.cover.peek .wing-r { transform: rotate(-58deg); }
.owl.shake { animation: ruffle 0.5s ease; }
@keyframes ruffle { 15%, 55% { translate: -9px 0; rotate: -2deg; } 35%, 75% { translate: 9px 0; rotate: 2deg; } }
.owl.hop { animation: hop 0.7s cubic-bezier(.3,1.4,.5,1); }
@keyframes hop { 0% { translate: 0 0; } 35% { translate: 0 -34px; } 70% { translate: 0 0; } 82% { translate: 0 -8px; } 100% { translate: 0 0; } }
.hoot { opacity: 0; translate: 0 10px; transition: opacity 0.25s, translate 0.35s cubic-bezier(.3,1.5,.5,1); }
.owl.happy .hoot { opacity: 1; translate: 0 0; }
.hoot text { font-family: var(--font); font-weight: 900; }
/* ── Panel ─────────────────────────────────── */
.panel {
position: relative;
width: min(400px, 100%);
justify-self: center;
padding: 34px 32px 28px;
border-radius: 26px;
background: linear-gradient(160deg, rgba(38, 34, 92, 0.78), rgba(12, 12, 40, 0.84));
border: 1px solid var(--line);
box-shadow: 0 30px 80px rgba(2, 3, 18, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.08);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
display: grid;
gap: 18px;
}
.moonmark { position: absolute; top: -22px; right: 28px; width: 44px; height: 44px; border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #fffbe8, var(--moon) 55%, #d9cfa6);
box-shadow: 0 0 30px rgba(243, 236, 210, 0.55); }
.eyebrow { margin: 0; font-size: 14px; font-weight: 700; color: var(--amber); letter-spacing: 0.01em; }
h1 { margin: -10px 0 0; font-size: clamp(30px, 3.2vw, 38px); line-height: 1.05; font-weight: 900; letter-spacing: -0.02em; }
.sub { margin: -6px 0 2px; color: var(--muted); font-size: 16px; line-height: 1.45; }
label { display: grid; gap: 7px; font-size: 14px; font-weight: 800; color: var(--ink); }
.field { position: relative; display: block; }
input {
width: 100%; min-height: 50px; padding: 0 16px; border-radius: 14px;
border: 1.5px solid var(--line); background: var(--field); color: var(--ink);
font: 600 16px/1 var(--font);
transition: border-color 0.15s, box-shadow 0.15s;
}
input::placeholder { color: #7c78a8; }
input:focus { outline: none; border-color: var(--amber); box-shadow: 0 0 0 4px rgba(246, 194, 91, 0.22); }
input[aria-invalid="true"] { border-color: var(--err); box-shadow: 0 0 0 4px rgba(255, 155, 133, 0.18); }
.pw input { padding-right: 84px; }
.show {
position: absolute; right: 6px; top: 6px; bottom: 6px; min-width: 70px; border: 0; border-radius: 10px;
background: rgba(246, 194, 91, 0.12); color: var(--amber); font: 800 14px/1 var(--font); cursor: pointer;
}
.show:hover { background: rgba(246, 194, 91, 0.2); }
.show:focus-visible, .btn:focus-visible, a:focus-visible { outline: 3px solid var(--amber); outline-offset: 2px; }
.row { display: flex; justify-content: flex-end; margin-top: -6px; }
a { color: var(--amber); font-weight: 700; font-size: 14px; text-underline-offset: 3px; }
.btn {
min-height: 52px; border: 0; border-radius: 14px; cursor: pointer;
background: linear-gradient(180deg, #ffd47a, var(--amber)); color: var(--amber-ink);
font: 900 17px/1 var(--font); letter-spacing: 0.01em;
box-shadow: 0 10px 26px rgba(246, 194, 91, 0.28), inset 0 -2px 0 rgba(120, 70, 0, 0.25);
transition: transform 0.12s, box-shadow 0.2s;
}
.btn:hover { transform: translateY(-1px); box-shadow: 0 14px 30px rgba(246, 194, 91, 0.36), inset 0 -2px 0 rgba(120, 70, 0, 0.25); }
.btn:active { transform: translateY(1px); }
.btn[aria-busy="true"] { cursor: progress; opacity: 0.85; }
.msg { min-height: 22px; margin: -4px 0 0; font-size: 15px; font-weight: 700; line-height: 1.4; }
.msg.err { color: var(--err); }
.msg.ok { color: var(--ok); }
.hint { margin: 0; font-size: 13px; color: #8f8ab9; line-height: 1.45; border-top: 1px solid var(--line); padding-top: 14px; }
@media (max-width: 820px) {
.scene { grid-template-columns: 1fr; gap: 0; padding: 12px 18px 32px; align-items: start; }
.perch { height: 300px; align-self: start; }
.panel { margin-top: -6px; padding: 30px 22px 22px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation: none !important; transition: none !important; }
}
</style>
</head>
<body>
<svg class="forest" viewBox="0 0 1440 900" preserveAspectRatio="xMidYMax slice" aria-hidden="true">
<defs>
<radialGradient id="moonGlow" cx="0.5" cy="0.5" r="0.5">
<stop offset="0" stop-color="#fff6d6" stop-opacity="0.55" />
<stop offset="0.45" stop-color="#cfc3ff" stop-opacity="0.14" />
<stop offset="1" stop-color="#cfc3ff" stop-opacity="0" />
</radialGradient>
<radialGradient id="moonFace" cx="0.38" cy="0.36" r="0.7">
<stop offset="0" stop-color="#fffbea" />
<stop offset="0.6" stop-color="#f1e8c6" />
<stop offset="1" stop-color="#cfc29a" />
</radialGradient>
<radialGradient id="ff" cx="0.5" cy="0.5" r="0.5">
<stop offset="0" stop-color="#fff3a8" />
<stop offset="0.35" stop-color="#ffd86b" stop-opacity="0.7" />
<stop offset="1" stop-color="#ffd86b" stop-opacity="0" />
</radialGradient>
<linearGradient id="mist" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#3a2f86" stop-opacity="0" />
<stop offset="1" stop-color="#3a2f86" stop-opacity="0.45" />
</linearGradient>
</defs>
<!-- stars -->
<g fill="#e9e4ff">
<circle class="twinkle" cx="140" cy="90" r="1.6" /><circle cx="260" cy="40" r="1.1" /><circle class="twinkle" cx="420" cy="120" r="1.4" />
<circle cx="610" cy="60" r="1" /><circle class="twinkle" cx="760" cy="150" r="1.5" /><circle cx="880" cy="70" r="1.2" />
<circle class="twinkle" cx="1020" cy="40" r="1.4" /><circle cx="1180" cy="110" r="1" /><circle class="twinkle" cx="1330" cy="60" r="1.6" />
<circle cx="1390" cy="190" r="1.1" /><circle cx="70" cy="230" r="1" /><circle class="twinkle" cx="980" cy="220" r="1.2" />
<circle cx="560" cy="210" r="1" /><circle cx="1240" cy="260" r="1.2" />
</g>
<!-- moon -->
<circle cx="720" cy="150" r="220" fill="url(#moonGlow)" />
<circle cx="720" cy="150" r="70" fill="url(#moonFace)" />
<g fill="#d6c99f" opacity="0.55">
<circle cx="698" cy="132" r="11" /><circle cx="742" cy="172" r="8" /><circle cx="748" cy="124" r="5" /><circle cx="704" cy="180" r="4.5" />
</g>
<!-- far ridge -->
<path d="M0 640 L60 590 L110 620 L170 540 L230 600 L300 520 L360 590 L420 548 L500 610 L560 560 L640 618 L720 552 L800 606 L880 540 L960 600 L1040 560 L1120 618 L1200 548 L1280 600 L1360 556 L1440 600 V900 H0Z" fill="#1d1856" />
<!-- mid pines -->
<g fill="#141046">
<path d="M40 900 L40 700 L10 700 L60 600 L30 600 L80 500 L130 600 L100 600 L150 700 L120 700 L120 900Z" />
<path d="M1250 900 L1250 680 L1210 680 L1270 570 L1235 570 L1300 450 L1365 570 L1330 570 L1390 680 L1350 680 L1350 900Z" />
<path d="M1080 900 L1080 740 L1050 740 L1100 650 L1075 650 L1120 570 L1165 650 L1140 650 L1190 740 L1160 740 L1160 900Z" />
<path d="M560 900 L560 760 L530 760 L575 680 L550 680 L595 600 L640 680 L615 680 L660 760 L630 760 L630 900Z" />
</g>
<rect x="0" y="560" width="1440" height="340" fill="url(#mist)" />
<!-- near silhouettes -->
<g fill="#08081f">
<path d="M1440 900 V560 L1415 560 L1440 520Z" />
<path d="M0 900 V720 Q120 690 260 760 T560 780 T900 770 T1200 790 T1440 760 V900Z" />
<path d="M1360 900 L1360 640 L1320 640 L1390 520 L1350 520 L1420 400 L1440 430 V900Z" />
</g>
<!-- fireflies -->
<g>
<circle class="firefly" cx="220" cy="640" r="10" fill="url(#ff)" />
<circle class="firefly" cx="470" cy="700" r="8" fill="url(#ff)" />
<circle class="firefly" cx="760" cy="620" r="9" fill="url(#ff)" />
<circle class="firefly" cx="1010" cy="690" r="11" fill="url(#ff)" />
<circle class="firefly" cx="1190" cy="610" r="8" fill="url(#ff)" />
<circle class="firefly" cx="1310" cy="740" r="10" fill="url(#ff)" />
<circle class="firefly" cx="640" cy="520" r="7" fill="url(#ff)" />
</g>
</svg>
<main class="scene">
<div class="perch" aria-hidden="true">
<svg class="owl-svg" viewBox="0 0 400 520" preserveAspectRatio="xMidYMax meet">
<defs>
<radialGradient id="bodyG" cx="0.5" cy="0.35" r="0.75">
<stop offset="0" stop-color="#9a7560" />
<stop offset="0.7" stop-color="#6e4f42" />
<stop offset="1" stop-color="#4c3530" />
</radialGradient>
<radialGradient id="bellyG" cx="0.5" cy="0.3" r="0.8">
<stop offset="0" stop-color="#f1dfc1" />
<stop offset="1" stop-color="#c9a985" />
</radialGradient>
<radialGradient id="discG" cx="0.5" cy="0.45" r="0.6">
<stop offset="0" stop-color="#f6e6c8" />
<stop offset="1" stop-color="#d2b48e" />
</radialGradient>
<radialGradient id="irisG" cx="0.5" cy="0.45" r="0.55">
<stop offset="0" stop-color="#ffe9a6" />
<stop offset="0.55" stop-color="#f6c25b" />
<stop offset="1" stop-color="#d98a1f" />
</radialGradient>
<linearGradient id="wingG" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#7a5848" />
<stop offset="1" stop-color="#4a332c" />
</linearGradient>
<linearGradient id="branchG" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#3a2a3d" />
<stop offset="1" stop-color="#1a1224" />
</linearGradient>
<radialGradient id="rim" cx="0.15" cy="0.2" r="0.9">
<stop offset="0" stop-color="#cfc3ff" stop-opacity="0.35" />
<stop offset="0.5" stop-color="#cfc3ff" stop-opacity="0" />
</radialGradient>
</defs>
<!-- branch, running off to the left edge -->
<path d="M-900 470 C-500 452 -150 468 60 460 C160 456 260 472 360 462 C392 459 414 448 430 432 L440 440 C426 460 400 476 360 482 C250 494 150 480 60 486 C-150 494 -500 498 -900 500Z" fill="url(#branchG)" />
<path d="M300 470 C330 440 350 420 392 404" stroke="#24192e" stroke-width="9" fill="none" stroke-linecap="round" />
<ellipse cx="400" cy="398" rx="16" ry="7" fill="#2d5a4a" transform="rotate(-24 400 398)" />
<ellipse cx="378" cy="414" rx="12" ry="5" fill="#244a3d" transform="rotate(18 378 414)" />
<path d="M-200 476 C-120 470 -40 474 20 470" stroke="#4a3850" stroke-width="2" fill="none" opacity="0.6" />
<g class="owl" id="owl">
<!-- tufts -->
<path class="tuft tuft-l" d="M96 128 L72 40 L150 108Z" fill="#5a3e34" />
<path class="tuft tuft-l" d="M104 120 L88 66 L140 110Z" fill="#7d5c4b" />
<path class="tuft tuft-r" d="M304 128 L328 40 L250 108Z" fill="#5a3e34" />
<path class="tuft tuft-r" d="M296 120 L312 66 L260 110Z" fill="#7d5c4b" />
<!-- body -->
<path d="M200 70 C300 70 352 150 350 260 C348 380 292 460 200 462 C108 460 52 380 50 260 C48 150 100 70 200 70Z" fill="url(#bodyG)" />
<path d="M200 70 C300 70 352 150 350 260 C348 380 292 460 200 462 C108 460 52 380 50 260 C48 150 100 70 200 70Z" fill="url(#rim)" />
<!-- belly with chevron feathers -->
<path d="M200 220 C270 220 300 300 296 360 C292 420 250 452 200 452 C150 452 108 420 104 360 C100 300 130 220 200 220Z" fill="url(#bellyG)" />
<g fill="none" stroke="#a9845f" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round" opacity="0.8">
<path d="M160 280 l10 9 l10 -9" /><path d="M196 272 l10 9 l10 -9" /><path d="M232 280 l10 9 l10 -9" />
<path d="M140 318 l10 9 l10 -9" /><path d="M178 312 l10 9 l10 -9" /><path d="M214 312 l10 9 l10 -9" /><path d="M250 318 l10 9 l10 -9" />
<path d="M156 354 l10 9 l10 -9" /><path d="M196 350 l10 9 l10 -9" /><path d="M236 354 l10 9 l10 -9" />
<path d="M174 390 l10 9 l10 -9" /><path d="M214 390 l10 9 l10 -9" />
<path d="M194 424 l10 9 l10 -9" />
</g>
<!-- talons -->
<g fill="#e09a3c" stroke="#a8631a" stroke-width="1.5">
<path d="M156 452 q-6 18 -2 26 q6 -6 6 -22Z" /><path d="M168 454 q-2 18 2 26 q5 -8 2 -24Z" /><path d="M180 452 q4 16 10 22 q2 -10 -4 -22Z" />
<path d="M220 452 q-6 12 -4 22 q8 -6 10 -22Z" /><path d="M232 454 q2 18 -2 26 q-5 -8 -2 -24Z" /><path d="M244 452 q6 18 2 26 q-6 -6 -6 -22Z" />
</g>
<!-- face disc -->
<path d="M200 116 C236 96 300 104 316 160 C328 206 298 246 252 246 C228 246 210 236 200 224 C190 236 172 246 148 246 C102 246 72 206 84 160 C100 104 164 96 200 116Z" fill="url(#discG)" />
<path d="M200 116 C236 96 300 104 316 160 C328 206 298 246 252 246 C228 246 210 236 200 224 C190 236 172 246 148 246 C102 246 72 206 84 160 C100 104 164 96 200 116Z" fill="none" stroke="#8a6650" stroke-width="3" opacity="0.5" />
<!-- eyes -->
<g class="eyes-open">
<circle cx="148" cy="172" r="46" fill="#3a2620" />
<circle cx="252" cy="172" r="46" fill="#3a2620" />
<circle cx="148" cy="172" r="38" fill="url(#irisG)" />
<circle cx="252" cy="172" r="38" fill="url(#irisG)" />
<g class="pupils" id="pupils">
<circle cx="148" cy="174" r="18" fill="#140c08" />
<circle cx="252" cy="174" r="18" fill="#140c08" />
<circle cx="156" cy="164" r="6.5" fill="#fff" />
<circle cx="260" cy="164" r="6.5" fill="#fff" />
<circle cx="142" cy="182" r="2.6" fill="#fff" opacity="0.8" />
<circle cx="246" cy="182" r="2.6" fill="#fff" opacity="0.8" />
</g>
<ellipse class="lid" cx="148" cy="172" rx="47" ry="47" fill="#7a5848" />
<ellipse class="lid" cx="252" cy="172" rx="47" ry="47" fill="#7a5848" />
</g>
<g class="happy-eyes" fill="none" stroke="#3a2620" stroke-width="9" stroke-linecap="round">
<path d="M118 182 Q148 146 178 182" />
<path d="M222 182 Q252 146 282 182" />
</g>
<!-- frown brows (error) -->
<g class="brow" stroke="#4a302a" stroke-width="10" stroke-linecap="round">
<path d="M108 118 L182 140" /><path d="M292 118 L218 140" />
</g>
<!-- beak -->
<path d="M200 196 C214 206 216 222 200 252 C184 222 186 206 200 196Z" fill="#eaa443" />
<path d="M200 196 C208 204 210 214 200 232" fill="none" stroke="#fff1c9" stroke-width="2.5" opacity="0.7" stroke-linecap="round" />
<!-- wings (rotate up over the eyes for the password) -->
<g class="wing wing-l">
<path d="M112 300 C56 294 34 366 46 428 C56 468 98 484 128 470 C150 430 150 352 112 300Z" fill="url(#wingG)" />
<g fill="none" stroke="#3a2620" stroke-width="2.4" stroke-linecap="round" opacity="0.55">
<path d="M72 380 q20 4 44 -6" /><path d="M70 410 q22 6 48 -4" /><path d="M78 440 q20 4 40 -4" />
</g>
</g>
<g class="wing wing-r">
<path d="M288 300 C344 294 366 366 354 428 C344 468 302 484 272 470 C250 430 250 352 288 300Z" fill="url(#wingG)" />
<g fill="none" stroke="#3a2620" stroke-width="2.4" stroke-linecap="round" opacity="0.55">
<path d="M328 380 q-20 4 -44 -6" /><path d="M330 410 q-22 6 -48 -4" /><path d="M322 440 q-20 4 -40 -4" />
</g>
</g>
<!-- hoot bubble -->
<g class="hoot" transform="translate(6 -34)">
<path d="M296 36 h86 a16 16 0 0 1 16 16 v28 a16 16 0 0 1 -16 16 h-58 l-18 16 v-16 h-10 a16 16 0 0 1 -16 -16 v-28 a16 16 0 0 1 16 -16Z" fill="#f3ecd2" />
<text x="339" y="74" text-anchor="middle" font-size="22" fill="#2a1d63">hoo hoo!</text>
</g>
</g>
</svg>
</div>
<form class="panel" id="form" novalidate>
<p class="eyebrow">Night Owl</p>
<h1>Welcome back</h1>
<p class="sub">Sign in to pick up where you left off. The owl's keeping watch.</p>
<label>Email
<input id="email" name="email" type="email" autocomplete="username" inputmode="email" placeholder="you@example.com" required>
</label>
<label>Password
<span class="field pw">
<input id="password" name="password" type="password" autocomplete="current-password" required minlength="8">
<button class="show" id="show" type="button" aria-pressed="false" aria-controls="password">Show</button>
</span>
</label>
<div class="row"><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 and an 8+ character password. Use "wrongpass" to see the error.</p>
</form>
</main>
<script>
(() => {
const owl = document.getElementById("owl");
const pupils = document.getElementById("pupils");
const form = document.getElementById("form");
const email = document.getElementById("email");
const password = document.getElementById("password");
const show = document.getElementById("show");
const submit = document.getElementById("submit");
const msg = document.getElementById("msg");
const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
const look = (x, y) => { pupils.style.transform = `translate(${x}px, ${y}px)`; };
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const covered = () => owl.classList.contains("cover");
// Eyes follow the caret while typing the email (the form is to the owl's right).
function followCaret() {
const n = Math.min(email.value.length, 34);
look(-8 + (n / 34) * 22, 9);
}
email.addEventListener("focus", () => { owl.classList.remove("cover", "peek", "cross"); followCaret(); });
email.addEventListener("input", followCaret);
// Wings over the eyes while the password is being typed.
password.addEventListener("focus", () => { owl.classList.remove("cross", "happy"); owl.classList.add("cover"); });
const uncoverLater = () => setTimeout(() => {
const a = document.activeElement;
if (a !== password && a !== show) owl.classList.remove("cover", "peek");
}, 0);
password.addEventListener("blur", uncoverLater);
show.addEventListener("blur", uncoverLater);
show.addEventListener("click", () => {
const visible = password.type === "password";
password.type = visible ? "text" : "password";
show.textContent = visible ? "Hide" : "Show";
show.setAttribute("aria-pressed", String(visible));
owl.classList.add("cover");
owl.classList.toggle("peek", visible);
if (visible) look(10, 8);
password.focus();
});
// Away from the form, the eyes follow the pointer.
document.addEventListener("pointermove", (e) => {
if (document.activeElement === email || covered()) return;
const r = owl.getBoundingClientRect();
const cx = r.left + r.width / 2, cy = r.top + r.height * 0.3;
look(clamp((e.clientX - cx) / 30, -14, 14), clamp((e.clientY - cy) / 40, -10, 12));
});
// Occasional blink.
if (!reduced) {
(function blinkLoop() {
setTimeout(() => {
if (!covered() && !owl.classList.contains("happy")) {
owl.classList.add("blink");
setTimeout(() => owl.classList.remove("blink"), 140);
}
blinkLoop();
}, 2600 + Math.random() * 3200);
})();
}
function replay(cls) { owl.classList.remove(cls); void owl.getBoundingClientRect(); owl.classList.add(cls); }
form.addEventListener("submit", async (e) => {
e.preventDefault();
msg.className = "msg"; msg.textContent = "";
email.removeAttribute("aria-invalid"); password.removeAttribute("aria-invalid");
owl.classList.remove("happy");
const fail = (input, text) => {
input.focus();
input.setAttribute("aria-invalid", "true");
msg.className = "msg err"; msg.textContent = text;
owl.classList.remove("cover", "peek");
owl.classList.add("cross");
replay("shake");
look(0, 4);
};
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.");
submit.setAttribute("aria-busy", "true"); submit.disabled = true; submit.textContent = "Signing in…";
await new Promise((r) => setTimeout(r, 750)); // your real request goes here
submit.removeAttribute("aria-busy"); submit.disabled = false; submit.textContent = "Sign in";
if (password.value === "wrongpass") return fail(password, "That password doesn't match this email. Try again, or reset it.");
owl.classList.remove("cover", "peek", "cross");
owl.classList.add("happy");
replay("hop");
msg.className = "msg ok"; msg.textContent = "Signed in. Welcome back, night owl.";
});
})();
</script>
</body>
</html>