Design System

Foundations

Visual foundations

Shape, colour, icons and brand assets define the visual language of the Malkai mid-office. Canonical values live in tokens/tokens.css; this page shows how those values appear in the interface.

Shape, borders & elevation

Editorial print, not glossy SaaS: radii small, borders hairline, one low shadow. A line is a thickness and a rhythm before it is a colour — the whole layer is drawn with three steps of one ink and two styles.

Corner radius

--mk-radius-sm · 2pxcontrols — buttons, chips, inputs
--mk-radius-compact · 4pxdense cards in a band — the service card
--mk-radius-md · 6pxcontainers — cards, sheets, tables

Borders — three steps, two styles

Every border in the workspace is 1px and comes from the hairline family, three steps deep: --mk-hairline (.14) the quiet line, --mk-hairline-mid the frame, --mk-hairline-strong (.32) the drawn edge of an instrument. Solid is the default; dashed is a state. --mk-border / --mk-border-strong are deprecated aliases — write the line out.

--mk-hairline · .14the quiet line — row separators, rules inside a surface
--mk-hairline-midthe card dress — the frame every container wears: card, widget, table
--mk-hairline-strong · .32the instrument edge — controls, menus, popovers, add-row
1px dashed --mk-hairline-dashthe state mark — empty, offered, provisional · 3 on / 2 off
.mk-railthe gradient-drawn rail — the same 3/2 rhythm, vertical in use
--mk-hairline-accentthe warm edge of a field — inputs
3px solid · a state's inkthe state rail on a box's left edge — the corners it rides are square

The dash is ONE rhythm (07-16): a 1px CSS dash draws 3px on / 2px off, and a gradient-drawn rail (.mk-rail) matches it exactly — never 2px, never another period. Its ink is --mk-hairline-dash (.5): a 3/2 dash lays ink on 60% of its length, so at the solid strong's .32 it read as a solid ~.19 — the ink overshoots to land at the strong's weight (08-02). A dashed border is a state mark, never an affordance (C15): what it bounds says what is missing; the action lives elsewhere. Neither neighbour holds a frame: the quiet hairline dissolves as an outline (08-02), and strong solid is an instrument's edge — a card, widget or table framed in either stood out of the family.

Elevation

--mk-shadow-carda card rises off the page ground
--mk-shadow-popa dialog floats over a scrim

Two steps and deliberately no third. Both are the brand ink at a low opacity — nothing casts a grey shadow.

Colors

The pigments, grouped by family — every colour value the workspace holds, and the tokens that point at each one. A pigment is often shared: the same gold is the accent, the build dot and the brand’s dark camel. Where a colour is used belongs to the section about that thing — buttons with buttons, rules with borders. Values are read from tokens.css, so this cannot drift from the code. Both palettes live here, each shown on its own ground — a pigment is only true against the surface it was chosen for — and the theme you are reading in comes first.

The brand gold

The camel family and the pale gold that carries a badge. One pigment often wears several names — --mk-camel-dark is also the accent and the build dot.

#BBAC75--mk-camel-light2.3:1 on white
#968037--mk-camel-mid3.9:1 on white
#82670E--mk-camel-dark--mk-dot-build--mk-accent5.4:1 on white
#785F0D--mk-camel-deep--mk-accent-deep6.1:1 on white
#EEE6CB--mk-badge-bg--mk-badge-shell1.2:1 on white

Black and white

Three values, and the middle one is the interesting one. The brand book defines its «black» as #1D1F1D — a warm Pantone near-black, the right answer for print, where a true zero is neither reachable nor wanted. A screen is not paper, and the book is a reference rather than the source of our decisions, so pure black exists here too (29 Jul). --mk-ink stays the voice of the brand and the default for text; --mk-black is the true zero for the places that need one.

#000000--mk-black21.0:1 on white
#1D1F1D--mk-ink--mk-text16.6:1 on white
#FFFFFF--mk-surface1.0:1 on white

The greys — ink at a distance, not pigments of their own

A design system usually keeps a grey ramp. This one does not, and the reason is worth stating: almost every grey here is the brand ink held at an opacity, not a colour that was chosen. That is deliberate — a flat grey is only correct on the ground it was picked against. #656765 reads right on white and wrong on the cream page, wrong again on a sunken band and on a state tint. The ink at 68% is right on all four, and it follows the dark theme without a second value. Two true grey pigments do exist, and they are here because they are colours, not distances: the closed-phase dot and the brand’s warm neutral. Below is the ladder those opacities actually produce on white; its 100% ink anchor is already shown once in Black and white above.

#343634--mk-text-secondaryink 68% over muted
#4A4C4A--mk-icon-inkink 80%
#4F504F--mk-text-labelink 78%
#656765--mk-text-mutedink 68%
#9AA0A6--mk-dot-exitpigment — closed phase
#B7B7B7--mk-hairline-strongink 32%
#C8C5BE--mk-warm-greypigment — unused brand neutral
#DFE0DF--mk-hairlineink 14%

Brand neutrals and the surface ramp

The brand’s beige, and the interface neutrals between it and white: the ground a page runs on, and the well inside a card.

#F6F3EA--mk-beige--mk-accent-contrast1.1:1 on white
#E8E1CD--mk-beige-dark--mk-surface-sunken1.3:1 on white
#F7F5F0--mk-page1.1:1 on white
#FBF9F2--mk-surface-raised1.1:1 on white

System colours

What the interface says went right, went wrong, or is still running. Each has a fill and, where the fill could not carry words at 4.5:1, a darker ink beside it.

#4F6B4A--mk-success--mk-success-text--mk-dot-win5.9:1 on white
#B3772E--mk-warning--mk-dot-wait3.8:1 on white
#9A4E16--mk-warning-text6.0:1 on white
#8F2D2B--mk-error--mk-error-text8.1:1 on white

System tints

The plate each state sits on.

#E4EADF--mk-success-tint1.2:1 on white
#F2E4CF--mk-warning-tint1.3:1 on white
#F0DBD7--mk-error-tint1.3:1 on white

Phase and lifecycle

Where an enquiry stands. Two pigments of their own; the other three reuse the system and brand colours above, which is why a build dot and the accent are the same gold.

#5E7A8C--mk-dot-lead4.5:1 on white
#9AA0A6--mk-dot-exit2.6:1 on white

Telling stays apart

The builder’s date picker marks every other stay of the journey in the months it shows, so a property that appears twice in a route reads as two stays. Qualitative, not sequential. Flagged: this is the one set here that does not sound like Malkai — saturated blue, magenta and indigo against a workspace of camel and sage. Naming them fixed the plumbing; redrawing them is its own look.

#3B72B8--mk-stay-14.9:1 on white
#A0549B--mk-stay-25.0:1 on white
#A3623B--mk-stay-34.8:1 on white
#5F63B8--mk-stay-45.3:1 on white

Elsewhere

The brand’s muted neutral, and the ink that darkens a photograph — cooler and deeper than --mk-ink, so an image darkens without going warm. The neutral was --mk-blue-grey #BFC7C4 until 29 Jul, when its green channel was found sitting above the other two — an olive cast that read as a different family from everything around it. It now carries --mk-beige’s own warm ratio at the same lightness, and the name went with the colour. Still unused: the brand book carries it and the README calls it a rare background accent, but no screen states it — not a prototype, not the component layer.

#C8C5BE--mk-warm-grey1.7:1 on white
#121410--mk-scrim-ink18.5:1 on white

Derived — not pigments

These hold no colour of their own: each is one of the pigments above at an opacity, or two of them mixed. Why not simply pick a colour? Because a flat value is only correct on the ground it was chosen against. This workspace stacks four grounds — the cream page, the white card, the sunken band and a state tint — and a divider, a hover or a muted word has to look right on all of them. A hex would need one token per ground; the recipe needs one, and it lands correctly on every surface, including the ones designed later. It also carries the dark theme for free: mix from semantic tokens and the value remaps itself, so nothing here needs a second definition. Before 29 Jul all of them were mixed at the place they were used — 131 times, in 67 different recipes, so a hover was 4% in one component and 8% in its neighbour.

--mk-text-mutedrgba(29, 31, 29, 0.68
--mk-text-labelrgba(29, 31, 29, 0.78
--mk-text-secondarytext 68%, var(--mk-text-muted
--mk-icon-inkrgba(29, 31, 29, 0.8
--mk-hairlinergba(29, 31, 29, 0.14
--mk-hairline-midhairline-strong 55%, var(--mk-hairline
--mk-hairline-strongrgba(29, 31, 29, 0.32
--mk-hairline-accentaccent 50%, var(--mk-hairline-strong
--mk-wash-fainttext 4%, transparent
--mk-washtext 6%, transparent
--mk-wash-strongtext 8%, transparent
--mk-wash-presstext 12%, transparent
--mk-accent-washaccent 8%, transparent
--mk-accent-wash-strongaccent 14%, transparent
--mk-surface-bandsurface-sunken 35%, var(--mk-surface
--mk-surface-band-strongsurface-sunken 55%, var(--mk-surface
--mk-success-washsuccess-text 30%, transparent
--mk-warning-washwarning-text 30%, transparent
--mk-error-washerror 8%, transparent
--mk-error-edgeerror 40%, transparent
--mk-scrimrgba(29, 31, 29, 0.32

Elevation

Not colours, but they are made of one: both shadows are the brand ink at a low opacity, so nothing in the interface ever casts a grey shadow.

--mk-shadow-card0 1px 3px rgba(29, 31, 29, 0.08)
--mk-shadow-pop0 8px 32px rgba(29, 31, 29, 0.24)

Rules, borders and dashed edges live in Shape, borders & elevation — they are a thickness and a rhythm before they are a colour.

The dark theme

Every value the [data-theme="dark"] block overrides, shown on its own ground. The chips read the tokens live, so the panel cannot drift from tokens/tokens.css; the hex captions and the contrast figures are generated from the same block. Measured against the dark card, #151515, except where a row names another ground.

The ladder of grounds

Neutral greys, R=G=B, stepping down to a true-black page. The ink had run out of headroom, so contrast is bought by dropping the grounds under it. Neighbouring steps stay 1.06–1.16:1 apart, which is what keeps a band tellable from the one beneath it.

#000000--mk-pagebody ink 19.9:1 on it
#0D0D0D--mk-surface-sunkenbody ink 18.4:1 on it
#151515--mk-surfacebody ink 17.3:1 on it
#1E1E1E--mk-surface-raisedbody ink 15.8:1 on it
#2A2A2A--mk-badge-shellbody ink 13.6:1 on it

The ink ladder

One voice at full ink and four registers under it. Body sits at the ceiling because every step below it proved invisible on a real page; under body come meta (a timestamp, a count), the caps label, and absence.

#FBF9F2--mk-text17.3:1 on the card
#FBF9F2--mk-text-muted17.3:1 on the card
rgba(251, 249, 242, 0.72)--mk-text-meta0.72 ink · 9.3:1 on the card
rgba(251, 249, 242, 0.60)--mk-text-label0.6 ink · 6.8:1 on the card
rgba(251, 249, 242, 0.52)--mk-text-quiet0.52 ink · 5.4:1 on the card
rgba(251, 249, 242, 0.8)--mk-icon-ink0.8 ink · 11.3:1 on the card

Lines and veils

Neutral white at three opacities, matched to the neutral grounds, plus the veil behind a sheet.

rgba(255, 255, 255, 0.13)--mk-hairline0.13 opacity
rgba(255, 255, 255, 0.30)--mk-hairline-strong0.3 opacity
rgba(255, 255, 255, 0.48)--mk-hairline-dash0.48 opacity
rgba(0, 0, 0, 0.66)--mk-scrim0.66 opacity

The accent, and what rides on it

One hue at 47.9°, walked in lightness: the ink that carries text, the saturated ink a quiet act's label takes, the focus ring, then the fill and the two faces under it. Nothing here is a different gold, which is the whole point — before 08-19 these values ranged over four hues and the focus ring was still wearing the light theme's brand camel. Saturation was walked three times that day (42% → 58% → 63%) with no visible change, so the applied value is two notches on from where it started; concept-rounds/dark-gold-ladder.html lays out G0 to G4 with the whole family in use, and the next move should be a pick from those columns.

#F7D13B--mk-accentink · 12.3:1 on the card
#D6AB00--mk-accent-ink-stronga quiet act's label · 8.4:1
#F5C50A--mk-camel-midthe focus ring · 11.2:1
#8A6E00--mk-accent-fillthe fill · 3.75:1 as a block, 4.6:1 under its label
#6E5800--mk-accent-fill-hover1.41:1 under the fill
#5B4900--mk-accent-busy1.80:1 under the fill
#6C5600--mk-accent-deepthe pressed step
#FBF9F2--mk-accent-contrastthe label on every fill above

The same accent, doing its job

A swatch and a label are not the same colour to the eye, and the label is the one that ships — so the family is also shown here as the real components reading the real tokens. This strip is the reference's own check: if a gold looks right in the chips above and wrong here, here is right.

A confirmed stay at four nights, and the agent still has to open the guest's profile before the deposit can be taken.

The functional palette

Lifted off the light values: on a dark ground those fills measured 2.7:1 and the text variants failed outright. Each text value clears 4.5:1 on the card, on the raised surface and on its own 14% tint.

#75946F--mk-success5.4:1 on the card
#CE8C42--mk-warning6.5:1 on the card
#D07C72--mk-error5.9:1 on the card
#9CB098--mk-success-text7.9:1 on the card
#DDA05A--mk-warning-text8.0:1 on the card
#E09A92--mk-error-text8.0:1 on the card

Phase dots

Three of the five needed lifting to clear the 3:1 non-text floor on a dark badge. The base names point at the lifted variants, so no page has to opt in.

#6E8CA0--mk-dot-lead-dark4.0:1 on the badge shell
#AE8E2E--mk-dot-build-dark4.6:1 on the badge shell
#75946F--mk-dot-win-dark4.3:1 on the badge shell

Icons

One library, one hand. Every glyph is drawn on a 16 grid and painted in currentColor — so an icon takes the ink of whatever it sits beside. Line icons use --mk-icon-stroke with butt terminals and mitre joins; semantic markers and disclosure triangles are deliberately filled. The stroke does not scale with the box: a 12px mark and a 24px mark both put down the same hairline, which is how the brand draws its own UI. This grid is generated from the registry at load, so it cannot drift from the product the way the hand-drawn Feather set that stood here until 29 July had.

Ask for one by name — <svg data-mk-icon="route"></svg> in markup, mkIcon('route', 'ico') in rendered strings. Never inline the path: a copy cannot travel, and the chevron proved it — it was the most-set mark in the workspace and lived as fourteen inline drawings until it entered the library.

In use across the workspace

Measured on the builder, the enquiry record, the queue and the itineraries list on 29 July — every icon those four pages actually render.

Drawn, not yet placed

In the library and ready to ask for. They wait on the surfaces that have not been built.

The filled glyphs. star marks the person a trip answers to — a marker is read at a glance, and an outline star reads as a rating control you could click. chevron and its legacy caret alias are the same solid triangle on every disclosure and attached menu. The triangle owns its fill in the registry, so a page cannot accidentally turn it back into a hairline.