Pages Hard
E-Learning Platform — Meridian
Full e-learning platform UI with indigo/violet palette, Outfit font, Canvas skill tree with bezier node connections, animated course progress bars, scroll-linked stat counters, floating skill badges, and GSAP FAQ accordion.
Open in Lab
MCP
gsap scrolltrigger lenis accordion canvas-2d outfit
Targets: JS HTML
Code
/* ── Demo 52: E-Learning Platform — Meridian ── */
/* Design: Fresh indigo/violet on white — educational, approachable, modern */
/* Fonts: Outfit (geometric sans) + JetBrains Mono (code/data) */
:root {
--bg: #fafbff;
--white: #ffffff;
--surface: #f3f4ff;
--border: #e4e6f8;
--text: #111230;
--muted: #6b6e8a;
--indigo: #4f46e5;
--indigo-light: #818cf8;
--indigo-dim: rgba(79, 70, 229, 0.08);
--violet: #7c3aed;
--green: #059669;
--amber: #d97706;
--red: #dc2626;
}
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: "Outfit", -apple-system, sans-serif;
background: var(--bg);
color: var(--text);
-webkit-font-smoothing: antialiased;
line-height: 1.6;
overflow-x: hidden;
}
/* ── Section tag ── */
.section-tag {
display: inline-block;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--indigo);
margin-bottom: 0.75rem;
}
/* ── Buttons ── */
.btn-primary {
display: inline-block;
padding: 0.8rem 1.75rem;
background: var(--indigo);
color: white;
font-family: "Outfit", sans-serif;
font-size: 0.88rem;
font-weight: 600;
border-radius: 6px;
text-decoration: none;
transition: background 0.2s, transform 0.2s, box-shadow 0.2s;
box-shadow: 0 2px 12px rgba(79, 70, 229, 0.3);
}
.btn-primary:hover {
background: var(--violet);
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(79, 70, 229, 0.4);
}
.btn-primary--lg {
padding: 1rem 2.25rem;
font-size: 1rem;
}
.btn-text {
font-size: 0.88rem;
font-weight: 500;
color: var(--muted);
text-decoration: none;
transition: color 0.2s;
}
.btn-text:hover {
color: var(--text);
}
.btn-ghost {
font-size: 0.9rem;
font-weight: 500;
color: var(--indigo);
text-decoration: none;
transition: gap 0.2s;
}
.btn-ghost:hover {
text-decoration: underline;
}
.btn-outline-dark {
display: inline-block;
padding: 0.75rem 1.5rem;
border: 1px solid var(--border);
background: transparent;
color: var(--text);
font-size: 0.88rem;
font-weight: 500;
border-radius: 6px;
text-decoration: none;
transition: border-color 0.2s, background 0.2s;
}
.btn-outline-dark:hover {
border-color: var(--indigo);
background: var(--indigo-dim);
}
/* ── Nav ── */
.nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 3rem;
background: rgba(250, 251, 255, 0.9);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border);
}
.nav-logo {
display: flex;
align-items: center;
gap: 0.6rem;
text-decoration: none;
font-weight: 700;
font-size: 1.05rem;
color: var(--text);
}
.logo-mark {
width: 24px;
height: 24px;
background: var(--indigo);
border-radius: 6px;
position: relative;
}
.logo-mark::after {
content: "";
position: absolute;
top: 4px;
left: 4px;
width: 16px;
height: 16px;
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
}
.nav-links {
display: flex;
gap: 2rem;
list-style: none;
}
.nav-links a {
font-size: 0.88rem;
font-weight: 500;
color: var(--muted);
text-decoration: none;
transition: color 0.2s;
}
.nav-links a:hover {
color: var(--text);
}
.nav-ctas {
display: flex;
align-items: center;
gap: 1rem;
}
/* ── Hero ── */
.hero {
padding: 7rem 3rem 5rem;
background: var(--bg);
min-height: 100vh;
display: flex;
align-items: center;
}
.hero-inner {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
align-items: center;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
.hero-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.9rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 100px;
font-size: 0.78rem;
font-weight: 500;
color: var(--muted);
margin-bottom: 1.5rem;
}
.hb-dot {
width: 7px;
height: 7px;
background: var(--green);
border-radius: 50%;
animation: pulse-green 2s ease-in-out infinite;
}
@keyframes pulse-green {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.hero-h1 {
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 800;
line-height: 1.15;
letter-spacing: -0.03em;
margin-bottom: 1.25rem;
}
.h1-highlight {
background: linear-gradient(90deg, var(--indigo), var(--violet));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-p {
font-size: 1.05rem;
color: var(--muted);
line-height: 1.8;
margin-bottom: 2rem;
max-width: 460px;
}
.hero-actions {
display: flex;
gap: 1rem;
align-items: center;
margin-bottom: 3rem;
}
.hero-stats {
display: flex;
gap: 2rem;
align-items: center;
padding-top: 2rem;
border-top: 1px solid var(--border);
}
.hs-item {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.hs-num {
font-family: "JetBrains Mono", monospace;
font-size: 1.6rem;
font-weight: 700;
color: var(--text);
line-height: 1;
}
.hs-label {
font-size: 0.72rem;
color: var(--muted);
font-weight: 500;
}
.hs-div {
width: 1px;
height: 36px;
background: var(--border);
}
/* ── Progress card (hero visual) ── */
.hero-visual {
position: relative;
}
.progress-card {
background: var(--white);
border: 1px solid var(--border);
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 8px 40px rgba(79, 70, 229, 0.1);
position: relative;
z-index: 2;
}
.pc-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.pc-avatar {
width: 36px;
height: 36px;
border-radius: 8px;
background: linear-gradient(135deg, var(--indigo), var(--violet));
flex-shrink: 0;
}
.pc-meta {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.pc-meta strong {
font-size: 0.9rem;
font-weight: 700;
color: var(--text);
}
.pc-meta span {
font-size: 0.72rem;
color: var(--muted);
}
.pc-pct {
font-family: "JetBrains Mono", monospace;
font-size: 0.8rem;
font-weight: 700;
color: var(--indigo);
}
.pc-progress {
height: 6px;
background: var(--surface);
border-radius: 3px;
overflow: hidden;
margin-bottom: 1.25rem;
}
.pc-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, var(--indigo), var(--indigo-light));
border-radius: 3px;
transition: width 0.1s linear;
}
.pc-lessons {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.pc-lesson {
font-size: 0.82rem;
color: var(--muted);
padding: 0.5rem 0.75rem;
border-radius: 6px;
transition: background 0.2s;
}
.pc-lesson--done {
color: var(--green);
background: rgba(5, 150, 105, 0.06);
}
.pc-lesson--active {
color: var(--indigo);
background: var(--indigo-dim);
font-weight: 600;
}
/* Floating skill badges */
.skill-badge {
position: absolute;
padding: 0.4rem 0.9rem;
background: var(--white);
border: 1px solid var(--border);
border-radius: 100px;
font-size: 0.75rem;
font-weight: 600;
color: var(--text);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
white-space: nowrap;
}
.sb--1 {
top: -1.5rem;
right: 1.5rem;
}
.sb--2 {
bottom: 1.5rem;
right: -1.5rem;
}
.sb--3 {
bottom: -1.5rem;
left: 1rem;
}
.sb--4 {
top: 2rem;
left: -1.5rem;
}
/* ── Skills section ── */
.skills-section {
padding: 6rem 3rem;
background: var(--text);
color: white;
}
.skills-inner {
max-width: 1100px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
align-items: center;
}
.skills-inner .section-tag {
color: var(--indigo-light);
}
.skills-header h2 {
font-size: clamp(2rem, 4vw, 3rem);
font-weight: 800;
line-height: 1.2;
letter-spacing: -0.02em;
margin-bottom: 1rem;
}
.skills-header p {
font-size: 0.95rem;
color: rgba(255, 255, 255, 0.55);
line-height: 1.8;
}
.skill-tree {
position: relative;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(6, 56px);
gap: 0;
min-height: 400px;
}
#tree-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.st-node {
position: absolute;
width: 100px;
padding: 0.45rem 0.8rem;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
font-size: 0.72rem;
font-weight: 600;
color: rgba(255, 255, 255, 0.5);
background: rgba(255, 255, 255, 0.05);
text-align: center;
transform: translate(calc((var(--col) - 1) * 110px + 10px), calc((var(--row) - 1) * 60px));
transition: all 0.3s ease;
cursor: default;
}
.st-node--done {
border-color: var(--green);
color: var(--green);
background: rgba(5, 150, 105, 0.1);
}
.st-node--active {
border-color: var(--indigo-light);
color: white;
background: rgba(99, 102, 241, 0.2);
box-shadow: 0 0 16px rgba(99, 102, 241, 0.3);
}
.st-node--locked {
opacity: 0.3;
}
/* ── Courses ── */
.courses-section {
padding: 6rem 3rem;
max-width: 1200px;
margin: 0 auto;
}
.courses-header {
display: flex;
align-items: baseline;
justify-content: space-between;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 2.5rem;
}
.courses-header h2 {
font-size: clamp(1.8rem, 3vw, 2.5rem);
font-weight: 800;
letter-spacing: -0.02em;
}
.filter-tabs {
display: flex;
gap: 0.4rem;
}
.ft-btn {
padding: 0.4rem 1rem;
border: 1px solid var(--border);
background: transparent;
color: var(--muted);
font-family: "Outfit", sans-serif;
font-size: 0.82rem;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.ft-btn.active,
.ft-btn:hover {
background: var(--indigo);
color: white;
border-color: var(--indigo);
}
.courses-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.25rem;
}
.course-card {
background: var(--white);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
transition: transform 0.3s ease, box-shadow 0.3s ease;
cursor: pointer;
}
.course-card:hover {
transform: translateY(-4px);
box-shadow: 0 16px 48px rgba(79, 70, 229, 0.12);
}
.cc-art {
height: 140px;
}
.cc-art--1 {
background: linear-gradient(135deg, #4f46e5, #7c3aed);
}
.cc-art--2 {
background: linear-gradient(135deg, #0891b2, #0d9488);
}
.cc-art--3 {
background: linear-gradient(135deg, #d97706, #dc2626);
}
.cc-art--4 {
background: linear-gradient(135deg, #7c3aed, #db2777);
}
.cc-art--5 {
background: linear-gradient(135deg, #059669, #0891b2);
}
.cc-art--6 {
background: linear-gradient(135deg, #1d4ed8, #4f46e5);
}
.cc-body {
padding: 1.25rem;
}
.cc-tags {
display: flex;
gap: 0.4rem;
margin-bottom: 0.75rem;
}
.cc-tag {
font-size: 0.65rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 0.2rem 0.55rem;
border-radius: 4px;
background: var(--indigo-dim);
color: var(--indigo);
}
.cc-tag--dev {
background: rgba(8, 145, 178, 0.1);
color: #0891b2;
}
.cc-tag--data {
background: rgba(217, 119, 6, 0.1);
color: var(--amber);
}
.cc-level {
font-size: 0.65rem;
font-weight: 500;
color: var(--muted);
padding: 0.2rem 0.55rem;
border: 1px solid var(--border);
border-radius: 4px;
}
.cc-body h3 {
font-size: 1rem;
font-weight: 700;
margin-bottom: 0.5rem;
line-height: 1.3;
}
.cc-body p {
font-size: 0.82rem;
color: var(--muted);
line-height: 1.65;
margin-bottom: 1rem;
}
.cc-footer {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.cc-progress-bar {
height: 4px;
background: var(--surface);
border-radius: 2px;
overflow: hidden;
}
.cc-progress-fill {
height: 100%;
width: var(--w, 0%);
background: var(--indigo);
border-radius: 2px;
transform-origin: left;
transform: scaleX(0);
}
.cc-meta {
display: flex;
gap: 0.4rem;
font-size: 0.72rem;
color: var(--muted);
}
.cc-dot {
color: var(--border);
}
/* ── Pricing ── */
.pricing-section {
padding: 6rem 3rem;
background: var(--surface);
border-top: 1px solid var(--border);
}
.pricing-header {
text-align: center;
margin-bottom: 3rem;
}
.pricing-header h2 {
font-size: clamp(2rem, 4vw, 3rem);
font-weight: 800;
letter-spacing: -0.02em;
}
.pricing-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.25rem;
max-width: 900px;
margin: 0 auto;
}
.price-card {
background: var(--white);
border: 1px solid var(--border);
border-radius: 12px;
padding: 2rem;
position: relative;
}
.price-card--featured {
border-color: var(--indigo);
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
}
.prce-badge {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
padding: 0.25rem 0.75rem;
background: var(--indigo);
color: white;
font-size: 0.65rem;
font-weight: 700;
letter-spacing: 0.08em;
border-radius: 100px;
white-space: nowrap;
}
.prce-tier {
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 0.75rem;
}
.prce-price {
font-family: "JetBrains Mono", monospace;
font-size: 2.5rem;
font-weight: 700;
color: var(--text);
margin-bottom: 1.5rem;
line-height: 1;
}
.prce-price span {
font-size: 0.85rem;
color: var(--muted);
font-weight: 400;
}
.prce-features {
list-style: none;
display: flex;
flex-direction: column;
gap: 0.6rem;
margin-bottom: 1.5rem;
}
.prce-features li {
font-size: 0.85rem;
color: var(--text);
}
.prce-off {
color: var(--muted);
text-decoration: line-through;
}
/* ── FAQ accordion ── */
.faq-section {
padding: 6rem 3rem;
max-width: 700px;
margin: 0 auto;
}
.faq-section h2 {
font-size: clamp(2rem, 4vw, 3rem);
font-weight: 800;
letter-spacing: -0.02em;
margin-bottom: 2.5rem;
}
.accordion {
border-top: 1px solid var(--border);
}
.acc-item {
border-bottom: 1px solid var(--border);
}
.acc-trigger {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.25rem 0;
background: none;
border: none;
cursor: pointer;
font-family: "Outfit", sans-serif;
font-size: 0.95rem;
font-weight: 600;
color: var(--text);
text-align: left;
transition: color 0.2s;
}
.acc-trigger:hover {
color: var(--indigo);
}
.at-arrow {
font-size: 1.3rem;
font-weight: 300;
color: var(--muted);
flex-shrink: 0;
transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), color 0.2s;
}
.acc-trigger:hover .at-arrow {
color: var(--indigo);
}
.acc-body {
overflow: hidden;
height: 0;
}
.acc-inner {
padding: 0 0 1.25rem;
}
.acc-inner p {
font-size: 0.9rem;
color: var(--muted);
line-height: 1.8;
}
/* ── Footer ── */
.footer {
padding: 1.5rem 3rem;
background: var(--text);
display: flex;
align-items: center;
justify-content: space-between;
}
.footer-brand {
display: flex;
align-items: center;
gap: 0.5rem;
color: white;
font-weight: 700;
font-size: 0.9rem;
}
.footer-brand .logo-mark {
background: white;
}
.footer-brand .logo-mark::after {
background: rgba(17, 18, 48, 0.3);
}
.footer p {
font-size: 0.72rem;
color: rgba(255, 255, 255, 0.4);
}
/* ── Responsive ── */
@media (max-width: 1024px) {
.hero-inner {
grid-template-columns: 1fr;
}
.skills-inner {
grid-template-columns: 1fr;
}
.courses-grid {
grid-template-columns: 1fr 1fr;
}
.pricing-grid {
grid-template-columns: 1fr;
max-width: 400px;
}
}
@media (max-width: 768px) {
.nav {
padding: 1rem 1.5rem;
}
.nav-links {
display: none;
}
.hero {
padding: 6rem 1.5rem 4rem;
}
.skills-section,
.courses-section {
padding: 4rem 1.5rem;
}
.pricing-section,
.faq-section {
padding: 4rem 1.5rem;
}
.courses-grid {
grid-template-columns: 1fr;
}
.hero-visual {
display: none;
}
.courses-header {
flex-direction: column;
}
.footer {
flex-direction: column;
gap: 0.75rem;
text-align: center;
padding: 1.5rem;
}
}
/* ── Reduced motion ── */
html.reduced-motion .course-card:hover {
transform: none;
}
html.reduced-motion .hb-dot {
animation: none;
}
html.reduced-motion .btn-primary:hover {
transform: none;
}
html.reduced-motion * {
transition-duration: 0.01ms !important;
}if (!window.MotionPreference) {
const __mql = window.matchMedia("(prefers-reduced-motion: reduce)");
const __listeners = new Set();
const MotionPreference = {
prefersReducedMotion() {
return __mql.matches;
},
setOverride(value) {
const reduced = Boolean(value);
document.documentElement.classList.toggle("reduced-motion", reduced);
window.dispatchEvent(new CustomEvent("motion-preference", { detail: { reduced } }));
for (const listener of __listeners) {
try {
listener({ reduced, override: reduced, systemReduced: __mql.matches });
} catch {}
}
},
onChange(listener) {
__listeners.add(listener);
try {
listener({
reduced: __mql.matches,
override: null,
systemReduced: __mql.matches,
});
} catch {}
return () => __listeners.delete(listener);
},
getState() {
return { reduced: __mql.matches, override: null, systemReduced: __mql.matches };
},
};
window.MotionPreference = MotionPreference;
}
function prefersReducedMotion() {
return window.MotionPreference.prefersReducedMotion();
}
function initDemoShell() {
// No-op shim in imported standalone snippets.
}
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import Lenis from "lenis";
gsap.registerPlugin(ScrollTrigger);
initDemoShell({
title: "Meridian — E-Learning Platform",
category: "pages",
tech: ["gsap", "scroll-trigger", "lenis", "accordion", "canvas-2d", "outfit"],
});
const reduced = prefersReducedMotion();
if (reduced) document.documentElement.classList.add("reduced-motion");
// ── Lenis ─────────────────────────────────────────────────────────────────────
const lenis = new Lenis({ lerp: 0.1, smoothWheel: true });
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);
// ── Nav ───────────────────────────────────────────────────────────────────────
// Nav is always visible (solid), no scroll-triggered changes needed
// ── Hero entrance ─────────────────────────────────────────────────────────────
if (!reduced) {
gsap.set([".hero-badge", ".hero-h1", ".hero-p", ".hero-actions", ".hero-stats"], {
opacity: 0,
y: 20,
});
gsap.set(".progress-card", { opacity: 0, x: 30 });
gsap.set(".skill-badge", { opacity: 0, scale: 0.8 });
gsap
.timeline({ delay: 0.3, defaults: { ease: "expo.out" } })
.to(".hero-badge", { opacity: 1, y: 0, duration: 0.6 })
.to(".hero-h1", { opacity: 1, y: 0, duration: 0.9 }, "-=0.3")
.to(".hero-p", { opacity: 1, y: 0, duration: 0.7 }, "-=0.5")
.to(".hero-actions", { opacity: 1, y: 0, duration: 0.6 }, "-=0.4")
.to(".hero-stats", { opacity: 1, y: 0, duration: 0.6 }, "-=0.3")
.to(".progress-card", { opacity: 1, x: 0, duration: 0.9, ease: "back.out(1.5)" }, 0.5)
.to(
".skill-badge",
{ opacity: 1, scale: 1, duration: 0.5, stagger: 0.1, ease: "back.out(2)" },
0.8
);
}
// ── Hero stat counters ────────────────────────────────────────────────────────
document.querySelectorAll(".hs-num").forEach((el) => {
const target = parseFloat(el.dataset.target);
const suffix = el.dataset.suffix || "";
if (!reduced) {
ScrollTrigger.create({
trigger: ".hero-stats",
start: "top 85%",
end: "top 30%",
onUpdate: (self) => {
const val = Math.round(target * self.progress);
el.textContent = val.toLocaleString() + suffix;
},
});
} else {
el.textContent = target.toLocaleString() + suffix;
}
});
// ── Progress card animation ───────────────────────────────────────────────────
if (!reduced) {
// Animate progress bar to 37%
gsap.to("#pc-fill", {
width: "37%",
duration: 1.5,
ease: "expo.out",
delay: 1.2,
onUpdate: function () {
const w = parseFloat(this.targets()[0].style.width);
document.getElementById("pc-pct").textContent = Math.round(w) + "%";
},
});
// Floating skill badges gently float
document.querySelectorAll(".skill-badge").forEach((badge, i) => {
gsap.to(badge, {
y: -8 + (i % 2) * 16,
duration: 2 + i * 0.3,
repeat: -1,
yoyo: true,
ease: "sine.inOut",
delay: i * 0.5,
});
});
} else {
document.getElementById("pc-fill").style.width = "37%";
document.getElementById("pc-pct").textContent = "37%";
}
// ── Skill tree canvas ─────────────────────────────────────────────────────────
const treeCanvas = document.getElementById("tree-canvas");
if (treeCanvas) {
const tctx = treeCanvas.getContext("2d");
function drawTree() {
treeCanvas.width = treeCanvas.offsetWidth;
treeCanvas.height = treeCanvas.offsetHeight;
const COL_W = 110;
const ROW_H = 60;
const X_OFF = 60; // center of first col
// Connections: [fromCol, fromRow, toCol, toRow, done]
const connections = [
[2, 1, 1, 2, true],
[2, 1, 3, 2, true],
[1, 2, 2, 3, false],
[3, 2, 2, 3, false],
[2, 3, 1, 4, false],
[2, 3, 3, 4, false],
[1, 4, 2, 5, false],
[3, 4, 2, 5, false],
[2, 5, 1, 6, false],
[2, 5, 3, 6, false],
];
connections.forEach(([c1, r1, c2, r2, done]) => {
const x1 = (c1 - 1) * COL_W + X_OFF + 50;
const y1 = (r1 - 1) * ROW_H + 28;
const x2 = (c2 - 1) * COL_W + X_OFF + 50;
const y2 = (r2 - 1) * ROW_H + 28;
tctx.beginPath();
tctx.moveTo(x1, y1);
tctx.bezierCurveTo(x1, (y1 + y2) / 2, x2, (y1 + y2) / 2, x2, y2);
tctx.strokeStyle = done ? "rgba(5, 150, 105, 0.5)" : "rgba(255, 255, 255, 0.08)";
tctx.lineWidth = 1.5;
tctx.stroke();
});
}
drawTree();
window.addEventListener("resize", drawTree);
}
// ── Course progress bars animate on scroll ────────────────────────────────────
if (!reduced) {
document.querySelectorAll(".cc-progress-fill").forEach((fill, i) => {
gsap.to(fill, {
scaleX: 1,
duration: 1,
ease: "expo.out",
delay: i * 0.08,
scrollTrigger: {
trigger: ".courses-grid",
start: "top 75%",
toggleActions: "play none none reverse",
},
});
});
}
// ── Course cards reveal ───────────────────────────────────────────────────────
if (!reduced) {
document.querySelectorAll(".course-card").forEach((card, i) => {
gsap.set(card, { opacity: 0, y: 30 });
gsap.to(card, {
opacity: 1,
y: 0,
duration: 0.8,
ease: "expo.out",
delay: i * 0.08,
scrollTrigger: {
trigger: ".courses-grid",
start: "top 75%",
toggleActions: "play none none reverse",
},
});
});
}
// ── Filter tabs ───────────────────────────────────────────────────────────────
document.querySelectorAll(".ft-btn").forEach((btn) => {
btn.addEventListener("click", () => {
document.querySelectorAll(".ft-btn").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
// In a real implementation: filter .course-card by data-category
// Here we just toggle the active class for the demo
});
});
// ── Pricing cards reveal ──────────────────────────────────────────────────────
if (!reduced) {
gsap.set(".price-card", { opacity: 0, y: 25 });
gsap.to(".price-card", {
opacity: 1,
y: 0,
duration: 0.8,
stagger: 0.12,
ease: "expo.out",
scrollTrigger: {
trigger: ".pricing-grid",
start: "top 75%",
toggleActions: "play none none reverse",
},
});
}
// ── Skill tree nodes reveal ───────────────────────────────────────────────────
if (!reduced) {
gsap.set(".st-node", { opacity: 0, scale: 0.8 });
gsap.to(".st-node", {
opacity: 1,
scale: 1,
duration: 0.5,
stagger: 0.07,
ease: "back.out(1.5)",
scrollTrigger: {
trigger: ".skill-tree",
start: "top 70%",
toggleActions: "play none none reverse",
},
});
}
// ── FAQ Accordion ─────────────────────────────────────────────────────────────
const DURATION = reduced ? 0 : 0.4;
document.querySelectorAll("#faq-acc .acc-trigger").forEach((trigger) => {
const item = trigger.closest(".acc-item");
const body = item.querySelector(".acc-body");
const arrow = trigger.querySelector(".at-arrow");
gsap.set(body, { height: 0, overflow: "hidden" });
trigger.addEventListener("click", () => {
const isOpen = item.classList.contains("is-open");
// Close all
document.querySelectorAll("#faq-acc .acc-item").forEach((other) => {
if (other !== item && other.classList.contains("is-open")) {
other.classList.remove("is-open");
other.querySelector(".acc-trigger").setAttribute("aria-expanded", "false");
gsap.to(other.querySelector(".acc-body"), {
height: 0,
duration: DURATION,
ease: "expo.in",
overwrite: true,
});
gsap.to(other.querySelector(".at-arrow"), {
rotation: 0,
duration: DURATION * 0.7,
ease: "power2.in",
overwrite: true,
});
}
});
if (isOpen) {
item.classList.remove("is-open");
trigger.setAttribute("aria-expanded", "false");
gsap.to(body, { height: 0, duration: DURATION, ease: "expo.in", overwrite: true });
gsap.to(arrow, { rotation: 0, duration: DURATION * 0.7, ease: "power2.in", overwrite: true });
} else {
item.classList.add("is-open");
trigger.setAttribute("aria-expanded", "true");
gsap.to(body, { height: "auto", duration: DURATION, ease: "expo.out", overwrite: true });
gsap.to(arrow, { rotation: 45, duration: DURATION, ease: "back.out(2)", overwrite: true });
}
});
});
// ── Motion toggle ─────────────────────────────────────────────────────────────
window.addEventListener("motion-preference", (e) => {
gsap.globalTimeline.paused(e.detail.reduced);
});<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Meridian — Learn Without Limits</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<script type="importmap">{"imports":{"gsap":"https://esm.sh/gsap@3.13.0","gsap/ScrollTrigger":"https://esm.sh/gsap@3.13.0/ScrollTrigger","gsap/SplitText":"https://esm.sh/gsap@3.13.0/SplitText","gsap/Flip":"https://esm.sh/gsap@3.13.0/Flip","gsap/ScrambleTextPlugin":"https://esm.sh/gsap@3.13.0/ScrambleTextPlugin","gsap/TextPlugin":"https://esm.sh/gsap@3.13.0/TextPlugin","gsap/all":"https://esm.sh/gsap@3.13.0/all","gsap/":"https://esm.sh/gsap@3.13.0/","lenis":"https://esm.sh/lenis@1.1.13/dist/lenis.mjs","three":"https://esm.sh/three@0.171.0","three/addons/":"https://esm.sh/three@0.171.0/examples/jsm/"}}</script>
<style>html.lenis,
html.lenis body {
height: auto;
}
.lenis:not(.lenis-autoToggle).lenis-stopped {
overflow: clip;
}
.lenis [data-lenis-prevent],
.lenis [data-lenis-prevent-wheel],
.lenis [data-lenis-prevent-touch] {
overscroll-behavior: contain;
}
.lenis.lenis-smooth iframe {
pointer-events: none;
}
.lenis.lenis-autoToggle {
transition-property: overflow;
transition-duration: 1ms;
transition-behavior: allow-discrete;
}</style>
</head>
<body>
<!-- Nav -->
<nav class="nav" id="nav">
<a href="#" class="nav-logo">
<div class="logo-mark"></div>
<span>Meridian</span>
</a>
<ul class="nav-links">
<li><a href="#courses">Courses</a></li>
<li><a href="#paths">Learning Paths</a></li>
<li><a href="#skills">Skills</a></li>
<li><a href="#pricing">Pricing</a></li>
</ul>
<div class="nav-ctas">
<a href="#" class="btn-text">Log in</a>
<a href="#" class="btn-primary">Start Free</a>
</div>
</nav>
<!-- Hero -->
<section class="hero">
<div class="hero-inner">
<div class="hero-text">
<div class="hero-badge">
<span class="hb-dot"></span>
<span>2,400+ learners enrolled this week</span>
</div>
<h1 class="hero-h1">
Learn skills that<br>
<span class="h1-highlight">actually matter.</span>
</h1>
<p class="hero-p">Structured paths from beginner to job-ready. Expert instructors, real projects, and a community that keeps you accountable.</p>
<div class="hero-actions">
<a href="#courses" class="btn-primary btn-primary--lg">Browse Courses</a>
<a href="#" class="btn-ghost">See how it works →</a>
</div>
<div class="hero-stats">
<div class="hs-item">
<span class="hs-num" data-target="48000" data-suffix="">0</span>
<span class="hs-label">Students</span>
</div>
<div class="hs-div"></div>
<div class="hs-item">
<span class="hs-num" data-target="320" data-suffix="+">0</span>
<span class="hs-label">Courses</span>
</div>
<div class="hs-div"></div>
<div class="hs-item">
<span class="hs-num" data-target="94" data-suffix="%">0</span>
<span class="hs-label">Completion rate</span>
</div>
</div>
</div>
<div class="hero-visual">
<!-- Progress card -->
<div class="progress-card" id="progress-card">
<div class="pc-header">
<div class="pc-avatar"></div>
<div class="pc-meta">
<strong>Design Systems</strong>
<span>Week 3 of 8</span>
</div>
<span class="pc-pct" id="pc-pct">0%</span>
</div>
<div class="pc-progress">
<div class="pc-fill" id="pc-fill"></div>
</div>
<div class="pc-lessons">
<div class="pc-lesson pc-lesson--done">✓ Introduction to tokens</div>
<div class="pc-lesson pc-lesson--done">✓ Color scales</div>
<div class="pc-lesson pc-lesson--active">▶ Typography system</div>
<div class="pc-lesson">○ Spacing & grid</div>
<div class="pc-lesson">○ Component variants</div>
</div>
</div>
<!-- Floating skill badges -->
<div class="skill-badge sb--1">Figma</div>
<div class="skill-badge sb--2">Design Systems</div>
<div class="skill-badge sb--3">React</div>
<div class="skill-badge sb--4">CSS</div>
</div>
</div>
</section>
<!-- Skill tree -->
<section class="skills-section" id="skills">
<div class="skills-inner">
<div class="skills-header">
<span class="section-tag">Learning Path</span>
<h2>Your skill tree,<br>visualized.</h2>
<p>Every course unlocks the next. See exactly where you are and where you're headed.</p>
</div>
<div class="skill-tree" id="skill-tree">
<canvas id="tree-canvas" aria-hidden="true"></canvas>
<!-- Nodes -->
<div class="st-node st-node--done" style="--col:2; --row:1">HTML & CSS</div>
<div class="st-node st-node--done" style="--col:1; --row:2">Flexbox</div>
<div class="st-node st-node--done" style="--col:3; --row:2">Grid</div>
<div class="st-node st-node--active" style="--col:2; --row:3">JavaScript</div>
<div class="st-node" style="--col:1; --row:4">React</div>
<div class="st-node" style="--col:3; --row:4">TypeScript</div>
<div class="st-node" style="--col:2; --row:5">Full-Stack</div>
<div class="st-node st-node--locked" style="--col:1; --row:6">Next.js</div>
<div class="st-node st-node--locked" style="--col:3; --row:6">Node.js</div>
</div>
</div>
</section>
<!-- Courses grid -->
<section class="courses-section" id="courses">
<div class="courses-header">
<span class="section-tag">Featured Courses</span>
<h2>What do you want to learn?</h2>
<div class="filter-tabs">
<button class="ft-btn active" data-filter="all">All</button>
<button class="ft-btn" data-filter="design">Design</button>
<button class="ft-btn" data-filter="dev">Development</button>
<button class="ft-btn" data-filter="data">Data</button>
</div>
</div>
<div class="courses-grid">
<article class="course-card" data-category="design">
<div class="cc-art cc-art--1"></div>
<div class="cc-body">
<div class="cc-tags"><span class="cc-tag">Design</span><span class="cc-level">Beginner</span></div>
<h3>UI Fundamentals</h3>
<p>Learn the principles that make great interfaces — contrast, hierarchy, whitespace, and motion.</p>
<div class="cc-footer">
<div class="cc-progress-bar"><div class="cc-progress-fill" style="--w: 0%"></div></div>
<div class="cc-meta"><span>24 lessons</span><span class="cc-dot">·</span><span>4.8 ★</span></div>
</div>
</div>
</article>
<article class="course-card" data-category="dev">
<div class="cc-art cc-art--2"></div>
<div class="cc-body">
<div class="cc-tags"><span class="cc-tag cc-tag--dev">Dev</span><span class="cc-level">Intermediate</span></div>
<h3>React + TypeScript</h3>
<p>Build scalable applications with modern React patterns, TypeScript, and component architecture.</p>
<div class="cc-footer">
<div class="cc-progress-bar"><div class="cc-progress-fill" style="--w: 0%"></div></div>
<div class="cc-meta"><span>36 lessons</span><span class="cc-dot">·</span><span>4.9 ★</span></div>
</div>
</div>
</article>
<article class="course-card" data-category="data">
<div class="cc-art cc-art--3"></div>
<div class="cc-body">
<div class="cc-tags"><span class="cc-tag cc-tag--data">Data</span><span class="cc-level">Beginner</span></div>
<h3>Python for Data</h3>
<p>From pandas to machine learning fundamentals — a practical path through data science with Python.</p>
<div class="cc-footer">
<div class="cc-progress-bar"><div class="cc-progress-fill" style="--w: 0%"></div></div>
<div class="cc-meta"><span>42 lessons</span><span class="cc-dot">·</span><span>4.7 ★</span></div>
</div>
</div>
</article>
<article class="course-card" data-category="design">
<div class="cc-art cc-art--4"></div>
<div class="cc-body">
<div class="cc-tags"><span class="cc-tag">Design</span><span class="cc-level">Advanced</span></div>
<h3>Motion Design Systems</h3>
<p>GSAP, Framer Motion, and principled animation choreography for production interfaces.</p>
<div class="cc-footer">
<div class="cc-progress-bar"><div class="cc-progress-fill" style="--w: 0%"></div></div>
<div class="cc-meta"><span>28 lessons</span><span class="cc-dot">·</span><span>5.0 ★</span></div>
</div>
</div>
</article>
<article class="course-card" data-category="dev">
<div class="cc-art cc-art--5"></div>
<div class="cc-body">
<div class="cc-tags"><span class="cc-tag cc-tag--dev">Dev</span><span class="cc-level">Intermediate</span></div>
<h3>CSS Architecture</h3>
<p>Design tokens, cascade layers, logical properties, and building design systems with modern CSS.</p>
<div class="cc-footer">
<div class="cc-progress-bar"><div class="cc-progress-fill" style="--w: 0%"></div></div>
<div class="cc-meta"><span>18 lessons</span><span class="cc-dot">·</span><span>4.8 ★</span></div>
</div>
</div>
</article>
<article class="course-card" data-category="data">
<div class="cc-art cc-art--6"></div>
<div class="cc-body">
<div class="cc-tags"><span class="cc-tag cc-tag--data">Data</span><span class="cc-level">Advanced</span></div>
<h3>Data Visualization</h3>
<p>D3.js, Observable Plot, and Canvas 2D — telling stories with data that people actually understand.</p>
<div class="cc-footer">
<div class="cc-progress-bar"><div class="cc-progress-fill" style="--w: 0%"></div></div>
<div class="cc-meta"><span>31 lessons</span><span class="cc-dot">·</span><span>4.9 ★</span></div>
</div>
</div>
</article>
</div>
</section>
<!-- Pricing -->
<section class="pricing-section" id="pricing">
<div class="pricing-header">
<span class="section-tag">Pricing</span>
<h2>Simple, honest pricing.</h2>
</div>
<div class="pricing-grid">
<div class="price-card">
<div class="prce-tier">Free</div>
<div class="prce-price">$0<span>/mo</span></div>
<ul class="prce-features">
<li>✓ 5 courses</li>
<li>✓ Community access</li>
<li>✓ Basic projects</li>
<li class="prce-off">✗ Certificates</li>
<li class="prce-off">✗ Mentor sessions</li>
</ul>
<a href="#" class="btn-outline-dark">Get started free</a>
</div>
<div class="price-card price-card--featured">
<div class="prce-badge">Most Popular</div>
<div class="prce-tier">Pro</div>
<div class="prce-price">$29<span>/mo</span></div>
<ul class="prce-features">
<li>✓ All 320+ courses</li>
<li>✓ Learning paths</li>
<li>✓ Real projects + feedback</li>
<li>✓ Certificates</li>
<li>✓ 2 mentor sessions/mo</li>
</ul>
<a href="#" class="btn-primary">Start 7-day trial</a>
</div>
<div class="price-card">
<div class="prce-tier">Team</div>
<div class="prce-price">$19<span>/seat/mo</span></div>
<ul class="prce-features">
<li>✓ Everything in Pro</li>
<li>✓ Team analytics</li>
<li>✓ Custom paths</li>
<li>✓ Priority support</li>
<li>✓ Unlimited mentoring</li>
</ul>
<a href="#" class="btn-outline-dark">Contact sales</a>
</div>
</div>
</section>
<!-- FAQ accordion -->
<section class="faq-section">
<div class="faq-inner">
<span class="section-tag">FAQ</span>
<h2>Common questions</h2>
<div class="accordion" id="faq-acc">
<div class="acc-item">
<button class="acc-trigger" aria-expanded="false">
<span>Can I cancel anytime?</span>
<span class="at-arrow">+</span>
</button>
<div class="acc-body">
<div class="acc-inner"><p>Yes — no contracts, no lock-in. Cancel with a single click from your dashboard. You'll keep access until the end of your billing period.</p></div>
</div>
</div>
<div class="acc-item">
<button class="acc-trigger" aria-expanded="false">
<span>Do I get a certificate?</span>
<span class="at-arrow">+</span>
</button>
<div class="acc-body">
<div class="acc-inner"><p>Pro and Team members receive verified completion certificates for every course. They're shareable directly to LinkedIn and as credential URLs.</p></div>
</div>
</div>
<div class="acc-item">
<button class="acc-trigger" aria-expanded="false">
<span>How do learning paths work?</span>
<span class="at-arrow">+</span>
</button>
<div class="acc-body">
<div class="acc-inner"><p>A learning path is a curated sequence of courses that takes you from zero to a specific goal (e.g., "Frontend Developer" or "UX Researcher"). The skill tree unlocks as you progress through each course.</p></div>
</div>
</div>
<div class="acc-item">
<button class="acc-trigger" aria-expanded="false">
<span>What if a course isn't right for me?</span>
<span class="at-arrow">+</span>
</button>
<div class="acc-body">
<div class="acc-inner"><p>Every course has a 30-minute free preview. If you're a Pro member and still find a course isn't right, contact us within 7 days for a full refund of that course.</p></div>
</div>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="footer-brand">
<div class="logo-mark"></div>
<span>Meridian</span>
</div>
<p>© 2025 Meridian Learning Inc. · All rights reserved.</p>
</footer>
<script type="module" src="script.js"></script>
</body>
</html>E-Learning Platform — Meridian
Full e-learning platform UI with indigo/violet palette, Outfit font, Canvas skill tree with bezier node connections, animated course progress bars, scroll-linked stat counters, floating skill badges, and GSAP FAQ accordion.
Source
- Repository:
libs-genclaude - Original demo id:
52-elearning-platform
Notes
Full e-learning platform UI with indigo/violet palette, Outfit font, Canvas skill tree with bezier node connections, animated course progress bars, scroll-linked stat counters, floating skill badges, and GSAP FAQ accordion.