@import url("./fonts/fonts.css");

/* ==========================================================================
   Adkil — kierunek 01 „Lampa o 22:00"
   Wersja 1.0 — pełna strona. Ruch: styl 1 „Spokojny".
   Źródło prawdy: design.md
   --------------------------------------------------------------------------
   Spis:
   00 Tokeny
   01 Podstawy i typografia
   02 Elementy wspólne (etykieta, znacznik czasu, karty, bloki zastępcze)
   03 Przyciski, linki, focus
   04 Nawigacja
   05 Hero
   06 Pasek zaufania · Problem
   07 Proces
   08 Usługi
   09 Przykłady
   10 Dowody
   11 Cennik
   12 O firmie
   13 Pytania (akordeon)
   14 Kontakt i formularz
   15 Stopka
   16 Podstrony prawne
   17 Ruch
   18 Okno demo
   19 Ziarno
   20 Kursor
   21 Mikro-interakcje
   22 Lenis
   ========================================================================== */

/* 00 Tokeny ================================================================ */

:root {
  /* Paleta — wyłącznie OKLCH, żadnej czystej czerni ani bieli */
  --ground: oklch(0.962 0.014 78);
  --surface: oklch(0.988 0.008 80);
  --surface-sunk: oklch(0.935 0.016 76);
  --ink: oklch(0.255 0.035 48);
  /* „Kawa z mlekiem" — tekst pomocniczy. Odcień i nasycenie od początku
     bez zmian; ruszała się wyłącznie jasność, dwa razy, oba razy pod
     przymusem rachunku.

     2026-08-15: 0.545 → 0.51. Przy 0.545 wychodziło 4.48:1 na --ground
     i 4.11:1 na --surface-sunk, czyli poniżej progu 4.5:1 z „nie do
     negocjacji".

     2026-08-19: 0.51 → 0.44. Dwa powody, oba mierzone.

     1. STARA WARTOŚĆ NIE SPEŁNIAŁA PROGU NA --surface-sunk. Liczby z 2026-08-15
        (5.22:1 i 4.79:1) były policzone BEZ ZIARNA — wbrew zasadzie, którą
        ten sam projekt stawia w design.md: „każdy pomiar kontrastu na tej
        stronie musi uwzględniać ziarno". Z ziarnem ×0.87 (najciemniejszy
        możliwy piksel) 0.51 dawało 4.78:1 na --ground, ale tylko 4.39:1
        na --surface-sunk. To było naruszenie progu, tyle że niewidoczne,
        bo zapisane w dokumentacji jako 4.79.

     2. DYM ZA KURSOREM MUSIAŁ POCIEMNIEĆ. Dym leży POD tekstem i dochodzi
        do pełnego krycia, więc jego czysta barwa jest tłem pod literami —
        a ciemny dym to tło ODEJMUJE luminancji. Przy 0.51 zapas nad progiem
        na --ground wynosił 0.28, co pozwalało przyciemnić tło o marne 5.8%
        (dym byłby wtedy dwa razy słabiej widoczny niż poprzednia, jasna
        wersja). To NIE DYM był ograniczeniem, tylko ten token. Przy 0.44
        zapas rośnie na 1.67 i dym może zejść do oklch(0.870), czyli
        przyciemnić papier o 26.1%.

     Zmierzone przy 0.44, ziarno ×0.87:
       --ground        6.17:1     --surface-sunk  5.67:1
       --surface       6.60:1     na czystym dymie oklch(0.870): 4.65:1
     Wszystkie z zapasem; --surface-sunk wraca nad próg pierwszy raz.

     CENA: tekst pomocniczy ciemnieje na CAŁEJ stronie, nie tylko pod dymem.
     Różnica wobec --ink (0.255) zostaje czytelna, ale jest wyraźnie mniejsza
     niż była — hierarchia „główny / pomocniczy" opiera się teraz bardziej
     na stopniu pisma niż na samej barwie. To była świadoma decyzja przy
     wyborze wariantu C dla dymu (design.md, „Paleta → zasady koloru”). */
  --ink-muted: oklch(0.44 0.028 55);
  --line: oklch(0.885 0.020 70);
  --accent: oklch(0.735 0.155 68);
  --accent-deep: oklch(0.505 0.130 58);

  /* Miód rozświetlony — barwa KRĘGU ŚWIATŁA. Po przycięciu do gamutu
     sRGB wychodzi rgb(255, 242, 149): jasny, ciepły miód.

     DO 2026-08-19 BYŁA TO TAKŻE BARWA DYMU ZA KURSOREM. Już nie —
     dym ma własny token --smoke i jest neutralny (nota niżej). Miód
     zostaje przy kręgu, bo tam jest na miejscu: krąg UDAJE ŚWIATŁO LAMPY,
     a lampa w tej scenie świeci ciepło. Dym niczego nie oświetla.

     Wartość jest wynikiem przeszukania siatki (jasność × chroma × odcień),
     nie doboru na oko. Maksymalizowana była różnica percepcyjna od
     --ground w OKLab, przy dwóch twardych ograniczeniach:

     1. KONTRAST. Krąg leży pod tekstem, a jego rdzeń ma alfę 0.60 —
        tło pod literami nigdy nie wyjdzie poza czysty --accent-glow, więc
        kontrast liczony na tej barwie jest podłogą dla dowolnej alfy.
        Zmierzone dla tekstu pomocniczego, z ziarnem nałożonym na tekst
        i tło:
          – najciemniejszy możliwy piksel ziarna (×0.87): 6.02:1
          – średnie ziarno (×0.935):                      6.41:1
          – bez ziarna:                                   6.84:1
        Tekst główny nie schodzi poniżej 10.8:1.
        (Przeliczone 2026-08-19 po przyciemnieniu --ink-muted na 0.44 —
        poprzednio odpowiednio 4.64 / 4.87 / 5.09. Krąg nie zmienił się
        ani o jotę; zmienił się tekst, który na nim leży.)

        Ta podłoga była kiedyś liczona dla dymu (który nakłada się sam na
        siebie do pełnego krycia) i krąg ją DZIEDZICZYŁ. Teraz jest
        odwrotnie: liczba stoi na własnych nogach przy kręgu, a dym ma
        własny pomiar przy --smoke. Rozejście tokenów rozeszło też dowody.

     2. ODCIEŃ ZOSTAJE W RODZINIE MIODU (62–80). Bez tego ograniczenia
        optymalizator wychodzi na czystą żółć rgb(255, 252, 0), a nawet
        limonkę — formalnie najlepsze wyniki, ale to już inny kolor niż
        --accent i złamanie zasady jednego nasyconego koloru.

     Wysoka jasność jest tu narzędziem, nie ozdobą: dopiero ona pozwala
     unieść chromę do 0.13 bez utraty luminancji, a to chroma odpowiada
     za to, że krąg nie zlewa się z ciepłym papierem.

     Historia, żeby nie cofnąć się w któryś ślepy zaułek:
       oklch(0.93 0.075 70)  — liczone BEZ ziarna, po nałożeniu 4.19:1,
                               czyli poniżej progu. Błąd.
       oklch(0.995 0.12 70)  — poprawne 4.60:1, ale odcień prawie taki sam
                               jak tło, więc efekt był ledwo widoczny.
       oklch(1.0 0.16 80)    — widoczny, ale odcień odjechał w złoto
                               i wypadał z palety.
       oklch(0.995 0.13 76)  — obecne. Punkt pośredni: różnica od tła
                               o 19% większa niż przy wersji bladej,
                               a kanał niebieski (149) wraca w okolice
                               miodu (162) zamiast złota (116).
     PRZY ZMIANIE TEJ WARTOŚCI POWTÓRZYĆ POMIAR. */
  --accent-glow: oklch(0.995 0.13 76);
  --on-accent: oklch(0.220 0.030 60);

  /* Dym za kursorem — SZARY, neutralny, świadomie poza rodziną akcentu.
     Po przycięciu do gamutu sRGB: rgb(215, 212, 206). Wdrożone 2026-08-19
     (wariant C, patrz design.md „Paleta → zasady koloru").

     DLACZEGO NIE MIÓD. Dym z papierosa nie ma własnej barwy — jest
     zawiesiną, która rozprasza i pochłania światło. Do 2026-08-19 dym brał
     --accent-glow, czyli barwę kręgu światła, i przez to czytał się jako
     druga miodowa plama w kadrze, w którym krąg już stoi: formalne złamanie
     „maksymalnie jedna duża plama akcentu na ekran", a w oku po prostu
     rozlany akcent zamiast dymu.

     DLACZEGO CIEMNY, SKORO PO DRODZE BYŁ JASNY. Bo jasny nie był dymem,
     tylko mgiełką. Droga do tej wartości przebiegła przez ślepy zaułek
     i warto go opisać, żeby nikt nie wracał:

       oklch(0.995 0.008 85) → oklch(1.0 0.012 90)  — biel, dwie iteracje.
       Kierunek wymuszony rachunkiem: przy ÓWCZESNYM --ink-muted (0.51)
       tekst pomocniczy miał na papierze tylko 0.28 zapasu nad progiem,
       więc dym mógł przyciemnić tło o marne 5.8%, a rozjaśnić o 11.4%.
       Jasny wygrywał liczbowo — i przegrywał w oku, bo biel na kremowym
       papierze jest po prostu mało widoczna, a rozjaśnianie nie wygląda
       jak dym, tylko jak przetarcie.

     WŁAŚCIWYM OGRANICZENIEM NIE BYŁ DYM, TYLKO --ink-muted. Dym leży POD
     tekstem i dochodzi do pełnego krycia, więc jego czysta barwa jest tłem
     pod literami; ciemny dym to tło przyciemnia, a więc ZJADA kontrast
     zamiast go dokładać. Dopóki tekst pomocniczy stał na 0.51, każdy
     naprawdę szary dym leciał pod próg:
       oklch(0.90) → 3.94:1   oklch(0.80) → 2.87:1   oklch(0.50) → 1.02:1
     Dopiero przyciemnienie --ink-muted do 0.44 (nota przy tokenie — było
     do zrobienia i tak, bo stara wartość nie spełniała progu na
     --surface-sunk) otworzyło miejsce na prawdziwą szarość.

     KONTRAST — PEŁNY POMIAR. Podłogą jest czysty --smoke, bo w gorących
     miejscach dym dochodzi do pełnego krycia. Tekst pomocniczy:
          – najciemniejszy możliwy piksel ziarna (×0.87): 4.65:1
          – średnie ziarno (×0.935):                      4.98:1
          – bez ziarna:                                   5.28:1
     Tekst główny na czystym dymie: 8.44:1.

     RAMPY POŚREDNIE SPRAWDZONE OSOBNO, bo tym razem NIE wystarczy pokazać
     końców przedziału. Przy jasnym dymie każdy kanał rósł monotonicznie
     i wartości pośrednie musiały leżeć między końcami. Tu, nad rdzeniem
     kręgu światła, przejście z rgb(255,242,149) na rgb(215,212,206) ma
     kanały rozbieżne (R −40, G −30, B +57), więc monotoniczność trzeba
     było policzyć, a nie założyć. Policzona:
       nad --ground:  6.17 → 5.76 → 5.37 → 5.01 → 4.65  (alfa 0…1)
       nad kręgiem:   6.02 → 5.69 → 5.43 → 5.17 → 4.92 → 4.65
     Oba ciągi schodzą monotonicznie do podłogi — nigdzie nie ma zapadki
     poniżej wartości końcowej, więc próg trzyma się przy dowolnej gęstości.

     ODCIEŃ. Chroma 0.008 przy odcieniu 80 to ciepła szarość, nie szarość
     obojętna — paleta nie dopuszcza neutralnych bez podbarwienia w stronę
     światła lampy. Przy tej chromie odcień jest już poniżej progu
     widoczności; 80 trzyma go po ciepłej stronie „na wszelki wypadek".
     Wyżej z chromą nie wchodzimy: szary dym z wyczuwalnym kolorem
     przestaje być dymem i zaczyna być brudem na ekranie.

     WIDOCZNOŚĆ. Dym przyciemnia papier o 26.1% luminancji, |ΔL| w OKLab
     wynosi 0.092 — dla skali: poprzednia, jasna wersja dawała 11.4%
     i 0.038, czyli obecny dym jest 2.4× mocniejszy percepcyjnie. To jest
     ta różnica, przez którą smugę widać na gołym papierze, a nie tylko
     tam, gdzie przecina krąg światła.

     PRZY ZMIANIE TEJ WARTOŚCI POWTÓRZYĆ POMIAR — i pamiętać, że --smoke
     i --ink-muted są teraz SPRZĘŻONE: każde pociemnienie dymu wymaga
     zapasu w tekście pomocniczym i odwrotnie. Wartość dla tła ciemnego
     stoi przy .about i rządzi się osobnym rachunkiem. */
  --smoke: oklch(0.870 0.008 80);

  /* Przyciemnienie pod oknem modalnym. Nie czerń — przyciemniony --ink,
     żeby tło pozostało ciepłe, zgodnie z zasadą palety. */
  --scrim: oklch(0.255 0.035 48 / 0.62);

  /* Stany formularza. Ta sama jasność co --accent-deep, więc komunikat
     nie wyskakuje z ciepłej palety — niesie tylko informację. */
  --ok: oklch(0.505 0.105 152);
  --err: oklch(0.505 0.150 28);

  /* Kroje */
  --font-display: "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif;
  --font-body: "Instrument Sans", ui-sans-serif, system-ui, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", monospace;

  /* Rytm — skala 4 px */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-12: 3rem;
  --space-16: 4rem;
  --space-24: 6rem;
  --space-32: 8rem;

  /* Powtarzalne wartości */
  --wrap: 72rem;
  --gutter: clamp(1.25rem, 5vw, 3rem);
  --read: 42rem;
  --section-gap: clamp(4rem, 10vw, 8rem);
  --card-pad: clamp(1.25rem, 2.5vw, 2rem);
  --r-sm: 0.7rem;
  --r-card: 1.25rem;
  --r-pill: 999px;

  /* Ruch */
  --ease: cubic-bezier(0.2, 0, 0, 1);
  --t-enter: 380ms;    /* drabinka hero — wchodzi 6 elementów po kolei,
                          więc pojedynczy krok musi zostać krótki */
  --t-reveal: 560ms;   /* wejście sekcji przy przewijaniu — dłuższe celowo,
                          tu element jest na ekranie sam i ma być zauważony */
  --t-ui: 160ms;
  --t-fold: 240ms;

  /* Dystans wejścia. Hero zostaje przy 12 px (drabinka sześciu elementów
     przy większym skoku robi się niespokojna), sekcje przy przewijaniu
     dostają 22 px — ruch ma być widoczny, nie domyślany. */
  --shift-enter: 12px;
  --shift-reveal: 22px;

  interpolate-size: allow-keywords;
}

/* 01 Podstawy i typografia ================================================= */

*,
*::before,
*::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: 6rem;
  /* clip, nie hidden — hidden zrobiłby z html kontener przewijania
     i zepsuł position: sticky w nagłówku */
  overflow-x: clip;
}

body {
  margin: 0;
  min-height: 100svh;
  /* Powtórzone z html: przeglądarka przenosi overflow z html na viewport,
     przez co samo html nic nie przycina. Dopiero clip na body faktycznie
     ucina to, co wystaje poza prawą krawędź. */
  overflow-x: clip;
  background-color: var(--ground);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  line-height: 1.65;
  font-synthesis-weight: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

.wrap {
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
  position: relative;
  z-index: 1;
}

h1, h2, h3 {
  font-family: var(--font-display);
  margin: 0;
  text-wrap: balance;
}

h1 {
  font-weight: 700;
  font-size: clamp(2.25rem, 6vw, 4.25rem);
  line-height: 1.04;
  letter-spacing: -0.03em;
}

h2, .sec-head__title {
  font-weight: 700;
  font-size: clamp(1.6rem, 3.6vw, 2.5rem);
  line-height: 1.12;
  letter-spacing: -0.02em;
}

h3 {
  font-weight: 700;
  font-size: clamp(1.15rem, 2vw, 1.45rem);
  line-height: 1.2;
  letter-spacing: -0.01em;
}

p { margin: 0 0 var(--space-4); max-width: 66ch; }
p:last-child { margin-bottom: 0; }

.lead {
  font-size: clamp(1.05rem, 1.6vw, 1.3rem);
  line-height: 1.6;
  max-width: 62ch;
}

em { font-style: normal; color: var(--accent-deep); }
strong { font-weight: 600; }

/* Nagłówek sekcji — zawsze do lewej, nigdy wyśrodkowany */
.sec-head { margin-bottom: var(--space-8); max-width: 46rem; }
.sec-head__note { margin-top: var(--space-4); color: var(--ink-muted); font-size: 0.9rem; }

section { padding-block: var(--section-gap); }

/* 02 Elementy wspólne ====================================================== */

.label {
  font-family: var(--font-mono);
  font-weight: 400;
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin: 0 0 var(--space-4);
}

/* Znacznik czasu — sygnatura marki (maks. 4 na stronie) */
.stamp {
  margin: var(--space-8) 0 0;
  border-top: 1px solid var(--line);
  max-width: 26rem;
}

.stamp__row {
  display: flex;
  gap: var(--space-4);
  align-items: baseline;
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--line);
}

.stamp dt {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 0.9rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  flex: none;
}

.stamp dd {
  margin: 0;
  font-size: 0.9rem;
  color: var(--ink-muted);
}

.stamp--inline { margin-top: var(--space-12); }
.stamp--quote { margin-top: var(--space-6); max-width: 30rem; }

/* Karta — głębia z koloru tła, nigdy z cienia */
.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: var(--card-pad);
}

/* Krąg światła — lampa poza kadrem. Maks. 3 na stronie.

   ===== PRZEBUDOWA 2026-08-18: --accent na --accent-glow =====

   Krąg był zbudowany z --accent, czyli rgb(232, 148, 27) — barwy
   CIEMNIEJSZEJ od --ground rgb(248, 242, 232). „Światło lampy" fizycznie
   PRZYCIEMNIAŁO stronę w miejscu, w którym miało świecić. Stąd brały się
   naraz dwie rzeczy: mulisty, ledwo widoczny efekt i zjadanie kontrastu.

   Krąg leży pod treścią (z-index -1), więc podlega temu samemu rachunkowi
   co dym. Liczby niżej pochodzą z pomiaru przy ÓWCZESNYM --ink-muted (0.51)
   i tak zostają, bo to one uzasadniają wycofanie --accent — po
   przyciemnieniu tekstu na 0.44 (2026-08-19) wszystkie urosły o ~1.2
   i dziś żadna nie jest już naruszeniem. Zmierzone dla tekstu pomocniczego
   z ziarnem ×0.87 na --ground:

     --accent  @ 0.16 (poprzednia wersja):  4.23:1  ← WTEDY PONIŻEJ PROGU
     --accent  @ 0.24:                      3.98:1
     --accent  @ 0.32:                      3.75:1

   Poprzednia wersja NIE spełniała progu 4.5:1 z „nie do negocjacji”, a
   każde wzmocnienie alfą pogłębiało naruszenie zamiast je naprawiać.
   Dlatego wzmocnienie MUSIAŁO pójść przez zmianę barwy, nie przez alfę.

   --accent-glow rgb(255, 242, 149) jest JAŚNIEJSZY od --ground, więc
   działa jak prawdziwe światło: dokłada luminancji, zamiast ją odejmować.
   Kompozycja na --ground to interpolacja między rgb(248,242,232)
   a rgb(255,242,149) — kanał G stały (242), R rośnie o 7, spada tylko
   B (waga luminancji 0.0722). Kontrast na końcach przedziału:

     alfa 0.00 (czyste --ground):      6.17:1
     alfa 0.60 (rdzeń kręgu):          6.06:1
     alfa 1.00 (czysty --accent-glow): 6.02:1   ← podłoga, patrz token

   (Przeliczone 2026-08-19 przy --ink-muted 0.44; poprzednio 4.75 / 4.67
   / 4.64. Sam krąg nie drgnął — zmienił się tekst, który na nim leży.)

   Wszystkie wartości pośrednie leżą między tymi końcami, więc PRÓG TRZYMA
   SIĘ PRZY DOWOLNEJ ALFIE. Krąg dziedziczy dowód bezpieczeństwa postawiony
   przy --accent-glow dla dymu: to ta sama barwa, pod tym samym tekstem.
   Tekst główny nie schodzi poniżej 10.8:1 (przy rdzeniu 0.60: 10.96:1).

   SIŁA RDZENIA: 0.60, zejście z 0.85 na wyraźną decyzję (2026-08-18).
   Przy 0.85 krąg zalewał ciepłą żółcią zbyt dużą część kadru w hero.
   Odległość barwna od tła spada z 70.8 na 50.0 — nadal o 38% dalej od
   tła niż wycofana wersja na --accent (36.2), czyli krąg zostaje wyraźnie
   widoczny, tylko przestaje dominować.

   CAŁA RAMPA JEST SKALOWANA RAZEM — to nie jest jedna liczba do ruszenia.
   Zejście z samego stopu 0% zostawiłoby stop przy 26% na 0.62, czyli
   JAŚNIEJSZY OD ŚRODKA, i krąg dostałby pierścień zamiast rdzenia.
   Przy zmianie siły mnożyć wszystkie stopy przez ten sam współczynnik
   (tu: 0.60/0.85 ≈ 0.706), żeby zachować kształt zejścia.

   ODRZUCONE: domieszka --accent do rdzenia „dla ciepła”. Glow 0.85
   + accent 0.10 daje rgb(252, 233, 149) — ładniejsze w oku, ale 4.35:1,
   czyli poniżej progu. --accent jest ciemny i ściąga luminancję w dół
   niezależnie od tego, jak mało się go doda. Rdzeń zostaje CZYSTY.

   Zasięg gradientu wydłużony z 68% na 80%: przy krótkim zejściu krąg
   kończył się widoczną krawędzią zamiast rozpłynąć się w tle. */
.glow { position: relative; isolation: isolate; }

.glow::before {
  content: "";
  position: absolute;
  z-index: -1;
  width: min(90vw, 60rem);
  aspect-ratio: 1;
  pointer-events: none;
  background: radial-gradient(
    circle at center,
    oklch(0.995 0.13 76 / 0.60) 0%,
    oklch(0.995 0.13 76 / 0.44) 26%,
    oklch(0.995 0.13 76 / 0.21) 48%,
    oklch(0.995 0.13 76 / 0.07) 65%,
    oklch(0.995 0.13 76 / 0) 80%
  );
}

.glow--right::before { right: -18%; top: -22%; }
.glow--left::before { left: -24%; top: -10%; }

/* Bloki zastępcze — obraz: USUNIĘTE. Rodzina .ph-media (ramka przerywana,
   .ph-media--portrait, --map, __tag, __desc, __meta, __caption) straciła
   ostatniego użytkownika, gdy blok zastępczy mapy w sekcji kontaktu ustąpił
   miejsca paskowi zasięgu — patrz .contact__reach niżej. Do odtworzenia
   z historii: `git show a1502b8:assets/styles.css`.

   .ph-text ZOSTAJE: używa go jeszcze proof.note w assets/i18n.js, czyli
   teksty zdjętej sekcji „Dowody", które czekają na prawdziwe opinie. */

/* Bloki zastępcze — tekst */
.ph-text {
  text-decoration: underline dashed 1px;
  text-decoration-color: var(--accent);
  text-underline-offset: 4px;
}

/* 03 Przyciski, linki, focus =============================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0.85rem 1.4rem;
  border-radius: var(--r-sm);
  border: 1px solid transparent;
  font-family: var(--font-body);
  font-size: 1rem;
  font-weight: 600;
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  transition: transform var(--t-ui) var(--ease), background-color var(--t-ui) var(--ease);
}

.btn--primary {
  background: var(--accent);
  color: var(--on-accent);
}

.btn--primary:hover { background: oklch(0.695 0.155 66); transform: translateY(-2px); }

/* Magnetyczny przycisk główny. Klasę dokłada site.js i tylko tam, gdzie
   jest prawdziwy kursor i włączony ruch — na dotyku i przy
   prefers-reduced-motion przycisk zostaje zwykłym .btn--primary.

   Przesunięcie liczy JS w rAF (własne wygładzanie), dlatego transform tu
   nie ma przejścia — inaczej ruch spóźniałby się o kolejne 160 ms i robił
   się gumowy. Zostaje wyłącznie przejście tła.
   Uniesienie z :hover dokładamy do tej samej funkcji translate, żeby oba
   efekty się składały, a nie nadpisywały. */
.btn--magnetic {
  transform: translate3d(var(--mag-x, 0px), var(--mag-y, 0px), 0);
  transition: background-color var(--t-ui) var(--ease);
}

.btn--magnetic:hover {
  transform: translate3d(var(--mag-x, 0px), calc(var(--mag-y, 0px) - 2px), 0);
}

.btn--ghost {
  background: transparent;
  border-color: var(--line);
  color: var(--ink);
}

.btn--ghost:hover { background: var(--surface); transform: translateY(-2px); }

.btn--sm { padding: 0.6rem 1rem; font-size: 0.95rem; }
.btn--full { width: 100%; }

/* WhatsApp — druga droga kontaktu, nigdy przycisk główny.
   Znak rysowany liniowo (1.5 px), zgodnie z resztą ikon: design.md
   zakazuje emoji w interfejsie, a logo w kółeczku to „tania strona". */
.btn--wa { gap: 0.6rem; }

.wa-mark {
  flex: none;
  width: 1.25rem;
  height: 1.25rem;
}

.wa-mark .ico-line { stroke: var(--accent-deep); }

a { color: var(--accent-deep); }

.link,
.qa__a a,
p a {
  color: var(--accent-deep);
  text-decoration: underline 1px;
  text-underline-offset: 3px;
}

.skip-link {
  position: absolute;
  left: var(--gutter);
  top: 0;
  transform: translateY(-120%);
  z-index: 99;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 0.75rem 1rem;
  text-decoration: none;
  font-weight: 600;
}

.skip-link:focus { transform: translateY(var(--space-2)); }

:focus-visible {
  outline: 3px solid var(--accent-deep);
  outline-offset: 3px;
  border-radius: 2px;
}

/* 04 Nawigacja ============================================================= */

/* Pasek stoi na tle strony (--ground), więc póki jesteśmy w hero jest
   praktycznie niewidoczny — widać samą treść. Po minięciu hero site.js
   dokłada .is-quiet: tło przechodzi na --surface, a kreska ciemnieje.
   Bez rozmycia i bez efektu szkła — design.md zakazuje obu wprost.
   Animujemy wyłącznie kolory: żadnej zmiany wysokości, więc pasek nie
   przesuwa treści pod sobą. */
.site-head {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--ground);
  border-bottom: 1px solid var(--line);
  transition:
    background-color var(--t-ui) var(--ease),
    border-bottom-color var(--t-ui) var(--ease);
}

.site-head.is-quiet {
  background: var(--surface);
  border-bottom-color: oklch(0.845 0.024 70);
}

.site-head__inner {
  max-width: var(--wrap);
  margin-inline: auto;
  padding: var(--space-3) var(--gutter);
  display: flex;
  align-items: center;
  gap: var(--space-6);
}

.brand {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.05rem, 2.2vw, 1.25rem);
  letter-spacing: -0.02em;
  line-height: 1;
  color: var(--ink);
  text-decoration: none;
  margin: 0;
  margin-right: auto;
  white-space: nowrap;
}

.brand__dot {
  display: inline-block;
  width: 0.34em;
  height: 0.34em;
  margin-left: 0.18em;
  border-radius: var(--r-pill);
  background: var(--accent);
  vertical-align: 0.08em;
}

.nav__list {
  display: flex;
  gap: var(--space-6);
  list-style: none;
  margin: 0;
  padding: 0;
}

.nav a {
  color: var(--ink);
  text-decoration: none;
  font-size: 0.95rem;
  position: relative;
  padding-block: var(--space-2);
  white-space: nowrap;
}

.nav a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0.15rem;
  height: 1px;
  background: var(--accent-deep);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 180ms var(--ease);
}

.nav a:hover::after { transform: scaleX(1); }

/* Przełącznik języka — flagi SVG (nie emoji: emoji flag nie renderuje się
   na Windowsie i design.md zabrania emoji w interfejsie) */
.lang-switch {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex: none;
}

.flag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  border-radius: var(--r-sm);
  text-decoration: none;
  transition: opacity var(--t-ui) var(--ease), transform var(--t-ui) var(--ease);
}

.flag__img {
  display: block;
  width: 22px;
  height: auto;
  border: 1px solid var(--line);
  border-radius: 2px;
  overflow: hidden;
}

/* Nieaktywny język: bez koloru. Aktywny: pełna flaga. */
.flag:not(.is-active) .flag__img { filter: grayscale(1); opacity: 0.5; }
.flag:hover .flag__img { filter: none; opacity: 1; }
.flag:hover { transform: translateY(-1px); }
.flag.is-active { cursor: default; }

.menu-toggle {
  display: none;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: 0.5rem 0.75rem;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  cursor: pointer;
}

.menu-toggle__bars { display: grid; gap: 3px; }
.menu-toggle__bars span {
  display: block;
  width: 16px;
  height: 1.5px;
  background: currentColor;
}

/* Wiersz z językiem istnieje tylko w menu mobilnym — powyżej progu
   przełącznikiem są flagi w pasku. Stan domyślny musi stać PRZED zapytaniem
   medialnym, które go włącza: obie reguły mają tę samą wagę, więc decyduje
   kolejność w pliku. */
.nav__lang { display: none; }
.nav__lang .lang { margin: 0; align-items: center; }

/* Cel dotykowy 44 px — w stopce ten sam przełącznik jest drobny, bo stoi
   w gęstym bloku tekstu; w menu jest pełnoprawną pozycją listy. */
.nav__lang .lang a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
}

.site-head__cta { white-space: nowrap; }

/* Próg 1024, nie 900: pasek mieści brand + 6 pozycji + flagi + CTA dopiero
   od tej szerokości. Poniżej — menu pod przyciskiem, bez ściskania. */
@media (max-width: 1024px) {
  .nav {
    display: none;
    position: absolute;
    left: var(--gutter);
    right: var(--gutter);
    top: calc(100% + var(--space-2));
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-card);
    padding: var(--space-4);
  }

  .menu-open .nav { display: block; }

  .nav__list { flex-direction: column; gap: 0; }
  .nav__list li + li { border-top: 1px solid var(--line); }
  .nav a { display: block; padding: var(--space-3) var(--space-2); font-size: 1rem; }

  .menu-toggle { display: inline-flex; }
  .site-head__inner { position: relative; gap: var(--space-3); }

  /* Główne CTA zostaje w pasku także na telefonie — design.md, komponent
     „Nawigacja": na mobile pasek to nazwa + przycisk, reszta w rozwijanym
     menu. Miejsce na nie robią flagi: schodzą do menu jako tekstowe PL/DE
     (.nav__lang), bo przy 375 px brand + flagi + CTA + przycisk menu nie
     mieszczą się w jednym wierszu, a skracanie etykiety CTA jest zakazane —
     ma brzmieć tak samo we wszystkich czterech miejscach. */
  .site-head__cta { display: inline-flex; padding: 0.55rem 0.9rem; font-size: 0.9rem; }
  .lang-switch { display: none; }
  .nav__lang { display: block; }
}

/* Najwęższe telefony (320–380 px): przycisk menu jest już bez napisu
   (patrz niżej), CTA schodzi o pół kroku, żeby wiersz nie pękł. */
@media (max-width: 380px) {
  .site-head__cta { padding: 0.5rem 0.7rem; font-size: 0.8rem; }
  .site-head__inner { gap: var(--space-2); }
}

/* Wąskie pasmo tuż nad progiem: 6 pozycji + flagi + długie CTA („Kostenlose
   Demo erhalten") nie mieszczą się przy pełnych odstępach. Zamiast podnosić
   próg i zabierać tabletom poziome menu — zagęszczamy pasek na tych kilku
   setkach pikseli. Powyżej 1200 px wszystko wraca do normy. */
@media (min-width: 1025px) and (max-width: 1200px) {
  .site-head__inner { gap: var(--space-4); }
  .nav__list { gap: var(--space-4); }
  .nav a { font-size: 0.9rem; }
  .site-head__cta { padding: 0.55rem 0.85rem; font-size: 0.9rem; }
}

@media (max-width: 420px) {
  .menu-toggle__text { display: none; }
}

/* 05 Hero ================================================================== */

.hero {
  padding-top: clamp(3rem, 8vw, 6rem);
  padding-bottom: var(--section-gap);
}

.hero__inner {
  display: grid;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: start;
}

@media (min-width: 900px) {
  .hero__inner {
    grid-template-columns: 7fr 5fr;
    align-items: center;
  }
}

.hero__title { margin-bottom: var(--space-6); }

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-6);
  margin-top: var(--space-8);
}

/* Lead w dwóch długościach: krótki na telefonie, pełny od 700 px w górę.
   Próg 700, nie 900 (tam hero łamie się na dwie kolumny): tablet w pionie
   ma dość miejsca na pełne trzy zdania, a chodzi o telefon, gdzie tekst
   spycha pierwszy przycisk poza ekran.
   Domyślnie widoczna jest wersja krótka, a pełna dopiero w zapytaniu
   medialnym — przy odwrotnej kolejności telefon musiałby najpierw wczytać
   i schować dłuższy akapit. */
.lead--full { display: none; }

@media (min-width: 700px) {
  .lead--short { display: none; }
  .lead--full { display: block; }
}

/* Cena pod przyciskami. Drobna i stonowana — ma być do znalezienia wzrokiem,
   nie ma konkurować z przyciskiem demo stojącym tuż nad nią. Sam link bierze
   --accent-deep z reguły „p a" wyżej, czyli tyle akcentu, co każdy inny link
   w tekście. */
.hero__price {
  margin: var(--space-4) 0 0;
  font-size: 0.9rem;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

/* Wyliczenie branż pod leadem — ma być czytelne, ale nie konkurować
   z lead-em ani z przyciskami. */
.hero__branches {
  margin: var(--space-4) 0 0;
  color: var(--ink-muted);
  font-size: 0.95rem;
  max-width: 46ch;
}

.hero__media { margin: 0; }

/* EKSPERYMENT (2026-08-15, do oceny) — paralaksa hero na dotyku.
   design.md, „Zasada umiaru" zakazuje paralaksy wprost, a warstwa
   dopracowania odrzuciła ją osobno. Tu jest włączona świadomie i tylko po
   to, żeby zobaczyć efekt na żywo na telefonie. Wycofanie: usunąć te dwie
   reguły i wywołanie heroParallax() w site.js — nic innego od nich
   nie zależy.

   Klasę dokłada site.js wyłącznie przy grubym wskaźniku (palec) i przy
   włączonym ruchu, więc na komputerze i przy prefers-reduced-motion nie
   ma jej wcale.

   Promień przechodzi na figurę: obraz jest powiększony i przycinany, więc
   zaokrąglenie na nim samym zostałoby ścięte przez kadr. */
.hero--parallax .hero__media {
  overflow: hidden;
  border-radius: var(--r-card);
}

/* Powiększenie musi pokryć pełny zakres przesunięcia, inaczej przy krawędzi
   wyszłaby luka. Zapas to (scale − 1) / 2 wysokości kadru w każdą stronę,
   czyli przy 1.14 równo 7% — i tyle właśnie site.js bierze jako amplitudę.
   Skala stoi we WŁASNYM tokenie, bo skrypt ją odczytuje: wcześniej ta sama
   liczba była wpisana w dwóch miejscach z notatką „zmieniając jedno, poprawić
   drugie", a to jest dokładnie ten rodzaj zgodności, który prędzej czy później
   się rozjeżdża. Teraz źródło jest jedno.

   Bez will-change: nad stroną leży ziarno z mix-blend-mode i każda dodatkowa
   warstwa kompozytora pod nim kosztuje (patrz design.md, „Pułapka
   wydajnościowa"). translate3d wystarcza. */
.hero--parallax .hero__img {
  --par-scale: 1.14;
  transform: translate3d(0, var(--par-y, 0px), 0) scale(var(--par-scale));
}

/* Proporcja trzymana w CSS, nie przez wymiary pliku: gdyby kiedyś przyszło
   zdjęcie o innym kadrze, układ się nie ruszy — obraz dostanie przycięcie.
   Bez cienia, głębia z samego zdjęcia (design.md, karta). */
.hero__img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--r-card);
  background: var(--surface-sunk);
}

/* 06 Pasek zaufania · Problem · Demo ======================================= */

.trust {
  padding-block: 0;
  border-block: 1px solid var(--line);
  background: var(--surface-sunk);
}

.trust__inner {
  display: grid;
  gap: var(--space-4);
  padding-block: var(--space-6);
}

@media (min-width: 900px) {
  .trust__inner {
    grid-template-columns: repeat(3, auto);
    justify-content: space-between;
    gap: var(--space-8);
  }
}

.trust__item {
  margin: 0;
  display: flex;
  gap: var(--space-3);
  align-items: baseline;
  font-size: 0.95rem;
  max-width: none;
}

.trust__no {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

.problem__inner { max-width: 52rem; }

.problem__line {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.6rem, 3.6vw, 2.5rem);
  line-height: 1.12;
  letter-spacing: -0.02em;
  max-width: 22ch;
  text-wrap: balance;
  margin-bottom: var(--space-6);
}

.problem__note {
  color: var(--ink-muted);
  max-width: 56ch;
  padding-left: var(--space-4);
  border-left: 1px solid var(--line);
}

/* Lustro: co myśli ktoś, kto właśnie otworzył przestarzałą stronę.
   Pytania w kolumnach, ale bez kresek i numerów — inaczej czytałyby się
   jak trzeci wariant „Trzech kroków" i ścieżki demo. */
.mirror {
  list-style: none;
  margin: 0 0 var(--space-16);
  padding: 0;
  display: grid;
  gap: var(--space-8);
}

@media (min-width: 780px) {
  .mirror {
    grid-template-columns: repeat(3, 1fr);
    gap: var(--space-8);
  }
}

.mirror__q {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.05rem, 1.9vw, 1.25rem);
  line-height: 1.25;
  letter-spacing: -0.01em;
  margin: 0 0 var(--space-3);
  text-wrap: balance;
}

.mirror__a {
  margin: 0;
  color: var(--ink-muted);
  font-size: 0.95rem;
  max-width: 36ch;
}

/* Zwrot akcji: od „co tracisz" do „ile kosztuje to naprawić" */
.problem__turn {
  margin: 0 0 var(--space-3);
  color: var(--ink-muted);
  font-size: 0.95rem;
}

/* Ścieżka do demo — cztery przystanki.
   Celowo lżejsza od numerowanych wierszy w „Trzy kroki": dwie takie same
   listy jedna pod drugą czytałyby się jak powtórzona sekcja. Tu kreska nad
   przystankiem zamiast ikony i ramki. */
.demo__path {
  list-style: none;
  margin: 0 0 var(--space-12);
  padding: 0;
  display: grid;
  gap: var(--space-8);
}

@media (min-width: 760px) {
  .demo__path {
    grid-template-columns: repeat(4, 1fr);
    gap: var(--space-6);
  }
}

.demo__stop { padding-top: var(--space-4); border-top: 2px solid var(--line); }

/* Ostatni przystanek to moment decyzji klienta — jedyny akcent w sekcji. */
.demo__stop:last-child { border-top-color: var(--accent); }

.demo__no {
  margin: 0 0 var(--space-3);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted);
}

.demo__stop h3 { margin-bottom: var(--space-2); font-size: 1.05rem; }

.demo__text {
  margin: 0;
  color: var(--ink-muted);
  font-size: 0.95rem;
  max-width: 34ch;
}

.demo__promise {
  margin: 0;
  max-width: 52rem;
  padding: var(--card-pad);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  font-size: 1.05rem;
}

/* 07 Proces ================================================================ */

.process { background: var(--ground); }

.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--line);
}

/* Ponumerowane wiersze, nie siatka identycznych kart */
.step {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-4) var(--space-6);
  align-items: start;
  padding-block: clamp(1.5rem, 3vw, 2.5rem);
  border-bottom: 1px solid var(--line);
}

@media (min-width: 720px) {
  .step {
    grid-template-columns: 4rem 4.5rem 1fr;
    gap: var(--space-8);
    align-items: center;
  }
}

.step__no {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 0.9rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted);
  padding-top: 0.35rem;
}

.step__icon {
  width: 3.25rem;
  color: var(--ink);
}

.step__icon svg { display: block; width: 100%; height: auto; }
.step__icon .ico-dot { color: var(--accent); }
.step__icon .ico-line { stroke: var(--accent-deep); }

.step__body { grid-column: 1 / -1; }

@media (min-width: 720px) {
  .step__body { grid-column: auto; }
}

.step__body h3 { margin-bottom: var(--space-2); }
.step__body p { color: var(--ink-muted); max-width: 56ch; }

.cta-band {
  margin-top: var(--space-12);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-6);
  justify-content: space-between;
  padding: var(--card-pad);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
}

.cta-band__text { margin: 0; font-size: 1.05rem; max-width: 44ch; }

/* 08 Usługi ================================================================ */

/* Dwie usługi główne (co buduję) + cztery pozycje wliczone w abonament.
   Podział 2 + 4 zamiast sześciu identycznych kart — patrz design.md,
   „czego unikać": szablonowych układów. Bez kręgu światła: limit trzech
   na stronę jest już wykorzystany (hero, proces, kontakt). */

.services__grid {
  display: grid;
  gap: var(--space-6);
  align-items: stretch;
}

@media (min-width: 620px) {
  .services__grid { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 1000px) {
  .services__grid { grid-template-columns: repeat(4, 1fr); gap: var(--space-4); }
  .svc--lead { grid-column: span 2; }
}

.svc {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--card-pad);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
}

/* Usługi główne wyróżnione tą samą kreską co wybrany pakiet w cenniku */
.svc--lead { border-top: 2px solid var(--accent); }

.svc__icon {
  width: 2.5rem;
  color: var(--ink);
}

.svc__icon svg { display: block; width: 100%; height: auto; }
.svc__icon .ico-dot { color: var(--accent); }
.svc__icon .ico-line { stroke: var(--accent-deep); }

.svc__name { margin: 0; font-size: clamp(1.05rem, 1.8vw, 1.2rem); }

.svc p {
  margin: 0;
  color: var(--ink-muted);
  font-size: 0.95rem;
  max-width: 46ch;
}

.svc--lead p { font-size: 1rem; }

/* 09 Przykłady ============================================================= */

/* Sekcja z demo poglądowym. Tło domyślne (--ground), żeby nie zetknąć się
   z „Dowodami" na --surface-sunk. Bez kręgu światła: limit trzech na stronę
   jest wykorzystany (hero, proces, kontakt).

   Karta zastąpiła blok zastępczy .ph-media--examples (przerywany obrys,
   proporcja 16/7, na telefonie 4/3). Przerywany obrys musiał odejść razem
   z treścią, którą oznaczał: to sygnał „tu jeszcze nic nie ma", a teraz
   jest tu prawdziwy zrzut. Proporcja 16/7 zostaje — tyle ma sam obraz —
   ale już bez przełączania na 4/3 na telefonie: zrzut jest kadrem
   o stałych proporcjach, a nie polem do wypełnienia, więc przycinanie go
   w pionie obcięłoby nagłówek strony w demie. */

/* Dwie karty obok siebie — dema stoją tu po to, żeby dało się je porównać
   jednym spojrzeniem: jedna strona kontra kilka podstron. Jedna pod drugą
   ta różnica ginie, bo drugiej karty nie widać razem z pierwszą.
   Zrzut w połowie szerokości robi się mały i nie jest już materiałem do
   czytania — od tego jest podpis i przycisk pod nim; obraz zostaje sygnałem
   „to jest strona". Poniżej 760 px kolumny wracają do jednej. */
.demo-list {
  display: grid;
  gap: var(--space-8);
}

@media (min-width: 760px) {
  .demo-list {
    grid-template-columns: 1fr 1fr;
    gap: var(--space-6);
  }
}

.demo-card {
  margin: 0;
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  background: var(--surface);
  overflow: hidden;
}

/* Kadr jest linkiem, ale wyjętym z kolejności tabulacji (aria-hidden
   w znacznikach) — klawiaturą i czytnikiem ekranu prowadzi przycisk niżej.
   Dzięki temu ten sam cel nie występuje w nawigacji dwa razy. */
.demo-card__frame {
  display: block;
  position: relative;
  aspect-ratio: 16 / 7;
  background: var(--surface-sunk);
  border-bottom: 1px solid var(--line);
}

.demo-card__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
}

/* Plakietka „demo, nie prawdziwy klient" siedzi NA zrzucie, nie pod nim.
   Powód jest jeden: obrazek bywa jedyną rzeczą, na którą ktoś spojrzy,
   a wymyślona firma budowlana z adresem w Hamburgu nie może przez sekundę
   wyglądać na referencję. Ciemna, nie miodowa — akcent w tej sekcji należy
   do przycisku, a ostrzeżenie ma być czytelne, nie ozdobne. */
.demo-card__badge {
  position: absolute;
  top: var(--space-4);
  left: var(--space-4);
  max-width: calc(100% - 2 * var(--space-4));
  background: var(--ink);
  color: var(--ground);
  font-family: var(--font-mono);
  /* 0.8rem = 12.8 px. Nie schodzić niżej: to jedyne zastrzeżenie, które mówi,
     że firma na zrzucie nie istnieje, więc musi się czytać bez wysiłku —
     poprzednie 0.7rem (11.2 px), a w wariancie wąskim 0.62rem (9.9 px),
     było na to za małe. */
  font-size: 0.8rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 0.4rem 0.7rem;
  border-radius: var(--r-sm);
  /* Włos jasnej obwódki. Na jasnym zrzucie (Baumeister Voss) jest niewidoczny,
     na ciemnym (SPIEGELWERK) odcina plakietkę od prawie czarnego paska strony
     w tle — bez niej ciemne na ciemnym zlewa się i ostrzeżenie przestaje
     rzucać się w oczy, a to jedyne, co ma robić. */
  box-shadow: 0 0 0 1px color-mix(in oklch, var(--ground) 40%, transparent);
}

.demo-card__body {
  padding: var(--card-pad);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  align-items: flex-start;
}

.demo-card__tag {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent-deep);
  margin: 0;
}

.demo-card__desc {
  margin: 0;
  color: var(--ink-muted);
  max-width: 60ch;
}

.demo-card__cta { margin-top: var(--space-2); }

/* Najechanie na kadr podnosi obraz o włos i rozjaśnia obrys karty — ten sam
   język, co poświata pod wierszem w cenniku: reakcja jest, ale nic nie skacze. */
.demo-card,
.demo-card__img { transition: border-color var(--t-ui) var(--ease), transform var(--t-ui) var(--ease); }

.demo-card:has(.demo-card__frame:hover),
.demo-card:has(.demo-card__cta:hover) { border-color: var(--accent); }

.demo-card__frame:hover .demo-card__img { transform: scale(1.015); }

@media (prefers-reduced-motion: reduce) {
  .demo-card__frame:hover .demo-card__img { transform: none; }
}

/* Na wąskim ekranie zrzut pełnej szerokości ma ok. 155 px wysokości i sam
   z siebie nie jest już czytelny — dlatego treść niesie podpis i przycisk,
   a obraz zostaje jako sygnał „to jest strona", nie jako materiał do lektury.

   Ciaśniejsza plakietka obowiązuje w dwóch przypadkach naraz: na telefonie
   (poniżej 560 px) i od 760 px, gdzie karty stoją w dwóch kolumnach i kadr
   znowu ma około pół szerokości. Między tymi progami karta zajmuje cały
   pas i plakietka może oddychać.

   MIEJSCE ODDAJE ŚWIATŁO, NIE STOPIEŃ PISMA. Wcześniej ten blok zbijał także
   font-size do 0.62rem, czyli 9.9 px — ostrzeżenie robiło się wtedy najmniej
   czytelne dokładnie tam, gdzie zrzut jest najmniejszy i najbardziej myli.
   Stopień zostaje więc jeden dla wszystkich szerokości; ściągamy tylko odstęp
   międzyliterowy i wyściółkę. Najdłuższy wariant („Demo, nie prawdziwy klient",
   26 znaków) zajmuje przy 12.8 px około 205 px i mieści się w kadrze karty
   nawet przy 360 px, gdzie na plakietkę przypada ok. 302 px. */
@media (max-width: 560px), (min-width: 760px) {
  .demo-card__badge {
    letter-spacing: 0.06em;
    padding: 0.3rem 0.5rem;
    top: var(--space-2);
    left: var(--space-2);
  }
}

@media (max-width: 560px) {
  .demo-card__cta { width: 100%; text-align: center; }
}

/* 10 Dowody ================================================================ */

.proof { background: var(--surface-sunk); }

.quotes {
  display: grid;
  gap: var(--space-6);
}

@media (min-width: 900px) {
  .quotes {
    grid-template-columns: 6fr 5fr;
    align-items: start;
  }
  .quote--offset { margin-top: var(--space-12); }
  .stamp--quote { grid-column: 1; }
}

.quote {
  margin: 0;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: var(--card-pad);
}

.quote p {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.05rem, 1.8vw, 1.25rem);
  line-height: 1.4;
  color: var(--ink);
  margin-bottom: var(--space-4);
}

.quote__who {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

/* 11 Cennik ================================================================ */

.plans {
  display: grid;
  gap: var(--space-6);
  align-items: start;
}

/* Trzy pakiety — każdy ma inne traktowanie, żeby nie powstała
   siatka trzech identycznych kart (design.md → „czego unikać"). */
@media (min-width: 900px) {
  .plans {
    grid-template-columns: 1fr 1.12fr 1fr;
    gap: var(--space-4);
    align-items: stretch;
  }
  .plan--pick { margin-block: calc(-1 * var(--space-6)); }
}

.plan {
  position: relative;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: var(--card-pad);
}

.plan--start { background: transparent; }

.plan--pick {
  background: var(--surface);
  border-top: 2px solid var(--accent);
  padding: clamp(1.5rem, 3vw, 2.25rem);
}

.plan--premium { background: var(--surface-sunk); }

.plan__badge {
  position: absolute;
  top: 0;
  left: var(--card-pad);
  transform: translateY(-50%);
  margin: 0;
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--accent);
  border-radius: var(--r-pill);
  background: var(--surface);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent-deep);
  white-space: nowrap;
}

.plan__head { padding-bottom: var(--space-4); border-bottom: 1px solid var(--line); }

.plan__tier {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
  margin: 0 0 var(--space-2);
}

.plan__name { margin-bottom: var(--space-3); }

.plan__for {
  font-size: 0.95rem;
  color: var(--ink-muted);
  margin-bottom: var(--space-6);
  max-width: 34ch;
}

.plan__price {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin: 0 0 var(--space-1);
}

.plan__amount {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(1.9rem, 3.6vw, 2.6rem);
  line-height: 1;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.plan__unit {
  font-family: var(--font-mono);
  font-size: 0.9rem;
  color: var(--ink-muted);
}

.plan__pln {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted);
  margin: 0;
}

.plan__list {
  list-style: none;
  margin: var(--space-6) 0 var(--space-8);
  padding: 0;
  display: grid;
  gap: var(--space-3);
  align-content: start;
  flex: 1 1 auto; /* przyciski w jednej linii u dołu kolumn */
}

.plan__list li {
  position: relative;
  padding-left: var(--space-6);
  font-size: 0.975rem;
  line-height: 1.55;
  color: var(--ink);
  max-width: 52ch;
}

.plan__list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.68em;
  width: 0.85rem;
  height: 1px;
  background: var(--line);
}

.plan--pick .plan__list li::before { background: var(--accent); }

.plan__list strong { font-variant-numeric: tabular-nums; }

/* Zusatzleistungen — trzy pozycje dokupywane poza abonamentem.

   Świadomie NIE jest to czwarta karta pakietu: te rzeczy nie konkurują
   z pakietami, tylko je uzupełniają, a czwarta plama obok trzech kart
   rozbiłaby rytm cennika. Zamiast tego wiersz etykieta–kwota, ten sam,
   którym niżej opisujemy składniki kosztu — czytelnik zna już ten układ,
   więc nie musi się uczyć nowego.

   Węższa kolumna niż cała szerokość: trzy krótkie wiersze rozciągnięte
   na 1200 px czytają się jak cennik parkingu, z kwotą oderwaną od nazwy. */
.addons {
  margin-top: var(--space-12);
  padding-top: var(--space-10);
  border-top: 1px solid var(--line);
  max-width: 56ch;
}

.addons__title {
  font-size: clamp(1.15rem, 2.2vw, 1.45rem);
  margin-bottom: var(--space-4);
}

.addons__lead {
  margin: 0 0 var(--space-6);
  color: var(--ink-muted);
}

.addons__list { margin: 0; }

/* Ten sam wiersz co .compare__row — łącznie z lampą pod wierszem na hover,
   dopisaną tam, gdzie stoi reszta rodziny (sekcja „Ruch"). */
.addons__row {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-2) var(--space-4);
  padding-block: var(--space-4);
  border-bottom: 1px solid var(--line);
}

.addons__row:last-child { border-bottom: none; }

.addons__row dt {
  color: var(--ink);
  font-size: 0.95rem;
}

.addons__row dd {
  margin: 0;
  text-align: right;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--accent-deep);
}

/* „einmalig", „einmalig, je Seite" — dopisek pod kwotą, jak w zestawieniu. */
.addons__unit {
  display: block;
  font-family: var(--font-mono);
  font-weight: 400;
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  color: var(--ink-muted);
  margin-top: 0.15rem;
}

/* Zestawienie składników kosztu. Dwie kolumny, żadnego przekreślania cen
   konkurencji — to ma być punkt odniesienia, nie licytacja. */
.compare {
  margin-top: var(--space-16);
  padding-top: var(--space-12);
  border-top: 1px solid var(--line);
}

.compare__title {
  font-size: clamp(1.15rem, 2.2vw, 1.45rem);
  margin-bottom: var(--space-8);
  max-width: 30ch;
}

.compare__cols {
  display: grid;
  gap: var(--space-8);
}

@media (min-width: 760px) {
  .compare__cols { grid-template-columns: 1fr 1fr; gap: var(--space-12); }
}

.compare__col {
  padding-top: var(--space-4);
  border-top: 2px solid var(--line);
}

/* Ta sama sygnatura co ostatni przystanek ścieżki demo: akcent oznacza
   miejsce, w którym stoi nasza odpowiedź. */
.compare__col--us { border-top-color: var(--accent); }

.compare__who {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin: 0 0 var(--space-6);
}

.compare__col--us .compare__who { color: var(--accent-deep); }

.compare__list { margin: 0; }

.compare__row {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-2) var(--space-4);
  padding-block: var(--space-4);
  border-bottom: 1px solid var(--line);
}

.compare__row dt {
  color: var(--ink-muted);
  font-size: 0.95rem;
}

.compare__row dd {
  margin: 0;
  text-align: right;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.compare__col--us .compare__row dd { color: var(--accent-deep); }

/* Wiersz sumy — istnieje tylko po naszej stronie, więc musi czytać się
   jak zamknięcie kolumny, a nie jak piąta pozycja z brakującym odpowiednikiem. */
.compare__row--sum {
  border-bottom: none;
  padding-top: var(--space-6);
}

.compare__row--sum dt { color: var(--ink); font-weight: 600; }
.compare__row--sum dd { font-size: 1.15rem; }

/* Dopisek pod kwotą: „/ Monat", „einmalig", „jede weitere 10–15 €" */
.compare__unit {
  display: block;
  font-family: var(--font-mono);
  font-weight: 400;
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  text-transform: none;
  color: var(--ink-muted);
  margin-top: 0.15rem;
}

.compare__source {
  margin: var(--space-8) 0 0;
  font-size: 0.9rem;
  color: var(--ink-muted);
  max-width: 62ch;
}

.pricing__note {
  margin-top: var(--space-8);
  color: var(--ink-muted);
  max-width: 56ch;
}

/* Adnotacja o VAT — informacja cenowa, nie drobny druk. Pełny kontrast
   i kreska akcentu, żeby czytało się razem z kwotami, nie pod nimi. */
.pricing__vat {
  margin-top: var(--space-4);
  padding-left: var(--space-4);
  border-left: 2px solid var(--accent);
  color: var(--ink);
  max-width: 56ch;
}

/* Rozwijany drobny druk o płatności (SEPA).

   Sześć zdań o poleceniu zapłaty stało otwarte tuż pod kwotami i było
   najdłuższym blokiem tekstu w sekcji — w miejscu, w którym odwiedzający
   ma podjąć decyzję, a nie czytać warunki obciążeń. Teraz jest o jedno
   kliknięcie dalej. Treść bez zmian, nic nie zniknęło.

   To NIE jest drugi akordeon obok FAQ, tylko ten sam wzorzec w mniejszej
   skali: natywne <details>, ten sam znak +/− z obrysem, ta sama krzywa.
   Różnice wynikają z rangi — to podpis pod cennikiem, nie pytanie na
   liście, więc etykieta jest w foncie mono jak inne etykiety pomocnicze,
   a znak zmniejszony z 32 na 24 px. Cel dotyku trzyma 44 px wysokości
   przez min-height na summary, więc zmniejszenie znaku niczego nie łamie. */
.fine {
  margin-top: var(--space-4);
  max-width: 56ch;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

.fine summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding-block: var(--space-3);
  min-height: 44px;
  cursor: pointer;
  list-style: none;
}

.fine summary::-webkit-details-marker { display: none; }

.fine__label {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

/* Otwarty blok: etykieta przejmuje akcent, tak samo jak etykiety mono
   w wierszach procesu na hover — jeden język dla „ten element jest teraz
   aktywny". */
.fine[open] .fine__label { color: var(--accent-deep); }

/* Znak +/− zbudowany tak samo jak .qa__mark, tylko w polu 24 px zamiast
   32 px (kreski 12 px zamiast 14 px). Osobne reguły, nie współdzielony
   selektor z .qa__mark: akordeon FAQ jest komponentem o własnych, opisanych
   w design.md wymiarach i nie chcę, żeby zmiana rozmiaru drobnego druku
   pod cennikiem kiedykolwiek ruszyła listę pytań. */
.fine__mark {
  flex: none;
  position: relative;
  width: 24px;
  height: 24px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  transition:
    border-color var(--t-ui) var(--ease),
    background-color var(--t-ui) var(--ease);
}

.fine__mark::before,
.fine__mark::after {
  content: "";
  position: absolute;
  /* Kreski 12 px wyśrodkowane w polu 24 px */
  inset: 50% 6px auto 6px;
  height: 2px;
  border-radius: 1px;
  background: var(--accent-deep);
  transition: transform var(--t-fold) var(--ease);
}

.fine__mark::after { transform: rotate(90deg); }
.fine[open] .fine__mark::after { transform: rotate(0deg); }
.fine[open] .fine__mark { border-color: var(--accent-deep); }

/* Potwierdzenie dotyku — na telefonie nie ma hoveru, więc bez tego
   stuknięcie nie daje żadnego sygnału. Ta sama zasada co w .qa. */
.fine summary:active .fine__mark {
  background: color-mix(in oklch, var(--accent) 14%, transparent);
  border-color: var(--accent-deep);
}

/* Wysokość zostaje NATYWNA — patrz długa nota przy .qa__a. Nie odtwarzać
   wersji przez ::details-content: ozdoba nie może decydować o tym, czy
   treść jest widoczna. Bez animation-fill-mode z tego samego powodu. */
.fine[open] .pricing__fine {
  animation: qa-in var(--t-fold) var(--ease);
}

.pricing__fine {
  margin-top: 0;
  padding-bottom: var(--space-5);
  font-size: 0.9rem;
  color: var(--ink-muted);
  max-width: 56ch;
}

/* 12 O firmie ============================================================== */

/* ===== Ciemna cezura: rozmyte zdjęcie pod ciepłym nakładem =====
   Wdrożone 2026-08-18. Uzasadnienie wyboru i przepisy na odrzucone
   warianty A/C: design.md, sekcja „Eksperymenty do oceny", E-2.
   DOKŁADNIE JEDNA taka sekcja na stronę — druga zamieniłaby cezurę
   w motyw przewodni i rozbiła stronę na dwie różne strony.

   DLACZEGO AKURAT TA SEKCJA. Nie jest to wybór estetyczny, tylko jedyne
   miejsce bez kolizji: `.about` NIE MA ani kręgu światła, ani dymu za
   kursorem (dym leci tylko w .hero i .process). Wszystkie pomiary
   kontrastu postawione dla tamtych dwóch efektów zakładają JASNE tło —
   odwrócenie sekcji, w której one działają, unieważniłoby te dowody.
   Tutaj nie ma czego unieważniać.

   Do tego trafia w treść: ta sekcja opowiada dokładnie scenę źródłową
   całego kierunku — wieczór, biurko, jedna ciepła lampa.

   OBRAZ TO TEN SAM PLIK, co zdjęcie na pierwszym planie. To nie jest
   powtórzenie: po rozmyciu obraz jest nierozpoznawalny i działa jak
   abstrakcyjne ciepłe pole tonalne. Zysk jest mierzalny — ZERO dodatkowych
   bajtów, bo about.webp i tak jest pobierane przez tę sekcję.

   Barwy schodzą przez PRZESŁONIĘCIE TOKENÓW, nie przez nadpisywanie
   pojedynczych reguł. Powód z doświadczenia: przy nadpisywaniu reguła po
   regule zawsze zostaje coś pominiętego (w podglądzie przeoczone zostało
   „/ mies." przy cenie — ciemne na ciemnym). Przesłonięcie tokenu łapie
   każdego potomka, który używa var(--ink), var(--ink-muted) czy
   var(--line), także dopisanego w przyszłości.

   Kontrast zmierzony dla NAJGORSZEGO przypadku, czyli czystej bieli pod
   nakładem, z ziarnem ×0.87 (realistyczne, ciemne zdjęcie daje znacznie
   więcej — wartości w nawiasach):
     nagłówek i <strong> --ground:  7.79:1   (11.20:1)
     akapity  0.885 0.020 70:       6.17:1   ( 8.86:1)
     etykieta mono (ten sam token): 6.17:1   ( 8.86:1)
     --accent jako element graf.:   3.69:1   ( 5.30:1)
   Nakład 0.86 jest podłogą tych liczb: przy 0.82 akcent schodzi do 3.28:1,
   czyli pod próg 3:1. NIE ZMNIEJSZAĆ NAKŁADU bez powtórzenia pomiaru. */
.about {
  /* Własny kontekst układania: bez tego pseudoelementy z ujemnym z-index
     uciekłyby pod tło strony zamiast zostać w sekcji. */
  isolation: isolate;
  position: relative;
  overflow: hidden;             /* przycina rozmycie do krawędzi sekcji */

  --ink: oklch(0.962 0.014 78);
  --ink-muted: oklch(0.885 0.020 70);
  --line: oklch(0.50 0.028 55);
  --surface-sunk: oklch(0.315 0.032 50);   /* tło-zaślepka pod zdjęciem */

  /* Dym na ciemnym tle. NIEAKTYWNY — .about nie jest hostem dymu (patrz
     nota wyżej: efekty lecą tylko w .hero i .process). Token stoi tutaj,
     bo site.js czyta --smoke z SEKCJI, nie z :root, więc dopisanie
     magicTrail() dla ciemnej sekcji nie może po cichu wpuścić na nią
     wartości z :root — szary dym rgb(215,212,206) na tle rgb(49,30,20)
     zostawiłby jasny tekst na jasnoszarej plamie.

     Zasada jest jedna i nie zależy od tego, czy sekcja jest jasna czy
     ciemna: DYM MUSI ODSUNĄĆ SIĘ JASNOŚCIĄ OD TŁA SEKCJI, ale nie na tyle,
     żeby zbliżyć się do jasności leżącego na nim tekstu. Na jasnym tle
     z ciemnym tekstem oznacza to schodzenie w dół (do 0.870), tu — przy
     ciemnym tle i jasnym tekście — wychodzenie w górę. Sufit wyznacza
     --accent jako element graficzny (próg 3:1), nie tekst. Zmierzone przy
     pełnym kryciu dymu, ziarno ×0.87:
       --ink-muted sekcji (0.885 0.020 70):  5.21:1
       --ink sekcji       (0.962 0.014 78):  6.61:1
       --accent jako element graficzny:      3.12:1  ← to jest ograniczenie
     Przy 0.44 akcent schodzi na 2.93:1, czyli pod próg. NIE PODNOSIĆ
     JASNOŚCI bez powtórzenia pomiaru.

     Globalne przyciemnienie --ink-muted do 0.44 nie dotyczy tej sekcji:
     .about przesłania --ink-muted własną, jasną wartością (wyżej). */
  --smoke: oklch(0.42 0.010 80);

  color: var(--ink);
  background: oklch(0.255 0.035 48);
}

/* Zdjęcie. Rozmycie liczone na MAŁEJ powierzchni: kafelek ~0.5% docelowej,
   dopiero potem skalowany. `filter` działa na wyrenderowanej treści
   elementu, `transform` przekształca dopiero wynik, więc blur widzi
   kilkaset pikseli zamiast kilkuset tysięcy. Promień 1.2 px na kafelku
   odpowiada po przeskalowaniu ~21 px.

   Zapas przy krawędziach (ujemne top/left, skala z nadmiarem) jest po to,
   żeby rozmycie nie wciągnęło przezroczystości spoza kafelka i nie zrobiło
   jasnej obwódki przy brzegu sekcji. */
.about::before {
  content: "";
  position: absolute;
  top: -6%;
  left: -6%;
  width: 7%;
  height: 7%;
  z-index: -3;
  transform-origin: top left;
  transform: scale(18);
  background-image: url("./about.webp");
  background-size: cover;
  background-position: center 30%;
  filter: blur(1.2px) saturate(0.75);
}

/* Nakład. Nie czerń — przyciemniony --ink, żeby ciemna sekcja została
   ciepła i nie wypadła z palety. */
.about::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -2;
  background: oklch(0.255 0.035 48 / 0.86);
}

/* Poświata wiersza na hover leży na z-index -1, czyli NAD nakładem (-2)
   i nad zdjęciem (-3), ale pod treścią. Gdyby nakład został na -1, byłby
   rysowany po niej (ten sam z-index, dalsza pozycja w drzewie) i zjadłby
   cały efekt najechania.

   Barwa zmieniona z --accent na --accent-glow: miód przy 8% na ciemnym
   tle jest praktycznie niewidoczny, bo jest od niego CIEMNIEJSZY. Na
   ciemnym poświata musi dokładać światła — ta sama zasada, co przy kręgu.
   Zmierzone: <strong> na podświetlonym wierszu trzyma 6.12:1. */
.about .promises li::before {
  background: oklch(0.995 0.13 76 / 0.10);
}

.about__inner {
  display: grid;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
}

@media (min-width: 900px) {
  .about__inner { grid-template-columns: 5fr 6fr; }
}

.about__media { margin: 0; }

/* Jak .hero__img, tylko kadr pionowy — 4/5 przejęte po .ph-media--portrait,
   które trzymało tu miejsce, zanim przyszło zdjęcie. */
.about__img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: var(--r-card);
  background: var(--surface-sunk);
}

.about__text .sec-head__title { margin-bottom: var(--space-6); }
.about__text p { color: var(--ink-muted); }

.promises {
  list-style: none;
  margin: var(--space-8) 0 0;
  padding: 0;
  border-top: 1px solid var(--line);
}

.promises li {
  padding-block: var(--space-3);
  border-bottom: 1px solid var(--line);
  max-width: 52ch;
}

/* 13 Pytania =============================================================== */

.faq { background: var(--surface-sunk); }

.faq__list {
  border-top: 1px solid var(--line);
  max-width: 52rem;
}

.qa { border-bottom: 1px solid var(--line); }

.qa summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
  padding-block: var(--space-4);
  min-height: 44px;
  cursor: pointer;
  list-style: none;
}

.qa summary::-webkit-details-marker { display: none; }

.qa__q {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.05rem, 2vw, 1.25rem);
  letter-spacing: -0.01em;
  line-height: 1.3;
}

/* Znak +/− jest teraz JEDYNYM sygnałem, że wiersz się rozwija: od chwili,
   gdy wszystkie pytania startują zwinięte, nie ma na ekranie ani jednej
   otwartej odpowiedzi, która by to pokazywała. Wcześniejsza wersja — kreski
   1.5 px w polu 16 px, bez obrysu — na telefonie ginęła obok dwuwierszowego
   nagłówka i lista czytała się jak zwykły spis treści.

   Stąd pole 32 px z cienkim obrysem: czyta się jako cel dotyku, a nie jako
   ozdobnik. To nie jest „ikona w kółeczku" z listy zakazów — kwadrat
   o promieniu --r-sm, ten sam co przyciski, bez wypełnienia i bez cienia. */
.qa__mark {
  flex: none;
  position: relative;
  width: 32px;
  height: 32px;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  transition:
    border-color var(--t-ui) var(--ease),
    background-color var(--t-ui) var(--ease);
}

.qa__mark::before,
.qa__mark::after {
  content: "";
  position: absolute;
  /* Kreski 14 px wyśrodkowane w polu 32 px */
  inset: 50% 9px auto 9px;
  height: 2px;
  border-radius: 1px;
  background: var(--accent-deep);
  transition: transform var(--t-fold) var(--ease);
}

.qa__mark::after { transform: rotate(90deg); }
.qa[open] .qa__mark::after { transform: rotate(0deg); }

/* Otwarte pytanie: obrys przejmuje akcent, żeby po rozwinięciu było widać,
   który wiersz jest aktywny — na długiej liście to jedyny punkt odniesienia. */
.qa[open] .qa__mark { border-color: var(--accent-deep); }

/* Odpowiedź na dotyk. Hover na telefonie nie istnieje, więc bez tego
   stuknięcie nie daje żadnego potwierdzenia i wygląda, jakby nic się nie
   stało. :active działa na dotyku od razu. */
.qa summary:active .qa__mark {
  background: color-mix(in oklch, var(--accent) 14%, transparent);
  border-color: var(--accent-deep);
}

.qa__a {
  padding-bottom: var(--space-6);
  color: var(--ink-muted);
  max-width: 60ch;
}

/* Rozwijanie: wysokość zostaje NATYWNA, animujemy samą treść odpowiedzi.
   Trwanie i krzywa bez zmian — 240 ms, --t-fold.

   ==========  NIE ODTWARZAĆ WERSJI PRZEZ ::details-content  ==========

   Poprzednia wersja animowała wysokość przez `.qa::details-content`
   (`block-size: 0` → `auto`, plus `content-visibility` z `allow-discrete`)
   i ROZBIJAŁA CAŁY AKORDEON: po kliknięciu `<details>` dostawał atrybut
   `open`, ale treść zostawała zwinięta. Zmierzone w Chrome, 2026-08-15:

     — selektor `.qa[open]::details-content` trafia (podmieniony
       `outline-width` się propaguje),
     — mimo to `block-size` liczy się jako `0px`, nawet z `!important`,
     — bo `content-visibility` zostaje na `hidden` i nie da się jej ruszyć
       nawet wymuszeniem `visible !important` — dyskretne przejście się
       zacina, a `hidden` zeruje wysokość niezależnie od `block-size`,
     — po wyłączeniu całej reguły wysokość wraca do normy: 65 → 173 → 65 px.

   Błąd był NIEWIDOCZNY w testach, dopóki dwa pierwsze pytania miały atrybut
   `open` w HTML-u — te otwierały się poprawnie przy wczytaniu strony i pomiar
   wychodził dobrze. Rozwinąć kliknięciem nie dawało się żadnego. Wyszło
   dopiero na prawdziwym telefonie.

   Wniosek na przyszłość: `::details-content` jest za świeże, żeby wieszać na
   nim DZIAŁANIE, a nie samą ozdobę. Wysokość zostawiamy przeglądarce —
   działa wszędzie i nie ma czego zepsuć. */
/* BEZ animation-fill-mode: both — i to jest celowe.

   Z „both" element przed startem animacji trzyma klatkę „from", czyli
   opacity: 0. Gdyby animacja z jakiegokolwiek powodu nie ruszyła — a
   przeglądarki wstrzymują je choćby w karcie w tle — odpowiedź zostałaby
   rozwinięta, ale NIEWIDOCZNA. To dokładnie ten sam błąd, co w wycofanej
   wersji przez ::details-content: ozdoba decydowała o tym, czy treść jest
   widoczna.

   Bez „both" stan spoczynkowy to normalne opacity: 1. Animacja tylko
   podbarwia wejście; jeśli nie ruszy, odpowiedź po prostu pojawia się od
   razu. Najgorszy możliwy skutek awarii to brak animacji, nie brak treści. */
.qa[open] .qa__a {
  animation: qa-in var(--t-fold) var(--ease);
}

@keyframes qa-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

/* 14 Kontakt i formularz =================================================== */

.contact__inner {
  display: grid;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: start;
}

@media (min-width: 900px) {
  .contact__inner { grid-template-columns: 5fr 6fr; }
  /* Pasek zasięgu pod obiema kolumnami, nie w jednej z nich */
  .contact__reach { grid-column: 1 / -1; }
}

/* Zasięg działania — zastąpiło blok zastępczy mapy (uzasadnienie stoi przy
   znaczniku w index.html). Płaski pasek zbudowany z tych samych części, co
   wiersze .contact__direct nad nim: etykieta mono wersalikiem w --ink-muted,
   treść zwykłym stopniem w --ink. Kreska akcentu z lewej jak w .contact__risk,
   żeby oba wtrącenia w tej sekcji czytały się jako jedna rodzina.

   Sekcja stoi na --ground, więc pasek dostaje --surface: unosi się o jeden
   stopień, tak samo jak karty demo wyżej. Bez gradientu i bez przezroczystości
   — paleta z design.md dopuszcza tu wyłącznie pełne tony. */
.contact__reach {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-6);
  margin: 0;
  padding: var(--space-4) var(--card-pad);
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 2px solid var(--accent);
  border-radius: var(--r-sm);
}

.contact__reach-key {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
  min-width: 6rem;
}

.contact__reach-val {
  margin: 0;
  font-size: 1rem;
  color: var(--ink);
}

.contact__aside .sec-head__title { margin-bottom: var(--space-6); }

.contact__risk {
  max-width: 46ch;
  padding-left: var(--space-4);
  border-left: 2px solid var(--accent);
}

.contact__wa { margin-top: var(--space-6); }

.contact__waNote {
  margin-top: var(--space-3);
  max-width: 44ch;
  font-size: 0.9rem;
  color: var(--ink-muted);
}

.contact__direct {
  list-style: none;
  margin: var(--space-6) 0 0;
  padding: 0;
  border-top: 1px solid var(--line);
}

.contact__direct li {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-6);
  align-items: baseline;
  padding-block: var(--space-3);
  border-bottom: 1px solid var(--line);
}

.contact__key {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
  min-width: 6rem;
}

.contact__val {
  font-family: var(--font-mono);
  font-size: 1rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  text-decoration: none;
}

a.contact__val { color: var(--accent-deep); text-decoration: underline 1px; text-underline-offset: 3px; }

.form {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: clamp(1.5rem, 3vw, 2.5rem);
  display: grid;
  gap: var(--space-6);
}

.field { display: grid; gap: var(--space-2); }

.field label {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.field input,
.field select,
.field textarea {
  font-family: var(--font-body);
  font-size: 1.0625rem;
  color: var(--ink);
  background: var(--ground);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: 0.75rem 0.9rem;
  min-height: 44px;
  width: 100%;
}

.field textarea { resize: vertical; line-height: 1.6; }

/* Załącznik: natywny input, tylko dociągnięty do reszty pól.
   Przycisk wyboru pliku dostaje wygląd przycisku drugorzędnego. */
.field--file input[type="file"] {
  padding: 0.6rem 0.9rem;
  line-height: 1.5;
  cursor: pointer;
}

.field--file input[type="file"]::file-selector-button {
  margin-right: var(--space-3);
  padding: 0.45rem 0.8rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  font-family: var(--font-body);
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
}

.field--file input[type="file"]:hover::file-selector-button { border-color: var(--ink-muted); }

.field input:hover,
.field select:hover,
.field textarea:hover { border-color: var(--ink-muted); }

.field__hint { margin: 0; font-size: 0.9rem; color: var(--ink-muted); }

/* Zgoda na przetwarzanie danych. Etykieta jest zdaniem do przeczytania,
   a nie nazwą pola — dlatego wychodzi z mono i wersalików reszty formularza.
   Kwadrat zgody i tekst obok siebie, wyrównane do pierwszej linii. */
.field--consent {
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: var(--space-3);
}

/* Zerowanie stylów pól tekstowych wyżej (ramka, tło, promień) — na kwadracie
   zgody zostawiają w części przeglądarek drugą ramkę wokół natywnej kontrolki. */
.field--consent input[type="checkbox"] {
  width: 1.15rem;
  height: 1.15rem;
  min-height: 0;
  margin-top: 0.2rem;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  accent-color: var(--accent-deep);
  cursor: pointer;
}

.field--consent label {
  font-family: var(--font-body);
  font-size: 0.95rem;
  line-height: 1.55;
  letter-spacing: normal;
  text-transform: none;
  color: var(--ink);
  cursor: pointer;
}

.form__note { font-size: 0.9rem; color: var(--ink-muted); margin: 0; }

/* Pole-pułapka na boty. Nie display:none ani visibility:hidden — część
   botów rozpoznaje oba i takie pole pomija. Wyniesienie poza ekran działa
   lepiej, a aria-hidden i tabindex="-1" trzymają je z dala od czytników
   ekranu i tabulacji. */
.form__hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* Komunikat po wysyłce. Kolor tylko na lewej krawędzi — sam tekst zostaje
   czytelny, bez zielonego czy czerwonego prostokąta na całą szerokość. */
.form__status {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  font-size: 0.95rem;
  line-height: 1.55;
  color: var(--ink);
  background: var(--surface-sunk);
  border-left: 3px solid var(--line);
  border-radius: var(--r-sm);
}

.form__status[hidden] { display: none; }
.form__status.is-ok { border-left-color: var(--ok); }
.form__status.is-err { border-left-color: var(--err); }

/* 15 Stopka ================================================================ */

.site-foot {
  border-top: 1px solid var(--line);
  background: var(--surface-sunk);
  padding-block: var(--section-gap) var(--space-12);
}

.site-foot__inner {
  display: grid;
  gap: var(--space-12);
}

/* Cztery kolumny od 900 px: marka, kontakt, mapa strony, reszta.
   Mapa dostała najwęższą kolumnę — to sama lista krótkich haseł. */
@media (min-width: 900px) {
  .site-foot__inner {
    grid-template-columns: 4fr 3fr 2fr 3fr;
    gap: var(--space-8);
    align-items: start;
  }
}

/* Mapa strony — spis sekcji, nie druga nawigacja: bez podkreśleń
   rozsuwanych jak w pasku, bez wyróżnień. Cele dotykowe 44 px, bo to
   na telefonie ten blok ma robić robotę. */
/* Ta sama drobna etykieta co przy „Telefon" i „Obserwuj", ale tutaj otwiera
   blok, więc dostaje własny wiersz i odstęp. */
.site-foot__map .site-foot__key {
  display: block;
  margin-bottom: var(--space-2);
}

.site-foot__maplist {
  list-style: none;
  margin: 0;
  padding: 0;
}

.site-foot__maplist a {
  display: flex;
  align-items: center;
  min-height: 44px;
  color: var(--ink);
  text-decoration: none;
  font-size: 0.95rem;
}

.site-foot__maplist a:hover { color: var(--accent-deep); }

.brand--foot { font-size: clamp(1.4rem, 3vw, 1.9rem); margin-bottom: var(--space-3); }
.site-foot__claim { color: var(--ink-muted); max-width: 34ch; margin-bottom: var(--space-6); }

.site-foot__big {
  display: block;
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: clamp(1.1rem, 2.4vw, 1.4rem);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  text-decoration: none;
  padding-block: var(--space-2);
}

.site-foot__big:hover { color: var(--accent-deep); }

/* Telefon i WhatsApp w stopce — drobniej niż e-mail, celowo. */
.site-foot__small {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-3);
  margin-top: var(--space-2);
  font-family: var(--font-mono);
  font-size: 0.9rem;
  font-variant-numeric: tabular-nums;
}

.site-foot__small a { color: var(--accent-deep); text-decoration: underline 1px; text-underline-offset: 3px; }

.site-foot__key {
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.site-foot__area { margin-top: var(--space-3); color: var(--ink-muted); font-size: 0.9rem; }

.lang {
  font-family: var(--font-mono);
  font-size: 0.9rem;
  letter-spacing: 0.08em;
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.lang a { color: var(--ink-muted); text-decoration: none; padding: var(--space-1) var(--space-2); }
.lang a[aria-current="true"] { color: var(--ink); font-weight: 500; }

.site-foot__legal { font-size: 0.9rem; color: var(--ink-muted); margin-bottom: var(--space-2); }
.site-foot__legal a { color: var(--ink-muted); }

/* Social — znaki liniowe, bez kółeczek i bez emoji (design.md) */
.site-foot__social {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  margin-bottom: var(--space-2);
}

.social {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  min-height: 44px;
  font-size: 0.9rem;
  color: var(--ink);
  text-decoration: none;
}

.social:hover { color: var(--accent-deep); }

.social__mark {
  flex: none;
  width: 1.15rem;
  height: 1.15rem;
}

.social__mark .ico-dot { color: var(--accent); }
.social__mark .ico-line { stroke: var(--accent-deep); }

.site-foot__hint {
  font-size: 0.8rem;
  color: var(--ink-muted);
  margin-bottom: var(--space-4);
  max-width: 30ch;
}

/* 16 Podstrony prawne ====================================================== */

/* Impressum, Datenschutz, AGB. Nagłówek bez nawigacji: to strony, na które
   wchodzi się ze stopki i z których wraca się na główną — pełne menu
   sześciu kotwic prowadzących do innego dokumentu tylko myliłoby. */
.site-head__back {
  font-size: 0.95rem;
  color: var(--ink-muted);
  text-decoration: none;
  white-space: nowrap;
}

.site-head__back:hover { color: var(--ink); }

/* Na wąskim ekranie marka, flagi i ten link nie mieszczą się w jednym pasku.
   Znika link, nie flagi: powrót jest i tak pod treścią, a marka prowadzi
   na stronę główną. */
@media (max-width: 560px) {
  .site-head__back { display: none; }
}

/* <main>, więc reguła „section { padding-block }" go nie obejmuje — odstęp
   spod przyklejonego nagłówka trzeba dać osobno. */
.legal { display: block; padding-block: var(--space-16) var(--section-gap); }

.legal__inner { max-width: 48rem; }
.legal__title { margin-bottom: var(--space-6); }

.legal__intro {
  color: var(--ink-muted);
  max-width: 58ch;
  margin-bottom: var(--space-12);
}

.legal__section {
  padding-block: var(--space-8);
  border-top: 1px solid var(--line);
}

.legal__section h2 {
  font-size: clamp(1.15rem, 2.2vw, 1.4rem);
  margin-bottom: var(--space-3);
}

/* Treść paragrafu. Tekst prawny czyta się inaczej niż stronę sprzedażową:
   drobniej, ciaśniej, dłuższymi blokami — ale nadal w kolumnie, której nie
   trzeba przeskakiwać wzrokiem. Cały blok jest jednym kluczem w i18n.js,
   dlatego style muszą działać na dowolnym zestawie akapitów i list w środku. */
.legal__body {
  font-size: 0.975rem;
  color: var(--ink);
}

.legal__body p { max-width: 68ch; }

.legal__body h3 {
  font-size: 1.02rem;
  margin: var(--space-6) 0 var(--space-2);
}

.legal__body h3:first-child { margin-top: 0; }

.legal__list {
  margin: 0 0 var(--space-4);
  padding-left: var(--space-6);
  max-width: 66ch;
  list-style: none;
}

.legal__list li {
  position: relative;
  margin-bottom: var(--space-2);
}

/* Kreska zamiast kropki — ten sam znak, co w listach pakietów w cenniku. */
.legal__list li::before {
  content: "—";
  position: absolute;
  left: calc(var(--space-6) * -1);
  color: var(--accent-deep);
}

.legal__list li:last-child { margin-bottom: 0; }

/* Adres w bloku: pełne dane firmy albo urzędu. Bez kursywy, bez ramki —
   ma wyglądać jak wpisany na kopercie. */
.legal__addr {
  line-height: 1.55;
  margin-bottom: var(--space-4);
}

/* Pary etykieta → wartość: e-mail, telefon, wpis w przeglądarce. */
.legal__data {
  margin: 0 0 var(--space-4);
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--space-2) var(--space-6);
  max-width: 62ch;
}

.legal__data dt {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
  padding-top: 0.15rem;
}

.legal__data dd { margin: 0; }

.legal__body code {
  font-family: var(--font-mono);
  font-size: 0.9em;
  background: var(--surface-sunk);
  border-radius: 0.35rem;
  padding: 0.1em 0.35em;
}

/* Formularz odstąpienia od umowy — dosłowny wzór ustawowy. Cytat, nie nasz
   tekst, więc odcięty od reszty kreską i tłem karty. */
.legal__quote {
  margin: 0 0 var(--space-4);
  padding: var(--space-4);
  background: var(--surface);
  border-left: 2px solid var(--line);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  font-size: 0.94rem;
  line-height: 1.6;
  max-width: 62ch;
}

@media (max-width: 560px) {
  .legal__data {
    grid-template-columns: 1fr;
    gap: 0;
  }

  .legal__data dd { margin-bottom: var(--space-3); }
}

.legal__updated {
  margin: var(--space-12) 0 0;
  color: var(--ink-muted);
  font-size: 0.9rem;
}

.legal__back { margin-top: var(--space-8); }

.site-foot--slim { padding-block: var(--space-12); }
.site-foot--slim .site-foot__inner { display: block; }

@media (min-width: 900px) {
  .site-foot--slim .site-foot__inner { grid-template-columns: none; }
}

/* 17 Ruch ================================================================== */
/* Styl „Spokojny": opacity 0→1, translateY 12px→0, 380 ms, raz.
   Klasa .motion dodawana przez JS — bez JS treść jest po prostu widoczna. */

.motion .reveal {
  opacity: 0;
  transform: translateY(var(--shift-reveal));
  transition:
    opacity var(--t-reveal) var(--ease),
    transform var(--t-reveal) var(--ease);
}

/* Hero ma własne, krótsze wejście — patrz nota przy tokenach. */
.motion .hero .reveal {
  transform: translateY(var(--shift-enter));
  transition-duration: var(--t-enter);
}

.motion .reveal.is-in,
.motion .hero.is-lit .reveal {
  opacity: 1;
  transform: none;
}

/* Hero żyje przed pierwszym przewinięciem — wejście po załadowaniu */
.motion .hero .reveal[data-reveal-order="1"] { transition-delay: 40ms; }
.motion .hero .reveal[data-reveal-order="2"] { transition-delay: 110ms; }
.motion .hero .reveal[data-reveal-order="3"] { transition-delay: 180ms; }
.motion .hero .reveal[data-reveal-order="4"] { transition-delay: 250ms; }
.motion .hero .reveal[data-reveal-order="5"] { transition-delay: 320ms; }
.motion .hero .reveal[data-reveal-order="6"] { transition-delay: 390ms; }

/* Lampa rozjaśnia się razem z hero */
.motion .hero::before {
  opacity: 0;
  transition: opacity 400ms var(--ease) 120ms;
}

.motion .hero.is-lit::before { opacity: 1; }

/* Oddychanie kręgu światła — powolna zmiana jasności w pętli.
   Żarówka pod ciepłym kloszem nigdy nie świeci idealnie równo i to jest
   cały sens: krąg przestaje być naklejonym gradientem, a zaczyna być
   źródłem światła.

   5.5 s na pełny cykl — bez zmian. Animujemy samo opacity.

   Zejście pogłębione z 62% na 45% (2026-08-18). Powód: przy poprzednim
   kręgu na --accent amplituda 1 → 0.62 była zmianą 38% z i tak ledwo
   widocznej plamy, czyli praktycznie niczego. Teraz, gdy krąg faktycznie
   świeci, płytkie oddychanie ginęłoby tym bardziej. Ruch nadal jest
   wolniejszy, niż oko świadomie śledzi — chodzi o wrażenie niestałego
   światła, nie o zauważalne pulsowanie.

   Kontrast: opacity skaluje alfę całego gradientu, więc dolna klatka daje
   rdzeń 0.60 × 0.45 = 0.27 (zmierzone: 6.12:1). Zejście w dół jest zawsze BEZPIECZNE, bo
   zbliża tło do czystego --ground (6.17:1) — im ciemniejsza klatka
   oddychania, tym wyższy kontrast. Podłogę wyznacza klatka górna, czyli
   pomiar przy regule .glow::before wyżej.

   Hero dostaje opóźnienie równe swojemu wejściu (400 ms + 120 ms), żeby
   oddychanie zaczęło się dopiero po rozjaśnieniu, a nie zamiast niego.
   Bez fill-mode: przez czas opóźnienia obowiązuje przejście wyżej,
   a pierwsza klatka animacji ma opacity 1, więc nie ma przeskoku. */
.motion .glow::before {
  animation: glow-breath 5.5s var(--ease) infinite;
}

.motion .hero::before { animation-delay: 520ms; }

@keyframes glow-breath {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.45; }
}

/* prefers-reduced-motion: cały ruch wyłączony, stan końcowy od razu */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }

  .reveal { opacity: 1 !important; transform: none !important; }
  .hero::before { opacity: 1 !important; }
}

/* 18 Okno demo ============================================================= */
/* Natywny <dialog>. Zachowanie (focus, Escape, blokada tła) niesie
   przeglądarka — tutaj jest wyłącznie wygląd. */

/* Blokada przewijania strony pod otwartym oknem. clip, nie hidden —
   z tego samego powodu co przy <html> na górze pliku: hidden zrobiłby
   z <html> kontener przewijania i zepsułby sticky w nagłówku. */
.dlg-open { overflow: clip; }

.dlg {
  /* Reset domyślnego wyglądu <dialog> */
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  background: var(--surface);
  color: var(--ink);

  width: min(38rem, calc(100vw - 2 * var(--gutter)));
  max-width: none;
  max-height: min(88dvh, 52rem);
  overflow: hidden;          /* przycina treść do zaokrąglonych rogów */
}

/* display trzyma zamknięte okno w ukryciu, więc flex tylko przy [open] */
.dlg[open] { display: flex; }

.dlg::backdrop { background: var(--scrim); }

/* Panel jest tym, co się przewija — nie całe okno. Dzięki temu nagłówek
   z krzyżykiem zostaje na wierzchu przy długim formularzu. */
.dlg__panel {
  width: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;   /* przewijanie nie „przeskakuje" na stronę pod spodem */
  padding: clamp(1.25rem, 3vw, 2rem);
  display: grid;
  gap: var(--space-6);
  align-content: start;
}

.dlg__head {
  position: sticky;
  top: calc(-1 * clamp(1.25rem, 3vw, 2rem));   /* kompensuje padding panelu */
  z-index: 1;
  display: flex;
  align-items: start;
  gap: var(--space-4);
  /* Tło pod przyklejonym nagłówkiem, żeby treść nie prześwitywała */
  background: var(--surface);
  margin: calc(-1 * clamp(1.25rem, 3vw, 2rem));
  margin-bottom: 0;
  padding: clamp(1.25rem, 3vw, 2rem);
  padding-bottom: var(--space-4);
}

.dlg__title {
  font-family: var(--font-display);
  font-size: clamp(1.35rem, 3.5vw, 1.75rem);
  line-height: 1.2;
  font-weight: 800;
  margin: 0;
  flex: 1;
}

.dlg__close {
  flex: none;
  width: 44px;
  height: 44px;
  margin: -0.35rem -0.35rem 0 0;   /* wizualnie równo z krawędzią, cel dotykowy pełny */
  display: grid;
  place-items: center;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  color: var(--ink-muted);
  cursor: pointer;
  transition: color var(--t-ui) var(--ease), border-color var(--t-ui) var(--ease);
}

.dlg__close svg { width: 20px; height: 20px; }

.dlg__close:hover {
  color: var(--ink);
  border-color: var(--ink-muted);
}

.dlg__intro {
  margin: 0;
  color: var(--ink-muted);
  font-size: 0.95rem;
}

/* Formularz w oknie nie powtarza ramki ani tła — ma je już panel. */
.form--dlg {
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 0;
}

/* Grupa „e-mail albo telefon": jedna ramka na dwa pola, żeby było widać,
   że stanowią całość i wystarczy wypełnić jedno.
   Klasa stoi w sekcji okna demo, ale od skrócenia formularza kontaktowego
   (2026-08-15) używają jej oba formularze — dlatego nie jest zagnieżdżona
   pod .form--dlg. */
.field--group {
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  padding: var(--space-4);
  margin: 0;
  display: grid;
  gap: var(--space-4);
}

.field--group legend {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
  padding-inline: var(--space-2);
}

/* Etykieta <legend> siedzi w przerwie obrysu, więc złamana na dwie linie
   przepuszcza kreskę między wierszami — wygląda to na błąd, nie na zamysł.
   „Jak się z Tobą skontaktować?" mierzy 252 px przy 254 px dostępnych na
   telefonie 390 px, czyli przelewa się o dwa piksele. Zamiast skracać tekst
   w obu wersjach językowych — zagęszczamy etykietę na wąskich ekranach.
   Zapas po zmianie to ~30 px, więc dłuższe tłumaczenie też się zmieści. */
@media (max-width: 460px) {
  .field--group legend {
    font-size: 0.7rem;
    letter-spacing: 0.1em;
    padding-inline: var(--space-1);
  }
}

/* Telefon pełnoekranowo: okno na całą wysokość, bez zaokrągleń i ramki —
   inaczej przy klawiaturze systemowej zostaje pasek tła bez funkcji. */
@media (max-width: 600px) {
  .dlg {
    width: 100%;
    height: 100dvh;
    max-height: 100dvh;
    margin: 0;
    border: 0;
    border-radius: 0;
  }
}

/* Wejście okna: ta sama krzywa i czas co wejście sekcji. Bez animacji
   wyjścia — zamknięcie ma być natychmiastowe.
   Klasa .motion pochodzi z site.js; przy prefers-reduced-motion nie ma jej
   wcale, a blok na końcu pliku i tak zeruje wszystkie animacje. */
.motion .dlg[open] { animation: dlg-in var(--t-enter) var(--ease) both; }
.motion .dlg[open]::backdrop { animation: dlg-scrim var(--t-fold) var(--ease) both; }

@keyframes dlg-in {
  from { opacity: 0; transform: translateY(12px); }
}

@keyframes dlg-scrim {
  from { opacity: 0; }
}

/* 19 Ziarno ================================================================ */
/* Ziarnistość filmowa na całej stronie: cienka warstwa szumu w kolorze
   zdjętym do szarości, nałożona przez multiply. Nie przyciemnia obrazu —
   nadaje tłu fakturę papieru, tak żeby duże płaszczyzny --ground
   nie czytały się jak plastikowa plama.

   Szum jest generowany w SVG (feTurbulence), nie plikiem PNG: nic się nie
   pobiera, a kafelek jest dokładnie taki, jaki ma być.

   Warstwa leży NAD nagłówkiem (z-index 50), bo faktura ma być na całej
   stronie, a nie tylko na treści. Pod skip-linkiem (99), żeby ten został
   ostry. Okno demo jest w warstwie górnej przeglądarki (top layer), więc
   ziarno go nie dotyka — formularz zostaje czysty.

   pointer-events: none — warstwa nie łapie ani jednego kliknięcia. */
.grain {
  position: fixed;
  inset: -6%;                 /* zapas na przesunięcia w animacji */
  z-index: 60;
  pointer-events: none;
  /* 0.13 wyznaczone na oko na dużych płaszczyznach --ground: niżej ziarno
     przestaje być widoczne i jest tylko kilobajtami, wyżej zaczyna brudzić
     jasne tło i zjadać kontrast drobnego tekstu. */
  opacity: 0.13;
  mix-blend-mode: multiply;
  /* Szum wychodzi z feTurbulence kolorowy, więc najpierw zdejmujemy
     nasycenie, a potem drugą macierzą przyciemniamy kanał niebieski
     mocniej niż czerwony. Ziarno przestaje być obojętnie szare i robi się
     ciepłe — paleta z design.md nie dopuszcza neutralnych bez podbarwienia
     w stronę światła lampy, a przy mnożeniu szary szum ściągałby ciepło
     z tła zamiast je podbijać. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeColorMatrix type='matrix' values='1 0 0 0 0 0 0.94 0 0 0 0 0 0.82 0 0 0 0 0 1 0'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23n)'/%3E%3C/svg%3E");
  background-repeat: repeat;
}

/* „Żywe" ziarno — przeskoki co klatkę, jak w projektorze. steps(1) daje
   skok, nie płynne przesunięcie: płynne czytałoby się jak dryfująca tapeta.
   Animujemy wyłącznie transform.
   Klasa .grain-live pochodzi z site.js i jest dokładana tylko przy
   prawdziwym kursorze i włączonym ruchu — na telefonie i przy
   prefers-reduced-motion ziarno zostaje nieruchomą fakturą. */
.grain-live .grain {
  will-change: transform;
  animation: grain-drift 1.4s steps(1) infinite;
}

@keyframes grain-drift {
  0%    { transform: translate3d(0, 0, 0); }
  12.5% { transform: translate3d(-1.5%, -1%, 0); }
  25%   { transform: translate3d(1%, -1.5%, 0); }
  37.5% { transform: translate3d(-1%, 1.5%, 0); }
  50%   { transform: translate3d(1.5%, 1%, 0); }
  62.5% { transform: translate3d(-1.5%, 0.5%, 0); }
  75%   { transform: translate3d(1%, -0.5%, 0); }
  87.5% { transform: translate3d(-0.5%, -1.5%, 0); }
}

/* 20 Magiczny ślad — dym za kursorem ======================================== */

/* NAJPIERW, ŻEBY NIE POMYLIĆ DWÓCH RÓŻNYCH RZECZY:

   WYCOFANY (2026-08-15) — własny kursor. Punkt akcentu z pierścieniem,
   ZAMIAST natywnej strzałki. Zdjęty, bo wymagał schowania natywnego
   kursora (znikała łapka nad linkiem i belka nad polem tekstowym),
   a pierścień zasłaniał litery. Nie przywracać.

   OBECNY — ślad światła. Natywny kursor zostaje nietknięty i widoczny
   cały czas. Ślad jest DODATKOWĄ warstwą, która leży POD treścią
   (z-index 0 wobec .wrap na 1), więc nie ma czego zasłonić — świeci zza
   tekstu, nie przed nim.

   GDZIE (2026-08-15, wersja 2): hero i „jak to działa". Warunkiem jest
   odsłonięte tło sekcji — cząsteczki lecą pod treścią, więc sekcja pełna
   nieprzezroczystych kart zjadłaby efekt. Dlatego NIE w cenniku
   (.plan--pick i .plan--premium mają tła) ani przy formularzu (.form ma
   tło). W „jak to działa" wiersze .step są samymi kreskami bez tła, więc
   światło przechodzi całą sekcją — i tam kursor i tak wędruje, bo wiersze
   mają stan hover.

   BARWA (zmiana 2026-08-19): dym NIE jest już miodowy. Do tej daty brał
   --accent-glow, czyli barwę kręgu światła, i przez to czytał się jako
   drugą plamę akcentu w kadrze, w którym krąg już stoi. Teraz ma własny
   token --smoke: neutralną ciepłą biel. Uzasadnienie i wszystkie pomiary
   kontrastu — przy tokenie, sekcja 00.

   Praktyczny skutek dla tej sekcji: dym przestał być „tym samym kręgiem
   światła reagującym na rękę". Krąg jest światłem lampy i zostaje ciepły;
   dym jest powietrzem i światła nie niesie. Wspólna została zasada,
   nie barwa: miękka plama o niskiej nieprzezroczystości, zawsze pod
   treścią. */

/* Cały efekt rysuje JEDEN canvas na sekcję, nie zestaw elementów.
   To nie jest wybór estetyczny, tylko wniosek z pomiaru: nad stroną leży
   ziarno z mix-blend-mode, które musi spłaszczyć wszystko pod spodem do
   jednej tekstury. Kilkadziesiąt osobnych elementów-cząsteczek pod tą
   blendą zatkałoby kompozytor — poprzednia wersja robiła to już przy
   dwunastu (patrz nota w design.md). Canvas to jedna warstwa niezależnie
   od tego, ile cząsteczek lata.

   Rozdzielczość bufora jest celowo niższa od ekranowej (site.js, SCALE):
   dym jest miękką plamą, więc nikt nie zobaczy różnicy, a malowania jest
   ponad trzy razy mniej. */
.trail-canvas {
  position: absolute;
  inset: 0;
  z-index: 0;              /* nad tłem sekcji, POD .wrap (z-index 1) */
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* 21 Mikro-interakcje ====================================================== */
/* Jeden stan hover dla wszystkich wierszy na stronie: kroki procesu,
   wiersze zestawienia kosztów, obietnice, dane kontaktowe i pytania FAQ.
   Ciepła poświata akcentu przy 8% — ta sama rodzina co krąg światła,
   więc najechanie czyta się jak podłożenie lampy pod wiersz, a nie jak
   podświetlenie z arkusza kalkulacyjnego.

   Plama siedzi w ::before, nie w tle wiersza: dzięki temu wychodzi poza
   kolumnę tekstu i ma zaokrąglone rogi, a układ nie drgnie ani o piksel
   (nic nie dostaje paddingu na hover). Animujemy samo opacity. */
.step,
.compare__row,
.addons__row,
.promises li,
.contact__direct li,
.fine summary,
.qa summary { position: relative; }

.step::before,
.compare__row::before,
.addons__row::before,
.promises li::before,
.contact__direct li::before,
.fine summary::before,
.qa summary::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline: calc(-1 * var(--space-4));
  z-index: -1;
  pointer-events: none;
  border-radius: var(--r-sm);
  background: oklch(0.735 0.155 68 / 0.08);
  opacity: 0;
  transition: opacity var(--t-ui) var(--ease);
}

.step:hover::before,
.compare__row:hover::before,
.addons__row:hover::before,
.promises li:hover::before,
.contact__direct li:hover::before,
.fine summary:hover::before,
.qa summary:hover::before { opacity: 1; }

/* Etykieta mono w wierszu przejmuje kolor akcentu — ten sam sygnał,
   co przy aktywnym linku nawigacji. */
.step:hover .step__no,
.contact__direct li:hover .contact__key { color: var(--accent-deep); }

.step__no,
.contact__key { transition: color var(--t-ui) var(--ease); }

/* Karty i przystanki: samo uniesienie, bez zmiany tła — inaczej sekcja
   „usługi" zaczęłaby migotać sześcioma prostokątami naraz. */
.svc,
.demo__stop { transition: transform var(--t-ui) var(--ease); }
.svc:hover,
.demo__stop:hover { transform: translateY(-2px); }

/* 22 Lenis ================================================================= */
/* Wymagane przez bibliotekę — bez tego przewijanie z bezwładnością walczy
   z natywnym scroll-behavior. Klasy na <html> zakłada sam Lenis.
   lenis-stopped ma overflow: clip, nie hidden — z tego samego powodu, co
   przy .dlg-open wyżej: hidden zrobiłby z <html> kontener przewijania
   i zepsuł sticky w nagłówku. */
html.lenis,
html.lenis body { height: auto; }

.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-stopped { overflow: clip; }
.lenis.lenis-smooth iframe { pointer-events: none; }

/* Odsłonięciami steruje GSAP — CSS oddaje mu pole, żeby przejście nie
   działo się dwa razy (raz z transition, raz z tweenu). Stan początkowy
   (opacity 0, translateY 12px) zostaje w CSS, więc gdyby GSAP się nie
   wczytał, wraca ścieżka z IntersectionObserverem i przejściem CSS. */
.motion--gsap .reveal { transition: none; }
.motion--gsap .hero .reveal { transition-delay: 0ms; }
