Helikona Design System

Every asset @helikona/brand ships, on one page, for visual review — no consuming app required. Rendered from the real sources: these are the shipped stylesheets and the actual Icon component, not a copy of either.

@helikona/brand version 1.5.0 63 icons generated by pnpm preview

Colour

Green

Primary brand. Gravity (deep) → approachability (light).

--deep-jungle#0B3D2E
--jungle-mid#1A6B47
--leaf-light#5DC48A
--leaf-pale#E8F2EC

Rust

Accent & emphasis. Use sparingly.

--iron-oxide#6B2A14
--rust-mid#B84C2A
--rust-light#D4896A
--rust-pale#F5EDE7

Amber

Warning / pending states.

--amber-mid#9E5E00

Stone

Neutrals with character, in place of flat greys.

--temple-stone#5A5C50
--stone-pale#EAEAE2

Surface

Page and panel grounds, plus the two tappable washes.

--off-white#F2F3EE
--dark#080D09
--page-bg#F2F3EE
--surface#ffffff
--surface-hoverrgba(26, 107, 71, 0.06)
--surface-activergba(26, 107, 71, 0.18)

Text & borders

--text#181A16
--text-muted#4A4D44
--border#E2E4DA

Semantic

Sit above the palette — these are the ones that flip in dark mode.

--heading#0B3D2E
--link#1A6B47
--action-bg#F5EDE7
--action-text#6B2A14
--leaf-strong#1A6B47
--focus-ring#1A6B47
--press-fill#1A6B47
--press-text#F2F3EE

Typography

Display — --font-display

'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif

Heliconia DM Sans 700 / 48px

The brand, set large DM Sans 700 / 32px

A section heading DM Sans 600 / 24px

A card title DM Sans 600 / 17px

Body — --font-body

'Inter', -apple-system, BlinkMacSystemFont, sans-serif

The heliconia grows toward the light it can reach. Inter 400

The heliconia grows toward the light it can reach. Inter 500

The heliconia grows toward the light it can reach. Inter 600

Icons

Outline glyphs 59

24×24, fill: none, stroked in currentColor at 2 — so one component serves a leaf-green icon button and a muted table cell alike. Shown here at 24px.

alert-triangle
arrow-left
arrow-right
arrow-up
ban
bell
book-open
briefcase
building
calendar
check
check-circle
chevron-down
chevron-left
chevron-right
chevron-up
clock
compass
copy
credit-card
dollar-sign
dot
external-link
eye
file-plus
file-text
flag
grid
help-circle
home
inbox
key
link
list
log-in
log-out
mail
message-circle
message-square
mic
paperclip
pencil
person-plus
phone
play
plug
plus
printer
reply
send
settings
shield
star
trending-up
upload
user
user-plus
users
x

Brand marks 4

The platforms' own solid logos, in their own colours rather than currentColor. Where a UI names a channel, the logo is the thing you scan for — a generic speech bubble does not read as one.

linkedin
messenger
slack
whatsapp

Colour inheritance

The same three glyphs, drawn inside three different colours. Nothing is passed to the component — it takes the colour of whatever it sits in.

var(--leaf-light)var(--text-muted)var(--action-text)

Sizes

A single size number, defaulting to 14.

141620243248

Buttons & links

Looks

.btn.btn-primary

The one thing to do on the page.

.btn.btn-secondary

The action beside a primary one — Cancel next to Approve.

.btn.btn-outline

Section-level. A page can carry several without any claiming rank.

.btn.btn-ghost

Borderless — a Cancel that shouldn't draw a second box to say so.

.btn.btn-danger

Delete's confirm.

.btn.btn-danger-outline

Its pair.

Sizes

Default — 15px, the page scale

.btn-md

The form scale, for a button under a field

.btn-sm

The tight scale — an in-row action

.btn-tall

Thumb-sized, whatever the look

Icon-only

Squares the padding so the glyph sits in a square box. Callers must supply an accessible name — the glyph alone is not one.

.btn.btn-outline.btn-icon

5px padding

.btn.btn-primary.btn-icon.btn-md

8px padding

With a label

States

[data-pending] is mid-submit — you've taken the action and the server hasn't come back. Not the same as :disabled, which says you can't take it; plenty of these stay clickable while pending. Hover and press are live — try them.

Icon-only page actions

36px, growing to 44px below 640px so the tap target stays comfortable on a phone — the glyph stays the same size, centred. Narrow the window to see it. Solid outranks outline, the same pairing the labelled buttons use.

Text

A text link

.btn-quiet is the smallest action there is — it sits inside a line of body text, where a box would outrank the sentence it annotates. Its dotted underline goes solid on press.

Chat

The shared vocabulary every agent rail draws on — @helikona/brand/chat.css for the layout and @helikona/brand/chat-markdown's <ChatMarkdown> for rendering a reply. This section doubles as the regression check for the two things ChatMarkdown exists to fix: a GFM table rendering as an actual table, and a reply mid-stream surviving an unterminated ** as literal text rather than breaking.

Turn shapes

The user's message is a bubble; the assistant's is bare text — the reply is the page, not a log entry. The flag affordance sits at the assistant turn's left edge, at rest (opacity: .6) — hover it to see it sharpen.

Tell me about the forms we have.

Forms — 1 total

FieldType
Nametext
Emailemail

Mid-stream, unterminated markdown

The exact failure mode a streamed reply hits before its closing ** arrives. This must read as literal asterisks, never throw.

Checking the Landing Pages config… **almost do

Tool chips

Only a clean success may read in the past tense — .is-failed and .is-declined exist so a failed or declined call never looks like it happened.

Sent the email Failed to open Landing Pages Declined to send the campaign

Composer

.hk-chat-composer, .hk-chat-icon-btn and .hk-chat-send — the input row every rail assembles the same way.

Motion

The brand's answer to a generic spinner. Two nested rotations make the sway read as wind rather than a rocking icon — the whole plant pivots at the base of the stem, and the upper bracts pivot on the same period but phase-shifted, so the tip lags the stem and catches up. Purely decorative (aria-hidden); pair it with your own visible status text.

1.5em2em3em5em

In context, at the size it's usually drawn:

Working…

It respects prefers-reduced-motion on its own — under that setting the sway is replaced by a slow breathe. Toggle it in your OS to check.

Email

Email clients mostly strip custom properties, so the templates carry literal hex. These are the same brand values, flattened — and light-mode only: don't attempt dark-mode switching in email.

deepJungle#0B3D2E
jungleMid#1A6B47
leafLight#5DC48A
leafPale#E8F2EC
offWhite#F2F3EE
dark#080D09
text#181A16
textMuted#4A4D44
border#E2E4DA
ironOxide#6B2A14
rustMid#B84C2A
rustPale#F5EDE7
templeStone#5A5C50
stonePale#EAEAE2

Signatures

Table-based, inline styles only, for pasting straight into Gmail's signature settings. Rendered below in an iframe on a white ground, which is what a mail client will give them.

Accessibility

Measured on this build from the token values themselves, not asserted. Ratios are WCAG 2.2 relative luminance. The threshold applied depends on what the pair is for: 4.5:1 under 1.4.3 Contrast (Minimum) for body text, 3:1 under 1.4.11 Non-text Contrast for borders, glyphs and other interface parts. AAA marks a text pair clearing 1.4.6's 7:1.

Only pairings the shipped CSS actually draws are listed. A full cross-product would be noise, and would imply the package endorses combinations it never puts on screen.

Light mode

WhereTokensRatioWCAG 2.2
Body text on the page --text on --page-bg 15.72:1 AAA
Body text on a card or panel --text on --surface 17.53:1 AAA
Muted text on the page --text-muted on --page-bg 7.73:1 AAA
Muted text on a card — .btn-ghost's label --text-muted on --surface 8.62:1 AAA
Headings on the page --heading on --page-bg 10.94:1 AAA
Text links, .link --link on --page-bg 5.82:1 AA (4.5:1)
Text links on a card --link on --surface 6.49:1 AA (4.5:1)
.btn-primary — label on its fill --deep-jungle on --leaf-light 5.65:1 AA (4.5:1)
.btn-primary:active — label on the press fill --press-text on --press-fill 5.82:1 AA (4.5:1)
.btn-danger-outline label, .btn-quiet-danger --action-text on --surface 10.68:1 AAA
.btn-danger — label on its fill --surface on --action-text 10.68:1 AAA
.btn-outline label and border --leaf-strong on --surface 6.49:1 AA (4.5:1)
.icon-outline-btn glyph and border --leaf-strong on --page-bg 5.82:1 AA (3:1)
.btn-outline label on its own hover wash --leaf-strong on --leaf-pale 5.67:1 AA (4.5:1)
Keyboard focus ring against the page --focus-ring on --page-bg 5.82:1 AA (3:1)
Keyboard focus ring against a card --focus-ring on --surface 6.49:1 AA (3:1)
.btn-secondary's border against its ground --border on --surface 1.28:1 fails 3:1
Warning / pending text --amber-mid on --surface 5.18:1 AA (4.5:1)
Eyebrow labels and muted chrome --temple-stone on --page-bg 6.11:1 AA (4.5:1)

Dark mode

WhereTokensRatioWCAG 2.2
Body text on the page --text on --page-bg 14.57:1 AAA
Body text on a card or panel --text on --surface 13.27:1 AAA
Muted text on the page --text-muted on --page-bg 5.48:1 AA (4.5:1)
Muted text on a card — .btn-ghost's label --text-muted on --surface 4.99:1 AA (4.5:1)
Headings on the page --heading on --page-bg 8.60:1 AAA
Text links, .link --link on --page-bg 8.60:1 AAA
Text links on a card --link on --surface 7.83:1 AAA
.btn-primary — label on its fill --deep-jungle on --leaf-light 5.65:1 AA (4.5:1)
.btn-primary:active — label on the press fill --press-text on --press-fill 5.65:1 AA (4.5:1)
.btn-danger-outline label, .btn-quiet-danger --action-text on --surface 6.11:1 AA (4.5:1)
.btn-danger — label on its fill --surface on --action-text 6.11:1 AA (4.5:1)
.btn-outline label and border --leaf-strong on --surface 7.83:1 AAA
.icon-outline-btn glyph and border --leaf-strong on --page-bg 8.60:1 AA (3:1)
.btn-outline label on its own hover wash --leaf-strong on --leaf-pale 7.48:1 AAA
Keyboard focus ring against the page --focus-ring on --page-bg 8.60:1 AA (3:1)
Keyboard focus ring against a card --focus-ring on --surface 7.83:1 AA (3:1)
.btn-secondary's border against its ground --border on --surface 1.17:1 fails 3:1
Warning / pending text --amber-mid on --surface 7.83:1 AAA
Eyebrow labels and muted chrome --temple-stone on --page-bg 7.15:1 AAA

What the numbers say

Light: 18 pass, 1 fail — .btn-secondary's border against its ground (1.28:1). Dark: 18 pass, 1 fail — .btn-secondary's border against its ground (1.17:1).

Two defects this analysis found on its first run have since been fixed, and one entry remains listed deliberately:

  1. Fixed — brand green as a label colour on a light ground. --leaf-light is a mid-tone green drawn to sit on --deep-jungle, and it does that well, but .btn-outline also used it for its text on --surface, at 2.16:1 against a 4.5:1 requirement — and for .icon-outline-btn's glyph, which missed the looser 3:1 bar too. Both now take --leaf-strong, which is --jungle-mid in light mode and --leaf-light again in dark. The control looks unchanged on a dark ground and merely becomes legible on a light one. The palette token itself was not touched: it is a brand colour, and it was never wrong — it was being asked to do a job it was not drawn for.
  2. Fixed — the press state in dark mode. --off-white on dark mode's --jungle-mid was 2.52:1. The press now runs through --press-fill / --press-text, which flip: in light mode it darkens to jungle green with a pale label as before, and in dark it inverts to --deep-jungle with a --leaf-light label. Both clear 4.5:1, and both still read unmistakably as a press, because each is a change of hue rather than of lightness.
  3. Listed, not failed — --border on --surface. 1.4.11 asks 3:1 of visual information required to identify a control. .btn-secondary carries a full-contrast text label, so its border is not doing that work alone, and a heavier rule would cost the quiet that makes it read as secondary in the first place. It stays in the table because the number is real — but it does mean a border must never be the only thing marking a control.

Both fixes were made as semantic tokens rather than as a prefers-color-scheme block inside buttons.css. That is deliberate: Turbopack is documented in two consuming repos as dropping @media blocks from a package's imported CSS, so a theme-dependent rule written there would have failed silently in exactly the apps that need it.

Beyond contrast

Contrast is the part a page can measure itself. These are the rest of the accessibility decisions baked into the shipped CSS and components — stated plainly, including where they fall short.

Target size — 2.5.8 (AA, WCAG 2.2)
Met by .icon-outline-btn / .icon-solid-btn, which grow from 36px to 44px below 640px, and by .btn-tall. Not met by .btn-sm (29px) or .btn-icon (26px) if they are the only way to reach a function. 2.5.8 asks for 24×24 CSS px as a floor, which both clear — but the 44×44 of 2.5.5 (AAA) is the target worth hitting on touch. Reach for .btn-tall on anything a thumb has to find.
Pointer gestures and hover — 1.4.13
Met. No control in the package depends on hover to reveal content or function; every :hover rule is decorative and gated behind @media (hover: hover), so a touch device never enters a hover state it cannot leave. Nothing here requires a drag, a path gesture, or a multi-point gesture.
Use of colour — 1.4.1
Partially met, and the gap is the consumer's to close. Press states change hue by design, but they are transient feedback rather than information. The real risk is downstream: a status shown only as --amber-mid text or a bare dot glyph conveys its meaning by colour alone. Pair any colour-carried status with a word or a distinct glyph.
Non-text content — 1.1.1
Met by the component, unmet by default at the call site. Icon renders aria-hidden="true", which is correct: the glyph is decorative and the surrounding text is the label. But an icon-only button has no surrounding text, so .btn-icon, .icon-outline-btn and .icon-solid-btn are inaccessible until the caller supplies aria-label. The package cannot enforce this; every example on this page does it.
Focus visible — 2.4.7
Met. Every control in buttons.css takes a 2px --focus-ring outline on :focus-visible, at a 2px offset. :focus-visible rather than :focus, so a mouse press leaves no ring behind — that is the reason rings get deleted from stylesheets, and the fix for it rather than an excuse to ship none. The offset is what lets one colour serve all six looks: drawn on .btn-primary itself the ring would be leaf-on-leaf, but two pixels out it lands on the page. outline rather than box-shadow, so it follows the border radius, takes no part in layout, and survives a forced-colours mode where a shadow is discarded. Tab through this page to see it. Measured against both grounds in the table above.
Motion — 2.3.3
Met. BrandSway honours prefers-reduced-motion on its own, replacing the sway with a slow breathe. Button transitions are 180ms opacity and colour changes, below the threshold at which motion becomes a vestibular concern, and press deliberately drops its transition entirely.
Resize text — 1.4.4 / 1.4.10
Met with one caveat. Type is set in px, which browser zoom scales but a user's minimum-font-size setting does not. Buttons carry white-space: nowrap, so a long label in an enlarged font will overflow its container rather than wrap. Worth reviewing on any button whose label is user- or locale-supplied.
Contrast of the washes — 1.4.11
Not measurable here, and unmeasured. --surface-hover and --surface-active are translucent greens (6% and 18% light; 7% and 20% dark) composited over whatever sits behind them. They carry no information on their own — they are feedback layered on a control that is already distinguishable — so 1.4.11 does not apply to them directly. They are excluded from the table above rather than silently given a number.

This is a static analysis of the design tokens and the shipped CSS. It is not an audit of any application built from them: focus order, heading structure, form labelling, live regions and keyboard traps are all properties of the assembled page, and none of them can be settled here.