From b42c332c1b486de47867f3e76f7ed405650a6605 Mon Sep 17 00:00:00 2001 From: rivassec Date: Fri, 23 Oct 2026 22:58:08 -0700 Subject: [PATCH 1/2] a11y: test the PR's own CSS, scan every page, underline prose links The 30 pa11y contrast "errors" on post headers were axe needs-review results, not measured failures: the hero's ::after glow overlay hides the background from axe, and pa11y 6 reported every incomplete result as an error. Worst-case measured contrast is 5.5:1 (links), above AA. - pa11y-ci 3.1.0 -> 5.0.0 (pa11y 10, axe 4.13; needs Node 22) with levelCapWhenNeedsReview: warning. - Build with SITEURL=http://127.0.0.1:8000: publishconf's absolute URLs made pa11y load the live stylesheet, so CSS changes in a PR were never tested. - Scan every page from the sitemap instead of 6 hand-picked URLs. - Underline links in running text: the newer axe found 234 links marked by color alone (#6cb4ff vs body text, 1.78:1; WCAG 1.4.1). Locally (pa11y-ci 5.0.0, Chrome): 35/36 pages pass. The remaining error is a nested-interactive figure in prompt-injection-supply-chain-evasion (a scrollable
 inside role="img"), left for a content decision. The job
stays non-blocking; making it blocking is a separate PR.

Co-Authored-By: Claude Opus 5.5 
---
 .github/workflows/a11y-check.yml | 30 ++++++++++++++++++------------
 .pa11yci.json                    | 23 +++++++++++------------
 content/static/custom.css        | 16 ++++++++++++++++
 3 files changed, 45 insertions(+), 24 deletions(-)

diff --git a/.github/workflows/a11y-check.yml b/.github/workflows/a11y-check.yml
index 8eb3e9bc..15df7c13 100644
--- a/.github/workflows/a11y-check.yml
+++ b/.github/workflows/a11y-check.yml
@@ -33,15 +33,20 @@ jobs:
         run: pip install --require-hashes --no-deps -r requirements.txt
 
       - name: Build site
-        run: pelican content -o output -s publishconf.py
+        # SITEURL points at the local server: publishconf's absolute
+        # https://rivassec.com URLs made pa11y load the LIVE stylesheet, so a
+        # PR's CSS change was never what got tested (found 2026-10-23).
+        run: pelican content -o output -s publishconf.py -e 'SITEURL="http://127.0.0.1:8000"'
 
       - name: Set up Node
         uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
         with:
-          node-version: "20"
+          node-version: "22"
 
       - name: Install pa11y-ci
-        run: npm install --global pa11y-ci@3.1.0
+        # pa11y-ci 5 (pa11y 10, axe 4.13) for levelCapWhenNeedsReview: pa11y 6
+        # reported axe "incomplete" (needs review) results as errors.
+        run: npm install --global pa11y-ci@5.0.0
 
       - name: Serve built site
         run: |
@@ -57,13 +62,14 @@ jobs:
           done
 
       - name: Run pa11y-ci
-        # Non-blocking for now (2026-06-12). Pa11y is reporting contrast
-        # ratios that disagree with computed styles I can reproduce
-        # locally. Until I can attach a real Chrome devtools session to
-        # the live page and verify the actual computed colors on .c1
-        # spans and the hero h1, this workflow logs findings without
-        # blocking pushes. Flip to blocking once the residual findings
-        # are reproduced and either fixed or proven to be axe gradient
-        # false-positives.
+        # Still non-blocking. The 2026-06-12 contrast "errors" were axe
+        # needs-review results (the hero gradient overlay hides the
+        # background; measured worst case is 5.5:1, above AA), which pa11y 6
+        # reported as errors. levelCapWhenNeedsReview now keeps them as
+        # warnings. Every page in the sitemap is scanned. Flip to blocking
+        # in its own PR once the run is clean.
         continue-on-error: true
-        run: pa11y-ci --config .pa11yci.json
+        run: >-
+          pa11y-ci --config .pa11yci.json
+          --sitemap http://127.0.0.1:8000/sitemap.xml
+          --sitemap-exclude '\.(txt|md|xml)$'
diff --git a/.pa11yci.json b/.pa11yci.json
index fb852f78..a1bd578b 100644
--- a/.pa11yci.json
+++ b/.pa11yci.json
@@ -3,18 +3,17 @@
     "standard": "WCAG2AA",
     "timeout": 30000,
     "wait": 500,
-    "runners": ["axe", "htmlcs"],
+    "runners": [
+      "axe",
+      "htmlcs"
+    ],
     "chromeLaunchConfig": {
-      "args": ["--no-sandbox", "--disable-dev-shm-usage"]
-    }
+      "args": [
+        "--no-sandbox",
+        "--disable-dev-shm-usage"
+      ]
+    },
+    "levelCapWhenNeedsReview": "warning"
   },
-
-  "urls": [
-    "http://127.0.0.1:8000/",
-    "http://127.0.0.1:8000/prompt-injection-supply-chain-evasion.html",
-    "http://127.0.0.1:8000/iam-safe-defaults-fail-loud.html",
-    "http://127.0.0.1:8000/leap-second-chaos-2012.html",
-    "http://127.0.0.1:8000/teensy-efi-bruteforce-hours-late.html",
-    "http://127.0.0.1:8000/venezuela-twitter-proxy-osint.html"
-  ]
+  "urls": []
 }
diff --git a/content/static/custom.css b/content/static/custom.css
index 3894c571..451d2478 100644
--- a/content/static/custom.css
+++ b/content/static/custom.css
@@ -943,3 +943,19 @@ main footer a {
 .series-nav li { margin: 2px 0; line-height: 1.5; }
 .series-nav a { color: var(--accent); }
 .series-nav [aria-current="page"] { font-weight: 600; color: var(--fg); }
+
+/* Links inside running text are underlined (WCAG 1.4.1, 2026-10-23): the
+   accent blue is only 1.78:1 against body text, so color alone does not
+   mark them. Navigation, cards and pills keep their own styles. */
+main p a,
+main li a,
+main td a,
+footer p a {
+  text-decoration: underline;
+  text-underline-offset: 0.15em;
+  text-decoration-thickness: 1px;
+}
+main p a:hover,
+main li a:hover,
+main td a:hover,
+footer p a:hover { text-decoration-thickness: 2px; }

From d7a5d86fc1be2c078386fa5ffa54a1322b95e193 Mon Sep 17 00:00:00 2001
From: rivassec 
Date: Fri, 23 Oct 2026 23:23:38 -0700
Subject: [PATCH 2/2] copy-code: no copy button on diagrams inside role="img"

The button injected into the ASCII trust-boundary diagram in
prompt-injection-supply-chain-evasion sat inside 
: a nested interactive control (axe nested-interactive), and a diagram is not code to copy. With this, pa11y passes 36/36 pages locally. Co-Authored-By: Claude Opus 5.5 --- content/static/copy-code.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/content/static/copy-code.js b/content/static/copy-code.js index 8764275b..5119874d 100644 --- a/content/static/copy-code.js +++ b/content/static/copy-code.js @@ -45,6 +45,9 @@ for (var i = 0; i < blocks.length; i++) { var pre = blocks[i]; if (pre.querySelector('.copy-btn')) continue; + // A
 inside role="img" is a drawn diagram, not code: a button
+      // there is a nested interactive control (axe nested-interactive).
+      if (pre.closest('[role="img"]')) continue;
       pre.classList.add('has-copy');
       var btn = document.createElement('button');
       btn.className = 'copy-btn';