PageSourceSearch

https://grassgigs.com/venue/floyd-country-store

html grassgigs.com collected 2026-10-02 00:29:49 UTC 92,098 bytes, 1,436 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>Venue - GrassGigs</title>
7    <meta name="description" content="View upcoming bluegrass and grassroots music events at this venue">
8    <meta name="robots" content="noindex, follow" id="robots-meta">
9    
10    <!-- Canonical URL - will be set dynamically -->
11    <link rel="canonical" href="https://grassgigs.com/venue/" id="canonical-url">
12    
13    <!-- Icons -->
14    <link rel="icon" type="image/png" href="pwa-base-bg-transparent-192.png">
15    
16    <!-- Fonts -->
17    <link rel="preconnect" href="https://fonts.googleapis.com">
18    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
19    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Lora:wght@500;600;700&display=swap" media="print" onload="this.media='all'">
20    <noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Lora:wght@500;600;700&display=swap"></noscript>
21    
22    <!-- Leaflet CSS is loaded lazily via ensureLeaflet() when a map is needed -->
23    
24    <!-- Main Styles -->
25    <link rel="stylesheet" href="/style.css">
26</head>
27<body>
28    <!-- Background art (loaded from shared file) -->
29    <div id="bg-art-placeholder"></div>
30    
30<script src="/bg-art-loader.js"></script>
30
31
32    <!-- Shared Header -->
33    <div id="header-placeholder"></div>
34    
34<script src="/header-loader.js"></script>
34
35    
36    <main class="how-it-works-page">
37        <!-- Venue Details Section -->
38        <section style="text-align: center; padding: 2rem 2rem 1rem 2rem;">
39            <h2 style="font-size: 1.5rem; color: #cbd5e1; margin-bottom: 2rem;">Venue Details</h2>
40        </section>
41        
42        <!-- Hero Section with Venue Info -->
43        <section class="hero-section-hiw">
44            <h1 id="venue-name"><span class="location-pulse"></span> Loading...</h1>
45            <p class="subtitle">
46                <span id="venue-city-state"></span>
47            </p>
48            <p class="subtitle" id="venue-address" style="font-size: 1rem; margin-top: 0.5rem;"></p>
49            <p class="subtitle" id="venue-festival-link" style="display: none;"></p>
50            
51            <!-- Venue Profile Image -->
52            <div id="venue-profile-image-section" style="display: none; margin-top: 1.5rem;">
53                <img id="venue-profile-image" src="" alt="Venue" style="max-width: 600px; width: 100%; height: auto; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); border: 2px solid rgba(5, 150, 105, 0.3); cursor: pointer;" onclick="openLightbox(this.src)">
54            </div>
55            
56            <!-- Venue Stats -->
57            <div id="venue-stats" style="display: none; gap: 2rem; justify-content: center; margin-top: 1.5rem; flex-wrap: wrap;">
58                <div style="text-align: center;">
59                    <div id="stat-upcoming" style="font-size: 1.5rem; font-weight: 700; color: #10b981;">-</div>
60                    <div style="font-size: 0.85rem; color: #94a3b8;">Upcoming Shows</div>
61                </div>
62                <div style="text-align: center;">
63                    <div id="stat-bands" style="font-size: 1.5rem; font-weight: 700; color: #10b981;">-</div>
64                    <div style="font-size: 0.85rem; color: #94a3b8;">Artists</div>
65                </div>
66            </div>
67            
68            <!-- Venue Description -->
69            <div id="venue-description-section" style="display: none; margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid rgba(5, 150, 105, 0.2);">
70                <p id="venue-description" style="color: #cbd5e1; line-height: 1.6; max-width: 600px; margin: 0 auto;"></p>
71            </div>
72            
73            <!-- Venue Gallery Images -->
74            <div id="venue-gallery-section" style="display: none; margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid rgba(5, 150, 105, 0.2);">
75                <h3 style="color: #10b981; margin-bottom: 1rem; font-size: 1.2rem;">Gallery</h3>
76                <div id="venue-gallery" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; max-width: 800px; margin: 0 auto;"></div>
77            </div>
78            
79            <!-- Contact & Social Links -->
80            <div id="venue-links-section" style="display: none; margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid rgba(5, 150, 105, 0.2);">
81                <div style="display: flex; flex-wrap: wrap; gap: 1rem; justify-content: center; align-items: center;">
82                    <a id="venue-website" href="#" target="_blank" style="display: none; color: #10b981; text-decoration: none; font-weight: 500;">
83                        <img src="/custom-link.png" alt="" class="custom-emoji"> Website
84                    </a>
85                    <a id="venue-email" href="#" style="display: none; color: #10b981; text-decoration: none; font-weight: 500;">
86                        ✉️ Email
87                    </a>
88                    <a id="venue-phone" href="#" style="display: none; color: #10b981; text-decoration: none; font-weight: 500;">
89                        📞 Phone
90                    </a>
91                    <a id="venue-facebook" href="#" target="_blank" style="display: none; color: #10b981; text-decoration: none; font-weight: 500;">
92                        Facebook
93                    </a>
94                    <a id="venue-instagram" href="#" target="_blank" style="display: none; color: #10b981; text-decoration: none; font-weight: 500;">
95                        Instagram
96                    </a>
97                    <a id="venue-youtube" href="#" target="_blank" style="display: none; color: #10b981; text-decoration: none; font-weight: 500;">
98                        YouTube
99                    </a>
100                    <a id="venue-twitter" href="#" target="_blank" style="display: none; color: #10b981; text-decoration: none; font-weight: 500;">
101                        Twitter/X
102                    </a>
103                    <a id="venue-tiktok" href="#" target="_blank" style="display: none; color: #10b981; text-decoration: none; font-weight: 500;">
104                        TikTok
105                    </a>
106                    <a id="venue-bandcamp" href="#" target="_blank" style="display: none; color: #10b981; text-decoration: none; font-weight: 500;">
107                        Bandcamp
108                    </a>
109                </div>
110            </div>
111            
112            <!-- Manage Venue CTA (for logged-in owners) -->
113            <div id="manage-venue-section" style="display: none; margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid rgba(5, 150, 105, 0.2);">
114                <a href="/venue-manage.html" style="display: inline-block; padding: 0.75rem 1.5rem; background: linear-gradient(135deg, #059669 0%, #0ea5e9 100%); color: #ffffff !important; text-decoration: none; border-radius: 8px; font-weight: 600; transition: transform 0.2s, box-shadow 0.2s; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);">
115                    Manage This Venue →
116                </a>
117            </div>
118            
119            <!-- Request Login Link (for claimed venues) -->
120            <div id="request-login-section" style="display: none; margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid rgba(5, 150, 105, 0.2);">
121                <p style="font-size: 0.95rem; color: #cbd5e1; margin-bottom: 0.75rem;">
122                    🔑 Venue Owner? Manage your listing on GrassGigs
123                </p>
124                <a href="#" onclick="requestLoginLink(); return false;" style="display: inline-block; padding: 0.6rem 1.25rem; background: linear-gradient(135deg, #059669 0%, #0ea5e9 100%); color: #ffffff !important; -webkit-text-fill-color: #ffffff !important; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 0.9rem; transition: transform 0.2s;">
125                    Get Your Login Link →
126                </a>
127                <div id="login-link-message" style="display: none; margin-top: 0.5rem; padding: 0.75rem; border-radius: 8px;"></div>
128            </div>
129            
130            <!-- Claim Venue CTA -->
131            <div id="claim-venue-section" style="margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid rgba(5, 150, 105, 0.2);">
132                <p style="font-size: 0.9rem; color: #94a3b8; margin-bottom: 0.5rem;">
133                    Are you affiliated with this venue?
134                </p>
135                <a href="#" onclick="showClaimModal(); return false;" style="color: #10b981; text-decoration: none; font-weight: 500; font-size: 0.95rem; transition: color 0.2s;">
136                    Claim this page →
137                </a>
138            </div>
139        </section>
140
141        <!-- Map Section -->
142        <section class="content-section">
143            <div id="venue-map" style="width: 100%; height: 400px; border-radius: 20px; margin-bottom: 3rem; box-shadow: 0 20px 40px rgba(0,0,0,0.2); border: 1px solid rgba(5, 150, 105, 0.2);"></div>
144        </section>
145        
146        <!-- Events Section -->
147        <section class="content-section">
148            <div style="background: rgba(15, 23, 42, 0.8); backdrop-filter: blur(10px); border-radius: 20px; padding: 2rem; box-shadow: 0 20px 40px rgba(0,0,0,0.2); border: 1px solid rgba(5, 150, 105, 0.1);">
149                <h2 style="color: #10b981; margin-bottom: 1.5rem; font-size: 1.5rem;">Upcoming Events</h2>
150                <div id="events-list">
151                    <div class="skeleton-container">
152                        <div class="skeleton-card"><div class="skeleton-line skeleton-line-band"></div><div class="skeleton-line skeleton-line-venue"></div><div class="skeleton-line skeleton-line-date"></div></div>
153                        <div class="skeleton-card"><div class="skeleton-line skeleton-line-band"></div><div class="skeleton-line skeleton-line-venue"></div><div class="skeleton-line skeleton-line-date"></div></div>
154                        <div class="skeleton-card"><div class="skeleton-line skeleton-line-band"></div><div class="skeleton-line skeleton-line-venue"></div><div class="skeleton-line skeleton-line-date"></div></div>
155                    </div>
156                </div>
157            </div>
158        </section>
159        
160        <!-- Past Events Section -->
161        <section class="content-section" id="past-events-section" style="display: none; margin-top: 1.5rem;">
162            <div style="background: rgba(15, 23, 42, 0.8); backdrop-filter: blur(10px); border-radius: 20px; padding: 2rem; box-shadow: 0 20px 40px rgba(0,0,0,0.2); border: 1px solid rgba(5, 150, 105, 0.1);">
163                <h2 style="color: #94a3b8; margin-bottom: 1rem; font-size: 1.25rem;">Recent Performers</h2>
164                <div id="past-events-list"></div>
165            </div>
166        </section>
167        
168        <!-- Subscribe CTA -->
169        <section class="content-section" style="margin-top: 2rem;">
170            <div style="background: rgba(15, 23, 42, 0.8); backdrop-filter: blur(10px); border-radius: 20px; padding: 2rem; box-shadow: 0 20px 40px rgba(0,0,0,0.2); border: 1px solid rgba(5, 150, 105, 0.1); text-align: center;">
171                <h2 id="venue-subscribe-heading" style="color: #10b981; margin-bottom: 0.5rem; font-size: 1.3rem;">Get Notified About Shows Here</h2>
172                <p id="venue-subscribe-text" style="color: #cbd5e1; margin-bottom: 1rem;">Be the first to know when new events are announced at this venue</p>
173                <a id="venue-subscribe-link" href="#" style="display: inline-block; padding: 0.75rem 1.5rem; background: linear-gradient(135deg, #059669 0%, #0ea5e9 100%); color: #ffffff !important; text-decoration: none; border-radius: 8px; font-weight: 600; transition: transform 0.2s, box-shadow 0.2s; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);">
174                    📧 Subscribe to Updates
175                </a>
176                <p style="color: #64748b; font-size: 0.85rem; margin-top: 0.75rem;">Free · Unsubscribe anytime</p>
177            </div>
178        </section>
179    </main>
180
181    <!-- Shared Footer -->
182    <div id="footer-placeholder"></div>
183    
184    <!-- Claim Venue Modal -->
185    <div id="claimVenueModal" class="modal" style="display: none;">
186        <div class="modal-content" style="max-width: 500px;">
187            <span class="close" onclick="closeClaimModal()">&times;</span>
188            
189            <!-- Form View -->
190            <div id="claimFormView">
191                <h2 style="color: #10b981; margin-bottom: 1rem;">Claim This Venue</h2>
192                <p style="color: #cbd5e1; line-height: 1.6; margin-bottom: 1.5rem;">
193                    Submit a request to claim this venue page. We'll review your request and contact you via email.
194                </p>
195                
196                <form id="claimVenueForm">
197                    <input type="hidden" id="claimVenueId" name="venue_id">
198                    <input type="hidden" id="claimVenueName" name="venue_name">
199                    
200                    <label for="claimName" style="color: #cbd5e1; display: block; margin-bottom: 0.5rem; font-weight: 500;">Your Name *</label>
201                    <input type="text" id="claimName" name="name" required style="width: 100%; padding: 0.75rem; margin-bottom: 1rem; border: 1px solid rgba(5, 150, 105, 0.3); border-radius: 8px; background: rgba(15, 23, 42, 0.6); color: #f8fafc; font-size: 1rem;">
202                    
203                    <label for="claimEmail" style="color: #cbd5e1; display: block; margin-bottom: 0.5rem; font-weight: 500;">Email Address *</label>
204                    <input type="email" id="claimEmail" name="email" required style="width: 100%; padding: 0.75rem; margin-bottom: 1rem; border: 1px solid rgba(5, 150, 105, 0.3); border-radius: 8px; background: rgba(15, 23, 42, 0.6); color: #f8fafc; font-size: 1rem;">
205                    
206                    <label for="claimRole" style="color: #cbd5e1; display: block; margin-bottom: 0.5rem; font-weight: 500;">Your Role *</label>
207                    <select id="claimRole" name="role" required style="width: 100%; padding: 0.75rem; margin-bottom: 1.5rem; border: 1px solid rgba(5, 150, 105, 0.3); border-radius: 8px; background: rgba(15, 23, 42, 0.6); color: #f8fafc; font-size: 1rem;">
208                        <option value="">Select your role...</option>
209                        <option value="owner">Owner</option>
210                        <option value="manager">Manager</option>
211                        <option value="booker">Booker</option>
212                    </select>
213                    
214                    <div id="claimError" style="display: none; color: #ef4444; margin-bottom: 1rem; padding: 0.75rem; background: rgba(239, 68, 68, 0.1); border-radius: 8px; border: 1px solid rgba(239, 68, 68, 0.3);"></div>
215                    
216                    <button type="submit" style="width: 100%; padding: 0.75rem; background: linear-gradient(135deg, #059669 0%, #0ea5e9 100%); color: white; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; font-size: 1rem;">
217                        Submit Claim Request
218                    </button>
219                </form>
220            </div>
221            
222            <!-- Success View -->
223            <div id="claimSuccessView" style="display: none;">
224                <h2 style="color: #10b981; margin-bottom: 1rem;">✓ Claim Submitted!</h2>
225                
226                <div style="background: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16, 185, 129, 0.3); border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem;">
227                    <h3 style="color: #10b981; font-size: 1rem; margin-bottom: 0.5rem;">Next Steps:</h3>
228                    <ol style="color: #cbd5e1; line-height: 1.8; margin: 0; padding-left: 1.5rem;">
229                        <li><strong>Check your email</strong> for a verification link</li>
230                        <li><strong>Click the link</strong> to verify your email address</li>
231                        <li><strong>We'll review</strong> your claim (typically 1-3 business days)</li>
232                        <li><strong>Get your magic link</strong> in the approval email to manage your venue</li>
233                    </ol>
234                </div>
235                
236                <p style="color: #94a3b8; font-size: 0.9rem; margin-bottom: 1rem;">
237                    💡 <strong>Tip:</strong> Check your spam folder if you don't see the verification email within a few minutes.
238                </p>
239                
240                <p style="color: #94a3b8; font-size: 0.9rem; margin-bottom: 1.5rem;">
241                    Questions? Email us at <a href="mailto:[email protected]" style="color: #10b981;">[email protected]</a>
242                </p>
243                
244                <button onclick="closeClaimModal()" style="width: 100%; padding: 0.75rem; background: linear-gradient(135deg, #059669 0%, #0ea5e9 100%); color: white; border: none; border-radius: 8px; font-weight: 600; cursor: pointer;">
245                    Got It!
246                </button>
247            </div>
248        </div>
249    </div>
250    
251    <!-- Image Lightbox Modal -->
252    <div id="imageLightbox" class="modal" style="display: none; z-index: 2000; background: rgba(15, 23, 42, 0.95);">
253        <div class="modal-content" style="max-width: 90vw; max-height: 90vh; padding: 1rem; background: rgba(15, 23, 42, 0.95); border: 2px solid rgba(5, 150, 105, 0.3); box-shadow: 0 20px 60px rgba(0,0,0,0.5);">
254            <span class="close" onclick="closeLightbox()" style="position: absolute; top: 1rem; right: 1rem; font-size: 2rem; z-index: 2001; background: linear-gradient(135deg, rgba(5, 150, 105, 0.2) 0%, rgba(14, 165, 233, 0.2) 100%); width: 45px; height: 45px; border-radius: 50%; display: flex; align-items: center; justify-content: center; border: 2px solid rgba(5, 150, 105, 0.4); color: #10b981; cursor: pointer; transition: all 0.2s;" onmouseover="this.style.background='linear-gradient(135deg, rgba(5, 150, 105, 0.4) 0%, rgba(14, 165, 233, 0.4) 100%)'" onmouseout="this.style.background='linear-gradient(135deg, rgba(5, 150, 105, 0.2) 0%, rgba(14, 165, 233, 0.2) 100%)'">&times;</span>
255            <img id="lightboxImage" src="" alt="Gallery image" style="width: 100%; height: auto; max-height: 85vh; object-fit: contain; border-radius: 12px; border: 2px solid rgba(5, 150, 105, 0.3);">
256        </div>
257    </div>
258    
259    
259<script>
260        const API_BASE_URL = 'https://bmbn5a7v4c.execute-api.us-east-1.amazonaws.com/Prod';
261        const IMAGES_CDN = 'https://d3t15gta06qa0f.cloudfront.net';
262        
263        let map;
264        let allVenueEvents = []; // Store events for modal lookup
265
266        // Lazily load Leaflet (CSS + JS) only when a map is actually needed.
267        // Keeps ~150KB of render-blocking assets off the critical path.
268        let _leafletPromise = null;
269        function ensureLeaflet() {
270            if (window.L) return Promise.resolve();
271            if (_leafletPromise) return _leafletPromise;
272            _leafletPromise = new Promise((resolve, reject) => {
273                const css = document.createElement('link');
274                css.rel = 'stylesheet';
275                css.href = 'https://unpkg.com/[email protected]/dist/leaflet.css';
276                document.head.appendChild(css);
277
278                const js = document.createElement('script');
279                js.src = 'https://unpkg.com/[email protected]/dist/leaflet.js';
280                js.onload = () => resolve();
281                js.onerror = reject;
282                document.head.appendChild(js);
283            });
284            return _leafletPromise;
285        }
286        
287        // Image helper functions
288        function getImageUrl(s3Key, size = 'large') {
289            if (!s3Key) return null;
290            if (s3Key.startsWith('http')) return s3Key;
291            
292            // Encode each path segment to handle spaces in venue names
293            const encodedKey = s3Key.split('/').map(segment => encodeURIComponent(segment)).join('/');
294            
295            if (size === 'original') {
296                return `${IMAGES_CDN}/${encodedKey}`;
297            }
298            
299            const base = s3Key.substring(0, s3Key.lastIndexOf('.'));
300            const encodedBase = base.split('/').map(segment => encodeURIComponent(segment)).join('/');
301            
302            return `${IMAGES_CDN}/${encodedBase}-${size}.jpg`;
303        }
304        
305        // Helper to handle image load errors - fallback to original
306        function handleImageError(img, s3Key) {
307            if (!img.dataset.fallbackAttempted) {
308                img.dataset.fallbackAttempted = 'true';
309                if (s3Key.startsWith('http')) {
310                    img.src = s3Key;
311                } else {
312                    const encodedKey = s3Key.split('/').map(segment => encodeURIComponent(segment)).join('/');
313                    img.src = `${IMAGES_CDN}/${encodedKey}`;
314                }
315            }
316        }
317        
318        // Get venue slug from URL
319        const pathParts = window.location.pathname.split('/').filter(p => p);
320        
321        // Check if this is actually a venue URL
322        if (pathParts[0] !== 'venue' || !pathParts[1]) {
323            // Not a venue URL, redirect to home
324            window.location.href = '/';
325        }
326        
327        const venueSlug = pathParts[1];
328        
329        // Set canonical URL to the static SEO page
330        const canonicalUrl = `https://grassgigs.com/venues/${venueSlug}.html`;
331        document.getElementById('canonical-url').setAttribute('href', canonicalUrl);
332        
333        function toTitleCase(str) {
334            if (!str) return '';
335            return str.replace(/\b\w+/g, word => {
336                const lower = word.toLowerCase();
337                if (['and', 'or', 'the', 'in', 'at', 'of', 'for', 'on', 'to', 'a'].includes(lower) && str.indexOf(word) !== 0) {
338                    return lower;
339                }
340                return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
341            });
342        }
343
344        // Display a venue name with proper capitalization. Only reformats names
345        // that are entirely lowercase so intentional casing (e.g. "NOLA", "AJ's")
346        // is preserved.
347        function displayVenueName(name) {
348            if (!name) return '';
349            if (/[A-Z]/.test(name)) return name;
350            return toTitleCase(name);
351        }
352
353        // Helper: slugify text (matches backend logic)
354        function slugify(text) {
355            if (!text) return '';
356            text = text.toLowerCase();
357            text = text.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
358            text = text.replace(/&/g, 'and');
359            text = text.replace(/\s+/g, '-');
360            text = text.replace(/[^a-z0-9-]/g, '');
361            text = text.replace(/-+/g, '-');
362            text = text.replace(/^-+|-+$/g, '');
363            return text;
364        }
365
366        // Format date for display
367        function formatDate(dateStr, endDateStr) {
368            const date = new Date(dateStr + 'T00:00:00');
369            
370            // If there's an end date (multi-day event), format as range
371            if (endDateStr && endDateStr !== dateStr) {
372                const endDate = new Date(endDateStr + 'T00:00:00');
373                
374                // Same month and year - show "15-17 Aug, 2026"
375                if (date.getMonth() === endDate.getMonth() && date.getFullYear() === endDate.getFullYear()) {
376                    const monthYear = date.toLocaleDateString('en-US', {
377                        month: 'short',
378                        year: 'numeric'
379                    });
380                    return `${date.getDate()}-${endDate.getDate()} ${monthYear}`;
381                }
382                // Different months - show "Aug 30 - Sep 2, 2026"
383                else if (date.getFullYear() === endDate.getFullYear()) {
384                    const startMonth = date.toLocaleDateString('en-US', { month: 'short' });
385                    const endMonth = endDate.toLocaleDateString('en-US', { month: 'short' });
386                    const year = date.getFullYear();
387                    return `${startMonth} ${date.getDate()} - ${endMonth} ${endDate.getDate()}, ${year}`;
388                }
389                // Different years - show full dates
390                else {
391                    const startFormatted = date.toLocaleDateString('en-US', {
392                        month: 'short',
393                        day: 'numeric',
394                        year: 'numeric'
395                    });
396                    const endFormatted = endDate.toLocaleDateString('en-US', {
397                        month: 'short',
398                        day: 'numeric',
399                        year: 'numeric'
400                    });
401                    return `${startFormatted} - ${endFormatted}`;
402                }
403            }
404            
405            // Single day event
406            return date.toLocaleDateString('en-US', { 
407                weekday: 'long', 
408                year: 'numeric', 
409                month: 'long', 
410                day: 'numeric' 
411            });
412        }
413        
414        // Format time for display
415        function formatTime(timeStr) {
416            if (!timeStr || timeStr === '00:00') return '';
417            const [hours, minutes] = timeStr.split(':');
418            const hour = parseInt(hours);
419            const ampm = hour >= 12 ? 'PM' : 'AM';
420            const displayHour = hour % 12 || 12;
421            return `${displayHour}:${minutes} ${ampm}`;
422        }
423        
424        // Initialize map
425        async function initMap(lat, lng, venueName, isPremium = false) {
426            await ensureLeaflet();
427            map = L.map('venue-map').setView([lat, lng], 15);
428            
429            L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
430                attribution: '© OpenStreetMap contributors'
431            }).addTo(map);
432            
433            // Create custom marker icon matching main map style
434            let markerIcon = undefined;
435            
436            if (isPremium) {
437                markerIcon = L.divIcon({
438                    className: 'premium-marker-icon',
439                    html: `<div class="premium-marker">
440                        <svg width="30" height="40" viewBox="0 0 30 40" xmlns="http://www.w3.org/2000/svg">
441                            <defs><linearGradient id="premiumGrad" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" style="stop-color:#fbbf24;stop-opacity:1" /><stop offset="100%" style="stop-color:#f59e0b;stop-opacity:1" /></linearGradient></defs>
442                            <path d="M15 0 C6.716 0 0 6.716 0 15 C0 23.284 15 40 15 40 S30 23.284 30 15 C30 6.716 23.284 0 15 0 Z" fill="url(#premiumGrad)" stroke="#d97706" stroke-width="1.5"/>
443                        </svg>
444                    </div>`,
445                    iconSize: [30, 40],
446                    iconAnchor: [15, 40],
447                    popupAnchor: [0, -40]
448                });
449            }
450            
451            // Build styled popup content matching main map style
452            let popupContent = '<div style="width: 100%; max-height: 300px; overflow-y: auto; box-sizing: border-box; padding-top: 35px; padding-left: 12px; padding-right: 12px;">';
453            
454            // Venue name
455            popupContent += '<h4 style="margin: 5px 0; text-align: center;">' + venueName;
456            if (isPremium) {
457                popupContent += ' <span style="background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: white; padding: 2px 6px; border-radius: 10px; font-size: 10px; font-weight: bold; margin-left: 4px; display: inline-block;">⭐ FEATURED</span>';
458            }
459            popupContent += '</h4>';
460            
461            // Get venue data from the page
462            const cityState = document.getElementById('venue-city-state').textContent;
463            const address = document.getElementById('venue-address').textContent;
464            
465            if (cityState) {
466                popupContent += '<p style="margin: 3px 0; text-align: center;">' + cityState + '</p>';
467            }
468            
469            if (address && address.trim()) {
470                popupContent += '<p style="margin: 8px 0 3px 0; font-size: 0.85em; text-align: center; color: #64748b; line-height: 1.4;"><img src="/custom-map-pin.png" alt="" style="height:1.1em;width:auto;vertical-align:middle;"> ' + address + '</p>';
471            }
472            
473            popupContent += '</div>';
474            
475            L.marker([lat, lng], markerIcon ? { icon: markerIcon } : {})
476                .addTo(map)
477                .bindPopup(popupContent, {
478                    maxWidth: 300,
479                    className: 'custom-popup'
480                })
481                .openPopup();
482        }
483        
484        // Load venue details
485        async function loadVenueDetails() {
486            try {
487                const response = await fetch(`${API_BASE_URL}/venue/${venueSlug}`);
488                
489                if (!response.ok) {
490                    throw new Error('Venue not found');
491                }
492                
493                const data = await response.json();
494                
495                // Debug: Log the full API response
496                console.log('Full API response:', data);
497                console.log('Events array:', data.events);
498                console.log('Events count:', data.events ? data.events.length : 0);
499                
500                const { venue, events } = data;
501                allVenueEvents = events || []; // Store for modal lookup
502                
503                // If this venue has been migrated to a festival, redirect there
504                if (venue.festival_redirect) {
505                    const festivalSlug = venue.festival_redirect
506                        .toLowerCase()
507                        .replace(/[^a-z0-9\s-]/g, '')
508                        .replace(/\s+/g, '-')
509                        .replace(/-+/g, '-')
510                        .replace(/^-+|-+$/g, '');
511                    window.location.replace('/festival/' + festivalSlug);
512                    return;
513                }
514                
515                // Debug: Log the venue data to console
516                console.log('Venue data received:', venue);
517                
518                // Store venue data for claim form
519                // Generate location-aware venue_id if we have city/state.
520                // Use the shared slugify() so the suffix matches the backend
521                // canonical key, and guard against double-suffixing when the slug
522                // already ends with -city-state OR -state (location-qualified keys
523                // produced by canonical_venue_key).
524                let claimVenueId = venueSlug;
525                if (venue.city && venue.state) {
526                    const citySlug = slugify(venue.city);
527                    const stateSlug = slugify(venue.state);
528                    const endsWithLocation =
529                        (citySlug && stateSlug && venueSlug.endsWith(`-${citySlug}-${stateSlug}`)) ||
530                        (stateSlug && venueSlug.endsWith(`-${stateSlug}`));
531                    if (!endsWithLocation) {
532                        const suffix = [citySlug, stateSlug].filter(
532Boolean).join('-');
533                        if (suffix) {
534                            claimVenueId = `${venueSlug}-${suffix}`;
535                        }
536                    }
537                }
538                
539                // Normalize the display name (fixes all-lowercase scraped names)
540                venue.name = displayVenueName(venue.name);
541
542                currentVenueData = {
543                    venue_id: claimVenueId,
544                    name: venue.name
545                };
546                
547                // Update page title and meta
548                document.title = `${venue.name} - ${venue.city}, ${venue.state} | GrassGigs`;
549                
550                // Update venue info
551                document.getElementById('venue-name').textContent = venue.name;
552                
553                // Add verified badge for claimed venues
554                if (venue.claimed || venue.user_submitted) {
555                    const badge = document.createElement('span');
556                    badge.className = 'verified-badge-venue';
557                    badge.innerHTML = '✓ Official';
558                    badge.style.cssText = 'display: inline-flex; align-items: center; background: #10b981; color: white; -webkit-text-fill-color: white; padding: 4px 12px; border-radius: 16px; font-size: 14px; font-weight: 600; margin-left: 12px; vertical-align: middle;';
559                    document.getElementById('venue-name').appendChild(badge);
560                }
561                
562                // Update page title and meta description
563                const locationParts = [];
564                if (venue.city) locationParts.push(venue.city);
565                if (venue.state) locationParts.push(venue.state);
566                const locationText = locationParts.length > 0 ? locationParts.join(', ') : '';
567                
568                document.title = `${venue.name}${locationText ? ' - ' + locationText : ''} | Live Bluegrass Events | GrassGigs`;
569                document.querySelector('meta[name="description"]').setAttribute('content',
570                    `View upcoming bluegrass and live music events at ${venue.name}${locationText ? ' in ' + locationText : ''}. Find show dates, artists, and tickets.`);
571                
572                document.getElementById('venue-city-state').innerHTML = locationText ? '<img src="/custom-map-pin.png" alt="" style="height:1.1em;width:auto;vertical-align:middle;"> ' + locationText : '';
573                
574                if (venue.address) {
575                    document.getElementById('venue-address').textContent = venue.address;
576                }
577                
578                // Display description if available, or generate a default
579                if (venue.description && venue.description.trim()) {
580                    // Decode any HTML entities (e.g. &#x27; -> ') for display
581                    const descEl = document.getElementById('venue-description');
582                    const tmp = document.createElement('textarea');
583                    tmp.innerHTML = venue.description;
584                    descEl.textContent = tmp.value;
585                    document.getElementById('venue-description-section').style.display = 'block';
586                } else if (events.length > 0) {
587                    // Generate a default description from event data
588                    const descEl = document.getElementById('venue-description');
589                    const venueName = venue.name || toTitleCase(venue.venue_name || venueSlug.replace(/-/g, ' '));
590                    const locationParts = [];
591                    if (venue.city) locationParts.push(venue.city);
592                    if (venue.state) locationParts.push(venue.state);
593                    const locationStr = locationParts.length > 0 ? ` in ${locationParts.join(', ')}` : '';
594                    
595                    let descParts = [];
596                    descParts.push(`${venueName}${locationStr} is a live music venue featuring bluegrass and grassroots music.`);
597                    descParts.push(`There ${events.length === 1 ? 'is' : 'are'} ${events.length} upcoming show${events.length !== 1 ? 's' : ''} scheduled.`);
598                    
599                    // Mention some bands playing there
600                    const upcomingBands = events.slice(0, 3).map(e =>
600 e.band).filter(Boolean);
601                    if (upcomingBands.length > 0) {
602                        descParts.push(`Upcoming artists include ${upcomingBands.join(', ')}.`);
603                    }
604                    
605                    descEl.textContent = descParts.join(' ');
606                    document.getElementById('venue-description-section').style.display = 'block';
607                }
608                
609                // Display contact and social links if available
610                let hasLinks = false;
611                const isPremium = venue.subscription_tier === 'premium';
612                
613                if (venue.website && venue.website.trim()) {
614                    const websiteLink = document.getElementById('venue-website');
615                    websiteLink.href = venue.website;
616                    websiteLink.onclick = () => trackVenueConversion(venueSlug, 'website', null, isPremium);
617                    websiteLink.style.display = 'inline-block';
618                    hasLinks = true;
619                }
620                
621                if (venue.contact_email && venue.contact_email.trim()) {
622                    const emailLink = document.getElementById('venue-email');
623                    emailLink.href = `mailto:${venue.contact_email}`;
624                    emailLink.onclick = () => trackVenueConversion(venueSlug, 'email', null, isPremium);
625                    emailLink.style.display = 'inline-block';
626                    hasLinks = true;
627                }
628                
629                if (venue.contact_phone && venue.contact_phone.trim()) {
630                    const phoneLink = document.getElementById('venue-phone');
631                    phoneLink.href = `tel:${venue.contact_phone}`;
632                    phoneLink.textContent = `📞 ${venue.contact_phone}`;
633                    phoneLink.onclick = () => trackVenueConversion(venueSlug, 'phone', null, isPremium);
634                    phoneLink.style.display = 'inline-block';
635                    hasLinks = true;
636                }
637                
638                if (venue.facebook && venue.facebook.trim()) {
639                    const fbLink = document.getElementById('venue-facebook');
640                    fbLink.href = venue.facebook;
641                    fbLink.onclick = () => trackVenueConversion(venueSlug, 'social_facebook', null, isPremium);
642                    fbLink.style.display = 'inline-block';
643                    hasLinks = true;
644                }
645                
646                if (venue.instagram && venue.instagram.trim()) {
647                    const igLink = document.getElementById('venue-instagram');
648                    igLink.href = venue.instagram;
649                    igLink.onclick = () => trackVenueConversion(venueSlug, 'social_instagram', null, isPremium);
650                    igLink.style.display = 'inline-block';
651                    hasLinks = true;
652                }
653                
654                if (venue.youtube && venue.youtube.trim()) {
655                    const ytLink = document.getElementById('venue-youtube');
656                    ytLink.href = venue.youtube;
657                    ytLink.onclick = () => trackVenueConversion(venueSlug, 'social_youtube', null, isPremium);
658                    ytLink.style.display = 'inline-block';
659                    hasLinks = true;
660                }
661                
662                if (venue.twitter && venue.twitter.trim()) {
663                    const twLink = document.getElementById('venue-twitter');
664                    twLink.href = venue.twitter;
665                    twLink.onclick = () => trackVenueConversion(venueSlug, 'social_twitter', null, isPremium);
666                    twLink.style.display = 'inline-block';
667                    hasLinks = true;
668                }
669                
670                if (venue.tiktok && venue.tiktok.trim()) {
671                    const ttLink = document.getElementById('venue-tiktok');
672                    ttLink.href = venue.tiktok;
673                    ttLink.onclick = () => trackVenueConversion(venueSlug, 'social_tiktok', null, isPremium);
674                    ttLink.style.display = 'inline-block';
675                    hasLinks = true;
676                }
677                
678                if (venue.bandcamp && venue.bandcamp.trim()) {
679                    const bcLink = document.getElementById('venue-bandcamp');
680                    bcLink.href = venue.bandcamp;
681                    bcLink.onclick = () => trackVenueConversion(venueSlug, 'social_bandcamp', null, isPremium);
682                    bcLink.style.display = 'inline-block';
683                    hasLinks = true;
684                }
685                
686                if (hasLinks) {
687                    document.getElementById('venue-links-section').style.display = 'block';
688                }
689                
690                // Display profile image if available
691                if (venue.profile_image) {
692                    const profileImg = document.getElementById('venue-profile-image');
693                    profileImg.src = getImageUrl(venue.profile_image, 'large');
694                    profileImg.alt = venue.name;
695                    profileImg.onerror = function() { handleImageError(this, venue.profile_image); };
696                    document.getElementById('venue-profile-image-section').style.display = 'block';
697                }
698                
699                // Display gallery images if available
700                if (venue.gallery_images && venue.gallery_images.length > 0) {
701                    const gallery = document.getElementById('venue-gallery');
702                    gallery.innerHTML = venue.gallery_images.map(s3Key => {
703                        const imageUrl = getImageUrl(s3Key, 'thumbnail');
704                        const largeUrl = getImageUrl(s3Key, 'original');
705                        const escapedKey = s3Key.replace(/'/g, "\\'");
706                        const escapedUrl = largeUrl.replace(/'/g, "\\'");
707                        return `<div onclick="openLightbox('${escapedUrl}')" style="cursor: pointer;">
708                            <img src="${imageUrl}" alt="${venue.name} gallery" 
709                                onerror="handleImageError(this, '${escapedKey}')"
710                                style="width: 100%; height: 200px; object-fit: cover; border-radius: 8px; transition: transform 0.2s;"
711                                onmouseover="this.style.transform='scale(1.05)'"
712                                onmouseout="this.style.transform='scale(1)'">
713                        </div>`;
714                    }).join('');
715                    document.getElementById('venue-gallery-section').style.display = 'block';
716                }
717                
718                // Hide claim section if venue is already claimed or user-submitted
719                if (venue.claimed || venue.user_submitted) {
720                    document.getElementById('claim-venue-section').style.display = 'none';
721                    // Show request login link for claimed/submitted venues
722                    document.getElementById('request-login-section').style.display = 'block';
723                }
724                
725                // Check if current user is the owner of this venue
726                checkVenueOwnership(venueSlug);
727                
728                // Track venue page view for premium venues
729                if (venue.subscription_tier === 'premium') {
730                    trackVenueView(venueSlug, true);
731                }
732                
733                // Initialize map
734                if (venue.latitude && venue.longitude) {
735                    const isPremium = venue.subscription_tier === 'premium';
736                    initMap(venue.latitude, venue.longitude, venue.name, isPremium);
737                }
738                
739                // Allow indexing if page has sufficient content
740                // (has events, or has description, or has contact info)
741                const hasEvents = events.length > 0;
742                const hasDescription = venue.description && venue.description.trim().length > 0;
743                const hasContactInfo = venue.website || venue.contact_email || venue.facebook || venue.instagram;
744                if (hasEvents || hasDescription || hasContactInfo) {
745                    document.getElementById('robots-meta').setAttribute('content', 'index, follow');
746                }
747                
748                // Check if any event is linked to a festival — show link in hero
749                const festivalEvent = events.find(e => e.festival_id);
750                if (festivalEvent) {
751                    const festEl = document.getElementById('venue-festival-link');
752                    const festName = festivalEvent.festival_name ? toTitleCase(festivalEvent.festival_name) : 'View Festival';
753                    festEl.innerHTML = `🎪 <a href="/festival/${festivalEvent.festival_id}" style="color: #a855f7; text-decoration: none; font-weight: 500;">${festName} →</a>`;
754                    festEl.style.display = 'block';
755                }
756
757                // Display venue stats
758                if (events.length > 0) {
759                    const uniqueBands = new Set(events.map(e => e.band).filter(Boolean));
760                    document.getElementById('stat-upcoming').textContent = events.length;
761                    document.getElementById('stat-bands').textContent = uniqueBands.size;
762                    document.getElementById('venue-stats').style.display = 'flex';
763                }
764                
765                // Update subscribe link
766                document.getElementById('venue-subscribe-heading').textContent = `Get Notified About Shows at ${venue.name}`;
767                document.getElementById('venue-subscribe-text').textContent = `Be the first to know when new events are announced`;
768                document.getElementById('venue-subscribe-link').href = `/?subscribe=venue&location=${encodeURIComponent(venueSlug)}#subscribe`;
769
770                // Display events using step-like cards
771                const eventsList = document.getElementById('events-list');
772                
773                if (events.length === 0) {
774                    eventsList.innerHTML = '<p style="text-align: center; color: #cbd5e1; padding: 2rem;">No upcoming events at this venue</p>';
775                } else {
776                    // Group events by month for section headers
777                    let currentMonth = '';
778                    eventsList.innerHTML = events.map((event) => {
779                        const time = formatTime(event.time);
780                        const isPremium = venue.subscription_tier === 'premium';
781                        const isJam = event.event_type === 'jam';
782                        
783                        // Date parts for calendar chip
784                        const dateParts = event.date.split('-');
785                        const eventDate = new Date(parseInt(dateParts[0]), parseInt(dateParts[1]) - 1, parseInt(dateParts[2]));
786                        const monthShort = eventDate.toLocaleDateString('en-US', { month: 'short' }).toUpperCase();
787                        const dayNum = eventDate.getDate();
788                        const dayOfWeek = eventDate.toLocaleDateString('en-US', { weekday: 'short' });
789                        
790                        // Month group header
791                        const monthKey = eventDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
792                        let monthHeader = '';
793                        if (monthKey !== currentMonth) {
794                            currentMonth = monthKey;
795                            monthHeader = `<div style="padding: 0.75rem 0 0.5rem; margin-top: 0.5rem; border-bottom: 1px solid rgba(5, 150, 105, 0.15); margin-bottom: 0.75rem;"><span style="color: #94a3b8; font-size: 0.8rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em;">${monthKey}</span></div>`;
796                        }
797                        
798                        // Create band link with tracking
799                        const bandDisplay = toTitleCase(event.band || '');
800                        const bandSlug = slugify(event.band || '');
801                        let bandLink;
802                        if (bandSlug) {
803                            const onclickAttr = isPremium ? 
804                                `onclick="trackVenueEventClick('${venueSlug}', '${event.event_id}', true); return true;"` : 
805                                '';
806                            bandLink = `<a href="/band/${bandSlug}" ${onclickAttr} style="color: #e2e8f0; text-decoration: none; transition: color 0.2s;" onmouseover="this.style.color='#10b981'" onmouseout="this.style.color='#e2e8f0'">${bandDisplay}</a>`;
807                        } else {
808                            bandLink = `<span style="color: #e2e8f0;">${bandDisplay}</span>`;
809                        }
810                        
811                        // Create ticket link with tracking if available
812                        let ticketLink = '';
813                        if (event.ticket_url) {
814                            const ticketOnclick = isPremium ? 
815                                `onclick="event.stopPropagation(); trackVenueConversion('${venueSlug}', 'ticket', '${event.event_id}', true); return true;"` : 
816                                'onclick="event.stopPropagation();"';
817                            ticketLink = `<a href="${event.ticket_url}" target="_blank" ${ticketOnclick} style="display: inline-flex; align-items: center; gap: 4px; margin-top: 0.5rem; padding: 0.3rem 0.75rem; background: rgba(251, 191, 36, 0.1); border: 1px solid rgba(251, 191, 36, 0.3); border-radius: 6px; color: #fbbf24; text-decoration: none; font-size: 0.8rem; font-weight: 600; transition: background 0.2s;" onmouseover="this.style.background='rgba(251, 191, 36, 0.2)'" onmouseout="this.style.background='rgba(251, 191, 36, 0.1)'">🎟️ Tickets</a>`;
818                        }
819                        
820                        // Add jam description if available
821                        const jamDescription = isJam && event.description ? 
822                            `<p style="color: #94a3b8; font-size: 0.85rem; margin: 0.3rem 0 0; font-style: italic;">${event.description}</p>` : '';
823                        
824                        // Add jam page link if this is a jam
825                        let jamPageLink = '';
826                        if (isJam && event.jam_id) {
827                            jamPageLink = `<a href="/jam.html?id=${encodeURIComponent(event.jam_id)}" onclick="event.stopPropagation();" style="display: inline-flex; align-items: center; gap: 4px; margin-top: 0.5rem; margin-right: 0.5rem; padding: 0.3rem 0.75rem; background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.3); border-radius: 6px; color: #f59e0b; text-decoration: none; font-size: 0.8rem; font-weight: 500;">🎵 Jam Details</a>`;
828                        } else if (isJam && event.venue_name && event.venue_city && event.venue_state) {
829                            const jamSlug = `${event.venue_name}-${event.venue_city}-${event.venue_state}`.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/&/g, 'and').replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-+|-+$/g, '');
830                            jamPageLink = `<a href="/jams/${jamSlug}.html" onclick="event.stopPropagation();" style="display: inline-flex; align-items: center; gap: 4px; margin-top: 0.5rem; margin-right: 0.5rem; padding: 0.3rem 0.75rem; background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.3); border-radius: 6px; color: #f59e0b; text-decoration: none; font-size: 0.8rem; font-weight: 500;">🎵 Jam Details</a>`;
831                        }
832                        
833                        // Add festival link if event is part of a festival
834                        let festivalLink = '';
835                        if (event.festival_id) {
836                            const festName = event.festival_name ? toTitleCase(event.festival_name) : 'View Festival';
837                            festivalLink = `<a href="/festival/${event.festival_id}" onclick="event.stopPropagation();" style="display: inline-flex; align-items: center; gap: 4px; margin-top: 0.5rem; margin-right: 0.5rem; padding: 0.3rem 0.75rem; background: rgba(168, 85, 247, 0.1); border: 1px solid rgba(168, 85, 247, 0.3); border-radius: 6px; color: #a855f7; text-decoration: none; font-size: 0.8rem; font-weight: 500;">🎪 ${festName}</a>`;
838                        }
839
840                        // Multi-day badge
841                        let multiDayBadge = '';
842                        if (event.end_date && event.end_date !== event.date) {
843                            const endParts = event.end_date.split('-');
844                            const endDate = new Date(parseInt(endParts[0]), parseInt(endParts[1]) - 1, parseInt(endParts[2]));
845                            const diffDays = Math.ceil((endDate - eventDate) / (1000 * 60 * 60 * 24)) + 1;
846                            multiDayBadge = `<span style="display: inline-block; margin-left: 0.5rem; padding: 2px 8px; background: rgba(14, 165, 233, 0.15); border: 1px solid rgba(14, 165, 233, 0.3); border-radius: 4px; color: #7dd3fc; font-size: 0.7rem; font-weight: 600; vertical-align: middle;">${diffDays} days</span>`;
847                        }
848
849                        return `${monthHeader}
850                            <div style="display: flex; gap: 1rem; padding: 1rem; margin-bottom: 0.5rem; border-radius: 12px; cursor: pointer; transition: background 0.2s ease, transform 0.15s ease; border: 1px solid transparent;" onclick="openEventModal('${event.event_id}')" onmouseover="this.style.background='rgba(5, 150, 105, 0.06)'; this.style.borderColor='rgba(5, 150, 105, 0.15)'; this.style.transform='translateX(4px)'" onmouseout="this.style.background='transparent'; this.style.borderColor='transparent'; this.style.transform='none'">
851                                <!-- Date chip -->
852                                <div style="flex-shrink: 0; width: 56px; text-align: center;">
853                                    <div style="background: linear-gradient(135deg, rgba(5, 150, 105, 0.2), rgba(14, 165, 233, 0.15)
853); border: 1px solid rgba(5, 150, 105, 0.25); border-radius: 10px; padding: 0.5rem 0.25rem; min-height: 62px; display: flex; flex-direction: column; align-items: center; justify-content: center;">
854                                        <div style="font-size: 0.65rem; font-weight: 700; color: #10b981; letter-spacing: 0.05em;">${monthShort}</div>
855                                        <div style="font-size: 1.4rem; font-weight: 800; color: #f1f5f9; line-height: 1.1;">${dayNum}</div>
856                                        <div style="font-size: 0.65rem; color: #94a3b8; font-weight: 500;">${dayOfWeek}</div>
857                                    </div>
858                                </div>
859                                <!-- Event details -->
860                                <div style="flex: 1; min-width: 0;">
861                                    <h3 style="color: #e2e8f0; font-size: 1.1rem; font-weight: 600; margin: 0 0 0.3rem; line-height: 1.3;">${bandLink}${multiDayBadge}</h3>
862                                    ${time ? `<p style="color: #94a3b8; margin: 0 0 0.2rem; font-size: 0.85rem;">🕐 ${time}</p>` : ''}
863                                    ${jamDescription}
864                                    <div style="display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;">
865                                        ${festivalLink}${jamPageLink}${ticketLink}
866                                    </div>
867                                </div>
868                            </div>
869                        `;
870                    }).join('');
871                }
872                
873                // Load past events for this venue (last 12 months)
874                try {
875                    const today = new Date();
876                    const oneYearAgo = new Date(today);
877                    oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
878                    const startDate = oneYearAgo.toISOString().split('T')[0];
879                    const endDate = new Date(today.getTime() - 86400000).toISOString().split('T')[0]; // yesterday
880                    
881                    const venueName = venue.name || toTitleCase(venueSlug.replace(/-/g, ' '));
882                    const pastResp = await fetch(`${API_BASE_URL}/events?venue=${encodeURIComponent(venueName)}&start_date=${startDate}&end_date=${endDate}`);
883                    if (pastResp.ok) {
884                        const pastData = await pastResp.json();
885                        let pastEvents = pastData.events || pastData || [];
886                        
887                        // Sort most recent first, deduplicate by band, limit to 15
888                        pastEvents.sort((a, b) => (b.date || '').localeCompare(a.date || ''));
889                        const seenBands = new Set();
890                        const dedupedPastEvents = [];
891                        for (const event of pastEvents) {
892                            const bandKey = (event.band || '').toLowerCase();
893                            if (bandKey && !seenBands.has(bandKey)) {
894                                seenBands.add(bandKey);
895                                dedupedPastEvents.push(event);
896                            }
897                            if (dedupedPastEvents.length >= 15) break;
898                        }
899                        
900                        if (dedupedPastEvents.length > 0) {
901                            const pastSection = document.getElementById('past-events-section');
902                            const pastList = document.getElementById('past-events-list');
903                            
904                            pastList.innerHTML = dedupedPastEvents.map(event => {
905                                const bandName = toTitleCase(event.band || '');
906                                const bandSlug = slugify(event.band || '');
907                                const dateStr = formatDate(event.date, event.end_date);
908                                
909                                let bandLink = bandName;
910                                if (bandSlug) {
911                                    bandLink = `<a href="/band/${bandSlug}" style="color: #94a3b8; text-decoration: none;">${bandName}</a>`;
912                                }
913                                
914                                return `
915                                    <div style="display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 0; border-bottom: 1px solid rgba(5, 150, 105, 0.08); flex-wrap: wrap; opacity: 0.7;">
916                                        <span style="color: #94a3b8; font-size: 0.875rem; font-weight: 500; white-space: nowrap;">${dateStr}</span>
917                                        <span style="color: #475569; font-size: 0.75rem;">•</span>
918                                        <span style="color: #cbd5e1; font-size: 0.9rem; font-weight: 500;">${bandLink}</span>
919                                    </div>
920                                `;
921                            }).join('');
922                            
923                            pastSection.style.display = 'block';
924                        }
925                    }
926                } catch (e) {
927                    console.log('Could not load past events:', e);
928                }
929                
930                // Check for deep-linked event modal
931                const urlParams = new URLSearchParams(window.location.search);
932                const eventId = urlParams.get('event');
933                if (eventId) openEventModal(eventId);
934                
935            } catch (error) {
936                console.error('Error loading venue:', error);
937                document.getElementById('venue-name').textContent = 'Venue Not Found';
938                document.getElementById('venue-city-state').textContent = '';
939                document.getElementById('events-list').innerHTML = 
940                    '<div style="text-align: center; padding: 2.5rem 1.5rem;">' +
941                        '<p style="color: #94a3b8; margin-bottom: 1.5rem;">This venue could not be found. It may have been removed or the URL may be incorrect.</p>' +
942                        '<div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;">' +
943                            '<a href="/" style="padding: 0.6rem 1.5rem; background: linear-gradient(135deg, #059669 0%, #0ea5e9 100%); color: white !important; border-radius: 8px; text-decoration: none; font-weight: 600;">Browse Events</a>' +
944                            '<a href="/venues/index.html" style="padding: 0.6rem 1.5rem; background: rgba(255,255,255,0.08); color: white !important; border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; text-decoration: none; font-weight: 600;">All Venues</a>' +
945                        '</div>' +
946                    '</div>';
947            }
948        }
949        
950        // Load on page load
951        document.addEventListener('DOMContentLoaded', loadVenueDetails);
952        
953        // Claim venue modal functions
954        let currentVenueData = null;
955        
956        // Lightbox functions
957        function openLightbox(imageUrl) {
958            document.getElementById('lightboxImage').src = imageUrl;
959            document.getElementById('imageLightbox').style.display = 'block';
960        }
961        
962        function closeLightbox() {
963            document.getElementById('imageLightbox').style.display = 'none';
964        }
965        
966        // Close lightbox on escape key
967        document.addEventListener('keydown', function(e) {
968            if (e.key === 'Escape') {
969                closeLightbox();
970            }
971        });
972        
973        // Close lightbox when clicking outside the image
974        document.getElementById('imageLightbox').addEventListener('click', function(e) {
975            if (e.target === this) {
976                closeLightbox();
977            }
978        });
979        
980        function showClaimModal() {
981            // Populate hidden fields with venue data
982            if (currentVenueData) {
983                document.getElementById('claimVenueId').value = currentVenueData.venue_id;
984                document.getElementById('claimVenueName').value = currentVenueData.name;
985            }
986            
987            // Reset form and show form view
988            document.getElementById('claimVenueForm').reset();
989            document.getElementById('claimFormView').style.display = 'block';
990            document.getElementById('claimSuccessView').style.display = 'none';
991            document.getElementById('claimError').style.display = 'none';
992            
993            document.getElementById('claimVenueModal').style.display = 'block';
994        }
995        
996        function closeClaimModal() {
997            document.getElementById('claimVenueModal').style.display = 'none';
998        }
999        
1000        // Handle claim form submission
1001        document.getElementById('claimVenueForm').addEventListener('submit', async function(e) {
1002            e.preventDefault();
1003            
1004            const submitBtn = e.target.querySelector('button[type="submit"]');
1005            const originalText = submitBtn.textContent;
1006            submitBtn.textContent = 'Submitting...';
1007            submitBtn.disabled = true;
1008            
1009            const formData = {
1010                venue_id: document.getElementById('claimVenueId').value,
1011                venue_name: document.getElementById('claimVenueName').value,
1012                name: document.getElementById('claimName').value,
1013                email: document.getElementById('claimEmail').value,
1014                role: document.getElementById('claimRole').value,
1015                website: document.getElementById('claimWebsite')?.value || null
1016            };
1017            
1018            try {
1019                const response = await fetch(`${API_BASE_URL}/venue/claim`, {
1020                    method: 'POST',
1021                    headers: {
1022                        'Content-Type': 'application/json'
1023                    },
1024                    body: JSON.stringify(formData)
1025                });
1026                
1027                if (!response.ok) {
1028                    const error = await response.json();
1029                    throw new Error(error.error || 'Failed to submit claim request');
1030                }
1031                
1032                // Show success view
1033                document.getElementById('claimFormView').style.display = 'none';
1034                document.getElementById('claimSuccessView').style.display = 'block';
1035                
1036            } catch (error) {
1037                console.error('Error submitting claim:', error);
1038                const errorDiv = document.getElementById('claimError');
1039                errorDiv.textContent = error.message;
1040                errorDiv.style.display = 'block';
1041                
1042                submitBtn.textContent = originalText;
1043                submitBtn.disabled = false;
1044            }
1045        });
1046        
1047        // Close modal when clicking outside
1048        window.onclick = function(event) {
1049            const modal = document.getElementById('claimVenueModal');
1050            if (event.target === modal) {
1051                closeClaimModal();
1052            }
1053        }
1054        
1055        // Check if current user owns this venue
1056        function checkVenueOwnership(venueSlug) {
1057            const sessionToken = localStorage.getItem('venue_session_token');
1058            const storedVenueId = localStorage.getItem('venue_id');
1059            
1060            // If user is logged in and this is their venue, show manage button
1061            if (sessionToken && storedVenueId === venueSlug) {
1062                document.getElementById('manage-venue-section').style.display = 'block';
1063                document.getElementById('claim-venue-section').style.display = 'none';
1064                document.getElementById('request-login-section').style.display = 'none';
1065            }
1066        }
1067        
1068        // Request new login link
1069        async function requestLoginLink() {
1070            const messageDiv = document.getElementById('login-link-message');
1071            
1072            try {
1073                messageDiv.style.display = 'block';
1074                messageDiv.style.background = 'rgba(16, 185, 129, 0.1)';
1075                messageDiv.style.border = '1px solid rgba(16, 185, 129, 0.3)';
1076                messageDiv.style.color = '#10b981';
1077                messageDiv.textContent = 'Sending login link...';
1078                
1079                // Use location-aware venue_id (matches what was used during claim)
1080                const slugToSend = (currentVenueData && currentVenueData.venue_id) ? currentVenueData.venue_id : venueSlug;
1081                
1082                const response = await fetch(`${API_BASE_URL}/venue/request-login`, {
1083                    method: 'POST',
1084                    headers: {
1085                        'Content-Type': 'application/json'
1086                    },
1087                    body: JSON.stringify({
1088                        venue_slug: slugToSend,
1089                        base_slug: venueSlug
1090                    })
1091                });
1092                
1093                const data = await response.json();
1094                
1095                if (response.ok) {
1096                    messageDiv.textContent = '✓ Login link sent! Check your email (including spam folder).';
1097                } else {
1098                    throw new Error(data.error || 'Failed to send login link');
1099                }
1100                
1101            } catch (error) {
1102                console.error('Error requesting login link:', error);
1103                messageDiv.style.background = 'rgba(239, 68, 68, 0.1)';
1104                messageDiv.style.border = '1px solid rgba(239, 68, 68, 0.3)';
1105                messageDiv.style.color = '#ef4444';
1106                messageDiv.textContent = error.message || 'Failed to send login link. Please try again.';
1107            }
1108        }
1109        
1110        // ============================================================================
1111        // EVENT DETAIL MODAL
1112        // ============================================================================
1113        
1114        function openEventModal(eventId) {
1115            const event = allVenueEvents.find(e => e.event_id === eventId);
1116            if (!event) return;
1117            
1118            // Create modal if it doesn't exist
1119            let modal = document.getElementById('eventDetailsModal');
1120            if (!modal) {
1121                modal = document.createElement('div');
1122                modal.id = 'eventDetailsModal';
1123                modal.className = 'modal';
1124                modal.innerHTML = '<div class="modal-content"><span class="close" onclick="closeEventModal()">&times;</span>
1124<div id="modalEventContent"></div></div>';
1125                document.body.appendChild(modal);
1126                
1127                modal.addEventListener('click', function(e) {
1128                    if (e.target === modal) closeEventModal();
1129                });
1130            }
1131            
1132            const content = document.getElementById('modalEventContent');
1133            content.innerHTML = buildEventModalContent(event);
1134            modal.style.display = 'block';
1135            document.body.style.overflow = 'hidden';
1136            
1137            if (event.latitude && event.longitude) {
1138                ensureLeaflet().then(() => {
1139                    setTimeout(() => {
1140                        const modalMap = L.map('modalEventMap').setView([event.latitude, event.longitude], 13);
1141                        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(modalMap);
1142                        L.marker([event.latitude, event.longitude]).addTo(modalMap)
1143                            .bindPopup(`<strong>${toTitleCase(event.venue || '')}</strong><br>${event.city || ''}, ${event.state || ''}`)
1144                            .openPopup();
1145                    }, 100);
1146                });
1147            }
1148            
1149            history.pushState({ eventId }, '', `?event=${eventId}`);
1150        }
1151        
1152        function closeEventModal() {
1153            const modal = document.getElementById('eventDetailsModal');
1154            if (modal) {
1155                modal.style.display = 'none';
1156                document.body.style.overflow = 'auto';
1157                const url = new URL(window.location);
1158                url.searchParams.delete('event');
1159                history.pushState({}, '', url);
1160            }
1161        }
1162        
1163        function buildEventModalContent(event) {
1164            const bandName = event.band || 'Unknown Band';
1165            const bandSlugLocal = slugify(bandName);
1166            const venueName = toTitleCase(event.venue || 'Unknown Venue');
1167            const eventUrl = `https://grassgigs.com/venue/${venueSlug}?event=${event.event_id}`;
1168            const eventText = `${bandName} at ${venueName} - ${formatDate(event.date, event.end_date)}`;
1169            
1170            let html = '<div class="modal-event-details">';
1171            
1172            // Band name + venue
1173            html += `<h2 style="color: #10b981; margin: 0 0 0.5rem 0; font-size: 1.75rem; text-align: center;">${bandSlugLocal ? `<a href="/band/${bandSlugLocal}" style="color: #10b981; text-decoration: none;">${bandName}</a>` : bandName}</h2>`;
1174            html += `<h3 style="color: #38bdf8; text-align: center; margin: 0 0 1.5rem 0; font-size: 1.25rem; font-weight: normal;">at ${venueName}</h3>`;
1175            
1176            // Info panel
1177            html += '<div style="background: rgba(56, 189, 248, 0.1); border-radius: 8px; padding: 1.5rem; margin: 1.5rem 0;">';
1178            html += '<div style="display: grid; gap: 1rem;">';
1179            
1180            // Date & Time
1181            if (event.date) {
1182                html += `<div style="display: flex; align-items: start; gap: 0.75rem;">
1183                    <span style="font-size: 1.5rem;"><img src="/custom-calendar.png" alt="" style="height:1.2em;width:auto;vertical-align:middle;"></span>
1184                    <div>
1185                        <div style="color: #94a3b8; font-size: 0.875rem;">Date & Time</div>
1186                        <div style="color: #f1f5f9; font-weight: 600;">${formatDate(event.date, event.end_date)}${event.time && event.time !== '00:00' ? ' at ' + formatTime(event.time) : ''}</div>
1187                    </div>
1188                </div>`;
1189            }
1190            
1191            // Venue
1192            html += `<div style="display: flex; align-items: start; gap: 0.75rem;">
1193                <span style="font-size: 1.5rem;"><img src="/custom-map-pin.png" alt="" style="height:1.2em;width:auto;vertical-align:middle;"></span>
1194                <div>
1195                    <div style="color: #94a3b8; font-size: 0.875rem;">Venue</div>
1196                    <div style="color: #f1f5f9; font-weight: 600;">${venueName}</div>
1197                </div>
1198            </div>`;
1199            
1200            // Location
1201            if (event.city && event.state) {
1202                const fullAddress = [event.address, event.city, event.state].filter(
1202Boolean).join(', ');
1203                html += `<div style="display: flex; align-items: start; gap: 0.75rem;">
1204                    <span style="font-size: 1.5rem;">📍</span>
1205                    <div>
1206                        <div style="color: #94a3b8; font-size: 0.875rem;">Location</div>
1207                        <div style="color: #f1f5f9; font-weight: 600;">${event.city}, ${event.state}</div>
1208                        <div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem;">
1209                            <button onclick="navigator.clipboard.writeText('${fullAddress.replace(/'/g, "\\'")}'); this.textContent='✓ Copied!'; setTimeout(() => { this.textContent='Copy Address'; }, 1500);" style="background: rgba(56, 189, 248, 0.2); color: #38bdf8; border: none; padding: 0.4rem 0.75rem; border-radius: 6px; cursor: pointer; font-size: 0.8rem;">Copy Address</button>
1210                            <a href="https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(fullAddress)}" target="_blank" rel="noopener noreferrer" style="background: rgba(16, 185, 129, 0.2); color: #10b981; border: none; padding: 0.4rem 0.75rem; border-radius: 6px; text-decoration: none; font-size: 0.8rem; font-weight: 500;">Get Directions</a>
1211                        </div>
1212                    </div>
1213                </div>`;
1214            }
1215            
1216            html += '</div></div>';
1217            
1218            // Map
1219            if (event.latitude && event.longitude) {
1220                html += '<div id="modalEventMap" style="height: 250px; border-radius: 8px; margin-bottom: 1.5rem; border: 1px solid rgba(56, 189, 248, 0.2);"></div>';
1221            }
1222            
1223            // Action buttons
1224            html += '<div style="display: flex; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 1.5rem;">';
1225            if (bandSlugLocal) html += `<a href="/band/${bandSlugLocal}" style="flex: 1; min-width: 140px; text-align: center; background: rgba(16, 185, 129, 0.1); color: #10b981; padding: 0.75rem; border-radius: 8px; text-decoration: none; font-weight: 600;">View Band Page</a>`;
1226            if (event.source_url) html += `<a href="${event.source_url}" target="_blank" rel="noopener noreferrer" style="flex: 1; min-width: 140px; text-align: center; background: rgba(56, 189, 248, 0.1); color: #38bdf8; padding: 0.75rem; border-radius: 8px; text-decoration: none; font-weight: 600;">Band Website</a>`;
1227            if (event.ticket_url) html += `<a href="${event.ticket_url}" target="_blank" rel="noopener" style="flex: 1; min-width: 140px; text-align: center; background: rgba(251, 191, 36, 0.1); color: #fbbf24; padding: 0.75rem; border-radius: 8px; text-decoration: none; font-weight: 600;">🎫 Get Tickets</a>`;
1228            html += '</div>';
1229            
1230            // Share buttons
1231            html += '<div style="margin-top: 1.5rem; text-align: center; padding-top: 1rem; border-top: 1px solid rgba(148, 163, 184, 0.2);">';
1232            html += '<span style="display: block; font-size: 0.75rem; color: rgba(255, 255, 255, 0.5); margin-bottom: 0.5rem; font-weight: 500;">Share this event:</span>';
1233            html += '<div style="display: flex; gap: 0.5rem; justify-content: center; align-items: center;">';
1234            html += `<button onclick="window.open('https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(eventUrl)}', '_blank')" title="Share on Facebook" aria-label="Share on Facebook" style="padding: 0.5rem; border: none; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; background: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.7); transition: all 0.2s;" onmouseover="this.style.background='rgba(24, 119, 242, 0.2)'; this.style.color='#1877f2'" onmouseout="this.style.background='rgba(255, 255, 255, 0.1)'; this.style.color='rgba(255, 255, 255, 0.7)'"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg></button>`;
1235            html += `<button onclick="window.open('https://twitter.com/intent/tweet?text=${encodeURIComponent(eventText)}&url=${encodeURIComponent(eventUrl)}', '_blank')" title="Share on X/Twitter" aria-label="Share on X/Twitter" style="padding: 0.5rem; border: none; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; background: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.7); transition: all 0.2s;" onmouseover="this.style.background='rgba(255, 255, 255, 0.2)'; this.style.color='#ffffff'" onmouseout="this.style.background='rgba(255, 255, 255, 0.1)'; this.style.color='rgba(255, 255, 255, 0.7)'"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg></button>`;
1236            html += `<button onclick="window.open('https://wa.me/?text=${encodeURIComponent(eventText + ' ' + eventUrl)}', '_blank')" title="Share on WhatsApp" aria-label="Share on WhatsApp" style="padding: 0.5rem; border: none; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; background: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.7); transition: all 0.2s;" onmouseover="this.style.background='rgba(37, 211, 102, 0.2)'; this.style.color='#25d366'" onmouseout="this.style.background='rgba(255, 255, 255, 0.1)'; this.style.color='rgba(255, 255, 255, 0.7)'"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.
1236198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/></svg></button>`;
1237            html += '</div></div>';
1238            
1239            // Calendar export
1240            html += '<div style="text-align: center; margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid rgba(148, 163, 184, 0.2);">';
1241            html += '<p style="color: #94a3b8; font-size: 0.875rem; margin-bottom: 0.75rem;">Add to your calendar:</p>';
1242            html += '<div style="display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap;">';
1243            html += `<button onclick="downloadEventICS('${event.event_id}')" style="display: inline-flex; align-items: center; gap: 0.5rem; background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.3); padding: 0.625rem 1.25rem; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 0.875rem;" onmouseover="this.style.background='rgba(16, 185, 129, 0.2)'" onmouseout="this.style.background='rgba(16, 185, 129, 0.1)'">📅 Download .ics</button>`;
1244            html += `<button onclick="addEventToGoogleCalendar('${event.event_id}')" style="display: inline-flex; align-items: center; gap: 0.5rem; background: rgba(56, 189, 248, 0.1); color: #38bdf8; border: 1px solid rgba(56, 189, 248, 0.3); padding: 0.625rem 1.25rem; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 0.875rem;" onmouseover="this.style.background='rgba(56, 189, 248, 0.2)'" onmouseout="this.style.background='rgba(56, 189, 248, 0.1)'"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M19.5 3h-1V1.5h-2V3h-9V1.5h-2V3h-1C3.12 3 2 4.12 2 5.5v14C2 20.88 3.12 22 4.5 22h15c1.38 0 2.5-1.12 2.5-2.5v-14C22 4.12 20.88 3 19.5 3zm0 16.5h-15V8h15v11.5z"/></svg> Google Calendar</button>`;
1245            html += '</div></div>';
1246            
1247            // Subscribe CTA
1248            html += `<div style="text-align: center; margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid rgba(148, 163, 184, 0.2);">
1249                <a href="/?subscribe=venue&location=${encodeURIComponent(venueSlug)}#subscribe" style="display: inline-block; background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); color: white !important; padding: 0.75rem 1.5rem; border-radius: 8px; text-decoration: none; font-weight: 600; box-shadow: 0 4px 6px rgba(0,0,0,0.2);">📧 Get Notified of New Shows</a>
1250            </div>`;
1251            
1252            html += '</div>';
1253            return html;
1254        }
1255        
1256        // Calendar export helpers
1257        function formatICSDate(date) {
1258            return date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
1259        }
1260        
1261        function formatICSDateOnly(date) {
1262            return date.getFullYear().toString() +
1263                String(date.getMonth() + 1).padStart(2, '0') +
1264                String(date.getDate()).padStart(2, '0');
1265        }
1266        
1267        function downloadEventICS(eventId) {
1268            const event = allVenueEvents.find(e => e.event_id === eventId);
1269            if (!event) return;
1270            
1271            // On Android, open Google Calendar directly
1272            if (/Android/i.test(navigator.userAgent)) {
1273                addEventToGoogleCalendar(eventId);
1274                return;
1275            }
1276            
1277            const parts = event.date.split('-');
1278            const startDate = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
1279            const bandName = event.band || 'Live Music';
1280            const venueName = toTitleCase(event.venue || 'Venue TBA');
1281            const location = [event.venue, event.city, event.state].filter(
1281Boolean).join(', ');
1282            
1283            let dtstart, dtend;
1284            if (event.time && event.time !== '00:00') {
1285                const timeParts = event.time.split(':');
1286                startDate.setHours(parseInt(timeParts[0]), parseInt(timeParts[1]), 0);
1287                dtstart = 'DTSTART:' + formatICSDate(startDate);
1288                const endDate = new Date(startDate.getTime() + 3 * 60 * 60 * 1000);
1289                dtend = 'DTEND:' + formatICSDate(endDate);
1290            } else {
1291                dtstart = 'DTSTART;VALUE=DATE:' + formatICSDateOnly(startDate);
1292                if (event.end_date && event.end_date !== event.date) {
1293                    const endParts = event.end_date.split('-');
1294                    const endDate = new Date(parseInt(endParts[0]), parseInt(endParts[1]) - 1, parseInt(endParts[2]) + 1);
1295                    dtend = 'DTEND;VALUE=DATE:' + formatICSDateOnly(endDate);
1296                } else {
1297                    const endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
1298                    dtend = 'DTEND;VALUE=DATE:' + formatICSDateOnly(endDate);
1299                }
1300            }
1301            
1302            const ics = [
1303                'BEGIN:VCALENDAR',
1304                'VERSION:2.0',
1305                'PRODID:-//GrassGigs//Events//EN',
1306                'BEGIN:VEVENT',
1307                'UID:' + eventId + '@grassgigs.com',
1308                dtstart,
1309                dtend,
1310                'SUMMARY:' + bandName + ' at ' + venueName,
1311                'LOCATION:' + location,
1312                'URL:https://grassgigs.com/venue/' + venueSlug + '?event=' + eventId,
1313                'END:VEVENT',
1314                'END:VCALENDAR'
1315            ].join('\r\n');
1316            
1317            const blob = new Blob([ics], { type: 'text/calendar;charset=utf-8' });
1318            const url = URL.createObjectURL(blob);
1319            const a = document.createElement('a');
1320            a.href = url;
1321            a.download = slugify(bandName) + '-' + event.date + '.ics';
1322            document.body.appendChild(a);
1323            a.click();
1324            document.body.removeChild(a);
1325            URL.revokeObjectURL(url);
1326        }
1327        
1328        function addEventToGoogleCalendar(eventId) {
1329            const event = allVenueEvents.find(e => e.event_id === eventId);
1330            if (!event) return;
1331            
1332            const bandName = event.band || 'Live Music';
1333            const venueName = toTitleCase(event.venue || 'Venue TBA');
1334            const location = [event.venue, event.city, event.state].filter(Boolean).join(', ');
1335            const title = bandName + ' at ' + venueName;
1336            
1337            const parts = event.date.split('-');
1338            let dates;
1339            if (event.time && event.time !== '00:00') {
1340                const startDate = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
1341                const timeParts = event.time.split(':');
1342                startDate.setHours(parseInt(timeParts[0]), parseInt(timeParts[1]), 0);
1343                const endDate = new Date(startDate.getTime() + 3 * 60 * 60 * 1000);
1344                dates = formatICSDate(startDate) + '/' + formatICSDate(endDate);
1345            } else {
1346                const startStr = formatICSDateOnly(new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])));
1347                if (event.end_date && event.end_date !== event.date) {
1348                    const endParts = event.end_date.split('-');
1349                    const endStr = formatICSDateOnly(new Date(parseInt(endParts[0]), parseInt(endParts[1]) - 1, parseInt(endParts[2]) + 1));
1350                    dates = startStr + '/' + endStr;
1351                } else {
1352                    const endDate = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]) + 1);
1353                    dates = startStr + '/' + formatICSDateOnly(endDate);
1354                }
1355            }
1356            
1357            const url = `https://www.google.com/calendar/render?action=TEMPLATE&text=${encodeURIComponent(title)}&dates=${dates}&location=${encodeURIComponent(location)}&details=${encodeURIComponent('Event found on GrassGigs: https://grassgigs.com/venue/' + venueSlug)}`;
1358            window.open(url, '_blank');
1359        }
1360        
1361        // Handle browser back/forward for event modal
1362        window.addEventListener('popstate', function(e) {
1363            if (e.state && e.state.eventId) {
1364                openEventModal(e.state.eventId);
1365            } else {
1366                closeEventModal();
1367            }
1368        });
1369        
1370        // Close modal on Escape
1371        document.addEventListener('keydown', function(e) {
1372            if (e.key === 'Escape') closeEventModal();
1373        });
1374        
1375        // ============================================================================
1376        // VENUE ANALYTICS TRACKING
1377        // ============================================================================
1378        
1379        // Track venue profile page view (ONLY for premium venues)
1380        function trackVenueView(venueSlug, isPremium) {
1381            if (!venueSlug || !isPremium) return;
1382            
1383            const deviceType = /Mobile|Android|iPhone/i.test(navigator.userAgent) ? 'mobile' : 
1384                              /Tablet|iPad/i.test(navigator.userAgent) ? 'tablet' : 'desktop';
1385            
1386            fetch(`${API_BASE_URL}/venue/analytics/track-view`, {
1387                method: 'POST',
1388                headers: {'Content-Type': 'application/json'},
1389                body: JSON.stringify({
1390                    venue_id: venueSlug,
1391                    referrer: document.referrer || 'direct',
1392                    device_type: deviceType
1393                })
1394            }).catch(err => console.log('Analytics tracking failed:', err));
1395        }
1396        
1397        // Track event click from venue context (ONLY for premium venues)
1398        function trackVenueEventClick(venueSlug, eventId, isPremium) {
1399            if (!venueSlug || !eventId || !isPremium) return;
1400            
1401            fetch(`${API_BASE_URL}/venue/analytics/track-click`, {
1402                method: 'POST',
1403                headers: {'Content-Type': 'application/json'},
1404                body: JSON.stringify({
1405                    venue_id: venueSlug,
1406                    event_id: eventId
1407                })
1408            }).catch(err => console.log('Analytics tracking failed:', err));
1409        }
1410        
1411        // Track conversion actions (ONLY for premium venues)
1412        function trackVenueConversion(venueSlug, conversionType, eventId, isPremium) {
1413            if (!venueSlug || !conversionType || !isPremium) return;
1414            
1415            const payload = {
1416                venue_id: venueSlug,
1417                conversion_type: conversionType
1418            };
1419            
1420            if (eventId) {
1421                payload.event_id = eventId;
1422            }
1423            
1424            fetch(`${API_BASE_URL}/venue/analytics/track-conversion`, {
1425                method: 'POST',
1426                headers: {'Content-Type': 'application/json'},
1427                body: JSON.stringify(payload)
1428            }).catch(err => console.log('Conversion tracking failed:', err));
1429        }
1430    </script>
1430
1431    
1432    <!-- Load Shared Footer -->
1433    
1433<script src="/footer-loader.js"></script>
1433
1434    
1434<script src="/mobile-nav-loader.js"></script>
1434
1435</body>
1436</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.