diff --git a/app/globals.css b/app/globals.css
index e05a418e..8f8ce6e9 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -372,3 +372,1096 @@ pre[class*="language-"] {
pre[class*="language-"] {
color: var(--prism-text) !important;
}
+
+/* EternalCode 2026 visual system */
+:root {
+ color-scheme: light;
+ --ec-bg: #f7f7f2;
+ --ec-panel: #fdfdf9;
+ --ec-panel-strong: #eeeee7;
+ --ec-footer: #efefe8;
+ --ec-text: #171914;
+ --ec-muted: rgb(23 25 20 / 0.66);
+ --ec-faint: rgb(23 25 20 / 0.46);
+ --ec-line: rgb(23 25 20 / 0.14);
+ --ec-soft: rgb(23 25 20 / 0.065);
+ --ec-accent: #3b82f6;
+ --ec-accent-text: #1d4ed8;
+ --ec-accent-ink: #ffffff;
+ --ec-nav: rgb(247 247 242 / 0.9);
+ --ec-code: #171a15;
+ --ec-code-text: #f3f5ec;
+ --ec-glow: rgb(59 130 246 / 0.18);
+ --ec-glow-core: rgb(59 130 246 / 0.34);
+}
+
+.dark {
+ color-scheme: dark;
+ --ec-bg: #0d0f0d;
+ --ec-panel: #141713;
+ --ec-panel-strong: #090b09;
+ --ec-footer: #080a08;
+ --ec-text: #f1f3ea;
+ --ec-muted: rgb(241 243 234 / 0.62);
+ --ec-faint: rgb(241 243 234 / 0.42);
+ --ec-line: rgb(241 243 234 / 0.12);
+ --ec-soft: rgb(255 255 255 / 0.06);
+ --ec-accent: #60a5fa;
+ --ec-accent-text: #60a5fa;
+ --ec-accent-ink: #07111f;
+ --ec-nav: rgb(11 13 12 / 0.84);
+ --ec-code: #080a08;
+ --ec-code-text: #f4f5ef;
+ --ec-glow: rgb(96 165 250 / 0.17);
+ --ec-glow-core: rgb(96 165 250 / 0.32);
+}
+
+html {
+ scroll-behavior: smooth;
+ background: var(--ec-bg);
+}
+body {
+ background: var(--ec-bg);
+ color: var(--ec-text);
+ font-family: var(--font-manrope), sans-serif;
+}
+::selection {
+ background: var(--ec-accent);
+ color: var(--ec-accent-ink);
+}
+.site-shell {
+ min-height: 100vh;
+ overflow: hidden;
+ background: var(--ec-bg);
+ color: var(--ec-text);
+}
+.site-nav {
+ border: 1px solid var(--ec-line);
+ border-radius: 1.1rem;
+ background: var(--ec-nav);
+ box-shadow: 0 18px 60px rgb(0 0 0 / 0.12);
+ backdrop-filter: blur(18px);
+}
+.nav-link {
+ border-radius: 999px;
+ padding: 0.55rem 0.9rem;
+ color: var(--ec-muted);
+ font-size: 0.82rem;
+ font-weight: 600;
+ transition: 0.2s ease;
+}
+.nav-link:hover,
+.nav-link-active {
+ background: var(--ec-soft);
+ color: var(--ec-text);
+}
+.section-shell {
+ width: min(100% - 2rem, 92rem);
+ margin-inline: auto;
+}
+.hero-grid {
+ position: absolute;
+ inset: 0;
+ opacity: 0.48;
+ background-image:
+ linear-gradient(var(--ec-line) 1px, transparent 1px),
+ linear-gradient(90deg, var(--ec-line) 1px, transparent 1px);
+ background-size: 64px 64px;
+ mask-image: linear-gradient(to bottom, black 20%, transparent 90%);
+}
+.eyebrow,
+.section-kicker {
+ display: flex;
+ align-items: center;
+ gap: 0.65rem;
+ font-family: var(--font-jetbrains-mono), monospace;
+ font-size: 0.68rem;
+ line-height: 1;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+ color: var(--ec-muted);
+}
+.section-kicker span,
+.hero-title span,
+.text-link {
+ color: var(--ec-accent-text);
+}
+.status-dot {
+ display: inline-block;
+ width: 0.45rem;
+ height: 0.45rem;
+ flex: none;
+ border-radius: 50%;
+ background: var(--ec-accent);
+ box-shadow: 0 0 14px color-mix(in srgb, var(--ec-accent) 65%, transparent);
+}
+.hero-title {
+ max-width: 65rem;
+ margin-top: 1.7rem;
+ font-size: clamp(3.3rem, 8vw, 8.2rem);
+ font-weight: 600;
+ line-height: 0.88;
+ letter-spacing: -0.075em;
+}
+.primary-action,
+.secondary-action,
+.pill-link,
+.text-link {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.6rem;
+ font-size: 0.82rem;
+ font-weight: 700;
+ transition: 0.2s ease;
+}
+.primary-action {
+ min-height: 3.2rem;
+ border-radius: 999px;
+ padding: 0 1.35rem;
+ background: var(--ec-accent);
+ color: var(--ec-accent-ink);
+}
+.primary-action:hover {
+ filter: brightness(1.04);
+ transform: translateY(-2px);
+}
+.secondary-action,
+.pill-link {
+ min-height: 3.2rem;
+ border: 1px solid var(--ec-line);
+ border-radius: 999px;
+ padding: 0 1.25rem;
+ color: var(--ec-muted);
+}
+.secondary-action:hover,
+.pill-link:hover {
+ border-color: color-mix(in srgb, var(--ec-text) 28%, transparent);
+ color: var(--ec-text);
+}
+.project-card,
+.ec-card {
+ border-color: var(--ec-line);
+ background-color: var(--ec-panel);
+ color: var(--ec-text);
+}
+.project-card {
+ display: block;
+ min-height: 31rem;
+ border-width: 1px;
+ border-radius: 1.75rem;
+ padding: 1.5rem;
+ overflow: hidden;
+ transition:
+ transform 0.35s ease,
+ border-color 0.35s ease,
+ box-shadow 0.35s ease;
+}
+.project-card:hover {
+ transform: translateY(-6px);
+ border-color: color-mix(in srgb, var(--ec-text) 25%, transparent);
+ box-shadow: 0 22px 55px rgb(0 0 0 / 0.1);
+}
+.project-icon {
+ display: grid;
+ width: 3.25rem;
+ height: 3.25rem;
+ place-items: center;
+ border-radius: 1rem;
+ background: var(--ec-soft);
+ color: var(--ec-text);
+}
+.project-blue {
+ background-image: radial-gradient(
+ circle at 15% 20%,
+ color-mix(in srgb, var(--ec-accent) 20%, transparent),
+ transparent 38%
+ );
+}
+.project-blue .project-icon {
+ color: var(--ec-accent-text);
+}
+.project-violet {
+ background-image: radial-gradient(
+ circle at 15% 20%,
+ rgb(37 99 235 / 0.16),
+ transparent 38%
+ );
+}
+.project-violet .project-icon {
+ color: #2563eb;
+}
+.project-cyan {
+ background-image: radial-gradient(
+ circle at 15% 20%,
+ rgb(14 165 233 / 0.15),
+ transparent 38%
+ );
+}
+.project-cyan .project-icon {
+ color: #0284c7;
+}
+.display-title {
+ max-width: 46rem;
+ font-size: clamp(2.55rem, 5vw, 5rem);
+ font-weight: 600;
+ line-height: 0.98;
+ letter-spacing: -0.06em;
+}
+.code-panel {
+ overflow: hidden;
+ border: 1px solid var(--ec-line);
+ border-radius: 1.25rem;
+ background: var(--ec-code);
+ color: var(--ec-code-text);
+ box-shadow: 0 30px 80px rgb(0 0 0 / 0.16);
+}
+.terminal-caret {
+ display: inline-block;
+ width: 0.5rem;
+ height: 1rem;
+ margin-left: 0.2rem;
+ vertical-align: middle;
+ background: var(--ec-accent);
+ animation: blink 1s steps(2) infinite;
+}
+@keyframes blink {
+ 50% {
+ opacity: 0;
+ }
+}
+.cta-panel {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 3rem;
+ border-radius: 2rem;
+ padding: clamp(2rem, 6vw, 5rem);
+ background: var(--ec-accent);
+ color: var(--ec-accent-ink);
+}
+.cta-button {
+ display: inline-flex;
+ width: fit-content;
+ align-items: center;
+ gap: 0.65rem;
+ border-radius: 999px;
+ padding: 1rem 1.25rem;
+ background: var(--ec-accent-ink);
+ color: white;
+ font-size: 0.85rem;
+ font-weight: 700;
+}
+.cursor-glow {
+ position: relative;
+}
+.cursor-glow [data-glow-card] {
+ --card-glow-x: 50%;
+ --card-glow-y: 50%;
+ --card-glow-opacity: 0;
+ isolation: isolate;
+ position: relative;
+ overflow: hidden;
+}
+.cursor-glow [data-glow-card]::before {
+ content: "";
+ pointer-events: none;
+ position: absolute;
+ z-index: 0;
+ left: var(--card-glow-x);
+ top: var(--card-glow-y);
+ width: 19rem;
+ height: 19rem;
+ border-radius: 999px;
+ opacity: var(--card-glow-opacity);
+ background: radial-gradient(
+ circle,
+ var(--ec-glow-core),
+ var(--ec-glow) 42%,
+ transparent 72%
+ );
+ filter: blur(24px);
+ transform: translate(-50%, -50%);
+ transition: opacity 0.22s ease;
+ will-change: left, top, opacity;
+}
+.cursor-glow [data-glow-card] > :not([class*="absolute"]) {
+ position: relative;
+ z-index: 1;
+}
+
+main > div:not(.site-shell),
+main > section {
+ background-color: var(--ec-bg) !important;
+ color: var(--ec-text);
+}
+.bg-gray-50,
+.bg-light-gray-100 {
+ background-color: var(--ec-bg) !important;
+}
+.bg-white {
+ background-color: var(--ec-panel) !important;
+}
+.border-gray-200,
+.border-white\/10 {
+ border-color: var(--ec-line) !important;
+}
+.text-gray-900,
+.text-gray-800 {
+ color: var(--ec-text) !important;
+}
+.text-gray-600,
+.text-gray-500 {
+ color: var(--ec-muted) !important;
+}
+.bg-blue-600 {
+ background-color: var(--ec-accent) !important;
+ color: var(--ec-accent-ink) !important;
+}
+.text-blue-600 {
+ color: var(--ec-accent-text) !important;
+}
+.rounded-xl,
+.rounded-2xl {
+ border-radius: 1.1rem;
+}
+.prose {
+ --tw-prose-body: var(--ec-muted);
+ --tw-prose-headings: var(--ec-text);
+ --tw-prose-links: var(--ec-accent);
+ --tw-prose-bold: var(--ec-text);
+ --tw-prose-code: var(--ec-accent);
+}
+
+@media (min-width: 768px) {
+ .cta-panel {
+ flex-direction: row;
+ align-items: end;
+ }
+}
+@media (max-width: 640px) {
+ .project-card {
+ min-height: 27rem;
+ }
+}
+@media (hover: none), (pointer: coarse), (prefers-reduced-motion: reduce) {
+ .cursor-glow-blob {
+ display: none;
+ }
+}
+
+.code-panel [class*="text-white/"] {
+ color: rgb(255 255 255 / 0.48) !important;
+}
+
+/* Minimal Minecraft hero */
+.hero-minimal {
+ position: relative;
+ display: grid;
+ min-height: 92svh;
+ place-items: center;
+ overflow: hidden;
+ border-bottom: 1px solid var(--ec-line);
+ padding: 9rem 1rem 5rem;
+}
+.hero-minimal::before {
+ content: "";
+ position: absolute;
+ top: 20%;
+ left: 50%;
+ width: min(56rem, 90vw);
+ height: 24rem;
+ border-radius: 50%;
+ background: color-mix(in srgb, var(--ec-accent) 9%, transparent);
+ filter: blur(100px);
+ transform: translateX(-50%);
+}
+.hero-minimal-inner {
+ position: relative;
+ z-index: 2;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+}
+.hero-minimal-title {
+ max-width: 70rem;
+ margin-top: 1.7rem;
+ font-size: clamp(3.8rem, 9vw, 8.5rem);
+ font-weight: 600;
+ line-height: 0.88;
+ letter-spacing: -0.075em;
+}
+.hero-minimal-title span {
+ color: var(--ec-accent-text);
+}
+.hero-minimal-lead {
+ max-width: 43rem;
+ margin-top: 2rem;
+ color: var(--ec-muted);
+ font-size: clamp(1rem, 1.5vw, 1.18rem);
+ line-height: 1.75;
+}
+.hero-minimal-actions {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 0.75rem;
+ margin-top: 2rem;
+}
+.hero-minimal-scroll {
+ position: absolute;
+ z-index: 3;
+ bottom: 1.2rem;
+ left: 50%;
+ display: flex;
+ align-items: center;
+ gap: 0.45rem;
+ color: var(--ec-faint);
+ font-family: var(--font-jetbrains-mono), monospace;
+ font-size: 0.58rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ transform: translateX(-50%);
+ transition: color 0.2s ease;
+}
+.hero-minimal-scroll:hover {
+ color: var(--ec-text);
+}
+@media (max-width: 640px) {
+ .hero-minimal {
+ min-height: 88svh;
+ padding-top: 8rem;
+ }
+ .hero-minimal-title {
+ font-size: clamp(3.25rem, 16vw, 5.2rem);
+ }
+ .hero-minimal-scroll {
+ display: none;
+ }
+}
+
+/* EternalCore product page */
+.ec-project-page {
+ overflow: hidden;
+ background: var(--ec-bg);
+ color: var(--ec-text);
+}
+.ec-project-hero {
+ position: relative;
+ min-height: 100svh;
+ overflow: hidden;
+ border-bottom: 1px solid var(--ec-line);
+ padding-top: 9rem;
+}
+.ec-project-hero::before {
+ content: "";
+ position: absolute;
+ width: 46rem;
+ height: 46rem;
+ right: -14rem;
+ top: 5rem;
+ border-radius: 50%;
+ background: radial-gradient(
+ circle,
+ color-mix(in srgb, var(--ec-accent) 13%, transparent),
+ transparent 68%
+ );
+}
+.ec-project-hero-layout {
+ position: relative;
+ z-index: 2;
+ display: grid;
+ min-height: calc(100svh - 12rem);
+ align-items: center;
+ gap: clamp(3rem, 7vw, 7rem);
+ padding-bottom: 5rem;
+}
+.ec-project-copy {
+ max-width: 48rem;
+}
+.ec-project-copy h1 {
+ max-width: 54rem;
+ margin-top: 1.6rem;
+ font-size: clamp(3.8rem, 7.5vw, 7.6rem);
+ font-weight: 600;
+ line-height: 0.9;
+ letter-spacing: -0.072em;
+}
+.ec-project-copy h1 span {
+ color: var(--ec-accent-text);
+}
+.ec-project-copy > p {
+ max-width: 42rem;
+ margin-top: 1.7rem;
+ color: var(--ec-muted);
+ font-size: clamp(1rem, 1.4vw, 1.18rem);
+ line-height: 1.75;
+}
+.ec-project-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ margin-top: 2rem;
+}
+.ec-project-note {
+ display: flex;
+ align-items: center;
+ gap: 0.55rem;
+ margin-top: 2rem;
+ color: var(--ec-faint);
+ font-family: var(--font-jetbrains-mono), monospace;
+ font-size: 0.62rem;
+}
+.ec-project-note svg {
+ color: var(--ec-accent-text);
+}
+.ec-project-visual {
+ width: 100%;
+ max-width: 44rem;
+ justify-self: end;
+}
+.ec-project-banner-frame {
+ position: relative;
+ aspect-ratio: 16 / 10;
+ overflow: hidden;
+ border: 1px solid var(--ec-line);
+ border-radius: 1.8rem;
+ background: var(--ec-panel);
+ box-shadow: 0 40px 100px rgb(3 10 24 / 0.18);
+}
+.ec-project-banner-overlay {
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(145deg, transparent 52%, rgb(2 6 23 / 0.38));
+}
+.ec-project-banner-chip {
+ position: absolute;
+ border: 1px solid rgb(255 255 255 / 0.18);
+ border-radius: 999px;
+ padding: 0.5rem 0.7rem;
+ background: rgb(5 10 20 / 0.62);
+ color: white;
+ font-family: var(--font-jetbrains-mono), monospace;
+ font-size: 0.58rem;
+ backdrop-filter: blur(12px);
+}
+.ec-chip-top {
+ top: 1rem;
+ right: 1rem;
+}
+.ec-chip-bottom {
+ right: 1rem;
+ bottom: 1rem;
+}
+.ec-project-scroll {
+ position: absolute;
+ z-index: 3;
+ bottom: 1.2rem;
+ left: 50%;
+ display: flex;
+ align-items: center;
+ gap: 0.45rem;
+ color: var(--ec-faint);
+ font-family: var(--font-jetbrains-mono), monospace;
+ font-size: 0.58rem;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ transform: translateX(-50%);
+}
+.ec-capabilities {
+ padding-block: 8rem;
+}
+.ec-section-intro {
+ max-width: 55rem;
+}
+.ec-section-intro h2 {
+ margin-top: 1.5rem;
+ font-size: clamp(2.8rem, 5.4vw, 5.7rem);
+ font-weight: 600;
+ line-height: 0.96;
+ letter-spacing: -0.06em;
+}
+.ec-section-intro > p {
+ max-width: 42rem;
+ margin-top: 1.5rem;
+ color: var(--ec-muted);
+ line-height: 1.75;
+}
+.ec-capability-grid {
+ display: grid;
+ gap: 1px;
+ margin-top: 4rem;
+ overflow: hidden;
+ border: 1px solid var(--ec-line);
+ border-radius: 1.6rem;
+ background: var(--ec-line);
+}
+.ec-capability {
+ display: flex;
+ min-height: 15rem;
+ flex-direction: column;
+ justify-content: space-between;
+ padding: 1.5rem;
+ background: var(--ec-panel);
+}
+.ec-capability svg {
+ color: var(--ec-accent-text);
+}
+.ec-capability strong {
+ margin-top: 3rem;
+ font-family: var(--font-jetbrains-mono), monospace;
+ font-size: clamp(2.5rem, 5vw, 4.5rem);
+ font-weight: 500;
+ letter-spacing: -0.07em;
+}
+.ec-capability span {
+ max-width: 12rem;
+ color: var(--ec-muted);
+ font-size: 0.72rem;
+ line-height: 1.5;
+}
+.ec-showcase-section {
+ border-block: 1px solid var(--ec-line);
+ padding-block: 8rem;
+ background: var(--ec-panel-strong);
+}
+.ec-section-intro-wide {
+ max-width: 67rem;
+}
+.ec-showcase-list {
+ display: flex;
+ flex-direction: column;
+ gap: 9rem;
+ margin-top: 7rem;
+}
+.ec-showcase-row {
+ display: grid;
+ align-items: center;
+ gap: clamp(2rem, 7vw, 7rem);
+}
+.ec-showcase-copy {
+ max-width: 29rem;
+}
+.ec-showcase-copy > span {
+ color: var(--ec-accent-text);
+ font-family: var(--font-jetbrains-mono), monospace;
+ font-size: 0.62rem;
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+}
+.ec-showcase-copy h3 {
+ margin-top: 1rem;
+ font-size: clamp(2.25rem, 4vw, 4.4rem);
+ font-weight: 600;
+ line-height: 1;
+ letter-spacing: -0.055em;
+}
+.ec-showcase-copy p {
+ margin-top: 1.3rem;
+ color: var(--ec-muted);
+ line-height: 1.75;
+}
+.ec-showcase-copy a {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-top: 1.5rem;
+ color: var(--ec-accent-text);
+ font-size: 0.78rem;
+ font-weight: 700;
+}
+.ec-gif-frame {
+ position: relative;
+ aspect-ratio: 16 / 10;
+ overflow: hidden;
+ border: 1px solid var(--ec-line);
+ border-radius: 1.7rem;
+ background: #080b10;
+ box-shadow: 0 28px 70px rgb(3 10 24 / 0.16);
+}
+.ec-gif-frame::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ border-radius: inherit;
+ box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.06);
+}
+.ec-gif-blue {
+ box-shadow: 0 28px 80px rgb(37 99 235 / 0.14);
+}
+.ec-gif-indigo {
+ box-shadow: 0 28px 80px rgb(79 70 229 / 0.14);
+}
+.ec-gif-sky {
+ box-shadow: 0 28px 80px rgb(14 165 233 / 0.14);
+}
+.ec-mini-gallery {
+ display: grid;
+ gap: 1rem;
+ margin-top: 9rem;
+}
+.ec-mini-card {
+ overflow: hidden;
+ border: 1px solid var(--ec-line);
+ border-radius: 1.25rem;
+ background: var(--ec-panel);
+}
+.ec-mini-image {
+ position: relative;
+ aspect-ratio: 16 / 10;
+ overflow: hidden;
+ background: #080b10;
+}
+.ec-mini-card > div:last-child {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.9rem 1rem;
+ color: var(--ec-muted);
+ font-size: 0.7rem;
+}
+.ec-mini-card svg {
+ color: var(--ec-accent-text);
+}
+.ec-config-section {
+ display: grid;
+ align-items: center;
+ gap: clamp(3rem, 8vw, 8rem);
+ padding-block: 10rem;
+}
+.ec-config-copy {
+ max-width: 34rem;
+}
+.ec-config-copy h2 {
+ margin-top: 1.4rem;
+ font-size: clamp(2.7rem, 5vw, 5.3rem);
+ font-weight: 600;
+ line-height: 0.97;
+ letter-spacing: -0.06em;
+}
+.ec-config-copy > p {
+ margin-top: 1.4rem;
+ color: var(--ec-muted);
+ line-height: 1.75;
+}
+.ec-config-copy ul {
+ display: grid;
+ gap: 0.8rem;
+ margin-top: 2rem;
+}
+.ec-config-copy li {
+ display: flex;
+ align-items: center;
+ gap: 0.65rem;
+ color: var(--ec-muted);
+ font-size: 0.78rem;
+}
+.ec-config-copy li svg {
+ color: var(--ec-accent-text);
+}
+.ec-config-preview {
+ min-width: 0;
+ overflow: hidden;
+ border: 1px solid var(--ec-line);
+ border-radius: 1.5rem;
+ background: var(--ec-code);
+ box-shadow: 0 35px 85px rgb(3 10 24 / 0.2);
+}
+.ec-config-window-bar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border-bottom: 1px solid rgb(255 255 255 / 0.1);
+ padding: 0.9rem 1rem;
+ color: rgb(255 255 255 / 0.45);
+ font-family: var(--font-jetbrains-mono), monospace;
+ font-size: 0.6rem;
+}
+.ec-config-code {
+ height: 36rem;
+ overflow: hidden;
+ padding: 1rem;
+ mask-image: linear-gradient(to bottom, black 76%, transparent);
+}
+.ec-config-code pre {
+ margin: 0 !important;
+ background: transparent !important;
+}
+.ec-project-cta {
+ display: flex;
+ align-items: end;
+ justify-content: space-between;
+ gap: 3rem;
+ margin-bottom: 8rem;
+ border-radius: 2rem;
+ padding: clamp(2rem, 6vw, 5rem);
+ background: linear-gradient(135deg, #2563eb, #4338ca);
+ color: white;
+}
+.ec-project-cta .section-kicker {
+ color: rgb(255 255 255 / 0.65);
+}
+.ec-project-cta .section-kicker span {
+ color: white;
+}
+.ec-project-cta h2 {
+ max-width: 52rem;
+ margin-top: 1.3rem;
+ font-size: clamp(2.7rem, 5vw, 5.5rem);
+ font-weight: 600;
+ line-height: 0.97;
+ letter-spacing: -0.06em;
+}
+.ec-project-cta-actions {
+ display: flex;
+ flex: none;
+ flex-direction: column;
+ gap: 0.7rem;
+}
+.ec-project-cta .primary-action {
+ background: white;
+ color: #111827;
+}
+.ec-project-cta .secondary-action {
+ border-color: rgb(255 255 255 / 0.25);
+ color: white;
+}
+@media (min-width: 768px) {
+ .ec-capability-grid {
+ grid-template-columns: repeat(4, 1fr);
+ }
+ .ec-mini-gallery {
+ grid-template-columns: repeat(3, 1fr);
+ }
+}
+@media (min-width: 1024px) {
+ .ec-project-hero-layout {
+ grid-template-columns: minmax(0, 1fr) minmax(31rem, 0.85fr);
+ }
+ .ec-showcase-row {
+ grid-template-columns: 0.7fr 1.3fr;
+ }
+ .ec-showcase-reverse {
+ grid-template-columns: 1.3fr 0.7fr;
+ }
+ .ec-showcase-reverse .ec-showcase-copy {
+ order: 2;
+ }
+ .ec-showcase-reverse .ec-gif-frame {
+ order: 1;
+ }
+ .ec-config-section {
+ grid-template-columns: 0.75fr 1.25fr;
+ }
+}
+@media (max-width: 1023px) {
+ .ec-project-hero {
+ padding-top: 8rem;
+ }
+ .ec-project-visual {
+ max-width: none;
+ justify-self: stretch;
+ }
+ .ec-config-preview {
+ transform: none !important;
+ }
+ .ec-project-cta {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+ .ec-project-cta-actions {
+ flex-direction: row;
+ flex-wrap: wrap;
+ }
+}
+@media (max-width: 640px) {
+ .ec-project-copy h1 {
+ font-size: clamp(3.3rem, 15vw, 5rem);
+ }
+ .ec-capabilities,
+ .ec-showcase-section,
+ .ec-config-section {
+ padding-block: 6rem;
+ }
+ .ec-showcase-list {
+ gap: 6rem;
+ margin-top: 5rem;
+ }
+ .ec-mini-gallery {
+ margin-top: 6rem;
+ }
+ .ec-config-code {
+ height: 30rem;
+ }
+ .ec-project-scroll {
+ display: none;
+ }
+}
+@media (prefers-reduced-motion: reduce) {
+ .ec-project-copy,
+ .ec-project-visual,
+ .ec-config-preview {
+ transform: none !important;
+ }
+}
+
+/* Clean community CTA */
+.cta-panel-clean {
+ position: relative;
+ overflow: visible;
+ align-items: end;
+ background: linear-gradient(135deg, #2563eb, #4338ca);
+ color: white;
+}
+.cta-wumpus-column {
+ display: flex;
+ width: 13rem;
+ flex: none;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.75rem;
+ align-self: stretch;
+ justify-content: flex-end;
+}
+.cta-wumpus {
+ position: relative;
+ display: grid;
+ width: 13.25rem;
+ height: 15.25rem;
+ margin-top: -3rem;
+ place-items: end center;
+}
+.cta-wumpus-glow {
+ position: absolute;
+ right: 1.75rem;
+ bottom: 1rem;
+ left: 1.75rem;
+ height: 3.75rem;
+ border-radius: 50%;
+ background: rgb(96 165 250 / 0.62);
+ filter: blur(28px);
+ transition:
+ transform 0.35s ease,
+ opacity 0.35s ease;
+}
+.cta-wumpus-image {
+ position: relative;
+ z-index: 1;
+ width: auto;
+ height: 15.75rem;
+ object-fit: contain;
+ filter: drop-shadow(0 18px 22px rgb(15 23 42 / 0.22));
+ transform: translateY(-0.9rem) rotate(-1deg);
+ transform-origin: 50% 80%;
+ transition: transform 0.35s cubic-bezier(0.2, 0.8, 0.2, 1);
+}
+.cta-wumpus:hover .cta-wumpus-image {
+ transform: translateY(-1.35rem) rotate(1.5deg) scale(1.025);
+}
+.cta-wumpus:hover .cta-wumpus-glow {
+ opacity: 0.9;
+ transform: scale(1.12);
+}
+.cta-button-clean {
+ width: 100%;
+ min-width: 12rem;
+ justify-content: center;
+ background: white;
+ color: #172554;
+ box-shadow: 0 14px 35px rgb(15 23 42 / 0.2);
+}
+.cta-button-clean:hover {
+ background: #eff6ff;
+ transform: translateY(-2px);
+}
+
+/* EternalCore minimal pass */
+.ec-project-hero::before {
+ width: 38rem;
+ height: 38rem;
+ opacity: 0.65;
+}
+.ec-project-banner-frame {
+ border-radius: 1.25rem;
+ box-shadow: 0 24px 60px rgb(3 10 24 / 0.12);
+}
+.ec-project-banner-overlay,
+.ec-project-banner-chip {
+ display: none;
+}
+.ec-capabilities {
+ padding-block: 7rem;
+}
+.ec-capability {
+ min-height: 12rem;
+}
+.ec-showcase-section {
+ padding-block: 7rem;
+ background: var(--ec-bg);
+}
+.ec-showcase-list {
+ gap: 7rem;
+ margin-top: 5rem;
+}
+.ec-showcase-row {
+ padding-block: 1rem;
+}
+.ec-gif-frame {
+ border-radius: 1.25rem;
+ box-shadow: none;
+}
+.ec-gif-blue,
+.ec-gif-indigo,
+.ec-gif-sky {
+ box-shadow: none;
+}
+.ec-mini-gallery {
+ display: none;
+}
+.ec-config-section {
+ padding-block: 8rem;
+}
+.ec-config-preview {
+ border-radius: 1.25rem;
+ box-shadow: 0 24px 60px rgb(3 10 24 / 0.14);
+}
+.ec-project-cta {
+ margin-bottom: 7rem;
+ border: 1px solid var(--ec-line);
+ background: var(--ec-panel);
+ color: var(--ec-text);
+}
+.ec-project-cta .section-kicker {
+ color: var(--ec-muted);
+}
+.ec-project-cta .section-kicker span {
+ color: var(--ec-accent-text);
+}
+.ec-project-cta .primary-action {
+ background: var(--ec-accent);
+ color: var(--ec-accent-ink);
+}
+.ec-project-cta .secondary-action {
+ border-color: var(--ec-line);
+ color: var(--ec-muted);
+}
+@media (max-width: 767px) {
+ .cta-panel-clean {
+ align-items: flex-start;
+ }
+ .cta-wumpus-column {
+ width: 100%;
+ }
+ .cta-wumpus {
+ width: 11.5rem;
+ height: 12.75rem;
+ margin-top: -1rem;
+ }
+ .cta-wumpus-image {
+ height: 13.25rem;
+ }
+ .cta-button-clean {
+ width: 100%;
+ }
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index 8054d389..eeaddaf3 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,5 +1,5 @@
import type { Metadata, Viewport } from "next";
-import { JetBrains_Mono, Poppins } from "next/font/google";
+import { JetBrains_Mono, Manrope } from "next/font/google";
import NextTopLoader from "nextjs-toploader";
import "./globals.css";
import "lenis/dist/lenis.css";
@@ -15,13 +15,12 @@ import { Providers } from "./providers";
export const dynamic = "force-static";
export const revalidate = 5;
-const poppins = Poppins({
- weight: ["400", "500", "600", "700"],
+const manrope = Manrope({
subsets: ["latin"],
display: "swap",
preload: true,
fallback: ["system-ui", "arial"],
- variable: "--font-poppins",
+ variable: "--font-manrope",
});
const jetbrainsMono = JetBrains_Mono({
@@ -36,8 +35,8 @@ export const viewport: Viewport = {
maximumScale: 5,
userScalable: true,
themeColor: [
- { media: "(prefers-color-scheme: light)", color: "#eff1f5" },
- { media: "(prefers-color-scheme: dark)", color: "#0d1117" },
+ { media: "(prefers-color-scheme: light)", color: "#0b0d0c" },
+ { media: "(prefers-color-scheme: dark)", color: "#0b0d0c" },
],
};
@@ -108,12 +107,12 @@ export default function RootLayout({
}>) {
return (
@@ -124,7 +123,7 @@ export default function RootLayout({
easing="ease"
height={3}
initialPosition={0.08}
- shadow="0 0 10px #3b82f6,0 0 5px #3b82f6"
+ shadow="0 0 10px #3b82f6"
showSpinner={false}
speed={200}
/>
@@ -132,7 +131,7 @@ export default function RootLayout({
-
+
{children}
diff --git a/app/notification-generator/page.tsx b/app/notification-generator/page.tsx
index d1245b4b..37422c6f 100644
--- a/app/notification-generator/page.tsx
+++ b/app/notification-generator/page.tsx
@@ -62,7 +62,7 @@ export default function NotificationGeneratorPage() {
-
+
Notification Generator
diff --git a/app/projects/eternalcombat/page.tsx b/app/projects/eternalcombat/page.tsx
index 365a298e..3b39e63c 100644
--- a/app/projects/eternalcombat/page.tsx
+++ b/app/projects/eternalcombat/page.tsx
@@ -20,12 +20,12 @@ export default function EternalCombatPage() {
const scale = useTransform(scrollYProgress, [0, 0.5], [1, 0.95]);
return (
-
+
{/* Background Decor */}
@@ -42,9 +42,9 @@ export default function EternalCombatPage() {
PvP Evolved
-
+
Combat Logging,
-
+
Solved Forever.
@@ -103,7 +103,7 @@ export default function EternalCombatPage() {
-
+
@@ -145,7 +145,7 @@ export default function EternalCombatPage() {
[19:23:01] {" "}
-
+
⚠ You are in combat!
{" "}
@@ -153,7 +153,7 @@ export default function EternalCombatPage() {
...
-
+
[19:23:45] vLucky tried to
disconnect.
diff --git a/app/projects/eternalcore/page.tsx b/app/projects/eternalcore/page.tsx
index ec290222..8f95c8b7 100644
--- a/app/projects/eternalcore/page.tsx
+++ b/app/projects/eternalcore/page.tsx
@@ -1,317 +1,239 @@
"use client";
-import { m, motion, useScroll, useTransform } from "framer-motion";
-import { Book, Check, ChevronRight, Download, Layers, Settings, Zap } from "lucide-react";
+import { motion, useScroll, useTransform } from "framer-motion";
+import {
+ ArrowDownRight,
+ ArrowRight,
+ BookOpen,
+ Box,
+ Check,
+ Download,
+ Github,
+ Layers3,
+ MessagesSquare,
+ Settings2,
+ ShieldCheck,
+ Zap,
+} from "lucide-react";
import Image from "next/image";
+import Link from "next/link";
import { useRef } from "react";
-import { Button } from "@/components/ui/button";
-import { Card } from "@/components/ui/card";
-import { FacadePattern } from "@/components/ui/facade-pattern";
-import { FadeIn, MotionSection, ScaleIn, SlideIn } from "@/components/ui/motion/motion-components";
-import { slideUp } from "@/lib/animations/variants";
-import netlifyImageLoader from "@/lib/netlify-image-loader";
-
import { ConfigPreview } from "./config-preview";
-import { EternalShowcase } from "./eternal-showcase";
+
+const capabilities = [
+ { icon: Box, value: "80+", label: "commands without legacy baggage" },
+ { icon: Zap, value: "Paper", label: "built for the modern server stack" },
+ { icon: Layers3, value: "Folia", label: "region-aware from the beginning" },
+ { icon: ShieldCheck, value: "OSS", label: "inspect it, change it, ship it" },
+];
+
+const showcases = [
+ {
+ eyebrow: "Homes",
+ title: "Teleportation that feels predictable.",
+ description:
+ "Named homes, permission-based limits and clear feedback for players. Useful defaults first; configuration when you need it.",
+ image: "/docs/eternalcore/homes/sethome.gif",
+ accent: "blue",
+ },
+ {
+ eyebrow: "Communication",
+ title: "Messages people can actually read.",
+ description:
+ "Private messages, broadcasts and admin channels with modern formatting—without assembling a small novel of nested builders.",
+ image: "/docs/eternalcore/adminchat-channel.gif",
+ accent: "sky",
+ },
+];
export default function EternalCorePage() {
- const targetRef = useRef(null);
- const { scrollYProgress } = useScroll({
- target: targetRef,
+ const heroRef = useRef
(null);
+ const configRef = useRef(null);
+ const { scrollYProgress: heroProgress } = useScroll({
+ target: heroRef,
offset: ["start start", "end start"],
});
-
- const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0]);
- const scale = useTransform(scrollYProgress, [0, 0.5], [1, 0.95]);
+ const { scrollYProgress: configProgress } = useScroll({
+ target: configRef,
+ offset: ["start end", "end start"],
+ });
+ const heroY = useTransform(heroProgress, [0, 1], [0, 120]);
+ const heroScale = useTransform(heroProgress, [0, 0.8], [1, 0.94]);
+ const configY = useTransform(configProgress, [0, 1], [70, -70]);
return (
-
- {/* Background Decor */}
-
-
-
- {/* Hero Section */}
-
-
- {/* Text Content */}
-
-
-
-
-
- Next-Gen Essentials
-
-
-
-
-
- Essential commands.
-
-
- Uncompromised performance.
-
-
-
-
-
-
- EternalCore is a modern, open-source replacement for EssentialsX. Built for
- Paper & Folia to deliver maximum performance without the legacy bloat.
-
-
-
-
- }
- shine
- variant="primary"
- >
- Download
-
-
- }
- rel="noopener noreferrer"
- target="_blank"
- variant="outline"
- >
- Documentation
-
-
-
+
+
+
+
+
+ EternalCore · Open source Minecraft plugin
+
+ Everything your server needs.
+ Nothing it does not.
+
+
+ A modern essentials plugin for Paper and Folia. Commands, homes, moderation, chat and
+ everyday server tools—designed as one coherent experience.
+
+
+
+ Download EternalCore
+
+
+ Read documentation
+
-
- {/* Project Banner Placeholder */}
-
-
-
-
+
+ Free, open source and not followed by an enterprise
+ pricing call.
-
- {/* Features Section */}
-
-
-
-
- The core of your network.
-
-
- Everything you need to run a professional server, available for free.
-
-
+
+
+
+
+
+ See it in action
+
+
+
+
+
+
+ 01 One plugin, one consistent system
+
+
Essentials rebuilt for how servers run today.
+
+ EternalCore replaces a patchwork of utilities with a focused foundation that is easier
+ to operate, easier to configure and easier to extend.
+
+
+
+ {capabilities.map((item) => {
+ const Icon = item.icon;
+ return (
+
+
+ {item.value}
+ {item.label}
+
+ );
+ })}
+
+
+
+
+
+
+
+ 02 Real features, not mockups
+
+
Watch the plugin work.
+
Two everyday workflows, shown exactly as players and administrators use them.
-
- {[
- {
- title: "80+ Commands",
- description:
- "From essential administration to fun mechanics, we have covered every use case you can imagine.",
- icon: Layers,
- iconColor: "text-purple-500",
- gradient: "from-purple-500/20 to-indigo-500/20",
- },
- {
- title: "Open Source & Free",
- description:
- "EternalCore is and will always be free. Inspect the code, contribute, and build with confidence.",
- icon: Download,
- iconColor: "text-green-500",
- gradient: "from-green-500/20 to-emerald-500/20",
- },
- {
- title: "Paper & Folia",
- description:
- "Built specifically for modern server software. 100% async and optimized for high-performance.",
- icon: Zap,
- iconColor: "text-yellow-500",
- gradient: "from-yellow-500/20 to-orange-500/20",
- },
- {
- title: "Rich Formatting",
- description:
- "Full support for modern MiniMessage and legacy &c&lcolor codes simultaneously.",
- icon: Check,
- iconColor: "text-blue-500",
- gradient: "from-blue-500/20 to-cyan-500/20",
- },
- ].map((feature) => (
-
-
-
+ {showcases.map((showcase, index) => (
+
+
+
{showcase.eyebrow}
+
{showcase.title}
+
{showcase.description}
+
+ Explore this feature
+
+
+
+
-
-
-
-
-
- {feature.title}
-
-
- {feature.description}
-
-
-
+
+
))}
-
-
-
-
-
-
-
- {/* Configuration Section */}
-
-
-
-
-
- Configuration
-
-
- Configuration
- Done Right.
-
-
- Forget about guessing property names. EternalCore uses a strongly-typed
- configuration system that ensures your settings are always valid and automatically
- up-to-date.
-
-
- {[
- "Automatic config updates",
- "Detailed comments generated automatically",
- "Reload-safe architecture",
- ].map((item) => (
-
-
-
-
- {item}
-
- ))}
-
-
-
-
-
- {/* The "Long Screenshot" Container */}
-
- {/* Window Controls */}
-
-
-
- eternalcore
-
- config.yml
-
-
-
- {/* Tilted Content */}
- {/* Content Container (Standard, non-tilted) */}
-
- {/* Inner blur container */}
-
- {/* Auto-scrolling animation container */}
-
-
- {/* Duplicate content for seamless loop */}
-
-
-
-
-
- {/* Gradient overlay to fade bottom */}
-
+
+
+
- {/* Animation Styles */}
-
-
-
-
+
+
+
+ 03 Configuration without archaeology
-
+
Detailed when you need it. Sensible before you touch it.
+
+ Start with useful defaults, then tune homes, chat, databases, cooldowns and server
+ behavior from one readable configuration.
+
+
+
+ Human-readable YAML
+
+
+ English and Polish messages
+
+
+ SQLite, MySQL, MariaDB and PostgreSQL
+
+
+
+
+
+ config.yml
+ EternalCore / main
+
+
+
+
+
+
- {/* Footer CTA */}
-
-
-
- Ready to upgrade your server?
-
-
- Join hundreds of servers already using EternalCore for a more stable and efficient
- experience.
-
-
-
- Get Started
-
-
- View Source
-
-
-
-
-
+
+
+
+ 04 Ready when your server is
+
+
Replace the essentials pile with one modern core.
+
+
+
);
}
diff --git a/components/builds/build-header.tsx b/components/builds/build-header.tsx
index b9668752..1fd40371 100644
--- a/components/builds/build-header.tsx
+++ b/components/builds/build-header.tsx
@@ -4,12 +4,18 @@ export function BuildHeader() {
return (
-
+
+ Releases Distribution center
+
+
Build Explorer
Access stable releases and development builds for all our projects.
+
+ Stable means tested. Dev means you enjoy surprises.
+
);
diff --git a/components/builds/build-row.tsx b/components/builds/build-row.tsx
index e4102435..045e3412 100644
--- a/components/builds/build-row.tsx
+++ b/components/builds/build-row.tsx
@@ -26,8 +26,8 @@ function BuildStatusBadge({ type }: { type: "STABLE" | "DEV" }) {
{type === "STABLE" ?
:
}
diff --git a/components/contribute/contribute-hero.tsx b/components/contribute/contribute-hero.tsx
index b52ed2dc..7f26b793 100644
--- a/components/contribute/contribute-hero.tsx
+++ b/components/contribute/contribute-hero.tsx
@@ -4,17 +4,15 @@ import { MotionSection, SlideIn } from "@/components/ui/motion/motion-components
export function ContributeHero() {
return (
-
+
-
- Become an Open Source{" "}
-
-
- Volunteer
-
- {" "}
+
+ Contribute Build in public
+
+
+ Make open source better with us.
diff --git a/components/contribute/contribution-card.tsx b/components/contribute/contribution-card.tsx
index ca1d4198..5f4cb017 100644
--- a/components/contribute/contribution-card.tsx
+++ b/components/contribute/contribution-card.tsx
@@ -38,7 +38,7 @@ export function ContributionCard({
return (
-
+
diff --git a/components/docs/eternalcore/placeholder/placeholder-table.tsx b/components/docs/eternalcore/placeholder/placeholder-table.tsx
index 1ae385af..ab71a5b5 100644
--- a/components/docs/eternalcore/placeholder/placeholder-table.tsx
+++ b/components/docs/eternalcore/placeholder/placeholder-table.tsx
@@ -57,7 +57,7 @@ export function PlaceholderTable({ placeholders }: PlaceholderTableProps) {
className={cn(
"rounded px-2 py-0.5 font-medium text-xs",
p.requiresPlayer
- ? "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300"
+ ? "bg-sky-100 text-sky-700 dark:bg-sky-900/30 dark:text-sky-300"
: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
)}
>
diff --git a/components/docs/search/search-modal.tsx b/components/docs/search/search-modal.tsx
index c0958049..07a1ece9 100644
--- a/components/docs/search/search-modal.tsx
+++ b/components/docs/search/search-modal.tsx
@@ -45,7 +45,7 @@ const CATEGORY_BADGE_STYLES: Record = {
Multification:
"bg-blue-100 text-blue-700 ring-blue-200 dark:bg-blue-500/20 dark:text-blue-200 dark:ring-blue-500/40",
Contribute:
- "bg-emerald-100 text-emerald-700 ring-emerald-200 dark:bg-emerald-500/20 dark:text-emerald-200 dark:ring-emerald-500/40",
+ "bg-sky-100 text-sky-700 ring-sky-200 dark:bg-sky-500/20 dark:text-sky-200 dark:ring-sky-500/40",
};
const DOCS_PREFIX_REGEX = /^\/docs\//;
diff --git a/components/docs/sidebar/doc-sidebar.tsx b/components/docs/sidebar/doc-sidebar.tsx
index 8bc3c609..71bdae48 100644
--- a/components/docs/sidebar/doc-sidebar.tsx
+++ b/components/docs/sidebar/doc-sidebar.tsx
@@ -75,7 +75,7 @@ const DocSidebar: FC = ({ className = "", onItemClick, sidebarS
rotate: prefersReducedMotion ? 0 : 5,
}}
>
-
+
Documentation
diff --git a/components/docs/view/docs-card.tsx b/components/docs/view/docs-card.tsx
index ad237b75..cc3a4c2c 100644
--- a/components/docs/view/docs-card.tsx
+++ b/components/docs/view/docs-card.tsx
@@ -17,7 +17,7 @@ export function DocsCard({ project }: DocsCardProps) {
return (
-
+
diff --git a/components/docs/view/docs-header.tsx b/components/docs/view/docs-header.tsx
index 472e3e95..1f0381b7 100644
--- a/components/docs/view/docs-header.tsx
+++ b/components/docs/view/docs-header.tsx
@@ -4,16 +4,22 @@ export function DocsHeader() {
const { title, description } = DOCS_PAGE_CONFIG;
return (
-
+
-
+
+ Library Product documentation
+
+
{title}
{description}
+
+ Written by humans. Tested by humans. Typos also provided by humans.
+
diff --git a/components/footer/footer.tsx b/components/footer/footer.tsx
index a2a19c95..0e8b8f01 100644
--- a/components/footer/footer.tsx
+++ b/components/footer/footer.tsx
@@ -1,214 +1,74 @@
-"use client";
-
-import { Icon } from "@iconify/react";
-import { motion } from "framer-motion";
+import { ArrowUpRight } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
-
-import DiscordIcon from "@/components/icons/discord";
-import GitHubIcon from "@/components/icons/github";
-import TikTokIcon from "@/components/icons/round-tiktok";
-import YouTubeIcon from "@/components/icons/youtube";
import logo from "@/public/logo.svg";
-interface FooterLink {
- href: string;
- label: string;
- isExternal?: boolean;
-}
-
-interface FooterSection {
- title: string;
- links: FooterLink[];
-}
-
-const footerSections: FooterSection[] = [
+const groups = [
{
- title: "Product",
+ title: "Explore",
links: [
- { href: "/builds", label: "Build Explorer" },
- { href: "https://docs.eternalcode.pl/", label: "Documentation", isExternal: true },
- { href: "https://repo.eternalcode.pl/#/", label: "Repository", isExternal: true },
- { href: "https://status.eternalcode.pl/", label: "Status", isExternal: true },
+ ["Documentation", "/docs"],
+ ["Team", "/team"],
+ ["Contribute", "/contribute"],
+ ["Builds", "/builds"],
],
},
{
title: "Projects",
links: [
- { href: "/projects/eternalcore", label: "EternalCore" },
- { href: "/projects/eternalcombat", label: "EternalCombat" },
- ],
- },
- {
- title: "Community",
- links: [
- { href: "https://discord.com/invite/FQ7jmGBd6c", label: "Discord", isExternal: true },
- { href: "https://github.com/EternalCodeTeam", label: "GitHub", isExternal: true },
- {
- href: "https://www.youtube.com/channel/UC2BEaMJWxxUscN50AD0oY_Q",
- label: "YouTube",
- isExternal: true,
- },
- { href: "https://www.tiktok.com/@eternalcode.pl", label: "TikTok", isExternal: true },
+ ["EternalCore", "/projects/eternalcore"],
+ ["EternalCombat", "/projects/eternalcombat"],
+ ["Multification", "/docs/multification"],
],
},
- {
- title: "Resource",
- links: [
- {
- href: "https://spigotmc.org/resources/authors/eternalcodeteam.1570200/",
- label: "SpigotMC",
- isExternal: true,
- },
- {
- href: "https://modrinth.com/organization/eternalcodeteam",
- label: "Modrinth",
- isExternal: true,
- },
- { href: "https://bstats.org/author/vLucky", label: "bStats", isExternal: true },
- ],
- },
- {
- title: "Company",
- links: [
- { href: "/#about", label: "About" },
- { href: "/team", label: "Team" },
- { href: "/contribute", label: "Contribute" },
- ],
- },
-];
-
-const socialLinks = [
- {
- href: "https://github.com/EternalCodeTeam",
- label: "GitHub",
- icon: ,
- },
- {
- href: "https://discord.com/invite/FQ7jmGBd6c",
- label: "Discord",
- icon: ,
- },
- {
- href: "https://www.youtube.com/channel/UC2BEaMJWxxUscN50AD0oY_Q",
- label: "YouTube",
- icon: ,
- },
- {
- href: "https://www.tiktok.com/@eternalcode.pl",
- label: "TikTok",
- icon: ,
- },
];
export default function Footer() {
- const currentYear = new Date().getFullYear();
-
return (
-
-
-
-
-
-
-
- EternalCode
-
-
-
- Building high-quality, open-source Minecraft solutions. Empowering communities with
- reliable software since 2021.
-
-
- {socialLinks.map((item) => (
-
+
+
+
+
+
+
+
EternalCode.
+
+
+ Open-source infrastructure for ambitious Minecraft communities, designed and maintained
+ in Poland.
+
+
+ Follow development
+
+
+ {groups.map((group) => (
+
+
+ {group.title}
+
+
+ {group.links.map(([label, href]) => (
+
- {item.icon}
-
+ {label}
+
))}
-
-
- {footerSections.map((section) => (
-
-
- {section.title}
-
-
- {section.links.map((link) => (
-
- {link.isExternal ? (
-
- {link.label}
-
- ) : (
-
- {link.label}
-
- )}
-
- ))}
-
-
- ))}
-
-
-
-
-
-
- © {currentYear} EternalCodeTeam. All rights reserved.
-
-
-
-
-
-
-
-
-
- Powered by Netlify
-
-
-
- Designed with ❤ by the EternalCodeTeam.
-
-
-
-
+ ))}
+
+
+ © {new Date().getFullYear()} EternalCodeTeam
+ Open source. Built with care. Occasionally fixed after midnight.
);
diff --git a/components/hero/hero.tsx b/components/hero/hero.tsx
index d5ea9902..1d046a35 100644
--- a/components/hero/hero.tsx
+++ b/components/hero/hero.tsx
@@ -1,106 +1,66 @@
"use client";
-import { m } from "framer-motion";
-import { Github } from "lucide-react";
-import ArrowForwardHeroIcon from "@/components/icons/arrow-forward-hero";
-import { Button } from "@/components/ui/button";
-import { FadeIn, MotionSection, SlideIn } from "@/components/ui/motion/motion-components";
-import Terminal from "./terminal/terminal-window";
+import { motion } from "framer-motion";
+import { ArrowDownRight, ArrowRight, Github } from "lucide-react";
+import Link from "next/link";
export default function Hero() {
return (
-
- {/* Background Blob - using CSS animation for better performance */}
-
+
+
+
+
+ Open source, built by friends
+
-
- {/* Text Content */}
-
- {/* Title */}
-
-
-
- EternalCode.pl
-
-
-
+
+ Minecraft plugins
+
+ made for real servers.
+
- {/* Subtitle */}
-
-
- Building the future with{" "}
-
-
- open source
-
-
-
-
-
- . Join a community of passionate developers crafting the next generation of software
- tools.
-
-
+
+ We are a group of friends building open-source tools we enjoy using ourselves. Useful
+ defaults, readable docs and no corporate nonsense.
+
- {/* Buttons */}
-
+
+ Explore plugins
+
+
-
- }
- shine
- size="lg"
- variant="primary"
- >
- About us
-
+ GitHub
+
+
+
- }
- rel="noopener noreferrer"
- size="lg"
- target="_blank"
- variant="outline"
- >
- GitHub
-
-
-
-
- {/* Terminal - Right Side */}
-
-
-
-
-
-
-
+
+ More about us
+
+
);
}
diff --git a/components/hero/navbar.tsx b/components/hero/navbar.tsx
index 63fa258e..c0cdc1ca 100644
--- a/components/hero/navbar.tsx
+++ b/components/hero/navbar.tsx
@@ -1,446 +1,96 @@
"use client";
import { AnimatePresence, motion } from "framer-motion";
-import { ChevronDown, ExternalLink, Menu, Wrench, X } from "lucide-react";
+import { ArrowUpRight, Menu, X } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
-import { useCallback, useEffect, useRef, useState } from "react";
-import { createPortal } from "react-dom";
-
-import AnnouncementBanner from "@/components/hero/announcement-banner";
-import { slideDown } from "@/lib/animations/variants";
-import { cn } from "@/lib/utils";
+import { useState } from "react";
import logo from "@/public/logo.svg";
import ThemeSwitchButton from "./theme-switch-button";
-import { TOOLS_OPTIONS } from "./tools-dropdown";
-
-interface NavLink {
- href: string;
- label: string;
- isExternal?: boolean;
-}
-const NAV_LINKS: NavLink[] = [
+const links = [
{ href: "/", label: "Home" },
- { href: "/team", label: "Team" },
{ href: "/docs", label: "Docs" },
+ { href: "/team", label: "Team" },
{ href: "/contribute", label: "Contribute" },
+ { href: "/builds", label: "Builds" },
];
-const EXTERNAL_LINKS: NavLink[] = [
- {
- href: "https://repo.eternalcode.pl/#/",
- label: "Repository",
- isExternal: true,
- },
- {
- href: "https://status.eternalcode.pl/",
- label: "Status",
- isExternal: true,
- },
-];
-
-const TOOLS_LINKS = TOOLS_OPTIONS.map((opt) => ({
- label: opt.label,
- href: opt.value,
-}));
+export default function Navbar() {
+ const pathname = usePathname();
+ const [open, setOpen] = useState(false);
-function NavItem({ href, label, isActive, isExternal }: NavLink & { isActive: boolean }) {
return (
-
-
- {label}
- {!!isExternal && }
-
- {!!isActive && (
-
- )}
-
- );
-}
-
-function ToolsMenu() {
- const [isOpen, setIsOpen] = useState(false);
- const menuRef = useRef
(null);
-
- useEffect(() => {
- const handleClick = (e: MouseEvent) => {
- if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
- setIsOpen(false);
- }
- };
- if (isOpen) {
- document.addEventListener("mousedown", handleClick);
- }
- return () => document.removeEventListener("mousedown", handleClick);
- }, [isOpen]);
+
+
+
+
+
+
+
+ EternalCode.
+
+
+
+
+ {links.map((link) => {
+ const active = link.href === "/" ? pathname === "/" : pathname.startsWith(link.href);
+ return (
+
+ {link.label}
+
+ );
+ })}
+
- return (
-
-
setIsOpen((v) => !v)}
- type="button"
- >
-
- Tools
-
-
+
+
+
+ GitHub
+
+
setOpen((value) => !value)}
+ type="button"
+ >
+ {open ? : }
+
+
+
- {!!isOpen && (
+ {open ? (
- {TOOLS_LINKS.map((tool) => (
+ {links.map((link) => (
setIsOpen(false)}
+ className="block rounded-xl px-4 py-3 font-medium text-[var(--ec-muted)] hover:bg-[var(--ec-soft)] hover:text-[var(--ec-text)]"
+ href={link.href}
+ key={link.href}
+ onClick={() => setOpen(false)}
>
- {tool.label}
+ {link.label}
))}
- )}
+ ) : null}
-
- );
-}
-
-function MobileNav({
- isOpen,
- onClose,
- pathname,
-}: {
- isOpen: boolean;
- onClose: () => void;
- pathname: string;
-}) {
- const [toolsOpen, setToolsOpen] = useState(false);
- const menuRef = useRef(null);
-
- useEffect(() => {
- if (isOpen) {
- document.body.style.overflow = "hidden";
- } else {
- document.body.style.overflow = "";
- }
- return () => {
- document.body.style.overflow = "";
- };
- }, [isOpen]);
-
- useEffect(() => {
- const handleEscape = (e: KeyboardEvent) => {
- if (e.key === "Escape") {
- onClose();
- }
- };
- if (isOpen) {
- document.addEventListener("keydown", handleEscape);
- }
- return () => document.removeEventListener("keydown", handleEscape);
- }, [isOpen, onClose]);
-
- return (
-
- {!!isOpen && (
- <>
-
-
- {/* Header */}
-
-
-
- EternalCode
-
-
-
-
-
-
- {/* Navigation */}
-
-
- {NAV_LINKS.map((link, i) => {
- const active = pathname === link.href;
- return (
-
-
- {link.label}
-
-
- );
- })}
-
-
- {/* Tools accordion */}
-
- setToolsOpen((v) => !v)}
- type="button"
- >
-
-
- Tools
-
-
-
-
- {!!toolsOpen && (
-
- {TOOLS_LINKS.map((tool) => (
-
-
- {tool.label}
-
-
- ))}
-
- )}
-
-
-
- {/* External links */}
-
-
-
- {/* Footer */}
-
-
-
- © {new Date().getFullYear()} EternalCode
-
-
-
-
-
- >
- )}
-
- );
-}
-
-export default function Navbar() {
- const [isMenuOpen, setIsMenuOpen] = useState(false);
- const [scrolled, setScrolled] = useState(false);
- const [isMounted, setIsMounted] = useState(false);
- const pathname = usePathname();
-
- const closeMenu = useCallback(() => setIsMenuOpen(false), []);
-
- useEffect(() => {
- setIsMounted(true);
- }, []);
-
- // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally close menu on pathname change
- useEffect(() => {
- setIsMenuOpen(false);
- }, [pathname]);
-
- useEffect(() => {
- let ticking = false;
- const handleScroll = () => {
- if (!ticking) {
- window.requestAnimationFrame(() => {
- setScrolled(window.scrollY > 10);
- ticking = false;
- });
- ticking = true;
- }
- };
- window.addEventListener("scroll", handleScroll, { passive: true });
- return () => window.removeEventListener("scroll", handleScroll);
- }, []);
-
- return (
- <>
-
-
-
-
- {/* Logo */}
-
-
-
- EternalCode
-
-
-
- {/* Desktop nav */}
-
- {NAV_LINKS.map((link) => (
-
- ))}
-
-
- {EXTERNAL_LINKS.map((link) => (
-
- ))}
-
-
- {/* Right side */}
-
-
-
-
-
- {/* Mobile hamburger */}
-
setIsMenuOpen((v) => !v)}
- type="button"
- >
-
-
-
-
-
-
- {isMounted &&
- createPortal(
- ,
- document.body
- )}
- >
+
);
}
diff --git a/components/home/animated-home.tsx b/components/home/animated-home.tsx
index 634771fa..3370738f 100644
--- a/components/home/animated-home.tsx
+++ b/components/home/animated-home.tsx
@@ -1,77 +1,237 @@
"use client";
-import { m } from "framer-motion";
-import dynamic from "next/dynamic";
+import { motion } from "framer-motion";
+import {
+ ArrowRight,
+ Blocks,
+ BookOpen,
+ Box,
+ Check,
+ Code2,
+ Github,
+ PackageCheck,
+ Sparkles,
+ Users,
+} from "lucide-react";
+import Link from "next/link";
import Hero from "@/components/hero/hero";
-import { FacadePattern } from "@/components/ui/facade-pattern";
-import VisibleSection from "@/components/ui/visible-section";
+import Cta from "@/components/shared/cta-section";
+import { CursorGlow } from "@/components/ui/cursor-glow";
-const About = dynamic(() => import("@/components/home/about/about-section"), {
- loading: () =>
,
-});
-const Cta = dynamic(() => import("@/components/shared/cta-section"), {
- loading: () =>
,
-});
-const Faq = dynamic(() => import("@/components/home/faq/faq-section"), {
- loading: () =>
,
-});
-const Features = dynamic(() => import("@/components/home/features/features"), {
- loading: () =>
,
-});
-const Sponsors = dynamic(() => import("@/components/home/sponsors/sponsors-section"), {
- loading: () =>
,
-});
+const projects = [
+ {
+ name: "EternalCore",
+ description: "A complete, production-ready foundation for modern Minecraft servers.",
+ href: "/projects/eternalcore",
+ tag: "Server toolkit",
+ icon: Box,
+ tone: "blue",
+ },
+ {
+ name: "EternalCombat",
+ description: "Precise combat control designed for competitive communities.",
+ href: "/projects/eternalcombat",
+ tag: "Combat engine",
+ icon: Sparkles,
+ tone: "violet",
+ },
+ {
+ name: "Multification",
+ description: "A flexible text format that makes rich messages effortless.",
+ href: "/docs/multification",
+ tag: "Developer library",
+ icon: Code2,
+ tone: "cyan",
+ },
+];
+
+const principles = [
+ {
+ icon: PackageCheck,
+ title: "Production first",
+ text: "Tools shaped by real servers, strict reviews and measurable performance.",
+ },
+ {
+ icon: BookOpen,
+ title: "Documented deeply",
+ text: "Clear guides, practical examples and APIs that respect your time.",
+ },
+ {
+ icon: Users,
+ title: "Built in public",
+ text: "Open roadmaps, transparent decisions and a community that can contribute.",
+ },
+ {
+ icon: Blocks,
+ title: "Designed to compose",
+ text: "Focused projects that work brilliantly alone and even better together.",
+ },
+];
export default function AnimatedHome() {
return (
-
-
-
-
-
+
+
-
+
+
+ 01 Selected projects
+
+
+ Things we built because apparently playing Minecraft was not enough.
+
+
+ {projects.map((project, index) => {
+ const Icon = project.icon;
+ return (
+
+
+
+
+
+
+
+ 0{index + 1} / 03
+
+
+
+
+ {project.tag}
+
+
+ {project.name}
+
+
+ {project.description}
+
+
+
+ Explore project{" "}
+
+
+
+
+ );
+ })}
+
+
-
+
+
+
+
+ 02 Our standard
+
+
Software people trust at 3 AM.
+
+ We are an open-source engineering team from Poland. We build dependable tools for
+ server owners and developers who care about quality, clarity and control.
+
+
+ Meet the team
+
+
+
+ {principles.map((item) => {
+ const Icon = item.icon;
+ return (
+
+
+
+ {item.title}
+
+
{item.text}
+
+ );
+ })}
+
+
+
-
+
+
+ {[
+ ["10+", "open-source projects"],
+ ["100k+", "monthly downloads"],
+ ["24/7", "community-driven development"],
+ ].map(([value, label]) => (
+
+
+ {value}
+
+ {label}
+
+ ))}
+
+
-
-
+
+
+
+
+
+ eternalcode / main
+
+
+
+ $ ./gradlew build
+
+
Resolving production modules...
+
Running quality gates...
+
+
+ Build successful in 2.4s
+
+
+ $ deploy --with-confidence{" "}
+
+
+
+
+
+
+ 03 Developer experience
+
+
Complex under the hood. Calm on the surface.
+
+ Predictable configuration, useful defaults and documentation written for shipping—not
+ guessing.
+
+
+
+
+
-
-
-
}
- >
-
-
-
}
- >
-
-
-
}
- >
-
-
-
}
- >
-
-
-
}
- >
-
-
-
-
+
+
);
}
diff --git a/components/home/features/features.tsx b/components/home/features/features.tsx
index c1ae8b0c..fbf6decb 100644
--- a/components/home/features/features.tsx
+++ b/components/home/features/features.tsx
@@ -7,6 +7,7 @@ import JavaIcon from "@/components/icons/java";
import TabNew from "@/components/icons/tab-new";
import SectionTitle from "@/components/section-title";
import { Card } from "@/components/ui/card";
+import { CursorGlow } from "@/components/ui/cursor-glow";
import { MotionSection, SlideIn } from "@/components/ui/motion/motion-components";
import { slideUp } from "@/lib/animations/variants";
@@ -24,14 +25,14 @@ const features: Feature[] = [
title: "Java Ecosystem",
description:
"We harness the full power of the Java ecosystem to build scalable, high-performance server-side applications and plugins that you can rely on.",
- color: "from-orange-500/20 to-red-500/20",
+ color: "from-blue-500/20 to-indigo-500/20",
},
{
icon: ,
title: "Open Source",
description:
"We believe in the power of open source. Transparency, collaboration, and community are the core values that drive every line of code we write.",
- color: "from-green-500/20 to-emerald-500/20",
+ color: "from-sky-500/20 to-blue-500/20",
},
{
icon: ,
@@ -54,31 +55,33 @@ export default function Features() {
/>
-
- {features.map((feature) => (
-
-
-
+
+
+ {features.map((feature) => (
+
+
+
-
-
-
- {feature.title}
-
+
+ {feature.title}
+
-
- {feature.description}
-
-
-
- ))}
-
+
+ {feature.description}
+
+
+
+ ))}
+
+
);
diff --git a/components/notification-generator/tabs/sound/sound-table.tsx b/components/notification-generator/tabs/sound/sound-table.tsx
index c7a4c1a4..4ba49756 100644
--- a/components/notification-generator/tabs/sound/sound-table.tsx
+++ b/components/notification-generator/tabs/sound/sound-table.tsx
@@ -267,7 +267,7 @@ function SoundRow({
className={`border-gray-100 border-b hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800 ${
selectedSound?.id === sound.id ? "bg-blue-50 dark:bg-blue-900/20" : ""
} ${
- currentlyPlayingId === sound.id ? "bg-green-50 dark:bg-green-900/20" : ""
+ currentlyPlayingId === sound.id ? "bg-blue-50 dark:bg-blue-900/20" : ""
} cursor-pointer transition-colors duration-150`}
onClick={() => onSelectSound(sound.id)}
>
diff --git a/components/shared/cta-section.tsx b/components/shared/cta-section.tsx
index c75850ce..9a80a933 100644
--- a/components/shared/cta-section.tsx
+++ b/components/shared/cta-section.tsx
@@ -1,39 +1,56 @@
"use client";
import { Icon } from "@iconify/react";
-
-import { Button } from "@/components/ui/button";
-import { SlideIn } from "@/components/ui/motion/motion-components";
+import { ArrowUpRight, MessageCircle, Users } from "lucide-react";
+import Image from "next/image";
export default function Cta() {
return (
-
-
-
-
-
- Join our Community
-
-
- Connect with other developers, showcase your projects, and get help with EternalCode
- libraries. The best discussions happen on our Discord.
-
-
- }
- rel="noopener noreferrer"
- shine
- size="lg"
- target="_blank"
- variant="primary"
- >
- Join Discord
-
-
+
+
+
+
+ Build with us
+
+
+ Good software gets better in good company.
+
+
+ Ask questions, share feedback or just hang out with people who also spend too much time
+ thinking about Minecraft server plugins.
+
+
+
+ Developers & server owners
+
+
+ Help, feedback & releases
+
+
+
+
+
);
diff --git a/components/team/team-hero.tsx b/components/team/team-hero.tsx
index cccf57c0..d825af5c 100644
--- a/components/team/team-hero.tsx
+++ b/components/team/team-hero.tsx
@@ -4,22 +4,23 @@ import { MotionSection, SlideIn } from "@/components/ui/motion/motion-components
export default function TeamHero() {
return (
-
+
-
- The Architects of{" "}
-
- Innovation
-
+
+ People EternalCode team
+
+
+ Friends who happen to build{" "}
+ dependable software.
- Meet the developers, designers, and creators who build powerful open-source tools.
- Driven by curiosity and united by code.
+ No office, no matching hoodies, no quarterly targets. Just people who enjoy making
+ useful open-source things together.
diff --git a/components/team/team-member.tsx b/components/team/team-member.tsx
index ab9171e5..5742f8ad 100644
--- a/components/team/team-member.tsx
+++ b/components/team/team-member.tsx
@@ -19,9 +19,9 @@ export default function TeamMember({ member, index }: TeamMemberProps) {
return (
-
+
{/* Background Gradient */}
-
+
{/* Avatar */}
diff --git a/components/team/team.tsx b/components/team/team.tsx
index 731bc082..dd43de0e 100644
--- a/components/team/team.tsx
+++ b/components/team/team.tsx
@@ -1,4 +1,5 @@
import Image from "next/image";
+import { CursorGlow } from "@/components/ui/cursor-glow";
import { MotionSection, SlideIn } from "@/components/ui/motion/motion-components";
import { getTeamData } from "@/lib/team";
import TeamMember from "./team-member";
@@ -52,17 +53,19 @@ export default async function Team() {
))}
) : (
-
- {section.members.map((member, index) => (
-
-
-
- ))}
-
+
+
+ {section.members.map((member, index) => (
+
+
+
+ ))}
+
+
)}
))}
diff --git a/components/ui/alert-box.tsx b/components/ui/alert-box.tsx
index 3b6798dc..717551ee 100644
--- a/components/ui/alert-box.tsx
+++ b/components/ui/alert-box.tsx
@@ -23,11 +23,10 @@ const alertContainerVariants = cva(
type: {
info: "border border-blue-500/20 bg-blue-500/10 dark:border-blue-500/20 dark:bg-blue-500/10",
warning:
- "border border-yellow-500/20 bg-yellow-500/10 dark:border-yellow-500/20 dark:bg-yellow-500/10",
+ "border border-indigo-500/20 bg-indigo-500/10 dark:border-indigo-500/20 dark:bg-indigo-500/10",
danger: "border border-red-500/20 bg-red-500/10 dark:border-red-500/20 dark:bg-red-500/10",
- tip: "border border-green-500/20 bg-green-500/10 dark:border-green-500/20 dark:bg-green-500/10",
- success:
- "border border-emerald-500/20 bg-emerald-500/10 dark:border-emerald-500/20 dark:bg-emerald-500/10",
+ tip: "border border-blue-500/20 bg-blue-500/10 dark:border-blue-500/20 dark:bg-blue-500/10",
+ success: "border border-sky-500/20 bg-sky-500/10 dark:border-sky-500/20 dark:bg-sky-500/10",
note: "border border-purple-500/20 bg-purple-500/10 dark:border-purple-500/20 dark:bg-purple-500/10",
question:
"border border-indigo-500/20 bg-indigo-500/10 dark:border-indigo-500/20 dark:bg-indigo-500/10",
@@ -47,10 +46,10 @@ const alertIconVariants = cva("h-5 w-5", {
variants: {
type: {
info: "text-blue-500 dark:text-blue-400",
- warning: "text-yellow-500 dark:text-yellow-400",
+ warning: "text-indigo-500 dark:text-indigo-400",
danger: "text-red-500 dark:text-red-400",
- tip: "text-green-500 dark:text-green-400",
- success: "text-emerald-500 dark:text-emerald-400",
+ tip: "text-blue-500 dark:text-blue-400",
+ success: "text-sky-500 dark:text-sky-400",
note: "text-purple-500 dark:text-purple-400",
question: "text-indigo-500 dark:text-indigo-400",
important: "text-amber-500 dark:text-amber-400",
@@ -66,10 +65,10 @@ const alertTextVariants = cva("prose-sm wrap-break-word md:prose-base max-w-full
variants: {
type: {
info: "text-blue-800 dark:text-blue-200",
- warning: "text-yellow-800 dark:text-yellow-200",
+ warning: "text-indigo-800 dark:text-indigo-200",
danger: "text-red-800 dark:text-red-200",
- tip: "text-green-800 dark:text-green-200",
- success: "text-emerald-800 dark:text-emerald-200",
+ tip: "text-blue-800 dark:text-blue-200",
+ success: "text-sky-800 dark:text-sky-200",
note: "text-purple-800 dark:text-purple-200",
question: "text-indigo-800 dark:text-indigo-200",
important: "text-amber-800 dark:text-amber-200",
@@ -85,10 +84,10 @@ const alertTitleVariants = cva("mb-1 font-semibold text-sm md:text-base", {
variants: {
type: {
info: "text-blue-900 dark:text-blue-100",
- warning: "text-yellow-900 dark:text-yellow-100",
+ warning: "text-indigo-900 dark:text-indigo-100",
danger: "text-red-900 dark:text-red-100",
- tip: "text-green-900 dark:text-green-100",
- success: "text-emerald-900 dark:text-emerald-100",
+ tip: "text-blue-900 dark:text-blue-100",
+ success: "text-sky-900 dark:text-sky-100",
note: "text-purple-900 dark:text-purple-100",
question: "text-indigo-900 dark:text-indigo-100",
important: "text-amber-900 dark:text-amber-100",
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
index 1e266c40..c08381bb 100644
--- a/components/ui/button.tsx
+++ b/components/ui/button.tsx
@@ -9,29 +9,30 @@ import { interactionSpring } from "@/lib/animations/variants";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
- "group relative inline-flex cursor-pointer select-none items-center justify-center overflow-visible font-medium transition-colors focus:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ "group relative inline-flex cursor-pointer select-none items-center justify-center overflow-visible font-semibold transition-all focus:outline-hidden focus-visible:ring-2 focus-visible:ring-[var(--ec-accent)] focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
primary:
- "border border-transparent bg-blue-600 text-white shadow-sm hover:bg-blue-700 hover:shadow-md dark:bg-blue-500 dark:hover:bg-blue-600",
+ "border border-transparent bg-[var(--ec-accent)] text-[var(--ec-accent-ink)] shadow-sm hover:shadow-md hover:brightness-105",
secondary:
- "border border-transparent bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700",
+ "border border-[var(--ec-line)] bg-[var(--ec-soft)] text-[var(--ec-text)] hover:brightness-95 dark:hover:brightness-125",
outline:
- "border border-gray-200 bg-transparent hover:bg-gray-50 hover:text-gray-900 dark:border-gray-700 dark:hover:bg-gray-800 dark:hover:text-white",
- ghost: "border border-transparent bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800",
- link: "h-auto border-transparent bg-transparent p-0 text-blue-600 underline-offset-4 hover:underline dark:text-blue-400",
+ "border border-[var(--ec-line)] bg-transparent text-[var(--ec-muted)] hover:bg-[var(--ec-soft)] hover:text-[var(--ec-text)]",
+ ghost:
+ "border border-transparent bg-transparent text-[var(--ec-muted)] hover:bg-[var(--ec-soft)] hover:text-[var(--ec-text)]",
+ link: "h-auto border-transparent bg-transparent p-0 text-[var(--ec-accent)] underline-offset-4 hover:underline",
danger:
"border border-transparent bg-red-600 text-white shadow-sm hover:bg-red-700 hover:shadow-red-500/20 dark:bg-red-700 dark:hover:bg-red-800",
contrast:
- "border border-transparent bg-gray-900 text-white shadow-sm hover:bg-gray-800 hover:shadow-lg dark:bg-white dark:text-gray-900 dark:hover:bg-gray-100 dark:hover:shadow-blue-500/20",
+ "border border-transparent bg-[var(--ec-text)] text-[var(--ec-bg)] shadow-sm hover:bg-[var(--ec-accent)] hover:text-[var(--ec-accent-ink)] hover:shadow-lg",
},
size: {
xs: "h-7 rounded-md px-3 text-xs",
sm: "h-9 rounded-lg px-4 text-sm",
- md: "h-11 rounded-lg px-6 py-2 text-sm",
- lg: "h-12 rounded-xl px-8 py-3 text-base",
- xl: "h-14 rounded-xl px-10 py-4 text-lg",
+ md: "h-11 rounded-full px-6 py-2 text-sm",
+ lg: "h-12 rounded-full px-8 py-3 text-sm",
+ xl: "h-14 rounded-full px-10 py-4 text-base",
},
fullWidth: {
true: "w-full",
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
index 3acbd7bc..e2b2d9d1 100644
--- a/components/ui/card.tsx
+++ b/components/ui/card.tsx
@@ -8,9 +8,10 @@ const Card = ({
}: React.HTMLAttributes & { ref?: React.Ref }) => (
diff --git a/components/ui/copy-to-clipboard.tsx b/components/ui/copy-to-clipboard.tsx
index 2c870a53..79bfccf0 100644
--- a/components/ui/copy-to-clipboard.tsx
+++ b/components/ui/copy-to-clipboard.tsx
@@ -39,7 +39,7 @@ export function CopyToClipboard({
t.visible ? "translate-y-0 opacity-100" : "translate-y-3 opacity-0"
)}
>
-
+
@@ -74,7 +74,7 @@ export function CopyToClipboard({
{showIcon ? (
{copied ? (
-
+
) : (
)}
diff --git a/components/ui/cursor-glow.tsx b/components/ui/cursor-glow.tsx
new file mode 100644
index 00000000..6a5f3bab
--- /dev/null
+++ b/components/ui/cursor-glow.tsx
@@ -0,0 +1,77 @@
+"use client";
+
+import type { PointerEvent, ReactNode } from "react";
+import { useCallback, useRef } from "react";
+import { cn } from "@/lib/utils";
+
+interface CursorGlowProps {
+ children: ReactNode;
+ className?: string;
+}
+
+const GLOW_REACH = 180;
+
+export function CursorGlow({ children, className }: CursorGlowProps) {
+ const containerRef = useRef(null);
+ const frameRef = useRef(null);
+
+ const updateCards = useCallback((clientX: number, clientY: number, active: boolean) => {
+ const container = containerRef.current;
+ if (!container) {
+ return;
+ }
+
+ const cards = Array.from(container.querySelectorAll("[data-glow-card]"));
+
+ for (const card of cards) {
+ if (!active) {
+ card.style.setProperty("--card-glow-opacity", "0");
+ continue;
+ }
+
+ const bounds = card.getBoundingClientRect();
+ const distanceX = Math.max(bounds.left - clientX, 0, clientX - bounds.right);
+ const distanceY = Math.max(bounds.top - clientY, 0, clientY - bounds.bottom);
+ const distance = Math.hypot(distanceX, distanceY);
+ const opacity = Math.max(0, 1 - distance / GLOW_REACH);
+
+ card.style.setProperty("--card-glow-x", `${clientX - bounds.left}px`);
+ card.style.setProperty("--card-glow-y", `${clientY - bounds.top}px`);
+ card.style.setProperty("--card-glow-opacity", opacity.toFixed(3));
+ }
+ }, []);
+
+ const handlePointerMove = useCallback(
+ (event: PointerEvent) => {
+ if (event.pointerType === "touch") {
+ return;
+ }
+
+ if (frameRef.current !== null) {
+ cancelAnimationFrame(frameRef.current);
+ }
+
+ const { clientX, clientY } = event;
+ frameRef.current = requestAnimationFrame(() => updateCards(clientX, clientY, true));
+ },
+ [updateCards]
+ );
+
+ const clearGlow = useCallback(() => {
+ if (frameRef.current !== null) {
+ cancelAnimationFrame(frameRef.current);
+ }
+ updateCards(0, 0, false);
+ }, [updateCards]);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/components/ui/mdx/badge.tsx b/components/ui/mdx/badge.tsx
index a4b669b9..659bc92a 100644
--- a/components/ui/mdx/badge.tsx
+++ b/components/ui/mdx/badge.tsx
@@ -12,9 +12,9 @@ const badgeVariants = cva(
default:
"border-gray-200 bg-gray-100 text-gray-900 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100",
success:
- "border-green-200 bg-green-100 text-green-800 dark:border-green-800 dark:bg-green-900/30 dark:text-green-300",
+ "border-blue-200 bg-blue-100 text-blue-800 dark:border-blue-800 dark:bg-blue-900/30 dark:text-blue-300",
warning:
- "border-yellow-200 bg-yellow-100 text-yellow-800 dark:border-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300",
+ "border-indigo-200 bg-indigo-100 text-indigo-800 dark:border-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-300",
danger:
"border-red-200 bg-red-100 text-red-800 dark:border-red-800 dark:bg-red-900/30 dark:text-red-300",
info: "border-blue-200 bg-blue-100 text-blue-800 dark:border-blue-800 dark:bg-blue-900/30 dark:text-blue-300",
diff --git a/components/ui/mdx/before-after.tsx b/components/ui/mdx/before-after.tsx
index b3157e36..e173f93a 100644
--- a/components/ui/mdx/before-after.tsx
+++ b/components/ui/mdx/before-after.tsx
@@ -164,14 +164,14 @@ export function BeforeAfterItem({ type, title, label, children }: BeforeAfterIte
"flex items-center justify-between border-b px-4 py-3",
isBefore
? "border-red-200 bg-red-50 dark:border-red-900 dark:bg-red-900"
- : "border-green-200 bg-green-50 dark:border-green-900 dark:bg-green-900"
+ : "border-blue-200 bg-blue-50 dark:border-blue-900 dark:bg-blue-900"
)}
>
{isBefore ? (
) : (
-
+
)}
{title ?? (isBefore ? "Before" : "After")}
@@ -182,7 +182,7 @@ export function BeforeAfterItem({ type, title, label, children }: BeforeAfterIte
"rounded-full px-2 py-0.5 font-medium text-xs",
isBefore
? "bg-red-100 text-red-700 dark:bg-red-800 dark:text-red-100"
- : "bg-green-100 text-green-700 dark:bg-green-800 dark:text-green-100"
+ : "bg-blue-100 text-blue-700 dark:bg-blue-800 dark:text-blue-100"
)}
>
{label}
diff --git a/components/ui/mdx/code-block.tsx b/components/ui/mdx/code-block.tsx
index 8145f7a9..0cb8c6a7 100644
--- a/components/ui/mdx/code-block.tsx
+++ b/components/ui/mdx/code-block.tsx
@@ -44,7 +44,7 @@ export const CodeBlock = memo(({ children, language: _language, className }: Cod
text={textToCopy}
>
{({ copied }) =>
- copied ? :
+ copied ? :
}
diff --git a/components/ui/mdx/command.tsx b/components/ui/mdx/command.tsx
index b07cd57c..74d31e24 100644
--- a/components/ui/mdx/command.tsx
+++ b/components/ui/mdx/command.tsx
@@ -19,12 +19,12 @@ const variantStyles: Record = {
light: "bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-100",
},
gradle: {
- dark: "bg-gray-900 text-green-400 dark:bg-gray-900 dark:text-green-400",
- light: "bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-300",
+ dark: "bg-gray-900 text-blue-400 dark:bg-gray-900 dark:text-blue-400",
+ light: "bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300",
},
maven: {
- dark: "bg-gray-900 text-yellow-400 dark:bg-gray-900 dark:text-yellow-400",
- light: "bg-yellow-50 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-300",
+ dark: "bg-gray-900 text-indigo-400 dark:bg-gray-900 dark:text-indigo-400",
+ light: "bg-indigo-50 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300",
},
npm: {
dark: "bg-gray-900 text-red-400 dark:bg-gray-900 dark:text-red-400",
@@ -100,7 +100,7 @@ export function Command({
>
{({ copied }) =>
copied ? (
-
+
) : (
)
diff --git a/components/ui/mdx/heading.tsx b/components/ui/mdx/heading.tsx
index b9b10c15..ec83260c 100644
--- a/components/ui/mdx/heading.tsx
+++ b/components/ui/mdx/heading.tsx
@@ -39,7 +39,7 @@ export const Heading = ({ children, id, tag, className, ...props }: HeadingProps
{({ copied }) => (
{copied ? (
-
+
) : (
)}
diff --git a/public/community/wumpus.png b/public/community/wumpus.png
new file mode 100644
index 00000000..3dc30da0
Binary files /dev/null and b/public/community/wumpus.png differ