Exceptions are the interface
Screens answer "is anything wrong?" first. Nothing to do is a designed, confident state, not an empty one. Show changes, not levels.
Light theme · web and mobile · v1
One Link runs a grain and fertilizer business: trades and mill coverage, weighbridge intake, stock, and farmer input finance, with a portal for counterparties and a field app for drivers. Every screen leads with what needs attention, and every figure is derived, never typed.
Cards, tickets and figure buttons are white on white, held apart by very soft shadows. Grey marks only what is secondary or switched off.
Text and hairlines are navy-black at set opacities, so one token reads right on white and on grey. Each has a solid twin for use over photos.
Components use semantic tokens only. Those point at 11-step palettes, so a palette change never touches a component.
Blue is for links and entry points. Inside a commodity, its colour drives the share bars, chart lines and the trade ticket. Decisions stay primary dark.
Anything that commits sits on a 4px edge and sinks into it. Buttons scale to 0.97, motion settles on a spring, and every working button shows it is working.
Hover the card for the commodity wash. Press a figure to sink it. Switch commodity in the rail.
These come from how the business works, not from taste. Every component and screen obeys them.
Screens answer "is anything wrong?" first. Nothing to do is a designed, confident state, not an empty one. Show changes, not levels.
Balances, stock, exposure and margin are computed, never typed. A headline figure reveals the subtraction behind it in one interaction.
How a weight was captured and whether the counterparty agrees are two separate chips, with two separate icons.
A figure withheld from a role is genuinely missing. It shows as a quaternary dash with eye-off, and never as K0, blank, loading or blurred.
Within tolerance books. Held for review goes to the owner. A hard block, like tare above gross, never books and is shown calmly, not as an error flood.
Records are append-only. A correction is a compensating entry with a reason, an actor and a time, beside the original.
A silent weighbridge feed makes the position look flat, so sync health is always shown, and silence is unmistakable. Offline is a state with a queue.
The label fades and scales to 0.8 while a spinner takes its place. The button keeps its size and stays busy until the action lands.
Status is always an icon and a word, never colour alone. Icons are Lucide only: no emoji, no Unicode symbols standing in for icons. Charts are TradingView Lightweight Charts.
One link with two loops, in One Link blue. In the product it is always the blue mark on a white surface. The white-on-blue tile exists only for the two places that must be a filled square: the app icon and the browser tab.
The mark: blue on white. Header, sidebar, collapsed rail, documents.
White on blue: only on a brand-blue surface, never on a commodity colour.
The tile: app icon and browser tab only, where a filled square is required.
The mark is about 1.6 times wider than it is tall. Where there is room, which is everywhere inside the product, it sits on its own in One Link blue: the cleanest form of the brand. A phone's home screen and a browser tab are small, square and filled, and a wide mark alone in a square shrinks to a thin line. The tile gives it a square of its own, so it reads at 16 pixels and on any wallpaper.
UAG's business is connection. It stands between Zambia's farmers and its mills, and between grain that arrives by sea and the mills that need it. Every tonne UAG handles is a link between someone who grows or ships it and someone who needs it.
The mark is two links joined into one. One side is the farmer, the other the mill. Where they meet, the overlap forms a single grain kernel. The grain sits at the exact point where the two join, because that is what UAG does: it sits in the middle and makes the connection work, one load at a time. It is also what One Link does, turning many spreadsheets, phone calls and workbooks into one record that everyone reads.
The shape is deliberately simple. It has to be recognisable on a farmer's phone screen in bright sun, on a clerk's screen at the weighbridge, and as a small icon in the app store. It uses One Link's own blue, and every commodity keeps its own colour inside the product, so the logo never competes with the wheat, maize, soya and fertiliser it serves.
The mark's one animation: the mark, there at once, with a light running round its loops while the app loads. It shows on every full page load and fades once the app is ready. With reduced motion it is the still. The full rules are on the Splash card.
Animated: one-link-splash.svg. Full page loads only, never a move inside the app.
Reduced motion: one-link-splash-still.svg, the same frame with no light.
Two layers. Components use only the semantic tokens; the "Points to" column shows the palette step each one resolves to. Hover mixes 5% black into light fills and 10% white into filled buttons.
White does the lifting. Grey is kept for what recedes: search, unselected capsules, disabled fills, closed records.
| Token | Value | Points to |
|---|---|---|
--surfacePage ground |
#ffffff |
gray-50-solid |
--elevatedCards, figure buttons, quantity chips, tickets |
#ffffff |
gray-50-solid |
--groupedSecondary tiles, skeletons, row hover |
#f4f5f7 |
gray-100-solid |
--grouped-lightSearch, unselected capsules, disabled fills |
#00153f0b |
gray-100 |
--grouped-elevatedGrey tile inside a card |
#f4f5f7 |
gray-100-solid |
--invertedInverted chips, chart crosshair labels |
#23272d |
gray-900-solid |
--overlayDialog and sheet scrim |
#00000066 |
literal |
Text and icons are navy-black at set opacities, so one token reads right on white and on grey.
| Token | Value | Points to |
|---|---|---|
--content-primaryTitles, figures, main text |
Aa#00050cdc |
gray-900 |
--content-secondaryMeta lines, units, secondary text |
Aa#000c1c94 |
gray-700 |
--content-tertiaryInactive tabs, captions. Never a figure |
Aa#000f2174 |
gray-600 |
--content-quaternaryChevrons, placeholders, the restricted dash |
Aa#00102a40 |
gray-400 |
--content-disabledDisabled labels |
Aa#00152e54 |
gray-500 |
--content-accent-brandLinks, active stepper value |
Aa#2563EB |
brand-500 |
--content-accent-upPositive delta text |
Aa#007e26 |
green-600 |
--content-accent-downNegative delta text |
Aa#df0c10 |
red-500 |
--accent-up-fillUp triangle, in-bars, live dot. Fills only, never text |
Aa#04af52 |
green-400 |
The same greys as solid colours, for text laid over a photo, where translucency would pick up the image.
| Token | Value | Points to |
|---|---|---|
--content-primary-solidPrimary text over photos |
Aa#23272d |
gray-900-solid |
--content-secondary-solidSecondary text over photos |
Aa#6b727b |
gray-700-solid |
--content-tertiary-solidCaptions over photos |
Aa#8b929b |
gray-600-solid |
--content-quaternary-solidChevrons over photos |
Aa#bfc3ca |
gray-400-solid |
Subtle is the pale fill, strong is the text on it. Status is always an icon and a word, never colour alone.
| Token | Value | Points to |
|---|---|---|
--info-subtleInfo banner fill |
#f7fbfe |
brand-50 |
--info-strongInfo banner text |
Aa#0135aa |
brand-700 |
--success-subtleClean, within spec, confirmed |
#e4ffe9 |
green-50 |
--success-strongText on success |
Aa#006118 |
green-700 |
--warning-subtleAttention, amber band, held for review |
#fefce8 |
yellow-50 |
--warning-strongText on warning. #a16207 rather than #ca8a04, for AA |
Aa#a16207 |
yellow-700 |
--error-subtleBreach, red band, hard block, sync silent |
#fff6f2 |
red-50 |
--error-strongText on error |
Aa#bc0000 |
red-600 |
Hairlines, the button fills, and the BETA badge.
| Token | Value | Points to |
|---|---|---|
--borderCard outlines, dividers, selected capsule border |
#00123719 |
gray-200 |
--border-light3D edge, table row dividers |
#0000000f |
literal |
--border-elevatedOutlines on elevated layers |
#00123719 |
gray-200 |
--buttons-primaryDecisions: Approve, Finalise, Resolve case |
#23272d |
gray-900-solid |
--buttons-brandEntry points: New trade, Raise GRN |
#2563EB |
brand-500 |
--buttons-criticalDecline, Void batch |
#df0c10 |
red-500 |
--buttons-labelLabels on filled buttons |
#ffffff |
gray-50-solid |
--hover-overlay-darkerSubtle button fill, keyboard hint |
#0000000d |
literal |
--beta-fillBETA badge |
#2563EB |
brand-500 |
--beta-labelBETA badge label |
#ffffff |
gray-50-solid |
Eleven steps each. The brand ramp keeps the reference's lightness per step, with One Link blue at 500.
The deep shade carries a white label at AA or better. Underlines are 2px, washes are 9% in oklab, tiles 16%.
Two families. Inter for everything you read. Instrument Sans condensed, always uppercase, for headline figures, commodity tabs and detail titles. Specs are size/line height · weight · tracking.
A touch heavier than 400, which keeps small translucent text crisp on white. Strong body is 580, strong headings 600.
body-3 is the workhorse for tables, meta lines and rows. Headings are rare; condensed figures do the shouting.
Money, tonnes, percentages, refs and timestamps all use tabular numerals. Kwacha millions take M: K125M, K174,840.
A radius scale with one odd step, 14px, used only on 48px buttons. On white, shadows do the separating that grey fills would do elsewhere.
--radius-5xs2pxsmall badges--radius-4xs4pxkeyboard hint, skeleton text, severity tag--radius-3xs6px--radius-2xs8pxquantity chips, selected segment--radius-xs10px32px buttons, stepper--radius-sm12px40px buttons, figure buttons, search, segmented track--radius-sm-h14px48px buttons--radius-md16pxhover rows, tiles, banners, popovers--radius-lg20pxcards, tickets, chart cards, dialogs--radius-xl24px--radius-2xl32px--radius-max999pxpills, capsules, avatarsSix exact shadow stacks.
--shadow-flatBorder only, no lift--shadow-minimalStepper, selected segment, carousel arrows--shadow-minimal-softCards, tickets, decision panels, chart cards--shadow-middleQuantity chips--shadow-strong48px actions, menus, popovers--shadow-dialogDialogs, bottom sheetsSprings are CSS linear() curves, so there is no animation library. Every animation has a reduced-motion fallback that keeps colour changes and drops scale, translate and ping.
--ease-springTab underline, label and spinner swaps, toggles · overshoots to 1.015--ease-expandAccordions, bottom sheets · no overshoot--ease-cardCard commodity wash · 400ms in, 160ms out--dur-defaultcolour + hover--dur-press-down3D press--dur-press-up--dur-capsuleweight swap--dur-underlinetab underline glide (spring)--dur-swaplabel / spinner swap (spring)--dur-card-in--dur-card-out--dur-sheetbottom sheet (expand)--dur-skeleton--dur-photo--dur-ping--dur-spin--dur-marcha marching track, one full wrap--dur-splash-outthe splash fading away once the app is ready--dur-splash-stuckthe splash says it is still loading--scale-press-button--scale-press-cardThese are the real components from project/components, running live. Every one is interactive; the commodity switch in the rail recolours the ones that take a commodity.
Brand for entry points, primary for decisions, critical for destructive, commodity only on the trade ticket. Every button that starts work has the loading state.
A face on a 4px edge. It sinks 4px in 50ms and returns in 100ms. Filled edges are the fill under 15% black.
Selected capsules go white with a 1px ring and a 3px halo; the label weight settles over 180ms. Tab underlines glide on the spring.
Status marks are an icon and a word. Provenance and confirmation are separate chips. Sync health is green while live and red, without a ping, when silent.
List items carry share bars in the commodity colour, quantity chips and figure buttons. Figures open their derivation.
Dense, tabular, with ref links, a restricted cell, a derived cell and a correction row. Tables scroll inside their own container on phones.
TradingView Lightweight Charts, styled to the system: dotted grid, magnet crosshair, end-of-line labels, dashed policy lines. The TradingView logo stays visible.
The main action panel: side toggle, commodity tabs, amount, quick amounts and a full-width commit in the commodity colour.
28px commodity tile, context in body-3 secondary, subject in body-1 strong.
Two-up 48px press buttons. The chosen side fills with the commodity colour.
Label on the left, condensed figure on the right with a quaternary unit, quick amounts beneath.
48px large press button with the strong shadow. It keeps its size and spins until the trade lands.
The goods-received reconcile in its three outcomes, evidence tiles, and an append-only timeline with a compensating entry.
Interactive UI kits built only from these components, with fictional sample data. The desktop app is drawn at 1440; the mobile app, partner portal and field app at 390.
A centred 1293px shell. List pages have a content column and a sticky "Needs you" rail; detail pages swap the rail for the 350px action panel. Phones get one column and a bottom tab bar.
Every specimen card from the Claude Design export, each a standalone page. They are the source these pages are built from.