Layout
Structural components for organising content on a page. Cards surface discrete pieces of content in a scannable grid; section transitions create intentional visual breaks between full-width areas; and the footer provides a reusable site-wide shell.
Cards
Entry points that surface content and invite action. Background, border, and border-radius are driven by --card-* Tier-3 tokens — retheme in one place without touching the semantic layer.
Default
Background fill, no visible border. Use for most card contexts.
Outlined
Adds a --color-border edge for same-level surfaces.
Highlight
High-contrast background — adapts per theme.
Anatomy
When anatomy children are present the wrapper's padding is removed automatically via:has() — each section carries its own var(--card-padding)..card-media must be the first child so its top corners align with the card's border-radius.
Fern Valley Trail
A shaded loop through old-growth forest with seasonal wildflower meadows along the ridge.
Full reference — Cards
Variants
| Variant | Class | Use |
|---|---|---|
| Default | .card | Article lists, feature grids, project indexes. Background: --card-bg → --color-bg-panel. |
| Outlined | .card-outlined | Adds var(--color-border) edge when the surrounding surface matches the card's background. |
| Highlight | .card-highlight | Featured or promoted content. Background: Deepwater (--fo3-deepwater) in light, Forest (--fo2-forest) in dark — Birch Mist text in both modes (8.6:1 on deepwater, 6.7:1 on forest). |
| Interactive | .card-interactive | Any card that is a navigation target or action trigger — adds hover, focus, and active states. |
Anatomy
<div class="card">
<div class="card-media"><img src="trail.jpg" alt="…"></div>
<div class="card-header"><h3>Title</h3></div>
<div class="card-body"><p>Description</p></div>
<div class="card-footer">
<button class="btn btn-p btn-sm">Action</button>
</div>
</div>| Class | Purpose | Notes |
|---|---|---|
.card-media | Top image / visual area | Must be first child. overflow:hidden clips to card radius. |
.card-header | Title and metadata row | Use semantic heading elements (h2–h4). |
.card-body | Main content area | flex: 1 — expands to fill in equal-height grids. |
.card-footer | Actions row | margin-top: auto pins to bottom. Divider appears when preceded by .card-body. |
Interactive cards
Add .card-interactive to any card that is a navigation target or action trigger. Works on both <div> (with tabindex="0") and <a>.
<!-- div card -->
<div class="card card-interactive" tabindex="0"> … </div>
<!-- anchor card (whole card is the link) -->
<a href="/trail/fern-valley" class="card card-interactive"> … </a>Card grid
.card-grid creates a responsive auto-fit grid. Minimum column width: 280px. Cards collapse to a single column on narrow viewports.
<div class="card-grid">
<div class="card card-interactive"> … </div>
<div class="card card-interactive"> … </div>
<div class="card card-interactive"> … </div>
</div>Token reference
| Token | Default | Purpose |
|---|---|---|
--card-bg | var(--color-bg-panel) | Default card background |
--card-hover-bg | var(--color-bg-inset) | Background on hover (interactive cards) |
--card-border | transparent | Border colour — .card-outlined overrides to --color-border |
--card-radius | var(--radius-lg) | Border radius for card and media |
--card-padding | var(--space-md) | Padding for bare cards and all anatomy sections |
--card-highlight-bg | --fo3-deepwater (light) · --fo2-forest (dark) | Background for .card-highlight |
--card-highlight-border | transparent | Border for .card-highlight |
--card-highlight-text | var(--color-card-highlight-text) | Primary text on highlight card — adapts per theme |
--card-highlight-text-secondary | var(--color-card-highlight-text-secondary) | Secondary text on highlight card |
--card-highlight-text-tertiary | var(--color-card-highlight-text-tertiary) | Tertiary text on highlight card |
Overriding tokens
/* Tighter padding for a dense list layout */
.article-list {
--card-padding: var(--space-sm);
}
/* Pill cards in a marketing section */
.feature-grid {
--card-radius: var(--radius-xl);
}CSS reference
Shipped in dist/farn-components.css. Load alongside farn.css or farn-tokens.css.
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--space-md);
}
.card {
display: flex;
flex-direction: column;
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: var(--card-radius);
padding: var(--card-padding);
}
.card:has(> .card-header, > .card-body, > .card-footer, > .card-media) {
padding: 0;
}
.card-outlined { border-color: var(--color-border); }
.card-highlight { background: var(--card-highlight-bg); border-color: var(--card-highlight-border); }
.card-interactive {
cursor: pointer;
transition: background var(--duration-fast) var(--ease-out);
}
.card-interactive:hover { background: var(--card-hover-bg); }
.card-interactive:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 3px; }
.card-interactive:active { transform: scale(0.99); }
a.card-interactive { color: inherit; text-decoration: none; }Preview card pattern
A card with a fixed-height live-demo strip at the top — useful whenever a card should preview the content or context it links to, giving the viewer a visual taste before they navigate. Composes the existing card anatomy: .card-media.card-preview for the strip,.card-body for the label and description.
Full reference — Preview card
Markup
<a class="card card-interactive card-outlined" href="/section">
<div class="card-media card-preview" aria-hidden="true">
<!-- live mini-demo content -->
</div>
<div class="card-body">
<p>Section title</p>
<p>One-sentence description.</p>
</div>
</a>.card-mediamust be the first child for correct corner clipping..card-previewsetsheight: 9rem, flex-centres content, and clips overflow.a.card-interactivesuppresses default link colour and underline — child text renders in--color-text.- Add
aria-hidden="true"to the preview strip — the link label comes from the card body, not the demo content. - Add
tabindex="-1"to any interactive elements inside the preview strip so they are not keyboard-reachable.
Token reference
| Token | Default | Purpose |
|---|---|---|
--card-preview-height | 9rem | Height of the preview strip. Override on a parent element to resize without touching the class. |
All other --card-* tokens apply — background and border are inherited from the card context.
CSS reference
.card-preview {
height: 9rem;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
padding: var(--space-sm);
}
a.card-interactive {
color: inherit;
text-decoration: none;
}Section Transitions
Intentional visual breaks between full-width page sections — signalling a shift in content, depth, or energy. All patterns consume the semantic token layer so transitions stay coherent as the palette evolves. Use one primary pattern per page.
Layered overlap
The only pure-CSS pattern — no SVG. The next section or a card bleeds upward into the preceding one using a negative margin-top and rounded top corners.Never add overflow:hidden to the preceding section — it clips the overlap entirely.
Preceding section
Dark area — never add overflow:hidden here.
Overlapping section
Rounded top corners float over the section above.
Sine wave
An SVG divider using layered cubic bezier curves — two offset paths create the illusion of a receding tide. Sits absolutely at the bottom of the parent section; fill matches the next section's background.
Preceding section
Wave at the bottom — fill matches the section below.
Next section
Override --color-section-next to match this background.
Convex arc
A single quadratic bezier spanning the full width. Calmer than the sine wave — one smooth dome suited to centred hero layouts. Animates via animation-timeline: view()in supporting browsers; static fallback otherwise. The preview below shows the static state.
Preceding section
Arc at the bottom — fill matches the section below.
Next section
Override --color-section-next on the parent to match this background.
Organic blob
A freeform SVG boundary — no mathematical regularity. Two offset paths soften the edge. Fade-only animation keeps the irregular shape from looking chaotic in motion. Use at most once per page for a high-emphasis brand moment. Draw paths in Figma or Inkscape — do not hand-author blob paths.
Preceding section
Blob at the bottom — replace the example paths with exported Figma paths for production.
Next section
Diagonal cut
A skewed ::before pseudo-element paints the section background at an angle — content stays horizontal and box-shadow still works. In supporting browsers the angle flattens via animation-timeline: view().
Preceding section
Background skewed via ::before — content stays horizontal.
Next section
Set --color-section on the element to match the preceding section's background.
Full reference — Section Transitions
Pattern guide
| Pattern | Classes | Character | Best for |
|---|---|---|---|
| Layered overlap | .overlap-preceding / .overlap-section / .overlap-card | Dimensional, modern | Dark hero → light content; featured CTA panels |
| Sine wave | .section-wave | Organic, rhythmic | Repeated section breaks; any natural flowing transition |
| Convex arc | .section-arc | Refined, quiet | Centred hero layouts; calm lens-like elegance |
| Organic blob | .section-blob | Expressive, freeform | High-emphasis brand moments; one-off expressive breaks |
| Diagonal cut | .section-skew | Sharp, directional | CTA sections; forward-momentum transitions |
Use one primary type per page. A second type is acceptable for a single high-emphasis moment (e.g., sine waves throughout with one layered overlap at a CTA). More than two distinct types creates visual noise.
Layered overlap — details
<section class="overlap-preceding">
<!-- Hero — never overflow:hidden -->
</section>
<section class="overlap-section" style="background: var(--color-bg-panel);">
<!-- Slides up over the hero -->
</section>| Class / attribute | Element | Purpose |
|---|---|---|
.overlap-preceding | Any block | Sets overflow: visible explicitly. Prevents accidental overflow:hidden clipping the overlap. |
.overlap-section | <section> | Full section that slides up. Sets z-index, negative margin, rounded top corners. Consumer sets background. |
.overlap-card | <div> | Card variant — floats upward with Tier-1 entry animation (opacity + translateY + shadow grow). |
data-scroll-reveal | .overlap-card | Opts into the shared IntersectionObserver. Adds .is-visible on first entry. |
| Token | Default | Purpose |
|---|---|---|
--overlap-section-radius | var(--radius-xl) | Top corners on .overlap-section |
--overlap-section-offset | 60px | How far .overlap-section bleeds upward |
--overlap-card-radius | var(--card-radius) | Border radius on .overlap-card |
--overlap-card-offset | 80px | How far .overlap-card bleeds upward |
--overlap-card-duration | 0.7s | Entry animation duration |
Sine wave — details
<section style="position: relative; padding-bottom: var(--wave-height);
--color-section-next: var(--color-bg-panel);">
<div class="section-wave" aria-hidden="true">
<svg viewBox="0 0 1200 80" preserveAspectRatio="none">
<!-- depth layer first (SVG paints in document order) -->
<path d="M0,40 C300,80 900,0 1200,40 L1200,80 L0,80 Z"
fill="var(--color-section-next)" opacity="0.4"/>
<path d="M0,45 C300,85 900,5 1200,45 L1200,80 L0,80 Z"
fill="var(--color-section-next)"/>
</svg>
</div>
</section>| Token | Default | Purpose |
|---|---|---|
--wave-height | 80px | SVG rendered height and required parent padding-bottom |
--color-section-next | var(--color-bg-panel) | Fill colour — set on the parent to match the destination background |
Change amplitude by adjusting control-point Y values. Equal and opposite offsets (C300,80 900,0) produce a symmetric wave. To reverse direction, place .section-wave at the top of the next section with transform: scaleY(-1).
Convex arc — details
<!-- Convex (dome) -->
<section style="position: relative; padding-bottom: var(--arc-height);
--color-section-next: var(--color-bg-panel);">
<div class="section-arc" aria-hidden="true">
<svg viewBox="0 0 1200 80" preserveAspectRatio="none">
<path d="M0,80 Q600,0 1200,80 Z" fill="var(--color-section-next)"/>
</svg>
</div>
</section>
<!-- Concave (bowl) — redrawn path, always static -->
<div class="section-arc section-arc--concave" aria-hidden="true">
<svg viewBox="0 0 1200 80" preserveAspectRatio="none">
<path d="M0,0 Q600,80 1200,0 L1200,80 L0,80 Z" fill="var(--color-section-next)"/>
</svg>
</div>| Token | Default | Purpose |
|---|---|---|
--arc-height | 80px | SVG rendered height and required parent padding-bottom |
--color-section-next | var(--color-bg-panel) | Fill colour — set on the parent to match the destination background |
Organic blob — details
<section style="position: relative; padding-bottom: var(--blob-height);
--color-section-next: var(--color-bg-panel);">
<div class="section-blob" aria-hidden="true">
<svg viewBox="0 0 1200 100" preserveAspectRatio="none">
<!-- depth layer first (SVG paints in document order) -->
<path d="M0,60 C150,25 200,80 400,55 C600,28 700,75 900,50 C1050,28 1150,65 1200,45 L1200,100 L0,100 Z"
fill="var(--color-section-next)" opacity="0.45"/>
<path d="M0,65 C150,30 200,85 400,60 C600,33 700,80 900,55 C1050,33 1150,70 1200,50 L1200,100 L0,100 Z"
fill="var(--color-section-next)"/>
</svg>
</div>
</section>| Token | Default | Purpose |
|---|---|---|
--blob-height | 100px | SVG rendered height and required parent padding-bottom |
--color-section-next | var(--color-bg-panel) | Fill colour — set on the parent to match the destination background |
Diagonal cut — details
Three implementation approaches in increasing precision. Only approach 2 ships as a CSS class; approaches 1 and 3 are code patterns to adapt.Note: clip-path (approach 1) clips box-shadowon the same element — use the ::before approach if the section needs a shadow.
Approach 1 — clip-path polygon
Simplest option. GPU-accelerated. Does not work if the section needs box-shadow.
<section style="clip-path: polygon(0 0, 100% 0, 100% 92%, 0 100%);">
<!-- Section content -->
</section>Approach 2 — ::before skew (shipped as .section-skew)
The section element itself must have no background — only the ::beforepseudo-element provides the background. In supporting browsers the angle flattens viaanimation-timeline: view(). Static fallback shows var(--diagonal-angle).
<section class="section-skew"
style="padding: 80px 0; --color-section: var(--color-bg-panel);">
<!-- Content sits horizontally above the skewed ::before background -->
</section>Approach 3 — SVG triangle
Precise pixel control. Place the SVG at the bottom of the preceding section.
<section style="position: relative; padding-bottom: 60px;">
<svg viewBox="0 0 1200 60" preserveAspectRatio="none"
style="display:block; position:absolute; bottom:0; left:0; width:100%; height:60px;"
aria-hidden="true">
<polygon points="0,60 1200,0 1200,60" fill="var(--color-section-next)"/>
</svg>
</section>| Token | Default | Purpose |
|---|---|---|
--diagonal-angle | -3deg | Static display angle (approach 2). Keep between -2deg and -5deg. Scroll-driven animation always starts at -5deg regardless. |
--color-section | var(--color-bg) | Background fill for the ::before (approach 2). Set to match the section's intended background. |
--color-section-next | var(--color-bg-panel) | SVG fill for the triangle approach. Set to match the destination section background. |
Direction carries meaning: negative skewY (bottom-left to top-right) reads as forward momentum. Be consistent within a page.
Separator
Content-level horizontal rules — distinct from section-transition patterns. Three purpose-named variants cover the full range from tight list spacing to editorial accent moments.
Hairline — between list items
Section — between content groups
Decorative — editorial scene break
Full reference — Separator
Variants
| Class | Description |
|---|---|
.hairline | 1px subtle rule — tight spacing; between list items or dense content |
.section-divider | 2px standard rule — medium spacing; between content groups |
.decorative | 2px accent-color rule, 32px wide — editorial scene break |
Anatomy
<!-- Tight rule between list items -->
<hr class="hairline">
<!-- Standard rule between content groups -->
<hr class="section-divider">
<!-- Short accent rule for editorial breaks -->
<hr class="decorative">
<!-- Purely decorative — hide from assistive tech -->
<hr class="decorative" aria-hidden="true">Token reference
Separator uses semantic tokens directly — no Tier-3 override layer.
| Token | Default | Purpose |
|---|---|---|
--separator-section-weight | 2px | Section divider border thickness |
--color-border-subtle | color-mix() at 12% / 25% | Hairline color |
--color-border | Light: --in3-ash · Dark: --in2-slate | Section divider color |
--color-accent | Light: --fo1-fern · Dark: --fo0-glade | Decorative rule color |
--space-sm | 0.75rem | Hairline vertical margin |
--space-lg | 2rem | Section divider vertical margin |
--space-md | 1.5rem | Decorative rule vertical margin |
CSS reference
Shipped in dist/farn-components.css. Load alongside farn.css or farn-tokens.css.
hr { border: none; }
.hairline { border-top: 1px solid var(--color-border-subtle); margin: var(--space-sm) 0; }
.section-divider { border-top: var(--separator-section-weight) solid var(--color-border); margin: var(--space-lg) 0; }
.decorative { border-top: 2px solid var(--color-accent); width: 32px; margin: var(--space-md) 0; }Quotes
Three patterns for surfacing cited text, featured statements, and testimonials. All variants consume --quote-* Tier-3 tokens — retheme in one place.
Blockquote
The platform is never finished. The moment you ship it, the next set of needs is already forming at the edges.
Pullquote
Good platform thinking starts with the developer, not the platform.
Testimonial card
Working this way changed how I think about internal tooling entirely. It stopped being infrastructure and started being product.
Full reference — Quotes
Variants
| Variant | Element | Class | Use |
|---|---|---|---|
| Blockquote | <blockquote> | .quote | Inline prose citation with left accent border. Body text uses Fraunces italic. |
| Pullquote | <figure> | .quote-pull | Centred featured statement. Accent rule generated via ::before. Caption via <figcaption>. |
| Testimonial | <figure> | .quote-testimonial | Testimonial card with decorative quote mark, quote text, and an attribution row (avatar + name/role). |
Anatomy
<!-- Blockquote -->
<blockquote class="quote">
<p>Quote body text here.</p>
<footer>— Author Name, <cite>Source Title</cite></footer>
</blockquote>
<!-- Pullquote -->
<figure class="quote-pull">
<p>Featured statement goes here.</p>
<figcaption>Attribution name</figcaption>
</figure>
<!-- Testimonial card — .card provides the shell (bg, border, radius, flex-column) -->
<figure class="card quote-testimonial">
<p class="quote-testimonial-text">Quote text here.</p>
<figcaption class="quote-testimonial-attribution">
<div class="quote-testimonial-avatar" aria-hidden="true">AB</div>
<div class="quote-testimonial-meta">
<span class="quote-testimonial-name">Full Name</span>
<span class="quote-testimonial-role">Title, Company</span>
</div>
</figcaption>
</figure>Token reference
| Token | Default | Purpose |
|---|---|---|
--quote-border-width | 3px | Blockquote left accent bar width |
--quote-border-color | --color-accent | Blockquote left accent bar color |
--quote-text-color | --color-text-secondary | Body text color |
--quote-footer-color | --color-text-tertiary | Footer / cite text color |
--quote-padding | --space-sm 0 --space-sm --space-lg | Blockquote inset |
--quote-margin-bottom | --space-lg | Spacing below all quote variants |
--quote-opsz | 16 | Fraunces optical size for blockquote body text |
--quote-pull-rule-width | 32px | Pullquote decorative rule width |
--quote-pull-rule-height | 2px | Pullquote decorative rule height |
--quote-pull-rule-color | --color-accent | Pullquote rule color |
--quote-testimonial-border | --color-border-subtle | Border color override (card default is transparent) |
--quote-testimonial-padding | --space-lg | Padding override (card default is --space-md) |
--quote-testimonial-gap | --space-sm | Gap between quote mark, text, and attribution row |
--quote-testimonial-mark-color | --color-accent | Decorative opening quote mark color |
--quote-testimonial-mark-size | 3rem | Decorative opening quote mark font size |
--quote-testimonial-avatar-size | 2.5rem | Avatar circle diameter |
--quote-testimonial-avatar-bg | color-mix(fern 15%) | Avatar circle background tint |
--quote-testimonial-attribution-gap | --space-sm | Gap between avatar and name/role block |
--quote-testimonial-attribution-top | --space-xs | Extra top margin on attribution row (adds to parent gap) |
CSS reference
Shipped in dist/farn-components.css. Load alongside farn.css or farn-tokens.css.
.quote {
border-left: var(--quote-border-width) solid var(--quote-border-color);
padding: var(--quote-padding);
margin-bottom: var(--quote-margin-bottom);
font-family: var(--font-display);
font-style: italic;
font-variation-settings: 'opsz' var(--quote-opsz);
color: var(--quote-text-color);
}
.quote-pull { text-align: center; padding: 0 var(--space-xl); }
.quote-pull::before { /* accent rule — width/height/color via tokens */ }
.quote-pull p { font-family: var(--font-display); font-variation-settings: 'opsz' 20; }
.quote-testimonial { /* compose with .card — adds border-color, padding, gap overrides */ border-color: var(--quote-testimonial-border); padding: var(--quote-testimonial-padding); gap: var(--quote-testimonial-gap); }
.quote-testimonial::before { content: '\201C'; font-family: var(--font-display); font-variation-settings: 'opsz' 72; font-size: 3rem; line-height: 0.8; color: var(--quote-testimonial-mark-color); }
.quote-testimonial-text { font-family: var(--font-display); font-variation-settings: 'opsz' 20; font-style: italic; font-size: 1rem; }
.quote-testimonial-attribution { display: flex; align-items: center; gap: var(--quote-testimonial-attribution-gap); }
.quote-testimonial-avatar { width: var(--quote-testimonial-avatar-size); border-radius: var(--radius-full); }
.quote-testimonial-name { font-weight: 600; color: var(--color-text); }
.quote-testimonial-role { font-size: 0.75rem; color: var(--color-text-secondary); }Footer
A site-wide footer shell — logo, tagline, navigation slot, copyright bar, and secondary links — styled by --footer-* Tier-3 tokens. Works on any background; pair with data-theme="dark" for the standard inverted treatment.
.footer component is rendered at the bottom of this page. Scroll down to see the logo, tagline, navigation links, and footer bar in action.Full reference — Footer
Anatomy
<footer class="footer" data-theme="dark">
<div class="footer-inner">
<div class="footer-top">
<div class="footer-logo">
<svg ...></svg> <!-- your logo icon -->
Site Name<span>.</span>
</div>
<p class="footer-tagline">Short tagline.</p>
<nav class="footer-nav">
<a href="/docs">Docs</a>
<a href="/components">Components</a>
</nav>
</div>
<hr class="footer-hr">
<div class="footer-bar">
<p class="footer-copy">© 2026 Your Name</p>
<div class="footer-links">
<a href="/about">About</a>
<a href="/contact">Contact</a>
</div>
</div>
</div>
</footer>| Class | Element | Purpose |
|---|---|---|
.footer | <footer> | Outer shell — top/bottom padding via --footer-padding-top/bottom |
.footer-inner | <div> | Max-width container with horizontal padding |
.footer-top | <div> | Logo + tagline + nav row |
.footer-logo | <div> | Flex row for logo icon and wordmark |
.footer-logo-icon | <svg> | 56px tall logo icon — set on the <svg> or <img> |
.footer-tagline | <p> | Short one-liner below logo — --color-text-secondary, max-width 48ch |
.footer-nav | <nav> or <div> | Horizontal link row — --color-text-tertiary with hover/focus states |
.footer-hr | <hr> | Subtle horizontal rule — --color-border-subtle |
.footer-bar | <div> | Copyright + secondary links row — space-between layout |
.footer-copy | <p> | 12px copyright line — --color-text-tertiary |
.footer-links | <div> | Flex row for legal/secondary links — 12px, --color-text-tertiary |
Theme
Add data-theme="dark" to get the standard dark-footer treatment — all semantic tokens flip automatically without any extra CSS.
<footer class="footer" data-theme="dark"> … </footer>The footer slot accepts any content via <slot /> (Astro) or direct children — use .footer-nav with plain <a> elements for navigation links.
Token reference
| Token | Default | Purpose |
|---|---|---|
--footer-padding-top | var(--space-3xl) | Space above footer content |
--footer-padding-bottom | var(--space-xl) | Space below footer content |
CSS reference
Shipped in dist/farn-components.css. Load alongside farn.css or farn-tokens.css.
.footer {
padding: var(--footer-padding-top) 0 var(--footer-padding-bottom);
}
.footer-inner {
max-width: var(--width-content); margin: 0 auto; padding: 0 var(--space-lg);
}
.footer-top { margin-bottom: var(--space-xl); }
.footer-logo {
display: flex; align-items: center;
font-size: 1.75rem; font-weight: 700; text-decoration: none; color: var(--color-text);
}
.footer-logo span { color: var(--color-accent); }
.footer-logo-icon { height: 56px; width: auto; margin-right: 0.45em; }
.footer-tagline { font-size: 14px; color: var(--color-text-secondary); max-width: 48ch; }
.footer-nav { display: flex; gap: var(--space-md); flex-wrap: wrap; }
.footer-nav a { font-size: 14px; color: var(--color-text-tertiary); text-decoration: none; }
.footer-nav a:hover { color: var(--color-text-secondary); }
.footer-nav a:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 3px; }
.footer-hr { border: none; border-top: 1px solid var(--color-border-subtle); margin: var(--space-xl) 0; }
.footer-bar {
display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--space-sm);
}
.footer-copy { font-size: 12px; color: var(--color-text-tertiary); }
.footer-links { display: flex; gap: var(--space-md); flex-wrap: wrap; }
.footer-links a { font-size: 12px; color: var(--color-text-tertiary); text-decoration: none; }
.footer-links a:hover { color: var(--color-text-secondary); }
.footer-links a:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 3px; }