Web3 — NFT Detail (traits · history · buy)
A glassy, dark-mode NFT detail page for a fictional marketplace: a CSS-drawn generative artwork with fullscreen zoom, favorite and share actions, a verified collection link, owner chip, and a gradient-bordered price panel with Buy now, Make offer, and a live auction countdown. Includes a traits grid with rarity bars and hover tooltips, plus Properties, Offers, and Activity tabs with an animated price-history chart, sales table, and a confirm sheet with fee breakdown and signing risk warning.
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;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background:
radial-gradient(1100px 540px at 85% -10%, rgba(124, 92, 255, 0.14), transparent 60%),
radial-gradient(900px 480px at -10% 30%, rgba(0, 224, 198, 0.07), transparent 55%),
var(--bg);
color: var(--text);
font-family: "Space Grotesk", system-ui, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
}
.mono { font-family: "JetBrains Mono", ui-monospace, monospace; }
.muted { color: var(--muted); }
.small { font-size: 0.8rem; }
.pos { color: var(--pos); }
.neg { color: var(--neg); }
.link { color: var(--accent-2); cursor: pointer; }
.sr-only {
position: absolute;
width: 1px; height: 1px;
margin: -1px; padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
a { color: inherit; text-decoration: none; }
button {
font: inherit;
color: inherit;
cursor: pointer;
border: none;
background: none;
}
:is(button, a, input):focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
border-radius: var(--r-sm);
}
/* ---------- glass surfaces ---------- */
.glass {
background: rgba(19, 21, 28, 0.72);
border: 1px solid var(--line);
border-radius: var(--r-lg);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
}
.glass-strong {
background: linear-gradient(160deg, rgba(35, 38, 47, 0.85), rgba(19, 21, 28, 0.9));
border: 1px solid var(--line-2);
border-radius: var(--r-lg);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
}
/* ---------- topbar ---------- */
.topbar {
display: flex;
align-items: center;
gap: 28px;
padding: 14px 28px;
border-bottom: 1px solid var(--line);
background: rgba(10, 11, 15, 0.8);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
position: sticky;
top: 0;
z-index: 40;
}
.brand {
display: inline-flex;
align-items: center;
gap: 10px;
font-weight: 700;
font-size: 1.05rem;
letter-spacing: 0.01em;
}
.brand-dim { color: var(--muted); font-weight: 500; }
.brand-mark {
width: 22px; height: 22px;
border-radius: 7px;
background: conic-gradient(from 210deg, var(--accent), var(--accent-2), var(--accent));
box-shadow: 0 0 14px var(--accent-glow);
}
.topbar-nav { display: flex; gap: 6px; margin-right: auto; }
.topnav-link {
padding: 7px 14px;
border-radius: var(--r-pill);
color: var(--muted);
font-weight: 500;
font-size: 0.92rem;
transition: color 0.15s, background 0.15s;
}
.topnav-link:hover { color: var(--text); background: rgba(255, 255, 255, 0.05); }
.topnav-link.is-active { color: var(--text); background: rgba(124, 92, 255, 0.16); }
.wallet-chip {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 8px 14px;
border: 1px solid var(--line-2);
border-radius: var(--r-pill);
background: var(--surface);
font-size: 0.82rem;
transition: border-color 0.15s, box-shadow 0.15s;
}
.wallet-chip:hover { border-color: var(--accent); box-shadow: 0 0 16px rgba(124, 92, 255, 0.25); }
.wallet-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--pos);
box-shadow: 0 0 8px rgba(38, 208, 124, 0.8);
}
.wallet-bal {
color: var(--accent-2);
border-left: 1px solid var(--line);
padding-left: 10px;
}
/* ---------- breadcrumbs ---------- */
.crumbs {
max-width: 1240px;
margin: 0 auto;
padding: 18px 28px 0;
display: flex;
gap: 10px;
font-size: 0.85rem;
color: var(--muted);
}
.crumbs a:hover { color: var(--accent-2); }
.crumbs [aria-current] { color: var(--text); }
/* ---------- layout ---------- */
.layout {
max-width: 1240px;
margin: 0 auto;
padding: 22px 28px 80px;
display: grid;
grid-template-columns: minmax(340px, 480px) 1fr;
gap: 36px;
align-items: start;
}
/* ---------- artwork ---------- */
.art-frame {
position: relative;
margin: 0;
border-radius: var(--r-lg);
padding: 1px;
background: linear-gradient(155deg, rgba(124, 92, 255, 0.7), rgba(255, 255, 255, 0.08) 40%, rgba(0, 224, 198, 0.55));
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55), 0 0 44px rgba(124, 92, 255, 0.16);
}
.art-canvas {
position: relative;
aspect-ratio: 1;
border-radius: calc(var(--r-lg) - 1px);
overflow: hidden;
background:
radial-gradient(120% 90% at 80% 110%, rgba(0, 224, 198, 0.18), transparent 55%),
radial-gradient(100% 80% at 15% -5%, rgba(124, 92, 255, 0.3), transparent 60%),
#0c0e16;
}
.art-stars {
position: absolute; inset: 0;
background-image:
radial-gradient(1.5px 1.5px at 12% 22%, rgba(255, 255, 255, 0.9), transparent 60%),
radial-gradient(1px 1px at 34% 64%, rgba(255, 255, 255, 0.7), transparent 60%),
radial-gradient(2px 2px at 58% 14%, rgba(255, 255, 255, 0.55), transparent 60%),
radial-gradient(1px 1px at 73% 42%, rgba(255, 255, 255, 0.8), transparent 60%),
radial-gradient(1.5px 1.5px at 88% 78%, rgba(255, 255, 255, 0.65), transparent 60%),
radial-gradient(1px 1px at 22% 86%, rgba(255, 255, 255, 0.7), transparent 60%),
radial-gradient(1px 1px at 47% 35%, rgba(255, 255, 255, 0.45), transparent 60%);
animation: twinkle 5s ease-in-out infinite alternate;
}
@keyframes twinkle {
from { opacity: 0.55; }
to { opacity: 1; }
}
.art-grid {
position: absolute;
inset: auto -20% -32% -20%;
height: 62%;
background:
linear-gradient(rgba(0, 224, 198, 0.22) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 224, 198, 0.22) 1px, transparent 1px);
background-size: 44px 34px;
transform: perspective(420px) rotateX(62deg);
transform-origin: top;
mask-image: linear-gradient(to bottom, transparent, #000 25%, #000 70%, transparent);
-webkit-mask-image: linear-gradient(to bottom, transparent, #000 25%, #000 70%, transparent);
}
.art-orb {
position: absolute;
top: 16%;
left: 50%;
width: 46%;
aspect-ratio: 1;
transform: translateX(-50%);
animation: drift 7s ease-in-out infinite alternate;
}
@keyframes drift {
from { transform: translateX(-54%) translateY(0); }
to { transform: translateX(-46%) translateY(4%); }
}
.art-orb-core {
position: absolute; inset: 0;
border-radius: 50%;
background:
radial-gradient(circle at 32% 28%, #c9b8ff, var(--accent) 42%, #2b1e6b 78%, #16113a);
box-shadow:
0 0 60px var(--accent-glow),
0 0 120px rgba(0, 224, 198, 0.18),
inset -16px -20px 48px rgba(0, 0, 0, 0.55);
}
.art-orb-ring {
position: absolute;
left: 50%; top: 50%;
border: 1.5px solid rgba(0, 224, 198, 0.55);
border-radius: 50%;
transform: translate(-50%, -50%) rotateX(72deg);
}
.art-orb-ring.r1 { width: 150%; height: 150%; animation: ringspin 14s linear infinite; }
.art-orb-ring.r2 {
width: 184%; height: 184%;
border-color: rgba(124, 92, 255, 0.4);
border-style: dashed;
animation: ringspin 22s linear infinite reverse;
}
@keyframes ringspin {
from { transform: translate(-50%, -50%) rotateX(72deg) rotateZ(0deg); }
to { transform: translate(-50%, -50%) rotateX(72deg) rotateZ(360deg); }
}
.art-comet {
position: absolute;
top: 18%;
left: -12%;
width: 90px; height: 2px;
border-radius: var(--r-pill);
background: linear-gradient(90deg, transparent, var(--accent-2));
box-shadow: 0 0 12px rgba(0, 224, 198, 0.8);
animation: comet 6.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
opacity: 0;
}
@keyframes comet {
0% { transform: translate(0, 0) rotate(14deg); opacity: 0; }
6% { opacity: 1; }
26% { transform: translate(360px, 110px) rotate(14deg); opacity: 0; }
100% { transform: translate(360px, 110px) rotate(14deg); opacity: 0; }
}
.art-sig {
position: absolute;
bottom: 14px;
right: 16px;
font-size: 0.72rem;
letter-spacing: 0.22em;
color: rgba(233, 236, 242, 0.55);
}
.art-zoom {
position: absolute;
top: 14px;
right: 14px;
display: grid;
place-items: center;
width: 38px; height: 38px;
border-radius: var(--r-md);
background: rgba(10, 11, 15, 0.6);
border: 1px solid var(--line-2);
color: var(--text);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s;
}
.art-zoom:hover { border-color: var(--accent); box-shadow: 0 0 14px var(--accent-glow); transform: scale(1.06); }
/* fullscreen mode */
.art-frame.is-fullscreen {
position: fixed;
inset: 0;
z-index: 100;
border-radius: 0;
padding: 0;
background: rgba(8, 9, 12, 0.96);
display: grid;
place-items: center;
}
.art-frame.is-fullscreen .art-canvas {
width: min(92vw, 86vh);
border-radius: var(--r-lg);
}
.art-frame.is-fullscreen .art-zoom { top: 22px; right: 22px; }
/* ---------- art actions ---------- */
.art-actions {
display: flex;
align-items: center;
gap: 10px;
margin-top: 16px;
}
.icon-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
border-radius: var(--r-pill);
border: 1px solid var(--line);
background: var(--surface);
color: var(--muted);
font-size: 0.85rem;
font-weight: 500;
transition: color 0.15s, border-color 0.15s, box-shadow 0.15s;
}
.icon-btn:hover { color: var(--text); border-color: var(--line-2); }
.icon-btn[aria-pressed="true"] {
color: var(--neg);
border-color: rgba(255, 77, 109, 0.5);
box-shadow: 0 0 14px rgba(255, 77, 109, 0.2);
}
.icon-btn[aria-pressed="true"] svg { fill: var(--neg); stroke: var(--neg); }
.icon-btn.fav-pop svg { animation: favpop 0.35s ease; }
@keyframes favpop {
0% { transform: scale(1); }
40% { transform: scale(1.45); }
100% { transform: scale(1); }
}
.views { margin-left: auto; font-size: 0.82rem; }
/* ---------- chain details card ---------- */
.chain-card { margin-top: 18px; padding: 18px 20px; }
.panel-h {
margin: 0 0 12px;
font-size: 0.92rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted);
}
.kv { margin: 0; display: grid; gap: 9px; }
.kv > div {
display: flex;
justify-content: space-between;
gap: 16px;
font-size: 0.88rem;
}
.kv dt { color: var(--muted); }
.kv dd { margin: 0; text-align: right; }
.kv-wide { gap: 12px; }
/* ---------- right column ---------- */
.info-col { min-width: 0; }
.collection-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.collection-link {
display: inline-flex;
align-items: center;
gap: 9px;
font-weight: 600;
color: var(--accent-2);
}
.collection-link:hover { text-decoration: underline; text-underline-offset: 3px; }
.collection-avatar {
width: 26px; height: 26px;
border-radius: 50%;
background: conic-gradient(from 120deg, var(--accent-2), var(--accent), #ff7ad9, var(--accent-2));
box-shadow: 0 0 10px rgba(0, 224, 198, 0.4);
}
.verified { color: var(--accent-2); flex-shrink: 0; }
.floor-pill {
font-size: 0.78rem;
color: var(--muted);
border: 1px solid var(--line);
border-radius: var(--r-pill);
padding: 5px 12px;
background: var(--surface);
}
.token-title {
margin: 10px 0 12px;
font-size: clamp(1.7rem, 3.4vw, 2.4rem);
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.15;
}
.token-id {
background: linear-gradient(90deg, var(--accent), var(--accent-2));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: 700;
}
.owner-row {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 22px;
}
.owner-chip {
display: inline-flex;
align-items: center;
gap: 9px;
padding: 7px 14px 7px 8px;
border-radius: var(--r-pill);
border: 1px solid var(--line);
background: var(--surface);
font-size: 0.85rem;
color: var(--muted);
}
.owner-chip a { color: var(--accent-2); font-size: 0.82rem; }
.owner-chip a:hover { text-decoration: underline; }
.owner-avatar {
width: 22px; height: 22px;
border-radius: 50%;
background: linear-gradient(135deg, #ff7ad9, var(--accent));
}
.rank-pill {
font-size: 0.8rem;
color: var(--warn);
border: 1px solid rgba(255, 179, 71, 0.35);
background: rgba(255, 179, 71, 0.08);
border-radius: var(--r-pill);
padding: 6px 13px;
}
/* ---------- price panel ---------- */
.price-panel {
position: relative;
padding: 22px 24px;
overflow: hidden;
}
.price-panel::before {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
padding: 1px;
background: linear-gradient(140deg, var(--accent), transparent 35%, transparent 65%, var(--accent-2));
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
.auction-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
font-size: 0.85rem;
color: var(--muted);
padding-bottom: 16px;
border-bottom: 1px solid var(--line);
}
.live-dot {
width: 9px; height: 9px;
border-radius: 50%;
background: var(--neg);
box-shadow: 0 0 0 0 rgba(255, 77, 109, 0.6);
animation: livepulse 1.6s ease-out infinite;
}
@keyframes livepulse {
0% { box-shadow: 0 0 0 0 rgba(255, 77, 109, 0.55); }
100% { box-shadow: 0 0 0 9px rgba(255, 77, 109, 0); }
}
.countdown { display: flex; align-items: baseline; gap: 4px; margin-left: auto; }
.cd-cell {
display: inline-flex;
flex-direction: column;
align-items: center;
min-width: 46px;
padding: 6px 8px 4px;
border-radius: var(--r-sm);
background: var(--surface);
border: 1px solid var(--line);
}
.cd-cell b { font-size: 1.05rem; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }
.cd-cell i { font-style: normal; font-size: 0.6rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }
.cd-sep { color: var(--muted); font-weight: 700; align-self: center; }
.countdown.is-urgent .cd-cell { border-color: rgba(255, 77, 109, 0.5); }
.countdown.is-urgent .cd-cell b { color: var(--neg); }
.price-row {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 20px;
padding: 18px 0;
flex-wrap: wrap;
}
.price-main .muted, .price-side .muted { font-size: 0.78rem; letter-spacing: 0.05em; text-transform: uppercase; }
.price-big {
font-size: clamp(1.9rem, 4vw, 2.6rem);
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.1;
}
.price-big .ticker {
font-size: 0.95rem;
font-weight: 600;
color: var(--accent-2);
letter-spacing: 0.06em;
}
.price-fiat { color: var(--muted); font-size: 0.85rem; }
.price-side { text-align: right; }
.offer-top { font-size: 1.1rem; font-weight: 700; color: var(--text); }
.cta-row { display: flex; gap: 12px; flex-wrap: wrap; }
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 13px 24px;
border-radius: var(--r-md);
font-weight: 600;
font-size: 0.95rem;
transition: transform 0.15s, box-shadow 0.15s, background 0.15s, border-color 0.15s;
}
.btn:active { transform: translateY(1px) scale(0.99); }
.btn-primary {
flex: 1;
min-width: 200px;
color: #fff;
background: linear-gradient(135deg, var(--accent), #5a3df0);
box-shadow: 0 8px 26px var(--accent-glow);
}
.btn-primary:hover { box-shadow: 0 10px 34px var(--accent-glow), 0 0 0 1px rgba(255, 255, 255, 0.14) inset; transform: translateY(-1px); }
.btn-primary:disabled { opacity: 0.65; cursor: progress; transform: none; }
.btn-ghost {
border: 1px solid var(--line-2);
background: rgba(255, 255, 255, 0.03);
color: var(--text);
}
.btn-ghost:hover { border-color: var(--accent-2); color: var(--accent-2); }
.fee-note { margin: 14px 0 0; font-size: 0.78rem; }
/* ---------- traits ---------- */
.traits-section { margin-top: 26px; }
.traits-grid {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px;
}
.trait-card {
position: relative;
padding: 12px 14px;
border-radius: var(--r-md);
border: 1px solid var(--line);
background: var(--surface);
transition: transform 0.15s, border-color 0.15s, box-shadow 0.15s;
cursor: default;
}
.trait-card:hover, .trait-card:focus-visible {
transform: translateY(-2px);
border-color: rgba(124, 92, 255, 0.55);
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.4), 0 0 16px rgba(124, 92, 255, 0.15);
}
.trait-type {
display: block;
font-size: 0.68rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--accent-2);
margin-bottom: 3px;
}
.trait-value { display: block; font-weight: 600; font-size: 0.92rem; }
.trait-rarity {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
font-size: 0.72rem;
color: var(--muted);
}
.trait-bar {
flex: 1;
height: 4px;
border-radius: var(--r-pill);
background: rgba(255, 255, 255, 0.07);
overflow: hidden;
}
.trait-bar i {
display: block;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, var(--accent), var(--accent-2));
}
.trait-card.is-legendary { border-color: rgba(255, 179, 71, 0.45); }
.trait-card.is-legendary .trait-type { color: var(--warn); }
.trait-tooltip {
position: fixed;
z-index: 90;
max-width: 240px;
padding: 9px 12px;
border-radius: var(--r-sm);
background: var(--elevated);
border: 1px solid var(--line-2);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.5);
font-size: 0.74rem;
color: var(--text);
pointer-events: none;
}
/* ---------- tabs ---------- */
.tabs-section { margin-top: 26px; padding: 8px 20px 20px; }
.tabs {
display: flex;
gap: 4px;
border-bottom: 1px solid var(--line);
margin-bottom: 18px;
overflow-x: auto;
}
.tab {
position: relative;
padding: 13px 16px;
color: var(--muted);
font-weight: 600;
font-size: 0.92rem;
white-space: nowrap;
transition: color 0.15s;
}
.tab:hover { color: var(--text); }
.tab.is-active { color: var(--text); }
.tab.is-active::after {
content: "";
position: absolute;
left: 12px; right: 12px; bottom: -1px;
height: 2px;
border-radius: var(--r-pill);
background: linear-gradient(90deg, var(--accent), var(--accent-2));
box-shadow: 0 0 10px var(--accent-glow);
}
.tab-badge {
font-size: 0.7rem;
padding: 2px 7px;
border-radius: var(--r-pill);
background: rgba(124, 92, 255, 0.2);
color: #b7a6ff;
margin-left: 4px;
}
.tab-panel { animation: panelin 0.25s ease; }
@keyframes panelin {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
/* ---------- tables ---------- */
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 0.86rem;
}
.data-table th {
text-align: left;
font-size: 0.7rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
padding: 8px 10px;
border-bottom: 1px solid var(--line);
}
.data-table td { padding: 11px 10px; border-bottom: 1px solid var(--line); }
.data-table tbody tr:last-child td { border-bottom: none; }
.data-table tbody tr { transition: background 0.12s; }
.data-table tbody tr:hover { background: rgba(255, 255, 255, 0.025); }
.event-pill {
display: inline-block;
padding: 3px 10px;
border-radius: var(--r-pill);
font-size: 0.72rem;
font-weight: 600;
}
.ev-sale { background: rgba(38, 208, 124, 0.14); color: var(--pos); }
.ev-list { background: rgba(124, 92, 255, 0.16); color: #b7a6ff; }
.ev-xfer { background: rgba(255, 255, 255, 0.07); color: var(--muted); }
.ev-mint { background: rgba(0, 224, 198, 0.12); color: var(--accent-2); }
/* ---------- chart ---------- */
.chart-wrap { margin-bottom: 20px; }
.chart-head {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.85rem;
color: var(--muted);
margin-bottom: 8px;
}
#priceChart {
display: block;
width: 100%;
height: 150px;
border-radius: var(--r-md);
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--line);
}
.chart-axis {
display: flex;
justify-content: space-between;
font-size: 0.68rem;
margin-top: 6px;
padding: 0 4px;
}
/* ---------- confirm sheet ---------- */
.sheet-backdrop {
position: fixed;
inset: 0;
z-index: 60;
background: rgba(6, 7, 10, 0.7);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
animation: fadein 0.2s ease;
}
@keyframes fadein { from { opacity: 0; } to { opacity: 1; } }
.sheet {
position: fixed;
z-index: 70;
left: 50%;
bottom: 0;
transform: translateX(-50%);
width: min(520px, 100%);
max-height: 92vh;
overflow-y: auto;
padding: 14px 26px 26px;
border-radius: var(--r-lg) var(--r-lg) 0 0;
border-bottom: none;
box-shadow: 0 -20px 60px rgba(0, 0, 0, 0.6);
animation: sheetup 0.3s cubic-bezier(0.2, 0.9, 0.3, 1);
}
@keyframes sheetup {
from { transform: translate(-50%, 40px); opacity: 0; }
to { transform: translate(-50%, 0); opacity: 1; }
}
.sheet-grab {
width: 44px; height: 4px;
border-radius: var(--r-pill);
background: var(--line-2);
margin: 0 auto 16px;
}
.sheet h2 { margin: 0 0 16px; font-size: 1.2rem; }
.sheet-item {
display: flex;
align-items: center;
gap: 14px;
padding: 14px;
border-radius: var(--r-md);
background: var(--surface);
border: 1px solid var(--line);
margin-bottom: 16px;
}
.sheet-item > div { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.sheet-item strong { font-size: 0.95rem; }
.sheet-price { margin-left: auto; font-weight: 700; color: var(--accent-2); white-space: nowrap; }
.sheet-thumb {
width: 46px; height: 46px;
flex-shrink: 0;
border-radius: var(--r-sm);
background:
radial-gradient(circle at 35% 30%, #c9b8ff, var(--accent) 50%, #16113a),
#0c0e16;
box-shadow: 0 0 14px var(--accent-glow);
}
.offer-field { display: grid; gap: 6px; margin-bottom: 16px; }
.offer-field label { font-size: 0.82rem; color: var(--muted); }
.offer-field input {
width: 100%;
padding: 12px 14px;
border-radius: var(--r-md);
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--text);
font-size: 1rem;
}
.offer-field input:focus-visible { outline: 2px solid var(--accent); outline-offset: 0; border-color: var(--accent); }
.sheet-kv {
padding: 14px;
border-radius: var(--r-md);
background: rgba(255, 255, 255, 0.025);
border: 1px solid var(--line);
margin-bottom: 16px;
}
.kv-total { padding-top: 9px; border-top: 1px solid var(--line); font-weight: 700; }
.kv-total dd { color: var(--accent-2); }
.risk-note {
display: flex;
gap: 10px;
align-items: flex-start;
padding: 12px 14px;
border-radius: var(--r-md);
background: rgba(255, 179, 71, 0.08);
border: 1px solid rgba(255, 179, 71, 0.32);
color: var(--warn);
font-size: 0.8rem;
line-height: 1.45;
margin-bottom: 18px;
}
.risk-note svg { flex-shrink: 0; margin-top: 2px; }
.sheet-actions { display: flex; gap: 12px; }
.sheet-actions .btn-ghost { flex: 1; }
.sheet-actions .btn-primary { flex: 2; min-width: 0; }
.btn-spinner {
width: 16px; height: 16px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.35);
border-top-color: #fff;
animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ---------- toasts ---------- */
.toast-stack {
position: fixed;
bottom: 22px;
right: 22px;
z-index: 110;
display: flex;
flex-direction: column;
gap: 10px;
}
.toast {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 18px;
border-radius: var(--r-md);
background: var(--elevated);
border: 1px solid var(--line-2);
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.5);
font-size: 0.88rem;
animation: toastin 0.3s cubic-bezier(0.2, 0.9, 0.3, 1);
}
.toast.is-success { border-color: rgba(38, 208, 124, 0.45); }
.toast.is-success::before { content: "✓"; color: var(--pos); font-weight: 700; }
.toast.is-leaving { opacity: 0; transform: translateY(8px); transition: opacity 0.3s, transform 0.3s; }
@keyframes toastin {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
/* number flash on price update */
.flash-pos { animation: flashpos 0.8s ease; }
@keyframes flashpos {
0% { color: var(--pos); text-shadow: 0 0 14px rgba(38, 208, 124, 0.6); }
100% { color: inherit; text-shadow: none; }
}
/* ---------- responsive ---------- */
@media (max-width: 980px) {
.layout { grid-template-columns: 1fr; gap: 26px; }
.art-col { max-width: 560px; }
.topbar-nav { display: none; }
}
@media (max-width: 520px) {
.topbar { padding: 12px 16px; gap: 12px; }
.wallet-bal { display: none; }
.crumbs, .layout { padding-left: 16px; padding-right: 16px; }
.layout { padding-top: 16px; }
.token-title { font-size: 1.55rem; }
.price-panel { padding: 18px 16px; }
.auction-row { font-size: 0.78rem; }
.countdown { margin-left: 0; width: 100%; justify-content: flex-start; }
.cd-cell { min-width: 40px; }
.price-side { text-align: left; }
.cta-row { flex-direction: column; }
.btn-primary { min-width: 0; }
.traits-grid { grid-template-columns: repeat(2, 1fr); }
.tabs-section { padding: 4px 12px 14px; }
.data-table { font-size: 0.78rem; }
.data-table th, .data-table td { padding: 8px 6px; }
.sheet { padding: 12px 18px 20px; }
.views { display: none; }
.toast-stack { left: 16px; right: 16px; }
.toast { justify-content: center; }
}/* Web3 — NFT Detail (traits · history · buy)
UI-only simulation. No wallet, RPC, or on-chain calls — all data is mocked. */
(() => {
"use strict";
const $ = (sel, root = document) => root.querySelector(sel);
const NOVA_USD = 179.0; // mock spot price for fiat estimates
const BUY_PRICE = 8.45;
const MARKET_FEE = 0.025;
const ROYALTY = 0.05;
const GAS = 0.0021;
/* ---------------------------------- toast ---------------------------------- */
const toastStack = $("#toastStack");
function toast(msg, type = "default") {
const el = document.createElement("div");
el.className = "toast" + (type === "success" ? " is-success" : "");
el.textContent = msg;
toastStack.appendChild(el);
setTimeout(() => {
el.classList.add("is-leaving");
setTimeout(() => el.remove(), 320);
}, 3200);
}
/* ---------------------------------- traits ---------------------------------- */
const TRAITS = [
{ type: "Background", value: "Deep Void", rarity: 11.2, floor: "6.2 NOVA", count: 995 },
{ type: "Core", value: "Violet Nebula", rarity: 6.4, floor: "7.1 NOVA", count: 569 },
{ type: "Ring System", value: "Twin Halo", rarity: 3.1, floor: "9.8 NOVA", count: 276 },
{ type: "Comet Trail", value: "Teal Streak", rarity: 4.8, floor: "8.4 NOVA", count: 427 },
{ type: "Star Field", value: "Dense Cluster", rarity: 9.7, floor: "6.6 NOVA", count: 862 },
{ type: "Horizon Grid", value: "Cyan Mesh", rarity: 7.5, floor: "6.9 NOVA", count: 667 },
{ type: "Aura", value: "Genesis Glow", rarity: 0.9, floor: "21.0 NOVA", count: 80, legendary: true },
];
const traitsGrid = $("#traitsGrid");
const tooltip = $("#traitTooltip");
TRAITS.forEach((t) => {
const li = document.createElement("li");
const btn = document.createElement("button");
btn.type = "button";
btn.className = "trait-card" + (t.legendary ? " is-legendary" : "");
btn.setAttribute(
"aria-label",
`${t.type}: ${t.value}, ${t.rarity}% of tokens have this trait. Trait floor ${t.floor}.`
);
btn.innerHTML = `
<span class="trait-type">${t.type}</span>
<span class="trait-value">${t.value}</span>
<span class="trait-rarity">
<span class="trait-bar"><i style="width:${Math.max(t.rarity * 4, 4)}%"></i></span>
<span class="mono">${t.rarity}%</span>
</span>`;
btn.dataset.tip = `${t.count} of 8,888 (${t.rarity}%) · floor ${t.floor}`;
li.appendChild(btn);
traitsGrid.appendChild(li);
});
function showTooltip(target, x, y) {
tooltip.textContent = target.dataset.tip;
tooltip.hidden = false;
const pad = 14;
const rect = tooltip.getBoundingClientRect();
let left = x + pad;
let top = y + pad;
if (left + rect.width > window.innerWidth - 8) left = x - rect.width - pad;
if (top + rect.height > window.innerHeight - 8) top = y - rect.height - pad;
tooltip.style.left = `${Math.max(8, left)}px`;
tooltip.style.top = `${Math.max(8, top)}px`;
}
traitsGrid.addEventListener("mousemove", (e) => {
const card = e.target.closest(".trait-card");
if (card) showTooltip(card, e.clientX, e.clientY);
else tooltip.hidden = true;
});
traitsGrid.addEventListener("mouseleave", () => (tooltip.hidden = true));
traitsGrid.addEventListener("focusin", (e) => {
const card = e.target.closest(".trait-card");
if (!card) return;
const r = card.getBoundingClientRect();
showTooltip(card, r.left + r.width / 2, r.bottom);
});
traitsGrid.addEventListener("focusout", () => (tooltip.hidden = true));
/* ---------------------------------- countdown ---------------------------------- */
const cdH = $("#cdH");
const cdM = $("#cdM");
const cdS = $("#cdS");
const countdown = $("#countdown");
let remaining = 2 * 3600 + 14 * 60 + 9; // 02:14:09
const pad2 = (n) => String(n).padStart(2, "0");
function tick() {
remaining -= 1;
if (remaining <= 0) {
// anti-snipe style extension keeps the demo alive
remaining = 10 * 60;
toast("Auction extended by 10 minutes (anti-snipe)");
}
cdH.textContent = pad2(Math.floor(remaining / 3600));
cdM.textContent = pad2(Math.floor((remaining % 3600) / 60));
cdS.textContent = pad2(remaining % 60);
countdown.classList.toggle("is-urgent", remaining < 15 * 60);
}
tick();
setInterval(tick, 1000);
/* ---------------------------------- tabs ---------------------------------- */
const tabs = Array.from(document.querySelectorAll('[role="tab"]'));
function selectTab(tab) {
tabs.forEach((t) => {
const active = t === tab;
t.classList.toggle("is-active", active);
t.setAttribute("aria-selected", String(active));
t.tabIndex = active ? 0 : -1;
document.getElementById(t.getAttribute("aria-controls")).hidden = !active;
});
}
tabs.forEach((tab, i) => {
tab.addEventListener("click", () => selectTab(tab));
tab.addEventListener("keydown", (e) => {
let next = null;
if (e.key === "ArrowRight") next = tabs[(i + 1) % tabs.length];
if (e.key === "ArrowLeft") next = tabs[(i - 1 + tabs.length) % tabs.length];
if (e.key === "Home") next = tabs[0];
if (e.key === "End") next = tabs[tabs.length - 1];
if (next) {
e.preventDefault();
selectTab(next);
next.focus();
}
});
});
/* ---------------------------------- price chart ---------------------------------- */
const SALES = [3.2, 3.6, 3.4, 4.25, 4.9, 5.4, 5.1, 6.1, 6.8, 7.4, 8.45];
function renderChart() {
const svg = $("#priceChart");
const W = 560;
const H = 160;
const padX = 14;
const padY = 18;
const min = Math.min(...SALES);
const max = Math.max(...SALES);
const x = (i) => padX + (i / (SALES.length - 1)) * (W - padX * 2);
const y = (v) => H - padY - ((v - min) / (max - min)) * (H - padY * 2);
const pts = SALES.map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`);
const ns = "http://www.w3.org/2000/svg";
const defs = document.createElementNS(ns, "defs");
defs.innerHTML = `
<linearGradient id="lineGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#7c5cff"/><stop offset="1" stop-color="#00e0c6"/>
</linearGradient>
<linearGradient id="fillGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="rgba(124,92,255,0.30)"/>
<stop offset="1" stop-color="rgba(124,92,255,0)"/>
</linearGradient>`;
svg.appendChild(defs);
const area = document.createElementNS(ns, "path");
area.setAttribute(
"d",
`M${pts[0]} L${pts.slice(1).join(" L")} L${x(SALES.length - 1)},${H} L${x(0)},${H} Z`
);
area.setAttribute("fill", "url(#fillGrad)");
svg.appendChild(area);
const line = document.createElementNS(ns, "path");
line.setAttribute("d", `M${pts[0]} L${pts.slice(1).join(" L")}`);
line.setAttribute("fill", "none");
line.setAttribute("stroke", "url(#lineGrad)");
line.setAttribute("stroke-width", "2.5");
line.setAttribute("stroke-linecap", "round");
line.setAttribute("stroke-linejoin", "round");
svg.appendChild(line);
const dot = document.createElementNS(ns, "circle");
dot.setAttribute("cx", x(SALES.length - 1));
dot.setAttribute("cy", y(SALES[SALES.length - 1]));
dot.setAttribute("r", "4");
dot.setAttribute("fill", "#0a0b0f");
dot.setAttribute("stroke", "#00e0c6");
dot.setAttribute("stroke-width", "2");
svg.appendChild(dot);
// draw-in animation
const len = line.getTotalLength();
line.style.strokeDasharray = len;
line.style.strokeDashoffset = len;
line.getBoundingClientRect(); // force layout
line.style.transition = "stroke-dashoffset 1.1s ease";
line.style.strokeDashoffset = "0";
}
renderChart();
/* ---------------------------------- favorites ---------------------------------- */
const favBtn = $("#favBtn");
const favCount = $("#favCount");
let favs = 312;
let faved = false;
favBtn.addEventListener("click", () => {
faved = !faved;
favs += faved ? 1 : -1;
favCount.textContent = favs.toLocaleString("en-US");
favBtn.setAttribute("aria-pressed", String(faved));
favBtn.setAttribute("aria-label", faved ? "Remove from favorites" : "Add to favorites");
favBtn.classList.remove("fav-pop");
void favBtn.offsetWidth; // restart animation
favBtn.classList.add("fav-pop");
if (faved) toast("Added to your favorites", "success");
});
/* ---------------------------------- share / refresh ---------------------------------- */
$("#shareBtn").addEventListener("click", async () => {
const url = "https://lumen.market/nebula-drifters/2481";
try {
await navigator.clipboard.writeText(url);
toast("Link copied to clipboard", "success");
} catch {
toast(`Share link: ${url}`);
}
});
$("#refreshBtn").addEventListener("click", () => {
toast("Metadata refresh queued — already frozen on-chain");
});
$("#walletChip").addEventListener("click", () => {
toast("Wallet 0x7a3f…c41d connected · 14.82 NOVA (simulated)");
});
/* ---------------------------------- fullscreen artwork ---------------------------------- */
const artFrame = $("#artFrame");
const zoomBtn = $("#zoomBtn");
function setFullscreen(on) {
artFrame.classList.toggle("is-fullscreen", on);
zoomBtn.setAttribute("aria-label", on ? "Exit fullscreen view" : "Toggle fullscreen view");
document.body.style.overflow = on ? "hidden" : "";
}
zoomBtn.addEventListener("click", () => setFullscreen(!artFrame.classList.contains("is-fullscreen")));
artFrame.addEventListener("click", (e) => {
if (artFrame.classList.contains("is-fullscreen") && e.target === artFrame) setFullscreen(false);
});
/* ---------------------------------- confirm sheet (buy / offer) ---------------------------------- */
const backdrop = $("#sheetBackdrop");
const sheet = $("#confirmSheet");
const sheetTitle = $("#sheetTitle");
const sheetPrice = $("#sheetPrice");
const offerField = $("#offerField");
const offerInput = $("#offerInput");
const rowItem = $("#rowItem");
const rowFee = $("#rowFee");
const rowRoyalty = $("#rowRoyalty");
const rowTotal = $("#rowTotal");
const cancelBtn = $("#sheetCancel");
const confirmBtn = $("#sheetConfirm");
const confirmLabel = confirmBtn.querySelector(".btn-label");
const spinner = confirmBtn.querySelector(".btn-spinner");
let mode = "buy"; // "buy" | "offer"
let lastFocus = null;
let signing = false;
const fmt = (n) => `${n.toFixed(4)} NOVA`;
function updateBreakdown() {
const base = mode === "buy" ? BUY_PRICE : Math.max(parseFloat(offerInput.value) || 0, 0);
const fee = base * MARKET_FEE;
const royalty = base * ROYALTY;
rowItem.textContent = fmt(base);
rowFee.textContent = fmt(fee);
rowRoyalty.textContent = fmt(royalty);
rowTotal.textContent = fmt(base + fee + royalty + GAS);
sheetPrice.textContent = `${base.toFixed(2)} NOVA`;
}
function openSheet(which) {
mode = which;
lastFocus = document.activeElement;
sheetTitle.textContent = mode === "buy" ? "Confirm purchase" : "Make an offer";
confirmLabel.textContent = mode === "buy" ? "Approve in wallet" : "Sign offer";
offerField.hidden = mode === "buy";
updateBreakdown();
backdrop.hidden = false;
sheet.hidden = false;
(mode === "offer" ? offerInput : confirmBtn).focus();
}
function closeSheet() {
if (signing) return;
backdrop.hidden = true;
sheet.hidden = true;
if (lastFocus) lastFocus.focus();
}
$("#buyBtn").addEventListener("click", () => openSheet("buy"));
$("#offerBtn").addEventListener("click", () => openSheet("offer"));
cancelBtn.addEventListener("click", closeSheet);
backdrop.addEventListener("click", closeSheet);
offerInput.addEventListener("input", updateBreakdown);
document.addEventListener("keydown", (e) => {
if (e.key !== "Escape") return;
if (!sheet.hidden) closeSheet();
else if (artFrame.classList.contains("is-fullscreen")) setFullscreen(false);
});
// rudimentary focus trap inside the sheet
sheet.addEventListener("keydown", (e) => {
if (e.key !== "Tab") return;
const focusables = sheet.querySelectorAll("button, input");
const first = focusables[0];
const last = focusables[focusables.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
});
confirmBtn.addEventListener("click", () => {
if (signing) return;
if (mode === "offer") {
const v = parseFloat(offerInput.value);
if (!v || v <= 0) {
toast("Enter an offer above 0 NOVA");
offerInput.focus();
return;
}
}
signing = true;
confirmBtn.disabled = true;
cancelBtn.disabled = true;
confirmLabel.textContent = mode === "buy" ? "Awaiting signature…" : "Signing offer…";
spinner.hidden = false;
// simulated wallet signature + confirmation
setTimeout(() => {
signing = false;
confirmBtn.disabled = false;
cancelBtn.disabled = false;
spinner.hidden = true;
confirmLabel.textContent = mode === "buy" ? "Approve in wallet" : "Sign offer";
closeSheet();
if (mode === "buy") {
toast("Purchase confirmed · tx 0x8c2f…a90e (simulated)", "success");
} else {
toast(`Offer of ${parseFloat(offerInput.value).toFixed(2)} NOVA signed (simulated)`, "success");
}
}, 1800);
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Web3 — NFT Detail · Nebula Drifters #2481</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>
<header class="topbar">
<a class="brand" href="#" aria-label="Lumen Market home">
<span class="brand-mark" aria-hidden="true"></span>
Lumen<span class="brand-dim">Market</span>
</a>
<nav class="topbar-nav" aria-label="Primary">
<a href="#" class="topnav-link">Explore</a>
<a href="#" class="topnav-link is-active" aria-current="page">Collections</a>
<a href="#" class="topnav-link">Activity</a>
</nav>
<button class="wallet-chip" type="button" id="walletChip" aria-label="Connected wallet 0x7a3f…c41d, balance 14.82 NOVA">
<span class="wallet-dot" aria-hidden="true"></span>
<span class="mono">0x7a3f…c41d</span>
<span class="wallet-bal mono">14.82 NOVA</span>
</button>
</header>
<nav class="crumbs" aria-label="Breadcrumb">
<a href="#">Collections</a>
<span aria-hidden="true">/</span>
<a href="#">Nebula Drifters</a>
<span aria-hidden="true">/</span>
<span aria-current="page" class="mono">#2481</span>
</nav>
<main class="layout">
<!-- ============ LEFT: ARTWORK ============ -->
<section class="art-col" aria-label="Artwork">
<figure class="art-frame" id="artFrame">
<div class="art-canvas" id="artCanvas" role="img" aria-label="Generative artwork: a violet-teal nebula orb drifting over a star grid">
<div class="art-stars" aria-hidden="true"></div>
<div class="art-grid" aria-hidden="true"></div>
<div class="art-orb" aria-hidden="true">
<div class="art-orb-core"></div>
<div class="art-orb-ring r1"></div>
<div class="art-orb-ring r2"></div>
</div>
<div class="art-comet" aria-hidden="true"></div>
<div class="art-sig mono" aria-hidden="true">NBD·2481</div>
</div>
<button class="art-zoom" type="button" id="zoomBtn" aria-label="Toggle fullscreen view">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<path d="M9 3H3v6M15 3h6v6M9 21H3v-6M15 21h6v-6"/>
</svg>
</button>
<figcaption class="sr-only">Nebula Drifters #2481 — on-chain generative artwork (simulated)</figcaption>
</figure>
<div class="art-actions">
<button class="icon-btn" type="button" id="favBtn" aria-pressed="false" aria-label="Add to favorites">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true">
<path d="M12 21s-7.5-4.7-9.7-9.2C.7 8.4 2.7 4.8 6.2 4.8c2 0 3.6 1.1 4.5 2.7.2.4.4.8.5 1.2h1.6c.1-.4.3-.8.5-1.2.9-1.6 2.5-2.7 4.5-2.7 3.5 0 5.5 3.6 3.9 7C19.5 16.3 12 21 12 21z"/>
</svg>
<span id="favCount" class="mono">312</span>
</button>
<button class="icon-btn" type="button" id="shareBtn">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<circle cx="6" cy="12" r="3"/><circle cx="18" cy="6" r="3"/><circle cx="18" cy="18" r="3"/>
<path d="M8.7 10.7l6.6-3.4M8.7 13.3l6.6 3.4"/>
</svg>
Share
</button>
<button class="icon-btn" type="button" id="refreshBtn">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<path d="M21 12a9 9 0 1 1-2.6-6.4M21 3v6h-6"/>
</svg>
Refresh
</button>
<span class="views muted"><span class="mono">4,207</span> views</span>
</div>
<div class="chain-card glass">
<h2 class="panel-h">Details</h2>
<dl class="kv">
<div><dt>Contract</dt><dd class="mono link">0x9bC4…e80a</dd></div>
<div><dt>Token ID</dt><dd class="mono">2481</dd></div>
<div><dt>Standard</dt><dd class="mono">LRC-721</dd></div>
<div><dt>Chain</dt><dd>Lumen Chain</dd></div>
<div><dt>Royalty</dt><dd class="mono">5%</dd></div>
<div><dt>Metadata</dt><dd>Frozen · on-chain</dd></div>
</dl>
</div>
</section>
<!-- ============ RIGHT: DETAILS ============ -->
<section class="info-col" aria-label="Token details">
<div class="collection-row">
<a class="collection-link" href="#">
<span class="collection-avatar" aria-hidden="true"></span>
Nebula Drifters
<svg class="verified" viewBox="0 0 24 24" width="16" height="16" aria-label="Verified collection" role="img">
<path fill="currentColor" d="M12 1.6l2.4 2 3.1-.3 1 3 2.8 1.4-.7 3.1 2 2.4-2 2.4.7 3.1-2.8 1.4-1 3-3.1-.3-2.4 2-2.4-2-3.1.3-1-3-2.8-1.4.7-3.1-2-2.4 2-2.4-.7-3.1L5.5 6.3l1-3 3.1.3z"/>
<path fill="#0a0b0f" d="M10.6 15.4l-2.8-2.8 1.3-1.3 1.5 1.5 4.3-4.3 1.3 1.3z"/>
</svg>
</a>
<span class="floor-pill mono">Floor 7.90 NOVA</span>
</div>
<h1 class="token-title">Nebula Drifter <span class="token-id mono">#2481</span></h1>
<div class="owner-row">
<span class="owner-chip">
<span class="owner-avatar" aria-hidden="true"></span>
Owned by <a href="#" class="mono">0x51ee…9b2c</a>
</span>
<span class="rank-pill">Rarity rank <strong class="mono">#86</strong> / 8,888</span>
</div>
<!-- Price / auction panel -->
<div class="price-panel glass-strong" aria-labelledby="priceH">
<div class="auction-row">
<span class="live-dot" aria-hidden="true"></span>
<span id="priceH">Live auction ends in</span>
<div class="countdown mono" id="countdown" role="timer" aria-live="off" aria-label="Auction countdown">
<span class="cd-cell"><b id="cdH">02</b><i>hrs</i></span>
<span class="cd-sep">:</span>
<span class="cd-cell"><b id="cdM">14</b><i>min</i></span>
<span class="cd-sep">:</span>
<span class="cd-cell"><b id="cdS">09</b><i>sec</i></span>
</div>
</div>
<div class="price-row">
<div class="price-main">
<span class="muted">Current price</span>
<div class="price-big"><span class="mono" id="priceNova">8.45</span> <span class="ticker">NOVA</span></div>
<span class="price-fiat mono" id="priceFiat">≈ $1,512.55</span>
</div>
<div class="price-side">
<span class="muted">Top offer</span>
<div class="offer-top mono">7.80 NOVA</div>
<span class="muted small">by <span class="mono">0xfe12…0a77</span></span>
</div>
</div>
<div class="cta-row">
<button class="btn btn-primary" type="button" id="buyBtn">Buy now · <span class="mono">8.45 NOVA</span></button>
<button class="btn btn-ghost" type="button" id="offerBtn">Make offer</button>
</div>
<p class="fee-note muted">Includes 2.5% marketplace fee + 5% creator royalty · est. gas <span class="mono">0.0021 NOVA</span></p>
</div>
<!-- Traits -->
<section class="traits-section" aria-labelledby="traitsH">
<h2 class="panel-h" id="traitsH">Traits <span class="muted">(7)</span></h2>
<ul class="traits-grid" id="traitsGrid">
<!-- populated by script.js -->
</ul>
</section>
<!-- Tabs -->
<section class="tabs-section glass" aria-label="Token history">
<div class="tabs" role="tablist" aria-label="Properties, offers and activity">
<button class="tab is-active" role="tab" id="tab-properties" aria-selected="true" aria-controls="panel-properties" type="button">Properties</button>
<button class="tab" role="tab" id="tab-offers" aria-selected="false" aria-controls="panel-offers" tabindex="-1" type="button">Offers <span class="tab-badge mono">4</span></button>
<button class="tab" role="tab" id="tab-activity" aria-selected="false" aria-controls="panel-activity" tabindex="-1" type="button">Activity</button>
</div>
<div class="tab-panel" id="panel-properties" role="tabpanel" aria-labelledby="tab-properties">
<dl class="kv kv-wide">
<div><dt>Generation</dt><dd>Genesis · Series 1</dd></div>
<div><dt>Minted</dt><dd class="mono">2025-11-03 14:22 UTC</dd></div>
<div><dt>Mint price</dt><dd class="mono">0.80 NOVA</dd></div>
<div><dt>Last sale</dt><dd class="mono">6.10 NOVA <span class="pos">(+662% vs mint)</span></dd></div>
<div><dt>Holders of collection</dt><dd class="mono">3,941</dd></div>
<div><dt>Unique trait score</dt><dd class="mono">412.6</dd></div>
</dl>
</div>
<div class="tab-panel" id="panel-offers" role="tabpanel" aria-labelledby="tab-offers" hidden>
<table class="data-table">
<caption class="sr-only">Open offers on this token</caption>
<thead><tr><th scope="col">Price</th><th scope="col">USD</th><th scope="col">From</th><th scope="col">Expires</th></tr></thead>
<tbody>
<tr><td class="mono">7.80 NOVA</td><td class="mono muted">$1,396</td><td class="mono link">0xfe12…0a77</td><td class="muted">in 22h</td></tr>
<tr><td class="mono">7.55 NOVA</td><td class="mono muted">$1,351</td><td class="mono link">0x33da…b1c9</td><td class="muted">in 2d</td></tr>
<tr><td class="mono">7.21 NOVA</td><td class="mono muted">$1,290</td><td class="mono link">0x0bb8…44f1</td><td class="muted">in 5d</td></tr>
<tr><td class="mono">6.90 NOVA</td><td class="mono muted">$1,235</td><td class="mono link">0x9a07…d3e5</td><td class="muted">in 6d</td></tr>
</tbody>
</table>
</div>
<div class="tab-panel" id="panel-activity" role="tabpanel" aria-labelledby="tab-activity" hidden>
<div class="chart-wrap">
<div class="chart-head">
<span>Price history</span>
<span class="pos mono">+38.5% · 90d</span>
</div>
<svg id="priceChart" viewBox="0 0 560 160" preserveAspectRatio="none" role="img" aria-label="Price history line chart, rising from 3.2 to 8.45 NOVA over 90 days"></svg>
<div class="chart-axis mono muted"><span>Mar</span><span>Apr</span><span>May</span><span>Jun</span></div>
</div>
<table class="data-table">
<caption class="sr-only">Sales and transfer activity</caption>
<thead><tr><th scope="col">Event</th><th scope="col">Price</th><th scope="col">From</th><th scope="col">To</th><th scope="col">Date</th></tr></thead>
<tbody>
<tr><td><span class="event-pill ev-sale">Sale</span></td><td class="mono">6.10 NOVA</td><td class="mono link">0xab3c…77e0</td><td class="mono link">0x51ee…9b2c</td><td class="muted">May 18</td></tr>
<tr><td><span class="event-pill ev-list">List</span></td><td class="mono">6.50 NOVA</td><td class="mono link">0xab3c…77e0</td><td class="muted">—</td><td class="muted">May 02</td></tr>
<tr><td><span class="event-pill ev-sale">Sale</span></td><td class="mono">4.25 NOVA</td><td class="mono link">0x5c91…12dd</td><td class="mono link">0xab3c…77e0</td><td class="muted">Apr 11</td></tr>
<tr><td><span class="event-pill ev-xfer">Transfer</span></td><td class="mono muted">—</td><td class="mono link">0x77b2…fa30</td><td class="mono link">0x5c91…12dd</td><td class="muted">Mar 29</td></tr>
<tr><td><span class="event-pill ev-mint">Mint</span></td><td class="mono">0.80 NOVA</td><td class="mono muted">0x0000…0000</td><td class="mono link">0x77b2…fa30</td><td class="muted">Nov 03</td></tr>
</tbody>
</table>
</div>
</section>
</section>
</main>
<!-- ============ CONFIRM SHEET ============ -->
<div class="sheet-backdrop" id="sheetBackdrop" hidden></div>
<div class="sheet glass-strong" id="confirmSheet" role="dialog" aria-modal="true" aria-labelledby="sheetTitle" hidden>
<div class="sheet-grab" aria-hidden="true"></div>
<h2 id="sheetTitle">Confirm purchase</h2>
<div class="sheet-item">
<span class="sheet-thumb" aria-hidden="true"></span>
<div>
<strong>Nebula Drifter <span class="mono">#2481</span></strong>
<span class="muted small">Nebula Drifters · Lumen Chain</span>
</div>
<span class="sheet-price mono" id="sheetPrice">8.45 NOVA</span>
</div>
<div class="offer-field" id="offerField" hidden>
<label for="offerInput">Your offer (NOVA)</label>
<input class="mono" id="offerInput" type="number" min="0.01" step="0.01" value="7.85" inputmode="decimal" />
<span class="muted small">Top offer is <span class="mono">7.80 NOVA</span> · floor <span class="mono">7.90 NOVA</span></span>
</div>
<dl class="kv sheet-kv">
<div><dt>Item price</dt><dd class="mono" id="rowItem">8.4500 NOVA</dd></div>
<div><dt>Marketplace fee (2.5%)</dt><dd class="mono" id="rowFee">0.2113 NOVA</dd></div>
<div><dt>Creator royalty (5%)</dt><dd class="mono" id="rowRoyalty">0.4225 NOVA</dd></div>
<div><dt>Est. network gas</dt><dd class="mono">0.0021 NOVA</dd></div>
<div class="kv-total"><dt>Total</dt><dd class="mono" id="rowTotal">9.0859 NOVA</dd></div>
</dl>
<div class="risk-note" role="note">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<path d="M12 9v4M12 17h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/>
</svg>
<span>You are signing a transaction that transfers funds. Verify the contract <span class="mono">0x9bC4…e80a</span> and total before approving. This action cannot be undone.</span>
</div>
<div class="sheet-actions">
<button class="btn btn-ghost" type="button" id="sheetCancel">Cancel</button>
<button class="btn btn-primary" type="button" id="sheetConfirm">
<span class="btn-label">Approve in wallet</span>
<span class="btn-spinner" aria-hidden="true" hidden></span>
</button>
</div>
</div>
<div class="trait-tooltip mono" id="traitTooltip" role="tooltip" hidden></div>
<div class="toast-stack" id="toastStack" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>NFT Detail (traits · history · buy)
A full NFT detail page for the fictional Nebula Drifters collection on Lumen Chain. The left column holds a pure-CSS generative artwork — a drifting violet orb with spinning rings, a comet streak, and a perspective grid — wrapped in a gradient-border frame with a fullscreen zoom toggle, favorite counter, share (copies a mock link), and metadata refresh. A details card lists the contract, token standard, and royalty in monospace.
The right column carries the verified collection link, rarity rank, owner chip, and a glassy price panel: current price in NOVA with a fiat estimate, top offer, and a live auction countdown that turns red when under fifteen minutes and self-extends with an anti-snipe toast at zero. Below it, a seven-trait grid renders rarity bars and floor-price tooltips on hover or keyboard focus, with a legendary trait highlighted in amber.
Tabs switch between Properties, Offers (open bids table), and Activity — an SVG price-history chart that draws itself in plus a sales/transfer table with event pills. Buy now and Make offer open a bottom confirm sheet with a live fee breakdown (marketplace fee, royalty, gas), an editable offer input, an explicit risk warning to verify the contract, a focus trap, and a simulated signing spinner that resolves into a success toast with a fake tx hash.
UI-only simulation — no real wallet, RPC, or on-chain calls. Mock data, fictional tokens.