diff --git a/.github/workflows/a11y.yml b/.github/workflows/a11y.yml
new file mode 100644
index 00000000..ea8cd816
--- /dev/null
+++ b/.github/workflows/a11y.yml
@@ -0,0 +1,80 @@
+name: Accessibility Tests
+
+on:
+ workflow_dispatch:
+ inputs:
+ url:
+ description: 'URL to test (e.g., https://uss-staging.netlify.app). Leave empty to build and test locally.'
+ required: false
+ type: string
+
+jobs:
+ a11y:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Install Playwright chromium
+ run: npx playwright install --with-deps chromium
+
+ # When testing a deployed URL, just fetch config for test scaffolding
+ - name: Fetch config
+ if: ${{ inputs.url != '' }}
+ env:
+ CONFIG_URL: ${{ secrets.CONFIG_URL }}
+ run: curl -fsSL "$CONFIG_URL" -o public/config.json
+
+ # When no URL provided, do a full static build
+ - name: Build static site
+ if: ${{ inputs.url == '' }}
+ env:
+ STATIC_BUILD: 'true'
+ CONFIG_URL: ${{ secrets.CONFIG_URL }}
+ GITHUB_OWNER: ${{ secrets.CONTENT_GITHUB_OWNER }}
+ GITHUB_REPO: ${{ secrets.CONTENT_GITHUB_REPO }}
+ GITHUB_BRANCH: ${{ secrets.CONTENT_GITHUB_BRANCH }}
+ GITHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.CONTENT_GITHUB_PERSONAL_ACCESS_TOKEN }}
+ MONGODB_URI: ${{ secrets.MONGODB_URI }}
+ MONGODB_NAME: ${{ secrets.MONGODB_NAME }}
+ MONGODB_COLLECTION_NAME: ${{ secrets.MONGODB_COLLECTION_NAME }}
+ NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
+ run: npm run build
+
+ - name: Start preview server
+ if: ${{ inputs.url == '' }}
+ run: npx astro preview &
+
+ - name: Wait for server
+ if: ${{ inputs.url == '' }}
+ run: |
+ for i in $(seq 1 30); do
+ curl -sf http://localhost:4321 > /dev/null 2>&1 && exit 0
+ sleep 1
+ done
+ echo "Server failed to start" && exit 1
+
+ - name: Run accessibility tests
+ env:
+ A11Y_HOST: ${{ inputs.url || 'http://localhost:4321' }}
+ STATIC_BUILD: ${{ inputs.url != '' && 'false' || 'true' }}
+ run: npx playwright test --project=chromium
+
+ - name: Upload report
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: a11y-report
+ path: |
+ playwright-report/
+ test-results/
+ retention-days: 30
diff --git a/docs/a11y-requirements.md b/docs/a11y-requirements.md
new file mode 100644
index 00000000..3da2d9ef
--- /dev/null
+++ b/docs/a11y-requirements.md
@@ -0,0 +1,92 @@
+# Accessibility Requirements
+
+Requirements for WCAG 2.2 AA compliance in Core Data Places deployments. Originates from the USS project's UVA Non-Visual Access procurement agreement and applies to all CDP sites.
+
+## Web Content (Automated — Playwright + axe-core)
+
+These are tested automatically by the existing test suite (`test/browser/a11y.test.ts`). See `a11y-test-results.md` for current status.
+
+- WCAG 2.0/2.1/2.2 Level A and AA criteria across all page types
+- Image alt text on all meaningful images; decorative images marked appropriately
+- Color contrast ratios meeting AA thresholds
+- Keyboard navigability for all interactive elements
+- Semantic HTML structure (headings, landmarks, lists, tables)
+- Form labels, error messages, and focus management
+- ARIA attributes where semantic HTML is insufficient
+
+## Web Content (Manual)
+
+These require human review and cannot be fully automated. See `accessibility-checklist.md` in the USS repo for the full manual checklist.
+
+- Screen reader testing (VoiceOver, NVDA, JAWS, TalkBack)
+- Logical reading order and content flow
+- Meaningful link text and navigation patterns
+- Content comprehensibility and reading level
+- Assistive technology compatibility across browsers
+- Browser zoom behavior (up to 200%)
+
+## PDF Accessibility
+
+PDFs are uploaded by clients into FairData, stored on FairImage, and served as IIIF images. PDF accessibility is the client's responsibility, but we should test uploaded PDFs and report issues so clients can remediate.
+
+**Not testable via Playwright** — PDFs served as IIIF image tiles lose their internal structure. Testing requires tools that inspect the PDF file directly.
+
+### Automated checks (Playwright + pdf-lib or veraPDF)
+
+Pages with PDF download links can be tested in the existing Playwright suite using a hybrid approach:
+
+1. Playwright navigates to a page with a PDF download link
+2. Triggers the download via Playwright's download handling API
+3. A Node.js PDF library (`pdf-lib`, or shelling out to `veraPDF`) inspects the downloaded file
+4. Test asserts on accessibility properties and reports failures
+
+This keeps PDF checks in the same test suite and CI workflow as web content checks. Playwright handles navigation and download; the PDF library handles inspection.
+
+**Properties we can check automatically:**
+
+- **Tagged PDF flag** — confirm the PDF is tagged (structural requirement for screen readers)
+- **Document metadata** — title, language, author set in document properties
+- **Content copying enabled** — not locked against assistive technology extraction
+
+### Manual checks (client responsibility, we document and report)
+
+These require PAC (PDF Accessibility Checker) or Adobe Acrobat and human judgment:
+
+#### Structure and reading order
+- Correct tags for headings, paragraphs, lists, and tables
+- Tag tree order matches logical reading order
+- Table of Contents tagged appropriately
+- Header/footer content tagged where meaningful
+
+#### Images and graphics
+- Meaningful images have concise, descriptive alt text
+- Decorative images tagged as artifacts
+- Text-in-images avoided; scanned content has OCR applied
+- Color contrast sufficient; information not conveyed by color alone
+
+#### Lists and tables
+- Lists use proper tag hierarchy (L, LI, Lbl, LBody)
+- Tables use TH/TD correctly with scope attributes
+- Tables have a description/summary
+- No empty cells used for layout
+
+#### Links and footnotes
+- Link text is descriptive (not "click here")
+- Links are keyboard accessible and visually distinct
+- New-window links warn users
+- Citations and footnotes tagged correctly
+
+#### Forms (if applicable)
+- All fields labeled clearly with tooltips and required indicators
+- Logical tab order
+- Accessible error messages
+
+#### Navigation
+- Bookmarks present for documents over 10 pages
+
+### Testing tools
+
+- **PAC (PDF Accessibility Checker)** — free, Windows-only, comprehensive automated checks
+- **Adobe Acrobat Pro** — built-in accessibility checker and remediation tools
+- **veraPDF** — open source, CLI, good for batch automated checks
+- **Screen reader validation** — manual testing with NVDA or VoiceOver reading the PDF directly
diff --git a/docs/a11y-test-results.md b/docs/a11y-test-results.md
new file mode 100644
index 00000000..07b1ba10
--- /dev/null
+++ b/docs/a11y-test-results.md
@@ -0,0 +1,57 @@
+# Accessibility Test Results
+
+**Date:** 2026-02-19
+**Engine:** axe-core 4.11 via `@axe-core/playwright`
+**Ruleset:** WCAG 2.0/2.1/2.2 Level A and AA only
+**Browser:** Chromium
+**Target:** USS content deployment (localhost:4321)
+
+## Summary
+
+| Status | Count |
+|--------|-------|
+| Passed | 6 |
+| Failed | 1 |
+| Skipped | 7 |
+
+**1 unique WCAG violation** found. The test suite filters to WCAG 2.2 AA criteria and uses soft assertions to report all violations per page without stopping early.
+
+## WCAG Violations
+
+### Critical: `image-alt` — Images must have alternative text
+
+- **WCAG:** 1.1.1 Non-text Content (Level A)
+- **Issue:** [#553](https://github.com/performant-software/core-data-places/issues/553)
+- **Pages:** `/en/pages/Institutions`, `/en/pages/About`, `/en/` (Home, intermittent due to `server:defer` timing)
+- **Element:** `` in Banner component (`src/apps/pages/Banner.astro` line 52)
+- **Root cause:** `Banner.astro` uses `alt={imageAlt}`, but when TinaCMS content omits the `imageAlt` field, the `alt` attribute is undefined (missing entirely).
+- **Fix:** Default to `alt={imageAlt || ''}` for decorative images, or require alt text in the CMS schema.
+
+## Best Practice Issues (Not WCAG Criteria)
+
+These were found during initial testing with all axe-core rules enabled. They are not WCAG violations but are worth addressing:
+
+- **`page-has-heading-one`** — 6 pages lack a visible `