Accessibility

Project baseline — merge blocker for Blocker/High gaps

Accessibility is a hard requirement and a merge blocker for this bootstrap. Prefer simplicity over visual novelty. Custom CSS lives in styles.scss on top of Quarto’s default Bootstrap theme (chosen because it keeps native focus styles; we do not use a theme that zeroes outlines). Post-render fixes live in scripts/post_render.py. CI runs scripts/check_a11y.py and must pass.

This page is the project baseline. The thirteen rules below are the Accessibility Specialist peer-review requirements.

Baseline

1. Skip link to main content (visible on focus)

Every page starts with Skip to main content, injected as the first child of <body>. It is clipped until focused, then shown as a fixed control above other chrome. The target is <main id="quarto-document-content" tabindex="-1"> so keyboard focus actually moves.

Blocker if missing, not first, or not visible on focus.

2. One h1 per page; heading outline only

The Quarto page title is the only h1 and the first heading in the document. Body headings start at ##. Do not fake headings with bold, font-size, or colour. Callout titles are labels, not outline headings. The right-hand TOC label (“On this page”) is a styled paragraph, not an h2, so it cannot steal the outline ahead of the page title.

Blocker if a page has no h1, more than one h1, a heading before the page h1, or a skipped heading level.

3. Landmark structure

Required landmarks: header, nav, main, footer. There is one banner: the site chrome (#quarto-header, named “Site”). The page title block is a div, not a second <header>. Multiple nav elements are named:

Data table
Landmark Accessible name
Site banner Site
Primary navbar Primary
Secondary/sidebar toggle bar Section menu
Sidebar In this section
Table of contents On this page
Breadcrumb breadcrumb (Quarto)
Prev/next Previous and next pages

The navbar collapse control is a native button with aria-expanded / aria-controls. It must not have role="menu" (that role is for menu widgets, not a disclosure).

The current page is indicated by aria-current="page" plus underline/weight plus visually hidden “(current page)” — not colour alone. Home uses aria-current on the site title. 404 is exempt.

Blocker if a landmark is missing, a nav is unnamed, or the current page is colour-only.

4. Visible :focus-visible rings

Do not remove outlines. styles.scss sets a 3px ember (#C8653A) outline plus a white/ember double box-shadow. Ember on cream is about 3.5:1 and ember on forge purple is about 4.5:1, so the ring stays ≥3:1 on both the light page and the dark navbar. Ember is not used for body text.

Blocker if a control we added has outline: none without a visible replacement.

5. prefers-reduced-motion

scroll-behavior is auto. Quarto’s sticky/headroom header is unstuck and de-animated in CSS — hide-on-scroll is not essential. Any future timeline or animation must honour prefers-reduced-motion. There is no JavaScript timeline.

High if essential information is conveyed only by motion.

6. Reflow at 200% zoom / ~320px width

The header is static, not sticky, so chrome cannot cover wrapped text. Body text uses overflow-wrap. Wide tables may scroll inside a .table-scroll region (role="region", tabindex="0", named as a horizontally scrollable table); the page’s prose must not require horizontal scrolling. The Timeline page also has a short note that the grid scrolls sideways on small screens. Navbar items collapse to a hamburger below large breakpoints. Toggle controls are at least 44×44 px.

Blocker if text requires horizontal scrolling or sticky chrome eats content at ~320px or 200% zoom.

8. Contrast and status

Data table
Surface Tokens Target
Body text #3E3A3A on #F8F3EA ≥4.5:1 (about 10.2:1)
Links and headings #1F1626 on #F8F3EA ≥4.5:1 (about 15.8:1)
Navbar #F8F3EA on #1F1626 ≥4.5:1 (about 15.8:1)
UI / focus ember #C8653A ring + white halo ≥3:1

Source and timeline statuses are visible text inside a bordered .status-label. Do not encode status by colour alone.

Blocker if status is colour-only or body text fails 4.5:1.

9. Tables

Timeline and catalogue views are real <table> elements with <caption>, <thead>, and scope="col" headers. Do not invent role="grid". A CSS-only card layout is not a substitute for these data tables.

Blocker if a data table lacks a caption/headers or uses grid roles.

10. Forms and contribution

This site has no on-page contribution form. People contribute through GitHub issues and pull requests (see About / Contribute). Site search is disabled — the Quarto autocomplete widget is an unlabeled custom control we chose not to keep.

Any future control must have a programmatic label, text errors, and ~44px targets for buttons.

Blocker if an unlabeled control or hover-only widget is added.

11. Images and diagrams

Meaningful images need descriptive alt. Decorative images use alt="". Do not put critical history only in an image. The navbar carries a decorative jordanforge corner mark (alt=""). The maker relationship is the footer link “A jordanforge.ca project”, not the image.

Blocker if an <img> is missing an alt attribute.

12. Quarto theme and in-repo CSS

  • Theme: Quarto default (Bootstrap) + styles.scss.
  • styles.scss is grouped by baseline rule number. Treat it as the a11y override file.
  • scripts/post_render.py patches landmark names, skip target, current page, table captions, external-link text, TOC title demotion, navbar role="menu" removal, single banner landmark, and contained table overflow after each render.

High if a theme or custom widget removes focus styles or adds inaccessible chrome.

13. Comfortable default body text

Do not ship tiny theme defaults. Root/body text is 18px / 1rem with line-height 1.6 ($font-size-root: 18px in styles.scss). Never drop body text below ~16px. There is no in-page text-size widget — simplicity over novelty. Browser zoom to 200% must remain usable (see rule 6).

High if default body text is under ~16px or 200% zoom breaks reading.

Automated check

quarto render
python3 scripts/check_a11y.py _site

CI runs the same command in .github/workflows/pages.yml. A failing check is a merge blocker.

The script is a first-pass HTML gate. It does not compute contrast ratios or drive a browser. Manual keyboard, zoom, and screen-reader passes remain follow-up G, but they do not delay baking this baseline into the bootstrap.

Authoring rules

  • Start body headings at ##.
  • Use descriptive link text.
  • Put status in text (bibliographically-confirmed), not colour alone.
  • Give data tables a caption and column headers.
  • Do not add autoplay media, custom widgets, or hover-only menus.
  • Do not re-enable site search or a sticky header without an a11y review that still meets this baseline.