/* ══════════════════════════════════════════════════════════════════════════════
   NINERDECK - PANEL · WSPÓLNY ARKUSZ MAKIET
   ══════════════════════════════════════════════════════════════════════════════

   PLIK GENEROWANY - NIE EDYTUJ RĘCZNIE.
   Źródło: admin/src/styles/ (arkusze panelu, w kolejności kaskady z admin/src/main.tsx)
           + design/panel/rama.css (kanwa, okno przeglądarki, inwentarz - tylko makieta)
   Odtworzenie: npm run panel:css --workspace admin
   Równość pliku ze źródłem przybija admin/test/panelCss.generated.test.ts.

   CZYM JEST TEN PLIK
   Odpowiednik bloku <style> z makiet telefonu (design/*.html), wyniesiony do osobnego
   pliku, bo makiet panelu jest kilkanaście. Makieta linkuje go jednym wierszem:
   <link rel="stylesheet" href="panel.css">. Panel i makiety mają DOKŁADNIE te same
   reguły i te same nazwy klas - grep po `cell-sub` znajduje jednocześnie makietę
   i ekran, a recenzent porównuje DOM z plikiem HTML linia w linię.

   KIERUNEK: MAKIETA PROWADZI UKŁAD, ARKUSZ JEST JEDEN
   Nowy ekran zaczyna się od makiety (design/panel/SZABLON.html); nowy komponent
   dokłada się do arkusza w admin/src/styles/components/ i uruchamia generator -
   makieta widzi go w tej samej chwili, co panel. Klasy tylko makiety (rama, atrapa
   przycisku Google, komponenty czekające na kod) mieszkają w rama.css.

   CZCIONKI
   Z Google Fonts, przez <link> w <head> makiety - nie z self-hostowanych woff2,
   którymi jedzie panel (admin/src/styles/fonts.css). Makieta ma się otwierać z dysku
   dwuklikiem, a @font-face ze ścieżkami /fonts/* działa wyłącznie na serwerze panelu.
   Kroje i wagi są te same (Bebas Neue · Archivo 400-700 · JetBrains Mono 400/500/700),
   więc metryka też.
   ══════════════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════════════
   01. TOKENY
   źródło: admin/src/styles/tokens.css
   ═══════════════════════════════════════════════════════════════════════════ */

/* ══════════════════════════════════════════════════════════════════════
   PLIK GENEROWANY - NIE EDYTUJ RĘCZNIE.
   Źródło: packages/tokens (THEMES.night) · generator: packages/tokens/scripts/emitCss.ts
   Odtworzenie: npm run tokens:css --workspace admin
   Równość pliku ze źródłem przybija admin/test/tokens.generated.test.ts.

   Panel ma JEDEN motyw (night) i nie ma przełącznika: motyw jasny aplikacji
   istnieje dla kokpitu w słońcu, a administrator siedzi przy biurku
   (docs/architektura-panelu-frontend.md §1.6).

   Wymiarów ramy panelu (--sidebar-w, --topbar-h) TU NIE MA i nie będzie -
   to układ jednej powierzchni, a nie token produktu. Mieszkają w layout.css.
   ══════════════════════════════════════════════════════════════════════ */

:root {
  --bg: #0D0D0D;
  --bg-tint: #111318;
  --surface: #141414;
  --surface-raised: #1A1A1A;
  --surface-hover: #222222;
  --border: #252525;
  --border-strong: #333333;
  --hairline: rgba(255,255,255,0.04);
  --text-primary: #E8E8E8;
  --text-secondary: #888888;
  --text-muted: #7A7A7A;
  --text-placeholder: #565656;
  --green: #2ECC71;
  --green-hover: #3DDC82;
  --green-muted: rgba(46,204,113,0.12);
  --green-border: rgba(46,204,113,0.28);
  --green-glow: rgba(46,204,113,0.4);
  --amber: #F39C12;
  --amber-muted: rgba(243,156,18,0.12);
  --amber-border: rgba(243,156,18,0.28);
  --amber-glow: rgba(243,156,18,0.35);
  --red: #E74C3C;
  --red-muted: rgba(231,76,60,0.12);
  --red-border: rgba(231,76,60,0.28);
  --red-glow: rgba(231,76,60,0.4);
  --blue: #3498DB;
  --blue-muted: rgba(52,152,219,0.12);
  --blue-border: rgba(52,152,219,0.28);
  --overlay: rgba(0,0,0,0.74);
  --selection: rgba(255,255,255,0.22);
  --font-display: 'Bebas Neue', sans-serif;
  --font-body: 'Archivo', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', monospace;
}

/* ═══════════════════════════════════════════════════════════════════════════
   02. RESET I KORZEŃ DOKUMENTU
   źródło: admin/src/styles/base.css
   ═══════════════════════════════════════════════════════════════════════════ */

/**
 * Ninerdeck - panel: reset, korzeń dokumentu i stany globalne.
 *
 * `body` JEST przeglądarką (w makiecie jest kanwą wokół obrazka okna - stąd tam
 * gradienty i `--app-scale`, a tu ich nie ma). Tło korzenia to `--bg`, czyli CHROME
 * panelu: od stylu lekkiego (issue #107) pasek górny i kolumna boczna stoją na `--bg`,
 * a obszar treści leży NA NICH jako warstwa wyżej - na `--surface`, z zaokrąglonym
 * rogiem (`layout.css`). Pasek i kolumna są PRZEZROCZYSTE: tło (kolor i lekki
 * gradient) maluje korzeń, żeby chrome był jedną powierzchnią bez szwów.
 *
 * Stopień pisma 14 px i interlinia 1,45 na korzeniu: panel czyta się jak strona
 * (GitLab, GitHub), nie jak kokpit, więc gęstość 12,5 px z panelu 2.0 zeszła.
 */

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html,
body {
  height: 100%;
}

body {
  font-family: var(--font-body);
  font-size: 14px;
  line-height: 1.45;
  background-color: var(--bg);
  /* Gradient chrome'u - definicja i uzasadnienie w `layout.css` (`--chrome-bg`).
     `fixed`, bo body nie przewija się nigdy (przewija się kontener treści),
     a gradient ma stać w miejscu jak tło okna. */
  background-image: var(--chrome-bg);
  background-attachment: fixed;
  color: var(--text-primary);
  -webkit-font-smoothing: antialiased;
}

#root {
  height: 100%;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/**
 * Link w treści jest błękitny, jak w każdej aplikacji web - to jedyny kolor, którego
 * użytkownik przeglądarki nie musi się uczyć. Komponenty z własną barwą (`.btn`,
 * `.nav-item`, `.crumbs a`) nadpisują to swoją klasą.
 */
a {
  color: var(--blue);
}

/**
 * Widoczny fokus klawiatury - panel jest nawigowany klawiaturą: tabele, filtry,
 * formularze. `:focus-visible`, nie `:focus`, żeby kliknięcie myszą nie rysowało
 * obwódki tam, gdzie nikt jej nie potrzebuje.
 */
:focus-visible {
  outline: 2px solid var(--green-border);
  outline-offset: 2px;
  border-radius: 6px;
}

::selection {
  background: var(--selection);
}

/**
 * Treść WYŁĄCZNIE dla czytnika ekranu - podpis tabeli (`<caption>`).
 *
 * Czytnik ekranu potrzebuje nazwy tabeli WEWNĄTRZ tabeli, bo czyta ją jako osobny
 * obiekt i inaczej ogłasza „tabela, 8 kolumn" bez słowa o tym, czego dotyczy.
 * Widoczny `<caption>` zmieniłby wygląd, więc go chowamy - nie usuwamy.
 */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ═══════════════════════════════════════════════════════════════════════════
   03. WYMIARY RAMY I SZKIELET UKŁADU
   źródło: admin/src/styles/layout.css
   ═══════════════════════════════════════════════════════════════════════════ */

/**
 * Ninerdeck - panel: WYMIARY RAMY i szkielet układu.
 *
 * Osobny plik od `tokens.css` i to jest twarda linia: tokeny to wartości PRODUKTU,
 * wspólne z aplikacją pilota; `--topbar-h` i `--sidebar-w` to wymiary jednej
 * powierzchni. Telefon nie ma paska ani kolumny panelu, więc te zmienne nie mają
 * czego szukać w pakiecie tokenów.
 *
 * ══ UKŁAD OD STYLU LEKKIEGO (issue #107) ══
 *
 *     ┌──────────────── .topbar ────────────────┐   48 px, na --bg (chrome)
 *     ├── .sidebar ──╭──────── .content ────────┤
 *     │   240 px     │  na --surface, przewija  │   WARSTWA WYŻEJ: zaokrąglony górny
 *     │   na --bg    │  ┌──── .page ────┐       │   lewy róg i włos krawędzi - jak
 *     │              │  │  nagłówek…    │       │   panel treści w GitLabie
 *     └──────────────┴──┴───────────────┴───────┘
 *
 * Treść jest WARSTWĄ NAD chrome'em (uwaga właściciela przy przeglądzie #107: „główny
 * kontent powinien być tak jakby w oknie o zaokrąglonych krawędziach, tak jakby
 * warstwa wyżej"): chrome ciemniejszy (`--bg`), treść jaśniejsza (`--surface`),
 * oba górne rogi zaokrąglone i odstęp od prawej krawędzi okna, dół dociągnięty do
 * krawędzi. Pasek i kolumna NIE są sekcjami z własnymi liniami - są tłem, na którym
 * leży kontener. Karty i tabele na tym tle rysuje sama ramka `--border`; to jest
 * sens stylu lekkiego - powierzchnia nie potrzebuje drugiego odcienia, żeby być kartą.
 *
 * `.workspace` to rząd (kolumna + treść) pod paskiem; `.page` jest wewnętrzną kolumną
 * treści. Dwa pudełka zamiast jednego, bo `.content` jest OBSZAREM PRZEWIJANIA
 * (i warstwą z tłem), a `.page` - szerokością czytania: gdyby jedno pudełko trzymało
 * oba, tło kończyłoby się razem z kolumną 1200 px i na szerokim monitorze treść
 * stałaby na wyspie.
 */

:root {
  --topbar-h: 48px;
  --sidebar-w: 240px;
  /* Treść nie rozlewa się na pełną szerokość monitora: wiersz tabeli szerszy niż
     to oko przenosi między kolumnami, gubiąc wiersz w połowie drogi. */
  --content-max: 1200px;
  /* TŁO CHROME'U - lekki gradient pod paskiem i kolumną (uwaga właściciela przy
     przeglądzie #107: „tło z lekkim gradientem daje efekt nowoczesności"). Zielona
     poświata przy znaku w lewym górnym rogu i chłodny ton `--bg-tint` u góry, oba
     gasnące w `--bg`. Wyłącznie z tokenów - to nie jest nowy kolor, tylko rozłożenie
     istniejących; kontener treści jest nieprzezroczysty, więc gradient widać tylko
     tam, gdzie chrome jest tłem (i na logowaniu, gdzie tłem jest cały ekran).
     Zmienna RAMY, nie tokenu produktu - telefon nie ma chrome'u. */
  --chrome-bg:
    radial-gradient(900px 480px at 0% 0%, var(--green-muted), transparent 70%),
    linear-gradient(180deg, var(--bg-tint) 0%, var(--bg) 55%);
}

.workspace {
  flex: 1;
  display: flex;
  min-height: 0;
}

/* Kontener treści: oba górne rogi zaokrąglone, odstęp od prawej krawędzi okna,
   dół dociągnięty do krawędzi (tam jest przewijanie - zaokrąglony dół czytałby się
   jak koniec strony). Włos na trzech krawędziach; dolnej nie ma z tego samego powodu. */
.content {
  flex: 1;
  min-width: 0;
  overflow-y: auto;
  margin-right: 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-bottom: none;
  border-radius: 12px 12px 0 0;
  padding: 20px 32px 48px;
}

.page {
  width: 100%;
  max-width: var(--content-max);
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* Kolumna scrollowana: dzieci NIE mogą się kurczyć - inaczej `.table-wrap`
   z `overflow:hidden` obcina wiersze, zamiast pozwolić `.content` przewinąć. */
.page > *,
.drawer-body > * {
  flex-shrink: 0;
}

.content::-webkit-scrollbar {
  width: 10px;
}

.content::-webkit-scrollbar-thumb {
  background: var(--border-strong);
  border-radius: 99px;
  border: 3px solid var(--surface);
}

/** Ekran PRZED ramą (plamka sesji) - kolumna wyśrodkowana w oknie. */
.centered {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  padding: 24px;
  overflow-y: auto;
}

/** Siatka dwóch pól w rzędzie (rok i pojemność, minimum i zbiornik oleju). */
.field-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

/* ═══════════════════════════════════════════════════════════════════════════
   04. RAMA - PASEK GÓRNY, KOLUMNA BOCZNA, OKRUSZKI
   źródło: admin/src/styles/components/shell.css
   ═══════════════════════════════════════════════════════════════════════════ */

/**
 * Ninerdeck - panel: RAMA - pasek górny, kolumna boczna, okruszki.
 *
 * Od stylu lekkiego (issue #107, 2026-09-08) nawigacja wraca do KOLUMNY BOCZNEJ, jak
 * w GitLabie: pasek górny niesie wyłącznie markę i zalogowanego, kolumna - kontekst
 * (w którym klubie jest ta sesja) i pozycje modułów z ikonami. Pasek 2.0 z czterema
 * zakładkami pośrodku zniknął razem z argumentem, który go trzymał („kolumna przy
 * dwóch modułach oddaje ćwierć okna pod dwa słowa"): modułów jest cztery, a od
 * wielofirmowości 2.0.0 nad nimi stoi jeszcze kontekst klubu - w pasku nie było na to
 * miejsca, w kolumnie jest jego naturalne.
 *
 * Kolory bez zmian wobec panelu 2.0 - zmienił się PODZIAŁ: chrome (pasek i kolumna)
 * stoi na `--bg`, a treść leży na nim jako warstwa wyżej na `--surface` z zaokrąglonym
 * rogiem (`layout.css`). Pasek i kolumna NIE mają własnych włosów: granicę rysuje
 * krawędź panelu treści, a linia między paskiem a kolumną dzieliłaby chrome, który
 * ma być jedną powierzchnią. Lekkość ma wyjść z układu i typografii, nie z palety
 * (decyzja właściciela przy issue #107).
 */

/* ── pasek górny ── */
.topbar {
  height: var(--topbar-h);
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 0 16px;
  /* Bez własnego tła: chrome maluje korzeń (`base.css`) - kolor i lekki gradient -
     a pasek i kolumna są jego częścią, nie sekcjami. */
  background: transparent;
}

/* Znak jest linkiem na ekran startowy - w każdej aplikacji web logo prowadzi „do domu",
   a użytkownik przeglądarki tego oczekuje, zanim przeczyta cokolwiek innego. */
.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
  color: var(--text-primary);
  text-decoration: none;
}

.brand-mark {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  background: var(--green-muted);
  color: var(--green);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* Jedyne miejsce w ramie pisane krojem display: marka ma prawo wyglądać jak marka.
   Wszystko poniżej paska - tytuły stron, kart, szuflad - jedzie krojem tekstowym. */
.brand-name {
  font-family: var(--font-display);
  font-size: 18px;
  letter-spacing: 2.5px;
}

.topbar-right {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 10px;
}

/* Zalogowany: kółko z inicjałami i nazwisko. Inicjały, nie zdjęcie - panel nie ma
   awatarów i nie ciągnie obrazków z Google; kółko jest tu wyłącznie po to, żeby oko
   znalazło „kto" w tym samym rogu, co w każdej innej aplikacji. */
.who {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.avatar {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  color: var(--text-primary);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.5px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.who-name {
  font-size: 13px;
  color: var(--text-secondary);
}

/* ── kolumna boczna ── */
.sidebar {
  width: var(--sidebar-w);
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 6px 12px 16px;
  background: transparent;
  overflow-y: auto;
}

/**
 * KONTEKST - w którym klubie jest ta sesja (wielofirmowość 2.0.0).
 *
 * Pierwsza rzecz w kolumnie, nad pozycjami modułów: nazwa klubu odpowiada na pytanie
 * „czyj to dziennik", które pada przy każdym wklejonym linku - więc stoi także przy
 * jednym członkostwie. Jest linkiem na wybór klubu (nowa sesja, bez ponownego
 * logowania). Rama superadministratora ma ten sam kafel z innym podpisem
 * (`.sidebar-context.scope`): moduł Organizacje nie jest w żadnym klubie.
 */
.sidebar-context {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 8px 8px 6px;
  border-radius: 8px;
  color: var(--text-primary);
  text-decoration: none;
  min-width: 0;
}

.sidebar-context:hover {
  background: var(--surface-raised);
}

/* Kafel z inicjałami klubu - jak awatar projektu w GitLabie: nie identyfikuje sam
   z siebie, ale daje oku kotwicę, do której wraca przy każdej zmianie ekranu. */
.context-mark {
  width: 32px;
  height: 32px;
  border-radius: 8px;
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  color: var(--text-secondary);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.5px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.context-body {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1;
}

.context-kind {
  font-size: 11px;
  color: var(--text-muted);
  line-height: 1.3;
}

.context-name {
  font-size: 13.5px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 1.3;
}

.context-switch {
  color: var(--text-muted);
  flex-shrink: 0;
}

.sidebar-nav {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-top: 4px;
}

/* Pozycja modułu: ikona + nazwa. Aktywna jest ODWRÓCONA (jasne tło, ciemny napis) -
   ten sam sygnał, którym w tym stylu mówi „wybrane" chip filtra; jeden język
   zaznaczenia w całym panelu. Zielony spód zakładek z 2.0 zniknął razem z zakładkami. */
.nav-item {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 34px;
  padding: 0 10px;
  border-radius: 6px;
  color: var(--text-secondary);
  text-decoration: none;
  font-size: 14px;
}

.nav-item svg {
  flex-shrink: 0;
  color: var(--text-muted);
}

.nav-item:hover {
  background: var(--surface-raised);
  color: var(--text-primary);
}

.nav-item.active {
  background: var(--text-primary);
  color: var(--bg);
  font-weight: 600;
}

.nav-item.active svg {
  color: var(--bg);
}

/* ── okruszki ──
   Wyłącznie tam, gdzie ekran leży POD innym ekranem (dziennik: flota → maszyna →
   operacja). Na liście modułu okruszek opisywałby jedno kliknięcie w kolumnie obok -
   dokładnie to, za co panel 2.0 wyrzucił okruszki z paska. Ostatni człon jest bieżącą
   stroną i nie jest linkiem. */
.crumbs {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  font-size: 13px;
  color: var(--text-secondary);
}

.crumbs a {
  color: var(--text-secondary);
  text-decoration: none;
}

.crumbs a:hover {
  color: var(--text-primary);
  text-decoration: underline;
}

.crumbs .sep {
  color: var(--text-muted);
}

.crumbs .current {
  color: var(--text-primary);
  font-weight: 600;
}

/* Okruszki i nagłówek strony czyta się razem - odstęp między nimi jest mniejszy niż
   rytm reszty kolumny. */
.crumbs + .page-head {
  margin-top: -6px;
}

/* ═══════════════════════════════════════════════════════════════════════════
   05. PRZYCISKI I POLA FORMULARZA
   źródło: admin/src/styles/components/controls.css
   ═══════════════════════════════════════════════════════════════════════════ */

/**
 * Ninerdeck - panel: przyciski i pola formularza.
 *
 * Styl lekki (issue #107): przycisk 32 px z promieniem 6, pole 34 px z promieniem 6,
 * etykieta krojem tekstowym zamiast mono-wersalików. Reguła z aplikacji pilota
 * zostaje: zaznaczenie i fokus są zielone, wybór to lista kart, nigdy natywny `<select>`.
 */

/* ── przyciski ── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 32px;
  padding: 5px 12px;
  border-radius: 6px;
  font-family: var(--font-body);
  font-size: 13.5px;
  font-weight: 500;
  line-height: 1.3;
  border: 1px solid var(--border-strong);
  background: var(--surface-raised);
  color: var(--text-primary);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
}

.btn:hover {
  background: var(--surface-hover);
}

.btn.primary {
  background: var(--green);
  border-color: var(--green);
  color: var(--bg);
  font-weight: 600;
}

.btn.primary:hover {
  background: var(--green-hover);
  border-color: var(--green-hover);
}

/* Czerwona OBRAMÓWKA, wypełnienie dopiero pod kursorem: akcja odcinająca ma być
   widoczna, ale nie ma krzyczeć z każdego wiersza tabeli. */
.btn.danger {
  color: var(--red);
  border-color: var(--red-border);
  background: transparent;
}

.btn.danger:hover {
  background: var(--red-muted);
}

/* Bez tła i bez ramki - trzeciorzędna akcja („Edytuj" w kolumnie tabeli, „Anuluj"
   w stopce szuflady). Jedna taka w wierszu nie robi z listy farmy guzików. */
.btn.ghost {
  background: transparent;
  border-color: transparent;
  color: var(--text-secondary);
}

.btn.ghost:hover {
  background: var(--surface-raised);
  color: var(--text-primary);
}

/* Zielona OBRAMÓWKA - akcja PRZYWRACAJĄCA („Włącz konto"), odwrotność `.btn.danger`
   o tej samej wadze. `.primary` zostaje dla akcji głównej ekranu. */
.btn.ok {
  color: var(--green);
  border-color: var(--green-border);
  background: transparent;
}

.btn.ok:hover {
  background: var(--green-muted);
}

/**
 * Blokada przycisku - ATRYBUT i klasa, oba tak samo: `.btn.disabled` opisuje LINK
 * zablokowany (`<span class="btn disabled">`), `:disabled` - `<button>`. Zielony
 * przycisk, który nie reaguje na kliknięcie, czyta się jak zawieszona aplikacja.
 */
.btn:disabled,
.btn.disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.btn.sm {
  min-height: 28px;
  padding: 3px 10px;
  font-size: 12.5px;
  border-radius: 5px;
}

/* Przycisk na całą szerokość karty (logowanie). */
.btn.block {
  width: 100%;
  min-height: 40px;
}

/* ── pola ── */
.field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* Etykieta krojem tekstowym, półgruba, w kolorze treści - jak w każdym formularzu web.
   Mono-wersaliki 8,5 px z panelu 2.0 czytały się jak etykiety przyrządu, nie pola. */
.label {
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 600;
  color: var(--text-primary);
  line-height: 1.4;
}

.input {
  min-height: 34px;
  border-radius: 6px;
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  padding: 6px 10px;
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 14px;
  outline: none;
  width: 100%;
}

.input:focus {
  border-color: var(--green-border);
  box-shadow: 0 0 0 3px var(--green-muted);
}

.input.mono {
  font-family: var(--font-mono);
  letter-spacing: 0.3px;
}

.input::placeholder {
  color: var(--text-placeholder);
}

/* Pole odrzucone przez serwer. Modyfikator, nie styl inline - stan błędu pola wraca
   na każdym formularzu panelu. */
.input.invalid {
  border-color: var(--red-border);
  box-shadow: 0 0 0 3px var(--red-muted);
}

/**
 * Pole DO ODCZYTU - „Aktualny stan" karty samolotu, gdy maszynę prowadzi już dziennik,
 * i każde inne pole, którego edycja nic by nie zmieniła. Wartość zostaje pełnym
 * kontrastem, bo to jest LICZBA, po którą się tu przyszło; gaśnie POWIERZCHNIA
 * i kursor - pole ma czytać się jak wiersz danych, a nie jak formularz, który przestał
 * odpowiadać. Tło karty (`--surface`) zrównuje je z otoczeniem: nic nie zaprasza
 * do kliknięcia.
 */
.input:disabled {
  background: var(--surface);
  border-color: var(--border);
  color: var(--text-primary);
  cursor: default;
}

/* Pole wieloliniowe (komentarz do odczytu, powód odrzucenia). */
.input.area {
  resize: vertical;
  line-height: 1.5;
  min-height: 92px;
}

.hint {
  font-size: 12.5px;
  color: var(--text-secondary);
  line-height: 1.5;
}

.hint b {
  color: var(--amber);
  font-weight: 600;
}

/**
 * Podpowiedź pod polem w tonie błędu - „ten kod ma już inny pilot". Stoi pod polem,
 * którego dotyczy, a nie w banerze nad formularzem: baner każe szukać, które z czterech
 * pól poprawić, a czerwona ramka bez zdania nie mówi dlaczego.
 */
.hint.danger {
  color: var(--red);
}

/* Pole z przyciskami W JEDNEJ LINII (adres do skopiowania). Przyciski są RODZEŃSTWEM
   pola, bo `<input>` nie ma dzieci; wysokości równa `align-items: stretch`. */
.field-row {
  display: flex;
  gap: 8px;
  align-items: stretch;
}

.field-row .input {
  flex: 1;
}

/* ── wybór jako lista kart ──
   ZAKAZ natywnego `<select>` (`CLAUDE.md`) - wybór to zawsze widoczna lista kart. */
.opt-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* Siatka zamiast kolumny - dwie karty wyboru obok siebie ALBO dwa pola w rzędzie. */
.opt-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 6px;
}

/* `font`, `text-align` i `width` obsługują wariant `<button>`: przeglądarka narzuca
   przyciskowi własną czcionkę i wyśrodkowanie, a karta ma wyglądać identycznie
   niezależnie od znacznika. W panelu wybór JEST przyciskiem, bo `<label>` z mockupu
   nie jest osiągalny z klawiatury. */
.opt {
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  padding: 9px 12px;
  cursor: pointer;
  min-height: 46px;
  text-decoration: none;
  color: inherit;
  font: inherit;
  text-align: left;
  width: 100%;
}

.opt:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.opt:hover {
  background: var(--surface-raised);
}

/* Zaznaczona karta: zielona ramka, a nie zielone tło - kolor ma mówić „ta", nie
   przemalowywać połowy formularza. */
.opt.selected {
  border-color: var(--green);
  box-shadow: 0 0 0 1px var(--green);
}

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

.opt-name {
  font-size: 14px;
  font-weight: 600;
}

/* Druga linia karty mówi o SKUTKU wyboru - krojem tekstowym, drobniej. Mono z 2.0
   zostało przy wartościach maszynowych (`.mono`), a skutek jest zdaniem. */
.opt-desc {
  font-size: 12.5px;
  color: var(--text-secondary);
  line-height: 1.45;
  overflow-wrap: anywhere;
}

.opt-check {
  color: var(--green);
  opacity: 0;
  flex-shrink: 0;
}

.opt.selected .opt-check {
  opacity: 1;
}

/* Szewron pozycji prowadzącej w głąb - zajmuje miejsce `.opt-check`, bo mówi to
   samo („to jest do kliknięcia”), tylko o ruchu, a nie o zaznaczeniu. */
.opt-go {
  color: var(--text-muted);
  flex-shrink: 0;
  display: flex;
}

/**
 * Wiersz sekcji „Dostęp": nazwa rzeczy po lewej, akcja po prawej.
 *
 * Nie tabela klucz-wartość, bo po prawej stronie nie stoi WARTOŚĆ, tylko przycisk -
 * a wiersz, w którym jedno i drugie wygląda tak samo, uczy klikać w liczby.
 */
.access-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 9px 0;
  border-bottom: 1px solid var(--hairline);
}

.access-row:last-child {
  border-bottom: none;
}

/**
 * Wartość maszynowa w zdaniu i w wierszu - e-mail konta Google, stempel zgłoszenia,
 * uuid wklejony w rozmowie. Klasa jest UTYLITĄ jednej właściwości i tylko dlatego
 * wolno jej być utylitą: nie niesie ani koloru, ani rozmiaru.
 */
.mono {
  font-family: var(--font-mono);
  letter-spacing: 0.3px;
}

/* ═══════════════════════════════════════════════════════════════════════════
   06. KARTY, PLAKIETKI, BANERY, STANY PUSTE
   źródło: admin/src/styles/components/surfaces.css
   ═══════════════════════════════════════════════════════════════════════════ */

/**
 * Ninerdeck - panel: karty, plakietki, banery i stany puste.
 *
 * Jeden plik dla całej rodziny „powierzchni i etykiet", bo to jedna warstwa wizualna
 * i jeden zestaw tonów. Styl lekki (issue #107): promień 8 zamiast 14, tytuł karty
 * krojem tekstowym bez zielonej kreski, plakietka bez obramówki i bez wersalików.
 */

/* ── karta ── */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* Tytuł karty nazywa sekcję, jak nagłówek h3 na stronie: półgruby, w kolorze treści,
   podkreślony włosem. Slot `.spacer` odsuwa plakietkę stanu na prawą krawędź. */
.card-title {
  font-family: var(--font-body);
  font-size: 15px;
  font-weight: 600;
  color: var(--text-primary);
  display: flex;
  align-items: center;
  gap: 8px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
}

.card-title .spacer {
  margin-left: auto;
}

/* ── plakietki stanu ──
   Bez ramki i bez wersalików: kolor tła mówi ton, napis mówi stan - pisany jak zdanie,
   z wielkiej litery. Plakietka z obwódką i literami rozstrzelonymi na 1,2 px była
   najgłośniejszym elementem każdego wiersza tabeli. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 9px;
  border-radius: 999px;
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 500;
  line-height: 1.5;
  white-space: nowrap;
}

.pill.green {
  color: var(--green);
  background: var(--green-muted);
}

.pill.amber {
  color: var(--amber);
  background: var(--amber-muted);
}

.pill.red {
  color: var(--red);
  background: var(--red-muted);
}

.pill.blue {
  color: var(--blue);
  background: var(--blue-muted);
}

.pill.dim {
  color: var(--text-secondary);
  background: var(--surface-raised);
}

.pill .dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}

.pill .dot.live {
  animation: pulse 1800ms ease-in-out infinite;
}

@keyframes pulse {
  0%,
  100% {
    box-shadow: 0 0 0 4px var(--green-muted);
    opacity: 1;
  }
  50% {
    box-shadow: 0 0 0 8px transparent;
    opacity: 0.6;
  }
}

/* ── banery: trzy typy z `docs/design-notes.md` + `ok` ── */
.banner {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  border-radius: 8px;
  padding: 11px 14px;
  font-size: 13.5px;
  line-height: 1.5;
  border: 1px solid;
}

.banner svg {
  flex-shrink: 0;
  margin-top: 2px;
}

.banner b {
  font-weight: 600;
}

.banner.status {
  background: var(--blue-muted);
  border-color: var(--blue-border);
  color: var(--text-secondary);
}

.banner.status svg,
.banner.status b {
  color: var(--blue);
}

.banner.warn {
  background: var(--amber-muted);
  border-color: var(--amber-border);
  color: var(--text-secondary);
}

.banner.warn svg,
.banner.warn b {
  color: var(--amber);
}

.banner.danger {
  background: var(--red-muted);
  border-color: var(--red-border);
  color: var(--text-secondary);
}

.banner.danger svg,
.banner.danger b {
  color: var(--red);
}

.banner.ok {
  background: var(--green-muted);
  border-color: var(--green-border);
  color: var(--text-secondary);
}

.banner.ok svg,
.banner.ok b {
  color: var(--green);
}

/* ── stan pusty ── */
.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 48px 20px;
  text-align: center;
}

.empty-icon {
  width: 44px;
  height: 44px;
  border-radius: 10px;
  background: var(--surface-raised);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  margin-bottom: 4px;
}

.empty-title {
  font-family: var(--font-body);
  font-size: 16px;
  font-weight: 600;
  color: var(--text-primary);
}

.empty-note {
  font-size: 13px;
  color: var(--text-secondary);
  max-width: 420px;
  line-height: 1.55;
}

/* ── wiersz klucz/wartość (szczegóły) ──
   Klucz krojem tekstowym, wartość mono - kolumnę liczb czyta się w dół. */
.kv {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
  padding: 7px 0;
  border-bottom: 1px solid var(--hairline);
}

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

.kv-k {
  font-size: 13px;
  color: var(--text-secondary);
  flex-shrink: 0;
}

.kv-v {
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 500;
  text-align: right;
  letter-spacing: 0.3px;
}

.kv-v.green {
  color: var(--green);
}

.kv-v.amber {
  color: var(--amber);
}

.kv-v.red {
  color: var(--red);
}

.kv-v.blue {
  color: var(--blue);
}

.kv-v small {
  font-size: 11px;
  font-weight: 400;
  color: var(--text-secondary);
}

/* Wartość SPRZED zmiany w zestawieniu „przed → po": wygaszona, bo czyta się ją jako
   punkt odniesienia, a nie jako liczbę do zapamiętania. */
.kv-v .was {
  color: var(--text-muted);
  font-weight: 400;
}

/* ══ LISTA SZCZEGÓŁÓW - pola workowatego JSON-a ══
   Osobny komponent od `.kv`: tu wartością bywa całe zdanie, a kluczy jest tyle, ile
   akurat zapisała komenda - dwa tory pozwalają czytać w dół po nazwach pól. */
.det {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.det-row {
  display: grid;
  grid-template-columns: 132px 1fr;
  gap: 10px;
  align-items: baseline;
}

.det-k {
  font-size: 12px;
  color: var(--text-muted);
}

.det-v {
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.55;
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}

/* Pole, którego panel nie umie NAZWAĆ: pokazane z surowym kluczem, wygaszone - ale
   obecne. Rejestr, który ukrywa pole, bo go nie zna, przestaje być narzędziem nadzoru. */
.det-v.raw {
  color: var(--text-muted);
}

.det-empty {
  font-size: 12px;
  color: var(--text-muted);
}

/* Stan „twoja rola tego nie obejmuje". Bliźniak `.empty`, ale osobna klasa: pusty
   ekran i ekran zamknięty to dwie różne wiadomości. */
.no-access {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 56px 20px;
  text-align: center;
}

/* Odwołanie do pliku źródłowego - panel bywa czytany razem z repozytorium. */
.code-ref {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-muted);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 2px 7px;
  letter-spacing: 0.3px;
}

/**
 * Pytanie o potwierdzenie NIEODWRACALNEJ akcji - w miejscu, w którym ją kliknięto.
 * Nie `window.confirm` i nie osobne okno: pytanie ma stać przy przycisku, którego
 * dotyczy. Ton czerwony, bo pyta o odcięcie, a nie o zapis.
 */
.confirm {
  margin-top: 10px;
  padding: 12px;
  border-radius: 8px;
  background: var(--red-muted);
  border: 1px solid var(--red-border);
}

.confirm-q {
  font-size: 14px;
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: 4px;
}

.confirm-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 10px;
}

/**
 * KOD KLUBU - jedyna droga do klubu (`docs/wielofirmowosc.md` §3.8; issue #101, E3).
 *
 * Duży, mono, z odstępem między znakami: administrator odczytuje go z ekranu i DYKTUJE
 * pilotowi, który wpisuje go z klawiatury telefonu. Nie jest sekretem (daje wyłącznie
 * zgłoszenie do rozpatrzenia), więc nie ma „pokaż raz" ani zasłony - stoi jawnie.
 *
 * Do issue #101 klasa czekała w `design/panel/rama.css`; przeszła tu pod TĄ SAMĄ nazwą
 * w chwili, w której panel zaczął ją rysować.
 */
.club-code {
  font-family: var(--font-mono);
  font-size: 24px;
  font-weight: 700;
  letter-spacing: 5px;
  color: var(--text-primary);
  line-height: 1;
}

.club-code-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 12px 14px;
  border-radius: 8px;
  border: 1px solid var(--border-strong);
  background: var(--surface-raised);
}

/* Stan WYŁĄCZONY (klub skasował kod): kreski przygaszone zamiast kodu. */
.club-code.off {
  color: var(--text-muted);
}

/* ═══════════════════════════════════════════════════════════════════════════
   07. NAGŁÓWEK STRONY I KAFLE
   źródło: admin/src/styles/components/page.css
   ═══════════════════════════════════════════════════════════════════════════ */

/**
 * Ninerdeck - panel: nagłówek strony i kafle podsumowań.
 *
 * Styl lekki (issue #107): tytuł strony krojem tekstowym, półgruby, w pisowni
 * zdaniowej („Dziennik", nie „DZIENNIK") - Bebas Neue zostaje w marce i na logowaniu.
 * Nagłówek domyka włos: pod nim zaczyna się treść (zawężenia, tabela), a linia daje
 * oku ten sam punkt startu na każdym ekranie.
 */

/* ── nagłówek strony ── */
.page-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
}

.page-title {
  font-family: var(--font-body);
  font-size: 22px;
  font-weight: 600;
  letter-spacing: -0.2px;
  line-height: 1.25;
}

.page-sub {
  font-size: 13.5px;
  color: var(--text-secondary);
  margin-top: 4px;
  line-height: 1.5;
  max-width: 720px;
}

.page-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}

/* ── kafle ──
   Bez ekranu w panelu 2.0 (kafle z licznikami wypadły razem z przypisami); klasy
   zostają dla modułu, który ich kiedyś zażąda, w tym samym stylu, co reszta. */
.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 12px;
}

.tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  text-decoration: none;
  color: inherit;
}

.tile:hover {
  border-color: var(--border-strong);
}

.tile-key {
  font-size: 12px;
  color: var(--text-secondary);
}

.tile-val {
  font-family: var(--font-mono);
  font-size: 26px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.5px;
}

.tile-val small {
  font-size: 12px;
  font-weight: 400;
  color: var(--text-secondary);
  letter-spacing: 0;
}

.tile-val.green {
  color: var(--green);
}

.tile-val.amber {
  color: var(--amber);
}

.tile-val.red {
  color: var(--red);
}

.tile-val.blue {
  color: var(--blue);
}

.tile-note {
  font-size: 12px;
  color: var(--text-secondary);
  line-height: 1.45;
}

.tile-val.sm {
  font-size: 20px;
}

/* ═══════════════════════════════════════════════════════════════════════════
   08. WYSZUKIWANIE I ZAWĘŻENIA
   źródło: admin/src/styles/components/filters.css
   ═══════════════════════════════════════════════════════════════════════════ */

/**
 * Ninerdeck - panel: pasek nad tabelą - wyszukiwanie i zawężenia.
 *
 * `.search` jest `<label>`, a `.chip` - `<button>`: filtr, którego nie da się
 * ustawić bez myszy, psuje scenariusz „przygotuj link do tego wycinka".
 *
 * Chip WŁĄCZONY jest odwrócony (jasne tło, ciemny napis) - ten sam sygnał, którym
 * kolumna boczna mówi „tu jesteś". Zielony chip z 2.0 znaczył kolorem to, co tutaj
 * znaczy kontrast; zieleń zostaje dla stanu „w normie" i akcji głównej.
 */

.filters {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.search {
  flex: 1;
  min-width: 240px;
  max-width: 360px;
  height: 34px;
  border-radius: 6px;
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 10px;
}

.search:focus-within {
  border-color: var(--green-border);
  box-shadow: 0 0 0 3px var(--green-muted);
}

.search svg {
  color: var(--text-muted);
  flex-shrink: 0;
}

.search input {
  flex: 1;
  background: none;
  border: none;
  outline: none;
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 13.5px;
  min-width: 0;
}

.search input::placeholder {
  color: var(--text-placeholder);
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 30px;
  padding: 3px 12px;
  border-radius: 999px;
  background: transparent;
  border: 1px solid var(--border-strong);
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 500;
  color: var(--text-secondary);
  cursor: pointer;
}

.chip:hover {
  background: var(--surface-raised);
  color: var(--text-primary);
}

.chip.on {
  background: var(--text-primary);
  border-color: var(--text-primary);
  color: var(--bg);
  font-weight: 600;
}

/* ═══════════════════════════════════════════════════════════════════════════
   09. TABELA
   źródło: admin/src/styles/components/table.css
   ═══════════════════════════════════════════════════════════════════════════ */

/**
 * Ninerdeck - panel: TABELA, kręgosłup back-office'u.
 *
 * Reguła, której nie wolno tu zgubić: **liczby zawsze mono i do prawej** (`.num`),
 * bo panel czyta się kolumną, a proporcjonalna czcionka przesuwa cyfry względem siebie.
 *
 * Styl lekki (issue #107): nagłówek tabeli krojem tekstowym w kolorze treści, na tym
 * samym tle, co wiersze (odcień `#101010` z 2.0 zniknął - był jedynym literałem koloru
 * w tym arkuszu); promień 8; wiersze rozdziela włos `--border`.
 */

.table-wrap {
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  background: var(--surface);
}

/* Tabela zagnieżdżona w karcie - bez własnej ramki i tła, bo ramkę niesie karta. */
.table-wrap.plain {
  border: none;
  border-radius: 0;
  background: none;
}

table {
  width: 100%;
  border-collapse: collapse;
}

thead th {
  text-align: left;
  font-family: var(--font-body);
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-primary);
  padding: 10px 12px;
  border-bottom: 1px solid var(--border-strong);
  white-space: nowrap;
}

thead th.sortable {
  /* Padding przechodzi na przycisk w środku, żeby cały obszar nagłówka był celem
     kliknięcia - inaczej klikalna byłaby wyłącznie sama etykieta. */
  padding: 0;
}

/* Nagłówek sortowalny jest PRZYCISKIEM, nie klikalnym `<th>`: komórka z `onClick`
   jest nieosiągalna z klawiatury, a panel jest nią nawigowany. */
thead th.sortable > button {
  width: 100%;
  padding: 10px 12px;
  background: none;
  border: none;
  cursor: pointer;
  font: inherit;
  color: inherit;
  text-align: inherit;
  white-space: nowrap;
}

thead th.sortable > button:hover {
  color: var(--text-secondary);
}

thead th.num.sortable > button {
  text-align: right;
}

thead th .arrow {
  color: var(--text-secondary);
  margin-left: 4px;
}

tbody td {
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  font-size: 13.5px;
  color: var(--text-primary);
  vertical-align: middle;
}

tbody tr:last-child td {
  border-bottom: none;
}

tbody tr:hover {
  background: var(--surface-raised);
}

tbody tr.clickable {
  cursor: pointer;
}

/* Wiersz otwarty w szufladzie - tło i zielona krawędź od strony treści, żeby po
   zamknięciu szuflady wzrok wrócił tam, gdzie był. */
tbody tr.opened {
  background: var(--surface-raised);
  box-shadow: inset 3px 0 0 var(--green);
}

/* Mono dostaje KOMÓRKA z liczbą, nie jej nagłówek: nagłówki kolumn są jednym rzędem
   krojem tekstowym - nagłówek liczbowy w mono odstawał od sąsiadów, choć nie niesie
   żadnej liczby. Wyrównanie do prawej zostaje na obu. */
td.num,
td.mono {
  font-family: var(--font-mono);
  letter-spacing: 0.3px;
}

td.num,
th.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

td.dim {
  color: var(--text-secondary);
}

.cell-strong {
  font-weight: 600;
}

/* Druga linia komórki - KWALIFIKUJE wartość nad sobą (typ maszyny pod znakiem, wiek
   odczytu pod liczbą). Krojem tekstowym; wartość maszynowa dostaje do tego `.mono`. */
.cell-sub {
  display: block;
  /* Jawnie krój tekstowy: podpis w komórce liczbowej (`td.num`) dziedziczyłby mono
     po komórce, a „dolano 60 L" jest zdaniem, nie odczytem. Wartość maszynowa
     w podpisie (sygnatura, e-mail) wraca do mono przez `.cell-sub.mono` niżej. */
  font-family: var(--font-body);
  font-size: 12px;
  line-height: 1.4;
  letter-spacing: 0;
  color: var(--text-secondary);
  margin-top: 2px;
}

.cell-sub.mono {
  font-family: var(--font-mono);
  letter-spacing: 0.3px;
}

/* `.cell-sub` bywa też klasą CAŁEJ komórki (kolumna „E-mail", „Kiedy"): wtedy niesie
   sam ton i stopień, a `display: block` nie ma prawa wyjąć komórki z wiersza. */
td.cell-sub {
  display: table-cell;
  margin-top: 0;
}

/* ŚWIEŻOŚĆ w komórce tabeli - podpis niosący WIEK danych z telefonu. Trzy stany §4.8
   dosłownie: `fresh` = live, `stale` = cache starszy niż doba, `none` = brak danych. */
.cell-sub.fresh {
  color: var(--green);
}

.cell-sub.stale {
  color: var(--amber);
}

.cell-sub.none {
  color: var(--text-muted);
}

/* Wiek sprawy, która leży ZA DŁUGO. */
.cell-age.old {
  color: var(--red);
  font-weight: 700;
}

.reg {
  font-family: var(--font-mono);
  font-weight: 600;
  letter-spacing: 0.6px;
}

/* Link W TREŚCI komórki - zawęża listę do tej wartości. Wygląda jak link strony,
   nie jak przycisk: dwa `.btn` na wiersz w tabeli o sześciu kolumnach zamieniają
   listę w farmę guzików. */
.cell-link {
  color: var(--blue);
  text-decoration: none;
}

.cell-link:hover,
.cell-link:focus-visible {
  text-decoration: underline;
}

.row-actions {
  display: flex;
  gap: 6px;
  justify-content: flex-end;
}

/* WARTOŚĆ ZEGARA w komórce tabeli - trzy tony, trzy różne zdania: `dim` - zegary się
   zgadzają, `amber` - rozjazd ponad próg, `red` - nie ma czego porównać. */
.clock-val.amber {
  color: var(--amber);
  font-weight: 700;
}

.clock-val.red {
  color: var(--red);
}

.clock-val.dim {
  color: var(--text-secondary);
}

/* Wartość zegara, którą KOREKTA przestawiła: przekreślenie znaczy „to już nie jest
   liczba, którą liczy projekcja"; nowy czas stoi pod spodem jako `.cell-sub`.
   Reguła stoi PO tonach, żeby wygaszenie wygrywało z czerwienią „brak fixa". */
.clock-val.struck {
  text-decoration: line-through;
  color: var(--text-muted);
}

/* Zdarzenie UNIEWAŻNIONE korektą. Wiersz ZOSTAJE w tabeli - rejestr jest append-only.
   Przekreślamy czasy i identyfikatory, nie całą treść. */
tbody tr.voided td.num,
tbody tr.voided td.mono {
  text-decoration: line-through;
  color: var(--text-muted);
}

/* ROZWINIĘCIE WIERSZA - wiersz-satelita pod wierszem właściwym, przez całą szerokość
   tabeli. Reguła stoi PO `tbody tr:hover`, żeby najechanie myszą nie podświetlało
   satelity jak osobnego wiersza listy. */
tbody tr.row-expand:hover {
  background: none;
}

tbody tr.row-expand > td {
  padding: 0 12px 16px;
  background: var(--surface-raised);
}

/* Wiersz z OTWARTĄ FLAGĄ. Reguła stoi PO `tbody tr:hover`, żeby najechanie myszą nie
   zdejmowało bursztynu z wiersza, który jest wyróżniony z powodu. */
tbody tr.flagged,
tbody tr.flagged:hover {
  background: var(--amber-muted);
}

/**
 * Wiersz PRZYGASZONY - konto bez dostępu, samolot poza służbą.
 *
 * Sam kolor, bez przekreśleń i bez ikony: to nadal prawdziwy wiersz, który da się
 * otworzyć i poprawić. Serwer stawia takie wiersze na końcu listy, więc przygaszenie
 * mówi „to już nie pracuje", a nie „to jest błąd".
 */
tbody tr.muted td {
  color: var(--text-muted);
}

tbody tr.muted .reg,
tbody tr.muted .cell-strong {
  color: var(--text-secondary);
}

/* Druga linia komórki w tonie ostrzeżenia - jednostka wyłączona, na której ktoś
   jeszcze lata. Jedyne miejsce w panelu, gdzie wiersz mówi coś ponad dane. */
.cell-sub.warn {
  color: var(--amber);
}

/* ═══════════════════════════════════════════════════════════════════════════
   10. SZUFLADA SZCZEGÓŁU
   źródło: admin/src/styles/components/drawer.css
   ═══════════════════════════════════════════════════════════════════════════ */

/**
 * Ninerdeck - panel: szuflada szczegółu.
 *
 * **Pozycjonowanie `fixed`, nie `absolute`.** Ekran renderuje się WEWNĄTRZ `.content`,
 * które ma `overflow-y:auto` - szuflada `absolute` przewijałaby się razem z listą.
 * `fixed` daje warstwę nad całym oknem i nie zależy od przewinięcia.
 *
 * **Przesłona zaczyna się od LEWEJ KRAWĘDZI OKNA** - także nad kolumną boczną (styl
 * lekki, issue #107). Szuflada jest oknem dialogowym nad całym panelem: kolumna
 * z aktywną pozycją pod półprzezroczystą przesłoną dalej mówi, gdzie jesteśmy,
 * a przesłona omijająca kolumnę zapraszałaby do kliknięcia w moduł z otwartym
 * i niezapisanym formularzem.
 *
 * Styl lekki: tytuł krojem tekstowym w pisowni zdaniowej („Anna Kowal", nie
 * „ANNA KOWAL"), cień lżejszy, przycisk zamknięcia bez ramki, tło w tonie warstwy
 * treści (`--surface`).
 */

.drawer-scrim {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  /* NIE `var(--overlay)`: token niesie 0,74 (przyciemnienie modala w aplikacji
     pilota), a szuflada panelu przyciemnia listę słabiej, bo lista pod spodem ma
     dalej być czytelna. To jest sens szuflady: kontekst zostaje. */
  background: rgba(0, 0, 0, 0.5);
  z-index: 60;
  /* Przesłona jest `<button>` (kliknięcie w tło to akcja), więc zdejmujemy z niej
     wygląd przycisku - zostaje sama powierzchnia. */
  border: none;
  padding: 0;
  cursor: pointer;
}

.drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: 520px;
  /* Ten sam ton, co warstwa treści - szuflada jest jej częścią wysuniętą nad listę,
     nie kawałkiem chrome'u. */
  background: var(--surface);
  border-left: 1px solid var(--border);
  z-index: 61;
  display: flex;
  flex-direction: column;
  box-shadow: -8px 0 32px rgba(0, 0, 0, 0.45);
}

/* Szuflada SZERSZA - dla szuflady, która jest formularzem, a nie podglądem. */
.drawer.wide {
  width: 660px;
}

.drawer-head {
  flex-shrink: 0;
  padding: 14px 20px;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
}

.drawer-title {
  font-family: var(--font-body);
  font-size: 18px;
  font-weight: 600;
  line-height: 1.3;
}

.drawer-sub {
  font-size: 12.5px;
  color: var(--text-secondary);
  margin-top: 2px;
  line-height: 1.5;
}

.drawer-body {
  flex: 1;
  overflow-y: auto;
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.drawer-foot {
  flex-shrink: 0;
  padding: 12px 20px;
  border-top: 1px solid var(--border);
  display: flex;
  gap: 8px;
  justify-content: flex-end;
}

/* Notatka po lewej stronie akcji stopki - mówi, czego to okno NIE zrobi. */
.drawer-note {
  margin-right: auto;
  align-self: center;
  font-size: 12px;
  color: var(--text-muted);
}

.x-btn {
  width: 30px;
  height: 30px;
  border-radius: 6px;
  border: none;
  background: transparent;
  color: var(--text-secondary);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
}

.x-btn:hover {
  background: var(--surface-raised);
  color: var(--text-primary);
}

/* ═══════════════════════════════════════════════════════════════════════════
   11. PLAMKI ŁADOWANIA
   źródło: admin/src/styles/components/skeleton.css
   ═══════════════════════════════════════════════════════════════════════════ */

/**
 * Ninerdeck - panel: PLAMKI ŁADOWANIA.
 *
 * Reguła produktu (CLAUDE.md, issue #33): ekran czekający na odczyt rysuje plamki
 * W GEOMETRII DOCELOWEJ - nigdy spinnera i nigdy pustki.
 *
 * Puls przezroczystości, nie shimmer: gradient wędrujący pod maską to druga animacja
 * i drugi powód do przemalowania - a plamka ma być tłem, nie przedstawieniem.
 */

.skeleton {
  background: var(--surface-raised);
  border-radius: 4px;
  animation: skeleton-pulse 1.25s ease-in-out infinite;
}

/* Wszystkie plamki ekranu pulsują RAZEM (wspólny czas trwania i faza z animacji CSS),
   bo rozjechane fazy czytają się jak kilka niezależnych rzeczy, które się psują. */
@keyframes skeleton-pulse {
  0%,
  100% {
    opacity: 0.55;
  }
  50% {
    opacity: 0.95;
  }
}

/* Plamka w komórce tabeli - wysokość wiersza tekstu, szerokość z atrybutu `style`,
   bo różni się kolumna po kolumnie i jest wymiarem układu, nie tokenem. */
.skeleton.cell {
  height: 12px;
}

@media (prefers-reduced-motion: reduce) {
  .skeleton {
    animation: none;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   12. EKRAN LOGOWANIA
   źródło: admin/src/styles/components/login.css
   ═══════════════════════════════════════════════════════════════════════════ */

/**
 * Ninerdeck - panel: EKRAN LOGOWANIA.
 *
 * Osobny arkusz, bo to jedyny ekran panelu, który nie jest tabelą ani formularzem
 * w szufladzie - i jedyny poza znakiem w pasku, na którym marka ma prawo być duża.
 *
 * Styl lekki (issue #107): bez poświaty pod znakiem; tłem jest to samo tło, co pod
 * chrome'em panelu (kolor i lekki gradient z `base.css`) - logowanie nie ma własnego
 * gradientu, jak w 2.0, tylko przepuszcza ten jeden. Znak na zielonej plamie, karta
 * na `--surface` z włosem, tak jak każda inna karta.
 */

.login {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 28px;
  padding: 40px 24px 64px;
  overflow-y: auto;
}

.login-mark {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}

.login-badge {
  width: 60px;
  height: 60px;
  border-radius: 16px;
  background: var(--green-muted);
  color: var(--green);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.login-title {
  font-family: var(--font-display);
  font-size: 40px;
  letter-spacing: 6px;
  /* Bebas Neue ma duży odstęp wewnętrzny; bez tego podpis odkleja się od nazwy. */
  line-height: 0.9;
  color: var(--text-primary);
  /* Wersaliki mają odstęp PO ostatniej literze, więc blok wygląda na przesunięty
     w lewo. Wcięcie o połowę odstępu wyrównuje go optycznie do środka. */
  text-indent: 6px;
}

.login-note {
  font-size: 13px;
  color: var(--text-secondary);
}

/**
 * Karta formularza. Własna, nie `.card`: tam padding jest dobrany do gęstej treści
 * back-office'u, a tu ma być powietrze.
 */
.login-card {
  width: 100%;
  max-width: 400px;
  padding: 28px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: var(--surface);
}

/* Odstęp między polami trzyma FORMULARZ, nie karta: karta ma jedno dziecko, więc
   `gap` na niej nie miałby czego rozsunąć. */
.login-card form {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

/* Pola logowania są WYŻSZE niż w reszcie panelu: na ekranie są dwa, więc nie ma
   czego upychać, a 44 px to wygodny cel także na tablecie w hangarze. */
.login-card .input {
  min-height: 44px;
  border-radius: 8px;
  padding: 10px 14px;
  font-size: 14px;
}

.login-card .field {
  gap: 8px;
}

.login-card .btn {
  min-height: 44px;
  border-radius: 8px;
  font-size: 15px;
  margin-top: 4px;
}

/* Baner błędu trzyma szerokość karty, żeby kolumna miała jedną krawędź. */
.login-banner {
  width: 100%;
  max-width: 400px;
}

/**
 * Przycisk Google. Rysuje go SKRYPT GOOGLE w tym kontenerze (`auth/googleIdentity.ts`),
 * więc ostylować da się tylko ramę wokół niego: wyśrodkowanie i wysokość, żeby karta
 * nie skakała, gdy przycisk wjeżdża. Dopóki nie ma go w drzewie (`pending`), kontener
 * nie zajmuje miejsca - miejsce trzyma plamka o jego geometrii.
 */
.login-google {
  display: flex;
  justify-content: center;
  min-height: 44px;
}

.login-google.pending {
  min-height: 0;
}

/* Plamka w geometrii przycisku Google (44 px, pigułka) - nigdy pustka, nigdy spinner. */
.login-google-skeleton {
  height: 44px;
  max-width: 336px;
  margin: 0 auto;
  border-radius: 22px;
  background: var(--surface-raised);
  border: 1px solid var(--border);
}

.login-status {
  margin: 14px 0 0;
  text-align: center;
  font-size: 13px;
  color: var(--text-muted);
}

/* ═══════════════════════════════════════════════════════════════════════════
   13. DZIENNIK - PARY, BRAKI, ZAKRES DAT
   źródło: admin/src/styles/components/logbook.css
   ═══════════════════════════════════════════════════════════════════════════ */

/**
 * Ninerdeck - panel: DZIENNIK - komórki par, braki i kontrolka zakresu dat.
 *
 * Trzy rzeczy, których reszta panelu nie potrzebowała: para wartości w jednej komórce,
 * ton braku i pola daty. Wszystko inne (tabela, karta, plakietka, chip) grid bierze
 * z istniejącego inwentarza.
 */

/* Para „08:42 → 10:22" w jednej linii. Strzałka jest WYGASZONA, bo nie jest wartością
   - to znak relacji między dwiema, które nią są. */
.cell-pair {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--text-primary);
  white-space: nowrap;
}

.cell-arrow {
  color: var(--text-muted);
  padding: 0 5px;
}

/* Wejście w głąb z wiersza poziomu 1 - sam znak, bo cały wiersz jest klikalny. */
.cell-go {
  color: var(--text-muted);
  font-size: 16px;
}

/* Wiersz UNIEWAŻNIONY: zostaje w tabeli, bo rejestr jest append-only, a znikający
   wiersz byłby kłamstwem o dniu. Przekreślenie mówi, że fakt został cofnięty. */
tbody tr.voided td {
  color: var(--text-muted);
  text-decoration: line-through;
  text-decoration-color: var(--red-border);
}

/* Podpis pod tabelą przyciętą limitem - lista ucięta po cichu wygląda jak komplet. */
.list-foot {
  font-size: 13px;
  color: var(--text-secondary);
  padding: 4px 2px;
}

/**
 * Pole daty. Natywne `input[type=date]` - panel jest stroną, więc kalendarz ma od
 * przeglądarki; `color-scheme` przestawia jej własny widżet na ciemny. Wygląd jak
 * `.input` i `.search`: ta sama wysokość, ramka i fokus.
 */
.daterange {
  display: inline-flex;
  align-items: center;
  height: 34px;
  border-radius: 6px;
  background: var(--surface-raised);
  border: 1px solid var(--border-strong);
  padding: 0 8px;
}

.daterange:focus-within {
  border-color: var(--green-border);
  box-shadow: 0 0 0 3px var(--green-muted);
}

.daterange input {
  color-scheme: dark;
  background: none;
  border: none;
  outline: none;
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-size: 13px;
}

.daterange-sep {
  color: var(--text-muted);
  font-family: var(--font-mono);
}

/* ═══════════════════════════════════════════════════════════════════════════
   14. ŚLAD GPS - MAPA I PROFIL PIONOWY
   źródło: admin/src/styles/components/track.css
   ═══════════════════════════════════════════════════════════════════════════ */

/**
 * Ninerdeck - panel: mapa śladu i profil pionowy (ekran operacji, poziom 3).
 *
 * Kolory wyłącznie z tokenów. Płótno mapy stoi na `--bg` - ciemniejszym od karty
 * (`--surface`), żeby ślad miał na czym leżeć; literał `#0f1216` z panelu 2.0 zniknął
 * przy stylu lekkim (issue #107) razem z ostatnim literałem koloru w arkuszach.
 */

/* Wysokości TU NIE MA: proporcje płótna niosą stałe `viewBox` (`aspect-ratio`
   w atrybucie `style`), bo tylko wtedy skala jest jedna dla obu osi. */
.map-canvas {
  position: relative;
  background: var(--bg);
  border-radius: 8px;
  overflow: hidden;
  border: 1px solid var(--border);
}
.map-canvas svg {
  display: block;
  width: 100%;
  height: 100%;
}
.map-overlay {
  position: absolute;
  inset: 0;
}

/* Siatka współrzędnych - podkład, który zastąpił kafelki (decyzja 2026-08-04). */
.map-grid line {
  stroke: var(--border);
  stroke-width: 1;
}

/* Pas startowy lotniska: jedyny szczegół terenu, jaki rysujemy. */
.map-runway {
  fill: var(--border-strong);
}
.map-airfield-dot {
  fill: var(--border-strong);
}

.map-scale {
  position: absolute;
  left: 12px;
  bottom: 9px;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.map-scale-bar {
  height: 5px;
  border: 1px solid var(--text-secondary);
  border-top: none;
}
.map-scale-label {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-secondary);
  letter-spacing: 0.5px;
}

/* Atrybucja katalogu lotnisk: licencja ODbL wymaga podpisu przy publicznym użyciu
   danych OSM, więc stoi przy mapie, a nie w odległej stopce. */
.map-attrib {
  position: absolute;
  right: 8px;
  bottom: 7px;
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-muted);
  background: var(--surface);
  border: 1px solid var(--border);
  padding: 2px 7px;
  border-radius: 5px;
  letter-spacing: 0.3px;
}

/* Legenda opisuje RODZAJE znaczników, więc ma stałe wiersze niezależnie od tego,
   ile lotów miała operacja - patrz nagłówek `TrackMap.tsx`. */
.map-legend {
  position: absolute;
  left: 12px;
  top: 12px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 10px;
}
.legend-row {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 12px;
  color: var(--text-secondary);
}
.legend-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}
.legend-dot.line {
  width: 14px;
  height: 2.5px;
  border-radius: 2px;
}
/* Próbka kołowania (issue #75 pkt 4): przerywana szara, jak kreska na mapie. */
.legend-dash {
  width: 14px;
  height: 0;
  border-top: 2px dashed var(--text-muted);
  flex-shrink: 0;
}

/* Profil pionowy - ten sam wykres co w aplikacji, na szerokości panelu. `svg`
   w normalnym przepływie, więc `height: auto` bierze proporcję wprost z `viewBox`. */
.profile-chart {
  position: relative;
  padding: 14px 16px 6px;
}
.profile-chart svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}
.axis-label {
  font-family: var(--font-mono);
  font-size: 9px;
  fill: var(--text-muted);
  letter-spacing: 0.5px;
}
.grid-line {
  stroke: var(--border);
  stroke-width: 1;
}
.grid-line.dashed {
  stroke-dasharray: 2 4;
}

/* Liczby pod mapą: dystans, pułap, prędkość. Trzy, nie trzynaście - reszta statystyk
   jest materiałem do strojenia progów, nie odpowiedzią na pytanie z tego ekranu. */
.track-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 28px;
  padding: 12px 2px 2px;
}
.track-fact {
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.track-fact-k {
  font-size: 12px;
  color: var(--text-secondary);
}
.track-fact-v {
  font-family: var(--font-mono);
  font-size: 15px;
  color: var(--text-primary);
}

/* Podpis pod profilem - wysokość jest GPS-owa i to trzeba powiedzieć. */
.profile-foot {
  padding: 6px 16px 12px;
  font-size: 12.5px;
  color: var(--text-muted);
}

/* ═══════════════════════════════════════════════════════════════════════════
   15. ZGŁOSZENIA BŁĘDÓW
   źródło: admin/src/styles/components/bugs.css
   ═══════════════════════════════════════════════════════════════════════════ */

/**
 * Ninerdeck - panel: moduł „Zgłoszenia" (issue #87).
 *
 * Trzy klasy, bo tyle brakowało bibliotece - reszta ekranu jedzie na tym, co już jest
 * (`.card`, `.kv`, `.opt-list`, `.filters`, tabela). Arkusz istnieje osobno, a nie
 * w `surfaces.css`, z tego samego powodu, co katalog `components/bug/` w aplikacji:
 * narzędzie fazy testów ma dać się usunąć decyzją, nie archeologią.
 */

/**
 * Opis zgłoszenia - jedyne miejsce w panelu, gdzie tekst przyszedł od CZŁOWIEKA
 * i ma łamania linii, które coś znaczą (kroki odtworzenia bywają listą).
 * `pre-wrap` je zachowuje, `break-word` broni układu przed wklejonym uuid-em.
 */
.bug-text {
  font-size: 14px;
  line-height: 1.6;
  color: var(--text-primary);
  white-space: pre-wrap;
  overflow-wrap: break-word;
}

/**
 * Wartości kontekstu bywają długie (ścieżki, JSON dopisany przez aplikację), a kolumna
 * `.kv-v` jest wąska. Łamiemy je zamiast rozpychać szufladę - to jest lista do
 * przeczytania, nie tabela do porównywania.
 */
.bug-context .kv-v {
  overflow-wrap: anywhere;
  text-align: right;
}

/*
 * LICZBY PRZY FILTRACH nie mają tu własnej klasy i to jest decyzja: wchodzą
 * w ETYKIETĘ chipa („Nowe · 7"), więc `FilterChip` zostaje nietknięty. Przy triażu
 * „ile jest nowych" jest najważniejszą liczbą na stronie.
 */

/* ══════════════════════════════════════════════════════════════════════════════
   RAMA MAKIETY - KANWA, OKNO PRZEGLĄDARKI, INWENTARZ, SPIS
   ══════════════════════════════════════════════════════════════════════════════

   WSZYSTKO W TYM PLIKU ISTNIEJE WYŁĄCZNIE W `design/panel/*.html`.
   Panel jest CAŁĄ stroną w przeglądarce, więc nie ma ani kanwy, ani obrazka okna,
   ani inwentarza komponentów - i nie ma ich mieć. Przeniesienie którejkolwiek z tych
   klas do `admin/src/styles/` jest błędem, a nie porządkowaniem.

   Plik jest DRUGĄ POŁOWĄ `panel.css`: generator (`npm run panel:css` w `admin/`)
   skleja arkusze panelu z `admin/src/styles/` i dopisuje ten plik na końcu. Makieta
   linkuje wyłącznie `panel.css`; tu się EDYTUJE, tam się nie dotyka.

   Rama okna działa dokładnie tak, jak `--phone-scale` w makietach telefonu.
   `.shell` jest KOLUMNĄ (pasek górny nad `.workspace`) - odpowiednikiem `#root`
   z `base.css`; kolumnę boczną i treść układa `.workspace` z `layout.css`.
   ══════════════════════════════════════════════════════════════════════════════ */

/* ── kanwa ──
   `base.css` maluje `body` na chrome panelu, bo tam `body` JEST przeglądarką. Tutaj
   `body` jest KANWĄ wokół obrazka okna, więc dostaje gradienty i układ kolumnowy.
   `height:100%` z `base.css` też schodzi: kanwa rośnie z inwentarzem. */
html,
body {
  height: auto;
  min-height: 100%;
}

body {
  /* Dokładnie ten sam trik, co `--phone-scale` w makietach telefonu. */
  --app-scale: min(1, calc((100vh - 250px) / 900px), calc((100vw - 60px) / 1440px));
  background-image:
    radial-gradient(ellipse 80% 50% at 50% 0%, var(--green-muted), transparent 60%),
    linear-gradient(180deg, var(--bg-tint) 0%, var(--bg) 30%);
  background-attachment: fixed;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 44px 24px 80px;
  gap: 26px;
}

/* ── etykieta ekranu na kanwie ── */
.canvas-label {
  width: 100%;
  max-width: 1440px;
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: baseline;
  gap: 20px;
  padding-bottom: 18px;
  border-bottom: 1px solid var(--border);
}

.film-strip {
  font-family: var(--font-display);
  font-size: 34px;
  letter-spacing: 3px;
  line-height: 1;
}

.slug {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-muted);
  letter-spacing: 1.5px;
  text-transform: uppercase;
  border-left: 1px solid var(--border);
  padding-left: 18px;
}

.canvas-meta {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-secondary);
  letter-spacing: 1px;
  display: inline-flex;
  align-items: center;
  gap: 10px;
}

/* Kropka bierze `@keyframes pulse` z `surfaces.css` - ta sama animacja, co w `.pill
   .dot.live`, co do wartości. */
.canvas-meta .pulse {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--green);
  box-shadow: 0 0 0 4px var(--green-muted);
  animation: pulse 1800ms ease-in-out infinite;
}

/* ── panel „Warianty tego ekranu" ──
   Reguła z `CLAUDE.md`: ekran mający warianty dostaje na kanwie panel z linkami do
   całej rodziny I OPISEM, kiedy który wariant się wyświetla. */
.variants-panel {
  width: 100%;
  max-width: 1440px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px 12px 10px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 7px;
}

.vp-title {
  grid-column: 1 / -1;
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--text-muted);
  padding: 0 2px 2px;
}

.vp-item {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 9px 11px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--surface-raised);
  text-decoration: none;
  color: var(--text-primary);
}

.vp-item:hover {
  border-color: var(--green-border);
}

.vp-item.active {
  border-color: var(--green-border);
  background: rgba(46, 204, 113, 0.05);
  pointer-events: none;
}

.vp-badge {
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 1px;
  color: var(--green);
  background: var(--green-muted);
  border: 1px solid var(--green-border);
  border-radius: 6px;
  padding: 3px 7px;
  flex-shrink: 0;
}

.vp-badge.amber {
  color: var(--amber);
  background: var(--amber-muted);
  border-color: var(--amber-border);
}

.vp-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
}

.vp-name {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-primary);
}

.vp-when {
  font-size: 11px;
  color: var(--text-secondary);
  line-height: 1.45;
}

.vp-tag {
  font-family: var(--font-mono);
  font-size: 8px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--green);
  margin-top: 3px;
  white-space: nowrap;
}

/* ── pasek nawigacji między makietami ── */
.nav-strip {
  width: 100%;
  max-width: 1440px;
  display: flex;
  gap: 12px;
  justify-content: center;
  flex-wrap: wrap;
  font-family: var(--font-mono);
  font-size: 11px;
}

.nav-strip a {
  color: var(--text-muted);
  text-decoration: none;
  padding: 4px 8px;
  border-radius: 5px;
}

.nav-strip a:hover,
.nav-strip a.active {
  color: var(--green);
}

.nav-strip span {
  color: var(--border-strong);
}

/* ══ OKNO PRZEGLĄDARKI - desktopowy odpowiednik `.phone` ══
   `transform: scale()` robi tu DWIE rzeczy naraz i druga jest niewidoczna, dopóki
   się nie zepsuje: skaluje makietę do okna ORAZ czyni `.browser` blokiem
   zawierającym dla potomków `position: fixed`. Dzięki temu szuflada (`.drawer`,
   `.drawer-scrim`) - w panelu przypięta do okna przeglądarki - w makiecie przypina
   się do RAMY, bez zmiany ani jednej właściwości w `drawer.css`. */
.browser {
  width: 1440px;
  height: 900px;
  border-radius: 12px;
  /* Tło okna = chrome panelu (`body` w `base.css`): kolor i lekki gradient. */
  background-color: var(--bg);
  background-image: var(--chrome-bg);
  /* Chrome okna: odcienie opisują OBRAZEK przeglądarki, którego panel nie ma. */
  border: 1px solid #1c1e24;
  box-shadow:
    0 0 0 1px rgba(255, 255, 255, 0.03),
    0 40px 120px rgba(0, 0, 0, 0.7),
    0 24px 48px rgba(0, 0, 0, 0.45);
  overflow: hidden;
  position: relative;
  isolation: isolate;
  flex-shrink: 0;
  transform: scale(var(--app-scale));
  transform-origin: top center;
  margin-bottom: calc((var(--app-scale) - 1) * 900px);
  display: flex;
  flex-direction: column;
}

.chrome {
  height: 40px;
  flex-shrink: 0;
  background: #0a0a0c;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 0 14px;
}

.chrome-dots {
  display: flex;
  gap: 7px;
  flex-shrink: 0;
}

.chrome-dots i {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: var(--border-strong);
}

.chrome-url {
  flex: 1;
  max-width: 460px;
  height: 24px;
  border-radius: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 0 10px;
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-muted);
  letter-spacing: 0.5px;
}

.chrome-url svg {
  color: var(--green);
  flex-shrink: 0;
}

.chrome-url b {
  color: var(--text-secondary);
  font-weight: 400;
}

/* Wnętrze okna = pasek górny + `.workspace` (kolumna boczna i treść). Odpowiednik
   `#root` z `base.css`. */
.shell {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* SZUFLADA W RAMIE MAKIETY. `.drawer` i `.drawer-scrim` są `position: fixed`, więc
     blokiem zawierającym jest najbliższy przodek z transformem - czyli `.browser`,
     a warstwa przykrywałaby pasek chrome przeglądarki, którego panel nie ma. Własny
     kontekst na `.shell` przesuwa granicę dokładnie tam, gdzie w panelu leży sama
     z siebie (`top: 0` = góra OKNA). */
  transform: translateZ(0);
}

/* ══ INWENTARZ KOMPONENTÓW ══
   Stoi POD ramą, na kanwie: rama pokazuje ekran, inwentarz pokazuje CZĘŚCI, z których
   się go składa. */
.canvas-section {
  width: 100%;
  max-width: 1440px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.canvas-section-title {
  font-family: var(--font-display);
  font-size: 24px;
  letter-spacing: 3px;
  text-transform: uppercase;
  color: var(--text-muted);
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: 12px;
}

.canvas-section-title::before {
  content: '';
  width: 4px;
  height: 22px;
  border-radius: 2px;
  background: var(--green);
  flex-shrink: 0;
}

.inv-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(440px, 1fr));
  gap: 14px;
  align-items: start;
}

.inv {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* Pozycja na całą szerokość siatki - tabela i szuflada nie mieszczą się w połowie. */
.inv.full {
  grid-column: 1 / -1;
}

.inv-head {
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.inv-name {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--text-primary);
}

.inv-when {
  font-size: 12px;
  color: var(--text-secondary);
  line-height: 1.5;
}

.inv-classes {
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: 0.5px;
  color: var(--text-muted);
  line-height: 1.6;
  overflow-wrap: anywhere;
}

.inv-classes b {
  color: var(--green);
  font-weight: 400;
}

/* Podłoże demonstracji jest TŁEM WARSTWY TREŚCI panelu (`--surface`), żeby komponent
   wyglądał dokładnie tak, jak w ekranie: karta i tabela na tym tle rysują się samą
   ramką, i tak ma być. Włos oddziela demonstrację od nagłówka pozycji. */
.inv-demo {
  background: var(--surface);
  border-top: 1px solid var(--border);
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* Demonstracja RAMY (pasek i kolumna) stoi na chrome'ie (`--bg`), nie na warstwie
   treści - inaczej kolumna zlewałaby się z podłożem, a panel treści nie miałby
   się od czego odciąć. */
.inv-demo.chrome-demo {
  background: var(--bg);
  padding: 0;
}

.inv-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 9px;
}

/* Podpis stanu W demonstracji - „tak wygląda wiersz wyłączony". Mono i drobny,
   żeby nie czytał się jak treść komponentu. */
.inv-mark {
  font-family: var(--font-mono);
  font-size: 8.5px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* Szuflada w inwentarzu. `transform` robi to samo, co w `.browser`: zamienia
   `position: fixed` szuflady w pozycjonowanie WZGLĘDEM TEGO PUDEŁKA. */
.drawer-demo {
  position: relative;
  height: 430px;
  overflow: hidden;
  transform: translateZ(0);
  background: var(--surface);
}

.drawer-demo .drawer {
  width: 460px;
}

/* Kolumna boczna w inwentarzu - pudełko o wysokości kilku pozycji, nie całego okna. */
.sidebar-demo {
  display: flex;
  height: 320px;
}

.sidebar-demo .sidebar {
  height: 100%;
}

.sidebar-demo .content {
  flex: 1;
  padding: 20px 24px;
}

/**
 * PRZYCISK GOOGLE - ATRAPA WYŁĄCZNIE DLA MAKIETY.
 *
 * W panelu ten przycisk rysuje SKRYPT GOOGLE we wnętrzu `.login-google`, więc
 * `admin/src/styles/` nie ma dla niego ani jednej reguły i mieć nie może. Bez atrapy
 * makieta jedynego ekranu logowania pokazywałaby szarą plamkę w miejscu jedynej
 * akcji, jaką ten ekran ma.
 *
 * KOLORY SĄ GOOGLE'A i to jedyne miejsce w arkuszu z wartością spoza palety -
 * znaku towarowego nie wolno przemalować na tokeny marki. Geometria trzyma się
 * plamki (`.login-google-skeleton`): 44 px i pigułka.
 */
.login-google-mock {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  height: 44px;
  width: 100%;
  max-width: 336px;
  padding: 0 20px;
  border: 1px solid #747775;
  border-radius: 22px;
  background: #ffffff;
  color: #1f1f1f;
  font-family: 'Roboto', var(--font-body);
  font-size: 15px;
  font-weight: 500;
  letter-spacing: 0.15px;
  cursor: pointer;
}

/* ══ SPIS MAKIET (`design/panel/index.html`) ══
   Prefiks `idx-`, bo karta spisu jest INNYM bytem niż `.card` panelu. */
.idx-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 8px 0 18px;
}

.idx-hero h1 {
  font-family: var(--font-display);
  font-size: 46px;
  letter-spacing: 5px;
  line-height: 1;
  text-align: center;
}

.idx-hero h1 span {
  color: var(--green);
}

.idx-hero-sub {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 2.5px;
  text-transform: uppercase;
  color: var(--text-muted);
  text-align: center;
}

/* Akapit o decyzjach - stoi RAZ, na górze spisu, bo dotyczy całego katalogu. */
.idx-lead {
  width: 100%;
  max-width: 1000px;
  font-size: 13px;
  line-height: 1.7;
  color: var(--text-secondary);
}

.idx-lead b {
  color: var(--text-primary);
  font-weight: 600;
}

.idx-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 14px;
}

.idx-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 16px 18px;
  text-decoration: none;
  color: var(--text-primary);
  display: flex;
  align-items: flex-start;
  gap: 13px;
}

.idx-card:hover {
  background: var(--surface-hover);
  border-color: var(--green-border);
}

/* Makieta, której jeszcze nie ma. Przygaszona i BEZ reakcji na kursor. */
.idx-card.soon {
  opacity: 0.5;
  pointer-events: none;
}

.idx-badge {
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 1px;
  color: var(--green);
  background: var(--green-muted);
  border: 1px solid var(--green-border);
  border-radius: 8px;
  min-width: 34px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 7px;
  flex-shrink: 0;
  margin-top: 1px;
}

.idx-card.soon .idx-badge {
  color: var(--text-muted);
  background: var(--surface-raised);
  border-color: var(--border);
}

.idx-body {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

.idx-name {
  font-family: var(--font-mono);
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: 0.5px;
}

/* Adres w panelu - po nim się ten ekran znajduje i o nim się rozmawia. */
.idx-route {
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: 0.8px;
  color: var(--green);
}

.idx-card.soon .idx-route {
  color: var(--text-muted);
}

.idx-when {
  font-size: 12px;
  color: var(--text-secondary);
  line-height: 1.55;
}
