diff --git a/frontend/src/components/site-finder/ptica/InvestScoreBlock.tsx b/frontend/src/components/site-finder/ptica/InvestScoreBlock.tsx
index 5824e9ff..45c0e6b7 100644
--- a/frontend/src/components/site-finder/ptica/InvestScoreBlock.tsx
+++ b/frontend/src/components/site-finder/ptica/InvestScoreBlock.tsx
@@ -80,11 +80,14 @@ export function InvestScoreBlock({
Риск
-
+
{inv.risk.value}
+ {/* #2934: «предв.» было только в атрибуте title — на экране его не
+ видно, а на печати и подавно. Пометка о том, что оценка
+ предварительная, должна читаться там же, где значение. */}
+ {inv.risk.caption && (
+ {inv.risk.caption}
+ )}
diff --git a/frontend/src/components/site-finder/ptica/__tests__/InvestScoreBlock.risk.test.tsx b/frontend/src/components/site-finder/ptica/__tests__/InvestScoreBlock.risk.test.tsx
new file mode 100644
index 00000000..d7c87468
--- /dev/null
+++ b/frontend/src/components/site-finder/ptica/__tests__/InvestScoreBlock.risk.test.tsx
@@ -0,0 +1,50 @@
+/**
+ * #2934: пометка «предв.» у строки «Риск» должна читаться на экране.
+ *
+ * Значение берётся из adaptRiskGauge = `100 - buildability`, то есть обратной
+ * застраиваемости. К природным опасностям (затопление, оползни, карст) оно
+ * отношения не имеет, а источники природного риска пусты: cad_risk_zones 0 строк,
+ * слои risk_* НСПД 0 объектов на 669 дампов (замер 19.08.2026).
+ *
+ * До правки «предв.» лежало ТОЛЬКО в атрибуте title — на экране невидимо, при
+ * печати тем более, а значение при этом красилось зелёным.
+ */
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import { InvestScoreBlock } from "../InvestScoreBlock";
+import analyzeFixture from "@/lib/mocks/parcel-analyze.json";
+import type { ParcelAnalysis } from "@/types/site-finder";
+
+/** Участок с gate_verdict — иначе застраиваемость null и строка «Риск» пуста. */
+function analysisWithGate(): ParcelAnalysis {
+ return {
+ ...(analyzeFixture as unknown as ParcelAnalysis),
+ gate_verdict: {
+ can_build_mkd: true,
+ verdict_label: "Можно",
+ blockers: [],
+ warnings: [],
+ checks_performed: [],
+ source: "nspd_dump",
+ },
+ } as unknown as ParcelAnalysis;
+}
+
+describe("InvestScoreBlock — строка «Риск» (#2934)", () => {
+ it("показывает пометку «предв.» видимым текстом", () => {
+ render(
+ {}}
+ />,
+ );
+
+ // Предусловие: строка «Риск» вообще отрисована со значением.
+ expect(screen.getByText("Риск")).toBeInTheDocument();
+ expect(screen.getByText("Низкий")).toBeInTheDocument();
+
+ // На origin/main «предв.» живёт только в атрибуте title — getByText не найдёт.
+ expect(screen.getByText("предв.")).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/components/site-finder/ptica/__tests__/ptica-adapt.honesty.test.ts b/frontend/src/components/site-finder/ptica/__tests__/ptica-adapt.honesty.test.ts
index c6c7a3cd..618f3f11 100644
--- a/frontend/src/components/site-finder/ptica/__tests__/ptica-adapt.honesty.test.ts
+++ b/frontend/src/components/site-finder/ptica/__tests__/ptica-adapt.honesty.test.ts
@@ -17,6 +17,8 @@ import { describe, expect, it } from "vitest";
import {
adaptBuySignalGauge,
adaptEconomyCard,
+ adaptInvestScore,
+ adaptRiskGauge,
adaptFinanceDrawer,
adaptInvestmentClearance,
adaptLegalDrawer,
@@ -352,3 +354,44 @@ describe("ptica-adapt gate (#1892 ЗОУИТ area-gate)", () => {
expect(v.tone).toBe("good");
});
});
+
+describe("ptica-adapt risk placeholder (#2934)", () => {
+ /** Участок, у которого застраиваемость СЧИТАЕТСЯ: без gate_verdict она null,
+ * и тон был бы "none" просто из-за отсутствия данных — проверка прошла бы
+ * вхолостую и на неисправленном коде тоже. */
+ function analysisWithGate(): ParcelAnalysis {
+ return {
+ ...analysis,
+ gate_verdict: {
+ can_build_mkd: true,
+ verdict_label: "Можно",
+ blockers: [],
+ warnings: [],
+ checks_performed: [],
+ source: "nspd_dump",
+ },
+ } as unknown as ParcelAnalysis;
+ }
+
+ it("«Риск» не окрашивается как вердикт, пока значение не настоящее", () => {
+ const a = analysisWithGate();
+ const gauge = adaptRiskGauge(a);
+ // Предусловие: значение реально посчитано, иначе тон был бы "none" по другой
+ // причине и проверка ничего бы не доказывала.
+ expect(gauge.value).not.toBeNull();
+ expect(gauge.tone).toBe("good"); // именно тот случай, что красился зелёным
+ expect(gauge.isReal).toBe(false);
+
+ expect(adaptInvestScore(a).riskTone).toBe("none");
+ });
+
+ it("подпись «предв.» доезжает до поля, а не только до тултипа", () => {
+ const inv = adaptInvestScore(analysisWithGate());
+ expect(inv.risk.caption).toBe("предв.");
+ expect(inv.risk.isReal).toBe(false);
+ });
+
+ it("контроль: «Потенциал» приглушён как и раньше", () => {
+ expect(adaptInvestScore(analysis).potentialTone).toBe("none");
+ });
+});
diff --git a/frontend/src/components/site-finder/ptica/ptica-adapt.ts b/frontend/src/components/site-finder/ptica/ptica-adapt.ts
index c8ac2df0..615dd149 100644
--- a/frontend/src/components/site-finder/ptica/ptica-adapt.ts
+++ b/frontend/src/components/site-finder/ptica/ptica-adapt.ts
@@ -353,9 +353,16 @@ export function adaptInvestScore(
risk.value != null
? { value: risk.label, isReal: false, caption: "предв." }
: placeholder("предв.");
- // Hero «Риск» value color (prototype .v.warn): green when low, yellow otherwise.
- const riskTone: PticaValueTone =
- risk.value == null ? "none" : risk.tone === "good" ? "good" : "warn";
+ // #2934: плейсхолдер не красим как вердикт. `adaptRiskGauge` считает риск как
+ // `100 - buildability`, то есть обратную застраиваемость — к природным опасностям
+ // (затопление, оползни, карст) он отношения не имеет, и сам помечен
+ // `isReal: false`. Зелёное «Низкий» читалось как «рисков нет», хотя источники
+ // природного риска пусты: cad_risk_zones 0 строк, слои risk_* НСПД 0 объектов на
+ // 669 дампов (замер 19.08).
+ //
+ // Приглушаем так же, как уже приглушён `potentialTone` — соглашение в этом же
+ // файле: пока значение не настоящее, цвета-вердикта у него нет.
+ const riskTone: PticaValueTone = "none";
const overall =
report?.scoring?.overall ?? report?.exec_summary.key_numbers.overall_score;