krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

9e58a36915bb1fe8cf88423c9519c0c12dbbf2e2

unsigned

author: Christian Cleberg <hello@cleberg.net> · 2026-07-21T01:25:01Z

fix(a11y): rebalance the light palette so hues survive

Reported as "colors seem muted and hard to see on light mode", and
correct. The light palette optimised contrast and produced mud: #7A5600
reads olive rather than amber, #146C2E bottle-dark rather than green.
Contrast passed while the UI got harder to read, because hue identity is
what distinguishes warning from critical at a glance.

Two causes, both fixed.

Every foreground was required to clear 4.5:1 against its own 16% badge
tint — the harshest surface it ever sits on — which pushed each colour
about 20% darker than the common case needed. Most of what is actually on
screen is plain text on a card, with far more headroom. The fill is now
decoupled from the foreground: AppStatusTone carries a foreground and a
surface authored independently, with matching …Surface colorsets, so a
foreground no longer has to survive a wash of itself. Every status
foreground is now fully saturated.

And warning was yellow. Yellow cannot stay yellow at a lightness low
enough to pass 4.5:1 on white — it becomes olive. That is colorimetric,
not a tuning problem. Warning is now orange: #AD5100 light, #FF9F0A dark.

New light values: positive #008035, warning #AD5100, critical #CC0700.
Worst-case ratios 4.54–6.76 across page, card, and surface in both
schemes. Audit findings are unchanged — light 21, dark 18 — so the
vividness costs nothing.

Also picks up a literal .blue missed in phase 1: DomainDiffItem's
low-severity change colour, which the phase 1 sweep did not cover because
its pattern listed only cyan/yellow/green/red/orange/pink.
 Docs/ACCESSIBILITY.md                              | 36 ++++++++--
 DomainDig/BatchResultsView.swift                   | 16 ++---
 DomainDig/ContentView.swift                        | 60 ++++++++---------
 DomainDig/DashboardView.swift                      | 24 +++----
 DomainDig/DomainDig/AuditViews.swift               | 20 +++---
 DomainDig/DomainDigUI.swift                        | 63 ++++++++++++++---
 DomainDig/HistoryView.swift                        |  2 +-
 DomainDig/MonitoringView.swift                     | 10 +--
 DomainDig/TimelineView.swift                       |  2 +-
 DomainDig/WatchlistView.swift                      |  8 +--
 .../StatusCritical.colorset/Contents.json          | 12 ++--
 .../StatusCriticalSurface.colorset/Contents.json   | 78 ++++++++++++++++++++++
 .../StatusInfoSurface.colorset/Contents.json       | 78 ++++++++++++++++++++++
 .../StatusNeutralSurface.colorset/Contents.json    | 78 ++++++++++++++++++++++
 .../StatusPositive.colorset/Contents.json          | 12 ++--
 .../StatusPositiveSurface.colorset/Contents.json   | 78 ++++++++++++++++++++++
 .../StatusWarning.colorset/Contents.json           | 14 ++--
 .../StatusWarningSurface.colorset/Contents.json    | 78 ++++++++++++++++++++++
 18 files changed, 566 insertions(+), 103 deletions(-)

diff --git a/Docs/ACCESSIBILITY.md b/Docs/ACCESSIBILITY.md
index 7153381..40acc1c 100644
--- a/Docs/ACCESSIBILITY.md
+++ b/Docs/ACCESSIBILITY.md
@@ -24,11 +24,37 @@ worst of those three is shown:
 
 | Role | Light | Dark | Worst light | Worst dark |
 | --- | --- | --- | --- | --- |
-| `StatusInfo` / `AccentColor` | `#0000FF` | `#4DA3FF` | 5.50 | 5.85 |
-| `StatusPositive` | `#146C2E` | `#30D158` | 4.65 | 7.32 |
-| `StatusWarning` | `#7A5600` | `#FFD60A` | 4.74 | 9.61 |
-| `StatusCritical` | `#B3261E` | `#FF6961` | 4.51 | 5.54 |
-| `StatusNeutral` | `#5A5A5F` | `#A1A1A6` | 4.92 | 5.88 |
+| `StatusInfo` / `AccentColor` | `#0000FF` | `#4DA3FF` | 6.76 | 6.47 |
+| `StatusPositive` | `#008035` | `#30D158` | 4.54 | 7.62 |
+| `StatusWarning` | `#AD5100` | `#FF9F0A` | 4.59 | 7.76 |
+| `StatusCritical` | `#CC0700` | `#FF6961` | 4.68 | 6.12 |
+| `StatusNeutral` | `#5A5A5F` | `#A1A1A6` | 5.84 | 6.76 |
+
+Each status foreground has a matching `…Surface` colour for the fill behind it,
+paired through `AppStatusTone`.
+
+### Contrast alone is not a palette
+
+The first version of this palette maximised contrast and produced mud. Requiring
+every foreground to clear 4.5:1 against *its own 16% tint* — the harshest
+surface it ever sits on — pushed each colour ~20% darker than the common case
+needed. `#7A5600` is not amber, it is olive; `#146C2E` is not green so much as
+bottle-dark. Contrast passed and the UI was still hard to read, because hue
+identity is what tells "warning" from "critical" at a glance.
+
+Two fixes:
+
+1. **Decouple the fill from the foreground.** `AppStatusTone` carries a
+   `foreground` and a `surface` that are authored independently, so the
+   foreground no longer has to survive a wash of itself. Every status foreground
+   is now fully saturated (`S = 1.0`).
+2. **Warning is orange, not yellow.** Yellow cannot stay yellow at a lightness
+   low enough to clear 4.5:1 on white — it *becomes* olive. That is
+   colorimetric, not a tuning problem. Orange holds its identity when darkened,
+   so warning is `#AD5100` in light and `#FF9F0A` in dark.
+
+When adding a colour, search for the most saturated value that passes, not the
+darkest. The darkest is always easy and always wrong.
 | `AppTextSecondary` | `#5A5A5F` | `#A1A1A6` | 6.15 | 7.50 |
 
 `AppTextSecondary` replaces `.secondary` for body text. iOS's own `secondaryLabel`
diff --git a/DomainDig/BatchResultsView.swift b/DomainDig/BatchResultsView.swift
index bf211dc..4b4f6fd 100644
--- a/DomainDig/BatchResultsView.swift
+++ b/DomainDig/BatchResultsView.swift
@@ -104,13 +104,13 @@ struct BatchResultRowView: View {
             if let changeClassification = result.changeClassification {
                 Text("Impact: \(changeClassification.title)")
                     .font(appDensity.font(.caption2))
-                    .foregroundStyle(changeClassification.color)
+                    .foregroundStyle(changeClassification.tone.foreground)
             }
 
             if let errorMessage = result.errorMessage {
                 Text(errorMessage)
                     .font(appDensity.font(.caption2))
-                    .foregroundStyle(result.status == .failed ? Color(.statusCritical) : .secondary)
+                    .foregroundStyle(result.status == .failed ? Color(.statusCritical) : Color(.appTextSecondary))
             }
         }
         .frame(maxWidth: .infinity, alignment: .leading)
@@ -147,20 +147,20 @@ struct BatchResultRowView: View {
         case .pending:
             return .init(title: "Pending", systemImage: "clock", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
         case .running:
-            return .init(title: "Running", systemImage: "arrow.clockwise", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfo).opacity(0.16))
+            return .init(title: "Running", systemImage: "arrow.clockwise", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfoSurface))
         case .completed:
             if result.changeClassification == .critical || result.certificateWarningLevel == .critical || result.riskLevel == .high {
-                return .init(title: "Critical", systemImage: "exclamationmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
+                return .init(title: "Critical", systemImage: "exclamationmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface))
             }
             if result.changeClassification == .warning || result.changeSeverity == .medium || result.certificateWarningLevel == .warning {
-                return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
+                return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
             }
             if result.quickStatus == "Changed" {
-                return .init(title: "Changed", systemImage: "arrow.triangle.2.circlepath", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfo).opacity(0.16))
+                return .init(title: "Changed", systemImage: "arrow.triangle.2.circlepath", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfoSurface))
             }
-            return .init(title: "Stable", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
+            return .init(title: "Stable", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
         case .failed:
-            return .init(title: "Failed", systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
+            return .init(title: "Failed", systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface))
         }
     }
 }
diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift
index efaaf05..47512c8 100644
--- a/DomainDig/ContentView.swift
+++ b/DomainDig/ContentView.swift
@@ -1029,21 +1029,21 @@ struct DomainChangeSummaryView: View {
             HStack {
                 Label(summary.hasChanges ? "Changed" : "Stable", systemImage: summary.hasChanges ? "arrow.triangle.2.circlepath" : "checkmark.circle")
                     .font(appDensity.font(.caption))
-                    .foregroundStyle(summary.hasChanges ? severityColor(summary.severity) : Color(.statusPositive))
+                    .foregroundStyle(summary.hasChanges ? severityTone(summary.severity).foreground : Color(.statusPositive))
                 Spacer()
                 Text(summary.severity.title.uppercased())
                     .font(appDensity.font(.caption2))
-                    .foregroundStyle(summary.hasChanges ? severityColor(summary.severity) : .secondary)
+                    .foregroundStyle(summary.hasChanges ? severityTone(summary.severity).foreground : Color(.appTextSecondary))
                     .padding(.horizontal, 8)
                     .padding(.vertical, 4)
-                    .background((summary.hasChanges ? severityColor(summary.severity) : .secondary).opacity(0.16))
+                    .background((summary.hasChanges ? severityTone(summary.severity) : AppStatusTone.neutral).surface)
                     .clipShape(Capsule())
                 Text(summary.impactClassification.title.uppercased())
                     .font(appDensity.font(.caption2))
-                    .foregroundStyle(summary.impactClassification.color)
+                    .foregroundStyle(summary.impactClassification.tone.foreground)
                     .padding(.horizontal, 8)
                     .padding(.vertical, 4)
-                    .background(summary.impactClassification.color.opacity(0.16))
+                    .background(summary.impactClassification.tone.surface)
                     .clipShape(Capsule())
                 Text(summary.generatedAt, style: .time)
                     .font(appDensity.font(.caption2))
@@ -1096,14 +1096,14 @@ struct DomainChangeSummaryView: View {
         }
     }
 
-    private func severityColor(_ severity: ChangeSeverity) -> Color {
+    private func severityTone(_ severity: ChangeSeverity) -> AppStatusTone {
         switch severity {
         case .low:
-            return .secondary
+            return .neutral
         case .medium:
-            return Color(.statusWarning)
+            return .warning
         case .high:
-            return Color(.statusCritical)
+            return .critical
         }
     }
 
@@ -1173,10 +1173,10 @@ struct DomainDiffView: View {
                                         Spacer()
                                         Text("\(item.changeType.marker) \(item.severity.title) • \(changeLabel(for: item.changeType))")
                                             .font(.system(.caption2, design: .monospaced))
-                                            .foregroundStyle(changeColor(for: item))
+                                            .foregroundStyle(changeTone(for: item).foreground)
                                             .padding(.horizontal, 8)
                                             .padding(.vertical, 4)
-                                            .background(changeColor(for: item).opacity(0.16))
+                                            .background(changeTone(for: item).surface)
                                             .clipShape(Capsule())
                                     }
 
@@ -1205,7 +1205,7 @@ struct DomainDiffView: View {
                                     }
                                 }
                                 .padding(10)
-                                .background(item.hasChanges ? changeColor(for: item).opacity(0.08) : Color(.appSurface))
+                                .background(item.hasChanges ? changeTone(for: item).surface : Color(.appSurface))
                                 .cornerRadius(8)
                             }
                         } label: {
@@ -1249,18 +1249,18 @@ struct DomainDiffView: View {
         }
     }
 
-    private func changeColor(for item: DomainDiffItem) -> Color {
+    private func changeTone(for item: DomainDiffItem) -> AppStatusTone {
         if item.changeType == .unchanged {
-            return .secondary
+            return .neutral
         }
 
         switch item.severity {
         case .low:
-            return .blue
+            return .info
         case .medium:
-            return Color(.statusWarning)
+            return .warning
         case .high:
-            return Color(.statusCritical)
+            return .critical
         }
     }
 
@@ -1337,7 +1337,7 @@ struct DomainSectionView: View {
         CollapsibleSectionView(title: "Domain", isCollapsed: $isCollapsed) {
             if let trackedDomain {
                 HStack(spacing: 8) {
-                    AppStatusBadgeView(model: .init(title: "Tracked", systemImage: "eye.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16)))
+                    AppStatusBadgeView(model: .init(title: "Tracked", systemImage: "eye.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface)))
                     Button {
                         onTogglePinned()
                     } label: {
@@ -2186,9 +2186,9 @@ struct EmailSectionView: View {
     private func emailRowBadge(_ row: EmailRowViewData) -> AppStatusBadgeModel {
         switch row.statusTone {
         case .success:
-            return .init(title: row.status, systemImage: "checkmark.shield.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
+            return .init(title: row.status, systemImage: "checkmark.shield.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
         case .warning:
-            return .init(title: row.status, systemImage: "shield.lefthalf.filled", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
+            return .init(title: row.status, systemImage: "shield.lefthalf.filled", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
         case .failure:
             return .init(title: row.status, systemImage: "minus.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
         case .primary, .secondary:
@@ -2345,11 +2345,11 @@ struct NetworkSectionView: View {
     private func reachabilityBadge(_ row: ReachabilityRowViewData) -> AppStatusBadgeModel {
         switch row.statusTone {
         case .success:
-            return .init(title: row.statusLabel, systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
+            return .init(title: row.statusLabel, systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
         case .warning:
-            return .init(title: row.statusLabel, systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
+            return .init(title: row.statusLabel, systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
         case .failure:
-            return .init(title: row.statusLabel, systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
+            return .init(title: row.statusLabel, systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface))
         case .primary, .secondary:
             return .init(title: row.statusLabel, systemImage: "circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
         }
@@ -2396,11 +2396,11 @@ struct PortRowsView: View {
     private func portBadge(_ row: PortScanRowViewData) -> AppStatusBadgeModel {
         switch row.statusTone {
         case .success:
-            return .init(title: row.statusLabel, systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
+            return .init(title: row.statusLabel, systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
         case .warning:
-            return .init(title: row.statusLabel, systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
+            return .init(title: row.statusLabel, systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
         case .failure:
-            return .init(title: row.statusLabel, systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
+            return .init(title: row.statusLabel, systemImage: "xmark.circle.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface))
         case .primary, .secondary:
             return .init(title: row.statusLabel, systemImage: "circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
         }
@@ -3754,11 +3754,11 @@ private struct DataImportPreviewSheet: View {
 }
 
 extension ChangeImpactClassification {
-    var color: Color {
+    var tone: AppStatusTone {
         switch self {
-        case .informational: return .secondary
-        case .warning: return Color(.statusWarning)
-        case .critical: return Color(.statusCritical)
+        case .informational: return .neutral
+        case .warning: return .warning
+        case .critical: return .critical
         }
     }
 }
diff --git a/DomainDig/DashboardView.swift b/DomainDig/DashboardView.swift
index 8d41ccb..16c4478 100644
--- a/DomainDig/DashboardView.swift
+++ b/DomainDig/DashboardView.swift
@@ -237,20 +237,20 @@ struct DashboardView: View {
         let criticalCount = states.filter { $0.health == .critical }.count
         let warningCount = states.filter { $0.health == .warning }.count
         let title: String
-        let color: Color
+        let tone: AppStatusTone
         let systemImage: String
 
         if criticalCount > 0 {
             title = "\(criticalCount) critical"
-            color = Color(.statusCritical)
+            tone = .critical
             systemImage = "exclamationmark.octagon.fill"
         } else if warningCount > 0 {
             title = "\(warningCount) warning"
-            color = Color(.statusWarning)
+            tone = .warning
             systemImage = "exclamationmark.triangle.fill"
         } else {
             title = "Healthy"
-            color = Color(.statusPositive)
+            tone = .positive
             systemImage = "checkmark.circle.fill"
         }
 
@@ -258,8 +258,8 @@ struct DashboardView: View {
             model: .init(
                 title: title,
                 systemImage: systemImage,
-                foregroundColor: color,
-                backgroundColor: color.opacity(0.16)
+                foregroundColor: tone.foreground,
+                backgroundColor: tone.surface
             )
         )
     }
@@ -331,11 +331,11 @@ private struct PortfolioAttentionRow: View {
     private var badgeModel: AppStatusBadgeModel {
         switch item.health {
         case .healthy:
-            return .init(title: "Healthy", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
+            return .init(title: "Healthy", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
         case .warning:
-            return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
+            return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
         case .critical:
-            return .init(title: "Critical", systemImage: "exclamationmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
+            return .init(title: "Critical", systemImage: "exclamationmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface))
         }
     }
 }
@@ -370,11 +370,11 @@ private struct PortfolioExpiryRow: View {
     private var badgeModel: AppStatusBadgeModel {
         switch state.certificateExpiryState {
         case .none:
-            return .init(title: "Healthy", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
+            return .init(title: "Healthy", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
         case .warning:
-            return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
+            return .init(title: "Warning", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
         case .critical:
-            return .init(title: "Critical", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
+            return .init(title: "Critical", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface))
         }
     }
 }
diff --git a/DomainDig/DomainDig/AuditViews.swift b/DomainDig/DomainDig/AuditViews.swift
index 50ea137..4e57837 100644
--- a/DomainDig/DomainDig/AuditViews.swift
+++ b/DomainDig/DomainDig/AuditViews.swift
@@ -526,33 +526,33 @@ private func auditStatusBadge(_ status: AuditStatus) -> some View {
     let model: AppStatusBadgeModel
     switch status {
     case .draft:
-        model = .init(title: "Draft", systemImage: "square.and.pencil", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
+        model = .init(title: "Draft", systemImage: "square.and.pencil", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
     case .inReview:
-        model = .init(title: "In Review", systemImage: "doc.text.magnifyingglass", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfo).opacity(0.16))
+        model = .init(title: "In Review", systemImage: "doc.text.magnifyingglass", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfoSurface))
     case .complete:
-        model = .init(title: "Complete", systemImage: "checkmark.seal.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
+        model = .init(title: "Complete", systemImage: "checkmark.seal.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
     }
     return AppStatusBadgeView(model: model)
 }
 
 private func findingSeverityBadge(_ severity: AuditFindingSeverity) -> some View {
-    let color: Color
+    let tone: AppStatusTone
     switch severity {
     case .informational:
-        color = .secondary
+        tone = .neutral
     case .low:
-        color = Color(.statusPositive)
+        tone = .positive
     case .medium:
-        color = Color(.statusWarning)
+        tone = .warning
     case .high:
-        color = Color(.statusCritical)
+        tone = .critical
     }
     return AppStatusBadgeView(
         model: .init(
             title: severity.title,
             systemImage: "exclamationmark.circle.fill",
-            foregroundColor: color,
-            backgroundColor: color.opacity(0.16)
+            foregroundColor: tone.foreground,
+            backgroundColor: tone.surface
         )
     )
 }
diff --git a/DomainDig/DomainDigUI.swift b/DomainDig/DomainDigUI.swift
index 7d99e4e..3aec259 100644
--- a/DomainDig/DomainDigUI.swift
+++ b/DomainDig/DomainDigUI.swift
@@ -120,6 +120,53 @@ extension EnvironmentValues {
     }
 }
 
+/// A status colour pairing: the foreground and the surface it sits on.
+///
+/// These travel together because they cannot be derived from one another. The
+/// badge fill used to be `foreground.opacity(0.16)`, which forced every
+/// foreground dark enough to stay legible against its own wash — that is how the
+/// light palette ended up olive-and-mud rather than amber-and-green. Decoupling
+/// them lets the foregrounds stay fully saturated.
+///
+/// See `Docs/ACCESSIBILITY.md` for the measured ratios.
+enum AppStatusTone {
+    case positive
+    case warning
+    case critical
+    case info
+    case neutral
+
+    var foreground: Color {
+        switch self {
+        case .positive:
+            return Color(.statusPositive)
+        case .warning:
+            return Color(.statusWarning)
+        case .critical:
+            return Color(.statusCritical)
+        case .info:
+            return Color(.statusInfo)
+        case .neutral:
+            return Color(.statusNeutral)
+        }
+    }
+
+    var surface: Color {
+        switch self {
+        case .positive:
+            return Color(.statusPositiveSurface)
+        case .warning:
+            return Color(.statusWarningSurface)
+        case .critical:
+            return Color(.statusCriticalSurface)
+        case .info:
+            return Color(.statusInfoSurface)
+        case .neutral:
+            return Color(.statusNeutralSurface)
+        }
+    }
+}
+
 struct AppStatusBadgeModel: Equatable {
     let title: String
     let systemImage: String?
@@ -131,9 +178,9 @@ enum AppStatusFactory {
     static func availability(_ status: DomainAvailabilityStatus?) -> AppStatusBadgeModel {
         switch status {
         case .available:
-            return .init(title: "Available", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
+            return .init(title: "Available", systemImage: "checkmark.circle.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
         case .registered:
-            return .init(title: "Registered", systemImage: "circle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
+            return .init(title: "Registered", systemImage: "circle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
         case .unknown, .none:
             return .init(title: "Unknown", systemImage: "questionmark.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
         }
@@ -141,12 +188,12 @@ enum AppStatusFactory {
 
     static func tls(sslInfo: SSLCertificateInfo?, error: String?) -> AppStatusBadgeModel {
         if error != nil || sslInfo == nil {
-            return .init(title: "Invalid", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
+            return .init(title: "Invalid", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface))
         }
         if let sslInfo, sslInfo.daysUntilExpiry <= 14 {
-            return .init(title: "Expiring", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
+            return .init(title: "Expiring", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
         }
-        return .init(title: "Valid", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
+        return .init(title: "Valid", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
     }
 
     static func email(_ result: EmailSecurityResult?, error: String?) -> AppStatusBadgeModel {
@@ -157,9 +204,9 @@ enum AppStatusFactory {
         let foundCount = [result.spf.found, result.dmarc.found, result.dkim.found].filter { $0 }.count
         switch foundCount {
         case 3:
-            return .init(title: "Secure", systemImage: "checkmark.shield.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
+            return .init(title: "Secure", systemImage: "checkmark.shield.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
         case 1, 2:
-            return .init(title: "Partial", systemImage: "shield.lefthalf.filled", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
+            return .init(title: "Partial", systemImage: "shield.lefthalf.filled", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
         default:
             return .init(title: "Missing", systemImage: "minus.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
         }
@@ -170,7 +217,7 @@ enum AppStatusFactory {
             return .init(title: "Unchanged", systemImage: "circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
         }
         if summary.hasChanges {
-            return .init(title: "Changed", systemImage: "arrow.triangle.2.circlepath", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfo).opacity(0.16))
+            return .init(title: "Changed", systemImage: "arrow.triangle.2.circlepath", foregroundColor: Color(.statusInfo), backgroundColor: Color(.statusInfoSurface))
         }
         return .init(title: "Unchanged", systemImage: "checkmark.circle", foregroundColor: Color(.appTextSecondary), backgroundColor: Color(.appSurfaceElevated))
     }
diff --git a/DomainDig/HistoryView.swift b/DomainDig/HistoryView.swift
index 5b788c3..f291c81 100644
--- a/DomainDig/HistoryView.swift
+++ b/DomainDig/HistoryView.swift
@@ -56,7 +56,7 @@ struct HistoryView: View {
                                                 title: severity.title,
                                                 systemImage: "arrow.triangle.2.circlepath",
                                                 foregroundColor: severity == .high ? Color(.statusCritical) : Color(.statusWarning),
-                                                backgroundColor: (severity == .high ? Color(.statusCritical) : Color(.statusWarning)).opacity(0.16)
+                                                backgroundColor: (severity == .high ? AppStatusTone.critical : .warning).surface
                                             )
                                         )
                                     }
diff --git a/DomainDig/MonitoringView.swift b/DomainDig/MonitoringView.swift
index db6f685..3863e47 100644
--- a/DomainDig/MonitoringView.swift
+++ b/DomainDig/MonitoringView.swift
@@ -78,10 +78,10 @@ struct MonitoringView: View {
                                 HStack(spacing: 8) {
                                     metricBadge(title: "\(log.domainsChecked) checked")
                                     if log.changesFound > 0 {
-                                        metricBadge(title: "\(log.changesFound) changed", tint: Color(.statusWarning))
+                                        metricBadge(title: "\(log.changesFound) changed", tone: .warning)
                                     }
                                     if log.alertsTriggered > 0 {
-                                        metricBadge(title: "\(log.alertsTriggered) alerts", tint: Color(.statusCritical))
+                                        metricBadge(title: "\(log.alertsTriggered) alerts", tone: .critical)
                                     }
                                 }
                             }
@@ -100,13 +100,13 @@ struct MonitoringView: View {
         }
     }
 
-    private func metricBadge(title: String, tint: Color = Color(.statusInfo)) -> some View {
+    private func metricBadge(title: String, tone: AppStatusTone = .info) -> some View {
         Text(title)
             .font(appDensity.font(.caption2))
-            .foregroundStyle(tint)
+            .foregroundStyle(tone.foreground)
             .padding(.horizontal, 8)
             .padding(.vertical, 4)
-            .background(tint.opacity(0.16))
+            .background(tone.surface)
             .clipShape(Capsule())
     }
 
diff --git a/DomainDig/TimelineView.swift b/DomainDig/TimelineView.swift
index 16e2bf8..5352a94 100644
--- a/DomainDig/TimelineView.swift
+++ b/DomainDig/TimelineView.swift
@@ -117,7 +117,7 @@ private struct TimelineRow: View {
                             title: severity.title,
                             systemImage: "arrow.triangle.2.circlepath",
                             foregroundColor: severity == .high ? Color(.statusCritical) : Color(.statusWarning),
-                            backgroundColor: (severity == .high ? Color(.statusCritical) : Color(.statusWarning)).opacity(0.16)
+                            backgroundColor: (severity == .high ? AppStatusTone.critical : .warning).surface
                         )
                     )
                 }
diff --git a/DomainDig/WatchlistView.swift b/DomainDig/WatchlistView.swift
index ddb6f4a..dda97ad 100644
--- a/DomainDig/WatchlistView.swift
+++ b/DomainDig/WatchlistView.swift
@@ -516,7 +516,7 @@ struct WatchlistRowView: View {
                         title: "Shared",
                         systemImage: "person.2.fill",
                         foregroundColor: Color(.statusInfo),
-                        backgroundColor: Color(.statusInfo).opacity(0.16)
+                        backgroundColor: Color(.statusInfoSurface)
                     )
                 )
             }
@@ -533,11 +533,11 @@ struct WatchlistRowView: View {
         let days = trackedDomain.certificateDaysRemaining.map { "\($0)d" } ?? "Soon"
         switch trackedDomain.certificateWarningLevel {
         case .critical:
-            return .init(title: "Invalid \(days)", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCritical).opacity(0.16))
+            return .init(title: "Invalid \(days)", systemImage: "xmark.octagon.fill", foregroundColor: Color(.statusCritical), backgroundColor: Color(.statusCriticalSurface))
         case .warning:
-            return .init(title: "Expiring \(days)", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarning).opacity(0.16))
+            return .init(title: "Expiring \(days)", systemImage: "exclamationmark.triangle.fill", foregroundColor: Color(.statusWarning), backgroundColor: Color(.statusWarningSurface))
         case .none:
-            return .init(title: "Valid", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositive).opacity(0.16))
+            return .init(title: "Valid", systemImage: "lock.fill", foregroundColor: Color(.statusPositive), backgroundColor: Color(.statusPositiveSurface))
         }
     }
 }
diff --git a/Shared/Colors.xcassets/StatusCritical.colorset/Contents.json b/Shared/Colors.xcassets/StatusCritical.colorset/Contents.json
index 8f97cd9..2963513 100644
--- a/Shared/Colors.xcassets/StatusCritical.colorset/Contents.json
+++ b/Shared/Colors.xcassets/StatusCritical.colorset/Contents.json
@@ -5,9 +5,9 @@
       "color": {
         "color-space": "srgb",
         "components": {
-          "red": "0xB3",
-          "green": "0x26",
-          "blue": "0x1E",
+          "red": "0xCC",
+          "green": "0x07",
+          "blue": "0x00",
           "alpha": "1.000"
         }
       }
@@ -41,9 +41,9 @@
       "color": {
         "color-space": "srgb",
         "components": {
-          "red": "0x8C",
-          "green": "0x1D",
-          "blue": "0x16",
+          "red": "0xA3",
+          "green": "0x05",
+          "blue": "0x00",
           "alpha": "1.000"
         }
       }
diff --git a/Shared/Colors.xcassets/StatusCriticalSurface.colorset/Contents.json b/Shared/Colors.xcassets/StatusCriticalSurface.colorset/Contents.json
new file mode 100644
index 0000000..31b3941
--- /dev/null
+++ b/Shared/Colors.xcassets/StatusCriticalSurface.colorset/Contents.json
@@ -0,0 +1,78 @@
+{
+  "colors": [
+    {
+      "idiom": "universal",
+      "color": {
+        "color-space": "srgb",
+        "components": {
+          "red": "0xFB",
+          "green": "0xE0",
+          "blue": "0xDF",
+          "alpha": "1.000"
+        }
+      }
+    },
+    {
+      "idiom": "universal",
+      "appearances": [
+        {
+          "appearance": "luminosity",
+          "value": "dark"
+        }
+      ],
+      "color": {
+        "color-space": "srgb",
+        "components": {
+          "red": "0x2E",
+          "green": "0x13",
+          "blue": "0x10",
+          "alpha": "1.000"
+        }
+      }
+    },
+    {
+      "idiom": "universal",
+      "appearances": [
+        {
+          "appearance": "contrast",
+          "value": "high"
+        }
+      ],
+      "color": {
+        "color-space": "srgb",
+        "components": {
+          "red": "0xF7",
+          "green": "0xCF",
+          "blue": "0xCD",
+          "alpha": "1.000"
+        }
+      }
+    },
+    {
+      "idiom": "universal",
+      "appearances": [
+        {
+          "appearance": "luminosity",
+          "value": "dark"
+        },
+        {
+          "appearance": "contrast",
+          "value": "high"
+        }
+      ],
+      "color": {
+        "color-space": "srgb",
+        "components": {
+          "red": "0x3D",
+          "green": "0x19",
+          "blue": "0x15",
+          "alpha": "1.000"
+        }
+      }
+    }
+  ],
+  "info": {
+    "author": "xcode",
+    "version": 1
+  }
+}
diff --git a/Shared/Colors.xcassets/StatusInfoSurface.colorset/Contents.json b/Shared/Colors.xcassets/StatusInfoSurface.colorset/Contents.json
new file mode 100644
index 0000000..e0a2c53
--- /dev/null
+++ b/Shared/Colors.xcassets/StatusInfoSurface.colorset/Contents.json
@@ -0,0 +1,78 @@
+{
+  "colors": [
+    {
+      "idiom": "universal",
+      "color": {
+        "color-space": "srgb",
+        "components": {
+          "red": "0xDF",
+          "green": "0xE3",
+          "blue": "0xFB",
+          "alpha": "1.000"
+        }
+      }
+    },
+    {
+      "idiom": "universal",
+      "appearances": [
+        {
+          "appearance": "luminosity",
+          "value": "dark"
+        }
+      ],
+      "color": {
+        "color-space": "srgb",
+        "components": {
+          "red": "0x10",
+          "green": "0x1C",
+          "blue": "0x33",
+          "alpha": "1.000"
+        }
+      }
+    },
+    {
+      "idiom": "universal",
+      "appearances": [
+        {
+          "appearance": "contrast",
+          "value": "high"
+        }
+      ],
+      "color": {
+        "color-space": "srgb",
+        "components": {
+          "red": "0xCB",
+          "green": "0xD2",
+          "blue": "0xF7",
+          "alpha": "1.000"
+        }
+      }
+    },
+    {
+      "idiom": "universal",
+      "appearances": [
+        {
+          "appearance": "luminosity",
+          "value": "dark"
+        },
+        {
+          "appearance": "contrast",
+          "value": "high"
+        }
+      ],
+      "color": {
+        "color-space": "srgb",
+        "components": {
+          "red": "0x15",
+          "green": "0x27",
+          "blue": "0x44",
+          "alpha": "1.000"
+        }
+      }
+    }
+  ],
+  "info": {
+    "author": "xcode",
+    "version": 1
+  }
+}
diff --git a/Shared/Colors.xcassets/StatusNeutralSurface.colorset/Contents.json b/Shared/Colors.xcassets/StatusNeutralSurface.colorset/Contents.json
new file mode 100644
index 0000000..8b8c6bd
--- /dev/null
+++ b/Shared/Colors.xcassets/StatusNeutralSurface.colorset/Contents.json
@@ -0,0 +1,78 @@
+{
+  "colors": [
+    {
+      "idiom": "universal",
+      "color": {
+        "color-space": "srgb",
+        "components": {
+          "red": "0xEB",
+          "green": "0xED",
+          "blue": "0xEF",
+          "alpha": "1.000"
+        }
+      }
+    },
+    {
+      "idiom": "universal",
+      "appearances": [
+        {
+          "appearance": "luminosity",
+          "value": "dark"
+        }
+      ],
+      "color": {
+        "color-space": "srgb",
+        "components": {
+          "red": "0x1A",
+          "green": "0x1A",
+          "blue": "0x1C",
+          "alpha": "1.000"
+        }
+      }
+    },
+    {
+      "idiom": "universal",
+      "appearances": [
+        {
+          "appearance": "contrast",
+          "value": "high"
+        }
+      ],
+      "color": {
+        "color-space": "srgb",
+        "components": {
+          "red": "0xDF",
+          "green": "0xE1",
+          "blue": "0xE4",
+          "alpha": "1.000"
+        }
+      }
+    },
+    {
+      "idiom": "universal",
+      "appearances": [
+        {
+          "appearance": "luminosity",
+          "value": "dark"
+        },
+        {
+          "appearance": "contrast",
+          "value": "high"
+        }
+      ],
+      "color": {
+        "color-space": "srgb",
+        "components": {
+          "red": "0x23",
+          "green": "0x23",
+          "blue": "0x26",
+          "alpha": "1.000"
+        }
+      }
+    }
+  ],
+  "info": {
+    "author": "xcode",
+    "version": 1
+  }
+}
diff --git a/Shared/Colors.xcassets/StatusPositive.colorset/Contents.json b/Shared/Colors.xcassets/StatusPositive.colorset/Contents.json
index 22ce5e5..30280d9 100644
--- a/Shared/Colors.xcassets/StatusPositive.colorset/Contents.json
+++ b/Shared/Colors.xcassets/StatusPositive.colorset/Contents.json
@@ -5,9 +5,9 @@
       "color": {
         "color-space": "srgb",
         "components": {
-          "red": "0x14",
-          "green": "0x6C",
-          "blue": "0x2E",
+          "red": "0x00",
+          "green": "0x80",
+          "blue": "0x35",
           "alpha": "1.000"
         }
       }
@@ -41,9 +41,9 @@
       "color": {
         "color-space": "srgb",
         "components": {
-          "red": "0x0B",
-          "green": "0x50",
-          "blue": "0x23",
+          "red": "0x00",
+          "green": "0x66",
+          "blue": "0x1F",
           "alpha": "1.000"
         }
       }
diff --git a/Shared/Colors.xcassets/StatusPositiveSurface.colorset/Contents.json b/Shared/Colors.xcassets/StatusPositiveSurface.colorset/Contents.json
new file mode 100644
index 0000000..dc80f8f
--- /dev/null
+++ b/Shared/Colors.xcassets/StatusPositiveSurface.colorset/Contents.json
@@ -0,0 +1,78 @@
+{
+  "colors": [
+    {
+      "idiom": "universal",
+      "color": {
+        "color-space": "srgb",
+        "components": {
+          "red": "0xE3",
+          "green": "0xF7",
+          "blue": "0xEC",
+          "alpha": "1.000"
+        }
+      }
+    },
+    {
+      "idiom": "universal",
+      "appearances": [
+        {
+          "appearance": "luminosity",
+          "value": "dark"
+        }
+      ],
+      "color": {
+        "color-space": "srgb",
+        "components": {
+          "red": "0x0E",
+          "green": "0x2A",
+          "blue": "0x18",
+          "alpha": "1.000"
+        }
+      }
+    },
+    {
+      "idiom": "universal",
+      "appearances": [
+        {
+          "appearance": "contrast",
+          "value": "high"
+        }
+      ],
+      "color": {
+        "color-space": "srgb",
+        "components": {
+          "red": "0xD3",
+          "green": "0xF0",
+          "blue": "0xE0",
+          "alpha": "1.000"
+        }
+      }
+    },
+    {
+      "idiom": "universal",
+      "appearances": [
+        {
+          "appearance": "luminosity",
+          "value": "dark"
+        },
+        {
+          "appearance": "contrast",
+          "value": "high"
+        }
+      ],
+      "color": {
+        "color-space": "srgb",
+        "components": {
+          "red": "0x12",
+          "green": "0x3A",
+          "blue": "0x20",
+          "alpha": "1.000"
+        }
+      }
+    }
+  ],
+  "info": {
+    "author": "xcode",
+    "version": 1
+  }
+}
diff --git a/Shared/Colors.xcassets/StatusWarning.colorset/Contents.json b/Shared/Colors.xcassets/StatusWarning.colorset/Contents.json
index cc79e86..003f80c 100644
--- a/Shared/Colors.xcassets/StatusWarning.colorset/Contents.json
+++ b/Shared/Colors.xcassets/StatusWarning.colorset/Contents.json
@@ -5,8 +5,8 @@
       "color": {
         "color-space": "srgb",
         "components": {
-          "red": "0x7A",
-          "green": "0x56",
+          "red": "0xAD",
+          "green": "0x51",
           "blue": "0x00",
           "alpha": "1.000"
         }
@@ -24,7 +24,7 @@
         "color-space": "srgb",
         "components": {
           "red": "0xFF",
-          "green": "0xD6",
+          "green": "0x9F",
           "blue": "0x0A",
           "alpha": "1.000"
         }
@@ -41,8 +41,8 @@
       "color": {
         "color-space": "srgb",
         "components": {
-          "red": "0x5C",
-          "green": "0x41",
+          "red": "0x8A",
+          "green": "0x3F",
           "blue": "0x00",
           "alpha": "1.000"
         }
@@ -64,8 +64,8 @@
         "color-space": "srgb",
         "components": {
           "red": "0xFF",
-          "green": "0xE5",
-          "blue": "0x66",
+          "green": "0xBF",
+          "blue": "0x5C",
           "alpha": "1.000"
         }
       }
diff --git a/Shared/Colors.xcassets/StatusWarningSurface.colorset/Contents.json b/Shared/Colors.xcassets/StatusWarningSurface.colorset/Contents.json
new file mode 100644
index 0000000..8030a19
--- /dev/null
+++ b/Shared/Colors.xcassets/StatusWarningSurface.colorset/Contents.json
@@ -0,0 +1,78 @@
+{
+  "colors": [
+    {
+      "idiom": "universal",
+      "color": {
+        "color-space": "srgb",
+        "components": {
+          "red": "0xFB",
+          "green": "0xEC",
+          "blue": "0xDF",
+          "alpha": "1.000"
+        }
+      }
+    },
+    {
+      "idiom": "universal",
+      "appearances": [
+        {
+          "appearance": "luminosity",
+          "value": "dark"
+        }
+      ],
+      "color": {
+        "color-space": "srgb",
+        "components": {
+          "red": "0x2E",
+          "green": "0x1F",
+          "blue": "0x08",
+          "alpha": "1.000"
+        }
+      }
+    },
+    {
+      "idiom": "universal",
+      "appearances": [
+        {
+          "appearance": "contrast",
+          "value": "high"
+        }
+      ],
+      "color": {
+        "color-space": "srgb",
+        "components": {
+          "red": "0xF7",
+          "green": "0xE0",
+          "blue": "0xC9",
+          "alpha": "1.000"
+        }
+      }
+    },
+    {
+      "idiom": "universal",
+      "appearances": [
+        {
+          "appearance": "luminosity",
+          "value": "dark"
+        },
+        {
+          "appearance": "contrast",
+          "value": "high"
+        }
+      ],
+      "color": {
+        "color-space": "srgb",
+        "components": {
+          "red": "0x3D",
+          "green": "0x2A",
+          "blue": "0x0B",
+          "alpha": "1.000"
+        }
+      }
+    }
+  ],
+  "info": {
+    "author": "xcode",
+    "version": 1
+  }
+}