diff --git a/docs/superpowers/plans/2026-08-07-wcag-skrot-i-graf.md b/docs/superpowers/plans/2026-08-07-wcag-skrot-i-graf.md
new file mode 100644
index 000000000..06b988131
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-07-wcag-skrot-i-graf.md
@@ -0,0 +1,1600 @@
+# Skrót `/` i nawigacja po grafie — plan wdrożenia
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use
+> superpowers:subagent-driven-development (recommended) or
+> superpowers:executing-plans to implement this plan task-by-task. Steps use
+> checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Domknąć kryteria WCAG 2.1.4 (mechanizm wyłączania skrótu `/`),
+2.5.7 (nawigacja po grafie powiązań wskaźnikiem) i 2.1.1 (ta sama nawigacja
+klawiaturą).
+
+**Architecture:** Dwa niezależne moduły JS window-globalnych, wzorem
+`related-records-highlight.js` z poprzedniej iteracji. Pierwszy trzyma
+preferencję skrótów w `localStorage` i obsługuje przełącznik w stopce;
+handlery `/` pytają go przed reakcją. Drugi wystawia czyste funkcje nad
+instancją Cytoscape (`przesun`, `zoomuj`, `dopasuj`), z których korzystają
+zarówno przyciski nakładki, jak i handler klawiatury.
+
+**Tech Stack:** Django (szablony), JavaScript (moduły window-globalne, bez
+bundlera dla `skroty-klawiszowe.js`; `nawigacja.js` idzie przez esbuild do
+`cytoscape-bundle.js`), Cytoscape.js ^3.34.0, vitest, pytest, Playwright,
+SCSS + grunt.
+
+**Spec:** `docs/superpowers/specs/2026-08-07-wcag-skrot-i-graf-design.md`
+
+## Global Constraints
+
+- Wszystkie polecenia Pythona przez `uv run` — nigdy gołe `python`/`pytest`.
+- Max długość linii Pythona: 88 znaków (ruff). JS i szablonów nie dotyczy.
+- Testy: pytest, funkcje standalone (nie klasy), `@pytest.mark.django_db`
+ gdzie potrzebna baza; `model_bakery.baker.make` do tworzenia obiektów.
+- Testy JS: vitest, pliki w `tests/js/**/*.test.js`. Domyślne środowisko to
+ `node` — plik testujący kod używający `window`/`localStorage` musi
+ deklarować u siebie `// @vitest-environment jsdom`.
+- Komentarze Django `{# … #}` **jednoliniowe** — każda linia własne
+ otwarcie i zamknięcie. Wieloliniowy wycieka do wyrenderowanego HTML-a.
+- **NIE modyfikować istniejących migracji** w `src/*/migrations/`.
+- `pre-commit` uruchamiać BEZ ARGUMENTÓW. Nigdy `--all-files`, nigdy
+ `ruff check --fix` — problemy naprawiać ręcznie, po jednym.
+- Na hoście mogą działać cudze kontenery testowe: NIE uruchamiać
+ `make clean-testcontainers`.
+- Praca w worktree `/Volumes/SSD/Programowanie/bpp-wcag-faza2`, gałąź
+ `fix-wcag-2140-2570` (stacked na `fix-wcag-naprawy-stwierdzone`).
+- Po zmianach SCSS: `grunt build` (albo `make assets`).
+
+## Struktura plików
+
+**Tworzone:**
+
+| plik | odpowiedzialność |
+|---|---|
+| `src/bpp/static/bpp/js/skroty-klawiszowe.js` | preferencja skrótów + przełącznik |
+| `tests/js/skroty-klawiszowe.test.js` | testy vitest modułu preferencji |
+| `src/powiazania_autorow/static/powiazania_autorow/js/powiazania/nawigacja.js` | czyste funkcje nawigacji nad Cytoscape |
+| `tests/js/nawigacja-grafu.test.js` | testy vitest modułu nawigacji |
+| `src/bpp/static/scss/graf_powiazan.scss` | nakładka nawigacji + pierścień focusa |
+| `src/bpp/tests/test_wcag/test_skrot_szukania.py` | testy szablonowe części A |
+| `src/powiazania_autorow/tests/__init__.py` | pakiet testów (jeśli nie istnieje) |
+| `src/powiazania_autorow/tests/test_graf_dostepnosc.py` | testy szablonowe grafu |
+| `src/integration_tests/test_wcag_skrot_i_graf.py` | testy Playwright |
+| `src/bpp/newsfragments/wcag-skrot-i-graf.feature.rst` | newsfragment |
+
+**Modyfikowane:**
+
+| plik | zmiana |
+|---|---|
+| `src/django_bpp/templates/base.html:37-51` | **usunięcie** zduplikowanego handlera `/`; dodanie `
+```
+
+Bez `defer` — moduł musi być dostępny, gdy handlery sprawdzają preferencję
+przy naciśnięciu klawisza, a przełącznik w stopce podpina się po
+`DOMContentLoaded`.
+
+- [ ] **Step 4: Usuń zduplikowany handler z `base.html`**
+
+W `src/django_bpp/templates/base.html` usuń CAŁY blok (linie 37-51):
+
+```django
+
+```
+
+Handler w `global_search_modal.html` robi to samo i dodatkowo sprawdza stan
+modala. Zostawienie obu oznacza dwa wywołania `openGlobalSearch` na jedno
+naciśnięcie klawisza.
+
+- [ ] **Step 5: Dodaj warunek w `global_search_modal.html`**
+
+W `src/django_bpp/templates/global_search_modal.html`, linie 1047-1052,
+zamień:
+
+```javascript
+ // Check for "/" key (forward slash)
+ if (e.key === '/' && !$('#globalSearchModal').hasClass('show')) {
+ e.preventDefault();
+ openGlobalSearch(null, true); // Pass true to indicate keyboard trigger
+ }
+```
+
+na:
+
+```javascript
+ // Check for "/" key (forward slash)
+ // WCAG 2.1.4: skrot jednoznakowy musi dac sie wylaczyc.
+ // Guard `typeof` — gdy modul sie nie zaladuje, degradujemy do
+ // zachowania dotychczasowego zamiast rzucac TypeError.
+ var skrotyWl = (typeof window.bppSkrotyWlaczone !== 'function'
+ || window.bppSkrotyWlaczone());
+ if (e.key === '/' && skrotyWl
+ && !$('#globalSearchModal').hasClass('show')) {
+ e.preventDefault();
+ openGlobalSearch(null, true); // Pass true to indicate keyboard trigger
+ }
+```
+
+- [ ] **Step 6: Dodaj warunek w `browse/uczelnia.html`**
+
+Dwie zmiany w `src/bpp/templates/browse/uczelnia.html`.
+
+Pierwsza — handler zamykający baner (linie 134-141), zamień:
+
+```javascript
+ document.addEventListener('keydown', function(e) {
+ if (e.key === '/' && !$(e.target).is('input, textarea, select')) {
+```
+
+na:
+
+```javascript
+ document.addEventListener('keydown', function(e) {
+ var skrotyWl = (typeof window.bppSkrotyWlaczone !== 'function'
+ || window.bppSkrotyWlaczone());
+ if (e.key === '/' && skrotyWl && !$(e.target).is('input, textarea, select')) {
+```
+
+Druga — pokazywanie banera (linie 102-108). Baner reklamuje skrót
+(„Wciśnij klawisz `/`…"), więc przy wyłączonych skrótach nie może się
+pojawiać. Zamień:
+
+```javascript
+ if (!bannerDismissed || (dismissedDate && daysSince(dismissedDate) > 30)) {
+```
+
+na:
+
+```javascript
+ // WCAG 2.1.4: nie reklamuj skrotu, ktory uzytkownik wylaczyl.
+ var skrotyWl = (typeof window.bppSkrotyWlaczone !== 'function'
+ || window.bppSkrotyWlaczone());
+ if (skrotyWl
+ && (!bannerDismissed || (dismissedDate && daysSince(dismissedDate) > 30))) {
+```
+
+- [ ] **Step 7: Dodaj przycisk w stopce**
+
+W `src/django_bpp/templates/base_footer.html`, wewnątrz
+`
` zamykającym `#graf-wrapper`)
+- Modify: `src/powiazania_autorow/static/powiazania_autorow/js/powiazania/controls.js`
+- Create: `src/bpp/static/scss/graf_powiazan.scss`
+- Modify: `src/bpp/static/scss/common.scss`
+- Create: `src/powiazania_autorow/tests/__init__.py` (jeśli nie istnieje)
+- Test: `src/powiazania_autorow/tests/test_graf_dostepnosc.py`
+
+**Interfaces:**
+- Consumes: `przesun`, `zoomuj`, `dopasuj` z Task 3
+- Produces: siedem przycisków o identyfikatorach `graf-nav-gora`,
+ `graf-nav-dol`, `graf-nav-lewo`, `graf-nav-prawo`, `graf-nav-zoom-in`,
+ `graf-nav-zoom-out`, `graf-nav-dopasuj` (używane w Task 6)
+
+- [ ] **Step 1: Sprawdź, czy pakiet testów istnieje**
+
+```bash
+ls src/powiazania_autorow/tests/__init__.py 2>/dev/null \
+ || (mkdir -p src/powiazania_autorow/tests && touch src/powiazania_autorow/tests/__init__.py)
+```
+
+Uwaga: istniejące testy tej aplikacji leżą płasko
+(`src/powiazania_autorow/test_views.py`), ale nowy plik dotyczy dostępności i
+zyskuje na wydzieleniu.
+
+- [ ] **Step 2: Napisz testy szablonowe**
+
+Utwórz `src/powiazania_autorow/tests/test_graf_dostepnosc.py`:
+
+```python
+"""Dostępność grafu powiązań: nawigacja wskaźnikiem (2.5.7) i klawiaturą (2.1.1).
+
+Graf stoi na Cytoscape.js i do tej pory dawał się przesuwać wyłącznie
+przeciąganiem. Kryterium 2.5.7 wymaga alternatywy realizowanej pojedynczym
+wskaźnikiem — stąd przyciski. Sam kontener zyskuje `tabindex`, żeby te same
+funkcje dało się wywołać z klawiatury (2.1.1).
+"""
+
+from pathlib import Path
+
+SZABLON = (
+ Path(__file__).resolve().parents[1]
+ / "templates"
+ / "powiazania_autorow"
+ / "graf.html"
+)
+
+PRZYCISKI = [
+ "graf-nav-gora",
+ "graf-nav-dol",
+ "graf-nav-lewo",
+ "graf-nav-prawo",
+ "graf-nav-zoom-in",
+ "graf-nav-zoom-out",
+ "graf-nav-dopasuj",
+]
+
+
+def _tresc():
+ return SZABLON.read_text(encoding="utf-8")
+
+
+def test_wszystkie_przyciski_nawigacji_obecne():
+ tresc = _tresc()
+ for identyfikator in PRZYCISKI:
+ assert f'id="{identyfikator}"' in tresc, f"brak przycisku {identyfikator}"
+
+
+def test_kazdy_przycisk_ma_aria_label():
+ # Sam glif strzałki nic nie mówi czytnikowi ekranu.
+ tresc = _tresc()
+ fragmenty = tresc.split("