diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..cfebe9b --- /dev/null +++ b/SETUP.md @@ -0,0 +1,64 @@ +# Setup notes + +## 1. Copy files +- `components/*` → your `components/` folder +- `app/page.tsx` → replace your existing `app/page.tsx` +- `public/images/*` → your `public/images/` folder + +## 2. Tailwind config +Merge `tailwind.config.snippet.js` into your existing `tailwind.config.js` +(the `colors`, `fontFamily`, `keyframes`, and `animation` keys under +`theme.extend`). Don't overwrite the whole file — just merge these keys in. + +## 3. Fonts (optional but recommended — matches the design as built) +In `app/layout.tsx`, add next/font and expose them as CSS variables that the +tailwind config's `fontFamily` reads from: + +```tsx +import { Playfair_Display, Inter } from "next/font/google"; + +const display = Playfair_Display({ + subsets: ["latin"], + weight: ["500", "600", "700"], + variable: "--font-display", +}); +const body = Inter({ + subsets: ["latin"], + variable: "--font-body", +}); + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} +``` + +If you'd rather skip next/font, everything still works — `font-serif` and +`font-sans` just fall back to system serif/sans stacks. + +## 4. Path alias +Components import via `@/components/...`. Confirm your `tsconfig.json` has: +```json +"paths": { "@/*": ["./*"] } +``` +If your project already uses a different alias (e.g. `src/`), adjust the +import paths in `app/page.tsx` accordingly. + +## What's included +- **Header.tsx** — sticky, scroll-aware shrink/shadow, animated nav underline, mobile slide-menu +- **Hero.tsx** — masthead + live Countdown + rotating gear watermark (signature element) +- **Countdown.tsx** — client-side ticking countdown to the ceremony +- **EventDetails.tsx** — date / time / venue cards +- **Handover.tsx** — outgoing vs incoming board, gavel-pass layout +- **Guests.tsx** — Chief Guest & Guest of Honour cards +- **OfficeBearers.tsx** — full 46-role roster, tabbed (Leadership / Directors / Committee Chairs), dotted-leader rows +- **CtaBand.tsx** — gold RSVP band with shimmer animation +- **Footer.tsx** — club info, links, back-to-top +- **GearMotif.tsx** / **Reveal.tsx** — shared primitives (rotating SVG gear, scroll-reveal wrapper) + +All animation is Tailwind-only: utility transitions, `animate-*` classes +from the extended config, and one small IntersectionObserver hook (`Reveal`) +for scroll-triggered fades — no animation library used. diff --git a/public/images/club-advisor.jpg b/public/images/club-advisor.jpg new file mode 100644 index 0000000..f75d5e1 Binary files /dev/null and b/public/images/club-advisor.jpg differ diff --git a/public/images/guest-of-honour.jpg b/public/images/guest-of-honour.jpg new file mode 100644 index 0000000..f712792 Binary files /dev/null and b/public/images/guest-of-honour.jpg differ diff --git a/public/images/president.jpg b/public/images/president.jpg new file mode 100644 index 0000000..9a60ee0 Binary files /dev/null and b/public/images/president.jpg differ diff --git a/public/images/rotary-logo.png b/public/images/rotary-logo.png new file mode 100644 index 0000000..f6ff269 Binary files /dev/null and b/public/images/rotary-logo.png differ diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..c56f95f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,79 @@ @import "tailwindcss"; -:root { - --background: #ffffff; - --foreground: #171717; +@theme { + /* Rotary brand colors — generates bg-rotary-navy, text-rotary-gold, etc. */ + --color-rotary-navy: #0b2545; + --color-rotary-blue: #12468c; + --color-rotary-azure: #2e7cd6; + --color-rotary-gold: #f2a900; + --color-rotary-goldlight: #ffd166; + --color-rotary-amber: #c2660c; + --color-rotary-paper: #f4f6fa; + --color-rotary-ink: #101b2d; + + /* Fonts — generates font-serif / font-sans overrides. + Works even without next/font: falls back to system stacks. */ + --font-serif: var(--font-display, ui-serif), Georgia, serif; + --font-sans: var(--font-body, ui-sans-serif), system-ui, sans-serif; + + /* Custom animation utilities — generates animate-spin-slow, animate-fade-up, etc. */ + --animate-spin-slow: spin-slow 50s linear infinite; + --animate-spin-slower: spin-slow 90s linear infinite; + --animate-fade-up: fade-up 0.7s cubic-bezier(0.16, 1, 0.3, 1) both; + --animate-shimmer: shimmer 2.5s linear infinite; + --animate-ring-pulse: ring-pulse 2.4s ease-out infinite; + --animate-shine: shine 3.5s ease-in-out infinite; } -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); +@keyframes spin-slow { + to { + transform: rotate(360deg); + } } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; +@keyframes fade-up { + 0% { + opacity: 0; + transform: translateY(16px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +@keyframes ring-pulse { + 0%, + 100% { + box-shadow: 0 0 0 0 rgba(242, 169, 0, 0.45); + } + 50% { + box-shadow: 0 0 0 10px rgba(242, 169, 0, 0); + } +} + +@keyframes shine { + 0% { + transform: translateX(-120%); + } + 50% { + transform: translateX(220%); + } + 100% { + transform: translateX(220%); } } body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; -} + background-color: var(--color-rotary-paper); + color: var(--color-rotary-ink); +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 976eb90..7946cd3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -13,8 +13,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Rotary Club of Vellore — Installation Ceremony 2026", + description: "The President and Members cordially invite you, with family, to witness the installation of the incoming Board and to Create Lasting Impact together.", }; export default function RootLayout({ diff --git a/src/app/page.tsx b/src/app/page.tsx index 3f36f7c..b419c82 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,65 +1,21 @@ -import Image from "next/image"; +import Hero from "@/components/Hero"; +import EventDetails from "@/components/EventDetails"; +import Handover from "@/components/Handover"; +import Guests from "@/components/Guests"; +import OfficeBearers from "@/components/OfficeBearers"; +import CtaBand from "@/components/CtaBand"; +import Footer from "@/components/Footer"; -export default function Home() { +export default function Page() { return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
-
- - Vercel logomark - Deploy Now - - - Documentation - -
-
-
+
+ + + + + + +
); -} +} \ No newline at end of file diff --git a/src/components/Countdown.tsx b/src/components/Countdown.tsx new file mode 100644 index 0000000..87119f6 --- /dev/null +++ b/src/components/Countdown.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useEffect, useState } from "react"; + +const TARGET = new Date("2026-07-03T18:30:00+05:30").getTime(); + +function getParts(msLeft: number) { + const clamped = Math.max(msLeft, 0); + return { + days: Math.floor(clamped / (1000 * 60 * 60 * 24)), + hours: Math.floor((clamped / (1000 * 60 * 60)) % 24), + minutes: Math.floor((clamped / (1000 * 60)) % 60), + seconds: Math.floor((clamped / 1000) % 60), + }; +} + +export default function Countdown() { + const [parts, setParts] = useState | null>(null); + + useEffect(() => { + const tick = () => setParts(getParts(TARGET - Date.now())); + tick(); + const id = setInterval(tick, 1000); + return () => clearInterval(id); + }, []); + + const units = [ + { label: "Days", value: parts?.days }, + { label: "Hours", value: parts?.hours }, + { label: "Min", value: parts?.minutes }, + { label: "Sec", value: parts?.seconds }, + ]; + + return ( +
+ {units.map((u) => ( +
+ + {u.value !== undefined ? String(u.value).padStart(2, "0") : "--"} + + + {u.label} + +
+ ))} +
+ ); +} \ No newline at end of file diff --git a/src/components/CtaBand.tsx b/src/components/CtaBand.tsx new file mode 100644 index 0000000..4c71ec1 --- /dev/null +++ b/src/components/CtaBand.tsx @@ -0,0 +1,19 @@ +import Reveal from "./Reveal"; + +export default function CtaBand() { + return ( +
+ +
+

+ Dear Rotarians, your presence is most essential. +

+

+ Join the President and Members of Rotary Club of Vellore, with + family, for the Installation Ceremony on 3rd July 2026. +

+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/EventDetails.tsx b/src/components/EventDetails.tsx new file mode 100644 index 0000000..3e4dc3c --- /dev/null +++ b/src/components/EventDetails.tsx @@ -0,0 +1,66 @@ +import Reveal from "./Reveal"; + +const DETAILS = [ + { + label: "Date", + value: "3rd July 2026", + sub: "Friday", + icon: ( + + ), + }, + { + label: "Time", + value: "6:30 PM", + sub: "Doors open at 6:00 PM", + icon: <>, + }, + { + label: "Venue", + value: "Hotel Khanna, Laya Hall", + sub: "Anna Salai, Vellore", + icon: ( + <> + ), + }, +]; + +export default function EventDetails() { + return ( +
+
+ +

+ Save the date +

+

+ Join us for the evening +

+
+ +
+ {DETAILS.map((d, i) => ( + +
+
+ + {d.icon} + +
+
+

+ {d.label} +

+

+ {d.value} +

+

{d.sub}

+
+
+
+ ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000..39d2389 --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,93 @@ +import Image from "next/image"; +import GearMotif from "./GearMotif"; + +const LINKS = [ + { href: "#event", label: "The Ceremony" }, + { href: "#handover", label: "Handover" }, + { href: "#guests", label: "Guests" }, + { href: "#bearers", label: "Office Bearers" }, +]; + +export default function Footer() { + return ( +
+ + +
+ {/* Brand */} +
+
+
+ Rotary International — Create Lasting Impact +
+ + Rotary Club of Vellore + +
+

+ Club No. 15900 · Chartered 5th March 1948 · District 3231. + Serving Vellore since generations, one project at a time. +

+
+ + {/* Quick links */} +
+

+ Quick links +

+ +
+ + {/* Contact */} +
+

+ The evening +

+
    +
  • 3rd July 2026 · Friday · 6:30 PM
  • +
  • Hotel Khanna, Laya Hall
  • +
  • Anna Salai, Vellore
  • +
+
+
+ +
+

© 2026 Rotary Club of Vellore. All are welcome.

+
+

+ Powered by{" "} + + Rootxwire + +

+ + Back to top + + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/GearMotif.tsx b/src/components/GearMotif.tsx new file mode 100644 index 0000000..6c34915 --- /dev/null +++ b/src/components/GearMotif.tsx @@ -0,0 +1,35 @@ +type Props = { + className?: string; + duration?: "spin-slow" | "spin-slower"; +}; + +/** A single-color outline gear, meant to sit behind content at low opacity. */ +export default function GearMotif({ className = "", duration = "spin-slow" }: Props) { + const spin = duration === "spin-slower" ? "animate-spin-slower" : "animate-spin-slow"; + return ( + + ); +} \ No newline at end of file diff --git a/src/components/Guests.tsx b/src/components/Guests.tsx new file mode 100644 index 0000000..74f8bcd --- /dev/null +++ b/src/components/Guests.tsx @@ -0,0 +1,61 @@ +import Image from "next/image"; +import Reveal from "./Reveal"; + +const GUESTS = [ + { + name: "Rtn. PDG. M. Rajan Babu", + role: "Chief Guest", + subrole: "Past District Governor, 3231", + photo: "/images/chief-guest-couple.png", + }, + { + name: "Rtn. Yelagiri V. Selvam", + role: "Guest of Honour", + subrole: "Chairman, Kodai Vizha 2026 – 27", + photo: "/images/guest-of-honour.jpg", + }, +]; + +export default function Guests() { + return ( +
+
+ +

+ With us this evening +

+

+ Distinguished Guests +

+
+ +
+ {GUESTS.map((g, i) => ( + +
+
+ {g.name} +
+
+

+ {g.role} +

+

+ {g.name} +

+

{g.subrole}

+
+
+
+ ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/Handover.tsx b/src/components/Handover.tsx new file mode 100644 index 0000000..301944f --- /dev/null +++ b/src/components/Handover.tsx @@ -0,0 +1,109 @@ +import Reveal from "./Reveal"; + +const OUTGOING = { + label: "2025 – 26", + officers: [ + { role: "President", name: "Rtn. Prof. S. Thiyagukumar" }, + { role: "Secretary", name: "Rtn. A.S. Abdul Rahaman" }, + { role: "Treasurer", name: "Rtn. Er. G. Muralidharan" }, + ], +}; + +const INCOMING = { + label: "2026 – 27", + officers: [ + { role: "President", name: "Rtn. Er. G. Muralidharan" }, + { role: "Secretary", name: "Rtn. M. Bhimarao" }, + { role: "Treasurer", name: "Rtn. K. Kumar" }, + ], +}; + +function OfficerCard({ + group, + tone, +}: { + group: typeof OUTGOING; + tone: "outgoing" | "incoming"; +}) { + const incoming = tone === "incoming"; + return ( +
+ {incoming && ( +
+
+
+ )} +

+ {incoming ? "Incoming Board" : "Outgoing Board"} +

+

+ {group.label} +

+ +
    + {group.officers.map((o) => ( +
  • + + {o.role} + + + {o.name} + +
  • + ))} +
+
+ ); +} + +export default function Handover() { + return ( +
+
+ +

+ The gavel passes +

+

+ A year of service, handed forward +

+
+ +
+ + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000..e035c89 --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Image from "next/image"; + +const NAV_LINKS = [ + { href: "#event", label: "The Ceremony" }, + { href: "#handover", label: "Handover" }, + { href: "#guests", label: "Guests" }, + { href: "#bearers", label: "Office Bearers" }, +]; + +export default function Header() { + const [scrolled, setScrolled] = useState(false); + const [open, setOpen] = useState(false); + + useEffect(() => { + const onScroll = () => setScrolled(window.scrollY > 12); + onScroll(); + window.addEventListener("scroll", onScroll, { passive: true }); + return () => window.removeEventListener("scroll", onScroll); + }, []); + + return ( +
+
+ {/* Brand */} + + + Rotary International District 3231 + + + + Rotary Club of Vellore + + + District 3231 · Est. 1948 + + + + + {/* Desktop nav (hidden) */} + + + {/* Mobile toggle (hidden) */} + +
+ + {/* Mobile menu (hidden) */} + +
+ ); +} \ No newline at end of file diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx new file mode 100644 index 0000000..25f2fdf --- /dev/null +++ b/src/components/Hero.tsx @@ -0,0 +1,145 @@ +import Image from "next/image"; +import GearMotif from "./GearMotif"; +import Countdown from "./Countdown"; + +export default function Hero() { + return ( +
+ {/* signature watermark */} + + + +
+ {/* Brand row — replaces the old header's logo now that it's gone */} +
+
+ Rotary International — Create Lasting Impact +
+ + + + Rotary Club of Vellore + + + District 3231 · Est. 1948 + + +
+ +
+ {/* Copy */} +
+ {/* Advisor + President thumbnails */} +
+
+ Rtn. Dr. T. Jacob John, Club Advisor +
+
+ Rtn. Er. G. Muralidharan, President 2026-27 +
+
+ +

+ Rotary Club of Vellore · Club No. 15900 · Charter 05.03.1948 +

+ +

+ Installation + Ceremony 2026 – 27 +

+ +

+ The President and Members cordially invite you, with family, to + witness the installation of the incoming Board and to + Create Lasting Impact together. +

+ + {/* date / venue chips */} +
+
+ + + + + 3 July 2026 · Friday · 6:30 PM +
+
+ + + + + Hotel Khanna, Laya Hall, Vellore +
+
+ +
+

+ Counting down to installation +

+ +
+ + + View Ceremony Details + + + + +
+ + {/* President portrait */} +
+
+
+
+ Rtn. Er. G. Muralidharan, President 2026-27 +
+

+ President, 2026 – 27 +

+

+ Rtn. Er. G. Muralidharan +

+
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/OfficeBearers.tsx b/src/components/OfficeBearers.tsx new file mode 100644 index 0000000..695879d --- /dev/null +++ b/src/components/OfficeBearers.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { useState } from "react"; +import Reveal from "./Reveal"; + +const GROUPS = [ + { + label: "Leadership", + roster: [ + { role: "Club Advisor", name: "Rtn. Dr. T. Jacob John" }, + { role: "Club Mentor", name: "Rtn. Dr. T. Daniel Ezhilarasu" }, + { role: "Club Trainer", name: "Rtn. Dr. T. Jacob John" }, + { role: "President", name: "Rtn. Er. G. Muralidharan" }, + { role: "Secretary", name: "Rtn. M. Bhimarao" }, + { role: "Immediate Past President", name: "Rtn. Prof. S. Thiyagukumar" }, + { role: "President Elect (2027-28)", name: "Rtn. B. Nirmala Gopinath" }, + { role: "President Nominee (2028-29)", name: "Rtn. A.S. Abdul Rahiman" }, + { role: "Treasurer", name: "Rtn. K. Kumar" }, + { role: "Sergeant at Arms – 1", name: "Rtn. Dr. D. Daniel Ezhilarasu" }, + ], + }, + { + label: "Directors", + roster: [ + { role: "Club Administration", name: "Rtn. P. Srinivasan" }, + { role: "Vocational Service", name: "Rtn. Joseph Annaiah" }, + { role: "Community Service — Health", name: "Rtn. Dr. Thiagarajan" }, + { role: "Community Service — Development", name: "Rtn. Emperumal M." }, + { role: "International Service", name: "Rtn. Suthakarraj" }, + { role: "Youth Service", name: "Rtn. A.S. Abdul Rahiman" }, + ], + }, + { + label: "Committee Chairs", + roster: [ + { role: "Rotary Foundation", name: "Rtn. Subash Chandaran" }, + { role: "Membership Development", name: "Rtn. Dr. M. Thangamani" }, + { role: "Public Image", name: "Rtn. Boominatthan" }, + { role: "Family Participation", name: "Rtn. P. Srinivasan" }, + { role: "DE-RYLA", name: "Rtn. Shyam Sundar Jaising" }, + { role: "Blood Camp", name: "Rtn. Prof. D. Thirumaran" }, + { role: "School into Smiles", name: "Rtn. Gopinath" }, + { role: "Award Team", name: "Rtn. ASA. Shanmugam" }, + { role: "Polio Plus", name: "Rtn. Viswanathan" }, + { role: "Water and Sanitation", name: "Rtn. R. Ravi" }, + { role: "Literacy", name: "Rtn. Dr. K. Saravanan" }, + { role: "Bulletin", name: "Rtn. Dr. M. Thangamani" }, + { role: "Eye Camps", name: "Rtn. A.S. Abdul Rahiman" }, + { role: "Dental Camps", name: "Rtn. Dr. Bavithara" }, + { role: "General Medical Camps", name: "Rtn. Emperumal M." }, + { role: "Dist. Special Project", name: "Rtn. Emperumal M." }, + { role: "Pongal Vizha & Festivals", name: "Rtn. M. Bhima Rao" }, + { role: "Environment", name: "Rtn. TKT Srinivasan" }, + { role: "Skill India", name: "Rtn. K.V. Elangovan" }, + { role: "Rotract", name: "Rtn. Prof. B. Nirmal Prasad" }, + { role: "Interact", name: "Rtn. P. Duraisamy Padmanaban" }, + { role: "Go Green", name: "Rtn. Dr. M. Kumaran" }, + { role: "WinS", name: "Rtn. Balaji" }, + { role: "Low Cost Shelter", name: "Rtn. Chandru" }, + { role: "Be Safety Town", name: "Rtn. R. Ravi" }, + { role: "Clean Town — Swachh Bharat", name: "Rtn. M. Bhima Rao" }, + { role: "Job Fair", name: "Rtn. TKT Srinivasan" }, + { role: "Rally (Car, Bike, Walk)", name: "Rtn. Joseph Annaiah" }, + { role: "IWC Ann Club", name: "Rtn. Nirmala Gopinath" }, + { role: "Competition & Sports", name: "Rtn. P. Duraisamy" }, + ], + }, +]; + +export default function OfficeBearers() { + const [active, setActive] = useState(0); + + return ( +
+
+ +

+ Office Bearers of 2026 – 27 +

+

+ The Directory +

+
+ + {/* Tabs */} + +
+ {GROUPS.map((g, i) => ( + + ))} +
+
+ + {/* Roster */} +
+ {GROUPS[active].roster.map((b, i) => ( +
+ + {b.role} + + + {b.name} + +
+ ))} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/Reveal.tsx b/src/components/Reveal.tsx new file mode 100644 index 0000000..4f38973 --- /dev/null +++ b/src/components/Reveal.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; + +type Props = { + children: React.ReactNode; + delay?: number; // ms + className?: string; + variant?: "fade" | "glow"; +}; + +export default function Reveal({ children, delay = 0, className = "", variant = "fade" }: Props) { + const ref = useRef(null); + const [visible, setVisible] = useState(false); + + useEffect(() => { + const node = ref.current; + if (!node) return; + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setVisible(true); + observer.disconnect(); + } + }, + { threshold: 0.15 } + ); + observer.observe(node); + return () => observer.disconnect(); + }, []); + + const hiddenState = + variant === "glow" + ? "opacity-0 translate-y-6 scale-95 drop-shadow-none" + : "opacity-0 translate-y-6"; + + const visibleState = + variant === "glow" + ? "opacity-100 translate-y-0 scale-100 drop-shadow-[0_0_36px_rgba(242,169,0,0.28)]" + : "opacity-100 translate-y-0"; + + return ( +
+ {children} +
+ ); +} \ No newline at end of file diff --git a/tailwind.config.snippet.js b/tailwind.config.snippet.js new file mode 100644 index 0000000..3ced245 --- /dev/null +++ b/tailwind.config.snippet.js @@ -0,0 +1,48 @@ +/** + * MERGE THIS into your existing tailwind.config.js/ts — do not replace the file. + * Add the `colors`, `fontFamily`, `keyframes`, and `animation` keys below + * into your existing `theme.extend` block. + */ +module.exports = { + theme: { + extend: { + colors: { + rotary: { + navy: "#0B2545", // primary — headers, deep panels + blue: "#12468C", // Rotary International blue — brand primary + azure: "#2E7CD6", // secondary accent, links, gradients + gold: "#F2A900", // Rotary gold — CTAs, dividers, highlights + goldlight: "#FFD166", + paper: "#F4F6FA", // page background (cool paper, not cream) + ink: "#101B2D", // body text + }, + }, + fontFamily: { + serif: ["var(--font-display)", "Georgia", "serif"], + sans: ["var(--font-body)", "ui-sans-serif", "system-ui", "sans-serif"], + }, + keyframes: { + "spin-slow": { to: { transform: "rotate(360deg)" } }, + "fade-up": { + "0%": { opacity: "0", transform: "translateY(16px)" }, + "100%": { opacity: "1", transform: "translateY(0)" }, + }, + shimmer: { + "0%": { backgroundPosition: "-200% 0" }, + "100%": { backgroundPosition: "200% 0" }, + }, + "ring-pulse": { + "0%, 100%": { boxShadow: "0 0 0 0 rgba(242,169,0,0.45)" }, + "50%": { boxShadow: "0 0 0 10px rgba(242,169,0,0)" }, + }, + }, + animation: { + "spin-slow": "spin-slow 50s linear infinite", + "spin-slower": "spin-slow 90s linear infinite", + "fade-up": "fade-up 0.7s cubic-bezier(0.16,1,0.3,1) both", + shimmer: "shimmer 2.5s linear infinite", + "ring-pulse": "ring-pulse 2.4s ease-out infinite", + }, + }, + }, +};