diff --git a/docs/android-capacitor-oauth-template.md b/docs/android-capacitor-oauth-template.md
new file mode 100644
index 0000000..642a161
--- /dev/null
+++ b/docs/android-capacitor-oauth-template.md
@@ -0,0 +1,286 @@
+# Android + Capacitor 8 + OAuth — полный гайд по проблемам и решениям
+
+> **Контекст:** Vue SPA завёрнут в Capacitor 8. OAuth через внешний провайдер (gnexus-auth). Должно работать одинаково на Android и десктопе.
+> **Дата:** 2026-06-08. **Android:** 15 (API 35). **Capacitor:** 8.4.0.
+
+---
+
+## 1. Архитектура авторизации
+
+```
+┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
+│ Android App │────▶│ Chrome (full) │────▶│ gnexus-auth │
+│ (Capacitor WV) │ │ Intent.ACTION_VIEW│ │ (OAuth page) │
+└─────────────────┘ └──────────────────┘ └─────────────────┘
+ ▲ │ │
+ │ ▼ ▼
+ │ ┌──────────────────┐ ┌─────────────────┐
+ └──────────────│ /auth/callback │────▶│ /auth/mobile- │
+ intent:// │ (server OAuth │ │ bridge │
+ deep link │ callback) │ │ (bridge page) │
+ └──────────────────┘ └─────────────────┘
+```
+
+### Почему именно такой flow
+
+- **Custom Tabs** (`Browser.open()` из `@capacitor/browser`) — не поддерживает `intent://` URL. Браузер открывает CCT, авторизуется, но не может вернуть управление в приложение. **Нельзя использовать для deep link return.**
+- **Full Chrome** (`Intent.ACTION_VIEW`) — открывает полный Chrome с адресной строкой. Chrome нативно поддерживает `intent://` ссылки. После авторизации bridge page делает `window.location.href = "intent://auth/callback?token=...#Intent;scheme=shserv;package=com.gnexus.shserv;end"` — и управление возвращается в приложение.
+- **Capacitor `BridgeWebViewClient`** автоматически перехватывает навигацию на внешние хосты и открывает их через `Intent.ACTION_VIEW`. Поэтому в Vue-коде достаточно `window.location.href = buildServerUrl('/auth/login?return_to=/auth/mobile-bridge')` — без `Browser.open()`.
+
+---
+
+## 2. Проблема: OAuth не возвращает в приложение
+
+### Симптом
+Нажимаешь "Sign In", открывается браузер, авторизуешься, но приложение не получает токен. Пользователь остаётся в браузере.
+
+### Причина 1: Custom Tabs блокируют intent://
+`Browser.open({ url: '...' })` открывает Chrome Custom Tabs (или DuckDuckGo CCT). CCT — это sandboxed WebView, который не умеет обрабатывать `intent://` ссылки. Bridge page пытается `window.location.href = "intent://..."`, но CCT не передаёт это в систему.
+
+### Решение: не использовать Browser.open()
+```javascript
+// ❌ НЕЛЬЗЯ — открывает CCT, intent:// не сработает
+import { Browser } from '@capacitor/browser';
+Browser.open({ url: authUrl });
+
+// ✅ ПРАВИЛЬНО — Capacitor BridgeWebViewClient сам перехватывает
+// внешние хосты и открывает через Intent.ACTION_VIEW (full Chrome)
+window.location.href = buildServerUrl(
+ `/auth/login?return_to=${encodeURIComponent('/auth/mobile-bridge')}`
+);
+```
+
+### Причина 2: Сервер теряет returnTo
+На сервере `AuthController::callback()` вызывал `$service->handleCallback($code, $state)`, который **удаляет** OAuth state из PHP-сессии (`stateStore->forget($state)`). После этого код пытался прочитать `$_SESSION['gauth_state'][$state]['context']` — но state уже стёрт. `returnTo` всегда падал в `'/'`.
+
+### Решение: читать returnTo ДО handleCallback()
+```php
+public function callback()
+{
+ // ...
+
+ // ✅ Читаем returnTo ПОКА state живой в сессии
+ $context = $_SESSION['gauth_state'][$state]['context'] ?? [];
+
+ $service = new AuthService();
+ $user = $service->handleCallback($code, $state); // state тут стирается
+
+ // ...
+
+ $returnTo = $context['return_to'] ?? '/';
+ return $this->utils()->redirect($returnTo);
+}
+```
+
+---
+
+## 3. Проблема: Desktop сломался после добавления mobile
+
+### Симптом
+После добавления Capacitor-зависимостей десктопный клиент показывает `/#/mobile-setup` и требует указать адрес сервера. Либо авторизация цикличит.
+
+### Причина: `isNativeApp()` проверял `window.Capacitor !== undefined`
+```javascript
+// ❌ НЕЛЬЗЯ — window.Capacitor injects и в браузер (dev serve, live reload)
+export function isNativeApp() {
+ return typeof window !== "undefined" && window.Capacitor !== undefined;
+}
+```
+
+Capacitor runtime может присутствовать и в браузере (например, при `npm run dev` через Vite proxy). Десктоп думает что он нативное приложение:
+- Показывает `/#/mobile-setup` (router guard `hasServerUrl()`)
+- Редиректит на `/auth/mobile-bridge` с `intent://` (который в браузере не работает)
+- `initAccessToken()` не вызывается (был под `if (isNativeApp())`), токен не загружается
+
+### Решение: использовать `Capacitor.isNativePlatform()`
+```javascript
+import { Capacitor } from "@capacitor/core";
+
+export function isNativeApp() {
+ return Capacitor.isNativePlatform(); // true только на реальных iOS/Android
+}
+```
+
+Также: **всегда** вызывать `initAccessToken()` в `bootstrap()`, не под `if (isNativeApp())`. Эта функция сама внутри различает web (`localStorage`) и native (`Preferences`).
+
+---
+
+## 4. Проблема: Статус бар перекрывает интерфейс (Android 15)
+
+### Симптом
+Верхняя часть приложения (заголовок, логотип) залезает под статус бар. Статус бар прозрачный.
+
+### Причина: Android 15 (API 35) mandates edge-to-edge
+Android 15 **принудительно** включает edge-to-edge для всех приложений. Контент рисуется за статус баром и navigation bar. Это нельзя отключить стандартными `windowTranslucentStatus=false`.
+
+Capacitor 8 добавляет `SystemBars` plugin, который:
+1. Устанавливает `OnApplyWindowInsetsListener` на CoordinatorLayout
+2. Возвращает `systemBars insets = 0`, блокируя `fitsSystemWindows`
+3. Пытается управлять padding программно, но это ненадёжно
+
+### Решение: комплексный фикс трёх уровней
+
+#### A. Custom layout с `fitsSystemWindows="true"`
+Создать `res/layout/capacitor_bridge_layout_main.xml` — override'ит Capacitor'овский layout:
+```xml
+
+
+
+```
+
+#### B. MainActivity: сброс SystemBars listener + fitsSystemWindows
+```java
+public class MainActivity extends BridgeActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ setTheme(R.style.AppTheme_NoActionBar);
+ super.onCreate(savedInstanceState);
+
+ // Prevent edge-to-edge
+ Window window = getWindow();
+ WindowCompat.setDecorFitsSystemWindows(window, true);
+ window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+ window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+
+ // ✅ Сбрасываем OnApplyWindowInsetsListener от Capacitor SystemBars plugin
+ View webView = findViewById(com.getcapacitor.android.R.id.webview);
+ if (webView != null) {
+ ViewGroup parent = (ViewGroup) webView.getParent();
+ if (parent != null) {
+ ViewCompat.setOnApplyWindowInsetsListener(parent, null);
+ parent.setFitsSystemWindows(true);
+ }
+ }
+ }
+}
+```
+
+#### C. Theme opt-out + capacitor config
+`styles.xml` — оба стиля:
+```xml
+- true
+```
+
+`capacitor.config.json`:
+```json
+"android": {
+ "adjustMarginsForEdgeToEdge": "force"
+}
+```
+
+---
+
+## 5. Проблема: Splash screen размытый / расплющенный
+
+### Симптом
+Иконка на splash screen размытая (низкое разрешение) или расплющенная (неправильные пропорции).
+
+### Причина 1: PNG слишком маленькие (размытость)
+Capacitor по умолчанию генерирует splash PNG ~200×200px для `drawable/`. На xxxhdpi (1440×2560) это растягивается в ~7× — получается мыло.
+
+### Решение: правильные размеры per density
+```
+mdpi: 360×640 / 640×360 (logo 120px)
+hdpi: 540×960 / 960×540 (logo 180px)
+xhdpi: 720×1280 / 1280×720 (logo 240px)
+xxhdpi: 1080×1920 / 1920×1080 (logo 360px)
+xxxhdpi: 1440×2560 / 2560×1440 (logo 480px)
+```
+Генерировать из SVG через ImageMagick, центрировать логотип `gravity=center`.
+
+### Причина 2: Android 12+ SplashScreen API (расплющенность)
+Android 12+ (API 31+) использует SplashScreen API. Иконка должна быть **288dp** canvas с **192dp** content area. Если иконка меньше или неправильного формата — система растягивает/сплющивает.
+
+### Решение: SplashScreen API с per-density PNG
+`values-v31/styles.xml`:
+```xml
+
+```
+
+`drawable-*dpi-v31/ic_splash.png`:
+```
+mdpi: 288×288 (logo 192px)
+hdpi: 432×432 (logo 288px)
+xhdpi: 576×576 (logo 384px)
+xxhdpi: 864×864 (logo 576px)
+xxxhdpi: 1152×1152 (logo 768px)
+```
+
+Генерация:
+```bash
+magick -size ${canvas}x${canvas} xc:#0f172a \
+ logo.svg -resize ${logo}x${logo} \
+ -gravity center -compose over -composite \
+ drawable-${d}-v31/ic_splash.png
+```
+
+**Не использовать `layer-list` с маленьким `48dp`** — это неправильный размер для SplashScreen API.
+
+---
+
+## 6. Чеклист для следующего проекта
+
+### Capacitor setup
+- [ ] `capacitor.config.json` с `"appId": "com.company.app"` — уникальный
+- [ ] `androidScheme` не конфликтует с deep link scheme (shserv / https)
+- [ ] `CapacitorHttp.enabled: true` если нужен native HTTP
+- [ ] `adjustMarginsForEdgeToEdge: "force"` для Android 15+
+
+### Deep link (OAuth return)
+- [ ] `AndroidManifest.xml` intent-filter для `scheme://host/path`
+- [ ] Server bridge page с `intent://` URL
+- [ ] Vue `appUrlOpen` listener + `getLaunchUrl()` для cold-start
+- [ ] `window.location.reload()` после получения токена (чтобы bootstrap перезапустился)
+
+### Desktop vs Native branching
+- [ ] `isNativeApp()` через `Capacitor.isNativePlatform()`, НЕ `window.Capacitor`
+- [ ] `initAccessToken()` и `initServerUrl()` вызывать **всегда** в bootstrap
+- [ ] Storage abstraction (`storage.js`) — единая точка для Preferences/localStorage
+- [ ] Navigation abstraction (`navigation.js`) — единая точка для OAuth redirect
+
+### Android visual
+- [ ] Custom `MainActivity.java` с `setTheme()` ПЕРЕД `super.onCreate()`
+- [ ] Custom `capacitor_bridge_layout_main.xml` с `fitsSystemWindows="true"`
+- [ ] Сброс `OnApplyWindowInsetsListener` от SystemBars plugin в `onCreate()`
+- [ ] `windowOptOutEdgeToEdgeEnforcement=true` в обоих theme стилях
+- [ ] Splash PNG per density в `drawable-land-*dpi/` и `drawable-port-*dpi/`
+- [ ] Android 12+ splash API: `drawable-*dpi-v31/ic_splash.png` 288dp canvas
+- [ ] `colors.xml` с `bg_dark`, `toolbar_bg`, `accent`, `text_primary`
+
+### Server (OAuth callback)
+- [ ] `returnTo` читать из сессии **ДО** `handleCallback()` — state стирается
+- [ ] Bridge page `/auth/mobile-bridge` с `intent://` redirect
+- [ ] `sanitizeReturnTo()` разрешает `/auth/mobile-bridge` и same-origin URLs
+
+---
+
+## 7. Ключевые файлы проекта
+
+| Файл | Зачем |
+|------|-------|
+| `server/SHServ/Controllers/AuthController.php` | OAuth callback + mobileBridge bridge page |
+| `webclient/src/app/main.js` | Bootstrap, deep link handling, `initAccessToken()` |
+| `webclient/src/api/server-config.js` | `isNativeApp()`, `buildServerUrl()`, storage |
+| `webclient/src/api/storage.js` | Единый адаптер Preferences/localStorage |
+| `webclient/src/api/navigation.js` | `redirectToOAuth()`, `getOAuthReturnTo()` |
+| `webclient/capacitor.config.json` | Capacitor config, plugins, android scheme |
+| `android/app/src/main/java/.../MainActivity.java` | Theme override, fitsSystemWindows, edge-to-edge opt-out |
+| `android/app/src/main/res/layout/capacitor_bridge_layout_main.xml` | Override с fitsSystemWindows="true" |
+| `android/app/src/main/res/values/styles.xml` | Theme + windowOptOutEdgeToEdgeEnforcement |
+| `android/app/src/main/res/values-v31/styles.xml` | Android 12+ SplashScreen API |
+| `android/app/src/main/AndroidManifest.xml` | Deep link intent-filter |
+
+---
+
+*Написано после реального production-дебага. Все проблемы воспроизводились на Android 15 (API 35), Motorola, Capacitor 8.4.0.*