diff --git a/lessons/00-welcome.json b/lessons/00-welcome.json
index bcf8724..c56b700 100644
--- a/lessons/00-welcome.json
+++ b/lessons/00-welcome.json
@@ -5,6 +5,7 @@
"description": "Welcome to Code Crispies - your interactive web development learning platform",
"mode": "html",
"difficulty": "beginner",
+ "excludeFromProgress": true,
"lessons": [
{
"id": "get-started",
diff --git a/lessons/99-goodbye.json b/lessons/99-goodbye.json
index 7a34dea..5d82431 100644
--- a/lessons/99-goodbye.json
+++ b/lessons/99-goodbye.json
@@ -5,6 +5,7 @@
"description": "Congratulations on completing your learning journey!",
"mode": "html",
"difficulty": "beginner",
+ "excludeFromProgress": true,
"lessons": [
{
"id": "congratulations",
diff --git a/lessons/ar/00-welcome.json b/lessons/ar/00-welcome.json
index be26ed2..4a96ac4 100644
--- a/lessons/ar/00-welcome.json
+++ b/lessons/ar/00-welcome.json
@@ -5,6 +5,7 @@
"description": "مرحباً بك في Code Crispies - منصتك التفاعلية لتعلم تطوير الويب",
"mode": "html",
"difficulty": "beginner",
+ "excludeFromProgress": true,
"lessons": [
{
"id": "get-started",
diff --git a/lessons/de/00-welcome.json b/lessons/de/00-welcome.json
index 8c29486..ad3dc7a 100644
--- a/lessons/de/00-welcome.json
+++ b/lessons/de/00-welcome.json
@@ -5,6 +5,7 @@
"description": "Willkommen bei Code Crispies - deine interaktive Lernplattform für Webentwicklung",
"mode": "html",
"difficulty": "beginner",
+ "excludeFromProgress": true,
"lessons": [
{
"id": "get-started",
diff --git a/lessons/es/00-welcome.json b/lessons/es/00-welcome.json
index 8ff2960..2f55f3c 100644
--- a/lessons/es/00-welcome.json
+++ b/lessons/es/00-welcome.json
@@ -5,6 +5,7 @@
"description": "Bienvenido a Code Crispies - tu plataforma interactiva de aprendizaje de desarrollo web",
"mode": "html",
"difficulty": "beginner",
+ "excludeFromProgress": true,
"lessons": [
{
"id": "get-started",
diff --git a/lessons/pl/00-welcome.json b/lessons/pl/00-welcome.json
index 3b7fb8c..c23511c 100644
--- a/lessons/pl/00-welcome.json
+++ b/lessons/pl/00-welcome.json
@@ -5,6 +5,7 @@
"description": "Witaj w Code Crispies - twojej interaktywnej platformie do nauki tworzenia stron",
"mode": "html",
"difficulty": "beginner",
+ "excludeFromProgress": true,
"lessons": [
{
"id": "get-started",
diff --git a/lessons/uk/00-welcome.json b/lessons/uk/00-welcome.json
index 5e10fd7..9e2a463 100644
--- a/lessons/uk/00-welcome.json
+++ b/lessons/uk/00-welcome.json
@@ -5,6 +5,7 @@
"description": "Ласкаво просимо до Code Crispies - вашої інтерактивної платформи для вивчення веб-розробки",
"mode": "html",
"difficulty": "beginner",
+ "excludeFromProgress": true,
"lessons": [
{
"id": "get-started",
diff --git a/src/app.js b/src/app.js
index 68d1686..5e884e4 100644
--- a/src/app.js
+++ b/src/app.js
@@ -3,7 +3,8 @@ import { CodeEditor } from "./impl/CodeEditor.js";
import { renderLesson, renderModuleList, renderLevelIndicator, updateActiveLessonInSidebar } from "./helpers/renderer.js";
import { loadModules } from "./config/lessons.js";
import { initI18n, t, getLanguage, setLanguage, applyTranslations } from "./i18n.js";
-import { parseHash, updateHash, replaceHash, getShareableUrl } from "./helpers/router.js";
+import { parseHash, updateHash, replaceHash, getShareableUrl, RouteType, navigateTo } from "./helpers/router.js";
+import { sections, getSection, getModuleSection, getModulesBySection } from "./config/sections.js";
// Simplified state - LessonEngine now manages lesson state and progress
const state = {
@@ -22,6 +23,19 @@ const elements = {
logoLink: document.getElementById("logo-link"),
langSelect: document.getElementById("lang-select"),
helpBtn: document.getElementById("help-btn"),
+ mainNav: document.getElementById("main-nav"),
+
+ // Page containers
+ landingPage: document.getElementById("landing-page"),
+ sectionPage: document.getElementById("section-page"),
+ gameLayout: document.getElementById("main-content"),
+
+ // Section page elements
+ sectionTitle: document.getElementById("section-title"),
+ sectionDescription: document.getElementById("section-description"),
+ sectionProgressFill: document.getElementById("section-progress-fill"),
+ sectionProgressText: document.getElementById("section-progress-text"),
+ moduleGrid: document.getElementById("module-grid"),
// Left panel
instructionsSection: document.querySelector(".instructions"),
@@ -293,23 +307,8 @@ function initializeModules() {
// Use the new renderModuleList function with both callbacks
renderModuleList(elements.moduleList, modules, selectModule, selectLesson);
- // Check URL first for shareable links
- const urlState = parseHash();
-
- if (urlState) {
- // URL takes priority - navigate to specified lesson
- navigateToLesson(urlState.moduleId, urlState.lessonIndex, false);
- } else {
- // No URL - use saved progress (existing logic)
- const progressData = lessonEngine.loadUserProgress();
- const lastModuleId = progressData?.lastModuleId;
-
- if (lastModuleId && modules.find((m) => m.id === lastModuleId)) {
- selectModule(lastModuleId);
- } else if (modules.length > 0) {
- selectModule(modules[0].id);
- }
- }
+ // Handle route (home, section, or lesson)
+ handleRoute(false);
updateProgressDisplay();
clearLoadingTimeout();
@@ -325,6 +324,9 @@ function selectModule(moduleId) {
const success = lessonEngine.setModuleById(moduleId);
if (!success) return;
+ // Show lesson UI
+ showLessonUI();
+
// Update URL
const engineState = lessonEngine.getCurrentState();
updateHash(moduleId, engineState.lessonIndex);
@@ -355,6 +357,9 @@ function selectLesson(moduleId, lessonIndex) {
lessonEngine.setLessonByIndex(lessonIndex);
+ // Show lesson UI
+ showLessonUI();
+
// Update URL
updateHash(moduleId, lessonIndex);
@@ -814,7 +819,7 @@ async function copyShareUrl() {
}
}
-// ================= URL ROUTING =================
+// ================= URL ROUTING & PAGE SWITCHING =================
function initRouter() {
// Handle browser back/forward
@@ -822,13 +827,196 @@ function initRouter() {
}
function handlePopState() {
- const parsed = parseHash();
- if (parsed) {
- navigateToLesson(parsed.moduleId, parsed.lessonIndex, false);
+ handleRoute(false);
+}
+
+/**
+ * Main route handler - switches between page types
+ */
+function handleRoute(shouldUpdateUrl = true) {
+ const route = parseHash();
+
+ if (!route) {
+ // Invalid route - go to home
+ navigateTo("");
+ showLandingPage();
+ return;
}
+
+ switch (route.type) {
+ case RouteType.HOME:
+ showLandingPage();
+ break;
+ case RouteType.SECTION:
+ showSectionPage(route.sectionId);
+ break;
+ case RouteType.REFERENCE:
+ // Reference pages - TODO: implement later
+ showLandingPage();
+ break;
+ case RouteType.LESSON:
+ navigateToLesson(route.moduleId, route.lessonIndex, shouldUpdateUrl);
+ break;
+ default:
+ showLandingPage();
+ }
+
+ updateNavHighlight(route);
+}
+
+/**
+ * Hide all page containers
+ */
+function hideAllPages() {
+ elements.landingPage?.classList.add("hidden");
+ elements.sectionPage?.classList.add("hidden");
+ elements.gameLayout?.classList.add("hidden");
+}
+
+/**
+ * Show home landing page
+ */
+function showLandingPage() {
+ hideAllPages();
+ elements.landingPage?.classList.remove("hidden");
+
+ // Update section progress on landing page
+ updateLandingProgress();
+}
+
+/**
+ * Update progress indicators on landing page
+ */
+function updateLandingProgress() {
+ ["css", "html", "tailwind"].forEach((sectionId) => {
+ const progressEl = document.getElementById(`${sectionId}-progress`);
+ if (progressEl) {
+ const sectionModules = getModulesBySection(lessonEngine.modules, sectionId);
+ let completed = 0;
+ let total = 0;
+ sectionModules.forEach((m) => {
+ total += m.lessons.length;
+ const progress = lessonEngine.userProgress[m.id];
+ if (progress?.completed) {
+ completed += progress.completed.length;
+ }
+ });
+ if (total > 0) {
+ progressEl.textContent = `${completed}/${total} lessons`;
+ } else {
+ progressEl.textContent = "";
+ }
+ }
+ });
+}
+
+/**
+ * Show section landing page
+ */
+function showSectionPage(sectionId) {
+ hideAllPages();
+ elements.sectionPage?.classList.remove("hidden");
+
+ const section = getSection(sectionId);
+ if (!section) {
+ showLandingPage();
+ return;
+ }
+
+ // Update section header
+ if (elements.sectionTitle) elements.sectionTitle.textContent = section.title;
+ if (elements.sectionDescription) elements.sectionDescription.textContent = section.description;
+
+ // Get modules for this section
+ const sectionModules = getModulesBySection(lessonEngine.modules, sectionId);
+
+ // Calculate section progress
+ let completed = 0;
+ let total = 0;
+ sectionModules.forEach((m) => {
+ total += m.lessons.length;
+ const progress = lessonEngine.userProgress[m.id];
+ if (progress?.completed) {
+ completed += progress.completed.length;
+ }
+ });
+
+ // Update progress bar
+ const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
+ if (elements.sectionProgressFill) elements.sectionProgressFill.style.width = `${percent}%`;
+ if (elements.sectionProgressText) elements.sectionProgressText.textContent = `${completed} of ${total} lessons complete`;
+
+ // Render module cards
+ renderModuleGrid(sectionModules, sectionId);
+}
+
+/**
+ * Render module cards in section page
+ */
+function renderModuleGrid(modules, sectionId) {
+ if (!elements.moduleGrid) return;
+
+ elements.moduleGrid.innerHTML = "";
+
+ modules.forEach((module) => {
+ const card = document.createElement("a");
+ card.href = `#${module.id}/0`;
+ card.className = "module-card";
+
+ // Calculate module progress
+ const progress = lessonEngine.userProgress[module.id];
+ const completed = progress?.completed?.length || 0;
+ const total = module.lessons.length;
+
+ card.innerHTML = `
+
${module.title}
+
+ ${total} lessons
+ ${completed > 0 ? `${completed}/${total}` : ""}
+
+ `;
+
+ elements.moduleGrid.appendChild(card);
+ });
+}
+
+/**
+ * Show lesson UI (game layout)
+ */
+function showLessonUI() {
+ hideAllPages();
+ elements.gameLayout?.classList.remove("hidden");
+}
+
+/**
+ * Update nav link highlighting
+ */
+function updateNavHighlight(route) {
+ if (!elements.mainNav) return;
+
+ const navLinks = elements.mainNav.querySelectorAll(".nav-link");
+ navLinks.forEach((link) => {
+ link.classList.remove("active");
+
+ if (route?.type === RouteType.SECTION && link.dataset.section === route.sectionId) {
+ link.classList.add("active");
+ } else if (route?.type === RouteType.LESSON) {
+ // Highlight section based on module's inferred section
+ const module = lessonEngine.modules.find((m) => m.id === route.moduleId);
+ if (module) {
+ const moduleSection = getModuleSection(module);
+ if (link.dataset.section === moduleSection) {
+ link.classList.add("active");
+ }
+ }
+ }
+ });
}
function navigateToLesson(moduleId, lessonIndex, shouldUpdateUrl = true) {
+ // Show lesson UI
+ showLessonUI();
+
// Validate moduleId exists
const module = lessonEngine.modules.find((m) => m.id === moduleId);
if (!module) {
@@ -915,13 +1103,11 @@ function init() {
elements.closeSidebar.addEventListener("click", closeSidebar);
elements.sidebarBackdrop.addEventListener("click", closeSidebar);
- // Logo click - navigate to welcome
+ // Logo click - navigate to home landing
elements.logoLink.addEventListener("click", (e) => {
e.preventDefault();
- lessonEngine.setModuleById("welcome");
- updateHash("welcome", 0);
- loadCurrentLesson();
- updateModuleHighlight("welcome");
+ navigateTo("");
+ showLandingPage();
});
// Language select
diff --git a/src/config/sections.js b/src/config/sections.js
new file mode 100644
index 0000000..2082c6f
--- /dev/null
+++ b/src/config/sections.js
@@ -0,0 +1,75 @@
+/**
+ * Section definitions for Code Crispies
+ * Sections group related modules (CSS, HTML, Tailwind)
+ */
+
+export const sections = {
+ css: {
+ id: "css",
+ title: "CSS",
+ description: "Styling, layout, and animations",
+ color: "#264de4",
+ order: 1
+ },
+ html: {
+ id: "html",
+ title: "HTML",
+ description: "Semantic markup and native elements",
+ color: "#e34c26",
+ order: 2
+ },
+ tailwind: {
+ id: "tailwind",
+ title: "Tailwind CSS",
+ description: "Utility-first CSS framework",
+ color: "#06b6d4",
+ order: 3
+ }
+};
+
+/**
+ * Get section by ID
+ * @param {string} sectionId
+ * @returns {object|null}
+ */
+export function getSection(sectionId) {
+ return sections[sectionId] || null;
+}
+
+/**
+ * Get all sections sorted by order
+ * @returns {object[]}
+ */
+export function getSectionList() {
+ return Object.values(sections).sort((a, b) => a.order - b.order);
+}
+
+/**
+ * Infer section from module mode
+ * @param {object} module
+ * @returns {string}
+ */
+export function getModuleSection(module) {
+ // Explicit section takes precedence
+ if (module.section) return module.section;
+
+ // Infer from mode
+ const mode = module.mode || "css";
+ if (mode === "html") return "html";
+ if (mode === "tailwind") return "tailwind";
+ return "css";
+}
+
+/**
+ * Filter modules by section
+ * @param {object[]} modules
+ * @param {string} sectionId
+ * @returns {object[]}
+ */
+export function getModulesBySection(modules, sectionId) {
+ return modules.filter((m) => {
+ // Skip excluded modules (welcome, goodbye)
+ if (m.excludeFromProgress) return false;
+ return getModuleSection(m) === sectionId;
+ });
+}
diff --git a/src/helpers/router.js b/src/helpers/router.js
index 9806016..2dfae9e 100644
--- a/src/helpers/router.js
+++ b/src/helpers/router.js
@@ -1,26 +1,81 @@
/**
* URL Router for Code Crispies
- * Handles hash-based routing for shareable lesson links
- * Format: #module-id/lesson-index (e.g., #flexbox/2)
+ * Handles hash-based routing for pages, sections, and lessons
+ *
+ * Route formats:
+ * - # -> Home landing page
+ * - #css -> CSS section landing
+ * - #html -> HTML section landing
+ * - #tailwind -> Tailwind section landing
+ * - #reference/css -> CSS cheatsheet
+ * - #module/index -> Lesson (e.g., #flexbox/2)
*/
/**
- * Parse current URL hash into module and lesson info
- * @returns {{ moduleId: string, lessonIndex: number } | null}
+ * Route types
+ */
+export const RouteType = {
+ HOME: "home",
+ SECTION: "section",
+ REFERENCE: "reference",
+ LESSON: "lesson"
+};
+
+/**
+ * Valid section IDs
+ */
+const SECTIONS = ["css", "html", "tailwind"];
+
+/**
+ * Parse current URL hash into route info
+ * @returns {{ type: string, moduleId?: string, lessonIndex?: number, sectionId?: string, refId?: string } | null}
*/
export function parseHash() {
const hash = window.location.hash.slice(1); // Remove '#'
- if (!hash) return null;
+
+ // Empty hash = home
+ if (!hash) {
+ return { type: RouteType.HOME };
+ }
const parts = hash.split("/");
- if (parts.length !== 2) return null;
- const moduleId = parts[0];
- const lessonIndex = parseInt(parts[1], 10);
+ // Single segment routes
+ if (parts.length === 1) {
+ const segment = parts[0];
- if (!moduleId || isNaN(lessonIndex) || lessonIndex < 0) return null;
+ // Section landing pages
+ if (SECTIONS.includes(segment)) {
+ return { type: RouteType.SECTION, sectionId: segment };
+ }
- return { moduleId, lessonIndex };
+ // Reference index (no specific ref)
+ if (segment === "reference") {
+ return { type: RouteType.REFERENCE, refId: null };
+ }
+
+ // Single segment could be module with implicit lesson 0
+ return { type: RouteType.LESSON, moduleId: segment, lessonIndex: 0 };
+ }
+
+ // Two segment routes
+ if (parts.length === 2) {
+ // Reference subpages
+ if (parts[0] === "reference") {
+ return { type: RouteType.REFERENCE, refId: parts[1] };
+ }
+
+ // Lesson route (existing behavior)
+ const moduleId = parts[0];
+ const lessonIndex = parseInt(parts[1], 10);
+
+ if (moduleId && !isNaN(lessonIndex) && lessonIndex >= 0) {
+ return { type: RouteType.LESSON, moduleId, lessonIndex };
+ }
+ }
+
+ // Invalid route
+ return null;
}
/**
@@ -35,6 +90,17 @@ export function updateHash(moduleId, lessonIndex) {
}
}
+/**
+ * Update URL to a specific route
+ * @param {string} route - The route string (e.g., "css", "reference/flexbox", "")
+ */
+export function navigateTo(route) {
+ const newHash = route ? `#${route}` : "#";
+ if (window.location.hash !== newHash) {
+ history.pushState(null, "", newHash);
+ }
+}
+
/**
* Replace URL hash without history entry (for invalid URL fallbacks)
* @param {string} moduleId
@@ -45,6 +111,15 @@ export function replaceHash(moduleId, lessonIndex) {
history.replaceState(null, "", newHash);
}
+/**
+ * Replace URL to a specific route without history entry
+ * @param {string} route - The route string
+ */
+export function replaceTo(route) {
+ const newHash = route ? `#${route}` : "#";
+ history.replaceState(null, "", newHash);
+}
+
/**
* Build full shareable URL for current lesson
* @param {string} moduleId
@@ -55,3 +130,11 @@ export function getShareableUrl(moduleId, lessonIndex) {
const base = window.location.origin + window.location.pathname;
return `${base}#${moduleId}/${lessonIndex}`;
}
+
+/**
+ * Get valid section IDs
+ * @returns {string[]}
+ */
+export function getSectionIds() {
+ return [...SECTIONS];
+}
diff --git a/src/impl/LessonEngine.js b/src/impl/LessonEngine.js
index 9f90997..3c6c53f 100644
--- a/src/impl/LessonEngine.js
+++ b/src/impl/LessonEngine.js
@@ -463,9 +463,12 @@ export class LessonEngine {
let totalCompleted = 0;
this.modules.forEach((module) => {
+ // Skip modules excluded from progress (e.g., welcome, goodbye)
+ if (module.excludeFromProgress) return;
+
totalLessons += module.lessons.length;
const progress = this.userProgress[module.id];
- if (progress && progress.completed) {
+ if (progress?.completed) {
totalCompleted += progress.completed.length;
}
});
diff --git a/src/index.html b/src/index.html
index a5d91ea..1a2a7d5 100644
--- a/src/index.html
+++ b/src/index.html
@@ -19,6 +19,11 @@
+
@@ -30,6 +35,55 @@
+
+
+
+
+
+ Learn Web Development
Interactively
+ Master HTML, CSS, and Tailwind through hands-on exercises. Free and open source.
+
+
+
+
+
+
+
+
+
@@ -37,9 +91,19 @@
diff --git a/src/main.css b/src/main.css
index df7675a..c353f47 100644
--- a/src/main.css
+++ b/src/main.css
@@ -384,7 +384,9 @@ kbd {
cursor: pointer;
color: var(--light-text);
border-radius: var(--border-radius-sm);
- transition: color 0.2s, background 0.2s;
+ transition:
+ color 0.2s,
+ background 0.2s;
}
.share-btn:hover {
@@ -1481,6 +1483,223 @@ input:checked + .toggle-slider::before {
text-decoration: none;
}
+/* ================= MAIN NAV ================= */
+.main-nav {
+ display: none;
+ align-items: center;
+ gap: var(--spacing-xs);
+}
+
+.nav-link {
+ padding: 6px 12px;
+ border-radius: var(--border-radius-sm);
+ text-decoration: none;
+ font-size: 0.85rem;
+ font-weight: 500;
+ color: var(--light-text);
+ transition:
+ background 0.2s,
+ color 0.2s;
+}
+
+.nav-link:hover {
+ background: var(--primary-bg-light);
+ color: var(--text-color);
+}
+
+.nav-link.active {
+ background: var(--primary-bg-medium);
+ color: var(--primary-color);
+}
+
+@media (min-width: 769px) {
+ .main-nav {
+ display: flex;
+ }
+}
+
+/* ================= LANDING PAGE ================= */
+.landing-page {
+ flex: 1;
+ overflow-y: auto;
+ background: var(--bg-color);
+}
+
+.landing-content {
+ max-width: 900px;
+ margin: 0 auto;
+ padding: var(--spacing-lg);
+}
+
+.hero {
+ text-align: center;
+ padding: 3rem 1rem;
+}
+
+.hero-logo {
+ margin-bottom: 1rem;
+}
+
+.hero h1 {
+ font-size: 2.5rem;
+ font-weight: 800;
+ color: var(--text-color);
+ line-height: 1.2;
+ margin-bottom: 1rem;
+}
+
+.hero-highlight {
+ color: var(--primary-color);
+}
+
+.hero-subtitle {
+ font-size: 1.1rem;
+ color: var(--light-text);
+ max-width: 500px;
+ margin: 0 auto;
+}
+
+/* Section Cards */
+.section-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: var(--spacing-lg);
+ margin-top: 2rem;
+}
+
+.section-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: var(--spacing-lg);
+ background: var(--panel-bg);
+ border-radius: var(--border-radius-lg);
+ box-shadow: var(--shadow);
+ text-decoration: none;
+ color: var(--text-color);
+ transition:
+ transform 0.2s,
+ box-shadow 0.2s;
+}
+
+.section-card:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
+}
+
+.section-card-icon {
+ width: 60px;
+ height: 60px;
+ border-radius: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 800;
+ font-size: 1.2rem;
+ color: white;
+ margin-bottom: var(--spacing-md);
+}
+
+.section-card h2 {
+ font-size: 1.25rem;
+ margin-bottom: var(--spacing-xs);
+}
+
+.section-card p {
+ font-size: 0.9rem;
+ color: var(--light-text);
+ text-align: center;
+ margin-bottom: var(--spacing-sm);
+}
+
+.section-card-progress {
+ font-size: 0.8rem;
+ color: var(--primary-color);
+ font-weight: 500;
+}
+
+/* ================= SECTION PAGE ================= */
+.section-page {
+ flex: 1;
+ overflow-y: auto;
+ background: var(--bg-color);
+ padding: var(--spacing-lg);
+}
+
+.section-hero {
+ text-align: center;
+ padding: 2rem 1rem;
+ max-width: 600px;
+ margin: 0 auto;
+}
+
+.section-hero h1 {
+ font-size: 2rem;
+ color: var(--primary-dark);
+ margin-bottom: var(--spacing-xs);
+}
+
+.section-hero p {
+ color: var(--light-text);
+ margin-bottom: var(--spacing-md);
+}
+
+.section-progress-bar {
+ max-width: 400px;
+ margin: 0 auto;
+}
+
+.section-progress-bar .progress-bar {
+ height: 8px;
+ margin-bottom: var(--spacing-xs);
+}
+
+/* Module Grid */
+.module-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: var(--spacing-md);
+ max-width: 1000px;
+ margin: 0 auto;
+}
+
+.module-card {
+ display: block;
+ padding: var(--spacing-md);
+ background: var(--panel-bg);
+ border-radius: var(--border-radius-md);
+ box-shadow: var(--shadow);
+ text-decoration: none;
+ color: var(--text-color);
+ border-left: 4px solid var(--primary-color);
+ transition:
+ transform 0.2s,
+ box-shadow 0.2s;
+}
+
+.module-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
+}
+
+.module-card h3 {
+ font-size: 1rem;
+ color: var(--primary-dark);
+ margin-bottom: var(--spacing-xs);
+}
+
+.module-card-meta {
+ display: flex;
+ justify-content: space-between;
+ font-size: 0.8rem;
+ color: var(--light-text);
+}
+
+.module-card-progress {
+ color: var(--success-color);
+ font-weight: 500;
+}
+
/* ================= UTILITY ================= */
.hidden {
display: none !important;