/* 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 = '';
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 (
);
}
Object.assign(window, { Icon, Eyebrow, GoldRule, Pill, Button, Container, SectionHead, Reveal, useReveal, Wordmark, ParallaxLayer });