diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..5c97876
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,5 @@
+Dockerfile
+.dockerignore
+.git
+.gitignore
+README.md
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..73b52cc
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,18 @@
+# Use an official Nginx image as the base image
+FROM nginx:alpine
+
+# Set the working directory in the container
+WORKDIR /usr/share/nginx/html
+
+# Remove default Nginx welcome page
+RUN rm -rf ./*
+
+# Copy all the static assets from your project to the Nginx public directory
+# This includes index.html, app.js, style.css, and any images or data files
+COPY . .
+
+# Expose port 80 (the default Nginx port)
+EXPOSE 80
+
+# Command to run Nginx when the container starts
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/app.js b/app.js
new file mode 100644
index 0000000..ca7d019
--- /dev/null
+++ b/app.js
@@ -0,0 +1,543 @@
+// Hotel data
+const hotelData = [
+ {
+ "name": "Hotel Britannique",
+ "address": "20 Av. Victoria, 75001 Paris",
+ "rating": 4.6,
+ "price_range": "€200-320",
+ "arrondissement": "1st",
+ "coordinates": [48.8567, 2.3471],
+ "description": "Classic 19th-century hotel near Châtelet with theatrical red decor and excellent Metro access",
+ "website": "https://hotel-britannique.fr",
+ "amenities": ["Free WiFi", "Minibar", "Air Conditioning", "Elevator"]
+ },
+ {
+ "name": "Hôtel Signature Saint Germain des Prés",
+ "address": "5 Rue Chomel, 75007 Paris",
+ "rating": 4.8,
+ "price_range": "€220-290",
+ "arrondissement": "7th",
+ "coordinates": [48.8485, 2.3244],
+ "description": "Chic boutique property in Saint-Germain with elegant, colorful rooms and vintage-chic lobby",
+ "website": "http://www.signature-saintgermain.com",
+ "amenities": ["Free WiFi", "Air Conditioning", "Buffet Breakfast", "24-hour Reception"]
+ },
+ {
+ "name": "Paris France Hotel",
+ "address": "72 R. de Turbigo, 75003 Paris",
+ "rating": 4.7,
+ "price_range": "€180-280",
+ "arrondissement": "3rd",
+ "coordinates": [48.8644, 2.3614],
+ "description": "Traditional hotel with a cafe/bar and lounge in the heart of the Marais district",
+ "website": "https://www.paris-france-hotel.com",
+ "amenities": ["Free WiFi", "Café/Bar", "Lounge", "Optional Breakfast"]
+ },
+ {
+ "name": "Hôtel Lenox Montparnasse",
+ "address": "15 Rue Delambre, 75014 Paris",
+ "rating": 4.5,
+ "price_range": "€170-260",
+ "arrondissement": "14th",
+ "coordinates": [48.8427, 2.3269],
+ "description": "Classic rooms & suites with warmly decorated honesty bar near Montparnasse",
+ "website": "http://www.paris-hotel-lenox.com",
+ "amenities": ["Free WiFi", "Honesty Bar", "Classic Decor", "Cozy Atmosphere"]
+ },
+ {
+ "name": "Hôtel Caron de Beaumarchais",
+ "address": "12 Rue Vieille du Temple, 75004 Paris",
+ "rating": 4.6,
+ "price_range": "€200-280",
+ "arrondissement": "4th",
+ "coordinates": [48.8572, 2.3598],
+ "description": "18th-century lodging with antiques-filled rooms and private balconies in Le Marais",
+ "website": "http://www.carondebeaumarchais.com",
+ "amenities": ["Free WiFi", "Private Balconies", "Antique Furnishings", "18th Century Charm"]
+ },
+ {
+ "name": "Hôtel Bourg Tibourg",
+ "address": "19 Rue du Bourg Tibourg, 75004 Paris",
+ "rating": 4.7,
+ "price_range": "€250-300",
+ "arrondissement": "4th",
+ "coordinates": [48.8547, 2.3581],
+ "description": "Lavish boutique hotel with opulent rooms and sophisticated dramatic atmosphere",
+ "website": "https://www.bourgtibourg.com",
+ "amenities": ["Free WiFi", "Opulent Decor", "Sophisticated Atmosphere", "Dramatic Design"]
+ },
+ {
+ "name": "Hôtel de l'Abbaye Saint-Germain",
+ "address": "10 Rue Cassette, 75006 Paris",
+ "rating": 4.6,
+ "price_range": "€280-300",
+ "arrondissement": "6th",
+ "coordinates": [48.8495, 2.3309],
+ "description": "Refined hotel with plush lounges and garden, plus free WiFi & breakfast",
+ "website": "https://www.hotelabbayeparis.com",
+ "amenities": ["Free WiFi", "Garden", "Plush Lounges", "Breakfast Included"]
+ },
+ {
+ "name": "Hotel Quartier Latin",
+ "address": "9 Rue des Écoles, 75005 Paris",
+ "rating": 4.3,
+ "price_range": "€160-250",
+ "arrondissement": "5th",
+ "coordinates": [48.8490, 2.3468],
+ "description": "Traditional hotel with simple rooms, minibars and breakfast buffet option",
+ "website": "https://hotelquartierlatin.com",
+ "amenities": ["Free WiFi", "Minibar", "Breakfast Buffet", "Traditional Decor"]
+ },
+ {
+ "name": "CitizenM Paris Gare de Lyon",
+ "address": "8 Rue Van Gogh, 75012 Paris",
+ "rating": 4.4,
+ "price_range": "€150-220",
+ "arrondissement": "12th",
+ "coordinates": [48.8448, 2.3732],
+ "description": "Trendy budget hotel with compact rooms, ambient lighting and 24-hour cafe/bar",
+ "website": "https://www.citizenm.com/hotels/europe/paris/gare-de-lyon-hotel",
+ "amenities": ["24-hour Café/Bar", "Ambient Lighting", "Compact Design", "Modern Amenities"]
+ },
+ {
+ "name": "Hôtel Migny Opéra-Montmartre",
+ "address": "13 Rue Victor Massé, 75009 Paris",
+ "rating": 4.1,
+ "price_range": "€140-200",
+ "arrondissement": "9th",
+ "coordinates": [48.8810, 2.3375],
+ "description": "Colorful rooms with satellite TV, casual breakfast area and lounge near Opéra",
+ "website": "https://www.hotelmigny.com",
+ "amenities": ["Free WiFi", "Satellite TV", "Breakfast Area", "LGBTQ+ Friendly"]
+ },
+ {
+ "name": "Hôtel Saint-André des Arts",
+ "address": "66 Rue Saint-André des Arts, 75006 Paris",
+ "rating": 4.8,
+ "price_range": "€180-280",
+ "arrondissement": "6th",
+ "coordinates": [48.8540, 2.3401],
+ "description": "Boutique hotel in Saint-Germain with unique rooms and bar",
+ "website": "https://www.saintandredesarts.com",
+ "amenities": ["Bar", "Unique Rooms", "Saint-Germain Location", "Boutique Style"]
+ },
+ {
+ "name": "Grand Hôtel des Gobelins",
+ "address": "57 Bd Saint-Marcel, 75013 Paris",
+ "rating": 4.1,
+ "price_range": "€130-180",
+ "arrondissement": "13th",
+ "coordinates": [48.8375, 2.3556],
+ "description": "Casual hotel with warmly furnished rooms and lobby bar",
+ "website": "http://www.grandhotelgobelins.com",
+ "amenities": ["Free WiFi", "Lobby Bar", "Warm Furnishings", "Relaxed Atmosphere"]
+ },
+ {
+ "name": "Hotel Marignan",
+ "address": "13 Rue du Sommerard, 75005 Paris",
+ "rating": 4.1,
+ "price_range": "€120-180",
+ "arrondissement": "5th",
+ "coordinates": [48.8504, 2.3445],
+ "description": "Laid-back hotel with simple rooms, guest kitchen and library, plus free breakfast",
+ "website": "http://www.hotel-marignan.com",
+ "amenities": ["Free Breakfast", "Free WiFi", "Guest Kitchen", "Library"]
+ },
+ {
+ "name": "Hôtel La Comtesse Tour Eiffel",
+ "address": "29 Av. de Tourville, 75007 Paris",
+ "rating": 4.6,
+ "price_range": "€250-300",
+ "arrondissement": "7th",
+ "coordinates": [48.8548, 2.3078],
+ "description": "Traditional hotel with classic rooms and Eiffel Tower views",
+ "website": "http://www.comtesse-hotel.com",
+ "amenities": ["Free WiFi", "Eiffel Tower Views", "Bar", "Traditional Decor"]
+ },
+ {
+ "name": "Boutique Hôtel Mareuil Paris",
+ "address": "51 Rue de Malte, 75011 Paris",
+ "rating": 4.7,
+ "price_range": "€200-280",
+ "arrondissement": "11th",
+ "coordinates": [48.8640, 2.3677],
+ "description": "Hip property with modern rooms & suites, plus bar, hammam & exercise room",
+ "website": "https://www.hotelmareuil.com/fr/",
+ "amenities": ["Bar", "Hammam", "Exercise Room", "Modern Design"]
+ },
+ {
+ "name": "Generator Paris",
+ "address": "9-11 Pl. du Colonel Fabien, 75010 Paris",
+ "rating": 4.1,
+ "price_range": "€90-150",
+ "arrondissement": "10th",
+ "coordinates": [48.8772, 2.3711],
+ "description": "Trendy hostel with stylish rooms & streamlined dorms, rooftop terrace & vibrant eatery",
+ "website": "https://staygenerator.com/hostels/paris?lang=fr-FR",
+ "amenities": ["Rooftop Terrace", "Restaurant", "Bar", "Budget-Friendly"]
+ },
+ {
+ "name": "Hôtel Henriette",
+ "address": "9 rue des Gobelins, 75013 Paris",
+ "rating": 4.5,
+ "price_range": "€130-180",
+ "arrondissement": "13th",
+ "coordinates": [48.8363, 2.3518],
+ "description": "Stylish boutique hotel with subtly decorated rooms and courtyard breakfast area",
+ "website": "https://www.hotelhenriette.com",
+ "amenities": ["Free WiFi", "Courtyard", "Breakfast", "Design Interiors"]
+ },
+ {
+ "name": "Hotel Paradis",
+ "address": "41 Rue Des Petities Ecuries, 75010 Paris",
+ "rating": 4.3,
+ "price_range": "€120-180",
+ "arrondissement": "10th",
+ "coordinates": [48.8718, 2.3502],
+ "description": "Tastefully decorated rooms in a top-notch location near trendy hotspots",
+ "website": "https://hotelparadisparis.com",
+ "amenities": ["Free WiFi", "Designer Decor", "Trendy Area", "Modern Amenities"]
+ },
+ {
+ "name": "COQ Hotel Paris",
+ "address": "15 Rue Edouard Manet, 75013 Paris",
+ "rating": 4.4,
+ "price_range": "€140-190",
+ "arrondissement": "13th",
+ "coordinates": [48.8261, 2.3542],
+ "description": "Stylish design hotel with cozy bar, extensive breakfast and warm atmosphere",
+ "website": "https://coq-hotel-paris.com",
+ "amenities": ["Bar", "Breakfast", "Design Interiors", "Cozy Atmosphere"]
+ },
+ {
+ "name": "Hotel Diana",
+ "address": "73 Rue Saint-Jacques, 75005 Paris",
+ "rating": 4.2,
+ "price_range": "€150-200",
+ "arrondissement": "5th",
+ "coordinates": [48.8485, 2.3456],
+ "description": "Simple but comfortable 2-star hotel with bright rooms and modern bathrooms",
+ "website": "http://www.hoteldiana.fr",
+ "amenities": ["Free WiFi", "Modern Bathrooms", "Central Location", "Budget-Friendly"]
+ }
+];
+
+// Global variables
+let map;
+let markers = [];
+let filteredHotels = hotelData;
+let selectedHotel = null;
+
+// Initialize the application
+document.addEventListener('DOMContentLoaded', function() {
+ initializeApp();
+});
+
+function initializeApp() {
+ initializeMap();
+ populateArrondissementFilter();
+ bindEventListeners();
+ applyFilters();
+ updateHotelCount();
+}
+
+// Initialize Leaflet map
+function initializeMap() {
+ // Initialize map centered on Paris
+ map = L.map('map').setView([48.8566, 2.3522], 12);
+
+ // Add OpenStreetMap tiles
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ attribution: '© OpenStreetMap contributors',
+ maxZoom: 18
+ }).addTo(map);
+
+ // Add hotels to map
+ addHotelsToMap(hotelData);
+}
+
+// Add hotel markers to map
+function addHotelsToMap(hotels) {
+ // Clear existing markers
+ markers.forEach(marker => map.removeLayer(marker));
+ markers = [];
+
+ hotels.forEach(hotel => {
+ const marker = L.marker([hotel.coordinates[0], hotel.coordinates[1]], {
+ icon: L.divIcon({
+ className: 'custom-marker',
+ html: `
`,
+ iconSize: [24, 24],
+ iconAnchor: [12, 12]
+ })
+ });
+
+ // Create popup content
+ const popupContent = `
+
+ `;
+
+ marker.bindPopup(popupContent);
+ marker.addTo(map);
+ markers.push(marker);
+
+ // Store hotel data with marker for easy access
+ marker.hotelData = hotel;
+ });
+}
+
+// Populate arrondissement dropdown
+function populateArrondissementFilter() {
+ const select = document.getElementById('arrondissement');
+ const arrondissements = [...new Set(hotelData.map(hotel => hotel.arrondissement))].sort();
+
+ arrondissements.forEach(arr => {
+ const option = document.createElement('option');
+ option.value = arr;
+ option.textContent = `${arr} Arrondissement`;
+ select.appendChild(option);
+ });
+}
+
+// Bind event listeners
+function bindEventListeners() {
+ // View toggle buttons
+ document.getElementById('map-view-btn').addEventListener('click', () => switchView('map'));
+ document.getElementById('list-view-btn').addEventListener('click', () => switchView('list'));
+
+ // Search input
+ document.getElementById('search').addEventListener('input', applyFilters);
+
+ // Price filter buttons
+ document.querySelectorAll('.price-filter').forEach(button => {
+ button.addEventListener('click', function() {
+ document.querySelectorAll('.price-filter').forEach(btn => btn.classList.remove('active'));
+ this.classList.add('active');
+ applyFilters();
+ });
+ });
+
+ // Arrondissement filter
+ document.getElementById('arrondissement').addEventListener('change', applyFilters);
+
+ // Rating slider
+ const ratingSlider = document.getElementById('rating');
+ const ratingValue = document.getElementById('rating-value');
+ ratingSlider.addEventListener('input', function() {
+ ratingValue.textContent = this.value;
+ applyFilters();
+ });
+
+ // Reset filters button
+ document.getElementById('reset-filters').addEventListener('click', resetFilters);
+}
+
+// Switch between map and list view
+function switchView(view) {
+ const mapViewBtn = document.getElementById('map-view-btn');
+ const listViewBtn = document.getElementById('list-view-btn');
+ const mapView = document.getElementById('map-view');
+ const listView = document.getElementById('list-view');
+
+ if (view === 'map') {
+ mapViewBtn.classList.add('active');
+ mapViewBtn.classList.remove('btn--secondary');
+ mapViewBtn.classList.add('btn--primary');
+ listViewBtn.classList.remove('active');
+ listViewBtn.classList.remove('btn--primary');
+ listViewBtn.classList.add('btn--secondary');
+ mapView.classList.add('active');
+ listView.classList.remove('active');
+
+ // Refresh map after view switch
+ setTimeout(() => map.invalidateSize(), 100);
+ } else {
+ listViewBtn.classList.add('active');
+ listViewBtn.classList.remove('btn--secondary');
+ listViewBtn.classList.add('btn--primary');
+ mapViewBtn.classList.remove('active');
+ mapViewBtn.classList.remove('btn--primary');
+ mapViewBtn.classList.add('btn--secondary');
+ listView.classList.add('active');
+ mapView.classList.remove('active');
+
+ renderHotelList();
+ }
+}
+
+// Apply all filters
+function applyFilters() {
+ const searchTerm = document.getElementById('search').value.toLowerCase();
+ const selectedArrondissement = document.getElementById('arrondissement').value;
+ const minRating = parseFloat(document.getElementById('rating').value);
+ const activePriceFilter = document.querySelector('.price-filter.active');
+ const minPrice = parseInt(activePriceFilter.dataset.min);
+ const maxPrice = parseInt(activePriceFilter.dataset.max);
+
+ filteredHotels = hotelData.filter(hotel => {
+ // Search filter
+ if (searchTerm && !hotel.name.toLowerCase().includes(searchTerm)) {
+ return false;
+ }
+
+ // Arrondissement filter
+ if (selectedArrondissement !== 'all' && hotel.arrondissement !== selectedArrondissement) {
+ return false;
+ }
+
+ // Rating filter
+ if (hotel.rating < minRating) {
+ return false;
+ }
+
+ // Price filter
+ const hotelMinPrice = parseInt(hotel.price_range.match(/€(\d+)/)[1]);
+ const hotelMaxPrice = parseInt(hotel.price_range.match(/€\d+-(\d+)/)[1]);
+
+ // Check if hotel price range overlaps with selected price range
+ if (hotelMaxPrice < minPrice || hotelMinPrice > maxPrice) {
+ return false;
+ }
+
+ return true;
+ });
+
+ // Update map markers
+ addHotelsToMap(filteredHotels);
+
+ // Update list view if active
+ if (document.getElementById('list-view').classList.contains('active')) {
+ renderHotelList();
+ }
+
+ updateHotelCount();
+}
+
+// Update hotel count display
+function updateHotelCount() {
+ document.getElementById('hotel-count').textContent = filteredHotels.length;
+}
+
+// Reset all filters
+function resetFilters() {
+ document.getElementById('search').value = '';
+ document.getElementById('arrondissement').value = 'all';
+ document.getElementById('rating').value = '4.0';
+ document.getElementById('rating-value').textContent = '4.0';
+
+ // Reset price filter to "Under €150" to focus on budget options
+ document.querySelectorAll('.price-filter').forEach(btn => btn.classList.remove('active'));
+ document.querySelector('.price-filter[data-max="150"]').classList.add('active');
+
+ applyFilters();
+}
+
+// Render hotel list
+function renderHotelList() {
+ const listContainer = document.getElementById('hotels-list');
+
+ if (filteredHotels.length === 0) {
+ listContainer.innerHTML = `
+
+
No hotels found
+
Try adjusting your filters to see more results.
+
+ `;
+ return;
+ }
+
+ listContainer.innerHTML = filteredHotels.map(hotel => `
+
+
+
+ ${generateStars(hotel.rating)}
+ ${hotel.rating}
+
+
${hotel.address}
+
${hotel.description}
+
+ ${hotel.amenities.map(amenity => `${amenity}`).join('')}
+
+
+
+ `).join('');
+}
+
+// Center map on selected hotel
+function centerMapOnHotel(hotelName) {
+ const hotel = hotelData.find(h => h.name === hotelName);
+ if (hotel) {
+ // Switch to map view
+ switchView('map');
+
+ // Center map on hotel
+ map.setView([hotel.coordinates[0], hotel.coordinates[1]], 15);
+
+ // Find and open the marker popup
+ const marker = markers.find(m => m.hotelData && m.hotelData.name === hotelName);
+ if (marker) {
+ marker.openPopup();
+
+ // Highlight the marker temporarily
+ const markerElement = marker.getElement();
+ if (markerElement) {
+ const pin = markerElement.querySelector('.marker-pin');
+ if (pin) {
+ pin.style.backgroundColor = '#A84B2F'; // Warning color
+ setTimeout(() => {
+ pin.style.backgroundColor = '#21808D'; // Back to primary
+ }, 2000);
+ }
+ }
+ }
+ }
+}
+
+// Generate star rating HTML
+function generateStars(rating) {
+ const fullStars = Math.floor(rating);
+ const hasHalfStar = rating % 1 >= 0.5;
+ let stars = '';
+
+ for (let i = 0; i < fullStars; i++) {
+ stars += '';
+ }
+
+ if (hasHalfStar) {
+ stars += '';
+ }
+
+ const emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
+ for (let i = 0; i < emptyStars; i++) {
+ stars += '';
+ }
+
+ return stars;
+}
+
+// Initialize with focus on budget hotels (under €150)
+document.addEventListener('DOMContentLoaded', function() {
+ // Small delay to ensure everything is loaded
+ setTimeout(() => {
+ // Set default filter to show budget hotels under €150
+ document.querySelectorAll('.price-filter').forEach(btn => btn.classList.remove('active'));
+ document.querySelector('.price-filter[data-max="150"]').classList.add('active');
+ applyFilters();
+ }, 500);
+});
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..611c8a2
--- /dev/null
+++ b/index.html
@@ -0,0 +1,126 @@
+
+
+
+
+
+ Paris Hotels Under 300€ Per Night
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/paris_hotels_chart.png b/paris_hotels_chart.png
new file mode 100644
index 0000000..0bde0d8
Binary files /dev/null and b/paris_hotels_chart.png differ
diff --git a/paris_hotels_map.html b/paris_hotels_map.html
new file mode 100644
index 0000000..f7bb63d
--- /dev/null
+++ b/paris_hotels_map.html
@@ -0,0 +1,150 @@
+
+
+
+
+ Paris Hotels Under 300€
+
+
+
+
+
+
+ Paris Hotels Under 300€ Per Night
+
+
+
+
+ Top 5 Hotels Under 300€
+
+
+
+
Hôtel Signature Saint Germain des Prés
+
Rating: 4.8/5.0
+
Price Range: €220-290
+
Address: 5 Rue Chomel, 75007 Paris (Arr. 7th)
+
Chic boutique property in Saint-Germain with elegant, colorful rooms and vintage-chic lobby
+
+
+
+
+
Hôtel Saint-André des Arts
+
Rating: 4.8/5.0
+
Price Range: €180-280
+
Address: 66 Rue Saint-André des Arts, 75006 Paris (Arr. 6th)
+
Boutique hotel in Saint-Germain with unique rooms and bar
+
+
+
+
+
Paris France Hotel
+
Rating: 4.7/5.0
+
Price Range: €180-280
+
Address: 72 R. de Turbigo, 75003 Paris (Arr. 3rd)
+
Traditional hotel with a cafe/bar and lounge in the heart of the Marais district
+
+
+
+
+
Hôtel Bourg Tibourg
+
Rating: 4.7/5.0
+
Price Range: €250-300
+
Address: 19 Rue du Bourg Tibourg, 75004 Paris (Arr. 4th)
+
Lavish boutique hotel with opulent rooms and sophisticated dramatic atmosphere
+
+
+
+
+
Boutique Hôtel Mareuil Paris
+
Rating: 4.7/5.0
+
Price Range: €200-280
+
Address: 51 Rue de Malte, 75011 Paris (Arr. 11th)
+
Hip property with modern rooms & suites, plus bar, hammam & exercise room
+
+
+
+
+
+ Budget-Friendly Options (Under 200€)
+
+
+
+
Generator Paris
+
Rating: 4.1/5.0
+
Price Range: €90-150
+
Address: 9-11 Pl. du Colonel Fabien, 75010 Paris (Arr. 10th)
+
Trendy hostel with stylish rooms & streamlined dorms, rooftop terrace & vibrant eatery
+
+
+
+
+
Hotel Marignan
+
Rating: 4.1/5.0
+
Price Range: €120-180
+
Address: 13 Rue du Sommerard, 75005 Paris (Arr. 5th)
+
Laid-back hotel with simple rooms, guest kitchen and library, plus free breakfast
+
+
+
+
+
Hotel Paradis
+
Rating: 4.3/5.0
+
Price Range: €120-180
+
Address: 41 Rue Des Petities Ecuries, 75010 Paris (Arr. 10th)
+
Tastefully decorated rooms in a top-notch location near trendy hotspots
+
+
+
+
+
Grand Hôtel des Gobelins
+
Rating: 4.1/5.0
+
Price Range: €130-180
+
Address: 57 Bd Saint-Marcel, 75013 Paris (Arr. 13th)
+
Casual hotel with warmly furnished rooms and lobby bar
+
+
+
+
+
Hôtel Henriette
+
Rating: 4.5/5.0
+
Price Range: €130-180
+
Address: 9 rue des Gobelins, 75013 Paris (Arr. 13th)
+
Stylish boutique hotel with subtly decorated rooms and courtyard breakfast area
+
+
+
+
+
+
diff --git a/paris_hotels_map_data.json b/paris_hotels_map_data.json
new file mode 100644
index 0000000..715023f
--- /dev/null
+++ b/paris_hotels_map_data.json
@@ -0,0 +1,209 @@
+{
+ "center": [
+ 48.8566,
+ 2.3522
+ ],
+ "zoom": 12,
+ "markers": [
+ {
+ "name": "Hotel Britannique",
+ "coords": [
+ 48.8567,
+ 2.3471
+ ],
+ "price": "€200-320",
+ "rating": 4.6,
+ "arrondissement": "1st"
+ },
+ {
+ "name": "Hôtel Signature Saint Germain des Prés",
+ "coords": [
+ 48.8485,
+ 2.3244
+ ],
+ "price": "€220-290",
+ "rating": 4.8,
+ "arrondissement": "7th"
+ },
+ {
+ "name": "Paris France Hotel",
+ "coords": [
+ 48.8644,
+ 2.3614
+ ],
+ "price": "€180-280",
+ "rating": 4.7,
+ "arrondissement": "3rd"
+ },
+ {
+ "name": "Hôtel Lenox Montparnasse",
+ "coords": [
+ 48.8427,
+ 2.3269
+ ],
+ "price": "€170-260",
+ "rating": 4.5,
+ "arrondissement": "14th"
+ },
+ {
+ "name": "Hôtel Caron de Beaumarchais",
+ "coords": [
+ 48.8572,
+ 2.3598
+ ],
+ "price": "€200-280",
+ "rating": 4.6,
+ "arrondissement": "4th"
+ },
+ {
+ "name": "Hôtel Bourg Tibourg",
+ "coords": [
+ 48.8547,
+ 2.3581
+ ],
+ "price": "€250-300",
+ "rating": 4.7,
+ "arrondissement": "4th"
+ },
+ {
+ "name": "Hôtel de l'Abbaye Saint-Germain",
+ "coords": [
+ 48.8495,
+ 2.3309
+ ],
+ "price": "€280-300",
+ "rating": 4.6,
+ "arrondissement": "6th"
+ },
+ {
+ "name": "Hotel Quartier Latin",
+ "coords": [
+ 48.849,
+ 2.3468
+ ],
+ "price": "€160-250",
+ "rating": 4.3,
+ "arrondissement": "5th"
+ },
+ {
+ "name": "CitizenM Paris Gare de Lyon",
+ "coords": [
+ 48.8448,
+ 2.3732
+ ],
+ "price": "€150-220",
+ "rating": 4.4,
+ "arrondissement": "12th"
+ },
+ {
+ "name": "Hôtel Migny Opéra-Montmartre",
+ "coords": [
+ 48.881,
+ 2.3375
+ ],
+ "price": "€140-200",
+ "rating": 4.1,
+ "arrondissement": "9th"
+ },
+ {
+ "name": "Hôtel Saint-André des Arts",
+ "coords": [
+ 48.854,
+ 2.3401
+ ],
+ "price": "€180-280",
+ "rating": 4.8,
+ "arrondissement": "6th"
+ },
+ {
+ "name": "Grand Hôtel des Gobelins",
+ "coords": [
+ 48.8375,
+ 2.3556
+ ],
+ "price": "€130-180",
+ "rating": 4.1,
+ "arrondissement": "13th"
+ },
+ {
+ "name": "Hotel Marignan",
+ "coords": [
+ 48.8504,
+ 2.3445
+ ],
+ "price": "€120-180",
+ "rating": 4.1,
+ "arrondissement": "5th"
+ },
+ {
+ "name": "Hôtel La Comtesse Tour Eiffel",
+ "coords": [
+ 48.8548,
+ 2.3078
+ ],
+ "price": "€250-300",
+ "rating": 4.6,
+ "arrondissement": "7th"
+ },
+ {
+ "name": "Boutique Hôtel Mareuil Paris",
+ "coords": [
+ 48.864,
+ 2.3677
+ ],
+ "price": "€200-280",
+ "rating": 4.7,
+ "arrondissement": "11th"
+ },
+ {
+ "name": "Generator Paris",
+ "coords": [
+ 48.8772,
+ 2.3711
+ ],
+ "price": "€90-150",
+ "rating": 4.1,
+ "arrondissement": "10th"
+ },
+ {
+ "name": "Hôtel Henriette",
+ "coords": [
+ 48.8363,
+ 2.3518
+ ],
+ "price": "€130-180",
+ "rating": 4.5,
+ "arrondissement": "13th"
+ },
+ {
+ "name": "Hotel Paradis",
+ "coords": [
+ 48.8718,
+ 2.3502
+ ],
+ "price": "€120-180",
+ "rating": 4.3,
+ "arrondissement": "10th"
+ },
+ {
+ "name": "COQ Hotel Paris",
+ "coords": [
+ 48.8261,
+ 2.3542
+ ],
+ "price": "€140-190",
+ "rating": 4.4,
+ "arrondissement": "13th"
+ },
+ {
+ "name": "Hotel Diana",
+ "coords": [
+ 48.8485,
+ 2.3456
+ ],
+ "price": "€150-200",
+ "rating": 4.2,
+ "arrondissement": "5th"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/paris_hotels_price_range.png b/paris_hotels_price_range.png
new file mode 100644
index 0000000..0879cd2
Binary files /dev/null and b/paris_hotels_price_range.png differ
diff --git a/paris_hotels_under_300.csv b/paris_hotels_under_300.csv
new file mode 100644
index 0000000..899d71c
--- /dev/null
+++ b/paris_hotels_under_300.csv
@@ -0,0 +1,21 @@
+name,address,rating,price_range,arrondissement,latitude,longitude,description,website,amenities
+Hotel Britannique,"20 Av. Victoria, 75001 Paris",4.6,€200-320,1st,48.8567,2.3471,Classic 19th-century hotel near Châtelet with theatrical red decor and excellent Metro access,https://hotel-britannique.fr,"Free WiFi, Minibar, Air Conditioning, Elevator"
+Hôtel Signature Saint Germain des Prés,"5 Rue Chomel, 75007 Paris",4.8,€220-290,7th,48.8485,2.3244,"Chic boutique property in Saint-Germain with elegant, colorful rooms and vintage-chic lobby",http://www.signature-saintgermain.com,"Free WiFi, Air Conditioning, Buffet Breakfast, 24-hour Reception"
+Paris France Hotel,"72 R. de Turbigo, 75003 Paris",4.7,€180-280,3rd,48.8644,2.3614,Traditional hotel with a cafe/bar and lounge in the heart of the Marais district,https://www.paris-france-hotel.com,"Free WiFi, Café/Bar, Lounge, Optional Breakfast"
+Hôtel Lenox Montparnasse,"15 Rue Delambre, 75014 Paris",4.5,€170-260,14th,48.8427,2.3269,Classic rooms & suites with warmly decorated honesty bar near Montparnasse,http://www.paris-hotel-lenox.com,"Free WiFi, Honesty Bar, Classic Decor, Cozy Atmosphere"
+Hôtel Caron de Beaumarchais,"12 Rue Vieille du Temple, 75004 Paris",4.6,€200-280,4th,48.8572,2.3598,18th-century lodging with antiques-filled rooms and private balconies in Le Marais,http://www.carondebeaumarchais.com,"Free WiFi, Private Balconies, Antique Furnishings, 18th Century Charm"
+Hôtel Bourg Tibourg,"19 Rue du Bourg Tibourg, 75004 Paris",4.7,€250-300,4th,48.8547,2.3581,Lavish boutique hotel with opulent rooms and sophisticated dramatic atmosphere,https://www.bourgtibourg.com,"Free WiFi, Opulent Decor, Sophisticated Atmosphere, Dramatic Design"
+Hôtel de l'Abbaye Saint-Germain,"10 Rue Cassette, 75006 Paris",4.6,€280-300,6th,48.8495,2.3309,"Refined hotel with plush lounges and garden, plus free WiFi & breakfast",https://www.hotelabbayeparis.com,"Free WiFi, Garden, Plush Lounges, Breakfast Included"
+Hotel Quartier Latin,"9 Rue des Écoles, 75005 Paris",4.3,€160-250,5th,48.849,2.3468,"Traditional hotel with simple rooms, minibars and breakfast buffet option",https://hotelquartierlatin.com,"Free WiFi, Minibar, Breakfast Buffet, Traditional Decor"
+CitizenM Paris Gare de Lyon,"8 Rue Van Gogh, 75012 Paris",4.4,€150-220,12th,48.8448,2.3732,"Trendy budget hotel with compact rooms, ambient lighting and 24-hour cafe/bar",https://www.citizenm.com/hotels/europe/paris/gare-de-lyon-hotel,"24-hour Café/Bar, Ambient Lighting, Compact Design, Modern Amenities"
+Hôtel Migny Opéra-Montmartre,"13 Rue Victor Massé, 75009 Paris",4.1,€140-200,9th,48.881,2.3375,"Colorful rooms with satellite TV, casual breakfast area and lounge near Opéra",https://www.hotelmigny.com,"Free WiFi, Satellite TV, Breakfast Area, LGBTQ+ Friendly"
+Hôtel Saint-André des Arts,"66 Rue Saint-André des Arts, 75006 Paris",4.8,€180-280,6th,48.854,2.3401,Boutique hotel in Saint-Germain with unique rooms and bar,https://www.saintandredesarts.com,"Bar, Unique Rooms, Saint-Germain Location, Boutique Style"
+Grand Hôtel des Gobelins,"57 Bd Saint-Marcel, 75013 Paris",4.1,€130-180,13th,48.8375,2.3556,Casual hotel with warmly furnished rooms and lobby bar,http://www.grandhotelgobelins.com,"Free WiFi, Lobby Bar, Warm Furnishings, Relaxed Atmosphere"
+Hotel Marignan,"13 Rue du Sommerard, 75005 Paris",4.1,€120-180,5th,48.8504,2.3445,"Laid-back hotel with simple rooms, guest kitchen and library, plus free breakfast",http://www.hotel-marignan.com,"Free Breakfast, Free WiFi, Guest Kitchen, Library"
+Hôtel La Comtesse Tour Eiffel,"29 Av. de Tourville, 75007 Paris",4.6,€250-300,7th,48.8548,2.3078,Traditional hotel with classic rooms and Eiffel Tower views,http://www.comtesse-hotel.com,"Free WiFi, Eiffel Tower Views, Bar, Traditional Decor"
+Boutique Hôtel Mareuil Paris,"51 Rue de Malte, 75011 Paris",4.7,€200-280,11th,48.864,2.3677,"Hip property with modern rooms & suites, plus bar, hammam & exercise room",https://www.hotelmareuil.com/fr/,"Bar, Hammam, Exercise Room, Modern Design"
+Generator Paris,"9-11 Pl. du Colonel Fabien, 75010 Paris",4.1,€90-150,10th,48.8772,2.3711,"Trendy hostel with stylish rooms & streamlined dorms, rooftop terrace & vibrant eatery",https://staygenerator.com/hostels/paris?lang=fr-FR,"Rooftop Terrace, Restaurant, Bar, Budget-Friendly"
+Hôtel Henriette,"9 rue des Gobelins, 75013 Paris",4.5,€130-180,13th,48.8363,2.3518,Stylish boutique hotel with subtly decorated rooms and courtyard breakfast area,https://www.hotelhenriette.com,"Free WiFi, Courtyard, Breakfast, Design Interiors"
+Hotel Paradis,"41 Rue Des Petities Ecuries, 75010 Paris",4.3,€120-180,10th,48.8718,2.3502,Tastefully decorated rooms in a top-notch location near trendy hotspots,https://hotelparadisparis.com,"Free WiFi, Designer Decor, Trendy Area, Modern Amenities"
+COQ Hotel Paris,"15 Rue Edouard Manet, 75013 Paris",4.4,€140-190,13th,48.8261,2.3542,"Stylish design hotel with cozy bar, extensive breakfast and warm atmosphere",https://coq-hotel-paris.com,"Bar, Breakfast, Design Interiors, Cozy Atmosphere"
+Hotel Diana,"73 Rue Saint-Jacques, 75005 Paris",4.2,€150-200,5th,48.8485,2.3456,Simple but comfortable 2-star hotel with bright rooms and modern bathrooms,http://www.hoteldiana.fr,"Free WiFi, Modern Bathrooms, Central Location, Budget-Friendly"
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..9f583de
--- /dev/null
+++ b/style.css
@@ -0,0 +1,1165 @@
+
+:root {
+ /* Colors */
+ --color-background: rgba(252, 252, 249, 1);
+ --color-surface: rgba(255, 255, 253, 1);
+ --color-text: rgba(19, 52, 59, 1);
+ --color-text-secondary: rgba(98, 108, 113, 1);
+ --color-primary: rgba(33, 128, 141, 1);
+ --color-primary-hover: rgba(29, 116, 128, 1);
+ --color-primary-active: rgba(26, 104, 115, 1);
+ --color-secondary: rgba(94, 82, 64, 0.12);
+ --color-secondary-hover: rgba(94, 82, 64, 0.2);
+ --color-secondary-active: rgba(94, 82, 64, 0.25);
+ --color-border: rgba(94, 82, 64, 0.2);
+ --color-btn-primary-text: rgba(252, 252, 249, 1);
+ --color-card-border: rgba(94, 82, 64, 0.12);
+ --color-card-border-inner: rgba(94, 82, 64, 0.12);
+ --color-error: rgba(192, 21, 47, 1);
+ --color-success: rgba(33, 128, 141, 1);
+ --color-warning: rgba(168, 75, 47, 1);
+ --color-info: rgba(98, 108, 113, 1);
+ --color-focus-ring: rgba(33, 128, 141, 0.4);
+ --color-select-caret: rgba(19, 52, 59, 0.8);
+
+ /* Common style patterns */
+ --focus-ring: 0 0 0 3px var(--color-focus-ring);
+ --focus-outline: 2px solid var(--color-primary);
+ --status-bg-opacity: 0.15;
+ --status-border-opacity: 0.25;
+ --select-caret-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23134252' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
+ --select-caret-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23f5f5f5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
+
+ /* RGB versions for opacity control */
+ --color-success-rgb: 33, 128, 141;
+ --color-error-rgb: 192, 21, 47;
+ --color-warning-rgb: 168, 75, 47;
+ --color-info-rgb: 98, 108, 113;
+
+ /* Typography */
+ --font-family-base: "FKGroteskNeue", "Geist", "Inter", -apple-system,
+ BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ --font-family-mono: "Berkeley Mono", ui-monospace, SFMono-Regular, Menlo,
+ Monaco, Consolas, monospace;
+ --font-size-xs: 11px;
+ --font-size-sm: 12px;
+ --font-size-base: 14px;
+ --font-size-md: 14px;
+ --font-size-lg: 16px;
+ --font-size-xl: 18px;
+ --font-size-2xl: 20px;
+ --font-size-3xl: 24px;
+ --font-size-4xl: 30px;
+ --font-weight-normal: 400;
+ --font-weight-medium: 500;
+ --font-weight-semibold: 550;
+ --font-weight-bold: 600;
+ --line-height-tight: 1.2;
+ --line-height-normal: 1.5;
+ --letter-spacing-tight: -0.01em;
+
+ /* Spacing */
+ --space-0: 0;
+ --space-1: 1px;
+ --space-2: 2px;
+ --space-4: 4px;
+ --space-6: 6px;
+ --space-8: 8px;
+ --space-10: 10px;
+ --space-12: 12px;
+ --space-16: 16px;
+ --space-20: 20px;
+ --space-24: 24px;
+ --space-32: 32px;
+
+ /* Border Radius */
+ --radius-sm: 6px;
+ --radius-base: 8px;
+ --radius-md: 10px;
+ --radius-lg: 12px;
+ --radius-full: 9999px;
+
+ /* Shadows */
+ --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.02);
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02);
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.04),
+ 0 2px 4px -1px rgba(0, 0, 0, 0.02);
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.04),
+ 0 4px 6px -2px rgba(0, 0, 0, 0.02);
+ --shadow-inset-sm: inset 0 1px 0 rgba(255, 255, 255, 0.15),
+ inset 0 -1px 0 rgba(0, 0, 0, 0.03);
+
+ /* Animation */
+ --duration-fast: 150ms;
+ --duration-normal: 250ms;
+ --ease-standard: cubic-bezier(0.16, 1, 0.3, 1);
+
+ /* Layout */
+ --container-sm: 640px;
+ --container-md: 768px;
+ --container-lg: 1024px;
+ --container-xl: 1280px;
+}
+
+/* Dark mode colors */
+@media (prefers-color-scheme: dark) {
+ :root {
+ --color-background: rgba(31, 33, 33, 1);
+ --color-surface: rgba(38, 40, 40, 1);
+ --color-text: rgba(245, 245, 245, 1);
+ --color-text-secondary: rgba(167, 169, 169, 0.7);
+ --color-primary: rgba(50, 184, 198, 1);
+ --color-primary-hover: rgba(45, 166, 178, 1);
+ --color-primary-active: rgba(41, 150, 161, 1);
+ --color-secondary: rgba(119, 124, 124, 0.15);
+ --color-secondary-hover: rgba(119, 124, 124, 0.25);
+ --color-secondary-active: rgba(119, 124, 124, 0.3);
+ --color-border: rgba(119, 124, 124, 0.3);
+ --color-error: rgba(255, 84, 89, 1);
+ --color-success: rgba(50, 184, 198, 1);
+ --color-warning: rgba(230, 129, 97, 1);
+ --color-info: rgba(167, 169, 169, 1);
+ --color-focus-ring: rgba(50, 184, 198, 0.4);
+ --color-btn-primary-text: rgba(19, 52, 59, 1);
+ --color-card-border: rgba(119, 124, 124, 0.2);
+ --color-card-border-inner: rgba(119, 124, 124, 0.15);
+ --shadow-inset-sm: inset 0 1px 0 rgba(255, 255, 255, 0.1),
+ inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+ --button-border-secondary: rgba(119, 124, 124, 0.2);
+ --color-border-secondary: rgba(119, 124, 124, 0.2);
+ --color-select-caret: rgba(245, 245, 245, 0.8);
+
+ /* Common style patterns - updated for dark mode */
+ --focus-ring: 0 0 0 3px var(--color-focus-ring);
+ --focus-outline: 2px solid var(--color-primary);
+ --status-bg-opacity: 0.15;
+ --status-border-opacity: 0.25;
+ --select-caret-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23134252' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
+ --select-caret-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23f5f5f5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
+
+ /* RGB versions for dark mode */
+ --color-success-rgb: 50, 184, 198;
+ --color-error-rgb: 255, 84, 89;
+ --color-warning-rgb: 230, 129, 97;
+ --color-info-rgb: 167, 169, 169;
+ }
+}
+
+/* Data attribute for manual theme switching */
+[data-color-scheme="dark"] {
+ --color-background: rgba(31, 33, 33, 1);
+ --color-surface: rgba(38, 40, 40, 1);
+ --color-text: rgba(245, 245, 245, 1);
+ --color-text-secondary: rgba(167, 169, 169, 0.7);
+ --color-primary: rgba(50, 184, 198, 1);
+ --color-primary-hover: rgba(45, 166, 178, 1);
+ --color-primary-active: rgba(41, 150, 161, 1);
+ --color-secondary: rgba(119, 124, 124, 0.15);
+ --color-secondary-hover: rgba(119, 124, 124, 0.25);
+ --color-secondary-active: rgba(119, 124, 124, 0.3);
+ --color-border: rgba(119, 124, 124, 0.3);
+ --color-error: rgba(255, 84, 89, 1);
+ --color-success: rgba(50, 184, 198, 1);
+ --color-warning: rgba(230, 129, 97, 1);
+ --color-info: rgba(167, 169, 169, 1);
+ --color-focus-ring: rgba(50, 184, 198, 0.4);
+ --color-btn-primary-text: rgba(19, 52, 59, 1);
+ --color-card-border: rgba(119, 124, 124, 0.15);
+ --color-card-border-inner: rgba(119, 124, 124, 0.15);
+ --shadow-inset-sm: inset 0 1px 0 rgba(255, 255, 255, 0.1),
+ inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+ --color-border-secondary: rgba(119, 124, 124, 0.2);
+ --color-select-caret: rgba(245, 245, 245, 0.8);
+
+ /* Common style patterns - updated for dark mode */
+ --focus-ring: 0 0 0 3px var(--color-focus-ring);
+ --focus-outline: 2px solid var(--color-primary);
+ --status-bg-opacity: 0.15;
+ --status-border-opacity: 0.25;
+ --select-caret-light: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23134252' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
+ --select-caret-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23f5f5f5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
+
+ /* RGB versions for dark mode */
+ --color-success-rgb: 50, 184, 198;
+ --color-error-rgb: 255, 84, 89;
+ --color-warning-rgb: 230, 129, 97;
+ --color-info-rgb: 167, 169, 169;
+}
+
+[data-color-scheme="light"] {
+ --color-background: rgba(252, 252, 249, 1);
+ --color-surface: rgba(255, 255, 253, 1);
+ --color-text: rgba(19, 52, 59, 1);
+ --color-text-secondary: rgba(98, 108, 113, 1);
+ --color-primary: rgba(33, 128, 141, 1);
+ --color-primary-hover: rgba(29, 116, 128, 1);
+ --color-primary-active: rgba(26, 104, 115, 1);
+ --color-secondary: rgba(94, 82, 64, 0.12);
+ --color-secondary-hover: rgba(94, 82, 64, 0.2);
+ --color-secondary-active: rgba(94, 82, 64, 0.25);
+ --color-border: rgba(94, 82, 64, 0.2);
+ --color-btn-primary-text: rgba(252, 252, 249, 1);
+ --color-card-border: rgba(94, 82, 64, 0.12);
+ --color-card-border-inner: rgba(94, 82, 64, 0.12);
+ --color-error: rgba(192, 21, 47, 1);
+ --color-success: rgba(33, 128, 141, 1);
+ --color-warning: rgba(168, 75, 47, 1);
+ --color-info: rgba(98, 108, 113, 1);
+ --color-focus-ring: rgba(33, 128, 141, 0.4);
+
+ /* RGB versions for light mode */
+ --color-success-rgb: 33, 128, 141;
+ --color-error-rgb: 192, 21, 47;
+ --color-warning-rgb: 168, 75, 47;
+ --color-info-rgb: 98, 108, 113;
+}
+
+/* Base styles */
+html {
+ font-size: var(--font-size-base);
+ font-family: var(--font-family-base);
+ line-height: var(--line-height-normal);
+ color: var(--color-text);
+ background-color: var(--color-background);
+ -webkit-font-smoothing: antialiased;
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: inherit;
+}
+
+/* Typography */
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ margin: 0;
+ font-weight: var(--font-weight-semibold);
+ line-height: var(--line-height-tight);
+ color: var(--color-text);
+ letter-spacing: var(--letter-spacing-tight);
+}
+
+h1 {
+ font-size: var(--font-size-4xl);
+}
+h2 {
+ font-size: var(--font-size-3xl);
+}
+h3 {
+ font-size: var(--font-size-2xl);
+}
+h4 {
+ font-size: var(--font-size-xl);
+}
+h5 {
+ font-size: var(--font-size-lg);
+}
+h6 {
+ font-size: var(--font-size-md);
+}
+
+p {
+ margin: 0 0 var(--space-16) 0;
+}
+
+a {
+ color: var(--color-primary);
+ text-decoration: none;
+ transition: color var(--duration-fast) var(--ease-standard);
+}
+
+a:hover {
+ color: var(--color-primary-hover);
+}
+
+code,
+pre {
+ font-family: var(--font-family-mono);
+ font-size: calc(var(--font-size-base) * 0.95);
+ background-color: var(--color-secondary);
+ border-radius: var(--radius-sm);
+}
+
+code {
+ padding: var(--space-1) var(--space-4);
+}
+
+pre {
+ padding: var(--space-16);
+ margin: var(--space-16) 0;
+ overflow: auto;
+ border: 1px solid var(--color-border);
+}
+
+pre code {
+ background: none;
+ padding: 0;
+}
+
+/* Buttons */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--space-8) var(--space-16);
+ border-radius: var(--radius-base);
+ font-size: var(--font-size-base);
+ font-weight: 500;
+ line-height: 1.5;
+ cursor: pointer;
+ transition: all var(--duration-normal) var(--ease-standard);
+ border: none;
+ text-decoration: none;
+ position: relative;
+}
+
+.btn:focus-visible {
+ outline: none;
+ box-shadow: var(--focus-ring);
+}
+
+.btn--primary {
+ background: var(--color-primary);
+ color: var(--color-btn-primary-text);
+}
+
+.btn--primary:hover {
+ background: var(--color-primary-hover);
+}
+
+.btn--primary:active {
+ background: var(--color-primary-active);
+}
+
+.btn--secondary {
+ background: var(--color-secondary);
+ color: var(--color-text);
+}
+
+.btn--secondary:hover {
+ background: var(--color-secondary-hover);
+}
+
+.btn--secondary:active {
+ background: var(--color-secondary-active);
+}
+
+.btn--outline {
+ background: transparent;
+ border: 1px solid var(--color-border);
+ color: var(--color-text);
+}
+
+.btn--outline:hover {
+ background: var(--color-secondary);
+}
+
+.btn--sm {
+ padding: var(--space-4) var(--space-12);
+ font-size: var(--font-size-sm);
+ border-radius: var(--radius-sm);
+}
+
+.btn--lg {
+ padding: var(--space-10) var(--space-20);
+ font-size: var(--font-size-lg);
+ border-radius: var(--radius-md);
+}
+
+.btn--full-width {
+ width: 100%;
+}
+
+.btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Form elements */
+.form-control {
+ display: block;
+ width: 100%;
+ padding: var(--space-8) var(--space-12);
+ font-size: var(--font-size-md);
+ line-height: 1.5;
+ color: var(--color-text);
+ background-color: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-base);
+ transition: border-color var(--duration-fast) var(--ease-standard),
+ box-shadow var(--duration-fast) var(--ease-standard);
+}
+
+textarea.form-control {
+ font-family: var(--font-family-base);
+ font-size: var(--font-size-base);
+}
+
+select.form-control {
+ padding: var(--space-8) var(--space-12);
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background-image: var(--select-caret-light);
+ background-repeat: no-repeat;
+ background-position: right var(--space-12) center;
+ background-size: 16px;
+ padding-right: var(--space-32);
+}
+
+/* Add a dark mode specific caret */
+@media (prefers-color-scheme: dark) {
+ select.form-control {
+ background-image: var(--select-caret-dark);
+ }
+}
+
+/* Also handle data-color-scheme */
+[data-color-scheme="dark"] select.form-control {
+ background-image: var(--select-caret-dark);
+}
+
+[data-color-scheme="light"] select.form-control {
+ background-image: var(--select-caret-light);
+}
+
+.form-control:focus {
+ border-color: var(--color-primary);
+ outline: var(--focus-outline);
+}
+
+.form-label {
+ display: block;
+ margin-bottom: var(--space-8);
+ font-weight: var(--font-weight-medium);
+ font-size: var(--font-size-sm);
+}
+
+.form-group {
+ margin-bottom: var(--space-16);
+}
+
+/* Card component */
+.card {
+ background-color: var(--color-surface);
+ border-radius: var(--radius-lg);
+ border: 1px solid var(--color-card-border);
+ box-shadow: var(--shadow-sm);
+ overflow: hidden;
+ transition: box-shadow var(--duration-normal) var(--ease-standard);
+}
+
+.card:hover {
+ box-shadow: var(--shadow-md);
+}
+
+.card__body {
+ padding: var(--space-16);
+}
+
+.card__header,
+.card__footer {
+ padding: var(--space-16);
+ border-bottom: 1px solid var(--color-card-border-inner);
+}
+
+/* Status indicators - simplified with CSS variables */
+.status {
+ display: inline-flex;
+ align-items: center;
+ padding: var(--space-6) var(--space-12);
+ border-radius: var(--radius-full);
+ font-weight: var(--font-weight-medium);
+ font-size: var(--font-size-sm);
+}
+
+.status--success {
+ background-color: rgba(
+ var(--color-success-rgb, 33, 128, 141),
+ var(--status-bg-opacity)
+ );
+ color: var(--color-success);
+ border: 1px solid
+ rgba(var(--color-success-rgb, 33, 128, 141), var(--status-border-opacity));
+}
+
+.status--error {
+ background-color: rgba(
+ var(--color-error-rgb, 192, 21, 47),
+ var(--status-bg-opacity)
+ );
+ color: var(--color-error);
+ border: 1px solid
+ rgba(var(--color-error-rgb, 192, 21, 47), var(--status-border-opacity));
+}
+
+.status--warning {
+ background-color: rgba(
+ var(--color-warning-rgb, 168, 75, 47),
+ var(--status-bg-opacity)
+ );
+ color: var(--color-warning);
+ border: 1px solid
+ rgba(var(--color-warning-rgb, 168, 75, 47), var(--status-border-opacity));
+}
+
+.status--info {
+ background-color: rgba(
+ var(--color-info-rgb, 98, 108, 113),
+ var(--status-bg-opacity)
+ );
+ color: var(--color-info);
+ border: 1px solid
+ rgba(var(--color-info-rgb, 98, 108, 113), var(--status-border-opacity));
+}
+
+/* Container layout */
+.container {
+ width: 100%;
+ margin-right: auto;
+ margin-left: auto;
+ padding-right: var(--space-16);
+ padding-left: var(--space-16);
+}
+
+@media (min-width: 640px) {
+ .container {
+ max-width: var(--container-sm);
+ }
+}
+@media (min-width: 768px) {
+ .container {
+ max-width: var(--container-md);
+ }
+}
+@media (min-width: 1024px) {
+ .container {
+ max-width: var(--container-lg);
+ }
+}
+@media (min-width: 1280px) {
+ .container {
+ max-width: var(--container-xl);
+ }
+}
+
+/* Utility classes */
+.flex {
+ display: flex;
+}
+.flex-col {
+ flex-direction: column;
+}
+.items-center {
+ align-items: center;
+}
+.justify-center {
+ justify-content: center;
+}
+.justify-between {
+ justify-content: space-between;
+}
+.gap-4 {
+ gap: var(--space-4);
+}
+.gap-8 {
+ gap: var(--space-8);
+}
+.gap-16 {
+ gap: var(--space-16);
+}
+
+.m-0 {
+ margin: 0;
+}
+.mt-8 {
+ margin-top: var(--space-8);
+}
+.mb-8 {
+ margin-bottom: var(--space-8);
+}
+.mx-8 {
+ margin-left: var(--space-8);
+ margin-right: var(--space-8);
+}
+.my-8 {
+ margin-top: var(--space-8);
+ margin-bottom: var(--space-8);
+}
+
+.p-0 {
+ padding: 0;
+}
+.py-8 {
+ padding-top: var(--space-8);
+ padding-bottom: var(--space-8);
+}
+.px-8 {
+ padding-left: var(--space-8);
+ padding-right: var(--space-8);
+}
+.py-16 {
+ padding-top: var(--space-16);
+ padding-bottom: var(--space-16);
+}
+.px-16 {
+ padding-left: var(--space-16);
+ padding-right: var(--space-16);
+}
+
+.block {
+ display: block;
+}
+.hidden {
+ display: none;
+}
+
+/* Accessibility */
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border-width: 0;
+}
+
+:focus-visible {
+ outline: var(--focus-outline);
+ outline-offset: 2px;
+}
+
+/* Dark mode specifics */
+[data-color-scheme="dark"] .btn--outline {
+ border: 1px solid var(--color-border-secondary);
+}
+
+@font-face {
+ font-family: 'FKGroteskNeue';
+ src: url('https://r2cdn.perplexity.ai/fonts/FKGroteskNeue.woff2')
+ format('woff2');
+}
+
+/* Application Header */
+.app-header {
+ background-color: var(--color-surface);
+ border-bottom: 1px solid var(--color-border);
+ padding: var(--space-16) 0;
+ box-shadow: var(--shadow-sm);
+ position: sticky;
+ top: 0;
+ z-index: 1000;
+}
+
+.app-header h1 {
+ color: var(--color-primary);
+ font-size: var(--font-size-2xl);
+ margin: 0;
+}
+
+.view-toggle {
+ display: flex;
+ gap: var(--space-8);
+}
+
+.view-toggle .btn {
+ display: flex;
+ align-items: center;
+ gap: var(--space-8);
+ padding: var(--space-8) var(--space-16);
+}
+
+.view-toggle .btn.active {
+ background: var(--color-primary);
+ color: var(--color-btn-primary-text);
+}
+
+/* Main Layout */
+.app-layout {
+ display: grid;
+ grid-template-columns: 350px 1fr;
+ gap: var(--space-24);
+ padding: var(--space-24) 0;
+ min-height: calc(100vh - 200px);
+}
+
+/* Sidebar */
+.sidebar {
+ background-color: var(--color-surface);
+ border-radius: var(--radius-lg);
+ border: 1px solid var(--color-border);
+ height: fit-content;
+ position: sticky;
+ top: 120px;
+ overflow-y: auto;
+ max-height: calc(100vh - 140px);
+}
+
+.filter-section {
+ padding: var(--space-24);
+ border-bottom: 1px solid var(--color-border);
+}
+
+.filter-section h2 {
+ color: var(--color-text);
+ font-size: var(--font-size-xl);
+ margin-bottom: var(--space-20);
+}
+
+.price-filters {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--space-8);
+}
+
+.price-filter {
+ padding: var(--space-8) var(--space-12);
+ border: 1px solid var(--color-border);
+ background: var(--color-surface);
+ color: var(--color-text);
+ border-radius: var(--radius-base);
+ cursor: pointer;
+ transition: all var(--duration-fast) var(--ease-standard);
+ font-size: var(--font-size-sm);
+ text-align: center;
+}
+
+.price-filter:hover {
+ background: var(--color-secondary-hover);
+}
+
+.price-filter.active {
+ background: var(--color-primary);
+ color: var(--color-btn-primary-text);
+ border-color: var(--color-primary);
+}
+
+.filter-stats {
+ margin-top: var(--space-20);
+ padding-top: var(--space-16);
+ border-top: 1px solid var(--color-border);
+ text-align: center;
+}
+
+.filter-stats p {
+ font-weight: var(--font-weight-medium);
+ color: var(--color-text-secondary);
+ margin-bottom: var(--space-12);
+}
+
+.chart-section {
+ padding: var(--space-24);
+}
+
+.chart-section h3 {
+ color: var(--color-text);
+ font-size: var(--font-size-lg);
+ margin-bottom: var(--space-16);
+}
+
+.chart-container {
+ margin-bottom: var(--space-24);
+}
+
+.chart {
+ width: 100%;
+ height: auto;
+ border-radius: var(--radius-base);
+ border: 1px solid var(--color-border);
+}
+
+/* Content Area */
+.content {
+ position: relative;
+ min-height: 600px;
+}
+
+.view {
+ display: none;
+ width: 100%;
+ height: 100%;
+}
+
+.view.active {
+ display: block;
+}
+
+/* Map Styles */
+#map-view {
+ position: relative;
+}
+
+#map {
+ width: 100%;
+ height: 700px;
+ border-radius: var(--radius-lg);
+ border: 1px solid var(--color-border);
+ box-shadow: var(--shadow-md);
+}
+
+.map-legend {
+ position: absolute;
+ top: var(--space-16);
+ right: var(--space-16);
+ background: var(--color-surface);
+ padding: var(--space-16);
+ border-radius: var(--radius-base);
+ border: 1px solid var(--color-border);
+ box-shadow: var(--shadow-sm);
+ z-index: 1000;
+}
+
+.map-legend h4 {
+ margin: 0 0 var(--space-12) 0;
+ font-size: var(--font-size-sm);
+ color: var(--color-text);
+}
+
+.legend-item {
+ display: flex;
+ align-items: center;
+ gap: var(--space-8);
+ margin-bottom: var(--space-8);
+}
+
+.legend-item:last-child {
+ margin-bottom: 0;
+}
+
+.marker {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ border: 2px solid white;
+}
+
+.blue-marker {
+ background-color: var(--color-primary);
+}
+
+.highlighted-marker {
+ background-color: var(--color-warning);
+}
+
+/* List View Styles */
+#list-view {
+ max-height: 700px;
+ overflow-y: auto;
+ border-radius: var(--radius-lg);
+ border: 1px solid var(--color-border);
+}
+
+.hotel-card {
+ background: var(--color-surface);
+ border-bottom: 1px solid var(--color-border);
+ padding: var(--space-20);
+ cursor: pointer;
+ transition: background-color var(--duration-fast) var(--ease-standard);
+}
+
+.hotel-card:hover {
+ background: var(--color-secondary);
+}
+
+.hotel-card:last-child {
+ border-bottom: none;
+}
+
+.hotel-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: var(--space-12);
+}
+
+.hotel-name {
+ font-size: var(--font-size-lg);
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-text);
+ margin: 0;
+}
+
+.hotel-price {
+ font-size: var(--font-size-lg);
+ font-weight: var(--font-weight-bold);
+ color: var(--color-primary);
+}
+
+.hotel-rating {
+ display: flex;
+ align-items: center;
+ gap: var(--space-4);
+ margin-bottom: var(--space-8);
+}
+
+.stars {
+ color: #FFD700;
+}
+
+.rating-value {
+ font-weight: var(--font-weight-medium);
+ color: var(--color-text-secondary);
+}
+
+.hotel-address {
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-sm);
+ margin-bottom: var(--space-12);
+}
+
+.hotel-description {
+ color: var(--color-text);
+ margin-bottom: var(--space-16);
+ line-height: var(--line-height-normal);
+}
+
+.hotel-amenities {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-8);
+ margin-bottom: var(--space-16);
+}
+
+.amenity-tag {
+ background: var(--color-secondary);
+ color: var(--color-text-secondary);
+ padding: var(--space-4) var(--space-8);
+ border-radius: var(--radius-full);
+ font-size: var(--font-size-xs);
+}
+
+.hotel-actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.arrondissement-badge {
+ background: var(--color-primary);
+ color: var(--color-btn-primary-text);
+ padding: var(--space-4) var(--space-8);
+ border-radius: var(--radius-full);
+ font-size: var(--font-size-xs);
+ font-weight: var(--font-weight-medium);
+}
+
+.visit-website {
+ color: var(--color-primary);
+ text-decoration: none;
+ font-weight: var(--font-weight-medium);
+ display: flex;
+ align-items: center;
+ gap: var(--space-4);
+}
+
+.visit-website:hover {
+ color: var(--color-primary-hover);
+}
+
+/* Custom Leaflet Popup Styles */
+.leaflet-popup-content-wrapper {
+ border-radius: var(--radius-base);
+ border: 1px solid var(--color-border);
+}
+
+.leaflet-popup-content {
+ margin: var(--space-16);
+ line-height: var(--line-height-normal);
+}
+
+.popup-hotel-name {
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-primary);
+ margin-bottom: var(--space-8);
+}
+
+.popup-rating {
+ display: flex;
+ align-items: center;
+ gap: var(--space-4);
+ margin-bottom: var(--space-8);
+}
+
+.popup-price {
+ font-weight: var(--font-weight-bold);
+ color: var(--color-warning);
+ margin-bottom: var(--space-8);
+}
+
+.popup-arrondissement {
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-sm);
+}
+
+/* Footer */
+footer {
+ background-color: var(--color-surface);
+ border-top: 1px solid var(--color-border);
+ padding: var(--space-16) 0;
+ margin-top: var(--space-32);
+ text-align: center;
+ color: var(--color-text-secondary);
+}
+
+/* Responsive Design */
+@media (max-width: 1024px) {
+ .app-layout {
+ grid-template-columns: 300px 1fr;
+ gap: var(--space-16);
+ }
+
+ .sidebar {
+ top: 100px;
+ }
+}
+
+@media (max-width: 768px) {
+ .app-header .container {
+ flex-direction: column;
+ gap: var(--space-16);
+ text-align: center;
+ }
+
+ .app-header h1 {
+ font-size: var(--font-size-xl);
+ }
+
+ .app-layout {
+ grid-template-columns: 1fr;
+ gap: var(--space-16);
+ }
+
+ .sidebar {
+ position: static;
+ max-height: none;
+ }
+
+ .filter-section {
+ padding: var(--space-16);
+ }
+
+ .chart-section {
+ padding: var(--space-16);
+ }
+
+ .price-filters {
+ grid-template-columns: 1fr;
+ }
+
+ #map {
+ height: 500px;
+ }
+
+ .map-legend {
+ position: static;
+ margin-top: var(--space-16);
+ }
+}
+
+@media (max-width: 480px) {
+ .container {
+ padding-left: var(--space-12);
+ padding-right: var(--space-12);
+ }
+
+ .app-layout {
+ padding: var(--space-16) 0;
+ }
+
+ .view-toggle {
+ flex-direction: column;
+ width: 100%;
+ }
+
+ .view-toggle .btn {
+ justify-content: center;
+ }
+
+ .hotel-header {
+ flex-direction: column;
+ gap: var(--space-8);
+ }
+
+ .hotel-actions {
+ flex-direction: column;
+ gap: var(--space-8);
+ align-items: flex-start;
+ }
+
+ #map {
+ height: 400px;
+ }
+}
+
+/* Loading and Empty States */
+.loading {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 200px;
+ color: var(--color-text-secondary);
+}
+
+.empty-state {
+ text-align: center;
+ padding: var(--space-32);
+ color: var(--color-text-secondary);
+}
+
+.empty-state h3 {
+ margin-bottom: var(--space-16);
+}
+
+/* Accessibility improvements */
+.btn:focus-visible,
+.form-control:focus-visible,
+.price-filter:focus-visible {
+ outline: var(--focus-outline);
+ outline-offset: 2px;
+}
+
+/* Range slider styling */
+input[type="range"] {
+ width: 100%;
+ height: 6px;
+ border-radius: var(--radius-sm);
+ background: var(--color-secondary);
+ outline: none;
+ -webkit-appearance: none;
+}
+
+input[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: var(--color-primary);
+ cursor: pointer;
+ border: 2px solid var(--color-surface);
+ box-shadow: var(--shadow-sm);
+}
+
+input[type="range"]::-moz-range-thumb {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: var(--color-primary);
+ cursor: pointer;
+ border: 2px solid var(--color-surface);
+ box-shadow: var(--shadow-sm);
+}
\ No newline at end of file