/* =====================================================================
 * components.css — biblioteka komponentów panelu.
 *
 * Konwencja nazw: `.blok`, `.blok-element`, `.blok.is-modyfikator`.
 * Stany semantyczne mają JEDNE nazwy w całym panelu:
 *   .is-ok  .is-warn  .is-bad  .is-unknown  .is-info
 * i te same nazwy występują w kontrakcie danych (`level`, `Flash.level`,
 * `Problem.level`), więc szablon mapuje je wprost: `class="is-{{ x.level }}"`.
 *
 * Podgląd wszystkich wariantów: panel/preview/system.html
 * ===================================================================== */

/* =====================================================================
 * NAWIGACJA BOCZNA
 * ===================================================================== */
.nav {
  display: flex;
  flex-direction: column;
  background: var(--c-nav-bg);
  color: var(--c-nav-fg);
  border-right: 1px solid var(--c-nav-border);
  overflow-y: auto;
}

.nav-marka {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-4) var(--sp-3);
  color: var(--c-nav-fg-active);
  font-weight: var(--fw-semibold);
  font-size: var(--fs-base);
  letter-spacing: -0.01em;
  text-decoration: none;
}

.nav-marka:hover { color: var(--c-nav-fg-active); }

.nav-marka-podpis {
  display: block;
  font-size: var(--fs-xs);
  font-weight: var(--fw-normal);
  color: var(--c-nav-muted);
  letter-spacing: 0;
}

.nav-grupa {
  padding: var(--sp-2) var(--sp-2) var(--sp-3);
}

.nav-grupa-tytul {
  padding: var(--sp-2) var(--sp-2) var(--sp-1);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--c-nav-muted);
}

.nav-poz {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-2);
  border-radius: var(--r-sm);
  color: var(--c-nav-fg);
  text-decoration: none;
  font-size: var(--fs-md);
  line-height: 1.3;
  min-height: var(--tap);
}

.nav-poz:hover {
  background: var(--c-nav-active-bg);
  color: var(--c-nav-fg-active);
}

/* Pozycja bieżąca. Oprócz tła dostaje lewy pasek — sam kolor tła
   jest zbyt subtelny na ciemnym tle, a `aria-current` musi mieć
   odpowiednik wizualny. */
.nav-poz[aria-current="page"] {
  background: var(--c-nav-active-bg);
  color: var(--c-nav-fg-active);
  font-weight: var(--fw-medium);
  box-shadow: inset 2px 0 0 var(--c-accent);
}

.nav-poz-ikona { width: 16px; height: 16px; opacity: 0.9; }

/* Licznik przy pozycji (np. liczba stron w awarii). Zawsze z tekstem
   dla czytnika — sama liczba nic nie znaczy. */
.nav-licznik {
  margin-left: auto;
  min-width: 20px;
  padding: 0 var(--sp-1);
  border-radius: var(--r-full);
  background: var(--c-bad-fg);
  color: var(--c-on-bad);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  font-variant-numeric: var(--num);
  text-align: center;
  line-height: 18px;
}

.nav-licznik.is-warn { background: var(--c-warn-fg); color: var(--c-on-warn); }

.nav-stopka {
  margin-top: auto;
  padding: var(--sp-3);
  border-top: 1px solid var(--c-nav-border);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.nav-operator {
  font-size: var(--fs-xs);
  color: var(--c-nav-muted);
  line-height: 1.4;
}

.nav-operator strong {
  display: block;
  color: var(--c-nav-fg);
  font-weight: var(--fw-medium);
  font-size: var(--fs-sm);
}

/* Podnawigacja per strona — pojawia się tylko na ekranach strony. */
.nav-podgrupa {
  margin: var(--sp-1) 0 var(--sp-2) var(--sp-4);
  padding-left: var(--sp-2);
  border-left: 1px solid var(--c-nav-border);
}

.nav-podgrupa .nav-poz {
  font-size: var(--fs-sm);
  min-height: 28px;
  padding-top: var(--sp-1);
  padding-bottom: var(--sp-1);
}

.nav-podgrupa .nav-poz[aria-current="page"] {
  box-shadow: none;
  background: var(--c-nav-active-bg);
}

/* Przełącznik motywu w stopce nawigacji. */
.przelacznik-motywu {
  display: flex;
  gap: 2px;
  padding: 2px;
  background: rgba(255, 255, 255, 0.06);
  border-radius: var(--r-sm);
}

.przelacznik-motywu button {
  flex: 1;
  padding: var(--sp-1) var(--sp-2);
  background: transparent;
  border: 0;
  border-radius: 2px;
  color: var(--c-nav-muted);
  font-size: var(--fs-xs);
  cursor: pointer;
  line-height: 20px;
}

.przelacznik-motywu button:hover { color: var(--c-nav-fg-active); }

.przelacznik-motywu button[aria-pressed="true"] {
  background: var(--c-nav-active-bg);
  color: var(--c-nav-fg-active);
  font-weight: var(--fw-medium);
}

/* =====================================================================
 * BREADCRUMB
 * ===================================================================== */
.okruchy {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-1);
  font-size: var(--fs-sm);
  color: var(--c-muted);
  margin-bottom: var(--sp-2);
}

.okruchy li { display: flex; align-items: center; gap: var(--sp-1); }

.okruchy a { color: var(--c-muted); text-decoration: none; }
.okruchy a:hover { color: var(--c-accent); text-decoration: underline; }

.okruchy-sep { color: var(--c-faint); }

.okruchy [aria-current="page"] { color: var(--c-text); font-weight: var(--fw-medium); }

/* =====================================================================
 * NAGŁÓWEK STRONY
 * ===================================================================== */
.naglowek {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-4);
  flex-wrap: wrap;
  padding-bottom: var(--sp-4);
  border-bottom: 1px solid var(--c-border);
  margin-bottom: var(--sp-5);
}

.naglowek-tekst { min-width: 0; flex: 1; }

.naglowek-tytul {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
}

.naglowek-podtytul {
  margin-top: var(--sp-1);
  color: var(--c-muted);
  font-size: var(--fs-md);
  line-height: var(--lh-relaxed);
  max-width: 74ch; /* dłuższe wiersze męczą przy czytaniu */
}

.naglowek-akcje {
  display: flex;
  gap: var(--sp-2);
  flex-wrap: wrap;
  align-items: center;
}

/* =====================================================================
 * KARTA
 * Bez cienia — warstwy oddziela obramowanie. Patrz komentarz przy
 * --shadow-* w tokens.css.
 * ===================================================================== */
.karta {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
}

.karta-naglowek {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--c-border);
  flex-wrap: wrap;
}

.karta-tytul {
  font-size: var(--fs-md);
  font-weight: var(--fw-semibold);
  margin: 0;
}

.karta-tresc { padding: var(--sp-4); }
.karta-tresc.is-plaska { padding: 0; } /* gdy w środku jest tabela */

.karta-stopka {
  padding: var(--sp-3) var(--sp-4);
  border-top: 1px solid var(--c-border);
  background: var(--c-surface-2);
  border-radius: 0 0 var(--r-md) var(--r-md);
}

/* Rozwijany formularz w karcie — przycisk, pod którym wyjeżdżają pola
   (np. „Dodaj operatora" w sekcji Operatorzy).

   `<details>`, a NIE panel otwierany skryptem: formularze panelu mają
   działać bez JS (patrz layout/_csrf.html.twig), więc kontrolka, która
   je odsłania, tym bardziej. Rozwijanie robi przeglądarka.

   Marker chowamy, bo rolę przycisku niesie `.btn` na `<summary>`:
   trójkącik obok wyglądałby jak drugi, zepsuty wskaźnik. `display`
   z `.btn` (inline-flex) i tak zdejmuje `list-item`, ale reguła zostaje
   dla przeglądarek, które rysują marker mimo to. */
.karta-rozwijane { margin-bottom: var(--sp-4); }
.karta-rozwijane > summary::-webkit-details-marker { display: none; }
.karta-rozwijane > summary::marker { content: ""; }
.karta-rozwijane > form { margin-top: var(--sp-4); }

/* Karta z sygnałem stanu — lewa krawędź. Używana dla kart stron
   we flocie: poziom widać, zanim przeczyta się treść. */
.karta.is-ok      { border-left: 3px solid var(--c-ok-fg); }
.karta.is-warn    { border-left: 3px solid var(--c-warn-fg); }
.karta.is-bad     { border-left: 3px solid var(--c-bad-fg); }
.karta.is-unknown { border-left: 3px solid var(--c-unknown-fg); }
/* `muted` = NIE OCENIAMY (strona ma wyłączone ostrzeżenia). Wygaszenie robi
   WGŁĘBIENIE, nie przezroczystość. Poprzednia wersja miała `opacity: .72`
   i to był najdroższy błąd kontrastu w panelu: krycie nakłada się na CAŁĄ
   kartę, więc liczy się kompozyt wobec tła STRONY — `--c-faint` schodził
   tam do 2,99:1, a `--c-muted` do 3,47:1 (motyw jasny), mimo że obie pary
   tokenów w tabeli wyglądały poprawnie. Tło z tokena jest mierzalne:
   wszystkie trzy poziomy tekstu są policzone na `--c-surface-2`. */
.karta.is-muted   { border-left: 3px solid var(--c-border-strong); background: var(--c-surface-2); }
/* `connection` = strona „samej łączności” bez problemów. Ten sam wygląd co
   `muted` i z tego samego powodu: to NIE jest poziom ważności, tylko zakres
   obsługi — nie ma prawa do palety semantycznej i nie może konkurować
   o uwagę z czerwienią. Selektor dopisany do rodziny, a nie skopiowany:
   dwie listy deklaracji rozjeżdżają się z czasem. */
.karta.is-connection { border-left: 3px solid var(--c-border-strong); background: var(--c-surface-2); }

/* =====================================================================
 * KARTA STATYSTYKI
 * ===================================================================== */
.stat {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  padding: var(--sp-3) var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  min-width: 0;
}

.stat-etykieta {
  /* Flex, bo etykieta niesie ikonę poziomu — patrz `.stat-ikona` niżej. */
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--c-muted);
}

/* Ikona poziomu przy etykiecie. Kolor sam w sobie nie jest nośnikiem
   informacji (WCAG 1.4.1) — na karcie, która krzyczy czerwienią, kształt
   musi mówić to samo, co barwa. Rozmiar mniejszy od standardowych 16 px,
   bo stoi przy tekście 12 px. */
.stat-ikona { width: 14px; height: 14px; }

.stat-wartosc {
  font-size: var(--fs-2xl);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-tight);
  font-variant-numeric: var(--num);
  letter-spacing: -0.02em;
}

.stat-wartosc .stat-jednostka {
  font-size: var(--fs-md);
  font-weight: var(--fw-normal);
  color: var(--c-muted);
  margin-left: 2px;
}

.stat-opis {
  font-size: var(--fs-xs);
  color: var(--c-faint);
  line-height: 1.4;
}

/* ── Poziom stanu: zmienia BRYŁĘ karty, nie tylko barwę cyfry ──────────
   Wcześniej warianty dokładały wyłącznie `color` na `.stat-wartosc`.
   Skutek dawał się zmierzyć: na ekranie floty karta „Awarie 2” i karta
   „Sprawne 5” miały identyczne tło (białe), identyczną ramkę i identyczny
   padding — były tym samym obiektem wizualnym, różniącym się odcieniem
   jednej cyfry. Wzrok skanujący ekran peryferyjnie („czy coś się pali?”)
   takiej różnicy nie widzi: peryferia rozróżniają kontrast i kształt,
   nie barwę drobnych elementów.

   Dlatego stany wymagające DZIAŁANIA (bad, warn) dostają wypełnienie,
   ramkę w swoim kolorze i pasek sygnałowy przy lewej krawędzi; stan
   „nie wiemy” (unknown) — ciche, wgłębione tło; a stan „w porządku” (ok)
   zostaje płaski. Płaska karta w rzędzie wypełnionych jest informacją
   sama w sobie: tutaj nic nie trzeba robić.

   Pasek na `inset box-shadow`, nie na `border-left` jak w `.karta`: karty
   statystyk stoją w rzędzie obok siebie, a obramowanie zabrałoby 2 px
   z padding-boxu i etykieta karty z paskiem przestałaby stać w jednej
   linii pionowej z etykietami sąsiadów. To nie jest cień w rozumieniu
   tokens.css (warstwa unosząca się nad treścią) — to sygnał stanu.

   UWAGA DLA WYWOŁUJĄCYCH: od tej zmiany poziom `bad`/`warn` przy wartości
   ZERO to fałszywy alarm — karta krzyczy o czymś, czego nie ma. Podawaj
   poziom warunkowo (`liczba > 0 ? 'bad' : 'ok'`), tak jak robią to ekrany
   Indeksacji i Migracji. */
.stat.is-bad {
  background: var(--c-bad-bg);
  border-color: var(--c-bad-border);
  box-shadow: inset 3px 0 0 var(--c-bad-fg);
}

.stat.is-warn {
  background: var(--c-warn-bg);
  border-color: var(--c-warn-border);
  box-shadow: inset 3px 0 0 var(--c-warn-fg);
}

/* Unknown jest wypełniony, ale BEZ paska sygnałowego i w tonie powierzchni
   bezwładnych — `--c-unknown-bg` to w obu motywach dokładnie ta sama wartość
   co `--c-surface-2` (nagłówek tabeli, pole wejściowe). „Nie wiemy” ma odpaść
   od stanów wymagających działania, a nie z nimi konkurować. */
.stat.is-unknown {
  background: var(--c-unknown-bg);
  border-color: var(--c-unknown-border);
}

.stat.is-ok      .stat-wartosc { color: var(--c-ok-fg); }
.stat.is-warn    .stat-wartosc { color: var(--c-warn-fg); }
.stat.is-bad     .stat-wartosc { color: var(--c-bad-fg); }
.stat.is-unknown .stat-wartosc { color: var(--c-unknown-fg); }

.stat.is-bad     .stat-ikona { color: var(--c-bad-fg); }
.stat.is-warn    .stat-ikona { color: var(--c-warn-fg); }
.stat.is-unknown .stat-ikona { color: var(--c-unknown-fg); }

/* Opis na wypełnieniu schodzi o stopień wyżej w hierarchii tekstu.
   Powód jest policzony, nie estetyczny: --c-faint na --c-bad-bg daje
   4,39:1 — poniżej progu AA 4,5:1. --c-muted trzyma 5,58:1 na bad-bg,
   5,80:1 na warn-bg i 5,74:1 na unknown-bg (motyw jasny; ciemny ma
   wszędzie ≥ 6,9:1). */
.stat.is-bad .stat-opis,
.stat.is-warn .stat-opis,
.stat.is-unknown .stat-opis { color: var(--c-muted); }

/* NIE dokładaj tu wariantu układu „jedna karta szersza od reszty”. Był —
   i wypadł razem ze swoim jedynym odbiorcą, pasem czterech kart na ekranie
   Zdrowia floty (patrz zdrowie/_region.html.twig): te cztery liczby były
   ścisłym powtórzeniem liczników w zakładkach filtra, więc zamiast ważyć
   je szerokością, usunięto powtórzenie, a kolor przeniesiono na zakładkę,
   którą DA SIĘ kliknąć. Gdy jedna liczba w rzędzie naprawdę jest ważniejsza
   od pozostałych, najpierw sprawdź, czy w ogóle powinna być osobną kartą. */

/* Zmiana wartości względem poprzedniego okresu. Kierunek NIE przesądza
   o kolorze — spadek pozycji w Google jest dobry, spadek kliknięć zły.
   Kolor nadaje wywołujący, przez .is-ok / .is-bad. */
.stat-delta {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  font-variant-numeric: var(--num);
  color: var(--c-muted);
}

.stat-delta.is-ok  { color: var(--c-ok-fg); }
.stat-delta.is-bad { color: var(--c-bad-fg); }

/* =====================================================================
 * PIGUŁKA STATUSU
 * ===================================================================== */
.pigulka {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: 1px var(--sp-2);
  border-radius: var(--r-full);
  border: 1px solid transparent;
  font-size: var(--fs-xs);
  font-weight: var(--fw-medium);
  line-height: 18px;
  white-space: nowrap;
  vertical-align: middle;
}

.pigulka::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: var(--r-full);
  background: currentColor;
  flex: none;
}

/* Wariant bez kropki — gdy pigułka niesie liczbę albo etykietę tekstową,
   a nie stan (np. „produkty: 12"). */
.pigulka.is-bez-kropki::before { display: none; }

.pigulka.is-ok      { color: var(--c-ok-fg);      background: var(--c-ok-bg);      border-color: var(--c-ok-border); }
.pigulka.is-warn    { color: var(--c-warn-fg);    background: var(--c-warn-bg);    border-color: var(--c-warn-border); }
.pigulka.is-bad     { color: var(--c-bad-fg);     background: var(--c-bad-bg);     border-color: var(--c-bad-border); }
.pigulka.is-unknown { color: var(--c-unknown-fg); background: var(--c-unknown-bg); border-color: var(--c-unknown-border); }
.pigulka.is-info    { color: var(--c-info-fg);    background: var(--c-info-bg);    border-color: var(--c-info-border); }

/* Wariant neutralny — METADANA, nie stan (platforma, wersja, przełącznik
   funkcji). Kształt jest tu nośnikiem znaczenia, nie ozdobą: pigułka
   (`--r-full`) z wypełnieniem = POZIOM STANU, prostokąt (`--r-sm`) bez
   wypełnienia = etykieta. Trzy różnice naraz (kształt, wypełnienie, kontur),
   bo każda z osobna zawodzi — uzasadnienie liczbowe przy `--c-neutral-*`
   w tokens.css.

   `is-muted` renderuje się TAK SAMO, choć przychodzi z innej strony: to
   szósta wartość `level` z kontraktu danych (UI-KONTRAKT §2) i znaczy
   „strona ma wyłączone ostrzeżenia", czyli świadomie JEJ NIE OCENIAMY.
   Skoro nie niesie ważności, nie ma prawa do palety semantycznej — należy
   do rodziny metadanych i dzieli z nią regułę.

   Selektor jest wspólny, a nie skopiowany, celowo: `muted` ma wyglądać
   identycznie na każdym ekranie, więc dwie listy deklaracji mogłyby się
   z czasem rozjechać. Wcześniej reguły nie było wcale — pigułka trafiała
   tylko w bazowe `.pigulka` (kontur `transparent`, kolor dziedziczony)
   i na ekranie Strony „ostrzeżenia wyłączone" renderowały się jako goły
   tekst z kropką, podczas gdy Zdrowie floty mapowało ten sam stan na
   `neutralna` u siebie w szablonie. Ten sam stan domenowy, dwa wyglądy. */
.pigulka.is-neutralna,
.pigulka.is-muted,
.pigulka.is-connection {
  color: var(--c-neutral-fg);
  background: var(--c-neutral-bg);
  border-color: var(--c-neutral-border);
  border-radius: var(--r-sm);
}

.pigulka.is-neutralna::before,
.pigulka.is-muted::before,
.pigulka.is-connection::before { display: none; }

/* =====================================================================
 * WSKAŹNIK ŚWIEŻOŚCI DANYCH  ★ komponent krytyczny
 *
 * Problem, który rozwiązuje: obecny hub miesza w jednym widoku dane
 * pobrane na żywo, dane z cache i snapshot z heartbeatu sprzed godzin.
 * Operator patrzy na plan sprzed miesiąca przekonany, że jest aktualny,
 * i podejmuje na tej podstawie decyzje kosztowe.
 *
 * Reguła: KAŻDY region danych, który nie pochodzi z bazy panelu w tym
 * żądaniu, MUSI mieć ten wskaźnik. Nie jest ozdobą — jest częścią
 * poprawności ekranu.
 *
 * Wskaźnik odpowiada na trzy pytania naraz:
 *   1. SKĄD  — origin: baza panelu / strona klienta / usługa zewnętrzna
 *   2. JAK STARE — wiek, słownie
 *   3. CZY AKTUALNE — czy to świeży odczyt, czy cache po nieudanej próbie
 * ===================================================================== */
.swiezosc {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: 1px var(--sp-2) 1px var(--sp-1);
  border-radius: var(--r-full);
  border: 1px solid var(--c-border);
  background: var(--c-surface-2);
  color: var(--c-muted);
  font-size: var(--fs-xs);
  line-height: 18px;
  white-space: nowrap;
  cursor: help; /* pełne wyjaśnienie jest w title= */
}

.swiezosc-ikona { width: 12px; height: 12px; flex: none; }

.swiezosc-wiek {
  font-variant-numeric: var(--num);
  font-weight: var(--fw-medium);
}

/* Nazwa źródła jest DRUGORZĘDNA wobec wieku, ale hierarchii nie robi tu
   krycie. Warianty niżej podnoszą ten tekst do koloru poziomu i kiedyś
   przygaszały go `opacity: .8/.85` — kompozyt wypadał wtedy na własnym tle
   pigułki na 3,69–4,44:1 w motywie jasnym, czyli poniżej AA na KAŻDYM
   z pięciu wariantów naraz. Hierarchię niesie waga pisma (`--fw-medium`
   na wieku), która kontrastu nie dotyka. */
.swiezosc-zrodlo { color: var(--c-faint); }

/* --- Wariant 1: NA ŻYWO. Odczyt wykonany w tym żądaniu. ------------- */
.swiezosc.is-live {
  color: var(--c-ok-fg);
  background: var(--c-ok-bg);
  border-color: var(--c-ok-border);
}

.swiezosc.is-live .swiezosc-zrodlo { color: var(--c-ok-fg); }

/* --- Wariant 2: Z CACHE. Źródło nie odpowiedziało, pokazujemy ostatnie
   znane dane. To NIE jest błąd — to normalna praca przy offline'owej
   stronie — ale operator musi wiedzieć, że patrzy na przeszłość.
   Dlatego kolor ostrzegawczy, nie czerwony. --------------------------- */
.swiezosc.is-cache {
  color: var(--c-warn-fg);
  background: var(--c-warn-bg);
  border-color: var(--c-warn-border);
}

.swiezosc.is-cache .swiezosc-zrodlo { color: var(--c-warn-fg); }

/* --- Wariant 3: SNAPSHOT. Dane z ostatniego heartbeatu agenta.
   Nie pytaliśmy strony teraz — strona sama zgłosiła to jakiś czas temu.
   Świeży snapshot (≤45 min) jest w porządku, stary wprowadza w błąd. --- */
.swiezosc.is-snapshot {
  color: var(--c-info-fg);
  background: var(--c-info-bg);
  border-color: var(--c-info-border);
}

.swiezosc.is-snapshot .swiezosc-zrodlo { color: var(--c-info-fg); }

/* Eskalacja po wieku — nakładana NA wariant źródła. Progi (45 min / 6 h)
   liczy backend i podaje jako `age_s`; tutaj jest tylko prezentacja.
   Snapshot starszy niż 6 h przestaje być „informacją", a staje się
   ostrzeżeniem, niezależnie od tego, skąd pochodzi. */
.swiezosc.is-stary {
  color: var(--c-warn-fg);
  background: var(--c-warn-bg);
  border-color: var(--c-warn-border);
}

.swiezosc.is-stary .swiezosc-zrodlo { color: var(--c-warn-fg); }

.swiezosc.is-przestarzaly {
  color: var(--c-bad-fg);
  background: var(--c-bad-bg);
  border-color: var(--c-bad-border);
  font-weight: var(--fw-medium);
}

.swiezosc.is-przestarzaly .swiezosc-zrodlo { color: var(--c-bad-fg); }

/* --- Wariant 4: DANE PANELU. Baza lokalna, zawsze aktualna.
   Wskaźnik jest tu celowo najcichszy — to przypadek domyślny i nie ma
   powodu, żeby zajmował uwagę. Renderuj go tylko tam, gdzie obok stoją
   dane z innych źródeł i brak plakietki byłby dwuznaczny. ------------- */
.swiezosc.is-db {
  color: var(--c-faint);
  background: transparent;
  border-color: var(--c-border);
}

/* Powód nieświeżości pod nagłówkiem regionu — pełne zdanie, nie skrót. */
.swiezosc-powod {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  background: var(--c-warn-bg);
  border: 1px solid var(--c-warn-border);
  border-radius: var(--r-sm);
  color: var(--c-warn-fg);
  font-size: var(--fs-sm);
  line-height: var(--lh-normal);
}

/* =====================================================================
 * PRZYCISKI
 * ===================================================================== */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-1);
  min-height: var(--tap);
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--r-sm);
  border: 1px solid var(--c-border-strong);
  background: var(--c-surface);
  color: var(--c-text);
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  line-height: 1.3;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease),
              color var(--t-fast) var(--ease);
}

.btn:hover { background: var(--c-surface-2); color: var(--c-text); }
.btn:active { background: var(--c-surface-3); }

/* Stan nieaktywny. `disabled` na <button> wystarcza; klasa jest dla <a>,
   który nie zna tego atrybutu i musi dostać aria-disabled. */
.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

/* Główne — jedno na ekran. Jeżeli masz dwa, jedno z nich nie jest główne. */
.btn.is-primary {
  background: var(--c-accent);
  border-color: var(--c-accent);
  color: var(--c-accent-fg);
}

.btn.is-primary:hover {
  background: var(--c-accent-hover);
  border-color: var(--c-accent-hover);
  color: var(--c-accent-fg);
}

/* Niebezpieczne — usunięcie, rotacja sekretu, akcja nieodwracalna.
   Obramowanie zamiast wypełnienia: czerwony przycisk przyciąga klik,
   a to ostatnia rzecz, jakiej chcemy przy „Usuń stronę". Wypełnienie
   dostaje dopiero przycisk potwierdzający w modalu. */
.btn.is-danger {
  color: var(--c-bad-fg);
  border-color: var(--c-bad-border);
  background: var(--c-surface);
}

.btn.is-danger:hover {
  background: var(--c-bad-bg);
  border-color: var(--c-bad-fg);
  color: var(--c-bad-fg);
}

.btn.is-danger-solid {
  background: var(--c-bad-fg);
  border-color: var(--c-bad-fg);
  color: var(--c-on-bad);
}

.btn.is-danger-solid:hover { background: var(--c-bad-strong); border-color: var(--c-bad-strong); color: var(--c-on-bad); }

/* Akcja poboczna — „Kod połączenia", „Wyłącz AI", „Konfiguracja", „Anuluj" w modalu.
   RAMKA JEST TU OBOWIĄZKOWA, mimo że styl nazywa się „cichy".
   Wcześniej stało tu `border-color: transparent` + `color: var(--c-muted)`, czyli
   przezroczyste tło, przezroczysta ramka i wyszarzony tekst — przycisk nie do
   odróżnienia od podpisu. Zgłoszone przez właściciela wprost („nie wyglądają jak
   guziki") na ekranie Strony, gdzie „Kod połączenia" i „Wyłącz AI" stoją OBOK
   zwykłego `.btn` („⬆ Aktualizuj agenta") i `.is-danger` („Usuń") — jedyne dwa
   bez obrysu wyglądały na wyłączone. Kosztowało to realny czas w dniu przepinania
   floty, bo „Kod połączenia" jest wtedy akcją pierwszej potrzeby.
   Hierarchię trzyma teraz SAMO TŁO (przezroczyste kontra `--c-surface` zwykłego
   przycisku) i grubość pisma — afordancja klikalności zostaje. Dotyczy 16 miejsc
   nadających `style => quiet` w backendzie i 24 wystąpień w szablonach, więc
   poprawka jest jedna, a nie per przycisk. */
.btn.is-quiet {
  background: transparent;
  border-color: var(--c-border);
  color: var(--c-text);
  font-weight: var(--fw-normal);
}

.btn.is-quiet:hover {
  background: var(--c-surface-2);
  color: var(--c-text);
}

.btn.is-mala {
  min-height: 26px;
  padding: 0 var(--sp-2);
  font-size: var(--fs-sm);
}

.btn-ikona { width: 15px; height: 15px; }

/* Przycisk tylko z ikoną — MUSI mieć aria-label. */
.btn.is-tylko-ikona {
  padding: var(--sp-1);
  width: var(--tap);
}

.btn-grupa {
  display: inline-flex;
  gap: var(--sp-1);
  flex-wrap: wrap;
  align-items: center;
}

/* Kręciołek zajętości wewnątrz przycisku (htmx). */
.btn-spinner {
  width: 13px;
  height: 13px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: var(--r-full);
  animation: obrot 620ms linear infinite;
}

@keyframes obrot { to { transform: rotate(360deg); } }

/* =====================================================================
 * ZAKŁADKI
 * ===================================================================== */
.zakladki {
  display: flex;
  gap: var(--sp-1);
  border-bottom: 1px solid var(--c-border);
  overflow-x: auto;
  scrollbar-width: thin;
}

.zakladka {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border: 0;
  background: transparent;
  color: var(--c-muted);
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  /* Przezroczysta ramka trzyma wysokość — bez niej aktywacja zakładki
     przesuwa treść o 2 px. */
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}

.zakladka:hover { color: var(--c-text); }

.zakladka[aria-selected="true"],
.zakladka[aria-current="page"] {
  color: var(--c-accent);
  border-bottom-color: var(--c-accent);
}

.zakladka-licznik {
  padding: 0 var(--sp-1);
  border-radius: var(--r-full);
  background: var(--c-surface-3);
  color: var(--c-muted);
  font-size: var(--fs-xs);
  font-variant-numeric: var(--num);
  line-height: 16px;
}

/* Licznik niosący STAN, nie tylko liczbę.

   DLACZEGO WYPEŁNIENIE DOSTAJĄ TYLKO `bad` I `warn`
   Kolor ma w pasku triage wskazywać JEDNO miejsce, a nie pięć. Zielone
   „Sprawne 5” świeciłoby równie mocno co czerwone „Awarie 2” i skasowało cały
   zysk — dlatego stany, z którymi nie ma nic do zrobienia, zostają szare.
   To ta sama zasada, na której `.btn.is-danger` jest obwódką, a wypełnienie
   dostaje dopiero przycisk potwierdzający.

   Pary kolor/tekst są celowo TE SAME co w `.nav-licznik` — policzone
   w UI-SYSTEM §2.2 (6,56:1 w jasnym, 6,48:1 w ciemnym). Nie podmieniaj
   `--c-on-bad` na `#fff`: w motywie ciemnym `--c-bad-fg` jest jasnym łososiem
   i biały tekst spada tam do 2,54:1.

   Klasę nakłada szablon WYŁĄCZNIE przy liczniku > 0 — czerwone „Błędy 0”
   byłoby fałszywym alarmem. */
.zakladka-licznik.is-bad {
  background: var(--c-bad-fg);
  color: var(--c-on-bad);
  font-weight: var(--fw-semibold);
}

.zakladka-licznik.is-warn {
  background: var(--c-warn-fg);
  color: var(--c-on-warn);
  font-weight: var(--fw-semibold);
}

/* =====================================================================
 * TABELA DANYCH
 * ===================================================================== */
.tabela {
  width: 100%;
  font-size: var(--fs-sm);
}

.tabela th,
.tabela td {
  padding: var(--sp-2) var(--sp-3);
  text-align: left;
  vertical-align: middle;
  border-bottom: 1px solid var(--c-border);
}

.tabela thead th {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  background: var(--c-surface-2);
  color: var(--c-muted);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
}

.tabela tbody tr:hover { background: var(--c-surface-2); }

.tabela tbody tr:last-child td { border-bottom: 0; }

/* Wiersz wyciszony — strona uśpiona, temat wykorzystany. Treść zostaje
   czytelna (nie schodzimy poniżej AA), traci tylko pierwszeństwo. */
.tabela tr.is-muted td,
.tabela tr.is-connection td { color: var(--c-muted); }

.tabela td.is-akcje {
  text-align: right;
  white-space: nowrap;
  width: 1%; /* kolumna zwęża się do zawartości */
}

/* --- Nagłówek sortowalny --------------------------------------------
   Sortowanie to link (hx-get), a nie JS na kliknięciu — dzięki temu
   działa w nowej karcie, da się je zakładkować i nie wymaga skryptu.
   Kierunek niesie aria-sort, więc czytnik ekranu też go ogłasza. */
.tabela th.is-sortowalna { padding: 0; }

.tabela th.is-sortowalna > a,
.tabela th.is-sortowalna > button {
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  width: 100%;
  padding: var(--sp-2) var(--sp-3);
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-transform: inherit;
  letter-spacing: inherit;
  text-decoration: none;
  cursor: pointer;
}

.tabela th.is-sortowalna > a:hover,
.tabela th.is-sortowalna > button:hover {
  background: var(--c-surface-3);
  color: var(--c-text);
}

.sort-strzalka {
  width: 12px;
  height: 12px;
  /* 0 w spoczynku (miejsce zarezerwowane zawsze — nagłówki nie skaczą),
     0.6 przy hoverze nagłówka, 1 gdy kolumna jest sortowana. 0.6 nie jest
     wartością estetyczną: poprzednie 0.45 dawało kompozyt 2,82:1 na
     --c-surface-3, poniżej progu 3:1 dla elementów nietekstowych. */
  opacity: 0;
  transition: opacity var(--t-fast) var(--ease);
}

.tabela th.is-sortowalna:hover .sort-strzalka { opacity: 0.6; }

.tabela th[aria-sort="ascending"]  .sort-strzalka,
.tabela th[aria-sort="descending"] .sort-strzalka { opacity: 1; }

.tabela th[aria-sort="descending"] .sort-strzalka { transform: rotate(180deg); }

.tabela th[aria-sort] > a,
.tabela th[aria-sort] > button { color: var(--c-text); }

/* Podpis tabeli — kontekst i świeżość. `caption-side: top`, bo to
   nagłówek treści, nie przypis. */
.tabela caption {
  caption-side: top;
  padding: var(--sp-2) var(--sp-3);
  text-align: left;
  color: var(--c-muted);
  font-size: var(--fs-xs);
}

.tabela-stopka {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border-top: 1px solid var(--c-border);
  background: var(--c-surface-2);
  font-size: var(--fs-xs);
  color: var(--c-muted);
  flex-wrap: wrap;
}

/* =====================================================================
 * PAGINACJA
 * ===================================================================== */
.paginacja {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-sm);
  color: var(--c-muted);
}

.paginacja-zakres { font-variant-numeric: var(--num); }

/* =====================================================================
 * STAN PUSTY
 * Zawsze mówi, CO ZROBIĆ, żeby przestało być pusto. „Brak danych"
 * bez podpowiedzi to nie jest stan pusty, to jest ślepy zaułek.
 * ===================================================================== */
.pusto {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--sp-2);
  padding: var(--sp-7) var(--sp-4);
  color: var(--c-muted);
}

.pusto-ikona {
  width: 28px;
  height: 28px;
  color: var(--c-faint);
  margin-bottom: var(--sp-1);
}

.pusto-tytul {
  font-size: var(--fs-base);
  font-weight: var(--fw-semibold);
  color: var(--c-text);
}

.pusto-podpowiedz {
  max-width: 52ch;
  font-size: var(--fs-sm);
  line-height: var(--lh-relaxed);
}

.pusto-akcje { margin-top: var(--sp-2); }

/* Wariant kompaktowy — pusty region w środku karty, nie cały ekran. */
.pusto.is-kompakt { padding: var(--sp-5) var(--sp-4); }

/* =====================================================================
 * STAN ŁADOWANIA — SZKIELET
 * Szkielet zamiast kręciołka, bo utrzymuje wysokość regionu: treść
 * nie przeskakuje po doczytaniu. Kręciołek zostaje dla przycisków.
 * ===================================================================== */
.szkielet {
  background: linear-gradient(
    90deg,
    var(--c-surface-2) 25%,
    var(--c-surface-3) 37%,
    var(--c-surface-2) 63%
  );
  background-size: 400% 100%;
  animation: szkielet-puls 1.4s ease-in-out infinite;
  border-radius: var(--r-sm);
  /* Element dekoracyjny — treść dla czytnika niesie aria-busy
     na kontenerze regionu. */
}

@keyframes szkielet-puls {
  0%   { background-position: 100% 50%; }
  100% { background-position: 0 50%; }
}

.szkielet-linia { height: 11px; margin-bottom: var(--sp-2); }
.szkielet-linia.is-krotka  { width: 40%; }
.szkielet-linia.is-srednia { width: 68%; }
.szkielet-linia.is-dluga   { width: 92%; }
.szkielet-tytul { height: 15px; width: 34%; margin-bottom: var(--sp-3); }
.szkielet-blok { height: 76px; }

.ladowanie {
  padding: var(--sp-4);
}

.ladowanie-etykieta {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-3);
  color: var(--c-muted);
  font-size: var(--fs-sm);
}

/* Wiersze szkieletu w tabeli — zachowują siatkę kolumn. */
.tabela .szkielet-linia { margin: 2px 0; }

/* =====================================================================
 * BANER ALERTU
 * Trwały komunikat w treści strony (nie znika sam). Toast jest dla
 * wyniku akcji, baner dla stanu.
 * ===================================================================== */
.baner {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  background: var(--c-surface-2);
  color: var(--c-text);
  font-size: var(--fs-md);
  line-height: var(--lh-normal);
}

.baner-ikona { width: 17px; height: 17px; margin-top: 1px; flex: none; }
.baner-tresc { flex: 1; min-width: 0; }

.baner-tytul {
  font-weight: var(--fw-semibold);
  margin-bottom: 2px;
}

.baner-opis {
  color: var(--c-muted);
  font-size: var(--fs-sm);
  line-height: var(--lh-relaxed);
}

.baner-akcje {
  display: flex;
  gap: var(--sp-2);
  margin-top: var(--sp-2);
  flex-wrap: wrap;
}

.baner.is-ok   { background: var(--c-ok-bg);   border-color: var(--c-ok-border); }
.baner.is-ok   .baner-ikona, .baner.is-ok .baner-tytul   { color: var(--c-ok-fg); }
.baner.is-warn { background: var(--c-warn-bg); border-color: var(--c-warn-border); }
.baner.is-warn .baner-ikona, .baner.is-warn .baner-tytul { color: var(--c-warn-fg); }
.baner.is-bad  { background: var(--c-bad-bg);  border-color: var(--c-bad-border); }
.baner.is-bad  .baner-ikona, .baner.is-bad .baner-tytul  { color: var(--c-bad-fg); }
.baner.is-info { background: var(--c-info-bg); border-color: var(--c-info-border); }
.baner.is-info .baner-ikona, .baner.is-info .baner-tytul { color: var(--c-info-fg); }

/* Treść banera zostaje w kolorze tekstu — kolorujemy ikonę i tytuł.
   Cały akapit w kolorze semantycznym jest męczący i gorzej się czyta. */

/* =====================================================================
 * PASEK POSTĘPU
 * ===================================================================== */
.postep {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
}

.postep-naglowek {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-2);
  font-size: var(--fs-sm);
}

.postep-etykieta { color: var(--c-muted); }

.postep-wartosc {
  font-variant-numeric: var(--num);
  font-weight: var(--fw-medium);
}

.postep-tor {
  height: 6px;
  border-radius: var(--r-full);
  background: var(--c-surface-3);
  overflow: hidden;
}

.postep-wypelnienie {
  height: 100%;
  border-radius: var(--r-full);
  background: var(--c-accent);
  transition: width var(--t-med) var(--ease);
}

.postep.is-ok   .postep-wypelnienie { background: var(--c-ok-fg); }
.postep.is-warn .postep-wypelnienie { background: var(--c-warn-fg); }
.postep.is-bad  .postep-wypelnienie { background: var(--c-bad-fg); }

.postep-opis {
  font-size: var(--fs-xs);
  color: var(--c-faint);
}

/* Postęp nieokreślony — trwa, ale nie znamy udziału. */
.postep.is-nieokreslony .postep-wypelnienie {
  width: 35% !important;
  animation: postep-suwa 1.3s ease-in-out infinite;
}

@keyframes postep-suwa {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(320%); }
}

/* =====================================================================
 * FORMULARZ
 * ===================================================================== */
.pole {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  margin-bottom: var(--sp-4);
}

.pole-etykieta {
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  color: var(--c-text);
}

.pole-wymagane {
  color: var(--c-bad-fg);
  margin-left: 2px;
}

/* Opis pomocniczy NAD kontrolką, nie pod nią. Powód: pod kontrolką
   ląduje komunikat błędu, a operator czyta opis PRZED wypełnieniem,
   nie po. */
.pole-opis {
  font-size: var(--fs-sm);
  color: var(--c-muted);
  line-height: var(--lh-relaxed);
  max-width: 78ch;
}

.pole-kontrolka,
.pole input[type="text"],
.pole input[type="url"],
.pole input[type="email"],
.pole input[type="password"],
.pole input[type="number"],
.pole input[type="date"],
.pole input[type="time"],
.pole input[type="search"],
.pole select,
.pole textarea {
  width: 100%;
  min-height: var(--tap);
  padding: var(--sp-1) var(--sp-2);
  background: var(--c-surface);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--r-sm);
  color: var(--c-text);
  font-size: var(--fs-md);
  transition: border-color var(--t-fast) var(--ease),
              box-shadow var(--t-fast) var(--ease);
}

.pole textarea {
  min-height: 84px;
  padding: var(--sp-2);
  line-height: var(--lh-relaxed);
  resize: vertical;
}

.pole textarea.is-mono {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
}

.pole select {
  /* Strzałka jako data-URI — zero zapytań sieciowych, a panel działa
     w sieci wewnętrznej. Kolor stroke musi być stały (SVG w tle nie
     dziedziczy currentColor), więc bierzemy średni szary czytelny
     w obu motywach. */
  appearance: none;
  padding-right: var(--sp-6);
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5L6 8l3.5-3.5' fill='none' stroke='%237e8693' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--sp-2) center;
}

.pole input::placeholder,
.pole textarea::placeholder {
  color: var(--c-faint);
}

.pole input:focus,
.pole select:focus,
.pole textarea:focus {
  outline: none; /* zastępujemy pierścieniem poniżej — mniej agresywnym */
  border-color: var(--c-accent);
  box-shadow: 0 0 0 3px var(--c-accent-soft);
}

.pole input:disabled,
.pole select:disabled,
.pole textarea:disabled,
.pole input[readonly],
.pole textarea[readonly] {
  background: var(--c-surface-2);
  color: var(--c-muted);
  cursor: not-allowed;
}

/* Pole tylko do odczytu z powodu — np. prompt nadpisany przez zestaw
   z biblioteki. Powód MUSI być podany obok, inaczej wygląda na awarię. */
.pole.is-zablokowane .pole-etykieta { color: var(--c-muted); }

/* --- Błąd walidacji -------------------------------------------------
   Kolor nie jest jedynym nośnikiem informacji: jest ikona, tekst
   i aria-invalid. */
.pole.is-blad input,
.pole.is-blad select,
.pole.is-blad textarea,
.pole input[aria-invalid="true"],
.pole select[aria-invalid="true"],
.pole textarea[aria-invalid="true"] {
  border-color: var(--c-bad-fg);
}

.pole.is-blad input:focus,
.pole.is-blad textarea:focus,
.pole.is-blad select:focus {
  box-shadow: 0 0 0 3px var(--c-bad-bg);
}

.pole-blad {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-1);
  color: var(--c-bad-fg);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  line-height: var(--lh-normal);
}

.pole-blad-ikona { width: 14px; height: 14px; margin-top: 2px; flex: none; }

/* --- Checkbox i radio ----------------------------------------------- */
.pole-check {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  margin-bottom: var(--sp-2);
}

.pole-check input[type="checkbox"],
.pole-check input[type="radio"] {
  width: 15px;
  height: 15px;
  margin: 3px 0 0;
  accent-color: var(--c-accent);
  flex: none;
  cursor: pointer;
}

.pole-check label { cursor: pointer; font-size: var(--fs-md); }

.pole-check .pole-opis { margin-top: 1px; }

/* --- Grupa pól ------------------------------------------------------ */
.pole-grupa {
  border: 0;
  padding: 0;
  margin: 0 0 var(--sp-5);
}

.pole-grupa > legend {
  padding: 0 0 var(--sp-1);
  font-size: var(--fs-base);
  font-weight: var(--fw-semibold);
}

.pole-rzad {
  display: grid;
  gap: var(--sp-4);
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}

.pole-rzad .pole { margin-bottom: 0; }

/* Pole wąskie — liczba, godzina. Bez tego input na liczbę zajmuje
   całą szerokość formularza i wygląda absurdalnie. */
.pole.is-waskie input,
.pole.is-waskie select { max-width: 160px; }

.form-akcje {
  display: flex;
  gap: var(--sp-2);
  align-items: center;
  padding-top: var(--sp-4);
  border-top: 1px solid var(--c-border);
  flex-wrap: wrap;
}

/* =====================================================================
 * MODAL POTWIERDZENIA
 * Oparty na <dialog> — natywna pułapka focusu, obsługa Esc i warstwa
 * górna bez zarządzania z-index. Alternatywa (własny div) wymaga ~80
 * linii JS do samej dostępności.
 * ===================================================================== */
.modal {
  padding: 0;
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-surface);
  color: var(--c-text);
  box-shadow: var(--shadow-lg);
  max-width: min(480px, calc(100vw - 2rem));
  width: 100%;
}

.modal::backdrop {
  background: var(--c-overlay);
}

.modal-naglowek {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-4) var(--sp-2);
}

.modal-ikona {
  width: 20px;
  height: 20px;
  flex: none;
  margin-top: 2px;
  color: var(--c-warn-fg);
}

.modal.is-danger .modal-ikona { color: var(--c-bad-fg); }

.modal-tytul {
  font-size: var(--fs-lg);
  font-weight: var(--fw-semibold);
  line-height: var(--lh-tight);
}

.modal-tresc {
  padding: 0 var(--sp-4) var(--sp-4);
  color: var(--c-muted);
  font-size: var(--fs-md);
  line-height: var(--lh-relaxed);
}

/* Skutki uboczne akcji — schemat kaskaduje (usunięcie strony kasuje
   plany), więc operator musi zobaczyć KONKRETNĄ listę, a nie „na pewno?". */
.modal-skutki {
  margin-top: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  background: var(--c-bad-bg);
  border: 1px solid var(--c-bad-border);
  border-radius: var(--r-sm);
  color: var(--c-bad-fg);
  font-size: var(--fs-sm);
}

.modal-skutki ul { list-style: disc; padding-left: var(--sp-4); margin-top: var(--sp-1); }

.modal-stopka {
  display: flex;
  justify-content: flex-end;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  border-top: 1px solid var(--c-border);
  background: var(--c-surface-2);
  border-radius: 0 0 var(--r-lg) var(--r-lg);
}

/* =====================================================================
 * TOASTY
 * Kontener #komunikaty ma role="status" aria-live="polite" (patrz
 * base.html.twig). Akcje htmx dorzucają tu treść przez
 * hx-swap-oob="beforeend", dzięki czemu wynik akcji, która zmieniła
 * tylko fragment tabeli, jest ogłaszany przez czytnik ekranu.
 * ===================================================================== */
#komunikaty {
  position: fixed;
  right: var(--sp-4);
  bottom: var(--sp-4);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  width: min(380px, calc(100vw - 2rem));
  /* Kontener nie łapie kliknięć — tylko same toasty. Inaczej niewidoczny
     pas w rogu ekranu blokowałby przyciski pod spodem. */
  pointer-events: none;
}

.toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  padding: var(--sp-3);
  border: 1px solid var(--c-border);
  border-left-width: 3px;
  border-radius: var(--r-md);
  background: var(--c-surface);
  box-shadow: var(--shadow-lg);
  font-size: var(--fs-sm);
  line-height: var(--lh-normal);
  animation: toast-wjazd var(--t-med) var(--ease);
}

@keyframes toast-wjazd {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}

.toast-ikona { width: 16px; height: 16px; flex: none; margin-top: 1px; }
.toast-tresc { flex: 1; min-width: 0; }
.toast-tytul { font-weight: var(--fw-medium); }

.toast-opis {
  color: var(--c-muted);
  font-size: var(--fs-xs);
  margin-top: 2px;
}

/* Zamknięcie toastu: rysunek 20×20, cel kliknięcia `--tap` (32 px) przez
   pseudoelement. Toast znika sam, więc chybienie w krzyżyk nie jest awarią —
   ale to jedyny sposób zamknięcia go RĘCZNIE, a stoi w rogu ekranu, gdzie
   celuje się najgorzej. Kontener toastów nie przycina zawartości, więc cel
   może rosnąć symetrycznie. */
.toast-zamknij {
  position: relative;
  flex: none;
  padding: 0;
  width: 20px;
  height: 20px;
  border: 0;
  background: transparent;
  color: var(--c-faint);
  cursor: pointer;
  border-radius: var(--r-sm);
  display: flex;
  align-items: center;
  justify-content: center;
}

.toast-zamknij::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--tap);
  height: var(--tap);
  transform: translate(-50%, -50%);
}

.toast-zamknij:hover { color: var(--c-text); background: var(--c-surface-2); }

.toast.is-success { border-left-color: var(--c-ok-fg); }
.toast.is-success .toast-ikona { color: var(--c-ok-fg); }
.toast.is-info    { border-left-color: var(--c-info-fg); }
.toast.is-info    .toast-ikona { color: var(--c-info-fg); }
.toast.is-warn    { border-left-color: var(--c-warn-fg); }
.toast.is-warn    .toast-ikona { color: var(--c-warn-fg); }
.toast.is-error   { border-left-color: var(--c-bad-fg); }
.toast.is-error   .toast-ikona { color: var(--c-bad-fg); }

/* Błąd zostaje na ekranie do zamknięcia — komunikat o nieudanej
   publikacji nie może zniknąć, zanim operator go przeczyta.
   Znikanie po czasie dotyczy wyłącznie potwierdzeń. */
.toast.is-trwaly { border-style: solid; }

/* =====================================================================
 * BOX BŁĘDU REGIONU
 * Region, który się nie załadował. Inaczej niż baner: zastępuje treść,
 * więc musi zaproponować wyjście (ponów / zmień konfigurację).
 * ===================================================================== */
.blad-regionu {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--sp-2);
  padding: var(--sp-6) var(--sp-4);
  color: var(--c-muted);
}

.blad-regionu-ikona { width: 26px; height: 26px; color: var(--c-bad-fg); }

.blad-regionu-tytul {
  font-size: var(--fs-base);
  font-weight: var(--fw-semibold);
  color: var(--c-text);
}

.blad-regionu-opis { max-width: 56ch; font-size: var(--fs-sm); line-height: var(--lh-relaxed); }

.blad-regionu-kod {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--c-faint);
}

/* =====================================================================
 * DROBIAZGI WSPÓŁDZIELONE
 * ===================================================================== */

/* Klucz→wartość: metadane strony, szczegóły planu. */
.klucz-wartosc {
  display: grid;
  grid-template-columns: minmax(120px, auto) 1fr;
  gap: var(--sp-1) var(--sp-3);
  font-size: var(--fs-sm);
}

.klucz-wartosc dt { color: var(--c-muted); }
.klucz-wartosc dd { margin: 0; min-width: 0; }

/* Separator poziomy w karcie. */
.rozdzielacz {
  height: 1px;
  background: var(--c-border);
  border: 0;
  margin: var(--sp-4) 0;
}

/* Kropka stanu bez etykiety — tylko tam, gdzie znaczenie niesie sąsiedni
   tekst. Samodzielnie ZAWSZE z .tylko-czytnik. */
.kropka {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: var(--r-full);
  flex: none;
}

.kropka.is-ok      { background: var(--c-ok-fg); }
.kropka.is-warn    { background: var(--c-warn-fg); }
.kropka.is-bad     { background: var(--c-bad-fg); }
.kropka.is-unknown { background: var(--c-unknown-fg); }
.kropka.is-info    { background: var(--c-info-fg); }

/* Odcisk sekretu — nigdy sama wartość. */
.odcisk {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  padding: 1px var(--sp-1);
  background: var(--c-surface-2);
  border: 1px solid var(--c-border);
  border-radius: var(--r-sm);
  color: var(--c-muted);
  letter-spacing: 0.02em;
}

/* ══════════════════════════════════════════════════════════════════════
   KONSOLA CLAUDE CODE (`konsola.html.twig`, `konsola/_dziennik.html.twig`)

   Ekran czyta się jak rozmowa, więc rola musi być rozpoznawalna ZANIM
   operator przeczyta treść — kolorem i lewą krawędzią, nie samą etykietą.
   Wszystko na tokenach motywu, żeby tryb ciemny nie wymagał drugiego
   zestawu reguł: `--c-*` są już przełączane wyżej w tym pliku.

   PIĘĆ RÓL, KAŻDA MÓWI CO INNEGO
     operator  — co kazałeś (akcent, wyróżnione tło)
     claude    — odpowiedź (neutralne, największy kontrast tekstu)
     narzedzie — co robi na serwerze (przygaszone; tych wierszy jest najwięcej)
     blad      — awaria (czerwone)
     koniec    — podsumowanie przebiegu (zielone)
   ══════════════════════════════════════════════════════════════════════ */

.konsola-pasek {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--sp-3);
	margin-bottom: var(--sp-3);
}

.konsola-wpisy {
	display: flex;
	flex-direction: column;
	gap: var(--sp-2);
	max-height: 60vh;
	overflow-y: auto;
	padding: var(--sp-3);
	background: var(--c-surface-2);
	border: 1px solid var(--c-border);
	border-radius: var(--r-2, 8px);
}

.konsola-wpis {
	display: grid;
	grid-template-columns: 6.5rem 1fr;
	gap: var(--sp-3);
	align-items: start;
	padding: var(--sp-2) var(--sp-3);
	border-left: 3px solid var(--c-border-strong);
	border-radius: 0 var(--r-1, 4px) var(--r-1, 4px) 0;
	background: var(--c-surface);
}

.konsola-rola {
	font-size: 0.78rem;
	font-weight: 600;
	letter-spacing: 0.02em;
	text-transform: uppercase;
	color: var(--c-muted);
	padding-top: 0.15rem;
}

/* Treść bywa wielolinijkowa (wyjście polecenia, fragment kodu), więc łamiemy
   ją zamiast puszczać poziomy pasek przewijania przez całą rozmowę. */
.konsola-tresc {
	margin: 0;
	font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
	font-size: 0.88rem;
	line-height: 1.5;
	white-space: pre-wrap;
	overflow-wrap: anywhere;
	color: var(--c-text);
}

.konsola-wpis.is-operator {
	border-left-color: var(--c-accent);
	background: var(--c-accent-soft);
}
.konsola-wpis.is-operator .konsola-rola { color: var(--c-accent); }

.konsola-wpis.is-claude { border-left-color: var(--c-border-strong); }

/* Wierszy narzędziowych jest najwięcej i są tłem dla właściwej odpowiedzi —
   dlatego mniejsze i przygaszone. Gdyby krzyczały tak samo jak odpowiedź,
   rozmowa stałaby się nieczytelna dokładnie wtedy, gdy dzieje się najwięcej. */
.konsola-wpis.is-narzedzie {
	border-left-color: var(--c-info-border);
	background: var(--c-info-bg);
}
.konsola-wpis.is-narzedzie .konsola-rola { color: var(--c-info-fg); }
.konsola-wpis.is-narzedzie .konsola-tresc {
	font-size: 0.82rem;
	color: var(--c-info-fg);
}

/* Ponowienie to NIE błąd — praca trwa, tylko czeka na dostawcę. Kolor
   ostrzegawczy zamiast czerwonego, żeby operator nie czytał zaplanowanego
   odstępu jako awarii i nie przerywał polecenia, które samo dojdzie do końca. */
.konsola-wpis.is-ponowienie {
	border-left-color: var(--c-warn-border);
	background: var(--c-warn-bg);
}
.konsola-wpis.is-ponowienie .konsola-rola { color: var(--c-warn-fg); }
.konsola-wpis.is-ponowienie .konsola-tresc {
	font-size: 0.82rem;
	color: var(--c-warn-fg);
}

.konsola-wpis.is-blad {
	border-left-color: var(--c-bad-border);
	background: var(--c-bad-bg);
}
.konsola-wpis.is-blad .konsola-rola,
.konsola-wpis.is-blad .konsola-tresc { color: var(--c-bad-fg); }

.konsola-wpis.is-koniec {
	border-left-color: var(--c-ok-border);
	background: var(--c-ok-bg);
}
.konsola-wpis.is-koniec .konsola-rola { color: var(--c-ok-fg); }

.konsola-pole {
	font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
	font-size: 0.9rem;
}

@media (max-width: 40rem) {
	/* Na telefonie kolumna etykiet zjadałaby połowę szerokości treści. */
	.konsola-wpis { grid-template-columns: 1fr; gap: var(--sp-1); }
	.konsola-rola { padding-top: 0; }
}
