/* ============================================================================
   Kropka — Strona statusu (status.kropka.infotech.edu.pl)
   status.css

   Ten katalog (`status-page/`) jest CELOWO niezależny od reszty repo: brak
   build stepu, brak importu z `static/kropka-ds/`, bo strona żyje na innym
   originie/serwowana jest bezpośrednio przez nginx, nie przez Django +
   whitenoise (patrz README.md tego katalogu). Dlatego tokeny DS poniżej są
   WŁASNĄ, samodzielną kopią — dokładnie ten sam wzorzec, jakim
   `static/kropka-ds/tokens-colors.css` kopiuje `docs/Kropka Design
   System/tokens/colors.css` z udokumentowanymi odstępstwami. Wartości
   kolorów pochodzą WYŁĄCZNIE z tokenów katalogu DS — nigdzie w tym pliku
   nie ma hexa/rgb wpisanego "z głowy" bez odniesienia do źródła.

   Źródło: docs/Kropka Design System/tokens/{colors,typography,spacing,base}.css
   oraz static/kropka-ds/tokens-colors.css (tryb ciemny + tokeny -ink).

   Prefiks lokalny: `.ksp-*` (Kropka Status Page) — strona jest samodzielnym
   dokumentem, więc nie podlega regule wejścia do katalogu `.kp-*` (ta reguła
   dotyczy współdzielonego arkusza aplikacji), ale prefiks trzyma się tej
   samej konwencji co inne wyspy (`.kt-`, `.dsh-`, `.ot-`, `.tr-`, `.dl-`,
   `.nt-`), na wypadek gdyby strona kiedyś wróciła pod dach Django.
   ============================================================================ */

/* ── Reset minimalny ─────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html, body, h1, h2, h3, p, ul, ol, li, figure, blockquote { margin: 0; }
ul, ol { padding: 0; list-style: none; }
img, svg { display: block; max-width: 100%; }
button { font: inherit; color: inherit; }

/* ============================================================================
   TOKENY — jasny motyw (:root)
   ============================================================================ */
:root {
  /* Marka — niebieski, jedyny akcent interakcji/chrome (tokens/colors.css) */
  --blue-700: rgb(29, 78, 216);
  --blue-600: rgb(37, 99, 235);
  --blue-500: rgb(59, 130, 246);
  --blue-100: rgb(219, 234, 254);
  --blue-050: rgb(239, 246, 255);

  /* Neutralne — ciepła szarość */
  --paper:       rgb(241, 241, 239);
  --paper-2:     rgb(240, 241, 239);
  --surface:     rgb(255, 255, 255);
  --ink:         rgb(0, 0, 0);
  --ink-2:       rgb(45, 47, 46);
  --muted:       rgb(90, 92, 90);
  --hint:        rgb(93, 93, 93);
  --border:      rgb(232, 232, 230);
  --border-soft: rgba(172, 173, 172, 0.2);
  --divider:     rgb(216, 216, 216);

  /* Status — cztery znaczenia, zero dodatkowych barw:
     działa = ok (zielony) · zakłócenia = warning (pomarańcz)
     awaria = urgent (ceglasty) · prace serwisowe = info (niebieski) */
  --status-ok:      rgb(68, 131, 97);
  --status-warning: rgb(255, 106, 0);
  --status-urgent:  rgb(179, 27, 37);
  --status-info:    rgb(37, 99, 235);

  --status-ok-bg:      rgba(68, 131, 97, 0.10);
  --status-warning-bg: rgba(255, 106, 0, 0.10);
  --status-urgent-bg:  rgba(179, 27, 37, 0.10);
  --status-info-bg:    rgba(37, 99, 235, 0.10);

  /* Barwa TEKSTU na własnym 10% tincie — próg AA 4.5:1 dla drobnego tekstu.
     --status-ok-ink jest wartością już zmierzoną i przyjętą w katalogu DS
     (static/kropka-ds/tokens-colors.css). Pozostałe trzy zmierzone tutaj tą
     samą metodą (luminancja WCAG, tło = kolor @10% na --surface): urgent
     przechodzi próg już w swojej podstawowej wartości (5.71:1), więc nie
     potrzebuje osobnego tokenu; warning i info dostają przyciemniony
     odcień tego samego hue. Wartości i wyniki zapisane przy każdym tokenie. */
  --status-ok-ink:      rgb(56, 120, 88);   /* 4.71:1 na --status-ok-bg (wartość z katalogu DS) */
  --status-warning-ink: rgb(140, 58, 0);    /* 6.93:1 na --status-warning-bg (zmierzone) */
  --status-urgent-ink:  rgb(179, 27, 37);   /* = --status-urgent, 5.71:1 na --status-urgent-bg (zmierzone) */
  --status-info-ink:    rgb(29, 78, 216);   /* = --blue-700, 5.83:1 na --status-info-bg (zmierzone) */

  /* Skala oceny 1–6 (kopia `docs/Kropka Design System/tokens/colors.css`,
     sekcja "Grade-mark palette", ten sam wzorzec kopiowania jak reszta tego
     pliku). Tu użyta WYŁĄCZNIE do koloru słupka paska 90-dniowej historii,
     zależnie od % dostępności DNIA (patrz `uptimeTier()` w status.js), nie
     do ocen szkolnych — decyzja właściciela, 2026-08-06: liniowy gradient
     0–100% spłaszczyłby realne awarie w jeden odcień zieleni, a skala
     ocen ma już gotowe, nasycone stopnie od czerwonego do zielonego.
     Reguła DS #3 (`docs/Kropka Design System/readme.md`) wprost dopuszcza
     nasycone barwy dla "skali ocen 1–6" — dostępność dnia jest dokładnie
     taką skalą (stopnie ważności), więc ponowne użycie tych tokenów jest
     właściwsze niż wymyślanie nowego gradientu.

     Świadomie tylko PIĘĆ z sześciu tokenów źródłowych: `--mark-6`
     (celujący, niebieski) leży POZA osią czerwony→zielony — to wyjątek
     dla najwyższej oceny, nie kontynuacja gradientu — i nie ma sensownego
     miejsca w skali dostępności.

     BEZ nadpisania w trybie ciemnym — wierna kopia zasady źródłowej
     (`static/kropka-ds/tokens-colors.css`, komentarz przy `--mark-ink`:
     "Skala --mark-* jest w obu motywach identyczna", bo body.dark jej nie
     nadpisuje). Te tokeny kolorują WYPEŁNIENIE słupka, nie tekst — słupek
     nigdy nie niesie własnego tekstu (opis dnia stoi w panelu
     `.ksp-day-detail` niżej, na tokenach `--status-*-ink`, już zmierzonych
     na AA 4.5:1), więc próg tekstowy tu nie obowiązuje.

     UWAGA — poprawka 2026-08-06 po recenzji: próg 4.5:1 (tekst) to NIE
     jedyny próg, który dotyczy tego wypełnienia. WCAG 1.4.11 (kontrast
     nietekstowy) wymaga 3:1 dla granicy elementu graficznego względem tła,
     na którym stoi (tu: `--surface`, bo `.ksp-card` ma
     `background: var(--surface)`). Ta wartość NIE była wcześniej
     zmierzona i część kombinacji jej nie spełnia — zmierzone (luminancja
     WCAG, wypełnienie @100% vs `--surface`):

       jasny motyw:  tier 5 = 4,08:1 ✓ | tier 4 = 2,51:1 ✗ | tier 3 = 1,93:1 ✗
                      tier 2 = 2,51:1 ✗ | tier 1 = 6,77:1 ✓
       ciemny motyw: tier 5 = 4,07:1 ✓ | tier 4 = 6,62:1 ✓ | tier 3 = 8,61:1 ✓
                      tier 2 = 6,60:1 ✓ | tier 1 = 2,45:1 ✗ (najgorszy przypadek:
                      to tier AWARII, w trybie ciemnym, najsłabiej widoczny)

     Rozwiązanie z 2026-08-06 (WYCOFANE 2026-08-07, patrz niżej): NIE
     zmieniono wartości `--mark-*` (fałszywa precyzja + zerwanie wierności
     ze skalą ocen DS) — dodano osobny, niezależny od `data-tier` obrys
     słupka (`box-shadow: 0 0 0 1px var(--hint)` na `.ksp-bar`), który sam
     spełniał 3:1 względem `--surface` w obu motywach.

     DECYZJA WŁAŚCICIELA 2026-08-07: obrys USUNIĘTY — wizualnie czytał się
     jako ramka wokół każdego słupka, czego właściciel nie chciał (wzorzec
     status.claude.com: słupki bez obrysu, tylko wyraźny odstęp między nimi).
     Usunięcie obrysu PRZYWRACA problem z tabeli wyżej: te same kombinacje
     (jasny motyw: tier 2/3/4; ciemny motyw: tier 1) formalnie NIE spełniają
     WCAG 1.4.11 (3:1) w tym stanie kodu — to świadomie przyjęte, udokumen-
     towane odstępstwo, nie przeoczenie.

     Rozważona i ODRZUCONA alternatywa: przyciemnienie/rozjaśnienie tła POD
     słupkami zamiast obrysu (np. `--paper-2` zamiast `--surface`), żeby
     wypełnienia same odcinały się mocniej. Policzone (ten sam wzór
     luminancji WCAG):
       - Jasny motyw: `--surface` (biel, rgb 255,255,255) to i tak
         NAJJAŚNIEJSZY token w całym zestawie — każde inne tło (`--paper-2`
         240,241,239, `--paper` 241,241,239) jest CIEMNIEJSZE, więc kontrast
         wobec kolorowych wypełnień tylko by SPADŁ (np. tier 4 z 2,51:1 do
         2,21:1 wobec `--paper-2`). W jasnym motywie nie ma w zestawie
         tokenów jaśniejszego tła niż to już użyte — tej ścieżki fizycznie
         nie da się poprawić bez wprowadzenia nowego, jaśniejszego niż biel
         tokenu (nie istnieje) albo przyciemnienia samych wypełnień (odrzu-
         cone wyżej: zrywa wierność ze skalą ocen DS).
       - Ciemny motyw: nawet wobec CZYSTEJ CZERNI (rgb 0,0,0) — czyli
         najciemniejszego możliwego tła, więcej niż jakikolwiek token DS —
         tier 1 (`--mark-1`, rgb 179,27,37) osiąga tylko 3,10:1: ledwie nad
         progiem i tylko w skrajnym, niepraktycznym przypadku (czarna plama
         pod paskiem, niespójna z resztą karty). Wobec realnego tokenu
         `--paper` (19,20,19, najciemniejszy w zestawie) wychodzi 2,73:1 —
         nadal poniżej 3:1. Zmiana tła nie rozwiązuje więc problemu w żadnym
         z dwóch motywów bez zepsucia reszty designu.

     Dlaczego mimo to zaakceptowano brak 3:1 w tych kombinacjach: pasek ma
     teraz (2026-08-07) 30 słupków zamiast 90 — przy tej samej szerokości
     karty słupek jest ok. trzy razy szerszy, a `gap` między słupkami wzrósł
     (patrz `.ksp-bar`/`.ksp-bars` w sekcji "PASEK HISTORII" niżej) na tyle,
     że sąsiednie słupki są rozróżnialne GEOMETRIĄ (wyraźna przerwa,
     zaokrąglone rogi), nie tylko kolorem/obrysem. Kolor NADAL nie jest
     jedynym nośnikiem informacji o dniu w żadnym z trzech pozostałych
     kanałów: tooltip na hover/dotyk, panel `.ksp-day-detail` pod paskiem i
     `aria-valuetext` slidera — wszystkie trzy niosą ten sam tekst
     (`describeDay()`) niezależnie od tego, czy wypełnienie słupka spełnia
     3:1. Pełne wyliczenie i historia decyzji: README.md → "Kolor słupka i
     kontrast". */
  --mark-5: rgb(56, 142, 78);    /* bardzo dobry — zielony: dzień bez zakłóceń */
  --mark-4: rgb(124, 179, 66);   /* dobry — jasna zieleń: drobne zakłócenia (≥99%) */
  --mark-3: rgb(232, 179, 30);   /* dostateczny — żółty: zauważalne zakłócenia (≥95%) */
  --mark-2: rgb(240, 138, 30);   /* dopuszczający — pomarańcz: poważne zakłócenia (≥85%) */
  --mark-1: rgb(179, 27, 37);    /* niedostateczny — czerwony: poważna awaria (<85% — i wszystko gorsze, patrz uptimeTier()) */

  /* Odnośniki */
  --link:       var(--blue-600);
  --link-hover: var(--blue-700);

  /* Geometria (baza 4px, promienie i cienie z tokens/{spacing,base}.css) */
  --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px;
  --sp-5: 20px; --sp-6: 24px; --sp-8: 32px; --sp-10: 40px;

  --r-chip:    6px;
  --r-card:    12px;
  --r-card-lg: 16px;
  --r-bento:   24px;
  --r-pill:    9999px;

  --shadow-soft: 0px 1px 2px 0px rgba(0, 0, 0, 0.05);
  --shadow-card: 2px 4px 15px 0px rgba(0, 0, 0, 0.20);

  --ease-out: cubic-bezier(0.32, 0.72, 0, 1);
  --dur-fast: 120ms;
  --dur-slow: 240ms;

  --font-display: "Plus Jakarta Sans", "Inter", system-ui, sans-serif;
  --font-body:    "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;

  color-scheme: light;
}

/* ============================================================================
   TOKENY — ciemny motyw
   Podwójna droga wejścia (bez JS i z JS), zgodnie z pułapką opisaną w
   readme DS: alias wskazujący na token nadpisywany w trybie ciemnym MUSI
   być powtórzony w bloku ciemnym (var() liczy się w miejscu deklaracji).
   Tutaj nie ma takich aliasów (każdy token jest wartością wprost), więc
   pułapka nie występuje — ale blok i tak trzyma WSZYSTKIE nadpisywane
   nazwy jawnie, dla czytelności i przyszłej rozbudowy.
   ============================================================================ */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { color-scheme: dark; }
  :root:not([data-theme="light"]) {
    --paper:       rgb(19, 20, 19);
    --paper-2:     rgb(30, 32, 30);
    --surface:     rgb(29, 31, 29);
    --ink:         rgb(238, 239, 236);
    --ink-2:       rgb(214, 216, 213);
    --muted:       rgb(151, 154, 151);
    --hint:        rgb(140, 142, 140);
    --border:      rgb(48, 51, 48);
    --border-soft: rgba(120, 124, 120, 0.18);
    --divider:     rgb(58, 61, 58);

    --status-ok:      rgb(110, 195, 148);
    --status-warning: rgb(255, 160, 80);
    --status-urgent:  rgb(240, 100, 108);
    --status-info:    rgb(110, 160, 245);

    --status-ok-bg:      rgba(90, 180, 130, 0.16);
    --status-warning-bg: rgba(255, 150, 60, 0.16);
    --status-urgent-bg:  rgba(232, 80, 90, 0.16);
    --status-info-bg:    rgba(80, 140, 240, 0.16);

    --status-ok-ink:      rgb(90, 180, 130);   /* 4.97:1 (wartość z katalogu DS) */
    --status-warning-ink: rgb(255, 160, 80);   /* = --status-warning, 6.09:1 (zmierzone) */
    --status-urgent-ink:  rgb(245, 130, 138);  /* 5.56:1 (zmierzone; baza 240,100,108 dawała 4.44 — za mało) */
    --status-info-ink:    rgb(110, 160, 245);  /* = --status-info, 5.07:1 (zmierzone) */

    --link:       rgb(120, 170, 255);
    --link-hover: rgb(160, 195, 255);
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --paper:       rgb(19, 20, 19);
  --paper-2:     rgb(30, 32, 30);
  --surface:     rgb(29, 31, 29);
  --ink:         rgb(238, 239, 236);
  --ink-2:       rgb(214, 216, 213);
  --muted:       rgb(151, 154, 151);
  --hint:        rgb(140, 142, 140);
  --border:      rgb(48, 51, 48);
  --border-soft: rgba(120, 124, 120, 0.18);
  --divider:     rgb(58, 61, 58);

  --status-ok:      rgb(110, 195, 148);
  --status-warning: rgb(255, 160, 80);
  --status-urgent:  rgb(240, 100, 108);
  --status-info:    rgb(110, 160, 245);

  --status-ok-bg:      rgba(90, 180, 130, 0.16);
  --status-warning-bg: rgba(255, 150, 60, 0.16);
  --status-urgent-bg:  rgba(232, 80, 90, 0.16);
  --status-info-bg:    rgba(80, 140, 240, 0.16);

  --status-ok-ink:      rgb(90, 180, 130);
  --status-warning-ink: rgb(255, 160, 80);
  --status-urgent-ink:  rgb(245, 130, 138);
  --status-info-ink:    rgb(110, 160, 245);

  --link:       rgb(120, 170, 255);
  --link-hover: rgb(160, 195, 255);
}

/* ============================================================================
   BAZA
   ============================================================================ */
html {
  -webkit-text-size-adjust: 100%;
}
body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  transition: background-color var(--dur-fast) ease, color var(--dur-fast) ease;
}
a { color: var(--link); text-decoration: underline; text-underline-offset: 2px; }
a:hover { color: var(--link-hover); }

.ksp-num { font-variant-numeric: tabular-nums; }

:focus-visible {
  outline: 2px solid var(--blue-600);
  outline-offset: 2px;
  border-radius: 4px;
}

.ksp-skiplink {
  position: absolute;
  left: var(--sp-3);
  top: -48px;
  background: var(--ink);
  color: var(--paper);
  padding: var(--sp-2) var(--sp-4);
  border-radius: var(--r-card);
  z-index: 100;
  transition: top var(--dur-fast) var(--ease-out);
}
.ksp-skiplink:focus { top: var(--sp-3); }

.ksp-visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ============================================================================
   LAYOUT — kolumna wyśrodkowana, max ~1180px (spec DS "Layout")
   ============================================================================ */
.ksp-shell {
  max-width: 1180px;
  margin: 0 auto;
  padding: 0 var(--sp-6);
}
@media (max-width: 640px) {
  .ksp-shell { padding: 0 var(--sp-4); }
}

/* ============================================================================
   NAGŁÓWEK
   ============================================================================ */
.ksp-topbar {
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}
.ksp-topbar-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--sp-4) 0;
}
.ksp-brand { display: flex; align-items: center; gap: var(--sp-3); }

/* Znak marki — recepta z DS (redesign znaku marki, 2026-08-03): zaokrąglenie
   24% boku, kropka 40% boku, wartości PROCENTOWE żeby znak skalował się bez
   drugiego kompletu liczb. Zawsze niebieski — to znak marki, nie wskaźnik
   statusu (ten żyje osobno w sekcji hero). */
.ksp-brand-mark {
  --size: 34px;
  width: var(--size);
  height: var(--size);
  border-radius: 24%;
  background: var(--blue-600);
  position: relative;
  flex-shrink: 0;
}
.ksp-brand-mark::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 40%;
  height: 40%;
  border-radius: 50%;
  background: var(--surface);
}
.ksp-brand-text { display: flex; align-items: baseline; gap: var(--sp-2); }
.ksp-wordmark {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 19px;
  letter-spacing: -0.6px;
  color: var(--ink);
}
.ksp-subtitle {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 13px;
  color: var(--muted);
}

.ksp-theme-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  background: var(--paper-2);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  padding: var(--sp-2) var(--sp-4);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: background-color var(--dur-fast) ease, border-color var(--dur-fast) ease;
}
.ksp-theme-toggle:hover { background: var(--border); }
.ksp-theme-toggle svg { width: 16px; height: 16px; }
.ksp-theme-toggle .ksp-icon-dark { display: none; }
:root[data-theme="dark"] .ksp-theme-toggle .ksp-icon-light { display: none; }
:root[data-theme="dark"] .ksp-theme-toggle .ksp-icon-dark { display: block; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .ksp-theme-toggle .ksp-icon-light { display: none; }
  :root:not([data-theme="light"]) .ksp-theme-toggle .ksp-icon-dark { display: block; }
}

/* ============================================================================
   HERO — sygnatura strony: "kropka" jako sam wskaźnik statusu.
   Marka Kropki to dosłownie kropka — więc na TEJ stronie duża kropka
   pulsująca w kolorze statusu jest tłumaczeniem nazwy marki na semantykę
   strony, a nie ozdobnikiem. Kolor NIE jest jedynym nośnikiem: obok stoi
   ikona kształtu (SVG) i pełny tekst stanu.
   ============================================================================ */
.ksp-hero {
  padding: var(--sp-10) 0 var(--sp-8);
  text-align: center;
}
.ksp-hero-dot-wrap {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 96px;
  height: 96px;
  border-radius: 50%;
  margin-bottom: var(--sp-5);
  background: var(--status-ok-bg);
  position: relative;
}
.ksp-hero-dot-wrap[data-state="degraded"]    { background: var(--status-warning-bg); }
.ksp-hero-dot-wrap[data-state="down"]        { background: var(--status-urgent-bg); }
.ksp-hero-dot-wrap[data-state="maintenance"] { background: var(--status-info-bg); }
.ksp-hero-dot-wrap[data-state="unknown"]     { background: var(--paper-2); }
.ksp-hero-dot-wrap[data-state="unknown"]::before { animation: none; }
.ksp-hero-dot-wrap::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: inherit;
  animation: ksp-pulse 2.4s var(--ease-out) infinite;
}
@media (prefers-reduced-motion: reduce) {
  .ksp-hero-dot-wrap::before { animation: none; }
}
@keyframes ksp-pulse {
  0%   { transform: scale(1);    opacity: 0.65; }
  70%  { transform: scale(1.55); opacity: 0; }
  100% { transform: scale(1.55); opacity: 0; }
}
.ksp-hero-dot {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--status-ok);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--surface);
  position: relative;
  z-index: 1;
  transition: background-color var(--dur-slow) var(--ease-out);
}
.ksp-hero-dot-wrap[data-state="degraded"] .ksp-hero-dot    { background: var(--status-warning); }
.ksp-hero-dot-wrap[data-state="down"] .ksp-hero-dot        { background: var(--status-urgent); }
.ksp-hero-dot-wrap[data-state="maintenance"] .ksp-hero-dot { background: var(--status-info); }
.ksp-hero-dot-wrap[data-state="unknown"] .ksp-hero-dot     { background: var(--hint); }
.ksp-hero-dot svg { width: 28px; height: 28px; }

.ksp-hero-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 32px;
  letter-spacing: -0.6px;
  margin-bottom: var(--sp-2);
  color: var(--status-ok-ink);
  transition: color var(--dur-slow) var(--ease-out);
}
.ksp-hero-title[data-state="degraded"]    { color: var(--status-warning-ink); }
.ksp-hero-title[data-state="down"]        { color: var(--status-urgent-ink); }
.ksp-hero-title[data-state="maintenance"] { color: var(--status-info-ink); }
.ksp-hero-title[data-state="unknown"]     { color: var(--hint); }
.ksp-hero-desc {
  font-size: 15px;
  color: var(--muted);
  max-width: 480px;
  margin: 0 auto var(--sp-5);
}
.ksp-hero-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2) var(--sp-4);
  font-size: 13px;
  color: var(--hint);
}
.ksp-refresh-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  background: none;
  border: none;
  color: var(--link);
  font-weight: 600;
  font-size: 13px;
  cursor: pointer;
  padding: var(--sp-1) var(--sp-2);
  border-radius: var(--r-chip);
}
.ksp-refresh-btn:hover { color: var(--link-hover); background: var(--paper-2); }
.ksp-refresh-btn svg { width: 14px; height: 14px; }
.ksp-refresh-btn.is-spinning svg { animation: ksp-spin 0.7s linear; }
@keyframes ksp-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .ksp-refresh-btn.is-spinning svg { animation: none; }
}

/* ============================================================================
   PASEK ALERTÓW (incydent przypięty w Kumie / prace serwisowe)
   Wzorzec zbliżony do .kp-alert z katalogu DS: ramka w barwie statusu,
   BEZ tła karty (żeby nie mylić się z kartami usług).
   ============================================================================ */
.ksp-banners {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  margin-bottom: var(--sp-6);
}
.ksp-banner {
  display: flex;
  gap: var(--sp-3);
  align-items: flex-start;
  border: 1.5px solid var(--status-info);
  background: var(--status-info-bg);
  border-radius: var(--r-card);
  padding: var(--sp-4);
}
.ksp-banner[data-kind="warning"] { border-color: var(--status-warning); background: var(--status-warning-bg); }
.ksp-banner[data-kind="urgent"]  { border-color: var(--status-urgent); background: var(--status-urgent-bg); }
.ksp-banner-icon {
  flex-shrink: 0;
  width: 22px; height: 22px;
  color: var(--status-info-ink);
}
.ksp-banner[data-kind="warning"] .ksp-banner-icon,
.ksp-banner[data-kind="warning"] .ksp-banner-title { color: var(--status-warning-ink); }
.ksp-banner[data-kind="urgent"] .ksp-banner-icon,
.ksp-banner[data-kind="urgent"] .ksp-banner-title { color: var(--status-urgent-ink); }
.ksp-banner-icon svg { width: 100%; height: 100%; }
.ksp-banner-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 15px;
  color: var(--status-info-ink);
  margin-bottom: 2px;
}
.ksp-banner-text {
  font-size: 14px;
  color: var(--ink-2);
  white-space: pre-line;
}
.ksp-banner-time {
  font-size: 12px;
  color: var(--hint);
  margin-top: var(--sp-1);
}

/* ============================================================================
   LEGENDA
   ============================================================================ */
.ksp-legend {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-4);
  padding: var(--sp-3) 0 var(--sp-5);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--sp-6);
}
.ksp-legend-item {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: 13px;
  color: var(--muted);
}
.ksp-legend-item svg { width: 16px; height: 16px; flex-shrink: 0; }
.ksp-legend-item[data-state="ok"] svg        { color: var(--status-ok); }
.ksp-legend-item[data-state="degraded"] svg  { color: var(--status-warning); }
.ksp-legend-item[data-state="down"] svg      { color: var(--status-urgent); }
.ksp-legend-item[data-state="maintenance"] svg { color: var(--status-info); }
.ksp-legend-item[data-state="planned"] svg     { color: var(--muted); }

/* ============================================================================
   SIATKA USŁUG
   ============================================================================ */
.ksp-section-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 20px;
  letter-spacing: -0.4px;
  margin-bottom: var(--sp-4);
}
/* Klasa `.ksp-section-note` ("Pamiętaj: nasza historia pomiarów bywa
   krótsza niż okres wskaźnika…", nad `#services-grid` w index.html) istniała
   tu do 2026-08-07 — usunięta na wyraźne życzenie właściciela razem ze
   znacznikiem w index.html. Była następczynią podpisu `.basis` (patrz niżej)
   i niosła tę samą informację w jednym, skonsolidowanym miejscu. Ryzyko
   interpretacyjne tej decyzji i potwierdzenie, że to świadomy wybór
   właściciela po przedstawieniu konsekwencji — patrz README.md → notatka
   datowana 2026-08-07 (runda 3). */
.ksp-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(440px, 1fr));
  gap: var(--sp-4);
  margin-bottom: var(--sp-8);
}
@media (max-width: 900px) {
  .ksp-grid { grid-template-columns: 1fr; }
}

/* Grupa "monitorowanie w przygotowaniu" — osobny nagłówek + jedno
   wyjaśniające zdanie NAD siatką kart, żeby czytelnik trafił na kontekst
   zanim zobaczy szarą kartę, a nie odwrotnie. Odsunięta od siatki
   monitorowanych usług hairline'em, nie samą tylko przerwą, żeby granica
   dwóch grup była widoczna także bez koloru. */
.ksp-pending-group {
  margin-top: var(--sp-6);
  padding-top: var(--sp-6);
  border-top: 1px dashed var(--border);
}
.ksp-pending-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 15px;
  letter-spacing: -0.2px;
  color: var(--muted);
  margin-bottom: var(--sp-1);
}
/* BEZ max-width (usunięty 2026-08-07, na życzenie właściciela): przy
   poprzednim, dwuzdaniowym brzmieniu 640px dawało czytelną długość wiersza,
   ale odkąd tekst skrócono do jednego zdania (patrz `PENDING_GROUP_INTRO`
   w status.js), ten sam limit zawijał akapit po ok. połowie szerokości
   sekcji zamiast pozwolić mu rozciągnąć się na pełną szerokość, spójnie
   z resztą sekcji (`.ksp-pending-title`, `.ksp-grid` obok nie mają własnego
   ograniczenia szerokości). */
.ksp-pending-intro {
  font-size: 13.5px;
  color: var(--muted);
  margin: 0 0 var(--sp-4);
}

.ksp-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-card-lg);
  box-shadow: var(--shadow-soft);
  padding: var(--sp-5);
  transition: box-shadow var(--dur-fast) ease;
}

/* flex-wrap + .ksp-card-title[min-width:0] + .ksp-pill[flex-shrink:0]:
   przyczyna, dla której kafle "monitorowanie w przygotowaniu" wychodziły
   szersze niż resztę na wąskich ekranach. Etykieta piątego stanu, "Monitoro-
   wanie w przygotowaniu" (pill niżej, biało-szara, ~29 znaków), jest dużo
   dłuższa niż etykiety czterech stanów działania ("Działa", "Awaria"…).
   Przy white-space: nowrap na pillu i domyślnym min-width: auto na flex
   itemach ta jedna kombinacja (długi pill + tytuł usługi w jednym wierszu)
   nie mieściła się w karcie na telefonie — zamiast się skurczyć, treść
   WYCHODZIŁA poza obrys karty (overflow: visible domyślnie), więc kafle w
   tej grupie wizualnie wydawały się szersze, choć siatka (.ksp-grid, ta
   sama klasa co karty monitorowane, patrz sekcja "SIATKA USŁUG" wyżej)
   przydzielała im identyczną szerokość kolumny. Z flex-wrap pill po prostu
   spada do własnego wiersza, gdy nie ma miejsca — bez zmiany szerokości
   kart, które i tak się mieściły. */
.ksp-card-head {
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-bottom: var(--sp-1);
}
.ksp-card-icon {
  width: 22px; height: 22px;
  flex-shrink: 0;
  margin-top: 1px;
  color: var(--status-ok-ink);
}
.ksp-card[data-state="degraded"] .ksp-card-icon    { color: var(--status-warning-ink); }
.ksp-card[data-state="down"] .ksp-card-icon        { color: var(--status-urgent-ink); }
.ksp-card[data-state="maintenance"] .ksp-card-icon { color: var(--status-info-ink); }
.ksp-card[data-state="unknown"] .ksp-card-icon     { color: var(--hint); }
.ksp-card[data-state="planned"] .ksp-card-icon     { color: var(--muted); }
.ksp-card-icon svg { width: 100%; height: 100%; }

/* Piąty stan — "monitorowanie w przygotowaniu": powierzchnia przygaszona
   (--paper-2) + hairline w --border, ZERO barw statusowych, żeby karta była
   od pierwszego wejrzenia odróżnialna od czterech stanów działania, a nie
   tylko "kolejnym szarym wariantem". Tytuł też przygaszony (--muted) — na
   --paper-2 ma to samo, zmierzone AA co pill niżej (patrz README). */
.ksp-card[data-state="planned"] {
  background: var(--paper-2);
  border-color: var(--border);
}
.ksp-card[data-state="planned"] .ksp-card-title { color: var(--muted); }

.ksp-card-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 16px;
  letter-spacing: -0.2px;
  flex: 1;
  min-width: 0;
}
.ksp-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border-radius: var(--r-pill);
  padding: 4px 11px;
  font-size: 12px;
  font-weight: 700;
  white-space: nowrap;
  flex-shrink: 0;
  color: var(--status-ok-ink);
  background: var(--status-ok-bg);
}
.ksp-pill[data-state="degraded"]    { color: var(--status-warning-ink); background: var(--status-warning-bg); }
.ksp-pill[data-state="down"]        { color: var(--status-urgent-ink); background: var(--status-urgent-bg); }
.ksp-pill[data-state="maintenance"] { color: var(--status-info-ink); background: var(--status-info-bg); }
.ksp-pill[data-state="unknown"]     { color: var(--hint); background: var(--paper-2); }
/* Piąty stan: neutralna szarość z tokenów (muted na paper-2, ramka
   border) — bez tła w barwie statusu, w odróżnieniu od czterech
   pozostałych warunków. Kontrast muted/paper-2 zmierzony w README:
   ok. 5,9:1 jasny motyw / 5,8:1 ciemny — AA przy dużym marginesie. */
.ksp-pill[data-state="planned"] {
  color: var(--muted);
  background: var(--paper-2);
  border: 1px solid var(--border);
}
.ksp-pill svg { width: 12px; height: 12px; }

.ksp-card-desc {
  font-size: 13.5px;
  color: var(--muted);
  margin: 0 0 var(--sp-4);
  padding-left: 34px;
}

/* Wyjaśnienie widoczne na KAŻDEJ karcie w przygotowaniu — nie tylko w
   nagłówku grupy, bo karta ma stać na własnych nogach: kto trafi tu z
   linku do jednej pozycji, ma dostać to samo zapewnienie, że brak
   pomiaru nie znaczy awarii. */
.ksp-card-note {
  display: flex;
  gap: var(--sp-2);
  align-items: flex-start;
  font-size: 12.5px;
  color: var(--muted);
  padding-left: 34px;
  padding-top: var(--sp-3);
  border-top: 1px solid var(--border);
}
.ksp-card-note svg { width: 14px; height: 14px; flex-shrink: 0; margin-top: 1px; }

/* TRZY kolumny (24h / 30 dni / 90 dni) — kafel "90 dni" krótko (tego samego
   dnia, 2026-08-07) usunięty, bo pasek obok pokazuje fizycznie tylko 30 dni,
   i PRZYWRÓCONY na wyraźne życzenie właściciela: chce widzieć trzy okresy
   niezależnie od tego, ile dni ma pasek pod spodem. `uptime90` liczy się
   zawsze z surowych heartbeatów (`uptimeFromHeartbeats`, okno 90 dni) w
   `buildEntries()` w status.js — NIEZALEŻNIE od `HISTORY_DAYS` (stałej, która
   reguluje wyłącznie pasek/dzienne kubełki, patrz tam). Patrz README.md →
   notatka datowana 2026-08-07 (runda 3) o ryzyku interpretacyjnym tej
   kombinacji (wskaźnik za dłuższy okres niż pasek pokazuje). */
.ksp-uptime-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--sp-2);
  margin-bottom: var(--sp-4);
}
.ksp-uptime-stat {
  background: var(--paper-2);
  border-radius: var(--r-chip);
  padding: var(--sp-2) var(--sp-1);
  text-align: center;
}
.ksp-uptime-stat .num {
  display: block;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 17px;
  letter-spacing: -0.2px;
}
.ksp-uptime-stat .label {
  display: block;
  font-size: 10.5px;
  color: var(--hint);
  margin-top: 1px;
}
/* Klasa `.basis` ("na czym się oparł wskaźnik", np. "z 1 dnia pomiarów")
   istniała tu do 2026-08-07 — usunięta na życzenie właściciela razem z jej
   znacznikiem w `status.js` (powtarzała się pod KAŻDYM z dwóch/trzech
   wskaźników na KAŻDEJ karcie). Informacja, którą niosła, była przeniesiona
   w jedno miejsce (`.ksp-section-note` nad siatką kart) — TA klasa z kolei
   usunięta tego samego dnia (runda 3, patrz komentarz przy `.ksp-grid`
   wyżej). Informacja jako widoczny tekst nie istnieje już nigdzie na
   stronie — świadoma decyzja właściciela, udokumentowana w README.md →
   notatka datowana 2026-08-07 (runda 3). */

/* Pasek historii — 30 dziennych słupków (zmniejszone z 90, 2026-08-07,
   wzorzec status.claude.com — patrz `HISTORY_DAYS` w status.js), ale JEDEN
   widget klawiatury: `role="slider"` + `tabindex` na kontenerze `.ksp-bars`,
   strzałki/Home/End przechodzą między dniami (patrz `selectDay()` w
   status.js). Pojedyncze słupki (`.ksp-bar`) są `aria-hidden` i BEZ
   własnego `tabindex` — nie są przystankami Tab — ale nadal reagują na
   hover/klik/dotyk (nasłuchy per-słupek + delegacja kliknięcia na
   `.ksp-bars`, patrz status.js), więc mysz i dotyk mogą skoczyć na dowolny
   dzień bez przechodzenia przez slider. Patrz README → "Szczegóły dnia na
   pasku historii". `:focus-visible` na `.ksp-bars` dostaje domyślny
   pierścień z reguły globalnej (patrz sekcja BAZA) — nie trzeba go tu
   powtarzać.

   Geometria (2026-08-07): przy trzykrotnie mniejszej liczbie słupków (30
   zamiast 90) każdy słupek ma na tej samej szerokości karty ok. trzy razy
   więcej miejsca — wykorzystane na WYRAŹNY odstęp (`gap`) i zauważalne
   zaokrąglenie rogów, żeby słupki czytały się jako osobne, zaokrąglone
   prostokąty (wzorzec status.claude.com), nie jako ciągły pasek. Ten sam
   odstęp geometryczny jest też argumentem za usunięciem obrysu niżej — patrz
   komentarz przy `.ksp-bar` i przy tokenach `--mark-*` wyżej. */
.ksp-bars {
  display: flex;
  align-items: flex-end;
  gap: 3px;
  height: 34px;
  margin-bottom: 4px;
  cursor: pointer;
  border-radius: 3px;
}
.ksp-bar {
  flex: 1 1 0;
  min-width: 4px;
  height: 100%;
  border-radius: 3px;
  background: var(--border);
  /* BEZ obrysu (usunięty 2026-08-07, na życzenie właściciela — czytał się
     jako ramka wokół każdego słupka; poprzednia wersja miała tu
     `box-shadow: 0 0 0 1px var(--hint)`, dodany 2026-08-06 właśnie po to,
     żeby spełnić WCAG 1.4.11 (3:1) niezależnie od tego, czy wypełnienie
     samo go spełnia). Konsekwencje usunięcia, zmierzone liczby i
     rozważona/odrzucona alternatywa (przyciemnienie tła zamiast obrysu) —
     patrz obszerny komentarz przy tokenach `--mark-*` w bloku TOKENY na
     górze pliku. Skrót decyzji: 3:1 formalnie NIE jest tu spełnione dla
     części kombinacji (jasny motyw: tier 2/3/4; ciemny motyw: tier 1) —
     świadomie przyjęte, bo szersze słupki (patrz `.ksp-bars` wyżej) dają
     rozróżnialność geometrią, a kolor i tak nigdy nie jest jedynym
     nośnikiem informacji (tooltip, panel `.ksp-day-detail`, `aria-valuetext`
     — wszystkie trzy niosą ten sam tekst). */
}
/* Dzień z rozstrzygającym pomiarem (`data-status="measured"`, ustawiane w
   status.js) — kolor NIE jest już kategorią ("był DOWN → czerwony",
   niezależnie od czasu trwania), tylko stopniem dostępności TEGO dnia
   (`data-tier`, 1–5, patrz `uptimeTier()` w status.js). Nieliniowo:
   różnicujemy gęsto blisko 100% (różnica 100% vs 99% to realnie ~1% czasu
   przestoju — dokładnie tam, gdzie liniowy gradient spłaszczałby wszystko
   do jednego odcienia zieleni) i grubo poniżej 85% — 85%, 40% i 10% to
   ten sam, najciemniejszy odcień, bo wszystkie trzy znaczą "poważna
   awaria" i dalsze różnicowanie nic by nie powiedziało. Wartości kolorów:
   tokeny `--mark-{1..5}` (skala ocen z DS, zdefiniowana w bloku TOKENY
   wyżej) — zero nowych hexów, zero `color-mix` (nie potrzebne: każdy
   słupek to JEDNA płaska barwa reprezentująca jeden dzień, nie gradient
   wewnątrz słupka). Kolorowanie idzie WYŁĄCZNIE przez atrybut
   `data-tier`, czytany tym selektorem CSS — status.js nigdy nie dotyka
   `bar.style…`, więc nawet gdyby dotknął (byłaby to własność JS, nie
   atrybut `style=""` w HTML — CSP `style-src` tego nie blokuje), i tak nie
   ma potrzeby: to jest prostsze, bardziej deklaratywne i idzie tym samym
   wzorcem co już istniejące `data-status`/`data-active` na tym elemencie. */
.ksp-bar[data-tier="5"] { background: var(--mark-5); }
.ksp-bar[data-tier="4"] { background: var(--mark-4); }
.ksp-bar[data-tier="3"] { background: var(--mark-3); }
.ksp-bar[data-tier="2"] { background: var(--mark-2); }
.ksp-bar[data-tier="1"] { background: var(--mark-1); }
.ksp-bar[data-status="maintenance"] {
  background: repeating-linear-gradient(
    -45deg, var(--status-info), var(--status-info) 2px,
    color-mix(in srgb, var(--status-info) 55%, var(--surface)) 2px,
    color-mix(in srgb, var(--status-info) 55%, var(--surface)) 4px
  );
}
.ksp-bar[data-status="no-data"] {
  background: repeating-linear-gradient(
    -45deg, var(--border), var(--border) 2px,
    var(--surface) 2px, var(--surface) 4px
  );
}
/* Dzień aktualnie pokazany w panelu szczegółów pod paskiem — pierścień
   niebieski niezależny od stanu fokusu (widoczny też po kliknięciu myszą/
   dotyku), żeby było jasne, KTÓREGO słupka dotyczy tekst poniżej. Jedyny
   obrys, jaki ten element w ogóle dostaje (bazowy `.ksp-bar` jest od
   2026-08-07 bez obrysu — patrz komentarz tam) — kontrast niebieskiego
   pierścienia wobec `--surface` jest i tak dużo wyższy niż wymagane 3:1,
   więc nie ma potrzeby dokładać nic dodatkowego. */
.ksp-bar[data-active="true"] {
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--blue-600);
}
.ksp-bars-caption {
  display: flex;
  justify-content: space-between;
  font-size: 11px;
  color: var(--hint);
  margin-bottom: 2px;
}
.ksp-bars-hint {
  font-size: 11px;
  color: var(--hint);
  margin: 0 0 var(--sp-3);
}

/* Panel szczegółów WYBRANEGO dnia — jedno miejsce pod paskiem (nie 90
   chmurek na hover). Kolor akcentu przełącza się na stan tego jednego dnia,
   tak jak pill w nagłówku karty przełącza się na stan bieżący. */
.ksp-day-detail {
  background: var(--paper-2);
  border: 1px solid var(--border);
  border-radius: var(--r-chip);
  padding: var(--sp-3);
  margin-bottom: var(--sp-4);
}
.ksp-day-detail-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-2);
  margin-bottom: 2px;
}
.ksp-day-detail-date {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 13px;
}
.ksp-day-detail-status {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-2);
}
/* Kolor tekstu statusu w panelu — reuse istniejących tokenów `--status-*-ink`
   (już zmierzonych na AA 4.5:1 na `--paper-2`, patrz blok TOKENY), NIE
   `--mark-*` z paska wyżej: te kolorują tylko WYPEŁNIENIE słupka (gdzie próg
   kontrastu tekstu nie obowiązuje), a jako kolor TEKSTU część z nich
   (zwłaszcza `--mark-4`/`--mark-3`, jasna zieleń i żółty) nie przechodzi AA
   na tym tle — stąd świadome pogrupowanie 5 stopni w 3 tony tekstu:
   4–5 (dobrze) → zielony, 2–3 (zakłócenia) → pomarańcz, 1 (awaria) → czerwony.
   Sam kolor SŁUPKA obok (5 odcieni) i tak niesie pełną rozdzielczość —
   tu liczy się głównie treść zdania, nie kolor. */
.ksp-day-detail[data-tier="5"] .ksp-day-detail-status,
.ksp-day-detail[data-tier="4"] .ksp-day-detail-status { color: var(--status-ok-ink); }
.ksp-day-detail[data-tier="3"] .ksp-day-detail-status,
.ksp-day-detail[data-tier="2"] .ksp-day-detail-status { color: var(--status-warning-ink); }
.ksp-day-detail[data-tier="1"] .ksp-day-detail-status { color: var(--status-urgent-ink); }
.ksp-day-detail[data-status="maintenance"] .ksp-day-detail-status { color: var(--status-info-ink); }
.ksp-day-detail[data-status="no-data"] .ksp-day-detail-status     { color: var(--hint); }
.ksp-day-detail-note {
  font-size: 12px;
  color: var(--muted);
  margin: var(--sp-1) 0 0;
}

/* Tooltip słupka — hover myszą i dotyk (patrz status.js, sekcja "Tooltip
   słupka" w `renderServiceCard`), pozycjonowany dynamicznie przez
   `tooltipEl.style.left/top` (WŁAŚCIWOŚĆ JS, nie atrybut `style=""` w
   znaczniku — zgodne z CSP `style-src 'self'`, patrz README). `position:
   fixed`, bo współrzędne liczone są z `getBoundingClientRect()` (viewport),
   nie względem jakiegoś przodka — nie potrzeba dodatkowego `position:
   relative` na karcie/pasku. `pointer-events: none`, żeby tooltip sam nigdy
   nie przechwycił hovera/kliknięcia (czysto informacyjny, nie interaktywny)
   i `aria-hidden="true"` w znaczniku — ta sama treść jest już dostępna
   czytnikom ekranu przez `aria-valuetext` slidera, więc tooltip nigdy nie
   jest jedynym nośnikiem informacji. Kolory z pary `--ink`/`--paper`
   ODWRÓCONEJ względem strony (tło = `--ink`, tekst = `--paper`) — w obu
   motywach to niemal czysta czerń na niemal czystej bieli (albo odwrotnie
   w trybie ciemnym), więc kontrast tekstu jest zawsze bardzo wysoki bez
   osobnego pomiaru dla tego jednego komponentu. */
.ksp-bar-tooltip {
  position: fixed;
  z-index: 60;
  max-width: 240px;
  background: var(--ink);
  color: var(--paper);
  border-radius: var(--r-chip);
  padding: var(--sp-2) var(--sp-3);
  box-shadow: var(--shadow-card);
  pointer-events: none;
}
.ksp-bar-tooltip-date {
  display: block;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 12.5px;
  margin-bottom: 2px;
}
.ksp-bar-tooltip-status {
  display: block;
  font-size: 12px;
  line-height: 1.4;
}

.ksp-meta-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-1) var(--sp-4);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--border);
  font-size: 12.5px;
  color: var(--muted);
}
.ksp-meta-item { display: inline-flex; align-items: center; gap: 5px; }
.ksp-meta-item svg { width: 14px; height: 14px; flex-shrink: 0; }
.ksp-trend-up   { color: var(--status-warning-ink); }
.ksp-trend-down { color: var(--status-ok-ink); }
.ksp-trend-flat { color: var(--hint); }

.ksp-card-unmatched {
  font-size: 13px;
  color: var(--hint);
  font-style: italic;
}

/* ============================================================================
   OŚ CZASU ZDARZEŃ
   ============================================================================ */
.ksp-incidents { margin-bottom: var(--sp-8); }
.ksp-incident-list {
  border-left: 2px solid var(--border);
  padding-left: var(--sp-5);
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
}
.ksp-incident {
  position: relative;
}
.ksp-incident::before {
  content: "";
  position: absolute;
  left: calc(-1 * var(--sp-5) - 5px);
  top: 4px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--status-urgent-ink);
  border: 2px solid var(--surface);
  box-shadow: 0 0 0 1px var(--border);
}
.ksp-incident[data-state="resolved"]::before { background: var(--status-ok-ink); }
.ksp-incident-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-1);
}
.ksp-incident-service {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 14px;
}
.ksp-incident-text {
  font-size: 13.5px;
  color: var(--ink-2);
  margin-bottom: 4px;
}
.ksp-incident-foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  font-size: 12px;
  color: var(--hint);
}

.ksp-empty {
  text-align: center;
  padding: var(--sp-8) var(--sp-4);
  color: var(--muted);
  font-size: 14px;
  border: 1px dashed var(--border);
  border-radius: var(--r-card-lg);
}

/* ============================================================================
   STAN BŁĘDU — API niedostępne
   ============================================================================ */
.ksp-error {
  text-align: center;
  padding: var(--sp-10) var(--sp-4);
  border: 1px solid var(--status-urgent);
  background: var(--status-urgent-bg);
  border-radius: var(--r-card-lg);
  margin-bottom: var(--sp-8);
}
.ksp-error-icon { color: var(--status-urgent-ink); width: 32px; height: 32px; margin: 0 auto var(--sp-3); }
.ksp-error h2 {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 20px;
  color: var(--status-urgent-ink);
  margin-bottom: var(--sp-2);
}
.ksp-error p { color: var(--ink-2); max-width: 440px; margin: 0 auto var(--sp-4); }
.ksp-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  background: var(--blue-600);
  color: var(--blue-050);
  border: none;
  border-radius: var(--r-pill);
  padding: var(--sp-2) var(--sp-5);
  font-weight: 700;
  font-size: 14px;
  cursor: pointer;
  transition: background-color var(--dur-fast) ease, transform var(--dur-fast) ease;
}
.ksp-btn:hover { background: var(--blue-700); }
.ksp-btn:active { transform: scale(0.98); }
.ksp-btn svg { width: 16px; height: 16px; }

/* ============================================================================
   SZKIELET ŁADOWANIA
   ============================================================================ */
.ksp-skeleton-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-card-lg);
  padding: var(--sp-5);
  height: 220px;
}
.ksp-skeleton-line {
  height: 14px;
  border-radius: var(--r-chip);
  background: linear-gradient(90deg, var(--paper-2) 25%, var(--border) 37%, var(--paper-2) 63%);
  background-size: 400% 100%;
  animation: ksp-shimmer 1.4s ease infinite;
  margin-bottom: var(--sp-3);
}
@keyframes ksp-shimmer {
  0% { background-position: 100% 50%; }
  100% { background-position: 0 50%; }
}
@media (prefers-reduced-motion: reduce) {
  .ksp-skeleton-line { animation: none; background: var(--paper-2); }
}

/* ============================================================================
   STOPKA
   ============================================================================ */
.ksp-footer {
  border-top: 1px solid var(--border);
  padding: var(--sp-6) 0 var(--sp-10);
  text-align: center;
  color: var(--hint);
  font-size: 12.5px;
}
.ksp-footer p { margin-bottom: var(--sp-2); }

/* Sprite ikon — musi być wizualnie ukryty, ale czytany przez <use>. Klasa,
   nie inline style: nginx tej domeny serwuje `style-src 'self'` bez
   `'unsafe-inline'`, więc atrybut style="" byłby zablokowany przez CSP i
   sprite wróciłby do domyślnych wymiarów SVG (300×150), robiąc pustą
   przerwę nad nagłówkiem. */
.ksp-icon-sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

/* Ikona w stanie błędu wypełnia cały swój kontener (.ksp-error-icon ma
   już wymiary 32×32 z reguły niżej) — bez inline style z tego samego
   powodu co sprite wyżej. */
.ksp-error-icon svg {
  width: 100%;
  height: 100%;
}

/* Szerokości linii szkieletu ładowania — wcześniej inline style="width:…",
   przeniesione tu z tego samego powodu (CSP `style-src 'self'`). Wartości
   celowo identyczne dla obu kart placeholderowych: to czysto kosmetyczna
   sugestia tekstu pod spodem, różnica między kartami nie niosła żadnej
   informacji. */
.ksp-skeleton-card .ksp-skeleton-line:nth-child(1) { width: 60%; }
.ksp-skeleton-card .ksp-skeleton-line:nth-child(2) { width: 90%; }
.ksp-skeleton-card .ksp-skeleton-line:nth-child(3) { width: 40%; }

/* Wariant <noscript> — treść neutralna (nie czerwona jak błąd API), bez
   inline style z tego samego powodu co wyżej. */
.ksp-noscript-shell { padding-top: var(--sp-6); }
.ksp-error--neutral {
  border-color: var(--border);
  background: var(--paper-2);
}
.ksp-error--neutral h2 { color: var(--ink); }

/* ============================================================================
   OSTRZEŻENIE O NIEAKTUALNYCH DANYCH
   Liczone lokalnie z wieku najświeższego heartbeatu — API może zwracać 200
   nawet gdy silnik monitoringu przestał realnie sprawdzać usługi. Wizualnie
   spokrewnione z .ksp-banner (ten sam wzorzec: ramka w barwie statusu, bez
   wypełnienia karty), ale to osobny, lokalnie liczony komunikat, nie treść
   z API — patrz README, sekcja "Dane mogą być nieaktualne".
   ============================================================================ */
.ksp-stale-notice {
  display: flex;
  gap: var(--sp-3);
  align-items: flex-start;
  border: 1.5px solid var(--status-warning);
  background: var(--status-warning-bg);
  border-radius: var(--r-card);
  padding: var(--sp-4);
  margin-bottom: var(--sp-6);
  color: var(--status-warning-ink);
  font-size: 14px;
  line-height: 1.5;
}
.ksp-stale-notice svg {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  margin-top: 1px;
}

[hidden] { display: none !important; }
