/* ============================================================
   Kropka — prymitywy Design Systemu dla treści stron

   ŹRÓDŁO: docs/Kropka Design System/components/ (Button, Select,
           Card, SegmentedControl, EmptyState) + guidelines/.

   Pierwszym konsumentem jest ekran wiadomości (druga wyspa DS po
   pasku bocznym, openspec: redesign-messages-unified). Plik żyje
   osobno od bloku <style> w templates/base.html celowo: tamten ma
   ~1600 linii i jest punktem kolizji równoległych sesji, a te
   prymitywy mają być pierwszą rzeczą, po którą sięgną kolejne
   wyspy (dashboard, kartoteka, kalendarz).

   REGUŁY: wyłącznie tokeny — ani jednej wartości hex/rgb wpisanej
   wprost. Tekst na niebieskim to --blue-050 (a nie „biały"), bo
   --surface przełącza się na ciemny w body.dark, a skala blue nie.
   ============================================================ */

/* ── Wyspa DS: cofnięcie resetu ramek Tailwinda ──────────────
   MUSI STAĆ PRZED DEFINICJAMI PRYMITYWÓW. To nie jest kwestia stylu
   zapisu, tylko jedyne miejsce, w którym ta reguła działa poprawnie.

   Preflight Tailwinda nadaje KAŻDEMU elementowi
   `border-width: 0; border-style: solid; border-color: <gray-200>`.
   Szerokość jest zerowa, więc nic nie widać — ale `getComputedStyle`
   zwraca `solid` i szarość, podczas gdy makieta (bez resetu) zwraca
   `none` i `currentColor`. Zmierzone: 18 takich pozycji na kontrakcie
   listy tablic i 20 na kontrakcie statystyk, na każdej kombinacji
   viewport×motyw. Ten sam koszt doliczał się do każdej kolejnej wyspy.

   Dlaczego specyficzność KLASOWA, a nie uniwersalna: kontrakt kolejności
   z templates/base.html mówi, że Tailwind z CDN wstrzykuje się PO arkuszu
   wyspy. Reguła `:where(...) *` miałaby (0,0,0), zremisowała z preflightem
   i przegrała kolejnością. `.kp-island *` ma (0,1,0) i wygrywa niezależnie
   od kolejności.

   Dlaczego PRZED prymitywami: `.kp-card`, `.kp-prow`, `.kp-modal-box` oraz
   wzorce stron mają tę samą specyficzność (0,1,0). Stojąc niżej — w tym
   pliku albo w arkuszu wyspy, który wczytuje się po katalogu — wygrywają
   kolejnością i zachowują swoje hairline'y. Przesunięcie tej reguły w dół
   pliku zdjęłoby obramowania kartom.

   `currentColor` to wartość początkowa `border-color` w CSS — dokładnie to,
   co raportuje makieta.

   Sam korzeń przestawia jeszcze trzy rzeczy DZIEDZICZONE, z tego samego
   powodu: preflight narzuca `body` krój Tailwinda, `line-height: 1.5`
   i `color: gray-900`, a makiety nie mają resetu, więc raportują krój DS,
   `normal` i `--ink`. Zmierzone na kontrakcie księgowości, gdzie wyspa tego
   nie ustawiała: 8 rozjazdów na `lineHeight`, 7 na `color` i 6 na
   `fontFamily` — wszystkie z samego dziedziczenia. Wysokość wiersza inną niż
   `normal` deklaruje się tam, gdzie makieta też ją deklaruje (akapity, tytuły
   wielowierszowe), a nie na korzeniu.                                       */

.kp-island {
  font-family: var(--font-body);
  color: var(--ink);
  line-height: normal;
}

.kp-island * {
  border-style: none;
  border-color: currentColor;
}

/* ── Typografia ─────────────────────────────────────────────── */

/* Krój display bez stopnia.
 *
 * ⚠️ TA KLASA CELOWO NIE MA `font-size` — I MA GO NIE DOSTAĆ ⚠️
 *
 * Kusi, żeby jej rozmiar dopisać: 39 elementów w produkcie nosi ją bez żadnego
 * innego źródła rozmiaru i renderuje się przez to gołym preflightem (16px).
 * Sprawdzone i odrzucone — rozmiary nadawane tej klasie ręcznie idą od 12.5px
 * do 42px (14.5 ×34, 15 ×28, 13 ×26, 30 ×19, 26 ×15), więc nie ma tu żadnego
 * środka do obrania. Jedna wartość domyślna zepsułaby nagłówki w nowy sposób:
 * `<h1>` chce 30px, `<h2>` 19px, a jedna liczba nie jest ani jednym, ani drugim.
 *
 * To jest rola KROJU, nie stopnia — tak samo jak `.kp-num` jest rolą wariantu
 * cyfr. Rozmiar należy do klasy STOPNIA (`.kp-h1`, `.kp-h2`) albo do strony.
 *
 * Z tych 39 elementów 38 to `<h1>` i `<h2>`; dostały stopień, nie rozmiar
 * wpisany przy elemencie. Pilnuje tego strażnik w `tests/test_ds_stylesheets.py`. */
.kp-display {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: -0.5px;
  color: var(--ink);
}

/* Tytuł STRONY — najwyższy stopień skali.
 *
 * ⚠️ DO 2026-09-06 TEGO STOPNIA NIE BYŁO, A `<h1 class="kp-display">`
 * WYSTĘPOWAŁO 21 RAZY ⚠️
 *
 * Powtórka historii `.kp-h2` sprzed dnia: nagłówek najwyższego poziomu nosił
 * sam krój, więc renderował się w rozmiarze preflightu (16px) — czyli tytuł
 * strony był tej samej wielkości co tekst pod nim. Znalazło to policzenie
 * elementów bez źródła rozmiaru, nie oglądanie stron.
 *
 * Wartości z makiety (`Kropka PPP.html`), nie wymyślone:
 * `.kp-h1` 30px/−0.7px/1.05. Interlinia jest tu WPISANA, w odróżnieniu od
 * `.kp-h2` — makieta deklaruje ją tylko przy tym stopniu, bo przy 30px
 * `normal` daje odstęp, który rozrywa dwuwierszowy tytuł. */
.kp-h1 {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 30px;
  line-height: 1.05;
  letter-spacing: -0.7px;
  color: var(--ink);
}

/* Nagłówek SEKCJI — stopień niżej niż tytuł strony.
 *
 * ⚠️ DO 2026-09-05 TA KLASA NIE ISTNIAŁA, A BYŁA UŻYWANA W OŚMIU MIEJSCACH ⚠️
 *
 * `<h2 class="kp-h2">` renderowało się wtedy gołym preflightem Tailwinda
 * (Inter 400/16px): wyglądało jak nagłówek i nie robiło nic, więc żadna
 * weryfikacja wzrokowa tego nie łapała. Znalazł to dopiero pomiar zgodności
 * z makietą (`dsprobe`, kontrakt `PPP-Dzienniki`), który porównuje wartości,
 * a nie wrażenie.
 *
 * Wartości wzięte z makiety, nie wymyślone: `Kropka PPP.html` daje skalę
 * `.kp-h1` 30px/-0.7px · `.kp-h2` 19px/-0.4px · `.kp-h3` 14.5px/-0.25px.
 * Rozmiar jest tu WPISANY, w odróżnieniu od `.kp-display`, które ustala sam
 * krój i wagę — bo „h2" nazywa STOPIEŃ, a stopień bez rozmiaru nie znaczy nic.
 *
 * ⚠️ `.kp-h3` DOSZEDŁ 2026-09-10 — PO TRZECIEJ KOPII, NIE PO DRUGIEJ ⚠️
 *
 * Poprzednia wersja tego komentarza zapowiadała awans „przy drugim
 * konsumencie" i nazywała `.ppp-dz-tytul` jedynym. Zapowiedź nie zadziałała:
 * wzorzec rozszedł się na TRZY bliźniacze nazwy w jednym arkuszu wyspy
 * (`static/ppp/dzienniki.css`) — `.ppp-dz-tytul`, `.ppp-dz-karty-tytul`
 * (obie bajt w bajt identyczne) i `.ppp-dz-dzien-tytul` (ta sama skala,
 * rozmiar przykręcony do 13px). Cztery miejsca w znacznikach, trzy definicje,
 * jeden wzorzec.
 *
 * Wniosek na przyszłość: komentarz z instrukcją „drugi konsument ma
 * PRZENIEŚĆ" jest słabszy niż wygoda dopisania kolejnej lokalnej nazwy.
 * Wzorzec, o którym wiadomo, że będzie miał kolejnych konsumentów, warto
 * awansować od razu — koszt awansu jest stały, a koszt sprzątania rośnie
 * z każdą kopią.
 *
 * Dziś oba warianty 14.5px biorą `.kp-h3` z katalogu, a nagłówek dnia zawęża
 * go do 13px pod nazwą strony (`.ppp-dz-dzien-tytul` — jedyna właściwość,
 * jaka jej została). Czwarty konsument tej skali stoi jeszcze poza tą
 * konsolidacją: `.lekcja-*` w `static/journal/lekcja.css` (14.5px/-0.25px,
 * z pomiaru `dsprobe`) — do przepisania osobno, razem z przemiarem kontraktu
 * `Lekcja`.
 */
.kp-h2 {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 19px;
  line-height: normal;
  letter-spacing: -0.4px;
  color: var(--ink);
}

.kp-h3 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 14.5px;
  line-height: normal;
  letter-spacing: -0.25px;
  color: var(--ink);
}

/* Tekst treściowy.
 *
 * ⚠️ ROZMIAR DOSZEDŁ 2026-09-06 — WCZEŚNIEJ 67 ELEMENTÓW BRAŁO 16px
 * Z PREFLIGHTU ⚠️
 *
 * Klasa ustalała krój i kolor, ale nie rozmiar, więc akapit, któremu nikt nie
 * dopisał `text-[Npx]` przy okazji, stawał obok rodzeństwa w 12px o cztery
 * piksele większy. Wyglądało to na przypadek, a nie na regułę, i nie dawało
 * się wykryć oglądaniem klasy — klasa wyglądała na kompletną.
 *
 * 14px jest z makiety: `body { font-family: var(--font-body); font-weight: 400;
 * font-size: 14px; line-height: 20px; color: var(--ink) }`. Wartość broni się
 * też rozkładem: rozmiary nadawane tej klasie ręcznie skupiają się ciasno
 * (12px ×90, 12.5px ×62, 13px ×53, 11px ×46) — to jedna rola o jednym środku,
 * w odróżnieniu od `.kp-display` wyżej.
 *
 * `line-height` świadomie NIE dochodzi, choć makieta daje 20px. Rozmiar
 * dotyka 67 elementów bez własnego źródła; interlinia dotknęłaby WSZYSTKICH
 * ~340 wystąpień klasy, w tym tych z jawnym rozmiarem — to osobna zmiana
 * o własnym zasięgu. Dzisiejszy rozjazd interlinii z makietą zostaje
 * nietknięty, a nie ukryty. */
.kp-body {
  font-family: var(--font-body);
  font-size: 14px;
  color: var(--ink-2);
}

.kp-meta {
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--muted);
}

/* Godziny, daty, rozmiary plików, liczniki */
.kp-num {
  font-family: var(--font-body);
  font-variant-numeric: tabular-nums;
  /* Reset ramki z preflightu — uzasadnienie przy .kp-th. */
  border-style: none;
  border-color: currentColor;
}

/* Duza liczba licznika — ten sam byt w kroju display. Dashboard mial dla
   niego gola redefinicje `.kp-num`. */
.kp-num--display {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: .95;
  font-feature-settings: 'tnum' 1;
}

/* ── Karta ──────────────────────────────────────────────────── */

/* Promień z makiet Claude Design: KAŻDA makieta `.dc.html` definiuje
   `.kp-card { border-radius: var(--r-bento) }` (24px), nie --r-card-lg.
   Katalog stał na 16px, przez co dsprobe pokazywał FAIL na `borderRadius`
   każdej karty. --r-card-lg zostaje w tokenach dla gęstych kart wewnętrznych.

   `line-height: normal` — makieta nie ustawia interlinii nigdzie, więc
   dziedziczy `normal` z UA. W aplikacji preflight Tailwinda daje 1.5,
   co podnosiło zmierzoną wysokość każdego wiersza i nagłówka tabeli. */
.kp-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-bento);
  box-shadow: var(--shadow-soft);
  line-height: normal;
}

.kp-card--bento { border-radius: var(--r-bento); }
.kp-card--flush { padding: 0; overflow: hidden; }

/* `.kp-card` NIE ma paddingu z definicji (patrz komentarz wyżej) — konsument
   dokłada go sam, dziś w trzech formach: klasami narzędziowymi Tailwinda
   (`p-5`/`p-6`) wpisanymi na znaczniku, lokalnym prymitywem strony (np.
   `.sbs-panel { padding: var(--sp-5) }` w `static/users/sidebar_settings.css`),
   albo wcale (audyt UX `dashboard-settings.md`, finding #1: `.prs-card` w
   `static/users/personalizacja.css` dodaje `gap`, ale zapomina paddingu —
   treść styka się z hairlinem karty).

   `--pad` daje czwartą, poprawną drogę: modyfikator w katalogu zamiast
   powtarzania tej samej wartości w każdym arkuszu strony. Wartość `--sp-6`
   (24px) wybrana z przeglądu istniejących konsumentów `.kp-card` w
   `templates/`: klasa `.kp-card` łączona w tym samym atrybucie z `p-6`
   występuje 14 razy, z `p-5` — 12. Poprawka po recenzji (2026-08-09):
   poprzednia wersja tego komentarza uzasadniała `--sp-5` tym, że `--sp-6`
   jest „zarezerwowane w skali promieni na --r-bento" — to nieprawda,
   `--r-bento` jest tokenem `border-radius`, niepowiązanym z paddingiem;
   karta może mieć promień 24px i padding 24px jednocześnie. Przy poprawnym
   odczycie własnych danych przewagę ma `p-6`, więc modyfikator idzie za
   danymi, nie za pojedynczym precedensem `.sbs-panel` (który i tak zostaje
   przy `--sp-5` jako osobny, lokalny prymityw strony). Strona, której
   karta potrzebuje 20px, dalej może to zapisać lokalnie — modyfikator
   pokrywa wyłącznie wspólny przypadek. */
.kp-card--pad { padding: var(--sp-6); }

/* ── Przyciski ──────────────────────────────────────────────── */

/* Typografia i metryka przepisane z makiety (`.kp-btn` w *.dc.html):
   krój display 700/13px z trackingiem -.2px, padding 9/16, gap 7, nowrap.
   Katalog miał krój body 600/14px i padding 10/18 — osiem właściwości
   przycisku rozjeżdżało się z makietą w każdej kombinacji pomiaru.

   SEMANTYKA ODWRÓCONA 2026-07-30 (decyzja usera). Domyślny `.kp-btn` jest
   NEUTRALNY — powierzchnia, hairline, tekst atramentowy — a błękit wchodzi
   dopiero modyfikatorem `--primary`, dokładnie jak `.kp-btn.primary`
   w makietach. Wcześniej było odwrotnie: błękit domyślnie, przez co każdy
   przycisk w projekcie krzyczał jak główne CTA.

   Migracja: wszystkim 30 gołym `.kp-btn` w szablonach dopisano `--primary`,
   więc wygląd aplikacji NIE zmienił się ani o piksel — zmieniło się to, co
   dostaje przycisk dopisany w przyszłości bez modyfikatora.

   Dług: `--ghost` opisuje dziś to samo co domyślny (powierzchnia + ramka),
   a w makiecie `.ghost` znaczy „przezroczysty, bez ramki, tekst przygaszony",
   czyli tyle co nasz `--quiet`. Nie ruszam tego przy okazji — 25 użyć
   `--ghost` zmieniłoby wygląd, a to osobna decyzja. */
.kp-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 9px 16px;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  --surface: var(--surface-1);
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: -.2px;
  line-height: normal;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--dur-fast) ease, transform var(--dur-fast) ease,
              border-color var(--dur-fast) ease, box-shadow var(--dur-fast) ease;
}

.kp-btn:hover  { border-color: var(--divider); box-shadow: var(--shadow-card); }
.kp-btn:active { transform: scale(0.98); }

/* Główne CTA. Makieta maluje tekst na wypełnieniu na czysto biało; katalog
   dawał --blue-050 (rgb(239,246,255)). --on-overlay to jedyny token białego
   stałego w obu motywach (powtórzony w bloku body.dark). */
.kp-btn--primary {
  background: var(--blue-600);
  border-color: transparent;
  color: var(--on-overlay);
}

.kp-btn--primary:hover { background: var(--blue-700); box-shadow: none; }

.kp-btn--ghost {
  --surface: var(--surface-1);
  background: var(--surface);
  border-color: var(--border);
  color: var(--ink);
}

.kp-btn--ghost:hover {
  --surface: var(--surface-1);
  background: var(--surface);
  border-color: var(--divider);
  box-shadow: var(--shadow-card);
}

/* Mniejszy przycisk. Dashboard mial dla niego wlasna gola definicje
   `.kp-btn` (padding 8/16, 12px) obok katalogowej (10/18, 14px) — ten sam
   przycisk wygladal inaczej na dashboardzie niz wszedzie indziej. */
.kp-btn--sm {
  gap: 6px;
  padding: 8px 16px;
  font-size: 12px;
  justify-content: center;
}

/* `border-color: transparent` dołożone przy odwróceniu semantyki: domyślny
   `.kp-btn` ma teraz hairline, a wariant „cichy" ma go nie mieć (w makiecie
   to `.kp-btn.ghost` — przezroczysty, bez ramki, tekst przygaszony). */
.kp-btn--quiet {
  background: transparent;
  border-color: transparent;
  color: var(--muted);
  padding: 9px 12px;
}

.kp-btn--quiet:hover { background: var(--paper-2); color: var(--ink); box-shadow: none; }

/* `color` i `border-color` MUSZĄ tu stać jawnie. Wcześniej wariant podawał
   samo tło i brał biały tekst po domyślnym `.kp-btn`; po odwróceniu semantyki
   domyślny daje --ink, więc bez tych dwóch linii wyszedłby czarny napis
   i szara ramka na czerwonym wypełnieniu. */
.kp-btn--danger {
  background: var(--status-urgent);
  border-color: transparent;
  color: var(--on-overlay);
}
.kp-btn--danger:hover { background: var(--status-urgent); filter: brightness(0.92); box-shadow: none; }

/* Stan wyłączony. Do katalogu wchodzi przy DRUGIM konsumencie w produkcie:
   pierwszym był ekran wejścia (`static/users/login.css`, zawężenie
   `.lg-submit.kp-btn[disabled]`), drugim panel kopii zapasowych, gdzie
   przycisk operacji nieodwracalnej jest nieaktywny do chwili potwierdzenia.
   Katalog ma już `.kp-field:disabled`, więc przycisk bez własnego stanu był
   luką, nie decyzją — nieaktywny wyglądał dokładnie jak gotowy do kliknięcia.

   Zawężenia stron zostają ważne: `.lg-submit.kp-btn[disabled]` (0,3,0) bije
   regułę katalogu (0,2,0), więc ekran wejścia zachowuje swoje `opacity: .72`
   i `cursor: default`. */
.kp-btn:disabled,
.kp-btn[disabled] {
  opacity: 0.45;
  cursor: not-allowed;
  box-shadow: none;
}

.kp-btn:disabled:hover,
.kp-btn[disabled]:hover {
  border-color: var(--border);
  box-shadow: none;
}

.kp-btn:focus-visible,
.kp-field:focus-visible,
.kp-seg-option:focus-visible {
  outline: 2px solid var(--blue-600);
  outline-offset: 2px;
}

/* ── Przełącznik segmentowy ─────────────────────────────────── */

.kp-seg {
  display: inline-flex;
  padding: 3px;
  gap: 2px;
  background: var(--surface-well);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
}

/* Metryka i krój z makiet (.kp-tab): krój display 700 z ujemnym trackingiem,
   padding 9/15. Aktywna zakładka jest BIAŁĄ pigułką z tekstem --ink, a nie
   niebieskim napisem — niebieski w tym miejscu robił z segmentu drugi akcent
   obok primary CTA. Zmiana z 2026-07-30, decyzja użytkownika (patrz
   docs/Nowy-Design-Kropka/contracts/Kartoteka.anchors.json, group-tab-active). */
.kp-seg-option {
  appearance: none;
  /* Makieta daje zakładce przezroczystą ramkę 1px, żeby stan aktywny nie
     przesuwał sąsiadów. `border: 0` dawało 0px przeciw 1px z makiety. */
  border: 1px solid transparent;
  background: transparent;
  padding: 9px 15px;
  border-radius: var(--r-pill);
  font-family: var(--font-display);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: -0.2px;
  /* Jak w .kp-card/.kp-btn/.kp-chip: makieta nie ustawia interlinii nigdzie,
     a preflight Tailwinda daje 1.5 i podnosi zmierzoną wysokość pigułki. */
  line-height: normal;
  color: var(--muted);
  cursor: pointer;
  transition: background var(--dur-fast) ease, color var(--dur-fast) ease;
}

.kp-seg-option:hover { color: var(--ink); }

/* Trzeci wariant stanu aktywnego: `aria-current="page"`. Segment bywa nie tylko
   widgetem sterowanym skryptem (`tablist` → `aria-selected`) ani grupą przycisków
   (`aria-pressed`), ale też NAWIGACJĄ NA ODNOŚNIKACH, gdzie stan rozstrzyga adres,
   a nie skrypt — a wtedy właściwym atrybutem jest `aria-current`. Dopisane
   2026-08-27 przy scalaniu oprawy ekranu wsparcia; w chwili dopisania żaden
   z konsumentów `.kp-seg` nie używał tego atrybutu, więc reguła była dla nich
   martwa i niczego im nie zmieniła. */
.kp-seg-option[aria-selected="true"],
.kp-seg-option[aria-pressed="true"],
.kp-seg-option[aria-current="page"],
.kp-seg-radio:checked + .kp-seg-option {
  --surface: var(--surface-1);
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-soft);
}

/* Czwarty wariant: segment na UKRYTYCH RADIACH (`<input class="kp-seg-radio">`
   tuż przed swoją `<label class="kp-seg-option">`). Stan aktywny daje
   `:checked`, więc przełącznik działa bez JavaScriptu i bez przeładowania —
   a radio, jako pole formularza, przenosi wybraną zakładkę przez nieudany
   zapis. Pierwszy konsument: sekcja „Z kim" spotkań PPP (2026-10-01), gdzie
   zakładka pokazuje jedną z dwóch list, a obie zostają w formularzu.
   Radio jest schowane wizualnie, NIE `display: none` — inaczej wypadłoby
   z kolejności tabulacji i strzałki nie przełączałyby zakładek. */
.kp-seg-radio {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.kp-seg-radio:focus-visible + .kp-seg-option {
  outline: 2px solid var(--blue-600);
  outline-offset: 2px;
}

/* ── Pola formularza ────────────────────────────────────────── */

/* UWAGA: selektor jest zawężony do <label>. Nazwa `.kp-label` jest już
   zajęta przez podpis pozycji paska bocznego (base.html, <span>/<p>/<svg>),
   który dziedziczy kolor po .kp-nav-item. Goła klasa nadpisywała mu kolor
   na --muted, więc podpis aktywnej pozycji robił się szary na kolorowej
   plaszce. Wszystkie użycia w kompozytorze wiadomości to <label>, więc
   zawężenie niczego tam nie zmienia. */
label.kp-label {
  display: block;
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
  margin-bottom: 6px;
}

/* Metryka pola pochodzi z makiet Claude Design (.kp-input / .kp-sel):
   13.5px/500, padding 9/11, radius 10. Do 2026-07-30 katalog trzymał tu
   14px/400, padding 10/14, radius --r-card — patrz komentarz przy
   select.kp-field niżej. Decyzja użytkownika z 2026-07-30 (kontrakt
   Kartoteka.anchors.json): metryka makiety wygrywa, także w prymitywach
   współdzielonych. Zmierzone dsprobe: pole schodzi z 43px na 36px wysokości.

   Do tej samej metryki doszły niezależnie dwie wyspy (kartoteka i księgowość)
   — zbieżność, nie przypadek. Uwaga: wersja z księgowości głosiła „promień już
   się zgadzał (--r-card = 12px)"; to nieaktualne, bo pole stoi dziś na osobnym
   `--r-field` (10px), dobranym pomiarem. */
.kp-field {
  width: 100%;
  padding: 9px 11px;
  --surface: var(--surface-1);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-field);
  font-family: var(--font-body);
  font-size: 13.5px;
  font-weight: 500;
  /* `normal`, nie odziedziczone 1.5 z <body> — interlinia rozpychała pole
     z 36px na 40.25px przy identycznym paddingu (zmierzone dsprobe). */
  line-height: normal;
  color: var(--ink);
  transition: border-color var(--dur-fast) ease;
}

/* `appearance: none` na <select> — makieta rysuje listę rozwijaną jako zwykłe
   pole, BEZ systemowej strzałki, i taka była decyzja usera (2026-07-30:
   „dokładnie jak w makiecie"). Świadomy koszt: select przestaje odróżniać się
   wizualnie od pola tekstowego. Odrzucony wariant: własny szewron inline-SVG
   w wadze DS — zachowywał tę różnicę, ale odchodził od makiety.

   Bez tego pole i tak nie wyglądałoby jak w makiecie: przeglądarka dokłada
   własny chrom (na macOS zaokrąglenia i cień), którego nie da się ostylować. */

.kp-field::placeholder { color: var(--hint); }
.kp-field:focus { outline: none; border-color: var(--blue-600); }
textarea.kp-field { resize: vertical; min-height: 120px; }

/* Dołożone przy scalaniu wyspy tablicy (consolidate-ds-primitive-layer).
   Tablica miała własne .kp-input/.kp-sel/.kp-area — nazwy rodzajowe
   w przestrzeni katalogu, różniące się od .kp-field o pół piksela
   (13.5px kontra 14px, padding 9/11 kontra 10/14). Przeszła na .kp-field,
   a te dwa zachowania, których katalog jeszcze nie miał, weszły tutaj.

   AKTUALIZACJA 2026-07-30: to właśnie te „pół piksela" wróciły — .kp-field
   powyżej nosi dziś metrykę makiety, więc tamto scalenie zostało cofnięte
   świadomie, a nie przez przypadek.

   Reguła scalona z drugiej, dopisanej równolegle przez wyspę księgowości
   (`appearance: none`) — dwie gołe definicje tego samego selektora to
   dokładnie ten dryf, przed którym ostrzega readme DS.

   O prawym paddingu 28px: makieta rezerwuje to miejsce pod WŁASNY szewron
   rysowany osobnym spanem. Po `appearance: none` nasz select nie rysuje
   żadnej strzałki, więc te 28px trzyma dziś sam kształt pola tekstowego
   zgodny z makietą, a nie miejsce na cokolwiek. Zostawione ŚWIADOMIE:
   zdjęcie ich rozjechałoby metrykę mierzoną kontraktem, a szewrona i tak
   nie ma z decyzji usera („dokładnie jak w makiecie"). */
select.kp-field {
  appearance: none;
  cursor: pointer;
  font-size: 13px;
  padding: 9px 28px 9px 11px;
}

.kp-field:disabled {
  background: var(--surface-well);
  color: var(--muted);
  cursor: not-allowed;
}

/* Stan błędu pola. Wariant BEM prymitywu katalogowego — nazwa rodzajowa
   `.kp-field` należy do katalogu, więc jej stan też, a nie do arkusza
   jednej strony (goła definicja `.kp-*` poza katalogiem cicho nadpisałaby
   prymityw dla tej strony; pilnuje tego `tests/test_ds_stylesheets.py`).

   Powtórzone na `:focus`, bo `.kp-field:focus` wyżej przestawia
   `border-color` na `--blue-600` — bez tego obramowanie błędu znikałoby
   dokładnie w momencie, w którym admin wchodzi do pola, żeby je poprawić.

   Pierwszy konsument: wyspa granic semestrów (journal/semester-config/). */
.kp-field--error { border-color: var(--status-urgent); }
.kp-field--error:focus { border-color: var(--status-urgent); }

.kp-hint {
  font-family: var(--font-body);
  font-size: 11px;
  color: var(--hint);
  margin-top: 6px;
}

/* ── Wiersz listy ───────────────────────────────────────────── */

.kp-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px 20px;
  border-top: 1px solid var(--border);
  color: inherit;
  text-decoration: none;
  transition: background var(--dur-fast) ease;
}

.kp-row:first-child { border-top: 0; }
.kp-row:hover { background: var(--paper-2); }
.kp-row--unread { background: var(--blue-050); }
.kp-row--unread:hover { background: var(--blue-100); }

/* ── Znaczniki i liczniki ───────────────────────────────────── */

/* Licznik. Nazywal sie `.kp-badge` — nazwa ZASTRZEZONA dla paska bocznego
   (ds-component-layer wymienia ja wprost). Pasek styluje swoje bejdze utilami
   Tailwinda w znaczniku plus zawezeniami `.kp-nav-item:hover .kp-badge`
   w base.html, wiec golej reguly stad nie potrzebowal — a ta gola regula
   scigala sie z jego znacznikiem o wynik. */
.kp-counter {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  border-radius: var(--r-pill);
  background: var(--status-urgent);
  color: var(--blue-050);
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* Licznik ZAZNACZEŃ kontrolki — nie „coś pilnego". Bazowy `.kp-counter` stoi
   na `--status-urgent`, bo jego konsumenci liczą nieprzeczytane i problemy;
   liczba osób zaznaczonych w zakładce wyboru (`ppp-spotkania-wielu-
   uczestnikow`, sekcja „Z kim") jest stanem kontrolki funkcjonalnej, więc
   dostaje jej akcent — `--blue-600` z tekstem `--on-fill`. Mniejszy niż bazowy,
   bo siedzi w pigułce `.kp-seg-option`, obok jej podpisu. */
.kp-counter--accent {
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  margin-left: 6px;
  background: var(--blue-600);
  color: var(--on-fill);
  font-size: 10.5px;
  line-height: 1;
  vertical-align: 1px;
}

/* `white-space: nowrap` + `flex: 0 0 auto` + `line-height: normal` —
   trzy braki, przez które pasek zakładek księgowości rozsypywał się na
   telefonie: pigułki jako kurczliwe dzieci flexa zbijały się w „bloby"
   94.8×95 zamiast poziomego paska 181×35 (zmierzone na 390px). Makieta
   ma `white-space: nowrap` w samym `.kp-chip`. */
.kp-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  border-radius: var(--r-chip);
  background: var(--status-info-bg);
  color: var(--status-info);
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 600;
  line-height: normal;
  white-space: nowrap;
  flex: 0 0 auto;
}

/* `--status-ok-ink`, nie surowy `--status-ok` — uzasadnienie przy
   `.kp-tag--ok`. Chip i znacznik muszą mówić tym samym odcieniem, inaczej
   „zapisano" wygląda inaczej zależnie od użytego prymitywu. */
.kp-chip--ok    { background: var(--status-ok-bg);    color: var(--status-ok-ink); }
.kp-chip--muted { background: var(--paper-2);         color: var(--muted); }

/* Trzy modyfikatory dołożone dla modułów, które dziś malują status gołymi
   klasami Tailwinda (audyt `zamowienia.md`, finding #3: `bg-amber-100
   text-amber-700` / `bg-red-100 text-red-700` / `bg-blue-100 text-blue-700`
   w `templates/procurement/list.html`). Tło = 10% alpha tego samego odcienia
   (para tokenów `--status-*-bg`).

   Kontrast policzony niezależnie na każdym z trzech (sRGB→linear→luminancja
   WCAG, tło = tint złożony na `--surface`), z wynikami INNYMI dla każdego —
   stąd trzy różne decyzje, nie jedna wspólna:

   · `--urgent` — surowy `--status-urgent` na `--status-urgent-bg` liczy
     5.71:1. Przechodzi z zapasem, zostaje bez `-ink`.
   · `--warning` — surowy `--status-warning` liczył 2.58:1, dwa razy poniżej
     progu 4.5:1. Naprawione: kolor bierze `--status-warning-ink`
     (`tokens-colors.css`, obok `--status-ok-ink`) — 4.63:1 w trybie jasnym,
     6.09:1 w ciemnym (tam token wskazuje na `--status-warning` ciemny,
     patrz komentarz przy tokenie).
   · `--info` NIE jest tu modyfikowany (patrz niżej) — powtarza bazę
     `.kp-chip`, która maluje się surowym `--status-info`: 4.4999:1, formalnie
     pod progiem o ułamek promila. To zachowanie bazowego `.kp-chip` sprzed
     tego katalogu, poza zakresem tej zmiany — odnotowane, nie naprawione. */
.kp-chip--warning { background: var(--status-warning-bg); color: var(--status-warning-ink); }
.kp-chip--urgent  { background: var(--status-urgent-bg);  color: var(--status-urgent); }
/* `--info` powtarza dokładnie niezmodyfikowaną bazę `.kp-chip` (linie wyżej)
   — baza już maluje się kolorem informacyjnym. Modyfikator jest tu mimo to,
   bo pozwala napisać intencję w znaczniku wprost (`kp-chip kp-chip--info`),
   zamiast polegać na tym, że ktoś pamięta, iż brak modyfikatora TEŻ znaczy
   „informacja" — i jest odporny na przyszłą zmianę koloru bazowego. */
.kp-chip--info  { background: var(--status-info-bg);  color: var(--status-info); }

/* Zakładka modułu — pigułka nawigacyjna, nie chip statusowy.
   Makieta rysuje ją tym samym `.kp-chip`, ale w wariancie klikalnym:
   waga 700 i tracking -.1px. Metryka (padding, rozmiar, kolor wypełnienia)
   zostaje w szablonie paska, bo wypełnienie liczy się z koloru modułu. */
.kp-chip--tab {
  font-weight: 700;
  letter-spacing: -.1px;
  cursor: pointer;
}

/* Pigułka filtra (klasa, przedmiot, …) — przycisk ALBO link, nie chip
   statusowy: `.kp-chip` wyżej to 11px, nieklikalne, tło `--status-info-bg`.
   Tu makieta rysuje krój display, pełne zaokrąglenie, białą powierzchnię
   z hairlinem. Powstała w `static/journal/teacher_grades.css` jako
   `.kp-tg-chip`, z zapisem „awansuje do katalogu, gdy zgłosi się drugi
   konsument" — zgłosił się: `templates/education/class_grades.html`
   (filtr klasy i przedmiotu). Dwaj konsumenci to `<button>` (Część C,
   filtr z `name="class_id"`) i `<a>` (arkusz klasy, nawigacja przez URL),
   stąd `text-decoration: none` w bazie i stan aktywny na dwóch atrybutach:
   `aria-pressed` dla przycisku, `aria-current="page"` dla linku. */
.kp-filterchip {
  appearance: none;
  display: inline-flex;
  align-items: center;
  font-family: var(--font-display);
  font-weight: var(--fw-bold);
  font-size: 12.5px;
  padding: 8px 14px;
  border-radius: var(--r-pill);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--muted);
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-out),
              background .14s var(--ease-out),
              color .14s var(--ease-out),
              border-color .14s var(--ease-out);
}

.kp-filterchip:hover { color: var(--ink); border-color: var(--ink-3); transform: translateY(-1px); }

.kp-filterchip:focus-visible {
  outline: 2px solid var(--blue-600);
  outline-offset: 2px;
}

/* Stan aktywny niesie `aria-pressed` (przycisk) albo `aria-current="page"`
   (link) — nie osobna klasa. Czytnik ekranu dostaje stan, a CSS nie
   potrzebuje drugiego źródła prawdy. */
.kp-filterchip[aria-pressed="true"],
.kp-filterchip[aria-current="page"] {
  background: var(--blue-600);
  color: var(--on-fill);
  border-color: transparent;
  box-shadow: var(--shadow-soft);
}

.kp-filterchip:active { transform: scale(0.98); }

/* ── Stan pusty ─────────────────────────────────────────────── */

.kp-empty {
  padding: 64px 24px;
  text-align: center;
  font-family: var(--font-body);
  color: var(--muted);
}

.kp-empty svg { margin: 0 auto 12px; color: var(--divider); }

/* ── Awatar ─────────────────────────────────────────────────── */

.kp-avatar {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: var(--r-pill);
  object-fit: cover;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--paper-2);
  color: var(--muted);
  /* Inicjały to liczba/skrót, nie treść — makiety rysują je krojem display. */
  font-family: var(--font-display);
  font-size: 13px;
  font-weight: 700;
}

.kp-avatar--accent { background: var(--blue-600); color: var(--on-overlay); }
/* --r-avatar (11px), nie --r-card (12px): tak rysują to makiety. */
.kp-avatar--square { border-radius: var(--r-avatar); }

/* ── Znak marki ─────────────────────────────────────────────── */

/* Kafelek znaku: biała kropka na niebieskim kwadracie. Znacznik siedzi
   w `templates/components/brand_mark.html` — tam też pełne uzasadnienie.

   Zaokrąglenie i średnica kropki są PROCENTOWE, nie w pikselach: znak stoi
   w dwóch rozmiarach (30px w pasku, 36px na ekranach wejścia), a makieta
   podaje proporcje przy boku 250 (rx 60 = 24%, kropka Ø100 = 40%). Piksele
   wymusiłyby drugi komplet liczb na każdy rozmiar i to właśnie tak znak
   rozjechał się poprzednio.

   Bez cienia — makieta wstawia płaski `<img>`, a reguła DS zabrania
   gradientów i faktur w chrome. Dawny kafelek z literą „K" nosił
   `shadow-sm`; to była ozdoba spoza makiety. */
.kp-brand-mark {
  flex-shrink: 0;
  display: grid;
  place-items: center;
  width: var(--kp-mark-size, 30px);
  height: var(--kp-mark-size, 30px);
  border-radius: 24%;
  background: var(--blue-600);
  color: var(--on-overlay);
}

/* Glif wypełnia kafelek, więc jego 40% liczy się od boku znaku — dzięki temu
   `--kp-mark-size` jest jedynym pokrętłem. `display: block` gasi domyślny
   odstęp linii bazowej pod inline SVG (kafelek jest gridem, ale glif bywa
   wstawiany też poza nim przy pomiarach kontraktu). */
.kp-brand-mark svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* ══════════════════════════════════════════════════════════════
   Prymitywy dołożone przez wyspę OCEN (redesign-oceny-student-card).

   UWAGA NA NAZWY: wiersz listy `.kp-row` powyżej jest flexowy
   (padding + hairline). Wiersz tabeli ocen potrzebuje samego
   zachowania hover, więc nosi osobną nazwę `.kp-trow` — nadpisanie
   `.kp-row` zamieniłoby <tr> w kontener flex i rozwaliło tabelę.

   Przełącznik semestrów i przyciski NIE mają tu własnych klas:
   korzystają z `.kp-seg` / `.kp-seg-option` i `.kp-btn` powyżej.
   ══════════════════════════════════════════════════════════════ */

/* ── Wiersz tabeli ──────────────────────────────────────────── */

.kp-trow {
  transition: background var(--dur-fast) ease;
}
.kp-trow:hover {
  background: var(--paper-2);
}

/* ── Podniesienie na hover ──────────────────────────────────── */

.kp-lift {
  transition: transform var(--dur-med) var(--ease-out), box-shadow var(--dur-med) ease;
}
.kp-lift:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-bento);
}

/* ── Pigułka oceny (MarkPill) ───────────────────────────────── */
/* Kolor przychodzi z szablonu jako var(--mark-N) wyliczony
   z WARTOŚCI oceny — patrz filtr mark_color w apps/education. */

.kp-pill {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 36px;
  height: 36px;
  padding: 0 9px;
  border: none;
  border-radius: 10px;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 14px;
  letter-spacing: -0.2px;
  font-variant-numeric: tabular-nums;
  color: var(--mark-ink);
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) ease;
}
.kp-pill:hover {
  transform: translateY(-2px) scale(1.06);
  box-shadow: var(--shadow-card);
  z-index: 2;
}
.kp-pill:focus-visible {
  outline: 2px solid var(--blue-600);
  outline-offset: 2px;
}

/* Średnia roczna — nieco większa od pigułki oceny */
.kp-pill.kp-pill-lg {
  min-width: 58px;
  height: 42px;
  border-radius: 14px;
  font-size: 17px;
  padding: 0 10px;
}
/* Pigułka wiodąca w modalu szczegółów */
.kp-pill.kp-pill-hero {
  min-width: 64px;
  height: 64px;
  border-radius: var(--r-card-lg);
  font-size: 30px;
  padding: 0 12px;
}
/* Warianty nieinteraktywne (średnie) */
.kp-pill.kp-pill-static { cursor: default; }
.kp-pill.kp-pill-static:hover { transform: none; box-shadow: none; }

/* Aura ostrzegawcza oceny niedostatecznej — czerwień skali ocen
   rozcieńczona przez color-mix, żeby nie wpisywać rgba wprost. */
@keyframes kp-grade-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--mark-1) 55%, transparent); }
  50%      { box-shadow: 0 0 0 6px transparent; }
}
.kp-pill.kp-pill-pulse {
  animation: kp-grade-pulse 1.7s ease-in-out infinite;
}

/* Znacznik wagi w rogu pigułki */
.kp-pill-weight {
  position: absolute;
  top: -6px;
  right: -6px;
  min-width: 16px;
  height: 16px;
  padding: 0 3px;
  border-radius: var(--r-pill);
  border: 1.5px solid var(--surface);
  background: var(--ink-2);
  color: var(--surface);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 9px;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* ── Para „ocena poprawiana → poprawa" ───────────────────────
   Poprawa NIE nadpisuje oceny: w dzienniku zostają obie, wiązane
   `Grade.improves`, i stoją obok siebie (porządek ustawia
   `apps.journal.assessments.oznacz_poprawy`, nie CSS).

   Oba stany jadą POZA kanałem barwy — barwa pigułki koduje już wartość
   oceny w skali `--mark-1…6` i drugiego znaczenia nie przyjmie. Stąd
   przekreślenie dla oceny zdjętej ze średniej i znacznik ↻ dla poprawy.

   Wariant, a nie klasa strony: te same trzy znaki czyta karta ocen ucznia,
   ekran lekcji i arkusz kolumnowy. Trzy kopie rozjechałyby się przy
   pierwszej korekcie. */
.kp-pill--old {
  opacity: 0.5;
  text-decoration: line-through;
  text-decoration-thickness: 2px;
}
.kp-pill--fix::after {
  content: '↻';
  position: absolute;
  top: -5px;
  right: -5px;
  width: 14px;
  height: 14px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-pill);
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--ink-2);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 9px;
  line-height: 1;
}
/* Pigułka z wagą ma już bejdż w prawym górnym narożniku (`.kp-pill-weight`)
   — znacznik poprawy przenosi się wtedy na lewo, żeby jeden nie zasłaniał
   drugiego. Warunek na `:has()`, bo o obecności bejdża decyduje szablon
   (waga ≠ 1), a nie druga klasa na pigułce. */
.kp-pill--fix:has(.kp-pill-weight)::after { right: auto; left: -5px; }
.kp-pill-join {
  color: var(--hint);
  font-size: 12px;
  line-height: 1;
  user-select: none;
}

/* ── Mikro-etykieta sekcji (kicker) ─────────────────────────── */

/* `--sectlabel-ink`, nie `--blue-600`: skala blue nie ma nadpisania ciemnego,
   więc ta etykieta miała w trybie ciemnym 3.62:1. Token ma odpowiednik
   w bloku body.dark — rationale przy jego deklaracji w tokens-colors.css.
   W trybie jasnym wartość jest identyczna jak dotąd. */
.kp-sectlabel {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 10.5px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--sectlabel-ink);
}

/* ── Podpowiedź (tooltip) ───────────────────────────────────── */

.kp-tt-wrap { position: relative; }
.kp-tt {
  position: absolute;
  top: calc(100% + 9px);
  left: 0;
  z-index: 80;
  width: max-content;
  max-width: 280px;
  padding: 9px 12px;
  border-radius: var(--r-card);
  background: var(--ink);
  color: var(--surface);
  font-family: var(--font-body);
  font-size: 12px;
  line-height: 1.35;
  box-shadow: var(--shadow-card);
  opacity: 0;
  visibility: hidden;
  transform: translateY(4px);
  pointer-events: none;
  transition: opacity var(--dur-fast) ease, transform var(--dur-fast) ease, visibility var(--dur-fast);
}
.kp-tt-wrap:hover .kp-tt,
.kp-tt-wrap:focus-within .kp-tt {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

/* ── Przewijanie poziome ────────────────────────────────────── */

/* Przewijanie i wyglad paska. Dwie polowy tej definicji powstaly osobno:
   `overflow-x` tutaj, stylizacja paska w arkuszu dashboardu — pod TA SAMA
   nazwa. Arkusz strony wchodzi po katalogu, wiec wygrywal i dashboard
   GUBIL `overflow-x: auto`. Zlozone (consolidate-ds-primitive-layer).

   PASEK MUSI BYC WIDOCZNY BEZ PRZEWIJANIA, i to jest cala tresc tej reguly.
   Na macOS system domyslnie rysuje pasek NAKLADKOWY: pojawia sie na czas
   gestu i gasnie. Tresc schowana za prawa krawedzia nie zostawia wtedy zadnego
   sladu — zgloszenie brzmialo „kolumny tablicy w mniejszym oknie sie nie
   wyswietlaja", a kolumny byly na miejscu i przewijaly sie poprawnie.

   `scrollbar-width` i `scrollbar-color` (wlasnosci standardowe) NIE MOGA tu
   stac bezwarunkowo, choc wygladaja na wlasciwe narzedzie. Ustawienie
   ktorejkolwiek z nich przelacza Chromium na sciezke standardowa i CALKOWICIE
   UNIEWAZNIA reguly `::-webkit-scrollbar` ponizej — pasek wraca do trybu
   nakladkowego, czyli do usterki. Zmierzone w przegladarce z GUI (headless
   zawsze raportuje 0 i NIE nadaje sie do sprawdzania tej reguly), wysokosc
   zajeta przez pasek:

     scrollbar-color + ::-webkit-scrollbar   ->  0px  (nakladka, stan sprzed)
     scrollbar-width + ::-webkit-scrollbar   ->  0px  (nakladka)
     samo ::-webkit-scrollbar                -> 10px  (pasek klasyczny)

   Stad bramka @supports: Chromium i Safari biora pseudoelementy, a Firefox
   (ktory pseudoelementow nie zna, za to zawsze rysuje pasek klasyczny) —
   wlasnosci standardowe. Nie scalaj tych dwoch galezi z powrotem.

   Kolor kciuka liczony wzgledem `--surface`, wiec tryb ciemny dzieje sie sam;
   `--divider` przed color-mix to deklaracja zapasowa dla starszych silnikow.
   Sam `--divider` na `--paper` daje kontrast 1.26:1 — za malo, zeby pasek
   dalo sie zauwazyc, i to byl drugi powod niewidocznosci. */
.kp-scroll {
  overflow-x: auto;
}
.kp-scroll::-webkit-scrollbar { height: 10px; width: 10px; }
.kp-scroll::-webkit-scrollbar-track {
  background: var(--bg-sunken);
  border-radius: var(--r-pill);
}
.kp-scroll::-webkit-scrollbar-thumb {
  background: var(--divider);
  background: color-mix(in srgb, var(--muted) 45%, var(--surface));
  border-radius: var(--r-pill);
}
.kp-scroll::-webkit-scrollbar-thumb:hover {
  background: var(--muted);
}
@supports not selector(::-webkit-scrollbar) {
  .kp-scroll {
    scrollbar-width: thin;
    scrollbar-color: var(--divider) var(--bg-sunken);
  }
}

/* ── Wejście treści ─────────────────────────────────────────── */
/* ════════════════════════════════════════════════════════════════
   DOPISANE PRZEZ redesign-news-page (trzecia wyspa DS)

   Powyżej — prymitywy zostawione przez ekran wiadomości, nietknięte.
   Poniżej — dwa bloki: prymityw modala (bezekranowy, następna wyspa
   weźmie go bez zmian) oraz specyfika Aktualności w przestrzeni
   .kp-news-*, zgodnie z konwencją .kp-<ekran>-* z tamtej zmiany.
   ════════════════════════════════════════════════════════════════ */

/* ── Ruch ───────────────────────────────────────────────────── */

@keyframes kp-fade {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
.kp-fade { animation: kp-fade 280ms var(--ease-out) both; }

@media (prefers-reduced-motion: reduce) {
  .kp-fade,
  .kp-pill.kp-pill-pulse { animation: none; }
  .kp-lift, .kp-pill { transition: none; }
  .kp-lift:hover, .kp-pill:hover { transform: none; }
}

/* ── Obwoluta treści strony ─────────────────────────────────── */
/* Odsunięcie od paska bocznego robi #main-content (base.html);
   tu wyłącznie maksymalna szerokość i wewnętrzne odstępy makiety. */

.kp-page {
  max-width: 1280px;
  margin: 0 auto;
  padding: 22px 34px 80px;
  display: flex;
  flex-direction: column;
  gap: 22px;
  font-family: var(--font-body);
  color: var(--ink);
}
@media (max-width: 768px) {
  .kp-page { padding: 16px 16px 64px; }
}

.kp-fade { animation: kp-fade var(--dur-slow) var(--ease-out) both; }

/* ── Dialog (prymityw na <dialog>) ──────────────────────────────
   Oparty na <dialog>: Escape, ::backdrop, pułapka fokusu i powrót
   fokusu po zamknięciu są zachowaniem przeglądarki, nie naszym
   kodem. Reguły wiążą się z elementem, a nie tylko z klasą — to
   jedyne takie miejsce w tym pliku i wynika z tego, że trzeba
   przykryć domyślne style dialogu.

   NAZWA JEST INNA NIŻ `.kp-modal` I MUSI TAKA POZOSTAĆ.
   `.kp-modal` należy do kontraktu static/kropka-ds/modal.js, który jest
   wpięty globalnie w base.html i na Escape robi
   `querySelectorAll('.kp-modal:not([hidden])').forEach(close)`, a `close()`
   ustawia `el.hidden = true`. Zamknięty <dialog> NIE MA atrybutu `hidden`
   (chowa go brak `open`), więc pasował do tego selektora i po pierwszym
   Escape na stronie dostawał `hidden` na stałe. Razem z regułą
   `[hidden] { display: none !important }` z tego pliku znaczyło to, że
   `showModal()` ustawiał `open`, a okno pozostawało niewidoczne — przycisk
   otwierający przestawał działać do przeładowania strony, bez błędu
   w konsoli. Zmierzone na /news/ przed rozdzieleniem nazw.

   Dialekt na <div> + `hidden` + `data-kp-modal-open` (księgowość) siedzi
   niżej w tym pliku pod nazwą `.kp-modal` — to ten, którym steruje modal.js. */

dialog.kp-dialog {
  padding: 0;
  border: none;
  background: transparent;
  color: inherit;
  max-width: none;
  max-height: none;
  overflow: visible;
}

dialog.kp-dialog::backdrop {
  background: var(--overlay);
  backdrop-filter: blur(2px);
}

/* Podczęści modala są ZASKOPOWANE do dialog.kp-dialog celowo, nie
   z ostrożności. Tablica kanban (templates/boards/board_detail.html)
   ma własny, lokalny `.kp-modal` na <div> i własny `.kp-dialog-title`,
   który NIE ustawia `margin`. Niezaskopowana reguła stąd dokładałaby
   mu `margin: 0` — cicha zmiana cudzej strony. Sprawdzone przed
   zmianą: `grep -rl kp-modal templates/`. */
/* Szerokosc niesie OKNO, panel ja wypelnia. Wczesniej obie warstwy mialy
   wlasna: `.kp-dialog` 440px (regula starszego pokolenia, nizej w pliku),
   panel 520px. Panel jest tym, co widac — to on maluje powierzchnie — wiec
   wystawal poza okno o 80px i KAZDE okno z panelem stalo 40px od srodka
   ekranu. Zmierzone: okno 440 wysrodkowane (480|480), panel 520 (480|360).
   Teraz rozmiar deklaruje sie w jednym miejscu, a panel go wypelnia. */
dialog.kp-dialog:has(> .kp-dialog-panel) { width: min(92vw, 520px); }

dialog.kp-dialog .kp-dialog-panel {
  width: 100%;
  max-height: 88vh;
  overflow-y: auto;
  padding: 26px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-bento);
  box-shadow: var(--shadow-hero);
}

dialog.kp-dialog .kp-dialog-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}

dialog.kp-dialog .kp-dialog-kicker {
  font-family: var(--font-body);
  font-size: var(--fs-tiny);
  font-weight: var(--fw-bold);
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--blue-600);
}

dialog.kp-dialog .kp-dialog-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  font-weight: var(--fw-bold);
  letter-spacing: -0.4px;
  color: var(--ink);
}

dialog.kp-dialog .kp-dialog-foot {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 22px;
}

/* ── Okno Z PANELEM: odstępy zewnętrzne trzyma panel ──────────────
   W tym pliku żyją DWA pokolenia reguł okna. Starsze (bez panelu) daje
   padding SEKCJOM — `.kp-dialog-head` 18/20, `.kp-dialog-body` 12/20,
   `.kp-dialog-foot` 16/20. Nowsze opakowuje treść w `.kp-dialog-panel`
   z własnym `padding: 26px`. Gdy okno ma panel, OBA obowiązują i sumują
   się: zmierzone 26 + 20 = 46px z każdej strony, przez co przy oknie
   520px na treść zostawało 426px, a stopka z trzema przyciskami łamała
   się na dwie linie, choć zmieściłaby się bez trudu.

   Zerujemy więc padding sekcji, ale WYŁĄCZNIE wewnątrz panelu. Trzy okna
   nie mają panelu w ogóle (`journal/_group_delete_dialog.html`,
   `notes/note_trash.html`, `messaging/_channels.html`) i tam padding sekcji
   jest JEDYNYM — bez tego zawężenia treść przykleiłaby się do krawędzi. */
dialog.kp-dialog .kp-dialog-panel > .kp-dialog-head,
dialog.kp-dialog .kp-dialog-panel > .kp-dialog-body,
dialog.kp-dialog .kp-dialog-panel > .kp-dialog-foot {
  padding: 0;
}

/* Rytm pionowy między sekcjami — po wyzerowaniu paddingu niosą go odstępy,
   a nie suma paddingów sąsiadów. Stopka ma już `margin-top: 22px` wyżej. */
/* Odstep miedzy sekcjami ma byc nie mniejszy niz oddech przy krawedzi —
   inaczej sekcje tloczyly sie w srodku, podczas gdy brzegi mialy 26px.
   Stopka ma wlasne `margin-top: 22px` wyzej; tu wyrownujemy je do 22px. */
dialog.kp-dialog .kp-dialog-panel > .kp-dialog-body { margin-top: 22px; }

/* Typografię nagłówka niesie `.kp-dialog-title`, a nie kontener.
   Reguła kontenerowa (`.kp-dialog-head { font-size: 18px; font-weight: 700 }`)
   jest starsza od panelu i sprawia, że KAŻDE dziecko nagłówka dziedziczy krój
   displayowy — podpis pod tytułem czytał się jak drugi tytuł. Zerujemy ją
   tylko w panelu, bo okna bez panelu (`notes/note_trash.html`,
   `messaging/_channels.html`) własnego `.kp-dialog-title` NIE mają i na tej
   regule stoją. */
dialog.kp-dialog .kp-dialog-panel > .kp-dialog-head {
  font-family: var(--font-body);
  font-size: var(--fs-body);
  font-weight: var(--fw-regular);
  letter-spacing: normal;
}

/* Kwadratowy przycisk ikonowy — geometria przycisku zamknięcia
   z makiety modala. Kolorystyka i stany z .kp-btn--ghost. */
.kp-icon-btn {
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  padding: 0;
  justify-content: center;
}

/* ── Aktualności ────────────────────────────────────────────── */

.kp-news-tile {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  padding: 16px;
  background: var(--bg-sunken);
  border: 1px solid var(--border);
  border-radius: var(--r-card-lg);
}

.kp-news-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.kp-news-tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

/* Mikro-etykieta DANYCH (nie pola formularza) — jedyne dopuszczone
   wersaliki poza bejdżami. Podpisy pól używają prymitywu .kp-label. */
.kp-news-meta {
  font-family: var(--font-body);
  font-size: 9.5px;
  font-weight: var(--fw-bold);
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.kp-news-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: 14.5px;
  font-weight: var(--fw-bold);
  letter-spacing: -0.2px;
  color: var(--ink);
}

.kp-news-excerpt {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--fs-caption);
  line-height: 1.45;
  color: var(--muted);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Rekord ukryty — gasimy treść, nie obramowanie i nie bejdże. */
.kp-news-tile.is-hidden .kp-news-title { color: var(--muted); }

/* Chip odbiorców. Kolor wjeżdża zmienną --kp-news-color; tło to ten
   sam odcień przy 10% alpha — reguła DS dla tintów statusowych,
   rozszerzona na paletę kategoryczną (dla --c-* nie ma gotowych
   tokenów `-bg`, więc liczymy je color-mixem).

   TEKST JEST NEUTRALNY, KOLOR NIESIE KROPKA — i to nie jest wybór
   estetyczny, tylko wynik pomiaru. Tekst w pełnym odcieniu na własnym
   10-procentowym tincie daje 2.90–3.55:1 przy wymaganych 4.5:1 (teal
   najgorszy). Neutralny --ink-2 na tym samym tincie daje 10.5:1.
   Kropka może zostać kolorowa, bo kolor jest tu REDUNDANTNY — odbiorca
   jest zawsze wypisany słowem obok niej, więc kropka nie jest jedynym
   nośnikiem informacji. Szczegóły i odrzucone warianty: design D2. */
.kp-news-audience {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  border-radius: var(--r-pill);
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: var(--fw-semibold);
  white-space: nowrap;
  color: var(--ink-2);
  background: color-mix(in srgb, var(--kp-news-color, var(--ink-3)) 10%, transparent);
}

.kp-news-audience::before {
  content: '';
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  border-radius: var(--r-pill);
  background: var(--kp-news-color, var(--ink-3));
}

/* Mapa rola → kolor, dosłownie z Kartoteka.dc.html:1085. Mapowanie
   żyje w warstwie stylu, bo to decyzja designu: `audience` niesie
   nazwę roli, a DS ma dla ról gotową paletę. Wartość `all` nie
   występuje w mapie źródłowej i zostaje neutralna. */
.kp-news-audience[data-audience="TEACHER"]   { --kp-news-color: var(--c-green); }
.kp-news-audience[data-audience="PEDAGOGUE"] { --kp-news-color: var(--c-teal); }
.kp-news-audience[data-audience="DIRECTOR"]  { --kp-news-color: var(--c-clay); }
.kp-news-audience[data-audience="STUDENT"]   { --kp-news-color: var(--c-cyan); }
.kp-news-audience[data-audience="PARENT"]    { --kp-news-color: var(--c-magenta); }

/* Bejdż „przypięty" — wartości z Dashboard.dc.html:300. Emoji
   zostaje: makieta trzyma je świadomie (design D9). */
.kp-news-pin {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 8px;
  border-radius: var(--r-pill);
  background: var(--blue-050);
  color: var(--blue-600);
  font-family: var(--font-body);
  font-size: 9.5px;
  font-weight: 800;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  white-space: nowrap;
}

.kp-news-pin--muted {
  background: var(--divider);
  color: var(--muted);
}

/* Pigułka załącznika — wartości z Dashboard.dc.html:306/309. */
.kp-news-attach {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  padding: 5px 10px;
  --surface: var(--surface-1);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  font-family: var(--font-body);
  font-size: 11px;
  color: var(--ink-2);
  text-decoration: none;
  cursor: pointer;
  transition: border-color var(--dur-fast) ease, color var(--dur-fast) ease;
}

.kp-news-attach:hover { border-color: var(--divider); color: var(--ink); }
.kp-news-attach:focus-visible { outline: 2px solid var(--blue-600); outline-offset: 2px; }
.kp-news-attach svg { flex: 0 0 auto; }
.kp-news-attach-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kp-news-attach-size { flex: 0 0 auto; color: var(--muted); font-variant-numeric: tabular-nums; }

/* Rozwinięcie pełnej treści. Makieta pokazuje ją w dymku na hover,
   co jest nieosiągalne z klawiatury i na dotyku; <details> daje to
   samo bez JS-a i z obsługą klawiatury z pudełka. */
.kp-news-more > summary {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  list-style: none;
  cursor: pointer;
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: var(--fw-semibold);
  color: var(--link);
  transition: color var(--dur-fast) ease;
}

.kp-news-more > summary::-webkit-details-marker { display: none; }
.kp-news-more > summary:hover { color: var(--link-hover); }
.kp-news-more > summary:focus-visible { outline: 2px solid var(--blue-600); outline-offset: 2px; }
.kp-news-more > summary svg { transition: transform var(--dur-med) var(--ease-out); }
.kp-news-more[open] > summary svg { transform: rotate(180deg); }
.kp-news-more > summary .kp-news-more-less { display: none; }
.kp-news-more[open] > summary .kp-news-more-less { display: inline; }
.kp-news-more[open] > summary .kp-news-more-open { display: none; }

.kp-news-more-body {
  margin: 8px 0 0;
  font-family: var(--font-body);
  font-size: var(--fs-sub);
  line-height: 1.55;
  color: var(--ink-2);
  white-space: pre-line;
}

.kp-news-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}

.kp-news-actions .kp-btn.is-on { color: var(--blue-600); background: var(--blue-050); }
.kp-news-actions .kp-btn.is-danger:hover { color: var(--status-urgent); background: var(--status-urgent-bg); }

@media (prefers-reduced-motion: reduce) {
  .kp-fade { animation: none; }
  .kp-news-attach,
  .kp-news-more > summary,
  .kp-news-more > summary svg { transition: none; }
}

/* ============================================================
   DOŁOŻONE PRZEZ MODUŁ KSIĘGOWOŚCI (redesign-accounting-module)

   Prymitywy, których katalog jeszcze nie miał: tabela na siatce,
   okno modalne, powiadomienie, drobne przyciski tekstowe i ikonowe
   oraz dwie animacje wejścia. Zachowanie dokładają
   static/kropka-ds/table-sort.js i static/kropka-ds/modal.js.

   Nazwy istniejące wcześniej (kp-card, kp-btn, kp-chip, kp-seg,
   kp-num, kp-scroll, kp-fade, kp-lift) NIE są tu redefiniowane —
   moduł księgowości korzysta z wersji zdefiniowanych wyżej.
   ============================================================ */

/* ── Atrybut `hidden` ma znaczyć „schowane" ──────────────────
   Domyślne `[hidden] { display: none }` z UA ma zerową specyficzność
   i przegrywa z KAŻDĄ klasą układu (`flex`, `grid` z Tailwinda) oraz
   ze stylem inline. Element z `hidden` i klasą `flex` zostawał
   widoczny — pułapka przy przełączaniu paneli z JS.                 */

[hidden] { display: none !important; }

/* ── Tabela na siatce ────────────────────────────────────────
   Szerokości kolumn są własnością konkretnej tabeli, nie systemu:
   deklaruje się je RAZ na kontenerze jako `style="--kp-cols: …"`.   */

/* `border: 0 none currentColor` — preflight Tailwinda nadaje KAŻDEMU
   elementowi `border: 0 solid rgb(229,231,235)`. Przy zerowej szerokości
   nic tego nie widać, ale wartość jest realna i mierzalna: dsprobe
   pokazywał `solid` przeciw `none` i szarość Tailwinda przeciw
   currentColor makiety na każdym prymitywie bez własnej ramki.
   Makiety nie mają preflightu, więc trzymają wartości początkowe CSS.
   Sprawdzone przed dodaniem: żaden szablon nie nakłada na te klasy
   narzędziowej szerokości ramki (`border-2` itp.), której ten reset
   mógłby zgasić. */
.kp-th {
  display: grid;
  grid-template-columns: var(--kp-cols);
  align-items: end;
  gap: 10px;
  padding: 9px var(--sp-3);
  background: var(--surface-well);
  border-style: none;
  border-color: currentColor;
}

/* Nagłówek 12px/800 (`--fs-caption`), nie mikroetykieta 9.5px jak wcześniej —
   właściciel zgłosił nagłówki tabel jako nieczytelne, DRUGA runda: „bardziej
   widoczne, powiększ i pogrub" (rozmiar/caps załatwione w poprzedniej — patrz
   562ccb1c — to wzmacnia kolor i wagę). Rezygnacja z uppercase i
   letter-spacingu jest WYMUSZONA regułą 8 DS: ALL-CAPS wolno wyłącznie
   mikroetykietom ~10px z trackingiem, a 12px już ten próg przekracza — tracking
   zostawiony przy uppercase zniknąłby razem z nim, inaczej liter przy takim
   rozmiarze robiłyby się nienaturalnie rozstrzelone.

   Waga `--fw-heavy` (800, „ExtraBold" Inter) zamiast `--fw-bold` (700, dawny
   sufit skali) — realny stopień wyżej, zmierzony w pliku fontu (fontTools:
   oś `wght` Inter sięga 900), z rozszerzoną deklaracją `@font-face`
   (tokens-fonts.css) i zweryfikowany w przeglądarce jako Inter, nie fallback
   (patrz dsprobe/ log w commit message tej zmiany).

   Kolor `--ink-3` zamiast `--muted` — WYRAŹNIE ciemniejszy/jaśniejszy, nie na
   styk: 9.29:1 jasny / 11.68:1 ciemny na `--bg-sunken` (poprzednio 5.95 / 5.29
   z `--muted`). Hierarchia sprawdzona względem najsilniejszego tekstu wiersza
   w konsumentach tej klasy (np. `.p3d-cell--title a` w Druku 3D, kolor
   `--ink`, waga `--fw-semibold`): `--ink` jest ciemniejszy/jaśniejszy niż
   `--ink-3` w OBU motywach (jasny: 0,0,0 przeciw 63,63,63; ciemny: 238,239,236
   przeciw 226,227,224), więc nagłówek zostaje wyraźnie słabszy od głównej
   treści wiersza niezależnie od tego, jakiego dokładnie tokenu użyje
   konkretny konsument tej klasy (21 szablonów). Kolumny poboczne malowane
   `--muted` (daty, liczniki) stają się słabsze od nagłówka — to zamierzone,
   nagłówek-etykieta ma być czytelniejszy niż wygaszone metadane, nie na
   odwrót. */
.kp-th span {
  font-family: var(--font-body);
  font-weight: var(--fw-heavy);
  font-size: var(--fs-caption);
  letter-spacing: normal;
  text-transform: none;
  color: var(--ink-3);
}

.kp-th span[data-sort] { cursor: pointer; }
/* Hover NIE `--ink-2` — na `--bg-sunken` `--ink-2` jest w trybie ciemnym
   JAŚNIEJSZY (mniejszy kontrast) niż spoczynkowy `--ink-3`: 11.68:1 → 10.49:1,
   czyli hover subtelnie GASIŁ nagłówek zamiast go wzmacniać (w jasnym motywie
   wychodziło poprawnie, 9.29:1 → 11.90:1, bo tam `--ink-2` jest ciemniejszy).
   Przyczyna: `--ink-3` w trybie ciemnym to wypełnienie pigułki, nie stopień
   skali tekstu, więc nie mieści się między `--ink` a `--ink-2` jak w jasnym.
   `var(--ink)` wzmacnia w obu motywach: 11.68:1 → 13.04:1 ciemny, 9.29:1 →
   18.53:1 jasny (na `--bg-sunken`, WCAG, sRGB→linear→luminancja). */
.kp-th span[data-sort]:hover { color: var(--ink); }

/* Wskaźnik kierunku sortowania — wstawiany przez table-sort.js.
   W spoczynku jest PUSTY: Plus Jakarta Sans nie ma glifu U+2195,
   rysował się jako pusty prostokąt. */
.kp-sort-ind {
  font-style: normal;
  margin-left: var(--sp-1);
  color: var(--divider);
  transition: color var(--dur-fast) ease;
}

.kp-sort-ind.active { color: var(--blue-600); }

.kp-prow {
  display: grid;
  grid-template-columns: var(--kp-cols);
  align-items: center;
  gap: 10px;
  padding: var(--sp-2) var(--sp-3);
  /* Ramka TYLKO u góry — trzy pozostałe krawędzie wracają do wartości
     początkowych CSS, zamiast zostawać na `solid` z preflightu (patrz
     komentarz przy .kp-th). */
  border-style: solid none none;
  border-color: var(--border) currentColor currentColor;
  border-width: 1px 0 0;
  transition: background var(--dur-fast) ease;
}

.kp-prow:hover { background: var(--bg-sunken); }

/* ── Drobne akcje ────────────────────────────────────────────── */

.kp-link {
  appearance: none;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 11.5px;
  color: var(--link-ink);
}

.kp-link:hover { text-decoration: underline; }

/* Kontrolka powrotu — JEDYNA w produkcie. Rysuje ją slot w `base.html`, nie
   strona; kierunek liczy rejestr `common/powroty.py`. Wcześniej ta sama
   kontrolka istniała w piętnastu wariantach (`.opl-back`, `.exc-back`,
   `.kd-sub-link`, goły Tailwind…), więc „zwiększyć przycisk powrotu"
   znaczyło edycję ponad stu miejsc; teraz znaczy edycję tej reguły.

   Podpis nazywa CEL („‹ Przegląd opłat"), nie czynność, dlatego bywa długi —
   stąd skracanie wielokropkiem na etykiecie, a nie zawijanie.

   `min-height` zamiast większego kroju: pole klikalne ma mieć 40px, a napis
   ma zostać drobny względem nagłówka strony, pod którym stoi. Wcięcie
   z lewej na telefonie (kolizja z przyciskiem-hamburgerem) należy do SLOTU
   `.kp-back-slot`, nie do kontrolki — kolizja dotyczy położenia w oknie,
   nie wyglądu odnośnika. */
.kp-back {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  min-height: 40px;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 15px;
  line-height: 1.2;
  color: var(--muted);
  text-decoration: none;
  transition: color var(--dur-fast) ease;
}

.kp-back:hover { color: var(--ink); }

.kp-back:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: var(--r-chip);
}

/* `flex: none` — bez tego chevron kurczy się razem z etykietą, gdy ta jest
   dłuższa niż kolumna treści. */
.kp-back svg {
  width: 16px;
  height: 16px;
  flex: none;
}

/* `min-width: 0` odblokowuje kurczenie się elementu w kontenerze flex —
   bez niego `text-overflow` nie ma czego skracać. */
.kp-back-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Slot ramy. Renderuje się WYŁĄCZNIE, gdy rozstrzyganie zwróciło cel —
   strona bez wpisu w rejestrze wygląda dokładnie jak przed zmianą. */
.kp-back-slot {
  /* Przycisk-hamburger leży na stałe w lewym górnym rogu okna, więc na
     telefonie zajmuje miejsce, w którym zaczyna się treść. Wcześniej
     obchodził to `pt-14` w powłoce modułu PPP — pojedynczy moduł łatał
     kolizję dotyczącą całego produktu.

     Wcięcia poziome siedzą TUTAJ, a nie w klasie `px-4` Tailwinda na tym
     samym elemencie: arkusze wysp wchodzą do `<head>` PRZED Tailwindem, więc
     przy równej specyficzności utile wygrywają i `px-4` skasowałoby odsunięcie
     spod hamburgera. */
  padding-left: 48px;
  padding-right: var(--sp-4);
  margin-bottom: var(--sp-2);
}

@media (min-width: 640px) {
  .kp-back-slot { padding-left: var(--sp-4); }
}

/* Nawigacja nie należy na papier. Strony wydruku niosą własny powrót w pasku
   `no-print`, obok „Drukuj" — tam ma sens, bo stoi przy czynności, dla której
   ta strona istnieje. */
@media print {
  .kp-back-slot { display: none; }
}

.kp-mini {
  appearance: none;
  cursor: pointer;
  border: none;
  background: transparent;
  color: var(--muted);
  padding: 3px;
  border-radius: var(--sp-2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background var(--dur-fast) ease, color var(--dur-fast) ease;
}

.kp-mini:hover { background: var(--bg-sunken); color: var(--ink); }

/* ── Okna modalne ────────────────────────────────────────────
   Zachowanie w static/kropka-ds/modal.js: otwieranie atrybutem
   data-kp-modal-open, zamykanie tłem i Esc, powrót fokusu.         */

.kp-modal {
  position: fixed;
  inset: 0;
  z-index: 110;
  background: color-mix(in srgb, var(--ink) 32%, transparent);
  backdrop-filter: blur(2px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-6);
}

.kp-modal-box {
  width: 100%;
  max-width: 480px;
  max-height: calc(100vh - var(--sp-10));
  overflow-y: auto;
  background: var(--surface);
  border-radius: 28px;
  box-shadow: var(--shadow-hero);
  padding: 26px 28px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  animation: kp-up .26s var(--ease-out) both;
}

.kp-modal-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 20px;
  letter-spacing: -.6px;
  color: var(--ink);
}

/* ── Powiadomienia ───────────────────────────────────────────
   Kolor paska po lewej niesie status; treść zostaje neutralna.     */

.kp-toast {
  display: flex;
  gap: 11px;
  align-items: flex-start;
  padding: 13px 15px;
  --surface: var(--surface-2);
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 4px solid var(--blue-600);
  border-radius: 18px;
  box-shadow: var(--shadow-hero);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 12.8px;
  color: var(--ink-2);
  text-wrap: pretty;
}

.kp-toast.error   { border-left-color: var(--status-urgent); }
.kp-toast.warning { border-left-color: var(--status-warning); }
.kp-toast.success { border-left-color: var(--status-ok); }

/* Toast TRWAŁY — nie znika sam, bo niesie coś do przepisania (hasło
   tymczasowe nowego konta). Tekst da się zaznaczyć, zamyka go przycisk. */
.kp-toast-text { flex: 1; min-width: 0; user-select: text; overflow-wrap: anywhere; }
.kp-toast-close {
  flex: none;
  width: 24px;
  height: 24px;
  margin: -3px -5px 0 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  transition: background 120ms, color 120ms;
}
.kp-toast-close:hover { background: var(--paper-2); color: var(--ink); }
.kp-toast-close:focus-visible { outline: 2px solid var(--blue-600); outline-offset: 1px; }

/* ── Animacja wejścia ────────────────────────────────────────── */

@keyframes kp-up {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}

.kp-up { animation: kp-up .26s var(--ease-out) both; }

a.kp-chip, a.kp-btn { text-decoration: none; }

@media (prefers-reduced-motion: reduce) {
  .kp-up, .kp-fade { animation: none; }
  .kp-lift, .kp-btn, .kp-chip { transition: none; }
  .kp-lift:hover, .kp-btn:hover, .kp-chip:hover { transform: none; }
}

/* ════════════════════════════════════════════════════════════════
   AWANSOWANE PRZY SCALANIU WARSTWY (consolidate-ds-primitive-layer)

   Wzorce, które powstały niezależnie w dwóch szablonach naraz — bajt
   w bajt te same reguły, bo drugi ekran robił kopiuj-wklej z pierwszego.
   Reguła wejścia do katalogu (drugi konsument) jest spełniona z definicji.

   Uwaga na cenę awansu: od teraz zmiana na jednym ekranie wiadomości
   dotyka wszystkich, które te klasy noszą. To jest sens katalogu, ale
   trzeba o tym wiedzieć, a nie odkryć.
   ════════════════════════════════════════════════════════════════ */

/* ── Obwoluty ekranów ────────────────────────────────────────────
   Wcześniej `class="kp-msg" style="max-width: 780px; margin: 0 auto"`
   powtórzone w czterech szablonach, przy nazwie NIEZDEFINIOWANEJ nigdzie
   — cała obwoluta żyła w atrybucie `style`. Nie da się tego zastąpić
   `.kp-page`: ta jest powłoką strony (1280px, własne wypełnienie), a tu
   chodzi o wąską kolumnę do czytania. Szerokości przeniesione 1:1, żeby
   scalanie nie zmieniło ani jednego ekranu.                              */

.kp-msg  { max-width: 780px; margin: 0 auto; }
.kp-msg--wide { max-width: 860px; }   /* nowa wiadomość: dwie kolumny odbiorców */
.kp-news { max-width: 820px; margin: 0 auto; }

/* ── Załącznik ───────────────────────────────────────────────────
   Wątek i szczegóły broadcastu. Ostatni `<span>` niesie rozmiar pliku. */

.kp-attach-list { display: flex; flex-wrap: wrap; gap: 6px; }

.kp-attach {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  padding: 4px 10px;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  --surface: var(--surface-1);
  background: var(--surface);
  color: var(--blue-600);
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 600;
  text-decoration: none;
}

.kp-attach:hover { background: var(--paper-2); }
.kp-attach span:last-child { color: var(--muted); font-weight: 400; font-variant-numeric: tabular-nums; }

/* ── Podelementy wiersza listy ───────────────────────────────────
   `.kp-row-main` i `.kp-row-title` powstały w dwóch szablonach; trzy
   pozostałe mają jednego konsumenta, ale awansują razem z nimi, bo są
   podelementami tego samego bloku `.kp-row`, który już tu jest.
   Rozbicie rodziny na katalog i szablon kosztowałoby więcej niż trzy
   reguły z jednym konsumentem.

   Konwencja, którą ta rodzina pokazuje: JEDEN dywiz to podelement
   (`.kp-row-title`), DWA to modyfikator (`.kp-row--unread` wyżej,
   `.kp-row-title--strong` niżej). Dlatego `.kp-row-title` nie wymaga
   przemianowania — nie udaje modyfikatora `.kp-row`.                     */

.kp-row-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }

.kp-row-title {
  font-family: var(--font-body);
  font-size: 14px;
  font-weight: 500;
  color: var(--ink-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.kp-row-title--strong { font-weight: 700; color: var(--ink); }

.kp-row-preview {
  font-family: var(--font-body);
  font-size: 13px;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.kp-row-side {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
  min-height: 40px;
}

/* ── Komunikat błędu na ekranach wiadomości ──────────────────────
   Nakładany RAZEM z `.kp-card` (`class="kp-card kp-msg-error"`), więc
   niesie tylko odchylenia od karty. Wątek i nowa wiadomość.             */

.kp-msg-error {
  padding: 12px 16px;
  border-color: var(--status-urgent);
  background: var(--status-urgent-bg);
  color: var(--status-urgent);
  font-family: var(--font-body);
  font-size: 13px;
}

/* ════════════════════════════════════════════════════════════════
   DOPISANE PRZEZ redesign-boards-list-stats (wyspa DS nr … tablice)

   Cztery prymitywy z makiet `Kanban.dc.html` i `Statystyki tablic.dc.html`,
   których katalog nie miał. Każdy ma DWÓCH konsumentów (lista tablic
   i statystyki tablic) — po jednym awansu by nie było, zgodnie z regułą
   „prymityw awansuje, gdy ma drugiego konsumenta".

   Nazwy istniejące wcześniej (kp-card, kp-btn, kp-chip, kp-field, kp-lift,
   kp-link, kp-prow, kp-num, kp-empty, kp-hint, kp-meta, kp-scroll, kp-modal)
   NIE są tu redefiniowane — obie strony biorą wersje zdefiniowane wyżej.
   Odwzorowanie nazw z makiety na katalog: `.kp-input` → `.kp-field`,
   `.kp-btn` (neutralny) → `.kp-btn--ghost`, `.kp-btn.primary` → `.kp-btn`.
   ============================================================ */

/* ── Mikro-chip (10px) ───────────────────────────────────────────
   Najmniejszy chip w systemie: bejdż roli na kafelku tablicy, licznik
   sekcji, priorytet i termin na karcie. Różni się od `.kp-chip` nie
   odcieniem, a skalą — 10px zamiast 11px i pill zamiast `--r-chip`,
   więc jest osobnym prymitywem, nie modyfikatorem.

   Wypełnienie podaje WOŁAJĄCY (inline `background`/`color`), bo chip
   niesie kolor kategorii albo skali — tak samo jak w makiecie. Wariant
   `--muted` jest jedynym z wpisanym tłem, bo „bez kategorii" to też stan.

   UWAGA: `.kd-chipx` w `templates/boards/board_detail.html` to ten sam
   kształt pod inną nazwą. NIE scalono go tutaj świadomie — nazwa jest
   tam hakiem stylistycznym, po którym cepia się CSS kafelka, a jej
   zmiana dotyka ~15 wywołań `classList` w `static/boards/board_detail.js`
   (ryzyko opisane w decyzji D2 change'a redesign-kanban-board: konflikt
   na przemianowanej linii jest konfliktem semantycznym). Do zwinięcia,
   gdy `board_detail.js` będzie i tak ruszany.                          */

.kp-chipx {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: 3px var(--sp-2);
  border-radius: var(--r-pill);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 10px;
  letter-spacing: .2px;
  white-space: nowrap;
}

.kp-chipx--muted { background: var(--bg-sunken); color: var(--muted); }

/* ── Wiersz-słupek ───────────────────────────────────────────────
   Wariant `.kp-prow` dla zestawień „nazwa · słupek · liczba". Katalogowy
   `.kp-prow` jest wierszem TABELI (hairline u góry, gęstsze odstępy);
   tu hairline przeszkadza, bo wiersze stoją jeden pod drugim jako wykres,
   a nie jako tabela. Kolumny nadal idą przez `--kp-cols`.               */

.kp-prow--bar {
  border-top: none;
  border-radius: var(--r-card);
  padding: 3px var(--sp-1);
}

/* Promień: `--r-card` (12px), nie 10px z makiety. Skala promieni projektu ma
   stopnie 6 / 12 / 16 / 24 / pill (CLAUDE.md, reguła 5) i nie ma 10px — makieta
   używa go obok 11px, 14px i 18px, czyli sama nie trzyma się skali. Świadome
   odstępstwo, zapisane: kontrakt statystyk pokazuje na tej kotwicy FAIL
   `borderRadius 10px vs 12px` i ma go pokazywać, żeby nikt nie „naprawił" go
   z powrotem poza skalę. */

/* ── Etykieta rozdziału strony ───────────────────────────────────
   Szerszy tracking i kolor DZIEDZICZONY. Katalogowy `.kp-sectlabel` jest
   na stałe niebieski (kicker w chrome), a rozdziały treści w makietach
   niosą token kategorialny — `Za okres` lazurowy, `Stan obecny` gliniany.
   `color: inherit` przenosi wybór na kontener, więc strona podaje token
   zamiast nadpisywać prymityw gołą regułą.                             */

.kp-sectlabel--wide {
  letter-spacing: 3px;
  color: inherit;
}

/* ── Strzałka rozwijania ─────────────────────────────────────────
   Dla `<details>`: obrót przy otwarciu. Reguła obrotu jest zawężeniem
   (`details[open] .kp-caret`), nie drugą definicją nazwy.               */

.kp-caret {
  display: inline-flex;
  flex: 0 0 auto;
  color: var(--muted);
  transition: transform var(--dur-med) var(--ease-out);
}

details[open] > summary .kp-caret { transform: rotate(90deg); }

/* ── Etykieta pola formularza ────────────────────────────────────
   Mikro-etykieta nad polem: 10px, wersaliki, tracking. Makiety obu stron
   tablic mają ją inline; nazwa `.kp-label` jest ZASTRZEŻONA dla paska
   bocznego (patrz SIDEBAR_RESERVED w tests/test_ds_stylesheets.py), stąd
   `-fieldlabel`. Różni się od `.kp-sectlabel` przeznaczeniem i kolorem:
   sectlabel jest kickerem sekcji w akcencie chrome, ta jest podpisem pola
   i zostaje neutralna.                                                  */

.kp-fieldlabel {
  display: block;
  margin-bottom: 5px;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 10px;
  letter-spacing: .9px;
  text-transform: uppercase;
  color: var(--muted);
}
/* ── Plakietka statusu ───────────────────────────────────────────
   Awans z wyspy dashboardu (`.dsh-tag`) po tym, jak nazwa zyskała
   drugiego konsumenta — moduł PPP (redesign-ppp-ds, decyzja D6).

   NAZWA: `.kp-tag`, NIE `.kp-badge`. Ta druga należy do paska bocznego
   (`templates/base.html`) i jest zastrzeżona wprost w spec
   `ds-component-layer` — goła definicja przejęłaby podpisy paska.

   DWIE GEOMETRIE, JEDEN BYT. Dashboard używał plakietki jako
   mikro-etykiety (9.5px, wersaliki, dodatni letter-spacing) — DS dopuszcza
   wersaliki wyłącznie w tej roli. Moduł PPP potrzebuje jej jako zwykłej
   plakietki stojącej obok treści wiersza (11px, bez wersalików). Definicja
   podstawowa jest tą drugą, wersja skompresowana nosi `--micro`: dzięki temu
   dashboard nie zmienia wyglądu ani o piksel, a kolejne wyspy dostają wariant
   czytelny w wierszu, zamiast krzyczeć wersalikami z każdej listy.

   Tło statusu to ten sam odcień przy 10% alpha — stąd para tokenów
   `--status-*` / `--status-*-bg`, nigdy własne mieszanie. Wariant `--draft`
   nie ma pary w tokenach statusu (szkic dokumentu to nie stan zdrowia
   rekordu), więc liczy tło `color-mix` względem `--surface` — dzięki temu
   podąża za trybem ciemnym bez osobnej reguły.                          */

.kp-tag {
  display: inline-flex;
  align-items: center;
  justify-self: start;
  gap: 5px;
  width: fit-content;
  max-width: 100%;
  padding: 3.5px 9px;
  border-radius: var(--r-pill);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 11px;
  /* Jak w `.kp-card` / `.kp-btn` / `.kp-chip` / `.kp-seg-option`: makieta nie
     ustawia interlinii nigdzie, a preflight Tailwinda daje 1.5 i podnosi
     zmierzoną wysokość plakietki (21px w makiecie wobec 23.5px tutaj).
     Brakowało tego jednego prymitywu — dopisane 2026-08-18 przy okazji
     pomiaru kotwicy `plakietka-statusu`. */
  line-height: normal;
  letter-spacing: -0.1px;
  white-space: nowrap;
}

.kp-tag--micro {
  padding: 3px 8px;
  font-weight: 800;
  font-size: 9.5px;
  letter-spacing: 0.6px;
  text-transform: uppercase;
}


/* ── Kolor przedmiotu — mapa klucz → token ───────────────────────
   JEDYNE miejsce, w którym klucz spotyka się z tokenem. Python i szablony
   operują wyłącznie kluczami (`ev-color-math`), a klucz liczy
   `apps/journal/subject_colors.resolve()` — deterministycznie, przez
   `zlib.crc32`, żeby kolor nie zmieniał się między workerami ani po
   restarcie.

   AWANS Z WYSPY KALENDARZA (2026-08-19), po tym jak mapa zyskała drugiego
   konsumenta: ekran lekcji rysuje nią pasek przedmiotu w nagłówku. Reguła
   wejścia do katalogu mówi, że wzorzec przenosi się przy drugim konsumencie —
   a dwie kopie tej mapy rozjechałyby się przy pierwszej korekcie kolorystyki
   i nikt by tego nie zauważył, bo strażnik arkuszy pilnuje nazw `.kp-*`,
   nie `.ev-color-*`.

   Klasa ustawia WYŁĄCZNIE `--ev-accent`. Co z tym akcentem zrobi konsument —
   obramowanie, pasek, wypełnienie liczone `color-mix` — jest jego sprawą.

   TRZECI KONSUMENT (2026-08-29): kolor PLACÓWKI w zakładce Plan
   (`apps/education/school_colors.py`). Mapa jest z definicji generyczna —
   „klucz → akcent" — więc placówki reużywają jej części kategorycznej zamiast
   zakładać drugą kopię. Nazwa `ev-color-*` została historyczna (powstała dla
   przedmiotów); nie zmieniamy jej, bo czytają ją trzy moduły Pythona
   i cztery szablony, a przemianowanie kosztowałoby więcej niż wnosi. */

/* — kolor przedmiotu: 9 przypisań jawnych (tokeny --subj-*) — */
.ev-color-math      { --ev-accent: var(--subj-math); }
.ev-color-polish    { --ev-accent: var(--subj-polish); }
.ev-color-english   { --ev-accent: var(--subj-english); }
.ev-color-physics   { --ev-accent: var(--subj-physics); }
.ev-color-chemistry { --ev-accent: var(--subj-chemistry); }
.ev-color-info      { --ev-accent: var(--subj-info); }
.ev-color-geo       { --ev-accent: var(--subj-geo); }
.ev-color-history   { --ev-accent: var(--subj-history); }
.ev-color-bio       { --ev-accent: var(--subj-bio); }

/* — kolor przedmiotu: paleta kategoryczna (fallback, 19 kolorów bez blue) — */
.ev-color-red     { --ev-accent: var(--c-red); }
.ev-color-coral   { --ev-accent: var(--c-coral); }
.ev-color-orange  { --ev-accent: var(--c-orange); }
.ev-color-amber   { --ev-accent: var(--c-amber); }
.ev-color-yellow  { --ev-accent: var(--c-yellow); }
.ev-color-olive   { --ev-accent: var(--c-olive); }
.ev-color-lime    { --ev-accent: var(--c-lime); }
.ev-color-green   { --ev-accent: var(--c-green); }
.ev-color-emerald { --ev-accent: var(--c-emerald); }
.ev-color-teal    { --ev-accent: var(--c-teal); }
.ev-color-cyan    { --ev-accent: var(--c-cyan); }
.ev-color-indigo  { --ev-accent: var(--c-indigo); }
.ev-color-violet  { --ev-accent: var(--c-violet); }
.ev-color-purple  { --ev-accent: var(--c-purple); }
.ev-color-magenta { --ev-accent: var(--c-magenta); }
.ev-color-pink    { --ev-accent: var(--c-pink); }
.ev-color-clay    { --ev-accent: var(--c-clay); }
.ev-color-brown   { --ev-accent: var(--c-brown); }
.ev-color-slate   { --ev-accent: var(--c-slate); }

/* ── Pole wyboru ─────────────────────────────────────────────────
   Awans z wyspy dashboardu (`.dsh-checkbox`) — drugim konsumentem jest
   moduł PPP (obecni na spotkaniu, wybór uczniów w planerze).
   `accent-color` zamiast własnego rysowania: natywna kontrolka zachowuje
   obsługę klawiatury i stan `:indeterminate`, których podrobiony kwadracik
   nie ma.                                                                */
.kp-check {
  width: 16px;
  height: 16px;
  border-radius: var(--r-chip);
  accent-color: var(--blue-600);
  flex: 0 0 auto;
}

/* ── Przełącznik ─────────────────────────────────────────────────
   Awans z wyspy personalizacji (`.prs-switch`) — drugim konsumentem jest
   hub ustawień, który niesie 21 pól wyboru przeniesionych z dwóch dawnych
   stron administracyjnych.

   Kontrolką jest ukryte `input[type=checkbox]` (`.kp-switch-input`), a `<span
   class="kp-switch">` jest wyłącznie rysunkiem — stąd `+` w selektorach
   stanu. Ukrycie przez `clip-path`, nie `display: none`: kontrolka musi
   zostać w kolejności czytania i przyjmować fokus.                        */
.kp-switch-input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.kp-switch {
  position: relative;
  flex: 0 0 auto;
  width: 40px;
  height: 22px;
  border-radius: var(--r-pill);
  background: var(--surface-well);
  border: 1px solid var(--border);
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}

.kp-switch::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: var(--r-pill);
  --surface: var(--surface-1);
  background: var(--surface);
  box-shadow: var(--shadow-soft);
  transition: transform var(--dur-fast) var(--ease-out);
}

.kp-switch-input:checked + .kp-switch {
  background: var(--blue-600);
  border-color: var(--blue-600);
}

.kp-switch-input:checked + .kp-switch::after {
  transform: translateX(18px);
}

.kp-switch-input:disabled + .kp-switch {
  opacity: 0.6;
}

.kp-switch-input:focus-visible + .kp-switch {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* Ten sam ruch co przy `--warning` niżej i z tego samego powodu: surowy
   `--status-ok` (rgb(68,131,97)) na własnym tincie daje 3.99:1 w trybie
   jasnym. `--status-ok-ink` istniał już dla `.kp-alert--ok` i nikt go
   pod znacznik nie podpiął. Zmierzone po podmianie: 4.65:1.

   RÓŻNICA wobec `--warning`: tam token ciemny ma wartość identyczną
   z surowym, więc tryb ciemny się nie ruszał. Tutaj jest inny, więc ciemny
   ZMIENIA się: zmierzone 5.94:1 → 4.97:1. To utrata zapasu, nie zejście pod próg —
   i cena za to, że znacznik przestaje być nieczytelny w trybie jasnym oraz
   że mówi tym samym odcieniem co `.kp-alert--ok`. Wartości tokena NIE
   ruszamy: czyta go dziś alert i podmiana przemalowałaby go przy okazji. */
.kp-tag--ok      { color: var(--status-ok-ink);  background: var(--status-ok-bg); }

/* Tekst bierze --status-warning-INK, nie --status-warning. Ten drugi
   (rgb(255,106,0)) na własnym 10-procentowym tincie daje 2.58:1 — ponad dwa
   razy pod progiem 4.5:1. Liczba nie jest nowa: stoi wpisana przy
   `--status-warning-ink` w tokens-colors.css i to dla niej ten token
   powstał, ale podpięto go wtedy wyłącznie pod `.kp-chip--warning`.
   Znacznik został na starym odcieniu i przez to ~50 miejsc w produkcie
   malowało ostrzeżenia tekstem nie do odczytania.
   Zmierzone w przeglądarce po podmianie: 4.63:1.

   TRYB CIEMNY bez zmian: tam `--status-warning-ink` przyjmuje wartość
   identyczną z `--status-warning` (rgb(255,160,80), 6.09:1), więc ta
   podmiana dotyka wyłącznie trybu jasnego. */
.kp-tag--warning { color: var(--status-warning-ink); background: var(--status-warning-bg); }
.kp-tag--urgent  { color: var(--status-urgent);  background: var(--status-urgent-bg); }
.kp-tag--info    { color: var(--status-info);    background: var(--status-info-bg); }
.kp-tag--neutral { color: var(--muted);          background: var(--bg-sunken); }
.kp-tag--draft   { color: var(--c-violet);       background: color-mix(in srgb, var(--c-violet) 13%, var(--surface)); }

/* ════════════════════════════════════════════════════════════════
   DOPISANE PRZEZ merge-communication-channels (wyspa wiadomości)

   Druga oś kompozytora — „czym" — czyli sekcja kanałów wysyłki, tabela
   numerów, zestawienia salda i kosztu oraz okno potwierdzenia.

   Okno stoi na `<dialog>` i nosi `.kp-dialog-*`, NIE `.kp-modal-*`:
   zamknięty `<dialog>` nie ma atrybutu `hidden`, więc globalny
   `static/kropka-ds/modal.js` zgasiłby je na stałe.
   ════════════════════════════════════════════════════════════════ */

.kp-msg-channels {
  margin-top: 24px;
  padding-top: 20px;
  border-top: 1px solid var(--border);
}

.kp-msg-channel-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 10px;
}

/* Przełącznik kanału — pigułka, nie checkbox systemowy. Stan „włączony"
   niesie wypełnienie, żeby dało się go odczytać jednym spojrzeniem obok
   ceny wysyłki.                                                        */
.kp-msg-channel {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  --surface: var(--surface-1);
  background: var(--surface);
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-2);
  cursor: pointer;
  transition: background 120ms, border-color 120ms, color 120ms;
}

.kp-msg-channel:hover { background: var(--bg-sunken); }
.kp-msg-channel input { accent-color: var(--blue-600); }

.kp-msg-channel--on {
  border-color: var(--blue-600);
  background: color-mix(in srgb, var(--blue-600) 10%, var(--surface));
  color: var(--blue-600);
}

/* Panel kanału płatnego — otwiera się pod pigułkami. */
.kp-msg-paid {
  margin-top: 16px;
  padding: 16px;
  border: 1px solid var(--border);
  border-radius: var(--r-card);
  background: var(--bg-sunken);
}

.kp-msg-paid + .kp-msg-paid { margin-top: 12px; }

.kp-msg-counter {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 6px;
  font-family: var(--font-body);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

.kp-msg-counter strong { color: var(--ink-2); font-weight: 600; }

/* Podgląd treści, która naprawdę pójdzie bramką, gdy włączony jest tryb
   „bez znaków specjalnych". Wgłębienie, nie karta: to jest echo treści
   z pola wyżej, a nie osobny byt do wypełnienia. */
.kp-msg-gsm7 {
  margin-top: 8px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--r-chip);
  background: var(--surface-well);
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--muted);
}

.kp-msg-gsm7-head { margin: 0; }

/* Treść bramkowa czcionką o stałej szerokości: różnica między `-` a `–`
   i między `"` a `”` jest w proporcjonalnej ledwo widoczna, a to właśnie
   ją ten podgląd ma pokazać. `pre-wrap` zachowuje spacje i łamania. */
.kp-msg-gsm7-text {
  margin: 6px 0 0;
  /* Drugi konsument się zgłosił (lista tematów planu nauczania), więc stos
     przeszedł do tokenu `--font-mono` w `tokens-fonts.css` — zgodnie
     z warunkiem, który stał tu wcześniej wpisany wprost. */
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  color: var(--ink-2);
}

.kp-msg-gsm7-warn {
  margin: 6px 0 0;
  color: var(--c-orange);
}

/* Reguła numerów: uczeń / matka / ojciec na całą wysyłkę. */
.kp-msg-rule {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 8px;
}

/* Raport rozwiązania odbiorców. Odbiorcy bez numeru MUSZĄ być wymienieni
   z nazwiska — sama różnica liczników nie jest informacją o pominięciu. */
.kp-msg-resolve {
  margin-top: 12px;
  font-family: var(--font-body);
  font-size: 13px;
  color: var(--ink-2);
}

.kp-msg-resolve summary {
  cursor: pointer;
  font-size: 12px;
  color: var(--blue-600);
}

.kp-msg-numbers {
  width: 100%;
  margin-top: 10px;
  border-collapse: collapse;
  font-family: var(--font-body);
  font-size: 12px;
}

.kp-msg-numbers th,
.kp-msg-numbers td {
  padding: 6px 8px;
  border-bottom: 1px solid var(--border);
  text-align: left;
  color: var(--ink-2);
}

.kp-msg-numbers th { color: var(--muted); font-weight: 600; }
.kp-msg-numbers td:not(:first-child) { text-align: center; }
.kp-msg-numbers input { accent-color: var(--blue-600); }

.kp-msg-numbers-wrap { max-height: 280px; overflow: auto; }

.kp-msg-table {
  border: 1px solid var(--border);
  border-radius: var(--r-card);
  background: var(--surface);
  overflow: hidden;
}

.kp-msg-table-head {
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
  background: var(--paper-2);
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

.kp-msg-table-row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 7px 12px;
  font-family: var(--font-body);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
}

.kp-msg-table-row + .kp-msg-table-row { border-top: 1px solid var(--border); }
.kp-msg-table-row strong { font-weight: 600; }
.kp-msg-table-row--total { background: var(--bg-sunken); font-weight: 600; }

/* ── Boczna kolumna zestawień (kompozytor) ────────────────────────
   Wraca z ekranu `sms_compose` usuniętego w `abfb578f`. `.kp-msg--aside`
   MUSI stać po `.kp-msg--wide` (wiersz 1258): korzeń niesie obie nazwy
   naraz i przy równej specyficzności wygrywa późniejsza deklaracja. */

/* 860 formularza + 20 gapu + 320 kolumny. `container-type` czyni z korzenia
   kontener zapytań — próg niżej mierzy WŁASNĄ szerokość tej wyspy, a nie
   szerokość okna. To nie kosmetyka: pasek boczny zjada 274px w stanie
   przypiętym i 102px w zwiniętym (`base.html`), więc próg na `@media`
   znaczył dwie różne rzeczy zależnie od preferencji użytkownika. Przy
   1440×900 z przypiętym paskiem siatka włączała się mając 1140px i
   odbierała formularzowi 60px, których nie miała skąd wziąć. */
.kp-msg--aside {
  max-width: 1200px;
  container-type: inline-size;
}

/* Jedna kolumna: formularz zostaje przy projektowanych 860px zamiast
   rozlewać się na całą wyspę (do 1199px szerokości kontenera). */
.kp-msg-layout {
  display: grid;
  grid-template-columns: minmax(0, 860px);
  gap: 20px;
  align-items: start;
  margin-top: 20px;
}

.kp-msg-aside {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* NIE ZDEJMUJ tego `flex-shrink`. Kolumna jest kontenerem flex z
   `max-height`, a domyślne `flex-shrink: 1` sprawia, że przy braku miejsca
   flexbox ZGNIATA panele proporcjonalnie zamiast przepełnić kontener.
   Każdy panel to `.kp-msg-table` z `overflow: hidden`, więc nadmiar był
   po cichu OBCINANY — bez paska przewijania i bez żadnego sygnału;
   `aside.scrollHeight === aside.clientHeight` twierdziło, że nic nie
   wystaje. Zmierzone 2026-08-10 przy 1440×900: z kolumny znikały
   „wiadomości MMS", „Saldo po wysyłce" i legenda kodowań, a przy
   1366×768 także „SMS + MMS" — czyli akurat kwoty. Kontekst:
   `docs/DECYZJE.md` → „Kolumna kosztów obcinała wiersze zamiast się
   przewijać". */
.kp-msg-aside > * { flex-shrink: 0; }

/* Kolumna staje obok formularza od chwili, gdy oboje mieszczą się naraz:
   860 formularza + 20 gapu + 260 minimalnej kolumny = 1140.

   Tor formularza jest SZTYWNY (do 860), tor kolumny ELASTYCZNY — dzięki
   temu przy niedoborze zwęża się kolumna, a nie formularz, i jeden próg
   trafia w oba końce zakresu: przy 1140 kolumna ma 260px, przy 1200
   (kres `max-width` wyspy) dokładnie projektowe 320px. Odwrotny podział
   — `minmax(0, 1fr) 320px` — trzymał kolumnę na sztywnych 320 i to
   formularz schodził do 800px na 13-calowym laptopie. */
@container (min-width: 1140px) {
  .kp-msg-layout { grid-template-columns: minmax(0, 860px) minmax(260px, 1fr); }
  .kp-msg-aside  {
    position: sticky;
    top: 20px;
    max-height: calc(100vh - 40px);
    overflow-y: auto;
  }
}

/* ── Panel zwijany w kolumnie zestawień ──────────────────────────
   Tablica referencyjna „ile znaków w N częściach" to 385 z 840px
   naturalnej wysokości kolumny — prawie połowa miejsca na materiał,
   który nigdy się nie zmienia. Zwinięta domyślnie, żeby wiersze kosztu
   miały pierwszeństwo przy niedoborze wysokości. */
.kp-msg-fold > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  cursor: pointer;
  list-style: none;
}

.kp-msg-fold > summary::-webkit-details-marker { display: none; }

/* Zwinięty panel to sam nagłówek — kreska pod nim wisiałaby w powietrzu. */
.kp-msg-fold:not([open]) > summary { border-bottom: 0; }

.kp-msg-fold-chevron {
  flex-shrink: 0;
  transition: transform 120ms;
}

.kp-msg-fold[open] .kp-msg-fold-chevron { transform: rotate(180deg); }

/* Cena za sztukę pod etykietą wiersza — WEWNĄTRZ wiersza, nie obok niego.
   Osobny element między wierszami zerwałby sąsiedztwo
   `.kp-msg-table-row + .kp-msg-table-row` i następny wiersz zostałby bez kreski. */
.kp-msg-table-sub {
  display: block;
  font-style: normal;
  font-size: 11px;
  color: var(--hint);
}

/* Wiersz scenariusza odpowiadający bieżącemu zaznaczeniu kanałów.
   Barwa liczona od `--surface`, więc tryb ciemny wychodzi sam. Wprost
   `--blue-050` dałby w ciemnym świecący blok — skala blue nie ma
   nadpisania ciemnego (komentarz w `tokens-colors.css:286`). */
.kp-msg-table-row--active {
  --surface: var(--surface-1);
  background: var(--surface);
  background: color-mix(in srgb, var(--blue-600) 10%, var(--surface));
  font-weight: 600;
  color: var(--ink);
}

.kp-msg-table-foot {
  padding: 6px 12px 10px;
  font-family: var(--font-body);
  font-size: 11px;
  color: var(--hint);
}

/* Tablica referencyjna „Ile SMS wg długości". */
.kp-msg-parts {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-body);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
}

.kp-msg-parts th,
.kp-msg-parts td { padding: 4px 12px; text-align: right; }

.kp-msg-parts th:first-child,
.kp-msg-parts td:first-child { text-align: left; }

.kp-msg-parts th {
  font-size: 11px;
  font-weight: 600;
  color: var(--muted);
  border-bottom: 1px solid var(--border);
}

.kp-msg-parts-row--on {
  --surface: var(--surface-1);
  background: var(--surface);
  background: color-mix(in srgb, var(--blue-600) 10%, var(--surface));
}

.kp-msg-parts-col--on { color: var(--ink); font-weight: 600; }

/* Nagłówek aktywnej kolumny. Osobna reguła, bo `.kp-msg-parts th`
   ma wyższą specyficzność i inaczej wygrywa z samym `--on`. */
.kp-msg-parts th.kp-msg-parts-col--on { color: var(--ink); }

/* Kolumna nieaktywna przygaszona — odpowiednik starego `opacity-30`,
   ale barwą tokenową zamiast przezroczystością. */
.kp-msg-parts td:not(:first-child):not(.kp-msg-parts-col--on) { color: var(--hint); }

/* Wynik bramki na ekranie szczegółów rozsyłki. */
.kp-msg-gateway {
  margin-top: 16px;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: var(--r-card);
  --surface: var(--surface-1);
  background: var(--surface);
  font-family: var(--font-body);
  font-size: 13px;
  color: var(--ink-2);
}

.kp-msg-gateway-list {
  margin: 8px 0 0;
  padding-left: 18px;
  font-size: 12px;
  color: var(--muted);
}

/* ── Okno potwierdzenia kosztu ──────────────────────────────────── */

.kp-dialog {
  width: min(440px, calc(100vw - 32px));
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--r-card-lg);
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-soft);
}

.kp-dialog::backdrop { background: var(--overlay); }

.kp-dialog-head {
  padding: 18px 20px 0;
  font-family: var(--font-display);
  font-size: 18px;
  font-weight: 700;
  letter-spacing: -0.3px;
}

.kp-dialog-body { padding: 12px 20px 4px; }

.kp-dialog-foot {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 16px 20px 18px;
}

/* ── Wyszukiwarka globalna — okno palety ────────────────────────── */

/* NAZWA MA ZNACZENIE: `.kp-search-*` na `<dialog class="kp-dialog …">`,
   NIGDY `.kp-modal*`. `modal.js` gasi przy każdym Escape wszystko, co pasuje
   do `.kp-modal:not([hidden])` — a zamknięty `<dialog>` atrybutu `hidden`
   nie ma (jego widoczność niesie `open`), więc pod tamtą nazwą okno zostałoby
   wygaszone na stałe po pierwszym Escape na stronie. To się w tym projekcie
   już zdarzyło (patrz komentarz w `modal.js`).

   Prymitywy stoją w KATALOGU, nie w arkuszu strony, bo mają dwóch
   konsumentów: okno renderowane z `base.html` na każdej stronie i wiersz
   wyniku na `/szukaj/`. Ten sam wzorzec co `.kp-news-*`. */

/* UWAGA NA KONTRAKT `.kp-dialog`: sam `<dialog>` jest PRZEZROCZYSTY
   (`dialog.kp-dialog { background: transparent }`, wyżej w tym pliku),
   a powierzchnię — tło, krawędź, promień, cień — maluje dopiero
   `.kp-dialog-panel` w środku. Okno bez panelu renderuje się bez tła; nie
   widać tego od razu na jasnym motywie, bo prześwituje biała strona pod
   spodem. Stąd struktura: <dialog class="kp-dialog kp-search-dialog">
                              <div class="kp-dialog-panel kp-search-panel">

   Paleta stoi bliżej GÓRY, nie w pionowym środku: wyśrodkowana skakałaby
   przy każdej zmianie liczby wyników. */
.kp-search-dialog { margin-top: 9vh; }

/* SZEROKOŚĆ NA SAMYM `<dialog>`, nie tylko na panelu — i to nie jest nadmiar.
   Okno modalne centruje się `margin: auto` względem WŁASNEGO pudełka, więc gdy
   pudełko jest węższe niż panel w środku, panel wystaje w prawo o połowę
   różnicy i okno wygląda na przesunięte. Zmierzone: pudełko 440, panel 640,
   przesunięcie +100 px na każdej szerokości ekranu.

   Skąd 440: w katalogu stoi DRUGA deklaracja `.kp-dialog` (sekcja „Okno
   potwierdzenia kosztu"), która nadaje `width: min(440px, …)` KAŻDEMU oknu
   o tej nazwie. `dialog.kp-dialog` wyżej nie deklaruje szerokości w ogóle,
   więc tamta reguła rządzi także paletą. Własna szerokość odwiązuje paletę od
   cudzego okna — dopóki `.kp-dialog` nie przestanie być używane jak nazwa
   jednego komponentu zamiast nazwy rodzajowej. */
dialog.kp-search-dialog {
  width: min(640px, calc(100vw - 32px));
}

/* Zawężenie `.kp-dialog-panel`. Specyficzność (0,2,1) — tyle samo, co
   `dialog.kp-dialog .kp-dialog-panel`, a reguła stoi w pliku niżej, więc
   wygrywa. Zdejmuje `padding: 26px` prymitywu (pola i wiersze niosą własne
   odstępy) i poszerza okno, żeby tytuł z podtytułem mieściły się w wierszu. */
dialog.kp-search-dialog .kp-search-panel {
  width: min(640px, calc(100vw - 32px));
  max-height: min(72vh, 620px);
  padding: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.kp-search-field {
  display: flex;
  align-items: center;
  gap: 12px;
  flex: none;
  padding: 16px 18px;
  border-bottom: 1px solid var(--border);
  color: var(--muted);
}

/* Zawężenie o `.kp-field`, żeby zdjąć prymityw pola: znacznik nosi tę klasę
   WYŁĄCZNIE jako opt-in do furtki `:not(.kp-field)` w bloku trybu ciemnego
   (`base.html`), a nie po jego wygląd. Pole palety jest bezramkowe i siedzi
   w wierszu z lupą — obramowanie, promień i własne tło prymitywu byłyby tu
   drugim pudełkiem w pudełku. Ten sam zabieg co kiedyś w pigułce paska. */
.kp-search-input.kp-field {
  width: auto;
  padding: 1px 2px;
  background: transparent;
  border: none;
  border-radius: 0;
}

.kp-search-input {
  flex: 1;
  min-width: 0;
  border: none;
  outline: none;
  background: transparent;
  /* Krój display i 17px — pole zapytania jest największym tekstem w oknie,
     bo to jedyne miejsce, w którym użytkownik cokolwiek pisze. */
  font-family: var(--font-display);
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.3px;
  padding: 1px 2px;
  color: var(--ink);
}

/* Podpowiedź wraca do kroju treści i wagi zwykłej — inaczej pusta paleta
   wygląda, jakby ktoś już coś wpisał. */
.kp-search-input::placeholder {
  color: var(--hint);
  font-weight: 500;
  letter-spacing: normal;
}

.kp-search-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding-bottom: 6px;
  transition: opacity var(--dur-fast) var(--ease-out);
}

/* Stan „odpowiedź w drodze": poprzedni wynik ZOSTAJE, przygaszony.
   Świadomie nie szkielet — podmiana treści na placeholder przy locie rzędu
   200 ms czyta się WOLNIEJ niż przyciemnienie, bo oko traci punkt odniesienia. */
.kp-search-list--stale { opacity: 0.55; pointer-events: none; }

.kp-search-group-label {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px 5px;
  --surface: var(--surface-1);
  background: var(--surface);
  font-family: var(--font-body);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 1.1px;
  text-transform: uppercase;
  color: var(--muted);
}

/* „+N więcej" przy grupie uciętej limitem. Prowadzi na stronę wyników
   z zakładką tego rodzaju, więc jest odnośnikiem, nie przyciskiem —
   ma działać środkowym klikiem i „otwórz w nowej karcie". */
.kp-search-group-more {
  margin-left: auto;
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0;
  text-transform: none;
  color: var(--blue-600);
  border-radius: var(--r-chip);
  padding: 2px 4px;
}

.kp-search-group-more:hover {
  background: color-mix(in srgb, var(--blue-600) 10%, var(--surface));
}

/* Wskaźnik oczekiwania w polu. Ruch jest jedyną rzeczą, która odróżnia
   „czekam na odpowiedź" od „to jest wynik" — sama przygaszona lista czyta się
   jak wynik nieaktywny. */
@keyframes kp-search-spin { to { transform: rotate(360deg); } }

.kp-search-spin {
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  border: 2px solid var(--border);
  border-top-color: var(--blue-600);
  animation: kp-search-spin 0.7s linear infinite;
}

.kp-search-spin[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .kp-search-spin { animation-duration: 2.4s; }
}

/* Wiersz wyniku — WSPÓLNY dla palety i strony `/szukaj/`. Osobny zestaw klas
   dla strony rozjechałby oba widoki przy pierwszej korekcie wyglądu, a wtedy
   ta sama osoba wygląda inaczej w oknie i inaczej na liście.

   Układ: [bejdż rodzaju] [tytuł + wiersz podtytułu] [metka + szewron]. */
.kp-search-option {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 9px 12px;
  cursor: pointer;
  scroll-margin: 40px;
  border-radius: var(--r-card-lg);
  /* Wiersz jest odnośnikiem — kolor linku jak w makiecie. Wizualnie bez
     znaczenia (każdy tekst w środku deklaruje własny), ale `currentColor`
     obramowań i szewronu idzie za tą wartością. */
  color: var(--link);
  text-decoration: none;
  transition: background var(--dur-fast) var(--ease-out);
}

.kp-search-option:hover { background: var(--bg-sunken); }

/* Podświetlenie idzie za `aria-selected`, nie za osobną klasą stanu — jeden
   atrybut niesie i wygląd, i informację dla czytnika ekranu, więc nie da się
   ich rozjechać.

   Tint liczony WZGLĘDEM `--surface`, nie gotowym `--blue-050`: powierzchnia
   zmienia się w trybie ciemnym, a stały jasny tint zostawiłby tam biały pas.
   Deklaracja zapasowa przed `color-mix` dla silników bez jego obsługi. */
.kp-search-option[aria-selected="true"] {
  background: var(--paper-2);
  background: color-mix(in srgb, var(--blue-600) 10%, var(--surface));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--blue-600) 22%, transparent);
}

/* Bejdż rodzaju: wypełnienie surowym tokenem kategorycznym, glif w `--on-fill`.
   Barwa MUSI NIE zależeć od akcentu roli — rodzaj wyniku jest tą samą
   informacją dla każdego konta, tak samo jak kolor przedmiotu w planie lekcji.
   Sam token przychodzi z szablonu/JS jako `--kind`, bo mieszka w Pythonie. */
.kp-search-option-badge {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  border-radius: var(--r-field);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--kind, var(--c-slate));
  color: var(--on-fill);
}

.kp-search-option-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1;
}

.kp-search-option-title {
  font-family: var(--font-body);
  font-size: 13.5px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Wiersz pod tytułem: etykieta rodzaju (tylko na stronie wyników), podtytuł
   i chip trafienia. Zawija, bo przy trzyczłonowym podtytule i chipie nie
   mieści się w jednej linii na wąskim oknie. */
.kp-search-option-line {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  font-family: var(--font-body);
  font-size: 12px;
  line-height: 1.3;
  color: var(--muted);
}

/* Tekstowa etykieta rodzaju — na stronie wyników, gdzie wiersze różnych
   rodzajów sąsiadują bez nagłówków grup. W palecie rolę tę pełni nagłówek. */
.kp-search-option-kind {
  font-weight: 700;
  color: var(--kind, var(--muted));
}

.kp-search-option-subtitle {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Prawa strona wiersza — metka i szewron. */
.kp-search-option-trail {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 7px;
  flex: 0 0 auto;
}

.kp-chev {
  color: var(--border);
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}

.kp-search-option:hover .kp-chev,
.kp-search-option[aria-selected="true"] .kp-chev {
  opacity: 1;
  color: var(--muted);
  transform: translateX(2px);
}

/* Chip trafienia — „po którym polu to znaleziono". Nazwa pola, nigdy jego
   wartość: tą samą ścieżką chodzi „Wykonawca" i „PESEL". */
.kp-hintchip {
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 600;
  color: var(--ink-2);
  background: var(--bg-sunken);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  padding: 1px 8px;
  white-space: nowrap;
}

/* Metka: czego spodziewać się po kliknięciu (pobranie pliku, kotwica) albo
   w jakim stanie jest obiekt (tablica archiwalna).

   NAZWA: `.kp-filetag`, NIE `.kp-tag`. Do 2026-08-18 ten blok nazywał się
   `.kp-tag` i stał W TYM SAMYM PLIKU, NIŻEJ niż plakietka statusu (patrz
   „Plakietka statusu" wyżej) — czyli przy równej specyficzności wygrywał
   kolejnością i narzucał SWOJĄ metrykę wszystkim konsumentom tamtej nazwy.
   Skutek: 265 użyć w 79 szablonach renderowało się jako WERSALIKI 10px
   z dodatnim trackingiem zamiast plakietki 11px sentence case — czyli
   dokładnie to, czego reguła nr 8 systemu designu zabrania poza
   mikro-etykietami. Kolizja weszła 2026-08-14 z wierszem wyszukiwarki
   (`d1255427`) i była niewidoczna, bo ekran, dla którego powstała, wersalików
   akurat chciał, a żaden test nie porównuje plakietki z katalogiem.

   Metka ma dwóch konsumentów: `templates/search/_result_row.html`
   i `static/kropka-ds/kp-search.js`. Plakietka statusu ma 79 szablonów —
   dlatego to metka dostała nową nazwę, a nie odwrotnie. */
.kp-filetag {
  font-family: var(--font-body);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  border-radius: var(--r-pill);
  padding: 3px 8px;
  white-space: nowrap;
  color: var(--muted);
  background: var(--bg-sunken);
}

.kp-filetag--pdf {
  color: var(--status-urgent);
  background: var(--status-urgent-bg);
}

.kp-filetag--anchor {
  color: var(--c-cyan);
  background: color-mix(in srgb, var(--c-cyan) 12%, var(--surface));
}

/* Podświetlenie dopasowanego fragmentu. Tint liczony względem `--surface`
   z tego samego powodu, co przy wierszu wybranym — mieszanie z `transparent`
   zostawiłoby na ciemnym motywie jaskrawą plamę. */
.kp-search-option-title mark,
.kp-search-option-line mark {
  background: var(--paper-2);
  background: color-mix(in srgb, var(--c-yellow) 34%, transparent);
  color: inherit;
  border-radius: 3px;
  padding: 0 1px;
  font-weight: 700;
}

.kp-search-option-line mark { font-weight: 600; }

/* Rodzaj → token barwy. Python zwraca KLUCZ (`data-kind`), token podstawia
   arkusz — ten sam podział, co przy kolorach przedmiotów w planie lekcji.
   Wpisanie barwy do szablonu albo do JSON-a przeniosłoby literał koloru do
   warstwy, w której jest zakazany, i rozjechałoby tryb ciemny.

   Kolejność jak w rejestrze źródeł: ludzie, potem rzeczy i sprawy, na końcu
   dwa rodzaje spoza indeksu. */
.kp-search-option[data-kind="student"]       { --kind: var(--c-green); }
.kp-search-option[data-kind="staff"]         { --kind: var(--c-emerald); }
.kp-search-option[data-kind="parent"]        { --kind: var(--c-teal); }
.kp-search-option[data-kind="class"]         { --kind: var(--c-cyan); }
.kp-search-option[data-kind="board"]         { --kind: var(--c-indigo); }
.kp-search-option[data-kind="board_card"]    { --kind: var(--c-violet); }
.kp-search-option[data-kind="leave"]         { --kind: var(--c-magenta); }
.kp-search-option[data-kind="delegation"]    { --kind: var(--c-pink); }
.kp-search-option[data-kind="trip"]          { --kind: var(--c-orange); }
.kp-search-option[data-kind="lesson_series"] { --kind: var(--c-amber); }
.kp-search-option[data-kind="certificate"]   { --kind: var(--c-olive); }
.kp-search-option[data-kind="equipment"]     { --kind: var(--c-clay); }
.kp-search-option[data-kind="help"]          { --kind: var(--c-brown); }
.kp-search-option[data-kind="action"]        { --kind: var(--c-purple); }
.kp-search-option[data-kind="page"]          { --kind: var(--c-slate); }

.kp-search-empty,
.kp-search-error {
  padding: 22px 18px;
  font-family: var(--font-body);
  font-size: 14px;
  color: var(--muted);
}

.kp-search-error { color: var(--status-urgent); }

/* Ponowienie żądania bez zamykania okna. Bez niego stan błędu jest ślepy:
   jedynym wyjściem zostaje zamknięcie palety i otwarcie jej od nowa. */
.kp-search-retry {
  display: inline-block;
  margin-top: 10px;
}

.kp-search-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  flex: none;
  padding: 11px 16px;
  border-top: 1px solid var(--border);
  background: var(--surface-well);
  font-family: var(--font-body);
}

/* Krój i kolor na DZIECIACH, nie na pojemniku — makieta trzyma je tak samo,
   a pojemnik zostaje neutralny. */
.kp-search-foot > * {
  font-size: 12px;
  color: var(--muted);
}

/* Odnośnik „zobacz wszystkie" — ten sam wzorzec, co „+N więcej" przy grupie:
   akcent niebieski jako kontrolka funkcjonalna, nie jako powierzchnia. */
.kp-search-foot .kp-search-all {
  font-weight: 700;
  letter-spacing: 0.2px;
  color: var(--blue-600);
  border-radius: var(--r-chip);
  padding: 2px 4px;
  text-decoration: none;
  /* Bez tego na wąskim ekranie etykieta łamie się na dwie linie i stopka
     rośnie o 15px (zmierzone na 390px). */
  white-space: nowrap;
}

.kp-search-foot .kp-search-all:hover {
  background: color-mix(in srgb, var(--blue-600) 10%, var(--surface));
}

/* Tekst dla czytnika ekranu, niewidoczny dla oka. Trafia do katalogu, bo
   „ogłoś liczbę wyników, nie pokazując jej" to potrzeba przekrojowa,
   nie właściwość tego jednego okna. */
.kp-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Na telefonie paleta idzie na cały ekran. Powód jest konkretny: klawiatura
   ekranowa zjada mniej więcej połowę okna w chwili wejścia w pole, a pływająca
   karta zostałaby wtedy zgnieciona do kilku wierszy. `dvh`, nie `vh` —
   `vh` na iOS Safari nie uwzględnia paska adresu i klawiatury. */
@media (max-width: 640px) {
  /* Szerokość i tu na samym oknie, z tego samego powodu co wyżej: panel idzie
     na `100vw`, więc pudełko musi za nim nadążyć, inaczej pełny ekran wystaje
     poza własne okno. */
  .kp-search-dialog { margin: 0; }

  dialog.kp-search-dialog { width: 100vw; }

  dialog.kp-search-dialog .kp-search-panel {
    width: 100vw;
    height: 100dvh;
    max-height: 100dvh;
    border: none;
    border-radius: 0;
  }
}

/* ── Pas alertów ────────────────────────────────────────────────── */

/* AWANS z wyspy księgowości (2026-08-03). Wzorzec powstał jako `.mtr-alert*`
   w `static/accounting/mt940_registry.css` i miał jednego konsumenta; drugim
   jest strona „Rachunki placówek". Zgodnie z regułą wejścia do katalogu
   („prymityw awansuje, gdy sięgnie po niego druga strona") nazwa jest tu, a nie
   skopiowana pod drugim prefiksem — dwie kopie tych samych sześciu reguł to
   dokładnie ten dryf słownika, przed którym ostrzega readme DS.

   Alert NIE jest kartą: nosi ramkę 2px w barwie statusu, żeby czytał się jako
   komunikat, nie jako powierzchnia treści. Wariant `--info` zostaje na
   `--border`, bo informacja neutralna nie ma barwy statusu. */
.kp-alert {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  align-items: flex-start;
  padding: 16px 18px;
  border-radius: var(--r-bento);
  border: 2px solid var(--border);
  --surface: var(--surface-1);
  background: var(--surface);
}

.kp-alert__icon {
  width: 38px;
  height: 38px;
  border-radius: 12px;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* `flex: 1 1 0%` (nie tylko `min-width: 0`) — bez wymuszonej podstawy 0
   przeglądarka liczy hipotetyczny rozmiar wiersza po TREŚCI (max-content)
   PRZED shrinkiem: dwuzdaniowy tekst (np. druk 3D, formularz zgłoszenia)
   był szerszy niż `.kp-alert`, więc `flex-wrap: wrap` na kontenerze zawijał
   całą kolumnę tekstu pod ikonę zamiast obok niej — zmierzone na 375px,
   ikona lądowała nad tekstem, nie po jego lewej. Podstawa 0 + `flex-grow: 1`
   każe kolumnie od razu zająć resztę wiersza obok ikony i zawijać SŁOWA
   w środku, a nie wypychać się na osobną linię kontenera. Dla dwóch
   konsumentów z `.kp-alert__action` (mt940_registry, tasks/_panel_perf)
   zachowanie nie zmienia się wizualnie: margines `margin-left: auto` tej
   akcji i tak dostawał całą wolną przestrzeń, którą teraz w tej samej
   ilości zajmuje `flex-grow` kolumny tekstu. */
.kp-alert__body {
  display: flex;
  flex-direction: column;
  flex: 1 1 0%;
  gap: 3px;
  min-width: 0;
}

.kp-alert__title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 16px;
  letter-spacing: -0.4px;
  color: var(--ink);
}

.kp-alert__text {
  font-size: 12.5px;
  color: var(--ink-2);
  text-wrap: pretty;
}

.kp-alert__action {
  margin-left: auto;
  align-self: center;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 12.5px;
  white-space: nowrap;
  text-decoration: none;
}

/* Wariant „pilne" — plakietka WYPEŁNIONA barwą statusu, glif na `--on-fill`.
   Token, nie literał `#fff`: `--on-fill` jest opisany wprost jako tekst leżący
   NA wypełnieniu i jest stały w obu motywach, więc niesie informację, że ta
   biel jest decyzją. Token motywu (`--surface`) byłby tu błędem — przełączyłby
   się razem z motywem, a `--status-urgent` w ciemnym pozostaje czerwienią. */
.kp-alert--urgent {
  border-color: var(--status-urgent);
  background: var(--status-urgent-bg);
}
.kp-alert--urgent .kp-alert__icon { background: var(--status-urgent); color: var(--on-fill); }
.kp-alert--urgent .kp-alert__title { color: var(--status-urgent); }
.kp-alert--urgent .kp-alert__action { color: var(--status-urgent); }

/* Wariant „ostrzeżenie" — plakietka na tincie, glif w `--ink`.
   Biel na `--status-warning` daje 2,87:1 w trybie jasnym i 1,84:1 w ciemnym —
   `--ink` na tincie przechodzi z zapasem w obu motywach, więc zostaje tu mimo
   że `--status-warning-ink` (dopisany później dla `.kp-chip--warning`, tekst
   11px) już istnieje: ten token jest kalibrowany pod inny kontekst (chip),
   nie pod kwadrat ikony alertu. To ta sama zmierzona decyzja co w transfers_list. */
.kp-alert--warning {
  border-color: var(--status-warning);
  background: var(--status-warning-bg);
}
.kp-alert--warning .kp-alert__icon { background: var(--status-warning-bg); color: var(--ink); }

.kp-alert--info {
  border-color: var(--border);
  background: var(--bg-sunken);
}
.kp-alert--info .kp-alert__icon { background: var(--surface); color: var(--muted); }

/* Wariant „potwierdzenie" — dopisany przy migracji edukacji na tokeny DS
   (Etap 2a): dotąd `--urgent`/`--warning`/`--info` pokrywały błąd, uwagę
   i neutralny komunikat, ale żaden wariant nie niósł pozytywnego
   potwierdzenia zapisu (`messages.success`). Ta sama logika koloru co
   `--urgent`, przeniesiona na `--status-ok` — z JEDNĄ różnicą: tytuł
   i akcja biorą `--status-ok-ink`, nie surowy `--status-ok`. Surowa
   zieleń na tinach tego alertu (tekst 16px/700, próg AA 4.5:1, „duży
   tekst" zaczyna się dopiero od 18.66px bold) daje 3.99:1 na `--surface`
   i 3.55:1 na `--paper` — pod progiem w obu przypadkach. `--status-ok-ink`
   (dopisany wcześniej właśnie po to) daje w jasnym 5.26:1 na `--surface`
   i 4.65:1 na `--paper`; ciemny wariant tokenu (z body.dark) daje 6.54:1
   i 7.29:1 — oba tryby z zapasem. Ikona zostaje na surowym `--status-ok`,
   bo tekst na niej to `--on-fill`, kalibrowany osobno. */
.kp-alert--ok {
  border-color: var(--status-ok);
  background: var(--status-ok-bg);
}
.kp-alert--ok .kp-alert__icon { background: var(--status-ok); color: var(--on-fill); }
.kp-alert--ok .kp-alert__title { color: var(--status-ok-ink); }
.kp-alert--ok .kp-alert__action { color: var(--status-ok-ink); }

/* ── Przeszukiwalna lista wyboru ─────────────────────────────────────────
   Awans z `static/trips/trips.css` (`.tr-picker*`) przy zmianie
   `redesign-delegations-ds`: wzorzec uzyskał drugiego konsumenta — wycieczki
   wybierają nim kadrę i uczniów, delegacje osobę delegowaną, akceptującą
   i kraj. Nazwa jest RODZAJOWA, więc od tej chwili ma tu jedyną gołą
   definicję w produkcie; definicje w `trips.css` zniknęły w tej samej
   zmianie, bo dwie gołe definicje tej samej nazwy to stan, który
   `tests/test_ds_stylesheets.py` zgłasza jako błąd.

   Wartości przeniesione bez zmian — awans nie jest okazją do przeprojektowania
   wzorca, tylko do przeniesienia go tam, gdzie należy. */

.kp-picker {
  border: 1px solid var(--border);
  border-radius: var(--r-card);
  overflow: hidden;
  max-height: 380px;
  overflow-y: auto;
  background: var(--surface);
}

.kp-picker--short { max-height: 280px; }

.kp-picker-head {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: 7px var(--sp-3);
  background: var(--surface-well);
  border-bottom: 1px solid var(--border);
}

.kp-picker-title {
  font-family: var(--font-body);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--ink-2);
}

.kp-picker-toggle {
  appearance: none;
  border: 0;
  background: transparent;
  padding: 0;
  cursor: pointer;
  font-family: var(--font-body);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: var(--muted);
  transition: color var(--dur-fast) ease;
}

.kp-picker-toggle:hover { color: var(--blue-600); }

.kp-picker-body {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 0 var(--sp-3);
  padding: 4px var(--sp-2);
}

/* Wariant jednokolumnowy — lista w wąskim oknie (wybór kraju) nie ma szerokości
   na siatkę auto-fit, a rozbita na dwie kolumny czyta się jak dwie listy. */
.kp-picker--single .kp-picker-body { grid-template-columns: 1fr; }

.kp-picker-row {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 5px 8px;
  border-radius: var(--r-chip);
  font-size: 13px;
  color: var(--ink-2);
  cursor: pointer;
  transition: background var(--dur-fast) ease;
}

.kp-picker-row:hover { background: var(--paper-2); }
.kp-picker-row input { accent-color: var(--blue-600); width: 15px; height: 15px; flex: 0 0 auto; }
.kp-picker-row:has(input:checked) { color: var(--ink); font-weight: 600; }

.kp-picker-flag { font-size: 10.5px; color: var(--hint); font-style: italic; }

/* ── Opakowanie pickera: pole szukania i nota ───────────────────────
   Awans z `.dl-search*` (delegacje) przy trzecim konsumencie. Wartości
   przeniesione 1:1 — awans ma być niewidoczny na zrzucie. */

.kp-picker-search-wrap { position: relative; margin-bottom: var(--sp-2); }

.kp-picker-search-icon {
  position: absolute;
  left: 10px;
  top: 50%;
  transform: translateY(-50%);
  width: 15px;
  height: 15px;
  color: var(--hint);
  pointer-events: none;
}

.kp-picker-search { padding-left: 32px; }

.kp-picker-note {
  font-family: var(--font-body);
  font-size: 11px;
  color: var(--hint);
  margin-top: 5px;
}

/* ── Chip opcji ─────────────────────────────────────────────────
   Klikalna pigułka z polem wyboru w środku: `<label class="kp-optchip">`
   opakowuje `<input type="checkbox">` albo `radio`. Awans z `.ti-chip`
   (import tematów) przy drugim konsumencie — ekranie tworzenia odwołania
   masowego. Wartości przeniesione 1:1; awans ma być niewidoczny na zrzucie.

   To NIE jest `.kp-chip`. `.kp-chip` znaczy „znacznik statusu" i nic nie
   przyjmuje kliknięcia; ten prymityw jest kontrolką formularza. Makiety
   nazywają oba „chip" i moduł notatek już raz na tym poległ. */

.kp-optchip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 11px;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  --surface: var(--surface-1);
  background: var(--surface);
  color: var(--ink-2);
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 600;
  line-height: normal;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--dur-fast) ease, border-color var(--dur-fast) ease;
}

.kp-optchip:hover { background: var(--paper-2); }

.kp-optchip input {
  accent-color: var(--blue-600);
  width: 14px;
  height: 14px;
  flex: 0 0 auto;
  margin: 0;
}

.kp-optchip:has(input:checked) {
  border-color: var(--blue-600);
  /* Nie `var(--accent-tint)` — ten token jest w produkcie ŚWIADOMIE
     pominięty (patrz nagłówek `tokens-colors.css`), więc odwołanie do
     niego cicho gasiłoby wypełnienie (nieważna deklaracja przy liczeniu
     wartości → tło zostaje przezroczyste, widać tylko ramkę). Kanoniczna
     receptura tintu DS liczy się względem `--surface`, więc tryb ciemny
     wychodzi sam — bez reguły `body.dark`. */
  background: color-mix(in srgb, var(--blue-600) 12%, var(--surface));
  color: var(--ink);
}

/* ════════════════════════════════════════════════════════════════
   Lightbox podglądu obrazka (redesign-attachment-preview)

   DWÓCH konsumentów od startu: modal ogłoszeń na pulpicie i podgląd
   załączników w wiadomościach — stąd katalog, nie arkusz jednej wyspy.
   Kontener `.kp-lightbox` NIE stoi w żadnym szablonie: go buduje i
   dokleja do <body> `static/kropka-ds/modal.js` (druga, osobna IIFE
   w tym pliku — zobacz komentarz tam). Tu leżą tylko style.

   `.kp-lightbox` jest WŁASNĄ nazwą, nie wariantem `.kp-modal` — ta
   druga jest zastrzeżona dla kontraktu modal.js opisanego wyżej w
   tym arkuszu (stan `hidden` + jeden kontener w markupie strony).
   Lightbox ma inny kontrakt DOM z innego powodu: `_attachments.html`
   renderuje się wielokrotnie na stronie (raz na wiadomość), więc
   kontener-singleton w markupie duplikowałby ID.

   Tło zawsze `--overlay` (ta sama ciemna zasłona w obu motywach —
   patrz tokens-colors.css), więc przyciski na niej biorą kolor
   `--on-overlay`, NIE `--ink`/`--muted`: te przełączają się w trybie
   ciemnym i na ciemnym tle zlałyby się z zasłoną.                     */

.kp-lightbox {
  position: fixed;
  inset: 0;
  z-index: 10050;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-8);
  background: var(--overlay);
  backdrop-filter: blur(2px);
}

.kp-lightbox-img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: var(--r-card-lg);
  box-shadow: var(--shadow-hero);
}

/* Przypięte do rogu VIEWPORTU, nie do obrazka — jego wymiary są
   zmienne (pionowy skan kontra panoramiczne zdjęcie), a przyciski
   muszą siedzieć w tym samym miejscu niezależnie od kadru.

   Tło OPAQUE `--kp-inverse`, nie `transparent`: `--overlay`, na którym
   te przyciski stoją, jest półprzezroczystą zasłoną (rgba z alfą 0.32),
   nie stałym kolorem — w jasnym motywie pod nią przeziera `--paper`
   (jasny), więc tło przycisku wypadało ~rgb(168,169,167) i biała ikona
   `--on-overlay` dawała tam 2.37:1 (poniżej progu 3:1 z WCAG 1.4.11),
   a półprzezroczyste obramowanie 1.33:1 — po prostu niewidoczne. `--kp-inverse`
   to token opisany w tokens-colors.css jako ciemna powierzchnia w OBU
   motywach właśnie do takich przypadków: biała ikona ma pod sobą stały,
   ciemny kontrast niezależnie od tego, co świeci przez zasłonę pod spodem
   (zmierzone po zmianie: 17.5:1 w jasnym motywie, 13.3:1 w ciemnym —
   patrz raport zadania). Stan hover musi mieszać podświetlenie Z
   `--kp-inverse`, nie z `transparent` — inaczej `background` w :hover
   NADPISZE (nie doda) tę opaczność i wskaźnik wróci do tego samego
   problemu tylko podczas najechania kursorem.

   Obramowanie zostaje `color-mix(..., transparent)`, bo tło pod nim
   maluje się jako pierwsze (domyślny `background-clip: border-box`) —
   ale odsetek `--on-overlay` schodzi z 30% na 25%: przy tle opaque
   `--kp-inverse` wynikowy piksel obramowania to w praktyce mieszanka
   z KOLOREM TŁA (nie z zasłoną pod spodem), a 30% dawało 2.73:1 w
   jasnym motywie (kp-inverse jest tam ciemny, obramowanie zbyt blisko
   tła) przy 3.68:1 w ciemnym; 25% wyrównuje obie strony do ≥3:1 —
   zmierzone 3.26:1 (jasny) i 3.16:1 (ciemny), patrz raport zadania. */
.kp-lightbox-close,
.kp-lightbox-download {
  position: absolute;
  top: var(--sp-6);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border: 1px solid color-mix(in srgb, var(--on-overlay) 25%, transparent);
  border-radius: var(--r-pill);
  background: var(--kp-inverse);
  color: var(--on-overlay);
  cursor: pointer;
  text-decoration: none;
  transition: background var(--dur-fast) ease;
}

.kp-lightbox-close:hover,
.kp-lightbox-download:hover {
  background: color-mix(in srgb, var(--on-overlay) 12%, var(--kp-inverse));
}

.kp-lightbox-close:focus-visible,
.kp-lightbox-download:focus-visible {
  outline: 2px solid var(--blue-600);
  outline-offset: 2px;
}

.kp-lightbox-close { right: var(--sp-6); }
.kp-lightbox-download { right: calc(var(--sp-6) + 48px); }

/* ── Miniaturka obrazka w bąbelku wiadomości ──────────────────────
   Wyzwalacz lightboksa. Pobranie jest OSOBNĄ, opcjonalną akcją
   (`.kp-attach-img-dl`) obok miniaturki, nie skutkiem samego kliknięcia
   w podgląd — inaczej niż `.kp-attach` (załącznik nie-obrazkowy), który
   dalej ściąga na dysk od razu, bo nie ma dla niego podglądu w
   przeglądarce.                                                        */

.kp-attach-img-group {
  display: inline-flex;
  align-items: flex-end;
  gap: 6px;
}

.kp-attach-img-btn {
  display: block;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--r-card);
  overflow: hidden;
  line-height: 0;
  background: var(--bg-sunken);
  cursor: pointer;
  transition: border-color var(--dur-fast) ease;
}

.kp-attach-img-btn:hover { border-color: var(--blue-600); }

.kp-attach-img-btn:focus-visible {
  outline: 2px solid var(--blue-600);
  outline-offset: 2px;
}

.kp-attach-img {
  display: block;
  width: 132px;
  height: 96px;
  object-fit: cover;
}

.kp-attach-img-dl {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  flex-shrink: 0;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  --surface: var(--surface-1);
  background: var(--surface);
  color: var(--blue-600);
  text-decoration: none;
  transition: background var(--dur-fast) ease;
}

.kp-attach-img-dl:hover { background: var(--paper-2); }

.kp-attach-img-dl:focus-visible {
  outline: 2px solid var(--blue-600);
  outline-offset: 2px;
}

/* ============================================================
   GÓRNY PASEK — narzędzia konta w prawym górnym rogu

   ŹRÓDŁO: docs/Nowy-Design-Kropka/Górny pasek konta.dc.html
   ZMIANA: openspec/changes/gorny-pasek-konta

   DLACZEGO W KATALOGU, a nie w arkuszu wyspy: pasek jest chrome
   obecnym na KAŻDEJ stronie aplikacji. Arkusz wyspy ładuje się na
   jednej trasie, więc nie może być jego domem — ten sam argument
   stoi już przy `.kp-notif-*` w templates/base.html.

   DLACZEGO PRZESTRZEŃ `.kp-topbar-*`, a nie nazwy z makiety:
   makieta używa `.kp-dot` na liczniku oraz `.kp-label` i `.kp-badge`.
   Wszystkie trzy są ZASTRZEŻONE dla paska bocznego
   (tests/test_ds_stylesheets.py, SIDEBAR_RESERVED) i wolno je
   definiować gołą klasą wyłącznie w templates/base.html. `.kp-row`
   i `.kp-pop` są z kolei nazwami rodzajowymi — `.kp-row` już tu
   istnieje, w innym znaczeniu (flexowy wiersz listy ocen).

   ODSUNIĘCIE OD PASKA BOCZNEGO NIE JEST TUTAJ. Robi je ten sam
   przełącznik `html[data-sidebar-pinned]`, który przesuwa treść —
   reguły stoją obok siebie w templates/base.html, żeby dwie
   wartości nie rozjechały się przy korekcie szerokości paska.
   ============================================================ */

.kp-topbar {
  position: sticky;
  top: 0;
  z-index: 36;
  /* SIATKA, NIE FLEX — wyłącznie po to, żeby wyszukiwarka stała w OSI kolumny
     treści (2026-08-11, decyzja użytkownika). Skrajne tory są równe, więc
     środkowy trafia w oś niezależnie od tego, ile miejsca zajmują narzędzia
     po prawej (superadmin ma dodatkowo pigułkę kopii, reszta nie).
     `minmax(0, 520px)` pozwala pigułce ZWĘZIĆ SIĘ zamiast zejść z osi:
     przy 1280px z przypiętym paskiem bocznym kolumna treści ma 980px,
     narzędzia ~320px, więc pełne 520px centrowane bezwzględnie nachodziłoby
     na nie o ~90px. Flex z `margin: auto` tego nie umie — auto-marginesy
     dzielą wolne miejsce, a nie ustawiają w osi kontenera. */
  display: grid;
  /* Środkowy tor jest dziś PUSTY — pigułka wyszła z siatki i centruje się
     bezwzględnie w osi okna (patrz `.kp-topbar-search`). Tor zostaje, bo
     narzędzia mają jawnie `grid-column: 3`; skasowanie go przesunęłoby je
     o jeden tor w lewo. */
  grid-template-columns: 1fr minmax(0, 520px) 1fr;
  align-items: center;

  /* Szerokość grupy narzędzi po prawej — ZMIERZONA, nie zgadnięta: 371 px
     w najszerszym wariancie (superadmin, z pigułką kopii zapasowej). Służy
     wyłącznie do policzenia, ile miejsca zostaje dla pigułki wyszukiwarki,
     żeby ta nigdy na narzędzia nie weszła. Gdyby narzędzia kiedyś urosły,
     objaw jest łagodny i widoczny: pigułka zacznie ich dotykać na wąskim
     ekranie — wtedy podnieś tę liczbę, nie zdejmuj centrowania. */
  --kp-topbar-tools-w: 371px;
  /* 14px, nie --sp-4 (16px): metryka makiety wygrywa, także w prymitywach —
     ta sama zasada co przy geometrii kart kartoteki. */
  gap: 14px;
  padding: 10px 26px;
  /* TŁO NIESIE PSEUDOELEMENT `::before`, nie ten element — patrz reguła niżej.
     Sam pasek jest przezroczysty. */
  background: transparent;
  /* Krawędzie GASZONE JAWNIE mimo braku obramowania: `.kp-island *` zdejmuje
     preflight Tailwinda z POTOMKÓW, ale nie z samego korzenia wyspy — bez
     tego pasek niósłby `solid rgb(229,231,235)` przy zerowej szerokości. */
  border-style: none;
  border-color: currentColor;
}

/* ── Welon paska: półprzezroczysty, gaśnie ku dołowi ──
   Czwarta i docelowa odsłona tła tego paska (2026-08-11, decyzja
   użytkownika). Droga była taka: belka z makiety → brak tła w ogóle →
   nieprzezroczyste pasmo → welon gradientowy. Każdy krok miał powód i każdy
   jest zapisany w docs/DECYZJE.md.

   ODSTĘPSTWO OD REGUŁY 7 DESIGN SYSTEMU, ŚWIADOME I ZATWIERDZONE PRZEZ
   UŻYTKOWNIKA. CLAUDE.md mówi „Tło płaskie — żadnych gradientów, tekstur ani
   obrazów w chrome", a readme DS wylicza dwa jedyne dozwolone gradienty
   w produkcie (wypełnienie sparkline'u i dysk awatara asystenta). To jest
   trzeci i pierwszy w chrome. Nie usuwaj go jako naruszenia reguły — reguła
   ma teraz zapisany wyjątek w readme DS.

   DLACZEGO PSEUDOELEMENT, A NIE TŁO NA PASKU. Dwa powody, każdy wystarczający.
   Po pierwsze welon sięga 24px PONIŻEJ paska, żeby treść rozpływała się
   stopniowo, zamiast urywać się na krawędzi — to jest cały sens gradientu.
   Po drugie `backdrop-filter` działa na całe pudełko jednakowo i NIE DA SIĘ
   go wygasić gradientem tła; gaśnie dopiero razem z `mask-image`, a maska
   musi objąć element niosący rozmycie.

   JEDEN SPADEK, NIE DWA. Tło welonu jest PŁASKIE, a całe wygaszanie robi
   maska. Gdyby gradient siedział i w tle, i w masce, przezroczystość
   mnożyłaby się przez siebie i dół gasłby dwa razy szybciej, niż wynika
   z liczb w regule.

   MASKA NA TOKENIE, NIE NA `#000`. `mask-image` liczy się z kanału alfa, więc
   barwa nie ma znaczenia — a katalog nie może nieść literałów kolorystycznych
   (pilnuje tego tests/test_ds_stylesheets.py). `var(--ink)` jest w pełni
   nieprzezroczysty i tym samym maskuje tak samo jak czerń.

   TRYB CIEMNY JEST ODWRÓCONY WZGLĘDEM RESZTY PASKA, i to jest celowe
   (2026-08-11, decyzja użytkownika): welon ROZJAŚNIA w jasnym motywie
   i PRZYCIEMNIA w ciemnym. Do wersji z tego samego dnia było odwrotnie, bo
   mieszanie szło z `--ink`, a ten przełącza się z motywem. Odwrócenia nie da
   się wyliczyć z istniejących tokenów — żaden nie jest biały w jasnym
   i czarny w ciemnym — więc niesie je token `--veil` z `tokens-colors.css`,
   a nie reguła `body.dark` tutaj. Katalog nie ma ani jednej takiej reguły
   i nie zaczynajmy od tej. */
.kp-topbar::before {
  content: "";
  position: absolute;
  /* Pudełko paska plus ogon pod jego dolną krawędzią. */
  inset: 0 0 -24px 0;
  /* Za treścią paska, ale wewnątrz jego kontekstu układania (`z-index: 36`
     na pasku), więc welon nie chowa się pod treść strony. */
  z-index: -1;
  pointer-events: none;
  /* 45%, nie 5% jak przy welonie liczonym z `--ink`. Powód jest w palecie,
     nie w guście: `--veil` celuje w biel (jasny) i w rgb(13,15,14) (ciemny),
     a papier leży blisko obu — 14 jednostek od bieli, 6 od ciemnej wartości.
     Mała domieszka daje więc efekt niemal zerowy. Welon i tak pracuje przede
     wszystkim na TREŚCI wjeżdżającej pod pasek, a nie na samym papierze. */
  background: color-mix(in srgb, var(--veil) 45%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  /* Pełna krycie do 45% wysokości pudełka, potem zanik. 45% wypada tuż pod
     rzędem elementów, więc plakietki leżą na welonie w pełnej sile. */
  -webkit-mask-image: linear-gradient(to bottom, var(--ink) 45%, transparent);
  mask-image: linear-gradient(to bottom, var(--ink) 45%, transparent);
}

/* ── Podkładka pojedynczego elementu ──
   Kolor jest dokładnie tym, który do 2026-08-11 miał CAŁY pasek: domieszka
   88% była chwytem na półprzezroczystość, a nie osobną barwą, więc token
   brzmi `--surface`. Cień `--shadow-soft` (najsłabszy w systemie) odkleja
   element od papieru — bez niego biały krążek na `--paper` rgb(241,241,239)
   czyta się jak dziura, a nie jak coś unoszącego się.

   Jedna reguła grupująca zamiast czterech kopii tych samych dwóch
   deklaracji. Specyficzność (0,1,0), więc wypełnienia hoveru narzędzi
   (`.kp-topbar-btn--help:hover`, (0,2,0)) nadal wygrywają.

   `.kp-topbar-avabtn` CELOWO POZA GRUPĄ — patrz komentarz przy nim. */
.kp-topbar-search,
.kp-topbar-backup,
.kp-topbar-btn {
  --surface: var(--surface-1);
  background: var(--surface);
  box-shadow: var(--shadow-soft);
}

/* KOLUMNA TYTUŁU USUNIĘTA 2026-08-11 (decyzja użytkownika). Stały tu
   `.kp-topbar-heading`, `.kp-topbar-title` i `.kp-topbar-subtitle` — tytuł
   strony i nazwa placówki w lewym rogu paska. Znikły razem ze znacznikami,
   blokiem szablonu i wartościami kontekstu; żadna z tych nazw nie miała
   innego konsumenta. Skutki uboczne, obie liczby zmierzone dsprobe:
   pasek wrócił z 69px do wysokości makietowej (kolumna była najwyższym
   elementem rzędu), a pigułka wyszukiwarki znowu dobija do swoich 520px,
   bo nikt nie zabiera jej 28px paddingu. */

/* ── Wyzwalacz wyszukiwarki ──
   Do 2026-08-12 stał tu `<label>` z wyłączonym `<input>` i napisem
   „Wyszukiwarka — wkrótce". Dziś to `<button>` otwierający `<dialog>`.

   Zamiana `<input disabled>` na `<button>`, zamiast samego zdjęcia atrybutu,
   spełnia dawną decyzję D6 („pole połykające Enter wygląda jak zepsute")
   MOCNIEJ, niż ona wymagała: w przycisku nie ma czego pisać, więc pytanie
   „co robi Enter w pasku" w ogóle nie powstaje. Pisanie zaczyna się dopiero
   w oknie, gdzie Enter działa od pierwszego znaku. */

/* Bez `flex: 1` — szerokość toru ustawia siatka paska (`minmax(0, 520px)`),
   a `max-width` zostaje jako górna granica pigułki na szerokich ekranach.

   `grid-column` JAWNIE, bo automatyczne rozmieszczanie wkłada elementy po
   kolei: pole jest pierwszym dzieckiem paska, więc bez tego trafiało do toru
   PIERWSZEGO i stało przy lewej krawędzi, zwężone do 231px — dokładnie to,
   co centrowanie miało naprawić. */
/* PIGUŁKA STOI W OSI OKNA PRZEGLĄDARKI (decyzja użytkownika 2026-08-14).
   Odwraca decyzję z 2026-08-11, która stawiała ją w osi KOLUMNY TREŚCI —
   powód zmiany: kolumna treści jest przesunięta o pasek boczny, więc na
   szerokim ekranie pigułka siedziała 124 px na prawo od środka okna
   i czytała się jako niewyśrodkowana (zmierzone przy 1600 px).

   Dlaczego pozycjonowanie bezwzględne, a nie tor siatki: pasek ma
   `padding-left` równy szerokości paska bocznego (102 albo 274 px), więc jego
   pudełko treści NIE jest wyśrodkowane w oknie — środkowy tor siatki trafia
   w oś kolumny treści i nie da się go z niej zdjąć bez zdejmowania wcięcia,
   które chroni treść przed schowaniem pod nawigacją. Element bezwzględny
   liczy `left: 50%` od pudełka PADDING paska, czyli od pełnej szerokości okna.

   Szerokość: pełne 520 px tylko wtedy, gdy zostaje miejsce; inaczej pigułka
   się ZWĘŻA, zamiast wchodzić na narzędzia. To samo ryzyko opisywał komentarz
   przy poprzedniej decyzji — teraz jest domknięte odejmowaniem, a nie
   rezygnacją z osi. */
.kp-topbar-search {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(520px, calc(100% - 2 * (var(--kp-topbar-tools-w) + 26px + 14px)));
  display: flex;
  align-items: center;
  gap: 10px;
  height: 42px;
  padding: 0 var(--sp-4);
  border-radius: var(--r-pill);
  /* Tło i cień z reguły grupującej. Do 2026-08-11 było tu `--bg-sunken`,
     czyli zapadnięcie w powierzchnię paska — na pasku bez powierzchni to
     sprzeczność sama w sobie: zapaść można tylko w coś. */
  border: 1px solid transparent;
  /* Kolor POJEMNIKA jak w makiecie (`--muted`) — niesie go lupa. Wyszarzenie
     dotyczy samego podpisu (`--hint` niżej). */
  color: var(--muted);
  /* Trzy deklaracje wymuszone zamianą `<label>` na `<button>`: przeglądarka
     daje przyciskowi własny krój, wyśrodkowanie i kursor strzałki. */
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out);
}

/* Podpis w pigułce. Zastąpił `placeholder` wyłączonego pola — stąd `--hint`,
   ten sam kolor, którym placeholder świecił wcześniej.

   `.kp-field` zniknęło ze znacznika razem z `<input>`. Nie jest już potrzebne:
   furtka `:not(.kp-field)` w bloku TRYB CIEMNY w base.html celuje w pola
   formularza, a `<button>` polem nie jest — nie łapie jej w ogóle. Zniknęła
   przy okazji cała neutralizacja prymitywu `.kp-field`, która była tu wcześniej
   (trzy reguły na szerokość, padding, promień i stan `:disabled`). */
.kp-topbar-search-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
  font-family: var(--font-body);
  font-size: 14px;
  font-weight: 400;
  color: var(--hint);
}

/* Stan „pigułka niesie aktualne zapytanie" — na stronie wyników zamiast
   podpowiedzi stoi w niej to, czego użytkownik przed chwilą szukał
   (makieta wyszukiwarki, `.kp-trigger.filled`). Klasę dokłada `kp-search.js`
   po odczytaniu `?q=` z adresu; bez JS-u zostaje podpowiedź, czyli stan
   dotychczasowy — pigułka i tak jest wyłącznie skrótem do okna.

   Krój display i pełny `--ink`, bo to już nie podpowiedź, tylko treść. */
.kp-topbar-search--filled,
.kp-topbar-search--filled .kp-topbar-search-label {
  font-family: var(--font-display);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.2px;
  color: var(--ink);
}

.kp-topbar-search--filled {
  --surface: var(--surface-1);
  background: var(--surface);
  border-color: var(--border);
}

/* Stany interaktywne — makieta („Górny pasek konta.dc.html", `.kp-search:hover`
   i `:focus-within`) miała je od początku, ale nigdy nie zostały przeniesione,
   bo stub był nieczynny i reagowanie na hover byłoby kłamstwem. */
.kp-topbar-search:hover { border-color: var(--border); }

/* Obrys focusu jak wszędzie w katalogu (`.kp-btn`, `.kp-pill`, `.kp-news-*`):
   `outline` 2px `--blue-600` z odstępem 2px. Świadomie NIE `box-shadow`
   z `--blue-050` — skala blue nie ma nadpisania ciemnego, więc jasna poświata
   waliłaby po oczach na ciemnym pasku. */
.kp-topbar-search:focus-visible {
  outline: 2px solid var(--blue-600);
  outline-offset: 2px;
}

/* Skrót klawiszowy jako podpowiedź na prawym końcu pigułki. Ukryty na wąskich
   ekranach razem z podpisem — telefon nie ma klawisza Cmd. */
.kp-topbar-search-kbd {
  flex: none;
  padding: 2px 6px;
  border: 1px solid var(--border);
  border-radius: var(--r-chip);
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 500;
  line-height: 1.4;
  color: var(--muted);
}

/* ── Prawa strona: narzędzia ── */

/* Tor TRZECI jawnie (patrz komentarz przy `.kp-topbar-search`), a dosunięcie
   do prawej przez `justify-self`, nie `margin-left: auto`: w siatce
   auto-margines zabierałby wolne miejsce z toru i przesuwał oś wyszukiwarki. */
.kp-topbar-tools {
  grid-column: 3;
  justify-self: end;
  display: flex;
  align-items: center;
  /* 12px, nie makietowe 4px (2026-08-11, decyzja użytkownika). Makieta mogła
     stawiać przyciski ciasno, bo były PRZEZROCZYSTE — rozdzielał je sam
     odstęp między glifami. Odkąd każdy niesie własną podkładkę `--surface`,
     przy 4px trzy krążki zlewają się w jedną plamę. */
  gap: var(--sp-3);
  position: relative;
}

/* Kolor przypisany PO NAZWIE przycisku, nigdy przez `nth-of-type`.
   Makieta koloruje pozycyjnie i to jest mina: dołożenie albo zdjęcie
   jednego przycisku przemalowuje wszystkie pozostałe. */

.kp-topbar-btn {
  appearance: none;
  cursor: pointer;
  position: relative;
  width: 42px;
  height: 42px;
  border-radius: 50%;
  border: none;
  /* Tło i cień z reguły grupującej wyżej — do 2026-08-11 przycisk był
     przezroczysty i barwę dawał mu dopiero hover. */
  color: var(--ink-2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background var(--dur-fast) ease,
              color var(--dur-fast) ease,
              transform var(--dur-fast) var(--ease-out);
}

.kp-topbar-btn:active { transform: scale(.94); }

.kp-topbar-btn:focus-visible {
  outline: 2px solid var(--blue-600);
  outline-offset: 2px;
}

/* Barwy WRÓCIŁY NA MIEJSCA Z MAKIETY (2026-08-12). Makieta koloruje przyciski
   pozycyjnie — (1) emerald, (2) amber, (3) magenta — przy kolejności
   kropki–pomoc–dzwonek. Gdy launchera nie było, całą kolejkę trzeba było
   przesunąć o jeden (pomoc emerald, dzwonek amber). Launcher wrócił, więc
   wracają i barwy. Podmiana trzech wartości zamiast przebudowy selektorów
   jest dokładnie tym, po co przypisanie idzie PO NAZWIE wariantu. */
.kp-topbar-btn--apps:hover,
.kp-topbar-btn--apps[aria-expanded="true"] {
  background: var(--c-emerald);
  color: var(--on-fill);
}

.kp-topbar-btn--help:hover,
.kp-topbar-btn--help[aria-expanded="true"] {
  background: var(--c-amber);
  color: var(--on-fill);
}

.kp-topbar-btn--bell:hover,
.kp-topbar-btn--bell[aria-expanded="true"] {
  background: var(--c-magenta);
  color: var(--on-fill);
}

/* PRZYCISK KONTA CELOWO POZA REGUŁĄ GRUPUJĄCĄ. Dwa powody, oba
   rozstrzygające. Awatar to już wypełnione koło 34px wewnątrz przycisku
   40px, więc podkładka dałaby wokół niego 3px białej aureoli. A `:hover`
   tego przycisku używa `box-shadow` do narysowania obwódki `--c-violet` —
   cień odklejający zderzyłby się z nią na tej samej właściwości. Unoszenie
   niesie więc samo koło awatara (`.kp-topbar-ava` niżej). */
.kp-topbar-avabtn {
  appearance: none;
  cursor: pointer;
  /* Bez `margin-left: 6px` z makiety (zdjęte 2026-08-11): dokładało się do
     odstępu grupy i awatar odjeżdżał od reszty bardziej niż przyciski od
     siebie. Przy 12px rytm jest równy i tego dodatku nie potrzeba. */
  width: 40px;
  height: 40px;
  padding: 0;
  border-radius: 50%;
  border: none;
  background: transparent;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 0 0 0 var(--c-violet);
  transition: box-shadow var(--dur-med) ease,
              transform var(--dur-fast) var(--ease-out);
}

.kp-topbar-avabtn:hover,
.kp-topbar-avabtn[aria-expanded="true"] {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-violet) 32%, transparent);
}

.kp-topbar-avabtn:active { transform: scale(.94); }

.kp-topbar-avabtn:focus-visible {
  outline: 2px solid var(--blue-600);
  outline-offset: 2px;
}

.kp-topbar-ava {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  overflow: hidden;
  /* Odklejenie od papieru na samym kole, nie na przycisku — patrz komentarz
     przy `.kp-topbar-avabtn`. */
  box-shadow: var(--shadow-soft);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 13px;
  letter-spacing: -.2px;
  color: var(--on-fill);
  background: var(--c-violet);
}

.kp-topbar-ava img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Licznik nieprzeczytanych. NIE `.kp-dot` — ta nazwa należy do paska
   bocznego i goła definicja tutaj nadpisałaby ją globalnie. */
.kp-topbar-dot {
  position: absolute;
  top: 7px;
  right: 7px;
  min-width: 17px;
  height: 17px;
  padding: 0 var(--sp-1);
  border-radius: var(--r-pill);
  background: var(--c-magenta);
  color: var(--on-fill);
  border: 2px solid var(--surface);
  font-weight: 700;
  font-size: 9.5px;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.kp-topbar-btn:hover .kp-topbar-dot,
.kp-topbar-btn[aria-expanded="true"] .kp-topbar-dot {
  background: var(--surface);
  color: var(--c-magenta);
  border-color: transparent;
}

/* ── Pigułka stanu kopii zapasowej (tylko superadmin) ──
   Postać alarmowa bierze `--c-brick`, nie `--status-urgent`: token
   statusowy przełącza się z motywem i w ciemnym schodzi na tyle
   jasno, że biały tekst przepada. `--c-brick` ma stałą wartość
   w obu motywach i daje na białym 6.77:1 (patrz tokens-colors.css). */

.kp-topbar-backup {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 32px;
  padding: 0 var(--sp-3);
  margin-right: var(--sp-2);
  border-radius: var(--r-pill);
  /* Krawędź PRZEZROCZYSTA: od 2026-08-11 odklejenie od papieru niesie cień
     z reguły grupującej, a hairline obok cienia robiłby z pigułki element
     cięższy od sąsiednich krążków, które mają sam cień. Krawędź zostaje
     zadeklarowana (a nie usunięta), bo wariant alarmowy ją przejmuje. */
  border: 1px solid transparent;
  color: var(--ink-2);
  font-weight: 600;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  text-decoration: none;
  transition: box-shadow var(--dur-fast) ease, color var(--dur-fast) ease;
}

/* Hover PODNOSI, zamiast zmieniać barwę — to jest język tego paska.
   Wcześniej hover przestawiał tło z `--bg-sunken` na `--surface`; teraz
   spoczynek już jest na `--surface`, więc różnicę musi nieść cień. */
.kp-topbar-backup:hover {
  box-shadow: var(--shadow-nav);
  color: var(--ink);
}

.kp-topbar-backup:focus-visible {
  outline: 2px solid var(--blue-600);
  outline-offset: 2px;
}

.kp-topbar-backup--alarm {
  background: var(--c-brick);
  border-color: transparent;
  color: var(--on-fill);
}

.kp-topbar-backup--alarm:hover {
  background: var(--c-brick);
  border-color: transparent;
  color: var(--on-fill);
  filter: brightness(1.08);
}

/* Pigułka kopii przy kilku szkołach jest <button> (rozwija listę szkół), przy jednej
   — <a>. Przycisk dziedziczy krój, bo preflight go nie ustawia. */
button.kp-topbar-backup { cursor: pointer; font-family: inherit; }

/* Panel listy szkół (`BackupMenu`): wiersz = kropka szkoły, nazwa + podpowiedź,
   chip stanu, link do panelu kopii (pod nazwą). */
.kp-topbar-panel--backup { width: 440px; padding: 0 0 var(--sp-2); }
.kp-backup-list { margin: 0; padding: 0; list-style: none; max-height: 70vh; overflow-y: auto; }
.kp-backup-item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  column-gap: var(--sp-3);
  row-gap: 2px;
  padding: 10px var(--sp-4);
  border-top: 1px solid var(--border);
}
.kp-backup-text { display: flex; flex-direction: column; min-width: 0; }
.kp-backup-name { font-weight: 600; font-size: 13.5px; color: var(--ink); overflow-wrap: anywhere; }
.kp-backup-hint { font-size: 12px; color: var(--muted); overflow-wrap: anywhere; }
.kp-backup-link {
  grid-column: 2 / -1;
  justify-self: start;
  font-size: 12px;
  font-weight: 600;
  color: var(--blue-600);
  text-decoration: none;
}
.kp-backup-link:hover { text-decoration: underline; }
.kp-backup-link:focus-visible { outline: 2px solid var(--blue-600); outline-offset: 2px; border-radius: 4px; }

/* ── Dymki podpowiedzi ──
   Tło z `--ink`, tekst z `--surface`: odwrócona para neutralnych, więc
   dymek jest ciemny w trybie jasnym i jasny w ciemnym, bez ani jednej
   reguły pisanej osobno dla motywu. */

.kp-topbar-tip { position: relative; }

/* `display: none` w stanie spoczynku to NIE kosmetyka — to jedyne, co trzyma
   poziomy pasek przewijania z dala od KAŻDEJ strony platformy.

   Dymek jest `position: absolute`, `white-space: nowrap` i wyśrodkowany na
   swoim przycisku (`left: 50%` + `translateX(-50%)`). Samo `opacity: 0` go
   NIE wyjmuje z układu: niewidoczny, ale wciąż rozmieszczony, powiększa
   obszar przewijania dokumentu. Skrajnie prawy wyzwalacz to
   `#topbar-acct-btn`, a jego treść brzmi „Konto: <imię i nazwisko>", więc
   szerokość dymka rośnie z DŁUGOŚCIĄ NAZWISKA ZALOGOWANEJ OSOBY i połowa tej
   szerokości wystaje poza prawą krawędź okna. Zmierzone na `/dashboard/`
   (2026-08-24, przed poprawką): „Konto: Admin" → nadmiar 1px przy 1920px,
   „Konto: Aleksander Bielajew" → 40px, „Konto: Małgorzata
   Wiśniewska-Kowalczyk" → 83px. Nadmiar jest ten sam przy 1920, 1512, 1440,
   1280 i 1024px — to nie jest usterka wąskiego ekranu. Dlatego objaw widać na
   koncie z długim nazwiskiem, a na koncie „Admin" z bazy deweloperskiej
   prawie wcale.

   Dlaczego nie `overflow-x: clip` na `.kp-topbar`: krawędzią przycięcia jest
   padding box paska, a panele (`.kp-topbar-panel`, `right: 0`,
   `max-width: calc(100vw - 2 * var(--sp-4))`) bywają szersze niż jego kolumna
   treści na wąskim ekranie — obcięłoby im lewy bok.

   `transition-behavior: allow-discrete` + `@starting-style` zachowują zwłokę
   0.25s i przenikanie. Tam, gdzie ich nie ma, dymek pojawia się skokowo —
   degradacja jest łagodna, a `display: none` działa wszędzie. */
.kp-topbar-tip::after {
  content: attr(data-tip);
  position: absolute;
  top: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%) translateY(-3px);
  padding: 5px 9px;
  border-radius: var(--r-chip);
  background: var(--ink);
  color: var(--surface);
  white-space: nowrap;
  font-weight: 600;
  font-size: 11.5px;
  letter-spacing: .1px;
  display: none;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-fast) ease .25s, transform var(--dur-fast) ease .25s,
              display var(--dur-fast) allow-discrete .25s;
  z-index: 60;
}

.kp-topbar-tip:hover::after {
  display: block;
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

@starting-style {
  .kp-topbar-tip:hover::after { opacity: 0; transform: translateX(-50%) translateY(-3px); }
}

/* Dymek milknie, gdy panel jest otwarty — inaczej zasłania jego róg. */
.kp-topbar-tip[aria-expanded="true"]::after { display: none; }

/* ── Panele ──
   Ukrywane ATRYBUTEM `hidden`, nie klasą `.hidden`: klasę generuje
   dopiero Tailwind z CDN w trakcie działania strony, więc do tego
   czasu wszystkie panele byłyby widoczne naraz. Reguła [hidden] jest
   konieczna, bo bez niej wygrywa `display` z reguły bazowej. */

@keyframes kp-topbar-pop {
  from { opacity: 0; transform: translateY(-8px) scale(.985); }
  to   { opacity: 1; transform: none; }
}

.kp-topbar-panel {
  position: absolute;
  top: calc(100% + 10px);
  right: 0;
  z-index: 55;
  max-width: calc(100vw - 2 * var(--sp-4));
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 22px;
  box-shadow: var(--shadow-hero);
  overflow: hidden;
  animation: kp-topbar-pop 180ms var(--ease-out);
}

.kp-topbar-panel[hidden] { display: none; }

/* Resetu ramek z preflightu Tailwinda NIE ma tu osobno: panele siedzą
   wewnątrz <header class="kp-topbar kp-island">, a `.kp-island *` wyżej
   w tym pliku robi dokładnie to samo dla całego poddrzewa. */

.kp-topbar-panel--apps { width: 326px; padding: var(--sp-3) var(--sp-4) var(--sp-4); }
.kp-topbar-panel--help { width: 292px; padding: var(--sp-2) 0; }
.kp-topbar-panel--bell { width: 392px; }
.kp-topbar-panel--acct { width: 352px; padding: 0 0 var(--sp-2); }

.kp-topbar-panel-title {
  padding: 6px var(--sp-4) 10px;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 14px;
  letter-spacing: -.3px;
  color: var(--ink);
}

/* ── Siatka usług w panelu launchera ──
   Geometria z makiety (kafel 44px, promień 14px, gap 7px między znakiem
   a podpisem). Trzy kolumny mimo trzech kafli: makieta ma dziewięć i to jej
   rytm, a jeden rząd trzech wygląda w nim naturalnie — dwa kafle w rzędzie
   po dwa zostawiałyby dziurę. */
.kp-topbar-apps {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 2px;
}

.kp-topbar-app {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  padding: var(--sp-3) var(--sp-1);
  border-radius: 14px;
  text-decoration: none;
  transition: background var(--dur-fast) ease,
              transform var(--dur-med) var(--ease-out);
}

.kp-topbar-app:hover {
  background: var(--bg-sunken);
  transform: translateY(-2px);
}

.kp-topbar-app:focus-visible {
  outline: 2px solid var(--blue-600);
  outline-offset: -2px;
}

/* Wypełnienie kategoryczne podaje szablon (`--c-magenta`, `--c-cyan`,
   `--c-indigo`) — prymityw trzyma samą geometrię i kolor glifu. */
.kp-topbar-app-mark {
  width: 44px;
  height: 44px;
  border-radius: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--on-fill);
}

.kp-topbar-app-label {
  font-weight: 600;
  font-size: 11.5px;
  color: var(--ink-2);
  text-align: center;
}

.kp-topbar-app:hover .kp-topbar-app-label { color: var(--ink); }

.kp-topbar-kicker {
  padding: var(--sp-3) 18px 6px;
  font-weight: 700;
  font-size: 9.5px;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--hint);
}

/* Wiersz panelu. NIE `.kp-row` — ta nazwa już należy do katalogu
   w innym znaczeniu (wiersz listy ocen). */
.kp-topbar-row {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  padding: 11px var(--sp-4);
  border: none;
  background: transparent;
  text-align: left;
  cursor: pointer;
  text-decoration: none;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 13.5px;
  color: var(--ink-2);
  transition: background var(--dur-fast) ease, color var(--dur-fast) ease;
}

.kp-topbar-row:hover { background: var(--bg-sunken); color: var(--ink); }
.kp-topbar-row svg { flex: 0 0 auto; color: var(--muted); }
.kp-topbar-row:hover svg { color: var(--ink); }

.kp-topbar-row:focus-visible {
  outline: 2px solid var(--blue-600);
  outline-offset: -2px;
}

.kp-topbar-row--danger { color: var(--c-brick); }
.kp-topbar-row--danger svg { color: var(--c-brick); }
.kp-topbar-row--danger:hover { color: var(--c-brick); }
.kp-topbar-row--danger:hover svg { color: var(--c-brick); }

.kp-topbar-rowmark {
  margin-left: auto;
  font-weight: 700;
  font-size: 10.5px;
  color: var(--hint);
  border: 1px solid var(--border);
  border-radius: var(--r-chip);
  padding: 2px 6px;
}

.kp-topbar-divider {
  height: 1px;
  margin: var(--sp-2) 0;
  background: var(--border-soft);
}

/* ── Panel konta: szapka z awatarem ── */

.kp-topbar-acct-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 9px;
  padding: 22px 18px var(--sp-4);
  background: var(--surface-well);
  text-align: center;
}

.kp-topbar-acct-ava {
  width: 74px;
  height: 74px;
  border-radius: 50%;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 26px;
  letter-spacing: -.5px;
  color: var(--on-fill);
  background: var(--c-violet);
}

.kp-topbar-acct-ava img { width: 100%; height: 100%; object-fit: cover; }

.kp-topbar-acct-name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 17px;
  letter-spacing: -.4px;
  color: var(--ink);
}

.kp-topbar-acct-mail { font-size: 12.5px; color: var(--muted); }

.kp-topbar-acct-link {
  margin-top: 5px;
  padding: 9px 18px;
  border-radius: var(--r-pill);
  border: 1px solid var(--border);
  --surface: var(--surface-1);
  background: var(--surface);
  font-weight: 600;
  font-size: 13px;
  color: var(--ink-2);
  text-decoration: none;
}

.kp-topbar-acct-link:hover { color: var(--ink); }

/* Przełącznik motywu w wierszu panelu. */
.kp-topbar-switch {
  margin-left: auto;
  width: 38px;
  height: 22px;
  border-radius: var(--r-pill);
  padding: 2px;
  display: flex;
  flex: 0 0 auto;
  background: var(--border);
  justify-content: flex-start;
  transition: background var(--dur-med) ease;
}

.kp-topbar-switch-knob {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  --surface: var(--surface-1);
  background: var(--surface);
}

/* Stan włączony. Bez tej reguły `data-on` był atrybutem, którego nie czytał
   żaden selektor: motyw się przełączał, ale kciuk stał w lewej krawędzi w obu
   stanach, a tor zmieniał barwę tylko dlatego, że `--border` sam jedzie
   z motywem. Wartości wprost z makiety „Górny pasek konta" (switchBg /
   switchPos). `--blue-600` jest tu na miejscu: to akcent kontrolki
   funkcjonalnej, a nie barwa powierzchni.

   Kciuk bierze `--on-fill`, nie `--surface` — włączony przełącznik występuje
   wyłącznie razem z trybem ciemnym, gdzie `--surface` to rgb(29, 31, 29)
   i zlałby się z niebieskim torem. Tu kciuk leży NA wypełnieniu, więc token
   jest ten sam, którego używa tekst na kafelku licznikowym.

   `justify-content` nie jest animowalny, więc kciuk przeskakuje — tak samo
   jak w makiecie, która przejściem obejmuje samo tło. */
.kp-topbar-switch[data-on="1"] {
  background: var(--blue-600);
  justify-content: flex-end;
}

.kp-topbar-switch[data-on="1"] .kp-topbar-switch-knob {
  background: var(--on-fill);
}

/* ── Wąski ekran ──
   Pole wyszukiwania znika pierwsze: jest nieczynne, więc zabiera
   miejsce, nie dając nic w zamian. Narzędzia zostają. */

@media (max-width: 940px) {
  /* `gap` i podpis placówki ZOSTAJĄ takie jak na szerokim ekranie — makieta
     ich nie zmienia, a zwężenie odstępu i schowanie podpisu robiły z paska
     inny komponent na telefonie (zmierzone dsprobe 390px: gap 8 zamiast 14,
     wysokość 63 zamiast 86). Znika WYŁĄCZNIE pole wyszukiwania: jest
     nieczynne, więc zabiera miejsce, nie dając nic w zamian. */
  /* JEDEN tor zamiast trzech: pola wyszukiwania tu nie ma, więc środkowy tor
     byłby pusty, a dwa równe skrajne zepchnęłyby narzędzia do połowy ekranu
     zamiast do prawej krawędzi. */
  .kp-topbar { padding: 10px var(--sp-4); grid-template-columns: 1fr; }
  /* Narzędzia wracają do skrajnego toru — bez tego `grid-column: 3`
     wskazywałby tor, którego tu nie ma, i siatka dorobiłaby dwa puste. */
  .kp-topbar-tools { grid-column: 2; }
  /* Wyszukiwarka NIE znika (do 2026-08-12 było tu `display: none`).
     Tamta reguła miała uzasadnienie „jest nieczynne, więc zabiera miejsce,
     nie dając nic w zamian" — po podłączeniu prawdziwej wyszukiwarki
     przesłanka przestała być prawdziwa, a samo `display: none` znaczyłoby
     BRAK WYSZUKIWARKI NA TELEFONIE. Pigułka zwija się więc do samej lupy,
     tak jak obok robi to pigułka kopii zapasowej. */
  .kp-topbar-search {
    /* Powrót do siatki: na wąskim ekranie pigułka jest samą lupą i ma stać
       PRZY narzędziach, a nie w osi okna — wyśrodkowana lupa zostawiłaby po
       obu stronach puste pole, a po prawej ścisk. Bezwzględne centrowanie
       z szerokiego układu trzeba zdjąć jawnie, bo `grid-column` i
       `justify-self` nie działają na elemencie wyjętym z układu. */
    position: static;
    transform: none;
    grid-column: 1;
    justify-self: end;
    width: 42px;
    max-width: none;
    padding: 0;
    justify-content: center;
  }
  .kp-topbar-search-label,
  .kp-topbar-search-kbd { display: none; }
  .kp-topbar-panel { max-width: calc(100vw - var(--sp-4)); }
  /* Pigułka backupu zwija się do samej ikony — na telefonie jej podpis zjadał
     ~100px. Sygnał nie ginie: alarm niesie WYPEŁNIENIE, a nie napis, a wiek
     kopii zostaje w `title` i na panelu kopii, do którego pigułka prowadzi. */
  .kp-topbar-backup { padding: 0; width: 32px; height: 32px; justify-content: center; }
  .kp-topbar-backup span { display: none; }
  /* Reguły `.kp-topbar-heading { flex: 1 1 auto }` i zawijania podpisu
     placówki zniknęły stąd 2026-08-11 razem z całą kolumną tytułu. */
}


/* ═══════════════════════════════════════════════════════════════
   Przeszukiwalna lista wyboru (`components/fancy_select.html`)
   ═══════════════════════════════════════════════════════════════

   Kontrolka zastępuje natywny `<select>` na ekranach, które o to poproszą
   klasą `js-fancy-select`. Natywny selekt zostaje w drzewie i dalej niesie
   wartość formularza — to, co niżej, jest wyłącznie interfejsem.

   Style stoją TUTAJ, a nie w atrybutach `style` skryptu, z dwóch powodów.
   Po pierwsze nazwy `.kp-*` rodzajowe mają jedno miejsce zamieszkania i jest
   nim katalog (`ds-component-layer`). Po drugie do 2026-08-25 komponent nosił
   sześć wartości barwnych wpisanych wprost i przez rok nie zauważył tego
   żaden pomiar: zapadka literałów czyta pliki `.css`, a tamto było
   JavaScriptem w szablonie. Tu jest widoczne dla strażnika od pierwszego dnia.

   Panel jest przyklejany do `<body>` (`position: fixed`), żeby nie przycinał
   go rodzic z `overflow: hidden`. Tokeny stoją na `:root`, więc barwy działają
   tak samo poza wyspą strony — przeniesienie panelu w drzewo nie jest do
   niczego potrzebne. Współrzędne i wymiary liczy skrypt; wszystko inne jest
   tutaj. */

.kp-fsel { position: relative; }

/* Zawężenie `.kp-field`, nie druga jego definicja: pole daje powierzchnię,
   obramowanie i krój, ta klasa dokłada wyłącznie układ treści przycisku. */
.kp-fsel-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  text-align: left;
  cursor: pointer;
}

.kp-fsel-label {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.kp-fsel-chevron {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  opacity: 0.5;
  transition: transform var(--dur-fast) ease;
}

.kp-fsel-trigger[aria-expanded="true"] .kp-fsel-chevron { transform: rotate(180deg); }

/* Natywny selekt zostaje w drzewie (wysyłka, `required`, `form=`), ale znika
   z widoku i z kolejności tabulacji. Nie `display: none` — ukryta w ten sposób
   kontrolka wymagana wywraca walidację przeglądarki („not focusable"). */
.kp-fsel-native {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}

.kp-fsel-panel {
  position: fixed;
  z-index: 9999;
  display: none;
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-deep);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.kp-fsel-panel[data-open="1"] { display: block; }

.kp-fsel-search { margin-bottom: 4px; }

.kp-fsel-sep {
  height: 1px;
  margin: 4px 4px 6px;
  background: var(--divider);
}

.kp-fsel-group {
  padding: 8px 12px 4px;
  font-family: var(--font-body);
  font-size: 10px;
  font-weight: var(--fw-heavy);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.kp-fsel-opt {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 12px;
  border-radius: 10px;
  font-family: var(--font-body);
  font-size: 13px;
  color: var(--ink-2);
  cursor: pointer;
  transition: background var(--dur-fast) ease;
}

.kp-fsel-opt-label {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Pozycja pusta („— wybierz —") jest wyborem, ale nie danymi. */
.kp-fsel-opt--empty { color: var(--muted); }

/* Wskazanie klawiatury i najechanie myszą to ten sam stan wizualny — dwa
   różne wyglądy dla „to zaraz wybiorę" uczyłyby dwóch rzeczy naraz. */
.kp-fsel-opt:hover,
.kp-fsel-opt.is-active { background: var(--bg-sunken); }

/* Wybrane liczone WZGLĘDEM POWIERZCHNI, nie kolorem roli konta: do 2026-08-25
   wiersz dostawał `var(--accent)` z białym tekstem, czyli barwę tożsamości
   użytkownika w roli znacznika wyboru. Tryb ciemny wychodzi sam, bo `--link`
   i `--surface` są w nim inne. */
.kp-fsel-opt[aria-selected="true"] {
  background: color-mix(in srgb, var(--link) 14%, var(--surface));
  color: var(--ink);
  font-weight: 600;
}

.kp-fsel-check {
  flex-shrink: 0;
  font-size: 12px;
  color: var(--link);
}

.kp-fsel-empty {
  padding: 10px 12px;
  font-family: var(--font-body);
  font-size: 13px;
  text-align: center;
  color: var(--muted);
}
