/* global React */ // KSM B2B Website — shared primitives const { useState, useEffect, useRef } = React; // ---- Icon (Lucide) ------------------------------------------------------- // Custom composite glyphs (not in Lucide) — drawn in Lucide's stroke style. const CUSTOM_ICONS = { // Praying hands enclosed in the heraldic shield — for "Prayer and Spiritual Warfare" "shield-pray": '' }; function Icon({ name, size = 20, stroke = 1.75, color, style = {}, className = "" }) { const ref = useRef(null); useEffect(() => { const el = ref.current; if (!el) return; const custom = CUSTOM_ICONS[name]; if (custom) { el.innerHTML = '' + custom + ''; return; } if (window.lucide && window.lucide.icons) { const key = name.split("-").map(s => s[0].toUpperCase() + s.slice(1)).join(""); const node = window.lucide.icons[key]; if (node) { el.innerHTML = ""; const svg = window.lucide.createElement(node); svg.setAttribute("width", size); svg.setAttribute("height", size); svg.setAttribute("stroke-width", stroke); if (color) svg.setAttribute("stroke", color); el.appendChild(svg); } } }, [name, size, stroke, color]); return ; } // ---- Eyebrow ------------------------------------------------------------- function Eyebrow({ children, tone = "navy", style = {} }) { const colors = { navy: "var(--navy-700)", gold: "var(--gold-600)", light: "var(--sky-300)", sky: "var(--sky-500)" }; return (
{children}
); } // ---- Gold rule ----------------------------------------------------------- function GoldRule({ w = 48, style = {} }) { return
; } // ---- Pill / tag ---------------------------------------------------------- function Pill({ children, tone = "navy", style = {} }) { const tones = { navy: { bg: "var(--sky-100)", fg: "var(--navy-700)" }, sky: { bg: "rgba(0,174,239,0.12)", fg: "var(--navy-700)" }, gold: { bg: "rgba(201,162,75,0.16)", fg: "var(--gold-600)" }, light: { bg: "rgba(255,255,255,0.12)", fg: "#fff" }, dark: { bg: "var(--ink)", fg: "#fff" }, }; const t = tones[tone] || tones.navy; return ( {children} ); } // ---- Button -------------------------------------------------------------- function Button({ children, variant = "primary", size = "md", icon, onClick, href, type, style = {} }) { const [hover, setHover] = useState(false); const [press, setPress] = useState(false); const pads = size === "lg" ? "16px 30px" : size === "sm" ? "10px 18px" : "13px 24px"; const fs = size === "lg" ? 13.5 : size === "sm" ? 11.5 : 12.5; const base = { fontFamily: "var(--font-label)", fontWeight: 600, fontSize: fs, letterSpacing: "0.1em", textTransform: "uppercase", padding: pads, borderRadius: 6, border: "1.5px solid transparent", cursor: "pointer", display: "inline-flex", alignItems: "center", gap: 9, lineHeight: 1, transition: "all .2s var(--ease)", transform: press ? "scale(0.98)" : "none", textDecoration: "none", whiteSpace: "nowrap", ...style }; const variants = { primary: hover ? { background: "linear-gradient(135deg, var(--gold-200) 0%, var(--gold-500) 46%, var(--gold-600) 100%)", color: "#1c1407", boxShadow: "0 6px 18px rgba(201,162,75,0.42), inset 0 1px 0 rgba(255,255,255,0.55)" } : { background: "var(--navy-700)", color: "#fff", boxShadow: "none" }, accent: hover ? { background: "linear-gradient(135deg, var(--gold-200) 0%, var(--gold-500) 46%, var(--gold-600) 100%)", color: "#1c1407", boxShadow: "0 6px 18px rgba(201,162,75,0.42), inset 0 1px 0 rgba(255,255,255,0.55)" } : { background: "var(--sky-500)", color: "#04263f", boxShadow: "none" }, gold: { background: hover ? "var(--gold-600)" : "var(--gold-500)", color: "#1c1407", boxShadow: hover ? "var(--shadow-md)" : "none" }, enroll: hover ? { background: "linear-gradient(135deg, var(--gold-200) 0%, var(--gold-500) 46%, var(--gold-600) 100%)", color: "#1c1407", boxShadow: "0 6px 18px rgba(201,162,75,0.42), inset 0 1px 0 rgba(255,255,255,0.55)" } : { background: "var(--sky-500)", color: "#04263f", boxShadow: "none" }, secondary: hover ? { background: "linear-gradient(135deg, var(--gold-200) 0%, var(--gold-500) 46%, var(--gold-600) 100%)", borderColor: "var(--gold-500)", color: "#1c1407", boxShadow: "0 6px 18px rgba(201,162,75,0.42), inset 0 1px 0 rgba(255,255,255,0.55)" } : { background: "transparent", borderColor: "var(--navy-700)", color: "var(--navy-700)" }, "secondary-light": hover ? { background: "linear-gradient(135deg, var(--gold-200) 0%, var(--gold-500) 46%, var(--gold-600) 100%)", borderColor: "var(--gold-500)", color: "#1c1407", boxShadow: "0 6px 18px rgba(201,162,75,0.42), inset 0 1px 0 rgba(255,255,255,0.55)" } : { background: "transparent", borderColor: "rgba(255,255,255,0.55)", color: "#fff" }, ghost: hover ? { background: "rgba(201,162,75,0.16)", color: "var(--gold-600)", borderColor: "transparent" } : { background: "transparent", color: "var(--navy-700)", borderColor: "transparent" }, }; const Tag = href ? "a" : "button"; return ( setHover(true)} onMouseLeave={() => { setHover(false); setPress(false); }} onMouseDown={() => setPress(true)} onMouseUp={() => setPress(false)} style={{ ...base, ...variants[variant] }}> {children} {icon && } ); } // ---- Section container --------------------------------------------------- function Container({ children, narrow, style = {} }) { return
{children}
; } // ---- Section heading block (eyebrow + title + optional lead) ------------- function SectionHead({ eyebrow, eyebrowTone = "navy", title, lead, align = "left", dark = false, maxLead = 620, style = {} }) { return (
{eyebrow && {eyebrow}}

{title}

{align !== "center" && } {lead &&

{lead}

}
); } // reveal-on-scroll hook function useReveal() { const ref = useRef(null); const [shown, setShown] = useState(false); const [forced, setForced] = useState(false); // hard fallback: snap visible (no transition) useEffect(() => { const el = ref.current; if (!el) { setShown(true); return; } let done = false; const reveal = () => { if (!done) { done = true; setShown(true); } }; const io = new IntersectionObserver((entries, obs) => { if (entries[0].isIntersecting) { reveal(); obs.disconnect(); } }, { threshold: 0, rootMargin: "0px 0px -8% 0px" }); io.observe(el); const t = setTimeout(reveal, 700); // Guarantee final visible state even where CSS transitions never advance // (print, PDF/PPTX capture, non-painting frames). Removing the transition // makes opacity:1 apply instantly instead of being stuck mid-tween. const f = setTimeout(() => setForced(true), 1500); return () => { io.disconnect(); clearTimeout(t); clearTimeout(f); }; }, []); return [ref, shown, forced]; } function Reveal({ children, delay = 0, style = {} }) { const [ref, shown, forced] = useReveal(); const anim = forced ? { opacity: 1, transform: "none", transition: "none" } : { opacity: shown ? 1 : 0, transform: shown ? "none" : "translateY(16px)", transition: `opacity .6s var(--ease) ${delay}ms, transform .6s var(--ease) ${delay}ms` }; return
{children}
; } // ---- Parallax (artistic, depth-layered scroll movement) ------------------ // Lightweight, registry-driven: one shared scroll/resize listener (rAF-throttled) // drives every registered layer, so adding parallax anywhere costs ~nothing extra. const _parallaxRegistry = []; let _parallaxRAF = null; const _prefersReducedMotion = () => window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; function _parallaxTick() { _parallaxRAF = null; const vh = window.innerHeight || 800; for (const item of _parallaxRegistry) { const el = item.ref.current; if (!el) continue; const rect = el.getBoundingClientRect(); // progress: ~ -1 (entering bottom) .. 0 (centered) .. 1 (exiting top) const center = rect.top + rect.height / 2; const span = vh / 2 + rect.height / 2; const progress = span > 0 ? (vh / 2 - center) / span : 0; const shift = progress * item.speed; el.style.transform = (item.base ? item.base + " " : "") + "translate3d(0," + shift.toFixed(2) + "px,0)"; } } function _scheduleParallax() { if (_parallaxRAF == null) _parallaxRAF = requestAnimationFrame(_parallaxTick); } let _parallaxBound = false; function _ensureParallaxBound() { if (_parallaxBound) return; _parallaxBound = true; window.addEventListener("scroll", _scheduleParallax, { passive: true }); window.addEventListener("resize", _scheduleParallax); _scheduleParallax(); } // speed = max px of drift as the layer transits the viewport (try 14-50). // baseTransform = a static transform to preserve (e.g. centering) alongside the drift. function ParallaxLayer({ children, speed = 24, baseTransform = "", style = {}, className, ...rest }) { const ref = useRef(null); useEffect(() => { if (_prefersReducedMotion()) return; _ensureParallaxBound(); const item = { ref, speed, base: baseTransform }; _parallaxRegistry.push(item); _scheduleParallax(); return () => { const i = _parallaxRegistry.indexOf(item); if (i >= 0) _parallaxRegistry.splice(i, 1); }; }, [speed, baseTransform]); return
{children}
; } // ---- Wordmark lockup (crest + Kingdom / School of Ministry) -------------- // One full-color logo; when `dark` (over a dark hero / transparent nav) it is // whitened with a CSS filter, and shows its true navy/blue on a solid nav. function Wordmark({ dark = false, crestHeight = 40, href = "index.html", onClick }) { const r = window.__resources || {}; const crest = dark ? (r.crestWhite || "assets/logo-crest-white.png") : (r.crestNavy || "assets/logo-crest.png"); const word = dark ? (r.wordWhite || "assets/logo-wordmark-white.png") : (r.wordNavy || "assets/logo-wordmark.png"); return ( Kingdom School of Ministry crest Kingdom School of Ministry ); } Object.assign(window, { Icon, Eyebrow, GoldRule, Pill, Button, Container, SectionHead, Reveal, useReveal, Wordmark, ParallaxLayer });