diff --git a/.claude/settings.local.json b/.claude/settings.local.json index a8ccf0f..3ab194d 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -30,7 +30,8 @@ "Bash(\"/Volumes/SSD/Projects/Clients/Axil Accountants/eslint.config.mjs\":*)", "Bash(node_modules/.bin/eslint:*)", "Bash(node --input-type=module:*)", - "Bash(python3:*)" + "Bash(python3:*)", + "Bash(ls:*)" ] } } diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index bb98d94..7ec2cc1 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -1,4 +1,5 @@ import Link from 'next/link'; +import Image from 'next/image'; import { TextHoverEffect, FooterBackgroundGradient } from '@/components/ui/TextHoverEffect'; const SERVICES = [ @@ -54,7 +55,7 @@ export function Footer() { {/* TextHoverEffect brand name */} -
+
@@ -63,28 +64,14 @@ export function Footer() {
{/* Brand column */}
- {/* SVG logo — light version for dark background */} -
- -
-
- - AXIL - - - ACCOUNTANTS - -
-
-
+
+ Axil Accountants

@@ -125,10 +112,23 @@ export function Footer() {

{/* Bottom bar */} -
+

© {new Date().getFullYear()} Axil Accountants Ltd. All rights reserved.

+ +

+ Developed by{' '} + + AImpress LTD + +

+

ICAEW Member · ACCA Certified · Registered in England & Wales

diff --git a/src/components/sections/home/FinalCTASection.tsx b/src/components/sections/home/FinalCTASection.tsx index 988bdba..e84ea78 100644 --- a/src/components/sections/home/FinalCTASection.tsx +++ b/src/components/sections/home/FinalCTASection.tsx @@ -1,5 +1,6 @@ import { FadeIn } from '@/components/ui/FadeIn'; import { Button } from '@/components/ui/Button'; +import { BeamButton } from '@/components/ui/BeamButton'; import { CheckCircleIcon } from '@/components/ui/icons'; const REASSURANCES = [ @@ -35,13 +36,9 @@ export function FinalCTASection() {

- + + diff --git a/src/components/sections/home/SolutionSection.tsx b/src/components/sections/home/SolutionSection.tsx index bafdb75..379a5fc 100644 --- a/src/components/sections/home/SolutionSection.tsx +++ b/src/components/sections/home/SolutionSection.tsx @@ -1,3 +1,6 @@ +'use client'; + +import { useState } from 'react'; import { FadeIn } from '@/components/ui/FadeIn'; import { CheckCircleIcon } from '@/components/ui/icons'; @@ -26,11 +29,32 @@ const CHECKLIST = [ 'Real-time financial dashboard', ]; +// Per-card: stack position + whether to show grayscale overlay in idle state +const STACK = [ + { + pos: '', + hover: 'hover:-translate-y-10', + idle: "before:absolute before:inset-0 before:z-10 before:rounded-xl before:content-[''] before:bg-bg/50 before:transition-opacity before:duration-500 grayscale", + }, + { + pos: 'translate-x-20 translate-y-12', + hover: 'hover:-translate-y-2', + idle: "before:absolute before:inset-0 before:z-10 before:rounded-xl before:content-[''] before:bg-bg/50 before:transition-opacity before:duration-500 grayscale", + }, + { + pos: 'translate-x-40 translate-y-24', + hover: 'hover:translate-y-6', + idle: '', + }, +]; + export function SolutionSection() { + const [active, setActive] = useState(null); + return (
-
+
{/* Left — text */}

@@ -56,22 +80,47 @@ export function SolutionSection() { - {/* Right — numbered features */} - -

- {FEATURES.map((f) => ( -
- - {f.number} - -
-

- {f.title} -

-

{f.body}

+ {/* Right — stacked feature cards */} + +
+ {FEATURES.map((f, i) => { + const isActive = active === i; + const isDimmed = active !== null && !isActive; + const { pos, hover, idle } = STACK[i]; + + return ( +
setActive(isActive ? null : i)} + className={[ + '[grid-area:stack]', + pos, + 'relative flex h-56 w-[30rem] cursor-pointer flex-col justify-between rounded-xl border px-8 py-6 transition-all duration-500 select-none', + isActive + ? 'border-emerald/30 z-50 skew-y-0 bg-white shadow-2xl' + : [ + '-skew-y-[8deg] border-black/7 bg-white/90', + hover, + "after:from-bg after:absolute after:top-[-5%] after:-right-1 after:h-[110%] after:w-80 after:bg-gradient-to-l after:to-transparent after:content-['']", + idle, + ].join(' '), + isDimmed ? 'opacity-40' : '', + ] + .join(' ') + .trim()} + > +
+ + {f.number} + +

+ {f.title} +

+
+

{f.body}

-
- ))} + ); + })}
diff --git a/src/components/ui/BeamButton.tsx b/src/components/ui/BeamButton.tsx new file mode 100644 index 0000000..c89e825 --- /dev/null +++ b/src/components/ui/BeamButton.tsx @@ -0,0 +1,144 @@ +'use client'; + +import { useEffect, useId, useRef, useState } from 'react'; +import { motion } from 'framer-motion'; +import { ArrowRightIcon } from './icons'; +import { Spinner } from './Spinner'; + +type Size = 'sm' | 'md' | 'lg'; +type Variant = 'dark' | 'light'; + +interface BeamButtonProps extends React.ButtonHTMLAttributes { + size?: Size; + variant?: Variant; + loading?: boolean; + trailingArrow?: boolean; + leadingIcon?: React.ReactNode; +} + +const HEIGHTS: Record = { sm: 36, md: 44, lg: 56 }; +const PADDINGS: Record = { + sm: 'px-4 text-sm', + md: 'px-6 text-base', + lg: 'px-8 text-lg', +}; +const GAPS: Record = { sm: 'gap-1.5', md: 'gap-2', lg: 'gap-2.5' }; + +const VARIANTS: Record = { + dark: { + bg: 'bg-charcoal hover:bg-charcoal/85', + text: 'text-white', + border: 'rgba(255,255,255,0.1)', + }, + light: { + bg: 'bg-white hover:bg-emerald-mist', + text: 'text-charcoal', + border: 'rgba(22,37,32,0.1)', + }, +}; + +export function BeamButton({ + children, + size = 'md', + variant = 'dark', + loading = false, + trailingArrow = false, + leadingIcon, + className = '', + disabled, + ...props +}: BeamButtonProps) { + const uid = useId().replace(/:/g, ''); + const ref = useRef(null); + const [w, setW] = useState(180); + + useEffect(() => { + const el = ref.current; + if (!el) return; + const ro = new ResizeObserver(([e]) => setW(Math.round(e.contentRect.width))); + ro.observe(el); + return () => ro.disconnect(); + }, []); + + const h = HEIGHTS[size]; + // Perimeter of pill: two straight edges + one full circle (r = h/2) + const perimeter = 2 * (w - h) + Math.PI * h; + const beamLen = Math.min(perimeter * 0.28, 90); + const { bg, text, border } = VARIANTS[variant]; + const arrowSize = size === 'sm' ? 14 : size === 'lg' ? 20 : 16; + + return ( + + ); +} diff --git a/src/components/ui/TextHoverEffect.tsx b/src/components/ui/TextHoverEffect.tsx index d65126f..98d7734 100644 --- a/src/components/ui/TextHoverEffect.tsx +++ b/src/components/ui/TextHoverEffect.tsx @@ -20,46 +20,36 @@ export const TextHoverEffect = ({ const [maskPosition, setMaskPosition] = useState({ cx: '50%', cy: '50%' }); useEffect(() => { - if (svgRef.current && cursor.x !== null && cursor.y !== null) { + if (svgRef.current) { const svgRect = svgRef.current.getBoundingClientRect(); const cxPercentage = ((cursor.x - svgRect.left) / svgRect.width) * 100; const cyPercentage = ((cursor.y - svgRect.top) / svgRect.height) * 100; - setMaskPosition({ - cx: `${cxPercentage}%`, - cy: `${cyPercentage}%`, - }); + setMaskPosition({ cx: `${cxPercentage}%`, cy: `${cyPercentage}%` }); } }, [cursor]); + // First word → blue (#1B9AD6), remaining → emerald (#3CC68A) — matches logo + const [firstWord, ...rest] = text.split(' '); + const secondWord = rest.join(' '); + return ( setHovered(true)} onMouseLeave={() => setHovered(false)} onMouseMove={(e) => setCursor({ x: e.clientX, y: e.clientY })} - className={cn('cursor-pointer uppercase select-none', className)} + className={cn('cursor-pointer select-none', className)} > - - {hovered && ( - <> - - - - - - )} - - {/* @ts-expect-error — motion.radialGradient is valid framer-motion SVG element */} - {/* Stroke outline — visible on hover */} + {/* Base outlines — always visible at low opacity, correct brand colors */} - {text} + + {firstWord}{' '} + + + {secondWord} + {/* Animated draw-on stroke */} - {text} + {firstWord} + {secondWord} - {/* Colour reveal on mouse move */} + {/* Hover fill reveal under radial cursor mask */} - {text} + + {firstWord}{' '} + + + {secondWord} + ); @@ -124,7 +143,7 @@ export const FooterBackgroundGradient = () => ( className="absolute inset-0 z-0" style={{ background: - 'radial-gradient(125% 125% at 50% 10%, rgba(22,37,32,0.6) 50%, rgba(27,154,214,0.15) 100%)', + 'radial-gradient(110% 110% at 50% 0%, rgba(22,37,32,0.8) 40%, rgba(27,154,214,0.1) 100%)', }} /> );