diff --git a/client/src/app/App.tsx b/client/src/app/App.tsx
index 64654c5..65634bb 100644
--- a/client/src/app/App.tsx
+++ b/client/src/app/App.tsx
@@ -4,6 +4,7 @@ import { AppRoutes } from '@/app/routes'
import { NotificationStack } from '@/shared/ui/NotificationStack'
import { ErrorBoundary } from '@/shared/ui/ErrorBoundary'
import { NoiseOverlay } from '@/shared/ui/NoiseOverlay'
+import { DemoOverlay } from '@/shared/ui/DemoOverlay'
export function App() {
return (
@@ -14,6 +15,7 @@ export function App() {
+
)
diff --git a/client/src/shared/ui/DemoOverlay.tsx b/client/src/shared/ui/DemoOverlay.tsx
new file mode 100644
index 0000000..3916dff
--- /dev/null
+++ b/client/src/shared/ui/DemoOverlay.tsx
@@ -0,0 +1,56 @@
+import Box from '@mui/material/Box'
+import { useTheme } from '@mui/material/styles'
+import { IS_DEMO_MODE } from '@/shared/config'
+
+export function DemoOverlay() {
+ const theme = useTheme()
+ const isDark = theme.palette.mode === 'dark'
+
+ if (!IS_DEMO_MODE) return null
+
+ return (
+ <>
+
+ ДЕМО
+
+
+
+ ДЕМО-РЕЖИМ
+
+ >
+ )
+}
diff --git a/client/src/shared/ui/__tests__/DemoOverlay.test.tsx b/client/src/shared/ui/__tests__/DemoOverlay.test.tsx
new file mode 100644
index 0000000..3130c6b
--- /dev/null
+++ b/client/src/shared/ui/__tests__/DemoOverlay.test.tsx
@@ -0,0 +1,44 @@
+import { render } from '@testing-library/react'
+import { describe, expect, it, vi } from 'vitest'
+
+let mockDemoMode = true
+
+vi.mock('@/shared/config', async () => {
+ const actual = await vi.importActual('@/shared/config')
+ return {
+ ...actual,
+ get IS_DEMO_MODE() {
+ return mockDemoMode
+ },
+ }
+})
+
+import { DemoOverlay } from '../DemoOverlay'
+
+describe('DemoOverlay', () => {
+ it('рендерит водяной знак и плашку когда демо включён', () => {
+ mockDemoMode = true
+ const { container } = render()
+
+ const text = container.textContent
+ expect(text).toContain('ДЕМО')
+ expect(text).toContain('ДЕМО-РЕЖИМ')
+
+ const allBoxes = container.querySelectorAll('.MuiBox-root')
+ expect(allBoxes.length).toBeGreaterThanOrEqual(2)
+
+ const [watermark, badge] = allBoxes
+
+ expect(watermark.getAttribute('aria-hidden')).toBe('true')
+ expect(watermark.textContent).toBe('ДЕМО')
+
+ expect(badge.getAttribute('aria-hidden')).toBe('true')
+ expect(badge.textContent).toBe('ДЕМО-РЕЖИМ')
+ })
+
+ it('не рендерит ничего когда демо выключен', () => {
+ mockDemoMode = false
+ const { container } = render()
+ expect(container.textContent).toBe('')
+ })
+})
diff --git a/docs/superpowers/specs/2026-06-03-demo-overlay-design.md b/docs/superpowers/specs/2026-06-03-demo-overlay-design.md
new file mode 100644
index 0000000..2d2c725
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-03-demo-overlay-design.md
@@ -0,0 +1,67 @@
+# DemoOverlay — индикация демо-режима
+
+## Контекст
+
+Демо-режим активируется через `VITE_DEMO_MODE=true` (`client/.env.local`). Сейчас есть только `DemoBanner` (Alert в потоке страницы, не фиксированный). Нужно добавить постоянную визуальную индикацию — оверлей, который не мешает взаимодействию с сайтом.
+
+## Что делаем
+
+Новый компонент `DemoOverlay` в `client/src/shared/ui/DemoOverlay.tsx`.
+
+Два фиксированных слоя, оба `pointer-events: none`:
+
+1. **Водяной знак** — крупная надпись «ДЕМО», полупрозрачная, повёрнута на ~-30°, по центру экрана.
+2. **Плашка** — правый нижний угол, скруглённая полупрозрачная тёмная плашка с текстом «ДЕМО-РЕЖИМ».
+
+Оба рендерятся только при `IS_DEMO_MODE === true`.
+
+## Размещение
+
+В `App.tsx` на одном уровне с ``, вне роутов:
+
+```tsx
+
+
+```
+
+`DemoBanner` (существующий Alert в MainLayout) — не трогаем, остаётся как есть.
+
+## Водяной знак
+
+- Текст: `ДЕМО`
+- Размер шрифта: `10vw` (адаптивный)
+- Поворот: `rotate(-30deg)`
+- Цвет: `rgba(0,0,0,0.04)` (тёмная тема: `rgba(255,255,255,0.04)`)
+- Позиция: `position: fixed`, `inset: 0`, центрирование через flex
+- z-index: `9990`
+
+## Плашка
+
+- Текст: `ДЕМО-РЕЖИМ`
+- Позиция: `position: fixed`, `bottom: 16px`, `right: 16px`
+- Фон: `rgba(0,0,0,0.6)` (тёмная тема: `rgba(255,255,255,0.08)`)
+- Цвет текста: `#fff` (тёмная тема: `rgba(255,255,255,0.6)`)
+- Паддинги: `6px 16px`
+- Скругление: `8px`
+- Размер шрифта: `12px`, `font-weight: 600`
+- z-index: `9991`
+
+## Тёмная тема
+
+Компонент читает тему через `useTheme()` из MUI и применяет соответствующие цвета для watermark и плашки.
+
+## Тесты
+
+Проверяем:
+- Компонент рендерится когда `IS_DEMO_MODE === true` (водяной знак + плашка видны)
+- Компонент не рендерится когда `IS_DEMO_MODE === false`
+- Плашка в правом нижнем углу (проверяем CSS-свойства)
+- `pointer-events: none` на обоих элементах
+
+## Файлы
+
+| Действие | Файл |
+|----------|------|
+| Создать | `client/src/shared/ui/DemoOverlay.tsx` |
+| Изменить | `client/src/app/App.tsx` |
+| Создать | `client/src/shared/ui/__tests__/DemoOverlay.test.tsx` |
diff --git a/server/prisma/prisma/dev.db b/server/prisma/prisma/dev.db
index abbf38c..7065504 100644
Binary files a/server/prisma/prisma/dev.db and b/server/prisma/prisma/dev.db differ