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; + } +}