diff --git a/AR_README.md b/AR_README.md
new file mode 100644
index 0000000..ae0f902
--- /dev/null
+++ b/AR_README.md
@@ -0,0 +1,166 @@
+# Earth AR Visualization
+
+This project adds Apple ARKit support to the existing Three.js Earth visualization using the WebXR Device API.
+
+## Features
+
+- **AR Earth Placement**: Place a 3D Earth globe in your real environment
+- **Flight Route Visualization**: View your flight routes in augmented reality
+- **Touch Interaction**: Tap to place the Earth and interact with it
+- **Cross-Platform**: Works on iOS devices with ARKit support
+
+## How It Works
+
+### WebXR Device API
+The AR functionality uses the **WebXR Device API**, which is the web standard for AR/VR experiences. This allows the Three.js visualization to work with:
+
+- **Apple ARKit** (iOS devices)
+- **Google ARCore** (Android devices)
+- **WebXR-compatible browsers**
+
+### Key Components
+
+1. **AR Session Management**: Handles the AR session lifecycle
+2. **Hit Testing**: Detects surfaces for Earth placement
+3. **Reticle System**: Visual indicator for placement location
+4. **Simplified Earth Model**: Optimized for AR performance
+
+## Browser Support
+
+### iOS (Apple ARKit)
+- **Safari**: Full support with ARKit
+- **Chrome for iOS**: Limited support
+- **Firefox for iOS**: Limited support
+
+### Android (ARCore)
+- **Chrome**: Full support with ARCore
+- **Firefox**: Limited support
+
+## Usage
+
+### Basic AR Experience
+
+1. Open `earth-ar.html` on a supported device
+2. Allow camera permissions when prompted
+3. Point your camera at a flat surface
+4. Tap the screen to place the Earth
+5. Explore your flight routes in AR space
+
+### Integration with Existing Earth
+
+To integrate AR with your existing Earth visualization:
+
+```javascript
+// In your existing earth.js file
+import { EarthAR } from './earth-ar.js';
+
+// After initializing your existing earth
+const earthAR = new EarthAR();
+await earthAR.init();
+
+// Integrate with existing earth mesh
+earthAR.integrateWithExistingEarth(existingEarthMesh);
+```
+
+## Technical Implementation
+
+### AR Session Setup
+```javascript
+const session = await navigator.xr.requestSession('immersive-ar', {
+ requiredFeatures: ['hit-test'],
+ optionalFeatures: ['dom-overlay']
+});
+```
+
+### Hit Testing
+```javascript
+const hitTestSource = await session.requestHitTestSource({
+ space: referenceSpace
+});
+```
+
+### Rendering Loop
+```javascript
+renderer.setAnimationLoop((timestamp, frame) => {
+ // AR rendering logic
+ renderer.render(scene, camera);
+});
+```
+
+## Performance Considerations
+
+### AR Optimizations
+- **Reduced Geometry**: Simplified Earth model for AR
+- **Efficient Materials**: Basic materials instead of complex textures
+- **Frame Rate**: Maintains 60fps for smooth AR experience
+- **Memory Management**: Proper cleanup of AR resources
+
+### Device Requirements
+- **iOS 11+** with ARKit support
+- **Modern browser** with WebXR support
+- **A9 processor or newer** for optimal performance
+
+## Troubleshooting
+
+### Common Issues
+
+1. **"AR Not Supported" Error**
+ - Check if device supports ARKit/ARCore
+ - Ensure browser supports WebXR
+ - Try Safari on iOS for best compatibility
+
+2. **Camera Permission Denied**
+ - Allow camera access in browser settings
+ - Refresh page and try again
+
+3. **Poor Performance**
+ - Close other AR apps
+ - Ensure good lighting conditions
+ - Restart browser if needed
+
+### Debug Mode
+Enable console logging for debugging:
+```javascript
+// In earth-ar.js
+console.log('AR Session State:', this.isARSession);
+console.log('Hit Test Results:', hitTestResults);
+```
+
+## Future Enhancements
+
+### Planned Features
+- **Gesture Controls**: Pinch to zoom, rotate Earth
+- **Multiple Earths**: Place multiple globes
+- **Flight Path Animation**: Animated flight routes
+- **Voice Commands**: Voice-controlled interactions
+- **Social Sharing**: Share AR experiences
+
+### Advanced Integration
+- **Real-time Data**: Live flight data in AR
+- **Weather Overlay**: Real weather on Earth
+- **Time Zones**: Dynamic day/night cycles
+- **Custom Markers**: Personal location pins
+
+## Development
+
+### Local Development
+1. Serve files over HTTPS (required for WebXR)
+2. Use a local server: `python -m http.server 8000`
+3. Access via `https://localhost:8000/earth-ar.html`
+
+### Testing
+- Test on physical iOS device (simulator doesn't support AR)
+- Use Safari for best iOS compatibility
+- Test various lighting conditions
+- Verify performance on different devices
+
+## Resources
+
+- [WebXR Device API Documentation](https://developer.mozilla.org/en-US/docs/Web/API/WebXR_Device_API)
+- [Three.js WebXR Examples](https://threejs.org/docs/#examples/en/webxr/AR_handling_and_displaying_a_model)
+- [Apple ARKit Documentation](https://developer.apple.com/augmented-reality/)
+- [WebXR Polyfill](https://github.com/immersive-web/webxr-polyfill) for broader compatibility
+
+## License
+
+This AR implementation is part of the existing project and follows the same licensing terms.
\ No newline at end of file
diff --git a/CNAME b/CNAME
new file mode 100644
index 0000000..7e98ee0
--- /dev/null
+++ b/CNAME
@@ -0,0 +1 @@
+patrickfreyer.com
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..37f0f21
--- /dev/null
+++ b/README.md
@@ -0,0 +1,55 @@
+# Patrick Freyer's Personal Website
+
+This is my personal website hosted on GitHub Pages. It showcases my portfolio, projects, and professional information.
+
+## Features
+
+- Modern, responsive design
+- Smooth scrolling and animations
+- Mobile-friendly navigation
+- Project showcase section
+- Contact information
+
+## Technologies Used
+
+- HTML5
+- CSS3 (with CSS Variables and Flexbox/Grid)
+- JavaScript (ES6+)
+- Font Awesome for icons
+- Google Fonts (Inter)
+
+## Development
+
+To run this project locally:
+
+1. Clone the repository:
+```bash
+git clone https://github.com/patrickfreyer/patrickfreyer.github.io.git
+```
+
+2. Navigate to the project directory:
+```bash
+cd patrickfreyer.github.io
+```
+
+3. Open `index.html` in your browser or use a local server.
+
+## Deployment
+
+This website is automatically deployed to GitHub Pages when changes are pushed to the main branch.
+
+## Customization
+
+To customize the website:
+
+1. Update the content in `index.html`
+2. Modify styles in `css/style.css`
+3. Add any additional JavaScript functionality in `js/main.js`
+
+## License
+
+MIT License - feel free to use this code for your own personal website!
+
+## Contact
+
+Feel free to reach out to me for any questions or suggestions.
\ No newline at end of file
diff --git a/README.pdf b/README.pdf
new file mode 100644
index 0000000..baf32c4
Binary files /dev/null and b/README.pdf differ
diff --git a/_config.yml b/_config.yml
new file mode 100644
index 0000000..49d7fc7
--- /dev/null
+++ b/_config.yml
@@ -0,0 +1,38 @@
+title: Patrick Freyer
+description: Personal website of Patrick Freyer, Software Developer & Technology Enthusiast
+url: https://patrickfreyer.github.io
+baseurl: ""
+
+# Build settings
+markdown: kramdown
+plugins:
+ - jekyll-feed
+
+# Include processing
+include:
+ - _includes
+ - _layouts
+ - _data
+ - assets
+ - css
+
+# Exclude from processing
+exclude:
+ - README.md
+ - LICENSE
+ - .gitignore
+ - Gemfile
+ - Gemfile.lock
+ - node_modules
+ - vendor
+
+# Sass
+sass:
+ style: compressed
+
+# Default front matter
+defaults:
+ - scope:
+ path: ""
+ values:
+ layout: default
\ No newline at end of file
diff --git a/_data/about.yaml b/_data/about.yaml
new file mode 100644
index 0000000..117f88e
--- /dev/null
+++ b/_data/about.yaml
@@ -0,0 +1,2 @@
+about_text: >-
+ GenAI builder & developer, passionate about building the products of the future and reshaping strategies to keep up with the new age of AI. Built and published a range of successful apps, ranging from podcasts to health and forestry. Always working on one more thing...
\ No newline at end of file
diff --git a/_data/flightRoutes.yaml b/_data/flightRoutes.yaml
new file mode 100644
index 0000000..108bf8d
--- /dev/null
+++ b/_data/flightRoutes.yaml
@@ -0,0 +1,3282 @@
+### 2000
+
+- origin: "Frankfurt"
+ destination: "Santo Domingo"
+ airline: "XXX"
+ year: 2000
+ month: "December"
+ occasion: "Private"
+ travelers: ["Patrick", "Regina", "Harald"]
+
+- origin: "Santo Domingo"
+ destination: "Frankfurt"
+ airline: "XXX"
+ year: 2000
+ month: "December"
+ occasion: "Private"
+ travelers: ["Patrick", "Regina", "Harald"]
+
+### 2002
+
+- origin: "Frankfurt"
+ destination: "Lisbon"
+ airline: "XXX"
+ year: 2002
+ month: "XXX"
+ occasion: "Private"
+ travelers: ["Patrick", "Regina", "Harald"]
+
+- origin: "Lisbon"
+ destination: "Frankfurt"
+ airline: "XXX"
+ year: 2002
+ month: "XXX"
+ occasion: "Private"
+ travelers: ["Patrick", "Regina", "Harald"]
+
+- origin: "Frankfurt"
+ destination: "Mombasa"
+ airline: "XXX"
+ year: 2002
+ month: "XXX"
+ occasion: "Private"
+ travelers: ["Patrick", "Regina", "Harald"]
+
+- origin: "Mombasa"
+ destination: "Masai Mara"
+ airline: "XXX"
+ year: 2002
+ month: "XXX"
+ occasion: "Private"
+ travelers: ["Patrick", "Regina", "Harald"]
+
+- origin: "Masai Mara"
+ destination: "Mombasa"
+ airline: "XXX"
+ year: 2002
+ month: "XXX"
+ occasion: "Private"
+ travelers: ["Patrick", "Regina", "Harald"]
+
+- origin: "Mombasa"
+ destination: "Frankfurt"
+ airline: "XXX"
+ year: 2002
+ month: "XXX"
+ occasion: "Private"
+ travelers: ["Patrick", "Regina", "Harald"]
+
+### 2004
+
+- origin: "Frankfurt"
+ destination: "Cape Town"
+ airline: "XXX"
+ year: 2004
+ month: "XXX"
+ occasion: "Private"
+ travelers: ["Patrick", "Regina", "Harald"]
+
+- origin: "Cape Town"
+ destination: "Frankfurt"
+ airline: "XXX"
+ year: 2004
+ month: "XXX"
+ occasion: "Private"
+ travelers: ["Patrick", "Regina", "Harald"]
+
+- origin: "Frankfurt"
+ destination: "Mauritius"
+ airline: "XXX"
+ year: 2004
+ month: "XXX"
+ occasion: "Private"
+ travelers: ["Patrick", "Regina", "Harald"]
+
+- origin: "Mauritius"
+ destination: "Frankfurt"
+ airline: "XXX"
+ year: 2004
+ month: "XXX"
+ occasion: "Private"
+ travelers: ["Patrick", "Regina", "Harald"]
+
+### 2005
+
+### 2006
+
+- origin: "Frankfurt"
+ destination: "Cairo"
+ airline: "XXX"
+ year: 2006
+ month: "XXX"
+ occasion: "Private"
+ travelers: ["Patrick", "Harald"]
+
+- origin: "Cairo"
+ destination: "Frankfurt"
+ airline: "XXX"
+ year: 2006
+ month: "XXX"
+ occasion: "Private"
+ travelers: ["Patrick", "Harald"]
+
+### 2007
+
+- origin: "Frankfurt"
+ destination: "Reykjavik"
+ airline: "XXX"
+ year: 2007
+ month: "June"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Reykjavik"
+ destination: "Frankfurt"
+ airline: "XXX"
+ year: 2007
+ month: "June"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+### 2008
+
+- origin: "Stuttgart"
+ destination: "London"
+ airline: "British Airways"
+ year: 2008
+ month: "December"
+ occasion: "Private"
+ travelers: ["Patrick", "Harald", "Pascal", "Regina", "Maria", "Manfred"]
+
+- origin: "London"
+ destination: "Cape Town"
+ airline: "British Airways"
+ year: 2008
+ month: "December"
+ occasion: "Private"
+ travelers: ["Patrick", "Harald", "Pascal", "Regina", "Maria", "Manfred"]
+
+### 2009
+
+- origin: "Cape Town"
+ destination: "London"
+ airline: "British Airways"
+ year: 2009
+ month: "January"
+ occasion: "Private"
+ travelers: ["Patrick", "Harald", "Pascal", "Regina", "Maria", "Manfred", "Zita"]
+
+- origin: "London"
+ destination: "Stuttgart"
+ airline: "British Airways"
+ year: 2009
+ month: "January"
+ occasion: "Private"
+ travelers: ["Patrick", "Harald", "Pascal", "Regina", "Maria", "Manfred", "Zita"]
+
+
+### 2013
+- origin: "Stuttgart"
+ destination: "Zurich"
+ airline: "Lufthansa"
+ year: 2013
+ month: "April"
+ occasion: "Private"
+ travelers: ["Patrick", "Harald"]
+
+- origin: "Zurich"
+ destination: "New York"
+ airline: "Lufthansa"
+ year: 2013
+ month: "April"
+ occasion: "Private"
+ travelers: ["Patrick", "Harald"]
+
+- origin: "New York"
+ destination: "Zurich"
+ airline: "Lufthansa"
+ year: 2013
+ month: "April"
+ occasion: "Private"
+ travelers: ["Patrick", "Harald"]
+
+- origin: "Zurich"
+ destination: "Stuttgart"
+ airline: "Lufthansa"
+ year: 2013
+ month: "April"
+ occasion: "Private"
+ travelers: ["Patrick", "Harald"]
+
+- origin: "Frankfurt"
+ destination: "Beijing"
+ airline: "Air China"
+ year: 2013
+ month: "July"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Beijing"
+ destination: "Xi'an"
+ airline: "Air China"
+ year: 2013
+ month: "July"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Shanghai"
+ destination: "Beijing"
+ airline: "Air China"
+ year: 2013
+ month: "July"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Beijing"
+ destination: "Frankfurt"
+ airline: "Air China"
+ year: 2013
+ month: "July"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+
+### 2014
+- origin: "Frankfurt"
+ destination: "Los Angeles"
+ airline: "Lufthansa"
+ year: 2014
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Los Angeles"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2014
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+### 2015
+
+- origin: "Brussels"
+ destination: "Valeta"
+ airline: "XXX"
+ year: 2015
+ month: "April"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Valeta"
+ destination: "Brussels"
+ airline: "XXX"
+ year: 2015
+ month: "April"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Frankfurt"
+ destination: "Cancun"
+ airline: "Condor"
+ year: 2015
+ month: "July"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Cancun"
+ destination: "Frankfurt"
+ airline: "Condor"
+ year: 2015
+ month: "July"
+
+### 2016
+- origin: "Frankfurt"
+ destination: "Singapore"
+ airline: "Lufthansa"
+ year: 2016
+ month: "January"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Singapore"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2016
+ month: "February"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Brussels"
+ destination: "Moscow"
+ airline: "Aeroflot"
+ year: 2016
+ month: "April"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "St. Petersburg"
+ destination: "Berlin"
+ airline: "Aeroflot"
+ year: 2016
+ month: "April"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Berlin"
+ destination: "Brussels"
+ airline: "Aeroflot"
+ year: 2016
+ month: "April"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Brussels"
+ destination: "Venice"
+ airline: "Ryanair"
+ year: 2016
+ month: "May"
+ occasion: "School"
+ travelers: ["Patrick"]
+
+- origin: "Venice"
+ destination: "Brussels"
+ airline: "Ryanair"
+ year: 2016
+ month: "May"
+ occasion: "School"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Miami"
+ airline: "XXX"
+ year: 2016
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Miami"
+ destination: "Belize"
+ airline: "American Airlines"
+ year: 2016
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Belize"
+ destination: "Miami"
+ airline: "American Airlines"
+ year: 2016
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Miami"
+ destination: "Frankfurt"
+ airline: "XXX"
+ year: 2016
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+### 2017
+
+- origin: "Brussels"
+ destination: "Budapest"
+ airline: "XXX"
+ year: 2017
+ month: "January"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Budapest"
+ destination: "Brussels"
+ airline: "XXX"
+ year: 2017
+ month: "January"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Frankfurt"
+ destination: "Rome"
+ airline: "XXX"
+ year: 2017
+ month: "June"
+ occasion: "Private"
+ travelers: ["Patrick", "Zita"]
+
+- origin: "Rome"
+ destination: "Frankfurt"
+ airline: "XXX"
+ year: 2017
+ month: "June"
+ occasion: "Private"
+ travelers: ["Patrick", "Zita"]
+
+- origin: "Frankfurt"
+ destination: "Singapore"
+ airline: "Lufthansa"
+ year: 2017
+ month: "July"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Singapore"
+ destination: "Kota Kinabalu"
+ airline: "AirAsia"
+ year: 2017
+ month: "July"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Kota Kinabalu"
+ destination: "Singapore"
+ airline: "AirAsia"
+ year: 2017
+ month: "July"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Singapore"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2017
+ month: "July"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Brussels"
+ destination: "Hamburg"
+ airline: "XXX"
+ year: 2017
+ month: "October"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Hamburg"
+ destination: "Brussels"
+ airline: "XXX"
+ year: 2017
+ month: "October"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Brussels"
+ destination: "Venice"
+ airline: "XXX"
+ year: 2017
+ month: "November"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Venice"
+ destination: "Brussels"
+ airline: "XXX"
+ year: 2017
+ month: "November"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+### 2018
+
+- origin: "Brussels"
+ destination: "New York"
+ airline: "Brussels Airlines"
+ year: 2018
+ month: "March"
+ occasion: "Private"
+ travelers: ["Patrick", "Julia"]
+
+- origin: "New York"
+ destination: "Brussels"
+ airline: "Brussels Airlines"
+ year: 2018
+ month: "March"
+ occasion: "Private"
+ travelers: ["Patrick", "Julia"]
+
+- origin: "Brussels"
+ destination: "Dubai"
+ airline: "Emirates"
+ year: 2018
+ month: "March"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Dubai"
+ destination: "Durban"
+ airline: "Emirates"
+ year: 2018
+ month: "March"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Durban"
+ destination: "Dubai"
+ airline: "Emirates"
+ year: 2018
+ month: "March"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Dubai"
+ destination: "Brussels"
+ airline: "Emirates"
+ year: 2018
+ month: "April"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Brussels"
+ destination: "Berlin"
+ airline: "Brussels Airlines"
+ year: 2018
+ month: "May"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Berlin"
+ destination: "Brussels"
+ airline: "Brussels Airlines"
+ year: 2018
+ month: "May"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Helsinki"
+ airline: "Finnair"
+ year: 2018
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick", "Sophia"]
+
+- origin: "Helsinki"
+ destination: "New Delhi"
+ airline: "Finnair"
+ year: 2018
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick", "Sophia"]
+
+- origin: "Jaipur"
+ destination: "Mumbai"
+ airline: "xxx"
+ year: 2018
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick", "Sophia"]
+
+- origin: "Bangalore"
+ destination: "New Delhi"
+ airline: "xxx"
+ year: 2018
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick", "Sophia"]
+
+- origin: "New Delhi"
+ destination: "Frankfurt"
+ airline: "Air India"
+ year: 2018
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick", "Sophia"]
+
+- origin: "Birmingham"
+ destination: "Stuttgart"
+ airline: "FlyBe"
+ year: 2018
+ month: "December"
+ occasion: "Warwick"
+ travelers: ["Patrick"]
+
+### 2019
+
+- origin: "Stuttgart"
+ destination: "Birmingham"
+ airline: "FlyBe"
+ year: 2019
+ month: "January"
+ occasion: "Warwick"
+ travelers: ["Patrick"]
+
+- origin: "Birmingham"
+ destination: "Stuttgart"
+ airline: "FlyBe"
+ year: 2019
+ month: "February"
+ occasion: "Warwick"
+ travelers: ["Patrick"]
+
+- origin: "Stuttgart"
+ destination: "Birmingham"
+ airline: "FlyBe"
+ year: 2019
+ month: "February"
+ occasion: "Warwick"
+ travelers: ["Patrick"]
+
+- origin: "Birmingham"
+ destination: "Stuttgart"
+ airline: "FlyBe"
+ year: 2019
+ month: "June"
+ occasion: "Warwick"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Doha"
+ airline: "Qatar"
+ year: 2019
+ month: "June"
+ occasion: "Warwick"
+ travelers: ["Patrick"]
+
+- origin: "Doha"
+ destination: "Hong Kong"
+ airline: "Qatar"
+ year: 2019
+ month: "June"
+ occasion: "Warwick"
+ travelers: ["Patrick"]
+
+- origin: "Hong Kong"
+ destination: "Bangkok"
+ airline: "Ethiopian"
+ year: 2019
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Bangkok"
+ destination: "Addis Ababa"
+ airline: "Ethiopian"
+ year: 2019
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Addis Ababa"
+ destination: "Mombasa"
+ airline: "Ethiopian"
+ year: 2019
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Mombasa"
+ destination: "Nairobi"
+ airline: "XXX"
+ year: 2019
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Nairobi"
+ destination: "Masai Mara"
+ airline: "XXX"
+ year: 2019
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Masai Mara"
+ destination: "Nairobi"
+ airline: "XXX"
+ year: 2019
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Nairobi"
+ destination: "Dubai"
+ airline: "Emirates"
+ year: 2019
+ month: "September"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Dubai"
+ destination: "Frankfurt"
+ airline: "Emirates"
+ year: 2019
+ month: "September"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Stuttgart"
+ destination: "Birmingham"
+ airline: "FlyBe"
+ year: 2019
+ month: "September"
+ occasion: "Warwick"
+ travelers: ["Patrick"]
+
+- origin: "Birmingham"
+ destination: "Stuttgart"
+ airline: "FlyBe"
+ year: 2019
+ month: "December"
+ occasion: "Warwick"
+ travelers: ["Patrick"]
+
+### 2020
+
+- origin: "Stuttgart"
+ destination: "Birmingham"
+ airline: "FlyBe"
+ year: 2020
+ month: "January"
+ occasion: "Warwick"
+ travelers: ["Patrick"]
+
+- origin: "Birmingham"
+ destination: "Dublin"
+ airline: "AerLingus"
+ year: 2020
+ month: "February"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Dublin"
+ destination: "New York"
+ airline: "AerLingus"
+ year: 2020
+ month: "February"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "New York"
+ destination: "Dublin"
+ airline: "AerLingus"
+ year: 2020
+ month: "February"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Dublin"
+ destination: "Birmingham"
+ airline: "AerLingus"
+ year: 2020
+ month: "February"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Birmingham"
+ destination: "Amsterdam"
+ airline: "KLM"
+ year: 2020
+ month: "March"
+ occasion: "Warwick"
+ travelers: ["Patrick"]
+
+- origin: "Amsterdam"
+ destination: "Stuttgart"
+ airline: "KLM"
+ year: 2020
+ month: "March"
+ occasion: "Warwick"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "London"
+ airline: "Lufthansa"
+ year: 2020
+ month: "September"
+ occasion: "Warwick"
+ travelers: ["Patrick"]
+
+- origin: "London"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2020
+ month: "December"
+ occasion: "Warwick"
+ travelers: ["Patrick"]
+
+### 2021
+
+- origin: "Frankfurt"
+ destination: "London"
+ airline: "Lufthansa"
+ year: 2021
+ month: "February"
+ occasion: "Warwick"
+ travelers: ["Patrick"]
+
+- origin: "London"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2021
+ month: "June"
+ occasion: "Warwick"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Athens"
+ airline: "Lufthansa"
+ year: 2021
+ month: "June"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Athens"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2021
+ month: "June"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Frankfurt"
+ destination: "New York"
+ airline: "Lufthansa"
+ year: 2021
+ month: "August"
+ occasion: "Yale"
+ travelers: ["Patrick"]
+
+- origin: "New York"
+ destination: "Aruba"
+ airline: "United"
+ year: 2021
+ month: "October"
+ occasion: "Private"
+ travelers: ["Patrick", "Fabio", "Rutvik"]
+
+- origin: "Aruba"
+ destination: "New York"
+ airline: "United"
+ year: 2021
+ month: "October"
+ occasion: "Private"
+ travelers: ["Patrick", "Fabio", "Rutvik"]
+
+- origin: "New York"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2021
+ month: "December"
+ occasion: "Yale"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Nursultan"
+ airline: "Lufthansa"
+ year: 2021
+ month: "December"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+### 2022
+
+- origin: "Nursultan"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2022
+ month: "January"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Frankfurt"
+ destination: "New York"
+ airline: "Singapore Airlines"
+ year: 2022
+ month: "January"
+ occasion: "Yale"
+ travelers: ["Patrick"]
+
+- origin: "New York"
+ destination: "Panama"
+ airline: "Copa Airlines"
+ year: 2022
+ month: "March"
+ occasion: "Yale"
+ travelers: ["Patrick"]
+
+- origin: "Panama"
+ destination: "Montevideo"
+ airline: "Copa Airlines"
+ year: 2022
+ month: "March"
+ occasion: "Yale"
+ travelers: ["Patrick"]
+
+- origin: "Montevideo"
+ destination: "Panama"
+ airline: "Copa Airlines"
+ year: 2022
+ month: "March"
+ occasion: "Yale"
+ travelers: ["Patrick"]
+
+- origin: "Panama"
+ destination: "New York"
+ airline: "Copa Airlines"
+ year: 2022
+ month: "March"
+ occasion: "Yale"
+ travelers: ["Patrick"]
+
+- origin: "New York"
+ destination: "Frankfurt"
+ airline: "Singapore Airlines"
+ year: 2022
+ month: "June"
+ occasion: "Yale"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Stockholm"
+ airline: "Lufthansa"
+ year: 2022
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Stockholm"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2022
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Copenhagen"
+ airline: "Lufthansa"
+ year: 2022
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Copenhagen"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2022
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Copenhagen"
+ airline: "Lufthansa"
+ year: 2022
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Copenhagen"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2022
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Copenhagen"
+ airline: "Lufthansa"
+ year: 2022
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Copenhagen"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2022
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Berlin"
+ airline: "Lufthansa"
+ year: 2022
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "San Francisco"
+ airline: "Condor"
+ year: 2022
+ month: "September"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Oakland"
+ destination: "Big Island"
+ airline: "Southwest"
+ year: 2022
+ month: "September"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Big Island"
+ destination: "Honolulu"
+ airline: "Southwest"
+ year: 2022
+ month: "September"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Honolulu"
+ destination: "Oakland"
+ airline: "Southwest"
+ year: 2022
+ month: "September"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "San Francisco"
+ destination: "Frankfurt"
+ airline: "Condor"
+ year: 2022
+ month: "September"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald"]
+
+- origin: "Munich"
+ destination: "Düsseldorf"
+ airline: "Eurowings"
+ year: 2022
+ month: "October"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Düsseldorf"
+ destination: "London"
+ airline: "Eurowings"
+ year: 2022
+ month: "October"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "London"
+ destination: "Cologne"
+ airline: "Eurowings"
+ year: 2022
+ month: "October"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Cologne"
+ destination: "Munich"
+ airline: "Eurowings"
+ year: 2022
+ month: "October"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Düsseldorf"
+ airline: "Lufthansa"
+ year: 2022
+ month: "October"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Düsseldorf"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2022
+ month: "October"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Düsseldorf"
+ airline: "Lufthansa"
+ year: 2022
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Düsseldorf"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2022
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Düsseldorf"
+ airline: "Lufthansa"
+ year: 2022
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Düsseldorf"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2022
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Düsseldorf"
+ airline: "Lufthansa"
+ year: 2022
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Düsseldorf"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2022
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Düsseldorf"
+ airline: "Lufthansa"
+ year: 2022
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Düsseldorf"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2022
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Düsseldorf"
+ airline: "Lufthansa"
+ year: 2022
+ month: "December"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Düsseldorf"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2022
+ month: "December"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Düsseldorf"
+ airline: "Lufthansa"
+ year: 2022
+ month: "December"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Düsseldorf"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2022
+ month: "December"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Doha"
+ airline: "Qatar"
+ year: 2022
+ month: "December"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Doha"
+ destination: "Muscat"
+ airline: "Qatar"
+ year: 2022
+ month: "December"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+### 2023
+
+- origin: "Muscat"
+ destination: "Doha"
+ airline: "Qatar"
+ year: 2023
+ month: "January"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Doha"
+ destination: "Frankfurt"
+ airline: "Qatar"
+ year: 2023
+ month: "January"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Munich"
+ destination: "Düsseldorf"
+ airline: "Lufthansa"
+ year: 2023
+ month: "January"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Düsseldorf"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2023
+ month: "January"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Vienna"
+ airline: "Lufthansa"
+ year: 2023
+ month: "February"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Vienna"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2023
+ month: "February"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2023
+ month: "February"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Riyadh"
+ airline: "Lufthansa"
+ year: 2023
+ month: "February"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Riyadh"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2023
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2023
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2023
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Riyadh"
+ airline: "Lufthansa"
+ year: 2023
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Riyadh"
+ destination: "Dubai"
+ airline: "FlyDubai"
+ year: 2023
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dubai"
+ destination: "Riyadh"
+ airline: "Saudi"
+ year: 2023
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Riyadh"
+ destination: "Dubai"
+ airline: "Emirates"
+ year: 2023
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dubai"
+ destination: "Kuwait"
+ airline: "Emirates"
+ year: 2023
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Kuwait"
+ destination: "Riyadh"
+ airline: "Saudi"
+ year: 2023
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Riyadh"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2023
+ month: "April"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Riyadh"
+ airline: "Lufthansa"
+ year: 2023
+ month: "April"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Riyadh"
+ destination: "Dammam"
+ airline: "Saudi"
+ year: 2023
+ month: "April"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dammam"
+ destination: "Riyadh"
+ airline: "Saudi"
+ year: 2023
+ month: "April"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Riyadh"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2023
+ month: "April"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2023
+ month: "April"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2023
+ month: "April"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Washington DC"
+ airline: "United"
+ year: 2023
+ month: "April"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Washington DC"
+ destination: "Munich"
+ airline: "United"
+ year: 2023
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2023
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Riyadh"
+ airline: "Lufthansa"
+ year: 2023
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Riyadh"
+ destination: "Doha"
+ airline: "Qatar"
+ year: 2023
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Doha"
+ destination: "Abu Dhabi"
+ airline: "Qatar"
+ year: 2023
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Abu Dhabi"
+ destination: "Riyadh"
+ airline: "Etihad"
+ year: 2023
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Riyadh"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2023
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2023
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2023
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Dubai"
+ airline: "Lufthansa"
+ year: 2023
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Doha"
+ destination: "Riyadh"
+ airline: "Qatar"
+ year: 2023
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Riyadh"
+ destination: "Tabuk"
+ airline: "Saudia"
+ year: 2023
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Tabuk"
+ destination: "Riyadh"
+ airline: "Saudia"
+ year: 2023
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Riyadh"
+ destination: "Dubai"
+ airline: "Saudia"
+ year: 2023
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dubai"
+ destination: "Zurich"
+ airline: "Swiss"
+ year: 2023
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Zurich"
+ destination: "Munich"
+ airline: "Swiss"
+ year: 2023
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2023
+ month: "June"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Riyadh"
+ airline: "Lufthansa"
+ year: 2023
+ month: "June"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Riyadh"
+ destination: "Dubai"
+ airline: "Saudia"
+ year: 2023
+ month: "June"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dubai"
+ destination: "Riyadh"
+ airline: "Saudia"
+ year: 2023
+ month: "June"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Riyadh"
+ destination: "Arar"
+ airline: "Saudia"
+ year: 2023
+ month: "June"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Arar"
+ destination: "Riyadh"
+ airline: "Saudia"
+ year: 2023
+ month: "June"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Riyadh"
+ destination: "Dubai"
+ airline: "Saudia"
+ year: 2023
+ month: "June"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dubai"
+ destination: "Tabuk"
+ airline: "Saudia"
+ year: 2023
+ month: "June"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Tabuk"
+ destination: "Dubai"
+ airline: "Saudia"
+ year: 2023
+ month: "June"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dubai"
+ destination: "Zurich"
+ airline: "Swiss"
+ year: 2023
+ month: "June"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Zurich"
+ destination: "Munich"
+ airline: "Swiss"
+ year: 2023
+ month: "June"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Zurich"
+ airline: "Swiss"
+ year: 2023
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Zurich"
+ destination: "Dubai"
+ airline: "Swiss"
+ year: 2023
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Abu Dhabi"
+ destination: "Riyadh"
+ airline: "Etihad"
+ year: 2023
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Riyadh"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2023
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Riyadh"
+ airline: "Lufthansa"
+ year: 2023
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Riyadh"
+ destination: "Muscat"
+ airline: "Omanair"
+ year: 2023
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Muscat"
+ destination: "Dubai"
+ airline: "Omanair"
+ year: 2023
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dubai"
+ destination: "Dammam"
+ airline: "Saudia"
+ year: 2023
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dammam"
+ destination: "Riyadh"
+ airline: "Saudia"
+ year: 2023
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Riyadh"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2023
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2023
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Budapest"
+ airline: "Lufthansa"
+ year: 2023
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Budapest"
+ destination: "Vienna"
+ airline: "Austrian"
+ year: 2023
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Vienna"
+ destination: "Munich"
+ airline: "Austrian"
+ year: 2023
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Dublin"
+ airline: "Air Lingus"
+ year: 2023
+ month: "August"
+ occasion: "Rowing"
+ travelers: ["Patrick"]
+
+- origin: "Dublin"
+ destination: "Munich"
+ airline: "Air Lingus"
+ year: 2023
+ month: "August"
+ occasion: "Rowing"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Zurich"
+ airline: "Swiss"
+ year: 2023
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Zurich"
+ destination: "Dubai"
+ airline: "Swiss"
+ year: 2023
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dubai"
+ destination: "Riyadh"
+ airline: "Saudia"
+ year: 2023
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Riyadh"
+ destination: "Dubai"
+ airline: "Saudia"
+ year: 2023
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dubai"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2023
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2023
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Brussels"
+ airline: "Lufthansa"
+ year: 2023
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Brussels"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2023
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Zurich"
+ airline: "Swiss"
+ year: 2023
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Zurich"
+ destination: "Dubai"
+ airline: "Swiss"
+ year: 2023
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dubai"
+ destination: "Riyadh"
+ airline: "Saudia"
+ year: 2023
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Riyadh"
+ destination: "Dubai"
+ airline: "Saudia"
+ year: 2023
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dubai"
+ destination: "Riyadh"
+ airline: "Emirates"
+ year: 2023
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Riyadh"
+ destination: "Dubai"
+ airline: "FlyDubai"
+ year: 2023
+ month: "October"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dubai"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2023
+ month: "October"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2023
+ month: "October"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Vienna"
+ airline: "Lufthansa"
+ year: 2023
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Vienna"
+ destination: "Berlin"
+ airline: "Lufthansa"
+ year: 2023
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Berlin"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2023
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Berlin"
+ airline: "Lufthansa"
+ year: 2023
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Berlin"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2023
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Doha"
+ airline: "Qatar"
+ year: 2023
+ month: "November"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Doha"
+ destination: "Hanoi"
+ airline: "Qatar"
+ year: 2023
+ month: "November"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Hanoi"
+ destination: "Da Nang"
+ airline: "Vietjet"
+ year: 2023
+ month: "December"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Da Nang"
+ destination: "Ho Chi Minh"
+ airline: "Bamboo Airways"
+ year: 2023
+ month: "December"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Ho Chi Minh"
+ destination: "Phu Quoc"
+ airline: "Vietjet"
+ year: 2023
+ month: "December"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Phu Quoc"
+ destination: "Hanoi"
+ airline: "Vietjet"
+ year: 2023
+ month: "December"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Hanoi"
+ destination: "Doha"
+ airline: "Qatar"
+ year: 2023
+ month: "December"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Doha"
+ destination: "Munich"
+ airline: "Qatar"
+ year: 2023
+ month: "December"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Munich"
+ destination: "Hamburg"
+ airline: "Lufthansa"
+ year: 2023
+ month: "December"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Hamburg"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2023
+ month: "December"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "London"
+ airline: "Lufthansa"
+ year: 2023
+ month: "December"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "London"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2023
+ month: "December"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Doha"
+ airline: "Qatar"
+ year: 2023
+ month: "December"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Doha"
+ destination: "Almaty"
+ airline: "Qatar"
+ year: 2023
+ month: "December"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+### 2024
+
+- origin: "Almaty"
+ destination: "Doha"
+ airline: "Qatar"
+ year: 2024
+ month: "January"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Doha"
+ destination: "Dubai"
+ airline: "Qatar"
+ year: 2024
+ month: "January"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dubai"
+ destination: "Munich"
+ airline: "Emirates"
+ year: 2024
+ month: "January"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Dubai"
+ airline: "Emirates"
+ year: 2024
+ month: "January"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dubai"
+ destination: "Riyadh"
+ airline: "Saudia"
+ year: 2024
+ month: "February"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Riyadh"
+ destination: "Dubai"
+ airline: "Saudia"
+ year: 2024
+ month: "February"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dubai"
+ destination: "Munich"
+ airline: "Emirates"
+ year: 2024
+ month: "February"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Dubai"
+ airline: "Emirates"
+ year: 2024
+ month: "February"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dubai"
+ destination: "Munich"
+ airline: "Emirates"
+ year: 2024
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Dubai"
+ airline: "Emirates"
+ year: 2024
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dubai"
+ destination: "Munich"
+ airline: "Emirates"
+ year: 2024
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2024
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Seattle"
+ airline: "Lufthansa"
+ year: 2024
+ month: "March"
+ occasion: "Projects"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Seattle"
+ destination: "Seattle"
+ airline: "Patrick"
+ year: 2024
+ month: "March"
+ occasion: "Pilots License"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Seattle"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2024
+ month: "April"
+ occasion: "Projects"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Munich"
+ destination: "London"
+ airline: "Lufthansa"
+ year: 2024
+ month: "May"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "London"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2024
+ month: "May"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Düsseldorf"
+ airline: "Lufthansa"
+ year: 2024
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Düsseldorf"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2024
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Hamburg"
+ airline: "Lufthansa"
+ year: 2024
+ month: "June"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Hamburg"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2024
+ month: "June"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Barcelona"
+ airline: "Lufthansa"
+ year: 2024
+ month: "June"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Barcelona"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2024
+ month: "June"
+ occasion: "Work"
+ travelers: ["Patrick", "Michael"]
+
+- origin: "Munich"
+ destination: "Paris"
+ airline: "AirFrance"
+ year: 2024
+ month: "June"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Paris"
+ destination: "Marseille"
+ airline: "AirFrance"
+ year: 2024
+ month: "June"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Marseille"
+ destination: "Paris"
+ airline: "AirFrance"
+ year: 2024
+ month: "June"
+ occasion: "Private"
+
+- origin: "Paris"
+ destination: "Munich"
+ airline: "AirFrance"
+ year: 2024
+ month: "June"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Munich"
+ destination: "Hamburg"
+ airline: "Lufthansa"
+ year: 2024
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Hamburg"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2024
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Bodrum"
+ airline: "Eurowings Discover"
+ year: 2024
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya", "Masha"]
+
+- origin: "Bodrum"
+ destination: "Munich"
+ airline: "Eurowings Discover"
+ year: 2024
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Munich"
+ destination: "Vienna"
+ airline: "Austrian"
+ year: 2024
+ month: "August"
+ occasion: "Yale"
+ travelers: ["Patrick"]
+
+- origin: "Vienna"
+ destination: "New York"
+ airline: "Austrian"
+ year: 2024
+ month: "August"
+ occasion: "Yale"
+ travelers: ["Patrick"]
+
+- origin: "New York"
+ destination: "Hong Kong"
+ airline: "Cathay Pacific"
+ year: 2024
+ month: "September"
+ occasion: "Rowing"
+ travelers: ["Patrick"]
+
+- origin: "Hong Kong"
+ destination: "Chengdu"
+ airline: "Air China"
+ year: 2024
+ month: "September"
+ occasion: "Rowing"
+ travelers: ["Patrick"]
+
+- origin: "Chengdu"
+ destination: "Hong Kong"
+ airline: "Air China"
+ year: 2024
+ month: "September"
+ occasion: "Rowing"
+ travelers: ["Patrick"]
+
+- origin: "Hong Kong"
+ destination: "New York"
+ airline: "Cathay Pacific"
+ year: 2024
+ month: "September"
+ occasion: "Rowing"
+ travelers: ["Patrick"]
+
+- origin: "New York"
+ destination: "Denver"
+ airline: "Delta"
+ year: 2024
+ month: "October"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Denver"
+ destination: "New York"
+ airline: "Delta"
+ year: 2024
+ month: "October"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "New York"
+ destination: "Beijing"
+ airline: "Air China"
+ year: 2024
+ month: "November"
+ occasion: "Rowing"
+ travelers: ["Patrick"]
+
+- origin: "Beijing"
+ destination: "Shenzhen"
+ airline: "Air China"
+ year: 2024
+ month: "November"
+ occasion: "Rowing"
+ travelers: ["Patrick", "Ioanna", "Jay", "Alex", "Bastian"]
+
+- origin: "Shenzhen"
+ destination: "Beijing"
+ airline: "Air China"
+ year: 2024
+ month: "November"
+ occasion: "Rowing"
+ travelers: ["Patrick", "Ioanna", "Jay", "Alex", "Bastian"]
+
+- origin: "Beijing"
+ destination: "New York"
+ airline: "Air China"
+ year: 2024
+ month: "November"
+ occasion: "Rowing"
+ travelers: ["Patrick", "Ioanna", "Jay", "Alex", "Bastian"]
+
+- origin: "New York"
+ destination: "Toronto"
+ airline: "Air Canada"
+ year: 2024
+ month: "November"
+ occasion: "Projects"
+ travelers: ["Patrick"]
+
+- origin: "Toronto"
+ destination: "Halifax"
+ airline: "Air Canada"
+ year: 2024
+ month: "November"
+ occasion: "Projects"
+ travelers: ["Patrick"]
+
+- origin: "Halifax"
+ destination: "Toronto"
+ airline: "Air Canada"
+ year: 2024
+ month: "November"
+ occasion: "Projects"
+ travelers: ["Patrick"]
+
+- origin: "Toronto"
+ destination: "New York"
+ airline: "Air Canada"
+ year: 2024
+ month: "November"
+ occasion: "Projects"
+ travelers: ["Patrick"]
+
+- origin: "New York"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2024
+ month: "December"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+### 2025
+
+- origin: "Munich"
+ destination: "Cologne"
+ airline: "Lufthansa"
+ year: 2025
+ month: "January"
+ occasion: "Projects"
+ travelers: ["Patrick"]
+
+- origin: "Cologne"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2025
+ month: "January"
+ occasion: "Projects"
+ travelers: ["Patrick", "Matthias"]
+
+- origin: "Munich"
+ destination: "New York"
+ airline: "Lufthansa"
+ year: 2025
+ month: "January"
+ occasion: "Yale"
+ travelers: ["Patrick"]
+
+- origin: "New York"
+ destination: "San Diego"
+ airline: "Delta"
+ year: 2025
+ month: "February"
+ occasion: "Projects"
+ travelers: ["Patrick"]
+
+- origin: "San Diego"
+ destination: "New York"
+ airline: "Delta"
+ year: 2025
+ month: "March"
+ occasion: "Projects"
+ travelers: ["Patrick"]
+
+- origin: "New York"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2025
+ month: "March"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Madrid"
+ airline: "Lufthansa"
+ year: 2025
+ month: "March"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Madrid"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2025
+ month: "March"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Frankfurt"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2025
+ month: "March"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Munich"
+ destination: "New York"
+ airline: "Lufthansa"
+ year: 2025
+ month: "March"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2025
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Vienna"
+ airline: "Austrian"
+ year: 2025
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Istanbul"
+ airline: "Turkish Airlines"
+ year: 2025
+ month: "June"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "Istanbul"
+ destination: "Astana"
+ airline: "Turkish Airlines"
+ year: 2025
+ month: "June"
+ occasion: "Private"
+ travelers: ["Patrick", "Pascal", "Regina", "Harald","Eric", "Felix", "Max", "Benjamin"]
+
+- origin: "Astana"
+ destination: "Istanbul"
+ airline: "Turkish Airlines"
+ year: 2025
+ month: "June"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Istanbul"
+ destination: "Munich"
+ airline: "Turkish Airlines"
+ year: 2025
+ month: "June"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Vienna"
+ destination: "Frankfurt"
+ airline: "Austrian"
+ year: 2025
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Frankfurt"
+ destination: "Denver"
+ airline: "Lufthansa"
+ year: 2025
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "New York"
+ airline: "United"
+ year: 2025
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "New York"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Boston"
+ airline: "United"
+ year: 2025
+ month: "July"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Boston"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Washington DC"
+ airline: "United"
+ year: 2025
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Washington DC"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Chicago"
+ airline: "United"
+ year: 2025
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Chicago"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Raleigh Durham"
+ airline: "United"
+ year: 2025
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Raleigh Durham"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "London"
+ airline: "United"
+ year: 2025
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "London"
+ destination: "New York"
+ airline: "United"
+ year: 2025
+ month: "August"
+ occasion: "Private"
+ travelers: ["Patrick"]
+
+- origin: "New York"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "August"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Toronto"
+ airline: "United"
+ year: 2025
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Toronto"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Toronto"
+ airline: "United"
+ year: 2025
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Toronto"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Toronto"
+ airline: "United"
+ year: 2025
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Toronto"
+ destination: "New Delhi"
+ airline: "Air Canada"
+ year: 2025
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "New Delhi"
+ destination: "Goa"
+ airline: "Air India"
+ year: 2025
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Goa"
+ destination: "New Delhi"
+ airline: "Air India"
+ year: 2025
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Goa"
+ destination: "New Delhi"
+ airline: "Air India"
+ year: 2025
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "New Delhi"
+ destination: "Toronto"
+ airline: "Air Canada"
+ year: 2025
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Toronto"
+ destination: "Denver"
+ airline: "Air Canada"
+ year: 2025
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Mexico City"
+ airline: "Delta"
+ year: 2025
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Mexico City"
+ destination: "Dallas"
+ airline: "United"
+ year: 2025
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dallas"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Washington DC"
+ airline: "United"
+ year: 2025
+ month: "September"
+ occasion: "Work"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Washington DC"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "October"
+ occasion: "Work"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Denver"
+ destination: "London"
+ airline: "United"
+ year: 2025
+ month: "October"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "London"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "October"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Toronto"
+ airline: "United"
+ year: 2025
+ month: "October"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Toronto"
+ destination: "San Francisco"
+ airline: "United"
+ year: 2025
+ month: "October"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "San Francisco"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "October"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Toronto"
+ airline: "United"
+ year: 2025
+ month: "October"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Toronto"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "October"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+# Week of October 20th
+- origin: "Denver"
+ destination: "Toronto"
+ airline: "United"
+ year: 2025
+ month: "October"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Toronto"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "October"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+# Week of October 27th
+- origin: "Denver"
+ destination: "Toronto"
+ airline: "United"
+ year: 2025
+ month: "October"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Toronto"
+ destination: "San Francisco"
+ airline: "United"
+ year: 2025
+ month: "October"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "San Francisco"
+ destination: "Boston"
+ airline: "United"
+ year: 2025
+ month: "October"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+# Week of November 3rd
+- origin: "Boston"
+ destination: "Orlando"
+ airline: "JetBlue"
+ year: 2025
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Orlando"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Denver"
+ destination: "New York"
+ airline: "United"
+ year: 2025
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "New York"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "New York"
+ airline: "United"
+ year: 2025
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "New York"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Denver"
+ destination: "San Francisco"
+ airline: "United"
+ year: 2025
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "San Francisco"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "November"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Toronto"
+ airline: "United"
+ year: 2025
+ month: "Air Canada"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Toronto"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "December"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Munich"
+ airline: "United"
+ year: 2025
+ month: "December"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Berlin"
+ airline: "Lufthansa"
+ year: 2025
+ month: "December"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Berlin"
+ destination: "Stuttgart"
+ airline: "Eurowings"
+ year: 2025
+ month: "December"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "December"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Miami"
+ airline: "United"
+ year: 2025
+ month: "December"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Miami"
+ destination: "Denver"
+ airline: "United"
+ year: 2025
+ month: "December"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "New York"
+ airline: "United"
+ year: 2025
+ month: "December"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+### 2026
+
+- origin: "New York"
+ destination: "Denver"
+ airline: "United"
+ year: 2026
+ month: "January"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Dallas"
+ airline: "United"
+ year: 2025
+ month: "January"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Dallas"
+ destination: "Denver"
+ airline: "United"
+ year: 2026
+ month: "January"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "New York"
+ airline: "United"
+ year: 2026
+ month: "January"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "New York"
+ destination: "Denver"
+ airline: "United"
+ year: 2026
+ month: "January"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "London"
+ destination: "Munich"
+ airline: "Lufthansa"
+ year: 2026
+ month: "February"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Denver"
+ airline: "United"
+ year: 2026
+ month: "February"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "New York"
+ airline: "United"
+ year: 2026
+ month: "February"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "New York"
+ destination: "Munich"
+ airline: "United"
+ year: 2026
+ month: "February"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Stuttgart"
+ airline: "Lufthansa"
+ year: 2026
+ month: "February"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Denver"
+ airline: "United"
+ year: 2026
+ month: "February"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Miami"
+ airline: "United"
+ year: 2026
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Miami"
+ destination: "New York"
+ airline: "United"
+ year: 2026
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "New York"
+ destination: "Denver"
+ airline: "United"
+ year: 2026
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Houston"
+ airline: "United"
+ year: 2026
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Houston"
+ destination: "New York"
+ airline: "United"
+ year: 2026
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "New York"
+ destination: "Denver"
+ airline: "United"
+ year: 2026
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Miami"
+ airline: "United"
+ year: 2026
+ month: "March"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Miami"
+ destination: "New York"
+ airline: "United"
+ year: 2026
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "New York"
+ destination: "Denver"
+ airline: "United"
+ year: 2026
+ month: "March"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Los Angeles"
+ airline: "United"
+ year: 2026
+ month: "March"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Los Angeles"
+ destination: "Hong Kong"
+ airline: "United"
+ year: 2026
+ month: "March"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Hong Kong"
+ destination: "Phu Quoc"
+ airline: "Hong Kong Airlines"
+ year: 2026
+ month: "March"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Phu Quoc"
+ destination: "Hong Kong"
+ airline: "Hong Kong Airlines"
+ year: 2026
+ month: "April"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Hong Kong"
+ destination: "San Francisco"
+ airline: "United"
+ year: 2026
+ month: "April"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "San Francisco"
+ destination: "Denver"
+ airline: "United"
+ year: 2026
+ month: "April"
+ occasion: "Private"
+ travelers: ["Patrick", "Sofya"]
+
+- origin: "Denver"
+ destination: "Calgary"
+ airline: "United"
+ year: 2026
+ month: "April"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Calgary"
+ destination: "San Francisco"
+ airline: "United"
+ year: 2026
+ month: "April"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "San Francisco"
+ destination: "Denver"
+ airline: "United"
+ year: 2026
+ month: "April"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Denver"
+ destination: "Munich"
+ airline: "United"
+ year: 2026
+ month: "April"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Frankfurt"
+ airline: "Lufthansa"
+ year: 2026
+ month: "April"
+ occasion: "Work"
+ travelers: ["Patrick"]
+
+- origin: "Munich"
+ destination: "Denver"
+ airline: "United"
+ year: 2026
+ month: "May"
+ occasion: "Work"
+ travelers: ["Patrick"]
diff --git a/_data/heroLocations.yaml b/_data/heroLocations.yaml
new file mode 100644
index 0000000..6b43653
--- /dev/null
+++ b/_data/heroLocations.yaml
@@ -0,0 +1,315 @@
+- name: "New Haven"
+ lat: 41.3083
+ lon: -72.9279
+- name: "Coventry"
+ lat: 52.4068
+ lon: -1.5197
+- name: "London"
+ lat: 51.5074
+ lon: -0.1278
+- name: "Munich"
+ lat: 48.1351
+ lon: 11.5820
+- name: "Brussels"
+ lat: 50.8503
+ lon: 4.3517
+- name: "Singapore"
+ lat: 1.3521
+ lon: 103.8198
+- name: "Hong Kong"
+ lat: 22.3193
+ lon: 114.1694
+- name: "Dubai"
+ lat: 25.2048
+ lon: 55.2708
+- name: "Riyadh"
+ lat: 24.7136
+ lon: 46.6753
+- name: "Denver"
+ lat: 39.7392
+ lon: -104.9903
+- name: "Toulouse"
+ lat: 43.6047
+ lon: 1.4442
+- name: "Den Haag"
+ lat: 52.0705
+ lon: 4.3007
+- name: "Muscat"
+ lat: 23.5859
+ lon: 58.4059
+- name: "Doha"
+ lat: 25.2854
+ lon: 51.5310
+- name: "Frankfurt"
+ lat: 50.0379
+ lon: 8.5622
+- name: "Düsseldorf"
+ lat: 51.2895
+ lon: 6.7668
+- name: "Vienna"
+ lat: 48.1200
+ lon: 16.5700
+- name: "Kuwait"
+ lat: 29.2403
+ lon: 47.9698
+- name: "Dammam"
+ lat: 26.4207
+ lon: 50.0888
+- name: "Washington DC"
+ lat: 38.9072
+ lon: -77.0369
+- name: "Abu Dhabi"
+ lat: 24.4539
+ lon: 54.6778
+- name: "Tabuk"
+ lat: 28.3835
+ lon: 36.5662
+- name: "Zurich"
+ lat: 47.4502
+ lon: 8.5616
+- name: "Arar"
+ lat: 30.9750
+ lon: 41.0382
+- name: "Budapest"
+ lat: 47.4979
+ lon: 19.0402
+- name: "Dublin"
+ lat: 53.4264
+ lon: -6.2499
+- name: "Berlin"
+ lat: 52.5200
+ lon: 13.4050
+- name: "Hanoi"
+ lat: 21.0285
+ lon: 105.8542
+- name: "Da Nang"
+ lat: 16.0544
+ lon: 108.2022
+- name: "Ho Chi Minh"
+ lat: 10.8231
+ lon: 106.6297
+- name: "Phu Quoc"
+ lat: 10.2276
+ lon: 103.9809
+- name: "Hamburg"
+ lat: 53.6302
+ lon: 10.0041
+- name: "Almaty"
+ lat: 43.2551
+ lon: 76.9126
+- name: "Nursultan"
+ lat: 51.0924
+ lon: 71.4304
+- name: "Astana"
+ lat: 51.0924
+ lon: 71.4304
+- name: "New York"
+ lat: 40.7128
+ lon: -74.0060
+- name: "Panama"
+ lat: 8.9824
+ lon: -79.5199
+- name: "Montevideo"
+ lat: -34.9011
+ lon: -56.1645
+- name: "Stockholm"
+ lat: 59.3293
+ lon: 18.0686
+- name: "Copenhagen"
+ lat: 55.6761
+ lon: 12.5683
+- name: "San Francisco"
+ lat: 37.7749
+ lon: -122.4194
+- name: "Oakland"
+ lat: 37.8044
+ lon: -122.2712
+- name: "Big Island"
+ lat: 19.8968
+ lon: -155.5828
+- name: "Honolulu"
+ lat: 21.3069
+ lon: -157.8583
+- name: "Cologne"
+ lat: 50.9375
+ lon: 6.9603
+- name: "Seattle"
+ lat: 47.6062
+ lon: -122.3321
+- name: "Barcelona"
+ lat: 41.3851
+ lon: 2.1734
+- name: "Bodrum"
+ lat: 37.0343
+ lon: 27.4305
+- name: "Chengdu"
+ lat: 30.5728
+ lon: 103.8652
+- name: "Beijing"
+ lat: 39.9042
+ lon: 116.4074
+- name: "Shenzhen"
+ lat: 22.5431
+ lon: 114.0579
+- name: "Toronto"
+ lat: 43.6532
+ lon: -79.3832
+- name: "Halifax"
+ lat: 44.6488
+ lon: -63.5752
+- name: "Aruba"
+ lat: 12.5211
+ lon: -69.9683
+- name: "San Diego"
+ lat: 32.7157
+ lon: -117.1611
+- name: "Madrid"
+ lat: 40.4168
+ lon: -3.7038
+- name: "Xi'an"
+ lat: 34.3416
+ lon: 108.9398
+- name: "Shanghai"
+ lat: 31.2304
+ lon: 121.4737
+- name: "Borneo"
+ lat: 0.9619
+ lon: 114.5548
+- name: "Durban"
+ lat: -29.8587
+ lon: 31.0218
+- name: "Helsinki"
+ lat: 60.1699
+ lon: 24.9384
+- name: "New Delhi"
+ lat: 28.6139
+ lon: 77.2090
+- name: "Jaipur"
+ lat: 26.9124
+ lon: 75.7873
+- name: "Mumbai"
+ lat: 19.0760
+ lon: 72.8777
+- name: "Bangalore"
+ lat: 12.9716
+ lon: 77.5946
+- name: "Bangkok"
+ lat: 13.7563
+ lon: 100.5018
+- name: "Addis Ababa"
+ lat: 9.0320
+ lon: 38.7492
+- name: "Mombasa"
+ lat: -4.0435
+ lon: 39.6682
+- name: "Nairobi"
+ lat: -1.2921
+ lon: 36.8219
+- name: "Masai Mara"
+ lat: -1.5437
+ lon: 35.1233
+- name: "Birmingham"
+ lat: 52.4862
+ lon: -1.8904
+- name: "Geneva"
+ lat: 46.2044
+ lon: 6.1432
+- name: "Istanbul"
+ lat: 41.0082
+ lon: 28.9784
+- name: "Cairo"
+ lat: 30.0444
+ lon: 31.2357
+- name: "Rome"
+ lat: 41.9028
+ lon: 12.4964
+- name: "Edinburgh"
+ lat: 55.9533
+ lon: -3.1883
+- name: "Athens"
+ lat: 37.9838
+ lon: 23.7275
+- name: "Stuttgart"
+ lat: 48.7758
+ lon: 9.1829
+- name: "Amsterdam"
+ lat: 52.3676
+ lon: 4.9041
+- name: "Paris"
+ lat: 48.8566
+ lon: 2.3522
+- name: "Marseille"
+ lat: 43.2965
+ lon: 5.3698
+- name: "Cancun"
+ lat: 21.1619
+ lon: -86.8515
+- name: "St. Petersburg"
+ lat: 59.9311
+ lon: 30.3609
+- name: "Moscow"
+ lat: 55.7558
+ lon: 37.6173
+- name: "Venice"
+ lat: 45.4408
+ lon: 12.3155
+- name: "Los Angeles"
+ lat: 34.0522
+ lon: -118.2437
+- name: "Miami"
+ lat: 25.7617
+ lon: -80.1918
+- name: "Belize"
+ lat: 17.1899
+ lon: -88.4976
+- name: "Kota Kinabalu"
+ lat: 5.9804
+ lon: 116.0735
+- name: "Valeta"
+ lat: 35.8989
+ lon: 14.5146
+- name: "Cape Town"
+ lat: -33.9249
+ lon: 18.4241
+- name: "Boston"
+ lat: 42.3656
+ lon: -71.0096
+- name: "Santo Domingo"
+ lat: 18.4861
+ lon: -69.9312
+- name: "Lisbon"
+ lat: 38.7223
+ lon: -9.1393
+- name: "Mauritius"
+ lat: -20.3484
+ lon: 57.5522
+- name: "Cagliari"
+ lat: 39.2238
+ lon: 9.1217
+- name: "Reykjavik"
+ lat: 64.1466
+ lon: -21.9426
+- name: "Chicago"
+ lat: 41.8781
+ lon: -87.6298
+- name: "Raleigh Durham"
+ lat: 35.7796
+ lon: -78.6382
+- name: "Goa"
+ lat: 15.4909
+ lon: 73.8278
+- name: "Mexico City"
+ lat: 19.4326
+ lon: -99.1332
+- name: "Dallas"
+ lat: 32.7767
+ lon: -96.7970
+- name: "Orlando"
+ lat: 28.5383
+ lon: -81.3792
+- name: "Houston"
+ lat: 29.7604
+ lon: -95.3698
+- name: "Calgary"
+ lat: 51.0447
+ lon: -114.0719
diff --git a/_data/social.yaml b/_data/social.yaml
new file mode 100644
index 0000000..e2e60e8
--- /dev/null
+++ b/_data/social.yaml
@@ -0,0 +1,27 @@
+- platform: LinkedIn
+ username: patrickfreyer
+ url: https://linkedin.com/in/patrickfreyer
+ icon: fab fa-linkedin
+ color: "#0077B5"
+ description: Connect with me professionally
+
+- platform: GitHub
+ username: patrickfreyer
+ url: https://github.com/patrickfreyer
+ icon: fab fa-github
+ color: "#333333"
+ description: Check out my code and projects
+
+- platform: X (Twitter)
+ username: patrickfreyer
+ url: https://x.com/real_patrick_f
+ icon: fab fa-x-twitter
+ color: "#000000"
+ description: Follow my thoughts and updates
+
+- platform: Instagram
+ username: patrickfreyer
+ url: hhttps://www.instagram.com/brightlyshadowed/
+ icon: fab fa-instagram
+ color: "#E4405F"
+ description: See my visual journey
\ No newline at end of file
diff --git a/_includes/about.html b/_includes/about.html
new file mode 100644
index 0000000..c05df22
--- /dev/null
+++ b/_includes/about.html
@@ -0,0 +1,15 @@
+
+
+
About Me
+
+
+
+
+
+
+
+
{{ site.data.about.about_text }}
+
+
+
+
\ No newline at end of file
diff --git a/_includes/hero.html b/_includes/hero.html
new file mode 100644
index 0000000..74ae83e
--- /dev/null
+++ b/_includes/hero.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
Patrick Freyer
+
Generative AI Builder, Developer, and Strategist
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_includes/social.html b/_includes/social.html
new file mode 100644
index 0000000..9c62b5a
--- /dev/null
+++ b/_includes/social.html
@@ -0,0 +1,23 @@
+
\ No newline at end of file
diff --git a/_layouts/default.html b/_layouts/default.html
new file mode 100644
index 0000000..698837b
--- /dev/null
+++ b/_layouts/default.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ {{ site.title }}
+
+
+
+ {{ content }}
+
+
\ No newline at end of file
diff --git a/assets/8k_earth_nightmap.jpg b/assets/8k_earth_nightmap.jpg
new file mode 100644
index 0000000..e211b6c
Binary files /dev/null and b/assets/8k_earth_nightmap.jpg differ
diff --git a/assets/FineTunedBanner.png b/assets/FineTunedBanner.png
new file mode 100644
index 0000000..8886196
Binary files /dev/null and b/assets/FineTunedBanner.png differ
diff --git a/assets/earth_albedo.jpg b/assets/earth_albedo.jpg
new file mode 100644
index 0000000..00e9928
Binary files /dev/null and b/assets/earth_albedo.jpg differ
diff --git a/assets/earth_bump.jpg b/assets/earth_bump.jpg
new file mode 100644
index 0000000..46aa4d2
Binary files /dev/null and b/assets/earth_bump.jpg differ
diff --git a/assets/earth_clouds.jpg b/assets/earth_clouds.jpg
new file mode 100644
index 0000000..5d4d35e
Binary files /dev/null and b/assets/earth_clouds.jpg differ
diff --git a/assets/earth_elevation.jpg b/assets/earth_elevation.jpg
new file mode 100644
index 0000000..46aa4d2
Binary files /dev/null and b/assets/earth_elevation.jpg differ
diff --git a/assets/earth_night.jpg b/assets/earth_night.jpg
new file mode 100644
index 0000000..7c1a96d
Binary files /dev/null and b/assets/earth_night.jpg differ
diff --git a/assets/earth_normal.jpg b/assets/earth_normal.jpg
new file mode 100644
index 0000000..46aa4d2
Binary files /dev/null and b/assets/earth_normal.jpg differ
diff --git a/assets/earth_roughness.jpg b/assets/earth_roughness.jpg
new file mode 100644
index 0000000..d1e0b05
Binary files /dev/null and b/assets/earth_roughness.jpg differ
diff --git a/assets/earth_specular.jpg b/assets/earth_specular.jpg
new file mode 100644
index 0000000..d1e0b05
Binary files /dev/null and b/assets/earth_specular.jpg differ
diff --git a/assets/js/earth-ar.js b/assets/js/earth-ar.js
new file mode 100644
index 0000000..4a09b6d
--- /dev/null
+++ b/assets/js/earth-ar.js
@@ -0,0 +1,252 @@
+import * as THREE from 'https://cdn.skypack.dev/three@0.128.0/build/three.module.js';
+import { ARButton } from 'https://cdn.skypack.dev/three@0.128.0/examples/jsm/webxr/ARButton.js';
+
+// WebXR Polyfill for better iOS support
+let WebXRPolyfill = null;
+try {
+ WebXRPolyfill = await import('https://cdn.jsdelivr.net/npm/webxr-polyfill@latest/build/webxr-polyfill.min.js');
+} catch (e) {
+ console.log('WebXR Polyfill not available, continuing without it');
+}
+
+class EarthAR {
+ constructor() {
+ this.scene = null;
+ this.camera = null;
+ this.renderer = null;
+ this.earthMesh = null;
+ this.isARSession = false;
+ this.reticle = null;
+ this.earthScale = 0.3; // Smaller scale for AR
+ }
+
+ async init() {
+ // Initialize WebXR Polyfill if available
+ if (WebXRPolyfill && !navigator.xr) {
+ new WebXRPolyfill();
+ console.log('WebXR Polyfill initialized');
+ }
+
+ // Wait a bit for polyfill to initialize
+ await new Promise(resolve => setTimeout(resolve, 100));
+
+ // Check if WebXR is supported
+ if (!navigator.xr) {
+ console.warn('WebXR not supported - trying alternative detection');
+
+ // Alternative detection for iOS Safari
+ if (navigator.userAgent.includes('iPhone') || navigator.userAgent.includes('iPad')) {
+ console.log('iOS device detected, attempting AR setup');
+ return this.setupForIOS();
+ }
+
+ return false;
+ }
+
+ // Check if AR is supported
+ try {
+ const isARSupported = await navigator.xr.isSessionSupported('immersive-ar');
+ if (!isARSupported) {
+ console.warn('AR not supported on this device');
+ return false;
+ }
+ return true;
+ } catch (error) {
+ console.error('Error checking AR support:', error);
+ return false;
+ }
+ }
+
+ async setupForIOS() {
+ // iOS Safari specific setup
+ console.log('Setting up for iOS Safari');
+
+ // Check for iOS version and Safari
+ const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
+ const isSafari = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
+
+ if (isIOS && isSafari) {
+ console.log('iOS Safari detected, AR should be available');
+ return true;
+ }
+
+ return false;
+ }
+
+ createARScene() {
+ // Create scene
+ this.scene = new THREE.Scene();
+
+ // Create camera
+ this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
+
+ // Create renderer
+ this.renderer = new THREE.WebGLRenderer({
+ alpha: true,
+ antialias: true,
+ preserveDrawingBuffer: true
+ });
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
+ this.renderer.setPixelRatio(window.devicePixelRatio);
+ this.renderer.xr.enabled = true;
+
+ // Add to DOM
+ document.body.appendChild(this.renderer.domElement);
+
+ // Add AR button
+ const arButton = ARButton.createButton(this.renderer, {
+ sessionInit: {
+ requiredFeatures: ['hit-test'],
+ optionalFeatures: ['dom-overlay'],
+ domOverlay: { root: document.body }
+ }
+ });
+ document.body.appendChild(arButton);
+
+ // Setup lighting
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
+ this.scene.add(ambientLight);
+
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
+ directionalLight.position.set(0, 10, 0);
+ this.scene.add(directionalLight);
+
+ // Create reticle for placement
+ this.createReticle();
+
+ // Setup session events
+ this.renderer.xr.addEventListener('sessionstart', () => {
+ this.isARSession = true;
+ console.log('AR session started');
+ });
+
+ this.renderer.xr.addEventListener('sessionend', () => {
+ this.isARSession = false;
+ console.log('AR session ended');
+ });
+
+ // Start render loop
+ this.renderer.setAnimationLoop(this.render.bind(this));
+ }
+
+ createReticle() {
+ const geometry = new THREE.RingGeometry(0.15, 0.2, 32).rotateX(-Math.PI / 2);
+ const material = new THREE.MeshBasicMaterial();
+ this.reticle = new THREE.Mesh(geometry, material);
+ this.reticle.matrixAutoUpdate = false;
+ this.reticle.visible = false;
+ this.scene.add(this.reticle);
+ }
+
+ createEarthForAR() {
+ // Create a simplified earth for AR
+ const earthRadius = this.earthScale;
+ const geometry = new THREE.SphereGeometry(earthRadius, 32, 32);
+
+ // Simple material for AR (no complex textures)
+ const material = new THREE.MeshPhongMaterial({
+ color: 0x4B6CB7, // Blue color
+ shininess: 30
+ });
+
+ this.earthMesh = new THREE.Mesh(geometry, material);
+ this.earthMesh.visible = false;
+ this.scene.add(this.earthMesh);
+
+ // Add some basic flight paths for AR
+ this.addSimpleFlightPaths();
+ }
+
+ addSimpleFlightPaths() {
+ // Create simple curved paths for AR
+ const curve = new THREE.CubicBezierCurve3(
+ new THREE.Vector3(-0.5, 0, 0),
+ new THREE.Vector3(-0.2, 0.3, 0.2),
+ new THREE.Vector3(0.2, 0.3, -0.2),
+ new THREE.Vector3(0.5, 0, 0)
+ );
+
+ const points = curve.getPoints(50);
+ const geometry = new THREE.BufferGeometry().setFromPoints(points);
+ const material = new THREE.LineBasicMaterial({ color: 0xFFFF00 });
+ const line = new THREE.Line(geometry, material);
+
+ this.earthMesh.add(line);
+ }
+
+ setupHitTesting() {
+ let hitTestSource = null;
+ let hitTestSourceRequested = false;
+
+ const session = this.renderer.xr.getSession();
+
+ session.addEventListener('select', () => {
+ if (this.reticle.visible) {
+ // Place earth at reticle position
+ this.earthMesh.position.setFromMatrixPosition(this.reticle.matrix);
+ this.earthMesh.visible = true;
+ this.reticle.visible = false;
+ }
+ });
+
+ session.requestReferenceSpace('viewer').then((referenceSpace) => {
+ session.requestHitTestSource({ space: referenceSpace }).then((source) => {
+ hitTestSource = source;
+ });
+ });
+
+ session.addEventListener('end', () => {
+ hitTestSourceRequested = false;
+ hitTestSource = null;
+ });
+
+ const frame = this.renderer.xr.getFrame();
+
+ if (hitTestSourceRequested === false) {
+ session.requestReferenceSpace('viewer').then((referenceSpace) => {
+ session.requestHitTestSource({ space: referenceSpace }).then((source) => {
+ hitTestSource = source;
+ });
+ });
+ hitTestSourceRequested = true;
+ }
+
+ if (hitTestSourceRequested === false) return;
+
+ if (hitTestSource) {
+ const hitTestResults = frame.getHitTestResults(hitTestSource);
+
+ if (hitTestResults.length) {
+ const hit = hitTestResults[0];
+ const pose = hit.getPose(this.reticle.parent);
+
+ this.reticle.visible = true;
+ this.reticle.matrix.fromArray(pose.transform.matrix);
+ } else {
+ this.reticle.visible = false;
+ }
+ }
+ }
+
+ render() {
+ if (this.isARSession) {
+ this.setupHitTesting();
+ }
+
+ this.renderer.render(this.scene, this.camera);
+ }
+
+ // Method to integrate with existing earth visualization
+ integrateWithExistingEarth(existingEarthMesh) {
+ if (this.earthMesh && existingEarthMesh) {
+ // Clone the existing earth and scale it for AR
+ this.earthMesh = existingEarthMesh.clone();
+ this.earthMesh.scale.setScalar(this.earthScale);
+ this.earthMesh.visible = false;
+ this.scene.add(this.earthMesh);
+ }
+ }
+}
+
+// Export for use
+window.EarthAR = EarthAR;
\ No newline at end of file
diff --git a/assets/js/earth.js b/assets/js/earth.js
new file mode 100644
index 0000000..2242000
--- /dev/null
+++ b/assets/js/earth.js
@@ -0,0 +1,619 @@
+import * as THREE from 'https://cdn.skypack.dev/three@0.128.0/build/three.module.js';
+import { OrbitControls } from 'https://cdn.skypack.dev/three@0.128.0/examples/jsm/controls/OrbitControls.js';
+
+// Ensure required data is available
+if (typeof locationsData === 'undefined' || typeof flightRoutesData === 'undefined') {
+ console.error('Required data is not defined. Make sure locationsData and flightRoutesData are passed correctly from Jekyll.');
+} else {
+ initEarth();
+}
+
+// Helper function to find location data by name
+function findLocationByName(name) {
+ return locationsData.find(loc => loc.name === name);
+}
+
+// Helper function to count route frequencies
+function countRouteFrequencies(routes) {
+ const frequencies = {};
+
+ routes.forEach(route => {
+ // Create a consistent key for the route (sort cities alphabetically to count both directions)
+ const cities = [route.origin, route.destination].sort();
+ const routeKey = `${cities[0]}-${cities[1]}`;
+ frequencies[routeKey] = (frequencies[routeKey] || 0) + 1;
+ });
+
+ return frequencies;
+}
+
+// Function to get route frequency
+function getRouteFrequency(origin, destination, frequencies) {
+ const cities = [origin, destination].sort();
+ const routeKey = `${cities[0]}-${cities[1]}`;
+ return frequencies[routeKey] || 1;
+}
+
+// Function to create curved flight path using great circle
+function createFlightPath(startPoint, endPoint, earthRadius, numLines = 1) {
+ const pathsPoints = [];
+ const numPoints = 50;
+
+ // Normalize the start and end points to get unit vectors
+ const startNormalized = startPoint.clone().normalize();
+ const endNormalized = endPoint.clone().normalize();
+
+ // Calculate the great circle distance (angle between points)
+ const dotProduct = startNormalized.dot(endNormalized);
+ const angle = Math.acos(Math.max(-1, Math.min(1, dotProduct)));
+
+ // Calculate distance for height scaling
+ const distance = startPoint.distanceTo(endPoint);
+ const maxHeightScale = 0.08;
+ const baseScale = Math.atan(distance) / (Math.PI / 2) * maxHeightScale;
+
+ // Generate base curved path points using great circle interpolation
+ const basePoints = [];
+ for (let i = 0; i <= numPoints; i++) {
+ const t = i / numPoints;
+
+ // Use spherical linear interpolation (slerp) for great circle
+ const sinAngle = Math.sin(angle);
+ if (sinAngle === 0) {
+ // Points are the same or opposite, use direct interpolation
+ const point = startNormalized.clone().lerp(endNormalized, t);
+ const heightScale = earthRadius * (1 + baseScale * Math.sin(Math.PI * t));
+ point.normalize().multiplyScalar(heightScale);
+ basePoints.push(point);
+ } else {
+ // Use proper spherical interpolation
+ const sinT = Math.sin(t * angle);
+ const sinOneMinusT = Math.sin((1 - t) * angle);
+
+ const point = new THREE.Vector3();
+ point.addScaledVector(startNormalized, sinOneMinusT / sinAngle);
+ point.addScaledVector(endNormalized, sinT / sinAngle);
+
+ // Add height curve above the great circle
+ const heightScale = earthRadius * (1 + baseScale * Math.sin(Math.PI * t));
+ point.normalize().multiplyScalar(heightScale);
+ basePoints.push(point);
+ }
+ }
+
+ // Calculate perpendicular direction for parallel lines
+ const pathDirection = endPoint.clone().sub(startPoint).normalize();
+ const globeNormal = startPoint.clone().add(endPoint).normalize();
+ const perpDirection = pathDirection.clone().cross(globeNormal).normalize();
+
+ // Create offset paths
+ for (let i = 0; i < numLines; i++) {
+ const offset = perpDirection.clone().multiplyScalar(0.01 * (i - (numLines - 1) / 2));
+ const offsetPoints = basePoints.map(point => {
+ return point.clone().add(offset);
+ });
+ pathsPoints.push(offsetPoints);
+ }
+
+ return pathsPoints;
+}
+
+// Function to create flight path lines (optional dashed for planned flights)
+function createFlightLines(pathsPoints, color = 0x00ff00, isPlanned = false) {
+ const lines = [];
+ const dashSize = 0.15;
+ const gapSize = 0.08;
+
+ pathsPoints.forEach(points => {
+ const geometry = new THREE.BufferGeometry().setFromPoints(points);
+
+ // Create a brighter version of the color for better contrast
+ const brightColor = new THREE.Color(color);
+ brightColor.multiplyScalar(1.5); // Make it 50% brighter
+
+ const material = isPlanned
+ ? new THREE.LineDashedMaterial({
+ color: brightColor,
+ dashSize,
+ gapSize,
+ depthTest: true,
+ depthWrite: true,
+ side: THREE.FrontSide
+ })
+ : new THREE.LineBasicMaterial({
+ color: brightColor,
+ transparent: false,
+ linewidth: 3,
+ depthTest: true,
+ depthWrite: true,
+ side: THREE.FrontSide
+ });
+
+ const line = new THREE.Line(geometry, material);
+ if (isPlanned) {
+ line.computeLineDistances(); // Required for dashed lines to render
+ }
+ line.renderOrder = 1; // Render after earth mesh
+
+ // Add a glow effect by creating a thicker line behind (dashed for planned)
+ const glowGeometry = new THREE.BufferGeometry().setFromPoints(points);
+ const glowMaterial = isPlanned
+ ? new THREE.LineDashedMaterial({
+ color: brightColor,
+ transparent: true,
+ opacity: 0.4,
+ dashSize,
+ gapSize,
+ depthTest: true,
+ depthWrite: false,
+ side: THREE.FrontSide
+ })
+ : new THREE.LineBasicMaterial({
+ color: brightColor,
+ transparent: true,
+ opacity: 0.4,
+ linewidth: 5,
+ depthTest: true,
+ depthWrite: false,
+ side: THREE.FrontSide
+ });
+ const glowLine = new THREE.Line(glowGeometry, glowMaterial);
+ if (isPlanned) {
+ glowLine.computeLineDistances();
+ }
+ glowLine.renderOrder = 0; // Render before main line
+
+ lines.push(glowLine); // Add glow first (behind)
+ lines.push(line); // Add main line on top
+ });
+ return lines;
+}
+
+// Add these functions before initEarth()
+function getUniqueValues(data, key) {
+ if (key === 'travelers') {
+ // Special handling for travelers array
+ const allTravelers = new Set();
+ data.forEach(item => {
+ if (Array.isArray(item[key])) {
+ item[key].forEach(traveler => allTravelers.add(traveler));
+ }
+ });
+ return [...allTravelers].sort();
+ }
+ return [...new Set(data.map(item => item[key]))].filter(Boolean).sort();
+}
+
+function populateFilterDropdowns(flightData) {
+ const years = getUniqueValues(flightData, 'year');
+ const airlines = getUniqueValues(flightData, 'airline');
+ const occasions = getUniqueValues(flightData, 'occasion');
+ const months = getUniqueValues(flightData, 'month');
+ const travelers = getUniqueValues(flightData, 'travelers');
+
+ // Helper function to populate dropdowns
+ function populateDropdown(elementId, values) {
+ const select = document.getElementById(elementId);
+ if (!select) return;
+
+ select.innerHTML = '';
+ values.forEach(value => {
+ const option = document.createElement('option');
+ option.value = value;
+ option.textContent = value;
+ select.appendChild(option);
+ });
+ }
+
+ populateDropdown('year-filter', years);
+ populateDropdown('airline-filter', airlines);
+ populateDropdown('occasion-filter', occasions);
+ populateDropdown('month-filter', months);
+ populateDropdown('travelers-filter', travelers);
+}
+
+function getSelectedValues(elementId) {
+ const element = document.getElementById(elementId);
+ if (!element) return [];
+ return Array.from(element.selectedOptions).map(option => option.value);
+}
+
+function filterFlightData(data, filters) {
+ return data.filter(flight => {
+ // Convert years to numbers for comparison
+ const yearMatch = filters.years.length === 0 ||
+ filters.years.map(Number).includes(Number(flight.year));
+ const airlineMatch = filters.airlines.length === 0 || filters.airlines.includes(flight.airline);
+ const occasionMatch = filters.occasions.length === 0 || filters.occasions.includes(flight.occasion);
+ const monthMatch = filters.months.length === 0 || filters.months.includes(flight.month);
+
+ // Check if any selected travelers are in the flight's travelers array
+ const travelersMatch = filters.travelers.length === 0 ||
+ (Array.isArray(flight.travelers) &&
+ filters.travelers.some(traveler => flight.travelers.includes(traveler)));
+
+ return yearMatch && airlineMatch && occasionMatch && monthMatch && travelersMatch;
+ });
+}
+
+function setupFilterHandlers(earthMesh, initializeFlightPaths, scene) {
+ const applyButton = document.getElementById('apply-filters');
+ const resetButton = document.getElementById('reset-filters');
+ const toggleButton = document.getElementById('toggle-filters');
+ const filterPanel = document.getElementById('filter-panel');
+
+ // Setup toggle functionality
+ if (toggleButton && filterPanel) {
+ toggleButton.addEventListener('click', () => {
+ filterPanel.classList.toggle('collapsed');
+ });
+ }
+
+ if (applyButton) {
+ applyButton.addEventListener('click', () => {
+ const filters = {
+ years: getSelectedValues('year-filter'),
+ airlines: getSelectedValues('airline-filter'),
+ occasions: getSelectedValues('occasion-filter'),
+ months: getSelectedValues('month-filter'),
+ travelers: getSelectedValues('travelers-filter')
+ };
+
+ // Remove existing flight paths from scene
+ scene.children = scene.children.filter(child => !(child instanceof THREE.Line));
+
+ // Apply filtered data
+ const filteredData = filterFlightData(flightRoutesData, filters);
+ initializeFlightPaths(filteredData, earthMesh);
+ });
+ }
+
+ if (resetButton) {
+ resetButton.addEventListener('click', () => {
+ // Clear all selections
+ ['year-filter', 'airline-filter', 'occasion-filter', 'month-filter', 'travelers-filter'].forEach(id => {
+ const element = document.getElementById(id);
+ if (element) element.selectedIndex = -1;
+ });
+
+ // Reset to original data
+ scene.children = scene.children.filter(child => !(child instanceof THREE.Line));
+ initializeFlightPaths(flightRoutesData, earthMesh);
+ });
+ }
+}
+
+// Function to generate distinct colors
+function generateDistinctColors(count) {
+ // Simplified, bright color palette for better visibility
+ const baseColors = [
+ 0xFFFF00, // Bright Yellow
+ 0xFF8C00, // Bright Orange
+ 0x00BFFF, // Bright Blue
+ 0xFFFFFF, // White
+ 0xFF1493, // Deep Pink
+ 0x00FF00, // Bright Green
+ 0xFF4500, // Orange Red
+ 0x00FFFF, // Cyan
+ 0xFFD700, // Gold
+ 0xFF69B4, // Hot Pink
+ 0x00CED1, // Dark Turquoise
+ 0xFF6347 // Tomato
+ ];
+
+ const colors = [];
+ for (let i = 0; i < count; i++) {
+ // Use modulo to cycle through colors if we need more than the base palette
+ colors.push(baseColors[i % baseColors.length]);
+ }
+
+ return colors;
+}
+
+function initEarth() {
+ const container = document.getElementById('earth-container');
+ if (!container) {
+ console.error('Earth container not found');
+ return;
+ }
+
+ // Scene setup
+ const scene = new THREE.Scene();
+ const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
+ const renderer = new THREE.WebGLRenderer({
+ alpha: true,
+ antialias: true,
+ logarithmicDepthBuffer: true // Helps with z-fighting
+ });
+
+ renderer.setSize(container.clientWidth, container.clientHeight);
+ renderer.setPixelRatio(window.devicePixelRatio);
+ container.appendChild(renderer.domElement);
+
+ // Earth radius and state
+ const earthRadius = 5;
+ const state = {
+ isDaylight: true // Can be toggled for day/night transitions
+ };
+
+ // Texture Loader
+ const textureLoader = new THREE.TextureLoader();
+ const loadTexture = (path) => textureLoader.load(`https://patrickfreyer.com/assets/${path}`);
+
+ // Load all textures
+ const earthDayTexture = loadTexture('earth_albedo.jpg');
+ const earthNightTexture = loadTexture('earth_night.jpg');
+ const normalTexture = loadTexture('earth_normal.jpg');
+ const specularTexture = loadTexture('earth_specular.jpg');
+ const roughnessTexture = loadTexture('earth_roughness.jpg');
+ const bumpTexture = loadTexture('earth_bump.jpg');
+ const cloudsTexture = loadTexture('earth_clouds.jpg');
+
+ // 0. Solid Base Sphere (to prevent flight routes from showing through)
+ const baseGeometry = new THREE.SphereGeometry(earthRadius, 64, 64);
+ const baseMaterial = new THREE.MeshBasicMaterial({
+ color: 0x000000, // Black color
+ side: THREE.FrontSide
+ });
+ const baseMesh = new THREE.Mesh(baseGeometry, baseMaterial);
+ baseMesh.renderOrder = -1; // Render first, before everything else
+ scene.add(baseMesh);
+
+ // 1. Base Earth Layer
+ const globeGeometry = new THREE.SphereGeometry(earthRadius, 64, 64);
+ const globeMaterial = new THREE.MeshPhongMaterial({
+ map: earthDayTexture,
+ normalMap: normalTexture,
+ normalScale: new THREE.Vector2(0.8, 0.8),
+ specularMap: specularTexture,
+ bumpMap: bumpTexture,
+ bumpScale: 0.02,
+ specular: new THREE.Color(0x444444),
+ shininess: 15
+ });
+ const earthMesh = new THREE.Mesh(globeGeometry, globeMaterial);
+ earthMesh.renderOrder = 0; // Render first
+ scene.add(earthMesh);
+
+ // 2. Night Lights Layer
+ const nightGeometry = new THREE.SphereGeometry(earthRadius * 1.001, 64, 64);
+ const nightMaterial = new THREE.MeshBasicMaterial({
+ map: earthNightTexture,
+ blending: THREE.AdditiveBlending,
+ transparent: true,
+ opacity: state.isDaylight ? 0 : 0.8,
+ depthWrite: false
+ });
+ const nightMesh = new THREE.Mesh(nightGeometry, nightMaterial);
+ nightMesh.renderOrder = 0; // Render first
+ scene.add(nightMesh);
+
+ // 3. Cloud Layer
+ const cloudGeometry = new THREE.SphereGeometry(earthRadius * 1.008, 64, 64);
+ const cloudMaterial = new THREE.MeshPhongMaterial({
+ map: cloudsTexture,
+ transparent: true,
+ opacity: state.isDaylight ? 0.3 : 0.1,
+ depthWrite: false,
+ side: THREE.DoubleSide,
+ blending: THREE.NormalBlending
+ });
+ const cloudMesh = new THREE.Mesh(cloudGeometry, cloudMaterial);
+ cloudMesh.renderOrder = 0; // Render first
+ scene.add(cloudMesh);
+
+ // Enhanced Lighting System
+ // 1. Ambient Light
+ const ambientLight = new THREE.AmbientLight(0xffffff, state.isDaylight ? 0.3 : 0.1);
+ scene.add(ambientLight);
+
+ // 2. Directional Light (Sun)
+ const directionalLight = new THREE.DirectionalLight(0xffffff, state.isDaylight ? 2.0 : 0.15);
+ directionalLight.position.set(5, 3, 5);
+ scene.add(directionalLight);
+
+ // 3. Hemisphere Light
+ const hemisphereLight = new THREE.HemisphereLight(
+ 0xffffff, // Sky color
+ 0x444444, // Ground color
+ state.isDaylight ? 0.6 : 0
+ );
+ scene.add(hemisphereLight);
+
+ // Controls
+ const controls = new OrbitControls(camera, renderer.domElement);
+ controls.enableDamping = true;
+ controls.dampingFactor = 0.3; // Increased for smoother damping
+ controls.screenSpacePanning = false;
+ controls.minDistance = 6;
+ controls.maxDistance = 12;
+ controls.enablePan = false;
+ controls.autoRotate = true;
+ controls.autoRotateSpeed = 0.3;
+
+ // Zoom settings for smoother zooming
+ controls.zoomSpeed = 0.3; // Reduced zoom speed (default is 1.0)
+ controls.enableZoom = true;
+ controls.zoomDampingFactor = 0.1; // Smooth zoom damping
+
+ // Function to convert Lat/Lon to 3D coordinates
+ function latLonToVector3(lat, lon, radius) {
+ const phi = (90 - lat) * (Math.PI / 180);
+ const theta = (lon + 180) * (Math.PI / 180);
+
+ const x = -(radius * Math.sin(phi) * Math.cos(theta));
+ const z = radius * Math.sin(phi) * Math.sin(theta);
+ const y = radius * Math.cos(phi);
+
+ return new THREE.Vector3(x, y, z);
+ }
+
+ // Add pins for locations
+ // Create a group for the pin meshes
+ const createPin = () => {
+ const pinGroup = new THREE.Group();
+
+ // Create a simple sphere for the location marker
+ const headGeometry = new THREE.SphereGeometry(0.015, 8, 8); // Smaller and less detailed sphere
+ const headMaterial = new THREE.MeshPhongMaterial({
+ color: 0x4169E1,
+ emissive: 0x0000ff,
+ emissiveIntensity: 0.3,
+ shininess: 50
+ });
+ const head = new THREE.Mesh(headGeometry, headMaterial);
+
+ // Create a subtle glow effect
+ const glowGeometry = new THREE.SphereGeometry(0.04, 12, 12);
+ const glowMaterial = new THREE.MeshBasicMaterial({
+ color: 0x6495ED,
+ transparent: true,
+ opacity: 0.25
+ });
+ const glow = new THREE.Mesh(glowGeometry, glowMaterial);
+
+ pinGroup.add(head);
+ pinGroup.add(glow);
+
+ return pinGroup;
+ };
+
+ locationsData.forEach(location => {
+ const position = latLonToVector3(location.lat, location.lon, earthRadius);
+ const pin = createPin();
+
+ // Calculate the rotation to make the pin point towards the earth's center
+ const normal = position.clone().normalize();
+ pin.position.copy(position);
+ pin.lookAt(new THREE.Vector3(0, 0, 0));
+ pin.rotateX(Math.PI / 2);
+
+ earthMesh.add(pin);
+ });
+
+ // After creating earthMesh, add this:
+ populateFilterDropdowns(flightRoutesData);
+
+ // Extract flight path initialization into a separate function
+ function initializeFlightPaths(routes, targetMesh) {
+ const routeFrequencies = countRouteFrequencies(routes);
+ const processedRoutes = new Set();
+
+ // Track which route keys have planned vs completed flights (for dashed vs solid lines)
+ const routeKeyPlanned = {};
+ const routeKeyCompleted = {};
+ routes.forEach(route => {
+ const cities = [route.origin, route.destination].sort();
+ const routeKey = `${cities[0]}-${cities[1]}`;
+ if (route.planned) {
+ routeKeyPlanned[routeKey] = true;
+ } else {
+ routeKeyCompleted[routeKey] = true;
+ }
+ });
+
+ // Get unique airlines and generate colors
+ const uniqueAirlines = [...new Set(routes.map(route => route.airline))].filter(Boolean);
+ const colors = generateDistinctColors(uniqueAirlines.length);
+ const airlineColors = Object.fromEntries(
+ uniqueAirlines.map((airline, index) => [airline, colors[index]])
+ );
+ // Add default color
+ airlineColors['default'] = 0x00ff00;
+
+ routes.forEach(route => {
+ const cities = [route.origin, route.destination].sort();
+ const routeKey = `${cities[0]}-${cities[1]}`;
+
+ if (processedRoutes.has(routeKey)) return;
+ processedRoutes.add(routeKey);
+
+ const originLoc = findLocationByName(route.origin);
+ const destLoc = findLocationByName(route.destination);
+
+ if (!originLoc || !destLoc) {
+ console.warn(`Skipping route: ${route.origin} -> ${route.destination} due to missing location data`);
+ return;
+ }
+
+ const startPoint = latLonToVector3(originLoc.lat, originLoc.lon, earthRadius);
+ const endPoint = latLonToVector3(destLoc.lat, destLoc.lon, earthRadius);
+
+ const frequency = getRouteFrequency(route.origin, route.destination, routeFrequencies);
+ const numLines = Math.min(Math.max(frequency, 1), 10);
+
+ const pathsPoints = createFlightPath(startPoint, endPoint, earthRadius, numLines);
+ const color = airlineColors[route.airline] || airlineColors.default;
+
+ // Draw solid line for completed flights on this route
+ if (routeKeyCompleted[routeKey]) {
+ const flightLines = createFlightLines(pathsPoints, color, false);
+ flightLines.forEach(line => scene.add(line));
+ }
+ // Draw dashed line for planned flights on this route
+ if (routeKeyPlanned[routeKey]) {
+ const plannedLines = createFlightLines(pathsPoints, color, true);
+ plannedLines.forEach(line => scene.add(line));
+ }
+ });
+ }
+
+ // Initialize flight paths with all data
+ initializeFlightPaths(flightRoutesData, earthMesh);
+
+ // Setup filter handlers
+ setupFilterHandlers(earthMesh, initializeFlightPaths, scene);
+
+ // Initial Camera Position
+ camera.position.set(4, 8, 8); // Position camera above and to the side of Europe
+ camera.lookAt(0, 0, 0); // Look at the center of the Earth
+ controls.update(); // Update controls after changing camera position
+
+ // Animation Loop with cloud rotation
+ let frameCount = 0;
+ function animate() {
+ requestAnimationFrame(animate);
+
+ frameCount++;
+
+ // Rotate clouds slightly faster than the Earth
+ if (cloudMesh) {
+ cloudMesh.rotation.y = earthMesh.rotation.y * 1.1;
+ }
+
+ // Update Controls
+ controls.update();
+
+ renderer.render(scene, camera);
+ }
+
+ // Handle Window Resize
+ function onWindowResize() {
+ if (!container) return;
+ camera.aspect = container.clientWidth / container.clientHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(container.clientWidth, container.clientHeight);
+ }
+
+ window.addEventListener('resize', onWindowResize, false);
+
+ // Start animation when textures are loaded
+ Promise.all([
+ earthDayTexture,
+ earthNightTexture,
+ normalTexture,
+ specularTexture,
+ roughnessTexture,
+ bumpTexture,
+ cloudsTexture
+ ]).then(() => {
+ console.log("All textures loaded successfully");
+ animate();
+ }).catch(error => {
+ console.error('Error loading textures:', error);
+ // Start animation anyway to show at least something
+ animate();
+ });
+
+ console.log("Three.js Earth initialized with enhanced visualization");
+}
\ No newline at end of file
diff --git a/assets/profile.png b/assets/profile.png
new file mode 100644
index 0000000..411f2f6
Binary files /dev/null and b/assets/profile.png differ
diff --git a/cname b/cname
new file mode 100644
index 0000000..7e98ee0
--- /dev/null
+++ b/cname
@@ -0,0 +1 @@
+patrickfreyer.com
\ No newline at end of file
diff --git a/css/style.css b/css/style.css
new file mode 100644
index 0000000..6927a91
--- /dev/null
+++ b/css/style.css
@@ -0,0 +1,1677 @@
+:root {
+ /* Core colors */
+ --primary-color: #2563eb;
+ --secondary-color: #6366f1;
+ --accent-color: #8b5cf6;
+ --success-color: #10b981;
+ --warning-color: #f59e0b;
+ --error-color: #ef4444;
+
+ /* Text colors */
+ --text-color: #1f2937;
+ --text-light: #6b7280;
+ --text-lighter: #9ca3af;
+
+ /* Background colors */
+ --background: #ffffff;
+ --background-alt: #f3f4f6;
+ --background-dark: #111827;
+
+ /* Gradients */
+ --gradient-primary: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
+ --gradient-accent: linear-gradient(135deg, var(--secondary-color), var(--accent-color));
+
+ /* Shadows */
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+ --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+
+ /* Transitions */
+ --transition-fast: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-normal: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-slow: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
+
+ /* Border Radius */
+ --radius-sm: 0.375rem;
+ --radius-md: 0.5rem;
+ --radius-lg: 1rem;
+ --radius-full: 9999px;
+
+ /* Spacing */
+ --space-1: 0.25rem;
+ --space-2: 0.5rem;
+ --space-3: 0.75rem;
+ --space-4: 1rem;
+ --space-6: 1.5rem;
+ --space-8: 2rem;
+ --space-12: 3rem;
+ --space-16: 4rem;
+
+ /* Z-index */
+ --z-header: 1000;
+ --z-modal: 1100;
+ --z-tooltip: 1200;
+
+ /* Container */
+ --container-max: 1400px;
+ --container-padding: 2rem;
+
+ --primary-color-rgb: 37, 99, 235;
+ --secondary-color-rgb: 99, 102, 241;
+ --accent-color-rgb: 139, 92, 246;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: 'Inter', sans-serif;
+ line-height: 1.6;
+ color: var(--text-color);
+ background-color: var(--background);
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 2rem;
+}
+
+/* Header Styles */
+.header {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ background: rgba(255, 255, 255, 0.8);
+ backdrop-filter: blur(10px) saturate(180%);
+ -webkit-backdrop-filter: blur(10px) saturate(180%);
+ z-index: 1000;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.3);
+}
+
+.nav-container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem 2rem;
+ max-width: 1400px;
+ margin: 0 auto;
+}
+
+.logo {
+ font-size: 1.75rem;
+ font-weight: 800;
+ background: linear-gradient(135deg, var(--primary-color), #6366f1);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ text-decoration: none;
+ transition: var(--transition);
+}
+
+.logo:hover {
+ transform: scale(1.05);
+}
+
+.mobile-menu-button {
+ display: none;
+ background: none;
+ border: none;
+ color: var(--text-color);
+ font-size: 1.25rem;
+ cursor: pointer;
+ padding: 0.5rem;
+ transition: var(--transition);
+ margin-left: auto;
+}
+
+.mobile-menu-button:hover {
+ color: var(--primary-color);
+}
+
+.nav-links {
+ display: flex;
+ gap: 2.5rem;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.nav-links a {
+ text-decoration: none;
+ color: var(--text-color);
+ font-weight: 500;
+ font-size: 1.05rem;
+ transition: var(--transition);
+ position: relative;
+ padding: 0.5rem 0;
+}
+
+.nav-links a::before {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 0;
+ height: 2px;
+ background: linear-gradient(135deg, var(--primary-color), #6366f1);
+ transition: width 0.3s ease;
+}
+
+.nav-links a:hover::before {
+ width: 100%;
+}
+
+.nav-links a:hover {
+ color: var(--primary-color);
+}
+
+@media (min-width: 769px) {
+ .nav-container {
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .nav-header {
+ width: auto;
+ }
+}
+
+@media (max-width: 768px) {
+ .mobile-menu-button {
+ display: block;
+ }
+
+ .nav-container {
+ display: flex;
+ align-items: center;
+ padding: 1rem;
+ gap: 1rem;
+ }
+
+ .nav-links {
+ display: none;
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ background: rgba(255, 255, 255, 0.95);
+ backdrop-filter: blur(10px) saturate(180%);
+ -webkit-backdrop-filter: blur(10px) saturate(180%);
+ padding: 1rem;
+ flex-direction: column;
+ gap: 1rem;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.3);
+ }
+
+ .nav-links.nav-open {
+ display: flex;
+ }
+
+ .nav-links a {
+ padding: 0.75rem 1.5rem;
+ border-radius: 0.5rem;
+ }
+
+ .nav-links a:hover {
+ background: rgba(99, 102, 241, 0.1);
+ }
+
+ .timeline::before {
+ left: 24px;
+ }
+
+ .timeline-item {
+ width: calc(100% - 48px);
+ margin-left: 48px !important;
+ margin-right: 0 !important;
+ }
+
+ .timeline-item:nth-child(odd) .timeline-icon,
+ .timeline-item:nth-child(even) .timeline-icon {
+ left: -36px !important;
+ right: auto !important;
+ transform: translateY(-50%) !important;
+ }
+
+ .timeline-content {
+ margin-left: 0;
+ }
+}
+
+@media (max-width: 480px) {
+ .timeline {
+ padding: 1rem 0;
+ }
+
+ .timeline::before {
+ left: 24px;
+ }
+
+ .timeline-item {
+ width: calc(100% - 48px);
+ margin-left: 48px !important;
+ }
+
+ .timeline-item .timeline-icon {
+ width: 2rem;
+ height: 2rem;
+ left: -36px;
+ }
+
+ .timeline-icon i {
+ font-size: 0.875rem;
+ }
+
+ .timeline-content {
+ padding: 1rem;
+ }
+}
+
+@keyframes slideDown {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* Hero Section Styles */
+.hero-section {
+ position: relative;
+ overflow: hidden;
+ min-height: 100vh;
+ padding: 6rem 0 4rem;
+ background: linear-gradient(135deg, rgba(var(--primary-color-rgb), 0.1), rgba(var(--secondary-color-rgb), 0.1));
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ justify-content: center;
+}
+
+#earth-container {
+ position: relative;
+ width: 90%;
+ max-width: 600px;
+ height: 600px;
+ margin-top: 3rem;
+ z-index: 2;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ pointer-events: auto;
+}
+
+#earth-container canvas {
+ display: block;
+ width: 100% !important;
+ height: 100% !important;
+ max-width: 100%;
+ max-height: 100%;
+}
+
+.hero-section::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: radial-gradient(circle at 50% 50%, rgba(var(--accent-color-rgb), 0.1) 0%, transparent 50%);
+ z-index: 0;
+}
+
+.hero-section .container {
+ position: relative;
+ z-index: 1;
+ width: 100%;
+ max-width: 1140px;
+ margin: 0 auto;
+ padding: 0 15px;
+ text-align: center;
+ pointer-events: none;
+}
+
+.hero-content {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 4rem;
+ max-width: 1200px;
+ margin: 0 auto;
+ width: 100%;
+}
+
+.hero-text {
+ flex: 1;
+ width: 100%;
+ pointer-events: auto;
+}
+
+.hero-section h1 {
+ font-size: 3.5rem;
+ font-weight: 700;
+ margin-bottom: 1rem;
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ animation: fadeIn 1s ease-out;
+}
+
+.subtitle {
+ font-size: 1.5rem;
+ color: var(--text-light);
+ margin-bottom: 2rem;
+ animation: fadeIn 1s ease-out 0.3s backwards;
+}
+
+.social-links {
+ display: flex;
+ gap: 1.5rem;
+ margin-top: 2rem;
+ animation: fadeIn 1s ease-out 0.6s backwards;
+ justify-content: center;
+ pointer-events: auto;
+}
+
+.about-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ animation: fadeIn 1s ease-out 0.9s backwards;
+}
+
+.about-image-wrapper {
+ width: 100%;
+ max-width: 300px;
+ margin: 0 auto;
+}
+
+.about-image {
+ position: relative;
+ width: 100%;
+ max-width: 300px;
+ margin: 0 auto;
+}
+
+.profile-image {
+ width: 100%;
+ height: auto;
+ display: block;
+}
+
+.about-text {
+ font-size: clamp(1rem, 2vw, 1.1rem);
+ text-align: left;
+}
+
+.about-text p {
+ margin-bottom: 2rem;
+ line-height: 1.8;
+}
+
+@media (max-width: 1024px) {
+ .hero-content {
+ gap: 2rem;
+ }
+
+ .hero-section h1 {
+ font-size: 3rem;
+ }
+
+ .subtitle {
+ font-size: 1.25rem;
+ }
+}
+
+@media (max-width: 768px) {
+ .hero-content {
+ flex-direction: column;
+ text-align: center;
+ gap: 3rem;
+ }
+
+ .hero-text {
+ order: 1;
+ }
+
+ .about-content {
+ order: 2;
+ }
+
+ .hero-section h1 {
+ font-size: 2.5rem;
+ }
+
+ .about-image-wrapper {
+ max-width: 250px;
+ }
+}
+
+@media (max-width: 480px) {
+ .hero-section {
+ padding: 6rem 0 3rem;
+ }
+
+ .hero-section h1 {
+ font-size: 2rem;
+ }
+
+ .subtitle {
+ font-size: 1.1rem;
+ }
+
+ .about-image-wrapper {
+ max-width: 200px;
+ }
+}
+
+@keyframes slideUp {
+ from {
+ transform: translateY(20px);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+/* Section Styles */
+section {
+ padding: 6rem 0;
+}
+
+section:nth-child(even) {
+ background-color: var(--background-alt);
+}
+
+h2 {
+ font-size: clamp(2rem, 5vw, 2.5rem);
+ margin-bottom: 2rem;
+ color: var(--text-color);
+}
+
+/* Projects Section */
+.projects-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
+ gap: 2rem;
+ padding: 2rem 0;
+}
+
+/* Contact Section */
+.contact-button {
+ display: inline-block;
+ padding: 1rem 2rem;
+ background-color: var(--primary-color);
+ color: white;
+ text-decoration: none;
+ border-radius: 8px;
+ font-weight: 500;
+ transition: var(--transition);
+ margin-top: 2rem;
+}
+
+.contact-button:hover {
+ background-color: var(--secondary-color);
+ transform: translateY(-2px);
+}
+
+/* Footer */
+.footer {
+ background-color: var(--text-color);
+ color: white;
+ padding: 2rem 0;
+ text-align: center;
+}
+
+/* Animations */
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.hero-section > div,
+section > div {
+ animation: fadeIn 1s ease-out;
+}
+
+/* About Section */
+.about-section {
+ padding: clamp(3rem, 8vh, 6rem) 0;
+}
+
+.about-content {
+ display: grid;
+ grid-template-columns: 1fr 2fr;
+ gap: clamp(2rem, 5vw, 4rem);
+ align-items: start;
+}
+
+.about-image-wrapper {
+ position: relative;
+ width: 100%;
+}
+
+.about-image {
+ position: relative;
+ width: 100%;
+ max-width: 300px;
+ margin: 0 auto;
+}
+
+.profile-image {
+ width: 100%;
+ height: auto;
+}
+
+.about-text {
+ font-size: clamp(1rem, 2vw, 1.1rem);
+}
+
+.about-text p {
+ margin-bottom: 1.5rem;
+}
+
+.about-highlights {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1.5rem;
+ margin-top: 2rem;
+}
+
+.highlight {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 1rem;
+ background-color: var(--background-alt);
+ border-radius: 0.5rem;
+ transition: var(--transition-normal);
+}
+
+.highlight:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-md);
+}
+
+.highlight-icon {
+ font-size: 1.25rem;
+ color: var(--primary-color);
+ width: 2.5rem;
+ height: 2.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--background);
+ border-radius: 50%;
+ box-shadow: var(--shadow-sm);
+ transition: var(--transition-normal);
+}
+
+.highlight-icon i {
+ font-size: 1.25rem;
+ width: auto;
+ height: auto;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.highlight:hover .highlight-icon {
+ transform: scale(1.1);
+ color: var(--secondary-color);
+}
+
+.highlight-text {
+ font-weight: 500;
+}
+
+/* Updates Timeline */
+.timeline {
+ position: relative;
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 2rem 0;
+}
+
+.timeline::before {
+ content: '';
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 2px;
+ height: 100%;
+ background-color: var(--primary-color);
+}
+
+.timeline-item {
+ position: relative;
+ margin-bottom: 2rem;
+ width: calc(50% - 2rem);
+}
+
+.timeline-item:nth-child(odd) {
+ margin-left: auto;
+}
+
+.timeline-icon {
+ position: absolute;
+ top: 50%;
+ width: 2.5rem;
+ height: 2.5rem;
+ background: var(--background);
+ border: 2px solid var(--primary-color);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transform: translateY(-50%);
+ z-index: 1;
+}
+
+.timeline-item:nth-child(odd) .timeline-icon {
+ right: calc(100% + 2rem);
+ transform: translate(50%, -50%);
+}
+
+.timeline-item:nth-child(even) .timeline-icon {
+ left: calc(100% + 2rem);
+ transform: translate(-50%, -50%);
+}
+
+.timeline-icon i {
+ color: var(--primary-color);
+ font-size: 1rem;
+ width: auto;
+ height: auto;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.timeline-content {
+ background: var(--background);
+ padding: 1.5rem;
+ border-radius: 0.5rem;
+ box-shadow: var(--shadow-md);
+}
+
+/* Projects Grid */
+.project-card {
+ position: relative;
+ background: rgba(255, 255, 255, 0.8);
+ backdrop-filter: blur(10px) saturate(180%);
+ -webkit-backdrop-filter: blur(10px) saturate(180%);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ border-radius: 1rem;
+ padding: 2rem;
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+ overflow: hidden;
+}
+
+.project-card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 4px;
+ background: linear-gradient(90deg, var(--primary-color), #6366f1);
+ transform: scaleX(0);
+ transform-origin: left;
+ transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.project-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
+}
+
+.project-card:hover::before {
+ transform: scaleX(1);
+}
+
+.project-icon {
+ font-size: 2.5rem;
+ width: 4rem;
+ height: 4rem;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(135deg, rgba(var(--primary-color-rgb), 0.1));
+ border-radius: var(--radius-lg);
+ margin-bottom: 1.5rem;
+ position: relative;
+}
+
+.project-icon i {
+ background: linear-gradient(135deg, var(--primary-color));
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+
+.project-title {
+ font-size: 1.5rem;
+ font-weight: 700;
+ margin-bottom: 1rem;
+ background: linear-gradient(135deg, var(--text-color), #4b5563);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+
+.project-description {
+ color: var(--text-light);
+ margin-bottom: 1.5rem;
+ line-height: 1.6;
+}
+
+.project-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ margin-top: 1.5rem;
+ margin-bottom: 3rem;
+}
+
+.tag {
+ background: rgba(99, 102, 241, 0.1);
+ color: var(--primary-color);
+ padding: 0.5rem 1rem;
+ border-radius: 2rem;
+ font-size: 0.875rem;
+ font-weight: 500;
+ transition: var(--transition-normal);
+}
+
+.tag:hover {
+ background: rgba(99, 102, 241, 0.2);
+ transform: translateY(-2px);
+}
+
+.project-links {
+ display: flex;
+ gap: 1rem;
+ margin-top: 1.5rem;
+}
+
+.project-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ color: var(--primary-color);
+ text-decoration: none;
+ font-weight: 500;
+ transition: var(--transition-normal);
+}
+
+.project-link:hover {
+ transform: translateX(5px);
+}
+
+.project-link i {
+ font-size: 1.25rem;
+}
+
+/* Status Badges */
+.development-badge,
+.stealth-badge {
+ position: absolute;
+ top: 2rem;
+ right: -4rem;
+ padding: 0.5rem 4rem;
+ transform: rotate(45deg);
+ font-size: 0.875rem;
+ font-weight: 500;
+ z-index: 2;
+ box-shadow: var(--shadow-md);
+ transition: all 0.3s ease;
+}
+
+.development-badge {
+ background: linear-gradient(135deg, var(--success-color), #34d399);
+ color: white;
+}
+
+.development-badge i {
+ margin-right: 0.5rem;
+ animation: spin 2s linear infinite;
+}
+
+.stealth {
+ background: linear-gradient(135deg,
+ rgba(23, 23, 23, 0.95),
+ rgba(32, 32, 32, 0.95)
+ );
+ backdrop-filter: blur(20px);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ position: relative;
+ overflow: hidden;
+}
+
+.stealth::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background:
+ repeating-linear-gradient(
+ 45deg,
+ transparent,
+ transparent 10px,
+ rgba(99, 102, 241, 0.03) 10px,
+ rgba(99, 102, 241, 0.03) 20px
+ );
+ pointer-events: none;
+}
+
+.stealth::after {
+ content: '';
+ position: absolute;
+ top: -50%;
+ left: -50%;
+ right: -50%;
+ bottom: -50%;
+ background: radial-gradient(
+ circle at center,
+ rgba(99, 102, 241, 0.1) 0%,
+ transparent 70%
+ );
+ opacity: 0.5;
+ animation: pulse 4s infinite ease-in-out;
+ pointer-events: none;
+}
+
+.stealth .project-title {
+ background: linear-gradient(135deg, #e2e8f0, #94a3b8);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ text-shadow: 0 0 30px rgba(255, 255, 255, 0.1);
+}
+
+.stealth .project-description {
+ color: #94a3b8;
+}
+
+.stealth .project-icon {
+ background: rgba(99, 102, 241, 0.1);
+ border: 1px solid rgba(99, 102, 241, 0.2);
+ box-shadow: 0 0 20px rgba(99, 102, 241, 0.1);
+}
+
+.stealth .tag {
+ background: rgba(99, 102, 241, 0.15);
+ color: #818cf8;
+ border: 1px solid rgba(99, 102, 241, 0.2);
+}
+
+.stealth-badge {
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.8), rgba(139, 92, 246, 0.8));
+ color: white;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+.stealth-badge i {
+ margin-right: 0.5rem;
+ animation: lockPulse 2s infinite ease-in-out;
+}
+
+@keyframes spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+@keyframes lockPulse {
+ 0% { transform: scale(1); }
+ 50% { transform: scale(1.2); filter: brightness(1.2); }
+ 100% { transform: scale(1); }
+}
+
+@keyframes pulse {
+ 0% { transform: scale(1); opacity: 0.5; }
+ 50% { transform: scale(1.2); opacity: 0.3; }
+ 100% { transform: scale(1); opacity: 0.5; }
+}
+
+.stealth:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 20px 40px rgba(99, 102, 241, 0.2);
+ border-color: rgba(99, 102, 241, 0.3);
+}
+
+.stealth:hover::before {
+ animation: scanline 2s linear infinite;
+}
+
+.stealth:hover .stealth-badge {
+ box-shadow: 0 0 20px rgba(99, 102, 241, 0.3);
+}
+
+@keyframes scanline {
+ 0% {
+ background-position: 0% 0%;
+ }
+ 100% {
+ background-position: 200% 200%;
+ }
+}
+
+/* Thoughts Section */
+.thoughts-grid {
+ display: grid;
+ gap: 2rem;
+ padding: 2rem 0;
+}
+
+.thought-card {
+ background: var(--background);
+ border-radius: 1rem;
+ box-shadow: var(--shadow-md);
+ overflow: hidden;
+}
+
+.thought-header {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 1.5rem;
+ cursor: pointer;
+ transition: var(--transition-normal);
+}
+
+.thought-header:hover {
+ background: var(--background-alt);
+}
+
+.thought-icon {
+ font-size: 1.5rem;
+}
+
+.thought-meta {
+ flex: 1;
+}
+
+.thought-category,
+.thought-date {
+ font-size: 0.875rem;
+ color: var(--text-light);
+}
+
+.expand-icon {
+ transition: var(--transition-normal);
+}
+
+.expand-icon.expanded {
+ transform: rotate(180deg);
+}
+
+.thought-content {
+ padding: 1.5rem;
+ border-top: 1px solid var(--background-alt);
+}
+
+/* Social Section */
+.social-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 2rem;
+ padding: 2rem 0;
+}
+
+.social-card {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 1.5rem;
+ background: var(--background);
+ border-radius: 1rem;
+ box-shadow: var(--shadow-md);
+ text-decoration: none;
+ color: var(--text-color);
+ transition: var(--transition-normal);
+}
+
+.social-card:hover {
+ transform: translateY(-5px);
+ box-shadow: var(--shadow-lg);
+}
+
+.social-icon {
+ font-size: 1.5rem;
+ width: 3rem;
+ height: 3rem;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(var(--accent-color-rgb, var(--primary-color-rgb)), 0.1);
+ border-radius: var(--radius-lg);
+ transition: var(--transition-normal);
+}
+
+.social-card:hover .social-icon {
+ transform: scale(1.1);
+}
+
+.social-info {
+ flex: 1;
+}
+
+.social-platform {
+ font-weight: 600;
+ margin-bottom: 0.25rem;
+}
+
+.social-username {
+ font-size: 0.875rem;
+ color: var(--text-light);
+}
+
+.social-arrow {
+ color: var(--accent-color, var(--primary-color));
+}
+
+/* Global Responsive Design */
+@media (max-width: 1024px) {
+ .container {
+ padding: 0 1.5rem;
+ }
+
+ section {
+ padding: clamp(3rem, 6vh, 5rem) 0;
+ }
+}
+
+@media (max-width: 768px) {
+ .about-content {
+ grid-template-columns: 1fr;
+ gap: 2.5rem;
+ }
+
+ .about-image {
+ max-width: 250px;
+ }
+
+ .about-text {
+ text-align: center;
+ }
+
+ .about-highlights {
+ grid-template-columns: 1fr;
+ max-width: 500px;
+ margin: 2rem auto 0;
+ }
+
+ .highlight {
+ justify-content: center;
+ }
+
+ /* Global mobile optimizations */
+ .container {
+ padding: 0 1rem;
+ }
+
+ .projects-grid,
+ .social-grid,
+ .thoughts-grid {
+ grid-template-columns: 1fr;
+ max-width: 500px;
+ margin: 0 auto;
+ }
+
+ .timeline::before {
+ left: 24px;
+ }
+
+ .timeline-item {
+ width: calc(100% - 48px);
+ margin-left: 48px !important;
+ margin-right: 0 !important;
+ }
+
+ .timeline-item:nth-child(odd) .timeline-icon,
+ .timeline-item:nth-child(even) .timeline-icon {
+ left: -36px !important;
+ right: auto !important;
+ transform: translateY(-50%) !important;
+ }
+
+ .timeline-content {
+ margin-left: 0;
+ }
+}
+
+@media (max-width: 480px) {
+ section {
+ padding: 2.5rem 0;
+ }
+
+ .about-image {
+ max-width: 200px;
+ }
+
+ .highlight {
+ padding: 0.75rem;
+ }
+
+ .highlight-icon {
+ width: 2rem;
+ height: 2rem;
+ font-size: 1rem;
+ }
+
+ .timeline {
+ padding: 1rem 0;
+ }
+
+ .timeline::before {
+ left: 24px;
+ }
+
+ .timeline-item {
+ width: calc(100% - 48px);
+ margin-left: 48px !important;
+ }
+
+ .timeline-item .timeline-icon {
+ width: 2rem;
+ height: 2rem;
+ left: -36px;
+ }
+
+ .timeline-icon i {
+ font-size: 0.875rem;
+ }
+
+ .timeline-content {
+ padding: 1rem;
+ }
+}
+
+/* Add smooth scrolling for all devices */
+html {
+ scroll-behavior: smooth;
+}
+
+/* Improve touch targets for mobile */
+@media (hover: none) and (pointer: coarse) {
+ .nav-links a,
+ .social-links a,
+ .project-card,
+ .thought-header,
+ .social-card {
+ padding: 0.75rem;
+ min-height: 44px;
+ }
+
+ .highlight {
+ min-height: 44px;
+ }
+}
+
+/* Markdown Content Styles */
+.markdown-content {
+ line-height: 1.7;
+ color: var(--text-color);
+ margin-top: 1.5rem;
+}
+
+.markdown-content h1,
+.markdown-content h2,
+.markdown-content h3,
+.markdown-content h4,
+.markdown-content h5,
+.markdown-content h6 {
+ margin: 1.5rem 0 1rem;
+ font-weight: 600;
+ line-height: 1.3;
+}
+
+.markdown-content h2 {
+ font-size: 1.5rem;
+ color: var(--primary-color);
+}
+
+.markdown-content h3 {
+ font-size: 1.25rem;
+}
+
+.markdown-content p {
+ margin: 1rem 0;
+}
+
+.markdown-content ul,
+.markdown-content ol {
+ margin: 1rem 0;
+ padding-left: 1.5rem;
+}
+
+.markdown-content li {
+ margin: 0.5rem 0;
+}
+
+.markdown-content code {
+ background: var(--background-alt);
+ padding: 0.2rem 0.4rem;
+ border-radius: 0.25rem;
+ font-size: 0.875em;
+ font-family: monospace;
+}
+
+.markdown-content pre {
+ background: var(--background-alt);
+ padding: 1rem;
+ border-radius: 0.5rem;
+ overflow-x: auto;
+ margin: 1rem 0;
+}
+
+.markdown-content pre code {
+ background: none;
+ padding: 0;
+}
+
+.markdown-content blockquote {
+ border-left: 4px solid var(--primary-color);
+ padding-left: 1rem;
+ margin: 1rem 0;
+ color: var(--text-light);
+}
+
+.markdown-content img {
+ max-width: 100%;
+ height: auto;
+ border-radius: 0.5rem;
+ margin: 1rem 0;
+}
+
+.markdown-content a {
+ color: var(--primary-color);
+ text-decoration: none;
+}
+
+.markdown-content a:hover {
+ text-decoration: underline;
+}
+
+/* LinkedIn Section */
+.linkedin-section {
+ padding: 4rem 0;
+ background-color: var(--background-alt);
+}
+
+.linkedin-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 2rem;
+ margin-top: 2rem;
+}
+
+.linkedin-card {
+ background: var(--background);
+ border-radius: var(--radius-lg);
+ padding: 1.5rem;
+ box-shadow: var(--shadow-md);
+ transition: var(--transition-normal);
+}
+
+.linkedin-card:hover {
+ transform: translateY(-5px);
+ box-shadow: var(--shadow-lg);
+}
+
+.linkedin-card h3 {
+ font-size: 1.25rem;
+ margin-bottom: 1rem;
+ color: var(--text-color);
+}
+
+.linkedin-embed-container {
+ position: relative;
+ width: 100%;
+ overflow: hidden;
+ border-radius: var(--radius-md);
+}
+
+.linkedin-embed-container iframe {
+ width: 100%;
+ border: none;
+ transition: var(--transition-normal);
+ background: var(--background);
+}
+
+@media (max-width: 768px) {
+ .linkedin-grid {
+ grid-template-columns: 1fr;
+ max-width: 600px;
+ margin: 2rem auto 0;
+ }
+
+ .linkedin-card {
+ padding: 1rem;
+ }
+}
+
+/* Icon Styles */
+.fa,
+.fas,
+.far,
+.fab {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 1em;
+ height: 1em;
+ vertical-align: middle;
+ font-style: normal;
+}
+
+/* Mobile Menu Icon */
+.mobile-menu-button i {
+ font-size: 1.5rem;
+ width: 1.5rem;
+ height: 1.5rem;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* Floating elements (keep them in the background) */
+.floating-elements {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 0;
+ pointer-events: none;
+}
+
+/* Responsive adjustments for wider screens */
+@media (min-width: 992px) {
+ .hero-section {
+ flex-direction: row;
+ justify-content: space-between;
+ padding: 8rem 0 4rem;
+ }
+
+ .hero-section .container {
+ flex-basis: 55%;
+ max-width: none;
+ margin: 0;
+ padding-left: 5%;
+ padding-right: 2%;
+ z-index: 1;
+ text-align: left;
+ }
+
+ .hero-content {
+ /* Adjust if needed based on .container changes */
+ }
+
+ .hero-text {
+ /* Adjust if needed based on .container changes */
+ }
+
+ .social-links {
+ justify-content: flex-start;
+ }
+
+ #earth-container {
+ position: relative;
+ flex-basis: 45%;
+ width: 45%;
+ height: 85vh;
+ min-height: 600px;
+ max-height: 1000px;
+ margin-top: 0;
+ max-width: none;
+ z-index: 2;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ #earth-container canvas {
+ max-width: none;
+ max-height: none;
+ width: 100% !important;
+ height: 100% !important;
+ }
+
+ .hero-section .container {
+ pointer-events: auto;
+ z-index: 1;
+ }
+}
+
+/* Remove the specific max-width media query as defaults handle mobile now */
+/* @media (max-width: 991.98px) { ... } */
+
+.cta-button {
+ margin-top: 2rem;
+ animation: fadeIn 1s ease-out 0.9s backwards;
+ pointer-events: auto;
+}
+
+.cta-button .btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 1rem 2rem;
+ font-size: 1.125rem;
+ font-weight: 600;
+ color: white;
+ background: var(--gradient-primary);
+ border-radius: var(--radius-full);
+ text-decoration: none;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ box-shadow: 0 4px 6px -1px rgba(var(--primary-color-rgb), 0.2),
+ 0 2px 4px -1px rgba(var(--primary-color-rgb), 0.1);
+ position: relative;
+ overflow: hidden;
+}
+
+.cta-button .btn::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0));
+ transform: translateX(-100%) rotate(45deg);
+ transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.cta-button .btn:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 15px -3px rgba(var(--primary-color-rgb), 0.2),
+ 0 4px 6px -2px rgba(var(--primary-color-rgb), 0.1);
+}
+
+.cta-button .btn:hover::before {
+ transform: translateX(100%) rotate(45deg);
+}
+
+.cta-button .btn:active {
+ transform: translateY(0);
+}
+
+.cta-button .btn i {
+ font-size: 1.25rem;
+}
+
+@media (max-width: 768px) {
+ .cta-button .btn {
+ padding: 0.875rem 1.75rem;
+ font-size: 1rem;
+ }
+}
+
+/* Filter Panel Styles */
+.filter-panel {
+ position: absolute;
+ top: 20px;
+ left: 20px;
+ background: rgba(23, 23, 23, 0.85);
+ padding: 0;
+ border-radius: 12px;
+ color: white;
+ z-index: 1000;
+ max-width: 300px;
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ transition: transform 0.3s ease, width 0.3s ease;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+}
+
+.filter-panel.collapsed {
+ transform: translateX(-280px);
+}
+
+.filter-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 15px 20px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.filter-header h3 {
+ margin: 0;
+ font-size: 1.1em;
+ font-weight: 600;
+ color: #fff;
+}
+
+.toggle-filters-btn {
+ background: none;
+ border: none;
+ color: #fff;
+ cursor: pointer;
+ padding: 5px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: transform 0.3s ease;
+}
+
+.filter-panel.collapsed .toggle-filters-btn i {
+ transform: rotate(180deg);
+}
+
+.filter-content {
+ padding: 20px;
+ overflow-y: auto;
+ max-height: calc(100vh - 200px);
+}
+
+.filter-group {
+ margin-bottom: 20px;
+}
+
+.filter-group label {
+ display: block;
+ margin-bottom: 8px;
+ color: #e2e8f0;
+ font-size: 0.9em;
+ font-weight: 500;
+}
+
+.filter-group select {
+ width: 100%;
+ padding: 8px 12px;
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 6px;
+ color: white;
+ outline: none;
+ font-size: 0.9em;
+ transition: all 0.2s ease;
+}
+
+.filter-group select:hover {
+ border-color: rgba(255, 255, 255, 0.3);
+}
+
+.filter-group select:focus {
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.2);
+}
+
+.filter-group select option {
+ background: #1a1a1a;
+ color: white;
+ padding: 8px;
+}
+
+.filter-group select[multiple] {
+ min-height: 100px;
+}
+
+.filter-actions {
+ display: flex;
+ gap: 10px;
+ margin-top: 20px;
+}
+
+.filter-button {
+ flex: 1;
+ padding: 8px 16px;
+ border: none;
+ border-radius: 6px;
+ font-weight: 500;
+ font-size: 0.9em;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.filter-button:first-child {
+ background: var(--primary-color);
+ color: white;
+}
+
+.filter-button:last-child {
+ background: rgba(255, 255, 255, 0.1);
+ color: white;
+}
+
+.filter-button:hover {
+ transform: translateY(-1px);
+ filter: brightness(1.1);
+}
+
+.filter-button:active {
+ transform: translateY(0);
+}
+
+@media (max-width: 768px) {
+ .filter-panel {
+ top: 10px;
+ left: 10px;
+ max-width: calc(100% - 20px);
+ }
+
+ .filter-panel.collapsed {
+ transform: translateX(calc(-100% + 40px));
+ }
+}
\ No newline at end of file
diff --git a/earth-ar.html b/earth-ar.html
new file mode 100644
index 0000000..7e21601
--- /dev/null
+++ b/earth-ar.html
@@ -0,0 +1,166 @@
+
+
+
+
+
+ Earth AR Visualization
+
+
+
+
+
+
🌍 Earth AR Visualization
+
Experience your flight routes in augmented reality!
+
+ Point your camera at a flat surface
+ Tap to place the earth
+ Explore your flight routes in 3D space
+
+
+
+
+
+
Loading AR...
+
Please wait while we initialize the AR session
+
+
+
AR Not Available
+
This device doesn't support AR or WebXR
+
+
+
+
+ Start AR Experience
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/earthGlobeVisualization.md b/earthGlobeVisualization.md
new file mode 100644
index 0000000..90d2354
--- /dev/null
+++ b/earthGlobeVisualization.md
@@ -0,0 +1,222 @@
+# Earth Globe Visualization in Three.js
+
+This document provides a detailed explanation of how the 3D Earth globe is implemented using Three.js, including all its layers, lighting systems, and visual effects.
+
+## Layer Structure
+
+The Earth visualization consists of multiple layered spheres, each serving a specific purpose:
+
+### 1. Base Earth Layer (Radius: 1.0)
+```javascript
+const globeGeometry = new THREE.SphereGeometry(1, 64, 64);
+const globeMaterial = new THREE.MeshPhongMaterial({
+ map: earthTexture, // Day texture (land, seas, etc.)
+ normalMap: normalTexture, // Surface detail/bump mapping
+ specularMap: specularTexture,// Reflectivity map
+ bumpMap: bumpTexture, // Additional surface detail
+ bumpScale: 0.05, // Intensity of the bump effect
+ specular: new THREE.Color(0x333333), // Color of specular highlights
+ shininess: 25 // Size/sharpness of specular highlights
+});
+```
+
+**Texture Maps Used:**
+- `earthTexture`: High-resolution day map showing continents, oceans
+- `normalTexture`: Adds surface detail through normal mapping
+- `specularTexture`: Controls which areas are reflective
+- `bumpTexture`: Provides additional surface relief
+
+### 2. Night Lights Layer (Radius: 1.001)
+```javascript
+const nightGeometry = new THREE.SphereGeometry(1.001, 64, 64);
+const nightMaterial = new THREE.MeshBasicMaterial({
+ map: nightTexture, // City lights texture
+ blending: THREE.AdditiveBlending,
+ transparent: true,
+ opacity: state.isDaylight ? 0 : 0.8,
+ depthWrite: false
+});
+```
+
+**Key Features:**
+- Slightly larger radius to prevent z-fighting
+- Additive blending for realistic light glow
+- Dynamic opacity based on day/night state
+- No depth writing to prevent visual artifacts
+
+### 3. Cloud Layer (Radius: 1.008)
+```javascript
+const cloudGeometry = new THREE.SphereGeometry(1.008, 64, 64);
+const cloudMaterial = new THREE.MeshPhongMaterial({
+ map: cloudsTexture,
+ transparent: true,
+ opacity: state.isDaylight ? 0.3 : 0.1,
+ depthWrite: false,
+ side: THREE.DoubleSide,
+ blending: THREE.NormalBlending
+});
+```
+
+**Characteristics:**
+- Outermost layer for cloud coverage
+- Semi-transparent for realistic cloud effect
+- Double-sided rendering
+- Dynamic opacity based on day/night state
+
+## Lighting System
+
+The Earth uses a complex lighting setup with three light types:
+
+### 1. Ambient Light
+```javascript
+const ambientLight = new THREE.AmbientLight(0xffffff, state.isDaylight ? 1.0 : 0.1);
+```
+- Provides base illumination
+- Intensity varies with day/night state
+- Ensures shadows aren't too dark
+
+### 2. Directional Light (Sun)
+```javascript
+const directionalLight = new THREE.DirectionalLight(0xffffff, state.isDaylight ? 2.0 : 0.15);
+directionalLight.position.set(5, 3, 5);
+```
+- Simulates sunlight
+- Creates realistic shadows and highlights
+- Position determines day/night regions
+
+### 3. Hemisphere Light
+```javascript
+const hemisphereLight = new THREE.HemisphereLight(
+ 0xffffff, // Sky color
+ 0x444444, // Ground color
+ state.isDaylight ? 1.0 : 0
+);
+```
+- Provides subtle atmospheric lighting
+- Improves realism of day lighting
+- Automatically disabled at night
+
+## Day/Night Transition System
+
+The visualization includes a smooth transition system between day and night states:
+
+```javascript
+function transitionDayNight(state, globeMaterial) {
+ // Target values for day/night transition
+ const targetValues = {
+ nightOpacity: state.isDaylight ? 0 : 0.8,
+ cloudOpacity: state.isDaylight ? 0.4 : 0.2,
+ ambientIntensity: state.isDaylight ? 1.0 : 0.1,
+ directionalIntensity: state.isDaylight ? 2.0 : 0.15,
+ hemisphereIntensity: state.isDaylight ? 1.0 : 0
+ };
+
+ // Smooth easing function
+ const eased = progress < 0.5
+ ? 2 * progress * progress
+ : 1 - Math.pow(-2 * progress + 2, 2) / 2;
+
+ // Apply transitions
+ updateLightingValues(eased, currentValues, targetValues);
+}
+```
+
+## Visual Effects and Post-Processing
+
+### 1. Atmosphere Effect
+The atmosphere is simulated through a combination of:
+- Cloud layer transparency
+- Hemisphere lighting
+- Bloom post-processing
+
+### 2. Bloom Effect
+```javascript
+const bloomPass = new UnrealBloomPass(
+ new THREE.Vector2(window.innerWidth, window.innerHeight),
+ 1.5, // Intensity
+ 0.4, // Radius
+ 0.85 // Threshold
+);
+```
+- Adds realistic glow to bright areas
+- Enhances night lights
+- Creates subtle atmospheric scatter
+
+## Performance Considerations
+
+### 1. Geometry Optimization
+- Using appropriate polygon counts (64 segments)
+- Shared geometries between instances
+- Proper disposal of unused resources
+
+### 2. Texture Management
+```javascript
+const textureLoader = new THREE.TextureLoader();
+// Preload and reuse textures
+const earthTexture = textureLoader.load('./textures/earth_albedo.jpg', onTextureLoaded);
+```
+- Preloading of textures
+- Proper texture compression
+- Mipmap generation for distant views
+
+### 3. Render Layer Organization
+- Proper layer ordering
+- Strategic use of transparency
+- Depth testing optimization
+
+## Required Textures
+
+For a complete Earth visualization, you need these texture maps:
+
+1. **Day Map** (`earth_albedo.jpg`)
+ - Color texture of Earth during daylight
+ - Shows continents, oceans, terrain
+
+2. **Night Map** (`earth_night.jpg`)
+ - City lights and illuminated areas
+ - Used for night side visualization
+
+3. **Normal Map** (`earth_normal.jpg`)
+ - Surface detail and terrain information
+ - Enhances visual depth
+
+4. **Specular Map** (`earth_specular.jpg`)
+ - Controls surface reflectivity
+ - Different for land and water
+
+5. **Cloud Map** (`earth_clouds.jpg`)
+ - Cloud coverage patterns
+ - Semi-transparent white clouds
+
+6. **Bump Map** (`earth_bump.jpg`)
+ - Additional surface detail
+ - Enhances terrain visualization
+
+## Implementation Tips
+
+1. **Texture Resolution**
+ - Use power-of-two textures (2048x1024, 4096x2048)
+ - Consider device performance for texture size
+ - Implement progressive loading for mobile
+
+2. **Layer Order**
+ - Base Earth (1.0)
+ - Night Lights (1.001)
+ - Clouds (1.008)
+ - Maintain small radius differences
+
+3. **Lighting Setup**
+ - Position lights for realistic shadows
+ - Adjust intensities for desired contrast
+ - Consider performance vs. quality
+
+4. **Animation**
+ - Smooth cloud layer rotation
+ - Day/night transition timing
+ - Performance-conscious update rate
+
+## Resources
+
+- [Earth Texture Maps](https://visibleearth.nasa.gov/collection/1484/blue-marble)
+- [Normal Map Generation](http://cpetry.github.io/NormalMap-Online/)
+- [Three.js Earth Example](https://threejs.org/examples/#webgl_materials_earth)
\ No newline at end of file
diff --git a/flightRouteVisualization.md b/flightRouteVisualization.md
new file mode 100644
index 0000000..d62dc0e
--- /dev/null
+++ b/flightRouteVisualization.md
@@ -0,0 +1,244 @@
+# Flight Globe Visualization with Three.js
+
+This document explains the implementation of a 3D flight path visualization using Three.js, similar to the one used in flight tracking applications.
+
+## Overview
+
+The visualization creates an interactive 3D globe with flight paths rendered as curved lines between airports. The implementation includes multiple visual layers, dynamic lighting, and smooth animations.
+
+## Core Components
+
+### 1. Globe Structure
+
+The globe consists of multiple layers:
+- Base Earth layer with day texture
+- Night lights layer for city illumination
+- Cloud layer with transparency
+- Flight paths rendered on top
+
+```javascript
+const globeGeometry = new THREE.SphereGeometry(1, 64, 64);
+const globeMaterial = new THREE.MeshPhongMaterial({
+ map: earthTexture,
+ normalMap: normalTexture,
+ specularMap: specularTexture,
+ bumpMap: bumpTexture,
+ bumpScale: 0.05,
+ specular: new THREE.Color(0x333333),
+ shininess: 25
+});
+```
+
+### 2. Flight Path Generation
+
+Each flight path is created using these key steps:
+
+1. **Coordinate Conversion**
+```javascript
+// Convert airport lat/long to 3D vectors
+const startPoint = latLngToVector3(fromLat, fromLng);
+const endPoint = latLngToVector3(toLat, toLng);
+```
+
+2. **Path Curvature**
+```javascript
+// Calculate height scale based on distance
+const maxHeightScale = 0.08;
+const baseScale = Math.atan(distance) / (Math.PI / 2) * maxHeightScale;
+
+// Generate curved path points
+const points = [];
+for (let i = 0; i <= numPoints; i++) {
+ const t = i / numPoints;
+ const point = startPoint.clone().normalize();
+ point.lerp(endPoint.clone().normalize(), t).normalize();
+ const heightScale = 1 + baseScale * Math.sin(Math.PI * t);
+ point.multiplyScalar(heightScale);
+ points.push(point);
+}
+```
+
+3. **Smooth Curve Creation**
+```javascript
+const curve = new THREE.CatmullRomCurve3(points, false, 'catmullrom', 0.5);
+```
+
+### 3. Multiple Flight Lines
+
+For busy routes, multiple parallel lines are created to represent frequency:
+
+```javascript
+// Calculate perpendicular direction for parallel lines
+const pathDirection = curveEnd.clone().sub(curveStart).normalize();
+const globeNormal = curveStart.clone().add(curveEnd).normalize();
+const perpDirection = pathDirection.clone().cross(globeNormal).normalize();
+
+// Create offset lines
+for (let i = 0; i < numLines; i++) {
+ const offset = perpDirection.clone().multiplyScalar(0.001 * (i - (numLines - 1) / 2));
+ const points = basePoints.map(point => {
+ return point.clone().add(offset);
+ });
+ // Create line with offset
+}
+```
+
+### 4. Visual Effects
+
+#### Flight Path Material
+```javascript
+const lineMaterial = new THREE.LineBasicMaterial({
+ color: airlineColors[airline] || 0xffffff,
+ transparent: true,
+ opacity: 0.8,
+ linewidth: state.lineWidth,
+ depthTest: true,
+ depthWrite: false
+});
+```
+
+#### Airport Markers
+```javascript
+const dotGeometry = new THREE.SphereGeometry(0.005, 16, 16);
+const dotMaterial = new THREE.MeshBasicMaterial({
+ color: airlineColors[airline] || 0xffffff,
+ transparent: true,
+ opacity: 0.8
+});
+```
+
+### 5. Post-Processing Effects
+
+The visualization uses post-processing for enhanced visual appeal:
+
+```javascript
+// Setup effect composer
+const composer = new EffectComposer(renderer);
+const renderPass = new RenderPass(scene, camera);
+composer.addPass(renderPass);
+
+// Add bloom effect
+const bloomPass = new UnrealBloomPass(
+ new THREE.Vector2(window.innerWidth, window.innerHeight),
+ 1.5, // Bloom intensity
+ 0.4, // Bloom radius
+ 0.85 // Bloom threshold
+);
+composer.addPass(bloomPass);
+```
+
+## Performance Optimizations
+
+1. **Efficient Geometry**
+```javascript
+const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
+```
+
+2. **Visibility Management**
+```javascript
+// Update visibility based on filters
+flightPath.line.visible = visible;
+flightPath.additionalLines.forEach(line => line.visible = visible);
+```
+
+3. **Resource Cleanup**
+```javascript
+// Dispose of geometries and materials when removing flight paths
+path.line.geometry.dispose();
+path.line.material.dispose();
+```
+
+## Implementation Guide
+
+To implement this visualization in your own project:
+
+1. **Basic Setup**
+```javascript
+const scene = new THREE.Scene();
+const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
+const renderer = new THREE.WebGLRenderer({ antialias: true });
+```
+
+2. **Create Earth**
+```javascript
+const globeGeometry = new THREE.SphereGeometry(1, 64, 64);
+const globeMaterial = new THREE.MeshPhongMaterial({
+ map: earthTexture,
+ normalMap: normalTexture,
+ specularMap: specularTexture,
+ bumpMap: bumpTexture
+});
+const globe = new THREE.Mesh(globeGeometry, globeMaterial);
+scene.add(globe);
+```
+
+3. **Add Flight Paths**
+```javascript
+function createFlightPath(startPoint, endPoint) {
+ const points = [];
+ const numPoints = 50;
+ const maxHeight = 0.08;
+
+ for (let i = 0; i <= numPoints; i++) {
+ const t = i / numPoints;
+ const point = startPoint.clone().normalize();
+ point.lerp(endPoint.clone().normalize(), t).normalize();
+ const heightScale = 1 + maxHeight * Math.sin(Math.PI * t);
+ point.multiplyScalar(heightScale);
+ points.push(point);
+ }
+
+ const curve = new THREE.CatmullRomCurve3(points);
+ const geometry = new THREE.BufferGeometry().setFromPoints(curve.getPoints(50));
+ const material = new THREE.LineBasicMaterial({
+ color: 0xffffff,
+ transparent: true,
+ opacity: 0.8
+ });
+
+ return new THREE.Line(geometry, material);
+}
+```
+
+4. **Setup Post-Processing**
+```javascript
+const composer = new EffectComposer(renderer);
+const renderPass = new RenderPass(scene, camera);
+const bloomPass = new UnrealBloomPass(
+ new THREE.Vector2(window.innerWidth, window.innerHeight),
+ 1.5, 0.4, 0.85
+);
+composer.addPass(renderPass);
+composer.addPass(bloomPass);
+```
+
+5. **Animation Loop**
+```javascript
+function animate() {
+ requestAnimationFrame(animate);
+ // Update any animations
+ composer.render();
+}
+```
+
+## Key Tips for Success
+
+1. **Smooth Curves**: Use Catmull-Rom curves for natural-looking flight paths
+2. **Multiple Lines**: Implement parallel lines for busy routes
+3. **Visual Effects**: Add bloom and glow effects for enhanced appearance
+4. **Performance**: Use BufferGeometry and implement proper cleanup
+5. **Depth Testing**: Configure proper depth testing and transparency
+6. **Animation**: Implement smooth transitions and animations
+
+## Required Dependencies
+
+- Three.js
+- EffectComposer
+- UnrealBloomPass
+- OrbitControls (for interaction)
+
+## Resources
+
+- [Three.js Documentation](https://threejs.org/docs/)
+- [WebGL Fundamentals](https://webglfundamentals.org/)
+- [Three.js Examples](https://threejs.org/examples/)
\ No newline at end of file
diff --git a/flights-ar.html b/flights-ar.html
new file mode 100644
index 0000000..cea3071
--- /dev/null
+++ b/flights-ar.html
@@ -0,0 +1,325 @@
+---
+layout: default
+title: Flights AR Experience
+permalink: /flights/ar/
+---
+
+
+
+
+
+
+ Flights AR Experience
+
+
+
+
+
← Back to Flights
+
+
+
🌍 Flight Routes in AR
+
Experience your flight routes in augmented reality!
+
+ Point your camera at a flat surface
+ Tap to place the Earth
+ Explore your flight routes in 3D space
+
+
+
+
+
+
Loading AR...
+
Please wait while we initialize the AR session
+
+
+
AR Not Available
+
This device doesn't support AR or WebXR
+
+
+
+
+ Start AR Experience
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/flights.html b/flights.html
new file mode 100644
index 0000000..170de8f
--- /dev/null
+++ b/flights.html
@@ -0,0 +1,267 @@
+---
+layout: default
+title: Flights
+permalink: /flights/
+---
+
+
+
+
+
+
+ Year
+
+
+
+
+
+ Airline
+
+
+
+
+
+ Apply
+ Reset
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/flights/stats.html b/flights/stats.html
new file mode 100644
index 0000000..0804a02
--- /dev/null
+++ b/flights/stats.html
@@ -0,0 +1,338 @@
+---
+layout: default
+title: Flight Statistics
+permalink: /flights/stats/
+---
+
+
+
+
+
+
+
+
+
🌍
+
+
Calculating...
+
Total Distance
+
kilometers
+
+
+
+
+
✈️
+
+
Calculating...
+
Total Flights
+
flights
+
+
+
+
+
🏛️
+
+
Calculating...
+
Countries Visited
+
countries
+
+
+
+
+
📅
+
+
Calculating...
+
Years of Travel
+
years
+
+
+
+
+
+
+
Distance and Flights by Year
+
+
+
+
+
+
+
Distance by Companion
+
+
+
+
+
+
+
Cumulative Distance Over Time
+
+
+
+
+
+
+
Distance by Month
+
+
+
+
+
+
+
Top 10 Airports Over Time
+
+
+
+
+
+
+
+
+
Top Destinations
+
+
+
+
+
+
+
Most Frequent Airlines
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/index.html b/index.html
index be5d95a..81b9c92 100644
--- a/index.html
+++ b/index.html
@@ -1 +1,68 @@
-"Hello World"
\ No newline at end of file
+---
+layout: default
+---
+
+
+
+
+
+
+ Patrick Freyer | Personal Website
+
+
+
+
+
+
+
+
+
+
+
+
+ {% include hero.html %}
+
+ {% assign profiles = site.data.social %}
+
+ {% include about.html %}
+ {% include social.html profiles=profiles %}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/js/flight-stats.js b/js/flight-stats.js
new file mode 100644
index 0000000..bc3cbb8
--- /dev/null
+++ b/js/flight-stats.js
@@ -0,0 +1,726 @@
+// Flight Statistics Calculator
+class FlightStatsCalculator {
+ constructor(locationsData, flightRoutesData) {
+ this.locations = locationsData;
+ this.flights = flightRoutesData;
+ this.locationMap = this.createLocationMap();
+ this.init();
+ }
+
+ createLocationMap() {
+ const map = {};
+ this.locations.forEach(location => {
+ map[location.name] = {
+ lat: location.lat,
+ lon: location.lon
+ };
+ });
+ return map;
+ }
+
+ // Calculate distance between two points using Haversine formula
+ calculateDistance(lat1, lon1, lat2, lon2) {
+ const R = 6371; // Earth's radius in kilometers
+ const dLat = this.toRadians(lat2 - lat1);
+ const dLon = this.toRadians(lon2 - lon1);
+ const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
+ Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) *
+ Math.sin(dLon / 2) * Math.sin(dLon / 2);
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ return R * c;
+ }
+
+ toRadians(degrees) {
+ return degrees * (Math.PI / 180);
+ }
+
+ // Calculate total distance for all flights
+ calculateTotalDistance() {
+ let totalDistance = 0;
+ this.flights.forEach(flight => {
+ const origin = this.locationMap[flight.origin];
+ const destination = this.locationMap[flight.destination];
+
+ if (origin && destination) {
+ const distance = this.calculateDistance(
+ origin.lat, origin.lon,
+ destination.lat, destination.lon
+ );
+ totalDistance += distance;
+ }
+ });
+ return Math.round(totalDistance*1.07);
+ }
+
+ // Get statistics by year
+ getStatsByYear() {
+ const yearStats = {};
+ this.flights.forEach(flight => {
+ const year = flight.year;
+ if (!yearStats[year]) {
+ yearStats[year] = { distance: 0, count: 0 };
+ }
+
+ const origin = this.locationMap[flight.origin];
+ const destination = this.locationMap[flight.destination];
+
+ if (origin && destination) {
+ const distance = this.calculateDistance(
+ origin.lat, origin.lon,
+ destination.lat, destination.lon
+ );
+ yearStats[year].distance += distance;
+ yearStats[year].count += 1;
+ }
+ });
+ return yearStats;
+ }
+
+ // Get statistics by companion
+ getStatsByCompanion() {
+ const companionStats = {};
+ this.flights.forEach(flight => {
+ if (flight.travelers) {
+ flight.travelers.forEach(traveler => {
+ if (traveler !== "Patrick") { // Exclude self
+ if (!companionStats[traveler]) {
+ companionStats[traveler] = { distance: 0, count: 0 };
+ }
+
+ const origin = this.locationMap[flight.origin];
+ const destination = this.locationMap[flight.destination];
+
+ if (origin && destination) {
+ const distance = this.calculateDistance(
+ origin.lat, origin.lon,
+ destination.lat, destination.lon
+ );
+ companionStats[traveler].distance += distance;
+ companionStats[traveler].count += 1;
+ }
+ }
+ });
+ }
+ });
+ return companionStats;
+ }
+
+ // Get cumulative distance data over time
+ getCumulativeDistanceData() {
+ const sortedFlights = this.flights
+ .map(flight => {
+ const origin = this.locationMap[flight.origin];
+ const destination = this.locationMap[flight.destination];
+ if (origin && destination) {
+ return {
+ ...flight,
+ distance: this.calculateDistance(
+ origin.lat, origin.lon,
+ destination.lat, destination.lon
+ )
+ };
+ }
+ return null;
+ })
+ .filter(flight => flight !== null)
+ .sort((a, b) => {
+ if (a.year !== b.year) return a.year - b.year;
+ if (a.month !== b.month) return a.month - b.month;
+ return (a.day || 1) - (b.day || 1);
+ });
+
+ let cumulativeDistance = 0;
+ const data = [];
+
+ sortedFlights.forEach(flight => {
+ cumulativeDistance += flight.distance;
+ const dateStr = flight.day ?
+ `${flight.year}-${String(flight.month).padStart(2, '0')}-${String(flight.day).padStart(2, '0')}` :
+ `${flight.year}-${String(flight.month).padStart(2, '0')}-01`;
+ data.push({
+ date: dateStr,
+ cumulative: Math.round(cumulativeDistance)
+ });
+ });
+
+ return data;
+ }
+
+ // Get statistics by month across all years
+ getStatsByMonth() {
+ const monthStats = {};
+ const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+
+ this.flights.forEach(flight => {
+ const monthIndex = flight.month - 1;
+ const monthName = monthNames[monthIndex];
+
+ if (!monthStats[monthName]) {
+ monthStats[monthName] = { distance: 0, count: 0 };
+ }
+
+ const origin = this.locationMap[flight.origin];
+ const destination = this.locationMap[flight.destination];
+
+ if (origin && destination) {
+ const distance = this.calculateDistance(
+ origin.lat, origin.lon,
+ destination.lat, destination.lon
+ );
+ monthStats[monthName].distance += distance;
+ monthStats[monthName].count += 1;
+ }
+ });
+
+ // Ensure all months are present
+ monthNames.forEach(month => {
+ if (!monthStats[month]) {
+ monthStats[month] = { distance: 0, count: 0 };
+ }
+ });
+
+ return monthStats;
+ }
+
+ // Get airport visit counts over time
+ getAirportTimelineData() {
+ const airportStats = {};
+ const sortedFlights = this.flights.sort((a, b) => {
+ if (a.year !== b.year) return a.year - b.year;
+ return a.month - b.month;
+ });
+
+ sortedFlights.forEach(flight => {
+ // Count both origin and destination
+ [flight.origin, flight.destination].forEach(airport => {
+ if (!airportStats[airport]) {
+ airportStats[airport] = { count: 0, firstYear: flight.year };
+ }
+ airportStats[airport].count += 1;
+ });
+ });
+
+ // Get top 10 airports by visit count
+ const topAirports = Object.entries(airportStats)
+ .sort(([,a], [,b]) => b.count - a.count)
+ .slice(0, 10);
+
+ return topAirports;
+ }
+
+ // Get unique countries visited
+ getUniqueCountries() {
+ const countries = new Set();
+ this.flights.forEach(flight => {
+ countries.add(flight.origin);
+ countries.add(flight.destination);
+ });
+ return countries.size;
+ }
+
+ // Get top destinations
+ getTopDestinations() {
+ const destinations = {};
+ this.flights.forEach(flight => {
+ const dest = flight.destination;
+ destinations[dest] = (destinations[dest] || 0) + 1;
+ });
+
+ return Object.entries(destinations)
+ .sort(([,a], [,b]) => b - a)
+ .slice(0, 10);
+ }
+
+ // Get top airlines
+ getTopAirlines() {
+ const airlines = {};
+ this.flights.forEach(flight => {
+ const airline = flight.airline;
+ airlines[airline] = (airlines[airline] || 0) + 1;
+ });
+
+ return Object.entries(airlines)
+ .sort(([,a], [,b]) => b - a)
+ .slice(0, 10);
+ }
+
+ // Update the UI with calculated statistics
+ updateUI() {
+ const totalDistance = this.calculateTotalDistance();
+ const totalFlights = this.flights.length;
+ const countriesVisited = this.getUniqueCountries();
+ const years = new Set(this.flights.map(f => f.year)).size;
+
+ document.getElementById('total-distance').textContent = totalDistance.toLocaleString();
+ document.getElementById('total-flights').textContent = totalFlights;
+ document.getElementById('countries-visited').textContent = countriesVisited;
+ document.getElementById('years-traveled').textContent = years;
+
+ this.createYearChart();
+ this.createCompanionChart();
+ this.createCumulativeChart();
+ this.createMonthlyChart();
+ this.createAirportsTimelineChart();
+ this.updateTopDestinations();
+ this.updateTopAirlines();
+ }
+
+ createYearChart() {
+ const yearStats = this.getStatsByYear();
+ const years = Object.keys(yearStats).sort();
+ const distances = years.map(year => Math.round(yearStats[year].distance));
+ const flightCounts = years.map(year => yearStats[year].count);
+
+ const ctx = document.getElementById('year-chart').getContext('2d');
+ new Chart(ctx, {
+ type: 'bar',
+ data: {
+ labels: years,
+ datasets: [
+ {
+ label: 'Distance (km)',
+ data: distances,
+ backgroundColor: 'rgba(37, 99, 235, 0.8)',
+ borderColor: 'rgba(37, 99, 235, 1)',
+ borderWidth: 1,
+ borderRadius: 4,
+ yAxisID: 'y'
+ },
+ {
+ label: 'Number of Flights',
+ data: flightCounts,
+ backgroundColor: 'rgba(16, 185, 129, 0.8)',
+ borderColor: 'rgba(16, 185, 129, 1)',
+ borderWidth: 1,
+ borderRadius: 4,
+ yAxisID: 'y1'
+ }
+ ]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ display: true,
+ position: 'top',
+ labels: {
+ color: '#6b7280',
+ font: {
+ family: 'Inter'
+ },
+ usePointStyle: true,
+ padding: 20
+ }
+ },
+ tooltip: {
+ mode: 'index',
+ intersect: false,
+ callbacks: {
+ label: function(context) {
+ if (context.dataset.label === 'Distance (km)') {
+ return `Distance: ${context.parsed.y.toLocaleString()} km`;
+ } else {
+ return `Flights: ${context.parsed.y}`;
+ }
+ }
+ }
+ }
+ },
+ scales: {
+ y: {
+ type: 'linear',
+ display: true,
+ position: 'left',
+ beginAtZero: true,
+ ticks: {
+ color: '#6b7280',
+ font: {
+ family: 'Inter'
+ }
+ },
+ grid: {
+ color: 'rgba(0, 0, 0, 0.1)'
+ },
+ title: {
+ display: true,
+ text: 'Distance (km)',
+ color: '#6b7280',
+ font: {
+ family: 'Inter'
+ }
+ }
+ },
+ y1: {
+ type: 'linear',
+ display: true,
+ position: 'right',
+ beginAtZero: true,
+ ticks: {
+ color: '#6b7280',
+ font: {
+ family: 'Inter'
+ }
+ },
+ grid: {
+ drawOnChartArea: false
+ },
+ title: {
+ display: true,
+ text: 'Number of Flights',
+ color: '#6b7280',
+ font: {
+ family: 'Inter'
+ }
+ }
+ },
+ x: {
+ ticks: {
+ color: '#6b7280',
+ font: {
+ family: 'Inter'
+ }
+ },
+ grid: {
+ display: false
+ }
+ }
+ }
+ }
+ });
+ }
+
+ createCompanionChart() {
+ const companionStats = this.getStatsByCompanion();
+ const companions = Object.keys(companionStats);
+ const distances = companions.map(companion => Math.round(companionStats[companion].distance));
+
+ const ctx = document.getElementById('companion-chart').getContext('2d');
+ new Chart(ctx, {
+ type: 'doughnut',
+ data: {
+ labels: companions,
+ datasets: [{
+ data: distances,
+ backgroundColor: [
+ 'rgba(37, 99, 235, 0.8)',
+ 'rgba(99, 102, 241, 0.8)',
+ 'rgba(139, 92, 246, 0.8)',
+ 'rgba(16, 185, 129, 0.8)',
+ 'rgba(245, 158, 11, 0.8)',
+ 'rgba(239, 68, 68, 0.8)',
+ 'rgba(107, 114, 128, 0.8)',
+ 'rgba(156, 163, 175, 0.8)'
+ ],
+ borderColor: [
+ 'rgba(37, 99, 235, 1)',
+ 'rgba(99, 102, 241, 1)',
+ 'rgba(139, 92, 246, 1)',
+ 'rgba(16, 185, 129, 1)',
+ 'rgba(245, 158, 11, 1)',
+ 'rgba(239, 68, 68, 1)',
+ 'rgba(107, 114, 128, 1)',
+ 'rgba(156, 163, 175, 1)'
+ ],
+ borderWidth: 2
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ position: 'bottom',
+ labels: {
+ color: '#6b7280',
+ font: {
+ family: 'Inter'
+ },
+ padding: 20
+ }
+ }
+ },
+ cutout: '60%'
+ }
+ });
+ }
+
+ createCumulativeChart() {
+ const cumulativeData = this.getCumulativeDistanceData();
+ const labels = cumulativeData.map(d => d.date);
+ const distances = cumulativeData.map(d => d.cumulative);
+
+ const ctx = document.getElementById('cumulative-chart').getContext('2d');
+ new Chart(ctx, {
+ type: 'line',
+ data: {
+ labels: labels,
+ datasets: [{
+ label: 'Cumulative Distance (km)',
+ data: distances,
+ borderColor: 'rgba(37, 99, 235, 1)',
+ backgroundColor: 'rgba(37, 99, 235, 0.1)',
+ borderWidth: 3,
+ fill: true,
+ tension: 0.4,
+ pointBackgroundColor: 'rgba(37, 99, 235, 1)',
+ pointBorderColor: '#ffffff',
+ pointBorderWidth: 2,
+ pointRadius: 4,
+ pointHoverRadius: 6
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ display: false
+ },
+ tooltip: {
+ callbacks: {
+ label: function(context) {
+ return `Total Distance: ${context.parsed.y.toLocaleString()} km`;
+ }
+ }
+ }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ ticks: {
+ color: '#6b7280',
+ font: {
+ family: 'Inter'
+ },
+ callback: function(value) {
+ return (value/1000).toFixed(0) + 'K';
+ }
+ },
+ grid: {
+ color: 'rgba(0, 0, 0, 0.1)'
+ }
+ },
+ x: {
+ ticks: {
+ color: '#6b7280',
+ font: {
+ family: 'Inter'
+ },
+ maxTicksLimit: 10
+ },
+ grid: {
+ display: false
+ }
+ }
+ }
+ }
+ });
+ }
+
+ createMonthlyChart() {
+ const monthStats = this.getStatsByMonth();
+ const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+ const distances = monthNames.map(month => Math.round(monthStats[month].distance));
+
+ const ctx = document.getElementById('monthly-chart').getContext('2d');
+ new Chart(ctx, {
+ type: 'bar',
+ data: {
+ labels: monthNames,
+ datasets: [{
+ label: 'Distance (km)',
+ data: distances,
+ backgroundColor: [
+ 'rgba(37, 99, 235, 0.8)', 'rgba(99, 102, 241, 0.8)', 'rgba(139, 92, 246, 0.8)',
+ 'rgba(16, 185, 129, 0.8)', 'rgba(5, 150, 105, 0.8)', 'rgba(245, 158, 11, 0.8)',
+ 'rgba(251, 191, 36, 0.8)', 'rgba(239, 68, 68, 0.8)', 'rgba(220, 38, 127, 0.8)',
+ 'rgba(107, 114, 128, 0.8)', 'rgba(75, 85, 99, 0.8)', 'rgba(55, 65, 81, 0.8)'
+ ],
+ borderColor: [
+ 'rgba(37, 99, 235, 1)', 'rgba(99, 102, 241, 1)', 'rgba(139, 92, 246, 1)',
+ 'rgba(16, 185, 129, 1)', 'rgba(5, 150, 105, 1)', 'rgba(245, 158, 11, 1)',
+ 'rgba(251, 191, 36, 1)', 'rgba(239, 68, 68, 1)', 'rgba(220, 38, 127, 1)',
+ 'rgba(107, 114, 128, 1)', 'rgba(75, 85, 99, 1)', 'rgba(55, 65, 81, 1)'
+ ],
+ borderWidth: 1,
+ borderRadius: 4
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ display: false
+ },
+ tooltip: {
+ callbacks: {
+ label: function(context) {
+ return `Distance: ${context.parsed.y.toLocaleString()} km`;
+ }
+ }
+ }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ ticks: {
+ color: '#6b7280',
+ font: {
+ family: 'Inter'
+ }
+ },
+ grid: {
+ color: 'rgba(0, 0, 0, 0.1)'
+ }
+ },
+ x: {
+ ticks: {
+ color: '#6b7280',
+ font: {
+ family: 'Inter'
+ }
+ },
+ grid: {
+ display: false
+ }
+ }
+ }
+ }
+ });
+ }
+
+ createAirportsTimelineChart() {
+ const airportData = this.getAirportTimelineData();
+ const airports = airportData.map(([airport]) => airport);
+ const counts = airportData.map(([, data]) => data.count);
+
+ const ctx = document.getElementById('airports-timeline-chart').getContext('2d');
+ new Chart(ctx, {
+ type: 'bar',
+ data: {
+ labels: airports,
+ datasets: [{
+ label: 'Visits',
+ data: counts,
+ backgroundColor: 'rgba(37, 99, 235, 0.8)',
+ borderColor: 'rgba(37, 99, 235, 1)',
+ borderWidth: 1,
+ borderRadius: 4
+ }]
+ },
+ options: {
+ indexAxis: 'y',
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ display: false
+ },
+ tooltip: {
+ callbacks: {
+ label: function(context) {
+ return `Visits: ${context.parsed.x}`;
+ }
+ }
+ }
+ },
+ scales: {
+ x: {
+ beginAtZero: true,
+ ticks: {
+ color: '#6b7280',
+ font: {
+ family: 'Inter'
+ }
+ },
+ grid: {
+ color: 'rgba(0, 0, 0, 0.1)'
+ }
+ },
+ y: {
+ ticks: {
+ color: '#6b7280',
+ font: {
+ family: 'Inter',
+ size: 11
+ }
+ },
+ grid: {
+ display: false
+ }
+ }
+ }
+ }
+ });
+ }
+
+ updateTopDestinations() {
+ const topDestinations = this.getTopDestinations();
+ const container = document.getElementById('top-destinations');
+ container.innerHTML = '';
+
+ topDestinations.forEach(([destination, count]) => {
+ const item = document.createElement('div');
+ item.className = 'destination-item';
+ item.innerHTML = `
+ ${destination}
+ ${count}
+ `;
+ container.appendChild(item);
+ });
+ }
+
+ updateTopAirlines() {
+ const topAirlines = this.getTopAirlines();
+ const container = document.getElementById('top-airlines');
+ container.innerHTML = '';
+
+ topAirlines.forEach(([airline, count]) => {
+ const item = document.createElement('div');
+ item.className = 'airline-item';
+ item.innerHTML = `
+ ${airline}
+ ${count}
+ `;
+ container.appendChild(item);
+ });
+ }
+
+ init() {
+ this.updateUI();
+ }
+}
+
+// Initialize when DOM is loaded
+document.addEventListener('DOMContentLoaded', function() {
+ console.log('DOM loaded, checking for data...');
+
+ if (typeof locationsData !== 'undefined' && typeof flightRoutesData !== 'undefined') {
+ console.log('Data found, initializing calculator...');
+ console.log('Locations:', locationsData.length);
+ console.log('Flights:', flightRoutesData.length);
+
+ try {
+ new FlightStatsCalculator(locationsData, flightRoutesData);
+ console.log('FlightStatsCalculator initialized successfully');
+ } catch (error) {
+ console.error('Error initializing FlightStatsCalculator:', error);
+ }
+ } else {
+ console.error('Required data is missing:', {
+ locationsData: typeof locationsData,
+ flightRoutesData: typeof flightRoutesData
+ });
+ }
+
+ // Check if Chart.js is loaded
+ if (typeof Chart === 'undefined') {
+ console.error('Chart.js is not loaded!');
+ } else {
+ console.log('Chart.js is loaded successfully');
+ }
+});
\ No newline at end of file
diff --git a/js/main.js b/js/main.js
new file mode 100644
index 0000000..35861cf
--- /dev/null
+++ b/js/main.js
@@ -0,0 +1,78 @@
+// Smooth scrolling for navigation links
+document.querySelectorAll('a[href^="#"]').forEach(anchor => {
+ anchor.addEventListener('click', function (e) {
+ e.preventDefault();
+ const target = document.querySelector(this.getAttribute('href'));
+ if (target) {
+ target.scrollIntoView({
+ behavior: 'smooth',
+ block: 'start'
+ });
+ }
+ });
+});
+
+// Header scroll effect
+const header = document.querySelector('.header');
+let lastScroll = 0;
+
+window.addEventListener('scroll', () => {
+ const currentScroll = window.pageYOffset;
+
+ if (currentScroll <= 0) {
+ header.classList.remove('scroll-up');
+ return;
+ }
+
+ if (currentScroll > lastScroll && !header.classList.contains('scroll-down')) {
+ // Scrolling down
+ header.classList.remove('scroll-up');
+ header.classList.add('scroll-down');
+ } else if (currentScroll < lastScroll && header.classList.contains('scroll-down')) {
+ // Scrolling up
+ header.classList.remove('scroll-down');
+ header.classList.add('scroll-up');
+ }
+
+ lastScroll = currentScroll;
+});
+
+// Intersection Observer for fade-in animations
+const observerOptions = {
+ root: null,
+ threshold: 0.1,
+ rootMargin: '0px'
+};
+
+const observer = new IntersectionObserver((entries, observer) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ entry.target.classList.add('fade-in');
+ observer.unobserve(entry.target);
+ }
+ });
+}, observerOptions);
+
+// Observe all sections
+document.querySelectorAll('section').forEach(section => {
+ section.classList.add('fade-in-section');
+ observer.observe(section);
+});
+
+// Mobile menu toggle (if needed in the future)
+function setupMobileMenu() {
+ const menuButton = document.querySelector('.mobile-menu-button');
+ const navLinks = document.querySelector('.nav-links');
+
+ if (menuButton && navLinks) {
+ menuButton.addEventListener('click', () => {
+ navLinks.classList.toggle('active');
+ menuButton.classList.toggle('active');
+ });
+ }
+}
+
+// Initialize when DOM is loaded
+document.addEventListener('DOMContentLoaded', () => {
+ setupMobileMenu();
+});
\ No newline at end of file
Stay in Touch
+{{ profile.platform }}
+@{{ profile.username }}
+{{ profile.description }}
+