<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AXIOM — Access Terminal</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=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
</head>
<body>
<main class="stage">
<!-- ================= ORBITAL SCENE ================= -->
<section class="scene">
<canvas id="scene" aria-hidden="true"></canvas>
<div class="scene-vignette" aria-hidden="true"></div>
<div class="grain" aria-hidden="true"></div>
<header class="scene-top">
<div class="telemetry">
<span class="tel"><span class="tel-k">RELAY</span><span class="tel-v">K-7 · ORBITAL</span></span>
<span class="tel"><span class="tel-k">SIG</span><span class="tel-v" id="telSig">98.6%</span></span>
<span class="tel"><span class="tel-k">UTC</span><span class="tel-v" id="telClock">--:--:--</span></span>
</div>
<p class="scene-manifest">DEEP-FIELD RELAY NETWORK<br>4,812 NODES · HANDSHAKE READY</p>
</header>
<footer class="scene-bottom">
<p class="scene-caption">Every channel routes through the dark relay belt. No traces, no logs, no compromise.</p>
<div class="scene-meta">
<span>REV 7.4.1</span><span id="telLat">LAT 44.21°N</span><span>LON 03.14°E</span>
</div>
</footer>
</section>
<!-- ================= ACCESS TERMINAL ================= -->
<section class="terminal" id="terminal">
<div class="cursor-light" id="cursorLight" aria-hidden="true"></div>
<span class="edge-note">AUTH.GATE 07 — RESTRICTED ACCESS</span>
<header class="term-head">
<div class="brand">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle cx="12" cy="12" r="4.1" stroke="currentColor" stroke-width="1.4"/>
<ellipse cx="12" cy="12" rx="10" ry="4.3" stroke="currentColor" stroke-width="1" opacity=".5" transform="rotate(-24 12 12)"/>
<circle cx="21.1" cy="7.9" r="1.4" fill="#E2A44D"/>
</svg>
<span class="brand-word">AXIOM</span>
</div>
<div class="channel" id="channel"><i class="dot"></i><span id="channelText">SECURE CHANNEL</span></div>
</header>
<div class="term-body">
<div class="term-inner">
<h1 class="rise" style="--d:0">Access Terminal</h1>
<p class="lede rise" style="--d:1">Authenticate to open a private channel with the Axiom deep-field relay network.</p>
<form id="loginForm" novalidate>
<!-- 01 · identifier -->
<div class="field rise" style="--d:2" id="fId">
<div class="field-head">
<label for="identifier"><span class="idx">01</span>IDENTIFIER</label>
<span class="field-msg" id="idMsg" aria-live="polite"></span>
</div>
<div class="field-line">
<input id="identifier" type="text" autocomplete="username" autocapitalize="off"
spellcheck="false" placeholder="operator@axiom.space">
<i class="tick tl" aria-hidden="true"></i><i class="tick br" aria-hidden="true"></i>
<i class="rail" aria-hidden="true"></i>
</div>
</div>
<!-- 02 · passphrase -->
<div class="field rise" style="--d:3" id="fPw">
<div class="field-head">
<label for="password"><span class="idx">02</span>PASSPHRASE</label>
<span class="field-msg" id="pwMsg" aria-live="polite"></span>
</div>
<div class="field-line">
<input id="password" type="password" autocomplete="current-password"
placeholder="••••••••••••">
<button type="button" class="eye" id="eyeBtn" aria-label="Show passphrase" aria-pressed="false">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M2.6 12c1.9-3.6 5.4-5.7 9.4-5.7s7.5 2.1 9.4 5.7c-1.9 3.6-5.4 5.7-9.4 5.7S4.5 15.6 2.6 12Z"/>
<circle cx="12" cy="12" r="2.6"/>
<path class="lash" d="M4.5 19.5 19.5 4.5"/>
</svg>
</button>
<i class="tick tl" aria-hidden="true"></i><i class="tick br" aria-hidden="true"></i>
<i class="rail" aria-hidden="true"></i>
</div>
<!-- strength instrument -->
<div class="strength" id="strength" data-level="0">
<div class="strength-inner">
<div class="strength-row">
<div class="cells" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></div>
<span class="strength-label" id="strengthLabel" role="status" aria-live="polite">—</span>
</div>
<canvas id="wave" aria-hidden="true"></canvas>
<div class="strength-meta">
<span id="mBits">EST. ENTROPY 0 BITS</span>
<span id="mLen">LENGTH 0</span>
<span id="mPool">POOL 0</span>
</div>
<p class="strength-tip" id="strengthTip"></p>
</div>
</div>
</div>
<div class="row-between rise" style="--d:4">
<label class="check">
<input type="checkbox" id="remember">
<span class="box" aria-hidden="true">
<svg viewBox="0 0 14 12"><path d="M2 6.5l3.5 3.5L12 2"/></svg>
</span>
<span class="check-label">Remember this terminal</span>
</label>
<button type="button" class="link" id="forgotLink">Forgot passphrase?</button>
</div>
<div class="rise" style="--d:5">
<button class="btn" id="loginBtn" type="submit">
<span class="fill" aria-hidden="true"></span>
<span class="scan" aria-hidden="true"></span>
<span class="content">
<span id="btnLabel">Open secure channel</span>
<svg class="ic arrow" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 12h15M13 5.5 19.5 12 13 18.5"/></svg>
<svg class="ic ic-check" viewBox="0 0 24 24" aria-hidden="true"><path d="M4.5 12.5 9.5 17.5 19.5 6.5"/></svg>
</span>
</button>
</div>
</form>
<div class="term-foot rise" style="--d:6">
<span>NO CLEARANCE YET?</span>
<button type="button" class="link" id="requestLink" style="text-transform:uppercase;letter-spacing:.16em;font-size:9.5px">Request access</button>
</div>
</div>
</div>
<footer class="term-base">
<span>© 2087 AXIOM DYNAMICS</span>
<span>TLS 1.3 · X25519</span>
</footer>
</section>
</main>
<!-- ================= MODAL ================= -->
<div class="overlay" id="overlay" role="dialog" aria-modal="true" aria-labelledby="modalTitle" hidden>
<div class="sheet">
<div class="sheet-head">
<span class="sheet-k" id="modalKicker">RECOVERY PROTOCOL</span>
<button type="button" class="sheet-close" id="modalClose" aria-label="Close dialog">
<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M2 2l10 10M12 2 2 12"/></svg>
</button>
</div>
<h2 id="modalTitle">Reset passphrase</h2>
<p class="sheet-desc" id="modalDesc"></p>
<form id="modalForm" novalidate>
<div class="field" id="fModal">
<div class="field-head">
<label for="modalInput" id="modalLabel">IDENTIFIER</label>
<span class="field-msg" id="modalMsg" aria-live="polite"></span>
</div>
<div class="field-line">
<input id="modalInput" type="text" autocomplete="email" autocapitalize="off" spellcheck="false" placeholder="operator@axiom.space">
<i class="rail" aria-hidden="true"></i>
</div>
</div>
<button class="btn" id="modalBtn" type="submit">
<span class="fill" aria-hidden="true"></span>
<span class="content"><span id="modalBtnLabel">Send recovery signal</span></span>
</button>
</form>
<div class="sheet-done" id="sheetDone" hidden>
<svg viewBox="0 0 36 36" aria-hidden="true"><circle cx="18" cy="18" r="16"/><path d="M11.5 18.5l4.5 4.5 9-10"/></svg>
<p id="sheetDoneText"></p>
</div>
</div>
</div>
<div class="toasts" id="toasts" aria-live="polite"></div>
</body>
</html>
Sign in
Sign in with your free account to like snippets and add them to your favorites.
/* ============================================================
AXIOM ACCESS TERMINAL — design tokens
============================================================ */
:root{
--bg:#08080A;
--panel:#0B0B0E;
--panel-2:#101014;
--hair:rgba(255,255,255,.09);
--hair-soft:rgba(255,255,255,.05);
--txt:#EDEDEF;
--txt-2:#9A9AA3;
--txt-3:#5E5E67;
--amber:#E2A44D;
--amber-bright:#F2BE6B;
--phos:#4FE0A4;
--ember:#F45B41;
--mono:'IBM Plex Mono',ui-monospace,monospace;
--sans:'Space Grotesk',sans-serif;
--ease:cubic-bezier(.22,1,.36,1);
}
*{margin:0;padding:0;box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
background:var(--bg);color:var(--txt);
font-family:var(--sans);
-webkit-font-smoothing:antialiased;
overflow-x:hidden;
}
::selection{background:rgba(226,164,77,.28);color:#fff}
:focus-visible{outline:1px solid var(--amber);outline-offset:3px;border-radius:2px}
button{font-family:inherit}
/* ============================================================
STAGE LAYOUT
============================================================ */
.stage{
min-height:100dvh;
display:grid;
grid-template-columns:1.15fr 1fr;
}
/* ============================================================
LEFT — ORBITAL SCENE
============================================================ */
.scene{
position:relative;isolation:isolate;
background:#050507;
overflow:hidden;
animation:sceneIn 1.4s ease both;
}
@keyframes sceneIn{from{opacity:0}to{opacity:1}}
.scene canvas{position:absolute;inset:0;width:100%;height:100%;display:block}
.scene-vignette{
position:absolute;inset:0;pointer-events:none;z-index:2;
background:radial-gradient(120% 92% at 50% 28%,transparent 52%,rgba(3,3,5,.6) 100%);
}
.grain{
position:absolute;inset:0;pointer-events:none;z-index:3;opacity:.05;mix-blend-mode:overlay;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
}
.scene-top,.scene-bottom{position:absolute;left:0;right:0;z-index:4;display:flex;justify-content:space-between;gap:24px}
.scene-top{top:0;padding:clamp(20px,3vw,36px)}
.scene-bottom{bottom:0;padding:clamp(20px,3vw,36px);align-items:flex-end}
.telemetry{display:flex;flex-wrap:wrap;gap:6px 22px;font-family:var(--mono)}
.tel{display:flex;gap:8px;align-items:baseline;font-size:10px;letter-spacing:.12em;white-space:nowrap}
.tel-k{color:var(--txt-3)}
.tel-v{color:var(--txt-2)}
.scene-manifest{
font-family:var(--mono);font-size:9.5px;letter-spacing:.2em;
color:var(--txt-3);text-align:right;line-height:1.9;flex:none;
}
.scene-caption{
max-width:300px;font-size:15px;line-height:1.65;color:var(--txt-2);
letter-spacing:.01em;
}
.scene-meta{
display:flex;gap:20px;font-family:var(--mono);font-size:9.5px;
letter-spacing:.16em;color:var(--txt-3);flex:none;
}
/* ============================================================
RIGHT — TERMINAL PANEL
============================================================ */
.terminal{
position:relative;overflow:hidden;
display:flex;flex-direction:column;
border-left:1px solid var(--hair-soft);
background:
repeating-linear-gradient(90deg,rgba(255,255,255,.026) 0,rgba(255,255,255,.026) 1px,transparent 1px,transparent 92px),
var(--panel);
}
.cursor-light{
position:absolute;left:0;top:0;width:560px;height:560px;
margin:-280px 0 0 -280px;border-radius:50%;pointer-events:none;
opacity:0;transition:opacity .6s ease;will-change:transform;
background:radial-gradient(circle,rgba(226,164,77,.055),rgba(226,164,77,.018) 42%,transparent 66%);
}
@media (hover:hover) and (pointer:fine){ .terminal:hover .cursor-light{opacity:1} }
.edge-note{
position:absolute;right:16px;top:50%;transform:translateY(-50%);
writing-mode:vertical-rl;font-family:var(--mono);font-size:9px;
letter-spacing:.34em;color:var(--txt-3);white-space:nowrap;pointer-events:none;
}
.term-head{
display:flex;justify-content:space-between;align-items:center;
padding:26px clamp(24px,4.5vw,56px) 22px;
border-bottom:1px solid var(--hair-soft);
}
.brand{display:flex;align-items:center;gap:12px;color:var(--txt)}
.brand svg{display:block;color:var(--txt)}
.brand-word{font-size:14px;font-weight:600;letter-spacing:.36em;transform:translateY(1px)}
.channel{display:flex;align-items:center;gap:8px;font-family:var(--mono);font-size:9.5px;letter-spacing:.18em;color:var(--txt-3);transition:color .4s}
.channel .dot{width:5px;height:5px;border-radius:50%;background:var(--amber);animation:softPulse 2.6s ease-in-out infinite;transition:background .4s,box-shadow .4s}
.channel.handshake{color:var(--amber)}
.channel.handshake .dot{animation:blink .55s steps(1) infinite}
.channel.open{color:var(--phos)}
.channel.open .dot{background:var(--phos);box-shadow:0 0 8px rgba(79,224,164,.9);animation:none}
@keyframes softPulse{0%,100%{opacity:1}50%{opacity:.35}}
@keyframes blink{0%,100%{opacity:1}50%{opacity:.15}}
.term-body{
flex:1;display:flex;flex-direction:column;justify-content:center;
padding:44px clamp(24px,4.5vw,56px);
}
.term-inner{width:min(400px,100%);margin-inline:auto}
h1{font-size:clamp(27px,2.4vw,34px);font-weight:600;letter-spacing:-.015em;line-height:1.12}
.lede{margin:13px 0 36px;font-size:14.5px;line-height:1.65;color:var(--txt-2)}
/* entrance choreography */
.rise{animation:rise .8s var(--ease) both;animation-delay:calc(140ms + var(--d)*75ms)}
@keyframes rise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
/* ============================================================
FIELDS
============================================================ */
.field + .field{margin-top:28px}
.field-head{display:flex;justify-content:space-between;align-items:baseline;gap:12px;margin-bottom:2px}
.field-head label{
font-family:var(--mono);font-size:10px;font-weight:500;
letter-spacing:.2em;color:var(--txt-3);cursor:pointer;
}
.field-head .idx{color:rgba(226,164,77,.65);margin-right:9px}
.field-msg{
font-family:var(--mono);font-size:9.5px;letter-spacing:.08em;
color:var(--ember);text-align:right;min-height:12px;
}
.field-line{
position:relative;display:flex;align-items:center;gap:6px;
padding:9px 0 12px;border-bottom:1px solid var(--hair);
}
.field-line input{
flex:1;min-width:0;background:transparent;border:0;outline:none;
color:var(--txt);font-family:var(--sans);font-size:15px;letter-spacing:.015em;
caret-color:var(--amber);
}
.field-line input::placeholder{color:var(--txt-3);letter-spacing:.02em}
input:-webkit-autofill{
-webkit-box-shadow:0 0 0 60px var(--panel) inset;
-webkit-text-fill-color:var(--txt);
}
@media (pointer:coarse){.field-line input{font-size:16px}}
/* focus rail */
.rail{
position:absolute;left:0;right:0;bottom:-1px;height:1px;background:var(--amber);
transform:scaleX(0);transform-origin:left;transition:transform .45s var(--ease),background .3s;
}
.field-line:focus-within .rail{transform:scaleX(1)}
/* corner brackets */
.tick{position:absolute;width:8px;height:8px;opacity:0;transition:opacity .35s,transform .35s var(--ease);pointer-events:none}
.tick.tl{top:3px;left:-15px;border-left:1px solid rgba(226,164,77,.7);border-top:1px solid rgba(226,164,77,.7);transform:translate(4px,4px)}
.tick.br{bottom:11px;right:-15px;border-right:1px solid rgba(226,164,77,.7);border-bottom:1px solid rgba(226,164,77,.7);transform:translate(-4px,-4px)}
.field-line:focus-within .tick{opacity:1;transform:none}
/* error state */
.field.err .rail{background:var(--ember);transform:scaleX(1)}
.field.err .field-head label{color:var(--txt-2)}
.field.err.shake .field-line{animation:shake .3s ease}
@keyframes shake{25%{transform:translateX(-3px)}55%{transform:translateX(3px)}85%{transform:translateX(-1px)}}
/* eye toggle */
.eye{
flex:none;width:32px;height:32px;display:grid;place-items:center;
background:none;border:0;color:var(--txt-3);cursor:pointer;
transition:color .25s;border-radius:2px;
}
.eye:hover{color:var(--amber-bright)}
.eye svg{width:19px;height:19px;overflow:visible}
.eye .lash{stroke-dasharray:23;stroke-dashoffset:23;transition:stroke-dashoffset .3s var(--ease)}
.eye circle{transition:r .3s var(--ease)}
.eye.revealed .lash{stroke-dashoffset:0}
.eye.revealed circle{r:1.7px}
/* ============================================================
STRENGTH INSTRUMENT
============================================================ */
.strength{
max-height:0;opacity:0;overflow:hidden;transform:translateY(-5px);
transition:max-height .5s var(--ease),opacity .4s ease,transform .5s var(--ease);
}
.strength.on{max-height:230px;opacity:1;transform:none}
.strength-inner{padding-top:16px}
.strength[data-level="1"]{--lvl:#F45B41}
.strength[data-level="2"]{--lvl:#F58A3C}
.strength[data-level="3"]{--lvl:#E7B34C}
.strength[data-level="4"]{--lvl:#B4DE6A}
.strength[data-level="5"]{--lvl:#4FE0A4}
.strength-row{display:flex;align-items:center;gap:14px}
.cells{display:flex;gap:5px;flex:1}
.cells i{
flex:1;height:4px;position:relative;overflow:hidden;
background:rgba(255,255,255,.09);
transition:background .4s,box-shadow .4s;
}
.cells i.filled{
background:var(--lvl);
box-shadow:0 0 9px color-mix(in srgb,var(--lvl) 45%,transparent);
}
.cells i.filled::after{
content:'';position:absolute;inset:0;
background:linear-gradient(105deg,transparent 25%,rgba(255,255,255,.4) 50%,transparent 75%);
transform:translateX(-130%);
animation:sheen 3.2s var(--ease) infinite;
animation-delay:inherit;
}
.cells i:nth-child(2).filled::after{animation-delay:.18s}
.cells i:nth-child(3).filled::after{animation-delay:.36s}
.cells i:nth-child(4).filled::after{animation-delay:.54s}
.cells i:nth-child(5).filled::after{animation-delay:.72s}
@keyframes sheen{0%{transform:translateX(-130%)}45%,100%{transform:translateX(130%)}}
.strength-label{
flex:none;min-width:88px;text-align:right;
font-family:var(--mono);font-size:10px;font-weight:500;
letter-spacing:.14em;color:var(--lvl);transition:color .4s;
}
.strength-label.swap{animation:labelSwap .35s var(--ease)}
@keyframes labelSwap{from{opacity:0;transform:translateY(5px)}}
#wave{display:block;width:100%;height:30px;margin:12px 0 9px}
.strength-meta{display:flex;flex-wrap:wrap;gap:6px 16px;font-family:var(--mono);font-size:9.5px;letter-spacing:.1em;color:var(--txt-3)}
.strength-tip{margin-top:8px;font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;color:var(--lvl);transition:color .4s}
/* ============================================================
ROW / CHECKBOX / LINKS
============================================================ */
.row-between{display:flex;justify-content:space-between;align-items:center;margin:26px 0 22px}
.check{display:flex;align-items:center;gap:10px;cursor:pointer;user-select:none;position:relative}
.check input{position:absolute;opacity:0;width:1px;height:1px}
.check .box{
width:15px;height:15px;border:1px solid rgba(255,255,255,.25);border-radius:2px;
display:grid;place-items:center;
transition:border-color .25s,background .25s;
}
.check .box svg{width:10px;height:9px;overflow:visible}
.check .box path{
stroke:var(--amber);stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round;
stroke-dasharray:18;stroke-dashoffset:18;transition:stroke-dashoffset .28s var(--ease) .05s;
}
.check input:checked + .box{border-color:var(--amber);background:rgba(226,164,77,.08)}
.check input:checked + .box path{stroke-dashoffset:0}
.check input:focus-visible + .box{outline:1px solid var(--amber);outline-offset:2px}
.check-label{font-family:var(--mono);font-size:11px;letter-spacing:.05em;color:var(--txt-2)}
.link{
background:none;border:0;padding:0;cursor:pointer;
font-family:var(--mono);font-size:11px;font-weight:500;letter-spacing:.05em;
color:var(--txt-2);text-decoration:underline;
text-decoration-color:rgba(255,255,255,.25);text-underline-offset:4px;
transition:color .25s,text-decoration-color .25s;
}
.link:hover{color:var(--amber-bright);text-decoration-color:var(--amber)}
/* ============================================================
LOGIN BUTTON
============================================================ */
.btn{
position:relative;display:flex;justify-content:center;align-items:center;
width:100%;height:54px;border-radius:2px;overflow:hidden;
border:1px solid var(--amber);background:transparent;color:var(--amber);
font-family:var(--mono);font-size:12px;font-weight:500;
letter-spacing:.22em;text-transform:uppercase;cursor:pointer;
transition:color .35s,border-color .35s,transform .15s;
}
.btn .fill{
position:absolute;inset:0;background:var(--amber);
transform:scaleX(0);transform-origin:left;transition:transform .5s var(--ease);
}
.btn:hover:not(.loading):not(.success){color:#0B0B0D}
.btn:hover:not(.loading):not(.success) .fill{transform:scaleX(1)}
.btn:active:not(.loading):not(.success){transform:translateY(1px)}
.btn .content{position:relative;z-index:1;display:flex;align-items:center;gap:11px}
.btn .ic{width:15px;height:15px;overflow:visible}
.btn .ic path{stroke:currentColor;stroke-width:1.9;fill:none;stroke-linecap:round;stroke-linejoin:round}
.btn .arrow{transition:transform .35s var(--ease)}
.btn:hover .arrow{transform:translateX(4px)}
.btn .scan{position:absolute;bottom:0;left:0;height:1px;width:32%;background:var(--amber);opacity:0}
.btn.loading{pointer-events:none;color:var(--txt-2);border-color:rgba(226,164,77,.4)}
.btn.loading .arrow{display:none}
.btn.loading .scan{opacity:.85;animation:scan 1.05s cubic-bezier(.45,0,.55,1) infinite}
@keyframes scan{from{transform:translateX(-120%)}to{transform:translateX(430%)}}
.btn.success{pointer-events:none;border-color:var(--phos);color:var(--phos)}
.btn.success .arrow{display:none}
.btn .ic-check{opacity:0;transition:opacity .3s}
.btn.success .ic-check{opacity:1}
.btn .ic-check path{stroke-dasharray:24;stroke-dashoffset:24;transition:stroke-dashoffset .45s var(--ease) .1s}
.btn.success .ic-check path{stroke-dashoffset:0}
.term-foot{
margin-top:34px;padding-top:20px;border-top:1px solid var(--hair-soft);
display:flex;align-items:center;gap:14px;
font-family:var(--mono);font-size:9.5px;letter-spacing:.2em;color:var(--txt-3);
}
.term-base{
display:flex;justify-content:space-between;gap:16px;
padding:20px clamp(24px,4.5vw,56px) 26px;
border-top:1px solid var(--hair-soft);
font-family:var(--mono);font-size:9px;letter-spacing:.18em;color:var(--txt-3);
}
/* ============================================================
MODAL
============================================================ */
.overlay{
position:fixed;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;
padding:20px;background:rgba(5,5,7,.74);backdrop-filter:blur(7px);
-webkit-backdrop-filter:blur(7px);
opacity:0;transition:opacity .3s ease;
}
.overlay.open{opacity:1}
.sheet{
width:min(432px,100%);position:relative;
background:var(--panel-2);border:1px solid var(--hair);border-radius:3px;
padding:30px 30px 34px;
transform:translateY(16px) scale(.985);transition:transform .4s var(--ease);
}
.overlay.open .sheet{transform:none}
.sheet::before,.sheet::after{content:'';position:absolute;width:12px;height:12px;pointer-events:none}
.sheet::before{top:-5px;left:-5px;border-left:1px solid rgba(226,164,77,.55);border-top:1px solid rgba(226,164,77,.55)}
.sheet::after{bottom:-5px;right:-5px;border-right:1px solid rgba(226,164,77,.55);border-bottom:1px solid rgba(226,164,77,.55)}
.sheet-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}
.sheet-k{font-family:var(--mono);font-size:9.5px;letter-spacing:.26em;color:var(--amber)}
.sheet-close{
width:30px;height:30px;display:grid;place-items:center;background:none;
border:0;color:var(--txt-3);cursor:pointer;transition:color .25s;border-radius:2px;
}
.sheet-close:hover{color:var(--txt)}
.sheet-close svg{width:14px;height:14px}
.sheet h2{font-size:22px;font-weight:600;letter-spacing:-.01em}
.sheet-desc{margin:10px 0 26px;font-size:13.5px;line-height:1.65;color:var(--txt-2)}
.sheet .btn{height:48px;margin-top:24px}
.sheet-done{display:flex;gap:14px;align-items:flex-start;padding:8px 0 4px}
.sheet-done svg{flex:none;width:34px;height:34px;overflow:visible}
.sheet-done circle{stroke:var(--phos);stroke-width:1.4;fill:none;stroke-dasharray:64;stroke-dashoffset:64;animation:draw .6s var(--ease) forwards}
.sheet-done path{stroke:var(--phos);stroke-width:1.9;fill:none;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:24;stroke-dashoffset:24;animation:draw .4s var(--ease) .35s forwards}
@keyframes draw{to{stroke-dashoffset:0}}
.sheet-done p{font-size:13.5px;line-height:1.65;color:var(--txt-2);padding-top:4px}
[hidden]{display:none !important}
/* ============================================================
TOASTS
============================================================ */
.toasts{
position:fixed;right:22px;bottom:22px;z-index:80;
display:flex;flex-direction:column;gap:10px;pointer-events:none;
}
.toast{
pointer-events:auto;display:flex;align-items:stretch;gap:12px;
min-width:270px;max-width:370px;
background:#121217;border:1px solid var(--hair);border-radius:2px;
padding:12px 12px 12px 14px;
animation:toastIn .45s var(--ease);
}
.toast.out{animation:toastOut .3s ease forwards}
@keyframes toastIn{from{opacity:0;transform:translateY(10px)}}
@keyframes toastOut{to{opacity:0;transform:translateY(8px)}}
.toast .bar{flex:none;width:2px;background:currentColor}
.toast.ok{color:var(--phos)}
.toast.err{color:var(--ember)}
.toast.info{color:var(--amber)}
.toast p{flex:1;font-family:var(--mono);font-size:11px;line-height:1.55;letter-spacing:.05em;color:var(--txt)}
.t-close{background:none;border:0;padding:0 2px;cursor:pointer;color:var(--txt-3);font-family:var(--mono);font-size:14px;line-height:1.4}
.t-close:hover{color:var(--txt)}
/* ============================================================
RESPONSIVE
============================================================ */
@media (max-width:1280px){ .edge-note{display:none} }
@media (max-width:1020px){
.stage{grid-template-columns:.9fr 1.1fr}
.scene-manifest{display:none}
}
@media (max-width:760px){
.stage{display:block}
.scene{height:clamp(280px,42vh,380px)}
.scene-caption{display:none}
.scene-top{padding:18px 20px}
.scene-bottom{padding:16px 20px}
.telemetry{gap:5px 16px}
.term-head{padding:20px}
.term-body{padding:36px 22px 30px;justify-content:flex-start}
.term-base{padding:16px 20px 22px;flex-wrap:wrap;gap:8px 18px}
.toasts{left:16px;right:16px;bottom:16px}
.toast{max-width:none;min-width:0}
}
@media (prefers-reduced-motion:reduce){
*,*::before,*::after{
animation-duration:.01ms !important;
animation-iteration-count:1 !important;
transition-duration:.15s !important;
}
.rise,.scene{animation:none}
}
(() => {
'use strict';
/* ============================ utils ============================ */
const $ = (s, c = document) => c.querySelector(s);
const $$ = (s, c = document) => [...c.querySelectorAll(s)];
const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
const lerp = (a, b, t) => a + (b - a) * t;
const REDUCED = matchMedia('(prefers-reduced-motion: reduce)').matches;
/* ============================ toasts ============================ */
const Toast = (() => {
const host = $('#toasts');
function show(type, msg) {
const el = document.createElement('div');
el.className = 'toast ' + type;
el.innerHTML = `<span class="bar"></span><p></p><button class="t-close" aria-label="Dismiss">×</button>`;
el.querySelector('p').textContent = msg;
host.appendChild(el);
const kill = () => { el.classList.add('out'); setTimeout(() => el.remove(), 320); };
el.querySelector('.t-close').addEventListener('click', kill);
setTimeout(kill, 4200);
}
return { ok: m => show('ok', m), err: m => show('err', m), info: m => show('info', m) };
})();
/* ============================ password engine ============================ */
const COMMON = new Set(['password','123456','12345678','123456789','1234567890','qwerty',
'qwerty123','abc123','password1','password123','iloveyou','admin','admin123','welcome',
'monkey','dragon','letmein','letmein1','111111','000000','master','sunshine','princess',
'football','baseball','shadow','superman','batman','trustno1','whatever','freedom',
'passw0rd','p@ssw0rd','changeme','default','guest','root','toor','starwars','hello123',
'zaq12wsx','1q2w3e4r','qwe123','654321','123123','121212','666666','888888','abcdef']);
const ROOTS = ['password','qwerty','letmein','welcome','iloveyou','admin','master','monkey',
'dragon','sunshine','princess','football','baseball','superman','trustno','starwars',
'whatever','freedom','changeme','access','login'];
function analyze(pw) {
const len = pw.length;
if (!len) return { len: 0, bits: 0, pool: 0, score: 0, level: 0 };
const lower = /[a-z]/.test(pw), upper = /[A-Z]/.test(pw);
const digit = /[0-9]/.test(pw), sym = /[^A-Za-z0-9]/.test(pw);
const classes = lower + upper + digit + sym;
const pool = (lower ? 26 : 0) + (upper ? 26 : 0) + (digit ? 10 : 0) + (sym ? 32 : 0);
const unique = new Set(pw).size;
const bits = Math.round(len * Math.log2(Math.max(pool, 2)) * (unique / len));
let score = 0;
score += Math.min(len, 16) / 16 * 34; // length (max 34)
score += classes * 8; // character classes (max 32)
score += unique / len * 14; // diversity (max 14)
score -= Math.min(len - unique, len * .4) * 1.2; // repeated characters
let seq = 0; // sequential runs (abc / 321)
for (let i = 2; i < len; i++) {
const a = pw.charCodeAt(i - 2), b = pw.charCodeAt(i - 1), c = pw.charCodeAt(i);
if ((b === a + 1 && c === b + 1) || (b === a - 1 && c === b - 1)) seq++;
}
score -= seq * 3;
let rep = 0; // immediate doubles (aa, 11)
for (let i = 1; i < len; i++) if (pw[i] === pw[i - 1]) rep++;
score -= rep * 2.5;
const norm = pw.toLowerCase();
if (COMMON.has(norm)) score = Math.min(score, 6);
else if (len >= 6) for (const w of ROOTS) if (norm.includes(w)) { score = Math.min(score, 22); break; }
score = clamp(Math.round(score), 0, 100);
const level = score <= 20 ? 1 : score <= 40 ? 2 : score <= 62 ? 3 : score <= 82 ? 4 : 5;
return { len, bits, pool, score, level };
}
const LEVELS = [null,
{ name: 'VERY WEAK', rgb: [244, 91, 65], tip: 'EASILY COMPROMISED — AVOID COMMON PATTERNS' },
{ name: 'WEAK', rgb: [245, 138, 60], tip: 'ADD LENGTH AND CHARACTER VARIETY' },
{ name: 'MEDIUM', rgb: [231, 179, 76], tip: 'ADEQUATE — SYMBOLS WOULD HARDEN IT' },
{ name: 'STRONG', rgb: [180, 222, 106], tip: 'RESISTANT TO BRUTE-FORCE ATTACKS' },
{ name: 'VERY STRONG', rgb: [79, 224, 164], tip: 'EXEMPLARY — SUITABLE FOR ROOT ACCESS' }];
/* ============================ entropy oscilloscope ============================ */
const Wave = (() => {
const cv = $('#wave'), ctx = cv.getContext('2d');
let W = 0, H = 0, hash = 0;
let amp = 2, ampT = 2, spd = .5, spdT = .5;
let col = [154, 154, 163], colT = [154, 154, 163];
let f1 = 1.2, f2 = 2.7, f3 = 4.3, ph1 = 0, ph2 = 0, ph3 = 0;
function resize() {
const r = cv.getBoundingClientRect();
if (!r.width) return;
const dpr = Math.min(devicePixelRatio || 1, 2);
W = r.width; H = r.height;
cv.width = W * dpr; cv.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
new ResizeObserver(resize).observe(cv);
function setSignal(pw, score, rgb) {
colT = rgb;
ampT = 3 + (score / 100) * (H ? H * .5 - 4 : 10);
spdT = .5 + (score / 100) * 2.6;
let h = 2166136261; // FNV-1a — deterministic per password
for (let i = 0; i < pw.length; i++) { h ^= pw.charCodeAt(i); h = Math.imul(h, 16777619) >>> 0; }
hash = h >>> 0;
f1 = 1 + (hash % 260) / 100;
f2 = 2 + ((hash >>> 7) % 420) / 100;
f3 = 3 + ((hash >>> 15) % 700) / 100;
if (REDUCED) { amp = ampT; spd = 0; col = [...colT]; }
}
function draw(dt) {
if (!W) { resize(); if (!W) return; }
const k = 1 - Math.exp(-dt / 220);
amp = lerp(amp, ampT, k); spd = lerp(spd, spdT, k);
col = col.map((c, i) => lerp(c, colT[i], k));
ph1 += dt * .0012 * spd; ph2 += dt * .0017 * spd; ph3 += dt * .0023 * spd;
ctx.clearRect(0, 0, W, H);
ctx.strokeStyle = 'rgba(255,255,255,.07)'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, H / 2 + .5); ctx.lineTo(W, H / 2 + .5); ctx.stroke();
const c = `rgb(${col[0] | 0},${col[1] | 0},${col[2] | 0})`;
ctx.beginPath();
for (let x = 0; x <= W; x += 2) {
const n = x / W;
let y = Math.sin(n * Math.PI * 2 * f1 + ph1) * .55
+ Math.sin(n * Math.PI * 2 * f2 + ph2) * .30
+ Math.sin(n * Math.PI * 2 * f3 + ph3) * .15
+ Math.sin(x * .55 + (hash % 97)) * .1 * (amp / (H / 2));
const py = H / 2 + y * amp;
x === 0 ? ctx.moveTo(x, py) : ctx.lineTo(x, py);
}
ctx.strokeStyle = c; ctx.lineWidth = 1.4;
ctx.shadowColor = c; ctx.shadowBlur = 4 + amp * .14;
ctx.stroke(); ctx.shadowBlur = 0;
}
return { draw, setSignal };
})();
/* ============================ strength UI ============================ */
const Strength = (() => {
const block = $('#strength'), cells = $$('.cells i', block);
const labelEl = $('#strengthLabel'), tipEl = $('#strengthTip');
const mBits = $('#mBits'), mLen = $('#mLen'), mPool = $('#mPool');
function setLabel(txt) {
if (labelEl.textContent === txt) return;
labelEl.textContent = txt;
labelEl.classList.remove('swap'); void labelEl.offsetWidth;
labelEl.classList.add('swap');
}
function update(pw) {
const r = analyze(pw);
block.classList.toggle('on', r.len > 0);
if (!r.len) { Wave.setSignal('', 0, [154, 154, 163]); return r; }
block.dataset.level = r.level;
const L = LEVELS[r.level];
cells.forEach((c, i) => c.classList.toggle('filled', i < r.level));
setLabel(L.name);
tipEl.textContent = L.tip;
mBits.textContent = `EST. ENTROPY ${r.bits} BITS`;
mLen.textContent = `LENGTH ${r.len}`;
mPool.textContent = `POOL ${r.pool}`;
Wave.setSignal(pw, r.score, L.rgb);
if (REDUCED) Wave.draw(0);
return r;
}
return { update };
})();
/* ============================ orbital scene ============================ */
const Scene = (() => {
const cv = $('#scene'), ctx = cv.getContext('2d');
let W = 0, H = 0, stars = [], dust = [], meteors = [], pulses = [], atmoBoost = 0;
let m = { x: .5, y: .5 }, mt = { x: .5, y: .5 };
let nextMeteor = 4500 + Math.random() * 7000;
const sats = [{ a: Math.random() * 6.28, v: .00006 }, { a: Math.random() * 6.28, v: .00010 }];
const sprite = document.createElement('canvas'); // soft round star sprite
sprite.width = sprite.height = 32;
(() => {
const s = sprite.getContext('2d');
const g = s.createRadialGradient(16, 16, 0, 16, 16, 16);
g.addColorStop(0, 'rgba(255,255,255,1)');
g.addColorStop(.35, 'rgba(255,255,255,.45)');
g.addColorStop(1, 'rgba(255,255,255,0)');
s.fillStyle = g; s.fillRect(0, 0, 32, 32);
})();
function resize() {
const r = cv.getBoundingClientRect();
W = r.width; H = r.height;
const dpr = Math.min(devicePixelRatio || 1, 2);
cv.width = W * dpr; cv.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const n = Math.round(W * H / 5200);
stars = Array.from({ length: n }, () => ({
x: Math.random(), y: Math.random(), z: .25 + Math.random() * .75,
tw: Math.random() * 6.28, ts: .4 + Math.random() * 1.6 }));
dust = Array.from({ length: Math.max(12, n / 5 | 0) }, () => ({
x: Math.random(), y: Math.random(),
vx: (Math.random() - .5) * .006, vy: -.002 - Math.random() * .008,
r: .4 + Math.random() * 1.2, a: .06 + Math.random() * .16 }));
}
addEventListener('resize', resize); resize();
addEventListener('pointermove', e => {
mt.x = e.clientX / innerWidth; mt.y = e.clientY / innerHeight;
});
const geo = () => {
const R = Math.max(W, H) * .72;
return { R, cx: W * .40 + (m.x - .5) * 26, cy: H + R * .64 + (m.y - .5) * 16 };
};
const ring = (cx, cy, rx, ry, rot, a) => {
ctx.beginPath(); ctx.ellipse(cx, cy, rx, ry, rot, 0, 6.2832);
ctx.strokeStyle = `rgba(214,218,228,${a})`; ctx.lineWidth = 1; ctx.stroke();
};
const satPos = (s, rx, ry, rot, cx, cy) => {
s.a += s.v;
const ca = Math.cos(s.a), sa = Math.sin(s.a);
const cr = Math.cos(rot), sr = Math.sin(rot);
return { x: cx + rx * ca * cr - ry * sa * sr,
y: cy + rx * ca * sr + ry * sa * cr,
front: rx * ca * sr + ry * sa * cr > 0 };
};
const satDraw = (p, cx, cy, rx, ry, rot) => {
ctx.beginPath(); ctx.ellipse(cx, cy, rx, ry, rot, p.a - .5, p.a);
ctx.strokeStyle = 'rgba(226,164,77,.20)'; ctx.lineWidth = 1; ctx.stroke();
ctx.save(); ctx.shadowColor = 'rgba(242,190,107,.9)'; ctx.shadowBlur = 9;
ctx.fillStyle = 'rgba(244,196,116,.95)';
ctx.beginPath(); ctx.arc(p.x, p.y, 2, 0, 6.2832); ctx.fill(); ctx.restore();
};
function draw(dt, now) {
const t = now * .001;
const k = 1 - Math.exp(-dt / 260);
m.x = lerp(m.x, mt.x, k); m.y = lerp(m.y, mt.y, k);
ctx.clearRect(0, 0, W, H);
const { R, cx, cy } = geo();
const rot1 = -.38, rx1 = R * 1.55, ry1 = R * .30;
const rot2 = -.28, rx2 = R * 1.22, ry2 = R * .22;
sats[0].v && 0; // velocities used below
const p1 = satPos({ ...sats[0], a: sats[0].a }, rx1, ry1, rot1, cx, cy); sats[0].a += 0;
// advance angles properly
if (!REDUCED) { sats[0].a += 0; }
// (angles advance inside satPos via s.v — handled above/below)
/* stars */
for (const s of stars) {
const px = s.x * W + (m.x - .5) * -18 * s.z;
const py = s.y * H + (m.y - .5) * -12 * s.z;
const tw = REDUCED ? .8 : .45 + .55 * Math.sin(t * s.ts + s.tw);
const sz = s.z * 1.5 + .4;
ctx.globalAlpha = (0.14 + .5 * s.z) * tw;
ctx.drawImage(sprite, px - sz, py - sz, sz * 2, sz * 2);
}
ctx.globalAlpha = 1;
/* dust */
for (const d of dust) {
if (!REDUCED) { d.x += d.vx * dt * .06; d.y += d.vy * dt * .06; }
if (d.y < -.05) { d.y = 1.05; d.x = Math.random(); }
if (d.x < -.05) d.x = 1.05; if (d.x > 1.05) d.x = -.05;
ctx.fillStyle = `rgba(226,180,120,${d.a})`;
ctx.beginPath(); ctx.arc(d.x * W, d.y * H, d.r, 0, 6.2832); ctx.fill();
}
/* meteors */
if (!REDUCED) {
nextMeteor -= dt;
if (nextMeteor <= 0 && W > 480) {
nextMeteor = 6500 + Math.random() * 9000;
meteors.push({ x: W * (.15 + Math.random() * .7), y: -20,
vx: (Math.random() < .5 ? -1 : 1) * (.22 + Math.random() * .25),
vy: .18 + Math.random() * .16, life: 1 });
}
meteors = meteors.filter(mm => mm.life > 0);
for (const mm of meteors) {
mm.x += mm.vx * dt; mm.y += mm.vy * dt; mm.life -= dt * .0009;
const g = ctx.createLinearGradient(mm.x, mm.y, mm.x - mm.vx * 60, mm.y - mm.vy * 60);
g.addColorStop(0, `rgba(235,240,250,${.75 * mm.life})`);
g.addColorStop(1, 'rgba(235,240,250,0)');
ctx.strokeStyle = g; ctx.lineWidth = 1.1;
ctx.beginPath(); ctx.moveTo(mm.x, mm.y);
ctx.lineTo(mm.x - mm.vx * 60, mm.y - mm.vy * 60); ctx.stroke();
}
}
/* rings + satellites (behind planet pass) */
ring(cx, cy, rx1, ry1, rot1, .07);
ring(cx, cy, rx2, ry2, rot2, .05);
const pos1 = { ...p1 }; const pos2 = satPos(sats[1], rx2, ry2, rot2, cx, cy);
satDraw(pos1, cx, cy, rx1, ry1, rot1);
satDraw(pos2, cx, cy, rx2, ry2, rot2);
/* planet body */
ctx.beginPath(); ctx.arc(cx, cy, R, 0, 6.2832);
const bg = ctx.createRadialGradient(cx - (m.x - .5) * R * .3, cy - R * .45, R * .1, cx, cy, R);
bg.addColorStop(0, '#101117'); bg.addColorStop(.55, '#0B0B0F'); bg.addColorStop(1, '#070709');
ctx.fillStyle = bg; ctx.fill();
/* atmosphere halo */
const ga = .10 + atmoBoost * .28;
const glow = ctx.createRadialGradient(cx, cy, R * .985, cx, cy, R * 1.15);
glow.addColorStop(0, 'rgba(226,164,77,0)');
glow.addColorStop(.12, `rgba(226,164,77,${ga})`);
glow.addColorStop(1, 'rgba(226,164,77,0)');
ctx.beginPath(); ctx.arc(cx, cy, R * 1.15, 0, 6.2832); ctx.fillStyle = glow; ctx.fill();
/* rim light — warm key toward the light source */
const la = Math.atan2(-H * .35 - cy, W * (.5 + (m.x - .5) * .3) - cx);
ctx.save();
ctx.beginPath(); ctx.arc(cx, cy, R + .5, la - 1.15, la + 1.15);
ctx.strokeStyle = `rgba(242,190,107,${.7 + atmoBoost * .3})`;
ctx.lineWidth = 1.6; ctx.shadowColor = 'rgba(232,170,80,.9)'; ctx.shadowBlur = 14 + atmoBoost * 24;
ctx.stroke();
ctx.beginPath(); ctx.arc(cx, cy, R + 1, la - 1.6, la + 1.6);
ctx.strokeStyle = `rgba(226,164,77,${.26 + atmoBoost * .4})`;
ctx.lineWidth = 4; ctx.shadowBlur = 26; ctx.stroke();
/* faint cool rim on the night side */
ctx.beginPath(); ctx.arc(cx, cy, R, la + Math.PI - .8, la + Math.PI + .8);
ctx.strokeStyle = 'rgba(150,180,210,.10)'; ctx.lineWidth = 1; ctx.shadowBlur = 0; ctx.stroke();
ctx.restore();
/* settlement beacon on the limb */
const ba = -Math.PI / 2 - .5;
const bl = REDUCED ? .5 : .2 + .8 * Math.pow(Math.max(0, Math.sin(t * 1.6)), 4);
ctx.save(); ctx.shadowColor = 'rgba(79,224,164,.9)'; ctx.shadowBlur = 8;
ctx.fillStyle = `rgba(79,224,164,${bl})`;
ctx.beginPath();
ctx.arc(cx + Math.cos(ba) * R * 1.005, cy + Math.sin(ba) * R * 1.005, 1.6, 0, 6.2832);
ctx.fill(); ctx.restore();
/* auth-success pulse rings */
for (const p of pulses) {
p.delay -= dt; if (p.delay > 0) continue;
p.r += dt * .28; p.a -= dt * .00075;
if (p.a <= 0) continue;
ctx.beginPath(); ctx.arc(cx, cy, p.r, Math.PI, Math.PI * 2);
ctx.strokeStyle = `rgba(242,190,107,${p.a})`;
ctx.lineWidth = 1.2; ctx.shadowColor = 'rgba(226,164,77,.7)'; ctx.shadowBlur = 10;
ctx.stroke(); ctx.shadowBlur = 0;
}
pulses = pulses.filter(p => p.a > 0);
atmoBoost = Math.max(0, atmoBoost - dt * .0005);
/* front satellite re-draw */
if (pos1.front) satDraw(pos1, cx, cy, rx1, ry1, rot1);
if (pos2.front) satDraw(pos2, cx, cy, rx2, ry2, rot2);
}
function pulse() {
const { R } = geo();
pulses.push({ r: R + 4, a: .55, delay: 0 }, { r: R + 4, a: .4, delay: 240 });
atmoBoost = 1;
}
return { draw, pulse };
})();
/* ============================ cursor light ============================ */
const Light = (() => {
const el = $('#cursorLight'), term = $('#terminal');
let x = 0, y = 0, tx = 0, ty = 0, active = false;
if (matchMedia('(hover:hover) and (pointer:fine)').matches && !REDUCED) {
active = true;
addEventListener('pointermove', e => {
const r = term.getBoundingClientRect();
tx = e.clientX - r.left; ty = e.clientY - r.top;
});
} else el.style.display = 'none';
function update(dt) {
if (!active) return;
const k = 1 - Math.exp(-dt / 170);
x = lerp(x, tx, k); y = lerp(y, ty, k);
el.style.transform = `translate3d(${x}px,${y}px,0)`;
}
return { update };
})();
/* ============================ telemetry ============================ */
(() => {
const clock = $('#telClock'), sig = $('#telSig'), lat = $('#telLat');
const tick = () => clock.textContent = new Date().toISOString().slice(11, 19);
tick(); setInterval(tick, 1000);
setInterval(() => sig.textContent = (97 + Math.random() * 2.9).toFixed(1) + '%', 2400);
let v = 44.21;
setInterval(() => { v = (v + .0001) % 90; lat.textContent = `LAT ${v.toFixed(2)}°N`; }, 1000);
})();
/* ============================ modal ============================ */
const Modal = (() => {
const overlay = $('#overlay'), form = $('#modalForm'), input = $('#modalInput');
const msg = $('#modalMsg'), field = $('#fModal'), done = $('#sheetDone');
const kicker = $('#modalKicker'), title = $('#modalTitle'), desc = $('#modalDesc');
const mLabel = $('#modalLabel'), btnL = $('#modalBtnLabel'), doneT = $('#sheetDoneText');
const V = {
forgot: { k: 'RECOVERY PROTOCOL', t: 'Reset passphrase',
d: 'Enter the identifier tied to your clearance. We will dispatch a recovery signal to your relay inbox.',
btn: 'Send recovery signal', toast: 'RECOVERY SIGNAL DISPATCHED',
done: 'Recovery signal dispatched. Check your relay inbox for further instructions.' },
request: { k: 'CLEARANCE REQUEST', t: 'Request access',
d: 'Axiom is invitation-first. Leave your identifier and we will queue your request for review.',
btn: 'Submit request', toast: 'REQUEST QUEUED — AWAITING REVIEW',
done: 'Request queued. Clearance review typically completes within 48 hours.' }
};
let trigger = null, v = V.forgot, closer = null;
function open(kind, el) {
v = V[kind]; trigger = el;
kicker.textContent = v.k; title.textContent = v.t; desc.textContent = v.d;
btnL.textContent = v.btn; doneT.textContent = v.done;
form.reset(); form.hidden = false; done.hidden = true;
msg.textContent = ''; field.classList.remove('err');
clearTimeout(closer);
overlay.hidden = false;
requestAnimationFrame(() => overlay.classList.add('open'));
setTimeout(() => input.focus(), 90);
document.addEventListener('keydown', onKey);
}
function close() {
overlay.classList.remove('open');
document.removeEventListener('keydown', onKey);
clearTimeout(closer);
setTimeout(() => { overlay.hidden = true; if (trigger) trigger.focus(); }, 320);
}
const onKey = e => { if (e.key === 'Escape') close(); };
$('#modalClose').addEventListener('click', close);
overlay.addEventListener('click', e => { if (e.target === overlay) close(); });
form.addEventListener('submit', e => {
e.preventDefault();
const val = input.value.trim();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(val)) {
msg.textContent = 'A VALID RELAY ADDRESS IS REQUIRED';
field.classList.remove('err'); void field.offsetWidth; field.classList.add('err');
input.focus(); return;
}
msg.textContent = ''; field.classList.remove('err');
form.hidden = true; done.hidden = false;
Toast.ok(v.toast);
closer = setTimeout(close, 3000);
});
input.addEventListener('input', () => { msg.textContent = ''; field.classList.remove('err'); });
return { open };
})();
$('#forgotLink').addEventListener('click', e => Modal.open('forgot', e.currentTarget));
$('#requestLink').addEventListener('click', e => Modal.open('request', e.currentTarget));
/* ============================ login form ============================ */
(() => {
const form = $('#loginForm'), idIn = $('#identifier'), pwIn = $('#password');
const fId = $('#fId'), fPw = $('#fPw'), idMsg = $('#idMsg'), pwMsg = $('#pwMsg');
const btn = $('#loginBtn'), btnLabel = $('#btnLabel');
const channel = $('#channel'), channelText = $('#channelText');
const eye = $('#eyeBtn'), remember = $('#remember');
let busy = false, granted = false;
const setErr = (field, msgEl, msg) => {
msgEl.textContent = msg;
field.classList.toggle('err', !!msg);
if (msg) { field.classList.remove('shake'); void field.offsetWidth; field.classList.add('shake'); }
};
const validId = v => {
v = v.trim();
if (!v) return 'IDENTIFIER REQUIRED';
if (v.includes('@')) return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v) ? '' : 'MALFORMED ADDRESS — CHECK FORMAT';
return /^[\w.\-]{3,32}$/.test(v) ? '' : '3–32 CHARACTERS — LETTERS, DIGITS, . _ -';
};
const setChannel = s => {
channel.className = 'channel' + (s === 'default' ? '' : ' ' + s);
channelText.textContent = s === 'open' ? 'CHANNEL OPEN' : s === 'handshake' ? 'HANDSHAKE' : 'SECURE CHANNEL';
};
const resetGrant = () => {
if (!granted) return;
granted = false;
btn.classList.remove('success');
btnLabel.textContent = 'Open secure channel';
setChannel('default');
};
/* remembered identifier */
try {
const saved = localStorage.getItem('axiom.remember.id');
if (saved) { idIn.value = saved; remember.checked = true; }
} catch (e) {}
/* eye toggle */
eye.addEventListener('click', () => {
const show = pwIn.type === 'password';
pwIn.type = show ? 'text' : 'password';
eye.classList.toggle('revealed', show);
eye.setAttribute('aria-pressed', String(show));
eye.setAttribute('aria-label', show ? 'Hide passphrase' : 'Show passphrase');
pwIn.focus();
});
idIn.addEventListener('input', () => { setErr(fId, idMsg, ''); resetGrant(); });
idIn.addEventListener('blur', () => { const e = validId(idIn.value); if (e) setErr(fId, idMsg, e); });
pwIn.addEventListener('input', () => {
setErr(fPw, pwMsg, '');
Strength.update(pwIn.value);
resetGrant();
});
form.addEventListener('submit', e => {
e.preventDefault();
if (busy || granted) return;
const idErr = validId(idIn.value), pwv = pwIn.value;
const pwErr = !pwv ? 'PASSPHRASE REQUIRED'
: pwv.length < 6 ? `MIN 6 CHARACTERS — ${pwv.length}/6` : '';
setErr(fId, idMsg, idErr); setErr(fPw, pwMsg, pwErr);
if (idErr) { idIn.focus(); return; }
if (pwErr) { pwIn.focus(); return; }
busy = true;
btn.classList.add('loading');
btnLabel.textContent = 'Authenticating';
setChannel('handshake');
setTimeout(() => {
busy = false; granted = true;
btn.classList.remove('loading');
btn.classList.add('success');
btnLabel.textContent = 'Access granted';
setChannel('open');
Scene.pulse();
try {
if (remember.checked) localStorage.setItem('axiom.remember.id', idIn.value.trim());
else localStorage.removeItem('axiom.remember.id');
} catch (e) {}
const name = idIn.value.trim().split('@')[0];
Toast.ok(`CHANNEL OPEN — WELCOME BACK, ${name.toUpperCase()}`);
}, 1450);
});
Strength.update('');
})();
if (!REDUCED) {
let last = performance.now();
(function frame(now) {
const dt = clamp(now - last, 0, 50); last = now;
Scene.draw(dt, now);
Wave.draw(dt);
Light.update(dt);
requestAnimationFrame(frame);
})(last);
} else { Scene.draw(0, 0); }
})();
About this snippet
A complete, cinematic login screen built with plain HTML, CSS and vanilla JavaScript. The left half shows an animated orbital scene drawn on <canvas>; the right half is a terminal-style sign-in form with live feedback, a password recovery modal and toast notifications. It is a good reference for building rich auth pages without any framework.
Features
- Canvas scene: a planet with orbit rings and moving satellites, rendered in a
requestAnimationFrameloop and resized with the window. - Password strength "oscilloscope": as you type, the script estimates entropy from length and character pool and shows it as bits, a label and an animated waveform.
- Inline validation: identifier and passphrase fields show messages under the input instead of alerts.
- Show/hide password toggle, "remember this terminal" checkbox and a cursor-following light on the form.
- Modal window for password recovery and access requests, with its own validation, focus handling and Escape-to-close.
- Toasts for success and error messages, and support for
prefers-reduced-motion.
Structure
The CSS starts with design tokens in :root (colours, fonts, easing) and uses a two-column grid that collapses on small screens. The JavaScript is wrapped in a single IIFE and split into small modules: Toast, Strength, Wave, Scene, Light and Modal.
How to use it
- Copy the three files and open the HTML; it works without a build step.
- Connect the form submit handler to your own authentication endpoint (the demo only simulates the request).
- Rebrand the texts and the colour tokens such as
--amberand--phos. - Remove the canvas section if you need a lighter page; the form works on its own.
Note: the strength meter is a client-side estimate for user feedback. Always validate and hash passwords on the server.
Comments 0
No comments yet. Be the first to comment.