// Testimonials (3D cards on scroll), FAQ, final CTA, Footer
// Voices: quotes are real and anonymous. The avatars are illustrated characters, not photos of the clients.
const VOICES = [
{ q: "The first useful version was in our supervisors' hands within weeks, not months.", n: 'Operations lead', r: 'SME client', img: '/uploads/voice_avatar_1.webp', badge: 'Weeks, not months', bl: 'Time to first version', bg: 'var(--accent)' },
{ q: 'They understood why our Excel file existed before trying to replace it.', n: 'Manager', r: 'SME client', img: '/uploads/voice_avatar_2.webp', badge: 'Excel → system', bl: 'Workflow first', bg: '#1d1d1d' },
{ q: 'The dashboard finally gave management and the floor the same numbers.', n: 'Director', r: 'SME client', img: '/uploads/voice_avatar_3.webp', badge: 'One set of numbers', bl: 'Management + team', bg: '#e6dfd1' },
];
function Voices() {
const reduced = useReducedMotion();
const sectionRef = React.useRef(null);
const stickyRef = React.useRef(null);
const cardRefs = React.useRef([]);
const n = VOICES.length;
useScrollFrame(() => {
const section = sectionRef.current;
const sticky = stickyRef.current;
if (!section || !sticky) return;
if (reduced) {
cardRefs.current.forEach((c) => c && c.removeAttribute('style'));
return;
}
const p = pinProgress(section);
// Title fades out over the first slice, then each card swings in and out.
sticky.style.setProperty('--t', clamp01(p * n * 3).toFixed(3));
const span = 1 / n;
cardRefs.current.forEach((c, i) => {
if (!c) return;
const local = (p - i * span) / span; // 0..1 inside this card's window
let ry = 90;
let o = 0;
if (local > 0 && local < 1) {
if (local < 0.3) {
const k = local / 0.3;
ry = 90 * (1 - k);
o = k;
} else if (local < 0.75 || i === n - 1) {
ry = 0;
o = 1;
} else {
const k = (local - 0.75) / 0.25;
ry = -90 * k;
o = 1 - k;
}
} else if (local >= 1 && i === n - 1) {
ry = 0;
o = 1;
}
c.style.setProperty('--ry', `${ry.toFixed(2)}deg`);
c.style.setProperty('--o', o.toFixed(3));
c.style.visibility = o < 0.01 ? 'hidden' : 'visible';
});
}, [reduced]);
return (
);
}
const FAQS = [
{ q: 'Which industries do you work with?', a: 'Businesses of any industry. If your team runs on spreadsheets, chat groups and paper forms, we can turn that workflow into one system your people actually use.' },
{ q: 'How does the instant quote work?', a: 'Our online estimator asks about your company and project, follows up with 5-8 questions, verifies your email with a 6-digit code, then sends an itemised quotation and a project proposal as PDFs. It is free and takes about 5 minutes.' },
{ q: 'Can you replace our Excel files gradually?', a: 'Yes. We usually start with one painful spreadsheet, turn it into a small internal system, then connect the next workflow once the team is comfortable.' },
{ q: 'Do we need technical staff to use what you build?', a: 'No. Every system ships with training and handover. We design for front-line staff, admin teams and management, not just technical users.' },
{ q: "What if we don't know what to automate first?", a: 'We map the workflow, review the files and reports you already use, then rank the first few systems by business impact and rollout difficulty.' },
{ q: 'Can you work with our existing tools?', a: 'Almost certainly. We integrate with accounting, inventory, barcode workflows, WhatsApp Business, existing ERP systems and most modern SaaS tools.' },
{ q: 'What if the project takes longer than expected?', a: 'We will tell you before continuing if the scope exceeds the estimate.' },
{ q: 'Will there be surprise charges?', a: 'No. There is no extra work or extra charge without your prior approval.' },
{ q: 'Where are you based and who actually does the work?', a: 'Kuala Lumpur. Senior builders work directly with your team from discovery through launch.' },
];
function FAQ() {
const [open, setOpen] = React.useState(0);
return (
);
}
const CTA_WORDS = ["Let's", 'build', 'your', 'next', 'idea'];
const CTA_PILL_AFTER = 2;
// Closing call to action: words rise out of their masks, the pill pops in and its sun knob slides across and lights up, and
// an accent "horizon" arc rises behind the buttons as the section scrolls into view.
function CTA() {
const reduced = useReducedMotion();
const ref = React.useRef(null);
useScrollFrame(() => {
const el = ref.current;
if (!el) return;
if (reduced) {
el.style.removeProperty('--e');
return;
}
const r = el.getBoundingClientRect();
const vh = window.innerHeight;
el.style.setProperty('--e', clamp01((vh - r.top) / (vh * 0.85)).toFixed(4));
}, [reduced]);
return (