diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0847a46
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+autopilot.db
+*.log
diff --git a/app.js b/app.js
deleted file mode 100644
index aa0d74d..0000000
--- a/app.js
+++ /dev/null
@@ -1,616 +0,0 @@
-/*
- autopilot — semester RPG planner
- all state lives in localStorage, no backend needed
-*/
-
-var store = {
- load: function() {
- var raw = localStorage.getItem('autopilot');
- if (!raw) return null;
- try { return JSON.parse(raw); } catch(e) { return null; }
- },
- save: function(data) {
- localStorage.setItem('autopilot', JSON.stringify(data));
- },
- nuke: function() {
- localStorage.removeItem('autopilot');
- }
-};
-
-// default state
-function freshState() {
- return {
- semStart: '',
- semEnd: '',
- energyType: 'morning',
- courses: [],
- hp: 100,
- xp: 0,
- checkins: [],
- completedQuests: [],
- achievements: {}
- };
-}
-
-var state = store.load() || freshState();
-
-// ============ SEMESTER WEEKS ============
-
-function getSemesterWeeks() {
- if (!state.semStart || !state.semEnd) return [];
- var start = new Date(state.semStart + 'T00:00:00');
- var end = new Date(state.semEnd + 'T00:00:00');
- var weeks = [];
- var cur = new Date(start);
- var weekNum = 1;
- while (cur <= end) {
- var weekEnd = new Date(cur);
- weekEnd.setDate(weekEnd.getDate() + 6);
- if (weekEnd > end) weekEnd = new Date(end);
- weeks.push({
- num: weekNum,
- start: new Date(cur),
- end: new Date(weekEnd)
- });
- cur.setDate(cur.getDate() + 7);
- weekNum++;
- }
- return weeks;
-}
-
-function getWeekLoad(week) {
- // count deadlines in this week and weight by difficulty + type
- var load = 0;
- state.courses.forEach(function(course) {
- course.deadlines.forEach(function(dl) {
- var d = new Date(dl.date + 'T00:00:00');
- if (d >= week.start && d <= week.end) {
- var mult = dl.type === 'exam' ? 3 : dl.type === 'project' ? 2 : 1;
- load += course.difficulty * mult;
- }
- });
- });
- return load;
-}
-
-function classifyWeek(load) {
- if (load >= 12) return 'boss';
- if (load >= 7) return 'intense';
- if (load >= 3) return 'moderate';
- return 'chill';
-}
-
-function isCurrentWeek(week) {
- var now = new Date();
- return now >= week.start && now <= week.end;
-}
-
-function isPast(week) {
- return new Date() > week.end;
-}
-
-function getWeekDeadlines(week) {
- var items = [];
- state.courses.forEach(function(course) {
- course.deadlines.forEach(function(dl) {
- var d = new Date(dl.date + 'T00:00:00');
- if (d >= week.start && d <= week.end) {
- items.push({ course: course.name, label: dl.label, date: dl.date, type: dl.type });
- }
- });
- });
- return items;
-}
-
-// ============ RENDERING ============
-
-function renderWeekGrid() {
- var grid = document.getElementById('weekGrid');
- grid.innerHTML = '';
- var weeks = getSemesterWeeks();
- var totalW = document.getElementById('totalWeeks');
- totalW.textContent = weeks.length || '?';
-
- if (weeks.length === 0) {
- grid.innerHTML = '
Set up your semester dates first ⚙
';
- return;
- }
-
- var foundCurrent = false;
- weeks.forEach(function(week) {
- var load = getWeekLoad(week);
- var cls = classifyWeek(load);
- var tile = document.createElement('div');
- tile.className = 'week-tile ' + cls;
-
- if (isPast(week)) tile.classList.add('past');
- if (isCurrentWeek(week)) {
- tile.classList.add('current');
- foundCurrent = true;
- document.getElementById('currentWeek').textContent = week.num;
- }
- if (cls === 'boss') tile.classList.add('boss-week');
-
- var deadlines = getWeekDeadlines(week);
- var tooltipText = 'Week ' + week.num;
- if (deadlines.length > 0) {
- tooltipText += ': ' + deadlines.map(function(d) { return d.course + ' - ' + d.label; }).join(', ');
- }
-
- tile.innerHTML =
- 'W' + week.num + '' +
- '' + cls + '' +
- '' + tooltipText + '
';
-
- grid.appendChild(tile);
- });
-
- if (!foundCurrent && weeks.length > 0) {
- document.getElementById('currentWeek').textContent = '—';
- }
-}
-
-function renderQuests() {
- var list = document.getElementById('questList');
- var noMsg = document.getElementById('noQuests');
- list.innerHTML = '';
-
- var today = new Date();
- today.setHours(0,0,0,0);
- var endOfWeek = new Date(today);
- endOfWeek.setDate(endOfWeek.getDate() + (7 - endOfWeek.getDay()));
-
- var quests = [];
-
- // gather deadlines coming up in the next 7 days
- state.courses.forEach(function(course) {
- course.deadlines.forEach(function(dl) {
- var d = new Date(dl.date + 'T00:00:00');
- if (d >= today && d <= endOfWeek) {
- quests.push({
- id: course.name + '|' + dl.date + '|' + dl.label,
- text: course.name + ': ' + dl.label,
- date: dl.date,
- type: dl.type
- });
- }
- });
- });
-
- // add daily wellness quests based on energy type
- var wellnessQuests = buildWellnessQuests();
- quests = quests.concat(wellnessQuests);
-
- if (quests.length === 0) {
- noMsg.style.display = 'block';
- return;
- }
- noMsg.style.display = 'none';
-
- // sort by date, wellness quests first for today
- quests.sort(function(a, b) {
- if (a.type === 'wellness' && b.type !== 'wellness') return -1;
- if (b.type === 'wellness' && a.type !== 'wellness') return 1;
- return a.date < b.date ? -1 : 1;
- });
-
- quests.forEach(function(q) {
- var li = document.createElement('li');
- var done = state.completedQuests.indexOf(q.id) !== -1;
- if (done) li.classList.add('done');
-
- var label = document.createElement('span');
- label.textContent = q.text;
-
- var btn = document.createElement('button');
- btn.className = 'quest-check';
- btn.textContent = done ? '✓' : '';
- btn.onclick = function() { toggleQuest(q.id); };
-
- li.appendChild(label);
- li.appendChild(btn);
- list.appendChild(li);
- });
-}
-
-function buildWellnessQuests() {
- var today = todayStr();
- var quests = [];
- var prefix = 'wellness|' + today + '|';
-
- // everyone gets these
- quests.push({ id: prefix + 'water', text: '💧 Drink water (8 glasses)', date: today, type: 'wellness' });
- quests.push({ id: prefix + 'meal', text: '🍽 Eat a real meal', date: today, type: 'wellness' });
-
- if (state.energyType === 'morning') {
- quests.push({ id: prefix + 'morning-study', text: '📖 Deep study block (morning)', date: today, type: 'wellness' });
- quests.push({ id: prefix + 'evening-wind', text: '🌙 Wind down by 10pm', date: today, type: 'wellness' });
- } else if (state.energyType === 'night') {
- quests.push({ id: prefix + 'night-study', text: '📖 Deep study block (evening)', date: today, type: 'wellness' });
- quests.push({ id: prefix + 'sleep-in', text: '😴 No alarm — sleep full cycle', date: today, type: 'wellness' });
- } else {
- quests.push({ id: prefix + 'afternoon-study', text: '📖 Deep study block (afternoon)', date: today, type: 'wellness' });
- quests.push({ id: prefix + 'break', text: '🚶 Take a 15min break outside', date: today, type: 'wellness' });
- }
-
- quests.push({ id: prefix + 'move', text: '🏃 Move your body (any exercise)', date: today, type: 'wellness' });
- return quests;
-}
-
-function renderBossFights() {
- var list = document.getElementById('bossList');
- var noMsg = document.getElementById('noBosses');
- list.innerHTML = '';
-
- var today = new Date();
- today.setHours(0,0,0,0);
- var bosses = [];
-
- state.courses.forEach(function(course) {
- course.deadlines.forEach(function(dl) {
- var d = new Date(dl.date + 'T00:00:00');
- if (d >= today && (dl.type === 'exam' || dl.type === 'project')) {
- bosses.push({ course: course.name, label: dl.label, date: dl.date, type: dl.type });
- }
- });
- });
-
- bosses.sort(function(a, b) { return a.date < b.date ? -1 : 1; });
-
- if (bosses.length === 0) {
- noMsg.style.display = 'block';
- return;
- }
- noMsg.style.display = 'none';
-
- bosses.slice(0, 5).forEach(function(b) {
- var li = document.createElement('li');
- var icon = b.type === 'exam' ? '☠' : '⚔';
- li.innerHTML =
- '' + icon + ' ' + b.course + ' — ' + b.label + '' +
- '' + formatDate(b.date) + '';
- list.appendChild(li);
- });
-}
-
-// ============ ACHIEVEMENTS ============
-
-var achievementDefs = [
- { id: 'first-checkin', icon: '🌅', name: 'First Dawn', desc: 'Complete your first check-in' },
- { id: 'week-streak-3', icon: '🔥', name: 'On Fire', desc: '3-day check-in streak' },
- { id: 'week-streak-7', icon: '⚡', name: 'Unstoppable', desc: '7-day check-in streak' },
- { id: '5-quests', icon: '⚔', name: 'Adventurer', desc: 'Complete 5 quests' },
- { id: '20-quests', icon: '🛡', name: 'Veteran', desc: 'Complete 20 quests' },
- { id: '50-quests', icon: '👑', name: 'Legend', desc: 'Complete 50 quests' },
- { id: 'hp-full-week', icon: '💚', name: 'Iron Will', desc: 'Keep HP above 80 for 7 days' },
- { id: 'early-bird', icon: '🐦', name: 'Early Bird', desc: 'Complete all quests before noon' }
-];
-
-function checkAchievements() {
- var completed = state.completedQuests.length;
- var checkins = state.checkins.length;
-
- if (checkins >= 1) unlock('first-checkin');
- if (getStreak() >= 3) unlock('week-streak-3');
- if (getStreak() >= 7) unlock('week-streak-7');
- if (completed >= 5) unlock('5-quests');
- if (completed >= 20) unlock('20-quests');
- if (completed >= 50) unlock('50-quests');
-}
-
-function unlock(id) {
- if (!state.achievements[id]) {
- state.achievements[id] = true;
- store.save(state);
- }
-}
-
-function getStreak() {
- if (state.checkins.length === 0) return 0;
- var sorted = state.checkins.map(function(c) { return c.date; }).sort().reverse();
- var streak = 1;
- for (var i = 1; i < sorted.length; i++) {
- var prev = new Date(sorted[i-1] + 'T00:00:00');
- var curr = new Date(sorted[i] + 'T00:00:00');
- var diff = (prev - curr) / (1000 * 60 * 60 * 24);
- if (diff === 1) streak++;
- else break;
- }
- return streak;
-}
-
-function renderAchievements() {
- var grid = document.getElementById('achievementGrid');
- grid.innerHTML = '';
-
- achievementDefs.forEach(function(ach) {
- var div = document.createElement('div');
- var unlocked = state.achievements[ach.id];
- div.className = 'achievement ' + (unlocked ? 'unlocked' : 'locked');
- div.innerHTML = '' + ach.icon + '' + ach.name;
- div.title = ach.desc;
- grid.appendChild(div);
- });
-}
-
-// ============ HP / XP LOGIC ============
-
-function recalcHP() {
- // hp is based on recent check-ins
- // no check-ins = hp slowly decays from 100
- var todayCheckin = getTodayCheckin();
- if (!todayCheckin) {
- // decay 2 per day without check-in, min 20
- var daysSinceCheckin = daysSinceLastCheckin();
- state.hp = Math.max(20, 100 - (daysSinceCheckin * 5));
- } else {
- // hp comes from sleep and stress scores
- var sleepBoost = (todayCheckin.sleep - 1) * 10; // 0 to 40
- var stressPenalty = (todayCheckin.stress - 1) * 8; // 0 to 32
- var exerciseBoost = todayCheckin.exercise ? 15 : 0;
- state.hp = Math.min(100, Math.max(10, 50 + sleepBoost - stressPenalty + exerciseBoost));
- }
- store.save(state);
- updateBars();
-}
-
-function addXP(amount) {
- state.xp += amount;
- store.save(state);
- updateBars();
-}
-
-function updateBars() {
- document.getElementById('hpFill').style.width = state.hp + '%';
- document.getElementById('hpText').textContent = Math.round(state.hp);
-
- // xp bar wraps every 100
- var xpPercent = (state.xp % 100);
- document.getElementById('xpFill').style.width = xpPercent + '%';
- document.getElementById('xpText').textContent = state.xp;
-}
-
-function getTodayCheckin() {
- var today = todayStr();
- for (var i = 0; i < state.checkins.length; i++) {
- if (state.checkins[i].date === today) return state.checkins[i];
- }
- return null;
-}
-
-function daysSinceLastCheckin() {
- if (state.checkins.length === 0) return 0;
- var sorted = state.checkins.map(function(c) { return c.date; }).sort().reverse();
- var last = new Date(sorted[0] + 'T00:00:00');
- var now = new Date();
- return Math.floor((now - last) / (1000 * 60 * 60 * 24));
-}
-
-// ============ QUEST COMPLETION ============
-
-function toggleQuest(id) {
- var idx = state.completedQuests.indexOf(id);
- if (idx === -1) {
- state.completedQuests.push(id);
- addXP(10);
- } else {
- state.completedQuests.splice(idx, 1);
- state.xp = Math.max(0, state.xp - 10);
- }
- store.save(state);
- checkAchievements();
- renderAll();
-}
-
-// ============ MODALS ============
-
-function openModal(id) { document.getElementById(id).classList.add('open'); }
-function closeModal(id) { document.getElementById(id).classList.remove('open'); }
-
-// settings
-document.getElementById('settingsBtn').onclick = function() {
- document.getElementById('semStart').value = state.semStart;
- document.getElementById('semEnd').value = state.semEnd;
- document.getElementById('energyType').value = state.energyType;
- renderCourseList();
- openModal('settingsModal');
-};
-
-document.getElementById('closeSettingsModal').onclick = function() { closeModal('settingsModal'); };
-
-document.getElementById('settingsForm').onsubmit = function(e) {
- e.preventDefault();
- state.semStart = document.getElementById('semStart').value;
- state.semEnd = document.getElementById('semEnd').value;
- state.energyType = document.getElementById('energyType').value;
- store.save(state);
- renderAll();
-};
-
-// check-in
-document.getElementById('checkInBtn').onclick = function() { openModal('checkinModal'); };
-document.getElementById('closeCheckinModal').onclick = function() { closeModal('checkinModal'); };
-
-document.getElementById('checkinForm').onsubmit = function(e) {
- e.preventDefault();
- var entry = {
- date: todayStr(),
- sleep: parseInt(document.getElementById('sleepScore').value),
- stress: parseInt(document.getElementById('stressScore').value),
- exercise: document.getElementById('exerciseCheck').value === 'yes'
- };
-
- // replace if already checked in today
- var existing = -1;
- for (var i = 0; i < state.checkins.length; i++) {
- if (state.checkins[i].date === entry.date) { existing = i; break; }
- }
- if (existing >= 0) {
- state.checkins[existing] = entry;
- } else {
- state.checkins.push(entry);
- }
-
- store.save(state);
- closeModal('checkinModal');
- checkAchievements();
- recalcHP();
- renderAll();
-};
-
-// courses
-document.getElementById('addCourseBtn').onclick = function() {
- closeModal('settingsModal');
- document.getElementById('courseForm').reset();
- document.getElementById('deadlineList').innerHTML = 'Deadlines
';
- document.getElementById('diffLabel').textContent = '3';
- openModal('courseModal');
-};
-document.getElementById('closeCourseModal').onclick = function() {
- closeModal('courseModal');
- openModal('settingsModal');
-};
-
-document.getElementById('addDeadlineBtn').onclick = function() {
- var row = document.createElement('div');
- row.className = 'deadline-row';
- row.innerHTML =
- '' +
- '' +
- '' +
- '';
- row.querySelector('.remove-dl').onclick = function() { row.remove(); };
- document.getElementById('deadlineList').appendChild(row);
-};
-
-document.getElementById('courseForm').onsubmit = function(e) {
- e.preventDefault();
- var name = document.getElementById('courseName').value.trim();
- var diff = parseInt(document.getElementById('courseDiff').value);
- var deadlines = [];
- var rows = document.getElementById('deadlineList').querySelectorAll('.deadline-row');
- rows.forEach(function(row) {
- var inputs = row.querySelectorAll('input');
- var sel = row.querySelector('select');
- if (inputs[0].value && inputs[1].value) {
- deadlines.push({
- label: inputs[0].value.trim(),
- date: inputs[1].value,
- type: sel.value
- });
- }
- });
-
- state.courses.push({ name: name, difficulty: diff, deadlines: deadlines });
- store.save(state);
- closeModal('courseModal');
- openModal('settingsModal');
- renderCourseList();
- renderAll();
-};
-
-// course difficulty slider label
-document.getElementById('courseDiff').oninput = function() {
- document.getElementById('diffLabel').textContent = this.value;
-};
-
-// check-in slider labels
-document.getElementById('sleepScore').oninput = function() {
- document.getElementById('sleepLabel').textContent = this.value;
-};
-document.getElementById('stressScore').oninput = function() {
- document.getElementById('stressLabel').textContent = this.value;
-};
-
-// reset
-document.getElementById('resetBtn').onclick = function() {
- if (confirm('Wipe all data and start fresh?')) {
- store.nuke();
- state = freshState();
- closeModal('settingsModal');
- renderAll();
- }
-};
-
-function renderCourseList() {
- var container = document.getElementById('courseListDisplay');
- container.innerHTML = '';
- state.courses.forEach(function(course, i) {
- var div = document.createElement('div');
- div.className = 'course-chip';
- div.innerHTML =
- '' + course.name + ' (diff: ' + course.difficulty + ', ' + course.deadlines.length + ' deadlines)' +
- '';
- div.querySelector('.remove-course').onclick = function() {
- state.courses.splice(i, 1);
- store.save(state);
- renderCourseList();
- renderAll();
- };
- container.appendChild(div);
- });
-}
-
-// close modals on backdrop click
-document.querySelectorAll('.modal-backdrop').forEach(function(backdrop) {
- backdrop.onclick = function(e) {
- if (e.target === backdrop) backdrop.classList.remove('open');
- };
-});
-
-// ============ HELPERS ============
-
-function todayStr() {
- var d = new Date();
- return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
-}
-
-function pad(n) { return n < 10 ? '0' + n : '' + n; }
-
-function formatDate(dateStr) {
- var parts = dateStr.split('-');
- var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
- return months[parseInt(parts[1]) - 1] + ' ' + parseInt(parts[2]);
-}
-
-// ============ CLEAN OLD COMPLETED QUESTS ============
-
-function cleanOldQuests() {
- // remove completed quest IDs older than 7 days so storage doesn't bloat
- var cutoff = new Date();
- cutoff.setDate(cutoff.getDate() - 7);
- var cutoffStr = cutoff.getFullYear() + '-' + pad(cutoff.getMonth() + 1) + '-' + pad(cutoff.getDate());
-
- state.completedQuests = state.completedQuests.filter(function(id) {
- var parts = id.split('|');
- if (parts.length >= 2) {
- return parts[1] >= cutoffStr;
- }
- return true;
- });
- store.save(state);
-}
-
-// ============ RENDER ALL ============
-
-function renderAll() {
- renderWeekGrid();
- renderQuests();
- renderBossFights();
- renderAchievements();
- recalcHP();
- updateBars();
-}
-
-// boot
-cleanOldQuests();
-renderAll();
-
-// first time? pop settings
-if (!state.semStart) {
- setTimeout(function() { openModal('settingsModal'); }, 400);
-}
diff --git a/db.js b/db.js
new file mode 100644
index 0000000..7b502fe
--- /dev/null
+++ b/db.js
@@ -0,0 +1,94 @@
+var initSqlJs = require('sql.js');
+var fs = require('fs');
+var path = require('path');
+
+var DB_PATH = path.join(__dirname, 'autopilot.db');
+var db = null;
+
+async function getDb() {
+ if (db) return db;
+
+ var SQL = await initSqlJs();
+
+ // load existing db file if it exists
+ if (fs.existsSync(DB_PATH)) {
+ var buffer = fs.readFileSync(DB_PATH);
+ db = new SQL.Database(buffer);
+ } else {
+ db = new SQL.Database();
+ }
+
+ // create tables
+ db.run(`
+ CREATE TABLE IF NOT EXISTS users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ email TEXT UNIQUE NOT NULL,
+ username TEXT UNIQUE NOT NULL,
+ password TEXT NOT NULL,
+ sem_start TEXT DEFAULT '',
+ sem_end TEXT DEFAULT '',
+ energy_type TEXT DEFAULT 'morning',
+ hp INTEGER DEFAULT 100,
+ xp INTEGER DEFAULT 0
+ )
+ `);
+
+ db.run(`
+ CREATE TABLE IF NOT EXISTS courses (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ name TEXT NOT NULL,
+ difficulty INTEGER DEFAULT 3
+ )
+ `);
+
+ db.run(`
+ CREATE TABLE IF NOT EXISTS deadlines (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ course_id INTEGER NOT NULL,
+ label TEXT NOT NULL,
+ date TEXT NOT NULL,
+ type TEXT DEFAULT 'assignment'
+ )
+ `);
+
+ db.run(`
+ CREATE TABLE IF NOT EXISTS checkins (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ date TEXT NOT NULL,
+ sleep INTEGER DEFAULT 3,
+ stress INTEGER DEFAULT 3,
+ exercise INTEGER DEFAULT 0
+ )
+ `);
+
+ db.run(`
+ CREATE TABLE IF NOT EXISTS completed_quests (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ quest_id TEXT NOT NULL
+ )
+ `);
+
+ db.run(`
+ CREATE TABLE IF NOT EXISTS achievements (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ achievement_id TEXT NOT NULL,
+ UNIQUE(user_id, achievement_id)
+ )
+ `);
+
+ persist();
+ return db;
+}
+
+function persist() {
+ if (!db) return;
+ var data = db.export();
+ var buffer = Buffer.from(data);
+ fs.writeFileSync(DB_PATH, buffer);
+}
+
+module.exports = { getDb, persist };
diff --git a/index.html b/index.html
deleted file mode 100644
index 8d3ce9f..0000000
--- a/index.html
+++ /dev/null
@@ -1,151 +0,0 @@
-
-
-
-
-
- Autopilot — Your Semester RPG
-
-
-
-
-
-
-
-
-
-
- Semester Map
-
-
- Chill
- Moderate
- Intense
- Boss Fight
-
-
-
-
-
-
-
Today's Quests
-
-
No quests yet — add courses to begin your adventure.
-
-
-
-
Upcoming Boss Fights
-
-
No boss fights on the horizon.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Semester Setup
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..a844f6d
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,915 @@
+{
+ "name": "autopilot",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "autopilot",
+ "version": "1.0.0",
+ "dependencies": {
+ "bcryptjs": "^2.4.3",
+ "cookie-parser": "^1.4.6",
+ "express": "^4.18.2",
+ "express-session": "^1.17.3",
+ "sql.js": "^1.10.3"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/bcryptjs": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
+ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
+ "license": "MIT"
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.4",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.14.0",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-parser": {
+ "version": "1.4.7",
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
+ "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "0.7.2",
+ "cookie-signature": "1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.3",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.14.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express-session": {
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz",
+ "integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "~0.7.2",
+ "cookie-signature": "~1.0.7",
+ "debug": "~2.6.9",
+ "depd": "~2.0.0",
+ "on-headers": "~1.1.0",
+ "parseurl": "~1.3.3",
+ "safe-buffer": "~5.2.1",
+ "uid-safe": "~2.1.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express-session/node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/random-bytes": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
+ "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/sql.js": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz",
+ "integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==",
+ "license": "MIT"
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/uid-safe": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+ "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
+ "license": "MIT",
+ "dependencies": {
+ "random-bytes": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..fd2ef5f
--- /dev/null
+++ b/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "autopilot",
+ "version": "1.0.0",
+ "description": "gamified semester planner",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js",
+ "dev": "node server.js"
+ },
+ "dependencies": {
+ "express": "^4.18.2",
+ "sql.js": "^1.10.3",
+ "bcryptjs": "^2.4.3",
+ "express-session": "^1.17.3",
+ "cookie-parser": "^1.4.6"
+ }
+}
diff --git a/public/css/auth.css b/public/css/auth.css
new file mode 100644
index 0000000..3a6eed6
--- /dev/null
+++ b/public/css/auth.css
@@ -0,0 +1,137 @@
+/* auth pages - login & signup */
+
+.auth-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: calc(100vh - 56px);
+ padding: 32px;
+}
+
+.auth-box {
+ position: relative;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ padding: 40px 36px;
+ width: 100%;
+ max-width: 440px;
+}
+
+.auth-title {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 14px;
+ color: var(--white);
+ text-align: center;
+ letter-spacing: 3px;
+ margin-bottom: 8px;
+}
+
+.auth-subtitle {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 7px;
+ color: var(--text-dim);
+ text-align: center;
+ letter-spacing: 2px;
+ margin-bottom: 32px;
+}
+
+.field {
+ margin-bottom: 20px;
+}
+
+.field label {
+ display: block;
+ font-family: 'Press Start 2P', monospace;
+ font-size: 8px;
+ color: var(--white);
+ letter-spacing: 1px;
+ margin-bottom: 8px;
+}
+
+.field input {
+ width: 100%;
+ padding: 12px 14px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ color: var(--text);
+ font-family: 'Space Mono', monospace;
+ font-size: 13px;
+ outline: none;
+ transition: border-color 0.2s;
+}
+
+.field input:focus {
+ border-color: var(--accent);
+}
+
+.field input::placeholder {
+ color: var(--text-dim);
+ opacity: 0.5;
+}
+
+.password-wrap {
+ position: relative;
+}
+
+.password-wrap input {
+ padding-right: 44px;
+}
+
+.toggle-pw {
+ position: absolute;
+ right: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ background: none;
+ border: none;
+ color: var(--text-dim);
+ font-size: 16px;
+ cursor: pointer;
+}
+
+.toggle-pw:hover { color: var(--white); }
+
+.error-msg {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 7px;
+ color: var(--red);
+ text-align: center;
+ min-height: 14px;
+ margin-bottom: 8px;
+ letter-spacing: 1px;
+}
+
+.auth-submit {
+ width: 100%;
+ padding: 14px;
+ background: var(--panel-lighter);
+ border: 1px solid var(--border);
+ color: var(--white);
+ font-family: 'Press Start 2P', monospace;
+ font-size: 9px;
+ letter-spacing: 2px;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.auth-submit:hover {
+ background: var(--accent);
+ border-color: var(--accent);
+}
+
+.auth-footer {
+ text-align: center;
+ margin-top: 24px;
+}
+
+.auth-footer a {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 7px;
+ color: var(--accent);
+ letter-spacing: 1px;
+ transition: color 0.2s;
+}
+
+.auth-footer a:hover {
+ color: var(--white);
+}
diff --git a/public/css/dashboard.css b/public/css/dashboard.css
new file mode 100644
index 0000000..69d8a77
--- /dev/null
+++ b/public/css/dashboard.css
@@ -0,0 +1,793 @@
+/* dashboard styles */
+
+/* hud bar tweaks */
+.hud-bar {
+ gap: 16px;
+}
+
+.week-badge {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 8px;
+ color: var(--accent);
+ background: var(--bg);
+ padding: 4px 10px;
+ border: 1px solid var(--border);
+ letter-spacing: 1px;
+}
+
+.hud-stats {
+ display: flex;
+ gap: 20px;
+ flex: 1;
+ max-width: 420px;
+ margin: 0 16px;
+}
+
+.stat-bar {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.stat-label {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 7px;
+ color: var(--text-dim);
+ width: 20px;
+}
+
+.bar-track {
+ flex: 1;
+ height: 14px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ overflow: hidden;
+}
+
+.bar-track.wide {
+ height: 18px;
+}
+
+.bar-fill {
+ height: 100%;
+ transition: width 0.5s ease;
+}
+
+.hp-fill {
+ background: linear-gradient(90deg, var(--red), #f87171);
+ box-shadow: 0 0 6px var(--red-dim);
+}
+
+.xp-fill {
+ background: linear-gradient(90deg, var(--green), #4ade80);
+ box-shadow: 0 0 6px var(--green-dim);
+}
+
+.stress-fill {
+ background: linear-gradient(90deg, var(--orange), var(--red));
+ box-shadow: 0 0 6px var(--red-dim);
+}
+
+.stat-val {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 7px;
+ color: var(--text-dim);
+ width: 32px;
+ text-align: right;
+}
+
+/* ==== TAB BAR ==== */
+
+.tab-bar {
+ display: flex;
+ border-bottom: 1px solid var(--border);
+ background: var(--panel);
+ padding: 0 32px;
+ gap: 0;
+}
+
+.tab {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 8px;
+ padding: 14px 20px;
+ background: none;
+ border: none;
+ border-bottom: 2px solid transparent;
+ color: var(--text-dim);
+ cursor: pointer;
+ letter-spacing: 1px;
+ transition: all 0.2s;
+}
+
+.tab:hover {
+ color: var(--white);
+}
+
+.tab.active {
+ color: var(--white);
+ border-bottom-color: var(--accent);
+}
+
+/* ==== MAIN AREA ==== */
+
+.dash-main {
+ max-width: 1000px;
+ margin: 0 auto;
+ padding: 32px;
+}
+
+.tab-panel {
+ display: none;
+}
+
+.tab-panel.active {
+ display: block;
+}
+
+.panel-header {
+ margin-bottom: 32px;
+}
+
+.panel-header h2 {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 12px;
+ color: var(--white);
+ letter-spacing: 3px;
+ margin-bottom: 8px;
+}
+
+.panel-sub {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 7px;
+ color: var(--text-dim);
+ letter-spacing: 2px;
+}
+
+.empty-state {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 8px;
+ color: var(--text-dim);
+ text-align: center;
+ padding: 32px;
+ letter-spacing: 1px;
+}
+
+/* ==== WEEK GRID ==== */
+
+.week-grid {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: 6px;
+ margin-bottom: 20px;
+}
+
+.week-tile {
+ aspect-ratio: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid var(--border);
+ cursor: pointer;
+ transition: all 0.2s;
+ position: relative;
+ padding: 4px;
+}
+
+.week-tile:hover {
+ transform: scale(1.06);
+ border-color: var(--white);
+ z-index: 2;
+}
+
+.week-tile .w-num {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 9px;
+ opacity: 0.9;
+}
+
+.week-tile .w-label {
+ font-family: 'Space Mono', monospace;
+ font-size: 8px;
+ margin-top: 2px;
+ opacity: 0.5;
+ text-transform: uppercase;
+}
+
+.week-tile.chill {
+ background: rgba(34, 197, 94, 0.08);
+ color: var(--green);
+ border-color: rgba(34, 197, 94, 0.2);
+}
+
+.week-tile.moderate {
+ background: rgba(245, 158, 11, 0.08);
+ color: var(--gold);
+ border-color: rgba(245, 158, 11, 0.2);
+}
+
+.week-tile.intense {
+ background: rgba(249, 115, 22, 0.1);
+ color: var(--orange);
+ border-color: rgba(249, 115, 22, 0.2);
+}
+
+.week-tile.boss {
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--red);
+ border-color: rgba(239, 68, 68, 0.25);
+}
+
+.week-tile.past {
+ opacity: 0.35;
+}
+
+.week-tile.current {
+ border-color: var(--accent);
+ box-shadow: 0 0 10px rgba(74, 124, 255, 0.2);
+ animation: weekPulse 2.5s ease-in-out infinite;
+}
+
+@keyframes weekPulse {
+ 0%, 100% { box-shadow: 0 0 10px rgba(74, 124, 255, 0.2); }
+ 50% { box-shadow: 0 0 18px rgba(74, 124, 255, 0.35); }
+}
+
+.boss-icon {
+ position: absolute;
+ top: 2px;
+ right: 3px;
+ font-size: 10px;
+}
+
+/* week tooltip */
+.week-tip {
+ display: none;
+ position: absolute;
+ bottom: calc(100% + 8px);
+ left: 50%;
+ transform: translateX(-50%);
+ background: var(--bg);
+ border: 1px solid var(--accent);
+ padding: 8px 12px;
+ font-size: 10px;
+ white-space: nowrap;
+ z-index: 20;
+ pointer-events: none;
+ font-family: 'Space Mono', monospace;
+}
+
+.week-tile:hover .week-tip {
+ display: block;
+}
+
+/* legend */
+.map-legend {
+ display: flex;
+ gap: 20px;
+ justify-content: center;
+ margin-bottom: 32px;
+}
+
+.legend-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-family: 'Press Start 2P', monospace;
+ font-size: 7px;
+ color: var(--text-dim);
+}
+
+.legend-dot {
+ width: 10px;
+ height: 10px;
+}
+
+.legend-dot.chill { background: var(--green); }
+.legend-dot.moderate { background: var(--gold); }
+.legend-dot.intense { background: var(--orange); }
+.legend-dot.boss { background: var(--red); }
+
+/* boss fights list */
+.boss-list-section {
+ margin-top: 16px;
+}
+
+.boss-list-section h3 {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 9px;
+ color: var(--white);
+ letter-spacing: 2px;
+ margin-bottom: 16px;
+}
+
+.boss-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ margin-bottom: 6px;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-left: 3px solid var(--red);
+}
+
+.boss-item-name {
+ font-size: 13px;
+ color: var(--text);
+}
+
+.boss-item-date {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 7px;
+ color: var(--text-dim);
+}
+
+/* ==== QUEST LIST ==== */
+
+.stress-meter {
+ margin-bottom: 24px;
+}
+
+.meter-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.meter-label {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 8px;
+ color: var(--text-dim);
+ width: 60px;
+}
+
+.quest-list {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.quest-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 16px;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-left: 3px solid var(--accent);
+ transition: all 0.15s;
+}
+
+.quest-item.wellness {
+ border-left-color: var(--green);
+}
+
+.quest-item.done {
+ opacity: 0.35;
+}
+
+.quest-item.done .quest-text {
+ text-decoration: line-through;
+}
+
+.quest-check {
+ width: 22px;
+ height: 22px;
+ border: 1px solid var(--border);
+ background: var(--bg);
+ color: var(--green);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ flex-shrink: 0;
+ transition: all 0.15s;
+}
+
+.quest-check:hover {
+ border-color: var(--accent);
+ background: var(--panel-lighter);
+}
+
+.quest-item.done .quest-check {
+ background: var(--green);
+ border-color: var(--green);
+ color: var(--bg);
+}
+
+.quest-text {
+ font-size: 13px;
+ flex: 1;
+}
+
+.quest-xp {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 7px;
+ color: var(--green);
+}
+
+/* ==== CHARACTER SHEET ==== */
+
+.char-stats {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 12px;
+ margin-bottom: 24px;
+}
+
+.char-stat-card {
+ position: relative;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ padding: 20px 12px;
+ text-align: center;
+}
+
+.char-stat-icon {
+ display: block;
+ font-size: 24px;
+ margin-bottom: 8px;
+}
+
+.char-stat-val {
+ display: block;
+ font-family: 'Press Start 2P', monospace;
+ font-size: 16px;
+ color: var(--white);
+ margin-bottom: 6px;
+}
+
+.char-stat-name {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 7px;
+ color: var(--text-dim);
+ letter-spacing: 1px;
+}
+
+/* burnout badge */
+.burnout-section {
+ margin-bottom: 32px;
+ text-align: center;
+}
+
+.burnout-badge {
+ display: inline-block;
+ padding: 10px 24px;
+ border: 1px solid var(--border);
+ font-family: 'Press Start 2P', monospace;
+ font-size: 8px;
+ letter-spacing: 1px;
+}
+
+.burnout-badge.safe {
+ color: var(--green);
+ border-color: rgba(34, 197, 94, 0.3);
+ background: var(--green-dim);
+}
+
+.burnout-badge.warning {
+ color: var(--gold);
+ border-color: rgba(245, 158, 11, 0.3);
+ background: var(--gold-dim);
+}
+
+.burnout-badge.danger {
+ color: var(--red);
+ border-color: rgba(239, 68, 68, 0.3);
+ background: var(--red-dim);
+ animation: burnoutFlash 1.5s ease-in-out infinite;
+}
+
+@keyframes burnoutFlash {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.7; }
+}
+
+/* achievements */
+.achievements-section h3 {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 9px;
+ color: var(--white);
+ letter-spacing: 2px;
+ margin-bottom: 16px;
+ text-align: center;
+}
+
+.achievement-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 10px;
+}
+
+.ach-card {
+ text-align: center;
+ padding: 16px 8px;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ transition: all 0.2s;
+}
+
+.ach-card .ach-icon {
+ display: block;
+ font-size: 24px;
+ margin-bottom: 6px;
+}
+
+.ach-card .ach-name {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 7px;
+ color: var(--text-dim);
+ letter-spacing: 1px;
+}
+
+.ach-card.locked {
+ opacity: 0.2;
+ filter: grayscale(1);
+}
+
+.ach-card.unlocked {
+ border-color: var(--gold);
+ box-shadow: 0 0 8px var(--gold-dim);
+}
+
+.ach-card.unlocked .ach-name {
+ color: var(--gold);
+}
+
+/* ==== MODALS ==== */
+
+.modal-overlay {
+ display: none;
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.75);
+ z-index: 200;
+ align-items: center;
+ justify-content: center;
+}
+
+.modal-overlay.open {
+ display: flex;
+}
+
+.modal-box {
+ position: relative;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ padding: 32px;
+ width: 90%;
+ max-width: 460px;
+ max-height: 85vh;
+ overflow-y: auto;
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24px;
+}
+
+.modal-header h2 {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 11px;
+ color: var(--white);
+ letter-spacing: 2px;
+}
+
+.modal-close {
+ background: none;
+ border: none;
+ color: var(--text-dim);
+ font-size: 20px;
+ cursor: pointer;
+}
+
+.modal-close:hover { color: var(--white); }
+
+/* shared form stuff for modals */
+.modal-box .field {
+ margin-bottom: 16px;
+}
+
+.modal-box .field label {
+ display: block;
+ font-family: 'Press Start 2P', monospace;
+ font-size: 7px;
+ color: var(--text-dim);
+ letter-spacing: 1px;
+ margin-bottom: 6px;
+}
+
+.modal-box .field input,
+.modal-box .field select {
+ width: 100%;
+ padding: 10px 12px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ color: var(--text);
+ font-family: 'Space Mono', monospace;
+ font-size: 13px;
+ outline: none;
+}
+
+.modal-box .field input:focus,
+.modal-box .field select:focus {
+ border-color: var(--accent);
+}
+
+.modal-box .field select {
+ cursor: pointer;
+}
+
+.range-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.range-row input[type="range"] {
+ flex: 1;
+ accent-color: var(--accent);
+}
+
+.range-val {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 10px;
+ color: var(--accent);
+ width: 20px;
+ text-align: center;
+}
+
+.divider {
+ border: none;
+ border-top: 1px solid var(--border);
+ margin: 20px 0;
+}
+
+.add-course-btn {
+ width: 100%;
+ padding: 10px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ color: var(--text);
+ font-family: 'Press Start 2P', monospace;
+ font-size: 8px;
+ cursor: pointer;
+ letter-spacing: 1px;
+ transition: all 0.2s;
+}
+
+.add-course-btn:hover {
+ border-color: var(--accent);
+ color: var(--white);
+}
+
+.danger-btn {
+ width: 100%;
+ padding: 10px;
+ background: none;
+ border: 1px solid var(--red);
+ color: var(--red);
+ font-family: 'Press Start 2P', monospace;
+ font-size: 8px;
+ cursor: pointer;
+ letter-spacing: 1px;
+ transition: all 0.2s;
+}
+
+.danger-btn:hover {
+ background: var(--red);
+ color: var(--white);
+}
+
+/* course chips in settings */
+.course-chip {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 10px 12px;
+ margin-top: 8px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ font-size: 12px;
+}
+
+.course-chip .remove-course {
+ background: none;
+ border: none;
+ color: var(--red);
+ cursor: pointer;
+ font-size: 14px;
+}
+
+/* deadline rows in add course */
+.deadlines-section {
+ margin-bottom: 16px;
+}
+
+.deadlines-section h3 {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 8px;
+ color: var(--text-dim);
+ margin-bottom: 12px;
+}
+
+.dl-row {
+ display: flex;
+ gap: 6px;
+ margin-bottom: 8px;
+ align-items: center;
+}
+
+.dl-row input[type="text"] {
+ flex: 2;
+ padding: 8px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ color: var(--text);
+ font-family: 'Space Mono', monospace;
+ font-size: 12px;
+ outline: none;
+}
+
+.dl-row input[type="date"] {
+ flex: 1;
+ padding: 8px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ color: var(--text);
+ font-family: 'Space Mono', monospace;
+ font-size: 12px;
+ outline: none;
+}
+
+.dl-row select {
+ width: 100px;
+ padding: 8px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ color: var(--text);
+ font-size: 11px;
+ cursor: pointer;
+}
+
+.dl-row .remove-dl {
+ background: none;
+ border: none;
+ color: var(--red);
+ cursor: pointer;
+ font-size: 14px;
+}
+
+.small-btn {
+ padding: 6px 12px;
+ background: none;
+ border: 1px solid var(--border);
+ color: var(--text-dim);
+ font-family: 'Press Start 2P', monospace;
+ font-size: 7px;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.small-btn:hover {
+ border-color: var(--accent);
+ color: var(--white);
+}
+
+/* ==== RESPONSIVE ==== */
+
+@media (max-width: 768px) {
+ .hud-stats { display: none; }
+ .topbar { flex-wrap: wrap; gap: 8px; height: auto; padding: 10px 16px; }
+ .tab-bar { padding: 0 8px; overflow-x: auto; }
+ .tab { padding: 12px 12px; font-size: 7px; }
+ .dash-main { padding: 16px; }
+ .week-grid { grid-template-columns: repeat(4, 1fr); }
+ .char-stats { grid-template-columns: repeat(2, 1fr); }
+ .achievement-grid { grid-template-columns: repeat(2, 1fr); }
+}
diff --git a/public/css/main.css b/public/css/main.css
new file mode 100644
index 0000000..2318122
--- /dev/null
+++ b/public/css/main.css
@@ -0,0 +1,353 @@
+@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Space+Mono:wght@400;700&display=swap');
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ --bg: #0b0e13;
+ --panel: #111820;
+ --panel-lighter: #161d27;
+ --border: #1e2a38;
+ --border-hover: #2a3a4d;
+ --text: #c5cdd8;
+ --text-dim: #5a6a7a;
+ --white: #e8ecf0;
+ --accent: #4a7cff;
+ --accent-dim: #3355aa;
+ --green: #22c55e;
+ --green-dim: rgba(34, 197, 94, 0.15);
+ --red: #ef4444;
+ --red-dim: rgba(239, 68, 68, 0.15);
+ --gold: #f59e0b;
+ --gold-dim: rgba(245, 158, 11, 0.15);
+ --orange: #f97316;
+ --purple: #a855f7;
+ --purple-dim: rgba(168, 85, 247, 0.15);
+}
+
+body {
+ background: var(--bg);
+ color: var(--text);
+ font-family: 'Space Mono', monospace;
+ min-height: 100vh;
+ line-height: 1.6;
+}
+
+a { color: inherit; text-decoration: none; }
+
+/* ==== TOPBAR ==== */
+
+.topbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 32px;
+ height: 56px;
+ background: var(--panel);
+ border-bottom: 1px solid var(--border);
+ position: sticky;
+ top: 0;
+ z-index: 100;
+}
+
+.topbar-left {
+ display: flex;
+ align-items: center;
+ gap: 32px;
+}
+
+.nav-logo {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 11px;
+ color: var(--white);
+ letter-spacing: 2px;
+}
+
+.logo-square {
+ color: var(--accent);
+ font-size: 8px;
+ vertical-align: middle;
+}
+
+.nav-links {
+ display: flex;
+ gap: 24px;
+}
+
+.nav-links a {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 8px;
+ color: var(--text-dim);
+ letter-spacing: 1px;
+ transition: color 0.2s;
+}
+
+.nav-links a:hover { color: var(--white); }
+
+.nav-diamond {
+ font-size: 6px;
+ vertical-align: middle;
+ color: var(--text-dim);
+}
+
+.topbar-right {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.nav-btn {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 8px;
+ padding: 8px 16px;
+ letter-spacing: 1px;
+ border: 1px solid var(--border);
+ transition: all 0.2s;
+}
+
+.nav-btn:hover {
+ border-color: var(--accent);
+ color: var(--white);
+}
+
+.signup-btn {
+ background: var(--white);
+ color: var(--bg);
+ border-color: var(--white);
+}
+
+.signup-btn:hover {
+ background: var(--accent);
+ border-color: var(--accent);
+ color: var(--white);
+}
+
+/* ==== HERO ==== */
+
+.hero {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: calc(100vh - 56px);
+ text-align: center;
+ position: relative;
+ overflow: hidden;
+}
+
+.hero::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background:
+ radial-gradient(circle at 20% 50%, rgba(74, 124, 255, 0.04) 0%, transparent 50%),
+ radial-gradient(circle at 80% 50%, rgba(168, 85, 247, 0.03) 0%, transparent 50%);
+ pointer-events: none;
+}
+
+.hero-inner {
+ position: relative;
+ z-index: 1;
+}
+
+.hero-title {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 48px;
+ margin-bottom: 24px;
+ letter-spacing: 4px;
+}
+
+.hero-auto { color: var(--white); }
+.hero-pilot { color: var(--accent); }
+
+.hero-tagline {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 11px;
+ color: var(--text-dim);
+ letter-spacing: 3px;
+ min-height: 1.5em;
+ display: inline;
+}
+
+.hero-cursor {
+ display: inline;
+ font-family: 'Press Start 2P', monospace;
+ font-size: 11px;
+ color: var(--text-dim);
+ margin-left: 2px;
+}
+
+.cta-btn {
+ display: inline-block;
+ margin-top: 48px;
+ font-family: 'Press Start 2P', monospace;
+ font-size: 10px;
+ padding: 14px 28px;
+ background: var(--accent);
+ color: var(--white);
+ border: none;
+ letter-spacing: 2px;
+ transition: all 0.2s;
+ cursor: pointer;
+}
+
+.cta-btn:hover {
+ background: #5a8cff;
+ transform: translateY(-1px);
+}
+
+.btn-square {
+ font-size: 7px;
+ vertical-align: middle;
+}
+
+/* ==== SECTIONS ==== */
+
+.section-title {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 12px;
+ text-align: center;
+ color: var(--white);
+ letter-spacing: 3px;
+ margin-bottom: 48px;
+}
+
+/* ==== FEATURES ==== */
+
+.features {
+ padding: 80px 32px;
+ max-width: 1100px;
+ margin: 0 auto;
+}
+
+.features-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 20px;
+}
+
+.feature-card {
+ position: relative;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ padding: 32px 24px;
+ transition: border-color 0.2s;
+}
+
+.feature-card:hover {
+ border-color: var(--border-hover);
+}
+
+/* corner brackets like college.xyz modals */
+.card-corner {
+ position: absolute;
+ width: 8px;
+ height: 8px;
+ border-color: var(--accent);
+ border-style: solid;
+}
+.card-corner.tl { top: -1px; left: -1px; border-width: 2px 0 0 2px; }
+.card-corner.tr { top: -1px; right: -1px; border-width: 2px 2px 0 0; }
+.card-corner.bl { bottom: -1px; left: -1px; border-width: 0 0 2px 2px; }
+.card-corner.br { bottom: -1px; right: -1px; border-width: 0 2px 2px 0; }
+
+.feature-icon {
+ font-size: 28px;
+ margin-bottom: 16px;
+ color: var(--accent);
+}
+
+.feature-card h3 {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 10px;
+ color: var(--white);
+ margin-bottom: 12px;
+ letter-spacing: 1px;
+}
+
+.feature-card p {
+ font-size: 13px;
+ color: var(--text-dim);
+ line-height: 1.7;
+}
+
+/* ==== HOW IT WORKS ==== */
+
+.how-section {
+ padding: 80px 32px;
+ max-width: 800px;
+ margin: 0 auto;
+}
+
+.steps {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+}
+
+.step {
+ display: flex;
+ gap: 24px;
+ align-items: flex-start;
+ padding: 24px;
+ background: var(--panel);
+ border: 1px solid var(--border);
+}
+
+.step-num {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 20px;
+ color: var(--accent);
+ flex-shrink: 0;
+ line-height: 1;
+ padding-top: 4px;
+}
+
+.step h3 {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 9px;
+ color: var(--white);
+ margin-bottom: 8px;
+ letter-spacing: 1px;
+}
+
+.step p {
+ font-size: 13px;
+ color: var(--text-dim);
+ line-height: 1.7;
+}
+
+/* ==== FOOTER ==== */
+
+.site-footer {
+ text-align: center;
+ padding: 48px 32px;
+ border-top: 1px solid var(--border);
+}
+
+.site-footer p {
+ font-family: 'Press Start 2P', monospace;
+ font-size: 9px;
+ color: var(--text-dim);
+ letter-spacing: 2px;
+}
+
+.footer-sub {
+ margin-top: 8px;
+ font-size: 8px !important;
+ color: var(--text-dim);
+ opacity: 0.5;
+}
+
+/* ==== RESPONSIVE ==== */
+
+@media (max-width: 768px) {
+ .topbar { padding: 0 16px; }
+ .nav-links { display: none; }
+ .hero-title { font-size: 28px; }
+ .features-grid { grid-template-columns: 1fr; }
+ .features { padding: 48px 16px; }
+ .how-section { padding: 48px 16px; }
+}
diff --git a/public/dashboard.html b/public/dashboard.html
new file mode 100644
index 0000000..6f1ec7b
--- /dev/null
+++ b/public/dashboard.html
@@ -0,0 +1,287 @@
+
+
+
+
+
+ Autopilot — Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
SET UP YOUR SEMESTER TO SEE THE MAP ⚙
+
+
+
+ CHILL
+ MODERATE
+ INTENSE
+ BOSS FIGHT
+
+
+
+
☠ UPCOMING BOSS FIGHTS
+
+
NO BOSS FIGHTS ON THE HORIZON
+
+
+
+
+
+
+
+
+
+
+
+
ADD COURSES TO BEGIN YOUR ADVENTURE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
★
+
0
+
TOTAL XP
+
+
+
+
+
+
+
🔥
+
0
+
DAY STREAK
+
+
+
+
+
+
+
⚔
+
0
+
QUESTS DONE
+
+
+
+
+
+ BURNOUT RISK:
+ LOW
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..e2b71dd
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,148 @@
+
+
+
+
+
+ Autopilot
+
+
+
+
+
+
+
+
+
+
+
+ ■ CORE SYSTEMS ■
+
+
+
+
+
+
+
+
☣
+
SEMESTER MAP
+
14-week heatmap of your whole semester. Boss fights marked from day one. See the hard weeks before they hit you.
+
+
+
+
+
+
+
+
⚔
+
QUEST LOG
+
Daily missions sorted by urgency. One tap to complete. XP awarded instantly. The most satisfying part of your day.
+
+
+
+
+
+
+
+
♥
+
LIFE BAR HUD
+
HP drops when you skip sleep. Stress rises near deadlines. Hit zero and the burnout warning fires. Take care of yourself.
+
+
+
+
+
+
+
+
★
+
CHARACTER SHEET
+
Your stats page. Sleep streak, total XP, achievements unlocked. See exactly how you're playing the semester.
+
+
+
+
+
+ ■ HOW IT WORKS ■
+
+
+
01
+
+
SET YOUR SEMESTER
+
Enter your start date, end date, and courses. Add every deadline — assignments, midterms, finals. The map builds itself.
+
+
+
+
02
+
+
PLAY EACH DAY
+
Check in daily. Log your sleep, stress, exercise. Complete your quests. Watch your XP climb and your HP stay green.
+
+
+
+
03
+
+
SURVIVE THE BOSS FIGHTS
+
Midterms and finals are boss fights. Prep quests appear weeks before. Recovery blocks protect you after. Play smart, not hard.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/js/dashboard.js b/public/js/dashboard.js
new file mode 100644
index 0000000..9725092
--- /dev/null
+++ b/public/js/dashboard.js
@@ -0,0 +1,741 @@
+/*
+ dashboard.js — autopilot game logic
+ handles semester map, quests, character sheet, check-ins
+ talks to the express backend via fetch
+*/
+
+var user = null;
+var courses = [];
+var checkins = [];
+var completedQuests = [];
+var achievements = [];
+
+// ============ BOOT ============
+
+loadUser();
+
+async function loadUser() {
+ try {
+ var res = await fetch('/api/auth/me');
+ if (!res.ok) {
+ window.location.href = '/login';
+ return;
+ }
+ user = await res.json();
+ await loadAll();
+ renderAll();
+
+ // first time? pop settings
+ if (!user.sem_start) {
+ setTimeout(function() { openModal('settingsModal'); }, 300);
+ }
+ } catch(e) {
+ window.location.href = '/login';
+ }
+}
+
+async function loadAll() {
+ var results = await Promise.all([
+ fetch('/api/courses').then(function(r) { return r.json(); }),
+ fetch('/api/checkins').then(function(r) { return r.json(); }),
+ fetch('/api/quests/completed').then(function(r) { return r.json(); }),
+ fetch('/api/quests/achievements').then(function(r) { return r.json(); })
+ ]);
+ courses = results[0];
+ checkins = results[1];
+ completedQuests = results[2].map(function(q) { return q.quest_id; });
+ achievements = results[3];
+}
+
+// ============ TABS ============
+
+var tabs = document.querySelectorAll('.tab');
+var panels = document.querySelectorAll('.tab-panel');
+
+tabs.forEach(function(tab) {
+ tab.onclick = function() {
+ tabs.forEach(function(t) { t.classList.remove('active'); });
+ panels.forEach(function(p) { p.classList.remove('active'); });
+ tab.classList.add('active');
+ var target = tab.getAttribute('data-tab');
+ document.getElementById('panel-' + target).classList.add('active');
+ };
+});
+
+// ============ SEMESTER WEEKS ============
+
+function getWeeks() {
+ if (!user.sem_start || !user.sem_end) return [];
+ var start = new Date(user.sem_start + 'T00:00:00');
+ var end = new Date(user.sem_end + 'T00:00:00');
+ var weeks = [];
+ var cur = new Date(start);
+ var n = 1;
+
+ while (cur <= end) {
+ var weekEnd = new Date(cur);
+ weekEnd.setDate(weekEnd.getDate() + 6);
+ if (weekEnd > end) weekEnd = new Date(end);
+
+ weeks.push({
+ num: n,
+ start: new Date(cur),
+ end: new Date(weekEnd)
+ });
+
+ cur.setDate(cur.getDate() + 7);
+ n++;
+ }
+ return weeks;
+}
+
+function weekLoad(week) {
+ var load = 0;
+ courses.forEach(function(c) {
+ c.deadlines.forEach(function(dl) {
+ var d = new Date(dl.date + 'T00:00:00');
+ if (d >= week.start && d <= week.end) {
+ var w = dl.type === 'exam' ? 3 : dl.type === 'project' ? 2 : 1;
+ load += c.difficulty * w;
+ }
+ });
+ });
+ return load;
+}
+
+function weekClass(load) {
+ if (load >= 12) return 'boss';
+ if (load >= 7) return 'intense';
+ if (load >= 3) return 'moderate';
+ return 'chill';
+}
+
+function isCurrent(week) {
+ var now = new Date();
+ return now >= week.start && now <= week.end;
+}
+
+function isPast(week) {
+ return new Date() > week.end;
+}
+
+function weekDeadlines(week) {
+ var out = [];
+ courses.forEach(function(c) {
+ c.deadlines.forEach(function(dl) {
+ var d = new Date(dl.date + 'T00:00:00');
+ if (d >= week.start && d <= week.end) {
+ out.push({ course: c.name, label: dl.label, date: dl.date, type: dl.type });
+ }
+ });
+ });
+ return out;
+}
+
+// ============ RENDER: SEMESTER MAP ============
+
+function renderMap() {
+ var grid = document.getElementById('weekGrid');
+ grid.innerHTML = '';
+ var weeks = getWeeks();
+
+ if (weeks.length === 0) {
+ grid.innerHTML = 'SET UP YOUR SEMESTER TO SEE THE MAP ⚙
';
+ document.getElementById('currentWeek').textContent = '-';
+ return;
+ }
+
+ var foundCurrent = false;
+
+ weeks.forEach(function(week) {
+ var load = weekLoad(week);
+ var cls = weekClass(load);
+ var dls = weekDeadlines(week);
+
+ var tile = document.createElement('div');
+ tile.className = 'week-tile ' + cls;
+ if (isPast(week)) tile.classList.add('past');
+ if (isCurrent(week)) {
+ tile.classList.add('current');
+ foundCurrent = true;
+ document.getElementById('currentWeek').textContent = week.num;
+ }
+
+ var tipText = 'Week ' + week.num;
+ if (dls.length) {
+ tipText += ': ' + dls.map(function(d) { return d.course + ' — ' + d.label; }).join(', ');
+ }
+
+ var bossIcon = cls === 'boss' ? '☠' : '';
+
+ tile.innerHTML =
+ bossIcon +
+ 'W' + week.num + '' +
+ '' + cls + '' +
+ '' + tipText + '
';
+
+ grid.appendChild(tile);
+ });
+
+ if (!foundCurrent) {
+ document.getElementById('currentWeek').textContent = '-';
+ }
+}
+
+// ============ RENDER: BOSS FIGHTS ============
+
+function renderBosses() {
+ var container = document.getElementById('bossList');
+ container.innerHTML = '';
+
+ var today = new Date();
+ today.setHours(0, 0, 0, 0);
+ var bosses = [];
+
+ courses.forEach(function(c) {
+ c.deadlines.forEach(function(dl) {
+ var d = new Date(dl.date + 'T00:00:00');
+ if (d >= today && (dl.type === 'exam' || dl.type === 'project')) {
+ bosses.push({ course: c.name, label: dl.label, date: dl.date, type: dl.type });
+ }
+ });
+ });
+
+ bosses.sort(function(a, b) { return a.date < b.date ? -1 : 1; });
+
+ if (bosses.length === 0) {
+ container.innerHTML = 'NO BOSS FIGHTS ON THE HORIZON
';
+ return;
+ }
+
+ bosses.slice(0, 6).forEach(function(b) {
+ var icon = b.type === 'exam' ? '☠' : '⚔';
+ var div = document.createElement('div');
+ div.className = 'boss-item';
+ div.innerHTML =
+ '' + icon + ' ' + b.course + ' — ' + b.label + '' +
+ '' + prettyDate(b.date) + '';
+ container.appendChild(div);
+ });
+}
+
+// ============ RENDER: QUESTS ============
+
+function renderQuests() {
+ var container = document.getElementById('questList');
+ container.innerHTML = '';
+
+ var today = new Date();
+ today.setHours(0, 0, 0, 0);
+ var endOfWeek = new Date(today);
+ endOfWeek.setDate(endOfWeek.getDate() + (7 - endOfWeek.getDay()));
+
+ var quests = [];
+
+ // deadline quests coming up this week
+ courses.forEach(function(c) {
+ c.deadlines.forEach(function(dl) {
+ var d = new Date(dl.date + 'T00:00:00');
+ if (d >= today && d <= endOfWeek) {
+ quests.push({
+ id: c.name + '|' + dl.date + '|' + dl.label,
+ text: c.name + ': ' + dl.label,
+ type: dl.type,
+ date: dl.date
+ });
+ }
+ });
+ });
+
+ // wellness quests
+ var wellness = buildWellnessQuests();
+ quests = wellness.concat(quests);
+
+ if (quests.length === 0) {
+ container.innerHTML = 'ADD COURSES TO BEGIN YOUR ADVENTURE
';
+ return;
+ }
+
+ quests.forEach(function(q) {
+ var done = completedQuests.indexOf(q.id) !== -1;
+ var div = document.createElement('div');
+ div.className = 'quest-item';
+ if (q.type === 'wellness') div.classList.add('wellness');
+ if (done) div.classList.add('done');
+
+ var checkBtn = document.createElement('button');
+ checkBtn.className = 'quest-check';
+ checkBtn.textContent = done ? '✓' : '';
+ checkBtn.onclick = function() { toggleQuest(q.id); };
+
+ var text = document.createElement('span');
+ text.className = 'quest-text';
+ text.textContent = q.text;
+
+ var xp = document.createElement('span');
+ xp.className = 'quest-xp';
+ xp.textContent = '+10 XP';
+
+ div.appendChild(checkBtn);
+ div.appendChild(text);
+ div.appendChild(xp);
+ container.appendChild(div);
+ });
+}
+
+function buildWellnessQuests() {
+ var today = todayStr();
+ var prefix = 'wellness|' + today + '|';
+ var q = [];
+ var type = user.energy_type || 'morning';
+
+ q.push({ id: prefix + 'water', text: 'Drink water (8 glasses)', type: 'wellness', date: today });
+ q.push({ id: prefix + 'meal', text: 'Eat a real meal', type: 'wellness', date: today });
+
+ if (type === 'morning') {
+ q.push({ id: prefix + 'study', text: 'Deep study block (morning)', type: 'wellness', date: today });
+ q.push({ id: prefix + 'wind', text: 'Wind down by 10pm', type: 'wellness', date: today });
+ } else if (type === 'night') {
+ q.push({ id: prefix + 'study', text: 'Deep study block (evening)', type: 'wellness', date: today });
+ q.push({ id: prefix + 'sleep', text: 'No alarm — full sleep cycle', type: 'wellness', date: today });
+ } else {
+ q.push({ id: prefix + 'study', text: 'Deep study block (afternoon)', type: 'wellness', date: today });
+ q.push({ id: prefix + 'break', text: 'Take a 15min break outside', type: 'wellness', date: today });
+ }
+
+ q.push({ id: prefix + 'move', text: 'Move your body (any exercise)', type: 'wellness', date: today });
+ return q;
+}
+
+// ============ QUEST TOGGLE ============
+
+async function toggleQuest(id) {
+ var idx = completedQuests.indexOf(id);
+ if (idx === -1) {
+ completedQuests.push(id);
+ user.xp = (user.xp || 0) + 10;
+ await fetch('/api/quests/complete', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ quest_id: id })
+ });
+ } else {
+ completedQuests.splice(idx, 1);
+ user.xp = Math.max(0, (user.xp || 0) - 10);
+ await fetch('/api/quests/uncomplete', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ quest_id: id })
+ });
+ }
+
+ await fetch('/api/auth/stats', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ xp: user.xp })
+ });
+
+ checkAchievements();
+ renderAll();
+}
+
+// ============ HP / XP ============
+
+function recalcHP() {
+ var todayCheckin = getTodayCheckin();
+
+ if (!todayCheckin) {
+ var gap = daysSinceCheckin();
+ user.hp = Math.max(20, 100 - (gap * 5));
+ } else {
+ var sleepBoost = (todayCheckin.sleep - 1) * 10;
+ var stressPenalty = (todayCheckin.stress - 1) * 8;
+ var exerciseBoost = todayCheckin.exercise ? 15 : 0;
+ user.hp = Math.min(100, Math.max(10, 50 + sleepBoost - stressPenalty + exerciseBoost));
+ }
+
+ // update server (fire and forget)
+ fetch('/api/auth/stats', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ hp: user.hp })
+ });
+}
+
+function updateBars() {
+ var hp = user.hp || 100;
+ var xp = user.xp || 0;
+
+ document.getElementById('hpBar').style.width = hp + '%';
+ document.getElementById('hpVal').textContent = Math.round(hp);
+ document.getElementById('xpBar').style.width = (xp % 100) + '%';
+ document.getElementById('xpVal').textContent = xp;
+
+ // character sheet stats
+ document.getElementById('charHP').textContent = Math.round(hp);
+ document.getElementById('charXP').textContent = xp;
+ document.getElementById('charStreak').textContent = getStreak();
+ document.getElementById('charQuests').textContent = completedQuests.length;
+
+ // stress bar
+ var todayCheckin = getTodayCheckin();
+ var stressPercent = todayCheckin ? (todayCheckin.stress / 5) * 100 : 30;
+ document.getElementById('stressBar').style.width = stressPercent + '%';
+
+ // burnout badge
+ updateBurnout(hp);
+}
+
+function updateBurnout(hp) {
+ var badge = document.getElementById('burnoutBadge');
+ var level = document.getElementById('burnoutLevel');
+
+ badge.classList.remove('safe', 'warning', 'danger');
+
+ if (hp > 60) {
+ badge.classList.add('safe');
+ level.textContent = 'LOW';
+ } else if (hp > 30) {
+ badge.classList.add('warning');
+ level.textContent = 'MODERATE';
+ } else {
+ badge.classList.add('danger');
+ level.textContent = 'HIGH — TAKE A BREAK';
+ }
+}
+
+function getTodayCheckin() {
+ var today = todayStr();
+ for (var i = 0; i < checkins.length; i++) {
+ if (checkins[i].date === today) return checkins[i];
+ }
+ return null;
+}
+
+function daysSinceCheckin() {
+ if (checkins.length === 0) return 0;
+ var dates = checkins.map(function(c) { return c.date; }).sort().reverse();
+ var last = new Date(dates[0] + 'T00:00:00');
+ var now = new Date();
+ return Math.floor((now - last) / (86400000));
+}
+
+function getStreak() {
+ if (checkins.length === 0) return 0;
+ var dates = checkins.map(function(c) { return c.date; }).sort().reverse();
+
+ // dedupe
+ var unique = [];
+ dates.forEach(function(d) {
+ if (unique.indexOf(d) === -1) unique.push(d);
+ });
+
+ var streak = 1;
+ for (var i = 1; i < unique.length; i++) {
+ var prev = new Date(unique[i - 1] + 'T00:00:00');
+ var curr = new Date(unique[i] + 'T00:00:00');
+ var diff = (prev - curr) / 86400000;
+ if (diff === 1) streak++;
+ else break;
+ }
+ return streak;
+}
+
+// ============ ACHIEVEMENTS ============
+
+var achievementDefs = [
+ { id: 'first-checkin', icon: '🌅', name: 'FIRST DAWN' },
+ { id: 'streak-3', icon: '🔥', name: 'ON FIRE' },
+ { id: 'streak-7', icon: '⚡', name: 'UNSTOPPABLE' },
+ { id: '5-quests', icon: '⚔', name: 'ADVENTURER' },
+ { id: '20-quests', icon: '🛡', name: 'VETERAN' },
+ { id: '50-quests', icon: '👑', name: 'LEGEND' },
+ { id: 'hp-guardian', icon: '💚', name: 'IRON WILL' },
+ { id: 'early-bird', icon: '🐦', name: 'EARLY BIRD' }
+];
+
+function checkAchievements() {
+ var count = completedQuests.length;
+ var streak = getStreak();
+
+ if (checkins.length >= 1) unlockAch('first-checkin');
+ if (streak >= 3) unlockAch('streak-3');
+ if (streak >= 7) unlockAch('streak-7');
+ if (count >= 5) unlockAch('5-quests');
+ if (count >= 20) unlockAch('20-quests');
+ if (count >= 50) unlockAch('50-quests');
+ if (user.hp >= 80 && streak >= 7) unlockAch('hp-guardian');
+}
+
+async function unlockAch(id) {
+ if (achievements.indexOf(id) !== -1) return;
+ achievements.push(id);
+ await fetch('/api/quests/achievements', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ achievement_id: id })
+ });
+}
+
+function renderAchievements() {
+ var grid = document.getElementById('achievementGrid');
+ grid.innerHTML = '';
+
+ achievementDefs.forEach(function(ach) {
+ var unlocked = achievements.indexOf(ach.id) !== -1;
+ var div = document.createElement('div');
+ div.className = 'ach-card ' + (unlocked ? 'unlocked' : 'locked');
+ div.innerHTML =
+ '' + ach.icon + '' +
+ '' + ach.name + '';
+ grid.appendChild(div);
+ });
+}
+
+// ============ MODALS ============
+
+function openModal(id) {
+ document.getElementById(id).classList.add('open');
+}
+
+function closeModal(id) {
+ document.getElementById(id).classList.remove('open');
+}
+
+// close on backdrop click
+document.querySelectorAll('.modal-overlay').forEach(function(overlay) {
+ overlay.onclick = function(e) {
+ if (e.target === overlay) overlay.classList.remove('open');
+ };
+});
+
+// --- settings modal ---
+document.getElementById('settingsBtn').onclick = function() {
+ document.getElementById('semStart').value = user.sem_start || '';
+ document.getElementById('semEnd').value = user.sem_end || '';
+ document.getElementById('energyType').value = user.energy_type || 'morning';
+ renderCourseList();
+ openModal('settingsModal');
+};
+
+document.getElementById('closeSettings').onclick = function() { closeModal('settingsModal'); };
+
+document.getElementById('settingsForm').onsubmit = async function(e) {
+ e.preventDefault();
+ user.sem_start = document.getElementById('semStart').value;
+ user.sem_end = document.getElementById('semEnd').value;
+ user.energy_type = document.getElementById('energyType').value;
+
+ await fetch('/api/auth/settings', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ sem_start: user.sem_start,
+ sem_end: user.sem_end,
+ energy_type: user.energy_type
+ })
+ });
+
+ renderAll();
+};
+
+// --- check-in modal ---
+document.getElementById('checkinBtn').onclick = function() { openModal('checkinModal'); };
+document.getElementById('closeCheckin').onclick = function() { closeModal('checkinModal'); };
+
+document.getElementById('sleepScore').oninput = function() {
+ document.getElementById('sleepLabel').textContent = this.value;
+};
+document.getElementById('stressScore').oninput = function() {
+ document.getElementById('stressLabel').textContent = this.value;
+};
+
+document.getElementById('checkinForm').onsubmit = async function(e) {
+ e.preventDefault();
+
+ var entry = {
+ date: todayStr(),
+ sleep: parseInt(document.getElementById('sleepScore').value),
+ stress: parseInt(document.getElementById('stressScore').value),
+ exercise: document.getElementById('exerciseCheck').value === 'yes'
+ };
+
+ await fetch('/api/checkins', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(entry)
+ });
+
+ // update local state
+ var existing = -1;
+ for (var i = 0; i < checkins.length; i++) {
+ if (checkins[i].date === entry.date) { existing = i; break; }
+ }
+ if (existing >= 0) {
+ checkins[existing] = entry;
+ } else {
+ checkins.unshift(entry);
+ }
+
+ closeModal('checkinModal');
+ checkAchievements();
+ recalcHP();
+ renderAll();
+};
+
+// --- course modal ---
+document.getElementById('addCourseBtn').onclick = function() {
+ closeModal('settingsModal');
+ document.getElementById('courseForm').reset();
+ document.getElementById('deadlineRows').innerHTML = '';
+ document.getElementById('diffLabel').textContent = '3';
+ openModal('courseModal');
+};
+
+document.getElementById('closeCourse').onclick = function() {
+ closeModal('courseModal');
+ openModal('settingsModal');
+};
+
+document.getElementById('courseDiff').oninput = function() {
+ document.getElementById('diffLabel').textContent = this.value;
+};
+
+document.getElementById('addDeadlineBtn').onclick = function() {
+ var row = document.createElement('div');
+ row.className = 'dl-row';
+ row.innerHTML =
+ '' +
+ '' +
+ '' +
+ '';
+
+ row.querySelector('.remove-dl').onclick = function() { row.remove(); };
+ document.getElementById('deadlineRows').appendChild(row);
+};
+
+document.getElementById('courseForm').onsubmit = async function(e) {
+ e.preventDefault();
+ var name = document.getElementById('courseName').value.trim();
+ var diff = parseInt(document.getElementById('courseDiff').value);
+ var deadlines = [];
+
+ var rows = document.getElementById('deadlineRows').querySelectorAll('.dl-row');
+ rows.forEach(function(row) {
+ var inputs = row.querySelectorAll('input');
+ var sel = row.querySelector('select');
+ if (inputs[0].value && inputs[1].value) {
+ deadlines.push({
+ label: inputs[0].value.trim(),
+ date: inputs[1].value,
+ type: sel.value
+ });
+ }
+ });
+
+ var res = await fetch('/api/courses', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name: name, difficulty: diff, deadlines: deadlines })
+ });
+
+ var course = await res.json();
+ courses.push(course);
+
+ closeModal('courseModal');
+ openModal('settingsModal');
+ renderCourseList();
+ renderAll();
+};
+
+function renderCourseList() {
+ var container = document.getElementById('courseListDisplay');
+ container.innerHTML = '';
+
+ courses.forEach(function(c) {
+ var div = document.createElement('div');
+ div.className = 'course-chip';
+ div.innerHTML =
+ '' + c.name + ' (diff: ' + c.difficulty + ', ' + c.deadlines.length + ' deadlines)' +
+ '';
+
+ div.querySelector('.remove-course').onclick = async function() {
+ await fetch('/api/courses/' + c.id, { method: 'DELETE' });
+ courses = courses.filter(function(x) { return x.id !== c.id; });
+ renderCourseList();
+ renderAll();
+ };
+
+ container.appendChild(div);
+ });
+}
+
+// --- logout ---
+document.getElementById('logoutBtn').onclick = async function() {
+ await fetch('/api/auth/logout', { method: 'POST' });
+ window.location.href = '/';
+};
+
+// --- reset ---
+document.getElementById('resetBtn').onclick = async function() {
+ if (!confirm('Wipe all data and start fresh?')) return;
+
+ // delete all courses (cascades deadlines on server)
+ for (var i = 0; i < courses.length; i++) {
+ await fetch('/api/courses/' + courses[i].id, { method: 'DELETE' });
+ }
+
+ courses = [];
+ completedQuests = [];
+ user.hp = 100;
+ user.xp = 0;
+ user.sem_start = '';
+ user.sem_end = '';
+
+ await fetch('/api/auth/settings', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ sem_start: '', sem_end: '', energy_type: 'morning' })
+ });
+
+ await fetch('/api/auth/stats', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ hp: 100, xp: 0 })
+ });
+
+ closeModal('settingsModal');
+ renderAll();
+};
+
+// ============ HELPERS ============
+
+function todayStr() {
+ var d = new Date();
+ return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
+}
+
+function pad(n) {
+ return n < 10 ? '0' + n : '' + n;
+}
+
+function prettyDate(str) {
+ var months = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];
+ var parts = str.split('-');
+ return months[parseInt(parts[1]) - 1] + ' ' + parseInt(parts[2]);
+}
+
+// ============ RENDER ALL ============
+
+function renderAll() {
+ recalcHP();
+ renderMap();
+ renderBosses();
+ renderQuests();
+ renderAchievements();
+ updateBars();
+}
diff --git a/public/login.html b/public/login.html
new file mode 100644
index 0000000..f2a137c
--- /dev/null
+++ b/public/login.html
@@ -0,0 +1,96 @@
+
+
+
+
+
+ Autopilot — Login
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
■ SIGN IN ■
+
WELCOME BACK! SIGN IN TO YOUR ACCOUNT.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/signup.html b/public/signup.html
new file mode 100644
index 0000000..3480c19
--- /dev/null
+++ b/public/signup.html
@@ -0,0 +1,102 @@
+
+
+
+
+
+ Autopilot — Sign Up
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
■ CREATE ACCOUNT ■
+
JOIN THE QUEST. BUILD YOUR CHARACTER.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/routes/auth.js b/routes/auth.js
new file mode 100644
index 0000000..c51d463
--- /dev/null
+++ b/routes/auth.js
@@ -0,0 +1,134 @@
+var express = require('express');
+var bcrypt = require('bcryptjs');
+var { getDb, persist } = require('../db');
+var router = express.Router();
+
+// sign up
+router.post('/signup', async function(req, res) {
+ var { email, username, password } = req.body;
+
+ if (!email || !username || !password) {
+ return res.status(400).json({ error: 'all fields required' });
+ }
+
+ if (password.length < 6) {
+ return res.status(400).json({ error: 'password must be at least 6 characters' });
+ }
+
+ var db = await getDb();
+
+ // check if user exists
+ var existing = db.exec('SELECT id FROM users WHERE email = ? OR username = ?', [email, username]);
+ if (existing.length > 0 && existing[0].values.length > 0) {
+ return res.status(400).json({ error: 'email or username already taken' });
+ }
+
+ var hash = bcrypt.hashSync(password, 10);
+ db.run('INSERT INTO users (email, username, password) VALUES (?, ?, ?)', [email, username, hash]);
+ persist();
+
+ // get the new user id
+ var result = db.exec('SELECT last_insert_rowid() as id');
+ var userId = result[0].values[0][0];
+
+ req.session.userId = userId;
+ req.session.username = username;
+
+ res.json({ ok: true, username: username });
+});
+
+// login
+router.post('/login', async function(req, res) {
+ var { email, password } = req.body;
+
+ if (!email || !password) {
+ return res.status(400).json({ error: 'email and password required' });
+ }
+
+ var db = await getDb();
+ var result = db.exec('SELECT * FROM users WHERE email = ?', [email]);
+
+ if (result.length === 0 || result[0].values.length === 0) {
+ return res.status(401).json({ error: 'invalid credentials' });
+ }
+
+ var cols = result[0].columns;
+ var row = result[0].values[0];
+ var user = {};
+ cols.forEach(function(c, i) { user[c] = row[i]; });
+
+ var match = bcrypt.compareSync(password, user.password);
+ if (!match) {
+ return res.status(401).json({ error: 'invalid credentials' });
+ }
+
+ req.session.userId = user.id;
+ req.session.username = user.username;
+
+ res.json({ ok: true, username: user.username });
+});
+
+// logout
+router.post('/logout', function(req, res) {
+ req.session.destroy();
+ res.json({ ok: true });
+});
+
+// get current user
+router.get('/me', async function(req, res) {
+ if (!req.session.userId) {
+ return res.status(401).json({ error: 'not logged in' });
+ }
+
+ var db = await getDb();
+ var result = db.exec(
+ 'SELECT id, email, username, sem_start, sem_end, energy_type, hp, xp FROM users WHERE id = ?',
+ [req.session.userId]
+ );
+
+ if (result.length === 0 || result[0].values.length === 0) {
+ return res.status(401).json({ error: 'user not found' });
+ }
+
+ var cols = result[0].columns;
+ var row = result[0].values[0];
+ var user = {};
+ cols.forEach(function(c, i) { user[c] = row[i]; });
+
+ res.json(user);
+});
+
+// update semester settings
+router.put('/settings', async function(req, res) {
+ if (!req.session.userId) {
+ return res.status(401).json({ error: 'not logged in' });
+ }
+
+ var { sem_start, sem_end, energy_type } = req.body;
+ var db = await getDb();
+ db.run('UPDATE users SET sem_start = ?, sem_end = ?, energy_type = ? WHERE id = ?',
+ [sem_start || '', sem_end || '', energy_type || 'morning', req.session.userId]);
+ persist();
+
+ res.json({ ok: true });
+});
+
+// update hp/xp
+router.put('/stats', async function(req, res) {
+ if (!req.session.userId) {
+ return res.status(401).json({ error: 'not logged in' });
+ }
+
+ var db = await getDb();
+ var { hp, xp } = req.body;
+ if (hp !== undefined) {
+ db.run('UPDATE users SET hp = ? WHERE id = ?', [hp, req.session.userId]);
+ }
+ if (xp !== undefined) {
+ db.run('UPDATE users SET xp = ? WHERE id = ?', [xp, req.session.userId]);
+ }
+ persist();
+ res.json({ ok: true });
+});
+
+module.exports = router;
diff --git a/routes/checkins.js b/routes/checkins.js
new file mode 100644
index 0000000..d7160b8
--- /dev/null
+++ b/routes/checkins.js
@@ -0,0 +1,49 @@
+var express = require('express');
+var { getDb, persist } = require('../db');
+var router = express.Router();
+
+function requireAuth(req, res, next) {
+ if (!req.session.userId) return res.status(401).json({ error: 'not logged in' });
+ next();
+}
+
+function toObjects(result) {
+ if (!result.length || !result[0].values.length) return [];
+ var cols = result[0].columns;
+ return result[0].values.map(function(row) {
+ var obj = {};
+ cols.forEach(function(c, i) { obj[c] = row[i]; });
+ return obj;
+ });
+}
+
+// get all check-ins
+router.get('/', requireAuth, async function(req, res) {
+ var db = await getDb();
+ var result = db.exec('SELECT * FROM checkins WHERE user_id = ? ORDER BY date DESC', [req.session.userId]);
+ res.json(toObjects(result));
+});
+
+// log a check-in
+router.post('/', requireAuth, async function(req, res) {
+ var { date, sleep, stress, exercise } = req.body;
+ if (!date) return res.status(400).json({ error: 'date required' });
+
+ var db = await getDb();
+ var existing = db.exec('SELECT id FROM checkins WHERE user_id = ? AND date = ?',
+ [req.session.userId, date]);
+ var rows = toObjects(existing);
+
+ if (rows.length) {
+ db.run('UPDATE checkins SET sleep = ?, stress = ?, exercise = ? WHERE id = ?',
+ [sleep || 3, stress || 3, exercise ? 1 : 0, rows[0].id]);
+ } else {
+ db.run('INSERT INTO checkins (user_id, date, sleep, stress, exercise) VALUES (?, ?, ?, ?, ?)',
+ [req.session.userId, date, sleep || 3, stress || 3, exercise ? 1 : 0]);
+ }
+
+ persist();
+ res.json({ ok: true });
+});
+
+module.exports = router;
diff --git a/routes/courses.js b/routes/courses.js
new file mode 100644
index 0000000..c3ddbaf
--- /dev/null
+++ b/routes/courses.js
@@ -0,0 +1,82 @@
+var express = require('express');
+var { getDb, persist } = require('../db');
+var router = express.Router();
+
+function requireAuth(req, res, next) {
+ if (!req.session.userId) return res.status(401).json({ error: 'not logged in' });
+ next();
+}
+
+// helper to turn sql.js result into array of objects
+function toObjects(result) {
+ if (!result.length || !result[0].values.length) return [];
+ var cols = result[0].columns;
+ return result[0].values.map(function(row) {
+ var obj = {};
+ cols.forEach(function(c, i) { obj[c] = row[i]; });
+ return obj;
+ });
+}
+
+// get all courses with deadlines
+router.get('/', requireAuth, async function(req, res) {
+ var db = await getDb();
+ var coursesResult = db.exec('SELECT * FROM courses WHERE user_id = ?', [req.session.userId]);
+ var courses = toObjects(coursesResult);
+
+ courses.forEach(function(c) {
+ var dlResult = db.exec('SELECT * FROM deadlines WHERE course_id = ? ORDER BY date', [c.id]);
+ c.deadlines = toObjects(dlResult);
+ });
+
+ res.json(courses);
+});
+
+// add a course
+router.post('/', requireAuth, async function(req, res) {
+ var { name, difficulty, deadlines } = req.body;
+ if (!name) return res.status(400).json({ error: 'course name required' });
+
+ var db = await getDb();
+ db.run('INSERT INTO courses (user_id, name, difficulty) VALUES (?, ?, ?)',
+ [req.session.userId, name, difficulty || 3]);
+
+ var idResult = db.exec('SELECT last_insert_rowid() as id');
+ var courseId = idResult[0].values[0][0];
+
+ if (deadlines && deadlines.length) {
+ deadlines.forEach(function(dl) {
+ db.run('INSERT INTO deadlines (course_id, label, date, type) VALUES (?, ?, ?, ?)',
+ [courseId, dl.label, dl.date, dl.type || 'assignment']);
+ });
+ }
+
+ persist();
+
+ // return created course
+ var courseResult = db.exec('SELECT * FROM courses WHERE id = ?', [courseId]);
+ var course = toObjects(courseResult)[0];
+ var dlResult = db.exec('SELECT * FROM deadlines WHERE course_id = ?', [courseId]);
+ course.deadlines = toObjects(dlResult);
+
+ res.json(course);
+});
+
+// delete a course
+router.delete('/:id', requireAuth, async function(req, res) {
+ var db = await getDb();
+ var courseResult = db.exec('SELECT * FROM courses WHERE id = ? AND user_id = ?',
+ [req.params.id, req.session.userId]);
+
+ if (!toObjects(courseResult).length) {
+ return res.status(404).json({ error: 'course not found' });
+ }
+
+ db.run('DELETE FROM deadlines WHERE course_id = ?', [req.params.id]);
+ db.run('DELETE FROM courses WHERE id = ?', [req.params.id]);
+ persist();
+
+ res.json({ ok: true });
+});
+
+module.exports = router;
diff --git a/routes/quests.js b/routes/quests.js
new file mode 100644
index 0000000..9e02916
--- /dev/null
+++ b/routes/quests.js
@@ -0,0 +1,80 @@
+var express = require('express');
+var { getDb, persist } = require('../db');
+var router = express.Router();
+
+function requireAuth(req, res, next) {
+ if (!req.session.userId) return res.status(401).json({ error: 'not logged in' });
+ next();
+}
+
+function toObjects(result) {
+ if (!result.length || !result[0].values.length) return [];
+ var cols = result[0].columns;
+ return result[0].values.map(function(row) {
+ var obj = {};
+ cols.forEach(function(c, i) { obj[c] = row[i]; });
+ return obj;
+ });
+}
+
+// get completed quests
+router.get('/completed', requireAuth, async function(req, res) {
+ var db = await getDb();
+ var result = db.exec('SELECT * FROM completed_quests WHERE user_id = ?', [req.session.userId]);
+ res.json(toObjects(result));
+});
+
+// mark quest done
+router.post('/complete', requireAuth, async function(req, res) {
+ var { quest_id } = req.body;
+ if (!quest_id) return res.status(400).json({ error: 'quest_id required' });
+
+ var db = await getDb();
+ var existing = db.exec('SELECT id FROM completed_quests WHERE user_id = ? AND quest_id = ?',
+ [req.session.userId, quest_id]);
+
+ if (!toObjects(existing).length) {
+ db.run('INSERT INTO completed_quests (user_id, quest_id) VALUES (?, ?)',
+ [req.session.userId, quest_id]);
+ persist();
+ }
+
+ res.json({ ok: true });
+});
+
+// uncomplete quest
+router.post('/uncomplete', requireAuth, async function(req, res) {
+ var { quest_id } = req.body;
+ var db = await getDb();
+ db.run('DELETE FROM completed_quests WHERE user_id = ? AND quest_id = ?',
+ [req.session.userId, quest_id]);
+ persist();
+ res.json({ ok: true });
+});
+
+// get achievements
+router.get('/achievements', requireAuth, async function(req, res) {
+ var db = await getDb();
+ var result = db.exec('SELECT achievement_id FROM achievements WHERE user_id = ?', [req.session.userId]);
+ var rows = toObjects(result);
+ res.json(rows.map(function(a) { return a.achievement_id; }));
+});
+
+// unlock achievement
+router.post('/achievements', requireAuth, async function(req, res) {
+ var { achievement_id } = req.body;
+ if (!achievement_id) return res.status(400).json({ error: 'achievement_id required' });
+
+ var db = await getDb();
+ try {
+ db.run('INSERT OR IGNORE INTO achievements (user_id, achievement_id) VALUES (?, ?)',
+ [req.session.userId, achievement_id]);
+ persist();
+ } catch(e) {
+ // already exists
+ }
+
+ res.json({ ok: true });
+});
+
+module.exports = router;
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..0ac05ba
--- /dev/null
+++ b/server.js
@@ -0,0 +1,61 @@
+var express = require('express');
+var session = require('express-session');
+var cookieParser = require('cookie-parser');
+var path = require('path');
+var { getDb } = require('./db');
+
+var app = express();
+var PORT = process.env.PORT || 3000;
+
+// middleware
+app.use(express.json());
+app.use(express.urlencoded({ extended: false }));
+app.use(cookieParser());
+app.use(session({
+ secret: 'autopilot-semester-rpg-2024',
+ resave: false,
+ saveUninitialized: false,
+ cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 }
+}));
+
+// static files
+app.use(express.static(path.join(__dirname, 'public')));
+
+// routes
+app.use('/api/auth', require('./routes/auth'));
+app.use('/api/courses', require('./routes/courses'));
+app.use('/api/checkins', require('./routes/checkins'));
+app.use('/api/quests', require('./routes/quests'));
+
+// page routes
+app.get('/login', function(req, res) {
+ res.sendFile(path.join(__dirname, 'public', 'login.html'));
+});
+
+app.get('/signup', function(req, res) {
+ res.sendFile(path.join(__dirname, 'public', 'signup.html'));
+});
+
+app.get('/dashboard', function(req, res) {
+ if (!req.session.userId) {
+ return res.redirect('/login');
+ }
+ res.sendFile(path.join(__dirname, 'public', 'dashboard.html'));
+});
+
+app.get('/', function(req, res) {
+ if (req.session.userId) {
+ return res.redirect('/dashboard');
+ }
+ res.sendFile(path.join(__dirname, 'public', 'index.html'));
+});
+
+// init db then start server
+getDb().then(function() {
+ app.listen(PORT, function() {
+ console.log('autopilot running on http://localhost:' + PORT);
+ });
+}).catch(function(err) {
+ console.error('failed to init database:', err);
+ process.exit(1);
+});
diff --git a/style.css b/style.css
deleted file mode 100644
index fd10231..0000000
--- a/style.css
+++ /dev/null
@@ -1,497 +0,0 @@
-@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Inter:wght@400;600;700&display=swap');
-
-* { margin: 0; padding: 0; box-sizing: border-box; }
-
-:root {
- --bg: #0a0a1a;
- --panel: #12122a;
- --border: #2a2a4a;
- --text: #e0e0f0;
- --muted: #6a6a8a;
- --hp: #e63946;
- --hp-glow: #ff1744;
- --xp: #06d6a0;
- --xp-glow: #00e676;
- --gold: #ffd166;
- --boss: #ef476f;
- --chill: #06d6a0;
- --moderate: #ffd166;
- --intense: #f77f00;
- --accent: #118ab2;
-}
-
-body {
- background: var(--bg);
- color: var(--text);
- font-family: 'Inter', sans-serif;
- min-height: 100vh;
-}
-
-/* ---- HUD header ---- */
-#hud {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12px 24px;
- background: var(--panel);
- border-bottom: 2px solid var(--border);
- position: sticky;
- top: 0;
- z-index: 50;
-}
-
-.hud-left {
- display: flex;
- align-items: center;
- gap: 16px;
-}
-
-.logo {
- font-family: 'Press Start 2P', monospace;
- font-size: 14px;
- color: var(--gold);
- letter-spacing: 2px;
-}
-
-.level-badge {
- background: var(--border);
- padding: 4px 10px;
- border-radius: 4px;
- font-size: 12px;
- font-family: 'Press Start 2P', monospace;
- color: var(--muted);
-}
-
-.hud-bars {
- display: flex;
- gap: 24px;
- flex: 1;
- max-width: 500px;
- margin: 0 32px;
-}
-
-.bar-group {
- flex: 1;
-}
-
-.bar-group label {
- font-size: 11px;
- font-family: 'Press Start 2P', monospace;
- display: block;
- margin-bottom: 4px;
-}
-
-.bar {
- height: 18px;
- background: #1a1a3a;
- border-radius: 3px;
- overflow: hidden;
- border: 1px solid var(--border);
-}
-
-.bar-fill {
- height: 100%;
- transition: width 0.6s ease;
- border-radius: 2px;
-}
-
-.hp-bar .bar-fill {
- background: linear-gradient(90deg, var(--hp), #ff6b6b);
- box-shadow: 0 0 8px var(--hp-glow);
-}
-
-.xp-bar .bar-fill {
- background: linear-gradient(90deg, var(--xp), #6bffb8);
- box-shadow: 0 0 8px var(--xp-glow);
-}
-
-.hud-right {
- display: flex;
- gap: 8px;
-}
-
-/* ---- buttons ---- */
-.btn-pixel {
- font-family: 'Press Start 2P', monospace;
- font-size: 10px;
- padding: 8px 14px;
- border: 2px solid var(--gold);
- background: transparent;
- color: var(--gold);
- cursor: pointer;
- transition: all 0.15s;
-}
-.btn-pixel:hover {
- background: var(--gold);
- color: var(--bg);
-}
-
-.btn-sm { font-size: 9px; padding: 5px 10px; }
-
-.btn-ghost {
- border-color: var(--muted);
- color: var(--muted);
-}
-.btn-ghost:hover {
- background: var(--muted);
- color: var(--bg);
-}
-
-.btn-danger {
- border-color: var(--hp);
- color: var(--hp);
-}
-.btn-danger:hover {
- background: var(--hp);
- color: white;
-}
-
-/* ---- main layout ---- */
-main {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 20px;
- padding: 20px 24px;
- max-width: 1400px;
- margin: 0 auto;
-}
-
-section {
- background: var(--panel);
- border: 1px solid var(--border);
- border-radius: 6px;
- padding: 20px;
-}
-
-section h2 {
- font-family: 'Press Start 2P', monospace;
- font-size: 12px;
- color: var(--gold);
- margin-bottom: 16px;
-}
-
-/* ---- semester map / week grid ---- */
-#weekGrid {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- gap: 8px;
-}
-
-.week-tile {
- aspect-ratio: 1;
- border-radius: 4px;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- font-size: 11px;
- cursor: pointer;
- border: 2px solid transparent;
- transition: all 0.2s;
- position: relative;
-}
-.week-tile:hover {
- transform: scale(1.08);
- border-color: var(--gold);
-}
-.week-tile.current {
- border-color: var(--gold);
- box-shadow: 0 0 12px rgba(255, 209, 102, 0.3);
-}
-
-.week-tile .week-num {
- font-family: 'Press Start 2P', monospace;
- font-size: 10px;
- opacity: 0.8;
-}
-
-.week-tile .week-label {
- font-size: 9px;
- margin-top: 2px;
- opacity: 0.6;
-}
-
-.week-tile.boss-week::after {
- content: '☠';
- position: absolute;
- top: 3px;
- right: 5px;
- font-size: 12px;
-}
-
-.week-tile.chill { background: rgba(6, 214, 160, 0.2); color: var(--chill); }
-.week-tile.moderate { background: rgba(255, 209, 102, 0.2); color: var(--moderate); }
-.week-tile.intense { background: rgba(247, 127, 0, 0.2); color: var(--intense); }
-.week-tile.boss { background: rgba(239, 71, 111, 0.25); color: var(--boss); }
-.week-tile.past { opacity: 0.4; }
-
-/* legend */
-#legendBar {
- display: flex;
- gap: 16px;
- margin-top: 12px;
- justify-content: center;
-}
-.legend-item { font-size: 11px; display: flex; align-items: center; gap: 4px; color: var(--muted); }
-.dot {
- width: 10px; height: 10px; border-radius: 2px;
-}
-.dot.chill { background: var(--chill); }
-.dot.moderate { background: var(--moderate); }
-.dot.intense { background: var(--intense); }
-.dot.boss { background: var(--boss); }
-
-/* ---- quests ---- */
-#questList, #bossList {
- list-style: none;
-}
-
-#questList li, #bossList li {
- padding: 10px 12px;
- margin-bottom: 6px;
- background: rgba(255,255,255,0.03);
- border-left: 3px solid var(--accent);
- border-radius: 0 4px 4px 0;
- display: flex;
- justify-content: space-between;
- align-items: center;
- font-size: 13px;
-}
-
-#bossList li {
- border-left-color: var(--boss);
-}
-
-#questList li.done {
- opacity: 0.4;
- text-decoration: line-through;
-}
-
-#questList li .quest-check {
- cursor: pointer;
- width: 20px;
- height: 20px;
- border: 2px solid var(--accent);
- border-radius: 3px;
- background: transparent;
- color: var(--xp);
- font-size: 12px;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
-}
-#questList li .quest-check:hover { background: var(--accent); }
-#questList li.done .quest-check { background: var(--xp); border-color: var(--xp); }
-
-.boss-date {
- font-size: 10px;
- color: var(--muted);
- font-family: 'Press Start 2P', monospace;
-}
-
-.muted { color: var(--muted); font-size: 12px; font-style: italic; }
-
-/* ---- achievements ---- */
-#achievementGrid {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- gap: 8px;
-}
-
-.achievement {
- text-align: center;
- padding: 10px 4px;
- border-radius: 4px;
- background: rgba(255,255,255,0.02);
- border: 1px solid var(--border);
- font-size: 9px;
- transition: all 0.2s;
-}
-
-.achievement .ach-icon {
- font-size: 22px;
- display: block;
- margin-bottom: 4px;
-}
-
-.achievement.locked {
- opacity: 0.3;
- filter: grayscale(1);
-}
-
-.achievement.unlocked {
- border-color: var(--gold);
- box-shadow: 0 0 8px rgba(255,209,102,0.2);
-}
-
-/* ---- modals ---- */
-.modal-backdrop {
- display: none;
- position: fixed;
- inset: 0;
- background: rgba(0,0,0,0.7);
- z-index: 100;
- align-items: center;
- justify-content: center;
-}
-.modal-backdrop.open {
- display: flex;
-}
-
-.modal {
- background: var(--panel);
- border: 2px solid var(--gold);
- border-radius: 6px;
- padding: 24px;
- width: 90%;
- max-width: 440px;
- max-height: 80vh;
- overflow-y: auto;
-}
-
-.modal h2 {
- font-family: 'Press Start 2P', monospace;
- font-size: 12px;
- color: var(--gold);
- margin-bottom: 16px;
-}
-
-.modal label {
- display: block;
- margin-bottom: 12px;
- font-size: 13px;
-}
-
-.modal input[type="text"],
-.modal input[type="date"],
-.modal select {
- display: block;
- width: 100%;
- margin-top: 4px;
- padding: 8px;
- background: var(--bg);
- border: 1px solid var(--border);
- border-radius: 4px;
- color: var(--text);
- font-size: 14px;
- font-family: inherit;
-}
-
-.modal input[type="range"] {
- width: 80%;
- margin-top: 4px;
- vertical-align: middle;
-}
-
-.modal-actions {
- display: flex;
- gap: 8px;
- margin-top: 16px;
-}
-
-.modal hr {
- border: none;
- border-top: 1px solid var(--border);
- margin: 16px 0;
-}
-
-.deadline-row {
- display: flex;
- gap: 8px;
- margin-bottom: 8px;
- align-items: center;
-}
-
-.deadline-row input[type="text"] {
- flex: 2;
- padding: 6px;
- background: var(--bg);
- border: 1px solid var(--border);
- border-radius: 4px;
- color: var(--text);
- font-size: 13px;
-}
-
-.deadline-row input[type="date"] {
- flex: 1;
- padding: 6px;
- background: var(--bg);
- border: 1px solid var(--border);
- border-radius: 4px;
- color: var(--text);
- font-size: 13px;
-}
-
-.deadline-row select {
- width: 90px;
- padding: 6px;
- background: var(--bg);
- border: 1px solid var(--border);
- border-radius: 4px;
- color: var(--text);
- font-size: 12px;
-}
-
-.deadline-row .remove-dl {
- background: none;
- border: none;
- color: var(--hp);
- cursor: pointer;
- font-size: 16px;
-}
-
-#courseListDisplay {
- margin-top: 12px;
-}
-
-.course-chip {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 8px 10px;
- margin-bottom: 6px;
- background: rgba(255,255,255,0.03);
- border-radius: 4px;
- font-size: 13px;
-}
-
-.course-chip .remove-course {
- background: none;
- border: none;
- color: var(--hp);
- cursor: pointer;
- font-size: 14px;
-}
-
-/* week tooltip on hover */
-.week-tooltip {
- display: none;
- position: absolute;
- bottom: calc(100% + 6px);
- left: 50%;
- transform: translateX(-50%);
- background: var(--bg);
- border: 1px solid var(--gold);
- padding: 8px 10px;
- border-radius: 4px;
- font-size: 11px;
- white-space: nowrap;
- z-index: 10;
- pointer-events: none;
-}
-.week-tile:hover .week-tooltip { display: block; }
-
-/* ---- responsive ---- */
-@media (max-width: 800px) {
- main { grid-template-columns: 1fr; }
- .hud-bars { display: none; }
- #hud { flex-wrap: wrap; gap: 8px; }
-}
-
-/* lil pulse for current week */
-@keyframes pulse {
- 0%, 100% { box-shadow: 0 0 12px rgba(255,209,102,0.3); }
- 50% { box-shadow: 0 0 20px rgba(255,209,102,0.6); }
-}
-.week-tile.current { animation: pulse 2s infinite; }