Accessibility
Contrast ratios, focus states, gradient text-fill vendor-prefix requirements, and reduced-motion rules for the Sandworm design system. Ratios computed 2026-06-10 via WCAG 2.1 relative-luminance math.
Contrast ratios — dark surfaces
Key foreground/background pairs on stone-950 /stone-975. The critical caveat: magenta-600 on dark is 3.1:1 — display/headline scope only. For body-size magenta on dark, use magenta-400 (5.6:1).
Large text (18pt+/14pt bold) or non-essential metadata only — fails AA at body size
Display/headline only (≥24px). Fails AA at body size. Use magenta-400 for small text on dark.
Use this for body-size magenta accents on dark surfaces
The warm-hero gradient's violet tail. Same large-text scope as magenta-600 on dark.
Contrast ratios — light surfaces
Key pairs on stone-025. Note that magenta-600 is comfortably AA on light (5.8:1) — the dark-mode caveat does not apply here.
Comfortably AA on light — the dark-mode caveat does not apply here
Brand emphasis and the dark-mode caveat
The signature emphasis devices — font-semibold text-magenta-600 spans and the warm-hero gradient text-fill — are display patterns, not body-text patterns, on dark surfaces.
- ✓Hero/headline emphasis (h1/h2, ≥24px) — magenta-600 and gradient text-fills are fine on dark
- ✓Body-size emphasis on light surfaces — magenta-600 is 5.8:1, comfortably AA
- ✓Body-size magenta accent on dark — use
magenta-400(5.6:1) - ✗Don't carry essential meaning in body-size magenta-600 or gradient text on dark — at those sizes it's decorative emphasis; the surrounding font-light copy must carry the content
Gradient text-fill profile on stone-950: sand-300 end 10.7 → red-400 mid 7.6 → violet-600 tail 3.0. The tail is the constraint — gradient emphasis carries the same large-text scoping as solid magenta.
Gradient text-fill — Safari vendor prefix rule
background-clip: text requires both the standard and -webkit- forms or Safari/iOS renders the gradient as a background block instead of clipping to the text shape. Always include all three declarations.
Required CSS pattern
background: linear-gradient(to right, #ffb370, #f9808a, #6242e0); background-clip: text; -webkit-background-clip: text; /* Safari */ -webkit-text-fill-color: transparent; /* Safari */ color: transparent; /* fallback */
Without -webkit-text-fill-color: transparent, Safari may render the original text color over the clipped gradient.
Live example — warm-hero gradient headline
Sandworm.
sand-300 → red-400 → violet-600 · warm-hero gradient
Backdrop blur — also needs -webkit-
backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); /* Safari */ background-color: rgba(23, 13, 28, 0.9); /* stone-950/90 fallback */
Card-dark and modal surfaces. Always include a solid background fallback — backdrop-filter fails silently in browsers that don't support it.
Focus rings
Every interactive element needs a visible focus ring on keyboard navigation. Use :focus-visible (not :focus) so the ring only appears when needed — keyboard nav, not mouse click. Keyboard accessibility is a brand requirement: if a custom interactive element doesn't have a visible focus ring, it isn't shippable.
Token spec
Ring color: magenta-600 at 2px
Ring offset: 2px
Offset color on dark: stone-950 (matches surface)
Offset color on light: stone-025 (matches surface)
Focusable demo — Tab to this element
Tailwind utility pattern
/* Applied to interactive elements */ focus-visible:ring-2 focus-visible:ring-[hsl(var(--magenta-600))] focus-visible:ring-offset-2 focus-visible:ring-offset-[hsl(var(--stone-950))] /* dark */ focus-visible:ring-offset-[hsl(var(--stone-025))] /* light */
Don't substitute the browser's default blue focus ring — the magenta-600 ring works on both modes because magenta-600 is a brand spine color, not a status color.
Disabled state
opacity: 0.5
pointer-events: none
Reduced motion
@media (prefers-reduced-motion: reduce) is non-optional for any animated surface. Ship the reduced-motion off-switch in the same change as the animation. If an animation can't degrade gracefully, gate it explicitly.
Recommended global baseline
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}This is the recommended hub pattern. The marketing site currently implements reduced motion per-surface rather than via one global wildcard — both approaches are valid.
Per-surface pattern (current marketing site)
- Rotating border — drops to
animation: none+ static linear-gradient fallback - Marquee logo wall —
animation: none !important - Float/twinkle/spinner loops —
animation: none - JS-driven loops — read
window.matchMedia('(prefers-reduced-motion: reduce)')and haltrequestAnimationFrameloops
JS guard pattern for animated components
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
if (!prefersReducedMotion) {
requestAnimationFrame(animationLoop);
}Touch targets
Interactive touch targets should be at minimum 44×44px (WCAG 2.5.5 AAA target; the AA bar is 24×24px). For icon-only buttons, use a wrapper with padding to meet the minimum hit area even if the visual element is smaller. The size="icon" Button variant defaults to h-9 w-9 (36px) — add h-11 w-11 for primary touch surfaces.