Tokens, components, and page conventions for ox.antalfamily.ca. Canonical reference for everything in ox.css.
Tokens are the what. These principles are the why. When a token or component doesn't fit, fall back to the principle, not improvisation.
Text-heavy pages exist to be read. Optimize for a comfortable reading experience first; everything else is secondary.
.container--wide (100rem ≈ 1600px) at body size is ~140 chars — far too long for sustained reading. Use .container (44rem ≈ 65–75 chars) for prose-heavy pages..container--wide for pages that genuinely need horizontal real estate: dashboards, multi-column data tables, side-by-side comparisons. The European IVF index, ranking, and pricing pages qualify. Prescriptions, sperm-dna-scenarios, cycle-flexibility do not — they are mostly prose with embedded tables.max-width: 44rem), also center it (margin-left: auto; margin-right: auto) so it sits balanced on the page rather than flush-left in an awkwardly wide column. Tables, grids, and full-width demos stay where they are.text-align: center for prose. Centered body text breaks reading flow because the eye can't predict where each line starts. Center the block, keep the text left-aligned.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.
nav for navigation, header for the page header, button for actions, a for navigation. Don't fake it with divs..prose is sized smaller than the page H1 because it's a section title, not a page title.--text #202124 on --bg #fafbfc is AAA-contrast. Don't introduce off-token text colors without checking.aria-label. Any button or link whose visible content is just a glyph (✕, ↑, ▶) needs a text label for screen readers.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.
xs → 3xl). Type has 7 sizes. Radius has 3. Use the scale; do not introduce arbitrary px.var(--accent), not #1f3d2a. Hardcoded hex is reserved for the few documented exceptions (warning red, note-box amber).ox.css rather than forking it inline.--bg (page) → --surface (cards) → --surface-subtle (inline elements). Don't introduce new surface tones.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.
european-ivf/index.html) → leaf pages. Don't nest deeper without a strong reason..nav). Always anchor the leftmost crumb to the OX root so the reader can always escape upward..back-to-top link placed just before the container closes. It floats fixed in the bottom-right and only appears once the reader has scrolled past ~320px — quietly hidden until it's useful. One button per page, no inline duplicates..top-links row inside <header> is a quick-jump bar to frequently-used sibling pages, not a sitemap. Cap at 4–5 items. If more pages need surfacing, push them into a .card-group below the header where each gets a title and one-line description.european-ivf/index.html currently has 8 top-link buttons (Clinic Ranking, Consultation Prep, Pricing Comparison, Sperm DNA Scenarios, Cycle Flexibility, Prescriptions, Reputation & Quality, Google Drive). That is overloaded. Solutions: (a) move secondary links to a footer block, (b) group into 2–3 categories with a card-group, (c) demote less-used links to the body of the page.details.section to hide secondary content behind a collapsible olive banner. Active sections open by default; archive sections closed.<header>..callout or .intro at the top of body content for the bottom line — the prescriptions page is a good example..data-table thead. They steal vertical space and degrade reading on small screens.Two families, loaded from Google Fonts. Every page must include the link tag below in <head>.
| Family | Used for | Weights 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.
All colors are CSS variables defined on :root in ox.css. Pages should reference the variable, not the hex.
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.
| Variable | Hex | Used 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 |
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.
| Hex | Where | Why |
|---|---|---|
| #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 |
Use these tokens for any margin, padding, or gap. Do not introduce off-scale values.
| Variable | Value | Usage |
|---|---|---|
| --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 |
| Variable | Value | Usage |
|---|---|---|
| --radius-sm | 0.375rem 6px | Inline code, small buttons, top-link buttons, breadcrumb hover |
| --radius-md | 0.5rem 8px | Callouts, note-box, code blocks, section banner |
| --radius-lg | 0.75rem 12px | Cards (.intro, .entry, .stat-card), maps, large surfaces |
Pills (status chips, back-to-top) use hardcoded border-radius: 999px.
Every text size is tokenized. Andrea-specific rule: body 14px floor, captions 12px floor — never go smaller.
| Class | Max width | Use 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.
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.
| Class | Element | Purpose |
|---|---|---|
| .nav | nav wrapper | Flex row, DM Sans, muted color, 16px text |
| .nav a | link | Underlined, padded for hover hit target |
| .nav .sep | span | Slash separator — uses --text-faint |
| .nav .current | span | Final crumb (no link), --text-strong weight 500 |
A header bundles four elements in this order:
| Class | Element | Purpose |
|---|---|---|
| .section-label | div | Uppercase rubric above h1 — domain pill (e.g. "Baby 2 · IVF") |
| h1 | h1 | Page title — Source Serif 4 700, clamped responsive size |
| .subtitle | p or div | Single line, DM Sans, muted |
| .last-updated | div | Caption, faint — always include real date+time, never fabricate |
A short single-line description of what this page covers.
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.
<nav class="top-links"> <a href="ranking.html">Clinic Ranking</a> <a href="pricing.html">Pricing Comparison</a> </nav>
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.
<div class="intro"> <strong>What this page covers:</strong> … </div>
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 .prose | Behavior |
|---|---|
| p | 19px Source Serif 4, line-height 1.65, --space-md bottom margin |
| h2 | 1.5rem DM Sans 600, large top margin |
| h3 | 1.3125rem DM Sans 600 |
| h4 | 1rem DM Sans 600 |
| ul / ol | 1.5rem indent, disc / decimal markers |
| li | line-height 1.6, xs bottom margin |
| strong | --text-strong, weight 600 |
| em | italic |
| a | Underlined, faint underline color, darkens on hover |
| code | SF Mono, surface-subtle bg, sm radius |
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.
Body content goes inside <div class="section-body"> for consistent top/bottom padding.
Click the banner to expand. Chevron rotates 90° via CSS.
<details class="section" open> <summary>Section title</summary> <div class="section-body"> … </div> </details>
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).
| Class | Element | Notes |
|---|---|---|
| .card-group | wrapper div | Flex column, --space-sm gap between cards |
| a.entry | anchor | Whole-card link variant |
| div.entry--has-sublinks | div | Wrapper for entry-main + entry-sublinks |
| .entry-main | a | Inner main link inside .entry--has-sublinks |
| .entry-header | div | Optional flex row for title + date |
| .entry-title | div | 22px DM Sans 600, --text-strong |
| .dot-urgent | span | 0.5rem red dot — uses --urgent #c2185b |
| .entry-date | span | 14px caption, faint, no-wrap |
| .entry-desc | div | 16px DM Sans, muted |
| .entry-sublinks | ul | Divided from main by 1px top border, padded |
Inline emphasis box — white surface, 1px full border, 3px colored left rule. Four variants. Default = neutral. Use sparingly; one or two per page max.
--border-hover. Context, supporting info, framing.
<div class="callout">…</div> <!-- neutral --> <div class="callout green">…</div> <div class="callout caution">…</div> <div class="callout warning">…</div>
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.
strong renders in amber #c77700.
| Class | Element | Notes |
|---|---|---|
| .note-box | div | BG #fff8e1, left border 4px #f9a825, no top/right border |
| .note-box .note-date | span | 14px caption, --text-faint |
| .note-box strong | strong | Amber #c77700, weight 600 — for the lead phrase |
Compact key/value or label/value reference table. Two columns is the canonical layout — first column auto-bolded as label.
| Field | Value |
|---|---|
| Doctor | Dr. Eva Stásná |
| Cycle cost | €3,300 + medication |
| Total estimate | €5,100 per cycle |
| Storage | 10 years (Czech legal limit) |
| Class | Element | Notes |
|---|---|---|
| .info-table | table | 16px DM Sans, full width |
| thead th | th | 14px uppercase muted, 1px bottom border |
| tbody td:first-child | td | Auto-bold label, muted color, no-wrap |
| tr.highlight td | tr | Bold value, --text-strong color |
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.
| Clinic | City | Status | Notes |
|---|---|---|---|
| Section row — all caps muted divider | |||
| IVF Cube | Prague | Done | Default row |
| ReproGenesis | Brno | Active | Even row gets surface bg |
| Repromeda | Brno | Waiting | Hover any row to see surface-subtle bg |
| Old clinic | — | Empty | tr.inactive dims all cells to faint |
| Class | Element | Notes |
|---|---|---|
| .table-wrap | div | Required wrapper — provides horizontal scroll |
| .data-table | table | 16px DM Sans, full width, collapsed borders |
| thead th | th | Sticky top, 14px uppercase muted, 2px bottom border |
| tbody tr:nth-child(even) | — | Surface bg (white on default page bg) |
| tbody tr:hover | — | Surface-subtle bg |
| tr.section-row | tr | Section divider — caption-size, all caps, gray bg |
| tr.inactive | tr | Dimmed cells via --text-faint |
| Class | Bg / Fg | Meaning |
|---|---|---|
| .status.done | #1a1a1a / #ffffff | Completed, finalized |
| .status.active | #e8f5e9 / #2e7d32 | In progress, currently being worked |
| .status.waiting | #fff3e0 / #c77700 | Action pending on someone (often us) |
| .status.initial | #e3f2fd / #1565c0 | Just contacted, no response yet |
| .status.empty | #f1f3f4 / #80868b | Placeholder, deprioritized |
3-column responsive stat row — collapses to 1 column under 640px. Title (caption uppercase), value (Source Serif 700), optional sub (caption muted).
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.
Preview only — positioning overridden so it sits inline. In real use the pill is position: fixed bottom-right.
| Class | Element | Notes |
|---|---|---|
| .back-to-top | a | Floating 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.visible | a | Toggled 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>. |
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.
| Selector | Behavior under 640px |
|---|---|
| .container | Side padding shrinks to --space-md; bottom to --space-2xl |
| .stat-row | Collapses to single column |
| .entry-header | Tighter gap (--space-xs) |
| .info-table td:first-child | Allows wrapping (no longer no-wrap) |
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:
| Layout | Below the threshold | Why |
|---|---|---|
Wide <table> | Wrap in .table-wrap — horizontal scroll | Rows stay aligned; user scrolls sideways |
| Grid / calendar | Restructure to a single column — stacked cards | Sideways-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.
Use emojis judiciously, for meaning — never decoration. They earn their place when they speed scanning, signal status, or mark a category at a glance.
<style> block. Reference the variable.--space-md isn't right, ask why a different size is needed before reaching for arbitrary px..callout, .note-box, .data-table, .status, .stat-card) before inventing new ones.<style> block and use design system tokens for color, spacing, type.ox.css instead of duplicating.european-ivf/, fertility-summary.html, ivf-cycle1-grace-fertility.html, natural-conception.html). For those: edit in B2, then copy into ~/Desktop/stuff/ox/ and push.ox.css, ox.js, this design-system.html, favicon.svg) are canonical in ox. B2 keeps passive mirrors of ox.css / ox.js for local preview only — never edit them in B2.last-updated timestamp on every changed page. Always run date to get the real time — never fabricate.index.html card group; sub-pages go in their parent section index (e.g. european-ivf/index.html top-links + clinic table). Match how sibling pages are linked. No existing pattern or unclear placement → ask, don't invent.aria-label on icon-only buttons.