51
GEO Score
Prioritized fixes
Every audit returns ordered tasks, not a report.
GEO·00 — Design system audit
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.
Index — 010 entries
GEO·01 — Color field
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.
Every surface is a variable. Toggle the theme and the meaning flips — never the markup. Name a role and the page re-themes itself.
--backgroundL #ffffffD #000000
Page canvas
--cardL #ffffffD #141414
Elevated panels, cards
--popoverL #ffffffD #171717
Floating layers — menus, tooltips
--mutedL zinc-100D zinc-800
Secondary fills, inactive tabs
--secondaryL zinc-100D zinc-800
Auxiliary actions
--secondary-softL zinc-50D #141414
Softest secondary fill
--accentL zinc-100D zinc-800
Highlighted rows
--section-mutedL zinc-50D #141414
Muted section ground
--section-accentL zinc-100D zinc-800
Accented section ground
--foregroundL zinc-950D #ffffff
Default text
--card-foregroundL zinc-950D #ffffff
Text on cards
--popover-foregroundL zinc-950D #ffffff
Text on popovers
--muted-foregroundL zinc-600D zinc-400
Secondary copy, captions
--secondary-foregroundL zinc-900D #ffffff
Text on secondary
--accent-foregroundL zinc-900D #ffffff
Text on accent
--primaryL zinc-900D #ffffff
Key CTAs, active states
--primary-foregroundL #ffffffD zinc-950
Text on primary
--primary-softL zinc-200D zinc-800
Quiet primary fill
--primary-subtleL zinc-100D #141414
Subtlest primary ground
--primary-outlineL zinc-300D rgb(255 255 255 / 0.14)
Primary outline
--secondary-strongL zinc-700D zinc-300
Strong secondary ink
--focus-ringL infoD info
Focus-visible ring — never remove
--borderL zinc-200D rgb(255 255 255 / 0.08)
Dividers, card edges, inputs
--inputL zinc-200D rgb(255 255 255 / 0.08)
Input strokes
--ringL zinc-400D zinc-500
Focus ring outline
--successL oklch(0.696 0.17 162.48)D oklch(0.696 0.17 162.48)
Positive trends, healthy readings
--warningL oklch(0.769 0.188 70.08)D oklch(0.769 0.188 70.08)
Caution, pending state
--infoL oklch(0.623 0.214 259.815)D oklch(0.623 0.214 259.815)
Informational callouts
--destructiveL oklch(0.577 0.245 27.325)D oklch(0.704 0.191 22.216)
Errors, destructive actions
--destructive-foregroundL whiteD white
Text on destructive
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-emphasisDerivation 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.
Zinc does the heavy lifting on every screen. Two quieter ramps — bunker, pampas — sit in reserve for surfaces that need a different temperature.
#fafafa#f4f4f5#e4e4e7#d4d4d8#a1a1aa#71717a#52525b#3f3f46#27272a#18181b#09090b| Token | Value | Role |
|---|---|---|
--color-bunker-* | oklch 0.98→0.12 | Cool dark neutrals |
--color-pampas-* | oklch 0.98→0.20 | Warm neutrals |
Chrome gets its own scale: the sidebar, the control fill ladder, and the ring pieces that make elevation legible at 1px.
| Token | Light | Dark | Role | |
|---|---|---|---|---|
--sidebar | #ffffff | #000000 | Sidebar ground | |
--sidebar-foreground | zinc-900 | zinc-100 | Sidebar text | |
--sidebar-primary | zinc-950 | #ffffff | Active sidebar item | |
--sidebar-primary-foreground | #ffffff | #000000 | Text on active item | |
--sidebar-accent | zinc-100 | #141414 | Hovered sidebar item | |
--sidebar-accent-foreground | zinc-900 | #ffffff | Text on hover | |
--sidebar-border | zinc-200 | rgb(255 255 255 / 0.08) | Sidebar dividers | |
--sidebar-ring | zinc-400 | zinc-500 | Sidebar focus ring |
| Token | Light | Dark | Role | |
|---|---|---|---|---|
--control-bg | #ffffff | #222225 | Control resting surface | |
--control-hover | #fafafa | #2e2e33 | Control hover surface | |
--control-selected | #f1f1f1 | #333338 | Control selected surface | |
--control-primary | #18181a | #ffffff | Primary control fill | |
--control-primary-hover | #2c2c2e | #fafafa | Primary control hover |
| Class | Value | Role |
|---|---|---|
--shadow-ring-surface | L rgba(0, 0, 0, 0.05) · D rgba(255, 255, 255, 0.03) | Ambient ring |
--shadow-ring-2 | L rgba(0, 0, 0, 0.08) · D rgba(255, 255, 255, 0.06) | Deeper ring |
--shadow-ring-edge | L rgba(0, 0, 0, 0.05) · D rgba(255, 255, 255, 0.08) | 1px edge ring |
The only saturated hues in the system. They encode series and status — never mood.
| Token | Light | Dark | Role | |
|---|---|---|---|---|
--chart-1 | oklch(0.646 0.222 41.116) | oklch(0.488 0.243 264.376) | Series 1 | |
--chart-2 | oklch(0.6 0.118 184.704) | oklch(0.696 0.17 162.48) | Series 2 | |
--chart-3 | oklch(0.398 0.07 227.392) | oklch(0.769 0.188 70.08) | Series 3 | |
--chart-4 | oklch(0.828 0.189 84.429) | oklch(0.627 0.265 303.9) | Series 4 | |
--chart-5 | oklch(0.769 0.188 70.08) | oklch(0.645 0.246 16.439) | Series 5 |
| Token | Light | Dark | Role | |
|---|---|---|---|---|
--chart-positive | success | success | Positive series | |
--chart-negative | destructive | destructive | Negative series | |
--chart-warning | warning | warning | Watch series | |
--chart-info | info | info | Informational series | |
--chart-primary | primary | primary | Primary series |
Third-party marks keep their own hex. It is the one place raw color is allowed.
| Token | Value | Role |
|---|---|---|
--color-brand-linkedin | #0a66c2 | |
--color-brand-twitter | #1da1f2 | X / Twitter |
--color-brand-facebook | #1877f2 |
GEO·02 — Type register
Inter carries prose and UI; JetBrains Mono carries every reading. Composite text tokens bind size, leading, tracking and weight into a single step.
Inter does the talking, JetBrains Mono does the counting. Two weights of one and one of the other, vendored — nothing arrives from a CDN.
| Family | Value | Role |
|---|---|---|
Inter · var(--font-inter) | Inter-Regular.woff2 w400 + Inter-Bold.woff2 w700 | UI, headings, body |
JetBrains Mono · var(--font-mono) | JetBrainsMono-Regular.woff2 w400 | Code, metrics, technical data |
Audit. Fix. Track. — Inter 400/700
GEO Score: 51/100 — JetBrains Mono 400
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
| Class | Value | Role |
|---|---|---|
text-label-xs | 9px · lh 1.33 · ls — · w400 | Dense micro labels (alias: text-9px) |
text-label-sm | 10px · lh 1.4 · ls — · w400 | Dense micro labels (alias: text-10px) |
text-label | 11px · lh 1.45 · ls — · w400 | Dense micro labels (alias: text-11px) |
text-micro | 12px · lh 1.2 · ls — · w400 | Badges, fine print |
text-caption | 13px · lh 1.38 · ls — · w400 | Captions, metadata |
text-body-sm | 14px · lh 1.43 · ls -0.175px · w500 | Compact UI text |
text-body | 16px · lh 1.5 · ls — · w400 | Default reading size |
text-headline | 22px · lh 1.28 · ls -0.25px · w600 | Section headlines |
text-title-mini | 18px · lh 1.28 · ls -0.25px · w500 | Small titles |
text-title-small | 24px · lh 1.33 · ls -0.3px · w600 | Card titles |
text-title-regular | 28px · lh 1.14 · ls -0.25px · w600 | Page titles |
text-display | 56px · lh 1.1 · ls -0.02em · w700 | Hero display |
text-button-sm | 13px · lh 1.38 · ls — · w500 | Button sm |
text-button-md | 14px · lh 1.43 · ls -0.175px · w500 | Button md |
text-table-header | 11px · lh 1.45 · ls 0.05em · w600 | Table headers |
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
The score is the contract
A 0–100 GEO Score paired with the fixes that move it.
Every audit returns ordered optimization tasks — the work that changes the reading, not a report to file away.
“The audit found what the dashboard was missing.”
51
GEO Score · out of 100
GEO Score · Pillar breakdown
| Class | Value | Role |
|---|---|---|
geolify-h1 | 40→48→56px · lh 1.14–1.17 · medium · ls -0.03em→-0.04em | Page hero headline |
geolify-h2 | 36→40→48px · lh 1.17–1.2 · medium · ls -0.03em | Section headline |
geolify-lead | 15→18px · lh 22→28px · regular · ls -0.01em · muted | Section lede under h2 |
geolify-body | 14→16px · lh 20→24px · muted | Marketing body copy |
geolify-detail | 12px · lh 1.2 · muted-70 | Fine print, footnotes |
geolify-quote | 14→16px · lh 20→24px · italic · muted | Pull quotes, testimonials |
geolify-attribution | text-body-sm · foreground | Quote attribution |
geolify-caption | 12px · lh 1.2 · ls -0.15px · muted-70 | Captions, stat labels |
geolify-meta | 12→14px · lh 1.2→1.43 · ls -0.15→-0.175px · muted | Dates, categories, meta |
geolify-card-title | 18→20px · lh 1.33→1.2 · semibold · ls -0.045→-0.25px | Card headline |
geolify-card-subtitle | 14→16px · lh 20→24px · muted | Card subline |
geolify-stat-number | 32px · lh 1.25 · medium · ls -0.72px | Hero stat figure |
geolify-stat-label | 12→14px · lh 1.2→1.43 · muted | Hero 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
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.
Every control below is the shipped primitive, rendered live — this guide composes the same contracts it documents.
| Component | Variants / sizes | Role |
|---|---|---|
Button | 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 | All clickable actions; icon slots via lucide children |
Toggle · ToggleGroup | — | Binary state controls |
| Component | Variants / sizes | Role |
|---|---|---|
Badge | default · secondary · destructive · outline · success · warning · orange · purple · primary-soft — sizes: default · xs · sm · lg · weights: default · normal · semibold | Status chips — every variant maps to the hue tier matrix |
ScoreBadge | score 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 |
| Component | Variants / sizes | Role |
|---|---|---|
Input | size: default · sm — control-bg surface, shadow-low → shadow-low-focus | Text 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 |
| Component | Variants / sizes | Role |
|---|---|---|
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 |
| Component | Variants / sizes | Role |
|---|---|---|
Table · PinnedTable · TableSort | — | Dense data tables |
ChartContainer | Recharts wrapper | Chart scoping + theming |
Avatar · UserAvatar | — | Identity |
Separator · ScrollArea · AspectRatio | — | Structure helpers |
| Component | Variants / sizes | Role |
|---|---|---|
Layout · Section · Container · Stack | — | Page primitives |
Sidebar | — | App shell navigation |
NavigationMenu · Breadcrumb · Tabs · Accordion · Collapsible | — | Wayfinding |
Card · SurfaceCard | edge contract — no border/ring/shadow restyle | Contained surfaces |
| Component | Variants / sizes | Role |
|---|---|---|
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 |
Control rules — the contract
GEO·04 — Mark & ground
Every asset below ships in the app's public directory. Two systems: app-shell SVGs the theme inverts, and plated lockups that carry their own ground.
Bare artwork is canonical — the ground does the work; the app shell flips black art to white with dark:invert. Plated lockups are self-contained tiles for avatars and app icons only.
/images/logo-full.svg/images/logo-full.svg/images/logo-symbol.svg/images/brand/geolify-symbol.svg/images/brand/geolify-logo-dark.svg/images/brand/geolify-logo-light.svgNaming: in the plated set, the suffix describes the plate color — geolify-logo-dark is white art on a black plate (use on light surfaces); geolify-logo-light is black art on white (use on dark surfaces). Never place a plated tile on a matching ground and never repaint the mark.
| File | Kind / ground | Role |
|---|---|---|
/images/logo-full.svg | Full lockup, black artwork — Light surfaces — app shell applies dark:invert | Canonical header logo (921×254 viewBox) |
/images/logo-symbol.svg | Symbol, black artwork — Light surfaces — app shell applies dark:invert | Compact/header mark (206×200 viewBox) |
/images/brand/geolify-logo-dark.svg | Full lockup on black plate — Light surfaces | Self-contained tile — white art on #000 plate |
/images/brand/geolify-logo-light.svg | Full lockup on white plate — Dark surfaces | Self-contained tile — black art on #fff plate |
/images/brand/geolify-symbol.svg | Symbol, white artwork — Dark surfaces only | Transparent ground — needs dark behind |
/images/logo-icon.svg | Square icon tile — Either — black plate + white symbol | App-icon style square (512 viewBox) |
/images/logo-email.png | Email lockup, 460×127 PNG — Light (email clients) | Raster for email only |
/logo.png | Full lockup, 1068×512 PNG — Light surfaces | Raster fallback of the SVG lockup |
/icon.svg · /favicon.ico · /apple-icon.png | App icons — Browser chrome | 512 SVG icon, ICO fallback, 180px touch icon |
/favicon-dark.svg · /favicon-light.svg | Theme-aware favicons — Auto via prefers-color-scheme | Paired in root layout metadata |
/og-image.jpg | Default social card, 1200×630 — Social embeds | Fallback OG; routes ship generated cards |
GEO·05 — Grid & measure
One spacing ladder drives padding, gap and rhythm. Layout primitives own the 1024px measure, card density and control heights — nothing else may set them.
One ladder spaces everything — card padding, section rhythm and the container measure all resolve to the same scale.
00pxpx1px0.50.125rem10.25rem1.50.375rem20.5rem2.50.625rem30.75rem3.50.875rem41rem51.25rem61.5rem71.75rem82rem92.25rem102.5rem112.75rem123rem143.5rem164rem205rem246rem287rem328rem369rem4010rem4411rem4812rem5213rem5614rem6015rem6416rem7218rem8020rem9624rem| Class | Value | Role |
|---|---|---|
--container-max-width | 1024px | Page container (site-container) |
--spacing-container-max | 1024px | Same value, spacing namespace |
--card-padding-desktop | spacing-card-padding | Card inner padding ≥sm |
--card-padding-mobile | spacing-card-padding-mobile | Card inner padding mobile |
--card-gap | 0.75rem | Card internal gap |
--spacing-card-padding | 1.5rem | Card padding (canonical) |
--spacing-card-padding-mobile | 1rem | Card padding mobile |
--spacing-card-gap | 0.75rem | Card internal gap |
--spacing-table-row | 2.25rem | Table row height (36px) |
--spacing-table-header-row | 2rem | Table header row height |
--spacing-table-cell-x | 0.75rem | Table cell inline padding |
--spacing-table-cell-y | 0.5rem | Table cell block padding |
--spacing-toolbar | 2.5rem | Toolbar bar height |
--spacing-control-xs | 1.75rem | Extra-small control height |
--spacing-control-sm | 2rem | Compact control height |
--spacing-control-md | 2.25rem | Medium control height |
--spacing-control | 2.5rem | Default control height |
--spacing-control-lg | 2.75rem | Large control height |
--spacing-hero-card | min(100%, 420px) | Hero card width cap |
--spacing-safe-top | max(env(safe-area-inset-top), 0.5rem) | Notch-safe top |
--spacing-safe-bottom | max(env(safe-area-inset-bottom), 1rem) | Notch-safe bottom |
| Class | Value | Role |
|---|---|---|
site-container | max-width 1024px · px-4 / sm:px-6 / lg:px-0 | Marketing page container (components.css) |
geolify-home-section | py-16 / sm:py-20 / lg:py-24 | Vertical rhythm for a marketing section |
geolify-section-header | mb-8 / sm:mb-10 / lg:mb-12 | Space under headline+lede block |
geolify-grid-gap | gap-4 / sm:gap-6 | Default card-grid gutter |
geolify-home-grid-three-columns | grid · gap-6→8 · md:2col · lg:3col | Three-up feature grid |
geolify-home-grid-pricing-columns | grid · gap-4 · md:2col · lg:4col | Pricing tier grid |
geolify-section-subtitle-spacing | mt-6 / sm:mt-7 | Space before a section subtitle row |
geolify-section-subtitle-spacing-wide | mt-7 / sm:mt-8 | Wider subtitle spacing |
| Class | Value | Role |
|---|---|---|
rounded-hairline | radius-1 | |
rounded-tight | radius-2 | |
rounded-snug | radius-4 | |
rounded-sm | radius-6 | |
rounded-btn | radius-8 | |
rounded-base | radius-10 | |
rounded-md | radius-10 | |
rounded-lg | radius-12 | |
rounded-xl | radius-16 | |
rounded-2xl | radius-20 | |
rounded-3xl | radius-24 | |
rounded-full | 9999px |
| Class | Value | Role |
|---|---|---|
shadow-low | 0 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-medium | 0 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-high | 0 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-focus | 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) | Focus state on controls |
shadow-medium-accent | 0 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-accent | 0 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-accent | 0 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-borderless | 0 1px 2px 0 var(--shadow-ring-surface), 0 2px 4px 0 var(--shadow-ring-surface) | Elevation without stroke |
shadow-medium-borderless | 0 4px 4px 0 var(--shadow-ring-surface), 0 4px 80px 8px var(--shadow-ring-surface) | Elevation without stroke |
shadow-high-borderless | 0 4px 5px 0 var(--shadow-ring-surface), 0 8px 32px 0 var(--shadow-ring-2) | Elevation without stroke |
--shadow-focus | 0 0 0 3px var(--focus-ring) | Focus ring piece |
| Token | Value | Role |
|---|---|---|
sm | 390px | Phones |
md | 810px | Tablets |
lg | 1200px | Desktop |
--breakpoint-mobile | 480px | Secondary mobile split |
| Class | Value | Role |
|---|---|---|
z-base | 1 | |
z-button | 5 | |
z-sticky | 20 | |
z-sidebar | 25 | |
z-header | 30 | |
z-dialog | 100 | |
z-modal | 100 | |
z-drawer | 1000 | |
z-dropdown | 1001 | |
z-popover | 1001 | |
z-tooltip | 1000000 | |
z-toast | 10000000 |
| Class | Value | Role |
|---|---|---|
--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) |
| Class | Value | Role |
|---|---|---|
--blur-card | 0.5px | |
--blur-card-hover | 1.5px | |
--blur-subtle | 2px | |
--blur-xs | 2px | |
--blur-sm-plus | 3px | |
--blur-medium | 36px | |
--blur-heavy | 120px | |
--blur-120 | 120px |
All animation honors prefers-reduced-motion — enforced globally in the app stylesheet.
GEO·06 — Verified fragments
Canonical fragments lifted verbatim from source — each block names the file it was verified against.
@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… */// 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>// 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>@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);
}// .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 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))" /> // ✗Paste them knowing they compile; what you copy is what the linter already accepts.
| Utility | Effect | Role |
|---|---|---|
focusable-element-outline | Standard focus-visible outline contract | |
shadow-low-focus | Control focus elevation (low + primary edge) | |
glass-effect | Translucent surface + backdrop blur (app layer) | |
dashed-grid-line | Dashed hairline via border token | |
animate-fade-in-up | Entrance — fade + rise | |
animate-dialog-content-in / -out | Dialog enter/exit | |
animate-marquee-x / -y | Marquee loops | |
animate-ripple | Ripple pulse | |
animate-accordion-down / -up | Accordion height animation | |
animate-pulse-subtle | Gentle opacity pulse | |
hover-expand-1 / -2 | Hover growth affordances | |
will-change-contents · lazy-section | Render performance hints | |
h-/min-h-/max-h-*vh | Viewport-height steps (40–70vh) | |
min-h-card / -section / -proof-panel* | Marketing min-height guards | |
max-w-panel-sm / -lg · min-w-panel-default | Panel width constraints |
GEO·07 — Container anatomy
Marketing pages repeat one skeleton: rhythm from geolify-home-section, width from site-container, then badge → headline → lede → grid.
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>A 0–100 GEO Score paired with the fixes that move it.
geolify-badge → geolify-h2 → geolify-lead
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
Every audit returns ordered tasks, not a report.
geolify-card — live class, no overrides
Container rules — the contract
Workspace shells (.geolify-workspace-*) follow a denser register and are documented separately — do not mix the two.
GEO·08 — Readings
Charts consume the --chart-1…5 series palette plus semantic aliases — all OKLCH, all theme-adaptive, all scoped through ChartContainer.
Five series hues — the one sanctioned burst of chroma in the system, and it always means data.
| Token | Light | Dark | Role | |
|---|---|---|---|---|
--chart-1 | oklch(0.646 0.222 41.116) | oklch(0.488 0.243 264.376) | Series 1 | |
--chart-2 | oklch(0.6 0.118 184.704) | oklch(0.696 0.17 162.48) | Series 2 | |
--chart-3 | oklch(0.398 0.07 227.392) | oklch(0.769 0.188 70.08) | Series 3 | |
--chart-4 | oklch(0.828 0.189 84.429) | oklch(0.627 0.265 303.9) | Series 4 | |
--chart-5 | oklch(0.769 0.188 70.08) | oklch(0.645 0.246 16.439) | Series 5 |
| Token | Light | Dark | Role | |
|---|---|---|---|---|
--chart-positive | success | success | Positive series | |
--chart-negative | destructive | destructive | Negative series | |
--chart-warning | warning | warning | Watch series | |
--chart-info | info | info | Informational series | |
--chart-primary | primary | primary | Primary series |
Viz rules — the instrument contract
The categorical palette is intentionally high-chroma in dark mode — it is the single sanctioned departure from the zinc-dominant surface.
GEO·09 — Position
The positioning record, verbatim from the canonical brand record. The visual register follows from it: zinc-dominant, data-dense, audit-first.
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.
| Term | Value |
|---|---|
Category | GEO platform (Generative Engine Optimization) |
Anchor metric | GEO Score — 0–100, six pillars, public methodology |
Mechanism | Audit → Fix → Track |
Primary audience | Mid-size SEO agencies |
Secondary audience | Brand owners / solopreneurs |
Differentiator | Audit-first methodology + open GEO Score |
| Tier | Copy | Usage |
|---|---|---|
Primary | Audit. Fix. Track. | Default across product, marketing, social bio, footer, deck cover |
Sales hook | What's your GEO Score? | Conversation opener, cold-email subject, demo opener, ad headline |
Phrasing rules | Three 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.
Audit
Measure how AI engines read the site. The GEO Score is the reading.
Fix
Apply the prioritized tasks — ordered work, not a report.
Track
Watch the score move, and keep it honest.
GEO·10 — The voice
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.
| Principle | Weight | Role |
|---|---|---|
Expert | 5/5 | Speak with insight, not hype. Precise terminology. |
Practical | 5/5 | Every statement leads to a concrete action. |
Direct | 4/5 | Get to the point. Reduce filler words. |
Agency-native | 4/5 | Retainers, QBRs, billable scope. |
Approachable | 3/5 | Senior consultant explaining over coffee. |
| Use | Instead of |
|---|---|
AI visibility | AI SEO |
AI search engines | AI chatbots |
GEO Score | SEO score |
AI-ready | AI-optimized |
optimization tasks | recommendations |
audit | scan, check |
| Banned | Instead of |
|---|---|
revolutionize / game-changing | Describe the specific benefit |
cutting-edge / state-of-the-art | Describe the specific capability |
leverage (verb) | use, apply, with |
unlock (marketing) | get access to, enable |
empower | help [user] do [thing] |
seamless / frictionless | Describe the specific UX behavior |
best-in-class | Cite a specific differentiator |
robust | reliable, handles [case] |
innovative | Show, do not tell |
solutions (standalone) | Name the actual product or feature |
AI-powered (no context) | Describe what the AI does |
100% / completely | Cite the measured rate |
absolutely / definitely | Drop the word |
Representative set — the canonical voice spec holds the full table, including the Vietnamese banned list.
| Term | Rule | Wrong |
|---|---|---|
Geolify | Capital G, no “the” | ✗ the Geolify platform |
GEO | All caps | ✗ geo, Geo |
GEO Score | Two words, both capitalized | ✗ geo score, GeoScore |
GEO Audit | Two words, both capitalized | ✗ geo audit |
Action Center | Two words, both capitalized | ✗ action center |
Prompt Monitor | Two words, both capitalized | ✗ prompt monitor |
AI engines | “engines” lowercase | ✗ AI Engines |
| Channel | Tone | Formality · CTA |
|---|---|---|
Marketing pages | Expert + Approachable | Medium · One clear CTA per section |
In-app copy | Direct + Practical | Low · Action verb |
Error messages | Direct + Approachable | Low · Recovery action |
Documentation | Expert + Direct | Medium-high · N/A |
Blog (Geodocs) | Expert + Approachable | Medium · Contextual, at the end |
Write it like this
Never like this
GEO·11 — Hold the standard
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.