PageSourceSearch

https://gapubdef.org/js/components.js?v=7d897a7e

js gapubdef.org collected 2026-10-04 01:39:31 UTC 105,077 bytes, 2,160 lines download raw bytes

1/**
2 * GPDC Dynamic Components Loader
3 * Loads shared header, footer, hero components, and CMS-managed images across all pages
4 */
5
6(function() {
7    'use strict';
8
9    // Hardcoded fallback heroes (used if _data/heroes.json fetch fails)
10    const fallbackHeroes = {
11        'home': { image: 'images/heroes/scales-of-justice.jpg', position: 'center center' },
12        'about': { image: 'images/heroes/georgia-capitol.jpg', label: 'ABOUT US', title: 'Who We Are', subtitle: 'Providing high-quality legal defense for individuals accused of crimes who cannot afford representation.', position: 'center 30%' },
13        'team': { image: 'images/heroes/team-collaboration.jpg', label: 'ABOUT US', title: 'Our Team', subtitle: 'Meet the dedicated professionals working to ensure justice for all Georgians.', position: 'center center' },
14        'divisions': { image: 'images/heroes/modern-office.jpg', label: 'ABOUT US', title: 'Divisions and Services', subtitle: 'Specialized divisions working together to provide comprehensive legal defense across Georgia.', position: 'center center' },
15        'clients': { image: 'images/heroes/community-support.jpg', label: 'CLIENTS & FAMILIES', title: 'Clients & Families', subtitle: 'Re
15sources and information for those we serve and their loved ones.', position: 'center center' },
16        'find-defender': { image: 'images/heroes/find-defender.jpg', label: 'CLIENTS & FAMILIES', title: 'Find Your Public Defender', subtitle: 'Locate the public defender office serving your county.', position: 'center center' },
17        'apply': { image: 'images/heroes/community-support.jpg', label: 'CLIENTS & FAMILIES', title: 'Applying For Legal Representation', subtitle: 'Learn how to request a public defender if you cannot afford an attorney.', position: 'center center' },
18        'legal-process': { image: 'images/heroes/scales-of-justice.jpg', label: 'CLIENTS & FAMILIES', title: 'The Legal Process', subtitle: 'Understanding the steps in a criminal case in Georgia.', position: 'center center' },
19        'faq': { image: 'images/heroes/community-support.jpg', label: 'CLIENTS & FAMILIES', title: 'FAQs and Resources', subtitle: 'Answers to common questions about public defense in Georgia.', position: 'center center' },
20        'careers': { image: 'images/heroes/careers.jpg', label: 'CAREERS', title: 'Join Our Team', subtitle: 'Build a meaningful career defending the constitutional rights of Georgians.', position: 'center center' },
21        'positions': { image: 'images/heroes/careers.jpg', label: 'CAREERS', title: 'Available Positions', subtitle: 'Current job openings across Georgia\'s public defender offices.', position: 'center center' },
22        'newsroom': { image: 'images/heroes/newsroom.jpg', label: 'NEWSROOM', title: 'News & Updates', subtitle: 'The latest news and announcements from GPDC.', position: 'center center' },
23        'contact': { image: 'images/heroes/contact.jpg', label: 'CONTACT', title: 'Contact Us', subtitle: 'Get in touch with the Georgia Public Defender Council.', position: 'center center' },
24        'division-default': { image: 'images/heroes/division-default.jpg', position: 'center center' },
25        'circuit-default': { image: 'images/heroes/scales-of-justice.jpg', position: 'center center' },
26        'article-default': { image: 'images/heroes/newsroom.jpg', position: 'center center' }
27    };
28
29    // Active heroes object (will be overwritten by CMS data if available)
30    let heroes = fallbackHeroes;
31
32    // Determine base path based on current page location
33    function getBasePath() {
34        const path = window.location.pathname;
35        const depth = (path.match(/\//g) || []).length - 1;
36
37        // Division pages can be nested (e.g. /divisions/training/leadership-academy),
38        // so derive the number of "../" from the actual directory depth.
39        if (path.includes('/divisions/')) {
40            return '../'.repeat(Math.max(1, depth));
41        }
42
43        // Other one-level-deep sections
44        if (path.includes('/local-offices/') ||
45            path.includes('/articles/') ||
46            path.includes('/circuits/') ||
47            path.includes('/tools/')) {
48            return '../';
49        }
50        return '';
51    }
52
53    // Fetch a JSON data file with fallback
54    async function fetchData(basePath, dataPath) {
55        try {
56            // _headers already serves /_data/* as must-revalidate, so a plain URL
57            // gets a cheap 304 instead of a fresh download on every page view.
58            const response = await fetch(basePath + dataPath);
59            if (!response.ok) throw new Error('Data file not found');
60            return await response.json();
61        } catch (error) {
62            console.warn('Failed to load data file: ' + dataPath, error);
63            return null;
64        }
65    }
66
67    function setText(target, value) {
68        if (!target || typeof value !== 'string') return;
69        target.textContent = value;
70    }
71
72    function setHtml(target, value) {
73        if (!target || typeof value !== 'string') return;
74        target.innerHTML = value;
75    }
76
77    function setLabelText(target, value) {
78        if (!target || typeof value !== 'string') return;
79        var textNode = Array.prototype.find.call(target.childNodes, function(node) {
80            return node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0;
81        });
82        if (textNode) {
83            textNode.textContent = value + ' ';
84            return;
85        }
86        target.insertBefore(document.createTextNode(value + ' '), target.firstChild);
87    }
88
89    function normalizeHeroConfig(entry) {
90        if (!entry) return null;
91        return entry.page_hero ? entry.page_hero : entry;
92    }
93
94    async function loadHeroesConfig(basePath) {
95        // New structure: one hero file per page under _data/heroes/*.json
96        const manifest = await fetchData(basePath, '_data/heroes/index.json');
97        if (manifest && Array.isArray(manifest.items) && manifest.items.length > 0) {
98            const loaded = await Promise.all(manifest.items.map(async function(item) {
99                const data = await fetchData(basePath, '_data/heroes/' + item.file);
100                return { key: item.key, data: normalizeHeroConfig(data) };
101            }));
102
103            const nextHeroes = {};
104            loaded.forEach(function(entry) {
105                if (entry.data && entry.data.image) {
106                    nextHeroes[entry.key] = entry.data;
107                }
108            });
109
110            if (Object.keys(nextHeroes).length > 0) {
111                return nextHeroes;
112            }
113        }
114
115        // Legacy fallback: monolithic heroes object
116        const legacy = await fetchData(basePath, '_data/heroes.json');
117        if (legacy) return legacy;
118        return null;
119    }
120
121    // Load a component and insert it into the page
122    async function loadComponent(elementId, componentPath, basePath) {
123        const element = document.getElementById(elementId);
124        if (!element) return;
125
126        try {
127            const response = await fetch(basePath + componentPath + '?v=8');
128            if (!response.ok) throw new Error('Component not found');
129
130            let html = await response.text();
131
132            // Replace {{BASE}} placeholder with actual base path
133            html = html.replace(/\{\{BASE\}\}/g, basePath);
134
135            element.innerHTML = html;
136
137            // Dispatch event when component is loaded
138            element.dispatchEvent(new CustomEvent('componentLoaded', {
139                bubbles: true,
140                detail: { component: componentPath }
141            }));
142        } catch (error) {
143            console.warn(`Failed to load component: ${componentPath}`, error);
144        }
145    }
146
147    // The skip link has to exist before anything else focusable, and the header is
148    // injected, so it is prepended here rather than shipped in components/header.html.
149    function ensureSkipLink(headerEl) {
150        if (!headerEl || headerEl.querySelector('a.skip')) return;
151
152        const skip = document.createElement('a');
153        skip.className = 'skip';
154        skip.href = '#main';
155        skip.textContent = 'Skip to content';
156        headerEl.insertBefore(skip, headerEl.firstChild);
157    }
158
159    // Every template lays its sections out as direct <body> siblings between
160    // #site-header and the footer, so there is no existing element to promote to
161    // <main> — the landmark has to be built around them. The run is re-parented in
162    // one synchronous block so media elements are not paused (the spec only pauses
163    // a media element still detached at the next stable state).
164    function ensureMainLandmark() {
165        const existing = document.querySelector('main');
166        if (existing) {
167            if (!existing.id) existing.id = 'main';
168            if (!existing.hasAttribute('tabindex')) existing.setAttribute('tabindex', '-1');
169            return;
170        }
171
172        const header = document.getElementById('site-header');
173        const end = document.getElementById('site-connect') || document.getElementById('site-footer');
174        if (!header || !end || header.parentNode !== end.parentNode) return;
175
176        const content = [];
177        for (let node = header.nextElementSibling; node && node !== end; node = node.nextElementSibling) {
178            content.push(node);
179        }
180        if (content.length === 0) return;
181
182        const main = document.createElement('main');
183        main.id = 'main';
184        main.setAttribute('tabindex', '-1');
185        header.parentNode.insertBefore(main, header.nextSibling);
186
187        const frag = document.createDocumentFragment();
188        content.forEach(function(node) { frag.appendChild(node); });
189        main.appendChild(frag);
190    }
191
192    // Load hero section based on data-hero attribute
193    function loadHero() {
194        const heroElement = document.getElementById('page-hero');
195        if (!heroElement) {
196            return;
197        }
198
199        const heroKey = heroElement.dataset.hero;
200        if (!heroKey) {
201            console.warn('Hero element found but no data-hero attribute');
202            return;
203        }
204
205        if (!heroes[heroKey]) {
206            console.warn('Hero config not found for key:', heroKey);
207            return;
208        }
209
210        const config = heroes[heroKey];
211        const basePath = getBasePath();
212
213        // Resolve image path — prepend basePath for relative local paths
214        const imgSrc = (config.image.startsWith('http') || config.image.startsWith('/'))
215            ? config.image
216            : basePath + config.image;
217
218        // Build hero HTML
219        const html = `
220            <div class="page-hero-bg">
221                <img src="${imgSrc}" alt="${config.title || 'Hero image'}" style="object-position: ${config.position || 'center center'}">
222            </div>
223            <div class="page-hero-content">
224                ${config.label ? `<p class="label">${config.label}</p>` : ''}
225                ${config.title ? `<h1 class="page-hero-title">${config.title}</h1>` : ''}
226                ${config.subtitle ? `<p class="page-hero-subtitle">${config.subtitle}</p>` : ''}
227            </div>
228        `;
229
230        heroElement.innerHTML = html;
231    }
232
233    // Apply hero image position to existing hero sections (for pages n
233ot using component)
234    function applyHeroPosition() {
235        const heroImg = document.querySelector('.page-hero-bg img');
236        if (!heroImg) return;
237
238        // Check for data-position attribute on the hero section or image
239        const heroSection = document.querySelector('.page-hero');
240        const position = heroSection?.dataset.position || heroImg.dataset.position;
241
242        if (position) {
243            heroImg.style.objectPosition = position;
244        }
245    }
246
247    // Default placeholder image for members without photos
248    var PLACEHOLDER_IMG = 'https://cdn.prod.website-files.com/66c9595306b0d169d1677ecc/696679f045b5c1a68607201e_general-img-landscape.png';
249
250    // Helper: escape text for HTML attributes
251    function escAttr(str) {
252        return str.replace(/'/g, "\\'").replace(/"/g, '&quot;');
253    }
254
255    // Map of leadership member id -> their public division page.
256    // Source of truth is _data/team-divisions.json, shared with the intranet
257    // hub so a director change only has to be made in one place. Falls back to
258    // an empty map if the fetch fails; cards then render without the link.
259    let teamDivisionLinks = {};
260
261
262    function getTeamDivisionLink(member, basePath) {
263        const link = teamDivisionLinks[member.id];
264        if (!link) return null;
265        return {
266            href: basePath + link.href,
267            label: link.label || 'View Division',
268            ariaLabel: link.ariaLabel || link.label || 'View Division'
269        };
270    }
271
272    // Generate a single team card HTML
273    function renderTeamCard(member, divisionLink) {
274        var actionHtml = divisionLink ?
275            '<a class="btn btn-outline team-division-btn" href="' + divisionLink.href + '" aria-label="' + escAttr(divisionLink.ariaLabel) + '">' +
276                '<span>' + divisionLink.label + '</span>' +
277                '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
278                    '<path d="M5 12h14M12 5l7 7-7 7"/>' +
279                '</svg>' +
280            '</a>' :
281            '';
282
283        return '<div class="team-card-new fade-in" data-person="' + member.id + '">' +
284            '<div class="team-image">' +
285                '<img loading="lazy" decoding="async" width="260" height="260" src="' + (member.photo || PLACEHOLDER_IMG) + '" alt="' + escAttr(member.name) + '">' +
286            '</div>' +
287            '<div class="team-info">' +
288                '<h3>' + member.name + '</h3>' +
289                '<p class="team-title">' + member.title + '</p>' +
290                actionHtml +
291            '</div>' +
292        '</div>';
293    }
294
295    // Generate the featured executive card HTML
296    function renderFeaturedCard(member) {
297        return '<div class="featured-image">' +
298                '<img loading="lazy" decoding="async" width="300" height="300" src="' + (member.photo || PLACEHOLDER_IMG) + '" alt="' + escAttr(member.name) + '">' +
299            '</div>' +
300            '<div class="featured-content">' +
301                '<p class="label">' + member.title.toUpperCase() + '</p>' +
302                '<h2>' + member.name + '</h2>' +
303                '<p class="featured-bio">Leading the Georgia Public Defender Council with a commitment to ensuring quality legal representation for all Georgians. Focused on augmenting indigent defense services and promoting rehabilitative outcomes throughout the state.</p>' +
304                '<a href="assets/docs/Omotayo-Alli-Biography.pdf" class="btn btn-primary" style="width: auto; display: inline-flex;" download>' +
305                    '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
306                        '<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/>' +
307                        '<polyline points="7 10 12 15 17 10"/>' +
308                        '<line x1="12" y1="15" x2="12" y2="3"/>' +
309                    '</svg>' +
310                    '<span>Download Bio</span>' +
311                '</a>' +
312            '</div>';
313    }
314
315    // Load leadership data and dynamically render team sections
316    async function loadLeadership(basePath) {
317        // Only run on team.html
318        const pagePath = window.location.pathname;
319        if (!pagePath.endsWith('/team.html') && !pagePath.endsWith('/team')) return;
320
321        const [data, divMap] = await Promise.all([
322            fetchData(basePath, '_data/leadership.json'),
323            fetchData(basePath, '_data/team-divisions.json')
324        ]);
325        if (divMap && divMap.members) teamDivisionLinks = divMap.members;
326        if (!data || !data.members) return;
327
328        const members = data.members;
329
330        // Build teamImages lookup for the contact modal
331        var teamImagesMap = {};
332        members.forEach(function(m) {
333            teamImagesMap[m.id] = m.photo;
334        });
335
336        // Expose to global scope so the inline modal script can use it
337        window.teamImages = teamImagesMap;
338
339        // Split members by role (preserve JSON order within each group)
340        var executives = members.filter(function(m) { return m.role === 'Executive'; });
341        var directors = members.filter(function(m) { return m.role !== 'Executive'; });
342
343        // Render featured executive card (first executive in the list)
344        var featuredEl = document.getElementById('featuredExecutive');
345        if (featuredEl && executives.length > 0) {
346            featuredEl.setAttribute('data-person', executives[0].id);
347            featuredEl.innerHTML = renderFeaturedCard(executives[0]);
348        }
349
350        // Render executives grid (all executives, in JSON order)
351        var execGrid = document.getElementById('executivesGrid');
352        if (execGrid) {
353            execGrid.innerHTML = executives.map(function(member) { return renderTeamCard(member); }).join('');
354        }
355
356        // Render directors/managers grid (in JSON order)
357        var dirGrid = document.getElementById('directorsGrid');
358        if (dirGrid) {
359            dirGrid.innerHTML = directors.map(function(member) {
360                return renderTeamCard(member, getTeamDivisionLink(member, basePath));
361            }).join('');
362        }
363
364        // Trigger fade-in animations for dynamically added cards
365        requestAnimationFrame(function() {
366            document.querySelectorAll('.team-card-new.fade-in, .featured-card.fade-in').forEach(function(el) {
367                el.classList.add('visible');
368            });
369        });
370    }
371
372    // Division slug → leadership member ID mapping (fallback only)
373    const divisionLeaderMap = {
374        'training': 'tbertram',
375        'appellate-defense': 'charrison',
376        'capital-defense': 'nstudelska',
377        'customer-support': 'jwalker',
378        'mental-health-defense': 'srhinehart',
379        'youth-defense': 'gmurray',
380        'conflict': 'aragas',
381        'administration': 'lonley'
382    };
383
384    // Division slug → title keywords used to infer the current leader assignment.
385    // This allows CMS updates to propagate without changing hardcoded IDs.
386    const divisionTitleKeywords = {
387        'training': ['training', 'professional development'],
388        'appellate-defense': ['appellate'],
389        'capital-defense': ['capital'],
390        'customer-support': ['customer support', 'compliance', 'client support'],
391        'mental-health-defense': ['mental health'],
392        'youth-defense': ['youth'],
393        'conflict': ['conflict'],
394        'administration': ['administration']
395    };
396
397    // Load divisions data and update division images
398    // Cross-references leadership.json for director photos
399    async function loadDivisions(basePath) {
400        var pagePath = window.location.pathname;
401        var isDivisionsOverview = pagePath.endsWith('/divisions.html') || pagePath.endsWith('/divisions');
402        var isDivisionPage = pagePath.includes('/divisions/');
403
404        if (!isDivisionsOverview && !isDivisionPage) return;
405
406        // Fetch divisions, leadership, and editable division copy data in parallel
407        var [divData, leaderData, divisionContentData] = await Promise.all([
408            fetchData(basePath, '_data/divisions.json'),
409            fetchData(basePath, '_data/leadership.json'),
410            fetchData(basePath, '_data/division-content.json')
411        ]);
412
413        if (!divData || !divData.divisions) return;
414
415        var divisions = divData.divisions;
416
417        // Build leadership lookup by ID
418        var leaderLookup = {};
419        if (leaderData && leaderData.members) {
420            leaderData.members.forEach(function(m) {
421                leaderLookup[m.id] = m;
422            });
423        }
424
425        function normalizeName(value) {
426            return (value || '')
427                .toLowerCase()
428                .replace(/[^a-z\s'-]/g, '')
429                .replace(/\s+/g, ' ')
430                .trim();
431        }
432
433        function getDivisionLeaderByCardName(profileCard) {
434            if (!profileCard || !leaderData || !leaderData.members) return null;
435            var nameEl = profileCard.querySelector('.leader-profile-info h3') ||
436                         profileCard.querySelector('.pd-leader-info h3');
437            if (!nameEl) return null;
438
439            var rendered = normalizeName(nameEl.textContent || '');
440            if (!rendered) return null;
441
442            var exact = leaderData.members.find(function(member) {
443                return normalizeName(member.name) === rendered;
444            });
445            if (exact) return exact;
446
447            var renderedLast = rendered.split(' ').pop();
448            if (!renderedLast) return null;
449
450            return leaderData.members.find(function(member) {
451                var last = normalizeName(member.name).split(' ').pop();
452                return last && last === renderedLast;
453            }) || null;
454        }
455
456        // Helper: find leader assignment for a division slug.
457        // 1) Prefer matching the currently rendered card name.
458        // 2) Then title keyword matching (dynamic and CMS-friendly).
459        // 3) Fall back to static ID map for legacy data.
460        function getDivisionLeader(slug, profileCard) {
461            var byCardName = getDivisionLeaderByCardName(profileCard);
462            if (byCardName) return byCardName;
463
464            var keywords = divisionTitleKeywords[slug] || [];
465            if (keywords.length > 0 && leaderData && leaderData.members) {
466                var byTitle = leaderData.members.find(function(member) {
467                    var title = (member.title || '').toLowerCase();
468                    return keywords.some(function(keyword) {
469                        return title.indexOf(keyword) !== -1;
470                    });
471                });
472                if (byTitle) return byTitle;
473            }
474
475            var leaderId = divisionLeaderMap[slug];
476            if (leaderId && leaderLookup[leaderId]) {
477                return leaderLookup[leaderId];
478            }
479            return null;
480        }
481
482        if (isDivisionsOverview) {
483            // Optional editable section copy for the overview page
484            if (divisionContentData && divisionContentData.overview) {
485                var overviewHeader = document.querySelector('.content-section .section-header');
486                if (overviewHeader) {
487                    if (divisionContentData.overview.section_label) {
488                        var overviewLabel = overviewHeader.querySelector('.label');
489                        if (overviewLabel) overviewLabel.textContent = divisionContentData.overview.section_label;
490                    }
491                    if (divisionContentData.overview.title) {
492                        var overviewTitle = overviewHeader.querySelector('.section-title');
493                        if (overviewTitle) overviewTitle.textContent = divisionContentData.overview.title;
494                    }
495                    if (divisionContentData.overview.subtitle) {
496                        var overviewSubtitle = overviewHeader.querySelector('.section-subtitle');
497                        if (overviewSubtitle) overviewSubtitle.textContent = divisionContentData.overview.subtitle;
498                    }
499                }
500            }
501
502            // Update division cards on the overview page
503            divisions.forEach(function(div) {
504                var card = document.querySelector('[data-division="' + div.slug + '"]');
505                if (!card) return;
506
507                // Update header image
508                var headerImg = card.querySelector('.division-card-image img');
509                if (headerImg) {
510                    headerImg.src = div.header_image;
511                }
512
513                // Update director photo from leadership data
514                var directorImg = card.querySelector('.division-card-leader img');
515                if (directorImg) {
516                    var leader = getDivisionLeader(div.slug, null);
517                    if (leader && leader.photo) {
518                        directorImg.src = leader.photo;
519                    }
520                }
521
522                // Optional editable card copy
523                if (divisionContentData && divisionContentData.overview && divisionContentData.overview.cards) {
524                    var cardCopy = divisionContentData.overview.cards.find(function(c) { return c.slug === div.slug; });
525                    if (cardCopy) {
526                        var contentWrap = card.querySelector('.division-card-content');
527                        if (contentWrap) {
528                            if (cardCopy.label) {
529                                var labelEl = contentWrap.querySelector('.label');
530                                if (labelEl) labelEl.textContent = cardCopy.label;
531                            }
532                            if (cardCopy.title) {
533                                var titleEl = contentWrap.querySelector('h3');
534                                if (titleEl) titleEl.textContent = cardCopy.title;
535                            }
536                            if (cardCopy.description) {
537                                var descEl = contentWrap.querySelector('p:not(.label)');
538                                if (descEl) descEl.textContent = cardCopy.description;
539                            }
540                        }
541                    }
542                }
543            });
544        }
545
546        if (isDivisionPage) {
547            // Extract the division slug from the URL
548            var slug = pagePath.split('/divisions/')[1].replace('.html', '').replace(/\/$/, '');
549            var divMatch = divisions.find(function(d) { return d.slug === slug; });
550            if (!divMatch) return;
551
552            // Update leader profile image (supports both standard and training page layouts)
553            var profileCard = document.querySelector('[data-division="' + slug + '"]');
554            if (profileCard) {
555                var leader = getDivisionLeader(slug, profileCard);
556                if (!leader) return;
557
558                var profileImg = profileCard.querySelector('.leader-profile-image img') ||
559                                 profileCard.querySelector('.pd-leader-image img');
560                if (profileImg && leader.photo) {
561                    profileImg.src = leader.photo;
562                    profileImg.alt = leader.name || profileImg.alt;
563                }
564
565                var profileName = profileCard.querySelector('.leader-profile-info h3') ||
566                                  profileCard.querySelector('.pd-leader-info h3');
567                if (profileName && leader.name) {
568                    profileName.textContent = leader.name;
569                }
570
571                var profileTitle = profileCard.querySelector('.leader-title');
572                if (profileTitle && leader.title) {
573                    profileTitle.textContent = leader.title;
574                }
575
576                // Optional editable body/section copy for standard division pages
577                if (divisionContentData && divisionContentData.pages) {
578                    var pageCopy = divisionContentData.pages.find(function(p) { return p.slug === slug; });
579                    if (pageCopy) {
580                        if (pageCopy.hero_description) {
581                            var heroDesc = document.querySelector('.division-hero .hero-description');
582                            if (heroDesc) heroDesc.textContent = pageCopy.hero_description;
583                        }
584
585                        var aboutSectionHeader = document.querySelector('.division-about .section-header');
586                        if (aboutSectionHeader) {
587                            if (pageCopy.about_section_label) {
588                                var aboutLabel = aboutSectionHeader.querySelector('.label');
589                                if (aboutLabel) aboutLabel.textContent = pageCopy.about_section_label;
590                            }
591                            if (pageCopy.about_heading) {
592                                var aboutHeading = aboutSectionHeader.querySelector('.section-title');
593                                if (aboutHeading) aboutHeading.textContent = pageCopy.about_heading;
594                            }
595                        }
596
597                        var aboutBlocks = document.querySelectorAll('.division-about .division-about-text');
598                        if (aboutBlocks.length >= 1) {
599                            if (pageCopy.mission_title) {
600                                var missionTitleEl = aboutBlocks[0].querySelector('h3');
601                                if (missionTitleEl) missionTitleEl.textContent = pageCopy.mission_title;
602                            }
603                            if (pageCopy.mission_body) {
604                                var missionBodyEl = aboutBlocks[0].querySelector('p');
605                                if (missionBodyEl) missionBodyEl.textContent = pageCopy.mission_body;
606                            }
607                        }
608                        if (aboutBlocks.length >= 2) {
609                            if (pageCopy.expertise_title) {
610                                var expertiseTitleEl = aboutBlocks[1].querySelector('h3');
611                                if (expertiseTitleEl) expertiseTitleEl.textContent = pageCopy.expertise_title;
612                            }
613                            if (pageCopy.expertise_body) {
614                                var expertiseBodyEl = aboutBlocks[1].querySelector('p');
615                                if (expertiseBodyEl) expertiseBodyEl.textContent = pageCopy.expertise_body;
616                            }
617                        }
618
619                        var servicesHeader = document.querySelector('.division-services .section-header');
620                        if (servicesHeader) {
621                            if (pageCopy.services_section_label) {
622                                var servicesLabel = servicesHeader.querySelector('.label');
623                                if (servicesLabel) servicesLabel.textContent = pageCopy.services_section_label;
624                            }
625                            if (pageCopy.services_heading) {
626                                var servicesHeading = servicesHeader.querySelector('.section-title');
627                                if (servicesHeading) servicesHeading.textContent = pageCopy.services_heading;
628                            }
629                            if (pageCopy.services_subtitle) {
630                                var servicesSubtitle = servicesHeader.querySelector('.section-subtitle');
631                                if (servicesSubtitle) servicesSubtitle.textContent = pageCopy.services_subtitle;
632                            }
633                        }
634
635                        if (Array.isArray(pageCopy.services)) {
636                            var serviceCards = document.querySelectorAll('.division-services .service-card-modern');
637                            pageCopy.services.forEach(function(service, idx) {
638                                if (!serviceCards[idx]) return;
639                                if (service.title) {
640                                    var sTitle = serviceCards[idx].querySelector('h3');
641                                    if (sTitle) sTitle.textContent = service.title;
642                                }
643                                if (service.description) {
644                                    var sDesc = serviceCards[idx].querySelector('p');
645                                    if (sDesc) sDesc.textContent = service.description;
646                                }
647                            });
648                        }
649                    }
650                }
651            }
652        }
653    }
654
655    // Only these pages read CMS copy below. Keyed by filename so every other page
656    // can skip the manifest instead of pulling all seven content files for nothing.
657    var PAGE_CONTENT_KEYS = {
658        'about.html': 'about',
659        'clients.html': 'clients',
660        'apply.html': 'apply',
661        'legal-process.html': 'legal_process',
662        'faq.html': 'faq',
663        'contact.html': 'contact',
664        'careers.html': 'careers'
665    };
666
667    async function loadPageContent(basePath) {
668        var pagePath = window.location.pathname.split('/').pop() || 'index.html';
669        var pageKey = PAGE_CONTENT_KEYS[pagePath];
670        if (!pageKey) return;
671
672        var pages = null;
673
674        // Preferred structure: one file per page under _data/page-content/*.json
675        var manifest = await fetchData(basePath, '_data/page-content/index.json');
676        var item = manifest && Array.isArray(manifest.items)
677            ? manifest.items.filter(function(candidate) { return candidate.key === pageKey; })[0]
678            : null;
679        if (item) {
680            var entry = await fetchData(basePath, '_data/page-content/' + item.file);
681            if (entry) {
682                pages = {};
683                pages[pageKey] = entry;
684            }
685        }
686
687        // Backward compatibility: legacy monolithic file
688        if (!pages) {
689            var legacyContentData = await fetchData(basePath, '_data/page-content.json');
690            if (legacyContentData && legacyContentData.pages) {
691                pages = legacyContentData.pages;
692            }
693        }
694
695        if (!pages) return;
696
697        if (pagePath === 'about.html' && pages.about) {
698            var about = pages.about;
699            setText(document.querySelector('.ds-hero__label'), about.hero_label);
700            setHtml(document.querySelector('.ds-hero__title'), about.hero_title_html);
701            setText(document.querySelector('.ds-hero__subtitle'), about.hero_subtitle);
702            setText(document.querySelector('.content-section .content-text .label'), about.mission_label);
703            setHtml(document.querySelector('.content-section .content-text .section-title'), about.mission_title_html);
704
705            var missionParagraphs = document.querySelectorAll('.content-section .content-text .content-paragraph');
706            if (Array.isArray(about.mission_paragraphs)) {
707                about.mission_paragraphs.forEach(function(p, idx) {
708                    setText(missionParagraphs[idx], p);
709                });
710            }
711
712            setText(document.querySelector('#principles .section-header .label'), about.values_label);
713            setText(document.querySelector('#principles .section-header .section-title'), about.values_title);
714            if (Array.isArray(about.values_cards)) {
715                var aboutValueCards = document.querySelectorAll('#principles .values-card');
716                about.values_cards.forEach(function(card, idx) {
717                    var cardEl = aboutValueCards[idx];
718                    if (!cardEl) return;
719                    setText(cardEl.querySelector('.values-card__title'), card.title);
720                    setText(cardEl.querySelector('.values-card__desc'), card.description);
721                });
722            }
723        }
724
725        if (pagePath === 'clients.html' && pages.clients) {
726            var clients = pages.clients;
727            setText(document.querySelector('.ds-hero__label'), clients.hero_label);
728            setHtml(document.querySelector('.ds-hero__title'), clients.hero_title_html);
729            setText(document.querySelector('.ds-hero__subtitle'), clients.hero_subtitle);
730
731            /* The lead hero carries whichever installment of the series is
732               current, so its link has to move with it. Without this the copy
733               would update from the CMS each week while the button kept
734               pointing at the first article — the worst of both, because it
735               would look right. Both fields are optional; absent, the markup's
736               own href and label stand. */
737            var heroCta = document.querySelector('[data-hero-cta]');
738            if (heroCta && typeof clients.hero_cta_href === 'string' && clients.hero_cta_href) {
739                heroCta.setAttribute('href', clients.hero_cta_href);
740            }
741            setText(document.querySelector('[data-hero-cta-label]'), clients.hero_cta_label);
742            setText(document.querySelector('.cf-journey .section-header .label'), clients.journey_label);
743            setText(document.querySelector('.cf-journey .section-header .section-title'), clients.journey_title);
744
745            if (Array.isArray(clients.journey_cards)) {
746                var journeyCards = document.querySelectorAll('.cf-journey-gri
746d .cf-tile');
747                clients.journey_cards.forEach(function(card, idx) {
748                    var cardEl = journeyCards[idx];
749                    if (!cardEl) return;
750                    setText(cardEl.querySelector('h3'), card.title);
751                    setText(cardEl.querySelector('p'), card.description);
752                    setLabelText(cardEl.querySelector('.link-arrow'), card.link_label);
753                });
754            }
755
756            setText(document.querySelector('.content-section-alt .section-header .label'), clients.family_support_label);
757            setText(document.querySelector('.content-section-alt .section-header .section-title'), clients.family_support_title);
758            if (Array.isArray(clients.family_support_cards)) {
759                var supportCards = document.querySelectorAll('.content-section-alt .values-card');
760                clients.family_support_cards.forEach(function(card, idx) {
761                    var cardEl = supportCards[idx];
762                    if (!cardEl) return;
763                    setText(cardEl.querySelector('.values-card__title'), card.title);
764                    setText(cardEl.querySelector('.values-card__desc'), card.description);
765                });
766            }
767        }
768
769        if (pagePath === 'apply.html' && pages.apply) {
770            var apply = pages.apply;
771            setText(document.querySelector('.apply-intro h2'), apply.intro_title);
772            setText(document.querySelector('.apply-intro p'), apply.intro_body);
773
774            if (Array.isArray(apply.steps)) {
775                var applySteps = document.querySelectorAll('.apply-steps .apply-step');
776                apply.steps.forEach(function(step, idx) {
777                    var stepEl = applySteps[idx];
778                    if (!stepEl) return;
779                    setText(stepEl.querySelector('.step-content h3'), step.title);
780                    setText(stepEl.querySelector('.step-content p'), step.description);
781                });
782            }
783
784            setText(document.querySelector('.apply-note h4'), apply.note_title);
785            setText(document.querySelector('.apply-note p'), apply.note_body);
786            setText(document.querySelector('.apply-cta h3'), apply.cta_title);
787            setText(document.querySelector('.apply-cta p'), apply.cta_body);
788            setText(document.querySelector('.apply-cta .btn span'), apply.cta_button_label);
789        }
790
791        if (pagePath === 'legal-process.html' && pages.legal_process) {
792            var process = pages.legal_process;
793            setText(document.querySelector('.process-intro h2'), process.intro_title);
794            setText(document.querySelector('.process-intro p'), process.intro_body);
795            if (Array.isArray(process.steps)) {
796                var processSteps = document.querySelectorAll('.process-timeline .process-step');
797                process.steps.forEach(function(step, idx) {
798                    var stepEl = processSteps[idx];
799                    if (!stepEl) return;
800                    setText(stepEl.querySelector('h3'), step.title);
801                    setText(stepEl.querySelector('p'), step.description);
802                });
803            }
804        }
805
806        if (pagePath === 'faq.html' && pages.faq) {
807            var faq = pages.faq;
808            if (Array.isArray(faq.items)) {
809                var faqItems = document.querySelectorAll('.faq-section .faq-item');
810                faq.items.forEach(function(item, idx) {
811                    var itemEl = faqItems[idx];
812                    if (!itemEl) return;
813                    setText(itemEl.querySelector('.faq-question span'), item.question);
814                    setHtml(itemEl.querySelector('.faq-answer p'), item.answer_html);
815                });
816            }
817            setText(document.querySelector('.cta-card h2'), faq.cta_title);
818            setText(document.querySelector('.cta-card > p'), faq.cta_body);
819            setText(document.querySelector('.cta-contact-item:first-child span'), faq.cta_phone);
820            setText(document.querySelector('.cta-contact-item:last-child span'), faq.cta_email);
821            setText(document.querySelector('.cta-card .btn span'), faq.cta_button_label);
822        }
823
824        if (pagePath === 'contact.html' && pages.contact) {
825            var contact = pages.contact;
826            setText(document.querySelector('.contact-hero-hq-info .label'), contact.headquarters_label);
827            setText(document.querySelector('.contact-hero-hq-info h2'), contact.headquarters_city);
828            setText(document.querySelector('.contact-hero-hq-detail:nth-child(1) span'), contact.headquarters_address);
829            setText(document.querySelector('.contact-hero-hq-detail:nth-child(2) a'), contact.headquarters_phone);
830            setText(document.querySelector('.contact-hero-hq-detail:nth-child(3) span'), contact.headquarters_hours);
831            setText(document.querySelector('.contact-hq-banner-cta-title'), contact.office_cta_title);
832            setText(document.querySelector('.contact-hq-banner-cta-sub'), contact.office_cta_subtitle);
833            setText(document.querySelector('.contact-cta-label'), contact.service_label);
834            setText(document.querySelector('.contact-cta-phone'), contact.service_phone);
835            setText(document.querySelector('.contact-cta-hours'), contact.service_hours);
836            setText(document.querySelector('.contact-cta-actions .btn span'), contact.service_button_label);
837            setText(document.querySelector('.contact-cta-email'), contact.service_email_label);
838
839            if (Array.isArray(contact.contact_rows)) {
840                var rows = document.querySelectorAll('.contact-list .contact-list-row');
841                contact.contact_rows.forEach(function(row, idx) {
842                    var rowEl = rows[idx];
843                    if (!rowEl) return;
844                    setText(rowEl.querySelector('h4'), row.title);
845                    setText(rowEl.querySelector('p'), row.description);
846                });
847            }
848
849            setText(document.querySelector('.contact-faq-header .label'), contact.faq_label);
850            setText(document.querySelector('.contact-faq-header h3'), contact.faq_title);
851            if (Array.isArray(contact.faq_items)) {
852                var contactFaqItems = document.querySelectorAll('.contact-faq-list .faq-item');
853                contact.faq_items.forEach(function(item, idx) {
854                    var itemEl = contactFaqItems[idx];
855                    if (!itemEl) return;
856                    setText(itemEl.querySelector('.faq-question span'), item.question);
857                    setHtml(itemEl.querySelector('.faq-answer p'), item.answer_html);
858                });
859            }
860            setText(document.querySelector('.contact-faq-more .btn span'), contact.faq_button_label);
861        }
862
863        if (pagePath === 'careers.html' && pages.careers) {
864            var careers = pages.careers;
865            setText(document.querySelector('.ds-hero__label'), careers.hero_label);
866            setHtml(document.querySelector('.ds-hero__title'), careers.hero_title_html);
867            setText(document.querySelector('.ds-hero__subtitle'), careers.hero_subtitle);
868            setText(document.querySelector('.ds-hero__actions .btn.btn-primary span'), careers.hero_primary_button_label);
869            setText(document.querySelector('.ds-hero__actions .hero-fmpd-btn'), careers.hero_secondary_button_label);
870
871            setText(document.querySelector('.content-section-alt .section-header .label'), careers.benefits_label);
872            setText(document.querySelector('.content-section-alt .section-header .section-title'), careers.benefits_title);
873            if (Array.isArray(careers.benefits_cards)) {
874                var benefitCards = document.querySelectorAll('.values-grid--3col .values-card');
875                careers.benefits_cards.forEach(function(card, idx) {
876                    var cardEl = benefitCards[idx];
877                    if (!cardEl) return;
878                    setText(cardEl.querySelector('.values-card__title'), card.title);
879                    setText(cardEl.querySelector('.values-card__desc'), card.description);
880                });
881            }
882
883            setText(document.querySelector('.cr-paths .section-header .label'), careers.career_paths_label);
884            setText(document.querySelector('.cr-paths .section-header .section-title'), careers.career_paths_title);
885            if (Array.isArray(careers.career_paths)) {
886                var pathCards = document.querySelectorAll('.cr-paths-grid .cr-path-card');
887                careers.career_paths.forEach(function(path, idx) {
888                    var pathEl = pathCards[idx];
889                    if (!pathEl) return;
890                    setText(pathEl.querySelector('h3'), path.title);
891                    setText(pathEl.querySelector('p'), path.description);
892                    setLabelText(pathEl.querySelector('.link-arrow'), path.link_label);
893                });
894            }
895        }
896    }
897
898    async function loadDivisionSpecialContent(basePath) {
899        var pagePath = window.location.pathname;
900        var key = null;
901        if (pagePath.endsWith('/divisions/training.html') || pagePath.endsWith('/divisions/training')) {
902            key = 'training';
903        } else if (pagePath.endsWith('/divisions/social-services.html') || pagePath.endsWith('/divisions/social-services')) {
904            key = 'social-services';
905        }
906
907        if (!key) return;
908
909        var manifest = await fetchData(basePath, '_data/division-special-content/index.json');
910        if (!manifest || !Array.isArray(manifest.items)) return;
911        var item = manifest.items.find(function(it) { return it.key === key; });
912        if (!item || !item.file) return;
913
914        var data = await fetchData(basePath, '_data/division-special-content/' + item.file);
915        if (!data) return;
916
917        if (key === 'training') {
918            if (data.hero) {
919                setText(document.querySelector('.pd-hero-content .label'), data.hero.label);
920                setText(document.querySelector('.pd-hero-content h1'), data.hero.title);
921                setText(document.querySelector('.pd-hero-content .hero-description'), data.hero.description);
922                setText(document.querySelector('.pd-hero-actions .btn.btn-primary span'), data.hero.primary_button);
923                setText(document.querySelector('.pd-hero-actions .btn.btn-outline span'), data.hero.secondary_button);
924            }
925
926            if (Array.isArray(data.stats)) {
927                var stats = document.querySelectorAll('.pd-stats .pd-stat');
928                data.stats.forEach(function(stat, idx) {
929                    var statEl = stats[idx];
930                    if (!statEl) return;
931                    setText(statEl.querySelector('.number'), stat.number);
932                    setText(statEl.querySelector('.label'), stat.label);
933                });
934            }
935
936            if (data.benefits) {
937                setText(document.querySelector('.pd-benefits-section .section-header .label'), data.benefits.label);
938                setText(document.querySelector('.pd-benefits-section .section-header .section-title'), data.benefits.title);
939                if (Array.isArray(data.benefits.cards)) {
940                    var benefitCards = document.querySelectorAll('.pd-benefits-section .fade-in.fade-in-delay-1 > div');
941                    data.benefits.cards.forEach(function(card, idx) {
942                        var cardEl = benefitCards[idx];
943                        if (!cardEl) return;
944                        setText(cardEl.querySelector('h3'), card.title);
945                        setText(cardEl.querySelector('p'), card.description);
946                    });
947                }
948            }
949
950            if (data.events) {
951                setText(document.querySelector('#events .section-header .label'), data.events.label);
952                setText(document.querySelector('#events .section-header .section-title'), data.events.title);
953                setText(document.querySelector('#events .section-header .section-subtitle'), data.events.subtitle);
954            }
955
956            if (data.programs) {
957                setText(document.querySelector('#programs .section-header .label'), data.programs.label);
958                setText(document.querySelector('#programs .section-header .section-title'), data.programs.title);
959                setText(document.querySelector('#programs .section-header .section-subtitle'), data.programs.subtitle);
960                if (Array.isArray(data.programs.cards)) {
961                    var programCards = document.querySelectorAll('#programs .program-card');
962                    data.programs.cards.forEach(function(card, idx) {
963                        var cardEl = programCards[idx];
964                        if (!cardEl) return;
965                        setText(cardEl.querySelector('h3'), card.title);
966                        setText(cardEl.querySelector('.program-card-body > p'), card.description);
967                        if (Array.isArray(card.features)) {
968                            var features = cardEl.querySelectorAll('.program-features li');
969                            card.features.forEach(function(feature, fIdx) {
970                                var featureEl = features[fIdx];
971                                if (!featureEl) return;
972                                var textNode = Array.prototype.find.call(featureEl.childNodes, function(node) {
973                                    return node.nodeType === Node.TEXT_NODE && node.textContent.trim().length > 0;
974                                });
975                                if (textNode) {
976                                    textNode.textContent = ' ' + feature;
977                                } else {
978                                    featureEl.appendChild(document.createTextNode(' ' + feature));
979                                }
980                            });
981                        }
982                    });
983                }
984            }
985        }
986
987        if (key === 'social-services') {
988            if (data.hero) {
989                setText(document.querySelector('.division-hero-content .label'), data.hero.label);
990                setText(document.querySelector('.division-hero-content h1'), data.hero.title);
991                setText(document.querySelector('.division-hero-content .hero-description'), data.hero.description);
992                setText(document.querySelector('.division-hero-actions .btn.btn-primary span'), data.hero.primary_button);
993                setText(document.querySelector('.division-hero-actions .btn.btn-outline span'), data.hero.secondary_button);
994            }
995
996            if (data.crisis_banner) {
997                setText(document.querySelector('.crisis-banner-content h3'), data.crisis_banner.title);
998                setText(document.querySelector('.crisis-banner-content p'), data.crisis_banner.description);
999                if (Array.isArray(data.crisis_banner.links)) {
1000                    var crisisLinks = document.querySelectorAll('.crisis-banner-links .crisis-banner-link');
1001                    data.crisis_banner.links.forEach(function(link, idx) {
1002                        var linkEl = crisisLinks[idx];
1003                        if (!linkEl) return;
1004                        setText(linkEl.querySelector('.crisis-banner-number'), link.number);
1005                        setText(linkEl.querySelector('.crisis-banner-name'), link.name);
1006                    });
1007                }
1008            }
1009
1010            if (Array.isArray(data.managers)) {
1011                var managers = document.querySelectorAll('.managers-bar .manager-card');
1012                data.managers.forEach(function(manager, idx) {
1013                    var card = managers[idx];
1014                    if (!card) return;
1015                    setText(card.querySelector('.manager-info h3'), manager.name);
1016                    setText(card.querySelector('.manager-role'), manager.role);
1017                    setText(card.querySelector('.manager-program'), manager.program);
1018                });
1019            }
1020
1021            if (data.overview) {
1022                setText(document.querySelector('.wwd-hero-label'), data.overview.label);
1023                setText(document.querySelector('.wwd-hero-text h2'), data.overview.title);
1024                setText(document.querySelector('.wwd-hero-text p'), data.overview.description);
1025                setText(document.querySelector('.wwd-services-label'), data.overview.support_label);
1026                setText(document.querySelector('.wwd-services-header h3'), data.overview.support_title);
1027                if (Array.isArray(data.overview.support_items)) {
1028                    var supportItems = document.querySelectorAll('.wwd-services-list li');
1029                    data.overview.support_items.forEach(function(item, idx) {
1030                        var itemEl = supportItems[idx];
1031                        if (!itemEl) return;
1032                        var dot = itemEl.querySelector('.wwd-list-dot');
1033                        itemEl.textContent = item;
1034                        if (dot) itemEl.prepend(dot);
1035                    });
1036                }
1037            }
1038
1039            if (data.program_basics) {
1040                setText(document.querySelector('.program-basics-top .label'), data.program_basics.label);
1041                setText(document.querySelector('.program-basics-top h2'), data.program_basics.title);
1042                setText(document.querySelector('.program-basics-top p:not(.label)'), data.program_basics.description);
1043                setText(document.querySelector('.program-basics-quiz h3'), data.program_basics.quiz_title);
1044                setText(document.querySelector('.program-basics-quiz p'), data.program_basics.quiz_description);
1045                setLabelText(document.querySelector('.quiz-start-btn'), data.program_basics.quiz_button);
1046                if (Array.isArray(data.program_basics.cards)) {
1047                    var basicCards = document.querySelectorAll('.program-basics-grid .program-basics-card');
1048                    data.program_basics.cards.forEach(function(card, idx) {
1049                        var cardEl = basicCards[idx];
1050                        if (!cardEl) return;
1051                        setText(cardEl.querySelector('h3'), card.title);
1052                        setText(cardEl.querySelector('p'), card.description);
1053                    });
1054                }
1055            }
1056
1057            if (data.css_experience) {
1058                setText(document.querySelector('#css-hero .css-exp-hero-badge'), data.css_experience.hero_badge);
1059                setHtml(document.querySelector('#css-hero h2'), data.css_experience.hero_title_html);
1060                setText(document.querySelector('#css-hero .css-exp-hero-sub'), data.css_experience.hero_subtitle);
1061                setText(document.querySelector('#css-services .css-exp-section-label'), data.css_experience.services_label);
1062                setText(document.querySelector('#css-services .css-what-we-do h3'), data.css_experience.services_title);
1063                if (Array.isArray(data.css_experience.services_intro)) {
1064                    var introPs = document.querySelectorAll('#css-services .css-what-we-do p');
1065                    data.css_experience.services_intro.forEach(function(p, idx) {
1066                        setText(introPs[idx], p);
1067                    });
1068                }
1069                if (Array.isArray(data.css_experience.services_cards)) {
1070                    var serviceCards = document.querySelectorAll('#css-services .css-expand-card');
1071                    data.css_experience.services_cards.forEach(function(card, idx) {
1072                        var cardEl = serviceCards[idx];
1073                        if (!cardEl) return;
1074                        setText(cardEl.querySelector('.css-expand-card-body h4'), card.title);
1075                        setText(cardEl.querySelector('.css-expand-card-body p'), card.summary);
1076                        setText(cardEl.querySelector('.css-expand-detail p'), card.detail);
1077                    });
1078                }
1079                setText(document.querySelector('#css-process .css-exp-section-label'), data.css_experience.process_label);
1080                setText(document.querySelector('#css-process .css-process-section h3'), data.css_experience.process_title);
1081                if (Array.isArray(data.css_experience.process_steps)) {
1082                    var processSteps = document.querySelectorAll('#css-process .css-scroll-timeline-step');
1083                    data.css_experience.process_steps.forEach(function(step, idx) {
1084                        var stepEl = processSteps[idx];
1085                        if (!stepEl) return;
1086                        setText(stepEl.querySelector('h4'), step.title);
1087                        setText(stepEl.querySelector('p'), step.description);
1088                    });
1089                }
1090                setText(document.querySelector('#css-contact .css-exp-section-label'), data.css_experience.contact_label);
1091                setHtml(document.querySelector('#css-contact h3'), data.css_experience.contact_title_html);
1092                setText(document.querySelector('#css-contact p'), data.css_experience.contact_description);
1093                setText(document.querySelector('#css-contact a'), data.css_experience.contact_email);
1094            }
1095
1096            if (data.ladders_experience) {
1097                setText(document.querySelector('#ld-hero .ld-hero-badge'), data.ladders_experience.hero_badge);
1098                setHtml(document.querySelector('#ld-hero h2'), data.ladders_experience.hero_title_html);
1099                setText(document.querySelector('#ld-hero .ld-hero-sub'), data.ladders_experience.hero_subtitle);
1100                setLabelText(document.querySelector('#ld-hero .ld-hero-cta'), data.ladders_experience.hero_button);
1101                setText(document.querySelector('#ld-pillars .ld-section-label'), data.ladders_experience.pillars_label);
1102                setText(document.querySelector('#ld-pillars h3'), data.ladders_experience.pillars_title);
1103                setText(document.querySelector('#ld-pillars > p'), data.ladders_experience.pillars_description);
1104                if (Array.isArray(data.ladders_experience.pillars)) {
1105                    var pillars = document.querySelectorAll('#ld-pillars .ld-
1105pillar-card');
1106                    data.ladders_experience.pillars.forEach(function(pillar, idx) {
1107                        var pillarEl = pillars[idx];
1108                        if (!pillarEl) return;
1109                        setText(pillarEl.querySelector('h4'), pillar.title);
1110                        setText(pillarEl.querySelector('p'), pillar.description);
1111                    });
1112                }
1113                setText(document.querySelector('#ld-quiz .ld-section-label'), data.ladders_experience.quiz_label);
1114                setText(document.querySelector('#ld-quiz .ld-quiz-header h3'), data.ladders_experience.quiz_title);
1115                setText(document.querySelector('#ld-quiz .ld-quiz-header p'), data.ladders_experience.quiz_description);
1116                setText(document.querySelector('#ld-journey .ld-section-label'), data.ladders_experience.journey_label);
1117                setText(document.querySelector('#ld-journey h3'), data.ladders_experience.journey_title);
1118                setText(document.querySelector('#ld-journey > p'), data.ladders_experience.journey_description);
1119                if (Array.isArray(data.ladders_experience.journey_steps)) {
1120                    var journeySteps = document.querySelectorAll('#ld-journey .ld-timeline-step');
1121                    data.ladders_experience.journey_steps.forEach(function(step, idx) {
1122                        var stepEl = journeySteps[idx];
1123                        if (!stepEl) return;
1124                        setText(stepEl.querySelector('.ld-timeline-month'), step.month);
1125                        setText(stepEl.querySelector('.ld-timeline-title'), step.title);
1126                        setText(stepEl.querySelector('.ld-timeline-desc'), step.description);
1127                    });
1128                }
1129                setText(document.querySelector('#ld-testimonial blockquote'), data.ladders_experience.testimonial_quote);
1130                setText(document.querySelector('#ld-testimonial cite span:last-child'), data.ladders_experience.testimonial_name);
1131                setHtml(document.querySelector('#ld-apply h3'), data.ladders_experience.apply_title_html);
1132                setText(document.querySelector('#ld-apply p'), data.ladders_experience.apply_description);
1133                setLabelText(document.querySelector('#ld-apply .ld-btn-primary'), data.ladders_experience.apply_primary_button);
1134                setLabelText(document.querySelector('#ld-apply .ld-btn-secondary'), data.ladders_experience.apply_secondary_button);
1135            }
1136        }
1137    }
1138
1139    // Initialize components when DOM is ready
1140    async function init() {
1141        const basePath = getBasePath();
1142
1143        // Inject Connect With Us section before footer (skip on contact page)
1144        const footerEl = document.getElementById('site-footer');
1145        const isContactPage = window.location.pathname.includes('contact');
1146        if (footerEl && !document.getElementById('site-connect') && !isContactPage) {
1147            const connectEl = document.createElement('div');
1148            connectEl.id = 'site-connect';
1149            footerEl.parentNode.insertBefore(connectEl, footerEl);
1150        }
1151
1152        // Homepage hero command bar (mobile-only inline search)
1153        initNavCommandSearch(document.getElementById('heroCommandBar'));
1154
1155        // Start shared component fetches immediately so the shell does not
1156        // wait on hero/CMS requests before the nav can render.
1157        const headerPromise = loadComponent('site-header', 'components/header.html', basePath);
1158        const connectPromise = loadComponent('site-connect', 'components/connect.html', basePath);
1159        const footerPromise = loadComponent('site-footer', 'components/footer.html', basePath);
1160
1161        headerPromise.then(() => {
1162            ensureSkipLink(document.getElementById('site-header'));
1163            ensureMainLandmark();
1164            initNavigation();
1165            decodeEmails();
1166            // Needs the nav in the DOM before it can reveal the dot.
1167            initJobAlerts(basePath);
1168        });
1169
1170        Promise.all([connectPromise, footerPromise]).then(() => {
1171            decodeEmails();
1172        });
1173
1174        // The hero config is a manifest plus one file per page; only pages that
1175        // actually render a hero from it need to pay for those requests.
1176        if (document.getElementById('page-hero')) {
1177            const heroData = await loadHeroesConfig(basePath);
1178            if (heroData) {
1179                heroes = heroData;
1180            }
1181        }
1182
1183        // Load hero component (if using data-hero attribute)
1184        loadHero();
1185
1186        // Apply hero position (for pages with inline hero)
1187        applyHeroPosition();
1188
1189        // Load CMS-managed data (runs in parallel, non-blocking)
1190        loadLeadership(basePath);
1191        loadDivisions(basePath);
1192        loadPageContent(basePath);
1193        loadDivisionSpecialContent(basePath);
1194        loadCircuits(basePath);
1195        loadCouncil(basePath);
1196        loadCouncilMeetings(basePath);
1197    }
1198
1199    // ==================== NEW-JOB INDICATOR ====================
1200    // Diffs the active-requisition ids in _data/job-alerts.json against the
1201    // ids this visitor has already been shown (localStorage). The indicator
1202    // therefore means "posted since you last looked", not "we are hiring" —
1203    // and it clears itself the moment the visitor reaches a page that shows
1204    // the new opening, so it only ever appears on that first visit.
1205    var JOB_ALERT_KEY = 'gpdc:jobAlerts:seen';
1206    var JOB_ALERT_CAP = 400; // keep the key bounded as postings churn
1207
1208    function readSeenJobs() {
1209        try {
1210            var raw = window.localStorage.getItem(JOB_ALERT_KEY);
1211            if (!raw) return null; // null = no record yet (first ever visit)
1212            var parsed = JSON.parse(raw);
1213            return Array.isArray(parsed) ? parsed : null;
1214        } catch (e) {
1215            return null; // private mode, disabled storage, or corrupt value
1216        }
1217    }
1218
1219    function writeSeenJobs(ids) {
1220        // Dedupe — callers concat freely, and a duplicate would otherwise eat
1221        // a slot against the cap and eventually evict a still-live id, which
1222        // would re-alert a posting the visitor has already been shown.
1223        var deduped = [];
1224        var already = Object.create(null);
1225        ids.forEach(function(id) {
1226            if (already[id]) return;
1227            already[id] = true;
1228            deduped.push(id);
1229        });
1230        try {
1231            window.localStorage.setItem(JOB_ALERT_KEY, JSON.stringify(deduped.slice(-JOB_ALERT_CAP)));
1232        } catch (e) {
1233            // Storage unavailable — the indicator simply won't persist.
1234        }
1235    }
1236
1237    function showJobAlertEls(selector) {
1238        var els = document.querySelectorAll(selector);
1239        els.forEach(function(el) { el.removeAttribute('hidden'); });
1240        return els.length;
1241    }
1242
1243    async function initJobAlerts(basePath) {
1244        var data = await fetchData(basePath, '_data/job-alerts.json');
1245        if (!data || !Array.isArray(data.all) || !data.all.length) return;
1246
1247        var seen = readSeenJobs();
1248
1249        // First ever visit: record the current slate silently. Without this
1250        // every new visitor would get a "new postings" dot for a listing they
1251        // never had the chance to miss, which makes the signal meaningless.
1252        if (seen === null) {
1253            writeSeenJobs(data.all);
1254            return;
1255        }
1256
1257        var seenSet = Object.create(null);
1258        seen.forEach(function(id) { seenSet[id] = true; });
1259
1260        var unseen = data.all.filter(function(id) { return !seenSet[id]; });
1261        if (!unseen.length) return;
1262
1263        // Ids this particular page puts in front of the visitor. Anything in
1264        // here is considered delivered and is marked seen on the way out.
1265        var delivered = Object.create(null);
1266
1267        // --- Circuit page: flag a new opening in this circuit specifically.
1268        var circuitSection = document.querySelector('[data-job-alert-circuit]');
1269        if (circuitSection) {
1270            var circuitName = circuitSection.getAttribute('data-job-alert-circuit');
1271            var circuitIds = (data.circuits && data.circuits[circuitName]) || [];
1272            var circuitUnseen = circuitIds.filter(function(id) { return !seenSet[id]; });
1273            if (circuitUnseen.length) {
1274                circuitSection.querySelectorAll('[data-job-alert-circuit-pill]').forEach(function(el) {
1275                    el.textContent = circuitUnseen.length > 1
1276                        ? circuitUnseen.length + ' New Openings'
1277                        : 'New Opening';
1278                    el.removeAttribute('hidden');
1279                });
1280                circuitUnseen.forEach(function(id) { delivered[id] = true; });
1281            }
1282        }
1283
1284        // --- Positions page: the whole slate is on screen, so consume it all.
1285        var onPositionsPage = /\/positions(\.html)?$/.test(window.location.pathname);
1286        if (onPositionsPage) {
1287            data.all.forEach(function(id) { delivered[id] = true; });
1288        }
1289
1290        // --- Nav dot: only for postings this page did NOT already surface.
1291        var remaining = unseen.filter(function(id) { return !delivered[id]; });
1292        if (remaining.length) {
1293            showJobAlertEls('[data-job-alert-dot]');
1294            showJobAlertEls('[data-job-alert-pill]');
1295            var anchor = document.querySelector('[data-job-alert-anchor]');
1296            if (anchor) {
1297                anchor.setAttribute('title', remaining.length > 1
1298                    ? remaining.length + ' new job postings since your last visit'
1299                    : 'New job posting since your last visit');
1300            }
1301            // The nav dot is informational only; mark it delivered so it does
1302            // not follow the visitor from page to page for the same openings.
1303            remaining.forEach(function(id) { delivered[id] = true; });
1304        }
1305
1306        var deliveredIds = Object.keys(delivered);
1307        if (deliveredIds.length) {
1308            // Drop ids that are no longer posted before storing. Everything we
1309            // deliver comes from the active slate, so the record stays the size
1310            // of the current listing instead of growing with every closed job.
1311            var stillActive = Object.create(null);
1312            data.all.forEach(function(id) { stillActive[id] = true; });
1313            var retained = seen.filter(function(id) { return stillActive[id]; });
1314            writeSeenJobs(retained.concat(deliveredIds));
1315        }
1316    }
1317
1318    // Load circuit data from CMS and override the hardcoded global
1319    async function loadCircuits(basePath) {
1320        var data = await fetchData(basePath, '_data/circuits.json');
1321        if (!data) return;
1322
1323        if (data.circuits && data.circuits.length > 0) {
1324            // Normalize counties: CMS stores {name,seat} objects but
1325            // main.js search expects plain strings. Keep objects only
1326            // in a separate property for scripts that need seat info.
1327            data.circuits.forEach(function(c) {
1328                if (c.counties && c.counties.length > 0 && typeof c.counties[0] === 'object') {
1329                    c._countiesRaw = c.counties;
1330                    c.counties = c.counties.map(function(county) { return county.name; });
1331                }
1332            });
1333            window.circuitData = data.circuits;
1334        }
1335        if (data.optOutCircuits && data.optOutCircuits.length > 0) {
1336            data.optOutCircuits.forEach(function(c) {
1337                if (c.counties && c.counties.length > 0 && typeof c.counties[0] === 'object') {
1338                    c.counties = c.counties.map(function(county) { return county.name; });
1339                }
1340            });
1341            window.optOutCircuits = data.optOutCircuits;
1342        }
1343
1344        // Notify county-map.js and other scripts that fresh data is available
1345        window.dispatchEvent(new CustomEvent('circuitDataUpdated'));
1346    }
1347
1348    // Load council members and dynamically render on team.html
1349    async function loadCouncil(basePath) {
1350        var pagePath = window.location.pathname;
1351        if (!pagePath.endsWith('/team.html') && !pagePath.endsWith('/team')) return;
1352
1353        var data = await fetchData(basePath, '_data/council.json');
1354        if (!data || !data.members) return;
1355
1356        var grid = document.getElementById('council-grid');
1357        if (!grid) return;
1358
1359        grid.innerHTML = data.members.map(function(member, index) {
1360            var isChairman = member.role && member.role.toLowerCase().indexOf('chairman') !== -1;
1361            var delay = (index * 0.06).toFixed(2);
1362            return '<div class="council-card" style="animation-delay: ' + delay + 's;">' +
1363                '<h4>' + member.name + '</h4>' +
1364                '<p class="council-role' + (isChairman ? ' chairman' : '') + '">' + member.role + '</p>' +
1365            '</div>';
1366        }).join('');
1367    }
1368
1369    // Load council meeting agendas & minutes and render on team.html
1370    async function loadCouncilMeetings(basePath) {
1371        var pagePath = window.location.pathname;
1372        if (!pagePath.endsWith('/team.html') && !pagePath.endsWith('/team')) return;
1373
1374        var data = await fetchData(basePath, '_data/council-meetings.json');
1375        if (!data || !data.meetings || !data.meetings.length) return;
1376
1377        var pillsEl  = document.getElementById('cm-year-pills');
1378        var recordsEl = document.getElementById('cm-records');
1379        var band      = document.getElementById('council-meetings-band');
1380        if (!pillsEl || !recordsEl || !band) return;
1381
1382        // Group by year (JSON is already newest-first)
1383        var byYear = {}, years = [];
1384        data.meetings.forEach(function(m) {
1385            var y = m.date.slice(0, 4);
1386            if (!byYear[y]) { byYear[y] = []; years.push(y); }
1387            byYear[y].push(m);
1388        });
1389
1390        var pdfIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
1391
1392        function cardsForYear(year) {
1393            return byYear[year].map(function(m) {
1394                var links = '';
1395                if (m.agenda)         links += '<a href="' + m.agenda         + '" target="_blank" rel="noopener" class="cm-link cm-link--agenda">'  + pdfIcon + 'Agenda</a>';
1396                if (m.agenda_amended) links += '<a href="' + m.agenda_amended + '" target="_blank" rel="noopener" class="cm-link cm-link--amended">' + pdfIcon + 'Amended</a>';
1397                if (m.minutes)        links += '<a href="' + m.minutes        + '" target="_blank" rel="noopener" class="cm-link cm-link--minutes">' + pdfIcon + 'Minutes</a>';
1398                return '<div class="cm-card"><div class="cm-card__date">' + m.label + '</div><div class="cm-card__links">' + links + '</div></div>';
1399            }).join('');
1400        }
1401
1402        function selectYear(year) {
1403            pillsEl.querySelectorAll('.cm-year-btn').forEach(function(btn) {
1404                btn.classList.toggle('active', btn.dataset.year === year);
1405            });
1406            recordsEl.innerHTML = cardsForYear(year);
1407        }
1408
1409        // Render year pills
1410        pillsEl.innerHTML = years.map(function(y) {
1411            return '<button class="cm-year-btn" data-year="' + y + '">' + y + '</button>';
1412        }).join('');
1413
1414        pillsEl.addEventListener('click', function(e) {
1415            var btn = e.target.closest('.cm-year-btn');
1416            if (btn) selectYear(btn.dataset.year);
1417        });
1418
1419        // Default to most recent year
1420        selectYear(years[0]);
1421        band.style.display = '';
1422    }
1423
1424    // Initialize navigation (mobile toggle, dropdowns, scroll behavior)
1425    function initNavigation() {
1426        const nav = document.getElementById('nav');
1427        const navToggle = document.getElementById('navToggle');
1428        const mobileNav = document.getElementById('mobileNav');
1429
1430        let savedScrollY = 0;
1431
1432        // A collapsed group animates grid-template-rows rather than display, so its
1433        // sublinks still have layout boxes and have to be filtered out by hand or
1434        // the trap walks focus through links the visitor cannot see.
1435        function mobileNavFocusables() {
1436            if (!mobileNav) return [];
1437            return Array.prototype.filter.call(
1438                mobileNav.querySelectorAll('a[href], button:not([disabled])'),
1439                function(el) {
1440                    return !el.closest('.mobile-nav-group:not(.expanded) .mobile-nav-sublinks');
1441                }
1442            );
1443        }
1444
1445        function setMobileNav(open) {
1446            if (!navToggle || !mobileNav) return;
1447
1448            navToggle.classList.toggle('active', open);
1449            mobileNav.classList.toggle('active', open);
1450            navToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
1451
1452            if (open) {
1453                savedScrollY = window.scrollY;
1454                document.body.classList.add('nav-open');
1455                document.body.style.top = `-${savedScrollY}px`;
1456                // The panel is visibility:hidden until the class lands, and focus()
1457                // is a no-op on an invisible element, so wait for the next frame.
1458                requestAnimationFrame(function() {
1459                    const first = mobileNavFocusables()[0];
1460                    if (first) first.focus();
1461                });
1462            } else {
1463                document.body.classList.remove('nav-open');
1464                document.body.style.top = '';
1465                window.scrollTo(0, savedScrollY);
1466            }
1467        }
1468
1469        if (navToggle && mobileNav) {
1470            navToggle.addEventListener('click', function() {
1471                setMobileNav(!mobileNav.classList.contains('active'));
1472            });
1473
1474            mobileNav.addEventListener('keydown', function(e) {
1475                if (e.key === 'Escape') {
1476                    setMobileNav(false);
1477                    navToggle.focus();
1478                    return;
1479                }
1480                if (e.key !== 'Tab') return;
1481
1482                const items = mobileNavFocusables();
1483                if (items.length === 0) return;
1484
1485                const first = items[0];
1486                const last = items[items.length - 1];
1487                if (e.shiftKey && document.activeElement === first) {
1488                    e.preventDefault();
1489                    last.focus();
1490                } else if (!e.shiftKey && document.activeElement === last) {
1491                    e.preventDefault();
1492                    first.focus();
1493                }
1494            });
1495        }
1496
1497        // Scroll behavior for nav (throttled via rAF)
1498        if (nav) {
1499            let scrollTicking = false;
1500            window.addEventListener('scroll', function() {
1501                if (!scrollTicking) {
1502                    requestAnimationFrame(function() {
1503                        const currentScroll = window.pageYOffset;
1504                        if (currentScroll > 100) {
1505                            nav.classList.add('scrolled');
1506                        } else {
1507                            nav.classList.remove('scrolled');
1508                        }
1509                        scrollTicking = false;
1510                    });
1511                    scrollTicking = true;
1512                }
1513            });
1514        }
1515
1516        // Close mobile nav when clicking any link inside it
1517        const mobileLinks = document.querySelectorAll('#mobileNav a[href]');
1518        mobileLinks.forEach(link => {
1519            link.addEventListener('click', () => setMobileNav(false));
1520        });
1521
1522        // Mobile nav collapsible groups
1523        const groupHeaders = document.querySelectorAll('.mobile-nav-group-header');
1524        groupHeaders.forEach(header => {
1525            header.addEventListener('click', function(e) {
1526                e.preventDefault();
1527                const group = this.closest('.mobile-nav-group');
1528                const isExpanded = group.classList.contains('expanded');
1529
1530                // Close all other groups
1531                document.querySelectorAll('.mobile-nav-group.expanded').forEach(g => {
1532                    if (g !== group) {
1533                        g.classList.remove('expanded');
1534                        g.querySelector('.mobile-nav-group-header').setAttribute('aria-expanded', 'false');
1535                    }
1536                });
1537
1538                // Toggle current group
1539                group.classList.toggle('expanded');
1540                this.setAttribute('aria-expanded', !isExpanded);
1541            });
1542        });
1543
1544        // FMPD inline search in mobile nav (no-ops if element no longer present)
1545        initFmpdNavSearch();
1546
1547        // Nav command bar (replaces old .search-wrapper)
1548        initNavCommandSearch(document.getElementById('navCommandBar'));
1549
1550        initNavDropdowns(nav);
1551    }
1552
1553    // The dropdowns open on :hover and :focus-within in CSS, which leaves no way to
1554    // dismiss one or to reach it on a device without hover. This owns the explicit
1555    // "is-open" state and the aria-expanded that goes with it; the CSS reveals stay.
1556    function initNavDropdowns(nav) {
1557        if (!nav) return;
1558
1559        const groups = Array.prototype.slice.call(nav.querySelectorAll('.dropdown, .has-submenu'));
1560        if (groups.length === 0) return;
1561
1562        function triggerOf(group) {
1563            return group.querySelector(':scope > a, :scope > button');
1564        }
1565
1566        function setOpen(group, open) {
1567            group.classList.toggle('is-open', open);
1568
1569            const trigger = triggerOf(group);
1570            if (trigger) trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
1571
1572            if (!open) {
1573                group.querySelectorAll('.dropdown, .has-submenu').forEach(function(inner) {
1574                    setOpen(inner, false);
1575                });
1576            }
1577        }
1578
1579        function closeOthers(keep) {
1580            groups.forEach(function(group) {
1581                if (group === keep || (keep && group.contains(keep))) return;
1582                setOpen(group, false);
1583            });
1584        }
1585
1586        function toggle(group) {
1587            const open = !group.classList.contains('is-open');
1588            closeOthers(group);
1589            setOpen(group, open);
1590        }
1591
1592        groups.forEach(function(group) {
1593            const trigger = triggerOf(group);
1594            if (!trigger) return;
1595
1596            trigger.addEventListener('keydown', function(e) {
1597                const isSpace = e.key === ' ' || e.key === 'Spacebar';
1598                if (e.key !== 'Enter' && !isSpace) return;
1599                // Enter on an already-open trigger falls through to the link, so the
1600                // trigger's own page stays reachable from the keyboard — only the
1601                // first Enter is spent opening the menu.
1602                if (!isSpace && group.classList.contains('is-open')) return;
1603                e.preventDefault();
1604                toggle(group);
1605            });
1606
1607            trigger.addEventListener('click', function(e) {
1608                // Mouse users still navigate on click and get the menu from :hover;
1609                // only a device without hover needs the tap to open it instead.
1610                if (e.detail === 0 || window.matchMedia('(hover: hover)').matches) return;
1611                e.preventDefault();
1612                toggle(group);
1613            });
1614
1615            group.addEventListener('keydown', function(e) {
1616                if (e.key !== 'Escape' || !group.classList.contains('is-open')) return;
1617                // Stop here so an Escape inside a submenu does not also close its parent.
1618                e.stopPropagation();
1619                setOpen(group, false);
1620                trigger.focus();
1621            });
1622
1623            group.addEventListener('focusout', function(e) {
1624                if (group.contains(e.relatedTarget)) return;
1625                setOpen(group, false);
1626            });
1627        });
1628
1629        /* Tells the stylesheet its :focus-within fallback can stand down: from
1630           here on .is-open is the single source of truth for an open menu. */
1631        nav.classList.add('js-nav');
1632
1633        document.addEventListener('click', function(e) {
1634            if (!nav.contains(e.target)) closeOthers(null);
1635        });
1636    }
1637
1638    // Initialize the FMPD county search in the mobile nav
1639    function initFmpdNavSearch() {
1640        var searchInput = document.getElementById('fmpdNavSearch');
1641        var resultsContainer = document.getElementById('fmpdNavResults');
1642        if (!searchInput || !resultsContainer) return;
1643
1644        var basePath = getBasePath();
1645        var debounceTimer = null;
1646
1647        searchInput.addEventListener('input', function() {
1648            clearTimeout(debounceTimer);
1649            var query = this.value.trim().toLowerCase();
1650
1651            if (query.length < 2) {
1652                resultsContainer.innerHTML = '';
1653                return;
1654            }
1655
1656            debounceTimer = setTimeout(function() {
1657                searchCounty(query, resultsContainer, basePath);
1658            }, 200);
1659        });
1660
1661        // Prevent nav from closing when interacting with search
1662        searchInput.addEventListener('click', function(e) {
1663            e.stopPropagation();
1664        });
1665    }
1666
1667    function searchCounty(query, container, basePath) {
1668        var circuits = window.circuitData;
1669        if (!circuits || !circuits.length) {
1670            container.innerHTML = '<div class="fmpd-no-results">Loading circuit data...</div>';
1671            return;
1672        }
1673
1674        // Normalize query: replace hyphens with spaces for matching
1675        var normalizedQuery = query.replace(/-/g, ' ');
1676        var results = [];
1677
1678        circuits.forEach(function(circuit) {
1679            circuit.counties.forEach(function(c) {
1680                var name = typeof c === 'string' ? c : c.name;
1681                var countyLower = name.toLowerCase();
1682                if (countyLower.indexOf(normalizedQuery) === 0 || countyLower.indexOf(query) === 0) {
1683                    results.push({
1684                        county: name,
1685                        circuit: circuit.circuit,
1686                        slug: circuit.slug,
1687                        phone: circuit.phone || '',
1688                        defender: circuit.defender || ''
1689                    });
1690                }
1691            });
1692        });
1693
1694        // Also check opt-out circuits
1695        var optOuts = window.optOutCircuits;
1696        if (optOuts && optOuts.length) {
1697            optOuts.forEach(function(circuit) {
1698                circuit.counties.forEach(function(c) {
1699                    var name = typeof c === 'string' ? c : c.name;
1700                    var countyLower = name.toLowerCase();
1701                    if (countyLower.indexOf(normalizedQuery) === 0 || countyLower.indexOf(query) === 0) {
1702                        results.push({
1703                            county: name,
1704                            circuit: circuit.circuit,
1705                            slug: circuit.slug || '',
1706                            phone: '',
1707                            note: circuit.note || ''
1708                        });
1709                    }
1710                });
1711            });
1712        }
1713
1714        // Limit to 4 results
1715        results = results.slice(0, 4);
1716
1717        if (results.length === 0) {
1718            var nr = document.createElement('div'); nr.className = 'fmpd-no-results'; nr.textContent = 'No county found matching \u201c' + query + '\u201d'; container.innerHTML = '';
1718 container.appendChild(nr);
1719            return;
1720        }
1721
1722        container.innerHTML = results.map(function(r) {
1723            if (r.note) {
1724                var noteInner = '<div class="fmpd-result-circuit">' + r.county + ' County</div>' +
1725                    '<div class="fmpd-result-county">' + r.note + '</div>';
1726                if (r.slug) {
1727                    return '<a href="' + basePath + 'local-offices/' + r.slug + '.html" class="fmpd-result-item">' +
1728                        noteInner +
1729                        '<div class="fmpd-result-phone">View office contact details &rarr;</div>' +
1730                        '</a>';
1731                }
1732                return '<div class="fmpd-result-item">' + noteInner + '</div>';
1733            }
1734            var href = basePath + 'local-offices/' + r.slug + '.html';
1735            return '<a href="' + href + '" class="fmpd-result-item">' +
1736                '<div class="fmpd-result-circuit">' + r.circuit + '</div>' +
1737                '<div class="fmpd-result-county">' + r.county + ' County — ' + r.defender + '</div>' +
1738                (r.phone ? '<div class="fmpd-result-phone">' + r.phone + '</div>' : '') +
1739                '</a>';
1740        }).join('');
1741    }
1742
1743    // ── NAV COMMAND SEARCH ────────────────────────────────────────────────
1744    // Vanilla-JS driver for the FYPD-style command bar used in the site nav
1745    // and homepage hero. Reuses all .fypd-command-* CSS classes.
1746    // Selecting a result navigates to local-offices/{slug}.html.
1747    function initNavCommandSearch(root) {
1748        if (!root) return;
1749
1750        var bar        = root.querySelector('.fypd-command-bar');
1751        var input      = root.querySelector('.nav-cmd-input');
1752        var clearBtn   = root.querySelector('.nav-cmd-clear');
1753        var dropdown   = root.querySelector('.nav-cmd-dropdown');
1754        var detectedBtn  = root.querySelector('.nav-cmd-detected');
1755        var detectedName = root.querySelector('.nav-cmd-detected-name');
1756        var divider    = root.querySelector('.nav-cmd-divider');
1757        var geoBtn     = root.querySelector('.nav-cmd-geo');
1758        var geoLabel   = root.querySelector('.nav-cmd-geo-label');
1759        var geoRing    = root.querySelector('.nav-cmd-geo .fypd-geo-ring');
1760
1761        if (!bar || !input || !dropdown) return;
1762
1763        var basePath = getBasePath();
1764
1765        // State
1766        var query        = '';
1767        var results      = [];
1768        var focusIdx     = -1;
1769        var isOpen       = false;
1770        var geoAvail     = !!window.isSecureContext && 'geolocation' in navigator;
1771        var geoState     = 'idle';   // idle | loading | success | error
1772        var geoCircuit   = null;
1773        var geoErrorTimer = null;
1774
1775        // Show geo button if geolocation is available (user must click to activate)
1776        if (geoAvail && divider && geoBtn) {
1777            divider.style.display = '';
1778            geoBtn.style.display = '';
1779        }
1780
1781        // ── Events ─────────────────────────────────────────────────────
1782        input.addEventListener('input', function() {
1783            query = input.value;
1784            updateClear();
1785            runSearch();
1786        });
1787
1788        input.addEventListener('focus', function() {
1789            bar.classList.add('is-focused');
1790            if (results.length > 0 && !isGeoActive()) openDropdown();
1791        });
1792
1793        input.addEventListener('blur', function() {
1794            setTimeout(function() {
1795                bar.classList.remove('is-focused');
1796                closeDropdown();
1797            }, 180);
1798        });
1799
1800        input.addEventListener('keydown', function(e) {
1801            if (!isOpen) return;
1802            if (e.key === 'ArrowDown') {
1803                e.preventDefault();
1804                moveFocus(1);
1805            } else if (e.key === 'ArrowUp') {
1806                e.preventDefault();
1807                moveFocus(-1);
1808            } else if (e.key === 'Enter') {
1809                e.preventDefault();
1810                var idx = focusIdx >= 0 ? focusIdx : (results.length > 0 ? 0 : -1);
1811                if (idx >= 0 && results[idx]) navigate(results[idx]);
1812            } else if (e.key === 'Escape') {
1813                clearSearch();
1814                input.blur();
1815            }
1816        });
1817
1818        if (clearBtn) clearBtn.addEventListener('click', clearSearch);
1819
1820        if (detectedBtn) {
1821            detectedBtn.addEventListener('click', function() {
1822                if (geoCircuit) navigate(geoCircuit);
1823            });
1824        }
1825
1826        if (geoBtn) {
1827            geoBtn.addEventListener('click', function() {
1828                if (geoState === 'success' && geoCircuit) {
1829                    navigate(geoCircuit);
1830                    return;
1831                }
1832                runGeo(false);
1833            });
1834        }
1835
1836        document.addEventListener('click', function(e) {
1837            if (!root.contains(e.target)) closeDropdown();
1838        });
1839
1840        // ── Search ─────────────────────────────────────────────────────
1841        function runSearch() {
1842            var q = query.toLowerCase().trim();
1843            if (!q) {
1844                results = [];
1845                focusIdx = -1;
1846                closeDropdown();
1847                return;
1848            }
1849
1850            var circuits = window.circuitData || [];
1851            var scored = circuits.map(function(c) {
1852                var score = 0, matchLabel = '';
1853                if (c.circuit.toLowerCase().indexOf(q) !== -1) {
1854                    score += 10;
1855                    matchLabel = 'Circuit';
1856                }
1857                var countyHit = null;
1858                for (var i = 0; i < c.counties.length; i++) {
1859                    var cn = typeof c.counties[i] === 'string' ? c.counties[i] : c.counties[i].name;
1860                    if (cn.toLowerCase().indexOf(q) !== -1) { countyHit = cn; score += 15; break; }
1861                }
1862                if (countyHit) matchLabel = countyHit + ' County';
1863                if ((c.defender || '').toLowerCase().indexOf(q) !== -1) {
1864                    score += 5;
1865                    if (!matchLabel) matchLabel = 'Defender';
1866                }
1867                return Object.assign({}, c, { _score: score, _matchLabel: matchLabel });
1868            }).filter(function(c) { return c._score > 0; })
1869              .sort(function(a, b) { return b._score - a._score; });
1870
1871            // Include opt-out (independent) circuits so counties like Cobb resolve.
1872            var optOuts = window.optOutCircuits || [];
1873            optOuts.forEach(function(o) {
1874                if (!o.slug) return;
1875                var score = 0, matchLabel = '';
1876                if ((o.circuit || '').toLowerCase().indexOf(q) !== -1) { score += 10; matchLabel = 'Circuit'; }
1877                var countyHit = null;
1878                var oCounties = o.counties || [];
1879                for (var k = 0; k < oCounties.length; k++) {
1880                    var ocn = typeof oCounties[k] === 'string' ? oCounties[k] : oCounties[k].name;
1881                    if (ocn.toLowerCase().indexOf(q) !== -1) { countyHit = ocn; score += 15; break; }
1882                }
1883                if (countyHit) matchLabel = countyHit + ' County';
1884                if (score > 0) {
1885                    scored.push(Object.assign({}, o, { _score: score, _matchLabel: matchLabel, _optOut: true }));
1886                }
1887            });
1888            scored.sort(function(a, b) { return b._score - a._score; });
1889
1890            results  = scored.slice(0, 8);
1891            focusIdx = results.length > 0 ? 0 : -1;
1892            renderDropdown();
1893            if (results.length > 0) openDropdown();
1894            else closeDropdown();
1895        }
1896
1897        function renderDropdown() {
1898            if (!results.length) { dropdown.innerHTML = ''; return; }
1899            dropdown.innerHTML = results.map(function(r, idx) {
1900                var cnt  = (r.counties || []).length;
1901                var cntTxt = cnt + (cnt === 1 ? ' county' : ' counties');
1902                var metaTxt = r._optOut ? 'Independent office' : cntTxt;
1903                return '<div class="fypd-dropdown-item' + (idx === focusIdx ? ' is-focused' : '') +
1904                    (r._optOut ? ' is-optout' : '') +
1905                    '" data-idx="' + idx + '" role="option">' +
1906                    '<div class="fypd-dropdown-item-icon">' +
1907                        '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">' +
1908                            '<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/>' +
1909                            '<circle cx="12" cy="10" r="3"/>' +
1910                        '</svg>' +
1911                    '</div>' +
1912                    '<div class="fypd-dropdown-item-body">' +
1913                        '<span class="fypd-dropdown-circuit-name">' + esc(r.circuit) + '</span>' +
1914                        '<span class="fypd-dropdown-match-label">' + esc(r._matchLabel) + '</span>' +
1915                    '</div>' +
1916                    '<div class="fypd-dropdown-item-meta">' +
1917                        '<span class="fypd-dropdown-county-count">' + metaTxt + '</span>' +
1918                    '</div>' +
1919                '</div>';
1920            }).join('');
1921
1922            dropdown.querySelectorAll('.fypd-dropdown-item').forEach(function(item) {
1923                item.addEventListener('mousedown', function(e) {
1924                    e.preventDefault();
1925                    var i = parseInt(item.dataset.idx, 10);
1926                    if (!isNaN(i) && results[i]) navigate(results[i]);
1927                });
1928                item.addEventListener('mouseenter', function() {
1929                    focusIdx = parseInt(item.dataset.idx, 10);
1930                    updateFocusHighlight();
1931                });
1932            });
1933        }
1934
1935        function moveFocus(dir) {
1936            if (!results.length) return;
1937            focusIdx = Math.max(0, Math.min(results.length - 1, focusIdx + dir));
1938            updateFocusHighlight();
1939        }
1940
1941        function updateFocusHighlight() {
1942            dropdown.querySelectorAll('.fypd-dropdown-item').forEach(function(item) {
1943                item.classList.toggle('is-focused', parseInt(item.dataset.idx, 10) === focusIdx);
1944            });
1945        }
1946
1947        function navigate(circuit) {
1948            window.location.href = basePath + 'local-offices/' + cir
1948cuit.slug + '.html';
1949        }
1950
1951        function openDropdown() {
1952            if (isGeoActive()) return;
1953            dropdown.style.display = '';
1954            bar.classList.add('has-results');
1955            isOpen = true;
1956        }
1957
1958        function closeDropdown() {
1959            dropdown.style.display = 'none';
1960            bar.classList.remove('has-results');
1961            isOpen = false;
1962        }
1963
1964        function clearSearch() {
1965            query = input.value = '';
1966            results = [];
1967            focusIdx = -1;
1968            updateClear();
1969            closeDropdown();
1970        }
1971
1972        function updateClear() {
1973            if (clearBtn) clearBtn.style.display = query.length > 0 ? '' : 'none';
1974        }
1975
1976        function isGeoActive() {
1977            return geoState === 'success';
1978        }
1979
1980        // ── Geolocation ─────────────────────────────────────────────────
1981        function runGeo(silent) {
1982            if (!geoAvail || geoState === 'loading') return;
1983            setGeoState('loading');
1984            navigator.geolocation.getCurrentPosition(
1985                function(pos) { onGeoSuccess(pos, silent); },
1986                function()    { onGeoError(silent); },
1987                { enableHighAccuracy: false, timeout: 10000, maximumAge: 300000 }
1988            );
1989        }
1990
1991        function onGeoSuccess(pos, silent) {
1992            var lat = pos.coords.latitude, lon = pos.coords.longitude;
1993            var url = 'https://geo.fcc.gov/api/census/block/find?latitude=' +
1994                encodeURIComponent(lat) + '&longitude=' + encodeURIComponent(lon) + '&format=json';
1995            fetch(url, { headers: { 'Accept': 'application/json' } })
1996                .then(function(r) { return r.ok ? r.json() : Promise.reject(); })
1997                .then(function(data) {
1998                    if (!data || !data.County) { onGeoError(silent); return; }
1999                    var slug = normalizeSlug(data.County.name);
2000                    var circuit = findCircuitByCountySlug(slug);
2001                    if (circuit) applyGeoMatch(circuit);
2002                    else onGeoError(silent);
2003                })
2004                .catch(function() { onGeoError(silent); });
2005        }
2006
2007        function onGeoError(silent) {
2008            if (silent) { setGeoState('idle'); return; }
2009            setGeoState('error');
2010            if (geoErrorTimer) clearTimeout(geoErrorTimer);
2011            geoErrorTimer = setTimeout(function() { setGeoState('idle'); }, 5000);
2012        }
2013
2014        function applyGeoMatch(circuit) {
2015            geoCircuit = circuit;
2016            setGeoState('success');
2017            bar.classList.add('is-geo-active');
2018            // Show detected button, hide input
2019            if (detectedName) detectedName.textContent = circuit.circuit;
2020            if (detectedBtn)  detectedBtn.style.display = '';
2021            input.style.display = 'none';
2022            clearSearch();
2023            closeDropdown();
2024        }
2025
2026        function setGeoState(state) {
2027            geoState = state;
2028            if (!geoBtn) return;
2029            geoBtn.classList.remove('loading', 'success', 'error');
2030            if (state !== 'idle') geoBtn.classList.add(state);
2031            if (geoRing) geoRing.style.display = state === 'loading' ? '' : 'none';
2032            geoBtn.disabled = state === 'loading';
2033            if (geoLabel) {
2034                var labels = { idle: 'Use my location', loading: 'Locating…', success: '', error: 'Try again' };
2035                geoLabel.textContent = labels[state] || '';
2036            }
2037        }
2038
2039        function findCircuitByCountySlug(slug) {
2040            var circuits = window.circuitData || [];
2041            for (var i = 0; i < circuits.length; i++) {
2042                var c = circuits[i];
2043                for (var j = 0; j < c.counties.length; j++) {
2044                    var cn = typeof c.counties[j] === 'string' ? c.counties[j] : c.counties[j].name;
2045                    if (normalizeSlug(cn) === slug) return c;
2046                }
2047            }
2048            return null;
2049        }
2050
2051        function normalizeSlug(v) {
2052            return (v || '').replace(/\s+county$/i, '').toLowerCase()
2053                .replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
2054        }
2055
2056        function esc(s) {
2057            return (s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;')
2058                .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
2059        }
2060
2061        // ── Typewriter placeholder (hero only) ────────────────────────
2062        if (root.id === 'heroCommandBar') {
2063            var twTexts = [
2064                'Find your public defender',
2065                'Search "Tift County"',
2066                'Search "Cordele Circuit"',
2067                'Search "Lowndes County"',
2068                'Search "Waycross Circuit"'
2069            ];
2070            var twIdx = 0, twChar = 0, twDeleting = false, twTimer = null, twActive = false;
2071
2072            function twRun() {
2073                if (input.value.length > 0) {
2074                    twStop();
2075                    return;
2076                }
2077                twActive = true;
2078                var text = twTexts[twIdx];
2079                if (twDeleting) {
2080                    twChar--;
2081                    input.placeholder = text.substring(0, twChar);
2082                    if (twChar === 0) {
2083                        twDeleting = false;
2084                        twIdx = (twIdx + 1) % twTexts.length;
2085                        twTimer = setTimeout(twRun, 300);
2086                    } else {
2087                        twTimer = setTimeout(twRun, 30);
2088                    }
2089                } else {
2090                    twChar++;
2091                    input.placeholder = text.substring(0, twChar);
2092                    if (twChar === text.length) {
2093                        twDeleting = true;
2094                        twTimer = setTimeout(twRun, 2000);
2095                    } else {
2096                        twTimer = setTimeout(twRun, 80);
2097                    }
2098                }
2099            }
2100
2101            function twStop() {
2102                if (twTimer) { clearTimeout(twTimer); twTimer = null; }
2103                twIdx = 0; twChar = 0; twDeleting = false; twActive = false;
2104                input.placeholder = twTexts[0];
2105            }
2106
2107            function twStart() {
2108                if (!twActive && input.value.length === 0) {
2109                    twTimer = setTimeout(twRun, 500);
2110                }
2111            }
2112
2113            // Start on focus
2114            input.addEventListener('focus', twStart);
2115            input.addEventListener('blur', function() { twStop(); });
2116
2117            // Auto-start when visible on screen
2118            if ('IntersectionObserver' in window) {
2119                var twObs = new IntersectionObserver(function(entries) {
2120                    entries.forEach(function(e) {
2121                        if (e.isIntersecting) { twStart(); }
2122                        else { twStop(); }
2123                    });
2124                }, { threshold: 0.5 });
2125                twObs.observe(root);
2126            }
2127        }
2128    }
2129
2130    /* Email obfuscation — decode data-email attributes at runtime */
2131    function decodeEmails() {
2132        document.querySelectorAll('a[data-email]').forEach(function(el) {
2133            try {
2134                var email = atob(el.getAttribute('data-email'));
2135                el.href = 'mailto:' + email;
2136                if (el.querySelector('.email-text')) {
2137                    el.querySelector('.email-text').textContent = email;
2138                } else if (!el.children.length) {
2139                    el.textContent = email;
2140                } else if (!el.querySelector('svg') && el.textContent.trim() === '') {
2141                    el.textContent = email;
2142                }
2143            } catch(e) {}
2144        });
2145    }
2146
2147    // Run when DOM is ready
2148    if (document.readyState === 'loading') {
2149        document.addEventListener('DOMContentLoaded', init);
2150    } else {
2151        init();
2152    }
2153
2154    // Also decode any page-level emails after DOM is ready
2155    if (document.readyState === 'loading') {
2156        document.addEventListener('DOMContentLoaded', decodeEmails);
2157    } else {
2158        decodeEmails();
2159    }
2160})();

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.