/* =========================================================
Shadowbiz — core.jsx
Mark (Option C: solid overlap + shadow), Wordmark, icons,
Button, reveal hooks. Exports to window for other files.
========================================================= */
const { useState, useEffect, useRef, useCallback } = React;
/* ---- Option C mark: two rounded panels, overlap shows a darker shadow ---- */
function Mark({ size = 40, variant = "light", animate = false, idle = false }) {
// variant picks the back/front/overlap tones
const sets = {
light: { back: "#1d3a2e", front: "#84cc16", ovr: "#2e5016" }, // on cream
dark: { back: "#f5f1e8", front: "#84cc16", ovr: "#65a30d" }, // on ink/teal
lime: { back: "#1d3a2e", front: "#f5f1e8", ovr: "#2e4a3e" }, // on lime
mono: { back: "#1d3a2e", front: "#1d3a2e", ovr: "#1d3a2e" },
};
const c = sets[variant] || sets.light;
const cid = "ovr" + useRef(Math.random().toString(36).slice(2)).current;
return (
);
}
/* ---- wordmark ---- */
function Wordmark({ size = 21, tone = "light", className = "" }) {
const shadow = tone === "dark" ? "var(--cream)" : "var(--ink)";
const biz = tone === "dark" ? "var(--lime)" : "var(--lime-deep)";
return (
shadowbiz
);
}
function Brand({ tone = "light", markSize = 32, wordSize = 21, animate = false }) {
return (
);
}
/* ---- line icons — Lucide-flavoured, 1.75 stroke ---- */
function Icon({ name, size = 22, stroke = 1.75 }) {
const P = { width: size, height: size, viewBox: "0 0 24 24", fill: "none",
stroke: "currentColor", strokeWidth: stroke, strokeLinecap: "round", strokeLinejoin: "round" };
const paths = {
ear: <>>,
target: <>>,
blocks: <>>,
rocket: <>>,
compass: <>>,
layers: <>>,
spark: <>>,
mail: <>>,
phone: <>>,
globe: <>>,
arrow: <>>,
check: <>>,
pin: <>>,
clock: <>>,
school: <>>,
bag: <>>,
quote: <>>,
};
return ;
}
/* ---- button ---- */
function Btn({ kind = "primary", children, href, withArrow = false, className = "", onClick, type }) {
const cls = `btn btn-${kind} ${className}`;
const inner = <>{children}{withArrow && }>;
if (href) return {inner};
return ;
}
/* ---- reveal-on-scroll (synchronous scroll handler; rAF-free so it works
even when the tab is backgrounded / rAF is throttled) ---- */
function useReveal() {
useEffect(() => {
let els = Array.from(document.querySelectorAll(".reveal"));
const check = () => {
const h = window.innerHeight || 800;
els = els.filter(e => {
if (e.getBoundingClientRect().top < h * 0.92) { e.classList.add("in"); return false; }
return true;
});
if (!els.length) detach();
};
const detach = () => {
window.removeEventListener("scroll", check);
window.removeEventListener("resize", check);
};
check();
window.addEventListener("scroll", check, { passive: true });
window.addEventListener("resize", check, { passive: true });
// safety: never leave content hidden — force instant (bypass any frozen transition)
const safety = setTimeout(() => {
els.forEach(e => { e.style.transition = "none"; e.classList.add("in"); });
}, 1800);
return () => { detach(); clearTimeout(safety); };
});
}
/* ---- scroll position (for nav) ---- */
function useScrolled(at = 40) {
const [s, setS] = useState(false);
useEffect(() => {
const on = () => setS(window.scrollY > at);
on(); window.addEventListener("scroll", on, { passive: true });
return () => window.removeEventListener("scroll", on);
}, [at]);
return s;
}
Object.assign(window, { Mark, Wordmark, Brand, Icon, Btn, useReveal, useScrolled,
React, useState, useEffect, useRef, useCallback });