theexperiencecompany/gaia
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·July 23, 2026
Elevated
Design risk in this codebase.
Verdict
This is a straightforward asset-download gallery with clean, token-based muted text and thoughtful contrast pairing between logo color and swatch background. The real gap is hierarchy: the copy calls out the GAIA logo as the "primary" brand mark, but all six sections render with identical heading weight and structure, so nothing in the layout backs up what the words are telling the user. The card interaction pattern (click to copy, checkmark confirms) is the right idea, but the feedback loop is broken in a few concrete ways: success shows even when the clipboard write fails, screen readers never hear the confirmation, and the copy hint only reveals itself on hover-capable pointers. Fix the promise handling and the live region first — those are the two places where a user genuinely can't tell what happened after they act. The component's core interaction, revealing download buttons on hover, breaks down completely on touch devices and leaves keyboard focus invisible, which blocks the entire download action for a large share of users. Underneath that, a small gray-scale token mix and a hand-rolled anchor-click pattern add drift and lose native link affordances. Fixing the hover-to-focus/touch gating is the one change that would make this component actually work everywhere.
Files Rams reviewed
apps/web/src/app/[locale]/(landing)/brand/components/BrandAssets.tsx
apps/web/src/app/[locale]/(landing)/brand/components/BrandColors.tsx
apps/web/src/app/[locale]/(landing)/brand/components/DownloadAsset.tsx
apps/mobile/src/app/(app)/_layout.tsx
apps/mobile/src/app/_layout.tsx
apps/web/src/app/[locale]/(desktop)/desktop-popup/feed/page.tsx
apps/web/src/app/[locale]/(landing)/about/page.tsx
apps/web/src/app/[locale]/(landing)/ai-chief-of-staff/page.tsx
apps/web/src/app/[locale]/(landing)/alternative-to/[slug]/page.tsx
apps/web/src/app/[locale]/(landing)/alternative-to/page.tsx
apps/web/src/app/[locale]/(landing)/automate/[combo]/page.tsx
apps/web/src/app/[locale]/(landing)/automate/page.tsx
apps/web/src/app/[locale]/(landing)/blog/[slug]/page.tsx
apps/web/src/app/[locale]/(landing)/blog/page.tsx
apps/web/src/app/[locale]/(landing)/bots/page.tsx
apps/web/src/app/[locale]/(landing)/brand/page.tsx
apps/web/src/app/[locale]/(landing)/cli/page.tsx
apps/web/src/app/[locale]/(landing)/compare/[slug]/page.tsx
apps/web/src/app/[locale]/(landing)/compare/page.tsx
apps/web/src/app/[locale]/(landing)/contact/page.tsx
apps/web/src/app/[locale]/(landing)/desktop-login/page.tsx
apps/web/src/app/[locale]/(landing)/download/page.tsx
apps/web/src/app/[locale]/(landing)/faq/page.tsx
apps/web/src/app/[locale]/(landing)/features/[slug]/page.tsx
apps/web/src/app/[locale]/(landing)/for/page.tsx
apps/web/src/app/[locale]/(landing)/inbox-zero-ai/page.tsx
apps/web/src/app/[locale]/(landing)/learn/[term]/page.tsx
apps/web/src/app/[locale]/(landing)/learn/page.tsx
apps/web/src/app/[locale]/(landing)/login/page.tsx
apps/web/src/app/[locale]/(landing)/manifesto/page.tsx
UX
apps/web/src/app/[locale]/(landing)/brand/components/BrandColors.tsx:14
Copy button shows a success checkmark even when clipboard write fails
In `copyToClipboard`, `setCopied(true)` runs immediately after calling `navigator.clipboard.writeText(text)`, without awaiting the returned promise or catching a rejection. If the clipboard write is blocked (denied permission, insecure context, Safari restrictions), the UI still swaps to the `Tick01Icon` success state.
Why it matters
Users see a checkmark and assume the hex value is on their clipboard, then paste nothing or stale data elsewhere: the feedback actively lies about the outcome instead of failing silently or visibly.
Fix
Only show success state after the clipboard promise resolves, and surface a failure state when it rejects.
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};const copyToClipboard = (text: string) => {
navigator.clipboard
.writeText(text)
.then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
})
.catch(() => setCopied(false));
};apps/web/src/app/[locale]/(landing)/brand/components/BrandAssets.tsx:11
Copy calls the GAIA logo "primary" but its treatment matches every secondary section
The GAIA Logo section's subtitle reads "The primary GAIA brand mark", but its heading ("GAIA Logo") uses the exact same `text-3xl font-medium` treatment as the five sections below it ("GAIA Wordmarks", "The Experience Company Logo", "Experience Wordmarks", "The Experience Company Full Wordmarks"). Nothing in size, weight, or layout signals that this is the flagship asset the copy claims it is.
Why it matters
When the text says one asset is primary but the visual system treats all six sections identically, users scanning the page get no cue about where to start, and the stated hierarchy in the copy becomes meaningless.
Fix
Give the section explicitly called out as primary a visibly larger or heavier heading than the secondary sections that follow it.
<h2 className="mb-3 text-3xl font-medium">GAIA Logo</h2><h2 className="mb-3 text-4xl font-semibold">GAIA Logo</h2>apps/web/src/app/[locale]/(landing)/brand/components/BrandColors.tsx:39
"Copy" hint only appears on hover, leaving touch users no affordance
The `Copy` label at line 39 is `opacity-0 group-hover:opacity-100` with no `@media (hover: hover)` gate. On touch devices, hover never fires as a persistent state, so mobile users tapping a color card get zero visual cue that clicking copies the value until after they've already clicked.
Why it matters
The only discoverability hint for the card's core action is invisible on the device class most people browse a brand page on, so first-time mobile users have no way to know what tapping a swatch does.
Fix
Show the copy affordance by default at a lower opacity, and enhance to full opacity only on real hover-capable pointers.
<span className="text-xs opacity-0 group-hover:opacity-100">
Copy
</span><span className="text-xs opacity-60 sm:opacity-0 sm:group-hover:opacity-100">
Copy
</span>apps/web/src/app/[locale]/(landing)/brand/components/BrandColors.tsx:70
Section copy promises RGB copying that the button never does
The intro text reads "Click any color to copy its hex or RGB value," but `copyToClipboard(hex)` at line 23 only ever copies the hex string: there is no RGB conversion or alternate copy path anywhere in the file.
Why it matters
A user who wants an RGB value clicks expecting a choice, gets hex on their clipboard instead, and has to convert it manually or hunt for a feature that doesn't exist.
Fix
Match the description to the actual behavior of the action it describes.
<p className="text-foreground-600 dark:text-foreground-400">
Our primary brand colors. Click any color to copy its hex or RGB
value.
</p><p className="text-foreground-600 dark:text-foreground-400">
Our primary brand colors. Click any color to copy its hex value.
</p>Accessibility
apps/web/src/app/[locale]/(landing)/brand/components/DownloadAsset.tsx:46
Keyboard focus lands on download buttons with zero visible indication
Because visibility is controlled only by `isHovered` (mouse enter/leave) and not by `:focus-within`, tabbing to a download `<Button>` inside the `opacity-0` wrapper gives it keyboard focus while it remains invisible. A sighted keyboard user tabbing through the page has no way to see where focus is.
Why it matters
Keyboard-only users lose track of focus entirely on this control, which fails WCAG 2.4.7 and makes the download action effectively unusable without a mouse.
Fix
Tie the reveal state to group-focus-within as well as group-hover so keyboard focus makes the control visible.
<div
className="group relative"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
><div className="group relative">Get this score on every PR.
Rams reviews each pull request on your repo and posts inline one-click fixes — about a minute per review.
Install Rams freeColor
apps/mobile/src/app/(app)/_layout.tsx:51
Hardcoded hex blue on the loading spinner bypasses the token system
The loading screen renders `<ActivityIndicator size="large" color="#00bbff" />` inside a `<View className="flex-1 ... bg-background">`. The container uses the semantic `bg-background` token but the spinner's accent color is a raw hex value with no traceable source.
Why it matters
If the brand accent ever changes or the app adds a dark/light theme variant, this hex value won't update with the rest of the palette, and the spinner drifts out of sync with every other accent color in the app.
Fix
Reference the app's accent token instead of a literal hex value.
<ActivityIndicator size="large" color="#00bbff" /><ActivityIndicator size="large" color={colors.accent} />Typography
Spacing
Components
Motion
Craft
Working well
- Centralizing every route's transition animation in one `screenOptions`/`Stack.Screen` list (tabs get `animation: "none"`, modals get `slide_from_bottom` with `presentation: "modal"`, detail screens get `slide_from_right`) keeps navigation motion consistent and predictable across the whole app instead of each screen picking its own transition.
- Each wordmark pair thoughtfully matches logo color to background contrast: "GAIA Wordmark Black" gets `backgroundColor="light"` and "GAIA Wordmark White" gets `backgroundColor="dark"`. This guarantees every swatch stays legible regardless of the asset's own color, which is exactly the kind of pairing a brand-asset page needs to get right.
- Button labels adapt to the option: `option.variant ? \`${option.format} (${option.variant})\` : option.format` (line 76-78) gives users a specific outcome ('PNG (Dark)') instead of a generic 'Download' label, so each control tells you exactly what you'll get before you click it.
- The onboarding status fetch defaults `onboardingCompleted` to `true` on failure, with a comment explaining that existing users shouldn't be blocked if the endpoint isn't deployed yet. That's a defensive fallback tied to a real deployment scenario, not a silently swallowed error.
Scored July 23, 2026 with Rams Engine v0.0.2 · Engine changelog
Score your own repo.
Free on public repos, no account. The same engine that scored this page reads your UI code and mints a score page like this one.
Public repos only. The full engine reviews the UI code and mints a public score page — we email you the link too. Already-scored repos open instantly.
Or get this review on every PR.