A Pen by mode-mercury on CodePen.
Created
December 10, 2025 21:54
-
-
Save mode-mercury/82015112e852b9dc5800dde81f2e9f09 to your computer and use it in GitHub Desktop.
Untitled
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>grumpster · the dumpster for happiness</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap'); | |
| :root { | |
| color-scheme: dark; | |
| --bg: #020617; | |
| --bg-card: #020617; | |
| --bg-card-soft: #020617; | |
| --accent: #22d3ee; /* cyan */ | |
| --accent-soft: rgba(34,211,238,0.3); | |
| --accent-alt: #a855f7; /* purple */ | |
| --danger: #fb923c; /* orange dumpster fire */ | |
| --danger-soft: rgba(251,146,60,0.3); | |
| --good: #4ade80; | |
| --good-soft: rgba(74,222,128,0.25); | |
| --border-soft: #1e293b; | |
| --fg: #e5e7eb; | |
| --fg-soft: #9ca3af; | |
| --shadow-soft: 0 22px 55px rgba(0,0,0,0.9); | |
| font-family: "Space Grotesk", system-ui, -apple-system, BlinkMacSystemFont, sans-serif; | |
| } | |
| * { box-sizing: border-box; } | |
| html, body { | |
| margin: 0; | |
| padding: 0; | |
| min-height: 100%; | |
| color: var(--fg); | |
| background: | |
| radial-gradient(circle at top left, #0f172a 0, transparent 52%), | |
| radial-gradient(circle at bottom right, #111827 0, transparent 55%), | |
| radial-gradient(circle at top right, #1e293b 0, #020617 65%); | |
| } | |
| body { | |
| padding: 0.75rem; | |
| padding-bottom: 3.25rem; /* space for bottom nav */ | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| } | |
| header { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.25rem; | |
| } | |
| .title-row { | |
| display: flex; | |
| flex-wrap: wrap; | |
| align-items: center; | |
| gap: 0.6rem; | |
| } | |
| .logo { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 0.08rem; | |
| margin: 0; | |
| font-size: 1.6rem; | |
| text-transform: lowercase; | |
| letter-spacing: 0.12em; | |
| text-shadow: | |
| 0 0 10px rgba(148,163,184,0.6), | |
| 0 0 26px rgba(34,211,238,0.5); | |
| } | |
| .logo-text { | |
| margin: 0; | |
| } | |
| .logo-cross { | |
| position: relative; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 1.1em; | |
| height: 1.1em; | |
| margin-inline: 0.08rem; | |
| border-radius: 999px; | |
| border: 2px solid #ef4444; | |
| box-shadow: 0 0 12px rgba(239,68,68,0.7); | |
| overflow: hidden; | |
| color: transparent; /* hide literal 'e' */ | |
| } | |
| .logo-cross::before { | |
| content: "e"; | |
| position: relative; | |
| font-size: 0.7em; | |
| color: #fecaca; | |
| opacity: 0.9; | |
| } | |
| .logo-cross::after { | |
| content: ""; | |
| position: absolute; | |
| width: 140%; | |
| height: 2px; | |
| background: #ef4444; | |
| transform: rotate(-40deg); | |
| box-shadow: 0 0 8px rgba(239,68,68,0.9); | |
| } | |
| .chip { | |
| font-size: 0.7rem; | |
| padding: 0.25rem 0.7rem; | |
| border-radius: 999px; | |
| border: 1px solid rgba(148,163,184,0.6); | |
| background: radial-gradient(circle at top, rgba(248,250,252,0.2), rgba(15,23,42,0.98)); | |
| display: inline-flex; | |
| gap: 0.3rem; | |
| align-items: center; | |
| box-shadow: 0 0 20px rgba(15,23,42,0.95); | |
| } | |
| .chip-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 999px; | |
| background: var(--danger); | |
| box-shadow: 0 0 12px var(--danger-soft); | |
| } | |
| .tagline { | |
| font-size: 0.8rem; | |
| opacity: 0.8; | |
| max-width: 580px; | |
| } | |
| .meta-row { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 0.5rem; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-top: 0.1rem; | |
| } | |
| .stats { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 0.4rem; | |
| font-size: 0.75rem; | |
| } | |
| .stat-pill { | |
| padding: 0.16rem 0.6rem; | |
| border-radius: 999px; | |
| border: 1px solid rgba(148,163,184,0.7); | |
| background: radial-gradient(circle at top left, rgba(248,250,252,0.16), rgba(15,23,42,0.98)); | |
| display: inline-flex; | |
| gap: 0.25rem; | |
| align-items: center; | |
| box-shadow: 0 10px 22px rgba(0,0,0,0.7); | |
| } | |
| .stat-label { opacity: 0.7; } | |
| .stat-value { font-weight: 500; } | |
| .happiness-wrap { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: flex-end; | |
| gap: 0.1rem; | |
| } | |
| .happiness-bar { | |
| position: relative; | |
| width: 200px; | |
| height: 11px; | |
| border-radius: 999px; | |
| background: rgba(15,23,42,0.9); | |
| border: 1px solid rgba(148,163,184,0.8); | |
| overflow: hidden; | |
| box-shadow: | |
| 0 0 12px rgba(15,23,42,0.9), | |
| 0 0 20px rgba(34,211,238,0.25); | |
| } | |
| .happiness-fill { | |
| position: absolute; | |
| inset: 0; | |
| width: 25%; | |
| background: linear-gradient(90deg, var(--danger), var(--accent), var(--accent-alt), var(--good)); | |
| background-size: 220% 100%; | |
| animation: shift 9s linear infinite; | |
| transition: width 220ms ease-out; | |
| } | |
| @keyframes shift { | |
| 0% { background-position: 0% 0; } | |
| 100% { background-position: 200% 0; } | |
| } | |
| .happiness-label { | |
| font-size: 0.68rem; | |
| opacity: 0.75; | |
| text-align: right; | |
| } | |
| /* Header dumpster-fire badge */ | |
| .dumpster-badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 0.3rem; | |
| font-size: 0.7rem; | |
| padding: 0.18rem 0.55rem; | |
| border-radius: 999px; | |
| border: 1px solid rgba(248,113,113,0.7); | |
| background: radial-gradient(circle at top, rgba(239,68,68,0.2), rgba(15,23,42,0.98)); | |
| box-shadow: 0 0 16px rgba(248,113,113,0.7); | |
| } | |
| .dumpster-icon { | |
| width: 18px; | |
| height: 18px; | |
| border-radius: 4px; | |
| border: 1px solid rgba(148,163,184,0.8); | |
| background: | |
| linear-gradient(to top, #020617 40%, #0f172a 100%); | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| position: relative; | |
| overflow: hidden; | |
| font-size: 0.85rem; | |
| } | |
| .dumpster-icon::before { | |
| content: ""; | |
| position: absolute; | |
| inset: auto 2px 6px 2px; | |
| height: 3px; | |
| background: #0f172a; | |
| border-radius: 1px; | |
| } | |
| .dumpster-icon span { | |
| position: relative; | |
| top: -3px; | |
| } | |
| main { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| } | |
| .panel { | |
| background: radial-gradient(circle at top, rgba(248,250,252,0.08), rgba(15,23,42,0.98)); | |
| border-radius: 1rem; | |
| border: 1px solid var(--border-soft); | |
| box-shadow: var(--shadow-soft); | |
| padding: 0.75rem; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.6rem; | |
| min-height: 0; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .panel::before { | |
| content: ""; | |
| position: absolute; | |
| inset: -40%; | |
| background: | |
| radial-gradient(circle at 0 0, rgba(34,211,238,0.16), transparent 55%), | |
| radial-gradient(circle at 100% 0, rgba(168,85,247,0.14), transparent 55%); | |
| mix-blend-mode: screen; | |
| opacity: 0.4; | |
| pointer-events: none; | |
| } | |
| .panel-inner { | |
| position: relative; | |
| z-index: 1; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.6rem; | |
| } | |
| .panel-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: baseline; | |
| gap: 0.5rem; | |
| } | |
| .panel-title { | |
| font-size: 0.82rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.16em; | |
| opacity: 0.9; | |
| } | |
| .panel-sub { | |
| font-size: 0.72rem; | |
| opacity: 0.7; | |
| } | |
| .btn { | |
| font-size: 0.68rem; | |
| padding: 0.26rem 0.7rem; | |
| border-radius: 999px; | |
| border: 1px solid rgba(148,163,184,0.8); | |
| background: linear-gradient(135deg, rgba(15,23,42,0.98), rgba(30,64,175,0.9)); | |
| color: var(--fg-soft); | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 0.26rem; | |
| cursor: pointer; | |
| transition: background 90ms ease, transform 80ms ease, box-shadow 120ms ease, border-color 120ms ease; | |
| white-space: nowrap; | |
| } | |
| .btn span.icon { font-size: 0.8rem; } | |
| .btn:hover { | |
| background: linear-gradient(135deg, rgba(30,64,175,1), rgba(37,99,235,1)); | |
| transform: translateY(-0.5px); | |
| box-shadow: 0 10px 22px rgba(15,23,42,0.9); | |
| border-color: rgba(191,219,254,1); | |
| color: #e5e7eb; | |
| } | |
| .btn-ghost { | |
| background: linear-gradient(135deg, rgba(15,23,42,0.95), rgba(15,23,42,0.98)); | |
| border-style: dashed; | |
| border-color: rgba(148,163,184,0.8); | |
| } | |
| .pane-section-label { | |
| font-size: 0.68rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.16em; | |
| opacity: 0.8; | |
| } | |
| /* VIEWS */ | |
| .panel-view { display: none; } | |
| .panel-view.active { display: flex; } | |
| /* RANT composer & feed */ | |
| .composer { | |
| border-radius: 0.9rem; | |
| padding: 0.55rem 0.6rem; | |
| background: linear-gradient(135deg, rgba(15,23,42,0.98), rgba(30,64,175,0.98)); | |
| border: 1px solid rgba(129,140,248,0.9); | |
| box-shadow: 0 15px 28px rgba(15,23,42,0.96); | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.4rem; | |
| } | |
| .composer-row { | |
| display: flex; | |
| gap: 0.4rem; | |
| align-items: flex-start; | |
| } | |
| .composer-avatar { | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 999px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.1rem; | |
| background: radial-gradient(circle at top, rgba(248,250,252,0.4), rgba(15,23,42,0.98)); | |
| border: 1px solid rgba(191,219,254,0.9); | |
| flex-shrink: 0; | |
| } | |
| .composer-main { | |
| flex: 1; | |
| min-width: 0; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.25rem; | |
| } | |
| .composer-input { | |
| width: 100%; | |
| resize: none; | |
| border-radius: 0.7rem; | |
| border: 1px solid rgba(148,163,184,0.8); | |
| background: rgba(15,23,42,0.98); | |
| color: var(--fg); | |
| padding: 0.45rem 0.55rem; | |
| font-size: 0.78rem; | |
| min-height: 52px; | |
| max-height: 90px; | |
| outline: none; | |
| box-shadow: inset 0 0 0 1px rgba(15,23,42,0.95); | |
| } | |
| .composer-input:focus { | |
| border-color: var(--accent-soft); | |
| box-shadow: | |
| inset 0 0 0 1px rgba(15,23,42,0.98), | |
| 0 0 18px rgba(34,211,238,0.4); | |
| } | |
| .composer-meta { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| gap: 0.5rem; | |
| font-size: 0.68rem; | |
| opacity: 0.8; | |
| } | |
| .char-count { | |
| font-variant-numeric: tabular-nums; | |
| } | |
| .char-count.over { color: var(--danger); } | |
| .btn-post { | |
| border-color: var(--accent-soft); | |
| background: linear-gradient(135deg, rgba(56,189,248,1), rgba(251,113,133,1)); | |
| color: #020617; | |
| font-weight: 600; | |
| box-shadow: 0 12px 26px rgba(15,23,42,0.98); | |
| } | |
| .btn-post:disabled { | |
| opacity: 0.45; | |
| cursor: default; | |
| box-shadow: none; | |
| transform: none; | |
| } | |
| .rant-feed { | |
| margin-top: 0.35rem; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.4rem; | |
| max-height: 320px; | |
| overflow-y: auto; | |
| padding-right: 0.1rem; | |
| scrollbar-width: thin; | |
| scrollbar-color: rgba(148,163,184,0.7) transparent; | |
| } | |
| .rant-feed::-webkit-scrollbar { width: 4px; } | |
| .rant-feed::-webkit-scrollbar-thumb { | |
| background: rgba(148,163,184,0.8); | |
| border-radius: 999px; | |
| } | |
| .rant { | |
| border-radius: 0.85rem; | |
| padding: 0.5rem 0.55rem 0.45rem; | |
| background: linear-gradient(135deg, rgba(15,23,42,0.98), rgba(30,64,175,0.96)); | |
| border: 1px solid rgba(129,140,248,0.9); | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.3rem; | |
| box-shadow: 0 10px 22px rgba(15,23,42,0.98); | |
| } | |
| .rant-top { | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 0.3rem; | |
| align-items: center; | |
| } | |
| .rant-author-block { | |
| display: flex; | |
| gap: 0.4rem; | |
| align-items: center; | |
| } | |
| .rant-avatar { | |
| width: 28px; | |
| height: 28px; | |
| border-radius: 999px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1rem; | |
| background: radial-gradient(circle at top, rgba(248,250,252,0.4), rgba(15,23,42,0.98)); | |
| border: 1px solid rgba(191,219,254,0.9); | |
| flex-shrink: 0; | |
| } | |
| .rant-author { | |
| font-size: 0.76rem; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.02rem; | |
| } | |
| .rant-name-row { | |
| display: flex; | |
| gap: 0.25rem; | |
| align-items: baseline; | |
| } | |
| .rant-display-name { font-weight: 500; } | |
| .rant-handle { font-size: 0.7rem; opacity: 0.7; } | |
| .rant-time { font-size: 0.66rem; opacity: 0.6; } | |
| .rant-text { | |
| font-size: 0.8rem; | |
| line-height: 1.4; | |
| } | |
| .rant-image { | |
| border-radius: 0.75rem; | |
| overflow: hidden; | |
| border: 1px solid rgba(148,163,184,0.8); | |
| margin-top: 0.15rem; | |
| max-height: 220px; | |
| } | |
| .rant-image img { | |
| display: block; | |
| width: 100%; | |
| height: auto; | |
| } | |
| .rant-actions { | |
| display: flex; | |
| gap: 0.3rem; | |
| align-items: center; | |
| font-size: 0.7rem; | |
| margin-top: 0.1rem; | |
| } | |
| .reaction-btn { | |
| border-radius: 999px; | |
| padding: 0.18rem 0.45rem; | |
| border: 1px solid rgba(148,163,184,0.8); | |
| background: rgba(15,23,42,0.98); | |
| display: inline-flex; | |
| gap: 0.18rem; | |
| align-items: center; | |
| cursor: pointer; | |
| transition: background 80ms ease, border-color 80ms ease, transform 80ms ease; | |
| } | |
| .reaction-btn span.icon { font-size: 0.8rem; } | |
| .reaction-btn span.count { | |
| font-variant-numeric: tabular-nums; | |
| opacity: 0.7; | |
| } | |
| .reaction-btn:hover { | |
| transform: translateY(-0.5px); | |
| border-color: rgba(248,250,252,0.9); | |
| } | |
| .reaction-btn.active.down { | |
| border-color: rgba(148,163,184,1); | |
| background: rgba(15,23,42,0.95); | |
| } | |
| .reaction-btn.active.fire { | |
| border-color: var(--danger-soft); | |
| background: rgba(24,20,12,0.98); | |
| } | |
| .reaction-btn.active.unfollow { | |
| border-color: rgba(56,189,248,0.8); | |
| background: rgba(8,47,73,0.98); | |
| } | |
| /* FRIEND LIST VIEW */ | |
| .friend-list { | |
| margin-top: 0.1rem; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.45rem; | |
| max-height: 360px; | |
| overflow-y: auto; | |
| padding-right: 0.1rem; | |
| scrollbar-width: thin; | |
| scrollbar-color: rgba(148,163,184,0.7) transparent; | |
| } | |
| .friend-list::-webkit-scrollbar { width: 4px; } | |
| .friend-list::-webkit-scrollbar-thumb { | |
| background: rgba(148,163,184,0.8); | |
| border-radius: 999px; | |
| } | |
| .friend-card { | |
| position: relative; | |
| border-radius: 0.85rem; | |
| overflow: hidden; | |
| background: linear-gradient(135deg, rgba(15,23,42,0.98), rgba(30,64,175,0.96)); | |
| border: 1px solid rgba(148,163,184,0.8); | |
| padding: 0.5rem 0.55rem; | |
| display: grid; | |
| grid-template-columns: auto minmax(0, 1fr) auto; | |
| gap: 0.45rem; | |
| align-items: center; | |
| box-shadow: 0 14px 28px rgba(15,23,42,0.98); | |
| transition: transform 90ms ease-out, box-shadow 90ms ease-out, border-color 120ms ease-out; | |
| } | |
| .friend-card.favorite { | |
| border-color: var(--good-soft); | |
| box-shadow: 0 0 0 1px rgba(74,222,128,0.25), 0 16px 32px rgba(15,23,42,0.98); | |
| } | |
| .friend-card.dumped { | |
| opacity: 0.45; | |
| border-style: dashed; | |
| border-color: rgba(251,146,60,0.7); | |
| } | |
| .friend-avatar { | |
| width: 34px; | |
| height: 34px; | |
| border-radius: 999px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.2rem; | |
| background: radial-gradient(circle at top, rgba(248,250,252,0.4), rgba(15,23,42,0.98)); | |
| border: 1px solid rgba(191,219,254,0.9); | |
| box-shadow: 0 0 14px rgba(15,23,42,1); | |
| z-index: 1; | |
| } | |
| .friend-main { | |
| position: relative; | |
| z-index: 1; | |
| flex: 1; | |
| min-width: 0; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.18rem; | |
| } | |
| .friend-name-row { | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 0.45rem; | |
| align-items: baseline; | |
| } | |
| .friend-name { | |
| font-size: 0.88rem; | |
| font-weight: 500; | |
| white-space: nowrap; | |
| text-overflow: ellipsis; | |
| overflow: hidden; | |
| } | |
| .pill { | |
| font-size: 0.63rem; | |
| padding: 0.14rem 0.5rem; | |
| border-radius: 999px; | |
| border: 1px solid rgba(148,163,184,0.9); | |
| background: rgba(15,23,42,0.98); | |
| white-space: nowrap; | |
| } | |
| .friend-tag { | |
| font-size: 0.72rem; | |
| opacity: 0.8; | |
| white-space: nowrap; | |
| text-overflow: ellipsis; | |
| overflow: hidden; | |
| } | |
| .friend-meta { | |
| display: flex; | |
| gap: 0.35rem; | |
| align-items: center; | |
| font-size: 0.68rem; | |
| opacity: 0.78; | |
| } | |
| .meter-dots { | |
| display: inline-flex; | |
| gap: 2px; | |
| } | |
| .dot { | |
| width: 6px; | |
| height: 6px; | |
| border-radius: 999px; | |
| background: rgba(148,163,184,0.7); | |
| } | |
| .dot.hot { | |
| background: var(--danger); | |
| box-shadow: 0 0 10px var(--danger-soft); | |
| } | |
| .dot.chill { | |
| background: var(--good); | |
| box-shadow: 0 0 10px var(--good-soft); | |
| } | |
| .friend-actions { | |
| position: relative; | |
| z-index: 1; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.25rem; | |
| align-items: flex-end; | |
| } | |
| .btn-fav { | |
| border-color: var(--good-soft); | |
| color: var(--good); | |
| background: linear-gradient(135deg, rgba(5,46,22,0.98), rgba(22,163,74,0.98)); | |
| } | |
| .btn-dump { | |
| border-color: var(--danger-soft); | |
| color: var(--danger); | |
| background: linear-gradient(135deg, rgba(30,64,175,0.98), rgba(251,146,60,0.98)); | |
| } | |
| footer { | |
| text-align: center; | |
| font-size: 0.66rem; | |
| opacity: 0.6; | |
| margin-top: 0.35rem; | |
| } | |
| footer span.brand { | |
| letter-spacing: 0.18em; | |
| text-transform: uppercase; | |
| } | |
| /* Bottom nav */ | |
| .bottom-nav { | |
| position: fixed; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| height: 3rem; | |
| background: radial-gradient(circle at top, rgba(15,23,42,0.96), rgba(2,6,23,1)); | |
| border-top: 1px solid rgba(148,163,184,0.8); | |
| display: flex; | |
| justify-content: space-around; | |
| align-items: center; | |
| z-index: 20; | |
| box-shadow: 0 -10px 28px rgba(15,23,42,1); | |
| backdrop-filter: blur(16px); | |
| } | |
| .nav-item { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 0.05rem; | |
| font-size: 0.62rem; | |
| opacity: 0.7; | |
| cursor: pointer; | |
| transition: opacity 80ms ease, transform 80ms ease; | |
| } | |
| .nav-item-icon { font-size: 1.2rem; line-height: 1; } | |
| .nav-item.active { | |
| opacity: 1; | |
| transform: translateY(-1px); | |
| } | |
| .nav-item.active .nav-item-icon { | |
| text-shadow: 0 0 14px rgba(248,250,252,0.9); | |
| } | |
| @media (min-width: 900px) { | |
| .bottom-nav { | |
| max-width: 720px; | |
| margin: 0 auto; | |
| border-radius: 999px 999px 0 0; | |
| } | |
| body { | |
| max-width: 900px; | |
| margin: 0 auto; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <a id="top"></a> | |
| <header> | |
| <div class="title-row"> | |
| <h1 class="logo"> | |
| <span class="logo-text">grumpst</span> | |
| <span class="logo-cross">e</span> | |
| <span class="logo-text">r</span> | |
| </h1> | |
| <span class="chip"> | |
| <span class="chip-dot"></span> | |
| the dumpster for happiness | |
| </span> | |
| <span class="dumpster-badge"> | |
| <span class="dumpster-icon"><span>🔥</span></span> | |
| live dumpster fire simulator | |
| </span> | |
| </div> | |
| <div class="tagline"> | |
| You start friends with everyone. Your job is ruthless curation: keep your favorites, dump the rest, and post tiny rants instead of pretending it’s fine. | |
| </div> | |
| <div class="meta-row"> | |
| <div class="stats"> | |
| <div class="stat-pill"> | |
| <span class="stat-label">Friends:</span> | |
| <span class="stat-value" id="statTotal">0</span> | |
| </div> | |
| <div class="stat-pill"> | |
| <span class="stat-label">Favorites:</span> | |
| <span class="stat-value" id="statFavs">0</span> | |
| </div> | |
| <div class="stat-pill"> | |
| <span class="stat-label">Dumped:</span> | |
| <span class="stat-value" id="statDumped">0</span> | |
| </div> | |
| <div class="stat-pill"> | |
| <span class="stat-label">Rants:</span> | |
| <span class="stat-value" id="statRants">0</span> | |
| </div> | |
| </div> | |
| <div class="happiness-wrap"> | |
| <div class="happiness-bar"> | |
| <div class="happiness-fill" id="happinessFill"></div> | |
| </div> | |
| <div class="happiness-label" id="happinessLabel"> | |
| Happiness: 0% curated · 0 rants released | |
| </div> | |
| </div> | |
| </div> | |
| </header> | |
| <main> | |
| <!-- FEED VIEW (default) --> | |
| <section class="panel panel-view active" id="feedView"> | |
| <div class="panel-inner"> | |
| <div class="panel-header"> | |
| <div> | |
| <div class="panel-title">micro-rants feed</div> | |
| <div class="panel-sub"> | |
| Character cap: <strong>144</strong>. No likes. Only 👎, 🔥, and 🚫. | |
| </div> | |
| </div> | |
| </div> | |
| <div class="composer"> | |
| <div class="composer-row"> | |
| <div class="composer-avatar">😶🌫️</div> | |
| <div class="composer-main"> | |
| <textarea | |
| id="rantInput" | |
| class="composer-input" | |
| maxlength="144" | |
| placeholder="Type a tiny update about your social chaos… (max 144 chars)" | |
| ></textarea> | |
| <div class="composer-meta"> | |
| <span class="char-count" id="charCount">0 / 144</span> | |
| <div style="display:flex;gap:0.3rem;align-items:center;"> | |
| <span style="font-size:0.65rem;opacity:0.7;">Mode: public venting</span> | |
| <button class="btn btn-post" id="postBtn" disabled> | |
| <span class="icon">🗑</span> post to dumpster | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="pane-section-label">latest rants</div> | |
| <div class="rant-feed" id="rantFeed"> | |
| <!-- rants injected --> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- FRIENDS VIEW --> | |
| <section class="panel panel-view" id="friendsView"> | |
| <div class="panel-inner"> | |
| <div class="panel-header"> | |
| <div> | |
| <div class="panel-title">everyone you know (for now)</div> | |
| <div class="panel-sub"> | |
| Starts as “friends with everyone”. Ends as “people you’d actually text back”. | |
| </div> | |
| </div> | |
| <button class="btn btn-ghost" id="resetBtn"> | |
| <span class="icon">⟲</span> reset experiment | |
| </button> | |
| </div> | |
| <div class="friend-list" id="friendList"> | |
| <!-- friend cards injected --> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <footer id="footer"> | |
| <span class="brand">grumpster · beta</span> · no dopamine hits, just boundary hygiene | |
| </footer> | |
| <!-- Bottom nav --> | |
| <nav class="bottom-nav"> | |
| <div class="nav-item active" data-view="feedView"> | |
| <div class="nav-item-icon">🔥</div> | |
| <div>Feed</div> | |
| </div> | |
| <div class="nav-item" data-view="friendsView"> | |
| <div class="nav-item-icon">👥</div> | |
| <div>Friends</div> | |
| </div> | |
| <div class="nav-item" data-anchor="top"> | |
| <div class="nav-item-icon">🏠</div> | |
| <div>Top</div> | |
| </div> | |
| <div class="nav-item" data-anchor="footer"> | |
| <div class="nav-item-icon">ℹ️</div> | |
| <div>About</div> | |
| </div> | |
| </nav> | |
| <script> | |
| // ------------------ FRIEND MODEL -------------------- | |
| const seedFriends = [ | |
| { name: "Alex from high school", tag: "Sends reels at 3:17am", avatar: "📹" }, | |
| { name: "Jamie (coworker)", tag: "Calendar pings your soul", avatar: "📅" }, | |
| { name: "Rae the oversharer", tag: "Voice notes, 7 min each", avatar: "🎙" }, | |
| { name: "Theo from the skatepark", tag: "Actually pretty great", avatar: "🛹" }, | |
| { name: "Jess (family thread)", tag: "Replies with only GIFs", avatar: "📱" }, | |
| { name: "Casey the hustle guru", tag: "DMs ‘you up? let’s build’", avatar: "💼" }, | |
| { name: "Old bandmate", tag: "Still tagging you in throwbacks", avatar: "🎸" }, | |
| { name: "Neighbor with leaf blower", tag: "Mows at weird spiritual hours", avatar: "🍂" }, | |
| { name: "That one barista", tag: "Knows your order, your trauma", avatar: "☕️" }, | |
| { name: "Online game squad", tag: "Disappear for months, then 6hr raid", avatar: "🎮" }, | |
| { name: "Mystery number", tag: "You’re too scared to ask who this is", avatar: "❓" }, | |
| { name: "You from three years ago", tag: "Still haunting the group chat", avatar: "👻" } | |
| ]; | |
| let friends = []; | |
| const friendListEl = document.getElementById("friendList"); | |
| const statTotalEl = document.getElementById("statTotal"); | |
| const statFavsEl = document.getElementById("statFavs"); | |
| const statDumpedEl = document.getElementById("statDumped"); | |
| const statRantsEl = document.getElementById("statRants"); | |
| const happinessFillEl = document.getElementById("happinessFill"); | |
| const happinessLabelEl = document.getElementById("happinessLabel"); | |
| const resetBtn = document.getElementById("resetBtn"); | |
| function randomInt(min, max) { | |
| return Math.floor(Math.random() * (max - min + 1)) + min; | |
| } | |
| function initFriends() { | |
| friends = seedFriends.map((f, index) => ({ | |
| id: index + 1, | |
| name: f.name, | |
| tag: f.tag, | |
| avatar: f.avatar, | |
| heat: randomInt(1, 5), | |
| favorite: false, | |
| dumped: false | |
| })); | |
| } | |
| function renderFriends() { | |
| friendListEl.innerHTML = ""; | |
| friends.forEach(friend => { | |
| const card = document.createElement("div"); | |
| card.className = "friend-card"; | |
| if (friend.favorite) card.classList.add("favorite"); | |
| if (friend.dumped) card.classList.add("dumped"); | |
| const avatar = document.createElement("div"); | |
| avatar.className = "friend-avatar"; | |
| avatar.textContent = friend.avatar || "🙂"; | |
| const main = document.createElement("div"); | |
| main.className = "friend-main"; | |
| const nameRow = document.createElement("div"); | |
| nameRow.className = "friend-name-row"; | |
| const nameEl = document.createElement("div"); | |
| nameEl.className = "friend-name"; | |
| nameEl.textContent = friend.name; | |
| const pill = document.createElement("div"); | |
| pill.className = "pill"; | |
| pill.textContent = friend.favorite | |
| ? "Protected" | |
| : friend.dumped | |
| ? "Dumped" | |
| : "Undecided"; | |
| nameRow.appendChild(nameEl); | |
| nameRow.appendChild(pill); | |
| const tagEl = document.createElement("div"); | |
| tagEl.className = "friend-tag"; | |
| tagEl.textContent = friend.tag; | |
| const meta = document.createElement("div"); | |
| meta.className = "friend-meta"; | |
| const meter = document.createElement("div"); | |
| meter.className = "meter-dots"; | |
| for (let i = 0; i < 5; i++) { | |
| const dot = document.createElement("div"); | |
| dot.className = "dot"; | |
| if (i < friend.heat) { | |
| dot.classList.add(friend.heat >= 4 ? "hot" : "chill"); | |
| } | |
| meter.appendChild(dot); | |
| } | |
| const heatLabel = document.createElement("span"); | |
| heatLabel.textContent = | |
| friend.heat >= 4 ? "High chaos" : | |
| friend.heat >= 2 ? "Mild static" : | |
| "Low noise"; | |
| meta.appendChild(meter); | |
| meta.appendChild(heatLabel); | |
| main.appendChild(nameRow); | |
| main.appendChild(tagEl); | |
| main.appendChild(meta); | |
| const actions = document.createElement("div"); | |
| actions.className = "friend-actions"; | |
| const favBtn = document.createElement("button"); | |
| favBtn.className = "btn btn-fav"; | |
| favBtn.innerHTML = '<span class="icon">★</span> keep'; | |
| favBtn.disabled = friend.dumped; | |
| favBtn.addEventListener("click", () => toggleFavorite(friend.id)); | |
| const dumpBtn = document.createElement("button"); | |
| dumpBtn.className = "btn btn-dump"; | |
| dumpBtn.innerHTML = '<span class="icon">🗑</span> dump'; | |
| dumpBtn.disabled = friend.favorite || friend.dumped; | |
| dumpBtn.addEventListener("click", () => dumpFriend(friend.id)); | |
| actions.appendChild(favBtn); | |
| actions.appendChild(dumpBtn); | |
| card.appendChild(avatar); | |
| card.appendChild(main); | |
| card.appendChild(actions); | |
| friendListEl.appendChild(card); | |
| }); | |
| } | |
| function toggleFavorite(id) { | |
| const f = friends.find(fr => fr.id === id); | |
| if (!f || f.dumped) return; | |
| f.favorite = !f.favorite; | |
| renderAll(); | |
| } | |
| function dumpFriend(id) { | |
| const f = friends.find(fr => fr.id === id); | |
| if (!f || f.dumped) return; | |
| f.dumped = true; | |
| f.favorite = false; | |
| renderAll(); | |
| } | |
| function renderStats() { | |
| const total = friends.length; | |
| const favs = friends.filter(f => f.favorite).length; | |
| const dumped = friends.filter(f => f.dumped).length; | |
| const rantCount = rants.length; | |
| statTotalEl.textContent = total; | |
| statFavsEl.textContent = favs; | |
| statDumpedEl.textContent = dumped; | |
| statRantsEl.textContent = rantCount; | |
| const curatedRatio = total === 0 ? 0 : (favs / total) * 100; | |
| const barWidth = Math.min(100, Math.max(6, curatedRatio)); | |
| happinessFillEl.style.width = barWidth + "%"; | |
| happinessLabelEl.textContent = | |
| `Happiness: ${Math.round(curatedRatio)}% curated · ${rantCount} rant${rantCount === 1 ? "" : "s"} released`; | |
| } | |
| function resetAll() { | |
| initFriends(); | |
| initDummyRants(); | |
| renderAll(); | |
| } | |
| function renderAll() { | |
| renderFriends(); | |
| renderRants(); | |
| renderStats(); | |
| } | |
| // ------------------ RANT MODEL -------------------- | |
| const rantInput = document.getElementById("rantInput"); | |
| const charCountEl = document.getElementById("charCount"); | |
| const postBtn = document.getElementById("postBtn"); | |
| const rantFeed = document.getElementById("rantFeed"); | |
| const RANT_LIMIT = 144; | |
| let rants = []; | |
| let rantIdCounter = 1; | |
| function initDummyRants() { | |
| const now = Date.now(); | |
| rants = [ | |
| { | |
| id: 1, | |
| text: "Muting 27 group chats today. Emotional bandwidth is not a shared resource.", | |
| time: new Date(now - 5 * 60 * 1000), | |
| authorName: "You", | |
| handle: "@you", | |
| avatar: "😶🌫️", | |
| imageUrl: null, | |
| reactions: { down: 1, fire: 6, unfollow: 0 }, | |
| userReact: { down: false, fire: false, unfollow: false } | |
| }, | |
| { | |
| id: 2, | |
| text: "Being everyone’s therapist is wild when nobody booked an appointment.", | |
| time: new Date(now - 25 * 60 * 1000), | |
| authorName: "Rae", | |
| handle: "@voiceNote3000", | |
| avatar: "🎙", | |
| imageUrl: "https://picsum.photos/seed/grump1/600/360", | |
| reactions: { down: 0, fire: 9, unfollow: 2 }, | |
| userReact: { down: false, fire: false, unfollow: false } | |
| }, | |
| { | |
| id: 3, | |
| text: "Unfriending people in my head like: no beef, just quieter air.", | |
| time: new Date(now - 60 * 60 * 1000), | |
| authorName: "Theo", | |
| handle: "@kickpush", | |
| avatar: "🛹", | |
| imageUrl: "https://picsum.photos/seed/grump2/600/340", | |
| reactions: { down: 2, fire: 7, unfollow: 1 }, | |
| userReact: { down: false, fire: false, unfollow: false } | |
| }, | |
| { | |
| id: 4, | |
| text: "Social media needs a “don’t show me their face for 30 days” button.", | |
| time: new Date(now - 3 * 60 * 60 * 1000), | |
| authorName: "Jess", | |
| handle: "@replyGifOnly", | |
| avatar: "📱", | |
| imageUrl: null, | |
| reactions: { down: 0, fire: 4, unfollow: 3 }, | |
| userReact: { down: false, fire: false, unfollow: false } | |
| } | |
| ]; | |
| rantIdCounter = rants.length + 1; | |
| } | |
| function updateCharCount() { | |
| const text = rantInput.value || ""; | |
| const len = text.length; | |
| charCountEl.textContent = `${len} / ${RANT_LIMIT}`; | |
| if (len > RANT_LIMIT) { | |
| charCountEl.classList.add("over"); | |
| } else { | |
| charCountEl.classList.remove("over"); | |
| } | |
| postBtn.disabled = len === 0 || len > RANT_LIMIT; | |
| } | |
| function postRant() { | |
| const text = (rantInput.value || "").trim(); | |
| if (!text || text.length > RANT_LIMIT) return; | |
| const rant = { | |
| id: rantIdCounter++, | |
| text, | |
| time: new Date(), | |
| authorName: "You", | |
| handle: "@you", | |
| avatar: "😶🌫️", | |
| imageUrl: null, | |
| reactions: { down: 0, fire: 0, unfollow: 0 }, | |
| userReact: { down: false, fire: false, unfollow: false } | |
| }; | |
| rants.unshift(rant); | |
| rantInput.value = ""; | |
| updateCharCount(); | |
| renderAll(); | |
| } | |
| function renderRants() { | |
| rantFeed.innerHTML = ""; | |
| if (rants.length === 0) { | |
| const empty = document.createElement("div"); | |
| empty.className = "rant-text"; | |
| empty.style.opacity = "0.7"; | |
| empty.textContent = "No rants yet. The silence is suspiciously healthy."; | |
| rantFeed.appendChild(empty); | |
| return; | |
| } | |
| rants.forEach(r => { | |
| const rantEl = document.createElement("div"); | |
| rantEl.className = "rant"; | |
| const top = document.createElement("div"); | |
| top.className = "rant-top"; | |
| const authorBlock = document.createElement("div"); | |
| authorBlock.className = "rant-author-block"; | |
| const avatar = document.createElement("div"); | |
| avatar.className = "rant-avatar"; | |
| avatar.textContent = r.avatar || "🙂"; | |
| const author = document.createElement("div"); | |
| author.className = "rant-author"; | |
| const nameRow = document.createElement("div"); | |
| nameRow.className = "rant-name-row"; | |
| const displayName = document.createElement("span"); | |
| displayName.className = "rant-display-name"; | |
| displayName.textContent = r.authorName || "Unknown"; | |
| const handle = document.createElement("span"); | |
| handle.className = "rant-handle"; | |
| handle.textContent = r.handle || "@anon"; | |
| nameRow.appendChild(displayName); | |
| nameRow.appendChild(handle); | |
| const time = document.createElement("span"); | |
| time.className = "rant-time"; | |
| time.textContent = r.time.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); | |
| author.appendChild(nameRow); | |
| author.appendChild(time); | |
| authorBlock.appendChild(avatar); | |
| authorBlock.appendChild(author); | |
| top.appendChild(authorBlock); | |
| const text = document.createElement("div"); | |
| text.className = "rant-text"; | |
| text.textContent = r.text; | |
| rantEl.appendChild(top); | |
| rantEl.appendChild(text); | |
| if (r.imageUrl) { | |
| const imgWrap = document.createElement("div"); | |
| imgWrap.className = "rant-image"; | |
| const img = document.createElement("img"); | |
| img.src = r.imageUrl; | |
| img.alt = "attached image"; | |
| imgWrap.appendChild(img); | |
| rantEl.appendChild(imgWrap); | |
| } | |
| const actions = document.createElement("div"); | |
| actions.className = "rant-actions"; | |
| const downBtn = makeReactionButton("👎", "down", r); | |
| const fireBtn = makeReactionButton("🔥", "fire", r); | |
| const unfollowBtn = makeReactionButton("🚫", "unfollow", r); | |
| actions.appendChild(downBtn); | |
| actions.appendChild(fireBtn); | |
| actions.appendChild(unfollowBtn); | |
| rantEl.appendChild(actions); | |
| rantFeed.appendChild(rantEl); | |
| }); | |
| } | |
| function makeReactionButton(emoji, key, rant) { | |
| const btn = document.createElement("button"); | |
| btn.className = "reaction-btn"; | |
| btn.classList.add(key === "down" ? "down" : key === "fire" ? "fire" : "unfollow"); | |
| if (rant.userReact[key]) { | |
| btn.classList.add("active"); | |
| } | |
| const iconSpan = document.createElement("span"); | |
| iconSpan.className = "icon"; | |
| iconSpan.textContent = emoji; | |
| const countSpan = document.createElement("span"); | |
| countSpan.className = "count"; | |
| countSpan.textContent = rant.reactions[key]; | |
| btn.appendChild(iconSpan); | |
| btn.appendChild(countSpan); | |
| btn.addEventListener("click", () => { | |
| toggleReaction(rant.id, key); | |
| }); | |
| return btn; | |
| } | |
| function toggleReaction(id, key) { | |
| const r = rants.find(rr => rr.id === id); | |
| if (!r) return; | |
| const currentlyOn = r.userReact[key]; | |
| if (currentlyOn) { | |
| r.userReact[key] = false; | |
| r.reactions[key] = Math.max(0, r.reactions[key] - 1); | |
| } else { | |
| r.userReact[key] = true; | |
| r.reactions[key] += 1; | |
| } | |
| renderRants(); | |
| renderStats(); | |
| } | |
| // ------------------ BOTTOM NAV -------------------- | |
| const navItems = document.querySelectorAll(".bottom-nav .nav-item"); | |
| const views = document.querySelectorAll(".panel-view"); | |
| navItems.forEach(item => { | |
| item.addEventListener("click", () => { | |
| navItems.forEach(i => i.classList.remove("active")); | |
| item.classList.add("active"); | |
| const viewId = item.dataset.view; | |
| const anchorId = item.dataset.anchor; | |
| if (viewId) { | |
| views.forEach(v => v.classList.remove("active")); | |
| const targetView = document.getElementById(viewId); | |
| if (targetView) { | |
| targetView.classList.add("active"); | |
| targetView.scrollIntoView({ behavior: "smooth", block: "start" }); | |
| } | |
| } else if (anchorId) { | |
| const el = document.getElementById(anchorId); | |
| if (el) { | |
| el.scrollIntoView({ behavior: "smooth", block: "start" }); | |
| } | |
| } | |
| }); | |
| }); | |
| // ------------------ INIT & EVENTS -------------------- | |
| rantInput.addEventListener("input", updateCharCount); | |
| postBtn.addEventListener("click", postRant); | |
| resetBtn.addEventListener("click", resetAll); | |
| initFriends(); | |
| initDummyRants(); | |
| renderAll(); | |
| updateCharCount(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment