diff --git a/JobTrackerApi.Tests/CvBuilderTests.cs b/JobTrackerApi.Tests/CvBuilderTests.cs
index f5cd432..4ada67b 100644
--- a/JobTrackerApi.Tests/CvBuilderTests.cs
+++ b/JobTrackerApi.Tests/CvBuilderTests.cs
@@ -210,6 +210,36 @@ public sealed class CvBuilderTests
Assert.DoesNotContain("transform:scale", html);
}
+ [Fact]
+ public void Header_band_and_sidebar_contact_text_own_their_contrasting_palette()
+ {
+ var renderer = new ThemedCvRenderer();
+ var model = CvVariantResolver.Build(Rich(), new CvVariantSettings(), "F", null);
+ var modern = renderer.Render(model, CvThemeCatalog.Resolve("modern"), new CvVariantSettings { ThemeId = "modern" }).Html;
+ var lightAccent = renderer.Render(model, CvThemeCatalog.Resolve("modern"), new CvVariantSettings { ThemeId = "modern", AccentColor = "#f8fafc" }).Html;
+ var technicalTheme = CvThemeCatalog.Resolve("technical");
+ var technical = renderer.Render(model, technicalTheme, new CvVariantSettings { ThemeId = "technical" }).Html;
+
+ Assert.Contains(".header-band .name,.header-band .headline,.header-band .contact,.header-band .contact-item,.header-band a{color:#fff;}", modern);
+ Assert.Contains(".header-band .name,.header-band .headline,.header-band .contact,.header-band .contact-item,.header-band a{color:#000;}", lightAccent);
+ Assert.Contains($".sidebar .contact,.sidebar .headline,.sidebar .entry-meta,.sidebar .entry-subtitle{{color:{technicalTheme.SidebarInk};}}", technical);
+ }
+
+ [Fact]
+ public void Variant_settings_reject_css_injection_in_visual_overrides()
+ {
+ var settings = CvVariantSettingsJson.Normalize(new CvVariantSettings
+ {
+ AccentColor = "#fff;} ",
+ HeadingFont = "Arial;}",
+ BodyFont = "'Segoe UI', Roboto, Arial, sans-serif",
+ });
+
+ Assert.Null(settings.AccentColor);
+ Assert.Null(settings.HeadingFont);
+ Assert.Equal("'Segoe UI', Roboto, Arial, sans-serif", settings.BodyFont);
+ }
+
[Fact]
public void Accent_override_reaches_the_css()
{
diff --git a/JobTrackerApi/Models/CvVariantSettings.cs b/JobTrackerApi/Models/CvVariantSettings.cs
index 0e7999b..005adda 100644
--- a/JobTrackerApi/Models/CvVariantSettings.cs
+++ b/JobTrackerApi/Models/CvVariantSettings.cs
@@ -63,6 +63,16 @@ public sealed class CvCustomSectionSetting
public static class CvVariantSettingsJson
{
+ private static readonly HashSet AllowedFonts = new(StringComparer.Ordinal)
+ {
+ "'Segoe UI', Roboto, Arial, sans-serif",
+ "Arial, Helvetica, sans-serif",
+ "Georgia, 'Times New Roman', serif",
+ "'Helvetica Neue', Arial, sans-serif",
+ "'Roboto', Arial, sans-serif",
+ "'Poppins', 'Segoe UI', Arial, sans-serif",
+ };
+
private static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web)
{
PropertyNameCaseInsensitive = true,
@@ -83,9 +93,28 @@ public static class CvVariantSettingsJson
{
s ??= new CvVariantSettings();
s.ThemeId = string.IsNullOrWhiteSpace(s.ThemeId) ? "modern" : s.ThemeId.Trim().ToLowerInvariant();
+ s.AccentColor = NormalizeColor(s.AccentColor);
+ s.HeadingFont = NormalizeFont(s.HeadingFont);
+ s.BodyFont = NormalizeFont(s.BodyFont);
s.Sections ??= new();
s.Overrides ??= new();
s.CustomSections ??= new();
return s;
}
+
+ private static string? NormalizeColor(string? value)
+ {
+ var candidate = value?.Trim();
+ return candidate is { Length: 7 }
+ && candidate[0] == '#'
+ && candidate.Skip(1).All(Uri.IsHexDigit)
+ ? candidate.ToLowerInvariant()
+ : null;
+ }
+
+ private static string? NormalizeFont(string? value)
+ {
+ var candidate = value?.Trim();
+ return candidate is not null && AllowedFonts.Contains(candidate) ? candidate : null;
+ }
}
diff --git a/JobTrackerApi/Services/ThemedCvRenderer.cs b/JobTrackerApi/Services/ThemedCvRenderer.cs
index 1f00365..08c087f 100644
--- a/JobTrackerApi/Services/ThemedCvRenderer.cs
+++ b/JobTrackerApi/Services/ThemedCvRenderer.cs
@@ -22,6 +22,7 @@ public sealed class ThemedCvRenderer : IThemedCvRenderer
{
settings = CvVariantSettingsJson.Normalize(settings);
var accent = Override(settings.AccentColor, theme.Accent);
+ var headerInk = ContrastInk(accent);
var headingColor = theme.HeadingColor ?? accent;
var headingFont = Override(settings.HeadingFont, theme.HeadingFont);
var bodyFont = Override(settings.BodyFont, theme.BodyFont);
@@ -35,7 +36,7 @@ public sealed class ThemedCvRenderer : IThemedCvRenderer
? SplitColumns(model, theme, showIcons)
: (string.Empty, RenderSections(model.Sections, theme));
- var css = BuildCss(theme, accent, headingColor, headingFont, bodyFont, density, pageDims, twoColumn);
+ var css = BuildCss(theme, accent, headerInk, headingColor, headingFont, bodyFont, density, pageDims, twoColumn);
var header = RenderHeader(model, theme, showIcons, twoColumn);
var body = theme.Layout switch
{
@@ -189,7 +190,7 @@ public sealed class ThemedCvRenderer : IThemedCvRenderer
return s;
}
- private static string BuildCss(CvTheme t, string accent, string headingColor, string headingFont, string bodyFont, double density, (string w, string h) page, bool twoColumn)
+ private static string BuildCss(CvTheme t, string accent, string headerInk, string headingColor, string headingFont, string bodyFont, double density, (string w, string h) page, bool twoColumn)
{
var margin = F(t.PageMarginMm * density);
var sectionGap = F(t.SectionGapMm * density);
@@ -209,13 +210,14 @@ public sealed class ThemedCvRenderer : IThemedCvRenderer
.sidebar{{background:{t.SidebarBg};color:{t.SidebarInk};padding:{margin}mm;}}
.sidebar .section-title{{color:{t.SidebarInk};border-color:rgba(255,255,255,.35);}}
.sidebar .tag{{border-color:rgba(255,255,255,.4);}}
+.sidebar .contact,.sidebar .headline,.sidebar .entry-meta,.sidebar .entry-subtitle{{color:{t.SidebarInk};}}
.sidebar a{{color:inherit;}}
.main{{padding:{margin}mm;}}
.hero .name{{color:{t.SidebarInk};}}"
: $@".main{{padding:0 {margin}mm {margin}mm {margin}mm;}}
.header{{padding:{margin}mm {margin}mm {F(t.SectionGapMm * density)}mm {margin}mm;display:flex;gap:6mm;align-items:center;}}
-.header-band{{background:{accent};color:#fff;}}
-.header-band .name,.header-band .headline,.header-band a{{color:#fff;}}
+.header-band{{background:{accent};color:{headerInk};}}
+.header-band .name,.header-band .headline,.header-band .contact,.header-band .contact-item,.header-band a{{color:{headerInk};}}
.header-centered{{flex-direction:column;text-align:center;justify-content:center;}}
.header-centered .contact{{justify-content:center;}}
.header-plain{{border-bottom:2px solid {accent};}}";
@@ -293,6 +295,21 @@ h1,h2{{font-family:{headingFont};}}
$@"";
private static string Override(string? value, string fallback) => string.IsNullOrWhiteSpace(value) ? fallback : value.Trim();
+
+ private static string ContrastInk(string background)
+ {
+ if (background.Length != 7 || background[0] != '#' || !background.Skip(1).All(Uri.IsHexDigit)) return "#000";
+
+ var r = Convert.ToInt32(background.Substring(1, 2), 16) / 255d;
+ var g = Convert.ToInt32(background.Substring(3, 2), 16) / 255d;
+ var b = Convert.ToInt32(background.Substring(5, 2), 16) / 255d;
+ static double Channel(double value) => value <= 0.04045 ? value / 12.92 : Math.Pow((value + 0.055) / 1.055, 2.4);
+ var luminance = 0.2126 * Channel(r) + 0.7152 * Channel(g) + 0.0722 * Channel(b);
+ var whiteContrast = 1.05 / (luminance + 0.05);
+ var blackContrast = (luminance + 0.05) / 0.05;
+ return whiteContrast >= blackContrast ? "#fff" : "#000";
+ }
+
private static string F(double v) => v.ToString("0.##", CultureInfo.InvariantCulture);
private static string Enc(string? v) => WebUtility.HtmlEncode(v ?? string.Empty);
private static string Attr(string? v) => WebUtility.HtmlEncode(v ?? string.Empty).Replace("'", "'", StringComparison.Ordinal);
diff --git a/docs/audits/verification-log.md b/docs/audits/verification-log.md
index 02b2950..d20251d 100644
--- a/docs/audits/verification-log.md
+++ b/docs/audits/verification-log.md
@@ -199,3 +199,4 @@ Output was reduced to filenames and commit counts. The token artifact appears un
| V-165 | CV renderer/resolver/template tests; Builder helper/editor/list Jest; optimized frontend build; real headless Chromium DOM/PDF probe; diff check | Repository root / `job-tracker-ui` | Verify professional multi-page CV rendering, physical preview metrics, custom-section ordering and stored-output safety under pathological content | PASS — backend 25/25; frontend 21/21; build passes. A 14-role/75-skill fixture with oversized name/email/URL produced zero horizontal offenders and a 9-page 173,196-byte PDF with extractable final-page text. Custom sections share persisted order; partial pages use ceiling count; stale preview/save/export races are gated | Synthetic data and local Chromium only; authenticated 375/768/1440 app journey, real private CV, production browser binary and DOCX remain unverified | Renderer/PDF repository scope verified; application-browser and production gates remain |
| V-166 | Focused/full backend and frontend tests; optimized frontend build/TypeScript; diff review | Repository root / `job-tracker-ui` | Verify an administrator can identify the deployed application version without exposing build metadata in the normal-user UI/API bootstrap | PASS — focused Auth/System 36/36 and AppShell 2/2; backend 640/640; frontend 53 suites/221 tests; production build passes. Configured version/commit reaches admin `/api/auth/me`, normal users receive no configured metadata, and the responsive header badge is absent unless the admin-owned prop is supplied | Local/JSDOM evidence only; remote CI and deployed version comparison remain. Jest retains the documented force-exit/open-handle notice | Priority repository increment verified; ship proof remains |
| V-167 | Failing Career round-trip reproduction; affected/full backend; focused Career/Profile Jest; persistence-consumer and diff review | Repository root / `job-tracker-ui` | Preserve every reviewed Career text value across save/get/version/projection/import use without weakening extraction cleanup or write bounds | PASS — pre-fix location became `Oslo, Norway and` and the test failed; post-fix affected backend 112/112, backend 642/642 and Career/Profile UI 17/17 pass. Website path/query, remote location, free-form date, custom language and incomplete WIP entry round-trip; oversized website is rejected explicitly | Local SQLite/JSDOM only; no real private CV/model/provider/production data. One initial Jest command used nonexistent paths and was corrected; the correct files passed | Reviewed/extracted normalization boundary verified locally |
+| V-168 | Failing renderer contrast test; focused/full backend; CV Builder/public Jest; real Chromium computed-style/overflow probe; pathological A4 PDF export and text inspection | Repository root / `job-tracker-ui` | Make header/sidebar contact text readable for theme and custom palettes while keeping public renderer settings inert | PASS — renderer/settings 25/25, backend 644/644 and CV UI 22/22. Chromium computed white on Modern blue, black on `#f8fafc`, white on Technical sidebar, with zero element overflow. A 14-role/75-skill fixture produced a 17-page 259,447-byte A4 PDF with 1,685 final-page characters. CSS-like accent/font payloads normalize to null | Synthetic local data/browser only; authenticated application journey and production browser binary remain unverified. A direct PowerShell assembly probe failed to load dependencies before the compiled temporary test probe passed; temporary proof cleanup was blocked by execution policy | Renderer contrast/public-setting boundary verified locally |
diff --git a/docs/verification/career-002-cv-builder.md b/docs/verification/career-002-cv-builder.md
index 0d0701a..953bb26 100644
--- a/docs/verification/career-002-cv-builder.md
+++ b/docs/verification/career-002-cv-builder.md
@@ -31,6 +31,8 @@ Status: `IMPLEMENTED — NOT VERIFIED`. Repository, automated and pathological C
- Autosaves are serialized; stale preview responses are ignored; export/public actions save pending edits first.
- CV deletion and version restore use the shared application dialog system.
- The non-functional page-number switch is no longer advertised; `ShowPageNumbers` remains a backward-compatible exporter extension point until the Chromium CLI path supports controlled PDF footers.
+- Header-band contact/headline text now owns a computed black-or-white foreground chosen for the stronger WCAG contrast against both theme and custom accents. Sidebar contact/headline text owns `SidebarInk` instead of inheriting the main-page muted colour.
+- Accent overrides are restricted to six-digit hex colours and font overrides to the six editor-supported stacks before CSS generation, preventing malformed/public settings from escaping the generated stylesheet.
## Save-integrity increment
@@ -51,10 +53,11 @@ Status: `IMPLEMENTED — NOT VERIFIED`. Repository, automated and pathological C
## Verification to date
- Focused Builder list/helper/deep-link/save/navigation: 3 suites, 21/21 tests; editor deep-link/interaction 9/9.
-- Focused renderer/template backend: 25/25 tests.
+- Focused renderer/template backend: 25/25 tests, including header/sidebar contrast ownership and visual-override sanitization.
- Full frontend: 49/49 suites, 184/184 tests.
- Production build/TypeScript and `git diff --check`: pass.
- Pathological Chromium render: 14 long roles, 75 long skills, oversized name/email/URL, zero horizontal-overflow elements, nine-page PDF (173,196 bytes) with extractable final-page content.
+- Contrast rerun in real Chromium: Modern default computed white on `rgb(37, 99, 235)`, light custom accent computed black on `rgb(248, 250, 252)`, Technical sidebar computed white on `rgb(15, 76, 92)`, and all three reported zero element overflow at a 1400px viewport. The harder 14-role/75-skill fixture exported a 17-page A4 PDF (259,447 bytes) with 1,685 extractable characters on the final page.
- Implementation commits: `b58cc19`, `a5b74e0`, `2043349` plus the V-165 checkpoint.
## Product research
diff --git a/docs/work-programmes/master-progress.md b/docs/work-programmes/master-progress.md
index ad6ef63..26257d0 100644
--- a/docs/work-programmes/master-progress.md
+++ b/docs/work-programmes/master-progress.md
@@ -10,9 +10,9 @@ Updated: 2026-08-15
- **Production-verified work:** None.
- **Blocked work:** SEC-006 parser upgrades remain outside the scoped frontend advisory permission; PROD-001/003/004 and REL-001 require documented production access and unfinished dependencies. Real provider, SMTP/MariaDB and production environments are unavailable; DEP-001 awaits approved merge/live verification. The in-app browser is available for local UI checks.
- **Deferred work:** None. Conditional multi-replica coordination, model deletion, realtime operation delivery and unrelated production changes remain outside current packages.
-- **Immediate order:** CV contact contrast; JOBS-002 parity/regression; cross-app contrast/accessibility; PRODUCT-001; VER-001. The admin version indicator is pushed in `a6cffe0`; Career lossless manual persistence is locally complete. External-only work remains skipped, not allowed to stall this queue.
+- **Immediate order:** JOBS-002 parity/regression; cross-app contrast/accessibility; PRODUCT-001; VER-001. Admin version (`a6cffe0`), Career lossless persistence (`f0b9b22`) and CV contact contrast are locally complete. External-only work remains skipped, not allowed to stall this queue.
- **Status counts:** 7 `VERIFIED LOCALLY`; 22 `IMPLEMENTED — NOT VERIFIED`; 1 `IN PROGRESS`; 4 `NOT STARTED`; 5 `BLOCKED`; 0 `DONE`; 0 `DEFERRED`.
-- **Test status:** backend 642/642; affected Career/import/profile/job paths 112/112; Career/Profile UI 17/17; prior frontend 53/53 suites and 221/221 tests plus optimized production build/TypeScript pass. The version indicator also has focused Auth/System 36/36 and AppShell 2/2 evidence. Prior AI sidecar 22/22, Playwright 6/6, pathological nine-page Chromium/PDF proof and npm audit 0 evidence remain current. Historical JT-019 and Jest force-exit/open-handle behavior remain recorded.
+- **Test status:** backend 644/644; CV renderer/settings 25/25; CV Builder/public UI 22/22; affected Career/import/profile/job paths 112/112; Career/Profile UI 17/17; prior frontend 53/53 suites and 221/221 tests plus optimized production build/TypeScript pass. Real Chromium contrast/overflow checks pass and the harder pathological fixture exports a 17-page A4 PDF with final-page text. Prior AI sidecar 22/22, Playwright 6/6 and npm audit 0 evidence remain current. Historical JT-019 and Jest force-exit/open-handle behavior remain recorded.
- **Deployment status:** Gitea pull-request run 609 passed the complete CI job in 4m20s. Deploy was intentionally skipped because the workflow deploys only a `push` to `main`; live remains unchanged. No merge/deployment was performed directly, no production migrations were run and the AI operation worker remains disabled by default.
- **Production status:** Unchanged and unverified. No provider/model call, model pull, external request or paid API occurred.
- **Known regressions:** None found by automated/local browser checks. Jest still needs `--forceExit` and reports its existing open-handle notice. Email-provider/send tests are fake/local only; real delivery is not claimed. Current MAIL browser evidence is 1280×720 only because the browser surface could not resize or perform native Tab traversal. Interrupted attempts are aged after 15 minutes and notified without retry; the five-minute scan is unmeasured on a large ledger. Direct clean EF-only SQLite migration still hits the pre-existing historical blank-chain defect before later migrations; normal startup owns reconciliation. Cross-feature monthly AI usage accounting remains a rollout gap.
diff --git a/docs/work-programmes/master-work-plan.md b/docs/work-programmes/master-work-plan.md
index 5257fb7..f763e07 100644
--- a/docs/work-programmes/master-work-plan.md
+++ b/docs/work-programmes/master-work-plan.md
@@ -56,8 +56,8 @@ This queue records the highest-value work that can proceed without production cr
|---:|---|---|---|
| 1 | Admin-only deployed-version indicator in the application header | DEP-001, VER-001 | Implemented with authenticated API and shell tests. The badge shows the CI deployment version and exposes the commit SHA in its accessible label/tooltip only for administrators; full regression, remote CI and deployment smoke remain. |
| 2 | Lossless Career field persistence | CAREER-001 | Implemented and locally verified. Manual website/location/contact/date/language values now use a reviewed-data persistence boundary; extraction heuristics remain isolated to extraction. Full remote/production smoke remains. |
-| 3 | CV contact/header/sidebar contrast correction | CAREER-002 | Next implementable correction. Apply renderer-scoped theme ownership for contact/headline text and rerun pathological HTML/PDF proof without shrinking typography. |
-| 4 | Dedicated Job Details parity and JOBS-002 closure | JOBS-002 | In progress. Finish any remaining legacy follow-up/application-package parity, dirty-edit behavior, tenant authorization and 375/768/1440 theme/keyboard/history/error/long-data verification. |
+| 3 | CV contact/header/sidebar contrast correction | CAREER-002 | Implemented and locally verified. Header/custom-accent and sidebar palettes own readable foregrounds; real Chromium computed-style/overflow checks and a 17-page A4 PDF proof pass. |
+| 4 | Dedicated Job Details parity and JOBS-002 closure | JOBS-002 | Next implementable package and in progress. Finish any remaining legacy follow-up/application-package parity, dirty-edit behavior, tenant authorization and 375/768/1440 theme/keyboard/history/error/long-data verification. |
| 5 | Cross-application contrast/accessibility pass | UX-002, UX-003, VER-001 | Queued after the scoped Career/CV corrections. Audit semantic alerts, secondary text, focus, loading/empty/error states and remaining hardcoded colors before documenting larger redesigns. |
| 6 | Honest Free/Pro homepage and upgrade surfaces | PRODUCT-001 | Not started. Inventory existing claims first; do not invent pricing, limits or trial terms before billing configuration is real. |
| 7 | Complete application action matrix and full regression | VER-001 | Not started. Populate incrementally, then run the complete backend/frontend/sidecar/E2E gates and accurately classify external production/provider checks. |
@@ -648,9 +648,9 @@ This queue records the highest-value work that can proceed without production cr
- **Required production verification:** existing variants/edit/export/public render smoke.
- **Status:** `IMPLEMENTED — NOT VERIFIED`.
- **Blocker:** authenticated JobTracker three-width/theme/keyboard checks and production synthetic-variant smoke require their runtime environments.
-- **Evidence:** `docs/verification/career-002-cv-builder.md`; V-120–V-125 and V-165. Builder 21/21 and renderer/templates 25/25 pass plus build. Pathological Chromium output has zero horizontal offenders and produces a readable nine-page PDF without text shrinking.
+- **Evidence:** `docs/verification/career-002-cv-builder.md`; V-120–V-125, V-165 and V-168. Builder/public UI 22/22 and renderer/templates 25/25 pass. Real Chromium confirms contrast ownership and zero overflow for default/light custom/sidebar palettes; a harder pathological fixture produces a readable 17-page A4 PDF with final-page text and no text shrinking.
- **Commit:** `b58cc19`, `a5b74e0`, `2043349` plus the V-165 checkpoint.
-- **Remaining work:** correct header/sidebar contact contrast, then complete authenticated application-browser/production gates and the honest deployed DOCX capability check. Public competitor-pattern research is complete; no authenticated competitor session is claimed.
+- **Remaining work:** complete authenticated application-browser/production gates and the honest deployed DOCX capability check. Public competitor-pattern research is complete; no authenticated competitor session is claimed.
### MAIL-001 — Consolidated job-email hub and explicit sending
diff --git a/docs/work-programmes/session-handoff.md b/docs/work-programmes/session-handoff.md
index d14873f..6e6127d 100644
--- a/docs/work-programmes/session-handoff.md
+++ b/docs/work-programmes/session-handoff.md
@@ -2,17 +2,17 @@
Updated: 2026-08-15
-- **Exact current task:** commit/push lossless Career manual-field persistence, then correct CV header/sidebar contact contrast before returning to JOBS-002 closure.
-- **Last completed step:** separated strict extraction normalization from lossless reviewed-profile persistence and routed Career save/version/restore/import-merge/legacy consumers through the correct boundary.
-- **Files currently modified:** structured profile persistence/validation, Career and CV/job projection consumers, Career/import regressions, architecture and work-programme evidence.
-- **Commands already run:** failing Career round-trip reproduction; affected backend 112/112; full backend 642/642; Career/Profile UI 17/17; diff check.
-- **Test results:** all corrected commands pass. One initial Jest invocation named two nonexistent test paths and returned “No tests found”; the correct Career/Profile files then passed 17/17. Jest retains the documented force-exit/open-handle notice.
+- **Exact current task:** commit/push the CV contrast/public-render hardening increment, then continue JOBS-002 parity and regression closure.
+- **Last completed step:** fixed header/sidebar contact contrast, added automatic custom-accent foreground selection, and restricted public renderer colour/font overrides to safe supported values.
+- **Files currently modified:** CV settings normalization, themed renderer, renderer regressions and CAREER-002/work-programme evidence.
+- **Commands already run:** failing renderer contrast reproduction; renderer/settings 25/25; full backend 644/644; CV Builder/public UI 22/22; real Chromium computed-style/overflow and 17-page PDF proof; diff check.
+- **Test results:** all repository tests listed above pass. Chromium computed expected white/dark/white foregrounds for Modern/default, Modern/light override and Technical/sidebar, with zero overflow; the 259,447-byte A4 PDF has 17 pages and extractable final-page text. Jest retains the documented force-exit/open-handle notice.
- **Services currently running:** none on task-owned ports 3000/5202. Playwright stopped its disposable API/Next servers. Pre-existing Docker services were not changed.
- **Temporary files or processes:** no task-owned process is running and the failed disposable migration database was removed. Existing synthetic browser evidence/account and startup-created local backup remain documented. No provider account, real email, private content, paid service or production service was accessed.
- **Production changes currently active:** none. No deployment, migration, provider connection/sync/send or production payload occurred.
- **Rollback status:** downgrade `20260810080858_AddEmailDraftClientRequestId`, then `20260810075206_AddEmailDrafts`, before reverting draft commits; then follow the existing MAIL rollback order (`ee5ef7e`, `449faeb`, `123fc55`/`e9937ac`, ledger downgrade before `653f011`). No production migration/deploy/provider grant occurred.
-- **Uncommitted changes:** V-167 Career reviewed-value persistence and tracking update; no dependency/schema/config/migration change. V-166 is committed/pushed as `a6cffe0`.
+- **Uncommitted changes:** V-168 CV contrast and renderer-setting hardening; no dependency/schema/config/migration change. V-166/V-167 are pushed as `a6cffe0`/`f0b9b22`.
- **Known failures:** live deployment is not verified because PR deploy is intentionally skipped and the active branch is not approved for merge. Draft export/API/UI, full thread/category actions and non-Gmail review remain; existing accounts need re-consent and IMAP stays read-only. A clean full-chain SQLite apply fails in the pre-existing JT-019 migration before the new draft migration. Browser/provider/MariaDB/production unavailable or unverified; recovery scan performance is unmeasured at large ledger scale; Jest open handles; SEC-006 parser dependency work is still separately gated; parser isolation remains SEC-007.
-- **Exact next action:** review, commit and push V-167; then fix CV contact/header/sidebar contrast with renderer and PDF regressions.
+- **Exact next action:** review, commit and push V-168; then trace the remaining legacy-vs-dedicated Job Details feature parity and close the highest-value JOBS-002 gap.
- **Work that can continue independently:** the immediate queue in the master plan: Career lossless persistence, CV contrast, JOBS-002 closure, cross-app contrast/accessibility, PRODUCT-001 and VER-001. UX/JOBS production, MAIL provider mutations, SEC-006/007 and PROD packages retain their recorded external gates.
- **Decisions still required from the user:** none for synthetic/code-inspected repository work. Any provider connection or send test, internet/package upgrades, private data, external/paid providers and production actions retain explicit approval/safety gates; SEC-009 retention/legal policy remains unresolved.