feat(brand): official logo, forest green/gold theme, and favicon set

Wire public/logo.png into nav/footer LogoMark with responsive sizing,
remap primary/secondary Tailwind scales to brand colors, and refresh
CTAs, PWA manifest, and favicon assets for mim4u.org.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
defiQUG
2026-05-26 18:14:43 -07:00
co-authored by Cursor
parent 101fdb90e2
commit f5d305bfcb
35 changed files with 490 additions and 335 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

+3 -3
View File
@@ -4,6 +4,8 @@
<meta charset="UTF-8" />
<!-- CSP is applied at nginx/reverse-proxy in production (see MIM4U_502_ERROR_RESOLUTION.md). Meta CSP omitted to avoid breaking dev/build. -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Miracles In Motion | 501(c)3 Non-Profit Organization</title>
<meta name="description" content="Miracles In Motion is a 501(c)3 non-profit organization dedicated to creating positive change in our community through compassionate action and support." />
@@ -24,12 +26,10 @@
<!-- Favicon and Web App Manifest -->
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<!-- Theme Color -->
<meta name="theme-color" content="#ec4899" />
<meta name="theme-color" content="#1a3c34" />
<!-- Fonts: self-hosted via @fontsource/inter (see src/index.css); no external font requests for LCP -->
</head>
+10
View File
@@ -10,6 +10,7 @@
"license": "MIT",
"dependencies": {
"@fontsource/inter": "^5.2.8",
"@fontsource/playfair-display": "^5.2.8",
"@react-three/fiber": "^8.16.8",
"@stripe/react-stripe-js": "^2.8.1",
"@stripe/stripe-js": "^4.7.0",
@@ -2417,6 +2418,15 @@
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/playfair-display": {
"version": "5.2.8",
"resolved": "https://registry.npmjs.org/@fontsource/playfair-display/-/playfair-display-5.2.8.tgz",
"integrity": "sha512-fUEhib70SszNhQVsGbUMSsWJhr7Je0rdeuZdtGpDNu0GKF1xJM8QhpI/y0pckU25GcChXm9TLOmeZupkvvZo2g==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
+1
View File
@@ -38,6 +38,7 @@
"homepage": "https://miraclesinmotion.org",
"dependencies": {
"@fontsource/inter": "^5.2.8",
"@fontsource/playfair-display": "^5.2.8",
"@react-three/fiber": "^8.16.8",
"@stripe/react-stripe-js": "^2.8.1",
"@stripe/stripe-js": "^4.7.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

+14 -13
View File
@@ -1,13 +1,14 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#ec4899;stop-opacity:1" />
<stop offset="50%" style="stop-color:#8b5cf6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#3b82f6;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="32" height="32" rx="8" fill="url(#grad1)"/>
<path d="M16 8L20.5 14H11.5L16 8Z" fill="white" opacity="0.9"/>
<circle cx="16" cy="18" r="3" fill="white" opacity="0.9"/>
<path d="M10 22L16 20L22 22L20 26H12L10 22Z" fill="white" opacity="0.9"/>
</svg>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Miracles in Motion Foundation">
<rect width="32" height="32" rx="6" fill="#1a3c34"/>
<!-- Stylized figure + starburst (brand mark simplification) -->
<circle cx="16" cy="7.5" r="2.2" fill="#d4af37"/>
<path d="M11 22c2-5 4.5-7.5 5-12 1.2 4 3.2 6.5 5 12" stroke="#c5a059" stroke-width="1.8" stroke-linecap="round" fill="none"/>
<path d="M13.5 11.5l2.5-3 2.5 3" stroke="#d4af37" stroke-width="1.2" stroke-linecap="round" fill="none"/>
<g stroke="#d4af37" stroke-width="0.7" stroke-linecap="round">
<line x1="20" y1="6" x2="20" y2="3.5"/>
<line x1="22.5" y1="7" x2="24.5" y2="5.5"/>
<line x1="23.5" y1="9.5" x2="26" y2="9.5"/>
<line x1="17.5" y1="7" x2="15.5" y2="5.5"/>
<line x1="16.5" y1="9.5" x2="14" y2="9.5"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 695 B

After

Width:  |  Height:  |  Size: 889 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

+15 -7
View File
@@ -1,21 +1,29 @@
{
"name": "Miracles In Motion",
"short_name": "MiraclesInMotion",
"name": "Miracles In Motion Foundation",
"short_name": "MiM",
"description": "A 501(c)3 non-profit providing essentials for student success",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#ec4899",
"background_color": "#1a3c34",
"theme_color": "#1a3c34",
"icons": [
{
"src": "/favicon-192x192.png",
"sizes": "192x192",
"type": "image/png"
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/favicon-512x512.png",
"sizes": "512x512",
"type": "image/png"
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/favicon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
}
],
"categories": ["education", "social", "non-profit"],
@@ -25,4 +33,4 @@
"scope": "/",
"related_applications": [],
"prefer_related_applications": false
}
}
+19 -19
View File
@@ -1493,7 +1493,7 @@ function Testimonial() {
function GetInvolved() {
const options = [
{ title: "Donate", body: "Fuel fast responses for urgent student needs.", href: "#/donate", accent: "from-rose-500 to-primary-500", icon: Heart },
{ title: "Donate", body: "Fuel fast responses for urgent student needs.", href: "#/donate", accent: "from-secondary-500 to-primary-500", icon: Heart },
{ title: "Volunteer", body: "Assemble kits, deliver items, host a drive.", href: "#/volunteers", accent: "from-sky-500 to-secondary-500", icon: Users },
{ title: "Corporate sponsor", body: "Schools and community orgs: let's team up.", href: "#/sponsors", accent: "from-emerald-500 to-lime-500", icon: Globe },
]
@@ -2939,7 +2939,7 @@ function PortalsPage() {
<div className="grid gap-8 md:grid-cols-2 xl:grid-cols-4">
{/* Admin Portal */}
<motion.div
className="card bg-gradient-to-br from-red-50 to-rose-50 dark:from-red-900/20 dark:to-rose-900/20 border-red-200 dark:border-red-800"
className="card bg-gradient-to-br from-red-50 to-secondary-50 dark:from-red-900/20 dark:to-secondary-900/20 border-red-200 dark:border-red-800"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
@@ -2979,7 +2979,7 @@ function PortalsPage() {
{/* Volunteer Portal */}
<motion.div
className="card bg-gradient-to-br from-blue-50 to-sky-50 dark:from-blue-900/20 dark:to-sky-900/20 border-blue-200 dark:border-blue-800"
className="card bg-gradient-to-br from-primary-50 to-sky-50 dark:from-primary-900/20 dark:to-sky-900/20 border-blue-200 dark:border-blue-800"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
@@ -3059,22 +3059,22 @@ function PortalsPage() {
{/* AI Assistance Portal - Phase 3 */}
<motion.div
className="card bg-gradient-to-br from-purple-50 to-violet-50 dark:from-purple-900/20 dark:to-violet-900/20 border-purple-200 dark:border-purple-800"
className="card bg-gradient-to-br from-primary-50 to-primary-50 dark:from-primary-900/20 dark:to-primary-900/20 border-primary-200 dark:border-primary-800"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
>
<div className="text-center mb-6">
<div className="mx-auto w-16 h-16 bg-purple-600 text-white rounded-xl flex items-center justify-center mb-4">
<div className="mx-auto w-16 h-16 bg-primary-600 text-white rounded-xl flex items-center justify-center mb-4">
<Brain className="h-8 w-8" />
</div>
<h3 className="text-xl font-semibold text-purple-900 dark:text-purple-100">AI Assistance Portal</h3>
<p className="text-sm text-purple-700 dark:text-purple-300 mt-2">
<h3 className="text-xl font-semibold text-primary-900 dark:text-primary-100">AI Assistance Portal</h3>
<p className="text-sm text-primary-700 dark:text-primary-300 mt-2">
AI-powered request matching and insights
</p>
</div>
<div className="space-y-2 text-sm text-purple-800 dark:text-purple-200 mb-6">
<div className="space-y-2 text-sm text-primary-800 dark:text-primary-200 mb-6">
<div className="flex items-center gap-2">
<Cpu className="h-4 w-4" />
<span>Real-time AI request processing</span>
@@ -3091,7 +3091,7 @@ function PortalsPage() {
<a
href="#/ai-portal"
className="block w-full text-center bg-purple-600 text-white py-3 px-4 rounded-lg hover:bg-purple-700 transition-colors font-medium focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
className="block w-full text-center bg-primary-600 text-white py-3 px-4 rounded-lg hover:bg-primary-700 transition-colors font-medium focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
>
Access AI Portal
</a>
@@ -3108,7 +3108,7 @@ function PortalsPage() {
<div className="grid gap-8 md:grid-cols-2 xl:grid-cols-3">
{/* Advanced Analytics */}
<motion.div
className="card bg-gradient-to-br from-indigo-50 to-blue-50 dark:from-indigo-900/20 dark:to-blue-900/20 border-indigo-200 dark:border-indigo-800"
className="card bg-gradient-to-br from-indigo-50 to-primary-50 dark:from-indigo-900/20 dark:to-primary-900/20 border-indigo-200 dark:border-indigo-800"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
@@ -4115,7 +4115,7 @@ function AnalyticsDashboard() {
{/* Key Metrics */}
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
<motion.div
className="card bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20 border-blue-200 dark:border-blue-800"
className="card bg-gradient-to-br from-primary-50 to-primary-100 dark:from-primary-900/20 dark:to-primary-800/20 border-blue-200 dark:border-blue-800"
whileHover={{ scale: 1.02, y: -2 }}
>
<div className="flex items-center justify-between">
@@ -4151,19 +4151,19 @@ function AnalyticsDashboard() {
</motion.div>
<motion.div
className="card bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/20 border-purple-200 dark:border-purple-800"
className="card bg-gradient-to-br from-primary-50 to-primary-100 dark:from-primary-900/20 dark:to-primary-800/20 border-primary-200 dark:border-primary-800"
whileHover={{ scale: 1.02, y: -2 }}
>
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-purple-600 dark:text-purple-400">Students Helped</p>
<p className="text-2xl font-bold text-purple-900 dark:text-purple-100">1,847</p>
<p className="text-sm text-primary-600 dark:text-primary-400">Students Helped</p>
<p className="text-2xl font-bold text-primary-900 dark:text-primary-100">1,847</p>
</div>
<Target className="h-8 w-8 text-purple-500" />
<Target className="h-8 w-8 text-primary-500" />
</div>
<div className="mt-2 flex items-center text-sm">
<span className="text-green-600 dark:text-green-400 font-medium">+15.2%</span>
<span className="text-purple-600 dark:text-purple-400 ml-1">vs last month</span>
<span className="text-primary-600 dark:text-primary-400 ml-1">vs last month</span>
</div>
</motion.div>
@@ -4273,12 +4273,12 @@ function AnalyticsDashboard() {
</div>
<span className="text-2xl font-bold text-blue-600">789</span>
</div>
<div className="flex items-center justify-between p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<div className="flex items-center justify-between p-4 bg-primary-50 dark:bg-primary-900/20 rounded-lg">
<div className="flex items-center gap-3">
<AlertCircle className="h-6 w-6 text-purple-600" />
<AlertCircle className="h-6 w-6 text-primary-600" />
<span className="font-medium">Emergency Responses</span>
</div>
<span className="text-2xl font-bold text-purple-600">156</span>
<span className="text-2xl font-bold text-primary-600">156</span>
</div>
</div>
</div>
+1 -1
View File
@@ -77,7 +77,7 @@ const App: React.FC = () => {
return (
<AuthProvider>
<NotificationProvider>
<div className="min-h-screen bg-gradient-to-br from-purple-50 via-white to-pink-50 dark:from-gray-900 dark:via-gray-800 dark:to-purple-900">
<div className="min-h-screen bg-gradient-to-br from-primary-50 via-white to-secondary-50 dark:from-gray-900 dark:via-gray-800 dark:to-primary-900">
{/* Navigation */}
<Navigation
darkMode={darkMode}
+7 -7
View File
@@ -192,7 +192,7 @@ export function AIAssistancePortal({ userRole }: AIAssistancePortalProps) {
animate={{ opacity: 1, x: 0 }}
>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2 text-gray-900 dark:text-gray-100">
<Brain className="w-5 h-5 text-purple-500" />
<Brain className="w-5 h-5 text-primary-500" />
AI Insights
</h3>
@@ -200,7 +200,7 @@ export function AIAssistancePortal({ userRole }: AIAssistancePortalProps) {
{aiInsights.map((insight) => (
<motion.div
key={insight.id}
className="insight-card p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg mb-3 border border-purple-100 dark:border-purple-800"
className="insight-card p-3 bg-primary-50 dark:bg-primary-900/20 rounded-lg mb-3 border border-primary-100 dark:border-primary-800"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
@@ -217,7 +217,7 @@ export function AIAssistancePortal({ userRole }: AIAssistancePortalProps) {
<div className="mt-2 flex items-center gap-2">
<div className="w-16 bg-gray-200 dark:bg-gray-600 rounded-full h-1">
<div
className="bg-purple-500 h-1 rounded-full transition-all duration-300"
className="bg-primary-500 h-1 rounded-full transition-all duration-300"
style={{ width: `${insight.confidence * 100}%` }}
/>
</div>
@@ -324,7 +324,7 @@ function RequestCard({ request, onApprove, onModify, showAIRecommendations, isSe
{showAIRecommendations && request.aiRecommendations && (
<motion.div
className="ai-recommendations bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 p-4 rounded-xl mb-4 border border-blue-200 dark:border-blue-800"
className="ai-recommendations bg-gradient-to-r from-primary-50 to-indigo-50 dark:from-primary-900/20 dark:to-indigo-900/20 p-4 rounded-xl mb-4 border border-blue-200 dark:border-blue-800"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
transition={{ duration: 0.3 }}
@@ -542,14 +542,14 @@ function MetricCard({ title, value, trend, color, icon }: MetricCardProps) {
const colorClasses = {
green: 'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800',
blue: 'bg-blue-50 border-blue-200 dark:bg-blue-900/20 dark:border-blue-800',
purple: 'bg-purple-50 border-purple-200 dark:bg-purple-900/20 dark:border-purple-800',
purple: 'bg-primary-50 border-primary-200 dark:bg-primary-900/20 dark:border-primary-800',
orange: 'bg-orange-50 border-orange-200 dark:bg-orange-900/20 dark:border-orange-800'
}
const iconColors = {
green: 'text-green-500',
blue: 'text-blue-500',
purple: 'text-purple-500',
purple: 'text-primary-500',
orange: 'text-orange-500'
}
@@ -712,7 +712,7 @@ function getInsightColor(type: string): string {
case 'anomaly': return 'bg-red-400'
case 'optimization': return 'bg-green-400'
case 'trend': return 'bg-blue-400'
case 'prediction': return 'bg-purple-400'
case 'prediction': return 'bg-primary-400'
case 'recommendation': return 'bg-yellow-400'
default: return 'bg-gray-400'
}
@@ -140,7 +140,7 @@ const AdvancedAnalyticsDashboard: React.FC = () => {
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center">
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-indigo-100 flex items-center justify-center">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
@@ -152,7 +152,7 @@ const AdvancedAnalyticsDashboard: React.FC = () => {
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-6">
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-indigo-100 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<motion.div
@@ -212,11 +212,11 @@ const AdvancedAnalyticsDashboard: React.FC = () => {
<div className="bg-white rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-700">Donations Processed</h3>
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
<span className="text-purple-600 font-bold">💝</span>
<div className="w-10 h-10 bg-primary-100 rounded-lg flex items-center justify-center">
<span className="text-primary-600 font-bold">💝</span>
</div>
</div>
<div className="text-3xl font-bold text-purple-600 mb-2">{formatCurrency(metrics?.totalDonationsProcessed || 0)}</div>
<div className="text-3xl font-bold text-primary-600 mb-2">{formatCurrency(metrics?.totalDonationsProcessed || 0)}</div>
<div className="text-sm text-gray-500">+15% from last {selectedTimeframe}</div>
</div>
@@ -247,7 +247,7 @@ const AdvancedAnalyticsDashboard: React.FC = () => {
initial={{ height: 0 }}
animate={{ height: `${(trend.studentsServed / 400) * 100}%` }}
transition={{ delay: 0.6 + index * 0.1, duration: 0.8 }}
className="bg-gradient-to-t from-blue-500 to-blue-300 rounded-t-lg w-full mb-2 min-h-[20px]"
className="bg-gradient-to-t from-primary-500 to-primary-300 rounded-t-lg w-full mb-2 min-h-[20px]"
/>
<div className="text-sm font-medium text-gray-700">{trend.month}</div>
<div className="text-xs text-gray-500">{trend.studentsServed}</div>
+3 -6
View File
@@ -11,12 +11,9 @@ export function Footer() {
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
<div className="grid gap-8 lg:grid-cols-4">
<div className="lg:col-span-2">
<div className="flex items-center gap-3">
<LogoMark />
<div>
<div className="font-semibold">Miracles in Motion</div>
<div className="text-sm text-neutral-600 dark:text-neutral-400">Essentials for every student</div>
</div>
<div className="footer-brand-block">
<LogoMark variant="footer" />
<p className="footer-tagline">Essentials for every student</p>
</div>
<p className="mt-4 max-w-md text-sm text-neutral-600 dark:text-neutral-400">
A 501(c)(3) nonprofit providing students with school supplies, clothing, and emergency support to help them succeed.
+9 -9
View File
@@ -155,10 +155,10 @@ export default function MimChatAgent({ currentPath }: MimChatAgentProps) {
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 12, scale: 0.96 }}
transition={{ duration: 0.18 }}
className="w-[min(24rem,calc(100vw-2rem))] overflow-hidden rounded-[1.75rem] border border-white/60 bg-white/92 shadow-[0_24px_80px_rgba(219,39,119,0.22)] backdrop-blur-xl dark:border-white/10 dark:bg-neutral-950/92"
className="w-[min(24rem,calc(100vw-2rem))] overflow-hidden rounded-[1.75rem] border border-white/60 bg-white/92 shadow-[0_24px_80px_rgba(26,60,52,0.22)] backdrop-blur-xl dark:border-white/10 dark:bg-neutral-950/92"
aria-label="Miracles in Motion AI chat assistant"
>
<div className="bg-gradient-to-r from-primary-600 via-pink-500 to-fuchsia-500 px-4 py-4 text-white">
<div className="bg-gradient-to-r from-primary-700 via-primary-600 to-secondary-500 px-4 py-4 text-white">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex items-center gap-2 text-sm font-semibold uppercase tracking-[0.2em] text-white/80">
@@ -191,7 +191,7 @@ export default function MimChatAgent({ currentPath }: MimChatAgentProps) {
className={`max-w-[85%] rounded-2xl px-4 py-3 text-sm leading-6 shadow-sm ${
message.role === 'user'
? 'rounded-br-md bg-neutral-900 text-white dark:bg-white dark:text-neutral-900'
: 'rounded-bl-md bg-pink-50 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-100'
: 'rounded-bl-md bg-primary-50 text-neutral-800 dark:bg-neutral-900 dark:text-neutral-100'
}`}
>
{message.content}
@@ -201,7 +201,7 @@ export default function MimChatAgent({ currentPath }: MimChatAgentProps) {
{isSending && (
<div className="flex justify-start">
<div className="rounded-2xl rounded-bl-md bg-pink-50 px-4 py-3 text-sm text-neutral-700 dark:bg-neutral-900 dark:text-neutral-200">
<div className="rounded-2xl rounded-bl-md bg-primary-50 px-4 py-3 text-sm text-neutral-700 dark:bg-neutral-900 dark:text-neutral-200">
Thinking...
</div>
</div>
@@ -214,7 +214,7 @@ export default function MimChatAgent({ currentPath }: MimChatAgentProps) {
key={suggestion}
type="button"
onClick={() => void handleSubmit(undefined, suggestion)}
className="rounded-full border border-pink-200 bg-white px-3 py-2 text-left text-xs font-medium text-neutral-700 transition hover:border-pink-300 hover:bg-pink-50 dark:border-white/10 dark:bg-neutral-950 dark:text-neutral-200 dark:hover:bg-neutral-900"
className="rounded-full border border-primary-200 bg-white px-3 py-2 text-left text-xs font-medium text-neutral-700 transition hover:border-secondary-400 hover:bg-primary-50 dark:border-white/10 dark:bg-neutral-950 dark:text-neutral-200 dark:hover:bg-neutral-900"
>
{suggestion}
</button>
@@ -231,12 +231,12 @@ export default function MimChatAgent({ currentPath }: MimChatAgentProps) {
onChange={event => setInput(event.target.value)}
rows={2}
placeholder="Ask about programs, requests, donations, or volunteering..."
className="min-h-[3.25rem] flex-1 resize-none rounded-2xl border border-neutral-200 bg-white px-4 py-3 text-sm text-neutral-900 outline-none transition focus:border-pink-400 focus:ring-2 focus:ring-pink-200 dark:border-white/10 dark:bg-neutral-950 dark:text-white dark:focus:border-pink-500 dark:focus:ring-pink-500/20"
className="min-h-[3.25rem] flex-1 resize-none rounded-2xl border border-neutral-200 bg-white px-4 py-3 text-sm text-neutral-900 outline-none transition focus:border-primary-500 focus:ring-2 focus:ring-primary-200 dark:border-white/10 dark:bg-neutral-950 dark:text-white dark:focus:border-secondary-500 dark:focus:ring-secondary-500/20"
/>
<button
type="submit"
disabled={!input.trim() || isSending}
className="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-r from-primary-600 to-pink-500 text-white shadow-lg transition hover:scale-[1.02] disabled:cursor-not-allowed disabled:opacity-60"
className="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-r from-primary-600 to-secondary-500 text-white shadow-lg transition hover:scale-[1.02] disabled:cursor-not-allowed disabled:opacity-60"
aria-label="Send message"
>
<Send className="h-4 w-4" />
@@ -250,7 +250,7 @@ export default function MimChatAgent({ currentPath }: MimChatAgentProps) {
<button
type="button"
onClick={resetConversation}
className="text-xs font-medium text-pink-600 transition hover:text-pink-700 dark:text-pink-400 dark:hover:text-pink-300"
className="text-xs font-medium text-primary-600 transition hover:text-primary-700 dark:text-secondary-400 dark:hover:text-secondary-300"
>
Clear
</button>
@@ -276,7 +276,7 @@ export default function MimChatAgent({ currentPath }: MimChatAgentProps) {
aria-expanded={isOpen}
aria-controls="mim-chat-agent-panel"
>
<span className="inline-flex h-11 w-11 items-center justify-center rounded-full bg-gradient-to-r from-primary-600 to-pink-500 text-white shadow-inner">
<span className="inline-flex h-11 w-11 items-center justify-center rounded-full bg-gradient-to-r from-primary-600 to-secondary-500 text-white shadow-inner">
{isOpen ? <X className="h-5 w-5" /> : <MessageCircle className="h-5 w-5" />}
</span>
<span className="hidden sm:flex sm:flex-col sm:items-start sm:leading-tight">
+2 -2
View File
@@ -217,7 +217,7 @@ const MobileVolunteerApp: React.FC = () => {
switch (status) {
case 'pending': return 'text-orange-600 bg-orange-100'
case 'accepted': return 'text-blue-600 bg-blue-100'
case 'in-progress': return 'text-purple-600 bg-purple-100'
case 'in-progress': return 'text-primary-600 bg-primary-100'
case 'completed': return 'text-green-600 bg-green-100'
default: return 'text-gray-600 bg-gray-100'
}
@@ -288,7 +288,7 @@ const MobileVolunteerApp: React.FC = () => {
{assignment.status === 'in-progress' && (
<button
onClick={() => completeAssignment(assignment.id)}
className="flex-1 bg-purple-500 text-white py-2 px-4 rounded-lg text-sm font-medium hover:bg-purple-600 transition-colors"
className="flex-1 bg-primary-500 text-white py-2 px-4 rounded-lg text-sm font-medium hover:bg-primary-600 transition-colors"
>
Mark Complete
</button>
+6 -9
View File
@@ -37,16 +37,13 @@ export function Navigation({ darkMode, setDarkMode, mobileMenuOpen, setMobileMen
<>
<nav className="mx-auto flex w-full max-w-7xl items-center justify-between px-4 py-3 sm:px-6 lg:px-8" role="navigation" aria-label="Main navigation">
<div className="flex items-center gap-3">
<a
href="#/"
className="flex items-center gap-3 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded-lg p-1"
aria-label="Miracles in Motion - Home"
<a
href="#/"
className="nav-brand-link focus:outline-none"
aria-label="Miracles in Motion Foundation — Home"
>
<LogoMark />
<div className="-space-y-1">
<div className="font-semibold tracking-tight">Miracles in Motion</div>
<div className="text-xs text-neutral-600 dark:text-neutral-400">Essentials for every student</div>
</div>
<LogoMark variant="nav" />
<span className="sr-only">Essentials for every student</span>
</a>
</div>
+14 -14
View File
@@ -249,7 +249,7 @@ const StaffTrainingDashboard: React.FC = () => {
const getProficiencyColor = (level: string) => {
switch (level) {
case 'expert': return 'text-purple-600 bg-purple-100'
case 'expert': return 'text-primary-600 bg-primary-100'
case 'proficient': return 'text-blue-600 bg-blue-100'
case 'competent': return 'text-green-600 bg-green-100'
case 'novice': return 'text-orange-600 bg-orange-100'
@@ -279,19 +279,19 @@ const StaffTrainingDashboard: React.FC = () => {
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-purple-50 to-pink-100 flex items-center justify-center">
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-secondary-100 flex items-center justify-center">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
className="w-12 h-12 border-4 border-purple-500 border-t-transparent rounded-full"
className="w-12 h-12 border-4 border-primary-500 border-t-transparent rounded-full"
/>
<span className="ml-4 text-lg text-purple-700">Loading Training System...</span>
<span className="ml-4 text-lg text-primary-700">Loading Training System...</span>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-purple-50 to-pink-100 p-6">
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-secondary-100 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<motion.div
@@ -309,8 +309,8 @@ const StaffTrainingDashboard: React.FC = () => {
onClick={() => setCurrentView(view)}
className={`px-4 py-2 rounded-lg font-medium transition-all ${
currentView === view
? 'bg-purple-500 text-white shadow-lg'
: 'bg-white text-gray-700 hover:bg-purple-50'
? 'bg-primary-500 text-white shadow-lg'
: 'bg-white text-gray-700 hover:bg-primary-50'
}`}
>
{view.charAt(0).toUpperCase() + view.slice(1)}
@@ -331,7 +331,7 @@ const StaffTrainingDashboard: React.FC = () => {
<h3 className="text-lg font-semibold text-gray-700">Total Staff</h3>
<span className="text-2xl">👥</span>
</div>
<div className="text-3xl font-bold text-purple-600 mb-2">{staff.length}</div>
<div className="text-3xl font-bold text-primary-600 mb-2">{staff.length}</div>
<div className="text-sm text-gray-500">Across all departments</div>
</div>
@@ -415,7 +415,7 @@ const StaffTrainingDashboard: React.FC = () => {
</div>
<button
onClick={() => setSelectedModule(module)}
className="px-4 py-2 bg-purple-500 text-white rounded-lg font-medium hover:bg-purple-600 transition-colors"
className="px-4 py-2 bg-primary-500 text-white rounded-lg font-medium hover:bg-primary-600 transition-colors"
>
View Details
</button>
@@ -461,7 +461,7 @@ const StaffTrainingDashboard: React.FC = () => {
initial={{ width: 0 }}
animate={{ width: `${member.trainingProgress}%` }}
transition={{ delay: 0.2, duration: 1 }}
className="bg-purple-500 h-2 rounded-full"
className="bg-primary-500 h-2 rounded-full"
/>
</div>
</div>
@@ -521,7 +521,7 @@ const StaffTrainingDashboard: React.FC = () => {
</p>
</div>
<div className="text-right">
<div className="text-2xl font-bold text-purple-600">
<div className="text-2xl font-bold text-primary-600">
{checklist.completedItems}/{checklist.totalItems}
</div>
<div className="text-sm text-gray-500">Items Complete</div>
@@ -538,7 +538,7 @@ const StaffTrainingDashboard: React.FC = () => {
initial={{ width: 0 }}
animate={{ width: `${(checklist.completedItems / checklist.totalItems) * 100}%` }}
transition={{ delay: 0.3, duration: 1.2 }}
className="bg-gradient-to-r from-purple-500 to-pink-500 h-3 rounded-full"
className="bg-gradient-to-r from-primary-500 to-secondary-500 h-3 rounded-full"
/>
</div>
</div>
@@ -576,7 +576,7 @@ const StaffTrainingDashboard: React.FC = () => {
)}
</div>
<span className={`px-2 py-1 rounded text-xs font-medium ${
item.category === 'certification' ? 'bg-purple-100 text-purple-600' :
item.category === 'certification' ? 'bg-primary-100 text-primary-600' :
item.category === 'training' ? 'bg-blue-100 text-blue-600' :
item.category === 'practice' ? 'bg-green-100 text-green-600' :
item.category === 'mentoring' ? 'bg-orange-100 text-orange-600' :
@@ -659,7 +659,7 @@ const StaffTrainingDashboard: React.FC = () => {
</div>
<div className="flex gap-3">
<button className="flex-1 bg-purple-500 text-white py-3 px-4 rounded-lg font-medium hover:bg-purple-600 transition-colors">
<button className="flex-1 bg-primary-500 text-white py-3 px-4 rounded-lg font-medium hover:bg-primary-600 transition-colors">
Start Training
</button>
<button className="px-4 py-3 border border-gray-300 rounded-lg font-medium text-gray-700 hover:bg-gray-50 transition-colors">
+6 -4
View File
@@ -22,12 +22,14 @@ describe('Navigation Component', () => {
vi.clearAllMocks()
})
it('renders navigation with logo and brand name', () => {
it('renders navigation with logo and accessible brand label', () => {
render(<Navigation {...mockProps} />)
expect(screen.getByTestId('logo-mark')).toBeInTheDocument()
expect(screen.getByText('Miracles in Motion')).toBeInTheDocument()
expect(screen.getByText('Essentials for every student')).toBeInTheDocument()
expect(
screen.getByLabelText('Miracles in Motion Foundation — Home')
).toBeInTheDocument()
expect(screen.getByText('Essentials for every student')).toHaveClass('sr-only')
})
it('renders desktop navigation links', () => {
@@ -140,8 +140,8 @@ function PaymentForm({ amount, isRecurring = false, onSuccess, onError }: Paymen
<p className="text-gray-600 mb-6">
Your donation of {formatCurrency(amount)} will help students in need.
</p>
<div className="bg-gradient-to-r from-purple-50 to-blue-50 rounded-lg p-6">
<div className="flex items-center justify-center gap-2 text-purple-600 mb-2">
<div className="bg-gradient-to-r from-primary-50 to-primary-50 rounded-lg p-6">
<div className="flex items-center justify-center gap-2 text-primary-600 mb-2">
<Sparkles className="w-5 h-5" />
<span className="font-semibold">Impact Preview</span>
</div>
@@ -172,7 +172,7 @@ function PaymentForm({ amount, isRecurring = false, onSuccess, onError }: Paymen
required
value={customerInfo.name}
onChange={(e) => setCustomerInfo(prev => ({ ...prev, name: e.target.value }))}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
placeholder="John Doe"
/>
</div>
@@ -186,7 +186,7 @@ function PaymentForm({ amount, isRecurring = false, onSuccess, onError }: Paymen
required
value={customerInfo.email}
onChange={(e) => setCustomerInfo(prev => ({ ...prev, email: e.target.value }))}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
placeholder="[email protected]"
/>
</div>
@@ -200,7 +200,7 @@ function PaymentForm({ amount, isRecurring = false, onSuccess, onError }: Paymen
type="tel"
value={customerInfo.phone}
onChange={(e) => setCustomerInfo(prev => ({ ...prev, phone: e.target.value }))}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
placeholder="(555) 123-4567"
/>
</div>
@@ -219,12 +219,12 @@ function PaymentForm({ amount, isRecurring = false, onSuccess, onError }: Paymen
</div>
{/* Donation Summary */}
<div className="bg-gradient-to-r from-purple-50 to-blue-50 rounded-lg p-6">
<div className="bg-gradient-to-r from-primary-50 to-primary-50 rounded-lg p-6">
<div className="flex items-center justify-between mb-4">
<span className="text-lg font-semibold text-gray-900">
{isRecurring ? 'Monthly' : 'One-time'} Donation
</span>
<span className="text-2xl font-bold text-purple-600">
<span className="text-2xl font-bold text-primary-600">
{formatCurrency(amount)}
</span>
</div>
@@ -254,7 +254,7 @@ function PaymentForm({ amount, isRecurring = false, onSuccess, onError }: Paymen
<button
type="submit"
disabled={!stripe || isLoading || paymentStatus === 'processing'}
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 text-white py-4 px-6 rounded-lg font-semibold text-lg hover:from-purple-700 hover:to-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2"
className="w-full bg-gradient-to-r from-primary-600 to-primary-600 text-white py-4 px-6 rounded-lg font-semibold text-lg hover:from-primary-700 hover:to-primary-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center justify-center gap-2"
>
{isLoading ? (
<>
+1 -1
View File
@@ -17,7 +17,7 @@ export function HeroSection() {
<section ref={containerRef} className="relative min-h-screen flex items-center justify-center overflow-hidden">
{/* Animated Background */}
<motion.div
className="absolute inset-0 bg-gradient-to-br from-primary-50 via-white to-secondary-50 dark:from-gray-900 dark:to-purple-900"
className="absolute inset-0 bg-gradient-to-br from-primary-50 via-white to-secondary-50 dark:from-gray-900 dark:to-primary-900"
style={{ y }}
/>
+2 -2
View File
@@ -34,7 +34,7 @@ export function ImpactSection() {
label: "Students Helped",
description: "Individual students who received direct support",
trend: "+23% this year",
color: "from-blue-500 to-blue-600"
color: "from-primary-500 to-primary-600"
},
{
icon: Heart,
@@ -67,7 +67,7 @@ export function ImpactSection() {
label: "Success Rate",
description: "Students showing improved academic performance",
trend: "Consistent excellence",
color: "from-purple-500 to-purple-600"
color: "from-primary-500 to-primary-600"
},
{
icon: Award,
+3 -3
View File
@@ -9,7 +9,7 @@ export function ProgramsSection() {
title: "School Supplies",
description: "Essential learning materials including notebooks, pens, calculators, and art supplies",
impact: "2,847 students equipped",
color: "from-blue-500 to-blue-600"
color: "from-primary-500 to-primary-600"
},
{
icon: Shirt,
@@ -30,14 +30,14 @@ export function ProgramsSection() {
title: "Educational Technology",
description: "Laptops, tablets, and internet access for remote learning success",
impact: "645 devices provided",
color: "from-purple-500 to-purple-600"
color: "from-primary-500 to-primary-600"
},
{
icon: Heart,
title: "Mentorship Programs",
description: "One-on-one support and guidance for academic and personal growth",
impact: "432 mentor relationships",
color: "from-pink-500 to-pink-600"
color: "from-secondary-500 to-secondary-600"
},
{
icon: Home,
+7 -7
View File
@@ -83,7 +83,7 @@ export function LanguageSwitcher({
key={code}
onClick={() => handleLanguageChange(code)}
className={`w-full px-3 py-2 text-left hover:bg-gray-50 transition-colors flex items-center gap-2 ${
code === currentLang ? 'bg-purple-50 text-purple-600' : 'text-gray-700'
code === currentLang ? 'bg-primary-50 text-primary-600' : 'text-gray-700'
}`}
>
<span className="text-base">{lang.flag}</span>
@@ -102,7 +102,7 @@ export function LanguageSwitcher({
<div className={`w-full ${className}`}>
<button
onClick={() => setIsOpen(!isOpen)}
className="w-full flex items-center justify-between p-3 bg-white rounded-lg border border-gray-200 hover:border-purple-300 transition-colors"
className="w-full flex items-center justify-between p-3 bg-white rounded-lg border border-gray-200 hover:border-primary-300 transition-colors"
>
<div className="flex items-center gap-3">
<Globe className="w-5 h-5 text-gray-500" />
@@ -134,7 +134,7 @@ export function LanguageSwitcher({
key={code}
onClick={() => handleLanguageChange(code)}
className={`w-full px-4 py-3 text-left hover:bg-gray-50 transition-colors border-b border-gray-100 last:border-b-0 ${
code === currentLang ? 'bg-purple-50 text-purple-600' : 'text-gray-700'
code === currentLang ? 'bg-primary-50 text-primary-600' : 'text-gray-700'
}`}
>
<div className="flex items-center gap-3">
@@ -144,7 +144,7 @@ export function LanguageSwitcher({
<div className="text-sm text-gray-500">{code.toUpperCase()}</div>
</div>
{code === currentLang && (
<div className="ml-auto w-2 h-2 bg-purple-600 rounded-full" />
<div className="ml-auto w-2 h-2 bg-primary-600 rounded-full" />
)}
</div>
</button>
@@ -161,7 +161,7 @@ export function LanguageSwitcher({
<div className={`relative ${className}`}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-3 py-2 bg-white rounded-lg border border-gray-200 hover:border-purple-300 transition-colors shadow-sm"
className="flex items-center gap-2 px-3 py-2 bg-white rounded-lg border border-gray-200 hover:border-primary-300 transition-colors shadow-sm"
aria-label={t('accessibility.toggleLanguage')}
>
<Globe className="w-4 h-4 text-gray-500" />
@@ -202,7 +202,7 @@ export function LanguageSwitcher({
key={code}
onClick={() => handleLanguageChange(code)}
className={`w-full px-4 py-2 text-left hover:bg-gray-50 transition-colors flex items-center gap-3 ${
code === currentLang ? 'bg-purple-50 text-purple-600' : 'text-gray-700'
code === currentLang ? 'bg-primary-50 text-primary-600' : 'text-gray-700'
}`}
>
<span className="text-xl">{lang.flag}</span>
@@ -211,7 +211,7 @@ export function LanguageSwitcher({
<div className="text-xs text-gray-500">{code.toUpperCase()}</div>
</div>
{code === currentLang && (
<div className="w-2 h-2 bg-purple-600 rounded-full" />
<div className="w-2 h-2 bg-primary-600 rounded-full" />
)}
</button>
))}
+26 -18
View File
@@ -1,24 +1,32 @@
// LogoMark component
import { motion } from 'framer-motion'
import { Sparkles } from 'lucide-react'
export type LogoMarkVariant = 'nav' | 'footer' | 'square'
export interface LogoMarkProps {
/** nav/footer: full wordmark; square: compact icon tile */
variant?: LogoMarkVariant
className?: string
}
const LOGO_SRC = '/logo.png'
export function LogoMark({ variant = 'nav', className = '' }: LogoMarkProps) {
const isDecorative = variant === 'nav' || variant === 'footer'
export function LogoMark() {
return (
<div className="relative grid h-10 w-10 place-items-center overflow-hidden rounded-2xl bg-gradient-to-br from-primary-500 via-secondary-500 to-secondary-600 shadow-lg shadow-primary-500/20">
<motion.div
className="absolute inset-0 opacity-60"
animate={{
background: [
"radial-gradient(120px 80px at 20% 20%, rgba(255,255,255,0.4), transparent)",
"radial-gradient(120px 80px at 80% 30%, rgba(255,255,255,0.4), transparent)",
"radial-gradient(120px 80px at 50% 80%, rgba(255,255,255,0.4), transparent)",
]
}}
transition={{ duration: 6, repeat: Infinity, ease: "easeInOut" }}
<span
className={['logo-mark', `logo-mark--${variant}`, className].filter(Boolean).join(' ')}
aria-hidden={isDecorative || undefined}
>
<img
src={LOGO_SRC}
alt={isDecorative ? '' : 'Miracles in Motion Foundation'}
className="logo-mark__img"
width={1024}
height={1024}
decoding="async"
fetchPriority={variant === 'nav' ? 'high' : undefined}
/>
<Sparkles className="relative h-6 w-6 text-white drop-shadow" />
</div>
</span>
)
}
export default LogoMark
export default LogoMark
+1 -1
View File
@@ -184,7 +184,7 @@ export function PerformanceMonitor({
</div>
<div className="flex items-center justify-between text-xs">
<span className="flex items-center gap-1">
<Image className="w-3 h-3 text-purple-500" />
<Image className="w-3 h-3 text-primary-500" />
Images
</span>
<span className="font-mono">{bundleMetrics.imageSize}KB</span>
+271 -161
View File
@@ -1,27 +1,40 @@
/* Self-hosted Inter (LCP-friendly; no external font requests) */
/* Self-hosted fonts (LCP-friendly; no external font requests) */
@import "@fontsource/inter/300.css";
@import "@fontsource/inter/400.css";
@import "@fontsource/inter/500.css";
@import "@fontsource/inter/600.css";
@import "@fontsource/inter/700.css";
@import "@fontsource/playfair-display/600.css";
@import "@fontsource/playfair-display/700.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Official MIM brand palette (logo: forest green + gold) */
:root {
--mim-brand-green: #1a3c34;
--mim-brand-green-deep: #0f2922;
--mim-brand-green-mid: #2d6b5c;
--mim-brand-gold: #c5a059;
--mim-brand-gold-bright: #d4af37;
--mim-focus-ring: var(--mim-brand-gold);
--mim-shadow-brand: rgba(26, 60, 52, 0.28);
--mim-shadow-gold: rgba(197, 160, 89, 0.32);
}
@layer base {
html {
scroll-behavior: smooth;
}
/* Skip to content: visible on focus for keyboard/screen reader (a11y) */
.skip-link {
position: absolute;
top: -100px;
left: 0.5rem;
z-index: 100;
padding: 0.75rem 1rem;
background: #db2777;
background: var(--mim-brand-green);
color: #fff;
font-weight: 500;
border-radius: 0.5rem;
@@ -29,43 +42,166 @@
}
.skip-link:focus {
top: 0.5rem;
outline: 2px solid #ec4899;
outline: 2px solid var(--mim-brand-gold);
outline-offset: 2px;
}
body {
font-family: "Inter", ui-sans-serif, system-ui, sans-serif;
font-feature-settings: 'rlig' 1, 'calt' 1;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(135deg, #ec4899, #8b5cf6);
background: linear-gradient(135deg, var(--mim-brand-green), var(--mim-brand-gold));
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(135deg, #db2777, #7c3aed);
background: linear-gradient(135deg, var(--mim-brand-green-deep), var(--mim-brand-gold-bright));
}
}
@layer components {
/* Button Components */
/* Official logo (public/logo.png) */
.logo-mark {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
line-height: 0;
}
.logo-mark__img {
display: block;
object-fit: contain;
}
/* Mobile-first; larger on tablet/desktop */
.logo-mark--nav {
height: 2.125rem;
}
.logo-mark--nav .logo-mark__img {
height: 100%;
width: auto;
max-width: 8.75rem;
}
@media (min-width: 640px) {
.logo-mark--nav {
height: 2.75rem;
}
.logo-mark--nav .logo-mark__img {
max-width: 10.5rem;
}
}
@media (min-width: 1024px) {
.logo-mark--nav {
height: 3.25rem;
}
.logo-mark--nav .logo-mark__img {
max-width: 12rem;
}
}
.logo-mark--footer {
height: 2.75rem;
}
.logo-mark--footer .logo-mark__img {
height: 100%;
width: auto;
max-width: 10rem;
}
@media (min-width: 640px) {
.logo-mark--footer {
height: 3.25rem;
}
.logo-mark--footer .logo-mark__img {
max-width: 12rem;
}
}
@media (min-width: 1024px) {
.logo-mark--footer {
height: 3.75rem;
}
.logo-mark--footer .logo-mark__img {
max-width: 14rem;
}
}
.logo-mark--square {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.375rem;
overflow: hidden;
box-shadow: 0 1px 3px rgb(15 41 34 / 0.25);
}
.logo-mark--square .logo-mark__img {
width: 100%;
height: 100%;
object-fit: cover;
}
.nav-brand-link {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.25rem;
border-radius: 0.5rem;
transition: opacity 0.2s ease;
}
.nav-brand-link:hover {
opacity: 0.9;
}
.nav-brand-link:focus-visible {
outline: 2px solid var(--mim-brand-gold);
outline-offset: 3px;
}
.footer-brand-block {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.footer-brand-block .footer-tagline {
font-size: 0.875rem;
color: #52525b;
max-width: 20rem;
}
@media (prefers-color-scheme: dark) {
.footer-brand-block .footer-tagline {
color: #d4d4d8;
}
}
.btn-primary {
display: inline-flex;
align-items: center;
gap: 0.5rem;
border-radius: 9999px;
background-image: linear-gradient(to right, #db2777, #7c3aed); /* Replace with your primary-600 and secondary-600 colors */
background-image: linear-gradient(
to right,
var(--mim-brand-green),
var(--mim-brand-green-mid),
var(--mim-brand-gold)
);
padding-left: 1.5rem;
padding-right: 1.5rem;
padding-top: 0.75rem;
@@ -73,64 +209,66 @@
font-size: 0.875rem;
font-weight: 500;
color: #fff;
box-shadow: 0 10px 15px -3px rgba(236, 72, 153, 0.25), 0 4px 6px -4px rgba(236, 72, 153, 0.25);
box-shadow: 0 10px 15px -3px var(--mim-shadow-brand), 0 4px 6px -4px var(--mim-shadow-gold);
transition: transform 0.2s, box-shadow 0.2s;
outline: none;
}
.btn-primary:hover {
transform: scale(1.05);
box-shadow: 0 20px 25px -5px rgba(236, 72, 153, 0.25), 0 8px 10px -6px rgba(236, 72, 153, 0.25);
box-shadow: 0 20px 25px -5px var(--mim-shadow-brand), 0 8px 10px -6px var(--mim-shadow-gold);
}
.btn-primary:focus {
outline: none;
box-shadow: 0 0 0 2px #ec4899, 0 10px 15px -3px rgba(236, 72, 153, 0.25), 0 4px 6px -4px rgba(236, 72, 153, 0.25);
box-shadow: 0 0 0 2px var(--mim-focus-ring), 0 10px 15px -3px var(--mim-shadow-brand);
}
.btn-primary:focus-visible {
outline: 2px solid #ec4899;
outline: 2px solid var(--mim-focus-ring);
outline-offset: 2px;
}
.btn-secondary {
display: inline-flex;
align-items: center;
gap: 0.5rem;
border-radius: 9999px;
border: 1px solid #d4d4d8; /* border-neutral-300 */
background-color: rgba(255,255,255,0.7); /* bg-white/70 */
padding-left: 1.5rem; /* px-6 */
border: 1px solid #d4d4d8;
background-color: rgba(255, 255, 255, 0.7);
padding-left: 1.5rem;
padding-right: 1.5rem;
padding-top: 0.75rem; /* py-3 */
padding-top: 0.75rem;
padding-bottom: 0.75rem;
font-size: 0.875rem; /* text-sm */
font-weight: 500; /* font-medium */
color: #52525b; /* text-neutral-700 */
backdrop-filter: blur(8px); /* backdrop-blur */
transition: background 0.2s, box-shadow 0.2s;
font-size: 0.875rem;
font-weight: 500;
color: #52525b;
backdrop-filter: blur(8px);
transition: background 0.2s, box-shadow 0.2s, border-color 0.2s;
outline: none;
}
.btn-secondary:hover {
background-color: #fff;
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
border-color: var(--mim-brand-gold);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
}
.btn-secondary:focus {
outline: none;
box-shadow: 0 0 0 2px #737373, 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
box-shadow: 0 0 0 2px var(--mim-brand-green), 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.btn-secondary:focus-visible {
outline: 2px solid #737373; /* focus:ring-neutral-500 */
outline: 2px solid var(--mim-brand-green);
outline-offset: 2px;
}
@media (prefers-color-scheme: dark) {
.btn-secondary {
border: 1px solid rgba(255,255,255,0.2); /* dark:border-white/20 */
background-color: rgba(255,255,255,0.1); /* dark:bg-white/10 */
color: #e5e5e5; /* dark:text-neutral-200 */
border: 1px solid rgba(255, 255, 255, 0.2);
background-color: rgba(255, 255, 255, 0.1);
color: #e5e5e5;
}
.btn-secondary:hover {
background-color: rgba(255,255,255,0.2); /* dark:hover:bg-white/20 */
background-color: rgba(255, 255, 255, 0.2);
border-color: var(--mim-brand-gold);
}
}
.btn-white {
display: inline-flex;
align-items: center;
@@ -143,84 +281,72 @@
padding-bottom: 0.75rem;
font-size: 0.875rem;
font-weight: 500;
color: #18181b;
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
color: var(--mim-brand-green-deep);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
outline: none;
}
.btn-white:hover {
transform: scale(1.05);
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1);
}
.btn-white:focus {
outline: none;
box-shadow: 0 0 0 2px #fff, 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
}
.btn-white:focus-visible {
outline: 2px solid #fff;
outline: 2px solid var(--mim-brand-gold);
outline-offset: 2px;
}
.navlink {
font-size: 0.875rem; /* text-sm */
font-weight: 500; /* font-medium */
color: #52525b; /* text-neutral-600 */
font-size: 0.875rem;
font-weight: 500;
color: #52525b;
transition: color 0.2s;
}
.navlink:hover {
color: #ec4899; /* text-primary-600 */
color: var(--mim-brand-green);
}
@media (prefers-color-scheme: dark) {
.navlink {
color: #d4d4d8; /* dark:text-neutral-300 */
color: #d4d4d8;
}
.navlink:hover {
color: #a78bfa; /* dark:hover:text-primary-400 */
color: var(--mim-brand-gold-bright);
}
}
/* Form Components */
.input-field {
width: 100%;
border-radius: 0.75rem;
border: 1px solid #d1d5db; /* gray-300 */
border: 1px solid #d1d5db;
background-color: #fff;
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.75rem;
padding-bottom: 0.75rem;
padding: 0.75rem 1rem;
font-size: 0.875rem;
color: #111827; /* gray-900 */
color: #111827;
transition: border-color 0.2s, box-shadow 0.2s;
}
.input-field::placeholder {
color: #6b7280; /* gray-500 */
color: #6b7280;
opacity: 1;
}
.input-field:focus {
border-color: #ec4899; /* primary-500 */
border-color: var(--mim-brand-green);
outline: none;
box-shadow: 0 0 0 2px #ec4899, 0 0 0 2px rgba(236, 72, 153, 0.2);
box-shadow: 0 0 0 2px var(--mim-brand-green), 0 0 0 4px rgba(26, 60, 52, 0.15);
}
.input-field:focus-visible {
outline: 2px solid #ec4899;
outline: 2px solid var(--mim-brand-green);
outline-offset: 2px;
}
@media (prefers-color-scheme: dark) {
.input-field {
border: 1px solid #4b5563; /* gray-600 */
background-color: #1f2937; /* gray-800 */
color: #f3f4f6; /* gray-100 */
}
.input-field::placeholder {
color: #9ca3af; /* gray-400 */
border: 1px solid #4b5563;
background-color: #1f2937;
color: #f3f4f6;
}
.input-field:focus {
border-color: #a21caf; /* primary-400 */
border-color: var(--mim-brand-gold);
}
}
/* Text Utilities */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
@@ -228,7 +354,7 @@
-webkit-box-orient: vertical;
overflow: hidden;
}
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
@@ -236,140 +362,144 @@
-webkit-box-orient: vertical;
overflow: hidden;
}
.input {
width: 100%;
border-radius: 0.75rem; /* rounded-xl */
border: 1px solid rgba(255,255,255,0.3); /* border-white/30 */
background-color: rgba(255,255,255,0.7); /* bg-white/70 */
padding-left: 0.75rem; /* px-3 */
padding-right: 0.75rem;
padding-top: 0.5rem; /* py-2 */
padding-bottom: 0.5rem;
font-size: 0.875rem; /* text-sm */
backdrop-filter: blur(8px); /* backdrop-blur */
border-radius: 0.75rem;
border: 1px solid rgba(255, 255, 255, 0.3);
background-color: rgba(255, 255, 255, 0.7);
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
backdrop-filter: blur(8px);
transition: border-color 0.2s, box-shadow 0.2s;
outline: none;
}
.input:focus {
border-color: #ec4899; /* primary-500 */
border-color: var(--mim-brand-green);
outline: none;
box-shadow: 0 0 0 2px #ec4899, 0 0 0 2px rgba(236, 72, 153, 0.2);
box-shadow: 0 0 0 2px var(--mim-brand-green), 0 0 0 4px rgba(26, 60, 52, 0.15);
}
.input:focus-visible {
outline: 2px solid #ec4899;
outline: 2px solid var(--mim-brand-green);
outline-offset: 2px;
}
@media (prefers-color-scheme: dark) {
.input {
border: 1px solid rgba(255,255,255,0.1); /* dark:border-white/10 */
background-color: rgba(255,255,255,0.1); /* dark:bg-white/10 */
border: 1px solid rgba(255, 255, 255, 0.1);
background-color: rgba(255, 255, 255, 0.1);
}
.input:focus {
border-color: #a21caf; /* dark:focus:border-primary-400 */
border-color: var(--mim-brand-gold);
}
}
/* Card Components */
.card {
border-radius: 1rem; /* rounded-2xl */
border: 1px solid rgba(255,255,255,0.3); /* border-white/30 */
background-color: rgba(255,255,255,0.7); /* bg-white/70 */
padding: 1.5rem; /* p-6 */
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1); /* shadow-xl */
backdrop-filter: blur(8px); /* backdrop-blur */
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.3);
background-color: rgba(255, 255, 255, 0.7);
padding: 1.5rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(8px);
}
@media (prefers-color-scheme: dark) {
.card {
border: 1px solid rgba(255,255,255,0.1); /* dark:border-white/10 */
background-color: rgba(255,255,255,0.05); /* dark:bg-white/5 */
border: 1px solid rgba(255, 255, 255, 0.1);
background-color: rgba(255, 255, 255, 0.05);
}
}
.card-hover {
transition: transform 0.2s, box-shadow 0.2s;
}
.card-hover:hover {
transform: translateY(-0.25rem); /* -translate-y-1 */
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25), 0 8px 10px -6px rgba(0,0,0,0.1); /* shadow-2xl */
transform: translateY(-0.25rem);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
}
/* Section Header */
.section-header {
margin-left: auto;
margin-right: auto;
max-width: 48rem; /* 3xl = 48rem */
max-width: 48rem;
text-align: center;
}
.section-eyebrow {
font-size: 0.875rem; /* text-sm */
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em; /* tracking-wider */
color: #db2777; /* text-primary-600 */
letter-spacing: 0.12em;
color: var(--mim-brand-gold);
font-weight: 600;
}
@media (prefers-color-scheme: dark) {
.section-eyebrow {
color: #a78bfa; /* dark:text-primary-400 */
color: var(--mim-brand-gold-bright);
}
}
.section-title {
margin-top: 0.5rem; /* mt-2 */
font-size: 1.875rem; /* text-3xl */
font-weight: 700; /* font-bold */
letter-spacing: -0.025em; /* tracking-tight */
margin-top: 0.5rem;
font-family: "Playfair Display", Georgia, "Times New Roman", serif;
font-size: 1.875rem;
font-weight: 700;
letter-spacing: -0.025em;
line-height: 2.25rem;
color: var(--mim-brand-green-deep);
}
@media (min-width: 640px) {
.section-title {
font-size: 2.25rem; /* sm:text-4xl */
font-size: 2.25rem;
line-height: 2.5rem;
}
}
@media (prefers-color-scheme: dark) {
.section-title {
color: #f4f4f5;
}
}
.section-subtitle {
margin-top: 1rem; /* mt-4 */
font-size: 1.125rem; /* text-lg */
color: #52525b; /* text-neutral-600 */
margin-top: 1rem;
font-size: 1.125rem;
color: #52525b;
}
@media (prefers-color-scheme: dark) {
.section-subtitle {
color: #d4d4d8; /* dark:text-neutral-300 */
color: #d4d4d8;
}
}
}
@layer utilities {
/* Gradient Text */
.gradient-text {
background-image: linear-gradient(to right, #ec4899, #a21caf, #7c3aed); /* Replace with your Tailwind color values */
background-image: linear-gradient(
to right,
var(--mim-brand-green),
var(--mim-brand-green-mid),
var(--mim-brand-gold-bright)
);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
/* Glass Effect */
.glass {
background-color: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(12px);
}
.glass-dark {
background-color: rgba(0, 0, 0, 0.1);
backdrop-filter: blur(12px);
}
/* Text Balance */
.text-balance {
text-wrap: balance;
}
/* Animation Utilities */
.animate-in {
animation: animate-in 0.6s ease-out;
}
@keyframes animate-in {
from {
opacity: 0;
@@ -380,29 +510,17 @@
transform: translateY(0);
}
}
/* Focus States for Accessibility */
.focus-visible\:ring-2:focus-visible {
outline: 2px solid transparent;
outline-offset: 2px;
box-shadow: 0 0 0 2px rgba(236, 72, 153, 0.5);
box-shadow: 0 0 0 2px rgba(197, 160, 89, 0.55);
}
}
/* High Contrast Mode Support */
@media (prefers-contrast: high) {
.btn-primary {
border-width: 2px;
border-style: solid;
border-color: currentColor;
}
.btn-secondary {
border-width: 2px;
border-style: solid;
border-color: currentColor;
}
.btn-primary,
.btn-secondary,
.card {
border-width: 2px;
border-style: solid;
@@ -410,7 +528,6 @@
}
}
/* Reduced Motion Support */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
@@ -420,34 +537,27 @@
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
.animate-marquee {
animation: none;
}
.animate-float {
animation: none;
}
.animate-marquee,
.animate-float,
.animate-pulse-slow {
animation: none;
}
}
/* Print Styles */
@media print {
.no-print {
display: none !important;
}
body {
color: black !important;
background: white !important;
}
.gradient-text {
color: black !important;
background: none !important;
-webkit-text-fill-color: initial !important;
}
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ export const MainLayout: React.FC<MainLayoutProps> = ({
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
return (
<div className={`min-h-screen bg-gradient-to-br from-purple-50 via-white to-pink-50 dark:from-gray-900 dark:via-gray-800 dark:to-purple-900 ${className}`}>
<div className={`min-h-screen bg-gradient-to-br from-primary-50 via-white to-secondary-50 dark:from-gray-900 dark:via-gray-800 dark:to-primary-900 ${className}`}>
<a href="#content" className="skip-link">Skip to content</a>
<header className="sticky top-0 z-40 bg-white/80 backdrop-blur dark:bg-black/80">
<Navigation
+3 -3
View File
@@ -38,7 +38,7 @@ export const HomePage: React.FC = () => {
</div>
<h1 className="text-5xl md:text-7xl font-bold text-gray-900 dark:text-white mb-6">
Miracles in <span className="text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-pink-600">Motion</span>
Miracles in <span className="text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-secondary-600">Motion</span>
</h1>
<p className="text-xl md:text-2xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto mb-12 leading-relaxed">
@@ -70,7 +70,7 @@ export const HomePage: React.FC = () => {
{/* Background Elements */}
<div className="absolute inset-0 -z-10">
<div className="absolute top-1/4 left-1/4 w-64 h-64 bg-primary-200/30 rounded-full blur-3xl" />
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-pink-200/30 rounded-full blur-3xl" />
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-secondary-200/30 rounded-full blur-3xl" />
</div>
</section>
@@ -124,7 +124,7 @@ export const HomePage: React.FC = () => {
</section>
{/* Call to Action */}
<section className="py-20 bg-gradient-to-r from-primary-600 to-pink-600">
<section className="py-20 bg-gradient-to-r from-primary-600 to-secondary-600">
<div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8 text-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
+2 -2
View File
@@ -6,12 +6,12 @@
align-items: center;
gap: 0.5rem;
border-radius: 9999px;
background: linear-gradient(to right, #c026d3, #4f46e5);
background: linear-gradient(to right, #1a3c34, #2d6b5c, #c5a059);
padding: 0.75rem 1.5rem;
font-size: 0.875rem;
font-weight: 500;
color: white;
box-shadow: 0 10px 15px -3px rgba(236, 72, 153, 0.25);
box-shadow: 0 10px 15px -3px rgba(26, 60, 52, 0.28);
transition: all 0.3s;
text-decoration: none;
}
+33 -24
View File
@@ -8,33 +8,42 @@ module.exports = {
theme: {
extend: {
colors: {
primary: {
50: '#fdf4ff',
100: '#fae8ff',
200: '#f5d0fe',
300: '#f0abfc',
400: '#e879f9',
500: '#d946ef',
600: '#c026d3',
700: '#a21caf',
800: '#86198f',
900: '#701a75',
brand: {
green: '#1a3c34',
'green-deep': '#0f2922',
gold: '#c5a059',
'gold-bright': '#d4af37',
},
/* Forest green — primary UI */
primary: {
50: '#f0f7f5',
100: '#d9ebe6',
200: '#b3d7cd',
300: '#7db8a8',
400: '#4d9482',
500: '#2d6b5c',
600: '#1a3c34',
700: '#153029',
800: '#0f2922',
900: '#0a1f1a',
},
/* Gold — accents, CTAs, highlights */
secondary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
50: '#faf6ef',
100: '#f3ebda',
200: '#e6d5b0',
300: '#d4af37',
400: '#c5a059',
500: '#b8924a',
600: '#9a7a3d',
700: '#7d6232',
800: '#604b27',
900: '#45361c',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
serif: ['"Playfair Display"', 'Georgia', 'Times New Roman', 'serif'],
},
animation: {
'marquee': 'marquee 30s linear infinite',
@@ -56,8 +65,8 @@ module.exports = {
},
},
boxShadow: {
'glow': '0 0 20px rgba(236, 72, 153, 0.3)',
'glow-lg': '0 0 40px rgba(236, 72, 153, 0.4)',
'glow': '0 0 20px rgba(26, 60, 52, 0.35)',
'glow-lg': '0 0 40px rgba(197, 160, 89, 0.35)',
},
backdropBlur: {
xs: '2px',
@@ -85,4 +94,4 @@ module.exports = {
})
}
],
}
}
+16 -4
View File
@@ -9,13 +9,13 @@ export default defineConfig({
react(),
VitePWA({
registerType: 'prompt',
includeAssets: ['favicon.svg', 'robots.txt', 'site.webmanifest'],
includeAssets: ['favicon.svg', 'logo.png', 'robots.txt', 'site.webmanifest'],
manifest: {
name: 'Miracles in Motion',
name: 'Miracles in Motion Foundation',
short_name: 'MiM',
description: 'A 501(c)(3) nonprofit providing students with essential support',
theme_color: '#7c3aed',
background_color: '#ffffff',
theme_color: '#1a3c34',
background_color: '#1a3c34',
display: 'standalone',
scope: '/',
start_url: '/',
@@ -24,6 +24,18 @@ export default defineConfig({
src: '/favicon.svg',
sizes: 'any',
type: 'image/svg+xml',
purpose: 'any'
},
{
src: '/favicon-192x192.png',
sizes: '192x192',
type: 'image/png',
purpose: 'any maskable'
},
{
src: '/favicon-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any maskable'
}
]