@analitikisler/ai-tag-debugger 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.20.0 (2026-10-25)
4
+
5
+ - Node 22 or newer is required. Node 20 reached its end of life on 30 April 2026, and the OpenAI SDK the tool installs already needed 22. `aitd doctor` and `npm run setup` say so on an older Node, and CI tests on Node 22 and 24.
6
+ - The HTML report and `plan.html` take the look of Tally, the Analitik İşler design system, so they read as one family with analitikisler.com: navy grounds (`#f4f6fa` and white in light, `#071529` and `#0e1e34` in dark), one blue for links, focus and selected states (`#1161bb` / `#69b1ff`), Google Sans, Google Sans Flex and Google Sans Code named first with the system fonts behind them (the report still ships no font files), 2 px chips and badges, 4 px buttons and inputs, 6 px cards, 1 px rules and no card shadows. Labels that were set in capitals are in sentence case.
7
+ - Severity colours follow the design system: broken, warning and ok are its red, amber and green, info is its blue (a fixed value, so a white label never recolours it), and risk keeps a plum of its own so it stays apart from broken where only a dot or a rule shows. Owner, platform, viewport and consent colours keep their hues, retuned for the navy grounds. Every text colour passes 4.5:1 and every mark 3:1 in both themes; `docs/design/visual-system.md` lists the values and ratios.
8
+ - White label: both themes now check the brand colour the same way. The link colour is darkened (light) or lightened (dark) in the brand's own hue until it reads at 4.5:1 on the card, the page, the hover ground, the well and the brand's own tint, and the colour of the tab and rail rules, focus rings and pressed filter edges until it reaches 3:1 on them. A pale yellow such as `#ffe066` no longer gives light links at 4.39:1 on their tint and a focus ring at 1.3:1 on white, and a dark navy brand no longer gives a focus ring that disappears into the dark card. A colour that already reads, such as `#0a7d5a`, is unchanged. The PDF page footer uses the report's font and muted colour.
9
+ - Without the Google Sans family, which most readers do not have, the score sits in the middle of its ring whatever face the system lends, and a score line that wraps keeps its points and chips beside its first line instead of halfway down. `docs/design/visual-system.md` lists what macOS, Windows, Linux and Android show in its place. On Windows 11 the stacks reach Segoe UI Variable, so labels keep their medium weight. The Summary's score lines stack below 1100px instead of 900px, so an event name no longer splits across lines with the rail open on a mid-width screen. A journey's name in the Summary's Journeys card no longer breaks in the middle of a word.
10
+
3
11
  ## 0.19.0 (2026-10-25)
4
12
 
5
13
  - New brand: the tool's name is written `ai tag debugger`, lowercase, wherever the reports, `plan.html`, the README, the MCP registry listing and the GitHub Actions examples show it. The reports and `plan.html` carry the new lockup with its red point, the ai mark on phones, the shared ai favicon, and the Analitik İşler master logo with its red point and three bars in the byline and footer. The logos take the page's text colour in light, dark and print, with only the point in red. The "broken" red is the brand's one red, `#C12D30` in light and `#FA6B6D` in dark. The `aitd` and `ai-tag-debugger` commands, the npm package and the config keys are unchanged.
package/README.md CHANGED
@@ -150,7 +150,7 @@ Adding a native provider means implementing two functions in [`src/providers.js`
150
150
 
151
151
  ## Quick start
152
152
 
153
- Requires Node 20+.
153
+ Requires Node 22+.
154
154
 
155
155
  ```bash
156
156
  npm install -g @analitikisler/ai-tag-debugger
package/README.tr.md CHANGED
@@ -150,7 +150,7 @@ Yerel bir sağlayıcı eklemek için [`src/providers.js`](src/providers.js) içi
150
150
 
151
151
  ## Hızlı başlangıç
152
152
 
153
- Node 20+ gerekir.
153
+ Node 22+ gerekir.
154
154
 
155
155
  ```bash
156
156
  npm install -g @analitikisler/ai-tag-debugger
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@analitikisler/ai-tag-debugger",
3
3
  "mcpName": "io.github.analitikisler/ai-tag-debugger",
4
- "version": "0.19.0",
4
+ "version": "0.20.0",
5
5
  "description": "Open-source AI agent that QA-tests your website's analytics, ad and consent tracking (GA4, GTM, Google Ads, Meta, TikTok). Bring your own model (Claude, OpenAI, Gemini) or use it from your chat app over MCP.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -23,7 +23,7 @@
23
23
  "demo": "node src/cli.js run --config examples/demo.config.json --scripted --no-ai"
24
24
  },
25
25
  "engines": {
26
- "node": ">=20"
26
+ "node": ">=22"
27
27
  },
28
28
  "keywords": [
29
29
  "analytics",
package/src/html/css.js CHANGED
@@ -5,34 +5,45 @@
5
5
  // mockup drew as a data URI in the filter bar is the sprite's ui-search, placed by the filter bar's markup.
6
6
  import { brandCss } from "../whitelabel.js";
7
7
 
8
- // ---------- tokens: colours (light, then dark under both selectors), spacing, radii, fonts ----------
9
- const TOKENS = `/* Direction D tokens. Light on :root; dark twice (OS preference unless the page forces light, and the forced dark theme),
10
- so <html data-theme> beats the OS both ways. Every -fg passes 4.5:1 on --card, --page and its own -bg in both modes;
11
- every base passes 3:1 on --card (VISUAL.md has the table). Builders reference these names only, never a hex.
12
- --brand-point is the logos' point. Broken is the same red (brand/ai/BRAND.md §4), so a page never shows two reds. */
8
+ // ---------- tokens: colours (light, then dark under both selectors), spacing, radii, fonts, motion ----------
9
+ const TOKENS = `/* Tally design system tokens, under the names the builders already use. Light on :root; dark twice
10
+ (OS preference unless the page forces light, and the forced dark theme), so <html data-theme> beats the OS both ways.
11
+ Which Tally token each name follows: --page bg, --card surface, --card-2 surface-2, --well well, --fg text,
12
+ --fg-strong text-strong, --muted muted, --border border, --border-strong border-strong, --accent and --accent-fg accent,
13
+ --accent-bg accent-soft, --selection selection, --shadow-overlay the overlay shadow (popovers and tooltips only).
14
+ Severity: broken bad, warning warn, ok ok, info accent (its value, not var(--accent), so a white label never moves it).
15
+ Risk keeps a hue of its own, a plum (allowed by Tally when risk and broken sit side by side): the report puts them next
16
+ to each other in the score lines, the filter dots, the rail and the card rules, where a dot or a 3 px rule has no room
17
+ for the glyph. Risk still carries its diamond glyph and its word everywhere it has room.
18
+ Each status is one colour for text and marks (-fg = solid); its -bg tint is the colour mixed into --card, 10% light
19
+ and 16% dark. Categories (owners, platforms, viewports, consent) keep their hues, retuned for the navy grounds; GA4 is
20
+ Tally's orange (mark, mark-ink). Every -fg passes 4.5:1 on --page, --card, --card-2, --well and its own -bg, every
21
+ solid 3:1 on --page, --card and --well, in both modes (docs/design/visual-system.md has the table). Builders reference
22
+ these names only, never a hex. --brand-point is the logos' point: the brand's one red, the same as broken
23
+ (brand/ai/BRAND.md §4), so a page never shows two reds. */
13
24
  :root{
14
25
  color-scheme:light dark;
15
- --page:#F6F7F9;--card:#FFFFFF;--well:#EEF1F6;--fg:#111827;--muted:#4B5567;--border:#D9DEE8;--border-strong:#C3CAD8;
16
- --accent:#2E6CDB;--accent-fg:#2459C0;--accent-bg:#EBF1FE;
17
- --shadow-sm:0 1px 2px rgb(16 24 40 / 6%);--shadow-md:0 1px 2px rgb(16 24 40 / 5%),0 4px 16px -4px rgb(16 24 40 / 10%);
18
- --brand-point:#C12D30;--sev-broken:#C12D30;--sev-broken-fg:#C12D30;--sev-broken-bg:#FEE4E2;
19
- --sev-risk:#9B3B9A;--sev-risk-fg:#86338A;--sev-risk-bg:#F7E8F7;
20
- --sev-warning:#C07B03;--sev-warning-fg:#7A5A00;--sev-warning-bg:#FBF0D0;
21
- --sev-info:#5B6B8C;--sev-info-fg:#3F4D6B;--sev-info-bg:#E9EDF5;
22
- --sev-ok:#17805A;--sev-ok-fg:#167A55;--sev-ok-bg:#E1F3EA;
23
- --owner-developer:#5550C0;--owner-developer-fg:#5A58C2;--owner-developer-bg:#EDEFFE;
24
- --owner-gtm:#0593BF;--owner-gtm-fg:#026F90;--owner-gtm-bg:#DFF4FE;
25
- --owner-marketing:#A72A6A;--owner-marketing-fg:#AE3A72;--owner-marketing-bg:#FEEAF1;
26
- --plat-ga4:#D17102;--plat-ga4-fg:#9C5301;--plat-ga4-bg:#FEECE0;
27
- --plat-meta:#3F87FE;--plat-meta-fg:#105FD9;--plat-meta-bg:#E8F1FE;
28
- --plat-google_ads:#299E48;--plat-google_ads-fg:#02782E;--plat-google_ads-bg:#DFF8E1;
29
- --vp-desktop:#08908C;--vp-desktop-fg:#097376;--vp-desktop-bg:#D2F9FA;
30
- --vp-mobile:#799506;--vp-mobile-fg:#596F01;--vp-mobile-bg:#EBF5D8;
31
- --consent-granted:#019F68;--consent-granted-fg:#167A55;--consent-granted-bg:#E1F3EA;
32
- --consent-denied:#B1011B;--consent-denied-fg:#B1011B;--consent-denied-bg:#FFEFEF;
33
- --consent-none:#5F636C;--consent-none-fg:#555963;--consent-none-bg:#EEF0F4;
34
- --plat-tiktok:#1F2433;--plat-tiktok-fg:#252B3B;--plat-tiktok-bg:#E9EBF0;
35
- /* Aliases: a semantic family that means the same as a status wears its colours, defined once so dark mode follows. */
26
+ --page:#f4f6fa;--card:#ffffff;--card-2:#ecf1f8;--well:#e9eef5;--fg:#142338;--fg-strong:#071529;--muted:#566479;--border:#d5dde8;--border-strong:#78869a;
27
+ --accent:#1161bb;--accent-fg:#1161bb;--accent-bg:#deedff;--selection:#c6defc;
28
+ --shadow-overlay:0 16px 48px rgb(7 21 41 / 14%);
29
+ --brand-point:#c12d30;--sev-broken:#c12d30;--sev-broken-fg:#c12d30;--sev-broken-bg:#f9eaea;
30
+ --sev-risk:#964799;--sev-risk-fg:#964799;--sev-risk-bg:#f5edf5;
31
+ --sev-warning:#8b6200;--sev-warning-fg:#8b6200;--sev-warning-bg:#f3efe6;
32
+ --sev-info:#1161bb;--sev-info-fg:#1161bb;--sev-info-bg:#e7eff8;
33
+ --sev-ok:#197746;--sev-ok-fg:#197746;--sev-ok-bg:#e8f1ed;
34
+ --owner-developer:#7b7ee4;--owner-developer-fg:#5b5bbd;--owner-developer-bg:#f2f2fc;
35
+ --owner-gtm:#2194b9;--owner-gtm-fg:#086f8d;--owner-gtm-bg:#e9f4f8;
36
+ --owner-marketing:#d85f92;--owner-marketing-fg:#af3a6f;--owner-marketing-bg:#fbeff4;
37
+ --plat-ga4:#d46700;--plat-ga4-fg:#b14c00;--plat-ga4-bg:#fbf0e6;
38
+ --plat-meta:#4b85f6;--plat-meta-fg:#2960ce;--plat-meta-bg:#edf3fe;
39
+ --plat-google_ads:#379b55;--plat-google_ads-fg:#027634;--plat-google_ads-bg:#ebf5ee;
40
+ --vp-desktop:#1e9895;--vp-desktop-fg:#08726f;--vp-desktop-bg:#e9f5f4;
41
+ --vp-mobile:#7a922c;--vp-mobile-fg:#596e01;--vp-mobile-bg:#f2f4ea;
42
+ --consent-denied:#a8162c;--consent-denied-fg:#a8162c;--consent-denied-bg:#f6e8ea;
43
+ /* Aliases: a semantic family that means the same as a status or a ground wears its colours, defined once so dark mode follows. */
44
+ --plat-tiktok:var(--fg);--plat-tiktok-fg:var(--fg);--plat-tiktok-bg:var(--card-2);
45
+ --consent-granted:var(--sev-ok);--consent-granted-fg:var(--sev-ok-fg);--consent-granted-bg:var(--sev-ok-bg);
46
+ --consent-none:var(--border-strong);--consent-none-fg:var(--muted);--consent-none-bg:var(--card-2);
36
47
  --run-completed:var(--sev-ok);--run-completed-fg:var(--sev-ok-fg);--run-completed-bg:var(--sev-ok-bg);
37
48
  --run-incomplete:var(--sev-warning);--run-incomplete-fg:var(--sev-warning-fg);--run-incomplete-bg:var(--sev-warning-bg);
38
49
  --group-failed:var(--sev-broken);--group-failed-fg:var(--sev-broken-fg);--group-failed-bg:var(--sev-broken-bg);
@@ -42,53 +53,51 @@ color-scheme:light dark;
42
53
  --score-mid:var(--sev-warning);--score-mid-fg:var(--sev-warning-fg);--score-mid-bg:var(--sev-warning-bg);
43
54
  --score-pass:var(--sev-ok);--score-pass-fg:var(--sev-ok-fg);--score-pass-bg:var(--sev-ok-bg);
44
55
  --score-track:var(--well);
56
+ /* 4 px base. Radii: cells 1, chips, badges and code 2, buttons and inputs 4, cards and panels 6; no pills. */
45
57
  --s1:4px;--s2:8px;--s3:12px;--s4:16px;--s5:24px;--s6:32px;--s7:48px;
46
- --r-sm:4px;--r-md:8px;--r-lg:12px;--r-pill:999px;
47
- --font-sans:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif;
48
- --font-mono:ui-monospace,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;
58
+ --r-cell:1px;--r-sm:2px;--r-ctl:4px;--r-md:6px;
59
+ /* The report is one file and ships no fonts: the families are named first and the system stands in where they are not installed. */
60
+ --font-sans:"Google Sans","Segoe UI Variable Text",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
61
+ --font-display:"Google Sans Flex","Google Sans","Segoe UI Variable Display",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
62
+ --font-mono:"Google Sans Code",ui-monospace,"SF Mono","Cascadia Mono",Menlo,Consolas,monospace;
63
+ --t-fast:160ms;--t-base:220ms;--ease:cubic-bezier(.2,0,0,1);
49
64
  --rail:200px;--top:48px}
50
65
  @media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
51
- --page:#0B0F19;--card:#141927;--well:#1B2235;--fg:#EEF2FF;--muted:#9AA4BD;--border:#263050;--border-strong:#34406A;
52
- --accent:#4F8EF7;--accent-fg:#8CBBFF;--accent-bg:#1A2F5C;
53
- --shadow-sm:none;--shadow-md:none;
54
- --brand-point:#FA6B6D;--sev-broken:#FA6B6D;--sev-broken-fg:#FA6B6D;--sev-broken-bg:#3D1D1D;
55
- --sev-risk:#D78AD6;--sev-risk-fg:#E3A6E0;--sev-risk-bg:#34203A;
56
- --sev-warning:#E8B44A;--sev-warning-fg:#F5C451;--sev-warning-bg:#352B10;
57
- --sev-info:#8191B8;--sev-info-fg:#B4C0DC;--sev-info-bg:#1E2536;
58
- --sev-ok:#3FB98A;--sev-ok-fg:#5BD39A;--sev-ok-bg:#113128;
59
- --owner-developer:#6967E1;--owner-developer-fg:#A2A8FE;--owner-developer-bg:#262748;
60
- --owner-gtm:#059AC7;--owner-gtm-fg:#3CBFF0;--owner-gtm-bg:#013041;
61
- --owner-marketing:#C64082;--owner-marketing-fg:#F490BA;--owner-marketing-bg:#421E2D;
62
- --plat-ga4:#B76300;--plat-ga4-fg:#F09C59;--plat-ga4-bg:#412308;
63
- --plat-meta:#2974F0;--plat-meta-fg:#83B2FE;--plat-meta-bg:#192B49;
64
- --plat-google_ads:#078C38;--plat-google_ads-fg:#70C47E;--plat-google_ads-bg:#133319;
65
- --vp-desktop:#029394;--vp-desktop-fg:#0FC6CB;--vp-desktop-bg:#023234;
66
- --vp-mobile:#728D05;--vp-mobile-fg:#A2BC59;--vp-mobile-bg:#263009;
67
- --consent-granted:#0AB074;--consent-granted-fg:#5BD39A;--consent-granted-bg:#113128;
68
- --consent-denied:#D01C29;--consent-denied-fg:#FF8A85;--consent-denied-bg:#4A1418;
69
- --consent-none:#747982;--consent-none-fg:#ABB0B9;--consent-none-bg:#282B31;
70
- --plat-tiktok:#E4E8F1;--plat-tiktok-fg:#E4E8F1;--plat-tiktok-bg:#272C3A;}}
66
+ --page:#071529;--card:#0e1e34;--card-2:#15263f;--well:#040e1f;--fg:#e1e8f3;--fg-strong:#f9fbff;--muted:#a2b1c6;--border:#22354e;--border-strong:#607695;
67
+ --accent:#69b1ff;--accent-fg:#69b1ff;--accent-bg:#0c325b;--selection:#123f6f;
68
+ --shadow-overlay:0 16px 48px rgb(2 8 18 / 55%);
69
+ --brand-point:#fa6b6d;--sev-broken:#fa6b6d;--sev-broken-fg:#fa6b6d;--sev-broken-bg:#342a3d;
70
+ --sev-risk:#dc86de;--sev-risk-fg:#dc86de;--sev-risk-bg:#2f2f4f;
71
+ --sev-warning:#f4cf5a;--sev-warning-fg:#f4cf5a;--sev-warning-bg:#333a3a;
72
+ --sev-info:#69b1ff;--sev-info-fg:#69b1ff;--sev-info-bg:#1d3654;
73
+ --sev-ok:#5ad093;--sev-ok-fg:#5ad093;--sev-ok-bg:#1a3a43;
74
+ --owner-developer:#7679de;--owner-developer-fg:#a5abff;--owner-developer-bg:#1f2d4f;
75
+ --owner-gtm:#148eb2;--owner-gtm-fg:#54bde3;--owner-gtm-bg:#0f3048;
76
+ --owner-marketing:#d1598d;--owner-marketing-fg:#ff88b7;--owner-marketing-bg:#2d2742;
77
+ --plat-ga4:#fa8c31;--plat-ga4-fg:#fa8c31;--plat-ga4-bg:#343034;
78
+ --plat-meta:#457fef;--plat-meta-fg:#88b1fe;--plat-meta-bg:#172e52;
79
+ --plat-google_ads:#2f954f;--plat-google_ads-fg:#63c47b;--plat-google_ads-bg:#133138;
80
+ --vp-desktop:#0f928f;--vp-desktop-fg:#53c1bd;--vp-desktop-bg:#0e3143;
81
+ --vp-mobile:#748d25;--vp-mobile-fg:#9fba56;--vp-mobile-bg:#1e3032;
82
+ --consent-denied:#ff5c69;--consent-denied-fg:#ff5c69;--consent-denied-bg:#35283c;}}
71
83
  :root[data-theme="dark"]{
72
- --page:#0B0F19;--card:#141927;--well:#1B2235;--fg:#EEF2FF;--muted:#9AA4BD;--border:#263050;--border-strong:#34406A;
73
- --accent:#4F8EF7;--accent-fg:#8CBBFF;--accent-bg:#1A2F5C;
74
- --shadow-sm:none;--shadow-md:none;
75
- --brand-point:#FA6B6D;--sev-broken:#FA6B6D;--sev-broken-fg:#FA6B6D;--sev-broken-bg:#3D1D1D;
76
- --sev-risk:#D78AD6;--sev-risk-fg:#E3A6E0;--sev-risk-bg:#34203A;
77
- --sev-warning:#E8B44A;--sev-warning-fg:#F5C451;--sev-warning-bg:#352B10;
78
- --sev-info:#8191B8;--sev-info-fg:#B4C0DC;--sev-info-bg:#1E2536;
79
- --sev-ok:#3FB98A;--sev-ok-fg:#5BD39A;--sev-ok-bg:#113128;
80
- --owner-developer:#6967E1;--owner-developer-fg:#A2A8FE;--owner-developer-bg:#262748;
81
- --owner-gtm:#059AC7;--owner-gtm-fg:#3CBFF0;--owner-gtm-bg:#013041;
82
- --owner-marketing:#C64082;--owner-marketing-fg:#F490BA;--owner-marketing-bg:#421E2D;
83
- --plat-ga4:#B76300;--plat-ga4-fg:#F09C59;--plat-ga4-bg:#412308;
84
- --plat-meta:#2974F0;--plat-meta-fg:#83B2FE;--plat-meta-bg:#192B49;
85
- --plat-google_ads:#078C38;--plat-google_ads-fg:#70C47E;--plat-google_ads-bg:#133319;
86
- --vp-desktop:#029394;--vp-desktop-fg:#0FC6CB;--vp-desktop-bg:#023234;
87
- --vp-mobile:#728D05;--vp-mobile-fg:#A2BC59;--vp-mobile-bg:#263009;
88
- --consent-granted:#0AB074;--consent-granted-fg:#5BD39A;--consent-granted-bg:#113128;
89
- --consent-denied:#D01C29;--consent-denied-fg:#FF8A85;--consent-denied-bg:#4A1418;
90
- --consent-none:#747982;--consent-none-fg:#ABB0B9;--consent-none-bg:#282B31;
91
- --plat-tiktok:#E4E8F1;--plat-tiktok-fg:#E4E8F1;--plat-tiktok-bg:#272C3A;}`;
84
+ --page:#071529;--card:#0e1e34;--card-2:#15263f;--well:#040e1f;--fg:#e1e8f3;--fg-strong:#f9fbff;--muted:#a2b1c6;--border:#22354e;--border-strong:#607695;
85
+ --accent:#69b1ff;--accent-fg:#69b1ff;--accent-bg:#0c325b;--selection:#123f6f;
86
+ --shadow-overlay:0 16px 48px rgb(2 8 18 / 55%);
87
+ --brand-point:#fa6b6d;--sev-broken:#fa6b6d;--sev-broken-fg:#fa6b6d;--sev-broken-bg:#342a3d;
88
+ --sev-risk:#dc86de;--sev-risk-fg:#dc86de;--sev-risk-bg:#2f2f4f;
89
+ --sev-warning:#f4cf5a;--sev-warning-fg:#f4cf5a;--sev-warning-bg:#333a3a;
90
+ --sev-info:#69b1ff;--sev-info-fg:#69b1ff;--sev-info-bg:#1d3654;
91
+ --sev-ok:#5ad093;--sev-ok-fg:#5ad093;--sev-ok-bg:#1a3a43;
92
+ --owner-developer:#7679de;--owner-developer-fg:#a5abff;--owner-developer-bg:#1f2d4f;
93
+ --owner-gtm:#148eb2;--owner-gtm-fg:#54bde3;--owner-gtm-bg:#0f3048;
94
+ --owner-marketing:#d1598d;--owner-marketing-fg:#ff88b7;--owner-marketing-bg:#2d2742;
95
+ --plat-ga4:#fa8c31;--plat-ga4-fg:#fa8c31;--plat-ga4-bg:#343034;
96
+ --plat-meta:#457fef;--plat-meta-fg:#88b1fe;--plat-meta-bg:#172e52;
97
+ --plat-google_ads:#2f954f;--plat-google_ads-fg:#63c47b;--plat-google_ads-bg:#133138;
98
+ --vp-desktop:#0f928f;--vp-desktop-fg:#53c1bd;--vp-desktop-bg:#0e3143;
99
+ --vp-mobile:#748d25;--vp-mobile-fg:#9fba56;--vp-mobile-bg:#1e3032;
100
+ --consent-denied:#ff5c69;--consent-denied-fg:#ff5c69;--consent-denied-bg:#35283c;}`;
92
101
  // ---------- end of tokens ----------
93
102
 
94
103
  // ---------- components: chips, badges, cards, tabs, filter bar, tables, scorecard ----------
@@ -102,10 +111,10 @@ html{-webkit-text-size-adjust:100%;text-size-adjust:100%}
102
111
  body{margin:0;background:var(--page);color:var(--fg);font:400 14px/1.5 var(--font-sans);-webkit-font-smoothing:antialiased}
103
112
  h1,h2,h3,h4,h5,h6,p,ul,ol,figure{margin:0}
104
113
  ul,ol{padding:0}
105
- h1{font-size:24px;line-height:1.2;font-weight:700;letter-spacing:-.01em;overflow-wrap:anywhere}
106
- h2{font-size:17px;line-height:1.3;font-weight:600}
107
- h3,h4{font-size:14px;line-height:1.4;font-weight:600}
108
- h5,h6{font-size:12px;line-height:1.4;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
114
+ h1{font:570 24px/1.2 var(--font-display);letter-spacing:-.012em;color:var(--fg-strong);overflow-wrap:anywhere}
115
+ h2{font-size:17px;line-height:1.3;font-weight:600;color:var(--fg-strong)}
116
+ h3,h4{font-size:14px;line-height:1.4;font-weight:600;color:var(--fg-strong)}
117
+ h5,h6{font-size:12px;line-height:1.4;font-weight:500;color:var(--muted)}
109
118
  b,strong{font-weight:600}
110
119
  a{color:var(--accent-fg);text-decoration:none}
111
120
  a:hover{text-decoration:underline}
@@ -118,10 +127,11 @@ svg{flex:none}
118
127
  .xs{font-size:12.5px}
119
128
  .num,.tnum{font-variant-numeric:tabular-nums}
120
129
  .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
130
+ ::selection{background:var(--selection);color:var(--fg)}
121
131
  :focus-visible{outline:2px solid var(--accent);outline-offset:2px}
122
132
  .n{display:inline-block;min-width:18px;padding:0 5px;border-radius:var(--r-sm);background:var(--well);color:var(--muted);font:500 11px/17px var(--font-mono);text-align:center;font-variant-numeric:tabular-nums}
123
- .btn{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;border:1px solid var(--border);border-radius:var(--r-sm);background:var(--card);color:var(--fg);font:500 12.5px var(--font-sans);cursor:pointer}
124
- .btn:hover{background:var(--well)}
133
+ .btn{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;border:1px solid var(--border-strong);border-radius:var(--r-ctl);background:var(--card);color:var(--fg);font:500 12.5px var(--font-sans);cursor:pointer}
134
+ .btn:hover{background:var(--card-2)}
125
135
  .chips{display:flex;flex-wrap:wrap;gap:6px;align-items:center}
126
136
 
127
137
  /* ---------- chips ----------
@@ -134,7 +144,7 @@ svg{flex:none}
134
144
  .chip>svg{color:var(--k)}
135
145
  .chip>svg{width:13px;height:13px}
136
146
  .chip>svg:first-child{margin-left:-3px}
137
- .chip b{font-weight:700}
147
+ .chip b{font-weight:600}
138
148
  .chip .k{color:var(--fg);font-weight:400}
139
149
  .chip.mono{font-family:var(--font-mono);font-size:11.5px}
140
150
  .chip .dot,.dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--k,var(--muted));flex:none}
@@ -170,9 +180,9 @@ a.chip:hover{text-decoration:none;border-color:var(--k)}
170
180
  /* Denied shares broken's red on purpose; it is told apart by weight: a deeper red, an outline and a bold value,
171
181
  in the mono key/value form, never with the octagon. Broken is a filled tint with the octagon glyph. */
172
182
  .chip-consent{font-family:var(--font-mono);font-size:11.5px}
173
- .chip-consent.denied{border-color:var(--k);font-weight:700}
174
- .chip-consent.granted{font-weight:600}
175
- .chip-consent.none{font-weight:500}
183
+ .chip-consent.denied{border-color:var(--k);font-weight:600}
184
+ .chip-consent.granted{font-weight:500}
185
+ .chip-consent.none{font-weight:400}
176
186
 
177
187
  /* ---------- severity badge (C's, unchanged in shape) ---------- */
178
188
  .sev-broken{--sev:var(--sev-broken);--sev-fg:var(--sev-broken-fg);--sev-bg:var(--sev-broken-bg)}
@@ -188,24 +198,22 @@ a.chip:hover{text-decoration:none;border-color:var(--k)}
188
198
  /* ---------- cards ----------
189
199
  <details class="card [sev-x]"><summary>…</summary><div class="body">…</div></details>, collapsed unless [open].
190
200
  A plain <div class="card"> is the Summary's static card. */
191
- .card{display:block;background:var(--card);border:1px solid var(--border);border-radius:var(--r-md);box-shadow:var(--shadow-sm);min-width:0}
201
+ .card{display:block;background:var(--card);border:1px solid var(--border);border-radius:var(--r-md);min-width:0}
192
202
  .card+.card{margin-top:var(--s2)}
193
203
  .card[class*=sev-]{border-left:3px solid var(--sev)}
194
204
  .card>.body{padding:var(--s3) var(--s4) var(--s4)}
195
205
  div.card>.body{padding-top:var(--s4)}
196
206
  details.card>summary{display:flex;align-items:center;gap:var(--s2) var(--s3);flex-wrap:wrap;min-height:44px;padding:10px 40px 10px var(--s4);position:relative;cursor:pointer;list-style:none;border-radius:calc(var(--r-md) - 1px);user-select:none}
197
207
  details.card>summary::-webkit-details-marker{display:none}
198
- details.card>summary::after{content:"";position:absolute;right:18px;top:50%;width:7px;height:7px;margin-top:-5px;border-right:1.75px solid var(--muted);border-bottom:1.75px solid var(--muted);transform:rotate(-45deg);transition:transform .15s}
208
+ details.card>summary::after{content:"";position:absolute;right:18px;top:50%;width:7px;height:7px;margin-top:-5px;border-right:1.75px solid var(--muted);border-bottom:1.75px solid var(--muted);transform:rotate(-45deg);transition:transform var(--t-fast) var(--ease)}
199
209
  details.card[open]>summary::after{transform:rotate(45deg);margin-top:-6px}
200
- details.card>summary:hover{background:var(--well)}
210
+ details.card>summary:hover{background:var(--card-2)}
201
211
  details.card>summary:hover::after{border-color:var(--fg)}
202
212
  details.card>summary:focus-visible{outline-offset:-2px}
203
213
  .revealed:focus,.revealed:focus-visible{outline:none}
204
214
  details.card[open]>summary{border-bottom:1px solid var(--border);border-bottom-left-radius:0;border-bottom-right-radius:0}
205
215
  details.card>summary .t{font-weight:600;min-width:0;overflow-wrap:anywhere}
206
216
  details.card>summary .end{margin-left:auto;display:inline-flex;align-items:center;gap:6px}
207
- /* nested cards sit inside a body: hairline only, no second shadow */
208
- .card .card{box-shadow:none}
209
217
  .card .card>summary{min-height:38px;padding-top:7px;padding-bottom:7px}
210
218
 
211
219
  /* ---------- tabs ----------
@@ -214,9 +222,9 @@ details.card>summary .end{margin-left:auto;display:inline-flex;align-items:cente
214
222
  .tabs{min-width:0}
215
223
  .tabs [role=tablist]{display:flex;gap:2px;border-bottom:1px solid var(--border);overflow-x:auto;scrollbar-width:none;position:relative}
216
224
  .tabs [role=tablist]::-webkit-scrollbar{display:none}
217
- .tabs [role=tab]{appearance:none;background:none;border:0;border-bottom:2px solid transparent;margin-bottom:-1px;padding:8px 12px;color:var(--muted);font:500 13px/1.3 var(--font-sans);white-space:nowrap;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:color .15s,border-color .15s}
225
+ .tabs [role=tab]{appearance:none;background:none;border:0;border-bottom:2px solid transparent;margin-bottom:-1px;padding:8px 12px;color:var(--muted);font:500 13px/1.3 var(--font-sans);white-space:nowrap;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:color var(--t-fast) var(--ease),border-color var(--t-fast) var(--ease)}
218
226
  .tabs [role=tab]:hover{color:var(--fg)}
219
- .tabs [role=tab][aria-selected=true]{color:var(--fg);border-bottom-color:var(--accent)}
227
+ .tabs [role=tab][aria-selected=true]{color:var(--fg-strong);border-bottom-color:var(--accent)}
220
228
  .tabs [role=tab][aria-selected=true] .n{background:var(--accent-bg);color:var(--accent-fg)}
221
229
  .tabs [role=tab]:focus-visible{outline-offset:-2px}
222
230
  .tabs [role=tabpanel]{padding-top:var(--s3);min-width:0}
@@ -225,24 +233,24 @@ details.card>summary .end{margin-left:auto;display:inline-flex;align-items:cente
225
233
  .tabs:not(.js) [role=tablist]{display:none}
226
234
  .tabs:not(.js) [role=tabpanel]{display:block}
227
235
  .tabs:not(.js) [role=tabpanel]+[role=tabpanel]{margin-top:var(--s3);border-top:1px solid var(--border)}
228
- .tabs:not(.js) [role=tabpanel][data-label]::before{content:attr(data-label);display:block;margin-bottom:var(--s2);font:600 12px/1.4 var(--font-sans);color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
236
+ .tabs:not(.js) [role=tabpanel][data-label]::before{content:attr(data-label);display:block;margin-bottom:var(--s2);font:500 12px/1.4 var(--font-sans);color:var(--muted)}
229
237
 
230
238
  /* ---------- filter bar ----------
231
239
  <div class="filterbar" data-filterbar="#LIST"><span class="fl">Severity</span><button data-filter="sev=broken"
232
240
  aria-pressed="false" class="chip-sev broken"><span class="dot"></span>Broken <span class="n">2</span></button>…
233
241
  <input type="search" data-filter-search placeholder="Search"><p data-filter-count>4 of 4</p></div> */
234
- .filterbar{display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:var(--s2) var(--s3);background:var(--card);border:1px solid var(--border);border-radius:var(--r-md);box-shadow:var(--shadow-sm)}
235
- .filterbar .fl{font:600 11px/1 var(--font-sans);color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin:0 2px 0 6px}
242
+ .filterbar{display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:var(--s2) var(--s3);background:var(--card);border:1px solid var(--border);border-radius:var(--r-md)}
243
+ .filterbar .fl{font:500 12px/1 var(--font-sans);color:var(--muted);margin:0 2px 0 6px}
236
244
  .filterbar .fl:first-child{margin-left:0}
237
245
  .filterbar .sep{width:1px;align-self:stretch;margin:2px 4px;background:var(--border)}
238
246
  .filterbar [data-filter]{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;border:1px solid var(--border);border-radius:var(--r-sm);background:var(--card);color:var(--fg);font:500 12.5px/1 var(--font-sans);white-space:nowrap;cursor:pointer;font-variant-numeric:tabular-nums}
239
- .filterbar [data-filter]:hover{border-color:var(--border-strong);background:var(--well)}
247
+ .filterbar [data-filter]:hover{border-color:var(--border-strong);background:var(--card-2)}
240
248
  .filterbar [data-filter][aria-pressed=true]{background:var(--accent-bg);border-color:var(--accent);color:var(--accent-fg)}
241
249
  .filterbar [data-filter][aria-pressed=true]::before{content:"";width:8px;height:4px;margin:-3px 0 0 -1px;border-left:1.75px solid currentColor;border-bottom:1.75px solid currentColor;transform:rotate(-45deg)}
242
250
  .filterbar [data-filter] svg{width:14px;height:14px}
243
- .filterbar [data-filter-search]{flex:1 1 180px;min-width:0;max-width:320px;height:30px;margin-left:auto;padding:0 12px 0 32px;border:1px solid var(--border);border-radius:var(--r-sm);background:var(--page) no-repeat 11px 50%/14px 14px;color:var(--fg);font:400 13px var(--font-sans);}
251
+ .filterbar [data-filter-search]{flex:1 1 180px;min-width:0;max-width:320px;height:30px;margin-left:auto;padding:0 12px 0 32px;border:1px solid var(--border-strong);border-radius:var(--r-ctl);background:var(--card);color:var(--fg);font:400 13px var(--font-sans);}
244
252
  .filterbar [data-filter-search]::placeholder{color:var(--muted)}
245
- .filterbar [data-filter-search]:focus{outline:2px solid var(--accent);outline-offset:0;border-color:var(--accent)}
253
+ .filterbar [data-filter-search]:focus{outline:2px solid var(--accent);outline-offset:2px;border-color:var(--accent)}
246
254
  .filterbar [data-filter-count]{flex-basis:100%;margin:2px 0 0;font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums}
247
255
  .filterbar [data-filter-count]:empty{display:none}
248
256
  html:not(.js) .filterbar{display:none}
@@ -251,12 +259,12 @@ html:not(.js) .filterbar{display:none}
251
259
  .tscroll{overflow-x:auto;position:relative;border:1px solid var(--border);border-radius:var(--r-md);background:var(--card)}
252
260
  .tbl{width:100%;border-collapse:collapse;font-size:13px;font-variant-numeric:tabular-nums}
253
261
  .tbl th,.tbl td{text-align:left;padding:7px 10px;border-bottom:1px solid var(--border);vertical-align:middle;overflow-wrap:anywhere}
254
- .tbl thead th{font-weight:500;font-size:12px;color:var(--muted);background:var(--well);white-space:nowrap}
262
+ .tbl thead th{font-weight:500;font-size:12px;color:var(--muted);border-bottom-color:var(--border-strong);white-space:nowrap}
255
263
  .tbl tbody th{font-weight:500}
256
264
  .tbl th:first-child,.tbl td:first-child{padding-left:var(--s4)}
257
265
  .tbl th:last-child,.tbl td:last-child{padding-right:var(--s4)}
258
266
  .tbl tbody tr:last-child>*{border-bottom:0}
259
- .tbl tbody tr:hover>*{background:color-mix(in srgb,var(--well) 55%,transparent)}
267
+ .tbl tbody tr:hover>*{background:var(--card-2)}
260
268
  .tbl .num{text-align:right;font-family:var(--font-mono);white-space:nowrap}
261
269
  .tbl .bad{color:var(--sev-broken-fg);font-weight:600}
262
270
  .tbl .dim{color:var(--muted)}
@@ -265,11 +273,11 @@ html:not(.js) .filterbar{display:none}
265
273
  <div class="grid"><div class="info"><span class="ic-plate"><svg/></span><span class="lbl">Vendors</span>
266
274
  <span class="val">2 of 4</span><span class="sub">GA4 · Meta</span></div></div> */
267
275
  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:var(--s3)}
268
- .grid>.info{display:grid;grid-template-columns:auto minmax(0,1fr);grid-template-areas:"ic lbl" "val val" "sub sub";align-items:center;gap:6px var(--s2);padding:14px var(--s4);background:var(--card);border:1px solid var(--border);border-radius:var(--r-md);box-shadow:var(--shadow-sm);min-width:0}
276
+ .grid>.info{display:grid;grid-template-columns:auto minmax(0,1fr);grid-template-areas:"ic lbl" "val val" "sub sub";align-items:center;gap:6px var(--s2);padding:14px var(--s4);background:var(--card);border:1px solid var(--border);border-radius:var(--r-md);min-width:0}
269
277
  .info .ic-plate{grid-area:ic;display:grid;place-items:center;width:26px;height:26px;border-radius:var(--r-sm);background:var(--well);color:var(--muted)}
270
278
  .info .ic-plate svg{width:15px;height:15px}
271
279
  .info .lbl{grid-area:lbl;font-size:12.5px;font-weight:500;color:var(--muted)}
272
- .info .val{grid-area:val;margin-top:4px;font-size:20px;line-height:1.2;font-weight:650;letter-spacing:-.01em;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
280
+ .info .val{grid-area:val;margin-top:4px;font-size:20px;line-height:1.2;font-weight:600;color:var(--fg-strong);letter-spacing:-.01em;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
273
281
  .info .val small{font-size:13px;font-weight:500;color:var(--muted);letter-spacing:0}
274
282
  .info .sub{grid-area:sub;font-size:12.5px;color:var(--muted);display:flex;flex-wrap:wrap;gap:4px;align-items:center;min-width:0}
275
283
  .grid>.info.wide{grid-column:span 2}
@@ -289,14 +297,14 @@ html:not(.js) .filterbar{display:none}
289
297
  .band-fail{--band:var(--score-fail);--band-fg:var(--score-fail-fg);--band-bg:var(--score-fail-bg)}
290
298
  .band-mid{--band:var(--score-mid);--band-fg:var(--score-mid-fg);--band-bg:var(--score-mid-bg)}
291
299
  .band-pass{--band:var(--score-pass);--band-fg:var(--score-pass-fg);--band-bg:var(--score-pass-bg)}
292
- .scorecard{display:grid;grid-template-columns:120px minmax(0,1fr);grid-template-areas:"ring verdict" "ring head" "ring costs" "chips chips";column-gap:var(--s6);align-items:start;padding:var(--s5) var(--s6);background:var(--card);border:1px solid var(--border);border-top:4px solid var(--band);border-radius:var(--r-lg);box-shadow:var(--shadow-md)}
300
+ .scorecard{display:grid;grid-template-columns:120px minmax(0,1fr);grid-template-areas:"ring verdict" "ring head" "ring costs" "chips chips";column-gap:var(--s6);align-items:start;padding:var(--s5) var(--s6);background:var(--card);border:1px solid var(--border);border-top:3px solid var(--band);border-radius:var(--r-md)}
293
301
  .sc-ring{grid-area:ring;position:relative;width:120px;height:120px;display:grid;place-items:center;align-self:center}
294
302
  .sc-ring svg{position:absolute;inset:0;width:100%;height:100%;transform:rotate(-90deg)}
295
303
  .sc-ring .trk{fill:none;stroke:var(--score-track);stroke-width:10}
296
- .sc-ring .arc{fill:none;stroke:var(--band);stroke-width:10;stroke-linecap:round;stroke-dasharray:var(--score) 100}
297
- .sc-num{position:relative;font:700 44px/1 var(--font-sans);letter-spacing:-.03em;color:var(--fg);margin-top:-8px}
304
+ .sc-ring .arc{fill:none;stroke:var(--band);stroke-width:10;stroke-linecap:butt;stroke-dasharray:var(--score) 100}
305
+ .sc-num{position:relative;font:570 44px/1 var(--font-display);letter-spacing:-.02em;color:var(--fg-strong);font-variant-numeric:tabular-nums;margin-top:-8px}
298
306
  .sc-of{position:absolute;left:0;right:0;top:calc(50% + 18px);text-align:center;font:500 12px/1 var(--font-mono);color:var(--muted)}
299
- .sc-verdict{grid-area:verdict;align-self:end;display:flex;align-items:center;gap:8px;font:700 30px/1.1 var(--font-sans);letter-spacing:-.02em;color:var(--sev,var(--band))}
307
+ .sc-verdict{grid-area:verdict;align-self:end;display:flex;align-items:center;gap:8px;font:570 30px/1.1 var(--font-display);letter-spacing:-.012em;color:var(--sev,var(--band))}
300
308
  /* the verdict is large text (24–30px bold), where 3:1 is the bar; the solid band colour clears it in both modes
301
309
  and keeps the amber of "Needs attention" from turning brown, as the 4.5:1 text step would */
302
310
  .sc-verdict .ic{width:26px;height:26px}
@@ -324,7 +332,6 @@ html:not(.js) .filterbar{display:none}
324
332
  @media (prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}
325
333
  @media print{
326
334
  body{background:#fff;font-size:12.5px}
327
- .card,.grid>.info,.scorecard,.filterbar{box-shadow:none}
328
335
  .chip,.badge,.n{background:none!important;border:1px solid currentColor}
329
336
  .chip.zero{border-color:var(--border)}
330
337
  .tbl thead th{background:none;border-bottom:1px solid #000}
@@ -332,12 +339,12 @@ html:not(.js) .filterbar{display:none}
332
339
  details.card>summary:hover{background:none}
333
340
  details.card>summary::after,.filterbar,.tabs [role=tablist]{display:none}
334
341
  .tabs [role=tabpanel],.tabs [role=tabpanel][hidden]{display:block}
335
- .tabs [role=tabpanel][data-label]::before{content:attr(data-label);display:block;margin:var(--s2) 0;font:600 11px/1.4 var(--font-sans);text-transform:uppercase;letter-spacing:.04em}
342
+ .tabs [role=tabpanel][data-label]::before{content:attr(data-label);display:block;margin:var(--s2) 0;font:500 12px/1.4 var(--font-sans)}
336
343
  /* writePdf prints with JavaScript off, so a card marked data-print-open is opened by CSS alone */
337
344
  details.card[data-print-open]::details-content{content-visibility:visible;display:block}
338
345
  details.card[data-print-open]>summary{border-bottom:1px solid var(--border)}
339
346
  .card,.grid>.info,.scorecard,.tbl tr{break-inside:avoid}
340
- .scorecard{border-top-width:4px}
347
+ .scorecard{border-top-width:3px}
341
348
  }`;
342
349
  // ---------- end of components ----------
343
350
 
@@ -354,16 +361,16 @@ const SHELL = `/* Page shell: skip link, top bar, rail (expanded 200 px, collaps
354
361
  html.js .js-only{display:inline-flex!important}
355
362
  html.js .sec-tools.js-only{display:flex!important}
356
363
  .logo{display:inline-flex}
357
- svg.logo{display:inline-block;flex:none;color:var(--fg)}
364
+ svg.logo{display:inline-block;flex:none;color:var(--fg-strong)}
358
365
  .logo .l-dark{display:none}
359
366
  html[data-theme="dark"] .logo .l-light{display:none}
360
367
  html[data-theme="dark"] .logo .l-dark{display:inline}
361
368
  @media (prefers-color-scheme:dark){html:not([data-theme="light"]) .logo .l-light{display:none}html:not([data-theme="light"]) .logo .l-dark{display:inline}}
362
- .eyebrow{font:600 11px/1.4 var(--font-sans);text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}
369
+ .eyebrow{font:500 12px/1.4 var(--font-sans);color:var(--muted)}
363
370
  .mock-note{display:flex;gap:var(--s2);align-items:flex-start;padding:var(--s2) var(--s3);margin:var(--s3) 0 var(--s4);border:1px dashed var(--sev-info);border-radius:var(--r-sm);color:var(--sev-info-fg);font-size:12.5px}
364
371
  .mock-note .ic{margin-top:2px}
365
372
 
366
- .skip{position:absolute;left:var(--s4);top:-48px;z-index:50;padding:var(--s2) var(--s3);background:var(--card);border:1px solid var(--border);border-radius:var(--r-sm);color:var(--fg)}
373
+ .skip{position:absolute;left:var(--s4);top:-48px;z-index:50;padding:var(--s2) var(--s3);background:var(--card);border:1px solid var(--border);border-radius:var(--r-ctl);color:var(--fg)}
367
374
  .skip:focus{top:var(--s2)}
368
375
 
369
376
  /* ---------- top bar ---------- */
@@ -374,36 +381,36 @@ html[data-theme="dark"] .logo .l-dark{display:inline}
374
381
  .top-site b{font-weight:600;overflow:hidden;text-overflow:ellipsis}
375
382
  .top-date{font-size:12.5px;font-variant-numeric:tabular-nums}
376
383
  .top-verdict{display:inline-flex;align-items:center;gap:5px;height:26px;padding:0 9px;border:1px solid var(--border);border-color:color-mix(in srgb,var(--v) 45%,var(--border));border-radius:var(--r-sm);background:var(--card);color:var(--v-fg);font:500 12.5px/1 var(--font-sans);font-variant-numeric:tabular-nums;flex:none}
377
- .top-verdict b{font-weight:700;font-size:14px}
384
+ .top-verdict b{font-weight:600;font-size:14px}
378
385
  .top-verdict .sc-l{color:var(--muted)}
379
- .top-verdict:hover{text-decoration:none;filter:brightness(.97)}
386
+ .top-verdict:hover{text-decoration:none;background:var(--card-2)}
380
387
  .top-verdict .sc{font-weight:500;color:var(--muted)}
381
388
  .v-fail{--v:var(--score-fail);--v-fg:var(--score-fail-fg);--v-bg:var(--score-fail-bg)}
382
389
  .v-mid{--v:var(--score-mid);--v-fg:var(--score-mid-fg);--v-bg:var(--score-mid-bg)}
383
390
  .v-pass{--v:var(--score-pass);--v-fg:var(--score-pass-fg);--v-bg:var(--score-pass-bg)}
384
391
  .top-end{margin-left:auto;display:flex;align-items:center;gap:var(--s2)}
385
392
  .byline{display:inline-flex;align-items:center;gap:var(--s2);font-size:12px;color:var(--muted);margin-right:var(--s2)}
386
- .mock-switch{align-items:center;gap:.3em;height:28px;padding:0 10px;border:1px dashed var(--border-strong);border-radius:var(--r-sm);background:none;color:var(--muted);font:500 12px var(--font-sans);cursor:pointer}
393
+ .mock-switch{align-items:center;gap:.3em;height:28px;padding:0 10px;border:1px dashed var(--border-strong);border-radius:var(--r-ctl);background:none;color:var(--muted);font:500 12px var(--font-sans);cursor:pointer}
387
394
  .mock-switch:hover{color:var(--fg);border-color:var(--muted)}
388
395
  .mock-switch[aria-pressed=true]{border-color:var(--sev-info);color:var(--sev-info-fg);background:var(--sev-info-bg)}
389
- .icon-btn{align-items:center;justify-content:center;width:32px;height:32px;padding:0;border:0;border-radius:var(--r-sm);background:none;color:var(--muted);cursor:pointer}
390
- .icon-btn:hover{background:var(--well);color:var(--fg)}
396
+ .icon-btn{align-items:center;justify-content:center;width:32px;height:32px;padding:0;border:0;border-radius:var(--r-ctl);background:none;color:var(--muted);cursor:pointer}
397
+ .icon-btn:hover{background:var(--card-2);color:var(--fg)}
391
398
 
392
399
  /* ---------- shell and rail ---------- */
393
400
  .shell{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:start}
394
401
  .rail{position:sticky;top:var(--top);z-index:20;width:var(--rail);height:calc(100vh - var(--top));overflow:hidden auto;padding:var(--s2) 4px;border-right:1px solid var(--border);background:var(--card)}
395
- html.rail-anim .rail{transition:width .2s ease}
402
+ html.rail-anim .rail{transition:width var(--t-base) var(--ease)}
396
403
  .rail ul{list-style:none;margin:0;padding:0;display:grid;gap:2px}
397
404
  .r-head{display:flex;margin-bottom:var(--s2);padding-bottom:var(--s2);border-bottom:1px solid var(--border)}
398
- .r-toggle,.rail a{position:relative;display:flex;align-items:center;gap:10px;height:36px;padding:0 10px 0 11px;border-radius:var(--r-sm);color:var(--fg);font-size:13.5px;white-space:nowrap}
405
+ .r-toggle,.rail a{position:relative;display:flex;align-items:center;gap:10px;height:36px;padding:0 10px 0 11px;border-radius:var(--r-ctl);color:var(--fg);font-size:13.5px;white-space:nowrap}
399
406
  .r-toggle{width:40px;border:0;background:none;color:var(--muted);cursor:pointer}
400
- .r-toggle .ic{transform:rotate(180deg);transition:transform .2s}
401
- .r-toggle:hover,.rail a[href]:hover{background:var(--well);text-decoration:none}
407
+ .r-toggle .ic{transform:rotate(180deg);transition:transform var(--t-base) var(--ease)}
408
+ .r-toggle:hover,.rail a[href]:hover{background:var(--card-2);text-decoration:none}
402
409
  .rail a[aria-disabled]{color:var(--muted);opacity:.6}
403
- .rail a[aria-current]{background:var(--accent-bg);color:var(--accent-fg);font-weight:600;box-shadow:inset 3px 0 var(--accent)}
410
+ .rail a[aria-current]{background:var(--accent-bg);color:var(--accent-fg);font-weight:600;box-shadow:inset 2px 0 var(--accent)}
404
411
  .rail .ic{width:18px;height:18px}
405
- .r-l{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;transition:opacity .15s}
406
- .r-t{display:inline-flex;align-items:center;min-width:20px;justify-content:center;padding:0 6px;border-radius:var(--r-pill);background:var(--t-bg);color:var(--t-fg);font:500 11.5px/18px var(--font-mono);font-variant-numeric:tabular-nums}
412
+ .r-l{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;transition:opacity var(--t-fast) var(--ease)}
413
+ .r-t{display:inline-flex;align-items:center;min-width:20px;justify-content:center;padding:0 6px;border-radius:var(--r-sm);background:var(--t-bg);color:var(--t-fg);font:500 11.5px/18px var(--font-mono);font-variant-numeric:tabular-nums}
407
414
  .r-t .ic{width:14px;height:14px;color:var(--t)}
408
415
  .r-t:has(.ic){background:none;padding:0}
409
416
  .r-d{display:none;position:absolute;left:26px;top:7px;width:6px;height:6px;border-radius:50%;background:var(--t);box-shadow:0 0 0 2px var(--card)}
@@ -416,7 +423,7 @@ html.rail-anim .rail{transition:width .2s ease}
416
423
  [data-tone=incomplete]{--t:var(--run-incomplete);--t-fg:var(--run-incomplete-fg);--t-bg:var(--run-incomplete-bg)}
417
424
  [data-tone=neutral]{--t:var(--muted);--t-fg:var(--muted);--t-bg:var(--well)}
418
425
  /* tooltip from data-tip: always on the toggle, on the links only when collapsed */
419
- .rail [data-tip]::after{content:attr(data-tip);position:absolute;left:calc(100% + 8px);top:50%;transform:translateY(-50%);padding:4px 8px;border-radius:var(--r-sm);background:var(--fg);color:var(--card);font:500 12px/1.4 var(--font-sans);white-space:nowrap;pointer-events:none;opacity:0;visibility:hidden;z-index:40;box-shadow:var(--shadow-md)}
426
+ .rail [data-tip]::after{content:attr(data-tip);position:absolute;left:calc(100% + 8px);top:50%;transform:translateY(-50%);padding:4px 8px;border-radius:var(--r-ctl);background:var(--fg);color:var(--card);font:500 12px/1.4 var(--font-sans);white-space:nowrap;pointer-events:none;opacity:0;visibility:hidden;z-index:40;box-shadow:var(--shadow-overlay)}
420
427
  .r-toggle:is(:hover,:focus-visible)::after{opacity:1;visibility:visible}
421
428
  @media (min-width:720px){
422
429
  html[data-rail=collapsed] .rail{width:48px;overflow:visible}
@@ -458,7 +465,7 @@ html:not(.js) main>.sec[id^="failed-"]{display:none}
458
465
  .shell{display:block}
459
466
  .rail{top:var(--top);width:auto;height:auto;padding:0;border-right:0;border-bottom:1px solid var(--border);overflow:visible}
460
467
  .r-head{display:none}
461
- .rail ul{display:flex;gap:0;padding:0 var(--s2);overflow-x:auto;scrollbar-width:none;-webkit-mask-image:linear-gradient(to right,black calc(100% - 28px),transparent);mask-image:linear-gradient(to right,black calc(100% - 28px),transparent)}
468
+ .rail ul{display:flex;gap:0;padding:0 var(--s2);overflow-x:auto;scrollbar-width:none}
462
469
  .rail ul::-webkit-scrollbar{display:none}
463
470
  .rail a{height:40px;gap:6px;padding:0 10px;border-radius:0;font-size:13px;border-bottom:2px solid transparent}
464
471
  .rail a>.ic{display:none}
@@ -516,19 +523,21 @@ const SUMMARY = `/* Summary (SPEC §2): scorecard with status chips, then six "a
516
523
  a page holds it only when a finding was accepted. */
517
524
  .sm .scorecard{grid-template-areas:"ring verdict" "ring head" "ring ai" "ring costs" "chips chips" "acc acc"}
518
525
  .sm .sc-ai{grid-area:ai;margin-top:var(--s1);max-width:72ch;font-size:13.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
519
- .sm .sc-ai .ai-l{margin-right:4px;font:600 11px/1 var(--font-sans);text-transform:uppercase;letter-spacing:.04em}
526
+ .sm .sc-ai .ai-l{margin-right:4px;font:600 12px/1 var(--font-sans)}
520
527
  .sm .scorecard>p.meta{grid-area:acc;margin-top:var(--s2);font-size:12.5px;color:var(--muted)}
521
528
  .sm .info .val .sm-note{font-size:14px;font-weight:500;color:var(--muted)}
522
529
 
523
- /* explanation lines: one subgrid so points, text, platform chips and the where chip line up across lines */
530
+ /* explanation lines: one subgrid so points, text, platform chips and the where chip line up across lines. The points
531
+ sit on the text's first baseline and the chips at the top of the row, so a line that wraps (a wider system font, a
532
+ longer language) keeps them beside its first line instead of halfway down; a one-line row stays centred in its 32px. */
524
533
  .sm .sc-costs{grid-template-columns:44px auto auto auto minmax(0,1fr);column-gap:var(--s3);row-gap:0;margin-top:var(--s3)}
525
534
  .sm .sc-costs li{grid-column:1/-1;display:grid;grid-template-columns:subgrid;font-size:14px}
526
- .sm .sc-costs li>a{grid-column:1/-1;display:grid;grid-template-columns:subgrid;align-items:center;min-height:32px;padding:3px var(--s2);margin:0 calc(-1 * var(--s2));border-radius:var(--r-sm);color:var(--fg)}
527
- .sm .sc-costs li>a:hover{background:var(--well);text-decoration:none}
535
+ .sm .sc-costs li>a{grid-column:1/-1;display:grid;grid-template-columns:subgrid;align-items:baseline;align-content:center;min-height:32px;padding:3px var(--s2);margin:0 calc(-1 * var(--s2));border-radius:var(--r-ctl);color:var(--fg)}
536
+ .sm .sc-costs li>a:hover{background:var(--card-2);text-decoration:none}
528
537
  .sm .sc-costs .pts{grid-column:1;font-size:14px}
529
538
  .sm .sc-costs .what{grid-column:2;min-width:0}
530
- .sm .sc-costs .pl{grid-column:3;display:flex;gap:4px}
531
- .sm .sc-costs .wh{grid-column:4;display:flex;gap:4px}
539
+ .sm .sc-costs .pl{grid-column:3;display:flex;gap:4px;align-self:start}
540
+ .sm .sc-costs .wh{grid-column:4;display:flex;gap:4px;align-self:start}
532
541
  .sm .sc-costs.nopts{grid-template-columns:auto auto auto minmax(0,1fr)}
533
542
  .sm .sc-costs.nopts .what{grid-column:1}
534
543
  .sm .sc-costs.nopts .pl{grid-column:2}
@@ -556,9 +565,9 @@ const SUMMARY = `/* Summary (SPEC §2): scorecard with status chips, then six "a
556
565
  .sm-matrix{border-collapse:collapse;font-size:13px;line-height:1}
557
566
  .sm-matrix th,.sm-matrix td{padding:0}
558
567
  .sm-matrix thead th{padding:0 0 4px 9px;text-align:left;color:var(--muted)}
559
- .sm-matrix tbody th{padding-right:var(--s2);text-align:left;font-weight:500;color:var(--fg)}
560
- .sm-matrix td a{display:inline-flex;align-items:center;gap:5px;padding:3px 8px 3px 6px;border-radius:var(--r-sm);color:var(--fg)}
561
- .sm-matrix td a:hover{background:var(--well);text-decoration:none}
568
+ .sm-matrix tbody th{padding-right:var(--s2);text-align:left;font-weight:500;color:var(--fg);overflow-wrap:break-word}
569
+ .sm-matrix td a{display:inline-flex;align-items:center;gap:5px;padding:3px 8px 3px 6px;border-radius:var(--r-ctl);color:var(--fg)}
570
+ .sm-matrix td a:hover{background:var(--card-2);text-decoration:none}
562
571
  .sm-matrix .chip{width:20px;height:20px;padding:0;justify-content:center}
563
572
  .sm-matrix .chip>svg:first-child{margin:0}
564
573
  .sm-logo{display:inline-flex}
@@ -566,10 +575,12 @@ const SUMMARY = `/* Summary (SPEC §2): scorecard with status chips, then six "a
566
575
  /* four vendor chips wrap 3 + 1 in a third-width card; two by two reads as a set */
567
576
  @media (min-width:1024px){.sm .sm-chipval .val .chips{display:grid;grid-template-columns:repeat(2,max-content)}}
568
577
  @media (max-width:1023px){.sm .sm-about{grid-template-columns:repeat(2,minmax(0,1fr))}}
569
- @media (max-width:900px){
578
+ /* The score lines stack below 1100px: between 900 and 1100 with the rail open, the three chip columns left the text
579
+ column too narrow to keep an event name on one line. */
580
+ @media (max-width:1099px){
570
581
  .sm .sc-costs,.sm .sc-costs.nopts{display:grid;grid-template-columns:minmax(0,1fr)}
571
582
  .sm .sc-costs li,.sm .sc-costs li>a{grid-column:auto;grid-template-columns:none}
572
- .sm .sc-costs li>a{display:flex;flex-wrap:wrap;align-items:center;gap:4px var(--s2);padding:var(--s1) var(--s2)}
583
+ .sm .sc-costs li>a{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px var(--s2);padding:var(--s1) var(--s2)}
573
584
  .sm .sc-costs .pts{flex:0 0 36px}
574
585
  .sm .sc-costs .what{flex:1 1 calc(100% - 52px)}
575
586
  .sm .sc-costs:not(.nopts) .pl{margin-left:44px}
@@ -582,6 +593,15 @@ const SUMMARY = `/* Summary (SPEC §2): scorecard with status chips, then six "a
582
593
  .sm .sc-of{top:calc(50% + 13px)}
583
594
  }
584
595
  @media (max-width:599px){.sm .sm-about{grid-template-columns:minmax(0,1fr);grid-auto-rows:auto}}
596
+ /* The score and "/100" as one group centred in the ring, whatever font the system lends: trimmed to the figures' cap
597
+ height and baseline, their boxes no longer carry the font's own ascent and descent, which left the group 2.4px below
598
+ the middle in Google Sans and 4.1px in SF. A browser without text-box keeps the margins above. */
599
+ @supports (text-box:trim-both cap alphabetic){
600
+ .sm .sc-ring:has(.sc-of){align-content:center;row-gap:10px}
601
+ .sm .sc-ring .sc-num:not(.sm-none),.sm .sc-ring .sc-of{text-box:trim-both cap alphabetic;margin-top:0}
602
+ .sm .sc-ring .sc-of{position:static}
603
+ @media (max-width:640px){.sm .sc-ring:has(.sc-of){row-gap:5px}}
604
+ }
585
605
  @media print{
586
606
  .sm .sc-ai{white-space:normal}
587
607
  .sm .sc-costs li>a{padding-left:0;padding-right:0;margin:0}
@@ -604,7 +624,7 @@ const FINDINGS = `/* Findings (Direction D). Everything is scoped under .fnd so
604
624
  .fnd .fb-rest{order:5;display:flex;flex-wrap:wrap;align-items:center;gap:6px 0;min-width:0}
605
625
  .fnd .fg{display:inline-flex;align-items:center;gap:4px;flex:none}
606
626
  .fnd .fg+.fg,.fnd .fb-rest .fg+.fg{margin-left:8px;padding-left:8px;border-left:1px solid var(--border)}
607
- .fnd .filterbar .fl{margin:0 4px 0 0;font-size:10.5px;letter-spacing:.06em}
627
+ .fnd .filterbar .fl{margin:0 4px 0 0;font-size:12px}
608
628
  .fnd .filterbar [data-filter]{height:26px;padding:0 8px;gap:5px;font-size:12px}
609
629
  .fnd .filterbar [data-filter] .n{min-width:0;padding:0;background:none;font-size:11px;line-height:1;color:var(--muted)}
610
630
  .fnd .filterbar [data-filter][aria-pressed=true] .n{color:var(--accent-fg)}
@@ -617,7 +637,7 @@ const FINDINGS = `/* Findings (Direction D). Everything is scoped under .fnd so
617
637
  .fnd .fb-search{order:6;position:relative;display:flex;align-items:center;flex:1 1 120px;min-width:100px;max-width:260px;margin-left:auto}
618
638
  .fnd .fb-search>svg{position:absolute;left:11px;width:14px;height:14px;color:var(--muted);pointer-events:none}
619
639
  .fnd .filterbar [data-filter-search]{flex:1 1 auto;width:100%;max-width:none;margin-left:0;height:28px}
620
- .fnd .fb-clear{order:2;margin-left:auto;height:28px;padding:0 10px;border:0;border-radius:var(--r-pill);background:none;color:var(--accent-fg);font:500 12.5px var(--font-sans);cursor:pointer}
640
+ .fnd .fb-clear{order:2;margin-left:auto;height:28px;padding:0 10px;border:0;border-radius:var(--r-ctl);background:none;color:var(--accent-fg);font:500 12.5px var(--font-sans);cursor:pointer}
621
641
  .fnd .fb-clear:hover{background:var(--accent-bg)}
622
642
  .fnd .fb-clear[hidden]~[data-filter-count]{margin-left:auto}
623
643
  .fnd .filterbar [data-filter-count]{order:3;flex:none;margin:0 0 0 -4px;min-width:46px;text-align:right;font:500 12px var(--font-mono);color:var(--muted);white-space:nowrap}
@@ -626,7 +646,7 @@ const FINDINGS = `/* Findings (Direction D). Everything is scoped under .fnd so
626
646
  @media (max-width:719px){
627
647
  .fnd .filterbar{padding:8px}
628
648
  .fnd .fb-search{order:1;flex:1 1 100px;min-width:100px;max-width:none;margin-left:0}
629
- .fnd .fb-fold{order:2;display:inline-block;appearance:none;-webkit-appearance:none;margin:0;height:28px;padding:0 12px;border:1px solid var(--border);border-radius:var(--r-pill);background:var(--card);color:var(--fg);cursor:pointer;font:500 12.5px/26px var(--font-sans);white-space:nowrap}
649
+ .fnd .fb-fold{order:2;display:inline-block;appearance:none;-webkit-appearance:none;margin:0;height:28px;padding:0 12px;border:1px solid var(--border-strong);border-radius:var(--r-ctl);background:var(--card);color:var(--fg);cursor:pointer;font:500 12.5px/26px var(--font-sans);white-space:nowrap}
630
650
  .fnd .fb-fold::after{content:attr(data-label)}
631
651
  .fnd .filterbar:has([aria-pressed=true]) .fb-fold{border-color:var(--accent);background:var(--accent-bg);color:var(--accent-fg)}
632
652
  .fnd .filterbar:has([aria-pressed=true]) .fb-fold::after{content:attr(data-label) " (" counter(fc) ")"}
@@ -654,11 +674,11 @@ const FINDINGS = `/* Findings (Direction D). Everything is scoped under .fnd so
654
674
  .fnd .group>h3{margin-bottom:var(--s2)}
655
675
  .fnd details.group>summary{display:flex;align-items:center;position:relative;padding:2px 28px var(--s2) 0;cursor:pointer;list-style:none;user-select:none}
656
676
  .fnd details.group>summary::-webkit-details-marker{display:none}
657
- .fnd details.group>summary::after{content:"";position:absolute;right:16px;top:50%;width:7px;height:7px;margin-top:-6px;border-right:1.75px solid var(--muted);border-bottom:1.75px solid var(--muted);transform:rotate(-45deg);transition:transform .15s}
677
+ .fnd details.group>summary::after{content:"";position:absolute;right:16px;top:50%;width:7px;height:7px;margin-top:-6px;border-right:1.75px solid var(--muted);border-bottom:1.75px solid var(--muted);transform:rotate(-45deg);transition:transform var(--t-fast) var(--ease)}
658
678
  .fnd details.group[open]>summary::after{transform:rotate(45deg);margin-top:-7px}
659
679
  .fnd details.group>summary:hover::after{border-color:var(--fg)}
660
680
  .fnd details.group>summary:focus-visible{outline-offset:2px;border-radius:var(--r-sm)}
661
- .fnd .chip.lg b{font-weight:700}
681
+ .fnd .chip.lg b{font-weight:600}
662
682
  .fnd .g-rows>.card+.card{margin-top:6px}
663
683
  .fnd [hidden]{display:none!important}
664
684
 
@@ -682,8 +702,8 @@ const FINDINGS = `/* Findings (Direction D). Everything is scoped under .fnd so
682
702
  .fnd .run-multi .vp-mobile{color:var(--vp-mobile-fg)}
683
703
  .fnd .run-multi{gap:3px}
684
704
  .fnd .card .f-t code{font-size:12px}
685
- .fnd .card.line .go{position:absolute;right:12px;top:50%;margin-top:-10px;display:grid;place-items:center;width:20px;height:20px;color:var(--muted);border-radius:var(--r-sm)}
686
- .fnd .card.line .go:hover{color:var(--fg);background:var(--well);text-decoration:none}
705
+ .fnd .card.line .go{position:absolute;right:12px;top:50%;margin-top:-10px;display:grid;place-items:center;width:20px;height:20px;color:var(--muted);border-radius:var(--r-ctl)}
706
+ .fnd .card.line .go:hover{color:var(--fg);background:var(--card-2);text-decoration:none}
687
707
  .fnd .card.line .go svg{width:14px;height:14px}
688
708
  .fnd .card.line.flash,.fnd .card.flash{animation:aitd-flash 1.5s ease-out}
689
709
  @media (max-width:719px){
@@ -714,7 +734,7 @@ const FINDINGS = `/* Findings (Direction D). Everything is scoped under .fnd so
714
734
  .fnd-raw{margin-top:var(--s2);font-size:12.5px}
715
735
  .fnd-raw>summary{cursor:pointer;color:var(--accent-fg);list-style:none;display:inline-flex;align-items:center;gap:4px}
716
736
  .fnd-raw>summary::-webkit-details-marker{display:none}
717
- .fnd-raw>summary::before{content:"";width:6px;height:6px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transition:transform .15s}
737
+ .fnd-raw>summary::before{content:"";width:6px;height:6px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transition:transform var(--t-fast) var(--ease)}
718
738
  .fnd-raw[open]>summary::before{transform:rotate(45deg)}
719
739
  .fnd-raw pre{margin-top:var(--s2);max-height:240px;overflow:auto}
720
740
  .fnd-fix{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;font-size:13.5px}
@@ -723,8 +743,7 @@ const FINDINGS = `/* Findings (Direction D). Everything is scoped under .fnd so
723
743
  .fnd-where{list-style:none;display:grid;gap:6px}
724
744
  .fnd-where li{display:flex;flex-wrap:wrap;align-items:center;gap:4px 10px;font-size:13px}
725
745
  .fnd-step{min-width:0}
726
- .fnd .fnd-h{margin:var(--s4) 0 var(--s2);display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
727
- .fnd .fnd-h .chip{text-transform:none;letter-spacing:0}
746
+ .fnd .fnd-h{margin:var(--s4) 0 var(--s2);display:flex;align-items:center;gap:8px;font-size:12px;font-weight:500;color:var(--muted)}
728
747
  .fnd .fnd-h>svg{width:14px;height:14px}
729
748
  .fnd-fix>span{min-width:0}
730
749
  .fnd-recipe{list-style:none;counter-reset:rc;display:grid;gap:2px;margin:0;padding:var(--s2) var(--s3);background:var(--well);border-radius:var(--r-sm)}
@@ -738,7 +757,7 @@ const FINDINGS = `/* Findings (Direction D). Everything is scoped under .fnd so
738
757
  .fnd .filterbar{display:none}
739
758
  .fnd details.group[data-filter-group]::details-content{content-visibility:visible;display:block}
740
759
  .fnd details.group>summary::after{display:none}
741
- .fnd .card{break-inside:avoid;box-shadow:none}
760
+ .fnd .card{break-inside:avoid}
742
761
  .fnd-impact{background:none!important;border:1px solid currentColor}
743
762
  .fnd-recipe{background:none;border:1px solid var(--border)}
744
763
  }
@@ -784,10 +803,10 @@ const JOURNEYS = `/* Journeys (Direction D): journey tabs + viewport switch, ste
784
803
  #journeys .jr-g{display:inline-flex;color:var(--sev-fg)}
785
804
  #journeys .jr-g .ic{width:13px;height:13px;vertical-align:0}
786
805
  /* the viewport switch ([data-vp-switch]): a two-segment control; the pressed segment takes its viewport hue (identity, not status) */
787
- #journeys .jr-vp{display:inline-flex;gap:0;padding:2px;border:1px solid var(--border);border-radius:var(--r-pill);background:var(--well);margin:var(--s3) 0 var(--s2)}
806
+ #journeys .jr-vp{display:inline-flex;gap:0;padding:2px;border:1px solid var(--border);border-radius:var(--r-ctl);background:var(--well);margin:var(--s3) 0 var(--s2)}
788
807
  #journeys .jr-pick.js .jr-vp{position:absolute;top:3px;right:0;margin:0}
789
808
  html:not(.js) #journeys .jr-vp{display:none}
790
- #journeys .jr-vpb{--k:var(--vp-desktop);--k-fg:var(--vp-desktop-fg);--k-bg:var(--vp-desktop-bg);appearance:none;display:inline-flex;align-items:center;gap:5px;height:28px;padding:0 11px;border:0;border-radius:var(--r-pill);background:none;color:var(--muted);font:500 12.5px/1 var(--font-sans);white-space:nowrap;cursor:pointer}
809
+ #journeys .jr-vpb{--k:var(--vp-desktop);--k-fg:var(--vp-desktop-fg);--k-bg:var(--vp-desktop-bg);appearance:none;display:inline-flex;align-items:center;gap:5px;height:28px;padding:0 11px;border:0;border-radius:var(--r-sm);background:none;color:var(--muted);font:500 12.5px/1 var(--font-sans);white-space:nowrap;cursor:pointer}
791
810
  #journeys .jr-vpb.mobile{--k:var(--vp-mobile);--k-fg:var(--vp-mobile-fg);--k-bg:var(--vp-mobile-bg)}
792
811
  #journeys .jr-vpb:hover{color:var(--fg)}
793
812
  #journeys .jr-vpb .ic{width:14px;height:14px}
@@ -806,7 +825,7 @@ html.js #journeys .jr-runh{display:none}
806
825
  #journeys .jr-insp{display:grid;grid-template-columns:clamp(296px,22vw,380px) minmax(0,1fr);gap:var(--s4);align-items:start}
807
826
  html:not(.js) #journeys .jr-insp{grid-template-columns:minmax(0,1fr)}
808
827
  html:not(.js) #journeys .jr-steps{display:none}
809
- #journeys .jr-steps{position:sticky;top:calc(var(--top) + var(--s3));max-height:calc(100vh - var(--top) - var(--s5));overflow:auto;background:var(--card);border:1px solid var(--border);border-radius:var(--r-md);box-shadow:var(--shadow-sm);padding:var(--s1) 0 var(--s2);scrollbar-width:thin}
828
+ #journeys .jr-steps{position:sticky;top:calc(var(--top) + var(--s3));max-height:calc(100vh - var(--top) - var(--s5));overflow:auto;background:var(--card);border:1px solid var(--border);border-radius:var(--r-md);padding:var(--s1) 0 var(--s2);scrollbar-width:thin}
810
829
  #journeys .jr-steps ul{list-style:none;margin:0;padding:0}
811
830
  /* the timeline: one 2px line through the number column, from the first page to the last step */
812
831
  #journeys .jr-steps li{position:relative}
@@ -821,11 +840,11 @@ html:not(.js) #journeys .jr-steps{display:none}
821
840
  #journeys .jr-pg-p{justify-self:start;background:none;padding:0;font-size:11.5px;line-height:1.3;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
822
841
  #journeys .jr-st>button{appearance:none;position:relative;width:100%;display:grid;grid-template-columns:24px minmax(0,1fr) repeat(var(--jr-plats,2),30px) 14px;column-gap:6px;align-items:center;min-height:32px;padding:3px 12px 3px 13px;border:0;background:none;color:var(--fg);font:13px/1.3 var(--font-sans);text-align:left;cursor:pointer}
823
842
  #journeys .jr-st>button>.jr-t{margin-left:4px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
824
- #journeys .jr-st>button:hover{background:var(--well)}
843
+ #journeys .jr-st>button:hover{background:var(--card-2)}
825
844
  #journeys .jr-st>button:focus-visible{outline-offset:-2px}
826
- #journeys .jr-st>button:is([aria-selected=true],[aria-current=true],[aria-current=step],[aria-pressed=true],.selected){background:var(--accent-bg);box-shadow:inset 3px 0 0 var(--accent)}
845
+ #journeys .jr-st>button:is([aria-selected=true],[aria-current=true],[aria-current=step],[aria-pressed=true],.selected){background:var(--accent-bg);box-shadow:inset 2px 0 0 var(--accent)}
827
846
  #journeys .jr-st>button:is([aria-selected=true],[aria-current=true],[aria-current=step],[aria-pressed=true],.selected)>.jr-t{font-weight:600}
828
- #journeys .jr-n{position:relative;z-index:1;display:grid;place-items:center;width:24px;height:24px;border-radius:50%;background:var(--card);border:1.5px solid var(--border-strong);color:var(--fg);font:600 11px/1 var(--font-mono);font-variant-numeric:tabular-nums}
847
+ #journeys .jr-n{position:relative;z-index:1;display:grid;place-items:center;width:24px;height:24px;border-radius:var(--r-sm);background:var(--card);border:1.5px solid var(--border-strong);color:var(--fg);font:600 11px/1 var(--font-mono);font-variant-numeric:tabular-nums}
829
848
  #journeys .jr-st.zero .jr-n{border-style:dashed;color:var(--muted)}
830
849
  #journeys .jr-st.zero .jr-t{color:var(--muted)}
831
850
  #journeys .jr-st[class*=sev-] .jr-n{background:var(--sev-bg);border-color:var(--sev);color:var(--sev-fg)}
@@ -895,7 +914,7 @@ html:not(.js) #journeys .jr-steps{display:none}
895
914
  #journeys .jr-flag .ic{margin-top:2px}
896
915
  #journeys .jr-flag>span{flex:1 1 200px;min-width:0}
897
916
  #journeys .jr-hb{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,300px),1fr));gap:var(--s3) var(--s4)}
898
- #journeys .jr-h{display:flex;align-items:center;gap:6px;margin-bottom:6px;font-size:12px;line-height:1.4;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
917
+ #journeys .jr-h{display:flex;align-items:center;gap:6px;margin-bottom:6px;font-size:12px;line-height:1.4;font-weight:500;color:var(--muted)}
899
918
  #journeys .jr-hb .tbl{font-size:12.5px;border:1px solid var(--border);border-radius:var(--r-sm);border-collapse:separate;border-spacing:0}
900
919
  #journeys .jr-hb .tbl th,#journeys .jr-hb .tbl td{padding:4px 8px}
901
920
  #journeys .jr-kvt tbody th{width:38%;font:11.5px var(--font-mono);color:var(--muted);vertical-align:top}
@@ -929,7 +948,7 @@ html:not(.js) #journeys .jr-steps{display:none}
929
948
 
930
949
  /* the plan line under the status: the funnel sentence, then a chip per planned event that failed */
931
950
  #journeys .jr-plan{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s2) var(--s2);margin:calc(-1 * var(--s1)) 0 var(--s3);font-size:12.5px;min-width:0}
932
- #journeys .jr-plan-h{font:600 11px/1.4 var(--font-sans);color:var(--muted);text-transform:uppercase;letter-spacing:.04em;white-space:nowrap}
951
+ #journeys .jr-plan-h{font:500 12px/1.4 var(--font-sans);color:var(--muted);white-space:nowrap}
933
952
  #journeys .jr-plan-t{display:inline-flex;align-items:center;gap:5px;min-width:0}
934
953
  #journeys .jr-plan-t .jr-bad{color:var(--sev-broken)}
935
954
  #journeys .jr-plan-t .jr-good{color:var(--sev-ok)}
@@ -948,9 +967,9 @@ html:not(.js) #journeys .jr-steps{display:none}
948
967
  /* platform chips carry plat(): icon and name side by side; glossary tips on parameter names */
949
968
  #journeys .chip .plat{display:inline-flex;align-items:center;gap:5px}
950
969
  #journeys .chip .plat>svg{width:13px;height:13px}
951
- #journeys .q{position:relative;display:inline-grid;place-items:center;width:15px;height:15px;margin-left:5px;border:1px solid var(--muted);border-radius:50%;font:500 10px var(--font-sans);color:var(--muted);cursor:help;vertical-align:1px}
970
+ #journeys .q{position:relative;display:inline-grid;place-items:center;width:15px;height:15px;margin-left:5px;border:1px solid var(--muted);border-radius:var(--r-sm);font:500 10px var(--font-sans);color:var(--muted);cursor:help;vertical-align:1px}
952
971
  #journeys .q:hover{border-color:var(--accent);color:var(--accent-fg)}
953
- #journeys .q:hover::after{content:attr(data-tip);position:absolute;left:0;top:calc(100% + 6px);z-index:5;width:260px;padding:8px 10px;background:var(--fg);color:var(--page);font:400 12px/1.45 var(--font-sans);border-radius:var(--r-sm);white-space:normal;text-align:left;text-transform:none;letter-spacing:0}
972
+ #journeys .q:hover::after{content:attr(data-tip);position:absolute;left:0;top:calc(100% + 6px);z-index:5;width:260px;padding:8px 10px;background:var(--fg);color:var(--page);font:400 12px/1.45 var(--font-sans);border-radius:var(--r-ctl);box-shadow:var(--shadow-overlay);white-space:normal;text-align:left}
954
973
  #journeys .jr-kvt .pk{font-family:var(--font-mono)}
955
974
  #journeys .jr-kvt tr.pr td{font-family:var(--font-sans)}
956
975
  #journeys .jr-batch{margin-bottom:6px}
@@ -985,10 +1004,10 @@ html:not(.js) #journeys .jr-steps{display:none}
985
1004
  #journeys .jr-pg{display:block;width:1px;align-self:stretch;margin:4px 4px!important;padding:0;background:var(--border)}
986
1005
  #journeys .jr-pg:first-child{display:none}
987
1006
  #journeys .jr-pg>*{display:none}
988
- #journeys .jr-st>button{display:flex;gap:3px;width:auto;min-height:36px;padding:4px 6px;border-radius:var(--r-sm)}
1007
+ #journeys .jr-st>button{display:flex;gap:3px;width:auto;min-height:36px;padding:4px 6px;border-radius:var(--r-ctl)}
989
1008
  #journeys .jr-st>button>.jr-t,#journeys .jr-hc{display:none}
990
1009
  #journeys .jr-gs:empty{display:none}
991
- #journeys .jr-st>button:is([aria-selected=true],[aria-current=true],[aria-current=step],[aria-pressed=true],.selected){box-shadow:inset 0 -3px 0 var(--accent)}
1010
+ #journeys .jr-st>button:is([aria-selected=true],[aria-current=true],[aria-current=step],[aria-pressed=true],.selected){box-shadow:inset 0 -2px 0 var(--accent)}
992
1011
  #journeys .jr-ph{padding:var(--s3) var(--s3) 0}
993
1012
  #journeys .jr-ph-m{padding-left:0}
994
1013
  #journeys .jr-pane>.tabs{padding:0 var(--s3) var(--s3)}
@@ -1002,7 +1021,7 @@ html:not(.js) #journeys .jr-steps{display:none}
1002
1021
  #journeys .jr-pane,#journeys .jr-pick [role=tabpanel]{display:block!important}
1003
1022
  #journeys .jr-pane:not([data-flagged])>.tabs,#journeys .jr-pane:not([data-flagged])>p.jr-empty{display:none}
1004
1023
  #journeys .jr-pane:not([data-flagged]) .jr-ph{padding-bottom:var(--s2)}
1005
- #journeys .jr-pane{break-inside:avoid;box-shadow:none}
1024
+ #journeys .jr-pane{break-inside:avoid}
1006
1025
  /* on paper a step is its hits; the dataLayer and the requests stay in the HTML and capture.json */
1007
1026
  #journeys .jr-pane [role=tabpanel][id$="-dl"],#journeys .jr-pane [role=tabpanel][id$="-req"]{display:none!important}
1008
1027
  #journeys .jr-pane:not([data-flagged]){padding-bottom:0}
@@ -1052,7 +1071,7 @@ const CONSENT = `/* Consent: platform status lines, then the Consent Mode and Sc
1052
1071
  #consent .cs-go{margin-left:0}
1053
1072
  }
1054
1073
  @media print{
1055
- #consent .cs-row{box-shadow:none;break-inside:avoid}
1074
+ #consent .cs-row{break-inside:avoid}
1056
1075
  }
1057
1076
  /* journeys.css still has a bare .t (mono time); keep the card title independent of it */
1058
1077
  #consent details.card>summary>.t{font:600 14px/1.4 var(--font-sans);color:var(--fg);margin-left:0;white-space:normal}
@@ -1068,13 +1087,13 @@ const SCOPE = `/* Scope (Coverage, Inventory, Run). Selectors are scoped by sect
1068
1087
 
1069
1088
  /* ----- Coverage ----- */
1070
1089
  #coverage .cv-bar{display:inline-flex;gap:2px;align-items:center;height:14px;padding:0 2px}
1071
- #coverage .cv-bar i{width:7px;height:14px;border-radius:2px;background:var(--sev-ok)}
1090
+ #coverage .cv-bar i{width:7px;height:14px;border-radius:var(--r-cell);background:var(--sev-ok)}
1072
1091
  #coverage .cv-bar i.params{background:var(--sev-broken)}
1073
1092
  #coverage .cv-bar i.missing{background:var(--sev-warning)}
1074
1093
  #coverage .cv-pcs{gap:6px}
1075
1094
  #coverage .cv-pc .chip{gap:4px}
1076
1095
  #coverage .cv-pc .chip b{font-family:var(--font-mono);font-size:11px}
1077
- #coverage .filterbar{margin-bottom:var(--s3);box-shadow:none}
1096
+ #coverage .filterbar{margin-bottom:var(--s3)}
1078
1097
  #coverage .cv-tbl .cv-c{text-align:center;white-space:nowrap}
1079
1098
  #coverage .cv-tbl thead .cv-c{text-align:center}
1080
1099
  #coverage .cv-tbl .cv-g{padding:0 5px;height:22px}
@@ -1088,7 +1107,7 @@ const SCOPE = `/* Scope (Coverage, Inventory, Run). Selectors are scoped by sect
1088
1107
  #coverage .cv-key{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s2) var(--s3);margin-top:var(--s3)}
1089
1108
  /* first column stays put while the run columns scroll at 390 */
1090
1109
  #coverage .cv-scroll .cv-tbl th:first-child{position:sticky;left:0;z-index:1;background:var(--card)}
1091
- #coverage .cv-scroll .cv-tbl thead th:first-child{background:var(--well)}
1110
+ #coverage .cv-scroll .cv-tbl thead th:first-child{background:var(--card)}
1092
1111
 
1093
1112
  /* ----- Inventory ----- */
1094
1113
  #inventory .inv-rows{list-style:none;display:grid;gap:var(--s2);margin-top:var(--s3)}
@@ -1108,17 +1127,17 @@ const SCOPE = `/* Scope (Coverage, Inventory, Run). Selectors are scoped by sect
1108
1127
  #inventory .inv-list .chip.plain{height:20px;font-size:11.5px}
1109
1128
 
1110
1129
  /* ----- Run ----- */
1111
- #run .run-facts{margin:var(--s3) 0 var(--s2);padding:var(--s2) var(--s4);display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 var(--s5);background:var(--card);border:1px solid var(--border);border-radius:var(--r-md);box-shadow:var(--shadow-sm)}
1130
+ #run .run-facts{margin:var(--s3) 0 var(--s2);padding:var(--s2) var(--s4);display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 var(--s5);background:var(--card);border:1px solid var(--border);border-radius:var(--r-md)}
1112
1131
  #run .run-facts>div{display:grid;grid-template-columns:84px minmax(0,1fr);align-items:center;gap:var(--s2);min-height:38px;padding:5px 0;border-bottom:1px solid var(--border)}
1113
1132
  #run .run-facts>div.wide{grid-column:1/-1}
1114
1133
  #run .run-facts>div:nth-last-child(-n+1){border-bottom:0}
1115
- #run .run-facts dt{font:600 11px/1.2 var(--font-sans);color:var(--muted);text-transform:uppercase;letter-spacing:.05em}
1134
+ #run .run-facts dt{font:500 12px/1.3 var(--font-sans);color:var(--muted)}
1116
1135
  #run .run-facts dd{margin:0;display:flex;flex-wrap:wrap;gap:6px;align-items:center;min-width:0}
1117
1136
  #run .run-facts code{margin:0}
1118
1137
  #run>p.meta{margin:0 0 var(--s2);font-size:12.5px;color:var(--muted)}
1119
1138
  #run .run-facts code.file{display:inline-flex;align-items:center;gap:5px;color:var(--muted)}
1120
- #run .tm-tbl .tm-d{display:inline-block;width:80px;height:6px;margin-right:var(--s2);vertical-align:middle;border-radius:3px;background:var(--well);overflow:hidden;text-align:left}
1121
- #run .tm-tbl .tm-bar{display:block;height:100%;border-radius:3px;background:var(--muted)}
1139
+ #run .tm-tbl .tm-d{display:inline-block;width:80px;height:6px;margin-right:var(--s2);vertical-align:middle;border-radius:var(--r-cell);background:var(--well);overflow:hidden;text-align:left}
1140
+ #run .tm-tbl .tm-bar{display:block;height:100%;border-radius:var(--r-cell);background:var(--muted)}
1122
1141
  #run .tm-tbl .tm-bar.desktop{background:var(--vp-desktop)}
1123
1142
  #run .tm-tbl .tm-bar.mobile{background:var(--vp-mobile)}
1124
1143
  #run .tm-s{display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap}
@@ -1129,8 +1148,8 @@ const SCOPE = `/* Scope (Coverage, Inventory, Run). Selectors are scoped by sect
1129
1148
  #run .run-facts>div:nth-last-child(2).wide{border-bottom:1px solid var(--border)}
1130
1149
  }
1131
1150
  @media print{
1132
- #run .run-facts{box-shadow:none;break-inside:avoid}
1133
- #inventory .inv-line{box-shadow:none;break-inside:avoid}
1151
+ #run .run-facts{break-inside:avoid}
1152
+ #inventory .inv-line{break-inside:avoid}
1134
1153
  }
1135
1154
  /* tables keep their shape and scroll inside .tscroll on a phone */
1136
1155
  #coverage .cv-tbl{min-width:640px}
@@ -1163,7 +1182,7 @@ const SCOPE = `/* Scope (Coverage, Inventory, Run). Selectors are scoped by sect
1163
1182
  #run .spark circle{fill:var(--sev);stroke:var(--card);stroke-width:1}
1164
1183
  #run .spark circle.hollow{fill:var(--card);stroke:var(--sev);stroke-width:1.5}
1165
1184
  #run .spark-legend{display:flex;flex-wrap:wrap;gap:4px 12px}
1166
- #run .spark-legend i{display:inline-block;width:10px;height:3px;background:var(--sev);margin-right:5px;vertical-align:middle;border-radius:2px}
1185
+ #run .spark-legend i{display:inline-block;width:10px;height:3px;background:var(--sev);margin-right:5px;vertical-align:middle;border-radius:var(--r-cell)}
1167
1186
  #run .tr-tbl{min-width:420px}
1168
1187
  #run .tr-tbl th[scope=row]{white-space:nowrap}
1169
1188
  #run .tr-h{margin-top:var(--s2)}
package/src/plan-html.js CHANGED
@@ -18,12 +18,12 @@ const PLAN_CSS = `
18
18
  .top-end{margin-left:auto;display:flex;align-items:center;gap:var(--s2)}
19
19
  .byline{display:inline-flex;align-items:center;gap:var(--s2);font-size:12px;color:var(--muted)}
20
20
  .logo{display:inline-flex}
21
- svg.logo{display:inline-block;flex:none;color:var(--fg)}
21
+ svg.logo{display:inline-block;flex:none;color:var(--fg-strong)}
22
22
  .logo .l-dark{display:none}
23
23
  html[data-theme="dark"] .logo .l-light{display:none}
24
24
  html[data-theme="dark"] .logo .l-dark{display:inline}
25
25
  @media (prefers-color-scheme:dark){html:not([data-theme="light"]) .logo .l-light{display:none}html:not([data-theme="light"]) .logo .l-dark{display:inline}}
26
- .eyebrow{font:600 11px/1.4 var(--font-sans);text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}
26
+ .eyebrow{font:500 12px/1.4 var(--font-sans);color:var(--muted)}
27
27
  .plan{max-width:1100px;margin:0 auto;padding:var(--s5) var(--s6) var(--s7)}
28
28
  .plan-title{display:grid;gap:var(--s2);margin-bottom:var(--s5)}
29
29
  .plan-title .chips{margin-top:var(--s1)}
@@ -39,8 +39,8 @@ html[data-theme="dark"] .logo .l-dark{display:inline}
39
39
  .pl-trigger{font-size:12px;color:var(--muted);margin-bottom:10px}
40
40
  .pl-trigger b{font-weight:500;color:var(--fg)}
41
41
  .chip .plat{display:inline-flex;align-items:center;gap:5px}
42
- .chip.on{--k:var(--accent);--k-fg:var(--accent-fg);background:var(--accent-bg)}
43
- .chip.req{--k:var(--sev-warning);--k-fg:var(--sev-warning-fg);background:var(--sev-warning-bg)}
42
+ .chip.on{--k:var(--accent);--k-fg:var(--accent-fg)}
43
+ .chip.req{--k:var(--sev-warning);--k-fg:var(--sev-warning-fg)}
44
44
  .pl-card-body{padding:var(--s4)}
45
45
  .tbl td,.tbl th{vertical-align:top}
46
46
  /* a narrow table scrolls inside its .tscroll instead of breaking names mid-word */
package/src/whitelabel.js CHANGED
@@ -53,33 +53,66 @@ export const contrast = (a, b) => {
53
53
  return (h + 0.05) / (l + 0.05);
54
54
  };
55
55
  const MIN_TEXT_CONTRAST = 4.5;
56
- // The card surfaces of visual-system.md §1, which the link colour must read on.
57
- const LIGHT_CARD = "#ffffff";
58
- const DARK_CARD = "#141927";
56
+ const MIN_MARK_CONTRAST = 3;
57
+ // The grounds of visual-system.md §1 the accent is drawn on, in each theme: card (Tally's surface), page (bg), card-2
58
+ // (surface-2: hovered and selected rows, hovered controls) and well. Links sit on all four; the tint is added per colour.
59
+ const GROUNDS = {
60
+ light: ["#ffffff", "#f4f6fa", "#ecf1f8", "#e9eef5"],
61
+ dark: ["#0e1e34", "#071529", "#15263f", "#040e1f"],
62
+ };
63
+ const BLACK = [0, 0, 0];
64
+ const WHITE = [255, 255, 255];
65
+ // The dark tint is the brand colour laid on the navy card, as the default --accent-bg is.
66
+ const DARK_CARD_RGB = rgb(GROUNDS.dark[0]);
59
67
 
60
- /** The colour mixed step by step towards `toward` until it reads at 4.5:1 on `surface`; the input when it already does. */
61
- function readable(hex, toward, surface) {
68
+ /**
69
+ * The colour mixed step by step towards `toward` until it reads at `min`:1 on every ground; the input when it already
70
+ * does. Mixing towards black or white keeps the hue, so a brand colour only gets darker or lighter, never another colour.
71
+ */
72
+ function readable(hex, toward, grounds, min) {
62
73
  let c = hex;
63
- for (let i = 0; i < 20 && contrast(c, surface) < MIN_TEXT_CONTRAST; i++) c = mix(c, toward, 0.1);
74
+ while (grounds.some((g) => contrast(c, g) < min)) {
75
+ const next = mix(c, toward, 0.1);
76
+ if (next === c) break; // as far as rounding goes: black and white pass every ground long before this
77
+ c = next;
78
+ }
64
79
  return c;
65
80
  }
66
81
 
67
82
  /**
68
- * CSS that recolors the report's accent: the active tab rule, focus rings, links and the step plates (visual-system.md §9).
69
- * The light theme gets a link colour darkened until it reads on a white card, the dark theme one lightened until it
70
- * reads on the dark card, so a pale brand colour never gives unreadable links. The dark set is written under both dark
71
- * selectors the report uses, so a forced light theme on a dark OS keeps the light accent.
83
+ * The three accent tokens for a brand colour, per theme. The tint (--accent-bg) is the brand colour on the card: 10% on
84
+ * white in light, 25% on the navy in dark. The link colour (--accent-fg) is darkened (light) or lightened (dark) until it
85
+ * reads at 4.5:1 on the card, the page, card-2, the well and that tint; the colour for marks (--accent: the active tab
86
+ * and rail rule, focus rings, pressed filter edges) until it reaches 3:1 on the same grounds. A colour that already
87
+ * passes is left as it is.
88
+ * @returns {{ light: { accent: string, fg: string, bg: string }, dark: { accent: string, fg: string, bg: string } }}
89
+ */
90
+ export function brandTokens(color) {
91
+ const lightBg = mix(color, WHITE, 0.9);
92
+ const darkBg = mix(color, DARK_CARD_RGB, 0.75);
93
+ const light = [...GROUNDS.light, lightBg];
94
+ const dark = [...GROUNDS.dark, darkBg];
95
+ return {
96
+ light: { accent: readable(color, BLACK, light, MIN_MARK_CONTRAST), fg: readable(mix(color, BLACK, 0.2), BLACK, light, MIN_TEXT_CONTRAST), bg: lightBg },
97
+ dark: { accent: readable(mix(color, WHITE, 0.2), WHITE, dark, MIN_MARK_CONTRAST), fg: readable(mix(color, WHITE, 0.5), WHITE, dark, MIN_TEXT_CONTRAST), bg: darkBg },
98
+ };
99
+ }
100
+
101
+ /**
102
+ * CSS that recolors the report's accent: the active tab rule, focus rings, links and the step plates (visual-system.md §9),
103
+ * from brandTokens(), so a pale or a very dark brand colour never gives unreadable links or invisible focus rings in either
104
+ * theme. The dark set is written under both dark selectors the report uses, so a forced light theme on a dark OS keeps
105
+ * the light accent.
72
106
  */
73
107
  export function brandCss(color) {
74
108
  if (!color) return "";
75
- const black = [0, 0, 0], white = [255, 255, 255];
76
- const light = `--accent:${color};--accent-fg:${readable(mix(color, black, 0.2), black, LIGHT_CARD)};--accent-bg:${mix(color, white, 0.9)}`;
77
- const dark = `--accent:${mix(color, white, 0.2)};--accent-fg:${readable(mix(color, white, 0.5), white, DARK_CARD)};--accent-bg:${mix(color, [10, 14, 26], 0.75)}`;
78
- return `:root{${light}}@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){${dark}}}:root[data-theme="dark"]{${dark}}`;
109
+ const { light, dark } = brandTokens(color);
110
+ const set = (t) => `--accent:${t.accent};--accent-fg:${t.fg};--accent-bg:${t.bg}`;
111
+ return `:root{${set(light)}}@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){${set(dark)}}}:root[data-theme="dark"]{${set(dark)}}`;
79
112
  }
80
113
 
81
114
  // A forwarded PDF loses its file name, so every page carries the document title and the page number (Chromium fills the spans).
82
- const PDF_FOOTER = '<div style="width:100%;font:9px system-ui,sans-serif;color:#4b5567;padding:0 10mm;display:flex;justify-content:space-between"><span class="title"></span><span><span class="pageNumber"></span> / <span class="totalPages"></span></span></div>';
115
+ const PDF_FOOTER = '<div style="width:100%;font:9px \'Google Sans\',system-ui,sans-serif;color:#566479;padding:0 10mm;display:flex;justify-content:space-between"><span class="title"></span><span><span class="pageNumber"></span> / <span class="totalPages"></span></span></div>';
83
116
 
84
117
  /** Prints an HTML report to a PDF with Chromium. The print styles stack every tab and open every details, so the PDF holds the whole report. */
85
118
  export async function writePdf(htmlPath, pdfPath) {