- Documents recent infrastructure cleanup (8 CTs destroyed, 35 DNS records removed, Headscale cleanup) - Adds 24 new runbooks covering Authentik, PeerTube, Meshtastic, RECON, Proxmox, Mailcow, Internet Archive, GPU routing - Adds project documentation for headscale, vaultwarden, peertube, matrix, mmud, advbbs, arr stack - Updates services.md, environment.md, caddy.md, authentik.md to match live infrastructure - Removes 4 deprecated runbook duplicates (canonical versions live in projects/) - Adds .gitignore for binary archives and editor temp files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1442 lines
38 KiB
HTML
1442 lines
38 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>The Last Ember — meshMUD</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Crimson+Text:ital,wght@0,400;0,600;1,400&family=JetBrains+Mono:wght@300;400;500&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--ember: #e8713a;
|
||
--ember-glow: #ff9d5c;
|
||
--ember-deep: #c44e1a;
|
||
--ash: #1a1714;
|
||
--charcoal: #0d0b09;
|
||
--smoke: #2a2520;
|
||
--smoke-light: #3d3630;
|
||
--parchment: #d4c4a8;
|
||
--parchment-dark: #b8a88c;
|
||
--parchment-faded: #a89878;
|
||
--bone: #c8b898;
|
||
--blood: #8b2020;
|
||
--blood-bright: #cc3333;
|
||
--gold: #c4a44a;
|
||
--gold-dim: #8a7a3a;
|
||
--frost: #7a9ab0;
|
||
--poison: #5a8a4a;
|
||
--text-bright: #e8dcc8;
|
||
--text-dim: #9a8e78;
|
||
--text-ghost: #5a5244;
|
||
}
|
||
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
||
body {
|
||
background: var(--charcoal);
|
||
color: var(--text-bright);
|
||
font-family: 'Crimson Text', Georgia, serif;
|
||
min-height: 100vh;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
/* ═══ EMBER PARTICLE CANVAS ═══ */
|
||
#ember-canvas {
|
||
position: fixed;
|
||
top: 0; left: 0;
|
||
width: 100%; height: 100%;
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
/* ═══ TEXTURE OVERLAY ═══ */
|
||
body::before {
|
||
content: '';
|
||
position: fixed;
|
||
top: 0; left: 0;
|
||
width: 100%; height: 100%;
|
||
background:
|
||
repeating-linear-gradient(
|
||
0deg,
|
||
transparent,
|
||
transparent 2px,
|
||
rgba(0,0,0,0.03) 2px,
|
||
rgba(0,0,0,0.03) 4px
|
||
);
|
||
pointer-events: none;
|
||
z-index: 1;
|
||
}
|
||
|
||
.page-wrap {
|
||
position: relative;
|
||
z-index: 2;
|
||
max-width: 1000px;
|
||
margin: 0 auto;
|
||
padding: 0 24px;
|
||
}
|
||
|
||
/* ═══ HEADER ═══ */
|
||
.tavern-header {
|
||
text-align: center;
|
||
padding: 60px 0 20px;
|
||
position: relative;
|
||
}
|
||
|
||
.tavern-sigil {
|
||
width: 48px;
|
||
height: 48px;
|
||
margin: 0 auto 20px;
|
||
position: relative;
|
||
}
|
||
|
||
.tavern-sigil::before {
|
||
content: '🜂';
|
||
font-size: 42px;
|
||
display: block;
|
||
filter: drop-shadow(0 0 12px rgba(232, 113, 58, 0.6));
|
||
animation: sigil-pulse 4s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes sigil-pulse {
|
||
0%, 100% { opacity: 0.7; filter: drop-shadow(0 0 8px rgba(232, 113, 58, 0.4)); }
|
||
50% { opacity: 1; filter: drop-shadow(0 0 16px rgba(232, 113, 58, 0.8)); }
|
||
}
|
||
|
||
.tavern-name {
|
||
font-family: 'Cinzel', serif;
|
||
font-size: clamp(28px, 5vw, 44px);
|
||
font-weight: 700;
|
||
letter-spacing: 0.12em;
|
||
color: var(--parchment);
|
||
text-shadow:
|
||
0 0 40px rgba(232, 113, 58, 0.3),
|
||
0 2px 4px rgba(0,0,0,0.5);
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.tavern-subtitle {
|
||
font-family: 'Cinzel', serif;
|
||
font-size: 13px;
|
||
letter-spacing: 0.3em;
|
||
color: var(--text-ghost);
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.divider {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
margin: 28px 0;
|
||
color: var(--text-ghost);
|
||
font-size: 11px;
|
||
letter-spacing: 0.2em;
|
||
}
|
||
|
||
.divider::before, .divider::after {
|
||
content: '';
|
||
flex: 1;
|
||
height: 1px;
|
||
background: linear-gradient(90deg, transparent, var(--smoke-light), transparent);
|
||
}
|
||
|
||
/* ═══ EPOCH STATUS BAR ═══ */
|
||
.epoch-bar {
|
||
display: grid;
|
||
grid-template-columns: 1fr auto 1fr;
|
||
align-items: center;
|
||
gap: 20px;
|
||
padding: 16px 24px;
|
||
background: linear-gradient(135deg, rgba(26,23,20,0.9), rgba(42,37,32,0.7));
|
||
border: 1px solid var(--smoke-light);
|
||
border-radius: 2px;
|
||
margin-bottom: 32px;
|
||
position: relative;
|
||
}
|
||
|
||
.epoch-bar::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0; left: 0; right: 0;
|
||
height: 1px;
|
||
background: linear-gradient(90deg, transparent, var(--ember), transparent);
|
||
opacity: 0.4;
|
||
}
|
||
|
||
.epoch-stat {
|
||
text-align: center;
|
||
}
|
||
|
||
.epoch-stat .label {
|
||
font-family: 'Cinzel', serif;
|
||
font-size: 9px;
|
||
letter-spacing: 0.25em;
|
||
color: var(--text-ghost);
|
||
text-transform: uppercase;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.epoch-stat .value {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 18px;
|
||
font-weight: 500;
|
||
color: var(--parchment);
|
||
}
|
||
|
||
.epoch-stat .value.ember { color: var(--ember-glow); }
|
||
.epoch-stat .value.gold { color: var(--gold); }
|
||
|
||
.epoch-center-divider {
|
||
width: 1px;
|
||
height: 36px;
|
||
background: var(--smoke-light);
|
||
}
|
||
|
||
.epoch-mode-badge {
|
||
display: inline-block;
|
||
padding: 3px 12px;
|
||
border: 1px solid var(--gold-dim);
|
||
font-family: 'Cinzel', serif;
|
||
font-size: 11px;
|
||
letter-spacing: 0.15em;
|
||
color: var(--gold);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.breach-status {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 11px;
|
||
color: var(--text-ghost);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.breach-status .dot {
|
||
width: 6px; height: 6px;
|
||
border-radius: 50%;
|
||
background: var(--text-ghost);
|
||
}
|
||
|
||
.breach-status .dot.open {
|
||
background: var(--ember);
|
||
box-shadow: 0 0 6px var(--ember);
|
||
animation: dot-pulse 2s ease-in-out infinite;
|
||
}
|
||
|
||
.breach-status .dot.sealed {
|
||
background: var(--smoke-light);
|
||
}
|
||
|
||
@keyframes dot-pulse {
|
||
0%, 100% { opacity: 0.6; }
|
||
50% { opacity: 1; }
|
||
}
|
||
|
||
/* ═══ MAIN GRID ═══ */
|
||
.board-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 24px;
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
@media (max-width: 700px) {
|
||
.board-grid { grid-template-columns: 1fr; }
|
||
}
|
||
|
||
.board-panel {
|
||
background: linear-gradient(180deg, rgba(26,23,20,0.95), rgba(13,11,9,0.95));
|
||
border: 1px solid var(--smoke-light);
|
||
border-radius: 2px;
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
.board-panel::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0; left: 0; right: 0;
|
||
height: 1px;
|
||
background: linear-gradient(90deg, transparent, var(--smoke-light), transparent);
|
||
}
|
||
|
||
.panel-header {
|
||
padding: 14px 18px 10px;
|
||
border-bottom: 1px solid rgba(90,82,68,0.2);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.panel-title {
|
||
font-family: 'Cinzel', serif;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.2em;
|
||
color: var(--parchment-faded);
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.panel-icon {
|
||
font-size: 14px;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.panel-body {
|
||
padding: 14px 18px 18px;
|
||
}
|
||
|
||
/* ═══ LEADERBOARD ═══ */
|
||
.leaderboard-entry {
|
||
display: grid;
|
||
grid-template-columns: 20px 1fr auto;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid rgba(90,82,68,0.1);
|
||
}
|
||
|
||
.leaderboard-entry:last-child { border-bottom: none; }
|
||
|
||
.lb-rank {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 11px;
|
||
color: var(--text-ghost);
|
||
text-align: center;
|
||
}
|
||
|
||
.lb-rank.first { color: var(--gold); }
|
||
.lb-rank.second { color: var(--parchment-dark); }
|
||
.lb-rank.third { color: var(--ember-deep); }
|
||
|
||
.lb-name {
|
||
font-family: 'Crimson Text', serif;
|
||
font-size: 15px;
|
||
color: var(--text-bright);
|
||
}
|
||
|
||
.lb-title {
|
||
font-size: 12px;
|
||
font-style: italic;
|
||
color: var(--text-ghost);
|
||
}
|
||
|
||
.lb-stats {
|
||
display: flex;
|
||
gap: 12px;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 10px;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.lb-stat-val { color: var(--parchment-dark); }
|
||
|
||
.class-badge {
|
||
display: inline-block;
|
||
padding: 1px 5px;
|
||
border-radius: 1px;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 9px;
|
||
letter-spacing: 0.05em;
|
||
margin-left: 6px;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.class-badge.fighter { background: rgba(139,32,32,0.3); color: var(--blood-bright); border: 1px solid rgba(139,32,32,0.4); }
|
||
.class-badge.caster { background: rgba(90,138,74,0.2); color: var(--poison); border: 1px solid rgba(90,138,74,0.3); }
|
||
.class-badge.rogue { background: rgba(122,154,176,0.2); color: var(--frost); border: 1px solid rgba(122,154,176,0.3); }
|
||
|
||
/* ═══ BROADCAST LOG ═══ */
|
||
.broadcast-log {
|
||
max-height: 320px;
|
||
overflow-y: auto;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: var(--smoke-light) transparent;
|
||
}
|
||
|
||
.broadcast-entry {
|
||
padding: 7px 0;
|
||
border-bottom: 1px solid rgba(90,82,68,0.08);
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: flex-start;
|
||
animation: broadcast-in 0.4s ease-out;
|
||
}
|
||
|
||
@keyframes broadcast-in {
|
||
from { opacity: 0; transform: translateY(-4px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.broadcast-entry:last-child { border-bottom: none; }
|
||
|
||
.bc-icon { font-size: 13px; flex-shrink: 0; margin-top: 1px; }
|
||
|
||
.bc-text {
|
||
font-size: 14px;
|
||
line-height: 1.4;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.bc-text .bc-name { color: var(--parchment); font-weight: 600; }
|
||
.bc-text .bc-highlight { color: var(--ember-glow); }
|
||
.bc-text .bc-item { color: var(--gold); font-style: italic; }
|
||
|
||
.bc-time {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 9px;
|
||
color: var(--text-ghost);
|
||
margin-left: auto;
|
||
flex-shrink: 0;
|
||
margin-top: 3px;
|
||
}
|
||
|
||
/* ═══ BOUNTY BOARD ═══ */
|
||
.bounty-card {
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid rgba(90,82,68,0.15);
|
||
}
|
||
|
||
.bounty-card:last-child { border-bottom: none; }
|
||
|
||
.bounty-name {
|
||
font-family: 'Cinzel', serif;
|
||
font-size: 13px;
|
||
color: var(--parchment);
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.bounty-location {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 10px;
|
||
color: var(--text-ghost);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.hp-bar-wrap {
|
||
height: 8px;
|
||
background: var(--smoke);
|
||
border-radius: 1px;
|
||
overflow: hidden;
|
||
position: relative;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.hp-bar-fill {
|
||
height: 100%;
|
||
border-radius: 1px;
|
||
transition: width 1s ease;
|
||
position: relative;
|
||
}
|
||
|
||
.hp-bar-fill.high { background: linear-gradient(90deg, var(--blood), var(--blood-bright)); }
|
||
.hp-bar-fill.mid { background: linear-gradient(90deg, var(--ember-deep), var(--ember)); }
|
||
.hp-bar-fill.low { background: linear-gradient(90deg, var(--gold-dim), var(--gold)); }
|
||
|
||
.hp-bar-fill::after {
|
||
content: '';
|
||
position: absolute;
|
||
right: 0; top: 0; bottom: 0;
|
||
width: 20px;
|
||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.15));
|
||
}
|
||
|
||
.bounty-hp-text {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 10px;
|
||
color: var(--text-dim);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.bounty-contributors {
|
||
font-size: 11px;
|
||
color: var(--text-ghost);
|
||
margin-top: 4px;
|
||
font-style: italic;
|
||
}
|
||
|
||
/* ═══ MODE STATUS (FULL WIDTH) ═══ */
|
||
.board-full {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
/* Hold the Line bars */
|
||
.floor-row {
|
||
display: grid;
|
||
grid-template-columns: 70px 1fr 50px;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.floor-label {
|
||
font-family: 'Cinzel', serif;
|
||
font-size: 11px;
|
||
color: var(--text-dim);
|
||
letter-spacing: 0.1em;
|
||
}
|
||
|
||
.floor-bar-wrap {
|
||
height: 14px;
|
||
background: var(--smoke);
|
||
border-radius: 1px;
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
.floor-bar-fill {
|
||
height: 100%;
|
||
transition: width 1.5s ease;
|
||
position: relative;
|
||
}
|
||
|
||
.floor-bar-fill.f1 { background: linear-gradient(90deg, #2a4a2a, #4a8a4a); }
|
||
.floor-bar-fill.f2 { background: linear-gradient(90deg, #4a3a1a, #8a7a3a); }
|
||
.floor-bar-fill.f3 { background: linear-gradient(90deg, #5a2a1a, #c44e1a); }
|
||
.floor-bar-fill.f4 { background: linear-gradient(90deg, #3a1a1a, #8b2020); }
|
||
|
||
.floor-checkpoint {
|
||
position: absolute;
|
||
top: 0; bottom: 0;
|
||
width: 2px;
|
||
background: var(--gold);
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.floor-checkpoint.locked {
|
||
opacity: 1;
|
||
box-shadow: 0 0 4px var(--gold);
|
||
}
|
||
|
||
.floor-pct {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
text-align: right;
|
||
}
|
||
|
||
/* Raid boss bar */
|
||
.raid-boss-section { text-align: center; padding: 8px 0; }
|
||
|
||
.raid-boss-name {
|
||
font-family: 'Cinzel', serif;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: var(--parchment);
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.raid-boss-phase {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 10px;
|
||
color: var(--ember);
|
||
letter-spacing: 0.15em;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.raid-hp-bar-wrap {
|
||
height: 20px;
|
||
background: var(--smoke);
|
||
border-radius: 1px;
|
||
overflow: hidden;
|
||
position: relative;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.raid-hp-fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, var(--blood), var(--blood-bright), var(--ember-deep));
|
||
transition: width 2s ease;
|
||
position: relative;
|
||
}
|
||
|
||
.raid-hp-fill::after {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
background: repeating-linear-gradient(
|
||
90deg,
|
||
transparent,
|
||
transparent 30px,
|
||
rgba(0,0,0,0.15) 30px,
|
||
rgba(0,0,0,0.15) 31px
|
||
);
|
||
}
|
||
|
||
.raid-hp-markers {
|
||
position: absolute;
|
||
top: 0; left: 0; right: 0; bottom: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.raid-phase-marker {
|
||
position: absolute;
|
||
top: 0; bottom: 0;
|
||
width: 2px;
|
||
background: rgba(255,255,255,0.3);
|
||
}
|
||
|
||
.raid-hp-text {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 12px;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.raid-contributors {
|
||
font-size: 12px;
|
||
color: var(--text-ghost);
|
||
margin-top: 8px;
|
||
font-style: italic;
|
||
}
|
||
|
||
/* R&E carrier tracker */
|
||
.re-tracker {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 12px 0;
|
||
justify-content: center;
|
||
}
|
||
|
||
.re-floor {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.re-floor-label {
|
||
font-family: 'Cinzel', serif;
|
||
font-size: 9px;
|
||
color: var(--text-ghost);
|
||
letter-spacing: 0.1em;
|
||
}
|
||
|
||
.re-rooms {
|
||
display: flex;
|
||
gap: 2px;
|
||
}
|
||
|
||
.re-room {
|
||
width: 12px; height: 12px;
|
||
border: 1px solid var(--smoke-light);
|
||
border-radius: 1px;
|
||
background: var(--smoke);
|
||
position: relative;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.re-room.carrier {
|
||
background: var(--ember);
|
||
border-color: var(--ember-glow);
|
||
box-shadow: 0 0 6px var(--ember);
|
||
}
|
||
|
||
.re-room.pursuer {
|
||
background: var(--blood);
|
||
border-color: var(--blood-bright);
|
||
box-shadow: 0 0 6px var(--blood);
|
||
}
|
||
|
||
.re-room.cleared {
|
||
background: rgba(74,138,74,0.3);
|
||
border-color: rgba(74,138,74,0.5);
|
||
}
|
||
|
||
.re-room.warded {
|
||
background: rgba(90,138,74,0.2);
|
||
border-color: var(--poison);
|
||
}
|
||
|
||
.re-floor-sep {
|
||
width: 20px;
|
||
height: 1px;
|
||
background: var(--smoke-light);
|
||
margin: 0 4px;
|
||
margin-top: 14px;
|
||
}
|
||
|
||
.re-legend {
|
||
display: flex;
|
||
gap: 16px;
|
||
justify-content: center;
|
||
margin-top: 12px;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 9px;
|
||
color: var(--text-ghost);
|
||
}
|
||
|
||
.re-legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.re-legend-dot {
|
||
width: 8px; height: 8px;
|
||
border-radius: 1px;
|
||
}
|
||
|
||
.re-legend-dot.carrier-dot { background: var(--ember); }
|
||
.re-legend-dot.pursuer-dot { background: var(--blood); }
|
||
.re-legend-dot.cleared-dot { background: rgba(74,138,74,0.5); }
|
||
|
||
.re-status-text {
|
||
text-align: center;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 11px;
|
||
color: var(--ember-glow);
|
||
margin-top: 8px;
|
||
}
|
||
|
||
/* ═══ NPC STRIP ═══ */
|
||
.npc-strip {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 16px;
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
@media (max-width: 700px) {
|
||
.npc-strip { grid-template-columns: repeat(2, 1fr); }
|
||
}
|
||
|
||
.npc-card {
|
||
text-align: center;
|
||
padding: 16px 12px;
|
||
background: linear-gradient(180deg, rgba(42,37,32,0.4), rgba(13,11,9,0.6));
|
||
border: 1px solid rgba(90,82,68,0.15);
|
||
border-radius: 2px;
|
||
transition: border-color 0.3s ease;
|
||
}
|
||
|
||
.npc-card:hover {
|
||
border-color: rgba(232,113,58,0.3);
|
||
}
|
||
|
||
.npc-sigil {
|
||
font-size: 24px;
|
||
margin-bottom: 8px;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.npc-name {
|
||
font-family: 'Cinzel', serif;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--parchment);
|
||
letter-spacing: 0.1em;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.npc-role {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 9px;
|
||
color: var(--text-ghost);
|
||
letter-spacing: 0.15em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
/* ═══ FOOTER ═══ */
|
||
.tavern-footer {
|
||
text-align: center;
|
||
padding: 24px 0 48px;
|
||
border-top: 1px solid rgba(90,82,68,0.15);
|
||
}
|
||
|
||
.footer-mesh {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 10px;
|
||
color: var(--text-ghost);
|
||
letter-spacing: 0.15em;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.footer-mesh a {
|
||
color: var(--text-ghost);
|
||
text-decoration: none;
|
||
transition: color 0.2s;
|
||
}
|
||
|
||
.footer-mesh a:hover { color: var(--ember); }
|
||
|
||
.admin-link {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 8px 20px;
|
||
background: transparent;
|
||
border: 1px solid var(--smoke-light);
|
||
color: var(--text-ghost);
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 10px;
|
||
letter-spacing: 0.15em;
|
||
text-decoration: none;
|
||
text-transform: uppercase;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
border-radius: 1px;
|
||
}
|
||
|
||
.admin-link:hover {
|
||
border-color: var(--ember-deep);
|
||
color: var(--ember);
|
||
background: rgba(232,113,58,0.05);
|
||
}
|
||
|
||
.admin-lock {
|
||
font-size: 11px;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
/* ═══ ADMIN MODAL ═══ */
|
||
.modal-overlay {
|
||
display: none;
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0,0,0,0.85);
|
||
z-index: 100;
|
||
justify-content: center;
|
||
align-items: center;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
|
||
.modal-overlay.active { display: flex; }
|
||
|
||
.modal-box {
|
||
background: var(--ash);
|
||
border: 1px solid var(--smoke-light);
|
||
padding: 36px 32px;
|
||
width: 340px;
|
||
text-align: center;
|
||
position: relative;
|
||
animation: modal-in 0.3s ease-out;
|
||
}
|
||
|
||
@keyframes modal-in {
|
||
from { opacity: 0; transform: translateY(12px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.modal-box::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0; left: 0; right: 0;
|
||
height: 1px;
|
||
background: linear-gradient(90deg, transparent, var(--ember), transparent);
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.modal-title {
|
||
font-family: 'Cinzel', serif;
|
||
font-size: 14px;
|
||
letter-spacing: 0.2em;
|
||
color: var(--parchment-faded);
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.modal-input {
|
||
width: 100%;
|
||
padding: 10px 14px;
|
||
background: var(--charcoal);
|
||
border: 1px solid var(--smoke-light);
|
||
color: var(--text-bright);
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 13px;
|
||
outline: none;
|
||
margin-bottom: 12px;
|
||
border-radius: 1px;
|
||
transition: border-color 0.2s;
|
||
}
|
||
|
||
.modal-input:focus {
|
||
border-color: var(--ember-deep);
|
||
}
|
||
|
||
.modal-input::placeholder {
|
||
color: var(--text-ghost);
|
||
}
|
||
|
||
.modal-btn {
|
||
width: 100%;
|
||
padding: 10px;
|
||
background: linear-gradient(135deg, var(--ember-deep), var(--ember));
|
||
border: none;
|
||
color: var(--charcoal);
|
||
font-family: 'Cinzel', serif;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.15em;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
border-radius: 1px;
|
||
}
|
||
|
||
.modal-btn:hover {
|
||
filter: brightness(1.1);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.modal-close {
|
||
position: absolute;
|
||
top: 12px; right: 14px;
|
||
background: none;
|
||
border: none;
|
||
color: var(--text-ghost);
|
||
font-size: 18px;
|
||
cursor: pointer;
|
||
transition: color 0.2s;
|
||
}
|
||
|
||
.modal-close:hover { color: var(--text-bright); }
|
||
|
||
.modal-error {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 10px;
|
||
color: var(--blood-bright);
|
||
margin-top: 8px;
|
||
min-height: 14px;
|
||
}
|
||
|
||
/* ═══ DAILY DIGEST ═══ */
|
||
.digest-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 10px 18px;
|
||
background: rgba(196,164,74,0.05);
|
||
border: 1px solid rgba(196,164,74,0.15);
|
||
border-radius: 2px;
|
||
margin-bottom: 24px;
|
||
font-size: 13px;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.digest-icon { font-size: 14px; }
|
||
|
||
.digest-text .digest-hl { color: var(--parchment); font-weight: 600; }
|
||
|
||
/* ═══ SECRETS COUNTER ═══ */
|
||
.secrets-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-top: 12px;
|
||
padding-top: 10px;
|
||
border-top: 1px solid rgba(90,82,68,0.1);
|
||
}
|
||
|
||
.secrets-label {
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-size: 10px;
|
||
color: var(--text-ghost);
|
||
letter-spacing: 0.1em;
|
||
}
|
||
|
||
.secrets-dots {
|
||
display: flex;
|
||
gap: 3px;
|
||
}
|
||
|
||
.secret-dot {
|
||
width: 6px; height: 6px;
|
||
border-radius: 50%;
|
||
background: var(--smoke);
|
||
border: 1px solid var(--smoke-light);
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.secret-dot.found {
|
||
background: var(--gold);
|
||
border-color: var(--gold);
|
||
box-shadow: 0 0 4px rgba(196,164,74,0.4);
|
||
}
|
||
|
||
/* scrollbar */
|
||
.broadcast-log::-webkit-scrollbar { width: 4px; }
|
||
.broadcast-log::-webkit-scrollbar-track { background: transparent; }
|
||
.broadcast-log::-webkit-scrollbar-thumb { background: var(--smoke-light); border-radius: 2px; }
|
||
|
||
/* ═══ LOADING STATES ═══ */
|
||
.skeleton {
|
||
background: linear-gradient(90deg, var(--smoke) 25%, var(--smoke-light) 50%, var(--smoke) 75%);
|
||
background-size: 200% 100%;
|
||
animation: shimmer 1.5s infinite;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
@keyframes shimmer {
|
||
0% { background-position: -200% 0; }
|
||
100% { background-position: 200% 0; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<canvas id="ember-canvas"></canvas>
|
||
|
||
<div class="page-wrap">
|
||
|
||
<!-- HEADER -->
|
||
<header class="tavern-header">
|
||
<div class="tavern-sigil"></div>
|
||
<h1 class="tavern-name">The Last Ember</h1>
|
||
<div class="tavern-subtitle">meshMUD</div>
|
||
</header>
|
||
|
||
<div class="divider">◆</div>
|
||
|
||
<!-- EPOCH STATUS BAR -->
|
||
<div class="epoch-bar">
|
||
<div class="epoch-stat">
|
||
<div class="label">Epoch Day</div>
|
||
<div class="value ember" id="epoch-day">17</div>
|
||
<div class="breach-status">
|
||
<span class="dot open" id="breach-dot"></span>
|
||
<span id="breach-text">BREACH OPEN</span>
|
||
</div>
|
||
</div>
|
||
<div class="epoch-center-divider"></div>
|
||
<div class="epoch-stat">
|
||
<div class="label">Active Mode</div>
|
||
<div class="epoch-mode-badge" id="epoch-mode">HOLD THE LINE</div>
|
||
<div style="margin-top:6px">
|
||
<span class="breach-status">
|
||
<span style="font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text-ghost);" id="players-online">7 adventurers this epoch</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="epoch-center-divider"></div>
|
||
<div class="epoch-stat">
|
||
<div class="label">Days Remain</div>
|
||
<div class="value gold" id="days-remain">13</div>
|
||
<div class="breach-status">
|
||
<span style="font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text-ghost);" id="epoch-timer">ends Mar 10</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- DAILY DIGEST -->
|
||
<div class="digest-bar">
|
||
<span class="digest-icon">📜</span>
|
||
<span class="digest-text">
|
||
<span class="digest-hl">Day 16:</span> 3 battles fought, 1 death. The Bounty Troll fell at last.
|
||
<span class="digest-hl">Kael</span> leads at Lv8. Floor 2 holding at 71%.
|
||
</span>
|
||
</div>
|
||
|
||
<!-- MAIN GRID -->
|
||
<div class="board-grid">
|
||
|
||
<!-- LEADERBOARD -->
|
||
<div class="board-panel">
|
||
<div class="panel-header">
|
||
<span class="panel-title">Leaderboard</span>
|
||
<span class="panel-icon">⚔</span>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="leaderboard-entry">
|
||
<span class="lb-rank first">1</span>
|
||
<div>
|
||
<span class="lb-name">Kael</span>
|
||
<span class="class-badge fighter">FGT</span>
|
||
<div class="lb-title">the Unyielding</div>
|
||
</div>
|
||
<div class="lb-stats">
|
||
<span>Lv<span class="lb-stat-val">8</span></span>
|
||
<span>K:<span class="lb-stat-val">47</span></span>
|
||
<span>S:<span class="lb-stat-val">9</span></span>
|
||
</div>
|
||
</div>
|
||
<div class="leaderboard-entry">
|
||
<span class="lb-rank second">2</span>
|
||
<div>
|
||
<span class="lb-name">Mira</span>
|
||
<span class="class-badge caster">CST</span>
|
||
<div class="lb-title">the Keen-Eyed</div>
|
||
</div>
|
||
<div class="lb-stats">
|
||
<span>Lv<span class="lb-stat-val">7</span></span>
|
||
<span>K:<span class="lb-stat-val">31</span></span>
|
||
<span>S:<span class="lb-stat-val">12</span></span>
|
||
</div>
|
||
</div>
|
||
<div class="leaderboard-entry">
|
||
<span class="lb-rank third">3</span>
|
||
<div>
|
||
<span class="lb-name">Torr</span>
|
||
<span class="class-badge rogue">ROG</span>
|
||
<div class="lb-title">the Quiet</div>
|
||
</div>
|
||
<div class="lb-stats">
|
||
<span>Lv<span class="lb-stat-val">6</span></span>
|
||
<span>K:<span class="lb-stat-val">28</span></span>
|
||
<span>S:<span class="lb-stat-val">7</span></span>
|
||
</div>
|
||
</div>
|
||
<div class="leaderboard-entry">
|
||
<span class="lb-rank">4</span>
|
||
<div>
|
||
<span class="lb-name">Sable</span>
|
||
<span class="class-badge fighter">FGT</span>
|
||
</div>
|
||
<div class="lb-stats">
|
||
<span>Lv<span class="lb-stat-val">5</span></span>
|
||
<span>K:<span class="lb-stat-val">19</span></span>
|
||
<span>S:<span class="lb-stat-val">4</span></span>
|
||
</div>
|
||
</div>
|
||
<div class="leaderboard-entry">
|
||
<span class="lb-rank">5</span>
|
||
<div>
|
||
<span class="lb-name">Dren</span>
|
||
<span class="class-badge caster">CST</span>
|
||
</div>
|
||
<div class="lb-stats">
|
||
<span>Lv<span class="lb-stat-val">4</span></span>
|
||
<span>K:<span class="lb-stat-val">12</span></span>
|
||
<span>S:<span class="lb-stat-val">3</span></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Secrets found (server-wide) -->
|
||
<div class="secrets-bar">
|
||
<span class="secrets-label">SECRETS FOUND: 11/20</span>
|
||
<div class="secrets-dots" id="secret-dots">
|
||
<!-- filled by JS -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- BROADCAST LOG -->
|
||
<div class="board-panel">
|
||
<div class="panel-header">
|
||
<span class="panel-title">Broadcasts</span>
|
||
<span class="panel-icon">📡</span>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="broadcast-log" id="broadcast-log">
|
||
<div class="broadcast-entry">
|
||
<span class="bc-icon">🏰</span>
|
||
<span class="bc-text">Floor 2 <span class="bc-highlight">Checkpoint Alpha</span> established. The darkness cannot pass.</span>
|
||
<span class="bc-time">2h</span>
|
||
</div>
|
||
<div class="broadcast-entry">
|
||
<span class="bc-icon">🎯</span>
|
||
<span class="bc-text"><span class="bc-name">Kael</span> finished the <span class="bc-highlight">Bounty Troll</span>. Contributors: Kael, Mira, Torr.</span>
|
||
<span class="bc-time">4h</span>
|
||
</div>
|
||
<div class="broadcast-entry">
|
||
<span class="bc-icon">🔍</span>
|
||
<span class="bc-text"><span class="bc-name">Mira</span> activated the <span class="bc-item">Ancient Ward</span>! Dungeon regen halved for 24h.</span>
|
||
<span class="bc-time">5h</span>
|
||
</div>
|
||
<div class="broadcast-entry">
|
||
<span class="bc-icon">💀</span>
|
||
<span class="bc-text"><span class="bc-name">Sable</span> fell on Floor 2. 34g lost to the depths.</span>
|
||
<span class="bc-time">6h</span>
|
||
</div>
|
||
<div class="broadcast-entry">
|
||
<span class="bc-icon">⚡</span>
|
||
<span class="bc-text">The Breach stirs. Strange light pulses from between Floors 2 and 3.</span>
|
||
<span class="bc-time">8h</span>
|
||
</div>
|
||
<div class="broadcast-entry">
|
||
<span class="bc-icon">⚔</span>
|
||
<span class="bc-text"><span class="bc-name">Torr</span> reached <span class="bc-highlight">Level 6</span>. New ability unlocked: <span class="bc-item">Ambush</span>.</span>
|
||
<span class="bc-time">11h</span>
|
||
</div>
|
||
<div class="broadcast-entry">
|
||
<span class="bc-icon">⚠</span>
|
||
<span class="bc-text">Floor 2 lost 2 rooms. Frontline at Room 9. Next regen in ~6h.</span>
|
||
<span class="bc-time">14h</span>
|
||
</div>
|
||
<div class="broadcast-entry">
|
||
<span class="bc-icon">🔍</span>
|
||
<span class="bc-text"><span class="bc-name">Dren</span> found a hidden shortcut on Floor 1.</span>
|
||
<span class="bc-time">1d</span>
|
||
</div>
|
||
<div class="broadcast-entry">
|
||
<span class="bc-icon">🎯</span>
|
||
<span class="bc-text">New bounty: <span class="bc-highlight">Clear the Spider Nest</span> on Floor 2 (0/6 spiders).</span>
|
||
<span class="bc-time">1d</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- BOUNTY BOARD -->
|
||
<div class="board-panel">
|
||
<div class="panel-header">
|
||
<span class="panel-title">Bounty Board</span>
|
||
<span class="panel-icon">🎯</span>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="bounty-card">
|
||
<div class="bounty-name">Ironhide Basilisk</div>
|
||
<div class="bounty-location">Floor 2 · Room 14 · The Sunken Gallery</div>
|
||
<div class="hp-bar-wrap">
|
||
<div class="hp-bar-fill mid" style="width:62%"></div>
|
||
</div>
|
||
<div class="bounty-hp-text">
|
||
<span>124 / 200 HP</span>
|
||
<span>regen 10hp/8h</span>
|
||
</div>
|
||
<div class="bounty-contributors">Kael, Mira, Sable contributing</div>
|
||
</div>
|
||
<div class="bounty-card">
|
||
<div class="bounty-name">Spider Nest</div>
|
||
<div class="bounty-location">Floor 2 · Eastern Branch</div>
|
||
<div class="hp-bar-wrap">
|
||
<div class="hp-bar-fill high" style="width:83%"></div>
|
||
</div>
|
||
<div class="bounty-hp-text">
|
||
<span>5 / 6 spiders remain</span>
|
||
<span>kill bounty</span>
|
||
</div>
|
||
<div class="bounty-contributors">Torr contributing</div>
|
||
</div>
|
||
<div class="bounty-card" style="opacity:0.4">
|
||
<div class="bounty-name" style="text-decoration:line-through">Bounty Troll</div>
|
||
<div class="bounty-location">Floor 1 · Room 8 · Completed</div>
|
||
<div class="hp-bar-wrap">
|
||
<div class="hp-bar-fill low" style="width:0%"></div>
|
||
</div>
|
||
<div class="bounty-hp-text">
|
||
<span>0 / 200 HP — SLAIN</span>
|
||
<span></span>
|
||
</div>
|
||
<div class="bounty-contributors">Finished by Kael · 4h ago</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MODE STATUS: HOLD THE LINE -->
|
||
<div class="board-panel">
|
||
<div class="panel-header">
|
||
<span class="panel-title">Hold the Line</span>
|
||
<span class="panel-icon">🏰</span>
|
||
</div>
|
||
<div class="panel-body">
|
||
<div class="floor-row">
|
||
<span class="floor-label">Floor 1</span>
|
||
<div class="floor-bar-wrap">
|
||
<div class="floor-bar-fill f1" style="width:100%">
|
||
<div class="floor-checkpoint locked" style="left:30%"></div>
|
||
<div class="floor-checkpoint locked" style="left:60%"></div>
|
||
<div class="floor-checkpoint locked" style="left:95%"></div>
|
||
</div>
|
||
</div>
|
||
<span class="floor-pct">100%</span>
|
||
</div>
|
||
<div class="floor-row">
|
||
<span class="floor-label">Floor 2</span>
|
||
<div class="floor-bar-wrap">
|
||
<div class="floor-bar-fill f2" style="width:71%">
|
||
<div class="floor-checkpoint locked" style="left:42%"></div>
|
||
<div class="floor-checkpoint" style="left:85%"></div>
|
||
</div>
|
||
</div>
|
||
<span class="floor-pct">71%</span>
|
||
</div>
|
||
<div class="floor-row">
|
||
<span class="floor-label">Floor 3</span>
|
||
<div class="floor-bar-wrap">
|
||
<div class="floor-bar-fill f3" style="width:18%">
|
||
<div class="floor-checkpoint" style="left:33%"></div>
|
||
</div>
|
||
</div>
|
||
<span class="floor-pct">18%</span>
|
||
</div>
|
||
<div class="floor-row">
|
||
<span class="floor-label">Floor 4</span>
|
||
<div class="floor-bar-wrap">
|
||
<div class="floor-bar-fill f4" style="width:0%"></div>
|
||
</div>
|
||
<span class="floor-pct" style="color:var(--text-ghost)">locked</span>
|
||
</div>
|
||
|
||
<div style="text-align:center;margin-top:14px;">
|
||
<span style="font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text-ghost);letter-spacing:0.1em;">
|
||
NEXT REGEN TICK: ~4H · FLOOR 2 LOSES 1 ROOM
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- NPC STRIP -->
|
||
<div class="divider">THE REGULARS</div>
|
||
|
||
<div class="npc-strip">
|
||
<div class="npc-card">
|
||
<div class="npc-sigil">🍺</div>
|
||
<div class="npc-name">Grist</div>
|
||
<div class="npc-role">Barkeep</div>
|
||
</div>
|
||
<div class="npc-card">
|
||
<div class="npc-sigil">🩸</div>
|
||
<div class="npc-name">Maren</div>
|
||
<div class="npc-role">Healer</div>
|
||
</div>
|
||
<div class="npc-card">
|
||
<div class="npc-sigil">⚖</div>
|
||
<div class="npc-name">Torval</div>
|
||
<div class="npc-role">Merchant</div>
|
||
</div>
|
||
<div class="npc-card">
|
||
<div class="npc-sigil">👁</div>
|
||
<div class="npc-name">Whisper</div>
|
||
<div class="npc-role">Sage</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- FOOTER -->
|
||
<div class="tavern-footer">
|
||
<div class="footer-mesh">
|
||
meshMUD · LoRa text adventure · <a href="#">meshtastic</a>
|
||
</div>
|
||
<a class="admin-link" id="admin-btn">
|
||
<span class="admin-lock">🔒</span>
|
||
Operator Console
|
||
</a>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- ADMIN LOGIN MODAL -->
|
||
<div class="modal-overlay" id="admin-modal">
|
||
<div class="modal-box">
|
||
<button class="modal-close" id="modal-close">×</button>
|
||
<div class="modal-title">OPERATOR CONSOLE</div>
|
||
<input type="text" class="modal-input" placeholder="callsign" id="admin-user" autocomplete="off" spellcheck="false">
|
||
<input type="password" class="modal-input" placeholder="passphrase" id="admin-pass">
|
||
<button class="modal-btn" id="admin-submit">AUTHENTICATE</button>
|
||
<div class="modal-error" id="admin-error"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// ═══ EMBER PARTICLES ═══
|
||
const canvas = document.getElementById('ember-canvas');
|
||
const ctx = canvas.getContext('2d');
|
||
let embers = [];
|
||
|
||
function resize() {
|
||
canvas.width = window.innerWidth;
|
||
canvas.height = window.innerHeight;
|
||
}
|
||
resize();
|
||
window.addEventListener('resize', resize);
|
||
|
||
class Ember {
|
||
constructor() {
|
||
this.reset();
|
||
}
|
||
reset() {
|
||
this.x = Math.random() * canvas.width;
|
||
this.y = canvas.height + 10;
|
||
this.size = Math.random() * 2.5 + 0.5;
|
||
this.speedY = -(Math.random() * 0.4 + 0.1);
|
||
this.speedX = (Math.random() - 0.5) * 0.3;
|
||
this.opacity = Math.random() * 0.5 + 0.2;
|
||
this.decay = Math.random() * 0.001 + 0.0005;
|
||
this.wobble = Math.random() * Math.PI * 2;
|
||
this.wobbleSpeed = Math.random() * 0.02 + 0.005;
|
||
// Color between deep orange and pale gold
|
||
const t = Math.random();
|
||
this.r = Math.floor(200 + t * 55);
|
||
this.g = Math.floor(80 + t * 80);
|
||
this.b = Math.floor(20 + t * 30);
|
||
}
|
||
update() {
|
||
this.wobble += this.wobbleSpeed;
|
||
this.x += this.speedX + Math.sin(this.wobble) * 0.15;
|
||
this.y += this.speedY;
|
||
this.opacity -= this.decay;
|
||
if (this.opacity <= 0 || this.y < -20) this.reset();
|
||
}
|
||
draw() {
|
||
ctx.beginPath();
|
||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
||
ctx.fillStyle = `rgba(${this.r},${this.g},${this.b},${this.opacity})`;
|
||
ctx.fill();
|
||
// glow
|
||
ctx.beginPath();
|
||
ctx.arc(this.x, this.y, this.size * 3, 0, Math.PI * 2);
|
||
ctx.fillStyle = `rgba(${this.r},${this.g},${this.b},${this.opacity * 0.15})`;
|
||
ctx.fill();
|
||
}
|
||
}
|
||
|
||
for (let i = 0; i < 40; i++) {
|
||
const e = new Ember();
|
||
e.y = Math.random() * canvas.height;
|
||
embers.push(e);
|
||
}
|
||
|
||
function animateEmbers() {
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
embers.forEach(e => { e.update(); e.draw(); });
|
||
requestAnimationFrame(animateEmbers);
|
||
}
|
||
animateEmbers();
|
||
|
||
// ═══ SECRET DOTS ═══
|
||
const secretContainer = document.getElementById('secret-dots');
|
||
const found = 11;
|
||
for (let i = 0; i < 20; i++) {
|
||
const dot = document.createElement('div');
|
||
dot.className = 'secret-dot' + (i < found ? ' found' : '');
|
||
secretContainer.appendChild(dot);
|
||
}
|
||
|
||
// ═══ ADMIN MODAL ═══
|
||
const adminBtn = document.getElementById('admin-btn');
|
||
const modal = document.getElementById('admin-modal');
|
||
const modalClose = document.getElementById('modal-close');
|
||
const adminSubmit = document.getElementById('admin-submit');
|
||
const adminError = document.getElementById('admin-error');
|
||
|
||
adminBtn.addEventListener('click', () => {
|
||
modal.classList.add('active');
|
||
document.getElementById('admin-user').focus();
|
||
});
|
||
|
||
modalClose.addEventListener('click', () => modal.classList.remove('active'));
|
||
modal.addEventListener('click', (e) => {
|
||
if (e.target === modal) modal.classList.remove('active');
|
||
});
|
||
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') modal.classList.remove('active');
|
||
});
|
||
|
||
adminSubmit.addEventListener('click', () => {
|
||
const user = document.getElementById('admin-user').value;
|
||
const pass = document.getElementById('admin-pass').value;
|
||
if (!user || !pass) {
|
||
adminError.textContent = 'Credentials required.';
|
||
return;
|
||
}
|
||
// In production this would POST to /api/auth or redirect to Authentik
|
||
adminError.textContent = '';
|
||
adminSubmit.textContent = 'AUTHENTICATING...';
|
||
setTimeout(() => {
|
||
// Simulated redirect — replace with actual auth endpoint
|
||
window.location.href = '/admin';
|
||
}, 800);
|
||
});
|
||
|
||
// Enter key submits
|
||
document.getElementById('admin-pass').addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') adminSubmit.click();
|
||
});
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|