diff --git a/frontend/src/routes/invites/[token]/+page.svelte b/frontend/src/routes/(public)/invites/[token]/+page.svelte
similarity index 100%
rename from frontend/src/routes/invites/[token]/+page.svelte
rename to frontend/src/routes/(public)/invites/[token]/+page.svelte
diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/(public)/login/+page.svelte
similarity index 100%
rename from frontend/src/routes/login/+page.svelte
rename to frontend/src/routes/(public)/login/+page.svelte
diff --git a/frontend/src/routes/share/[token]/+page.svelte b/frontend/src/routes/(public)/share/[token]/+page.svelte
similarity index 64%
rename from frontend/src/routes/share/[token]/+page.svelte
rename to frontend/src/routes/(public)/share/[token]/+page.svelte
index 89e28cd..234b2be 100644
--- a/frontend/src/routes/share/[token]/+page.svelte
+++ b/frontend/src/routes/(public)/share/[token]/+page.svelte
@@ -3,6 +3,7 @@
import { api } from '$lib/api';
import Icon from '$lib/components/Icon.svelte';
import PhotoGrid from '$lib/components/PhotoGrid.svelte';
+ import Viewer from '$lib/components/Viewer.svelte';
import type { Album, Photo } from '$lib/types';
const token = $derived($page.params.token);
@@ -65,41 +66,13 @@
{/if}
{#if active >= 0}
-
- {/if}
-
- {#if lightboxIndex >= 0}
- (lightboxIndex = -1)}
- onIndexChange={(i) => (lightboxIndex = i)}
- />
- {/if}
-{/if}
diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts
index 80b45ff..4594d69 100644
--- a/frontend/tailwind.config.ts
+++ b/frontend/tailwind.config.ts
@@ -11,6 +11,8 @@ export default {
muted: 'hsl(var(--bg-muted) / )'
},
border: 'hsl(var(--border) / )',
+ chrome: 'hsl(var(--chrome) / )',
+ canvas: 'hsl(var(--canvas) / )',
text: {
DEFAULT: 'hsl(var(--text) / )',
muted: 'hsl(var(--text-muted) / )',
diff --git a/openspec/changes/lightroom-workspace-ui/.openspec.yaml b/openspec/changes/lightroom-workspace-ui/.openspec.yaml
new file mode 100644
index 0000000..53f62d7
--- /dev/null
+++ b/openspec/changes/lightroom-workspace-ui/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-10-10
diff --git a/openspec/changes/lightroom-workspace-ui/design.md b/openspec/changes/lightroom-workspace-ui/design.md
new file mode 100644
index 0000000..4f203d4
--- /dev/null
+++ b/openspec/changes/lightroom-workspace-ui/design.md
@@ -0,0 +1,128 @@
+# Design
+
+## Context
+
+См. proposal.md — Why. Текущее состояние, которое формирует подход:
+
+- `albums.cover_photo_id` уже существует (`migrations/000001_init.up.sql`), `PATCH /albums/:id`
+ принимает `cover_photo_id`/`clear_cover` (`albums/handlers.go:55`), но список и `GET`
+ отдают только id — URL миниатюры негде взять, поэтому карточки рисуют иконку-заглушку.
+- `photos.Service.attachURLs` уже пресигнит `ThumbKey`/`PreviewKey` (`photos/service.go:132`),
+ а `albums.Service` держит `storage *storage.Client` — приём с presigned URL доступен и там.
+- Фоновая обработка загруженного фото безусловно зовёт `photos.Store.SetCover`
+ (`photos/service.go:124`), перезаписывая выбор пользователя.
+- FK на `cover_photo_id` нет; удаление фото-обложки оставляет висячий id.
+- Фронтенд: один плоский `+layout.svelte` (топбар + `max-w-6xl` + центр), лайтбокс —
+ оверлей, на `share/[token]` свой инлайн-лайтбокс. Темы: light/dark/neutral/system, дефолт
+ `system` (`stores/theme.svelte.ts`).
+
+## Goals / Non-Goals
+
+**Goals:**
+- Постоянный каркас с рейлом и контекстной панелью, центр = текущий маршрут (Q).
+- Обложки доезжают до UI; явный выбор + разумный дефолт; корректное удаление.
+- Тёмная графитовая тема по умолчанию при сохранении остальных тем.
+- Просмотр фото как маршрут в каркасе, общий с публичной share-страницей.
+- Адаптивность до мобильного без потери каркаса.
+
+**Non-Goals:**
+- Мультивыбор фото и массовые действия / bulk-удаление.
+- Замена роутинга на «чистые панели» без маршрутов (вариант P).
+- Новые внешние зависимости (иконки и токены вендорятся/расширяются).
+- Изменение доменных контрактов API (только добавление поля ответа и FK).
+
+## Decisions
+
+### 1. Каркас: группы маршрутов, центр = маршрут
+`+layout.svelte` разбивается на группы: `(app)` — каркас с топбаром, левым рейлом и правой
+панелью, и `(public)` — только токены. Центр `(app)` рендерит текущий маршрут; рейл живёт
+между навигациями.
+Альтернатива — один layout с условиями по `$page.route.id` (проще, но разрастается) или P
+(панели вместо маршрутов) — отклонена: ломает deeplink и историю.
+
+### 2. Обложка: LEFT JOIN + presign (A)
+`albums.Store` добавляет `LEFT JOIN photos p ON p.id = a.cover_photo_id` и отдаёт
+`cover_thumb_key`; `albums.Service` пресигнит его в `Album.CoverThumbURL`. Один запрос и один
+presign на альбом; висячий id безопасно даёт `NULL`. Альтернативы: N+1 с `GET /photos/:id`
+(chatty) и выделенный thumb-эндпоинт (расходится с текущим паттерном presigned URL).
+
+### 3. Автообложка: писать только если пусто
+`processVariants` вызывает `SetCover` только при `cover_photo_id IS NULL`. Выбор пользователя
+не слетает, дефолт появляется с первым фото. Альтернатива — дефолт на чтении (COALESCE) —
+отклонена в пользу простоты записи/чтения; осознанно принимаем, что в пачке побеждает
+фото, обработанное первым.
+
+### 4. Целостность обложки: FK ON DELETE SET NULL (M)
+Миграция `000002_album_cover_fk`: сперва обнуляет уже висячие `cover_photo_id`, затем
+`ALTER TABLE albums ADD CONSTRAINT ... FOREIGN KEY (cover_photo_id) REFERENCES photos(id)
+ON DELETE SET NULL`. БД сама чинит любой путь удаления. Циклический FK `albums <-> photos`
+безвреден: альбом вставляется с `NULL`, фото ссылается на альбом, cover выставляется позже.
+Альтернатива — чистка в `photos.Delete` (легко забыть на новых путях удаления).
+
+### 5. Точки выбора обложки: панель + просмотр (T1+T2)
+Правая панель даёт `[Выбрать обложку]` (модальный пикер из миниатюр альбома) и `[Сбросить]`;
+в просмотре — действие «Сделать обложкой» для текущего фото. Всё под `roles.CanEditAlbum`.
+Hover-оверлей в сетке (T3) не берём — шум и случайные клики.
+
+### 6. Тема: сохранить темы, dark = графит и дефолт (α)
+Тёмная тема перенастраивается в графит Lightroom и становится значением по умолчанию
+(`theme.current = 'dark'` при отсутствии сохранённого выбора). Добавляются аддитивные токены
+`--chrome` (топбар/рейл/панель) и `--canvas` (центр), чтобы «утопить» контент между панелями.
+`light`/`neutral`/`system` остаются. Спека `ui-iconography` не затрагивается.
+
+### 7. Адаптив: drawer + sheet + горизонтальная лента (M1)
+Порог рабочего стола — `lg`/`md`. Ниже: рейл — off-canvas drawer по кнопке, правая панель —
+сворачиваемая секция/bottom-sheet, filmstrip — горизонтальный скролл. Публичные экраны
+получают ту же мобильную логику просмотра без рейла/панели.
+
+### 8. Плотность сетки, без выбора (D)
+Регулятор размера миниатюр в тулбаре сетки управляет `grid-template-columns` (пресеты или
+`minmax`), значение хранится в `localStorage`. Мультивыбор/bulk — вне объёма.
+
+### 9. Просмотр как маршрут (L2)
+Новый маршрут `albums/[id]/photos/[photoId]` рендерит общий компонент `Viewer` (крупное фото,
+filmstrip, панель метаданных/комментариев, тулбар). `share/[token]` переиспользует `Viewer`
+без рейла/панели. Навигация синхронизирует маршрут и filmstrip; клавиши ←/→/Esc.
+
+### 10. Загрузка из тулбара (U1)
+Иконочная кнопка загрузки (роль-gated) открывает файловый диалог; `dragover` на весь альбом
+показывает полноэкранный оверлей; пустой альбом показывает центральную зону-подсказку.
+Прогресс — компактная полоса/тост вместо крупного блока.
+
+### 11. Создание альбома через модалку
+Кнопка `+` с хинтом (в рейле и/или топбаре) открывает модалку с полем названия; `POST
+/albums`, созданный альбом становится активным. Инлайн-форма удаляется.
+
+## Risks / Trade-offs
+
+- [Много альбомов → много presign в списке] → presign это локальная крипто-операция без сети;
+ при необходимости кэшировать URL на короткий TTL.
+- [Presigned URL обложки истекает при долго открытой странице] → URL обновляется при загрузке
+ списка; при ошибке загрузки картинки показывать плейсхолдер.
+- [Циклический FK albums↔photos] → миграция обнуляет висячие id до создания constraint;
+ каскадное удаление альбома обрабатывается БД (SET NULL по уже удаляемой строке безвреден).
+- [Реструктуризация `+layout` задевает все маршруты] → выносить каркас инкрементально, группы
+ `(app)`/`(public)` в одном шаге, публичные страницы регрессионно проверяем.
+- [Добавление токенов темы задевает все экраны] → токены аддитивны, старые классы продолжают
+ работать; проверка триад тем.
+- [Пачка загрузок: обложкой становится первое обработанное фото] → принято; дальнейшие
+ загрузки обложку не трогают, пользователь может задать явно.
+- [Дополнительное состояние в маршруте просмотра (filmstrip, клавиатура)] → единый компонент
+ `Viewer` с props и общим источником фото.
+
+## Migration Plan
+
+1. Миграция `000002_album_cover_fk`: `UPDATE albums SET cover_photo_id = NULL WHERE
+ cover_photo_id IS NOT NULL AND cover_photo_id NOT IN (SELECT id FROM photos);` затем
+ `ADD CONSTRAINT ... ON DELETE SET NULL`. Down — `DROP CONSTRAINT`.
+2. Бэкенд: `models.Album` (+`CoverThumbURL`), `albums/store.go` (LEFT JOIN), `albums/service.go`
+ (enrich + presign), `photos/service.go` (SetCover только если NULL).
+3. Фронтенд: каркас и маршруты, компоненты, токены, `types.ts`.
+4. Откат: снять constraint (down-миграция); фронтенд откатывается независимо, поле ответа
+ игнорируется старым клиентом.
+
+## Open Questions
+
+- Набор пресетов регулятора плотности (сколько ступеней) — решается при реализации, спеки не
+ меняет.
+- Хинты: нативный `title` или кастомный тултип — косметика, спеки не меняет.
diff --git a/openspec/changes/lightroom-workspace-ui/proposal.md b/openspec/changes/lightroom-workspace-ui/proposal.md
new file mode 100644
index 0000000..c4e6a36
--- /dev/null
+++ b/openspec/changes/lightroom-workspace-ui/proposal.md
@@ -0,0 +1,64 @@
+# Proposal
+
+## Why
+
+Сейчас интерфейс — это центрированный «маркетинговый» лейаут: список альбомов с
+инлайн-формой создания, серая заглушка вместо обложек, отдельные страницы «Участники» и
+«Настройки», лайтбокс-оверлей. Обложки в бэкенде уже есть (`albums.cover_photo_id`,
+`PATCH /albums/:id`), но до UI не доходят. Пользователь хочет полноценный «рабочий стол»
+в духе Adobe Lightroom: постоянный рейл альбомов, контекстная панель, плотная сетка,
+режим просмотра как часть рабочего стола, обложки с явным выбором и единый тёмный
+графитовый хром.
+
+## What Changes
+
+- **Рабочий стол (шелл).** Группа `(app)` с постоянным левым рейлом альбомов, топбаром и
+ контекстной правой панелью. Публичные маршруты (`/login`, `/invites/[token]`,
+ `/share/[token]`) остаются вне шелла, но в тех же токенах.
+- **Создание альбома.** Инлайн-форма заменяется иконочной кнопкой с хинтом, открывающей
+ модалку ввода названия.
+- **Обложки.** `Album` получает `cover_thumb_url`; карточки и рейл показывают реальную
+ миниатюру. Выбор обложки: пикер в правой панели и действие «сделать обложкой» в
+ просмотре. Автообложка пишется только когда обложка ещё не задана; FK
+ `albums.cover_photo_id -> photos(id) ON DELETE SET NULL` чинит висячие ссылки.
+- **Просмотр как маршрут.** `Lightbox`-оверлей заменяется маршрутом
+ `/albums/:id/photos/:photoId` внутри шелла: крупное фото, filmstrip снизу, панель
+ метаданных/комментариев. `share/[token]` переиспользует тот же компонент.
+- **Плотность сетки.** Слайдер размера миниатюр в тулбаре (состояние в localStorage).
+ Мультивыбор и массовые действия — вне объёма.
+- **Загрузка.** Крупный dropzone заменяется кнопкой в тулбаре, полноэкранным
+ drag-overlay и dropzone-намёком только в пустом альбоме.
+- **Тёмная тема.** Существующие 4 темы сохраняются; `dark` переодевается в графит
+ Lightroom и становится дефолтом; добавляются токены `--chrome` и `--canvas`.
+- **Адаптив.** На узких экранах рейл становится off-canvas drawer, правая панель —
+ bottom-sheet, filmstrip — горизонтальным скроллом.
+
+## Capabilities
+
+### New Capabilities
+
+- `ui-workspace`: рабочий стол приложения — шелл, рейл альбомов, контекстная панель,
+ режим просмотра как маршрут, адаптивное поведение и тёмный хром.
+
+### Modified Capabilities
+
+- `albums`: отображение обложки в списке/карточках и явный выбор обложки; поведение
+ обложки при отсутствии выбора.
+- `photos`: автоустановка обложки первым загруженным фото выполняется только если обложка
+ ещё не задана.
+
+## Impact
+
+- **Backend:** миграция FK `albums.cover_photo_id -> photos(id) ON DELETE SET NULL`;
+ `internal/albums/store.go` (LEFT JOIN `photos` → `cover_thumb_key` в `ListForUser` и
+ `GetByID`); `internal/albums/service.go` (presign → `CoverThumbURL`, обогащение
+ списка); `internal/models` (`Album.CoverThumbURL`); `internal/photos/service.go`
+ (`SetCover` только при `cover_photo_id IS NULL`).
+- **Frontend:** реструктуризация `+layout.svelte` в группы `(app)`/`(public)`; новые
+ компоненты шелла, рейла, панели, модалки создания, пикера обложки, filmstrip и
+ просмотра; новый маршрут `albums/[id]/photos/[photoId]`; новые значения токенов темы;
+ `types.ts` (+`cover_thumb_url`); новые иконки в реестре.
+- **Спеки:** `ui-workspace` (новая), `albums`, `photos` (дельты). `ui-iconography` не
+ затрагивается — иконки по-прежнему наследуют цвет темы.
+- **Совместимость:** API-контракты сохраняются; добавляется поле ответа и один FK. Ломающих
+ изменений API нет.
diff --git a/openspec/changes/lightroom-workspace-ui/specs/albums/spec.md b/openspec/changes/lightroom-workspace-ui/specs/albums/spec.md
new file mode 100644
index 0000000..43a7d00
--- /dev/null
+++ b/openspec/changes/lightroom-workspace-ui/specs/albums/spec.md
@@ -0,0 +1,31 @@
+# Spec Delta
+
+## ADDED Requirements
+
+### Requirement: Отображение обложки альбома
+Данные альбома SHALL включать URL миниатюры обложки, когда обложка задана, чтобы клиенты
+могли показать её в списке, рейле и карточке.
+
+#### Scenario: Обложка задана
+- **WHEN** клиент запрашивает список альбомов или альбом по id, и у альбома есть обложка
+- **THEN** ответ содержит URL миниатюры обложки
+
+#### Scenario: Обложка отсутствует
+- **WHEN** у альбома нет обложки или фото обложки удалено
+- **THEN** ответ не содержит URL обложки, и клиент показывает плейсхолдер
+
+### Requirement: Выбор обложки
+Владелец и админ SHALL иметь возможность назначить обложкой любое фото альбома или сбросить
+обложку; после сброса обложка определяется заново по умолчанию.
+
+#### Scenario: Назначение обложки
+- **WHEN** владелец выбирает фото альбома как обложку
+- **THEN** система сохраняет выбор и отражает его в списке и карточках
+
+#### Scenario: Сброс обложки
+- **WHEN** админ сбрасывает обложку
+- **THEN** система удаляет явный выбор, и обложка возвращается к значению по умолчанию
+
+#### Scenario: Нет прав на смену обложки
+- **WHEN** участник без права редактирования пытается сменить обложку
+- **THEN** система отклоняет запрос
diff --git a/openspec/changes/lightroom-workspace-ui/specs/photos/spec.md b/openspec/changes/lightroom-workspace-ui/specs/photos/spec.md
new file mode 100644
index 0000000..b31b173
--- /dev/null
+++ b/openspec/changes/lightroom-workspace-ui/specs/photos/spec.md
@@ -0,0 +1,15 @@
+# Spec Delta
+
+## ADDED Requirements
+
+### Requirement: Обложка по умолчанию
+При обработке загруженного фото система SHALL назначать его обложкой альбома только если
+обложка ещё не задана.
+
+#### Scenario: Первое фото
+- **WHEN** фото обработано, и у альбома нет обложки
+- **THEN** система назначает это фото обложкой
+
+#### Scenario: Обложка уже задана
+- **WHEN** фото обработано, а обложка уже задана
+- **THEN** система не меняет обложку
diff --git a/openspec/changes/lightroom-workspace-ui/specs/ui-workspace/spec.md b/openspec/changes/lightroom-workspace-ui/specs/ui-workspace/spec.md
new file mode 100644
index 0000000..2de1845
--- /dev/null
+++ b/openspec/changes/lightroom-workspace-ui/specs/ui-workspace/spec.md
@@ -0,0 +1,121 @@
+# Spec Delta
+
+## Purpose
+
+Рабочий стол веб-приложения: постоянный каркас с рейлом альбомов, контекстной панелью,
+режимом просмотра фото и адаптивным поведением, обеспечивающий цельный «lightroom»-вид.
+
+## ADDED Requirements
+
+### Requirement: Рабочий стол приложения
+Аутентифицированные экраны SHALL отображаться внутри постоянного каркаса с топбаром, левым
+рейлом альбомов и контекстной правой панелью. Публичные экраны (вход, инвайт, share-ссылка)
+SHALL отображаться вне каркаса, но в общей теме оформления.
+
+#### Scenario: Внутренний экран
+- **WHEN** аутентифицированный пользователь открывает список альбомов или альбом
+- **THEN** экран отрисован внутри каркаса с рейлом и панелью, рейл не перезагружается при переходе
+
+#### Scenario: Публичный экран
+- **WHEN** пользователь открывает публичную share-ссылку без входа
+- **THEN** экран отрисован без рейла и контекстной панели, но в тех же цветовых токенах
+
+### Requirement: Рейл альбомов
+Рейл SHALL постоянно показывать список доступных альбомов с их обложками и отмечать
+активный альбом.
+
+#### Scenario: Активный альбом
+- **WHEN** пользователь находится в альбоме
+- **THEN** соответствующий элемент рейла выделен, остальные показывают обложки или плейсхолдер
+
+### Requirement: Создание альбома
+Создание альбома SHALL выполняться через иконочную кнопку с подсказкой, открывающую
+модальное окно ввода названия, вместо инлайн-формы.
+
+#### Scenario: Создание через модалку
+- **WHEN** пользователь нажимает кнопку создания, вводит название и подтверждает
+- **THEN** система создаёт альбом и делает его активным
+
+#### Scenario: Пустое название
+- **WHEN** пользователь подтверждает пустое название
+- **THEN** создание не выполняется
+
+### Requirement: Контекстная панель альбома
+Правая панель SHALL показывать превью обложки, название, счётчики фото и участников,
+управление обложкой и переходы к участникам, настройкам и ссылкам. Элементы управления
+обложкой SHALL быть доступны только owner и admin.
+
+#### Scenario: Владелец альбома
+- **WHEN** owner открывает альбом
+- **THEN** панель показывает управление обложкой и переходы к участникам и настройкам
+
+#### Scenario: Наблюдатель
+- **WHEN** viewer открывает альбом
+- **THEN** панель не показывает управление обложкой
+
+### Requirement: Режим просмотра фотографии
+Просмотр фото SHALL быть отдельным маршрутом внутри каркаса с крупным изображением,
+лентой миниатюр снизу и панелью метаданных и комментариев. Тот же режим SHALL
+использоваться на публичных share-страницах.
+
+#### Scenario: Открытие фото
+- **WHEN** пользователь выбирает фото в сетке
+- **THEN** открывается маршрут фото в каркасе, рейл и панель остаются доступны, адрес отражает фото
+
+#### Scenario: Навигация в просмотре
+- **WHEN** пользователь использует стрелки влево/вправо или ленту миниатюр
+- **THEN** показывается соседнее фото, адрес обновляется
+
+#### Scenario: Публичный просмотр
+- **WHEN** посетитель share-ссылки открывает фото
+- **THEN** используется тот же режим просмотра без рейла и панели
+
+### Requirement: Плотность сетки
+Сетка SHALL давать пользователю управлять размером миниатюр, и выбранный размер SHALL
+сохраняться между сессиями.
+
+#### Scenario: Изменение размера
+- **WHEN** пользователь двигает регулятор плотности
+- **THEN** размер миниатюр меняется, значение сохраняется и восстанавливается при следующем открытии
+
+### Requirement: Загрузка фотографий из рабочего стола
+Загрузка SHALL запускаться действием в тулбаре; перетаскивание файлов над альбомом SHALL
+показывать оверлей и загружать их; пустой альбом SHALL показывать подсказку-зону загрузки.
+Загрузка SHALL быть доступна только ролям с правом загрузки.
+
+#### Scenario: Перетаскивание
+- **WHEN** пользователь с правом загрузки перетаскивает файлы над альбомом
+- **THEN** показывается оверлей загрузки, и файлы загружаются
+
+#### Scenario: Пустой альбом
+- **WHEN** пользователь открывает альбом без фото и имеет право загрузки
+- **THEN** в центре показана зона-подсказка загрузки
+
+### Requirement: Тёмная графитовая тема
+Тёмная тема SHALL использовать графитовый хром Lightroom и SHALL применяться по умолчанию;
+остальные темы SHALL оставаться доступными.
+
+#### Scenario: Первый вход
+- **WHEN** пользователь впервые открывает приложение без сохранённой темы
+- **THEN** применяется тёмная графитовая тема
+
+#### Scenario: Смена темы
+- **WHEN** пользователь выбирает другую тему
+- **THEN** интерфейс и иконки переключаются согласованно и выбор сохраняется
+
+### Requirement: Адаптивность рабочего стола
+На узких экранах рейл SHALL становиться выдвижной панелью, контекстная панель —
+сворачиваемой секцией, а лента миниатюр — горизонтально прокручиваемой.
+
+#### Scenario: Узкий экран
+- **WHEN** ширина окна меньше порога рабочего стола
+- **THEN** рейл скрыт за кнопкой открытия, панель сворачиваема, лента прокручивается горизонтально
+
+### Requirement: Доступность рабочего стола
+Иконочные органы управления каркаса SHALL иметь доступные имена, панели SHALL быть
+достижимы с клавиатуры с видимым фокусом, анимации SHALL учитывать
+предпочтение уменьшенного движения.
+
+#### Scenario: Клавиатурная навигация
+- **WHEN** пользователь работает только с клавиатуры
+- **THEN** органы управления каркаса достижимы, доступные имена присутствуют, фокус виден
diff --git a/openspec/changes/lightroom-workspace-ui/tasks.md b/openspec/changes/lightroom-workspace-ui/tasks.md
new file mode 100644
index 0000000..286df5d
--- /dev/null
+++ b/openspec/changes/lightroom-workspace-ui/tasks.md
@@ -0,0 +1,44 @@
+# Tasks
+
+## 1. Backend: обложка и целостность
+
+- [x] 1.1 Добавить миграцию `000002_album_cover_fk` (up/down): обнулить висячие `cover_photo_id`, затем `ADD CONSTRAINT ... FOREIGN KEY (cover_photo_id) REFERENCES photos(id) ON DELETE SET NULL`. Проверка: `make migrate` применяется, `\d albums` показывает constraint, down-миграция снимает его.
+- [x] 1.2 Добавить `CoverThumbURL` в `models.Album` (json `cover_thumb_url,omitempty`). Проверка: `cd backend && go build ./...` проходит.
+- [x] 1.3 В `albums/store.go` добавить `LEFT JOIN photos p ON p.id = a.cover_photo_id` и выбирать `p.thumb_key AS cover_thumb_key` в `ListForUser` и `GetByID`. Проверка: запрос возвращает `cover_thumb_key` для альбома с обложкой и `NULL` без неё.
+- [x] 1.4 В `albums/service.go` пресигнить `cover_thumb_key` в `CoverThumbURL` в `List` и `Get`. Проверка: `GET /api/albums` в ответе содержит `cover_thumb_url` у альбома с обложкой.
+- [x] 1.5 В `photos/service.go` вызывать `SetCover` только при `cover_photo_id IS NULL`. Проверка: загрузка двух фото в новый альбом оставляет обложкой первое; после удаления фото-обложки cover становится `NULL`, следующая загрузка ставит новый.
+- [x] 1.6 Покрыть тестами store-join обложки и автообложку; проверить `cd backend && go test ./...`.
+
+## 2. Frontend: темы и каркас
+
+- [x] 2.1 Добавить токены `--chrome`/`--canvas`, перенастроить `dark` в графит и сделать его дефолтом (при отсутствии сохранённого выбора). Проверка: `cd frontend && npm run check` проходит, все три темы визуально согласованы.
+- [x] 2.2 Добавить `cover_thumb_url` в `lib/types.ts` и недостающие иконки (обложка, плотность/меню) в `lib/icons/registry.ts`. Проверка: `npm run check` без ошибок типов.
+- [x] 2.3 Разбить `+layout.svelte` на группы `(app)`/`(public)` и ввести `AppShell` (топбар, слот рейла, слот панели, центр = маршрут). Проверка: навигация между альбомами не перезагружает рейл; `/login`, `/invites/[token]`, `/share/[token]` рендерятся без каркаса.
+- [x] 2.4 Реализовать адаптивный каркас: рейл → drawer, панель → сворачиваемая секция, лента → горизонтальный скролл на узкой ширине. Проверка: при ширине меньше порога каркас управляем без горизонтального overflow.
+
+## 3. Рейл и создание альбома
+
+- [x] 3.1 Компонент `AlbumRail`: список альбомов с обложками, плейсхолдер, подсветка активного. Проверка: активный альбом выделен, остальные показывают обложку/плейсхолдер.
+- [x] 3.2 Кнопка создания `+` с хинтом и модалка ввода названия (замена инлайн-формы). Проверка: создание альбома через модалку, новый альбом становится активным; пустое название не создаёт альбом.
+
+## 4. Контекстная панель и обложки
+
+- [x] 4.1 Компонент `AlbumPanel`: превью обложки, название, счётчики, переходы к участникам/настройкам/ссылкам; управление обложкой только для owner/admin. Проверка: viewer не видит управление обложкой.
+- [x] 4.2 Модальный `CoverPicker` (сетка миниатюр) + `PATCH /albums/:id` с `cover_photo_id`/`clear_cover`. Проверка: назначение и сброс обложки отражаются в рейле, карточке и панели.
+- [x] 4.3 Действие «Сделать обложкой» в тулбаре просмотра (роль-gated). Проверка: из просмотра текущее фото становится обложкой альбома.
+
+## 5. Центр: сетка, плотность, загрузка
+
+- [x] 5.1 Сетка альбомов edge-to-edge с реальными обложками вместо карточек-заглушек. Проверка: карточки показывают `cover_thumb_url`, при отсутствии — плейсхолдер.
+- [x] 5.2 Регулятор плотности миниатюр в тулбаре сетки с сохранением в `localStorage`. Проверка: размер меняется и восстанавливается после перезагрузки.
+- [x] 5.3 Загрузка: иконочная кнопка в тулбаре, полноэкранный drag-overlay, зона-подсказка в пустом альбоме; прогресс компактной полосой. Проверка: перетаскивание и выбор файлов загружают фото, пустой альбом показывает подсказку.
+
+## 6. Просмотр как маршрут
+
+- [x] 6.1 Маршрут `albums/[id]/photos/[photoId]` и компонент `Viewer` (крупное фото, filmstrip, панель метаданных/комментариев, тулбар, клавиши ←/→/Esc). Проверка: переход из сетки открывает маршрут, адрес отражает фото, навигация синхронна с filmstrip.
+- [x] 6.2 Переиспользовать `Viewer` на `share/[token]` вместо инлайн-лайтбокса. Проверка: публичный просмотр использует тот же компонент без рейла/панели.
+
+## 7. Интеграция
+
+- [x] 7.1 Пройти сценарий: создать альбом → загрузить фото → задать обложку → открыть просмотр → открыть share-ссылку; проверить поведение на десктопе и узком экране.
+- [x] 7.2 Проверить `make build` (сборка фронтенда в `backend/static`, статический Go-бинарник) и `openspec validate "lightroom-workspace-ui"`.