feat(web): marketing Button, Nav (with mobile menu) and Footer from Figma, test-first
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014SdXCbrN5PtGExkpvMaMHx
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { Button } from '../../src/marketing/Button.tsx';
|
||||
|
||||
describe('Button', () => {
|
||||
it('renders a link with its label and destination', () => {
|
||||
render(<Button href="/signup">Get started</Button>);
|
||||
expect(screen.getByRole('link', { name: 'Get started' })).toHaveAttribute('href', '/signup');
|
||||
});
|
||||
|
||||
it('is the primary (inverse) pill by default', () => {
|
||||
render(<Button href="/signup">Get started</Button>);
|
||||
expect(screen.getByRole('link', { name: 'Get started' })).toHaveClass('btn', 'btn-primary', 'btn-md');
|
||||
});
|
||||
|
||||
it('supports the ghost variant and the large size', () => {
|
||||
render(<Button href="/product" variant="ghost" size="lg">See how it works</Button>);
|
||||
expect(screen.getByRole('link', { name: 'See how it works' })).toHaveClass('btn-ghost', 'btn-lg');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { Footer } from '../../src/marketing/Footer.tsx';
|
||||
|
||||
describe('Footer', () => {
|
||||
it('shows the brand and tagline', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.getByText('Your website, handled.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('has the four link columns from the design', () => {
|
||||
render(<Footer />);
|
||||
for (const heading of ['Product', 'Company', 'Resources', 'Legal']) {
|
||||
expect(screen.getByRole('heading', { name: heading })).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it('links only to pages that exist', () => {
|
||||
render(<Footer />);
|
||||
const nav = screen.getByRole('navigation', { name: 'Footer' });
|
||||
const links = within(nav).getAllByRole('link').map((a) => [a.textContent, a.getAttribute('href')]);
|
||||
expect(links).toEqual([
|
||||
['Overview', '/product'],
|
||||
['How it works', '/how-it-works'],
|
||||
['Trust and safety', '/trust'],
|
||||
['Pricing', '/pricing'],
|
||||
['Docs', '/docs'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('shows pages not built yet as plain text, not dead links (D-HL-17)', () => {
|
||||
render(<Footer />);
|
||||
for (const label of ['Changelog', 'About', 'Blog', 'Contact', 'Terms', 'Privacy']) {
|
||||
expect(screen.getByText(label).closest('a')).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('shows the copyright line', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.getByText('© 2026 Wursor')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { Nav } from '../../src/marketing/Nav.tsx';
|
||||
|
||||
describe('Nav', () => {
|
||||
it('links the brand to the home page', () => {
|
||||
render(<Nav />);
|
||||
expect(screen.getByRole('link', { name: 'Wursor' })).toHaveAttribute('href', '/');
|
||||
});
|
||||
|
||||
it('lists the five page links', () => {
|
||||
render(<Nav />);
|
||||
const primary = screen.getByRole('navigation', { name: 'Primary' });
|
||||
const hrefs = within(primary).getAllByRole('link').map((a) => [a.textContent, a.getAttribute('href')]);
|
||||
expect(hrefs).toEqual([
|
||||
['Product', '/product'],
|
||||
['How it works', '/how-it-works'],
|
||||
['Trust', '/trust'],
|
||||
['Pricing', '/pricing'],
|
||||
['Docs', '/docs'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('sends Sign in to sign-in and Get started to sign-up (PX-13)', () => {
|
||||
render(<Nav />);
|
||||
expect(screen.getAllByRole('link', { name: 'Sign in' })[0]).toHaveAttribute('href', '/signin');
|
||||
expect(screen.getAllByRole('link', { name: 'Get started' })[0]).toHaveAttribute('href', '/signup');
|
||||
});
|
||||
|
||||
it('keeps the mobile menu closed until the menu button is pressed', async () => {
|
||||
render(<Nav />);
|
||||
const button = screen.getByRole('button', { name: 'Open menu' });
|
||||
expect(button).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(screen.queryByRole('navigation', { name: 'Mobile' })).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(button);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Close menu' })).toHaveAttribute('aria-expanded', 'true');
|
||||
const menu = screen.getByRole('navigation', { name: 'Mobile' });
|
||||
expect(within(menu).getAllByRole('link').map((a) => a.textContent)).toEqual([
|
||||
'Product', 'How it works', 'Trust and safety', 'Pricing', 'Docs', 'Get started',
|
||||
]);
|
||||
});
|
||||
|
||||
it('closes the mobile menu with Escape', async () => {
|
||||
render(<Nav />);
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Open menu' }));
|
||||
await userEvent.keyboard('{Escape}');
|
||||
expect(screen.queryByRole('navigation', { name: 'Mobile' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes the mobile menu after a link is chosen', async () => {
|
||||
render(<Nav />);
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Open menu' }));
|
||||
const menu = screen.getByRole('navigation', { name: 'Mobile' });
|
||||
await userEvent.click(within(menu).getByRole('link', { name: 'Pricing' }));
|
||||
expect(screen.queryByRole('navigation', { name: 'Mobile' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,12 @@
|
||||
import { Footer } from '../../src/marketing/Footer.tsx';
|
||||
import { Nav } from '../../src/marketing/Nav.tsx';
|
||||
|
||||
export default function HomePage() {
|
||||
return <main>Wursor</main>;
|
||||
return (
|
||||
<>
|
||||
<Nav />
|
||||
<main />
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
Static assets exported from the Figma file "Wursor - weown.net" (marketing site page).
|
||||
Do not redraw or edit them. Re-export from Figma if the design changes.
|
||||
|
||||
| File | Figma node |
|
||||
|---|---|
|
||||
| brand-mark-light.svg | Brand/Mark (28:30), light |
|
||||
| brand-mark-dark.svg | Brand/Mark in Nav Bar, dark (136:72493) |
|
||||
| icons/menu.svg | Icon in Menu · Mobile (143:77164) |
|
||||
| icons/chevron-right.svg | Icon/chevron-right in Hero Announcement (64:1451) |
|
||||
|
||||
Single-colour icons are drawn with CSS masks so they take the token colour for the current theme.
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Brand/Mark">
|
||||
<path id="Vector" d="M4.5 15.5C4.5 11.358 7.858 8 12 8C16.142 8 19.5 11.358 19.5 15.5" stroke="#F2F1EC" stroke-width="3.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle id="ember" cx="12" cy="15.5" r="2" fill="#FF7A33"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 431 B |
@@ -0,0 +1,6 @@
|
||||
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Brand/Mark">
|
||||
<path id="Vector" d="M4.5 15.5C4.5 11.358 7.858 8 12 8C16.142 8 19.5 11.358 19.5 15.5" stroke="#14120B" stroke-width="3.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle id="ember" cx="12" cy="15.5" r="2" fill="#E8590C"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 431 B |
@@ -0,0 +1,5 @@
|
||||
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon/chevron-right">
|
||||
<path id="Vector" d="M7.5 15L12.5 10L7.5 5" stroke="#6B675E" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 335 B |
@@ -0,0 +1,5 @@
|
||||
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon">
|
||||
<path id="Vector" d="M5.33333 4H14M5.33333 8H14M5.33333 12H14M2 4H2.00667M2 8H2.00667M2 12H2.00667" stroke="#14120B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 377 B |
@@ -0,0 +1,11 @@
|
||||
/** Wursor mark: an arch with a dot at its centre. The arch follows the theme; the dot is accent. */
|
||||
export function BrandMark({ size = 24 }: { size?: number }) {
|
||||
return (
|
||||
<>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img className="brand-mark brand-mark-light" src="/marketing/brand-mark-light.svg" alt="" width={size} height={size} />
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img className="brand-mark brand-mark-dark" src="/marketing/brand-mark-dark.svg" alt="" width={size} height={size} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import Link from 'next/link';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
type Props = {
|
||||
href: string;
|
||||
children: ReactNode;
|
||||
variant?: 'primary' | 'ghost';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/** Figma "Button": primary is the one main action on a screen; secondary and danger are added when a screen needs them. Heights 28, 36, 44. */
|
||||
export function Button({ href, children, variant = 'primary', size = 'md', className }: Props) {
|
||||
const classes = ['btn', `btn-${variant}`, `btn-${size}`, className].filter(Boolean).join(' ');
|
||||
return (
|
||||
<Link href={href} className={classes}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import Link from 'next/link';
|
||||
import { FOOTER_COLUMNS, TAGLINE } from './content.ts';
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="footer">
|
||||
<div className="footer-top">
|
||||
<div className="footer-brand">
|
||||
<p className="t-title-m">Wursor</p>
|
||||
<p className="t-body-m footer-muted">{TAGLINE}</p>
|
||||
</div>
|
||||
<nav aria-label="Footer" className="footer-columns">
|
||||
{FOOTER_COLUMNS.map((col) => (
|
||||
<div key={col.heading} className="footer-column">
|
||||
<h2 className="t-label-m">{col.heading}</h2>
|
||||
<ul>
|
||||
{col.items.map((item) => (
|
||||
<li key={item.label} className="t-body-m footer-muted">
|
||||
{item.href ? <Link href={item.href}>{item.label}</Link> : item.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
<div className="footer-bottom t-label-s">
|
||||
<span className="footer-status">
|
||||
<span className="dot" aria-hidden="true" /> All systems normal
|
||||
</span>
|
||||
<span className="footer-spacer" />
|
||||
<span className="footer-muted">English</span>
|
||||
<span className="footer-faint">© 2026 Wursor</span>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
type Props = { name: 'menu' | 'chevron-right'; size?: number; className?: string };
|
||||
|
||||
/**
|
||||
* Draws a single-colour Figma icon with a CSS mask so it takes the text colour
|
||||
* of the current theme. The SVG file is the exact export from Figma.
|
||||
*/
|
||||
export function Icon({ name, size = 16, className }: Props) {
|
||||
const style = {
|
||||
'--icon-src': `url(/marketing/icons/${name}.svg)`,
|
||||
width: size,
|
||||
height: size,
|
||||
} as CSSProperties;
|
||||
return <span aria-hidden="true" className={['icon', className].filter(Boolean).join(' ')} style={style} />;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BrandMark } from './BrandMark.tsx';
|
||||
import { Button } from './Button.tsx';
|
||||
import { NAV_LINKS, ROUTES } from './content.ts';
|
||||
import { Icon } from './Icon.tsx';
|
||||
|
||||
export function Nav() {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setOpen(false);
|
||||
};
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => document.removeEventListener('keydown', onKey);
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<header className="nav">
|
||||
<div className="nav-bar">
|
||||
<Link href="/" className="nav-brand" onClick={() => setOpen(false)}>
|
||||
<BrandMark />
|
||||
<span className="t-title-s">Wursor</span>
|
||||
</Link>
|
||||
|
||||
<nav aria-label="Primary" className="nav-links t-body-m">
|
||||
{NAV_LINKS.map((l) => (
|
||||
<Link key={l.href} href={l.href}>
|
||||
{l.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="nav-actions">
|
||||
<Link href={ROUTES.signIn} className="nav-signin t-label-m">
|
||||
Sign in
|
||||
</Link>
|
||||
<Button href={ROUTES.signUp} size="md" className="nav-cta">
|
||||
Get started
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
className="nav-menu-button"
|
||||
aria-label={open ? 'Close menu' : 'Open menu'}
|
||||
aria-expanded={open}
|
||||
aria-controls="mobile-menu"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
>
|
||||
<Icon name="menu" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{open && (
|
||||
<nav id="mobile-menu" aria-label="Mobile" className="nav-menu">
|
||||
{NAV_LINKS.map((l) => (
|
||||
<Link key={l.href} href={l.href} className="t-title-m" onClick={() => setOpen(false)}>
|
||||
{l.mobileLabel ?? l.label}
|
||||
</Link>
|
||||
))}
|
||||
<Button href={ROUTES.signUp} size="lg" className="nav-menu-cta" >
|
||||
Get started
|
||||
</Button>
|
||||
</nav>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
export type NavLink = { label: string; mobileLabel?: string; href: string };
|
||||
|
||||
export const NAV_LINKS: readonly NavLink[] = [
|
||||
{ label: 'Product', href: '/product' },
|
||||
{ label: 'How it works', href: '/how-it-works' },
|
||||
{ label: 'Trust', mobileLabel: 'Trust and safety', href: '/trust' },
|
||||
{ label: 'Pricing', href: '/pricing' },
|
||||
{ label: 'Docs', href: '/docs' },
|
||||
];
|
||||
|
||||
export const ROUTES = {
|
||||
signUp: '/signup',
|
||||
signIn: '/signin',
|
||||
} as const;
|
||||
|
||||
/** A footer item with no `href` is a page that is not built yet (PRD D-HL-17). */
|
||||
export type FooterItem = { label: string; href?: string };
|
||||
export type FooterColumn = { heading: string; items: readonly FooterItem[] };
|
||||
|
||||
export const FOOTER_COLUMNS: readonly FooterColumn[] = [
|
||||
{
|
||||
heading: 'Product',
|
||||
items: [
|
||||
{ label: 'Overview', href: '/product' },
|
||||
{ label: 'How it works', href: '/how-it-works' },
|
||||
{ label: 'Trust and safety', href: '/trust' },
|
||||
{ label: 'Pricing', href: '/pricing' },
|
||||
{ label: 'Changelog' },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Company',
|
||||
items: [{ label: 'About' }, { label: 'Customers' }, { label: 'Careers' }, { label: 'Contact' }],
|
||||
},
|
||||
{
|
||||
heading: 'Resources',
|
||||
items: [{ label: 'Docs', href: '/docs' }, { label: 'Blog' }, { label: 'Examples' }, { label: 'Status' }],
|
||||
},
|
||||
{
|
||||
heading: 'Legal',
|
||||
items: [{ label: 'Terms' }, { label: 'Privacy' }, { label: 'Security' }, { label: 'Cookies' }],
|
||||
},
|
||||
];
|
||||
|
||||
export const TAGLINE = 'Your website, handled.';
|
||||
@@ -10,3 +10,108 @@ body {
|
||||
a { color: inherit; text-decoration: none; }
|
||||
button { font: inherit; color: inherit; }
|
||||
:focus-visible { outline: 2px solid var(--color-accent-default); outline-offset: 2px; }
|
||||
|
||||
/* ---------- Icons and brand mark ---------- */
|
||||
.icon {
|
||||
display: inline-block;
|
||||
flex: none;
|
||||
background-color: currentColor;
|
||||
-webkit-mask: var(--icon-src) center / contain no-repeat;
|
||||
mask: var(--icon-src) center / contain no-repeat;
|
||||
}
|
||||
.brand-mark { display: block; flex: none; }
|
||||
.brand-mark-dark { display: none; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) .brand-mark-light { display: none; }
|
||||
:root:not([data-theme="light"]) .brand-mark-dark { display: block; }
|
||||
}
|
||||
:root[data-theme="dark"] .brand-mark-light { display: none; }
|
||||
:root[data-theme="dark"] .brand-mark-dark { display: block; }
|
||||
|
||||
/* ---------- Button ---------- */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
border-radius: var(--radius-full);
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
font: 500 13px/16px var(--font-sans);
|
||||
}
|
||||
.btn-sm { height: var(--size-control-sm); padding: 0 var(--space-3); }
|
||||
.btn-md { height: var(--size-control-md); padding: 0 var(--space-4); }
|
||||
.btn-lg { height: var(--size-control-lg); padding: 0 var(--space-5); font: 500 14px/20px var(--font-sans); }
|
||||
.btn-primary { background: var(--color-bg-inverse); color: var(--color-text-inverse); }
|
||||
.btn-ghost { background: transparent; color: var(--color-text-primary); }
|
||||
|
||||
/* ---------- Nav ---------- */
|
||||
.nav { position: relative; background: var(--color-bg-canvas); }
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 72px;
|
||||
padding: 0 var(--space-10);
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
.nav-brand { display: flex; align-items: center; gap: var(--space-2); flex: 1 0 0; }
|
||||
.nav-links { display: flex; gap: var(--space-8); color: var(--color-text-secondary); }
|
||||
.nav-actions { display: flex; align-items: center; justify-content: flex-end; gap: var(--space-4); flex: 1 0 0; }
|
||||
.nav-menu-button {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--size-control-md);
|
||||
height: var(--size-control-md);
|
||||
border: 0;
|
||||
border-radius: var(--radius-full);
|
||||
background: transparent;
|
||||
color: var(--icon-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
.nav-menu { display: none; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.nav-bar { padding: 0 var(--space-5); }
|
||||
.nav-links, .nav-signin { display: none; }
|
||||
.nav-actions { flex: none; gap: 0; }
|
||||
.nav-cta { height: var(--size-control-sm); padding: 0 var(--space-3); }
|
||||
.nav-menu-button { display: inline-flex; }
|
||||
.nav-menu {
|
||||
position: fixed;
|
||||
inset: 72px 0 0 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-4) var(--space-6) var(--space-6);
|
||||
background: var(--color-bg-canvas);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.nav-menu > a:not(.btn) { padding: 14px 0; }
|
||||
.nav-menu .nav-menu-cta { margin-top: auto; width: 100%; }
|
||||
}
|
||||
|
||||
/* ---------- Footer ---------- */
|
||||
.footer { display: flex; flex-direction: column; gap: 56px; padding: 0 120px 48px; }
|
||||
.footer-top { display: flex; align-items: flex-start; gap: 48px; }
|
||||
.footer-brand { display: flex; flex-direction: column; gap: var(--space-2); width: 280px; flex: none; }
|
||||
.footer-columns { display: flex; flex: 1 0 0; gap: 48px; }
|
||||
.footer-column { display: flex; flex-direction: column; gap: var(--space-3); flex: 1 0 0; min-width: 0; }
|
||||
.footer-column h2 { margin: 0; }
|
||||
.footer-column ul { display: flex; flex-direction: column; gap: var(--space-3); margin: 0; padding: 0; list-style: none; }
|
||||
.footer-muted { color: var(--color-text-secondary); }
|
||||
.footer-faint { color: var(--color-text-tertiary); }
|
||||
.footer-bottom { display: flex; align-items: center; gap: var(--space-6); }
|
||||
.footer-status { display: inline-flex; align-items: center; gap: var(--space-2); color: var(--color-text-secondary); }
|
||||
.footer-status .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--color-status-success-dot); }
|
||||
.footer-spacer { flex: 1 0 0; }
|
||||
.footer p { margin: 0; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.footer { padding: 0 var(--space-5) var(--space-8); gap: var(--space-8); }
|
||||
.footer-top { flex-direction: column; gap: var(--space-8); }
|
||||
.footer-columns { flex-wrap: wrap; gap: var(--space-8) var(--space-6); width: 100%; }
|
||||
.footer-column { flex: 1 1 40%; }
|
||||
.footer-bottom { flex-wrap: wrap; gap: var(--space-3) var(--space-6); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user