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

VariantClassUse
Default.cardArticle lists, feature grids, project indexes. Background: --card-bg--color-bg-panel.
Outlined.card-outlinedAdds var(--color-border) edge when the surrounding surface matches the card's background.
Highlight.card-highlightFeatured or promoted content. Light: --in0-void. Dark: --in1-iron + --in2-slate border.
Interactive.card-interactiveAny 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

3.2 km · Moderate

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>
ClassPurposeNotes
.card-mediaTop image / visual areaMust be first child. overflow:hidden clips to card radius.
.card-headerTitle and metadata rowUse semantic heading elements (h2h4).
.card-bodyMain content areaflex: 1 — expands to fill in equal-height grids.
.card-footerActions rowmargin-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

TokenDefaultPurpose
--card-bgvar(--color-bg-panel)Default card background
--card-hover-bgvar(--color-bg-inset)Background on hover (interactive cards)
--card-bordertransparentBorder colour — .card-outlined overrides to --color-border
--card-radiusvar(--radius-lg)Border radius for card and media
--card-paddingvar(--space-md)Padding for bare cards and all anatomy sections
--card-highlight-bgLight: --in0-void · Dark: --in1-ironBackground for .card-highlight
--card-highlight-borderLight: transparent · Dark: --in2-slateBorder 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

PatternClassesCharacterBest for
Layered overlap.overlap-preceding / .overlap-section / .overlap-cardDimensional, modernDark hero → light content; featured CTA panels
Sine wave.section-waveOrganic, rhythmicRepeated section breaks; any natural flowing transition
Convex arc.section-arcRefined, quietCentred 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 / attributeElementPurpose
.overlap-precedingAny blockSets 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-cardOpts 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

TokenDefaultPurpose
--overlap-section-radiusvar(--radius-xl)Top corners on .overlap-section
--overlap-section-offset60pxHow far .overlap-section bleeds upward
--overlap-card-radiusvar(--card-radius)Border radius on .overlap-card
--overlap-card-offset80pxHow far .overlap-card bleeds upward
--overlap-card-duration0.7sEntry 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>
TokenDefaultPurpose
--wave-height80pxSVG rendered height and required parent padding-bottom
--color-section-nextvar(--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>
TokenDefaultPurpose
--arc-height80pxSVG rendered height and required parent padding-bottom
--color-section-nextvar(--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.