/* ==========================================================================
   Agencja Wrocławska — base + layout
   Desktop-first. Breakpoints: tablet ≤1024px (design 768), mobile ≤640px (375)
   ========================================================================== */

/* ----- Reset / base ----- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; padding: 0; }
/* scrollbar-gutter: pasek przewijania ma zarezerwowane miejsce od pierwszego
   renderu — bez tego po odblokowaniu intro strona (i flary tła w %) skakały
   w poziomie o szerokość paska */
html { scroll-behavior: smooth; overflow-x: clip; scrollbar-gutter: stable; }
body {
  font-family: var(--type-family-body), system-ui, sans-serif;
  background: #000;
  color: var(--text-primary);
  -webkit-font-smoothing: antialiased;
  overflow-x: clip;
  position: relative;
}
/* ==========================================================================
   FLARY — czerwona poświata tła.

   🔴 05.08.2026 (Karol, OLED): „wyraźnie widać, gdzie ten czerwony okrąg się
   kończy". Przyczyna ZMIERZONA: każda poświata miała tylko DWA przystanki
   (pełny kolor → przezroczystość), więc jasność spadała po linii prostej
   i urywała się w jednym punkcie. Oko widzi taką granicę jako obwódkę
   (efekt Macha), a na ekranie o dużym kontraście — bardzo wyraźnie.

   Lekarstwo 1: DZIESIĘĆ przystanków na krzywej alfa(t) = A0 × (1 − t²)².
   Ta krzywa dochodzi do zera z ZEROWYM nachyleniem — światło gaśnie coraz
   wolniej i nie ma miejsca, w którym coś się „kończy". Środek i zasięg
   poświat zostają bez zmian (te same środki, te same promienie, ta sama
   jasność w środku) — zmienia się wyłącznie sposób dochodzenia do czerni.
   ⚠️ Ostatni kolor to rgba(255,44,44,0), NIE rgba(0,0,0,0): przejście
   z czerwieni do przezroczystej CZERNI po drodze szarzeje.

   Lekarstwo 2: ziarno na wierzchu — bardzo słaby szum DOKŁADANY po równo
   (rozpraszanie, dithering). Rozbija równe pasy jasności: zamiast ostrej
   granicy między pasem „3" a pasem „2" powstaje wymieszanie obu.
   🔴 ZMIERZONE, DLACZEGO NIE „overlay": mieszanie overlay działa
   proporcjonalnie do jasności tła — w jasnym środku poświaty podbiło
   jasność ze 153 na 214, a w słabym ogonie (1–12), gdzie obwódki są
   widoczne, nie zmieniało prawie nic. Odwrotnie, niż trzeba.
   Dlatego szum jest BIAŁY o bardzo niskim kryciu (do 0,006): dokłada
   od 0 do ok. 1,5 poziomu jasności niezależnie od tła. Poziom 1 na 255
   jest niewidoczny nawet na OLED, a to wystarczy, by rozmyć granicę pasa.
   ⚠️ color-interpolation-filters='sRGB' — bez tego przeglądarka liczy szum
   w innej przestrzeni barw i wychodzi kilkakrotnie mocniejszy.
   ========================================================================== */
body::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='z' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0.006 0 0 0 0'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23z)'/%3E%3C/svg%3E"),
radial-gradient(1360px 940px at 88% 2%, rgba(255,44,44,0.6) 0%, rgba(255,44,44,0.5852) 5.69%, rgba(255,44,44,0.5424) 11.39%, rgba(255,44,44,0.4758) 17.08%, rgba(255,44,44,0.3921) 22.78%, rgba(255,44,44,0.2996) 28.47%, rgba(255,44,44,0.208) 34.17%, rgba(255,44,44,0.1264) 39.86%, rgba(255,44,44,0.0627) 45.56%, rgba(255,44,44,0.0217) 51.25%, rgba(255,44,44,0.0031) 56.95%, rgba(255,44,44,0) 62.64%),
    radial-gradient(1040px 800px at 2% 12%, rgba(176,26,26,0.46) 0%, rgba(176,26,26,0.4487) 5.89%, rgba(176,26,26,0.4159) 11.78%, rgba(176,26,26,0.3648) 17.67%, rgba(176,26,26,0.3006) 23.56%, rgba(176,26,26,0.2297) 29.45%, rgba(176,26,26,0.1595) 35.35%, rgba(176,26,26,0.0969) 41.24%, rgba(176,26,26,0.0481) 47.13%, rgba(176,26,26,0.0166) 53.02%, rgba(176,26,26,0.0024) 58.91%, rgba(176,26,26,0) 64.8%),
    radial-gradient(1120px 940px at 90% 30%, rgba(255,44,44,0.32) 0%, rgba(255,44,44,0.3121) 5.89%, rgba(255,44,44,0.2893) 11.78%, rgba(255,44,44,0.2538) 17.67%, rgba(255,44,44,0.2091) 23.56%, rgba(255,44,44,0.1598) 29.45%, rgba(255,44,44,0.1109) 35.35%, rgba(255,44,44,0.0674) 41.24%, rgba(255,44,44,0.0335) 47.13%, rgba(255,44,44,0.0116) 53.02%, rgba(255,44,44,0.0017) 58.91%, rgba(255,44,44,0) 64.8%),
    radial-gradient(1020px 860px at 4% 48%, rgba(255,44,44,0.24) 0%, rgba(255,44,44,0.2341) 6.09%, rgba(255,44,44,0.217) 12.17%, rgba(255,44,44,0.1903) 18.26%, rgba(255,44,44,0.1568) 24.35%, rgba(255,44,44,0.1199) 30.44%, rgba(255,44,44,0.0832) 36.52%, rgba(255,44,44,0.0506) 42.61%, rgba(255,44,44,0.0251) 48.7%, rgba(255,44,44,0.0087) 54.79%, rgba(255,44,44,0.0013) 60.87%, rgba(255,44,44,0) 66.96%),
    radial-gradient(1060px 920px at 90% 66%, rgba(255,44,44,0.19) 0%, rgba(255,44,44,0.1853) 6.09%, rgba(255,44,44,0.1718) 12.17%, rgba(255,44,44,0.1507) 18.26%, rgba(255,44,44,0.1242) 24.35%, rgba(255,44,44,0.0949) 30.44%, rgba(255,44,44,0.0659) 36.52%, rgba(255,44,44,0.04) 42.61%, rgba(255,44,44,0.0199) 48.7%, rgba(255,44,44,0.0069) 54.79%, rgba(255,44,44,0.001) 60.87%, rgba(255,44,44,0) 66.96%),
    radial-gradient(980px 860px at 6% 82%, rgba(176,26,26,0.17) 0%, rgba(176,26,26,0.1658) 6.09%, rgba(176,26,26,0.1537) 12.17%, rgba(176,26,26,0.1348) 18.26%, rgba(176,26,26,0.1111) 24.35%, rgba(176,26,26,0.0849) 30.44%, rgba(176,26,26,0.0589) 36.52%, rgba(176,26,26,0.0358) 42.61%, rgba(176,26,26,0.0178) 48.7%, rgba(176,26,26,0.0061) 54.79%, rgba(176,26,26,0.0009) 60.87%, rgba(176,26,26,0) 66.96%),
    radial-gradient(1080px 900px at 88% 96%, rgba(255,44,44,0.15) 0%, rgba(255,44,44,0.1463) 6.28%, rgba(255,44,44,0.1356) 12.57%, rgba(255,44,44,0.119) 18.85%, rgba(255,44,44,0.098) 25.13%, rgba(255,44,44,0.0749) 31.42%, rgba(255,44,44,0.052) 37.7%, rgba(255,44,44,0.0316) 43.99%, rgba(255,44,44,0.0157) 50.27%, rgba(255,44,44,0.0054) 56.55%, rgba(255,44,44,0.0008) 62.84%, rgba(255,44,44,0) 69.12%);
  pointer-events: none;
  transition: opacity 1.4s ease;
}
/* keep the background clean during the intro; fade the glow in once the
   hero title is formed (JS removes .intro-play at that point) */
html.intro-play body::before { opacity: 0; }
html.intro-play.intro-glow body::before { opacity: 1; }
img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; }
/* anty-sierotka (reguła Karola 10.07): akapit/nagłówek nie może kończyć się
   pojedynczym słowem w ostatniej linijce — balance wyrównuje linijki nagłówków,
   pretty każe przeglądarce unikać sierotek w akapitach */
h1, h2, h3 { text-wrap: balance; }
p, li { text-wrap: pretty; }
button { font: inherit; cursor: pointer; border: none; background: none; color: inherit; }

/* „Tylko dla czytnika ekranu": element jest w treści dla Google i czytników ekranu,
   ale niewidoczny na oczy. white-space:nowrap trzyma go w jednej linii — dzięki temu
   pomiar sierotek (scan-orphans / bramka) go pomija i mierzy wyłącznie widoczną prozę. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.container {
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: 32px;
}

/* ----- Buttons ----- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  font-weight: 700;
  font-size: var(--type-size-md);
  line-height: 1;
  white-space: nowrap;
  transition: background-color .2s ease, color .2s ease, box-shadow .2s ease, border-color .2s ease;
}
.btn--primary {
  background: var(--accent-default);
  color: var(--text-on-accent);
  padding: 17px 31px;
  border-radius: 50px;
  border: 1px solid transparent;
  box-shadow: 0 14px 17px var(--accent-glow);
}
.btn--primary:hover {
  background: var(--accent-hover);
  box-shadow: 0 16px 28px 2px rgba(255, 44, 44, 0.85);
}
/* 🔴 Guzik bez czerwonej poświaty (Karol, 05.08: „do usunięcia ta flara
   z guzika »Dalej«"). Poświata ma sens tam, gdzie guzik leży na czarnym tle
   i jest jedyną rzeczą na ekranie — w otwarciu. W panelu z audytem guzik stoi
   tuż obok świecącego kółka wyniku i dwie łuny biły się o uwagę.
   Sam guzik zostaje czerwony — schodzi tylko łuna pod nim. */
.btn--bez-flary, .btn--bez-flary:hover { box-shadow: none; }
.btn--secondary {
  background: var(--inverse-surface);
  color: var(--text-on-inverse);
  padding: 13px 30px;
  border-radius: 55px;
  border: 1px solid var(--inverse-surface);
  font-weight: 600;
}
.btn--secondary:hover {
  color: var(--text-accent);
  border-color: var(--border-accent);
}

/* Wydajność (wzorzec z podstron usługowych, sesja 4): sekcje strony głównej
   poza pierwszym ekranem nie są liczone przy starcie — przeglądarka renderuje
   je dopiero przy zbliżaniu (mniej pracy Style & Layout = niższy TBT na mobile).
   --cis = zapas wysokości ≈ realna wysokość sekcji (auto = po pierwszym
   wyrenderowaniu przeglądarka pamięta prawdziwą). UWAGA przy pomiarach
   geometrii: wyłączać przez content-visibility:visible (pułapka z sesji 6). */
.cv-auto { content-visibility: auto; contain-intrinsic-size: auto var(--cis, 900px); }

/* Skip-link (a11y): niewidoczny, dopóki nie dostanie fokusu z klawiatury */
.skip-link {
  position: fixed;
  top: -100px;
  left: 16px;
  z-index: 100;
  padding: 12px 20px;
  border-radius: 12px;
  background: #0a0a0a;
  color: #fff;
  border: 1px solid var(--accent-default);
  font-size: var(--skala-etykieta); /* 30.07: bylo dziedziczone 16 px, poza skala */
  font-weight: 600;
  text-decoration: none;
  transition: top .2s ease;
}
.skip-link:focus-visible { top: 16px; }

/* ==========================================================================
   Header / Navbar
   ========================================================================== */
.header {
  position: sticky;
  top: 0;
  z-index: 50;
  padding: 32px var(--page-pad-desktop) 0;
  pointer-events: none;
}
/* Belka ma stałą wysokość 70 px i NIE rośnie (06.08 wieczorem, Karol: „nie może
   rozwijać się teraz cała sekcja"). Menu usług zjeżdża pod nią jako panel —
   wzorzec adwise.pl. Panel siedzi w środku `.navbar` (`position: absolute`),
   więc bierze od niej szerokość i punkt zaczepienia. Reguły otwierania
   i wyglądu panelu: site.css, przy `.nav-dd`.
   ⚠️ Poprzedni układ (siatka o dwóch rzędach `70px 0fr` → `70px 1fr`) został
   zdjęty razem ze skryptem liczącym miejsce pod „Usługi" — nie zostawiaj tu
   martwego `grid-template-rows`. */
.navbar {
  position: relative;
  pointer-events: auto;
  max-width: var(--container-max);
  margin-inline: auto;
  height: 70px;
  background: var(--surface-nav);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-radius: 35px;
  box-shadow: -10px 10px 24px rgba(0, 0, 0, 0.3);
}
.navbar__rzad {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 100%;
  padding: 0 32px;
  min-width: 0;
}
/* thin white→red gradient border (masked ring, like Figma) */
.navbar::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(115deg, var(--border-strong) 0%, var(--accent-glow) 72%, var(--accent-default) 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  pointer-events: none;
}
.logo { display: block; height: 34px; width: auto; }
/* Wyściółka w pionie wyzerowana ujemnym odstępem: pole kliknięcia rośnie z 34 px
   do 44 px (tyle trzeba, żeby pewnie trafić kciukiem), a układ nie drga — logotyp
   zostaje dokładnie tam, gdzie był. Sprawdzone zrzutem: obraz przed i po jest
   co do piksela ten sam. (Etap 4, decyzja Karola 09.08.) */
.navbar__brand { display: flex; align-items: center; gap: 12px; min-height: 44px; padding: 12px 0; margin: -12px 0; }
.brand-mark {
  font-family: var(--type-family-display), sans-serif;
  font-weight: 900;
  font-size: 30px;
  color: var(--accent-default);
  letter-spacing: -2px;
  line-height: 1;
}
.brand-name {
  font-weight: 700;
  font-size: 17px;
  line-height: 1.05;
  color: var(--text-primary);
}
.navbar__links { display: flex; align-items: center; }
.navbar__links a {
  display: inline-block;
  padding: 0 20px;
  line-height: 50px;
  font-weight: 700;
  font-size: var(--skala-etykieta);
  color: var(--text-primary);
  white-space: nowrap;
  transition: color .18s ease;
}
.navbar__links a:hover { color: var(--text-accent); }

/* Navbar fit below full-HD: progressively tighter header padding so all items
   stay on one line down to the burger breakpoint (1024).
   30.07.2026: rozmiar liter zdjety stad — belka jest teraz na wspolnej skali
   (etykieta 15 px, bo nawigacja to oznaczenie, nie tekst do czytania).
   15 px jest MNIEJSZE niz dotychczasowe 16, wiec mieszczenie w jednej linii
   tylko na tym zyskuje. */
@media (max-width: 1600px) {
  .header { padding-left: 64px; padding-right: 64px; }
  .navbar__links a { padding: 0 16px; }
}
@media (max-width: 1200px) {
  .header { padding-left: 40px; padding-right: 40px; }
  .navbar__links a { padding: 0 13px; }
}

.navbar__burger {
  display: none;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  width: 28px;
  padding: 6px 0;
}
.navbar__burger span {
  display: block;
  height: 2px;
  width: 24px;
  border-radius: 2px;
  background: var(--graphic-on-dark);
  transition: transform .25s ease, opacity .2s ease;
}
.navbar__burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.navbar__burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; transform: scaleX(0); }
.navbar__burger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* Mobile overlay menu */
.mobile-menu {
  position: fixed;
  inset: 0;
  z-index: 49;
  background: rgba(10, 10, 12, 0.96);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  display: flex;
  justify-content: center;
  /* menu wyższe niż ekran: przewijane w środku, góra nie chowa się pod belką
     (margin:auto na liście centruje, gdy jest krótsza — sesja 8 mobile) */
  overflow-y: auto;
}
.mobile-menu[hidden] { display: none; }
.mobile-menu__links { display: flex; flex-direction: column; gap: 24px; text-align: center; margin: auto; padding: 118px 0 48px; }
.mobile-menu__links a {
  font-weight: 700;
  font-size: 26px;
  color: var(--text-primary);
}
.mobile-menu__links a:hover { color: var(--text-accent); }

/* Cele dotykowe na telefonie (sesja 8, WCAG 2.5.8): pola klikalne rosną
   paddingiem do ~44px, wygląd bez zmian (odstępy skorygowane gapem/marginesem). */
@media (max-width: 820px) {
  .navbar__burger { width: 44px; height: 44px; justify-content: center; padding: 0; margin-right: -8px; }
  .mobile-menu__links { gap: 6px; }
  .mobile-menu__links a { display: inline-block; padding: 9px 12px; }
  /* 11.08.2026, etap 4 domykany przed przełączeniem: odnośniki stopki miały 34 px
     przy progu 44 px dla palca. Wyściółka rośnie 7 → 12 px (pole 34 → 44), a ujemny
     odstęp -5 px z góry i z dołu sprawia, że w układzie strony element zajmuje
     dalej 34 px. Efekt: palec ma 44 px, a stopka wygląda co do piksela tak samo.
     Sprawdzone pomiarem — napisy co 46 px przed i po, kolumny w tych samych miejscach.
     ⚠️ Nie ruszać tego przez zmianę `gap` w liście: reguła `gap: 12px` z sekcji stopki
     stoi NIŻEJ w pliku i wygrywa, a odstępy rosną wtedy z 46 na 56 px. */
  .footer__col li a { display: inline-block; padding: 12px 0; margin: -5px 0; }
  /* Adres e-mail w stopce miał 20 px — sam w akapicie, więc bez przerwy do
     skorygowania: wyściółkę cofa ujemny odstęp, tekst nie drgnie. */
  .footer__email a { display: inline-block; padding: 12px 0; margin: -12px 0; }
  .chat__bar-label { font-size: 13px; }
}

/* ==========================================================================
   Hero
   ========================================================================== */
.hero {
  position: relative;
  min-height: 900px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  padding: 60px 20px;
}
.hero__content { position: relative; z-index: 2; text-align: center; display: inline-block; }
.hero__headline { position: relative; }
.hero__cta { position: relative; z-index: 3; margin-top: 44px; }
/* Hero CTA: czerwona obwódka + BIAŁY napis bez pogrubienia (uwagi Karola 10.07),
   hover = pełna czerwień */
.hero__cta .btn--primary {
  background: transparent;
  color: #fff;
  border-color: var(--accent-default);
  /* 🔴 12.08, uwaga Karola z przeglądu komputera: „obramowanie dostrój do
     wielkości fontu, po bokach za dużo wolnego miejsca".

     PRZYCZYNA Z POMIARU, a nie z oka. Do 12.08 stało tu `min-width: 323px` —
     sztuczne przytrzymanie dawnej szerokości z czasów, gdy na guziku był
     dłuższy napis („Zarezerwuj spotkanie online"). Po skróceniu napisu 11.08
     wyściółka przestała cokolwiek znaczyć: guzik i tak rozpychała minimalna
     szerokość. Zmierzone przy 1440 px:
       cała zawartość (napis + odstęp + strzałka)   158 px
       guzik                                        323 px  → treść to 49%
       realna przerwa przy krawędzi                82,5 px przy 10 px do strzałki
     Czyli po bokach było 8 razy więcej powietrza niż między napisem a strzałką.
     Dokładnie to, co widać na ekranie.

     NAPRAWA. Minimalna szerokość ZDJĘTA — guzik wraca do liczenia z wyściółki,
     tak jak każdy inny na stronie. Wyściółka pozioma 38 → 33 px, żeby zachować
     TĘ SAMĄ proporcję pion:poziom co w wersji telefonowej, którą zatwierdziłeś
     11.08 (9,8 : 16 px = 1 : 1,63; tutaj 20 : 33 px = 1 : 1,65).
     Po zmianie: guzik 224 px, treść 71% jego szerokości, przerwa przy krawędzi
     33 px przy 10 px do strzałki (3,3 zamiast 8). */
  padding: 20px 33px;
  font-size: 1.06em;
  font-weight: 400;
  box-shadow: none;
}
.hero__cta .btn--primary:hover {
  background: var(--accent-default);
  color: var(--text-on-accent);
  border-color: var(--accent-default);
  box-shadow: none;
}
.hero__title {
  font-family: var(--type-family-display), sans-serif;
  font-weight: 900;
  font-size: clamp(64px, 14.6vw, 250px);
  line-height: 0.98;
  letter-spacing: -0.045em;
  /* metallic gradient fill */
  background-image:
    linear-gradient(180deg, rgba(255,255,255,0.2) 24%, rgba(0,0,0,0.2) 49%, rgba(255,255,255,0.2) 73%),
    linear-gradient(115deg, var(--color-neutral-50) 22%, var(--color-neutral-300) 99%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
}
.hero__title-line { display: block; }
/* ⬇️ POŁOŻENIE CZERWONEGO HASŁA — 48%, czyli stan pierwotny.
   09.08 przesunęliśmy je na 102% (pod cały napis), bo przy 48% przechodzi przez
   styk wierszy „DIGITAL" i „MARKETING" i częściowo nakłada się na białe litery.
   Karol obejrzał obie wersje na żywo i zdecydował: WRACAMY DO 48%. Przesunięte
   hasło wyglądało jak usterka, a nakładanie jest zamierzonym efektem warstwowym.
   ⛔ Nie „poprawiaj" tego ponownie bez pytania — decyzja zapadła po obejrzeniu. */
.hero__accent {
  --haslo-w-pionie: 48%;
  position: absolute;
  top: var(--haslo-w-pionie);
  left: 50%;
  transform: translate(-50%, -50%);
  width: max-content;
  margin: 0;
  font-family: var(--type-family-display-expanded), sans-serif;
  font-weight: 500;
  font-size: clamp(24px, 5.5vw, 95px);
  letter-spacing: -0.02em;
  color: var(--accent-default);
  /* red glow + a soft dark shadow that falls onto DIGITAL MARKETING behind it */
  /* 🔴 11.08: ciemna warstwa `0 10px 26px` ZDJĘTA także tutaj. Zostaje sama
     czerwona poświata — czerń przeszła do `.hero__accent-cien` niżej, która ma
     własny ruch. Do 14:00 dotyczyło to tylko telefonu; polecenie Karola
     „ustaw też na innych urządzeniach" rozszerzyło to na komputer i tablet. */
  text-shadow: 0 4px 100px rgba(149, 88, 88, 0.7), 0 4px 50px rgba(255, 44, 44, 0.6);
}

/* ==========================================================================
   WARSTWA CIENIA HASŁA — `.hero__accent-cien` (kopia napisu, czarna i rozmyta)
   ==========================================================================
   Leży pod literami, ma ten sam krój i tę samą treść, więc jest cieniem
   dokładnie w kształcie hasła — nie prostokątem i nie owalem. Znaczy to, że po
   zgaszeniu tekstu nie ma czego zostawić na ekranie w formie plamy (błąd
   z rundy drugiej: poduszka `::before` nie znała cyklu hasła).

   DLACZEGO OSOBNA WARSTWA, A NIE `text-shadow`: cień zapisany jako `text-shadow`
   jest częścią rysowania tekstu, więc jedzie DOKŁADNIE ten sam ruch co litery.
   Nie da się mu nadać własnego ciężaru, o który prosił Karol.

   TRZY RÓŻNICE WZGLĘDEM RUCHU TEKSTU — z nich bierze się „ciężar":
     • WEJŚCIE: opóźnienie 90 ms i dłuższy czas (780 zamiast 550 ms) — cień
       dochodzi za tekstem, jak coś cięższego ciągniętego z tyłu.
     • WYJŚCIE: bez opóźnienia i krócej (430 ms) — cień gaśnie PRZED tekstem.
       To nie jest niekonsekwencja, tylko warunek Karola: w żadnej klatce nie
       może być ciemnej plamy bez napisu. Krzywa wyjścia jest TA SAMA co u tekstu,
       tylko czas krótszy — wtedy cień jest zawsze dalej w drodze do zera niż
       tekst i warunek zachodzi z arytmetyki, nie z trafienia pomiarem.
     • DROGA: cień jedzie krótszy odcinek niż tekst (0,62em wejścia przy 0,4em
       tekstu, 0,18em wyjścia przy 0,35em) i dokłada zmianę wielkości.

   SIŁA — DOCHODZONA W CZTERECH KROKACH, każdy po uwadze Karola.
   Mierzone tak: ten sam kadr z cieniem i z wyłączonym cieniem, różnica jasności
   w pasie 370 × 90 px wokół hasła. „Zasięg tła" = ile procent pikseli w tym
   pasie jest w ogóle przyciemnionych; „punkt" = najciemniejsze miejsce.
     s67  „za mocny"          · punkt 94,8 · zasięg —
     s68  „brak cienia"       · punkt 43,0 · zasięg —
     s71  „jeszcze mocniejszy"· punkt 106  · zasięg 21,7%
     TERAZ                    · punkt 128  · zasięg 54,0%
   Ostatni krok to polecenie: „ma się bardziej rozpraszać tworząc tło, najlepiej
   tak, żeby wystawał trochę dookoła liter, ale naprawdę niewiele". Stąd podział
   pracy między trzy rzeczy:
     • `-webkit-text-stroke` — POGRUBIENIE litery cienia. To ono odpowiada za
       „wystaje dookoła liter", więc trzymane nisko: 0,055em (1,6 px na telefonie).
     • `text-shadow` na tej warstwie — SZEROKIE, MIĘKKIE TŁO. To ono odpowiada za
       „rozprasza się": dwie łuny 0,62em i 1,17em. Zasięg skoczył 21,7 → 54,0%.
     • `filter: blur` — zmiękczenie całości, żeby nie było widać obrysu jako kreski.
   ⚠️ Gdyby czerń dochodziła samym kryciem, robiłaby ciemną obwódkę zamiast tła.

   JEDNOSTKA `em` ZAMIAST PIKSELI JEST CELOWA: hasło ma 29 px na telefonie,
   42 px na tablecie i 79 px na komputerze. W pikselach ten sam cień byłby
   na komputerze prawie niewidoczny, a na telefonie za ciężki. W `em` wygląda
   tak samo na każdej szerokości.

   ⚠️ `text-shadow` na tej warstwie MUSI być zapisany jawnie. Bez tego warstwa
   dziedziczy czerwoną poświatę z rodzica i rozmywa ją drugi raz, przez co
   czerwień rośnie zamiast maleć. */
.hero__accent-cien {
  display: block;
  position: absolute;
  left: 0;
  top: 0;
  z-index: 0;               /* tekst ma z-index: 1 — cień jest zawsze pod nim */
  white-space: nowrap;
  pointer-events: none;
  color: rgba(0, 0, 0, .66);
  -webkit-text-stroke: 0.055em rgba(0, 0, 0, .66);
  text-shadow: 0 0 0.62em rgba(0, 0, 0, .6), 0 0 1.17em rgba(0, 0, 0, .4);
  filter: blur(0.207em);
  transform: translateY(0.07em);
  will-change: opacity, transform;
  /* WEJŚCIE. 🔴 11.08, uwaga Karola: „animacja musi być płynniejsza, moment
     pojawiania się jest jakby lekko opóźniony, musi to być smooth".
     Zmienione DWIE rzeczy, obie o płynność:

     1. ZDJĘTE OPÓŹNIENIE 90 ms. Miało dać cieniowi ciężar — dochodzić za
        tekstem jak coś cięższego ciągniętego z tyłu. Na ekranie czytało się
        jako zacięcie, nie jako ciężar. Ciężar zostaje w tym, co go nie psuje:
        czas 780 ms przy 550 ms tekstu i dłuższa droga (0,62em przy 0,4em).

     2. KRZYWA TA SAMA CO U TEKSTU (była .16,.84,.44,1). Przy tej samej krzywej
        i dłuższym czasie cień jest w każdej chwili MNIEJ widoczny od napisu —
        z arytmetyki, nie z trafienia pomiarem. Wcześniej pilnowało tego
        opóźnienie; po jego zdjęciu musi to robić krzywa.

     ⚠️ ROZMYCIE NIE JEST JUŻ ANIMOWANE i to też jest o płynność. Rozmycie
     przelicza się na nowo w KAŻDEJ klatce, a ta warstwa ma pod spodem jeszcze
     dwie miękkie łuny — na telefonie to najdroższa rzecz w całym otwarciu.
     Krycie i przesunięcie telefon rysuje kartą graficzną prawie za darmo.
     Rozmycie zostaje stałe, więc nie ma czego przeliczać ani czym szarpać. */
  /* 🔴 16.08.2026 — POWRÓT DO WERSJI Z 11.08 (polecenie Karola).
     Przebudowa z 14.08 (przejazd na klatkach, dwa hasła w kadrze naraz)
     została zdjęta w całości: „miałeś wrócić do poprzedniej wersji,
     a po prostu przerobiłeś animację na jeszcze gorszą”.
     Cień wraca do stanu z 11.08: własny czas i własna droga — dłużej przy
     wejściu (0,78 s przy 0,55 s tekstu, droga 0,62em przy 0,4em), krócej przy
     wyjściu (0,43 s), plus zmiana wielkości. Stąd bierze się ciężar, o który
     Karol prosił. Krótsze wyjście to warunek z 11.08: w żadnej klatce nie ma
     ciemnej plamy bez napisu.
     Cofnięcie tej zmiany: wrócić do 0,55 s w obie strony, bez skalowania. */
  transition:
    opacity .78s cubic-bezier(.22, .61, .36, 1),
    transform .78s cubic-bezier(.22, .61, .36, 1);
}
/* wyjście: bez opóźnienia, krócej niż tekst, ta sama krzywa co tekst */
.hero__accent-cien.is-out {
  opacity: 0;
  transform: translateY(-0.18em) scale(.97);
  transition:
    opacity .43s cubic-bezier(.22, .61, .36, 1),
    transform .43s cubic-bezier(.22, .61, .36, 1);
}
/* pozycja startowa — ustawiana bez animacji, tuż przed wejściem */
.hero__accent-cien.is-enter {
  transition: none;
  opacity: 0;
  transform: translateY(0.62em) scale(1.04);
}

/* Napisy-marquee w tle hero USUNIĘTE (decyzja Karola 10.07) —
   tło hero to same flary + cząsteczki. */

/* ==========================================================================
   Responsive — Tablet (≤1024px, design 768)
   ========================================================================== */
@media (max-width: 1024px) {
  .header { padding: 20px var(--page-pad-tablet) 0; }
  /* poniżej 1024 px działa menu pod hamburgerem — panel usług nie ma tu nic do roboty */
  .navbar { height: 60px; border-radius: 30px; }
  .navbar__rzad { padding: 0 20px; }
  /* ⚠️ Chowanie panelu usług przeniesione do `site.css` (przy `.nav-dd`).
     Tutaj byłoby MARTWE: `site.css` jest wklejany do strony PO tym pliku,
     a panel ma tam `display: grid`, które biło to `display: none`. */
  .navbar__links { display: none; }
  .navbar__burger { display: flex; }
  .hero { min-height: 520px; padding: 40px var(--page-pad-tablet); }
}

/* ==========================================================================
   Responsive — Mobile (≤640px, design 375)
   ========================================================================== */
@media (max-width: 640px) {
  .header { padding: 20px var(--page-pad-mobile) 0; }
  .navbar__rzad { padding: 0 16px; }
  .brand-mark { font-size: 26px; }
  .brand-name { font-size: 15px; }
  .hero { min-height: 360px; padding: 32px var(--page-pad-mobile); }

  /* --- GUZIK OTWARCIA NA TELEFONIE (Karol, 11.08: „za duży, brakuje proporcji,
         powinien być delikatny, ale widoczny — pierwsze skrzypce gra hero") ------
     ZMIERZONA PRZYCZYNA: guzik miał 323 × 60 px na telefonie i DOKŁADNIE TE SAME
     323 × 60 px na komputerze — nie było dla niego ani jednej reguły telefonowej.
     W proporcjach do napisu: komputer 323/1228 = 26%, telefon 323/356 = 91%.
     Czyli na telefonie był 3,5 raza bardziej dominujący niż w projekcie.

     Po zmianie: 190 × 44 px = 53% szerokości napisu. Do 26% jak na komputerze
     zeszliśmy dzięki skróceniu napisu „Zarezerwuj spotkanie online" → „Umów
     spotkanie" (decyzja Karola 11.08) — szerokość guzika trzyma długość napisu,
     nie wyściółka.
     ⚠️ min-height 44 px NIE JEST OZDOBĄ: sama wyściółka 13 px daje 43 px, czyli
     o piksel poniżej progu dla palca (ta sama zasada, co przy stopce w etapie 4). */
  /* 🔴 DRUGIE ŚCIĘCIE, 11.08 (Karol po obejrzeniu na telefonie: „Umów spotkanie
     jeszcze do pomniejszenia, tak o 30%"). Pierwsze podejście dało 190 × 44 px
     = 53% szerokości napisu. Cel: ok. 133 px, czyli 37%.

     ⚠️ WYSOKOŚĆ NIE MOŻE ZEJŚĆ O 30% RAZEM Z RESZTĄ. 44 × 0,7 = 31 px, a 44 px
     to próg pola dotykowego przyjęty w tym projekcie (etap 4 podnosił dla niego
     stopkę z 34 px). Rozwiązanie to samo, co tam: guzik WYGLĄDA na ok. 32 px,
     a niewidoczna nakładka (`::after`) rozszerza pole klikania do 46 px w pionie.
     Wygląd maleje, palec dostaje swoje.
     ⚠️ Nakładka wymaga `position: relative` na guziku — bez tego liczyłaby się
     od hero i wylądowałaby w losowym miejscu ekranu. */
  .hero__cta { margin-top: 26px; }
  .hero__cta .btn--primary {
    position: relative;
    /* 🔴 11.08, uwaga Karola: „powiększyć o 20% plus odstęp do poprawienia —
       przerwa między lewą krawędzią guzika a literką U musi być większa,
       bo wygląda nieproporcjonalnie do strzałki i krawędzi".

       PRZYCZYNA Z POMIARU, bo to nie było widzimisię. Odstępy w guziku:
         telefon (przed):  krawędź→napis 11 px · napis→strzałka 10 px · strzałka→krawędź 11 px
         komputer:         krawędź→napis 82,5 px · napis→strzałka 10 px · strzałka→krawędź 82,5 px
       Na komputerze przerwa przy krawędzi jest 8 razy większa od przerwy między
       napisem a strzałką — strzałka „trzyma się" napisu, a po bokach jest powietrze.
       Na telefonie obie przerwy były prawie równe (11 do 10), więc strzałka
       odklejała się od napisu i guzik czytał się jak trzy rzeczy rozstawione
       po równo. Dokładnie to Karol zobaczył.

       NAPRAWA — dwie rzeczy naraz:
       1. Wielkość +20%: pismo 12 → 14,4 px, strzałka 12 → 14 px, wyściółka
          pionowa 8 → 9,8 px. Wysokość 30 → 36 px.
       2. Proporcja odstępów: wyściółka pozioma 10 → 16 px, a przerwa między
          napisem a strzałką 10 → 6 px. Stosunek przerwy przy krawędzi do
          przerwy przy strzałce idzie z 1,1 na 2,8 — w stronę komputera. */
    padding: 9.8px 16px;
    gap: 6px;
    font-size: 14.4px;
    min-height: 0;
    /* min-width: 0 zostaje jako bezpiecznik. Sztuczne 323 px z wersji
       komputerowej zniknęło 12.08 (patrz `.hero__cta .btn--primary` wyżej),
       więc dziś ta linia nic nie zdejmuje — ale kosztuje zero i chroni przed
       powrotem minimalnej szerokości na telefon. */
    min-width: 0;
  }
  .hero__cta .btn--primary::after {
    content: "";
    position: absolute;
    /* pole dotykowe równe progowi 44 px: guzik ma teraz 36 px, więc nakładka
       dokłada po 4 px z góry i z dołu (przy 30 px dokładała po 7) */
    inset: -4px 0;
    /* sama nakładka nie może zjadać kliknięć innym elementom — jest częścią
       guzika, więc kliknięcie w nią trafia w guzik */
  }
  .hero__cta .btn--primary .icon { width: 14px; height: 14px; }

  /* --- CZERWONE HASŁO NA TELEFONIE (Karol, 11.08: „bardzo małe i mało czytelne",
         po wyborze wariantu G: „musimy bardziej uwypuklić czerwony napis […]
         bo teraz się zlewa") --------------------------------------------------
     DWIE OSOBNE PRZYCZYNY, obie zmierzone:

     1. WIELKOŚĆ. Wzór `clamp(24px, 5.5vw, 95px)` przy ekranie 390 px liczy
        5,5% = 21,5 px, czyli poniżej dolnej granicy — hasło stało na sztywnych
        24 px i NIE SKALOWAŁO SIĘ z ekranem w ogóle.
        Nowa wartość 29 px to MAKSIMUM, nie „ładna liczba": najdłuższe z czterech
        haseł, „Zwiększamy sprzedaż", ma 341 px przy 28 px. Napis „MARKETING"
        ma 356 px. 28 × 356/341 = 29,2 px — powyżej hasło byłoby szersze
        od napisu, pod którym leży.

     2. CZYTELNOŚĆ. Hasło leży na styku wierszy „DIGITAL" i „MARKETING"
        (`--haslo-w-pionie: 48%` — świadoma decyzja Karola z 09.08, NIE RUSZAMY),
        więc jego dolna połowa wypada na BIAŁYCH, metalicznych literach.
        Czerwień #ff2c2c na bieli to kontrast 3,3:1 — za mało.
        Stary cień (`0 10px 26px` przy 60% czerni) był za rozmyty i za słaby,
        żeby cokolwiek odciąć.

     🔴 PIERWSZE PODEJŚCIE ODRZUCONE PRZEZ KAROLA (11.08, po obejrzeniu na telefonie):
     „cień jest za mocny i nie współpracuje z tekstem, jest na sztywno, a powinien
     być zanimowany spójnie do tekstu — czyli pojawiać się i znikać z nim, i być
     zawsze za literami. Teraz widać czarną plamę między treściami. Za bardzo
     zasłania DIGITAL MARKETING. Mieliśmy tylko lekko wypuklić czerwone napisy,
     a nie sprawić, że one dominują kompozycję."

     Trzy zarzuty, JEDNA przyczyna: poduszka była osobnym elementem doklejonym
     do RODZICA (`.hero__accent::before`), a hasło zmienia się co 3 sekundy
     i gaśnie na elemencie WNUKU (`.hero__accent-text` dostaje klasę `is-out`).
     Poduszka o tym cyklu nic nie wiedziała, więc po zgaszeniu tekstu zostawała
     goła ciemna plama przecinająca napis w poprzek — potwierdzone zrzutem klatki
     w trakcie podmiany hasła.

     NAPRAWA: żadnego osobnego elementu. Zostaje sam CIEŃ TEKSTU, który z definicji
     spełnia wszystkie trzy warunki Karola naraz:
       • znika i pojawia się razem z tekstem, bo jest częścią jego rysowania,
       • leży zawsze dokładnie za literami hasła i podąża za ich kształtem,
       • nie tworzy prostokąta ani owalu, więc nie ma czego „zasłonić".
     Siła zejściowa: z czterech ciemnych warstw (2, 5, 14 px przy kryciu 85-95%)
     na dwie (3 i 10 px przy kryciu 40-50%). Czerwona poświata jak dotąd.
     Sprawdzone na zrzutach: hasło dalej odcina się od białych liter, a napis
     „DIGITAL MARKETING" jest widoczny w całości.

     Odrzucone po drodze, każde ze zrzutem: sam stary cień przy 29 px (za słaby
     nad literami), mocniejsza czerwona poświata zamiast ciemnego konturu
     (czerwień na bieli dalej się zlewa), przygaszanie liter poziomym pasem
     (widoczna krawędź w połowie słowa „DIGITAL" — czyta się jak usterka).

     Reguła obejmuje TYLKO telefon (≤640 px), bo tam Karol to oglądał. */
  .hero__accent {
    font-size: 29px;
    /* 🔴 11.08, RUNDA TRZECIA. Uwaga Karola: „Cień dalej jest sztywny w tym samym
       miejscu. Cień musi być częścią animacji czerwonych napisów. Ma mieć też
       ciężar animacji, być smooth i dopracowany. Dalej jest zbyt mocny."

       Dwie ciemne warstwy (`0 0 3px` przy 50% i `0 0 10px` przy 40%) ZESZŁY
       z tej reguły. Zostaje sama czerwona poświata. Ciemny cień przeniesiony
       do `.hero__accent-cien` — osobnej warstwy pod literami, która ma własną
       krzywą i własne opóźnienie. Powód: `text-shadow` jest częścią rysowania
       tekstu, więc jedzie DOKŁADNIE ten sam ruch co litery — nie da mu się
       nadać ciężaru, o który prosi Karol. Poprzednia rozmowa uznała, że skoro
       cień gaśnie razem z tekstem, to „animacja jest załatwiona". Nie była. */
    text-shadow:
      0 4px 50px rgba(255, 44, 44, .6),
      0 4px 100px rgba(149, 88, 88, .7);
  }

  /* Warstwa cienia hasła (`.hero__accent-cien`) przeniesiona 11.08 POZA tę
     sekcję — od tej pory rysuje się na KAŻDEJ szerokości (polecenie Karola:
     „ustaw też na innych urządzeniach"). Szukaj jej przy regule `.hero__accent`
     wyżej w pliku. Wartości są tam w jednostce `em`, więc skalują się razem
     z wielkością hasła: 29 px na telefonie, 42 px na tablecie, 79 px na komputerze. */
}

/* --- Napis otwarcia na telefonie (etap 4, uwaga Karola: „za mały, ma iść
       na całą szerokość ekranu") --------------------------------------------
   Były DWIE usterki naraz, obie widoczne dopiero poniżej ~413 px:

   1. Słowo pękało w środku: „MARKETING" łamało się na „MARK" i „ETING".
      Powód nieoczywisty: każda litera jest osobnym elementem (ma własny ruch
      w animacji otwarcia), a między osobnymi elementami przeglądarce WOLNO
      złamać wiersz — nawet w środku słowa. Stąd `white-space: nowrap`.
   2. Napis nie mieścił się w szerokości ekranu: `clamp()` wyżej ma dolną
      granicę 64 px, a przy 64 px „MARKETING" zajmuje 373 px — na ekranie
      390 px zostaje 350 px, więc słowo nie miało prawa się zmieścić.
      Zamiast stałej liczby liczymy wielkość pisma z szerokości ekranu.

   Dzielnik 5,9 wzięty z POMIARU (nie z głowy): „MARKETING" jest 5,82 razy
   szersze niż wysokość pisma — sprawdzone na 320, 360, 390, 412 i 430 px.
   Zapas do 5,9 zostawia ~3% marginesu na zaokrąglenia kroju.
   Granica 430 px, bo dokładnie tam kończy się problem: przy 431 px i wyżej
   64 px z `clamp()` już się mieści i design z komputera działa bez zmian. */
@media (max-width: 430px) {
  .hero { padding-left: 12px; padding-right: 12px; }
  .hero__title { font-size: calc((100vw - 30px) / 5.9); }
  .hero__title-line { white-space: nowrap; }
}

/* ==========================================================================
   Sections — shared
   ========================================================================== */
.section { padding: 90px 0; position: relative; }
/* Nagłówki sekcji: skala jak .svc-h2 na podstronach usług (uwaga Karola 11.07 —
   przy nagłówkach-zdaniach stary rozmiar był nieproporcjonalnie wielki). */
.section-heading {
  font-weight: 600;
  font-size: clamp(30px, 3.5vw, 46px);
  line-height: 1.07;
  letter-spacing: -0.02em;
  color: var(--text-heading);
}
.section-heading--center { text-align: center; }
/* Podpis grupy usług („Klienci mają Cię znajdować:"). Karol, 06.08:
   „font ma być wielkości nazw usług" — więc NIE własna wartość, tylko ten sam
   znacznik rozmiaru co `.offer-card h4` (36 px). Jedna zmiana w `tokens.css`
   przesunie oba naraz i nie rozjadą się przy następnej zmianie skali.
   Było: clamp(19px, 1.7vw, 23px) → na komputerze 23 px, na telefonie 19 px. */
.section-subheading {
  font-weight: 600;
  font-size: var(--type-size-h2-sm);
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--text-heading);
  margin-top: 10px;
}
.section-text {
  font-weight: 400;
  font-size: var(--type-size-body);
  line-height: 1.62;
  color: var(--text-body);
  max-width: 620px;
}
.accent { color: var(--text-accent); }
.section__cta-center { display: flex; justify-content: center; margin-top: 40px; }
.icon { width: 18px; height: 18px; flex: none; }

/* ==========================================================================
   AI section
   ========================================================================== */
.ai__grid {
  margin-top: 52px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 48px;
  align-items: center;
}
/* --- ODDECH FORMULARZA AUDYTU (Karol, 11.08: „możesz dodać jakiś lekki efekt
       animacyjny? widzę tutaj coś w stylu lekkiego pulsowania albo glow
       świecącego na zmianę lekko mocniej i słabiej") -------------------------
   ⚠️ USTALENIE, KTÓRE ZMIENIŁO PODEJŚCIE: `.ai__form` NIE MA ŻADNEGO TŁA
   ANI OBWÓDKI — to goły pojemnik, formularz stoi wprost na tle sekcji.
   Nie ma więc czego „podświetlać po brzegach". Dlatego światło idzie ZA blok:
   rozmyta czerwona łuna, która wchodzi i schodzi. Tak wygląda „glow świecący
   na zmianę mocniej i słabiej" z uwagi Karola.

   Druga warstwa: obwódki pól jaśnieją w TYM SAMYM rytmie (ten sam czas trwania
   i ta sama krzywa), więc oko czyta to jako oddech jednego przedmiotu, a nie
   dwa niezależne migotania.
   🔴 Obwódka jaśnieje BIELĄ, nie czerwienią. Czerwona obwódka pola formularza
   to w każdej przeglądarce sygnał „błąd walidacji" — pulsowałaby informacja,
   że coś jest źle, zanim gość cokolwiek wpisze.

   ⛔ GUZIKA „ODBIERAM" ŚWIADOMIE NIE RUSZAMY. Ma klasę `btn--bez-flary`, czyli
   wyłączoną czerwoną poświatę — decyzja Karola z 05.08. Pulsowanie guzika byłoby
   jej cichym cofnięciem, a uwaga mówi „formularz", nie „guzik".

   Cykl 4 s: wolniej niż tętno (ok. 1 s) i wolniej niż oddech (ok. 4 s to dolna
   granica) — ma być tłem, nie migotaniem przyciągającym wzrok od treści.
   Przy systemowym ograniczeniu ruchu gaśnie razem z resztą animacji. */
.ai__form { position: relative; }
.ai__form::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: -8% -6%;
  border-radius: 40px;
  background: radial-gradient(closest-side, rgba(255, 44, 44, .16), rgba(255, 44, 44, 0) 78%);
  opacity: .35;
  pointer-events: none;
  animation: ai-form-oddech 4s ease-in-out infinite;
}
.ai__form .field { animation: ai-form-obwodka 4s ease-in-out infinite; }
@keyframes ai-form-oddech {
  0%, 100% { opacity: .3; transform: scale(.97); }
  50%      { opacity: .8; transform: scale(1); }
}
@keyframes ai-form-obwodka {
  0%, 100% { border-color: var(--border-default); }
  50%      { border-color: rgba(255, 255, 255, .3); }
}
@media (prefers-reduced-motion: reduce) {
  .ai__form::before, .ai__form .field { animation: none; }
}

.ai__form-title { font-weight: 600; font-size: var(--type-size-h2-sm); line-height: 1.2; letter-spacing: -0.02em; color: var(--text-primary); }
.ai__form-text { margin-top: 14px; font-size: var(--type-size-body); line-height: 1.62; color: var(--text-body); max-width: 560px; }
.ai__form-controls { margin-top: 28px; display: flex; flex-direction: column; gap: 24px; align-items: flex-start; }
/* Zgoda i klauzula (06.08): odstępy niesie `gap` układu, więc gasimy marginesy własne.
   Zgoda stoi bliżej pola, do którego się odnosi — stąd ujemne dociągnięcie. */
.ai__form-controls .zgl__consent { margin: -8px 0 0; max-width: 523px; }
.ai__form-controls .zgl__legal { margin: 0; max-width: 523px; }
.field {
  width: 100%;
  max-width: 523px;
  height: 49px;
  padding: 0 16px;
  border-radius: 11px;
  background: var(--surface-input);
  border: 1px solid var(--border-default);
  color: var(--text-primary);
  font-size: var(--type-size-md);
}
.field::placeholder { color: var(--text-muted); }

/* Chat */
.chat {
  position: relative;
  /* reserve room on the left for the external provider rail */
  margin-left: 64px;
  background: linear-gradient(180deg, var(--surface-raised), var(--surface-sunken));
  border: 1px solid var(--border-default);
  border-radius: 24px;
  padding: 21px;
  box-shadow: 0 50px 110px -50px rgba(0,0,0,0.95), inset 0 0 0 1px var(--surface-overlay);
}
.chat::after { content: ""; position: absolute; inset: 0; border: 1px solid var(--border-accent); border-radius: 24px; opacity: .55; pointer-events: none; }
.chat__bar { display: flex; align-items: center; gap: 7px; padding-bottom: 15px; border-bottom: 1px solid var(--border-subtle); }
.chat__dot { width: 9px; height: 9px; border-radius: 50%; background: var(--border-idle-dot); }
.chat__dot--red { background: var(--accent-default); }
.chat__bar-label { margin-left: 6px; font-size: var(--type-size-xs); font-weight: 700; letter-spacing: .02em; color: var(--text-muted); }
.chat__body { display: flex; flex-direction: column; gap: 13px; padding-top: 18px; }
.bubble { font-size: var(--type-size-sm); line-height: 1.62; border: 1px solid var(--border-default); }
.bubble--user { align-self: flex-end; max-width: 78%; background: var(--surface-bubble-user); color: var(--text-primary); padding: 11px 15px; border-radius: 17px 17px 6px 17px; }
.bubble-row { display: flex; gap: 11px; align-items: flex-start; }
.avatar { flex: none; width: 42px; height: 42px; border-radius: 99px; border: 1px solid var(--border-default); background: radial-gradient(circle at 30% 30%, var(--color-neutral-700), var(--color-neutral-900)); }
.bubble--ai { background: var(--surface-bubble-ai); border-color: var(--border-subtle); color: var(--text-answer); padding: 13px 16px; border-radius: 17px 17px 17px 6px; }
.bubble--ai p { margin: 0; }
.bubble__sources { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; font-size: 12px; font-weight: 700; }
.bubble__sources span { color: var(--text-muted); margin-right: 6px; }
.bubble__sources i { font-style: normal; color: var(--text-body); border: 1px solid var(--border-subtle); border-radius: 30px; padding: 3px 9px; }
/* provider rail: outside the card's LEFT edge, label stacked ABOVE the icons */
.chat__providers { position: absolute; top: 0; left: -52px; display: flex; flex-direction: column; align-items: center; gap: 11px; }
.chat__providers-label {
  writing-mode: vertical-rl; transform: rotate(180deg);
  margin-bottom: 8px;
  font-size: 11px; font-weight: 700; letter-spacing: .22em; text-transform: uppercase;
  color: var(--text-muted); white-space: nowrap; pointer-events: none;
}
.provider { width: 44px; height: 44px; display: grid; place-items: center; border-radius: 13px; background: var(--surface-chip); border: 1px solid var(--border-subtle); color: var(--text-muted); font-weight: 700; font-size: 15px; }

/* ==========================================================================
   PRZYKŁADOWY WYNIK AUDYTU AI SEARCH (sekcja 2) — zastąpił makietę czatu 05.08

   Jedna liczba niesie całą sekcję: kółko z widocznością. Pod nim rozbicie na
   cztery modele, na końcu zdanie wyjaśniające. Czerwień AW pada TYLKO na łuk
   kółka — cztery czerwone słupki obok zrobiłyby z akcentu tapetę.

   RUCH (uwaga Karola 05.08: „animacje mają być smooth, tempo nie stałe"):
   • łuk i liczba jadą razem z jednego licznika w main.js — krzywa wolno-szybko-
     wolno, 1,5 s. Liczba nie może dojść do 39 wcześniej niż łuk;
   • słupki modeli ruszają PO kółku, jeden po drugim (--op = opóźnienie w HTML),
     każdy własnym wyhamowaniem;
   • przy włączonym w systemie ograniczeniu ruchu skrypt nie robi nic —
     panel po prostu stoi gotowy.
   Stan spoczynkowy w CSS to stan KOŃCOWY (słupki pełne, łuk na 39%), więc bez
   JavaScriptu i dla wyszukiwarek panel jest kompletny. Klasa .is-czeka cofa go
   do zera dopiero wtedy, gdy skrypt naprawdę zamierza animować.
   ========================================================================== */
.audyt {
  position: relative;
  margin-left: 8px;
  background: linear-gradient(180deg, var(--surface-raised), var(--surface-sunken));
  border: 1px solid var(--border-default);
  border-radius: 24px;
  padding: 24px 26px 22px;
  box-shadow: 0 50px 110px -50px rgba(0,0,0,0.95), inset 0 0 0 1px var(--surface-overlay);
}
.audyt::after { content: ""; position: absolute; inset: 0; border: 1px solid var(--border-accent); border-radius: 24px; opacity: .55; pointer-events: none; }

/* 🔴 DWIE RÓWNE KOLUMNY (przebudowa 05.08 wieczorem, kierunek Karola).
   Do tej pory panel był poziomym pasem: u góry kółko 146 px obok zdania,
   pod spodem cztery modele rozciągnięte na CAŁE 595 px szerokości wnętrza.
   Paski dostawały po 433 px — najwięcej miejsca w panelu dostawał element,
   który niesie najmniej treści.
   Teraz wnętrze 595 px dzieli się na pół z 28 px odstępu → po 283 px na kolumnę.
   Lewa: samo kółko, urosło ze 146 na 248 px (1,7×). Prawa: zdanie z wynikiem
   i cztery modele — oba dostają tę samą szerokość, więc lista czyta się jako
   rozwinięcie zdania, a nie jako osobny blok pod spodem.
   `align-items: center` pionuje zawartość prawej kolumny do środka kółka. */
.audyt {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 28px;
  align-items: center;
}
/* Lewa kolumna: kółko wyśrodkowane. Osobne opakowanie, bo łuna kółka
   (`.audyt__ring-halo`) wystaje o 22% poza jego obrys — bez niego kółko
   musiałoby samo pilnować odstępu od krawędzi panelu. */
.audyt__kolo { display: grid; place-items: center; }
/* 🔴 146 → 248 px. Wszystko w środku rysunku wektorowego skaluje się razem
   z nim (grubość łuku, promień, główka), więc to czysty zoom, nie nowy rysunek.
   248 px to szerokość kolumny (283) minus zapas 35 px na łunę. */
.audyt__ring { position: relative; flex: none; width: 248px; height: 248px; }
/* 🔴 overflow: visible NIE JEST OZDOBĄ — to naprawa błędu graficznego z 05.08
   („widać odcięcie poświaty", Karol). Rysunek wektorowy domyślnie przycina
   wszystko na krawędzi swojego pola. Pole ma 120 jednostek, łuk sięga 56,5
   od środka, więc do krawędzi zostaje 3,5 jednostki — a poświata potrzebuje
   około 13. Efekt: czerwona łuna urywała się prostą pionową kreską po prawej
   stronie kółka. Odcięcie było w rysunku, nie w kolorze poświaty. */
.audyt__ring svg { position: relative; z-index: 1; width: 100%; height: 100%; transform: rotate(-90deg); overflow: visible; }
.audyt__ring-tor { fill: none; stroke: rgba(255,255,255,.09); stroke-width: 9; }
/* 🔴 POŚWIATA MOCNIEJSZA 11.08 (Karol: „chciałbym, żeby czerwona linia w okręgu
   miała trochę więcej efektu glow, jak się pojawia I ZOSTAJE").
   Kluczowe było to drugie słowo. Poświata była najmocniejsza przez ułamek sekundy
   „wdechu" po dobiciu licznika do 39% (krycie łuny szło na 1,0), po czym WRACAŁA
   do 0,55 — czyli stan, w którym kółko stoi przez resztę wizyty, był najsłabszy
   z wszystkich. Podnosimy więc SPOCZYNEK, nie tylko szczyt.
   Cztery liczby, wszystkie w tej sekcji: łuk 9 → 14 px i 45 → 60% krycia,
   łuna 0,55 → 0,80 krycia i 26 → 34% czerwieni w gradiencie, szczyt wdechu
   z powiększeniem 1,10 → 1,16. Cofnięcie = te same cztery liczby z powrotem. */
.audyt__ring-luk {
  fill: none; stroke: var(--accent-default); stroke-width: 9; stroke-linecap: round;
  filter: drop-shadow(0 0 14px rgba(255,44,44,.6));
}
/* Łuna pod kółkiem — daje głębię i „światło" wyniku. Leży POD rysunkiem
   wektorowym (własny element, nie filtr), więc nic jej nie przycina. */
/* Kolejność warstw wewnątrz kółka jest wpisana WPROST (0 łuna, 1 rysunek,
   2 liczba). Bez tego łuna — jako element ustawiony bezwzględnie — malowałaby
   się NAD rysunkiem, bo elementy ustawione zawsze biją te w normalnym biegu. */
.audyt__ring-halo {
  position: absolute; inset: -22%; border-radius: 50%; pointer-events: none; z-index: 0;
  background: radial-gradient(closest-side, rgba(255,44,44,.34), rgba(255,44,44,0) 76%);
  opacity: .8;
}
/* Główka na czubku łuku: biały punkt z czerwoną łuną. W trakcie doliczania
   jedzie po okręgu, więc oko widzi ruch po torze, a nie samo wydłużanie kreski. */
.audyt__ring-glowica {
  fill: #fff;
  filter: drop-shadow(0 0 6px rgba(255,44,44,.95));
  transition: opacity .35s ease;
}
/* Liczba w kółku: para „39 %" stoi na wspólnej dolnej krawędzi, a cała para
   jest w geometrycznym środku kółka. Stąd dwa poziomy — zewnętrzny centruje,
   wewnętrzny wyrównuje cyfry ze znakiem procentu. */
.audyt__ring-liczba { position: absolute; z-index: 2; inset: 0; display: grid; place-items: center; }
.audyt__ring-para { display: flex; align-items: flex-end; color: var(--text-primary); letter-spacing: -0.03em; }
/* Liczba rośnie w TEJ SAMEJ proporcji co kółko (248/146 = 1,70): 46 → 78 px,
   znak procentu 21 → 36 px. Gdyby została na 46 px, większe kółko zrobiłoby
   z niej mały napis w dużej obręczy — czyli odwrotność tego, o co chodziło. */
.audyt__ring-para b { font-size: 78px; font-weight: 600; line-height: 1; font-variant-numeric: tabular-nums; }
.audyt__ring-para i { font-size: 36px; font-weight: 500; font-style: normal; line-height: 1; margin-left: 3px; color: var(--text-body); }

/* Prawa kolumna: zdanie z wynikiem, pod nim cztery modele. 24 px odstępu —
   tyle, żeby lista czytała się jako rozwinięcie zdania, a nie jako oddzielny
   kafel. Do 05.08 modele leżały POD całym panelem, oddzielone 26 px pustki
   ciągnącej się przez całą szerokość. */
.audyt__dane { display: grid; row-gap: 24px; }

/* Zdanie i podpis „Sprawdzam…" leżą W TYM SAMYM MIEJSCU siatki, jeden pod
   drugim. Gdyby stały obok siebie w normalnym układzie, przejście z fazy
   sprawdzania na wynik przesuwałoby cały panel o wysokość jednej linijki. */
.audyt__zdanie { display: grid; }
.audyt__zdanie > * { grid-area: 1 / 1; align-self: center; transition: opacity .45s ease; }
/* 🔴 Rozmiar zdania podniesiony 05.08 z 16 na 22 px (Karol: „treść »twoja
   marka…« do powiększenia, to ważny komunikat"). Razem z rozmiarem poszła
   jasność (było 80% bieli, jest pełna) i grubość (400 → 500) — sam większy
   stopień pisma przy szarym, cienkim kroju dalej czytałby się jako podpis. */
.audyt__zdanie-wynik {
  font-size: 22px; line-height: 1.34; font-weight: 500; letter-spacing: -0.015em;
  color: var(--text-primary);
  transition: opacity .5s ease, transform .55s cubic-bezier(.22, 1, .36, 1);
}
/* inline-block na stałe, NIE tylko na czas animacji osiadania: przełączanie
   sposobu wyświetlania w trakcie potrafi przełamać wiersz w innym miejscu
   i zdanie „podskakuje" o linijkę */
.audyt__zdanie-wynik b { font-weight: 700; display: inline-block; }
.audyt__zdanie-praca { font-size: var(--type-size-md); line-height: 1.45; color: var(--text-muted); opacity: 0; }
/* Napis pracy złożony z liter — fala przechodzi przez nie w kolejności.
   Litery są zgrupowane w słowa, a słowo jest nierozrywalne: bez tego wiersz
   mógłby się złamać w środku wyrazu, bo każda litera jest osobnym elementem. */
.audyt__praca-slowo { white-space: nowrap; }
/* inline-block, bo sam element w wierszu tekstu nie da się podnieść w pionie */
.audyt__zdanie-praca i { display: inline-block; font-style: normal; }

/* --- cztery modele --- */
.audyt__silniki { display: grid; row-gap: 13px; }
.audyt__silnik {
  display: grid;
  grid-template-columns: 22px minmax(64px, auto) 1fr auto;
  align-items: center;
  column-gap: 12px;
}
.audyt__silnik-znak { width: 22px; height: 22px; opacity: .92; }
.audyt__silnik-nazwa { font-size: var(--type-size-sm); color: var(--text-body); }
.audyt__silnik-tor { height: 6px; border-radius: 99px; background: rgba(255,255,255,.08); overflow: hidden; }
.audyt__silnik-slup {
  display: block; height: 100%; border-radius: 99px;
  background: linear-gradient(90deg, rgba(255,255,255,.55), #fff);
  transform: scaleX(var(--p, 1)); transform-origin: left center;
  transition: transform .95s cubic-bezier(.22, 1, .36, 1) var(--op, 0ms);
}
/* liczba zapala się RAZEM ze swoim słupkiem — to samo opóźnienie --op — i przy
   okazji podnosi się o 6 px, żeby nie „mrugała" w miejscu obok ruchomego słupka */
.audyt__silnik-liczba {
  font-size: var(--type-size-sm); font-weight: 600; color: var(--text-primary);
  font-variant-numeric: tabular-nums; min-width: 40px; text-align: right;
  transition: opacity .4s ease var(--op, 0ms),
              transform .55s cubic-bezier(.22, 1, .36, 1) var(--op, 0ms);
}

/* ══ FAZA SPRAWDZANIA (pierwsze 3 s) ═════════════════════════════════════
   Karol 05.08: „na początku animacja ładowania tam z 3 sekundy, potem wynik
   też jako animacja". Panel najpierw UDAJE PRACĘ narzędzia: kręci się kreska
   w kółku, pod spodem lecą nazwy sprawdzanych modeli, a wiersze zapalają się
   jeden po drugim — dopiero potem wchodzi wynik.

   Wszystko poniżej jest ODSTĘPSTWEM od stanu spoczynkowego. Stan spoczynkowy
   to GOTOWY WYNIK, więc bez JavaScriptu i przy wyłączonym ruchu w systemie
   panel po prostu stoi z liczbami — żadnego wiecznego „ładowania". */
.audyt__ring-skan { fill: none; stroke: var(--accent-default); stroke-width: 9; stroke-linecap: round; opacity: 0; transform-origin: 60px 60px; }
.audyt__kropki { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 7px; opacity: 0; }
.audyt__kropki i { width: 8px; height: 8px; border-radius: 50%; background: var(--text-muted); animation: audyt-tetno 1.15s ease-in-out infinite; }
.audyt__kropki i:nth-child(2) { animation-delay: .18s; }
.audyt__kropki i:nth-child(3) { animation-delay: .36s; }

.audyt.is-ladowanie .audyt__ring-luk { opacity: 0; }
.audyt.is-ladowanie .audyt__ring-glowica { opacity: 0; }
.audyt.is-ladowanie .audyt__ring-halo { opacity: 0; }
.audyt.is-ladowanie .audyt__ring-skan { opacity: 1; animation: audyt-obrot 1.1s linear infinite; }
.audyt.is-ladowanie .audyt__ring-para { opacity: 0; }
.audyt.is-ladowanie .audyt__kropki { opacity: 1; }
.audyt.is-ladowanie .audyt__zdanie-wynik { opacity: 0; transform: translateY(10px); }
.audyt.is-ladowanie .audyt__zdanie-praca { opacity: 1; }
/* 🔴 FALA PRZEZ LITERY — to jest teraz jedyny sygnał „narzędzie pracuje".
   Każda kolejna litera rusza 42 ms po poprzedniej, więc przez napis
   przechodzi grzbiet: 27 liter × 42 ms = 1134 ms wędrówki w cyklu 1,6 s,
   czyli fala zdąża przejść i zaczyna od nowa bez postoju.
   Ruch jest CELOWO mały (2 px w górę, jasność 55% → 100%): ma czytać się jak
   oddech pracującego narzędzia, a nie jak skakanka. Napis musi zostać
   czytelny — to jego jedyne zadanie. */
.audyt.is-ladowanie .audyt__zdanie-praca i {
  animation: audyt-praca-fala 1.6s ease-in-out infinite;
  animation-delay: calc(var(--i, 0) * 42ms);
}
@keyframes audyt-praca-fala {
  0%, 62%, 100% { transform: translateY(0);    opacity: .55; }
  28%           { transform: translateY(-2px); opacity: 1; }
}
/* Trzy stany wiersza w fazie sprawdzania — do 05.08 były dwa i wszystkie cztery
   tory świeciły naraz, co czytało się jako „wszystko się ładuje". Teraz światło
   idzie tylko po wierszu, który narzędzie sprawdza W TEJ CHWILI, a sprawdzone
   zostają przygaszone — widać postęp, a nie migotanie całej listy. */
.audyt.is-ladowanie .audyt__silnik { opacity: .3; }
.audyt.is-ladowanie .audyt__silnik.is-sprawdzony { opacity: .72; }
.audyt.is-ladowanie .audyt__silnik.is-teraz { opacity: 1; }
.audyt.is-ladowanie .audyt__silnik-liczba { opacity: 0; transform: translateY(6px); }
.audyt.is-ladowanie .audyt__silnik-slup { transform: scaleX(0); transition: none; }
/* Przesuw światła po pustym torze — „coś się dzieje" bez udawania wyniku.
   Światło idzie po WSZYSTKICH torach, ale po sprawdzanym w tej chwili jest
   wyraźnie mocniejsze i szybsze. Sam pasek na jednym wierszu (próba z 05.08)
   wygasił panel: trzy z czterech wierszy stały martwe i całość wyglądała
   na zawieszoną, zamiast na pracującą. */
.audyt.is-ladowanie .audyt__silnik-tor { position: relative; overflow: hidden; }
.audyt.is-ladowanie .audyt__silnik-tor::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .10), transparent);
  animation: audyt-przesuw 1.9s ease-in-out infinite;
}
.audyt.is-ladowanie .audyt__silnik.is-teraz .audyt__silnik-tor::after {
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .32), transparent);
  animation-duration: 1.15s;
}
.audyt__ring-luk, .audyt__ring-para { transition: opacity .5s ease; }
/* 🔴 0,22 s, nie 0,5 s. Przy pół sekundy podpis „Sprawdzam Gemini…" pokazywał
   się, gdy podświetlony był JESZCZE ChatGPT — oko widziało rozjazd między
   napisem a wierszem. Zmierzone na zrzucie z 05.08. */
.audyt__silnik { transition: opacity .22s ease; }
.audyt__ring-halo { transition: opacity .6s ease; }

/* OSIADANIE WYNIKU — jedna klasa, trzy drobne domknięcia w tej samej sekundzie,
   w której licznik dobija do 39: łuna bierze wdech, liczba w kółku „klika"
   na miejsce, a zdanie obok dostaje ten sam mikroruch. To jest ten moment,
   po którym widz wie, że narzędzie skończyło liczyć. */
.audyt.is-osadza .audyt__ring-halo { animation: audyt-wdech 1.05s cubic-bezier(.3, .8, .3, 1) both; }
.audyt.is-osadza .audyt__ring-para { animation: audyt-klik 620ms cubic-bezier(.32, 1.15, .5, 1) both; }
.audyt.is-osadza .audyt__zdanie-wynik b { animation: audyt-klik 620ms cubic-bezier(.32, 1.15, .5, 1) both; }

@keyframes audyt-obrot { to { transform: rotate(360deg); } }
@keyframes audyt-tetno { 0%, 100% { opacity: .3; transform: translateY(0); } 50% { opacity: 1; transform: translateY(-3px); } }
@keyframes audyt-przesuw { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } }
/* Wdech wraca teraz do 0,80, nie do 0,55 — patrz notatka przy .audyt__ring-luk.
   Gdyby wracał do starej wartości, poświata gasłaby po animacji i uwaga Karola
   („i zostaje") nie byłaby spełniona. */
@keyframes audyt-wdech { 0% { opacity: .8; transform: scale(1); } 34% { opacity: 1; transform: scale(1.16); } 100% { opacity: .8; transform: scale(1); } }
@keyframes audyt-klik { 0% { transform: scale(1); } 36% { transform: scale(1.055); } 100% { transform: scale(1); } }

@media (prefers-reduced-motion: reduce) {
  .audyt__silnik-slup { transition: none; }
}

/* ==========================================================================
   Slider (specjalizacje, case study)
   ========================================================================== */
.spec-group { margin-top: 40px; }
.slider { position: relative; margin-top: 24px; }
.slider__track { display: flex; gap: 24px; }
.slider__dots { display: none; justify-content: center; gap: 8px; margin-top: 20px; }
.slider__dots span { width: 8px; height: 8px; border-radius: 50%; background: var(--border-idle-dot); cursor: pointer; transition: background .2s; }
.slider__dots span.active { background: var(--accent-default); }

/* ── KAFEL USŁUGI WEDŁUG PROJEKTU GRAFIKA (17.08.2026) ────────────────────
   Źródło: Zeplin, projekt „AW page", ekran „K3 — ŚWIATŁO STRONY + płaski
   kafelek", 1710 × 1568 px. Karol: „ma to wyglądać tak jak w projekcie:
   układ tekstu, tytuły, rozmieszczenie ikon, kolory, gradienty".

   Kafel w projekcie ma 417 px. Nasz przy 1440 px ma 416,7 px — projekt kładzie
   się na nasz układ jeden do jednego, bez przeliczania skali. Typografia była
   zgodna już wcześniej (36 px / 600 / −0,02em na tytule, 16 px / 1.6 / biel 80%
   na opisie) — projekt niczego w niej nie zmienia.

   🔴 CO ZASTĄPIŁO CO. Do 17.08 kafel był SZARĄ PŁYTĄ (przejście #2a2a33 →
   #131318, przebudowa z 05.08 po uwadze „belki zbyt transparentne i bezpieczne").
   Projekt odwraca ten kierunek: płyta prawie znika (#0b0b0e na stronie #0a0a0c),
   a kafel budują trzy rzeczy — dwie ramki, poświata i znak usługi.

   1. RAMKI. Dwie po 1 px do środka: biała 15% na całym obwodzie plus czerwona,
      która gaśnie ku prawej górze. Przeglądarka nie umie dwóch ramek naraz,
      więc obie są namalowane tłem: warstwa `padding-box` zakrywa wnętrze kafla,
      a dwie warstwy `border-box` zostają widoczne wyłącznie w pasku ramki.
      Kąt czerwonej ramki ZMIERZONY Z PIKSELI podglądu, bo Zeplin kąta przejścia
      nie podaje: pełna czerwień w lewym dolnym rogu, zero przy 55,6% biegu pod
      kątem 35,6°. Sprawdzone na 15 punktach obu krawędzi — różnica wobec
      projektu nie przekracza 0,02 udziału czerwieni.
      ⚠️ Czwarta warstwa (kolor kafla pod ramkami) NIE jest ozdobą. Bez niej biel
      15% jest półprzezroczysta i pokazuje to, co stoi ZA kaflem — czyli światło
      sekcji. Zmierzone: prawe kafle dostawały wtedy czerwony obrys, którego
      w projekcie nie ma. Ta warstwa robi to samo co „ramka do środka" w Figmie:
      kładzie ramkę na kolorze kafla, a nie na tle strony.

   2. POŚWIATA (w projekcie warstwa `bg / corner glow`). Jej środek to DOKŁADNIE
      środek znaku — wyszło z dopasowania profilu do podglądu: 0,283 udziału
      czerwieni w środku, 0,072 przy 42% promienia, zero przy 195 px. Dlatego
      liczy się ją od znaku, a nie wpisuje sztywnymi liczbami: jedna zmiana
      `--znak` przesuwa i skaluje poświatę razem z nim.

   3. ZNAK USŁUGI, 140 × 140 px, 28 px od prawej i od dołu. Dolna krawędź znaku
      stoi w jednej linii z dolną krawędzią strzałki — w projekcie obie kończą
      się na y = 312. Rysunki siedzą w `src/lib/znaki-uslug.js`.

   ⚠️ WYSOKOŚĆ: projekt daje sztywne 340 px, tu stoi `min-height`. Policzone dla
   najdłuższego kafla (tytuł w dwóch wierszach 94 px + opis w pięciu 128 px):
   33 + 94 + 16 + 128 + 24 + 28 = 323 px, czyli mieści się w 340. `min-height`
   daje tę samą wysokość co projekt, a przy dłuższym opisie kafel urośnie,
   zamiast uciąć zdanie w połowie.

   ⛔ ZIARNO ZOSTAJE (`.offer-card::before`). To wybór Karola z 05.08 („to są
   grafiki, a nie tła" — odrzucił wtedy cztery inne warianty), a projekt grafika
   faktury nie przewiduje. Cudzej decyzji nie kasujemy przy okazji innej roboty. */
.offer-card {
  /* Znak ma 140 px przy kaflu 417 px, czyli 33,6% jego szerokości. Od 1440 px
     w górę kafel ma dokładnie tyle co w projekcie, więc znak stoi na 140 px.
     Niżej kafel się zwęża (zmierzone: 387 px przy ekranie 1280, 327 px przy 1100)
     i sztywne 140 px zjadłoby kolumnę opisu — zostałoby na nią 109 px zamiast 190.
     Dlatego znak schodzi razem z ekranem, trzymając tę samą proporcję.

     ⚠️ WSZYSTKIE TE LICZBY MUSZĄ BYĆ DŁUGOŚCIAMI, NIE PROCENTAMI. Sekcja jest
     polem elastycznym (`display:flex` od 1025 px), a `.container` ma marginesy
     `auto`, więc szerokość kafli bierze się z ich własnej treści. Procent
     w `max-width` opisu jest wtedy dla przeglądarki wartością nieokreśloną,
     opis liczy sobie szerokość jak dla jednego wiersza i cała sekcja puchnie.
     Zmierzone na żywej wersji testowej: kafel 416,7 → 440 px, czyli 5,6% za
     szeroko wobec projektu. Stąd `--kafel-szer` liczone z szerokości okna. */
  --kafel-szer: min(417px, 29vw);          /* zmierzona szerokość kafla: 29% okna, nie więcej niż 417 px */
  --znak: min(140px, calc(var(--kafel-szer) * 0.336));
  --poswiata: calc(var(--znak) * 1.393);   /* 195 px przy znaku 140 px */
  --znak-odstep: calc(var(--znak) * 0.2);  /* 28 px przy znaku 140 px */
  --ramka: rgba(255, 255, 255, .15);
  position: relative;
  overflow: hidden;
  flex: 1 1 0;
  min-width: 0;
  background:
    linear-gradient(#0b0b0e, #0b0b0e) padding-box,
    linear-gradient(35.6deg, #ff2c2c 10.7%, rgba(255, 44, 44, 0) 55.6%) border-box,
    linear-gradient(var(--ramka), var(--ramka)) border-box,
    linear-gradient(#0b0b0e, #0b0b0e) border-box;
  border: 1px solid transparent;
  border-radius: 24px;
  padding: 33px 33px 28px;
  min-height: 340px;
  display: flex;
  flex-direction: column;
  transition: transform .28s cubic-bezier(.22, 1, .36, 1), background .28s ease;
}
/* ── FAKTURA PŁYTY: ZIARNO (Karol wybrał wariant A, 05.08.2026 nocą) ──────
   Runda 1 (arkady, sygnet, rowki, światło) odpadła w całości: „to są grafiki,
   a nie tła". I to była trafna uwaga — faktura nie jest motywem, tylko
   materiałem. Ziarno ma ok. 1 px i rysuje je sama przeglądarka (szum w pliku
   SVG wpisanym w arkusz) — zero plików graficznych do pobrania.
   `mix-blend-mode: overlay` sprawia, że warstwa NIE zabiela płyty, tylko
   miejscami ją rozjaśnia i przyciemnia; `isolation: isolate` zamyka to
   mieszanie w obrębie kafla, żeby nie sięgało tła strony.
   ⚠️ 17.08: na nowym, prawie czarnym tle ziarno zmienia jasność o ±4–5 poziomów
   na 255 zamiast dawnych ±7. Widać je słabiej niż na szarej płycie. */
.offer-card { isolation: isolate; }
.offer-card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  mix-blend-mode: overlay;
  opacity: .42;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='z'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='discrete' tableValues='1'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23z)'/%3E%3C/svg%3E");
}
/* Poświata pod znakiem. Kwadrat o boku dwóch promieni, przesunięty tak, żeby
   jego środek wypadł w środku znaku; nadmiar ucina `overflow: hidden` kafla. */
.offer-card::after {
  content: "";
  position: absolute;
  z-index: 0;
  pointer-events: none;
  right: calc(var(--znak-odstep) + var(--znak) / 2 - var(--poswiata));
  bottom: calc(var(--znak-odstep) + var(--znak) / 2 - var(--poswiata));
  width: calc(var(--poswiata) * 2);
  height: calc(var(--poswiata) * 2);
  background: radial-gradient(circle closest-side,
    rgba(255, 44, 44, .30) 0%, rgba(255, 44, 44, .07) 42%, rgba(255, 44, 44, 0) 100%);
}
/* Treść MUSI mieć własną warstwę. Warstwy z ziarnem i poświatą są
   pozycjonowane, więc bez tego malują się NAD nagłówkiem i opisem. */
.offer-card h4, .offer-card p, .offer-card__cta, .offer-card__znak { position: relative; z-index: 1; }
.offer-card:hover { transform: translateY(-4px); --ramka: var(--border-accent); }
.offer-card h4 { font-size: var(--type-size-h2-sm); font-weight: 600; color: var(--text-primary); letter-spacing: -0.02em; }
/* Opis ma się kończyć przed znakiem, nie wchodzić pod niego. W projekcie kolumna
   opisu ma 190 px przy kaflu 417 px; przy węższym kaflu zwęża się tak, żeby
   zostało 12 px światła między tekstem a znakiem. */
.offer-card p {
  margin-top: 16px;
  font-size: var(--type-size-md);
  line-height: 1.6;
  color: var(--text-body);
  max-width: min(190px, calc(var(--kafel-szer) - 66px - var(--znak) - 12px));
}
.offer-card__cta { margin-top: auto; width: 24px; height: 24px; color: var(--accent-default); }
.offer-card__cta svg { width: 100%; height: 100%; }
.offer-card__znak { position: absolute; right: var(--znak-odstep); bottom: var(--znak-odstep); width: var(--znak); height: var(--znak); pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
  .offer-card { transition: none; }
}

.case-card { flex: 1 1 0; min-width: 0; border-radius: 24px; overflow: hidden; background: var(--surface-raised); border: 1px solid var(--border-card); }
.case-card__img { aspect-ratio: 16/11; background: linear-gradient(135deg, var(--color-neutral-800), var(--color-maroon-950)); position: relative; }
.case-card__img::after { content: "Case study"; position: absolute; inset: 0; display: grid; place-items: center; color: var(--text-muted); font-size: 13px; letter-spacing: .1em; text-transform: uppercase; }
.case-card p { padding: 32px; font-size: var(--type-size-body); line-height: 1.6; color: var(--text-body); }

/* ==========================================================================
   Stats
   ========================================================================== */
.stats { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; align-items: center; }
.stats__visual { display: flex; flex-direction: column; gap: 24px; }
.perf-card { position: relative; border-radius: 24px; border: 1px solid var(--border-default); background: linear-gradient(180deg, var(--surface-raised), var(--surface-sunken)); padding: 21px; overflow: hidden; }
.perf-card__top { display: flex; justify-content: space-between; align-items: baseline; }
.perf-card__brand { font-weight: 600; font-size: 16px; color: var(--text-primary); }
.perf-card__tag { font-size: 11px; letter-spacing: .04em; color: var(--text-muted); }
.perf-card__chart { display: block; width: 100%; height: 260px; margin-top: 8px; }
.perf-card__dot { position: absolute; right: 24px; top: 56px; width: 14px; height: 14px; border-radius: 50%; background: #fff; box-shadow: 0 0 0 4px rgba(255,44,44,.4); }
.stats__numbers { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.stat { background: var(--surface-overlay); border: 1px solid var(--border-subtle); border-radius: 16px; padding: 24px; }
.stat strong { display: block; font-size: 36px; font-weight: 700; color: var(--accent-default); letter-spacing: -0.02em; }
.stat span { font-size: 14px; color: var(--text-muted); }

/* ==========================================================================
   Testimonials
   ========================================================================== */
.testimonial { position: relative; max-width: 880px; margin: 52px auto 0; }
.testimonial__card { background: linear-gradient(180deg, var(--surface-raised), var(--surface-sunken)); border: 1px solid var(--border-card); border-radius: 24px; padding: 52px; }
.testimonial__logo { font-weight: 800; letter-spacing: .04em; color: var(--text-primary); font-size: 22px; }
.testimonial__quote { margin-top: 28px; font-size: var(--type-size-body); line-height: 1.7; color: var(--text-body); }
.testimonial__author { margin-top: 24px; text-align: right; font-weight: 600; color: var(--text-primary); }
.testimonial__arrow { position: absolute; top: 50%; transform: translateY(-50%); width: 44px; height: 44px; font-size: 28px; line-height: 1; color: var(--text-muted); display: grid; place-items: center; }
.testimonial__arrow:hover { color: var(--text-accent); }
.testimonial__arrow--prev { left: -56px; }
.testimonial__arrow--next { right: -56px; }
.testimonial__dots { display: flex; margin-top: 28px; }

/* ==========================================================================
   Goals
   ========================================================================== */
.goals { display: grid; grid-template-columns: 1fr 1.1fr; gap: 48px; align-items: center; }
.goals__text .btn { margin-top: 28px; }
/* 18.08.2026 — przycisk sekcji „Dedykowany opiekun" wyszedl z kolumny z tekstem
   i stoi teraz jako osobny wiersz siatki. Uwaga Karola dotyczyla TELEFONU: tam ma
   byc pod zdjeciami i bez poswiaty. Na komputerze uklad zostaje taki jak byl —
   dlatego ponizej jawne rozstawienie na siatce dla ekranow od 1025 px. */
.goals__cta { grid-column: 1 / -1; margin-top: 32px; }
@media (min-width: 1025px) {
  /* row-gap: 0 — odstep 48px z siatki dotyczylby takze przerwy miedzy tekstem
     a przyciskiem, a ten ma stac dokladnie tam, gdzie stal (pomiar: 4007 px). */
  .goals { grid-template-rows: min-content min-content; align-content: center; align-items: start; row-gap: 0; }
  .goals__text { grid-column: 1; grid-row: 1; }
  .goals__cta { grid-column: 1; grid-row: 2; margin-top: 28px; }
  .goals__visual { grid-column: 2; grid-row: 1 / span 2; align-self: center; }
  /* poswiata na komputerze zostaje — uwaga dotyczyla telefonu */
  .goals__cta .btn--bez-poswiaty { box-shadow: 0 14px 17px var(--accent-glow); }
  .goals__cta .btn--bez-poswiaty:hover { box-shadow: 0 16px 28px 2px rgba(255, 44, 44, 0.85); }
}
/* Telefon i tablet: bez poswiaty. Kolejnosc w kodzie strony (tekst, zdjecia, przycisk)
   sprawia, ze przy jednej kolumnie przycisk laduje pod zdjeciami — o to chodzilo. */
.btn--bez-poswiaty, .btn--bez-poswiaty:hover { box-shadow: none; }
.goals__text .section-text { margin-top: 24px; }
/* Wizual: 3 zdjęcia z biura w geometrii brandu (liść / panel / pigułka),
   osobne kształty z odstępem i wyraźną ramką — bez nakładania (Karol 11.07).
   Kadr główny: gest "pokazuje na laptopie" musi być widoczny w całości. */
.goals__visual { display: grid; grid-template-columns: 1.5fr 1fr; grid-template-rows: 1fr auto; gap: 14px; }
.goals__visual > div {
  overflow: hidden; position: relative;
  border: 1px solid var(--border-strong);
  box-shadow: 0 26px 54px -26px rgba(0, 0, 0, 0.85);
}
.goals__visual img { width: 100%; height: 100%; object-fit: cover; filter: saturate(0.94) contrast(1.06) brightness(0.87); }
.goals__photo { grid-row: 1 / 3; border-radius: 140px 24px 140px 24px; height: 520px; }
.goals__photo img { object-position: 58% 28%; }
.goals__photo::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(90% 70% at 85% 100%, rgba(255, 44, 44, 0.22) 0%, transparent 62%);
  mix-blend-mode: screen;
}
.goals__mini { border-radius: 28px 28px 28px 96px; }
.goals__mini img { object-position: 78% 26%; }
.goals__pill { border-radius: 999px; height: 128px; }
.goals__pill img { object-position: center 40%; }
.bars { position: relative; height: 360px; padding-top: 40px; }
.bars__row { display: flex; align-items: flex-end; justify-content: space-between; gap: 14px; height: 100%; }
/* bars "heat up" toward the peak — rising red intensity conveys growth */
.bar {
  position: relative; flex: 1; height: var(--h); border-radius: 10px 10px 3px 3px;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--accent-default) calc(var(--i, 1) * 13%), transparent),
    color-mix(in srgb, var(--accent-default) calc(var(--i, 1) * 3%), transparent));
  border: 1px solid rgba(255, 44, 44, calc(var(--i, 1) * 0.05));
  transition: filter .25s ease;
}
.bar--peak { background: linear-gradient(180deg, var(--accent-default), rgba(255,44,44,.22)); border-color: var(--border-accent); box-shadow: 0 0 48px rgba(255,44,44,.5); }
/* target marker crowning the peak bar (goal reached) */
.bar__dot { position: absolute; top: -28px; left: 50%; transform: translateX(-50%); width: 16px; height: 16px; border-radius: 50%; background: #fff; box-shadow: 0 0 0 4px rgba(255,44,44,.35); }
.bar__dot::before { content: ""; position: absolute; top: 16px; left: 50%; transform: translateX(-50%); width: 1px; height: 12px; background: linear-gradient(180deg, rgba(255,255,255,.5), rgba(255,255,255,0)); }
.bar__label { position: absolute; top: -54px; left: 50%; transform: translateX(-50%); font-size: 12px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--text-primary); white-space: nowrap; }

/* ==========================================================================
   Press logos + Social
   ========================================================================== */
.section--press { padding: 56px 0; }
.press { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 24px 40px; }
.press li { color: var(--text-muted); font-weight: 700; letter-spacing: .08em; font-size: 18px; text-transform: uppercase; opacity: .8; }
.press li small { display: block; font-size: 9px; letter-spacing: .2em; }
.press__aw { display: flex; align-items: center; gap: 8px; text-transform: none; letter-spacing: 0; color: var(--text-primary); }
.press__aw .brand-mark { font-size: 22px; }

.section--social { padding: 32px 0; }
.social { display: flex; justify-content: center; gap: 28px; }
.social a { width: 40px; height: 40px; display: grid; place-items: center; color: var(--text-primary); transition: color .2s; }
.social a:hover { color: var(--text-accent); }
.social svg { width: 28px; height: 28px; }

/* ==========================================================================
   Footer
   ========================================================================== */
.footer { padding: 64px 0 80px; }
.footer__grid { display: grid; grid-template-columns: 1.4fr 1fr 1fr 1fr; gap: 32px; }
/* nagłówki stopki jako <p> — h4 po h2 psuł hierarchię nagłówków (SEO/dostępność) */
/* 30.07.2026: stopka na wspolnej skali. Naglowek kolumny ZAWIERA odnosniki,
   wiec jest od nich wiekszy — wstep 21 px nad etykieta 15 px. */
.footer__col h4, .footer__col .footer__head { font-size: var(--skala-wstep); font-weight: 700; color: var(--text-primary); margin-bottom: 16px; }
.footer__col ul { display: flex; flex-direction: column; gap: 12px; }
.footer__col a { color: var(--text-body); font-size: var(--skala-etykieta); }
.footer__col a:hover { color: var(--text-accent); }
.footer__addr { margin-top: 24px; font-style: normal; line-height: 1.9; color: var(--text-body); font-size: 15px; }
.footer__email { margin-top: 20px; }
.footer__email a { color: var(--text-primary); }

/* ==========================================================================
   Responsive — section overrides
   ========================================================================== */
@media (max-width: 1024px) {
  .section { padding: 56px 0; }
  .container { padding-inline: var(--page-pad-tablet); }

  /* stack two-column layouts */
  .ai__grid, .stats, .goals { grid-template-columns: 1fr; gap: 32px; }
  .goals__photo { height: 420px; }
  /* Telefon/tablet (uwaga Karola 10.07): kolejność sekcji AI = nagłówek →
     grafika z wynikiem (animacja) → tekst z formularzem */
  .ai__grid .chat, .ai__grid .audyt { order: 1; }
  .ai__grid .ai__form { order: 2; }
  .stats__text { order: -1; }
  .ai__form-controls { align-items: stretch; }
  .ai__form-controls .btn { align-self: flex-start; }

  /* chat providers inline */
  .chat { margin-left: 0; }
  .chat__providers { position: static; flex-direction: row; justify-content: flex-end; margin-top: 16px; }
  .chat__providers-label { display: none; }

  /* Poniżej 1024 px panel nie stoi już obok formularza, tylko dostaje CAŁĄ
     szerokość pojemnika (~968 px na tablecie). Podział na pół dalej ma sens
     i zostaje — kolumny robią się szersze, nie węższe. Kółko może przy tym
     urosnąć, ale zostawiamy 248 px: większe zjadałoby wysokość ekranu telefonu. */
  .audyt { margin-left: 0; padding: 20px 18px 18px; gap: 22px; }
  .audyt__silnik { grid-template-columns: 20px minmax(58px, auto) 1fr auto; column-gap: 10px; }

  /* sliders: exactly one card per viewport, no neighbour peeking */
  .slider__track {
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    overscroll-behavior-x: contain;
    scroll-padding: 0;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding-bottom: 4px;
  }
  .slider__track::-webkit-scrollbar { display: none; }
  .offer-card, .case-card { flex: 0 0 100%; scroll-snap-align: center; scroll-snap-stop: always; }
  /* Poniżej 1024 px kafle są przewijane palcem i jeden zajmuje całą szerokość
     (przy 1024 px ma 945 px). Liczenie znaku z szerokości okna przestaje tu mieć
     sens — wracamy do wielkości z projektu. Tu kafel ma szerokość określoną
     (100% toru przewijania), więc procent w opisie liczy się już normalnie. */
  .offer-card { --znak: 140px; }
  .offer-card p { max-width: min(190px, calc(100% - var(--znak) - 12px)); }
  .slider__dots { display: flex; }

  /* testimonial */
  .testimonial__arrow { display: none; }
  .testimonial__card { padding: 36px 28px; }

  /* footer 2 columns */
  .footer__grid { grid-template-columns: 1fr 1fr; gap: 32px 24px; }

  /* press wrap centered */
  .press { justify-content: center; gap: 28px 36px; }
}

@media (max-width: 640px) {
  .section { padding: 44px 0; }
  .container { padding-inline: var(--page-pad-mobile); }

  .stats__numbers { gap: 8px; }
  .stat { padding: 16px; }
  .stat strong { font-size: 26px; }

  .bars { height: 240px; }
  .goals__visual { gap: 10px; }
  .goals__photo { height: 330px; border-radius: 80px 18px 80px 18px; }
  .goals__mini { border-radius: 20px 20px 20px 56px; }
  .goals__pill { height: 92px; }
  .testimonial__card { padding: 24px 18px; }

  /* footer single column */
  .footer__grid { grid-template-columns: 1fr; gap: 28px; }
  .footer { padding: 48px 0 56px; }

  /* press 3 per row */
  .press li { font-size: 14px; }

  /* 🔴 Na telefonie dwie kolumny się nie mieszczą: przy 390 px ekranu na kolumnę
     zostaje ~150 px, czyli kółko wielkości znaczka i pasek modelu długości
     dwóch liter. Panel wraca do pionu: kółko nad danymi, oba na całą szerokość. */
  .audyt { grid-template-columns: 1fr; gap: 20px; }
  .audyt__ring { width: 176px; height: 176px; }
  .audyt__ring-para b { font-size: 55px; }
  .audyt__ring-para i { font-size: 26px; }
  /* Zdanie zostaje największym napisem w panelu, ale na wąskim ekranie 22 px
     rozbijało je na cztery linijki — stąd 19 px. */
  .audyt__zdanie-wynik { font-size: 19px; }
}

/* ===== Specjalizacje: trzy osobne płyty, nie jedna ramka z kreskami =====
   Do 05.08 trzy kafle siedziały wewnątrz JEDNEJ ramki (border-radius 24 na
   pojemniku, włosowe kreski między komórkami, tło przezroczyste). Karol zdjął
   to 05.08: taki układ czyta się jak pusta belka z liniami, a nie jak trzy
   propozycje. Teraz strukturę niesie odstęp między płytami. */
.spec-track { gap: 20px; }
@media (max-width: 1024px) {
  .spec-track { gap: 20px; overflow-x: auto; overflow-y: visible; }
}

/* ===== Case study tag + footer red headings (match design) ===== */
.case-card__img { position: relative; }
.case-card__tag {
  position: absolute; top: 16px; left: 16px;
  background: var(--inverse-surface); color: var(--text-on-inverse);
  font-size: 13px; font-weight: 700; padding: 7px 14px; border-radius: 30px;
}
.footer__col h4, .footer__col .footer__head { color: var(--text-accent); }
.footer__company { color: var(--text-accent); font-weight: 700; }

/* ===== Real icons: AI providers + social ===== */
.provider img { width: 22px; height: 22px; display: block; opacity: .85; }
.social a img { width: 28px; height: 28px; display: block; }
.social a:hover img { opacity: .8; }

/* ===== Input hover/focus: red border ===== */
.field:hover, .field:focus { border-color: var(--border-accent); outline: none; }

/* ===== Raster asset slots (avatar / case photos / client logo) ===== */
.avatar { object-fit: cover; }
.case-card__img { background-size: cover; background-position: center; }
.case-card__img::after { content: none; }
.testimonial__logo { display: inline-block; background: #fff; border-radius: 40px; padding: 12px 22px; }
.testimonial__logo img { display: block; height: 38px; width: auto; }

/* testimonial chevron arrows */
.testimonial__arrow svg { width: 13px; height: 22px; display: block; }

/* Prime Avenue Group tree (footer press) */
.press-tree { width: 100%; max-width: 1568px; height: auto; display: block; margin: 0 auto; }

/* --- PRIME AVENUE GROUP NA TELEFONIE (Karol, 11.08) ------------------------
   Na komputerze i tablecie nie ma jej wcale — tam zostaje płaski rysunek
   `pag.svg` bez jednej zmiany. Przełączenie jest w regule telefonowej niżej.
   ⚠️ `display: none` na tym pojemniku sprawia, że przeglądarka NIE POBIERA
   sześciu plików logotypów na komputerze — i odwrotnie, na telefonie nie pobiera
   85-kilobajtowego `pag.svg`. Żadna z wersji nie dokłada drugiej do rachunku. */
.pag { display: none; }

/* ══ DRZEWKO PRIME AVENUE — ODTWORZONE 1:1 ZE STAREJ STRONY, WERSJA NA TELEFON ══
   🔴 TRZECIE PODEJŚCIE. Dwa poprzednie były błędne i oba z mojej winy:
     1. siatka dwa na trzy BEZ łącznika — mój własny wymysł zamiast wskazania,
     2. drzewko poziome w jednym rzędzie — wykonanie wskazania dosłownie, ale
        wizualnie identyczne z tym, co już było, więc Karol słusznie napisał
        „wygląda dokładnie jak wcześniej".

   ŹRÓDŁO BŁĘDU NR 1, opisane, żeby nikt tego nie powtórzył: zmierzyłem na starej
   stronie element `.connector-line` i dostałem `display: none` z rozmiarem zero.
   Na tej podstawie napisałem, że „stara strona na telefonie chowa łącznik" —
   i go usunąłem. To był ZŁY ELEMENT. Stara strona faktycznie chowa
   `.connector-line` na telefonie, ale rysuje łącznik od nowa, dwoma warstwami
   doklejonymi do siatki logotypów (`.bottom-logos::before` i `::after`).
   Zrzut ekranu od Karola pokazał kreski, których mój pomiar nie widział.
   ⚠️ Wniosek: brak elementu w pomiarze nie znaczy, że efektu nie ma na ekranie.

   UKŁAD ZDJĘTY Z `agencjawroclawska.pl` przy 390 px:
     dwie kolumny po ~126 px, odstęp poziomy 70 px, pionowy 38 px
     pionowa oś 2 px dokładnie w połowie siatki, na całą jej wysokość
     trzy poziome odnogi po 30 px, wyśrodkowane na osi, na środku każdego rzędu
     wysokości logotypów: BRANDLIVE 13, VIRALMOOD 33, NETWORTH 39,
     CODERS PILOT 25, propseo 45, Agencja Wrocławska 22 px

   ⚠️ Odnogi rysujemy jako element doklejony do KAŻDEGO logotypu z lewej kolumny,
   a nie gradientem na sztywnych pikselach, jak robi to stara strona. Powód:
   gradient tam stoi na wartościach `50% 155px, 50% 235px, 50% 100%` policzonych
   pod DOKŁADNIE tamtą wysokość siatki. Każda zmiana wysokości logotypu rozjeżdża
   kreski. U nas odnoga siedzi na środku swojego rzędu i przelicza się sama. */
.pag__marki {
  position: relative;
  list-style: none;
  margin: 30px auto 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 38px 70px;
  align-items: center;
}
.pag__marki li { display: flex; align-items: center; justify-content: center; }

/* Pionowa oś — 2 px w połowie siatki, na całą jej wysokość */
.pag__marki::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 2px;
  transform: translateX(-1px);
  background: rgba(255, 255, 255, .85);
}
/* Odnoga w prawo z każdego logotypu w lewej kolumnie. Przerwa między kolumnami
   ma 70 px, odnoga 30 px — po 20 px światła z każdej strony, więc kreska jest
   dokładnie wyśrodkowana na osi. */
.pag__marki li:nth-child(odd) { position: relative; }
.pag__marki li:nth-child(odd)::after {
  content: "";
  position: absolute;
  top: 50%;
  left: calc(100% + 20px);
  width: 30px;
  height: 2px;
  transform: translateY(-1px);
  background: rgba(255, 255, 255, .85);
}

/* Wysokości ze starej strony przeskalowane o 1,11 — nasza kolumna ma 140 px
   zamiast 126 px, więc logotypy mogą być o tyle większe bez zmiany proporcji. */
.pag__marki img { width: auto; max-width: 100%; }
.pag__marki img[src*="brandlive"] { height: 14px; }
.pag__marki img[src*="viralmood"] { height: 37px; }
.pag__marki img[src*="networth"] { height: 43px; }
.pag__marki img[src*="coders-pilot"] { height: 28px; }
.pag__marki img[src*="propseo"] { height: 50px; }
.pag__marki img[src*="agencja-wroclawska-logo"] { height: 24px; }

@media (max-width: 640px) {
  /* przełączenie wersji: płaski rysunek znika, złożona wersja wchodzi */
  .press-tree { display: none; }
  .pag { display: block; text-align: center; }

  /* ⚠️ Zapasowa wysokość sekcji (--cis) MUSI urosnąć razem z zawartością.
     Sekcja ma `content-visibility: auto`, czyli przeglądarka nie rysuje jej,
     dopóki nie zbliży się do kadru, a do tego czasu rezerwuje miejsce z tej
     liczby. Stara wartość 320 px była policzona pod pasek 52 px; nowa wersja
     ma ok. 330 px, więc bez tej poprawki pasek przewijania podskakiwałby
     w chwili, gdy sekcja wchodzi w kadr. */
  .section--press { --cis: 480px; }

  /* Znak grupy — 62% szerokości pojemnika, tyle co na starej stronie na telefonie
     (357 px z 390 px ekranu przy naszym pojemniku 350 px). */
  .pag__grupa { width: 62%; height: auto; margin: 0 auto; display: block; }
}

/* Footer columns: align to top edge */
.footer__grid { align-items: start; }

/* ==========================================================================
   Review round — alignment to macket
   ========================================================================== */

/* Hero: full viewport height, text centred; navbar overlays top */
.hero { min-height: 100vh; margin-top: -102px; }

/* Case study cards — correct color (gradient + white border) */
.case-card { background: linear-gradient(180deg, var(--surface-raised), var(--surface-sunken)); border: 1px solid var(--border-default); border-radius: 26px; }
/* Case study: 3-across on desktop; one-card slider for the whole tablet+mobile
   range (≤1024) — avoids the cramped 3-column layout on narrow screens. */

/* Stats — reconstructed "window" */
.stats__visual { display: block; }
.perf { position: relative; background: linear-gradient(180deg, var(--surface-raised), var(--surface-sunken)); border: 1px solid var(--border-default); border-radius: 24px; padding: 22px 22px 22px; }
.perf__bar { display: flex; align-items: center; justify-content: space-between; padding-bottom: 14px; border-bottom: 1px solid var(--border-subtle); }
.perf__dots { display: flex; gap: 7px; }
.perf__dot { width: 10px; height: 10px; border-radius: 50%; background: var(--border-idle-dot); }
.perf__dot--red { background: var(--accent-default); }
.perf__tag { font-size: 12px; color: var(--text-muted); }
.perf__brand { display: block; margin-top: 16px; font-weight: 600; font-size: 17px; color: var(--text-primary); }
.perf__chart { position: relative; margin-top: 10px; height: 250px; }
.perf__chart svg { display: block; width: 100%; height: 100%; }
.perf__end { position: absolute; top: 11px; right: -7px; width: 14px; height: 14px; border-radius: 50%; background: #fff; box-shadow: 0 0 0 4px rgba(255,44,44,.4); }
.perf__stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-top: 20px; }
.perf-stat { background: linear-gradient(180deg, var(--surface-raised), var(--surface-sunken)); border: 1px solid var(--border-default); border-radius: 12px; padding: 20px; }
.perf-stat strong { display: block; font-size: 34px; font-weight: 700; color: var(--accent-default); letter-spacing: -0.02em; }
.perf-stat span { font-size: 14px; color: var(--text-muted); }

/* Goals — dashed reference grid behind bars */
.bars__grid { position: absolute; left: 0; right: 0; top: 40px; bottom: 0; display: flex; flex-direction: column; justify-content: space-between; pointer-events: none; }
.bars__grid span { display: block; border-top: 1px dashed rgba(255,255,255,0.12); }
.bars__row { position: relative; z-index: 1; }

/* Chat provider — active (animated) state */
.provider { transition: border-color .5s ease, box-shadow .5s ease; }
.provider.is-active { border-color: var(--accent-default); box-shadow: 0 0 16px rgba(255,44,44,.35); }

/* Bottom sections — transparent so the page background shows through */
.section--press, .section--social, .footer { position: relative; background: transparent; }

/* Input — hover / focus (active) states */
.field:hover { border-color: var(--border-strong); background: var(--surface-input); }
.field:focus { border-color: var(--accent-default); background: var(--surface-input); box-shadow: 0 0 0 3px rgba(255,44,44,.15); }

/* Testimonials — working slider */
.testimonial { position: relative; max-width: 880px; margin: 52px auto 0; }
.testimonial__viewport { overflow: hidden; border-radius: 24px; }
.testimonial__track { display: flex; transition: transform .45s ease; }
.testimonial__card { flex: 0 0 100%; }
.testimonial__dots { display: flex; justify-content: center; gap: 8px; margin-top: 28px; }
.testimonial__dots span { width: 8px; height: 8px; border-radius: 50%; background: var(--border-idle-dot); cursor: pointer; transition: background .2s; }
.testimonial__dots span.active { background: var(--accent-default); }

/* Footer columns aligned to top edge */
.footer__grid { align-items: start; }

@media (max-width: 1024px) { .hero { margin-top: -80px; } }

/* ==========================================================================
   Hero intro animation (storyboard frames 1–8)
   ========================================================================== */

/* Final resting state: a clean gap between DIGITAL and MARKETING holds the
   rotating slogan (this is also the no-animation / reduced-motion layout). */
.hero__title { --hero-gap: -0.04em; }
/* Ruch linii MARKETING robimy transformem, nie margin-top: margines zmienia
   wysokość bloku, a hero centruje treść w pionie — każda klatka animacji
   marginesu przesuwała też DIGITAL (reflow → zrywane klatki, uwaga Karola
   10.07). Transform nie dotyka układu, więc DIGITAL stoi w miejscu.
   UWAGA (sesja 8): transform/transition żyją TYLKO pod html.intro-play —
   trwały transform na dziecku h1 z background-clip:text wydziela linię do
   osobnej warstwy i Chromium przestaje malować gradientowy tekst
   („MARKETING" znikał po zakończeniu intra w renderach headless). Po
   sprzątnięciu intro-play linia wraca do zwykłego malowania; translateY(0)
   = brak transforma, więc wizualnie nic się nie zmienia. */
.hero__title-line--2 { margin-top: var(--hero-gap); }
/* Rotator haseł: wyjście do góry z rozmyciem, wejście od dołu z rozmycia do
   ostrości (uwaga Karola 10.07: „teraz wskakuje", ma się płynnie pojawiać) */
.hero__accent-text {
  display: inline-block;
  position: relative;   /* razem z z-index niżej trzyma tekst NAD warstwą cienia */
  z-index: 1;
  transition: opacity .55s cubic-bezier(.22,.61,.36,1), transform .55s cubic-bezier(.22,.61,.36,1), filter .55s ease;
  will-change: opacity, transform, filter;
}
/* Podmiana hasła: wyjście do góry z rozmyciem, wejście od dołu z rozmycia do
   ostrości. TEN RUCH JEST NIETKNIĘTY OD 10.07 — i to jest ta wersja, którą
   Karol zatwierdził. 14.08 został zastąpiony przejazdem na klatkach (dwa hasła
   w kadrze naraz, odsłanianie od lewej); 16.08 Karol kazał to cofnąć.
   ⛔ Nie przebudowywać tego ponownie bez wyraźnej prośby. */
.hero__accent-text.is-out { opacity: 0; transform: translateY(-0.35em); filter: blur(8px); }
/* pozycja startowa nowego hasła — ustawiana bez animacji, tuż przed wejściem */
.hero__accent-text.is-enter { transition: none; opacity: 0; transform: translateY(0.4em); filter: blur(8px); }

/* ==========================================================================
   HASŁO WCHODZI PO NAPISIE — NIE RAZEM Z NIM (12.08.2026)
   ==========================================================================
   🔴 Uwaga Karola: „na wejście sam napis DIGITAL MARKETING, bez czerwonych
   haseł. Hasła niech pojawiają się po ok. 2 sekundach, zaczynają od animacji
   wejścia i dopiero potem płynnie się przełączają".

   CZEKANIE SIEDZI TUTAJ, NIE W SKRYPCIE. Gdyby czasem sterował main.js, hasło
   byłoby widoczne przez pierwsze klatki — zanim skrypt zdąży cokolwiek zrobić —
   czyli mignęłoby dokładnie tam, gdzie ma go nie być. Animacja z `both` narzuca
   stan „niewidoczne" od PIERWSZEJ narysowanej klatki, więc migotu nie ma.
   Przy wyłączonym JavaScripcie hasło i tak wejdzie — nie będzie tylko dalszych
   podmian.

   DWIE RÓŻNE LICZBY, BO TO DWA RÓŻNE WEJŚCIA NA STRONĘ:
     • telefon i tablet (do 1024 px) — otwarcia z logotypem NIE MA (decyzja
       Karola z 09.08, wersja telefonowa musi być szybka), napis stoi od pierwszej
       klatki, więc hasło czeka równe 2 s;
     • komputer (od 1025 px) — najpierw leci otwarcie z logotypem, a napis
       „DIGITAL MARKETING" dochodzi do pełnego krycia na 3300 ms (2400 start
       + 900 czasu, patrz „SEKWENCJA WEJŚCIA" niżej). Te same 2 s liczone od
       gotowego napisu dają 5300 ms.
   ⚠️ Granica 1025 px to DOKŁADNIE ta sama, którą sprawdza skrypt w kodzie
   strony przy włączaniu otwarcia (index.astro). Rozjazd tych dwóch liczb =
   hasło wchodzące w środku otwarcia albo 3 sekundy pustego miejsca.

   Ruch jest TEN SAM, co przy każdej kolejnej podmianie hasła (od dołu,
   z rozmycia do ostrości) — przepisany na klatki, bo przejście (`transition`)
   nie umie samo wystartować po zadanym czasie. Warstwa cienia dostaje własne
   klatki: dłuższy czas i dłuższą drogę, czyli ten sam ciężar co przy podmianach.
   ⚠️ Skrypt zdejmuje klasę `is-czeka` po zakończeniu wejścia — animacja z `both`
   trzymałaby krycie na stałe i blokowała podmiany haseł. */
/* 🔴 RUNDA 4 (16.08, Karol): „3 sekundy trochę za długo, ma się wrażenie, że
   napis pojawił się za późno, bo coś się zacięło”.

   PRZERWA SKRÓCONA Z 3000 DO 1500 ms. Granica jest tu wąska i zmierzona po
   obu stronach: przy 3 s pusta scena trwa dłużej niż całe otwarcie strony
   (2,4 s do pokazania napisu) — oko czyta to jako zawieszenie, nie jako pauzę.
   Przy 1,5 s przerwa jest krótsza niż wejście napisu, więc czyta się jako
   część tej samej sekwencji.

   PRZERWA LICZONA OD NIERUCHOMEGO NAPISU, NIE OD WEJŚCIA NA STRONĘ:
     • telefon i tablet (do 1024 px) — otwarcia z logotypem nie ma, napis stoi
       od pierwszej klatki, więc równe 1500 ms;
     • komputer (od 1025 px) — ruch napisu kończy się o 3550 ms (otwieranie
       odstępu między wierszami: `aw-odstep`, start 2400 + 1150 ms czasu).
       3550 + 1500 = 5050 ms.
   Licząc od wejścia na stronę hasło wskoczyłoby w środek otwarcia, na logotyp,
   czyli dokładnie tam, gdzie ma go nie być.
   Poprzednie liczby: 3000 / 6550 ms (runda 3), 1400 / 4600 ms (runda 2 z 12.08
   — te nigdy nie zadziałały, patrz naprawa w main.js). */
:root { --haslo-start: 1500ms; }
@media (min-width: 1025px) { :root { --haslo-start: 5050ms; } }
/* Przy systemowym ograniczeniu ruchu hasło jest od razu — czekanie na animację,
   której nie ma, byłoby samą pustką. */
@media (prefers-reduced-motion: reduce) { :root { --haslo-start: 0ms; } }

/* 🔴 16.08, Karol: „pierwsze hasło zaczyna od animacji takiej, która rozpoczyna
   drugie hasło — czyli animacji wejścia”.

   Klatki niżej są PRZEPISANE JEDEN DO JEDNEGO Z PODMIANY: tekst startuje
   z 0,4em pod miejscem docelowym i z rozmycia 8 px (to samo, co robi
   `.hero__accent-text.is-enter`), cień z 0,62em i powiększenia 1,04
   (`.hero__accent-cien.is-enter`). Czasy też są te same: 550 ms tekst,
   780 ms cień. Zapis klatkami jest konieczny wyłącznie dlatego, że przejście
   (`transition`) nie umie samo wystartować po zadanym czasie czekania.

   ⚠️ ZMIENIASZ RUCH PODMIANY — ZMIEŃ TAK SAMO TE KLATKI. Inaczej pierwsze
   hasło znów zacznie wchodzić inaczej niż wszystkie następne.
   ⛔ To ODWRACA rundę 2 z 12.08 („wejście bez przesuwania w pionie”, wtedy
   pracowało samo krycie i wyostrzenie). Karol obejrzał obie i wybrał ruch. */
/* Te dwie reguły uruchamiają wejście. Klasę „is-czeka" nakłada kod strony,
   a skrypt zdejmuje ją po zakończeniu animacji — bez tego wypełnienie „both"
   trzymałoby stan końcowy na stałe i blokowało podmiany haseł. */
.hero__accent-text.is-czeka {
  animation: aw-haslo-wejscie 550ms cubic-bezier(.22, .61, .36, 1) var(--haslo-start) both;
}
.hero__accent-cien.is-czeka {
  animation: aw-haslo-cien-wejscie 780ms cubic-bezier(.22, .61, .36, 1) var(--haslo-start) both;
}
@keyframes aw-haslo-wejscie {
  from { opacity: 0; transform: translateY(0.4em); filter: blur(8px); }
  to   { opacity: 1; transform: translateY(0);     filter: blur(0); }
}
/* Cień jedzie dłuższą drogę i dłużej niż tekst — stąd jego ciężar. Rozmycie
   zostaje stałe (0,207em z reguły bazowej): przeliczane w każdej klatce było
   najdroższą rzeczą w całym otwarciu na telefonie. */
@keyframes aw-haslo-cien-wejscie {
  from { opacity: 0; transform: translateY(0.62em) scale(1.04); }
  to   { opacity: 1; transform: translateY(0.07em) scale(1); }
}

/* opakowanie odlotu staje się układem odniesienia dla warstwy cienia;
   `inline-block` zostaje z reguły w sekcji „Round 5" (linia ~2300) */
.hero__accent-odlot { position: relative; }

/* Scroll lock while the intro plays.
   UWAGA: bez height:100% — wymuszenie wysokości viewportu na body zmieniało
   punkty odniesienia %-owych radial-gradientów w body::before, przez co po
   odblokowaniu flary tła przeskakiwały na inne pozycje (uwaga Karola 09.07). */
html.intro-lock, html.intro-lock body { overflow: hidden !important; }

/* Intro overlay — hidden unless armed */
.hero__intro { display: none; }
html.intro-play .hero__intro {
  display: flex; position: fixed; inset: 0; z-index: 9999;
  align-items: center; justify-content: center;
  background: var(--bg-page);
}
.hero__intro-logo {
  --h: clamp(40px, 6vw, 72px);
  display: flex; align-items: center; gap: calc(var(--h) * 0.18);
}
.hero__intro-mark { height: var(--h); width: auto; display: block; }
/* Logotyp jest teraz rysunkiem wpisanym w kod strony, nie obrazkiem. Szerokość
   liczymy z proporcji pliku (231 : 62) — sam `width: auto` przy rysunku
   wektorowym bywa liczony z atrybutu, a nie z proporcji, i logotyp wychodziłby
   za szeroki. */
.hero__intro-word { height: var(--h); width: calc(var(--h) * 231 / 62); display: block; overflow: visible; }

/* 🔴 SYGNET PRZYCHODZI Z GŁĘBI, NIE Z BOKU (przebudowa 05.08 wieczorem).
   Do 05.08 sygnet przejeżdżał 1,95 swojej wysokości w POZIOMIE. Strona miała
   przez to trzy różne osie ruchu: sygnet w bok, logotyp w pionie, litery
   otwarcia w głąb. Teraz jest JEDNA oś — w głąb ekranu — ta sama, po której
   później lecą litery „DIGITAL MARKETING" i po której wyłania się sekcja 2.
   Sygnet startuje mały i rozmyty (jakby daleko) i dojeżdża ostry.
   Skutek uboczny, świadomy: sygnet stoi od pierwszej klatki na swoim
   docelowym miejscu — po lewej od logotypu, nie na środku ekranu. To jest
   cena za jedną oś ruchu i jest tego warta. */
.hero__intro-mark { opacity: 0; transform: scale(.36); filter: blur(14px); }
/* Litera logotypu: stan spoczynkowy to stan STARTOWY animacji — daleko i mało.
   Bez klasy .intro-graj cała nakładka i tak jest schowana (display: none),
   więc nikt tego stanu nie zobaczy.
   ⚠️ transform-box: fill-box NIE JEST OZDOBĄ — bez niego litera skalowałaby się
   względem początku układu współrzędnych całego rysunku (lewy górny róg,
   250 jednostek w bok), czyli odlatywałaby na ukos zamiast rosnąć w miejscu. */
.hero__intro-litera {
  opacity: 0;
  transform: scale(var(--w-sk, .5));
  transform-box: fill-box;
  transform-origin: center;
}

/* Stany przed odsłonięciem otwarcia — obowiązują od pierwszej klatki */
html.intro-play .hero__title { opacity: 0; transform: scale(.6); transform-origin: center; }
html.intro-play .hero__title-line--2 { transform: translateY(calc(-1 * var(--hero-gap))); }
html.intro-play .hero__accent { opacity: 0; }
html.intro-play .hero__cta { opacity: 0; }

/* ==========================================================================
   SEKWENCJA WEJŚCIA — JEDNA OŚ CZASU (przebudowa 04.08.2026)
   ==========================================================================
   Skrypt dodaje JEDNĄ klasę (.intro-graj), a każdy element ma własne
   opóźnienie startu wpisane niżej. Do 04.08 sekwencję składało OSIEM odliczeń
   zegarem w main.js. Dwa powody, dla których to nie mogło być czyste:
     • zegar nie jest zsynchronizowany z rysowaniem klatek — każdy z ośmiu
       kroków mógł wypaść w środku klatki (osiem okazji do drgnięcia),
     • odliczanie rusza natychmiast, a przejścia CSS dopiero przy PIERWSZEJ
       narysowanej klatce — przy pierwszym wejściu zmierzyłem 450 ms poślizgu
       (duże zdjęcie + fonty), czyli obie osie czasu się rozjeżdżały.

   KLATKI ZACHODZĄ NA SIEBIE (Karol 04.08: „ważne jest to, aby wszystkie klatki
   się ze sobą łączyły, żeby nie było widać przeskoków"). Poprzedni rozkład miał
   dwa martwe okna — 575–700 ms i 1500–1800 ms. Trzysta milisekund kompletnej
   ciszy w środku wejścia czytało się jako przeskok. Zmierzone co 25 ms.

     element              kiedy       zachodzi na
     sygnet krycie        0– 560      —
     sygnet z głębi       0– 900      wchodzące litery logotypu
     litery logotypu    380–1365      dochodzący sygnet
     odlot logo w głąb 1400–2750      gasnącą nakładkę i wchodzący napis
     nakładka gaśnie   1600–2900      lot logo i wejście napisu
     napis             2400–3550      lot logo i gasnącą nakładkę
     odstęp wierszy    3000–4400      slogan i belkę
     slogan + guzik    3450–4000      dochodzący napis
     belka             2400–3550      napis (te same trzy liczby — patrz niżej)

   🔴 ROZKŁAD PO PRZEBUDOWIE 05.08 WIECZOREM — trzy momenty są NIETYKALNE:
   napis 2400, nakładka 1600–2900 i belka 2400. Belka jedzie dokładnie w tempie
   napisu (ustalenie Karola z 05.08), a nakładka musi zasłaniać ekran, dopóki
   logo pracuje — przy 2400 ms ma jeszcze 21% krycia, więc belka zapala się
   spod niej, a nie na jej tle. Przebudowa wejścia zmieściła się PRZED tymi
   momentami: logo kończy lot na 2500, czyli już pod gasnącą nakładką.
   Zmieniając cokolwiek w wejściu — sprawdź te trzy liczby ponownie.

   CHARAKTER RUCHU: „miękki" — cubic-bezier(.4,.05,.2,1). Wybór Karola 04.08
   z trzech wariantów w public/podglad/warianty-intro-plynnosc/. Poprzednia
   krzywa cubic-bezier(.16,1,.3,1) pokonywała POŁOWĘ drogi w 75 ms, a ostatnie
   5 px pełzło przez pół sekundy — stąd „belka wskakuje, animacja nie ma wagi".
   Zmierzone, ile drogi po 10% czasu: poprzednia 49%, miękka 4%.
   ========================================================================== */
html.intro-graj .hero__intro-mark {
  animation: aw-sygnet-krycie 560ms ease 0ms both,
             aw-sygnet-zglebi 900ms cubic-bezier(.22,.61,.36,1) 0ms both;
}
/* Każda litera dostaje SWOJE dwie liczby (moment startu i długość) wpisane
   w kodzie strony przez wagę policzoną z jej obrysu. Tu jest tylko wspólny
   przepis ruchu — reszta przychodzi z liter. */
html.intro-graj .hero__intro-litera {
  animation: aw-litera-krycie calc(var(--w-cz) * 0.62) ease var(--w-op) both,
             aw-litera-zglebi var(--w-cz) cubic-bezier(.22,.61,.36,1) var(--w-op) both;
}
/* 🔴 WYJŚCIE LOGO TO ODLOT W GŁĄB — LOGO SIĘ ZMNIEJSZA, NIE POWIĘKSZA.
   Uwaga Karola z 05.08 wieczorem: „na wyjściu niech się nie powiększa, tylko
   zmniejsza — teraz nasze hero się powiększa i dwie takie same animacje nie
   wyglądają idealnie".
   🔴 To jest uwaga o KIERUNKU, nie o wielkości. Napis „DIGITAL MARKETING"
   nadchodzi z głębi (rośnie z 0,6 do 1). Jeśli logo też rośnie, oba przedmioty
   jadą w tę samą stronę i jeden powtarza drugi. Teraz logo COFA SIĘ w głąb
   i rozmywa, a napis stamtąd WYCHODZI — mijają się na tej samej osi, jadąc
   w przeciwne strony. Oś ruchu została ta sama, zmienił się zwrot.
   ⚠️ To NIE jest powrót do wersji sprzed 05.08. Tamta tylko zmniejszała logo
   do 0,55 — bez rozmycia i bez gaśnięcia, więc czytała się jak „obrazek się
   skurczył". Tu logo schodzi do 0,26, rozmywa się do 16 px i przygasa, czyli
   naprawdę odjeżdża w ciemność.

   🔴 ODLOT ZACZYNA SIĘ NA 1400 ms, CZYLI 200 ms PRZED NAKŁADKĄ — i to nie jest
   przypadek. Pierwsza wersja startowała na 1700, więc cały ruch leciał POD
   gasnącą nakładką: krycia się MNOŻĄ, przez co przy 2300 ms logo miało już
   tylko 19% widoczności (0,58 własnego × 0,32 nakładki) i zamiast odjeżdżać —
   po prostu bladło. Teraz logo zdąża odjechać na czarnym tle, zanim nakładka
   zacznie znikać. Z tego samego powodu logo gaśnie SAMO tylko do 18% — resztę
   zabiera nakładka, a nie druga, niezależna animacja.

   🔴 1350 ms, nie 950 — odlot MUSI zachodzić na wejście napisu. Przy 950 ms
   logo kończyło ruch na 2350, napis ruszał na 2400 od zera krycia, a między
   nimi wychodziło ~300 ms czarnego ekranu. Dokładnie ten błąd, o który Karol
   miał pretensje 04.08 („żeby nie było widać przeskoków"). Teraz przy 2400 ms
   logo jest w 74% odlotu — małe, rozmyte i jeszcze widoczne — i znika dokładnie
   wtedy, gdy z tego samego miejsca wychodzi napis. */
html.intro-graj .hero__intro-logo {
  animation: aw-logo-w-glab 1350ms linear 1400ms both;
}
html.intro-graj .hero__intro {
  animation: aw-nakladka-gasnie 1300ms ease 1600ms both;
}
html.intro-graj .hero__title {
  animation: aw-napis-krycie 900ms ease 2400ms both,
             aw-napis-ruch 1150ms cubic-bezier(.4,.05,.2,1) 2400ms both;
}
/* 🔴 RUNDA 2 (12.08, Karol: „literki lekko się przesuwają"). Odstęp między
   DIGITAL a MARKETING otwierał się od 3000 do 4400 ms — czyli JESZCZE PRZEZ
   850 ms PO tym, jak napis doszedł do pełnego krycia (3300 ms). Zmierzone:
   wiersz MARKETING jechał wtedy z 400,5 px na 393,8 px. Napis wyglądał jak
   gotowy, a mimo to pływał.
   Teraz odstęp jedzie DOKŁADNIE w takcie napisu (2400–3550 ms, te same liczby
   co `aw-napis-ruch`), więc scena zatrzymuje się w jednej chwili.
   ⚠️ Moment 2400 ms jest jednym z trzech nietykalnych (patrz rozkład wyżej) —
   nie przesuwam go, tylko dopasowuję do niego drugi ruch. */
html.intro-graj .hero__title-line--2 {
  animation: aw-odstep 1150ms cubic-bezier(.4,.05,.2,1) 2400ms both;
}
html.intro-graj .hero__accent { animation: aw-tresc-krycie 550ms ease 3450ms both; }
html.intro-graj .hero__cta    { animation: aw-tresc-krycie 550ms ease 3450ms both; }

@keyframes aw-sygnet-krycie { from { opacity: 0 } to { opacity: 1 } }
@keyframes aw-sygnet-zglebi {
  from { transform: scale(.36); filter: blur(14px) }
  to   { transform: scale(1);   filter: blur(0) }
}
@keyframes aw-litera-krycie { from { opacity: 0 } to { opacity: 1 } }
@keyframes aw-litera-zglebi {
  from { transform: scale(var(--w-sk, .5)) }
  to   { transform: scale(1) }
}
/* Odlot rozpisany na pięć klatek, a nie zdany na krzywą. Powód zmierzony:
   przy zwykłej krzywej logo przez pierwsze 300 ms zmieniało wielkość o 9% —
   czyli stało — a potem cała zmiana wypadała w 300 ms. Dokładnie to martwe
   okno, o które Karol miał pretensje 04.08.
   Kroki wielkości (1,00 → 0,62 → 0,44 → 0,33 → 0,26) NIE są równe i to jest
   celowe: przedmiot oddalający się ze stałą prędkością maleje najszybciej
   na początku, a potem coraz wolniej — im dalej jest, tym mniej zmienia się
   jego widziana wielkość. Równe kroki czytałyby się jak suwak, nie jak odjazd.
   ROZMYCIE dochodzi z opóźnieniem: przedmiot jest ostry, dopóki jest blisko,
   i rozmywa się dopiero w ciemności. Krycie schodzi tylko do 18% — resztę
   zabiera nakładka, żeby dwa gaśnięcia się nie mnożyły. */
@keyframes aw-logo-w-glab {
  0%   { transform: scale(1);   opacity: 1;   filter: blur(0) }
  25%  { transform: scale(.62); opacity: .96; filter: blur(2px) }
  50%  { transform: scale(.44); opacity: .84; filter: blur(5px) }
  75%  { transform: scale(.33); opacity: .58; filter: blur(10px) }
  100% { transform: scale(.26); opacity: .18; filter: blur(16px) }
}
/* nakładka znika krycien, a na sam koniec chowa się z układu — dopóki jest
   widoczna, przykrywa całe okno */
@keyframes aw-nakladka-gasnie {
  0%   { opacity: 1; visibility: visible }
  99%  { visibility: visible }
  100% { opacity: 0; visibility: hidden }
}
@keyframes aw-napis-krycie { from { opacity: 0 } to { opacity: 1 } }
@keyframes aw-napis-ruch { from { transform: scale(.6) } to { transform: scale(1) } }
/* odstęp między DIGITAL a MARKETING otwiera się transformem, nie marginesem —
   margines zmieniałby wysokość bloku i przesuwał też DIGITAL (patrz wyżej) */
@keyframes aw-odstep {
  from { transform: translateY(calc(-1 * var(--hero-gap))) }
  to   { transform: translateY(0) }
}
@keyframes aw-tresc-krycie { from { opacity: 0 } to { opacity: 1 } }

@media (prefers-reduced-motion: reduce) {
  .hero__accent-text { transition: none; }
  /* warstwa cienia razem z tekstem — przy systemowym ograniczeniu ruchu
     podmiana hasła ma być skokiem, nie animacją */
  .hero__accent-cien { transition: none; }
}

/* ==========================================================================
   Round 5 tweaks
   ========================================================================== */

/* Stats text block — breathing room between heading / paragraph / button */
.stats__text .section-text { margin-top: 24px; }
.stats__text .btn { margin-top: 28px; }

/* Case study cards — hover state (lift + accent border + subtle image zoom) */
.case-card { cursor: pointer; transition: transform .28s ease, border-color .28s ease, box-shadow .28s ease; }
.case-card__img { transition: transform .5s ease; }
.case-card:hover { transform: translateY(-6px); border-color: var(--border-accent); box-shadow: 0 26px 54px -26px rgba(0,0,0,.85); }
.case-card:hover .case-card__img { transform: scale(1.05); }
@media (prefers-reduced-motion: reduce) {
  .case-card, .case-card__img { transition: none; }
  .case-card:hover { transform: none; }
  .case-card:hover .case-card__img { transform: none; }
}

/* Mobile — smaller card headings so they don't dominate.
   06.08: podpis grupy schodzi RAZEM z nazwą usługi. Zasada Karola brzmi
   „font ma być wielkości nazw usług" — więc musi ją trzymać na KAŻDEJ
   szerokości, nie tylko na komputerze. Zmierzone przy 390 px: przy 36 px
   podpis „Nie wiesz, co działa, a co przepala budżet:" zajmował TRZY wiersze
   (124 px) i był większy od nagłówka sekcji (30 px) — czyli podpis krzyczał
   głośniej niż tytuł, nad którym stoi. */
@media (max-width: 640px) {
  .offer-card h4 { font-size: 22px; }
  /* Projekt jest zrobiony tylko na 1710 px. Na telefon idą PROPORCJE, nie liczby:
     znak 140/417 = 33,6% szerokości kafla → 116 px przy kaflu 350 px, poświata
     i odstęp schodzą razem z nim. Wysokość zostaje dzisiejsza (292 px), bo
     350 × 292 to ta sama proporcja co 417 × 340 w projekcie. */
  .offer-card { --znak: 116px; min-height: 292px; padding: 30px 32px 28px; }
  .section-subheading { font-size: 22px; }
}

/* Goals — bouncing ball marker (JS-driven). While armed, hide the static
   dot/label; the ball reveals them once it lands on the peak. */
.bars__ball {
  position: absolute; top: 0; left: 0; width: 16px; height: 16px; border-radius: 50%;
  background: #fff; box-shadow: 0 0 0 4px rgba(255,44,44,.35);
  visibility: hidden; will-change: transform; pointer-events: none; z-index: 2;
}
.bars--armed .bars__ball { visibility: visible; }
.bars--armed .bar__dot { opacity: 0; }
.bars--armed .bar__label { opacity: 0; transition: opacity .4s ease; }
/* iskry rozprysku przy uderzeniu kulki w szczytową kolumnę */
.bars__spark {
  position: absolute; top: 0; left: 0; width: 5px; height: 5px; border-radius: 50%;
  background: #fff; box-shadow: 0 0 8px rgba(255,255,255,.8);
  pointer-events: none; z-index: 3;
}
.bars__spark--red { background: var(--accent-default); box-shadow: 0 0 10px rgba(255,44,44,.85); }

/* ==========================================================================
   Round 7 — full-height sections, ai-search background/particles
   ========================================================================== */

/* Desktop: every section except the last three (press/social/footer) fills
   the viewport, content centred vertically. */
@media (min-width: 1025px) {
  .section:not(.section--press):not(.section--social) {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }
}

/* Sekcja 2 (AI) — własna warstwa, żeby treść stała nad tłem sekcji.
   ⛔ 05.08.2026: ruchome kropki w tle (.dotfield) USUNIĘTE na życzenie Karola
   („wyglądają tanio"). Zdjęty znacznik z BaseLayout, generator z main.js
   i cały ten blok stylów — nie przywracać bez wyraźnej prośby. */
.section--ai { position: relative; }
.section--ai .container { position: relative; z-index: 1; }

/* ==========================================================================
   Zaufali nam — ściana logotypów klientów: cztery przewijane linie (czysty
   CSS, bez JS). Odstęp w linii liczony z szerokości okna: jedna pętla
   (loga + odstępy) = ~100vw, więc logo znikające z lewej od razu wraca
   z prawej — wszystkie marki linii są widoczne cały czas. Zmienne --lw
   (suma szerokości log w px) i --n (liczba log) ustawia HTML per linia.
   Parzyste linie płyną w przeciwną stronę, tempa lekko różne.
   ========================================================================== */
.section--clients { padding: 54px 0 46px; }
.clients__head {
  text-align: center;
  font-size: 17px;
  font-weight: 700;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.clients__wall {
  margin-top: clamp(40px, 7vh, 76px);
  display: grid;
  row-gap: clamp(36px, 7vh, 72px);
}
.clients__marquee {
  display: flex;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
}
.clients__row {
  --g: max(40px, calc((100vw - var(--lw, 800px)) / var(--n, 5)));
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--g);
  padding-right: var(--g);
  animation: clientsScroll 100s linear infinite;
}
/* środkowa linia w przeciwną stronę; pętle linii są równe szerokości okna,
   więc ten sam czas = identyczna prędkość w px/s (uwaga Karola 10.07) */
.clients__marquee:nth-child(2) .clients__row { animation-direction: reverse; }
.clients__row li { flex: none; display: flex; align-items: center; position: relative; }
/* monochrom (biel) w spoczynku; różne pliki źródłowe → wysokości dobrane
   optycznie per logo (szerokie napisy niżej, znaki zwarte/kwadratowe wyżej) */
.clients__row img {
  position: relative;
  height: 48px;
  width: auto;
  max-width: none;
  filter: brightness(0) invert(1);
  opacity: .75;
}
/* sika.svg jest przemalowana na wersję monochromatyczną (biały trójkąt,
   litery "Sika" wycięte na czarno) — filtr wybielający zlałby napis z trójkątem */
.clients__row img.logo--sika { height: 64px; filter: none; }
/* KOI, Gaspol, Ekob-TBA, WPK: białe tło plików źródłowych usunięte skryptem
   (przezroczysty PNG), więc działa standardowy filtr monochromatyczny */
.clients__row img.logo--koi { height: 110px; }
.clients__row img[src*="gaspol"] { height: 44px; }
.clients__row img[src*="cd-action"] { height: 27px; }
.clients__row img[src*="futuremeds"] { height: 36px; }
.clients__row img[src*="thaibalispa"] { height: 68px; }
.clients__row img[src*="finergia"] { height: 44px; }
.clients__row img[src*="hydrosan"] { height: 80px; }
.clients__row img[src*="salimali"] { height: 88px; }
.clients__row img[src*="supermed"] { height: 66px; }
.clients__row img[src*="wpk"] { height: 66px; }
.clients__row img[src*="keratynownia"] { height: 76px; }
.clients__row img[src*="velaco"] { height: 36px; }
.clients__row img[src*="vobmat"] { height: 34px; }
.clients__row img[src*="protektom"] { height: 46px; }
.clients__row img[src*="bemybean"] { height: 52px; }
.clients__row img[src*="artsento"] { height: 76px; }
.clients__row img[src*="wamax"] { height: 48px; }
.clients__row img[src*="ekob-tba"] { height: 56px; }
/* UWAGA (Karol 10.07): wszystkie loga w IDENTYCZNYM monochromie — żadnych
   poświat/pogrubień/różnic bieli per logo; czytelność cienkich krojów
   wyrównujemy wyłącznie rozmiarem */
/* bez efektów najechania myszką — pasek przewija się jednostajnie (decyzja Karola 10.07) */
/* monochromatyczne CTA pod ścianą — biel jak loga, bez czerwieni */
.clients__cta { margin-top: clamp(40px, 7vh, 72px); text-align: center; }
.btn--mono {
  padding: 16px 34px;
  border-radius: 50px;
  border: 1px solid rgba(255, 255, 255, .38);
  color: #fff;
  font-weight: 400;
}
.btn--mono:hover { background: #fff; border-color: #fff; color: #0a0a0a; }
@keyframes clientsScroll {
  from { transform: translateX(0); }
  to { transform: translateX(-100%); }
}
@media (prefers-reduced-motion: reduce) { .clients__row { animation: none; } }
/* Zamiana SaliMali ↔ FutureMeds tylko na telefonie (Karol, 11.08). Obie marki
   są w kodzie strony w obu miejscach; o tym, która się rysuje, decyduje
   szerokość ekranu. Domyślnie — czyli na komputerze i tablecie — widać znak
   z listy, a wersja telefonowa jest schowana. */
.clients__row img.logo--telefon { display: none; }
@media (max-width: 640px) {
  .clients__row img.logo--komputer { display: none; }
  .clients__row img.logo--telefon { display: block; }
}
@media (max-width: 640px) {
  .section--clients { padding: 40px 0 34px; }
  .clients__head { font-size: 14px; }
  .clients__wall { margin-top: 32px; row-gap: 28px; }
  .clients__cta { margin-top: 34px; }
  /* 🔴 62 → 95 s (Karol, 11.08: „animacje logotypów przyspieszyły na mobile,
     powinny wolniej się przesuwać").
     ⚠️ Ruch NIE PRZYSPIESZYŁ — czas pętli był ten sam od tygodni, sprawdzone.
     Zmieniło się to, że pas rusza teraz dopiero po dojściu do sekcji (uwaga 6
     z tego samego dnia). Wcześniej jechał od wczytania strony i Karol trafiał
     na niego w połowie drogi, w ruchu jednostajnym, którego początku nie widział.
     Teraz widzi START — a ruch, który zaczyna się na oczach widza, czyta się
     szybciej niż ten sam ruch zastany w połowie. Zwalniamy o 35%, bo wrażenie
     jest wrażeniem gościa strony, nie usterką do wytłumaczenia. */
  .clients__row { --g: 40px; animation-duration: 95s; }
  .clients__row img { height: 38px; }
  .clients__row img.logo--sika { height: 50px; }
  .clients__row img.logo--koi { height: 86px; }
  .clients__row img[src*="gaspol"] { height: 34px; }
  .clients__row img[src*="cd-action"] { height: 22px; }
  .clients__row img[src*="futuremeds"] { height: 28px; }
  .clients__row img[src*="thaibalispa"] { height: 54px; }
  .clients__row img[src*="finergia"] { height: 34px; }
  .clients__row img[src*="hydrosan"] { height: 62px; }
  .clients__row img[src*="salimali"] { height: 68px; }
  .clients__row img[src*="supermed"] { height: 52px; }
  .clients__row img[src*="wpk"] { height: 52px; }
  .clients__row img[src*="keratynownia"] { height: 60px; }
  .clients__row img[src*="velaco"] { height: 28px; }
  .clients__row img[src*="vobmat"] { height: 27px; }
  .clients__row img[src*="protektom"] { height: 36px; }
  .clients__row img[src*="bemybean"] { height: 40px; }
  .clients__row img[src*="artsento"] { height: 60px; }
  .clients__row img[src*="wamax"] { height: 38px; }
  .clients__row img[src*="ekob-tba"] { height: 44px; }
}

/* ==========================================================================
   Klienci o nas — opinie z profilu Google (makieta pod wtyczkę)
   ========================================================================== */
.grev {
  margin-top: 52px;
  display: grid;
  grid-template-columns: 300px 1fr;
  gap: 28px;
  align-items: stretch;
}
.grev__summary {
  background: linear-gradient(180deg, var(--surface-raised), var(--surface-sunken));
  border: 1px solid var(--border-card);
  border-radius: 24px;
  padding: 32px 26px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 10px;
}
.grev__g { width: 38px; height: 38px; }
.grev__score { font-size: 54px; font-weight: 700; letter-spacing: -0.02em; line-height: 1; color: var(--text-heading); }
.grev__stars { display: flex; gap: 3px; }
.grev__stars svg { width: 21px; height: 21px; }
.grev__count { font-size: 14px; line-height: 1.5; color: var(--text-body); max-width: 24ch; }
.grev__summary .btn { margin-top: 12px; white-space: normal; text-align: center; }
/* ==========================================================================
   PAS OPINII PRZESUWANY RĘCZNIE (przebudowa 12.08.2026)
   ==========================================================================
   🔴 Uwaga Karola: „do zmiany animacja przełączania opinii, na opcję samodzielnego
   przesuwania — na komputerze strzałki, na telefonie przesuwanie palcem".

   Do 12.08 opinie zmieniały się SAME co 7 sekund: widoczna trójka odjeżdżała
   w głąb i rozmywała się, kolejna dojeżdżała z tej samej głębi, a reszta kart
   była schowana (`display: none`). Teraz wszystkie stoją w jednym rzędzie,
   który przewija się w bok.

   PRZEWIJANIE ROBI PRZEGLĄDARKA, NIE SKRYPT. `overflow-x` plus przyciąganie
   (`scroll-snap`) daje palec na telefonie, gładzik i kółko z Shiftem na
   komputerze oraz klawisze strzałek — wszystko bez ani jednej linijki kodu
   i bez opóźnienia, jakie ma podrabianie przesuwania w JavaScripcie.
   Skrypt dokłada tylko guziki ze strzałkami (patrz main.js, `opinieKaruzela`).

   ILE KART W KADRZE — trzy progi, każdy z policzonej szerokości karty:
     komputer (≥1025 px) 3 karty  — przy 1440 px karta ma 445 px, tyle co dotąd
     tablet  (641–1024)  2 karty  — przy 768 px karta ma 342 px
     telefon (≤640 px)   1 karta  — 88% szerokości, żeby zza krawędzi wystawał
                                    brzeg następnej: to jedyny sygnał, że pas
                                    da się przesunąć (na dotyku nie ma strzałek)
   Pasek przewijania jest schowany — na komputerze rolę wskaźnika pełnią strzałki,
   na telefonie wystający brzeg kolejnej karty. */
.grev__cards {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: calc((100% - 40px) / 3);
  gap: 20px;
  overflow-x: auto;
  /* przewijanie pasa nie „przeskakuje" na przewijanie całej strony przy dojechaniu
     do końca — inaczej na telefonie ruch palcem w bok potrafi szarpnąć stroną */
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -ms-overflow-style: none;
  /* miejsce na uniesienie karty przy najeździe kursorem (translateY(-4px)) —
     bez tego uniesiona karta byłaby ucięta krawędzią pasa */
  padding: 6px 0;
}
.grev__cards::-webkit-scrollbar { display: none; }
/* Gdy prawdziwych opinii jest mniej niż trzy, karta ma zająć tyle, ile jej trzeba,
   a nie trzecią część rzędu. Po wgraniu prawdziwych opinii z Google
   (npm run fetch:reviews) układ wraca sam. */
.grev__cards--malo { grid-auto-columns: minmax(280px, 520px); }

/* Pojemnik pasa jest układem odniesienia dla strzałek — stoją na jego krawędziach.
   ⚠️ `min-width: 0` NIE JEST OZDOBĄ. `.opk-pas` leży w siatce (`.grev`), a pole
   siatki domyślnie nie może być węższe niż jego zawartość. Zawartością jest rząd
   44 kart, więc bez tej linijki pas rozpychał się do 10 424 px zamiast zmieścić
   się w kontenerze i przewijać — zmierzone przed poprawką. */
.opk-pas { position: relative; min-width: 0; }

/* ── STRZAŁKI — TYLKO KOMPUTER, CAŁKOWICIE POZA KARTAMI ──────────────────────
   Poniżej 1025 px pasa przesuwa się palcem, więc strzałek tam nie ma: byłyby
   drugim sposobem na to samo i zabierałyby szerokość karcie.
   🔴 RUNDA 2 (12.08, Karol: „mają być poza okienkiem"). W pierwszej wersji guzik
   nachodził 22 px na róg płyty. Teraz pas dostaje na komputerze marginesy boczne
   po 56 px (44 px guzika + 12 px odstępu) i strzałki stoją WYŁĄCZNIE w nich —
   żaden piksel guzika nie leży na karcie. Cena jest policzona: karta schodzi
   z 440 px na 403 px przy oknie 1440 px, czyli o 8%. */
.opk-strzalka {
  position: absolute;
  top: 50%;
  display: none;
  width: 44px;
  height: 44px;
  margin-top: -22px;
  border-radius: 50%;
  background: linear-gradient(155deg, #2a2a33 0%, #1a1a21 56%, #131318 100%);
  border: 1px solid rgba(255, 255, 255, .16);
  color: var(--text-primary);
  cursor: pointer;
  align-items: center;
  justify-content: center;
  transition: border-color .25s ease, opacity .25s ease, transform .25s ease;
  z-index: 2;
}
@media (min-width: 1025px) { .opk-strzalka { display: flex; } }
/* Skrypt chowa guziki, gdy wszystkie opinie mieszczą się na ekranie. Ta reguła
   MUSI stać po `display: flex` wyżej — atrybut `hidden` sam z siebie przegrywa
   z ustawionym `display`. */
.opk-strzalka[hidden] { display: none; }
.opk-strzalka svg { width: 20px; height: 20px; }
.opk-strzalka:hover { border-color: var(--border-accent); transform: scale(1.06); }
.opk-strzalka:focus-visible { outline: 2px solid var(--border-accent); outline-offset: 3px; }
/* koniec pasa — guzik zostaje na miejscu, ale widać, że nie ma dokąd iść */
.opk-strzalka:disabled { opacity: .28; cursor: default; transform: none; }
.opk-strzalka--wstecz { left: 0; }
.opk-strzalka--dalej { right: 0; }
/* Marginesy robią miejsce guzikom. Tylko od 1025 px — niżej strzałek nie ma,
   więc pas bierze całą szerokość i karta nic nie traci. */
@media (min-width: 1025px) {
  .opk-pas > .grev__cards { margin-inline: 56px; }
}
/* ── KARTA OPINII = TA SAMA PŁYTA, CO KAFEL USŁUGI (przebudowa 05.08.2026 nocą) ──
   Uwaga Karola: „sekcja opinii mocno odstaje i nie wygląda wiarygodnie". Odstawała, bo
   stała na innej powierzchni niż reszta strony: wierzch #16161c na tle #0a0a0c to różnica
   jasności 1,10:1 — praktycznie niewidoczna, a kafel usługi tuż obok ma 1,39:1. Karta
   czytała się jako obrys, nie jako przedmiot. Bierzemy dokładnie tę samą płytę, co kafel
   (ten sam gradient, ta sama krawędź, to samo zaokrąglenie 24 px) — jeden język, nie dwa. */
.grev__card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: linear-gradient(155deg, #2a2a33 0%, #1a1a21 56%, #131318 100%);
  border: 1px solid rgba(255, 255, 255, .13);
  border-radius: 24px;
  padding: 26px;
  /* Karta zatrzymuje się lewą krawędzią w kadrze — dzięki temu po przesunięciu
     pas stoi na początku opinii, a nie w połowie zdania. */
  scroll-snap-align: start;
  transition: transform .25s ease, border-color .25s ease;
}
/* ⛔ 12.08.2026: zdjęte razem ze starą rotacją — `.grev__card--off` (chowanie kart
   spoza widocznej trójki), opóźnienia fali `[data-poz]` oraz klasy lotu w głąb
   `.grev__cards--w-glab` / `--z-glebi`. Wszystkie opinie stoją teraz w pasie,
   więc nie ma czego chować ani przełączać. */
.grev__card:hover { transform: translateY(-4px); border-color: var(--border-accent); }
.grev__who { display: flex; align-items: center; gap: 12px; }
.grev__avatar {
  flex: none;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--av, #555);
  color: #fff;
  font-weight: 700;
  font-size: 17px;
  display: grid;
  place-items: center;
}
/* Zdjęcie z profilu Google — leży u nas na dysku (public/assets/img/opinie/).
   Obwódka odcina jasne zdjęcie od jasnego wierzchu płyty, żeby twarz nie zlewała się z kartą. */
.grev__avatar--foto {
  object-fit: cover;
  background: #22222a;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, .16);
}
/* Bez kolumny z oceną sumaryczną (decyzja Karola 14.07) — trzy karty opinii biorą całą szerokość
   sekcji (są szersze, więc mniej tekstu ucina się w karcie). Rotacja pokazuje trójkę, jak dotąd. */
.grev--bezoceny { grid-template-columns: 1fr; }
.grev__meta { display: flex; flex-direction: column; min-width: 0; }
.grev__meta strong { font-size: 15px; color: var(--text-primary); }
.grev__meta time, .grev__podpis { font-size: 13px; color: var(--text-muted); }
.grev__badge { margin-left: auto; width: 18px; height: 18px; flex: none; opacity: .9; }
.grev__cardstars { display: flex; gap: 2px; margin-top: 14px; }
.grev__cardstars svg { width: 16px; height: 16px; }
/* Opinie z Google mają skrajnie różną długość (od 15 do 700 znaków). Bez ograniczenia rząd
   rozciąga się do najdłuższej i obok krótkiej opinii robi się pusta dziura. Przycinamy widok
   do 8 linijek — PEŁNA treść zostaje w HTML (dla wyszukiwarek), obcina tylko przeglądarka. */
.grev__text {
  margin-top: 12px;
  font-size: 15px;
  line-height: 1.62;
  color: var(--text-body);
  display: -webkit-box;
  -webkit-line-clamp: 8;
  line-clamp: 8;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
@media (max-width: 1024px) {
  .grev { grid-template-columns: 1fr; }
  /* tablet — dwie opinie w kadrze (przy 768 px karta ma 342 px) */
  .grev__cards { grid-auto-columns: calc((100% - 20px) / 2); }
}
@media (max-width: 640px) {
  /* telefon — jedna opinia naraz (ustalenie Karola z 11.08 zostaje w mocy),
     ale 88% zamiast pełnej szerokości: zza krawędzi wystaje brzeg następnej karty
     i widać, że pas da się przesunąć palcem */
  .grev__cards { grid-auto-columns: 88%; }
}

/* Multi-step AI audit form */
.ai__step { display: flex; flex-direction: column; gap: 24px; align-items: flex-start; width: 100%; }
.ai__step[hidden] { display: none; }
.ai__step-actions { display: flex; gap: 12px; flex-wrap: wrap; }
.ai__done { font-size: var(--type-size-body); color: var(--text-primary); font-weight: 600; }
.ai__step:not([hidden]) { animation: stepIn .35s ease both; }
@keyframes stepIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* Pasek wpisywania czatu (makieta komunikatora, uwaga Karola 10.07):
   tekst pisze się TU z migającym kursorem, po „wysłaniu" pole się czyści,
   a dymek pojawia się w rozmowie. Przy pisaniu przycisk wysyłania robi się
   czerwony; długi tekst przewija się w polu tak, że widać jego koniec. */
.chat__composer {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 14px;
  min-height: 48px;
  padding: 6px 6px 6px 16px;
  border: 1px solid var(--border-default);
  border-radius: 26px;
  background: var(--surface-input);
}
.chat__composer-field {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  overflow: hidden;
  font-size: var(--type-size-sm);
  line-height: 1.4;
  color: var(--text-primary);
}
.chat__composer-ph { color: var(--text-muted); }
.chat__composer.is-typing .chat__composer-ph { display: none; }
/* długi tekst: JS dosuwa widok pola do ostatniej litery (scrollLeft) */
.chat__composer-text { white-space: nowrap; }
.chat__composer-caret {
  display: none;
  flex: none;
  width: 2px;
  height: 1.15em;
  margin-left: 2px;
  background: var(--accent-default);
  animation: caretBlink .8s steps(1) infinite;
}
.chat__composer.is-typing .chat__composer-caret { display: inline-block; }
.chat__composer-send {
  flex: none;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: rgba(255, 255, 255, .07);
  color: var(--text-muted);
  transition: background .25s ease, color .25s ease, transform .18s ease;
}
.chat__composer-send svg { width: 16px; height: 16px; display: block; }
.chat__composer.is-typing .chat__composer-send { background: var(--accent-default); color: #fff; }
.chat__composer.is-send .chat__composer-send { background: var(--accent-default); color: #fff; transform: scale(.85); }

/* Live chat — jak prawdziwy komunikator (uwaga Karola 10.07): wiadomości
   dosunięte do DOŁU okna; nowa wiadomość wyrasta na dole (animacja wysokości
   od zera) i płynnie wypycha starsze do góry. Okno ma zarezerwowaną pełną
   wysokość (JS), więc sekcja się nie rozciąga w trakcie animacji. */
.chat--anim .chat__body { justify-content: flex-end; }
.chat--anim .chat__body > .is-collapsed { display: none; }
.chat--anim .chat__body > .msg-in {
  animation: msgIn .45s cubic-bezier(.22, .61, .36, 1) backwards;
  overflow: hidden;
}
@keyframes msgIn {
  from { opacity: 0; max-height: 0; transform: translateY(14px); }
  to { opacity: 1; max-height: 320px; transform: none; }
}
.chat__typing .dots { display: inline-flex; gap: 5px; align-items: center; }
.chat__typing .dots i { width: 6px; height: 6px; border-radius: 50%; background: var(--accent-default); animation: chatDot 1s ease-in-out infinite; }
.chat__typing .dots i:nth-child(2) { animation-delay: .15s; }
.chat__typing .dots i:nth-child(3) { animation-delay: .3s; }
@keyframes chatDot { 0%, 100% { opacity: .35; transform: translateY(0); } 50% { opacity: 1; transform: translateY(-3px); } }

/* Migający kursor (używa go pasek wpisywania czatu — .chat__composer-caret) */
@keyframes caretBlink { 50% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .ai__step:not([hidden]) { animation: none; }
}

/* Pułapka na roboty spamujące (sesja 23) — pole „firma" w formularzach.
   Chowamy je odsunięciem poza ekran, a NIE atrybutem `hidden` czy `display:none`:
   część robotów pomija pola ukryte w oczywisty sposób i wtedy pułapka nic nie łapie.
   Blok ma aria-hidden, więc czytnik ekranu go nie przeczyta.
   ⚠️ Musi być w TYM pliku — styles.css jest jedynym stylem wczytywanym na KAŻDEJ
   stronie (content.css nie ma strony głównej, svc.css nie mają strony treściowe). */
.conv__trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* ==========================================================================
   FORMULARZ ZGŁOSZENIA (.zgl) — JEDEN DLA CAŁEJ STRONY (31.07.2026)

   Karol 31.07: „dlaczego tutaj używamy inny formularz niż ten z kategorii
   usługowych?". Były TRZY, każdy z innej sesji: `.audit__form` na 8 podstronach
   usługowych, `.conv__form` przy realizacjach i na /o-nas/, `ContactForm`
   na /kontakt/. Wszystkie wysyłały w to samo miejsce — różniły się tylko polami
   i wyglądem. Teraz jest jeden komponent: src/components/FormularzZgloszenia.astro.

   ⛔ Te style MUSZĄ zostać w styles.css. content.css wczytują tylko blog, case
   study, /o-nas/ i /kontakt/; svc.css tylko 8 podstron usługowych. styles.css
   wczytuje BaseLayout na każdej podstronie — inaczej formularz gdzieś rozjedzie się.

   Wartości nie są dobrane „na oko": przepisane z formularza na /kontakt/,
   zatwierdzonego przez Karola w sesji 30 (pola czytelne, nie szare).
   ========================================================================== */
.zgl { display: block; }

/* Własna powierzchnia formularza — TYLKO tam, gdzie formularz nie leży już
   na gotowym panelu (przy realizacjach i na /o-nas/). Na /kontakt/ i podstronach
   usługowych panel jest dookoła, więc druga ramka dałaby pudełko w pudełku. */
.zgl--panel {
  padding: clamp(24px, 3vw, 32px);
  border: 1px solid var(--border-default);
  border-radius: 22px;
  background: rgba(255, 255, 255, 0.04);
  backdrop-filter: blur(10px);
}

.zgl__h { margin: 0 0 18px; font-size: 1.0625rem; font-weight: 700; line-height: 1.35; color: #fff; }

.zgl__field { display: block; margin-bottom: 14px; }

/* Nazwa pola NAD polem, nie w środku: napis w środku znika, gdy klient zaczyna
   pisać, i przy pięciu polach nie wiadomo, co jest już wypełnione.

   ⭐ 06.08 Karol: „Imię jest mniejsze niż napis w polu formularza, słabo to wygląda".
   Miał rację i to był realny błąd hierarchii. Pomiar sprzed zmiany:
     etykieta 13 px / grubość 500  (na /kontakt/ 15 px, a poniżej 480 px nawet 14 px)
     podpowiedź w polu 16 px / grubość 400
   Czyli NAZWA pola była cichsza niż tekst zastępczy, który i tak znika po pierwszym
   znaku. Teraz odwrotnie: etykieta 15 px półgrubo, podpowiedź 14 px zwykłą grubością.
   Tekst, który klient wpisuje, ZOSTAJE 16 px — poniżej tej wartości iPhone sam
   przybliża stronę przy wejściu w pole, a tego nie chcemy.

   ⚠️ Selektor musi być `form.zgl .zgl__label`, a nie samo `.zgl__label`.
   svc.css i content.css wczytują się PO styles.css i mają własne reguły
   (`.kt .zgl__label`, wspólna skala `--skala-etykieta`), które wygrywały
   z pojedynczą klasą. Dwie klasy plus znacznik biją tamte i dają JEDEN rozmiar
   etykiety na wszystkich 27 podstronach, także na telefonie. */
form.zgl .zgl__label {
  display: block;
  margin-bottom: 6px;
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1.4;
  color: rgba(255, 255, 255, 0.92);
}
form.zgl .field::placeholder { font-size: 0.875rem; font-weight: 400; }
/* Znak pola obowiązkowego — czerwona gwiazdka (Karol, 06.08 wieczorem). Wcześniej
   stało tu słowo „wymagane" w szarości: drugi napis w tej samej linijce, czyli
   stos podpisów. Gwiazdka mówi to samo jednym znakiem.
   ⚠️ Dwie klasy plus znacznik (`form.zgl`), bo svc.css i content.css wczytują się
   PO tym pliku i mają własne reguły na tekst wewnątrz etykiety.
   Gwiazdka jest lekko podniesiona, żeby siadła przy górnej krawędzi liter, a nie
   na linii podstawy — inaczej czyta się jak kropka po nazwie pola. */
form.zgl .zgl__must {
  color: var(--accent-default);
  font-weight: 700;
  font-size: 1.05em;
  line-height: 1;
  vertical-align: 2px;
  margin-left: 1px;
}

/* „Jak się o nas dowiedziałeś/aś?" — powrót pola ze starej strony (Karol 18.08.2026,
   obowiązkowe na każdym formularzu). Kółka jednokrotnego wyboru narysowane jako
   kapsułki do kliknięcia — samo kółko jest schowane (opacity), ale zostaje w miejscu
   kapsułki, więc klawiatura i czytnik ekranu działają, a dymek „zaznacz to pole"
   pokazuje się przy właściwej kapsułce.
   Etykieta: legend nie łapie reguły `form.zgl .zgl__label` w formularzu audytu AI
   (tam formularz nie ma klasy .zgl), stąd własna klasa z tym samym wyglądem. */
.zgl__zrodlo { border: 0; padding: 0; margin: 0 0 14px; min-width: 0; }
.zgl__zrodlo-legenda {
  display: block;
  padding: 0;
  margin-bottom: 12px;
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1.4;
  color: rgba(255, 255, 255, 0.92);
}
.zgl__zrodlo-legenda .zgl__must {
  color: var(--accent-default);
  font-weight: 700;
  font-size: 1.05em;
  line-height: 1;
  vertical-align: 2px;
  margin-left: 1px;
}
.zgl__zrodlo-opcje { display: flex; flex-wrap: wrap; gap: 8px; }
.zgl__radio { position: relative; cursor: pointer; }
.zgl__radio input { position: absolute; inset: 0; opacity: 0; margin: 0; pointer-events: none; }
.zgl__radio span {
  display: inline-block;
  padding: 9px 14px;
  border: 1px solid var(--border-default);
  border-radius: 999px;
  background: var(--surface-input);
  color: var(--text-body);
  font-size: 0.875rem;
  line-height: 1;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.zgl__radio:hover span { border-color: var(--border-accent); }
.zgl__radio input:checked + span { border-color: var(--accent-default); color: #fff; box-shadow: inset 0 0 0 1px var(--accent-default); }
.zgl__radio input:focus-visible + span { outline: 2px solid var(--accent-default); outline-offset: 2px; }
/* pole „Wpisz skąd" pokazuje się tylko po zaznaczeniu „Inne" — bez JavaScriptu */
.zgl__zrodlo-inne { display: none; }
.zgl__zrodlo:has(input[value="Inne"]:checked) .zgl__zrodlo-inne { display: block; margin-top: 10px; }

/* Temat rozmowy — lista rozwijana (Karol 20.08.2026: „wybadać, jakiej usługi może
   potrzebować klient"). Wygląd jak pozostałe pola (.field); strzałka własna, bo
   systemową każda przeglądarka rysuje inaczej. Póki nic nie wybrano, napis w polu
   ma kolor podpowiedzi — reguła :invalid gaśnie w chwili wyboru opcji. */
.zgl__select { position: relative; display: block; }
.zgl__select select.field { appearance: none; -webkit-appearance: none; padding-right: 44px; cursor: pointer; }
.zgl__select::after {
  content: "";
  position: absolute;
  right: 18px;
  top: 50%;
  width: 8px;
  height: 8px;
  margin-top: -6px;
  border-right: 1.6px solid var(--text-muted);
  border-bottom: 1.6px solid var(--text-muted);
  transform: rotate(45deg);
  pointer-events: none;
}
select.field:required:invalid { color: var(--text-muted); }
select.field option { color: var(--text-primary); background-color: #141416; }
select.field option[value=""] { color: var(--text-muted); }

/* Panel opcji w naszym wyglądzie (Karol 20.08.2026: systemowa lista przeglądarki
   „średnio ładna, niebieskie podświetlenie" — nie da się jej przemalować).
   Skrypt (main.js, sekcja „lista rozwijana") chowa gołą listę i rysuje przycisk
   + panel. Goła lista ZOSTAJE pod przyciskiem: trzyma wartość do wysyłki i dymek
   „wybierz opcję" — dlatego jest przezroczysta, a nie wycięta z układu.
   Bez skryptów nic z tego się nie włącza i działa systemowa lista wyżej. */
.zgl__select--js select.field { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; pointer-events: none; }
.zgl__select-przycisk { display: block; width: 100%; text-align: left; cursor: pointer; font: inherit; font-size: var(--type-size-md); }
.zgl__select-przycisk--pusty { color: var(--text-muted); }
.zgl__select--otwarte::after { transform: rotate(225deg); margin-top: -2px; }
.zgl__lista {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  z-index: 30;
  margin: 0;
  padding: 6px;
  background: #141416;
  border: 1px solid var(--border-default);
  border-radius: 11px;
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.5);
  display: none;
}
.zgl__select--otwarte .zgl__lista { display: block; }
.zgl__opcja {
  display: block;
  width: 100%;
  padding: 10px 12px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--text-body);
  font: inherit;
  font-size: 0.9375rem;
  text-align: left;
  cursor: pointer;
}
.zgl__opcja:hover, .zgl__opcja:focus-visible { background: rgba(255, 44, 44, 0.12); color: #fff; outline: none; }
.zgl__opcja--wybrana { color: #fff; box-shadow: inset 0 0 0 1px var(--accent-default); }

/* Wielokrotny wybór (Karol 20.08.2026: „co jak do kogoś pasuje kilka opcji") —
   przy opcji stoi kwadracik, który po zaznaczeniu wypełnia się czerwienią marki
   i dostaje ptaszek. Panel zostaje otwarty, więc widać, co się już zaznaczyło. */
.zgl__opcja--wiele { display: flex; align-items: center; gap: 10px; }
.zgl__ptaszek {
  position: relative;
  flex: 0 0 auto;
  width: 17px;
  height: 17px;
  border: 1px solid var(--border-default);
  border-radius: 5px;
  background: var(--surface-input);
}
.zgl__opcja--wybrana .zgl__ptaszek { background: var(--accent-default); border-color: var(--accent-default); }
.zgl__opcja--wybrana .zgl__ptaszek::after {
  content: "";
  position: absolute;
  left: 5px;
  top: 2px;
  width: 4px;
  height: 8px;
  border-right: 2px solid #fff;
  border-bottom: 2px solid #fff;
  transform: rotate(45deg);
}
/* Wybrane pozycje wypisuje przycisk; przy wielu wyborach obwódka wokół pozycji
   byłaby drugim sygnałem obok ptaszka — zostaje sam ptaszek. */
.zgl__opcja--wiele.zgl__opcja--wybrana { box-shadow: none; }
/* Zapas bez skryptów: gołej listy wielokrotnej nie da się schować pod przyciskiem
   (nie ma przycisku), więc musi być widoczna i wyższa niż zwykłe pole. */
.zgl__select:not(.zgl__select--js) select.zgl__select-wiele { height: auto; padding: 8px 12px; }
.zgl__select:not(.zgl__select--js)::after { display: none; }

/* ------------------------------------------------------------------
   FORMULARZE NA TELEFONIE (Karol 20.08.2026: „zoptymalizuj te formularze
   pod mobile"). Zmierzone przed poprawką na ekranie 390 px:
     • kapsułki „skąd o nas wiesz" — 34 px wysokości,
     • pozycje w liście tematów — 40 px,
   przy zalecanym minimum 44 px na cel dotykany palcem (wytyczne dostępności
   WCAG 2.2 i obu producentów telefonów). Pola tekstowe (49 px) i przyciski
   wysyłki (51–54 px) były już powyżej progu — ich nie ruszamy.
   Czcionka w polach ma 16 px i TAK ZOSTAJE: przy mniejszej iPhone sam
   przybliża stronę w chwili kliknięcia w pole.
   ------------------------------------------------------------------ */
@media (max-width: 640px) {
  /* kapsułki źródła: 34 → 44 px, bez zmiany wielkości napisu */
  .zgl__radio span {
    min-height: 44px;
    padding: 0 16px;
    display: inline-flex;
    align-items: center;
  }
  .zgl__zrodlo-opcje { gap: 10px; }

  /* pozycje listy tematów: 40 → 44 px */
  .zgl__opcja { min-height: 44px; padding: 11px 12px; }
  .zgl__opcja--wiele { align-items: center; }

  /* panel opcji nie może przerosnąć ekranu — przy dłuższej liście przewija się
     w środku, zamiast wystawać poza dolną krawędź telefonu */
  .zgl__lista { max-height: 60vh; overflow-y: auto; -webkit-overflow-scrolling: touch; }

  /* ptaszek zgody: sam kwadracik ma 18 px, ale klikalna jest cała etykieta
     (zmierzone 300×59 px), więc powiększamy tylko odstęp od tekstu */
  .zgl__consent { gap: 12px; align-items: flex-start; }
}

/* Telefon i e-mail obok siebie, gdy jest na to miejsce. Próg liczony od SZEROKOŚCI
   POJEMNIKA, nie ekranu — ten sam formularz stoi raz w wąskiej kolumnie (realizacje),
   raz w szerokiej (podstrony usługowe). */
.zgl__row { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; }

.zgl .field { max-width: none; }
.zgl textarea.field { height: auto; min-height: 88px; padding: 12px 16px; resize: vertical; font-family: inherit; line-height: 1.5; }

/* Pułapka na roboty spamujące — człowiek tego pola nie widzi. */
.zgl__trap { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* Zgoda do zaznaczenia (dziś tylko /kontakt/). */
.zgl__consent {
  display: flex;
  align-items: flex-start;
  gap: 11px;
  margin: 4px 0 18px;
  color: var(--text-body);
  font-size: 0.8125rem;
  line-height: 1.5;
  text-wrap: pretty;
  cursor: pointer;
}
.zgl__check { flex: none; width: 18px; height: 18px; margin: 1px 0 0; accent-color: var(--accent-default); cursor: pointer; }
.zgl__consent a { color: #fff; text-decoration: underline; text-decoration-color: var(--accent-default); text-underline-offset: 2px; }

.zgl__btn {
  width: 100%;
  margin-top: 6px;
  padding: 15px 24px;
  border: 0;
  border-radius: 12px;
  background: var(--accent-default);
  color: #fff;
  font-family: inherit;
  font-size: 1.0625rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.18s ease;
}
.zgl__btn:hover { background: var(--accent-hover); }
.zgl__btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

/* KLAUZULA INFORMACYJNA (art. 13 RODO) — drobny druk, nie tekst do czytania.

   ⭐ 06.08 Karol: „dopisek o administratorze za duży, jest prawie wielkości
   formularza". Miał rację, a przyczyna nie leżała w tym akapicie. Tu stało
   0.8125rem (13 px), ale ta wartość NIGDY nie dochodziła do skutku:
   svc.css ma `.svc p { font-size: var(--skala-tekst) }`, a content.css
   `.kt p { ... }` — obie reguły są bardziej szczegółowe (klasa + znacznik)
   i wczytują się później. Klauzula renderowała się więc jako 17 px, dokładnie
   tak samo jak proza na stronie, z odstępem między wierszami 1,65.
   Pomiar 06.08: 140 px wysokości przy formularzu 665 px = 21 % (komputer),
   238 px przy 880 px = 27 % (telefon).

   `form.zgl` daje dwie klasy plus znacznik i wygrywa z obiema tamtymi regułami,
   bez wchodzenia w svc.css i content.css. Stopień bierzemy ze wspólnej skalki
   (--skala-podpis = 13 px), żeby klauzula została w systemie, a nie obok niego. */
form.zgl .zgl__legal {
  margin: 12px 0 0;
  font-size: var(--skala-podpis, 0.8125rem);
  line-height: 1.5;
  color: var(--text-muted);
  text-wrap: pretty;
}
.zgl__legal { margin: 12px 0 0; font-size: var(--skala-podpis, 0.8125rem); line-height: 1.5; color: var(--text-muted); text-wrap: pretty; }
.zgl__legal a { color: rgba(255, 255, 255, 0.8); text-decoration: underline; text-underline-offset: 2px; }
form.zgl .zgl__legal a { font-size: inherit; }

/* ==========================================================================
   OTWARCIE — rozpad napisu przy przewijaniu (kierunek C „Przelot", 04.08.2026)

   Karol wybrał: otwarcie stoi przyklejone do góry ekranu przez 1,1 ekranu
   przewijania, napis DIGITAL MARKETING rozjeżdża się NA ZEWNĄTRZ i rośnie —
   jakby widz przelatywał przez litery. Kolejność znikania: napis → slogan →
   guzik. Guzik gaśnie przedostatni, bo to element handlowy.

   DRUGA SEKCJA NIE JEST OSOBNĄ WYSPĄ (uwaga Karola 04.08): wypływa z tej samej
   ciemności JESZCZE w trakcie przypięcia. Robią to dwie rzeczy naraz —
   wciągnięcie sekcji pod otwarcie ujemnym marginesem ORAZ zachodzące na siebie
   zakresy ruchu (przy 800 px guzik jest na 20%, a nagłówek już na 75%), więc
   w żadnym momencie ekran nie jest pusty.

   Ruchem steruje POŁOŻENIE PASKA PRZEWIJANIA, nie zegar (animation-timeline).
   Zero bibliotek do pobrania — otwarcie to najcięższy ekran serwisu i nie może
   zwolnić. Bez obsługi tej techniki albo przy włączonym w systemie ograniczeniu
   ruchu napis stoi, sekcje są jedna pod drugą, strona działa normalnie.
   Wzorzec zbudowany i zatwierdzony w public/podglad/warianty-hero-rozpad/.
   ========================================================================== */

/* Metaliczne wypełnienie MUSI siedzieć na literze, nie na nagłówku: przy
   wypełnieniu na nagłówku transform na literze psuje malowanie (ta sama
   pułapka, co opisana wyżej przy .hero__title-line--2). Dlatego nagłówek
   z rozbiciem na litery oddaje wypełnienie dzieciom — inaczej malowałby
   nieruchome „duchy" liter pod odlatującymi. */
.hero__title--rozpad {
  background-image: none;
  -webkit-text-fill-color: currentColor;
  color: var(--color-neutral-50);
}
.hero__lit {
  display: inline-block;
  background-image:
    linear-gradient(180deg, rgba(255,255,255,0.2) 24%, rgba(0,0,0,0.2) 49%, rgba(255,255,255,0.2) 73%),
    linear-gradient(115deg, var(--color-neutral-50) 22%, var(--color-neutral-300) 99%);
  /* --bgw = szerokość całego wiersza, --bgx = pozycja litery w wierszu; ustawia
     je main.js, żeby przejście jasności biegło przez CAŁY napis, a nie przez
     każdą literę osobno. Bez skryptu napis dalej jest widoczny. */
  background-size: var(--bgw, 100%) 100%;
  background-position: var(--bgx, 0) 0;
  background-repeat: no-repeat;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
}
.hero__accent-odlot, .hero__cta-odlot { display: inline-block; }

@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    /* ⚠️ 110vh musi być RÓWNE stałej HERO_PRZYPIECIE w src/pages/index.astro —
       tam liczone są zakresy lotu liter, tu wysokość sceny. Rozjazd tych dwóch
       liczb = albo pusty ekran, albo napis ucięty w połowie lotu. */
    .hero-scena { height: calc(100vh + 110vh); position: relative; }
    .hero-scena .hero { position: sticky; top: 0; height: 100vh; min-height: 100vh; }

    .hero__lit, .hero__accent-odlot, .hero__cta-odlot, .section--ai .container {
      animation-timeline: scroll(root block);
      animation-fill-mode: both;
      animation-duration: auto;
      will-change: transform, opacity, filter;
    }

    /* Ruch startuje dopiero po intrze. W trakcie intra strona jest zablokowana
       (html.intro-lock ma overflow:hidden), więc zakres przewijania wynosi zero
       — a przy zerowym zakresie przeglądarka potrafi ustawić animację na KONIEC,
       czyli otwarcie byłoby puste przez pierwsze cztery sekundy. */
    html:not(.intro-play) .hero__lit {
      animation-name: hero-odlot;
      animation-timing-function: cubic-bezier(.5, 0, .78, .5);
    }
    /* SLOGAN I GUZIK JADĄ TĄ SAMĄ KRZYWĄ CO LITERY (uwaga Karola 05.08:
       „nie rozmywa się w podobnym tempie, pod koniec wciąż się nie zmienia").
       Do 05.08 slogan ruszał dopiero na 44vh — czyli wtedy, gdy z napisu prawie
       nic już nie zostało — i kończył na 80vh, długo po literach. Efekt: przez
       pół rozpadu wisiał ostry, nieruchomy napis, a potem znikał sam jeden.
       Teraz slogan startuje na 4vh (razem z literami), guzik na 10vh, oba
       kończą lot w paczce liter (58–68vh). Kolejność znikania zostaje:
       napis → slogan → guzik.

       KRZYWA jest CELOWO INNA niż u liter i to nie pomyłka. Litery jadą krzywą
       „nic-nic-świst", ale jest ich jedenaście i każda rusza w innym momencie —
       razem czytają się jako ciągły ruch. Slogan i guzik to POJEDYNCZE napisy:
       na tej samej krzywej stały nieruchomo przez pół drogi, a potem znikały
       jednym skokiem. Dlatego dostają krzywą, która w połowie zakresu jest
       dokładnie w połowie rozmycia — zmieniają się przez cały czas przewijania,
       tak jak paczka liter obok. */
    html:not(.intro-play) .hero__accent-odlot {
      animation-name: hero-slogan;
      animation-range: 4vh 62vh;
      animation-timing-function: cubic-bezier(.38, .12, .6, .86);
    }
    html:not(.intro-play) .hero__cta-odlot {
      animation-name: hero-guzik;
      animation-range: 10vh 70vh;
      animation-timing-function: cubic-bezier(.38, .12, .6, .86);
    }

    /* Sekcja 2 wciągnięta pod otwarcie. To ma sens WYŁĄCZNIE przy działającym
       ruchu — bez ruchu sekcja stoi nieruchomo i nachodziłaby na otwarcie
       (tekst na tekście), dlatego margines siedzi w tym samym warunku. */
    /* 🔴 TO JEST MIEJSCE, KTÓRE ZABIŁO PUSTY EKRAN — nie długość przypięcia.
       Wciągnięcie równe 100vh (a nie 55vh jak do 05.08) ustawia górną krawędź
       sekcji 2 w dokumencie DOKŁADNIE na końcu przypięcia: 100 + 110 − 100.
       Skutek: sekcja jedzie w górę przez CAŁY czas, kiedy otwarcie stoi,
       i siada na swoim miejscu w tej samej sekundzie, w której otwarcie się
       odkleja. Przy 55vh sekcja startowała 45vh niżej, więc między zniknięciem
       napisu a jej pojawieniem się był kawałek czarnego ekranu.
       Przy zerowym przewinięciu krawędź sekcji leży na 110vh, czyli PONIŻEJ
       pierwszego ekranu — nic nie nachodzi na guzik otwarcia. */
    .section--ai { margin-top: -100vh; padding-top: 11vh; position: relative; z-index: 3; }

    /* 🔴 PRZYTRZYMANIE SEKCJI — poprawka Karola z 05.08: „druga sekcja nie ma
       pojawiać się od dołu, tylko ze środka, bo tak odkrywają się litery
       DIGITAL MARKETING".

       ZMIERZONA PRZYCZYNA starego wrażenia: całe odkrywanie sekcji (pojawianie
       + wyostrzanie) leciało PONIŻEJ dolnej krawędzi ekranu. Nagłówek „Jesteśmy
       Agencją AI ready" przy 30% przewinięcia stał na 105 vh (poza ekranem)
       i był już w 60% odkryty, a gdy pierwszy raz wchodził w kadr — na 83 vh —
       odkrywanie było zrobione w 96%. Widz nie oglądał żadnej animacji, tylko
       gotowy blok pełznący od dolnej krawędzi. Wydłużanie samego odkrywania nic
       by nie dało: problem był w MIEJSCU, nie w czasie.

       Naprawa: sekcja dostaje własny, LINIOWY ruch w górę, który częściowo
       kasuje przewijanie. Dzięki temu w chwili startu odkrywania nagłówek stoi
       już w połowie wysokości ekranu (49 vh zamiast 74 vh) i tam się materializuje,
       a potem normalnie odjeżdża w górę razem ze stroną.

       ⚠️ Krzywa MUSI być liniowa — to nie ozdoba, tylko odejmowanie: każdy
       procent zakresu odpowiada stałemu kawałkowi przewinięcia. Krzywa z
       przyspieszeniem rozjechałaby ruch sekcji z ruchem paska przewijania.

       🔴 DRUGA RUNDA 05.08 — Karol: „animacja rozjeżdża się tak, że zostawia
       PUSTE MIEJSCE NA ŚRODKU i chcę, żeby druga sekcja płynnie z tego miejsca
       się wyłaniała". Pierwsze podejście (−25 vh) tylko SKRACAŁO wjazd z dołu
       o połowę — sekcja dalej jechała w górę na oczach widza. Teraz jest
       −60 vh, dobrane RACHUNKIEM, nie na oko: nagłówek stoi naturalnie na
       (132,5 − przewinięcie) vh, a przy przesunięciu o (przewinięcie − 110) vh
       zostaje na 22,5 vh — DOKŁADNIE tam, gdzie wyląduje. Ruch pionowy w czasie
       wyłaniania: ZERO. Sekcja materializuje się w dziurze po literach i stoi,
       aż otwarcie się odklei; potem strona przewija się normalnie.

       ⚠️ To działa TYLKO przy liniowej krzywej — przesunięcie ma dokładnie
       kasować przewijanie. Krzywa z przyspieszeniem = nagłówek pływa.

       🔴 −60 vh znaczy, że przed startem zakresu (wypełnienie „both") sekcja
       stoi o 60 vh wyżej NA CAŁEJ stronie: jej górna krawędź leży w spoczynku
       na 50 vh, czyli NAD guzikiem „Zarezerwuj spotkanie online" (71,5–78,2 vh).
       Przezroczysta sekcja zjadałaby kliknięcia — guzik byłby widoczny, ale
       martwy. Dlatego druga, osobna animacja `sekcja2-klikalnosc` trzyma
       sekcję nieklikalną, dopóki nie wyląduje. To NIE jest ozdoba — bez niej
       najważniejszy guzik handlowy na stronie przestaje działać. */
    html:not(.intro-play) .section--ai {
      animation-name: sekcja2-przytrzymanie, sekcja2-klikalnosc;
      animation-timeline: scroll(root block), scroll(root block);
      animation-fill-mode: both, both;
      animation-duration: auto, auto;
      /* Klikalność wraca na 74 vh (środek zakresu 70–78). Nie później:
         przy 100 vh sekcja WYGLĄDA na gotową, a pole „Link do strony"
         jeszcze nie przyjmowałoby kliknięć. Nie wcześniej: guzik otwarcia
         gaśnie dopiero około 70 vh, a element o zerowej widoczności DALEJ
         łapie kliknięcia — przełączenie przed jego zgaśnięciem zabrałoby mu
         je bezpowrotnie. */
      animation-range: 50vh 110vh, 70vh 78vh;
      animation-timing-function: linear, linear;
    }
    html:not(.intro-play) .section--ai .container {
      animation-name: sekcja2-zglebi;
      /* 🔴 Start 58 → 52 vh (Karol, 05.08 wieczorem: „2 sekcja powinna pojawiać
         się nieco wcześniej"). 52 vh to nie liczba na oko: przytrzymanie
         (`sekcja2-przytrzymanie`) zaczyna się dokładnie na 50 vh, więc od 52 vh
         sekcja materializuje się już w PRZYTRZYMANYM położeniu — nagłówek stoi
         w miejscu, a nie płynie w górę w trakcie wyłaniania. Start przed 50 vh
         cofnąłby nas do „wjazdu z dołu”. */
      animation-range: 52vh 104vh;
      animation-timing-function: cubic-bezier(.24, .6, .3, 1);
    }
  }
}

@keyframes hero-odlot {
  to {
    transform: translate3d(var(--dx), var(--dy), 0) rotate(var(--rot)) scale(var(--sk));
    opacity: 0;
    filter: blur(var(--bl));
  }
}
/* Slogan i guzik lecą jak litery: NA ZEWNĄTRZ od środka ekranu, rosnąc
   i rozmywając się. Slogan siedzi tuż pod napisem, więc znosi go lekko w dół;
   guzik jest najniżej, więc odjeżdża najdalej w dół i rośnie najmniej —
   inaczej rozlałby się na cały ekran. Do 05.08 guzik tylko zjeżdżał o 8vh
   i gasł: żadnego rozmycia, żadnego powiększenia, więc odstawał od reszty. */
@keyframes hero-slogan { to { transform: translate3d(0, 10vh, 0) scale(2.8); opacity: 0; filter: blur(19px); } }
@keyframes hero-guzik  { to { transform: translate3d(0, 26vh, 0) scale(2.1); opacity: 0; filter: blur(14px); } }
/* Sekcja 2 przychodzi Z GŁĘBI, nie od dołu: staje w środku ekranu mała
   i rozmyta, po czym rośnie do swojej wielkości i wyostrza się. To ta sama oś
   ruchu, po której lecą litery otwarcia — tylko w drugą stronę. Litery mijają
   widza i znikają za nim, a to, co jest dalej, wychodzi z głębi na jego miejsce.
   Do 05.08 był tu zjazd o 70 px w pionie, czyli ruch „od dołu". */
/* 🔴 Oddalenie na starcie 0,74 → 0,50 i rozmycie 16 → 24 px (Karol, 05.08:
   „bardziej oddalona na starcie animacji, żeby dać mocniejszy efekt wyłaniania
   się ze środka”). SKUTEK UBOCZNY POLICZONY, nie przeoczony: kafel skaluje się
   od swojego środka, więc nagłówek zaczyna o (1 − skala) × 28,1 vh niżej niż
   wyląduje. Przy 0,74 było to 7,3 vh, przy 0,50 jest 14,0 vh — nagłówek startuje
   na 36,5 vh i ROŚNIE na miejsce. To wciąż rośnięcie od środka, nie wjazd z dołu:
   sekcja przez cały czas stoi w przytrzymaniu, w górę nie jedzie. */
/* 🔴 KLATKA 84% DODANA 11.08 — rozmycie schodzi do zera WCZEŚNIEJ niż reszta ruchu.
   Powód: cały zakres to 52–104% wysokości ekranu i przy zatrzymaniu w środku sekcja
   zostawała rozmyta na stałe (uwaga Karola: „zostaje z lekkim blurem i sam muszę
   dociągać"). Dociągnięcie w `main.js` załatwia ogon zakresu, ale ta klatka zdejmuje
   samą przyczynę na ostatnim odcinku: od 84% zakresu (ok. 88% wysokości ekranu)
   sekcja jest już OSTRA i tylko dojeżdża wielkością oraz kryciem.
   Zysk mierzalny: pas, w którym da się utknąć z widocznym rozmyciem, kurczy się
   z 439 px do 371 px, a rozmycie na jego końcu z 0,86 px do zera.
   ⚠️ Krycie i skala zostają liniowo do końca — gdyby też kończyły na 84%,
   ostatnie 16% zakresu byłoby martwe i wjazd wyglądałby na urwany.
   Cofnięcie: skasować wiersz `84%`. */
@keyframes sekcja2-zglebi {
  from { opacity: 0; transform: scale(.5); filter: blur(24px); }
  84%  { filter: blur(0); }
  to   { opacity: 1; transform: none; filter: blur(0); }
}
@keyframes sekcja2-przytrzymanie {
  from { transform: translate3d(0, -60vh, 0); }
  to   { transform: translate3d(0, 0, 0); }
}
/* Sekcja jest przezroczysta i wciągnięta nad otwarcie, więc dopóki nie
   wyląduje, musi PRZEPUSZCZAĆ kliknięcia na guzik otwarcia pod spodem.
   Osobna animacja, nie dodatkowa klatka w przytrzymaniu: klikalność jest
   przełącznikiem (włącz/wyłącz), a przesunięcie płynną liczbą — wrzucone
   do jednych klatek psułyby sobie nawzajem rozkład w czasie. */
@keyframes sekcja2-klikalnosc {
  from { pointer-events: none; }
  to   { pointer-events: auto; }
}

/* --- Górna belka wjeżdża z góry na końcu intra (Karol, 04.08) ---------------
   Do 04.08 menu było w trakcie intra ZASŁONIĘTE nakładką z logo. Po przypięciu
   otwarcia do góry ekranu (position: sticky) otwarcie zaczęło tworzyć własną
   warstwę malowania, więc nakładka — choć postawiona najwyżej — przestała
   sięgać poza otwarcie i menu wyskakiwało od pierwszej klatki.
   Zamiast wracać do zasłaniania, menu dostaje własne wejście: zjeżdża z góry
   jako ostatni element, po sloganie i guziku (main.js, klatka 3800 ms).
   Bez skryptu albo przy wyłączonym ruchu w systemie belka jest od razu na
   miejscu — klasy .intro-play po prostu nie ma. */
html.intro-play .header { transform: translateY(-160%); opacity: 0; }
html.intro-play .navbar { pointer-events: none; }
/* 🔴 BELKA JEDZIE DOKŁADNIE TYM SAMYM CO NAPIS „DIGITAL MARKETING" —
   te same trzy liczby co przy `.hero__title` (1150 ms ruch, 900 ms krycie,
   start 2400 ms). Zmiana z 05.08, uwaga Karola: „belka musi pojawiać się
   w takim samym tempie co napis; przez desynchronizację odciąga uwagę
   i tworzy wrażenie, jakby była spóźniona".

   ZMIERZONY ROZJAZD, który to powodował: napis pojawiał się przez **1143 ms**
   (2629→3772), belka przez **441 ms** (3939→4380) — **2,6 razy szybciej** —
   a między końcem napisu a startem belki było jeszcze 167 ms ciszy. Napis
   płynął, belka po chwili wskakiwała. Krzywa była ta sama od 04.08; różniła
   je DŁUGOŚĆ i moment startu, i to wystarczyło.

   ⚠️ Start 2400 ms nie łamie zasady z 04.08 („belka nie może być widoczna
   na początku wejścia"): o 2400 ms nakładka z logo ma już tylko 13 %
   widoczności, a gdy belka realnie się zapala — 4 %. Logo jest wtedy
   złożone i zmniejszone. Wcześniej niż 2400 ms wchodzić NIE MOŻE. */
html.intro-graj .header {
  animation: aw-belka-ruch 1150ms cubic-bezier(.4, .05, .2, 1) 2400ms both,
             aw-belka-krycie 900ms ease 2400ms both;
  /* Warstwa rysowania belki przygotowana Z GÓRY, zanim ruszy. Pasek ma rozmycie
     tła (backdrop-filter), maskowane obramowanie i cień — gdy zaczyna się
     przesuwać, przeglądarka musi mu dopiero WTEDY założyć osobną warstwę, a ta
     jedna klatka to „chrupnięcie" tuż przed pokazaniem belki (Karol 04.08).
     Podpowiedź żyje tylko na czas wejścia: po sprzątnięciu klasy .intro-graj
     znika. Trwała szkodzi — lekcja z cząsteczek tła (patrz .dotfield i). */
  will-change: transform, opacity;
}
/* Do 04.08 przesuwanie i pojawianie się belki siedziało w JEDNYM przejściu
   o różnych długościach (0,75 s ruch, 0,5 s krycie) — belka była w pełni
   widoczna 250 ms zanim dojechała. Teraz to dwie osobne animacje i krycie
   kończy się razem z ruchem. */
@keyframes aw-belka-ruch { from { transform: translateY(-160%) } to { transform: translateY(0) } }
@keyframes aw-belka-krycie { from { opacity: 0 } to { opacity: 1 } }

/* --- Sekcja realizacji na stronie głównej: pojemnik na pełną szerokość -------
   (12.08.2026, przy przenoszeniu kafli z /case-study/)

   Po podmianie przewijanego paska na siatkę kart okazało się, że pojemnik tej
   jednej sekcji ma 893 px zamiast 1361 px jak pozostałe — a siatka
   `.cards--cs` (kolumny po min. 340 px) mieściła przez to DWIE karty zamiast
   trzech: dwie w rzędzie i jedna sama pod spodem. Czyli dokładnie ta dziura,
   którą zgłosiłeś na liście realizacji.

   Przyczyna: sekcja ma `content-visibility: auto` (klasa `.cv-auto`), a razem
   z nim `contain-intrinsic-size: auto 900px` — podana JEDNA wartość obowiązuje
   w obu osiach, więc dopóki sekcja jest daleko od ekranu, przeglądarka układa
   ją tak, jakby miała 900 px szerokości. Stary pasek tego nie pokazywał, bo
   karty w nim i tak miały szerokość liczoną z ekranu.

   `width: 100%` zdejmuje zależność od tej wartości: pojemnik zawsze bierze
   szerokość sekcji, tak jak na /case-study/ (1361 px przy oknie 1440 px,
   trzy karty po 439 px).
   Cofnięcie: skasować ten blok. */
#case-study > .container { width: 100%; }
/* 🔴 12.08, druga runda uwag: „podnieś nagłówek, bo jest za blisko sekcji,
   zobacz jak odstaje od reszty strony" — i to samo na telefonie.
   Miałem rację co do mechanizmu, ale nie co do liczby: przepisałem odstęp
   z przewijanego paska (24 px), zamiast sprawdzić, ile mają sąsiednie sekcje.
   Zmierzone na stronie głównej, odległość od dołu nagłówka do pierwszego
   elementu treści:
     Klienci o nas (nagłówek + siatka kart)   52 px na komputerze i telefonie
     Zaufali nam                              67 px / 32 px
     Czego potrzebuje Twoja firma             40 px / 40 px
     Dedykowany opiekun (nagłówek + zdanie)   24 px / 24 px
     realizacje PRZED tą poprawką             24 px / 24 px  ← odstawało
   Bierzemy 52 px — wartość sekcji „Klienci o nas", czyli jedynej innej,
   która też jest nagłówkiem nad siatką kart. Ten sam odstęp na obu
   szerokościach, bo tak ma wzorzec.
   Cofnięcie: skasować tę regułę. */
#case-study .cards--cs { margin-top: 52px; }

/* --- Sekcja „Jesteśmy Agencją AI ready" — powietrze pod belką na telefonie ---
   🔴 12.08, uwaga Karola: „na telefonie sekcja »Jesteśmy Agencją AI ready« jest
   za blisko belki górnej, nie ma w ogóle powietrza".

   ZMIERZONE (telefon 390 px, strona przewinięta tak, że górna krawędź sekcji
   jest na górze ekranu — czyli dokładnie tak, jak się na nią wchodzi):
     belka jest przyklejona do góry i ma 80 px wysokości,
     sekcja miała u góry 92,8 px wyściółki (11% wysokości ekranu),
     więc widocznego odstępu zostawało 92,8 − 80 = 13 px.
   Przy wyłączonych animacjach w telefonie było jeszcze gorzej: wyściółka 44 px,
   czyli nagłówek wchodził 36 px POD belkę.

   Wyściółka 11% wysokości ekranu nie jest przypadkowa — to część układanki,
   w której na komputerze druga sekcja wypływa spod otwarcia. Dlatego nie ruszam
   jej tam, tylko podmieniam poniżej 1024 px, gdzie tej układanki nie ma
   (otwarcie jest wyłączone na telefonie i tablecie od 09.08).
   124 px = 80 px belki + 44 px realnego powietrza, czyli tyle, ile wynosi
   odstęp u góry każdej innej sekcji na telefonie.
   Cofnięcie: skasować ten blok. */
@media (max-width: 1024px) {
  .section--ai { padding-top: 124px; }
}

/* ============================================================
   POTWIERDZENIE WYSLANIA FORMULARZA (18.08.2026)
   Wstawia je main.js na koncu formularza po odpowiedzi serwera —
   zamiast przenosin na /dziekujemy/ (decyzja Karola z dnia przelaczenia).
   ============================================================ */
.form-notka { display: flex; align-items: center; gap: 10px; margin: 14px 0 0; font-size: 15px; line-height: 1.5; color: var(--text-body, rgba(255,255,255,.92)); }
.form-notka__znak { display: inline-flex; flex: none; }
.form-notka__znak svg { width: 22px; height: 22px; }
.form-notka__znak circle { fill: none; stroke: #4ade80; stroke-width: 1.6; }
.form-notka__znak path { fill: none; stroke: #4ade80; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.form-notka--blad { color: #f87171; }
