Skip to main contentClaim $5 in free credit — one-time, per account. Claim $5 free
Krova CloudKrova Cloud
Design system

One ramp, two ladders, one anchor, two curves.

Not a picture of the system — the system itself. Every size, leading, tracking, optical size, radius, duration, easing and colour below is read out of the stylesheet painting this page, in your theme, at your viewport. Resize the window or flip the theme and the numbers move. The mark is the one thing not in that stylesheet, so it is decoded out of the file the product ships instead — and the questions a measurement cannot settle are left standing as questions.

Nothing on this page is typed in. If a value here is wrong, the product is wrong.

01Type

The ramp

Base 16px, ratio about 1.25, nine rungs — matching GitHub Primer through the UI range, which is the closest analogue to a dense console for developers. Six rungs never move; three step at two widths, in :root, so markup never carries a breakpoint.

The ramp — nine rungs

size / leading · ratio · tracking · weight

Ship a Cube in 40 seconds
text-label-sm— / 500badges, chips, uppercase eyebrows — the floor
Ship a Cube in 40 seconds
text-body-sm— / 400table cells, dense rows, metadata, captions
Ship a Cube in 40 seconds
text-body— / 400the reading default — paragraphs, descriptions
Ship a Cube in 40 seconds
text-title— / 600card titles
Ship a Cube in 40 seconds
text-heading— / 600section headings, stat figures
Ship a Cube in 40 seconds
text-heading-lg— / 600the console page h1
Ship a Cube in 40 seconds
text-display-sm— / 600marketing section h2 — steps
Ship a Cube in 40 seconds
text-display— / 700marketing page h1 — steps
Ship a Cube in 40 seconds
text-display-lg— / 700the home hero, and nothing else — steps

Size, leading and the ratio between them are read back off the specimen above each line; the tracking is the rung’s own --letter-spacing, lifted from the stylesheet. Weight is the one column that is a convention rather than a token — design.md keeps weight at the call site on purpose, so there is nothing to measure and saying otherwise would be the lie this page exists to prevent. Base is 16px.

Where the display rungs step

reading the stylesheet…

Measuring from the live stylesheet.

02Type

The tracking ladder and the optical-size map

Two properties that have to follow the rendered size rather than the token name, and therefore cannot be set at a call site. Both are read here from the same declarations the browser is using.

The tracking ladder

--text-*--letter-spacing, plus the two caps recipes

text-label-sm
text-body-sm
text-body
text-title
text-heading
text-heading-lg
text-display-sm
text-display
text-display-lg

Uppercase has no ascender/descender rhythm to bind it, so it opens the other way. These two are the only sanctioned positive values in the system — a per-call-site tracking is the defect, the named recipe is the fix.

Built for developers--tracking-caps ·
Built for developers--tracking-caps-wide ·

Optical sizing follows rendered size

Google Sans Flex · opsz 6–144, default 18

Hamburgefonstiv
rendered
font-variation-settings
normal — opsz auto
font-weight
window
base

The base window carries no override at all, on purpose: auto resolves the axis to the rendered pixel size, which is the right answer at phone sizes. Pushing a display cut — tight sidebearings, fine strokes — onto 24px text is exactly where it costs the most legibility. The font-weight row is the proof of the other half of the rule: the declaration names opsz and nothing else, so weight stays free to drive its own axis. Naming a second axis there would clobber every weight on the rung.

Basis for every conversion above: 16px root font size. Tracking shown elsewhere on this page is in em, which at px means of real space per glyph pair.

03Colour

Colour, measured

A neutral ramp tinted toward the brand rather than an achromatic grey, a three-step surface hierarchy, and one soft-chip recipe whose alpha is capped by WCAG rather than by taste. Every ratio below is measured off the painted element, through a canvas, in the theme you are in.

The neutral ramp is not grey

0 hues across the family

base-100base-200base-300
tokendeclaredLCH
--color-base-100
--color-base-200
--color-base-300
--color-neutral
--color-secondary
--color-base-content

These neutrals resolve to 0 distinct hues. The system wants exactly one — a second hue is the “two different greys” defect returning.

The ink is the exception and is deliberately achromatic (C ): a tinted ramp under a tinted ink would double the cast. And the three surfaces are a real elevation ladder, not a card = background rule — a dense console needs the steps.

Brand roles

AA body text is 4.5:1 — every ratio re-measures on theme change

primary

the brand. L 0.485 is the lowest lightness that clears AA in all three renderings.

Aacontent on fill
Aaliteral /10 — 42 call sites predate the chip token
Aaon the surface
secondary

a surface role. Never painted as text.

Aacontent on fill
accent

same value as primary; only ever a fill.

Aacontent on fill
neutral

a surface role. Never painted as text.

Aacontent on fill

Only the renderings the product actually ships are measured. text-secondary would score 1.06:1 and light up a red failure — and nothing in the codebase writes it. Inventing a failing combination nobody uses costs a reader an investigation, which is worse than a gap.

Status roles & the chip recipe

--chip-fill — · --chip-border —

info
Aacontent on fill
Aaon the chip
Aaon the surface
success
Aacontent on fill
Aaon the chip
Aaon the surface
warning
Aacontent on fill
Aaon the chip
Aaon the surface
error
Aacontent on fill
Aaon the chip
Aaon the surface

The soft chip is a tint of the role under text of the same role, so every extra percent of fill costs contrast — the alpha is capped by WCAG, not by taste. It is therefore per theme: the values in the header of this panel are read from whichever theme you are in right now, and they differ. Switch the theme and watch them move.

RunningProvisioningFailedQueued

Tinted fill plus a full border in the tone — never a left-accent bar. A straight rule down one side cannot follow a corner radius, so it renders as a detached stub floating past the top and bottom of the panel it belongs to. That is a hard prohibition, checked by a test that walks every file in the app.

04Shape & motion

Radius and motion

One literal corner value in the whole system and a calc ladder off it; two curves and three durations for everything that moves. Both families are small on purpose — they are the two that drift fastest when they are written down instead of derived.

Radius is one anchor and a calc ladder

one anchor

rounded-field

--radius-field ·
controls — buttons, inputs, chips

rounded-box

--radius-box ·
containers — cards, panels, dialogs

rounded-selector

--radius-selector ·
checkboxes, radios, toggles

tokenauthoredresolves to
--radius
--radius-sm
--radius-md
--radius-lg
--radius-xl
--radius-2xl
--radius-3xl
--radius-4xl
--radius-selector
--radius-field
--radius-box

--radius is the only literal corner value in the system. Everything else is a calc() off it, which is why the “authored” column above is mostly expressions rather than numbers — and why one edit moves every corner in the product instead of four hundred call sites moving independently.

Motion

two easings · three durations

--ease-enter

Decelerating — for anything arriving.

--ease-exit

Accelerating — its mirror, for anything leaving.

Each track below animates on the token named beside it — no number is passed in, the browser resolves the custom property.

--transition-duration-micro
--transition-duration-short
--transition-duration-medium

Three durations and two curves is the whole motion system. A page that documents 150 / 250 / 400 and then ships .3s and .2s in its own stylesheet has not documented anything — which is precisely why the figures above are read out of the running CSS on every render rather than typed here.

05The mark

The logo, the lockup and the icon set

The one part of the system that is not in the stylesheet. Everything below is read out of the shipped binaries instead — decoded into a canvas and counted — or measured off the rendered lockup. Where a question can only be answered by a decision rather than a measurement, it is left visibly open.

One file, both themes

/logo.png

Krova Cloud

krova · base-100

Krova Cloud

krova · base-200

Krova Cloud

krova-dark · base-100

Krova Cloud

krova-dark · base-200

intrinsic

transparent

of the file’s own canvas

gradient, measured

The seams between the four lobes are knockouts, not white paint — they are transparent, and they take whatever surface is behind them. That is the whole reason one file serves both themes: on krova the seams read as the light surface, on krova-dark they read as the dark one, and the teal never changes. The four panels above are the same /logo.png, rendered four times, with nothing applied to it.

The lockup — one treatment, seven call sites

measured live

Krova CloudKrova Cloud

mark box

square, overflow-hidden

mark painted

letterboxed by object-contain

gap

box edge to wordmark

wordmark

surfacecall site
Landing headerapp/(landing)/layout.tsx
Sign-inapp/(auth)/layout.tsx
Onboardingapp/(onboarding)/layout.tsx
Footercomponents/landing/landing-footer.tsx
Mobile drawercomponents/landing/mobile-nav.tsx
Console sidebarcomponents/app-shell.tsx · unwrapped
Console mobile barcomponents/app-shell.tsx · markOnly

Seven call sites, one component — components/brand-lockup.tsx, which resolves the artwork through the one LOGO_PATH constant in config/platform.ts. It exposes no size prop, so a call site cannot ask for a size of its own — which is the whole mechanism. It applies no filter, mask, tint or dark: variant either: the mark is drawn as authored on every surface in the product.

This was measured, not chosen. The console sidebar was made canonical and its rendering read out of a real browser — the numbers above are that reading, live, in whatever theme and viewport you are in. Before it, the seven sites shipped four icon sizes (48, 44, 36 and the sidebar’s 32), two type rungs and two weights, and app-shell.tsx disagreed with itself: its own mobile header drew a different lockup from its own sidebar.

Three things differ, deliberately, and none of them is a size. The console sidebar renders the unwrapped variant — the same mark and the same wordmark as bare siblings, because the collapsed rail hides every child after the first (components/ui/sidebar.tsx) and a wrapper would defeat that. The console’s mobile top bar renders markOnly: at 390 that row has 136.2px left for the brand and the lockup needs 156.6, so the wordmark gives way rather than the mark shrinking — the same rule the collapsed rail follows, and the drawer one tap away still carries the wordmark. And email cannot run React at all, so lib/email/format.ts turns the same constant into an absolute URL and the templates size it themselves — at 43 × 28 beside an identically-set wordmark (20/28, weight 700, tracking −0.2px), which is the one place the mark is drawn at a size the app does not use.

The icon set, and the space it carries

decoded live

1:1filedeclaredsaysdecodedmarginplate
/favicon.icoapp/layout.tsx · icons.icon + icons.shortcutany
/icon-16.pngapp/layout.tsx · icons.icon16×16
/icon-32.pngapp/layout.tsx · icons.icon32×32
/apple-touch-icon.pngapp/layout.tsx · icons.apple180×180
/icon-192.pngapp/layout.tsx · icons.icon · app/manifest.ts192×192
/icon-512.pngapp/manifest.ts512×512
/favicon.pngnothing references it

The app icon is a different asset from the logo.

0 of the 7 files above decode to a solid plate under the mark. The wide /logo.png carries none — it is largely transparent and adapts to its surface, while a home-screen or browser-tab icon has no surface to adapt to and has to supply one. The two are not interchangeable, and the icon set is light-only by construction.

Clear space is baked into the square icons and nowhere else.

Every square icon reserves a transparent margin on all four sides. The measured amount is a few percent of the shorter side — a range, not one ratio, and proportionally largest on the smallest files. The wide logo reserves none at all: its opaque pixels run to the edge of its own canvas, so whatever room it gets is room the layout gave it, and the tightest the product currently gives it is the lockup gap in the table above.

What is true of the mark today

each line traceable to a file

  1. One component, seven call sites, and the email header.

    The landing, sign-in and onboarding layouts, the footer, the mobile drawer and both of the console shell's lockups all render one component, components/brand-lockup.tsx, which resolves the artwork through one exported constant, LOGO_PATH in config/platform.ts. lib/email/format.ts turns that same constant into the absolute URL every email header uses, because an inbox cannot run React. There is no second path to the artwork and, in the app, no second treatment of it.

  2. The PNG is the mark. The SVG beside it is wired to nothing.

    public/logo.svg and public/favicon.png both ship, and neither is referenced by a single line of application code. If you are placing the Krova mark somewhere, take /logo.png — that is the file the product itself draws, and the table above is that file decoded.

  3. It is never recoloured, and there is no dark variant.

    No call site applies a filter, a mask, a tint or a dark-variant class. The mark carries its own gradient, and its transparency — measured above — is what adapts it to a surface. That is why one file covers both themes instead of a light copy and a dark copy that drift.

  4. It cannot be stretched, or resized at a call site.

    The one mark box is square and sets object-contain, and the artwork is 1.548 : 1, so it is letterboxed rather than stretched — the painted mark measures shorter than its box, which the panel above shows live. A call site cannot change that: brand-lockup.tsx exposes no size prop, so the box, the fit and the wordmark rung are not arguments anybody passes.

  5. It always carries a text alternative.

    The alternative text is the brand name, and it is written once, inside brand-lockup.tsx — no call site can forget it because no call site supplies it. The repo's wider rule is the same shape: next/image types alt as required, and components/ui/image.tsx makes it required on the raw-img escape hatch too, so omitting it anywhere is a compile error rather than a lint warning somebody can suppress.

  6. It appears without the wordmark in exactly two places.

    The collapsed console rail hides everything after a menu button's first child — see components/ui/sidebar.tsx — so it needs no prop to get there, and it is also why the sidebar renders the lockup's unwrapped form, as two siblings rather than one wrapped row. The console's mobile top bar reaches the same state with brand-lockup.tsx's markOnly prop, because at 390 that row leaves 136.2px for a lockup that needs 156.6. Both are the same rule: where the chrome cannot hold the wordmark, the wordmark drops rather than the mark shrinking.

  7. The app icon is a different asset from the logo.

    Decoded, every square icon is the mark over a solid opaque plate; the wide logo is not. An icon has no surface to adapt to and has to supply one, which makes the icon set light-only by construction, and makes the two assets non-interchangeable rather than two sizes of the same thing.

  8. Every declared icon size is the size the file actually is.

    app/layout.tsx and app/manifest.ts announce a size for each icon they declare. The table above sets those declarations beside the same files decoded through a canvas, so that row is a comparison rather than a restatement — and today every pair agrees.

Not decided, and therefore not stated as a rule

not rules — questions

  • The minimum size.

    The mark now ships at exactly one size in the app — the 32px box measured above — plus the 16px favicon. Unifying the lockup removed the range without deciding anything: nothing enforces a floor, and no legibility test has been run at any size, so there is still no minimum to state here — only a smallest observed use, which is a different claim and must not be dressed up as the first.

  • A clear-space ratio.

    The square icons reserve a transparent margin on all four sides, but the measured amount ranges across the set rather than holding one ratio, and it is proportionally largest on the smallest files. The wide logo reserves none at all. Inside the lockup the mark now has one fixed neighbour — the 8px gap measured above — but around the lockup, clear space is still whatever each of the seven layouts happens to give it, and whether it should instead be a fixed fraction of the mark's height is undecided.

  • Rotation, cropping, monochrome, and using one lobe alone.

    The product does none of these anywhere, which is an observation and not a permission — and not a ban either. Nothing on this page should be read as licensing them or as forbidding them until somebody actually decides.

  • Which file is the source of truth.

    logo.png is what renders, and logo.svg carries the same silhouette, but no build step derives either one from the other and nothing tests that they still match. Until one is made the source and the other regenerated from it, the two can drift apart without anything saying so.

  • Whether the maskable icon should be a separate file.

    app/manifest.ts declares icon-512.png with both the any and the maskable purpose, so one artwork answers both. A maskable icon is cropped to whatever shape the host platform wants, and this one carries the same transparent margin as the rest of the set, measured above. Whether the maskable purpose needs its own full-bleed asset has not been looked at.

A brand page can state a minimum size, a clear-space ratio and a rotation rule in three sentences, and nothing in a codebase will ever contradict them. That is what makes them dangerous here: set in the same voice as the eight measured facts above, they would be indistinguishable from measurement. They are decisions the owner has not made, so they are shipped as questions.

06The contract

The laws, and the prohibitions

The laws are the part of the system that is not a number, and they are the reason the numbers above can be trusted. Each one is enforced by a test that walks every file in the application, and each guard has been proven to fail before it was kept.

The six laws of type

design.md §1

  1. 01Every size binds its own line-height.

    Never a global default, never inherited. A bare font-size makes the browser supply a 1.5 ratio — as a ratio, so it lands on a 12px chip and a 64px headline alike.

  2. 02Every step is perceptible.

    No ratio below ~1.15. Two sizes closer than that are not distinguishable as different, so they cannot carry hierarchy — they read as a rendering inconsistency. The ladder this replaced carried nineteen sizes, eleven of them under 22px.

  3. 03Leading is chosen by role, not by a formula.

    Single-line labels sit tight, multi-line reading copy sits loosest, and display type tightens as it grows. The old rule — the ratio tightens as the size grows — held only for display and was wrong at the reading end.

  4. 04Leading lands on a 4px grid.

    16 / 20 / 24 / 28 / 32 / 40 / 48 / 56 / 68. That quantisation is what makes blocks align down a page.

  5. 05Every size binds its own tracking.

    Tracking is a property of the rung, not a call-site choice. It tightens monotonically as size grows, and opens the other way only for uppercase, which has no ascender/descender rhythm to bind it.

  6. 06Tracking and optical size follow rendered size, not a token name.

    The same rung is a different size at a different viewport, so both properties move with it. That is why the overrides live inside the very same media queries as the sizes — pinned to a token name instead, a 72px headline would take the tracking meant for the 40px one.

Never

each one shipped as a real defect first

  • Never add a rung.

    Nine is the whole scale. If a screen seems to want a tenth, it wants weight or whitespace — every addition costs a perceptible step somewhere else.

  • Never put a leading or a tracking utility beside a type token.

    The token owns the ratio and the tracking exactly as it owns the size. 130 call sites once carried a hand-written leading over rungs whose ratio was already correct, and 132 carried a hand-written tracking — which, because tracking must follow rendered size, was not merely redundant but wrong at one of the three viewports.

  • Never wrap an element in a double border.

    One frame per object. A box inside a bordered card carries its grouping with a tinted fill and a radius. Row rules and section dividers are separators, not frames.

  • Never use a left-accent border bar.

    Every box here has a corner radius, and a straight rule down one side cannot follow it — it renders as a detached stub floating past the top and bottom of the panel it belongs to. Tinted fill plus a full border in the tone, instead.

  • Never put a viewport unit in a font-size.

    It tracks the window; a multi-column grid does not. Use a container query when a size must follow its column.

  • Never write a bare line-height beside a size token in CSS.

    A raw font-size does not pull the companion line-height token the way a utility does. One stylesheet rule once paired size tokens with 1.75 / 1.25 / 1.3 and shipped 31.5px leadings on every blog page.