PageSourceSearch

https://stalwart-tulumba-97f121.netlify.app/app.js

js stalwart-tulumba-97f121.netlify.app collected 2026-10-03 11:26:35 UTC 33,976 bytes, 622 lines download raw bytes

1/* ═══════════════════════════════════════════════════
2   RICHLAND COUNTY WILDFLOWER GUIDE — App Logic
3   ═══════════════════════════════════════════════════ */
4
5// ── Data ──────────────────────────────────────────────
6const LOCATIONS = [
7  {
8    id: 'malabar',
9    name: 'Malabar Farm State Park',
10    type: 'State Park',
11    typeClass: 'type-state-park',
12    lat: 40.6615,
13    lng: -82.3680,
14    color: '#3a6b3c',
15    county: 'Richland County',
16    address: '4050 Bromfield Rd, Lucas, OH 44843',
17    phone: '(419) 892-2784',
18    description: 'Located in Pleasant Valley, Malabar Farm was the vision of Pulitzer Prize-winning author Louis Bromfield. The beech-maple woodlands, ravines, and sandstone ridgetops host one of the most accessible wildflower displays in north-central Ohio. Trillium, spring beauties, and wild blue phlox carpet the forest floor each April.',
19    trails: [
20      { name: 'Jungle Brook Trail', length: '0.7 mi', difficulty: 'Easy', wildflowers: 'High' },
21      { name: 'Doris Duke Woods Trail', length: '1.0 mi', difficulty: 'Easy', wildflowers: 'High' },
22      { name: 'Butternut Trail', length: '0.8 mi', difficulty: 'Easy', wildflowers: 'Moderate' },
23      { name: 'Pleasant Valley Bridle Trail', length: '7.0 mi', difficulty: 'Moderate', wildflowers: 'Moderate' },
24    ],
25    parking: 'Free. Main parking lot at Visitor Center off Bromfield Rd. Additional lots near Big House.',
26    accessibility: 'Butternut and Jungle Brook trails are relatively flat. Visitor Center is ADA accessible.',
27    hours: 'Dawn to dusk daily. Visitor Center 9am–5pm year-round.',
28    species: ['Large-flowered Trillium', 'Spring Beauty', 'Wild Blue Phlox', 'Virginia Bluebells', 'Bloodroot'],
29    peakWindow: 'Mid-April through early May',
30    guidedHikes: 'Big House tours: Sat–Sun 11am & 2pm in April. Farm Tours begin May 1. Call ahead for naturalist hike schedules.',
31    mapLink: 'https://ohiodnr.gov/go-and-do/plan-a-visit/find-a-property/malabar-farm-state-park',
32    bloomStatus: 'blooming'
33  },
34  {
35    id: 'fowler',
36    name: 'Fowler Woods State Nature Preserve',
37    type: 'State Nature Preserve',
38    typeClass: 'type-nature-preserve',
39    lat: 40.9785,
40    lng: -82.4210,
41    color: '#7a52a0',
42    county: 'Richland County',
43    address: '7815 Olivesburg-Fitchville Rd, Greenwich, OH 44837',
44    phone: 'ODNR DNAP: (614) 265-6453',
45    description: 'The only state nature preserve in Richland County, Fowler Woods is a 187-acre beech-maple forest with buttonbush swamps and vernal pools. It is one of the best sites in north-central Ohio for spring wildflowers — particularly marsh marigolds in April and Trillium, violets, jack-in-the-pulpit, and phlox through May. Features a partial boardwalk (some sections closed due to ash tree safety).',
46    trails: [
47      { name: 'Boardwalk Loop (partial)', length: '1.25 mi', difficulty: 'Easy', wildflowers: 'Outstanding' },
48    ],
49    parking: 'Free. Small parking lot at trailhead off Olivesburg-Fitchville Rd.',
50    accessibility: 'Wheelchair-accessible boardwalk — note: some sections remain closed due to diseased ash tree safety concerns. Check with ODNR before visiting.',
51    hours: '½ hour before sunrise to ½ hour after sunset. No pets. No restrooms.',
52    species: ['Marsh Marigold', 'Large-flowered Trillium', 'Dutchman\'s Breeches', 'Violets', 'Jack-in-the-Pulpit', 'Wild Blue Phlox', 'Spring Beauty'],
53    peakWindow: 'Marsh Marigold: mid-April. Trillium & Phlox: late April–May',
54    guidedHikes: 'Self-guided only. ODNR occasionally hosts stewardship events. Contact DNAP for scheduled programs.',
55    mapLink: 'https://ohiodnr.gov/go-and-do/plan-a-visit/find-a-property/fowler-woods-state-nature-preserve',
56    bloomStatus: 'blooming'
57  },
58  {
59    id: 'gorman',
60    name: 'Gorman Nature Center',
61    type: 'Park District',
62    typeClass: 'type-park-district',
63    lat: 40.7025,
64    lng: -82.5275,
65    color: '#2a5a7a',
66    county: 'Richland County',
67    address: '2295 Lexington Ave, Mansfield, OH 44907',
68    phone: '(419) 884-3764',
69    description: 'The flagship property of the Richland County Park District, Gorman Nature Center offers 150 acres of forests, prairies, ponds, and streams, with over 5 miles of public trails. Conveniently located between Mansfield and Lexington, it is an ideal first stop for wildflower exploration with good accessibility and a staffed nature center.',
70    trails: [
71      { name: 'Forest Loop Trail', length: '~2.0 mi', difficulty: 'Easy–Moderate', wildflowers: 'High' },
72      { name: 'Prairie Trail', length: '~1.0 mi', difficulty: 'Easy', wildflowers: 'Moderate' },
73      { name: 'Pond Trail', length: '~0.75 mi', difficulty: 'Easy', wildflowers: 'Moderate' },
74    ],
75    parking: 'Free. Main parking lot at the nature center on Lexington Ave.',
76    accessibility: 'Portions of trails are paved or crushed limestone. Nature center building is ADA accessible.',
77    hours: 'Trails open dawn to dusk. Nature Center hours vary — call ahead.',
78    species: ['Spring Beauty', 'Wild Blue Phlox', 'Violets', 'Jack-in-the-Pulpit', 'W
78ild Ginger'],
79    peakWindow: 'Early to mid-April for earliest bloomers; late April for phlox and Jack-in-the-pulpit',
80    guidedHikes: 'Richland County Park District runs educational and naturalist programs. Call (419) 884-3764 or visit richlandcountyparks.com for 2026 spring program schedule.',
81    mapLink: 'https://richlandcountyparks.com',
82    bloomStatus: 'starting'
83  },
84  {
85    id: 'clearfork',
86    name: 'Clear Fork Gorge State Nature Preserve',
87    type: 'State Nature Preserve',
88    typeClass: 'type-nature-preserve',
89    lat: 40.5990,
90    lng: -82.3785,
91    color: '#7a52a0',
92    county: 'Ashland County (near Richland border)',
93    address: 'Access via State Forest Road 51 off SR-97, near Loudonville',
94    phone: 'ODNR: (614) 265-6453',
95    description: 'A 29-acre gem along the north bluff of the Clear Fork of the Mohican River, Clear Fork Gorge is a National Natural Landmark (1967). The white pine-hemlock forest community is one of the finest in Ohio. Visitors have reported excellent Dutchman\'s Breeches, Large-flowered Trillium, and Blue Phlox blooms in mid-April in past years. The gorge setting creates a cool microclimate that can delay blooms ~1 week later than nearby sites.',
96    trails: [
97      { name: 'Gorge Trail', length: '0.3 mi', difficulty: 'Moderate', wildflowers: 'Outstanding (gorge walls)' },
98    ],
99    parking: 'Park at the Mohican fire tower on the south side of State Forest Road 51. Trail head is on the north side.',
100    accessibility: 'Rugged gorge terrain — not accessible for mobility-limited visitors. Sturdy footwear essential.',
101    hours: '½ hour before sunrise to ½ hour after sunset. No pets. No restrooms.',
102    species: ['Dutchman\'s Breeches', 'Large-flowered Trillium', 'Wild Blue Phlox', 'Hepatica', 'Bloodroot'],
103    peakWindow: 'Late April — approximately 1 week later than Malabar due to cool gorge microclimate',
104    guidedHikes: 'Self-guided. Mohican State Park (adjacent) occasionally offers guided naturalist walks — check mohicanstatepark.com.',
105    mapLink: 'https://ohiodnr.gov/go-and-do/plan-a-visit/find-a-property/clear-fork-gorge-state-nature-preserve',
106    bloomStatus: 'starting'
107  },
108  {
109    id: 'mohican',
110    name: 'Mohican State Park & Forest',
111    type: 'State Forest',
112    typeClass: 'type-state-forest',
113    lat: 40.5965,
114    lng: -82.3630,
115    color: '#8B5E3C',
116    county: 'Ashland County (15 mi from Mansfield)',
117    address: '3116 State Route 3, Loudonville, OH 44842',
118    phone: '(419) 994-5125',
119    description: 'Mohican State Park and Mohican-Memorial State Forest together offer 32+ miles of hiking trails through spectacular hemlock-lined gorges, open ridgetops, and rich river bottomlands. Social media from past Aprils documents excellent blooms of Dutchman\'s Breeches, Squirrel Corn, Trillium, and Blue Phlox along the gorge trails. The 1.5-mile Discovery Forest interpretive trail is a great introduction. ODNR Week 4 (2026) lists Mohican as a top viewing site for the northern region.',
120    trails: [
121      { name: 'Lyons Falls Trail', length: '2.3 mi', difficulty: 'Moderate', wildflowers: 'Outstanding' },
122      { name: 'Discovery Forest Trail', length: '1.5 mi', difficulty: 'Easy', wildflowers: 'High (interpretive)' },
123      { name: 'Gorge Trail', length: '3.4 mi', difficulty: 'Moderate', wildflowers: 'High' },
124      { name: 'Pleasant Hill Trail', length: '5.0 mi', difficulty: 'Moderate', wildflowers: 'Moderate' },
125    ],
126    parking: 'Multiple lots throughout the park. Main day-use lot at SR-3 park headquarters. Free.',
127    accessibility: 'Discovery Forest Trail is relatively accessible. Gorge trails involve uneven terrain and steps.',
128    hours: 'Park open 6am–11pm daily. Forest open 6am–11pm.',
129    species: ['Dutchman\'s Breeches', 'Squirrel Corn', 'Large-flowered Trillium', 'Virginia Bluebells', 'Wild Blue Phlox', 'Hepatica', 'Trout Lily'],
130    peakWindow: 'Mid-to-late April 2026 — Trillium and Bluebells expected at peak mid-month per ODNR Week 4 report',
131    guidedHikes: 'Mohican State Park naturalist programs run periodically in spring. Call (419) 994-5125 or visit the park office. Mohican Trails Club (mohicantrailsclub.org) publishes detailed trail maps.',
132    mapLink: 'https://ohiodnr.gov/go-and-do/plan-a-visit/find-a-property/mohicanmemorial-state-forest',
133    bloomStatus: 'blooming'
134  },
135  {
136    id: 'mtgilead',
137    name: 'Mt. Gilead State Park',
138    type: 'State Park',
139    typeClass: 'type-state-park',
140    lat: 40.5465,
141    lng: -82.8295,
142    color: '#3a6b3c',
143    county: 'Morrow County (20 mi SW of Mansfield)',
144    address: '4119 SR 95, Mt. Gilead, OH 43338',
145    phone: '(419) 946-1961',
146    description: 'Listed by ODNR as a top northern region viewing site in the 2026 Week 4 bloom report. Mt. Gilead offers wooded trails around two lakes with solid spring ephemeral displays. A good alternative destination if Fowler Woods or Malabar are crowded on peak spring weekends.',
147    trails: [
148      { name: 'Covered Bridge Trail', length: '1.5 mi', difficulty: 'Easy', wildflowers: 'High' },
149      { name: 'Lakeside Trail', length: '2.5 mi', difficulty: 'Easy', wildflowers: 'Moderate' },
150    ],
151    parking: 'Free. Day-use lot at park entrance off SR-95.',
152    accessibility: 'Covered Bridge Trail is relatively level. Lakeside Trail has some elevation changes.',
153    hours: 'Dawn to dusk.',
154    species: ['Large-flowered Trillium', 'Spring Beauty', 'Wild Ginger', 'Violets', 'Mayapple'],
155    peakWindow: 'Mid-to-late April 2026',
156    guidedHikes: 'Self-guided primarily. Contact park office for occasional naturalist programs.',
157    mapLink: 'https://ohiodnr.gov/go-and-do/plan-a-visit/find-a-property/mt-gilead-state-park',
158    bloomStatus: 'starting'
159  },
160];
161
162const SPECIES = [
163  {
164    id: 'bloodroot',
165    name: 'Bloodroot',
166    latin: 'Sanguinaria canadensis',
167    status: 'peaking',
168    statusLabel: 'At Peak Now',
169    emoji: '🌸',
170    photoUrl: 'https://bygl.osu.edu/sites/default/files/inline-images/bloodroot%201%20MRG%204-26-18.JPG',
171    description: 'One of the earliest spring ephemerals, Bloodroot pushes up through leaf litter before its own leaves fully unfurl. The single white flower with golden stamens is striking but fleeting — blooms last only 1–3 days each. Named for the orange-red sap in its roots, once used as a dye.',
172    bloomTime: 'Mid-March to mid-April (Richland County: early–mid April)',
173    aprilStatus: 'Currently at or near peak in north-central Ohio. ODNR Week 1 (2026) reported Bloodroot actively blooming statewide.',
174    habitat: 'Rich, moist woodland slopes; often in beech-maple forest',
175    whereToFind: 'Malabar Farm (Jungle Brook Trail), Mohican State Park gorge trails, Clear Fork Gorge',
176    tip: 'Visit early — individual flowers last only 2–3 days. Look on south-facing slopes in rich soil.'
177  },
178  {
179    id: 'dutchmans',
180    name: "Dutchman's Breeches",
181    latin: 'Dicentra cucullaria',
182    status: 'peaking',
183    statusLabel: 'At Peak Now',
184    emoji: '🌼',
185    photoUrl: 'https://www.metroparks.net/wp-content/uploads/2026/03/Dutchman_s-Breeches-CB.jpeg',
186    description: 'The whimsical flowers of Dutchman\'s Breeches resemble miniature white pantaloons hanging upside-down on a clothesline. Only long-tongued bumblebees can reach the nectar within the spurs. A true spring ephemeral — the entire plant yellows and disappears by early summer.',
187    bloomTime: 'Late March to late April (Richland County: early–late April)',
188    aprilStatus: 'Actively blooming in north-central Ohio. ODNR reports it among current key species for the northern region.',
189    habitat: 'Rich deciduous forest floor, especially on slopes above streams',
190    whereToFind: 'Clear Fork Gorge (outstanding), Mohican State Park gorge trails, Fowler Woods',
191    tip: 'Look along gorge walls and stream banks. Often grows in dense colonies. Best in mid-morning light.'
192  },
193  {
194    id: 'bluebells',
195    name: 'Virginia Bluebells',
196    latin: 'Mertensia virginica',
197    status: 'blooming',
198    statusLabel: 'Blooming',
199    emoji: '💙',
200    photoUrl: 'https://dam.assets.ohio.gov/image/upload/ohiodnr.gov/documents/natural-areas/Wildflowers/Spring/VirginiaBlueBells-1k.jpg',
201    description: 'Virginia Bluebells create breathtaking blue carpets on river floodplains — one of Ohio\'s most spectacular wildflower displays. Pink buds open to sky-blue tubular flowers in arching cluster
201s. Like all spring ephemerals, the entire plant vanishes by early summer.',
202    bloomTime: 'Early to late April (Richland County: mid-April peak expected)',
203    aprilStatus: 'Currently blooming in northern Ohio. ODNR Week 4 (2026) confirms Virginia Bluebells are active in the northern region. Peak in Richland County expected around April 15–22.',
204    habitat: 'Moist floodplain forest, stream banks, bottomland woods',
205    whereToFind: 'Mohican State Park bottomlands, Malabar Farm (lower floodplain areas), Gorman Nature Center stream corridor',
206    tip: 'Floodplain colonies can be enormous — search near the Black Fork Mohican River in the Mohican area.'
207  },
208  {
209    id: 'trillium',
210    name: 'Large-flowered Trillium',
211    latin: 'Trillium grandiflorum',
212    status: 'starting',
213    statusLabel: 'Just Starting',
214    emoji: '🌷',
215    photoUrl: 'https://dam.assets.ohio.gov/image/upload/ohiodnr.gov/documents/natural-areas/Wildflowers/Spring/Large-flowered_Trillium4.jpg',
216    description: "Ohio's official state wildflower. Three white petals (turning pink with age) above three broad leaves — everything in threes. Colonies take 7–10 years to mature from seed, making them a testament to undisturbed forest quality. The ODNR Week 4 report (2026) confirms Large-flowered Trillium is beginning to bloom across northern Ohio.",
217    bloomTime: 'Mid-April to late May (Richland County: mid-April through May)',
218    aprilStatus: 'Just beginning in north-central Ohio as of April 10, 2026. Expect peak displays from approximately April 18 through May 5 in Richland County. Will continue into May.',
219    habitat: 'Rich mesic deciduous forest; beech-maple woodland slopes',
220    whereToFind: 'Malabar Farm (all wooded trails), Fowler Woods, Mohican State Park, Gorman Nature Center forest trails',
221    tip: "Ohio's state wildflower. Never pick Trillium — illegal in state nature preserves, and the plant requires years to re-establish from seed."
222  },
223  {
224    id: 'skunk',
225    name: 'Skunk Cabbage',
226    latin: 'Symplocarpus foetidus',
227    status: 'fading',
228    statusLabel: 'Past Peak',
229    emoji: '🌿',
230    photoUrl: 'https://trekohio.com/wp-content/uploads/20140507_flower-skunk-cabbage_img-1471.jpg',
231    description: 'Ohio\'s earliest spring wildflower — a true botanical marvel. Skunk Cabbage generates enough heat to melt surrounding snow (thermogenesis), emerging in February before any other wildflower. By April, the large tropical-looking leaves are fully expanded but the mottled spathe flower is past its prime. Worth seeing for its lush, prehistoric-looking foliage.',
232    bloomTime: 'February–March (past bloom by April; foliage through summer)',
233    aprilStatus: 'Bloom is past in Richland County. Large, lush green leaves are now fully expanded and visible in wetland areas. A good April sight even without the flower.',
234    habitat: 'Wet seeps, boggy stream margins, swamp forest edges',
235    whereToFind: 'Fowler Woods (buttonbush swamps), Gorman Nature Center (pond margins), wet roadside ditches throughout the county',
236    tip: 'Gently crush a leaf to experience the famously unpleasant odor (it attracts early pollinators). The leaves are harmless.'
237  },
238];
239
240// Bloom calendar data — [late Feb, Mar, early Apr, mid Apr, late Apr, May]
241// Values: 'peak', 'bloom', 'early', ''
242const CALENDAR_DATA = [
243  { name: 'Skunk Cabbage',      bars: ['peak', 'peak', 'bloom', 'early', '',       ''      ] },
244  { name: 'Bloodroot',          bars: ['',     'early','bloom', 'peak',  'bloom',  'early' ] },
245  { name: "Dutchman's Breeches",bars: ['',     'early','bloom', 'peak',  'bloom',  'early' ] },
246  { name: 'Spring Beauty',      bars: ['',     'early','bloom', 'peak',  'peak',   'bloom' ] },
247  { name: 'Hepatica',           bars: ['',     'peak', 'bloom', 'early', '',       ''      ] },
248  { name: 'Marsh Marigold',     bars: ['',     '',     'bloom', 'peak',  'peak',   'bloom' ] },
249  { name: 'Trout Lily',         bars: ['',     '',     'bloom', 'peak',  'bloom',  'early' ] },
250  { name: 'Virginia Bluebells', bars: ['',     '',     'early', 'peak',  'peak',   'bloom' ] },
251  { name: 'Wild Blue Phlox',    bars: ['',     '',     '',      'early', 'bloom',  'peak'  ] },
252  { name: 'Large-flowered Trillium', bars: ['','',    '',      'early', 'peak',   'peak'  ] },
253  { name: 'Mayapple',           bars: ['',     '',     '',      '',      'early',  'peak'  ] },
254  { name: 'Jack-in-the-Pulpit', bars: ['',     '',     '',      'early', 'bloom',  'peak'  ] },
255];
256
257// ── Petal Field Animation ──────────────────────────────
258function initPetals() {
259  const field = document.getElementById('petalField');
260  if (!field) return;
261  const petals = [
262    { w: 12, h: 8, delay: 0, dur: 14, left: 10 },
263    { w: 8,  h: 6, delay: 3, dur: 18, left: 25 },
264    { w: 15, h: 10, delay: 6, dur: 12, left: 45 },
265    { w: 10, h: 7, delay: 9, dur: 16, left: 65 },
266    { w: 7,  h: 5, delay: 2, dur: 20, left: 80 },
267    { w: 13, h: 9, delay: 11, dur: 15, left: 92 },
268    { w: 9,  h: 6, delay: 5, dur: 17, left: 38 },
269    { w: 11, h: 8, delay: 14, dur: 13, left: 70 },
270  ];
271  petals.forEach(p => {
272    const el = document.createElement('div');
273    el.className = 'petal';
274    el.style.cssText = `width:${p.w}px;height:${p.h}px;left:${p.left}%;bottom:-20px;animation-delay:${p.delay}s;animation-duration:${p.dur}s;`;
275    field.appendChild(el);
276  });
277}
278
279// ── MapLibre Map ──────────────────────────────────────
280let map, activeMarkerId = null;
281
282function buildRasterStyle() {
283  return {
284    version: 8,
285    sources: {
286      'raster-tiles': {
287        type: 'raster',
288        tiles: [
289          'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png',
290          'https://b.tile.openstreetmap.org/{z}/{x}/{y}.png',
291          'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png'
292        ],
293        tileSize: 256,
294        attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
295      }
296    },
297    layers: [{
298      id: 'raster-tiles',
299      type: 'raster',
300      source: 'raster-tiles',
301      minzoom: 0,
302      maxzoom: 19
303    }]
304  };
305}
306
307function initMap() {
308  const style = buildRasterStyle();
309
310  map = new maplibregl.Map({
311    container: 'map',
312    style: style,
313    center: [-82.45, 40.68],
314    zoom: 10.5,
315    attributionControl: true,
316  });
317
318  map.addControl(new maplibregl.NavigationControl(), 'top-right');
319  map.addControl(new maplibregl.ScaleControl({ unit: 'imperial' }), 'bottom-right');
320
321  map.on('error', (e) => {
322    console.warn('Map error:', e);
323    const mapEl = document.getElementById('map');
324    if (mapEl && !mapEl.querySelector('.map-fallback')) {
325      const msg = document.createElement('div');
326      msg.className = 'map-fallback';
327      msg.innerHTML = '<p style="padding:20px;color:#555;font-size:14px">Map tiles unavailable in this browser context. <a href="https://www.google.com/maps/@40.68,-82.45,11z" target="_blank" rel="noopener">Open in Google Maps</a> to navigate to locations.</p>';
328      mapEl.appendChild(msg);
329    }
330  });
331
332  map.on('load', () => {
333    LOCATIONS.forEach(loc => {
334      const el = document.createElement('div');
335      el.className = 'custom-marker';
336      el.innerHTML = createMarkerSVG(loc.color, loc.bloomStatus);
337      el.style.cursor = 'pointer';
338
339      const popup = new maplibregl.Popup({ offset: 30, closeButton: false, maxWidth: '260px' })
340        .setHTML(`
341          <div class="popup-title">${loc.name}</div>
342          <div class="popup-sub">${loc.type} · ${loc.county}</div>
343          <div class="popup-flowers">🌸 ${loc.species.slice(0, 3).join(' · ')}${loc.species.length > 3 ? ' +more' : ''}</div>
344        `);
345
346      new maplibregl.Marker({ element: el })
347        .setLngLat([loc.lng, loc.lat])
348        .setPopup(popup)
349        .addTo(map);
350
351      el.addEventListener('click', () => {
352        showLocationDetail(loc.id);
353        setActiveListItem(loc.id);
354      });
355    });
356  });
357}
358
359function createMarkerSVG(color, status) {
360  const pulseColor = status === 'blooming' || status === 'peaking' ? color : '#888';
361  return `<svg width="36" height="44" viewBox="0 0 36 44" fill="none" xmlns="http://www.w3.org/2000/svg">
362    <filter id="shadow-m"><feDropShadow dx="0" dy="2" stdDeviation="2" flood-opacity="0.25"/></filter>
363    <path d="M18 2C9.163 2 2 9.163 2 18C2 27.5 18 42 18 42C18 42 34 27.5 34 18C34 9.163 26.837 2 18 2Z" fill="${color}" filter="url(#shadow-m)"/>
364    <circle cx="18" cy="18" r="7" fill="white" opacity="0.9"/>
365    <circle cx="18" cy="18" r="4" fill="${color}"/>
366  </svg>`;
367}
368
369// ── Location List (Sidebar) ────────────────────────────
370function renderLocationList() {
371  const list = document.getElementById('locationList');
372  list.innerHTML = LOCATIONS.map(loc => `
373    <div class="location-item" data-id="${loc.id}" role="button" tabindex="0" aria-label="View ${loc.name}">
374      <div class="location-marker-dot" style="background:${loc.color}"></div>
375      <div class="location-item-content">
376        <h4>${loc.name}</h4>
377        <p>${loc.county}</p>
378      </div>
379      <span class="location-type-badge ${loc.typeClass}">${loc.type.split(' ').slice(0, 2).join(' ')}</span>
380    </div>
381  `).join('');
382
383  list.querySelectorAll('.location-item').forEach(item => {
384    item.addEventListener('click', () => {
385      const id = item.dataset.id;
386      showLocationDetail(id);
387      setActiveListItem(id);
388    });
389    item.addEventListener('keydown', e => {
390      if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); item.click(); }
391    });
392  });
393}
394
395function setActiveListItem(id) {
396  document.querySelectorAll('.location-item').forEach(el => el.classList.remove('active'));
397  const item = document.querySelector(`.location-item[data-id="${id}"]`);
398  if (item) item.classList.add('active');
399}
400
401function showLocationDetail(id) {
402  const loc = LOCATIONS.find(l => l.id === id);
403  if (!loc) return;
404
405  activeMarkerId = id;
406
407  const sidebarIntro = document.querySelector('.sidebar-intro');
408  const locationList = document.getElementById('locationList');
409  const locationDetail = document.getElementById('locationDetail');
410  const detailContent = document.getElementById('detailContent');
411
412  if (sidebarIntro) sidebarIntro.style.display = 'none';
413
414  const statusColors = { blooming: '#3a6b3c', starting: '#9a6e28', peaking: '#2c7a2e', fading: '#888' };
415  const statusLabels = { blooming: 'Actively Blooming', starting: 'Just Starting', peaking: 'At Peak', fading: 'Past Peak' };
416
417  detailContent.innerHTML = `
418    <div class="detail-title">${loc.name}</div>
419    <div class="detail-type">${loc.type} · ${loc.county}</div>
420
421    <div class="detail-section">
422      <h5>April 2026 Status</h5>
423      <p><span style="display:inline-flex;align-items:center;gap:6px;font-weight:500;color:${statusColors[loc.bloomStatus]}">
424        <svg width="8" height="8" viewBox="0 0 8 8"><circle cx="4" cy="4" r="4" fill="${statusColors[loc.bloomStatus]}"/></svg>
425        ${statusLabels[loc.bloomStatus]}
426      </span><br><small style="color:var(--color-text-muted)">${loc.peakWindow}</small></p>
427    </div>
428
429    <div class="detail-section">
430      <h5>Species to Find</h5>
431      <div class="species-chips">${loc.species.map(s => `<span class="species-chip">${s}</span>`).join('')}</div>
432    </div>
433
434    <div class="detail-section">
435      <h5>Trails</h5>
436      ${loc.trails.map(t => `<p style="margin-bottom:6px"><strong style="color:var(--color-text)">${t.name}</strong> — ${t.length}, ${t.difficulty} · Wildflowers: ${t.wildflowers}</p>`).join('')}
437    </div>
438
439    <div class="detail-section">
440      <h5>Parking</h5>
441      <p>${loc.parking}</p>
442    </div>
443
444    <div class="detail-section">
445      <h5>Accessibility</h5>
446      <p>${loc.accessibility}</p>
447    </div>
448
449    <div class="detail-section">
450      <h5>Hours</h5>
451      <p>${loc.hours}</p>
452    </div>
453
454    <div class="detail-section">
455      <h5>Guided Hikes & Programs</h5>
456      <p>${loc.guidedHikes}</p>
457    </div>
458
459    <div class="detail-section">
460      <h5>Links</h5>
461      <div class="detail-links">
462        <a href="${loc.mapLink}" target="_blank" rel="noopener">
463          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
464          Official page &amp; more info
465        </a>
466        <a href="https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(loc.address)}" target="_blank" rel="noopener">
467          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
468          Directions
469        </a>
470      </div>
471    </div>
472  `;
473
474  locationDetail.classList.remove('hidden');
475  locationList.style.display = 'none';
476
477  // Fly map to location
478  if (map) {
479    map.flyTo({ center: [loc.lng, loc.lat], zoom: 13, duration: 1200 });
480  }
481}
482
483document.addEventListener('DOMContentLoaded', () => {
484  const backBtn = document.getElementById('backBtn');
485  if (backBtn) {
486    backBtn.addEventListener('click', () => {
487      document.getElementById('locationDetail').classList.add('hidden');
488      document.getElementById('locationList').style.display = '';
489      const sidebarIntro = document.querySelector('.sidebar-intro');
490      if (sidebarIntro) sidebarIntro.style.display = '';
491      document.querySelectorAll('.location-item').forEach(el => el.classList.remove('active'));
492      if (map) map.flyTo({ center: [-82.45, 40.68], zoom: 10.5, duration: 1000 });
493    });
494  }
495});
496
497// ── Species Cards ─────────────────────────────────────
498function renderSpecies() {
499  const grid = document.getElementById('speciesGrid');
500  grid.innerHTML = SPECIES.map(sp => {
501    const statusClass = `status-${sp.status}`;
502    return `
503      <article class="species-card visible" data-status="${sp.status}" data-id="${sp.id}">
504        <img class="species-img" src="${sp.photoUrl}" alt="${sp.name} (${sp.latin}) wildflower in Ohio" loading="lazy"
505             onerror="this.style.display='none'; this.nextElementSibling.style.display='flex'">
506        <div class="species-img-placeholder" style="display:none">${sp.emoji}</div>
507        <div class="species-body">
508          <span class="species-status ${statusClass}">${sp.statusLabel}</span>
509          <div class="species-name">${sp.name}</div>
510          <div class="species-latin">${sp.latin}</div>
511          <p class="species-desc">${sp.description}</p>
512          <div class="species-meta">
513            <div class="species-meta-row"><strong>April 2026</strong><span>${sp.aprilStatus}</span></div>
514            <div class="species-meta-row"><strong>Bloom Time</strong><span>${sp.bloomTime}</span></div>
515            <div class="species-meta-row"><strong>Habitat</strong><span>${sp.habitat}</span></div>
516            <div class="species-meta-row"><strong>Find It At</strong><span>${sp.whereToFind}</span></div>
517            <div class="species-meta-row"><strong>Tip</strong><span>${sp.tip}</span></div>
518          </div>
519        </div>
520      </article>
521    `;
522  }).join('');
523}
524
525function initSpeciesFilter() {
526  const buttons = document.querySelectorAll('.filter-btn');
527  buttons.forEach(btn => {
528    btn.addEventListener('click', () => {
529      buttons.forEach(b => b.classList.remove('active'));
530      btn.classList.add('active');
531      const filter = btn.dataset.filter;
532      document.querySelectorAll('.species-card').forEach(card => {
533        if (filter === 'all' || card.dataset.status === filter) {
534          card.classList.add('visible');
535          card.style.display = '';
536        } else {
537          card.classList.remove('visible');
538          card.style.display = 'none';
539        }
540      });
541    });
542  });
543}
544
545// ── Bloom Calendar ────────────────────────────────────
546function renderCalendar() {
547  const body = document.getElementById('calendarBody');
548  const today = new Date();
549  // Highlight current approximate column (0=lateFeb, 1=Mar, 2=earlyApr, 3=midApr, 4=lateApr, 5=May)
550  // April 10 = mid-April column index 3
551  const currentCol = 3;
552
553  body.innerHTML = CALENDAR_DATA.map(row => `
554    <div class="cal-row">
555      <div class="cal-species-name">${row.name}</div>
556      <div class="cal-bar-area">
557        ${row.bars.map((b, i) => {
558          const cls = b === 'peak' ? 'cal-bar-peak' : b === 'bloom' ? 'cal-bar-bloom' : b === 'early' ? 'cal-bar-early' : 'cal-bar-empty';
559          const isNow = i === currentCol ? 'outline: 2px solid var(--color-primary); outline-offset: 2px; border-radius: 4px;' : '';
560          return `<div class="cal-bar-cell ${cls}" style="${isNow}" title="${b || 'not blooming'}" aria-label="${row.name} ${b || 'not blooming'}"></div>`;
561        }).join('')}
562      </div>
563    </div>
564  `).join('');
565}
566
567// ── Locations Cards ───────────────────────────────────
568function renderLocationCards() {
569  const grid = document.getElementById('locationsGrid');
570  grid.innerHTML = LOCATIONS.map(loc => `
571    <article class="location-card">
572      <div class="location-card-header">
573        <div>
574          <div class="location-card-title">${loc.name}</div>
575          <div style="font-size:var(--text-xs);color:var(--color-text-muted);margin-top:4px">${loc.county}</div>
576        </div>
577        <span class="location-type ${loc.typeClass}">${loc.type}</span>
578      </div>
579      <div class="location-card-body">
580        <p>${loc.description.substring(0, 200)}${loc.description.length > 200 ? '…' : ''}</p>
581        <div class="location-meta-grid">
582          <div class="meta-item"><span class="meta-label">Peak Window</span><span class="meta-value">${loc.peakWindow}</span></div>
583          <div class="meta-item"><span class="meta-label">Trail Length</span><span class="meta-value">${loc.trails.map(t => t.length).join(', ')}</span></div>
584          <div class="meta-item"><span class="meta-label">Parking</span><span class="meta-value">Free</span></div>
585          <div class="meta-item"><span class="meta-label">Guided Hikes</span><span class="meta-value">${loc.guidedHikes.split('.')[0]}</span></div>
586        </div>
587        <div class="location-species-list">
588          ${loc.species.slice(0,4).map(s => `<span class="species-pill">${s}</span>`).join('')}
589          ${loc.species.length > 4 ? `<span class="species-pill">+${loc.species.length - 4} more</span>` : ''}
590        </div>
591      </div>
592      <div class="location-card-footer">
593        <a href="${loc.mapLink}" target="_blank" rel="noopener" class="card-link">
594          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
595          Official Page
596        </a>
597        <a href="#map-section" class="card-link" onclick="showLocationDetail('${loc.id}'); setActiveListItem('${loc.id}');">
598          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
599          View on Map
600        </a>
601      </div>
602    </article>
603  `).join('');
604}
605
606// ── Init ──────────────────────────────────────────────
607document.addEventListener('DOMContentLoaded', () => {
608  initPetals();
609  initMap();
610  renderLocationList();
611  renderSpecies();
612  initSpeciesFilter();
613  renderCalendar();
614  renderLocationCards();
615
616  // Re-render icons after dynamic content injection
617  setTimeout(() => { if (window.lucide) lucide.createIcons(); }, 100);
618});
619
620// Expose for inline onclick in rendered HTML
621window.showLocationDetail = showLocationDetail;
622window.setActiveListItem = setActiveListItem;

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.