Skip to the system

The living design system

The system, as the pack locks it.

Ten colours, each carrying one meaning. Two grotesques and seven roles. Six shapes, four elevations, four durations and one ceremony. Every specimen below resolves through the same tokens the product ships, so a change to the token layer changes this page. Where this page and a pack file disagree, the file is right.

Nothing on this page is a claim about the service. Product strings appear only as format specimens, quoted from the pack and labelled as quotations.

01 Colour

Each colour carries one meaning, and may never be spent elsewhere.

Ten colours are named as tokens in ruzayo.css. Every one of them has a job and a prohibition. The prohibition is the useful half: a palette is only a system if a colour can be spent wrongly.

Source: CANON.md §9 · tokens.json · values from ruzayo.css

Measured, not asserted

Eleven pairings were measured against the stated background. Two of them fail, and both are recorded as failures rather than quietly avoided — a stated failure survives a redesign, and a convention does not.

Measured contrast ratios for every named pairing

Pairing Specimen Ratio Verdict
ink on oat 13.4:1 AAA
lead on oat 7.9:1 AAA
moss on oat 10.4:1 AAA
slate on oat 5.9:1 AA
terra deep on oat 5.7:1 AA
terracotta on oat 4.6:1 AA Body, not AAA. Do not use below 16px.
bone on moss 10.9:1 AAA
amber on moss 5.6:1 AA The vessel figure. Moss is the only surface that licenses amber as a letterform.
cream on terracotta 5.3:1 AA
amber on oat 1.8:1 Fails Amber is never text on the page.
sage on oat 2.5:1 Fails Sage is a surface, never text.

The amber rule

Amber is a fill, a tint and a figure on moss. It is never a letterform on the page. At 1.8:1 on oat it is not dimmed text, it is not-text. Where a live quantity needs a word beside it, the word is ink or slate and the amber is the surface behind it.

9.8 GB amber on moss, 5.6:1 — licensed

The sage rule

Sage is a surface. It tints a panel, it fills a dot, it never sets a word. And because colour never carries a state alone, a sage dot is never the whole message: the word travels with it, every time.

a sage surface --rz-tint-sage — a surface, not an ink

Named conflict · two colours with no token

tokens.json names twelve colours. ruzayo.css names ten. The two without a token are lead (#3C4740, lead paragraphs on oat, 7.9:1) and terra deep (#8E4527, text on a terracotta tint, 5.7:1); the stylesheet writes both as literals inside .rz-lead, .rz-btn--exit:hover and .rz-state--wants. Reported, not resolved here: ruzayo.css is not this page's to edit. Until it names them, this page declares the two values once, in its own stylesheet, cited to tokens.json.

02 Proportion and alpha

If the accent reads as a colour scheme, it has been overspent.

The palette is not a set of equals. It is a ratio, and the ratio is most of the identity: a page that is three-quarters neutral makes the one terracotta thing on it unmistakable.

Source: tokens.jsonproportion, alpha_ladder

Proportion, enforced

  • ~75% neutrals — oat, shell, ink, slate, cream, bone
  • ~18% moss
  • a few per cent, together terracotta and amber

The bar's third band is whatever is left when the first two are drawn. The pack states the accent share as a few per cent, together and puts no number on it, so neither does this page.

The alpha ladder

Three steps and a floor. Nothing readable below the floor — under 0.66 an ink is a rule or a wash, not text.

tokens.json measures eleven colour pairings and no alpha step, so no contrast ratio is printed beside the three steps above. Stated rather than estimated.

03 Type

Two families. Seven roles. Three floors.

Bricolage Grotesque takes display, titles and figures. Familjen Grotesk takes everything else. There is no third family, and nothing sits between two of these seven roles.

Source: tokens.jsontype · classes in ruzayo.css

Aa

Bricolage Grotesque

--rz-font-display

Display, titles, figures. Weights 400 and 500.

Aa

Familjen Grotesk

--rz-font-ui

Everything else. Weights 400, 500, 600 and 700.

The seven roles, at their real sizes

  1. Display

    Custody, in a line

    .rz-display

    clamp(44px, 6vw, 88px) · line 0.96 · tracking −0.04em · weight 400

  2. Title

    The address stays yours

    .rz-title

    clamp(30px, 3.4vw, 50px) · line 1.02 · tracking −0.03em · weight 400

  3. Section

    What the record says

    .rz-section

    26px · line 1.16 · tracking −0.02em · weight 400

  4. Lead

    A lead paragraph carries the one warm line, and then stops.

    .rz-lead

    19px · line 1.62 · lead ink on oat, 7.9:1

  5. Body

    Body copy is the default on the page. There is no class for it, and adding one is how a system grows a second body style.

    the default on <body>

    16px · line 1.7 · ink on oat, 13.4:1

  6. Fact

    61,204 messages · 9.8 GB · 4 Aug 06:40:12

    .rz-fact

    14.5px · weight 600 · tabular-nums, so figures stack in a column

  7. Label

    Under this name

    .rz-label

    12px · weight 700 · tracking 0.16em · uppercase · slate on oat, 5.9:1

The product floor

12px labels, 14.5px facts, 16px body. Nothing in the product is set smaller than the role it belongs to.

The floors outside the product

Print 12pt. Presentation 24px. The most important line on any surface, 26px.

Named conflict · the accent label

.rz-label--accent sets the label role in terracotta. The label role is 12px; terracotta measures 4.6:1 on oat and the pack's own verdict is do not use below 16px. The class as written asks for both at once. So this page sets every eyebrow in the slate label, and shows the accent variant once, raised to the terracotta floor.

Raised to 16px .rz-label--accent on oat at 16px — terracotta, 4.6:1, AA body

04 Shape

A capsule means you act on it. A panel or a tile means you read it.

Six forms, and the radius is the whole grammar. Nothing needs a label to say whether it is a control, because its corner already said so.

Source: tokens.jsonshape, targets

The signature element

The vessel.

An asymmetric organic form, heavier at its base, holding a light inside it and crossed once at the shoulder by the diagonal borrowed from the z. It is the name drawn: something open that carries weight and settles.

It appears once per surface and it always contains something real. It is never decoration, never repeated as a pattern, and never shown empty — which is why the shape card above shows the radius and this band shows the vessel.

The deep-corner padding formula

A deep corner eats its own content unless the padding clears the arc.

inset = R − √(R² − (R − dy)²)

Where dy is how far down the content starts. A 110px corner with content 46px down needs 21px. The token pads 52px, which still clears if the top edge tightens to 16px. This panel is a deep A, padded by --rz-deep-min-padding. Do not lower it.

The opposing pair

Deep B is deep A turned a quarter. They are used against each other so a run of panels has a direction, and they are the only two shapes in the system that are not symmetrical.

A capsule means you act on it. A panel or a tile means you read it. A deep corner means neither: it is composition, and it will never be a control.

Targets, and the focus ring

Touch target minimum 44px. Input minimum 46px.

Focus is a 2px terracotta outline at 2px offset, and it is never removed. Move a keyboard through the three controls below and it appears on each of them.

.rz-btn is the primary. .rz-btn--ghost is the secondary. .rz-btn--exit is terracotta, and terracotta is the exit — which is why there is never a fourth button colour to invent. The input is the one white surface in the system: oat is the page and may never be white, and a field is not the page.

05 Depth

One blurred shadow in the whole system.

Four elevations. Three of them are surface value and a hairline; only the fourth leaves the page, and it is reserved for the thing that is being held.

Source: tokens.jsondepth

The shadow

0 40px 70px -40px rgba(27,33,28,0.55)

--rz-shadow-held. There is exactly one. Everything else is surface value and a hairline, and a second shadow is how a system starts to look like every other system.

The light

One source, upper left, low opacity, every frame. The light pool behind the masthead and the light inside the vessel in §4 come from the same place the shadow falls, because two light sources read as an accident.

06 Motion

It lands. Nothing overshoots, nothing bounces.

Four durations and one easing. Every animation has a reduced-motion equivalent, and nothing important is ever carried by motion alone.

Source: tokens.jsonmotion

The settle easing

cubic-bezier(0.2, 0, 0, 1)

--rz-ease-settle. The curve never rises above its end value, which is the whole reason nothing bounces.

The four durations

  • leave 180ms
  • settle 260ms
  • arrive 340ms
  • swell 420ms

--rz-dur-leave, --rz-dur-settle, --rz-dur-arrive, --rz-dur-swell. Each track runs at its own token when it is pointed at, and the figure beside it is the value, so the reading never depends on the movement.

The one ceremony

listening

The swell, once: the moment a name reads as listening. It is 420ms, it scales to 1.05 and returns, and it happens one time. There is no second ceremony in the system and nothing else may borrow this one — a celebration that fires twice is a celebration of nothing.

.rz-swell · it played on load. Reload the page to see it again, which is exactly as often as the product plays it.

Arrive

held

.rz-arrive is 340ms: fourteen pixels in from the left, into place, no overshoot. It is how a row joins a list, and it is the only entrance.

Under prefers-reduced-motion both of these resolve to their end state immediately, and the page loses nothing — because neither of them was carrying information.

07 State

Colour never carries a state alone. Every state is a dot and the word.

Five states, no sixth. Each is a real value on the core object, each renders as a chip carrying both marks, and each has a reason a person can read.

Source: mailbox.d.tsMailboxState · lifecycle.md · classes in ruzayo.css

  • listening
  • held
  • wants you
  • refused
  • resting

The five mailbox states, with the class, the enum, and what each one means in life

State Class and enum Entered when Exits when Still receives
listening .rz-state--listening
'listening'
All six gates hold. A gate breaks, or a hold is placed. Yes
held .rz-state--held
'held'
The customer places a hold. The hold is lifted. Yes — kept, nothing forwarded
wants you .rz-state--wants
'wants_you'
A record broke, a webhook is failing, or the ramp paused. The cause clears. Yes
refused .rz-state--refused
'refused'
We declined it. Never. It is not reversible in place. No
resting .rz-state--resting
'resting'
Thirty days with no arrival. Mail arrives. Yes

Named conflict · one state, three spellings

The enum in mailbox.d.ts is wants_you. The class in ruzayo.css is .rz-state--wants. The word a person reads is wants you. Three spellings of one state is a bug waiting to be written, so it is written down here instead: the modifier is --wants, and it is not --wants-you.

The row, and its reserved gutter

sara@herstudio.com
listening · all set
4 Aug 06:40
hooks@herstudio.com
wants you · a webhook is failing
2 Aug 09:41
press@herstudio.com
resting · nothing has arrived for thirty days
5 Jul 18:02

The third row has no dot and does not shift left: the 8px gutter is always reserved, or a list goes ragged the moment one row falls quiet. Every dot here has its word in the detail line beside it, because a dot alone is a colour, and colour is not a state.

08 The mark

Small, large, small. Never four the same.

Three dots in the name's own rhythm, ru-ZA-yo. The sizes and the colours come from the class, not from the markup: three empty <i> elements are the whole mark.

Source: ruzayo.css → THE MARK · the stress pattern is recorded in LATER.md

The mark

9px terracotta, 19px moss, 9px sage, with a 4px gap. Three sizes, three inks, one rhythm.

ruzayo

The lockup

Bricolage 500 at 25px, tracking −0.02em, moss. In prose the product is Ruzayo; the lockup is set lowercase.

Under 20px

The middle dot stands in alone. .rz-mark--small hides the outer two rather than shrinking all three into mush.

On terracotta

All three dots take one ink — cream — or the left one vanishes into the surface. The rule lives on .rz-panel--terra .rz-mark and .rz-on-terra.

On moss

Only the middle dot changes: moss on moss is nothing, so it takes bone. The outer two keep their own inks, because they still read.

The markup

<span class="rz-mark" aria-hidden="true"><i></i><i></i><i></i></span>
<span class="rz-wordmark">ruzayo</span>

The dots carry no text, so they are hidden from the reader and the accessible name comes from the word beside them. Where a mark and a wordmark are one link, the wordmark is the name and a single .rz-sr word finishes the sentence rather than repeating it: the link at the top of this page announces ruzayo homepage. Where the mark stands alone with no wordmark, the whole name goes in the .rz-sr span, because otherwise the link has none at all.

09 Accessible names

Three requirements the stylesheet cannot enforce on its own.

These are markup rules, so they are written into ruzayo.css beside the class they apply to. An implementer reads the stylesheet; they do not read the audit. Each is a requirement, not a recommendation.

Source: ruzayo.css → ACCESSIBLE NAMES · closed at the 1.0 lock, CHANGELOG.md

  1. Requirement 1 · the vessel

    A vessel carries a real figure, so it is an image with a name.

    role="img" and aria-label on the vessel. The figure inside takes aria-hidden="true", because the label already announces it and announcing it twice is worse than not announcing it once.

    <div class="rz-vessel" role="img"
         aria-label="9.8 GB held under herstudio.com">
      <div class="rz-vessel__figure" aria-hidden="true">9.8 GB</div>
  2. Requirement 2 · the table

    A table of divs needs the roles a real table gets for free.

    Every .rz-table takes role="table" and aria-labelledby; every row takes role="row"; header cells take role="columnheader" and body cells role="cell". Any wrapper between the table and its rows takes role="presentation", or the association breaks silently.

    <div class="rz-table" role="table" aria-labelledby="keys-caption">
      <div class="rz-table__head" role="row">
        <span class="rz-table__cell" role="columnheader">Key</span>
      <div class="rz-table__row" role="row">
        <span class="rz-table__cell" role="cell">rz_live_…c241</span>

    Under 600px the grid reflows to one cell per line. Keep the roles and add a visible label per cell: a headerless column is unreadable to a screen reader and to a thumb alike. All three tables on this page do it, from a data-label attribute. Narrow the window and the labels appear.

  3. Requirement 3 · icon-only controls

    A control with no text node has no name.

    Each takes aria-label; the glyph inside takes aria-hidden="true". There is no exception for an obvious icon, because obvious is a visual property and the person who needs the name is not looking at it.

    <button class="rz-btn rz-btn--icon" aria-label="Copy the address">
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">…</svg>
    </button>

    Both are 44px square, which is the touch-target minimum, and both keep the focus ring. focusable="false" is on each SVG because older engines put the graphic itself in the tab order, which would give the control two stops and one name.

Also non-negotiable

Focus is a 2px terracotta outline at 2px offset and is never removed. Nothing important is carried by motion alone. And no state is carried by colour alone — §7 is that rule in five chips.

10 Content rules

The mechanical half of voice.

Formats, not semantics. Every string below is quoted from content-rules.md as a format specimen: it demonstrates a shape. None of them is a statement about what the service does, and the figures inside them are the pack's own examples.

Source: content-rules.md · semantics in CANON.md §9

Dates and times

  • 4 Aug 06:40:12 in the record of events — D MMM HH:mm:ss
  • 4 Aug 06:40 everywhere else — D MMM HH:mm
  • 11 min ago · 4 h ago · yesterday under 48 hours
  • 2 Aug 09:41 older than 48 hours — absolute, never relative
  • next rotation 4 Nov a known future
  • Checking again in 8 s a short future, counting

24-hour clock always. The timezone is named when it is not the reader's — 13:41 CEST. Never just now, never a while ago. No ordinals.

Numbers

  • 61,204 messages thousands separated with a comma
  • 9.8 GB bytes as GB to one decimal. MB only under 0.1 GB. Never KB
  • 1.4 s one decimal under ten seconds, none above
  • 6 minutes in prose, 6 min in a table
  • 2% whole numbers, unless the decimal changes a decision
  • one mailbox a count of one is written out

Tabular figures wherever numbers stack in a column — that is what .rz-fact is for. And where 1.4 s names the measured create-to-listening time, it carries its qualifier every time: only on a name that already listens.

Money

  • €3 / mo symbol, space, slash, space, unit. Not €3/month
  • €0.20 / GB two decimals when there are cents, none when there are not
  • 780–920k SEK salary bands in SEK, thousands, en dash, no space

Every price shown is the real price, including a competitor's.

Addresses, names, records

  • sara@herstudio.com addresses and domains always lowercase
  • mx · spf · dkim · dmarc lowercase in prose and in data
  • MX · SPF uppercase only as a column label or a chip
  • _dmarc DNS hostnames in mono, with the underscore where it is real
  • rz_live_…c241 keys truncated in the middle, never at the end

Never truncate a value a person may need to copy. Wrap it — .rz-table__cell already does. That includes addresses, record values, checksums and message ids.

Capitalisation

Sentence case everywhere: headings, buttons, labels, table headers, notification titles. No title case, ever.

Eyebrows and column headers are uppercase with letter-spacing, which is a type treatment and not capitalisation — the underlying string stays sentence case. Every .rz-label on this page is written in sentence case in the markup, and shouted by the stylesheet.

Pluralisation

Written out, never with a parenthesised s. Zero takes the plural — 0 mailboxes — except where a stated absence reads better, which is most of the time: nothing has arrived yet, not 0 messages.

Empty states

An empty state names what would be there, why it is not, and the one thing that changes it. It never apologises and it never uses an illustration. Absence is a designed output.

Errors

Four parts, in this order: what was expected, what was found, what happens next, and whether it needs the person at all. Never two apologies. Never blame. Never an error code alone, and never an error code hidden.

Two mechanical bans

No exclamation marks in any product string. No emoji in any surface, ever. Neither appears on this page, including inside the specimens above — the pack's own annotation marks were stripped when those strings were quoted, because an annotation is not part of the string.

11 Provenance

What this page is, and what it is not.

A rendering of eleven locked files, not a document with opinions of its own. Everything above cites the file it came from, and the disagreements found while rendering are listed rather than smoothed over.

The five rules that govern the pack, and this page

  1. One source of truth per fact. A value lives in one file; everywhere else cites it.
  2. The data file wins. Where a designed page and a pack file disagree, the file is right.
  3. Every claim traces to the capability matrix. Not a row, not on a public surface.
  4. Absence is stated, never blank.
  5. Marked assumptions are not facts, and may not appear on a customer-facing surface.

What this page does not do

It makes no capability claim, quotes no customer, and prints no measurement of the service. The only figures on it describe the system itself — ten colours, seven type roles, six shapes, four elevations, four durations, five states, eleven measured contrast pairings — plus the format specimens in §10, which are quotations and are labelled as quotations.

Disagreements found while rendering

Disagreements between pack files, found while building this page

Where What disagrees What this page did
tokens.json · ruzayo.css Twelve colours are named as data; ten are named as tokens. Lead and terra deep exist only as literals inside three rules. Rendered the ten through their tokens. Declared the other two once, in this page's own stylesheet, cited to tokens.json.
ruzayo.css · tokens.json .rz-label--accent is terracotta at 12px, and terracotta measures 4.6:1 with the verdict do not use below 16px. Used the slate label for every eyebrow. Showed the accent variant once, raised to the 16px floor.
mailbox.d.ts · ruzayo.css The state is wants_you; the modifier is --wants. Printed both beside the chip in §7, so neither can be guessed wrong.
tokens.json · README.md The pack is one version and nothing in it carries a version of its own. tokens.json carries $meta.version 0.2, refreshed 2026-08-06. Versioned as the pack does: 1.1, locked 7 August 2026. Reported the per-file stamp rather than repeating it.
tokens.json · CANON.md Bone has a meaning and no prohibition in either file. Stated the absence in the swatch, rather than leaving the line blank or inventing a rule.
content-rules.md DNS hostnames are set in mono, and no mono family is named in tokens.json or ruzayo.css. Used the platform mono stack for code and hostnames, and named the gap here.