PageSourceSearch

https://freshministries.org/

html freshministries.org collected 2026-10-02 10:48:16 UTC 216,697 bytes, 3,219 lines download raw bytes

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">&rarr;</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">&rarr;</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> &middot;
1686      <span>EIN #59-2967898</span> &middot;
1687      <span>Headquartered in Jacksonville, FL</span> &middot;
1688      <a href="/privacy-policy" class="footer_legal-link">Privacy Policy</a> &middot;
1689      <a href="/copyright-notice" class="footer_legal-link">Copyright</a> &middot;
1690      <a href="/sources-of-support" class="footer_legal-link">Sources of Support</a> &middot;
1691      <span>&copy; 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.