/* ==========================================================================
   WILCZY ŁUP — HUB-MANIFEST v5. Warstwa ruchu i sceny.
   Ładowany WYŁĄCZNIE na „/” (site.css niesie tokeny i resztę serwisu).

   ŹRÓDŁO: polecenie właściciela 2026-09-11 (po stykówce v3) nadpisujące otwarcie
   partytury KONCEPCJA_NARRACJI_hub_v3.md §3/§4. Reszta v3 (SEN 1–5, S6–S9, linia
   produktów) obowiązuje bez zmian i jest przeniesiona 1:1.

   ── CO ZOSTAŁO SKASOWANE ────────────────────────────────────────────────────
   S1 (Alfa 09 SIEDZI na bieli), T1 (cięcie na 04 SKOK i skok do dymka) oraz
   czarny wjazd kadru KV. Wraz z nimi: `.saga__veil`, `.scene--meet`, grupa G1
   i klatki t1-*. Strona zaczyna się kadrem KV — pierwsze malowanie JEST sceną.

   ── KONSTRUKCJA ─────────────────────────────────────────────────────────────
   KV, ZOOM, SEN 1–5, IRIS i OUTRO leżą w JEDNEJ przypiętej scenie („saga”,
   855 svh). v5 (polecenie właściciela 2026-09-16) dołożyła OUTRO: dawne sekcje
   S6 (WYBUDZENIE) i S7 (TWOJA CZĘŚĆ) nie są już osobnymi ekranami przewijanymi
   z góry na dół, tylko dwoma beatami W TYM SAMYM przypięciu. Przypięcie puszcza
   dopiero na klatce „pies przy misce” ze zdaniem „PIES NIE CZYTA SKŁADU. /
   Ufa ci. To twoje zadanie.” — i to jest ostatnia klatka opowieści.

   DYMEK JEST JEDEN — ten WPALONY W ZDJĘCIE. Warstwa wektorowa (okno `.saga__bubble`
   przycięte `clip-path` do zwektoryzowanego obrysu chmurki + `.saga__outline`)
   leży na nim 1:1 przy skali 1,0 i jest wtedy PRZEZROCZYSTA: widz widzi raster.
   Dopiero w zoomie zapala się wypełnienie — i tym samym gasi tekst „ŚNI, ŻE JEST
   MNĄ” oraz łeb wilka, bo oba są pikselami zdjęcia, nie osobnymi elementami.
   To jest cała mechanika przejścia, dosłownie wg polecenia właściciela.

   ZOOM ROBI KADR, NIE DYMEK. `transform` (skala + dryf na środek ekranu) siedzi
   na `.saga__frame`, a `transform-origin` stoi w ŚRODKU DYMKA (`--bu-ox/--bu-oy`,
   w procentach kadru, z danych zdjęcia). Okno dymka i obrys są DZIEĆMI kadru, więc
   dziedziczą tę samą transformację — raster i wektor nie mogą się rozjechać ani
   o piksel, bo jadą jednym transformem. Wnętrze snu dostaje przeciwskalę 1/k i
   jest wyśrodkowane na (50 %, 44,2 %) okna, czyli DOKŁADNIE na punkcie stałym
   skalowania — inaczej sen dryfowałby z ekranu przy dużych k.

   ── PARTYTURA R1 (2026-10-01, SKRYPT_zmian_sagi_R1 + decyzje właściciela) ──
   Budżet: KV 60 · ZOOM 60 · SEN 340 · IRIS 40 · OUTRO 140 = 640 svh (−25 % wobec
   855 z v5). Liczby stoją w content/pages/home.json (`saga_track`, `dream.beats[].vh`,
   `saga_outro.vh`); granice --b1…--b4, klatki snu i progi „Menu” liczy
   scripts/saga-score.mjs (`node scripts/saga-score.mjs --tabela`).
   ALFA NIGDY NIE ŚPI: pierwsza sylwetka po zoomie STOI (01 STOI, ok. 113 svh),
   pierwsze zdanie czytelne ok. 129 svh; w osi nie ma pozy leżącej/drzemiącej.
   Sen: ON ZASYPIA. JA RUSZAM. (stoi → krok) · TU NIE MA SMYCZY. (bieg) ·
   JEDEN TROP. ŻADEN INNY. (trop → skrada, noc) · JEDNA ZDOBYCZ. (pościg → skok) ·
   uczta bez zdania (siedzi nad zdobyczą) → iris na tę sylwetkę.
   Outro: ten sam pies (wybudzenie → [przebudzenie] → miska) i jedna puenta
   „JA MAM TROP. TY MASZ MISKĘ.” na klatce miski; puszczenie do S8.
   Opisy odcinków niżej (ZOOM, IRIS) są względne — ich procenty dotyczą osi
   lokalnej odcinka i nie zależą od długości toru.

   ZOOM · KV → SEN · „zbliżenie na dymek” ·60 svh (R1)· (oś lokalna 0–100 %)
       0–10 %   cisza — kadr bez ruchu. Tyle trwa przeczytanie dymka.
      10–30 %   wypełnienie dymka + obrys opacity 0 → 1: tekst i wilk GASNĄ,
                zostaje jednolita pomarańczowa bryła z czarną kreską
      10–100 %  skala kadru wokół środka dymka: 1 → 1,6 (30 %) → 3,8 (62 %)
                → `--bk-full` (100 %), równolegle dryf `--bu-tx/--bu-ty` (`--bp`),
                który sadza środek dymka na środku ekranu do 62 %
      74–82 %   fotografia opacity 1 → 0. Granica NIE jest z oka: zmierzona
                (260 zrzutów CDP, 4 psy × 5 rozmiarów ekranu) jako pierwszy
                postęp, przy którym pomarańcz kryje cały ekran — najgorszy
                przypadek puli to amstaff i (od re-cropu 2026-09-16) jamnik —
                oba z 72 %; k_full 9,2 i 10,0.
                Poprzednie 58 % gasiło zdjęcie, zanim pomarańcz je zakrył.
      66–80 %   obrys opacity 1 → 0 (jest wtedy poza kadrem; to polisa na
                wypadek, gdyby `--bk-full` okazał się dla jakiegoś ekranu ciasny)
      78–86 %   kadr lądowania opacity 0 → 1 — startuje po tym, jak fotografia
                zaczęła gasnąć, kończy przed cięciem na sen
      88 %      CIĘCIE: wnętrze snu opacity 0 → 1 na jednolitym pomarańczu
     100 %      cały ekran jednolicie pomarańczowy

       `--bk-full` jest PER ZDJĘCIE (dane, nie CSS): to najmniejsza skala, przy
       której prostokąt wpisany w chmurkę (0,852 × 0,580 okna, liczone
       z rastra ścieżki) kryje viewport — sprawdzone dla 17 rozmiarów ekranu od
       320×700 do 3440×1440, z zapasem 12 %. Wartości 7,0–10,0 (górny koniec to
       jamnik po re-cropie 2026-09-16: szersze okno kadru = mniejszy udział dymka
       w kadrze = większa skala potrzebna do zakrycia ekranu).

   SEN — 5 beatów WEWNĄTRZ dymka. Kolor, pozy i typografia PRZENIESIONE Z v2/v3
       BEZ ZMIAN — mają zamkniętą bramkę compliance i własną oś procentową.
       Maska nie rusza się przez cały sen (stoi na `--bk-full`): ani jednego
       przerysowania kształtu na 460 svh.

   IRIS · UCZTA → WYBUDZENIE · „ekran zamyka się kołem” ·40 svh (R1)· (oś lokalna 0–100 %)
       0–16 %   pod spodem stoi już `wybudzenie.jpg` (wszedł w zoomie, kiedy
                ekran był kryty — widz nie widział podmiany)
      16–88 %   `clip-path: circle(calc(var(--iris) * 1%) at 50 % 58 %)`, 84 → 0.
                Środek na 58 % wysokości = na wilku (L1), więc koło nie przecina
                go w ostatnich klatkach. Przy tym środku najdalsze są rogi GÓRNE
                i 84 % kryje je na każdej proporcji (granica: √2 × 0,58 = 82,0 %).
      88–100 %  cisza: pełnoekranowy kadr śpiącego psa

   OUTRO i SEN — patrz komentarz przy `@keyframes outro-*` (R1) i generator
   scripts/saga-score.mjs (oś snu liczona z wag beatów, nie wpisywana ręcznie).

   ── BUDŻET I ZAKAZY ─────────────────────────────────────────────────────────
   Wyłącznie `transform` i `opacity` (+ `clip-path`: chmurka statyczna, koło
   irysa animowane jednym promieniem). Zero animacji właściwości layoutu, zero
   filtrów per klatkę, zero animacji `background-color`. Zero rotacji na Alfie.
   Podmiana pozy NIGDY tweenem.
   prefers-reduced-motion / brak JS: cała saga rozpada się na statyczny stos
   w kolejności SKRYPT §17 (9 pasów, `data-strip` 1–9): KV · stoi/rusza · bieg ·
   trop/skrada · zdobycz/uczta · ten sam pies po śnie · miska · puenta · S8.
   ========================================================================== */

/* ---------- 0. ZMIENNE SCENY ---------- */

:root{
  --ease-out:cubic-bezier(0.22,1,0.36,1);
  --hub-pad:20px;
  /* Szerokość kadru KV. Cała pula ma proporcję 9:16, więc kadr jest jeden na
     wszystkie zdjęcia — dzięki temu okno dymka można trzymać w PROCENTACH kadru
     i przy skali 1,0 kryje ono raster 1:1 na każdej szerokości, bez ani jednego
     przeliczenia w JS. `--frame-h` liczy się z niej, nie odwrotnie. */
  --frame-w:min(100vw, calc(100svh * 9 / 16));
  --frame-h:calc(var(--frame-w) * 16 / 9);
  /* Domyślka na wypadek braku danych zdjęcia; realną wartość (7,0–10,0) wstawia
     szablon/skrypt rotacji per zdjęcie, bo zależy od tego, jaką część kadru
     zajmuje na nim dymek. */
  --bk-full:9.2;
}

@property --bk{syntax:"<number>";inherits:true;initial-value:1;}
/* ⚠️ KAŻDA animowana zmienna jest `<number>` i KAŻDA jej klatka kluczowa jest
   GOŁĄ LICZBĄ — bez `var()`, bez `calc()`, bez jednostki. To nie jest
   stylistyka, tylko warunek działania w Firefoksie; zmierzone 2026-09-16
   (Firefox 155, NATYWNY scroll, nie hak podglądu):

     1. Firefox nie interpoluje zarejestrowanych właściwości o składni
        `<length-percentage>` — animuje je dyskretnie.
     2. Firefox nie interpoluje TEŻ klatek, których wartość zawiera `var()`
        (np. `--btx: calc(var(--bu-tx) * 0.42)` → `var(--bu-tx)`) — nawet gdy
        zmienna jest `<number>`. Trzyma wtedy wartość klatki poprzedniej.

   Pomiar: przy postępie zoomu 40,3 %, między klatkami 30 % i 62 %, Chrome dawał
   `--btx: -11,2783`, Firefox `-7,80314` — dokładnie wartość klatki 30 %.
   Skutek na ekranie: raster i wektor dymka rozjeżdżały się o ~50 px przy k=2,3,
   a irys SKAKAŁ zamiast się zamykać.

   Stąd konstrukcja poniżej: animowane są wyłącznie liczby bezwymiarowe na
   literałach (`--bka`, `--bkb`, `--bp`, `--iris`), a wszystko, co zależy od
   zdjęcia (`--bu-tx`, `--bu-ty`, `--bk-full`), wchodzi dopiero w MIEJSCU UŻYCIA,
   w `calc()` poza animacją. Partytura się nie zmienia — zmienia się tylko to,
   co jest interpolowane, a co składane.
     --bka  skala kadru na odcinku 1 → 1,6 → 3,8 (literały)
     --bkb  domiar do `--bk-full` na ostatnim odcinku (0 → 1)
     --bp   postęp dryfu środka dymka na środek ekranu (0 → 0,42 → 1)
     --bk   NIE jest animowana: to złożenie dwóch powyższych, liczone co klatkę
            i dziedziczone przez okno snu i obrys. */
@property --bka{syntax:"<number>";inherits:true;initial-value:1;}
@property --bkb{syntax:"<number>";inherits:true;initial-value:0;}
@property --bp{syntax:"<number>";inherits:true;initial-value:0;}
@property --iris{syntax:"<number>";inherits:true;initial-value:84;}

/* `clip`, nie `hidden`: `overflow-x:hidden` robi z <html>/<body> KONTENER
   SCROLLA, a wtedy view-timeline mierzy postęp względem czegoś, co się nie
   przewija — oś zastyga na 100 % i sen nigdy nie rusza (zdiagnozowane
   2026-09-08 w Chrome 140). `overflow-anchor:none` — scroll anchoring inaczej
   cofa viewport na scenach transformowanych scrollem. */
html.hub, html.hub body{overflow-x:clip;}
html.hub{overflow-anchor:none;-webkit-text-size-adjust:100%;}
html.hub body{background:var(--wl-black);}

.hub main{display:block;}
/* ⚠️ 1×1 px, NIE 0×0 (zmierzone 2026-09-16, Firefox 155). Przy zerowym rozmiarze
   SVG-a Firefox nie buduje geometrii jego dzieci: `getBBox()` ścieżki w
   `<clipPath clipPathUnits="objectBoundingBox">` zwracał [0,0,0,0] (Chrome
   [0,0,1,1]), więc `clip-path: url(#wl-dymek)` MILCZĄCO nie działał — computed
   style pokazywał poprawny `url("#wl-dymek")`, a wypełnienie dymka malowało się
   jako PROSTOKĄT zamiast chmurki przez cały zoom. Jeden piksel poza ekranem
   wystarczy, żeby geometria się policzyła; `clipPath` i tak nigdy nie jest
   malowany, a element jest `position:absolute`, więc nie wchodzi w układ. */
.wl-defs{position:absolute;top:-1px;left:-1px;width:1px;height:1px;overflow:hidden;pointer-events:none;}

/* ---------- 1. TYPOGRAFIA SCEN ---------- */

.hub-display{
  font-family:var(--wl-display);font-weight:900;text-transform:uppercase;
  line-height:0.92;letter-spacing:-0.01em;margin:0;overflow-wrap:break-word;
  font-size:clamp(2.5rem,1.5rem + 4.6vw,5rem);
  max-width:13ch;color:var(--wl-black);
}
.hub-display--big{font-size:clamp(3.25rem,2rem + 6.4vw,7rem);max-width:9ch;}
.hub-sub{font-size:1.0625rem;line-height:1.45;max-width:32ch;margin:14px 0 0;}

/* ---------- 2. NAGŁÓWEK NA HUBIE (Z4) — ten sam partial i ta sama belka co na podstronach ---------- */
/* ⚠️ ZMIANA 2026-09-11 (ux-ui-director poz. 14), nazwana wprost jako odstępstwo od partytury §6.2/Z4: nawigacja jest widoczna OD
   PIERWSZEJ KLATKI, nie od S8/S9. Powód użytkowy: hub ma ~15 ekranów, a wejście z kodu QR na słoiku to użytkownik, który przyszedł po
   produkt — ukrycie jedynego wyjścia na jedenaście ekranów zostawia go bez skrótu.
   UNIFIKACJA 2026-09-29 (A1, decyzja właściciela): pierwszy ekran NADAL NIE MA BELKI — nad sagą stoi sama plakietka i dwa przyciski
   pełnym wypełnieniem wprost na zdjęciu (zero kafli, obrysów, białych teł), a klasa `nav-on` (IO na S8) zmienia już WYŁĄCZNIE tło:
   przezroczyste → biała belka z kreską 2 px, identyczna jak na podstronach (ten sam układ, wnętrze 1200 px). Kolor wypełnień i wersję logo
   wybiera TON kadru: `html[data-kv-tone=dark]` (przyciski: Menu i SKLEP białe z czarnym tekstem; jasny: oba czarne) i `html[data-kv-logo=negatyw]` (logo negatyw) —
   oba ustawia skrypt rotacji z pól `kv_pool[]` (`logo`, `tone`), zmierzonych na kadrach (luminancja obszaru pod elementem). Bez JS: statyczna
   biała belka (jak podstrony).
   Kolizja z dymkiem: kontrolki stoją w pasie 12–60 px od góry; górna krawędź dymka jest najwyżej na ~65 px (macierz 9 kadrów × 3 rozmiary). */
.hub .wl-header{
  position:fixed;top:0;left:0;right:0;z-index:50;
  background:transparent;border-bottom-color:transparent;
}
.js.hub .wl-header{transition:background .32s var(--ease-out);}
.js.hub.nav-on .wl-header{background:var(--wl-white);border-bottom-color:var(--wl-black);}
/* Menu na hubie: przed `nav-on` desktop pokazuje TYLKO logo + „Menu” + SKLEP (pozostałe linki wchodzą z `nav-on`); „Menu” otwiera ten sam
   arkusz co na mobile. Bez JS (brak klas js/nav-js) pełny rząd jest widoczny od początku — sceny i tak są wtedy statycznym stosem. */
.hub.nav-js:not(.nav-on) .wl-nav .wl-menu-btn{display:inline-flex;}
.hub.nav-js:not(.nav-on) .wl-nav__list li:not(.wl-nav__shop-li){display:none;}
/* TON KADRU (A1). Wyłączone przy reduced-motion: tam nagłówek jest zwykłą białą belką (§17). */
@media (prefers-reduced-motion:no-preference){
  html[data-kv-logo="negatyw"].hub:not(.nav-on) .wl-logo .wl-logo__pelna{display:none;}
  html[data-kv-logo="negatyw"].hub:not(.nav-on) .wl-logo .wl-logo__neg{display:block;}
  /* ciemny kadr/faza: OBA przyciski (Menu i SKLEP) białe z czarnym tekstem; jasny: oba czarne z białym tekstem (domyślne `.wl-btn`).
     FIX ROUND 1 (2026-09-29, OCENA_BRAND_wdrozenia_v1 pkt 3): pomarańczowy SKLEP na zdjęciu konkurował z dymkiem snu (jedyny pomarańcz kadru),
     a zdjęcie nie jest płaską czernią, więc reguła `.wl-on-black` (pomarańcz) tu nie obowiązuje — pomarańczowy SKLEP zostaje tylko na płaskiej
     czerni (pas, stopka). Hover w obu tonach = blok pomarańczowy z czarnym tekstem (jak wszędzie w systemie). */
  html[data-kv-tone="dark"].hub:not(.nav-on) .wl-header .wl-btn{--bg:var(--wl-white);--fg:var(--wl-black);--bg-h:var(--wl-orange);--fg-h:var(--wl-black);}
}
/* KOLIZJA MOBILNA (OCENA_UX_seo-rafal_v1.md §1, wariant b zawężony). Na telefonie dymek stoi raz po lewej, raz po prawej, więc ŻADNE
   miejsce u góry nie jest bezpieczne; belka z logo i „Menu” nachodziła na dymek w 9/9 kadrów. Przed `nav-on` (cała saga) logo znika
   (`visibility:hidden` — wypada też z kolejności Tab), a to samo `button[data-menu-btn]` stoi jako przycisk w prawym dolnym rogu ekranu,
   nad `env(safe-area-inset-*)` (pełne wypełnienie wg tonu kadru, bez obrysu — B1). Z `nav-on` reguła przestaje działać: przycisk wraca do
   belki u góry (obok SKLEPU) bez animacji ruchu (przejście ma tylko `background`), fokus zostaje na tym samym elemencie. Arkusz (z-index 60)
   bez zmian. `reduce`: stan z §17 (belka `static`).
   Zakres do 1023 px, bo 768–1023 to nadal kadry PIONOWE (pomiar: buldog/jamnik, dymek po prawej stronie, kolizja z belką u góry).
   R3 (2026-10-01, bloker telefonu): JEDNA POZYCJA przez całą sagę — prawy dolny róg. Dawna „wysoka” pozycja (`data-up`, +160 px w ~25–89 %
   sagi) skasowana: w realnym scrollu przycisk skakał o 160 px, a przy 375×667 „JEDNA ZDOBYCZ.” i tak wchodziło pod niego. Kolizję wyklucza
   teraz strefa ochronna po prawej stronie każdego zdania sagi (`--menu-safe`, §6 niżej) — z konstrukcji, bez skryptu i bez progów. */
@media (max-width:1023px) and (prefers-reduced-motion:no-preference){
  .js.hub.nav-js:not(.nav-on) .wl-header{pointer-events:none;}
  .js.hub.nav-js:not(.nav-on) .wl-logo{visibility:hidden;}
  .js.hub.nav-js:not(.nav-on) .wl-nav{
    position:fixed;right:max(12px,env(safe-area-inset-right));
    bottom:calc(16px + env(safe-area-inset-bottom));z-index:50;pointer-events:auto;
  }
}
@media (max-width:1023px) and (prefers-reduced-motion:no-preference){
  /* w trakcie sagi (telefon i tablet pionowo) stoi samo „Menu” (SKLEP wraca do belki z `nav-on`, a w arkuszu jest zawsze). FIX ROUND 2
     (2026-09-29): na 768–1023 SKLEP stał na styku czarnej kolumny letterboxu i zdjęcia, gdzie żaden ton nie daje 3:1 na całej powierzchni. */
  .js.hub.nav-js:not(.nav-on) .wl-nav__shop-li{display:none;}
}
/* Bez JS: zawijany rząd linków nie stoi `fixed` nad kadrem — zwykła biała belka w przepływie. */
html:not(.js).hub .wl-header{position:static;background:var(--wl-white);border-bottom-color:var(--wl-black);}

/* ---------- 3. SCENA — wspólny szkielet ---------- */

.scene{
  position:relative;min-height:calc(var(--vh,100) * 1svh);
  overflow:clip;display:grid;align-content:center;
  padding:88px var(--hub-pad) 56px;
}
.scene__inner{position:relative;z-index:2;width:100%;max-width:1120px;margin:0 auto;}
.scene__gate{margin:10px 0 0;}

/* wejścia narracyjne — TRIGGER (czas), nie scrub */
.js .fx{transition:opacity .58s var(--ease-out) var(--d,0ms),transform .58s var(--ease-out) var(--d,0ms);}
.js .scene:not(.on) .fx,
.js .manifest:not(.on) .fx,
.js .linia:not(.on) .fx,
.js .pas:not(.on) .fx,
.js .stado:not(.on) .fx{opacity:0;transform:translate3d(0,18px,0);}
.js .scene:not(.on) .alfa{transform:translate3d(0,10px,0) scale(0.94);}
.js .scene:not(.on) .photo{transform:scale(1.05);}

/* ==========================================================================
   4. SAGA — KV · ZOOM · SEN · IRIS na jednej przypiętej scenie
   ========================================================================== */

/* ── STAN BAZOWY = STATYCZNY (bez JS). Saga jest zwykłym stosem: kadr KV
      (z własnym, rastrowym dymkiem — to jest „dymek jako zwykła sekcja”)
      i pięć kart snu. Nic nie jest ukryte, nic nie wymaga skryptu. ── */

.saga{position:relative;background:var(--wl-black);}
.saga__back,.saga__band{display:none;}
.saga__iris{position:relative;}
.saga__frame{position:relative;background:var(--wl-black);}
.saga__photo{margin:0;}
.saga__photo img{display:block;width:100%;height:auto;}
/* H2b (znalezione przy dowodzie na luki, 2026-09-16): bez JS kadr KV niesie
   `<noscript>`, ale pusty `<picture data-kv-pic>` (jego `src` ustawia dopiero
   skrypt rotacji) NADAL rezerwuje pudełko z proporcji atrybutów — czyli drugi,
   CZARNY ekran tuż nad pierwszą klatką opowieści (zmierzone: 375×667 px przy
   375 px szerokości). Bez skryptu ten element nie ma czego pokazać, więc go nie
   ma. `.js` przywraca go bez zmian. */
.saga__photo picture[data-kv-pic]{display:none;}
.js .saga__photo picture[data-kv-pic]{display:block;}
.saga__wake{margin:0;background:var(--wl-black);}   /* R1: pkt 6 stosu statycznego — ten sam pies po śnie */
.saga__wake img,.saga__awake img{display:block;width:100%;height:auto;}
.saga__awake{margin:0;display:none;background:var(--wl-black);}
.saga__awake[data-on]{display:block;}
/* Gdy pies ma kadr przebudzenia, stos statyczny pokazuje JEGO, nie kadr śpiący. */
.saga[data-awake] .saga__wake{display:none;}
/* R3: bez JS kadr wybudzenia bez własnego pliku to to samo zdjęcie co pas 1 — nie dublujemy śpiącego psa w stosie. */
html:not(.js) .saga__wake[data-same-as-kv]{display:none;}
.saga__outline{display:none;}           /* raster KV ma własny obrys */
.saga__bubble{position:relative;}
.saga__bubble-fill{display:none;}
/* R1c — WARSTWA DYMKA (pary tego samego psa: zdjęcie snu jest czyste, dymek to osobny raster `dymek-L|P`).
   Stan bazowy = statyczny: figura KV jest pudełkiem zdjęcia, warstwa stoi w oknie chmurki w procentach PLIKU
   (`--bs-*`), poszerzonym o margines warstwy (`--ba-*`: przesunięcie i rozmiar w jednostkach okna). */
.saga__photo--kv{position:relative;}
.saga__photo .saga__bubble-art{
  position:absolute;display:block;max-width:none;pointer-events:none;
  left:calc(var(--bs-left) - var(--bs-w) * var(--ba-l));top:calc(var(--bs-top) - var(--bs-h) * var(--ba-t));
  width:calc(var(--bs-w) * var(--ba-w));height:calc(var(--bs-h) * var(--ba-h));
}
/* OUTRO bez JS: klatka końcowa i dwa zdania są zwykłymi kartami na końcu stosu.
   Nic nie jest ukryte — `.saga__bowl` to po prostu figura ze zdjęciem.
   `.saga__cut` (płaszczyzna cięcia) jest CZYSTO ruchowa: bez JS nie ma czego
   ciąć, więc jej po prostu nie ma. */
.saga__cut{display:none;}
.saga__bowl{margin:0;position:relative;background:var(--wl-black);}
.saga__bowl img{display:block;width:100%;height:auto;}
.saga__bowl-scrim{display:none;}
.saga__say{padding:26px var(--hub-pad) 34px;background:var(--wl-black);}
.saga__say .hub-display{color:var(--wl-white);max-width:14ch;}
/* Zdanie po zdaniu w osobnym wierszu (filtr `sentenceLines`): „JA MAM TROP. / TY MASZ MISKĘ.” */
.hub-display .ln{display:block;}

/* ── STAN ANIMOWANY (tylko z JS) ───────────────────────────────────────────── */

.js .saga{height:calc(var(--saga-vh,775) * 1svh);background:var(--wl-black);}
.js .saga__stage{
  position:sticky;top:0;height:100svh;overflow:clip;background:var(--wl-black);
}
.js .saga__back{display:block;position:absolute;inset:0;z-index:0;background:var(--wl-black);}

/* Pasy letterboksu w kolorze krawędzi zdjęcia. Na szerokim ekranie kadr jest
   ograniczony WYSOKOŚCIĄ, więc `100svh - --frame-h` schodzi do zera i pasy
   znikają same — boki zostają czarne, bez ani jednego media query. */
/* `+ 1px` NADMIARU, nie „ładna” liczba (ux-ui-director, runda 2, LOW „pasek pod
   headerem”): wysokość pasa liczy się z `(100svh - --frame-h) / 2`, a kadr stoi
   na `top:50%` z ujemnym marginesem `--frame-h / -2`. Obie formuły są poprawne
   i mimo to NIE STYKAJĄ SIĘ co do piksela — na 375×812 pas kończy się na
   72,66 px, a kadr zaczyna na 72,67 px. W tej szczelinie 0,013 px prześwituje
   czarne `.saga__back` i przy DPR 2 wychodzi z tego CIEMNA KRESKA na jeden
   piksel urządzenia, na górnej i dolnej krawędzi zdjęcia (zmierzone na
   NATYWNYM scrollu, nie pod hakiem podglądu: wiersz 146 z 1624 spadał z 247 na
   198). Pas dostaje więc 1 px zapasu i WSUWA SIĘ POD zdjęcie — zdjęcie jest
   nieprzezroczyste i leży wyżej (`.saga__iris` z-index 2), więc nadmiar nigdy
   nie jest widoczny, a szczelina znika z definicji. Zaokrąglenia nie da się
   usunąć „dokładniejszą” formułą: to ten sam rozjazd, co przy 1/3 piksela. */
.js .saga__band{
  display:block;position:absolute;left:0;right:0;z-index:0;
  height:calc(max(0px, calc((100svh - var(--frame-h)) / 2)) + 1px);
}
.js .saga__band--top{top:0;background:var(--kv-top,var(--wl-black));}
.js .saga__band--bottom{bottom:0;background:var(--kv-bottom,var(--wl-black));}

/* Kadr lądowania — pełny ekran, POD irysem. Wchodzi w trakcie zoomu. */
.js .saga__wake{
  display:block;position:absolute;inset:0;z-index:1;margin:0;opacity:0;background:transparent;
}
/* Przebudzenie (R1, kontrakt assetów): nad kadrem wybudzenia, ta sama geometria
   (`--wake-fit/--wake-pos`), zapalane w OUTRO. Bez `data-on` nie istnieje. */
.js .saga__awake{position:absolute;inset:0;z-index:1;margin:0;opacity:0;background:transparent;}
.js .saga__awake:not([data-on]){display:none;}
.js .saga[data-awake] .saga__wake{display:block;}
.js .saga__wake picture,.js .saga__awake picture{display:block;width:100%;height:100%;}
/* `--wake-fit` / `--wake-pos` (H1). Kadrem lądowania jest albo dedykowane ujęcie
   wybudzenia (amstaff: wybudzenie.jpg, POZIOME — `cover` wypełnia ekran), albo
   kadr KV wylosowanego psa (PIONOWY 9:16 — wtedy `contain`, czyli ta sama
   geometria co kadr KV: prostokąt 9:16 na czerni, z pasami letterboksu w kolorze
   krawędzi zdjęcia pod spodem). `cover` na kadrze 9:16 dawał na desktopie
   kilkukrotne powiększenie nosa psa zamiast kadru — sprawdzone zrzutem 1440×900.
   `--wake-pos` celuje w łeb psa tam, gdzie kadrowanie w ogóle ma luz.
   Obie domyślki = wartości PIERWSZEGO wpisu puli, więc bez JS nic się nie zmienia. */
.js .saga__wake img,.js .saga__awake img{
  width:100%;height:100%;
  object-fit:var(--wake-fit,contain);object-position:var(--wake-pos,center);
}

/* IRIS — przycina wszystko powyżej kurczącym się kołem. Osobny element, bo na
   jednym nie da się mieć dwóch clip-path (chmurka na oknie, koło na scenie).
   R1: wysokość środka koła przyszła do danych (`saga_track.iris_cy`, dziś 52 %:
   ostatnia poza to SIEDZĄCA sylwetka uczty, wyższa niż leżąca 13 TARZA, dla której
   liczono 58 %; storyboard R1 kl. 20–21: koło zamyka się na łbie/tułowiu). Promień
   startowy 84 % kryje rogi z zapasem także przy 52 % (granica √2 × 0,52 = 73,5 %).
   L1 (historia): środek koła zszedł z geometrycznego środka ekranu na 58 % wysokości —
   czyli NA WILKA (poza 9 stoi na 55–60 %). Dzięki temu koło nie przecina go
   w ostatnich klatkach, a ostatni punkt pomarańczu wypada na sylwetce; pies
   odsłania się od łba, nie od pościeli. */
.js .saga__iris{
  position:absolute;inset:0;z-index:2;
  clip-path:circle(calc(var(--iris,84) * 1%) at 50% var(--iris-cy,58%));
}

/* Kadr: prostokąt 9:16 wpisany w ekran, wyśrodkowany MARGINESAMI, nie
   transformem — `transform` jest w całości zarezerwowany dla zoomu. */
.js .saga__frame{
  position:absolute;top:50%;left:50%;
  width:var(--frame-w);height:var(--frame-h);
  margin-left:calc(var(--frame-w) / -2);margin-top:calc(var(--frame-h) / -2);
  background:transparent;aspect-ratio:auto;
  transform-origin:var(--bu-ox,50%) var(--bu-oy,50%);
  --bk:calc(var(--bka) + (var(--bk-full) - 3.8) * var(--bkb));
  transform:translate(calc(var(--bu-tx,0) * var(--bp) * 1%),calc(var(--bu-ty,0) * var(--bp) * 1%))
            scale(var(--bk));
}
.js .saga__photo{position:absolute;inset:0;}
.js .saga__photo picture{display:block;width:100%;height:100%;}
.js .saga__photo img{width:100%;height:100%;object-fit:cover;object-position:center;}
/* W ruchu warstwa dymka stoi w oknie chmurki w procentach KADRU (`--bu-*`, te same liczby co okno wektorowe
   `.saga__bubble`), więc raster i wektor jadą jednym transformem zoomu i nie rozjeżdżają się ani o piksel. */
.js .saga__photo .saga__bubble-art{
  left:calc(var(--bu-left) - var(--bu-w) * var(--ba-l));top:calc(var(--bu-top) - var(--bu-h) * var(--ba-t));
  width:calc(var(--bu-w) * var(--ba-w));height:calc(var(--bu-h) * var(--ba-h));object-fit:fill;
}

/* OKNO DYMKA — dziecko kadru, więc dziedziczy zoom. Przy skali 1,0 leży
   dokładnie na dymku wpalonym w zdjęcie i jest przezroczyste. */
.js .saga__bubble{
  position:absolute;
  left:var(--bu-left);top:var(--bu-top);width:var(--bu-w);height:var(--bu-h);
  /* `path()` w pikselach wstawia skrypt rotacji (`--bu-clip`) — powód
     w komentarzu przy `clipFor()` w src/index.njk: Firefox ignoruje
     `clip-path: url(<clipPath>)` pod przodkiem z transformacją, a kadr sagi
     jest skalowany zoomem. `url()` zostaje jako zapasowe wyjście. */
  clip-path:var(--bu-clip, url(#wl-dymek));
  transform:scaleX(var(--bu-mirror,1));transform-origin:50% 50%;
}
.js .saga__bubble-fill{
  display:block;position:absolute;inset:-2%;background:var(--wl-orange);opacity:0;
}
/* Przeciwskala: wnętrze jest pudełkiem wielkości ekranu, wyśrodkowanym na
   PUNKCIE STAŁYM skalowania (50 %, 44,2 % okna = `--bu-ox/--bu-oy` w układzie
   okna) i pomniejszonym 1/k. Przy okazji odwraca lustro okna, żeby sen nigdy
   nie był odbity. */
/* +6 px OVERSKANU (ux-ui-director, runda 2): wnętrze jest przeciwskalowane 1/k,
   więc jego krawędź po zaokrągleniu do piksela potrafi wypaść 1–2 px wewnątrz
   okna dymka — a pod spodem leży pomarańczowe wypełnienie, przez co wokół
   czerni beatu SEN 3→4 świeciła pomarańczowa ramka. Overskan jest symetryczny
   (marginesy skorygowane o połowę), więc punkt stały skalowania się nie rusza. */
.js .saga__bubble-inner{
  position:absolute;left:50%;top:44.2%;
  width:calc(100vw + 6px);height:calc(100svh + 6px);
  margin-left:calc(-50vw - 3px);margin-top:calc(-50svh - 3px);
  transform-origin:50% 50%;
  transform:scaleX(var(--bu-mirror,1)) scale(calc(1 / max(var(--bk),0.02)));
  opacity:0;
}

/* OBRYS — ta sama ścieżka i ten sam rodzic co okno, ale grubość kreski dzielona
   przez skalę: stała grubość w pikselach ekranu. Kryje kreskę rastra 1:1. */
.js .saga__outline{
  display:block;position:absolute;overflow:visible;pointer-events:none;
  left:var(--bu-left);top:var(--bu-top);width:var(--bu-w);height:var(--bu-h);
  transform:scaleX(var(--bu-mirror,1));transform-origin:50% 50%;
  opacity:0;
}
.js .saga__outline path{
  fill:none;stroke:#0D0D0D;stroke-linejoin:round;stroke-linecap:round;
  /* `px`, nie goła liczba: w SVG 1px = 1 jednostka użytkownika, więc wartość
     jest ta sama — ale Firefox 155 ODRZUCA `stroke-width: calc(<number>)`
     i spada na swoją domyślną jedynkę (zmierzone: Chrome `calc(6.20048px)`,
     Firefox `1px` przy tym samym `--bk`), przez co obrys kadru KV był
     włosowatą kreską zamiast kryć kreskę rastra. */
  stroke-width:calc(14.32px / max(var(--bk),0.05));
}

/* ── OUTRO (v5) — klatka końcowa i zdania, PONAD irysem ─────────────────────
   Klatka „pies przy misce” wchodzi po zamknięciu koła, więc nie może być przez
   nie przycięta: leży nad `.saga__iris`, nie w nim. Kadr wybudzenia zostaje pod
   spodem i NIE musi gasnąć — kryje go najpierw płaszczyzna cięcia, a potem
   nieprzezroczysta fotografia. Dzięki temu scena ani przez klatkę nie jest pusta
   i żaden element nie należy do dwóch grup naraz.
   Dwa kadry zdjęcia (16:9 / 9:16) wybiera `<picture media>`, nie JS — to jedyna
   rzecz w sadze, która nie zależy od wylosowanego psa.

   WARSTWY OUTRA (od spodu): irys 2 · płaszczyzna cięcia 3 · klatka końcowa 4 ·
   zdania 5. Płaszczyzna MUSI leżeć pod klatką końcową (ma ją poprzedzić, nie
   zakryć) i nad kadrem wybudzenia (ma go zgasić). */
.js .saga__cut{
  display:block;position:absolute;inset:0;z-index:3;
  background:var(--wl-orange);opacity:0;
}
.js .saga__bowl{
  position:absolute;inset:0;z-index:4;margin:0;opacity:0;
  transform-origin:50% 62%;            /* punkt stały skali = miska, nie środek ekranu */
}
.js .saga__bowl picture{display:block;width:100%;height:100%;}
.js .saga__bowl img{width:100%;height:100%;object-fit:cover;object-position:var(--bowl-pos,center 62%);}
/* Przyciemnienie pod tekstem. Gradient biegnie wzdłuż osi scrolla, co normalnie
   jest zakazane (ramka alfy jedzie wtedy po zdjęciu jak ruchoma krawędź) — tutaj
   jest dopuszczalne, bo scrim i zdjęcie są DZIEĆMI TEGO SAMEGO elementu: jadą
   jednym transformem, a ten kończy się (66 %) zanim tekst w ogóle wejdzie (66 %).
   Kiedy zdanie stoi na ekranie, pod nim nic się nie rusza.

   M2 (ux-ui-director 2026-09-16): przyciemnienie kończy się na wysokości KOLUMNY
   TEKSTU, nie na 64 % całego kadru. Poprzednia wersja gasiła grzbiet psa i zbocze
   przez dwie trzecie ekranu — a chroni zdanie, które zajmuje lewy górny róg.
   Ograniczenie jest DWUOSIOWE: w pionie gradientem (do 46 %), w poziomie MASKĄ
   (pełne krycie do 34 %, zejście do zera na 56 %). Maska, nie drugi gradient:
   dwa gradienty alfa sumują się, więc dołożenie poziomego POGŁĘBIŁOBY
   przyciemnienie w lewym górnym rogu zamiast je przyciąć. Maska jest statyczna —
   nic nie animuje ani `mask`, ani `background`. Na telefonie kolumna tekstu jest
   pełną szerokością ekranu, więc maski tam nie ma (patrz §16). */
.js .saga__bowl-scrim{
  display:block;position:absolute;inset:0;
  background:linear-gradient(to bottom,
    rgba(13,13,13,0.84), rgba(13,13,13,0.56) 26%, rgba(13,13,13,0) 46%);
}
/* R1c — PAS PRZYCIEMNIENIA: dokładany TYLKO tam, gdzie gradient systemu nie daje białej puencie kontrastu ≥ 4,5:1 na danym
   psie (`bowl.band.mobile|desktop` = krycie, zmierzone na zrzucie; 0 = brak pasa). Pełne krycie sięga do dołu bloku puenty
   (`--band-h`), poziomo przycina go ta sama maska co gradient (desktop).
   Poprawka (b), 2026-10-01: płaski pas kończył się TWARDĄ krawędzią w poprzek zdjęcia (widoczna na psach 12, 14, 17, 23).
   Dół pasa schodzi teraz do zera tym samym zejściem co gradient systemu nad nim (krycie → ~⅔ → 0, `--band-f` pod blokiem
   puenty) — to nie jest nowy element, tylko przedłużenie istniejącego przyciemnienia. Pas jest dzieckiem `.saga__bowl`, jak
   gradient: jedzie tym samym transformem, a ten kończy się, zanim puenta wejdzie, więc zejście nie przesuwa się po zdjęciu. */
.js .saga__bowl-scrim::after{
  content:"";position:absolute;left:0;right:0;top:0;height:calc(var(--band-h,220px) + var(--band-f,120px));
  background:linear-gradient(to bottom,
    rgba(13,13,13,var(--band-a,0)) 0 var(--band-h,220px),
    rgba(13,13,13,calc(var(--band-a,0) * 0.6)) calc(var(--band-h,220px) + var(--band-f,120px) * 0.45),
    rgba(13,13,13,0) 100%);
}
.js .saga__say{
  position:absolute;left:0;right:0;z-index:5;
  padding:0 var(--hub-pad);background:transparent;pointer-events:none;opacity:0;
}
.js .saga__say--wake{bottom:0;padding-bottom:46px;}
.js .saga__say--bowl{top:0;padding-top:92px;opacity:1;}
.js .saga__say--bowl .hub-display{opacity:0;}
/* Zdanie ⑤ pada na kadr wybudzenia, który nie ma własnego przyciemnienia —
   niesie je więc samo, w tej samej warstwie, żeby gasło razem ze zdaniem.

   Przyciemnienie siedzi na SAMYM ELEMENCIE, nie na `::before` z wysokością
   w procentach. Powód jest mechaniczny: `.saga__say--wake` jest przyklejone do
   dołu i NIE MA WŁASNEJ WYSOKOŚCI — mierzy się treścią. `height:66%` na
   pseudoelemencie dawało więc 66 % wysokości SAMEGO ZDANIA, czyli pasek niższy
   niż tekst, który miał chronić (zmierzone: kontrast 1,16:1 na buldogu).
   Gradient na elemencie plus duży `padding-top` kryje cały blok i kawałek nad nim.

   KRYCIE 0,94/0,90 to POMIAR, nie gust (2026-09-16, zrzuty DPR2): kadrem
   wybudzenia bywa kadr KV wylosowanego psa, a dwa z czterech psów leżą na BIELI
   (buldog w studiu, jamnik na pościeli). Na czystej bieli 0,90 daje tło L≈0,10,
   czyli ≥6:1 dla białego display. Gradient biegnie wzdłuż osi scrolla, ale kadr
   pod nim jest w tym odcinku NIERUCHOMY (wybudzenie nie ma animacji transformu),
   więc nie powstaje ruchoma krawędź. */
.js .saga__say--wake{
  padding-top:128px;
  background:linear-gradient(to top,
    rgba(13,13,13,0.94), rgba(13,13,13,0.90) 42%,
    rgba(13,13,13,0.52) 74%, rgba(13,13,13,0) 100%);
}

/* ==========================================================================
   6. SEN — pięć beatów wewnątrz okna dymka.
   Struktura i WSZYSTKIE klatki kluczowe przeniesione z v2 bez zmian: Alfa jest
   przypięta, świat przewija się pod nią, zmiana pozy to CIĘCIE, nigdy tween.
   ========================================================================== */

/* stan bazowy (bez JS): pięć statycznych kart na pomarańczu */
.dream{position:relative;background:var(--wl-orange);}
.dream__stage{position:relative;background:var(--wl-orange);}
.dream__layer,.dream__ground{display:none;}
.beat{padding:40px 0 8px;}
.beat + .beat{border-top:2px solid rgba(0,0,0,0.18);}
.beat__alfa{
  display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:center;
  gap:20px;padding:0 var(--hub-pad);
}
/* `max-width:none`: pozy to <p>, a site.css daje każdemu <p> `max-width:62ch` — na desktopie
   (62ch ≈ 617 px < pudełko 720 px) poza była dotąd węższa od pudełka i przesunięta w lewo
   (znalezione pomiarem R1b, 2026-10-01). */
.pose{margin:0;max-width:none;}
.pose img{max-height:26svh;max-width:min(70vw,420px);width:auto;height:auto;}
/* Poza-placeholder z TYM SAMYM plikiem co poza wcześniej w sadze (np. „krok” = 01 STOI,
   „pościg” = 05 BIEG): w ruchu nie przeszkadza (cięcie na ten sam rysunek), ale w stosie
   statycznym byłaby dublem — więc jej tam nie ma. Zniknie samo, gdy dostanie własny plik. */
.pose--dup{display:none;}
.beat__line--inv{display:none;}
/* ODBICIE (`flip` w danych): cały sen biegnie łbem w LEWO; rysunek patrzący w prawo dostaje
   lustro poziome właściwością `scale` (osobną od `transform`, więc nie gryzie się z ruchem).
   Nigdy obrót. Działa też w stosie statycznym. */
.pose--flip img{scale:-1 1;}
/* ZDOBYCZ (R1b): poza ze zdobyczą to figura `.duo` o proporcjach pozy-gospodarza (`--ar`).
   Gospodarz wypełnia figurę, zdobycz stoi obok w procentach JEGO szerokości (`--px` lewa
   krawędź, `--pw` szerokość, `--pb` podniesienie) — skala i odstęp nie zależą od ekranu.
   Statycznie: gospodarz ma szerokość `--duo-w`, a lewy margines robi miejsce na zdobycz
   wystającą w lewo (`--ovl` = −px). */
.duo{position:relative;display:block;aspect-ratio:var(--ar);width:var(--duo-w,min(46vw,300px));
  margin-left:calc(var(--duo-w,min(46vw,300px)) * var(--ovl,0) / 100);}
.pose .duo img{display:block;max-width:none;max-height:none;}
.pose .duo__host{width:100%;height:auto;}
.pose .duo__prey{position:absolute;left:calc(var(--px,0) * 1%);bottom:calc(var(--pb,0) * 1%);
  width:calc(var(--pw,50) * 1%);height:auto;}

/* stan animowany */
.js .dream{position:absolute;inset:0;height:auto;background:var(--wl-orange);}
.js .dream__stage{position:absolute;inset:0;overflow:clip;background:var(--wl-orange);}
.js .dream__layer{display:block;position:absolute;inset:0;z-index:0;}
/* ZMIANA v4: pomarańcz NIE wjeżdża już od dołu. W v2/v3 był to gest „sen się
   zaczyna”, bo T2 kończyło się na czerni. W v4 zoom oddaje sen na GOTOWYM,
   jednolitym pomarańczu — wjazd drugiego pomarańczu po nim dawał błysk czerni
   między przejściem a snem. Płaszczyzna jest więc statyczna, a beat SEN 3→4
   nadal ma swoje czernienie (`dream__night`). */
.js .dream__orange{background:var(--wl-orange);}
.js .dream__night{background:var(--wl-black);opacity:0;}
.js .dream__ground{
  display:block;position:absolute;left:0;bottom:var(--ground-y,32%);z-index:1;
  width:400vw;height:14px;opacity:0;
  background-image:url("/assets/alfa/ziemia.svg");
  background-repeat:repeat-x;background-size:auto 100%;
}
.js .beat{position:absolute;inset:0;padding:0;border-top:0;}
.js .beat__alfa{
  display:block;position:absolute;z-index:2;padding:0;
  left:50%;bottom:var(--ground-y,32%);
  width:min(84vw,560px);height:min(34svh,270px);
  margin-left:calc(min(84vw,560px) / -2);
  transform-origin:bottom center;
}
.js .pose{position:absolute;inset:0;margin:0;opacity:0;display:grid;place-items:end center;}
.js .pose img{max-width:100%;max-height:100%;}
.js .pose--dup{display:grid;}
/* FIGURA `.duo` w ruchu: wpisana w pudełko Alfy jak każda poza (contain), przez jednostki
   kontenera — `.pose` ma rozmiar z `inset:0`, więc cqw/cqh = pudełko. Sufit szerokości:
   telefon `--fig-m` (figura dosunięta do PRAWEJ krawędzi pudełka, zdobycz mieści się po lewej
   w 84vw + marginesie), desktop `--fig-d` (§16, wyśrodkowana; zdobycz wychodzi w lewo poza
   pudełko, na wolny pomarańcz). */
.js .pose--duo{container-type:size;}
.js .pose--duo .duo{margin:0;justify-self:end;
  width:min(calc(var(--fig-m,1) * 100cqw),calc(100cqh * var(--ar)));}
.js .pose .duo img{max-width:none;max-height:none;}
.js .beat__line--inv{display:block;}

.beat__line{padding:18px var(--hub-pad) 24px;}
.js .beat__line{
  position:absolute;z-index:3;left:0;right:0;bottom:0;
  padding:0 var(--hub-pad) 46px;opacity:0;pointer-events:none;
}
.beat__line .hub-display{max-width:12ch;}
.beat__gate{margin:8px 0 0;}
.js .beat__gate{position:absolute;left:var(--hub-pad);bottom:20px;margin:0;}
.beat__line--inv .hub-display{color:var(--wl-white);}
/* STREFA OCHRONNA „MENU” (R3, bloker telefonu). Poniżej 800 px zdania sagi stoją przy dolnej krawędzi na pełnej szerokości, a w prawym
   dolnym rogu stoi przez całą sagę pływające „Menu” (§2). Każde zdanie sagi zostawia więc po prawej pas szerokości przycisku: odstęp od
   krawędzi (12 px / safe-area) + przycisk (102 px, zmierzone getBoundingClientRect przy 375/390 px) + 16 px oddechu. Tekst nie może wejść
   pod przycisk przy żadnym postępie sagi i żadnej wysokości ekranu (test SAGA-R1-CDP: 375×667, 375×812, 390×844, 412×915). Od 800 px
   zdanie ma `max-width:52%` (snu) / lewą kolumnę, więc strefa nie jest potrzebna. Puenta przy misce stoi u GÓRY kadru — strefy nie
   potrzebuje i jej układ zostaje bez zmian. */
@media (max-width:799px) and (prefers-reduced-motion:no-preference){
  .js .saga{--menu-safe:calc(max(12px, env(safe-area-inset-right)) + 118px);}
  .js .beat__line,.js .saga__say--wake{padding-right:var(--menu-safe);}
  /* R4 (krytyk re-review): w zwężonej strefą kolumnie zdanie wybudzenia łamało się na trzy wiersze z sierotą „MISKI.”
     (390×844). `balance` wyrównuje długości wierszy — ostatni wiersz nie zostaje z jednym słowem; strefa Menu bez zmian. */
  .js .saga__say--wake .hub-display{text-wrap:balance;}
}

/* ==========================================================================
   DWA SILNIKI, JEDNA PARTYTURA, TRZY GRUPY.
   Klatki kluczowe są JEDYNYM źródłem prawdy o ruchu. Napędza je:
   (a) natywna oś scrolla — `animation-timeline: --saga` (Chrome/Edge 115+,
       Safari 26+), animacja idzie wtedy poza głównym wątkiem;
   (b) fallback dla przeglądarek bez osi (Firefox stable, 2026-09) — ta SAMA
       animacja jest `paused`, a rAF przesuwa ją ujemnym `animation-delay`.
   Nie ma tu drugiej implementacji ruchu — jest jedna partytura i dwa zegary.

   Grupy istnieją, żeby każdy odcinek mógł mieć SWOJE 0–100 %:
     GZ  ZOOM  (--b1 … --b2) — kadr, wypełnienie dymka, obrys, fotografia,
                               kadr lądowania. Ostatnia klatka HOLDUJE przez
                               cały sen (`fill-mode:both`), więc maska stoi
                               nieruchomo na `--bk-full` przez 460 svh.
     GD  SEN   (--b2 … --b3) — kolor, pozy i typografia snu (oś v2 BEZ ZMIAN)
     GI  IRIS  (--b3 … --b4) — promień koła
     GW  OUTRO (--b4 … 100 %) — klatka końcowa i dwa zdania outra
   Element należy do dokładnie jednej grupy — inaczej `fill-mode:both` z ostatniej
   animacji nadpisywałby pozostałe poza ich zakresem.
   ========================================================================== */

.js .saga__frame,.js .saga__bubble-fill,.js .saga__bubble-inner,
.js .saga__outline,.js .saga__photo--kv,.js .saga__wake,
.js .dream__night,.js .dream__ground,.js .beat__alfa,.js .pose,.js .beat__line,
.js .duo__host,.js .duo__prey,
.js .saga__iris,
.js .saga__cut,.js .saga__bowl,.js .saga__say--wake,.js .saga__awake,
.js .saga__say--bowl .hub-display{
  animation-duration:1s;animation-timing-function:linear;animation-fill-mode:both;
  animation-play-state:paused;
}
/* GZ */
.js .saga__frame,.js .saga__bubble-fill,.js .saga__bubble-inner,
.js .saga__outline,.js .saga__photo--kv,.js .saga__wake{
  animation-delay:calc(var(--pz,0) * -1s);
}
/* GD */
.js .dream__night,.js .dream__ground,.js .beat__alfa,.js .pose,.js .beat__line,
.js .duo__host,.js .duo__prey{
  animation-delay:calc(var(--pd,0) * -1s);
}
/* GI */
.js .saga__iris{animation-delay:calc(var(--pi,0) * -1s);}
/* GW */
.js .saga__cut,.js .saga__bowl,.js .saga__say--wake,.js .saga__awake,
.js .saga__say--bowl .hub-display{
  animation-delay:calc(var(--pw,0) * -1s);
}

.js .saga__frame{animation-name:zoom-tf;}
.js .saga__bubble-fill{animation-name:zoom-fill;}
.js .saga__bubble-inner{animation-name:zoom-inner;}
.js .saga__outline{animation-name:zoom-outline;}
.js .saga__photo--kv{animation-name:zoom-photo;}
.js .saga__wake{animation-name:zoom-wake;}
.js .saga__iris{animation-name:iris-out;}
.js .saga__cut{animation-name:outro-cut;}
.js .saga__bowl{animation-name:outro-bowl;}
.js .saga__say--wake{animation-name:outro-say-wake;}
.js .saga__say--bowl .hub-display{animation-name:outro-say-bowl;}
.js .saga__awake{animation-name:outro-awake;}

.js .dream__night{animation-name:dream-night;}
.js .dream__ground{animation-name:dream-ground;}
.js .beat__alfa{animation-name:dream-alfa;}
/* `pose-N`, `line-<beat>` (+ `-b`), `dream-alfa`, `dream-night` — klatki i nazwy animacji
   pojedynczych póz/zdań stoją w <head> (`<style id="saga-score">`), wygenerowane
   z home.json przez scripts/saga-score.mjs. Tu zostaje wyłącznie mechanika. */

@supports (animation-timeline:view()){
  .js:not(.reveal) .saga{view-timeline-name:--saga;view-timeline-axis:block;}
  .js:not(.reveal) .saga__frame,.js:not(.reveal) .saga__bubble-fill,
  .js:not(.reveal) .saga__bubble-inner,.js:not(.reveal) .saga__outline,
  .js:not(.reveal) .saga__photo--kv,.js:not(.reveal) .saga__wake,
  .js:not(.reveal) .dream__night,.js:not(.reveal) .dream__ground,
  .js:not(.reveal) .beat__alfa,.js:not(.reveal) .pose,.js:not(.reveal) .beat__line,
  .js:not(.reveal) .duo__host,.js:not(.reveal) .duo__prey,
  .js:not(.reveal) .saga__iris,
  .js:not(.reveal) .saga__cut,.js:not(.reveal) .saga__bowl,
  .js:not(.reveal) .saga__say--wake,.js:not(.reveal) .saga__awake,
  .js:not(.reveal) .saga__say--bowl .hub-display{
    animation-timeline:--saga;animation-delay:0s;animation-duration:auto;animation-play-state:running;
  }
  .js:not(.reveal) .saga__frame,.js:not(.reveal) .saga__bubble-fill,
  .js:not(.reveal) .saga__bubble-inner,.js:not(.reveal) .saga__outline,
  .js:not(.reveal) .saga__photo--kv,.js:not(.reveal) .saga__wake{
    animation-range:contain var(--b1) contain var(--b2);
  }
  .js:not(.reveal) .dream__night,.js:not(.reveal) .dream__ground,
  .js:not(.reveal) .beat__alfa,.js:not(.reveal) .pose,.js:not(.reveal) .beat__line,
  .js:not(.reveal) .duo__host,.js:not(.reveal) .duo__prey{
    animation-range:contain var(--b2) contain var(--b3);
  }
  .js:not(.reveal) .saga__iris{animation-range:contain var(--b3) contain var(--b4);}
  .js:not(.reveal) .saga__cut,
  .js:not(.reveal) .saga__bowl,.js:not(.reveal) .saga__say--wake,.js:not(.reveal) .saga__awake,
  .js:not(.reveal) .saga__say--bowl .hub-display{
    animation-range:contain var(--b4) contain 100%;
  }
}

/* ── GZ · ZOOM (oś lokalna 0–100 % = 70 svh) ──────────────────────────────── */
/* Kadr rośnie wokół środka dymka i jednocześnie sadza ten środek na środku
   ekranu. Krzywa jest łamana, nie liniowa: start wolno (widz ma zauważyć, że to
   jego zdjęcie się przybliża), koniec szybko (nie ma już czego oglądać). */
/* R1c: ostatni odcinek (3,8 → `--bk-full`) kończy się na 86 %, nie na 100 %. Pula par ma na desktopie MNIEJSZE dymki
   (28–42 % wysokości ekranu zamiast ~37 % Golemo, bo kadr 16:9 z pionowego mastera jest niski), więc `--bk-full` rośnie,
   a pomarańcz krył ekran dopiero przy ~86 % osi zoomu — PO wygaszeniu fotografii (74–82 %): na brzegach ekranu
   przez kilka svh prześwitywał kadr lądowania. Zmierzone CDP (6 psów × 1024/1280/1440/1920, krok 4 %): przy starym
   torze resztka zdjęcia 2–3,4 % ekranu przy 82 %, 0,25–0,4 % przy 86 %. Tor skrócony o 14 punktów przesuwa ten próg
   na ~77 % — fotografia gaśnie 78–84 %, czyli na już pełnym pomarańczu. 86–100 %: stoi pomarańcz, cięcie na sen 88 %. */
@keyframes zoom-tf{
  0%,10%{--bka:1;--bkb:0;--bp:0;}
  30%{--bka:1.6;--bkb:0;--bp:0.42;}
  62%{--bka:3.8;--bkb:0;--bp:1;}
  86%,100%{--bka:3.8;--bkb:1;--bp:1;}
}
/* Wypełnienie: to ONO gasi tekst „ŚNI, ŻE JEST MNĄ” i łeb wilka — oba są
   pikselami zdjęcia, więc nie da się ich zgasić inaczej niż przykryciem. */
@keyframes zoom-fill{
  0%,10%{opacity:0;}
  30%,100%{opacity:1;}
}
/* Obrys wchodzi razem z wypełnieniem (kryje kreskę rastra), wychodzi, kiedy jest
   już poza kadrem — polisa na ciasny `--bk-full`, nie efekt. */
@keyframes zoom-outline{
  0%,10%{opacity:0;}
  30%,66%{opacity:1;}
  80%,100%{opacity:0;}
}
/* Fotografia gaśnie dopiero wtedy, gdy pomarańcz kryje JUŻ CAŁY ekran.
   H2 — POMIAR, nie oszacowanie (2026-09-11, 260 zrzutów przez CDP, DPR1):
   dla każdego psa z puli i pięciu rozmiarów ekranu (320×700, 360×780, 375×812,
   390×844, 412×915) policzony udział pikseli, które nie są ani pomarańczem
   dymka, ani czernią obrysu. Pierwszy postęp zoomu, przy którym ten udział
   spada do zera i już nie wraca:
       jamnik 62 % · buldog 64 % · szpic 64 % · AMSTAFF 72 %
   (ten sam próg na wszystkich pięciu ekranach — decyduje udział dymka w kadrze,
   nie proporcja telefonu). Amstaff ma najmniejszy dymek w puli (k_full 9,2),
   więc on ustawia próg dla całej puli.
   PRZELICZONE PO RE-CROPIE 2026-09-16 (cztery rozmiary: 320×700, 375×812,
   390×844, 430×932, DPR1, ta sama metoda): jamnik ma teraz szersze okno kadru,
   więc dymek zajmuje w nim mniej (k_full 7,0 → 10,0) i próg zakrycia przesunął
   się z 62 % na 72 % — czyli DOKŁADNIE tam, gdzie stoi amstaff; przy 70 % zostaje
   jeszcze 0,08–0,18 % ekranu. Buldog angielski (k_full 8,1 → 8,7) kryje przy
   70 %. Najgorszy przypadek puli się NIE ZMIENIŁ, okno 74–82 % zostaje bez
   zmian i dalej ma 2 punkty zapasu.
   Poprzednie okno 58→74 % gasiło fotografię CZTERNAŚCIE punktów za wcześnie:
   przy z=0,58–0,70 pod pomarańczem widać było jeszcze pasek zdjęcia na górnej
   i dolnej krawędzi, a od 58 % wchodził tam kadr lądowania — czyli przenikanie
   dwóch kadrów na krawędziach ekranu (ux-ui-director H2).
   Nowe okna: fotografia trzyma do 74 % (2 punkty zapasu nad zmierzonymi 72),
   kadr lądowania startuje dopiero, gdy fotografia jest już w połowie wygaszona,
   i kończy przed cięciem na sen (88 %). Oba mieszczą się pod obrysem (do 80 %). */
@keyframes zoom-photo{
  0%,78%{opacity:1;}
  84%,100%{opacity:0;}
}
/* Kadr lądowania wchodzi pod spodem, kiedy ekran jest kryty — podmiany nie widać. */
@keyframes zoom-wake{
  0%,80%{opacity:0;}
  86%,100%{opacity:1;}
}
/* Wnętrze snu: CIĘCIE na jednolitym pomarańczu, nie przenikanie. */
@keyframes zoom-inner{
  0%,87.9%{opacity:0;}
  88%,100%{opacity:1;}
}

/* ── GI · IRIS (oś lokalna 0–100 % = 45 svh) ──────────────────────────────── */
/* Promień startowy musi kryć NAJDALSZY róg. Przy środku na 50 %/50 % wystarczało
   75 % (√2/2 ≈ 70,7 % + zapas). Po zejściu środka na 58 % (L1) najdalsze są rogi
   GÓRNE i wymagany promień rośnie: 80,1 % na 375×812, 80,2 % na 320×700, a dla
   ekranu nieskończenie wąskiego dąży do √2 × 0,58 = 82,0 % (im szerszy ekran, tym
   mniej — 74,1 % na 1440×900). Stąd 84 %: zapas 2 punkty nad matematyczną granicą,
   ten sam co wcześniej. Koszt jest żaden — koło startuje poza ekranem i wchodzi
   w kadr po ~0,8 svh. Liniowo i bez easingu: koło ma się zamykać, nie odbijać. */
@keyframes iris-out{
  0%,16%{--iris:84;}
  88%,100%{--iris:0;}
}

/* ── GW · OUTRO (R1, oś lokalna 0–100 % = 140 svh) ─────────────────────────── */
/* Zdanie ⑤ na kadrze wybudzenia: wjazd 5–12 %, stoi 12–34 % (≈31 svh), gaśnie do 40 %.
   (Koniec 40 % czyta scripts/saga-score.mjs jako górny próg pływającego „Menu”.)
   PRZEBUDZENIE 14–24 %: kadr z otwartymi oczami przenika się z kadrem snu — tylko
   gdy pies ma `awake` (kontrakt); bez niego element nie istnieje.
   CIĘCIE PRZEZ POMARAŃCZ 42–48 % (płaszczyzna), klatka miski 48–54 % na pomarańczu,
   skala 1,05 → 1 do 62 % — obraz staje, ZANIM wejdzie zdanie.
   „JA MAM TROP. TY MASZ MISKĘ.” wjazd 62–72 %, HOLD 72–100 % (≈39 svh) i puszczenie
   przypięcia do S8 „Jedna zdobycz, jedna decyzja.”. */
@keyframes outro-awake{
  0%,14%{opacity:0;}
  24%,100%{opacity:1;}
}
@keyframes outro-cut{
  0%,42%{opacity:0;}
  48%,100%{opacity:1;}
}
@keyframes outro-bowl{
  0%,48%{opacity:0;transform:scale(1.05);}
  54%{opacity:1;}
  62%,100%{opacity:1;transform:scale(1);}
}
@keyframes outro-say-wake{
  0%,5%{opacity:0;transform:translate3d(0,12px,0);}
  12%,34%{opacity:1;transform:translate3d(0,0,0);}
  40%,100%{opacity:0;transform:translate3d(0,0,0);}
}
@keyframes outro-say-bowl{
  0%,62%{opacity:0;transform:translate3d(0,14px,0);}
  72%,100%{opacity:1;transform:translate3d(0,0,0);}
}

/* ── G3 · SEN (oś lokalna 0–100 % = 460 svh) — PRZENIESIONE Z v2 ─────────── */
/* R1: `dream-night`, `dream-alfa`, `pose-N` i `line-<beat>` NIE są już pisane ręcznie —
   generuje je scripts/saga-score.mjs z `copy.dream.beats` (vh, `from`, `line`, `night`,
   `camera`) i wstawia do <head>. Tabela: `node scripts/saga-score.mjs --tabela`.
   Linia ziemi jest wyłączona (`ground_line.enabled:false`); jej klatki zostały z v2
   i NIE są przeliczone na oś R1 — przed włączeniem trzeba je przenieść do generatora. */
@keyframes dream-ground{
  0%,9%{opacity:0;transform:translate3d(0,0,0);}
  12%,21.7%{opacity:1;transform:translate3d(0,0,0);}
  39%,43.5%{opacity:1;transform:translate3d(-30%,0,0);}
  55%{opacity:1;transform:translate3d(-31.5%,0,0);}
  60%{opacity:0;transform:translate3d(-32%,0,0);}
  86%{opacity:0;transform:translate3d(-32%,0,0);}
  90%,100%{opacity:1;transform:translate3d(-33%,0,0);}
}

/* ---------- 7/8. S6 WYBUDZENIE i S7 TWOJA CZĘŚĆ — SKASOWANE w v5 ----------
   Obie sceny przestały być osobnymi ekranami; ich treść jest teraz beatami
   OUTRA wewnątrz przypiętej sagi (patrz sekcja „OUTRO (v5)” wyżej). Razem
   z nimi wypadły `.scene--wake`, `.scene--bowl`, `.wake-copy`, `.bowl-copy`
   i `.bowl-alfa`. `--wake-fit` / `--wake-pos` ZOSTAJĄ — używa ich kadr
   lądowania pod irysem (`.saga__wake`). */

/* ---------- 9. S8 FAKTY ---------- */
.scene--facts{background:var(--wl-white);padding:116px var(--hub-pad) 48px;overflow:visible;align-content:start;}
.scene--facts .scene__inner{
  display:grid;grid-template-columns:minmax(0,1fr) 30%;
  column-gap:16px;row-gap:20px;align-items:start;max-width:1000px;
}
.scene--facts .hub-display{grid-column:1;max-width:11ch;}
.facts-shot{grid-column:2;grid-row:1/3;margin:0;}
.facts-shot img{width:100%;height:auto;max-height:56svh;object-fit:contain;object-position:top center;}
.facts{grid-column:1;display:grid;gap:12px;margin:0;padding:0;list-style:none;}
.facts li{
  font-size:0.9375rem;line-height:1.45;max-width:44ch;
  border-left:3px solid var(--wl-orange);padding-left:12px;color:var(--wl-black);
}

/* ---------- 10. S9 WYZWANIE ---------- */
.scene--dare{background:var(--wl-orange);}
.scene--dare .scene__inner{display:grid;justify-items:start;gap:22px;}
.scene--dare .hub-display{max-width:12ch;}
.alfa--dare{margin:0;}
.js .scene--dare:not(.on) .alfa--dare{transform:translate3d(0,15%,0);}
.alfa--dare img{width:min(56vw,260px);height:auto;}
.dare-seal{width:min(78vw,320px);height:auto;}
/* przycisk wyzwania = `.wl-btn` (na polu pomarańczowym czarny — reguła powierzchni `.wl-on-color`) */

/* ---------- 11. MANIFEST ---------- */
.manifest{background:var(--wl-white);color:var(--wl-black);padding:72px var(--hub-pad) 56px;}
.manifest__inner{max-width:1120px;margin:0 auto;}
.manifest .wl-lbl{color:var(--wl-black);margin-bottom:16px;}
.manifest__text{
  font-family:var(--wl-body);font-weight:300;font-size:1.1875rem;line-height:1.5;
  max-width:34ch;margin:0;
}

/* ==========================================================================
   12. LINIA PRODUKTÓW — idiom decku (Deck.html slajd 06), przeniesiony
   Biel jest tu drugim (po S8) „wyjściem z opowieści”: po dziewięciu ekranach
   fabuły wchodzi rzecz i sekcja ma wyglądać jak KATALOG, nie jak dalszy ciąg.
   ========================================================================== */

.linia{background:var(--wl-white);color:var(--wl-black);padding:0 var(--hub-pad) 8px;scroll-margin-top:72px;}
/* Nagłówek sekcji = wspólny `.wl-sh` (kreska 2 px: miejsce, w którym opowieść się kończy i zaczyna półka; bez niej manifest i katalog
   zlewają się w jedną białą płachtę). Siatka i kafle = wspólny `.wl-tiles` / `.wl-tile` (site.css §10, partials/tile.njk). */
.linia .wl-sh,.linia .wl-tiles,.linia .wl-sec__link{max-width:var(--wl-col);margin-left:auto;margin-right:auto;}
/* Link „Wszystkie produkty” pod siatką (SEO v1) — ten sam rytm co siatka: max 1120, bez nowych kolorów. */
.linia .wl-sec__link{margin-top:24px;margin-bottom:0;}

/* ---------- 13. NIEZBĘDNIK — pas, nie kafel ---------- */
.pas{background:var(--wl-white);color:var(--wl-black);padding:44px var(--hub-pad) 36px;}
.pas__inner,.stado__inner{max-width:var(--wl-col);margin:0 auto;}
.pas__inner .wl-sh,.stado__inner .wl-sh{margin-bottom:0;}
.pas__links{list-style:none;margin:8px 0 0;padding:0;display:grid;gap:0;}
.pas__links li{margin:0;}

/* ---------- 14. STADO ---------- */
.stado{background:var(--wl-white);color:var(--wl-black);padding:0 var(--hub-pad) 56px;}
.stado-form{display:grid;gap:12px;margin-top:16px;max-width:520px;}
.stado-form__row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;}
.stado-form input[type="email"]{
  font-family:var(--wl-body);font-size:16px;padding:12px;min-height:var(--wl-ctl);
  border:var(--wl-rule);background:var(--wl-white);color:var(--wl-black);min-width:0;
}
.stado-consent{
  display:grid;grid-template-columns:auto minmax(0,1fr);gap:8px;align-items:start;
  font-family:var(--wl-body);font-size:0.8125rem;line-height:1.45;
}
.stado-consent input{margin-top:3px;}
.stado-form__note{font-size:0.8125rem;color:var(--wl-muted);margin:0;}

/* ---------- 15. STOPKA HUBU — wspólna z resztą serwisu (partials/footer.njk, site.css §6); własnej nie ma ---------- */

/* ---------- 16. DESKTOP ---------- */

@media (min-width:800px){
  :root{--hub-pad:40px;}
  .scene{padding:112px var(--hub-pad) 72px;}
  /* Na desktopie sylwetka schodzi z osi w prawo, a zdanie zajmuje lewą połowę:
     inaczej SKOK (skala 1,6) kładł się czarną masą na czarnej typografii. */
  .js .beat__alfa{width:min(52vw,720px);height:min(44svh,420px);left:64%;
    margin-left:calc(min(52vw,720px) / -2);
    /* Linia ziemi co najmniej 300 px nad dołem ekranu: na niskich desktopach (1024×768,
       1280×720) dwuwierszowe zdanie w lewej kolumnie sięga wyżej niż 32 % — świnia z pościgu
       stała na „JEDNA ZDOBYCZ.” (pomiar R1b). */
    bottom:max(var(--ground-y,32%),300px);
    --s4-x1:-10%;--s4-x2:-12%;--s4-y1:3%;--s4-y2:5%;--s4-k:1.45;}
  .js .beat__line{padding-bottom:76px;max-width:52%;}
  .js .pose--duo .duo{justify-self:center;
    width:min(calc(var(--fig-d,1) * 100cqw),calc(100cqh * var(--ar)));}
  /* Uczta nie ma zdania, więc lewa kolumna jest wolna: kompozycja wilk + świnia (świnia
     wystaje ~0,8 szerokości wilka w lewo) staje bliżej środka i jest wyższa, inaczej
     siedzący wilk wychodził wyraźnie mniejszy od wilka w pościgu tuż przed nim. */
  .js .beat__alfa[data-beat="sen5"]{left:58%;height:min(52svh,500px);}
}
/* Niski desktop (≤ 800 px wysokości): skok przy 1,45 wchodził ogonem pod Menu/SKLEP, a łapami
   na dwuwierszowe zdanie (1280×720, pomiar R1b). */
@media (min-width:800px) and (max-height:800px){
  .js .beat__alfa{--s4-k:1.22;--s4-x1:-6%;--s4-x2:-8%;}
}
/* R1c (naprawa regresji R1b): blok „niski desktop” wyżej połykał resztę reguł desktopu (maska przyciemnienia miski,
   16ch puenty, siatki S8/S9, manifest, linia) — działały tylko przy wysokości ≤ 800 px, więc na 1440×900 i 1920×1080
   przyciemnienie miski kryło całą szerokość kadru. Reguły wracają do zakresu `min-width:800px`, jak przed R1b. */
@media (min-width:800px){
  .dream__ground{width:300vw;height:18px;}
  .js .saga__say--wake{padding-bottom:64px;}
  .js .saga__say--bowl{padding-top:118px;}
  /* M2: od 800 px zdanie końcowe zajmuje LEWĄ kolumnę (max-width 16ch), więc
     przyciemnienie przycinamy do niej maską. Poza maską pies i miska zostają
     bez przyciemnienia — to był zarzut oceny v5. */
  .js .saga__bowl-scrim{
    -webkit-mask-image:linear-gradient(to right,var(--wl-black) 0 34%,transparent 56%);
    mask-image:linear-gradient(to right,var(--wl-black) 0 34%,transparent 56%);
  }
  .js .saga__say .hub-display{max-width:16ch;}
  .js .saga__bowl-scrim::after{--band-h:var(--band-h-d,300px);}
  .scene--facts .scene__inner{grid-template-columns:minmax(0,1.4fr) minmax(0,1fr);column-gap:56px;}
  .scene--dare .scene__inner{grid-template-columns:minmax(0,1fr) minmax(0,1fr);align-items:center;gap:40px;}
  .scene--dare .alfa--dare{grid-row:1/4;}
  .alfa--dare img{width:100%;max-width:420px;}
  .manifest{padding:112px var(--hub-pad) 72px;}
  .manifest__text{font-size:1.375rem;max-width:46ch;}
  .linia{padding:0 var(--hub-pad) 12px;}
  .stado-form{margin-top:0;}
}

/* R1c: przy 800–1599 px puenta (16ch, ~470 px) wychodzi poza 34 % szerokości, a maska kończyła przyciemnienie na 56 % — prawa część
   „TY MASZ MISKĘ.” stała na rozjaśnionym zdjęciu (kontrast 2,2–2,8:1 przy 1024, pomiar CDP). Ta sama maska, dłuższa. */
@media (min-width:800px) and (max-width:1599px){
  .js .saga__bowl-scrim{
    -webkit-mask-image:linear-gradient(to right,var(--wl-black) 0 52%,transparent 74%);
    mask-image:linear-gradient(to right,var(--wl-black) 0 52%,transparent 74%);
  }
}

/* ── 16b. DESKTOP PEŁNOEKRANOWY (≥1024 px) ───────────────────────────────────
   Polecenie właściciela 2026-09-16: „na desktopie wszystkie elementy mają
   wypełniać cały ekran”. Do v4 kadr KV był prostokątem 9:16 wpisanym w ekran,
   więc na 1440×900 stał pionowym pasem 506 px na czerni — dokładnie to, co
   właściciel odrzucił.

   Zmiana jest JEDNA i strukturalna: kadr przestaje mieć proporcję i staje się
   prostokątem viewportu, a zdjęcie wchodzi w nim `object-fit:cover`. Wszystko
   inne wynika z tego:
     · plik jest inny — kadr POZIOMY 16:9 tego samego psa (`kv_pool[].desktop`,
       zbudowany przez scripts/build-saga5-assets.py);
     · pasy letterboksu znikają, bo nie ma czego letterboksować;
     · geometria dymka nie może już stać w danych jako procent kadru (wycinek
       `cover` zależy od proporcji ekranu) — przelicza ją skrypt rotacji przy
       każdym layoucie, tą samą arytmetyką, którą stosuje przeglądarka;
     · `object-position` kotwiczy się do GÓRY: dymek stoi przy górnej krawędzi
       każdego zdjęcia, więc przy bardzo szerokim ekranie `cover` ma uciąć DÓŁ,
       nigdy dymek.
   Sen (pomarańcz + Alfa) i klatka końcowa są pełnoekranowe z konstrukcji i nie
   wymagają tu ani jednej reguły. */
@media (min-width:1024px){
  :root{--frame-w:100vw;--frame-h:100svh;}
  .js .saga__band{display:none;}
  .js .saga__frame{
    top:0;left:0;width:100%;height:100%;margin:0;
  }
  .js .saga__photo img{object-position:var(--kv-pos,center top);}
  /* Kadr lądowania: ten sam kadr poziomy, więc `cover` bez kompromisu. */
  .js .saga__wake img,.js .saga__awake img{object-position:var(--wake-pos,center top);}
  /* R1c: kadr miski 16:9 jest kadrowany ciasno, 5 % pod miską — kotwica z danych (`bowl.wide.pos`). */
  .js .saga__bowl img{object-position:var(--bowl-pos-wide,center 62%);}
  /* R1c: puenta po stronie kadru 16:9, po której nie ma łba psa (`bowl.text_side`, z danych pary) — lustro kolumny
     i maski przyciemnienia; gradient ten sam co po lewej (system), zmienia się tylko kierunek maski. */
  .js .saga[data-bowl-side="R"] .saga__say--bowl{display:flex;justify-content:flex-end;text-align:right;}
  /* R3 — ZDANIE WYBUDZENIA TAM, GDZIE BYŁ DYMEK (ocena UX R3: przy dole kadru leżało na psie w parach 03, 07, 10, 11).
     Kadr wybudzenia to ten sam kadr co KV, tylko bez dymka — więc miejsce dymka jest z konstrukcji wolne od psa
     (`head_box` rozłączny z oknem dymka: test SAGA-R1C). Zdanie staje w oknie dymka (`--bu-*`, liczone przez skrypt rotacji
     na żywo, razem z odsunięciem od kontrolek nagłówka): górna krawędź = góra okna, nie wyżej niż 96 px (kontrolki 12–60 px
     + oddech); po stronie dymka (`data-bu-side`, skrypt rotacji) — lewy dymek: zdanie od jego lewej krawędzi, prawy: zdanie
     kończy się na jego prawej krawędzi. Bez przyciemnienia (pas gradientu kładł się na psie): tekst czarny, biały tylko
     dla par z ciemnym tłem pod zdaniem (`desktop.wake_ink:"light"` → `data-wake-ink`, pomiar kontrastu na kadrach). */
  .js .saga__say--wake{
    top:max(96px, var(--bu-top,12%));bottom:auto;left:max(var(--hub-pad), var(--bu-left,8%));right:auto;
    padding:0;background:none;
  }
  .js .saga[data-bu-side="R"] .saga__say--wake{
    left:auto;right:max(var(--hub-pad), calc(100% - var(--bu-left,60%) - var(--bu-w,20%)));text-align:right;
    display:flex;justify-content:flex-end;
  }
  /* Niski desktop (1280×720, 1024×768): dwa wiersze przy 80 px dochodziły do uszu psów 01/06/23 (pomiar R3, `head_box`) —
     krój maleje z wysokością ekranu (9 svh), od ~890 px wysokości zostaje 80 px jak reszta zdań sagi. */
  .js .saga__say--wake .hub-display{font-size:min(clamp(2.5rem,1.5rem + 4.6vw,5rem), 9svh);}
  .js .saga[data-wake-ink="dark"] .saga__say--wake .hub-display{color:var(--wl-black);}
  .js .saga[data-bowl-side="R"] .saga__bowl-scrim{
    -webkit-mask-image:linear-gradient(to left,var(--wl-black) 0 34%,transparent 56%);
    mask-image:linear-gradient(to left,var(--wl-black) 0 34%,transparent 56%);
  }
}
@media (min-width:1024px) and (max-width:1599px){
  .js .saga[data-bowl-side="R"] .saga__bowl-scrim{
    -webkit-mask-image:linear-gradient(to left,var(--wl-black) 0 52%,transparent 74%);
    mask-image:linear-gradient(to left,var(--wl-black) 0 52%,transparent 74%);
  }
}

/* ── 16c. EKRANY ULTRASZEROKIE (≥ 2:1) ───────────────────────────────────────
   M3 (ux-ui-director 2026-09-16). Przy 2560×1080 `cover` na kadrze 16:9 ucina
   360 px — przy kotwicy `top` ginie DÓŁ: pysk psa w KV i dno miski w klatce
   końcowej.
     · KV i kadr lądowania: kotwicę liczy skrypt rotacji (`vAnchor`), bo ta sama
       liczba musi wejść do przeliczenia geometrii dymka. Tu nie ma więc żadnej
       reguły — `--kv-pos` / `--wake-pos` przychodzą z JS.
     · Klatka końcowa nie ma dymka, więc jest czystym CSS-em. Kotwica idzie
       w DÓŁ, nie w górę: `object-position` w pionie działa odwrotnie, niż
       sugerowała ocena („center 50 %”) — 50 % pokazałoby JESZCZE MNIEJ dna
       miski niż obecne 62 %. Zmierzone na kadrze `miska-pies-wide` (1600×900):
       dolna krawędź miski leży na 93,9 % wysokości pliku, przy 2560×1080 kadr
       skaluje się do 1440 px i z pokazania miski w całości wychodzi minimum
       75,6 %. Stąd 82 % — dwadzieścia parę pikseli zapasu pod miską. */
@media (min-aspect-ratio:2/1) and (min-width:1024px){
  .js .saga__bowl img{object-position:var(--bowl-pos-wide,center 82%);}
}

/* ==========================================================================
   17. DOSTĘPNOŚĆ — prefers-reduced-motion: KOMPLETNA strona statyczna.
   Saga przestaje być przypięta i rozpada się na karty: S1, kadr KV (dymek jako
   ZWYKŁA SEKCJA — raster niesie go sam), pięć kart snu. Żadna treść nie znika.
   ========================================================================== */

@media (prefers-reduced-motion:reduce){
  .js .fx,
  .js .scene:not(.on) .fx,
  .js .manifest:not(.on) .fx,
  .js .linia:not(.on) .fx,
  .js .pas:not(.on) .fx,
  .js .stado:not(.on) .fx,
  .js .scene:not(.on) .alfa,
  .js .scene:not(.on) .photo,
  .js .scene--dare:not(.on) .alfa--dare{opacity:1 !important;transform:none !important;transition:none !important;}
  .js .scene--bowl .photo img{transform:none !important;transition:none !important;}
  *,*::before,*::after{animation:none !important;scroll-behavior:auto !important;}

  /* Powrót DOKŁADNIE do stanu bazowego (tego samego, który widzi przeglądarka
     bez JS): zwykły stos sekcji, zero przypięcia, zero animacji. */
  /* Nagłówek przestaje być przyklejony (ux-ui-director, runda 2): przy
     `prefers-reduced-motion` strona jest zwykłym statycznym stosem, a stała
     belka przejeżdżała po treści — zdanie „…I ZJADAM JĄ CAŁĄ." wchodziło pod
     logo. Nic tu nie jest przypięte, więc i nagłówek nie musi być. */
  .hub .wl-header{position:static !important;background:var(--wl-white) !important;
    border-bottom:var(--wl-rule) !important;}

  .js .saga{height:auto !important;background:var(--wl-white) !important;}
  .js .saga__stage{position:static !important;height:auto !important;overflow:visible !important;background:transparent !important;}
  .js .saga__back,.js .saga__band{display:none !important;}
  .js .saga__wake{position:static !important;opacity:1 !important;display:block !important;}
  .js .saga[data-awake] .saga__wake{display:none !important;}
  /* R3: kadr wybudzenia bez własnego pliku = zdjęcie KV z pasa 1 — w stosie statycznym byłby drugim śpiącym psem. */
  .js .saga__wake[data-same-as-kv]{display:none !important;}
  .js .saga__awake[data-on]{position:static !important;opacity:1 !important;display:block !important;}
  .js .saga__wake img,.js .saga__awake img{height:auto !important;object-fit:contain !important;}
  .js .saga__iris{position:static !important;clip-path:none !important;}
  .js .saga__frame{position:relative !important;inset:auto !important;top:auto !important;left:auto !important;
    margin:0 !important;transform:none !important;height:auto !important;
    width:100% !important;background:var(--wl-black) !important;}
  .js .saga__photo{position:static !important;opacity:1 !important;transform:none !important;}
  .js .saga__photo picture{height:auto !important;}
  .js .saga__photo img{height:auto !important;object-fit:contain !important;}
  /* R1c: figura KV jest pudełkiem zdjęcia także w stosie statycznym (wtedy wewnątrz kadru stoi też sen) — warstwa
     dymka wraca do procentów pliku (`--bs-*`). */
  .js .saga__photo.saga__photo--kv{position:relative !important;}
  .js .saga__photo .saga__bubble-art{
    left:calc(var(--bs-left) - var(--bs-w) * var(--ba-l)) !important;top:calc(var(--bs-top) - var(--bs-h) * var(--ba-t)) !important;
    width:calc(var(--bs-w) * var(--ba-w)) !important;height:calc(var(--bs-h) * var(--ba-h)) !important;
  }
  .js .saga__outline{display:none !important;}
  .js .saga__bubble{
    position:static !important;width:auto !important;height:auto !important;
    clip-path:none !important;transform:none !important;opacity:1 !important;
  }
  .js .saga__bubble-fill{display:none !important;}
  .js .saga__bubble-inner{
    position:static !important;width:auto !important;height:auto !important;
    margin:0 !important;transform:none !important;opacity:1 !important;
  }
  .js .dream{position:static !important;height:auto !important;background:var(--wl-orange) !important;}
  .js .dream__stage{position:static !important;height:auto !important;overflow:visible !important;background:var(--wl-orange) !important;}
  .js .dream__layer,.js .dream__ground{display:none !important;}
  .js .beat{position:static !important;padding:40px 0 8px !important;}
  .js .beat + .beat{border-top:2px solid rgba(0,0,0,0.18) !important;}
  .js .beat__alfa{
    display:flex !important;position:static !important;width:auto !important;height:auto !important;
    margin:0 !important;transform:none !important;flex-wrap:wrap;align-items:flex-end;
    justify-content:center;gap:20px;padding:0 var(--hub-pad) !important;
  }
  .js .pose{position:static !important;opacity:1 !important;display:block !important;}
  .js .pose--dup{display:none !important;}
  .js .pose img{max-height:26svh !important;max-width:min(70vw,420px) !important;}
  /* `container-type:size` (figura w ruchu) w stosie statycznym zerowałby wysokość pozy —
     figura wylewała się wtedy na zdanie (storyboard R1b, fallback desktop). */
  .js .pose--duo{container-type:normal !important;}
  .js .pose .duo{width:var(--duo-w) !important;margin-left:calc(var(--duo-w) * var(--ovl,0) / 100) !important;}
  .js .pose .duo img{max-height:none !important;max-width:none !important;transform:none !important;}
  .js .beat__line{
    position:static !important;opacity:1 !important;transform:none !important;
    padding:18px var(--hub-pad) 24px !important;
  }
  .js .beat__line--inv{display:none !important;}
  .js .beat__line .hub-display{color:var(--wl-black) !important;}

  /* OUTRO: klatka końcowa i oba zdania wracają do stanu bazowego — zwykłe karty
     na końcu stosu. Kadr wybudzenia zostaje ukryty razem z resztą przypięcia
     (pokazuje to samo zdjęcie co kadr KV wyżej w stosie), ale klatka „pies przy
     misce” i OBA zdania MUSZĄ zostać: to ostatni człon opowieści i jedyne
     miejsce, w którym pada „Pies nie czyta składu”. */
  .js .saga__bowl{
    position:static !important;opacity:1 !important;transform:none !important;
    z-index:auto !important;
  }
  .js .saga__bowl picture{height:auto !important;}
  .js .saga__bowl img{height:auto !important;object-fit:contain !important;}
  .js .saga__cut{display:none !important;}
  .js .saga__bowl-scrim{display:none !important;}
  .js .saga__say,.js .saga__say .hub-display{
    position:static !important;opacity:1 !important;transform:none !important;
    padding:0;
  }
  .js .saga__say{padding:26px var(--hub-pad) 34px !important;background:var(--wl-black) !important;}
  .js .saga__say--wake{padding-top:26px !important;background:var(--wl-black) !important;}
}

/* ---------- 18. HAK PODGLĄDU — tylko do zrzutów ----------------------------
   ?reveal=1 wyłącza natywną oś scrolla i zamraża sagę na zadanym postępie:
     &P=0..1   postęp CAŁEJ sagi (jedna liczba = jedna klatka partytury)
     &z= &d= &i= &w=  nadpisanie pojedynczego odcinka (zoom / sen / iris / outro)
     &kv=<stem>   wymuszenie konkretnego psa z puli
     &y=<ekrany>  przewinięcie przed zrzutem (do scen poza sagą)
   Zrzuty robi scripts/shot.mjs (CDP) — NIE `--window-size`, który na tej
   maszynie nie schodzi poniżej 500 px szerokości.                          */
.reveal .fx{opacity:1 !important;transform:none !important;transition:none !important;}
.reveal .scene:not(.on) .alfa--dare,
.reveal .scene:not(.on) .photo{transform:none !important;}
/* (stan nawigacji nie wymaga już haka podglądu — belka jest widoczna od klatki 1) */
