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:
148
ACCESSIBILITY_AUDIT.md
Normal file
148
ACCESSIBILITY_AUDIT.md
Normal 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
42
PRODUCT_BRIEF.md
Normal 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 contribute—built 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
121
SECURITY_PRIVACY_AUDIT.md
Normal 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 30–90 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.
|
@@ -11,6 +11,7 @@ export default function ContactForm() {
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitMessage, setSubmitMessage] = useState(null);
|
||||
const [hp, setHp] = useState('');
|
||||
|
||||
const validate = () => {
|
||||
let newErrors = {};
|
||||
@@ -61,7 +62,11 @@ export default function ContactForm() {
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
{submitMessage && (
|
||||
@@ -78,10 +83,12 @@ export default function ContactForm() {
|
||||
name="name"
|
||||
value={formData.name}
|
||||
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'}`}
|
||||
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 className="mb-5">
|
||||
|
@@ -78,7 +78,8 @@ export default function HeroCarousel() {
|
||||
};
|
||||
|
||||
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 */}
|
||||
<div className="relative h-[600px] overflow-hidden">
|
||||
{slides.map((slide, index) => {
|
||||
@@ -93,6 +94,9 @@ export default function HeroCarousel() {
|
||||
? 'opacity-0 -translate-x-full'
|
||||
: 'opacity-0 translate-x-full'
|
||||
}`}
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
aria-label={`${index + 1} of ${slides.length}`}
|
||||
>
|
||||
{/* Background Image */}
|
||||
<div className="absolute inset-0">
|
||||
@@ -194,6 +198,7 @@ export default function HeroCarousel() {
|
||||
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"
|
||||
aria-label={isPlaying ? "Pause slideshow" : "Play slideshow"}
|
||||
aria-pressed={!isPlaying}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
|
@@ -21,7 +21,8 @@ export default function Lightbox({ images, currentIndex, onClose, onNext, onPrev
|
||||
if (!images || images.length === 0) return null;
|
||||
|
||||
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 */}
|
||||
<img
|
||||
src={images[currentIndex]}
|
||||
|
@@ -1,26 +1,34 @@
|
||||
---
|
||||
// src/layouts/BaseLayout.astro
|
||||
import '../styles/global.css';
|
||||
import '../styles/main.css';
|
||||
|
||||
export interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
lang?: string;
|
||||
}
|
||||
|
||||
const { title = "Omoluabi Association Netherlands", description = "Preserving Nigerian culture and heritage in the Netherlands" } = Astro.props;
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang={Astro.props.lang ?? 'en'}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<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>
|
||||
|
||||
<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 -->
|
||||
<header class="bg-white shadow-sm sticky top-0 z-50">
|
||||
<nav class="container mx-auto px-4 py-4">
|
||||
@@ -58,7 +66,7 @@ const { title = "Omoluabi Association Netherlands", description = "Preserving Ni
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
@@ -85,7 +93,7 @@ const { title = "Omoluabi Association Netherlands", description = "Preserving Ni
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
<main id="main" tabindex="-1">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
@@ -136,34 +144,22 @@ const { title = "Omoluabi Association Netherlands", description = "Preserving Ni
|
||||
|
||||
<!-- Scripts -->
|
||||
<script>
|
||||
// Mobile menu toggle
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
|
||||
if (mobileMenuButton && mobileMenu) {
|
||||
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) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('visible');
|
||||
}
|
||||
});
|
||||
entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('visible'); } });
|
||||
}, observerOptions);
|
||||
|
||||
// Observe all elements with animation attributes
|
||||
const animatedElements = document.querySelectorAll('[data-animate-on-scroll]');
|
||||
animatedElements.forEach(el => observer.observe(el));
|
||||
document.querySelectorAll('[data-animate-on-scroll]').forEach(el => observer.observe(el));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
@@ -3,21 +3,93 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<!-- 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">
|
||||
<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>
|
||||
<!-- Page Header with Flag Integration -->
|
||||
<section class="relative py-16 px-4 text-center text-white overflow-hidden" style="
|
||||
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="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>
|
||||
<span class="text-white/60">•</span>
|
||||
<span>About</span>
|
||||
</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">
|
||||
About Us
|
||||
</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.
|
||||
|
||||
<!-- 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">
|
||||
About Us
|
||||
</h1>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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>
|
||||
</section>
|
||||
|
||||
@@ -283,7 +355,7 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
</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 bottom-10 right-10 w-24 h-24 bg-white/10 rounded-full animate-float animation-delay-1s"></div>
|
||||
|
||||
|
@@ -5,13 +5,14 @@ import ContactForm from '../components/ContactForm.jsx';
|
||||
|
||||
<BaseLayout>
|
||||
<!-- 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="relative z-10 max-w-4xl mx-auto">
|
||||
<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>
|
||||
<span class="text-white/60">•</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>
|
||||
<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
|
||||
|
@@ -5,13 +5,14 @@ import DonationProgressBar from '../components/DonationProgressBar.jsx';
|
||||
|
||||
<BaseLayout>
|
||||
<!-- 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="relative z-10 max-w-4xl mx-auto">
|
||||
<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>
|
||||
<span class="text-white/60">•</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>
|
||||
<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
|
||||
|
@@ -9,13 +9,14 @@ const sortedEvents = events.sort((a, b) => new Date(b.frontmatter?.date || 0) -
|
||||
|
||||
<BaseLayout>
|
||||
<!-- 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="relative z-10 max-w-4xl mx-auto">
|
||||
<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>
|
||||
<span class="text-white/60">•</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>
|
||||
<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
|
||||
|
@@ -173,7 +173,7 @@ import HomeGallery from '../components/HomeGallery.astro';
|
||||
</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 bottom-10 right-10 w-24 h-24 bg-white/10 rounded-full animate-float animation-delay-1s"></div>
|
||||
|
||||
|
@@ -7,13 +7,14 @@ const members = memberFiles[0]?.frontmatter?.members || [];
|
||||
|
||||
<BaseLayout>
|
||||
<!-- 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="relative z-10 max-w-4xl mx-auto">
|
||||
<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>
|
||||
<span class="text-white/60">•</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>
|
||||
<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
|
||||
|
@@ -4,13 +4,14 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
|
||||
<BaseLayout>
|
||||
<!-- 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="relative z-10 max-w-4xl mx-auto">
|
||||
<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>
|
||||
<span class="text-white/60">•</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>
|
||||
<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
|
||||
|
@@ -11,6 +11,10 @@
|
||||
--kente-gold: #f59e0b;
|
||||
--ankara-red: #dc2626;
|
||||
--adire-blue: #2563eb;
|
||||
/* Dutch Flag Colors */
|
||||
--dutch-red: #21468b;
|
||||
--dutch-white: #ffffff;
|
||||
--dutch-blue: #1e4785;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
|
@@ -211,11 +211,29 @@
|
||||
box-shadow: 0 0 20px rgba(22, 163, 74, 0.6);
|
||||
}
|
||||
|
||||
/* Nigerian Flag Colors */
|
||||
.nigerian-gradient {
|
||||
background: linear-gradient(90deg, #16a34a 0%, #ffffff 50%, #16a34a 100%);
|
||||
/* Nigerian Flag Background (vertical stripes) */
|
||||
.nigerian-flag-bg {
|
||||
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 {
|
||||
background-image:
|
||||
@@ -342,3 +360,42 @@
|
||||
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);
|
||||
}
|
Reference in New Issue
Block a user