Skip to content
Accessibility Map — Romania
Accessibility Map Whole Romania
/* ============================================================================ ACCESSIBILITY MAP — script.js ============================================================================ Vanilla JS, no build step. Sections: 1) LOCATIONS — the data array (edit to add/remove places) 2) CONFIG — initial map view + tile provider 3) STATE — module-level mutables 4) MAP INIT — boots Leaflet 5) MARKERS — adds/removes pins from LOCATIONS 6) FILTERING — filters locations by city 7) DRAWER — open/close the left navigation drawer 8) SIDE PANEL — fills + opens the location details card 9) BOOTSTRAP — wires everything together on DOMContentLoaded To customize: • ADD / EDIT LOCATIONS -> edit the LOCATIONS array (section 1) • CHANGE INITIAL VIEW -> edit CONFIG.initialView (section 2) • CHANGE MAP TILES -> edit CONFIG.tileLayer (section 2) • ADD A NEW CITY FILTER -> add a button in index.html (data-city=”X”) AND add at least one location with city=”X” ============================================================================ */ /* ============================================================================ 1) LOCATIONS DATA ARRAY —————————————————————————- >>> ADD NEW LOCATIONS TO THIS ARRAY <<< Required fields per object: id, title, city, coordinates {lat, lng}, imageUrl, description, ratings { physicalAccessibility (0-5), physicalNotes, overallScore (0-5) } ============================================================================ */ const LOCATIONS = [ // ----------------------------- ORADEA ------------------------------------ { id: 1, title: "Piața Unirii", city: "Oradea", coordinates: { lat: 47.0556, lng: 21.9303 }, imageUrl: "https://images.unsplash.com/photo-1599689019338-9c4a8e1b1c1c?w=800&auto=format&fit=crop", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Piața Unirii is the central square of Oradea, " + "surrounded by Art Nouveau architecture and home to the Black Eagle Palace. Wide pedestrian zones make it " + "one of the more accessible city centers in the region.", ratings: { physicalAccessibility: 4, physicalNotes: "Mostly flat cobblestone surface with smooth pedestrian paths around the perimeter. Curb cuts at most " + "crossings, but cobble seams may be uncomfortable for power-chair users.", overallScore: 4.3, }, }, { id: 2, title: "Cetatea Oradea (Oradea Fortress)", city: "Oradea", coordinates: { lat: 47.0506, lng: 21.9425 }, imageUrl: "https://images.unsplash.com/photo-1564507592333-c60657eea523?w=800&auto=format&fit=crop", description: "Lorem ipsum dolor sit amet. The pentagonal Renaissance fortress is one of Oradea's landmark sites. " + "Recently renovated walkways and a central courtyard make most outdoor areas reachable, though some " + "of the inner exhibitions involve narrow stone staircases.", ratings: { physicalAccessibility: 3, physicalNotes: "Ramped main entrance and accessible courtyard. Several inner rooms reachable only by historic stairs " + "with no lift alternative. Accessible WC near the visitor center.", overallScore: 3.6, }, }, { id: 3, title: "Lotus Center", city: "Oradea", coordinates: { lat: 47.0428, lng: 21.9467 }, imageUrl: "https://images.unsplash.com/photo-1519567241046-7f570eee3ce6?w=800&auto=format&fit=crop", description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lotus Center is a modern shopping mall with a " + "supermarket, food court, and cinema. Built to current commercial standards, it is one of the most " + "consistently accessible venues in the city.", ratings: { physicalAccessibility: 5, physicalNotes: "Step-free entrances, wide automatic doors, multiple lifts between all floors, accessible parking " + "spots near the entrance, and clearly signed accessible WCs on each level.", overallScore: 4.7, }, }, { id: 4, title: "Parcul 1 Decembrie", city: "Oradea", coordinates: { lat: 47.0562, lng: 21.9269 }, imageUrl: "https://images.unsplash.com/photo-1500313830540-7b6650a74fd0?w=800&auto=format&fit=crop", description: "Lorem ipsum dolor sit amet. A central public park along the Crișul Repede river, a popular gathering " + "spot in the warmer months. Paved paths run through most of it, with shaded benches at regular intervals.", ratings: { physicalAccessibility: 4, physicalNotes: "Wide paved walkways throughout. A few benches lack flat approach areas. Some side paths are gravel " + "and may be difficult after rain. Accessible WCs only at nearby cafés.", overallScore: 4.0, }, }, { id: 5, title: "Bazilica Romano-Catolică", city: "Oradea", coordinates: { lat: 47.0648, lng: 21.9189 }, imageUrl: "https://images.unsplash.com/photo-1548276145-69a9521f0499?w=800&auto=format&fit=crop", description: "Lorem ipsum dolor sit amet. The Roman Catholic Basilica is the largest baroque cathedral in Romania. " + "The grand entrance steps are the main accessibility hurdle, though a side ramp provides an alternative " + "route into the nave.", ratings: { physicalAccessibility: 2, physicalNotes: "Main entrance has a long set of stairs. Side entrance with a temporary ramp available on request. " + "Interior pews are fixed but the central aisle is wide enough for wheelchairs. No accessible WC on site.", overallScore: 3.0, }, }, { id: 6, title: "Muzeul Țării Crișurilor", city: "Oradea", coordinates: { lat: 47.0494, lng: 21.9361 }, imageUrl: "https://images.unsplash.com/photo-1543968996-ee822b8176ba?w=800&auto=format&fit=crop", description: "Lorem ipsum dolor sit amet. The Criș Land Museum, housed in the former Roman Catholic Bishop's Palace, " + "covers natural history, archaeology, and ethnography. Recent renovations added a lift to the upper floors.", ratings: { physicalAccessibility: 4, physicalNotes: "Step-free side entrance with intercom. Lift to all public floors. Some narrow doorways between exhibit " + "rooms (~80 cm) may be tight for larger wheelchairs.", overallScore: 4.2, }, }, // ----------------------------- CLUJ-NAPOCA ------------------------------- { id: 7, title: "Iulius Mall Cluj", city: "Cluj-Napoca", coordinates: { lat: 46.7700, lng: 23.6236 }, imageUrl: "https://images.unsplash.com/photo-1555529669-e69e7aa0ba9a?w=800&auto=format&fit=crop", description: "Lorem ipsum dolor sit amet. A large modern shopping mall in Cluj-Napoca with a wide range of shops, " + "a hypermarket, and a multiplex cinema. Built to current standards.", ratings: { physicalAccessibility: 5, physicalNotes: "Step-free throughout, wide automatic doors at every entrance, lifts and travelators between levels, " + "ample accessible parking, and accessible WCs on every floor.", overallScore: 4.8, }, }, // ----------------------------- BUCHAREST --------------------------------- { id: 8, title: "Palatul Parlamentului", city: "Bucharest", coordinates: { lat: 44.4275, lng: 26.0875 }, imageUrl: "https://images.unsplash.com/photo-1601581875309-fafbf2d3ed3a?w=800&auto=format&fit=crop", description: "Lorem ipsum dolor sit amet. The Palace of the Parliament is the second-largest administrative building " + "in the world. Guided tours are available; the building is partially accessible with prior arrangement.", ratings: { physicalAccessibility: 3, physicalNotes: "Accessible entrance available with advance booking. Lifts cover most public tour areas, but a few " + "rooms on the standard tour route are reachable only by stairs. Accessible WC at the visitor center.", overallScore: 3.4, }, }, ]; /* ============================================================================ 2) CONFIG ---------------------------------------------------------------------------- Tile provider: We use CARTO Voyager (https://carto.com/basemaps/) instead of the OSM volunteer servers because OSM's tile usage policy now requires an HTTP Referer header, which is NOT sent when the page is opened directly via file:// — that produces a "blocked / referer required" error. CARTO is free for public use, returns CORS-friendly tiles, and looks cleaner. Other drop-in alternatives if CARTO is ever down: • CARTO Positron (lighter): https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png • Esri World Street: https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x} ============================================================================ */ const CONFIG = { initialView: { center: [47.0722, 21.9210], // Oradea, Romania zoom: 13, }, tileLayer: { // CARTO Voyager — free basemap, no API key, no referer requirement. url: "https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png", options: { maxZoom: 20, subdomains: "abcd", attribution: '© OpenStreetMap contributors ‘ + ‘© CARTO‘, }, }, // Padding (px) to leave around the marker bounds when fitting the map. fitPadding: [80, 80], }; /* ============================================================================ 3) STATE ============================================================================ */ let map = null; let markersById = new Map(); // id -> Leaflet marker let activeCity = “all”; // Cached DOM references (populated in cacheDom()). const dom = { hamburgerBtn: null, drawer: null, drawerClose: null, brandPillCity: null, backdrop: null, sidePanel: null, sidePanelClose: null, cardImage: null, cardCityBadge: null, cardTitle: null, cardOverallCircle: null, cardOverallScore: null, cardDescription: null, cardPhysicalStars: null, cardPhysicalNotes: null, cardCoords: null, filterButtons: null, }; /* ============================================================================ 4) MAP INIT ============================================================================ */ function initMap() { map = L.map(“map”, { center: CONFIG.initialView.center, zoom: CONFIG.initialView.zoom, zoomControl: true, scrollWheelZoom: true, /* Disable the default attribution prefix (“Leaflet”) to keep the corner text minimal — pure aesthetic call, remove this line if you want it. */ attributionControl: true, }); map.attributionControl.setPrefix(false); L.tileLayer(CONFIG.tileLayer.url, CONFIG.tileLayer.options).addTo(map); } /* ============================================================================ 5) MARKERS ============================================================================ */ /** Map an overallScore (0..5) to a marker color variant. */ function scoreToVariantClass(score) { if (score >= 4) return “is-good”; if (score >= 2.5) return “is-ok”; return “is-poor”; } /** Build a Leaflet divIcon HTML snippet for a single location. */ function buildIcon(location) { const variant = scoreToVariantClass(location.ratings.overallScore); const html = `
${location.ratings.overallScore.toFixed(1)}
`; return L.divIcon({ html, className: “”, iconSize: [36, 36], iconAnchor: [18, 36], popupAnchor: [0, -36], }); } /** Add markers for the currently-filtered locations and reframe the map. */ function renderMarkers() { // Remove existing markers markersById.forEach((m) => m.remove()); markersById.clear(); const visible = LOCATIONS.filter( (loc) => activeCity === “all” || loc.city === activeCity ); visible.forEach((loc) => { const marker = L.marker( [loc.coordinates.lat, loc.coordinates.lng], { icon: buildIcon(loc), title: loc.title } ); marker.on(“click”, () => openLocationCard(loc)); marker.addTo(map); markersById.set(loc.id, marker); }); fitMapToVisible(visible); } /** Reframe the map to fit the visible markers. */ function fitMapToVisible(visible) { if (visible.length === 0) { map.setView(CONFIG.initialView.center, CONFIG.initialView.zoom); return; } if (visible.length === 1) { const only = visible[0]; map.setView([only.coordinates.lat, only.coordinates.lng], 15); return; } const bounds = L.latLngBounds( visible.map((loc) => [loc.coordinates.lat, loc.coordinates.lng]) ); map.fitBounds(bounds, { padding: CONFIG.fitPadding }); } /* ============================================================================ 6) FILTERING ============================================================================ */ function setActiveCity(city) { activeCity = city; // Toggle .is-active on the matching drawer filter dom.filterButtons.forEach((btn) => { const isMatch = btn.dataset.city === city; btn.classList.toggle(“is-active”, isMatch); }); // Update the brand pill so the user can always see the current filter const label = (() => { const btn = Array.from(dom.filterButtons).find((b) => b.dataset.city === city); return btn ? btn.querySelector(“.drawer__filter-label”).textContent : “All”; })(); dom.brandPillCity.textContent = label; renderMarkers(); closeLocationCard(); // stale details would be confusing closeDrawer(); // give the user immediate feedback on the map } /* ============================================================================ 7) DRAWER ============================================================================ */ function openDrawer() { dom.drawer.classList.add(“is-open”); dom.drawer.setAttribute(“aria-hidden”, “false”); dom.hamburgerBtn.classList.add(“is-open”); dom.hamburgerBtn.setAttribute(“aria-expanded”, “true”); dom.hamburgerBtn.setAttribute(“aria-label”, “Close menu”); dom.backdrop.classList.add(“is-open”); } function closeDrawer() { dom.drawer.classList.remove(“is-open”); dom.drawer.setAttribute(“aria-hidden”, “true”); dom.hamburgerBtn.classList.remove(“is-open”); dom.hamburgerBtn.setAttribute(“aria-expanded”, “false”); dom.hamburgerBtn.setAttribute(“aria-label”, “Open menu”); // Only hide the backdrop if the side panel isn’t also using it if (!dom.sidePanel.classList.contains(“is-open”)) { dom.backdrop.classList.remove(“is-open”); } } function toggleDrawer() { if (dom.drawer.classList.contains(“is-open”)) closeDrawer(); else openDrawer(); } /* ============================================================================ 8) SIDE PANEL (location card) ============================================================================ */ function renderStars(el, score) { const filled = Math.max(0, Math.min(5, Math.round(score))); const empty = 5 – filled; el.textContent = “★”.repeat(filled) + “☆”.repeat(empty); if (score >= 4) el.style.color = “var(–color-good)”; else if (score >= 2.5) el.style.color = “var(–color-ok)”; else el.style.color = “var(–color-poor)”; } function colorOverallRing(circleEl, score) { // Sets the inset ring color of the circular score badge. let ring; if (score >= 4) ring = “var(–color-good)”; else if (score >= 2.5) ring = “var(–color-ok)”; else ring = “var(–color-poor)”; circleEl.style.boxShadow = `inset 0 0 0 4px ${ring}, 0 4px 12px rgba(15, 23, 42, 0.07)`; } function openLocationCard(loc) { // 1) Hero image + city badge dom.cardImage.src = loc.imageUrl; dom.cardImage.alt = loc.title; dom.cardCityBadge.textContent = loc.city; // 2) Title + description dom.cardTitle.textContent = loc.title; dom.cardDescription.textContent = loc.description; // 3) Overall score dom.cardOverallScore.textContent = loc.ratings.overallScore.toFixed(1); colorOverallRing(dom.cardOverallCircle, loc.ratings.overallScore); // 4) Physical accessibility (stars + notes) renderStars(dom.cardPhysicalStars, loc.ratings.physicalAccessibility); dom.cardPhysicalNotes.textContent = loc.ratings.physicalNotes; // 5) Coordinates footer dom.cardCoords.textContent = `Coordinates: ${loc.coordinates.lat.toFixed(5)}, ${loc.coordinates.lng.toFixed(5)}`; // 6) Slide panel + backdrop in dom.sidePanel.classList.add(“is-open”); dom.sidePanel.setAttribute(“aria-hidden”, “false”); dom.backdrop.classList.add(“is-open”); // 7) Pan the map slightly so the marker isn’t hidden behind the panel if (window.innerWidth > 768) { map.panTo([loc.coordinates.lat, loc.coordinates.lng], { animate: true }); } } function closeLocationCard() { dom.sidePanel.classList.remove(“is-open”); dom.sidePanel.setAttribute(“aria-hidden”, “true”); // Only hide the backdrop if the drawer isn’t also using it if (!dom.drawer.classList.contains(“is-open”)) { dom.backdrop.classList.remove(“is-open”); } } /* ============================================================================ 9) BOOTSTRAP ============================================================================ */ function cacheDom() { dom.hamburgerBtn = document.getElementById(“hamburgerBtn”); dom.drawer = document.getElementById(“drawer”); dom.drawerClose = document.getElementById(“drawerClose”); dom.brandPillCity = document.getElementById(“brandPillCity”); dom.backdrop = document.getElementById(“backdrop”); dom.sidePanel = document.getElementById(“sidePanel”); dom.sidePanelClose = document.getElementById(“sidePanelClose”); dom.cardImage = document.getElementById(“cardImage”); dom.cardCityBadge = document.getElementById(“cardCityBadge”); dom.cardTitle = document.getElementById(“cardTitle”); dom.cardOverallCircle = document.getElementById(“cardOverallCircle”); dom.cardOverallScore = document.getElementById(“cardOverallScore”); dom.cardDescription = document.getElementById(“cardDescription”); dom.cardPhysicalStars = document.getElementById(“cardPhysicalStars”); dom.cardPhysicalNotes = document.getElementById(“cardPhysicalNotes”); dom.cardCoords = document.getElementById(“cardCoords”); dom.filterButtons = document.querySelectorAll(“.drawer__filter”); } function bindEvents() { // Hamburger toggles the drawer dom.hamburgerBtn.addEventListener(“click”, toggleDrawer); dom.drawerClose.addEventListener(“click”, closeDrawer); // Drawer filter buttons dom.filterButtons.forEach((btn) => { btn.addEventListener(“click”, () => setActiveCity(btn.dataset.city)); }); // Side panel close dom.sidePanelClose.addEventListener(“click”, closeLocationCard); // Backdrop click closes whichever overlay is open dom.backdrop.addEventListener(“click”, () => { closeDrawer(); closeLocationCard(); }); // Escape closes everything document.addEventListener(“keydown”, (e) => { if (e.key === “Escape”) { closeDrawer(); closeLocationCard(); } }); } document.addEventListener(“DOMContentLoaded”, () => { cacheDom(); initMap(); renderMarkers(); bindEvents(); }); /* ============================================================================ ACCESSIBILITY MAP — styles.css ============================================================================ Layout philosophy: • The map fills the entire viewport. • Every UI element floats on top as a glass-morphism panel. • Soft, layered shadows + subtle borders give a premium feel. Sections: 1) :root — design tokens (colors, fonts, spacing, etc.) 2) Base / reset 3) Hamburger button — animated 3-bars → X 4) Brand pill — top-center floating title 5) Drawer — left slide-in menu 6) Map + legend 7) Side panel (location card) 8) Backdrop (shared) 9) Custom Leaflet markers 10) Responsive overrides 11) Reduced motion ============================================================================ */ /* ============================================================================ 1) DESIGN TOKENS —————————————————————————- >>> CHANGE PRIMARY COLORS / FONTS HERE <<< ============================================================================ */ :root { /* --- Brand / accent --- */ --color-primary: #6366f1; /* indigo-500 — main accent */ --color-primary-dark: #4f46e5; /* indigo-600 — hover/pressed */ --color-primary-soft: #eef2ff; /* indigo-50 — subtle backgrounds */ --color-primary-tint: rgba(99, 102, 241, 0.12); /* translucent fills */ /* --- Score / status colors --- */ --color-good: #10b981; /* emerald-500 */ --color-ok: #f59e0b; /* amber-500 */ --color-poor: #ef4444; /* red-500 */ /* --- Neutrals (warm-cool blend) --- */ --color-bg: #f5f7fb; --color-surface: #ffffff; --color-surface-2: #f8fafc; --color-border: #e5e7eb; --color-border-soft: rgba(15, 23, 42, 0.06); --color-text: #0f172a; --color-text-soft: #475569; --color-text-mute: #94a3b8; /* Glass surface — semi-transparent white used for floating panels. Combined with backdrop-filter: blur(...) for the frosted look. */ --glass-bg: rgba(255, 255, 255, 0.78); --glass-bg-solid: rgba(255, 255, 255, 0.95); --glass-border: rgba(255, 255, 255, 0.6); /* --- Typography --- */ --font-display: "Plus Jakarta Sans", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; --font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; --font-size-base: 16px; /* --- Spacing --- */ --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; --space-5: 24px; --space-6: 32px; --space-7: 48px; /* --- Radii --- */ --radius-sm: 8px; --radius-md: 14px; --radius-lg: 20px; --radius-xl: 28px; --radius-pill: 999px; /* --- Shadows (soft, layered) --- */ --shadow-xs: 0 1px 2px rgba(15, 23, 42, 0.06); --shadow-sm: 0 4px 12px rgba(15, 23, 42, 0.07); --shadow-md: 0 10px 30px rgba(15, 23, 42, 0.10); --shadow-lg: 0 24px 60px rgba(15, 23, 42, 0.18); --shadow-glow: 0 12px 30px rgba(99, 102, 241, 0.30); /* --- Layout sizes --- */ --drawer-width: 320px; --side-panel-width: 440px; /* --- Motion --- */ --ease: cubic-bezier(0.4, 0, 0.2, 1); --ease-out: cubic-bezier(0.16, 1, 0.3, 1); --duration: 300ms; --duration-fast: 200ms; } /* ============================================================================ 2) BASE / RESET ============================================================================ */ *, *::before, *::after { box-sizing: border-box; } html, body { margin: 0; padding: 0; height: 100%; } body { font-family: var(--font-body); font-size: var(--font-size-base); color: var(--color-text); background-color: var(--color-bg); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; overflow: hidden; /* the map itself handles scrolling/panning */ } button { font-family: inherit; cursor: pointer; border: none; background: none; color: inherit; } img { display: block; max-width: 100%; } /* Reusable glass-panel surface */ .glass { background-color: var(--glass-bg); -webkit-backdrop-filter: saturate(180%) blur(18px); backdrop-filter: saturate(180%) blur(18px); border: 1px solid var(--glass-border); } /* ============================================================================ 3) HAMBURGER BUTTON (top-left) ============================================================================ */ .hamburger { position: fixed; top: var(--space-4); left: var(--space-4); z-index: 1200; width: 52px; height: 52px; border-radius: var(--radius-md); background-color: var(--glass-bg); -webkit-backdrop-filter: saturate(180%) blur(18px); backdrop-filter: saturate(180%) blur(18px); border: 1px solid var(--glass-border); box-shadow: var(--shadow-md); display: flex; align-items: center; justify-content: center; transition: transform var(--duration-fast) var(--ease), background-color var(--duration-fast) var(--ease), box-shadow var(--duration-fast) var(--ease); } .hamburger:hover { background-color: var(--glass-bg-solid); transform: translateY(-1px); box-shadow: var(--shadow-lg); } .hamburger:active { transform: translateY(0); } /* The 3 bars container */ .hamburger__bars { position: relative; width: 22px; height: 16px; display: block; } .hamburger__bars span { position: absolute; left: 0; width: 100%; height: 2px; border-radius: 2px; background-color: var(--color-text); transition: transform var(--duration-fast) var(--ease), opacity var(--duration-fast) var(--ease), top var(--duration-fast) var(--ease); } .hamburger__bars span:nth-child(1) { top: 0; } .hamburger__bars span:nth-child(2) { top: 7px; } .hamburger__bars span:nth-child(3) { top: 14px; } /* Animate to X when drawer is open (.is-open is toggled by JS) */ .hamburger.is-open .hamburger__bars span:nth-child(1) { top: 7px; transform: rotate(45deg); } .hamburger.is-open .hamburger__bars span:nth-child(2) { opacity: 0; } .hamburger.is-open .hamburger__bars span:nth-child(3) { top: 7px; transform: rotate(-45deg); } /* ============================================================================ 4) BRAND PILL (top-center) ============================================================================ */ .brand-pill { position: fixed; top: var(--space-4); left: 50%; transform: translateX(-50%); z-index: 1100; display: flex; align-items: center; gap: var(--space-3); padding: 10px 18px 10px 14px; border-radius: var(--radius-pill); background-color: var(--glass-bg); -webkit-backdrop-filter: saturate(180%) blur(18px); backdrop-filter: saturate(180%) blur(18px); border: 1px solid var(--glass-border); box-shadow: var(--shadow-md); pointer-events: none; /* purely informational, don't intercept clicks */ user-select: none; max-width: calc(100vw - 140px); } .brand-pill__icon { width: 22px; height: 22px; fill: var(--color-primary); flex-shrink: 0; } .brand-pill__text { display: flex; flex-direction: column; line-height: 1.15; min-width: 0; } .brand-pill__title { font-family: var(--font-display); font-weight: 700; font-size: 0.92rem; letter-spacing: -0.01em; color: var(--color-text); } .brand-pill__city { font-size: 0.74rem; font-weight: 500; color: var(--color-primary-dark); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* ============================================================================ 5) DRAWER (slides in from the left) ============================================================================ */ .drawer { position: fixed; top: 0; left: 0; height: 100vh; width: var(--drawer-width); max-width: 90vw; background-color: var(--color-surface); box-shadow: var(--shadow-lg); z-index: 1300; /* Hidden state — pushed off the left edge */ transform: translateX(-100%); transition: transform var(--duration) var(--ease-out); display: flex; flex-direction: column; overflow: hidden; } .drawer.is-open { transform: translateX(0); } /* Subtle gradient stripe at the top of the drawer for visual interest */ .drawer::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: linear-gradient(90deg, var(--color-primary) 0%, #8b5cf6 50%, #06b6d4 100%); z-index: 1; } .drawer__header { display: flex; align-items: center; justify-content: space-between; padding: var(--space-6) var(--space-5) var(--space-4); } .drawer__brand { display: flex; align-items: center; gap: var(--space-3); } .drawer__logo { width: 32px; height: 32px; fill: var(--color-primary); } .drawer__brand-text { display: flex; flex-direction: column; line-height: 1.1; } .drawer__title { font-family: var(--font-display); font-weight: 800; font-size: 1.05rem; letter-spacing: -0.01em; } .drawer__subtitle { font-size: 0.75rem; font-weight: 500; color: var(--color-text-mute); margin-top: 2px; } .drawer__close { width: 36px; height: 36px; border-radius: 50%; font-size: 1.5rem; line-height: 1; color: var(--color-text-soft); background-color: var(--color-surface-2); display: flex; align-items: center; justify-content: center; transition: background-color var(--duration-fast) var(--ease); } .drawer__close:hover { background-color: var(--color-border); color: var(--color-text); } .drawer__section-title { margin: 0; padding: var(--space-2) var(--space-5) var(--space-3); font-size: 0.72rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-text-mute); } /* Filter list */ .drawer__filters { display: flex; flex-direction: column; gap: var(--space-1); padding: 0 var(--space-3); flex: 1; overflow-y: auto; } .drawer__filter { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 14px 16px; border-radius: var(--radius-md); font-size: 0.95rem; font-weight: 500; color: var(--color-text); text-align: left; transition: background-color var(--duration-fast) var(--ease), color var(--duration-fast) var(--ease), transform var(--duration-fast) var(--ease); } .drawer__filter:hover { background-color: var(--color-primary-soft); color: var(--color-primary-dark); transform: translateX(2px); } .drawer__filter-arrow { color: var(--color-text-mute); font-size: 1.2rem; transition: transform var(--duration-fast) var(--ease); } .drawer__filter:hover .drawer__filter-arrow { color: var(--color-primary); transform: translateX(2px); } /* Active filter (toggled by JS) */ .drawer__filter.is-active { background-color: var(--color-primary); color: #fff; font-weight: 600; box-shadow: var(--shadow-glow); } .drawer__filter.is-active .drawer__filter-arrow { color: rgba(255, 255, 255, 0.9); } .drawer__footer { padding: var(--space-5); font-size: 0.78rem; color: var(--color-text-mute); border-top: 1px solid var(--color-border); line-height: 1.5; } /* ============================================================================ 6) MAP + LEGEND ============================================================================ */ .map-wrapper { position: fixed; inset: 0; /* fills the entire viewport */ z-index: 1; } #map { width: 100%; height: 100%; background-color: #e8eef5; } /* Legend — bottom-left floating glass panel */ .legend { position: fixed; left: var(--space-4); bottom: var(--space-4); z-index: 600; padding: var(--space-3) var(--space-4); border-radius: var(--radius-md); background-color: var(--glass-bg); -webkit-backdrop-filter: saturate(180%) blur(18px); backdrop-filter: saturate(180%) blur(18px); border: 1px solid var(--glass-border); box-shadow: var(--shadow-md); font-size: 0.82rem; color: var(--color-text-soft); user-select: none; } .legend__title { font-family: var(--font-display); font-weight: 700; color: var(--color-text); margin-bottom: var(--space-2); font-size: 0.85rem; } .legend__row { display: flex; align-items: center; gap: var(--space-2); padding: 2px 0; } .dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.8); } .dot--good { background-color: var(--color-good); } .dot--ok { background-color: var(--color-ok); } .dot--poor { background-color: var(--color-poor); } /* Slim down Leaflet's default attribution box so it blends in */ .leaflet-control-attribution { background-color: rgba(255, 255, 255, 0.7) !important; -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); font-size: 0.7rem !important; border-radius: 6px 0 0 0; } /* Reposition Leaflet's zoom buttons to keep them clear of the hamburger */ .leaflet-top.leaflet-left { top: 80px; left: var(--space-4); } .leaflet-control-zoom { border: none !important; box-shadow: var(--shadow-md) !important; border-radius: var(--radius-md) !important; overflow: hidden; } .leaflet-control-zoom a { background-color: var(--glass-bg) !important; -webkit-backdrop-filter: saturate(180%) blur(18px); backdrop-filter: saturate(180%) blur(18px); color: var(--color-text) !important; border-bottom: 1px solid var(--color-border-soft) !important; } .leaflet-control-zoom a:hover { background-color: var(--glass-bg-solid) !important; } /* ============================================================================ 7) SIDE PANEL (location card) ============================================================================ */ .side-panel { position: fixed; top: 0; right: 0; height: 100vh; width: var(--side-panel-width); max-width: 100%; background-color: var(--color-surface); box-shadow: var(--shadow-lg); transform: translateX(100%); transition: transform var(--duration) var(--ease-out); z-index: 1250; overflow-y: auto; display: flex; flex-direction: column; } .side-panel.is-open { transform: translateX(0); } .side-panel__close { position: absolute; top: var(--space-3); right: var(--space-3); width: 38px; height: 38px; border-radius: 50%; background-color: rgba(255, 255, 255, 0.95); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); font-size: 1.5rem; line-height: 1; color: var(--color-text); display: flex; align-items: center; justify-content: center; z-index: 2; box-shadow: var(--shadow-sm); transition: transform var(--duration-fast) var(--ease); } .side-panel__close:hover { transform: rotate(90deg); } /* Hero image */ .card__image-wrap { position: relative; width: 100%; height: 240px; background-color: var(--color-surface-2); flex-shrink: 0; overflow: hidden; } .card__image { width: 100%; height: 100%; object-fit: cover; } /* Soft gradient overlay so the city badge is always readable */ .card__image-overlay { position: absolute; inset: 0; background: linear-gradient( to bottom, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0.55) 100% ); } .card__city-badge { position: absolute; left: var(--space-4); bottom: var(--space-4); padding: 6px 14px; border-radius: var(--radius-pill); background-color: rgba(255, 255, 255, 0.95); color: var(--color-primary-dark); font-size: 0.78rem; font-weight: 700; letter-spacing: 0.02em; box-shadow: var(--shadow-sm); } /* Card body */ .card__body { padding: var(--space-5); display: flex; flex-direction: column; gap: var(--space-4); } .card__title { margin: 0; font-family: var(--font-display); font-size: 1.55rem; font-weight: 800; letter-spacing: -0.02em; line-height: 1.2; } /* Overall score block — circular badge + meta text */ .card__overall { display: flex; align-items: center; gap: var(--space-4); padding: var(--space-4); border-radius: var(--radius-lg); background: linear-gradient(135deg, var(--color-surface-2) 0%, #ffffff 100%); border: 1px solid var(--color-border); } .card__overall-circle { width: 80px; height: 80px; border-radius: 50%; background-color: #fff; display: flex; flex-direction: column; align-items: center; justify-content: center; flex-shrink: 0; /* The ring color is set inline by JS based on the score (good/ok/poor) */ box-shadow: inset 0 0 0 4px var(--color-primary), var(--shadow-sm); } .card__overall-score { font-family: var(--font-display); font-size: 1.6rem; font-weight: 800; line-height: 1; letter-spacing: -0.02em; color: var(--color-text); } .card__overall-out { font-size: 0.7rem; color: var(--color-text-mute); font-weight: 600; margin-top: 2px; } .card__overall-meta { display: flex; flex-direction: column; gap: 2px; } .card__overall-label { font-family: var(--font-display); font-size: 1rem; font-weight: 700; color: var(--color-text); } .card__overall-sub { font-size: 0.82rem; color: var(--color-text-soft); } .card__description { margin: 0; color: var(--color-text-soft); line-height: 1.6; font-size: 0.95rem; } /* Ratings */ .card__ratings { display: flex; flex-direction: column; gap: var(--space-3); padding-top: var(--space-4); border-top: 1px solid var(--color-border); } .rating-row { display: flex; flex-direction: column; gap: 6px; } .rating-row__head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); } .rating-row__label { font-family: var(--font-display); font-weight: 700; font-size: 0.95rem; } .rating-row__stars { font-size: 1.05rem; letter-spacing: 3px; color: var(--color-ok); white-space: nowrap; } .rating-row__notes { margin: 0; font-size: 0.88rem; color: var(--color-text-soft); line-height: 1.5; } .card__coords { margin-top: var(--space-2); padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm); background-color: var(--color-surface-2); font-size: 0.75rem; color: var(--color-text-mute); font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; } /* ============================================================================ 8) BACKDROP ============================================================================ */ .backdrop { position: fixed; inset: 0; background-color: rgba(15, 23, 42, 0.45); -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px); opacity: 0; pointer-events: none; transition: opacity var(--duration) var(--ease); z-index: 1150; } .backdrop.is-open { opacity: 1; pointer-events: auto; } /* ============================================================================ 9) CUSTOM LEAFLET MARKERS ---------------------------------------------------------------------------- Built with L.divIcon() in JS. The variant class (is-good / is-ok / is-poor) is set by JS based on the location's overallScore. ============================================================================ */ .access-marker { width: 36px; height: 36px; border-radius: 50% 50% 50% 0; transform: rotate(-45deg); border: 3px solid #ffffff; box-shadow: 0 4px 14px rgba(15, 23, 42, 0.25), 0 0 0 1px rgba(15, 23, 42, 0.04); display: flex; align-items: center; justify-content: center; cursor: pointer; transition: transform var(--duration-fast) var(--ease), box-shadow var(--duration-fast) var(--ease); } .access-marker:hover { transform: rotate(-45deg) scale(1.18) translate(-2px, -2px); box-shadow: 0 10px 24px rgba(15, 23, 42, 0.35); z-index: 1000; } /* The score number inside the pin (rotated upright) */ .access-marker__inner { transform: rotate(45deg); color: #ffffff; font-weight: 700; font-size: 0.78rem; font-family: var(--font-display); text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); } .access-marker.is-good { background-color: var(--color-good); } .access-marker.is-ok { background-color: var(--color-ok); } .access-marker.is-poor { background-color: var(--color-poor); } /* ============================================================================ 10) RESPONSIVE OVERRIDES ============================================================================ */ @media (max-width: 768px) { /* Hide the brand pill text on small screens — keeps the chrome minimal */ .brand-pill { padding: 8px 14px 8px 12px; } .brand-pill__title { font-size: 0.85rem; } .brand-pill__city { font-size: 0.7rem; } /* Side panel becomes full width on phones (modal-like) */ .side-panel { width: 100%; } /* Pin Leaflet zoom controls below the hamburger more tightly */ .leaflet-top.leaflet-left { top: 80px; } } @media (max-width: 480px) { .brand-pill__city { display: none; } .card__title { font-size: 1.3rem; } .card__overall-circle { width: 68px; height: 68px; } .card__overall-score { font-size: 1.35rem; } .legend { font-size: 0.75rem; padding: var(--space-2) var(--space-3); } } /* ============================================================================ 11) REDUCED MOTION ---------------------------------------------------------------------------- Honors the user's OS-level preference to minimize animations. ============================================================================ */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; } }