feat: add robotics components and data structures
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions

- Introduced RobotProductCard component for displaying robot details.
- Added WhyUs component highlighting key reasons for choosing our robotics solutions.
- Implemented CursorSpotlight for enhanced user interaction.
- Created GlassPanel for a stylish UI element.
- Developed MotionSection for animated section visibility.
- Added PremiumButton for versatile button options.
- Established data structures for industries and robots, including detailed specifications and use cases.
- Included utility functions for retrieving robots by slug and category.
This commit is contained in:
Najjar\NajjarV02
2026-05-20 17:22:47 +04:00
parent 2eac53dac3
commit 92cf4aba3b
87 changed files with 6887 additions and 1430 deletions
@@ -0,0 +1,236 @@
# Component Rules
Per-element rules. Apply when designing or reviewing the corresponding component.
---
## Buttons
**Primary**
- Gradient gold: `bg-gradient-to-r from-gold-light via-gold to-gold-deep`
- Text: `text-obsidian` (dark on gold for contrast)
- Shape: `rounded-full px-7 py-3.5 text-sm font-medium tracking-wide`
- Shadow: `shadow-[0_10px_40px_-12px_rgba(212,164,55,0.6)]`
- Hover shadow only: `hover:shadow-[0_18px_50px_-12px_rgba(242,194,91,0.8)]`
- Optional `magnetic` wrapper for cursor pull effect on desktop.
**Outline**
- `border border-bone/20 text-bone hover:border-gold/60 hover:text-gold-light`
- Same shape as primary.
**Ghost**
- `text-bone hover:text-gold-light` — no border, no fill.
**Forbidden**
- Bright red CTA buttons (unless brand color is red).
- Square corners on action buttons.
- Solid `#000` or `#fff` backgrounds.
- Loading text inside button that breaks layout — use a spinner icon + word swap with fixed min-width.
- Success/error message **inside** button — render outside as a banner.
---
## Cards
**Standard card**
```tsx
<div className="group relative isolate h-full overflow-hidden rounded-2xl border border-bone/10 bg-graphite/40 p-6 backdrop-blur-sm transition-all duration-500 hover:-translate-y-1 hover:border-gold/30 hover:bg-graphite/60 md:p-7">
{/* hover amber glow (decorative) */}
{/* content */}
{/* gold underline sweep at bottom */}
</div>
```
**Premium / featured card** (used for cert card, sale card, hero CTA)
- `border-gold/25`
- `bg-gradient-to-br from-[#1a1407] via-[#0c0a06] to-[#1e1409]`
- `shadow-[0_30px_120px_-30px_rgba(212,164,55,0.45)]`
- Often has animated `shimmer` hairline on top edge.
**Grid layout**
- `grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-N gap-4 md:gap-5`
- Apply `cardGrid` + `cardCell` variants for staggered entry.
**Inside the card**
- Top row: small icon disc (`size-11 glass-gold`) + optional mono micro-label.
- Heading: `font-display text-2xl md:text-3xl`.
- Body: `text-sm text-mist leading-relaxed`.
- Bottom: optional "Learn more" link or gold underline sweep.
---
## Section heading
```tsx
<div className="flex flex-col items-center gap-6 text-center"> {/* or items-start for left-aligned */}
<SectionLabel index="01">Section name</SectionLabel>
<Reveal>
<h2 className="font-display text-5xl leading-[1.02] text-bone text-balance md:text-6xl">
Plain words and a{" "}
<span className="gold-text">gold accent phrase.</span>
</h2>
</Reveal>
<Reveal delay={0.1}>
<p className="max-w-2xl text-mist text-pretty md:text-lg">
One refined sentence describing the section.
</p>
</Reveal>
</div>
```
**Rules**
- Always `text-balance` on h2 for clean wrapping.
- `text-pretty` on subtitle.
- Use SectionLabel with sequential index per page (01, 02, 03…). No mono font.
- One gold-accent phrase max per heading — overdoing gold cheapens it.
---
## Navbar
- Fixed top: `fixed inset-x-0 top-0 z-50`.
- Logo left, nav center/right, primary CTA + WhatsApp icon right.
- Scrolled state: `backdrop-blur-xl bg-obsidian/70 border-b border-bone/5`.
- Nav links: `text-sm font-medium tracking-[0.005em] text-bone/85 hover:text-gold-light`.
- Active state via `usePathname()` → `text-gold-light` + full-width underline + `aria-current="page"`.
- Mobile: hamburger right, menu drawer below, links close on click.
---
## Footer
**Layout — 4 columns, no duplication**
| Column 1 | Column 2 | Column 3 | Column 4 |
|---|---|---|---|
| Brand + contact | Navigation | Services | Legal & App |
| Logo, tagline, phone, email, WhatsApp, Instagram | Home + all top-level routes | Specific service items | Privacy Policy + 2 store badges |
**Rules**
- No page link appears in two columns.
- Privacy Policy lives only under Legal.
- App badges only in Legal & App column (footer) — official SVGs at 132 px width, 8 px gap.
- External links open in new tab with rel attrs.
- Background: `bg-obsidian` with subtle "LUXAM"-style watermark behind content (translate-y-[18%], opacity 0.025).
- Bottom row: `© YYYY {brand}. All rights reserved.` + tagline + agency credit (subtle, gold-underlined on hover).
---
## Forms
- Field wrapper: `<label>` with small uppercase metadata label + input.
- Input class:
```
peer w-full rounded-xl border border-bone/12 bg-obsidian/60
px-4 py-3.5 text-sm text-bone placeholder-mist/40 outline-none
transition-colors duration-300
focus:border-gold/60 focus:bg-obsidian/80
focus:shadow-[0_0_0_3px_rgba(212,164,55,0.18)]
aria-[invalid=true]:border-[#a3261c]/60
```
- Honeypot field: hidden `<input name="website">` — silently succeed if filled.
- Validation: server-side via `submitContact()` action; client gets `state.fieldErrors` + `state.message`.
- Submission states: `useActionState` + `useFormStatus`. Spinner inside button, success/error banner **outside** button.
---
## Icons
- Source: `lucide-react` (already installed).
- Size: `size-4` inline, `size-5` standalone, `size-[18px]` inside discs.
- StrokeWidth: `1.4` for elegance (not the default 2).
- Wrapper for emphasis: `glass-gold size-11 grid place-items-center rounded-full`.
- **Forbidden**: oversized icons (>32 px) standalone, multi-color icon sets, cartoonish flat icons.
**No lucide WhatsApp glyph** — use custom inline SVG from `snippets/WhatsAppLink.tsx`.
**No lucide Instagram glyph in v1.16** — use custom inline SVG (see footer).
---
## App store badges
- Use **official SVG badges**, not custom-drawn pills with text.
- Sources: svgrepo.com `/show/<id>/<slug>.svg` (downloads clean SVG).
- Store locally in `public/images/app-store-badge.svg` + `public/images/google-play-badge.svg`.
- Render via plain `<img>` (next/image blocks SVG by default in Next 16).
- Width: `132 px` footer, `140-160 px` hero CTA section.
- Hover: `opacity-85` + `-translate-y-0.5`. No gold circles around them.
- Stack with `gap-2` (8 px) in footer.
```tsx
<a href={app.appStore} target="_blank" rel="noopener noreferrer" aria-label="Download Luxam on the App Store"
className="inline-flex transition duration-300 hover:-translate-y-0.5 hover:opacity-85">
<img src="/images/app-store-badge.svg" alt="Download on the App Store"
className="h-auto w-[132px] max-w-full" loading="lazy" />
</a>
```
---
## WhatsApp link
- URL form: `https://api.whatsapp.com/send/?phone=<digits-only>&text&type=phone_number&app_absent=0`
- Phone in URL: country code + number, **no `+` or spaces**.
- Display phone format: `+CC XX XXX XXXX`.
- Anchor: `target="_blank" rel="noopener noreferrer" aria-label="Chat with [Brand] on WhatsApp"`.
- Icon: inline SVG (lucide has no WhatsApp glyph). Refined gold tint, not bright green.
- Two surfaces: nav (circular icon button) + footer (icon + display number).
- Optional third surface: contact page row.
---
## Client logo section
- Horizontal marquee, `animation: marquee 50s linear infinite`.
- Hover pauses via `onMouseEnter/Leave` setting `animationPlayState`.
- Duplicate the data array `[...clients, ...clients]` for seamless loop.
- Tiles: `h-24 w-44 sm:h-28 sm:w-52 rounded-2xl border border-bone/10 bg-graphite/40 backdrop-blur-sm`.
- Logos: `object-contain max-h-14 sm:max-h-16`. Default `grayscale opacity-70`. Hover: `grayscale-0 opacity-100 scale-105`.
- Edge fade via `mask-image: linear-gradient(90deg, transparent, black 8%, black 92%, transparent)`.
- Reduced-motion fallback: render a static `grid-cols-2 sm:grid-cols-3 lg:grid-cols-6`.
---
## Certificate gallery
- 4-col on desktop, 2-col tablet, 1-col mobile.
- Each tile: portrait `aspect-[3/4]` with `next/image fill object-cover object-top`.
- Top-left badge: page indicator (`Page 1`, `Page 2 · Annex`).
- Top-right: ArrowUpRight disc (links open in new tab).
- Click anywhere on tile opens the PDF.
- Hover: scale 1.04 (1.2s ease-out), gold ring border, amber radial glow, caption title → gold-light.
- Caption below image: `font-mono` micro-label (document name in gold) + title + "Open PDF" CTA.
- Optional "Need the full PDFs?" pill row below grid for direct downloads.
---
## Final CTA section
- Wraps inside a premium gold-bordered glass card.
- Animated halo (`!reduce && motion.div opacity pulse`).
- Grid: `lg:grid-cols-12` (8/4 split).
- Left col: SectionLabel + h2 with one gold-accent phrase + paragraph.
- Right col: 2 buttons (`primary` + `outline`).
- Place at the end of every long page, before the footer.
---
## Privacy / Legal pages
- Single-column max-w-3xl reading layout.
- Each section gets `id="..."` + `scroll-mt-28` for anchored navigation.
- `font-display` h2 per section, `text-mist` body, `text-pretty` on every `<p>`.
- Effective date + `metallic-divider` at top.
- Bottom disclaimer: `text-sm text-mist/70` with mailto link.
---
## Don't
- Don't add hover effects without `transition-*` for smoothness.
- Don't use `transform-origin` defaults on rotated icon discs — keep them small (8-12°).
- Don't use `whileHover={{ scale: 1.1 }}` on cards — too theatrical. Use `-translate-y-1` instead.
- Don't add box-shadows with multiple layers and bright colors. One subtle amber glow max.
- Don't introduce a new font family per page — stick to display + sans.
- Don't render placeholder images (`via.placeholder.com`, gray boxes) in production code. Always use real assets the user provided.
@@ -0,0 +1,45 @@
# Component Snippets
Overview of the reusable TSX snippets in `snippets/`. Each is a copy-paste base for a new project — adapt brand tokens + content via your own `lib/content.ts`.
| File | Purpose | Depends on |
|---|---|---|
| `PremiumButton.tsx` | Gold-gradient primary + outline + ghost variants, optional magnetic wrapper | `lib/cn`, lucide ArrowUpRight |
| `SectionHeading.tsx` | Premium section heading w/ eyebrow + serif h2 + sub paragraph | `lib/cn`, framer Reveal |
| `GlassCard.tsx` | Dark glass card w/ hover lift, amber glow, gold underline sweep | lucide LucideIcon |
| `Footer.tsx` | 4-column footer w/ no duplication, watermark, agency credit | brand contact data |
| `AppBadges.tsx` | Official App Store + Google Play badge anchors (plain `<img>`) | `/public/images/*-badge.svg` |
| `WhatsAppLink.tsx` | Refined gold WhatsApp icon button + label variant | inline SVG (no lucide glyph) |
| `ClientLogoMarquee.tsx` | Auto-scroll logo marquee w/ hover pause + reduced-motion grid fallback | logos array, marquee keyframe in globals.css |
| `CertificateGallery.tsx` | 4-image bento gallery linking to PDFs in new tab | `next/image`, certs array |
## How to use
1. Copy the file you need into your project's `src/components/` (or wherever your component layer lives).
2. Update import paths (`@/lib/cn`, `@/components/ui/...`) to match your project alias.
3. Wire data: the snippets accept props or read from a `lib/content.ts` you control.
4. Confirm Tailwind v4 `@theme` tokens (`obsidian`, `gold`, `bone`, `mist`, etc.) exist — see `DESIGN_SYSTEM.md`.
5. Confirm Framer Motion + lucide-react installed:
```bash
npm i framer-motion lucide-react
```
## Adapt patterns
The snippets show the **shape**. Your brand may need:
- Different palette (warm copper instead of gold? `--color-gold` → your token, keep the same usage rules).
- Different copy hierarchy (more / fewer columns in footer).
- Different surfaces (e.g., add a Resources column).
Keep the **interaction grammar** consistent:
- Hover lift `-translate-y-1`.
- Amber/gold glow bottom-right.
- Underline sweep on cards.
- 1.4 strokeWidth on icons.
- Serif on h1/h2 only.
That's what makes any palette feel "Luxam-class" premium.
## Don't fork without reason
If a snippet does 95% of what you need, **wrap** it with project-specific props rather than forking. Forks drift.
@@ -0,0 +1,204 @@
# Design System
Reusable design tokens + scales extracted from the Luxam reference build. Drop into `globals.css` (Tailwind v4 `@theme`) of any new project to start with the same DNA.
---
## Color palette
```css
@theme {
/* Backgrounds — obsidian → graphite layering */
--color-obsidian: #08080A; /* page background */
--color-graphite: #15161A; /* dark glass card */
--color-graphite-2: #1C1D22; /* slightly lifted card variant */
/* Foreground */
--color-bone: #F4F0E6; /* primary text on dark */
--color-mist: #C7C2B5; /* secondary/muted text */
/* Brand — gold spectrum (use sparingly + structurally) */
--color-gold: #D4A437; /* core brand gold */
--color-gold-light: #F2C25B; /* hover, highlight, gradient stops */
--color-gold-deep: #7A4A12; /* gradient base */
--color-ember: #B5731A; /* warm secondary */
/* Sustainability accent (use only where ecology theme applies) */
--color-emerald: #1F8F6B;
--color-emerald-glow: #3FD8A4;
}
```
**Usage rules**
- **Background:** `bg-obsidian` body, `bg-graphite/40 backdrop-blur-sm` cards.
- **Borders:** `border-bone/10` default, `border-gold/30` on hover, `border-gold/40` active.
- **Text:** `text-bone` titles, `text-mist` body/secondary, `text-gold-light` brand accents.
- **Never use** raw `#fff` or pure `#000`. Always `bone` or `obsidian`.
- **Gradients:** single-axis brand gradient `from-gold-light via-gold to-gold-deep`. No rainbow.
---
## Typography
```ts
// app/layout.tsx
import { Cormorant_Garamond, Geist, Geist_Mono } from "next/font/google";
const display = Cormorant_Garamond({
subsets: ["latin"],
variable: "--font-display",
weight: ["400", "500", "600"],
display: "swap",
});
const sans = Geist({ subsets: ["latin"], variable: "--font-sans", display: "swap" });
const mono = Geist_Mono({ subsets: ["latin"], variable: "--font-mono", display: "swap" });
```
**Hierarchy**
| Role | Font | Size |
|---|---|---|
| Hero H1 | `font-display` (serif) | `text-[clamp(2.6rem,7vw,6.4rem)] leading-[1.0] tracking-[-0.02em]` |
| Section H2 | `font-display` | `text-[clamp(2.4rem,5vw,4.6rem)] leading-[1.02] tracking-[-0.02em]` |
| Card H3/H4 | `font-display` | `text-2xl md:text-3xl` |
| Body | `font-sans` (inherited default) | `text-base md:text-lg leading-[1.7]` |
| UI label | `font-sans` | `text-[10px] md:text-[11px] font-medium uppercase tracking-[0.22em]` |
| Nav link | `font-sans` | `text-sm font-medium tracking-[0.005em]` |
| Stats number | `font-display` | `text-5xl md:text-6xl` (responsive smaller on mobile) |
**Avoid**
- `font-mono` on long values or body text.
- `tracking-[0.3em]+` on small labels.
- `leading-[0.95]` on serif headlines (clips ascenders).
**Helper class** for one-color brand text:
```css
.gold-text {
background: linear-gradient(90deg, #FFEFC5, #F2C25B 40%, #D4A437 80%, #7A4A12);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
```
---
## Spacing scale
Section padding lives **only** on the `Section` primitive:
```tsx
// components/ui/Section.tsx
className={cn("relative scroll-mt-24 py-16 md:scroll-mt-28 md:py-20 lg:py-24", className)}
```
- Mobile: `py-16` (64 px)
- Tablet: `md:py-20` (80 px)
- Desktop: `lg:py-24` (96 px)
- All anchored sections get `scroll-mt-24 md:scroll-mt-28` for sticky-nav clearance.
**Don't** override per-section unless there's a structural reason. Centralize.
Card padding: `p-6 md:p-7` standard, `p-7 md:p-10` for hero/feature cards.
Grid gaps: `gap-4 md:gap-5` standard, `gap-6 md:gap-8` for large cards.
---
## Border radius scale
```css
--radius-xs: 4px;
--radius-sm: 8px;
--radius-md: 14px;
--radius-lg: 22px;
--radius-xl: 32px;
```
Tailwind shortcuts to match: `rounded-2xl` (cards), `rounded-3xl` (hero panels), `rounded-full` (pills + buttons).
---
## Motion
```ts
// shared variants
const cardGrid: Variants = {
hidden: {},
visible: { transition: { staggerChildren: 0.08, delayChildren: 0.15 } },
};
const cardCell: Variants = {
hidden: { opacity: 0, y: 24, filter: "blur(6px)" },
visible: {
opacity: 1, y: 0, filter: "blur(0px)",
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] },
},
};
```
- Entrance ease: `[0.16, 1, 0.3, 1]` (cubic-bezier "out-quart-ish").
- Duration: hero 1.3s, cards 0.8s, hover 0.3-0.5s.
- Hover lift: `-translate-y-1` max. No `-translate-y-2`+ (jumpy).
- Halo pulse: `[0.5, 0.85, 0.5]` opacity, 6s loop. Wrap in `!reduce && (...)`.
- `useReducedMotion()` must be read in any animated component.
**Forbidden**
- Bouncy springs (`stiffness > 200`) on entrance.
- Scale jumps > 1.05 on hover for cards.
- Parallax > 200 px translate range.
- Marquees without `mask-image` edge fade.
---
## Visual textures
| Class | Purpose |
|---|---|
| `noise-bg` | Subtle film-grain background overlay |
| `grain` | Animated grain (use sparingly on cards) |
| `metallic-divider` | Thin gold-tinted divider line |
| `radial-gold` | Faint amber radial wash (positioned absolute, blur-3xl) |
| `radial-emerald` | Same, emerald variant |
| `hairline-t`, `hairline-b` | 1 px top/bottom hairline borders |
| `mask-fade-y`, `mask-fade-x` | Edge fade masks |
Define these as utilities in `globals.css` per the Luxam reference.
---
## Hover state pattern
Every card/button uses the same hover vocabulary:
1. `-translate-y-1` lift (cards) or `-translate-y-0.5` (buttons).
2. Border step up: `border-bone/10 → border-gold/30 (or /35 /40)`.
3. Background step up: `bg-graphite/40 → bg-graphite/60`.
4. Amber radial glow bottom-right: `radial-gradient(circle, rgba(212,164,55,0.28), transparent 70%)` blurred.
5. Gold underline sweep (cards): `absolute bottom-0 left-0 h-px w-0 ... group-hover:w-full`.
6. Optional: top hairline gold gradient fade-in.
7. Icon disc gets `rotate-[8deg]` + amber halo shadow.
Apply via `group` + `group-hover:` Tailwind variants — never per-element JS listeners.
---
## Container widths
```tsx
// components/ui/Container.tsx — three variants
size="default" → max-w-7xl
size="wide" → max-w-[1480px]
size="narrow" → max-w-3xl
```
All variants: `mx-auto px-6 md:px-10`.
---
## When in doubt
- More dark, less gold.
- More space, less content.
- Less motion, more restraint.
- Real photography > illustration.
- Editorial typography > corporate sans.
@@ -0,0 +1,274 @@
# Prompt Templates
Copy-paste prompts for common tasks. Replace `{{placeholders}}` with project specifics.
---
## 1. Create homepage from scratch
```
Act as a senior frontend architect.
Build a premium, cinematic, dark/gold luxury homepage for {{BRAND}} ({{INDUSTRY}}) using the design DNA in .claude/skills/premium-frontend-designer/.
Stack: Next.js 16 App Router · TypeScript · Tailwind v4 · Framer Motion · lucide-react.
Composition (top → bottom):
1. Nav (fixed, scroll-aware)
2. Hero (min-h-100svh, parallax halos, 2 CTAs, eyebrow + serif h1 + sub + body)
3. BrandStrip (marquee of capability keywords)
4. About / Story (split 5/7, copy + visual)
5. WhyUCO-style problem→solution comparison
6. Services overview (6 cards)
7. Trusted Clients marquee (use ONLY logos I provide)
8. Stats / Impact metrics (animated counters)
9. Cinematic reel (lazy video)
10. Process / Closed loop (timeline)
11. Industries served
12. Certifications
13. Gallery (4-image bento)
14. Final CTA section
15. Footer (4 columns, no duplication)
Brand details I will provide:
{{BRAND_LEGAL_NAME, PHONE, EMAIL, WHATSAPP_URL, INSTAGRAM, APP_STORE, GOOGLE_PLAY, AGENCY_CREDIT}}
Do NOT invent links. Ask if missing.
```
---
## 2. Redesign an existing page
```
Inspect {{PAGE_ROUTE}}. Keep the content + intent but rebuild it to match the design DNA in .claude/skills/premium-frontend-designer/.
Requirements:
- Reuse existing primitives (Section, Container, Button, Reveal, SectionLabel, etc.) — don't duplicate.
- Apply the cardGrid + cardCell motion variants for grid entrances.
- Add a single gold-accent phrase per h2.
- Centralize section padding via the Section primitive (no per-page overrides).
- Run typecheck + grep for placeholder hrefs before reporting done.
Return a clear file change list with NEW / MODIFIED markers.
```
---
## 3. Add a polished footer
```
Build a 4-column premium footer for {{BRAND}}.
Columns:
1. Brand + contact (logo, tagline, phone, email, WhatsApp, Instagram)
2. Navigation (top-level pages)
3. Services (specific service items)
4. Legal & App (Privacy Policy + 2 store badges)
No duplicate link labels across columns. External links open in new tab.
Footer bottom row: copyright + tagline + subtle agency credit (gold underline on hover).
Watermark: project name in serif, large, opacity-0.025, contained inside footer overflow.
Brand details:
{{PHONE, EMAIL, WHATSAPP_URL, INSTAGRAM_URL, APP_STORE_URL, GOOGLE_PLAY_URL, AGENCY_CREDIT_URL}}
```
---
## 4. Add WhatsApp button
```
Add a refined WhatsApp button to:
1. Footer contact stack (icon + label "WhatsApp · {{DISPLAY_NUMBER}}").
2. Nav (circular icon-only button next to primary CTA, mobile menu pill).
3. Contact page info card (full-width tile w/ icon disc).
URL: {{WHATSAPP_API_URL}}
Display number: {{+CC XX XXX XXXX}}
Use inline SVG (lucide has no WhatsApp glyph). Refined gold tint, not bright green. Hover: gold border + soft glow + scale 1.04. aria-label="Chat with {{BRAND}} on WhatsApp". target="_blank" rel="noopener noreferrer".
```
---
## 5. Add app store badges
```
Add official App Store + Google Play badge SVGs.
URLs:
- App Store: {{APP_STORE_URL}}
- Google Play: {{GOOGLE_PLAY_URL}}
Tasks:
1. Download from svgrepo.com (use /show/ path not /download/):
- https://www.svgrepo.com/show/303128/download-on-the-app-store-apple-logo.svg
- https://www.svgrepo.com/show/303139/google-play-badge-logo.svg
2. Save to public/images/app-store-badge.svg + public/images/google-play-badge.svg.
3. Render via plain <img> (next/image blocks SVG in Next 16).
4. Width: 132 px footer / 140-160 px in hero CTA section.
5. Hover: opacity-85 + -translate-y-0.5. No gold circles.
6. Stack gap-2 (8 px) in footer.
7. aria-label on every anchor. target="_blank" rel="noopener noreferrer".
Use the snippet from snippets/AppBadges.tsx as a starting point.
```
---
## 6. Add client logo section
```
Add a premium "Trusted by leading {{INDUSTRY}} partners" client logo marquee to the homepage (after Services) and About page (after process).
Use ONLY these 12 logos I provide:
{{LIST OF LOGO URLS}}
Tasks:
1. Download each into public/images/clients/ with clean filenames.
2. Create src/lib/clients.ts with typed array { src, alt }.
3. Use recognisable brand names for alt text; for unidentified logos use "{{BRAND}} client logo".
4. Marquee: 50s linear infinite, hover-pause, mask-image edge fade.
5. Tile: h-24 w-44 mobile / h-28 w-52 tablet+. Default grayscale + opacity 70%, hover full color + scale 1.05.
6. Reduced-motion fallback: render a static grid-cols-2 sm:grid-cols-3 lg:grid-cols-6.
Use snippets/ClientLogoMarquee.tsx as the base.
```
---
## 7. Add certificate gallery
```
Replace the document-card list on /certifications with a 4-image gallery showing the actual certificate pages.
Use these exact images (download to public/images/certifications/):
{{IMG1_URL, IMG2_URL, IMG3_URL, IMG4_URL}}
Each image links to the corresponding PDF (already in public/documents/certifications/).
Layout: sm:grid-cols-2 lg:grid-cols-4 with aspect-[3/4] portrait tiles.
Tile chrome:
- Top-left page badge (Page 1 / Page 2 · Annex).
- Top-right ArrowUpRight disc.
- Bottom caption: mono category in gold + serif title + "Open PDF" CTA.
- Hover: scale 1.04, gold ring border, amber radial glow, gold underline sweep.
External: target="_blank" rel="noopener noreferrer" on every anchor.
Optional: secondary "Need the full PDFs?" pill row below grid with direct download links.
Use snippets/CertificateGallery.tsx as a starting point.
```
---
## 8. Fix mobile responsiveness
```
The {{PAGE}} layout breaks on mobile. Specific issues:
{{ISSUES_LIST}}
Walk through the responsive checklist in .claude/skills/premium-frontend-designer/RESPONSIVE_CHECKLIST.md and fix every applicable item.
Test at 320 / 375 / 390 / 414 / 768 / 1024 viewports.
Don't redesign — only fix layout, spacing, overflow, clipping. Preserve the visual identity.
Return a table of issues + fixes (Before / After columns).
```
---
## 9. Polish typography sitewide
```
Audit and refine typography across all pages:
1. Hero h1: clamp(2.4rem, 7vw, 6.4rem), leading-[1.0], tracking-[-0.02em]. Add pt buffer for sticky-nav clearance.
2. Two-font system: serif for h1/h2 only, clean sans (Geist/Inter) for everything else.
3. Remove font-mono from long values + body text. Keep only on tiny brand-pattern eyebrow tags if at all.
4. Tracking: reduce all tracking-[0.32em+] on small labels to tracking-[0.22em].
5. Nav links: text-sm font-medium tracking-[0.005em] (slightly larger + medium weight, near-zero tracking).
6. BrandStrip marquee: smaller (text-xs md:text-sm uppercase) so it doesn't compete with hero.
7. Section labels via SectionLabel primitive — never inline.
8. text-balance on h1/h2, text-pretty on body paragraphs.
Confirm:
- typecheck clean
- HTTP 200 on every route
- zero tracking-[0.32em] remaining
- no font-mono on user-facing body content
```
---
## 10. Add Privacy Policy
```
Create /privacy-policy route matching the premium dark/gold style.
9 sections with id="..." anchors + scroll-mt-28:
- introduction
- information-we-collect
- how-we-use-information
- contact-form-data
- cookies-and-analytics
- data-protection
- third-party-services
- user-rights
- contact-information
Use brand contact details from lib/content.ts.
Effective date stamp at top + metallic-divider. Bottom disclaimer with mailto link in gold underline.
Single column max-w-3xl. font-display h2 per section. text-pretty body.
Don't write fake legal claims. General professional language only. Tell users they can contact for privacy questions.
Add to footer Legal column.
```
---
## 11. Coolify deploy via Dockerfile
```
Deploy {{PROJECT}} to Coolify with a Dockerfile build pack.
Tasks:
1. Add `output: "standalone"` to next.config.ts.
2. Create multi-stage Dockerfile (node:22-alpine, 3 stages: deps → build → run).
3. Add .dockerignore (skip .git, node_modules, .next cache, env files, .claude).
4. Run-stage as non-root user (UID 1001).
5. EXPOSE 3000, CMD ["node", "server.js"] using standalone output.
Then via Coolify API:
- PATCH application: build_pack="dockerfile", dockerfile_location="/Dockerfile", ports_exposes="3000", domains="https://{{DOMAIN}}"
- POST /api/v1/deploy?uuid={{APP_UUID}}&force=true
- Poll deployment until terminal status.
If "no available server" appears on the domain after deploy: verify DNS resolves to Coolify host IP, then redeploy to trigger Let's Encrypt cert issuance.
```
---
## 12. General "make this premium" polish
```
Inspect {{COMPONENT_OR_PAGE}}. Identify what doesn't match the premium dark/gold luxury direction in .claude/skills/premium-frontend-designer/DESIGN_SYSTEM.md.
Specifically check:
- Are buttons using premium variants (gold gradient primary, outline secondary)?
- Are cards using the hover lift + amber glow + gold underline sweep pattern?
- Is there any cheap iconography or stock-photo placeholder?
- Are headings using the serif font with one gold-accent phrase?
- Is spacing centralized via Section primitive?
- Is motion subtle (no bounces, no scale > 1.05 on hover)?
Apply only the changes needed — don't rewrite working sections. Return a list of refinements made.
```
@@ -0,0 +1,108 @@
# Responsive Checklist
Run through every item before declaring a page done.
## Viewports to verify
- **320 px** (iPhone SE 1st gen, smallest practical)
- **375 px** (iPhone SE 2/3, iPhone 13 mini)
- **390 px** (iPhone 14/15/16 default)
- **414 px** (iPhone Plus)
- **768 px** (iPad portrait, `md:` breakpoint)
- **1024 px** (iPad landscape / small laptop, `lg:` breakpoint)
- **1440 px** (full laptop)
## Global guards
- [ ] `body { overflow-x: hidden }` in `globals.css`.
- [ ] No `min-w-[...]` on root containers.
- [ ] All decorative absolute elements live inside `relative isolate overflow-hidden` parents.
- [ ] `max-w-*` containers always paired with `mx-auto px-6 md:px-10`.
## Header / Nav
- [ ] Logo left, menu/CTA right on mobile.
- [ ] Hamburger button is `size-10` minimum (touch target).
- [ ] Mobile menu drawer closes when a link is clicked.
- [ ] No sticky-nav overlap on anchored sections — `scroll-mt-24 md:scroll-mt-28` on `<Section>`.
- [ ] Logo height responsive: `h-11 sm:h-14 md:h-16`.
## Hero
- [ ] H1 clamp: `clamp(2.4rem, 7vw, 6.4rem)` — never larger upper bound.
- [ ] `leading-[1.0]` on serif H1 (not `0.95` — clips ascenders).
- [ ] Top padding generous: `pt-32 sm:pt-36 md:pt-44` (clears sticky nav + breathing room).
- [ ] CTAs use `flex flex-wrap items-center gap-4` so they stack on narrow screens.
- [ ] Stats panel: collapse 1-col-tall mobile layouts to a horizontal 3-col compact row.
## Cards & grids
- [ ] Mobile: `grid-cols-1`. Tablet: `sm:grid-cols-2`. Desktop: `lg:grid-cols-N` matched to card count.
- [ ] Avoid `lg:grid-cols-3` for a 5-card list (leaves orphan) — use 5 or 2-row layout matched to count.
- [ ] Aspect-ratio used over fixed heights: `aspect-[4/3]` or `aspect-[3/4]`.
## Typography
- [ ] No tracking > `0.22em` on small labels.
- [ ] `text-balance` on h1/h2.
- [ ] `text-pretty` on body paragraphs.
- [ ] Long tokens (cert numbers, emails) use `break-all` or `break-words` to wrap.
## Images
- [ ] `next/image` with `sizes` attribute matched to layout.
- [ ] Hero LCP image gets `priority`.
- [ ] `object-cover` only when crop is visually intentional.
- [ ] `object-contain` for logos.
- [ ] Alt text meaningful — never "image" or filename.
## Forms
- [ ] Field grid: `grid gap-5 sm:grid-cols-2` for first/last + email/phone.
- [ ] Inputs `text-base` (16 px+) on mobile to prevent iOS zoom-on-focus.
- [ ] Submit button respects pending state w/ spinner.
- [ ] Field errors render below field, not in a popup.
- [ ] Success/error banner renders outside button.
## Motion
- [ ] `useReducedMotion()` checked in every component using motion props.
- [ ] Heavy halos / parallax skipped when reduced-motion is on.
- [ ] Marquee paused when reduced-motion is on (or replaced with grid).
## External links
- [ ] All `<a target="_blank">` carry `rel="noopener noreferrer"`.
- [ ] WhatsApp / Instagram / store badges have `aria-label`.
- [ ] App badges sized 132 px (footer) / 140-160 px (CTA).
## Footer
- [ ] No duplicated link labels across columns.
- [ ] Privacy Policy only in Legal column.
- [ ] Agency credit row sits below copyright, subtle.
- [ ] Watermark contained via `overflow-hidden` on footer root.
## Accessibility
- [ ] Every interactive element keyboard-reachable.
- [ ] `focus-visible:` rings on buttons, inputs, anchors.
- [ ] Modal/lightbox: scroll-locked body + `Escape` closes + arrows navigate.
- [ ] Color contrast checked (`text-bone` on obsidian = 16:1).
## Final smoke test
```bash
npx tsc --noEmit # clean
curl -s -o NUL -w "%{http_code}" http://localhost:3000/... # 200 on every route
```
Grep for placeholder leaks:
```bash
grep -rn 'href="#"' src/
grep -rn 'href=""' src/
grep -rn 'href="/#' src/
```
Should all return zero (unless intentional same-page anchors with matching IDs).
@@ -0,0 +1,143 @@
---
name: premium-frontend-designer
description: Build, redesign, and polish premium dark/gold luxury websites with React/Next.js/Tailwind. Use when the user asks to create a website, redesign a page, polish a section, fix mobile responsiveness, add premium components (hero, footer, CTA, client logos, certificates, app badges, WhatsApp button), or apply the Luxam-style luxury design DNA to another project.
---
# Premium Frontend Designer & Implementation Agent
You are a senior frontend architect + UI/UX designer + implementation engineer.
Your job: create production-ready, premium, cinematic, dark/gold luxury websites that feel high-end, trustworthy, and conversion-focused — the way the Luxam reference site does.
This skill captures the design DNA, component patterns, prompt templates, and snippets so any future project can match the same quality bar.
---
## When to use this skill
Auto-trigger whenever the user:
- asks for a "premium", "luxury", "cinematic", "dark/gold", or "Luxam-style" design
- creates or redesigns a page (homepage, about, services, contact, gallery, certifications, purchase-sale)
- polishes typography, spacing, or motion
- fixes mobile responsiveness
- adds: WhatsApp link, app store buttons, client logos, certificate gallery, premium hero, premium footer
- imports brand contact details into a layout
- wants reusable React/Tailwind components in the Luxam style
Refuse silently if the project explicitly uses a different design language (e.g. bright corporate, neobrutalist, retro 8-bit). In that case ask before applying this skill's DNA.
---
## Visual principles
1. **Dark cinematic background** — obsidian-class blacks (`#08080A`), graphite layering (`#15161A` / `#1C1D22`). Never pure white. Never bright primary colors as backgrounds.
2. **Gold/amber accents** — restrained, structural, single-tone gradient. Use on numbers, labels, dividers, hover states, hero highlights. Never bright yellow. Never multi-color rainbow gradients.
3. **Editorial typography pairing**
- Serif (Cormorant Garamond / Playfair / Libre Baskerville) for hero H1 + section H2 only.
- Sans (Geist / Inter / Manrope / Satoshi) for nav, body, buttons, labels, metadata.
- No monospace on user-facing copy or long values.
4. **Glass + grain** — semi-transparent `bg-bone/[0.03]` cards with `backdrop-blur`. Subtle noise/grain overlays to break flat surfaces.
5. **Cinematic motion** — Framer Motion: slow, restrained. `cubic-bezier(0.16, 1, 0.3, 1)`, 0.8-1.2s entrance, `useReducedMotion()` respected.
6. **Responsive from the start** — every layout works at 320 / 375 / 768 / 1024 / 1440 viewports.
7. **No corporate basic** — no boxy stock layouts, no flat colorful cards, no generic stock-photo hero, no cheap icon dumps.
Read `DESIGN_SYSTEM.md` for the full token palette + typography clamps.
---
## Coding principles
1. **Inspect before changing.** Always read the existing files, lockfile, framework version, and existing primitives. Reuse — don't duplicate.
2. **Preserve project identity.** Don't rewrite the whole brand for a small request.
3. **Real content only.** Never invent links, social handles, certifications, addresses, phone numbers, or legal claims. Ask if missing.
4. **External links** carry `target="_blank"` + `rel="noopener noreferrer"`.
5. **Reusable primitives first.** Look for existing `Section`, `Container`, `Button`, `Reveal`, `SectionLabel`, `GlassCard` before writing new ones.
6. **Clean imports.** Remove unused icons / helpers after edits.
7. **One source of truth** for spacing — push to a shared `Section` primitive instead of overriding per page.
8. **Typecheck after every change.** `npx tsc --noEmit` must pass.
9. **No backwards-compat shims.** Don't leave `// removed` comments, dead aliases, or "for future" stubs.
10. **No commented-out code** in committed work.
11. **No emojis** in code or files unless user asks.
Read `COMPONENT_RULES.md` for per-element rules (buttons, cards, footer, forms, icons).
---
## Accessibility requirements
- All anchors have meaningful text or `aria-label`.
- All interactive elements reachable by keyboard.
- Focus-visible states: `focus-visible:border-gold/60 focus-visible:ring-2 focus-visible:ring-gold/40`.
- Modal/lightbox: `role="dialog" aria-modal="true"`, `Escape` closes, scroll-lock on body.
- Decorative SVGs: `aria-hidden`.
- Color contrast: body text `text-bone` (≈ #F4F0E6) on obsidian = 16:1 ratio, well above WCAG AA.
- `prefers-reduced-motion` respected via Framer's `useReducedMotion()` — disable transforms/parallax, keep opacity fades.
---
## Responsive requirements
Test mentally (or in browser) at: **320, 375, 390, 414, 768, 1024, 1440**.
- Hero h1 clamp: `clamp(2.4rem, 7vw, 6.4rem)` with `leading-[1.0]`.
- Section padding: `py-16 md:py-20 lg:py-24` (uniform).
- Anchored sections: `scroll-mt-24 md:scroll-mt-28` for sticky-nav clearance.
- `body { overflow-x: hidden }` in `globals.css` as a global guard.
- Sticky decorations (halos, watermarks, blurs) must not introduce horizontal scroll — use `overflow-hidden` on parent + `translate` instead of fixed widths.
- Mobile cards stack `grid-cols-1 sm:grid-cols-2 lg:grid-cols-N`.
Read `RESPONSIVE_CHECKLIST.md` before shipping any page.
---
## Output requirements
Every meaningful change returns:
1. **Files changed** — explicit list with `NEW` / `MODIFIED` / `DELETED` markers.
2. **What was kept** vs **what was replaced** when redesigning.
3. **Receipts** — `HTTP 200`, typecheck clean, key content rendered (grep output).
4. **Responsive breakdown** — desktop / tablet / mobile behavior.
5. **Confirmations** — explicit yes/no on every checklist item the user asked.
6. **No placeholder leaks** — confirm zero `href="#"`, `href=""`, `href="/#"` remain in modified files.
---
## QA checklist (run before reporting done)
```
[ ] npx tsc --noEmit clean
[ ] dev server HTTP 200 on every modified route
[ ] grep zero placeholder hrefs in modified files
[ ] grep zero hardcoded brand values that should be in lib/content.ts
[ ] mobile (<640px) no horizontal overflow, hero not clipped, CTAs not cut
[ ] tablet (≥640 <1024) grids transition cleanly to 2-col
[ ] desktop (≥1024) spacing balanced, no orphan cards
[ ] external links target="_blank" + rel="noopener noreferrer"
[ ] focus-visible visible on every button/anchor
[ ] reduced-motion heavy parallax/scale animations disabled
[ ] no leftover console.log or commented-out code
```
---
## Brand-specific config (Luxam reference)
`examples/luxam-config.md` holds the exact Luxam contact details, WhatsApp URL, app store links, agency credit, etc. **Do not copy these values into other projects** — use them only as the shape pattern. Each new project gets its own `lib/content.ts` populated from real values the user provides.
---
## File map of this skill
```
SKILL.md — this file (entry point, principles, when to use)
DESIGN_SYSTEM.md — color tokens, typography, spacing scale, motion
COMPONENT_RULES.md — per-component rules (buttons, cards, footer, forms, icons)
RESPONSIVE_CHECKLIST.md — pre-ship checklist for every viewport
PROMPT_TEMPLATES.md — copy-paste prompts for common tasks
COMPONENT_SNIPPETS.md — high-level component overview
snippets/ — 8 reusable TSX component files
examples/luxam-config.md — Luxam-specific values (reference, not boilerplate)
```
Read the relevant section(s) for the task at hand. Don't dump the whole skill into the user's reply — surface only the rules + snippets relevant to the change being made.
@@ -0,0 +1,118 @@
# Luxam Reference Config
Concrete values used in the Luxam project. **Reference only** — do not paste into other projects unless the user explicitly says these are also their values.
## `src/lib/content.ts`
```ts
export const brand = {
name: "LUXAM",
legalName: "Luxam Oils Trading LLC",
tagline: "Sustainable Energy. Refined.",
promise: "Converting waste into the fuel of tomorrow.",
phone: "04 262 3314",
email: "info@luxam.ae",
location: "United Arab Emirates",
instagram: "@luxamtrading",
};
export const nav = [
{ label: "Home", href: "/" },
{ label: "About us", href: "/about-us" },
{ label: "Services", href: "/services" },
{ label: "Purchase & Sale", href: "/purchase-sale" },
{ label: "Gallery", href: "/gallery" },
{ label: "Certifications", href: "/certifications" },
{ label: "Contact us", href: "/contact-us" },
];
export const socials = {
instagram: "https://www.instagram.com/luxamtrading/",
whatsapp:
"https://api.whatsapp.com/send/?phone=971527312190&text&type=phone_number&app_absent=0",
whatsappDisplay: "+971 52 731 2190",
};
export const app = {
googlePlay: "https://play.google.com/store/apps/details?id=com.lootah.luxam",
appStore: "https://apps.apple.com/ae/app/luxam/id6740183015",
};
export const credit = {
agency: "YS Lootah Tech",
url: "https://yslootahtech.com/",
};
```
## Footer link cluster
```ts
const navigationLinks = [
{ label: "Home", href: "/" },
{ label: "About us", href: "/about-us" },
{ label: "Services", href: "/services" },
{ label: "Purchase & Sale", href: "/purchase-sale" },
{ label: "Gallery", href: "/gallery" },
{ label: "Certifications", href: "/certifications" },
{ label: "Contact us", href: "/contact-us" },
];
const serviceItems = [
{ label: "UCO Collection", href: "/services" },
{ label: "Logistics & Handling", href: "/services" },
{ label: "Recycling & Feedstock", href: "/services" },
{ label: "Compliance & Documentation", href: "/certifications" },
{ label: "Partner with us", href: "/contact-us" },
];
const legalLinks = [{ label: "Privacy Policy", href: "/privacy-policy" }];
```
## Deployment (Coolify)
```
Coolify project luxam (uuid h0840c84kgo0g4cgwg0os88c)
env production uuid s4ow44cwgo08g88g04gwkkcs
Application uuid zc4s4k4k0c44c8cs8oc8so4k
build_pack dockerfile
dockerfile_location /Dockerfile
ports_exposes 3000
fqdn https://luxamnew.yslootahtech.com
DNS A record luxamnew → 51.255.193.254 (DNS only)
```
Git remote:
```
origin https://git.devxsupport.com/mohammad/luxam-web.git (main)
```
Token policy: never commit. Use `COOLIFY_TOKEN` env var when scripting deploys.
## Dockerfile pattern
Multi-stage:
1. `deps` (node:22-alpine, `apk add libc6-compat`, `npm ci`)
2. `build` (`next build` w/ `output: "standalone"`)
3. `run` (non-root UID 1001, copies `.next/standalone` + `.next/static` + `public`, `CMD ["node","server.js"]`)
`.dockerignore` skips `.git`, `node_modules`, `.next/cache`, `.env*`, `.claude`.
## ISCC certificate facts (Luxam-specific)
Use only as a precedent for how to surface certificate metadata. Do not copy into other projects.
```
Certificate number EU-ISCC-Cert-DE100-27002125
Standard ISCC EU
Framework RED III (Directive (EU) 2023/2413)
Issuer SGS Germany GmbH (Europa Allee 12, D-49685 Emstek, Germany)
Certified entity Luxam Oils Trading LLC
Site Al Ttay – Al Khawaneej, Dubai, UAE
Scope Collecting point, warehouse
Material UCO (entirely of vegetable origin)
Valid from 11 August 2025
Valid to 10 August 2026
```
For any new project that surfaces compliance docs, follow the same data shape: number, standard, issuer, certified entity, site, scope, material, validity dates. Pull only from official documents — never invent.
@@ -0,0 +1,71 @@
/**
* Official App Store + Google Play badge anchors.
*
* Setup steps:
* 1. Download official SVGs (use the /show/ path which returns clean SVG):
* https://www.svgrepo.com/show/303128/download-on-the-app-store-apple-logo.svg
* https://www.svgrepo.com/show/303139/google-play-badge-logo.svg
* 2. Save to public/images/app-store-badge.svg + public/images/google-play-badge.svg.
* 3. Add to lib/content.ts:
* export const app = {
* googlePlay: "https://play.google.com/store/apps/details?id=...",
* appStore: "https://apps.apple.com/ae/app/.../id...",
* };
*
* next/image blocks raw SVG by default in Next 16 — keep plain <img>.
*/
import { app } from "@/lib/content";
type Props = {
store: "appstore" | "googleplay";
small?: boolean;
};
export function AppBadge({ store, small }: Props) {
const isApple = store === "appstore";
const href = isApple ? app.appStore : app.googlePlay;
const src = isApple
? "/images/app-store-badge.svg"
: "/images/google-play-badge.svg";
const alt = isApple ? "Download on the App Store" : "Get it on Google Play";
const label = isApple
? "Download on the App Store"
: "Get it on Google Play";
const width = small ? 132 : 160;
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={label}
className="group inline-flex transition duration-300 hover:-translate-y-0.5 hover:opacity-85"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={src}
alt={alt}
width={width}
height={small ? 40 : 48}
className={
small
? "h-auto w-[132px] max-w-full select-none"
: "h-auto w-[140px] max-w-full select-none sm:w-[160px]"
}
loading="lazy"
decoding="async"
draggable={false}
/>
</a>
);
}
/** Both badges in a vertical stack. Use in footer Legal column. */
export function AppBadgeStack({ small }: { small?: boolean }) {
return (
<div className="flex flex-col items-start gap-2">
<AppBadge store="appstore" small={small} />
<AppBadge store="googleplay" small={small} />
</div>
);
}
@@ -0,0 +1,125 @@
"use client";
/**
* 4-image certificate gallery — each tile links to a PDF in a new tab.
*
* Expected data shape:
* export type CertPage = {
* src: string; // /images/certifications/page.jpg
* alt: string;
* title: string; // ISCC Certificate 2025
* page: string; // Page 1 · Annex
* document: string; // ISCC Certificate
* href: string; // /documents/certifications/iscc.pdf OR upstream URL
* };
* export const certificatePages: CertPage[] = [ ... ];
*/
import Image from "next/image";
import { ArrowUpRight, FileText } from "lucide-react";
import { motion, type Variants } from "framer-motion";
export type CertPage = {
src: string;
alt: string;
title: string;
page: string;
document: string;
href: string;
};
const grid: Variants = {
hidden: {},
visible: { transition: { staggerChildren: 0.08, delayChildren: 0.15 } },
};
const cell: Variants = {
hidden: { opacity: 0, y: 24, filter: "blur(6px)" },
visible: {
opacity: 1,
y: 0,
filter: "blur(0px)",
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] },
},
};
export function CertificateGallery({ pages }: { pages: CertPage[] }) {
return (
<motion.div
variants={grid}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4 lg:gap-6"
>
{pages.map((p) => (
<motion.div key={p.src} variants={cell}>
<CertCard {...p} />
</motion.div>
))}
</motion.div>
);
}
function CertCard({ src, alt, title, page, document, href }: CertPage) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={`${title} — ${page}. Opens PDF in a new tab.`}
className="group relative isolate flex h-full flex-col overflow-hidden rounded-2xl border border-bone/10 bg-graphite/40 backdrop-blur-sm transition-all duration-500 hover:-translate-y-1 hover:border-gold/35 hover:bg-graphite/60 focus:outline-none focus-visible:border-gold/60 focus-visible:ring-2 focus-visible:ring-gold/40"
>
<div className="relative aspect-[3/4] w-full overflow-hidden bg-obsidian">
<Image
src={src}
alt={alt}
fill
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
className="object-cover object-top transition-transform duration-[1200ms] ease-[cubic-bezier(0.16,1,0.3,1)] group-hover:scale-[1.04]"
/>
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 top-0 h-20 bg-gradient-to-b from-obsidian/70 via-obsidian/20 to-transparent"
/>
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 bottom-0 h-24 bg-gradient-to-t from-obsidian/80 via-obsidian/30 to-transparent"
/>
<span className="absolute left-3 top-3 inline-flex items-center gap-2 rounded-full border border-gold/30 bg-obsidian/70 px-3 py-1 font-mono text-[10px] uppercase tracking-[0.22em] text-gold-light backdrop-blur">
<span className="size-1 rounded-full bg-gold-light shadow-[0_0_8px_rgba(242,194,91,0.8)]" />
{page}
</span>
<span className="absolute right-3 top-3 grid size-9 place-items-center rounded-full border border-bone/20 bg-obsidian/70 text-mist backdrop-blur transition-all duration-500 group-hover:-translate-y-0.5 group-hover:border-gold/50 group-hover:text-gold-light">
<ArrowUpRight className="size-4" />
</span>
<div
aria-hidden
className="pointer-events-none absolute -bottom-16 -right-16 size-48 rounded-full opacity-0 blur-3xl transition-opacity duration-700 group-hover:opacity-100"
style={{
background:
"radial-gradient(circle, rgba(212,164,55,0.32), transparent 70%)",
}}
/>
<span
aria-hidden
className="pointer-events-none absolute inset-0 rounded-t-2xl border border-transparent transition-colors duration-500 group-hover:border-gold/40"
/>
</div>
<div className="relative flex flex-col gap-2 p-5 md:p-6">
<span className="font-mono text-[10px] uppercase tracking-[0.22em] text-gold-light">
{document}
</span>
<h4 className="font-display text-lg text-bone text-balance transition-colors duration-500 group-hover:text-gold-light md:text-xl">
{title}
</h4>
<div className="mt-1 inline-flex items-center gap-2 text-xs text-mist">
<FileText className="size-3.5 text-gold-light" />
<span>Open PDF</span>
</div>
</div>
<span
aria-hidden
className="pointer-events-none absolute bottom-0 left-0 h-px w-0 bg-gradient-to-r from-gold via-gold-light to-transparent transition-all duration-700 group-hover:w-full"
/>
</a>
);
}
@@ -0,0 +1,132 @@
"use client";
/**
* Premium horizontal logo marquee.
*
* Setup:
* 1. globals.css must have:
* @keyframes marquee {
* 0% { transform: translateX(0); }
* 100% { transform: translateX(-50%); }
* }
* 2. Pass clients prop or create lib/clients.ts:
* export const clients = [
* { src: "/images/clients/brand-a.png", alt: "Brand A" },
* ...
* ];
*/
import Image from "next/image";
import { useReducedMotion } from "framer-motion";
export type Client = { src: string; alt: string };
export function ClientLogoMarquee({
clients,
eyebrow = "Our Clients",
title,
subtitle,
}: {
clients: Client[];
eyebrow?: string;
title: React.ReactNode;
subtitle?: React.ReactNode;
}) {
const reduce = useReducedMotion();
const loop = [...clients, ...clients];
return (
<section className="relative overflow-hidden py-16 md:py-20 lg:py-24">
<div
aria-hidden
className="pointer-events-none absolute -left-32 top-1/3 size-[480px] rounded-full radial-gold blur-3xl opacity-30"
/>
<div className="mx-auto max-w-7xl px-6 md:px-10">
<div className="flex flex-col items-center gap-6 text-center">
<div className="flex items-center gap-3 text-[11px] font-medium uppercase tracking-[0.22em] text-mist">
<span className="h-px w-8 bg-gold/40" />
<span>{eyebrow}</span>
</div>
<h2 className="font-display text-4xl leading-[1.04] text-bone text-balance md:text-5xl">
{title}
</h2>
{subtitle ? (
<p className="max-w-2xl text-pretty text-mist md:text-lg">{subtitle}</p>
) : null}
</div>
<div className="relative mt-12 md:mt-14">
{/* edge fades */}
<div
aria-hidden
className="pointer-events-none absolute inset-y-0 left-0 z-10 w-16 bg-gradient-to-r from-obsidian to-transparent md:w-32"
/>
<div
aria-hidden
className="pointer-events-none absolute inset-y-0 right-0 z-10 w-16 bg-gradient-to-l from-obsidian to-transparent md:w-32"
/>
<div
className="flex overflow-hidden"
style={{
maskImage:
"linear-gradient(90deg, transparent, black 8%, black 92%, transparent)",
}}
>
{reduce ? (
<ClientGrid clients={clients} />
) : (
<div
className="flex shrink-0 gap-4 pr-4 md:gap-6 md:pr-6"
style={{
animation: "marquee 50s linear infinite",
animationPlayState: "running",
}}
onMouseEnter={(e) =>
(e.currentTarget.style.animationPlayState = "paused")
}
onMouseLeave={(e) =>
(e.currentTarget.style.animationPlayState = "running")
}
>
{loop.map((c, i) => (
<LogoTile key={`${c.src}-${i}`} client={c} />
))}
</div>
)}
</div>
</div>
</div>
</section>
);
}
function LogoTile({ client }: { client: Client }) {
return (
<div className="group/tile relative grid h-24 w-44 shrink-0 place-items-center overflow-hidden rounded-2xl border border-bone/10 bg-graphite/40 px-5 backdrop-blur-sm transition-all duration-500 hover:border-gold/30 hover:bg-graphite/60 md:h-28 md:w-52">
<div
aria-hidden
className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-700 group-hover/tile:opacity-100"
style={{
background:
"radial-gradient(60% 80% at 50% 50%, rgba(212,164,55,0.18), transparent 70%)",
}}
/>
<Image
src={client.src}
alt={client.alt}
width={200}
height={100}
className="relative max-h-14 w-auto object-contain opacity-70 brightness-[1.05] contrast-[0.9] grayscale transition-all duration-500 group-hover/tile:scale-105 group-hover/tile:opacity-100 group-hover/tile:grayscale-0 md:max-h-16"
/>
</div>
);
}
function ClientGrid({ clients }: { clients: Client[] }) {
return (
<div className="grid w-full grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6">
{clients.map((c) => (
<LogoTile key={c.src} client={c} />
))}
</div>
);
}
@@ -0,0 +1,188 @@
/**
* Premium 4-column footer w/ no duplicated nav links.
*
* Expected lib/content.ts shape:
*
* export const brand = {
* name: "BRAND",
* legalName: "Brand Legal LLC",
* phone: "00 000 0000",
* email: "info@example.com",
* instagram: "@brand_handle",
* };
* export const socials = {
* instagram: "https://www.instagram.com/brand_handle/",
* whatsapp: "https://api.whatsapp.com/send/?phone=000000000000&text&type=phone_number&app_absent=0",
* whatsappDisplay: "+00 00 000 0000",
* };
* export const credit = {
* agency: "YS Lootah Tech",
* url: "https://yslootahtech.com/",
* };
*/
import { Mail, Phone } from "lucide-react";
import { brand, socials, credit } from "@/lib/content";
import { WhatsAppLink } from "./WhatsAppLink";
import { AppBadge } from "./AppBadges";
function InstagramIcon({ className }: { className?: string }) {
return (
<svg
aria-hidden
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<rect x="3" y="3" width="18" height="18" rx="5" />
<circle cx="12" cy="12" r="4" />
<circle cx="17.5" cy="6.5" r="1.1" fill="currentColor" stroke="none" />
</svg>
);
}
type Link = { label: string; href: string };
type Props = {
navigationLinks: Link[];
serviceItems: Link[];
legalLinks: Link[];
/** Brand logo node (provide your own LogoMark) */
logo?: React.ReactNode;
};
export function Footer({ navigationLinks, serviceItems, legalLinks, logo }: Props) {
return (
<footer className="relative overflow-hidden border-t border-bone/10 bg-obsidian pb-8 pt-11 md:pb-10 md:pt-14 lg:pb-12 lg:pt-[72px]">
<div className="relative z-10 mx-auto max-w-[1480px] px-6 md:px-10">
<div className="grid gap-10 lg:grid-cols-12 lg:gap-12">
{/* Col 1 — Brand + contact */}
<div className="space-y-5 lg:col-span-4">
<a href="/" className="inline-flex items-center gap-4">
{logo}
<span className="hidden font-display text-2xl tracking-[0.22em] text-bone sm:inline">
{brand.name}
</span>
</a>
<p className="max-w-md text-sm text-mist text-pretty md:text-base">
{brand.legalName}.
</p>
<div className="metallic-divider" />
<div className="space-y-2 text-sm text-mist">
<a
href={`tel:${brand.phone.replace(/\s/g, "")}`}
className="flex items-center gap-3 hover:text-gold-light"
>
<Phone className="size-4" />
{brand.phone}
</a>
<a
href={`mailto:${brand.email}`}
className="flex items-center gap-3 hover:text-gold-light"
>
<Mail className="size-4" />
{brand.email}
</a>
<WhatsAppLink
href={socials.whatsapp}
display={socials.whatsappDisplay}
ariaLabel={`Chat with ${brand.name} on WhatsApp`}
inline
/>
<a
href={socials.instagram}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 hover:text-gold-light"
>
<InstagramIcon className="size-4" />
Instagram · {brand.instagram}
</a>
</div>
</div>
{/* Cols 2-4 */}
<div className="grid gap-8 sm:grid-cols-3 lg:col-span-8 lg:gap-10">
<FooterCol title="Navigation" links={navigationLinks} />
<FooterCol title="Services" links={serviceItems} />
<div>
<h4 className="text-[10px] font-medium uppercase tracking-[0.22em] text-gold-light">
Legal &amp; App
</h4>
<ul className="mt-4 space-y-2.5">
{legalLinks.map((l) => (
<li key={l.label}>
<a
href={l.href}
className="text-sm text-mist transition-colors hover:text-bone"
>
{l.label}
</a>
</li>
))}
</ul>
<div className="mt-5 flex flex-col items-start gap-2">
<AppBadge store="appstore" small />
<AppBadge store="googleplay" small />
</div>
</div>
</div>
</div>
<div className="mt-9 flex flex-col items-start justify-between gap-3 border-t border-bone/10 pt-5 text-xs uppercase tracking-[0.22em] text-mist md:mt-10 md:flex-row md:items-center md:gap-6">
<span>
© {new Date().getFullYear()} {brand.legalName}. All rights reserved.
</span>
</div>
<div className="mt-3 text-[11px] tracking-[0.04em] text-mist/60 md:text-xs">
Website designed &amp; developed by{" "}
<a
href={credit.url}
target="_blank"
rel="noopener noreferrer"
className="text-mist/80 underline decoration-gold/30 decoration-1 underline-offset-4 transition-colors duration-500 hover:text-gold-light hover:decoration-gold/70"
>
{credit.agency}
</a>
.
</div>
</div>
{/* Watermark — contained inside footer */}
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 bottom-0 z-0 flex select-none justify-center overflow-hidden"
>
<span className="translate-y-[18%] font-display text-[14vw] leading-none tracking-[-0.02em] text-bone/[0.025] md:text-[12vw]">
{brand.name}
</span>
</div>
</footer>
);
}
function FooterCol({ title, links }: { title: string; links: Link[] }) {
return (
<div>
<h4 className="text-[10px] font-medium uppercase tracking-[0.22em] text-gold-light">
{title}
</h4>
<ul className="mt-4 space-y-2.5">
{links.map((l) => (
<li key={l.label}>
<a
href={l.href}
className="text-sm text-mist transition-colors hover:text-bone"
>
{l.label}
</a>
</li>
))}
</ul>
</div>
);
}
@@ -0,0 +1,75 @@
"use client";
import type { LucideIcon } from "lucide-react";
import type { ReactNode } from "react";
type Props = {
Icon?: LucideIcon;
title: ReactNode;
body?: ReactNode;
href?: string;
external?: boolean;
className?: string;
/** Append custom inner content below the body */
children?: ReactNode;
};
export function GlassCard({
Icon,
title,
body,
href,
external,
className,
children,
}: Props) {
const inner = (
<>
{/* hover amber radial glow */}
<div
aria-hidden
className="pointer-events-none absolute -bottom-16 -right-16 size-48 rounded-full opacity-0 blur-3xl transition-opacity duration-700 group-hover:opacity-100"
style={{
background:
"radial-gradient(circle, rgba(212,164,55,0.28), transparent 70%)",
}}
/>
{Icon ? (
<div className="grid size-11 place-items-center rounded-full glass-gold transition-all duration-500 group-hover:rotate-[8deg] group-hover:shadow-[0_0_22px_-6px_rgba(212,164,55,0.6)]">
<Icon className="size-[18px] text-gold-light" strokeWidth={1.4} />
</div>
) : null}
<h4 className="mt-5 font-display text-xl text-bone transition-colors duration-500 group-hover:text-gold-light md:text-2xl">
{title}
</h4>
{body ? (
<p className="mt-2 text-sm text-mist text-pretty leading-relaxed">
{body}
</p>
) : null}
{children}
{/* gold underline sweep */}
<span
aria-hidden
className="pointer-events-none absolute bottom-0 left-0 h-px w-0 bg-gradient-to-r from-gold via-gold-light to-transparent transition-all duration-700 group-hover:w-full"
/>
</>
);
const base = `group relative isolate h-full overflow-hidden rounded-2xl border border-bone/10 bg-graphite/40 p-6 backdrop-blur-sm transition-all duration-500 hover:-translate-y-1 hover:border-gold/30 hover:bg-graphite/60 md:p-7 ${className ?? ""}`;
if (href) {
return (
<a
href={href}
{...(external
? { target: "_blank", rel: "noopener noreferrer" }
: {})}
className={base}
>
{inner}
</a>
);
}
return <div className={base}>{inner}</div>;
}
@@ -0,0 +1,40 @@
"use client";
import { cn } from "@/lib/cn";
import { ArrowUpRight } from "lucide-react";
import type { ComponentPropsWithoutRef } from "react";
type Variant = "primary" | "ghost" | "outline";
type Props = ComponentPropsWithoutRef<"a"> & {
variant?: Variant;
arrow?: boolean;
};
const base =
"relative inline-flex items-center gap-2 rounded-full px-7 py-3.5 text-sm font-medium tracking-wide transition-all duration-500";
const variants: Record<Variant, string> = {
primary:
"bg-gradient-to-r from-gold-light via-gold to-gold-deep text-obsidian shadow-[0_10px_40px_-12px_rgba(212,164,55,0.6)] hover:shadow-[0_18px_50px_-12px_rgba(242,194,91,0.8)]",
outline:
"border border-bone/20 text-bone hover:border-gold/60 hover:text-gold-light",
ghost: "text-bone hover:text-gold-light",
};
export function PremiumButton({
variant = "primary",
className,
children,
arrow = true,
...rest
}: Props) {
return (
<a className={cn(base, variants[variant], className)} {...rest}>
<span>{children}</span>
{arrow ? (
<ArrowUpRight className="size-4 transition-transform duration-500 group-hover:translate-x-0.5" />
) : null}
</a>
);
}
@@ -0,0 +1,76 @@
"use client";
import { cn } from "@/lib/cn";
import { motion, useReducedMotion, type Variants } from "framer-motion";
import type { ReactNode } from "react";
type Props = {
eyebrow?: string;
index?: string;
title: ReactNode;
/** The gold-accent phrase rendered inside the title — pass as JSX or separate prop. */
accent?: ReactNode;
subtitle?: ReactNode;
align?: "left" | "center";
className?: string;
};
export function SectionHeading({
eyebrow,
index,
title,
accent,
subtitle,
align = "left",
className,
}: Props) {
const reduce = useReducedMotion();
const v: Variants = {
hidden: { opacity: 0, y: reduce ? 0 : 24, filter: reduce ? "blur(0px)" : "blur(6px)" },
visible: {
opacity: 1,
y: 0,
filter: "blur(0px)",
transition: { duration: 0.9, ease: [0.16, 1, 0.3, 1] },
},
};
return (
<div
className={cn(
"flex flex-col gap-6",
align === "center" ? "items-center text-center" : "items-start",
className
)}
>
{eyebrow ? (
<div className="flex items-center gap-3 text-[11px] font-medium uppercase tracking-[0.22em] text-mist">
{index ? <span className="text-gold/85">{index}</span> : null}
<span className="h-px w-8 bg-gold/40" />
<span className="text-mist/85">{eyebrow}</span>
</div>
) : null}
<motion.h2
variants={v}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
className="font-display text-[clamp(2.4rem,5vw,4.6rem)] leading-[1.02] tracking-[-0.02em] text-bone text-balance"
>
{title}
{accent ? <> <span className="gold-text">{accent}</span></> : null}
</motion.h2>
{subtitle ? (
<motion.p
variants={v}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
transition={{ delay: 0.1 }}
className="max-w-2xl text-pretty text-mist md:text-lg"
>
{subtitle}
</motion.p>
) : null}
</div>
);
}
@@ -0,0 +1,88 @@
/**
* Refined gold WhatsApp link.
*
* Use 3 surfaces:
* - <WhatsAppLink href={...} display="+CC XX XXX XXXX" ariaLabel="..." inline /> (footer row)
* - <WhatsAppIconButton href={...} ariaLabel="..." /> (nav)
* - <WhatsAppLink href={...} display={...} ariaLabel={...} /> (contact card)
*
* URL format expected: https://api.whatsapp.com/send/?phone=COUNTRYCODE+DIGITS&text&type=phone_number&app_absent=0
* Phone in URL = digits only (no + or spaces). Display string = formatted "+CC XX XXX XXXX".
*
* lucide-react has no WhatsApp glyph → use this inline SVG.
*/
import type { SVGProps } from "react";
export function WhatsAppIcon({ className, ...rest }: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden
className={className}
{...rest}
>
<path d="M19.11 4.86A9.94 9.94 0 0 0 12.04 2C6.58 2 2.13 6.45 2.13 11.91c0 1.75.46 3.45 1.33 4.95L2 22l5.27-1.38a9.93 9.93 0 0 0 4.76 1.21h.01c5.46 0 9.91-4.45 9.91-9.91 0-2.65-1.03-5.14-2.84-7.06ZM12.04 20.15c-1.5 0-2.96-.4-4.24-1.17l-.3-.18-3.13.82.83-3.05-.2-.31a8.21 8.21 0 0 1-1.26-4.35c0-4.53 3.69-8.22 8.22-8.22 2.2 0 4.27.86 5.82 2.41a8.16 8.16 0 0 1 2.41 5.81c0 4.53-3.69 8.24-8.15 8.24Zm4.72-6.16c-.26-.13-1.53-.75-1.76-.83-.24-.09-.41-.13-.58.13-.17.26-.66.83-.81 1-.15.17-.3.2-.55.07-.26-.13-1.1-.41-2.1-1.29-.78-.69-1.3-1.55-1.45-1.81-.15-.26-.02-.4.11-.53.11-.11.26-.3.39-.45.13-.15.17-.26.26-.43.09-.17.04-.32-.02-.45-.07-.13-.58-1.4-.79-1.92-.21-.5-.42-.43-.58-.44-.15-.01-.32-.01-.49-.01-.17 0-.45.06-.69.32-.24.26-.9.88-.9 2.15 0 1.27.92 2.5 1.05 2.67.13.17 1.82 2.78 4.42 3.9.62.27 1.1.43 1.48.55.62.2 1.18.17 1.63.1.5-.07 1.53-.62 1.74-1.23.21-.6.21-1.12.15-1.23-.06-.11-.23-.18-.49-.31Z" />
</svg>
);
}
type LinkProps = {
href: string;
display: string;
ariaLabel: string;
inline?: boolean; // true = footer row; false = larger contact-card row
};
export function WhatsAppLink({ href, display, ariaLabel, inline }: LinkProps) {
if (inline) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={ariaLabel}
className="flex items-center gap-3 text-sm text-mist hover:text-gold-light"
>
<WhatsAppIcon className="size-4" />
WhatsApp · {display}
</a>
);
}
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={ariaLabel}
className="group flex items-center gap-3 rounded-xl border border-bone/8 bg-bone/[0.03] p-3.5 transition-colors duration-500 hover:border-gold/30"
>
<span className="grid size-9 shrink-0 place-items-center rounded-full border border-gold/30 bg-gold/[0.08] text-gold-light shadow-[0_0_14px_-4px_rgba(212,164,55,0.6)]">
<WhatsAppIcon className="size-4" />
</span>
<span className="text-bone group-hover:text-gold-light">
WhatsApp · {display}
</span>
</a>
);
}
type ButtonProps = {
href: string;
ariaLabel: string;
className?: string;
};
export function WhatsAppIconButton({ href, ariaLabel, className }: ButtonProps) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={ariaLabel}
className={`group grid size-10 place-items-center rounded-full border border-bone/15 text-bone transition-all duration-500 hover:border-gold/40 hover:text-gold-light hover:shadow-[0_0_22px_-6px_rgba(212,164,55,0.6)] hover:scale-[1.04] ${className ?? ""}`}
>
<WhatsAppIcon className="size-[18px]" />
</a>
);
}