stage-26 polish: restyle the /admin gate as a secure console
Replaced the plain login box with a proper restricted-zone screen: ambient breathing radial backdrop, frosted glass card with a glowing top accent + animated HUD corner brackets, an SVG shield-lock emblem in a pulsing glow ring, a SECURE CHANNEL status line with a blinking dot, the QR in a white frame with a sweeping cyan scanline, a large OTP-style monospace code field, and a gradient UNLOCK button. Honors prefers-reduced-motion. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -376,32 +376,92 @@ tr.sev-low .sev-badge { color: var(--muted); }
|
|||||||
.disco-strobe, .disco-bolt, .ioc-fly { animation: none; }
|
.disco-strobe, .disco-bolt, .ioc-fly { animation: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── admin gate (TOTP) ──────────────────────────────────────── */
|
/* ── admin gate (TOTP) — secure console ─────────────────────── */
|
||||||
.gate-wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; }
|
.gate-body { overflow: hidden; }
|
||||||
|
.gate-bg {
|
||||||
|
position: fixed; inset: 0; z-index: 0; pointer-events: none;
|
||||||
|
background:
|
||||||
|
radial-gradient(900px 600px at 50% -10%, rgba(30,200,255,0.16), transparent 60%),
|
||||||
|
radial-gradient(700px 500px at 80% 110%, rgba(167,139,250,0.12), transparent 60%),
|
||||||
|
radial-gradient(600px 500px at 12% 90%, rgba(30,200,255,0.08), transparent 60%);
|
||||||
|
animation: gate-breathe 7s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes gate-breathe { 0%,100% { opacity: 0.75; } 50% { opacity: 1; } }
|
||||||
|
.gate-wrap { position: relative; z-index: 1; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; }
|
||||||
.gate-card {
|
.gate-card {
|
||||||
width: 360px; max-width: 100%; text-align: center; padding: 32px 28px;
|
width: 380px; max-width: 100%; text-align: center; padding: 38px 32px 28px;
|
||||||
background: var(--panel); border: 1px solid var(--border); border-radius: 12px;
|
background: linear-gradient(180deg, rgba(28,34,48,0.82), rgba(18,22,30,0.92));
|
||||||
box-shadow: 0 0 40px rgba(0,0,0,0.5); position: relative;
|
backdrop-filter: blur(14px) saturate(120%);
|
||||||
|
border: 1px solid rgba(120,140,170,0.18); border-radius: 16px;
|
||||||
|
box-shadow: 0 24px 70px rgba(0,0,0,0.55), 0 0 0 1px rgba(30,200,255,0.06), inset 0 1px 0 rgba(255,255,255,0.04);
|
||||||
|
position: relative; animation: gate-rise 0.5s cubic-bezier(.2,.9,.3,1);
|
||||||
}
|
}
|
||||||
.gate-card::before, .gate-card::after { content: ""; position: absolute; width: 16px; height: 16px; border: 2px solid var(--accent); opacity: 0.6; }
|
.gate-card::before { /* glowing top accent line */
|
||||||
.gate-card::before { top: 8px; left: 8px; border-right: none; border-bottom: none; }
|
content: ""; position: absolute; top: 0; left: 22%; right: 22%; height: 2px; border-radius: 2px;
|
||||||
.gate-card::after { bottom: 8px; right: 8px; border-left: none; border-top: none; }
|
background: linear-gradient(90deg, transparent, var(--accent), transparent);
|
||||||
.gate-logo { width: 56px; height: 56px; filter: drop-shadow(0 0 10px var(--accent-glow)); }
|
box-shadow: 0 0 14px var(--accent); opacity: 0.85;
|
||||||
.gate-title { font-family: var(--font-display); font-size: 22px; margin: 12px 0 4px; letter-spacing: 0.08em; text-shadow: 0 0 16px var(--accent-glow); }
|
}
|
||||||
.gate-sub { color: var(--muted); font-size: 13px; margin: 0 0 18px; }
|
@keyframes gate-rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
|
||||||
.gate-setup { margin: 16px 0; padding: 14px; background: var(--bg); border: 1px solid var(--border); border-radius: 8px; }
|
.corner { position: absolute; width: 18px; height: 18px; border: 2px solid var(--accent); opacity: 0.5; }
|
||||||
.gate-step { font-size: 12px; color: var(--text); margin: 0 0 12px; }
|
.corner.tl { top: 10px; left: 10px; border-right: none; border-bottom: none; border-radius: 4px 0 0 0; }
|
||||||
.gate-qr { width: 168px; height: 168px; border-radius: 6px; background: #fff; padding: 6px; }
|
.corner.tr { top: 10px; right: 10px; border-left: none; border-bottom: none; border-radius: 0 4px 0 0; }
|
||||||
.gate-error { color: var(--red); font-size: 13px; margin: 12px 0; }
|
.corner.bl { bottom: 10px; left: 10px; border-right: none; border-top: none; border-radius: 0 0 0 4px; }
|
||||||
.gate-form { display: flex; gap: 8px; margin-top: 14px; }
|
.corner.br { bottom: 10px; right: 10px; border-left: none; border-top: none; border-radius: 0 0 4px 0; }
|
||||||
|
|
||||||
|
.gate-emblem { position: relative; display: inline-grid; place-items: center; color: var(--accent); margin-bottom: 6px; }
|
||||||
|
.gate-emblem svg { filter: drop-shadow(0 0 10px var(--accent-glow)); position: relative; z-index: 1; }
|
||||||
|
.emblem-ring {
|
||||||
|
position: absolute; width: 76px; height: 76px; border-radius: 50%;
|
||||||
|
border: 1px solid rgba(30,200,255,0.35); box-shadow: 0 0 24px rgba(30,200,255,0.25) inset;
|
||||||
|
animation: ring-pulse 2.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes ring-pulse { 0%,100% { transform: scale(1); opacity: 0.5; } 50% { transform: scale(1.12); opacity: 0.9; } }
|
||||||
|
|
||||||
|
.gate-status {
|
||||||
|
display: inline-flex; align-items: center; gap: 7px; margin: 8px 0 2px;
|
||||||
|
font-size: 10.5px; letter-spacing: 0.22em; color: var(--muted); text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.pulse-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--green); box-shadow: 0 0 8px var(--green); animation: dot-blink 1.4s steps(2) infinite; }
|
||||||
|
@keyframes dot-blink { 0%,60% { opacity: 1; } 80%,100% { opacity: 0.25; } }
|
||||||
|
.gate-title { font-family: var(--font-display); font-size: 25px; font-weight: 700; margin: 6px 0 2px; letter-spacing: 0.16em; color: #eaf6ff; text-shadow: 0 0 22px rgba(30,200,255,0.45); }
|
||||||
|
.gate-sub { color: var(--muted); font-size: 12.5px; margin: 0 0 20px; letter-spacing: 0.04em; }
|
||||||
|
|
||||||
|
.gate-setup { margin: 4px 0 18px; }
|
||||||
|
.gate-qr-frame {
|
||||||
|
position: relative; display: inline-block; padding: 10px; border-radius: 12px;
|
||||||
|
background: #fff; box-shadow: 0 0 0 1px rgba(30,200,255,0.4), 0 0 26px rgba(30,200,255,0.22); overflow: hidden;
|
||||||
|
}
|
||||||
|
.gate-qr { display: block; width: 158px; height: 158px; border-radius: 4px; }
|
||||||
|
.scanline {
|
||||||
|
position: absolute; left: 10px; right: 10px; height: 22px; top: 10px; border-radius: 4px;
|
||||||
|
background: linear-gradient(180deg, rgba(30,200,255,0.45), transparent);
|
||||||
|
box-shadow: 0 0 14px var(--accent); animation: scan 2.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes scan { 0% { transform: translateY(0); } 50% { transform: translateY(136px); } 100% { transform: translateY(0); } }
|
||||||
|
.gate-step { font-size: 12px; color: var(--text); margin: 12px 0 0; opacity: 0.85; }
|
||||||
|
|
||||||
|
.gate-error { color: var(--red); font-size: 12.5px; margin: 0 0 12px; padding: 8px; border: 1px solid rgba(248,113,113,0.4); border-radius: 8px; background: rgba(248,113,113,0.08); }
|
||||||
|
.gate-form { display: flex; flex-direction: column; gap: 10px; margin-top: 6px; }
|
||||||
.gate-input {
|
.gate-input {
|
||||||
flex: 1; background: var(--bg); color: var(--text); border: 1px solid var(--border);
|
background: rgba(10,13,18,0.85); color: #eaf6ff; border: 1px solid rgba(120,140,170,0.25);
|
||||||
border-radius: 6px; padding: 10px; font: inherit; font-size: 22px; letter-spacing: 0.4em; text-align: center;
|
border-radius: 10px; padding: 14px; font-family: ui-monospace, Menlo, monospace;
|
||||||
|
font-size: 30px; letter-spacing: 0.5em; text-align: center; text-indent: 0.5em; transition: all 0.15s;
|
||||||
}
|
}
|
||||||
.gate-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
|
.gate-input::placeholder { color: rgba(125,133,151,0.5); letter-spacing: 0.4em; }
|
||||||
.gate-btn { padding: 10px 16px; }
|
.gate-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow), 0 0 22px rgba(30,200,255,0.25); }
|
||||||
.gate-hint { color: var(--muted); font-size: 11px; margin-top: 12px; }
|
.gate-btn {
|
||||||
|
padding: 13px; border: none; border-radius: 10px; cursor: pointer;
|
||||||
|
font-family: var(--font-display); font-weight: 700; font-size: 14px; letter-spacing: 0.18em;
|
||||||
|
color: #04121a; background: linear-gradient(180deg, #5ad8ff, var(--accent));
|
||||||
|
box-shadow: 0 6px 20px rgba(30,200,255,0.3); transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.gate-btn:hover { box-shadow: 0 8px 30px rgba(30,200,255,0.5); transform: translateY(-1px); }
|
||||||
|
.gate-btn:active { transform: translateY(0); }
|
||||||
|
.gate-hint { color: var(--muted); font-size: 10.5px; margin-top: 14px; letter-spacing: 0.06em; }
|
||||||
|
@media (prefers-reduced-motion: reduce) { .gate-bg, .emblem-ring, .pulse-dot, .scanline, .gate-card { animation: none; } }
|
||||||
|
|
||||||
|
/* ── admin dashboard ────────────────────────────────────────── */
|
||||||
.admin-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; margin-top: 18px; }
|
.admin-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; margin-top: 18px; }
|
||||||
.admin-tile { padding: 16px; background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px; }
|
.admin-tile { padding: 18px; background: linear-gradient(180deg, var(--panel-2), rgba(18,22,30,0.6)); border: 1px solid var(--border); border-radius: 10px; }
|
||||||
.admin-tile h2 { font-size: 15px; margin: 0 0 6px; }
|
.admin-tile h2 { font-size: 15px; margin: 0 0 6px; }
|
||||||
.admin-tile p { font-size: 13px; color: var(--muted); margin: 0; }
|
.admin-tile p { font-size: 13px; color: var(--muted); margin: 0; }
|
||||||
|
|||||||
@@ -2,35 +2,54 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>psyc · restricted</title>
|
<title>psyc · restricted</title>
|
||||||
<meta name="robots" content="noindex,nofollow">
|
<meta name="robots" content="noindex,nofollow">
|
||||||
<link rel="icon" type="image/png" href="/static/psyc-logo.png">
|
<link rel="icon" type="image/png" href="/static/psyc-logo.png">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/static/cockpit.css">
|
<link rel="stylesheet" href="/static/cockpit.css">
|
||||||
<link rel="stylesheet" href="/static/psyc-tokens.css">
|
<link rel="stylesheet" href="/static/psyc-tokens.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="gate-body">
|
||||||
|
<div class="gate-bg"></div>
|
||||||
<div class="gate-wrap">
|
<div class="gate-wrap">
|
||||||
<div class="gate-card">
|
<div class="gate-card">
|
||||||
<img class="gate-logo" src="/static/psyc-logo.png" alt="psyc">
|
<span class="corner tl"></span><span class="corner tr"></span>
|
||||||
<h1 class="gate-title">Restricted Zone</h1>
|
<span class="corner bl"></span><span class="corner br"></span>
|
||||||
<p class="gate-sub">Admin control center — authenticator required.</p>
|
|
||||||
|
<div class="gate-emblem">
|
||||||
|
<svg viewBox="0 0 48 48" width="46" height="46" aria-hidden="true">
|
||||||
|
<path d="M24 3 L42 10 V24 C42 35 34 42 24 45 C14 42 6 35 6 24 V10 Z"
|
||||||
|
fill="none" stroke="currentColor" stroke-width="2.2" stroke-linejoin="round"/>
|
||||||
|
<circle cx="24" cy="21" r="5" fill="none" stroke="currentColor" stroke-width="2.2"/>
|
||||||
|
<rect x="22" y="24" width="4" height="8" rx="2" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
<span class="emblem-ring"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gate-status"><span class="pulse-dot"></span> SECURE CHANNEL · TOTP</div>
|
||||||
|
<h1 class="gate-title">RESTRICTED ZONE</h1>
|
||||||
|
<p class="gate-sub">psyc · admin control center</p>
|
||||||
|
|
||||||
{% if not provisioned %}
|
{% if not provisioned %}
|
||||||
<div class="gate-setup">
|
<div class="gate-setup">
|
||||||
<p class="gate-step">First time? Scan this with Google Authenticator / Authy, then enter the code below.</p>
|
<div class="gate-qr-frame">
|
||||||
<img class="gate-qr" src="{{ qr }}" alt="TOTP QR code">
|
<img class="gate-qr" src="{{ qr }}" alt="TOTP enrollment QR">
|
||||||
|
<span class="scanline"></span>
|
||||||
|
</div>
|
||||||
|
<p class="gate-step">Scan with Google Authenticator or Authy to enroll</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if error %}<div class="gate-error">✗ Invalid or expired code — try the current one.</div>{% endif %}
|
{% if error %}<div class="gate-error">✗ Invalid or expired code</div>{% endif %}
|
||||||
|
|
||||||
<form method="post" action="/admin/verify" class="gate-form">
|
<form method="post" action="/admin/verify" class="gate-form">
|
||||||
<input type="text" name="code" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code"
|
<input type="text" name="code" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code"
|
||||||
maxlength="6" placeholder="000000" class="gate-input" autofocus>
|
maxlength="6" placeholder="••••••" class="gate-input" autofocus>
|
||||||
<button type="submit" class="btn btn-enforce gate-btn">Verify</button>
|
<button type="submit" class="gate-btn">UNLOCK</button>
|
||||||
</form>
|
</form>
|
||||||
<p class="gate-hint">6-digit code, rotates every 30s.</p>
|
<p class="gate-hint">6-digit code · rotates every 30 s</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user