Design system 2026-10-06
Style guide, measured.
Every token and component of the storefront, rendered from the stylesheet and the snippets that ship. One serif for what is said, one label face for what is named, one mono for what is counted. No rounded corners, no shadows, hairlines instead of boxes. Amber marks commitment only.
01
Colour
Nine tokens, and only the six text pairs that were checked. Amber is never decoration: it is the one commitment button of a view (“Find your formula”, “Make it yours”, add to bag, check out), the tick of the Measure and the progress fill. Links are ink.
paper
--paper #F3F3F0
Page ground. Most surfaces start here.
ink
--ink #17191B
Type, logo, hairlines, the default button.
amber
--amber #A85A17
Commitment only: the one amber button of a view, the tick of the Measure, progress.
glass
--glass #E6D3B8
Tint ground, packaging.
steel
--steel #5F6469
Secondary text.
oil
--oil #D9B46A
Blend bars only (the base oil). Never text.
white
--white #FFFFFF
The chosen size, the label of the bottle.
line
--line #DCDCD6
The quiet hairline between rows.
amber light
--amber-light #D9A45E
Amber on the ink ground only.
Checked text pairs
Ink on paper
15.9:1 · AA
Amber on paper
4.6:1 · AA
Steel on paper
5.4:1 · AA
White on amber
5.1:1 · AA
Ink on glass
12.1:1 · AA
Amber light on ink
7.9:1 · AA
Grounds
.ao-ground .ao-ground--paper | --white | --glass | --ink | --amber set the ground and redefine the contextual colours (--ao-text, --ao-muted, --ao-link, --ao-rule, --ao-focus), so every component stays inside the checked pairs wherever it stands.
02
Type
Newsreader 300 sets headlines, blend names and oil names (350 for lead text), with italic for emphasis and for every INCI name. Archivo Expanded 500 in capitals names things: navigation, eyebrows, buttons, role labels, never under 12 px. Schibsted Grotesk is body and interface text at 16/26. IBM Plex Mono is every number.
Editorial scale
Fluid between 390 px and 1280 px. .ao-display .ao-display--{xxl|xl|lg|md|sm}; h1 to h4 take xl, lg, md, sm.
--text-display-xxl · 48 at 320, 56 at 390, 97 at 1280 px
Your formula, measured to the gram.
--text-display-xl · 40 to 60 px
What’s your hair type?
--text-display-lg · 32 to 42 px
Three steps. One formula.
--text-display-md · 26 to 32 px
The quiz suggests.
--text-display-sm · 22 to 24 px
Argan, weighed to a tenth of a gram.
.ao-lead · Newsreader 350 · 20 to 24 px
A short quiz suggests a formula. You change any oil. We blend each bottle by hand and print its label with your formula and the name you give it.
body · Schibsted Grotesk 400 · 16/26
Six questions about your hair and scalp, about two minutes. We suggest your base oils and your actives, with the percent of each. Emphasis inside body text is the serif italic, never a slanted sans, and .ao-strong is weight 500.
.ao-serif · 20 px · .ao-inci · italic 17 px
Atlas Cedarwood
Cedrus Atlantica Bark Oil
Simmondsia Chinensis (Jojoba) Seed Oil
.ao-eyebrow · Archivo Expanded 500 · 12 px
Custom hair & scalp oil
Signature blendNº 0142
A number inside a label is set in the mono: wrap it in .ao-data or .ao-num.
.ao-data · IBM Plex Mono · 13, 12, 16 px
Question 2 of 7
4 oz (118 mL) · 108.6 g
78.5%
21.4 g of jojoba: .ao-num is a bare number, .ao-u its unit.
$64.00 .ao-price
$64 .ao-price--lg
.ao-meta · .ao-caption
Ratio, percent by weight4 oz · 108.6 g
Rich text
.ao-rte (and Shopify’s own .rte on policy pages): what the editor produces, in the same language.
Refund policy
Every bottle is made to order, so we don't take returns for a change of mind. If yours arrives damaged, leaking or wrong, or you have a reaction, we replace it free or refund you.
What we replace
- A bottle that arrives damaged or leaking.
- A bottle that is not the formula on its label.
Made to order, so no returns for a change of mind. Damaged, leaking or wrong bottle, or a reaction: free replacement or refund.
| Size | Net quantity | Closure |
|---|---|---|
| 1 oz | 30 mL | Dropper |
| 4 oz | 118 mL | Treatment pump |
| 8 oz | 237 mL | Treatment pump |
03
Space, layout, motion
Mobile first at 390 px. Breakpoints: 48em, 64em, 80em. The gutter is 20 px, 32 px from 48em and 48 px from 80em. Sections breathe: --section-y runs from 80 to 149 px.
Spacing steps
Sizes and motion
- Container
- .ao-container84rem + gutters
- Narrow
- .ao-container--narrow42rem
- Control
- --control-h56 px
- Tap target
- --tap44 px
- Header
- --header-h60, 72 px
- Radius
- --radius-1, -20
- Hairline
- --ao-rule, --ao-text1 px
- Motion
- --dur-1, --dur-2150, 250 ms
- Easing
- --ease.2, 0, 0, 1
Only opacity and a small translate move. .ao-rise fades a block in over 6 px when it first scrolls into view; the Measure may fill once (reveal: true). Reduced motion switches all of it off.
This line rises once.
Layout primitives
.ao-section (section padding), .ao-stack --xs --sm --lg --xl, .ao-cluster --between, .ao-grid (auto-fill, --ao-grid-min), .ao-split --5-7 --7-5 (12 columns from 64em, .ao-split__pin keeps a column in view), .ao-divider --strong, .ao-from-lg / .ao-until-lg, .ao-visually-hidden.
.ao-split--5-7
A head that stays, beside a list that runs.
- Base Argan Argania Spinosa Kernel Oil 78.5%
- Active Avocado Persea Gratissima (Avocado) Oil 10%
- Active Baobab Adansonia Digitata Seed Oil 10%
04
Logo
Outlined vectors only (snippets logo-primary, logo-stacked, logo-monogram): the wordmark is never typeset. Clear space is the height of the “A”. The tick follows the ground: amber on light grounds, amber light on ink, ink on amber.
On paper
On glass
On ink
On amber
05
Buttons and links
Wide, quiet, square: the label sits left and a thin arrow sits right, drawn by the stylesheet (the markup is only the label). Ink is the default and every step-advance. Amber is commitment only, at most one per view. Hover moves the arrow 4 px.
.ao-btn.ao-btn--primary
Ink. The default, and every step-advance.
.ao-btn.ao-btn--cta
Amber. Commitment only: at most one per view.
.ao-btn.ao-btn--secondary
Outline. Hover fills it.
.ao-btn.ao-btn--quiet
A label link with no box.
aria-busy="true" with .ao-btn__stack
The label swaps in place, so the button keeps its width; the arrow gives way to a rule that breathes.
Action groups
.ao-actions: stacked and full width on a phone with the quiet link centred under the buttons, a row from 48em. --center centres the row; --stack sets two buttons side by side from 64em.
On the ink ground
Links
A text link is ink with a hairline underline, and hover thickens the rule.
a, .ao-link
06
Fields
A field stands on a rule, like a line in a ledger: the label above in the label face, the value in the serif. Help and error are wired with aria-describedby. The palette has no error hue, so an error is a mark, weight and a doubled rule.
Default, with a placeholder and help.
Give your blend a name.
13 of 28 characters. It’s printed on your label.
.ao-input.ao-input--display
Several lines need their edges: the textarea is the one boxed field.
.ao-check (checkbox)
.ao-check (radio)
.ao-busy · .ao-note
Loading…
Refunds
Every bottle is made to order, so we don't take returns for a change of mind. If yours arrives damaged, leaking or wrong, or you have a reaction, we replace it free or refund you.
07
Option rows
Quiz answers and builder oils are rows on hairlines, never cards. The native radio or checkbox covers the row, so the whole row is the tap area and the keyboard works as the platform defines it. Selected is a filled mark, ink rules and an italic title, never colour alone. One answer has a round mark, several a square one.
One answer, with a leading glyph (.ao-option__lead)
Question 1 of 6Hair & Scalp Oil
What’s your hair type?
Several answers, with a maximum (.ao-options__note)
Question 4 of 6
What should your oil do?
Pick up to two, the most important first.
Oils on a scale (.ao-scale): plate, italic INCI name, weight meter, “Suited to you”, a note
States: hover, focus, off with a reason, selected and off
.ao-options--2: side by side from 48em
.ao-weight: the four-step meter, always beside its words
Weight 1 of 4 Weight 2 of 4 Weight 3 of 4 Weight 4 of 4
08
Steps
A step has a header (a mono count over a serif question), progress or navigation standing on a ruler, and a sticky bar at the foot of the screen. The step title takes focus on every step change.
.ao-step (h1, h2 or h3 for the title)
Step 2 of 5
Choose your base
The base is about 80% of your blend. Bases run from light to rich: 1 is the lightest, 4 the richest.
Quiz progress: {% render 'measure', variant: 'progress', steps: 7, step: 2 %}
Plain progress: value: 43
.ao-stepnav: steps standing on a ruler; the current one carries the amber tick. Only it shows its name until the navigation is 64rem wide.
.ao-actionbar: Back, then the one main action (sticky in a flow)
.ao-actionbar__note: the main action is never switched off. Pressed too early, it says why on the bar's top edge (the strip lies over the page and moves nothing)
.ao-actionbar__summary: the summary bar (the blend at a glance, on a phone)
09
Notices
Guardrail messages come from the rules engine, word for word. The palette has no warning hue, so the three levels differ by their label, their mark and the rule they stand under. A notice may carry one action: the engine’s nearest safe option, one tap.
Needs a change
Argan is out of stock right now, so this blend can’t be ordered as it is.
Worth knowing
That size is sold out right now.
Good to know
Added to your bag.
.ao-notice__title: where the notice is the main thing in its place · .ao-notice__more: a second, quieter sentence · the action may be a link with the same classes, and there may be several · a line and a quiet way round follow the actions
Can’t be reordered as is
We can’t reorder this one as it is.
Castor is out of stock right now. Your quiz answers on this device leave this oil out.
It opens in the builder with the swap already made.
Extra oils (app/ui: <ExtrasNote>): what the price includes and what one more oil adds, as a fact under the quiet rule. No badge, no nudge. It is silent on a price it was not given, and the store’s own wording for a tier is never printed.
Within what is included
Extra oils
3 base oils are included in the price.
The next one is an extra oil, no size chosen yet
Extra oils
3 base oils are included in the price. One more adds $5.00 at 4 oz and $8.00 at 8 oz. A 1 oz bottle holds no extra oils.
The blend holds extra oils, 4 oz chosen
Extra oils
4 active oils are included in the price. This blend has 3 extra oils, which add $15.00 at 4 oz.
Sample prices. A real one is the difference between two variants of the store, never a number in the code.
A size that cannot hold a blend (app/ui: <SizeNotice>): the engine’s sentence and its one tap on. A block, because the blend cannot be added in that size.
Bottle size
Your blend has 3 extra oils. A 1 oz bottle holds no extra oils, so this blend starts at 4 oz.
Bottle size
Your blend has 3 extra oils, and no bottle holds that many yet. Remove an oil to continue.
10
The Measure
A ruler (a tick every 1%, a long tick every 10%) under a ratio bar of a blend’s exact proportions. Segments always come from a real recipe, one per oil with its true share: several bases in the one oil colour, up to five actives, the essential oil, vitamin E. The markup contract is at the top of snippets/measure.liquid; app/ui/Measure.tsx emits the same markup and a parity test holds the two together.
size: 'lg', scale: true, reveal: true · the bar of the result, numbered 0 to 100, filling once
Ratio, percent by weight4 oz · 108.6 g
Default (md)
size: 'sm'
legend: true
- Argan 78.5%
- Avocado 10%
- Baobab 10%
- Atlas Cedarwood 1%
- Vitamin E 0.5%
Three actives, scale: true
Every role: five bases, five actives, the essential oil, vitamin E · size: 'lg', scale: true, legend: true. The bases are one colour, parted by the paper gap; the fourth and fifth actives are the first two colours cut by hairlines. The legend is the text of the bar and the place to find a segment of 1% or 0.5%.
- Jojoba 22.8%
- Grapeseed 18.3%
- Squalane 13.7%
- Argan 9.1%
- Fractionated Coconut 4.6%
- Hemp Seed 9.2%
- Pumpkin Seed 6.9%
- Rosehip 5.8%
- Evening Primrose 4.6%
- Prickly Pear Seed 3.5%
- Rosemary 1%
- Vitamin E 0.5%
Two bases · legend: true
- Grapeseed 52.3%
- Jojoba 26.2%
- Baobab 10%
- Pumpkin Seed 10%
- Sweet Orange 1%
- Vitamin E 0.5%
Two bases and three actives at 24%
Every role, size: 'sm' (the bag line)
The balance
How much of the bottle the actives take, set on a ruler (app/ui: <Balance>; snippets/balance.liquid prints the same markup for one set of figures). The ruler runs over what the rules allow, a tick per step. The marker is the thumb of a real range input laid over what the chosen actives can reach, so arrows, Home and End, a screen reader and a finger move the same thing, and it says both figures: “Actives 24%, base oils 74.5%”. The base oils take what the actives leave. Every figure here is the engine’s answer for a real blend; on this page the marker moves but nothing answers it.
Three actives, at the least, with what else the bottle holds (sample words; the builder writes them from the engine’s figures)
Actives20%
Base oils78.5%
Plus 1% essential oils and 0.5% Vitamin E.
Whole percents. Your base oils take what your actives leave.
Moved up, focus on the marker
Actives24%
Base oils74.5%
Whole percents. Your base oils take what your actives leave.
Two actives reach one share only: the ruler stands still and says why
Actives20%
Base oils78.5%
The actives you picked can make up 20% of the bottle. Add another to go higher.
No share yet (one active, the blend is blocked): rules, never numbers
Actives
Base oils
Actives24%
Base oils74.5%
Whole percents. Your base oils take what your actives leave.
variant: 'rule', tick: 80 · the ruler alone. Outside a container it runs from edge to edge, as an architectural line.
variant: 'beaker', bottle: '1oz'
variant: 'beaker', bottle: '4oz'
variant: 'beaker', bottle: '8oz'
Segment roles
- Base oils, every one --oil
- First active --amber
- Second active --steel
- Third active --glass
- Fourth active --amber, cut
- Fifth active --steel, cut
- Essential oil --ink
- In every blend --white
On white
On glass
On ink
On amber
11
Rows on hairlines
The recipe is set like a printed formulary, a few true counts as a ledger, the oils as a numbered index. An ink rule opens and closes a table; the quiet rule separates its rows. A bottle has four parts (base oils, actives, essential oils, vitamin E): the formulary shows them as heads over its rows, each with its share.
The parts of the bottle · snippets/formulary.liquid, app/ui: <Formulary groups={formularyGroups(formula)}> · the one way a recipe’s structure is shown. A quiet head per part with its share in the mono; under it each oil once, with its percent. The shares sum to 100, and so do the rows.
-
Base oils78.5%
- 01ArganArgania Spinosa Kernel Oil78.5%85.4 gYour base: a medium-light oil.
-
Actives20%
- 01AvocadoPersea Gratissima (Avocado) Oil10%10.8 gYour lead active, tagged for moisture and softness.
- 02BaobabAdansonia Digitata Seed Oil10%10.8 gAn active tagged for moisture, softness and less frizz.
-
Essential oils1%
- Essential oilAtlas CedarwoodCedrus Atlantica Bark Oil1%1 gAn essential oil you picked, at 1%.
-
In every blend0.5%
- AdditiveVitamin ETocopherol0.5%0.6 gIn every blend at 0.5% to slow oxidation.
Total 100% 108.6 g
Two bases · act_label: 'Change' (the way back to a whole part, beside its name; the share keeps the far end)
-
Base oils78.5%
- 01GrapeseedVitis Vinifera (Grape) Seed Oil52.3%56.9 g
- 02JojobaSimmondsia Chinensis (Jojoba) Seed Oil26.2%28.5 g
-
Actives20%
- 01BaobabAdansonia Digitata Seed Oil10%10.8 g
- 02Pumpkin SeedCucurbita Pepo (Pumpkin) Seed Oil10%10.8 g
-
Essential oils1%
- Essential oilSweet OrangeCitrus Aurantium Dulcis (Orange) Peel Oil1%1 g
-
In every blend0.5%
- AdditiveVitamin ETocopherol0.5%0.6 g
Every role: five bases, five actives, in grams for 8 oz. A role under a head is the oil’s place in its part. From 64em a formulary sets its figures in two columns beside the names, so it wants a column of 36rem or more there.
-
Base oils68.5%
- 01JojobaSimmondsia Chinensis (Jojoba) Seed Oil22.8%49.7 g
- 02GrapeseedVitis Vinifera (Grape) Seed Oil18.3%39.9 g
- 03SqualaneSqualane13.7%29.9 g
- 04ArganArgania Spinosa Kernel Oil9.1%19.8 g
- 05Fractionated CoconutCaprylic/Capric Triglyceride4.6%10 g
-
Actives30%
- 01Hemp SeedCannabis Sativa Seed Oil9.2%20.1 g
- 02Pumpkin SeedCucurbita Pepo (Pumpkin) Seed Oil6.9%15.1 g
- 03RosehipRosa Canina Fruit Oil5.8%12.7 g
- 04Evening PrimroseOenothera Biennis (Evening Primrose) Oil4.6%10 g
- 05Prickly Pear SeedOpuntia Ficus-Indica Seed Oil3.5%7.6 g
-
Essential oils1%
- Essential oilRosemaryRosmarinus Officinalis (Rosemary) Leaf Oil1%2.1 g
-
In every blend0.5%
- AdditiveVitamin ETocopherol0.5%1.1 g
.ao-formulary.ao-formulary--plates as one list: role, plate, oil, italic INCI name, percent, grams, one line of reason · .ao-formulary__act: the quiet way back to where an oil was chosen (what is in every blend has none, and takes the single drop as its plate)
- Base Argan Argania Spinosa Kernel Oil 78.5% 85.4 g Your base: a medium-light oil.
- Active Avocado Persea Gratissima (Avocado) Oil 10% 10.8 g Your lead active, tagged for moisture and softness.
- Active Baobab Adansonia Digitata Seed Oil 10% 10.8 g An active tagged for moisture, softness and less frizz.
- Essential oil Atlas Cedarwood Cedrus Atlantica Bark Oil 1% 1 g An essential oil you picked, at 1%.
- In every blend Vitamin E Tocopherol 0.5% 0.6 g In every blend at 0.5% to slow oxidation.
.ao-total: the two sums that close a formulary · .ao-inci-list: the ingredient list, highest to lowest · .ao-specs: a label and a figure per hairline, for what a bottle is (never for facts a line above already said) · .ao-disclosure: a serif line between two hairlines, plus when closed, minus when open. Where it opens a fuller form of a list, it stands over that list, so nothing above it moves
Total 100% 108.6 g
Argania Spinosa Kernel Oil, Persea Gratissima (Avocado) Oil, Adansonia Digitata Seed Oil, Cedrus Atlantica Bark Oil, Tocopherol
- Bottle
- 4 fl oz (118 mL)
- Closure
- Treatment pump
- Fill
- 108.6 g
Full formula
The formulary, its total and the ingredient list stand here.
.ao-ledger: counts from the oil library
- Base oils
- 05
- Actives
- 05
- Scents
- 07
- Sizes, in oz
- 1 · 4 · 8
.ao-index: the numbered oil index, with plates and the weight meter
Base oils05
-
01
Grapeseed
Vitis Vinifera (Grape) Seed Oil
-
02
Jojoba
Simmondsia Chinensis (Jojoba) Seed Oil
-
03
Argan
Argania Spinosa Kernel Oil
-
04
Fractionated Coconut
Caprylic/Capric Triglyceride
-
05
Sweet Almond
Prunus Amygdalus Dulcis (Sweet Almond) Oil
12
Size and quantity
The size picker is three joined cells on hairlines, one native radio each; the chosen one turns white with an ink edge and an italic label. The quantity stepper is three 44 px cells in one ink frame.
.ao-sizes · .ao-size
Sample prices. Real prices always come from the Shopify variant.
.ao-stepper: minus is off at one bottle
13
The bottle
The drawn bottle is the customer’s own: an amber Boston round with the label of docs/labels.md, their blend name on it and their recipe in the beaker. It stays for the personal moments (result, builder summary, name step, bag). A photograph of a bottle is never captioned as the customer’s. Snippet bottle.liquid and app/ui/Bottle.tsx are generated from one geometry.
{% render 'bottle', size: '1oz' | '4oz' | '8oz', name: …, recipe: … %} · drawn here to one scale
The beaker of a blend with two bases: one oil colour, the second base standing on a hairline as heavy as the gauge
Every role: five bases, five actives. The fourth and fifth actives are the first two colours cut by hairlines of the label’s white, drawn by the bottle itself.
The name on the label: blank, short, wrapped, and one word wider than the label (squeezed, never broken)
.ao-blendname: the blend-name moment (app/ui: <BlendName> keeps the label in step with every keystroke)
13 of 28 characters. It’s printed on your label.
.ao-summary: the summary column of a step flow (ao-app.css)
Your blend
- Base oils
- Argan78.5%
- Actives
- Avocado, Baobab20%
- Essential oils
- Atlas Cedarwood1%
- In every blend
- Vitamin E0.5%
Price, 4 oz$64
.ao-bottle-stage--label: the label close up, wherever a name is typed beside it or the column is narrow (the name is drawn at 13 px or more)
shadow: true · .ao-bottle-stage: the pale ground it stands on
14
Photographs
Fifty-five frames, listed with their size, alt text and intended use in data/shoot.json. The shot snippet (and app/ui/Shot.tsx) turns a frame id into a responsive image: srcset through asset_img_url, width and height so nothing shifts, alt text from the manifest, lazy unless it is the one eager image of the page. Generated bottles are unlabelled stand-ins: never caption one as the customer’s bottle.
{% render 'shot', id: 'ruler', sizes: '(min-width: 64em) 50vw, 100vw' %}
{% render 'shot', id: 'pour', ratio: '4 / 5', sizes: '(min-width: 64em) 33vw, 100vw' %}
{% render 'shot', id: 'plate-argan', sizes: '72px', class: 'ao-plate', decorative: true %}
{% render 'shot', id: 'hero-portrait', wide_id: 'hero-wide', sizes: '100vw', loading: 'eager' %}
Ingredient plates: plate-<oil id>, wherever an oil is chosen or listed (.ao-plate, .ao-plate--sm)
15
Header and footer
The header at the top of this page and the footer at its end are the real sections. Text only: navigation left, the logo in the centre, “Bag (0)” right. Below 80em the navigation is a full-screen menu, a modal dialog in the language of the oil index; a menu of more than three links takes a centred row of its own under the logo. The quiz and the builder have the same header, so the menu stays in reach mid-flow.
The rows of the full-screen menu (.ao-menu__list)
16
Bag
The bag drawer and the bag page share their lines and their totals (snippets cart-line and cart-content, styles in ao-cart.css). A line is drawn from a real bag item, so there is no static copy here: add a blend and open the bag from the header. The quantity stepper is under “Size and quantity”, the drawn bottle under “The bottle”.
Bag line
- .ao-cart-line · the drawn bottle at the size chosen, with the blend name on its label; the product as eyebrow, the name in the serif italic, size and prices in the mono.
- The small Measure and the Formula line are the recipe as the order carries it. Then the stepper, “Edit blend” and a quiet “Remove”: all ink, no amber.
- The one amber button of the bag is “Check out”, with the refund note directly above it.
Drawer
- dialog.ao-drawer · a sheet from the bottom below 48em, a 31rem panel on the right above. It does not move under reduced motion.
- .ao-drawer__head · title, item count and Close, set like the header's Menu and Bag.
- .ao-drawer__body · scrolls; the refund note always sits directly above Check out.
- .ao-cart__error · one line at the top when a change could not be saved.
Open it from the bag link in the header to see it with the bag as it is now.