diff --git a/manifest.json b/manifest.json
new file mode 100644
index 0000000..263191d
--- /dev/null
+++ b/manifest.json
@@ -0,0 +1,70 @@
+{
+ "name": "OpenRockets - Build the Future Together",
+ "short_name": "OpenRockets",
+ "description": "OpenRockets OSS - Advancing software, education, and data innovation through the power of open-source. Level up, Challenge, Grow, Contribute.",
+ "start_url": "/",
+ "display": "standalone",
+ "background_color": "#ffffff",
+ "theme_color": "#EC3750",
+ "orientation": "portrait-primary",
+ "icons": [
+ {
+ "src": "/i/assets/static/openthread_logo_bash-trans-removebg-preview (1).png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "any maskable"
+ },
+ {
+ "src": "/v/openrockets.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any maskable"
+ }
+ ],
+ "categories": ["education", "development", "productivity"],
+ "screenshots": [
+ {
+ "src": "/v/cityofgits-summercap-dark-lg-4000x1010.png",
+ "sizes": "4000x1010",
+ "type": "image/png"
+ }
+ ],
+ "shortcuts": [
+ {
+ "name": "Join OpenRockets",
+ "short_name": "Join",
+ "description": "Join the OpenRockets community",
+ "url": "/join.html",
+ "icons": [
+ {
+ "src": "/v/openrockets.png",
+ "sizes": "192x192"
+ }
+ ]
+ },
+ {
+ "name": "Events Calendar",
+ "short_name": "Events",
+ "description": "View upcoming events and hackathons",
+ "url": "/calendar.html",
+ "icons": [
+ {
+ "src": "/v/openrockets.png",
+ "sizes": "192x192"
+ }
+ ]
+ },
+ {
+ "name": "Community",
+ "short_name": "Community",
+ "description": "Connect with the OpenRockets community",
+ "url": "/community.html",
+ "icons": [
+ {
+ "src": "/v/openrockets.png",
+ "sizes": "192x192"
+ }
+ ]
+ }
+ ]
+}
diff --git a/robots.txt b/robots.txt
new file mode 100644
index 0000000..a5e0030
--- /dev/null
+++ b/robots.txt
@@ -0,0 +1,23 @@
+# robots.txt for OpenRockets.com
+
+User-agent: *
+Allow: /
+
+# Sitemap location
+Sitemap: https://openrockets.com/sitemap.xml
+
+# Disallow private directories
+Disallow: /node_modules/
+Disallow: /.git/
+Disallow: /.vscode/
+Disallow: /uploads/
+Disallow: /form/js/
+
+# Allow important resources
+Allow: /scripts/
+Allow: /styles/
+Allow: /v/
+Allow: /assets/
+
+# Crawl-delay (be nice to smaller bots)
+Crawl-delay: 1
diff --git a/scripts/enhancements.js b/scripts/enhancements.js
new file mode 100644
index 0000000..f9c4b26
--- /dev/null
+++ b/scripts/enhancements.js
@@ -0,0 +1,363 @@
+// Enhanced UX Features for OpenRockets
+// Smooth scrolling, animations, lazy loading, and more
+
+// 1. Smooth Scrolling for all anchor links
+document.querySelectorAll('a[href^="#"]').forEach(anchor => {
+ anchor.addEventListener('click', function (e) {
+ const href = this.getAttribute('href');
+ if (href === '#' || !href) return;
+
+ e.preventDefault();
+ const target = document.querySelector(href);
+
+ if (target) {
+ target.scrollIntoView({
+ behavior: 'smooth',
+ block: 'start'
+ });
+
+ // Update URL without jumping
+ history.pushState(null, null, href);
+ }
+ });
+});
+
+// 2. Intersection Observer for fade-in animations
+const observerOptions = {
+ threshold: 0.1,
+ rootMargin: '0px 0px -50px 0px'
+};
+
+const fadeInObserver = new IntersectionObserver((entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ entry.target.classList.add('animate-fade-in');
+ fadeInObserver.unobserve(entry.target);
+ }
+ });
+}, observerOptions);
+
+// Observe elements for fade-in
+document.addEventListener('DOMContentLoaded', () => {
+ const elementsToAnimate = document.querySelectorAll(
+ '.feature-card, .stat-item, .update-item, .news-content, .event-card'
+ );
+
+ elementsToAnimate.forEach(el => {
+ el.style.opacity = '0';
+ el.style.transform = 'translateY(20px)';
+ el.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
+ fadeInObserver.observe(el);
+ });
+});
+
+// 3. Lazy Loading for Images
+if ('loading' in HTMLImageElement.prototype) {
+ const images = document.querySelectorAll('img[data-src]');
+ images.forEach(img => {
+ img.src = img.dataset.src;
+ });
+} else {
+ // Fallback for browsers that don't support native lazy loading
+ const lazyImages = document.querySelectorAll('img[data-src]');
+
+ const imageObserver = new IntersectionObserver((entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ const img = entry.target;
+ img.src = img.dataset.src;
+ img.classList.add('loaded');
+ imageObserver.unobserve(img);
+ }
+ });
+ });
+
+ lazyImages.forEach(img => imageObserver.observe(img));
+}
+
+// 4. Add loading animation class
+const style = document.createElement('style');
+style.textContent = `
+ .animate-fade-in {
+ opacity: 1 !important;
+ transform: translateY(0) !important;
+ }
+
+ .pulse-animation {
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
+ }
+
+ @keyframes pulse {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: .7;
+ }
+ }
+
+ .slide-in-left {
+ animation: slideInLeft 0.6s ease-out;
+ }
+
+ @keyframes slideInLeft {
+ from {
+ transform: translateX(-50px);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+ }
+
+ .slide-in-right {
+ animation: slideInRight 0.6s ease-out;
+ }
+
+ @keyframes slideInRight {
+ from {
+ transform: translateX(50px);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+ }
+
+ /* Skeleton loading screens */
+ .skeleton {
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
+ background-size: 200% 100%;
+ animation: loading 1.5s ease-in-out infinite;
+ }
+
+ @keyframes loading {
+ 0% {
+ background-position: 200% 0;
+ }
+ 100% {
+ background-position: -200% 0;
+ }
+ }
+
+ /* Scroll progress indicator */
+ .scroll-progress {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 0;
+ height: 3px;
+ background: linear-gradient(90deg, var(--ada-pink, #EC3750), var(--ada-orange, #FF8C37));
+ z-index: 9999;
+ transition: width 0.1s ease;
+ }
+`;
+document.head.appendChild(style);
+
+// 5. Scroll Progress Indicator
+const createScrollProgress = () => {
+ const progressBar = document.createElement('div');
+ progressBar.className = 'scroll-progress';
+ document.body.appendChild(progressBar);
+
+ window.addEventListener('scroll', () => {
+ const windowHeight = window.innerHeight;
+ const documentHeight = document.documentElement.scrollHeight;
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
+ const scrollPercentage = (scrollTop / (documentHeight - windowHeight)) * 100;
+
+ progressBar.style.width = `${scrollPercentage}%`;
+ });
+};
+
+// 6. Back to Top Button
+const createBackToTop = () => {
+ const button = document.createElement('button');
+ button.innerHTML = '
';
+ button.className = 'back-to-top';
+ button.setAttribute('aria-label', 'Back to top');
+ button.style.cssText = `
+ position: fixed;
+ bottom: 30px;
+ right: 30px;
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #EC3750, #FF8C37);
+ color: white;
+ border: none;
+ cursor: pointer;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+ z-index: 1000;
+ box-shadow: 0 4px 20px rgba(236, 55, 80, 0.4);
+ font-size: 20px;
+ `;
+
+ button.addEventListener('mouseenter', () => {
+ button.style.transform = 'scale(1.1) translateY(-2px)';
+ button.style.boxShadow = '0 6px 25px rgba(236, 55, 80, 0.5)';
+ });
+
+ button.addEventListener('mouseleave', () => {
+ button.style.transform = 'scale(1) translateY(0)';
+ button.style.boxShadow = '0 4px 20px rgba(236, 55, 80, 0.4)';
+ });
+
+ window.addEventListener('scroll', () => {
+ if (window.pageYOffset > 300) {
+ button.style.opacity = '1';
+ button.style.visibility = 'visible';
+ } else {
+ button.style.opacity = '0';
+ button.style.visibility = 'hidden';
+ }
+ });
+
+ button.addEventListener('click', () => {
+ window.scrollTo({
+ top: 0,
+ behavior: 'smooth'
+ });
+ });
+
+ document.body.appendChild(button);
+};
+
+// 7. Enhanced Card Hover Effects
+document.addEventListener('DOMContentLoaded', () => {
+ const cards = document.querySelectorAll('.feature-card, .stat-item');
+
+ cards.forEach(card => {
+ card.addEventListener('mouseenter', function() {
+ this.style.transform = 'translateY(-8px)';
+ this.style.transition = 'transform 0.3s ease, box-shadow 0.3s ease';
+ });
+
+ card.addEventListener('mouseleave', function() {
+ this.style.transform = 'translateY(0)';
+ });
+ });
+});
+
+// 8. Performance Monitoring
+const measurePerformance = () => {
+ if ('PerformanceObserver' in window) {
+ const observer = new PerformanceObserver((list) => {
+ for (const entry of list.getEntries()) {
+ if (entry.entryType === 'largest-contentful-paint') {
+ console.log('LCP:', entry.renderTime || entry.loadTime);
+ }
+ }
+ });
+
+ observer.observe({ entryTypes: ['largest-contentful-paint'] });
+ }
+
+ // Log Core Web Vitals
+ if ('performance' in window) {
+ window.addEventListener('load', () => {
+ const perfData = performance.getEntriesByType('navigation')[0];
+ console.log('Page Load Time:', perfData.loadEventEnd - perfData.fetchStart, 'ms');
+ });
+ }
+};
+
+// 9. Parallax Effect for Hero Section
+const initParallax = () => {
+ const heroSection = document.querySelector('.hero');
+ if (heroSection) {
+ window.addEventListener('scroll', () => {
+ const scrolled = window.pageYOffset;
+ const parallaxSpeed = 0.5;
+ heroSection.style.transform = `translateY(${scrolled * parallaxSpeed}px)`;
+ });
+ }
+};
+
+// 10. Initialize all enhancements
+document.addEventListener('DOMContentLoaded', () => {
+ createScrollProgress();
+ createBackToTop();
+ measurePerformance();
+ // Parallax can be heavy, uncomment if desired
+ // initParallax();
+
+ // Add loaded class to body for CSS animations
+ document.body.classList.add('page-loaded');
+});
+
+// 11. Service Worker Registration for PWA
+if ('serviceWorker' in navigator) {
+ window.addEventListener('load', () => {
+ navigator.serviceWorker
+ .register('/sw.js')
+ .then(registration => {
+ console.log('ServiceWorker registered:', registration.scope);
+ })
+ .catch(error => {
+ console.log('ServiceWorker registration failed:', error);
+ });
+ });
+}
+
+// 12. Add to Home Screen Prompt (PWA)
+let deferredPrompt;
+window.addEventListener('beforeinstallprompt', (e) => {
+ e.preventDefault();
+ deferredPrompt = e;
+
+ // Show install button
+ const installButton = document.createElement('button');
+ installButton.textContent = '📱 Install App';
+ installButton.className = 'install-app-btn';
+ installButton.style.cssText = `
+ position: fixed;
+ bottom: 90px;
+ right: 30px;
+ padding: 12px 24px;
+ background: linear-gradient(135deg, #EC3750, #FF8C37);
+ color: white;
+ border: none;
+ border-radius: 25px;
+ cursor: pointer;
+ font-weight: 600;
+ box-shadow: 0 4px 15px rgba(236, 55, 80, 0.4);
+ z-index: 999;
+ animation: slideInRight 0.5s ease;
+ `;
+
+ installButton.addEventListener('click', async () => {
+ installButton.style.display = 'none';
+ deferredPrompt.prompt();
+ const { outcome } = await deferredPrompt.userChoice;
+ console.log(`User response to install prompt: ${outcome}`);
+ deferredPrompt = null;
+ });
+
+ document.body.appendChild(installButton);
+});
+
+// 13. Keyboard Accessibility Enhancements
+document.addEventListener('keydown', (e) => {
+ // Press '/' to focus search (if search exists)
+ if (e.key === '/' && !e.target.matches('input, textarea')) {
+ e.preventDefault();
+ const searchInput = document.querySelector('input[type="search"], input[placeholder*="search" i]');
+ if (searchInput) searchInput.focus();
+ }
+
+ // Press 'Escape' to close modals
+ if (e.key === 'Escape') {
+ const modals = document.querySelectorAll('.modal.show, [role="dialog"][aria-hidden="false"]');
+ modals.forEach(modal => {
+ modal.classList.remove('show');
+ modal.setAttribute('aria-hidden', 'true');
+ });
+ }
+});
+
+console.log('✨ Enhanced UX features loaded successfully!');
diff --git a/sitemap.xml b/sitemap.xml
new file mode 100644
index 0000000..d0f672e
--- /dev/null
+++ b/sitemap.xml
@@ -0,0 +1,39 @@
+
+
+
+ https://openrockets.com/
+ 2026-01-18
+ daily
+ 1.0
+
+
+ https://openrockets.com/index.html
+ 2026-01-18
+ daily
+ 1.0
+
+
+ https://openrockets.com/about.html
+ 2026-01-18
+ weekly
+ 0.8
+
+
+ https://openrockets.com/join.html
+ 2026-01-18
+ weekly
+ 0.9
+
+
+ https://openrockets.com/calendar.html
+ 2026-01-18
+ weekly
+ 0.8
+
+
+ https://openrockets.com/community.html
+ 2026-01-18
+ daily
+ 0.8
+
+
diff --git a/styles/super-enhancements.css b/styles/super-enhancements.css
new file mode 100644
index 0000000..c8a9b1d
--- /dev/null
+++ b/styles/super-enhancements.css
@@ -0,0 +1,486 @@
+/* Super Website Enhancements - Performance & Visual Polish */
+
+/* ==========================================
+ 1. PERFORMANCE OPTIMIZATIONS
+ ========================================== */
+
+/* Smooth scrolling for the entire page */
+html {
+ scroll-behavior: smooth;
+}
+
+/* Hardware acceleration for transforms */
+* {
+ -webkit-transform: translateZ(0);
+ -webkit-backface-visibility: hidden;
+ -webkit-perspective: 1000;
+}
+
+/* Reduce motion for users who prefer it */
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+ }
+}
+
+/* ==========================================
+ 2. LOADING STATES
+ ========================================== */
+
+/* Page load animation */
+body:not(.page-loaded) {
+ opacity: 0;
+ animation: fadeIn 0.5s ease forwards;
+}
+
+@keyframes fadeIn {
+ to {
+ opacity: 1;
+ }
+}
+
+/* Skeleton loading for images */
+img {
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
+ background-size: 200% 100%;
+}
+
+img.loaded,
+img[src] {
+ animation: none;
+ background: transparent;
+}
+
+/* ==========================================
+ 3. ENHANCED ANIMATIONS
+ ========================================== */
+
+/* Smooth hover transitions */
+.feature-card,
+.stat-item,
+.project-link,
+.btn {
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+/* Enhanced card hover with 3D effect */
+.feature-card:hover {
+ transform: translateY(-8px) scale(1.02);
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
+}
+
+/* Button hover effects */
+.btn {
+ position: relative;
+ overflow: hidden;
+ z-index: 1;
+}
+
+.btn::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
+ transition: left 0.5s;
+ z-index: -1;
+}
+
+.btn:hover::before {
+ left: 100%;
+}
+
+/* Pulse animation for CTA buttons */
+@keyframes pulse-glow {
+ 0%, 100% {
+ box-shadow: 0 0 0 0 rgba(236, 55, 80, 0.7);
+ }
+ 50% {
+ box-shadow: 0 0 0 10px rgba(236, 55, 80, 0);
+ }
+}
+
+.btn-primary.pulse {
+ animation: pulse-glow 2s infinite;
+}
+
+/* ==========================================
+ 4. MICRO-INTERACTIONS
+ ========================================== */
+
+/* Link hover underline effect */
+.nav-link {
+ position: relative;
+}
+
+.nav-link::after {
+ content: '';
+ position: absolute;
+ bottom: -2px;
+ left: 0;
+ width: 0;
+ height: 2px;
+ background: linear-gradient(90deg, #EC3750, #FF8C37);
+ transition: width 0.3s ease;
+}
+
+.nav-link:hover::after,
+.nav-link.active::after {
+ width: 100%;
+}
+
+/* Icon rotation on hover */
+.feature-link i,
+.project-link i {
+ transition: transform 0.3s ease;
+}
+
+.feature-link:hover i,
+.project-link:hover i {
+ transform: translateX(5px);
+}
+
+/* ==========================================
+ 5. SCROLL ANIMATIONS
+ ========================================== */
+
+/* Fade in from bottom */
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translateY(30px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.animate-on-scroll {
+ opacity: 0;
+ animation: fadeInUp 0.6s ease forwards;
+}
+
+/* Stagger animation for multiple items */
+.feature-card:nth-child(1) { animation-delay: 0.1s; }
+.feature-card:nth-child(2) { animation-delay: 0.2s; }
+.feature-card:nth-child(3) { animation-delay: 0.3s; }
+.feature-card:nth-child(4) { animation-delay: 0.4s; }
+.feature-card:nth-child(5) { animation-delay: 0.5s; }
+.feature-card:nth-child(6) { animation-delay: 0.6s; }
+
+/* ==========================================
+ 6. ENHANCED TYPOGRAPHY
+ ========================================== */
+
+/* Better text rendering */
+body {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ text-rendering: optimizeLegibility;
+}
+
+/* Text selection styling */
+::selection {
+ background: rgba(236, 55, 80, 0.3);
+ color: inherit;
+}
+
+::-moz-selection {
+ background: rgba(236, 55, 80, 0.3);
+ color: inherit;
+}
+
+/* ==========================================
+ 7. LOADING SPINNER
+ ========================================== */
+
+.spinner {
+ width: 50px;
+ height: 50px;
+ border: 5px solid #f3f3f3;
+ border-top: 5px solid #EC3750;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+/* ==========================================
+ 8. TOAST NOTIFICATIONS
+ ========================================== */
+
+.toast {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ background: white;
+ padding: 16px 24px;
+ border-radius: 12px;
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ animation: slideInRight 0.3s ease;
+ z-index: 10000;
+}
+
+.toast.success {
+ border-left: 4px solid #28a745;
+}
+
+.toast.error {
+ border-left: 4px solid #dc3545;
+}
+
+.toast.info {
+ border-left: 4px solid #17a2b8;
+}
+
+/* ==========================================
+ 9. FOCUS STYLES FOR ACCESSIBILITY
+ ========================================== */
+
+/* Better focus indicators */
+*:focus {
+ outline: 2px solid #EC3750;
+ outline-offset: 2px;
+}
+
+/* Remove outline for mouse users, keep for keyboard users */
+*:focus:not(:focus-visible) {
+ outline: none;
+}
+
+*:focus-visible {
+ outline: 2px solid #EC3750;
+ outline-offset: 2px;
+}
+
+/* Skip to main content link */
+.skip-to-main {
+ position: absolute;
+ top: -40px;
+ left: 0;
+ background: #EC3750;
+ color: white;
+ padding: 8px 16px;
+ text-decoration: none;
+ z-index: 100;
+}
+
+.skip-to-main:focus {
+ top: 0;
+}
+
+/* ==========================================
+ 10. DARK MODE SUPPORT
+ ========================================== */
+
+@media (prefers-color-scheme: dark) {
+ /* Adjust colors for dark mode users if not already implemented */
+ img {
+ opacity: 0.9;
+ }
+}
+
+/* ==========================================
+ 11. PRINT STYLES
+ ========================================== */
+
+@media print {
+ /* Hide non-essential elements when printing */
+ .nav-toggle,
+ .back-to-top,
+ .install-app-btn,
+ .scroll-progress,
+ .parrot-assistant,
+ .hero-illustrations {
+ display: none !important;
+ }
+
+ /* Optimize for print */
+ * {
+ background: white !important;
+ color: black !important;
+ }
+
+ a[href]::after {
+ content: " (" attr(href) ")";
+ }
+}
+
+/* ==========================================
+ 12. RESPONSIVE ENHANCEMENTS
+ ========================================== */
+
+/* Better touch targets for mobile */
+@media (max-width: 768px) {
+ button,
+ a,
+ input,
+ select,
+ textarea {
+ min-height: 44px;
+ min-width: 44px;
+ }
+
+ /* Reduce animations on mobile for performance */
+ * {
+ animation-duration: 0.3s !important;
+ transition-duration: 0.2s !important;
+ }
+}
+
+/* ==========================================
+ 13. GRID ENHANCEMENTS
+ ========================================== */
+
+/* Better grid spacing */
+.features-grid {
+ display: grid;
+ gap: 2rem;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+}
+
+/* ==========================================
+ 14. UTILITIES
+ ========================================== */
+
+/* Screen reader only */
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border-width: 0;
+}
+
+/* Hide on mobile */
+@media (max-width: 768px) {
+ .hide-mobile {
+ display: none !important;
+ }
+}
+
+/* Hide on desktop */
+@media (min-width: 769px) {
+ .hide-desktop {
+ display: none !important;
+ }
+}
+
+/* ==========================================
+ 15. GRADIENT TEXT
+ ========================================== */
+
+.text-gradient {
+ background: linear-gradient(135deg, #EC3750, #FF8C37);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+/* ==========================================
+ 16. GLASSMORPHISM EFFECT
+ ========================================== */
+
+.glass-effect {
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+/* ==========================================
+ 17. ANIMATION PRESETS
+ ========================================== */
+
+.bounce {
+ animation: bounce 1s infinite;
+}
+
+@keyframes bounce {
+ 0%, 100% {
+ transform: translateY(0);
+ }
+ 50% {
+ transform: translateY(-10px);
+ }
+}
+
+.shake {
+ animation: shake 0.5s;
+}
+
+@keyframes shake {
+ 0%, 100% { transform: translateX(0); }
+ 10%, 30%, 50%, 70%, 90% { transform: translateX(-10px); }
+ 20%, 40%, 60%, 80% { transform: translateX(10px); }
+}
+
+/* ==========================================
+ 18. IMAGE OPTIMIZATION
+ ========================================== */
+
+img {
+ max-width: 100%;
+ height: auto;
+ display: block;
+}
+
+/* Prevent layout shift */
+img[width][height] {
+ height: auto;
+}
+
+/* ==========================================
+ 19. PERFORMANCE - Will-change hints
+ ========================================== */
+
+.feature-card,
+.btn,
+.nav-link {
+ will-change: transform;
+}
+
+/* ==========================================
+ 20. CUSTOM SCROLLBAR
+ ========================================== */
+
+::-webkit-scrollbar {
+ width: 12px;
+}
+
+::-webkit-scrollbar-track {
+ background: #f1f1f1;
+}
+
+::-webkit-scrollbar-thumb {
+ background: linear-gradient(135deg, #EC3750, #FF8C37);
+ border-radius: 6px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: linear-gradient(135deg, #d62f45, #e67a2f);
+}
+
+/* Firefox scrollbar */
+* {
+ scrollbar-width: thin;
+ scrollbar-color: #EC3750 #f1f1f1;
+}
diff --git a/sw.js b/sw.js
new file mode 100644
index 0000000..39386bc
--- /dev/null
+++ b/sw.js
@@ -0,0 +1,88 @@
+const CACHE_NAME = 'openrockets-v1';
+const urlsToCache = [
+ '/',
+ '/index.html',
+ '/join.html',
+ '/calendar.html',
+ '/community.html',
+ '/about.html',
+ '/styles/hackclub-style.css',
+ '/styles/main.css',
+ '/styles/responsive.css',
+ '/scripts/navigation.js',
+ '/scripts/hackclub-interactions.js',
+ '/scripts/parrot-assistant.js',
+ '/v/openrockets.png',
+ '/i/assets/static/openthread_logo_bash-trans-removebg-preview (1).png'
+];
+
+// Install event - cache resources
+self.addEventListener('install', (event) => {
+ event.waitUntil(
+ caches.open(CACHE_NAME)
+ .then((cache) => {
+ console.log('Opened cache');
+ return cache.addAll(urlsToCache);
+ })
+ .catch((error) => {
+ console.log('Cache install error:', error);
+ })
+ );
+ self.skipWaiting();
+});
+
+// Fetch event - serve from cache, fallback to network
+self.addEventListener('fetch', (event) => {
+ event.respondWith(
+ caches.match(event.request)
+ .then((response) => {
+ // Cache hit - return response
+ if (response) {
+ return response;
+ }
+
+ // Clone the request
+ const fetchRequest = event.request.clone();
+
+ return fetch(fetchRequest).then((response) => {
+ // Check if valid response
+ if (!response || response.status !== 200 || response.type !== 'basic') {
+ return response;
+ }
+
+ // Clone the response
+ const responseToCache = response.clone();
+
+ caches.open(CACHE_NAME)
+ .then((cache) => {
+ cache.put(event.request, responseToCache);
+ });
+
+ return response;
+ });
+ })
+ .catch(() => {
+ // Return offline page if available
+ return caches.match('/index.html');
+ })
+ );
+});
+
+// Activate event - clean up old caches
+self.addEventListener('activate', (event) => {
+ const cacheWhitelist = [CACHE_NAME];
+
+ event.waitUntil(
+ caches.keys().then((cacheNames) => {
+ return Promise.all(
+ cacheNames.map((cacheName) => {
+ if (cacheWhitelist.indexOf(cacheName) === -1) {
+ return caches.delete(cacheName);
+ }
+ })
+ );
+ })
+ );
+
+ return self.clients.claim();
+});