diff --git a/.editorconfig b/.editorconfig
index e9840bf15..850846d0b 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -104,3 +104,19 @@ ij_json_wrap_long_lines = false
[*.cs]
# disable CA1848: Use the LoggerMessage delegates`
dotnet_diagnostic.ca1848.severity = none
+
+# --- Static-analysis pack adoption (ersatztv#15) ---
+# Roslynator / SonarAnalyzer / Meziantou / AsyncFixer are referenced centrally
+# (Directory.Build.targets). Default every analyzer diagnostic to `suggestion` so the new
+# packs don't fail the TreatWarningsAsErrors build; high-value rules get promoted to
+# warning/error one at a time (see ersatztv#15 / docs/contributing.md). Explicit per-rule
+# severities (e.g. ca1848 above) still take precedence over this bulk default.
+dotnet_analyzer_diagnostic.severity = suggestion
+
+# Blazor components: analyzers run on .razor/.cshtml @code too, and TWAE would otherwise
+# turn their default-severity findings into build errors — keep them at suggestion as well.
+[*.razor]
+dotnet_analyzer_diagnostic.severity = suggestion
+
+[*.cshtml]
+dotnet_analyzer_diagnostic.severity = suggestion
diff --git a/Directory.Build.targets b/Directory.Build.targets
index bb539cbf2..8703b853f 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -11,4 +11,31 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 560487eb0..afb0ee65e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,6 +4,7 @@
+
@@ -35,6 +36,7 @@
+
@@ -74,6 +76,7 @@
+
@@ -90,6 +93,7 @@
+
diff --git a/ErsatzTV/ErsatzTV.csproj b/ErsatzTV/ErsatzTV.csproj
index 853503151..7a16555e7 100644
--- a/ErsatzTV/ErsatzTV.csproj
+++ b/ErsatzTV/ErsatzTV.csproj
@@ -7,6 +7,12 @@
false
enable
VSTHRD200,CA1873
+
+ $(NoWarn);S6966;S3267;S2325;S3260;S2094;S3458;S3358;S6667;S125;S1135;S1751;S1125;S1066;S127;S927;S6610;S6580;S5693;S3440;S2933;S1871
true
Debug;Release;Debug No Sync
AnyCPU
diff --git a/docs/ci-cd.md b/docs/ci-cd.md
index c69945193..0ad27c3c2 100644
--- a/docs/ci-cd.md
+++ b/docs/ci-cd.md
@@ -134,6 +134,30 @@ Core `[9.0.x,10)`) are respected — no v10 jump. PR volume is throttled (`prCon
to drain faster. `workflow_dispatch` defaults to a safe **dry run**. Cross-repo rollout
tracked in server-management#484. (server-management#484)
+## Static analysis & formatting
+
+**Analyzer packs** — `Directory.Build.targets` references **Roslynator**, **SonarAnalyzer.CSharp**,
+**Meziantou.Analyzer**, and **AsyncFixer** for every project (versions central via CPM; guarded on
+`ManagePackageVersionsCentrally` so the gitignored `.mcp` tool isn't pulled in). They are introduced
+**incrementally** (ersatztv#15): `.editorconfig` sets `dotnet_analyzer_diagnostic.severity = suggestion`
+so the packs surface findings without failing the `TreatWarningsAsErrors` (TWAE) build. **Promotion is
+the enforcement** — raising a rule to `warning` makes it a CI-blocking error via the existing TWAE
+build, so no separate lint step is needed.
+
+- **StyleCop.Analyzers is intentionally excluded**: its latest stable (1.1.118) crashes (`AD0001`) on
+ C# `record` declarations, and its rules overlap the existing `.editorconfig`/Roslynator. Revisit via
+ the record-compatible `1.2.0-beta` only if specifically wanted.
+- **Blazor `.razor` caveat**: editorconfig severity overrides don't reach analyzer diagnostics in Razor
+ `@code` (source-generator limitation — `dotnet format` can't fix them either), so the currently-firing
+ SonarAnalyzer rules are temporarily `NoWarn`-ed in `ErsatzTV.csproj` and burned down rule-by-rule in
+ **ersatztv#25**. The same rules run at `suggestion` on `.cs`.
+
+**Formatting** — the tree isn't yet `dotnet format`-clean (mixed UTF-8 BOM + whitespace inherited from
+upstream: ~1,500 BOM files + ~480 whitespace). A one-time normalization lands as its **own dedicated
+PR** (kept out of the analyzer work to stay reviewable); afterwards `dotnet format whitespace
+--verify-no-changes` (+ `style`) joins the `test` job so drift can't return. `.gitattributes` already
+pins line endings.
+
## Registry
Gitea Packages, HTTP-only at `192.168.1.95:3000`. jazz's Docker daemon has it as an