diff --git a/server/tests/public-a11y.test.ts b/server/tests/public-a11y.test.ts new file mode 100644 index 00000000..0b7671ec --- /dev/null +++ b/server/tests/public-a11y.test.ts @@ -0,0 +1,42 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { createApp } from "../src/app.js"; +import { fixtureDatabase, root } from "./helpers.js"; + +const publicPages = ["/", "/privacy", "/support", "/terms", "/apple-music-soundtrack", "/private-driving-journal"]; + +test("public pages expose a skip link, 24px tap targets, and contrast overlay CSS", async () => { + const fixture = fixtureDatabase(); + const runtime = await createApp({ databasePath: fixture.filename, root, allowTestAuth: true, legacyUpstream: "", mode: "web" }); + try { + const css = await runtime.app.inject({ method: "GET", url: "/public-a11y.css?v=2" }); + assert.equal(css.statusCode, 200, css.body); + assert.match(String(css.headers["content-type"]), /text\/css/); + assert.equal(css.headers["cache-control"], "public, max-age=31536000, immutable"); + assert.match(css.body, /\.skip-link/); + assert.match(css.body, /min-height: 24px/); + assert.match(css.body, /min-width: 24px/); + assert.match(css.body, /flex-wrap: nowrap/); + assert.match(css.body, /#081832cc/); + assert.match(css.body, /#081832b3/); + assert.doesNotMatch(css.body, /#081832f8|#081832f4|#081832e8/); + assert.doesNotMatch(css.body, /[\u2013\u2014]/); + + for (const url of publicPages) { + const page = await runtime.app.inject({ method: "GET", url }); + assert.equal(page.statusCode, 200, url); + assert.match(page.body, /class="skip-link" href="#main">Skip to content<\/a>/, url); + assert.match(page.body, /id="main"/, url); + assert.match(page.body, /href="\/public-a11y\.css\?v=2"/, url); + assert.match(String(page.headers["content-security-policy"]), /default-src 'self'/, url); + } + + const missing = await runtime.app.inject({ method: "GET", url: "/missing-public-page" }); + assert.equal(missing.statusCode, 404); + assert.match(missing.body, /class="skip-link" href="#main">Skip to content<\/a>/); + assert.match(missing.body, /href="\/public-a11y\.css\?v=2"/); + } finally { + await runtime.app.close(); + fixture.cleanup(); + } +}); diff --git a/web/404.html b/web/404.html index 752562b6..95a23782 100644 --- a/web/404.html +++ b/web/404.html @@ -11,15 +11,17 @@ + Page Not Found — JourneyDeck + -
+

Route not found · 404

That road isn’t on the map.

diff --git a/web/apple-music-soundtrack.html b/web/apple-music-soundtrack.html index 40ddb014..0c90a366 100644 --- a/web/apple-music-soundtrack.html +++ b/web/apple-music-soundtrack.html @@ -25,6 +25,7 @@ + Record iPhone drives with optional Apple Music | JourneyDeck @@ -64,12 +65,13 @@ + -
+

Your drive, your music

diff --git a/web/beta.html b/web/beta.html index 5847b22d..2edbf15e 100644 --- a/web/beta.html +++ b/web/beta.html @@ -28,6 +28,7 @@ + diff --git a/web/privacy.html b/web/privacy.html index 644c9216..281a64ad 100644 --- a/web/privacy.html +++ b/web/privacy.html @@ -25,17 +25,19 @@ + JourneyDeck Privacy Policy + -
+

Your data, your journey

Privacy Policy

diff --git a/web/private-driving-journal.html b/web/private-driving-journal.html index ad87b868..dbbc6ea1 100644 --- a/web/private-driving-journal.html +++ b/web/private-driving-journal.html @@ -25,6 +25,7 @@ + Private driving journal for iPhone | JourneyDeck + -
+

YOUR ROADS, YOUR ARCHIVE

diff --git a/web/public-a11y.css b/web/public-a11y.css new file mode 100644 index 00000000..d8360803 --- /dev/null +++ b/web/public-a11y.css @@ -0,0 +1,36 @@ +.skip-link { position: fixed; z-index: 100; top: 8px; left: 8px; transform: translateY(-150%); min-height: 44px; padding: 12px 16px; border-radius: 7px; background: var(--ivory, #f6f0e2); color: var(--navy, #081832); font-size: 14px; font-weight: 650; text-decoration: none; } +.skip-link:focus { transform: none; } +.site-header, .header-actions { flex-wrap: nowrap; } +.site-header .brand { white-space: nowrap; } +.header-actions .button { white-space: nowrap; flex-shrink: 0; } +.desktop-nav a { display: inline-flex; align-items: center; min-height: 24px; padding: 6px 2px; } +.footer a, .footer-bottom a { display: inline-flex; align-items: center; min-height: 24px; padding: 4px 2px; } +.page-actions { gap: 16px; } +.contact, .secondary-action { min-height: 52px; } +.tour-dots button { min-width: 24px; width: 24px; } +.text-link, .hero-soundtrack-link, .feature-cta, .hero-baseline a { min-height: 24px; } +section.hero { + background: linear-gradient(90deg, #081832cc 0%, #081832cc 58%, #081832b3 78%, #08183280 100%), url("/assets/beta/grand-touring-home.webp") center 55% / cover; +} +section.hero:not(.has-photo)::before { content: none; } +section.hero.has-photo::before { + content: ""; + position: absolute; + z-index: 1; + inset: 0; + background: linear-gradient(90deg, #081832cc 0%, #081832cc 58%, #081832b3 78%, #08183280 100%); + pointer-events: none; +} +.hero > :not(.hero-photo) { position: relative; z-index: 2; } +@media (max-width: 620px) { + section.hero { + background: linear-gradient(90deg, #081832cc, #081832c6), url("/assets/beta/grand-touring-home.webp") center / cover; + } + section.hero.has-photo::before { background: linear-gradient(90deg, #081832cc, #081832c6); } +} +@media (max-width: 390px) { + .site-header { gap: 8px; padding-inline: 12px; flex-wrap: nowrap; } + .header-actions { gap: 6px; flex-wrap: nowrap; } + .header-actions .button-small { gap: 6px; padding-inline: 10px; white-space: nowrap; } + .brand { font-size: 17px; } +} diff --git a/web/support.html b/web/support.html index 87cd04d3..586fed92 100644 --- a/web/support.html +++ b/web/support.html @@ -25,6 +25,7 @@ + JourneyDeck Support + -
+

Here to help

JourneyDeck Support

diff --git a/web/terms.html b/web/terms.html index 646a89b9..c9cc6303 100644 --- a/web/terms.html +++ b/web/terms.html @@ -25,17 +25,19 @@ + JourneyDeck Terms of Use + -
+

The rules of the road

Terms of Use