■ Demo— not a validated GxP system — synthetic data

Affinis

GovernanceDesign system

Affinis design system

v0.1 · tokens --af-*

The reference for every screen in Affinis, written for implementers. Each token is a CSS custom property, each rule is testable, and each component below is the live component rather than a picture of one. Where this page and a screen disagree, this page wins.

1Principles

Six rules the rest of the system follows from. Where a later decision had to break one, it is written down in section 12 rather than quietly taken.

01Data is the interfaceNumbers, structures and curves carry the screen. Chrome stays quiet: borders rather than shadows, one accent, no decoration.
02Every value states its unit and originNo bare numbers. A value shows its unit and qualifier, and whether it is raw, derived (with version), calculated or predicted.
03Raw is untouchableOriginals are read-only and labelled as such. Masks, normalizations and fits are new layers with their own versions.
04Never colour aloneEvery state pairs colour with a word, icon, shape or strike line. The screen must still read in grayscale.
05Consequential actions have frictionMasking, rejecting, refitting and approving all ask for a reason or a signature. Nothing that changes a record is one click.
06Honest by defaultThe demo banner is permanent. Model outputs say “(pred.)”, poses say “modeled”, and thresholds are shown next to the values they judge.

2Colour

One cool-neutral ramp, one cobalt accent, four status hues. Every ratio below is computed from the token the browser resolved, against this app's two grounds — the panel surface and the page background. A text token that fell below 4.5:1 would be marked here, and a unit test asserts the same numbers against the ones the written design system publishes.

Neutrals

Neutrals tokens, their values and their contrast ratios
SwatchTokenValueRoleUsevs surfacevs background
--af-bg—groundApp background——
--af-surface—groundPanels, tables, dialogs——
--af-surface-sunken—groundSegmented track, read-only fields, row dividers——
--af-border—non-textPanel borders, dividers——
--af-border-strong—non-textControl borders——
--af-text—textPrimary text, data——
--af-text-2—textSecondary text, axis lines——
--af-text-3—textLabels, captions. The lightest text the system allows.——
--af-masked—non-textMasked wells, asymptotes——

Accent

Accent tokens, their values and their contrast ratios
SwatchTokenValueRoleUsevs surfacevs background
--af-accent—textPrimary action, selection, links, focus ring——
--af-accent-hover—textHover and pressed state of accent——
--af-accent-strong—textText on accent-subtle, active nav item——
--af-accent-subtle—groundSelected rows, active nav, CI band——

Status

Status tokens, their values and their contrast ratios
SwatchTokenValueRoleUsevs surfacevs background
--af-ok—textApproved, QC passed, included——
--af-ok-subtle—groundApproved panel ground——
--af-danger—textRejected, QC failed, destructive——
--af-danger-subtle—groundRejected panel ground——
--af-warn—textFlagged, threshold breach——
--af-warn-subtle—groundFlagged badge ground——
--af-warn-mark—non-textLC wells, replicate 2 marker——

Banner and viewer

Banner and viewer tokens, their values and their contrast ratios
SwatchTokenValueRoleUsevs surfacevs background
--af-banner-bg—groundDemo banner ground——
--af-banner-accent—non-text“Demo” keyword; H-bonds in 3D——
--af-viewer-bg—ground3D viewport ground——
--af-viewer-border—non-text3D viewport borders——

3Data scales

Data colours are separate from the interface colours and are never reused for meaning elsewhere. Each scale keeps its order in grayscale, and the value is always printed as well — the colour only ranks it.

Plate heatmap — viridis, 10 stops

--af-seq-viridis-0…9
0
1
2
3
4
5
6
7
8
9

Interpolated linearly between stops. Raw mode maps the plate's fixed range (about 4,000–48,000 RFU); % inhibition maps −10 to 110 %. A masked well overrides to --af-masked with a strike line, so it never reads as data.

Potency — cobalt, 7 bins

--af-seq-potency-0…6
5.5
6
6.5
7
7.5
8
≥ 8.5

Bins are 0.5 log units from pIC50 5.5; the last is open-ended. Text turns white from bin 4. The number is printed in every cell — a reader must never have to decode a shade to know a potency.

Plate layout

HCHigh controlLCLow controllowCompound, lowest concentrationhighCompound, highest concentration

Compound wells run a grey ramp from the lowest concentration to the highest, and the column groups are labelled above the plate as well as coloured.

Series in charts

Replicate 1Replicate 2Replicate 3Masked

The marks are the real ones the dose–response chart draws. A series must be distinguishable by shape alone, so a masked point is hollow and slashed rather than merely a different colour — and it stays on the chart, counted, at 100 nM like every other point.

4Typography

IBM Plex Sans for interface text, IBM Plex Mono for identifiers, hashes, timestamps and every number. Both are self-hosted woff2; there is no CDN request at run time. All numerals are tabular, so a column of values lines up.

The type scale
TokenUtilityUseSpecimen
--af-type-pagetext-pagePage title, one per screenPlate review
--af-type-sectiontext-sectionDialog and section titlesElectronic signature
--af-type-paneltext-panelPanel headersPlate QC
--af-type-bodytext-bodyDefault text and controlsPlate failing any rule are rejected automatically.
--af-type-smalltext-smallTable cells, metadataRead 2026-11-04 16:42:19 UTC
--af-type-labeltext-label uppercaseColumn headers, field labelsConcentration
--af-type-datafont-data text-dataIdentifiers, values, hashesAFN-000215-01
--af-type-metricfont-data text-metricOne key figure per panel0.72

A label is uppercased by CSS, not by the text, so a unit inside one keeps its case — µM must never arrive on screen as MM.

5Numbers, units and identifiers

Every rule below is enforced by one shared module with unit tests. A component never formats a number itself — including this page, whose right-hand column is the module's own output.

Formatting rules and what the shared module prints
QuantityRuleAffinis printsNever
ConcentrationThree significant figures, in the unit that keeps the value in [1, 1000). µ, never u.123 nM3.33 µM0.508 nM123.457nM · 0.00333 mM · 3.33 uM
PotencypIC50 to two decimals. IC50 to three significant figures with its 95 % CI.pIC50 7.6219.8–29.1 nMIC50 = 0.000000024
Censored valuesKeep the qualifier; never coerce it to a number.> 10.0 µM< 0.508 nM10000 · 0
Log scaleName the base and the unit, and use the true minus sign.−7.43log conc · -7.43
Unit spacingA non-breaking space between number and unit, % included. Fold change uses × with none.54.2 %34×54.2% · 34x
Large numbersComma for thousands and a point for decimals, whatever the browser's locale is.42,180 RFU34,65642.180 · 4.2e4
TimestampsISO 8601 in UTC, to the second, with the zone written out. Never a relative time alone.2026-11-04 16:42:19 UTC04/11/26 5:42 PM · 3 hours ago
HashesMono, algorithm named, middle-ellipsized; the whole value is there on copy.sha256 3f9a…c21e3f9a2b…

A value that was never measured is an em dash with the reason beside it, never a zero and never a blank cell. A predicted value is marked (pred.) and a computed one (calc.) in the column header, every time.

6Space, shape and density

A 4 px grid, four radii and a fixed set of heights. Page padding 24, panel padding 16, 16 between panels and 8 between controls. There are no resting shadows: a panel is a one-pixel border, and the only elevation in the system belongs to a dialog.

Spacing

  • --af-space-0_5—
  • --af-space-1—
  • --af-space-2—
  • --af-space-3—
  • --af-space-4—
  • --af-space-5—
  • --af-space-6—
  • --af-space-8—
  • --af-space-10—

Radius

rounded-wellWells, data cellsrounded-badgeBadges, pillsrounded-controlButtons, inputs, menusrounded-cardPanels, dialogs

Density

  • --af-h-control—Buttons, inputs, selects
  • --af-h-tab—Segmented tab
  • --af-h-row—Table row
  • --af-h-row-compact—Dense table row
  • --af-h-row-sar—SAR row, with its 2D depiction
  • --af-h-banner—Demo banner
  • --af-h-topbar—Top bar
  • --af-w-nav—Left navigation

7Components

Every specimen below is the live component. One primary button per view; a trailing ellipsis means the action asks for something first — a reason, a signature, a constraint — because nothing that changes a record is one click.

Button

Disabled is opacity .45 and a not-allowed cursor, never a button that silently does nothing.

Status

QC passedApproved · lockedRejectedFlaggedPending reviewMasked

Icon, word and colour — all three, every time, so the screen still reads in grayscale.

Segmented

Changes the view of the same data; it is a radiogroup, not three buttons.

Chip

Filters data. The pressed state is on the element, so a screen reader hears it.

Potency cell

8.417.656.02> 5.00—

The scale ranks it; the number says what it is. A censored result keeps its qualifier.

Fold bar

3.1×34×120×—

A log track to 100×, bold from 30×. The bar never starts anywhere but zero.

Threshold value

2.95.8

Amber text when the value is outside its MPO threshold. No fill, so the grid stays calm.

Metric

Z′0.72accept ≥ 0.50
CV, high control7.9 %accept ≤ 5.0 %

One key figure per panel, with the rule that judges it beside the number.

Key / value

Raw signal
23,410 RFU · original, read-only
% inhibition
54.2 % · derived, norm. v2

A raw value says it is original and read-only; a derived one names the version it came from.

Avatar

The initials disc from the top bar. Always decorative: the name it abbreviates is beside it.

8Data-integrity patterns

ALCOA+ in the shape of the interface. Raw values are read-only and labelled; every derivation is a new version with its own reason; masked data stays visible and counted; and nothing that changes a record happens without a reason or a signature.

Electronic signature

Electronically signed

M. Laurent (mlaurent)

2026-11-04 17:40:00 UTC

Meaning: Approved

sha256 3f9a…c21e

  1. The dialog states the record, its version and its key values before anything is signed.
  2. The signer picks a meaning — Approved or Reviewed — and it is written onto the signature.
  3. The password is re-entered for every signature, even inside an active session.
  4. The manifestation always shows name, meaning and UTC time together, with the record's hash.
  5. After approval the record is locked by the database. The only remaining action is a corrected version, with its own reason and its own signature.

Audit — this record

Audit — this record
Timestamp (UTC)ActorEventDetail
2026-11-04 17:03:11 UTCmlaurentWell D15 maskedDispense error — tip dispense error flagged in instrument log for D15
2026-11-04 17:03:11 UTCsystemNormalization v2 computedExcludes D15 · v1 retained, read-only
2026-11-04 17:40:00 UTCmlaurentRe-authenticatedPassword verified for signing
2026-11-04 17:40:00 UTCmlaurentSigned — ApprovedMeaning: Approved · fit v2

Append-only, and there is no edit or delete affordance anywhere on it — because there is none in the database either: the tables refuse an UPDATE and a DELETE, and the chain of hashes is recomputed on demand from the regulatory screen.

9Charts

Drawn with this project's own scale and path helpers — copy-and-own from Exalus, not a charting library. The live examples are on dose–response; the marks themselves are in section 3.

Axes
Axis line and ticks in --af-text-2, gridlines in --af-surface-sunken, tick labels in Plex Mono 11 px. Every axis carries a title with its unit.
Dose–response
x is log₁₀ concentration in M, increasing right; y is pinned to −10…110 % so two curves from different screens can be compared by eye. Fitted curve 2 px ink, asymptotes dashed in --af-masked, the IC50 drop line dashed accent, the 95 % CI an accent-subtle band.
Annotations are HTML
The IC50 pill is a positioned element over the SVG, not SVG text: it stays selectable, it wraps, and it is read out in order.
Scale honesty
Bars start at zero. A log scale says it is one. Nothing auto-zooms to make a difference look bigger, and a fit is never drawn past the concentrations that were tested.

10Structures

Two renderers, both written here. The 2D depiction is SVG drawn from coordinates RDKit computed at build time; the 3D viewer is WebGPU impostors with a Canvas 2D fallback. Neither calls a structure library at run time.

2D depiction — live, from the registry

NHNN
AFN-000100
NHNNN
AFN-000101
NHNNN
AFN-000102
  • Bond stroke 1.15 px with round caps; bond length is scaled to the box and never above 17 px.
  • A double bond is an inner line offset 20 % of the bond length towards the ring centre and trimmed 16 % at each end.
  • Bonds stop 30 % short of a heteroatom label, so the label sits in a gap rather than on a painted mask.
  • Condensed groups read outward from the attachment point: OMe to the right, MeO to the left. NH draws its hydrogen below.
  • Every depiction has an accessible name — the compound and its R groups — and copies as SMILES.

Atoms — 2D depiction, ink on white

Atoms — 2D depiction, ink on white tokens, their values and their contrast ratios
SwatchTokenValueRoleUsevs surfacevs background
--af-atom2d-c—textCarbon and the bond skeleton——
--af-atom2d-n—textNitrogen——
--af-atom2d-o—textOxygen——
--af-atom2d-halogen—textF, Cl, Br, I——
--af-atom2d-s—textSulfur, phosphorus——

Atoms — 3D viewer, marks on the dark viewport

Atoms — 3D viewer, marks on the dark viewport tokens, their values and their contrast ratios
SwatchTokenValueRoleUsevs viewport
--af-atom3d-ligand-c—non-textLigand carbon—
--af-atom3d-protein-c—non-textProtein carbon—
--af-atom3d-n—non-textNitrogen—
--af-atom3d-o—non-textOxygen—
--af-atom3d-s—non-textSulfur—
--af-atom3d-halogen—non-textHalogen—
--af-atom3d-dot—non-textPocket surface dots—
--af-hbond—non-textHydrogen bonds and their distance pills—
--af-measure—non-textMeasured distances and angles—

The 3D palette is checked against the viewport ground rather than white, which is why every row of it is marked non-text: nothing in the viewer carries a glyph. The labels that do — residue names and distance pills — are HTML over the canvas, in --af-viewer-text and --af-hbond on a translucent dark backing. See the structural context screen for both in use.

11Accessibility

WCAG 2.2 AA, checked rather than claimed: the e2e suite runs axe over every route and a violation fails the build.

  • WCAG 2.2 AA. Every text token clears 4.5:1 on both grounds, and the colour table above computes that rather than quoting it.
  • State is never carried by colour alone: every status is an icon, a word and a colour; masked data keeps a strike line; chart series differ by shape.
  • Focus ring: 2 px accent, offset 2 px, on every interactive element — including the plate wells and the canvas of the 3D viewer.
  • Pointer targets are at least 24 × 24 px (SC 2.5.8). A 384-well plate's wells are 26 px.
  • The plate is a grid widget: arrow keys walk the wells and each one has a name a screen reader can read (“H07, compound 123 nM, 23,410 RFU, masked”).
  • No single key commits a record. The signature dialog is the only path to an approval, and it asks for the password again.
  • Every route is checked with axe in the e2e suite; a violation fails the build.
The focus ring, on the real control — tab to it:

12Implementation, and where it differs

The tokens live in styles/tokens/affinis.css and are exposed to Tailwind as its theme. Everything on this page reads them at run time, so nothing here can quote a value the app does not use.

CSS Modules → Tailwind on the tokens
The handoff's §12 asks for plain CSS Modules. The app uses Tailwind 4 with the tokens as its theme, so a component writes `bg-surface` and `text-ink-2` rather than importing a stylesheet. The rule the handoff was protecting — no raw value in a component — is enforced instead by two lint checks: one rejects a hex, px or ms outside the token files, the other rejects any Tailwind class that is not in its canonical form.
tokens.ts mirror → read the stylesheet
A typed mirror of the tokens was specified for canvas and WebGPU code. There is none: the 3D viewer reads the tokens it needs with `getComputedStyle` at run time, and this page does the same. A mirror is a copy, and a copy drifts.
Cartoon ribbons → not in v1
§10 lists a cartoon toggle for the 3D viewer. A ribbon needs secondary-structure assignment and a tube mesher; ADR 0008 ruled both out of v1. The control is in the toolbar, disabled, with a title that says so.
`/` to focus search
§11 specifies it and the top bar prints the hint. The shortcut itself is not wired up yet.