Polish owl login after design review; add DESIGN.md

Fixes from the finish review of the natural owl:
- thinner glasses frames, closer to the user's reference owl
- smile-eyes on success instead of flat half-lids
- the page no longer shifts when an error appears; the card grows down
- invalid field keeps a red outline while focused
- stars stay clear of the title, owl and card
- softer ear tufts, warm glow under the owl, larger owl on desktop

DESIGN.md and .impeccable/design.json record the shipped system.
The surface brief records the seed reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
VasanthandClaude Opus 5.5 committed 2026-10-22 12:04:16 +05:30
1 parent 54fa7de9b0
commit 0317358dcf
4 files changed
+387 -16

No files matched your search

+30 -15
View File
@@ -17,7 +17,7 @@
--owl: #ec7d33;
--owl-soft: #f5a25a;
--error: #ff9a9a;
--owl-w: 188px;
--owl-w: 212px;
--ui: "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, sans-serif;
color-scheme: dark;
scrollbar-color: var(--panel-line) var(--night);
@@ -51,8 +51,8 @@
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px 16px 48px;
/* Anchored near the middle rather than centered, so an error line grows the card downward instead of moving the owl. */
padding: max(32px, calc(50vh - 250px)) 16px 48px;
text-align: center;
}
h1 {
@@ -100,7 +100,7 @@
letter-spacing: .04em;
}
input:focus-visible { outline: 2px solid var(--owl); outline-offset: 2px; border-color: var(--owl); }
input[aria-invalid="true"] { border-color: var(--error); }
input[aria-invalid="true"] { border-color: var(--error); outline-color: var(--error); }
.reveal {
position: absolute;
top: 4px;
@@ -163,7 +163,8 @@
.iris { transition: transform .14s ease-out; }
.lid { transform-box: fill-box; transform-origin: 50% 0; transform: scaleY(0); transition: transform .08s ease-out; }
.owl.blink .lid { transform: scaleY(1); }
.owl.happy .lid { transform: scaleY(.55); }
.smile { transform-box: fill-box; transform-origin: 50% 100%; transform: scaleY(0); transition: transform .12s ease-out; }
.owl.happy .smile { transform: none; }
.owl.idle #bob { animation: breathe 3.2s ease-in-out infinite; }
@keyframes breathe { 50% { transform: translateY(1.5px); } }
@@ -193,7 +194,7 @@
@keyframes flap-r { 20%, 60% { transform: rotate(-32deg); } 40%, 80% { transform: rotate(-8deg); } }
@media (max-width: 420px) {
:root { --owl-w: 164px; }
:root { --owl-w: 172px; }
}
@media (prefers-reduced-motion: reduce) {
.sky .tw, .owl, .owl * { animation: none !important; transition: none !important; }
@@ -229,10 +230,14 @@
<linearGradient id="beak" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#f4a142"/><stop offset="1" stop-color="#cf6620"/>
</linearGradient>
<radialGradient id="glow">
<stop offset="0" stop-color="#ffaa5a" stop-opacity=".35"/><stop offset="1" stop-color="#ffaa5a" stop-opacity="0"/>
</radialGradient>
<clipPath id="eye-l"><circle cx="59" cy="59" r="16"/></clipPath>
<clipPath id="eye-r"><circle cx="101" cy="59" r="16"/></clipPath>
</defs>
<ellipse cx="80" cy="146" rx="54" ry="8" fill="url(#glow)"/>
<g id="body">
<path fill="url(#fur)" d="M80 66C114 66 136 90 134 116C132 136 110 146 80 146C50 146 28 136 26 116C24 90 46 66 80 66Z"/>
<ellipse cx="80" cy="116" rx="30" ry="24" fill="url(#belly)"/>
@@ -253,7 +258,7 @@
</g>
<g id="bob"><g id="head">
<path fill="url(#fur)" d="M32 38C29 24 32 12 38 5C44 16 52 22 60 22ZM128 38C131 24 128 12 122 5C116 16 108 22 100 22Z"/>
<path fill="url(#fur)" d="M33 38C29 26 31 14 37 8C40 6 42 9 45 14C49 19 54 22 60 22ZM127 38C131 26 129 14 123 8C120 6 118 9 115 14C111 19 106 22 100 22Z"/>
<path fill="url(#fur)" d="M80 10C112 10 138 30 138 58C138 82 114 98 80 98C46 98 22 82 22 58C22 30 48 10 80 10Z"/>
<path fill="none" stroke="#f9b978" stroke-width="2" stroke-linecap="round" opacity=".7" d="M62 22q4-5 9-2M77 17q4-4 8 0M90 20q4-3 8 1"/>
<path fill="url(#face)" d="M80 34C96 26 124 30 128 54C132 76 112 92 94 88C88 87 84 90 80 94C76 90 72 87 66 88C48 92 28 76 32 54C36 30 64 26 80 34Z"/>
@@ -262,11 +267,13 @@
<circle cx="59" cy="59" r="16" fill="#fffaf2"/>
<g class="iris"><circle cx="59" cy="59" r="10.5" fill="url(#iris)"/><circle cx="59" cy="59" r="5.5" fill="#1b0e08"/><circle cx="55" cy="55" r="3" fill="#fff"/><circle cx="62.5" cy="62.5" r="1.4" fill="#fff" opacity=".8"/></g>
<rect class="lid" x="43" y="43" width="32" height="32" fill="#eebc8d"/>
<g class="smile"><path fill="#eebc8d" d="M43 63Q59 49 75 63L75 76L43 76Z"/><path fill="none" stroke="#6a3211" stroke-width="1.6" stroke-linecap="round" d="M45 62Q59 50 73 62"/></g>
</g>
<g clip-path="url(#eye-r)">
<circle cx="101" cy="59" r="16" fill="#fffaf2"/>
<g class="iris"><circle cx="101" cy="59" r="10.5" fill="url(#iris)"/><circle cx="101" cy="59" r="5.5" fill="#1b0e08"/><circle cx="97" cy="55" r="3" fill="#fff"/><circle cx="104.5" cy="62.5" r="1.4" fill="#fff" opacity=".8"/></g>
<rect class="lid" x="85" y="43" width="32" height="32" fill="#eebc8d"/>
<g class="smile"><path fill="#eebc8d" d="M85 63Q101 49 117 63L117 76L85 76Z"/><path fill="none" stroke="#6a3211" stroke-width="1.6" stroke-linecap="round" d="M87 62Q101 50 115 62"/></g>
</g>
<path fill="url(#beak)" d="M75 72C77 68 83 68 85 72C84 78 82 83 80 86C78 83 76 78 75 72Z"/>
@@ -274,9 +281,9 @@
<g id="glasses" fill="none" stroke-linecap="round">
<path stroke="#fff" stroke-width="2.2" opacity=".5" d="M46 47Q52 41 60 41M88 47Q94 41 102 41"/>
<g stroke="#1d1b20">
<circle cx="59" cy="58" r="20" stroke-width="4.5"/>
<circle cx="101" cy="58" r="20" stroke-width="4.5"/>
<path stroke-width="4" d="M77.5 54Q80 50 82.5 54M39.5 52L28 48M120.5 52L132 48"/>
<circle cx="59" cy="58" r="20" stroke-width="2.4"/>
<circle cx="101" cy="58" r="20" stroke-width="2.4"/>
<path stroke-width="2.2" d="M78.5 54Q80 51.5 81.5 54M39 52L28 48M121 52L132 48"/>
</g>
</g>
</g></g>
@@ -318,17 +325,25 @@ const done = document.getElementById("done");
const irises = owl.querySelectorAll(".iris");
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
(function drawSky(el) {
// Seeded so the stars stay put; none behind the text, owl or card.
function drawSky() {
let seed = 7;
const rand = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
const colors = ["#6ea8ff", "#4fd1c5", "#f29c4b", "#dfe7ff"];
const keepOut = [...document.querySelectorAll("h1, .sub, .card, .owl")].map(e => e.getBoundingClientRect());
let html = "";
for (let i = 0; i < 60; i++) {
html += `<span class="${rand() < .35 ? "tw" : ""}" style="left:${(rand() * 100).toFixed(2)}%;top:${(rand() * 100).toFixed(2)}%;` +
`--s:${(1.5 + rand() * 1.5).toFixed(1)}px;--c:${colors[Math.floor(rand() * 4)]};--o:${(.45 + rand() * .45).toFixed(2)};--d:${(rand() * 4).toFixed(2)}s"></span>`;
const tw = rand() < .35, x = rand() * innerWidth, y = rand() * innerHeight;
const size = (1.5 + rand() * 1.5).toFixed(1), color = colors[Math.floor(rand() * 4)];
const alpha = (.45 + rand() * .45).toFixed(2), delay = (rand() * 4).toFixed(2);
if (keepOut.some(r => x > r.left - 10 && x < r.right + 10 && y > r.top - 10 && y < r.bottom + 10)) continue;
html += `<span class="${tw ? "tw" : ""}" style="left:${x.toFixed(0)}px;top:${y.toFixed(0)}px;` +
`--s:${size}px;--c:${color};--o:${alpha};--d:${delay}s"></span>`;
}
el.innerHTML = html;
})(document.getElementById("sky"));
document.getElementById("sky").innerHTML = html;
}
drawSky();
addEventListener("resize", drawSky);
function look(dx, dy) {
irises.forEach(g => { g.style.transform = `translate(${dx}px, ${dy}px)`; });