# Raport: podgląd meczu przed startem kolejki

Gałąź: `podglad-przed-startem` (nie scalona do `main`). Zadanie: gdy dla
kolejki meczu żadna drużyna nie ma jeszcze danych (`MatchView::hasAnyData()
== false`), pokazać w miejscu składów ostatnie znane składy i zużyte chipy
z poprzedniej, zamkniętej kolejki, zamiast pustki.

## Zmiany plik po pliku

### Nowe pliki

**`src/Fpl/Api/Dto/ChipDto.php`**
DTO pojedynczego chipa z `entry/{id}/history/`: `name` (kod API, np.
`bboost`), `event` (numer kolejki, w której padł).

**`src/Fpl/Api/Dto/EntryHistoryDto.php`**
DTO całej odpowiedzi `history/`, zredukowane do jedynego potrzebnego pola:
`list<ChipDto> $chips`. Reszta odpowiedzi (sezony `current`/`past`) świadomie
pominięta — nieużywana.

**`src/Fpl/Api/FplApiClient.php` (nowa metoda `entryHistory()`)**
Pobiera `entry/{id}/history/` i mapuje pole `chips`. Trzyma się konwencji już
ustalonej w pliku dla `entry()`/`picks()`: zwraca `null` przy 404 (brak
menedżera o danym id to normalny stan), rzuca `FplApiException` przy każdym
innym błędzie.

**`src/Fpl/EntryChipsResolver.php`**
Nowa usługa, wzorowana wprost na `App\Fpl\PlayingClubsResolver` (ten sam
mechanizm cache'u i ten sam komentarz uzasadniający): `forEntry(int $entryId):
?list<ChipDto>`. `null` = pobranie zawiodło (zalogowane jako `warning`),
pusta lista = drużyna nie użyła jeszcze żadnego chipa — te dwa stany są
celowo rozróżnione, bo szablon ma je pokazywać inaczej. Wynik jest pamiętany
w prywatnej tablicy `$cache` (klucz = id wpisu FPL) na czas życia usługi —
usługi Symfony są dzielone (singleton) w obrębie jednego żądania HTTP, więc
ta sama drużyna nie jest odpytywana dwa razy. Nic nie zapisuje do bazy —
zgodnie z decyzją zapisaną w zadaniu.

**`src/League/UpcomingMatchPreview.php`**
Wartość podglądu. Udostępnia dokładnie tę samą fasadę, jakiej oczekuje
istniejący szablon `fixture/_picks.html.twig` — `getMySquad()`,
`getOpponentSquad()`, `getFormat()`, `getGameweek()` (jako „fixture") oraz
`snapshotFor()`, `isClubPlayingNow()` (jako „view") — dzięki czemu ten sam
obiekt można podać szablonowi pod obiema nazwami i włączyć go bez żadnej
zmiany. `getGameweek()` zwraca **poprzednią** kolejkę, nie kolejkę meczu, więc
szablon renderuje składy jako zamrożony wynik (`gameweek.finished == true`),
a nie „na żywo". Dodatkowo `chipsFor(int $entryId): ?list<ChipDto>`.

**`src/League/UpcomingMatchPreviewBuilder.php`**
Orkiestruje: `GameweekRepository::findPreviousChecked()` → jeśli `null`,
zwraca podgląd „niedostępny" bez żadnego zapytania do API. W przeciwnym razie
woła `EntryGameweekSync::syncMany($entries, $poprzedniaKolejka, force: false)`
(kolejka zamknięta jest zamrożona, więc to pobranie wykona się raz), czyta
zapisane snapshoty przez `EntryGameweekRepository::findFor()`, i dla każdej
drużyny osobno pyta `EntryChipsResolver::forEntry()`.

**`templates/fixture/_upcoming_preview.html.twig`**
Blok stojący w miejscu, gdzie normalnie są składy. Górny panel (`.panel
.upcoming-preview`) tłumaczy po polsku, że to stan sprzed startu kolejki,
z numerem kolejki meczu i numerem kolejki, z której pochodzą dane — zawsze
oba numery obok siebie, żeby nie dało się tego pomylić z bieżącym składem.
Tam samo lista chipów każdej drużyny (numer kolejki, w której padł chip;
brak — wprost jako „żaden chip nieużyty"; awaria pobrania — wiersz danej
drużyny po prostu nie renderuje się wcale, reszta strony i tak działa).
Poniżej — **bez zmiany** — `{% include 'fixture/_picks.html.twig' with
{'view': preview, 'fixture': preview} only %}`, czyli te same kolumny co
zawsze. Gdy nie ma żadnej wcześniejszej zamkniętej kolejki (pierwsza kolejka
sezonu), pokazuje tylko krótkie zdanie i nic więcej.

**`tests/fixtures/api/entry_history_101.json`, `entry_history_no_chips.json`**
Atrapy `entry/{id}/history/`: pierwsza z dwoma chipami (`bboost` w kolejce 3,
`wildcard` w kolejce 4), druga z pustą listą.

**`tests/Unit/Fpl/EntryChipsResolverTest.php`**
Zwraca chipy, zwraca pustą listę bez chipów, zwraca `null` i loguje `warning`
przy awarii, jedno zapytanie HTTP na id drużyny mimo kilku wywołań.

**`tests/Integration/GameweekRepositoryTest.php`**
`findPreviousChecked()`: najbliższa wcześniejsza zamknięta kolejka (nie
najstarsza), pomija niezamknięte kolejki pomiędzy, `null` dla pierwszej
kolejki sezonu.

**`tests/Integration/UpcomingMatchPreviewBuilderTest.php`**
Buduje podgląd z rzeczywistą synchronizacją i realnymi chipami; drugie
wywołanie na już zamrożonej kolejce nie rusza API składów; pierwsza kolejka
sezonu zwraca podgląd „niedostępny" **bez żadnego zapytania HTTP** (puste
tablice odpowiedzi w atrapie — każde nieoczekiwane wywołanie od razu by
wysypało test); chipy pobierane raz na drużynę.

### Zmienione pliki

**`src/Repository/GameweekRepository.php`**
Nowa metoda `findPreviousChecked(Gameweek $current): ?Gameweek` — najbliższa
wcześniejsza kolejka z `dataChecked = true`.

**`src/Controller/MatchController.php`**
Wstrzyknięty `UpcomingMatchPreviewBuilder`. Podgląd liczy się **tylko**, gdy
`!$view->hasAnyData()` — rozegrana i trwająca kolejka nigdy go nie dotykają
(zero dodatkowych zapytań). Wynik trafia do szablonu jako `preview` (może być
`null`, gdy dane już są).

**`templates/fixture/_chip.html.twig`**
Mapowanie kodów chipów (`wildcard`/`freehit`/`bboost`/`3xc` → `WC`/`FH`/`BB`/
`TC` i pełne nazwy) wyciągnięte tu jako makra `label(code)`/`name(code)` —
wspólne miejsce dla każdego szablonu pokazującego chipy. Nieznany kod nadal
pokazuje się wprost (fallback `?? code`).

**`templates/fixture/_standings.html.twig`**
Lokalne tablice `chipLabels`/`chipNames` usunięte, użycie zamienione na
`chip.label(...)`/`chip.name(...)` z `_chip.html.twig`. Zachowanie identyczne
— potwierdzone przez niezmienione, dalej zielone testy tabeli kolejki.

**`templates/fixture/_match.html.twig`**
Jedyna zmiana zachowania: gdy `view.hasAnyData`, wszystko jak dawniej
(`_picks.html.twig` bez zmian). Gdy nie — zamiast tego
`_upcoming_preview.html.twig` z `preview` i `fixture` (kolejka meczu, do
tekstu wyjaśnienia). Sekcje tabeli kolejki i różnic w składach **nie są
ruszane** — zadanie ograniczyło blok do miejsca składów.

**`templates/fixture/show.html.twig`**
Dopisane przekazanie `preview` do `_match.html.twig`.

**`tests/Unit/Fpl/FplApiClientTest.php`, `tests/Functional/MatchControllerTest.php`**
Nowe testy opisane niżej.

## Dowód: testy padały przed implementacją

**`FplApiClient::entryHistory()`** — przed dopisaniem metody:

```
Error: Call to undefined method App\Fpl\Api\FplApiClient::entryHistory()
```
(4 błędy + 1 nieudany `testThrowsOnServerErrorForEntryHistory`, 5 nowych testów.)

**`EntryChipsResolver`** — przed dodaniem klasy:

```
Error: Class "App\Fpl\EntryChipsResolver" not found
```
(4 błędy, wszystkie 4 nowe testy.)

**`GameweekRepository::findPreviousChecked()`** — przed dopisaniem metody:

```
BadMethodCallException: Undefined method "findPreviousChecked". The method
name must start with either findBy, findOneBy or countBy!
```
(3 błędy, wszystkie 3 nowe testy.)

**`UpcomingMatchPreview`/`UpcomingMatchPreviewBuilder`** — przed dodaniem klas:

```
Error: Class "App\League\UpcomingMatchPreviewBuilder" not found
```
(wszystkie 4 nowe testy integracyjne.)

**Kontroler + szablony** — to jedyna zmiana, którą zaimplementowałem przed
napisaniem testów funkcjonalnych, więc czerwony stan potwierdziłem osobno:
cofnąłem `git stash` zmiany w `MatchController.php`, `_match.html.twig` i
`show.html.twig` (plus tymczasowo schowałem nowy `_upcoming_preview.html.twig`)
i uruchomiłem `tests/Functional/MatchControllerTest.php`:

```
1) testShowsUpcomingPreviewFromPreviousCheckedGameweekWhenCurrentGameweekHasNoDataYet
Failed asserting that the Crawler matches selector ".upcoming-preview".

2) testFirstGameweekOfSeasonShowsShortMessageInsteadOfPreview
Failed asserting that the Crawler matches selector ".upcoming-preview".

FAILURES!
Tests: 24, Assertions: 90, Failures: 2.
```

Po `git stash pop` (przywrócenie wdrożenia) oba testy przechodzą.

## Wynik całej suity

```
vendor/bin/phpunit
OK (302 tests, 1042 assertions)
```

283 istniejące + 19 nowych: 5 w `FplApiClientTest` (`entryHistory`), 4 w
`EntryChipsResolverTest`, 3 w `GameweekRepositoryTest`, 4 w
`UpcomingMatchPreviewBuilderTest`, 3 w `MatchControllerTest`. Żaden istniejący
test nie został zmieniony w sposób osłabiający asercję — wszystkie 283
przechodzą bez modyfikacji swojej logiki (dotknięte tylko przez wspólny
import w `_standings.html.twig`, bez zmiany wyniku).

Dodatkowo: `php bin/console lint:twig templates/fixture/` → „All 9 Twig files
contain valid syntax", `php bin/console lint:container` → „all services are
injected with values that are compatible with their type declarations",
`php -l` czyste na wszystkich nowych/zmienionych plikach PHP.

## Ile zapytań do API wykonuje widok podglądu przy ośmiu drużynach

Licząc wyłącznie koszt dodany przez tę funkcję (`UpcomingMatchPreviewBuilder`
+ `EntryChipsResolver`) — bez zapytań, które `MatchViewBuilder` i tak już
wykonywał wcześniej dla kolejki samego meczu:

- **Pierwsze wejście** dla danej pary (mecz, poprzednia kolejka), gdy
  poprzednia kolejka nie ma jeszcze zapisanych snapshotów: 1× `event/{gw}/live/`
  + 8× `entry/{id}/event/{gw}/picks/` + 8× `entry/{id}/history/` = **17
  zapytań**.
- **Każde kolejne wejście** — poprzednia kolejka jest zamknięta (`data_checked`),
  więc `EntryGameweekSync` uznaje zapisane snapshoty za zamrożone
  (`isFinalised()`) i nie odpytuje już `live()`/`picks()` w ogóle. Chipy z
  celowego założenia nie są zapisywane w bazie, więc są pobierane na nowo przy
  każdym żądaniu HTTP: **8 zapytań** (samo `entry/{id}/history/`, po jednym na
  drużynę, dzięki cache'owi w obrębie jednego żądania nigdy więcej niż raz na
  drużynę).

## Czego nie zweryfikowałem wzrokowo

Zgodnie z zasadą „nie używaj przeglądarki" — nie uruchamiałem aplikacji ani
nie robiłem zrzutów ekranu. Nie zweryfikowałem wzrokowo:
- rzeczywistego wyglądu panelu wyjaśnienia i listy chipów w przeglądarce
  (kolory, odstępy, zawijanie długich nazw drużyn),
- zachowania `<details>`/`<summary>` (rozwijanie/zwijanie kart składu) na
  realnym urządzeniu dotykowym,
- kontrastu kolorów na rzeczywistym ekranie (opieram się na tym, że użyłem
  wyłącznie już istniejących klas `.panel`, `.hint`, `.badge` i zmiennych
  z góry `app.css`, bez żadnej nowej reguły CSS).

**Przeliczenie szerokości dla ekranu 390px:** nowy szablon
(`_upcoming_preview.html.twig`) nie wprowadza ani jednej stałej szerokości w
pikselach — żadnego `width`, `min-width` ani stałej szerokości kolumny.
Używa wyłącznie:
- klas siatki Bootstrapa `row row-cols-1 row-cols-md-2 g-2` (poniżej progu
  `md` = 768px renderuje się jako jedna kolumna na całą szerokość rodzica,
  więc przy 390px nie ma ryzyka przepełnienia w poziomie),
- istniejących klas `.panel` (`padding: 14px 16px` — to dopełnienie, nie
  szerokość) i `.badge` (`display: inline-block`, szerokość zależna od
  treści, `font-size: 11px`),
- `_picks.html.twig` włączony bez zmian — jego układ kolumn (`row-cols-1` na
  bazowej szerokości) jest już pokryty istniejącymi testami i istniejącym
  raportem (`docs/raport-bootstrap.md`).

Suma stałych szerokości wprowadzonych przez tę zmianę: **0px** — nic nowego
nie mogło złamać układu na 390px, bo nic nowego nie ma stałej szerokości.

## Wątpliwości / decyzje warte odnotowania

- Gdy `hasAnyData() == false` z innego powodu niż „kolejka się nie zaczęła"
  (np. chwilowa awaria FPL w trakcie już trwającej kolejki), podgląd i tak się
  pokaże i powie „kolejka jeszcze się nie rozpoczęła" — zgodnie z wprost
  wskazanym w zadaniu warunkiem (`hasAnyData()` to „właściwy warunek"), bez
  rozróżniania przyczyny. Uznałem to za świadomy wybór z treści zadania, nie
  lukę.
- Blok chipów stoi nad `_picks.html.twig` jako osobna, płaska lista drużyn
  (a nie wewnątrz każdej karty `<details class="squad-picks">`) — bo zadanie
  wprost zabroniło pisania drugiego szablonu składów, a edycja współdzielonego
  `_picks.html.twig` (używanego też przez rozegrane i trwające kolejki)
  wykraczałaby poza zakres zadania.
- `EntryChipsResolver` traktuje 404 z `entry/{id}/history/` tak samo jak
  `entry()`/`picks()` — jako normalny stan (nieznany menedżer), nie awarię.
  W konsekwencji drużyna, dla której historia w ogóle nie istnieje, dostaje
  pustą listę chipów („żaden chip nieużyty"), a nie komunikat o awarii. To
  spójne z resztą klienta API, ale warto o tym wiedzieć.

---

## Uzupełnienie: różnice w podglądzie i skład oczyszczony z chipa

Dwa dodatkowe zamówienia od właściciela, zrealizowane na tej samej gałęzi
(`podglad-przed-startem`), po jego własnej zmianie w `_match.html.twig`
(przed startem kolejki znika tabela kolejki i sekcja różnic dla *nadchodzącego*
meczu — puste ramki z zerami; plakietka mówi teraz „kolejka jeszcze się nie
rozpoczęła" zamiast mylącego „Remis"/„kolejka w toku"). Ta zmiana była już
w drzewie roboczym, zweryfikowałem, że nic nie psuje (cała suita zielona), i
scommitowałem ją osobno przed dalszą pracą.

### Zmiany plik po pliku

**`src/League/ChipNeutralSnapshot.php` (nowy)**
Czysta, bezstanowa transformacja `from(EntryGameweek $snapshot): EntryGameweek`.
Sklad (kto gra, kto na ławce) czyta z **pozycji** zapisanej przy wystawieniu
(1-11 = jedenastka, 12-15 ławka) — to stała struktura, której ani chip, ani
automatyczna zmiana FPL nie przestawia (`AutomaticSubsApplier` zmienia tylko
mnożnik, nigdy pozycję). Kapitanem tej normalizacji (mnożnik 2) zostaje
zawodnik z jedenastki, którego **oryginalny** mnożnik był >= 2 — obejmuje to
zarówno zwykłą opaskę, jak i przypadek, gdy kapitan nie zagrał i FPL już w
danych z API przeniósł podwojenie na wicekapitana (opaska sama w sobie,
`isCaptain()`, nic nie przesądza — to tylko etykieta). Ławka zawsze dostaje
mnożnik 0, niezależnie od tego, co zapisał Bench Boost. Surowe punkty
zawodnika (`rawPoints`) i statystyki (gole, karty itd.) zostają nietknięte —
zmienia się tylko mnożnik i wyliczone z niego `effectivePoints`. Nagłówkowy
`netPoints` przeliczony jako suma `effectivePoints` znormalizowanej jedenastki
minus oryginalny koszt transferów (transfer to fakt historyczny, chip go nie
dotyczy) — inaczej nagłówek karty pokazywałby stary, zawyżony przez chip
wynik nad listą, która już go nie zawiera. `activeChip` na kopii ustawiony na
`null` — dla tego widoku żaden chip „nie jest już aktywny".

**Bezpieczeństwo dla Doctrine (twardy warunek z zadania):** `from()` tworzy
NOWE obiekty przez `new EntryGameweek(...)` / `new EntryGameweekPick(...)` —
nigdy nie modyfikuje przekazanego `$snapshot` ani nie dodaje nic do jego
kolekcji `picks`. Nowe obiekty nigdy nie trafiają do `EntityManager::persist()`.
Obszerny komentarz na górze klasy ostrzega wprost: „jeśli ktoś kiedyś doda tu
`persist()`, zapisze do bazy zmyślony, nigdy nie zagrany skład — nie
„naprawiaj" tego w ten sposób". Dowód, że to działa: `ChipNeutralSnapshotTest::
testDoesNotMutateOriginalSnapshot` (oryginalny obiekt w pamięci nietknięty) +
`MatchControllerTest::testViewingPreviewDoesNotMutateStoredSnapshot`
(peiny przejazd żądaniem HTTP, snapshot ponownie odczytany z bazy po
`$em->clear()` — pole po polu identyczny jak przed).

**`src/League/UpcomingMatchPreviewBuilder.php` (zmieniony)**
Nowa zależność `DifferentialAnalyzer $analyzer`. Po odczycie zapisanych
snapshotów (`$rawSnapshots`, nietknięte, tylko do odczytu) liczy
`$normalizedSnapshots = array_map(ChipNeutralSnapshot::from(...), $rawSnapshots)`
i **to właśnie znormalizowaną wersję** przekazuje dalej — zarówno do
`UpcomingMatchPreview` (dla składów), jak i do `DifferentialAnalyzer::analyze()`
(dla różnic), pogrupowaną po stronach przez nową prywatną `snapshotsWithData()`
(ten sam wzorzec co `MatchViewBuilder::snapshotsOf()`).

**`src/League/UpcomingMatchPreview.php` (zmieniony)**
Nowa publiczna właściwość `DifferentialReport $differentials` (domyślnie pusty
raport, gdy podgląd niedostępny). `$snapshots` w komentarzu klasy opisane jako
już oczyszczone z efektu chipa.

**`templates/fixture/_upcoming_preview.html.twig` (zmieniony)**
Górny akapit wyjaśnienia rozszerzony o zdanie o różnicach i o chipie
(„bez efektu żadnego użytego wtedy chipa... bo w nadchodzącej kolejce już nie
zadziała") — jedno miejsce z zastrzeżeniem zamiast rozrzuconych podpisów.
Dopisany, bez zmiany istniejącego szablonu:
`{% include 'fixture/_differentials.html.twig' with {'report': preview.differentials,
'fixture': preview} only %}` — tuż przed `_picks.html.twig`, w tej samej
kolejności co na stronie rozegranego meczu (różnice przed składami). `fixture`
to znowu `preview` (ten sam trik co przy składach) — `_differentials.html.twig`
czyta `fixture.gameweek.finished`, żeby zdecydować, czy pokazywać przerywane
pudełko „jeszcze nie grał"; podstawiona poprzednia (zakończona) kolejka daje
poprawne, zamrożone wartości zamiast pudełek „czeka na start".

**`tests/Unit/League/ChipNeutralSnapshotTest.php` (nowy)**
7 testów: Bench Boost daje 11 w składzie + 4 na ławce; ławka zawsze ma mnożnik
0 niezależnie od oryginału; Potrójny Kapitan ogranicza się do mnożnika 2;
podwojenie idzie za oryginalnym mnożnikiem, nie za flagą `isCaptain()`
(wicekapitan, który przejął opaskę); surowe punkty i statystyki nietknięte;
snapshot bez danych zostaje bez danych; oryginalny snapshot niezmutowany.

**`tests/Integration/UpcomingMatchPreviewBuilderTest.php` (zmieniony)**
Dopisana zależność `DifferentialAnalyzer` we wszystkich trzech miejscach
budujących `UpcomingMatchPreviewBuilder`. Dwa nowe testy: znormalizowany
snapshot Bench Boost ma 11/4 w podglądzie; różnice z podglądu pomijają
zawodnika, którego jedyne wystawienie to ławka drużyny Bench Boost.

**`tests/Functional/MatchControllerTest.php` (zmieniony)**
Cztery nowe testy pełnego przejazdu przez kontroler i szablony — dokładnie te
wymienione przez właściciela: Bench Boost (11 w składzie, 4 na ławce, w
renderowanym HTML), Potrójny Kapitan (`×2` w wierszu kapitana, brak `×3`),
różnice pomijające ławkowicza Bench Boost (`assertSelectorTextNotContains`
w `.buckets`), i test integralności bazy opisany wyżej.

### Dowód: testy padały przed implementacją

**`ChipNeutralSnapshot`** — przed dodaniem klasy:
```
Error: Class "App\League\ChipNeutralSnapshot" not found
```
(7 błędów, wszystkie nowe testy jednostkowe.)

**Normalizacja i różnice w `UpcomingMatchPreviewBuilder`** — przed zmianą
`build()`:
```
1) testNormalizesBenchBoostSquadToElevenStartersAndFourOnBench
Bench Boost nie moze przemycic pietnastki jako wyjsciowej jedenastki w podgladzie.
Failed asserting that actual size 15 matches expected size 11.

2) testDifferentialsFromPreviewIgnoreBenchPlayerOfBenchBoostSquad
TypeError: array_map(): Argument #2 ($array) must be of type array, null given
```

**Cały przejazd przez kontroler** — cofnąłem tymczasowo (`git stash` +
schowanie `ChipNeutralSnapshot.php`) zmiany w `UpcomingMatchPreview.php`,
`UpcomingMatchPreviewBuilder.php` i `_upcoming_preview.html.twig`, i
uruchomiłem cztery nowe testy funkcjonalne:
```
1) testBenchBoostSquadShowsElevenStartersAndFourOnBenchInPreview
Failed asserting that actual size 15 matches expected size 11.

2) testTripleCaptainSquadShowsDoubleMultiplierAtCaptainInPreview
Failed asserting that '...×315' contains "×2".

3) testDifferentialsInPreviewIgnoreBenchPlayerOfBenchBoostSquad
Failed asserting that ... contains "Różnice w składach".

4) testViewingPreviewDoesNotMutateStoredSnapshot
Failed asserting that actual size 15 matches expected size 11.

Tests: 4, Assertions: 18, Failures: 4.
```
Po przywróceniu wdrożenia (`git stash pop`) wszystkie cztery przechodzą.

### Wynik całej suity

```
vendor/bin/phpunit
OK (315 tests, 1114 assertions)
```

302 z poprzedniego etapu + 13 nowych: 7 w `ChipNeutralSnapshotTest`, 2 w
`UpcomingMatchPreviewBuilderTest`, 4 w `MatchControllerTest`. Dodatkowo
`lint:twig`, `lint:container` i `php -l` na wszystkich nowych/zmienionych
plikach — czyste.

### Czego nie zweryfikowałem wzrokowo

Jak poprzednio — bez przeglądarki i zrzutów ekranu. Dodatkowo dla tego
uzupełnienia: nie widziałem na oko, jak wygląda sekcja „Różnice w składach"
osadzona bezpośrednio pod panelem wyjaśnienia w podglądzie (kolejność
sekcji, odstępy) — układ i klasy CSS są dokładnie te same, których
`_differentials.html.twig` już używa na stronie rozegranego meczu (żadna
nowa reguła w `app.css`), więc ryzyko ograniczam do samej kolejności, nie do
samego wyglądu bloku.

### Wątpliwości / decyzje warte odnotowania

- **Formuła nagłówkowego `netPoints`** znormalizowanej karty
  (suma efektywnych punktów jedenastki minus koszt transferów) nie była
  wprost podana w zadaniu — wynika z zasady „punkty pokazywane przy
  zawodniku wynikają z ustalonego mnożnika" zastosowanej też do sumy, żeby
  nagłówek karty nie kłócił się z listą zawodników pod nim (inaczej Bench
  Boost dalej podbijałby widoczną sumę, mimo ukrytej ławki). Koszt
  transferów (hit) zostawiłem bez zmian — to decyzja o transferach z tamtej
  kolejki, niezwiązana z chipem.
- **`pointsOnBench` na znormalizowanej kopii** przeliczony jako suma
  surowych punktów zawodników zepchniętych na ławkę — pole nieużywane w tym
  widoku (sekcja `_standings.html.twig` jest ukryta przed startem kolejki),
  ale zostawienie tam starej, chipem zawyżonej wartości byłoby tym samym
  rodzajem kłamstwa, któremu ma zapobiegać cała ta zmiana.
- Etykiety opaski (`C`/`V` przy nazwisku) zostają na oryginalnym posiadaczu
  — normalizacja rusza wyłącznie mnożnik i wynikające z niego punkty, nie
  to, kto historycznie nosił opaskę.
