started redesign
@ -7,10 +7,12 @@ import { ArtistsSection } from "@/components/artists-section"
|
||||
import { ServicesSection } from "@/components/services-section"
|
||||
import { ContactSection } from "@/components/contact-section"
|
||||
import { Footer } from "@/components/footer"
|
||||
import { BackgroundStrata } from "@/components/background-strata"
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<LenisProvider>
|
||||
<main className="min-h-screen">
|
||||
<main className="relative min-h-screen bg-[#0c0907]">
|
||||
<BackgroundStrata />
|
||||
<ScrollProgress />
|
||||
<ScrollToSection />
|
||||
<Navigation />
|
||||
|
||||
@ -1,257 +1,311 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { motion, AnimatePresence, useMotionValue, useTransform } from "framer-motion"
|
||||
|
||||
import { useFeatureFlag } from "@/components/feature-flags-provider"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { artists as staticArtists } from "@/data/artists"
|
||||
import { useActiveArtists } from "@/hooks/use-artists"
|
||||
import type { PublicArtist } from "@/types/database"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
type ArtistGridSet = {
|
||||
key: string
|
||||
items: PublicArtist[]
|
||||
}
|
||||
|
||||
const GRID_SIZE = 16
|
||||
const GRID_INTERVAL = 12000
|
||||
|
||||
export function ArtistsSection() {
|
||||
// Fetch artists from database
|
||||
const { data: dbArtistsData, isLoading, error } = useActiveArtists()
|
||||
const { data: dbArtistsData, isLoading, error } = useActiveArtists()
|
||||
|
||||
// Merge static and database data
|
||||
const artists = useMemo(() => {
|
||||
// If still loading or error, use static data
|
||||
if (isLoading || error || !dbArtistsData) {
|
||||
return staticArtists
|
||||
const artists = useMemo(() => {
|
||||
if (isLoading || error || !dbArtistsData) {
|
||||
return staticArtists
|
||||
}
|
||||
|
||||
return staticArtists.map((staticArtist) => {
|
||||
const dbArtist = dbArtistsData.artists.find(
|
||||
(db) => db.slug === staticArtist.slug || db.name === staticArtist.name
|
||||
)
|
||||
|
||||
if (dbArtist && dbArtist.portfolioImages.length > 0) {
|
||||
return {
|
||||
...staticArtist,
|
||||
workImages: dbArtist.portfolioImages.map((img) => img.url),
|
||||
}
|
||||
}
|
||||
|
||||
// Merge: use database portfolio images, keep static metadata
|
||||
return staticArtists.map(staticArtist => {
|
||||
const dbArtist = dbArtistsData.artists.find(
|
||||
(db) => db.slug === staticArtist.slug || db.name === staticArtist.name
|
||||
)
|
||||
return staticArtist
|
||||
})
|
||||
}, [dbArtistsData, error, isLoading])
|
||||
|
||||
// If found in database, use its portfolio images
|
||||
if (dbArtist && dbArtist.portfolioImages.length > 0) {
|
||||
return {
|
||||
...staticArtist,
|
||||
workImages: dbArtist.portfolioImages.map(img => img.url)
|
||||
const artistsRef = useRef(artists)
|
||||
const gridSetsRef = useRef<ArtistGridSet[]>([])
|
||||
const activeSetRef = useRef(0)
|
||||
|
||||
const [gridSets, setGridSets] = useState<ArtistGridSet[]>([])
|
||||
const [activeSetIndex, setActiveSetIndex] = useState(0)
|
||||
const [previousSetIndex, setPreviousSetIndex] = useState<number | null>(null)
|
||||
|
||||
artistsRef.current = artists
|
||||
gridSetsRef.current = gridSets
|
||||
activeSetRef.current = activeSetIndex
|
||||
|
||||
const shuffleArtists = useCallback((input: PublicArtist[]) => {
|
||||
const array = [...input]
|
||||
for (let i = array.length - 1; i > 0; i -= 1) {
|
||||
const j = Math.floor(Math.random() * (i + 1))
|
||||
;[array[i], array[j]] = [array[j], array[i]]
|
||||
}
|
||||
return array
|
||||
}, [])
|
||||
|
||||
const ensureGridCount = useCallback(
|
||||
(pool: PublicArtist[], chunk: PublicArtist[]) => {
|
||||
if (chunk.length >= GRID_SIZE) {
|
||||
return chunk.slice(0, GRID_SIZE)
|
||||
}
|
||||
const topUpSource = shuffleArtists(pool)
|
||||
const needed = GRID_SIZE - chunk.length
|
||||
return [...chunk, ...topUpSource.slice(0, needed)]
|
||||
},
|
||||
[shuffleArtists]
|
||||
)
|
||||
|
||||
const createKey = () => Math.random().toString(36).slice(2)
|
||||
|
||||
const regenerateSets = useCallback(() => {
|
||||
const pool = artistsRef.current
|
||||
if (pool.length === 0) {
|
||||
setGridSets([])
|
||||
setActiveSetIndex(0)
|
||||
setPreviousSetIndex(null)
|
||||
return
|
||||
}
|
||||
|
||||
const shuffled = shuffleArtists(pool)
|
||||
const batches: ArtistGridSet[] = []
|
||||
|
||||
for (let i = 0; i < shuffled.length; i += GRID_SIZE) {
|
||||
const slice = ensureGridCount(pool, shuffled.slice(i, i + GRID_SIZE))
|
||||
batches.push({ key: `${createKey()}-${i}`, items: slice })
|
||||
}
|
||||
|
||||
if (batches.length === 1) {
|
||||
const alternate = ensureGridCount(pool, shuffleArtists(pool))
|
||||
batches.push({ key: `${createKey()}-alt`, items: alternate })
|
||||
}
|
||||
|
||||
setGridSets(batches)
|
||||
setActiveSetIndex(0)
|
||||
setPreviousSetIndex(null)
|
||||
}, [ensureGridCount, shuffleArtists])
|
||||
|
||||
useEffect(() => {
|
||||
regenerateSets()
|
||||
}, [artists, regenerateSets])
|
||||
|
||||
const advanceSet = useCallback(() => {
|
||||
if (gridSetsRef.current.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
setPreviousSetIndex(activeSetRef.current)
|
||||
setActiveSetIndex((prev) => {
|
||||
const next = prev + 1
|
||||
if (next >= gridSetsRef.current.length) {
|
||||
regenerateSets()
|
||||
return 0
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [regenerateSets])
|
||||
|
||||
useEffect(() => {
|
||||
if (gridSets.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const interval = window.setInterval(() => {
|
||||
advanceSet()
|
||||
}, GRID_INTERVAL)
|
||||
|
||||
return () => window.clearInterval(interval)
|
||||
}, [advanceSet, gridSets.length])
|
||||
|
||||
const displayIndices = useMemo(() => {
|
||||
const indices = new Set<number>()
|
||||
indices.add(activeSetIndex)
|
||||
if (previousSetIndex !== null && previousSetIndex !== activeSetIndex) {
|
||||
indices.add(previousSetIndex)
|
||||
}
|
||||
return Array.from(indices)
|
||||
}, [activeSetIndex, previousSetIndex])
|
||||
|
||||
const getArtistImage = (artist: PublicArtist) => {
|
||||
const candidate = (artist as any).faceImage || artist.workImages?.[0]
|
||||
if (candidate) {
|
||||
return candidate
|
||||
}
|
||||
return "/placeholder.svg"
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="artists" className="relative isolate overflow-hidden pb-24 pt-24">
|
||||
<div className="pointer-events-none absolute inset-0 -z-10">
|
||||
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(14,11,9,0)_0%,rgba(14,11,9,0.85)_20%,rgba(14,11,9,0.92)_55%,rgba(14,11,9,0.98)_100%)]" />
|
||||
<div
|
||||
className="absolute -left-16 top-[8%] h-[480px] w-[420px] rotate-[-8deg] rounded-[36px] opacity-40 blur-[1px]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"image-set(url('/assets/liberty/mural-portrait-sun.avif') type('image/avif'), url('/assets/liberty/mural-portrait-sun.webp') type('image/webp'))",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute -right-24 top-[35%] hidden h-[540px] w-[420px] rotate-[6deg] rounded-[36px] opacity-30 lg:block"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"image-set(url('/assets/liberty/mural-orange-wall.avif') type('image/avif'), url('/assets/liberty/mural-orange-wall.webp') type('image/webp'))",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_20%,rgba(255,255,255,0.08),transparent_55%)]" />
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto flex max-w-6xl flex-col gap-16 px-6 lg:px-10 xl:flex-row">
|
||||
<div className="flex-1 space-y-10 text-white">
|
||||
<div className="space-y-4">
|
||||
<span className="inline-flex items-center gap-3 text-[0.7rem] font-semibold uppercase tracking-[0.55em] text-white/55">
|
||||
<span className="h-px w-8 bg-white/35" /> Resident & Guest Artists
|
||||
</span>
|
||||
<h2 className="font-playfair text-4xl leading-[1.1] tracking-tight sm:text-5xl lg:text-[3.6rem]">
|
||||
A Collective of Story-Driven Tattoo Artists
|
||||
</h2>
|
||||
<p className="max-w-xl text-base leading-relaxed text-white/70 sm:text-lg">
|
||||
United Tattoo is home to cover-up virtuosos, illustrative explorers, anime specialists, and fine line minimalists.
|
||||
Every artist curates their chair with intention—offering custom narratives, flash experiments, and collaborative pieces
|
||||
that evolve with you.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-5 text-xs uppercase tracking-[0.32em] text-white/60 sm:grid-cols-2">
|
||||
<div className="rounded-3xl border border-white/10 bg-[rgba(255,255,255,0.05)] p-6">
|
||||
<p className="text-[0.65rem] font-semibold text-white/55">What to Expect</p>
|
||||
<p className="mt-3 text-sm tracking-[0.28em] text-white">Consultation-first Process</p>
|
||||
<p className="mt-3 text-[0.68rem] leading-relaxed tracking-[0.26em] text-white/45">
|
||||
Artist pairing • Mood-boards • Aftercare guides • CalDAV-synced scheduling
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-3xl border border-white/10 bg-[rgba(255,255,255,0.04)] p-6">
|
||||
<p className="text-[0.65rem] font-semibold text-white/55">Specialties</p>
|
||||
<p className="mt-3 text-sm tracking-[0.28em] text-white">Layered Stylescapes</p>
|
||||
<p className="mt-3 text-[0.68rem] leading-relaxed tracking-[0.26em] text-white/45">
|
||||
Black & grey realism • Neo-traditional color • Bold cover-ups • Fine line botanicals
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6 pt-2 sm:flex-row sm:items-center">
|
||||
<Button
|
||||
asChild
|
||||
className="group relative w-full overflow-hidden rounded-full bg-white/90 px-8 py-4 text-xs font-semibold uppercase tracking-[0.38em] text-[#1c1713] transition-all duration-300 hover:bg-white sm:w-auto"
|
||||
>
|
||||
<Link href="/book">
|
||||
Reserve with an Artist
|
||||
<span className="ml-3 inline-flex h-[1px] w-6 bg-[#1c1713] transition-all duration-300 group-hover:w-10" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
asChild
|
||||
className="w-full justify-start rounded-full border border-white/15 bg-white/5 px-6 py-4 text-xs font-semibold uppercase tracking-[0.32em] text-white/80 backdrop-blur sm:w-auto"
|
||||
>
|
||||
<Link href="/artists">View full roster</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative flex-1">
|
||||
<div className="relative overflow-hidden rounded-[36px] border border-white/12 bg-[rgba(12,10,8,0.82)] p-6 shadow-[0_45px_90px_-35px_rgba(0,0,0,0.75)]">
|
||||
<div className="pointer-events-none absolute inset-0 rounded-[36px] border border-white/[0.05]" aria-hidden="true" />
|
||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_30%_20%,rgba(255,255,255,0.12),transparent_55%)]" aria-hidden="true" />
|
||||
|
||||
<div className="relative min-h-[520px]">
|
||||
{displayIndices.map((index) => {
|
||||
const set = gridSets[index]
|
||||
if (!set) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
const isActive = index === activeSetIndex
|
||||
|
||||
// Fall back to static data
|
||||
return staticArtist
|
||||
})
|
||||
}, [dbArtistsData, isLoading, error])
|
||||
|
||||
// Minimal animation: fade-in only (no parallax)
|
||||
const [visibleCards, setVisibleCards] = useState<number[]>([])
|
||||
const [hoveredCard, setHoveredCard] = useState<number | null>(null)
|
||||
const [portfolioIndices, setPortfolioIndices] = useState<Record<number, number>>({})
|
||||
const sectionRef = useRef<HTMLElement>(null)
|
||||
const advancedNavAnimations = useFeatureFlag("ADVANCED_NAV_SCROLL_ANIMATIONS_ENABLED")
|
||||
const allArtistIndices = useMemo(() => Array.from({ length: artists.length }, (_, idx) => idx), [artists.length])
|
||||
|
||||
useEffect(() => {
|
||||
if (!advancedNavAnimations) {
|
||||
setVisibleCards(allArtistIndices)
|
||||
return
|
||||
}
|
||||
setVisibleCards([])
|
||||
}, [advancedNavAnimations, allArtistIndices])
|
||||
|
||||
useEffect(() => {
|
||||
if (!advancedNavAnimations) return
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const cardIndex = Number.parseInt(entry.target.getAttribute("data-index") || "0")
|
||||
setVisibleCards((prev) => [...new Set([...prev, cardIndex])])
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.2, rootMargin: "0px 0px -10% 0px" },
|
||||
)
|
||||
const cards = sectionRef.current?.querySelectorAll("[data-index]")
|
||||
cards?.forEach((card) => observer.observe(card))
|
||||
return () => observer.disconnect()
|
||||
}, [advancedNavAnimations])
|
||||
|
||||
const cardVisibilityClass = (index: number) => {
|
||||
if (!advancedNavAnimations) return "opacity-100 translate-y-0"
|
||||
return visibleCards.includes(index) ? "opacity-100 translate-y-0" : "opacity-0 translate-y-6"
|
||||
}
|
||||
|
||||
const cardTransitionDelay = (index: number) => {
|
||||
if (!advancedNavAnimations) return undefined
|
||||
return `${index * 40}ms`
|
||||
}
|
||||
|
||||
// Vary aspect ratio to create a subtle masonry rhythm
|
||||
const aspectFor = (i: number) => {
|
||||
const variants = ["aspect-[3/4]", "aspect-[4/5]", "aspect-square"]
|
||||
return variants[i % variants.length]
|
||||
}
|
||||
|
||||
// Handle hover to cycle through portfolio images
|
||||
const handleHoverStart = (artistIndex: number) => {
|
||||
setHoveredCard(artistIndex)
|
||||
const artist = artists[artistIndex]
|
||||
if (artist.workImages.length > 0) {
|
||||
setPortfolioIndices((prev) => {
|
||||
const currentIndex = prev[artistIndex] ?? 0
|
||||
const nextIndex = (currentIndex + 1) % artist.workImages.length
|
||||
return { ...prev, [artistIndex]: nextIndex }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleHoverEnd = () => {
|
||||
setHoveredCard(null)
|
||||
}
|
||||
|
||||
const getPortfolioImage = (artistIndex: number) => {
|
||||
const artist = artists[artistIndex]
|
||||
if (artist.workImages.length === 0) return null
|
||||
const imageIndex = portfolioIndices[artistIndex] ?? 0
|
||||
return artist.workImages[imageIndex]
|
||||
}
|
||||
|
||||
return (
|
||||
<section ref={sectionRef} id="artists" className="relative overflow-hidden bg-black">
|
||||
{/* Faint logo texture */}
|
||||
<div className="absolute inset-0 opacity-[0.03]">
|
||||
<img
|
||||
src="/united-logo-full.jpg"
|
||||
alt=""
|
||||
className="w-full h-full object-cover object-center scale-150 blur-[2px]"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/80 backdrop-blur-sm" />
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="relative z-10 py-14 px-6 lg:px-10">
|
||||
<div className="max-w-[1800px] mx-auto">
|
||||
<div className="grid lg:grid-cols-3 gap-10 items-end mb-10">
|
||||
<div className="lg:col-span-2">
|
||||
<h2 className="text-6xl lg:text-8xl font-bold tracking-tight mb-4 text-white">ARTISTS</h2>
|
||||
<p className="text-lg lg:text-xl text-gray-200/90 leading-relaxed max-w-2xl">
|
||||
Our exceptional team of tattoo artists, each bringing unique expertise and artistic vision to create your perfect
|
||||
tattoo.
|
||||
return (
|
||||
<div
|
||||
key={set.key}
|
||||
className={cn(
|
||||
"absolute inset-0 grid grid-cols-2 gap-3 sm:gap-4 md:gap-5 lg:grid-cols-4",
|
||||
"transition-opacity duration-[1300ms] ease-out",
|
||||
isActive ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"
|
||||
)}
|
||||
>
|
||||
{set.items.map((artist) => {
|
||||
const href = `/artists/${artist.slug}`
|
||||
const image = getArtistImage(artist)
|
||||
return (
|
||||
<Link
|
||||
key={`${set.key}-${artist.id}-${artist.slug}`}
|
||||
href={href}
|
||||
className="group relative flex flex-col overflow-hidden rounded-3xl border border-white/12 bg-white/[0.06] p-3 text-left transition-all duration-500 hover:-translate-y-1 hover:border-white/25 hover:bg-white/[0.1]"
|
||||
>
|
||||
<div className="relative aspect-[4/5] overflow-hidden rounded-2xl">
|
||||
<img
|
||||
src={image}
|
||||
alt={`${artist.name} portfolio sample`}
|
||||
className="h-full w-full object-cover transition-transform duration-700 group-hover:scale-[1.06]"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-[#0b0907] via-transparent to-transparent" />
|
||||
<div className="absolute right-4 top-4 h-10 w-10 rounded-full border border-white/20 bg-white/10 backdrop-blur">
|
||||
<span className="absolute inset-2 rounded-full border border-white/15" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 space-y-1">
|
||||
<p className="text-sm font-semibold uppercase tracking-[0.35em] text-white">
|
||||
{artist.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<Button
|
||||
asChild
|
||||
className="bg-white text-black hover:bg-gray-100 px-7 py-3 text-base font-medium tracking-wide shadow-sm rounded-md"
|
||||
>
|
||||
<Link href="/book">BOOK CONSULTATION</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[0.65rem] uppercase tracking-[0.32em] text-white/55">
|
||||
{(artist as any).specialty || "Tattoo Artist"}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Masonry grid */}
|
||||
<div className="relative z-10 px-6 lg:px-10 pb-24">
|
||||
<div className="max-w-[1800px] mx-auto">
|
||||
{/* columns-based masonry; tighter spacing and wider section */}
|
||||
<div className="columns-1 sm:columns-2 lg:columns-3 gap-4 lg:gap-5 [column-fill:_balance]">
|
||||
{artists.map((artist, i) => {
|
||||
const transitionDelay = cardTransitionDelay(i)
|
||||
const portfolioImage = getPortfolioImage(i)
|
||||
const isHovered = hoveredCard === i
|
||||
|
||||
return (
|
||||
<article
|
||||
key={artist.id}
|
||||
data-index={i}
|
||||
className={`group mb-4 break-inside-avoid transition-all duration-700 ${cardVisibilityClass(i)}`}
|
||||
style={transitionDelay ? { transitionDelay } : undefined}
|
||||
>
|
||||
<Link href={`/artists/${artist.slug}`}>
|
||||
<motion.div
|
||||
className={`relative w-full ${aspectFor(i)} overflow-hidden rounded-md border border-white/10 bg-black cursor-pointer`}
|
||||
onHoverStart={() => handleHoverStart(i)}
|
||||
onHoverEnd={handleHoverEnd}
|
||||
>
|
||||
{/* Base layer: artist portrait */}
|
||||
<div className="absolute inset-0 artist-image">
|
||||
<img
|
||||
src={artist.faceImage || "/placeholder.svg"}
|
||||
alt={`${artist.name} portrait`}
|
||||
className="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Wipe overlay: portfolio image with curved boundary */}
|
||||
<AnimatePresence>
|
||||
{isHovered && portfolioImage && (
|
||||
<>
|
||||
{/* SVG clipPath with pronounced wave */}
|
||||
<svg className="absolute w-0 h-0">
|
||||
<defs>
|
||||
<clipPath id={`wipe-curve-${i}`} clipPathUnits="objectBoundingBox">
|
||||
<motion.path
|
||||
initial={{
|
||||
d: "M 0,0 L 1,0 L 1,0 Q 0.75,0 0.5,0 Q 0.25,0 0,0 Z"
|
||||
}}
|
||||
animate={{
|
||||
d: "M 0,0 L 1,0 L 1,1.1 Q 0.75,1.02 0.5,1.1 Q 0.25,1.18 0,1.1 Z"
|
||||
}}
|
||||
exit={{
|
||||
d: "M 0,0 L 1,0 L 1,0 Q 0.75,0 0.5,0 Q 0.25,0 0,0 Z"
|
||||
}}
|
||||
transition={{ duration: 0.5, ease: "easeInOut" }}
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
{/* Portfolio image with curved clip */}
|
||||
<div
|
||||
className="absolute inset-0 z-10"
|
||||
style={{
|
||||
clipPath: `url(#wipe-curve-${i})`,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={portfolioImage}
|
||||
alt={`${artist.name} work`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Minimal footer - only name */}
|
||||
<div className="absolute bottom-0 left-0 right-0 z-20 bg-gradient-to-t from-black/80 via-black/20 to-transparent p-4">
|
||||
<h3 className="text-xl font-semibold tracking-tight text-white">{artist.name}</h3>
|
||||
<p className="text-xs font-medium text-white/80">{artist.specialty}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</Link>
|
||||
</article>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA Footer */}
|
||||
<div className="relative z-20 bg-black text-white py-20 px-6 lg:px-10">
|
||||
<div className="max-w-[1800px] mx-auto text-center">
|
||||
<h3 className="text-5xl lg:text-7xl font-bold tracking-tight mb-8">READY?</h3>
|
||||
<p className="text-xl text-white/70 mb-12 max-w-2xl mx-auto">
|
||||
Choose your artist and start your tattoo journey with United Tattoo.
|
||||
</p>
|
||||
<Button
|
||||
asChild
|
||||
className="bg-white text-black hover:bg-gray-100 hover:text-black px-12 py-6 text-xl font-medium tracking-wide shadow-lg border border-white rounded-md"
|
||||
>
|
||||
<Link href="/book">START NOW</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
<div className="relative z-10 mt-24 flex flex-col items-center gap-6 px-6 text-center text-white lg:px-10">
|
||||
<p className="uppercase tracking-[0.4em] text-white/45">Let's Plan Your Piece</p>
|
||||
<h3 className="font-playfair text-3xl leading-tight sm:text-4xl">
|
||||
Choose your artist, share your story, and build a tattoo ritual around intentional ink.
|
||||
</h3>
|
||||
<Button
|
||||
asChild
|
||||
className="rounded-full border border-white/20 bg-white text-sm font-semibold uppercase tracking-[0.32em] text-[#1c1713] shadow-[0_30px_60px_-35px_rgba(255,255,255,0.65)] transition-transform duration-300 hover:scale-[1.03]"
|
||||
>
|
||||
<Link href="/book" className="px-10 py-4">
|
||||
Start A Consultation
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
51
components/background-strata.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useRef } from "react"
|
||||
|
||||
import { useReducedMotion } from "@/hooks/use-parallax"
|
||||
|
||||
export function BackgroundStrata() {
|
||||
const layerRef = useRef<HTMLDivElement>(null)
|
||||
const reducedMotion = useReducedMotion()
|
||||
|
||||
useEffect(() => {
|
||||
if (reducedMotion) {
|
||||
return
|
||||
}
|
||||
|
||||
const target = layerRef.current
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
|
||||
let frame = 0
|
||||
const animate = () => {
|
||||
const offset = window.scrollY * 0.08
|
||||
target.style.transform = `translate3d(0, ${offset}px, 0)`
|
||||
frame = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
frame = requestAnimationFrame(animate)
|
||||
return () => cancelAnimationFrame(frame)
|
||||
}, [reducedMotion])
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[230vh] overflow-hidden">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_20%_0%,rgba(255,255,255,0.12),transparent_55%),radial-gradient(circle_at_80%_10%,rgba(255,255,255,0.08),transparent_52%)]" />
|
||||
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(14,11,9,0.92)_0%,rgba(14,11,9,0.75)_28%,rgba(10,8,7,0.35)_68%,transparent_100%)]" />
|
||||
<div
|
||||
ref={layerRef}
|
||||
className="h-full w-full scale-[1.04] transform-gpu transition-transform duration-1000 ease-out"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"image-set(url('/assets/liberty/background-dove-wash.avif') type('image/avif'), url('/assets/liberty/background-dove-wash.webp') type('image/webp'))",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center top",
|
||||
backgroundRepeat: "no-repeat",
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-x-0 bottom-0 h-[420px] bg-[linear-gradient(180deg,rgba(12,10,8,0.2)_0%,rgba(12,10,8,0.65)_45%,rgba(12,10,8,1)_98%)]" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -35,10 +35,10 @@ export function ContactSection() {
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="contact" className="min-h-screen bg-black relative overflow-hidden">
|
||||
<section id="contact" className="relative min-h-screen overflow-hidden bg-[#0c0907]">
|
||||
{/* Background logo - desktop only */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.03] bg-cover bg-center bg-no-repeat blur-sm hidden lg:block"
|
||||
className="hidden opacity-[0.05] blur-sm lg:block absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: "url('/united-logo-full.jpg')",
|
||||
transform: `translateY(${scrollY * 0.2}px)`,
|
||||
@ -46,22 +46,22 @@ export function ContactSection() {
|
||||
/>
|
||||
|
||||
{/* Mobile solid background */}
|
||||
<div className="absolute inset-0 bg-black lg:hidden"></div>
|
||||
<div className="absolute inset-0 bg-[#0c0907] lg:hidden"></div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row min-h-screen relative z-10">
|
||||
<div className="w-full lg:w-1/2 bg-black flex items-center justify-center p-8 lg:p-12 relative">
|
||||
<div className="relative z-10 flex min-h-screen flex-col lg:flex-row">
|
||||
<div className="relative flex w-full items-center justify-center bg-[#0f0b09] p-8 lg:w-1/2 lg:p-12">
|
||||
{/* Mobile background overlay to hide logo */}
|
||||
<div className="absolute inset-0 bg-black lg:bg-transparent"></div>
|
||||
<div className="w-full max-w-md relative z-10">
|
||||
<div className="absolute inset-0 bg-[#0f0b09]/95 lg:bg-transparent" />
|
||||
<div className="relative z-10 w-full max-w-md">
|
||||
<div className="mb-8">
|
||||
<h2 className="text-4xl font-bold text-white mb-2">Let's Talk</h2>
|
||||
<p className="text-gray-400">Ready to create something amazing?</p>
|
||||
<h2 className="mb-2 font-playfair text-4xl text-white">Let's Talk</h2>
|
||||
<p className="text-white/55">Ready to create something amazing?</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-white mb-2">
|
||||
<label htmlFor="name" className="mb-2 block text-sm font-medium uppercase tracking-[0.3em] text-white/70">
|
||||
Name
|
||||
</label>
|
||||
<Input
|
||||
@ -70,12 +70,12 @@ export function ContactSection() {
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="bg-white/10 border-white/20 text-white placeholder:text-gray-400 focus:border-white focus:bg-white/15 transition-all"
|
||||
className="border-white/15 bg-white/10 text-white placeholder:text-white/40 transition-all focus:border-white focus:bg-white/15"
|
||||
placeholder="Your name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="phone" className="block text-sm font-medium text-white mb-2">
|
||||
<label htmlFor="phone" className="mb-2 block text-sm font-medium uppercase tracking-[0.3em] text-white/70">
|
||||
Phone
|
||||
</label>
|
||||
<Input
|
||||
@ -84,14 +84,14 @@ export function ContactSection() {
|
||||
type="tel"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
className="bg-white/10 border-white/20 text-white placeholder:text-gray-400 focus:border-white focus:bg-white/15 transition-all"
|
||||
className="border-white/15 bg-white/10 text-white placeholder:text-white/40 transition-all focus:border-white focus:bg-white/15"
|
||||
placeholder="(555) 123-4567"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-white mb-2">
|
||||
<label htmlFor="email" className="mb-2 block text-sm font-medium uppercase tracking-[0.3em] text-white/70">
|
||||
Email
|
||||
</label>
|
||||
<Input
|
||||
@ -101,13 +101,13 @@ export function ContactSection() {
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="bg-white/10 border-white/20 text-white placeholder:text-gray-400 focus:border-white focus:bg-white/15 transition-all"
|
||||
className="border-white/15 bg-white/10 text-white placeholder:text-white/40 transition-all focus:border-white focus:bg-white/15"
|
||||
placeholder="your@email.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium text-white mb-2">
|
||||
<label htmlFor="message" className="mb-2 block text-sm font-medium uppercase tracking-[0.3em] text-white/70">
|
||||
Message
|
||||
</label>
|
||||
<Textarea
|
||||
@ -118,13 +118,13 @@ export function ContactSection() {
|
||||
onChange={handleChange}
|
||||
placeholder="Tell us about your tattoo idea..."
|
||||
required
|
||||
className="bg-white/10 border-white/20 text-white placeholder:text-gray-400 focus:border-white focus:bg-white/15 transition-all resize-none"
|
||||
className="resize-none border-white/15 bg-white/10 text-white placeholder:text-white/40 transition-all focus:border-white focus:bg-white/15"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-white text-black hover:bg-gray-100 py-3 text-base font-medium transition-all"
|
||||
className="w-full rounded-full border border-white/15 bg-white/90 py-3 text-xs font-semibold uppercase tracking-[0.32em] text-[#1c1713] transition-all hover:bg-white"
|
||||
>
|
||||
Send Message
|
||||
</Button>
|
||||
@ -132,10 +132,10 @@ export function ContactSection() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full lg:w-1/2 bg-gray-50 relative flex items-center justify-center">
|
||||
<div className="relative flex w-full items-center justify-center bg-[radial-gradient(circle_at_top_right,rgba(255,255,255,0.07),transparent_55%),linear-gradient(180deg,#1a1512_0%,#110d0a_100%)] lg:w-1/2">
|
||||
{/* Brand asset as decorative element */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-20 bg-cover bg-center bg-no-repeat"
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat opacity-[0.08]"
|
||||
style={{
|
||||
backgroundImage: "url('/united-logo-text.png')",
|
||||
transform: `translateY(${scrollY * -0.1}px)`,
|
||||
@ -144,14 +144,14 @@ export function ContactSection() {
|
||||
|
||||
<div className="relative z-10 p-12 text-center">
|
||||
<div className="mb-12">
|
||||
<h2 className="text-5xl font-bold text-black mb-4">UNITED</h2>
|
||||
<h3 className="text-3xl font-bold text-gray-600 mb-6">TATTOO</h3>
|
||||
<p className="text-gray-700 text-lg max-w-md mx-auto leading-relaxed">
|
||||
<h2 className="font-playfair text-5xl text-white">UNITED</h2>
|
||||
<h3 className="mt-2 font-playfair text-3xl text-white/70">TATTOO</h3>
|
||||
<p className="mx-auto mt-6 max-w-md text-base leading-relaxed text-white/65">
|
||||
Where artistry, culture, and custom tattoos meet. Located in Fountain, just minutes from Colorado Springs.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 max-w-sm mx-auto">
|
||||
<div className="mx-auto max-w-sm space-y-6">
|
||||
{[
|
||||
{
|
||||
icon: MapPin,
|
||||
@ -177,10 +177,12 @@ export function ContactSection() {
|
||||
const Icon = item.icon
|
||||
return (
|
||||
<div key={index} className="flex items-start space-x-4 text-left">
|
||||
<Icon className="w-5 h-5 text-black mt-1 flex-shrink-0" />
|
||||
<div className="mt-1 flex h-9 w-9 items-center justify-center rounded-full border border-white/15 bg-white/10">
|
||||
<Icon className="h-4 w-4 text-white/70" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-black font-medium text-sm">{item.title}</p>
|
||||
<p className="text-gray-600 text-sm">{item.content}</p>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.4em] text-white/60">{item.title}</p>
|
||||
<p className="mt-1 text-sm text-white/70">{item.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -28,23 +28,22 @@ export function Footer() {
|
||||
<>
|
||||
<Button
|
||||
onClick={scrollToTop}
|
||||
className={`fixed bottom-8 right-8 z-50 rounded-full w-12 h-12 p-0 bg-white text-black hover:bg-gray-100 shadow-lg transition-all duration-300 ${
|
||||
showScrollTop ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4 pointer-events-none"
|
||||
className={`fixed bottom-8 right-8 z-50 h-12 w-12 rounded-full border border-white/15 bg-white/90 p-0 text-[#1c1713] shadow-[0_30px_60px_-35px_rgba(255,255,255,0.65)] transition-all duration-300 hover:scale-[1.05] hover:bg-white ${
|
||||
showScrollTop ? "translate-y-0 opacity-100" : "pointer-events-none translate-y-4 opacity-0"
|
||||
}`}
|
||||
aria-label="Scroll to top"
|
||||
>
|
||||
<ArrowUp size={20} />
|
||||
</Button>
|
||||
|
||||
<footer className="bg-black text-white py-16 font-mono">
|
||||
<footer className="bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.09),transparent_55%),linear-gradient(180deg,#15100d_0%,#0c0907_100%)] py-16 text-white">
|
||||
<div className="container mx-auto px-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 items-start">
|
||||
<div className="grid grid-cols-1 items-start gap-10 md:grid-cols-12">
|
||||
<div className="md:col-span-3">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<span className="text-white">↳</span>
|
||||
<h4 className="text-white font-medium tracking-wide text-lg">SERVICES</h4>
|
||||
<div className="mb-6 flex items-center gap-2 text-xs uppercase tracking-[0.4em] text-white/55">
|
||||
<span className="inline-flex h-2 w-2 rounded-full bg-white/40" /> Services
|
||||
</div>
|
||||
<ul className="space-y-3 text-base">
|
||||
<ul className="space-y-3 text-sm text-white/65">
|
||||
{[
|
||||
{ name: "TRADITIONAL", count: "" },
|
||||
{ name: "REALISM", count: "" },
|
||||
@ -55,7 +54,7 @@ export function Footer() {
|
||||
{ name: "ANIME", count: "" },
|
||||
].map((service, index) => (
|
||||
<li key={index}>
|
||||
<Link href="/book" className="text-gray-400 hover:text-white transition-colors duration-200">
|
||||
<Link href="/book" className="transition-colors duration-200 hover:text-white">
|
||||
{service.name}
|
||||
{service.count && <span className="text-white ml-2">{service.count}</span>}
|
||||
</Link>
|
||||
@ -65,11 +64,10 @@ export function Footer() {
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-3">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<span className="text-white">↳</span>
|
||||
<h4 className="text-white font-medium tracking-wide text-lg">ARTISTS</h4>
|
||||
<div className="mb-6 flex items-center gap-2 text-xs uppercase tracking-[0.4em] text-white/55">
|
||||
<span className="inline-flex h-2 w-2 rounded-full bg-white/40" /> Artists
|
||||
</div>
|
||||
<ul className="space-y-3 text-base">
|
||||
<ul className="space-y-3 text-sm text-white/65">
|
||||
{[
|
||||
{ name: "CHRISTY_LUMBERG", count: "" },
|
||||
{ name: "STEVEN_SOLE", count: "" },
|
||||
@ -77,7 +75,7 @@ export function Footer() {
|
||||
{ name: "VIEW_ALL", count: "" },
|
||||
].map((artist, index) => (
|
||||
<li key={index}>
|
||||
<Link href="/artists" className="text-gray-400 hover:text-white transition-colors duration-200">
|
||||
<Link href="/artists" className="transition-colors duration-200 hover:text-white">
|
||||
{artist.name}
|
||||
{artist.count && <span className="text-white ml-2">{artist.count}</span>}
|
||||
</Link>
|
||||
@ -87,17 +85,13 @@ export function Footer() {
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-3">
|
||||
<div className="text-gray-500 text-sm leading-relaxed mb-4">
|
||||
© <span className="text-white underline">UNITED.TATTOO</span> LLC 2025
|
||||
<br />
|
||||
ALL RIGHTS RESERVED.
|
||||
<div className="mb-4 text-xs uppercase tracking-[0.4em] text-white/40">
|
||||
© <span className="text-white/80">UNITED.TATTOO</span> LLC 2025 — All Rights Reserved
|
||||
</div>
|
||||
<div className="text-gray-400 text-sm">
|
||||
5160 FONTAINE BLVD
|
||||
<br />
|
||||
FOUNTAIN, CO 80817
|
||||
<br />
|
||||
<Link href="tel:+17196989004" className="hover:text-white transition-colors">
|
||||
<div className="space-y-2 text-sm text-white/60">
|
||||
<p>5160 Fontaine Blvd</p>
|
||||
<p>Fountain, CO 80817</p>
|
||||
<Link href="tel:+17196989004" className="transition-colors duration-200 hover:text-white">
|
||||
(719) 698-9004
|
||||
</Link>
|
||||
</div>
|
||||
@ -106,15 +100,14 @@ export function Footer() {
|
||||
<div className="md:col-span-3 space-y-8">
|
||||
{/* Legal */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<span className="text-white">↳</span>
|
||||
<h4 className="text-white font-medium tracking-wide text-lg">LEGAL</h4>
|
||||
<div className="mb-4 flex items-center gap-2 text-xs uppercase tracking-[0.4em] text-white/55">
|
||||
<span className="inline-flex h-2 w-2 rounded-full bg-white/40" /> Legal
|
||||
</div>
|
||||
<ul className="space-y-2 text-base">
|
||||
<ul className="space-y-2 text-sm text-white/65">
|
||||
<li>
|
||||
<Link
|
||||
href="/aftercare"
|
||||
className="text-gray-400 hover:text-white transition-colors duration-200 underline"
|
||||
className="transition-colors duration-200 hover:text-white underline"
|
||||
>
|
||||
AFTERCARE
|
||||
</Link>
|
||||
@ -122,7 +115,7 @@ export function Footer() {
|
||||
<li>
|
||||
<Link
|
||||
href="/deposit"
|
||||
className="text-gray-400 hover:text-white transition-colors duration-200 underline"
|
||||
className="transition-colors duration-200 hover:text-white underline"
|
||||
>
|
||||
DEPOSIT POLICY
|
||||
</Link>
|
||||
@ -130,7 +123,7 @@ export function Footer() {
|
||||
<li>
|
||||
<Link
|
||||
href="/terms"
|
||||
className="text-gray-400 hover:text-white transition-colors duration-200 underline"
|
||||
className="transition-colors duration-200 hover:text-white underline"
|
||||
>
|
||||
TERMS OF SERVICE
|
||||
</Link>
|
||||
@ -138,7 +131,7 @@ export function Footer() {
|
||||
<li>
|
||||
<Link
|
||||
href="/privacy"
|
||||
className="text-gray-400 hover:text-white transition-colors duration-200 underline"
|
||||
className="transition-colors duration-200 hover:text-white underline"
|
||||
>
|
||||
PRIVACY POLICY
|
||||
</Link>
|
||||
@ -146,7 +139,7 @@ export function Footer() {
|
||||
<li>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-gray-400 hover:text-white transition-colors duration-200 underline"
|
||||
className="transition-colors duration-200 hover:text-white underline"
|
||||
>
|
||||
WAIVER
|
||||
</Link>
|
||||
@ -156,23 +149,37 @@ export function Footer() {
|
||||
|
||||
{/* Social */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<span className="text-white">↳</span>
|
||||
<h4 className="text-white font-medium tracking-wide text-lg">SOCIAL</h4>
|
||||
<div className="mb-4 flex items-center gap-2 text-xs uppercase tracking-[0.4em] text-white/55">
|
||||
<span className="inline-flex h-2 w-2 rounded-full bg-white/40" /> Social
|
||||
</div>
|
||||
<ul className="space-y-2 text-base">
|
||||
<ul className="space-y-2 text-sm text-white/65">
|
||||
<li>
|
||||
<Link href="https://www.instagram.com/unitedtattoo719" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white transition-colors duration-200 underline">
|
||||
<Link
|
||||
href="https://www.instagram.com/unitedtattoo719"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-white/65 underline transition-colors duration-200 hover:text-white"
|
||||
>
|
||||
INSTAGRAM
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="https://www.facebook.com/unitedtattoo719" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white transition-colors duration-200 underline">
|
||||
<Link
|
||||
href="https://www.facebook.com/unitedtattoo719"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-white/65 underline transition-colors duration-200 hover:text-white"
|
||||
>
|
||||
FACEBOOK
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="https://www.tiktok.com/@united.tattoo" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white transition-colors duration-200 underline">
|
||||
<Link
|
||||
href="https://www.tiktok.com/@united.tattoo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-white/65 underline transition-colors duration-200 hover:text-white"
|
||||
>
|
||||
TIKTOK
|
||||
</Link>
|
||||
</li>
|
||||
@ -181,13 +188,12 @@ export function Footer() {
|
||||
|
||||
{/* Contact */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<span className="text-white">↳</span>
|
||||
<h4 className="text-white font-medium tracking-wide text-lg">CONTACT</h4>
|
||||
<div className="mb-4 flex items-center gap-2 text-xs uppercase tracking-[0.4em] text-white/55">
|
||||
<span className="inline-flex h-2 w-2 rounded-full bg-white/40" /> Contact
|
||||
</div>
|
||||
<Link
|
||||
href="mailto:info@united-tattoo.com"
|
||||
className="text-gray-400 hover:text-white transition-colors duration-200 underline text-base"
|
||||
className="text-sm text-white/65 underline transition-colors duration-200 hover:text-white"
|
||||
>
|
||||
INFO@UNITED-TATTOO.COM
|
||||
</Link>
|
||||
@ -195,9 +201,9 @@ export function Footer() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-8 gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-gray-400"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-white"></div>
|
||||
<div className="mt-10 flex justify-end gap-2">
|
||||
<div className="h-2 w-2 rounded-full bg-white/25" />
|
||||
<div className="h-2 w-2 rounded-full bg-white/60" />
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import Image from "next/image"
|
||||
|
||||
import { useFeatureFlag } from "@/components/feature-flags-provider"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@ -11,80 +13,161 @@ export function HeroSection() {
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const advancedNavAnimations = useFeatureFlag("ADVANCED_NAV_SCROLL_ANIMATIONS_ENABLED")
|
||||
const reducedMotion = useReducedMotion()
|
||||
|
||||
// Use new parallax system with proper accessibility support
|
||||
|
||||
const parallax = useMultiLayerParallax(!advancedNavAnimations || reducedMotion)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setIsVisible(true), 300)
|
||||
const timer = setTimeout(() => setIsVisible(true), 240)
|
||||
return () => clearTimeout(timer)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<section
|
||||
id="home"
|
||||
className="min-h-screen flex items-center justify-center relative overflow-hidden"
|
||||
<section
|
||||
id="home"
|
||||
className="relative flex min-h-[110vh] items-center justify-center overflow-hidden px-6 pb-24 pt-32 sm:px-10 lg:pt-44"
|
||||
data-reduced-motion={reducedMotion}
|
||||
>
|
||||
{/* Background Layer - Slowest parallax */}
|
||||
<div
|
||||
ref={parallax.background.ref}
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat will-change-transform"
|
||||
className="pointer-events-none absolute inset-0 will-change-transform"
|
||||
style={{
|
||||
backgroundImage: "url(/united-logo-full.jpg)",
|
||||
backgroundImage:
|
||||
"image-set(url('/assets/liberty/hero-statue-collage.avif') type('image/avif'), url('/assets/liberty/hero-statue-collage.webp') type('image/webp'))",
|
||||
backgroundPosition: "center top",
|
||||
backgroundSize: "cover",
|
||||
mixBlendMode: "soft-light",
|
||||
opacity: 0.35,
|
||||
...parallax.background.style,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Midground Layer - Overlay with subtle parallax */}
|
||||
|
||||
<div
|
||||
ref={parallax.midground.ref}
|
||||
className="absolute inset-0 bg-black/70 will-change-transform"
|
||||
className="absolute inset-0 bg-[radial-gradient(circle_at_20%_10%,rgba(255,255,255,0.14),transparent_52%),linear-gradient(115deg,rgba(18,14,12,0.86)_18%,rgba(18,14,12,0.62)_48%,rgba(18,14,12,0)_100%)] will-change-transform"
|
||||
style={parallax.midground.style}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Foreground Layer - Content with slight counter-parallax */}
|
||||
<div
|
||||
ref={parallax.foreground.ref}
|
||||
className="relative z-10 text-center max-w-4xl px-8 will-change-transform"
|
||||
className="relative z-10 w-full will-change-transform"
|
||||
style={parallax.foreground.style}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"transition-all duration-1000",
|
||||
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
|
||||
)}
|
||||
>
|
||||
<h1 className="font-playfair text-5xl lg:text-7xl font-bold text-white mb-6 tracking-tight">
|
||||
UNITED TATTOO
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"transition-all duration-1000 delay-300",
|
||||
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
|
||||
)}
|
||||
>
|
||||
<p className="text-xl lg:text-2xl text-gray-200 mb-12 font-light leading-relaxed">
|
||||
Custom Tattoos in Fountain, Colorado
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"transition-all duration-1000 delay-500",
|
||||
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-gray-50 text-gray-900 hover:bg-gray-100 px-8 py-4 text-lg font-medium rounded-lg w-full sm:w-auto transition-colors"
|
||||
<div className="mx-auto flex max-w-6xl flex-col gap-14 lg:grid lg:grid-cols-[minmax(0,0.95fr)_minmax(0,1.1fr)] lg:items-center">
|
||||
<div
|
||||
className={cn(
|
||||
"relative space-y-8 text-left text-white transition-all duration-1000",
|
||||
isVisible ? "opacity-100 translate-y-0" : "translate-y-8 opacity-0"
|
||||
)}
|
||||
>
|
||||
Book Consultation
|
||||
</Button>
|
||||
<span className="inline-flex items-center gap-3 text-[0.7rem] font-semibold uppercase tracking-[0.6em] text-white/60">
|
||||
<span className="h-[1px] w-10 bg-white/40" /> Fountain, Colorado
|
||||
</span>
|
||||
<div className="space-y-4">
|
||||
<h1 className="font-playfair text-4xl leading-[1.1] tracking-tight sm:text-5xl lg:text-[4.2rem]">
|
||||
A Studio of Ritual, Craft, and Skin Stories
|
||||
</h1>
|
||||
<p className="max-w-xl text-base leading-relaxed text-white/70 sm:text-lg">
|
||||
United Tattoo is a sanctuary for custom work—where layered narratives, sculptural light, and precise linework
|
||||
merge into living art. Appointments are curated with intention, calm focus, and a love of experimentation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6 pt-6 md:flex-row md:items-center">
|
||||
<Button
|
||||
asChild
|
||||
className="group relative w-full overflow-hidden rounded-full bg-white/90 px-8 py-4 text-xs font-semibold uppercase tracking-[0.4em] text-[#1c1713] transition-all duration-300 hover:bg-white md:w-auto"
|
||||
>
|
||||
<Link href="/book">
|
||||
<span className="flex items-center gap-3">
|
||||
Secure Consultation
|
||||
<span className="h-[1px] w-6 bg-[#1c1713] transition-transform duration-300 group-hover:w-10" />
|
||||
</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
asChild
|
||||
className="w-full justify-start rounded-full border border-white/15 bg-white/5 px-6 py-4 text-xs font-semibold uppercase tracking-[0.32em] text-white/80 backdrop-blur md:w-auto"
|
||||
>
|
||||
<Link href="#artists">View the Artists</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 pt-4 text-xs uppercase tracking-[0.32em] text-white/55 sm:grid-cols-2">
|
||||
<div className="rounded-2xl border border-white/10 bg-[rgba(255,255,255,0.06)] p-5">
|
||||
<p className="text-[0.65rem] font-semibold text-white/60">Studio Rhythm</p>
|
||||
<p className="mt-3 text-sm tracking-[0.2em] text-white">Appointment-Only Sessions</p>
|
||||
<p className="mt-2 text-[0.68rem] leading-relaxed tracking-[0.3em] text-white/45">
|
||||
Design consultations • Flash drops • Artist residencies
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/10 bg-[rgba(255,255,255,0.04)] p-5">
|
||||
<p className="text-[0.65rem] font-semibold text-white/60">Focus</p>
|
||||
<p className="mt-3 text-sm tracking-[0.2em] text-white">Layered Custom Work</p>
|
||||
<p className="mt-2 text-[0.68rem] leading-relaxed tracking-[0.3em] text-white/45">
|
||||
Black & grey realism • Neo-traditional • Fine line botanical
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto flex w-full max-w-[520px] justify-center lg:justify-end">
|
||||
<div className="relative aspect-[4/5] w-full">
|
||||
<div className="absolute -inset-6 rounded-[32px] border border-white/12 bg-white/10 backdrop-blur-[2px]" aria-hidden="true" />
|
||||
<div className="absolute -inset-x-10 -top-10 h-[140px] rounded-full bg-[radial-gradient(circle,rgba(255,255,255,0.08),transparent_65%)]" aria-hidden="true" />
|
||||
<div className="absolute -right-10 top-16 hidden h-36 w-36 rounded-full border border-white/10 backdrop-blur-sm lg:block" aria-hidden="true">
|
||||
<div className="absolute inset-3 rounded-full border border-white/10" />
|
||||
</div>
|
||||
|
||||
<figure
|
||||
className={cn(
|
||||
"relative h-full w-full overflow-hidden rounded-[28px] border border-white/14 bg-black/40",
|
||||
isVisible ? "shadow-[0_45px_90px_-40px_rgba(0,0,0,0.9)]" : ""
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
src="/assets/liberty/hero-statue-collage.webp"
|
||||
alt="Sculptural collage with the Statue of Liberty and tattoo studio elements"
|
||||
fill
|
||||
sizes="(min-width: 1024px) 520px, 90vw"
|
||||
priority
|
||||
className="object-cover object-center"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-[#0b0907]/70 via-transparent to-transparent" />
|
||||
</figure>
|
||||
|
||||
<div className="absolute -bottom-10 left-[-18%] hidden w-[220px] rounded-[26px] border border-white/12 bg-white/8 p-4 text-[0.65rem] uppercase tracking-[0.32em] text-white/70 backdrop-blur-md lg:block">
|
||||
<p>United Tattoo Collective</p>
|
||||
<p className="mt-2 text-[0.55rem] leading-[1.8] text-white/45">
|
||||
Sculptural light. Ritual care. Art that endures.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<figure className="absolute -top-10 left-[-15%] hidden w-40 rotate-[-6deg] overflow-hidden rounded-3xl border border-white/15 bg-white/10 shadow-[0_35px_60px_-30px_rgba(0,0,0,0.7)] backdrop-blur md:block">
|
||||
<Image
|
||||
src="/assets/liberty/dove-tableau-close.webp"
|
||||
alt="Still life of studio textures with a dove"
|
||||
width={320}
|
||||
height={420}
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</figure>
|
||||
|
||||
<figure className="absolute -right-16 bottom-16 hidden w-32 rotate-[5deg] overflow-hidden rounded-3xl border border-white/15 bg-white/10 shadow-[0_25px_45px_-25px_rgba(0,0,0,0.7)] backdrop-blur sm:block">
|
||||
<Image
|
||||
src="/assets/liberty/palette-brush-liberty.webp"
|
||||
alt="Color palette with statue illustration"
|
||||
width={260}
|
||||
height={320}
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -124,30 +124,38 @@ export function Navigation() {
|
||||
className={cn(
|
||||
"fixed top-0 left-0 right-0 z-50 transition-all duration-700 ease-out",
|
||||
isScrolled
|
||||
? "bg-black/95 backdrop-blur-md shadow-lg border-b border-white/10 opacity-100"
|
||||
: "bg-transparent backdrop-blur-none opacity-100",
|
||||
? "backdrop-blur-md bg-[rgba(20,18,16,0.92)] border-b border-white/10 shadow-[0_12px_40px_-20px_rgba(0,0,0,0.8)]"
|
||||
: "bg-transparent"
|
||||
)}
|
||||
>
|
||||
<div className="max-w-[1800px] mx-auto px-6 lg:px-10">
|
||||
<div className="flex items-center justify-between h-20">
|
||||
<div className="relative mx-auto max-w-[1600px] px-5 sm:px-8">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-x-3 -top-4 h-[130px] rounded-3xl border border-white/10 transition-opacity duration-700",
|
||||
"before:absolute before:inset-0 before:rounded-3xl before:bg-[linear-gradient(130deg,rgba(255,255,255,0.16)_0%,rgba(255,255,255,0.02)_55%,rgba(10,10,10,0.35)_100%)] before:opacity-90",
|
||||
"after:absolute after:inset-0 after:rounded-3xl after:bg-[url('/assets/liberty/sketch-blue-etching.webp')] after:bg-cover after:bg-center after:opacity-10",
|
||||
isScrolled ? "opacity-100" : "opacity-80"
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="relative flex h-[90px] items-center justify-between gap-3">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex flex-col items-start transition-all duration-500 text-white group"
|
||||
className="group relative flex flex-col items-start text-white transition-colors duration-500"
|
||||
>
|
||||
<span className="font-bold text-2xl lg:text-3xl tracking-[0.15em] leading-none">
|
||||
UNITED
|
||||
<span className="font-playfair text-[1.8rem] uppercase tracking-[0.28em] sm:text-[2.1rem]">
|
||||
United
|
||||
</span>
|
||||
<span className="mt-1 flex items-center gap-3 text-[0.65rem] font-semibold uppercase tracking-[0.4em] text-white/70">
|
||||
<span className="h-px w-9 bg-white/60 transition-all duration-500 group-hover:w-12" />
|
||||
Tattoo Studio
|
||||
</span>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="h-px w-10 bg-white"></span>
|
||||
<span className="text-xs lg:text-sm font-medium tracking-[0.2em] uppercase">
|
||||
TATTOO
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="hidden lg:flex items-center flex-1 justify-between ml-16">
|
||||
<NavigationMenu viewport={false} className="flex-initial items-center bg-transparent text-white">
|
||||
<NavigationMenuList className="flex items-center gap-8">
|
||||
<div className="hidden flex-1 items-center justify-end gap-12 text-white lg:flex">
|
||||
<NavigationMenu viewport={false} className="flex flex-1 justify-end">
|
||||
<NavigationMenuList className="flex items-center gap-8 xl:gap-10">
|
||||
{navItems
|
||||
.filter((item) => !item.isButton)
|
||||
.map((item) => {
|
||||
@ -159,12 +167,21 @@ export function Navigation() {
|
||||
asChild
|
||||
data-active={isActive || undefined}
|
||||
className={cn(
|
||||
"group relative inline-flex h-auto bg-transparent px-0 py-1 text-sm font-semibold tracking-[0.15em] uppercase transition-all duration-300",
|
||||
"text-white/90 hover:bg-transparent hover:text-white focus:bg-transparent focus:text-white",
|
||||
isActive && "text-white",
|
||||
"relative inline-flex items-center text-[0.72rem] font-semibold uppercase tracking-[0.42em] text-white/60 transition-colors duration-300",
|
||||
"group hover:text-white focus-visible:text-white",
|
||||
isActive && "text-white"
|
||||
)}
|
||||
>
|
||||
<Link href={item.href}>{item.label}</Link>
|
||||
<Link href={item.href} className="px-1 py-1">
|
||||
{item.label}
|
||||
<span
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-x-0 -bottom-2 h-[1px] origin-left scale-x-0 bg-white/70 transition-transform duration-300",
|
||||
isActive && "scale-x-100",
|
||||
"group-hover:scale-x-100"
|
||||
)}
|
||||
/>
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
)
|
||||
@ -175,79 +192,75 @@ export function Navigation() {
|
||||
<Button
|
||||
asChild
|
||||
className={cn(
|
||||
"px-8 py-3 text-sm font-semibold tracking-[0.1em] uppercase transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70 focus-visible:ring-offset-0 hover:scale-105 group",
|
||||
isScrolled
|
||||
? "bg-white text-black hover:bg-gray-100 shadow-xl hover:shadow-2xl"
|
||||
: "border border-white/80 bg-transparent text-white shadow-none hover:bg-white/10",
|
||||
"group relative overflow-hidden rounded-full px-8 py-3 text-xs font-semibold uppercase tracking-[0.36em] transition-all duration-300",
|
||||
"bg-white/90 text-[#1c1713] shadow-[0_10px_40px_rgba(0,0,0,0.22)] hover:bg-white"
|
||||
)}
|
||||
>
|
||||
<Link href="/book" className="flex items-center gap-2">
|
||||
<span>Book Now</span>
|
||||
<ArrowUpRight className="h-4 w-4 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
||||
<ArrowUpRight className="h-4 w-4 -translate-y-[1px] transition-transform duration-300 group-hover:translate-x-1 group-hover:-translate-y-1" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="lg:hidden p-4 rounded-lg transition-all duration-300 text-white hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70 focus-visible:ring-offset-0"
|
||||
className="relative inline-flex rounded-full border border-white/20 p-3 text-white transition-all duration-300 hover:border-white/40 lg:hidden"
|
||||
onClick={handleToggleMenu}
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
{isOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
{isOpen ? <X size={22} /> : <Menu size={22} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div className="lg:hidden bg-black/98 backdrop-blur-md border-t border-white/10">
|
||||
<div className="px-6 py-8 space-y-5">
|
||||
<NavigationMenu viewport={false} className="w-full">
|
||||
<NavigationMenuList className="flex w-full flex-col space-y-3">
|
||||
{navItems.map((item) => {
|
||||
const isActive = !item.isButton && activeSection === item.id
|
||||
|
||||
if (item.isButton) {
|
||||
return (
|
||||
<NavigationMenuItem key={item.id} className="w-full">
|
||||
<Button
|
||||
asChild
|
||||
className="w-full bg-white hover:bg-gray-100 text-black py-5 text-lg font-semibold tracking-[0.05em] uppercase shadow-xl mt-8"
|
||||
>
|
||||
<Link href={item.href} onClick={handleCloseMenu}>
|
||||
{item.label}
|
||||
</Link>
|
||||
</Button>
|
||||
</NavigationMenuItem>
|
||||
)
|
||||
}
|
||||
<div className="relative z-10 mt-1 overflow-hidden rounded-3xl border border-white/10 bg-[rgba(21,19,16,0.96)] px-6 py-8 shadow-[0_35px_60px_-30px_rgba(0,0,0,0.65)] lg:hidden">
|
||||
<NavigationMenu viewport={false} className="w-full">
|
||||
<NavigationMenuList className="flex w-full flex-col gap-4">
|
||||
{navItems.map((item) => {
|
||||
const isActive = !item.isButton && activeSection === item.id
|
||||
|
||||
if (item.isButton) {
|
||||
return (
|
||||
<NavigationMenuItem key={item.id} className="w-full">
|
||||
<NavigationMenuLink
|
||||
<NavigationMenuItem key={item.id} className="w-full pt-4">
|
||||
<Button
|
||||
asChild
|
||||
data-active={isActive || undefined}
|
||||
className={cn(
|
||||
"block w-full rounded-md px-4 py-4 text-lg font-semibold tracking-[0.1em] uppercase transition-all duration-300",
|
||||
isActive
|
||||
? "border-l-4 border-white pl-6 text-white"
|
||||
: "text-white/70 hover:text-white hover:pl-5 focus:text-white focus:pl-5",
|
||||
)}
|
||||
className="w-full rounded-full bg-white/90 py-4 text-sm font-semibold uppercase tracking-[0.32em] text-[#1c1713] shadow-[0_20px_45px_-25px_rgba(255,255,255,0.9)] hover:bg-white"
|
||||
>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={(event) => {
|
||||
handleNavClick(event, item)
|
||||
handleCloseMenu()
|
||||
}}
|
||||
>
|
||||
<Link href={item.href} onClick={handleCloseMenu}>
|
||||
{item.label}
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</Button>
|
||||
</NavigationMenuItem>
|
||||
)
|
||||
})}
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationMenuItem key={item.id} className="w-full">
|
||||
<NavigationMenuLink
|
||||
asChild
|
||||
data-active={isActive || undefined}
|
||||
className={cn(
|
||||
"block w-full rounded-2xl border border-transparent px-4 py-4 text-sm font-semibold uppercase tracking-[0.3em] text-white/70 transition-all duration-300",
|
||||
isActive
|
||||
? "border-white/20 bg-white/5 text-white"
|
||||
: "hover:border-white/10 hover:bg-white/5 hover:text-white"
|
||||
)}
|
||||
>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={(event) => {
|
||||
handleNavClick(event, item)
|
||||
handleCloseMenu()
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
)
|
||||
})}
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -69,55 +69,58 @@ export function ServicesSection() {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<section ref={sectionRef} id="services" className="min-h-screen relative">
|
||||
<div className="absolute inset-x-0 top-0 h-16 bg-black rounded-b-[100px]"></div>
|
||||
<div className="absolute inset-x-0 bottom-0 h-16 bg-black rounded-t-[100px]"></div>
|
||||
<section ref={sectionRef} id="services" className="relative min-h-screen bg-[#0c0907]">
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 h-24 bg-[linear-gradient(180deg,rgba(12,9,7,0)_0%,rgba(12,9,7,0.85)_45%,rgba(12,9,7,1)_100%)]" />
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-32 bg-[linear-gradient(0deg,rgba(12,9,7,0)_0%,rgba(12,9,7,0.92)_70%,rgba(12,9,7,1)_100%)]" />
|
||||
|
||||
<div className="bg-white py-20 px-8 lg:px-16 relative z-10">
|
||||
<div className="max-w-screen-2xl mx-auto">
|
||||
<div className="grid lg:grid-cols-2 gap-16 items-center">
|
||||
<div className="relative z-10 bg-[#f4efe6] px-8 py-20 shadow-[0_45px_90px_-40px_rgba(0,0,0,0.55)] lg:px-16">
|
||||
<div className="mx-auto max-w-screen-2xl">
|
||||
<div className="grid gap-16 lg:grid-cols-2 lg:items-center">
|
||||
<div className="relative">
|
||||
<div className="absolute -left-4 top-0 w-1 h-32 bg-black/10"></div>
|
||||
<div className="mb-8">
|
||||
<span className="text-sm font-medium tracking-widest text-black/90 uppercase">What We Offer</span>
|
||||
<div className="absolute -left-6 top-0 hidden h-32 w-1 bg-[#b9a18d]/40 lg:block" />
|
||||
<div className="mb-6">
|
||||
<span className="text-xs font-semibold uppercase tracking-[0.6em] text-[#6d5b4a]">What We Offer</span>
|
||||
</div>
|
||||
<h2 className="text-6xl lg:text-8xl font-bold tracking-tight mb-8 text-balance text-black">SERVICES</h2>
|
||||
<p className="text-xl text-black/90 leading-relaxed max-w-lg">
|
||||
From custom designs to cover-ups, we offer comprehensive tattoo services with the highest standards of
|
||||
quality and safety.
|
||||
<h2 className="font-playfair text-5xl tracking-tight text-[#1f1814] sm:text-6xl lg:text-[4.5rem]">
|
||||
Services
|
||||
</h2>
|
||||
<p className="mt-6 max-w-xl text-lg leading-relaxed text-[#3b3027]">
|
||||
From restorative cover-ups to experimental flash, each booking is curated with layered planning, precise
|
||||
execution, and a studio experience that keeps you grounded.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="bg-black/5 h-96 rounded-2xl overflow-hidden shadow-2xl">
|
||||
<div className="h-96 overflow-hidden rounded-3xl border border-[#d3c2b2]/40 bg-[#241c17] shadow-[0_50px_90px_-40px_rgba(0,0,0,0.5)]">
|
||||
<img
|
||||
src="/tattoo-equipment-and-tools.jpg"
|
||||
alt="Tattoo Equipment"
|
||||
className="w-full h-full object-cover"
|
||||
className="h-full w-full object-cover opacity-90"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-[linear-gradient(120deg,rgba(20,15,12,0.75)_10%,rgba(20,15,12,0.2)_85%)]" />
|
||||
</div>
|
||||
<div className="absolute -bottom-4 -right-4 w-24 h-24 bg-black/5 rounded-full"></div>
|
||||
<div className="absolute -bottom-6 -right-8 hidden h-24 w-24 rounded-full border border-[#d3c2b2]/60 bg-[#f4efe6] shadow-[0_25px_45px_-30px_rgba(0,0,0,0.6)] md:block" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:block bg-black text-white relative z-10">
|
||||
<div className="flex">
|
||||
<div className="relative z-10 hidden bg-[#13100d] text-white lg:block">
|
||||
<div className="flex items-start">
|
||||
{/* Left Side - Enhanced with split composition styling */}
|
||||
<div className="w-1/2 sticky top-0 h-screen bg-black relative">
|
||||
<div className="absolute right-0 top-0 w-px h-full bg-white/10"></div>
|
||||
<div className="h-full flex flex-col justify-center p-16 relative">
|
||||
<div className="sticky top-0 h-screen w-1/2 bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.1),transparent_60%),linear-gradient(180deg,#1d1713_0%,#110d0a_100%)]">
|
||||
<div className="absolute right-0 top-0 h-full w-px bg-white/10" />
|
||||
<div className="relative flex h-full flex-col justify-center p-16">
|
||||
<div className="space-y-8">
|
||||
<div className="mb-12">
|
||||
<div className="w-12 h-px bg-white/40 mb-6"></div>
|
||||
<span className="text-sm font-medium tracking-widest text-white/60 uppercase">Our Services</span>
|
||||
<h3 className="text-4xl font-bold tracking-tight mt-4 text-balance">Choose Your Style</h3>
|
||||
<div className="mb-6 h-px w-12 bg-white/35" />
|
||||
<span className="text-sm font-semibold uppercase tracking-[0.5em] text-white/55">Our Services</span>
|
||||
<h3 className="mt-4 font-playfair text-4xl tracking-tight text-white">Choose Your Style</h3>
|
||||
</div>
|
||||
|
||||
{services.map((service, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`transition-all duration-500 cursor-pointer group ${
|
||||
className={`group cursor-pointer transition-all duration-500 ${
|
||||
activeService === index ? "opacity-100" : "opacity-50 hover:opacity-75"
|
||||
}`}
|
||||
onClick={() => {
|
||||
@ -126,12 +129,12 @@ export function ServicesSection() {
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`border-l-2 pl-6 py-4 transition-all duration-300 ${
|
||||
activeService === index ? "border-white" : "border-white/20 group-hover:border-white/40"
|
||||
className={`border-l-[3px] py-4 pl-6 transition-all duration-300 ${
|
||||
activeService === index ? "border-white" : "border-white/15 group-hover:border-white/30"
|
||||
}`}
|
||||
>
|
||||
<h4 className="text-2xl font-bold mb-2">{service.title}</h4>
|
||||
<p className="text-white/70 text-sm">{service.price}</p>
|
||||
<h4 className="mb-2 text-2xl font-semibold tracking-wide">{service.title}</h4>
|
||||
<p className="text-sm text-white/60">{service.price}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@ -140,22 +143,22 @@ export function ServicesSection() {
|
||||
</div>
|
||||
|
||||
{/* Right Side - Enhanced with split composition styling */}
|
||||
<div className="w-full lg:w-1/2 bg-gradient-to-b from-black to-gray-900">
|
||||
<div className="w-1/2 bg-gradient-to-b from-[#15110d] via-[#110d0a] to-[#0c0907]">
|
||||
{services.map((service, index) => (
|
||||
<div
|
||||
key={index}
|
||||
data-service-index={index}
|
||||
className="min-h-screen flex items-center justify-center p-8 lg:p-16 relative"
|
||||
className="relative flex min-h-screen items-center justify-center p-12"
|
||||
>
|
||||
<div className="absolute left-0 top-1/2 w-px h-32 bg-white/10 -translate-y-1/2"></div>
|
||||
<div className="max-w-lg relative">
|
||||
<div className="absolute left-0 top-1/2 h-32 w-px -translate-y-1/2 bg-white/10" />
|
||||
<div className="relative max-w-lg">
|
||||
<div className="mb-6">
|
||||
<span className="text-sm font-medium tracking-widest text-white/60 uppercase">
|
||||
<span className="text-xs font-semibold uppercase tracking-[0.6em] text-white/55">
|
||||
Service {String(index + 1).padStart(2, "0")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-4xl lg:text-6xl font-bold tracking-tight mb-6 text-balance">
|
||||
<h3 className="mb-6 font-playfair text-4xl tracking-tight lg:text-5xl">
|
||||
{service.title.split(" ").map((word, i) => (
|
||||
<span key={i} className="block">
|
||||
{word}
|
||||
@ -163,26 +166,28 @@ export function ServicesSection() {
|
||||
))}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-6 mb-8">
|
||||
<p className="text-lg text-white/80 leading-relaxed">{service.description}</p>
|
||||
<div className="mb-8 space-y-6">
|
||||
<p className="text-base leading-relaxed text-white/75">{service.description}</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
{service.features.map((feature, idx) => (
|
||||
<p key={idx} className="text-white/70 flex items-center">
|
||||
<span className="w-1 h-1 bg-white/40 rounded-full mr-3"></span>
|
||||
<p key={idx} className="flex items-center text-white/60">
|
||||
<span className="mr-3 h-[2px] w-6 bg-white/30" />
|
||||
{feature}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-2xl font-bold text-white">{service.price}</p>
|
||||
<p className="text-xl font-semibold uppercase tracking-[0.4em] text-white/70">
|
||||
{service.price}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
asChild
|
||||
className="bg-white text-black hover:bg-white/90 !text-black px-8 py-4 text-lg font-medium tracking-wide transition-all duration-300 hover:scale-105"
|
||||
className="rounded-full border border-white/15 bg-white/90 px-8 py-4 text-xs font-semibold uppercase tracking-[0.36em] text-[#1c1713] transition-transform duration-300 hover:scale-[1.04] hover:bg-white"
|
||||
>
|
||||
<Link href="/book">BOOK NOW</Link>
|
||||
<Link href="/book">Book Now</Link>
|
||||
</Button>
|
||||
|
||||
<div className="mt-12">
|
||||
@ -190,9 +195,9 @@ export function ServicesSection() {
|
||||
<img
|
||||
src={`/abstract-geometric-shapes.png?height=300&width=400&query=${service.title.toLowerCase()} tattoo example`}
|
||||
alt={service.title}
|
||||
className="w-full max-w-sm h-auto object-cover rounded-lg shadow-2xl"
|
||||
className="h-auto w-full max-w-sm rounded-3xl border border-white/10 object-cover shadow-[0_35px_65px_-40px_rgba(0,0,0,0.8)]"
|
||||
/>
|
||||
<div className="absolute -bottom-2 -right-2 w-16 h-16 bg-white/5 rounded-lg"></div>
|
||||
<div className="absolute -bottom-3 -right-3 h-16 w-16 rounded-2xl border border-white/15 bg-white/10" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
BIN
public/assets/liberty/background-dove-wash.avif
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
public/assets/liberty/background-dove-wash.webp
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
public/assets/liberty/dove-tableau-close.avif
Normal file
|
After Width: | Height: | Size: 389 KiB |
BIN
public/assets/liberty/dove-tableau-close.webp
Normal file
|
After Width: | Height: | Size: 452 KiB |
BIN
public/assets/liberty/hero-statue-collage.avif
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
public/assets/liberty/hero-statue-collage.webp
Normal file
|
After Width: | Height: | Size: 261 KiB |
BIN
public/assets/liberty/mural-orange-wall.avif
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
public/assets/liberty/mural-orange-wall.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
public/assets/liberty/mural-portrait-sun.avif
Normal file
|
After Width: | Height: | Size: 304 KiB |
BIN
public/assets/liberty/mural-portrait-sun.webp
Normal file
|
After Width: | Height: | Size: 293 KiB |
BIN
public/assets/liberty/palette-brush-liberty.avif
Normal file
|
After Width: | Height: | Size: 314 KiB |
BIN
public/assets/liberty/palette-brush-liberty.webp
Normal file
|
After Width: | Height: | Size: 316 KiB |
BIN
public/assets/liberty/sketch-blue-etching.avif
Normal file
|
After Width: | Height: | Size: 869 KiB |
BIN
public/assets/liberty/sketch-blue-etching.webp
Normal file
|
After Width: | Height: | Size: 867 KiB |
BIN
public/assets/liberty/sketch-liberty-cup.avif
Normal file
|
After Width: | Height: | Size: 354 KiB |
BIN
public/assets/liberty/sketch-liberty-cup.webp
Normal file
|
After Width: | Height: | Size: 332 KiB |
BIN
public/new-images/0_0.png
Normal file
|
After Width: | Height: | Size: 6.1 MiB |
BIN
public/new-images/0_1.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
public/new-images/0_2.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
public/new-images/0_3.png
Normal file
|
After Width: | Height: | Size: 4.9 MiB |
BIN
public/new-images/0_4.png
Normal file
|
After Width: | Height: | Size: 5.4 MiB |
BIN
public/new-images/0_5.png
Normal file
|
After Width: | Height: | Size: 5.1 MiB |
BIN
public/new-images/0_6.png
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
BIN
public/new-images/0_7.png
Normal file
|
After Width: | Height: | Size: 8.6 MiB |
BIN
public/new-images/screenshot_20251023_035853.png
Normal file
|
After Width: | Height: | Size: 7.1 MiB |