Web3 — DAO Governance (proposals · vote)
A dark, glassy DAO governance page for the fictional Lumen Collective: a gradient-bordered hero shows your voting power (held plus delegated vNOVA), treasury value, and active-proposal count, above a filterable proposals list with status pills, proposer chips, For/Against/Abstain vote bars, quorum progress, and live countdowns. Selecting a proposal opens a sticky detail panel with description, on-chain action, current results, and vote buttons that record your ballot, update the tallies, and fire a signed-receipt toast — plus a create-proposal modal with a bond 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;
--abstain: #8a90a2;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--r-pill: 999px;
--font-ui: "Space Grotesk", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
}
body {
background: var(--bg);
color: var(--text);
font-family: var(--font-ui);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.mono {
font-family: var(--font-mono);
font-feature-settings: "tnum";
}
button {
font-family: inherit;
color: inherit;
cursor: pointer;
}
button:focus-visible,
a:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
border-radius: var(--r-sm);
}
/* ===== Ambient background ===== */
.bg-orbs {
position: fixed;
inset: 0;
overflow: hidden;
pointer-events: none;
z-index: 0;
}
.orb {
position: absolute;
border-radius: 50%;
filter: blur(110px);
opacity: 0.32;
}
.orb-a {
width: 480px;
height: 480px;
top: -180px;
left: -120px;
background: radial-gradient(circle, var(--accent), transparent 70%);
}
.orb-b {
width: 420px;
height: 420px;
top: 30%;
right: -160px;
background: radial-gradient(circle, var(--accent-2), transparent 70%);
opacity: 0.18;
}
.shell {
position: relative;
z-index: 1;
max-width: 1180px;
margin: 0 auto;
padding: 24px 24px 48px;
}
/* ===== Top bar ===== */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 18px;
border: 1px solid var(--line);
border-radius: var(--r-lg);
background: rgba(19, 21, 28, 0.7);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
}
.brand {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.brand-mark {
display: grid;
place-items: center;
width: 40px;
height: 40px;
flex: 0 0 auto;
border-radius: 12px;
color: #fff;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
box-shadow: 0 0 22px var(--accent-glow);
}
.brand-text {
display: flex;
flex-direction: column;
line-height: 1.25;
min-width: 0;
}
.brand-text strong {
font-size: 16px;
font-weight: 700;
letter-spacing: 0.01em;
white-space: nowrap;
}
.brand-sub {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--muted);
white-space: nowrap;
}
.chain-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--accent-2);
box-shadow: 0 0 8px var(--accent-2);
}
.topbar-actions {
display: flex;
align-items: center;
gap: 10px;
}
.wallet-chip {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 14px;
font-size: 13px;
border: 1px solid var(--line-2);
border-radius: var(--r-pill);
background: var(--surface-2);
transition: border-color 0.15s ease, background 0.15s ease;
}
.wallet-chip:hover {
border-color: var(--accent);
background: var(--elevated);
}
.wallet-status {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--pos);
box-shadow: 0 0 8px var(--pos);
}
/* ===== Buttons ===== */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
padding: 10px 16px;
font-size: 13.5px;
font-weight: 600;
letter-spacing: 0.01em;
border-radius: var(--r-md);
border: 1px solid transparent;
transition: transform 0.12s ease, box-shadow 0.15s ease, background 0.15s ease,
border-color 0.15s ease, filter 0.15s ease;
}
.btn:active {
transform: translateY(1px) scale(0.99);
}
.btn-primary {
color: #fff;
background: linear-gradient(135deg, var(--accent), #5a3df0);
box-shadow: 0 6px 20px var(--accent-glow);
}
.btn-primary:hover {
filter: brightness(1.12);
box-shadow: 0 8px 26px var(--accent-glow);
}
.btn-ghost {
background: transparent;
border-color: var(--line-2);
color: var(--text);
}
.btn-ghost:hover {
background: var(--surface-2);
border-color: var(--accent);
}
.btn[disabled] {
opacity: 0.45;
cursor: not-allowed;
box-shadow: none;
}
.icon-btn {
display: grid;
place-items: center;
width: 32px;
height: 32px;
border-radius: var(--r-sm);
border: 1px solid var(--line);
background: var(--surface-2);
color: var(--muted);
transition: color 0.15s ease, border-color 0.15s ease;
}
.icon-btn:hover {
color: var(--text);
border-color: var(--line-2);
}
/* ===== Hero stats ===== */
.hero {
display: grid;
grid-template-columns: 1.5fr 1fr 1fr 1fr;
gap: 14px;
margin-top: 18px;
}
.stat-card {
position: relative;
padding: 18px;
border-radius: var(--r-lg);
border: 1px solid var(--line);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent 60%),
var(--surface);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.stat-power {
border: 1px solid transparent;
background:
linear-gradient(var(--surface), var(--surface)) padding-box,
linear-gradient(135deg, var(--accent), var(--accent-2)) border-box;
box-shadow: 0 10px 36px rgba(124, 92, 255, 0.18);
}
.stat-label {
margin: 0 0 6px;
font-size: 11.5px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--muted);
}
.stat-value {
margin: 0;
font-size: 27px;
font-weight: 700;
line-height: 1.15;
}
.stat-unit {
font-size: 13px;
font-weight: 500;
color: var(--accent-2);
}
.stat-split {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 6px;
font-size: 12px;
color: var(--muted);
}
.split-dot {
color: var(--line-2);
}
.power-meter {
margin-top: 12px;
height: 6px;
border-radius: var(--r-pill);
background: var(--surface-2);
overflow: hidden;
}
.power-fill {
display: block;
height: 100%;
border-radius: var(--r-pill);
background: linear-gradient(90deg, var(--accent), var(--accent-2));
box-shadow: 0 0 10px var(--accent-glow);
transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
.stat-foot {
margin: 8px 0 0;
font-size: 12px;
color: var(--muted);
}
.delta-pos {
color: var(--pos);
}
.stat-cta-text {
margin: 0 0 12px;
font-size: 13px;
color: var(--muted);
}
.stat-cta .btn {
width: 100%;
}
/* ===== Filters ===== */
.filters {
display: flex;
gap: 8px;
margin-top: 22px;
overflow-x: auto;
padding-bottom: 4px;
scrollbar-width: none;
}
.filters::-webkit-scrollbar {
display: none;
}
.filter-tab {
display: inline-flex;
align-items: center;
gap: 7px;
flex: 0 0 auto;
padding: 8px 15px;
font-size: 13px;
font-weight: 600;
border-radius: var(--r-pill);
border: 1px solid var(--line);
background: var(--surface);
color: var(--muted);
transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
.filter-tab:hover {
color: var(--text);
border-color: var(--line-2);
}
.filter-tab.is-active {
color: #fff;
border-color: var(--accent);
background: linear-gradient(135deg, rgba(124, 92, 255, 0.28), rgba(0, 224, 198, 0.12));
box-shadow: 0 0 16px rgba(124, 92, 255, 0.22);
}
.filter-tab .count {
font-size: 11px;
padding: 1px 7px;
border-radius: var(--r-pill);
background: var(--surface-2);
border: 1px solid var(--line);
}
/* ===== Layout ===== */
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 400px;
gap: 16px;
align-items: start;
margin-top: 16px;
}
/* ===== Proposal cards ===== */
.proposal-list {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.proposal-card {
width: 100%;
text-align: left;
padding: 18px;
border-radius: var(--r-lg);
border: 1px solid var(--line);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.025), transparent 55%),
var(--surface);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.2s ease;
animation: card-in 0.3s ease both;
}
@keyframes card-in {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.proposal-card:hover {
border-color: var(--line-2);
transform: translateY(-2px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35);
}
.proposal-card.is-selected {
border-color: var(--accent);
box-shadow: 0 0 0 1px var(--accent), 0 10px 30px rgba(124, 92, 255, 0.18);
}
.card-top {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 8px;
}
.pid {
font-size: 12px;
color: var(--muted);
}
.status-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 3px 11px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
border-radius: var(--r-pill);
border: 1px solid;
}
.status-pill::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.status-active {
color: var(--accent-2);
border-color: rgba(0, 224, 198, 0.4);
background: rgba(0, 224, 198, 0.08);
}
.status-active::before {
animation: pulse 1.6s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
box-shadow: 0 0 0 0 rgba(0, 224, 198, 0.5);
}
50% {
box-shadow: 0 0 0 4px rgba(0, 224, 198, 0);
}
}
.status-passed {
color: var(--pos);
border-color: rgba(38, 208, 124, 0.4);
background: rgba(38, 208, 124, 0.08);
}
.status-failed {
color: var(--neg);
border-color: rgba(255, 77, 109, 0.4);
background: rgba(255, 77, 109, 0.08);
}
.status-pending {
color: var(--warn);
border-color: rgba(255, 179, 71, 0.4);
background: rgba(255, 179, 71, 0.08);
}
.countdown {
margin-left: auto;
font-size: 12px;
color: var(--accent-2);
}
.countdown.ending {
color: var(--warn);
}
.ended-at {
margin-left: auto;
font-size: 12px;
color: var(--muted);
}
.card-title {
margin: 0 0 8px;
font-size: 16.5px;
font-weight: 600;
letter-spacing: 0.005em;
}
.proposer-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 14px;
font-size: 12px;
color: var(--muted);
}
.proposer-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 3px 10px 3px 4px;
border-radius: var(--r-pill);
border: 1px solid var(--line);
background: var(--surface-2);
font-size: 12px;
color: var(--text);
}
.avatar {
width: 18px;
height: 18px;
border-radius: 50%;
flex: 0 0 auto;
}
.voted-badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
font-size: 11px;
font-weight: 600;
border-radius: var(--r-pill);
border: 1px solid rgba(124, 92, 255, 0.45);
background: rgba(124, 92, 255, 0.12);
color: #b6a4ff;
}
/* vote bar */
.vote-bar {
display: flex;
height: 9px;
border-radius: var(--r-pill);
overflow: hidden;
background: var(--surface-2);
}
.vote-bar .seg {
height: 100%;
transition: width 0.55s cubic-bezier(0.22, 1, 0.36, 1);
min-width: 0;
}
.seg-for {
background: linear-gradient(90deg, #1fae67, var(--pos));
}
.seg-against {
background: linear-gradient(90deg, var(--neg), #d63a58);
}
.seg-abstain {
background: rgba(138, 144, 162, 0.55);
}
.vote-legend {
display: flex;
flex-wrap: wrap;
gap: 6px 16px;
margin-top: 9px;
font-size: 12px;
color: var(--muted);
}
.vote-legend .dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 2px;
margin-right: 6px;
}
.dot-for {
background: var(--pos);
}
.dot-against {
background: var(--neg);
}
.dot-abstain {
background: var(--abstain);
}
.legend-for {
color: var(--pos);
}
.legend-against {
color: var(--neg);
}
/* quorum */
.quorum {
margin-top: 12px;
}
.quorum-row {
display: flex;
justify-content: space-between;
font-size: 11.5px;
color: var(--muted);
margin-bottom: 5px;
}
.quorum-track {
position: relative;
height: 5px;
border-radius: var(--r-pill);
background: var(--surface-2);
overflow: hidden;
}
.quorum-fill {
display: block;
height: 100%;
border-radius: var(--r-pill);
background: linear-gradient(90deg, var(--accent), var(--accent-2));
transition: width 0.55s cubic-bezier(0.22, 1, 0.36, 1);
}
.quorum-met {
color: var(--pos);
}
/* ===== Detail panel ===== */
.detail-panel {
position: sticky;
top: 18px;
border-radius: var(--r-lg);
border: 1px solid transparent;
background:
linear-gradient(var(--surface), var(--surface)) padding-box,
linear-gradient(160deg, var(--accent), rgba(0, 224, 198, 0.6)) border-box;
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.45);
overflow: hidden;
animation: panel-in 0.28s ease both;
}
@keyframes panel-in {
from {
opacity: 0;
transform: translateX(14px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.detail-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
padding: 18px 18px 0;
}
.detail-body {
padding: 0 18px 18px;
}
.detail-id-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 8px;
}
.detail-title {
margin: 0 0 10px;
font-size: 18px;
font-weight: 700;
line-height: 1.3;
}
.detail-desc {
margin: 0 0 14px;
font-size: 13.5px;
color: var(--muted);
}
.detail-meta {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 16px;
}
.meta-cell {
padding: 10px 12px;
border-radius: var(--r-md);
border: 1px solid var(--line);
background: var(--surface-2);
}
.meta-cell .k {
display: block;
font-size: 10.5px;
text-transform: uppercase;
letter-spacing: 0.09em;
color: var(--muted);
margin-bottom: 3px;
}
.meta-cell .v {
font-size: 13px;
font-weight: 600;
}
/* result rows */
.results h3,
.cast h3 {
margin: 0 0 10px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--muted);
}
.result-row {
display: grid;
grid-template-columns: 72px 1fr auto;
align-items: center;
gap: 10px;
margin-bottom: 9px;
font-size: 13px;
}
.result-row .label {
font-weight: 600;
}
.label-for {
color: var(--pos);
}
.label-against {
color: var(--neg);
}
.label-abstain {
color: var(--abstain);
}
.result-track {
height: 7px;
border-radius: var(--r-pill);
background: var(--surface-2);
overflow: hidden;
}
.result-fill {
display: block;
height: 100%;
border-radius: var(--r-pill);
transition: width 0.55s cubic-bezier(0.22, 1, 0.36, 1);
}
.result-row .amt {
font-size: 12px;
color: var(--muted);
text-align: right;
white-space: nowrap;
}
/* vote actions */
.cast {
margin-top: 18px;
padding-top: 16px;
border-top: 1px solid var(--line);
}
.cast-note {
margin: 0 0 12px;
font-size: 12.5px;
color: var(--muted);
}
.vote-actions {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 8px;
}
.vote-btn {
padding: 11px 8px;
font-size: 13px;
font-weight: 700;
border-radius: var(--r-md);
border: 1px solid var(--line-2);
background: var(--surface-2);
transition: transform 0.12s ease, box-shadow 0.15s ease, border-color 0.15s ease,
background 0.15s ease;
}
.vote-btn:hover {
transform: translateY(-1px);
}
.vote-btn:active {
transform: translateY(1px);
}
.vote-for {
color: var(--pos);
}
.vote-for:hover {
border-color: var(--pos);
background: rgba(38, 208, 124, 0.12);
box-shadow: 0 6px 18px rgba(38, 208, 124, 0.22);
}
.vote-against {
color: var(--neg);
}
.vote-against:hover {
border-color: var(--neg);
background: rgba(255, 77, 109, 0.12);
box-shadow: 0 6px 18px rgba(255, 77, 109, 0.22);
}
.vote-abstain {
color: var(--text);
}
.vote-abstain:hover {
border-color: var(--line-2);
background: var(--elevated);
}
.vote-btn[disabled] {
opacity: 0.4;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.vote-receipt {
display: flex;
align-items: center;
gap: 9px;
padding: 11px 13px;
border-radius: var(--r-md);
border: 1px solid rgba(38, 208, 124, 0.4);
background: rgba(38, 208, 124, 0.08);
color: var(--pos);
font-size: 13px;
font-weight: 600;
}
.vote-receipt .mono {
color: var(--muted);
font-weight: 400;
font-size: 12px;
}
.closed-note {
padding: 11px 13px;
border-radius: var(--r-md);
border: 1px solid var(--line);
background: var(--surface-2);
color: var(--muted);
font-size: 13px;
}
.empty-state {
padding: 44px 20px;
text-align: center;
border: 1px dashed var(--line-2);
border-radius: var(--r-lg);
color: var(--muted);
font-size: 14px;
}
/* ===== Footer ===== */
.page-foot {
display: flex;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-top: 28px;
font-size: 12px;
color: var(--muted);
}
/* ===== Modal ===== */
.modal-overlay[hidden] { display: none; }
.modal-overlay {
position: fixed;
inset: 0;
z-index: 60;
display: grid;
place-items: center;
padding: 20px;
background: rgba(6, 7, 10, 0.72);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.modal {
width: min(520px, 100%);
max-height: calc(100vh - 40px);
overflow-y: auto;
border-radius: var(--r-lg);
border: 1px solid transparent;
background:
linear-gradient(var(--surface-2), var(--surface-2)) padding-box,
linear-gradient(150deg, var(--accent), var(--accent-2)) border-box;
box-shadow: 0 28px 70px rgba(0, 0, 0, 0.55);
animation: modal-in 0.22s ease both;
}
@keyframes modal-in {
from {
opacity: 0;
transform: translateY(12px) scale(0.97);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.modal-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 20px 0;
}
.modal-head h2 {
margin: 0;
font-size: 18px;
font-weight: 700;
}
.modal-body {
padding: 16px 20px 4px;
}
.field {
display: block;
margin-bottom: 14px;
}
.field-label {
display: block;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
margin-bottom: 6px;
}
.field input,
.field textarea,
.field select {
width: 100%;
padding: 11px 13px;
font-family: var(--font-ui);
font-size: 14px;
color: var(--text);
border: 1px solid var(--line-2);
border-radius: var(--r-md);
background: var(--surface);
resize: vertical;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.field input:focus,
.field textarea:focus,
.field select:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(124, 92, 255, 0.25);
}
.field input::placeholder,
.field textarea::placeholder {
color: rgba(138, 144, 162, 0.7);
}
.field-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.risk-note {
display: flex;
gap: 10px;
align-items: flex-start;
padding: 12px 14px;
border-radius: var(--r-md);
border: 1px solid rgba(255, 179, 71, 0.4);
background: rgba(255, 179, 71, 0.08);
color: var(--warn);
}
.risk-note svg {
flex: 0 0 auto;
margin-top: 2px;
}
.risk-note p {
margin: 0;
font-size: 12.5px;
line-height: 1.5;
color: #ffd9a8;
}
.risk-note strong {
color: var(--warn);
}
.modal-foot {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 14px 20px 20px;
}
.modal-bond {
font-size: 11px;
opacity: 0.85;
font-weight: 500;
}
/* ===== Toasts ===== */
.toast-stack {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 80;
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
pointer-events: none;
}
.toast {
display: flex;
align-items: center;
gap: 9px;
max-width: min(440px, calc(100vw - 40px));
padding: 11px 18px;
font-size: 13.5px;
font-weight: 500;
border-radius: var(--r-pill);
border: 1px solid var(--line-2);
background: rgba(35, 38, 47, 0.92);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.5);
animation: toast-in 0.25s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.toast.toast-out {
animation: toast-out 0.3s ease both;
}
.toast-pos {
border-color: rgba(38, 208, 124, 0.45);
}
.toast-neg {
border-color: rgba(255, 77, 109, 0.45);
}
.toast-accent {
border-color: rgba(124, 92, 255, 0.55);
box-shadow: 0 12px 36px rgba(124, 92, 255, 0.25);
}
@keyframes toast-in {
from {
opacity: 0;
transform: translateY(14px) scale(0.96);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes toast-out {
to {
opacity: 0;
transform: translateY(10px) scale(0.96);
}
}
/* ===== Responsive ===== */
@media (max-width: 1020px) {
.hero {
grid-template-columns: 1fr 1fr;
}
.layout {
grid-template-columns: 1fr;
}
.detail-panel {
position: static;
order: -1;
}
}
@media (max-width: 680px) {
.topbar {
flex-wrap: wrap;
}
.topbar-actions {
width: 100%;
justify-content: flex-end;
}
}
@media (max-width: 520px) {
.shell {
padding: 14px 12px 36px;
}
.hero {
grid-template-columns: 1fr;
gap: 10px;
}
.stat-value {
font-size: 23px;
}
.topbar {
padding: 12px;
}
.brand-text strong {
font-size: 14.5px;
}
.topbar-actions .btn-ghost {
padding: 8px 11px;
font-size: 12.5px;
}
.proposal-card {
padding: 14px;
}
.card-title {
font-size: 15px;
}
.countdown,
.ended-at {
margin-left: 0;
width: 100%;
}
.detail-meta {
grid-template-columns: 1fr;
}
.vote-actions {
grid-template-columns: 1fr;
}
.field-row {
grid-template-columns: 1fr;
}
.modal-foot {
flex-direction: column-reverse;
}
.modal-foot .btn {
width: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}/* Lumen Collective — DAO Governance (UI simulation, no real chain calls) */
(() => {
"use strict";
// ---------- State ----------
const QUORUM = 40_000_000; // vNOVA
const MY_POWER = 184_250;
const now = Date.now();
const h = 3600 * 1000;
const d = 24 * h;
const proposals = [
{
id: "LIP-47",
title: "Deploy 4.2M lmUSD from treasury into the Lumen Chain LST liquidity program",
status: "active",
proposer: { name: "auroranode.lum", addr: "0x9d21…44fe", hue: 262 },
endsAt: now + 2 * d + 7 * h + 23 * 60 * 1000,
for: 21_400_000,
against: 6_850_000,
abstain: 1_920_000,
myVote: null,
description:
"Allocates 4,200,000 lmUSD from the community treasury to seed three concentrated liquidity pools (NOVA/lmUSD, wLUM/lmUSD, stNOVA/NOVA) for 26 weeks. Liquidity is managed by the on-chain LiquidityVault module with a 15% max drawdown circuit breaker; unused funds return to the treasury automatically at epoch 412.",
actions: "Treasury transfer · 4,200,000 lmUSD → 0x5be0…91aa (LiquidityVault)",
},
{
id: "LIP-46",
title: "Reduce governance quorum from 40M to 32M vNOVA for parameter-change proposals",
status: "active",
proposer: { name: "0x3fc8…b1d2", addr: "0x3fc8…b1d2", hue: 174 },
endsAt: now + 18 * h + 41 * 60 * 1000,
for: 14_100_000,
against: 15_900_000,
abstain: 4_300_000,
myVote: "against",
description:
"Lowers the quorum requirement for parameter-change proposals only (fee switches, emission curves, oracle configs) from 40M to 32M vNOVA. Treasury transfers and protocol upgrades keep the 40M threshold. Includes a 6-month sunset clause: the change reverts unless re-ratified in epoch 460.",
actions: "Parameter change · Governor.setQuorum(class=PARAM, 32_000_000e18)",
},
{
id: "LIP-45",
title: "Fund the Nebula Grants round 7 with 850,000 NOVA over two quarters",
status: "active",
proposer: { name: "nebula-guild.lum", addr: "0x61aa…07c3", hue: 32 },
endsAt: now + 4 * d + 11 * h,
for: 9_750_000,
against: 2_100_000,
abstain: 860_000,
myVote: null,
description:
"Funds the seventh Nebula grants cohort: 850,000 NOVA streamed over 180 days via the PaymentStream module to the Grants Council multisig (4-of-7). Focus areas are zk tooling, mobile light clients, and lmUSD payment rails. Council publishes monthly transparency reports on-chain.",
actions: "Treasury stream · 850,000 NOVA → 0xab44…2e09 (Grants 4/7 multisig)",
},
{
id: "LIP-44",
title: "Activate the protocol fee switch at 12% of sequencer revenue, routed to stakers",
status: "passed",
proposer: { name: "veloria.lum", addr: "0x82e4…9f1b", hue: 318 },
endedLabel: "Ended 3d ago · executed",
for: 47_300_000,
against: 8_900_000,
abstain: 2_400_000,
myVote: "for",
description:
"Turns on the long-debated fee switch: 12% of Lumen Chain sequencer revenue is redirected from the treasury to stNOVA stakers, distributed per epoch. Executed at block 18,442,067 — first distribution lands in epoch 408.",
actions: "Executed · FeeRouter.setStakerShare(1200) at block 18,442,067",
},
{
id: "LIP-43",
title: "Adopt the dual-oracle design (Starfall + Meridian feeds) for lmUSD collateral",
status: "passed",
proposer: { name: "auroranode.lum", addr: "0x9d21…44fe", hue: 262 },
endedLabel: "Ended 9d ago · executed",
for: 51_800_000,
against: 1_750_000,
abstain: 5_100_000,
myVote: null,
description:
"Replaces the single Starfall oracle with a dual-feed design: lmUSD collateral pricing now takes the median of Starfall and Meridian, with a 1.5% deviation guard that pauses mints when feeds disagree. Audited by Glasshouse Security (report GH-2026-031).",
actions: "Executed · OracleHub.setFeeds([starfall, meridian]) at block 18,301,554",
},
{
id: "LIP-42",
title: "Acquire 2% of the Driftway DEX token supply via treasury swap",
status: "failed",
proposer: { name: "0xkepler.lum", addr: "0xce17…3d88", hue: 0 },
endedLabel: "Ended 16d ago · quorum not met",
for: 11_200_000,
against: 19_400_000,
abstain: 6_300_000,
myVote: "against",
description:
"Proposed swapping 1.1M NOVA for 2% of DRIFT supply to deepen the partnership with Driftway DEX. Failed on both margin and turnout: Against led by 8.2M and total participation (36.9M) stayed below the 40M quorum.",
actions: "Not executed · proposal defeated",
},
{
id: "LIP-48",
title: "Migrate the staking module to vNOVA v2 with time-weighted boosts",
status: "pending",
proposer: { name: "lumen-labs.lum", addr: "0x44b7…aa05", hue: 205 },
startsLabel: "Voting opens in 1d 9h",
for: 0,
against: 0,
abstain: 0,
myVote: null,
description:
"Upgrades staking to vNOVA v2: voting power scales with lock duration (1x at 1 month up to 2.5x at 24 months), and delegation becomes revocable per-proposal. Currently in the 2-day review window — voting opens at epoch 409.",
actions: "Protocol upgrade · StakingModule v2 (audit pending, Glasshouse)",
},
];
let activeFilter = "all";
let selectedId = null;
let myPower = MY_POWER;
// ---------- Helpers ----------
const $ = (sel, root = document) => root.querySelector(sel);
const fmt = (n) => Math.round(n).toLocaleString("en-US");
const fmtCompact = (n) => {
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
if (n >= 1_000) return (n / 1_000).toFixed(1).replace(/\.0$/, "") + "K";
return String(Math.round(n));
};
const esc = (s) =>
String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
const randHash = () =>
"0x" +
Array.from({ length: 4 }, () => Math.floor(Math.random() * 0xffff).toString(16).padStart(4, "0")).join("") .slice(0, 6) +
"…" +
Math.floor(Math.random() * 0xffff).toString(16).padStart(4, "0");
function toast(msg, kind = "") {
const stack = $("#toastStack");
const el = document.createElement("div");
el.className = "toast" + (kind ? ` toast-${kind}` : "");
el.innerHTML = msg;
stack.appendChild(el);
setTimeout(() => {
el.classList.add("toast-out");
el.addEventListener("animationend", () => el.remove(), { once: true });
}, 3400);
}
function avatarSvg(hue, size = 18) {
return `<svg class="avatar" width="${size}" height="${size}" viewBox="0 0 18 18" aria-hidden="true">
<rect width="18" height="18" rx="9" fill="hsl(${hue} 70% 22%)"/>
<circle cx="6.5" cy="7" r="3" fill="hsl(${hue} 85% 62%)"/>
<circle cx="12" cy="11.5" r="4" fill="hsl(${(hue + 60) % 360} 80% 55%)" opacity="0.85"/>
</svg>`;
}
function countdownText(endsAt) {
let ms = Math.max(0, endsAt - Date.now());
const days = Math.floor(ms / d);
ms -= days * d;
const hours = Math.floor(ms / h);
ms -= hours * h;
const mins = Math.floor(ms / 60000);
const secs = Math.floor((ms % 60000) / 1000);
if (days > 0) return `${days}d ${hours}h ${mins}m`;
if (hours > 0) return `${hours}h ${mins}m ${secs}s`;
return `${mins}m ${secs}s`;
}
function animateNumber(el, from, to, ms = 700) {
const t0 = performance.now();
const step = (t) => {
const p = Math.min(1, (t - t0) / ms);
const eased = 1 - Math.pow(1 - p, 3);
el.textContent = fmt(from + (to - from) * eased);
if (p < 1) requestAnimationFrame(step);
};
requestAnimationFrame(step);
}
const totalVotes = (p) => p.for + p.against + p.abstain;
function pct(p) {
const t = totalVotes(p);
if (t === 0) return { for: 0, against: 0, abstain: 0 };
return {
for: (p.for / t) * 100,
against: (p.against / t) * 100,
abstain: (p.abstain / t) * 100,
};
}
// ---------- Render: proposal list ----------
function voteBarHtml(p) {
const s = pct(p);
const t = totalVotes(p);
const quorumPct = Math.min(100, (t / QUORUM) * 100);
const quorumMet = t >= QUORUM;
return `
<div class="vote-bar" role="img"
aria-label="For ${s.for.toFixed(1)} percent, Against ${s.against.toFixed(1)} percent, Abstain ${s.abstain.toFixed(1)} percent">
<span class="seg seg-for" style="width:${s.for}%"></span>
<span class="seg seg-against" style="width:${s.against}%"></span>
<span class="seg seg-abstain" style="width:${s.abstain}%"></span>
</div>
<div class="vote-legend">
<span class="legend-for"><span class="dot dot-for"></span>For <span class="mono">${s.for.toFixed(1)}%</span></span>
<span class="legend-against"><span class="dot dot-against"></span>Against <span class="mono">${s.against.toFixed(1)}%</span></span>
<span><span class="dot dot-abstain"></span>Abstain <span class="mono">${s.abstain.toFixed(1)}%</span></span>
</div>
<div class="quorum">
<div class="quorum-row">
<span>Quorum ${quorumMet ? '<span class="quorum-met">· reached ✓</span>' : ""}</span>
<span class="mono">${fmtCompact(t)} / ${fmtCompact(QUORUM)} vNOVA</span>
</div>
<div class="quorum-track"><span class="quorum-fill" style="width:${quorumPct}%"></span></div>
</div>`;
}
function statusMeta(p) {
if (p.status === "active") {
const ending = p.endsAt - Date.now() < d;
return `<span class="countdown mono${ending ? " ending" : ""}" data-ends="${p.endsAt}">⏳ ${countdownText(p.endsAt)} left</span>`;
}
if (p.status === "pending") return `<span class="ended-at">${esc(p.startsLabel)}</span>`;
return `<span class="ended-at">${esc(p.endedLabel)}</span>`;
}
function cardHtml(p) {
return `
<button type="button" class="proposal-card${p.id === selectedId ? " is-selected" : ""}"
data-id="${p.id}" aria-pressed="${p.id === selectedId}">
<div class="card-top">
<span class="status-pill status-${p.status}">${p.status}</span>
<span class="pid mono">${p.id}</span>
${statusMeta(p)}
</div>
<h2 class="card-title">${esc(p.title)}</h2>
<div class="proposer-row">
<span>by</span>
<span class="proposer-chip">${avatarSvg(p.proposer.hue)}<span class="mono">${esc(p.proposer.name)}</span></span>
${p.myVote ? `<span class="voted-badge">✓ you voted ${p.myVote}</span>` : ""}
</div>
${p.status === "pending" ? '<p class="cast-note" style="margin:0">No votes yet — voting has not opened.</p>' : voteBarHtml(p)}
</button>`;
}
function renderList() {
const list = $("#proposalList");
const visible = proposals.filter((p) => activeFilter === "all" || p.status === activeFilter);
list.innerHTML = visible.length
? visible.map(cardHtml).join("")
: '<div class="empty-state">No proposals match this filter.</div>';
list.querySelectorAll(".proposal-card").forEach((card) => {
card.addEventListener("click", () => {
selectedId = card.dataset.id;
renderList();
renderDetail();
if (window.matchMedia("(max-width: 1020px)").matches) {
$("#detailPanel").scrollIntoView({ behavior: "smooth", block: "start" });
}
});
});
}
function renderCounts() {
const counts = { all: proposals.length, active: 0, passed: 0, failed: 0, pending: 0 };
proposals.forEach((p) => counts[p.status]++);
document.querySelectorAll("[data-count]").forEach((el) => {
el.textContent = counts[el.dataset.count];
});
$("#activeCount").textContent = counts.active;
$("#votedCount").textContent = proposals.filter((p) => p.myVote).length;
}
// ---------- Render: detail panel ----------
function resultRow(label, cls, value, total, color) {
const w = total === 0 ? 0 : (value / total) * 100;
return `
<div class="result-row">
<span class="label label-${cls}">${label}</span>
<div class="result-track"><span class="result-fill" style="width:${w}%;background:${color}"></span></div>
<span class="amt mono">${fmtCompact(value)}</span>
</div>`;
}
function castSection(p) {
if (p.status === "pending") {
return `<div class="cast"><div class="closed-note">Voting has not opened yet — ${esc(p.startsLabel.toLowerCase())}.</div></div>`;
}
if (p.status !== "active") {
return `<div class="cast"><div class="closed-note">Voting closed. ${esc(p.endedLabel)}.${
p.myVote ? ` You voted <strong style="color:var(--text)">${p.myVote}</strong>.` : ""
}</div></div>`;
}
if (p.myVote) {
return `
<div class="cast">
<h3>Your vote</h3>
<div class="vote-receipt">
<span>✓ Voted ${p.myVote} with ${fmt(myPower)} vNOVA</span>
<span class="mono">tx ${p.txHash || "0x84c1…0b2e"}</span>
</div>
</div>`;
}
return `
<div class="cast">
<h3>Cast your vote</h3>
<p class="cast-note">You are signing an off-chain ballot worth <strong class="mono" style="color:var(--text)">${fmt(myPower)} vNOVA</strong>. Votes are final and cannot be changed for this proposal.</p>
<div class="vote-actions">
<button type="button" class="vote-btn vote-for" data-vote="for">For</button>
<button type="button" class="vote-btn vote-against" data-vote="against">Against</button>
<button type="button" class="vote-btn vote-abstain" data-vote="abstain">Abstain</button>
</div>
</div>`;
}
function renderDetail() {
const panel = $("#detailPanel");
const p = proposals.find((x) => x.id === selectedId);
if (!p) {
panel.hidden = true;
return;
}
panel.hidden = false;
const t = totalVotes(p);
panel.innerHTML = `
<div class="detail-head">
<div class="detail-id-row">
<span class="status-pill status-${p.status}">${p.status}</span>
<span class="pid mono">${p.id}</span>
</div>
<button type="button" class="icon-btn" id="detailClose" aria-label="Close detail panel">
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" aria-hidden="true">
<path d="M6 6l12 12M18 6 6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</div>
<div class="detail-body">
<h2 class="detail-title">${esc(p.title)}</h2>
<p class="detail-desc">${esc(p.description)}</p>
<div class="detail-meta">
<div class="meta-cell"><span class="k">Proposer</span><span class="v mono">${esc(p.proposer.addr)}</span></div>
<div class="meta-cell"><span class="k">${p.status === "active" ? "Time left" : "Status"}</span>
<span class="v mono" ${p.status === "active" ? `data-ends-detail="${p.endsAt}"` : ""}>${
p.status === "active" ? countdownText(p.endsAt) : p.status === "pending" ? "review window" : p.endedLabel.split("·")[1]?.trim() || p.status
}</span></div>
<div class="meta-cell"><span class="k">Turnout</span><span class="v mono">${fmtCompact(t)} vNOVA</span></div>
<div class="meta-cell"><span class="k">Quorum</span><span class="v mono ${t >= QUORUM ? "quorum-met" : ""}">${Math.min(100, (t / QUORUM) * 100).toFixed(0)}% ${t >= QUORUM ? "✓" : ""}</span></div>
</div>
<div class="meta-cell" style="margin-bottom:16px"><span class="k">On-chain action</span><span class="v mono" style="font-size:12px;font-weight:500">${esc(p.actions)}</span></div>
<div class="results">
<h3>Current results</h3>
${resultRow("For", "for", p.for, t, "linear-gradient(90deg,#1fae67,var(--pos))")}
${resultRow("Against", "against", p.against, t, "linear-gradient(90deg,var(--neg),#d63a58)")}
${resultRow("Abstain", "abstain", p.abstain, t, "rgba(138,144,162,0.55)")}
</div>
${castSection(p)}
</div>`;
$("#detailClose", panel).addEventListener("click", () => {
selectedId = null;
panel.hidden = true;
renderList();
});
panel.querySelectorAll(".vote-btn").forEach((btn) => {
btn.addEventListener("click", () => castVote(p, btn.dataset.vote));
});
}
// ---------- Vote ----------
function castVote(p, choice) {
if (p.status !== "active" || p.myVote) return;
p.myVote = choice;
p[choice] += myPower;
p.txHash = randHash();
const kind = choice === "for" ? "pos" : choice === "against" ? "neg" : "accent";
toast(
`Ballot signed — <strong>${choice.toUpperCase()}</strong> on <span class="mono">${p.id}</span> with <span class="mono">${fmt(myPower)}</span> vNOVA`,
kind
);
// pulse the voting power stat to show it was applied
const powerEl = $("#votingPower");
animateNumber(powerEl, 0, myPower, 800);
renderCounts();
renderList();
renderDetail();
}
// ---------- Filters ----------
document.querySelectorAll(".filter-tab").forEach((tab) => {
tab.addEventListener("click", () => {
activeFilter = tab.dataset.filter;
document.querySelectorAll(".filter-tab").forEach((t) => {
const on = t === tab;
t.classList.toggle("is-active", on);
t.setAttribute("aria-selected", String(on));
});
renderList();
});
});
// ---------- Countdowns ----------
setInterval(() => {
document.querySelectorAll("[data-ends]").forEach((el) => {
const ends = Number(el.dataset.ends);
el.innerHTML = `⏳ ${countdownText(ends)} left`;
el.classList.toggle("ending", ends - Date.now() < d);
});
document.querySelectorAll("[data-ends-detail]").forEach((el) => {
el.textContent = countdownText(Number(el.dataset.endsDetail));
});
}, 1000);
// ---------- Create proposal modal (stub) ----------
const modal = $("#proposalModal");
let lastFocus = null;
function openModal() {
lastFocus = document.activeElement;
modal.hidden = false;
$("#propTitle").focus();
}
function closeModal() {
modal.hidden = true;
if (lastFocus) lastFocus.focus();
}
$("#newProposalBtn").addEventListener("click", openModal);
$("#modalClose").addEventListener("click", closeModal);
$("#modalCancel").addEventListener("click", closeModal);
modal.addEventListener("click", (e) => {
if (e.target === modal) closeModal();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && !modal.hidden) closeModal();
});
$("#modalSubmit").addEventListener("click", () => {
const title = $("#propTitle").value.trim();
if (!title) {
toast("Add a title before submitting your proposal.", "neg");
$("#propTitle").focus();
return;
}
closeModal();
toast(
`Proposal draft signed — <span class="mono">${randHash()}</span> · 10,000 NOVA bond locked (simulated)`,
"accent"
);
$("#propTitle").value = "";
$("#propSummary").value = "";
});
// ---------- Misc topbar actions ----------
$("#delegateBtn").addEventListener("click", () => {
toast("Delegation manager coming soon — your power stays self-delegated.", "accent");
});
$("#walletChip").addEventListener("click", () => {
toast('Connected as <span class="mono">0x7a3f…c41d</span> on Lumen Chain (simulated)');
});
// ---------- Init ----------
renderCounts();
renderList();
renderDetail();
animateNumber($("#votingPower"), 0, myPower, 900);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lumen Collective — DAO Governance</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="bg-orbs" aria-hidden="true">
<span class="orb orb-a"></span>
<span class="orb orb-b"></span>
</div>
<div class="shell">
<!-- ===== Top bar ===== -->
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none">
<path d="M12 2 21 7v10l-9 5-9-5V7l9-5Z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
<circle cx="12" cy="12" r="3.2" fill="currentColor"/>
</svg>
</span>
<div class="brand-text">
<strong>Lumen Collective</strong>
<span class="brand-sub"><span class="chain-dot" aria-hidden="true"></span>Lumen Chain · Governor v3</span>
</div>
</div>
<div class="topbar-actions">
<button type="button" class="btn btn-ghost" id="delegateBtn">
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" aria-hidden="true">
<path d="M17 8l4 4-4 4M3 12h18" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Delegate
</button>
<button type="button" class="wallet-chip" id="walletChip" aria-label="Connected wallet 0x7a3f…c41d">
<span class="wallet-status" aria-hidden="true"></span>
<span class="mono">0x7a3f…c41d</span>
</button>
</div>
</header>
<!-- ===== Hero / stats ===== -->
<section class="hero" aria-label="Your governance overview">
<article class="stat-card stat-power">
<p class="stat-label">Your voting power</p>
<p class="stat-value mono"><span id="votingPower" data-value="184250">184,250</span> <span class="stat-unit">vNOVA</span></p>
<div class="stat-split">
<span><span class="mono">128,400</span> NOVA held</span>
<span class="split-dot" aria-hidden="true">·</span>
<span><span class="mono">55,850</span> delegated to you</span>
</div>
<div class="power-meter" role="img" aria-label="Your voting power is 0.74 percent of total supply">
<span class="power-fill" style="width:7.4%"></span>
</div>
<p class="stat-foot"><span class="mono">0.74%</span> of total voting supply</p>
</article>
<article class="stat-card">
<p class="stat-label">Treasury value</p>
<p class="stat-value mono">$42.8M</p>
<p class="stat-foot"><span class="delta-pos mono">▲ +2.31%</span> 7d · 61% NOVA / 27% lmUSD / 12% wLUM</p>
</article>
<article class="stat-card">
<p class="stat-label">Active proposals</p>
<p class="stat-value mono" id="activeCount">3</p>
<p class="stat-foot"><span class="mono" id="votedCount">1</span> voted by you · quorum <span class="mono">40.0M</span> vNOVA</p>
</article>
<article class="stat-card stat-cta">
<p class="stat-label">Got an idea?</p>
<p class="stat-cta-text">Proposing requires <span class="mono">100,000</span> vNOVA — you qualify.</p>
<button type="button" class="btn btn-primary" id="newProposalBtn">
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" aria-hidden="true">
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
New proposal
</button>
</article>
</section>
<!-- ===== Filters ===== -->
<nav class="filters" role="tablist" aria-label="Filter proposals by status">
<button type="button" class="filter-tab is-active" role="tab" aria-selected="true" data-filter="all">All <span class="count mono" data-count="all"></span></button>
<button type="button" class="filter-tab" role="tab" aria-selected="false" data-filter="active">Active <span class="count mono" data-count="active"></span></button>
<button type="button" class="filter-tab" role="tab" aria-selected="false" data-filter="passed">Passed <span class="count mono" data-count="passed"></span></button>
<button type="button" class="filter-tab" role="tab" aria-selected="false" data-filter="failed">Failed <span class="count mono" data-count="failed"></span></button>
<button type="button" class="filter-tab" role="tab" aria-selected="false" data-filter="pending">Pending <span class="count mono" data-count="pending"></span></button>
</nav>
<!-- ===== Main layout: list + detail ===== -->
<main class="layout">
<section class="proposal-list" id="proposalList" aria-label="Proposals" aria-live="polite">
<!-- proposal cards injected by script.js -->
</section>
<aside class="detail-panel" id="detailPanel" aria-label="Proposal detail" hidden>
<!-- detail injected by script.js -->
</aside>
</main>
<footer class="page-foot">
<span>Lumen Collective Governor v3 · <span class="mono">0x1c9b…e770</span></span>
<span>UI simulation — no real chain calls</span>
</footer>
</div>
<!-- ===== Create proposal modal (stub) ===== -->
<div class="modal-overlay" id="proposalModal" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<div class="modal-head">
<h2 id="modalTitle">Create proposal</h2>
<button type="button" class="icon-btn" id="modalClose" aria-label="Close dialog">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" aria-hidden="true">
<path d="M6 6l12 12M18 6 6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</div>
<div class="modal-body">
<label class="field">
<span class="field-label">Title</span>
<input type="text" id="propTitle" placeholder="LIP-48: Your proposal title" maxlength="90" />
</label>
<label class="field">
<span class="field-label">Summary</span>
<textarea id="propSummary" rows="4" placeholder="What should the DAO do, and why?"></textarea>
</label>
<div class="field-row">
<label class="field">
<span class="field-label">Voting period</span>
<select id="propPeriod">
<option>3 days</option>
<option selected>5 days</option>
<option>7 days</option>
</select>
</label>
<label class="field">
<span class="field-label">Action type</span>
<select id="propAction">
<option>Signal only</option>
<option>Treasury transfer</option>
<option>Parameter change</option>
</select>
</label>
</div>
<div class="risk-note" role="note">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" aria-hidden="true">
<path d="M12 9v4m0 4h.01M10.3 3.9 1.8 18.6a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<p>Submitting locks a <strong class="mono">10,000 NOVA</strong> proposal bond until voting ends. The bond is slashed if the Security Council flags the proposal as spam.</p>
</div>
</div>
<div class="modal-foot">
<button type="button" class="btn btn-ghost" id="modalCancel">Cancel</button>
<button type="button" class="btn btn-primary" id="modalSubmit">Sign & submit <span class="mono modal-bond">−10,000 NOVA</span></button>
</div>
</div>
</div>
<div class="toast-stack" id="toastStack" aria-live="polite" aria-atomic="false"></div>
<script src="script.js"></script>
</body>
</html>DAO Governance (proposals · vote)
A complete governance front-end for a fictional DAO, the Lumen Collective on Lumen Chain. The hero strip leads with a gradient-bordered voting-power card (held NOVA plus tokens delegated to you, with an animated counter and a share-of-supply meter), alongside treasury value, the live active-proposal count, and a qualifying-threshold call-to-action to create a new proposal. Below, pill-shaped status tabs filter the list between All, Active, Passed, Failed, and Pending, each tab carrying a live count badge.
Every proposal card stacks a pulsing status pill, monospace LIP identifier, and a ticking countdown over the title, a proposer chip with a generated avatar, and a three-segment For/Against/Abstain vote bar with percentage legend and a quorum progress track that flips to a green check once turnout crosses 40M vNOVA. Clicking a card opens a sticky, gradient-bordered detail panel with the full description, proposer address, turnout and quorum stats, the encoded on-chain action, and per-option result bars.
Active proposals expose Vote For / Against / Abstain buttons: casting a ballot adds your full voting power to the chosen tally, re-renders the bars and quorum meters, swaps the buttons for a signed receipt with a fake transaction hash, and raises a color-coded toast. The create-proposal modal stubs out the submission flow with title, summary, period and action fields plus an explicit bond-slashing risk note, and the script also handles countdown ticks, filter state, Escape/overlay dismissal, and focus return.
UI-only simulation — no real wallet, RPC, or on-chain calls. Mock data, fictional tokens.