Institutional A-grade nav logo: SVG lockups, WCAG audit, and deploy hygiene.
Add transparent nav SVG assets with raster fallbacks, header contrast and focus fixes, automated a11y audit scripts, and evidence reports for institutional review. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
+1
-1
@@ -4545,7 +4545,7 @@ function AppContent() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<header className="sticky top-0 z-50 backdrop-blur supports-[backdrop-filter]:bg-white/50 dark:supports-[backdrop-filter]:bg-black/40 border-b border-white/30 dark:border-white/10">
|
||||
<header className="sticky top-0 z-50 border-b border-secondary-200/40 bg-[var(--mim-warm-white)]/95 backdrop-blur dark:border-white/10 dark:bg-black/85">
|
||||
<Navigation
|
||||
darkMode={darkMode}
|
||||
setDarkMode={setDarkMode}
|
||||
|
||||
@@ -25,10 +25,9 @@ export function Footer() {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-neutral-700 transition hover:text-primary-600 dark:text-neutral-300 dark:hover:text-secondary-400"
|
||||
aria-label="Follow Miracles in Motion Foundation on Instagram"
|
||||
>
|
||||
<Instagram className="h-5 w-5" aria-hidden />
|
||||
@miraclesinmotionfoundation
|
||||
Follow @miraclesinmotionfoundation on Instagram
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('Footer Component', () => {
|
||||
it('renders service area and Instagram', () => {
|
||||
render(<Footer />)
|
||||
expect(screen.getByText(/Los Angeles County/)).toBeInTheDocument()
|
||||
expect(screen.getByLabelText(/Instagram/i)).toHaveAttribute(
|
||||
expect(screen.getByRole('link', { name: /Follow @miraclesinmotionfoundation on Instagram/i })).toHaveAttribute(
|
||||
'href',
|
||||
'https://www.instagram.com/miraclesinmotionfoundation'
|
||||
)
|
||||
|
||||
@@ -1,18 +1,59 @@
|
||||
export type LogoMarkVariant = 'nav' | 'footer' | 'square'
|
||||
|
||||
export interface LogoMarkProps {
|
||||
/** nav: symbol on mobile, horizontal on md+; footer: horizontal; square: stacked lockup */
|
||||
/** nav: symbol on mobile, horizontal on md+; footer: symbol on xs, horizontal on sm+ */
|
||||
variant?: LogoMarkVariant
|
||||
className?: string
|
||||
}
|
||||
|
||||
const BRAND = {
|
||||
symbolSvg: '/brand/logo-symbol.svg',
|
||||
symbolPng: '/brand/logo-symbol-512.png',
|
||||
symbolNavSvg: '/brand/logo-symbol-nav.svg',
|
||||
symbolNav: '/brand/logo-symbol-nav.png',
|
||||
horizontalNavSvg: '/brand/logo-horizontal-nav.svg',
|
||||
horizontalNav: '/brand/logo-horizontal-nav.png',
|
||||
horizontalNavWebp: '/brand/logo-horizontal-nav.webp',
|
||||
horizontal: '/brand/logo-horizontal.png',
|
||||
square: '/brand/logo-square.png',
|
||||
} as const
|
||||
|
||||
function ResponsiveLockup({
|
||||
symbolClassName,
|
||||
horizontalClassName,
|
||||
}: {
|
||||
symbolClassName: string
|
||||
horizontalClassName: string
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<picture className={symbolClassName}>
|
||||
<source srcSet={BRAND.symbolNavSvg} type="image/svg+xml" />
|
||||
<img
|
||||
src={BRAND.symbolNav}
|
||||
alt=""
|
||||
className="logo-mark__img logo-mark__img--symbol"
|
||||
width={256}
|
||||
height={256}
|
||||
decoding="async"
|
||||
fetchPriority="high"
|
||||
/>
|
||||
</picture>
|
||||
<picture className={horizontalClassName}>
|
||||
<source srcSet={BRAND.horizontalNavSvg} type="image/svg+xml" />
|
||||
<source srcSet={BRAND.horizontalNavWebp} type="image/webp" />
|
||||
<img
|
||||
src={BRAND.horizontalNav}
|
||||
alt=""
|
||||
className="logo-mark__img logo-mark__img--horizontal"
|
||||
width={1024}
|
||||
height={354}
|
||||
decoding="async"
|
||||
fetchPriority="high"
|
||||
/>
|
||||
</picture>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function LogoMark({ variant = 'nav', className = '' }: LogoMarkProps) {
|
||||
const isDecorative = variant === 'nav' || variant === 'footer'
|
||||
|
||||
@@ -22,23 +63,9 @@ export function LogoMark({ variant = 'nav', className = '' }: LogoMarkProps) {
|
||||
className={['logo-mark', 'logo-mark--nav', className].filter(Boolean).join(' ')}
|
||||
aria-hidden={true}
|
||||
>
|
||||
<img
|
||||
src={BRAND.symbolSvg}
|
||||
alt=""
|
||||
className="logo-mark__img logo-mark__img--symbol block lg:hidden dark:block dark:lg:block"
|
||||
width={100}
|
||||
height={99}
|
||||
decoding="async"
|
||||
fetchPriority="high"
|
||||
/>
|
||||
<img
|
||||
src={BRAND.horizontal}
|
||||
alt=""
|
||||
className="logo-mark__img logo-mark__img--horizontal hidden lg:block dark:hidden"
|
||||
width={1024}
|
||||
height={354}
|
||||
decoding="async"
|
||||
fetchPriority="high"
|
||||
<ResponsiveLockup
|
||||
symbolClassName="logo-mark__img-wrap logo-mark__img-wrap--symbol block md:hidden"
|
||||
horizontalClassName="logo-mark__img-wrap logo-mark__img-wrap--horizontal hidden md:block"
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
@@ -50,21 +77,9 @@ export function LogoMark({ variant = 'nav', className = '' }: LogoMarkProps) {
|
||||
className={['logo-mark', 'logo-mark--footer', className].filter(Boolean).join(' ')}
|
||||
aria-hidden={true}
|
||||
>
|
||||
<img
|
||||
src={BRAND.symbolSvg}
|
||||
alt=""
|
||||
className="logo-mark__img logo-mark__img--symbol hidden dark:block"
|
||||
width={100}
|
||||
height={99}
|
||||
decoding="async"
|
||||
/>
|
||||
<img
|
||||
src={BRAND.horizontal}
|
||||
alt=""
|
||||
className="logo-mark__img logo-mark__img--horizontal block dark:hidden"
|
||||
width={1024}
|
||||
height={354}
|
||||
decoding="async"
|
||||
<ResponsiveLockup
|
||||
symbolClassName="logo-mark__img-wrap logo-mark__img-wrap--symbol block sm:hidden"
|
||||
horizontalClassName="logo-mark__img-wrap logo-mark__img-wrap--horizontal hidden sm:block"
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { render } from '@testing-library/react'
|
||||
import { LogoMark } from '../LogoMark'
|
||||
|
||||
describe('LogoMark', () => {
|
||||
it('renders nav symbol and transparent horizontal lockup sources with SVG preference', () => {
|
||||
const { container } = render(<LogoMark variant="nav" />)
|
||||
|
||||
const symbolSvg = container.querySelector('picture source[type="image/svg+xml"]')
|
||||
const symbol = container.querySelector('picture img.logo-mark__img--symbol')
|
||||
const horizontal = container.querySelectorAll('picture')[1]?.querySelector('img.logo-mark__img--horizontal')
|
||||
const horizontalSvg = container.querySelectorAll('picture')[1]?.querySelector('source[type="image/svg+xml"]')
|
||||
const webp = container.querySelectorAll('picture')[1]?.querySelector('source[type="image/webp"]')
|
||||
|
||||
expect(symbolSvg).toHaveAttribute('srcset', '/brand/logo-symbol-nav.svg')
|
||||
expect(symbol).toHaveAttribute('src', '/brand/logo-symbol-nav.png')
|
||||
expect(horizontalSvg).toHaveAttribute('srcset', '/brand/logo-horizontal-nav.svg')
|
||||
expect(horizontal).toHaveAttribute('src', '/brand/logo-horizontal-nav.png')
|
||||
expect(webp).toHaveAttribute('srcset', '/brand/logo-horizontal-nav.webp')
|
||||
})
|
||||
|
||||
it('marks nav and footer logos as decorative for screen readers', () => {
|
||||
const { container: nav } = render(<LogoMark variant="nav" />)
|
||||
const { container: footer } = render(<LogoMark variant="footer" />)
|
||||
|
||||
expect(nav.querySelector('.logo-mark--nav')).toHaveAttribute('aria-hidden', 'true')
|
||||
expect(footer.querySelector('.logo-mark--footer')).toHaveAttribute('aria-hidden', 'true')
|
||||
})
|
||||
|
||||
it('uses the same transparent nav assets in footer', () => {
|
||||
const { container } = render(<LogoMark variant="footer" />)
|
||||
|
||||
expect(container.querySelector('picture img.logo-mark__img--symbol')).toHaveAttribute(
|
||||
'src',
|
||||
'/brand/logo-symbol-nav.png'
|
||||
)
|
||||
expect(container.querySelectorAll('picture')[1]?.querySelector('img.logo-mark__img--horizontal')).toHaveAttribute(
|
||||
'src',
|
||||
'/brand/logo-horizontal-nav.png'
|
||||
)
|
||||
})
|
||||
|
||||
it('renders square lockup with accessible alt text', () => {
|
||||
const { container } = render(<LogoMark variant="square" />)
|
||||
|
||||
const img = container.querySelector('img')
|
||||
expect(img).toHaveAttribute('src', '/brand/logo-square.png')
|
||||
expect(img).toHaveAttribute('alt', 'Miracles in Motion Foundation')
|
||||
})
|
||||
|
||||
it('applies responsive visibility classes for nav breakpoints', () => {
|
||||
const { container } = render(<LogoMark variant="nav" />)
|
||||
|
||||
expect(container.querySelector('picture')?.className).toMatch(/block md:hidden/)
|
||||
expect(container.querySelectorAll('picture')[1]?.className).toMatch(/hidden md:block/)
|
||||
})
|
||||
})
|
||||
+54
-12
@@ -21,11 +21,16 @@
|
||||
--mim-cream: #faf8f5;
|
||||
--mim-warm-beige: #f5f0e8;
|
||||
--mim-warm-white: #fffefb;
|
||||
--mim-focus-ring: var(--mim-brand-gold);
|
||||
--mim-focus-ring: var(--mim-brand-green-deep);
|
||||
--mim-eyebrow-text: #7d6232;
|
||||
--mim-shadow-brand: rgba(26, 60, 52, 0.28);
|
||||
--mim-shadow-gold: rgba(197, 160, 89, 0.32);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--mim-focus-ring: var(--mim-brand-gold-bright);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
@@ -103,7 +108,7 @@
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Brand logos: /brand/logo-symbol.svg, logo-horizontal.png, logo-square.png */
|
||||
/* Brand logos: nav lockups use transparent /brand/logo-*-nav.*; print/social use opaque PNGs */
|
||||
.logo-mark {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -112,30 +117,51 @@
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.logo-mark__img {
|
||||
.logo-mark__img,
|
||||
.logo-mark__img-wrap {
|
||||
display: block;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.logo-mark__img {
|
||||
object-fit: contain;
|
||||
object-position: left center;
|
||||
}
|
||||
|
||||
/* Nav: symbol (square) on mobile, horizontal lockup on md+ */
|
||||
.logo-mark--nav {
|
||||
height: 3rem;
|
||||
.logo-mark__img-wrap--horizontal .logo-mark__img--horizontal {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: auto;
|
||||
filter: drop-shadow(0 1px 1px rgb(15 41 34 / 0.12));
|
||||
}
|
||||
|
||||
/* Nav: symbol on mobile, transparent horizontal lockup on md+ */
|
||||
.logo-mark--nav {
|
||||
height: 3.25rem;
|
||||
}
|
||||
|
||||
.logo-mark--nav .logo-mark__img-wrap--symbol,
|
||||
.logo-mark--nav .logo-mark__img--symbol {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
max-width: 3.25rem;
|
||||
max-width: 3.5rem;
|
||||
filter: drop-shadow(0 1px 1px rgb(15 41 34 / 0.1));
|
||||
}
|
||||
|
||||
.logo-mark--nav .logo-mark__img-wrap--symbol .logo-mark__img--symbol {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.logo-mark--nav {
|
||||
height: 4.25rem;
|
||||
}
|
||||
.logo-mark--nav .logo-mark__img--horizontal {
|
||||
.logo-mark--nav .logo-mark__img-wrap--horizontal {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
max-width: min(20rem, 48vw);
|
||||
}
|
||||
.logo-mark--nav .logo-mark__img--horizontal {
|
||||
max-width: min(20rem, 48vw);
|
||||
}
|
||||
}
|
||||
@@ -144,6 +170,7 @@
|
||||
.logo-mark--nav {
|
||||
height: 4.5rem;
|
||||
}
|
||||
.logo-mark--nav .logo-mark__img-wrap--horizontal,
|
||||
.logo-mark--nav .logo-mark__img--horizontal {
|
||||
max-width: 21rem;
|
||||
}
|
||||
@@ -153,20 +180,33 @@
|
||||
.logo-mark--nav {
|
||||
height: 4.75rem;
|
||||
}
|
||||
.logo-mark--nav .logo-mark__img-wrap--horizontal,
|
||||
.logo-mark--nav .logo-mark__img--horizontal {
|
||||
max-width: 23rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Footer: horizontal on light; symbol on dark (horizontal PNG has opaque panel) */
|
||||
/* Footer: symbol on xs, transparent horizontal on sm+ (same assets as nav) */
|
||||
.logo-mark--footer {
|
||||
height: 2.75rem;
|
||||
}
|
||||
|
||||
.logo-mark--footer .logo-mark__img-wrap--symbol,
|
||||
.logo-mark--footer .logo-mark__img--symbol {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
max-width: 3.25rem;
|
||||
filter: drop-shadow(0 1px 1px rgb(15 41 34 / 0.1));
|
||||
}
|
||||
|
||||
.logo-mark--footer .logo-mark__img-wrap--symbol .logo-mark__img--symbol {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.logo-mark--footer .logo-mark__img-wrap--horizontal {
|
||||
height: 100%;
|
||||
max-width: min(12rem, 80vw);
|
||||
}
|
||||
|
||||
.logo-mark--footer .logo-mark__img--horizontal {
|
||||
@@ -179,6 +219,7 @@
|
||||
.logo-mark--footer {
|
||||
height: 3.25rem;
|
||||
}
|
||||
.logo-mark--footer .logo-mark__img-wrap--horizontal,
|
||||
.logo-mark--footer .logo-mark__img--horizontal {
|
||||
max-width: 14rem;
|
||||
}
|
||||
@@ -188,6 +229,7 @@
|
||||
.logo-mark--footer {
|
||||
height: 3.5rem;
|
||||
}
|
||||
.logo-mark--footer .logo-mark__img-wrap--horizontal,
|
||||
.logo-mark--footer .logo-mark__img--horizontal {
|
||||
max-width: 16rem;
|
||||
}
|
||||
@@ -240,7 +282,7 @@
|
||||
}
|
||||
|
||||
.nav-brand-link:focus-visible {
|
||||
outline: 2px solid var(--mim-brand-gold);
|
||||
outline: 2px solid var(--mim-focus-ring);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
@@ -481,7 +523,7 @@
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--mim-brand-gold);
|
||||
color: var(--mim-eyebrow-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.dark .section-eyebrow {
|
||||
|
||||
@@ -33,7 +33,7 @@ export const MainLayout: React.FC<MainLayoutProps> = ({
|
||||
return (
|
||||
<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">
|
||||
<header className="sticky top-0 z-40 border-b border-secondary-200/40 bg-[var(--mim-warm-white)]/95 backdrop-blur dark:border-white/10 dark:bg-black/85">
|
||||
<Navigation
|
||||
darkMode={darkMode}
|
||||
setDarkMode={setDarkMode}
|
||||
|
||||
@@ -40,8 +40,8 @@ export function BrandAssetsPage() {
|
||||
Keep clear space around the lockup (at least the height of the capital M in MIRACLES).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Website header:</strong> horizontal lockup on tablet/desktop; symbol mark on
|
||||
mobile.
|
||||
<strong>Website header:</strong> transparent nav lockups (`logo-horizontal-nav.svg` preferred, plus WebP/PNG;
|
||||
`logo-symbol-nav.svg` or PNG on mobile) on light and dark UI shells. Opaque horizontal PNG for print/partner kits only.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Favicon / app icon:</strong> symbol on forest green (<code>#1a3c34</code>).
|
||||
|
||||
Reference in New Issue
Block a user