Web3 — Staking / Yield (APR · stake · claim)
A dark, glassy DeFi staking page for the fictional NOVA token on Lumen Chain: a gradient-bordered hero with big APR, TVL, and a pending-rewards counter that ticks up live in monospace; a stake/unstake card with MAX button, lock-period selector (Flexible to 1 year) driving an APR multiplier and projected-earnings calculator; a confirm-sign-success modal with risk warnings; a claim animation; and a selectable list of five other pools — all vanilla JS, UI-only simulation.
MCP
Code
:root {
--bg: #0a0b0f;
--surface: #13151c;
--surface-2: #1b1e27;
--elevated: #23262f;
--text: #e9ecf2;
--muted: #8a90a2;
--line: rgba(255, 255, 255, 0.08);
--line-2: rgba(255, 255, 255, 0.16);
--accent: #7c5cff;
--accent-2: #00e0c6;
--accent-glow: rgba(124, 92, 255, 0.45);
--pos: #26d07c;
--neg: #ff4d6d;
--warn: #ffb347;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--r-pill: 999px;
--font-ui: "Space Grotesk", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
background-image:
radial-gradient(800px 420px at 12% -8%, rgba(124, 92, 255, 0.14), transparent 60%),
radial-gradient(700px 380px at 92% 0%, rgba(0, 224, 198, 0.08), transparent 55%);
background-attachment: fixed;
color: var(--text);
font-family: var(--font-ui);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.mono {
font-family: var(--font-mono);
}
.pos { color: var(--pos); }
.neg { color: var(--neg); }
.muted { color: var(--muted); }
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
button {
font-family: inherit;
color: inherit;
cursor: pointer;
}
[hidden] {
display: none !important;
}
button:focus-visible,
input:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
border-radius: var(--r-sm);
}
.page {
max-width: 1120px;
margin: 0 auto;
padding: 20px clamp(14px, 3vw, 28px) 48px;
}
/* ---------- Top bar ---------- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 0 22px;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
}
.brand-orb {
width: 28px;
height: 28px;
border-radius: 50%;
background: conic-gradient(from 200deg, var(--accent), var(--accent-2), var(--accent));
box-shadow: 0 0 18px var(--accent-glow);
}
.brand-name {
font-weight: 700;
font-size: 1.15rem;
letter-spacing: -0.01em;
}
.brand-tag {
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--accent-2);
border: 1px solid rgba(0, 224, 198, 0.35);
border-radius: var(--r-pill);
padding: 2px 9px;
}
.topbar-right {
display: flex;
align-items: center;
gap: 10px;
}
.chain-pill,
.wallet-pill {
display: inline-flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--line);
border-radius: var(--r-pill);
padding: 7px 14px;
font-size: 0.85rem;
font-weight: 500;
backdrop-filter: blur(8px);
transition: border-color 0.15s ease, background 0.15s ease;
}
.chain-pill:hover,
.wallet-pill:hover {
border-color: var(--line-2);
background: rgba(255, 255, 255, 0.07);
}
.chain-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--pos);
box-shadow: 0 0 8px rgba(38, 208, 124, 0.8);
}
.wallet-pill .mono {
font-size: 0.8rem;
}
.wallet-avatar {
width: 18px;
height: 18px;
border-radius: 50%;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
}
/* ---------- Hero ---------- */
.hero-card {
position: relative;
display: grid;
grid-template-columns: 1.6fr 1fr;
gap: 24px;
border-radius: var(--r-lg);
padding: 26px 28px;
background:
linear-gradient(var(--surface), var(--surface)) padding-box,
linear-gradient(120deg, rgba(124, 92, 255, 0.7), rgba(0, 224, 198, 0.5), rgba(124, 92, 255, 0.25)) border-box;
border: 1px solid transparent;
overflow: hidden;
}
.hero-glow {
position: absolute;
inset: -40% 40% 40% -10%;
background: radial-gradient(circle, var(--accent-glow), transparent 65%);
opacity: 0.5;
pointer-events: none;
filter: blur(20px);
}
.hero-main {
position: relative;
min-width: 0;
}
.hero-token {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
.hero-token h1 {
margin: 0;
font-size: clamp(1.25rem, 2.6vw, 1.6rem);
letter-spacing: -0.02em;
}
.hero-sub {
margin: 2px 0 0;
color: var(--muted);
font-size: 0.85rem;
}
.token-logo {
display: grid;
place-items: center;
width: 48px;
height: 48px;
border-radius: 50%;
font-weight: 700;
font-size: 1.2rem;
color: #0a0b0f;
flex-shrink: 0;
}
.token-logo.sm {
width: 32px;
height: 32px;
font-size: 0.85rem;
}
.token-logo.nova { background: linear-gradient(135deg, #9d7bff, #00e0c6); box-shadow: 0 0 16px var(--accent-glow); }
.token-logo.lum { background: linear-gradient(135deg, #ffd86b, #ff9d5c); }
.token-logo.aur { background: linear-gradient(135deg, #ffe29a, #d4a017); }
.token-logo.zphr { background: linear-gradient(135deg, #7fd4ff, #4a7dff); }
.token-logo.oblk { background: linear-gradient(135deg, #b8bdd0, #5a5f74); }
.token-logo.hlx { background: linear-gradient(135deg, #4a4e60, #2c2f3c); color: var(--muted); }
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.68rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 3px 10px;
border-radius: var(--r-pill);
}
.badge-live {
color: var(--pos);
border: 1px solid rgba(38, 208, 124, 0.4);
background: rgba(38, 208, 124, 0.08);
}
.badge-boost {
color: var(--accent-2);
border: 1px solid rgba(0, 224, 198, 0.35);
background: rgba(0, 224, 198, 0.08);
margin-left: 6px;
}
.badge-risk {
color: var(--warn);
border: 1px solid rgba(255, 179, 71, 0.4);
background: rgba(255, 179, 71, 0.08);
margin-left: 6px;
}
.pulse {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--pos);
animation: pulse 1.8s ease-out infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(38, 208, 124, 0.6); }
70% { box-shadow: 0 0 0 7px rgba(38, 208, 124, 0); }
100% { box-shadow: 0 0 0 0 rgba(38, 208, 124, 0); }
}
.hero-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-top: 26px;
}
.stat {
display: flex;
flex-direction: column;
gap: 2px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px 14px;
backdrop-filter: blur(6px);
}
.stat-label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--muted);
}
.stat-value {
font-size: 1.15rem;
font-weight: 700;
}
.stat-value.apr {
font-size: 1.7rem;
background: linear-gradient(90deg, var(--accent-2), #8affe6);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 0 24px rgba(0, 224, 198, 0.25);
}
.stat-note {
font-size: 0.72rem;
color: var(--muted);
}
.stat-note.pos { color: var(--pos); }
/* ---------- Position panel ---------- */
.position {
position: relative;
background: rgba(10, 11, 15, 0.55);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 18px;
backdrop-filter: blur(10px);
display: flex;
flex-direction: column;
gap: 9px;
}
.position-title {
margin: 0 0 4px;
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--muted);
}
.position-row {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 10px;
font-size: 0.88rem;
}
.position-row > span:first-child {
color: var(--muted);
}
.position-row .mono {
font-size: 0.85rem;
}
.position-row.sub {
margin-top: -7px;
font-size: 0.76rem;
}
.position-row.sub .mono {
font-size: 0.76rem;
color: var(--muted);
}
.reward-ticker {
color: var(--accent-2);
font-weight: 700;
font-size: 1.05rem !important;
text-shadow: 0 0 14px rgba(0, 224, 198, 0.4);
font-variant-numeric: tabular-nums;
}
.position-foot {
margin: 2px 0 0;
font-size: 0.68rem;
color: var(--muted);
}
/* ---------- Buttons ---------- */
.btn {
border: none;
border-radius: var(--r-md);
padding: 13px 18px;
font-size: 0.95rem;
font-weight: 600;
letter-spacing: 0.01em;
transition: transform 0.12s ease, box-shadow 0.2s ease, filter 0.15s ease, opacity 0.15s ease;
}
.btn:active:not(:disabled) {
transform: translateY(1px) scale(0.99);
}
.btn-primary {
width: 100%;
background: linear-gradient(110deg, var(--accent), #9d7bff 55%, var(--accent-2) 130%);
color: #fff;
box-shadow: 0 6px 24px var(--accent-glow);
}
.btn-primary:hover:not(:disabled) {
filter: brightness(1.1);
box-shadow: 0 8px 30px var(--accent-glow);
}
.btn-primary:disabled {
background: var(--surface-2);
color: var(--muted);
box-shadow: none;
cursor: not-allowed;
}
.btn-ghost {
background: transparent;
border: 1px solid var(--line-2);
color: var(--text);
}
.btn-ghost:hover {
background: rgba(255, 255, 255, 0.06);
}
.btn-claim {
margin-top: 4px;
background: rgba(0, 224, 198, 0.1);
border: 1px solid rgba(0, 224, 198, 0.45);
color: var(--accent-2);
position: relative;
overflow: hidden;
}
.btn-claim:hover:not(:disabled) {
background: rgba(0, 224, 198, 0.18);
box-shadow: 0 0 18px rgba(0, 224, 198, 0.25);
}
.btn-claim:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.btn-claim.is-claiming::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(110deg, transparent 30%, rgba(0, 224, 198, 0.35) 50%, transparent 70%);
animation: sheen 0.9s ease infinite;
}
@keyframes sheen {
from { transform: translateX(-100%); }
to { transform: translateX(100%); }
}
/* ---------- Main grid ---------- */
.grid {
display: grid;
grid-template-columns: 1.05fr 1fr;
gap: 22px;
margin-top: 22px;
align-items: start;
}
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 22px;
}
/* ---------- Stake card ---------- */
.tabs {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 4px;
margin-bottom: 20px;
}
.tab {
background: transparent;
border: none;
border-radius: 10px;
padding: 9px;
font-size: 0.9rem;
font-weight: 600;
color: var(--muted);
transition: background 0.15s ease, color 0.15s ease;
}
.tab:hover { color: var(--text); }
.tab.is-active {
background: var(--elevated);
color: var(--text);
box-shadow: inset 0 0 0 1px var(--line-2);
}
.field-label {
display: flex;
justify-content: space-between;
align-items: baseline;
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
margin-bottom: 8px;
padding: 0;
}
.field-balance {
text-transform: none;
letter-spacing: 0;
font-weight: 400;
}
.field-balance .mono { font-size: 0.76rem; color: var(--text); }
.amount-box {
display: flex;
align-items: center;
gap: 10px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px 12px 12px 14px;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.amount-box:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(124, 92, 255, 0.18);
}
.amount-box input {
flex: 1;
min-width: 0;
background: transparent;
border: none;
color: var(--text);
font-family: var(--font-mono);
font-size: 1.2rem;
font-weight: 500;
}
.amount-box input::placeholder { color: var(--muted); }
.amount-box input:focus { outline: none; }
.amount-usd {
font-size: 0.74rem;
color: var(--muted);
white-space: nowrap;
}
.btn-max {
background: rgba(124, 92, 255, 0.14);
border: 1px solid rgba(124, 92, 255, 0.45);
color: #b5a3ff;
border-radius: var(--r-pill);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.08em;
padding: 5px 12px;
transition: background 0.15s ease;
}
.btn-max:hover { background: rgba(124, 92, 255, 0.28); }
/* Lock period */
.lock-select {
border: none;
margin: 18px 0 0;
padding: 0;
}
.lock-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.lock-opt {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 10px 6px;
transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
}
.lock-opt:hover {
border-color: var(--line-2);
background: var(--elevated);
}
.lock-opt.is-active {
border-color: var(--accent);
background: rgba(124, 92, 255, 0.12);
box-shadow: 0 0 14px rgba(124, 92, 255, 0.25);
}
.lock-name {
font-size: 0.8rem;
font-weight: 600;
}
.lock-mult {
font-size: 0.72rem;
color: var(--accent-2);
}
/* Projection */
.projection {
margin-top: 18px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.proj-row {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 10px;
font-size: 0.86rem;
}
.proj-row > span:first-child { color: var(--muted); }
.proj-row .mono { font-size: 0.84rem; font-variant-numeric: tabular-nums; }
.proj-row.sub { font-size: 0.76rem; }
.proj-row.sub .mono { font-size: 0.74rem; color: var(--muted); }
.proj-usd {
font-style: normal;
color: var(--muted);
font-size: 0.74rem;
}
.lock-warning {
margin: 14px 0 0;
font-size: 0.8rem;
color: var(--warn);
background: rgba(255, 179, 71, 0.07);
border: 1px solid rgba(255, 179, 71, 0.3);
border-radius: var(--r-md);
padding: 10px 14px;
}
#actionBtn { margin-top: 16px; }
.fee-note {
margin: 10px 0 0;
font-size: 0.68rem;
color: var(--muted);
text-align: center;
}
/* ---------- Pools list ---------- */
.pools-head {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 14px;
}
.pools-head h2 {
margin: 0;
font-size: 1.05rem;
letter-spacing: -0.01em;
}
.pools-count {
font-size: 0.72rem;
color: var(--muted);
}
.pool-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.pool-row {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
text-align: left;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px 14px;
transition: border-color 0.15s ease, background 0.15s ease, transform 0.12s ease;
}
.pool-row:hover:not(:disabled) {
border-color: var(--line-2);
background: var(--elevated);
transform: translateX(2px);
}
.pool-row.is-active {
border-color: var(--accent);
background: rgba(124, 92, 255, 0.1);
box-shadow: 0 0 16px rgba(124, 92, 255, 0.18);
}
.pool-row.is-disabled {
opacity: 0.55;
cursor: not-allowed;
}
.pool-info {
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
flex: 1;
}
.pool-name {
font-weight: 600;
font-size: 0.95rem;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.pool-sub {
font-size: 0.74rem;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pool-stats {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 1px;
}
.pool-apr {
font-weight: 700;
font-size: 0.95rem;
}
.pool-tvl {
font-size: 0.72rem;
color: var(--muted);
}
/* ---------- Footer ---------- */
.foot {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
margin-top: 26px;
font-size: 0.72rem;
color: var(--muted);
}
/* ---------- Modal ---------- */
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(5, 6, 9, 0.7);
backdrop-filter: blur(6px);
display: grid;
place-items: center;
padding: 20px;
z-index: 60;
}
.modal {
width: 100%;
max-width: 420px;
background:
linear-gradient(var(--surface), var(--surface)) padding-box,
linear-gradient(140deg, rgba(124, 92, 255, 0.6), rgba(0, 224, 198, 0.35)) border-box;
border: 1px solid transparent;
border-radius: var(--r-lg);
padding: 24px;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6);
animation: modal-in 0.22s cubic-bezier(0.2, 0.9, 0.3, 1.2);
}
@keyframes modal-in {
from { transform: translateY(14px) scale(0.97); opacity: 0; }
to { transform: none; opacity: 1; }
}
.modal h3 {
margin: 0 0 16px;
font-size: 1.15rem;
letter-spacing: -0.01em;
}
.modal-step.center {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
text-align: center;
padding: 12px 0 4px;
}
.modal-step.center h3 { margin: 6px 0 0; }
.modal-summary {
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.msum-row {
display: flex;
justify-content: space-between;
gap: 12px;
font-size: 0.87rem;
}
.msum-row > span:first-child { color: var(--muted); }
.msum-row .mono { font-size: 0.82rem; }
.msum-row.sub { font-size: 0.76rem; }
.msum-row.sub .mono { font-size: 0.74rem; color: var(--muted); }
.modal-risk {
margin: 14px 0 0;
font-size: 0.78rem;
color: var(--warn);
background: rgba(255, 179, 71, 0.07);
border: 1px solid rgba(255, 179, 71, 0.3);
border-radius: var(--r-md);
padding: 10px 14px;
}
.modal-actions {
display: grid;
grid-template-columns: 1fr 1.4fr;
gap: 10px;
margin-top: 18px;
}
.modal-hash {
margin: 0;
font-size: 0.74rem;
color: var(--muted);
}
.spinner {
width: 44px;
height: 44px;
border-radius: 50%;
border: 3px solid var(--line-2);
border-top-color: var(--accent);
animation: spin 0.85s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.success-check svg {
width: 56px;
height: 56px;
fill: none;
stroke: var(--pos);
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
}
.success-check circle {
stroke-dasharray: 151;
stroke-dashoffset: 151;
animation: draw 0.5s ease forwards;
}
.success-check path {
stroke-dasharray: 36;
stroke-dashoffset: 36;
animation: draw 0.35s 0.35s ease forwards;
}
@keyframes draw { to { stroke-dashoffset: 0; } }
#modalSuccess .btn { margin-top: 14px; width: 100%; }
/* ---------- Toasts ---------- */
.toast-stack {
position: fixed;
bottom: 18px;
right: 18px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 80;
}
.toast {
background: var(--elevated);
border: 1px solid var(--line-2);
border-left: 3px solid var(--accent-2);
border-radius: var(--r-md);
padding: 11px 16px;
font-size: 0.85rem;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
animation: toast-in 0.25s ease;
max-width: 320px;
}
.toast.warn { border-left-color: var(--warn); }
.toast.neg { border-left-color: var(--neg); }
.toast.out {
animation: toast-out 0.3s ease forwards;
}
@keyframes toast-in {
from { transform: translateY(10px); opacity: 0; }
to { transform: none; opacity: 1; }
}
@keyframes toast-out {
to { transform: translateY(6px); opacity: 0; }
}
/* Claim fly-up animation */
.fly-amount {
position: fixed;
font-family: var(--font-mono);
font-weight: 700;
color: var(--accent-2);
text-shadow: 0 0 14px rgba(0, 224, 198, 0.5);
pointer-events: none;
z-index: 90;
animation: fly-up 1.1s ease-out forwards;
}
@keyframes fly-up {
from { transform: translateY(0); opacity: 1; }
to { transform: translateY(-46px); opacity: 0; }
}
/* ---------- Responsive ---------- */
@media (max-width: 900px) {
.hero-card { grid-template-columns: 1fr; }
.hero-stats { grid-template-columns: repeat(2, 1fr); }
.grid { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
.page { padding: 14px 12px 36px; }
.topbar { flex-wrap: wrap; }
.brand-tag { display: none; }
.chain-pill { font-size: 0.76rem; padding: 6px 10px; }
.hero-card { padding: 18px 16px; }
.hero-stats { grid-template-columns: 1fr 1fr; gap: 10px; }
.stat-value.apr { font-size: 1.4rem; }
.lock-grid { grid-template-columns: repeat(2, 1fr); }
.card { padding: 16px; }
.pool-row { padding: 10px 12px; }
.pool-sub { max-width: 130px; }
.modal { padding: 18px; }
.foot { flex-direction: column; }
}/* NovaYield — Staking / Yield (UI simulation, no real wallet or RPC) */
(() => {
"use strict";
/* ---------------- Mock data ---------------- */
const POOLS = {
nova: {
symbol: "NOVA",
title: "NOVA Staking Pool",
sub: "Single-sided staking · Audited · Rewards stream every block",
apr: 12.4,
tvl: "$48.2M",
tvlNote: "▲ 3.1% · 24h",
stakers: "21,408",
price: 2.84,
contract: "0x9bd2…77e1",
logo: "nova",
letter: "N",
staked: 1250,
wallet: 3418.52,
},
lum: {
symbol: "LUM",
title: "LUM Staking Pool",
sub: "Native gas token · Secures Lumen Chain consensus",
apr: 8.92,
tvl: "$112.7M",
tvlNote: "▲ 1.2% · 24h",
stakers: "64,902",
price: 0.92,
contract: "0x4fa1…2c9b",
logo: "lum",
letter: "L",
staked: 0,
wallet: 8200.4,
},
aur: {
symbol: "AURUM",
title: "AURUM Boosted Pool",
sub: "LST yield · Aurora Vaults · Boosted emissions",
apr: 19.75,
tvl: "$9.4M",
tvlNote: "▲ 6.4% · 24h",
stakers: "3,117",
price: 14.05,
contract: "0xd03e…91aa",
logo: "aur",
letter: "A",
staked: 42.5,
wallet: 96.31,
},
zphr: {
symbol: "ZPHR",
title: "ZPHR Governance Pool",
sub: "Governance staking · Zephyr DAO voting power",
apr: 6.3,
tvl: "$27.1M",
tvlNote: "▼ 0.8% · 24h",
stakers: "11,260",
price: 1.18,
contract: "0x71c8…e4d0",
logo: "zphr",
letter: "Z",
staked: 0,
wallet: 1500,
},
oblk: {
symbol: "OBSIDIAN",
title: "OBSIDIAN Insurance Pool",
sub: "Perps insurance fund · High risk, high emissions",
apr: 34.1,
tvl: "$2.8M",
tvlNote: "▲ 12.6% · 24h",
stakers: "884",
price: 0.41,
contract: "0xab57…03fe",
logo: "oblk",
letter: "O",
staked: 0,
wallet: 12000,
},
};
const LOCKS = {
flex: { label: "Flexible", days: 0, mult: 1.0 },
30: { label: "30 days", days: 30, mult: 1.25 },
90: { label: "90 days", days: 90, mult: 1.6 },
365: { label: "1 year", days: 365, mult: 2.2 },
};
/* ---------------- State ---------------- */
const state = {
poolId: "nova",
mode: "stake", // "stake" | "unstake"
lock: "flex",
pending: 0.184203, // pending rewards, in pool token
userLockMult: 1.0, // boost applied to the existing position
};
/* ---------------- DOM ---------------- */
const $ = (id) => document.getElementById(id);
const el = {
heroTitle: $("heroTitle"),
heroApr: $("heroApr"),
heroAprNote: $("heroAprNote"),
heroTvl: $("heroTvl"),
stakedBalance: $("stakedBalance"),
walletBalance: $("walletBalance"),
pendingRewards: $("pendingRewards"),
pendingUsd: $("pendingUsd"),
claimBtn: $("claimBtn"),
tabStake: $("tabStake"),
tabUnstake: $("tabUnstake"),
amountLabel: $("amountLabel"),
availLabel: $("availLabel"),
amountInput: $("amountInput"),
amountUsd: $("amountUsd"),
maxBtn: $("maxBtn"),
lockSelect: $("lockSelect"),
lockOpts: Array.from(document.querySelectorAll(".lock-opt")),
projApr: $("projApr"),
projDaily: $("projDaily"),
projHorizonLabel: $("projHorizonLabel"),
projTotal: $("projTotal"),
projTotalUsd: $("projTotalUsd"),
projUnlock: $("projUnlock"),
lockWarning: $("lockWarning"),
actionBtn: $("actionBtn"),
poolRows: Array.from(document.querySelectorAll(".pool-row[data-pool]")),
modalBackdrop: $("modalBackdrop"),
modalConfirm: $("modalConfirm"),
modalPending: $("modalPending"),
modalSuccess: $("modalSuccess"),
modalTitle: $("modalTitle"),
mAction: $("mAction"),
mAmount: $("mAmount"),
mLock: $("mLock"),
mApr: $("mApr"),
mUnlock: $("mUnlock"),
mRisk: $("mRisk"),
modalCancel: $("modalCancel"),
modalSign: $("modalSign"),
modalDone: $("modalDone"),
successTitle: $("successTitle"),
successHash: $("successHash"),
pendingHash: document.querySelector("#modalPending .modal-hash"),
heroLogo: document.querySelector(".hero-token .token-logo"),
inputLogo: document.querySelector(".amount-box .token-logo"),
rewardStatValue: document.querySelector(".hero-stats .stat:nth-child(4) .stat-value"),
rewardStatNote: document.querySelector(".hero-stats .stat:nth-child(4) .stat-note"),
stakersStat: document.querySelector(".hero-stats .stat:nth-child(3) .stat-value"),
tvlNote: document.querySelector(".hero-stats .stat:nth-child(2) .stat-note"),
feeNote: document.querySelector(".fee-note"),
toastStack: $("toastStack"),
};
/* ---------------- Helpers ---------------- */
const pool = () => POOLS[state.poolId];
const lock = () => LOCKS[state.lock];
const fmt = (n, dp = 4) =>
n.toLocaleString("en-US", { minimumFractionDigits: dp, maximumFractionDigits: dp });
const fmtUsd = (n) =>
"$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const parseAmount = () => {
const v = parseFloat(el.amountInput.value.replace(/,/g, ""));
return Number.isFinite(v) && v > 0 ? v : 0;
};
const randHash = () => {
const hex = "0123456789abcdef";
const part = (n) =>
Array.from({ length: n }, () => hex[(Math.random() * 16) | 0]).join("");
return `0x${part(4)}…${part(4)}`;
};
function toast(msg, type = "") {
const t = document.createElement("div");
t.className = `toast${type ? " " + type : ""}`;
t.textContent = msg;
el.toastStack.appendChild(t);
setTimeout(() => {
t.classList.add("out");
t.addEventListener("animationend", () => t.remove(), { once: true });
}, 3400);
}
/* ---------------- Rewards ticker ---------------- */
// rewards per second for the existing staked position
const rewardRate = () =>
(pool().staked * (pool().apr / 100) * state.userLockMult) / (365 * 24 * 3600);
let lastTick = performance.now();
function tick(now) {
const dt = (now - lastTick) / 1000;
lastTick = now;
state.pending += rewardRate() * dt;
el.pendingRewards.textContent = fmt(state.pending, 6);
el.pendingUsd.textContent = fmtUsd(state.pending * pool().price);
const claimable = state.pending >= 0.000001;
if (el.claimBtn.disabled === claimable) el.claimBtn.disabled = !claimable;
requestAnimationFrame(tick);
}
/* ---------------- Renderers ---------------- */
function renderPosition() {
const p = pool();
el.stakedBalance.textContent = `${fmt(p.staked)} ${p.symbol}`;
el.walletBalance.textContent = `${fmt(p.wallet)} ${p.symbol}`;
el.claimBtn.disabled = state.pending < 0.000001;
}
function renderHero() {
const p = pool();
el.heroTitle.textContent = p.title;
document.querySelector(".hero-sub").textContent = p.sub;
el.heroApr.textContent = p.apr.toFixed(2) + "%";
el.heroTvl.textContent = p.tvl;
el.tvlNote.textContent = p.tvlNote;
el.stakersStat.textContent = p.stakers;
el.rewardStatValue.textContent = p.symbol;
el.rewardStatNote.textContent = p.contract;
el.heroLogo.className = `token-logo ${p.logo}`;
el.heroLogo.textContent = p.letter;
el.inputLogo.className = `token-logo ${p.logo} sm`;
el.inputLogo.textContent = p.letter;
el.feeNote.textContent = `Network fee ≈ 0.0012 LUM · slippage n/a · pool ${p.contract}`;
}
function renderHeroAprNote() {
const l = lock();
el.heroAprNote.textContent = `${l.label} · ${l.mult.toFixed(2)}× boost`;
}
function available() {
return state.mode === "stake" ? pool().wallet : pool().staked;
}
function renderModeLabels() {
const p = pool();
const staking = state.mode === "stake";
el.amountLabel.textContent = staking ? "Amount to stake" : "Amount to unstake";
el.availLabel.textContent = `${fmt(available(), 2)} ${p.symbol}`;
el.lockSelect.hidden = !staking;
el.tabStake.classList.toggle("is-active", staking);
el.tabStake.setAttribute("aria-selected", String(staking));
el.tabUnstake.classList.toggle("is-active", !staking);
el.tabUnstake.setAttribute("aria-selected", String(!staking));
}
function unlockDate() {
const days = lock().days;
if (!days) return "Anytime";
const d = new Date();
d.setDate(d.getDate() + days);
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
}
function renderProjection() {
const p = pool();
const amount = parseAmount();
const l = lock();
const effApr = p.apr * l.mult;
const horizonDays = l.days || 365;
const daily = (amount * (effApr / 100)) / 365;
const total = daily * horizonDays;
el.amountUsd.textContent = fmtUsd(amount * p.price);
el.projApr.textContent = effApr.toFixed(2) + "%";
el.projDaily.textContent = `${fmt(daily)} ${p.symbol}`;
el.projHorizonLabel.textContent =
`Projected · ${l.days ? l.label : "1 year"}`;
el.projTotal.firstChild.textContent = `${fmt(total)} ${p.symbol} `;
el.projTotalUsd.textContent = `(${fmtUsd(total * p.price)})`;
el.projUnlock.textContent = state.mode === "stake" ? unlockDate() : "Now";
el.lockWarning.hidden = !(state.mode === "stake" && l.days > 0 && amount > 0);
}
function renderActionBtn() {
const p = pool();
const amount = parseAmount();
const max = available();
const verb = state.mode === "stake" ? "Stake" : "Unstake";
if (amount <= 0) {
el.actionBtn.disabled = true;
el.actionBtn.textContent = "Enter an amount";
} else if (amount > max) {
el.actionBtn.disabled = true;
el.actionBtn.textContent =
state.mode === "stake" ? "Insufficient balance" : "Exceeds staked amount";
} else {
el.actionBtn.disabled = false;
el.actionBtn.textContent = `${verb} ${fmt(amount, 2)} ${p.symbol}`;
}
}
function renderAll() {
renderHero();
renderHeroAprNote();
renderPosition();
renderModeLabels();
renderProjection();
renderActionBtn();
}
/* ---------------- Tabs ---------------- */
el.tabStake.addEventListener("click", () => {
state.mode = "stake";
el.amountInput.value = "";
renderModeLabels();
renderProjection();
renderActionBtn();
});
el.tabUnstake.addEventListener("click", () => {
state.mode = "unstake";
el.amountInput.value = "";
renderModeLabels();
renderProjection();
renderActionBtn();
});
/* ---------------- Amount input ---------------- */
el.amountInput.addEventListener("input", () => {
// keep digits and a single decimal point
let v = el.amountInput.value.replace(/[^0-9.]/g, "");
const i = v.indexOf(".");
if (i !== -1) v = v.slice(0, i + 1) + v.slice(i + 1).replace(/\./g, "");
if (v !== el.amountInput.value) el.amountInput.value = v;
renderProjection();
renderActionBtn();
});
el.maxBtn.addEventListener("click", () => {
el.amountInput.value = String(available());
el.amountInput.focus();
renderProjection();
renderActionBtn();
});
/* ---------------- Lock period ---------------- */
el.lockOpts.forEach((opt) => {
opt.addEventListener("click", () => {
state.lock = opt.dataset.lock;
el.lockOpts.forEach((o) => {
const active = o === opt;
o.classList.toggle("is-active", active);
o.setAttribute("aria-checked", String(active));
});
renderHeroAprNote();
renderProjection();
renderActionBtn();
});
});
/* ---------------- Pool selection ---------------- */
el.poolRows.forEach((row) => {
row.addEventListener("click", () => {
if (row.disabled || row.dataset.pool === state.poolId) return;
state.poolId = row.dataset.pool;
state.pending = pool().staked > 0 ? Math.random() * 0.4 : 0;
el.poolRows.forEach((r) => {
const active = r === row;
r.classList.toggle("is-active", active);
r.setAttribute("aria-pressed", String(active));
});
el.amountInput.value = "";
renderAll();
toast(`Switched to ${pool().symbol} pool`);
});
});
/* ---------------- Modal flow ---------------- */
let modalBusy = false;
function openModal() {
const p = pool();
const l = lock();
const amount = parseAmount();
const staking = state.mode === "stake";
el.modalTitle.textContent = staking ? "Confirm stake" : "Confirm unstake";
el.mAction.textContent = staking ? "Stake" : "Unstake";
el.mAmount.textContent = `${fmt(amount, 2)} ${p.symbol}`;
el.mLock.textContent = staking ? l.label : "—";
el.mApr.textContent = staking ? (p.apr * l.mult).toFixed(2) + "%" : p.apr.toFixed(2) + "%";
el.mUnlock.textContent = staking ? unlockDate() : "Immediately";
el.mRisk.textContent = staking
? l.days > 0
? `You are signing a simulated transaction. Tokens are locked until ${unlockDate()}. Early exit forfeits all accrued rewards.`
: "You are signing a simulated transaction. Flexible positions can be unstaked anytime; rewards accrue every block."
: "You are signing a simulated transaction. Unstaking claims pending rewards and returns tokens to your wallet.";
el.modalConfirm.hidden = false;
el.modalPending.hidden = true;
el.modalSuccess.hidden = true;
el.modalBackdrop.hidden = false;
modalBusy = false;
el.modalSign.focus();
}
function closeModal() {
if (modalBusy) return;
el.modalBackdrop.hidden = true;
el.actionBtn.focus();
}
el.actionBtn.addEventListener("click", openModal);
el.modalCancel.addEventListener("click", closeModal);
el.modalBackdrop.addEventListener("click", (e) => {
if (e.target === el.modalBackdrop) closeModal();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && !el.modalBackdrop.hidden) closeModal();
});
el.modalSign.addEventListener("click", () => {
const p = pool();
const amount = parseAmount();
const staking = state.mode === "stake";
const hash = randHash();
modalBusy = true;
el.modalConfirm.hidden = true;
el.modalPending.hidden = false;
el.pendingHash.textContent = `tx ${hash}`;
setTimeout(() => {
// settle the simulated transaction
if (staking) {
p.wallet -= amount;
p.staked += amount;
state.userLockMult = lock().mult;
} else {
p.staked -= amount;
p.wallet += amount + state.pending; // unstake auto-claims
if (state.pending > 0.000001) {
toast(`Claimed ${fmt(state.pending, 4)} ${p.symbol} with unstake`);
}
state.pending = 0;
if (p.staked <= 0) state.userLockMult = 1;
}
const block = (18204560 + ((Math.random() * 900) | 0)).toLocaleString("en-US");
el.successTitle.textContent = staking ? "Staked successfully" : "Unstaked successfully";
el.successHash.textContent = `tx ${hash} · block ${block}`;
el.modalPending.hidden = true;
el.modalSuccess.hidden = false;
modalBusy = false;
el.modalDone.focus();
el.amountInput.value = "";
renderPosition();
renderModeLabels();
renderProjection();
renderActionBtn();
toast(
staking
? `Staked ${fmt(amount, 2)} ${p.symbol} · ${lock().label}`
: `Unstaked ${fmt(amount, 2)} ${p.symbol}`
);
}, 1600);
});
el.modalDone.addEventListener("click", closeModal);
/* ---------------- Claim ---------------- */
el.claimBtn.addEventListener("click", () => {
if (state.pending < 0.000001 || el.claimBtn.classList.contains("is-claiming")) {
if (state.pending < 0.000001) toast("Nothing to claim yet", "warn");
return;
}
const p = pool();
const claimed = state.pending;
el.claimBtn.classList.add("is-claiming");
el.claimBtn.querySelector(".btn-claim-label").textContent = "Claiming…";
setTimeout(() => {
// fly-up amount animation from the rewards row
const rect = el.pendingRewards.getBoundingClientRect();
const fly = document.createElement("span");
fly.className = "fly-amount";
fly.textContent = `+${fmt(claimed, 4)} ${p.symbol}`;
fly.style.left = `${rect.left}px`;
fly.style.top = `${rect.top - 6}px`;
document.body.appendChild(fly);
fly.addEventListener("animationend", () => fly.remove(), { once: true });
p.wallet += claimed;
state.pending = 0;
el.claimBtn.classList.remove("is-claiming");
el.claimBtn.querySelector(".btn-claim-label").textContent = "Claim rewards";
renderPosition();
renderModeLabels();
toast(`Claimed ${fmt(claimed, 4)} ${p.symbol} · tx ${randHash()}`);
}, 1200);
});
/* ---------------- Misc ---------------- */
el.walletBtn = $("walletBtn");
el.walletBtn.addEventListener("click", () => {
toast("Wallet 0x7a3f…c41d copied (simulated)");
});
/* ---------------- Init ---------------- */
renderAll();
requestAnimationFrame((t) => {
lastTick = t;
requestAnimationFrame(tick);
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>NovaYield — Staking / Yield</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=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<!-- Top bar -->
<header class="topbar">
<div class="brand">
<span class="brand-orb" aria-hidden="true"></span>
<span class="brand-name">NovaYield</span>
<span class="brand-tag">Staking</span>
</div>
<div class="topbar-right">
<button class="chain-pill" type="button" aria-label="Network: Lumen Chain">
<span class="chain-dot" aria-hidden="true"></span>
Lumen Chain
</button>
<button class="wallet-pill" type="button" id="walletBtn" aria-label="Connected wallet 0x7a3f…c41d">
<span class="wallet-avatar" aria-hidden="true"></span>
<span class="mono">0x7a3f…c41d</span>
</button>
</div>
</header>
<!-- Hero: featured pool -->
<section class="hero" aria-labelledby="heroTitle">
<div class="hero-card">
<div class="hero-glow" aria-hidden="true"></div>
<div class="hero-main">
<div class="hero-token">
<div class="token-logo nova" aria-hidden="true">N</div>
<div>
<h1 id="heroTitle">NOVA Staking Pool</h1>
<p class="hero-sub">Single-sided staking · Audited · Rewards stream every block</p>
</div>
<span class="badge badge-live"><span class="pulse" aria-hidden="true"></span> Live</span>
</div>
<div class="hero-stats">
<div class="stat">
<span class="stat-label">Current APR</span>
<span class="stat-value apr mono" id="heroApr">12.40%</span>
<span class="stat-note" id="heroAprNote">Flexible · 1.00× boost</span>
</div>
<div class="stat">
<span class="stat-label">TVL</span>
<span class="stat-value mono" id="heroTvl">$48.2M</span>
<span class="stat-note pos">▲ 3.1% · 24h</span>
</div>
<div class="stat">
<span class="stat-label">Stakers</span>
<span class="stat-value mono">21,408</span>
<span class="stat-note">across 4 lock tiers</span>
</div>
<div class="stat">
<span class="stat-label">Reward token</span>
<span class="stat-value">NOVA</span>
<span class="stat-note mono">0x9bd2…77e1</span>
</div>
</div>
</div>
<!-- Your position -->
<aside class="position" aria-label="Your position">
<h2 class="position-title">Your position</h2>
<div class="position-row">
<span>Staked</span>
<span class="mono" id="stakedBalance">1,250.0000 NOVA</span>
</div>
<div class="position-row">
<span>Wallet</span>
<span class="mono" id="walletBalance">3,418.5200 NOVA</span>
</div>
<div class="position-row rewards">
<span>Pending rewards</span>
<span class="mono reward-ticker" id="pendingRewards" aria-live="off">0.000000</span>
</div>
<div class="position-row sub">
<span>≈ value</span>
<span class="mono" id="pendingUsd">$0.00</span>
</div>
<button class="btn btn-claim" type="button" id="claimBtn">
<span class="btn-claim-label">Claim rewards</span>
</button>
<p class="position-foot mono">Last claim · 6h 12m ago · tx 0x41be…09af</p>
</aside>
</div>
</section>
<main class="grid">
<!-- Stake / Unstake card -->
<section class="card stake-card" aria-labelledby="stakeTitle">
<div class="tabs" role="tablist" aria-label="Stake or unstake">
<button class="tab is-active" role="tab" aria-selected="true" id="tabStake" type="button">Stake</button>
<button class="tab" role="tab" aria-selected="false" id="tabUnstake" type="button">Unstake</button>
</div>
<h2 id="stakeTitle" class="sr-only">Stake NOVA</h2>
<label class="field-label" for="amountInput">
<span id="amountLabel">Amount to stake</span>
<span class="field-balance">Available: <span class="mono" id="availLabel">3,418.52 NOVA</span></span>
</label>
<div class="amount-box">
<div class="token-logo nova sm" aria-hidden="true">N</div>
<input type="text" inputmode="decimal" id="amountInput" placeholder="0.00" autocomplete="off" aria-describedby="amountUsd" />
<span class="amount-usd mono" id="amountUsd">$0.00</span>
<button class="btn-max" type="button" id="maxBtn">MAX</button>
</div>
<fieldset class="lock-select" id="lockSelect">
<legend class="field-label">Lock period</legend>
<div class="lock-grid" role="radiogroup" aria-label="Lock period">
<button class="lock-opt is-active" type="button" data-lock="flex" role="radio" aria-checked="true">
<span class="lock-name">Flexible</span>
<span class="lock-mult mono">1.00×</span>
</button>
<button class="lock-opt" type="button" data-lock="30" role="radio" aria-checked="false">
<span class="lock-name">30 days</span>
<span class="lock-mult mono">1.25×</span>
</button>
<button class="lock-opt" type="button" data-lock="90" role="radio" aria-checked="false">
<span class="lock-name">90 days</span>
<span class="lock-mult mono">1.60×</span>
</button>
<button class="lock-opt" type="button" data-lock="365" role="radio" aria-checked="false">
<span class="lock-name">1 year</span>
<span class="lock-mult mono">2.20×</span>
</button>
</div>
</fieldset>
<div class="projection" aria-live="polite">
<div class="proj-row">
<span>Effective APR</span>
<span class="mono pos" id="projApr">12.40%</span>
</div>
<div class="proj-row">
<span>Daily earnings</span>
<span class="mono" id="projDaily">0.0000 NOVA</span>
</div>
<div class="proj-row">
<span id="projHorizonLabel">Projected · 1 year</span>
<span class="mono" id="projTotal">0.0000 NOVA <em class="proj-usd" id="projTotalUsd">($0.00)</em></span>
</div>
<div class="proj-row sub">
<span>Unlock date</span>
<span class="mono" id="projUnlock">Anytime</span>
</div>
</div>
<p class="lock-warning" id="lockWarning" hidden>
<strong>Lock-up applies.</strong> Tokens cannot be unstaked before the unlock date. Early exit forfeits all rewards.
</p>
<button class="btn btn-primary" type="button" id="actionBtn" disabled>Enter an amount</button>
<p class="fee-note mono">Network fee ≈ 0.0012 LUM · slippage n/a · pool 0x9bd2…77e1</p>
</section>
<!-- Other pools -->
<section class="card pools-card" aria-labelledby="poolsTitle">
<div class="pools-head">
<h2 id="poolsTitle">All pools</h2>
<span class="pools-count mono">6 pools</span>
</div>
<ul class="pool-list" id="poolList">
<li>
<button class="pool-row is-active" type="button" data-pool="nova" aria-pressed="true">
<div class="token-logo nova sm" aria-hidden="true">N</div>
<div class="pool-info">
<span class="pool-name">NOVA</span>
<span class="pool-sub">Single stake · Lumen Chain</span>
</div>
<div class="pool-stats">
<span class="pool-apr mono pos">12.40%</span>
<span class="pool-tvl mono">$48.2M TVL</span>
</div>
</button>
</li>
<li>
<button class="pool-row" type="button" data-pool="lum" aria-pressed="false">
<div class="token-logo lum sm" aria-hidden="true">L</div>
<div class="pool-info">
<span class="pool-name">LUM</span>
<span class="pool-sub">Native gas token · Lumen Chain</span>
</div>
<div class="pool-stats">
<span class="pool-apr mono pos">8.92%</span>
<span class="pool-tvl mono">$112.7M TVL</span>
</div>
</button>
</li>
<li>
<button class="pool-row" type="button" data-pool="aur" aria-pressed="false">
<div class="token-logo aur sm" aria-hidden="true">A</div>
<div class="pool-info">
<span class="pool-name">AURUM <span class="badge badge-boost">Boosted</span></span>
<span class="pool-sub">LST yield · Aurora Vaults</span>
</div>
<div class="pool-stats">
<span class="pool-apr mono pos">19.75%</span>
<span class="pool-tvl mono">$9.4M TVL</span>
</div>
</button>
</li>
<li>
<button class="pool-row" type="button" data-pool="zphr" aria-pressed="false">
<div class="token-logo zphr sm" aria-hidden="true">Z</div>
<div class="pool-info">
<span class="pool-name">ZPHR</span>
<span class="pool-sub">Governance · Zephyr DAO</span>
</div>
<div class="pool-stats">
<span class="pool-apr mono pos">6.30%</span>
<span class="pool-tvl mono">$27.1M TVL</span>
</div>
</button>
</li>
<li>
<button class="pool-row" type="button" data-pool="oblk" aria-pressed="false">
<div class="token-logo oblk sm" aria-hidden="true">O</div>
<div class="pool-info">
<span class="pool-name">OBSIDIAN <span class="badge badge-risk">High risk</span></span>
<span class="pool-sub">Perps insurance fund</span>
</div>
<div class="pool-stats">
<span class="pool-apr mono pos">34.10%</span>
<span class="pool-tvl mono">$2.8M TVL</span>
</div>
</button>
</li>
<li>
<button class="pool-row is-disabled" type="button" data-pool="hlx" disabled>
<div class="token-logo hlx sm" aria-hidden="true">H</div>
<div class="pool-info">
<span class="pool-name">HELIX</span>
<span class="pool-sub">Coming soon · epoch 14</span>
</div>
<div class="pool-stats">
<span class="pool-apr mono muted">—</span>
<span class="pool-tvl mono">Opens Jun 21</span>
</div>
</button>
</li>
</ul>
</section>
</main>
<footer class="foot">
<span class="mono">epoch 9,412 · block 18,204,557</span>
<span>UI simulation — no real funds, fictional tokens.</span>
</footer>
</div>
<!-- Confirm modal -->
<div class="modal-backdrop" id="modalBackdrop" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<!-- Confirm step -->
<div class="modal-step" id="modalConfirm">
<h3 id="modalTitle">Confirm stake</h3>
<div class="modal-summary">
<div class="msum-row"><span>Action</span><span id="mAction">Stake</span></div>
<div class="msum-row"><span>Amount</span><span class="mono" id="mAmount">0 NOVA</span></div>
<div class="msum-row"><span>Lock period</span><span id="mLock">Flexible</span></div>
<div class="msum-row"><span>Effective APR</span><span class="mono pos" id="mApr">12.40%</span></div>
<div class="msum-row"><span>Unlock</span><span class="mono" id="mUnlock">Anytime</span></div>
<div class="msum-row sub"><span>Network fee</span><span class="mono">≈ 0.0012 LUM</span></div>
<div class="msum-row sub"><span>Contract</span><span class="mono">0x9bd2…77e1</span></div>
</div>
<p class="modal-risk" id="mRisk">
You are signing a simulated transaction. Locked positions cannot be withdrawn early; rewards are forfeited on emergency exit.
</p>
<div class="modal-actions">
<button class="btn btn-ghost" type="button" id="modalCancel">Cancel</button>
<button class="btn btn-primary" type="button" id="modalSign">Sign & confirm</button>
</div>
</div>
<!-- Pending step -->
<div class="modal-step center" id="modalPending" hidden>
<div class="spinner" aria-hidden="true"></div>
<h3>Broadcasting transaction…</h3>
<p class="mono modal-hash">tx 0x3c8a…f02b</p>
</div>
<!-- Success step -->
<div class="modal-step center" id="modalSuccess" hidden>
<div class="success-check" aria-hidden="true">
<svg viewBox="0 0 52 52"><circle cx="26" cy="26" r="24"/><path d="M14 27l8 8 16-17"/></svg>
</div>
<h3 id="successTitle">Staked successfully</h3>
<p class="mono modal-hash" id="successHash">tx 0x3c8a…f02b · block 18,204,560</p>
<button class="btn btn-primary" type="button" id="modalDone">Done</button>
</div>
</div>
</div>
<div class="toast-stack" id="toastStack" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Staking / Yield (APR · stake · claim)
A full staking page for the fictional NOVA pool on Lumen Chain. The gradient-bordered hero card shows the headline APR with a neon gradient treatment, TVL, staker count, and the reward-token contract, next to a glassy “Your position” panel where pending rewards tick up in real time (tabular monospace digits) with a live USD equivalent and a Claim button that plays a sheen + fly-up animation and credits your wallet balance.
The stake/unstake card has Stake and Unstake tabs, a decimal-sanitized amount input with MAX and live USD value, and a lock-period selector (Flexible / 30d / 90d / 1y) whose multiplier (1.00×–2.20×) feeds a projection box: effective APR, daily earnings, projected total over the horizon, and the unlock date — with an amber lock-up warning when a fixed term is chosen. The primary button validates the amount (“Enter an amount” / “Insufficient balance”) and opens a confirm modal summarizing action, amount, lock, APR, unlock date, and network fee plus a risk notice, then steps through a broadcasting spinner to an animated success check with a fake tx hash and block number.
Below, an “All pools” list offers five more fictional pools (LUM, AURUM boosted, ZPHR, high-risk OBSIDIAN, and a disabled coming-soon HELIX); selecting one swaps the hero stats, balances, token logo, and calculator over to that pool. Toast notifications confirm every action, and the layout collapses cleanly down to 360px.
UI-only simulation — no real wallet, RPC, or on-chain calls. Mock data, fictional tokens.