Geolify

GEO·00 — Design system audit

The system behind the score.

Everything needed to ship surfaces that read unmistakably Geolify — the tokens, the components, the mark, the voice. Every value on this page is verified against the shipped design-system source; the guide renders on the tokens it documents.

Scope: the marketing register (.geolify-*) plus @repo/ui primitives. The workspace register is documented separately.

Sections — 010Tokens — 095 verifiedRegisters — 002Tailwind v4 · @repo/ui
ViewThemeScroll ↓

GEO·01 — Color field

Zinc does the work. Color reports state.

A dark-first OKLCH palette: zinc carries every surface, and color arrives only to report state. Raw hex never appears in product markup — semantic tokens or nothing.

Semantic tokens — :root / .dark

Every surface is a variable. Toggle the theme and the meaning flips — never the markup. Name a role and the page re-themes itself.

--background

L #ffffffD #000000

Page canvas

--card

L #ffffffD #141414

Elevated panels, cards

--popover

L #ffffffD #171717

Floating layers — menus, tooltips

--muted

L zinc-100D zinc-800

Secondary fills, inactive tabs

--secondary

L zinc-100D zinc-800

Auxiliary actions

--secondary-soft

L zinc-50D #141414

Softest secondary fill

--accent

L zinc-100D zinc-800

Highlighted rows

--section-muted

L zinc-50D #141414

Muted section ground

--section-accent

L zinc-100D zinc-800

Accented section ground

--foreground

L zinc-950D #ffffff

Default text

--card-foreground

L zinc-950D #ffffff

Text on cards

--popover-foreground

L zinc-950D #ffffff

Text on popovers

--muted-foreground

L zinc-600D zinc-400

Secondary copy, captions

--secondary-foreground

L zinc-900D #ffffff

Text on secondary

--accent-foreground

L zinc-900D #ffffff

Text on accent

--primary

L zinc-900D #ffffff

Key CTAs, active states

--primary-foreground

L #ffffffD zinc-950

Text on primary

--primary-soft

L zinc-200D zinc-800

Quiet primary fill

--primary-subtle

L zinc-100D #141414

Subtlest primary ground

--primary-outline

L zinc-300D rgb(255 255 255 / 0.14)

Primary outline

--secondary-strong

L zinc-700D zinc-300

Strong secondary ink

--focus-ring

L infoD info

Focus-visible ring — never remove

--border

L zinc-200D rgb(255 255 255 / 0.08)

Dividers, card edges, inputs

--input

L zinc-200D rgb(255 255 255 / 0.08)

Input strokes

--ring

L zinc-400D zinc-500

Focus ring outline

--success

L oklch(0.696 0.17 162.48)D oklch(0.696 0.17 162.48)

Positive trends, healthy readings

--warning

L oklch(0.769 0.188 70.08)D oklch(0.769 0.188 70.08)

Caution, pending state

--info

L oklch(0.623 0.214 259.815)D oklch(0.623 0.214 259.815)

Informational callouts

--destructive

L oklch(0.577 0.245 27.325)D oklch(0.704 0.191 22.216)

Errors, destructive actions

--destructive-foreground

L whiteD white

Text on destructive

The 4-tier hue matrix8 hues × 4 roles

One hue, four roles. Border, muted-fill and emphasis derive from base in oklab, so a status stays legible on either theme without being re-picked.

-base-border-muted-fill-emphasis
--neutralneutral-baseneutral-borderneutral-muted-fillneutral-emphasis
--successsuccess-basesuccess-bordersuccess-muted-fillsuccess-emphasis
--warningwarning-basewarning-borderwarning-muted-fillwarning-emphasis
--destructivedestructive-basedestructive-borderdestructive-muted-filldestructive-emphasis
--infoinfo-baseinfo-borderinfo-muted-fillinfo-emphasis
--purplepurple-basepurple-borderpurple-muted-fillpurple-emphasis
--orangeorange-baseorange-borderorange-muted-fillorange-emphasis
--cyancyan-basecyan-bordercyan-muted-fillcyan-emphasis

Derivation in color-mix(oklab): light theme mixes border 20%, muted-fill 10%, emphasis 60% toward foreground; dark theme uses 25% / 15% / 60%. Neutral reads semantic vars directly.

Neutral ramps

Zinc does the heavy lifting on every screen. Two quieter ramps — bunker, pampas — sit in reserve for surfaces that need a different temperature.

50#fafafa
100#f4f4f5
200#e4e4e7
300#d4d4d8
400#a1a1aa
500#71717a
600#52525b
700#3f3f46
800#27272a
900#18181b
950#09090b
TokenValueRole
--color-bunker-*oklch 0.98→0.12Cool dark neutrals
--color-pampas-*oklch 0.98→0.20Warm neutrals

Sidebar, control & shadow-ring sets

Chrome gets its own scale: the sidebar, the control fill ladder, and the ring pieces that make elevation legible at 1px.

TokenLightDarkRole
--sidebar#ffffff#000000Sidebar ground
--sidebar-foregroundzinc-900zinc-100Sidebar text
--sidebar-primaryzinc-950#ffffffActive sidebar item
--sidebar-primary-foreground#ffffff#000000Text on active item
--sidebar-accentzinc-100#141414Hovered sidebar item
--sidebar-accent-foregroundzinc-900#ffffffText on hover
--sidebar-borderzinc-200rgb(255 255 255 / 0.08)Sidebar dividers
--sidebar-ringzinc-400zinc-500Sidebar focus ring
TokenLightDarkRole
--control-bg#ffffff#222225Control resting surface
--control-hover#fafafa#2e2e33Control hover surface
--control-selected#f1f1f1#333338Control selected surface
--control-primary#18181a#ffffffPrimary control fill
--control-primary-hover#2c2c2e#fafafaPrimary control hover
ClassValueRole
--shadow-ring-surfaceL rgba(0, 0, 0, 0.05) · D rgba(255, 255, 255, 0.03)Ambient ring
--shadow-ring-2L rgba(0, 0, 0, 0.08) · D rgba(255, 255, 255, 0.06)Deeper ring
--shadow-ring-edgeL rgba(0, 0, 0, 0.05) · D rgba(255, 255, 255, 0.08)1px edge ring

Chart paletteSeries + semantic aliases

The only saturated hues in the system. They encode series and status — never mood.

TokenLightDarkRole
--chart-1oklch(0.646 0.222 41.116)oklch(0.488 0.243 264.376)Series 1
--chart-2oklch(0.6 0.118 184.704)oklch(0.696 0.17 162.48)Series 2
--chart-3oklch(0.398 0.07 227.392)oklch(0.769 0.188 70.08)Series 3
--chart-4oklch(0.828 0.189 84.429)oklch(0.627 0.265 303.9)Series 4
--chart-5oklch(0.769 0.188 70.08)oklch(0.645 0.246 16.439)Series 5
TokenLightDarkRole
--chart-positivesuccesssuccessPositive series
--chart-negativedestructivedestructiveNegative series
--chart-warningwarningwarningWatch series
--chart-infoinfoinfoInformational series
--chart-primaryprimaryprimaryPrimary series

Social-brand colors

Third-party marks keep their own hex. It is the one place raw color is allowed.

TokenValueRole
--color-brand-linkedin#0a66c2LinkedIn
--color-brand-twitter#1da1f2X / Twitter
--color-brand-facebook#1877f2Facebook

GEO·02 — Type register

Two voices. One cadence.

Inter carries prose and UI; JetBrains Mono carries every reading. Composite text tokens bind size, leading, tracking and weight into a single step.

Font familiesvendored woff2, no CDN

Inter does the talking, JetBrains Mono does the counting. Two weights of one and one of the other, vendored — nothing arrives from a CDN.

FamilyValueRole
Inter · var(--font-inter)Inter-Regular.woff2 w400 + Inter-Bold.woff2 w700UI, headings, body
JetBrains Mono · var(--font-mono)JetBrainsMono-Regular.woff2 w400Code, metrics, technical data

Audit. Fix. Track. — Inter 400/700

GEO Score: 51/100 — JetBrains Mono 400

Semantic text scale--text-* composites

Each step carries its own line-height, tracking and weight — a size cannot drift from its cadence.

Audit. Fix. Track.

text-label-xs · 9px

Audit. Fix. Track.

text-label-sm · 10px

Audit. Fix. Track.

text-label · 11px

Audit. Fix. Track.

text-micro · 12px

Audit. Fix. Track.

text-caption · 13px

Audit. Fix. Track.

text-body-sm · 14px

Audit. Fix. Track.

text-body · 16px

Audit. Fix. Track.

text-headline · 22px

Audit. Fix. Track.

text-title-mini · 18px

Audit. Fix. Track.

text-title-small · 24px

Audit. Fix. Track.

text-title-regular · 28px

Audit. Fix. Track.

text-display · 56px

Audit. Fix. Track.

text-button-sm · 13px

Audit. Fix. Track.

text-button-md · 14px

Audit. Fix. Track.

text-table-header · 11px

ClassValueRole
text-label-xs9px · lh 1.33 · ls — · w400Dense micro labels (alias: text-9px)
text-label-sm10px · lh 1.4 · ls — · w400Dense micro labels (alias: text-10px)
text-label11px · lh 1.45 · ls — · w400Dense micro labels (alias: text-11px)
text-micro12px · lh 1.2 · ls — · w400Badges, fine print
text-caption13px · lh 1.38 · ls — · w400Captions, metadata
text-body-sm14px · lh 1.43 · ls -0.175px · w500Compact UI text
text-body16px · lh 1.5 · ls — · w400Default reading size
text-headline22px · lh 1.28 · ls -0.25px · w600Section headlines
text-title-mini18px · lh 1.28 · ls -0.25px · w500Small titles
text-title-small24px · lh 1.33 · ls -0.3px · w600Card titles
text-title-regular28px · lh 1.14 · ls -0.25px · w600Page titles
text-display56px · lh 1.1 · ls -0.02em · w700Hero display
text-button-sm13px · lh 1.38 · ls — · w500Button sm
text-button-md14px · lh 1.43 · ls -0.175px · w500Button md
text-table-header11px · lh 1.45 · ls 0.05em · w600Table headers

Marketing registerthemes/home.css

The marketing register layers editorial classes on top of the scale. The samples below run on the classes they document — this is the real rendering, not a mock.

Measure AI visibility

geolify-h1 · 40→56px · medium

The score is the contract

geolify-h2 · 36→48px · medium

A 0–100 GEO Score paired with the fixes that move it.

geolify-lead · 15→18px · muted

Every audit returns ordered optimization tasks — the work that changes the reading, not a report to file away.

geolify-body · 14→16px · muted

“The audit found what the dashboard was missing.”

geolify-quote · 14→16px · italic

51

geolify-stat-number · 32px · medium

GEO Score · out of 100

geolify-stat-label · 12→14px · muted

GEO Score · Pillar breakdown

geolify-caption · 12px · muted-70

Published 12 Mar 2025 · Methodology

geolify-meta · 12→14px · muted
ClassValueRole
geolify-h140→48→56px · lh 1.14–1.17 · medium · ls -0.03em→-0.04emPage hero headline
geolify-h236→40→48px · lh 1.17–1.2 · medium · ls -0.03emSection headline
geolify-lead15→18px · lh 22→28px · regular · ls -0.01em · mutedSection lede under h2
geolify-body14→16px · lh 20→24px · mutedMarketing body copy
geolify-detail12px · lh 1.2 · muted-70Fine print, footnotes
geolify-quote14→16px · lh 20→24px · italic · mutedPull quotes, testimonials
geolify-attributiontext-body-sm · foregroundQuote attribution
geolify-caption12px · lh 1.2 · ls -0.15px · muted-70Captions, stat labels
geolify-meta12→14px · lh 1.2→1.43 · ls -0.15→-0.175px · mutedDates, categories, meta
geolify-card-title18→20px · lh 1.33→1.2 · semibold · ls -0.045→-0.25pxCard headline
geolify-card-subtitle14→16px · lh 20→24px · mutedCard subline
geolify-stat-number32px · lh 1.25 · medium · ls -0.72pxHero stat figure
geolify-stat-label12→14px · lh 1.2→1.43 · mutedHero stat caption

Only weights that ship are documented: Inter 400/700, JetBrains Mono 400. Do not reference Inter Medium/SemiBold files — the browser synthesizes them.

GEO·03 — Control surface

Controls that report their state.

Product UI composes @repo/ui primitives — Radix underneath, CVA for variants, tokens for every visual decision. Call sites set layout; the internals own their styling.

Live demosreal components, no mocks

Every control below is the shipped primitive, rendered live — this guide composes the same contracts it documents.

FIG·01 — Button · 14 variants × 9 sizes
default · infosecondarydestructiveoutlinesuccesswarningorangepurpleprimary-soft
FIG·02 — Badge · tier-mapped variants
9484746445—
FIG·03 — ScoreBadge · score → tier mapping
FIG·04 — Input · Switch · Checkbox · control surface contract

Catalog

Actions

ComponentVariants / sizesRole
Buttondefault · destructive · outline · outline-soft · destructive-outline · success · secondary · secondary-outline · ghost · secondary-chip · ghost-muted · ghost-destructive · link · inverted — sizes: default · xs · sm · md · compact · lg · icon · icon-sm · icon-lgAll clickable actions; icon slots via lucide children
Toggle · ToggleGroup—Binary state controls

Indicators

ComponentVariants / sizesRole
Badgedefault · secondary · destructive · outline · success · warning · orange · purple · primary-soft — sizes: default · xs · sm · lg · weights: default · normal · semiboldStatus chips — every variant maps to the hue tier matrix
ScoreBadgescore prop → ≥90 success · ≥80 default · ≥70 warning · ≥60 orange · <60 destructive · null —GEO Score / numeric ratings
CountBadge—Numeric counts
Progress · Skeleton—Loading and progress states
StatusAlert · Alert—Inline status messaging

Forms

ComponentVariants / sizesRole
Inputsize: default · sm — control-bg surface, shadow-low → shadow-low-focusText fields; affixes via padding only
Textarea · Label · Field—Form primitives
Select · Combobox—Choice inputs
Checkbox · RadioGroup · Switch—Toggles and option sets
InputOTP · InputGroup—Specialized inputs
Calendar—Date picking

Overlays

ComponentVariants / sizesRole
Dialog · Sheet · Drawer-capable surfaces—Modal flows
Popover · Tooltip · HoverCard-free—Contextual floating UI
DropdownMenu · ContextMenu · Menubar-free—Action menus
Command—Command palettes
Sonner (toast)—Notifications
ImageLightbox—Media zoom

Data display

ComponentVariants / sizesRole
Table · PinnedTable · TableSort—Dense data tables
ChartContainerRecharts wrapperChart scoping + theming
Avatar · UserAvatar—Identity
Separator · ScrollArea · AspectRatio—Structure helpers

Navigation & layout

ComponentVariants / sizesRole
Layout · Section · Container · Stack—Page primitives
Sidebar—App shell navigation
NavigationMenu · Breadcrumb · Tabs · Accordion · Collapsible—Wayfinding
Card · SurfaceCardedge contract — no border/ring/shadow restyleContained surfaces

Brand & marketing

ComponentVariants / sizesRole
GeoAsset—Brand-aware image wrapper (logo rendering)
ThemeMenu—Light/dark theme switching
GeolifyLoader · MultiStepLoader—Branded loading states
Marquee · WordRotate · LightRays · Ripple—Marketing motion
BentoGrid · GeolifyFeatureCard—Marketing layouts

Usage contract

Control rules — the contract

heights
Control heights are owned by the size prop: sm 32px, default 40px, lg 44px. Never pass h-*/size-* to a control.
edges
Card, SurfaceCard, Dialog and Input own their border/ring/shadow composite. Call sites may pass layout classes only — the design-system lint rules enforce it.
icons
Icons come from lucide-react via @repo/ui. Icon affixes inherit the control's size contract.
banned
Banned in app code: raw palette utilities (bg-emerald-500), arbitrary values, inline styles, dynamic className construction.

GEO·05 — Grid & measure

Density on a grid — never clutter.

One spacing ladder drives padding, gap and rhythm. Layout primitives own the 1024px measure, card density and control heights — nothing else may set them.

Spacing scale--spacing-N · utility p-N, gap-N…

One ladder spaces everything — card padding, section rhythm and the container measure all resolve to the same scale.

00px
px1px
0.50.125rem
10.25rem
1.50.375rem
20.5rem
2.50.625rem
30.75rem
3.50.875rem
41rem
51.25rem
61.5rem
71.75rem
82rem
92.25rem
102.5rem
112.75rem
123rem
143.5rem
164rem
205rem
246rem
287rem
328rem
369rem
4010rem
4411rem
4812rem
5213rem
5614rem
6015rem
6416rem
7218rem
8020rem
9624rem

Layout tokens & marketing structure

ClassValueRole
--container-max-width1024pxPage container (site-container)
--spacing-container-max1024pxSame value, spacing namespace
--card-padding-desktopspacing-card-paddingCard inner padding ≥sm
--card-padding-mobilespacing-card-padding-mobileCard inner padding mobile
--card-gap0.75remCard internal gap
--spacing-card-padding1.5remCard padding (canonical)
--spacing-card-padding-mobile1remCard padding mobile
--spacing-card-gap0.75remCard internal gap
--spacing-table-row2.25remTable row height (36px)
--spacing-table-header-row2remTable header row height
--spacing-table-cell-x0.75remTable cell inline padding
--spacing-table-cell-y0.5remTable cell block padding
--spacing-toolbar2.5remToolbar bar height
--spacing-control-xs1.75remExtra-small control height
--spacing-control-sm2remCompact control height
--spacing-control-md2.25remMedium control height
--spacing-control2.5remDefault control height
--spacing-control-lg2.75remLarge control height
--spacing-hero-cardmin(100%, 420px)Hero card width cap
--spacing-safe-topmax(env(safe-area-inset-top), 0.5rem)Notch-safe top
--spacing-safe-bottommax(env(safe-area-inset-bottom), 1rem)Notch-safe bottom
ClassValueRole
site-containermax-width 1024px · px-4 / sm:px-6 / lg:px-0Marketing page container (components.css)
geolify-home-sectionpy-16 / sm:py-20 / lg:py-24Vertical rhythm for a marketing section
geolify-section-headermb-8 / sm:mb-10 / lg:mb-12Space under headline+lede block
geolify-grid-gapgap-4 / sm:gap-6Default card-grid gutter
geolify-home-grid-three-columnsgrid · gap-6→8 · md:2col · lg:3colThree-up feature grid
geolify-home-grid-pricing-columnsgrid · gap-4 · md:2col · lg:4colPricing tier grid
geolify-section-subtitle-spacingmt-6 / sm:mt-7Space before a section subtitle row
geolify-section-subtitle-spacing-widemt-7 / sm:mt-8Wider subtitle spacing

Radius

ClassValueRole
rounded-hairlineradius-1
rounded-tightradius-2
rounded-snugradius-4
rounded-smradius-6
rounded-btnradius-8
rounded-baseradius-10
rounded-mdradius-10
rounded-lgradius-12
rounded-xlradius-16
rounded-2xlradius-20
rounded-3xlradius-24
rounded-full9999px

Elevationcomposite ring-shadow ladder

ClassValueRole
shadow-low0 1px 2px 0 var(--shadow-ring-surface), 0 2px 4px 0 var(--shadow-ring-surface), 0 0 0 1px var(--shadow-ring-edge)Resting controls, inputs
shadow-medium0 4px 4px 0 var(--shadow-ring-surface), 0 4px 80px 8px var(--shadow-ring-surface), 0 0 0 1px var(--shadow-ring-edge)Cards, raised panels
shadow-high0 4px 5px 0 var(--shadow-ring-surface), 0 8px 32px 0 var(--shadow-ring-2), 0 0 0 1px var(--shadow-ring-edge)Dialogs, floating UI
shadow-low-focus0 1px 2px 0 var(--shadow-ring-surface), 0 2px 4px 0 var(--shadow-ring-surface), 0 0 0 1px var(--shadow-ring-edge), 0 0 0 3px var(--focus-ring)Focus state on controls
shadow-medium-accent0 4px 4px 0 var(--shadow-ring-surface), 0 4px 80px 8px var(--shadow-ring-surface), 0 0 0 1px var(--shadow-edge-primary)Accented raised card
shadow-high-accent0 4px 5px 0 var(--shadow-ring-surface), 0 8px 32px 0 var(--shadow-ring-2), 0 0 0 1px var(--shadow-edge-primary)Accented floating UI
shadow-low-destructive-accent0 1px 2px 0 var(--shadow-ring-surface), 0 2px 4px 0 var(--shadow-ring-surface), 0 0 0 1px var(--shadow-edge-destructive)Error/danger elevation
shadow-low-borderless0 1px 2px 0 var(--shadow-ring-surface), 0 2px 4px 0 var(--shadow-ring-surface)Elevation without stroke
shadow-medium-borderless0 4px 4px 0 var(--shadow-ring-surface), 0 4px 80px 8px var(--shadow-ring-surface)Elevation without stroke
shadow-high-borderless0 4px 5px 0 var(--shadow-ring-surface), 0 8px 32px 0 var(--shadow-ring-2)Elevation without stroke
--shadow-focus0 0 0 3px var(--focus-ring)Focus ring piece

Breakpoints

TokenValueRole
sm390pxPhones
md810pxTablets
lg1200pxDesktop
--breakpoint-mobile480pxSecondary mobile split

Z-index bands

ClassValueRole
z-base1
z-button5
z-sticky20
z-sidebar25
z-header30
z-dialog100
z-modal100
z-drawer1000
z-dropdown1001
z-popover1001
z-tooltip1000000
z-toast10000000

Motion & blur

ClassValueRole
--duration-instant100ms
--duration-fast150ms
--duration-normal200ms
--duration-slow300ms
--duration-slower500ms
--ease-smoothcubic-bezier(0.4, 0, 0.2, 1)
--ease-hovercubic-bezier(0.44, 0.085, 0.68, 0.53)
--ease-quintcubic-bezier(0.23, 1, 0.32, 1)
--ease-too-coolcubic-bezier(0.65, 0, 0.35, 1)
--ease-toastcubic-bezier(0.27, 0, 0.07, 1.01)
--ease-bouncecubic-bezier(0.68, -0.55, 0.265, 1.55)
--ease-snapcubic-bezier(0.4, 0, 1, 1)
ClassValueRole
--blur-card0.5px
--blur-card-hover1.5px
--blur-subtle2px
--blur-xs2px
--blur-sm-plus3px
--blur-medium36px
--blur-heavy120px
--blur-120120px

All animation honors prefers-reduced-motion — enforced globally in the app stylesheet.

GEO·06 — Verified fragments

Copy the contract, not the paint.

Canonical fragments lifted verbatim from source — each block names the file it was verified against.

Tailwind v4 entrystyles/tailwind.css

@import "@repo/ui/styles/preset.css";
@import "@repo/ui/styles/themes/home.css";
@import "@repo/analytics/src/styles/analytics.css";

@source "../features/**/*.{ts,tsx,js,jsx}"; /* …scan roots… */

Compose semantic utilitiesthemes/home.css (.geolify-card)

// Never raw hex — semantic tokens adapt to both themes
<article className="rounded-12 bg-card/80 p-5 shadow-medium backdrop-blur-md sm:p-6">
  <h3 className="geolify-card-title">Coverage</h3>
  <p className="geolify-card-subtitle">Pages eligible for AI citation.</p>
</article>

4-tier hue contracttheme.css (tier blocks)

// Every hue ships base / border / muted-fill / emphasis.
// Status UI = border + muted-fill + emphasis — never hardcode greens.
<span className="rounded-full border border-success-border bg-success-muted-fill px-2.5 py-0.5 text-micro font-medium text-success-emphasis">
  Healthy
</span>

// …or let the component map the tier for you:
<Badge variant="success">Healthy</Badge>

Focus contracttheme.css (@utility focusable-element-outline)

@utility focusable-element-outline {
  outline: none;

  &:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 3px;
  }
}

/* Inputs use the composite ring-shadow instead: */
@utility shadow-low-focus {
  box-shadow:
    0 1px 2px 0 var(--shadow-ring-surface),
    0 2px 4px 0 var(--shadow-ring-surface),
    0 0 0 1px var(--shadow-ring-edge),
    0 0 0 3px var(--focus-ring);
}

Theme mechanismcomponents/providers/theme-providers.tsx

// .dark class on <html>, persisted to "theme" storage key.
// Marketing + dashboard both default to dark.
<ThemeProvider
  attribute="class"
  defaultTheme="dark"
  enableSystem
  storageKey="theme"
  storage="hybrid"
>
  {children}
</ThemeProvider>

// In CSS, dark overrides live under ".dark { … }" —
// in markup, use the dark: variant sparingly (tokens already adapt).

Chart tokens — correct & wrongtheme.css (--chart-* / --color-chart-*)

// Chart tokens are OKLCH — compose via color utilities or var().
<Bar dataKey="score" fill="var(--chart-1)" />
<div className="bg-chart-positive">…</div>

// WRONG — tokens are already a complete color function.
// Wrapping them in hsl() produces invalid CSS:
<Bar dataKey="score" fill="hsl(var(--chart-1))" /> // ✗

Custom @utility inventory

Paste them knowing they compile; what you copy is what the linter already accepts.

UtilityEffectRole
focusable-element-outlineStandard focus-visible outline contract
shadow-low-focusControl focus elevation (low + primary edge)
glass-effectTranslucent surface + backdrop blur (app layer)
dashed-grid-lineDashed hairline via border token
animate-fade-in-upEntrance — fade + rise
animate-dialog-content-in / -outDialog enter/exit
animate-marquee-x / -yMarquee loops
animate-rippleRipple pulse
animate-accordion-down / -upAccordion height animation
animate-pulse-subtleGentle opacity pulse
hover-expand-1 / -2Hover growth affordances
will-change-contents · lazy-sectionRender performance hints
h-/min-h-/max-h-*vhViewport-height steps (40–70vh)
min-h-card / -section / -proof-panel*Marketing min-height guards
max-w-panel-sm / -lg · min-w-panel-defaultPanel width constraints

GEO·07 — Container anatomy

Every card is a finding.

Marketing pages repeat one skeleton: rhythm from geolify-home-section, width from site-container, then badge → headline → lede → grid.

Section anatomythemes/home.css + components.css

The same instrument every time — a chrome head that declares the register, then a body in fixed order. Nothing reorders, restyles or skips a slot.

<section className="geolify-home-section">
  <div className="site-container">
    <header className="geolify-section-header">
      <span className="geolify-badge">…kicker…</span>
      <h2 className="geolify-h2">…headline…</h2>
      <p className="geolify-lead">…lede…</p>
    </header>
    <div className="geolify-home-grid-three-columns">
      …cards…
    </div>
  </div>
</section>
GEO Platform

Measure AI visibility

A 0–100 GEO Score paired with the fixes that move it.

geolify-badge → geolify-h2 → geolify-lead

Card anatomythemes/home.css .geolify-card

A geolify-card reads like a finding: a number, a title, and the line that explains it. Edge and elevation come with the class.

<article className="geolify-card">
  {/* rounded-12 · bg-card/80 · shadow-medium · hover:shadow-high-accent · p-5→sm:p-6 */}
  <h3 className="geolify-card-title">…</h3>
  <p className="geolify-card-subtitle">…</p>
  <p className="geolify-stat-number">51</p>
  <p className="geolify-stat-label">GEO Score</p>
</article>

51

GEO Score

Prioritized fixes

Every audit returns ordered tasks, not a report.

geolify-card — live class, no overrides

Rules

Container rules — the contract

rhythm
Section rhythm comes from geolify-home-section (py-16→20→24). Do not improvise vertical padding on marketing pages.
width
Width is site-container (1024px, px-4→6→0). Nested containers are an anti-pattern.
edge
geolify-card owns its edge (shadow-medium → high-accent on hover). Do not add borders or rings on top.

Workspace shells (.geolify-workspace-*) follow a denser register and are documented separately — do not mix the two.

GEO·08 — Readings

Charts are readings, not decoration.

Charts consume the --chart-1…5 series palette plus semantic aliases — all OKLCH, all theme-adaptive, all scoped through ChartContainer.

Series paletteper-theme OKLCH

Five series hues — the one sanctioned burst of chroma in the system, and it always means data.

--chart-1
--chart-2
--chart-3
--chart-4
--chart-5
TokenLightDarkRole
--chart-1oklch(0.646 0.222 41.116)oklch(0.488 0.243 264.376)Series 1
--chart-2oklch(0.6 0.118 184.704)oklch(0.696 0.17 162.48)Series 2
--chart-3oklch(0.398 0.07 227.392)oklch(0.769 0.188 70.08)Series 3
--chart-4oklch(0.828 0.189 84.429)oklch(0.627 0.265 303.9)Series 4
--chart-5oklch(0.769 0.188 70.08)oklch(0.645 0.246 16.439)Series 5

Semantic aliases

TokenLightDarkRole
--chart-positivesuccesssuccessPositive series
--chart-negativedestructivedestructiveNegative series
--chart-warningwarningwarningWatch series
--chart-infoinfoinfoInformational series
--chart-primaryprimaryprimaryPrimary series

Rules

Viz rules — the instrument contract

tokens
Reference tokens directly: fill="var(--chart-1)" or the fill-chart-* / bg-chart-* utilities — never a literal color.
wrap
Never wrap a token in hsl() or rgb() — chart values are already complete oklch() functions; wrapping produces invalid CSS.
status
Status-driven series use the aliases (--chart-positive/-negative/-warning/-info/-primary) so palette changes propagate.
stack
Render through ChartContainer + the Recharts stack already in the repo. Do not add a second charting library.

The categorical palette is intentionally high-chroma in dark mode — it is the single sanctioned departure from the zinc-dominant surface.

GEO·09 — Position

Visibility meets precision.

The positioning record, verbatim from the canonical brand record. The visual register follows from it: zinc-dominant, data-dense, audit-first.

Positioningcanonical brand record

Verbatim from the brand record — the words the visual system answers to.

For SEO agencies and brand owners who need to know how AI answer engines see their websites, Geolify is a GEO platform that measures AI visibility as a 0–100 GEO Score and prescribes the specific fixes to improve it. Unlike rank trackers and prompt-monitoring-only tools, Geolify pairs a technical site audit with prioritized actions.
TermValue
CategoryGEO platform (Generative Engine Optimization)
Anchor metricGEO Score — 0–100, six pillars, public methodology
MechanismAudit → Fix → Track
Primary audienceMid-size SEO agencies
Secondary audienceBrand owners / solopreneurs
DifferentiatorAudit-first methodology + open GEO Score

Tagline systemcanonical brand record

TierCopyUsage
PrimaryAudit. Fix. Track.Default across product, marketing, social bio, footer, deck cover
Sales hookWhat's your GEO Score?Conversation opener, cold-email subject, demo opener, ad headline
Phrasing rulesThree words, three periods — never commas. The hook always carries its question mark.Applies to both lines above
Retired — do not reuse“GEO Audit. Built for Agencies.” · “Your Prioritized Roadmap to Dominating AI Search.”Do not use — non-canonical variants (e.g. “…At every scale.”)

Retired lines must not reappear — “Audit. Fix. Track.” is the canonical tagline; variants like “At every scale.” are non-canonical.

The mechanismthree words, three periods

  1. 01

    Audit

    Measure how AI engines read the site. The GEO Score is the reading.

  2. 02

    Fix

    Apply the prioritized tasks — ordered work, not a report.

  3. 03

    Track

    Watch the score move, and keep it honest.

How strategy maps to the visual system

  • Audit-first → data density is the aesthetic: zinc surfaces, oklch status tiers, mono numerals.
  • Visibility meets precision → dark-first canvas, high-contrast ink, a single blue focus signal.
  • Agency-native → enterprise restraint. No gradients-for-decoration, no emoji in product UI.

GEO·10 — The voice

Say what the data shows.

The verbal system in one pass: five principles, mechanical rules, preferred and banned vocabulary, required capitalization, and per-channel tone. This page follows every rule it lists.

Voice principles

PrincipleWeightRole
Expert5/5Speak with insight, not hype. Precise terminology.
Practical5/5Every statement leads to a concrete action.
Direct4/5Get to the point. Reduce filler words.
Agency-native4/5Retainers, QBRs, billable scope.
Approachable3/5Senior consultant explaining over coffee.

Writing rules

  • Sentences target 15–20 words; cap at 25. Paragraphs 2–3 sentences; cap at 4.
  • Active voice by default; passive only when the object is the focus.
  • Numbers under 10 spelled out; 10+ use digits. Scores and metrics always digits (GEO Score: 51/100).
  • Title Case for H1/H2; sentence case H3 and below. No trailing period in headings.
  • CTAs start with a verb and are specific — “Run your first audit”, not “Get started”. One primary CTA per section.
  • Claims need evidence: date + source; rates need denominators; label Observed / Derived / Inference.
  • Never name competitors on public surfaces.

Preferred vocabulary

UseInstead of
AI visibilityAI SEO
AI search enginesAI chatbots
GEO ScoreSEO score
AI-readyAI-optimized
optimization tasksrecommendations
auditscan, check

Banned vocabulary

BannedInstead of
revolutionize / game-changingDescribe the specific benefit
cutting-edge / state-of-the-artDescribe the specific capability
leverage (verb)use, apply, with
unlock (marketing)get access to, enable
empowerhelp [user] do [thing]
seamless / frictionlessDescribe the specific UX behavior
best-in-classCite a specific differentiator
robustreliable, handles [case]
innovativeShow, do not tell
solutions (standalone)Name the actual product or feature
AI-powered (no context)Describe what the AI does
100% / completelyCite the measured rate
absolutely / definitelyDrop the word

Representative set — the canonical voice spec holds the full table, including the Vietnamese banned list.

Required capitalization

TermRuleWrong
GeolifyCapital G, no “the”✗ the Geolify platform
GEOAll caps✗ geo, Geo
GEO ScoreTwo words, both capitalized✗ geo score, GeoScore
GEO AuditTwo words, both capitalized✗ geo audit
Action CenterTwo words, both capitalized✗ action center
Prompt MonitorTwo words, both capitalized✗ prompt monitor
AI engines“engines” lowercase✗ AI Engines

Tone by channel

ChannelToneFormality · CTA
Marketing pagesExpert + ApproachableMedium · One clear CTA per section
In-app copyDirect + PracticalLow · Action verb
Error messagesDirect + ApproachableLow · Recovery action
DocumentationExpert + DirectMedium-high · N/A
Blog (Geodocs)Expert + ApproachableMedium · Contextual, at the end

Do / don't

Write it like this

  • “GEO Score: 51/100.” — digits for metrics, always.
  • “Run your first audit.” — verb-first CTA, one per section.
  • “Observed across 30 days of tracked prompts.” — every claim carries date and scope.
  • “Optimization tasks,” not “recommendations” — the vocabulary is the product.

Never like this

  • “Our revolutionary, AI-powered platform…” — hype is banned vocabulary.
  • “Get started.” — vague; name the action.
  • “100% visibility guaranteed.” — never an unmeasured rate.
  • “AI SEO made seamless.” — wrong term plus a banned word.

GEO·11 — Hold the standard

Ship it as Geolify, or don't ship it.

The tokens are the contract; the voice is the record. If a page, deck or instrument carries the Geolify name it follows both — if it needs an exception, the exception is the bug.

Paste-ready contract for AI agents. Same data as the human view, rendered as a skill file — the two cannot drift apart.

---
name: geolify-design-system
description: Geolify (@repo/ui + Tailwind v4) design contract — semantic tokens, dark-first themes, 4-tier hue matrix, marketing register. Use when building or styling any Geolify surface.
keywords: [geolify, design-system, tailwind-v4, oklch, semantic-tokens, dark-mode, radix, cva]
---

# Geolify Design System

Token source of truth: `packages/ui/styles/theme.css` (@theme + @layer theme); generated
per-scope artifact: `packages/ui/spec/tokens.json` (regen: node scripts/design-system/sync-tokens.mjs).
Marketing register: `packages/ui/styles/themes/home.css` (`.geolify-*`).
Components: `@repo/ui` (Radix primitives + CVA variants). Icons: lucide-react via @repo/ui.
Site: geolify.ai — dark is the default theme on every surface; `.dark` class on <html>,
storage key `theme`. Every documented value exists in repo source — never invent.

## COLOR
--background — light: #ffffff · dark: #000000
--card — light: #ffffff · dark: #141414
--popover — light: #ffffff · dark: #171717
--muted — light: var(--color-zinc-100) · dark: var(--color-zinc-800)
--secondary — light: var(--color-zinc-100) · dark: var(--color-zinc-800)
--secondary-soft — light: var(--color-zinc-50) · dark: #141414
--accent — light: var(--color-zinc-100) · dark: var(--color-zinc-800)
--section-muted — light: var(--color-zinc-50) · dark: #141414
--section-accent — light: var(--color-zinc-100) · dark: var(--color-zinc-800)
--foreground — light: var(--color-zinc-950) · dark: #ffffff
--card-foreground — light: var(--color-zinc-950) · dark: #ffffff
--popover-foreground — light: var(--color-zinc-950) · dark: #ffffff
--muted-foreground — light: var(--color-zinc-600) · dark: var(--color-zinc-400)
--secondary-foreground — light: var(--color-zinc-900) · dark: #ffffff
--accent-foreground — light: var(--color-zinc-900) · dark: #ffffff
--primary — light: var(--color-zinc-900) · dark: #ffffff
--primary-foreground — light: #ffffff · dark: var(--color-zinc-950)
--primary-soft — light: var(--color-zinc-200) · dark: var(--color-zinc-800)
--primary-subtle — light: var(--color-zinc-100) · dark: #141414
--primary-outline — light: var(--color-zinc-300) · dark: rgb(255 255 255 / 0.14)
--secondary-strong — light: var(--color-zinc-700) · dark: var(--color-zinc-300)
--focus-ring — light: var(--info) · dark: var(--info)
--border — light: var(--color-zinc-200) · dark: rgb(255 255 255 / 0.08)
--input — light: var(--color-zinc-200) · dark: rgb(255 255 255 / 0.08)
--ring — light: var(--color-zinc-400) · dark: var(--color-zinc-500)
--success — light: oklch(0.696 0.17 162.48) · dark: oklch(0.696 0.17 162.48)
--warning — light: oklch(0.769 0.188 70.08) · dark: oklch(0.769 0.188 70.08)
--info — light: oklch(0.623 0.214 259.815) · dark: oklch(0.623 0.214 259.815)
--destructive — light: oklch(0.577 0.245 27.325) · dark: oklch(0.704 0.191 22.216)
--destructive-foreground — light: white · dark: white
--control-bg — light: #ffffff · dark: #222225
--control-hover — light: #fafafa · dark: #2e2e33
--control-selected — light: #f1f1f1 · dark: #333338
--control-primary — light: #18181a · dark: #ffffff
--control-primary-hover — light: #2c2c2e · dark: #fafafa
--shadow-ring-surface — light: rgba(0, 0, 0, 0.05) · dark: rgba(255, 255, 255, 0.03)
--shadow-ring-2 — light: rgba(0, 0, 0, 0.08) · dark: rgba(255, 255, 255, 0.06)
--shadow-ring-edge — light: rgba(0, 0, 0, 0.05) · dark: rgba(255, 255, 255, 0.08)

Zinc ramp (--color-zinc-50…950): #fafafa #f4f4f5 #e4e4e7 #d4d4d8 #a1a1aa #71717a #52525b #3f3f46 #27272a #18181b #09090b
Secondary ramps: bunker (cool), pampas (warm) — reserved, zinc is primary.

4-tier hue matrix: hues = neutral, success, warning, destructive, info, purple, orange, cyan
Roles = base / border / muted-fill / emphasis → tokens --<hue>-<role> and utilities
bg-<hue>-muted-fill, border-<hue>-border, text-<hue>-emphasis.
Derivation (color-mix oklab): see --success-border=color-mix(in oklab, var(--success) 20%, var(--border)) · --success-muted-fill=color-mix(in oklab, var(--success) 10%, var(--background)) · --success-emphasis=color-mix(in oklab, var(--success) 60%, var(--foreground)) (light; dark overrides in theme.css).

## TYPE
  text-label-xs (9px, lh 1.33, w400)
  text-label-sm (10px, lh 1.4, w400)
  text-label (11px, lh 1.45, w400)
  text-micro (12px, lh 1.2, w400)
  text-caption (13px, lh 1.38, w400)
  text-body-sm (14px, lh 1.43, ls -0.175px, w500)
  text-body (16px, lh 1.5, w400)
  text-headline (22px, lh 1.28, ls -0.25px, w600)
  text-title-mini (18px, lh 1.28, ls -0.25px, w500)
  text-title-small (24px, lh 1.33, ls -0.3px, w600)
  text-title-regular (28px, lh 1.14, ls -0.25px, w600)
  text-display (56px, lh 1.1, ls -0.02em, w700)
  text-button-sm (13px, lh 1.38, w500)
  text-button-md (14px, lh 1.43, ls -0.175px, w500)
  text-table-header (11px, lh 1.45, ls 0.05em, w600)
Marketing classes: geolify-h1, geolify-h2, geolify-lead, geolify-body, geolify-detail, geolify-quote, geolify-attribution, geolify-caption, geolify-meta, geolify-card-title, geolify-card-subtitle, geolify-stat-number, geolify-stat-label
Fonts: Inter (400/700) — var(--font-inter), Inter-Regular.woff2, Inter-Bold.woff2
  JetBrains Mono (400) — var(--font-mono), JetBrainsMono-Regular.woff2

## SPACING / LAYOUT
Scale: 0 px 0.5 1 1.5 2 2.5 3 3.5 4 5 6 7 8 9 10 11 12 14 16 20 24 28 32 36 40 44 48 52 56 60 64 72 80 96 (+ hairline, container-max)
--container-max-width: 1024px
--spacing-container-max: 1024px
--card-padding-desktop: var(--spacing-card-padding)
--card-padding-mobile: var(--spacing-card-padding-mobile)
--card-gap: 0.75rem
--spacing-card-padding: 1.5rem
--spacing-card-padding-mobile: 1rem
--spacing-card-gap: 0.75rem
--spacing-table-row: 2.25rem
--spacing-table-header-row: 2rem
--spacing-table-cell-x: 0.75rem
--spacing-table-cell-y: 0.5rem
--spacing-toolbar: 2.5rem
--spacing-control-xs: 1.75rem
--spacing-control-sm: 2rem
--spacing-control-md: 2.25rem
--spacing-control: 2.5rem
--spacing-control-lg: 2.75rem
--spacing-hero-card: min(100%, 420px)
--spacing-safe-top: max(env(safe-area-inset-top), 0.5rem)
--spacing-safe-bottom: max(env(safe-area-inset-bottom), 1rem)
Radius: hairline=var(--radius-1) · tight=var(--radius-2) · snug=var(--radius-4) · sm=var(--radius-6) · btn=var(--radius-8) · base=var(--radius-10) · md=var(--radius-10) · lg=var(--radius-12) · xl=var(--radius-16) · 2xl=var(--radius-20) · 3xl=var(--radius-24) · full=9999px
Breakpoints: sm 390px · md 810px · lg 1200px (container 1024px)

## ELEVATION / MOTION
Shadow ladder: shadow-low → medium → high; accents -accent / -destructive-accent;
borderless variants; --shadow-focus = 0 0 0 3px var(--focus-ring).
Z bands: base=1 button=5 sticky=20 sidebar=25 header=30 dialog=100 modal=100 drawer=1000 dropdown=1001 popover=1001 tooltip=1000000 toast=10000000
Motion: duration-instant=100ms · duration-fast=150ms · duration-normal=200ms · duration-slow=300ms · duration-slower=500ms · ease-smooth=cubic-bezier(0.4, 0, 0.2, 1) · ease-hover=cubic-bezier(0.44, 0.085, 0.68, 0.53) · ease-quint=cubic-bezier(0.23, 1, 0.32, 1) · ease-too-cool=cubic-bezier(0.65, 0, 0.35, 1) · ease-toast=cubic-bezier(0.27, 0, 0.07, 1.01) · ease-bounce=cubic-bezier(0.68, -0.55, 0.265, 1.55) · ease-snap=cubic-bezier(0.4, 0, 1, 1)

## COMPONENTS (@repo/ui only)
Button variants: default destructive outline outline-soft destructive-outline success secondary secondary-outline ghost secondary-chip ghost-muted ghost-destructive link inverted —
sizes: default xs sm md compact lg icon icon-sm icon-lg; focusRing: default strong.
Heights via size prop only.
Badge variants map to hue tiers: default secondary destructive outline success warning orange purple primary-soft;
sizes: default xs sm lg; weights: default normal semibold.
ScoreBadge: score → success(≥90) default(≥80) warning(≥70) orange(≥60) destructive(<60), null → "—".
Input: bg-control-bg shadow-low → focus-visible:shadow-low-focus; sizes: default sm.
Edge contract: Card/SurfaceCard/Dialog/Input own border+ring+shadow — call sites may pass
layout classes only (enforced by design-system-eslint no-restyle / no-edge-stomp).
Bans in app code: raw palette utilities (bg-emerald-500…), arbitrary values,
inline styles, dynamic className construction.

## DATAVIZ
--chart-1: oklch(0.646 0.222 41.116) (light) / oklch(0.488 0.243 264.376) (dark)
--chart-2: oklch(0.6 0.118 184.704) (light) / oklch(0.696 0.17 162.48) (dark)
--chart-3: oklch(0.398 0.07 227.392) (light) / oklch(0.769 0.188 70.08) (dark)
--chart-4: oklch(0.828 0.189 84.429) (light) / oklch(0.627 0.265 303.9) (dark)
--chart-5: oklch(0.769 0.188 70.08) (light) / oklch(0.645 0.246 16.439) (dark)
Aliases: --chart-positive, --chart-negative, --chart-warning, --chart-info, --chart-primary — use for status-driven charts.
ChartContainer wraps Recharts; never wrap a token in hsl()/rgb() — values are complete oklch.

## BRAND
Primary: Audit. Fix. Track.
Sales hook: What's your GEO Score?
Phrasing rules: Three words, three periods — never commas. The hook always carries its question mark.
Retired — do not reuse: “GEO Audit. Built for Agencies.” · “Your Prioritized Roadmap to Dominating AI Search.”
Vocabulary: AI visibility (not "AI SEO"); AI search engines (not "AI chatbots"); GEO Score (not "SEO score"); AI-ready (not "AI-optimized"); optimization tasks (not "recommendations"); audit (not "scan, check")
Banned: revolutionize / game-changing; cutting-edge / state-of-the-art; leverage (verb); unlock (marketing); empower; seamless / frictionless; best-in-class; robust; innovative; solutions (standalone); AI-powered (no context); 100% / completely; absolutely / definitely
Rules: Sentences target 15–20 words; cap at 25. Paragraphs 2–3 sentences; cap at 4. Active voice by default; passive only when the object is the focus. Numbers under 10 spelled out; 10+ use digits. Scores and metrics always digits (GEO Score: 51/100). Title Case for H1/H2; sentence case H3 and below. No trailing period in headings. CTAs start with a verb and are specific — “Run your first audit”, not “Get started”. One primary CTA per section. Claims need evidence: date + source; rates need denominators; label Observed / Derived / Inference. Never name competitors on public surfaces.

## APPLY
- Marketing page → features/<name>/ + .geolify-* classes from themes/home.css.
- Product UI → @repo/ui primitives; variant/size props, semantic utility classes.
- New token → packages/ui/styles/theme.css; marketing recipe → themes/home.css.
- Preferences app register (.geolify-workspace-*) is out of scope here.
- Verify: pnpm --filter @repo/geolify-app lint && type-check && test.