Design system¶
The rules the marketing site (ndakita_group_site) is built on, and why each
one is the way it is. Both hostnames share this system: a visitor crossing from
ndakita.com to build.ndakita.com should not be able to tell.
Tokens live in src/app/globals.css. Components live in src/components/ui/.
Colour¶
| Token | Value | Job |
|---|---|---|
--foreground |
#0f0f0f |
text, filled buttons |
--background |
#ffffff |
page |
--secondary |
#f7f7f7 |
alternating section band, raised cards |
--muted |
#6b6b6b |
secondary text, placeholders |
--border |
#e8e8e8 |
decorative only: card edges, dividers, section rules |
--border-strong |
#8a8a8a |
borders that are a control: inputs, selects, checkbox, secondary button, segmented toggles |
--accent |
#404040 |
hover partner for --foreground, never used alone |
--brand |
#143154 |
navy. The ground on dark surfaces, the fill on light ones |
--brand-ink |
#2a5c9c |
hover partner for --brand. Goes lighter, see below |
--mark |
#C9992A |
gold. The hero mark and the footer rule, nothing else |
--success |
#047857 |
money in, positive state |
--danger |
#b91c1c |
form errors |
Both brand values are read off the logo files in public/, not chosen. That is
why the site and the mark are the same two colours rather than a near miss.
The border split¶
--border at 1.23:1 on white is a hairline, which is correct for decoration
and fails WCAG 1.4.11 for anything that bounds a control. --border-strong is
3.45:1. If the border is the only thing telling you where a control begins, it
must be the strong one.
The brand rule¶
Navy is a ground before it is an accent. Gold is a mark and nothing else.
Navy carries white type at 13.15:1 and stands against the white page at the same 13.15:1, so it can be a surface, a fill, a boundary and body text without help. Gold is the opposite kind of colour: 5.06:1 on navy and 7.37:1 under the dark ink that sits on it, but only 2.6:1 against white, so it can be a block behind a word and must never be a word, a control edge, or the only thing carrying a meaning.
Two rules follow from single numbers, and both are load-bearing:
- Navy and
--foregroundmust never touch. 1.46:1 between them. Small black controls are fine because they sit on white; nothing black may sit on a navy ground and nothing navy on a black one. Every full-width dark band on the site is navy, so no two darks ever meet. - Gold must never carry a meter or a state. At hue 42 it sits 10 degrees from the amber that means "low stock", at only 1.26x its lightness. It works because the two never co-occur: gold is the hero and the footer, the amber lives inside the product animation.
--brand-ink goes lighter than --brand, which is the one
counter-intuitive value here. Navy is already near the bottom of the range, so
a deeper navy lands at 1.26:1 against the rest state, the same invisible hover
--accent was created to fix. The lighter partner is 1.95:1 apart.
What this replaced, and why it is worth knowing¶
The site ran on hi-vis yellow (#facc15) until 2026-08-01, and the switch is
recorded because the failure mode is instructive. Yellow is 12.5:1 on black and
1.5:1 on white; navy is 13.15:1 on white and 1.46:1 on black. Both are
one-ground colours, just opposite grounds — and the site's grounds are
mostly white.
So yellow needed a prop everywhere it went: a dark keyline under the button, because a 1.5:1 fill cannot be what identifies a control under 1.4.11; an inverted meter track, because yellow on a light grey measured 1.4:1; a "decorative only" caveat against the section rules and the nav underline. Navy needs none of them, and they were all deleted with it.
Four other candidates were measured and rejected: steel blue and indigo work
but are the colour every ERP competitor already uses; safety orange works but
sits 11 degrees from the low-stock amber; murram clay fails the dark hero at
2.93:1 and reads as an error at 16 degrees from --danger. The branches are
still in the repo (theme/*) if any of that needs revisiting.
Type¶
Archivo carries h1 and h2; Inter carries body, UI and every number.
Both are self-hosted through next/font, which is what lets the privacy policy
say no request leaves for Google.
The rule is display face at 30px and up. A few h2 elements are 14px
section labels; those opt back out with font-sans at the call site.
| Step | Size | Weight | Tracking | Leading | For |
|---|---|---|---|---|---|
| Display | text-5xl sm:text-6xl md:text-7xl |
700 | -0.02em |
1.05 |
the one hero h1 on / |
| H1 | text-4xl md:text-5xl |
700 | -0.015em |
1.1 |
page titles |
| H2 | text-3xl md:text-4xl |
700 | -0.01em |
1.15 |
section headings, tool-page h1 |
| H3 | text-2xl md:text-3xl |
600 | normal | snug | legal sections, utility pages |
| H4 | text-lg |
600 | normal | snug | roomy card titles |
| H5 | text-base |
600 | normal | normal | dense card titles |
Archivo needs less negative tracking than Inter, because its sidebearings
are tighter. tracking-tight (-0.025em) collides above ~96px. Each step
carries its own value instead, and less of it the larger it gets. The only
tracking-tight left is the wordmark, which is Inter.
Money and any figure a reader compares down a column uses tabular-nums.
Radius¶
Three values. rounded-md (6px) for dense controls, rounded-lg (8px) for
anything with a body, rounded-full for pills. rounded-xl survives on
exactly one element, the product animation's window chrome, which genuinely
draws a larger corner.
Rhythm¶
| Role | Padding |
|---|---|
| Page head | pt-32 pb-16 |
| Section | py-24 |
| Band (dark CTA, footer) | py-16 |
pt-32 clears the fixed header and matches scroll-padding-top: 6rem.
Container is max-w-7xl mx-auto px-6; prose columns (legal, pricing) are the
documented exception. The heading block is max-w-3xl mb-16 and reads heading,
then lede. It used to open with an uppercase eyebrow label above the heading;
those were dropped across all eighteen call sites and the component with them,
so a section now leads with what it is about. Sections alternate --background
and --secondary strictly: two adjacent sections sharing a background fuse
into one slab, and a navy hairline now separates them as well.
Components¶
Button/ButtonLink— variantsprimary,secondary,inverse,inverseGhost,ghost; sizessm,md,lg;fullWidth.smusesmin-hrather than padding so it reaches the 44px target inside a table row without growing the row.fullWidthexists because call sites passingw-fullplus their own padding silently overrode the size.Card/CardGrid— the one card treatment: a hairline grid,gap-pxover a border-coloured ground. Chosen because it makes unequal card heights impossible rather than merely tolerable, costs one border-width of noise instead of one per card, and degrades at a single column into horizontal rules. Bordered rounded boxes remain only for panels, which are single containers rather than repeated items.
Brand assets¶
The mark is two figures, one navy and one gold, sharing an overlap. Ndakita
Build's variant adds rising bars cut into that overlap, and those bars are the
only thing distinguishing the two brands' marks. Four files in public/, and
they are not interchangeable — each is drawn for a specific ground:
| File | Colours | Belongs on |
|---|---|---|
ndakita-navbar.svg |
navy + gold, white overlap | the white company header |
build-navbar.svg |
the same, plus navy bars in the overlap | the white Build header |
ndakita-footer.svg |
white + gold, navy overlap | the navy footer, and the company share card |
build-footer.svg |
the same, plus white bars in the overlap | the Build share card |
Putting either navbar file on the navy footer draws navy figures on navy at 1:1. Putting a footer file on the white header does the same with white. The bars invert with everything else: navy cut into a white overlap on the header, white cut into a navy one on the card. Recolouring the figures but not the bars is the failure that is easy to miss, because it costs only the two strokes that make the mark Build's.
public/ndakita-mark.png is the 1152px transparent raster master. Nothing
renders it; it exists for anywhere a vector will not do.
The header shows the mark beside the wordmark, not instead of it. The mark
reads as a symbol rather than a name, and on a header nobody has seen before
the name is the part that has to survive. Both sit inside one link with the
image aria-hidden, so the brand is announced once.
Favicons are per host, via the route segment. src/app/icon.svg is the
company mark and src/app/build/icon.svg the Build one; Next resolves the
icon convention per segment, so /build/* — which is every page on
build.ndakita.com after the middleware rewrite — emits the Build icon and
ndakita.com keeps the company one. Confirm a change by grepping rel="icon"
out of .next/server/app/build.html and .next/server/app/index.html after a
build. The href is /build/icon.svg, which carries an extension and so is
excluded from the middleware matcher: it is served literally on the Build host
rather than 308'd back to /icon.svg.
Both carry their own navy ground, unlike the transparent master. A transparent favicon takes whatever the browser or OS puts behind it, and on a dark tab strip the navy figure sits at 1.46:1 against near-black with half the logo gone. They use the footer treatment for the same reason: navy figures on a navy ground are nothing.
Share cards¶
Two opengraph-image.tsx routes, one per host: the company card at the app
root, Ndakita Build's under /build. Both are the hero treatment rather than a
logo panel — navy ground, the gold mark on the same word — so a link shared
into a WhatsApp thread looks like the page it opens.
Three things about them are worth knowing before editing:
-
The logo is inlined as a base64 data URI. Satori has no loader that takes a path, so each route reads its own footer variant and embeds it: the company card
public/ndakita-footer.svg, the Build cardpublic/build-footer.svg. Neither may read a navbar file — those are drawn for a white ground and lose a whole figure on navy. The Build card carried the company mark untilbuild-footer.svgexisted, which meant the two cards were identical apart from their words. -
A page that exports an
openGraphobject drops the inherited file-convention image. Next replaces the resolved object wholesale instead of merging, so the rootopengraph-imageonly ever reached the home page, the one page that sets noopenGraphof its own. Every other page unfurled as a bare link for weeks without any warning.socialMeta()inlib/metadata.tsnow names the image explicitly, deriving it from the page's own absolute URL so each host gets its own card without the call site having to know which. Any new page must go throughsocialMeta(). - Satori is the layout engine inside
next/og, not a font choice. It renders the card to a PNG on the server, where there is no browser and no CSS pipeline, sonext/font's self-hosted faces are unreachable and it falls back to a built-in one-weight Noto Sans. That is why the headlines rendered regular however the weight was declared.assets/now carries the latin subsets of Archivo Bold and Inter Regular, 48 KB for both, read withreadFile(join(process.cwd(), …))and passed toImageResponse. Any new card must load them the same way or it silently drops back to Noto.
Where each colour lands¶
The brand rule in §Colour is the constraint; this is where the two colours actually go.
Navy, --brand¶
| Surface | Treatment | Measured |
|---|---|---|
Hero, footer, /build CTA band |
The ground itself | white h1 13.15, white/70 lede 7.24 |
| Share cards | The ground, white type | 13.15 |
| Primary button | Navy fill, white ink, no keyline, hover lifts to --brand-ink |
13.15 label, 13.15 boundary |
| Between top-level sections | 1px rule, main > section + section |
13.15 |
| Card hover | 3px left edge, transparent at rest | decorative |
| Current nav item | Underline (desktop) or left edge (mobile) | decorative |
| Meters | Navy fill on a light track | 11.64 |
| Assessment progress bar | Navy on a --secondary track |
12.27 |
Gold, --mark¶
| Surface | Treatment | Measured |
|---|---|---|
| The word "Kenyan" in the hero | Block behind the word, dark ink on it | 5.06 on navy, 7.37 under the ink |
| Share cards | The same block, same treatment | 5.06 / 7.37 |
| The mark itself | One of the two figures, in every logo file | — |
Gold marks one word and one figure, and nothing else. It is not available for buttons, states, meters or borders. It briefly carried the footer's top rule, straight and then following the curve; the curve reads as an edge on its own and the line only competed with it.
--mark therefore resolves to a single call site in CSS. That looks like
overkill for a token until you try folding it into --brand: they are
opposite kinds of colour — one a ground white sits on, one a fill dark sits on
— and the logo needs both.
The alpha levels a navy ground forces¶
Navy is lighter than the #0f0f0f it replaced, so white-alpha text that
cleared 4.5:1 on black does not automatically clear it here. white/50 lands
at 4.48 and fails; the hero kicker moved to white/60 at 5.77. white/70
body and white/60 legal both pass. Check any new alpha against navy rather
than assuming.
Accessibility floor¶
Every interactive element has a focus-visible ring; there is a skip link and
an id="main-content" on every <main>; <main> never wraps the header or
footer. Anything that moves for more than five seconds has a pause control
(WCAG 2.2.2), which prefers-reduced-motion does not satisfy because it is
a different success criterion. Text links never get lighter on hover: on a
near-black foreground that reduces contrast, so hovers change background or
underline instead.