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.
Cards stable
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.
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. Light: --in0-void. Dark: --in1-iron + --in2-slate border. |
| Interactive | .card-interactive | Any card that is a navigation target or action trigger — adds hover, focus, and active states. |
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.
<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 | Light: --in0-void · Dark: --in1-iron | Background for .card-highlight |
--card-highlight-border | Light: transparent · Dark: --in2-slate | Border for .card-highlight |
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); } Section Transitions beta
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.
Preceding section
Wave at the bottom — fill matches the section below.
Next section
Override --color-section-next to match this background.
Full reference — Section Transitions
Patterns
| 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 |
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
The only pure-CSS pattern — no SVG. The next section or a card bleeds upward into the
preceding section using a negative margin-top combined with
border-radius on the top corners.
Preceding section
Dark area — never add overflow:hidden here.
Overlapping section
Rounded top corners float over the section above.
<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. |
Critical: overflow:hidden on .overlap-preceding or any ancestor will clip the overlapping element entirely.
Layered overlap tokens
| 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
An SVG divider using cubic bezier curves. Two layered paths create the illusion of a receding tide. The wave sits absolutely positioned at the bottom of its parent section and fills in the next section's background colour.
<section style="position: relative; padding-bottom: var(--wave-height);
--color-section-next: var(--color-bg-panel);">
<div class="section-wave" data-scroll-reveal 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 and apply transform: scaleY(-1) to it (not rotateX).
Convex arc
A single quadratic bezier curve spanning the full width. Calmer than the sine wave —
one smooth dome suited to centred hero layouts. The convex variant animates with
animation-timeline: view() (static fallback for unsupported browsers and
prefers-reduced-motion). The concave variant is always static.
<!-- 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 |
Upcoming patterns
Three additional patterns are planned:
- Organic blob — high-emphasis brand moments; paths drawn in Figma/Inkscape (T-25)
- Diagonal cut — energetic directional transition for CTAs (T-27)
- Stacked card reveal — cinematic depth for long 3–5 section pages (T-28)
Separator coming soon
Content-level horizontal rules — distinct from section-transition patterns. Tokenised
<hr> and .separator variants for use within prose and
between content blocks.