Web3 — NFT Marketplace (grid · filters · bid)
A glassy, dark-mode NFT marketplace browse page for a fictional Lumen Chain collection. A gradient-bordered hero shows floor price, volume, items, and owners with animated counters, above a sticky toolbar of Buy Now / On Auction toggles, an ETH price range, trait chips, a sort dropdown, and a grid density switch. A responsive grid renders pure-CSS generative art, rarity badges, ETH plus fiat prices, last-sale, a favorite heart with live count, and a hover Buy or Bid button that opens a confirm modal with a 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;
--font: "Space Grotesk", system-ui, -apple-system, sans-serif;
--mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", monospace;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: var(--font);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-image:
radial-gradient(820px 520px at 12% -10%, rgba(124, 92, 255, 0.16), transparent 60%),
radial-gradient(720px 480px at 100% 0%, rgba(0, 224, 198, 0.1), transparent 55%);
background-attachment: fixed;
min-height: 100vh;
}
.mono {
font-family: var(--mono);
font-feature-settings: "tnum" 1;
}
button {
font-family: inherit;
cursor: pointer;
}
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: var(--r-sm);
}
.skip-link {
position: absolute;
left: -9999px;
top: 0;
background: var(--accent);
color: #fff;
padding: 8px 14px;
border-radius: var(--r-sm);
z-index: 200;
text-decoration: none;
}
.skip-link:focus {
left: 12px;
top: 12px;
}
/* ── Topbar ─────────────────────────────────────────── */
.topbar {
position: sticky;
top: 0;
z-index: 40;
display: flex;
align-items: center;
gap: 14px;
padding: 12px clamp(14px, 4vw, 40px);
background: rgba(10, 11, 15, 0.72);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border-bottom: 1px solid var(--line);
}
.topbar-spacer {
flex: 1;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
letter-spacing: -0.01em;
}
.brand-mark {
width: 26px;
height: 26px;
border-radius: 8px;
background: conic-gradient(from 130deg, var(--accent), var(--accent-2), var(--accent));
box-shadow: 0 0 18px var(--accent-glow);
}
.brand-name span {
color: var(--accent-2);
}
.net-pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 7px 12px;
border-radius: var(--r-pill);
border: 1px solid var(--line-2);
background: rgba(255, 255, 255, 0.04);
font-size: 13px;
}
.net-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--pos);
box-shadow: 0 0 0 3px rgba(38, 208, 124, 0.18);
}
.wallet-chip {
display: inline-flex;
align-items: center;
gap: 9px;
padding: 6px 12px 6px 8px;
border-radius: var(--r-pill);
border: 1px solid var(--line-2);
background: rgba(255, 255, 255, 0.05);
color: var(--text);
font-size: 13px;
transition: border-color 0.18s, transform 0.18s;
}
.wallet-chip:hover {
border-color: var(--accent);
transform: translateY(-1px);
}
.wallet-avatar {
width: 22px;
height: 22px;
border-radius: 50%;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
}
.wallet-bal {
color: var(--muted);
border-left: 1px solid var(--line-2);
padding-left: 9px;
}
/* ── Page ───────────────────────────────────────────── */
.page {
max-width: 1280px;
margin: 0 auto;
padding: clamp(16px, 3vw, 32px) clamp(14px, 4vw, 40px) 80px;
}
/* ── Collection hero ────────────────────────────────── */
.collection {
border-radius: var(--r-lg);
border: 1px solid var(--line);
overflow: hidden;
background: var(--surface);
position: relative;
}
.collection::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1px;
background: linear-gradient(135deg, var(--accent-glow), transparent 40%, rgba(0, 224, 198, 0.3));
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
.collection-banner {
position: relative;
height: 168px;
overflow: hidden;
background:
linear-gradient(120deg, rgba(124, 92, 255, 0.35), rgba(0, 224, 198, 0.22)),
var(--surface-2);
}
.banner-orb {
position: absolute;
border-radius: 50%;
filter: blur(28px);
opacity: 0.8;
}
.banner-orb.o1 {
width: 240px;
height: 240px;
left: -40px;
top: -90px;
background: radial-gradient(circle, #9d7bff, transparent 70%);
animation: drift 14s ease-in-out infinite;
}
.banner-orb.o2 {
width: 300px;
height: 300px;
right: -60px;
top: -120px;
background: radial-gradient(circle, #2ff4d8, transparent 70%);
animation: drift 18s ease-in-out infinite reverse;
}
@keyframes drift {
50% {
transform: translate(30px, 24px) scale(1.08);
}
}
.banner-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(255, 255, 255, 0.06) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.06) 1px, transparent 1px);
background-size: 34px 34px;
mask-image: linear-gradient(to bottom, transparent, #000 60%);
}
.collection-body {
display: flex;
gap: 18px;
padding: 0 clamp(16px, 3vw, 28px) 22px;
margin-top: -44px;
position: relative;
}
.collection-avatar {
position: relative;
flex: 0 0 auto;
width: 92px;
height: 92px;
border-radius: 22px;
border: 3px solid var(--bg);
background: radial-gradient(circle at 30% 30%, #2a2d3a, #0d0f15);
display: grid;
place-items: center;
overflow: hidden;
}
.ca-ring {
position: absolute;
width: 70px;
height: 70px;
border-radius: 50%;
border: 2px solid rgba(0, 224, 198, 0.7);
box-shadow: 0 0 18px rgba(0, 224, 198, 0.4);
animation: spin 9s linear infinite;
}
.ca-core {
width: 34px;
height: 34px;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #c9b8ff, var(--accent));
box-shadow: 0 0 22px var(--accent-glow);
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.collection-meta {
padding-top: 50px;
min-width: 0;
}
.collection-title {
margin: 0;
font-size: clamp(22px, 3.4vw, 30px);
font-weight: 700;
letter-spacing: -0.02em;
display: flex;
align-items: center;
gap: 8px;
}
.verified {
color: var(--accent-2);
display: inline-flex;
}
.collection-sub {
margin: 4px 0 16px;
color: var(--muted);
font-size: 14px;
}
.collection-sub .mono {
color: var(--text);
}
.collection-stats {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.stat {
flex: 1 1 110px;
min-width: 96px;
padding: 10px 14px;
border-radius: var(--r-md);
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--line);
}
.stat-label {
display: block;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
}
.stat-val {
font-weight: 600;
font-size: 16px;
}
/* ── Toolbar ────────────────────────────────────────── */
.toolbar {
margin-top: 22px;
display: flex;
flex-direction: column;
gap: 14px;
}
.toolbar-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
.seg {
display: inline-flex;
padding: 4px;
gap: 2px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-pill);
}
.seg-btn {
border: 0;
background: transparent;
color: var(--muted);
padding: 7px 16px;
border-radius: var(--r-pill);
font-size: 13px;
font-weight: 500;
transition: color 0.18s, background 0.18s;
}
.seg-btn:hover {
color: var(--text);
}
.seg-btn.is-active {
color: #fff;
background: linear-gradient(135deg, var(--accent), #6a48ff);
box-shadow: 0 6px 18px -8px var(--accent-glow);
}
.price-range {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 6px 4px 12px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-pill);
}
.pr-eth {
color: var(--muted);
}
.pr-input {
width: 64px;
background: transparent;
border: 0;
color: var(--text);
font-size: 13px;
padding: 6px 2px;
}
.pr-input::placeholder {
color: var(--muted);
}
.pr-input:focus {
outline: none;
}
.pr-to {
color: var(--muted);
font-size: 12px;
}
.pr-apply {
border: 0;
background: rgba(124, 92, 255, 0.18);
color: var(--accent-2);
padding: 7px 13px;
border-radius: var(--r-pill);
font-size: 12px;
font-weight: 600;
transition: background 0.18s;
}
.pr-apply:hover {
background: rgba(124, 92, 255, 0.32);
}
.sort-wrap {
display: inline-flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
.sort-label {
font-size: 13px;
color: var(--muted);
}
.sort-sel {
background: var(--surface);
border: 1px solid var(--line);
color: var(--text);
padding: 9px 12px;
border-radius: var(--r-pill);
font-size: 13px;
}
.density {
display: inline-flex;
padding: 4px;
gap: 2px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-pill);
}
.dens-btn {
border: 0;
background: transparent;
color: var(--muted);
display: grid;
place-items: center;
padding: 7px;
border-radius: var(--r-pill);
transition: color 0.18s, background 0.18s;
}
.dens-btn svg rect {
fill: currentColor;
}
.dens-btn:hover {
color: var(--text);
}
.dens-btn.is-active {
color: #fff;
background: rgba(255, 255, 255, 0.08);
}
.trait-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.trait-chip {
border: 1px solid var(--line);
background: var(--surface);
color: var(--muted);
padding: 7px 14px;
border-radius: var(--r-pill);
font-size: 13px;
font-weight: 500;
transition: all 0.18s;
}
.trait-chip:hover {
color: var(--text);
border-color: var(--line-2);
}
.trait-chip.is-active {
color: #fff;
border-color: transparent;
background: linear-gradient(135deg, rgba(124, 92, 255, 0.9), rgba(0, 224, 198, 0.7));
box-shadow: 0 6px 18px -10px var(--accent-glow);
}
/* ── Results bar ────────────────────────────────────── */
.results-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin: 22px 0 14px;
}
.results-count {
font-size: 14px;
color: var(--muted);
}
.results-count strong {
color: var(--text);
}
.clear-btn {
border: 0;
background: none;
color: var(--accent-2);
font-size: 13px;
font-weight: 600;
padding: 4px;
}
.clear-btn:hover {
text-decoration: underline;
}
/* ── Grid ───────────────────────────────────────────── */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
gap: 16px;
}
.grid.large {
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 20px;
}
.card {
position: relative;
border-radius: var(--r-md);
border: 1px solid var(--line);
background: var(--surface);
overflow: hidden;
transition: transform 0.2s, border-color 0.2s, box-shadow 0.2s;
}
.card:hover,
.card:focus-visible {
transform: translateY(-4px);
border-color: var(--line-2);
box-shadow: 0 18px 40px -22px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(124, 92, 255, 0.25);
}
.card-art {
position: relative;
aspect-ratio: 1 / 1;
overflow: hidden;
}
.art-canvas {
position: absolute;
inset: 0;
}
.card:hover .art-canvas {
transform: scale(1.04);
transition: transform 0.5s ease;
}
.rar-badge {
position: absolute;
top: 8px;
left: 8px;
font-family: var(--mono);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
padding: 4px 8px;
border-radius: var(--r-pill);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
border: 1px solid var(--line-2);
}
.rar-badge.legendary {
color: #0a0b0f;
background: linear-gradient(135deg, #ffd76a, #ffb347);
border-color: transparent;
}
.rar-badge.epic {
color: #fff;
background: rgba(124, 92, 255, 0.55);
}
.rar-badge.rare {
color: #fff;
background: rgba(0, 224, 198, 0.4);
}
.rar-badge.common {
color: var(--text);
background: rgba(0, 0, 0, 0.45);
}
.status-tag {
position: absolute;
bottom: 8px;
left: 8px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
padding: 3px 8px;
border-radius: var(--r-pill);
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(6px);
border: 1px solid var(--line);
}
.status-tag.auction {
color: var(--warn);
border-color: rgba(255, 179, 71, 0.4);
}
.status-tag.buy {
color: var(--accent-2);
border-color: rgba(0, 224, 198, 0.35);
}
.fav {
position: absolute;
top: 8px;
right: 8px;
display: inline-flex;
align-items: center;
gap: 5px;
border: 1px solid var(--line-2);
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(6px);
color: var(--muted);
padding: 5px 9px 5px 7px;
border-radius: var(--r-pill);
font-size: 12px;
transition: color 0.18s, transform 0.18s;
}
.fav:hover {
color: var(--text);
transform: scale(1.06);
}
.fav[aria-pressed="true"] {
color: var(--neg);
border-color: rgba(255, 77, 109, 0.5);
}
.fav[aria-pressed="true"] .fav-ic path {
fill: var(--neg);
}
.fav.pulse {
animation: pop 0.32s ease;
}
@keyframes pop {
40% {
transform: scale(1.28);
}
}
.card-body {
padding: 12px 13px 14px;
}
.card-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.card-name {
font-weight: 600;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-rank {
font-size: 11px;
color: var(--accent-2);
flex: 0 0 auto;
}
.card-price {
display: flex;
align-items: flex-end;
justify-content: space-between;
margin-top: 10px;
gap: 8px;
}
.price-now {
display: flex;
flex-direction: column;
}
.price-eth {
font-weight: 700;
font-size: 16px;
}
.price-fiat {
font-size: 11px;
color: var(--muted);
}
.price-last {
text-align: right;
}
.pl-label {
display: block;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
}
.pl-val {
font-size: 12px;
color: var(--text);
}
.action {
margin-top: 12px;
width: 100%;
border: 0;
border-radius: var(--r-sm);
padding: 10px;
font-size: 13px;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, var(--accent), #6a48ff);
box-shadow: 0 10px 26px -14px var(--accent-glow);
opacity: 0;
max-height: 0;
padding-block: 0;
overflow: hidden;
transition: opacity 0.22s, max-height 0.22s, padding 0.22s, filter 0.18s;
}
.action.bid {
background: linear-gradient(135deg, var(--warn), #ff9d2e);
color: #0a0b0f;
box-shadow: 0 10px 26px -14px rgba(255, 179, 71, 0.6);
}
.card:hover .action,
.card:focus-within .action {
opacity: 1;
max-height: 60px;
padding-block: 10px;
}
.action:hover {
filter: brightness(1.08);
}
.action:active {
transform: translateY(1px);
}
.empty {
text-align: center;
color: var(--muted);
padding: 60px 20px;
font-size: 15px;
}
/* ── Modal ──────────────────────────────────────────── */
.modal-root {
position: fixed;
inset: 0;
z-index: 120;
display: grid;
place-items: center;
padding: 16px;
}
.modal-root[hidden] {
display: none;
}
.modal-scrim {
position: absolute;
inset: 0;
background: rgba(4, 5, 8, 0.72);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
animation: fade 0.2s ease;
}
.modal {
position: relative;
width: min(440px, 100%);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: var(--r-lg);
padding: 22px;
box-shadow: 0 40px 80px -30px rgba(0, 0, 0, 0.9);
animation: rise 0.24s cubic-bezier(0.2, 0.8, 0.2, 1);
}
@keyframes fade {
from {
opacity: 0;
}
}
@keyframes rise {
from {
opacity: 0;
transform: translateY(16px) scale(0.98);
}
}
.modal-x {
position: absolute;
top: 14px;
right: 14px;
width: 32px;
height: 32px;
border: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.06);
color: var(--muted);
font-size: 20px;
line-height: 1;
}
.modal-x:hover {
color: var(--text);
background: rgba(255, 255, 255, 0.12);
}
.modal-title {
margin: 0 0 16px;
font-size: 18px;
font-weight: 600;
}
.modal-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: var(--r-md);
background: var(--surface-2);
border: 1px solid var(--line);
}
.mi-thumb {
width: 56px;
height: 56px;
border-radius: var(--r-sm);
flex: 0 0 auto;
overflow: hidden;
}
.mi-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.mi-name {
font-weight: 600;
font-size: 14px;
}
.mi-coll {
font-size: 12px;
color: var(--muted);
}
.mi-rar {
font-family: var(--mono);
font-size: 11px;
color: var(--accent-2);
flex: 0 0 auto;
}
.bid-field {
margin-top: 14px;
}
.bid-label {
display: block;
font-size: 13px;
color: var(--muted);
margin-bottom: 6px;
}
.bid-input-wrap {
display: flex;
align-items: center;
gap: 8px;
padding: 0 12px;
border-radius: var(--r-md);
border: 1px solid var(--line-2);
background: var(--surface-2);
}
.bid-eth {
color: var(--muted);
}
.bid-input {
flex: 1;
background: transparent;
border: 0;
color: var(--text);
font-size: 18px;
font-weight: 600;
padding: 12px 0;
}
.bid-input:focus {
outline: none;
}
.bid-hint {
display: block;
font-size: 12px;
color: var(--muted);
margin-top: 6px;
}
.fee-list {
margin: 16px 0 0;
}
.fee-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 7px 0;
font-size: 13px;
color: var(--muted);
}
.fee-row dt {
margin: 0;
}
.fee-row dd {
margin: 0;
color: var(--text);
}
.fee-total {
border-top: 1px solid var(--line);
margin-top: 4px;
padding-top: 12px;
font-size: 15px;
}
.fee-total dt {
font-weight: 600;
color: var(--text);
}
.fee-total dd {
font-weight: 700;
color: var(--accent-2);
}
.risk {
display: flex;
gap: 9px;
align-items: flex-start;
margin: 16px 0;
padding: 12px;
border-radius: var(--r-md);
background: rgba(255, 179, 71, 0.08);
border: 1px solid rgba(255, 179, 71, 0.28);
color: var(--warn);
font-size: 12px;
line-height: 1.45;
}
.risk svg {
flex: 0 0 auto;
margin-top: 1px;
}
.risk .mono {
color: var(--text);
}
.modal-actions {
display: flex;
gap: 10px;
}
.btn {
flex: 1;
border: 0;
border-radius: var(--r-md);
padding: 13px;
font-size: 14px;
font-weight: 600;
transition: filter 0.18s, transform 0.12s;
}
.btn.ghost {
flex: 0 0 auto;
padding-inline: 18px;
background: rgba(255, 255, 255, 0.05);
color: var(--text);
border: 1px solid var(--line-2);
}
.btn.ghost:hover {
background: rgba(255, 255, 255, 0.1);
}
.btn.primary {
position: relative;
color: #fff;
background: linear-gradient(135deg, var(--accent), #6a48ff);
box-shadow: 0 12px 30px -14px var(--accent-glow);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn.primary:hover {
filter: brightness(1.08);
}
.btn.primary:active {
transform: translateY(1px);
}
.btn.primary[data-loading="true"] {
pointer-events: none;
opacity: 0.9;
}
.cb-spin {
display: none;
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;
}
.btn.primary[data-loading="true"] .cb-spin {
display: inline-block;
}
/* ── Toasts ─────────────────────────────────────────── */
.toast-host {
position: fixed;
bottom: 18px;
left: 50%;
transform: translateX(-50%);
z-index: 200;
display: flex;
flex-direction: column;
gap: 8px;
width: min(380px, calc(100% - 24px));
}
.toast {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
border-radius: var(--r-md);
background: var(--elevated);
border: 1px solid var(--line-2);
box-shadow: 0 18px 40px -20px rgba(0, 0, 0, 0.9);
font-size: 13px;
animation: toast-in 0.26s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.toast.out {
animation: toast-out 0.24s ease forwards;
}
.toast .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--pos);
flex: 0 0 auto;
}
.toast.warn .dot {
background: var(--warn);
}
.toast .mono {
color: var(--accent-2);
}
@keyframes toast-in {
from {
opacity: 0;
transform: translateY(14px);
}
}
@keyframes toast-out {
to {
opacity: 0;
transform: translateY(10px);
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}
/* ── Responsive ─────────────────────────────────────── */
@media (max-width: 720px) {
.sort-wrap {
margin-left: 0;
}
}
@media (max-width: 520px) {
.wallet-bal {
display: none;
}
.collection-body {
flex-direction: column;
gap: 0;
margin-top: -38px;
}
.collection-meta {
padding-top: 14px;
}
.toolbar-row {
gap: 10px;
}
.seg,
.price-range {
width: 100%;
justify-content: center;
}
.price-range {
justify-content: space-between;
}
.pr-input {
width: 100%;
flex: 1;
}
.sort-wrap {
flex: 1;
}
.sort-sel {
flex: 1;
}
.grid,
.grid.large {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.action {
opacity: 1;
max-height: 60px;
padding-block: 10px;
}
}
@media (max-width: 360px) {
.grid,
.grid.large {
grid-template-columns: 1fr;
}
}"use strict";
/* ════════════════════════════════════════════════════════════
Web3 — NFT Marketplace (UI simulation)
No wallet, RPC, or on-chain calls. All data is mock.
════════════════════════════════════════════════════════════ */
(function () {
const ETH_USD = 3284; // fictional fixed quote for fiat display
const TRAITS = ["Aurora", "Ember", "Glacier", "Void", "Solar"];
const RARITY = ["legendary", "epic", "rare", "common"];
// Trait → color seed for generative art
const TRAIT_COLORS = {
Aurora: ["#7c5cff", "#00e0c6"],
Ember: ["#ff6a3d", "#ffb347"],
Glacier: ["#5cc8ff", "#bfeaff"],
Void: ["#6a48ff", "#1b1e27"],
Solar: ["#ffd76a", "#ff8a3d"],
};
/* ── Mock dataset ──────────────────────────────────── */
function rng(seed) {
// deterministic pseudo-random from seed
let s = seed * 9301 + 49297;
return function () {
s = (s * 9301 + 49297) % 233280;
return s / 233280;
};
}
const items = Array.from({ length: 24 }, (_, i) => {
const r = rng(i + 7);
const trait = TRAITS[Math.floor(r() * TRAITS.length)];
const rarityRoll = r();
const rarity =
rarityRoll > 0.92 ? "legendary" : rarityRoll > 0.72 ? "epic" : rarityRoll > 0.42 ? "rare" : "common";
const isAuction = r() > 0.62;
const base =
rarity === "legendary" ? 6 + r() * 8 : rarity === "epic" ? 3 + r() * 4 : rarity === "rare" ? 2 + r() * 2 : 2.1 + r() * 1.2;
const price = Math.round(base * 100) / 100;
const last = Math.round(price * (0.7 + r() * 0.6) * 100) / 100;
const rank = Math.max(1, Math.floor(r() * 4096));
return {
id: 1000 + i,
idx: i,
trait,
rarity,
status: isAuction ? "auction" : "buy",
price,
last,
rank,
listedOrder: Math.floor(r() * 1000),
favs: Math.floor(r() * 90),
faved: false,
seed: i + 7,
};
});
/* ── State ─────────────────────────────────────────── */
const state = {
status: "all",
trait: "all",
min: null,
max: null,
sort: "recent",
};
/* ── DOM ───────────────────────────────────────────── */
const $ = (sel, ctx = document) => ctx.querySelector(sel);
const $$ = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));
const gridEl = $("#grid");
const tpl = $("#cardTpl");
const resultsCount = $("#resultsCount");
const emptyState = $("#emptyState");
const fmtUsd = (eth) =>
"$" + Math.round(eth * ETH_USD).toLocaleString("en-US");
/* ── Generative art (CSS-drawn thumbnail) ──────────── */
function paintArt(el, item) {
const [c1, c2] = TRAIT_COLORS[item.trait];
const r = rng(item.seed * 13);
const a1 = Math.floor(r() * 360);
const a2 = Math.floor(r() * 360);
const px = Math.floor(20 + r() * 60);
const py = Math.floor(20 + r() * 60);
const ring = item.rarity === "legendary" || item.rarity === "epic";
el.style.background = `
radial-gradient(circle at ${px}% ${py}%, ${c1}, transparent 55%),
conic-gradient(from ${a1}deg at 50% 50%, ${c1}, ${c2}, ${c1}),
linear-gradient(${a2}deg, ${c2}33, #0a0b0f)`;
el.style.position = "absolute";
el.style.inset = "0";
el.innerHTML =
`<span style="position:absolute;inset:0;background:
repeating-linear-gradient(${a1}deg, transparent 0 8px, rgba(255,255,255,.05) 8px 9px);"></span>` +
`<span style="position:absolute;left:50%;top:50%;width:${ring ? 64 : 40}%;height:${ring ? 64 : 40}%;
transform:translate(-50%,-50%);border-radius:50%;
background:radial-gradient(circle at 35% 30%, #ffffffcc, ${c1}aa 40%, transparent 70%);
box-shadow:0 0 40px ${c1}88;"></span>` +
(ring
? `<span style="position:absolute;left:50%;top:50%;width:84%;height:84%;transform:translate(-50%,-50%);
border-radius:50%;border:2px solid ${c2}cc;opacity:.7;"></span>`
: "");
}
/* ── Render ────────────────────────────────────────── */
function visible() {
let list = items.filter((it) => {
if (state.status !== "all" && it.status !== state.status) return false;
if (state.trait !== "all" && it.trait !== state.trait) return false;
if (state.min != null && it.price < state.min) return false;
if (state.max != null && it.price > state.max) return false;
return true;
});
const rarityOrder = { legendary: 0, epic: 1, rare: 2, common: 3 };
switch (state.sort) {
case "low":
list.sort((a, b) => a.price - b.price);
break;
case "high":
list.sort((a, b) => b.price - a.price);
break;
case "rarity":
list.sort(
(a, b) => rarityOrder[a.rarity] - rarityOrder[b.rarity] || a.rank - b.rank
);
break;
default: // recent
list.sort((a, b) => a.listedOrder - b.listedOrder);
}
return list;
}
function render() {
const list = visible();
gridEl.innerHTML = "";
emptyState.hidden = list.length !== 0;
const frag = document.createDocumentFragment();
list.forEach((it) => {
const node = tpl.content.firstElementChild.cloneNode(true);
node.dataset.id = it.id;
paintArt($(".art-canvas", node), it);
const badge = $(".rar-badge", node);
badge.textContent = it.rarity;
badge.classList.add(it.rarity);
const tag = $(".status-tag", node);
tag.textContent = it.status === "auction" ? "On auction" : "Buy now";
tag.classList.add(it.status);
$(".card-name", node).textContent = `Drifter #${it.id}`;
$(".card-rank", node).textContent = `#${it.rank}`;
$(".price-eth", node).textContent = `${it.price.toFixed(2)} ETH`;
$(".price-fiat", node).textContent = fmtUsd(it.price);
$(".pl-val", node).textContent = `${it.last.toFixed(2)} Ξ`;
const fav = $(".fav", node);
const favCount = $(".fav-count", node);
favCount.textContent = it.favs;
fav.setAttribute("aria-pressed", String(it.faved));
fav.setAttribute(
"aria-label",
it.faved ? "Remove from favorites" : "Add to favorites"
);
fav.addEventListener("click", (e) => {
e.stopPropagation();
it.faved = !it.faved;
it.favs += it.faved ? 1 : -1;
favCount.textContent = it.favs;
fav.setAttribute("aria-pressed", String(it.faved));
fav.setAttribute(
"aria-label",
it.faved ? "Remove from favorites" : "Add to favorites"
);
fav.classList.remove("pulse");
void fav.offsetWidth;
fav.classList.add("pulse");
});
const action = $(".action", node);
const isBid = it.status === "auction";
action.textContent = isBid ? "Place bid" : "Buy now";
if (isBid) action.classList.add("bid");
action.addEventListener("click", (e) => {
e.stopPropagation();
openModal(it);
});
// clicking the card art also opens the buy/bid flow
$(".card-art", node).addEventListener("click", () => openModal(it));
frag.appendChild(node);
});
gridEl.appendChild(frag);
const n = list.length;
resultsCount.innerHTML = `<strong>${n}</strong> item${n === 1 ? "" : "s"}`;
}
/* ── Filter controls ───────────────────────────────── */
$$(".seg-btn").forEach((btn) => {
btn.addEventListener("click", () => {
$$(".seg-btn").forEach((b) => {
b.classList.remove("is-active");
b.setAttribute("aria-pressed", "false");
});
btn.classList.add("is-active");
btn.setAttribute("aria-pressed", "true");
state.status = btn.dataset.status;
render();
});
});
$$(".trait-chip").forEach((chip) => {
chip.addEventListener("click", () => {
$$(".trait-chip").forEach((c) => {
c.classList.remove("is-active");
c.setAttribute("aria-pressed", "false");
});
chip.classList.add("is-active");
chip.setAttribute("aria-pressed", "true");
state.trait = chip.dataset.trait;
render();
});
});
const minIn = $("#priceMin");
const maxIn = $("#priceMax");
function applyPrice() {
const mn = parseFloat(minIn.value);
const mx = parseFloat(maxIn.value);
state.min = isNaN(mn) ? null : mn;
state.max = isNaN(mx) ? null : mx;
render();
}
$("#priceApply").addEventListener("click", applyPrice);
[minIn, maxIn].forEach((inp) =>
inp.addEventListener("keydown", (e) => {
if (e.key === "Enter") applyPrice();
})
);
$("#sortSel").addEventListener("change", (e) => {
state.sort = e.target.value;
render();
});
$("#clearFilters").addEventListener("click", () => {
state.status = "all";
state.trait = "all";
state.min = null;
state.max = null;
state.sort = "recent";
minIn.value = "";
maxIn.value = "";
$("#sortSel").value = "recent";
$$(".seg-btn").forEach((b, i) => {
b.classList.toggle("is-active", i === 0);
b.setAttribute("aria-pressed", String(i === 0));
});
$$(".trait-chip").forEach((c, i) => {
c.classList.toggle("is-active", i === 0);
c.setAttribute("aria-pressed", String(i === 0));
});
render();
});
// Density toggle
$$(".dens-btn").forEach((btn) => {
btn.addEventListener("click", () => {
$$(".dens-btn").forEach((b) => {
b.classList.remove("is-active");
b.setAttribute("aria-pressed", "false");
});
btn.classList.add("is-active");
btn.setAttribute("aria-pressed", "true");
gridEl.classList.toggle("large", btn.dataset.density === "large");
});
});
$("#walletChip").addEventListener("click", () =>
toast("Wallet 0x7a3f…c41d — balance 12.84 ETH (simulated)")
);
/* ── Quick-buy / bid modal ─────────────────────────── */
const modalRoot = $("#modalRoot");
const modal = $("#modal");
const confirmBtn = $("#confirmBtn");
const bidField = $("#bidField");
const bidInput = $("#bidInput");
let activeItem = null;
let lastFocus = null;
function feeBreakdown(price) {
const market = price * 0.025;
const royalty = price * 0.05;
const gas = 0.0021;
return { market, royalty, gas, total: price + market + royalty + gas };
}
function refreshFees(price) {
const f = feeBreakdown(price);
$("#feePrice").textContent = `${price.toFixed(2)} ETH`;
$("#feeMarket").textContent = `${f.market.toFixed(4)} ETH`;
$("#feeRoyalty").textContent = `${f.royalty.toFixed(4)} ETH`;
$("#feeGas").textContent = `${f.gas.toFixed(4)} ETH`;
$("#feeTotal").textContent = `${f.total.toFixed(4)} ETH`;
}
function openModal(it) {
activeItem = it;
lastFocus = document.activeElement;
const isBid = it.status === "auction";
$("#modalTitle").textContent = isBid ? "Place a bid" : "Complete purchase";
$("#miName").textContent = `Drifter #${it.id}`;
$("#miRarity").textContent = it.rarity;
paintArt($("#miThumb"), it);
$("#feePriceLabel").textContent = isBid ? "Your bid" : "Item price";
bidField.hidden = !isBid;
if (isBid) {
const minBid = Math.round((it.price + 0.05) * 100) / 100;
bidInput.value = minBid.toFixed(2);
bidInput.min = it.price;
$("#bidTop").textContent = `${it.price.toFixed(2)} ETH`;
refreshFees(minBid);
confirmBtn.querySelector(".cb-label").textContent = "Sign & bid";
} else {
refreshFees(it.price);
confirmBtn.querySelector(".cb-label").textContent = "Sign & buy";
}
modalRoot.hidden = false;
document.body.style.overflow = "hidden";
(isBid ? bidInput : confirmBtn).focus();
}
function closeModal() {
modalRoot.hidden = true;
document.body.style.overflow = "";
confirmBtn.dataset.loading = "false";
activeItem = null;
if (lastFocus && lastFocus.focus) lastFocus.focus();
}
bidInput.addEventListener("input", () => {
const v = parseFloat(bidInput.value);
if (!isNaN(v)) refreshFees(v);
});
modalRoot.addEventListener("click", (e) => {
if (e.target.closest("[data-close]")) closeModal();
});
document.addEventListener("keydown", (e) => {
if (modalRoot.hidden) return;
if (e.key === "Escape") closeModal();
if (e.key === "Tab") trapFocus(e);
});
function trapFocus(e) {
const f = $$(
'button, [href], input, select, [tabindex]:not([tabindex="-1"])',
modal
).filter((el) => !el.disabled && el.offsetParent !== null);
if (!f.length) return;
const first = f[0];
const last = f[f.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 (!activeItem) return;
const isBid = activeItem.status === "auction";
if (isBid) {
const v = parseFloat(bidInput.value);
if (isNaN(v) || v <= activeItem.price) {
toast(`Bid must exceed top bid of ${activeItem.price.toFixed(2)} ETH`, "warn");
bidInput.focus();
return;
}
}
confirmBtn.dataset.loading = "true";
// Simulated signing — no network. Resolve to a fake tx hash.
setTimeout(() => {
const hash =
"0x" +
Array.from({ length: 8 }, () =>
Math.floor(Math.random() * 16).toString(16)
).join("") +
"…" +
Array.from({ length: 4 }, () =>
Math.floor(Math.random() * 16).toString(16)
).join("");
const it = activeItem;
closeModal();
if (isBid) {
toast(`Bid placed on Drifter #${it.id} · tx <span class="mono">${hash}</span>`);
} else {
toast(`Bought Drifter #${it.id} · tx <span class="mono">${hash}</span>`);
}
}, 1400);
});
/* ── Toast ─────────────────────────────────────────── */
const toastHost = $("#toastHost");
function toast(html, kind) {
const el = document.createElement("div");
el.className = "toast" + (kind === "warn" ? " warn" : "");
el.innerHTML = `<span class="dot"></span><span>${html}</span>`;
toastHost.appendChild(el);
setTimeout(() => {
el.classList.add("out");
el.addEventListener("animationend", () => el.remove(), { once: true });
}, 3600);
}
/* ── Animated stat numbers ─────────────────────────── */
function animateStats() {
$$(".stat-val[data-count]").forEach((el) => {
const target = parseFloat(el.dataset.count);
const isInt = el.dataset.int === "1";
const suffix = el.dataset.suffix || "";
const dur = 900;
const start = performance.now();
function step(now) {
const p = Math.min(1, (now - start) / dur);
const eased = 1 - Math.pow(1 - p, 3);
const val = target * eased;
el.textContent = isInt
? Math.round(val).toLocaleString("en-US")
: val.toFixed(2) + suffix;
if (p < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
});
}
/* ── Init ──────────────────────────────────────────── */
render();
if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
animateStats();
}
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Web3 — NFT Marketplace</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>
<a class="skip-link" href="#grid">Skip to listings</a>
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true"></span>
<span class="brand-name">Aether<span>Market</span></span>
</div>
<div class="topbar-spacer"></div>
<div class="net-pill" title="Connected network">
<span class="net-dot" aria-hidden="true"></span>
Lumen Chain
</div>
<button class="wallet-chip" id="walletChip" type="button" aria-label="Connected wallet">
<span class="wallet-avatar" aria-hidden="true"></span>
<span class="wallet-addr mono">0x7a3f…c41d</span>
<span class="wallet-bal mono">12.84 ETH</span>
</button>
</header>
<main class="page">
<!-- Collection hero -->
<section class="collection" aria-label="Collection overview">
<div class="collection-banner" aria-hidden="true">
<span class="banner-orb o1"></span>
<span class="banner-orb o2"></span>
<span class="banner-grid"></span>
</div>
<div class="collection-body">
<div class="collection-avatar" aria-hidden="true">
<span class="ca-ring"></span>
<span class="ca-core"></span>
</div>
<div class="collection-meta">
<h1 class="collection-title">
Nebula Drifters
<span class="verified" title="Verified collection" aria-label="Verified collection">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M12 2l2.4 1.8 3 .2.9 2.9 2.3 1.9-1 2.8 1 2.8-2.3 1.9-.9 2.9-3 .2L12 22l-2.4-1.8-3-.2-.9-2.9L3.4 15.3l1-2.8-1-2.8 2.3-1.9.9-2.9 3-.2z" fill="currentColor"/><path d="M9.5 12.4l1.9 1.9 3.6-3.9" fill="none" stroke="#0a0b0f" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
</h1>
<p class="collection-sub">
4,096 generative drifters on <strong>Lumen Chain</strong> ·
<span class="mono">0x9e21…7b04</span>
</p>
<div class="collection-stats" role="list">
<div class="stat" role="listitem">
<span class="stat-label">Floor</span>
<span class="stat-val mono" data-count="2.14" data-suffix=" ETH">2.14 ETH</span>
</div>
<div class="stat" role="listitem">
<span class="stat-label">24h Vol</span>
<span class="stat-val mono" data-count="318.6" data-suffix=" ETH">318.6 ETH</span>
</div>
<div class="stat" role="listitem">
<span class="stat-label">Items</span>
<span class="stat-val mono" data-count="4096" data-int="1">4,096</span>
</div>
<div class="stat" role="listitem">
<span class="stat-label">Owners</span>
<span class="stat-val mono" data-count="2487" data-int="1">2,487</span>
</div>
<div class="stat" role="listitem">
<span class="stat-label">Listed</span>
<span class="stat-val mono">7.4%</span>
</div>
</div>
</div>
</div>
</section>
<!-- Filter / sort toolbar -->
<section class="toolbar" aria-label="Filters and sorting">
<div class="toolbar-row">
<div class="seg" role="group" aria-label="Listing status">
<button class="seg-btn is-active" data-status="all" type="button" aria-pressed="true">All</button>
<button class="seg-btn" data-status="buy" type="button" aria-pressed="false">Buy Now</button>
<button class="seg-btn" data-status="auction" type="button" aria-pressed="false">On Auction</button>
</div>
<div class="price-range" role="group" aria-label="Price range in ETH">
<span class="pr-eth mono">Ξ</span>
<input class="pr-input mono" id="priceMin" type="number" inputmode="decimal" min="0" step="0.01" placeholder="Min" aria-label="Minimum price in ETH" />
<span class="pr-to">to</span>
<input class="pr-input mono" id="priceMax" type="number" inputmode="decimal" min="0" step="0.01" placeholder="Max" aria-label="Maximum price in ETH" />
<button class="pr-apply" id="priceApply" type="button">Apply</button>
</div>
<div class="sort-wrap">
<label class="sort-label" for="sortSel">Sort</label>
<select class="sort-sel mono" id="sortSel" aria-label="Sort listings">
<option value="recent">Recently listed</option>
<option value="low">Price: low → high</option>
<option value="high">Price: high → low</option>
<option value="rarity">Rarity</option>
</select>
</div>
<div class="density" role="group" aria-label="Grid density">
<button class="dens-btn is-active" data-density="cozy" type="button" aria-pressed="true" aria-label="Standard grid" title="Standard grid">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></svg>
</button>
<button class="dens-btn" data-density="large" type="button" aria-pressed="false" aria-label="Large grid" title="Large grid">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><rect x="3" y="3" width="8" height="8" rx="1.5"/><rect x="13" y="3" width="8" height="8" rx="1.5"/><rect x="3" y="13" width="8" height="8" rx="1.5"/><rect x="13" y="13" width="8" height="8" rx="1.5"/></svg>
</button>
</div>
</div>
<div class="trait-row" role="group" aria-label="Trait filters">
<button class="trait-chip is-active" data-trait="all" type="button" aria-pressed="true">All traits</button>
<button class="trait-chip" data-trait="Aurora" type="button" aria-pressed="false">Aurora</button>
<button class="trait-chip" data-trait="Ember" type="button" aria-pressed="false">Ember</button>
<button class="trait-chip" data-trait="Glacier" type="button" aria-pressed="false">Glacier</button>
<button class="trait-chip" data-trait="Void" type="button" aria-pressed="false">Void</button>
<button class="trait-chip" data-trait="Solar" type="button" aria-pressed="false">Solar</button>
</div>
</section>
<!-- Results bar -->
<div class="results-bar">
<span class="results-count" id="resultsCount">— items</span>
<button class="clear-btn" id="clearFilters" type="button">Clear filters</button>
</div>
<!-- Grid -->
<section class="grid" id="grid" aria-label="NFT listings" aria-live="polite"></section>
<p class="empty" id="emptyState" hidden>No drifters match these filters. Try widening your price range.</p>
</main>
<!-- Quick-buy / bid modal -->
<div class="modal-root" id="modalRoot" hidden>
<div class="modal-scrim" data-close></div>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" id="modal">
<button class="modal-x" type="button" data-close aria-label="Close">×</button>
<h2 class="modal-title" id="modalTitle">Complete purchase</h2>
<div class="modal-item">
<div class="mi-thumb" id="miThumb" aria-hidden="true"></div>
<div class="mi-info">
<span class="mi-name" id="miName">Nebula Drifter</span>
<span class="mi-coll">Nebula Drifters · <span class="mono">Lumen Chain</span></span>
</div>
<span class="mi-rar" id="miRarity">—</span>
</div>
<div class="bid-field" id="bidField" hidden>
<label class="bid-label" for="bidInput">Your bid (ETH)</label>
<div class="bid-input-wrap">
<span class="bid-eth mono">Ξ</span>
<input class="bid-input mono" id="bidInput" type="number" inputmode="decimal" min="0" step="0.01" aria-describedby="bidHint" />
</div>
<span class="bid-hint" id="bidHint">Top bid is <span class="mono" id="bidTop">—</span>. Enter more to lead.</span>
</div>
<dl class="fee-list">
<div class="fee-row"><dt id="feePriceLabel">Item price</dt><dd class="mono" id="feePrice">0 ETH</dd></div>
<div class="fee-row"><dt>Marketplace fee (2.5%)</dt><dd class="mono" id="feeMarket">0 ETH</dd></div>
<div class="fee-row"><dt>Creator royalty (5%)</dt><dd class="mono" id="feeRoyalty">0 ETH</dd></div>
<div class="fee-row"><dt>Est. gas</dt><dd class="mono" id="feeGas">0.0021 ETH</dd></div>
<div class="fee-row fee-total"><dt>Total</dt><dd class="mono" id="feeTotal">0 ETH</dd></div>
</dl>
<p class="risk" role="note">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M12 3l9 16H3z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M12 9v5M12 16.5v.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
You are signing a transaction that moves funds. Verify the contract
<span class="mono">0x9e21…7b04</span> before confirming. This is a UI simulation — nothing is sent on-chain.
</p>
<div class="modal-actions">
<button class="btn ghost" type="button" data-close>Cancel</button>
<button class="btn primary" id="confirmBtn" type="button">
<span class="cb-label">Sign & buy</span>
<span class="cb-spin" aria-hidden="true"></span>
</button>
</div>
</div>
</div>
<div class="toast-host" id="toastHost" aria-live="polite" aria-atomic="true"></div>
<template id="cardTpl">
<article class="card" tabindex="0">
<div class="card-art">
<div class="art-canvas" aria-hidden="true"></div>
<span class="rar-badge"></span>
<span class="status-tag"></span>
<button class="fav" type="button" aria-pressed="false" aria-label="Add to favorites">
<svg class="fav-ic" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M12 20s-7-4.6-9.3-8.5C1.1 8.3 2.6 5 5.8 5c2 0 3.2 1.2 4.2 2.5C11 6.2 12.2 5 14.2 5 17.4 5 18.9 8.3 17.3 11.5 15 15.4 12 20 12 20z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>
<span class="fav-count mono">0</span>
</button>
</div>
<div class="card-body">
<div class="card-head">
<span class="card-name"></span>
<span class="card-rank mono"></span>
</div>
<div class="card-price">
<div class="price-now">
<span class="price-eth mono"></span>
<span class="price-fiat mono"></span>
</div>
<div class="price-last">
<span class="pl-label">Last</span>
<span class="pl-val mono"></span>
</div>
</div>
<button class="action" type="button"></button>
</div>
</article>
</template>
<script src="script.js"></script>
</body>
</html>NFT Marketplace (grid · filters · bid)
A full browse page for the fictional Nebula Drifters collection on Lumen Chain. The hero is a gradient-bordered glass card with a drifting-orb banner, an animated avatar, and a verified mark, followed by a strip of stats — floor, 24h volume, items, owners, and listed percent — whose numbers count up on load. A sticky topbar carries the connected network pill and a monospace wallet chip (0x7a3f…c41d).
Beneath the hero sits the filter toolbar: a segmented All / Buy Now / On Auction status control, an ETH price-range input, a row of trait chips (Aurora, Ember, Glacier, Void, Solar), a sort dropdown (Recently listed, Price low→high / high→low, Rarity), and a standard/large grid density toggle. Every control filters and re-sorts the responsive grid live, and a results bar reports the visible count with a one-tap Clear filters.
Each card draws a unique CSS-only generative thumbnail seeded per token, with a rarity badge, ETH price plus fiat estimate, last-sale, and a favorite heart that toggles a live count with a pop animation. Hovering reveals a Buy now or Place bid button that opens a focus-trapped confirm modal: an editable bid field for auctions, a live fee breakdown (marketplace fee, creator royalty, gas, total), an explicit risk note to verify the contract, 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.