diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7551134
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,31 @@
+# Qoder IDE folder
+.qoder/
+
+# Node modules (if any)
+node_modules/
+
+# OS generated files
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
+
+# Logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# IDE files
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# Temporary files
+*.tmp
+*.temp
\ No newline at end of file
diff --git a/15-Mapty/Mapty-architecture-final.png b/15-Mapty/Mapty-architecture-final.png
new file mode 100644
index 0000000..56ad00c
Binary files /dev/null and b/15-Mapty/Mapty-architecture-final.png differ
diff --git a/15-Mapty/Mapty-architecture-part-1.png b/15-Mapty/Mapty-architecture-part-1.png
new file mode 100644
index 0000000..9e2159e
Binary files /dev/null and b/15-Mapty/Mapty-architecture-part-1.png differ
diff --git a/15-Mapty/Mapty-flowchart.png b/15-Mapty/Mapty-flowchart.png
new file mode 100644
index 0000000..c0f24c0
Binary files /dev/null and b/15-Mapty/Mapty-flowchart.png differ
diff --git a/15-Mapty/icon.png b/15-Mapty/icon.png
new file mode 100644
index 0000000..d2f5031
Binary files /dev/null and b/15-Mapty/icon.png differ
diff --git a/15-Mapty/index.html b/15-Mapty/index.html
new file mode 100644
index 0000000..b6a0c4e
--- /dev/null
+++ b/15-Mapty/index.html
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mapty : Map your workouts
+
+
+
+
+
+
+
+
+
diff --git a/15-Mapty/logo.png b/15-Mapty/logo.png
new file mode 100644
index 0000000..50a9cba
Binary files /dev/null and b/15-Mapty/logo.png differ
diff --git a/15-Mapty/script.js b/15-Mapty/script.js
new file mode 100644
index 0000000..2d18a79
--- /dev/null
+++ b/15-Mapty/script.js
@@ -0,0 +1,469 @@
+'use strict';
+
+// Array of month names used for formatting dates in workout displays and calendar views
+const months = [
+ 'January',
+ 'February',
+ 'March',
+ 'April',
+ 'May',
+ 'June',
+ 'July',
+ 'August',
+ 'September',
+ 'October',
+ 'November',
+ 'December',
+];
+
+// DOM element selections for workout form inputs and containers to handle user interactions
+const form = document.querySelector('.form');
+const containerWorkouts = document.querySelector('.workouts');
+const inputType = document.querySelector('.form__input--type');
+const inputDistance = document.querySelector('.form__input--distance');
+const inputDuration = document.querySelector('.form__input--duration');
+const inputCadence = document.querySelector('.form__input--cadence');
+const inputElevation = document.querySelector('.form__input--elevation');
+
+class Workout {
+ date = new Date();
+ id = (Date.now() + '').slice(-10);
+ clicks = 0;
+ constructor(coords, distance, duration) {
+ this.coords = coords;
+ this.distance = distance;
+ this.duration = duration;
+ }
+
+ _setDescription() {
+ this.description = `${this.type[0].toUpperCase()}${this.type.slice(1)} on ${
+ months[this.date.getMonth()]
+ } ${this.date.getDate()}`;
+ }
+
+ click() {
+ this.clicks++;
+ }
+}
+
+class Running extends Workout {
+ type = 'running';
+ constructor(coords, distance, duration, cadence) {
+ super(coords, distance, duration);
+ this.cadence = cadence;
+ this.calcPace();
+ this._setDescription();
+ }
+
+ calcPace() {
+ this.pace = this.duration / this.distance;
+ return this.pace;
+ }
+}
+
+class Cycling extends Workout {
+ type = 'cycling';
+ constructor(coords, distance, duration, elevationGain) {
+ super(coords, distance, duration);
+ this.elevationGain = elevationGain;
+ this.calcSpeed();
+ this._setDescription();
+ }
+
+ calcSpeed() {
+ this.speed = this.distance / (this.duration / 60);
+ return this.speed;
+ }
+}
+
+// Main application class that handles map functionality and workout tracking
+class App {
+ #map;
+ #mapZoomLevel = 13;
+ #mapEvent;
+ #workouts = [];
+
+ constructor() {
+ this._getPosition();
+ form.addEventListener('submit', this._newWorkout.bind(this));
+ inputType.addEventListener('change', this._toggleElevationField);
+ containerWorkouts.addEventListener('click', this._moveToPopup.bind(this));
+ this._getLocalStorage();
+ }
+
+ _moveToPopup(e) {
+ const workoutEl = e.target.closest('.workout');
+ if (!workoutEl) return;
+
+ const workout = this.#workouts.find(
+ workout => workout.id === workoutEl.dataset.id,
+ );
+
+ this.#map.setView(workout.coords, this.#mapZoomLevel, {
+ animate: true,
+ pan: {
+ duration: 1,
+ },
+ });
+
+ // The click method is not part of the original project requirements for this function,
+ // but if you need it, it will now work because the workout object is a class instance.
+ // workout.click();
+ }
+
+ // Get user's current geolocation position using browser API
+ _getPosition() {
+ if (navigator.geolocation) {
+ navigator.geolocation.getCurrentPosition(
+ this._loadMap.bind(this),
+ function () {
+ // Handle geolocation errors by showing user-friendly alert
+ alert('Could not get your position β');
+ },
+ );
+ }
+ }
+
+ // Initialize and configure the map with user's current location
+ _loadMap(position) {
+ // Extract coordinates from geolocation position for map centering
+ const latitude = position.coords.latitude;
+ const longitude = position.coords.longitude;
+ const coords = [latitude, longitude];
+
+ // Initialize Leaflet map centered on user location with appropriate zoom level
+ this.#map = L.map('map').setView(coords, this.#mapZoomLevel);
+
+ // Add OpenStreetMap tile layer with attribution and max zoom configuration
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ maxZoom: 20,
+ attribution:
+ '© OpenStreetMap contributors',
+ }).addTo(this.#map);
+
+ // Set up event listener for map clicks to display workout form
+ this.#map.on('click', this._showForm.bind(this));
+
+ // Render markers for workouts loaded from localStorage
+ this.#workouts.forEach(work => {
+ this._renderWorkoutMarker(work);
+ });
+ }
+
+ // Display the workout form when user clicks on map
+ _showForm(mapE) {
+ this.#mapEvent = mapE;
+ form.classList.remove('hidden');
+ inputDistance.focus();
+ }
+
+ // Toggle visibility of elevation/cadence input fields based on workout type
+ _toggleElevationField() {
+ inputCadence.closest('.form__row').classList.toggle('form__row--hidden');
+ inputElevation.closest('.form__row').classList.toggle('form__row--hidden');
+ }
+
+ // Handle new workout submission and marker creation
+ _newWorkout(e) {
+ e.preventDefault();
+
+ // Get data from the form
+ const type = inputType.value;
+ const distance = +inputDistance.value;
+ const duration = +inputDuration.value;
+ let workout;
+
+ // If workout is running, create running object
+ // Helper function to validate numeric inputs
+ const validateInputs = (...inputs) => {
+ // Check if all inputs are valid positive numbers
+ if (!inputs.every(input => Number.isFinite(input) && input > 0)) {
+ return { isValid: false, message: 'Inputs must be positive numbers!' };
+ }
+ return { isValid: true, message: '' };
+ };
+
+ // If workout is running, create cycling object
+
+ if (type === 'running') {
+ const cadence = +inputCadence.value;
+
+ const validation = validateInputs(distance, duration, cadence);
+ if (!validation.isValid) {
+ return alert(validation.message);
+ }
+ workout = new Running(
+ [this.#mapEvent.latlng.lat, this.#mapEvent.latlng.lng],
+ distance,
+ duration,
+ cadence,
+ );
+ }
+
+ // If workout is cycling, create cycling object
+ if (type === 'cycling') {
+ const elevation = +inputElevation.value;
+
+ const validation = validateInputs(distance, duration, elevation);
+ if (!validation.isValid) {
+ return alert(validation.message);
+ }
+ workout = new Cycling(
+ [this.#mapEvent.latlng.lat, this.#mapEvent.latlng.lng],
+ distance,
+ duration,
+ elevation,
+ );
+ }
+
+ // Add new workout object to workout array
+ this.#workouts.push(workout);
+
+ // Display workout on map as a marker
+ this._renderWorkoutMarker(workout);
+ //Render workout on list
+ this._renderWorkout(workout);
+
+ // Hide form and clear input fields
+ this._hideForm();
+
+ // Set local storage to all workouts
+ this._setLocalStorage();
+ }
+
+ _renderWorkoutMarker(workout) {
+ L.marker(workout.coords)
+ .addTo(this.#map)
+ .bindPopup(
+ L.popup({
+ autoClose: false,
+ maxWidth: '300',
+ minWidth: '200',
+ closeOnClick: false,
+ className: `${workout.type}-popup`,
+ closeButton: true,
+ autoPan: true,
+ offset: [0, -30],
+ keepInView: true,
+ animation: true,
+ interactive: true,
+ }).setContent(
+ `${workout.type === 'running' ? 'πββοΈ' : 'π΄ββοΈ'} ${workout.type} on ${
+ months[workout.date.getMonth()]
+ } ${workout.date.getDate()}`,
+ ),
+ )
+ .openPopup();
+ }
+
+ _renderWorkout(workout) {
+ let html = `
+ ${workout.description}
+
+ ${
+ workout.type === 'running' ? 'πββοΈ' : 'π΄ββοΈ'
+ }
+ ${workout.distance}
+ km
+
+
+ β±
+ ${workout.duration}
+ min
+
`;
+
+ if (workout.type === 'running') {
+ html += `
+ β‘οΈ
+ ${workout.pace.toFixed(1)}
+ min/km
+
+
+ π¦ΆπΌ
+ ${workout.cadence}
+ spm
+
+ `;
+ }
+ if (workout.type === 'cycling') {
+ html += `
+ β‘οΈ
+ ${workout.speed.toFixed(1)}
+ km/h
+
+
+ β°
+ ${workout.elevationGain}
+ m
+
+ `;
+ }
+ form.insertAdjacentHTML('afterend', html);
+ }
+
+ _hideForm() {
+ inputDistance.value =
+ inputDuration.value =
+ inputCadence.value =
+ inputElevation.value =
+ '';
+ form.style.display = 'none';
+ form.classList.add('hidden');
+ setTimeout(() => (form.style.display = 'grid'), 1000);
+ }
+ _setLocalStorage() {
+ localStorage.setItem('workouts', JSON.stringify(this.#workouts));
+ }
+ _getLocalStorage() {
+ const data = JSON.parse(localStorage.getItem('workouts'));
+ if (!data) return;
+
+ // Re-instantiate objects to restore prototype chain
+ this.#workouts = data.map(work => {
+ let workout;
+ if (work.type === 'running') {
+ workout = new Running(
+ work.coords,
+ work.distance,
+ work.duration,
+ work.cadence,
+ );
+ }
+ if (work.type === 'cycling') {
+ workout = new Cycling(
+ work.coords,
+ work.distance,
+ work.duration,
+ work.elevationGain,
+ );
+ }
+ // Restore original id and date
+ workout.id = work.id;
+ workout.date = new Date(work.date);
+ // Fix description to use the correct stored date
+ workout._setDescription();
+ return workout;
+ });
+
+ this.#workouts.forEach(work => {
+ this._renderWorkout(work);
+ });
+ }
+
+ reset() {
+ localStorage.removeItem('workouts');
+ location.reload();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////
+// THEME MANAGER: Handles dark/light theme switching and persistence across sessions
+////////////////////////////////////////////////////////////////////////////
+
+// Theme manager class responsible for handling theme preferences and UI updates
+class ThemeManager {
+ constructor() {
+ // Initialize theme toggle UI elements and set up references
+ this.themeToggle = document.getElementById('themeToggle');
+ this.themeIcon = document.getElementById('themeIcon');
+ this.themeText = document.getElementById('themeText');
+
+ // Get stored theme preference or fall back to system preference
+ this.currentTheme = this.getStoredTheme() || this.getSystemTheme();
+
+ this.init(); // Initialize theme manager and set up event listeners
+ }
+
+ // Initialize theme manager with saved preferences and event listeners
+ init() {
+ // Apply the initial theme and update UI elements
+ this.applyTheme(this.currentTheme);
+ this.updateToggleUI();
+
+ // Set up click handler for theme toggle button
+ this.themeToggle.addEventListener('click', () => this.toggleTheme());
+
+ // Monitor system theme preference changes for automatic updates
+ window
+ .matchMedia('(prefers-color-scheme: dark)')
+ .addEventListener('change', e => {
+ if (!this.getStoredTheme()) {
+ // Update theme only if user hasn't set explicit preference
+ this.currentTheme = e.matches ? 'dark' : 'light';
+ this.applyTheme(this.currentTheme);
+ this.updateToggleUI();
+ }
+ });
+
+ // Add keyboard support for accessibility
+ this.themeToggle.addEventListener('keydown', e => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ this.toggleTheme();
+ }
+ });
+ }
+
+ // Get user's theme preference from local storage
+ getStoredTheme() {
+ return localStorage.getItem('theme');
+ }
+
+ // Save user's theme preference to local storage for persistence
+ setStoredTheme(theme) {
+ localStorage.setItem('theme', theme);
+ }
+
+ // Detect system-level theme preference for initial setup
+ getSystemTheme() {
+ return window.matchMedia('(prefers-color-scheme: dark)').matches
+ ? 'dark'
+ : 'light';
+ }
+
+ // Apply the selected theme by updating document attributes
+ applyTheme(theme) {
+ if (theme === 'dark') {
+ document.documentElement.setAttribute('data-theme', 'dark');
+ } else {
+ document.documentElement.removeAttribute('data-theme');
+ }
+ }
+
+ // Update theme toggle button appearance and accessibility attributes
+ updateToggleUI() {
+ const isDark = this.currentTheme === 'dark';
+ this.themeIcon.textContent = isDark ? 'βοΈ' : 'π';
+ this.themeText.textContent = isDark ? 'Light Mode' : 'Dark Mode';
+
+ // Update accessibility attributes for screen readers
+ this.themeToggle.setAttribute(
+ 'aria-label',
+ isDark ? 'Switch to light mode' : 'Switch to dark mode',
+ );
+ this.themeToggle.setAttribute('aria-pressed', isDark.toString());
+ }
+
+ // Toggle between light and dark themes with smooth animation
+ toggleTheme() {
+ this.currentTheme = this.currentTheme === 'light' ? 'dark' : 'light';
+ this.applyTheme(this.currentTheme);
+ this.setStoredTheme(this.currentTheme);
+ this.updateToggleUI();
+
+ // Add subtle scale animation for visual feedback
+ this.themeToggle.style.transform = 'scale(0.95)';
+ setTimeout(() => {
+ this.themeToggle.style.transform = '';
+ }, 150);
+ }
+}
+
+// Initialize application components when DOM is fully loaded
+document.addEventListener('DOMContentLoaded', () => {
+ // Create instances of theme manager and main app
+ new ThemeManager();
+ new App();
+});
diff --git a/15-Mapty/style.css b/15-Mapty/style.css
new file mode 100644
index 0000000..6eff0e7
--- /dev/null
+++ b/15-Mapty/style.css
@@ -0,0 +1,502 @@
+/* Root variables for theming */
+:root {
+ /* Light mode colors */
+ --color-primary: #6366f1; /* Main brand color */
+ --color-secondary: #10b981; /* Secondary brand color */
+ --color-accent: #f59e0b; /* Accent color for highlights */
+
+ /* Background colors */
+ --color-bg-primary: #ffffff; /* Main background */
+ --color-bg-secondary: #f8fafc; /* Secondary background */
+ --color-bg-tertiary: #f1f5f9; /* Tertiary background */
+
+ /* Text colors */
+ --color-text-primary: #0f172a; /* Main text color */
+ --color-text-secondary: #475569; /* Secondary text color */
+ --color-text-muted: #94a3b8; /* Muted/subtle text */
+
+ /* UI element colors */
+ --color-border: #e2e8f0; /* Border color */
+ --color-shadow: rgba(0, 0, 0, 0.1); /* Shadow color */
+
+ /* Status colors */
+ --color-success: #10b981; /* Success state */
+ --color-warning: #f59e0b; /* Warning state */
+ --color-error: #ef4444; /* Error state */
+
+ /* Layout variables */
+ --sidebar-width: 42rem;
+ --border-radius: 0.75rem;
+ --border-radius-sm: 0.5rem;
+ --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); /* Smooth transition effect */
+}
+
+/* Dark theme overrides */
+[data-theme='dark'] {
+ /* Dark mode colors */
+ --color-bg-primary: #0f172a; /* Dark background */
+ --color-bg-secondary: #1e293b; /* Dark secondary background */
+ --color-bg-tertiary: #334155; /* Dark tertiary background */
+
+ /* Dark mode text colors */
+ --color-text-primary: #f8fafc; /* Light text for dark mode */
+ --color-text-secondary: #cbd5e1; /* Secondary text for dark mode */
+ --color-text-muted: #64748b; /* Muted text for dark mode */
+
+ /* Dark mode UI elements */
+ --color-border: #334155; /* Dark mode borders */
+ --color-shadow: rgba(0, 0, 0, 0.3); /* Darker shadows */
+}
+
+/* Reset default styles */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+/* Base font size (10px) for easier rem calculations */
+html {
+ font-size: 62.5%;
+}
+
+/* Base body styles */
+body {
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
+ color: var(--color-text-primary);
+ font-weight: 400;
+ line-height: 1.6;
+ height: 100vh;
+ overflow: hidden;
+ background-color: var(--color-bg-primary);
+ display: flex;
+ transition: var(--transition);
+}
+
+/* Theme toggle button styling */
+.theme-toggle {
+ position: fixed;
+ top: 2rem;
+ right: 2rem;
+ z-index: 1000;
+ background: var(--color-bg-secondary);
+ border: 2px solid var(--color-border);
+ border-radius: var(--border-radius);
+ padding: 1rem;
+ cursor: pointer;
+ transition: var(--transition);
+ box-shadow: 0 4px 6px -1px var(--color-shadow);
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+/* Hover effect for theme toggle */
+.theme-toggle:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 15px -3px var(--color-shadow);
+}
+
+/* Focus state for accessibility */
+.theme-toggle:focus {
+ outline: 2px solid var(--color-primary);
+ outline-offset: 2px;
+}
+
+/* Theme toggle icon */
+.theme-icon {
+ font-size: 2rem;
+ transition: var(--transition);
+}
+
+/* Theme toggle text */
+.theme-text {
+ font-size: 1.4rem;
+ font-weight: 500;
+ color: var(--color-text-secondary);
+}
+
+/* Link styles */
+a:link,
+a:visited {
+ color: var(--color-primary);
+ text-decoration: none;
+ transition: var(--transition);
+}
+
+a:hover,
+a:focus {
+ color: var(--color-secondary);
+ outline: none;
+}
+
+/* Sidebar layout */
+.sidebar {
+ flex-basis: var(--sidebar-width);
+ background-color: var(--color-bg-secondary);
+ border-right: 1px solid var(--color-border);
+ padding: 3rem 3rem 2rem 3rem;
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto; /* allow vertical scrolling */
+ overflow-x: hidden; /* prevent horizontal cut-off */
+ box-shadow: 4px 0 6px -1px var(--color-shadow);
+ transition: var(--transition);
+}
+
+/* Logo styling */
+.logo {
+ height: 5rem;
+ align-self: center;
+ margin-bottom: 3rem;
+ filter: drop-shadow(0 2px 4px var(--color-shadow));
+ transition: var(--transition);
+}
+
+/* Logo hover effect */
+.logo:hover {
+ transform: scale(1.05);
+}
+
+/* Workouts list container */
+.workouts {
+ list-style: none;
+ flex-grow: 1;
+ overflow-y: auto; /* vertical scrolling */
+ overflow-x: hidden; /* prevent side clipping */
+ padding-right: 1rem;
+ margin-right: 0; /* removed negative margin */
+ max-width: 100%;
+ word-wrap: break-word;
+ white-space: normal;
+}
+
+/* Custom scrollbar styling */
+.workouts::-webkit-scrollbar {
+ width: 6px;
+}
+
+.workouts::-webkit-scrollbar-track {
+ background: transparent;
+ border-radius: 3px;
+}
+
+.workouts::-webkit-scrollbar-thumb {
+ background: var(--color-border);
+ border-radius: 3px;
+ transition: var(--transition);
+}
+
+.workouts::-webkit-scrollbar-thumb:hover {
+ background: var(--color-text-muted);
+}
+
+/* Individual workout card styling */
+.workout {
+ background-color: var(--color-bg-primary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius);
+ padding: 2rem 2.5rem;
+ margin-bottom: 1.5rem;
+ cursor: pointer;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
+ gap: 1rem 1.5rem;
+ transition: var(--transition);
+ box-shadow: 0 1px 3px 0 var(--color-shadow);
+ position: relative;
+ overflow: visible; /* make sure text/icons arenβt clipped */
+ word-break: break-word; /* break long words nicely */
+ white-space: normal; /* allow wrapping */
+}
+
+/* Workout type indicator bar */
+.workout::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 4px;
+ transition: var(--transition);
+ background-color: var(--color-accent, #00ff99); /* fallback */
+}
+
+/* Running workout indicator */
+.workout--running::before {
+ background: linear-gradient(135deg, var(--color-secondary), #059669);
+}
+
+/* Cycling workout indicator */
+.workout--cycling::before {
+ background: linear-gradient(135deg, var(--color-accent), #d97706);
+}
+
+/* Workout card hover effects */
+.workout:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px -5px var(--color-shadow);
+ border-color: var(--color-primary);
+}
+
+/* Workout card focus state */
+.workout:focus {
+ outline: 2px solid var(--color-primary);
+ outline-offset: 2px;
+}
+
+/* Workout title styling */
+.workout__title {
+ font-size: 1.8rem;
+ font-weight: 600;
+ grid-column: 1 / -1;
+ margin-bottom: 0.5rem;
+ color: var(--color-text-primary);
+}
+
+/* Workout details container */
+.workout__details {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+/* Workout icon styling */
+.workout__icon {
+ font-size: 2rem;
+ height: 2rem;
+ width: 2rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* Workout value styling */
+.workout__value {
+ font-size: 1.6rem;
+ font-weight: 600;
+ color: var(--color-text-primary);
+}
+
+/* Workout unit styling */
+.workout__unit {
+ font-size: 1.2rem;
+ color: var(--color-text-muted);
+ text-transform: uppercase;
+ font-weight: 500;
+ letter-spacing: 0.5px;
+}
+
+/* Form styling */
+.form {
+ background-color: var(--color-bg-primary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius);
+ padding: 2rem 2.5rem;
+ margin-bottom: 1.5rem;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1.5rem 2rem;
+ height: auto;
+ min-height: 12rem;
+ transition: var(--transition);
+ box-shadow: 0 4px 6px -1px var(--color-shadow);
+}
+
+/* Hidden form state */
+.form.hidden {
+ transform: translateY(-100%);
+ opacity: 0;
+ height: 0;
+ min-height: 0;
+ padding: 0 2.5rem;
+ margin-bottom: 0;
+ pointer-events: none;
+}
+
+/* Form row layout */
+.form__row {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+/* Hidden form row */
+.form__row--hidden {
+ display: none;
+}
+
+/* Form label styling */
+.form__label {
+ font-size: 1.4rem;
+ font-weight: 600;
+ color: var(--color-text-secondary);
+ margin-bottom: 0.5rem;
+}
+
+/* Form input styling */
+.form__input,
+.form__input--type {
+ width: 100%;
+ padding: 1rem 1.2rem;
+ font-family: inherit;
+ font-size: 1.4rem;
+ border: 2px solid var(--color-border);
+ border-radius: var(--border-radius-sm);
+ background-color: var(--color-bg-tertiary);
+ color: var(--color-text-primary);
+ transition: var(--transition);
+}
+
+/* Form input focus state */
+.form__input:focus,
+.form__input--type:focus {
+ outline: none;
+ border-color: var(--color-primary);
+ background-color: var(--color-bg-primary);
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
+}
+
+/* Form input placeholder */
+.form__input::placeholder {
+ color: var(--color-text-muted);
+}
+
+/* Hidden form button */
+.form__btn {
+ display: none;
+}
+
+/* Copyright section */
+.copyright {
+ margin-top: 2rem;
+ padding-top: 2rem;
+ border-top: 1px solid var(--color-border);
+ font-size: 1.2rem;
+ text-align: center;
+ color: var(--color-text-muted);
+ line-height: 1.5;
+}
+
+/* Twitter link styling */
+.twitter-link:link,
+.twitter-link:visited {
+ color: var(--color-primary);
+ font-weight: 500;
+ transition: var(--transition);
+}
+
+.twitter-link:hover,
+.twitter-link:active {
+ color: var(--color-secondary);
+}
+
+/* Map container */
+#map {
+ flex: 1;
+ height: 100vh;
+ background: linear-gradient(
+ 135deg,
+ var(--color-bg-tertiary),
+ var(--color-bg-secondary)
+ );
+ transition: var(--transition);
+}
+
+/* Leaflet popup styling */
+.leaflet-popup .leaflet-popup-content-wrapper {
+ background-color: var(--color-bg-primary);
+ color: var(--color-text-primary);
+ border-radius: var(--border-radius);
+ padding: 1rem;
+ border: 1px solid var(--color-border);
+ box-shadow: 0 10px 25px -5px var(--color-shadow);
+}
+
+.leaflet-popup .leaflet-popup-content {
+ font-size: 1.4rem;
+ margin: 0;
+}
+
+.leaflet-popup .leaflet-popup-tip {
+ background-color: var(--color-bg-primary);
+ border: 1px solid var(--color-border);
+}
+
+/* Running popup specific styling */
+.running-popup .leaflet-popup-content-wrapper {
+ border-left: 4px solid var(--color-secondary);
+}
+
+/* Cycling popup specific styling */
+.cycling-popup .leaflet-popup-content-wrapper {
+ border-left: 4px solid var(--color-accent);
+}
+
+/* Responsive design for mobile devices */
+@media (max-width: 768px) {
+ body {
+ flex-direction: column;
+ }
+
+ .sidebar {
+ flex-basis: auto;
+ height: 40vh;
+ border-right: none;
+ border-bottom: 1px solid var(--color-border);
+ }
+
+ #map {
+ height: 60vh;
+ }
+
+ .theme-toggle {
+ top: 1rem;
+ right: 1rem;
+ padding: 0.75rem;
+ }
+
+ .theme-text {
+ display: none;
+ }
+}
+
+/* Focus and accessibility improvements */
+*:focus {
+ outline: 2px solid var(--color-primary);
+ outline-offset: 2px;
+}
+
+/* Transition effects for interactive elements */
+button,
+select,
+input,
+a {
+ transition: var(--transition);
+}
+
+/* Smooth scrolling behavior */
+html {
+ scroll-behavior: smooth;
+}
+
+/* Reduced motion preferences */
+@media (prefers-reduced-motion: reduce) {
+ * {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+}
+
+/* High contrast mode support */
+@media (prefers-contrast: high) {
+ :root {
+ --color-border: #000000;
+ --color-shadow: rgba(0, 0, 0, 0.5);
+ }
+
+ [data-theme='dark'] {
+ --color-border: #ffffff;
+ }
+}