# Colour — Aleris Brand OS concept bundle

Sources in order; each file follows as it stands in the corpus, frontmatter first.

<!-- source: principles/colour.md · status: accepted -->

---
title: Colour
lang: en
status: accepted
version: 1
type: principles/identity-element
layer: principle
depends_on:
  - constitutional/accessibility-is-foundational
propagates_to:
  - data-products/tokens/*
  - communication/*
  - physical/*
last_verified: 2026-08-11
last_updated: 2026-08-11
translation_status: source
translation_source: null
---

# Colour

## Why the palette looks the way it does

The Aleris colour palette is warm-shifted. Even the greys lean towards brown rather than blue. This is a deliberate choice. The brand promise — the present expert — requires everything we do to feel human and present, not institutional. Petrol gives medical credibility without becoming cold corporate blue. Orange is warm and action-oriented without becoming alarming red. Sand is calm and natural — not clinical white.

Three colour families, three roles. The palette communicates before a single letter is written.

---

## The palette

### Petrol — structure and trust

Petrol carries text, headings, navigation, and structural surfaces. It is the colour that says: we know what we are doing.

| Step | HEX | Role |
|------|-----|------|
| petrol-500 | #004851 | Primary text, headings, navigation, header backgrounds |
| petrol-400 | #4F868E | Structure and accents. Large text only, from 18 pt or 14 pt bold. Not for body text — 4.09:1 on white |
| petrol-300 | #7FA9AE | Borders, dividers, decoration. Never text — 2.56:1 on white, 2.19:1 on sand-100. Dividers between list items only in lists of six or more |
| petrol-200 | #ABC7C9 | Borders, inactive states |
| petrol-100 | #D9E1E2 | Tinted backgrounds, highlighted rows, tags |

### Orange

Orange represents the warm, approachable and active side of Aleris.

While Petrol is the informational and rational frame for our brand, Orange is our emotional layer that drives attention and action.

#### Brand orange – making Aleris come alive

The orange gives our brand a vibrant, warm character, and its brightness serves as our highlighting feature. It's decorative as well as functional. Used in its softer tints, it can create calm and soothing spaces. At full brightness, it's an accent that commands attention and drives action.

#### Interactive orange – action and warmth

Interactive orange is used for primary actions in digital interfaces. It marks priority actions that can be taken right now, on primary buttons and controls. The deeper shade of our warm coral brand orange makes it easy to spot, even in the presence of Brand orange.

The ramp holds both roles. The boundary inside it runs two ways: brand against interactive, and print-and-digital against digital-only.

| Step | HEX | Role |
|------|-----|------|
| orange-700 | #B23C0E | Interactive – hover/active on the primary interactive surface (the darker step). Digital only |
| orange-600 | #D14811 | Interactive – primary interactive surface (CTA fill), active states. Digital only |
| orange-500 | #F58C61 | Brand – accent, decorative, print. The brand orange |
| orange-400 | #FAAA8D | Brand – accent tint, backgrounds and surfaces. Print and digital. Never an interactive state |
| orange-300 | #FFBE9F | Brand – light accent backgrounds. Print and digital |
| orange-200 | #FBD1C0 | Brand – soft tint. Print and digital |
| orange-100 | #FDE8DF | Brand – lightest orange tone. Print and digital |

### Sand — the surrounding layer

Sand is the page background. It creates the calm that everything else rests against. White is for cards and surfaces — what is lifted up from the background — and for sections layered on the sand.

| Step | HEX | Role |
|------|-----|------|
| sand-100 | #F2ECE4 | Page background — pages someone reads |
| sand-50 | #FAF8F6 | Page background — surfaces someone works in |
| sand-300 | #E7CEB5 | Mid-sand |
| sand-500 | #D9B48F | Warm amber. Also = warning colour (context decides) |

### Neutral tones

| Name | HEX | Role |
|------|-----|------|
| gray-100 | #D7D2CB | Borders, dividers, inactive elements. Dividers between list items only in lists of six or more |
| gray-300 | #9E9281 | Mid-tone, warm grey |
| gray-500 | #585044 | Dark warm grey. Secondary text |
| White | #FFFFFF | Card, surface and section background. Never the page's own background |

The renumber merged two older names into the scale: *Slate* is now gray-100, and *Sand dark* is now sand-500.

**Physical environment:** the domain's everyday language *"warm white"* and *"light grey"* are material descriptions, not new palette members — they map to sand-50 and gray-100 above. See `workspace/decisions/decision-record-physical-interior-2026-08-06.md` §3.

### Pantone and CMYK

The print values are constant — the same values regardless of printer or substrate. Physical documents how to specify for different materials.

**Primary colours:**

| Colour | Pantone | CMYK | sRGB | HEX |
|------|---------|------|------|-----|
| Petrol | 316 C/U | C98 M48 Y49 K46 | 0, 72, 81 | #004851 |
| Orange | 2024 C/U | C0 M56 Y62 K0 | 245, 140, 97 | #F58C61 |
| Sand | N/A | C6 M7 Y11 K0 | 242, 236, 228 | #F2ECE4 |

**Secondary colours — petrol:**

| Step | Pantone | CMYK | sRGB | HEX |
|------|---------|------|------|-----|
| petrol-400 | 5483 C/U | C70 M31 Y37 K12 | 79, 134, 142 | #4F868E |
| petrol-300 | 5493 C/U | C54 M21 Y30 K3 | 127, 169, 174 | #7FA9AE |
| petrol-200 | 5513 C/U | C38 M12 Y21 K0 | 171, 199, 201 | #ABC7C9 |

**Secondary colours — orange:**

| Step | Pantone | CMYK | sRGB | HEX |
|------|---------|------|------|-----|
| orange-700 | Digital only | Digital only | 178, 60, 14 | #B23C0E |
| orange-600 | Digital only | Digital only | 209, 72, 17 | #D14811 |
| orange-400 | 2022 C/U | C0 M38 Y40 K0 | 250, 170, 141 | #FAAA8D |
| orange-300 | – | *not yet built* | 255, 190, 159 | #FFBE9F |
| orange-200 | – | C0 M24 Y23 K0 | 251, 209, 192 | #FBD1C0 |

**Not every step has print values.** Interactive orange – orange-600 and orange-700 – exists to hold contrast on screen, so it has no print counterpart by design. Where a Pantone cell is empty, no Pantone match exists at that tint. In print, the brand orange is orange-500.

*Two utility notes, drafted by Claude 2026-07-30, correct on sight. The empty Pantone cells above are marked differently from the legacy teal further down this page: teal has a Pantone match that is simply unconfirmed and awaiting a press check, while orange-300 and orange-200 have no match to find. And orange-300's CMYK reads "not yet built" rather than "–", because unlike a Pantone match a CMYK breakdown is derivable from the hex and validated at press: it is a value to produce, not an absence to record. It has not been computed here, because producing a print value is a press decision rather than an arithmetic one.*

**Secondary colours — sand:**

| Step | Pantone | CMYK | sRGB | HEX |
|------|---------|------|------|-----|
| sand-500 | 727 C/U | C0 M18 Y36 K15 | 217, 180, 143 | #D9B48F |
| sand-300 | 2309 C/U | C0 M11 Y22 K9 | 231, 206, 181 | #E7CEB5 |

**Neutral:**

| Colour | Pantone | CMYK | sRGB | HEX |
|------|---------|------|------|-----|
| gray-100 | Warm Gray 1 C/U | C0 M2 Y5 K16 | 215, 210, 203 | #D7D2CB |

### Gradients

Gradients carry vibrancy and character where solid petrol or orange would feel heavy. They belong on background surfaces and in visual elements — and the petrol gradient may also fill display type.

| Gradient | Dark stop | Light stop | Use |
|----------|-----------|------------|------------|
| Petrol | `--color-petrol-500` | `--color-petrol-400` | Linear or radial, max two steps |
| Orange | `--color-orange-500` | `--color-orange-300` | Linear or radial, max two steps |

*(Stops named rather than restated, 2026-09-05, per `constitutional/tokens-are-canonical.md` § 1. They were hex here, which is the shape that rule forbids in a reference table.)*

Rules: low-contrast transitions; avoid in charts where flat colours give better clarity; WCAG contrast against text must be met; never distract or dominate.

#### The petrol gradient on display type

*(Ruled 2026-09-05, Torfinn. Prose below drafted by Claude — the rule is his, the wording is a draft.)*

**The petrol gradient may fill h1 and h2. No other gradient may fill text, and no smaller type may carry one.**

**Why h1 and h2 and not further down.** Two limits, and they are not the same limit. The outer one is measured: a gradient's *lightest* stop is what has to clear contrast, and petrol's light stop is `--color-petrol-400`, which reads 3.49:1 on sand-100, 3.86:1 on sand-50 and 4.09:1 on white. That clears the 3:1 floor WCAG sets for large text and clears nothing below it, so no gradient can ever fill body text, a label, or a caption. WCAG counts anything from 24px as large, which would admit h3 at 27px. **The rule stops at h2 by choice, not by measurement** — a gradient is a device for the one line that carries the page, and it stops meaning that as soon as it appears three times.

**The petrol gradient may be used on icons on white, sand and petrol-100 grounds, and never on an orange-300 or petrol section. No other gradient is used on icons.** An icon is a non-text graphic and needs 3:1 against its ground, and a gradient is only as strong as its lightest stop. `--color-petrol-400` reads 4.09:1 on white, 3.86:1 on sand-50, 3.49:1 on sand-100 and 3.08:1 on petrol-100, and falls to 2.56:1 on orange-300. On a petrol section the dark stop is the weak one, and `--color-petrol-500` on petrol-500 is 1:1. The orange gradient's light stop, `--color-orange-300`, reaches at most 1.60:1 on white and sand.

**In a presentation, the petrol gradient may fill the heading of a title slide or a section slide, and not every slide title.** A slide has no h1, and the deck is the page: the heading that carries it is the one on the slide that opens it or opens a part of it. On every slide of a twenty-slide deck the gradient would appear twenty times, and it stops marking the line that matters as soon as it appears three times.

**No cap on how many things carry a gradient.** Consistency within a surface is a design judgement, not a counted rule; the corpus does not set a number and should not be read as implying one.

**The orange gradient is never text.** Its light stop is `--color-orange-300`, at 1.36:1 on sand-100. It fails the large-text floor by a wide margin on every Aleris ground, so the restriction is arithmetic rather than preference.

**Both stops of a gradient are the foreground.** Read a gradient by its worst stop, never its average and never its darkest — a reading that passes on the first stop and fails on the last has not passed. The same applies to a gradient used as a ground: what sits on it is measured against the stop that gives the least contrast, not against the colour the block appears to be.

---

## Status colours

Status colours communicate state — never decoration. They are used only when something has happened, is happening, or needs attention.

| Status | HEX | Role |
|--------|-----|------|
| Error | #C14444 | Validation errors, destructive actions |
| Warning | #D9B48F | Attention states (= sand-500 – context distinguishes them) |
| Confirmation | #4F866E | Completed, approved. Large text or icon only — 4.23:1 on white, and never carry the state by colour alone |
| Information | #004851 | Informative messages (= petrol — info is neutral brand communication) |

The warning colour shares its HEX value with sand-500. This is deliberate – in a status context it reads as warning, in a background context as warm sand. The ambiguity is resolved by context, not by adding another colour.

---

## Hard rules

**The page is sand, and everything is layered on it.** The page's own background is always sand. A section can be white, light petrol, light orange or, once on a page, petrol — but it is a layer on top of the sand, even when it fills the screen. Cards are white on any section. The difference between ground and layer creates the visual hierarchy that lets the page breathe: sand for the ground, white and the section colours for what sits on it.

**One primary button per surface.** Primary buttons can have two visual designs depending on context. They are either orange on light surfaces or white on dark surfaces. We never have two primary buttons on the same surface, because that forces the user to choose between two equally strong signals. Use secondary buttons or text links for other options.

**Colour never carries meaning alone.** The rule is constitutional – see `constitutional/accessibility-is-foundational.md`. It matters more for this palette than for most, because our colours are all warm: under red-green colour blindness they collapse onto a single yellow-khaki band, and hue stops distinguishing anything. Error red and success green become near-identical, so two states that mean opposite things look the same. Colour blindness is not the only case. Hue is also lost in ordinary reading conditions, for people with full colour vision – a phone in sunlight, a dimmed screen, a greyscale printout, an ageing lens. And colour has to be matched against a legend, which a worried or unwell reader has the least capacity to do. Text and icons carry their meaning with them.

*(**There is no "no dark mode" rule, and one cannot be written.** Respecting a user's local display settings — dark mode, high contrast, dynamic text size — is a legal requirement, the same class of obligation as the WCAG floors elsewhere on this page, not a brand preference to override. A positive rule covering how Aleris presents in dark mode is pending; until it exists, no hard constraint governs this.)*

**Teal is legacy, not retired.** Teal (formerly called "turquoise") is a named palette colour at *legacy tier*. Petrol is the primary in every market, including Norway. Teal is acknowledged as Aleris history and kept in the palette because it remains strongly present in Norway — where the move to petrol has been slow and teal still appears in ways that don't match the current profile. See *Legacy colours* below.

**Cream does not exist.** Replaced by sand-50 (#FAF8F6). Use sand-50 for surfaces someone works in (workspaces, tools) and sand-100 (#F2ECE4) for pages someone reads (landing pages, patient material).

**Never use arbitrary HEX values.** Every colour in the palette has a purpose and a name. Introducing your own shades — "almost petrol", "a slightly lighter orange" — breaks the consistency. The palette *is* the brand. *(The legacy tier below is the one named exception: teal is part of the palette, not an arbitrary colour.)*

---

## Legacy colours

**Teal** (formerly called "turquoise") is the former Aleris primary colour. It is not retired — it sits at a **legacy tier**: petrol is now the primary across every market, and teal is kept in the palette for a transitional period, acknowledged as part of Aleris's visual history.

Teal's *status* is the same everywhere — legacy, not primary. What differs by market is how strongly it lingers in the real world:

| Market | Teal in practice |
|---|---|
| Norway | Strong lingering presence — heavy in physical environments (signage, interiors) and still used in ways that contradict the current profile. The move to petrol has been slow; teal is kept largely to respect this attachment while the transition continues. |
| Sweden | Faint legacy trace; new work uses the core palette. |
| Denmark | Faint legacy trace; new work uses the core palette. |

| Colour | HEX | sRGB | CMYK | Pantone | Tier |
|---|---|---|---|---|---|
| Teal | #00C6B2 | 0, 198, 178 | C100 M0 Y10 K22 *(computed — validate at press)* | *To match at press (unconfirmed; a teal in this range sits near Pantone 326 / 3262 C)* | Legacy |

Handling — **replace on touch.** New work in every market uses petrol, sand, and orange; teal is not specified for new work. Existing teal is replaced with the core palette **when a surface, asset, or template is next updated** — there is no campaign to remove it ahead of that, and in the meantime it is treated as heritage, not scrubbed reactively. Most of what remains is in Norway.

**Physical environment:** *"on touch"* is read narrowly — only the surface actually being renovated counts as touched. Signage is the deliberate exception: the whole signage programme is the unit of touch, not the individual sign, so a single sign replaced spontaneously reuses the existing design rather than introducing petrol in isolation and creating an unplanned mixed style. See `workspace/decisions/decision-record-physical-interior-2026-08-06.md` §4.

---

## Accessibility

WCAG AA requires 4.5:1 contrast for normal text and 3.0:1 for large text (≥18pt or ≥14pt bold) and UI components.

### Strong pairs

| Pair | Contrast | AA text | AA large text |
|-----|----------|---------|--------------|
| petrol-500 on sand-100 | 8.75:1 | ✅ | ✅ |
| petrol-500 on sand-50 | 9.69:1 | ✅ | ✅ |
| petrol-500 on white | 10.27:1 | ✅ | ✅ |
| White on petrol-500 | 10.27:1 | ✅ | ✅ |
| gray-500 on sand-100 | 6.76:1 | ✅ | ✅ |
| gray-500 on white | 7.94:1 | ✅ | ✅ |
| White on orange-600 | 4.52:1 | ✅ | ✅ |
| White on orange-700 | 5.92:1 | ✅ | ✅ |
| petrol-500 on petrol-100 | 7.73:1 | ✅ | ✅ |

petrol-500 on sand or white is the standard pair. It clears every threshold with a good margin.

### Pairs that require judgement

| Pair | Contrast | AA text | AA large text | Guidance |
|-----|----------|---------|--------------|------------|
| petrol-500 on orange-500 | 4.30:1 | ❌ | ✅ | Works for large text and UI components. No longer the recommended button |
| gray-300 on white | 3.05:1 | ❌ | ✅ | Large text only. Not for body text |
| gray-300 on sand-100 | 2.60:1 | ❌ | ❌ | Decoration only |
| petrol-300 on sand-100 | 2.19:1 | ❌ | ❌ | Decoration only |
| Confirmation (#4F866E) on white | 4.23:1 | ❌ | ✅ | Close to AA — always complement with an icon |
| Warning (#D9B48F) on white | 1.93:1 | ❌ | ❌ | Never as a text colour. Use as border or background with petrol text |

### Figure-ground: a fill against its surface

WCAG 1.4.11 requires 3:1 between a component and the surface behind it.

| Pair | Contrast | | 
|-----|----------|---|
| orange-600 on sand-100 | 3.85:1 | ✅ figure-ground fine |
| orange-600 on petrol-500 | 2.27:1 | ❌ never – this is why the primary button inverts on petrol |

### Retired

| Pair | Contrast | |
|-----|----------|---|
| White on orange-500 | 2.38:1 | the production failure this supersede removes |

### The interactive orange

**White text on the interactive orange meets accessibility requirements.** Interactive orange (#D14811) with white text gives 4.52:1. The minimum-size workaround this page previously described is no longer needed, and petrol text on orange is no longer the recommended button.

**An orange fill is never placed on a petrol surface.** Interactive orange on petrol gives 2.27:1, below the 3:1 that WCAG 1.4.11 requires between a component and its background.

**The colour is constant. The treatment follows the surface.** On light surfaces – sand, sand 50, white – the primary button is an interactive orange fill with white text. On petrol it inverts to a white fill with petrol text.

**Both variants darken on hover.** On a light surface the primary button darkens to #B23C0E and white text rises to 5.92:1. The inverted button already sits at the palette's highest contrast, so darkening lowers it: the fill moves to a petrol tint (#D9E1E2) at 7.73:1, a visible change that stays well above the floor. This replaces the earlier rule that hover always lightens, which could not hold – under white text a lighter fill always reduces contrast.

### Why colour cannot carry meaning on its own

Using colour as the only carrier of information fails WCAG 1.4.1. That is a Level A criterion – the lowest conformance bar, below everything else on this page.

For this palette the problem is specific. Every Aleris colour is warm, and warm colours converge under red-green colour blindness. An independent simulation of deuteranopia and protanopia (Machado 2009) puts the whole palette onto a single yellow-khaki band, where hue no longer distinguishes anything. The status pair is the worst case: error red (#C14444) and success green (#4F866E) sit ΔE 76.9 apart in normal vision and ΔE 14.6 apart under protanopia, and success stops reading as green at all. Two states that mean opposite things become near-identical.

Around 8% of men have red-green colour blindness, but the reasons colour fails are wider than that. Hue is also lost in ordinary reading conditions, by people with full colour vision: a phone screen in sunlight, a device dimmed for battery or for night, a page printed or photocopied in greyscale, an older reader whose lens has yellowed. Most of our traffic is mobile, so these are the normal conditions rather than the edge cases. And colour has to be matched against a legend held in memory, which is what a worried, tired or unwell reader has least of.

This is a requirement about difference. Pairing colour with an icon is not enough on its own if the icons do not tell the states apart – seeing that something has a state is not the same as knowing which state it is. Error and success must be distinguishable from each other without colour.

---

## Contextual: how the palette adapts

The palette is constant. The composition is contextual.

A landing page for patients uses sand generously, petrol as the text colour, and orange as an occasional accent. The feel is calm and accessible. A clinical interface for staff compresses the palette — petrol takes more space, sand recedes, information density increases. A printed patient brochure can give sand a dominant role to create the calm the format allows.

Same colours, different composition. What governs the composition is the recipient's emotional mode, the conditions of the channel, and the nature of the content. See *Emotional Mode as a Design Entry Point* and *Constant and Contextual* for the framework.

Baseline (digital), Communication, and Physical document how the composition is applied in their context.

---

## Quick test

Before you make a colour decision:

1. **Is the colour in the palette?** If not — don't use it. Never introduce your own shades.
2. **Are you using the colour in the right role?** Petrol for structure, orange for action, sand for background, status only for state.
3. **Does the combination meet the accessibility requirements?** Check the contrast table above, and check the second carrier – the colour-alone rule is constitutional, see `constitutional/accessibility-is-foundational.md`.

---

*The palette communicates before a single letter is written. Petrol, sand, and orange — in the right composition, in the right context — are Aleris. Swapping them out, mixing in your own shades, or ignoring their roles doesn't make the material more creative. It makes it less recognisable.*


<!-- source: constitutional/colour-is-the-aleris-palette.md · status: accepted -->

---
title: Colour is the Aleris palette
layer: constitutional
status: accepted
normative: true
owner: Head of Design
scope: all Aleris colour use — digital, print, physical
version: v1.1
lang: en
depends_on: []
propagates_to:
  - foundation/*
  - baseline/*
updated: 2026-09-24
supersedes: principles/colour.md (in part — the non-negotiable core)
---

# Colour is the Aleris palette.

> The palette communicates before a single letter is read. This file holds only what makes a colour choice *not Aleris*. The roles in full, the composition logic, and the accessibility detail live in `principles/colour.md`.

---

## 1. The palette is petrol, sand, and orange — three families, three roles.

The palette is petrol, sand, and orange – three families, three roles. Petrol carries structure and trust. Orange drives emotion, attention and action. Sand is the surrounding calm everything rests against.

**Implication:** A colour decision starts by naming the role, then the family. Off-palette or off-role is off-brand.

---

## 2. The non-negotiables

These are the choices that, if broken, make a surface not-Aleris.

- We use only the named palette colours. Never an arbitrary hex, never a "nearly-petrol" or "slightly lighter orange." The palette *is* the brand. *(Bounded instances of this one rule: "cream" does not exist — use sand 50. Teal — the former primary, once called "turquoise" — is **not** retired; it is a named palette colour at legacy tier. Petrol is primary in every market; teal is kept for a transitional period, mainly for its lingering presence in Norway. See `principles/colour` → Legacy colours.)*
- The background is a light sand that everything lives on top of. Cards and sections can have different colours, but they are all situated on sand. The page's own background is always sand; a white or coloured section is a layer on top of it, even when it fills the screen.
- The accessibility rules that involve colour (never information by colour alone; the contrast floors) are **referenced, not restated** — see [[accessibility-is-foundational]]. The sort routed them to a shared home so they live once, not once per page.

*(**There is no "no dark mode" rule here, and there cannot be one.** Respecting a user's local display settings — dark mode, high contrast, dynamic text size — is a legal requirement, the same class of obligation as the accessibility rules above it, not a branding question this page can answer. A positive rule on how Aleris presents in dark mode is pending, and it belongs in `principles/colour.md` rather than in this file.)*

**Implication:** These hold across every channel and composition. How the palette is *distributed* is contextual and lives in the essay; *which* colours exist, and where the ground sits, do not bend.

---

## Detection of violation

- **In a token audit.** Any hex not in the token set is a violation, caught automatically by the token-conformance check in `data-products/tokens/tokens.test.ts` (`npm test`). The token set of record is the `--color-*` declarations in `data-products/tokens/aleris-tokens.css`. Values already outside it are listed in the check as documented violations with a reason each, rather than being waved through.
- **In design review.** White page-grounds and off-role colour use are returned, with the failing element named. (Dark-mode surfaces are no longer a violation — see the note under §2.)
- **On a live surface.** A breach is logged against this file and fed back to `principles/colour.md` as evidence.

There is no override for the palette rule. *(WCAG-AA contrast is a bright line here, not a calibrated default: there is no documented exception to it. The rule itself lives at [[accessibility-is-foundational]]; its measurable floor is the fitness check in `data-products/tokens/tokens.test.ts`.)*

---

## Related

- [[principles/colour]] — the essay: palette tables, roles, composition, full accessibility guidance
- [[accessibility-is-foundational]] — the shared accessibility non-negotiables
- [[constant-and-contextual]] — the palette is constant; its composition is contextual


<!-- source: data-products/tokens/aleris-tokens.css · status: none -->

```css
/* ==========================================================================
   Aleris Design Tokens — Canonical Reference
   ==========================================================================
   Source of truth for all Aleris digital products.
   Three-layer architecture: Primitives → Semantic → Component.

   Figma is the upstream source for primitives. This file consolidates
   Figma exports with documented design decisions. Where values differ
   from Figma, a FIGMA-UPDATE comment marks what needs syncing back.

   Last updated: May 2026
   Maintainer: Torfinn Almers, Head of Design, Aleris Group
   ========================================================================== */

/* Companion stylesheet for Museo Sans @font-face declarations:
     baseline/tokens/aleris-fonts.css
   Load order: tokens, then fonts. Fonts file references --font-family-primary
   defined here. */


/* --------------------------------------------------------------------------
   LAYER 1: PRIMITIVES
   Raw values. No semantic meaning. Platform-independent.
   Naming convention: category.group.scale (100=light/tint, 300=mid, 500=full)
   -------------------------------------------------------------------------- */

:root {

  /* --- Colors: Brand --- */
  --color-petrol-100: #d9e1e2;   /* @usage Tinted backgrounds, selected row, inverted primary hover fill | @constraint Never as text color */
  --color-petrol-200: #abc7c9;   /* @usage Tinted backgrounds a step down from petrol-100 | @constraint Never as text color – too light */
  --color-petrol-300: #7fa9ae;   /* @usage Secondary accents, hover tints | @constraint Not for body text – too light. Never behind white text: 2.56:1 */
  --color-petrol-400: #4f868e;   /* @usage Mid petrol for structure and accents | @constraint Not for body text */
  --color-petrol-450: #0f6b73;   /* @usage The far stop of --surface-gradient-cold, and the petrol to reach for on any petrol ground light text sits on | @constraint Every light text role clears the 4.5:1 body floor on it — that is what this value is for. Deliberately NOT on the petrol-500 to petrol-400 interpolation line, so the gradient does look different; that is the cost of the wider floor, not an oversight. Value taken from sundviktlakemedel, which minted this name first. Card 156 */
  --color-petrol-500: #004851;   /* @usage Primary text, headings, structural elements | @constraint Default text color. Only specify when deviating */
  --color-petrol-700: #003238;   /* @usage Secondary/confirm button press fill | @constraint Interaction only, never decorative or print. Card 19 */

  --color-sand-50: #faf8f6;     /* @usage Page background for instrumental surfaces | @constraint Tools, admin, dashboards only. Communicative uses sand-100 */
  --color-sand-100: #f2ece4;    /* @usage Page background for communicative surfaces | @constraint Patient-facing, marketing, booking. Never use white as page bg */
  --color-sand-300: #e7ceb5;
  --color-sand-500: #d9b48f;

  /* --- Colors: Orange ---
       Seven steps. The ramp splits by job, not only by lightness: 100-500 are
       brand tints used in print and digital, 600-700 are interaction fills that
       exist to hold contrast on screen and have no print counterpart by design.
       SUPERSEDE 2026-07-29: orange-500 is no longer the primary button fill –
       white text on it fails AA. The interactive role moved to orange-600/700.
       orange-400 and orange-200 were briefly retired on 2026-07-28 and
       reinstated on 2026-07-29: both carry print specifications, so the
       retirement was decided on the digital half of the palette only. */
  --color-orange-100: #fde8df;
  --color-orange-200: #fbd1c0;   /* @usage Soft tint, backgrounds and surfaces. Print and digital | @constraint Decorative/background only, not for interactive states */
  --color-orange-300: #ffbe9f;   /* @usage Light accent backgrounds, soft orange tints | @constraint Decorative/background only, not for interactive states */
  --color-orange-400: #faaa8d;   /* @usage Accent tint, backgrounds and surfaces. Print and digital | @constraint Decorative/background only — never an interactive state; this is the exact hex the retired primary hover pointed at */
  --color-orange-500: #f58c61;   /* @usage Accent, decorative, print. The brand orange | @constraint Brand use only. Never an interactive state or interactive fill – use orange-600/700 */
  --color-orange-600: #d14811;   /* @usage Primary button fill, interactive orange | @constraint Interaction only, never decorative or print. White text 4.52:1. Never on a petrol surface */
  --color-orange-700: #b23c0e;   /* @usage Interaction hover and active fill | @constraint Interaction only, never decorative or print. White text 5.92:1 */

  /* --- Colors: Legacy ---
       Heritage brand colours kept in the palette at a legacy tier, not retired.
       In-set (so conformance treats them as named palette colours, not arbitrary
       hex) but flagged @tier legacy so new use warns rather than passes silently.
       Posture: replace on touch — swap for the core palette when a surface is next
       updated; no removal campaign. Other retired values may inherit this tier.
       NOTE: distinct from --color-chart-01-teal (#0f9081), an unrelated chart colour. */
  --color-legacy-teal: #00c6b2;  /* @usage Legacy / heritage brand colour (formerly "turquoise"); existing surfaces only, predominantly Norway | @constraint Not for new work; replace on touch. In-set legacy — conformance warns, never errors, on new use | @tier legacy */

  /* --- Colors: Neutral --- */
  --color-gray-100: #d7d2cb;
  --color-gray-300: #9e9281;
  --color-gray-500: #585044;

  --color-white: #ffffff;        /* @usage Card/surface backgrounds, inverse text bg | @constraint Never as page background. Pages are sand-100 or sand-50 */

  /* --- Colors: Feedback ---
       FIGMA-UPDATE: Figma has only warning-100 (#c14444). This file adds
       error-500 (consolidated from Figma's warning-100) and warning-500
       (from documented amber, fits sand family). Sync back to Figma. */
  --color-error-500: #c14444;    /* @usage Validation errors, destructive actions | @constraint State communication only. Never decoration */
  --color-warning-500: #d9b48f;  /* @usage Warning states, caution indicators | @constraint State communication only. Same hex as sand-500 — context differentiates */
  --color-info-500: #007bc7;     /* @usage Informational states — neutral notices that are neither a problem nor a completion | @constraint Indicator, or text on white only: 4.51:1 white, 4.26 sand-50, 3.84 sand-100 — AA for text on white and on neither sand ground (BASELINE.md § Forms, card 118: state text sits on a white card or an instrumental ground, never on the communicative one). Clears the 3:1 boundary floor everywhere as a UI component. Same hex as --color-goal-no-data and deliberately not an alias — that set is dashboard-only. Card 156 */

  /* --- Colors: Confirm ---
       Completion/confirmation action color. Distinct from goal-achieved
       (dashboard indicator) — this is for interactive "mark done" actions.
       Evidence: Hälsodeklarationer prototype "Klarmarkera" button.
       DECISION 2026-06-12: value aligned to Foundation F1 (#4f866e),
       replacing earlier #27ae60. White text on #4f866e = 4.23:1 — passes
       AA for large text/UI components; #27ae60 failed at 2.87:1.
       FIGMA-UPDATE: Not yet in Figma. Add as action color. */
  /* Narrowed 2026-07-31, applied here 2026-08-01 with the confirm sweep. The
     value is unchanged and the role is not: white on it is 4.23:1, so it never
     cleared the floor as an interactive fill. The confirm button variant was
     retired rather than recoloured – BASELINE.md hard rule 5 is the rule of
     record. Both greens are indicators now, told apart by what they indicate.
     The button tokens below now alias the secondary cluster (card 19,
     2026-08-05, applying card 16's decision) – this indicator value is no
     longer reachable through any --button-* fill. Card 23, the pairing rule,
     remains undecided and does not touch this: it would unwind the alias, not
     this indicator's value. */
  --color-confirm-500: #4f866e;  /* @usage Completion indicator – status dots, badges, chips | @constraint Indicator only, never an interactive fill: white on it is 4.23:1. Distinct from goal-achieved green, which indicates "target met" */

  /* --- Colors: Goal Status ---
       Traffic light indicators for KPI/goal dashboards only.
       Never use in patient-facing interfaces.
       MUST always pair with icon or shape. Reminder only – the rule lives at
       constitutional/accessibility-is-foundational.md */
  --color-goal-achieved: #2e8540;   /* @usage KPI target met indicator | @constraint Dashboard only. Always pair with icon/shape. Never for interactive confirm */
  --color-goal-borderline: #ffb81c; /* @usage KPI near-target indicator | @constraint Dashboard only. Always pair with icon/shape */
  --color-goal-missed: #d4351c;     /* @usage KPI target missed indicator | @constraint Dashboard only. Always pair with icon/shape */
  --color-goal-no-data: #007bc7;    /* @usage KPI no-data indicator | @constraint Dashboard only. Always pair with icon/shape */

  /* --- Colors: Data Visualization ---
       Dedicated chart/graph palette. Deliberately distinct from brand palette
       to prevent confusion between data points and interactive UI elements.
       Ordered by recommended usage sequence (start with 01, add as needed).
       Cool series (01-06) and warm series (07-12) can pair by index. */
  --color-chart-01-teal: #0f9081;
  --color-chart-02-mint: #94cbc4;
  --color-chart-03-blue: #577ba3;
  --color-chart-04-blue-light: #98c9ef;
  --color-chart-05-purple: #a078c2;
  --color-chart-06-purple-light: #d8b8ef;
  --color-chart-07-terracotta: #d77a61;
  --color-chart-08-terracotta-light: #f0c3b2;
  --color-chart-09-marine: #6b9495;
  --color-chart-10-olive: #bed0c0;
  --color-chart-11-warm-grey: #e4ded5;
  --color-chart-12-dark-sand: #d9b48f;
  /* Note: chart-12 dark-sand is same hex as sand-500 and warning-500.
     Context differentiates: sand-500 = brand primitive, warning-500 = UI state,
     chart-12 = data category. If chart-12 appears alongside a warning state
     in the same view, consider using a different chart color. */

  /* --- Spacing ---
       Modular scale: 1.5 ratio aligned to 4px grid.
       Shares ratio with type scale for cross-dimensional harmony.
       FIGMA-UPDATE: Replaces previous spacing scale. Sync to Figma. */
  --spacing-0: 0px;          /* @usage Zero gap, collapsed state | @constraint Use explicitly — don't omit spacing, set it to 0 */
  --spacing-3xs: 4px;       /* @usage Minimum gap, icon padding, label-to-field | @constraint Smallest usable gap. Never go below this */
  --spacing-2xs: 8px;       /* @usage Tight element spacing, inline gaps | @constraint Related inline elements */
  --spacing-xs: 12px;       /* @usage Related element spacing, input padding-y | @constraint Default vertical rhythm within components */
  --spacing-sm: 16px;       /* @usage Default component padding, grid margin mobile | @constraint Most common padding value */
  --spacing-md: 24px;       /* @usage Section spacing within components, card padding | @constraint Primary structural spacing */
  --spacing-lg: 36px;       /* @usage Between components, above h2 | @constraint Component-level separation */
  --spacing-xl: 48px;       /* @usage Between sections | @constraint Section-level separation */
  --spacing-2xl: 72px;      /* @usage Major section breaks | @constraint Communicative surfaces mainly */
  --spacing-3xl: 96px;      /* @usage Page-level spacing | @constraint Communicative surfaces only. Never on instrumental */

  /* --- Border Radius ---
       Four-tier system based on functional role:
       s (4px)    → general containers: panels, modals, tables
       l (16px)   → cards specifically
       m (8px)    → interactive: buttons, inputs, dropdowns, action cards
       full       → compact indicators: badges, tags, status dots

       radius-l retired 2026-08-07 at its old value (12px, sat reserved and
       unassigned from creation — no component ever took it, and the only
       document that assigned it was a stale reference table). Reinstated
       2026-08-10 at a new value, board card 45: computed styles measured
       live on aleris.se/.no/.dk showed every card shipping at 16px against
       Baseline's 4px, and the 4px container tier inverts the concentric-
       corner convention against anything nested flush inside it (an outer
       radius must be at least its inner radius plus the gap). Cards only,
       for now — panels, modals and tables were never measured live and stay
       --radius-s until they are; see BASELINE.md § Component shapes and
       workspace/archive/2026-08-07-radius-contradiction-and-nesting.md /
       2026-08-10-radius-verify-prototype-and-package.md. */
  --radius-0: 0px;            /* @usage Sharp corners, full-bleed edges | @constraint Explicit removal of rounding */
  --radius-s: 4px;            /* @usage General containers: panels, modals, tables | @constraint General container tier. Cards use --radius-l instead. Not for buttons or inputs */
  --radius-l: 16px;           /* @usage Cards only | @constraint Card tier, decided 2026-08-10 (board card 45) from a live production measurement. Not a general container value */
  --radius-m: 8px;            /* @usage Interactive: buttons, inputs, dropdowns | @constraint Interactive element tier. Not for containers */
  --radius-full: 100px;       /* @usage Compact indicators: badges, tags, avatars | @constraint Indicator tier only. No full-pill buttons exist in Aleris */

  /* --- Stroke --- */
  --stroke-0: 0px;
  --stroke-xs: 1px;
  --stroke-m: 2px;
  --stroke-xl: 4px;

  /* --- Typography: Font Family --- */
  --font-family-primary: 'Museo Sans', Arial, sans-serif;
  --font-family-fallback: Arial, sans-serif;
  --font-family-icons: 'Font Awesome 6 Pro';

  /* --- Typography: Font Size ---
       Perfect fifth modular scale. Ratio: 1.5. Base: 18px.
       Shares ratio with spacing scale for cross-dimensional harmony.
       Half-steps (sm, h4) use geometric means for pragmatic in-between sizes.

       CORRECTED 2026-08-05, card 18/19. Every rem value below used to assume
       a root font-size of 18px – this comment block said so directly, and
       `--font-size-xs` was commented "14px — accessibility floor". Nothing
       in `app/` has ever set the root to 18px: no `html { font-size: … }`
       exists anywhere, confirmed by search, and there is no Tailwind config
       to supply one. At the real, unset browser default (16px), every size
       rendered ~11% smaller than documented – including the floor itself,
       at 12.48px rather than 14px. One root cause, not seven: the px
       targets below (14/16/18/22/27/40/60) were always the right design
       values, only the rem numbers computed against a root that was never
       real. Rewritten to hit the same px targets at 16px instead – the
       modular ratios between sizes are unchanged, because every value
       scales by the same constant (16/18). See `tokens.test.ts`, "the 14px
       floor", and `baseline/conformance/index.html` § The 14px floor for
       the measurement that found this and now verifies the fix.

       FIGMA-UPDATE: Full scale replacement. Sync all sizes to Figma. */
  --font-size-xs: 0.875rem;     /* 14px @16px root — accessibility floor */
  --font-size-sm: 1rem;         /* 16px @16px root — labels, UI chrome */
  --font-size-base: 1.125rem;   /* 18px @16px root — body text. Scale anchor. */
  --font-size-md: 1.375rem;     /* 22px @16px root — h4 half-step */
  --font-size-lg: 1.6875rem;    /* 27px @16px root — base × 1.5 */
  --font-size-xl: 2.5rem;       /* 40px @16px root */
  --font-size-2xl: 3.75rem;     /* 60px @16px root */

  /* --- Typography: Font Weight ---
       Mapped to actual Museo Sans font files (woff2).
       Available weights: 100, 300, 500, 700, 900 (plus italics).
       IMPORTANT: Museo Sans has no 400 weight. Using font-weight: 400
       causes browser synthesis — always use these tokens instead.
       When falling back to Arial, 500 renders slightly heavier than
       Arial's 400 but is acceptable. */
  --font-weight-light: 300;     /* Museo Sans 300. @usage Large display text on communicative surfaces | @constraint Exceptional use only. Size must be xl (40px) or above */
  --font-weight-regular: 500;   /* Museo Sans 500 (Medium). @usage Body text, labels, buttons, UI chrome | @constraint The workhorse. Never use 400 — Museo Sans has no 400 weight */
  --font-weight-bold: 700;      /* Museo Sans 700. @usage Headings, emphasis, CTAs | @constraint The other workhorse. 500 and 700 are the system */
  --font-weight-black: 900;     /* Museo Sans 900. @usage Almost never | @constraint Exceptional use only. Try 700 at larger size first */

  /* --- Typography: Line Height ---
       Values chosen to resonate with the type and spacing scales.
       body 18px × 1.5 = 27px (= h3 font size)
       h3 27px × 1.33 = 36px (= spacing-lg)
       h2 40px × 1.2 = 48px (= spacing-xl) */
  --line-height-tight: 1.1;     /* Display text (60px) */
  --line-height-heading: 1.2;   /* h1, h2 */
  --line-height-subheading: 1.33; /* h3, h4 */
  --line-height-body: 1.5;      /* Body text — 18 × 1.5 = 27px = h3 size */

  /* --- Typography: Letter Spacing ---
       CORRECTED 2026-09-04. The inline comments below used to read "h2, h3" on
       --letter-spacing-tight and "h1" on --letter-spacing-tighter. The
       --type-*-letter-spacing composites in the Component layer have never
       assigned them that way: h1 takes tightest, h2 takes tighter, h3 takes
       tight, h4 and lead take normal. The comments described a scheme the file
       does not implement, and a reader who stopped here was told the wrong one.
       Found by an external consumer that read these four lines instead of the
       composites and filed a correction against its own — correct — values.
       Kept as one of a stale comment's costs: it produced a false finding
       downstream, silently, in the half of the file that looks authoritative.
       Asserted by tokens.test.ts § "letter-spacing roles match the composites
       that consume them", so the two halves cannot disagree again. */
  --letter-spacing-normal: 0;         /* h4, lead, body, labels */
  --letter-spacing-tight: -0.01em;    /* h3 (27px) */
  --letter-spacing-tighter: -0.015em; /* h2 (40px) */
  --letter-spacing-tightest: -0.02em; /* h1 (60px), the display size */

  /* --- Shadows ---
       Petrol-tinted for brand coherence. Mapped to Figma's e0–e3 scale.
       FIGMA-UPDATE: Figma uses black-based shadows. Update Figma to
       petrol-tinted values for brand consistency. */
  --shadow-e0: 0px 0px 0px rgba(0, 72, 81, 0);
  /* TWO-PART AND DIRECTIONAL from 2026-09-17, and this closes OQ-T11 —
     "is --elevation-frame a two-part shadow in a ladder that is single-part
     throughout?" The answer is that the ladder is two-part throughout.
     Torfinn ruled it on the rendered screens rather than on this file: the
     design realization's paintings were repainted on canonical values and
     shown beside their own, and the flat single-layer version is what he
     declined. Values taken from the realization unchanged.
     The model is a light source at the TOP RIGHT, soft. Every shadow drifts
     down and to the left — negative x, positive y — and each is two layers, a
     tight contact shadow plus a wider ambient one, which is what reads as a
     surface lifting rather than a card with a grey edge. The petrol tint is
     unchanged and was never in question.
     A consumer takes these by RE-VENDORING; a repin leaves the old flat
     values in place with no error, because a stale value is valid CSS. */
  --shadow-e1: -1px 1px 2px rgba(0, 72, 81, 0.04), -1px 2px 8px rgba(0, 72, 81, 0.06);
  --shadow-e2: -2px 2px 4px rgba(0, 72, 81, 0.06), -3px 8px 24px rgba(0, 72, 81, 0.1);   /* @usage Dropdowns; the hover state of a clickable card; and one featured element per communicative page at rest, such as the text card of a hero | @constraint One featured element per page, not several */
  --shadow-e3: -3px 8px 16px rgba(0, 72, 81, 0.08), -6px 24px 48px rgba(0, 72, 81, 0.16);

  /* --- Layout Grid ---
       12-column grid. Gutters use spacing tokens.
       Max-width varies by surface temperature:
       communicative = capped for reading comfort,
       instrumental = wider or fluid for data density. */
  --grid-columns: 12;
  --grid-gutter: var(--spacing-md);
  --grid-margin-mobile: var(--spacing-sm);
  --grid-margin-desktop: var(--spacing-md);
  --grid-max-width-communicative: 1200px;
  --grid-max-width-instrumental: 1440px;

  /* --- Column ladders, decided 2026-09-05, board card 120 (Torfinn, by looking) ---
       Chosen from baseline/conformance/layout/index.html: three candidates per
       surface rendered at 320/390/640/768/1024/1280/1440 from this file and the
       licensed face, each frame reporting its own column width and characters
       per line. The decision is the shape; the numbers below are what the
       shape measures, stated so the next reader knows they were seen.

       Communicative: ONE column at every width, and it grows. 40rem (640px) up
       to a 48rem window, then a quarter of every further pixel, held at 48rem
       (768px) from an 80rem window up. Both anchors are --breakpoint-sm and
       --breakpoint-md — asserted, since a clamp() cannot read a custom
       property and these are literals only meaningfully the tokens' values
       while something checks. Promoted from sundviktlakemedel's patient view,
       where Torfinn chose "grow the one column" over a second column on
       2026-09-02. DELIBERATE DEPARTURE, recorded rather than hidden: at 18px
       body in Museo Sans (measured average 7.31px per character) this column
       holds 83 characters at 640, 88 at 768, 96 at 1024 and 105 at 1280 —
       past the 45–75 reading guideline from 640px up. A measure-capped
       alternative (548px, 75 characters) was rendered beside it and not
       chosen. "6–8 of 12 grid columns" in BASELINE.md was never a measure
       proxy either — 600–800px is 82–109 characters — and is superseded by
       this token.

       Instrumental: fluid, no breakpoints. Tiles are however many columns of
       --grid-tile-min-instrumental fit: measured one-up to 390, two at 640,
       three at 768, four at 1024, FIVE at 1280 and 1440 — so a row of four
       tiles has an empty fifth slot from 1280 up. Rendered, seen, chosen. The
       stepped alternatives (12→6→1, 12→1) were beside it and not chosen. */
  --grid-column-communicative: clamp(40rem, 40rem + (100vw - 48rem) / 4, 48rem); /* @usage The reading column on a communicative surface — max-width of the content column inside --grid-max-width-communicative | @constraint One column at every width. Anchors are --breakpoint-sm and --breakpoint-md and are asserted equal to them. Holds 88 characters of 18px body at 768 and 105 at 1280 — chosen knowing that (card 120) */
  --grid-tile-min-instrumental: 14rem;    /* @usage Minimum tile width for an instrumental grid: grid-template-columns: repeat(auto-fit, minmax(var(--grid-tile-min-instrumental), 1fr)) | @constraint No breakpoints; the column count falls out of the width. Four tiles land on five columns from 1280 up — chosen knowing that (card 120) */

  /* --- Breakpoints ---
       Practical values, not derived from the modular scale.
       Use container queries where possible; these cover viewport fallbacks. */
  --breakpoint-sm: 640px;
  --breakpoint-md: 768px;
  --breakpoint-lg: 1024px;
  --breakpoint-xl: 1280px;

  /* --- Icons ---
       Font Awesome 6 Pro (licensed). Available styles: solid, regular, light.
       Icon sizes follow a scale independent of typography.
       FIGMA-UPDATE: Move icon sizes out of text-size styles in Figma. */
  --icon-xs: 12px;       /* Inline indicators, badge icons */
  --icon-sm: 16px;       /* Button icons, form field icons */
  --icon-md: 18px;       /* Default inline icon (matches body text) */
  --icon-lg: 24px;       /* Navigation, card header icons */
  --icon-xl: 48px;       /* Feature icons, empty states */


  /* --------------------------------------------------------------------------
     LAYER 2: SEMANTIC TOKENS
     Map primitives to usage context. Carry meaning.
     These are the tokens product teams should reference in code.
     -------------------------------------------------------------------------- */

  /* --- Brand Role --- */
  --brand-primary: var(--color-petrol-500);
  --brand-accent: var(--color-orange-500);
  --brand-light: var(--color-white);

  /* --- Text ---
       --text-accent REPOINTED 2026-09-04, from --color-orange-500 to
       --color-petrol-400. It was a semantic *text* alias resolving to a value
       that fails at every size: orange-500 is 2.38:1 on white, below the
       4.5:1 normal-text floor and below the 3:1 large-text floor as well, so
       no font size or weight could have made it conformant. foundation/colour.md
       gives orange-500 as "Brand – accent, decorative, print. The brand orange"
       and never as text; the same table gives petrol-400 as "Structure and
       accents. Large text only, from 18 pt or 14 pt bold. Not for body text —
       4.09:1 on white". Foundation takes precedence over Baseline (BASELINE.md,
       second sentence), so this was Baseline contradicting Foundation rather
       than a decision either document had made.
       Repointed rather than retired because Foundation does specify an accent
       text colour and this is where it belongs; the constraint that makes it
       usable now travels with it, which is what was missing. Grepped before
       changing: zero consumers in this repo, so nothing rendered was wrong —
       this was a loaded trap, not a live failure. --brand-accent stays on
       orange-500, which is the role Foundation actually assigns it.
       Asserted by tokens.test.ts § "semantic text tokens are legible at the
       size they claim".
       SUPERSEDED 2026-09-14: the token is retired, see the note above the
       --text-inverse line. The sentence beginning "Repointed rather than
       retired" is the half that did not hold — Foundation specifies an accent
       colour, not an accent text colour, and the two are not the same thing.
       Left standing rather than rewritten, because it records what was
       believed when the repoint was made. */
  --text-primary: var(--color-petrol-500);
  --text-secondary: var(--color-gray-500);
  /* --text-tertiary RETIRED 2026-09-04, board card 115 (Torfinn's ruling).
     Written without a colon after the name on purpose. The parser guard in
     tokens.test.ts reads any name-then-colon-then-value sequence inside a
     comment as a real declaration, so a retirement note in that shape
     resurrects the token whose death it records — and this note tripped that
     guard on its first run, by quoting the offending shape as an example of
     the offending shape. It held --color-gray-300 (#9e9281): 3.05:1 white,
     2.88:1 sand-50, 2.60:1 sand-100 — failing AA for normal text on all three
     grounds and the 3:1 large-text floor on both sands, so it carried text at
     no size on a sand ground.
     Retired rather than repointed, because the value was never the defect.
     gray-300 is right for what it is actually used for, and --text-disabled
     and --state-disabled-text already hold it. The name was the defect: a
     token reading as "the third level of body text" is reached for as caption
     ink, and it was — three times in one hour while building the site (card
     115's own evidence), each time as a caption, each time failing. Renaming
     it would have kept a fourth name for one value; removing it closes the
     invitation at the source.
     A consumer survey ran before the removal rather than after: 849 files
     across patientguide, sundviktlakemedel, aleris-assistant,
     aleris-vibe-coding and this repo hold exactly one text use of it —
     patientguide's print stylesheet, on the (url) suffix after an external
     link, which moves to --text-secondary. Three uses of the primitive as a
     border colour in sundviktlakemedel are unaffected: non-text, and 3.05:1
     clears the 3:1 boundary floor.
     Deliberately NOT replaced by a caption tier. There is nothing between
     gray-300 and gray-500 (7.94/7.49/6.76); the lightest warm grey clearing
     4.5:1 on all three grounds is around #6d6456 (5.82/5.49/4.96), which sits
     about 1.4 ratio steps from gray-500 and would not read as a separate
     tier. A caption wants --text-secondary.
     Asserted by tokens.test.ts § "retired tokens do not come back". */
  --text-inverse: var(--color-white);
  /* --text-accent RETIRED 2026-09-14 (Torfinn's ruling). Written without a
     colon after the name for the reason --text-tertiary gives above: the
     parser guard reads name-then-colon-then-value inside a comment as a real
     declaration, so a note in that shape resurrects the token it buries.
     It held --color-petrol-400: 4.09:1 white, 3.86:1 sand-50, 3.49:1
     sand-100 — large text only, never body.
     Retired rather than repointed back to orange, and the warm ruling is the
     reason it could not be. --color-orange-500 measures 2.38:1 on white,
     2.25:1 sand-50, 2.03:1 sand-100, which is under the 3:1 large-text floor
     as well as the 4.5:1 normal-text one, so no size and no weight make it
     conformant. The 2026-09-04 note below assumed Foundation specifies an
     accent TEXT colour and that this alias was where it belonged. It does
     not: Foundation gives orange as accent, decorative and print, and an
     accent colour is not a text colour. So the name was the defect, exactly
     as it was for --text-tertiary — an alias reading as "the text form of the
     brand accent" gets reached for, and there is no conformant value to give
     it. A warm accent that does carry text exists, --color-orange-700 at
     5.92/5.59/5.05, but it is the interaction hover and active colour; giving
     one hex a second role is a Foundation decision and not a token repoint.
     Grepped before changing: zero uses in patientguide, sundviktlakemedel, se
     and web, and one in this repo's own aleris-tailwind.css, repointed with
     it. --brand-accent stays on orange-500, which is the role Foundation
     actually assigns it.
     Asserted by tokens.test.ts § "retired tokens do not come back". */
  --text-link: var(--color-petrol-500);
  --text-disabled: var(--color-gray-300);
  --text-error: var(--color-error-500);                 /* @usage Error text — field validation, destructive-action warnings | @constraint 5.03:1 white, 4.74 sand-50, 4.28 sand-100 — normal-text-safe on white and sand-50, large-text-only on sand-100, which is the communicative page ground where a field error most often sits. Not repointed: the value is Foundation's and what to do about the sand-100 case is a decision. Card 118 */

  /* --- Backgrounds / Surfaces ---
       Two page surfaces by temperature:
       Communicative (patient-facing, marketing, booking): sand-100.
         Cards are white on sand — warmth is the brand, cards provide editorial structure.
       Instrumental (tools, admin, documentation, dashboards): sand-50.
         Near-white with faint sand warmth. Content lives directly on the surface.
         Cards used sparingly — only when content genuinely needs visual grouping.
       The surface choice drives whether cards are structural (communicative)
       or optional (instrumental). */
  --surface-page: var(--color-sand-100);                /* @usage Communicative page background | @constraint Patient-facing, marketing, booking. Cards mandatory on this surface */
  --surface-page-instrumental: var(--color-sand-50);    /* @usage Instrumental page background | @constraint Tools, admin, dashboards. Cards optional on this surface */
  --surface-card: var(--color-white);                   /* @usage Card and panel backgrounds | @constraint Always white. Never sand on cards */
  --surface-elevated: var(--color-white);               /* @usage Dropdowns, popovers, tooltips | @constraint White with elevation shadow */
  --surface-overlay: var(--color-white);                /* @usage Modal and dialog backgrounds | @constraint White with highest elevation */

  /* Surface temperature: neutral/warm/cold variants.
     These support the communicative (warm) vs. instrumental (cold/neutral)
     surface modes described in the design governance document. */
  --surface-subtle-neutral: var(--color-sand-100);
  --surface-subtle-warm: var(--color-orange-100);        /* @usage Warm tint under content that carries its own contrast (table row hover) | @constraint Not a section colour: orange-100 reads as one surface with sand-100 (1.01:1). Use --surface-section-warm for a band */
  --surface-subtle-cold: var(--color-petrol-100);
  --surface-strong-neutral: var(--color-sand-500);
  --surface-strong-warm: var(--color-orange-500);
  --surface-strong-cold: var(--color-petrol-500);

  /* Section colours. The page's own background is always a --surface-page token; a section is a
     full-width layer on top of it, even when it fills the screen. These four are the colours a
     section may take. Cards on any section stay --surface-card (white). orange-100 is not one of
     them: it measures 1.01:1 against sand-100 and the two read as one surface. */
  --surface-section-white: var(--color-white);          /* @usage Full-width content section on a communicative page | @constraint A white card on it is separated by its border and shadow */
  --surface-section-cold: var(--color-petrol-100);      /* @usage Full-width light petrol section | @constraint Text petrol-500 or gray-500 */
  --surface-section-warm: var(--color-orange-300);      /* @usage Full-width light orange section | @constraint No primary button (orange-600 is 2.82:1 against it) — actions use the secondary. No petrol-400 text (2.56:1); text petrol-500 or gray-500 */
  --surface-section-strong: var(--color-petrol-500);    /* @usage The one strong section on a page | @constraint Light text only; the primary action inverts, per --button-primary-inverse-* */

  /* Gradient grounds. Named for the axis this file already uses — cold, warm — rather than for
     the pigment, because the semantic layer names a role and --gradient-petrol would be
     primitive-layer thinking. Each starts on its own --surface-strong-* value and lightens one
     step, so the three cold surfaces are one family a reader can predict. Added 2026-09-13,
     board card 156, from the design system export; nothing canonical expressed a gradient before
     this, which is the gap board card 121 was opened on — a live action shipped invisible on
     five of seven seeded home screens because no control variant is defined against a branded
     ground. Adding the ground does not answer 121: which controls may sit on one, and how a
     check measures text over a gradient, are still that card's. */
  --surface-gradient-cold: linear-gradient(135deg, var(--color-petrol-500) 0%, var(--color-petrol-450) 100%);
  /* @usage Branded cold ground, and the only gradient that carries text | @constraint All four
     light text roles hold end to end, measured 2026-09-13: white 10.27 → 6.23, petrol-100
     7.73 → 4.69, sand-50 9.69 → 5.88, sand-100 8.75 → 5.31, every one clearing the 4.5:1 body
     floor at both stops. Dark text is not the question here — this is a dark ground.
     TWO RETUNES IN ONE DAY, and the second is the one that matters. It shipped running to
     petrol-400, where white is 4.09:1, and the design system's own painting puts 16px white body
     text on it eleven times, so a heading started legible and ended illegible along its own
     background. The first retune fixed white and left petrol-100 at 3.51:1. The second took
     sundviktlakemedel's value, which had already cleared both — a floor chosen against ONE text
     colour is how the second one keeps failing.
     `lib/gradient-contrast.test.ts` asserts all four rather than leaving this comment to be
     believed. */
  --surface-gradient-warm: linear-gradient(135deg, var(--color-orange-500) 0%, var(--color-orange-300) 100%);
  /* @usage Branded warm ground | @constraint DARK TEXT ONLY. Measured 2026-09-13: white is 2.38:1
     on the near stop and 1.60:1 on the far stop — nowhere near any floor, at either end, so white
     text on this ground is never permissible rather than conditionally so. Petrol-500 on the far
     stop is 6.41:1 and clears AA. This is a decorative ground, not a text surface. */

  /* --- Borders --- */
  --border-default: var(--color-gray-100);
  --border-strong: var(--color-gray-300);
  --border-focus: var(--color-petrol-500);
  --border-error: var(--color-error-500);

  /* --- Interactive States ---
       REVISED 2026-08-05 with card 19, applying board decisions 16 and 17.
       Two structural changes: hover stopped being a fill (card 16 – hover and
       focus merged into one ring-based signal, so every --button-*-hover-bg
       token and --state-hover are gone), and active became per-variant for
       buttons rather than falling back on a generic tint (card 17). */

  /* RETIRED 2026-08-05, card 16 (was --color-orange-700). Hover no longer
     changes a fill on any variant – see the dual ring below. A generic
     fallback for a state that no variant uses any more is dead weight rather
     than a safety net; reword this note if a future non-button component
     genuinely needs a hover colour rather than widening this one back out. */

  /* RETIRED 2026-07-31, card 17 (was --color-sand-500, also
     --surface-strong-neutral). A surface tint, never a button fill: white on
     it is 1.93:1. No variant defined its own active fill, so all six fell
     back on this token and failed without anything in the file saying so.
     Removed rather than repointed – a generic fallback for a per-variant
     state is exactly the mechanism that hid the failure. Buttons now use
     --button-*-active-bg plus --state-press-shade below. */

  /* Focus ring – dual, card 17. One petrol ring was specified for six variants
     on two surfaces: petrol-on-petrol (1.0:1) on secondary, petrol-on-petrol-
     page (1.0:1) on primary-inverse. A single colour cannot serve every
     ground, and a surface-aware ring fails silently when a button lands on
     the wrong one. Both rings are always drawn on focus – 2px inner, 2px
     outer – so whichever ground the button sits on, one ring contrasts. On
     petrol surfaces the two swap: see --button-primary-inverse-focus-ring*.
     On hover alone, card 16's merge draws the outer ring only, no fill
     change – see BASELINE.md § Buttons. */
  --state-focus-ring: var(--color-petrol-500);        /* Outer ring. Reads against light page surfaces */
  --state-focus-ring-inner: var(--color-white);       /* Inner ring. Reads against the button's own fill */
  --state-focus-ring-width: var(--stroke-m);          /* 2px per ring */

  /* Composed ring values, added 2026-09-16. The three parts above say what the
     ring is made of; these say what to set `box-shadow` to, and that gap was
     costing more than it looked. Measured in a browser the same day: the
     design realization's paintings express the ring as one composed value, so
     mapping its name onto the colour part above made `box-shadow: var(...)`
     compute to `none` — the ring did not degrade, it disappeared, on every
     control in the painting. A rule every consumer has to re-compose is a rule
     each of them can get silently wrong, and this is the class this file was
     thinnest in. Both rings are always drawn; the outer spread is twice the
     inner so the two read as one band, derived from the width token rather
     than written as 2px/4px so a change to the width still moves both. */
  --state-focus-ring-shadow: 0 0 0 var(--state-focus-ring-width) var(--state-focus-ring-inner), 0 0 0 calc(var(--state-focus-ring-width) * 2) var(--state-focus-ring);
  /* For a control on a petrol ground, where the two swap. The same swap the
     component layer already declares for one cell at
     --button-primary-inverse-focus-ring; composed here so the other three
     variants have it too, which is the gap the grounds matrix keeps naming. */
  --state-focus-ring-shadow-inverse: 0 0 0 var(--state-focus-ring-width) var(--state-focus-ring), 0 0 0 calc(var(--state-focus-ring-width) * 2) var(--state-focus-ring-inner);

  /* Hover draws the outer ring only and never a fill change (card 16's merge,
     BASELINE.md § Buttons). The colour follows the variant, which is why there
     are two: an orange control rings in its own hover orange, a petrol one in
     petrol. Nothing here existed before — hover was prose. */
  --state-hover-ring-shadow-primary: 0 0 0 var(--state-focus-ring-width) var(--color-orange-700);
  --state-hover-ring-shadow-secondary: 0 0 0 var(--state-focus-ring-width) var(--color-petrol-500);

  /* Press affordance, card 17. Supplementary to the accessible state, which
     the fill and (on focus) the ring already carry – this is not asserted in
     the fitness check. Deliberately neutral rather than petrol-tinted: a
     petrol inset on a petrol fill is invisible, the same mistake the single
     focus ring made. */
  --state-press-shade: rgba(0, 0, 0, 0.4);
  --state-press-translate-y: 1px;   /* Dropped under prefers-reduced-motion; the inset shade stays */
  --button-press-shadow: inset 0 1px 3px var(--state-press-shade);   /* @usage Every filled variant when pressed, replacing its rest shadow, together with --state-press-translate-y | @constraint The shade is neutral, not petrol, so it reads on every fill */

  --state-disabled-bg: var(--color-gray-100);   /* Inputs only – buttons do not disable, see BASELINE.md § Buttons */
  --state-disabled-text: var(--color-gray-300);

  /* --- Feedback / Status ---
       Reserved for state communication only. Never decoration. */
  --status-error: var(--color-error-500);
  --status-warning: var(--color-warning-500);
  --status-confirm: var(--color-confirm-500);
  --status-info: var(--color-info-500);
  --status-background: var(--color-white);

  /* --- Goal Status (semantic) ---
       Dashboard KPI indicators. Instrumental surfaces only.
       Always pair with icon or shape. Reminder only – the rule lives at
       constitutional/accessibility-is-foundational.md */
  --goal-achieved: var(--color-goal-achieved);
  --goal-borderline: var(--color-goal-borderline);
  --goal-missed: var(--color-goal-missed);
  --goal-no-data: var(--color-goal-no-data);

  /* --- Data Visualization (semantic) ---
       chart-1 through chart-12 for sequential, ordered category assignment.
       chart-pair-N-a and chart-pair-N-b for comparisons, before/after, two-series charts.
       12 colors total — sufficient for most visualizations.
       If you need more than 6-8 in one chart, reconsider the visualization. */

  /* Sequential assignment — use in this order */
  --chart-1: var(--color-chart-01-teal);
  --chart-2: var(--color-chart-02-mint);
  --chart-3: var(--color-chart-03-blue);
  --chart-4: var(--color-chart-04-blue-light);
  --chart-5: var(--color-chart-05-purple);
  --chart-6: var(--color-chart-06-purple-light);
  --chart-7: var(--color-chart-07-terracotta);
  --chart-8: var(--color-chart-08-terracotta-light);
  --chart-9: var(--color-chart-09-marine);
  --chart-10: var(--color-chart-10-olive);
  --chart-11: var(--color-chart-11-warm-grey);
  --chart-12: var(--color-chart-12-dark-sand);

  /* Paired assignment — for comparisons, before/after, two-series charts */
  --chart-pair-1-a: var(--color-chart-01-teal);
  --chart-pair-1-b: var(--color-chart-07-terracotta);
  --chart-pair-2-a: var(--color-chart-03-blue);
  --chart-pair-2-b: var(--color-chart-08-terracotta-light);
  --chart-pair-3-a: var(--color-chart-05-purple);
  --chart-pair-3-b: var(--color-chart-10-olive);

  /* --- Typography Compositions ---
       Composite tokens for common text roles.
       Each bundles: size, weight, line-height, letter-spacing, color.
       Perfect fifth (1.5) modular scale from 18px base.
       Line-heights resonate with spacing scale:
         body 18×1.5 = 27px (h3 size)
         h3 27×1.33 = 36px (spacing-lg)
         h2 40×1.2 = 48px (spacing-xl) */

  /* Heading 1 / Display — communicative hero, landing pages (60px) */
  --type-h1-size: var(--font-size-2xl);
  --type-h1-weight: var(--font-weight-bold);
  --type-h1-line-height: var(--line-height-tight);
  --type-h1-letter-spacing: var(--letter-spacing-tightest);
  --type-h1-color: var(--text-primary);

  /* Heading 2 — page title, major section (40px) */
  --type-h2-size: var(--font-size-xl);
  --type-h2-weight: var(--font-weight-bold);
  --type-h2-line-height: var(--line-height-heading);
  --type-h2-letter-spacing: var(--letter-spacing-tighter);
  --type-h2-color: var(--text-primary);

  /* Heading 3 — section heading (27px) */
  --type-h3-size: var(--font-size-lg);
  --type-h3-weight: var(--font-weight-bold);
  --type-h3-line-height: var(--line-height-subheading);
  --type-h3-letter-spacing: var(--letter-spacing-tight);
  --type-h3-color: var(--text-primary);

  /* Heading 4 — card header, sub-subsection (22px) */
  --type-h4-size: var(--font-size-md);
  --type-h4-weight: var(--font-weight-bold);
  --type-h4-line-height: var(--line-height-subheading);
  --type-h4-letter-spacing: var(--letter-spacing-normal);
  --type-h4-color: var(--text-primary);

  /* Lead — introductory paragraph below a heading.
     Bridges heading and body: heading-sized but body-weight, secondary color.
     Not a heading — it doesn't structure, it orients.
     Use once per page/section, directly after the h1 or h2. */
  --type-lead-size: var(--font-size-lg);          /* 27px — same as h3 */
  --type-lead-weight: var(--font-weight-regular);  /* 500 — not bold */
  --type-lead-line-height: var(--line-height-subheading);  /* 1.33 */
  --type-lead-letter-spacing: var(--letter-spacing-normal);
  --type-lead-color: var(--text-secondary);        /* Lighter than headings */

  /* Body text (18px) */
  --type-body-size: var(--font-size-base);
  --type-body-weight: var(--font-weight-regular);
  --type-body-line-height: var(--line-height-body);
  --type-body-color: var(--text-primary);

  /* Body bold — emphasis within body (18px bold) */
  --type-body-bold-weight: var(--font-weight-bold);

  /* Label — form labels, UI chrome (16px) */
  --type-label-size: var(--font-size-sm);
  --type-label-weight: var(--font-weight-regular);
  --type-label-line-height: var(--line-height-body);
  --type-label-color: var(--text-primary);

  /* Small — captions, metadata, timestamps (14px) */
  --type-small-size: var(--font-size-xs);
  --type-small-weight: var(--font-weight-regular);
  --type-small-line-height: var(--line-height-body);
  --type-small-color: var(--text-secondary);

  /* --- Spacing: Semantic ---
       Spacing above headings proportional to heading visual weight:
       Above h2 (40px) → spacing-lg (36px)
       Above h3 (27px) → spacing-md (24px)
       Above h4 (22px) → spacing-sm (16px) */
  --spacing-page-padding: var(--spacing-sm);
  --spacing-card-padding: var(--spacing-md);
  --spacing-section-gap: var(--spacing-md);
  --spacing-element-gap: var(--spacing-xs);
  --spacing-label-gap: var(--spacing-3xs);

  /* --- Elevation --- */
  --elevation-flat: var(--shadow-e0);
  --elevation-card: var(--shadow-e1);
  --elevation-dropdown: var(--shadow-e2);
  --elevation-modal: var(--shadow-e3);


  /* --------------------------------------------------------------------------
     LAYER 3: COMPONENT TOKENS (STUBS)
     Specific to individual components. Extend as the system matures.
     These are starting points — not a complete component library.
     -------------------------------------------------------------------------- */

  /* --- Button ---
     State model rebuilt 2026-08-05, card 19, applying board decisions 16 and
     17. Hover is no longer a fill on any variant – it draws the outer focus
     ring only, no colour change (card 16, the hover/focus merge). Focus draws
     both rings. Press (the old "active") gets its own fill per filled
     variant rather than falling back on the retired --state-active. Buttons
     do not disable – see BASELINE.md § Buttons. */
  --button-primary-bg: var(--color-orange-600);        /* Repointed off --brand-accent (orange-500) 2026-07-29: white text on orange-500 fails AA. orange-600 = 4.52:1 */
  --button-primary-text: var(--color-white);
  --button-primary-active-bg: var(--color-orange-700);   /* Was the hover value until card 16's merge; orange-700 is the ramp's last step, so press reuses it rather than adding a colour. 5.92:1 */
  --button-primary-radius: var(--radius-m);
  --button-primary-padding-x: var(--spacing-md);
  --button-primary-padding-y: var(--spacing-xs);
  --button-primary-font-size: var(--font-size-base);
  --button-primary-font-weight: var(--font-weight-regular);
  /* The bevel, taken from the Aleris Design System's Button component (version 1790164640-d5f5)
     on Torfinn's ruling of 2026-09-24. A soft shadow down and to the left, the ladder's light
     source at top right, plus a thin highlight along the top edge facing the light. That is a
     top-edge highlight, not a side accent. Recorded in export-reconciliation.json § rulings. */
  --button-primary-shadow: -1px 1px 2px rgba(0, 72, 81, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.28);

  /* Variant clusters alias padding / font-size / font-weight to the primary
     cluster so every variant has a complete contract. Override an alias if a
     specific variant truly needs different sizing — but the default is "use
     the primary cluster's spacing and type rules so all four variants match
     visually beyond the colour story." */

  /* Primary, inverted – the primary action ON a petrol surface.
     NEW VARIANT 2026-07-29. Surface-aware: orange-600 is specified for light
     surfaces, and it must never be placed on petrol. On a petrol ground the
     primary action inverts instead – white fill, petrol text. Rest 10.27:1,
     press 7.73:1. petrol-100 is reused, not added – it is also the outline/
     ghost press fill below.
     Focus ring swaps here, and only here: this variant sits on petrol, so the
     ring facing the page (the "outer" position) must read against petrol,
     and the ring facing the button's own white fill (the "inner" position)
     must read against white. The two --state-focus-ring* values are exactly
     backwards for that, hence the per-variant override – the token names
     keep their positional meaning, only the values assigned to them swap. */
  --button-primary-inverse-bg: var(--color-white);
  --button-primary-inverse-text: var(--color-petrol-500);
  --button-primary-inverse-active-bg: var(--color-petrol-100);   /* Was the hover value until card 16's merge. 7.73:1 */
  --button-primary-inverse-focus-ring: var(--state-focus-ring-inner);         /* white – reads against the petrol page */
  --button-primary-inverse-focus-ring-inner: var(--state-focus-ring);         /* petrol – reads against this button's white fill */
  --button-primary-inverse-radius: var(--button-primary-radius);
  --button-primary-inverse-padding-x: var(--button-primary-padding-x);
  --button-primary-inverse-padding-y: var(--button-primary-padding-y);
  --button-primary-inverse-font-size: var(--button-primary-font-size);
  --button-primary-inverse-font-weight: var(--button-primary-font-weight);
  --button-primary-inverse-shadow: -1px 1px 2px rgba(0, 24, 28, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.5);   /* The design system's primary-on-petrol bevel */

  --button-secondary-bg: var(--brand-primary);
  --button-secondary-text: var(--color-white);
  --button-secondary-active-bg: var(--color-petrol-700);   /* 13.88:1. First per-variant press fill this cluster has had – previously fell back on the retired --state-active at 1.93:1 */
  --button-secondary-radius: var(--radius-m);
  --button-secondary-padding-x: var(--button-primary-padding-x);
  --button-secondary-padding-y: var(--button-primary-padding-y);
  --button-secondary-font-size: var(--button-primary-font-size);
  --button-secondary-font-weight: var(--button-primary-font-weight);
  --button-secondary-shadow: -1px 1px 2px rgba(0, 72, 81, 0.24), inset 0 1px 0 rgba(255, 255, 255, 0.16);

  --button-outline-bg: transparent;
  --button-outline-text: var(--brand-primary);
  --button-outline-border: var(--brand-primary);
  --button-outline-border-rule: var(--stroke-xs) solid var(--button-outline-border);
  --button-outline-radius: var(--radius-m);
  --button-outline-padding-x: var(--button-primary-padding-x);
  --button-outline-padding-y: var(--button-primary-padding-y);
  --button-outline-font-size: var(--button-primary-font-size);
  --button-outline-font-weight: var(--button-primary-font-weight);
  --button-outline-shadow: -1px 1px 2px rgba(0, 72, 81, 0.08);   /* Contact shadow only: no fill, so no highlight */
  --button-outline-active-bg: var(--color-petrol-100);   /* Declared directly now hover-bg is gone. Boundary is carried by the border (8.75:1), not this fill (1.13:1 alone) – outline's border does that job in every state */

  --button-ghost-bg: transparent;
  --button-ghost-text: var(--brand-primary);
  --button-ghost-radius: var(--radius-m);
  --button-ghost-padding-x: var(--button-primary-padding-x);
  --button-ghost-padding-y: var(--button-primary-padding-y);
  --button-ghost-font-size: var(--button-primary-font-size);
  --button-ghost-font-weight: var(--button-primary-font-weight);
  --button-ghost-shadow: none;   /* Ghost carries no bevel */
  /* KNOWN BOUNDARY FAILURE, carried from the retired ghost/hover cell (card
     21) rather than fixed here: ghost has no border token, so petrol-100 on
     sand-100 (1.13:1) is the only thing marking the control in this state.
     Tracked in tokens.test.ts KNOWN_BOUNDARY_FAILURES['ghost/active']. Ghost
     has zero instances in app/ or components/ – not fixed unilaterally
     because a border changes what "ghost" means as a variant, which is
     Torfinn's call, not a value pick. */
  --button-ghost-active-bg: var(--color-petrol-100);

  /* Confirm button — completion/sign-off actions only.
     "Klarmarkera", "Godkänn", "Signera", "Markera som klar".
     Not for generic "yes/ok" in dialogs — use primary for those.
     Card 3, decided 2026-07-31: the confirm colour story is retired: confirm
     aliases the secondary cluster and adds a required check glyph, so "I'm
     done" is carried by the glyph and the verb rather than by a fill. The
     alias is by reference, not by value, so secondary's press fill (or any
     future change to it) reaches confirm automatically. */
  --button-confirm-bg: var(--button-secondary-bg);
  --button-confirm-text: var(--button-secondary-text);
  --button-confirm-active-bg: var(--button-secondary-active-bg);
  --button-confirm-icon: check;   /* Required, not optional – the variant's only remaining distinction from secondary. "check" is already in the F11 icon allowlist */
  --button-confirm-radius: var(--radius-m);

  /* Touch target, card 19. BASELINE.md § Accessibility has asserted a 44px
     minimum touch/click target since before this file existed; nothing
     enforced it. Global rather than per-variant – the floor does not vary by
     colour story. Not classified as a state suffix in tokens.test.ts: see
     GLOBAL_BUTTON_TOKENS there. */
  --button-min-height-touch: 44px;
  --button-padding-y-touch: var(--spacing-sm);

  /* Pointer target, card 63/OQ2, 2026-08-12. 44px is WCAG 2.5.5 (AAA);
     the AA floor is 2.5.8 at 24×24 CSS px. BASELINE.md § Accessibility
     relaxed the blanket 44px rule to this floor for dense instrumental
     surfaces on desktop pointer input – touch and patient-facing contexts
     keep --button-min-height-touch. */
  --button-min-height-pointer: 24px;   /* @usage Minimum height for buttons on dense instrumental surfaces under pointer input | @constraint A floor, not a size. There is no pointer padding token, so a button honouring this floor with default padding still renders at 44px; a compact control needs a padding value this file does not carry (sundviktlakemedel, 2026-08-25) */

  /* Small button variant */
  --button-small-font-size: var(--font-size-xs);
  --button-small-padding-x: var(--spacing-sm);
  --button-small-padding-y: var(--spacing-3xs);

  /* --- Tab Navigation ---
       Sticky horizontal tabs below page headings.
       Used for sub-navigation within a section (not primary nav).
       Active tab indicated by underline in accent color, not background change.
       Sticks below topbar on scroll (z-index between sticky and sidebar). */
  --tab-nav-bg: transparent;                                          /* Inherits page surface */
  --tab-nav-border-bottom: var(--border-default);                     /* Colour only */
  --tab-nav-border-bottom-rule: var(--stroke-xs) solid var(--tab-nav-border-bottom); /* Full shorthand */
  --tab-nav-padding-x: var(--spacing-sm);
  --tab-nav-padding-y: var(--spacing-xs);
  --tab-nav-gap: var(--spacing-md);                  /* Between tab items */
  --tab-nav-font-size: var(--font-size-sm);
  --tab-nav-font-weight: var(--font-weight-regular);
  --tab-nav-color: var(--text-secondary);
  --tab-nav-color-active: var(--text-primary);
  --tab-nav-color-hover: var(--text-primary);
  --tab-nav-indicator-color: var(--color-petrol-500); /* Underline on active tab */
  --tab-nav-indicator-width: 2px;
  --tab-nav-sticky-z: var(--z-sticky);               /* Sticks on scroll only when the tab nav is the page's list of sections (BASELINE § Surface temperature, sticky elements) */

  /* --- Card ---
       Cards use border + shadow together on sand backgrounds.
       Border provides definition; shadow provides subtle lift.
       Evidence: Hälsodeklarationer prototype — "the subtle line
       that helps with contrast making them pop subtly."
       Radius 4px → 16px, 2026-08-10, board card 45 — see the --radius-l
       primitive comment for the full reasoning. */
  --card-bg: var(--surface-card);
  --card-radius: var(--radius-l);
  --card-padding: var(--spacing-card-padding);
  --card-border: var(--border-default);                                /* Colour only — for fine-grained composition */
  --card-border-rule: var(--stroke-xs) solid var(--card-border);       /* Full shorthand — `border: var(--card-border-rule)` Just Works */
  --card-shadow: var(--elevation-card);

  /* Flush top-of-card media — a hero image with zero clearance from the
     card's own top/left/right edges. Corrected 2026-08-10, same day as
     board card 45: this does not need its own radius value at all. The
     card already clips its contents to its own rounded shape, so a flush
     image with no radius on it is clipped to match the card's top corners
     for free, and its bottom edge is already square because it sits nowhere
     near the card's own bottom corners. The image needs zero radius
     handling; the card needs --card-media-overflow. An earlier version of
     this decision added --card-media-radius-top/-bottom to hand-match the
     image's corners to the card's — reverted the same day once it was
     pointed out that clipping already produces the identical result with
     nothing to keep in sync. Does not apply to an image inset with
     clearance on all sides — that stays --image-radius-default, unaffected.
     Scoped to card+media, not every card: applying this to all cards
     forecloses anything that intentionally overhangs a card's edge (a
     corner badge, a dropdown) — nothing in Baseline does that today, but a
     plain card should stay free to, without a second decision reopening
     this one. */
  --card-media-overflow: hidden;

  /* --- Input ---
       Label above field (Fixed). Mark optional fields, not required.
       Error: "what happened + what to do" below the field.
       Disabled vs read-only: see governance comments below. */

  /* Field box */
  --input-bg: var(--color-white);
  --input-border: var(--border-default);                               /* Colour only */
  --input-border-rule: var(--stroke-xs) solid var(--input-border);     /* Full shorthand */
  --input-border-focus: var(--border-focus);
  --input-border-error: var(--border-error);
  --input-radius: var(--radius-m);
  --input-padding-x: var(--spacing-sm);
  --input-padding-y: var(--spacing-xs);
  --input-font-size: var(--font-size-base);
  --input-text: var(--text-primary);
  --input-placeholder: var(--color-gray-300);            /* @usage Placeholder text inside a field | @constraint Not required to meet the AA text floor — every field in this system carries a visible label (§ Forms), so nothing depends on the placeholder being read. Points at the primitive deliberately: a placeholder is not a text level. Card 115 */

  /* Label — always above the field */
  --input-label-size: var(--font-size-sm);
  --input-label-weight: var(--font-weight-bold);
  --input-label-color: var(--text-primary);
  --input-label-gap: var(--spacing-3xs);        /* 4px between label and field */

  /* Helper text — below field, replaced by error on validation failure */
  --input-helper-size: var(--font-size-xs);
  --input-helper-color: var(--text-secondary);

  /* Error — replaces helper text position on validation failure.
     Pattern: "what happened + what to do" (from den nära experten digital voice).
     Input gets aria-invalid="true", error msg linked via aria-describedby. */
  --input-error-color: var(--color-error-500);
  --input-error-size: var(--font-size-xs);
  --input-error-border: var(--border-error);     /* red ring on the input itself */

  /* Optional indicator — "(valfritt)" suffix in label.
     Mark optional fields, not required. In clinical forms 80%+ of fields
     are required — marking all of them creates noise. */
  --input-optional-color: var(--text-secondary);

  /* Focus — petrol ring. Consistent with keyboard navigation patterns.
     Uses focus-visible (not focus) to avoid showing ring on mouse click. */
  --input-focus-ring: var(--state-focus-ring);
  --input-focus-ring-offset: 2px;

  /* Disabled — use sparingly.
     Patient-facing: prefer explain-on-click over disabled state.
     Show what's possible, explain prerequisites to unlock it.
     Instrumental: disabled acceptable when workflow context is
     self-evident to the user (e.g. clinician knows they must
     complete the note before signing). */
  --input-disabled-bg: var(--color-gray-100);
  --input-disabled-text: var(--text-disabled);
  --input-disabled-border: var(--color-gray-100);

  /* Read-only — looks like content, not a greyed-out input.
     For data that can be unlocked to editable (role-based or
     state-based), present in an input-shaped container with a
     lock icon. Otherwise present as plain content text. */
  --input-readonly-bg: var(--surface-page);      /* sand — blends with page */
  --input-readonly-text: var(--text-primary);
  --input-readonly-border: transparent;

  /* --- Badge --- */
  --badge-radius: var(--radius-full);
  --badge-font-size: var(--font-size-xs);
  --badge-padding-x: var(--spacing-2xs);
  --badge-padding-y: var(--spacing-3xs);

  /* --- Table ---
       Two table types share the same token foundation:
       Read tables (display) and Work tables (interactive/editable).
       Density is user-selectable in instrumental surfaces. */

  /* Row density — three levels */
  --table-row-height-compact: 36px;
  --table-row-height-default: 48px;
  --table-row-height-comfortable: 64px;

  /* Cell spacing */
  --table-cell-padding-x: var(--spacing-sm);
  --table-cell-padding-y: var(--spacing-2xs);

  /* Header */
  --table-header-bg: var(--surface-subtle-cold);
  --table-header-text: var(--text-primary);
  --table-header-weight: var(--font-weight-bold);
  --table-header-size: var(--font-size-xs);
  --table-header-transform: none;
  --table-header-letter-spacing: 0.05em;

  /* Body */
  --table-body-size: var(--font-size-sm);
  --table-body-color: var(--text-primary);

  /* Row states */
  --table-row-hover-bg: var(--surface-subtle-warm);
  --table-row-selected-bg: var(--color-petrol-100);
  --table-row-border: var(--border-default);                          /* Colour only */
  --table-row-border-rule: var(--stroke-xs) solid var(--table-row-border); /* Full shorthand */

  /* Zebra striping — optional, off by default */
  --table-row-stripe-bg: var(--color-sand-100);

  /* --- Motion ---
       Animation serves feedback and state communication only, not decoration.
       Four duration levels, two easing curves. No more.
       Surface temperature selects from the same tokens: instrumental stays
       at instant/fast, communicative can use moderate/slow.
       Animate what does not move the layout, unless the user moved it:
       transform and opacity freely; colour, background, shadow and stroke to
       show a change of state; size only when the user caused it and it is
       contained, one element at a time, within --duration-moderate.
       BASELINE.md hard rule 7 is the rule of record.
       prefers-reduced-motion: reduce disables ALL animation (Fixed, a11y).
       See aleris-baseline-animation.md for full guidance. */

  /* Duration — four levels */
  --duration-instant: 100ms;    /* Focus rings, color shifts, checkbox toggles — feels immediate */
  --duration-fast: 200ms;       /* Buttons, hover, tooltips, badge updates — system reacting */
  --duration-moderate: 350ms;   /* Modals, accordions, sidebars, card flip — spatial tracking */
  --duration-slow: 500ms;       /* Page transitions, major layout shifts — used sparingly */

  /* Easing — two curves */
  --ease-out: cubic-bezier(0.0, 0.0, 0.2, 1);    /* Elements arriving: fast start, gentle stop */
  --ease-in-out: cubic-bezier(0.4, 0.0, 0.2, 1);  /* Elements moving: smooth at both ends */
  /* Never use ease-in alone — slow start reads as unresponsive. */

  /* Keyframe primitives (reference — apply via CSS @keyframes)
       fade-in:  opacity 0→1, translateY 4px→0  (ease-out, duration-fast)
       scale-in: opacity 0→1, scale 0.97→1       (ease-out, duration-fast)
     fade-in suits content entering the viewport (cards, list items, panels).
     scale-in suits overlays and modals (dialogs, dropdowns, popovers).
     Collapse is slightly faster than expand (300ms vs 350ms) — closing
     feels decisive, opening feels gradual. */

  /* --- Skeleton / Loading ---
       Shimmer over spinners. Products compose primitives (rectangle, circle,
       text-line) to match their content layouts.
       < 2s: shimmer only. > 2s: shimmer + progress. > 10s: navigate away + notify.
       No animation in email — all email layouts are static (Fixed). */
  --skeleton-bg: var(--color-gray-100);
  --skeleton-shimmer: linear-gradient(90deg, var(--color-gray-100) 0%, var(--color-sand-100) 50%, var(--color-gray-100) 100%);
  --skeleton-duration: 1400ms;                     /* @usage One shimmer sweep | @constraint A loop, not a transition — never alias the --duration-* scale, which carries transition levels and is a different kind of motion. Card 156 */
  --skeleton-easing: var(--ease-in-out);
  --skeleton-radius-rect: var(--radius-s);         /* Rectangle primitives */
  --skeleton-radius-circle: var(--radius-full);    /* Avatar/icon placeholders */

  /* --- Adaptive Responsive ---
       Component swap at breakpoints, not just scaling.
       The grid (12-col, container queries) handles layout.
       These tokens handle the stacking context. */

  /* Z-index hierarchy — canonical stacking order */
  --z-base: 0;
  --z-dropdown: 10;          /* Dropdowns, select menus, popovers */
  --z-sticky: 20;            /* The three page-level sticky elements (site header, portal side nav, in-page section list), and a table header row inside the table's own scroll area. Nothing else sticks */
  --z-sidebar: 30;           /* Sidebar navigation (when overlaying on mobile) */
  --z-search: 40;            /* Command palette / search overlay */
  --z-topbar: 50;            /* TopBar — always on top of page content */
  --z-modal: 100;            /* Dialogs, sheets, modal overlays */
  --z-toast: 110;            /* Toast notifications — above modals */

  /* --- Table Density Persistence ---
       Density (compact/default/comfortable) is a user preference,
       not a per-view design decision. Persist via localStorage
       or user profile. Key: 'aleris-table-density'.
       Default if no preference stored: 'default' (48px rows). */
  --table-density-preference-key: 'aleris-table-density';

  /* --- Images ---
       Format strategy: AVIF → WebP → JPEG (progressive enhancement via <picture>).
       Component sets the aspect ratio; image fills via object-fit: cover.
       EXIF metadata stripped by default — GPS data from clinic photos is a privacy risk.
       See aleris-baseline-images.md for full guidance. */

  /* Aspect ratios — used with CSS aspect-ratio property */
  --image-ratio-hero: 16 / 9;       /* Heroes, banners, video thumbnails */
  --image-ratio-content: 3 / 2;     /* Articles, staff portraits, cards — most versatile */
  --image-ratio-square: 1 / 1;      /* Avatars, thumbnails, small functional images */
  --image-ratio-document: 4 / 3;    /* Documentation, clinical photography */

  /* Border radius on images. Does not apply to an image flush to a card's
     top edge with zero clearance — that image takes no radius at all; the
     card's own --card-media-overflow clips it to match (§ Card). This token
     is for an inset image with clearance on all sides. */
  --image-radius-default: var(--radius-m);    /* 8px — softened corners */
  --image-radius-avatar: var(--radius-full);  /* Circular for profile images */
  --image-radius-none: 0;                     /* Full-bleed images */
}

```

<!-- source: baseline/BASELINE.md · status: accepted -->

# Baseline — AI Instructions

The Aleris brand is documented in two layers. **Foundation** defines identity and reasoning: colour, typography, voice, imagery, and the two reasoning tools (constant/contextual, emotional modes). **Baseline** is the implementation layer: the token architecture, component rules, and pattern library that apply Foundation across digital products. Read Foundation first; read Baseline for how to execute it.

When Foundation and Baseline conflict, Foundation takes precedence.

Before writing or modifying any UI code in an Aleris product, use only tokens from `aleris-tokens.css`. When uncertain about a decision, flag it as an open question in the code rather than guessing:

```css
/* OPEN: [description of the decision needed] */
```

---

## Token system

All values come from `tokens/aleris-tokens.css` — a three-layer architecture:

- **Primitives** — raw values (colors, spacing, type sizes). Platform-independent.
- **Semantic** — primitives mapped to usage (text colors, surface colors, states). Reference these in product code.
- **Component** — semantic tokens composed into component-level clusters (button, input, card, table, tab-nav).

Machine-readable token data with `usage` and `constraint` fields is in `tokens/baseline-tokens.json`. Consult it for lookup; where the two disagree, `constitutional/tokens-are-canonical.md` § 1 says which one wins and why no document may restate a value from either.

**Never use raw values.** No hex colors, no pixel values for spacing, no raw Tailwind color classes. Everything goes through tokens. This is a rule about product code, and it is not the same as the one above, which governs what a document may say.

**The one documented exception: a page rendered before the token stylesheet can load.** A server-rendered pre-auth surface (login, an error page, a maintenance page) is often served before the built, hashed-filename stylesheet is reachable — it sits behind the gate it is rendering, so it cannot `@import` or link `aleris-tokens.css` and has to inline its colours. The exception exists because a real build's first version of exactly this page used two values that aren't Aleris colours at all (`#d9663d`, `#e6e1da`) — there was nothing here to check against. **The four values a pre-auth page needs, cited from canonical rather than invented for this case:** page background `--surface-page` (sand-100, `#f2ece4`), text `--text-primary` (petrol-500, `#004851`), primary button `--color-orange-600` (`#d14811`), border `--border-default` (gray-100, `#d7d2cb`). Inlining these four is the exception; every other surface still goes through the token file. Pin the values with a test that extracts every hex a pre-auth page uses and asserts each one is one of these four — a build cannot see this rule fail otherwise.

**Serving this file.** The comments in `aleris-tokens.css` are its documentation and they are not optional — every `@usage` and `@constraint` annotation is a rule, and the supersede notes are why a value is what it is. They are also not for browsers. **Strip comments from the stylesheet at build time, keep them in the source a developer opens, and leave one pointer comment in the emitted file saying where the reasoning went.** Stripping is not minifying: remove the comments and the blank runs they leave behind, and nothing else — whitespace crunching, shorthand rewriting and selector merging each change what the browser computes if they are wrong, and the weight here is almost entirely in the prose.

Two reasons, and the second is the one that bites. **Weight:** the file is 50,487 bytes, roughly 34,000 of them comment. Gzipped it is 15.9 KB with the prose and 3.3 KB without, so about four fifths of the compressed download is English sentences — prose has little redundancy to squeeze while a token file is almost nothing but redundancy. On a page that inlines this file that is the largest single saving available, and one consumer measured 50.0 KB of comment in 82.4 KB of inlined stylesheet, on a patient-facing surface, on every load. **Build safety:** a comment body can contain a sequence that closes it early. Browsers recover silently and strict minifiers do not — this has already broken one consumer's production build, in dev-invisible fashion, and the fix at the time was to reword the comment. A build that strips comments before the minifier sees them is not exposed to it at all.

`patientguide/frame/lib/css.js` is a working implementation, including the guard that refuses to emit a stylesheet when an unterminated comment opener survives the strip — that failure mode silently swallows every declaration up to the next closer, which is a page with rules missing and no error anywhere.

---

## Hard rules

They apply to every surface, every product.

1. **Page background: sand, with everything layered on it.** Communicative = `--surface-page` (sand-100, #f2ece4). Instrumental = `--surface-page-instrumental` (sand-50, #faf8f6). The page's own background is always one of these two; a white or coloured section is a layer on top of it, even when it fills the screen. A section takes one of four colours: `--surface-section-white`, `--surface-section-cold` (petrol-100), `--surface-section-warm` (orange-300), or `--surface-section-strong` (petrol-500) for the one strong section on a page. orange-100 is not a section colour – it measures 1.01:1 against sand-100 and the two read as one surface.
2. **Cards and surfaces: white, not sand.** `--surface-card` is white. Sand on cards inverts the hierarchy. Cards stay white on any section; a white card on a white section is separated by its border and shadow.
3. **One primary CTA per screen.** Interactive orange (`--color-orange-600`, via `--button-primary-bg`) marks the primary action on a screen. Do not place two orange buttons in the same view. `--brand-accent` (orange-500) is the brand orange for accent, decoration and print, and no longer carries the primary button – white text on it fails AA.
4. **Status colors for state only.** Error red, warning amber, confirm green, goal colors — never decorative.
5. **Confirm green is an indicator, never a button.** `--color-confirm-500` (#4f866e) marks completion — status dots, badges, chips. It is **not an interactive fill**: white on it is 4.23:1, below the floor at rest, before any interaction. The confirm *button variant* was retired rather than recoloured; a completion action takes the petrol button form plus a required check glyph, so "I'm done" is carried by the glyph and the verb. Keep it distinct from `--color-goal-achieved` (#2e8540), which indicates "target met" on dashboards. **This is the rule of record for confirm; other documents reference it rather than restating it.**
6. **No all caps in content.** Sentence case everywhere. The only exception is abbreviations (AB, MRI) — see `foundation/typography.md`.
7. **Animate what does not move the layout, unless the user moved it.** Transform and opacity animate freely. Colour, background colour, shadow and stroke may transition to show a change of state. Size may animate only when the user caused it and it is contained — opening an accordion, expanding a panel — one element at a time, within `--duration-moderate`. Nothing changes size on its own: not on load, not on scroll, not behind the user's back. `prefers-reduced-motion: reduce` disables all of it.
8. **No animation in email.** All Aleris email layouts are static.
9. **14px is the floor.** `--font-size-xs` (14px) is the smallest text in the system. Nothing goes below it.
10. **No font-weight 400.** Museo Sans has no 400 weight. Using it causes browser synthesis. Use `--font-weight-regular` (500) or `--font-weight-bold` (700).

---

## Surface temperature

Baseline applies `foundation/constant-contextual.md` to surfaces through two modes: **communicative** and **instrumental**. Mode is determined by the situation, never the user's role — a patient managing their treatment plan in MyAleris is in instrumental mode; a staff member reading an internal newsletter is in communicative mode.

**Communicative** — Aleris tells, guides, explains, welcomes. Patient-facing content, marketing, service introductions. ~~onboarding~~ **Removed from this list.** Onboarding names when a flow happens, not what the person is doing in it, so it carries no mode of its own. Read the situation: a welcome sequence that introduces a service is communicative; setting up a treatment plan is instrumental from its first screen. The same holds for any word that names a stage rather than a situation — activation, renewal, offboarding.
- Page background: `--surface-page` (sand-100)
- Cards mandatory for content structure
- Generous spacing (`--spacing-lg` to `--spacing-3xl`)
- Full type scale, typography has room to breathe
- ~~Content spans 6–8 of 12 grid columns~~ **Superseded:** one reading column at every width, `--grid-column-communicative` — 640px up to a 768 window, growing a quarter of every further pixel to 768px from 1280 up. Decided by looking at three candidates rendered side by side (`baseline/conformance/layout/index.html`). The column holds 88 characters of 18px body at 768 and 105 at 1280, past the 45–75 guideline; that was seen and chosen, and the token says so. "6–8 columns" was never a measure proxy — 600–800px is 82–109 characters.
- Max-width: `--grid-max-width-communicative` (1200px)

**Instrumental** — The user works, navigates, configures, monitors. Tools, admin, dashboards, documentation, booking flows (a booking flow is instrumental throughout).
- Page background: `--surface-page-instrumental` (sand-50)
- Cards optional — content can live directly on the surface
- Tighter spacing. ~~compressed type range~~ **Superseded — surface temperature does not change type size.** Type size is the reader's to set and the browser already gives them that control — every size in the scale is a rem value, so a reader who raises their root font size raises the whole page, and the 14px floor is a floor at default settings rather than a cap. Instrumental density comes from layout utilisation and information hierarchy, which is what `aleris-design-governance.md` § Scanning vs. attending already says: *"not from spacing compression"*. "Compressed type range" told a builder to shrink type and named no token to do it with, because no mode-scoped type token has ever existed — the five mode-scoped tokens are all grid or background. Asserted in `tokens.test.ts`.
- Content can span all 12 columns. **Tiles and card grids:** fluid, no breakpoints — `repeat(auto-fit, minmax(var(--grid-tile-min-instrumental), 1fr))`, 14rem tiles, so the column count falls out of the width: one to 390, two at 640, three at 768, four at 1024, five from 1280. Four tiles leave an empty fifth slot from 1280 up; rendered, seen, chosen over the stepped 12→6→1 and 12→1 candidates. Tables scroll inside their own container at every width.
- Max-width: `--grid-max-width-instrumental` (1440px or fluid)

Mode does not shift between steps within a single flow.

**Sticky elements.** Three things may stay in view while the page scrolls: the site header, the portal side nav, and an in-page list of a long page's sections, which also marks the section being read. Nothing else sticks to the page. Inside a table's own scroll area its header row may stick, because it takes no space from the page. Tab navigation sticks only when it is the page's list of sections.

---

## Component shapes

Radius by functional role:

- **Cards**: `--radius-l` (16px) — from computed styles measured live on aleris.se/.no/.dk (every card ships at 16px, not Baseline's former 4px). Derived inward: nested elements take the card's own radius at zero gap, floored at 0. Cards only, for now — panels, modals and tables were never measured live and stay `--radius-s`.
- **General containers** (panels, modals, tables): `--radius-s` (4px)
- **Interactive elements** (buttons, inputs, dropdowns): `--radius-m` (8px)
- **Indicators** (badges, tags, avatars): `--radius-full` (100px)
- No full-pill buttons exist in Aleris interfaces.
- **No generated side accent.** **Never** mark an element as active, selected or important with a coloured bar, border or shadow along one of its sides. **Because** it is a default of AI design tools, not an Aleris choice. It makes the work read as generated rather than designed, and it carries state in a thin strip of colour alone. **Instead** mark the state with weight and fill: bold petrol text on a petrol-100 fill for an active navigation item, a 1px petrol-500 border all the way round for a selected card. A quotation may keep a thin, flat, neutral rule along its side (`--border-default`): that is a typographic convention, not a state marker. A rounded, shadowed or curved side treatment is not.
- **Flush top-of-card media** (a hero image with zero clearance from the card's top edge): carries no radius of its own. The card clips its contents to its own rounded shape (`--card-media-overflow: hidden`), which renders the image's top corners matching the card and its bottom edge square, for free — nothing to keep in sync, and the inversion the 4px card tier used to produce against an 8px image (see the corner-radius reasoning below) cannot recur. Scoped to cards that actually contain flush media, not every card, so a plain card stays free to let content overhang its edge without a second decision. Does not apply to an inset image with clearance on all sides, which keeps `--image-radius-default` (8px) unchanged.
- The radius set is `0 / l / m / s / full`.

**The corner-radius reasoning.** Baseline assigned radius by role — container, interactive, indicator — which is easy to teach but ignores the geometric convention that nested rounded rectangles must be concentric: an outer radius has to be at least its inner radius plus the gap between them, or the two curves aren't parallel and the corner reads as an error. At Baseline's 24px card padding, nothing sits close enough to a card's edge for this to show. It stopped being invisible the moment an image sits flush against the card. Rather than resolve that one pair in isolation, the question was reframed as one problem — what should the default corner state be — and answered from what production had already settled empirically rather than from a rule invented for the occasion. A four-model comparison (`baseline/conformance/radius/index.html`, unpublished) made the candidates visible side by side before the decision, per the same design-by-looking approach as `baseline/buttons/index.html`.

---

## Elevation

Shadows come from the ladder `--shadow-e0` to `--shadow-e3`, through the elevation tokens: a card rests at `--elevation-card` (`--shadow-e1`), a dropdown at `--elevation-dropdown` (`--shadow-e2`), a modal at `--elevation-modal` (`--shadow-e3`). **One featured element may rest one step up.** On a communicative page, a single featured element, such as the text card of a hero, may rest at `--shadow-e2` instead of `--shadow-e1`. One per page; a page where several things rest one step up has no featured element.

---

## Lists

Items in a list are separated by space from the spacing scale. A list of six or more items may also carry a horizontal divider between items (`--border-default`); a shorter list never does. This holds for lists across Aleris design. Tables are not lists and keep their own rules in § Tables.

---

## Typography

See `foundation/typography.md` for the typography system and principles. Baseline implementation:

- Reference typography tokens from `aleris-tokens.css` (`--type-body-*`, `--type-lead-*`, `--type-h1-*`, etc.) rather than specifying pixel sizes.
- Font weights available: `--font-weight-regular` (500) and `--font-weight-bold` (700). Do not use font-weight 400 — Museo Sans has no 400 weight and browser synthesis distorts the letterforms.
- **The licensed Museo Sans has no tabular figures, at either weight.** Setting `font-variant-numeric: tabular-nums` against it changes nothing and says so nowhere — measured in a browser against Arial as a control, digit advance widths are identical with and without the property, and the ten digits spread 8.72px at weight 500 and 7.50px at 700 where Arial spreads 0. At a 16px value size that puts 8.2px between "111,1" and "000,0". Right-alignment is what aligns a numeric column; the property is a silent no-op. *(Measured by `sundviktlakemedel`, 2026-08-25.)*
- **The licensed Museo Sans has no arrow glyphs, and no check mark.** U+2192 →, U+2190 ←, U+2264 ≤, U+2265 ≥ and U+2713 ✓ are absent from both licensed weights, so any of them in running text falls back to another face silently — a different stroke weight and baseline beside the words it sits in. Present and safe: en and em dash, bullet, ×, …, °. Use an icon for an arrow or a check, never the character. *(Measured 2026-09-05 by reading the character maps of `MuseoSans_500-webfont.woff2` and `_700`: 229 and 228 mapped codepoints. First noticed by `patientguide`, 2026-08-13.)*
- `--type-lead-*` is used once per section, after the heading.

---

## Spacing

Modular scale, 1.5 ratio, 4px grid. Use tokens — never fabricate values.

| Token | Value | Typical use |
|-------|-------|-------------|
| `--spacing-3xs` | 4px | Minimum gap, label-to-field |
| `--spacing-2xs` | 8px | Tight inline gaps |
| `--spacing-xs` | 12px | Related elements, input padding-y |
| `--spacing-sm` | 16px | Component padding |
| `--spacing-md` | 24px | Section spacing, card padding |
| `--spacing-lg` | 36px | Between components |
| `--spacing-xl` | 48px | Between sections |
| `--spacing-2xl` | 72px | Major section breaks |
| `--spacing-3xl` | 96px | Page-level (communicative only) |

---

## Buttons

Six variants, each with a job:

- **Primary** (orange): "Do this" — main action, booking, submission
- **Secondary** (petrol): "Go here" — navigation, exploration
- **Outline**: Less prominent alternative
- **Ghost**: Tertiary, minimal visual weight
- ~~**Confirm** (green): "I'm done" — completion, sign-off. Only for: Klarmarkera, Godkänn, Signera, Markera som klar. Not for generic "OK" in dialogs.~~
- **Small**: Compact variant for tight spaces

> **Superseded.** The confirm button variant listed above is retired; hard rule 5 is the rule of record for confirm.

All buttons use `--radius-m` (8px). No full-pill buttons.

**Contrast is the constraint.** No interaction state may take a component's text below the AA floor against its own fill.

**Darkening is the convention for interaction states.** If a component already sits at the palette's highest contrast, darkening will lower contrast. That is permitted so long as it stays above the floor.

> **Superseded.** The two rules above replace *"Hover lightens, never darkens"*: the floor is the rule, the direction is the convention.

**Primary on a petrol surface inverts.** orange-600 is specified for light surfaces and must never be placed on petrol. On a petrol ground the primary action becomes white fill with petrol-500 text (10.27:1) – `--button-primary-inverse-*`.

**No primary on the warm section.** orange-600 measures 2.82:1 against `--surface-section-warm` (orange-300), below the 3:1 a fill needs against the surface it sits on. An action on a warm section takes the secondary form. On `--surface-section-cold` the primary holds at 3.40:1.

**Filled buttons carry a bevel.** At rest a filled button has a soft shadow down and to the left and a thin highlight along the top edge, facing the light; when pressed it sinks into an inward shade. This is a top-edge highlight from the light source, not a side accent, so it does not conflict with § Component shapes. Tokens: `--button-{variant}-shadow` at rest, `--button-press-shadow` when pressed, together with `--state-press-translate-y`.

**Hover on a clickable card.** A card that is a link moves from the card shadow to the next step up (`--shadow-e2`) and rises 2px on hover; an arrow in it moves 4px towards where it leads. Transform and shadow only, `--duration-fast`, ease-out, off under reduced motion.

**States.**

- **Hover is a ring, not a fill.** One petrol ring specified for six variants on two surfaces produced two invisible focus rings – petrol-500 on a petrol-500 fill (secondary) and petrol-500 on the petrol-500 page (primary-inverse), both 1.0:1. Rather than add another fill step to fix hover and a second mechanism to fix focus, the two merged: **hover draws the outer ring only, focus draws both.** No `--button-*-hover-bg` token exists any more, and none should be added – a hover fill that changes colour is what the "hover lightens/darkens" rule used to constrain, and the constraint no longer applies because there is no fill to constrain.
- **Focus is a dual ring: 2px white inner, 2px petrol-500 outer**, both always drawn. Whichever ground a button sits on, one ring contrasts – the inner against the button's own fill, the outer against the page. `primary-inverse` swaps the two, because it sits on petrol rather than sand: `--button-primary-inverse-focus-ring` (outer position) is white, `--button-primary-inverse-focus-ring-inner` (inner position) is petrol.
- **Active (press) is per-variant, not a shared tint.** `--state-active` (sand-500) was retired rather than repointed – it was never a button fill, and reads 1.93:1 under white. Primary and primary-inverse reuse what used to be their hover fill (orange-700, petrol-100); secondary and confirm take a new step, petrol-700 (13.88:1); outline and ghost take petrol-100. A supplementary, unasserted second channel – `--state-press-shade`, a neutral inset, plus a 1px translate dropped under `prefers-reduced-motion` – is deliberately not petrol-tinted, for the same reason a single focus ring failed: a petrol inset on a petrol fill is invisible.
- **Buttons do not disable.** A disabled button dissolves into the page (gray-100 on sand-100 is 1.28:1) and offers no explanation for why it is unavailable. Patient-facing: prefer explain-on-click – clicking reveals the prerequisite rather than the control simply not responding. Instrumental: a transparent, gray-500-edged treatment is acceptable where the workflow context is self-evident to the user. `--state-disabled-*` remains for inputs, which have their own disabled semantics – see § Forms.
- **Buttons must survive text-zoom without causing horizontal scroll.** From a real conformance failure: a normal-length label at 200% text size on a 320px viewport measured wider than its container and pushed the page into horizontal scroll — an EN 301 549 11.7 failure, not a cosmetic one, since 11.7 requires layout to follow the platform's text-size setting. `max-width: 100%` and `white-space: normal` (label wraps to a second line rather than the button overflowing) are required on the button primitive, not optional per-instance styling. **The same failure recurs at the row level**: a flex row of buttons or other controls (a role switch, a toolbar) needs `flex-wrap` for the identical reason — a non-wrapping row overflowed a 375px viewport before the fix, independently of any single button's own width.

See `baseline/buttons/index.html` for every variant against every state, measured live from these tokens, and `tokens/tokens.test.ts` for the fitness check. One documented failure remains: ghost's press fill (petrol-100, reused from outline) has no border to carry its boundary, at 1.13:1 against sand-100 – tracked as `KNOWN_BOUNDARY_FAILURES['ghost/active']`. Ghost has no instances in `app/` or `components/`; fixing it means giving ghost a border, which is a design call about what distinguishes it from outline, not made here.

---

## Forms

- Labels above fields.
- Mark optional fields "(valfritt)", not required fields.
- Error text: "what happened + what to do" below the field, replacing helper text.
- `aria-invalid="true"` + `aria-describedby` for error linking.
- Focus: `focus-visible` with petrol ring, 2px offset. Never `focus`.
- Disabled: use sparingly. Patient-facing: prefer explain-on-click. Instrumental: acceptable when workflow context is self-evident.
- Read-only: looks like content, not a greyed-out input.
- **Error text sits on a white card or an instrumental ground, never directly on the communicative ground.** `--input-error-color` (error-500) measures 5.03:1 on white (clears), 4.74:1 on sand-50 (clears) and 4.28:1 on sand-100 (fails). `--input-error-size` is `--font-size-xs` at regular weight, so the failing case is the ordinary one — a field error at the smallest size in the system. A form on a communicative page goes in a card, which is already the rule there (§ Surface temperature: *"Cards mandatory for content structure"*). Enforced by `tokens.test.ts` § *"error text clears AA on every ground this rule permits"*, which reads the permitted grounds out of this bullet rather than holding its own list.

---

## Status messages

**Icon plus label, on error and on success both.** A status message carries its meaning in an icon and in words, never in the fill or text colour alone – red and green are the pair that collapses under red-green colour vision deficiency, which is roughly 8% of men. Success already required a checkmark in the goal-status rules; error now carries the same requirement, so the two are distinguishable with colour removed.

This is the domain implementation of the constitutional rule that meaning is never carried by colour alone. The rule itself lives once, at the constitutional layer – this section says what it means for a status message, it does not restate the rule.

---

## Tables

Two types: **read tables** (display data) and **work tables** (interactive).

- Three density levels: compact (36px), default (48px), comfortable (64px)
- **Note to builders:** user-selectable density that persists as a preference is the expectation for instrumental work tables, not a hard requirement every table must ship on day one. Build toward it; a first cut that ships with only the default density is a known gap, not a violation.
- Headers quieter than data (smaller, sentence case)
- Identifier in column one, always
- Numbers right-aligned, text left-aligned — right-alignment is what actually aligns a numeric column here, for the reason under Typography above
- Actions near the identifier, not at the far right

---

## Motion

Four durations, two curves.

| Token | Value | Use |
|-------|-------|-----|
| `--duration-instant` | 100ms | Focus rings, checkbox toggles |
| `--duration-fast` | 200ms | Buttons, hover, tooltips |
| `--duration-moderate` | 350ms | Modals, accordions, sidebars |
| `--duration-slow` | 500ms | Page transitions (sparingly) |

- `--ease-out`: elements arriving
- `--ease-in-out`: elements moving
- Never use `ease-in` alone — slow start reads as unresponsive.

Loading: skeleton shimmer, not spinners. < 2s: shimmer only. > 2s: shimmer + progress. > 10s: navigate away + notify.

---

## Anti-patterns (healthcare-specific)

If you experience a conflict between design requirements and anti-patterns, please note it and leave a comment.

1. **Consent asymmetry** — consent and decline must be visually equal. Same size, same prominence, same steps.
2. **Cost burial** — surface pricing before any data entry or emotional investment.
3. **Urgency fabrication** — no countdown timers or scarcity messaging unless clinically real and verifiable.
4. **Obstacle withdrawal** — anything booked online can be cancelled online with equal ease.
5. **Informational hiding** — risks and qualifying information inline at point of decision. Never progressively disclosed.
6. **Data maximalism** — collect only what the current service requires. Optional = labeled "(valfritt)".
7. **Default escalation** — no pre-selection on choices with financial, clinical, or privacy consequences.
8. **Emotional exploitation** — no shame, guilt, or fear to drive action. Motivate through clarity.
9. **Exit penalty** — no degraded experience for choosing a less profitable path.
10. **Notification coercion** — notifications for clinical relevance only. Marketing is separate with distinct opt-in.

**Smell test:** Is it easier to accept than to decline? Does the flow hide information that would change the decision? Does it require something not necessary for care?

---

## Accessibility

- WCAG 2.1 AA minimum
- 14px font floor (enforced by token scale)
- Meaning is never carried by colour alone – the rule is constitutional, see `constitutional/accessibility-is-foundational.md`. In this system it means pairing colour with icon, shape, or text, and it means error and success must be distinguishable from each other with colour removed (see Status messages above)
- **44px minimum touch/click target** (`--button-min-height-touch`, `--button-padding-y-touch`) on patient-facing and touch contexts. **Relaxed for dense instrumental surfaces on desktop pointer input:** 24×24 CSS px is the floor there (WCAG 2.1 AA 2.5.8 Target Size Minimum), not 44×44 (AAA 2.5.5 Target Size Enhanced, which 44px implements) — `--button-min-height-pointer`. A dense work table or admin toolbar built for mouse/trackpad use may use the smaller target; anything a patient touches, or anything that could be used on a touchscreen, keeps 44px.
- `focus-visible` for keyboard navigation, never `focus`. **The ring differs by control, and one rule for all of them cannot hold** — a single ring colour collides with something on six button variants across two surfaces (petrol-500 on a petrol-500 fill is 1.0:1). Inputs keep a single petrol ring, 2px offset – see § Forms. Buttons use a dual ring, inner and outer, so one half always contrasts regardless of ground – see § Buttons
- `prefers-reduced-motion: reduce` disables all animation
- `aria-invalid` + `aria-describedby` for form errors

---

## File reference

**Foundation (read first)**

| File | Purpose |
|------|---------|
| `foundation/index.md` | Brand in Brief — entry point to all Foundation pages |
| `foundation/colour.md` | Colour |
| `foundation/typography.md` | Typography |
| `foundation/voice.md` | Voice — "den nära experten" |
| `foundation/constant-contextual.md` | Constant and contextual — a reasoning tool |
| `foundation/emotional-modes.md` | Emotional modes — a reasoning tool |
| `foundation/imagery.md` | Imagery |

**Baseline (implementation)**

| File | Purpose |
|------|---------|
| `data-products/tokens/aleris-tokens.css` | Source of truth — all token values |
| `data-products/tokens/baseline-tokens.json` | Generated — token lookup with usage/constraint |
| `data-products/tokens/aleris-tailwind.css` | Optional Tailwind v4 theme bridge — `bg-aleris-petrol-500`, `p-aleris-md`, and so on, each compiling straight to a token. Generated from the token file, holds no value of its own, adds a namespace and switches Tailwind's own colour palette off; import it after Tailwind |
| `schemas/design-token.md` | The token record's shape, the layer architecture, framework/Figma integration. Not yet in the `agent-baseline` package |
| `principles/design-tokens.md` | Why each token value is what it is. Corpus-only — reasoning, not a build constraint |
| `governance/aleris-anti-patterns.md` | 10 healthcare-specific behavioural guardrails |
| `governance/aleris-design-governance.md` | Design decisions with certainty levels |
| `governance/aleris-privacy-jtbd-analysis.md` | Privacy as interaction design |
| `reference/aleris-progressive-enhancement.md` | Foundational design principle |
| `reference/aleris-grids-tables-dataviz.md` | Grid, table, and dataviz specs |
| `reference/aleris-baseline-animation.md` | Motion system (implements constant-contextual and emotional-modes) |
| `reference/aleris-baseline-images.md` | Image formats, aspect ratios (implements imagery) |
| `voice/` | Digital voice patterns, behaviour notes (implements voice) |
| Icon path data | **3,772 canonical FA Pro Regular SVGs are already extracted and reachable — no FA kit token required.** Fetch a glyph at `https://brand.dev.aleris.ai/icon-picker/svgs/regular/<name>.svg`, with the name index at `public/icon-picker/metadata/icons.json`. Only *re-extraction* on an FA upgrade needs the kit. Lives at `public/icon-picker/svgs/regular/` in the Brand OS repo, regenerable via `scripts/build-icon-metadata.py`. Any name may be used except the motifs `principles/iconography.md` § What we never use excludes |
| `logo/` | The logo files, the four ratified constants (clear space, primary variant, minimum height, prohibitions) and the sender rule. Read `logo/README.md` before placing the logo anywhere. New in `agent-baseline` v0.7 |

---

*Baseline — maintained by Torfinn Almers, Head of Design, Aleris Group.*

> *What changed on this page, and when, is in `workspace/status/changelog.md`. This page carries the rules and the reasoning behind them; the dates live there. A struck-through value beside its replacement is not a change note — it is the rule telling you what it no longer is.*
