1<!DOCTYPE html> 2<html lang="en"> 3<head> 4<!-- fm:gsc --> 5<meta name="google-site-verification" content="QpEeCmdWwnPNARZiMStbkTkUzeOlaJjdxlBZ3YAEKlk" /> 6 7<!-- fm:ga4 -->
vendor: 65 bytes, lines 7-8
7 8<script async src="https://www.googletagmanager.com/gtag/js?id=
8G-FZR6EVZ3YF
vendor: 13 bytes, line 8
8"></script>
9<script> 10
vendor: 136 bytes, lines 10-13
10window.dataLayer = window.dataLayer || []; 11 function gtag(){dataLayer.push(arguments);} 12 gtag('js', new Date()); 13 gtag('config', '
13G-FZR6EVZ3YF
vendor: 5 bytes, line 13
13');
14</script>
14 15<!-- /fm:ga4 --> 16 17<meta charset="UTF-8"> 18<meta name="viewport" content="width=device-width, initial-scale=1.0"> 19<title>FreshMinistries | Ending Poverty in Jacksonville Since 1989</title> 20<meta name="description" content="A Jacksonville nonprofit ending poverty since 1989 through workforce training, urban aquaponics, counseling, and youth programs that help neighbors stand on their own."> 21<link rel="canonical" href="https://freshministries.org/"> 22<link rel="preconnect" href="https://fonts.googleapis.com"> 23<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 24<link href="https://fonts.googleapis.com/css2?family=Marcellus&family=Inter:wght@400;500;600;700&family=Newsreader:ital,opsz,wght@0,6..72,400;1,6..72,400&family=Space+Mono&display=swap" rel="stylesheet"> 25<link rel="stylesheet" href="/fm-system.css"> 26<!-- FOUC guard: the hero text hides from FIRST PAINT, but only when the entrance will run 27 (JS on + motion OK). heroEntrance() unhides each piece exactly as its start state is 28 pinned, every bail path unhides, and a 3.5s failsafe unhides -- text can never stick 29 hidden. State-scoped descendant selectors = animation-layer CSS -> deliberate embed 30 in the Webflow port. -->
31<script>(function(){try{if(!(window.matchMedia&&matchMedia('(prefers-reduced-motion: reduce)').matches)){document.documentElement.className+=' fm-anim'}}catch(e){}})();</script>
31 32<style> 33.fm-anim .hero_hl-l1,.fm-anim .hero_hl-l2,.fm-anim .hero_sub,.fm-anim .hero_btn{visibility:hidden} 34</style> 35<!-- LOAD SEQUENCE GATE: the hero leads first, THEN the momentum carousel reveals + drifts. 36 FMHero.fire() runs when the hero cascade has led (~1.05s, success path) or immediately on any 37 hero bail; a 3.6s failsafe guarantees the carousel can never stick hidden. 38 r26 (hit list 7/24 #1): the carousel waits +1s AFTER the release (reveal ~2.05s on the 39 success path, ~4.6s absolute worst case) so it stops competing with the hero text/buttons. -->
40<script> 41window.FMHero=(function(){var done=false,q=[];return{onDone:function(cb){done?cb():q.push(cb);},fire:function(){if(done)return;done=true;var f;while(f=q.shift()){try{f();}catch(e){}}}};})(); 42setTimeout(function(){window.FMHero&&window.FMHero.fire();},3600); 43</script>
43 44 45<!-- ============================================================ 46 FRESH MINISTRIES HOMEPAGE -- assembled from converter-clean 47 library/sections, all sharing fm-system.css. 48 Order: nav-wave-menu / hero / program-cards / proof / programs / 49 map-stipple (GLOBAL) / news / give-horizon / v2-footer. 50 Shared CDN (GSAP + ScrollTrigger + Three.js) loaded ONCE near the 51 end of <body>, before the per-section IIFE scripts. 52 Nav anchors: Programs->#programs (programs), Global->#global 53 (map-stipple), About/Our Story->#mission (proof), News->#news, 54 Give->#give. Home logo -> #top (hero). 55 ============================================================ --> 56 57 58 59<!-- FM-NAV-FS:CSS --> 60<style id="fm-nav-fs"> 61/* self-contained: don't rely on a global reset for the overlay's box model */ 62.nav2,.nav2 *{box-sizing:border-box} 63 64/* ============================================================ 65 FRESHMINISTRIES -- FULL-SCREEN NAV OVERLAY (FS) 66 BUILD WAVE 2026-07-29 (BW-NAV-IA, PM-800 picks): desktop is C1-C 67 "The Horizon" -- search top center, one horizontal line of the 68 seven sections mid-screen, the 7-color runner resting on the rule 69 and gliding under the active item, detail panes in a fixed stage 70 below the line. Mobile (<=860px) is C1-B "Reading Order" -- search 71 on top, flush-left accordion, overview link first, indent killed. 72 Layers: nav2_solid (0, sliver-proof floor) < nav2_wave (1) < nav2_grid (2) 73 Sits at z-55, BELOW the z-60 .topnav, so the ONE hamburger (#navToggle) 74 morphs to X in place above the navy and stays the close control. 75 The 7/18 click-through gate lives in fm-system.css: 76 .nav2:not(.is-open):not(.is-wave) .nav2_panel{pointer-events:none} 77 Panels here MUST keep the .nav2_panel class or the gate stops 78 protecting them (the "squirrelly buttons" lesson). 79 NO SCROLLBARS in panes (locked): panes are overflow:hidden and 80 sized to fit; only the mobile sheet may scroll (hairline style). 81 ============================================================ */ 82.nav2{position:fixed;inset:0;z-index:55;opacity:0;visibility:hidden; 83 transition:opacity .4s ease,visibility .4s ease; 84 /* PROPOSED token (C1 kit): Foundation Blue lifted for navy ground -- raw 85 #295099 disappears on navy. Same hue, legible. Earn bucket label only. */ 86 --earnb:#7aa3e6} 87.nav2.is-open{opacity:1;visibility:visible} 88.nav2.is-wave{opacity:1;visibility:visible;transition:none} /* GSAP owns the reveal, no fade */ 89.nav2:focus{outline:none} 90 91/* --- SLIVER FIX: a solid navy floor the SAME hex as the lowest gradient stop, 92 stretched over the whole viewport BEHIND the wave. Transparent while the 93 wave sweeps (so the reveal still reads), then snapped opaque once the wave 94 has covered the screen -- the bottom edge can never show a page sliver. --- */ 95.nav2_solid{position:absolute;inset:0;z-index:0;background:var(--wave-floor,#0c1426);opacity:0;pointer-events:none; 96 transition:opacity .2s linear} 97.nav2.is-covered .nav2_solid{opacity:1} 98 99.nav2_wave{position:absolute;inset:0;z-index:1;pointer-events:none} 100.nav2_wave-svg{width:100%;height:100%;display:block} 101.nav2_wave-path{fill:url(#fsWaveFill)} 102 103/* --- content plane: ONE centered column. Search at the top, the horizon 104 line vertically centered, panes hanging in a fixed stage below it. --- */ 105.nav2_grid{position:relative;z-index:2;height:100%;width:100%; 106 display:flex;flex-direction:column;align-items:center; 107 padding:clamp(4.5rem,9vh,6.5rem) clamp(1.5rem,4vw,3rem) clamp(2rem,5vh,3rem); 108 opacity:0} 109.nav2.is-open .nav2_grid{opacity:1} 110.nav2.is-wave .nav2_grid{opacity:1} 111 112/* ---------- IN-MENU SEARCH (locked). A button styled as the kit's quiet 113 hairline field; clicking it opens the sitewide fm-search overlay 114 (z-2000, above this z-55 menu), which owns the real input. ---------- */ 115.nav2_search{display:flex;align-items:center;gap:.75rem;width:100%;max-width:22rem; 116 border:0;border-bottom:1px solid rgba(255,255,255,.28);background:none;cursor:pointer; 117 padding:.5625rem .25rem;margin:0 0 auto;color:rgba(255,255,255,.85); 118 font-family:var(--body);font-size:1rem;text-align:left; 119 transition:border-color .25s,color .25s} 120.nav2_search:hover{color:#fff;border-color:rgba(255,255,255,.55)} 121.nav2_search:focus-visible{outline:2px solid var(--sky);outline-offset:4px;border-radius:2px} 122.nav2_search svg{width:1.125rem;height:1.125rem;flex:none} 123.nav2_search-t{color:rgba(255,255,255,.42);transition:color .25s} 124.nav2_search:hover .nav2_search-t{color:rgba(255,255,255,.75)} 125 126/* ---------- THE LINE (C1-C horizon; becomes the accordion column on mobile) ---------- */ 127.nav2_list{position:relative;display:flex;gap:clamp(1.5rem,2.6vw,3rem);align-items:baseline;justify-content:center; 128 width:100%;max-width:76rem;padding-bottom:1.125rem;margin-bottom:auto; 129 border-bottom:1px solid rgba(255,255,255,.18)}
130.nav2_group{display:contents} /* heads join the line directly; panes position from .nav2_list */ 131.nav2_head{display:inline-flex;align-items:center;gap:.75rem} 132.nav2_item{position:relative;display:inline-flex;align-items:center; 133 font-family:var(--display);font-weight:400;font-size:clamp(1.3rem,1.85vw,1.6875rem);line-height:1.2; 134 color:rgba(255,255,255,.8);text-decoration:none;padding:0;cursor:pointer;background:none;border:0;text-align:left; 135 transition:color .25s ease} 136.nav2_item:hover,.nav2_item.is-active{color:#fff} 137.nav2_item:focus-visible{outline:2px solid var(--c,var(--sky));outline-offset:6px;border-radius:.35rem;color:#fff} 138/* GIVE -- orange at rest AND active: the one colored word on the line */ 139.nav2_item-give{color:var(--orange)} 140.nav2_item-give:hover,.nav2_item-give.is-active,.nav2_item-give:focus-visible{color:var(--orange)} 141 142/* the 7-color runner: rests centered on the horizon like a kicker, then glides 143 under whatever you touch (JS sets width + translateX; desktop only) */ 144.nav2_runner{position:absolute;bottom:-2px;left:0;height:3px;width:14rem;border-radius:2px;pointer-events:none; 145 background:linear-gradient(90deg,var(--blue),var(--sky),var(--teal),var(--yellow),var(--orange),var(--red),var(--purple)); 146 transition:transform .45s cubic-bezier(.2,.7,.2,1),width .35s cubic-bezier(.2,.7,.2,1)} 147 148/* caret toggle -- MOBILE ONLY (accordion) */ 149.nav2_caret{display:none;align-items:center;justify-content:center;width:2.75rem;height:2.75rem;flex:0 0 auto; 150 background:none;border:0;color:rgba(255,255,255,.62);cursor:pointer;-webkit-tap-highlight-color:transparent; 151 border-radius:.5rem;transition:color .2s} 152.nav2_caret:hover{color:#fff} 153.nav2_caret:focus-visible{outline:2px solid var(--sky);outline-offset:2px} 154.nav2_caret svg{width:1.5rem;height:1.5rem;display:block;transition:transform .3s cubic-bezier(.2,.7,.2,1)} 155.nav2_caret[aria-expanded="true"] svg{transform:rotate(180deg)} 156 157/* ---------- DETAIL PANES (the fixed stage below the line) ---------- 158 Absolutely stacked children of .nav2_list, so on mobile the same panels 159 fall back inline under their heads as the accordion. overflow:hidden -- 160 the no-scrollbar rule is locked; pane content is sized to fit. */ 161.nav2_panel{position:absolute;left:0;right:0;top:calc(100% + clamp(1.25rem,3.5vh,2.25rem)); 162 height:clamp(13rem,36vh,18.5rem); 163 display:flex;flex-direction:column;align-items:center;justify-content:flex-start;gap:1.125rem;text-align:center; 164 opacity:0;visibility:hidden;transform:translateY(10px);pointer-events:none;overflow:hidden; 165 transition:opacity .35s ease,transform .4s cubic-bezier(.2,.7,.2,1),visibility 0s .35s} 166.nav2_panel.is-active{opacity:1;visibility:visible;transform:none;pointer-events:auto; 167 transition:opacity .35s ease,transform .4s cubic-bezier(.2,.7,.2,1),visibility 0s 0s} 168 169/* overview link -- ONE element, two voices: desktop reads "Go to X" and sits 170 LAST in the pane (C1-C); mobile reads "X overview" and sits FIRST (C1-B). */ 171.nav2_over{display:inline-flex;align-items:center;gap:.4375rem;order:9; 172 font-family:var(--body);font-weight:600;font-size:.8125rem;letter-spacing:.1em;text-transform:uppercase; 173 color:var(--pc,var(--sky));text-decoration:none;transition:color .2s} 174.nav2_over::after{content:"\2192";letter-spacing:0;transition:transform .2s} 175.nav2_over:hover,.nav2_over:focus-visible{color:#fff;outline:none} 176.nav2_over:hover::after,.nav2_over:focus-visible::after{transform:translateX(4px)} 177.nav2_over:focus-visible{outline:2px solid var(--pc,var(--sky));outline-offset:4px;border-radius:.35rem} 178.nav2_over-ov{display:none} 179 180/* pane content grammar (kit .s- classes, productionised) */ 181.nav2_subrow{display:flex;gap:1rem 2.75rem;justify-content:center;flex-wrap:wrap} 182.nav2_subrow-sm{gap:.625rem 2rem;max-width:66rem} 183.nav2_sub{font-family:var(--display);font-weight:400;font-size:1.375rem;line-height:1.3;color:rgba(255,255,255,.92); 184 text-decoration:none;transition:color .2s} 185.nav2_sub:hover,.nav2_sub:focus-visible{color:var(--pc,var(--sky));outline:none} 186.nav2_sub:focus-visible{outline:2px solid var(--pc,var(--sky));outline-offset:4px;border-radius:.25rem} 187.nav2_sub-sm{font-size:1.125rem} 188.nav2_mini{font-family:var(--body);font-weight:700;font-size:.6875rem;letter-spacing:.14em;text-transform:uppercase; 189 color:rgba(255,255,255,.42);margin:.375rem 0 0} 190.nav2_quiet{font-family:var(--body);font-size:.9375rem;color:rgba(255,255,255,.55);margin:1rem 0 0} 191 192/* Programs pane: the four pillar buckets (locked order Learn / Earn / Eat / Heal) */ 193.nav2_bkts{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:2.25rem;width:100%;max-width:68rem;text-align:left} 194.nav2_bkt{display:flex;flex-direction:column;gap:.4375rem} 195.nav2_bktlabel{display:inline-flex;align-items:center;gap:.5625rem;font-family:var(--body);font-weight:700; 196 font-size:.75rem;letter-spacing:.14em;text-transform:uppercase;color:var(--bc,var(--sky));margin:0 0 .1875rem} 197.nav2_bktlabel::before{content:"";width:1.375rem;height:3px;border-radius:2px;background:var(--bc,var(--sky));flex:none} 198.nav2_bktlink{font-family:var(--display);font-weight:400;font-size:1.125rem;line-height:1.3; 199 color:rgba(255,255,255,.92);text-decoration:none;padding:.1875rem 0;transition:color .2s} 200.nav2_bktlink:hover,.nav2_bktlink:focus-visible{color:var(--bc,var(--sky));outline:none} 201.nav2_bktlink:focus-visible{outline:2px solid var(--bc,var(--sky));outline-offset:3px;border-radius:.25rem} 202.nav2_bktmore{font-family:var(--body);font-size:.8125rem;color:rgba(255,255,255,.5);text-decoration:none; 203 padding:.1875rem 0;transition:color .2s} 204.nav2_bktmore:hover,.nav2_bktmore:focus-visible{color:#fff;outline:none} 205.nav2_bktmore:focus-visible{outline:2px solid var(--bc,var(--sky));outline-offset:3px;border-radius:.25rem} 206 207/* News pane: three story cards. fm-cms.js (initNavNews) re-renders these rows 208 with the SAME classes -- .nav2_row / .nav2_rowname / .nav2_rowdesc names are 209 load-bearing, do not rename. */ 210.nav2_newslist{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:2.25rem;width:100%;max-width:62rem;text-align:left} 211.nav2_row{display:flex;flex-direction:column;gap:.4375rem;text-decoration:none} 212.nav2_rowname{font-family:var(--display);font-weight:400;font-size:1.125rem;line-height:1.3;color:#fff;margin:0; 213 display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;transition:color .2s} 214.nav2_row:hover .nav2_rowname,.nav2_row:focus-visible .nav2_rowname{color:var(--pc,var(--orange))} 215.nav2_row:focus-visible{outline:2px solid var(--pc,var(--orange));outline-offset:4px;border-radius:.25rem} 216.nav2_rowdesc{font-family:var(--body);font-size:.8125rem;line-height:1.5;color:rgba(255,255,255,.5);margin:0; 217 display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden} 218 219/* Follow pane: icon + wordmark, one quiet row */ 220.nav2_soc{display:flex;gap:2.25rem;justify-content:center;flex-wrap:wrap} 221.nav2_social-row{display:inline-flex;align-items:center;gap:.625rem;text-decoration:none; 222 color:rgba(255,255,255,.85);transition:color .2s} 223.nav2_social-ico{display:inline-flex;color:currentColor} 224.nav2_social-ico svg{width:1.375rem;height:1.375rem;display:block} 225.nav2_social-name{font-family:var(--display);font-weight:400;font-size:1.25rem;color:currentColor} 226.nav2_social-row:hover,.nav2_social-row:focus-visible{color:var(--pc,var(--sky));outline:none} 227.nav2_social-row:focus-visible{outline:2px solid var(--pc,var(--sky));outline-offset:4px;border-radius:.25rem} 228 229/* ============================================================ 230 RESPONSIVE -- C1-B "Reading Order" accordion: search on top, 231 flush-left children, overview link first, indent killed. 232 ============================================================ */ 233@media(max-width:860px){ 234 .nav2_grid{display:block;height:100%;overflow-y:auto; 235 padding:clamp(4.5rem,10vh,7rem) 8vw clamp(2rem,6vh,3rem); 236 /* if the sheet must scroll, a navy hairline, never the fat chrome bar (hit list 7/24) */ 237 scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.28) transparent} 238 .nav2_search{max-width:none;margin:0 0 1.5rem} 239 .nav2_list{display:flex;flex-direction:column;align-items:stretch;gap:.15rem; 240 max-width:none;border-bottom:0;padding-bottom:0;margin-bottom:0} 241 .nav2_runner{display:none}
242 .nav2_group{display:flex;flex-direction:column;align-items:stretch;width:100%} 243 .nav2_head{display:flex;width:100%;align-items:center;justify-content:space-between;gap:.5rem} 244 .nav2_item{font-size:clamp(1.75rem,7vw,2.4rem);padding:.14em 0} 245 .nav2_caret{display:inline-flex} 246 .nav2_group-follow{margin-top:clamp(1rem,3vh,1.75rem)} 247 .nav2_panel{position:static;height:auto;transform:none;opacity:1;visibility:visible;overflow:visible; 248 display:none;padding:.15rem 0 1.1rem;gap:.125rem;align-items:flex-start;text-align:left} 249 .nav2_panel.is-active{display:flex} 250 .nav2_over{order:0;padding:.25rem 0 .625rem;font-weight:700;font-size:.75rem;letter-spacing:.12em} 251 .nav2_over-go{display:none} 252 .nav2_over-ov{display:inline} 253 .nav2_subrow{flex-direction:column;align-items:flex-start;gap:0} 254 .nav2_sub{font-size:1.1875rem;padding:.3125rem 0} 255 .nav2_sub-sm{font-size:1.0625rem} 256 .nav2_mini{margin:.625rem 0 .125rem} 257 .nav2_bkts{display:flex;flex-direction:column;gap:0;max-width:none} 258 .nav2_bkt{gap:0} 259 .nav2_bktlabel{margin:.625rem 0 .125rem} 260 .nav2_bktlabel::before{width:1.125rem} 261 .nav2_bktlink{font-size:1.1875rem;padding:.3125rem 0} 262 .nav2_soc{flex-direction:column;align-items:flex-start;gap:0} 263 .nav2_social-row{padding:.3125rem 0} 264 .nav2_default{display:none !important} 265} 266 267/* reduced motion */ 268@media(prefers-reduced-motion:reduce){ 269 .nav2,.nav2_grid,.nav2_panel,.nav2_item,.nav2_runner,.nav2_sub,.nav2_over,.nav2_row, 270 .nav2_bktlink,.nav2_social-row,.nav2_search,.topnav_bar,.nav2_wave-path{ 271 transition:none !important} 272} 273 274/* Hamburger visible on ALL viewports now the FS overlay is the only nav. 275 (Belt-and-suspenders: the base .topnav_toggle is already inline-flex; this 276 also neutralises any stray .fmm markup if a legacy block ever survives.) */ 277.topnav .topnav_toggle{display:inline-flex} 278.topnav .fmm{display:none} 279 280/* ============================================================ 281 TOPNAV SEARCH TRIGGER + RIGHT-CLUSTER SPACING 282 fm-search.js injects <button class="fmsx_trigger"> back into .topnav_right 283 (the old <a class="topnav_search"> markup was removed 2026-07-14, so the base 284 .topnav_search rules no longer apply to anything). These HEAD rules present that 285 button correctly from FIRST PAINT -- never the browser's native grey button box -- 286 keep it visible on mobile where the base CSS hid the old search markup, and give 287 the right cluster real spacing so the hamburger isn't jammed in the corner. 288 fm-search.js layers its own hover / focus-visible / transition on top; the 289 lower-specificity .fmsx_trigger rules there do not conflict with these. 290 ============================================================ */ 291.topnav_right .fmsx_trigger{-webkit-appearance:none;appearance:none; 292 display:inline-flex;align-items:center;justify-content:center; 293 width:2.625rem;height:2.625rem;border-radius:50%; 294 background:none;border:0;padding:0;margin:0;box-shadow:none;font:inherit; 295 color:inherit;cursor:pointer} 296/* the search trigger is the replacement for the removed .topnav_search: keep it 297 shown on the viewports where the base CSS hides the old search/give markup */ 298@media(max-width:991px){ 299 .topnav_right .fmsx_trigger{display:inline-flex} 300} 301@media(max-width:767px){ 302 /* hit list 7/24: padding-right override REMOVED -- base .topnav shorthand pads both edges equally (right edge = left logo edge) */ 303 .topnav .topnav_right{gap:.75rem} /* search sits tight NEXT TO the hamburger; .topnav prefix wins the cascade on home, where the base topnav CSS bakes AFTER this block */ 304 .topnav .topnav_toggle{margin-left:0} 305 .topnav .topnav_brand{min-width:0;flex:0 1 auto;margin-right:.75rem} /* the wide wordmark yields under squeeze; it never presses the search icon */ 306 .topnav .topnav_logo{max-width:100%;object-fit:contain;object-position:left center} 307} 308</style> 309<!-- /FM-NAV-FS:CSS --> 310<!-- FM-SEO-HEAD:START (scripts/seo-head-inject.py -- do not hand-edit) --> 311<link rel="icon" href="/favicon.ico" sizes="48x48"> 312<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32.png"> 313<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16.png"> 314<link rel="apple-touch-icon" href="/apple-touch-icon.png"> 315<link rel="manifest" href="/site.webmanifest"> 316<meta name="theme-color" content="#295099"> 317<meta property="og:type" content="website"> 318<meta property="og:site_name" content="FreshMinistries"> 319<meta property="og:title" content="FreshMinistries | Ending Poverty in Jacksonville Since 1989"> 320<meta property="og:description" content="A Jacksonville nonprofit ending poverty since 1989 through workforce training, urban aquaponics, counseling, and youth programs that help neighbors stand on their own."> 321<meta property="og:url" content="https://freshministries.org/"> 322<meta property="og:image" content="https://freshministries.org/img/og-share.jpg">
323<meta property="og:image:width" content="1200"> 324<meta property="og:image:height" content="630"> 325<meta name="twitter:card" content="summary_large_image"> 326<meta name="twitter:title" content="FreshMinistries | Ending Poverty in Jacksonville Since 1989"> 327<meta name="twitter:description" content="A Jacksonville nonprofit ending poverty since 1989 through workforce training, urban aquaponics, counseling, and youth programs that help neighbors stand on their own."> 328<meta name="twitter:image" content="https://freshministries.org/img/og-share.jpg">
329<script type="application/ld+json">{"@context": "https://schema.org", "@type": "NGO", "name": "FreshMinistries", "alternateName": "Be The Change International", "url": "https://freshministries.org", "logo": "https://freshministries.org/img/fm-logo.png", "image": "https://freshministries.org/img/og-share.jpg", "description": "FreshMinistries is a Jacksonville, Florida nonprofit working to end poverty through workforce training, urban aquaponics, outpatient counseling, and youth programs.", "foundingDate": "1989", "taxID": "59-2967898", "address": {"@type": "PostalAddress", "streetAddress": "1131 N Laura Street", "addressLocality": "Jacksonville", "addressRegion": "FL", "postalCode": "32202", "addressCountry": "US"}, "sameAs": ["https://www.facebook.com/FreshMinistries/", "https://www.instagram.com/freshministries/", "https://www.youtube.com/channel/UCqlwglTByp-dF7UKQKvtSyw", "https://www.linkedin.com/company/fresh-ministries-inc/"], "contactPoint": {"@type": "ContactPoint", "telephone": "+1-904-355-0000", "contactType": "customer service", "areaServed": "US", "availableLanguage": "English"}}</script>
329 330<!-- FM-SEO-HEAD:END --> 331</head> 332<body class="fm-body" style="margin:0;background:#fff;overflow-x:hidden"> 333 334<!-- ============================================================ 335 SECTION 1 -- NAV + WAVE OVERLAY MENU (INVERTED / dark-hero variant) 336 ============================================================ --> 337<style> 338/* ============================================================ 339 FM SECTION -- NAV + WAVE OVERLAY MENU (INVERTED) 340 Fixed header. At the TOP it is transparent over the dark hero: 341 WHITE logo (fm-logo-clrwht.svg), WHITE search + hamburger, orange 342 GIVE pill. On scroll (.is-scrolled) the bar turns white, the logo 343 swaps to the dark fm-logo.svg, and the icons go blue-ink; GIVE stays 344 orange. Base tokens/fonts come from fm-system.css. 345 346 COLOR INHERITANCE (converter-clean): the icons read their tint from 347 the nav's own `color` (white at top, --blue-ink when scrolled) via 348 currentColor -- no descendant selectors, the state lives on .topnav. 349 The logo swap is wired in JS: it toggles .is-off / .is-on on the two 350 logo <img>s (class-only, element-self state), because a display swap 351 cannot be a descendant selector under converter-clean rules. 352 353 Class-only, single-underscore BEM, REM units, Webflow breakpoints 354 (991/767/479). 355 ============================================================ */ 356 357/* NAV -- fixed header; base = transparent over the dark hero, white icons */ 358.topnav{position:fixed;top:0;left:0;right:0;z-index:60;display:flex;justify-content:space-between;align-items:center;color:#fff; 359 padding:1.25rem clamp(1.5rem,2.8vw,2.875rem); 360 transition:transform .42s cubic-bezier(.45,0,.15,1),padding .35s,background .35s,box-shadow .35s,color .35s,opacity .3s ease,visibility .3s ease} 361.topnav.is-scrolled{padding:.75rem clamp(1.5rem,2.8vw,2.875rem);background:#fff;color:var(--blue-ink);box-shadow:0 1px 0 #e6ecf3,0 14px 34px -28px rgba(20,38,74,.5)} 362.topnav.is-tucked{transform:translateY(-100%)} 363.topnav.is-away{opacity:0;visibility:hidden;pointer-events:none} 364/* r22: menu-open state -- the nav bar STAYS PUT (z-60, above the z-55 overlay) so the ONE 365 hamburger morphs to X in place; everything else in the bar fades out. margin-right:var(--sbw) 366 offsets the vanished scrollbar so the toggle doesn't teleport when body scroll locks. */ 367.topnav.is-menu{background:transparent;box-shadow:none;color:#fff;margin-right:var(--sbw,0px)} 368.topnav.is-menu .topnav_brand,.topnav.is-menu .topnav_search,.topnav.is-menu .topnav_give{opacity:0;pointer-events:none} 369.topnav_brand,.topnav_search{transition:opacity .25s} 370 371.topnav_brand{position:relative;display:inline-flex;align-items:center} 372.topnav_logo{height:3.125rem;width:auto;transition:height .35s cubic-bezier(.45,0,.15,1),opacity .3s ease;display:block} 373.topnav_logo.is-small{height:2.5rem} 374/* r9: logo swap is now an OPACITY CROSSFADE (was a display none/block hard swap that 375 popped a "pipe" flash on scroll). The ink logo is stacked absolutely over the white 376 one so the brand box keeps the white logo's size -- no layout shift, smooth fade. 377 JS still toggles .is-off / .is-on per scroll state (class-only, element-self state). */ 378.topnav_logo-wht{opacity:1} 379.topnav_logo-ink{position:absolute;left:0;top:0;opacity:0} 380.topnav_logo-wht.is-off{opacity:0} 381.topnav_logo-ink.is-on{opacity:1} 382 383.topnav_right{display:flex;align-items:center;gap:1.125rem} 384.topnav_search{display:inline-flex;align-items:center;justify-content:center;width:2.625rem;height:2.625rem;border-radius:50%;color:inherit;transition:color .25s,background .25s} 385.topnav_search:hover{color:var(--sky);background:rgba(0,154,206,.12)} 386.topnav_search-svg{display:block} 387.topnav_give{font-size:.8125rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:#fff;text-decoration:none;background:var(--orange);padding:.6875rem 1.625rem;border-radius:999px;box-shadow:0 8px 22px -10px rgba(235,144,62,.75);transition:background .25s,color .25s,box-shadow .25s,transform .25s,opacity .25s} 388.topnav_give:hover{background:var(--red);color:#fff;transform:translateY(-1px);box-shadow:0 14px 28px -10px rgba(230,60,52,.75)} 389
390/* hamburger -- visible on desktop too (opens the wave overlay). Bars inherit 391 the nav's currentColor so they invert with the scroll state automatically. */ 392.topnav_toggle{display:inline-flex;width:3.25rem;height:3.25rem;flex-direction:column;align-items:center;justify-content:center;gap:.4375rem; 393 border:none;background:transparent;cursor:pointer;padding:0;color:inherit;-webkit-tap-highlight-color:transparent;transition:transform .2s ease} 394.topnav_toggle:active{transform:scale(.9)} /* r22: press feedback */ 395.topnav_bar{display:block;width:2.125rem;height:.15625rem;border-radius:2px;background:currentColor;transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .25s,background .3s,width .25s ease} 396.topnav_toggle:hover .topnav_bar-mid{width:1.4375rem} /* r22: hover feedback -- mid bar tucks */ 397/* r23: X geometry -- bar CENTERS sit .59375rem apart (.15625 bar + .4375 gap), so each 398 outer bar must travel the FULL .59375rem to cross dead-center (was .4375rem = off-axis X). */ 399.topnav_bar-top.is-open{transform:translateY(.59375rem) rotate(45deg);background:#fff} 400.topnav_bar-mid.is-open{opacity:0;background:#fff} 401.topnav_bar-bot.is-open{transform:translateY(-.59375rem) rotate(-45deg);background:#fff} 402.topnav_toggle:hover .topnav_bar-top.is-open,.topnav_toggle:hover .topnav_bar-bot.is-open{background:var(--red)} /* r22: X hover = red (was on the old close button) */ 403 404/* wave mode: JS-driven motion path shows instantly (no fade) so the wave does the reveal */ 405/* r22: separate close X REMOVED -- the hamburger itself is the close (morphs to X in place). */ 406/* r7: smaller link text + tighter vertical padding so the menu holds the socials cleanly */ 407/* r7: social icon row -- BELOW the links, ABOVE Give. href="#" placeholders -- FLAG: real FM social URLs needed. */ 408/* INTRINSIC ANIMATION (kept, flagged): parent-hover nudges the child badge. */ 409 410@media(max-width:991px){ 411 .topnav_search,.topnav_give{display:none} 412} 413/* r9 #6/#7 MOBILE NAV -- smaller logo (light at the top, not crammed), slimmer bar, 414 tighter scrolled state, small height delta so the scroll transition doesn't jump. */ 415@media(max-width:767px){ 416 .topnav{padding:.7rem clamp(1.1rem,4.6vw,1.5rem)} 417 .topnav{padding:.7rem clamp(1.1rem,4.6vw,1.5rem)} 418 .topnav.is-scrolled{padding:.4rem clamp(1.1rem,4.6vw,1.5rem)} 419 .topnav_logo{height:2.8rem} 420 .topnav_logo.is-small{height:1.9375rem} 421 .topnav_toggle{width:2.625rem;height:2.625rem;margin-left:.75rem} 422 .topnav_bar{width:1.75rem} 423} 424</style> 425<nav class="topnav" id="topnav"> 426 <a class="topnav_brand" href="/" aria-label="FreshMinistries home"> 427 <img class="topnav_logo topnav_logo-wht" src="/img/fm-logo-clrwht.svg" alt="FreshMinistries" data-img="logo-nav-wht"> 428 <img class="topnav_logo topnav_logo-ink" src="/img/fm-logo.svg" alt="" aria-hidden="true" data-img="logo-nav-ink"> 429 </a> 430 431 432 433 <div class="topnav_right"> 434 <a class="topnav_search" href="#" aria-label="Search"> 435 <svg class="topnav_search-svg" viewBox="0 0 24 24" width="21" height="21" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7.5"></circle><line x1="21" y1="21" x2="16.8" y2="16.8"></line></svg> 436 </a> 437 <a class="topnav_give" href="/give">Give</a> 438 <button class="topnav_toggle" id="navToggle" type="button" aria-label="Open menu" aria-expanded="false" aria-controls="fsMenu"> 439 <span class="topnav_bar topnav_bar-top"></span><span class="topnav_bar topnav_bar-mid"></span><span class="topnav_bar topnav_bar-bot"></span> 440 </button> 441 </div> 442</nav> 443<!-- FM-NAV-FS:MARKUP --> 444<!-- ============================================================ 445 FRESHMINISTRIES -- FULL-SCREEN NAV OVERLAY (FS) 446 Opened by the base topnav hamburger (#navToggle) on ALL viewports. 447 BUILD WAVE 2026-07-29 (BW-NAV-IA): C1-C "Horizon" desktop + C1-B 448 "Reading Order" mobile. Global panel regrouped per the ADOPTED IA: 449 three standing programs (WPM promoted out of "projects") + five 450 "Projects on the ground" place pages -- every place-name clicks 451 through to its own page, no dead-ends at global.html. 452 This block is baked IDENTICALLY on every production page and is 453 the source of truth (library/sections/nav-fullscreen-v2.html 454 mirrors it for preview only). 455 ============================================================ --> 456<div class="nav2" id="fsMenu" role="dialog" aria-modal="true" aria-label="Site menu" aria-hidden="true" tabindex="-1"> 457 458 <!-- sliver-proof solid navy floor (behind the wave) --> 459 <div class="nav2_solid" aria-hidden="true"></div> 460 461 <!-- the wave reveal (kept from the old mmenu) --> 462 <div class="nav2_wave" aria-hidden="true"> 463 <svg class="nav2_wave-svg" viewBox="0 0 1000 1000" preserveAspectRatio="none"> 464 <defs> 465 <linearGradient id="fsWaveFill" x1="0" y1="0" x2="0" y2="1"> 466 <stop offset="0" stop-color="#16335c"></stop> 467 <stop offset="0.5" stop-color="#11244a"></stop> 468 <stop offset="1" stop-color="#0c1426"></stop> 469 </linearGradient> 470 </defs> 471 <path class="nav2_wave-path" id="fsWavePath" d="M0 2S175 1 500 1s
471500 1 500 1V0H0Z"></path> 472 </svg> 473 </div> 474 475 <div class="nav2_grid"> 476 477 <!-- IN-MENU SEARCH (locked): opens the sitewide search overlay (fm-search.js, z-2000) --> 478 <button class="nav2_search" type="button" aria-label="Search the site"> 479 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.35-4.35"/></svg> 480 <span class="nav2_search-t">Search the site</span> 481 </button> 482 483 <!-- THE LINE (desktop horizon) / ACCORDION (mobile) --> 484 <nav class="nav2_list" aria-label="Primary"> 485 486 <!-- ABOUT --> 487 <div class="nav2_group"> 488 <div class="nav2_head"> 489 <a class="nav2_item" href="/about" data-panel="about" style="--c:var(--sky)">About</a> 490 <button class="nav2_caret" type="button" data-panel="about" aria-controls="fsPanel-about" aria-expanded="false" aria-label="Expand About sub-pages"> 491 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg> 492 </button> 493 </div> 494 <div class="nav2_panel nav2_about" id="fsPanel-about" data-panelbody="about" style="--pc:var(--sky)"> 495 <a class="nav2_over" href="/about"><span class="nav2_over-go">Go to About</span><span class="nav2_over-ov">About overview</span></a> 496 <div class="nav2_subrow"> 497 <a class="nav2_sub" href="/our-team">Our Team</a> 498 <a class="nav2_sub" href="/our-team#board">Our Board</a> 499 <a class="nav2_sub" href="/sources-of-support">Sources of Support</a> 500 </div> 501 </div> 502 </div> 503 504 <!-- PROGRAMS (locked pillars: Learn / Earn / Eat / Heal, in that order) --> 505 <div class="nav2_group"> 506 <div class="nav2_head"> 507 <a class="nav2_item" href="/programs" data-panel="programs" style="--c:var(--purple)">Programs</a> 508 <button class="nav2_caret" type="button" data-panel="programs" aria-controls="fsPanel-programs" aria-expanded="false" aria-label="Expand Programs sub-pages"> 509 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg> 510 </button> 511 </div> 512 <div class="nav2_panel nav2_programs" id="fsPanel-programs" data-panelbody="programs" style="--pc:var(--purple)"> 513 <a class="nav2_over" href="/programs"><span class="nav2_over-go">Go to Programs</span><span class="nav2_over-ov">Programs overview</span></a> 514 <div class="nav2_bkts"> 515 <div class="nav2_bkt" style="--bc:var(--purple)"> 516 <p class="nav2_bktlabel">Learn</p> 517 <a class="nav2_bktlink" href="/freshfutures">FreshFutures</a> 518 <a class="nav2_bktlink" href="/freshpath">FreshPath</a> 519 <a class="nav2_bktlink" href="/freshfoundations">FreshFoundations</a> 520 </div> 521 <div class="nav2_bkt" style="--bc:var(--earnb)"> 522 <p class="nav2_bktlabel">Earn</p> 523 <a class="nav2_bktlink" href="/lifepoint-career-institute">LifePoint Career Institute</a> 524 <a class="nav2_bktlink" href="/beaver-street-enterprise-center">Beaver Street Enterprise Center</a> 525 </div> 526 <div class="nav2_bkt" style="--bc:var(--teal)"> 527 <p class="nav2_bktlabel">Eat</p> 528 <a class="nav2_bktlink" href="/nativefresh">NativeFresh Aquaponics</a> 529 <a class="nav2_bktmore" href="/global">See the global work</a> 530 </div> 531 <div class="nav2_bkt" style="--bc:var(--sky)"> 532 <p class="nav2_bktlabel">Heal</p> 533 <a class="nav2_bktlink" href="/outpatient-counseling">Outpatient Counseling</a> 534 <a class="nav2_bktlink" href="/freshfamilies">FreshFamilies</a> 535 </div> 536 </div> 537 </div> 538 </div> 539 540 <!-- GLOBAL (ADOPTED IA 2026-07-29: programs first, then the ground) --> 541 <div class="nav2_group"> 542 <div class="nav2_head"> 543 <a class="nav2_item" href="/global" data-panel="global" style="--c:var(--teal)">Global</a> 544 <button class="nav2_caret" type="button" data-panel="global" aria-controls="fsPanel-global" aria-expanded="false" aria-label="Expand Global sub-pages"> 545 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg> 546 </button> 547 </div> 548 <div class="nav2_panel nav2_global" id="fsPanel-global" data-panelbody="global" style="--pc:var(--teal)"> 549 <a class="nav2_over" href="/global"><span class="nav2_over-go">Go to Global</span><span class="nav2_over-ov">Global overview</span></a> 550 <div class="nav2_subrow"> 551 <a class="nav2_sub" href="/aquaponics">Global Aquaponics</a> 552 <a class="nav2_sub" href="/desmond-tutu">The Desmond Tutu Programme</a> 553 <a class="nav2_sub" href="/worldwide-pilgrimage-ministries">Worldwide Pilgrimage Ministries</a> 554 </div> 555 <p class="nav2_mini">Projects on the ground</p> 556 <div class="nav2_subrow nav2_subrow-sm"> 557 <a class="nav2_sub nav2_sub-sm" href="/jubilee-housing-washington-dc">Washington, D.C. -- Jubilee Housing</a> 558 <a class="nav2_sub nav2_sub-sm" href="/island-fresh-st-croix">St. Croix, USVI -- Island Fresh</a> 559 <a class="nav2_sub nav2_sub-sm" href="/soweto-tutu-home">Soweto -- The Tutu Home</a> 560 <a class="nav2_sub nav2_sub-sm" href="/mpumalanga">Mpumalanga -- Acornhoek + Shalati</a> 561 <a class="nav2_sub nav2_sub-sm" href="/randburg-training-facility-johannesburg-south-africa">Randburg -- Training Facility</a> 562 </div> 563 </div> 564 </div> 565 566 <!-- NEWS (label -> news.html; desktop pane = latest 3; no mobile accordion, per C1-B) --> 567 <div class="nav2_group"> 568 <div class="nav2_head"> 569 <a class="nav2_item" href="/news" data-panel="news" style="--c:var(--orange)">News</a> 570 </div> 571 <div class="nav2_panel nav2_news" id="fsPanel-news" data-panelbody="news" style="--pc:var(--orange)"> 572 <!-- LATEST 3 ARTICLES -- fm-cms.js (initNavNews) replaces these rows on first 573 nav open, reusing the shared 'page:0' cache key (zero extra requests for 574 most visitors). The three rows below are the FALLBACK, deliberately kept: 575 if Sanity is unreachable or JS never runs, the menu still shows three real 576 stories instead of an empty pane. Refresh them if they go badly stale. --> 577 <div class="nav2_rows nav2_newslist" data-cms="nav-news" data-cms-limit="3"> 578 <a class="nav2_row" href="/news/desmond-tutu-programme-expands-soweto/"><span class="nav2_rowname">
578Desmond Tutu Hunger Programme Expands in Soweto</span><span class="nav2_rowdesc">Faith leaders and Rotary International gathered at Archbishop Tutu's Soweto home to mark the programme's expansion.</span></a> 579 <a class="nav2_row" href="/news/joannie-newton-freshministries-tribute/"><span class="nav2_rowname">Rest In Peace Joannie Newton</span><span class="nav2_rowdesc">A longtime FreshMinistries board member and benefactor, and our honorary cheer person for many years, has died.</span></a> 580 <a class="nav2_row" href="/news/freshministries-top-nonprofit-springfield-2025/"><span class="nav2_rowname">FreshMinistries Named Top 3 Non-Profit in Springfield for 2025</span><span class="nav2_rowdesc">BusinessRate named FreshMinistries a Top 3 non-profit of 2025, based entirely on verified Google reviews.</span></a> 581 </div> 582 <a class="nav2_over" href="/news"><span class="nav2_over-go">Read all news</span><span class="nav2_over-ov">All news</span></a> 583 </div> 584 </div> 585 586 <!-- GET INVOLVED (Give also lives here) --> 587 <div class="nav2_group"> 588 <div class="nav2_head"> 589 <a class="nav2_item" href="/get-involved" data-panel="getinvolved" style="--c:var(--red)">Get Involved</a> 590 <button class="nav2_caret" type="button" data-panel="getinvolved" aria-controls="fsPanel-getinvolved" aria-expanded="false" aria-label="Expand Get Involved sub-pages"> 591 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg> 592 </button> 593 </div> 594 <div class="nav2_panel nav2_getinvolved" id="fsPanel-getinvolved" data-panelbody="getinvolved" style="--pc:var(--red)"> 595 <a class="nav2_over" href="/get-involved"><span class="nav2_over-go">Go to Get Involved</span><span class="nav2_over-ov">Get Involved overview</span></a> 596 <div class="nav2_subrow"> 597 <a class="nav2_sub" href="/volunteer">Volunteer</a> 598 <a class="nav2_sub" href="/give">Give</a> 599 <a class="nav2_sub" href="/contact">Contact</a> 600 </div> 601 </div> 602 </div> 603 604 <!-- FOLLOW (no page; reveals socials; label toggles on mobile) --> 605 <div class="nav2_group nav2_group-follow"> 606 <div class="nav2_head"> 607 <button class="nav2_item nav2_item-follow" type="button" data-panel="follow" style="--c:var(--sky)" aria-controls="fsPanel-follow" aria-expanded="false">Follow</button> 608 <button class="nav2_caret" type="button" data-panel="follow" aria-controls="fsPanel-follow" aria-expanded="false" aria-label="Expand social links"> 609 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg> 610 </button> 611 </div> 612 <div class="nav2_panel nav2_follow" id="fsPanel-follow" data-panelbody="follow" style="--pc:var(--sky)"> 613 <div class="nav2_soc" aria-label="FreshMinistries on social media"> 614 <a class="nav2_social-row" href="https://www.facebook.com/FreshMinistries/" target="_blank" rel="noopener"> 615 <span class="nav2_social-ico" aria-hidden="true"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M13.5 21v-8h2.69l.4-3.12h-3.09V7.89c0-.9.25-1.52 1.55-1.52h1.65V3.57c-.29-.04-1.27-.13-2.41-.13-2.39 0-4.03 1.46-4.03 4.14v2.3H7.56V13h2.7v8h3.24z"/></svg></span><span class="nav2_social-name">Facebook</span> 616 </a> 617 <a class="nav2_social-row" href="https://www.instagram.com/freshministries/" target="_blank" rel="noopener"> 618 <span class="nav2_social-ico" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><rect x="3" y="3" width="18" height="18" rx="5"/><circle cx="12" cy="12" r="4"/><circle cx="17.4" cy="6.6" r="1.15" fill="currentColor" stroke="none"/></svg></span><span class="nav2_social-name">Instagram</span> 619 </a> 620 <a class="nav2_social-row" href="https://www.youtube.com/channel/UCqlwglTByp-dF7UKQKvtSyw" target="_blank" rel="noopener">
621 <span class="nav2_social-ico" aria-hidden="true"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M22.5 7.2c-.25-1-1-1.75-1.98-2.02C18.72 4.7 12 4.7 12 4.7s-6.72 0-8.52.48c-.98.27-1.73 1.02-1.98 2.02C1 9.02 1 12 1 12s0 2.98.5 4.8c.25 1 1 1.75 1.98 2.02C5.28 19.3 12 19.3 12 19.3s6.72 0 8.52-.48c.98-.27 1.73-1.02 1.98-2.02.5-1.82.5-4.8.5-4.8s0-2.98-.5-4.8zM9.75 15.5v-7l6 3.5-6 3.5z"/></svg></span><span class="nav2_social-name">YouTube</span> 622 </a> 623 <a class="nav2_social-row" href="https://www.linkedin.com/company/fresh-ministries-inc/" target="_blank" rel="noopener"> 624 <span class="nav2_social-ico" aria-hidden="true"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M6.94 5.5A1.94 1.94 0 1 1 3.06 5.5a1.94 1.94 0 0 1 3.88 0zM3.4 8.65h3.1V21H3.4V8.65zm5.72 0h2.97v1.69h.04c.41-.78 1.42-1.6 2.93-1.6 3.13 0 3.71 2.06 3.71 4.74V21h-3.1v-5.47c0-1.3-.02-2.98-1.81-2.98-1.82 0-2.1 1.42-2.1 2.88V21H9.12V8.65z"/></svg></span><span class="nav2_social-name">LinkedIn</span> 625 </a> 626 </div> 627 </div> 628 </div> 629 630 <!-- GIVE (own page; orange; hovering it rests the runner + default pane) --> 631 <div class="nav2_group nav2_group-give"> 632 <div class="nav2_head"> 633 <a class="nav2_item nav2_item-give" href="/give" data-panel="default" style="--c:var(--orange)">Give</a> 634 </div> 635 </div> 636 637 <!-- the 7-color runner (desktop only; JS glides it under the active item) -->
638 <span class="nav2_runner" id="fsRunner" aria-hidden="true"></span> 639 640 <!-- DEFAULT / resting pane (desktop only; hidden on mobile) --> 641 <div class="nav2_panel nav2_default is-active" data-panelbody="default" style="--pc:var(--sky)"> 642 <p class="nav2_quiet">From Jacksonville to the world. Hover a section.</p> 643 </div> 644 645 </nav> 646 </div><!-- /nav2_grid --> 647</div><!-- /nav2 --> 648<!-- /FM-NAV-FS:MARKUP --> 649 650 651<!-- ============================================================ 652 SECTION 2 -- HERO (DARK, text-reveal) 653 ============================================================ --> 654<style> 655/* ============================================================ 656 FM SECTION -- HERO (DARK) 657 Blue-ink field. White Marcellus headline at the biggest tier, a 658 faint cool-blue bloom, an --on-dark-2 subhead, and two pill CTAs 659 that pop on the dark ground (white-fill primary, white-outline 660 secondary). Keeps a GSAP text-reveal (per-char line 1, whole rainbow line 2). The background resolves 661 from --blue-ink at the top down to the ink #0c1426 at the bottom so 662 the hero flows seamlessly INTO the momentum carousel's ink band. 663 Base tokens/fonts come from fm-system.css. Class-only, single- 664 underscore BEM, REM units, Webflow breakpoints (991/767/479). 665 ============================================================ */ 666 667.hero{position:relative;text-align:center;overflow:hidden;color:#fff; 668 background:linear-gradient(180deg,#14264a 0%,#14264a 52%,#101d3a 78%,#0c1426 100%); 669 padding:clamp(7rem,15vh,11.25rem) 0 clamp(4rem,9vh,7rem)} 670/* faint COOL-BLUE bloom on the dark ground (sky over blue, low opacity) */ 671.hero::before{content:"";position:absolute;left:50%;top:-8%;width:120vw;height:60vh;transform:translateX(-50%); 672 background:radial-gradient(58% 100% at 50% 0%,rgba(0,154,206,.16) 0%,rgba(41,80,153,.12) 34%,transparent 72%);pointer-events:none} 673/* r6: headline container widened to ~90rem so each headline line sits on ONE line at 674 desktop widths. The subhead + CTA keep their own narrow max-widths (intentional stagger). */ 675.hero_inner{position:relative;z-index:2;max-width:90rem;margin:0 auto;padding:0 var(--pad)} 676.hero_hl{font-family:var(--display);font-weight:400;font-size:clamp(2.75rem,5.6vw,5.25rem);line-height:1.1; 677 letter-spacing:.004em;color:#fff;margin:0 auto 1.75rem} 678/* r18: the two hero lines are separate blocks; GSAP SplitText (mask:'lines') masks EACH VISUAL 679 line of each block, so a wrapped line 1 (mobile) never bleeds line 2 during the cascade, and 680 the descender room lives on .fm-splitline. No manual per-line mask element needed. */ 681.hero_hl-l1,.hero_hl-l2{display:block} 682/* r18: BOTH lines are SplitText-cascaded per visual line. Line 2's chars also get .hero_rainbow, 683 and the sweep is re-measured per visual line inside onSplit so it survives every re-split. */ 684/* r7: accent line -- brand RAINBOW clipped into the text (only the second hero line). 685 Fallback subtle sky for browsers without background-clip:text. The per-line GSAP reveal 686 animates opacity/transform on this same span, which still works with clipped text. */ 687.hero_accent{color:#3ab0dd} 688@supports ((-webkit-background-clip:text) or (background-clip:text)){ 689 .hero_accent{ 690 background:linear-gradient(90deg,#295099 0%,#009ace 17%,#00a287 33%,#fae537 50%,#eb903e 66%,#e63c34 83%,#9c3a8f 100%); 691 -webkit-background-clip:text;background-clip:text; 692 -webkit-text-fill-color:transparent;color:transparent; 693 } 694} 695/* r17: per-character rainbow for line 2 -- each char carries the SAME gradient; JS sets 696 background-size (full line-2 text width) + background-position-x (its x-offset) so the 697 per-char slices reconstitute ONE continuous sweep. Fallback solid sky without clip:text. */ 698.hero_rainbow{color:#3ab0dd} 699@supports ((-webkit-background-clip:text) or (background-clip:text)){ 700 .hero_rainbow{ 701 background:linear-gradient(90deg,#295099 0%,#009ace 17%,#00a287 33%,#fae537 50%,#eb903e 66%,#e63c34 83%,#9c3a8f 100%); 702 -webkit-background-clip:text;background-clip:text; 703 -webkit-text-fill-color:transparent;color:transparent; 704 background-repeat:no-repeat; 705 } 706} 707/* r7 UNIFIED SUBHEAD (matches programs / BTC / give) */ 708.hero_sub{font-family:var(--body);font-weight:400;font-size:clamp(1.0625rem,1.4vw,1.3125rem);line-height:1.65; 709 color:rgba(255,255,255,.6);max-width:54ch;margin:0 auto}
710.hero_cta{display:flex;gap:.875rem;justify-content:center;flex-wrap:wrap;margin-top:2.375rem} 711 712/* buttons -- section-specific 2-tier pill w/ circular arrow badge, retuned for the dark ground */ 713.hero_btn{display:inline-flex;align-items:center;gap:.75rem;font-family:var(--body);font-weight:600;font-size:.9375rem; 714 padding:.9375rem 1.875rem;border-radius:999px;border:none;cursor:pointer;text-decoration:none; 715 transition:transform .2s,box-shadow .25s,background .25s,color .25s} 716/* primary pops: white fill, blue-ink text */ 717.hero_btn-primary{background:#fff;color:var(--blue-ink);box-shadow:0 16px 34px -18px rgba(0,0,0,.6)} 718.hero_btn-primary:hover{transform:translateY(-.125rem);background:#eaf2ff} 719/* secondary: white outline (btn-white-outline language) */ 720.hero_btn-secondary{background:transparent;color:#fff;box-shadow:inset 0 0 0 1.6px rgba(255,255,255,.6)} 721.hero_btn-secondary:hover{background:#fff;color:var(--blue-ink);box-shadow:inset 0 0 0 1.6px #fff;transform:translateY(-.125rem)} 722.hero_btn-ico{display:grid;place-items:center;width:1.5rem;height:1.5rem;border-radius:50%;font-size:.8125rem;transition:transform .25s} 723/* INTRINSIC ANIMATION (kept, flagged): parent-hover nudging a child badge has no 724 class-only equivalent, so this single-level descendant is retained per the allowance. */ 725.hero_btn:hover .hero_btn-ico{transform:translateX(.1875rem)} 726 727@media(max-width:479px){ 728 .hero_cta{flex-direction:column;align-items:center} 729} 730</style> 731<header class="hero" id="top"> 732 <div class="hero_inner"> 733 <h1 class="hero_hl"> 734 <span class="hero_hl-l1">We help our neighbors first.</span>
735 <span class="hero_hl-l2 hero_accent">Then we help the world.</span> 736 </h1> 737 <p class="hero_sub">We started in Jacksonville in 1989 with one idea. We hand people the tools, and they do the rest. That work now reaches from our own backyard to communities around the world.</p> 738 <div class="hero_cta"> 739 <a class="hero_btn hero_btn-primary" href="/programs">See the Programs <span class="hero_btn-ico">→</span></a> 740 <a class="hero_btn hero_btn-secondary" href="/about">Our Story</a> 741 </div> 742 </div> 743</header> 744 745<!-- ============================================================ 746 SECTION 3 -- MOMENTUM-DRIFT CAROUSEL (top / LOCAL programs) 747 ============================================================ --> 748<style> 749/* ============================================================ 750 FM SECTION -- MOMENTUM-DRIFT CAROUSEL (namespace mdrift_) 751 A dark ink band (#0c1426) the hero descends into. The track lerps 752 toward your drag; idle it keeps a slow auto-drift; hover eases it to 753 a stop; a small tap opens a card, a real drag slides the band; the 754 wheel is never touched (vertical scroll belongs to the page). Edge 755 fades to the ink so cards dissolve into the band. Infinite wrap. 756 Reduced-motion = static (no rAF, no drag). Base tokens/fonts come 757 from fm-system.css. Class-only, single-underscore BEM, REM units, 758 Webflow breakpoints (991/767/479). 759 760 REVEAL NOTE: the card's hover (desktop) and .is-open (tap) reveals 761 animate the card's own children. A pure-CSS parent-hover that reaches 762 children -- and its .is-open tap twin -- have no class-only form (and 763 :hover must keep working on the JS-generated cards), so those single- 764 level descendant selectors are kept per the animation allowance, 765 grouped + flagged below. 766 ============================================================ */ 767 768/* r7: transparent bg + z-index 3 so the spanning globe shows behind the carousel band 769 while the cards/content sit above it. Edge-fades stay matched to #0c1426 (the wrapper 770 gradient starts at #0c1426, identical to the old carousel band -> seamless). */ 771.mdrift_stage{position:relative;z-index:3;background:transparent} 772.mdrift_viewport{position:relative;width:100%;overflow:hidden;touch-action:pan-y;cursor:grab;opacity:0;transition:opacity .9s ease} /* starts invisible -- cards are JS-built; fades in on fill (masks image pop-in) */ 773.mdrift_viewport.is-ready{opacity:1} 774.mdrift_viewport.is-grabbing{cursor:grabbing} 775.mdrift_track{display:flex;align-items:center;will-change:transform} 776.mdrift_card{position:relative;flex:0 0 auto;width:clamp(20rem,30vw,28.125rem);aspect-ratio:3/4;margin:1.625rem .5625rem;border-radius:.875rem;overflow:hidden; 777 background:var(--blue-ink);isolation:isolate;cursor:inherit;color:#fff;text-decoration:none; 778 transform:translateZ(0);backface-visibility:hidden;-webkit-backface-visibility:hidden;contain:paint} 779.mdrift_card::before{content:"";position:absolute;inset:0;z-index:5;border-radius:inherit;pointer-events:none; 780 box-shadow:inset 0 0 0 1px rgba(255,255,255,.3);transition:box-shadow .45s ease} 781.mdrift_card.is-tapped{animation:mdriftTap .42s cubic-bezier(.16,1,.3,1)} 782@keyframes mdriftTap{0%{transform:translateZ(0) scale(.965)}100%{transform:translateZ(0) scale(1)}} 783.mdrift_img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;transform:scale(1.06); 784 filter:grayscale(1) contrast(1.02) brightness(.94); 785 transition:filter .55s ease,transform .8s cubic-bezier(.16,1,.3,1);z-index:0;pointer-events:none;-webkit-user-drag:none} 786.mdrift_ov{position:absolute;inset:0;z-index:1;background:var(--c);opacity:.62;mix-blend-mode:multiply;transition:opacity .5s ease} 787.mdrift_shade{position:absolute;inset:0;z-index:2;background:linear-gradient(to top,rgba(7,12,26,.82) 4%,rgba(7,12,26,.15) 42%,transparent 66%)} 788/* r11: TOP scrim -- keeps the now-top-positioned name legible over the brightest photos */ 789.mdrift_top{position:absolute;inset:0;z-index:2;pointer-events:none;background:linear-gradient(to bottom,rgba(7,12,26,.62) 0%,rgba(7,12,26,.2) 24%,transparent 46%)} 790.mdrift_content{position:absolute;inset:0;z-index:3;display:flex;flex-direction:column;justify-content:space-between;padding:1.375rem 1.25rem 1.5rem} 791.mdrift_sum{display:block;font-size:.8125rem;line-height:1.5;color:rgba(255,255,255,.72);max-width:32ch; 792 max-height:0;opacity:0;overflow:hidden;transition:max-height .5s ease,opacity .45s ease,margin-bottom .5s ease} 793/* r11: foot group -- summary (hover-reveal) stacked above the icon-only arrow pill, pinned bottom-left */ 794.mdrift_foot{display:flex;flex-direction:column;align-items:flex-start} 795.mdrift_name{font-family:var(--display);font-weight:400;font-size:clamp(1.125rem,1.4vw,1.375rem);line-height:1.12;color:#fff;text-shadow:0 1px 14px rgba(0,0,0,.4);position:relative;margin:0} 796.mdrift_name::after{content:"";position:absolute;
796left:0;bottom:-.5rem;height:3px;width:2.625rem;border-radius:2px;background:var(--c);transform:scaleX(0);transform-origin:left;transition:transform .45s cubic-bezier(.16,1,.3,1)} 797.mdrift_go{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;color:#fff;padding:.5rem;border-radius:999px;background:var(--c);box-shadow:0 5px 16px -6px rgba(0,0,0,.55);opacity:0;transform:translateY(.375rem);transition:opacity .4s ease,transform .4s ease} 798.mdrift_go-svg{display:block;width:1.75rem;height:.8125rem} 799.mdrift_card:focus-visible{outline:3px solid #fff;outline-offset:-3px} 800 801/* REVEAL -- intrinsic hover/tap animation of the card's children (kept, flagged; see REVEAL NOTE) */ 802@media(hover:hover){ 803 .mdrift_card:hover .mdrift_img{filter:grayscale(0) contrast(1.04) brightness(1);transform:scale(1.1)} 804 .mdrift_card:hover .mdrift_ov{opacity:.2} 805 .mdrift_card:hover .mdrift_sum{max-height:5.5rem;opacity:1;margin-bottom:.875rem} 806 .mdrift_card:hover .mdrift_name::after{transform:scaleX(1)} 807 .mdrift_card:hover .mdrift_go{opacity:1;transform:none} 808 .mdrift_card:hover::before{box-shadow:inset 0 0 0 1px rgba(255,255,255,.34)} 809} 810.mdrift_card.is-open .mdrift_img{filter:grayscale(0) contrast(1.04) brightness(1);transform:scale(1.08)} 811.mdrift_card.is-open .mdrift_ov{opacity:.22} 812.mdrift_card.is-open .mdrift_sum{max-height:5.5rem;opacity:1;margin-bottom:.875rem} 813.mdrift_card.is-open .mdrift_name::after{transform:scaleX(1)} 814.mdrift_card.is-open .mdrift_go{opacity:1;transform:none} 815/* r9 #10: TOUCH open-state affordance -- the arrow becomes a clear circular "go" button so a 816 SECOND tap on the open card obviously navigates (grouped with the flagged is-open reveal). */ 817/* r10: MOBILE -- info (summary) + the "Learn more" CTA are ALWAYS shown; a single tap follows the link. 818 Shade pushed lower so more of the photo reads; name line-height eased for 2-line names. 819 r23 (owner 2026-07-06): (pointer:coarse) leg ADDED -- S Pen phones (Galaxy Ultra line) can 820 report hover:hover, which skips the hover:none leg; coarse-pointer catches them at any width. */ 821@media(hover:none),(pointer:coarse),(max-width:767px){ 822 .mdrift_sum{max-height:5.5rem;opacity:1;margin-bottom:.75rem} 823 .mdrift_name{line-height:1.2} 824 .mdrift_name::after{transform:scaleX(1)} 825 .mdrift_go{opacity:1;transform:none} 826 .mdrift_shade{background:linear-gradient(to top,rgba(7,12,26,.94) 0%,rgba(7,12,26,.7) 32%,rgba(7,12,26,.15) 56%,transparent 72%)} 827} 828 829/* r11: edge fades REMOVED (A4) -- their color no longer matched the surrounding gradient; cards clip clean at the section edge. */ 830 831@media(prefers-reduced-motion:reduce){ 832 .mdrift_img,.mdrift_ov,.mdrift_sum,.mdrift_name::after,.mdrift_go{transition:none} 833} 834@media(max-width:479px){ 835 .mdrift_card{width:74vw;margin:1.375rem .4375rem} 836} 837</style> 838<!-- r7: .dglobe_span EXTENDED UP to also enclose the momentum carousel (the "program 839 scroll"). Wrapper now spans [momentum carousel + proof + programs]; the faint 840 wireframe globe arcs behind the lower carousel, through the stats, into programs. 841 Carousel bg is transparent + z-index 3 so its cards sit ABOVE the globe. --> 842<div class="dglobe_span"> 843 <div class="dglobe_layer" aria-hidden="true"><canvas class="dglobe_canvas"></canvas></div> 844<section class="mdrift_stage" id="program-cards" aria-label="Our local programs"> 845 <div class="mdrift_viewport is-fade" id="mdriftVp" aria-label="Programs (drag to explore)"><div class="mdrift_track" id="mdriftTr"></div></div> 846</section> 847 848<!-- ============================================================
849 SPANNING WIREFRAME GLOBE WRAPPER -- wraps PROOF (SECTION 4) + PROGRAMS 850 (SECTION 5). One faint Three.js LineSegments lat/long globe (dglobe_) 851 arcs from the stats down behind local programs. r6: rendering converted 852 from dots to a faint wireframe sphere; span + natural spin unchanged. 853 Layer z1 (behind), section content z3 (above). 854 ============================================================ --> 855<style> 856/* dotted-globe spanning wrapper (converter-clean, class-only) 857 r6 GRADIENT SYSTEM: this wrapper carries the stats(proof)+programs journey -- 858 DARK ink at the top (stats), down through NORMAL blue at the stats/programs seam, 859 to LIGHT blue (#5f8bd0) at the very bottom of local programs. The stipple/BTC 860 section below picks up from #5f8bd0. proof + prog stay background:transparent. */ 861/* r7 GRADIENT -- one clean swell: DARK ink at the carousel/stats top -> MEDIUM blue -> 862 LIGHT blue (#5f8bd0) by the Local Programs floor. The stipple/BTC section picks up from 863 #5f8bd0. proof + prog stay background:transparent. */ 864.dglobe_span{position:relative;overflow:hidden;background:linear-gradient(180deg,#0c1426 0%,#14264a 18%,#295099 52%,#5f8bd0 100%)} 865/* r6: faint WIREFRAME globe -- thin lines read lighter than the old dots, so a touch more layer opacity keeps it legible */ 866.dglobe_layer{position:absolute;inset:0;z-index:1;pointer-events:none;overflow:hidden;opacity:0;transition:opacity 1.4s ease;-webkit-mask-image:linear-gradient(180deg,transparent 0%,#000 9%,#000 92%,transparent 100%);mask-image:linear-gradient(180deg,transparent 0%,#000 9%,#000 92%,transparent 100%)} 867.dglobe_layer.is-in{opacity:.5} 868.dglobe_canvas{position:absolute;inset:0;width:100%;height:100%;display:block} 869@media(max-width:991px){ .dglobe_layer.is-in{opacity:.28} } 870@media(max-width:767px){ .dglobe_layer.is-in{opacity:.24} } 871@media(prefers-reduced-motion:reduce){ .dglobe_layer{transition:none} } 872</style> 873 874<!-- ============================================================ 875 SECTION 4 -- PROOF / STATS (id="mission" -> About / Our Story) 876 Content tier of the spanning dotted-globe wrapper below. 877 ============================================================ --> 878<style> 879/* ============================================================ 880 FM SECTION -- PROOF / STATS
881 Background is TRANSPARENT so the .dglobe_span gradient + the spanning 882 dotted globe show through; padding + position kept. Content sits at 883 z-index 3 above the globe. The old boxed line-globe (.proof_stage/ 884 .proof_globe + its script) is removed -- Builder D's dotted globe now 885 spans proof -> programs behind this section. 886 Section-specific rules only; base tokens/classes from fm-system.css. 887 ============================================================ */ 888.proof{position:relative;overflow:hidden;color:#fff;background:transparent; 889 padding-block:clamp(6rem,16vh,12.5rem) clamp(3.5rem,9vh,7.25rem)} 890.proof_grid{position:relative;z-index:3} 891.proof_center{text-align:center} 892/* r7: standardized rhythm -- kicker->headline 1.5rem, headline->lead 1.25rem, lead->stats 2.25rem */ 893.proof_bar{margin:0 auto 1.5rem} 894.proof_big{font-family:var(--display);font-weight:400;font-size:clamp(5.125rem,13.5vw,12.25rem);line-height:.94;letter-spacing:-.012em;color:#fff} 895.proof_lead{font-family:var(--display);font-weight:400;font-size:clamp(1.1875rem,2vw,1.75rem);line-height:1.3;color:rgba(238,244,252,.86);margin:1.25rem auto 2.25rem;text-wrap:balance;max-width:24ch} 896.proof_stats{display:flex;gap:clamp(2.375rem,5vw,4.5rem);flex-wrap:wrap;justify-content:center;max-width:62.5rem;margin:0 auto} 897.proof_stat{min-width:9.375rem} 898.proof_num{font-family:var(--display);font-weight:400;font-size:clamp(2.125rem,3.7vw,3.25rem);color:var(--orange);line-height:1.04;letter-spacing:-.01em} 899.proof_slabel{font-family:var(--body);font-size:.84375rem;line-height:1.45;color:rgba(255,255,255,.66);margin-top:.75rem;letter-spacing:.005em} 900/* (odometer CSS removed 2026-07-03 -- the count-up is gone per owner; numbers are static) */ 901</style> 902<section class="section proof" id="mission" aria-label="Our impact"> 903 <div class="section_grid proof_grid"> 904 <div class="u-cols-2-7 proof_center"> 905 <span class="colorbar proof_bar" aria-hidden="true"><i class="colorbar_i colorbar_a"></i><i class="colorbar_i colorbar_b"></i><i class="colorbar_i colorbar_c"></i><i class="colorbar_i colorbar_d"></i><i class="colorbar_i colorbar_e"></i><i class="colorbar_i colorbar_f"></i><i class="colorbar_i colorbar_g"></i></span> 906 <div class="proof_big"><span class="count" data-target="4000" data-suffix="+">4,000+</span></div> 907 <div class="proof_lead">neighbors served every year, right here in Jacksonville.</div> 908 <div class="proof_stats"> 909 <div class="proof_stat"><div class="proof_num"><span class="count" data-target="37">37</span></div><div class="proof_slabel">years on the ground</div></div> 910 <div class="proof_stat"><div class="proof_num"><span class="count" data-target="750" data-suffix="+">750+</span></div><div class="proof_slabel">businesses launched at Beaver Street</div></div> 911 <div class="proof_stat"><div class="proof_num"><span class="count" data-target="72">72</span></div><div class="proof_slabel">schools reached</div></div> 912 </div> 913 </div> 914 </div> 915</section> 916 917<!-- ============================================================ 918 SECTION 5 -- LOCAL PROGRAMS INDEX (id="programs") -- inside .dglobe_span 919 ============================================================ --> 920<style> 921/* ============================================================ 922 FM SECTION -- LOCAL PROGRAMS INDEX (section-specific rules ONLY) 923 bg = --blue-ink (continuous dark descent). LEFT: colorbar + 924 headline + subhead + bare Marcellus accordion name-list (hover -> 925 brand-color tick + description drops via grid-template-rows + 926 white arrow slides in). RIGHT: two-column masonry, pulled IN from 927 the right edge so images read smaller/shorter and the block sits 928 close to the left text (no tall whitespace void). 929 State is toggled as classes on the element itself (is-on / is-open 930 / is-active / is-dim) by the IIFE -- NO descendant/hover CSS, NO 931 tag/ID selectors. Class-only . single-underscore BEM . REM . 932 Webflow breakpoints (991/767/479). 933 ============================================================ */ 934 935/* SECTION -- solid blue-ink, white text. position/overflow kept so the 936 spanning dotted-globe (mounted by conductor at z:1) sits above the 937 section's own bg fill and below content (z >= 3). */ 938.prog{position:relative;overflow:hidden;background:transparent;color:#fff; 939 padding-block:clamp(5rem,14vh,12.5rem) clamp(5.625rem,15vh,11.25rem)} 940/* r9 #2: center the left text column against the right image cluster (was align-items:start). */ 941.prog_grid{position:relative;z-index:3;align-items:center;column-gap:clamp(1.75rem,3.6vw,3.5rem)} 942 943/* LEFT -- headline + subhead + name list 944 r6: mild right inset pulls the text rail in a touch so it matches the contained 945 energy of the pulled-in masonry on the right (kept modest -- readable). */ 946.prog_left{position:relative;padding-right:clamp(0.75rem,3vw,3rem)} 947/* colorbar -> heading gap standardized to 1.375rem (bar removed in r6; rule kept harmless) */ 948.prog_bar{margin:0 0 1.375rem} 949/* heading -> tier-2 max clamp (equal to "Be The Change International"); brightest/largest step. 950 r7: headline -> subhead gap standardized to 1.25rem */ 951.prog_head{font-family:var(--display);font-weight:400;font-size:clamp(2.125rem,3.6vw,3.5rem);line-height:1.05;color:#fff;letter-spacing:-.01em;margin:0 0 1.25rem;max-width:16ch} 952/* r7 UNIFIED SUBHEAD (matches hero / BTC / give) -- a clear, smaller step below the headline. 953 subhead -> first list item gap standardized to 2.25rem */ 954.prog_sub{font-family:var(--body);font-weight:400;font-size:clamp(1.0625rem,1.4vw,1.3125rem);line-height:1.65;color:var(--on-dark-2);max-width:54ch;margin:0 0 2.25rem} 955 956/* bare type list on the blue bg. Divider lines only BETWEEN rows 957 (.prog_row-first drops its own top border -- class, not :first-of-type). */ 958.prog_list{position:relative} 959.prog_row{display:block;text-decoration:none;color:#fff;position:relative;border-top:1px solid rgba(255,255,255,.26)} 960.prog_row-first{border-top:none} 961.prog_top{display:flex;align-items:center;justify-content:space-between;gap:1.625rem;padding:clamp(1rem,1.9vw,1.5rem) 0}
962/* name base -> on-dark tier 2 (74%); .is-on brightens to #fff so hierarchy reads by opacity */ 963.prog_name{position:relative;font-family:var(--display);font-weight:400;font-size:clamp(1.3125rem,2.35vw,2.1875rem);line-height:1.14;color:var(--on-dark-2);transition:color .5s ease} 964.prog_name.is-on{color:#fff} 965/* program-color tick under the name on hover/focus (arrow stays WHITE -- reads better on the blue) */ 966.prog_name::after{content:"";position:absolute;left:0;bottom:-.5rem;height:3px;width:0;border-radius:2px;background:var(--c,#fff);transition:width .55s cubic-bezier(.22,1,.36,1)} 967.prog_name.is-on::after{width:3rem} 968/* r15 (#3): the arrow is a COLORED PILL button in the program's own --c (echoes the carousel 969 tiles' .mdrift_go): rounded, background:var(--c), white arrow. 970 r24 (owner 2026-07-14): it now has a RESTING STATE. It used to sit at opacity:0 until hover, 971 so the list read as dead text with no sign it was interactive. The arrow is now ALWAYS 972 visible -- a bare white arrow at the name's own resting tier (74%) -- and hover/focus/open 973 PROMOTES that same arrow to the colored pill via the existing .is-on. One component, two 974 states; no second arrow was invented. The pill takes whatever the row's own --c is, so 975 repeated program colors (FreshFutures + Outpatient are both purple, FreshFamilies + 976 FreshFoundations both sky) are a non-issue -- each row just paints its own. 977 The pill's padding is unchanged between states, so promoting it moves NOTHING. */ 978.prog_go{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;color:#fff;padding:.5rem;border-radius:999px; 979 background-color:transparent;box-shadow:none;opacity:.74; 980 transition:opacity .45s ease,background-color .45s ease,box-shadow .45s ease} 981.prog_go.is-on{opacity:1;background-color:var(--c,var(--orange));box-shadow:0 5px 16px -6px rgba(0,0,0,.55)} 982.prog_go-svg{display:block;width:1.5rem;height:.75rem} 983.prog_row:focus-visible{outline:2px solid #fff;outline-offset:3px} 984/* r25 HIT-AREA FIX: the ENTIRE block anchor is the hover/focus target, so the name, the 985 whitespace gap, the arrow pill, AND the full row height all give identical feedback with 986 NO JS dependency. These mirror the JS-set .is-on values 1:1 (same white name, same 3rem 987 tick, same colored pill) so the look + animation are unchanged -- this is purely a native 988 fallback that fires from anywhere on .prog_row. (Same hover+descendant pattern already used 989 by .swheel_card further down, so it is not a new convention for this file.) */ 990.prog_row:hover .prog_name, 991.prog_row:focus-visible .prog_name{color:#fff} 992.prog_row:hover .prog_name::after, 993.prog_row:focus-visible .prog_name::after{width:3rem} 994.prog_row:hover .prog_go, 995.prog_row:focus-visible .prog_go{opacity:1;background-color:var(--c,var(--orange));box-shadow:0 5px 16px -6px rgba(0,0,0,.55)} 996/* accordion description -- SMOOTH open via grid-template-rows (animates the REAL content height). 997 No-JS: rows default open (readable); JS adds .is-live to collapse; .is-open reopens the active row. */ 998.prog_desc{display:grid;grid-template-rows:1fr;transition:grid-template-rows .55s cubic-bezier(.22,1,.36,1)} 999.prog_desc.is-live{grid-template-rows:0fr} 1000.prog_desc.is-live.is-open{grid-template-rows:1fr} 1001/* clip wrapper carries NO padding; padding lives on the <p> inside instead. */ 1002.prog_desc-in{overflow:hidden;min-height:0} 1003/* description copy -> on-dark tier 2 (74%) per r5 spec */ 1004.prog_desc-p{margin:0;padding:0 clamp(1.125rem,2.4vw,2.5rem) clamp(1.375rem,2vw,2rem) 0;font-family:var(--body);font-size:clamp(.875rem,1.05vw,.96875rem);line-height:1.6;color:var(--on-dark-2);max-width:none} 1005/* r9 #11: per-program image that drops INSIDE the accordion on MOBILE (one image per open 1006 program, keyed to the row). Hidden on desktop -- desktop uses the right-side masonry. */ 1007.prog_descimg{display:none} 1008 1009/* RIGHT -- two-column masonry, PULLED IN from the right edge. 1010 Deterministic flex columns (not CSS `columns`, so the browser's 1011 column-balancer can't reshuffle and open a fresh void). The right 1012 inset narrows each column -> fixed-AR photos scale down -> shorter 1013 images -> the block's height reads close to the left text. */ 1014.prog_masonry{display:flex;gap:clamp(.625rem,1vw,1rem);padding-right:clamp(1.5rem,9vw,10rem)} 1015.prog_col{flex:1 1 0;min-width:0;display:flex;flex-direction:column;gap:clamp(.625rem,1vw,1rem)} 1016/* deliberate stagger: the right column drops down a touch */ 1017.prog_col-b{margin-top:clamp(1.5rem,5vw,3.25rem)} 1018.prog_ph{width:100%;display:block;object-fit:cover;border-radius:5px;background:var(--blue-ink); 1019 transition:filter .55s ease,opacity .55s ease,transform .55s cubic-bezier(.22,1,.36,1)} 1020/* hovering a program in the list brightens its photo (.is-active) + dims the rest (.is-dim) */ 1021.prog_ph.is-dim{filter:brightness(.34) saturate(.65);opacity:.72} 1022.prog_ph.is-active{filter:none;opacity:1;transform:scale(1.02)} 1023/* r25: each tile is now wrapped in a NATIVE <a class="prog_ph_link">
1023 so a click navigates via 1024 HTML with zero JS. display:block makes the anchor hug its .prog_ph child and BE the flex 1025 item in .prog_col, so the two-column layout + gaps + per-tile aspect ratios are unchanged. 1026 The tiles stay decorative duplicates of the canonical left-list nav (the anchors carry 1027 tabindex="-1" + aria-hidden in markup), so they are out of the tab order and the a11y tree. */ 1028.prog_ph_link{display:block} 1029/* NO-PHOTO placeholder (2026-07-16). Outpatient Counseling had a family candid and a 1030 personal selfie of identifiable people standing in for "substance use counseling" -- 1031 shipping those would imply real people are in substance-use treatment, so they were 1032 pulled. This is the same honest treatment the Outpatient PROGRAM page already uses 1033 (a tinted gradient + a label), in that program's own purple. It is a <div>, not an 1034 <img>: there is no photo to point at. Swap back to an <img> when a real one lands. */ 1035.prog_ph-empty{position:relative;background:linear-gradient(135deg,var(--purple),var(--blue))} 1036.prog_ph-empty::after{content:attr(data-label);position:absolute;inset:0;display:flex; 1037 align-items:center;justify-content:center;text-align:center;padding:0 1rem; 1038 font-family:var(--body);font-weight:700;font-size:.6875rem;letter-spacing:.14em; 1039 text-transform:uppercase;color:rgba(255,255,255,.85)} 1040/* per-photo aspect ratios (class-based so it stays converter-clean -- no :nth-child) */ 1041.ar-34{aspect-ratio:3/4} 1042.ar-11{aspect-ratio:1/1} 1043.ar-45{aspect-ratio:4/5} 1044.ar-43{aspect-ratio:4/3} 1045 1046@media(max-width:991px){ 1047 /* tighten the right inset on tablet so columns don't get too skinny */ 1048 .prog_masonry{padding-right:clamp(1rem,5vw,4rem)} 1049} 1050@media(max-width:767px){ 1051 /* r9 #11: single reading column. The desktop right-side masonry is REPLACED by an 1052 in-accordion image -- tap a program and its one photo drops under the name with the 1053 description. One image shows at a time (only the open row is expanded). */ 1054 .prog_head{max-width:none} 1055 .prog_left{padding-right:0} 1056 .prog_name{font-size:clamp(1.25rem,5.4vw,1.6875rem)} 1057 .prog_masonry{display:none} 1058 .prog_descimg{display:block;width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:7px;margin:0 0 1.125rem; 1059 filter:grayscale(.12) contrast(1.02)} 1060 .prog_desc-p{padding-right:0} 1061} 1062@media(prefers-reduced-motion:reduce){ 1063 .prog_name,.prog_go,.prog_desc,.prog_ph{transition:none} 1064} 1065</style> 1066<section class="section prog" id="programs" aria-label="Local programs"> 1067 <div class="section_grid prog_grid"> 1068 <!-- LEFT: colorbar + headline + subhead + the bare Marcellus program list --> 1069 <div class="u-cols-1-4 prog_left"> 1070 <!-- r6: colorbar removed over Local Programs (heading leads directly) --> 1071 <h2 class="prog_head">Local Programs</h2> 1072 <p class="prog_sub">Everything we run right here in Jacksonville. Most of it happens under one roof at the Weaver Center.</p> 1073 <nav class="prog_list" id="progList" aria-label="Programs"> 1074 <a class="prog_row prog_row-first" href="/freshfutures" data-key="freshfutures" style="--c:var(--purple)"> 1075 <span class="prog_top"><span class="prog_name">FreshFutures</span><span class="prog_go" aria-hidden="true"><svg class="prog_go-svg" viewBox="0 0 30 14"><path d="M0 7h27M22 2l5 5-5 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span></span> 1076 <span class="prog_desc"><span class="prog_desc-in"><img class="prog_descimg" src="/img/prog-freshfutures-b.jpg" alt="FreshFutures" loading="lazy"><p class="prog_desc-p">A year-long program for Duval County high-schoolers: financial literacy, college and job readiness, leadership, and academic help. It serves more than 80 students a year and ends in a paid six-week summer job for those who qualify.</p></span></span> 1077 </a> 1078 <a class="prog_row" href="/freshpath" data-key="freshpath" style="--c:var(--red)"> 1079 <span class="prog_top"><span class="prog_name">FreshPath</span><span class="prog_go" aria-hidden="true"><svg class="prog_go-svg" viewBox="0 0 30 14"><path d="M0 7h27M22 2l5 5-5 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span></span>
1080 <span class="prog_desc"><span class="prog_desc-in"><img class="prog_descimg" src="/img/prog-freshpath-b.jpg" alt="FreshPath" loading="lazy"><p class="prog_desc-p">A program for court-involved and at-risk youth, launched in 2014 with a U.S. Department of Labor grant. It serves 60 students a year with mentoring, tutoring, and work-readiness support, and finishing it can clear an eligible participant's record.</p></span></span> 1081 </a> 1082 <a class="prog_row" href="/freshfoundations" data-key="freshfoundations" style="--c:var(--sky)"> 1083 <span class="prog_top"><span class="prog_name">FreshFoundations</span><span class="prog_go" aria-hidden="true"><svg class="prog_go-svg" viewBox="0 0 30 14"><path d="M0 7h27M22 2l5 5-5 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span></span> 1084 <span class="prog_desc"><span class="prog_desc-in"><img class="prog_descimg" src="/img/prog-freshfoundations-b.jpg" alt="FreshFoundations" loading="lazy"><p class="prog_desc-p">Adult certifications and employability coaching, run out of the computer lab upstairs. Microsoft Office and other credentials employers actually ask for, plus the coaching to land the job and hold it.</p></span></span> 1085 </a> 1086 <a class="prog_row" href="/freshfamilies" data-key="freshfamilies" style="--c:var(--sky)"> 1087 <span class="prog_top"><span class="prog_name">FreshFamilies</span><span class="prog_go" aria-hidden="true"><svg class="prog_go-svg" viewBox="0 0 30 14"><path d="M0 7h27M22 2l5 5-5 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span></span> 1088 <span class="prog_desc"><span class="prog_desc-in"><img class="prog_descimg" src="/img/prog-families-b.jpg" alt="FreshFamilies" loading="lazy"><p class="prog_desc-p">Workshops that help families pull in the same direction, built on the 7 Habits. Parents and kids learn the same tools and use them at the same kitchen table.</p></span></span> 1089 </a> 1090 <a class="prog_row" href="/lifepoint-career-institute" data-key="lifepoint" style="--c:var(--orange)"> 1091 <span class="prog_top"><span class="prog_name">LifePoint Career Institute</span><span class="prog_go" aria-hidden="true"><svg class="prog_go-svg" viewBox="0 0 30 14"><path d="M0 7h27M22 2l5 5-5 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span></span> 1092 <span class="prog_desc"><span class="prog_desc-in"><img class="prog_descimg" src="/img/prog-lifepoint-b.jpg" alt="LifePoint Career Institute" loading="lazy"><p class="prog_desc-p">Vocational training for unemployed and underemployed adults. Ten weeks of hands-on and classroom instruction toward a hospitality certification accredited by the American Hotel and Lodging Association, with a life-skills component built in to help graduates keep the job.</p></span></span> 1093 </a> 1094 <a class="prog_row" href="/beaver-street-enterprise-center" data-key="bsec" style="--c:var(--blue)"> 1095 <span class="prog_top"><span class="prog_name">Beaver Street Enterprise Center</span><span class="prog_go" aria-hidden="true"><svg class="prog_go-svg" viewBox="0 0 30 14"><path d="M0 7h27M22 2l5 5-5 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span></span> 1096 <span class="prog_desc"><span class="prog_desc-in"><img class="prog_descimg" src="/img/prog-bsec-b.jpg" alt="Beaver Street Enterprise Center" loading="lazy"><p class="prog_desc-p">A full-service business incubator in northwest Jacksonville, named Business Incubator of the Year in 2010. It pairs professional office space with one-on-one coaching, mentoring, and financial-literacy support, and has helped launch hundreds of businesses, several of them now multimillion-dollar companies.</p></span></span> 1097 </a> 1098 <a class="prog_row" href="/outpatient-counseling" data-key="outpatient" style="--c:var(--purple)">
1099 <span class="prog_top"><span class="prog_name">Outpatient Counseling</span><span class="prog_go" aria-hidden="true"><svg class="prog_go-svg" viewBox="0 0 30 14"><path d="M0 7h27M22 2l5 5-5 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span></span> 1100 <span class="prog_desc"><span class="prog_desc-in"><!-- Face-free, consent-safe image (Pete 2026-07-17): two clasped hands, no identifiable people. Replaces the pulled candid; honest for the substance-use context. --><img class="prog_descimg" src="/img/prog-outpatient-hands.webp" alt="Two hands clasped in support" loading="lazy"><p class="prog_desc-p">Substance use counseling with licensed therapists, in person or online. Six clinicians, real appointments, and help getting past the things that usually stop people: a ride, childcare, a first step.</p></span></span> 1101 </a> 1102 <a class="prog_row" href="/nativefresh" data-key="aquaponics" style="--c:var(--teal)"> 1103 <span class="prog_top"><span class="prog_name">NativeFresh Aquaponics</span><span class="prog_go" aria-hidden="true"><svg class="prog_go-svg" viewBox="0 0 30 14"><path d="M0 7h27M22 2l5 5-5 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span></span> 1104 <span class="prog_desc"><span class="prog_desc-in"><img class="prog_descimg" src="/img/prog-aquaponics-b.jpg" alt="NativeFresh Aquaponics" loading="lazy"><p class="prog_desc-p">A 2,000-square-foot greenhouse behind the Weaver Center, rebuilt in 2024 as an off-grid, solar-powered operation. It grows more food on a fraction of the water. NativeFresh holds USDA Harmonized GAP certification and is licensed by the Florida Department of Agriculture. It doubles as a training hub: every partner we work with trains on a replica built right here.</p></span></span> 1105 </a> 1106 </nav> 1107 </div> 1108 1109 <!-- RIGHT: two-column masonry -- ALL 8 programs, condensed, a mix of square / portrait / 1110 horizontal ARs (alternating). data-keys drive the accordion hover-link. --> 1111 <div class="u-cols-5-8 prog_masonry" aria-hidden="true"> 1112 <div class="prog_col prog_col-a"> 1113 <a class="prog_ph_link" href="/freshfutures" tabindex="-1" aria-hidden="true"><img class="prog_ph ar-11" src="/img/opt/prog-freshfutures-c.jpg" alt="" loading="lazy" data-key="freshfutures"></a> 1114 <a class="prog_ph_link" href="/freshfoundations" tabindex="-1" aria-hidden="true"><img class="prog_ph ar-43" src="/img/opt/prog-freshfoundations-c.jpg" alt="" loading="lazy" data-key="freshfoundations"></a> 1115 <a class="prog_ph_link" href="/lifepoint-career-institute" tabindex="-1" aria-hidden="true"><img class="prog_ph ar-11" src="/img/opt/prog-lifepoint-c.jpg" alt="" loading="lazy" data-key="lifepoint"></a> 1116 <!-- Face-free, consent-safe image (Pete 2026-07-17): two clasped hands, no identifiable people. 1117 Replaces the pulled selfie; honest for the substance-use context. --> 1118 <a class="prog_ph_link" href="/outpatient-counseling" tabindex="-1" aria-hidden="true"><img class="prog_ph ar-34" src="/img/opt/prog-outpatient-hands.webp" alt="" loading="lazy" data-key="outpatient"></a> 1119 </div> 1120 <div class="prog_col prog_col-b"> 1121 <a class="prog_ph_link" href="/freshpath" tabindex="-1" aria-hidden="true"><img class="prog_ph ar-34" src="/img/opt/prog-freshpath-c.jpg" alt="" loading="lazy" data-key="freshpath"></a> 1122 <a class="prog_ph_link" href="/freshfamilies" tabindex="-1" aria-hidden="true"><img class="prog_ph ar-11" src="/img/opt/prog-families-c.jpg" alt="" loading="lazy" data-key="freshfamilies"></a> 1123 <a class="prog_ph_link" href="/beaver-street-enterprise-center" tabindex="-1" aria-hidden="true"><img class="prog_ph ar-43" src="/img/opt/prog-bsec-c.jpg" alt="" loading="lazy" data-key="bsec"></a> 1124 <a class="prog_ph_link" href="/nativefresh" tabindex="-1" aria-hidden="true"><img class="prog_ph ar-11" src="/img/opt/prog-aquaponics-c.jpg" alt="" loading="lazy" data-key="aquaponics"></a> 1125 </div> 1126 </div> 1127 </div> 1128</section> 1129</div> 1130<!-- /.dglobe_span --> 1131 1132<!-- ============================================================ 1133 SECTION 6 -- GLOBAL / STIPPLE MAP (id="global" -> nav Global)
1134 Desktop: stage capped ~75rem, centered (6-col feel, not full-bleed). 1135 <=991: full-bleed edge-to-edge, canvas cover-overflow ~10% each side. 1136 Mobile-STATIC preserved (no rAF loop / cursor FX on touch). 1137 ============================================================ --> 1138<style> 1139/* ============================================================ 1140 GLOBAL -- Stipple Map (section-specific rules ONLY) 1141 Fine ink-stipple of the continents (vintage cartography / currency 1142 feel), sampled from world-map-flat.jpg on a 2D canvas. Cursor adds a 1143 local heat-shimmer + ripple (desktop only). Base type/button/grid/ 1144 colorbar classes come from fm-system.css. 1145 Converter-clean: class-only, single-underscore BEM, REM, WF 1146 breakpoints (991/767/479). IDs on the canvas are JS-mount only. 1147 ============================================================ */ 1148/* r6 GRADIENT SYSTEM: the BTC/stipple section picks up the LIGHT blue (#5f8bd0) at its 1149 top (seamless with the programs bottom), dips back to DARK (#14264a) behind the map, 1150 then resolves to NORMAL blue (#295099) at its bottom -- the "undulation" middle. 1151 r6: bottom padding tightened so the map sits closer under the smart-wheel carousel. */ 1152/* r9 #8 GREEN EXPERIMENT -- routes light-blue -> a teal-GREEN midpoint -> dark blue. 1153 Top stays #5f8bd0 (seams with the programs floor), bottom stays #14264a (seams into the 1154 intl carousel). ONE-LINE REVERT: swap the active line for the r7 line commented below. 1155 r7 (revert to this): linear-gradient(180deg,#5f8bd0 0%,#5f8bd0 26%,#295099 62%,#14264a 100%) */ 1156.stipple{background:linear-gradient(180deg,#5f8bd0 0%,#5f8bd0 18%,#2f8f86 55%,#14264a 100%); overflow:hidden; padding-bottom:clamp(0.5rem,1.5vh,1.25rem);} 1157.stipple_head{display:flex; flex-direction:column; align-items:center; text-align:center; 1158 max-width:46rem; margin-left:auto; margin-right:auto; margin-bottom:clamp(2rem,5vh,3.5rem);} 1159.stipple_bar{margin-bottom:1.375rem;} /* colorbar -> heading gap (standardized) */ 1160.stipple_title{font-size:clamp(2.125rem,3.6vw,3.5rem); margin-bottom:1.25rem;} /* tier-2 (overrides .h1 size); headline->subhead 1.25rem */ 1161/* r7 UNIFIED SUBHEAD (replaces the old .body-lg lead; matches hero / programs / give). 1162 subhead -> button gap standardized to 2.25rem */ 1163.stipple_lead{font-family:var(--body); font-weight:400; font-size:clamp(1.0625rem,1.4vw,1.3125rem); line-height:1.65; 1164 color:var(--on-dark-2); max-width:54ch; margin-left:auto; margin-right:auto; margin-bottom:2.25rem;} 1165.stipple_cta{align-self:center;} 1166 1167/* ============================================================ 1168 r6: COMMUNITY-PEOPLE ROW (bcpeople_) -- replaces the colorbar above the 1169 "Be The Change International" heading. A compact centered row of 7 little 1170 figures, one per FM brand color, kicker-sized (~30px tall). Adapted converter- 1171 clean from motion/sections/community-people.html: class-only, single-underscore 1172 BEM, REM. Animation STATE is toggled on each element itself (is-armed / is- 1173 assembling on the slot, is-waving on the fig) -- NO descendant selectors, so it 1174 stays Moden-clean. --i / --dir are inline per slot and inherit to the fig. 1175 ============================================================ */ 1176.bcpeople_row{position:relative; z-index:2; display:flex; align-items:flex-end; justify-content:center; gap:0; 1177 margin:0 auto 1.5rem;} /* r7: row -> heading gap = 1.5rem (standardized rhythm) */ 1178/* r7: ~2x bigger -- slot ~20px wide -> figures ~60px tall, reads clearly as seven little people */ 1179.bcpeople_slot{width:clamp(0.95rem,2.8vw,1.25rem); display:flex; justify-content:center;} 1180.bcpeople_fig-anim{display:block; width:100%; transform-origin:50% 100%;} 1181.bcpeople_fig{display:block; width:100%; height:auto;} 1182/* armed (JS present): hide each figure until the assemble plays; no-JS shows the resting row */ 1183.bcpeople_slot.is-armed{opacity:0;} 1184@keyframes bcpeopleAssemble{ 1185 0%{transform:translateX(calc(var(--dir) * 2.75rem)); opacity:0;} 1186 100%{transform:translateX(0); opacity:1;} 1187} 1188.bcpeople_slot.is-assembling{animation:bcpeopleAssemble .72s cubic-bezier(.22,1,.36,1) both; animation-delay:calc(var(--i) * .12s);} 1189@keyframes bcpeopleBob{ 1190 0%,100%{transform:translateY(0) rotate(0deg);} 1191 28%{transform:translateY(-.375rem) rotate(-1.3deg);} 1192 56%{transform:translateY(0) rotate(0deg);} 1193} 1194.bcpeople_fig-anim.is-waving{animation:bcpeopleBob 1.9s ease-in-out infinite; animation-delay:calc(var(--i) * .11s);} 1195@media(prefers-reduced-motion:reduce){ 1196 .bcpeople_slot.is-armed{opacity:1;} 1197 .bcpeople_slot.is-assembling{animation:none; transform:none; opacity:1;} 1198 .bcpeople_fig-anim.is-waving{animation:none; transform:none;} 1199} 1200/* r9 #1 DESKTOP: full-width AND full-height, NO crop. The stage AR equals the map image AR 1201 (world-map-flat.jpg is 1920x1200 = 1.6), so the stage is exactly as tall as the map is at 1202 full width. With ZOOM=1.0 (JS) the desktop fit fills the width -> drawW=W, drawH=W/1.6 = 1203 stage height, offY=0, so the WHOLE map shows edge-to-edge with no continent clipped top or 1204 bottom. (Mobile keeps explicit heights + cover below, so aspect-ratio is ignored there.) */ 1205.stipple_stage{position:relative; width:100%; aspect-ratio:2.05; height:auto; overflow:hidden;} 1206.stipple_canvas{position:absolute; top:0; left:0; width:100%; height:100%; display:block; 1207 cursor:crosshair; touch-action:pan-y;} 1208.stipple_fallback{position:absolute; top:0; left:0; width:100%; height:100%; display:none; 1209 background-position:center; background-size:cover; background-repeat:no-repeat; 1210 opacity:.45; filter:brightness(2.4) grayscale(1) invert(1);} 1211 1212@media (max-width:991px){ 1213 /* TABLET/MOBILE: full-bleed edge-to-edge (canvas cover-overflow handled in JS layout). 1214 r6: reset width to 100% (desktop base is 92%) so the map stays edge-to-edge here. */ 1215 .stipple_stage{width:100%; max-width:none; height:clamp(20rem,50vh,38rem);} 1216} 1217@media (max-width:767px){ 1218 .stipple{padding-block:2.25rem;} /* tighten space above/below the map on mobile */ 1219 .stipple_head{max-width:none; margin-bottom:1.25rem;} 1220 .stipple_lead{margin-bottom:1.5rem;} 1221 .stipple_stage{height:clamp(22rem,62vh,34rem);} /* r14: taller so the mobile map reads bigger + more prominent */ 1222} 1223@media (max-width:479px){ 1224 .stipple{padding-block:1.75rem;} 1225 .stipple_stage{height:clamp(20rem,60vh,28rem);} /* r14: taller mobile map */ 1226} 1227 1228/* ============================================================ 1229 BTC STAT BAND (bcstat_) -- condensed dark-blue proof strip that
1230 ties the global block to the §8 dark-blue cards. Three numbers 1231 inline with thin vertical dividers, no hero stat. Numbers in 1232 Marcellus, labels in Inter. Class-only, REM, WF breakpoints; 1233 stacks on <=479. Placeholder numbers (see FLAG in markup). 1234 ============================================================ */ 1235/* r12: box removed -- the band now reads like the §4 proof stats (numbers on the section, 1236 no container/border/dividers). Dark-blue numbers, white labels, generous proof-stat gap. */ 1237.bcstat_band{display:flex; align-items:flex-start; justify-content:center; flex-wrap:wrap; 1238 gap:clamp(2.375rem,5vw,4.5rem); margin:0 auto 2.25rem;} 1239.bcstat_item{display:flex; flex-direction:column; align-items:center; text-align:center; gap:.75rem;} 1240/* r22: BTC stat numbers YELLOW per owner (was orange matching §4 proof; yellow pops on the ink band). */ 1241.bcstat_num{font-family:var(--display); font-weight:400; font-size:clamp(2.125rem,3.7vw,3.25rem); 1242 line-height:1.04; letter-spacing:-.01em; color:var(--yellow);} 1243.bcstat_label{font-family:var(--body); font-weight:400; font-size:.84375rem; line-height:1.45; 1244 letter-spacing:.005em; color:rgba(255,255,255,.66);} 1245/* harvest-hub body -- matches the subhead type step, centered, readable measure */ 1246.bcstat_body{font-family:var(--body); font-weight:400; font-size:clamp(1.0625rem,1.4vw,1.3125rem); 1247 line-height:1.65; color:var(--on-dark-2); max-width:62ch; margin:0 auto 2.25rem; text-align:center;} 1248 1249@media (max-width:479px){ 1250 .bcstat_band{flex-direction:column; gap:1.75rem;} 1251} 1252</style> 1253<section class="section stipple" id="global"> 1254 <div class="section_grid"> 1255 <div class="u-cols-2-7 stipple_head"> 1256 <!-- r6: 7 rainbow figures replace the colorbar over Be The Change International --> 1257 <div class="bcpeople_row" id="bcpeopleRow" role="img" aria-label="Seven figures in the Fresh Ministries rainbow, standing shoulder to shoulder"> 1258 <span class="bcpeople_slot" style="--i:0;--dir:-1;color:var(--blue)"><span class="bcpeople_fig-anim"><svg class="bcpeople_fig" viewBox="0 0 16.3 49.74" aria-hidden="true"><g fill="currentColor"><circle cx="8.15" cy="6.53" r="6.53"/><polygon points="16.3 13.46 0 13.46 0 17.69 3.88 17.69 7.52 49.74 9.29 49.74 12.46 17.69 16.3 17.69 16.3 13.46"/></g></svg></span></span> 1259 <span class="bcpeople_slot" style="--i:1;--dir:1;color:var(--sky)"><span class="bcpeople_fig-anim"><svg class="bcpeople_fig" viewBox="0 0 16.3 49.74" aria-hidden="true"><g fill="currentColor"><circle cx="8.15" cy="6.53" r="6.53"/><polygon points="16.3 13.46 0 13.46 0 17.69 3.88 17.69 7.52 49.74 9.29 49.74 12.46 17.69 16.3 17.69 16.3 13.46"/></g></svg></span></span> 1260 <span class="bcpeople_slot" style="--i:2;--dir:-1;color:var(--teal)"><span class="bcpeople_fig-anim"><svg class="bcpeople_fig" viewBox="0 0 16.3 49.74" aria-hidden="true"><g fill="currentColor"><circle cx="8.15" cy="6.53" r="6.53"/><polygon points="16.3 13.46 0 13.46 0 17.69 3.88 17.69 7.52 49.74 9.29 49.74 12.46 17.69 16.3 17.69 16.3 13.46"/></g></svg></span></span> 1261 <span class="bcpeople_slot" style="--i:3;--dir:1;color:var(--yellow)"><span class="bcpeople_fig-anim"><svg class="bcpeople_fig" viewBox="0 0 16.3 49.74" aria-hidden="true"><g fill="currentColor"><circle cx="8.15" cy="6.53" r="6.53"/><polygon points="16.3 13.46 0 13.46 0 17.69 3.88 17.69 7.52 49.74 9.29 49.74 12.46 17.69 16.3 17.69 16.3 13.46"/></g></svg></span></span> 1262 <span class="bcpeople_slot" style="--i:4;--dir:-1;color:var(--orange)"><span class="bcpeople_fig-anim"><svg class="bcpeople_fig" viewBox="0 0 16.3 49.74" aria-hidden="true"><g fill="currentColor"><circle cx="8.15" cy="6.53" r="6.53"/><polygon points="16.3 13.46 0 13.46 0 17.69 3.88 17.69 7.52 49.74 9.29 49.74 12.46 17.69 16.3 17.69 16.3 13.46"/></g></svg></span></span> 1263 <span class="bcpeople_slot" style="--i:5;--dir:1;color:var(--red)"><span class="bcpeople_fig-anim"><svg class="bcpeople_fig" viewBox="0 0 16.3 49.74" aria-hidden="true"><g fill="currentColor"><circle cx="8.15" cy="6.53" r="6.53"/><polygon points="16.3 13.46 0 13.46 0 17.69 3.88 17.69 7.52 49.74 9.29 49.74 12.46 17.69 16.3 17.69 16.3 13.46"/></g></svg></span></span>
1264 <span class="bcpeople_slot" style="--i:6;--dir:-1;color:var(--purple)"><span class="bcpeople_fig-anim"><svg class="bcpeople_fig" viewBox="0 0 16.3 49.74" aria-hidden="true"><g fill="currentColor"><circle cx="8.15" cy="6.53" r="6.53"/><polygon points="16.3 13.46 0 13.46 0 17.69 3.88 17.69 7.52 49.74 9.29 49.74 12.46 17.69 16.3 17.69 16.3 13.46"/></g></svg></span></span> 1265 </div> 1266 <h2 class="h1 text-white stipple_title">Be The Change International</h2> 1267 <p class="stipple_lead">What we learned fighting poverty in Jacksonville does not stop at the city line. The model is simple, and it travels.</p> 1268 <!-- FLAG: BTC stat band numbers are placeholders pending FM (bishop count disputed; 10,000 timeframe unsettled) --> 1269 <div class="bcstat_band"> 1270 <div class="bcstat_item"><span class="bcstat_num"><span class="count" data-target="7">7</span></span><span class="bcstat_label">African countries</span></div> 1271 <div class="bcstat_item"><span class="bcstat_num"><span class="count" data-target="220">220</span></span><span class="bcstat_label">bishops pledged</span></div> 1272 <div class="bcstat_item"><span class="bcstat_num"><span class="count" data-target="10000">10,000</span></span><span class="bcstat_label">systems, the goal</span></div> 1273 </div> 1274 <p class="bcstat_body">In some of the places we work, nearly half of what a farmer grows spoils before it reaches a market. A harvest hub changes that math. Greenhouses, cold storage, dehydration, solar power, and satellite internet, all in one place, open to every farmer nearby. The crop gets saved. The income stays home.</p> 1275 <a class="btn btn-white stipple_cta" href="/global">Explore the Global Work</a> 1276 </div> 1277 </div> 1278 1279 <div class="stipple_stage"> 1280 <canvas id="stippleCanvas" class="stipple_canvas" aria-hidden="true"></canvas> 1281 <div class="stipple_fallback" id="stippleFallback" aria-hidden="true"></div> 1282 </div> 1283</section> 1284 1285<!-- ============================================================ 1286 SMART-WHEEL INTL CAROUSEL (NEW -- below the map, above give) 1287 Blue-ink field (the section IS the background) with edge-fades to 1288 blue-ink. Direction-aware wheel (trackpad horizontal scrubs / mouse 1289 wheel scrolls page), drag-to-slide, tap-to-open, idle auto-drift. 1290 Namespaced swheel_ (must NOT collide with Builder A's mdrift_). 1291 ============================================================ --> 1292<style> 1293/* ============================================================ 1294 SMART-WHEEL -- Be The Change International (section-only) 1295 Foundation: fm-system.css supplies tokens/fonts. Section-specific: 1296 the card treatment + the trackpad-aware wheel band. 1297 Converter-clean: class-only, single-underscore BEM, REM, WF 1298 breakpoints. IDs are JS-mount only -- never styled here. 1299 ============================================================ */ 1300/* r6 GRADIENT SYSTEM: the smart-wheel band is NORMAL blue (#295099) -- it seams up into 1301 the stipple bottom (#295099) and carries down into give's top-fade (#295099). 1302 r6: top padding tightened so the carousel sits closer under the stipple map. */ 1303/* r7 GRADIENT: solid DARK (#14264a) -- seams up into the stipple bottom (#14264a) and 1304 carries down into give, which now opens from the same dark. */ 1305.swheel{position:relative; background:linear-gradient(180deg,#14264a 0%,#101d3a 50%,#0c1426 100%); overflow:hidden; 1306 padding:clamp(0.75rem,2vh,1.5rem) 0 clamp(3rem,7vh,5.5rem);} 1307.swheel_kicker{text-align:center; font-family:'Space Mono',monospace; font-size:.75rem; letter-spacing:.22em; 1308 text-transform:uppercase; color:var(--on-dark-3); margin-bottom:clamp(1.25rem,3vh,2rem);} 1309 1310.swheel_viewport{position:relative; width:100%; overflow:hidden; touch-action:pan-y; cursor:grab; opacity:0; transition:opacity .9s ease;} /* starts invisible -- cards are JS-built; fades in on fill (masks image pop-in) */ 1311.swheel_viewport.is-ready{opacity:1;} 1312.swheel_grabbing{cursor:grabbing;} /* toggled on the viewport during drag (cards inherit) */ 1313.swheel_track{display:flex; align-items:center; will-change:transform;} 1314 1315.swheel_card{position:relative; flex:0 0 auto; width:clamp(15rem,22vw,18.75rem); aspect-ratio:3/4; 1316 margin:1.625rem .5625rem; border-radius:.875rem; overflow:hidden; background:var(--blue-ink); 1317 isolation:isolate; cursor:inherit; color:#fff; transform:translateZ(0); backface-visibility:hidden; contain:paint;} 1318.swheel_card::before{content:""; position:absolute; inset:0; z-index:5; border-radius:inherit; pointer-events:none; 1319 box-shadow:inset 0 0 0 1px rgba(255,255,255,.3);
1319 transition:box-shadow .45s ease;} 1320.swheel_card.swheel_tapped{animation:swheelTap .42s cubic-bezier(.16,1,.3,1);} 1321@keyframes swheelTap{0%{transform:translateZ(0) scale(.965)}100%{transform:translateZ(0) scale(1)}} 1322.swheel_img{position:absolute; inset:0; width:100%; height:100%; object-fit:cover; transform:scale(1.06); 1323 filter:grayscale(1) contrast(1.02) brightness(.94); 1324 transition:filter .55s ease,transform .8s cubic-bezier(.16,1,.3,1); z-index:0; pointer-events:none; -webkit-user-drag:none;} 1325.swheel_ov{position:absolute; inset:0; z-index:1; background:var(--swheel-c); opacity:.62; mix-blend-mode:multiply; transition:opacity .5s ease;} 1326.swheel_shade{position:absolute; inset:0; z-index:2; background:linear-gradient(to top,rgba(7,12,26,.82) 4%,rgba(7,12,26,.15) 42%,transparent 66%);} 1327/* r11: TOP scrim -- keeps the now-top-positioned name legible over the brightest photos */ 1328.swheel_top{position:absolute; inset:0; z-index:2; pointer-events:none; background:linear-gradient(to bottom,rgba(7,12,26,.62) 0%,rgba(7,12,26,.2) 24%,transparent 46%);} 1329.swheel_content{position:absolute; inset:0; z-index:3; display:flex; flex-direction:column; justify-content:space-between; padding:1.375rem 1.25rem 1.5rem;} 1330.swheel_sum{display:block; font-size:.8125rem; line-height:1.5; color:rgba(255,255,255,.72); max-width:32ch; 1331 max-height:0; opacity:0; overflow:hidden; transition:max-height .5s ease,opacity .45s ease,margin-bottom .5s ease;} 1332/* r11: foot group -- summary (hover-reveal) stacked above the icon-only arrow pill, pinned bottom-left */ 1333.swheel_foot{display:flex; flex-direction:column; align-items:flex-start;} 1334.swheel_name{font-family:var(--display); font-weight:400; font-size:clamp(1.125rem,1.4vw,1.375rem); line-height:1.12; 1335 color:#fff; text-shadow:0 1px 14px rgba(0,0,0,.4); position:relative; margin:0;} 1336.swheel_name::after{content:""; position:absolute; left:0; bottom:-.5rem; height:.1875rem; width:2.625rem; border-radius:2px; 1337 background:var(--swheel-c); transform:scaleX(0); transform-origin:left; transition:transform .45s cubic-bezier(.16,1,.3,1);} 1338.swheel_go{flex:0 0 auto; display:inline-flex; align-items:center; justify-content:center; color:#fff; padding:.5rem; border-radius:999px; background:var(--swheel-c); box-shadow:0 5px 16px -6px rgba(0,0,0,.55); opacity:0; transform:translateY(.375rem); 1339 transition:opacity .4s ease,transform .4s ease;} 1340.swheel_arrow{display:block; width:1.75rem; height:.8125rem;} 1341 1342/* INTRINSIC-ANIMATION HOVER EXCEPTION (allowed): parent-hover reveals its own children. 1343 These parent-hover -> child descendant selectors are the sanctioned exception to the 1344 class-only rule; they carry the card's hover reveal and must stay together. */ 1345@media(hover:hover){ 1346 .swheel_card:hover .swheel_img{filter:grayscale(0) contrast(1.04) brightness(1); transform:scale(1.1);} 1347 .swheel_card:hover .swheel_ov{opacity:.2;} 1348 .swheel_card:hover .swheel_sum{max-height:5.5rem; opacity:1; margin-bottom:.875rem;} 1349 .swheel_card:hover .swheel_name::after{transform:scaleX(1);} 1350 .swheel_card:hover .swheel_go{opacity:1; transform:none;} 1351 .swheel_card:hover::before{box-shadow:inset 0 0 0 1px rgba(255,255,255,.34);} 1352} 1353 1354/* r11: edge fades REMOVED (A4) -- their color no longer matched the surrounding gradient; cards clip clean at the section edge. */ 1355 1356@media(max-width:767px){ 1357 .swheel_card{width:74vw; margin:1.375rem .4375rem;} 1358} 1359/* r10: MOBILE -- info + "Learn more" CTA always shown; single tap follows the link; shade pushed lower. 1360 r23 (owner 2026-07-06): (pointer:coarse) leg ADDED -- S Pen phones (Galaxy Ultra line) can 1361 report hover:hover, which skips the hover:none leg; coarse-pointer catches them at any width. */ 1362@media(hover:none),(pointer:coarse),(max-width:767px){ 1363 .swheel_sum{max-height:5.5rem; opacity:1; margin-bottom:.75rem;} 1364 .swheel_name{line-height:1.2;} 1365 .swheel_name::after{transform:scaleX(1);} 1366 .swheel_go{opacity:1; transform:none;} 1367 .swheel_shade{background:linear-gradient(to top,rgba(7,12,26,.94) 0%,rgba(7,12,26,.7) 32%,rgba(7,12,26,.15) 56%,transparent 72%);} 1368} 1369@media(prefers-reduced-motion:reduce){ 1370 .swheel_img,.swheel_ov,.swheel_sum,.swheel_name::after,.swheel_go{transition:none;} 1371} 1372</style> 1373<section class="swheel" aria-label="Be The Change International -- programs around the world"> 1374 <p class="swheel_kicker">Around the world</p> 1375 <div class="swheel_viewport" id="swheelVp" aria-label="International programs (trackpad-swipe or drag)"><div class="swheel_track" id="swheelTrack"></div></div> 1376</section> 1377 1378<!-- ============================================================ 1379 SECTION 8 -- GIVE / HORIZON (id="give" -> nav Give) 1380 ============================================================ --> 1381<style> 1382/* ============================================================ 1383 GIVE -- HORIZON (section-only, converter-clean) 1384 Foundation: fm-system.css supplies tokens, fonts, .section, 1385 and the colorbar COLOR classes (.colorbar_i / .colorbar_a..g). 1386 Section-specific (kept local): the dark->white "Horizon" melt + 1387 CSS fallback, the Three.js gradient canvas mount, the glowing 1388 colorbar geometry, and the give CTA buttons (orange w/ dark 1389 text -- deliberately NOT the shared .btn-donate, so kept local). 1390 Class-only . single-underscore BEM . REM . WF breakpoints. 1391 ============================================================ */ 1392/* r6: TALLER give (130vh) so the rainbow-colored middle reads tall before the white melt. */ 1393.give{position:relative;width:100%;overflow:hidden;isolation:isolate; 1394 min-height:130vh;min-height:130svh; 1395 display:flex;align-items:center;justify-content:center;text-align:center; 1396 padding:20vh var(--pad);color:#fff;font-family:var(--body);line-height:1.5; 1397 /* r7 CSS fallback if WebGL fails -- CTA opens from DARK (#14264a, seams with the intl 1398 carousel above), tall colored middle, then a LONG white fade over the bottom third. */ 1399 background:radial-gradient(60% 70% at 30% 40%,rgba(41,80,153,.5),transparent 60%), 1400 radial-gradient(60% 70% at 75% 62%,rgba(0,162,135,.45),transparent 60%), 1401 linear-gradient(180deg,#0c1426 0%,#0c1426 12%,#1d3a70 42%,#14264a 64%,#ffffff 100%)} 1402/* r10 top-edge fade: smooth melt from the SUPER-DARK blue (#0c1426) of the carousel bottom into give */ 1403.give::before{content:"";position:absolute;
1403left:0;right:0;top:0;height:26vh;z-index:1;pointer-events:none; 1404 background:linear-gradient(180deg,#0c1426 0%,rgba(12,20,38,.55) 42%,rgba(12,20,38,0) 100%)} 1405/* canvas embed block -- canvas +
1405<script> stay together for the Webflow port */ 1406.give_canvas{position:absolute;inset:0;z-index:0;pointer-events:none} 1407.give_gl{position:absolute;inset:0;width:100%;height:100%;display:block} 1408 1409.give_inner{position:relative;z-index:2;max-width:68.75rem;margin:0 auto;transform:translateY(-6.25rem)} 1410/* colorbar geometry local (glow + inline flow); the 7 color fills come from fm-system */ 1411/* r7 REFERENCE RHYTHM -- kicker->headline 1.5rem, headline->subhead 1.25rem, subhead->buttons 2.25rem */ 1412.give_bar{display:inline-flex;height:.375rem;width:8.25rem;border-radius:999px;overflow:hidden;vertical-align:middle; 1413 margin:0 auto 1.5rem;box-shadow:0 6px 22px -8px rgba(0,0,0,.5),inset 0 0 0 1px rgba(255,255,255,.35)} 1414.give_title{font-family:var(--display);font-weight:400;font-size:clamp(2.5rem,5vw,4.75rem);line-height:1.03;letter-spacing:.006em;margin:0 auto 1.25rem;max-width:15ch} 1415/* r7 UNIFIED SUBHEAD (matches hero / programs / BTC) */ 1416.give_text{font-family:var(--body);font-weight:400;max-width:54ch;margin:0 auto 2.25rem;font-size:clamp(1.0625rem,1.4vw,1.3125rem);line-height:1.65;color:var(--on-dark-2)} 1417.give_cta{display:flex;gap:.875rem;justify-content:center;flex-wrap:wrap} 1418 1419/* give CTA buttons -- section-local (orange w/ dark text; hover lift + arrow nudge via ::after) */ 1420.give_btn{display:inline-flex;align-items:center;gap:.625rem;padding:1rem 1.875rem;border-radius:999px;font-weight:600; 1421 font-size:.9375rem;letter-spacing:.01em;text-decoration:none;color:inherit; 1422 transition:transform .25s,box-shadow .25s,background .25s,color .25s;cursor:pointer} 1423.give_btn-primary{background:var(--orange);color:#241502;box-shadow:0 12px 34px -12px rgba(235,144,62,.9)} 1424.give_btn-primary::after{content:"â";transition:transform .25s} 1425.give_btn-primary:hover{transform:translateY(-2px)} 1426.give_btn-primary:hover::after{transform:translateX(4px)} 1427.give_btn-secondary{background:transparent;border:1.5px solid currentColor;color:inherit} 1428.give_btn-secondary:hover{transform:translateY(-2px)} 1429 1430@media(max-width:767px){ .give{min-height:104vh;min-height:104svh;padding:14vh var(--pad)} .give_inner{transform:translateY(-2.25rem)} } 1431@media(prefers-reduced-motion:reduce){ .give_gl{display:none} } 1432</style> 1433<!-- FM Give CTA -- Horizon treatment (Three.js gradient canvas, mouse-reactive white bloom) --> 1434<section class="section give" id="give"> 1435 <div class="give_canvas" aria-hidden="true"><canvas id="giveGradient" class="give_gl"></canvas></div> 1436 <div class="give_inner"> 1437 <span class="give_bar" aria-hidden="true"><i class="colorbar_i colorbar_a"></i><i class="colorbar_i colorbar_b"></i><i class="colorbar_i colorbar_c"></i><i class="colorbar_i colorbar_d"></i><i class="colorbar_i colorbar_e"></i><i class="colorbar_i colorbar_f"></i><i class="colorbar_i colorbar_g"></i></span> 1438 <h2 class="give_title">One yes can change a life.</h2> 1439 <p class="give_text">Your gift pays a teenager for a summer of real work, stocks a greenhouse, or puts an adult through job training. We will show you what it built.</p> 1440 <div class="give_cta"> 1441 <a class="give_btn give_btn-primary" href="/give">Give Today</a> 1442 <a class="give_btn give_btn-secondary" href="/get-involved">Become a Partner</a> 1443 </div> 1444 </div> 1445</section> 1446 1447<!-- ============================================================ 1448 SECTION 7 -- NEWS / BLOG (id="news" -> nav News) 1449 ============================================================ --> 1450<style> 1451/* ============================================================ 1452 NEWS / BLOG (section-only, converter-clean) 1453 Foundation: fm-system.css supplies tokens, fonts, .section. 1454 Section-specific (kept local): the 3-up CMS card grid + card 1455 hover polish. Near-black text (#0c1426) and the card hairline 1456 (#dbe2ea) are kept as literals -- fm-system's --ink/--line are 1457 different hexes, so a var swap would shift the color. 1458 Class-only . single-underscore BEM . REM . WF breakpoints. 1459 Card-hover image zoom + read-more gap toggle .is-hover via JS, 1460 so no descendant selectors are needed. 1461 ============================================================ */ 1462.news{background:var(--paper);color:#0c1426;font-family:var(--body);line-height:1.5;padding:clamp(0.5rem,1.5vh,1.75rem) 0 14vh} 1463.news_wrap{max-width:92.5rem;margin-inline:auto;padding-inline:var(--pad)} 1464.news_head{display:flex;justify-content:space-between;align-items:flex-end;
1464gap:1.25rem;flex-wrap:wrap;margin-bottom:2.875rem} 1465.news_title{font-family:var(--display);font-weight:400;font-size:clamp(1.875rem,3.6vw,3.25rem);line-height:1.13;letter-spacing:.006em;color:#0c1426;margin:0} 1466.news_more{display:inline-flex;align-items:center;gap:.625rem;font-size:.875rem;font-weight:600;color:var(--blue);text-decoration:none;transition:gap .25s} 1467.news_more:hover{gap:.875rem} 1468 1469.news_grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1.875rem} 1470 1471/* CARDS -- rendered LIVE from Sanity by fm-cms.js (initRelatedStrips, prefix "pblog"). 1472 Same proven card system news.html and every program-page "From the news" strip use, 1473 so this grid stays visually identical while pulling real posts: real featured photos 1474 and /news/<slug>/ links, never hand-picked stock imagery. */ 1475.pblog_card{display:flex;flex-direction:column;background:var(--paper);border:1px solid #dbe2ea;border-radius:1.125rem;overflow:hidden; 1476 text-decoration:none;color:#0c1426;transition:transform .3s,box-shadow .3s;will-change:transform} 1477.pblog_card:hover{transform:translateY(-4px);box-shadow:0 30px 60px -34px rgba(20,38,74,.45)} 1478.pblog_media{position:relative;overflow:hidden;background:#dde5ee;aspect-ratio:16/10} 1479.pblog_img{display:block;width:100%;height:100%;object-fit:cover;will-change:transform;transition:transform .7s cubic-bezier(.16,1,.3,1)} 1480.pblog_img.is-hover{transform:scale(1.05)} 1481.pblog_body{display:flex;flex-direction:column;flex:1;padding:1.5rem 1.625rem 1.75rem} 1482.pblog_date{font-family:var(--body);font-weight:500;font-size:.6875rem;letter-spacing:.08em;text-transform:uppercase;color:var(--mist)} 1483.pblog_heading{font-family:var(--display);font-weight:400;font-size:1.3125rem;line-height:1.22;letter-spacing:.006em;color:#0c1426;margin:.75rem 0 0} 1484.pblog_excerpt{font-family:var(--body);font-weight:400;font-size:.9375rem;line-height:1.55;color:var(--text-muted);margin:.75rem 0 0; 1485 display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden} 1486.pblog_read{display:inline-flex;align-items:center;gap:.5rem;margin-top:auto;padding-top:1.25rem;font-size:.8125rem;font-weight:600;color:var(--blue);transition:gap .25s} 1487.pblog_read.is-hover{gap:.75rem} 1488 1489/* image-less posts (12 of 42) -> deliberate TEXT card, brand colorbar on the top edge */ 1490.pblog_card.is-noimg{background:linear-gradient(180deg,#f7fafd 0%,var(--paper) 55%)} 1491.pblog_card.is-noimg .pblog_body{padding:2.25rem 1.625rem;justify-content:center} 1492.pblog_card.is-noimg .pblog_bar{margin:0 0 1.25rem} 1493.pblog_card.is-noimg .pblog_heading{font-size:1.5rem;line-height:1.2;margin-top:.625rem} 1494.pblog_card.is-noimg .pblog_excerpt{-webkit-line-clamp:6;font-size:.96875rem;margin-top:1rem} 1495.pblog_card.is-noimg .pblog_read{margin-top:1.5rem;padding-top:0} 1496 1497/* skeleton while Sanity answers; fm-cms.js removes the whole section if Sanity is unreachable */ 1498.pblog_skel{border:1px solid #dbe2ea;border-radius:1.125rem;overflow:hidden;background:var(--paper)} 1499.pblog_skel-media{aspect-ratio:16/10;background:var(--cloud);animation:pblogPulse 1.4s ease-in-out infinite} 1500.pblog_skel-body{padding:1.5rem 1.625rem 1.75rem;display:flex;flex-direction:column;gap:.75rem} 1501.pblog_skel-line{height:.875rem;border-radius:.3125rem;background:var(--cloud);animation:pblogPulse 1.4s ease-in-out infinite} 1502.pblog_skel-line.is-sm{width:34%;height:.625rem} 1503.pblog_skel-line.is-lg{height:1.375rem;width:88%} 1504.pblog_skel-line.is-short{width:64%} 1505@keyframes pblogPulse{0%,100%{opacity:.55}50%{opacity:1}} 1506@media(prefers-reduced-motion:reduce){.pblog_skel-media,.pblog_skel-line{animation:none}} 1507 1508@media(max-width:991px){ .news_grid{grid-template-columns:repeat(2,1fr)} } 1509@media(max-width:767px){ .news_grid{grid-template-columns:1fr} } 1510</style> 1511<section class="section news" id="news"> 1512 <div class="news_wrap"> 1513 <div class="news_head"> 1514 <h2 class="news_title reveal">Latest from FreshMinistries</h2> 1515 <a class="news_more" href="/news">All News <span aria-hidden="true">→</span></a> 1516 </div> 1517 <!-- ============================================================ 1518 LIVE FROM SANITY -- fm-cms.js (initRelatedStrips) fills this grid. 1519 The six hardcoded cards that used to live here linked to news.html 1520 (not the individual story) and reused program/mission stock photos 1521 -- that was the "wrong photos / wrong links" Pete reported. Cards now 1522 render from the newsPost collection: each post's real featuredImage 1523 and its /news/<slug>/ link. data-cms-limit = how many to show. 1524 Shares the cached 'page:0' request with the nav News panel + news.html, 1525 so it costs zero extra Sanity requests. If Sanity is unreachable, 1526 fm-cms.js removes this whole section rather than leave an empty grid. 1527 ============================================================ --> 1528 <div class="news_grid" data-cms="pblog-grid" data-cms-limit="6" aria-busy="true"> 1529 <div class="pblog_skel" aria-hidden="true"><div class="pblog_skel-media"></div><div class="pblog_skel-body"><div class="pblog_skel-line is-sm"></div><div class="pblog_skel-line is-lg"></div><div class="pblog_skel-line"></div><div class="pblog_skel-line is-short"></div></div></div> 1530 <div class="pblog_skel" aria-hidden="true"><div class="pblog_skel-media"></div><div class="pblog_skel-body"><div class="pblog_skel-line is-sm"></div><div class="pblog_skel-line is-lg"></div><div class="pblog_skel-line"></div><div class="pblog_skel-line is-short"></div></div></div> 1531 <div class="pblog_skel" aria-hidden="true"><div class="pblog_skel-media"></div><div class="pblog_skel-body"><div class="pblog_skel-line is-sm"></div><div class="pblog_skel-line is-lg"></div><div class="pblog_skel-line"></div><div class="pblog_skel-line is-short"></div></div></div> 1532 <div class="pblog_skel" aria-hidden="true"><div class="pblog_skel-media"></div><div class="pblog_skel-body"><div class="pblog_skel-line is-sm"></div><div class="pblog_skel-line is-lg"></div><div class="pblog_skel-line"></div><div class="pblog_skel-line is-short"></div></div></div> 1533 <div class="pblog_skel" aria-hidden="true"><div class="pblog_skel-media"></div><div class="pblog_skel-body"><div class="pblog_skel-line is-sm"></div><div class="pblog_skel-line is-lg"></div><div class="pblog_skel-line"></div><div class="pblog_skel-line is-short"></div></div></div> 1534 <div class="pblog_skel" aria-hidden="true"><div class="pblog_skel-media"></div><div class="pblog_skel-body"><div class="pblog_skel-line is-sm"></div><div class="pblog_skel-line is-lg"></div><div class="pblog_skel-line"></div><div class="pblog_skel-line is-short"></div></div></div> 1535 </div> 1536 </div> 1537</section> 1538 1539<!-- ============================================================ 1540 SECTION 9 -- FOOTER (v2) (id="footer") 1541 ============================================================ --> 1542<style> 1543/* ============================================================ 1544 FOOTER (v2) (section-only, converter-clean) 1545 Foundation: fm-system.css supplies tokens, fonts, .section, 1546 and .media (block img reset) for the logo. 1547 Section-specific (kept local): the deep brand gradient, the 1548 color/white logo lockup, and the muted on-dark link palette 1549 (#9fb2cd / #6f84a4 / #c2d2e6 -- not fm-system tokens). 1550 Class-only . single-underscore BEM . REM . WF breakpoints. 1551 ============================================================ */ 1552.footer{color:#9fb2cd;font-family:var(--body);line-height:1.5;padding:clamp(6rem,14vh,9.5rem) var(--pad) 6vh;position:relative; 1553 clip-path:ellipse(150% 100% at 50% 100%); 1554 background:radial-gradient(72% 90% at 12% 6%, rgba(41,80,153,.26), transparent 58%), 1555 radial-gradient(58% 80% at 92% 24%, rgba(0,162,135,.14), transparent 60%), 1556 linear-gradient(180deg,#16294f 0%,#0c1426 46%,#080d18 100%);overflow:hidden} 1557/* r12: footer top edge is now a true circular arc via clip-path ellipse (was a border-radius 1558 corner that tapered sharply). Centered bottom-center, 150% wide -> the sides are cut off well 1559 before the ellipse curves down, so the top reads as one gentle convex slice with NO edge taper. 1560 Top padding raised to clear the ~6%-of-height dip at the corners. Bottom stays full-width square. 1561 NOTE (Webflow port): this clip-path must carry as custom CSS. */ 1562.footer_top,.footer_bottom{position:relative;z-index:1} 1563.footer_top{display:flex;justify-content:space-between;align-items:flex-start;gap:2.5rem 3rem;flex-wrap:wrap;border-bottom:1px solid #ffffff14;padding-bottom:2.5rem}
1564/* r12: LEFT lead group is now the LOGO ONLY (subscribe/follow/social moved into the first 1565 right-hand column). Keeps the left anchor compact; the four columns balance on the right. */ 1566.footer_lead{display:flex;flex-direction:column;align-items:flex-start;gap:1.125rem;flex:0 0 auto} 1567/* 2026-08-18: the pillar line sits under the circle lockup. Nicole flagged the dead blue space 1568 beside the logo; the tagline fills it and promotes Learn/Earn/Eat/Heal out of the nav (Pete: both). */ 1569.footer_tagline{font-family:var(--display);font-weight:400;font-size:1.0625rem;letter-spacing:.055em;text-transform:uppercase;color:#9fb2cd;margin:0} 1570/* r11: Follow group -- headline (reuses footer_signup-h) tight to the social row directly beneath */ 1571.footer_follow{display:flex;flex-direction:column;align-items:flex-start;width:100%} 1572.footer_social{display:flex;align-items:center;gap:1rem} 1573.footer_social-link{display:inline-flex;color:#c2d2e6;transition:color .2s,transform .2s} 1574.footer_social-link:hover{color:#fff;transform:translateY(-2px)} 1575.footer_social-svg{display:block;width:1.25rem;height:1.25rem} 1576/* r11: .footer_right wrapper removed -- footer_top now holds footer_lead + footer_cols directly */ 1577.footer_logo{width:clamp(9rem,13vw,10.5rem);height:auto;opacity:.98} 1578/* r14: the four columns sit as a TIGHT group on the right (flex-end, no grow), so the logo 1579 anchors far-left with a real gap between it and the group -- not stretched across the width. */ 1580.footer_cols{display:flex;gap:clamp(1.75rem,3vw,3.25rem);flex-wrap:wrap;flex:0 1 auto;justify-content:flex-end} 1581/* r12: first right-hand column -- "Stay in the loop" signup stacked over the Follow + social row */ 1582.footer_col-stay{display:flex;flex-direction:column;gap:2rem;flex:1 1 18rem;max-width:23rem} 1583/* r11: GuideStar / Candid seal under the Give link -- small, unlinked (profile URL TBD) */ 1584.footer_seal{width:5rem;height:auto;display:block;margin-top:1.25rem} 1585.footer_head{font-family:'Space Mono',monospace;font-size:.6875rem;letter-spacing:.16em;text-transform:uppercase;color:#6f84a4;margin:0 0 1rem} 1586.footer_link{display:block;font-size:.875rem;color:#c2d2e6;padding:.3125rem 0;text-decoration:none;transition:color .2s} 1587.footer_link:hover{color:#fff} 1588.footer_bottom{display:flex;flex-direction:column;align-items:center;text-align:center;gap:.625rem;margin-top:2.125rem;font-size:.8125rem;color:#6f84a4} 1589.footer_btmline{display:flex;align-items:center;justify-content:center;gap:.75rem;flex-wrap:wrap} 1590.footer_credit{color:#5f7391} 1591.footer_legal{display:flex;align-items:center;flex-wrap:wrap;column-gap:1.5rem;row-gap:.5rem} 1592.footer_legal-link{color:#6f84a4;text-decoration:none;transition:color .2s} 1593.footer_legal-link:hover{color:#c2d2e6} 1594.footer_legal-link:focus-visible{outline:2px solid rgba(255,255,255,.6);outline-offset:2px;border-radius:.25rem} 1595/* r7: "Stay subscribed" newsletter signup -- fills the open gap under the logo lockup. 1596 action="#" placeholder -- FLAG: provider (Bloomerang / Mailchimp / etc.) wiring is TBD. */ 1597.footer_signup{width:100%;max-width:23rem;margin:0} 1598.footer_signup-h{font-family:var(--display);font-weight:400;font-size:1.375rem;line-height:1.15;color:#fff;margin:0 0 .5rem} 1599.footer_signup-p{font-size:.875rem;line-height:1.55;color:#9fb2cd;margin:0 0 1.125rem} 1600.footer_form{display:flex;gap:.5rem;flex-wrap:wrap} 1601.footer_field{flex:1 1 12rem;min-width:0} 1602.footer_field-half{flex:1 1 8rem} 1603.footer_input{width:100%;font-family:var(--body);font-size:.875rem;color:#fff;background:rgba(255,255,255,.06); 1604 border:1px solid rgba(255,255,255,.18);border-radius:999px;padding:.75rem 1.125rem;transition:border-color .2s,background .2s} 1605.footer_input::placeholder{color:#6f84a4} 1606.footer_input:focus{outline:none;border-color:var(--sky);background:rgba(255,255,255,.1)} 1607.footer_submit{flex:0 0 auto;font-family:var(--body);font-weight:600;font-size:.8125rem;letter-spacing:.04em;text-transform:uppercase; 1608 color:#241502;background:var(--orange);border:none;border-radius:999px;padding:.75rem 1.5rem;cursor:pointer; 1609 transition:background .25s,color .25s,transform .25s} 1610.footer_submit:hover{background:var(--red);color:#fff;transform:translateY(-1px)} 1611.footer_sronly{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0} 1612@media(max-width:767px){ .footer{clip-path:ellipse(380% 100% at 50% 100%);padding-top:clamp(3rem,8vh,4.5rem)} .footer_top{flex-direction:column;gap:2.75rem} .footer_lead{flex:1 1 auto} .footer_cols{flex-direction:column;gap:2.75rem;justify-content:flex-start} .footer_col-stay{max-width:none;flex:1 1 auto} .footer_signup{max-width:none;width:100%} } 1613</style> 1614<footer class="section footer" id="footer"> 1615 <div class="footer_top">
1616 <!-- r12: FAR-LEFT column -- FM logo ONLY (subscribe/follow moved into the first right column) --> 1617 <div class="footer_lead"> 1618 <div class="footer_brand"> 1619 <img class="media footer_logo" src="/img/opt/logo-footer-clrwht.png" loading="lazy" decoding="async" alt="FreshMinistries -- Be The Change International"> 1620 </div> 1621 <p class="footer_tagline">Learn. Earn. Eat. Heal.</p> 1622 </div> 1623 <!-- r12: four right-hand columns -- Stay+Follow | Local Programs | Global Programs | FreshMinistries --> 1624 <div class="footer_cols"> 1625 <!-- COLUMN 1: Stay in the loop + Follow/social --> 1626 <div class="footer_col footer_col-stay"> 1627 <!-- newsletter signup. FLAG: action="#" placeholder -- provider (Bloomerang / Mailchimp / etc.) TBD. --> 1628 <form class="footer_signup" novalidate> 1629 <h3 class="footer_signup-h">See the impact. Follow the work.</h3> 1630 <p class="footer_signup-p">Sign up for stories, updates, and ways to get involved, from Jacksonville to communities around the world.</p> 1631 <label class="footer_sronly" for="footerFirst">First name</label> 1632 <label class="footer_sronly" for="footerLast">Last name</label> 1633 <label class="footer_sronly" for="footerEmail">Email address</label> 1634 <div class="footer_form"> 1635 <span class="footer_field footer_field-half"><input class="footer_input" id="footerFirst" type="text" name="first_name" placeholder="First name" autocomplete="given-name" required></span> 1636 <span class="footer_field footer_field-half"><input class="footer_input" id="footerLast" type="text" name="last_name" placeholder="Last name" autocomplete="family-name" required></span> 1637 <span class="footer_field"><input class="footer_input" id="footerEmail" type="email" name="email" placeholder="[email protected]" autocomplete="email" required></span> 1638 <button class="footer_submit" type="submit">Subscribe</button> 1639 </div> 1640 </form> 1641 <!-- Follow -- social icons directly beneath. FLAG: href="#" placeholders -- real FM Facebook / Instagram / YouTube / LinkedIn URLs needed. --> 1642 <div class="footer_follow"> 1643 <h3 class="footer_signup-h">Follow</h3> 1644 <div class="footer_social"> 1645 <a class="footer_social-link" href="https://www.facebook.com/FreshMinistries/" target="_blank" rel="noopener" aria-label="Facebook"><svg class="footer_social-svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13.5 9V7.4c0-.6.4-.8.7-.8H16V4h-2.3C11 4 10.5 6 10.5 7.3V9H8.5v3h2v8h3v-8h2.2l.3-3z"/></svg></a> 1646 <a class="footer_social-link" href="https://www.instagram.com/freshministries/" target="_blank" rel="noopener" aria-label="Instagram"><svg class="footer_social-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3.5" y="3.5" width="17" height="17" rx="5"/><circle cx="12" cy="12" r="3.8"/><circle cx="17.2" cy="6.8" r="1.1" fill="currentColor" stroke="none"/></svg></a> 1647 <a class="footer_social-link" href="https://www.youtube.com/channel/UCqlwglTByp-dF7UKQKvtSyw" target="_blank" rel="noopener" aria-label="YouTube"><svg class="footer_social-svg" viewBox="0 0 24 24" fill="currentColor"><path d="M22 8.2a3 3 0 0 0-2.1-2.1C18 5.6 12 5.6 12 5.6s-6 0-7.9.5A3 3 0 0 0 2 8.2C1.5 10 1.5 12 1.5 12s0 2 .5 3.8A3 3 0 0 0 4.1 18c1.9.5 7.9.5 7.9.5s6 0 7.9-.5a3 3 0 0 0 2.1-2.1c.5-1.8.5-3.8.5-3.8s0-2-.5-3.8zM10 15.3V8.7l5.7 3.3z"/></svg></a> 1648 <a class="footer_social-link" href="https://www.linkedin.com/company/fresh-ministries-inc/" target="_blank" rel="noopener" aria-label="LinkedIn"><svg class="footer_social-svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6.9 8.8v11.2H3.6V8.8zM5.25 3.5a1.9 1.9 0 1 1 0 3.8 1.9 1.9 0 0 1 0-3.8zM20.4 20H17v-5.9c0-1.4-.5-2.4-1.8-2.4-1 0-1.5.7-1.7 1.3-.1.2-.1.5-.1.9V20h-3.3V9.4h3.2v1.4c.44-.7 1.2-1.6 2.9-1.6 2.1 0 3.7 1.4 3.7 4.4z"/></svg></a> 1649 </div> 1650 </div> 1651 </div> 1652 <!-- COLUMN 2: Local Programs (order matches the accordion in #2) --> 1653 <div class="footer_col"> 1654 <h4 class="footer_head">Local Programs</h4> 1655 <a class="footer_link" href="/freshfutures">FreshFutures</a> 1656 <a class="footer_link" href="/freshpath">FreshPath</a> 1657 <a class="footer_link" href="/freshfoundations">FreshFoundations</a> 1658 <a class="footer_link" href="/freshfamilies">FreshFamilies</a> 1659 <a class="footer_link" href="/lifepoint-career-institute">
1659LifePoint Career Institute</a> 1660 <a class="footer_link" href="/beaver-street-enterprise-center">Beaver Street Enterprise Center</a> 1661 <a class="footer_link" href="/outpatient-counseling">Outpatient Counseling</a> 1662 <a class="footer_link" href="/nativefresh">NativeFresh Aquaponics</a> 1663 </div> 1664 <!-- COLUMN 3: Global Programs --> 1665 <div class="footer_col"> 1666 <h4 class="footer_head">Global Programs</h4> 1667 <a class="footer_link" href="/global">Be The Change International</a> 1668 <a class="footer_link" href="/desmond-tutu">Desmond Tutu Programme</a> 1669 <a class="footer_link" href="/aquaponics">Global Aquaponics</a> 1670 <a class="footer_link" href="/worldwide-pilgrimage-ministries">Worldwide Pilgrimage Ministries</a> 1671 </div> 1672 <!-- COLUMN 4: FreshMinistries + Gold seal under Give --> 1673 <div class="footer_col"> 1674 <h4 class="footer_head">FreshMinistries</h4> 1675 <a class="footer_link" href="/about">About</a> 1676 <a class="footer_link" href="/news">News</a> 1677 <a class="footer_link" href="/give">Give</a> 1678 <!-- FLAG: seal link -> Candid/GuideStar profile URL TBD (left unlinked) --> 1679 <img class="footer_seal" src="/img/opt/gold-seal.webp" loading="lazy" decoding="async" width="80" alt="Candid 2018 Gold Seal of Transparency"> 1680 </div> 1681 </div> 1682 </div> 1683 <div class="footer_bottom"> 1684 <span class="footer_btmline"> 1685 <span>FreshMinistries</span> · 1686 <span>EIN #59-2967898</span> · 1687 <span>Headquartered in Jacksonville, FL</span> · 1688 <a href="/privacy-policy" class="footer_legal-link">Privacy Policy</a> · 1689 <a href="/copyright-notice" class="footer_legal-link">Copyright</a> · 1690 <a href="/sources-of-support" class="footer_legal-link">Sources of Support</a> · 1691 <span>© Copyright 2026</span> 1692 </span> 1693 <span class="footer_credit">Site by <a class="footer_legal-link" href="https://perceptionmarketingbrand.com" target="_blank" rel="noopener">Perception Marketing</a></span> 1694 </div> 1695</footer> 1696 1697<!-- ============================================================ 1698 SHARED CDN -- loaded ONCE, before the per-section scripts. 1699 GSAP 3.13 + ScrollTrigger + SplitText: nav / hero / headline reveals / proof / news. 1700 Three.js: proof globe + give-horizon gradient. 1701 (map-stipple + programs + footer use no CDN.) 1702 WEBFLOW PORT: needs BOTH the GSAP 3.13 core + ScrollTrigger AND the SplitText plugin 1703 tag (SplitText is free as of GSAP 3.13). Add all three in the site custom code. 1704 ============================================================ -->
1705<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/gsap.min.js"></script>
vendor: 2 bytes, line 1705
1705
1706<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/ScrollTrigger.min.js"></script>
vendor: 2 bytes, line 1706
1706
1707<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/SplitText.min.js"></script>
vendor: 2 bytes, line 1707
1707
1708<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script>
1708 1709 1710<!-- ===================== SHARED TEXT-REVEAL (mask + per-character cascade) ===================== --> 1711<style> 1712/* Shared text-reveal utilities -- used by the hero AND every key section headline. 1713 Reveals use GSAP SplitText (mask:'lines'), which wraps EACH VISUAL line in its own 1714 overflow-clip, so a wrapped heading never bleeds the next row during the cascade. 1715 .fm-splitline is SplitText's per-line element: the roomy line-height keeps Marcellus 1716 descenders (g/p/y) INSIDE each line's clip at rest (fixes the descender clipping) -- tune 1717 toward 1.2 if it reads too loose. .fm-char is each split character. Class-only . converter-clean. */ 1718.fm-splitline{line-height:1.2} 1719.prog_head .fm-splitline{line-height:1.3} /* r21: extra clip room for the 'g' descender in 'Local Programs' (still clipped at 1.2); scoped so the hero keeps its approved 1.2 */ 1720.fm-char{display:inline-block} 1721</style>
1722<script> 1723/* ONE shared helper, reused by every key section headline. 1724 .revealHeadline(heading, risers, opts): GSAP SplitText splits the heading into VISUAL lines 1725 (each an overflow-clip via mask:'lines') + chars; on a ScrollTrigger enter (once) the chars 1726 cascade UP out of their own line clip (yPercent:120 -> 0, dur 1.15, stagger .055, power4.out) 1727 in ONE continuous stagger across lines, then the "risers" underneath rise (opacity + y12, 1728 power3.out, ~.9s, gentle stagger). autoSplit re-splits on resize/font-load and re-runs onSplit. 1729 No-op (heading stays fully visible) if GSAP or SplitText is missing; the CALLER owns the 1730 reduced-motion decision (never calls this when reduced), so a heading can never stick hidden. */ 1731if(window.gsap && window.SplitText){ gsap.registerPlugin(window.SplitText); } 1732window.FMReveal = (function(){ 1733 function revealHeadline(heading, risers, opts){ 1734 if(!heading || !window.gsap || !window.SplitText) return; /* bail -> heading stays fully visible */ 1735 opts = opts || {}; 1736 var rs = []; 1737 (risers || []).forEach(function(sel){ 1738 if(typeof sel === 'string'){ rs = rs.concat(gsap.utils.toArray(sel)); } 1739 else if(sel){ rs.push(sel); } 1740 }); 1741 try{ 1742 window.SplitText.create(heading, { 1743 type:'lines,chars', mask:'lines', autoSplit:true, linesClass:'fm-splitline', charsClass:'fm-char', 1744 onSplit:function(self){ 1745 var lastStart = self.chars.length ? (self.chars.length - 1) * 0.055 : 0; 1746 var risersAt = (opts.risersAt != null) ? opts.risersAt : 0.15; /* r20: risers fade CONCURRENT with the cascade (simple slow fade) -- no more waiting for the headline to finish */ 1747 /* fromTo (explicit start+end) + invalidateOnRefresh:false so a ScrollTrigger.refresh 1748 can never re-read a mid-tween end and jump. once:true -> plays only when scrolled to. */ 1749 var tl = gsap.timeline({ scrollTrigger:{ trigger:(opts.trigger || heading), start:(opts.start || 'top 82%'), once:true, invalidateOnRefresh:false } }); 1750 tl.fromTo(self.chars, {yPercent:120}, {yPercent:0, duration:1.05, stagger:0.045, ease:'power4.out'}, 0); 1751 if(rs.length){ 1752 /* opacity+y together = one smooth fade+rise; clearProps:transform frees the CSS :hover lift. */ 1753 tl.fromTo(rs, {opacity:0, y:8}, {opacity:1, y:0, duration:1.4, ease:'power2.out', stagger:0.06, clearProps:'transform'}, risersAt); 1754 } 1755 return tl; /* returning the tween lets autoSplit revert + recreate cleanly */ 1756 } 1757 }); 1758 }catch(e){ /* SplitText failed -> heading + risers stay visible */ } 1759 } 1760 1761 return { revealHeadline: revealHeadline }; 1762})(); 1763</script>
1763 1764 1765<!-- ===================== SECTION SCRIPTS (in order, IIFEs) ===================== --> 1766 1767<!-- NAV -- scrolled state + logo swap + logo shrink + accessible wave overlay -->
1768<script> 1769(function(){ 1770 var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 1771 var hasGSAP = !!window.gsap; 1772 /* ---------- NAV scrolled state + logo swap + logo shrink ---------- */ 1773 var nav = document.getElementById('topnav'); 1774 if(!nav) return; 1775 var brandLogos = nav.querySelectorAll('.topnav_logo'); 1776 var logoWht = nav.querySelector('.topnav_logo-wht'); 1777 var logoInk = nav.querySelector('.topnav_logo-ink'); 1778 var lastY = window.scrollY; 1779 function onScroll(){ 1780 var y = window.scrollY; 1781 var sc = y > 60; 1782 nav.classList.toggle('is-scrolled', sc); /* transparent/dark -> solid white on scroll */ 1783 for(var i=0;i<brandLogos.length;i++){ brandLogos[i].classList.toggle('is-small', sc); } /* logo 50 -> 40 */ 1784 if(logoWht) logoWht.classList.toggle('is-off', sc); /* white logo hides on scroll */ 1785 if(logoInk) logoInk.classList.toggle('is-on', sc); /* dark logo shows on scroll */ 1786 if(Math.abs(y - lastY) > 6){ 1787 if(y > lastY && y > 150) nav.classList.add('is-tucked'); /* scrolling down past the hero -> tuck away */ 1788 else if(y < lastY) nav.classList.remove('is-tucked'); /* scrolling up -> reveal */ 1789 lastY = y; 1790 } 1791 if(y <= 60) nav.classList.remove('is-tucked'); /* always visible at the very top */ 1792 } 1793 window.addEventListener('scroll', onScroll, {passive:true}); onScroll(); 1794 /* ---------- OVERLAY MENU (accessible; focus + ESC + aria) ---------- */ 1795 })(); 1796</script>
1796 1797 1798<!-- HERO -- SplitText text-reveal (per-visual-line masked per-char cascade on BOTH lines, continuous rainbow on line 2, then subhead + buttons) -->
1799<script> 1800(function(){ 1801 var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 1802 var hasGSAP = !!window.gsap; 1803 1804 /* Per-VISUAL-line rainbow: stretch the gradient across each visual line's text so the per-char 1805 slices reconstitute ONE continuous sweep. Re-applied INSIDE onSplit so it survives every 1806 autoSplit re-split (resize / font-load); yPercent is vertical only, so left/right stay valid. */ 1807 function applyRainbow(self){ 1808 (self.lines || []).forEach(function(lineEl){ 1809 var cs = lineEl.querySelectorAll('.fm-char'); 1810 if(!cs.length) return; 1811 var first = cs[0].getBoundingClientRect(); 1812 var last = cs[cs.length - 1].getBoundingClientRect(); 1813 var w = Math.max(1, last.right - first.left); 1814 for(var i=0;i<cs.length;i++){ 1815 cs[i].classList.add('hero_rainbow'); 1816 var r = cs[i].getBoundingClientRect(); 1817 cs[i].style.backgroundSize = w + 'px 100%'; 1818 cs[i].style.backgroundPositionX = -(r.left - first.left) + 'px'; 1819 } 1820 }); 1821 } 1822 1823 /* FOUC guard partner: the head <style> hides the hero text from first paint (html.fm-anim). 1824 showHero() lifts that -- called on every bail path, right as each tween pins its start 1825 state, and by a 3.5s failsafe, so the text can never stick hidden. */ 1826 function showHero(){ 1827 ['.hero_hl-l1','.hero_hl-l2','.hero_sub','.hero_cta .hero_btn'].forEach(function(sel){ 1828 document.querySelectorAll(sel).forEach(function(el){ el.style.visibility = 'visible'; }); 1829 }); 1830 } 1831 function heroEntrance(){ 1832 if(!hasGSAP || reduce || !window.SplitText){ showHero(); if(window.FMHero) window.FMHero.fire(); return; } /* bail -> both lines fully visible, no motion */ 1833 var l1 = document.querySelector('.hero_hl-l1'); 1834 var l2 = document.querySelector('.hero_hl-l2'); 1835 if(!l1 || !l2){ showHero(); if(window.FMHero) window.FMHero.fire(); return; } 1836 1837 try{ 1838 /* LINE 1 -- per-visual-line masked cascade, plays on load (delay .25). */ 1839 window.SplitText.create(l1, { 1840 type:'lines,chars', mask:'lines', autoSplit:true, linesClass:'fm-splitline', charsClass:'fm-char', 1841 onSplit:function(self){ 1842 var t = gsap.fromTo(self.chars, {yPercent:120}, {yPercent:0, duration:1.05, stagger:0.045, ease:'power4.out', delay:0.25}); 1843 gsap.set(l1, {visibility:'visible'}); /* chars already sit displaced inside their clips -> flash-free unhide */ 1844 return t; 1845 } 1846 }); 1847 /* LINE 2 (rainbow) -- same masked cascade, +0.12s; rainbow re-applied on every split. */ 1848 window.SplitText.create(l2, { 1849 type:'lines,chars', mask:'lines', autoSplit:true, linesClass:'fm-splitline', charsClass:'fm-char', 1850 onSplit:function(self){ 1851 applyRainbow(self); 1852 var t = gsap.fromTo(self.chars, {yPercent:120}, {yPercent:0, duration:1.05, stagger:0.045, ease:'power4.out', delay:0.6}); 1853 gsap.set(l2, {visibility:'visible'}); 1854 return t; 1855 } 1856 }); 1857 }catch(e){ showHero(); if(window.FMHero) window.FMHero.fire(); return; } /* SplitText failed -> everything unhidden, no motion */ 1858 1859 /* SUBHEAD + BUTTONS -- CONCURRENT with the headline cascade (owner: don't make me wait for the 1860 text). A simple slow fade that overlaps the letters coming in. Plays ONCE. 1861 immediateRender pins opacity:0 NOW so the visibility unhide below is flash-free. */ 1862 gsap.timeline() 1863 .fromTo('.hero_sub', {opacity:0, y:10}, {opacity:1, y:0, duration:1.4, ease:'power2.out', immediateRender:true}, 0.5) 1864 .fromTo('.hero_cta .hero_btn', {opacity:0, y:12}, {opacity:1, y:0, duration:0.8, stagger:0.08, ease:'power2.out', clearProps:'transform', immediateRender:true}, 0.9); 1865 gsap.set(['.hero_sub', '.hero_cta .hero_btn'], {visibility:'visible'}); 1866 gsap.delayedCall(1.05, function(){ if(window.FMHero) window.FMHero.fire(); }); /* hero has led -> release the carousel */ 1867 } 1868 1869 if(document.readyState !== 'loading') heroEntrance(); 1870 else document.addEventListener('DOMContentLoaded', heroEntrance); 1871 setTimeout(showHero, 3500); /* failsafe: CDN hang or anything unexpected -> text appears un-animated */ 1872})(); 1873</script>
1873 1874 1875<!-- MOMENTUM-DRIFT CAROUSEL -- idle drift + drag + hover-stop + tap-open + infinite wrap + reduced-motion static -->
1876<script> 1877(function(){ 1878 'use strict'; 1879 var REDUCE = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 1880 var TOUCH = window.matchMedia && window.matchMedia('(hover:none)').matches; 1881 1882 var PROGRAMS = [ 1883 {name:'FreshFutures', c:'var(--purple)', href:'freshfutures.html', img:'img/opt/prog-freshfutures.jpg', sum:'A year of mentoring and money skills that ends in a paid summer job.'}, 1884 {name:'FreshPath', c:'var(--red)', href:'freshpath.html', img:'img/opt/prog-freshpath.jpg', sum:'Mentoring, tutoring, and a way back for court-involved and at-risk youth.'}, 1885 {name:'FreshFoundations', c:'var(--sky)', href:'freshfoundations.html', img:'img/opt/prog-freshfoundations.jpg', sum:'Adult certifications and coaching that lead to steady work.'}, 1886 {name:'LifePoint Career Institute', c:'var(--orange)', href:'lifepoint-career-institute.html', img:'img/opt/prog-lifepoint.jpg', sum:'Ten weeks of hands-on hotel training that leads straight to work.'}, 1887 {name:'FreshFamilies', c:'var(--sky)', href:'freshfamilies.html', img:'img/opt/prog-families.jpg', sum:'Workshops that help families pull in the same direction.'}, 1888 {name:'Outpatient Counseling', c:'var(--purple)', href:'outpatient-counseling.html', img:'img/opt/prog-outpatient.jpg', /* FLAG: stand-in image (HQ exterior), needs real Outpatient Counseling */ sum:'Substance use counseling with licensed therapists, in person or online.'}, 1889 {name:'Beaver Street Enterprise Center', c:'var(--blue)', href:'beaver-street-enterprise-center.html', img:'img/opt/prog-bsec.jpg', sum:'Fifty-two small businesses are growing inside this Eastside incubator.'}, 1890 {name:'NativeFresh Aquaponics', c:'var(--teal)', href:'nativefresh.html', img:'img/opt/prog-aquaponics.jpg', sum:'A USDA Harmonized GAP certified aquaponics farm, and where every greenhouse we build starts.'}, 1891 {name:'Desmond Tutu Program', c:'var(--orange)', href:'desmond-tutu.html', img:'img/opt/prog-desmond-tutu.jpg', sum:'Climate-smart greenhouses that let communities feed themselves, across seven African countries.'}, 1892 {name:'Soweto (Tutu Home)', c:'var(--red)', href:'soweto-tutu-home.html', img:'img/btc-soweto.jpg', sum:'The program has a home now: the Tutu family house in Soweto, restored with Rotary International, a greenhouse growing in the garden.'}, 1893 {name:'Island Fresh (St. Croix)', c:'var(--sky)', href:'island-fresh-st-croix.html', img:'img/opt/btc-island.jpg', /* FLAG: greenhouse stand-in (not St. Croix), needs real Island Fresh St. Croix */ sum:'Six greenhouses in a territory that imports 97 percent of its food.'}, 1894 {name:'Randburg (Johannesburg)', c:'var(--yellow)', href:'randburg-training-facility-johannesburg-south-africa.html', img:'img/opt/btc-randburg.jpg', sum:'Where Southern Africa\'s trainers learn the system, then carry it home.'} 1895 ]; 1896 1897 var ARROW = '<span class="mdrift_go"><svg class="mdrift_go-svg" viewBox="0 0 28 13" aria-hidden="true"><path d="M0 6.5h25M20 2l5 4.5-5 4.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>'; 1898 1899 function cardHTML(p){ 1900 return '<a class="mdrift_card" href="'+p.href+'" style="--c:'+p.c+'" aria-label="'+p.name+'" draggable="false">' + 1901 '<img class="mdrift_img" src="'+p.img+'" alt="'+p.name+'" draggable="false" decoding="async">' + 1902 '<span class="mdrift_ov"></span><span class="mdrift_top"></span><span class="mdrift_shade"></span>' + 1903 '<span class="mdrift_content">' + 1904 '<span class="mdrift_name">'+p.name+'</span>' + 1905 '<span class="mdrift_foot">' + 1906 '<span class="mdrift_sum">'+p.sum+'</span>' + 1907 ARROW + 1908 '</span>' + 1909 '</span></a>'; 1910 } 1911 1912 function measure(track){ 1913 var one = 0, cards = track.children; 1914 for(var j=0;j<PROGRAMS.length && j<cards.length;j++){ 1915 var r = cards[j].getBoundingClientRect(), s = getComputedStyle(cards[j]); 1916 one += r.width + parseFloat(s.marginLeft) + parseFloat(s.marginRight); 1917 } 1918 return one; 1919 } 1920 function fill(track, copies){ 1921 var html = ''; 1922 for(var k=0;k<copies;k++){ for(var i=0;i<PROGRAMS.length;i++){ html += cardHTML(PROGRAMS[i]); } } 1923 track.innerHTML = html; 1924 return measure(track); 1925 } 1926 1927 var lerp = function(a,b,t){ return a + (b-a)*t; }; 1928 function debounce(fn, ms){ var t; return function(){ var a=arguments,c=this; clearTimeout(t); t=setTimeout(function(){ fn.apply(c,a); }, ms); }; } 1929 /* r15 (#6): true only for hrefs that actually go somewhere -- a real path/URL, or an in-page 1930 anchor whose target element exists. Bare '#' and unresolved anchors (e.g. #freshfutures with 1931 no matching id) return false so we can BLOCK their navigation (which jumps the page to top). */ 1932 function hrefResolves(h){ 1933 if(!h || h === '#') return false; 1934 if(h.charAt(0) === '#'){ var id=h.slice(1); return !!(id && document.getElementById(id)); } 1935 return true; 1936 } 1937 1938 function initCarousel(vpId, trackId){ 1939 var vp = document.getElementById(vpId), track = document.getElementById(trackId); 1940 if(!vp || !track) return; 1941 1942 /* REDUCED-MOTION: static single render, no rAF, no drag. */ 1943 if(REDUCE){ 1944 fill(track, 2); 1945 vp.classList.add('is-ready');
1946 track.style.transform = 'translate3d(0,0,0)'; 1947 track.addEventListener('click', function(e){ e.preventDefault(); }); 1948 return; 1949 } 1950 1951 /* perf: measure ONE set, then clone only as many copies as the viewport can EVER reveal 1952 (2 at 2560px, was a blanket 6). The card sequence repeats, so a shorter track paints an 1953 identical frame -- it just stops handing the compositor a 22,900px-wide layer. 1954 Guarantee: with cur kept in (-setW,0] the visible window spans at most [setW, setW+vpW], 1955 so COPIES = ceil(vpW/setW)+1 always has content under it. */ 1956 function copiesNeeded(){ return Math.max(2, Math.ceil(vp.clientWidth / setW) + 1); } 1957 var setW = fill(track, 1); 1958 if(setW <= 0) return; 1959 var COPIES = copiesNeeded(); 1960 setW = fill(track, COPIES); 1961 /* is-ready is deferred to revealAndObserve() below (gated on the hero) -- load sequence */ 1962 1963 var DRIFT = 0.4, DRAG_MULT = 1.4, FLING_DECAY = 0.92, FLING_MAX = 55; /* r18: >1:1 drag + flick momentum */ 1964 var st = { cur:0, tgt:0, dragging:false, hovering:false, lastX:0, moved:0, lastY:0, movedY:0, driftCur:DRIFT, downCard:null, vel:0, fling:0 }; 1965 st.cur = st.tgt = 0; /* the sequence repeats, so 0 paints the exact frame -2*setW used to */ 1966 var justDragged = false; 1967 var running = false, scrollIdleAt = 0; 1968 /* perf: ambient drift yields to page scrolling -- the band eases to a stop while the 1969 user scrolls and resumes ~200ms after the last scroll event (drag input unaffected) */ 1970 window.addEventListener('scroll', function(){ scrollIdleAt = performance.now() + 220; }, { passive:true }); 1971 1972 /* keep cur in (-setW, 0] so the visible window never runs off the shortened track. 1973 cur and tgt shift together, so the drag/fling gap between them is preserved. */ 1974 function wrapBoth(){ 1975 while(st.cur <= -setW){ st.cur += setW; st.tgt += setW; } 1976 while(st.cur > 0){ st.cur -= setW; st.tgt -= setW; } 1977 } 1978 function frame(){ 1979 if(!running) return; 1980 var want = (st.dragging || st.hovering || performance.now() < scrollIdleAt) ? 0 : DRIFT; /* hover or page-scroll eases the drift to a stop */ 1981 st.driftCur = lerp(st.driftCur, want, 0.08); 1982 st.tgt -= st.driftCur; 1983 /* flick momentum: a decaying impulse coasts the band after release; the lerp below settles it */ 1984 if(!st.dragging && Math.abs(st.fling) > 0.1){ st.tgt += st.fling; st.fling *= FLING_DECAY; } else { st.fling = 0; } 1985 st.cur = lerp(st.cur, st.tgt, 0.09); 1986 wrapBoth(); 1987 track.style.transform = 'translate3d('+(Math.round(st.cur*100)/100)+'px,0,0)'; 1988 requestAnimationFrame(frame); 1989 } 1990 function start(){ if(running) return; running = true; requestAnimationFrame(frame); } 1991 function stop(){ running = false; } 1992 function pulse(card){ card.classList.remove('is-tapped'); void card.offsetWidth; card.classList.add('is-tapped'); } 1993 1994 var TAP_SLOP = 9; 1995 vp.addEventListener('pointerdown', function(e){ 1996 if(e.button === 2) return; 1997 st.dragging = true; st.lastX = e.clientX; st.lastY = e.clientY; st.moved = 0; st.movedY = 0; st.vel = 0; st.fling = 0; 1998 st.downCard = e.target.closest ? e.target.closest('.mdrift_card') : null; 1999 vp.classList.add('is-grabbing'); 2000 try{ vp.setPointerCapture(e.pointerId); }catch(_){} 2001 start(); 2002 }); 2003 vp.addEventListener('pointermove', function(e){ 2004 if(!st.dragging) return; 2005 var raw = e.clientX - st.lastX; 2006 st.lastX = e.clientX; 2007 st.moved += Math.abs(raw); /* tap-vs-drag uses RAW travel (threshold unchanged) */ 2008 st.movedY += Math.abs(e.clientY - st.lastY); st.lastY = e.clientY; /* r26: vertical travel counts against the tap too */ 2009 if(st.moved > TAP_SLOP){ var dx = raw * DRAG_MULT; st.tgt += dx; st.vel = st.vel * 0.6 + dx * 0.4; } /* band leads the thumb; smoothed velocity feeds the flick */ 2010 }); 2011 /* r26 (hit list 7/24 #3): tiles were firing navigation on MOBILE vertical page scroll. Two 2012 holes, both closed the same way as the swheel's r15 fix: (a) st.moved only counted 2013 HORIZONTAL travel, so a vertical swipe still read as a "tap" -- movedY now counts against 2014 TAP_SLOP too; (b) pointercancel (the browser claiming the pan-y scroll) ran the tap branch 2015 and navigated -- only a GENUINE pointerup may navigate now (canTap). Horizontal drag + 2016 fling math is untouched (fling still keys off X travel alone). */ 2017 function up(canTap){ 2018 if(!st.dragging) return; 2019 st.dragging = false; vp.classList.remove('is-grabbing'); 2020 var wasTap = st.moved <= TAP_SLOP && st.movedY <= TAP_SLOP; 2021 justDragged = !wasTap; 2022 if(st.moved > TAP_SLOP){ st.fling = Math.max(-FLING_MAX, Math.min(FLING_MAX, st.vel)); } /* release -> coast (real horizontal drags only) */ 2023 if(canTap && wasTap && st.downCard){ 2024 pulse(st.downCard); 2025 /* setPointerCapture on pointerdown suppresses the native <a> click, so a tap never 2026 actually navigated. Navigate explicitly here for real hrefs -- drag-clicks and 2027 placeholder '#' cards are still blocked by the capture-phase click handler below. */ 2028 var tapHref = st.downCard.getAttribute('href'); 2029 if(tapHref && hrefResolves(tapHref)){ window.location.href = tapHref; } 2030 } 2031 st.downCard = null; 2032 if(justDragged){ setTimeout(function(){ justDragged = false; }, 40); } 2033 } 2034 vp.addEventListener('pointerup', function(){ up(true); }); 2035 vp.addEventListener('pointercancel', function(){ up(false); }); 2036 vp.addEventListener('pointerleave', function(){ if(st.dragging) up(false); }); 2037 vp.addEventListener('mouseenter', function(){ st.hovering = true; }); 2038 vp.addEventListener('mouseleave', function(){ st.hovering = false; }); 2039 /* a drag (or the demo's dead anchors) must never fire the card link. 2040 r15 (#6): ALSO block navigation for placeholder card hrefs (# or an unresolved
2040in-page 2041 anchor) -- following them jumps the page to top on a tap or an end-of-scroll misread. 2042 Real hrefs (#global, future program pages) still navigate normally. */ 2043 vp.addEventListener('click', function(e){ 2044 if(justDragged){ e.preventDefault(); e.stopPropagation(); return; } 2045 var card = e.target.closest ? e.target.closest('.mdrift_card') : null; 2046 if(card && !hrefResolves(card.getAttribute('href'))){ e.preventDefault(); } 2047 }, true); 2048 2049 /* LOAD SEQUENCE: cards are built + measured above (no late shift), but the reveal (is-ready) 2050 and the drift are HELD until the hero has led, so the band never wins the first-paint race 2051 or plops in ahead of the hero. In-view gating then runs the loop only while on screen. 2052 r26 (hit list 7/24 #1): +1s AFTER the hero release -- the band was landing while the hero 2053 text/buttons were still animating in and competing with them. Only THIS carousel waits; 2054 the globe boot keeps its plain FMHero gate. */ 2055 function revealAndObserve(){ 2056 vp.classList.add('is-ready'); 2057 if('IntersectionObserver' in window){ 2058 new IntersectionObserver(function(es){ 2059 if(es[0].isIntersecting){ start(); } else { stop(); } 2060 },{threshold:0.02}).observe(vp); 2061 } else { start(); } 2062 } 2063 if(window.FMHero){ window.FMHero.onDone(function(){ setTimeout(revealAndObserve, 1000); }); } else { revealAndObserve(); } 2064 window.addEventListener('resize', debounce(function(){ 2065 var nw = measure(track); 2066 if(nw <= 0) return; 2067 setW = nw; 2068 /* cards clamp to a fixed 18.75rem past ~1364px, so a wider window needs MORE copies 2069 (2 at 2560, 3 at 3840). Grow -- never shrink -- so a resize can't expose a gap. */ 2070 var need = copiesNeeded(); 2071 if(need > COPIES){ COPIES = need; setW = fill(track, COPIES); } 2072 }, 200)); 2073 } 2074 2075 initCarousel('mdriftVp','mdriftTr'); 2076})(); 2077</script>
2077 2078 2079<!-- STAT NUMBERS -- static. The count-up was removed per owner (2026-07-03): too frenetic. 2080 The numbers rise in with their section reveals (see the SECTION HEADLINE REVEALS block). --> 2081 2082 2083<!-- ===================== WIREFRAME LAT/LONG GLOBE (Three.js LineSegments, perf-harnessed) ===================== -->
2084<script> 2085(function(){ 2086 'use strict'; 2087 if(!window.THREE) return; /* shared CDN missing -> bail quietly */ 2088 var canvas = document.querySelector('.dglobe_canvas'); 2089 var layer = document.querySelector('.dglobe_layer'); 2090 if(!canvas || !layer) return; 2091 2092 /* ---------- tunables (safe to retune; decorative) ---------- */ 2093 /* r6: faint WIREFRAME lat/long globe (was dotted THREE.Points). Same spanning 2094 architecture + natural slow spin -- only the RENDERING changed to LineSegments. */ 2095 var reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); 2096 var mobile = !!(window.matchMedia && (window.matchMedia('(max-width: 767px)').matches || window.matchMedia('(hover: none)').matches)); 2097 var LINE_COLOR= new THREE.Color(0xdfeafc); /* faint white-blue wireframe (spec) */ 2098 var LINE_ALPHA= 0.82; /* per-line shader alpha (layer opacity stays faint on top) */ 2099 var SPIN_SPEED= 0.055; /* rad/s -- slow, ~2min/rev (NATURAL, not scroll-driven) */ 2100 var AXIAL_TILT= 0.41; /* ~23.5deg Earth tilt (radians) */ 2101 var VIEW_TILT = -0.14; /* slight look-down so the north pole peeks */ 2102 2103 /* lat/long wireframe density -- reads clearly as a sphere without competing with content */ 2104 var PARALLELS = [-75,-60,-45,-30,-15,0,15,30,45,60,75]; /* latitude circles */ 2105 var MERIDIANS = 12; /* longitude arcs (every 30deg) */ 2106 var CSEG = 100; /* segments per parallel circle (smooth) */ 2107 var MSEG = 60; /* segments per meridian arc */ 2108 2109 /* globe placement across the tall spanning canvas (half-height = 1 units). 2110 center anchored UPPER-RIGHT, radius large -> only the lower-left arc shows, 2111 sweeping from the stats down into the programs band. */ 2112 /* r9 #5: MOBILE globe pushed further RIGHT + made BIGGER so it reads as a bold globe arcing 2113 through the stats (~3/4 the stats-section height), not a timid half-screen dome. Desktop 2114 unchanged. (diameter_px = canvasWidth * RADIUS_F, so RADIUS_F drives the on-screen size.) */ 2115 var ANCHOR_XF = mobile ? 0.5 : 0.76; /* x = ANCHOR_XF * halfWidth (mobile ~centered so the sphere BODY fills the frame) */ 2116 var ANCHOR_Y = mobile ? 0.34 : 0.55; /* y in half-height units (top = +1). r16: mobile RAISED 0.20 -> 0.34 so the sphere's BOTTOM lifts to the Local Programs headline (was bleeding down to the Beaver Street row) */ 2117 var RADIUS_F = mobile ? 2.6 : 0.92; /* radius = RADIUS_F * halfWidth. r19: mobile 2.85 -> 2.6 (owner eyeball). Desktop 0.92 UNCHANGED. */ 2118 2119 var d2r = Math.PI/180, TAU = Math.PI*2, PI = Math.PI; 2120 2121 /* ---------- build the lat/long wireframe as LineSegments (pairs of unit-sphere points) ---------- */ 2122 function buildWireframe(){ 2123 var pos=[], i, t0, t1; 2124 /* parallels: latitude circles (y = sin(lat), radius = cos(lat)) */ 2125 for(var pI=0;pI<PARALLELS.length;pI++){ 2126 var lat=PARALLELS[pI]*d2r, y=Math.sin(lat), r=Math.cos(lat); 2127 for(i=0;i<CSEG;i++){ 2128 t0=(i/CSEG)*TAU; t1=((i+1)/CSEG)*TAU; 2129 pos.push(r*Math.cos(t0), y, r*Math.sin(t0), r*Math.cos(t1), y, r*Math.sin(t1)); 2130 } 2131 } 2132 /* meridians: longitude arcs pole -> pole */ 2133 for(var mI=0;mI<MERIDIANS;mI++){ 2134 var lon=(mI/MERIDIANS)*TAU, cx=Math.cos(lon), cz=Math.sin(lon); 2135 for(i=0;i<MSEG;i++){ 2136 var a0=(i/MSEG)*PI, a1=((i+1)/MSEG)*PI, s0=Math.sin(a0), s1=Math.sin(a1); 2137 pos.push(s0*cx, Math.cos(a0), s0*cz, s1*cx, Math.cos(a1), s1*cz); 2138 } 2139 } 2140 return pos; 2141 } 2142 2143 /* ---------- three.js scene ---------- */ 2144 var renderer, scene, camera, anchor, spin, lines; 2145 var running=false, raf=0, built=false, last=0, inView=false; 2146 2147 /* line shaders: fade out the BACK hemisphere + soft rim at the silhouette, so the 2148 wireframe reads unmistakably as a sphere (normal of a unit sphere == its position). */ 2149 var VERT = [ 2150 'varying float vFacing;', 2151 'void main(){', 2152 ' vec3 nrm = normalize(normalMatrix * normalize(position));', 2153 ' vFacing = nrm.z;', /* >0 faces camera, <0 = back hemisphere */ 2154 ' gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);', 2155 '}' 2156 ].join('\n'); 2157 2158 var FRAG = [ 2159 'precision mediump float;', 2160 'uniform vec3 uColor;', 2161 'uniform float uAlpha;', 2162 'varying float vFacing;', 2163 'void main(){', 2164 ' float front = smoothstep(-0.28, 0.32, vFacing);', /* hide back hemisphere, soft rim fade */ 2165 ' float a = uAlpha * front;',
2166 ' if(a < 0.01) discard;', 2167 ' gl_FragColor = vec4(uColor, a);', 2168 '}' 2169 ].join('\n'); 2170 2171 function makeWireframe(){ 2172 var geo=new THREE.BufferGeometry(); 2173 geo.setAttribute('position', new THREE.Float32BufferAttribute(buildWireframe(),3)); 2174 var mat=new THREE.ShaderMaterial({ 2175 uniforms:{ uColor:{value:LINE_COLOR}, uAlpha:{value:LINE_ALPHA} }, 2176 vertexShader:VERT, fragmentShader:FRAG, 2177 transparent:true, depthTest:false, depthWrite:false, 2178 blending:THREE.NormalBlending 2179 }); 2180 return new THREE.LineSegments(geo, mat); 2181 } 2182 2183 function build(){ 2184 if(built) return; built=true; 2185 try{ 2186 renderer=new THREE.WebGLRenderer({canvas:canvas, alpha:true, antialias:true}); 2187 }catch(e){ built=false; return; } 2188 renderer.setClearColor(0x000000,0); 2189 /* DPR is set per-resize (pixel-budget cap) -- see resize() */ 2190 scene=new THREE.Scene(); 2191 camera=new THREE.OrthographicCamera(-1,1,1,-1,0.1,100); /* frustum set in resize() */ 2192 camera.position.set(0,0,10); 2193 2194 anchor=new THREE.Group(); /* holds placement (position + scale) across the panel */ 2195 var view=new THREE.Group(); view.rotation.x=VIEW_TILT; /* slight look-down */ 2196 var tilt=new THREE.Group(); tilt.rotation.z=AXIAL_TILT; /* Earth axial tilt */ 2197 spin=new THREE.Group(); /* the natural spin lives here */ 2198 tilt.add(spin); view.add(tilt); anchor.add(view); scene.add(anchor); 2199 2200 lines=makeWireframe(); 2201 spin.add(lines); 2202 resize(); 2203 render(); /* first paint */ 2204 observe(); 2205 } 2206 2207 function resize(){ 2208 if(!renderer) return; 2209 var w=Math.max(1, layer.clientWidth), h=Math.max(1, layer.clientHeight); 2210 /* r-hitlist 7/24: the layer spans carousel+stats+programs, so a raw DPR-2 backing store 2211 hits ~26MP+ at 4K with MSAA -- that per-paint GPU cost was half the jerk. Budget ~6MP: 2212 full sharpness on laptops, gracefully lower DPR on huge canvases. Lines are faint 2213 (layer opacity <= .5) so the cut is invisible; smooth 60fps paint is not. */ 2214 var pr=Math.min(window.devicePixelRatio||1, 2, Math.max(0.5, Math.sqrt(6000000/(w*h)))); 2215 renderer.setPixelRatio(pr); 2216 renderer.setSize(w,h,false); 2217 var aspect=w/h, halfW=aspect; /* half-height fixed at 1 */ 2218 camera.left=-halfW; camera.right=halfW; camera.top=1; camera.bottom=-1; 2219 camera.updateProjectionMatrix(); 2220 if(anchor){ 2221 anchor.position.set(halfW*ANCHOR_XF, ANCHOR_Y, 0); 2222 anchor.scale.setScalar(halfW*RADIUS_F); 2223 } 2224 } 2225 2226 function render(){ if(renderer) renderer.render(scene,camera); } 2227 2228 /* perf: .dglobe_layer spans the momentum carousel + stats + programs BY DESIGN, so the globe 2229 and the carousel drift loop are always in the same frame. r-hitlist 7/24: the old 2230 every-OTHER-frame paint (30fps) was itself visible as micro-stutter next to 60fps scroll 2231 and carousel motion (Pete: "a little jerky"). Paint now runs FULL RATE; the GPU cost that 2232 motivated 30fps is paid instead by the pixel-budget DPR cap in resize(), which cuts 2233 per-paint cost ~4x -- net cheaper than before AND smooth. */ 2234 function frame(ts){ 2235 if(!running) return; 2236 var dt = last ? (ts-last)/1000 : 0; last=ts; 2237 if(dt>0.1) dt=0.1; /* clamp after tab-restore */ 2238 if(spin) spin.rotation.y += SPIN_SPEED*dt; 2239 render(); /* full-rate paint, dt-accurate spin */ 2240 raf=requestAnimationFrame(frame); 2241 } 2242 function play(){ if(running||reduce) return; running=true; last=0; raf=requestAnimationFrame(frame); } 2243 function pause(){ running=false; if(raf){ cancelAnimationFrame(raf); raf=0; } } 2244 2245 function observe(){ 2246 if('IntersectionObserver' in window){ 2247 var io=new IntersectionObserver(function(es){ 2248 inView=es[0].isIntersecting; 2249 if(inView){ reduce ? render() : play(); } 2250 else pause(); 2251 },{threshold:0.01}); 2252 io.observe(layer); 2253 } else { reduce ? render() : play(); } 2254 } 2255 2256 /* boot: build lazily on first in-view (mirrors the proof-globe pattern). LOAD SEQUENCE:
2257 held until the hero has led (FMHero) so the globe FADES in after the hero (via .is-in on 2258 the layer) instead of popping in ahead of it. */ 2259 function bootGlobe(){ 2260 if('IntersectionObserver' in window){ 2261 var bootIo=new IntersectionObserver(function(es){ 2262 if(es[0].isIntersecting){ bootIo.disconnect(); build(); requestAnimationFrame(function(){ layer.classList.add('is-in'); }); } 2263 },{threshold:0.01, rootMargin:'200px'}); 2264 bootIo.observe(layer); 2265 } else { build(); layer.classList.add('is-in'); } 2266 } 2267 if(window.FMHero){ window.FMHero.onDone(bootGlobe); } else { bootGlobe(); } 2268 2269 /* pause when tab hidden; resize keeps the arc anchored across both sections */ 2270 document.addEventListener('visibilitychange', function(){ 2271 if(document.hidden) pause(); else if(inView && !reduce) play(); 2272 }); 2273 var rt=0; 2274 window.addEventListener('resize', function(){ 2275 if(!renderer) return; 2276 if(rt) cancelAnimationFrame(rt); 2277 rt=requestAnimationFrame(function(){ resize(); if(!running) render(); }); 2278 }); 2279 if('ResizeObserver' in window){ 2280 var ro=new ResizeObserver(function(){ if(!renderer) return; resize(); if(!running) render(); }); 2281 ro.observe(layer); 2282 } 2283})(); 2284</script>
2284 2285 2286<!-- PROGRAMS -- accordion (grid-template-rows) + masonry hover-dim (vanilla JS, IIFE) -->
2287<script> 2288(function(){ 2289 'use strict'; 2290 var list = document.getElementById('progList'); 2291 var masonry = document.querySelector('.prog_masonry'); 2292 if(!list) return; 2293 var rows = Array.prototype.slice.call(list.querySelectorAll('.prog_row')); 2294 /* .prog_ph[data-key], not img[data-key]: a tile with no usable photo is a <div> 2295 placeholder (see .prog_ph-empty). A tag-specific selector would silently drop it 2296 from the hover brighten/dim sync -- it only ever toggles classes, so the tag is 2297 irrelevant. Matches the click-nav selector further down. */ 2298 var mImgs = masonry ? Array.prototype.slice.call(masonry.querySelectorAll('.prog_ph[data-key]')) : []; 2299 if(!rows.length) return; 2300 /* cache each row's parts + mark its description live-collapsible. 2301 (converter-clean: state toggles on the child element, not via a parent descendant selector) */ 2302 var parts = rows.map(function(r){ 2303 var d = r.querySelector('.prog_desc'); if(d) d.classList.add('is-live'); 2304 return { row:r, name:r.querySelector('.prog_name'), go:r.querySelector('.prog_go'), desc:d, key:r.getAttribute('data-key') }; 2305 }); 2306 var mqMobile = window.matchMedia('(max-width:767px)'); 2307 /* hovering a program brightens its masonry photo and dims the rest */ 2308 function setImage(key){ 2309 mImgs.forEach(function(im){ 2310 var on = im.getAttribute('data-key')===key; 2311 im.classList.toggle('is-active', !!key && on); 2312 im.classList.toggle('is-dim', !!key && !on); 2313 }); 2314 } 2315 function setPart(p, on){ 2316 p.row.classList.toggle('is-open', on); 2317 if(p.name) p.name.classList.toggle('is-on', on); 2318 if(p.go) p.go.classList.toggle('is-on', on); 2319 if(p.desc) p.desc.classList.toggle('is-open', on); 2320 } 2321 function openRow(p){ parts.forEach(function(q){ setPart(q, q===p); }); if(p) setImage(p.key); } 2322 function reset(){ parts.forEach(function(q){ setPart(q, false); }); setImage(null); } // accordions closed; masonry all bright 2323 reset(); // nothing open by default; all photos bright 2324 parts.forEach(function(p){ 2325 p.row.addEventListener('mouseenter', function(){ if(!mqMobile.matches) openRow(p); }); 2326 p.row.addEventListener('focus', function(){ if(!mqMobile.matches) openRow(p); }); 2327 p.row.addEventListener('click', function(e){ 2328 if(mqMobile.matches && !p.row.classList.contains('is-open')){ 2329 e.preventDefault(); // first tap (mobile): open the accordion + swap the masonry highlight 2330 openRow(p); 2331 } 2332 // desktop, or a second tap on the active row: let the link navigate 2333 }); 2334 }); 2335 // leaving the list entirely (desktop) closes the accordion + returns to the default image state 2336 list.addEventListener('mouseleave', function(){ if(!mqMobile.matches) reset(); }); 2337 list.addEventListener('focusout', function(e){ if(!mqMobile.matches && !list.contains(e.relatedTarget)) reset(); }); 2338})(); 2339</script>
2339 2340<!-- ===================== STIPPLE MAP -- 2D canvas (mobile-STATIC) ===================== -->
2341<script> 2342(function(){ 2343 'use strict'; 2344 var canvas=document.getElementById('stippleCanvas'); 2345 var fallback=document.getElementById('stippleFallback'); 2346 if(!canvas) return; 2347 2348 var MAP_SRC='img/world-map-flat.jpg'; 2349 var ctx=canvas.getContext('2d'); 2350 if(!ctx){ showFallback(); return; } 2351 2352 /* ---- tunables ---- */ 2353 var reduce=!!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); 2354 /* touch / phone: crisp STATIC frame -- no rAF loop, no cursor FX, no scroll lag */ 2355 var mobile=!!(window.matchMedia && (window.matchMedia('(max-width: 767px)').matches || window.matchMedia('(hover: none)').matches)); 2356 var STATIC=reduce||mobile; 2357 var CAP=mobile?1500:6500; /* r13: dot density cut ~40% (mobile was way too dense; desktop a touch lighter too) -> fewer, more spread-out dots */ 2358 /* r12 #7: DESKTOP dots bolder so the continents read as crisply as mobile (mobile UNCHANGED). 2359 mobile keeps the fine 0.20-0.62px engraving; desktop gets larger 0.42-1.14px dots + fuller fill. */ 2360 /* r26 (hit list 7/24 #2): DESKTOP dots bigger again (radius 0.6-0.84 -> 0.9-1.3px) so the map 2361 reads more pronounced on desktop. Density (CAP + grid) deliberately untouched per the r13 2362 decision -- only the dot SIZE grows. Mobile UNCHANGED. */ 2363 var RMIN=mobile?0.5:0.9, RJIT=mobile?0.18:0.4; /* r19: mobile dots bumped up a touch (0.42->0.5) so the faint map reads a bit more prominent on phones */ 2364 var FILL_A=mobile?0.9:1.0; /* fine engraving fill (desktop pushed to full so dots read clearly) */ 2365 var LANDLUM=150; /* pixels darker than this = land */ 2366 var ZOOM=1.0; /* r9 #1: desktop FILL-WIDTH exactly -- drawW=W, drawH=W/1.6 = stage height (stage AR=1.6), offY=0 -> whole map, no top/bottom crop */ 2367 var COVER_OV=1.1; /* <=991 cover-overflow: ~10% bigger each side so the map reads full-bleed */ 2368 var ANCHOR_X=0.5, ANCHOR_Y=0.5; 2369 var CURSOR_R=112; /* px radius of local heat-shimmer */ 2370 var SHIM=3.4; /* shimmer displacement amplitude (px) */ 2371 var dpr=Math.min(window.devicePixelRatio||1, mobile?1.5:2); 2372 var TAU=Math.PI*2; 2373 2374 var off=document.createElement('canvas'), octx=off.getContext('2d'); 2375 var img=new Image(); 2376 var pts=null, mapAR=1.6, ready=false; 2377 var offX=0, offY=0, W_=0, H_=0; 2378 var ripples=[]; /* click ripples {x,y,t0} */ 2379 var px=-1e4, py=-1e4, active=false; 2380 2381 function coverFit(){ return !!(window.matchMedia && window.matchMedia('(max-width: 991px)').matches); } 2382 function showFallback(){ 2383 if(fallback){ fallback.style.display='block'; fallback.style.backgroundImage="url('"+MAP_SRC+"')"; } 2384 canvas.style.display='none'; 2385 } 2386 function hash(x,y){ var n=Math.sin(x*127.1+y*311.7)*43758.5453; return n-Math.floor(n); } 2387 2388 function buildPoints(cap){ 2389 var gw=mobile?360:380, gh=Math.max(1,Math.round(gw/mapAR)); /* r12 #7: desktop grid 440 -> 380 so each desktop dot samples a larger cell and reads bolder (mobile 360 unchanged) */ 2390 off.width=gw; off.height=gh; 2391 octx.clearRect(0,0,gw,gh); 2392 octx.drawImage(img,0,0,gw,gh); 2393 var data; 2394 try{ data=octx.getImageData(0,0,gw,gh).data; }catch(e){ return null; } 2395 var x,y,i,lum,land=0; 2396 for(y=0;y<gh;y++){ for(x=0;x<gw;x++){ i=(y*gw+x)*4; 2397 lum=0.299*data[i]+0.587*data[i+1]+0.114*data[i+2]; 2398 if(data[i+3]>40 && lum<LANDLUM) land++; } } 2399 if(land===0) return null; 2400 var stride=Math.max(1,Math.round(Math.sqrt(land/cap))); 2401 var list=[]; 2402 for(y=0;y<gh;y+=stride){ for(x=0;x<gw;x+=stride){ i=(y*gw+x)*4; 2403 lum=0.299*data[i]+0.587*data[i+1]+0.114*data[i+2]; 2404 if(data[i+3]>40 && lum<LANDLUM){ 2405 list.push({ 2406 u:(x+0.5)/gw, v:(y+0.5)/gh, /* r13: STRICT GRID -- position jitter removed so dots sit on a uniform grid */ 2407 r:RMIN+hash(x*1.3,y*1.7)*RJIT, /* varied dot size = engraving density */ 2408 ph:hash(x+7,y+3)*TAU /* phase for shimmer variance */ 2409 }); 2410 } } } 2411 return list; 2412 } 2413 2414 function layout(){ 2415 var W=Math.max(1,canvas.clientWidth), H=Math.max(1,canvas.clientHeight); 2416 canvas.width=Math.round(W*dpr); canvas.height=Math.round(H*dpr); 2417 ctx.setTransform(dpr,0,0,dpr,0,0); 2418 var drawW, drawH; 2419 if(coverFit()){ 2420 /* r14: FIT BY HEIGHT -- fill the stage height fully so the map is NOT cropped top/bottom; 2421 the width overflows so only the far edges (Alaska left / half of Australia right) crop off. 2422 Guard: if fitting by height would leave width gaps (wide tablet), fall back to fill-width. */ 2423 drawH=H*0.92; drawW=drawH*mapAR; /* r19: 8% shy of full height, centered -> a small top/bottom margin so the northern continents aren't flush-clipped at the top on mobile */ 2424 if(drawW<W){ drawW=W; drawH=drawW/mapAR; } 2425 } else { 2426 /* DESKTOP: FILL WIDTH edge-to-edge (crop only the empty polar ocean top
2426/bottom), centered */ 2427 drawW=W*ZOOM; drawH=drawW/mapAR; 2428 } 2429 offX=(W-drawW)*ANCHOR_X; offY=(H-drawH)*ANCHOR_Y; 2430 if(pts){ for(var k=0;k<pts.length;k++){ var p=pts[k]; p.bx=offX+p.u*drawW; p.by=offY+p.v*drawH; } } 2431 W_=W; H_=H; 2432 } 2433 2434 function render(t){ 2435 ctx.clearRect(0,0,W_,H_); 2436 if(!pts) return; 2437 var breathe=1+0.05*Math.sin(t*0.9); 2438 var near=[], i, p, x, y, r, dx, dy, d2, d, f; 2439 /* base ink stipple -- one batched path */ 2440 ctx.beginPath(); 2441 for(i=0;i<pts.length;i++){ 2442 p=pts[i]; x=p.bx; y=p.by; r=p.r*breathe; 2443 if(active){ 2444 dx=x-px; dy=y-py; d2=dx*dx+dy*dy; 2445 if(d2<CURSOR_R*CURSOR_R){ 2446 d=Math.sqrt(d2)||0.0001; f=1-d/CURSOR_R; 2447 var wave=Math.sin(d*0.16 - t*5 + p.ph)*(f*SHIM); 2448 x+=(dx/d)*wave; y+=(dy/d)*wave; 2449 r*=1+f*0.75; 2450 near.push(i, x, y, r); 2451 } 2452 } 2453 for(var q=0;q<ripples.length;q++){ 2454 var rp=ripples[q], rt=t-rp.t0; if(rt<0) continue; 2455 var rr=rt*260; var rdx=x-rp.x, rdy=y-rp.y, rd=Math.sqrt(rdx*rdx+rdy*rdy)||0.0001; 2456 var band=Math.exp(-Math.pow((rd-rr)/26,2))*Math.max(0,1-rt*0.9); 2457 if(band>0.01){ x+=(rdx/rd)*band*5; y+=(rdy/rd)*band*5; r*=1+band*0.5; } 2458 } 2459 ctx.moveTo(x+r,y); ctx.arc(x,y,r,0,TAU); 2460 } 2461 ctx.globalAlpha=FILL_A; ctx.fillStyle='#f4f7fa'; ctx.fill(); ctx.globalAlpha=1; 2462 2463 if(near.length){ 2464 ctx.globalCompositeOperation='lighter'; 2465 ctx.beginPath(); 2466 for(i=0;i<near.length;i+=4){ x=near[i+1]; y=near[i+2]; r=near[i+3]*1.15; 2467 ctx.moveTo(x+r,y); ctx.arc(x,y,r,0,TAU); } 2468 ctx.globalAlpha=0.5; ctx.fillStyle='#fae537'; ctx.fill(); ctx.globalAlpha=1; 2469 ctx.globalCompositeOperation='source-over'; 2470 } 2471 2472 if(ripples.length){ 2473 ctx.globalCompositeOperation='lighter'; 2474 for(var s=0;s<ripples.length;s++){ 2475 var rq=ripples[s], et=t-rq.t0; if(et<0) continue; 2476 var rad=et*260, a=Math.max(0,1-et*0.9)*0.22; 2477 if(a>0.005){ ctx.beginPath(); ctx.arc(rq.x,rq.y,rad,0,TAU); 2478 ctx.strokeStyle='rgba(250,229,55,'+a+')'; ctx.lineWidth=1.4; ctx.stroke(); } 2479 } 2480 ctx.globalCompositeOperation='source-over'; 2481 } 2482 } 2483 2484 /* ---- loop with true off-screen pause ---- */ 2485 var running=false, rafId=0, t0=0, tNow=0; 2486 function loop(ts){ 2487 if(!running) return; 2488 tNow=(ts-t0)/1000; 2489 ripples=ripples.filter(function(r){ return tNow-r.t0<1.4; }); 2490 render(tNow); 2491 rafId=requestAnimationFrame(loop); 2492 } 2493 function start(){ if(running||STATIC) return; running=true; t0=performance.now(); rafId=requestAnimationFrame(loop); } 2494 function stop(){ running=false; if(rafId) cancelAnimationFrame(rafId); rafId=0; } 2495 2496 /* ---- pointer (desktop only) ---- */ 2497 function toLocal(e){ var rc=canvas.getBoundingClientRect(); px=e.clientX-rc.left; py=e.clientY-rc.top; } 2498 if(!STATIC){ 2499 canvas.addEventListener('pointermove', function(e){ toLocal(e); active=true; }, {passive:true}); 2500 canvas.addEventListener('pointerleave', function(){ active=false; }); 2501 canvas.addEventListener('pointerdown', function(e){ toLocal(e); active=true; ripples.push({x:px,y:py,t0:tNow}); if(ripples.length>5) ripples.shift(); }); 2502 } 2503 2504 /* r15 (#7): MOBILE tap-to-pulse. The map stays a crisp STATIC frame; a genuine tap spawns a 2505 click-ripple and runs a SHORT rAF burst that animates ONLY while ripples are alive, then 2506 idles straight back to the static frame (no permanent loop -> perf-friendly). Desktop and 2507 reduced-motion are left untouched (this whole block is skipped unless mobile && !reduce). */ 2508 if(mobile && !reduce){ 2509 var burstRunning=false, burstRaf=0, burstStart=0; 2510 var burstFrame=function(ts){ 2511 if(!burstRunning) return; 2512 var tb=(ts-burstStart)/1000; 2513 ripples=ripples.filter(function(r){ return tb-r.t0<1.4; }); 2514 render(tb); 2515 if(ripples.length){ burstRaf=requestAnimationFrame(burstFrame); } 2516 else { burstRunning=false; burstRaf=0; render(0); } /* settle back to the crisp static frame */ 2517 }; 2518 var spawnPulse=function(lx,ly){ 2519 if(!ready) return; 2520 var tb = burstRunning ? (performance.now()-burstStart)/1000 : 0; 2521 ripples.push({x:lx,y:ly,t0:tb}); if(ripples.length>5) ripples.shift(); 2522 if(!burstRunning){ burstRunning=true; burstStart=performance.now(); burstRaf=requestAnimationFrame(burstFrame); } 2523 }; 2524 var tsx=0, tsy=0, tMoved=false; 2525 canvas.addEventListener('touchstart', function(e){
2526 var t=e.touches && e.touches[0]; if(!t) return; 2527 tsx=t.clientX; tsy=t.clientY; tMoved=false; 2528 }, {passive:true}); 2529 canvas.addEventListener('touchmove', function(e){ 2530 var t=e.touches && e.touches[0]; if(!t) return; 2531 if(Math.abs(t.clientX-tsx)>10 || Math.abs(t.clientY-tsy)>10) tMoved=true; /* a scroll, not a tap */ 2532 }, {passive:true}); 2533 canvas.addEventListener('touchend', function(e){ 2534 if(tMoved) return; /* only a genuine tap pulses */ 2535 var t=e.changedTouches && e.changedTouches[0]; if(!t) return; 2536 var rc=canvas.getBoundingClientRect(); 2537 spawnPulse(t.clientX-rc.left, t.clientY-rc.top); 2538 }, {passive:true}); 2539 } 2540 2541 var io=null; 2542 function observe(){ 2543 if('IntersectionObserver' in window){ 2544 io=new IntersectionObserver(function(es){ 2545 if(es[0].isIntersecting){ STATIC?render(0):start(); } else { stop(); } 2546 },{threshold:0.04}); 2547 io.observe(canvas); 2548 } else { STATIC?render(0):start(); } 2549 } 2550 2551 img.onload=function(){ 2552 mapAR=img.naturalWidth/img.naturalHeight || 1.6; 2553 pts=buildPoints(CAP); 2554 if(!pts){ showFallback(); return; } 2555 ready=true; layout(); render(0); observe(); 2556 }; 2557 img.onerror=function(){ showFallback(); }; 2558 img.src=MAP_SRC; 2559 2560 window.addEventListener('resize', function(){ if(ready) layout(); if(STATIC&&ready) render(0); }); 2561})(); 2562</script>
2562 2563 2564<!-- ===================== COMMUNITY-PEOPLE ROW (bcpeople_) -- assemble on scroll-in, then soft wave ===================== -->
2565<script> 2566(function(){ 2567 'use strict'; 2568 var row=document.getElementById('bcpeopleRow'); 2569 if(!row) return; 2570 var slots=Array.prototype.slice.call(row.querySelectorAll('.bcpeople_slot')); 2571 var figs=Array.prototype.slice.call(row.querySelectorAll('.bcpeople_fig-anim')); 2572 if(!slots.length) return; 2573 var reduce=!!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); 2574 if(reduce) return; /* leave the resting row visible, no motion */ 2575 2576 slots.forEach(function(s){ s.classList.add('is-armed'); }); /* JS present: hide until assembled */ 2577 2578 var ASSEMBLE_MS=720 + 6*120 + 140; /* longest slide-in + last stagger + buffer */ 2579 var started=false; 2580 function play(){ 2581 if(started) return; started=true; 2582 slots.forEach(function(s){ s.classList.remove('is-armed'); s.classList.add('is-assembling'); }); 2583 setTimeout(function(){ figs.forEach(function(f){ f.classList.add('is-waving'); }); }, ASSEMBLE_MS); 2584 } 2585 if('IntersectionObserver' in window){ 2586 var io=new IntersectionObserver(function(es){ 2587 es.forEach(function(e){ if(e.isIntersecting){ play(); io.disconnect(); } }); 2588 },{threshold:0.35}); 2589 io.observe(row); 2590 } else { play(); } 2591})(); 2592</script>
2592 2593 2594<!-- ===================== SMART-WHEEL intl carousel ===================== -->
2595<script> 2596(function(){ 2597 'use strict'; 2598 var REDUCE = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); 2599 2600 /* INTERNATIONAL / GLOBAL WORK -- PLACEHOLDER copy + reused imagery. FLAG: swap for real 2601 Be The Change International program names + summaries + photos before launch. */ 2602 var PROGRAMS = [ 2603 {name:'Desmond Tutu Program to End Global Hunger', c:'#295099', href:'desmond-tutu.html', img:'img/opt/btc-tutu-b.jpg', sum:'Climate-smart greenhouses that let communities feed themselves, across seven African countries and counting.'}, 2604 {name:'Soweto (Tutu Home)', c:'#295099', href:'soweto-tutu-home.html', img:'img/opt/btc-soweto-b.jpg', sum:'The program has a home now: the Tutu family house in Soweto, restored with Rotary International, a greenhouse growing in the garden.'}, 2605 {name:'Island Fresh (St. Croix, USVI)', c:'#295099', href:'island-fresh-st-croix.html', img:'img/opt/btc-island-b.jpg', /* FLAG: greenhouse stand-in (not St. Croix), needs real Island Fresh St. Croix */ sum:'Six greenhouses in a territory that imports 97 percent of its food, now run by the University of the Virgin Islands.'}, 2606 {name:'Mpumalanga (South Africa)', c:'#295099', href:'mpumalanga.html', img:'img/opt/btc-mpumalanga.jpg', sum:'A teaching greenhouse in Archbishop Tutu\'s home country, built with Conservation South Africa.'}, 2607 {name:'Randburg (Johannesburg)', c:'#295099', href:'randburg-training-facility-johannesburg-south-africa.html', img:'img/btc-randburg-b.jpg', sum:'Where Southern Africa\'s trainers learn the system, then carry it home.'}, 2608 {name:'Jubilee Housing -- Washington, D.C.', c:'#295099', href:'jubilee-housing-washington-dc.html', img:'img/btc-jubilee.jpg', /* FLAG: farm stand-in (not DC), needs real Jubilee DC */ sum:'A working farm inside an affordable apartment building for returning citizens.'} 2609 ]; 2610 2611 var ARROW = '<span class="swheel_go"><svg class="swheel_arrow" viewBox="0 0 28 13" aria-hidden="true"><path d="M0 6.5h25M20 2l5 4.5-5 4.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>'; 2612 2613 function cardHTML(p){ 2614 return '<a class="swheel_card" href="'+p.href+'" style="--swheel-c:'+p.c+'" aria-label="'+p.name+'" draggable="false">' + 2615 '<img class="swheel_img" src="'+p.img+'" alt="'+p.name+'" draggable="false" decoding="async">' + 2616 '<span class="swheel_ov"></span><span class="swheel_top"></span><span class="swheel_shade"></span>' + 2617 '<span class="swheel_content">' + 2618 '<span class="swheel_name">'+p.name+'</span>' + 2619 '<span class="swheel_foot">' + 2620 '<span class="swheel_sum">'+p.sum+'</span>' + 2621 ARROW + 2622 '</span>' + 2623 '</span></a>'; 2624 } 2625 2626 function measure(track){ 2627 var cards=track.children, one=0; 2628 for(var j=0;j<PROGRAMS.length && j<cards.length;j++){ 2629 var rc=cards[j].getBoundingClientRect(), s=getComputedStyle(cards[j]); 2630 one+=rc.width+parseFloat(s.marginLeft)+parseFloat(s.marginRight); 2631 } 2632 return one; 2633 } 2634 function fill(track, copies){ 2635 var html=''; 2636 for(var k=0;k<copies;k++){ for(var i=0;i<PROGRAMS.length;i++){ html+=cardHTML(PROGRAMS[i]); } } 2637 track.innerHTML=html; 2638 return measure(track); 2639 } 2640 2641 var lerp=function(a,b,t){ return a+(b-a)*t; }; 2642 function debounce(fn, ms){ var t; return function(){ var a=arguments,c=this; clearTimeout(t); t=setTimeout(function(){ fn.apply(c,a); }, ms); }; } 2643 /* r15 (#6): true only for hrefs that go somewhere (real path/URL, or an in-page anchor whose 2644 target exists). Bare '#' + unresolved anchors return false so a tap never navigates to '#' 2645 and jumps the page to top. (All swheel cards are '#' placeholders today, so none navigate.) */ 2646 function hrefResolves(h){ 2647 if(!h || h === '#') return false; 2648 if(h.charAt(0) === '#'){ var id=h.slice(1); return !!(id && document.getElementById(id)); } 2649 return true; 2650 } 2651 2652 function initCarousel(vpId, trackId){ 2653 var vp=document.getElementById(vpId), track=document.getElementById(trackId); 2654 if(!vp || !track) return; 2655 track.addEventListener('click', function(e){ e.preventDefault(); }); /* attach ONCE, not per fill() */ 2656 2657 /* perf: clone only as many copies as the viewport can EVER reveal (3 at 2560px, was a 2658 blanket 6). Same repeating sequence, identical frame, a third of the compositor layer. */ 2659 function copiesNeeded(){ return Math.max(2, Math.ceil(vp.clientWidth / setW) + 1); } 2660 var setW=fill(track, 1); 2661 if(setW<=0) return; 2662 var COPIES=copiesNeeded(); 2663 setW=fill(track, COPIES); 2664 vp.classList.add('is-ready'); /* band fades in now that the cards exist */ 2665 2666 var DRIFT=0.4, DRAG_MULT=1.4, FLING_DECAY=0.92, FLING_MAX=55; /* r18: >1:1 drag + flick momentum */ 2667 var st={ cur:0, tgt:0, dragging:false, hovering:false, lastX:0, moved:0, lastY:0, movedY:0, driftCur:DRIFT, downCard:null, vel:0, fling:0 }; 2668 st.cur=st.tgt=0; /* the sequence repeats, so 0 paints the exact frame -2*setW used to */ 2669 track.style.transform='translate3d('+st.cur+'px,0,0)'; /* paint centered before first frame */ 2670 2671 var running=false, inView=false, scrollIdleAt=0; 2672 /* perf: ambient drift yields to page scrolling (same treatment as mdrift) */ 2673 window.addEventListener('scroll', function(){ scrollIdleAt = performance.now() + 220; }, { passive:true });
2674 /* keep cur in (-setW, 0] so the visible window never runs off the shortened track */ 2675 function wrapBoth(){ 2676 while(st.cur <= -setW){ st.cur+=setW; st.tgt+=setW; } 2677 while(st.cur > 0){ st.cur-=setW; st.tgt-=setW; } 2678 } 2679 function frame(){ 2680 if(!running) return; 2681 if(!REDUCE){ 2682 var want=(st.dragging || st.hovering || performance.now()<scrollIdleAt) ? 0 : DRIFT; 2683 st.driftCur=lerp(st.driftCur, want, 0.08); 2684 st.tgt-=st.driftCur; 2685 /* flick momentum: a decaying impulse coasts the band after release; the lerp below settles it */ 2686 if(!st.dragging && Math.abs(st.fling) > 0.1){ st.tgt += st.fling; st.fling *= FLING_DECAY; } else { st.fling = 0; } 2687 } 2688 st.cur=lerp(st.cur, st.tgt, 0.09); 2689 wrapBoth(); 2690 track.style.transform='translate3d('+(Math.round(st.cur*100)/100)+'px,0,0)'; 2691 /* reduced-motion: no idle churn -- stop once a user gesture has settled */ 2692 if(REDUCE && !st.dragging && Math.abs(st.cur-st.tgt)<0.1){ running=false; return; } 2693 requestAnimationFrame(frame); 2694 } 2695 function start(){ if(running) return; running=true; requestAnimationFrame(frame); } 2696 function stop(){ running=false; } 2697 function pulse(card){ card.classList.remove('swheel_tapped'); void card.offsetWidth; card.classList.add('swheel_tapped'); } 2698 2699 var TAP_SLOP=9; 2700 vp.addEventListener('pointerdown', function(e){ 2701 if(e.button===2) return; 2702 st.dragging=true; st.lastX=e.clientX; st.lastY=e.clientY; st.moved=0; st.movedY=0; st.vel=0; st.fling=0; 2703 st.downCard=e.target.closest ? e.target.closest('.swheel_card') : null; 2704 vp.classList.add('swheel_grabbing'); 2705 try{ vp.setPointerCapture(e.pointerId); }catch(_){} 2706 start(); 2707 }); 2708 vp.addEventListener('pointermove', function(e){ 2709 if(!st.dragging) return; 2710 var raw=e.clientX-st.lastX; 2711 st.lastX=e.clientX; 2712 st.moved+=Math.abs(raw); /* tap-vs-drag uses RAW travel (threshold unchanged) */ 2713 st.movedY+=Math.abs(e.clientY-st.lastY); st.lastY=e.clientY; /* r26 (hit list 7/24 #3): vertical travel counts against the tap too, same as mdrift */ 2714 if(st.moved>TAP_SLOP){ var dx=raw*DRAG_MULT; st.tgt+=dx; st.vel=st.vel*0.6+dx*0.4; } /* band leads the thumb; smoothed velocity feeds the flick */ 2715 }); 2716 /* r15 (#6): only a GENUINE pointerup tap may follow a card link, and only when the href 2717 actually resolves. pointercancel / pointerleave (a scroll takeover, NOT a tap) pass 2718 canTap=false so they never navigate -- a vertical scroll that begins on a card used to 2719 fire window.location.href='#' here and jump the page to top right after the map. */ 2720 function up(canTap){ 2721 if(!st.dragging) return; 2722 st.dragging=false; vp.classList.remove('swheel_grabbing'); 2723 if(st.moved > TAP_SLOP && !REDUCE){ st.fling = Math.max(-FLING_MAX, Math.min(FLING_MAX, st.vel)); } /* release -> coast (non-reduced-motion) */ 2724 if(canTap && st.moved<=TAP_SLOP && st.movedY<=TAP_SLOP && st.downCard){ 2725 pulse(st.downCard); 2726 var _h=st.downCard.getAttribute('href'); 2727 if(hrefResolves(_h)){ window.location.href=_h; } 2728 } 2729 st.downCard=null; 2730 } 2731 vp.addEventListener('pointerup', function(){ up(true); }); 2732 vp.addEventListener('pointercancel', function(){ up(false); }); 2733 vp.addEventListener('pointerleave', function(){ if(st.dragging) up(false); }); 2734 vp.addEventListener('mouseenter', function(){ st.hovering=true; }); 2735 vp.addEventListener('mouseleave', function(){ st.hovering=false; }); 2736 2737 /* direction-aware wheel: horizontal intent (trackpad sideways) scrubs + claims the 2738 event; vertical intent (mouse wheel) is ignored so the page scrolls normally. */ 2739 vp.addEventListener('wheel', function(e){ 2740 if(Math.abs(e.deltaX) > Math.abs(e.deltaY)){ 2741 e.preventDefault(); 2742 st.tgt-=e.deltaX*1.15; 2743 start(); 2744 } 2745 }, { passive:false }); 2746 2747 /* in-view gating: idle-drift runs only while visible; paused off-screen */ 2748 if('IntersectionObserver' in window){ 2749 new IntersectionObserver(function(es){
2750 inView=es[0].isIntersecting; 2751 if(inView){ if(!REDUCE) start(); } else { stop(); } 2752 },{threshold:0.02}).observe(vp); 2753 } else { if(!REDUCE) start(); } 2754 2755 window.addEventListener('resize', debounce(function(){ 2756 var nw=measure(track); 2757 if(nw<=0) return; 2758 setW=nw; 2759 /* grow (never shrink) the copy count so a wider window can't expose a gap */ 2760 var need=copiesNeeded(); 2761 if(need>COPIES){ COPIES=need; setW=fill(track, COPIES); } 2762 }, 200)); 2763 } 2764 2765 initCarousel('swheelVp','swheelTrack'); 2766})(); 2767</script>
2767 2768 2769<!-- NEWS -- card-hover polish (image zoom + read-more gap) is now owned by fm-cms.js: 2770 it renders the blog cards from Sanity and binds the same .is-hover effect by DELEGATION 2771 (bindHover(grid,'pblog')), which survives re-rendering. The old parse-time querySelectorAll 2772 hover binding was removed with the hardcoded cards it targeted -- it could only ever bind 2773 nodes that no longer exist. --> 2774 2775<!-- GIVE / HORIZON -- Three.js gradient canvas (dark -> white melt, mouse-reactive bloom) -->
2776<script> 2777(function(){ 2778 'use strict'; 2779 var MODE=2; // 0 Nightfall (dark) · 1 Daybreak (light) · 2 Horizon (dark -> white) 2780 var section=document.getElementById('give'); 2781 var canvas=document.getElementById('giveGradient'); 2782 if(!canvas||!window.THREE) return; 2783 if(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; 2784 2785 var renderer=new THREE.WebGLRenderer({canvas:canvas,antialias:false}); 2786 renderer.setPixelRatio(Math.min(window.devicePixelRatio||1,2)); 2787 var scene=new THREE.Scene(); 2788 var camera=new THREE.OrthographicCamera(-1,1,1,-1,0,1); 2789 var mouse={x:0.30,y:0.42}, target={x:0.30,y:0.42}; 2790 2791 var VERT=['varying vec2 vUv;','void main(){vUv=uv;gl_Position=vec4(position.xy,0.0,1.0);}'].join('\n'); 2792 2793 var FRAG=[ 2794 'uniform float uTime;','uniform vec2 uMouse;','uniform int uMode;','varying vec2 vUv;', 2795 'float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}', 2796 'float noise(vec2 p){vec2 i=floor(p);vec2 f=fract(p);vec2 u=f*f*(3.0-2.0*f);', 2797 ' return mix(mix(hash(i),hash(i+vec2(1.0,0.0)),u.x),mix(hash(i+vec2(0.0,1.0)),hash(i+vec2(1.0,1.0)),u.x),u.y);}', 2798 'float fbm(vec2 p){float v=0.0;float a=0.5;for(int i=0;i<5;i++){v+=a*noise(p);p=p*2.1+vec2(3.7,8.4);a*=0.5;}return v;}', 2799 'void main(){', 2800 ' vec2 uv=vUv;', 2801 ' float t=uTime*0.15;', 2802 ' vec3 blue =vec3(0.161,0.314,0.600);', 2803 ' vec3 sky =vec3(0.000,0.604,0.808);', 2804 ' vec3 teal =vec3(0.000,0.635,0.529);', 2805 ' vec3 yellow=vec3(0.980,0.898,0.216);', 2806 ' vec3 orange=vec3(0.922,0.565,0.243);', 2807 ' vec3 red =vec3(0.902,0.235,0.204);', 2808 ' vec3 purple=vec3(0.612,0.227,0.561);', 2809 ' vec3 ink =vec3(0.047,0.078,0.149);', 2810 ' vec3 white =vec3(1.0);', 2811 // domain warp -> the color fields flow and morph into each other (no pebbly cells) 2812 ' float nx=fbm(uv*1.7+vec2(0.0,t*0.12));', 2813 ' float ny=fbm(uv*1.7+vec2(4.3,1.7)-t*0.10);', 2814 ' vec2 p=uv+(vec2(nx,ny)-0.5)*0.24;', 2815 // drifting centers -- cools left, warms right, so overlaps blend clean 2816 ' vec2 c1=vec2(0.16+0.06*sin(t*0.53),0.34+0.07*cos(t*0.47));', 2817 ' vec2 c2=vec2(0.24+0.07*cos(t*0.41),0.70+0.06*sin(t*0.63));', 2818 ' vec2 c3=vec2(0.44+0.07*sin(t*0.59),0.44+0.07*cos(t*0.37));', 2819 ' vec2 c4=vec2(0.74+0.06*cos(t*0.44),0.34+0.07*sin(t*0.55));', 2820 ' vec2 c5=vec2(0.84+0.06*sin(t*0.49),0.66+0.06*cos(t*0.61));', 2821 ' vec2 c6=vec2(0.62+0.06*cos(t*0.57),0.76+0.07*sin(t*0.39));', 2822 ' vec2 c7=vec2(0.90+0.05*sin(t*0.45),0.20+0.07*cos(t*0.51));', 2823 ' float k=3.7;', 2824 ' float w1=exp(-length(p-c1)*k);', 2825 ' float w2=exp(-length(p-c2)*k);', 2826 ' float w3=exp(-length(p-c3)*k);', 2827 ' float w4=exp(-length(p-c4)*k);', 2828 ' float w5=exp(-length(p-c5)*k);', 2829 ' float w6=exp(-length(p-c6)*k);', 2830 ' float w7=exp(-length(p-c7)*k);', 2831 ' float wc=w1+w2+w3+w4+w5+w6+w7;', 2832 ' vec3 field=(w1*blue+w2*sky+w3*teal+w4*yellow+w5*orange+w6*red+w7*purple)/(wc+0.0001);', 2833 ' float grain=(fbm(p*3.0+t*0.10)-0.5)*0.022;', 2834 ' float wM=exp(-length(uv-uMouse)*2.6);', 2835 ' float strength=1.0-exp(-wc*1.5);', 2836 // dark base: reference-style weighted blend; ink anchor holds the silhouette + text legibility, 2837 // the cursor rides INSIDE the blend as white light so it diffuses into the colors. 2838 ' float wD=0.82+0.85*exp(-length(uv-vec2(0.5,0.54))*1.6);', 2839 ' float wmG=wM*3.2;', 2840 ' float denom=wc+wD+wmG+0.0001;', 2841 ' vec3 base=(w1*blue+w2*sky+w3*teal+w4*yellow+w5*orange+w6*red+w7*purple+wD*ink+wmG*vec3(1.0,0.99,0.96))/denom;', 2842 ' base=clamp(mix(vec3(dot(base,vec3(0.333))),base,1.30),0.0,1.0);', 2843 ' vec3 col;', 2844 ' if(uMode==0){', 2845 ' col=base+grain;', 2846 ' float vig=1.0-dot(uv-0.5,(uv-0.5)*1.7); vig=clamp(vig,0.0,1.0);', 2847 ' col=mix(ink*0.5,col,vig);', 2848 ' }', 2849 ' else if(uMode==1){', 2850 ' col=mix(white,field,strength*0.85)+grain;', 2851 ' float sides=smoothstep(0.0,0.30,uv.x)*smoothstep(1.0,0.70,uv.x);', 2852 ' col=mix(white,col,clamp(sides+0.30,0.0,1.0));', 2853 ' float melt=smoothstep(0.02,0.32,uv.y);', 2854 ' col=mix(white,col,melt);', 2855 ' col=mix(col,field,wM*0.6);', 2856 ' }', 2857 ' else{', 2858 ' vec3 dark=base+grain;', 2859 ' float vig=1.0-dot(uv-0.5,(uv-0.5)*1.55); vig=clamp(vig,0.0,1.0);', 2860 ' dark=mix(ink*0.5,dark,vig);', 2861 /* r7: SOFTEN the melt into the white news band -- white fades in gradually over the 2862 bottom third (uv.y 0 = the bottom edge into news). Tall c
2862olored middle above it. */ 2863 ' float melt=smoothstep(0.0,0.33,uv.y);', 2864 ' col=mix(white,dark,melt);', 2865 ' }', 2866 ' col=clamp(col,0.0,1.0);', 2867 ' gl_FragColor=vec4(col,1.0);', 2868 '}' 2869 ].join('\n'); 2870 2871 var uniforms={uTime:{value:0.0},uMouse:{value:new THREE.Vector2(0.30,0.42)},uMode:{value:MODE}}; 2872 var material=new THREE.ShaderMaterial({uniforms:uniforms,vertexShader:VERT,fragmentShader:FRAG}); 2873 scene.add(new THREE.Mesh(new THREE.PlaneGeometry(2,2),material)); 2874 2875 function resize(){ var w=section.clientWidth,h=section.clientHeight; if(w&&h) renderer.setSize(w,h,false); } 2876 resize(); 2877 2878 window.addEventListener('mousemove',function(e){ 2879 var r=section.getBoundingClientRect(); 2880 if(!r.width||!r.height) return; 2881 target.x=(e.clientX-r.left)/r.width; 2882 target.y=1.0-(e.clientY-r.top)/r.height; 2883 },{passive:true}); 2884 2885 /* PERF GATE: this shader is the biggest continuous cost -- run the rAF loop ONLY 2886 while #give is in view and the tab is visible; pause otherwise. */ 2887 var lastT=0,clock=0,running=false,raf=0,inView=false; 2888 function animate(now){ 2889 if(!running) return; 2890 if(!lastT) lastT=now; 2891 clock+=(now-lastT)/1000; lastT=now; 2892 mouse.x+=(target.x-mouse.x)*0.05; mouse.y+=(target.y-mouse.y)*0.05; 2893 uniforms.uTime.value=clock; 2894 uniforms.uMouse.value.set(mouse.x,mouse.y); 2895 renderer.render(scene,camera); 2896 raf=requestAnimationFrame(animate); 2897 } 2898 function play(){ if(running) return; running=true; lastT=0; raf=requestAnimationFrame(animate); } 2899 function pause(){ running=false; if(raf){ cancelAnimationFrame(raf); raf=0; } } 2900 if('IntersectionObserver' in window){ 2901 var gio=new IntersectionObserver(function(es){ 2902 inView=es[0].isIntersecting; 2903 if(inView && !document.hidden) play(); else pause(); 2904 },{threshold:0.01}); 2905 gio.observe(section); 2906 } else { inView=true; play(); } 2907 document.addEventListener('visibilitychange',function(){ 2908 if(document.hidden) pause(); else if(inView) play(); 2909 }); 2910 window.addEventListener('resize',resize); 2911})(); 2912</script>
2912 2913 2914<!-- SCROLL-REVEAL SYSTEM -- each KEY SECTION HEADLINE (programs / global / give / news) uses the 2915 SHARED hero text-reveal: a masked per-character cascade on scroll-in, then the elements beneath 2916 it rise in. The MAP keeps its own zoom-in. Honors reduced-motion (whole block bails -> content 2917 stays visible). Carousels (mdrift / swheel) are NOT touched. Stat numbers are STATIC 2918 (count-up removed per owner 2026-07-03); the stat groups rise in as risers here. -->
2919<script> 2920(function(){ 2921 'use strict'; 2922 var reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); 2923 if(!window.gsap || reduce) return; /* reduced-motion: leave everything visible */ 2924 gsap.registerPlugin(ScrollTrigger); 2925 var R = window.FMReveal; 2926 if(!R || !R.revealHeadline){ return; } /* shared helper missing -> content stays visible */ 2927 2928 /* Each KEY SECTION HEADLINE now uses the SAME text-reveal as the hero: masked + split 2929 per-character, cascading UP on scroll-into-view (yPercent:120 -> 0, dur 1.15, stagger .055, 2930 power4.out); then the elements underneath rise in (opacity + y12, power3.out, ~.9s, gentle 2931 stagger). These headings are all solid-color, so per-character splitting is safe. */ 2932 2933 /* PROOF / STATS -- '4,000+' cascades like every other section headline; the lead and the 2934 three stats rise underneath (count-up removed per owner: keep it simple and clean). */ 2935 R.revealHeadline(document.querySelector('.proof_big'), 2936 ['.proof_lead', '.proof_stat'], 2937 { trigger:'.proof', start:'top 78%' }); 2938 2939 /* LOCAL PROGRAMS -- .prog_head cascade, then subhead + the accordion list rise underneath. 2940 (The right-side masonry is not a riser -- it stays put; add '.prog_masonry' here if wanted.) */ 2941 R.revealHeadline(document.querySelector('.prog_head'), 2942 ['.prog_sub', '.prog_list'], 2943 { trigger:'.prog', start:'top 78%' }); 2944 2945 /* BE THE CHANGE INTERNATIONAL -- .stipple_title cascade (SKIP .bcpeople_row: it has its own 2946 assemble anim). Then subhead / stat band / body / CTA rise in DOM order (the band joined 2947 the risers when the count-up was removed -- the numbers are static now). */ 2948 R.revealHeadline(document.querySelector('.stipple_title'), 2949 ['.stipple_lead', '.bcstat_item', '.bcstat_body', '.stipple_cta'], 2950 { trigger:'.stipple_head', start:'top 80%' }); 2951 2952 /* MAP -- subtle ZOOM-IN, KEPT per owner (not a headline; keeps its own tween) */ 2953 (function(){ 2954 var stage=document.querySelector('.stipple_stage'); 2955 if(!stage) return; 2956 gsap.from(stage, { scale:0.86, opacity:0, duration:1.35, ease:'power2.out', transformOrigin:'50% 50%', 2957 scrollTrigger:{ trigger:stage, start:'top 84%' } }); 2958 })(); 2959 2960 /* GIVE -- .give_title cascade, then .give_text + the two .give_btn rise. We do NOT tween 2961 .give_inner (it carries a CSS translateY baseline a y-tween would fight); the risers use 2962 their own small y instead. */ 2963 R.revealHeadline(document.querySelector('.give_title'), 2964 ['.give_text', '.give_btn'], 2965 { start:'top 85%' }); /* trigger defaults to the heading itself -- .give is 130vh tall with the title mid-section, so a section-top trigger would fire far too early */ 2966 2967 /* NEWS -- .news_title cascade (heading only). The blog cards below are now rendered LIVE 2968 from Sanity by fm-cms.js and arrive AFTER this reveal runs, so they are not faded here -- 2969 they appear as fm-cms fills the grid, exactly like every program-page "From the news" strip. */ 2970 R.revealHeadline(document.querySelector('.news_title'), [], { trigger:'.news', start:'top 82%' }); 2971})(); 2972</script>
2972 2973 2974<!-- Program masonry tiles are now NATIVE <a class="prog_ph_link"> anchors (see the markup in 2975 the #programs section): clicking a tile navigates via HTML with NO JS dependency, and it 2976 also gets a native pointer cursor + right/middle-click + status-bar URL for free. They stay 2977 decorative duplicates of the canonical, keyboard-reachable left-list nav (tabindex="-1" + 2978 aria-hidden), so they are kept out of the tab order and the a11y tree. The old JS click-nav 2979 shim (window.location on an <img> click) was removed -- native anchors supersede it, and the 2980 accordion IIFE above still syncs the tiles' hover brighten/dim via .prog_ph[data-key]. --> 2981 2982 2983<!-- SCROLLTRIGGER REFRESH (#5) -- recompute every trigger's start/end AFTER the page settles. 2984 The Three.js globe + 2D stipple canvases size late, web fonts (Marcellus/Newsreader/Inter) 2985 swap in and reflow text, and lazy images load after first paint -- all of which shift page 2986 height and leave the count-up + reveal triggers with STALE positions (so on mobile they 2987 never reach their 'top 85%' and never fire). Refresh on load, once fonts are ready, and on 2988 a couple of short timeouts to catch the last layout shift. -->
2989<script> 2990(function(){ 2991 if(!window.ScrollTrigger) return; 2992 function refresh(){ try{ ScrollTrigger.refresh(); }catch(e){} } 2993 window.addEventListener('load', refresh); 2994 if(document.fonts && document.fonts.ready && document.fonts.ready.then){ document.fonts.ready.then(refresh); } 2995 setTimeout(refresh, 700); 2996 setTimeout(refresh, 1800); 2997})(); 2998</script>
2998 2999 3000
3001<script src="/fm-search.js" defer></script>
3001 3002 3003 3004 3005<!-- FM-NAV-FS:JS -->
3006<script data-fm="nav-fs"> 3007/* ============================================================ 3008 FM FULL-SCREEN NAV OVERLAY (FS) controller. 3009 Opened by the EXISTING topnav hamburger (#navToggle) on all viewports. 3010 BUILD WAVE 2026-07-29 (BW-NAV-IA): C1-C "Horizon" desktop -- hover/focus 3011 on the line swaps the pane below it and glides the 7-color runner under 3012 the active item; leaving the line rests the pane + runner after 350ms. 3013 Mobile: C1-B accordion (caret / label toggles), unchanged mechanics. 3014 On open: set .topnav.is-menu (bar's own children fade; hamburger morphs to 3015 X in place), offset the vanished scrollbar via --sbw, lock body scroll, and 3016 play the GSAP wave reveal (fade + solid-floor fallback without GSAP / reduced 3017 motion). Escape + click-outside + a focus trap that keeps the hamburger (the 3018 close control) in the loop, focus restore on close. The in-menu search button 3019 opens the sitewide fm-search overlay (z-2000, above this menu). Reuses the 3020 page's already loaded GSAP -- does NOT load its own copy. 3021 ============================================================ */ 3022(function(){ 3023 if(window.__fmNavFS) return; window.__fmNavFS = true; 3024 var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 3025 var hasGSAP = !!window.gsap; 3026 3027 var toggle = document.getElementById('navToggle'); 3028 var menu = document.getElementById('fsMenu'); 3029 if(!toggle || !menu) return; 3030 var topnav = document.getElementById('topnav'); 3031 var bars = toggle.querySelectorAll('.topnav_bar'); 3032 var path = document.getElementById('fsWavePath'); 3033 var items = Array.prototype.slice.call(menu.querySelectorAll('.nav2_item')); 3034 var panels = Array.prototype.slice.call(menu.querySelectorAll('.nav2_panel')); 3035 var carets = Array.prototype.slice.call(menu.querySelectorAll('.nav2_caret')); 3036 var runner = document.getElementById('fsRunner'); 3037 var list = menu.querySelector('.nav2_list'); 3038 var searchBtn = menu.querySelector('.nav2_search'); 3039 var isOpen = false, openTl = null, closeTl = null, restTimer = null, lastKey = 'default'; 3040 var useWave = hasGSAP && !reduce && !!path; 3041 var mqMobile = window.matchMedia('(max-width:860px)'); 3042 3043 /* wave path keyframes -- INIT collapsed sliver / WAVE mid-crest / FULL crest 3044 clamped BELOW the viewport floor (all y > 1000) so the fill covers the whole 3045 screen with margin -- the bottom-clip / sliver fix. */ 3046 var INIT='M0 2S175 1 500 1s500 1 500 1V0H0Z'; 3047 var WAVE='M0 502S175 272 500 272s500 230 500 230V0H0Z'; 3048 var FULL='M0,1006S175,1002,500,1002s500,4,500,4V0H0Z'; 3049 3050 /* ---------- the 7-color runner (desktop): rests centered, glides under the active item ---------- */ 3051 function moveRunner(key){ 3052 if(!runner || !list || mqMobile.matches) return; 3053 if(!key || key === 'default'){ 3054 var w = Math.min(224, list.clientWidth * .22); 3055 runner.style.width = w + 'px'; 3056 runner.style.transform = 'translateX(' + ((list.clientWidth - w) / 2) + 'px)'; 3057 return; 3058 } 3059 var it = list.querySelector('.nav2_item[data-panel="' + key + '"]'); 3060 if(!it) return; 3061 runner.style.width = it.offsetWidth + 'px'; 3062 runner.style.transform = 'translateX(' + it.offsetLeft + 'px)'; 3063 } 3064 3065 /* ---------- panel swap ---------- */ 3066 function showPanel(key){ 3067 for(var i=0;i<panels.length;i++){ 3068 panels[i].classList.toggle('is-active', panels[i].getAttribute('data-panelbody')===key); 3069 } 3070 for(var j=0;j<items.length;j++){ 3071 items[j].classList.toggle('is-active', items[j].getAttribute('data-panel')===key); 3072 } 3073 for(var k=0;k<carets.length;k++){ 3074 carets[k].setAttribute('aria-expanded', carets[k].getAttribute('data-panel')===key ? 'true':'false'); 3075 } 3076 lastKey = key || 'default'; 3077 moveRunner(lastKey); 3078 } 3079 function resetPanels(){ showPanel(mqMobile.matches ? null : 'default'); } 3080 function toggleAccordion(key){ 3081 var active = menu.querySelector('.nav2_panel.is-active'); 3082 var curr = active ? active.getAttribute('data-panelbody') : null; 3083 showPanel(curr === key ? null : key); 3084 } 3085 3086 /* desktop hover/focus reveals the pane; mobile Follow (a <button>) toggles */
3087 items.forEach(function(it){ 3088 var key = it.getAttribute('data-panel'); 3089 it.addEventListener('mouseenter', function(){ if(!mqMobile.matches){ clearTimeout(restTimer); showPanel(key || 'default'); } }); 3090 it.addEventListener('focus', function(){ if(!mqMobile.matches){ clearTimeout(restTimer); showPanel(key || 'default'); } }); 3091 it.addEventListener('click', function(e){ 3092 if(mqMobile.matches && it.tagName === 'BUTTON'){ e.preventDefault(); toggleAccordion(key); } 3093 }); 3094 }); 3095 carets.forEach(function(btn){ 3096 btn.addEventListener('click', function(e){ 3097 e.preventDefault(); 3098 toggleAccordion(btn.getAttribute('data-panel')); 3099 }); 3100 }); 3101 /* C1-C: leaving the line + its stage rests the pane and re-centers the runner. 3102 The panes are DOM descendants of .nav2_list, so moving from the line down 3103 into a pane never counts as leaving -- only truly walking away does. */ 3104 if(list){ 3105 list.addEventListener('mouseleave', function(){ 3106 if(mqMobile.matches) return; 3107 clearTimeout(restTimer); 3108 restTimer = setTimeout(function(){ showPanel('default'); }, 350); 3109 }); 3110 list.addEventListener('mouseenter', function(){ clearTimeout(restTimer); }); 3111 } 3112 3113 /* in-menu search (locked): hand off to the sitewide fm-search overlay, which 3114 owns the real input (z-2000). Close the menu FIRST so the two overlays 3115 never fight over the body scroll lock; the topnav trigger is hidden while 3116 the menu is open but stays programmatically clickable. If fm-search has 3117 not injected its trigger yet, this is a harmless no-op. */ 3118 if(searchBtn){ 3119 searchBtn.addEventListener('click', function(){ 3120 var t = document.querySelector('.fmsx_trigger'); 3121 if(!t) return; 3122 setOpen(false); 3123 t.click(); 3124 }); 3125 } 3126 3127 /* ---------- open / close ---------- */ 3128 function playOpen(){ 3129 if(closeTl) closeTl.kill(); 3130 menu.classList.add('is-open','is-wave'); 3131 openTl = gsap.timeline(); 3132 openTl.set(path,{attr:{d:INIT}}) 3133 .set('#fsMenu .nav2_grid',{autoAlpha:1}) 3134 .to(path,{duration:0.5,attr:{d:WAVE},ease:'power2.in'}) 3135 .to(path,{duration:0.5,attr:{d:FULL},ease:'power2.out'},'-=0.12') 3136 .call(function(){ menu.classList.add('is-covered'); moveRunner(lastKey); }, null, '-=0.34') 3137 .fromTo('#fsMenu .nav2_search',{autoAlpha:0},{autoAlpha:1,duration:0.45,ease:'power2.out',clearProps:'opacity,visibility'},'-=0.34') 3138 .fromTo('#fsMenu .nav2_head',{autoAlpha:0,y:28},{autoAlpha:1,y:0,duration:0.5,ease:'power3.out',stagger:0.07,clearProps:'opacity,visibility,transform'},'-=0.4') 3139 .fromTo('#fsMenu .nav2_panel.is-active',{autoAlpha:0,y:14},{autoAlpha:1,y:0,duration:0.5,ease:'power3.out',clearProps:'opacity,visibility,transform'},'-=0.4'); 3140 } 3141 function playClose(onDone){ 3142 if(openTl) openTl.kill(); 3143 menu.classList.remove('is-covered'); 3144 closeTl = gsap.timeline({onComplete:function(){ menu.classList.remove('is-open','is-wave'); if(onDone) onDone(); }}); 3145 closeTl.to('#fsMenu .nav2_grid',{autoAlpha:0,duration:0.22,ease:'power2.in'}) 3146 .to(path,{duration:0.45,attr:{d:WAVE},ease:'power2.in'},'-=0.05') 3147 .to(path,{duration:0.4,attr:{d:INIT},ease:'power2.out'},'-=0.16'); 3148 } 3149 3150 function setOpen(open){ 3151 if(open === isOpen) return; 3152 isOpen = open; 3153 for(var i=0;i<bars.length;i++){ bars[i].classList.toggle('is-open', open); } /* hamburger -> X in place */ 3154 toggle.setAttribute('aria-expanded', open ? 'true':'false'); 3155 toggle.setAttribute('aria-label', open ? 'Close menu':'Open menu'); 3156 if(open) resetPanels(); 3157 /* the bar STAYS (z-60, above the z-55 overlay); only its other items fade via 3158 .is-menu. --sbw offsets the scrollbar that vanishes with the body lock so 3159 the hamburger does not teleport. */ 3160 if(open && topnav){ 3161 topnav.classList.remove('is-tucked'); 3162 var sbw = window.innerWidth - document.documentElement.clientWidth; 3163 document.documentElement.style.setProperty('--sbw', sbw + 'px'); 3164 topnav.classList.add('is-menu'); 3165 } 3166 if(!open && menu.contains(document.activeElement)){ try{document.activeElement.blur();}catch(e){} } 3167 menu.setAttribute('aria-hidden', open ? 'false':'true'); 3168 document.body.style.overflow = open ? 'hidden':''; 3169 if(!open){ document.documentElement.style.setProperty('--sbw','0px'); } 3170 3171 if(useWave){ 3172 if(open){ playOpen(); } 3173 else { playClose(function(){ if(topnav) topnav.classList.remove('is-menu'); try{ toggle.focus(); }catch(e){} }); } 3174 } else { 3175 /* no-GSAP / reduced-motion: no morph, just cover + fade */ 3176 if(path) path.setAttribute('d', open ? FULL : INIT);
3177 menu.classList.toggle('is-open', open); 3178 menu.classList.toggle('is-covered', open); 3179 if(open){ setTimeout(function(){ moveRunner(lastKey); }, 30); } 3180 if(!open && topnav) topnav.classList.remove('is-menu'); 3181 if(!open){ setTimeout(function(){ try{ toggle.focus(); }catch(e){} }, 30); } 3182 } 3183 if(open){ setTimeout(function(){ try{ menu.focus(); }catch(e){} }, 60); } 3184 } 3185 3186 /* ---------- wiring ---------- */ 3187 toggle.addEventListener('click', function(){ setOpen(!isOpen); }); 3188 /* click the empty navy (not a link/button) closes */ 3189 menu.addEventListener('click', function(e){ 3190 if(e.target === menu || e.target.classList.contains('nav2_solid') || 3191 e.target.classList.contains('nav2_wave') || e.target.classList.contains('nav2_grid') || 3192 e.target.classList.contains('nav2_list')){ 3193 setOpen(false); 3194 } 3195 }); 3196 document.addEventListener('keydown', function(e){ 3197 if(!isOpen) return; 3198 if(e.key === 'Escape'){ setOpen(false); return; } 3199 if(e.key === 'Tab'){ 3200 var f = menu.querySelectorAll('a[href],button:not([disabled]),[tabindex]:not([tabindex="-1"])'); 3201 var vis = []; 3202 for(var i=0;i<f.length;i++){ if(f[i].offsetParent !== null) vis.push(f[i]); } 3203 vis.unshift(toggle); /* the hamburger (the close control) stays in the loop */ 3204 if(!vis.length) return; 3205 var first = vis[0], last = vis[vis.length-1]; 3206 if(e.shiftKey && document.activeElement === first){ e.preventDefault(); last.focus(); } 3207 else if(!e.shiftKey && document.activeElement === last){ e.preventDefault(); first.focus(); } 3208 } 3209 }); 3210 window.addEventListener('resize', function(){ if(isOpen) moveRunner(lastKey); }); 3211 if(document.fonts && document.fonts.ready){ document.fonts.ready.then(function(){ if(isOpen) moveRunner(lastKey); }); } 3212 mqMobile.addEventListener('change', function(){ if(isOpen) resetPanels(); }); 3213})(); 3214</script>
vendor: 2 bytes, line 3214
3214
3215<script src="/fm-cms.js"></script>
3215 3216<!-- /FM-NAV-FS:JS -->
3217<script src="/fm-forms.js" defer></script>
3217 3218</body> 3219</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.