Add owl sign-in page for the G2 run screen
First UI for the ops team: a local sign-in page where an owl sits on the card, watches the password field as you type, shakes its head on a wrong password and hops on the right one. Uses a test password for now; where the real G2 password lives is still undecided. Also adds PRODUCT.md (product context for UI work) and the page's design brief under .impeccable/surfaces/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
48781f35eb
commit
54fa7de9b0
4 files changed
+514
No files matched your search
@@ -0,0 +1,421 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Sign in · Charge Billed Automation</title>
|
||||
<style>
|
||||
:root {
|
||||
--night: #040b26;
|
||||
--panel: #0c1638;
|
||||
--panel-line: #26335e;
|
||||
--field: #050c28;
|
||||
--field-line: #56689c;
|
||||
--pill-line: #c9d5f2;
|
||||
--ink: #eef2ff;
|
||||
--ink-2: #a9b6d8;
|
||||
--owl: #ec7d33;
|
||||
--owl-soft: #f5a25a;
|
||||
--error: #ff9a9a;
|
||||
--owl-w: 188px;
|
||||
--ui: "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, sans-serif;
|
||||
color-scheme: dark;
|
||||
scrollbar-color: var(--panel-line) var(--night);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { height: 100%; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--night);
|
||||
color: var(--ink);
|
||||
font: 15px/1.5 var(--ui);
|
||||
caret-color: var(--owl);
|
||||
}
|
||||
::selection { background: var(--owl); color: var(--night); }
|
||||
|
||||
.sky { position: fixed; inset: 0; overflow: hidden; pointer-events: none; }
|
||||
.sky span {
|
||||
position: absolute;
|
||||
width: var(--s);
|
||||
height: var(--s);
|
||||
border-radius: 50%;
|
||||
background: var(--c);
|
||||
opacity: var(--o);
|
||||
}
|
||||
.sky .tw { animation: twinkle 4s ease-in-out infinite; animation-delay: var(--d); }
|
||||
@keyframes twinkle { 50% { opacity: .15; } }
|
||||
|
||||
.stage {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px 16px 48px;
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(24px, 6vw, 30px);
|
||||
font-weight: 650;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -.01em;
|
||||
text-wrap: balance;
|
||||
}
|
||||
.sub { margin: 8px 0 0; color: var(--ink-2); }
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
width: min(360px, 100%);
|
||||
margin-top: calc(var(--owl-w) * 150 / 160 + 12px);
|
||||
padding: 28px 24px 24px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--panel-line);
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 18px 40px rgba(0, 3, 15, .5);
|
||||
text-align: left;
|
||||
}
|
||||
.owl {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: calc(100% - 7px);
|
||||
width: var(--owl-w);
|
||||
height: auto;
|
||||
margin-left: calc(var(--owl-w) / -2);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
label { display: block; font-weight: 600; margin-bottom: 8px; }
|
||||
.field { position: relative; }
|
||||
input {
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
padding: 0 52px 0 20px;
|
||||
background: var(--field);
|
||||
color: var(--ink);
|
||||
border: 1px solid var(--field-line);
|
||||
border-radius: 999px;
|
||||
font: 16px var(--ui);
|
||||
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); }
|
||||
.reveal {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
color: var(--ink-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
.reveal:hover { color: var(--ink); background: rgba(201, 213, 242, .1); }
|
||||
.reveal:focus-visible { outline: 2px solid var(--owl); outline-offset: 0; color: var(--ink); }
|
||||
.reveal svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.8;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.note, .error { margin: 8px 0 0; font-size: 14px; }
|
||||
.note { color: var(--owl-soft); }
|
||||
.error { color: var(--error); }
|
||||
.error:empty { margin: 0; }
|
||||
|
||||
.pill {
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
margin-top: 20px;
|
||||
background: #0a1435;
|
||||
color: #fff;
|
||||
border: 1.5px solid var(--pill-line);
|
||||
border-radius: 999px;
|
||||
font: 600 15px var(--ui);
|
||||
letter-spacing: .02em;
|
||||
cursor: pointer;
|
||||
transition: border-color .15s ease-out, background-color .15s ease-out;
|
||||
}
|
||||
.pill:hover { border-color: var(--owl); background: #101d45; }
|
||||
.pill:active { transform: translateY(1px); }
|
||||
.pill:focus-visible { outline: 2px solid var(--owl); outline-offset: 3px; }
|
||||
|
||||
.done h2 { margin: 0; font-size: 20px; }
|
||||
.done h2:focus { outline: none; }
|
||||
.done p { margin: 6px 0 0; color: var(--ink-2); }
|
||||
|
||||
/* Owl. Origins are in the SVG's own units (viewBox 0 0 160 150). */
|
||||
#head, #glasses, #wing-l, #wing-r { transform-box: view-box; }
|
||||
#head { transform-origin: 80px 96px; }
|
||||
#glasses { transform-origin: 80px 56px; }
|
||||
#wing-l { transform-origin: 40px 90px; }
|
||||
#wing-r { transform-origin: 120px 90px; }
|
||||
.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); }
|
||||
|
||||
.owl.idle #bob { animation: breathe 3.2s ease-in-out infinite; }
|
||||
@keyframes breathe { 50% { transform: translateY(1.5px); } }
|
||||
|
||||
.owl.shake #head { animation: no .65s ease-in-out; }
|
||||
@keyframes no {
|
||||
15% { transform: translateX(-5px) rotate(-5deg); }
|
||||
32% { transform: translateX(5px) rotate(5deg); }
|
||||
50% { transform: translateX(-4px) rotate(-4deg); }
|
||||
68% { transform: translateX(3px) rotate(3deg); }
|
||||
85% { transform: translateX(-1px) rotate(-1deg); }
|
||||
}
|
||||
.owl.shake #glasses { animation: wobble .65s ease-in-out .07s; }
|
||||
@keyframes wobble {
|
||||
20% { transform: rotate(4deg); } 40% { transform: rotate(-4deg); }
|
||||
60% { transform: rotate(3deg); } 80% { transform: rotate(-1.5deg); }
|
||||
}
|
||||
|
||||
.owl.tilt #head { animation: tilt .8s ease-in-out; }
|
||||
@keyframes tilt { 25%, 70% { transform: rotate(-10deg); } }
|
||||
|
||||
.owl.happy { animation: hop .75s cubic-bezier(.3, .6, .35, 1); }
|
||||
@keyframes hop { 40% { transform: translateY(-14px); } 70% { transform: none; } 82% { transform: translateY(-3px); } }
|
||||
.owl.happy #wing-l { animation: flap-l .75s ease-in-out; }
|
||||
.owl.happy #wing-r { animation: flap-r .75s ease-in-out; }
|
||||
@keyframes flap-l { 20%, 60% { transform: rotate(32deg); } 40%, 80% { transform: rotate(8deg); } }
|
||||
@keyframes flap-r { 20%, 60% { transform: rotate(-32deg); } 40%, 80% { transform: rotate(-8deg); } }
|
||||
|
||||
@media (max-width: 420px) {
|
||||
:root { --owl-w: 164px; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.sky .tw, .owl, .owl * { animation: none !important; transition: none !important; }
|
||||
.pill { transition: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="sky" id="sky" aria-hidden="true"></div>
|
||||
|
||||
<main class="stage">
|
||||
<h1>Charge Billed Automation</h1>
|
||||
<p class="sub">Sign in to run the bot</p>
|
||||
|
||||
<section class="card">
|
||||
<svg class="owl idle" id="owl" viewBox="0 0 160 150" aria-hidden="true">
|
||||
<defs>
|
||||
<radialGradient id="fur" cx="40%" cy="28%" r="78%">
|
||||
<stop offset="0" stop-color="#f8ad66"/><stop offset=".45" stop-color="#ee7f36"/><stop offset="1" stop-color="#c55a1f"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="wing" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#df6f2c"/><stop offset="1" stop-color="#b14e1a"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="face" cx="50%" cy="40%" r="65%">
|
||||
<stop offset="0" stop-color="#fff1df"/><stop offset="1" stop-color="#f2cba1"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="belly" cx="50%" cy="35%" r="70%">
|
||||
<stop offset="0" stop-color="#fbd6a9"/><stop offset="1" stop-color="#efb072"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="iris" cx="40%" cy="35%" r="70%">
|
||||
<stop offset="0" stop-color="#b8692c"/><stop offset="1" stop-color="#6a3211"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="beak" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#f4a142"/><stop offset="1" stop-color="#cf6620"/>
|
||||
</linearGradient>
|
||||
<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>
|
||||
|
||||
<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)"/>
|
||||
<path fill="none" stroke="#d88a4d" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" opacity=".55"
|
||||
d="M67 107L70 110L73 107M87 107L90 110L93 107M77 117L80 120L83 117M63 121L66 124L69 121M91 121L94 124L97 121M77 129L80 132L83 129"/>
|
||||
</g>
|
||||
<g id="wing-l">
|
||||
<path fill="url(#wing)" d="M38 90C24 100 22 124 34 136C40 132 44 118 44 100C43 95 41 92 38 90Z"/>
|
||||
<path fill="none" stroke="#f08f4b" stroke-width="1.4" stroke-linecap="round" opacity=".6" d="M36 104Q32 116 34 126"/>
|
||||
</g>
|
||||
<g id="wing-r">
|
||||
<path fill="url(#wing)" d="M122 90C136 100 138 124 126 136C120 132 116 118 116 100C117 95 119 92 122 90Z"/>
|
||||
<path fill="none" stroke="#f08f4b" stroke-width="1.4" stroke-linecap="round" opacity=".6" d="M124 104Q128 116 126 126"/>
|
||||
</g>
|
||||
<g fill="#f4a93f" stroke="#c97a22" stroke-width=".8">
|
||||
<ellipse cx="64" cy="144" rx="3.2" ry="4.2"/><ellipse cx="69.5" cy="145.5" rx="3.2" ry="4.2"/><ellipse cx="75" cy="144" rx="3.2" ry="4.2"/>
|
||||
<ellipse cx="85" cy="144" rx="3.2" ry="4.2"/><ellipse cx="90.5" cy="145.5" rx="3.2" ry="4.2"/><ellipse cx="96" cy="144" rx="3.2" ry="4.2"/>
|
||||
</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="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"/>
|
||||
|
||||
<g clip-path="url(#eye-l)">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<path fill="url(#beak)" d="M75 72C77 68 83 68 85 72C84 78 82 83 80 86C78 83 76 78 75 72Z"/>
|
||||
|
||||
<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"/>
|
||||
</g>
|
||||
</g>
|
||||
</g></g>
|
||||
</svg>
|
||||
|
||||
<form id="login" novalidate>
|
||||
<label for="password">Password</label>
|
||||
<div class="field">
|
||||
<input id="password" name="password" type="password" autocomplete="current-password"
|
||||
aria-describedby="password-msg caps" required>
|
||||
<button type="button" class="reveal" id="reveal" aria-label="Show password" aria-pressed="false"></button>
|
||||
</div>
|
||||
<p class="note" id="caps" aria-live="polite" hidden>Caps Lock is on</p>
|
||||
<p class="error" id="password-msg" role="alert"></p>
|
||||
<button type="submit" class="pill">Sign in</button>
|
||||
</form>
|
||||
|
||||
<div class="done" id="done" hidden>
|
||||
<h2 id="done-title" tabindex="-1">Run screen coming soon</h2>
|
||||
<p>You're signed in. This is where you'll start and watch the bot.</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// ponytail: test password in page source for this demo only; where the real G2 password is kept is still undecided (PRODUCT.md).
|
||||
const TEST_PASSWORD = "hoot2026";
|
||||
|
||||
const EYE = '<path d="M2.5 12s3.5-6.5 9.5-6.5 9.5 6.5 9.5 6.5-3.5 6.5-9.5 6.5S2.5 12 2.5 12z"/><circle cx="12" cy="12" r="2.8"/>';
|
||||
const EYE_OFF = '<path d="M4 4l16 16"/><path d="M9.9 5.7A10 10 0 0 1 12 5.5c6 0 9.5 6.5 9.5 6.5a16 16 0 0 1-2.6 3.4M6.7 7.2C4 9 2.5 12 2.5 12s3.5 6.5 9.5 6.5a9.6 9.6 0 0 0 4.7-1.2"/><path d="M10 10.1a2.8 2.8 0 0 0 3.9 3.9"/>';
|
||||
|
||||
const owl = document.getElementById("owl");
|
||||
const form = document.getElementById("login");
|
||||
const pw = document.getElementById("password");
|
||||
const reveal = document.getElementById("reveal");
|
||||
const caps = document.getElementById("caps");
|
||||
const msg = document.getElementById("password-msg");
|
||||
const done = document.getElementById("done");
|
||||
const irises = owl.querySelectorAll(".iris");
|
||||
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
||||
|
||||
(function drawSky(el) {
|
||||
let seed = 7;
|
||||
const rand = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
|
||||
const colors = ["#6ea8ff", "#4fd1c5", "#f29c4b", "#dfe7ff"];
|
||||
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>`;
|
||||
}
|
||||
el.innerHTML = html;
|
||||
})(document.getElementById("sky"));
|
||||
|
||||
function look(dx, dy) {
|
||||
irises.forEach(g => { g.style.transform = `translate(${dx}px, ${dy}px)`; });
|
||||
}
|
||||
|
||||
// Pupils slide from the left end of the field to the right as the typed text grows.
|
||||
const measure = document.createElement("canvas").getContext("2d");
|
||||
function followCaret() {
|
||||
if (document.activeElement !== pw) return look(0, 0);
|
||||
const cs = getComputedStyle(pw);
|
||||
measure.font = cs.font;
|
||||
measure.letterSpacing = cs.letterSpacing;
|
||||
const caret = pw.selectionStart ?? pw.value.length;
|
||||
const shown = pw.type === "password" ? "•".repeat(caret) : pw.value.slice(0, caret);
|
||||
const room = pw.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
|
||||
const ratio = Math.min(measure.measureText(shown).width / room, 1);
|
||||
look(Math.round(-4 + 8 * ratio), 4);
|
||||
}
|
||||
|
||||
const timers = {};
|
||||
function play(name, ms, then) {
|
||||
clearTimeout(timers[name]);
|
||||
owl.classList.remove(name);
|
||||
void owl.getBoundingClientRect();
|
||||
owl.classList.add(name);
|
||||
timers[name] = setTimeout(() => { owl.classList.remove(name); then?.(); }, ms);
|
||||
}
|
||||
|
||||
// Cleared first so a repeated identical message is announced again.
|
||||
function showError(text) {
|
||||
msg.textContent = "";
|
||||
pw.setAttribute("aria-invalid", text ? "true" : "false");
|
||||
if (text) setTimeout(() => { msg.textContent = text; }, 50);
|
||||
}
|
||||
|
||||
function setReveal(show) {
|
||||
pw.type = show ? "text" : "password";
|
||||
reveal.setAttribute("aria-pressed", String(show));
|
||||
reveal.setAttribute("aria-label", show ? "Hide password" : "Show password");
|
||||
reveal.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true">${show ? EYE_OFF : EYE}</svg>`;
|
||||
}
|
||||
setReveal(false);
|
||||
|
||||
for (const ev of ["focus", "blur", "input", "keyup", "click", "select"]) pw.addEventListener(ev, followCaret);
|
||||
pw.addEventListener("input", () => { if (msg.textContent) showError(""); });
|
||||
for (const ev of ["keydown", "keyup"]) {
|
||||
pw.addEventListener(ev, e => { caps.hidden = !e.getModifierState?.("CapsLock"); });
|
||||
}
|
||||
pw.addEventListener("blur", () => { caps.hidden = true; });
|
||||
|
||||
// Keep focus (and the owl's gaze) in the field when the eye button is clicked.
|
||||
reveal.addEventListener("mousedown", e => e.preventDefault());
|
||||
reveal.addEventListener("click", () => { setReveal(pw.type === "password"); followCaret(); });
|
||||
|
||||
form.addEventListener("submit", e => {
|
||||
e.preventDefault();
|
||||
if (!pw.value) {
|
||||
showError("Enter your password.");
|
||||
play("tilt", 800);
|
||||
pw.focus();
|
||||
return;
|
||||
}
|
||||
if (pw.value !== TEST_PASSWORD) {
|
||||
showError("That password isn't right. Try again.");
|
||||
pw.focus();
|
||||
pw.select();
|
||||
play("shake", 720, followCaret);
|
||||
return;
|
||||
}
|
||||
showError("");
|
||||
pw.value = "";
|
||||
setReveal(false);
|
||||
const enter = () => {
|
||||
form.hidden = true;
|
||||
done.hidden = false;
|
||||
document.getElementById("done-title").focus();
|
||||
};
|
||||
if (reducedMotion.matches) enter();
|
||||
else play("happy", 750, enter);
|
||||
});
|
||||
|
||||
(function blink() {
|
||||
setTimeout(() => {
|
||||
if (!reducedMotion.matches && !owl.classList.contains("happy")) play("blink", 160);
|
||||
blink();
|
||||
}, 2500 + Math.random() * 3500);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user