SAFFCA design system · about this site · site home

SAFFCA Design System

Applied reference. Where the design system's pieces — palette, grid, type, motion, components — live as roles, relationships, and refusals, not just values.

Orientation

What this is. A single navigable reference for SAFFCA's visual system as practised. It explains roles ("when do I use accent vs. muted?"), relationships ("how does this grid recompose?"), and refusals ("why no Bootstrap?"). The user-facing question this answers: what holds the look together, and when does each piece get used?

What this isn't. Not a token catalogue (the canonical values live in tokens.css — this doc explains what they're for). Not a content guide (see voice-guide.md). Not a theory paper (see design_philosophy.md). Not a snapshot of admin styling (Filament uses its own Amber theme — see §13).

Companion to design_rationale.md. Rationale = narrative + decisions. System = applied reference + governance + refusals. Read both. Where they disagree, tokens.css wins on values; this doc wins on roles.

Framing inheritance. This doc adopts the framing from the field essay How to Build a Design System (external, sibling private repo): a design system is a maintained dynamic agreement held by a value contract, seven faces (Structure / Seam / Signal / Graveyard / Ledger / Ritual / Residue), tokens-as-decisions, components-as-contracts, refusals, AI-assisted variation governance, and a Zone 5 for unmanaged territory.


§1 — Value contract

What this system protects:

What this system refuses:

The order of authority is: value → invariant → affordance → sensation → behaviour. The surface comes last; it carries values into use.


§2 — Seven faces of this system

The system honestly mapped, not aspirationally drawn.

Structure (the invariants)

Seam (where change passes through)

Signal (how failure reports itself)

Graveyard (deprecation paths)

Currently absent. No graveyard.md exists. No deprecation contract, no migration windows, no "removal_window" metadata. Components deprecate by quiet edit. Listed as a queued follow-up — see §14.

Ledger (the reasoning trail)

Ritual (cadenced practice)

Residue (named, not hidden)


§3 — Tokens at a glance

Reference scan. Canonical values live in tokens.css; the link target on each row points to its line.

Brand mark — as rendered

The logo is where every token below is composed. Masthead and footer apply two different role-treatments of the same SVG:

Masthead — on surface, full colour
SAFFCA — Southern African Foundation and Fellowship for Contemporary Art
width: var(--space-11xl) · 20rem · flex-shrink: 0
Footer — on ink, white silhouette
SAFFCA
width: var(--space-10xl) · 18rem · padding: 0 var(--space-sm) · filter: brightness(0) invert(1) · opacity: 0.85

The masthead variant carries the SVG's full palette (#166734 Forest ink, #cca42c Pollen rule, #e4b922 Bright Pollen stars, #57807f Sage figure). The footer variant flattens every fill to white via a CSS filter chain, then softens to 0.85 opacity so it sits with the rest of the footer text rhythm. Source: AppLayout.vue:45,97, layout.css:35–46,180–195.

Colour

--color-ink
#166734 — Forest
Primary text, structural authority. Matches logo wordmark fill.
--color-paper
#f8f7f3
Body background, the field
--color-accent
#7a6219 — Walnut
Interaction signal — links, hover, focus on cream only. Derived darker from logo Pollen for AA-body 5.46:1. Never on Forest ink.
--color-accent-hover
#635014
Accent under interaction
--color-rule
#cca42c — Pollen
Decorative ornament on cream only — hero rule, edges. Matches logo wordmark-separator. Not viable on Forest ink.
--color-sage
#57807f — Sage
Auxiliary — chips, tags, category indicators. Matches logo Egungun-figure teal. AA non-text on cream.
--color-muted
#6f6e62 — Warm grey
Secondary text, metadata. Warm taupe, less blue-corporate than the prior cool grey.
--color-border
#e2e0db
Quiet division
--color-surface
#ffffff
Cards, modals (elevation cue)
--color-highlight
#f0ede5
Placeholder, empty state
--color-focus
aliases --color-accent
Focus rings — reinforces accent's interaction role

Source rows: tokens.css:13–22.

Typography scale

--text-xs · 0.75remThe field abides quietly.
--text-sm · 0.875remThe field abides quietly.
--text-base · 1remThe field abides quietly.
--text-lg · 1.125remThe field abides quietly.
--text-xl · 1.25remThe field abides quietly.
--text-2xl · 1.5remSection heading
--text-3xl · 2remView headline
--text-4xl · 2.75remLarge headline
--text-5xl · 3.5remSAFFCA

Line heights --leading-tight/normal/relaxed = 1.2 / 1.5 / 1.75. Tracking --tracking-tight/normal/wide/wider = -0.02 / 0 / 0.05 / 0.1 em. See tokens.css:24–41.

Spacing

--space-xs · 0.375rem
--space-sm · 0.625rem
--space-md · 1rem
--space-lg · 1.5rem
--space-xl · 2rem
--space-2xl · 3rem
--space-3xl · 4rem
--space-4xl · 6rem
--space-5xl · 8rem
--space-6xl · 10rem
--space-7xl · 12rem
--space-8xl · 14rem
--space-9xl · 16rem
--space-10xl · 18rem
--space-11xl · 20rem

Adopted Option γ (minimum tweak) on 2026-04-27. xs and sm shifted up to 0.375 / 0.625rem to remove the two ×2 doublings that previously kicked the start of the curve. 6xl..11xl (linear +2rem) added to anchor logo widths on-scale: masthead = --space-11xl (20rem), footer = --space-10xl (18rem) — one step below.

Two-mode curve, deliberately. xs..5xl is perceptual — geometric ratios in the 1.33–1.5 band, where the eye reads steps as relative ("twice the gap" / "half the gap"). 5xl..8xl shifts to compositional — linear +2rem increments, where spacing serves layout anchors (logo widths, hero gutters, page rhythms) and clean integer distances matter more than perceptual ratios. The transition at ~8rem is the threshold where spacing stops being "feel" and starts being "fit." Ratios at the top narrow naturally (1.25 → 1.20 → 1.167) — that's the arithmetic of constant absolute steps over a growing baseline, not a flaw in the scale.

Source: tokens.css:44–58.

Layout

Page-rail tokens, rendered at one-third their real rem value so all three fit the doc page. Bars are proportional; numeric labels carry the actual value.

--content-width · 72rem
--content-narrow · 48rem
--sidebar-width · 16rem

Source: tokens.css:55–57.

Motion

Three durations rendered as live-animating bars. Each bar fills over its duration and reverses, looping — the eye reads the difference between fast and slow at real timing.

--duration-fast · 250ms
--duration-normal · 600ms
--duration-slow · 900ms

Three easings rendered as travelling dots. Same distance, same duration (600ms); the curve is the variable. Watch how each dot decelerates differently:

--easing-default · cubic-bezier(0.4, 0, 0.2, 1)
--easing-spring · cubic-bezier(0.34, 1.56, 0.64, 1)
--easing-depth · cubic-bezier(0.42, 0, 0.58, 1)

Source: tokens.css:60–65.

Adjacency rings (spatial discovery)

Three concentric rings centred on the focal node. Real values are viewport-relative (vmin), proportional rendering shown here at the 35 / 55 / 75 ratios:

ring 1 · 35vmin · documented
ring 2 · 55vmin · transitive
ring 3 · 75vmin · emergent

--adj-zoom-duration aliases --duration-slow (900ms — see Motion above). Source: tokens.css:68–71.

Z-index scale

Real layering: each card sits one z-level above the one below.

--z-base · 0
--z-raised · 10
--z-overlay · 100
--z-modal · 1000

Source: tokens.css:74–77.

Radii

Three corner-radius tokens, rendered at real value:

--radius-sm3px
--radius-md6px
--radius-lg12px

Source: tokens.css:80–82.


§4 — Colour palette and relationships (focus area)

a) Palette anatomy

Brand pivot 2026-04-28 — colours canonical to LOGO_20260428_R2_Pierre_r2.svg. Three brand hues:

Plus a derived text-role accent and an auxiliary:

Six utility colours that operationalise the brand:

Logo-internal (not tokenised):

b) Application logic — the "when"

Token When to use When NOT to use
--color-ink Primary text. Header / footer ground. Structural authority. The default text colour everywhere. Pure black anywhere — Forest replaces it.
--color-paper Body background. The field everything sits on. Also: text colour on Forest-ink backgrounds (footer, ink ribbons). As card surface (use --color-surface).
--color-accent Interactive cues on cream: links, hover, focus, type badges. Text-role gold, cream backgrounds only. Never on Forest ink (1.19:1 invisible). Never on large surfaces — that's decoration.
--color-accent-hover The state continuation of accent under pointer / keyboard interaction. As a default accent.
--color-rule Decorative ornament on cream: hero rule, edges, the wordmark-separator inside the brand mark. As a text colour anywhere. On Forest ink (2.95:1 — fails AA non-text).
--color-sage Auxiliary — chips, tags, supporter / photographer / artist category indicators in admin and public. Quiet earth-tone register that complements Forest. Body text. Primary surfaces.
--color-muted Secondary text — metadata, dates, source provenance, captions. Warm-grey taupe. Anything load-bearing. Body copy.
--color-border Card edges, dividers, table rules. Quiet division. To attract attention. As accent, rule, or sage.
--color-focus Focus rings. Aliased to accent so accent's interaction-role is reinforced visually. As a separate "branded accent."
--color-surface Cards, modals — elevation cue against --color-paper. As body background (paper is the field).
--color-highlight Placeholder cards, empty states, "no image yet" grounds. Absence-without-judgment. As a hover state. As accent.

The split between --color-accent and --color-rule is deliberate: text needs WCAG AA body 4.5:1; ornament only needs 3:1. By splitting, the system gets both — readable links and a properly bright gold for the hero rule, without compromising either.

c) Pairings and contrast

The intended pairings (computed against WCAG 2.1 AA):

Foreground Background Use Approx contrast
--color-ink #166734 --color-paper #f8f7f3 Body text on page 6.48 : 1 ✓ AA body (AAA-Large)
--color-ink #166734 --color-surface #ffffff Body text on cards 6.85 : 1 ✓ AA body
--color-paper #f8f7f3 --color-ink #166734 Text on Forest-ink ground (footer, ink ribbons) 6.48 : 1 ✓ AA body
--color-muted #6f6e62 --color-paper #f8f7f3 Metadata on page 4.80 : 1 ✓ AA body
--color-accent #7a6219 --color-paper #f8f7f3 Links / hover / focus 5.46 : 1 ✓ AA body
--color-accent #7a6219 --color-surface #ffffff Links on cards 5.77 : 1 ✓ AA body
--color-accent #7a6219 --color-ink #166734 Walnut on Forest 1.19 : 1 ✗ INVISIBLE — never use
--color-paper #f8f7f3 --color-accent #7a6219 Inverted button label 5.46 : 1 ✓ AA body
--color-rule #cca42c --color-paper #f8f7f3 Hero rule, ornament edge 2.20 : 1 — decorative-only per WCAG SC 1.4.11
--color-rule #cca42c --color-ink #166734 Pollen on Forest 2.95 : 1 ✗ — fails AA non-text; do not use
--color-sage #57807f --color-paper #f8f7f3 Auxiliary chips, tags 4.08 : 1 ✓ AA non-text + AA-Large body

Note on the rule on paper: at 2.1:1 it falls below AA-non-text 3:1. The rule is intended for graphical accent only, not user-interface lines. Where contrast matters (e.g. focus rings, links), use --color-accent. The hero rule is treated as a decorative atmospheric element — same category as a gold leaf in a fine-press masthead.

Forest ink on paper — body text in its native habitat6.48 : 1 ✓ AA body
Forest ink on surface — text on cards6.85 : 1 ✓ AA body
warm-grey muted on paper — metadata, captions4.80 : 1 ✓ AA body
Walnut accent on paper — links, hover, focus5.46 : 1 ✓ AA body
Walnut accent on surface — links on cards5.77 : 1 ✓ AA body
paper on Walnut — inverted button5.46 : 1 ✓ AA body
paper on Forest — header / footer ground, footer text6.48 : 1 ✓ AA body
Sage on paper — auxiliary chips, tags4.08 : 1 ✓ AA non-text
Pollen rule on paper — ornament only, not for text2.20 : 1 — decorative use
Walnut on Forest — INVISIBLE, never use1.19 : 1 ✗

Anything pairing --color-muted with --color-highlight or --color-border falls below AA — don't.

d) Aspirations and current residue

The rules above are the target. Living deviations the system currently tolerates (logged in §2, queued in §14):

The doc names these so they remediate, not so they hide.

e) Refusals (target-state)

f) Provenance

Derived from the SAFFCA logo. The system has now been through two pivots:

2026-04-27 — accent role split into two tokens against LOGO_20260427.svg: --color-accent#7d6428 (Walnut), --color-rule#caa84b (Rationale), --color-ink#192744 (Navy). The home hero rule, previously rendered as var(--color-border) (a pale grey, never actually gold in the implementation), was rewired to var(--color-rule).

2026-04-28 — brand pivot to forest green. LOGO_20260428_R2_Pierre_r2.svg replaces the navy-era logo. User direction: "less blue-corporate vibe, more Africa." Tokens canonical to the new SVG where viable: --color-ink#166734 (Forest), --color-rule#cca42c (Pollen, the logo's wordmark-separator), --color-sage#57807f (Sage, the Egungun-figure teal). The text-role accent --color-accent#7a6219 is derived from the SVG's Pollen, darkened ~25% to keep AA-body 5.46:1 on cream — the SVG's own Pollen at 2.20:1 fails text contrast and is reserved for decorative-only use. Warm-grey --color-muted#6f6e62 replaces the prior cool blue-grey, completing the move away from blue tones. The brightest yellow #e4b922 (EU stars) and the implementation of strict role enforcement (Walnut never on Forest ink at 1.19:1; Pollen never on Forest at 2.95:1) are documented in §4b's application-logic table.

The doc-as-spec ↔ logo-as-spec loop holds: where SVG values pass contrast, tokens take them canonical; where they don't, tokens are derived transparently from the same hue family.

Approved by Pierre 2026-04-28. The green regime is the settled brand.


§5 — Grid and spatial system (focus area)

The grid here is not a 12-col abstraction. It is a small set of named patterns that recompose responsively, plus a separate spatial-discovery layer (adjacency rings) that is conceptually distinct from the page grid.

a) Page-level layout

Three layout tokens carry the document container:

Token Value Use
--content-width 72rem Default max width for content rails
--content-narrow 48rem Long-form text, narrative views (about, support)
--sidebar-width 16rem Reserved (not used yet)

structure.css is currently minimal — body is a flex column on --color-paper. The grid lives in components.

b) Component grid patterns

Codified from current emergent use. Each pattern is a contract: purpose, minimum cell, behaviour, source, and an anti-example.

Pattern Purpose Min cell Behaviour Source Anti-example
Card grid (standard) Artists / team index 260–300px repeat(auto-fill, minmax(N, 1fr)) artists.css:5 (260px), team.css:20 (300px), events.css:5 (300px) Don't use as the sole hero
Card grid (compact) Dense lists, supporters, mobile reflow 140–160px same artists.css:10 (160px), pages.css:154 (supporters, 140px), artists.css:315 (140px ≤ 640px) Don't use for primary discovery
Mosaic Events feature region n/a repeat(var(--mosaic-cols, 3), …) × rows events.css:116 Don't use without curation
Masonry (CSS columns) Artwork browse, home hero artworks 220–260px columns: 4 N artworks.css:4 (250px), artworks.css:9 (220px browse), home.css:167 (260px) Reading order is column-down — flag in IA / a11y
Two-column profile Artist profile 300px 1fr Stacks ≤ 768px artists.css:74, artists.css:303 Don't use for non-portrait subjects
Asymmetric hero Home hero 1.4fr 1fr Stacks ≤ 768px home.css:7, home.css:90 Don't repeat off home
card grid (standard) · auto-fill, minmax(260–300px, 1fr)
card grid (compact) · auto-fill, minmax(140–160px, 1fr)
mosaic · explicit cols × rows, weighted cells
masonry (CSS columns) · arbitrary aspect ratios; reading order is column-down
two-column profile · 300px portrait + 1fr content; stacks ≤ 768px
asymmetric hero · 1.4fr / 1fr; stacks ≤ 768px

Note: supporters live in pages.css (not a supporters.css — that file does not exist).

c) Breakpoints and recomposition

Three breakpoints in active use today:

Width Used by
768px Artist profile stack, home recompose, layout main, adjacency
640px Artist compact grid shrink, content-strip, team stack
600px Events grid stack

The 600 / 640 mix is residue (§2). Canonicalise to 768 + 640 only (next-available ADR — see §14). Until then: 768 = "tablet recompose," 640 = "mobile pack," 600 = legacy events-only.

The principle from ux-vision.md is recompose, not reflow — at each breakpoint the layout is redrawn, not just narrowed.

d) Container queries

Not used today. Candidate where view-type recomposition would benefit (e.g. a card that must reflow based on its container width, not viewport width — adjacency edges, embedded rails). Listed in §14.

e) Adjacency rings (separate spatial concept)

Three rings at 35 / 55 / 75 vmin (--adj-ring1/2/3-radius) carry the spatial-discovery model documented in concept__permeable-spatial-navigation.md. This is not a page grid — it is a rendering geometry for computed adjacency. Page grid and adjacency rings co-exist; they don't reduce to each other.

f) Refusals


§6 — Type system

Family. Montserrat (Google Fonts), matching the WordPress theme. Weights loaded: 200, 300, 400, 500, 600, 700; italic 400 and 600. Mono fallback (--font-mono) reserved for code-like content.

Weight discipline.

Tracking. --tracking-wide and --tracking-wider reserved for uppercase labels (nav, type badges, metadata caps). --tracking-tight reserved for display sizes. Body uses --tracking-normal.

Multilingual. hyphens: auto for FR and NL; hyphens: manual for EN. text-wrap: balance on headings. The system must hold across three languages without one becoming a second-class citizen.

200 · displaySAFFCA
300 · section headingCross-continental dialogue
400 · bodySAFFCA connects African and European contemporary art communities through residencies, exhibitions, and multicultural dialogue.
500 · nav, badgesArtists · Events · Residencies
600 italic · quotes, emphasis"When I work in clay, the work works back."

Italic + 600 is the convention for quoted voice — artists describing their own practice (per Clean Language: use the artist's words, not a paraphrase), pulled passages in biography text, testimonials, and any content where the reader needs to feel the shift from institutional narration to first-person speech. Block quotes inherit this treatment; inline <em> should not, to preserve italic's legibility for body emphasis.

Implementation: typography.css. Tokens: tokens.css:24–41.


§7 — Motion

Three durations carry intent:

Token Value Use
--duration-fast 250ms Hover colour/opacity changes, focus rings
--duration-normal 600ms Card transitions, layout shifts, route fades
--duration-slow 900ms View-morphing transitions, adjacency zoom (--adj-zoom-duration aliases this)

Three easings:

Named view-transition groups. artwork-hero and artist-portrait are the two named morph anchors that survive route changes. See view-transitions.css.

Reduced motion is the floor, not a fallback. All timed work respects prefers-reduced-motion: reduce. Inertia transitions and named-group morphs disable themselves under that media query — see transitions.css and view-transitions.css.

Hover the buttons below to feel each duration:


§8 — Visual / aesthetic logic — applied (focus area)

The chain is value → invariant → affordance → sensation → behaviour. The system below is the chain made concrete.

Restraint as authority. Few colours, generous space, precise type. Institutional dignity (AX-4) does not arrive through ornament; it arrives through composure. The accent gets one role (interaction). The body gets one ground (paper). The hierarchy is carried by typography weight and spacing, not by colour proliferation.

Image primacy. Visual is content, not decoration. Chrome defers. Operationalised by image-led card grids (artwork hero takes the card; metadata is secondary) and by no-overlay artwork display — artworks are not draped with controls when shown in their dignity. See feedback_artwork-dignity-no-overlay.md in project memory.

Edge as continuity. Viewport edges hint at adjacency — content from neighbouring spaces leaks at the periphery. This is part of the system, not ornament. The adjacency rings (§3, §5e) are the geometry that makes edges meaningful: what is "near" is computed, not decorated.

Anti-decoration principle. Every colour, weight, motion, and grid choice serves content / hierarchy / relation (AX-0 — relationships are content). If a visual element does not serve one of these three, it does not belong. This is the cleanest available test: "What is this for?" If the answer is "atmosphere," the answer is no.

Parametric variation (P5). The system is meant to have range, not uniformity — see Munari's "a tree does not have identical leaves." The candidate axes (programme type, geographic context, temporal distance, artist medium) are not yet decided. Variation must emerge from meaningful parameters, not be imposed cosmetically (Eglash's African fractals, design_philosophy.md). Treat as Zone 5 until decided. The system permits variation; it does not yet specify which variation.

Graceful degradation as visual logic. The visual system holds without JavaScript. degraded.css is the floor. Image-led views must remain legible when images are missing — per-view-type fallback compositions are still TBD (rationale doc §"Missing Image Fallback") and tracked in §14.


§9 — Components as contracts (sketch)

Most components today live as ad-hoc CSS files plus Vue page components. They do not yet ship with the contract a real component should carry: anatomy, states, accessibility, content rules, refusals, deprecation path. This section sketches the contract format and shows one worked example. The rest is queued for iterative fill (§14).

Worked example: Artist card (standard)

Artist Name
Discipline · Region
Field Value
Purpose Index-grid representation of an artist; entry point to artist profile
Where used Artists index, home hero (artist row), event detail (participation list)
Anatomy Portrait (3:4) → name → discipline / nationality (muted)
States default · hover (border colour shifts to --color-accent) · focus-visible (focus ring on accent) · missing-image (text-led fallback — currently undefined)
Accessibility Card is a single semantic link; portrait has descriptive alt; focus ring inherits --color-focus
Tokens --color-surface ground · --color-ink name · --color-muted metadata · --color-accent interaction
Refusals No overlay controls on the portrait. No hover-only information. No fixed pixel height
Source artists.css:5–40
Open Missing-image fallback composition; parametric variation (e.g. artists with video)
Deprecation path n/a (current canonical)

Components inventory (contract-format pending)

Filling each row above is an iterative job. Don't block.


§10 — Refusals (consolidated)

Targets, with current residue acknowledged in §2. The system explicitly does not:


§11 — AI-assisted variation governance

Claude (and other AI tools) participate in producing this codebase. The system's stance:


§12 — Public ↔ admin boundary

The admin panel (Filament v3.3) uses Color::Amber and Filament's own design system. Public frontend uses Forest / Walnut / Pollen / Cream (with Sage as auxiliary) and the layered CSS architecture per ADR-002. The two systems do not share tokens. This is a current state, not a settled answer:

Listed as an open question in §14. No decision in this doc.


§13 — Open design questions / Zone 5

Honest gaps. Surfaced, not closed.

A design system with no wilderness becomes a small state.


§14 — Named follow-ups

These are not in this doc's scope. They are tracked here so they don't disappear.

FOLLOW-UP-A — Resolve undefined-token references (affects rendered UI today, not just docs). Components reference tokens that don't exist in tokens.css (--color-text-muted, --color-ink-muted, --color-surface-muted, --color-accent-dark, --color-danger) plus a literal #fff in auth.css:125. Browsers fall back to the inline hexes — production rendering is shaped by these unsanctioned values. Resolution: define the missing tokens in tokens.css (preferred) or migrate components to existing tokens and remove fallbacks. Track as a P-row. (--color-rule was previously in this list pre-2026-04-27 but is now defined.)

FOLLOW-UP-B — Refresh Playwright visual baselines under tests/visual/screenshots.spec.js-snapshots/ after any change that affects rendered output (FOLLOW-UP-A or FOLLOW-UP-C, not the doc-only motion fix in this batch).

FOLLOW-UP-C — Gold colour-quality pass.Done 2026-04-27, ✓ superseded by 2026-04-28 brand pivot. The Walnut + Rationale split was the right resolution against the navy logo; the forest-green pivot rotated the entire palette, so the gold values shifted to Pollen + derived Walnut. Roles preserved (text-AA accent + decorative rule); contrast figures recomputed in §4c. Visual baselines refresh queued (FOLLOW-UP-B) for the green-era render.

FOLLOW-UP-D — Render system.md in-browser.Done 2026-04-27. Live at /about/site/design-system (public, no env or auth gate; relocated 2026-05-16 from /about/design-system, which now 301-redirects, as part of the /about/site colophon). Backed by App\Http\Controllers\DesignSystemController, rendered via league/commonmark, wrapped in resources/views/design-system.blade.php with full design-token CSS so the inline-HTML demos render with real values.

Out of scope (queued, possibly via /schedule):