Compare commits
2 Commits
03a08de734
...
b75be374e8
| Author | SHA1 | Date | |
|---|---|---|---|
| b75be374e8 | |||
| 0df6e4931c |
@ -300,4 +300,18 @@
|
||||
.font-playfair {
|
||||
font-family: var(--font-playfair);
|
||||
}
|
||||
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.perspective-1000 {
|
||||
perspective: 1000px;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,348 @@
|
||||
"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 = 8
|
||||
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)
|
||||
|
||||
// Fall back to static data
|
||||
return staticArtist
|
||||
})
|
||||
}, [dbArtistsData, isLoading, error])
|
||||
const [gridSets, setGridSets] = useState<ArtistGridSet[]>([])
|
||||
const [activeSetIndex, setActiveSetIndex] = useState(0)
|
||||
const [previousSetIndex, setPreviousSetIndex] = useState<number | null>(null)
|
||||
const [centerIndex, setCenterIndex] = useState(0)
|
||||
|
||||
// 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])
|
||||
artistsRef.current = artists
|
||||
gridSetsRef.current = gridSets
|
||||
activeSetRef.current = activeSetIndex
|
||||
|
||||
useEffect(() => {
|
||||
if (!advancedNavAnimations) {
|
||||
setVisibleCards(allArtistIndices)
|
||||
return
|
||||
}
|
||||
setVisibleCards([])
|
||||
}, [advancedNavAnimations, allArtistIndices])
|
||||
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
|
||||
}, [])
|
||||
|
||||
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])])
|
||||
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()
|
||||
setCenterIndex(0)
|
||||
return 0
|
||||
}
|
||||
setCenterIndex(0)
|
||||
return next
|
||||
})
|
||||
}, [regenerateSets])
|
||||
|
||||
const rotateCarousel = useCallback((direction: 'next' | 'prev') => {
|
||||
if (gridSetsRef.current.length === 0) return
|
||||
|
||||
const currentSet = gridSetsRef.current[activeSetRef.current]
|
||||
if (!currentSet) return
|
||||
|
||||
setCenterIndex((prev) => {
|
||||
const nextIndex = direction === 'next'
|
||||
? (prev + 1) % currentSet.items.length
|
||||
: (prev - 1 + currentSet.items.length) % currentSet.items.length
|
||||
|
||||
// If we've looped back to start on 'next', advance to next set
|
||||
if (direction === 'next' && nextIndex === 0 && prev === currentSet.items.length - 1) {
|
||||
setTimeout(() => advanceSet(), 0)
|
||||
}
|
||||
|
||||
return nextIndex
|
||||
})
|
||||
}, [advanceSet])
|
||||
|
||||
useEffect(() => {
|
||||
if (gridSets.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const interval = window.setInterval(() => {
|
||||
rotateCarousel('next')
|
||||
}, GRID_INTERVAL)
|
||||
|
||||
return () => window.clearInterval(interval)
|
||||
}, [rotateCarousel, 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) => {
|
||||
// Try faceImage from static data first
|
||||
if ((artist as any).faceImage) {
|
||||
return (artist as any).faceImage
|
||||
}
|
||||
// Fall back to first work image
|
||||
if (artist.workImages && artist.workImages.length > 0) {
|
||||
return artist.workImages[0]
|
||||
}
|
||||
// Final fallback
|
||||
return "/placeholder.svg"
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="artists" className="relative isolate overflow-hidden pb-32 pt-32 lg:pb-40 lg:pt-40">
|
||||
<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-7xl flex-col gap-16 px-6 lg:px-10 xl:flex-row">
|
||||
<div className="xl:w-[40%] space-y-10 text-white">
|
||||
<div className="space-y-8">
|
||||
<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]">
|
||||
Artists Who Know What They're Doing
|
||||
</h2>
|
||||
<p className="max-w-xl text-base leading-relaxed text-white/70 sm:text-lg">
|
||||
Cover-up specialists, illustrative work, anime, and fine line. Each artist brings years of experience and their own style.
|
||||
Custom work and flash drops.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||
<Button
|
||||
asChild
|
||||
className="w-full bg-white px-8 py-4 text-sm font-semibold uppercase tracking-wide text-black transition-colors hover:bg-white/90 sm:w-auto"
|
||||
>
|
||||
<Link href="/book">Book Your Session</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
asChild
|
||||
className="w-full justify-start border border-white/20 bg-white/5 px-6 py-4 text-sm font-medium uppercase tracking-wide text-white/80 backdrop-blur hover:bg-white/10 sm:w-auto"
|
||||
>
|
||||
<Link href="/artists">View All Artists</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative xl:w-[60%]">
|
||||
<div className="relative h-[500px] sm:h-[550px] lg:h-[600px] flex items-center justify-center">
|
||||
{gridSets[activeSetIndex] && (
|
||||
<div className="relative w-full h-full flex items-center justify-center">
|
||||
{gridSets[activeSetIndex].items.map((artist, index) => {
|
||||
const href = `/artists/${artist.slug}`
|
||||
const image = getArtistImage(artist)
|
||||
|
||||
// Calculate position relative to center with wrapping for infinite loop
|
||||
let positionFromCenter = index - centerIndex
|
||||
const totalCards = gridSets[activeSetIndex].items.length
|
||||
|
||||
// Wrap around for continuous loop effect
|
||||
if (positionFromCenter < -2) {
|
||||
positionFromCenter += totalCards
|
||||
} else if (positionFromCenter > totalCards / 2) {
|
||||
positionFromCenter -= totalCards
|
||||
}
|
||||
})
|
||||
},
|
||||
{ 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"
|
||||
}
|
||||
// Only show center + 2 behind on each side
|
||||
const isVisible = Math.abs(positionFromCenter) <= 2
|
||||
|
||||
const cardTransitionDelay = (index: number) => {
|
||||
if (!advancedNavAnimations) return undefined
|
||||
return `${index * 40}ms`
|
||||
}
|
||||
// Calculate transforms for stacked deck effect
|
||||
const isCenterCard = positionFromCenter === 0
|
||||
|
||||
// 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]
|
||||
}
|
||||
// Cards stack behind based on position
|
||||
let translateY = 0
|
||||
let translateX = 0
|
||||
let scale = 1
|
||||
let opacity = 1
|
||||
let zIndex = 10
|
||||
|
||||
// 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 }
|
||||
})
|
||||
}
|
||||
}
|
||||
if (positionFromCenter > 0) {
|
||||
// Cards to the right (future cards) - stack behind on right
|
||||
translateY = positionFromCenter * 20
|
||||
translateX = positionFromCenter * 40
|
||||
scale = 1 - positionFromCenter * 0.08
|
||||
opacity = Math.max(0, 1 - positionFromCenter * 0.4)
|
||||
zIndex = 10 - positionFromCenter
|
||||
} else if (positionFromCenter < 0) {
|
||||
// Cards to the left (past cards) - slide out to left and fade
|
||||
translateX = positionFromCenter * 150
|
||||
opacity = Math.max(0, 1 + positionFromCenter * 0.5)
|
||||
zIndex = 10 + positionFromCenter
|
||||
}
|
||||
|
||||
const handleHoverEnd = () => {
|
||||
setHoveredCard(null)
|
||||
}
|
||||
if (!isVisible) return 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.
|
||||
</p>
|
||||
return (
|
||||
<Link
|
||||
key={`${artist.id}-${artist.slug}-${index}`}
|
||||
href={href}
|
||||
className="group absolute aspect-[3/4] w-[280px] sm:w-[320px] lg:w-[380px] overflow-hidden rounded-2xl border border-white/12 text-left transition-all duration-700 ease-out hover:border-white/25"
|
||||
style={{
|
||||
transform: `translate(${translateX}px, ${translateY}px) scale(${scale})`,
|
||||
opacity,
|
||||
zIndex,
|
||||
pointerEvents: isCenterCard ? 'auto' : 'none',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={image}
|
||||
alt={`${artist.name} portfolio sample`}
|
||||
className="absolute inset-0 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-5 top-5 h-12 w-12 rounded-full border border-white/20 bg-white/10 backdrop-blur">
|
||||
<span className="absolute inset-2 rounded-full border border-white/15" />
|
||||
</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 className="absolute bottom-0 left-0 right-0 p-6 space-y-2">
|
||||
<p className="text-lg font-semibold uppercase tracking-wider text-white">
|
||||
{artist.name}
|
||||
</p>
|
||||
<p className="text-sm uppercase tracking-wide text-white/60">
|
||||
{(artist as any).specialty || "Tattoo Artist"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Navigation buttons */}
|
||||
<button
|
||||
onClick={() => rotateCarousel('prev')}
|
||||
className="absolute left-0 z-20 p-3 text-white/60 transition-colors hover:text-white"
|
||||
aria-label="Previous artist"
|
||||
>
|
||||
<svg className="h-8 w-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => rotateCarousel('next')}
|
||||
className="absolute right-0 z-20 p-3 text-white/60 transition-colors hover:text-white"
|
||||
aria-label="Next artist"
|
||||
>
|
||||
<svg className="h-8 w-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</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>
|
||||
</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>
|
||||
)
|
||||
|
||||
@ -6,201 +6,205 @@ import { ArrowUp } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
export function Footer() {
|
||||
const [showScrollTop, setShowScrollTop] = useState(false)
|
||||
const [showScrollTop, setShowScrollTop] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const scrolled = window.scrollY
|
||||
const threshold = window.innerHeight * 0.5
|
||||
setShowScrollTop(scrolled > threshold)
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const scrolled = window.scrollY
|
||||
const threshold = window.innerHeight * 0.5
|
||||
setShowScrollTop(scrolled > threshold)
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", handleScroll)
|
||||
handleScroll()
|
||||
return () => window.removeEventListener("scroll", handleScroll)
|
||||
}, [])
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" })
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", handleScroll)
|
||||
handleScroll()
|
||||
return () => window.removeEventListener("scroll", handleScroll)
|
||||
}, [])
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={scrollToTop}
|
||||
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>
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" })
|
||||
}
|
||||
<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 items-start gap-10 md:grid-cols-12">
|
||||
<div className="md:col-span-3">
|
||||
<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-sm text-white/65">
|
||||
{[
|
||||
{ name: "TRADITIONAL", count: "" },
|
||||
{ name: "REALISM", count: "" },
|
||||
{ name: "BLACKWORK", count: "" },
|
||||
{ name: "FINE LINE", count: "" },
|
||||
{ name: "ILLUSTRATION", count: "" },
|
||||
{ name: "ANIME", count: "" },
|
||||
].map((service, index) => (
|
||||
<li key={index}>
|
||||
<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>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<>
|
||||
<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"
|
||||
}`}
|
||||
aria-label="Scroll to top"
|
||||
>
|
||||
<ArrowUp size={20} />
|
||||
</Button>
|
||||
<div className="md:col-span-3">
|
||||
<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-sm text-white/65">
|
||||
{[
|
||||
{ name: "CHRISTY_LUMBERG", count: "" },
|
||||
{ name: "STEVEN_SOLE", count: "" },
|
||||
{ name: "DONOVAN_L", count: "" },
|
||||
{ name: "VIEW_ALL", count: "" },
|
||||
].map((artist, index) => (
|
||||
<li key={index}>
|
||||
<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>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<footer className="bg-black text-white py-16 font-mono">
|
||||
<div className="container mx-auto px-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 items-start">
|
||||
<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>
|
||||
<ul className="space-y-3 text-base">
|
||||
{[
|
||||
{ name: "TRADITIONAL", count: "" },
|
||||
{ name: "REALISM", count: "" },
|
||||
{ name: "BLACKWORK", count: "" },
|
||||
{ name: "FINE LINE", count: "" },
|
||||
{ name: "WATERCOLOR", count: "" },
|
||||
{ name: "COVER-UPS", count: "" },
|
||||
{ name: "ANIME", count: "" },
|
||||
].map((service, index) => (
|
||||
<li key={index}>
|
||||
<Link href="/book" className="text-gray-400 hover:text-white transition-colors duration-200">
|
||||
{service.name}
|
||||
{service.count && <span className="text-white ml-2">{service.count}</span>}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="md:col-span-3">
|
||||
<div className="mb-4 text-xs uppercase tracking-[0.4em] text-white/40">
|
||||
© <span className="text-white/80">L3 INVESTMENTS</span> LLC 2025 — All Rights Reserved
|
||||
</div>
|
||||
<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>
|
||||
</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>
|
||||
<ul className="space-y-3 text-base">
|
||||
{[
|
||||
{ name: "CHRISTY_LUMBERG", count: "" },
|
||||
{ name: "STEVEN_SOLE", count: "" },
|
||||
{ name: "DONOVAN_L", count: "" },
|
||||
{ name: "VIEW_ALL", count: "" },
|
||||
].map((artist, index) => (
|
||||
<li key={index}>
|
||||
<Link href="/artists" className="text-gray-400 hover:text-white transition-colors duration-200">
|
||||
{artist.name}
|
||||
{artist.count && <span className="text-white ml-2">{artist.count}</span>}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="md:col-span-3 space-y-8">
|
||||
{/* Legal */}
|
||||
<div>
|
||||
<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-sm text-white/65">
|
||||
<li>
|
||||
<Link
|
||||
href="/aftercare"
|
||||
className="transition-colors duration-200 hover:text-white underline"
|
||||
>
|
||||
AFTERCARE
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/deposit"
|
||||
className="transition-colors duration-200 hover:text-white underline"
|
||||
>
|
||||
DEPOSIT POLICY
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/terms"
|
||||
className="transition-colors duration-200 hover:text-white underline"
|
||||
>
|
||||
TERMS OF SERVICE
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/privacy"
|
||||
className="transition-colors duration-200 hover:text-white underline"
|
||||
>
|
||||
PRIVACY POLICY
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="#"
|
||||
className="transition-colors duration-200 hover:text-white underline"
|
||||
>
|
||||
WAIVER
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</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>
|
||||
<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">
|
||||
(719) 698-9004
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{/* Social */}
|
||||
<div>
|
||||
<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-sm text-white/65">
|
||||
<li>
|
||||
<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-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-white/65 underline transition-colors duration-200 hover:text-white"
|
||||
>
|
||||
TIKTOK
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{/* Contact */}
|
||||
<div>
|
||||
<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-sm text-white/65 underline transition-colors duration-200 hover:text-white"
|
||||
>
|
||||
INFO@UNITED-TATTOO.COM
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
<ul className="space-y-2 text-base">
|
||||
<li>
|
||||
<Link
|
||||
href="/aftercare"
|
||||
className="text-gray-400 hover:text-white transition-colors duration-200 underline"
|
||||
>
|
||||
AFTERCARE
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/deposit"
|
||||
className="text-gray-400 hover:text-white transition-colors duration-200 underline"
|
||||
>
|
||||
DEPOSIT POLICY
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/terms"
|
||||
className="text-gray-400 hover:text-white transition-colors duration-200 underline"
|
||||
>
|
||||
TERMS OF SERVICE
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/privacy"
|
||||
className="text-gray-400 hover:text-white transition-colors duration-200 underline"
|
||||
>
|
||||
PRIVACY POLICY
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-gray-400 hover:text-white transition-colors duration-200 underline"
|
||||
>
|
||||
WAIVER
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<ul className="space-y-2 text-base">
|
||||
<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">
|
||||
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">
|
||||
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">
|
||||
TIKTOK
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<Link
|
||||
href="mailto:info@united-tattoo.com"
|
||||
className="text-gray-400 hover:text-white transition-colors duration-200 underline text-base"
|
||||
>
|
||||
INFO@UNITED-TATTOO.COM
|
||||
</Link>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</footer>
|
||||
</>
|
||||
)
|
||||
</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"
|
||||
@ -8,85 +10,133 @@ import { useMultiLayerParallax, useReducedMotion } from "@/hooks/use-parallax"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
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)
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const advancedNavAnimations = useFeatureFlag("ADVANCED_NAV_SCROLL_ANIMATIONS_ENABLED")
|
||||
const reducedMotion = useReducedMotion()
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setIsVisible(true), 300)
|
||||
return () => clearTimeout(timer)
|
||||
}, [])
|
||||
const parallax = useMultiLayerParallax(!advancedNavAnimations || reducedMotion)
|
||||
|
||||
return (
|
||||
<section
|
||||
id="home"
|
||||
className="min-h-screen flex items-center justify-center relative overflow-hidden"
|
||||
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"
|
||||
style={{
|
||||
backgroundImage: "url(/united-logo-full.jpg)",
|
||||
...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"
|
||||
style={parallax.midground.style}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setIsVisible(true), 240)
|
||||
return () => clearTimeout(timer)
|
||||
}, [])
|
||||
|
||||
{/* 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"
|
||||
style={parallax.foreground.style}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"transition-all duration-1000",
|
||||
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
|
||||
)}
|
||||
return (
|
||||
<section
|
||||
id="home"
|
||||
className="relative flex min-h-[100vh] items-center justify-center overflow-hidden px-6 pb-24 pt-22 sm:px-10 lg:pt-34"
|
||||
data-reduced-motion={reducedMotion}
|
||||
>
|
||||
<h1 className="font-playfair text-5xl lg:text-7xl font-bold text-white mb-6 tracking-tight">
|
||||
UNITED TATTOO
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
ref={parallax.background.ref}
|
||||
className="pointer-events-none absolute inset-0 will-change-transform"
|
||||
style={{
|
||||
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"
|
||||
/>
|
||||
|
||||
<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
|
||||
ref={parallax.midground.ref}
|
||||
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"
|
||||
/>
|
||||
|
||||
<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"
|
||||
>
|
||||
Book Consultation
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
<div
|
||||
ref={parallax.foreground.ref}
|
||||
className="relative z-10 w-full will-change-transform"
|
||||
style={parallax.foreground.style}
|
||||
>
|
||||
<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"
|
||||
)}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<h1 className="font-playfair text-5xl leading-[1.05] tracking-tight sm:text-6xl lg:text-[5.5rem]">
|
||||
UNITED
|
||||
</h1>
|
||||
<p className="max-w-xl text-lg leading-relaxed text-white/75 sm:text-xl">
|
||||
Tattoos by artists who know what they're doing. Custom tattoos, flash & cover-ups.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 pt-6 md:flex-row md:items-center">
|
||||
<Button
|
||||
asChild
|
||||
className="w-full bg-white px-8 py-4 text-sm font-semibold uppercase tracking-wide text-black transition-colors hover:bg-white/90 md:w-auto"
|
||||
>
|
||||
<Link href="/book">Book Your Session</Link>
|
||||
</Button>
|
||||
</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] border border-white/12 bg-white/8 p-4 backdrop-blur-md lg:block">
|
||||
<p className="text-sm font-semibold text-white">United Tattoo</p>
|
||||
<p className="mt-2 text-xs leading-relaxed text-white/50">
|
||||
Fountain, Colorado
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import { useCallback, useEffect, useState } from "react"
|
||||
import type { MouseEvent } from "react"
|
||||
import Link from "next/link"
|
||||
import { usePathname, useRouter } from "next/navigation"
|
||||
import { ArrowUpRight, Menu, X } from "lucide-react"
|
||||
import { Menu, X } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
@ -122,49 +122,47 @@ export function Navigation() {
|
||||
return (
|
||||
<nav
|
||||
className={cn(
|
||||
"fixed top-0 left-0 right-0 z-50 transition-all duration-700 ease-out",
|
||||
"fixed top-0 left-0 right-0 z-50 transition-all duration-300",
|
||||
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-xl bg-black/80 border-b border-white/10"
|
||||
: "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 className="relative flex h-20 items-center justify-between gap-3">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex flex-col items-start transition-all duration-500 text-white group"
|
||||
className="relative flex flex-col items-start text-white transition-opacity hover:opacity-80"
|
||||
>
|
||||
<span className="font-bold text-2xl lg:text-3xl tracking-[0.15em] leading-none">
|
||||
UNITED
|
||||
<span className="font-playfair text-2xl uppercase tracking-[0.2em] sm:text-3xl">
|
||||
United
|
||||
</span>
|
||||
<span className="text-[0.65rem] font-semibold uppercase tracking-[0.3em] text-white/60">
|
||||
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) => {
|
||||
const isActive = activeSection === item.id
|
||||
|
||||
return (
|
||||
<NavigationMenuItem key={item.id} className="min-w-max">
|
||||
<NavigationMenuLink
|
||||
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",
|
||||
)}
|
||||
>
|
||||
<Link href={item.href}>{item.label}</Link>
|
||||
<NavigationMenuItem key={item.id}>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={(e) => handleNavClick(e, item)}
|
||||
className={cn(
|
||||
"text-sm font-medium uppercase tracking-wider transition-colors px-3 py-2",
|
||||
isActive ? "text-white" : "text-white/60 hover:text-white"
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
)
|
||||
@ -174,22 +172,14 @@ 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",
|
||||
)}
|
||||
className="rounded-full bg-white px-6 py-2 text-sm font-semibold uppercase tracking-wide text-black transition-colors hover:bg-white/90"
|
||||
>
|
||||
<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" />
|
||||
</Link>
|
||||
<Link href="/book">Book Now</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="rounded-lg border border-white/20 p-2 text-white transition-colors hover:border-white/40 lg:hidden"
|
||||
onClick={handleToggleMenu}
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
@ -198,56 +188,51 @@ export function Navigation() {
|
||||
</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="absolute left-0 right-0 top-full mt-2 mx-5 rounded-2xl border border-white/10 bg-black/95 backdrop-blur-xl px-6 py-6 sm:mx-8 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-2">
|
||||
<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 py-3 text-sm font-semibold uppercase tracking-wide text-black hover:bg-white/90"
|
||||
>
|
||||
<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>
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={(event) => {
|
||||
handleNavClick(event, item)
|
||||
handleCloseMenu()
|
||||
}}
|
||||
className={cn(
|
||||
"block w-full px-4 py-3 text-sm font-medium uppercase tracking-wider transition-colors rounded-lg",
|
||||
isActive
|
||||
? "bg-white/10 text-white"
|
||||
: "text-white/60 hover:bg-white/5 hover:text-white"
|
||||
)}
|
||||
>
|
||||
{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>
|
||||
|
||||
283
docs/BRAND-LANGUAGE.md
Normal file
@ -0,0 +1,283 @@
|
||||
# United Tattoo Brand Language Brainstorming Session
|
||||
|
||||
**Session Date:** December 19, 2024
|
||||
**Facilitator:** Business Analyst Mary
|
||||
**Participant:** United Tattoo Team
|
||||
|
||||
## Executive Summary
|
||||
|
||||
**Topic:** Creating a structured brand language rulebook for United Tattoo
|
||||
|
||||
**Session Goals:** Develop authentic brand language guidelines that transform generic copy into distinctive United Tattoo voice
|
||||
|
||||
**Techniques Used:** Progressive technique flow with analyst-recommended methods
|
||||
|
||||
**Total Ideas Generated:** [To be updated during session]
|
||||
|
||||
## Technique Sessions
|
||||
|
||||
### Assumption Reversal - 10 minutes
|
||||
|
||||
**Description:** Identify what United Tattoo would NEVER say to establish authentic boundaries
|
||||
|
||||
**Ideas Generated:**
|
||||
1. "For the ones who live loud, tattoo proud, and believe in better" - forced verb usage, empty promises
|
||||
2. "This isn't your average tattoo shop" - defensive positioning, cliche opening
|
||||
3. "We're here to rewrite the narrative" - vague corporate speak with no clear meaning
|
||||
4. "where everyone feels seen, respected, and hyped to walk through our doors" - outdated slang, trying too hard
|
||||
5. "elevate the experience" - buzzword soup, unnecessarily verbose
|
||||
6. "create a space where real connection matters" - stating obvious human nature as unique value
|
||||
7. "we hire great people, not just great artists" - setting bar impossibly low
|
||||
8. "bring both skill and soul to the table" - cliche metaphor mixing
|
||||
9. "Every tattoo here is a story, a statement, and a shared moment" - overwrought emotional manipulation
|
||||
|
||||
**Insights Discovered:**
|
||||
- LLM-generated copy defaults to meaningless adjective stacking
|
||||
- Generic "transformation" language ignores actual tattoo shop reality
|
||||
- Forced emotional narratives sound inauthentic and manipulative
|
||||
- Defensive positioning ("not your average") suggests insecurity
|
||||
- Buzzwords replace actual concrete value propositions
|
||||
|
||||
**Notable Connections:**
|
||||
- All bad examples try to be everything to everyone instead of something specific
|
||||
- Corporate speak completely disconnects from tattoo culture authenticity
|
||||
- Overuse of transformation/elevation language feels condescending
|
||||
|
||||
### Role Playing - 15 minutes
|
||||
|
||||
**Description:** Discover authentic voice through real stakeholder interactions
|
||||
|
||||
**Ideas Generated:**
|
||||
|
||||
**Nervous First-Timer Response:**
|
||||
- "Hey that's okay! Girl we all have been scared of getting a tattoo before, shit, I get scared sometimes even now"
|
||||
- "You just let me know if you need a break, we can step out at any time, take a smoke break, just hang out"
|
||||
- "We can go at your pace"
|
||||
|
||||
**Picky Client Response:**
|
||||
- "Holy-fuck yeah- that's a lot-- that's okay though, I love having references!"
|
||||
- "Do you mind taking a seat so you can break this down for me?"
|
||||
|
||||
**Insights Discovered:**
|
||||
- Authentic voice uses mild profanity naturally, not performatively
|
||||
- Real empathy comes from shared experience ("I get scared sometimes even now")
|
||||
- Practical solutions over emotional theater ("take a smoke break, just hang out")
|
||||
- Direct acknowledgment of chaos without judgment ("that's a lot")
|
||||
- Collaborative problem-solving approach ("break this down for me")
|
||||
|
||||
**Notable Connections:**
|
||||
- Authenticity = vulnerability + practicality
|
||||
- Real tattoo artists talk like humans, not customer service scripts
|
||||
- Genuine care shows through actions offered, not feelings described
|
||||
|
||||
### First Principles Thinking - 15 minutes
|
||||
|
||||
**Description:** Extract fundamental language rules from authentic interactions
|
||||
|
||||
**Core Rules Identified:**
|
||||
|
||||
**Rule 1: Direct acknowledgment beats diplomatic deflection**
|
||||
- Rationale: When you leave things unsaid, people internalize and make assumptions. Blunt but friendly prevents judgment feelings.
|
||||
- Bad: "We understand everyone has different comfort levels"
|
||||
- Good: "Holy-fuck yeah- that's a lot"
|
||||
|
||||
**Rule 2: Offer practical solutions, not emotional theater**
|
||||
- Rationale: "I'm not your fuckin dad" - beautiful humans interacting with beautiful humans, not therapy sessions
|
||||
- Bad: "create a safe space where you feel supported"
|
||||
- Good: "take a smoke break, just hang out"
|
||||
|
||||
**Rule 3: Plain speaking about pricing/time**
|
||||
- Example: "Hey so because this is 6 inches long and I can tell that the complexity of the linework and shading is gonna take me an extra 2 hours, I'd feel comfortable doing this for $650, does that work for you?"
|
||||
- Principle: Transparent, specific, respectful
|
||||
|
||||
**Rule 4: Handle difficult clients with patience, like a human**
|
||||
- No elaborate customer service scripts
|
||||
- Human-to-human problem solving
|
||||
|
||||
**Rule 5: Describe work in quantifiable terms with justified confidence**
|
||||
- Bad: "93% proficient in opaques" (arbitrary metrics)
|
||||
- Good: "I've been doing opaques on shading for 5 years, would you like to see some examples so you can judge for yourself?"
|
||||
- Principle: If the artist, shop, portfolio or work can't justify the statement, don't make it
|
||||
|
||||
**Rule 6: Talk about other shops with kindness**
|
||||
- "The shop doesn't fucking matter. It's a building with some idiots in it. People only come for the idiots."
|
||||
- Focus on the artists, not competitive positioning
|
||||
|
||||
**Insights Discovered:**
|
||||
- Transparency prevents assumptions and judgment feelings
|
||||
- Confidence must be backed by demonstrable skill/experience
|
||||
- Human-to-human interaction trumps customer service performance
|
||||
- Competition isn't about shops, it's about individual artist quality
|
||||
|
||||
### Morphological Analysis - 10 minutes
|
||||
|
||||
**Description:** Test filtering system by transforming bad copy through United Tattoo rules
|
||||
|
||||
**Copy Transformation Examples:**
|
||||
|
||||
**Original:** "Artistry with integrity"
|
||||
**Rules Applied:** Direct acknowledgment + quantifiable terms
|
||||
**United Tattoo Version:** "We've been tattooing for [X years]. Here's our work."
|
||||
|
||||
**Original:** "More than ink—it's identity"
|
||||
**Rules Applied:** No emotional theater + plain speaking
|
||||
**United Tattoo Version:** "Good tattoos that'll look good in 20 years"
|
||||
|
||||
**Original:** "A space where creativity thrives"
|
||||
**Rules Applied:** Focus on the idiots, not the building
|
||||
**United Tattoo Version:** "Artists who know what they're doing"
|
||||
|
||||
**Test Case:** "We're here to rewrite the narrative, where everyone feels seen, respected, and hyped to walk through our doors"
|
||||
**United Tattoo Filtered Version:** "It doesn't matter who you are, you will always have a home with the United Tattoo family."
|
||||
|
||||
**Analysis of Transformation:**
|
||||
- Removed corporate buzzwords ("rewrite the narrative")
|
||||
- Replaced performative emotions ("hyped") with genuine warmth
|
||||
- Maintained inclusivity message but made it personal and direct
|
||||
- Used "family" concept authentically rather than as marketing device
|
||||
|
||||
### Mind Mapping - 10 minutes
|
||||
|
||||
**Description:** Organize findings into practical rulebook structure
|
||||
|
||||
**Central Concept:** United Tattoo Brand Language Filter
|
||||
|
||||
**Branch 1: Core Principles**
|
||||
- Respect reader intelligence - no big empty words that only impress idiots
|
||||
- Use common ground language - not corporate speak or legal jargon
|
||||
- Direct acknowledgment beats diplomatic deflection
|
||||
- Practical solutions over emotional theater
|
||||
|
||||
**Branch 2: Language Guidelines**
|
||||
- Plain speaking about pricing/process/time
|
||||
- Justified confidence only (backed by demonstrable skill)
|
||||
- Human-to-human tone in all interactions
|
||||
- Transparency prevents assumptions and judgment
|
||||
|
||||
**Branch 3: Content Transformation Rules**
|
||||
- Remove corporate buzzwords and meaningless adjective stacking
|
||||
- Replace performative emotions with genuine warmth
|
||||
- Convert abstract concepts to concrete actions
|
||||
- Focus on artists and work, not building or brand positioning
|
||||
|
||||
**Branch 4: Communication Standards**
|
||||
- Do not use words average person won't understand
|
||||
- Goal: efficient communication on common ground
|
||||
- Avoid condescending lawyer-speak and dehumanizing corporate language
|
||||
- Need examples demonstrating accessible vs. inaccessible language
|
||||
|
||||
**Insights Discovered:**
|
||||
- Intelligence respect = foundation of authentic communication
|
||||
- Common ground language builds connection vs. corporate performance language
|
||||
- Accessibility isn't dumbing down - it's efficient human communication
|
||||
- Everything needs clear guidelines - meet but never exceed bare minimum professionalism
|
||||
- 7th grade reading level should be maximum complexity for any content
|
||||
- Nobody wants to read something 5 times to understand it
|
||||
|
||||
**Practical Examples:**
|
||||
|
||||
**Aftercare Instructions - Bad vs. Good:**
|
||||
- Bad: "As the body's largest organ, your skin deserves careful attention after receiving a tattoo. At United Tattoo, we provide aftercare instructions based on recommended best practices to ensure the proper healing of your new body art. Our goal is to offer the most reliable and accurate information in the industry, informed by insights from medical professionals. These guidelines combine professional expertise, scientific research, and recommendations from the National Environmental Health Association's Body Art Model Code."
|
||||
- Good: "### Read our aftercare instructions: *(informed by the National Environmental Health Association's Body Art Model Code)*"
|
||||
|
||||
**Pricing Communication:**
|
||||
- Approach: "Pricing custom tattoos is hard. It depends on the artist and varies from one tattoo to the next."
|
||||
|
||||
**Tattoo Style Explanations:**
|
||||
- Format: "this is realism. this is american traditional this is neotraditional this is cyber sigilism"
|
||||
|
||||
**Notable Connections:**
|
||||
- Brevity respects time and intelligence
|
||||
- Direct statements eliminate confusion
|
||||
- Professional credibility through source citation, not verbose explanation
|
||||
- Practical honesty about complexity instead of false simplification
|
||||
|
||||
## Idea Categorization
|
||||
|
||||
## Idea Categorization
|
||||
|
||||
### Immediate Opportunities
|
||||
*Ideas ready to implement now*
|
||||
|
||||
1. **7th Grade Reading Level Standard**
|
||||
- Description: All content must be understandable without re-reading
|
||||
- Why immediate: Clear communication prevents customer confusion and builds trust
|
||||
- Resources needed: Reading level checker tool, content audit
|
||||
|
||||
2. **Minimal Professional Standard**
|
||||
- Description: Meet but never exceed bare minimum professionalism
|
||||
- Why immediate: Eliminates pretentious language that alienates customers
|
||||
- Resources needed: Style guide with specific examples
|
||||
|
||||
3. **Honest Complexity Acknowledgment**
|
||||
- Description: "Pricing custom tattoos is hard" approach to difficult topics
|
||||
- Why immediate: Builds trust through honesty vs. false simplification
|
||||
- Resources needed: Template responses for complex topics
|
||||
|
||||
### Future Innovations
|
||||
*Ideas requiring development/research*
|
||||
|
||||
1. **Complete Content Transformation System**
|
||||
- Description: LLM filter that transforms any input through United Tattoo rules
|
||||
- Development needed: Training examples, rule weighting, testing protocols
|
||||
- Timeline estimate: 2-3 months development and testing
|
||||
|
||||
2. **Industry-Wide Language Movement**
|
||||
- Description: Influence other tattoo shops to adopt authentic communication
|
||||
- Development needed: Case studies, success metrics, outreach strategy
|
||||
- Timeline estimate: 6-12 months for measurable impact
|
||||
|
||||
### Moonshots
|
||||
*Ambitious, transformative concepts*
|
||||
|
||||
1. **Anti-Corporate Communication Standard for Service Industries**
|
||||
- Description: United Tattoo approach becomes model for all local businesses
|
||||
- Transformative potential: Reshape how small businesses communicate authentically
|
||||
- Challenges to overcome: Corporate marketing industry resistance, scaling personalization
|
||||
|
||||
### Insights & Learnings
|
||||
*Key realizations from the session*
|
||||
|
||||
- Corporate speak exists because people think it sounds professional, but it actually insults customer intelligence
|
||||
- Authentic tattoo shop communication = vulnerability + practicality + justified confidence
|
||||
- The best language guideline is asking "Would a human being actually say this?"
|
||||
- Brevity that respects time and intelligence builds more trust than verbose explanations
|
||||
- Professional credibility comes from source citation and honest complexity acknowledgment, not big words
|
||||
|
||||
## Action Planning
|
||||
|
||||
## Action Planning
|
||||
|
||||
### Top 3 Priority Ideas
|
||||
|
||||
#### #1 Priority: Implement 7th Grade Reading Level Standard
|
||||
- **Rationale:** Immediate impact on all customer communications, eliminates confusion and re-reading
|
||||
- **Next steps:** Audit current website copy, create reading level checklist, rewrite problem areas
|
||||
- **Resources needed:** Reading level checker tool, content inventory spreadsheet
|
||||
- **Timeline:** 2-3 weeks for complete website overhaul
|
||||
|
||||
#### #2 Priority: Create LLM Brand Language Filter
|
||||
- **Rationale:** Scalable solution for all future content creation, prevents regression to corporate speak
|
||||
- **Next steps:** Document all transformation rules with examples, test with current bad copy
|
||||
- **Resources needed:** Rule documentation, before/after examples database, LLM prompt engineering
|
||||
- **Timeline:** 1-2 weeks for initial filter creation and testing
|
||||
|
||||
#### #3 Priority: Transform High-Impact Pages First
|
||||
- **Rationale:** Focus on pages customers see most (pricing, aftercare, artist bios)
|
||||
- **Next steps:** Identify top 5 customer-facing pages, apply rules, A/B test if possible
|
||||
- **Resources needed:** Analytics data for page priority, rewrite time allocation
|
||||
- **Timeline:** 1 week for core page transformation
|
||||
|
||||
## Reflection & Follow-up
|
||||
|
||||
### What Worked Well
|
||||
|
||||
### Areas for Further Exploration
|
||||
|
||||
### Recommended Follow-up Techniques
|
||||
|
||||
### Questions That Emerged
|
||||
|
||||
---
|
||||
|
||||
*Session facilitated using the BMAD-METHOD™ brainstorming framework*
|
||||
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 |