diff --git a/app.js b/app.js
new file mode 100644
index 0000000..322c1df
--- /dev/null
+++ b/app.js
@@ -0,0 +1,928 @@
+/* ── Data ────────────────────────────────────────────────── */
+const EXERCISES = [
+ // STRETCHES — release tight muscles
+ {
+ id: 'cat-cow',
+ name: 'Cat-Cow Stretch',
+ category: 'stretch',
+ emoji: '🐱',
+ muscle: 'Spine & Lower Back',
+ sets: 3,
+ reps: 10,
+ duration: null,
+ sides: false,
+ steps: [
+ 'Start on all fours with wrists under shoulders and knees under hips.',
+ 'Inhale — drop your belly toward the floor, lift your head and tailbone (cow).',
+ 'Exhale — round your spine toward the ceiling, tuck chin and pelvis (cat).',
+ 'Flow slowly and deliberately, matching breath to movement.'
+ ],
+ tip: 'Focus on tilting the pelvis posteriorly in the cat phase — that is exactly the movement that corrects APT.'
+ },
+ {
+ id: 'childs-pose',
+ name: "Child's Pose",
+ category: 'stretch',
+ emoji: '🧘',
+ muscle: 'Lower Back & Glutes',
+ sets: 3,
+ reps: null,
+ duration: 60,
+ sides: false,
+ steps: [
+ 'Kneel and sit back onto your heels, then fold forward and extend your arms overhead.',
+ 'Let your forehead rest on the floor and relax your entire back.',
+ 'Breathe deeply, allowing your lower back to lengthen on each exhale.',
+ 'Hold for the full duration without moving.'
+ ],
+ tip: 'Walk your hands further forward to feel a deeper stretch in the lumbar spine.'
+ },
+ {
+ id: 'hip-flexor-stretch',
+ name: 'Kneeling Hip Flexor Stretch',
+ category: 'stretch',
+ emoji: '🦵',
+ muscle: 'Hip Flexors (Psoas)',
+ sets: 3,
+ reps: null,
+ duration: 45,
+ sides: true,
+ steps: [
+ 'Kneel on one knee with the other foot forward at 90°.',
+ 'Tuck your pelvis slightly (posterior tilt) — this intensifies the stretch.',
+ 'Shift your hips gently forward until you feel a pull in the front of the kneeling hip.',
+ 'Keep your torso upright and core gently braced. Hold, then switch sides.'
+ ],
+ tip: 'The posterior pelvic tilt (PPT) during this stretch is critical. Without it, you compensate with the lower back and miss the psoas entirely.'
+ },
+ {
+ id: 'quad-stretch',
+ name: 'Standing Quad Stretch',
+ category: 'stretch',
+ emoji: '🏃',
+ muscle: 'Quadriceps & Rectus Femoris',
+ sets: 3,
+ reps: null,
+ duration: 30,
+ sides: true,
+ steps: [
+ 'Stand on one leg and bring the opposite heel toward your glute.',
+ 'Hold your ankle (not your toes) and keep knees together.',
+ 'Tuck your pelvis (posterior tilt) to deepen the rectus femoris stretch.',
+ 'Hold steady and switch sides.'
+ ],
+ tip: 'The rectus femoris crosses both the hip and knee. The PPT cue unlocks its upper portion, which standard quad stretches miss.'
+ },
+ {
+ id: 'pigeon-pose',
+ name: 'Pigeon Pose',
+ category: 'stretch',
+ emoji: '🕊️',
+ muscle: 'Hip Flexors & Piriformis',
+ sets: 2,
+ reps: null,
+ duration: 60,
+ sides: true,
+ steps: [
+ 'From all fours, bring one knee forward and place it behind the same-side wrist.',
+ 'Extend the opposite leg straight behind you with the top of the foot on the floor.',
+ 'Square your hips toward the floor as much as possible.',
+ 'Fold forward over the front leg, resting on forearms or a block. Hold, then switch.'
+ ],
+ tip: 'If your hip does not reach the floor, place a folded blanket under it to keep the pelvis level.'
+ },
+
+ // STRENGTHENING — activate weak muscles
+ {
+ id: 'ppt',
+ name: 'Posterior Pelvic Tilt',
+ category: 'strengthen',
+ emoji: '🎯',
+ muscle: 'Lower Abs & Glutes',
+ sets: 3,
+ reps: 15,
+ duration: null,
+ sides: false,
+ steps: [
+ 'Lie on your back with knees bent and feet flat on the floor.',
+ 'Flatten your lower back against the floor by contracting your lower abs and glutes simultaneously.',
+ 'Hold the tilt for 3 seconds, feeling your spine press into the ground.',
+ 'Release slowly and repeat — this is the exact correction movement for APT.'
+ ],
+ tip: 'Think of your pelvis as a bowl of water: APT spills it forward. PPT levels the bowl. Practice this movement awareness in sitting and standing throughout your day.'
+ },
+ {
+ id: 'glute-bridge',
+ name: 'Glute Bridge',
+ category: 'strengthen',
+ emoji: '🌉',
+ muscle: 'Glutes & Hamstrings',
+ sets: 3,
+ reps: 15,
+ duration: null,
+ sides: false,
+ steps: [
+ 'Lie on your back with knees bent, feet hip-width apart.',
+ 'Drive through your heels and squeeze your glutes to lift your hips.',
+ 'At the top, tuck your pelvis slightly to fully activate the glutes — do NOT arch your lower back.',
+ 'Lower slowly over 3 seconds and repeat.'
+ ],
+ tip: 'Squeeze the glutes hard at the top of each rep. Weak glutes are one of the primary drivers of APT.'
+ },
+ {
+ id: 'dead-bug',
+ name: 'Dead Bug',
+ category: 'strengthen',
+ emoji: '🐛',
+ muscle: 'Deep Core (TVA)',
+ sets: 3,
+ reps: 10,
+ duration: null,
+ sides: true,
+ steps: [
+ 'Lie on your back with arms pointing toward the ceiling and knees at 90° above hips.',
+ 'Press your lower back firmly into the floor (PPT) and hold it there throughout.',
+ 'Slowly lower your right arm and left leg toward the floor, breathing out.',
+ 'Return and alternate sides — if your back lifts off the floor, reduce range.'
+ ],
+ tip: 'Speed is the enemy here. Slow, controlled reps with the lower back glued to the floor are the goal.'
+ },
+ {
+ id: 'plank',
+ name: 'Forearm Plank',
+ category: 'strengthen',
+ emoji: '⚡',
+ muscle: 'Core & Glutes',
+ sets: 3,
+ reps: null,
+ duration: 30,
+ sides: false,
+ steps: [
+ 'Place forearms on the floor with elbows under shoulders, hands forward.',
+ 'Rise onto your toes, keeping the body in a straight line from head to heels.',
+ 'Actively tuck your pelvis (posterior tilt) and squeeze your glutes — do not let your hips sag or pike.',
+ 'Breathe steadily and hold for the full duration.'
+ ],
+ tip: 'Imagine pulling your elbows toward your feet without them moving — this activates your deep core far more than just holding position.'
+ },
+ {
+ id: 'reverse-crunch',
+ name: 'Reverse Crunch',
+ category: 'strengthen',
+ emoji: '🔄',
+ muscle: 'Lower Abs',
+ sets: 3,
+ reps: 15,
+ duration: null,
+ sides: false,
+ steps: [
+ 'Lie on your back with legs raised and knees at 90°, hands flat beside you.',
+ 'Use your lower abs to curl your hips off the floor, bringing knees toward your chest.',
+ 'At the top your hips and lower back should briefly peel off the floor.',
+ 'Lower slowly and controlled — do not let momentum take over.'
+ ],
+ tip: 'The lower abs are the key antagonist to the hip flexors in APT. Strengthening them directly counteracts the anterior pull.'
+ },
+ {
+ id: 'bird-dog',
+ name: 'Bird Dog',
+ category: 'strengthen',
+ emoji: '🐦',
+ muscle: 'Glutes & Core Stability',
+ sets: 3,
+ reps: 10,
+ duration: null,
+ sides: true,
+ steps: [
+ 'Start on all fours with a neutral spine (not arched or rounded).',
+ 'Extend your right arm and left leg simultaneously until they are parallel to the floor.',
+ 'Hold for 2 seconds, return, then switch to left arm and right leg.',
+ 'Keep your hips level — do not rotate or shift weight.'
+ ],
+ tip: 'If your lower back arches when you extend the leg, reduce range until strength improves.'
+ }
+];
+
+// Ordered daily routine (exercise IDs)
+const ROUTINE_ORDER = [
+ 'cat-cow',
+ 'childs-pose',
+ 'hip-flexor-stretch',
+ 'quad-stretch',
+ 'pigeon-pose',
+ 'ppt',
+ 'glute-bridge',
+ 'dead-bug',
+ 'plank',
+ 'reverse-crunch',
+ 'bird-dog'
+];
+
+const TIPS = [
+ 'When sitting, place a small lumbar pillow behind your lower back and consciously level your pelvis.',
+ 'Set an hourly reminder to check your posture — APT is mostly a habit of inattention.',
+ 'Avoid extended sitting. Stand and walk for at least 5 minutes every hour.',
+ 'When standing in line, gently activate your glutes. This alone resets pelvic alignment.',
+ 'Sleeping on your back with a pillow under your knees removes strain from your hip flexors overnight.',
+ 'Avoid excessive lower-back arching during gym exercises like squats and deadlifts.',
+ 'Building the habit of a daily routine is more valuable than any single perfect session.',
+ 'Consistency over 8–12 weeks produces lasting structural change. Do not rush the process.'
+];
+
+/* ── State & Storage ─────────────────────────────────────── */
+function loadState() {
+ try {
+ return JSON.parse(localStorage.getItem('tiltfix-state') || '{}');
+ } catch (_) {
+ return {};
+ }
+}
+
+function saveState(state) {
+ localStorage.setItem('tiltfix-state', JSON.stringify(state));
+}
+
+function todayKey() {
+ return new Date().toISOString().slice(0, 10);
+}
+
+/* ── Timer ───────────────────────────────────────────────── */
+let timerInterval = null;
+let timerSeconds = 0;
+let timerRunning = false;
+let currentExerciseId = null;
+let currentSet = 0;
+let completedSets = 0;
+
+function clearTimer() {
+ if (timerInterval !== null) {
+ clearInterval(timerInterval);
+ timerInterval = null;
+ }
+ timerRunning = false;
+}
+
+/* ── Navigation ──────────────────────────────────────────── */
+let currentView = 'home';
+
+function navigate(view) {
+ if (view === currentView) return;
+ clearTimer();
+ currentView = view;
+ document.querySelectorAll('.view').forEach((el) => el.classList.remove('active'));
+ document.querySelectorAll('.nav-item').forEach((el) => el.classList.remove('active'));
+
+ const viewEl = document.getElementById('view-' + view);
+ if (viewEl) viewEl.classList.add('active');
+ const navEl = document.querySelector('[data-view="' + view + '"]');
+ if (navEl) navEl.classList.add('active');
+
+ document.getElementById('app').scrollTop = 0;
+
+ renderView(view);
+}
+
+function renderView(view) {
+ if (view === 'home') renderHome();
+ if (view === 'exercises') renderExercises();
+ if (view === 'routine') renderRoutine();
+ if (view === 'progress') renderProgress();
+}
+
+/* ── HOME ─────────────────────────────────────────────────── */
+function renderHome() {
+ const state = loadState();
+ const today = todayKey();
+ const completedToday = state.completedToday || {};
+ const doneCount = Object.keys(completedToday[today] || {}).length;
+ const total = ROUTINE_ORDER.length;
+ const pct = Math.round((doneCount / total) * 100);
+
+ // Streak
+ const streak = calcStreak(state);
+
+ // Tip of the day (cycle by day-of-year)
+ const dayOfYear = Math.floor((Date.now() - new Date(new Date().getFullYear(), 0, 0)) / 86400000);
+ const tip = TIPS[dayOfYear % TIPS.length];
+
+ // Greeting
+ const hour = new Date().getHours();
+ const greeting = hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening';
+
+ const circumference = 2 * Math.PI * 30;
+ const offset = circumference - (pct / 100) * circumference;
+
+ document.getElementById('home-content').innerHTML = `
+
+
${greeting} 👋
+
Fix Your Anterior Pelvic Tilt
+
+
+
+
+
+
+
+
+ ${pct}%
+ DONE
+
+
+
+
${doneCount} / ${total}
+
exercises done
+
Today's routine
+
+
+
+
+
+
+
+ ${doneCount === 0 ? 'Start Today\'s Routine' : doneCount === total ? 'View Completed Routine' : 'Continue Routine'}
+
+
+
+
+
Your streak
+
+
🔥
+
+
${streak} day${streak !== 1 ? 's' : ''}
+
Current streak
+
${streak === 0 ? 'Complete today\'s routine to start your streak!' : 'Keep it up — consistency is everything.'}
+
+
+
+
+
+
+
+
What is Anterior Pelvic Tilt?
+
+
The Postural Imbalance
+
Anterior pelvic tilt (APT) is a postural dysfunction where the front of the pelvis drops and the back rises, creating an exaggerated lower-back arch. It is caused by a muscle imbalance between tight and weak groups.
+
+
+
Tight → Stretch
+
+ Hip Flexors
+ Quadriceps
+ Lower Back
+
+
+
+
Weak → Strengthen
+
+ Glutes
+ Hamstrings
+ Core / Abs
+
+
+
+
+
+ `;
+}
+
+/* ── EXERCISES ────────────────────────────────────────────── */
+let exerciseFilter = 'all';
+
+function renderExercises() {
+ const filtered = exerciseFilter === 'all'
+ ? EXERCISES
+ : EXERCISES.filter((e) => e.category === exerciseFilter);
+
+ document.getElementById('exercises-content').innerHTML = `
+
+ All
+
+
+ Stretch & Release
+
+
+
+ Strengthen & Activate
+
+
+
+ ${filtered.map((ex) => exerciseCardHTML(ex)).join('')}
+
+ `;
+}
+
+function setFilter(f) {
+ exerciseFilter = f;
+ renderExercises();
+}
+
+function exerciseCardHTML(ex) {
+ const stat1 = ex.duration
+ ? ``
+ : `${ex.reps}
${ex.sides ? 'Reps/Side' : 'Reps'}
`;
+
+ return `
+
+
+
+
+
+ ${stat1}
+ ${ex.sides ? '
' : ''}
+
+
+ ${ex.steps.map((s, i) => `${i + 1} ${s} `).join('')}
+
+
+ 💡
+ ${ex.tip}
+
+
+
+ `;
+}
+
+function toggleCard(id) {
+ const card = document.getElementById('ec-' + id);
+ if (card) card.classList.toggle('open');
+}
+
+/* ── ROUTINE ──────────────────────────────────────────────── */
+function renderRoutine() {
+ const state = loadState();
+ const today = todayKey();
+ const completedToday = (state.completedToday || {})[today] || {};
+ const doneCount = Object.keys(completedToday).length;
+ const allDone = doneCount === ROUTINE_ORDER.length;
+
+ if (allDone) {
+ renderRoutineComplete(state);
+ return;
+ }
+
+ // Find first not-done exercise as active
+ const activeId = currentExerciseId
+ || ROUTINE_ORDER.find((id) => !completedToday[id])
+ || null;
+ if (!currentExerciseId) currentExerciseId = activeId;
+
+ const activeEx = activeId ? EXERCISES.find((e) => e.id === activeId) : null;
+
+ document.getElementById('routine-content').innerHTML = `
+
+ ${activeEx ? timerCardHTML(activeEx) : ''}
+
+ ${ROUTINE_ORDER.map((id, idx) => routineItemHTML(id, idx, completedToday, activeId)).join('')}
+
+
+ ↩ Reset Today's Progress
+
+ `;
+
+ if (activeEx && activeEx.duration) {
+ initTimer(activeEx);
+ }
+}
+
+function routineItemHTML(id, idx, completedToday, activeId) {
+ const ex = EXERCISES.find((e) => e.id === id);
+ if (!ex) return '';
+ const done = !!completedToday[id];
+ const isActive = id === activeId && !done;
+ const detail = ex.duration
+ ? `${ex.sets} sets × ${ex.duration}s${ex.sides ? ' per side' : ''}`
+ : `${ex.sets} sets × ${ex.reps} reps${ex.sides ? ' per side' : ''}`;
+
+ return `
+
+
${done ? '✓' : idx + 1}
+
+
${ex.emoji} ${ex.name}
+
${detail}
+
+
${done ? '✅' : isActive ? '▶' : ''}
+
+ `;
+}
+
+function selectRoutineExercise(id) {
+ const state = loadState();
+ const today = todayKey();
+ const completedToday = (state.completedToday || {})[today] || {};
+ if (completedToday[id]) return; // already done
+
+ clearTimer();
+ currentExerciseId = id;
+ completedSets = 0;
+ currentSet = 1;
+ renderRoutine();
+}
+
+function timerCardHTML(ex) {
+ if (ex.duration) {
+ const display = formatTime(ex.duration);
+ const detail = `${ex.sets} sets · ${ex.duration}s${ex.sides ? ' each side' : ''}`;
+ return `
+
+
${ex.emoji} ${ex.name}
+
${detail}
+
${display}
+
Set ${completedSets + 1} of ${ex.sets}
+
+ ${Array.from({ length: ex.sets }, (_, i) => `
+
+ `).join('')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
✓
+
+
+ `;
+ } else {
+ const detail = `${ex.sets} sets · ${ex.reps} reps${ex.sides ? ' each side' : ''}`;
+ return `
+
+
${ex.emoji} ${ex.name}
+
${detail}
+
${ex.reps}
+
reps${ex.sides ? ' each side' : ''}
+
+ ${Array.from({ length: ex.sets }, (_, i) => `
+
+ `).join('')}
+
+
Set ${completedSets + 1} of ${ex.sets}
+
+
+ Set Done ✓
+
+
+
+ `;
+ }
+}
+
+function initTimer(ex) {
+ timerSeconds = ex.duration;
+ updateTimerDisplay();
+}
+
+function toggleTimer() {
+ if (timerRunning) {
+ pauseTimer();
+ } else {
+ startTimer();
+ }
+}
+
+function startTimer() {
+ timerRunning = true;
+ const btn = document.getElementById('play-pause-btn');
+ if (btn) btn.innerHTML = `
+
+
+
+ `;
+
+ timerInterval = setInterval(() => {
+ if (timerSeconds > 0) {
+ timerSeconds--;
+ updateTimerDisplay();
+ } else {
+ clearTimer();
+ const ex = EXERCISES.find((e) => e.id === currentExerciseId);
+ if (ex) markSetDone(ex.id);
+ }
+ }, 1000);
+}
+
+function pauseTimer() {
+ clearTimer();
+ const btn = document.getElementById('play-pause-btn');
+ if (btn) btn.innerHTML = `
+
+
+ `;
+}
+
+function resetCurrentTimer() {
+ clearTimer();
+ const ex = EXERCISES.find((e) => e.id === currentExerciseId);
+ if (ex && ex.duration) {
+ timerSeconds = ex.duration;
+ updateTimerDisplay();
+ }
+ const btn = document.getElementById('play-pause-btn');
+ if (btn) btn.innerHTML = `
+
+
+ `;
+}
+
+function updateTimerDisplay() {
+ const el = document.getElementById('timer-display');
+ if (el) el.textContent = formatTime(timerSeconds);
+}
+
+function formatTime(secs) {
+ const m = Math.floor(secs / 60);
+ const s = secs % 60;
+ return m > 0 ? `${m}:${String(s).padStart(2, '0')}` : `${s}`;
+}
+
+function markSetDone(exerciseId) {
+ clearTimer();
+ const ex = EXERCISES.find((e) => e.id === exerciseId);
+ if (!ex) return;
+
+ completedSets++;
+
+ if (completedSets >= ex.sets) {
+ // Exercise fully complete
+ const state = loadState();
+ const today = todayKey();
+ if (!state.completedToday) state.completedToday = {};
+ if (!state.completedToday[today]) state.completedToday[today] = {};
+ state.completedToday[today][exerciseId] = true;
+
+ // Advance streak
+ state.lastCompleted = today;
+ saveState(state);
+
+ // Move to next exercise
+ const remaining = ROUTINE_ORDER.filter((id) => !state.completedToday[today][id]);
+ currentExerciseId = remaining.length > 0 ? remaining[0] : null;
+ completedSets = 0;
+ currentSet = 1;
+
+ renderRoutine();
+ } else {
+ // Next set
+ timerSeconds = ex.duration || 0;
+ renderRoutine();
+ }
+}
+
+function resetRoutine() {
+ const state = loadState();
+ const today = todayKey();
+ if (state.completedToday) delete state.completedToday[today];
+ saveState(state);
+ clearTimer();
+ currentExerciseId = null;
+ completedSets = 0;
+ renderRoutine();
+}
+
+function renderRoutineComplete(state) {
+ document.getElementById('routine-content').innerHTML = `
+
+
🎉
+
+
Routine Complete!
+
Great work. Your body thanks you. Come back tomorrow to keep your streak alive.
+
+
+
+ ${ROUTINE_ORDER.map((id, idx) => routineItemHTML(id, idx, (state.completedToday || {})[todayKey()] || {}, null)).join('')}
+
+
+ ↩ Reset Today's Progress
+
+ `;
+}
+
+/* ── PROGRESS ─────────────────────────────────────────────── */
+function renderProgress() {
+ const state = loadState();
+ const streak = calcStreak(state);
+ const sessions = totalSessions(state);
+ const daysTracked = daysSinceStart(state);
+ const weekData = getWeekData(state);
+
+ document.getElementById('progress-content').innerHTML = `
+
+
Overview
+
+
+
🔥
+
${streak}
+
Day Streak
+
+
+
✅
+
${sessions}
+
Sessions Done
+
+
+
📅
+
${daysTracked}
+
Days Tracked
+
+
+
💪
+
${sessions > 0 ? Math.min(Math.round((sessions / Math.max(daysTracked, 1)) * 100), 100) : 0}%
+
Consistency
+
+
+
+
+
+
This Week
+
+
+ ${weekData.map((d) => `
+
+
${d.label}
+
${d.date}
+
+ `).join('')}
+
+
+
+
+
+
Lifestyle Tips
+
+
+
🪑
+
+
Sitting Posture
+
Sit with feet flat, knees at 90°, and a small lumbar support. Avoid letting your pelvis tuck under (slumping) or arch excessively.
+
+
+
+
🚶
+
+
Walking Gait
+
Push off with your toes and drive your hip through on each stride. Lazy hip extension is a sign of weak glutes contributing to APT.
+
+
+
+
🏋️
+
+
Gym Exercises
+
Avoid over-arching in squats and deadlifts. Cue "ribs down, pelvis neutral" to protect the lower back and build correct patterns.
+
+
+
+
😴
+
+
Sleep Position
+
Back sleepers: put a pillow under your knees. Side sleepers: put a pillow between your knees. Both positions reduce overnight hip flexor shortening.
+
+
+
+
⏱️
+
+
Timeline
+
With daily practice, most people notice meaningful improvement in 6–8 weeks and significant correction by 12 weeks. Consistency beats intensity.
+
+
+
+
+ `;
+}
+
+/* ── Helpers ─────────────────────────────────────────────── */
+function calcStreak(state) {
+ const completedToday = state.completedToday || {};
+ let streak = 0;
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+
+ for (let i = 0; i < 365; i++) {
+ const d = new Date(today);
+ d.setDate(today.getDate() - i);
+ const key = d.toISOString().slice(0, 10);
+ const done = completedToday[key];
+ if (done && Object.keys(done).length === ROUTINE_ORDER.length) {
+ streak++;
+ } else {
+ break;
+ }
+ }
+ return streak;
+}
+
+function totalSessions(state) {
+ const completedToday = state.completedToday || {};
+ return Object.values(completedToday).filter(
+ (d) => d && Object.keys(d).length === ROUTINE_ORDER.length
+ ).length;
+}
+
+function daysSinceStart(state) {
+ const completedToday = state.completedToday || {};
+ const dates = Object.keys(completedToday).sort();
+ if (dates.length === 0) return 0;
+ const first = new Date(dates[0]);
+ const now = new Date();
+ return Math.floor((now - first) / 86400000) + 1;
+}
+
+function getWeekData(state) {
+ const completedToday = state.completedToday || {};
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ // Get Mon–Sun of current week
+ const dayOfWeek = today.getDay(); // 0=Sun
+ const monday = new Date(today);
+ monday.setDate(today.getDate() - ((dayOfWeek + 6) % 7));
+
+ return Array.from({ length: 7 }, (_, i) => {
+ const d = new Date(monday);
+ d.setDate(monday.getDate() + i);
+ const key = d.toISOString().slice(0, 10);
+ const isToday = d.getTime() === today.getTime();
+ const done = completedToday[key]
+ && Object.keys(completedToday[key]).length === ROUTINE_ORDER.length;
+ return {
+ label: ['M', 'T', 'W', 'T', 'F', 'S', 'S'][i],
+ date: d.getDate(),
+ done,
+ today: isToday
+ };
+ });
+}
+
+/* ── PWA Install ─────────────────────────────────────────── */
+let deferredInstallPrompt = null;
+
+window.addEventListener('beforeinstallprompt', (e) => {
+ e.preventDefault();
+ deferredInstallPrompt = e;
+ const banner = document.getElementById('install-banner');
+ if (banner) banner.classList.add('show');
+});
+
+function installApp() {
+ if (!deferredInstallPrompt) return;
+ deferredInstallPrompt.prompt();
+ deferredInstallPrompt.userChoice.then(() => {
+ deferredInstallPrompt = null;
+ dismissInstall();
+ });
+}
+
+function dismissInstall() {
+ const banner = document.getElementById('install-banner');
+ if (banner) banner.classList.remove('show');
+}
+
+/* ── Boot ─────────────────────────────────────────────────── */
+document.addEventListener('DOMContentLoaded', () => {
+ // Register service worker
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.register('/sw.js').catch(() => {});
+ }
+
+ // Wire up navigation
+ document.querySelectorAll('.nav-item').forEach((btn) => {
+ btn.addEventListener('click', () => navigate(btn.dataset.view));
+ });
+
+ // Initial render
+ renderView('home');
+});
diff --git a/icons/icon-maskable.svg b/icons/icon-maskable.svg
new file mode 100644
index 0000000..fb58555
--- /dev/null
+++ b/icons/icon-maskable.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/icons/icon.svg b/icons/icon.svg
new file mode 100644
index 0000000..612dc1d
--- /dev/null
+++ b/icons/icon.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..74932db
--- /dev/null
+++ b/index.html
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TiltFix — Fix Anterior Pelvic Tilt
+
+
+
+
+
+
+
📲 Add TiltFix to your Home Screen
+
Use it offline, anytime, like a native app.
+
+
+ Install
+ Not now
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
+
+
+
+ Exercises
+
+
+
+
+
+
+
+ Routine
+
+
+
+
+
+
+
+
+ Progress
+
+
+
+
+
+
+
diff --git a/manifest.json b/manifest.json
new file mode 100644
index 0000000..127daeb
--- /dev/null
+++ b/manifest.json
@@ -0,0 +1,24 @@
+{
+ "name": "TiltFix — Anterior Pelvic Tilt",
+ "short_name": "TiltFix",
+ "description": "Fix your anterior pelvic tilt with guided stretches and strengthening exercises.",
+ "start_url": "/",
+ "display": "standalone",
+ "background_color": "#F4F7F5",
+ "theme_color": "#2F7A58",
+ "orientation": "portrait-primary",
+ "icons": [
+ {
+ "src": "/icons/icon.svg",
+ "sizes": "any",
+ "type": "image/svg+xml",
+ "purpose": "any"
+ },
+ {
+ "src": "/icons/icon-maskable.svg",
+ "sizes": "any",
+ "type": "image/svg+xml",
+ "purpose": "maskable"
+ }
+ ]
+}
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..4657c6a
--- /dev/null
+++ b/style.css
@@ -0,0 +1,875 @@
+/* ── Reset & Base ─────────────────────────────────────────── */
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+:root {
+ --clr-bg: #F4F7F5;
+ --clr-surface: #FFFFFF;
+ --clr-surface-2: #F0F5F2;
+ --clr-primary: #2F7A58;
+ --clr-primary-lt: #E4F2EB;
+ --clr-primary-dk: #1E5740;
+ --clr-accent: #F0A500;
+ --clr-accent-lt: #FFF3D6;
+ --clr-danger: #E05252;
+ --clr-danger-lt: #FDEAEA;
+ --clr-text: #111827;
+ --clr-text-2: #4B5563;
+ --clr-text-3: #9CA3AF;
+ --clr-border: #E5EBE8;
+ --clr-stretch: #4A7FB5;
+ --clr-stretch-lt: #EBF2FA;
+ --clr-strengthen: #2F7A58;
+ --clr-strengthen-lt:#E4F2EB;
+
+ --radius-sm: 8px;
+ --radius-md: 14px;
+ --radius-lg: 20px;
+ --radius-xl: 28px;
+
+ --shadow-sm: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
+ --shadow-md: 0 4px 16px rgba(0,0,0,.08);
+ --shadow-lg: 0 8px 32px rgba(0,0,0,.10);
+
+ --nav-h: 72px;
+ --header-h: 64px;
+ --safe-bottom: env(safe-area-inset-bottom, 0px);
+
+ --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
+ --transition: 200ms cubic-bezier(.4,0,.2,1);
+}
+
+html { height: 100%; }
+body {
+ font-family: var(--font);
+ background: var(--clr-bg);
+ color: var(--clr-text);
+ height: 100%;
+ overflow: hidden;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+#app {
+ position: fixed;
+ inset: 0 0 calc(var(--nav-h) + var(--safe-bottom)) 0;
+ overflow-y: auto;
+ scroll-behavior: smooth;
+ -webkit-overflow-scrolling: touch;
+}
+
+/* ── Bottom Navigation ────────────────────────────────────── */
+#bottom-nav {
+ position: fixed;
+ bottom: 0;
+ left: 0; right: 0;
+ height: calc(var(--nav-h) + var(--safe-bottom));
+ padding-bottom: var(--safe-bottom);
+ background: var(--clr-surface);
+ border-top: 1px solid var(--clr-border);
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-around;
+ z-index: 100;
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+}
+
+.nav-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ padding: 12px 20px 0;
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: var(--clr-text-3);
+ transition: color var(--transition);
+ flex: 1;
+ min-width: 0;
+}
+
+.nav-item.active {
+ color: var(--clr-primary);
+}
+
+.nav-item svg {
+ width: 24px; height: 24px;
+ flex-shrink: 0;
+}
+
+.nav-label {
+ font-size: 10px;
+ font-weight: 600;
+ letter-spacing: .3px;
+ white-space: nowrap;
+}
+
+.nav-indicator {
+ position: absolute;
+ bottom: calc(var(--nav-h) + var(--safe-bottom) - 3px);
+ width: 4px; height: 4px;
+ border-radius: 50%;
+ background: var(--clr-primary);
+ transition: transform var(--transition), opacity var(--transition);
+ opacity: 0;
+}
+
+.nav-item.active .nav-indicator { opacity: 1; }
+
+/* ── Views ────────────────────────────────────────────────── */
+.view {
+ display: none;
+ min-height: 100%;
+ padding-bottom: 24px;
+ animation: fadeUp 260ms cubic-bezier(.4,0,.2,1) both;
+}
+
+.view.active { display: block; }
+
+@keyframes fadeUp {
+ from { opacity: 0; transform: translateY(12px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* ── Page Header ──────────────────────────────────────────── */
+.page-header {
+ padding: 56px 20px 20px;
+ background: var(--clr-surface);
+ border-bottom: 1px solid var(--clr-border);
+}
+
+.page-header h1 {
+ font-size: 28px;
+ font-weight: 700;
+ letter-spacing: -.5px;
+ line-height: 1.2;
+}
+
+.page-header p {
+ font-size: 14px;
+ color: var(--clr-text-2);
+ margin-top: 4px;
+}
+
+/* ── Cards ────────────────────────────────────────────────── */
+.card {
+ background: var(--clr-surface);
+ border-radius: var(--radius-lg);
+ padding: 20px;
+ box-shadow: var(--shadow-sm);
+ border: 1px solid var(--clr-border);
+}
+
+.section {
+ padding: 20px 20px 0;
+}
+
+.section-title {
+ font-size: 13px;
+ font-weight: 700;
+ letter-spacing: .8px;
+ text-transform: uppercase;
+ color: var(--clr-text-3);
+ margin-bottom: 12px;
+}
+
+/* ── HOME VIEW ────────────────────────────────────────────── */
+.home-hero {
+ background: linear-gradient(135deg, var(--clr-primary-dk) 0%, var(--clr-primary) 100%);
+ padding: 52px 24px 32px;
+ color: white;
+}
+
+.home-greeting {
+ font-size: 14px;
+ font-weight: 500;
+ opacity: .8;
+ margin-bottom: 4px;
+}
+
+.home-title {
+ font-size: 26px;
+ font-weight: 700;
+ letter-spacing: -.4px;
+ line-height: 1.25;
+ margin-bottom: 24px;
+}
+
+/* Progress ring */
+.progress-ring-wrap {
+ display: flex;
+ align-items: center;
+ gap: 20px;
+ background: rgba(255,255,255,.12);
+ border-radius: var(--radius-md);
+ padding: 16px 20px;
+ backdrop-filter: blur(8px);
+}
+
+.ring-container {
+ position: relative;
+ width: 72px; height: 72px;
+ flex-shrink: 0;
+}
+
+.ring-svg { transform: rotate(-90deg); }
+.ring-track { fill: none; stroke: rgba(255,255,255,.2); stroke-width: 6; }
+.ring-fill {
+ fill: none;
+ stroke: white;
+ stroke-width: 6;
+ stroke-linecap: round;
+ transition: stroke-dashoffset .6s cubic-bezier(.4,0,.2,1);
+}
+
+.ring-text {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ color: white;
+}
+
+.ring-pct { font-size: 16px; font-weight: 700; line-height: 1; }
+.ring-lbl { font-size: 9px; font-weight: 600; opacity: .75; margin-top: 2px; }
+
+.ring-info { color: white; }
+.ring-info .done { font-size: 24px; font-weight: 700; letter-spacing: -.4px; }
+.ring-info .total { font-size: 14px; opacity: .7; margin-top: 1px; }
+.ring-info .label { font-size: 12px; font-weight: 500; opacity: .6; margin-top: 2px; }
+
+.btn-start {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ width: 100%;
+ margin-top: 20px;
+ padding: 16px;
+ background: white;
+ color: var(--clr-primary-dk);
+ border: none;
+ border-radius: var(--radius-md);
+ font-family: var(--font);
+ font-size: 16px;
+ font-weight: 700;
+ cursor: pointer;
+ transition: transform var(--transition), box-shadow var(--transition);
+ box-shadow: 0 4px 20px rgba(0,0,0,.15);
+}
+
+.btn-start:active { transform: scale(.98); }
+.btn-start svg { width: 20px; height: 20px; }
+
+/* Streak */
+.streak-card {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.streak-icon {
+ font-size: 36px;
+ line-height: 1;
+}
+
+.streak-info .streak-count {
+ font-size: 28px;
+ font-weight: 800;
+ color: var(--clr-accent);
+ letter-spacing: -.5px;
+ line-height: 1;
+}
+
+.streak-info .streak-label {
+ font-size: 13px;
+ color: var(--clr-text-2);
+ margin-top: 2px;
+}
+
+.streak-info .streak-sub {
+ font-size: 12px;
+ color: var(--clr-text-3);
+ margin-top: 2px;
+}
+
+/* APT Info */
+.info-card {}
+.info-card h3 { font-size: 16px; font-weight: 700; margin-bottom: 10px; }
+.info-card p { font-size: 14px; color: var(--clr-text-2); line-height: 1.6; }
+
+.apt-diagram {
+ display: flex;
+ gap: 12px;
+ margin-top: 16px;
+}
+
+.apt-col {
+ flex: 1;
+ background: var(--clr-surface-2);
+ border-radius: var(--radius-md);
+ padding: 14px 12px;
+ text-align: center;
+}
+
+.apt-col .apt-col-title {
+ font-size: 11px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: .5px;
+ margin-bottom: 10px;
+}
+
+.apt-col.tight .apt-col-title { color: var(--clr-danger); }
+.apt-col.weak .apt-col-title { color: var(--clr-stretch); }
+
+.apt-col ul {
+ list-style: none;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.apt-col ul li {
+ font-size: 12px;
+ color: var(--clr-text-2);
+ background: var(--clr-surface);
+ border-radius: var(--radius-sm);
+ padding: 5px 8px;
+}
+
+/* Tip */
+.tip-card {
+ display: flex;
+ gap: 14px;
+ align-items: flex-start;
+ background: var(--clr-accent-lt);
+ border-color: transparent;
+}
+
+.tip-icon { font-size: 24px; flex-shrink: 0; margin-top: 1px; }
+.tip-card h4 { font-size: 13px; font-weight: 700; color: var(--clr-accent); margin-bottom: 4px; }
+.tip-card p { font-size: 13px; color: var(--clr-text-2); line-height: 1.5; }
+
+/* ── EXERCISES VIEW ───────────────────────────────────────── */
+.filter-bar {
+ display: flex;
+ gap: 8px;
+ padding: 16px 20px;
+ overflow-x: auto;
+ scrollbar-width: none;
+}
+.filter-bar::-webkit-scrollbar { display: none; }
+
+.filter-chip {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 7px 14px;
+ border-radius: 999px;
+ border: 1.5px solid var(--clr-border);
+ background: var(--clr-surface);
+ font-family: var(--font);
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--clr-text-2);
+ cursor: pointer;
+ white-space: nowrap;
+ transition: all var(--transition);
+}
+.filter-chip.active {
+ background: var(--clr-primary);
+ border-color: var(--clr-primary);
+ color: white;
+}
+.filter-chip .dot {
+ width: 7px; height: 7px;
+ border-radius: 50%;
+}
+
+.exercises-list {
+ padding: 0 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.exercise-card {
+ background: var(--clr-surface);
+ border-radius: var(--radius-lg);
+ border: 1px solid var(--clr-border);
+ overflow: hidden;
+ box-shadow: var(--shadow-sm);
+ transition: box-shadow var(--transition);
+}
+
+.exercise-card-header {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ padding: 16px 18px;
+ cursor: pointer;
+ user-select: none;
+}
+
+.exercise-icon {
+ width: 44px; height: 44px;
+ border-radius: var(--radius-md);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 22px;
+ flex-shrink: 0;
+}
+
+.exercise-icon.stretch { background: var(--clr-stretch-lt); }
+.exercise-icon.strengthen { background: var(--clr-strengthen-lt); }
+
+.exercise-meta { flex: 1; min-width: 0; }
+.exercise-name { font-size: 15px; font-weight: 700; margin-bottom: 3px; }
+.exercise-tags { display: flex; gap: 6px; flex-wrap: wrap; }
+
+.tag {
+ font-size: 11px;
+ font-weight: 600;
+ padding: 2px 8px;
+ border-radius: 999px;
+}
+.tag.stretch { background: var(--clr-stretch-lt); color: var(--clr-stretch); }
+.tag.strengthen { background: var(--clr-strengthen-lt); color: var(--clr-strengthen); }
+.tag.muscle { background: var(--clr-surface-2); color: var(--clr-text-2); }
+
+.exercise-chevron {
+ color: var(--clr-text-3);
+ transition: transform var(--transition);
+ flex-shrink: 0;
+}
+.exercise-card.open .exercise-chevron { transform: rotate(180deg); }
+
+.exercise-body {
+ display: none;
+ padding: 0 18px 18px;
+ border-top: 1px solid var(--clr-border);
+}
+.exercise-card.open .exercise-body { display: block; }
+
+.exercise-stats {
+ display: flex;
+ gap: 10px;
+ padding: 14px 0 16px;
+}
+
+.stat-pill {
+ flex: 1;
+ background: var(--clr-surface-2);
+ border-radius: var(--radius-md);
+ padding: 10px 8px;
+ text-align: center;
+}
+.stat-pill .sv { font-size: 16px; font-weight: 800; color: var(--clr-primary); }
+.stat-pill .sl { font-size: 11px; color: var(--clr-text-3); font-weight: 600; margin-top: 1px; }
+
+.exercise-steps { list-style: none; display: flex; flex-direction: column; gap: 8px; }
+.exercise-steps li {
+ display: flex;
+ gap: 12px;
+ align-items: flex-start;
+ font-size: 13px;
+ color: var(--clr-text-2);
+ line-height: 1.5;
+}
+.step-num {
+ width: 22px; height: 22px;
+ border-radius: 50%;
+ background: var(--clr-primary);
+ color: white;
+ font-size: 11px;
+ font-weight: 700;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ margin-top: 1px;
+}
+
+.exercise-tip {
+ display: flex;
+ gap: 8px;
+ margin-top: 14px;
+ padding: 12px;
+ background: var(--clr-accent-lt);
+ border-radius: var(--radius-md);
+ font-size: 12px;
+ color: var(--clr-text-2);
+ line-height: 1.5;
+}
+.exercise-tip span:first-child { flex-shrink: 0; }
+
+/* ── ROUTINE VIEW ─────────────────────────────────────────── */
+.routine-header-card {
+ margin: 16px 20px 0;
+ background: linear-gradient(135deg, var(--clr-primary-dk), var(--clr-primary));
+ color: white;
+ border-color: transparent;
+}
+
+.routine-header-card h2 { font-size: 20px; font-weight: 700; margin-bottom: 4px; }
+.routine-header-card p { font-size: 13px; opacity: .75; }
+
+.routine-complete-banner {
+ margin: 16px 20px 0;
+ padding: 20px;
+ background: var(--clr-primary-lt);
+ border-radius: var(--radius-lg);
+ border: 1.5px solid var(--clr-primary);
+ display: flex;
+ align-items: center;
+ gap: 14px;
+}
+.routine-complete-banner .rbc-icon { font-size: 32px; }
+.routine-complete-banner h3 { font-size: 16px; font-weight: 700; color: var(--clr-primary-dk); }
+.routine-complete-banner p { font-size: 13px; color: var(--clr-text-2); margin-top: 2px; }
+
+/* Active exercise timer card */
+.timer-card {
+ margin: 16px 20px 0;
+ background: var(--clr-surface);
+ border-radius: var(--radius-xl);
+ padding: 24px 20px;
+ box-shadow: var(--shadow-lg);
+ border: 1px solid var(--clr-border);
+ text-align: center;
+}
+
+.timer-exercise-name {
+ font-size: 18px;
+ font-weight: 700;
+ margin-bottom: 6px;
+}
+
+.timer-exercise-meta {
+ font-size: 13px;
+ color: var(--clr-text-2);
+ margin-bottom: 24px;
+}
+
+.timer-display {
+ font-size: 64px;
+ font-weight: 800;
+ letter-spacing: -3px;
+ color: var(--clr-primary);
+ line-height: 1;
+ margin-bottom: 8px;
+ font-variant-numeric: tabular-nums;
+}
+
+.timer-label { font-size: 13px; color: var(--clr-text-3); margin-bottom: 24px; }
+
+.timer-controls {
+ display: flex;
+ gap: 12px;
+ justify-content: center;
+}
+
+.btn-icon {
+ width: 52px; height: 52px;
+ border-radius: 50%;
+ border: none;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 20px;
+ transition: transform var(--transition), opacity var(--transition);
+ background: var(--clr-surface-2);
+ color: var(--clr-text-2);
+}
+.btn-icon:active { transform: scale(.94); }
+.btn-icon.primary {
+ width: 64px; height: 64px;
+ background: var(--clr-primary);
+ color: white;
+ box-shadow: 0 4px 16px rgba(47,122,88,.35);
+ font-size: 24px;
+}
+.btn-icon.primary:active { transform: scale(.94); }
+.btn-icon.done-btn {
+ background: var(--clr-primary-lt);
+ color: var(--clr-primary);
+}
+
+/* Reps card (non-timed) */
+.reps-display {
+ font-size: 56px;
+ font-weight: 800;
+ letter-spacing: -2px;
+ color: var(--clr-primary);
+ line-height: 1;
+ margin-bottom: 6px;
+}
+.reps-label { font-size: 13px; color: var(--clr-text-3); margin-bottom: 24px; }
+
+.set-indicators {
+ display: flex;
+ gap: 8px;
+ justify-content: center;
+ margin-bottom: 24px;
+}
+
+.set-dot {
+ width: 10px; height: 10px;
+ border-radius: 50%;
+ border: 2px solid var(--clr-border);
+ background: var(--clr-surface-2);
+ transition: all var(--transition);
+}
+.set-dot.done { background: var(--clr-primary); border-color: var(--clr-primary); }
+.set-dot.current { border-color: var(--clr-primary); }
+
+/* Routine list */
+.routine-list {
+ padding: 16px 20px 0;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.routine-item {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ padding: 14px 16px;
+ background: var(--clr-surface);
+ border-radius: var(--radius-md);
+ border: 1px solid var(--clr-border);
+ transition: all var(--transition);
+ cursor: pointer;
+}
+
+.routine-item.active-item {
+ border-color: var(--clr-primary);
+ background: var(--clr-primary-lt);
+ box-shadow: 0 0 0 3px rgba(47,122,88,.12);
+}
+
+.routine-item.completed-item {
+ opacity: .55;
+}
+
+.routine-item-num {
+ width: 28px; height: 28px;
+ border-radius: 50%;
+ background: var(--clr-surface-2);
+ border: 2px solid var(--clr-border);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--clr-text-3);
+ flex-shrink: 0;
+ transition: all var(--transition);
+}
+.routine-item.active-item .routine-item-num {
+ background: var(--clr-primary);
+ border-color: var(--clr-primary);
+ color: white;
+}
+.routine-item.completed-item .routine-item-num {
+ background: var(--clr-primary);
+ border-color: var(--clr-primary);
+ color: white;
+}
+
+.routine-item-info { flex: 1; min-width: 0; }
+.routine-item-name { font-size: 14px; font-weight: 600; }
+.routine-item-detail { font-size: 12px; color: var(--clr-text-3); margin-top: 2px; }
+
+.routine-item-status { flex-shrink: 0; font-size: 18px; }
+
+/* Reset button */
+.btn-text {
+ display: block;
+ width: 100%;
+ margin: 20px 0 0;
+ padding: 12px;
+ background: none;
+ border: 1.5px solid var(--clr-border);
+ border-radius: var(--radius-md);
+ font-family: var(--font);
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--clr-text-2);
+ cursor: pointer;
+ transition: all var(--transition);
+}
+.btn-text:active { background: var(--clr-surface-2); }
+
+/* ── PROGRESS VIEW ────────────────────────────────────────── */
+.stats-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 12px;
+}
+
+.stat-card {
+ background: var(--clr-surface);
+ border-radius: var(--radius-lg);
+ padding: 18px 16px;
+ border: 1px solid var(--clr-border);
+ box-shadow: var(--shadow-sm);
+}
+.stat-card .sc-icon { font-size: 26px; margin-bottom: 8px; }
+.stat-card .sc-value { font-size: 28px; font-weight: 800; letter-spacing: -.6px; color: var(--clr-text); }
+.stat-card .sc-label { font-size: 12px; color: var(--clr-text-3); font-weight: 600; margin-top: 2px; }
+
+/* Weekly calendar */
+.week-grid {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: 8px;
+}
+
+.week-day {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 6px;
+}
+
+.week-day-label {
+ font-size: 11px;
+ font-weight: 700;
+ color: var(--clr-text-3);
+ text-transform: uppercase;
+}
+
+.week-day-dot {
+ width: 32px; height: 32px;
+ border-radius: 50%;
+ background: var(--clr-surface-2);
+ border: 2px solid var(--clr-border);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 11px;
+ font-weight: 700;
+ color: var(--clr-text-3);
+ transition: all var(--transition);
+}
+
+.week-day-dot.done {
+ background: var(--clr-primary);
+ border-color: var(--clr-primary);
+ color: white;
+}
+
+.week-day-dot.today {
+ border-color: var(--clr-primary);
+ color: var(--clr-primary);
+ font-weight: 800;
+}
+
+.week-day-dot.done.today {
+ color: white;
+}
+
+/* Month heatmap */
+.heatmap {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.heatmap-row {
+ display: flex;
+ gap: 4px;
+}
+
+.hm-cell {
+ width: 32px; height: 32px;
+ border-radius: 6px;
+ background: var(--clr-surface-2);
+ flex-shrink: 0;
+}
+
+.hm-cell.done { background: var(--clr-primary); opacity: .85; }
+.hm-cell.done-full { background: var(--clr-primary); }
+.hm-cell.future { background: var(--clr-border); opacity: .5; }
+
+/* Lifestyle tips */
+.tips-list { display: flex; flex-direction: column; gap: 10px; }
+
+.lifestyle-tip {
+ display: flex;
+ gap: 14px;
+ align-items: flex-start;
+ padding: 14px 16px;
+ background: var(--clr-surface);
+ border-radius: var(--radius-md);
+ border: 1px solid var(--clr-border);
+}
+.lifestyle-tip .lt-icon { font-size: 24px; flex-shrink: 0; }
+.lifestyle-tip .lt-title { font-size: 14px; font-weight: 700; margin-bottom: 3px; }
+.lifestyle-tip .lt-desc { font-size: 13px; color: var(--clr-text-2); line-height: 1.5; }
+
+/* ── Utilities ────────────────────────────────────────────── */
+.mt-16 { margin-top: 16px; }
+.mt-12 { margin-top: 12px; }
+.mt-8 { margin-top: 8px; }
+
+/* ── Scrollbar ────────────────────────────────────────────── */
+#app::-webkit-scrollbar { width: 0; }
+
+/* ── Install Banner ───────────────────────────────────────── */
+#install-banner {
+ display: none;
+ position: fixed;
+ top: 12px;
+ left: 12px; right: 12px;
+ background: var(--clr-surface);
+ border-radius: var(--radius-lg);
+ padding: 16px 18px;
+ box-shadow: var(--shadow-lg);
+ border: 1px solid var(--clr-border);
+ z-index: 200;
+ flex-direction: column;
+ gap: 12px;
+ animation: fadeUp 240ms ease both;
+}
+#install-banner.show { display: flex; }
+#install-banner p { font-size: 14px; font-weight: 600; }
+#install-banner small { font-size: 12px; color: var(--clr-text-2); }
+
+.install-actions { display: flex; gap: 8px; }
+
+.btn-primary {
+ flex: 1;
+ padding: 10px;
+ background: var(--clr-primary);
+ color: white;
+ border: none;
+ border-radius: var(--radius-md);
+ font-family: var(--font);
+ font-size: 14px;
+ font-weight: 700;
+ cursor: pointer;
+ transition: opacity var(--transition);
+}
+.btn-primary:active { opacity: .85; }
+
+.btn-secondary {
+ padding: 10px 16px;
+ background: var(--clr-surface-2);
+ color: var(--clr-text-2);
+ border: none;
+ border-radius: var(--radius-md);
+ font-family: var(--font);
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+}
diff --git a/sw.js b/sw.js
new file mode 100644
index 0000000..67699f9
--- /dev/null
+++ b/sw.js
@@ -0,0 +1,45 @@
+const CACHE_NAME = 'tiltfix-v1';
+const ASSETS = [
+ '/',
+ '/index.html',
+ '/style.css',
+ '/app.js',
+ '/manifest.json',
+ '/icons/icon.svg',
+ '/icons/icon-maskable.svg'
+];
+
+self.addEventListener('install', (event) => {
+ event.waitUntil(
+ caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS))
+ );
+ self.skipWaiting();
+});
+
+self.addEventListener('activate', (event) => {
+ event.waitUntil(
+ caches.keys().then((keys) =>
+ Promise.all(
+ keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))
+ )
+ )
+ );
+ self.clients.claim();
+});
+
+self.addEventListener('fetch', (event) => {
+ if (event.request.method !== 'GET') return;
+ event.respondWith(
+ caches.match(event.request).then((cached) => {
+ if (cached) return cached;
+ return fetch(event.request).then((response) => {
+ if (!response || response.status !== 200 || response.type === 'opaque') {
+ return response;
+ }
+ const clone = response.clone();
+ caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
+ return response;
+ });
+ })
+ );
+});