docs: add ACCESSIBILITY_AUDIT, SECURITY_PRIVACY_AUDIT, PRODUCT_BRIEF with file-specific actions; a11y: add skip link, lang+hreflang, ARIA for menu/carousel/dialog/form; noValidate+honeypot

This commit is contained in:
2025-08-08 23:00:09 +02:00
parent 910253c8f4
commit 3c74d71e22
16 changed files with 501 additions and 43 deletions

148
ACCESSIBILITY_AUDIT.md Normal file
View File

@@ -0,0 +1,148 @@
### Omoluabi Accessibility Audit (WCAG 2.2 AA)
Scope: Current Astro site as of this commit. Focus on perceivable, operable, understandable, and robust criteria for Nigerians in NL and Dutch partners.
---
### Executive summary
- **Overall**: Solid semantic base with clear landmarks in `src/layouts/BaseLayout.astro`. Reduced-motion support exists in `src/styles/global.css`. Key gaps: language/i18n markup, skip links, menu ARIA, carousel semantics, form error/ARIA, dialog semantics, and strict CSP readiness.
- **Risk**: Medium → High for navigation/keyboard and language; Low → Medium for contrast on image/gradient backgrounds.
---
### High-priority issues (fix now)
1) Missing skip link + focus target
- File: `src/layouts/BaseLayout.astro`
- Snippet (add just inside `<body>` and set `id` on `<main>`):
```astro
<a href="#main" class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:bg-white focus:text-black focus:px-3 focus:py-2 focus:rounded">
Skip to main content
</a>
...
<main id="main" tabindex="-1">
<slot />
</main>
```
- Rationale: Allows keyboard users to bypass repeated navigation (WCAG 2.4.1, 2.4.3).
2) Page language + `hreflang`
- File: `src/layouts/BaseLayout.astro`
- Snippet (top `<html>` and `<head>`):
```astro
<html lang={Astro.props.lang ?? 'en'}>
<head>
...
<link rel="alternate" hrefLang="en" href="/en/" />
<link rel="alternate" hrefLang="nl" href="/nl/" />
<link rel="alternate" hrefLang="x-default" href="/" />
```
- Rationale: Correct language for screen readers and localized SEO (WCAG 3.1.1, 3.1.2).
3) Mobile menu button lacks ARIA state wiring
- File: `src/layouts/BaseLayout.astro`
- Snippet (button + script):
```astro
<button class="md:hidden" id="mobile-menu-button" aria-controls="mobile-menu" aria-expanded="false">
...
</button>
```
```html
<script>
document.addEventListener('DOMContentLoaded', () => {
const mobileMenuButton = document.getElementById('mobile-menu-button');
const mobileMenu = document.getElementById('mobile-menu');
if (mobileMenuButton && mobileMenu) {
mobileMenuButton.addEventListener('click', () => {
const isHidden = mobileMenu.classList.toggle('hidden');
mobileMenuButton.setAttribute('aria-expanded', String(!isHidden));
});
}
});
</script>
```
- Rationale: Communicates expanded/collapsed state for assistive tech (WCAG 4.1.2).
4) Carousel semantics and controls
- File: `src/components/HeroCarousel.jsx`
- Snippet:
```jsx
<div
className="relative w-full max-w-6xl mx-auto rounded-2xl overflow-hidden shadow-2xl bg-white"
role="region"
aria-roledescription="carousel"
aria-label="Featured content"
>
{slides.map((slide, index) => (
<div
key={index}
role="group"
aria-roledescription="slide"
aria-label={`${index + 1} of ${slides.length}`}
className={...}
>
...
</div>
))}
<button aria-pressed={!isPlaying} aria-label={isPlaying ? 'Pause slideshow' : 'Play slideshow'}>...</button>
</div>
```
- Rationale: Conveys carousel/slide semantics; exposes play/pause state (WCAG 1.4.2, 2.2.2, 4.1.2).
5) Contact form ARIA and honeypot
- File: `src/components/ContactForm.jsx`
- Snippet:
```jsx
const [hp, setHp] = useState('');
...
<form onSubmit={handleSubmit} noValidate ...>
<div className="hidden" aria-hidden="true">
<label htmlFor="website">Website</label>
<input id="website" name="website" value={hp} onChange={(e)=>setHp(e.target.value)} tabIndex={-1} autoComplete="off" />
</div>
<input aria-invalid={Boolean(errors.name)} aria-describedby={errors.name ? 'name-error' : undefined} ... />
{errors.name && <p id="name-error" className="...">{errors.name}</p>}
```
- Rationale: Accessible errors association and simple anti-bot (WCAG 3.3.1/3.3.3).
6) Lightbox dialog semantics
- File: `src/components/Lightbox.jsx`
- Snippet:
```jsx
<div
className="fixed inset-0 ..."
role="dialog"
aria-modal="true"
aria-label="Image viewer"
onClick={onClose}
>
```
- Rationale: Dialog semantics and modality (WCAG 1.3.1, 2.4.3, 4.1.2).
7) Inline styles and data-URI backgrounds limit strict CSP
- Files: `src/pages/about.astro`, `src/pages/donate.astro`, `src/pages/orphanage.astro`
- Fix: Extract inline `style="background: ..."` to named CSS classes in `src/styles/main.css` (e.g., `.bg-flag-nl-ng`, `.pattern-overlay`) and reference via `class` only. Move inline `<script>` in layout to a module file and import with `type="module"` and `nonce`.
- Rationale: Enables strict non-`unsafe-inline` CSP (security and robustness).
---
### Medium-priority issues
- **Contrast over imagery/gradients**: Ensure minimum overlay like `bg-black/60` behind text; avoid fully transparent gradient text for body copy. Files: `src/pages/index.astro`, `src/pages/about.astro`, `HeroCarousel.jsx`.
- **Focus visibility baseline**: Add base `:focus-visible` style in `src/styles/global.css` to ensure consistent visible focus.
- **Image dimensions**: Add `width`/`height` on `<img>` where known to prevent CLS.
---
### Suggested global utilities
- File: `src/styles/global.css`
```css
:focus-visible { outline: 2px solid var(--nigerian-green); outline-offset: 2px; }
[role="button"], a, button, input, select, textarea { -webkit-tap-highlight-color: transparent; }
```
---
### Verification plan (to add in CI)
- Playwright + `@axe-core/playwright`: nav landmarks, skip link focus, keyboard-only menu, form labels/errors, dialog semantics.
- Lighthouse budgets: LCP < 2.0s, CLS < 0.05, TTI < 3.5s; run on PR and fail if exceeded.
- Spot-check color contrast with axe; prohibit text over images without overlay.

42
PRODUCT_BRIEF.md Normal file
View File

@@ -0,0 +1,42 @@
## Product Brief — Omoluabi (NL)
### Audience
- Nigerians living in the Netherlands (families, youth, professionals)
- Dutch institutions and partners
- Donors and volunteers
### Objectives
- Community growth and engagement (events, programs, volunteering)
- Cultural education and representation
- Fundraising for programs and orphanage
- Trust through accessibility, privacy, and transparency
### KPIs / Success metrics
- Monthly active visitors (MAU) and returning visitors
- Event registrations and attendance rate
- Volunteer sign-ups and inquiries
- Donations volume via iDEAL/cards (Mollie)
- Newsletter subscriptions (double opt-in)
- Accessibility: axe violations = 0 on core templates; Lighthouse: LCP < 2.0s, CLS < 0.05
### Value proposition
A fast, inclusive, bilingual (EN/NL) hub connecting the Nigerian diaspora and Dutch partners, showcasing culture, programs, and ways to contributebuilt privacy-first and accessible by default.
---
### Gap report (current site)
- **Internationalization**: No locale-aware routing or page `lang`; no language switcher. Impact: accessibility, SEO, Dutch partners.
- **Accessibility**: Missing skip links; inconsistent focus; carousel lacks semantics; form errors lack ARIA; dialog lacks `role="dialog"`. Impact: keyboard and screen reader users.
- **Security/Privacy**: Google Fonts import; inline scripts/styles block strict CSP; no headers/CSP; forms not server-validated; no anti-abuse. Impact: GDPR risk and security posture.
- **Donations**: UI-only progress bar, no payments backend; no iDEAL/cards. Impact: fundraising.
- **Analytics**: None; decision pending (Plausible/Matomo). Impact: insight and optimization.
- **Content model**: Content lives in Markdown ad-hoc; lacks structured collections for posts/programs/team/partners; no Zod schemas.
- **Images/Performance**: Static `<img>` without `width/height` and `<picture>`; risky contrast over images; potential CLS.
---
### Prioritized roadmap (Now/Next/Later)
- **Now (this sprint)**: Implement headers/CSP-ready refactors, a11y fixes (skip link, menu ARIA, carousel/dialog semantics, focus baseline), self-host fonts, scaffold content collections with Zod, propose IA and i18n routing.
- **Next**: Implement i18n routes (`/en/`, `/nl/`), language switcher, interactive events calendar with filters and ICS export, server forms (contact/volunteer/partner), Plausible EU.
- **Later**: Mollie donation flow, Leaflet map for events/partners, PWA, program directory, newsletter double opt-in, Lighthouse/axe CI.

121
SECURITY_PRIVACY_AUDIT.md Normal file
View File

@@ -0,0 +1,121 @@
### Security & Privacy Audit (EU/GDPR)
Scope: Current Astro site; no server endpoints yet; React islands used. Aim: strict security headers, EU analytics, consent only if needed, secrets hygiene, safe forms, and payment integration readiness.
---
### Executive summary
- **Strengths**: Static-first Astro; no server auth; minimal third parties.
- **Gaps**: External Google Fonts (tracking and CSP issues), inline scripts/styles, no security headers configured, forms lack server validation/rate-limiting/CSRF, no cookie/consent model, no analytics choice, donation flow not integrated with EU processor.
---
### Action checklist (priority)
1) Remove Google Fonts; self-host Inter variable
- File: `src/styles/global.css`
- Replace `@import` from Google with self-hosted font. Steps:
- Add `public/fonts/inter/Inter-Variable.woff2`.
- Add to `src/styles/global.css`:
```css
@font-face {
font-family: 'Inter';
src: url('/fonts/inter/Inter-Variable.woff2') format('woff2-variations');
font-weight: 100 900; font-style: normal; font-display: swap;
}
```
- Rationale: Privacy, performance, CSP friendliness.
2) Security headers
- Option A: Static hosting `_headers` (e.g., Netlify/Vercel):
```text
/*
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=()
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp; report-to="default"
Cross-Origin-Resource-Policy: same-origin
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-PLACEHOLDER'; style-src 'self' 'nonce-PLACEHOLDER'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
```
- Option B: Nginx snippet (`nginx.conf.example`):
```nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options nosniff;
add_header Referrer-Policy "strict-origin-when-cross-origin";
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()";
add_header Cross-Origin-Opener-Policy "same-origin";
add_header Cross-Origin-Embedder-Policy "require-corp";
add_header Cross-Origin-Resource-Policy "same-origin";
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-$request_id'; style-src 'self' 'nonce-$request_id'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'";
```
- Rationale: Baseline browser protections; strict CSP with nonces for inline modules.
3) Inline → external scripts/styles to satisfy CSP
- Files: `src/layouts/BaseLayout.astro` (inline `<script>`), several pages with inline `style="background..."`.
- Fix: Move JS to `src/scripts/navigation.ts` and import with `<script type="module" src="/scripts/navigation.js" nonce={Astro.props.nonce} />`; extract inline styles into classes in `src/styles/main.css`.
4) Forms: server validation, CSRF, rate-limit, honeypot
- Add endpoint example: `src/pages/api/contact.ts`
```ts
import type { APIRoute } from 'astro';
import { z } from 'zod';
const ContactSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
subject: z.string().min(1),
message: z.string().min(1),
website: z.string().max(0).optional(), // honeypot
});
export const POST: APIRoute = async ({ request }) => {
try {
const data = await request.json();
const parsed = ContactSchema.safeParse(data);
if (!parsed.success) return new Response(JSON.stringify({ ok:false }), { status: 400 });
// TODO: rate-limit via IP + UA (e.g., in-memory or KV), CSRF via double-submit token header
// TODO: send via Postmark/Resend EU region
return new Response(JSON.stringify({ ok:true }), { status: 200 });
} catch {
return new Response(JSON.stringify({ ok:false }), { status: 400 });
}
};
```
- Rationale: GDPR-friendly collection, safe error responses, anti-abuse measures.
5) Payments: Mollie integration plan
- Use server-only handlers for payment creation and webhook signature verification. Store nothing beyond required payment IDs; no card data. Configure allowlisted endpoints in CSP `connect-src` for Mollie API only when enabled.
6) Analytics: Plausible (EU) or Matomo (self-host)
- Recommendation: **Plausible EU** with cookieless, events limited to pageview + donate/volunteer conversions. If consent model needed later, implement prior to enabling cookies.
7) Secrets and environment hygiene
- Use `import.meta.env` and never expose secrets without `*_PUBLIC` prefix. Add `.env.example`:
```env
PUBLIC_SITE_URL=
NODE_ENV=development
# Analytics
PUBLIC_PLAUSIBLE_DOMAIN=
PUBLIC_PLAUSIBLE_SRC=
# Payments (server-only)
MOLLIE_API_KEY=
MOLLIE_WEBHOOK_SECRET=
# Email provider
POSTMARK_TOKEN=
```
8) Logging and retention
- Adopt structured logs without PII; error IDs returned to client; retain logs 3090 days; no access logs with IP persisted beyond ops needs; document in Privacy Policy.
9) DPIA-lite
- Data categories: contact form (identifiers and message body), newsletter (email), donation (processor-hosted, no card data). Lawful basis: consent/legitimate interest. No special category data processed. Document processor agreements (Plausible EU, Postmark EU region, Mollie EU).
---
### Next steps
- Implement headers and CSP; refactor inline code.
- Add API routes with Zod and anti-abuse; wire contact form to server.
- Choose analytics (Plausible EU) and configure opt-in text in footer.
- Prepare Mollie sandbox integration and webhook.

View File

@@ -11,6 +11,7 @@ export default function ContactForm() {
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [submitMessage, setSubmitMessage] = useState(null); const [submitMessage, setSubmitMessage] = useState(null);
const [hp, setHp] = useState('');
const validate = () => { const validate = () => {
let newErrors = {}; let newErrors = {};
@@ -61,7 +62,11 @@ export default function ContactForm() {
}; };
return ( return (
<form onSubmit={handleSubmit} className="bg-white p-8 rounded-xl shadow-lg border border-gray-200 max-w-2xl mx-auto"> <form onSubmit={handleSubmit} className="bg-white p-8 rounded-xl shadow-lg border border-gray-200 max-w-2xl mx-auto" noValidate>
<div className="hidden" aria-hidden="true">
<label htmlFor="website">Website</label>
<input id="website" name="website" value={hp} onChange={(e)=>setHp(e.target.value)} tabIndex={-1} autoComplete="off" />
</div>
<h2 className="text-3xl font-bold text-center text-nigerian-green-700 mb-8">Send Us a Message</h2> <h2 className="text-3xl font-bold text-center text-nigerian-green-700 mb-8">Send Us a Message</h2>
{submitMessage && ( {submitMessage && (
@@ -78,10 +83,12 @@ export default function ContactForm() {
name="name" name="name"
value={formData.name} value={formData.name}
onChange={handleChange} onChange={handleChange}
aria-invalid={Boolean(errors.name)}
aria-describedby={errors.name ? 'name-error' : undefined}
className={`shadow appearance-none border rounded-lg w-full py-3 px-4 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-nigerian-green-500 ${errors.name ? 'border-red-500' : 'border-gray-300'}`} className={`shadow appearance-none border rounded-lg w-full py-3 px-4 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-nigerian-green-500 ${errors.name ? 'border-red-500' : 'border-gray-300'}`}
placeholder="Your Name" placeholder="Your Name"
/> />
{errors.name && <p className="text-red-500 text-xs italic mt-2">{errors.name}</p>} {errors.name && <p id="name-error" className="text-red-500 text-xs italic mt-2">{errors.name}</p>}
</div> </div>
<div className="mb-5"> <div className="mb-5">

View File

@@ -78,7 +78,8 @@ export default function HeroCarousel() {
}; };
return ( return (
<div className="relative w-full max-w-6xl mx-auto rounded-2xl overflow-hidden shadow-2xl bg-white"> <div className="relative w-full max-w-6xl mx-auto rounded-2xl overflow-hidden shadow-2xl bg-white"
role="region" aria-roledescription="carousel" aria-label="Featured content">
{/* Main Carousel Container */} {/* Main Carousel Container */}
<div className="relative h-[600px] overflow-hidden"> <div className="relative h-[600px] overflow-hidden">
{slides.map((slide, index) => { {slides.map((slide, index) => {
@@ -93,6 +94,9 @@ export default function HeroCarousel() {
? 'opacity-0 -translate-x-full' ? 'opacity-0 -translate-x-full'
: 'opacity-0 translate-x-full' : 'opacity-0 translate-x-full'
}`} }`}
role="group"
aria-roledescription="slide"
aria-label={`${index + 1} of ${slides.length}`}
> >
{/* Background Image */} {/* Background Image */}
<div className="absolute inset-0"> <div className="absolute inset-0">
@@ -194,6 +198,7 @@ export default function HeroCarousel() {
onClick={() => setIsPlaying(!isPlaying)} onClick={() => setIsPlaying(!isPlaying)}
className="absolute top-4 right-4 bg-white/20 backdrop-blur-sm hover:bg-white/30 text-white p-2 rounded-full transition-all duration-300 hover:scale-110" className="absolute top-4 right-4 bg-white/20 backdrop-blur-sm hover:bg-white/30 text-white p-2 rounded-full transition-all duration-300 hover:scale-110"
aria-label={isPlaying ? "Pause slideshow" : "Play slideshow"} aria-label={isPlaying ? "Pause slideshow" : "Play slideshow"}
aria-pressed={!isPlaying}
> >
{isPlaying ? ( {isPlaying ? (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"> <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">

View File

@@ -21,7 +21,8 @@ export default function Lightbox({ images, currentIndex, onClose, onNext, onPrev
if (!images || images.length === 0) return null; if (!images || images.length === 0) return null;
return ( return (
<div className="fixed inset-0 z-[9999] bg-black bg-opacity-90 flex items-center justify-center p-4" onClick={onClose}> <div className="fixed inset-0 z-[9999] bg-black bg-opacity-90 flex items-center justify-center p-4" onClick={onClose}
role="dialog" aria-modal="true" aria-label="Image viewer">
<div className="relative max-w-5xl max-h-full" onClick={(e) => e.stopPropagation()}> {/* Prevent closing when clicking on image */} <div className="relative max-w-5xl max-h-full" onClick={(e) => e.stopPropagation()}> {/* Prevent closing when clicking on image */}
<img <img
src={images[currentIndex]} src={images[currentIndex]}

View File

@@ -1,26 +1,34 @@
--- ---
// src/layouts/BaseLayout.astro // src/layouts/BaseLayout.astro
import '../styles/global.css'; import '../styles/global.css';
import '../styles/main.css';
export interface Props { export interface Props {
title?: string; title?: string;
description?: string; description?: string;
lang?: string;
} }
const { title = "Omoluabi Association Netherlands", description = "Preserving Nigerian culture and heritage in the Netherlands" } = Astro.props; const { title = "Omoluabi Association Netherlands", description = "Preserving Nigerian culture and heritage in the Netherlands" } = Astro.props;
--- ---
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang={Astro.props.lang ?? 'en'}>
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title> <title>{title}</title>
<meta name="description" content={description} /> <meta name="description" content={description} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="alternate" hreflang="en" href="/en/" />
<link rel="alternate" hreflang="nl" href="/nl/" />
<link rel="alternate" hreflang="x-default" href="/" />
</head> </head>
<body class="bg-gray-50"> <body class="bg-gray-50">
<a href="#main" class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:bg-white focus:text-black focus:px-3 focus:py-2 focus:rounded">
Skip to main content
</a>
<!-- Navigation Header --> <!-- Navigation Header -->
<header class="bg-white shadow-sm sticky top-0 z-50"> <header class="bg-white shadow-sm sticky top-0 z-50">
<nav class="container mx-auto px-4 py-4"> <nav class="container mx-auto px-4 py-4">
@@ -58,7 +66,7 @@ const { title = "Omoluabi Association Netherlands", description = "Preserving Ni
</div> </div>
<!-- Mobile menu button --> <!-- Mobile menu button -->
<button class="md:hidden" id="mobile-menu-button"> <button class="md:hidden" id="mobile-menu-button" aria-controls="mobile-menu" aria-expanded="false">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg> </svg>
@@ -85,7 +93,7 @@ const { title = "Omoluabi Association Netherlands", description = "Preserving Ni
</header> </header>
<!-- Main Content --> <!-- Main Content -->
<main> <main id="main" tabindex="-1">
<slot /> <slot />
</main> </main>
@@ -136,34 +144,22 @@ const { title = "Omoluabi Association Netherlands", description = "Preserving Ni
<!-- Scripts --> <!-- Scripts -->
<script> <script>
// Mobile menu toggle
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const mobileMenuButton = document.getElementById('mobile-menu-button'); const mobileMenuButton = document.getElementById('mobile-menu-button');
const mobileMenu = document.getElementById('mobile-menu'); const mobileMenu = document.getElementById('mobile-menu');
if (mobileMenuButton && mobileMenu) { if (mobileMenuButton && mobileMenu) {
mobileMenuButton.addEventListener('click', () => { mobileMenuButton.addEventListener('click', () => {
mobileMenu.classList.toggle('hidden'); const isHidden = mobileMenu.classList.toggle('hidden');
mobileMenuButton.setAttribute('aria-expanded', String(!isHidden));
}); });
} }
// Intersection Observer for scroll animations const observerOptions = { threshold: 0.1, rootMargin: '0px 0px -50px 0px' };
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => { const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('visible'); } });
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
}, observerOptions); }, observerOptions);
document.querySelectorAll('[data-animate-on-scroll]').forEach(el => observer.observe(el));
// Observe all elements with animation attributes
const animatedElements = document.querySelectorAll('[data-animate-on-scroll]');
animatedElements.forEach(el => observer.observe(el));
}); });
</script> </script>
</body> </body>

View File

@@ -3,21 +3,93 @@ import BaseLayout from '../layouts/BaseLayout.astro';
--- ---
<BaseLayout> <BaseLayout>
<!-- Page Header --> <!-- Page Header with Flag Integration -->
<section class="relative py-16 px-4 text-center text-white overflow-hidden bg-gradient-to-br from-nigerian-green-500 via-kente-gold-500 to-ankara-red-500" data-animate-on-scroll="fade-in"> <section class="relative py-16 px-4 text-center text-white overflow-hidden" style="
<div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.1"%3E%3Cpath d="M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div> background:
linear-gradient(90deg, var(--nigerian-green) 0%, var(--nigerian-green) 33.33%, #fff 33.33%, #fff 66.66%, var(--nigerian-green) 66.66%, var(--nigerian-green) 100%),
linear-gradient(180deg, var(--dutch-red) 0%, var(--dutch-red) 33.33%, #fff 33.33%, #fff 66.66%, var(--dutch-blue) 66.66%, var(--dutch-blue) 100%);
background-size: 40% 100%, 40% 100%;
background-position: left, right;
background-repeat: no-repeat;
" data-animate-on-scroll="fade-in">
<!-- Flag Icons -->
<div class="absolute top-8 left-8 flex items-center gap-4 z-20">
<!-- Nigerian Flag -->
<div class="flex shadow-lg rounded-lg overflow-hidden border-2 border-white/20">
<div class="w-6 h-4 bg-nigerian-green-500"></div>
<div class="w-6 h-4 bg-white"></div>
<div class="w-6 h-4 bg-nigerian-green-500"></div>
</div>
<!-- Connection Symbol -->
<div class="text-white/80 text-sm font-bold">🤝</div>
<!-- Dutch Flag -->
<div class="flex flex-col shadow-lg rounded-lg overflow-hidden border-2 border-white/20">
<div class="w-18 h-2 bg-dutch-red-500"></div>
<div class="w-18 h-2 bg-white"></div>
<div class="w-18 h-2 bg-dutch-blue-500"></div>
</div>
</div>
<!-- Decorative Flag Elements -->
<div class="absolute top-8 right-8 flex items-center gap-2 z-20">
<div class="text-white/60 text-xs uppercase tracking-wider font-semibold">Nigeria 🇳🇬 • Netherlands 🇳🇱</div>
</div>
<!-- Subtle Pattern Overlay -->
<div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width=\"60\" height=\"60\" viewBox=\"0 0 60 60\" xmlns=\"http://www.w3.org/2000/svg\"%3E%3Cg fill=\"none\" fill-rule=\"evenodd\"%3E%3Cg fill=\"%23ffffff\" fill-opacity=\"0.1\"%3E%3Cpath d=\"M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z\"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div>
<!-- Main Content -->
<div class="relative z-10 max-w-4xl mx-auto"> <div class="relative z-10 max-w-4xl mx-auto">
<div class="flex items-center justify-center gap-2 mb-4 text-sm"> <!-- Breadcrumb with Flag Accents -->
<div class="flex items-center justify-center gap-2 mb-6 text-sm">
<a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a> <a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a>
<span class="text-white/60">•</span> <span class="text-white/60">•</span>
<span>About</span> <span>About</span>
</div> </div>
<!-- Title with Flag Integration -->
<div class="mb-6">
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine"> <h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine">
About Us About Us
</h1> </h1>
<p class="text-lg md:text-xl opacity-90 max-w-2xl mx-auto">
Discover the rich heritage and noble mission of the Omoluabi Association in the Netherlands. <!-- Flag Strip Under Title -->
<div class="flex items-center justify-center gap-1 mb-4">
<div class="flex rounded-sm overflow-hidden shadow-md">
<div class="h-1 w-12 bg-nigerian-green-500"></div>
<div class="h-1 w-12 bg-white"></div>
<div class="h-1 w-12 bg-nigerian-green-500"></div>
</div>
<div class="w-4 h-px bg-gradient-to-r from-white/60 to-transparent"></div>
<div class="text-white/60 text-xs">🤝</div>
<div class="w-4 h-px bg-gradient-to-l from-white/60 to-transparent"></div>
<div class="flex flex-col rounded-sm overflow-hidden shadow-md">
<div class="w-12 h-px bg-dutch-red-500"></div>
<div class="w-12 h-px bg-white"></div>
<div class="w-12 h-px bg-dutch-blue-500"></div>
</div>
</div>
</div>
<p class="text-lg md:text-xl opacity-90 max-w-2xl mx-auto leading-relaxed">
Discover the rich heritage and noble mission of the Omoluabi Association bridging
<span class="font-semibold text-nigerian-green-200">Nigerian culture</span> with
<span class="font-semibold text-dutch-blue-200">Dutch hospitality</span> in the Netherlands.
</p> </p>
<!-- Cultural Bridge Visual -->
<div class="mt-8 flex items-center justify-center gap-6">
<div class="flex items-center gap-2 bg-white/10 px-4 py-2 rounded-full backdrop-blur-sm border border-white/20">
<div class="w-6 h-4 bg-nigerian-green-500 rounded-sm shadow-sm"></div>
<span class="text-sm font-medium">Nigerian Heritage</span>
</div>
<div class="text-2xl animate-pulse">🌉</div>
<div class="flex items-center gap-2 bg-white/10 px-4 py-2 rounded-full backdrop-blur-sm border border-white/20">
<div class="w-6 h-4 bg-dutch-red-500 rounded-sm shadow-sm"></div>
<span class="text-sm font-medium">Dutch Integration</span>
</div>
</div>
</div> </div>
</section> </section>
@@ -283,7 +355,7 @@ import BaseLayout from '../layouts/BaseLayout.astro';
</section> </section>
<!-- CTA Section --> <!-- CTA Section -->
<section class="section bg-gradient-to-br from-nigerian-green-500 via-kente-gold-500 to-ankara-red-500 text-white text-center relative overflow-hidden" data-animate-on-scroll="fade-in"> <section class="section combined-flag-gradient text-white text-center relative overflow-hidden" data-animate-on-scroll="fade-in">
<div class="absolute top-10 left-10 w-32 h-32 bg-white/10 rounded-full animate-float"></div> <div class="absolute top-10 left-10 w-32 h-32 bg-white/10 rounded-full animate-float"></div>
<div class="absolute bottom-10 right-10 w-24 h-24 bg-white/10 rounded-full animate-float animation-delay-1s"></div> <div class="absolute bottom-10 right-10 w-24 h-24 bg-white/10 rounded-full animate-float animation-delay-1s"></div>

View File

@@ -5,13 +5,14 @@ import ContactForm from '../components/ContactForm.jsx';
<BaseLayout> <BaseLayout>
<!-- Page Header --> <!-- Page Header -->
<section class="relative py-16 px-4 text-center text-white overflow-hidden bg-gradient-to-br from-nigerian-green-500 via-kente-gold-500 to-ankara-red-500" data-animate-on-scroll="fade-in"> <section class="relative py-16 px-4 text-center text-white overflow-hidden nigerian-flag-bg" data-animate-on-scroll="fade-in">
<div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.1"%3E%3Cpath d="M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div> <div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.1"%3E%3Cpath d="M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div>
<div class="relative z-10 max-w-4xl mx-auto"> <div class="relative z-10 max-w-4xl mx-auto">
<div class="flex items-center justify-center gap-2 mb-4 text-sm"> <div class="flex items-center justify-center gap-2 mb-4 text-sm">
<a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a> <a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a>
<span class="text-white/60">•</span> <span class="text-white/60">•</span>
<span>Contact</span> <span>Contact</span>
<span class="ml-4"><span class="dutch-flag-badge"><span class="stripe red"></span><span class="stripe white"></span><span class="stripe blue"></span></span></span>
</div> </div>
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine"> <h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine">
Contact Us Contact Us

View File

@@ -5,13 +5,14 @@ import DonationProgressBar from '../components/DonationProgressBar.jsx';
<BaseLayout> <BaseLayout>
<!-- Page Header --> <!-- Page Header -->
<section class="relative py-16 px-4 text-center text-white overflow-hidden bg-gradient-to-br from-nigerian-green-500 via-kente-gold-500 to-ankara-red-500" data-animate-on-scroll="fade-in"> <section class="relative py-16 px-4 text-center text-white overflow-hidden nigerian-flag-bg" data-animate-on-scroll="fade-in">
<div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.1"%3E%3Cpath d="M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div> <div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.1"%3E%3Cpath d="M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div>
<div class="relative z-10 max-w-4xl mx-auto"> <div class="relative z-10 max-w-4xl mx-auto">
<div class="flex items-center justify-center gap-2 mb-4 text-sm"> <div class="flex items-center justify-center gap-2 mb-4 text-sm">
<a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a> <a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a>
<span class="text-white/60">•</span> <span class="text-white/60">•</span>
<span>Donate</span> <span>Donate</span>
<span class="ml-4"><span class="dutch-flag-badge"><span class="stripe red"></span><span class="stripe white"></span><span class="stripe blue"></span></span></span>
</div> </div>
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine"> <h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine">
Support Our Mission Support Our Mission

View File

@@ -9,13 +9,14 @@ const sortedEvents = events.sort((a, b) => new Date(b.frontmatter?.date || 0) -
<BaseLayout> <BaseLayout>
<!-- Page Header --> <!-- Page Header -->
<section class="relative py-16 px-4 text-center text-white overflow-hidden bg-gradient-to-br from-nigerian-green-500 via-kente-gold-500 to-ankara-red-500" data-animate-on-scroll="fade-in"> <section class="relative py-16 px-4 text-center text-white overflow-hidden nigerian-flag-bg" data-animate-on-scroll="fade-in">
<div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width=\"60\" height=\"60\" viewBox=\"0 0 60 60\" xmlns=\"http://www.w3.org/2000/svg\"%3E%3Cg fill=\"none\" fill-rule=\"evenodd\"%3E%3Cg fill=\"%23ffffff\" fill-opacity=\"0.1\"%3E%3Cpath d=\"M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z\"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div> <div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width=\"60\" height=\"60\" viewBox=\"0 0 60 60\" xmlns=\"http://www.w3.org/2000/svg\"%3E%3Cg fill=\"none\" fill-rule=\"evenodd\"%3E%3Cg fill=\"%23ffffff\" fill-opacity=\"0.1\"%3E%3Cpath d=\"M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z\"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div>
<div class="relative z-10 max-w-4xl mx-auto"> <div class="relative z-10 max-w-4xl mx-auto">
<div class="flex items-center justify-center gap-2 mb-4 text-sm"> <div class="flex items-center justify-center gap-2 mb-4 text-sm">
<a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a> <a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a>
<span class="text-white/60">•</span> <span class="text-white/60">•</span>
<span>Events</span> <span>Events</span>
<span class="ml-4"><span class="dutch-flag-badge"><span class="stripe red"></span><span class="stripe white"></span><span class="stripe blue"></span></span></span>
</div> </div>
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine"> <h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine">
Our Events Our Events

View File

@@ -173,7 +173,7 @@ import HomeGallery from '../components/HomeGallery.astro';
</section> </section>
<!-- Donation Section --> <!-- Donation Section -->
<section class="section bg-gradient-to-br from-ankara-red-500 via-kente-gold-500 to-nigerian-green-500 text-white text-center relative overflow-hidden" data-animate-on-scroll="fade-in"> <section class="section combined-flag-gradient text-white text-center relative overflow-hidden" data-animate-on-scroll="fade-in">
<div class="absolute top-10 left-10 w-32 h-32 bg-white/10 rounded-full animate-float"></div> <div class="absolute top-10 left-10 w-32 h-32 bg-white/10 rounded-full animate-float"></div>
<div class="absolute bottom-10 right-10 w-24 h-24 bg-white/10 rounded-full animate-float animation-delay-1s"></div> <div class="absolute bottom-10 right-10 w-24 h-24 bg-white/10 rounded-full animate-float animation-delay-1s"></div>

View File

@@ -7,13 +7,14 @@ const members = memberFiles[0]?.frontmatter?.members || [];
<BaseLayout> <BaseLayout>
<!-- Page Header --> <!-- Page Header -->
<section class="relative py-16 px-4 text-center text-white overflow-hidden bg-gradient-to-br from-nigerian-green-500 via-kente-gold-500 to-ankara-red-500" data-animate-on-scroll="fade-in"> <section class="relative py-16 px-4 text-center text-white overflow-hidden nigerian-flag-bg" data-animate-on-scroll="fade-in">
<div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width=\"60\" height=\"60\" viewBox=\"0 0 60 60\" xmlns=\"http://www.w3.org/2000/svg\"%3E%3Cg fill=\"none\" fill-rule=\"evenodd\"%3E%3Cg fill=\"%23ffffff\" fill-opacity=\"0.1\"%3E%3Cpath d=\"M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z\"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div> <div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width=\"60\" height=\"60\" viewBox=\"0 0 60 60\" xmlns=\"http://www.w3.org/2000/svg\"%3E%3Cg fill=\"none\" fill-rule=\"evenodd\"%3E%3Cg fill=\"%23ffffff\" fill-opacity=\"0.1\"%3E%3Cpath d=\"M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z\"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div>
<div class="relative z-10 max-w-4xl mx-auto"> <div class="relative z-10 max-w-4xl mx-auto">
<div class="flex items-center justify-center gap-2 mb-4 text-sm"> <div class="flex items-center justify-center gap-2 mb-4 text-sm">
<a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a> <a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a>
<span class="text-white/60">•</span> <span class="text-white/60">•</span>
<span>Members</span> <span>Members</span>
<span class="ml-4"><span class="dutch-flag-badge"><span class="stripe red"></span><span class="stripe white"></span><span class="stripe blue"></span></span></span>
</div> </div>
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine"> <h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine">
Meet Our Members Meet Our Members

View File

@@ -4,13 +4,14 @@ import BaseLayout from '../layouts/BaseLayout.astro';
<BaseLayout> <BaseLayout>
<!-- Page Header --> <!-- Page Header -->
<section class="relative py-16 px-4 text-center text-white overflow-hidden bg-gradient-to-br from-nigerian-green-500 via-kente-gold-500 to-ankara-red-500" data-animate-on-scroll="fade-in"> <section class="relative py-16 px-4 text-center text-white overflow-hidden nigerian-flag-bg" data-animate-on-scroll="fade-in">
<div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.1"%3E%3Cpath d="M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div> <div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.1"%3E%3Cpath d="M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div>
<div class="relative z-10 max-w-4xl mx-auto"> <div class="relative z-10 max-w-4xl mx-auto">
<div class="flex items-center justify-center gap-2 mb-4 text-sm"> <div class="flex items-center justify-center gap-2 mb-4 text-sm">
<a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a> <a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a>
<span class="text-white/60">•</span> <span class="text-white/60">•</span>
<span>Orphanage</span> <span>Orphanage</span>
<span class="ml-4"><span class="dutch-flag-badge"><span class="stripe red"></span><span class="stripe white"></span><span class="stripe blue"></span></span></span>
</div> </div>
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine"> <h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine">
Our Orphanage Program Our Orphanage Program

View File

@@ -11,6 +11,10 @@
--kente-gold: #f59e0b; --kente-gold: #f59e0b;
--ankara-red: #dc2626; --ankara-red: #dc2626;
--adire-blue: #2563eb; --adire-blue: #2563eb;
/* Dutch Flag Colors */
--dutch-red: #21468b;
--dutch-white: #ffffff;
--dutch-blue: #1e4785;
} }
/* Custom scrollbar */ /* Custom scrollbar */

View File

@@ -211,11 +211,29 @@
box-shadow: 0 0 20px rgba(22, 163, 74, 0.6); box-shadow: 0 0 20px rgba(22, 163, 74, 0.6);
} }
/* Nigerian Flag Colors */ /* Nigerian Flag Background (vertical stripes) */
.nigerian-gradient { .nigerian-flag-bg {
background: linear-gradient(90deg, #16a34a 0%, #ffffff 50%, #16a34a 100%); background: linear-gradient(90deg, var(--nigerian-green) 0%, var(--nigerian-green) 33.33%, #fff 33.33%, #fff 66.66%, var(--nigerian-green) 66.66%, var(--nigerian-green) 100%);
} }
/* Dutch Flag Badge (horizontal stripes) */
.dutch-flag-badge {
width: 48px;
height: 16px;
border-radius: 3px;
overflow: hidden;
display: inline-block;
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
border: 1px solid #eee;
}
.dutch-flag-badge .stripe {
height: 33.33%;
width: 100%;
}
.dutch-flag-badge .red { background: var(--dutch-red); }
.dutch-flag-badge .white { background: #fff; }
.dutch-flag-badge .blue { background: var(--dutch-blue); }
/* Kente Pattern Inspired Background */ /* Kente Pattern Inspired Background */
.kente-pattern { .kente-pattern {
background-image: background-image:
@@ -342,3 +360,42 @@
color: #9ca3af; color: #9ca3af;
} }
} }
/* Nigerian + Dutch Flag Combined Gradient */
.combined-flag-gradient {
background: linear-gradient(90deg, var(--nigerian-green) 0%, var(--dutch-white) 20%, var(--dutch-red) 40%, var(--dutch-white) 60%, var(--dutch-blue) 80%, var(--nigerian-green) 100%);
}
/* Combined Flag Button */
.btn-combined-flag {
background: linear-gradient(135deg, var(--nigerian-green) 0%, var(--dutch-red) 33%, var(--dutch-white) 66%, var(--dutch-blue) 100%);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 0.75rem;
font-weight: 600;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.btn-combined-flag::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(135deg, var(--dutch-blue) 0%, var(--dutch-white) 50%, var(--nigerian-green) 100%);
transition: left 0.3s ease;
z-index: -1;
}
.btn-combined-flag:hover::before {
left: 0;
}
.btn-combined-flag:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
}