OX Design System

Tokens, components, and page conventions for ox.antalfamily.ca. Canonical reference for everything in ox.css.

Last updated: Jul 2, 2026 11:00 AM CEST

★ Design principles

Tokens are the what. These principles are the why. When a token or component doesn't fit, fall back to the principle, not improvisation.

Legibility high priority

Text-heavy pages exist to be read. Optimize for a comfortable reading experience first; everything else is secondary.

Accessibility

The site is personal but should still meet baseline web accessibility — partly because it's the right thing, partly because Andrea's own readability needs (small text, low contrast) overlap with WCAG concerns.

Harmony

A design system is a contract: every page draws from the same tokens, the same components, the same palette. Visual consistency comes from restraint, not vigilance.

Information architecture

Pages tell readers where they are, where they can go next, and what's most important first. Structure works with the reader; clutter works against them.

1 · Fonts

Two families, loaded from Google Fonts. Every page must include the link tag below in <head>.

FamilyUsed forWeights loaded
Source Serif 4 Body / prose / h1 / .stat-value 400, 500, 600, 700 (variable opsz 8–60)
DM Sans UI: nav, headings h2–h4, buttons, table copy, labels 400, 500, 600
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,400;8..60,500;8..60,600;8..60,700&family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="ox.css">

Monospace: 'SF Mono', Menlo, Consolas, monospace — used for inline code in prose. Not loaded as a webfont; relies on the system stack.

2 · Color tokens

All colors are CSS variables defined on :root in ox.css. Pages should reference the variable, not the hex.

Surface & structure

--bg
#fafbfc
Page background (body)
--surface
#ffffff
Cards, intros, callouts, table even-rows
--surface-subtle
#eef1f5
Inline code background, hovered table rows
--border
#dadce0
Default 1px borders, table dividers
--border-hover
#9aa0a6
Hovered borders, callout left-rules

Text

--text
#202124
Body copy default
--text-strong
#1a1a1a
Headings, strong, emphasis, titles
--text-muted
#5f6368
Subtitles, descriptions, muted labels
--text-faint
#80868b
Timestamps, separators, lowest-priority text

Brand

--accent
#1f3d2a
Deep forest — section banners, top-links border, CTA links in tables
--urgent
#c2185b
Urgent dot indicator (.dot-urgent only)

3 · Status palette

Functional bg/fg pairs used by .status pills and several ad-hoc page styles. Always paired — never reuse a status bg without its matching fg.

VariableHexUsed by
--status-done-bg #1a1a1a .status.done background — black
--status-done-fg #ffffff .status.done text
--status-active-bg #e8f5e9 .status.active background — green pale
--status-active-fg #2e7d32 .status.active text — also .callout.green left rule, table row-best .val
--status-waiting-bg #fff3e0 .status.waiting background — amber pale
--status-waiting-fg #c77700 .status.waiting text — also .note-box strong, .callout.caution left rule
--status-initial-bg #e3f2fd .status.initial background — blue pale
--status-initial-fg #1565c0 .status.initial text
--status-empty-bg #f1f3f4 .status.empty background — neutral gray
--status-empty-fg #80868b .status.empty text — same as --text-faint

Hardcoded hex (not tokenized)

A few hex values appear inline in ox.css rather than as variables — usually for once-off semantic accents. Reuse the variable form where one exists.

HexWhereWhy
#c62828 .callout.warning left border, table row-worst .val Red — hard warning
#fff8e1 .note-box background Amber-tint note paper
#f9a825 .note-box left border Amber accent for dated inline notes
#f5f9f5 tr.row-best td background (sperm-dna-scenarios, cycle-flexibility) Page-specific best-row tint
#fef5f6 tr.row-worst td background (sperm-dna-scenarios, cycle-flexibility) Page-specific worst-row tint

4 · Spacing scale

Use these tokens for any margin, padding, or gap. Do not introduce off-scale values.

VariableValueUsage
--space-xs 0.25rem 4px Tight inline gap (between list items in a callout, between section-label and h1)
--space-sm 0.5rem 8px Standard small gap (between adjacent buttons, table cell padding)
--space-md 1rem 16px Default container/card padding, between-paragraph spacing
--space-lg 1.5rem 24px Container side padding, callout side padding
--space-xl 2.5rem 40px Margin between major content blocks (after table-wrap, before h2)
--space-2xl 4rem 64px Header-to-content separation, between top-level sections
--space-3xl 5.5rem 88px Container bottom padding

5 · Radii

VariableValueUsage
--radius-sm0.375rem 6pxInline code, small buttons, top-link buttons, breadcrumb hover
--radius-md0.5rem 8pxCallouts, note-box, code blocks, section banner
--radius-lg0.75rem 12pxCards (.intro, .entry, .stat-card), maps, large surfaces

Pills (status chips, back-to-top) use hardcoded border-radius: 999px.

6 · Type scale

Every text size is tokenized. Andrea-specific rule: body 14px floor, captions 12px floor — never go smaller.

H1 — page title --text-heading clamp(2.25rem, 1.75rem + 1.75vw, 2.75rem)
Source Serif 4 700
European IVF Clinics
H2 — section — (1.5rem fixed) DM Sans 600
Open Questions
H3 — subhead --text-subhead 1.3125rem / 21px
DM Sans 600
What we know
Card title --text-title 1.375rem / 22px
DM Sans 600
Hungary Trip
Body / prose --text-body 1.1875rem / 19px
Source Serif 4 400
Andrea is a Canadian private patient in both CZ and HU. No state reimbursement either side.
UI --text-ui 1rem / 16px
DM Sans 400
Table cells, descriptions, links, callouts
Caption --text-caption 0.875rem / 14px
DM Sans 400
Timestamps, table headers, contact phone numbers — floor for any UI text
Section banner label --text-section 0.875rem / 14px
DM Sans 600 uppercase
Baby 2 · IVF

7 · Layout containers

ClassMax widthUse for
.container 44rem 704px Default reading-width pages — narrative pages, prose-heavy docs (this page would normally use it).
.container.container--wide 100rem 1600px Dashboard / table-heavy pages where horizontal real estate matters (clinic index, prescriptions, design system reference).

Padding is --space-lg --space-lg --space-3xl (top, sides, bottom). Mobile drops to --space-md sides via the @media (max-width: 640px) rule.

8 · Page scaffolding

Canonical subpage skeleton. Copy this when starting a new page under european-ivf/, doc-apostille-*, etc.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page Title · OX</title>
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,400;8..60,500;8..60,600;8..60,700&family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../ox.css">
<style>
  /* page-specific styles ONLY — never redefine tokens */
</style>
</head>
<body>
<div class="container container--wide" id="top">

<nav class="nav">
  <a href="../index.html">← OX</a>
  <span class="sep">/</span>
  <a href="index.html">Parent</a>
  <span class="sep">/</span>
  <span class="current">This Page</span>
</nav>

<header>
  <div class="section-label">Baby 2 · IVF</div>
  <h1>Page Title</h1>
  <p class="subtitle">Brief one-line description.</p>
  <div class="last-updated">Last updated: Apr 27, 2026 9:41 PM PT</div>
</header>

<!-- content -->

<a href="#top" class="back-to-top">↑ Back to top</a>

</div>

<script src="../ox.js"></script>
</body>
</html>

The index page (index.html at the repo root) omits the breadcrumb and uses .container not .container--wide.

ClassElementPurpose
.navnav wrapperFlex row, DM Sans, muted color, 16px text
.nav alinkUnderlined, padded for hover hit target
.nav .sepspanSlash separator — uses --text-faint
.nav .currentspanFinal crumb (no link), --text-strong weight 500
Live

A header bundles four elements in this order:

ClassElementPurpose
.section-labeldivUppercase rubric above h1 — domain pill (e.g. "Baby 2 · IVF")
h1h1Page title — Source Serif 4 700, clamped responsive size
.subtitlep or divSingle line, DM Sans, muted
.last-updateddivCaption, faint — always include real date+time, never fabricate
Live

Sample Page Title

A short single-line description of what this page covers.

Last updated: Apr 27, 2026 9:41 PM PT

A row of pill buttons placed inside <header> as a sub-nav (used on hub pages like the European IVF index). Olive-bordered; fills with olive on hover.

Live
<nav class="top-links">
  <a href="ranking.html">Clinic Ranking</a>
  <a href="pricing.html">Pricing Comparison</a>
</nav>

12 · Intro box (.intro)

A short framing paragraph above the main content — soft white card, full border (not just left rule), DM Sans. Different from .callout, which has a colored left rule.

What this page covers: An intro card sits between the header and the first content block, framing the page in one or two sentences. Use sparingly — most pages don't need both an intro and a callout.
<div class="intro">
  <strong>What this page covers:</strong></div>

13 · Prose (.prose)

Wrap long-form reading content in <section class="prose"> to get serif body text, scoped heading sizes, and link styling. Headings (h2/h3/h4) inside .prose render in DM Sans for visual hierarchy contrast against the serif body.

Inside .proseBehavior
p19px Source Serif 4, line-height 1.65, --space-md bottom margin
h21.5rem DM Sans 600, large top margin
h31.3125rem DM Sans 600
h41rem DM Sans 600
ul / ol1.5rem indent, disc / decimal markers
liline-height 1.6, xs bottom margin
strong--text-strong, weight 600
emitalic
aUnderlined, faint underline color, darkens on hover
codeSF Mono, surface-subtle bg, sm radius

14 · Collapsible section (details.section)

Olive banner with chevron — used on the index page to group cards into expandable categories. Active categories should be open by default; archive categories closed.

Open by default

Body content goes inside <div class="section-body"> for consistent top/bottom padding.

Closed by default

Click the banner to expand. Chevron rotates 90° via CSS.

<details class="section" open>
  <summary>Section title</summary>
  <div class="section-body"></div>
</details>

15 · Card grid (.card-group, a.entry)

Vertical list of clickable card entries. Two variants: simple a.entry (whole card is one link) or div.entry--has-sublinks (a main link plus a sublink list).

Simple entry
A single-link card — the whole tile is the click target. Optional .dot-urgent red dot signals attention.
Entry with sublinks
Main link gets the title and description. Sublink list below sits inside the same card with a divider.
ClassElementNotes
.card-groupwrapper divFlex column, --space-sm gap between cards
a.entryanchorWhole-card link variant
div.entry--has-sublinksdivWrapper for entry-main + entry-sublinks
.entry-mainaInner main link inside .entry--has-sublinks
.entry-headerdivOptional flex row for title + date
.entry-titlediv22px DM Sans 600, --text-strong
.dot-urgentspan0.5rem red dot — uses --urgent #c2185b
.entry-datespan14px caption, faint, no-wrap
.entry-descdiv16px DM Sans, muted
.entry-sublinksulDivided from main by 1px top border, padded

16 · Callouts (.callout)

Inline emphasis box — white surface, 1px full border, 3px colored left rule. Four variants. Default = neutral. Use sparingly; one or two per page max.

Neutral — default. Left rule uses --border-hover. Context, supporting info, framing.
.green — positive / confirmed — left rule #2e7d32. Use for genuine confirmation signals (e.g. "Flights booked").
.caution — soft warning — left rule #c77700. "Think twice" — note something to verify, not a hard stop.
.warning — hard warning — left rule #c62828. Genuine warnings only. Pair with ⚠️ when extra salience matters.
<div class="callout"></div>            <!-- neutral -->
<div class="callout green"></div>
<div class="callout caution"></div>
<div class="callout warning"></div>

17 · Note box (.note-box)

Amber-tinted dated note — visually distinct from callouts. Use for time-stamped updates ("records received Apr 22"). Default placement: just under the page header, before main content.

Apr 22, 2026
📋 Records received: Body copy explains the update. strong renders in amber #c77700.
ClassElementNotes
.note-boxdivBG #fff8e1, left border 4px #f9a825, no top/right border
.note-box .note-datespan14px caption, --text-faint
.note-box strongstrongAmber #c77700, weight 600 — for the lead phrase

18 · Info table (.info-table)

Compact key/value or label/value reference table. Two columns is the canonical layout — first column auto-bolded as label.

FieldValue
DoctorDr. Eva Stásná
Cycle cost€3,300 + medication
Total estimate€5,100 per cycle
Storage10 years (Czech legal limit)
ClassElementNotes
.info-tabletable16px DM Sans, full width
thead thth14px uppercase muted, 1px bottom border
tbody td:first-childtdAuto-bold label, muted color, no-wrap
tr.highlight tdtrBold value, --text-strong color

19 · Data table (.data-table)

Wide multi-column dashboard table. Always wrap in .table-wrap for horizontal scroll on mobile. Header sticky to top of viewport on scroll. If the layout is a fixed-column grid rather than a table (e.g. a calendar), stack it to one column instead of scrolling — see the column-crush rule.

ClinicCityStatusNotes
Section row — all caps muted divider
IVF CubePragueDoneDefault row
ReproGenesisBrnoActiveEven row gets surface bg
RepromedaBrnoWaitingHover any row to see surface-subtle bg
Old clinicEmptytr.inactive dims all cells to faint
ClassElementNotes
.table-wrapdivRequired wrapper — provides horizontal scroll
.data-tabletable16px DM Sans, full width, collapsed borders
thead ththSticky top, 14px uppercase muted, 2px bottom border
tbody tr:nth-child(even)Surface bg (white on default page bg)
tbody tr:hoverSurface-subtle bg
tr.section-rowtrSection divider — caption-size, all caps, gray bg
tr.inactivetrDimmed cells via --text-faint

20 · Status pills (.status)

Live
Done  Active  Waiting  Initial  Empty
ClassBg / FgMeaning
.status.done#1a1a1a / #ffffffCompleted, finalized
.status.active#e8f5e9 / #2e7d32In progress, currently being worked
.status.waiting#fff3e0 / #c77700Action pending on someone (often us)
.status.initial#e3f2fd / #1565c0Just contacted, no response yet
.status.empty#f1f3f4 / #80868bPlaceholder, deprioritized

21 · Stat cards (.stat-row, .stat-card)

3-column responsive stat row — collapses to 1 column under 640px. Title (caption uppercase), value (Source Serif 700), optional sub (caption muted).

Cycles planned
3
May–Sep 2026
Embryo target
~4
before PGT-A batch
Cost estimate
€16k
3-cycle banking

A single floating pill anchored to #top, fixed in the bottom-right. Hidden by default; the shared ox.js scroll handler adds the .visible class once window.scrollY > 320, fading and lifting it into view. The actual button on this page is live — scroll down and it appears.

Static preview (.back-to-top.visible)
↑ Back to top

Preview only — positioning overridden so it sits inline. In real use the pill is position: fixed bottom-right.

ClassElementNotes
.back-to-topaFloating pill, 16px DM Sans, white surface, full-round border, soft shadow. position: fixed at bottom: var(--space-lg); right: var(--space-lg). Place once, just before the closing </div> of .container.
.back-to-top.visibleaToggled by ox.js when window.scrollY > 320. Without it the pill is opacity: 0 and ignores pointer events. Pages using .back-to-top must include <script src="ox.js"></script>.

23 · Responsive rules

640px is the single global breakpoint in ox.css. Fixed multi-column layouts need a second, layout-specific breakpoint on top of it — see the column-crush rule below.

SelectorBehavior under 640px
.containerSide padding shrinks to --space-md; bottom to --space-2xl
.stat-rowCollapses to single column
.entry-headerTighter gap (--space-xs)
.info-table td:first-childAllows wrapping (no longer no-wrap)

Wide multi-column layouts — the column-crush rule

A fixed N-column grid or table (calendars, dashboards, comparison matrices) does not reflow gracefully at the 640px global breakpoint. Each column needs ≈110px to stay readable, so the layout has to change below N × 110px — usually well above 640px. Leaving it in desktop mode between phone width and that threshold crushes the columns: a 7-column calendar at 430px gave ~49px columns and wrapped every cell into mush.

Derive the breakpoint from the column count, then pick one strategy:

LayoutBelow the thresholdWhy
Wide <table>Wrap in .table-wrap — horizontal scrollRows stay aligned; user scrolls sideways
Grid / calendarRestructure to a single column — stacked cardsSideways-scrolling a calendar is poor UX

Existing instances: european-ivf/repromeda.html (7-col calendar → stacks at 900px), european-ivf/index.html (table/map → 800px). These px values live inline in each page's <style> because media queries can't read token var()s — keep them consistent with this rule.

24 · Emoji usage

Use emojis judiciously, for meaning — never decoration. They earn their place when they speed scanning, signal status, or mark a category at a glance.

Use for:

Avoid:

25 · Authoring rules

Token discipline

Component reuse

Sync workflow

Accessibility & readability

↑ Back to top