Proof of concept for the other one
1import * as L from "leaflet";
2import type { CalendarEvent } from "./parser";
3
4/**
5 * Read the Obsidian accent color from CSS custom properties.
6 * Falls back to the default Obsidian purple if unavailable.
7 */
8function getAccentColor(): string {
9 return getComputedStyle(document.body).getPropertyValue("--interactive-accent").trim() || "#7f6df2";
10}
11
12/**
13 * Darken a hex color by a given factor (0–1). Used for the pin stroke.
14 */
15function darkenHex(hex: string, factor: number): string {
16 const h = hex.replace("#", "");
17 const r = Math.max(0, Math.round(parseInt(h.slice(0, 2), 16) * (1 - factor)));
18 const g = Math.max(0, Math.round(parseInt(h.slice(2, 4), 16) * (1 - factor)));
19 const b = Math.max(0, Math.round(parseInt(h.slice(4, 6), 16) * (1 - factor)));
20 return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
21}
22
23/**
24 * Create an SVG teardrop pin icon for map markers.
25 * Uses the Obsidian accent color, or a muted grey for sold-out events.
26 */
27function createPinIcon(soldOut: boolean): L.DivIcon {
28 const accent = getAccentColor();
29 const fill = soldOut ? "#999" : accent;
30 const stroke = soldOut ? "#777" : darkenHex(accent, 0.25);
31 const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="40" viewBox="0 0 28 40">` +
32 `<path d="M14 0C6.3 0 0 6.3 0 14c0 10.5 14 26 14 26s14-15.5 14-26C28 6.3 21.7 0 14 0z" ` +
33 `fill="${fill}" stroke="${stroke}" stroke-width="1.5"/>` +
34 `<circle cx="14" cy="14" r="5" fill="white" opacity="0.85"/>` +
35 `</svg>`;
36 return L.divIcon({
37 html: svg,
38 className: "cal-pin-icon",
39 iconSize: [28, 40],
40 iconAnchor: [14, 40],
41 popupAnchor: [0, -36],
42 });
43}
44
45export interface MapCallbacks {
46 onMarkerClick: (event: CalendarEvent) => void;
47}
48
49export interface MapController {
50 /** Update markers to reflect current events. Pass fitBounds=true to auto-zoom. */
51 updateMarkers(events: CalendarEvent[], fitBounds?: boolean): void;
52 /** Select an event: pan to its marker, open popup, highlight it. */
53 selectEvent(event: CalendarEvent | null): void;
54 /** Auto-fit map bounds to all current markers. */
55 fitBounds(): void;
56 /** Call when container is resized. */
57 invalidateSize(): void;
58 /** Clean up the Leaflet instance. */
59 destroy(): void;
60}
61
62// Stamen Watercolor tiles hosted by the Smithsonian / Cooper Hewitt (free, no API key)
63const WATERCOLOR_URL = "https://watercolormaps.collection.cooperhewitt.org/tile/watercolor/{z}/{x}/{y}.jpg";
64// CartoDB light labels overlay (free, no API key)
65const LABELS_URL = "https://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}{r}.png";
66const TILE_ATTRIBUTION =
67 'Map tiles by <a href="https://stamen.com/">Stamen Design</a>, hosted by <a href="https://collection.cooperhewitt.org/">Cooper Hewitt</a>. ' +
68 'Labels by <a href="https://carto.com/">CARTO</a>. ' +
69 'Data © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>';
70
71/**
72 * Format a popup's HTML content for a list of events at the same location.
73 */
74function popupContent(events: CalendarEvent[]): string {
75 return events
76 .map((ev) => {
77 const title = ev.url
78 ? `<a href="${ev.url}" target="_blank">${escapeHtml(ev.title)}</a>`
79 : escapeHtml(ev.title);
80 const soldOut = ev.soldOut ? ' <span class="cal-sold-out">(Sold out)</span>' : "";
81 const date = ev.date.toLocaleDateString("en-US", {
82 weekday: "short",
83 month: "short",
84 day: "numeric",
85 });
86 const time = ev.rawTime ? `, ${escapeHtml(ev.rawTime)}` : "";
87 const venue = ev.venue ? `<br><span class="cal-popup-venue">${escapeHtml(ev.venue)}</span>` : "";
88 return `<div class="cal-popup-event"><strong>${title}</strong>${soldOut}<br>${date}${time}${venue}</div>`;
89 })
90 .join("");
91}
92
93function escapeHtml(s: string): string {
94 return s
95 .replace(/&/g, "&")
96 .replace(/</g, "<")
97 .replace(/>/g, ">")
98 .replace(/"/g, """);
99}
100
101/**
102 * Group events by their lat/lng coordinates (rounded to avoid floating point issues).
103 */
104function groupByLocation(events: CalendarEvent[]): Map<string, CalendarEvent[]> {
105 const groups = new Map<string, CalendarEvent[]>();
106 for (const ev of events) {
107 if (ev.lat === undefined || ev.lng === undefined) continue;
108 // Round to 6 decimal places for grouping
109 const key = `${ev.lat.toFixed(6)},${ev.lng.toFixed(6)}`;
110 if (!groups.has(key)) groups.set(key, []);
111 groups.get(key)!.push(ev);
112 }
113 return groups;
114}
115
116/**
117 * Create a Leaflet map in the given container and return a MapController.
118 */
119export function createMap(container: HTMLElement, events: CalendarEvent[], callbacks: MapCallbacks): MapController {
120
121 // Inject Leaflet CSS if not already present
122 if (!document.getElementById("leaflet-css")) {
123 const link = document.createElement("style");
124 link.id = "leaflet-css";
125 link.textContent = leafletCss();
126 document.head.appendChild(link);
127 }
128
129 const mapDiv = container.createDiv({ cls: "cal-map" });
130
131 const map = L.map(mapDiv, {
132 zoomControl: true,
133 attributionControl: true,
134 });
135
136 // Watercolor base layer
137 L.tileLayer(WATERCOLOR_URL, {
138 attribution: TILE_ATTRIBUTION,
139 maxZoom: 19,
140 }).addTo(map);
141
142 // Labels overlay on top of watercolor
143 L.tileLayer(LABELS_URL, {
144 maxZoom: 19,
145 subdomains: "abcd",
146 pane: "overlayPane",
147 }).addTo(map);
148
149 // Track markers and their associated events
150 const markerLayer = L.layerGroup().addTo(map);
151 let markerMap = new Map<string, { marker: L.Marker; events: CalendarEvent[] }>();
152 let selectedMarker: L.Marker | null = null;
153 let highlightCircle: L.CircleMarker | null = null;
154
155 function clearHighlight(): void {
156 if (highlightCircle) {
157 highlightCircle.remove();
158 highlightCircle = null;
159 }
160 selectedMarker = null;
161 }
162
163 function highlightMarker(marker: L.Marker): void {
164 clearHighlight();
165 selectedMarker = marker;
166 const latlng = marker.getLatLng();
167 highlightCircle = L.circleMarker(latlng, {
168 radius: 18,
169 color: "var(--interactive-accent, #7b6cd9)",
170 fillColor: "var(--interactive-accent, #7b6cd9)",
171 fillOpacity: 0.2,
172 weight: 2,
173 className: "cal-marker-highlight",
174 }).addTo(map);
175 }
176
177 function buildMarkers(evts: CalendarEvent[]): void {
178 markerLayer.clearLayers();
179 markerMap.clear();
180 clearHighlight();
181
182 const groups = groupByLocation(evts);
183
184 for (const [key, groupEvents] of groups) {
185 const [lat, lng] = key.split(",").map(Number);
186
187 const allSoldOut = groupEvents.every((e) => e.soldOut);
188
189 const marker = L.marker([lat, lng], {
190 icon: createPinIcon(allSoldOut),
191 });
192
193 marker.bindPopup(popupContent(groupEvents), {
194 maxWidth: 240,
195 className: "cal-map-popup",
196 });
197
198 marker.on("click", () => {
199 highlightMarker(marker);
200 // Fire callback for the first event in the group
201 callbacks.onMarkerClick(groupEvents[0]);
202 });
203
204 marker.addTo(markerLayer);
205 markerMap.set(key, { marker, events: groupEvents });
206 }
207 }
208
209 buildMarkers(events);
210
211 function doFitBounds(): void {
212 const allMarkers = Array.from(markerMap.values()).map((m) => m.marker);
213 if (allMarkers.length === 0) {
214 // Default to a world view
215 map.setView([20, 0], 2);
216 return;
217 }
218 if (allMarkers.length === 1) {
219 map.setView(allMarkers[0].getLatLng(), 13);
220 return;
221 }
222 const group = L.featureGroup(allMarkers);
223 map.fitBounds(group.getBounds().pad(0.15));
224 }
225
226 // Initial fit after a tick (let the container settle)
227 setTimeout(() => {
228 map.invalidateSize();
229 doFitBounds();
230 }, 50);
231
232 const controller: MapController = {
233 updateMarkers(evts: CalendarEvent[], fit = true): void {
234 buildMarkers(evts);
235 if (fit) {
236 doFitBounds();
237 }
238 },
239
240 selectEvent(event: CalendarEvent | null): void {
241 if (!event) {
242 clearHighlight();
243 map.closePopup();
244 return;
245 }
246 if (event.lat === undefined || event.lng === undefined) return;
247
248 const key = `${event.lat.toFixed(6)},${event.lng.toFixed(6)}`;
249 const entry = markerMap.get(key);
250 if (entry) {
251 highlightMarker(entry.marker);
252 entry.marker.openPopup();
253 map.panTo(entry.marker.getLatLng(), { animate: true });
254 }
255 },
256
257 fitBounds(): void {
258 doFitBounds();
259 },
260
261 invalidateSize(): void {
262 map.invalidateSize();
263 },
264
265 destroy(): void {
266 map.remove();
267 },
268 };
269
270 return controller;
271}
272
273/**
274 * Inline Leaflet CSS — we inject this rather than importing a CSS file
275 * so esbuild doesn't need a CSS loader and the plugin stays self-contained.
276 *
277 * This is the minified core Leaflet 1.9.4 CSS.
278 */
279function leafletCss(): string {
280 // We'll load from the installed leaflet package at runtime via a simpler approach:
281 // Import the CSS content as a string. Since esbuild can't handle CSS imports
282 // from node_modules without a loader, we inline the essential Leaflet styles.
283 return `
284/* Leaflet 1.9.4 — essential styles */
285.leaflet-pane, .leaflet-tile, .leaflet-marker-icon, .leaflet-marker-shadow,
286.leaflet-tile-container, .leaflet-pane > svg, .leaflet-pane > canvas,
287.leaflet-zoom-box, .leaflet-image-layer, .leaflet-layer {
288 position: absolute; left: 0; top: 0;
289}
290.leaflet-container { overflow: hidden; }
291.leaflet-tile, .leaflet-marker-icon, .leaflet-marker-shadow { user-select: none; -webkit-user-select: none; }
292.leaflet-tile::selection { background: transparent; }
293.leaflet-safari .leaflet-tile { image-rendering: -webkit-optimize-contrast; }
294.leaflet-safari .leaflet-tile-container { width: 1600px; height: 1600px; -webkit-transform-origin: 0 0; }
295.leaflet-marker-icon, .leaflet-marker-shadow { display: block; }
296.leaflet-container .leaflet-overlay-pane svg { max-width: none !important; max-height: none !important; }
297.leaflet-container .leaflet-marker-pane img,
298.leaflet-container .leaflet-shadow-pane img,
299.leaflet-container .leaflet-tile-pane img,
300.leaflet-container img.leaflet-image-layer,
301.leaflet-container .leaflet-tile { max-width: none !important; max-height: none !important; width: auto; padding: 0; }
302.leaflet-container.leaflet-touch-zoom { touch-action: pan-x pan-y; }
303.leaflet-container.leaflet-touch-drag { touch-action: none; touch-action: pinch-zoom; }
304.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { touch-action: none; }
305.leaflet-container { -webkit-tap-highlight-color: transparent; }
306.leaflet-container a { -webkit-tap-highlight-color: rgba(51,181,229,0.4); }
307.leaflet-tile { filter: inherit; visibility: hidden; }
308.leaflet-tile-loaded { visibility: inherit; }
309.leaflet-zoom-box { width: 0; height: 0; box-sizing: border-box; z-index: 800; }
310.leaflet-overlay-pane svg { -moz-user-select: none; }
311.leaflet-pane { z-index: 400; }
312.leaflet-tile-pane { z-index: 200; }
313.leaflet-overlay-pane { z-index: 400; }
314.leaflet-shadow-pane { z-index: 500; }
315.leaflet-marker-pane { z-index: 600; }
316.leaflet-tooltip-pane { z-index: 650; }
317.leaflet-popup-pane { z-index: 700; }
318.leaflet-map-pane canvas { z-index: 100; }
319.leaflet-map-pane svg { z-index: 200; }
320.leaflet-vml-shape { width: 1px; height: 1px; }
321.lvml { behavior: url(#default#VML); display: inline-block; position: absolute; }
322.leaflet-control { position: relative; z-index: 800; pointer-events: visiblePainted; pointer-events: auto; }
323.leaflet-top, .leaflet-bottom { position: absolute; z-index: 1000; pointer-events: none; }
324.leaflet-top { top: 0; }
325.leaflet-right { right: 0; }
326.leaflet-bottom { bottom: 0; }
327.leaflet-left { left: 0; }
328.leaflet-control { float: left; clear: both; }
329.leaflet-right .leaflet-control { float: right; }
330.leaflet-top .leaflet-control { margin-top: 10px; }
331.leaflet-bottom .leaflet-control { margin-bottom: 10px; }
332.leaflet-left .leaflet-control { margin-left: 10px; }
333.leaflet-right .leaflet-control { margin-right: 10px; }
334.leaflet-fade-anim .leaflet-popup { opacity: 1; transition: opacity 0.2s linear; }
335.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { opacity: 1; }
336.leaflet-zoom-animated { transform-origin: 0 0; }
337.leaflet-zoom-anim .leaflet-zoom-animated { will-change: transform; transition: transform 0.25s cubic-bezier(0,0,0.25,1); }
338.leaflet-zoom-anim .leaflet-tile, .leaflet-pan-anim .leaflet-tile { transition: none; }
339.leaflet-zoom-anim .leaflet-zoom-hide { visibility: hidden; }
340.leaflet-interactive { cursor: pointer; }
341.leaflet-grab { cursor: grab; }
342.leaflet-crosshair, .leaflet-crosshair .leaflet-interactive { cursor: crosshair; }
343.leaflet-popup-pane, .leaflet-control { cursor: auto; }
344.leaflet-dragging .leaflet-grab, .leaflet-dragging .leaflet-grab .leaflet-interactive,
345.leaflet-dragging .leaflet-marker-draggable { cursor: move; cursor: grabbing; }
346.leaflet-marker-icon, .leaflet-marker-shadow, .leaflet-image-layer,
347.leaflet-pane > svg path, .leaflet-tile-container { pointer-events: none; }
348.leaflet-marker-icon.leaflet-interactive, .leaflet-image-layer.leaflet-interactive,
349.leaflet-pane > svg path.leaflet-interactive, svg.leaflet-image-layer.leaflet-interactive path { pointer-events: visiblePainted; pointer-events: auto; }
350.leaflet-container a.leaflet-active { outline: 2px solid orange; }
351.leaflet-zoom-box { border: 2px dotted #38f; background: rgba(255,255,255,0.5); }
352.leaflet-bar { box-shadow: 0 1px 5px rgba(0,0,0,0.65); border-radius: 4px; }
353.leaflet-bar a, .leaflet-bar a:hover { background-color: #fff; border-bottom: 1px solid #ccc; width: 26px; height: 26px; line-height: 26px; display: block; text-align: center; text-decoration: none; color: black; }
354.leaflet-bar a, .leaflet-control-layers-toggle { background-position: 50% 50%; background-repeat: no-repeat; display: block; }
355.leaflet-bar a:hover, .leaflet-bar a:focus { background-color: #f4f4f4; }
356.leaflet-bar a:first-child { border-top-left-radius: 4px; border-top-right-radius: 4px; }
357.leaflet-bar a:last-child { border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; border-bottom: none; }
358.leaflet-bar a.leaflet-disabled { cursor: default; background-color: #f4f4f4; color: #bbb; }
359.leaflet-touch .leaflet-bar a { width: 30px; height: 30px; line-height: 30px; }
360.leaflet-touch .leaflet-bar a:first-child { border-top-left-radius: 2px; border-top-right-radius: 2px; }
361.leaflet-touch .leaflet-bar a:last-child { border-bottom-left-radius: 2px; border-bottom-right-radius: 2px; }
362.leaflet-control-zoom-in, .leaflet-control-zoom-out { font: bold 18px 'Lucida Console', Monaco, monospace; text-indent: 1px; }
363.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { font-size: 22px; }
364.leaflet-control-layers { box-shadow: 0 1px 5px rgba(0,0,0,0.4); background: #fff; border-radius: 5px; }
365.leaflet-control-layers-toggle { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAQAAAADQ4RFAAACf0lEQVR4AY1TA5AVURDsmc22bdu2bdu2bdu2bdu2bZ3v3P3f1t2q7mL6AQBSNG03EMHQF8MwEiNxBmfxMz7FJ/iROI0zOINfCF2RC9lRG8MxFRdwSw0EcBu3cBEXcRt31ECq4AFMI3RFLVRHZ3RBf0zAHJzBDdzEHdzFfTzAQzzCYzzBUzzDc7zASwjBV3iDb/AO3+MHkiSf8dPf4mt8ha/xJb7AF/gcn+FTfIJP8DE+wof4AO/jPbyLd/A23sKbeAOv43W8hlfxCl7Gy3gJL+IFPIdncQ+P8RRP8ARP8BSvwGOAkBm50AsrsR+38BCv4BLO4DTu4h6e4SVe4CXe4CU+wPt4D+/iHbyNt/Am3sDreA2v4hW8jJfwIl7Ac3gWz+AuHuMpnuAJnuI5eMQjP4cLauA+buE6LuMCzuE0TuE8ruEObuMhHuEWHuABnuExnuIRnuAJnuEZ+FfFa3gVr+BlvISX8CJewPN4Ds/iGdzFYzzFEzzBUzzHSwjBV/gG3+I7fI8fCKLkM37+W3yNr/A1vsSX+AKf4zN8ik/wMT7Ch/gA7+M9vIt38DbewsN4C2/iDbyO1/EaXsUreBkv4SW8iBfwPJ7DU7iHx3iKx3iCJ3iGF+CRH+Ey7uEOLuMMzuMiruEGbuI2buMO7uE+HuIhHuMpHuMJnuI5XoJHvIJX8Rpexat4Ba/gZbyEl/ACnsdzeBbP4C4e4yme4Ame4hlewZ8+DJmRC30xAbtxDQ/xGu7gNE7hMu7gLu7jAR7iMZ7gKR7jCZ7hOV6AAG/hTbyBt/AWHsJbeAsP4x28i3fwNt7CG3gdr+E1vIpX8DJewot4Ac/hWTyNu3iEx3j67w8P/z9FWIJUIAAAAABJRU5ErkJggg==); width: 36px; height: 36px; }
366.leaflet-retina .leaflet-control-layers-toggle { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0CAQAAABvcdNgAAAEsklEQVR4AWL4TydIhpZK1kAAAABJRU5ErkJggg==); background-size: 26px 26px; }
367.leaflet-touch .leaflet-control-layers-toggle { width: 44px; height: 44px; }
368.leaflet-control-layers .leaflet-control-layers-list, .leaflet-control-layers-expanded .leaflet-control-layers-toggle { display: none; }
369.leaflet-control-layers-expanded .leaflet-control-layers-list { display: block; position: relative; }
370.leaflet-control-layers-expanded { padding: 6px 10px 6px 6px; color: #333; background: #fff; }
371.leaflet-control-layers-scrollbar { overflow-y: scroll; overflow-x: hidden; padding-right: 5px; }
372.leaflet-control-layers-selector { margin-top: 2px; position: relative; top: 1px; }
373.leaflet-control-layers label { display: block; font-size: 13px; font-size: 1.08333em; }
374.leaflet-control-layers-separator { height: 0; border-top: 1px solid #ddd; margin: 5px -10px 5px -6px; }
375.leaflet-default-icon-path { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAFgUlEQVR4Aa1XA5BjWRTN2oW17d3YassW17LFNesR17LWtAnOez/J5tsupervised/cLOGeli2lGZjUtMPail370a57y6u6deuj75NAfO/6fOM0M0SUR4JYAAAAABJRU5ErkJggg==); }
376.leaflet-container .leaflet-control-attribution { background: #fff; background: rgba(255,255,255,0.8); margin: 0; }
377.leaflet-control-attribution, .leaflet-control-scale-line { padding: 0 5px; color: #333; line-height: 1.4; }
378.leaflet-control-attribution a { text-decoration: none; }
379.leaflet-control-attribution a:hover, .leaflet-control-attribution a:focus { text-decoration: underline; }
380.leaflet-attribution-flag { display: none !important; }
381.leaflet-left .leaflet-control-scale { margin-left: 5px; }
382.leaflet-bottom .leaflet-control-scale { margin-bottom: 5px; }
383.leaflet-control-scale-line { border: 2px solid #777; border-top: none; line-height: 1.1; padding: 2px 5px 1px; white-space: nowrap; overflow: hidden; box-sizing: border-box; background: rgba(255,255,255,0.5); }
384.leaflet-control-scale-line:not(:first-child) { border-top: 2px solid #777; border-bottom: none; margin-top: -2px; }
385.leaflet-control-scale-line:not(:first-child):not(:last-child) { border-bottom: 2px solid #777; }
386.leaflet-touch .leaflet-control-attribution, .leaflet-touch .leaflet-control-layers,
387.leaflet-touch .leaflet-bar { box-shadow: none; }
388.leaflet-touch .leaflet-control-layers, .leaflet-touch .leaflet-bar { border: 2px solid rgba(0,0,0,0.2); background-clip: padding-box; }
389.leaflet-popup { position: absolute; text-align: center; margin-bottom: 20px; }
390.leaflet-popup-content-wrapper { padding: 1px; text-align: left; border-radius: 12px; }
391.leaflet-popup-content { margin: 13px 24px 13px 20px; line-height: 1.3; font-size: 13px; font-size: 1.08333em; min-height: 1px; }
392.leaflet-popup-content p { margin: 17px 0; margin: 1.3em 0; }
393.leaflet-popup-tip-container { width: 40px; height: 20px; position: absolute; left: 50%; margin-top: -1px; margin-left: -20px; overflow: hidden; pointer-events: none; }
394.leaflet-popup-tip { width: 17px; height: 17px; padding: 1px; margin: -10px auto 0; pointer-events: auto; transform: rotate(45deg); }
395.leaflet-popup-content-wrapper, .leaflet-popup-tip { background: white; color: #333; box-shadow: 0 3px 14px rgba(0,0,0,0.4); }
396.leaflet-container a.leaflet-popup-close-button { position: absolute; top: 0; right: 0; border: none; text-align: center; width: 24px; height: 24px; font: 16px/24px Tahoma, Verdana, sans-serif; color: #757575; text-decoration: none; background: transparent; }
397.leaflet-container a.leaflet-popup-close-button:hover, .leaflet-container a.leaflet-popup-close-button:focus { color: #585858; }
398.leaflet-popup-scrolled { overflow: auto; }
399.leaflet-oldie .leaflet-popup-content-wrapper { -ms-zoom: 1; }
400.leaflet-oldie .leaflet-popup-tip { width: 24px; margin: 0 auto; -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); }
401.leaflet-oldie .leaflet-control-zoom, .leaflet-oldie .leaflet-control-layers,
402.leaflet-oldie .leaflet-popup-content-wrapper, .leaflet-oldie .leaflet-popup-tip { border: 1px solid #999; }
403.leaflet-div-icon { background: #fff; border: 1px solid #666; }
404.leaflet-tooltip { position: absolute; padding: 6px; background-color: #fff; border: 1px solid #fff; border-radius: 3px; color: #222; white-space: nowrap; user-select: none; pointer-events: none; box-shadow: 0 1px 3px rgba(0,0,0,0.4); }
405.leaflet-tooltip.leaflet-interactive { cursor: pointer; pointer-events: auto; }
406.leaflet-tooltip-top:before, .leaflet-tooltip-bottom:before,
407.leaflet-tooltip-left:before, .leaflet-tooltip-right:before { position: absolute; pointer-events: none; border: 6px solid transparent; background: transparent; content: ""; }
408.leaflet-tooltip-bottom { margin-top: 6px; }
409.leaflet-tooltip-top { margin-top: -6px; }
410.leaflet-tooltip-bottom:before, .leaflet-tooltip-top:before { left: 50%; margin-left: -6px; }
411.leaflet-tooltip-top:before { bottom: 0; margin-bottom: -12px; border-top-color: #fff; }
412.leaflet-tooltip-bottom:before { top: 0; margin-top: -12px; margin-left: -6px; border-bottom-color: #fff; }
413.leaflet-tooltip-left { margin-left: -6px; }
414.leaflet-tooltip-right { margin-left: 6px; }
415.leaflet-tooltip-left:before, .leaflet-tooltip-right:before { top: 50%; margin-top: -6px; }
416.leaflet-tooltip-left:before { right: 0; margin-right: -12px; border-left-color: #fff; }
417.leaflet-tooltip-right:before { left: 0; margin-left: -12px; border-right-color: #fff; }
418@media print { .leaflet-control { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }
419`;
420}