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 & 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.