        :root {
            color-scheme: light dark;
            --bg: #f5f6f8;
            --text: #111827;   /* było #1a1a1a — neutralna czerń spoza reszty palety; #111827 to ta sama rodzina co --muted (#6b7280) i --border (#e5e7eb), zgodnie z makietą (21.09.2026). Motyw ciemny bez zmian. */
            --card-bg: #fff;
            /* --radius-card/--radius-field: tokeny z pliku referencyjnego (21.09.2026,
               po pełnym porównaniu). Dotyka każdej karty i każdego pola/przycisku w
               aplikacji — jedna zmiana wartości, nie osobne "8px"/"4px"/"6px" rozsiane
               po arkuszu. --card-shadow ZOSTAJE jako token (menu rozwijane, okno modalne
               nadal go używają — cień tam niesie znaczenie warstwy nad treścią), ale
               karty (.tablecard, .card, .filters) go już nie czytają — patrz niżej. */
            --radius-card: 12px;
            --radius-field: 8px;
            --card-shadow: 0 1px 3px rgba(0,0,0,0.08);
            --border: #e5e7eb;
            --th-bg: #f0f1f3;
            --hover-bg: #fafafa;
            --input-bg: #fff;
            --input-border: #d1d5db;
            --muted: #6b7280;
            /* Paleta stanów (ksiegowosc-poprawki.html) — ODDZIELONA od
               `--accent`, który jest per-firma i bywa pomarańczowy: pasek
               postępu i badge „Do akceptacji" mają wtedy niemal ten sam
               odcień, choć znaczą co innego. Wartości NIE z pliku
               referencyjnego (ten ma własny zielony akcent) — z tego, co już
               było w tej apce (.badge-assigned/.badge-partial/.badge-multi/
               .badge-rejected), żeby nie dodawać drugiego, nieco innego
               zielonego/bursztynowego obok już istniejącego. */
            --ok: #047857; --ok-bg: #d1fae5; --ok-line: #a7d9c4;
            --warn: #92400e; --warn-bg: #fef3c7; --warn-line: #eddfae;
            --danger: #991b1b; --danger-bg: #fee2e2; --danger-line: #f3b8b8;
            --info: #1e40af; --info-bg: #dbeafe; --info-line: #bcd4f5;
            /* --due-soon: TEKST samodzielny na tle strony/karty (kolumna
               Termin, karta telefonu), nie --warn. --warn liczony był jako
               tekst NA --warn-bg (6,37:1 tam) — na białym tle --card-bg
               staje się drugą czerwienią: #92400e (L*=37,5) vs --danger
               #991b1b (L*=33,2) różnią się o 4,3 L* i 23° i dają między sobą
               1,17:1 (1,0 = identyczne), nierozróżnialne w praktyce.
               --due-soon #b45309 (L*=46,9): różnica z --danger 13,7 L* —
               trzykrotnie więcej. Kontrast liczony na FAKTYCZNYCH
               powierzchniach tego komponentu (.tablecard/.m-card stoją na
               --card-bg #fff, nie na --th-bg, który tu w ogóle nie
               występuje): 5,02:1 na --card-bg, 4,81:1 na --hover-bg (wiersz
               pod kursorem) — oba nad progiem 4,5:1. Tryb ciemny bez zmian
               (#fbbf24, ta sama wartość co --warn tam) — para już działa:
               16,6 L* różnicy z --danger na realnych powierzchniach. */
            --due-soon: #b45309;
            /* Dwa tła „szeptem", osobne od --warn-bg i osobne OD SIEBIE.
               --control-warn-bg to tło POLA FORMULARZA wymagającego
               uzupełnienia — celowo słabsze niż --warn-bg, które jest tłem
               BLOKU ostrzeżenia (.warnbox): blok mówi „uwaga", pole mówi „tu
               czegoś brakuje". W trybie jasnym różnica jest sześciokrotna
               w L* i ma taka zostać.
               --inactive-bg to tło plakietki celu NIEAKTYWNEGO (.sg--dead).
               W trybie jasnym ma ten sam hex co wyżej, ale to ZBIEG
               OKOLICZNOŚCI dwóch podstaw równych bieli, nie wspólny token:
               pole stoi na --input-bg (L* 8,30 w ciemnym), plakietka na
               --card-bg (L* 16,27). Ten sam szept liczony od różnych podstaw
               daje różne hexy — wspólna zmienna zrobiłaby z plakietki dziurę
               w karcie. Nie zrównywać „dla spójności". */
            --control-warn-bg: #fffdf5;
            --inactive-bg: #fffdf5;
            /* PODZIAŁ RÓL AKCENTU (runda 30, krok 3):
               --accent    = kolor marki w roli WYPEŁNIENIA (tło przycisku,
                             plakietki, awatara, obramowanie) — dokładnie
                             wartość z panelu admina, nietknięta.
               --accent-fill = to samo wypełnienie, ale POD NAPISEM: akcent
                             przyciemniony tyle, ile trzeba, żeby biała
                             etykieta miała 4,5:1 (branding/colors.py).
                             Tło plakietki, obramowanie i podkreślenie
                             zostają na --accent — zmienia się wyłącznie
                             wypełnienie, na którym stoi tekst.
               --accent-fg = etykieta na tym wypełnieniu: biel, a czerń tylko
                             jako wyjście awaryjne dla akcentów zbyt jasnych,
                             żeby dało się je przyciemnić.
               --link      = ten sam kolor marki w roli TEKSTU — dowolnego,
                             nie tylko odnośnika (etykiety, obrysy ikon) —
                             doprowadzony do 4,5:1 na NAJTRUDNIEJSZEJ
                             powierzchni danego motywu: --th-bg w jasnym
                             (najciemniejsza), --hover-bg w ciemnym
                             (najjaśniejsza). Nie na --card-bg: odnośnik
                             „Wróć do listy" stoi na --bg, a ikona odbioru
                             na --hover-bg.
                             Nazwa jest dziś węższa niż rola; przemianowanie
                             czeka na osobną rundę (§7), bo dotyka całego
                             arkusza.
               Wartości zapasowe niżej są normalnie NADPISYWANE przez inline
               <style> z base.html (context processor `company_settings`),
               i to jest ich jedyne zadanie. Stoją tu na wypadek renderu bez
               context processorów: `django.views.defaults.server_error`
               renderuje 500.html BEZ `request`, więc gdyby ktoś kiedyś dodał
               taki szablon rozszerzający base.html, 21 reguł zostałoby bez
               wartości. Równe domyślnym z modelu (branding.CompanySettings).
               UWAGA: tu i tylko tu — wariant CIEMNY akcentu w tym arkuszu
               byłby przebity przez inline :root z base.html (ta sama
               specyficzność, późniejsze wystąpienie), więc powstaje po
               stronie szablonu. */
            --accent: #2563eb;
            --link: #2563eb;
            --accent-fg: #ffffff;
        }
        @media (prefers-color-scheme: dark) {
            :root:not([data-theme="light"]) {
                color-scheme: dark;
                --bg: #111827;
                --text: #e5e7eb;
                --card-bg: #1f2937;
                --card-shadow: none;
                --border: #374151;
                --th-bg: #111827;
                --hover-bg: #262f3d;
                --input-bg: #111827;
                --input-border: #374151;
                --muted: #9ca3af;
                /* Paleta stanów w wariancie ciemnym: podstawa JASNA na bardzo
                   ciemnym tle w tym samym odcieniu (w jasnym jest odwrotnie),
                   linia jeden krok jaśniejsza od tła. */
                --ok: #34d399; --ok-bg: #022c22; --ok-line: #065f46;
                --warn: #fbbf24; --warn-bg: #451a03; --warn-line: #78350f;
                --danger: #f87171; --danger-bg: #450a0a; --danger-line: #7f1d1d;
                --info: #60a5fa; --info-bg: #172554; --info-line: #1e3a8a;
                --control-warn-bg: #22190c;
                --inactive-bg: #382813;
                --due-soon: #fbbf24;
            }
        }
        :root[data-theme="dark"] {
            color-scheme: dark;
            --bg: #111827;
            --text: #e5e7eb;
            --card-bg: #1f2937;
            --card-shadow: none;
            --border: #374151;
            --th-bg: #111827;
            --hover-bg: #262f3d;
            --input-bg: #111827;
            --input-border: #374151;
            --muted: #9ca3af;
            /* Paleta stanów w wariancie ciemnym: podstawa JASNA na bardzo
               ciemnym tle w tym samym odcieniu (w jasnym jest odwrotnie),
               linia jeden krok jaśniejsza od tła. */
            --ok: #34d399; --ok-bg: #022c22; --ok-line: #065f46;
            --warn: #fbbf24; --warn-bg: #451a03; --warn-line: #78350f;
            --danger: #f87171; --danger-bg: #450a0a; --danger-line: #7f1d1d;
            --info: #60a5fa; --info-bg: #172554; --info-line: #1e3a8a;
            --control-warn-bg: #22190c;
            --inactive-bg: #382813;
            --due-soon: #fbbf24;
        }
        /* Gdy OS woli ciemny motyw, ale użytkownik ręcznie wybrał jasny
           przełącznikiem — bez tego natywne kontrolki (m.in. ikona kalendarza
           w input[type=date]) renderują się w kolorze dopasowanym do OS, nie
           do faktycznie widocznego (jasnego) tła, i stają się niewidoczne. */
        :root[data-theme="light"] {
            color-scheme: light;
        }
        body {
            /* Krój: Segoe UI (decyzja właściciela, 21.09.2026 — Inter WYCOFANY). Systemowy,
               bez pliku czcionki ani preloadu: Segoe UI jest zainstalowany na komputerach
               klienta (Windows), zapasowy stos dla innych systemów. */
            font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, sans-serif;
            /* Cyfry o stałej szerokości GLOBALNIE: Segoe UI ma je domyślnie, ta deklaracja
               jest zabezpieczeniem dla krojów zapasowych (systemowy Linux/macOS bez Segoe UI
               może mieć cyfry proporcjonalne). Kwoty, daty, numery faktur i liczniki mają stać
               w pionie równo, a każdy przyszły licznik dziedziczy to bez osobnej deklaracji. */
            font-variant-numeric: tabular-nums;
            /* TYPOGRAFIA ZGODNA Z PLIKIEM REFERENCYJNYM (uklad-aplikacji.html, 21.09.2026):
               makieta ustawia body na 14,5 px i każdemu elementowi rozmiar W PIKSELACH;
               aplikacja startowała z 16 px przeglądarki. UWAGA: rem liczy się od <html>
               (16 px), nie od body — ta linia NIE przeskalowuje wartości w rem, dlatego
               elementy z listy właściciela mają niżej rozmiary wpisane w px. */
            font-size: 14.5px;
            line-height: 1.5;
            margin: 0;
            background: var(--bg);
            color: var(--text);
        }
        main a { color: var(--link); }
        .invoice-number {
            font-weight: 700;
            font-size: 1rem;
            font-variant-numeric: tabular-nums;
            text-decoration: none;
        }
        .invoice-number:hover { text-decoration: underline; }
        /* header to czysty wrapper pozycjonujący (te same liczby co main:
           max-width/margin/padding), bez własnego tła/obramowania — dokładnie
           tak jak main jest niewidocznym kontenerem, a widoczną białą kartę
           (.filters/.card) renderuje DOPIERO jego dziecko, wypełniające
           content-box main (czyli realnie max-width, 1100px, nie
           max-width+padding main). Poprzednia wersja dawała headerowi WŁASNE
           tło+border na całym max-width+padding, więc wychodził szerszy niż
           realna, widoczna szerokość kart poniżej — i to o inną wartość przy
           różnych szerokościach viewportu (main/.card w trybie "wypełnij
           dostępną szerokość" poniżej ~1148px zachowują się inaczej niż
           header liczony jako pojedynczy płaski box). .topbar (dziecko
           header, patrz niżej) odgrywa dokładnie rolę .card — te same liczby,
           ta sama zagnieżdżona struktura, więc zgodność przy KAŻDEJ
           szerokości ekranu, nie tylko jednej przetestowanej. */
        /* Runda 30, krok 5: header istnieje już TYLKO na stronach bez powłoki
           (logowanie, MFA, wymuszona zmiana hasła) — szerokość tokenem, nie
           literałem; liczby nadal RÓWNE tym z main (komentarz wyżej). */
        header { max-width: var(--read-max); margin: 1.25rem auto 0; padding: 0 1.5rem; }
        .topbar {
            box-sizing: border-box; width: 100%;
            background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px;
            min-height: 64px; padding: 0 1.5rem;
            display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap;
            position: relative;
        }
        .brand { display: flex; align-items: center; gap: 0.5rem; text-decoration: none; color: var(--text); font-weight: 600; font-size: 1.05rem; white-space: nowrap; }
        .brand img { height: 34px; width: auto; display: block; }
        .nav-links { display: flex; align-items: center; gap: 0.25rem; flex: 1; }
        .nav-links a {
            color: var(--muted); text-decoration: none; font-size: 0.88rem; font-weight: 500;
            padding: 0.5rem 0.85rem; border-radius: 8px;
        }
        .nav-links a:hover { background: var(--hover-bg); color: var(--text); }
        .nav-links a.active { color: var(--accent-fg); background: var(--accent-fill, var(--accent)); font-weight: 600; }
        /* margin-left: auto — trzyma topbar-right przy prawej krawędzi nawet
           gdy .nav-links (flex:1, normalnie robi to samo) nie jest
           wyrenderowane, np. na stronie logowania dla niezalogowanego. */
        .topbar-right { display: flex; align-items: center; gap: 0.5rem; margin: 0.6rem 0; margin-left: auto; }
        .icon-btn {
            width: 34px; height: 34px; border-radius: 8px; border: 1px solid var(--input-border);
            background: var(--card-bg); color: var(--text); display: flex; align-items: center;
            justify-content: center; font-size: 0.95rem; cursor: pointer;
        }
        .icon-btn:hover { background: var(--hover-bg); }
        /* Ikona odzwierciedla aktualny motyw, tymi samymi selektorami co zmienne
           kolorów wyżej (OS preference + ręczny data-theme override) — bez JS,
           więc nigdy nie mignie zła ikona przy ładowaniu strony. */
        .icon-moon svg { display: block; color: #000080; }
        .icon-sun { display: none; }
        @media (prefers-color-scheme: dark) {
            :root:not([data-theme="light"]) .icon-moon { display: none; }
            :root:not([data-theme="light"]) .icon-sun { display: inline; }
        }
        :root[data-theme="dark"] .icon-moon { display: none; }
        :root[data-theme="dark"] .icon-sun { display: inline; }
        .user-menu { position: relative; }
        .user-menu-trigger {
            display: flex; align-items: center; gap: 0.5rem; padding: 0.3rem 0.6rem 0.3rem 0.3rem;
            border-radius: 8px; border: 1px solid var(--input-border); background: var(--card-bg);
            cursor: pointer; font: inherit; color: var(--text);
        }
        .user-menu-trigger:hover { background: var(--hover-bg); }
        .avatar {
            width: 26px; height: 26px; border-radius: 50%; background: var(--accent-fill, var(--accent)); color: var(--accent-fg);
            display: flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: 700;
            flex-shrink: 0;
        }
        .user-name { font-size: 0.85rem; color: var(--text); white-space: nowrap; }
        /* Zastępuje tekstowy znak &#9652; ikoną SVG (zgłoszenie właściciela
           21.09.2026: za mały, słabo widoczny). Rozmiar 18px przez `.side .ic`
           (dziedziczy klasę `.ic`) — CELOWO nie 15px jak w pliku referencyjnym
           (inline `style="width:15px;height:15px"` tylko na tym jednym elemencie
           makiety): polecenie było "w rozmiarze POZOSTAŁYCH ikon stopki", czyli
           tych samych 18px co `.side-tool`/`.side-exit` obok — nie własny,
           mniejszy rozmiar. `color: var(--muted)` steruje `stroke: currentColor`
           (`.side .ic`) — ta sama zasada co reszta ikon linii w stopce. */
        .chevron { color: var(--muted); }
        .dropdown {
            display: none; position: absolute; top: calc(100% + 0.4rem); right: 0; background: var(--card-bg);
            border: 1px solid var(--border); border-radius: 8px; box-shadow: var(--card-shadow);
            min-width: 170px; padding: 0.35rem; z-index: 20;
        }
        .dropdown.open { display: block; }
        .dropdown a, .dropdown button {
            display: block; width: 100%; text-align: left; font-size: 0.85rem; padding: 0.5rem 0.6rem;
            border-radius: 6px; color: var(--text); text-decoration: none; background: none; border: none;
            font: inherit; cursor: pointer; margin: 0;
        }
        .dropdown a:hover, .dropdown button:hover { background: var(--hover-bg); }
        .dropdown button.dropdown-danger { color: #dc2626; }
        .dropdown hr { border: none; border-top: 1px solid var(--border); margin: 0.3rem 0; }
        /* main bez powłoki — wyśrodkowany jak dotąd (token zamiast 1100px);
           z powłoką: blok „SZEROKOŚCI I TYPY STRON" na końcu arkusza. */
        body:not(.has-shell) main { max-width: var(--read-max); margin: 1.5rem auto; padding: 0 1.5rem; }
        .page-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.75rem; margin-bottom: 1.25rem; }
        .page-header h1 { margin: 0; font-size: 22px; font-weight: 700; }   /* makieta .pagebar h1 */
        /* letter-spacing: normal — bez tego liczba dziedziczy odstęp -0,01em
           z `.page-header h1` (usterka znaleziona porównaniem z makietą,
           21.09.2026): cyfry stoją wtedy odrobinę ściśnięte, makieta go zeruje. */
        .page-header .count { color: var(--muted); font-weight: 400; font-size: 14px; margin-left: 0.4rem; letter-spacing: normal; }   /* makieta .pagebar .cnt */
        .sync-status { font-size: 12.4px; }   /* makieta .sync („Ostatnia synchronizacja") */
        .table-card {
            background: var(--card-bg); box-shadow: var(--card-shadow); border: 1px solid var(--border);
            border-radius: 8px; overflow-x: auto; -webkit-overflow-scrolling: touch;
        }
        table { width: 100%; border-collapse: collapse; }
        th, td { padding: 0.7rem 0.9rem; border-bottom: 1px solid var(--border); text-align: left; font-size: 13.2px; } /* komórka tabeli: makieta 13,2 px */
        th {
            background: var(--th-bg); color: var(--muted); font-size: 0.75rem;
            text-transform: uppercase; letter-spacing: 0.04em; font-weight: 700;
        }
        tbody tr:last-child td { border-bottom: none; }
        tr:hover td { background: var(--hover-bg); }
        /* max-width:100% + ellipsis: bez tego długa nazwa kontraktu (Kontrakt.__str__
           zawsze dokleja " (Oddział)", więc to jest normalny przypadek, nie
           wyjątek) w wąskiej kolumnie listy po prostu wypływała poza swoją
           kolumnę i nachodziła na sąsiednią — inline-block domyślnie ignoruje
           szerokość przydzieloną przez rodzica flex. Dla krótkich odznak (status,
           etap) to zero-kosztowy no-op, bo i tak mieszczą się w swojej kolumnie. */
        /* Makieta .badge: 11,6 px / 600 / line-height 1 / 4px 9px / 999px (jedna wspólna klasa —
           lista, karty telefonu, nagłówek szczegółów, strony konta, kreator). inline-block, nie
           inline-flex jak w makiecie: text-overflow działa tylko na pudełku blokowym/inline-block. */
        .badge {
            display: inline-block; padding: 4px 9px; border-radius: 999px; font-size: 11.6px; line-height: 1;
            font-weight: 600; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
            gap: 5px;   /* makieta; bez efektu na inline-block dzisiaj (gap działa na flex/grid) — z sekcji 0 wprost */
        }
        .badge-unassigned { background: #e5e7eb; color: #374151; }
        .badge-assigned { background: #d1fae5; color: #065f46; }
        .badge-partial { background: #fef3c7; color: #92400e; }
        .badge-multi { background: #dbeafe; color: #1e40af; }
        .badge-rejected { background: #fee2e2; color: #991b1b; }
        /* "W opracowaniu" — pozycja u kierownika, jeszcze nie zgłoszona do
           księgowości. Chłodny szaroniebieski, celowo odróżnialny od szarego
           .badge-unassigned ("Do akceptacji"), bo to sąsiednie stany. */
        .badge-draft { background: #e2e8f0; color: #475569; }
        /* "Po terminie" — świadomie w tym samym słowniku .badge-*, nie jako
           osobny język wizualny. Od rundy 30 kroku 4 to JEDYNY stan terminu
           pokazywany jako plakietka (kolumna Płatność) — "zbliża się" i samo
           "po terminie" na dacie wyrażają się już kolorem kolumny Termin
           (`.row-termin-date.is-late`/`.is-soon`, `--danger`/`--due-soon`), więc
           `.badge-due-soon` (dawny drugi wariant tej pary) odpadło jako
           martwe razem z usunięciem jego jedynego użycia w invoice_list.html. */
        .badge-overdue { background: #fee2e2; color: #991b1b; }
        /* Wiersz usługi w Rozliczeniach/Usługach — neutralny indygo, ta sama
           konwencja co pozostałe badge'e statusów. */
        .nr-badge-service { background: #e0e7ff; color: #3730a3; }
        .service-row-actions { display: flex; flex-direction: column; align-items: flex-end; gap: 0.3rem; }
        /* Karta płaska: bez box-shadow, tylko ramka 1px --border (decyzja właściciela
           21.09.2026, po porównaniu z makietą) — --card-shadow zostaje jako token dla
           elementów UNOSZĄCYCH SIĘ NAD treścią (.dropdown, .modal, .target-combobox-results),
           gdzie cień niesie znaczenie warstwy; karta w normalnym przepływie strony go nie ma. */
        .card { background: var(--card-bg); padding: 1.25rem 1.5rem; border-radius: var(--radius-card); border: 1px solid var(--border); margin-bottom: 1rem; }
        .stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
        .stat-card { display: flex; flex-direction: column; gap: 0.25rem; text-decoration: none; color: inherit; }
        /* Tylko kafelki-linki (<a class="stat-card">) — kafelek statusu KSeF
           to zwykły <div>, więc świadomie nie dostaje tego samego sygnału
           (audyt UX, A18/2): bez tego nic nie odróżniało klikalnych kafelków
           od tego jednego, który nic nie robi. */
        a.stat-card:hover { background: var(--hover-bg); }
        .stat-number { font-size: 1.8rem; font-weight: 700; color: var(--text); }
        /* Niezerowy licznik zalegających — ta sama czerwień co .badge-overdue
           i .btn-danger, żeby "coś jest po terminie" czytało się od razu. */
        .stat-number-alert { color: #dc2626; }
        .stat-label { font-size: 0.85rem; color: var(--muted); }
        .stat-sub { font-size: 0.8rem; color: var(--muted); }
        /* Ten sam wzorzec kolorystyczny co .messages .error (audyt UX, A18/3)
           — błąd synchronizacji KSeF na Pulpicie wyglądał jak zwykły,
           wyciszony podpis kafelka, nie jak coś wymagające uwagi. */
        .dashboard-ksef-error { background: #fee2e2; color: #991b1b; padding: 0.3rem 0.5rem; border-radius: 4px; }
        .card h2 { margin: 0 0 1rem; padding-bottom: 0.75rem; border-bottom: 1px solid var(--border); font-size: 1.05rem; }
        .card table { margin-top: -0.25rem; }
        .card table th:first-child, .card table td:first-child { padding-left: 0; }
        .card table th:last-child, .card table td:last-child { padding-right: 0; }
        .section-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.75rem; margin: 0 0 1rem; padding-bottom: 0.75rem; border-bottom: 1px solid var(--border); }
        .card .section-header h2 { margin: 0; padding: 0; border: none; }
        .line-cards { display: flex; flex-direction: column; gap: 0.75rem; }
        .line-cards-header { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; padding: 0 0.9rem 0.5rem; }
        .line-cards-header span {
            font-size: 0.7rem; color: var(--muted); text-transform: uppercase;
            font-weight: 700; letter-spacing: 0.04em;
        }
        .line-card { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
        .line-card-top { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; padding: 0.75rem 0.9rem; }
        .line-card-pos { color: var(--muted); font-size: 0.85rem; width: 1rem; }
        .line-card-name { flex: 1 1 200px; font-size: 1rem; font-weight: 600; color: var(--text); overflow-wrap: anywhere; }
        .line-card-qty, .line-card-price { font-size: 0.85rem; color: var(--muted); width: 90px; text-align: right; }
        .line-card-total { font-size: 0.9rem; font-weight: 600; color: var(--text); width: 100px; text-align: right; }
        .line-card-vat { font-size: 0.85rem; color: var(--muted); width: 40px; text-align: right; }
        /* Panel przypisania/opisu/WZ/akceptacji pod nagłówkiem karty pozycji
           (i, zagnieżdżony, pod każdym przydziałem) — zastępuje dawny
           .line-card-actions. Formularz zapisu (.line-panel-form) i akcje
           akceptacji (.line-panel-bottom) to celowo DWA osobne <form> (HTML
           nie pozwala zagnieżdżać formularzy), złożone wizualnie w jeden
           spójny panel przez wspólny kontener .line-panel. */
        .line-panel {
            display: flex; flex-direction: column; gap: 0.6rem;
            padding: 0.75rem 0.9rem; background: var(--th-bg); border-top: 1px solid var(--border);
        }
        .line-panel-form { display: flex; flex-direction: column; gap: 0.6rem; }
        /* MUSI być po regułach ustawiających display na klasach (np.
           .line-panel-form), inaczej atrybut `hidden` nie działa: reguła klasowa
           bije domyślne `display:none` przeglądarki i "ukryty" formularz zostaje
           widoczny obok panelu odczytu. */
        [hidden] { display: none !important; }
        .line-panel-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
        .line-panel-row textarea { margin: 0; }
        .line-panel-bottom { display: flex; justify-content: space-between; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
        .line-panel-actions { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
        .line-panel-actions form { display: contents; }
        /* Grupa przycisków, która ma się rozpłynąć w rzędzie akcji zamiast
           tworzyć własne pudełko — potrzebna, bo `hidden` działa tylko na
           elemencie, a przełączamy widoczność całej pary przycisków naraz. */
        .acceptance-group { display: contents; }
        /* Makieta: 12,6 px / 650 / wys. 30. Selektor podwójny, bo `.btn-outline` (niżej)
           jest SAMOWYSTARCZALNY (własna specyficzność klasy, nie dziedziczy z `button, .btn`)
           i bez tego wygrywałby swoim rozmiarem pełnym. Wysokość WPROST jak w bazowym `.btn`,
           nie z paddingu — ten sam powód. */
        .btn-sm, .btn-outline.btn-sm { height: 30px; padding: 0 11px; font-size: 12.6px; font-weight: 650; }
        /* SAMOWYSTARCZALNY, bo bywa zakładany na <a>, a nie tylko na <button>.
           Kotwica nie łapie ani selektora `button`, ani `.btn`, więc bez tych
           czterech własności renderowała się podkreślona, bez zaokrąglenia i
           lżejszą czcionką — jak odnośnik wciśnięty w ramkę przycisku.

           DOPISANIE `.btn` NIE JEST ROZWIĄZANIEM: `button, .btn` ma `border:
           none` i stoi w tym pliku NIŻEJ, więc przy równej wadze wygrywa i
           kasuje ramkę tego wariantu. */
        .btn-outline {
            display: inline-flex; align-items: center; justify-content: center; gap: 6px;
            height: 36px; padding: 0 15px; border-radius: var(--radius-field);
            font-weight: 650; font-size: 13.4px; text-decoration: none;
            background: var(--card-bg); color: var(--text); border: 1px solid var(--input-border);
        }
        .btn-outline:hover { background: var(--hover-bg); }
        /* Modyfikator dla akcji destrukcyjnych na przyciskach .btn-outline
           (np. "Usuń", "Odrzuć") — te same odcienie czerwieni co już
           istniejące .btn-danger (#dc2626) i .messages .error (#fee2e2 na
           hover), tylko w wersji outline zamiast wypełnionej.
           ZGŁOSZONE PO WDROŻENIU (dyrektor-poprawki.html, poprawka 7): stale
           czerwony przycisk przyciągał wzrok mocniej niż cokolwiek innego na
           ekranie, a ta sama akcja ("Usuń" celu przypisania) wyglądała
           różnie zależnie od tego, czy usunięcie kasuje pracę kierownika —
           raz neutralnie, raz czerwoną ramką. Czerwień zostaje WYŁĄCZNIE na
           hover, spójnie niezależnie od kontekstu. */
        .btn-outline-danger:hover { color: #dc2626; border-color: #dc2626; background: #fee2e2; }

        .line-card-allocations {
            display: flex; flex-direction: column; gap: 0.5rem;
            padding: 0.6rem 0.9rem; border-top: 1px solid var(--border);
        }
        .allocation-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; font-size: 0.85rem; }
        .allocation-row form { display: contents; }
        .allocation-client { flex: 1 1 160px; font-weight: 600; color: var(--text); }
        .line-card-remaining { font-size: 0.8rem; color: var(--muted); }
        .allocation-add-form { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
        .allocation-add-form input[type="number"] { width: auto; max-width: 90px; }
        .allocation-add-form .target-combobox { flex: 1 1 200px; min-width: 160px; }
        .allocation-add-form .btn { flex: 0 0 auto; }

        /* Opis/skany WZ przydziału żyją w <details> wewnątrz .allocation-row
           — samo details musi dostać flex-basis:100% jak każdy inny wielo-
           liniowy blok tam, a formularz zapisu W ŚRODKU details musi odzyskać
           normalny display:block (zamiast dziedziczonego z .allocation-row
           form { display: contents }), inaczej textarea/przycisk uploadu
           rozjadą się w otaczający flex zamiast ułożyć się w pionie wewnątrz
           panelu. */
        .allocation-row > details { flex-basis: 100%; }
        .allocation-row details summary { cursor: pointer; font-size: 0.85rem; color: var(--muted); }
        .allocation-row details .allocation-save-form { display: block; margin-top: 0.4rem; }

        .opis-section { margin-top: 0.9rem; }
        .opis-section textarea { margin: 0.3rem 0; }

        /* .wz-scan-list/.wz-scan-item — używane już tylko przez sekcję
           Historii (lista wpisów); dokumenty WZ mają własny styl `.wz*`. */
        .wz-scan-section { margin-top: 0.9rem; }
        /* Krok 8.3, znalezisko z raportu 8.2: gołe `display:flex` bez
           warunku na `[open]` biło regułę UA `details:not([open]) > *`,
           więc zamknięta sekcja "Historia" renderowała swoje dziecko mimo
           natywnego zwinięcia (dziecko realnie zajmowało miejsce w
           layoucie). `.wz-scan-list` jest bezpośrednim dzieckiem
           `<details>` (invoice_detail.html) — zawężenie do `[open]`
           przywraca natywne chowanie, dotyczy wszystkich szerokości. */
        details[open] > .wz-scan-list { list-style: none; padding: 0; margin: 0.4rem 0; display: flex; flex-direction: column; gap: 0.3rem; }
        .wz-scan-item { display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; flex-wrap: wrap; }

        /* Combobox wyszukiwania Oddziału/Kontraktu (_target_combobox.html) —
           pierwszy taki widżet w apce, czysty vanilla JS bez frameworka
           (patrz initTargetCombobox w invoice_detail.html). */
        .target-combobox { position: relative; }
        .target-combobox-input { flex: 1 1 auto; }
        /* Szare "wybierz oddział / kontrakt" — musi wyglądać wyraźnie inaczej
           niż realnie wybrany oddział/kontrakt (zwykły kolor tekstu), inaczej
           oba stany są nie do odróżnienia na pierwszy rzut oka. */
        .target-combobox-input.is-placeholder { color: var(--muted); }
        .target-combobox-results {
            position: absolute; top: calc(100% + 2px); left: 0; right: 0; z-index: 15;
            margin: 0; padding: 0.25rem; list-style: none;
            background: var(--card-bg); border: 1px solid var(--border); border-radius: 6px;
            box-shadow: var(--card-shadow); max-height: 220px; overflow-y: auto;
        }
        .target-combobox-results li { padding: 0.4rem 0.6rem; border-radius: 4px; cursor: pointer; font-size: 0.88rem; }
        .target-combobox-results li:hover, .target-combobox-results li.active { background: var(--hover-bg); }

        /* Checkbox "potwierdzam odbiór" — alternatywa dla skanu WZ w bramce
           potwierdzenia. Przerywana ramka, bo to deklaracja użytkownika, a nie
           dokument; renderuje się tylko gdy skanu faktycznie brakuje. */
        .receipt-row {
            display: flex; align-items: flex-start; gap: 0.5rem; padding: 0.6rem 0.7rem;
            border: 1px dashed var(--border); border-radius: 6px; background: var(--hover-bg);
        }
        .receipt-row input[type="checkbox"] { width: auto; margin: 0.15rem 0 0; flex: 0 0 auto; }
        .receipt-row label { font-size: 0.85rem; cursor: pointer; }
        .receipt-row .hint { display: block; font-size: 0.75rem; color: var(--muted); margin-top: 0.15rem; }
        /* Wyjaśnienie przy nieaktywnym "Potwierdź opis i odbiór" — przycisk
           zostaje widoczny, żeby było wiadomo, czego brakuje. */
        .gate-msg { font-size: 0.78rem; color: var(--muted); }

        /* Panel zwinięty do odczytu. Zasada: OTWARTY FORMULARZ znaczy "tu
           jeszcze coś trzeba zrobić", podsumowanie z "Edytuj" znaczy "tu jest
           komplet". Bez tego wypełnione i puste pole wyglądają identycznie i
           przy kilkunastu pozycjach nie da się jednym rzutem oka odpowiedzieć,
           co jest zrobione. Kolor krawędzi niesie tę informację nawet przy
           przewijaniu, bez czytania treści. */
        .panel-done {
            display: flex; align-items: flex-start; gap: 0.9rem; padding: 0.85rem 0.9rem;
            background: var(--hover-bg); border-left: 3px solid #059669;
        }
        .panel-done-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0.5rem; }
        /* Własna klasa zamiast .allocation-client: tamta ma `flex: 1 1 160px`,
           co w kontenerze `flex-direction: column` znaczy 160px WYSOKOŚCI —
           stąd brała się pusta przerwa między nazwą celu a opisem. */
        .panel-done-target { font-weight: 600; color: var(--text); font-size: 0.9rem; }
        .panel-done-opis { margin: 0; font-size: 0.9rem; line-height: 1.5; white-space: pre-wrap; }
        .panel-done-meta { display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap; font-size: 0.78rem; color: var(--muted); }
        .panel-done-meta .ok { color: #059669; font-weight: 600; }
        .acceptance-actions-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem; }
        .panel-todo { border-left: 3px solid #d97706; }

        /* Kolor paska `.panel-done` WEDŁUG STANU opisu — tylko w podsumowaniu
           po wysyłce (`_wizard_locked_row.html`). ZGŁOSZONE PO WDROŻENIU: pasek
           domyślnie zielony wyglądał jak „zaakceptowana treść" niezależnie od
           stanu opisu. Ta sama zasada co w kroku 4 (`.ledited`): szary = bez
           zmian, niebieski = zmieniony, czerwony = brak opisu. Od poprawki
           „pełna treść przy każdej pozycji" (widoki-koncowe-poprawki, runda 2)
           pozycja domyślna cytuje swoją treść tak jak zmieniona — różni je
           tylko ten kolor paska i plakietka „poprawione". */
        .panel-done--domyslny { border-left-color: var(--input-border); }
        .panel-done--zmieniony { border-left-color: #1d4ed8; }
        .panel-done--brak { border-left-color: #b91c1c; }

        /* Numer księgowy w nagłówku karty „Pozycje..." (widok po wysyłce,
           runda 2 poprawek) — RAZ, przy jednym celu. Zastąpiło `.commondesc`
           (opis wspólny nad tabelą), zbędne od czasu, gdy każdy wiersz cytuje
           swoją treść wprost. */
        .section-header h2 .num2 {
            font-family: ui-monospace, Menlo, monospace; font-weight: 700;
            font-size: 0.8rem; color: var(--muted);
        }

        /* ===== Pozycja rozbita między kontrakty — JEDEN wiersz, rozbicie pod
           spodem (poprawka 4/5), nie dwa wiersze o tym samym numerze. */
        .splitrow { margin-top: 0.5rem; border: 1px solid var(--border); border-radius: 7px; overflow: hidden; }
        .splitrow .sr {
            display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap;
            padding: 0.5rem 0.7rem; border-bottom: 1px solid var(--border); font-size: 0.85rem;
        }
        .splitrow .sr:last-child { border-bottom: 0; }
        .splitrow .sr .c { font-weight: 600; min-width: 0; }
        .splitrow .sr .q { color: var(--muted); font-size: 0.78rem; }
        .splitrow .sr .v { margin-left: auto; font-weight: 600; white-space: nowrap; }
        .splitrow .sr .d {
            flex-basis: 100%; font-size: 0.8rem; color: var(--muted);
            border-left: 3px solid var(--border); padding-left: 0.6rem; margin-top: 0.2rem;
        }
        .splitrow .sr .d .dot { color: var(--input-border); }

        /* Krok 2 — jawny wybór poziomu opisu. Wcześniej ta decyzja zapadała
           sama, z tego czy ktoś dotknął pozycji. */
        .mode-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.9rem; }
        .mode-card {
            border: 1px solid var(--input-border); border-radius: var(--radius-card); padding: 1rem;
            display: flex; flex-direction: column; gap: 0.45rem; cursor: pointer;
            background: var(--card-bg); text-align: left; font-weight: 400; font-size: 0.9rem;
            color: var(--text); width: 100%;
        }
        .mode-card:hover { background: var(--hover-bg); }
        .mode-card.selected { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
        .mode-card strong { font-size: 0.95rem; }
        /* CELOWO nie zawężone do `.mode-card`. Tej samej klasy używa objaśnienie
           pod checkboxem „biorę odbiór na siebie" (`.wz-mine`), który kartą nie
           jest — przy zawężeniu zdanie renderowało się BEZ ŻADNEGO stylu: pełną
           wielkością i pełnym kontrastem, czyli wyglądało jak druga etykieta,
           a nie jak objaśnienie do niej. `display:block`, bo poza kartą (która
           jest flex-column) sam znacznik `<span>` zostałby w linii. */
        .mode-desc {
            display: block; color: var(--muted); font-size: 0.83rem; line-height: 1.5;
        }
        .mode-card .mode-when { font-size: 0.78rem; color: var(--link); font-weight: 600; }
        /* ===== Sekcja „Dokumenty WZ" w kroku 3 (deploy/ux/krok3-odbior-stany.html).
           Jeden komplet plików na całą część kierownika — lista wgranych plus
           strefa upuszczania, obie zawsze widoczne, bo dokładanie kolejnego
           dokumentu jest tu normą, nie wyjątkiem.

           `.wz-list`/`.wz-row` NIE SĄ tu zdefiniowane — te noszą listę
           TYLKO-DO-ODCZYTU w invoice_detail.html (księgowość/dyrektor widzą
           wszystkie dokumenty faktury naraz, bez podpisu/usuwania), więc
           zostają przy swoich regułach niżej. Ta sekcja (edytowalna, WŁASNA
           kreatora) ma inny, cięższy wiersz — stąd osobne `.frow`/`.fic`/itd.,
           nie kolejny wariant tych samych klas. */
        .seclabel { font-size: 0.85rem; font-weight: 650; color: var(--text); display: block; margin: 1rem 0 0.5rem; }

        .files { border: 1px solid var(--border); border-radius: 9px; overflow: hidden; background: var(--card-bg); }
        .files-head {
            display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap;
            padding: 0.5rem 0.7rem; border-bottom: 1px solid var(--border); background: var(--hover-bg);
            font-size: 0.8rem; font-weight: 650; color: var(--muted);
        }
        .files-head .cnt { margin-left: auto; font-weight: 600; }
        .frow {
            display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap;
            padding: 0.6rem 0.7rem; border-bottom: 1px solid var(--border);
        }
        .frow:last-child { border-bottom: 0; }
        /* Kwadrat w kolorze akcentu — ZGŁOSZONE PO WDROŻENIU: nazwa podkreślona
           jak zwykły link, bez ikony, ginęła w strefie uploadu o tym samym
           tle. Plik ma teraz wygląd OBIEKTU, nie linka. */
        .fic {
            width: 34px; height: 34px; border-radius: 7px; display: grid; place-items: center;
            flex: none; background: var(--hover-bg); color: var(--link);
        }
        /* `min-width:0` — bez tego długa nazwa pliku rozpycha wiersz i przy
           375 px wychodzi poza ekran. */
        .fname { display: flex; flex-direction: column; min-width: 0; flex: 1 1 11rem; }
        .fname a { font-weight: 650; overflow-wrap: anywhere; }
        .fname .meta { color: var(--muted); font-size: 0.75rem; margin-top: 0.05rem; }
        .fsig { flex: 1 1 9rem; min-width: 0; }
        .fsig input { width: 100%; font-size: 0.78rem; padding: 0.25rem 0.4rem; }
        .fsig input.zapis-blad { border-color: #dc2626; }
        .fdel {
            background: none; border: 0; cursor: pointer; color: var(--muted);
            font-size: 1rem; line-height: 1; padding: 0.25rem 0.4rem; border-radius: 4px; flex: none;
        }
        .fdel:hover { color: #dc2626; background: var(--hover-bg); }
        @media (max-width: 480px) {
            /* Pole podpisu schodzi pod nazwę, z wcięciem równym ikonie —
               inaczej przy wąskim ekranie ściska się do nieczytelnej wstążki. */
            .fsig { flex-basis: 100%; margin-left: calc(34px + 0.7rem); }
        }

        /* Strefa uploadu — DWA ROZMIARY: duża z ikoną, gdy nic nie wgrano,
           niska (`.drop--compact`), gdy lista plików już zrobiła swoje i nie
           musi dominować nad nią. `position:relative` kotwiczy schowane
           `input[type=file]` (`.wz-pick`) niżej. */
        .drop {
            position: relative; text-align: center; margin-top: 0.6rem;
            padding: 1.1rem 0.9rem; border: 2px dashed var(--input-border); border-radius: 9px;
            background: var(--hover-bg); transition: background-color 0.1s, border-color 0.1s;
        }
        /* `display: block` — bez tego ikona (span) i tekst zostają w JEDNEJ
           linii, jedno przy drugim, choć mają się układać ikona NAD tekstem
           (referencja: `.ic` tam jest `<div>`, stąd stackuje się sama). */
        .drop .ic { display: block; color: var(--muted); margin-bottom: 0.3rem; }
        .drop .t { font-size: 0.85rem; font-weight: 600; }
        .drop .fmt { display: block; font-size: 0.75rem; color: var(--muted); margin-top: 0.25rem; }
        .drop:hover { border-color: var(--accent); }
        .drop.is-dragover { outline: 2px dashed var(--accent); outline-offset: -2px; }
        .drop--compact { padding: 0.55rem 0.7rem; }
        .drop--compact .t { font-size: 0.8rem; font-weight: 500; }

        /* Runda 30, krok 8.1 — dwa wejścia wyboru pliku WZ, jeden wygląd.
           Dawniej `.wz-pick` był SAMYM tekstowym linkiem (104×15 px — jedna
           trzecia progu dotykowego 44 px, rozpoznanie R3). Teraz to
           przycisk: cała powierzchnia klikalna, nie sam tekst (§3.4). */
        .shoot { display: flex; gap: 0.5rem; flex-wrap: wrap; justify-content: center; margin-top: 0.6rem; }
        .wz-pick {
            display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem;
            min-height: 44px; padding: 0 0.9rem; border: 1px solid var(--input-border);
            border-radius: var(--radius-field); background: var(--card-bg);
            font-weight: 650; font-size: 0.85rem; color: var(--text); cursor: pointer;
            /* Krok 8.4: BEZ tego `.shoot .wz-pick{width:100%}` niżej liczyło 100%
               jako sam content-box, a padding/border (~31px) dokładały się NA
               WIERZCH — realna szerokość wychodziła poza `.shoot` (zmierzone:
               279px przy dostępnych 248px na 360px, telefon, krok 8.1's
               `width:100%`). Ten sam mechanizm co reportowane wychodzenie
               przycisków poza ramkę — nie tylko sama ramka. */
            box-sizing: border-box;
        }
        .wz-pick:hover { background: var(--hover-bg); }
        .wz-pick .ic {
            width: 16px; height: 16px; flex: none; stroke: currentColor; fill: none;
            stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round;
        }
        /* Natywny `input[type=file]` ma własną, nieskracalną szerokość i przy
           375 px rozpychałby kartę poza ekran. Chowamy go, a rolę przycisku
           przejmuje `.wz-pick` — SAMA etykieta, nie cała strefa; nazwy
           wybranych plików dopisuje JS na dole `_wizard.html`. */
        .wz-pick input[type="file"] {
            position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0;
        }
        /* Aparat WYŁĄCZNIE na urządzeniach dotykowych — regułą CSS, nigdy
           rozpoznawaniem systemu z nagłówka przeglądarki (decyzje-projektowe.md
           §1, ustalenie 23.09.2026). „Wybierz z dysku" zostaje widoczny zawsze. */
        .wz-pick--cam { display: none; }
        @media (pointer: coarse) {
            .wz-pick--cam { display: inline-flex; }
            /* Przeciąganie plików nie istnieje na telefonie (uklad-aplikacji.html
               §6) — dwa duże przyciski zastępują instrukcję, nie uzupełniają jej.
               `.drop .drag-hint` (nie goły `.drag-hint`) — musi przebić `.drop .ic
               { display: block }` wyżej, ta sama specyficzność, więc o kolejności
               decydowałaby wyłącznie pozycja w pliku bez tego doprecyzowania. */
            .drop .drag-hint { display: none; }
        }
        @media (max-width: 480px) {
            .shoot { flex-direction: column; }
            .shoot .wz-pick { width: 100%; }
        }

        /* Separator między drogami — słowo niesie różnicę między wyborem
           („albo", bez plików) i dopełnieniem („oraz", z plikami). */
        .orsep { display: flex; align-items: center; gap: 0.7rem; margin: 0.9rem 0; }
        .orsep::before, .orsep::after { content: ""; flex: 1; height: 1px; background: var(--border); }
        .orsep span { font-size: 0.7rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }

        /* Checkbox „biorę odbiór na siebie" — DROGA RÓWNORZĘDNA uploadowi, nie
           przypis do niego (stąd własna ramka, nie pasek wewnątrz `.wz`).
           Etykieta jest STAŁA (ZGŁOSZONE PO WDROŻENIU: dawniej zmieniał się
           sam tytuł, co przy wgraniu pliku wyglądało jak podmiana opcji) —
           zmienia się wyłącznie podpis `.d` pod spodem. */
        .self {
            display: flex; gap: 0.65rem; align-items: flex-start;
            border: 1px solid var(--border); border-radius: 9px; padding: 0.75rem 0.85rem;
            background: var(--card-bg); cursor: pointer;
        }
        .self:hover { border-color: var(--input-border); }
        .self input[type="checkbox"] { width: auto; margin: 0.15rem 0 0; flex: none; }
        .self > span { min-width: 0; }
        .self .t { display: block; font-weight: 650; font-size: 0.88rem; }
        .self .d { display: block; color: var(--muted); font-size: 0.8rem; margin-top: 0.2rem; line-height: 1.5; }
        .self--on { border-color: #e3dcc4; background: var(--hover-bg); }
        /* Uwagi kierowników dla księgowości — RAZ na fakturę (invoice_detail),
           i zwinięty panel dodawania w kroku 4 kreatora. Celowo NEUTRALNE:
           to adnotacja, nie ostrzeżenie — nigdy kolor/obramowanie w stylu
           .warnbox, nawet przy części już zaakceptowanej/odrzuconej. */
        .uwaga-list { list-style: none; margin: 0; padding: 0; }
        .uwaga-row { padding: 0.6rem 0.7rem; border-bottom: 1px solid var(--border); font-size: 0.85rem; }
        .uwaga-row:last-child { border-bottom: 0; }
        .uwaga-text { margin: 0.3rem 0; }
        /* Sekcja opisu jednego kontraktu w kroku 3. Nagłówek pokazuje się
           dopiero przy KILKU częściach — przy jednej (ponad 90% faktur) ekran
           ma wyglądać dokładnie jak dotąd. */
        .alloc-sec + .alloc-sec { margin-top: 1.1rem; padding-top: 0.9rem; border-top: 1px solid var(--border); }
        .alloc-sec-head {
            display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap;
            margin-bottom: 0.4rem;
        }
        .alloc-sec-head .nm { font-weight: 650; font-size: 0.9rem; }
        .alloc-sec-head .cnt { color: var(--muted); font-size: 0.8rem; }

        /* „Numer księgowy: 447/2026 — z kontraktu S19" w kroku 3. Fakt, nie
           pole: kierownik go nie edytuje (referencja: wz-i-opis-domyslny.html). */
        .fact {
            display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap;
            margin-bottom: 0.5rem; font-size: 0.85rem;
        }
        .fact .k { color: var(--muted); font-size: 0.8rem; }
        .fact .v { font-weight: 650; }
        .fact .src { color: var(--muted); font-size: 0.78rem; }
        .wiz-hint .warn { display: block; color: #92400e; margin-top: 0.15rem; }

        /* Sekcja jednego kontraktu w podsumowaniu kroku 4. */
        .sum-sec + .sum-sec { margin-top: 0.9rem; padding-top: 0.8rem; border-top: 1px solid var(--border); }
        .sum-sec .fact { margin-bottom: 0.35rem; }

        /* ===== Krok 2: lista pozycji z opisami (kreator-uproszczony-pozycje.html).
           Wiersz jest TEKSTEM, nie formularzem — dwadzieścia otwartych pól to
           ściana, w której nie da się niczego znaleźć. */
        .poz-grupa + .poz-grupa { margin-top: 1.2rem; padding-top: 0.9rem; border-top: 1px solid var(--border); }
        .poz-zbiorowe { margin: 0.5rem 0 0.9rem; }
        .poz-zbiorowe-row { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
        /* `min-width:0` na polu: bez tego nie kurczy się poniżej swojej
           domyślnej szerokości i przy 375 px wypycha przycisk poza kartę. */
        .poz-zbiorowe-row input { flex: 1 1 12rem; min-width: 0; }
        .poz-lista { list-style: none; margin: 0; padding: 0; border: 1px solid var(--border); border-radius: 9px; overflow: hidden; }
        /* KOTWICA NUMERYCZNA: numer w kółku po lewej, cała treść w kolumnie
           obok — ten sam wzorzec co numer kroku w kreatorze. Przy kilkunastu
           pozycjach numery tworzą pionową oś, po której da się wodzić okiem;
           wcześniej numer był zwykłym tekstem w linii i wiersze się zlewały. */
        .poz-wiersz {
            display: grid; grid-template-columns: 34px minmax(0, 1fr); gap: 0 0.7rem;
            padding: 0.7rem 0.85rem; border-bottom: 1px solid var(--border); font-size: 0.86rem;
        }
        .poz-wiersz:last-child { border-bottom: 0; }
        /* ZGŁOSZONE PO WDROŻENIU: numer w kółku to numer ORYGINALNY z faktury
           (`line.position`), nie kolejność na tej liście — przy krótkiej
           liście „2" jako jedyna pozycja wyglądało na błąd licznika.
           Etykieta „poz." nad numerem tłumaczy, skąd się bierze; ten sam
           numer, którym księgowość odsyła pozycję do poprawy. */
        .poz-num { display: flex; flex-direction: column; align-items: center; margin-top: 0.05rem; }
        .poz-num-lbl {
            font-size: 0.55rem; letter-spacing: 0.04em; text-transform: uppercase;
            color: var(--muted); font-weight: 700; line-height: 1; margin-bottom: 0.15rem;
        }
        .poz-num-val {
            width: 26px; height: 22px; border-radius: 6px; display: grid; place-items: center;
            background: var(--th-bg); color: var(--muted); font-size: 0.74rem; font-weight: 700;
        }
        /* Trzy stany trwałe + jeden przejściowy, rozróżniane paskiem, tłem i
           kolorem kotwicy. Kolor niesie tę samą informację co tekst, więc da
           się przeskanować listę bez czytania. */
        .poz-wiersz--zmieniony { background: #f7fbff; box-shadow: inset 3px 0 0 #1d4ed8; }
        .poz-wiersz--zmieniony .poz-num-val { background: #dbeafe; color: #1d4ed8; }
        .poz-wiersz--brak { background: #fffafa; box-shadow: inset 3px 0 0 #b91c1c; }
        .poz-wiersz--brak .poz-num-val { background: #fee2e2; color: #b91c1c; }
        .poz-wiersz--edycja { background: var(--hover-bg); box-shadow: inset 3px 0 0 var(--accent); }
        .poz-wiersz--edycja .poz-num-val { background: var(--accent-fill, var(--accent)); color: var(--accent-fg); }
        /* Ciemny motyw: samo przyciemnienie tła nie wystarcza — czerwień
           `#b91c1c` dobrana pod białe tło ląduje na ciemnobrunatnym i traci
           czytelność. Tekst i pasek jaśnieją razem z tłem. */
        @media (prefers-color-scheme: dark) {
            :root:not([data-theme="light"]) .poz-wiersz--zmieniony { background: #16233b; }
            :root:not([data-theme="light"]) .poz-wiersz--brak { background: #2b1a1a; }
            :root:not([data-theme="light"]) .poz-brak { color: #fca5a5; }
        }
        :root[data-theme="dark"] .poz-wiersz--zmieniony { background: #16233b; }
        :root[data-theme="dark"] .poz-wiersz--brak { background: #2b1a1a; }
        :root[data-theme="dark"] .poz-brak { color: #fca5a5; }
        .poz-glowa { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
        .poz-glowa .nm { font-weight: 600; flex: 1; min-width: 0; overflow-wrap: anywhere; }
        .poz-glowa .amt { margin-left: auto; color: var(--muted); white-space: nowrap; font-size: 0.8rem; }
        /* Krok 8.3, usterka 2.5.3: nazwa pozycji łamiąca się na telefonie
           zostawiała ilość/kwotę PRZYKLEJONE do pierwszej linii nazwy
           (margin-left:auto trzyma je na tym samym wierszu flexa, nawet gdy
           tekst .nm zawija się wewnątrz własnego pudełka). Na wąskim ekranie
           nazwa dostaje własny pełny wiersz, ilość/kwota schodzą pod spód. */
        @media (max-width: 767px) {
            .poz-glowa { flex-direction: column; align-items: flex-start; }
            .poz-glowa .amt { margin-left: 0; }
        }
        .poz-opis { margin-top: 0.3rem; display: flex; gap: 0.5rem; align-items: baseline; flex-wrap: wrap; }
        .poz-opis .quoted, .poz-opis .sl-missing { flex: 1 1 12rem; min-width: 0; }
        /* Wiersz z opisem DOMYŚLNYM nie pokazuje pełnego tekstu — przy
           kilkunastu pozycjach to kilkanaście razy to samo zdanie. Zamiast
           niego jedna szara linijka; „pokaż" rozwija treść w miejscu, bez
           wchodzenia w edycję (kierownik często chce tylko zweryfikować numer
           księgowy w opisie). */
        .poz-domyslny { margin-top: 0.3rem; font-size: 0.8rem; color: var(--muted); display: flex; gap: 0.5rem; align-items: baseline; flex-wrap: wrap; }
        .poz-domyslny .dot { color: var(--input-border); }
        /* Krok 8.3, usterka 2.5.4: kropka spięta z elementem, który następuje
           po niej, w jeden nierozdzielny kawałek — wrapuje jako całość,
           więc kropka nigdy nie zostaje sama na końcu zawiniętego wiersza. */
        .sep-group { display: inline-flex; align-items: baseline; gap: 0.5rem; white-space: nowrap; }
        /* `display: block` na cytacie, nie sam odstęp na kontenerze: `.quoted`
           to <span>, a element INLINE z pionowym paddingiem nie rozpycha
           sąsiednich linii — jego tło wchodziło na linijkę „opis domyślny…"
           tuż nad nim. W `.poz-opis` problemu nie było, bo tam ten sam element
           jest elementem flex i blokuje się sam. */
        .poz-pelny { margin-top: 0.35rem; }
        .poz-pelny .quoted { display: block; margin-top: 0; }
        /* ZGŁOSZONE PO WDROŻENIU: „pokaż" nigdy nie zamieniało się w „ukryj"
           — po rozwinięciu zostawał ten sam, już nieprawdziwy odnośnik.
           Wiersz pod tekstem ma teraz WŁASNY „ukryj" (patrz `-read`/`-edit`
           w `_wizard_step2_row.html`). */
        .poz-pelny-row { display: flex; gap: 0.5rem; align-items: baseline; margin-top: 0.4rem; font-size: 0.8rem; }
        .poz-pelny-row .dot { color: var(--input-border); }
        .poz-brak { color: #b91c1c; font-weight: 600; }
        .poz-edycja { margin-top: 0.35rem; }
        .poz-edycja .wiz-actions { margin-top: 0.4rem; }
        /* Stopka listy: po lewej rozwinięcie reszty, po prawej licznik
           poprawionych — to jedyna liczba, która mówi „ile tu było roboty". */
        .poz-stopka {
            display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap;
            margin-top: 0.6rem; font-size: 0.8rem; color: var(--muted);
        }
        .poz-stopka .right { margin-left: auto; }
        .poz-stopka b { color: var(--text); }

        .wz-save { margin-top: 0.55rem; }

        /* Lista dokumentów WZ TYLKO DO ODCZYTU (invoice_detail.html) — widok
           księgowości/dyrektora, wszystkie dokumenty faktury naraz, bez
           podpisu-jako-pola i bez usuwania. Osobna od `.files`/`.frow`
           wyżej (kreator, edytowalna, własna kierownika) — inny ciężar
           wizualny, inna treść wiersza. */
        .wz-list { list-style: none; margin: 0; padding: 0; }
        .wz-row {
            display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
            padding: 0.5rem 0.7rem; border-bottom: 1px solid var(--border);
            font-size: 0.82rem;
        }
        .wz-row:last-child { border-bottom: 0; }
        .wz-row .nm { display: flex; flex-direction: column; min-width: 0; flex: 1 1 11rem; }
        .wz-row .nm a { overflow-wrap: anywhere; }
        .wz-row .nm .meta { color: var(--muted); font-size: 0.75rem; }
        .wz-row .desc { flex: 1 1 9rem; min-width: 0; }

        /* ===== Karta przypisania dla Dyrektora/Księgowości
           (deploy/ux/dyrektor-przydzialy.html, stany 1 i 2). Paleta z tokenów
           :root, nie z pliku referencyjnego — ten ma własny zielony akcent, a
           tu akcent jest per firma (branding) i działa motyw jasny/ciemny.
           Amber bierzemy z tych samych wartości co .badge-partial niżej, żeby
           "uwaga" wyglądała wszędzie tak samo. ===== */
        .card-sub { margin: 0.2rem 0 0; font-size: 0.82rem; color: var(--muted); }
        .assign-fields { display: grid; grid-template-columns: minmax(0, 1fr) 210px; gap: 0.9rem; align-items: start; }
        @media (max-width: 720px) { .assign-fields { grid-template-columns: 1fr; } }
        .assign-hint { font-size: 0.78rem; color: var(--muted); margin: 0.3rem 0 0; min-height: 1.1rem; }
        .assign-hint b { color: var(--text); }
        /* Pole wymagane, którego KSeF nie wypełnił — bursztynowa ramka zamiast
           czerwonej: to jeszcze nie błąd, tylko brak do uzupełnienia. */
        .control--warn { border-color: #d8a92e !important; background: var(--control-warn-bg); }
        /* Licznik rozdysponowania (dyrektor-poprawki.html, poprawka 2) —
           NEUTRALNY do stanu wyjściowego, bursztynowy tylko przy
           `undeclared_gap` (ta sama paleta co `.warnbox`, żeby dwa alarmy o
           tym samym nigdy nie wyglądały inaczej). */
        .meter {
            display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap;
            padding: 0.7rem 0.9rem; border-radius: 8px; margin-bottom: 0.9rem;
            border: 1px solid; font-size: 0.85rem;
        }
        .meter--neutral { background: var(--hover-bg); border-color: var(--border); color: var(--muted); }
        .meter--neutral b { color: var(--text); }
        .meter--warn { background: var(--warn-bg); border-color: var(--warn-line); color: var(--warn); }
        .meter--warn b { font-weight: 700; }
        .meter .r { margin-left: auto; font-weight: 700; white-space: nowrap; }
        .meter .bar {
            flex-basis: 100%; height: 5px; border-radius: 3px;
            background: var(--input-border); overflow: hidden; display: flex;
        }
        .meter .bar i { height: 100%; display: block; background: var(--muted); }
        .meter--warn .bar i { background: var(--warn); }
        .gapnote {
            display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap;
            margin-top: 0.6rem; padding: 0.6rem 0.8rem; border-radius: 7px;
            background: var(--hover-bg); color: var(--muted); font-size: 0.83rem;
        }
        .gapnote b { color: var(--text); font-weight: 650; }
        .gapnote .r { margin-left: auto; }

        /* ===== Karta „Pozycje faktury" dla księgowości (ksiegowosc-poprawki.html)
           — przydział jako jednostka akceptacji, nie pozycja (poprawka 2). ===== */
        .alloc { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
        .alloc + .alloc { margin-top: 0.9rem; }
        .alloc--ready { border-color: var(--warn-line); }
        .alloc-head {
            display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap;
            padding: 0.8rem 0.95rem; background: var(--th-bg); border-bottom: 1px solid var(--border);
        }
        .alloc--ready .alloc-head { background: var(--warn-bg); }
        .alloc--progress .alloc-body { background: var(--hover-bg); }
        .alloc-head .kind {
            font-size: 0.66rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;
            color: var(--link); flex: none;
        }
        .alloc-head .nm { font-weight: 650; min-width: 0; }
        .alloc-head .num { font-family: ui-monospace, Menlo, monospace; font-size: 0.78rem; color: var(--muted); }
        .alloc-head .who { color: var(--muted); font-size: 0.82rem; white-space: nowrap; }
        .alloc-head .who .warn { color: var(--warn); }
        .alloc-head .amt { margin-left: auto; font-weight: 700; white-space: nowrap; }
        .alloc-body { padding: 0.9rem 0.95rem; }
        .numbox {
            background: var(--info-bg); border: 1px solid var(--info-line); border-radius: 8px;
            padding: 0.55rem 0.8rem; margin-bottom: 0.75rem; display: flex; align-items: baseline;
            gap: 0.7rem; flex-wrap: wrap;
        }
        .numbox .k {
            font-size: 0.7rem; letter-spacing: 0.05em; text-transform: uppercase; font-weight: 700;
            color: var(--info);
        }
        .numbox .v { font-family: ui-monospace, Menlo, monospace; font-size: 0.92rem; font-weight: 700; color: var(--text); }
        .numbox .src { margin-left: auto; font-size: 0.76rem; color: var(--muted); }
        .alloc-body .recline {
            display: flex; align-items: baseline; gap: 0.55rem; flex-wrap: wrap;
            font-size: 0.82rem; margin-bottom: 0.75rem; color: var(--muted);
        }
        .alloc-body .recline .ok { color: var(--ok); font-weight: 600; }
        .plist { border: 1px solid var(--border); border-radius: var(--radius-card); overflow: hidden; }
        .prow {
            display: grid; grid-template-columns: 42px minmax(0, 1fr) 90px 100px;
            gap: 0 0.7rem; padding: 0.55rem 0.75rem; border-bottom: 1px solid var(--border);
            align-items: baseline; font-size: 0.84rem;
        }
        .prow:last-child { border-bottom: 0; }
        @media (max-width: 700px) {
            .prow { grid-template-columns: 42px minmax(0, 1fr); gap: 0.2rem 0.7rem; }
            .prow .q, .prow .v { grid-column: 2; text-align: left; font-size: 0.79rem; color: var(--muted); }
        }
        .prow .nm { font-weight: 600; min-width: 0; overflow-wrap: anywhere; }
        .prow .q { text-align: right; color: var(--muted); white-space: nowrap; }
        .prow .q .of { opacity: 0.72; }
        .prow .v { text-align: right; font-weight: 600; white-space: nowrap; }
        .pdesc {
            grid-column: 2 / -1; margin-top: 0.3rem; font-size: 0.8rem; color: var(--muted);
            border-left: 3px solid var(--border); padding: 0.1rem 0 0.1rem 0.6rem;
        }
        .pdesc--custom { color: var(--text); border-left-color: var(--info); }
        .pdesc .tag {
            font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase; font-weight: 700;
            color: var(--info); margin-left: 0.5rem;
        }
        .prest { grid-column: 2 / -1; margin-top: 0.3rem; font-size: 0.8rem; color: var(--muted); }
        .prest b { font-weight: 650; }
        .alloc-actions {
            display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
            margin-top: 0.85rem; padding-top: 0.8rem; border-top: 1px solid var(--border);
        }
        .alloc-actions .note { font-size: 0.78rem; color: var(--muted); margin-right: auto; }
        .gap-lines { margin: 0.5rem 0 0; padding-left: 1.1rem; }
        /* Komunikat „pozycja bez właściciela" RAZ, nad tabelą — nie przy
           każdej pozycji z osobna (dyrektor-poprawki.html, poprawka 4). */
        .nobody { font-size: 0.85rem; color: var(--muted); margin-bottom: 0.75rem; background: var(--hover-bg); border-radius: 7px; padding: 0.6rem 0.8rem; }
        .gap-lines li { margin-top: 0.2rem; }
        .warnbox {
            background: #fef3c7; color: #92400e; border: 1px solid #eddfae;
            border-radius: 8px; padding: 0.7rem 0.9rem; font-size: 0.83rem;
            line-height: 1.5; margin-top: 0.9rem;
        }
        /* Ostrzeżenie przed zwolnieniem pozycji kierownika — dołożenie
           drugiego celu (dyrektor-poprawki.html, poprawka 6). Ta sama paleta
           co `.warnbox`, ale jako blok z ikoną, nie sam tekst — to jedyne
           miejsce w widoku dyrektora, gdzie jedno kliknięcie kasuje cudzą
           pracę, więc dostaje wagę wizualną odpowiadającą ryzyku. */
        .destructive {
            display: flex; gap: 0.6rem; align-items: flex-start; margin-top: 0.6rem;
            background: #fef3c7; border: 1px solid #eddfae; color: #92400e;
            border-radius: 7px; padding: 0.6rem 0.8rem; font-size: 0.8rem;
        }
        .destructive b { font-weight: 700; }
        /* Ramka z ikoną w karcie „Płatność" (runda 28, C6) — zapłata
           zadeklarowana przez wystawcę, wpłata części, opis terminu
           nierozpoznany. Ten sam kształt co `.destructive` (flex + ikona),
           kolor przez modyfikator z istniejącej palety `--warn/--info/--ok`
           (deploy/25 komentarz w app.css, sekcja `:root`) — żaden nowy
           odcień, tylko czwarty kontekst użycia tych samych trzech. */
        .info-box {
            display: flex; gap: 0.6rem; align-items: flex-start; margin-top: 0.6rem;
            border-radius: 7px; padding: 0.6rem 0.8rem; font-size: 0.83rem; line-height: 1.5;
        }
        .info-box svg { flex: none; margin-top: 0.1rem; }
        .info-box .lead { font-weight: 650; }
        /* `border`/`border-radius`/`overflow` zerowane jawnie: gołe `.sub`
           niżej w tym pliku (podsekcje A/B kroku 2) też trafia w ten sam
           selektor klasy i dorzuca sobie ramkę — niewidoczne w jasnym
           motywie (podobna jasność `--border` i `--ok-bg`), rażące w
           ciemnym (C9c, wyłapane dopiero na zrzucie ciemnego motywu). */
        .info-box .sub { color: var(--muted); font-size: 0.78rem; margin-top: 0.15rem; border: none; border-radius: 0; overflow: visible; }
        .info-box--warn { background: var(--warn-bg); border: 1px solid var(--warn-line); color: var(--warn); }
        .info-box--info { background: var(--info-bg); border: 1px solid var(--info-line); color: var(--info); }
        .info-box--ok { background: var(--ok-bg); border: 1px solid var(--ok-line); color: var(--ok); }
        .info-box--plain { background: var(--hover-bg); border: 1px solid var(--border); color: var(--text); }
        /* Przycisk osadzony w prawym rogu .info-box (np. "Cofnij oznaczenie"
           w karcie "Płatność", runda 28, C9c) — ten sam wzorzec co `.r`
           w deploy/ux/termin-zaplata-stany.html/oznaczanie-oplaconych.html:
           rozpycha się do prawej krawędzi ramki, nie zajmuje miejsca na
           szerokość. */
        .info-box-action { margin-left: auto; flex: none; }
        /* ≤640px: ramka "Zapłacono …" (info-box--ok) zawija się — tekst
           (ikona 18px + gap 0.6rem ≈ 28px) zostaje na swojej linii w pełnej
           szerokości, przycisk "Cofnij oznaczenie" spada pod spód, wcięty
           pod tekst (nie pod ikonę) — ten sam wzorzec co `.box`/`.box .r`
           w termin-zaplata-stany.html (C9e, zgłoszone po zrzucie 375px). */
        @media (max-width: 640px) {
            .info-box { flex-wrap: wrap; }
            .info-box-action { margin-left: 28px; }
        }
        /* Wiersz "Termin płatności: … [kontrolki]" w karcie "Płatność" (runda
           28) — do C9c NIE MIAŁ żadnej bazowej reguły poza jednym inline
           stylem na jednym z trzech wystąpień, więc pozostałe dwa renderowały
           się bez odstępów ani wyrównania (zgłoszone po porównaniu zrzutu
           z referencją, patrz decyzje-projektowe.md §5). `.sp` to ten sam
           wzorzec elementu rozpychającego co `.assign-actions .sp` wyżej w
           tym pliku — celowo nie `justify-content: space-between`, żeby przy
           zawijaniu (375px) grupy nie rozjeżdżały się nieprzewidywalnie. */
        .payment-row { display: flex; align-items: center; gap: 0.8rem; flex-wrap: wrap; margin-top: 0.8rem; }
        .payment-row .sp { flex: 1; }
        /* Formularz "Data zapłaty [pole] Oznacz/Przygotuj przelew" wewnątrz
           .payment-row — samo flex-wrap na .payment-row zawija TYLKO jej
           bezpośrednie dzieci (etykieta terminu / spacer / ten formularz
           jako jedna całość), nie elementy WEWNĄTRZ formularza. Bez
           własnego flex-wrap ten wiersz przycisków przelewał się poza
           375px (zgłoszone po zrzucie — C9c). */
        .mark-paid-form { display: inline-flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
        /* Krok 7.1 (F): .payment-row w karcie "Płatność" (prawa kolumna) —
           układ PIONOWY, elementy jeden pod drugim, przyciski na pełną
           szerokość. Zachowanie BEZ ZMIAN (te same formularze/action/nazwy
           pól) — zmienia się wyłącznie układ. Zawężone do `.payment-row
           .mark-paid-form`, NIE do gołego `.mark-paid-form` — ta klasa
           jest współdzielona ze stopką modala "Przygotuj przelew"
           (`.mark-paid-form.modal-foot`), którego układ zostaje bez zmian
           (poza zakresem kroku 7.1). Reguły wyżej zostają nietknięte:
           `.payment-row` niżej wygrywa przez KOLEJNOŚĆ (ta sama
           specyficzność, późniejsza reguła nadpisuje `flex-direction`/
           `align-items`), `.payment-row .mark-paid-form` niżej wygrywa
           przez WYŻSZĄ SPECYFICZNOŚĆ nad gołym `.mark-paid-form` — dwa
           różne mechanizmy cascade, nie ten sam. */
        .payment-row { flex-direction: column; align-items: stretch; }
        .payment-row .sp { display: none; }
        .payment-row .payment-actions { width: 100%; }
        .payment-row .mark-paid-form {
            display: flex; flex-direction: column; align-items: stretch; gap: 0.5rem; width: 100%;
        }
        .payment-row .mark-paid-form label { width: 100%; }
        .payment-row .mark-paid-form input[type="date"] { width: 100%; }
        .payment-row .mark-paid-form .btn,
        .payment-row .mark-paid-form .btn-outline { width: 100%; }
        /* PODSEKCJE A/B kroku 2 przy fakturze dzielonej. Krok 2 to wtedy DWIE
           czynności — zaznaczenie swoich pozycji i opisanie ich — a bez
           nagłówków wyglądały jak jedna długa ściana z trzema przyciskami.
           Numerowane litery robią to samo, co numery kroków w kreatorze:
           mówią, ile tego jest i gdzie się właśnie jest.

           B renderuje się OD POCZĄTKU, wygaszone. Pojawienie się nowej sekcji
           dopiero po kliknięciu zaskakuje; widoczna od razu zapowiada, że po
           zaznaczeniu przyjdzie jeszcze jedna robota. */
        .sub { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
        .sub + .sub { margin-top: 0.9rem; }
        .sub-head {
            display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap;
            padding: 0.7rem 0.9rem; background: var(--th-bg);
            border-bottom: 1px solid var(--border);
        }
        /* ZGŁOSZONE PO WDROŻENIU: kotwica podsekcji, wypełniona i w kolorze
           akcentu, miała ten sam ciężar wizualny co numer KROKU (.step-num,
           28px, wypełniony) — konkurowała z numeracją kreatora. Podsekcja
           jest podziałem WEWNĄTRZ kroku, musi być od niego wyraźnie słabsza:
           mniejsza, obrys zamiast wypełnienia. */
        .sub-letter {
            width: 20px; height: 20px; border-radius: 50%; display: grid; place-items: center;
            font-size: 0.7rem; font-weight: 700; flex: none;
            background: transparent; color: var(--link); border: 1.5px solid var(--accent);
        }
        .sub--done .sub-letter { background: var(--hover-bg); color: var(--link); border-color: var(--accent); }
        .sub--locked .sub-letter { background: transparent; border-style: dashed; border-color: var(--input-border); color: var(--muted); }
        .sub-title { font-weight: 650; font-size: 0.9rem; }
        .sub--locked .sub-title { color: var(--muted); }
        .sub-meta { margin-left: auto; font-size: 0.8rem; color: var(--muted); }
        .sub-body { padding: 0.85rem 0.9rem; }
        /* Wygaszenie dotyczy TREŚCI, nie nagłówka — tytuł podsekcji ma zostać
           czytelny, bo to on zapowiada, co przyjdzie. */
        .sub--locked .sub-body { opacity: 0.55; pointer-events: none; }
        .sub-lock { font-size: 0.82rem; color: var(--muted); padding: 0.7rem 0.9rem; }
        .sub-actions {
            display: flex; gap: 0.6rem; align-items: center; flex-wrap: wrap; margin-top: 0.75rem;
        }
        .sub-actions .hint { font-size: 0.78rem; color: var(--muted); }
        /* Ekran deklaracji pozycji (runda B) — kierownik zgłasza swoje pozycje przy
           fakturze dzielonej. Trzy kolumny na szerokim ekranie, jedna na
           telefonie: 375 px ma działać bez poziomego scrolla, a kierownik
           najczęściej patrzy na to właśnie na budowie. */
        .decl { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
        .decl-head, .decl-row {
            display: grid; grid-template-columns: minmax(0, 1fr) 190px 110px;
            gap: 0.75rem; padding: 0.7rem 0.9rem; align-items: start;
        }
        .decl-head {
            background: var(--th-bg); font-size: 0.72rem; letter-spacing: .05em;
            text-transform: uppercase; color: var(--muted);
        }
        .decl-head .r, .decl-row .cell-qty, .decl-row .cell-val { text-align: right; }
        /* Wariant z kolumną wyboru celu — kierownik prowadzący kilka kontraktów
           z tej samej faktury musi przy każdej pozycji wskazać, na który poszła
           (runda C2). Przy jednej części kolumny nie ma. */
        .decl--with-target .decl-head,
        .decl--with-target .decl-row {
            grid-template-columns: minmax(0, 1fr) 160px 190px 110px;
        }
        .decl-row .cell-target select {
            width: 100%; padding: 0.35rem 0.5rem; font: inherit; font-size: 0.85rem;
            border: 1px solid var(--input-border); border-radius: 6px;
            background: var(--input-bg); color: var(--text);
        }
        .shared .who { display: block; margin-top: 0.3rem; color: var(--muted); font-size: 0.8rem; }
        @media (max-width: 860px) {
            .decl--with-target .decl-head,
            .decl--with-target .decl-row { grid-template-columns: minmax(0, 1fr) 160px 110px; }
            .decl--with-target .decl-row .cell-target { grid-column: 1 / -1; }
        }
        /* Telefon: JEDNA kolumna. Trzymanie trzech kolumn do końca zwężało
           pierwszą do kilkudziesięciu pikseli i nazwa pozycji łamała się po
           jednym znaku („Kruszywo / łamane / 0/31,5"), a select wystawał poza
           kartę. Nagłówek kolumn znika — przy ułożeniu pionowym opisuje
           kolumny, których już nie ma. */
        @media (max-width: 720px) {
            .decl-head,
            .decl--with-target .decl-head { display: none; }
            .decl-row,
            .decl--with-target .decl-row { grid-template-columns: 1fr; gap: 0.45rem; }
            .decl-row .cell-qty, .decl-row .cell-val { text-align: left; }
            .decl-row .cell-val { font-weight: 600; }
            .sub-body { padding: 0.7rem 0.6rem; }
        }
        .decl-row { border-top: 1px solid var(--border); }
        .decl-row--mine { background: var(--hover-bg); }
        /* Pozycja wzięta w całości przez kogoś innego zostaje widoczna,
           wyszarzona — jej zniknięcie z listy wygląda na błąd aplikacji.
           WYRAŹNIE słabsza (tło + przygaszenie), nie tylko lekko: oko ma ją
           pomijać przy skanowaniu listy, bo nie ma tam nic do zrobienia. */
        .decl-row--full { opacity: 0.62; background: var(--th-bg); }
        .decl-row--full .taker { border-style: dashed; }
        .decl-row .nm { font-weight: 600; overflow-wrap: anywhere; }
        .decl-row .meta { font-size: 0.78rem; color: var(--muted); margin-top: 0.15rem; }
        .decl-row .takers { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-top: 0.35rem; }
        .decl-row .taker {
            font-size: 0.74rem; padding: 0.1rem 0.4rem; border-radius: 999px;
            border: 1px solid var(--border); color: var(--muted);
        }
        .decl-row .taker--me { border-color: var(--accent); color: var(--link); }
        .decl-row .qty { display: inline-flex; align-items: center; gap: 0.35rem; }
        .decl-row .qty input {
            width: 5.5rem; text-align: right; padding: 0.35rem 0.5rem;
            border: 1px solid var(--input-border); border-radius: 6px;
            background: var(--input-bg); color: var(--text); font: inherit;
        }
        .decl-row .qty .unit { font-size: 0.78rem; color: var(--muted); }
        /* ZGŁOSZONE PO WDROŻENIU: licznik „zostało X z Y" renderuje się tylko
           przy niezerowej deklaracji (świadomie — patrz komentarz w
           `_wizard_declaration.html`), ale bez zarezerwowanego miejsca
           wiersze BEZ licznika były niższe niż wiersze Z licznikiem i lista
           faluje. `min-height`, nie usunięcie warunku — pusty slot, nie
           pusty tekst. */
        .decl-row .left { font-size: 0.74rem; color: var(--muted); margin-top: 0.3rem; min-height: 1.1em; }
        .decl-row .left--zero { color: #92400e; }
        .decl-row .notmine { font-size: 0.78rem; color: var(--muted); font-style: italic; }
        .decl-row .take {
            display: inline-flex; align-items: center; gap: 0.4rem; cursor: pointer;
            font-size: 0.85rem; padding: 0.3rem 0.55rem; border-radius: 6px;
            border: 1px solid var(--border);
        }
        .decl-row .take--on { border-color: var(--accent); color: var(--link); }
        /* Wiersz rozpisany na kilka WŁASNYCH celów kierownika (jedna dostawa
           rozeszła się na dwa jego kontrakty). Zajmuje całą szerokość wiersza
           siatki, bo pola muszą się zmieścić także na 375 px — wciśnięte w
           kolumnę "Ile jest Twoje" miałyby szerokość dwóch cyfr. */
        /* Cel przy pozycji w trybie „każda pozycja osobno". Bez niego dwie
           części tej samej pozycji faktury (rozbitej między dwa kontrakty
           jednego kierownika) są nie do odróżnienia — ta sama nazwa, ten sam
           numer wiersza. */
        .posrow-target {
            display: inline-block; margin-left: 0.4rem; padding: 0.05rem 0.4rem;
            border: 1px solid var(--border); border-radius: 999px;
            font-size: 0.72rem; font-weight: 500; color: var(--muted);
            white-space: nowrap;
        }
        /* Zwinięta deklaracja w kroku 3 — jedna linijka zamiast tabeli, z
           odnośnikiem „Zmień". Ten sam ciężar wizualny co `.step-sum` przy
           zwiniętych krokach 1 i 2, żeby czytało się jako „to już zrobione",
           a nie jako kolejne zadanie. */
        .decl-done {
            display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
            font-size: 0.88rem; padding-bottom: 0.9rem; margin-bottom: 0.9rem;
            border-bottom: 1px solid var(--border);
        }
        .decl-done .ok { color: #0e8f4e; font-weight: 700; }
        .decl-done .sep { margin: 0 0.4rem; color: var(--muted); }
        .decl-done .lead { color: var(--muted); }
        .decl-done a { margin-left: auto; }
        .decl-split {
            grid-column: 1 / -1; margin-top: 0.5rem; padding: 0.6rem 0.75rem;
            border: 1px dashed var(--border); border-radius: 8px;
            display: grid; gap: 0.4rem;
        }
        .decl-split-row {
            display: flex; align-items: center; gap: 0.6rem; justify-content: space-between;
        }
        .decl-split-row .nm { font-size: 0.85rem; }
        .decl-split-row .qty { display: inline-flex; align-items: center; gap: 0.35rem; }
        .decl-split-row .qty input {
            width: 5.5rem; text-align: right; padding: 0.3rem 0.45rem;
            border: 1px solid var(--input-border); border-radius: 6px;
            background: var(--input-bg); color: var(--text); font: inherit;
        }
        .decl-split-sum {
            font-size: 0.78rem; color: var(--muted);
            border-top: 1px solid var(--border); padding-top: 0.35rem;
        }
        .shared {
            border: 1px solid var(--border); border-radius: 8px; padding: 0.6rem 0.8rem;
            font-size: 0.85rem; margin-bottom: 0.8rem; background: var(--hover-bg);
        }
        .mysum {
            display: flex; align-items: center; gap: 0.7rem; margin-top: 0.8rem;
            font-size: 0.88rem;
        }
        .mysum .amt { margin-left: auto; font-weight: 700; }
        @media (max-width: 640px) {
            .decl-head { display: none; }
            .decl-row { grid-template-columns: 1fr; }
            .decl-row .cell-qty, .decl-row .cell-val { text-align: left; }
        }
        /* Lista celów faktury na karcie przypisania (runda A). Bez kolumny
           kwoty i bez pozycji — na etapie przypisania jeszcze ich nie ma, bo
           deklarują je kierownicy. Wiersz zawija się na wąskim ekranie zamiast
           przewijać poziomo (wymóg 375 px z zadania). */
        .alloc-list { list-style: none; margin: 0 0 1rem; padding: 0; display: grid; gap: 0.4rem; }
        .alloc-row {
            display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
            border: 1px solid var(--border); border-radius: 8px; padding: 0.55rem 0.75rem;
            background: var(--hover-bg);
        }
        .alloc-row .alloc-target { font-weight: 600; }
        .alloc-row .alloc-who { font-size: 0.82rem; color: var(--muted); flex: 1 1 12ch; }
        .alloc-row .alloc-who .warn { color: #92400e; }
        .alloc-row .alloc-remove { margin-left: auto; }
        .add-target-form {
            border-top: 1px solid var(--border); margin-top: 1.1rem; padding-top: 1rem;
        }
        .add-target-row {
            display: flex; align-items: flex-start; gap: 0.6rem; margin-top: 0.35rem;
        }
        .add-target-row > :first-child { flex: 1 1 auto; min-width: 0; }
        @media (max-width: 480px) { .add-target-row { flex-direction: column; align-items: stretch; } }
        .assign-actions {
            display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap;
            border-top: 1px solid var(--border); margin-top: 1.1rem; padding-top: 1rem;
        }
        .assign-actions .note { font-size: 0.78rem; color: var(--muted); max-width: 46ch; }
        .assign-actions .sp { flex: 1; }
        /* Rozwidlenie "Kto opisze tę fakturę?" — ta sama karta co przy wyborze
           trybu opisu (.mode-card), więc bez osobnego komponentu. */
        .who-choice { display: grid; grid-template-columns: 1fr 1fr; gap: 0.9rem; margin-top: 0.5rem; }
        @media (max-width: 720px) { .who-choice { grid-template-columns: 1fr; } }
        .who-choice .mode-card { cursor: pointer; }
        .who-choice input[type="radio"] { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }
        .who-choice .mode-card:has(input:checked) { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }

        /* Podpowiedzi celu. Zawsze <button type="button"> — nigdy link i nigdy
           automatyczne wypełnienie pola; podpowiedź stoi OBOK pola i wymaga
           jednego kliknięcia. */
        .suggest { margin-top: 0.5rem; }
        .suggest-lab { font-size: 0.75rem; color: var(--muted); margin-bottom: 0.4rem; }
        .suggest-row { display: flex; flex-wrap: wrap; gap: 0.5rem; }
        .sg {
            display: inline-flex; align-items: center; gap: 0.5rem; cursor: pointer;
            border: 1px solid var(--accent); background: var(--card-bg); color: var(--text);
            border-radius: 999px; padding: 0.35rem 0.75rem; font-size: 0.82rem;
        }
        .sg:hover { background: var(--hover-bg); }
        .sg .kind { font-size: 0.65rem; font-weight: 700; text-transform: uppercase; color: var(--link); }
        .sg .nm { font-weight: 600; }
        .sg .basis { color: var(--muted); font-size: 0.75rem; }
        .sg--weak { border-color: var(--input-border); }
        .sg--weak .kind { color: var(--muted); }
        /* Cel nieaktywny: widoczny, ale nieklikalny — jawna informacja
           "dlaczego nie ma zwykłej podpowiedzi" zamiast ciszy. */
        .sg--dead {
            border-color: var(--warn-line); background: var(--inactive-bg); cursor: not-allowed; opacity: 0.85;
        }
        .sg--dead .kind, .sg--dead .basis { color: var(--warn); }

        /* ===== Kreator kierownika (Runda 2) — invoice_detail.html, tylko
           Role.KIEROWNIK. Tokeny z :root powyżej, nie paleta z pliku
           referencyjnego (branding per-firma na --accent). ===== */
        .wizard { background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius-card); margin-bottom: 1rem; }   /* płaska jak .card */
        .wiz-head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding: 1rem 1.25rem; border-bottom: 1px solid var(--border); }
        .wiz-head h2 { margin: 0; font-size: 1rem; font-weight: 650; }
        .wiz-count { font-size: 0.8rem; color: var(--muted); font-weight: 600; white-space: nowrap; }
        .wiz-bar { display: flex; gap: 4px; margin-top: 0.5rem; }
        .wiz-bar i { height: 4px; flex: 1; border-radius: 2px; background: var(--border); font-style: normal; }
        .wiz-bar i.on { background: var(--accent); }
        .wiz-bar i.fix { background: #92400e; }

        /* Krok 8.4 (wariant A, decyzja właściciela 24.09.2026): `.step-num`
           przeniesiony w znaczniku do wnętrza `.step-title` (pierwsze dziecko),
           żeby na telefonie stanął przed tytułem w tym samym wierszu (patrz
           767px niżej). Na szerokim ekranie ma wyglądać jak dotąd.

           PRÓBA W PRZEPŁYWIE (float:left + ujemny margines) ODRZUCONA po
           pomiarze: żeby float w pełni się mieścił, kontener musi go
           „zamknąć" (`clear`), a to wymusza na `.step-title` wysokość
           MINIMUM 28px — więcej niż jednowierszowy tytuł potrzebuje sam z
           siebie (~19px). Różnica kilku px na KAŻDYM kroku kumulowała się w
           przesunięcie całej karty w dół (złapane zrzutami 1920/1536px —
           patrz raport). Naprawa przez ujemne marginesy PIONOWE wracałaby
           do dokładnie tego ryzyka, którego ten wariant miał unikać
           (nachodzenie przy zmianie odstępów). `position:absolute` zamiast
           tego — poza przepływem, ale bez wpływu na wysokość wiersza, więc
           bez tego efektu ubocznego. Kontrolne pomiary: 1920/1536px z
           najdłuższym tytułem kroku (krok 4, "ponownie") — znacznik nie
           nachodzi na tekst (patrz raport). */
        .step { position: relative; padding: 1.1rem 1.25rem; border-bottom: 1px solid var(--border); }
        .step:last-child { border-bottom: 0; }
        .step-num {
            position: absolute; top: 1.1rem; left: 1.25rem;
            width: 28px; height: 28px; border-radius: 50%; display: grid; place-items: center;
            font-size: 0.78rem; font-weight: 700; background: var(--th-bg); color: var(--muted);
        }
        .step--now .step-num { background: var(--accent-fill, var(--accent)); color: var(--accent-fg); }
        .step--done .step-num { background: var(--hover-bg); color: var(--link); border: 1px solid var(--accent); }
        .step--fix .step-num { background: #92400e; color: #fff; }
        .step--fix .step-title { font-weight: 650; }
        .step-body { min-width: 0; padding-top: 0.1rem; padding-left: calc(32px + 0.85rem); }
        .step-title { font-size: 0.95rem; font-weight: 650; }
        .step--locked .step-title { color: var(--muted); font-weight: 600; }
        /* Przerywana obwódka, nie samo szare kółko: domyślny `.step-num` jest
           już szary, więc bez tego krok ZABLOKOWANY i krok BEZ STANU wyglądały
           identycznie. Kreska mówi „to jeszcze nie tutaj", a nie „to puste". */
        .step--locked .step-num { background: transparent; border: 1px dashed var(--input-border); }
        .step-lead { font-size: 0.85rem; color: var(--muted); margin-top: 0.25rem; }
        .step-sum { font-size: 0.85rem; color: var(--muted); margin-top: 0.2rem; }
        .step-sum b { color: var(--text); font-weight: 600; }
        .step-sum a { color: var(--link); font-weight: 600; text-decoration: none; margin-left: 0.5rem; }
        .step-sum a:hover { text-decoration: underline; }
        .step-sum .sep { margin: 0 0.4rem; color: var(--muted); }
        .step-content { margin-top: 0.85rem; }

        /* Poprawka kroku 3 (deploy/ux/poprawka-krok3.html): układ PIONOWY, nie
           siatka dwukolumnowa — opcje mają różną wysokość/złożoność (jedna
           niesie pole pliku), obok siebie zawsze wyglądały krzywo, a grid bez
           min-width:0 nie kurczy się poniżej szerokości input[type=file] i
           rozpycha kartę poza jej granice. */
        /* Stan wybrany liczony z DOM-u przez :has(), NIE z klasy .selected
           dopisywanej warunkowo w szablonie dla każdej etykiety osobno — tamto
           dawało dwie NIEZALEŻNE decyzje, które przy braku wspólnego name=""
           (patrz radios w _wizard.html) potrafiły obie wyjść "prawda" naraz.
           .selected zostaje jako fallback WYŁĄCZNIE dla przeglądarek bez
           :has(), dopisywany przez JS tylko po CSS.supports(...) === false
           (patrz _wizard.html) — nigdy renderowany przez serwer. */

        .flabel { font-size: 0.8rem; font-weight: 600; color: var(--muted); display: block; margin-bottom: 0.35rem; }
        .wiz-actions { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; margin-top: 1rem; }
        /* "To nie moja faktura" — czerwona akcja negatywna, ale tekstowa: nigdy
           jako pełny przycisk, nigdy jako jedyny widoczny element (zadanie,
           sekcja "Hierarchia przycisków"). Ten sam wzorzec co .link-btn,
           tylko czerwień zamiast --muted. */
        .link-btn-danger { background: none; border: none; padding: 0; margin: 0; cursor: pointer; font-size: 0.85rem; color: #dc2626; text-decoration: underline; }
        .link-btn-danger:hover { color: #b91c1c; }
        .wiz-hint { font-size: 0.78rem; color: var(--muted); margin-top: 0.6rem; }

        .wiz-todo { background: #fef3c7; border: 1px solid #eddfae; color: #92400e; border-radius: 6px; padding: 0.75rem 0.9rem; font-size: 0.85rem; margin-top: 0.9rem; }
        .wiz-todo b { font-weight: 700; }
        .wiz-todo ul { margin: 0.4rem 0 0; padding-left: 1.2rem; }

        .wiz-info { background: var(--hover-bg); border: 1px solid var(--border); border-radius: 6px; padding: 0.75rem 0.9rem; font-size: 0.85rem; margin-top: 0.9rem; }

        /* Krok 8.2: .m-fixed owija ISTNIEJĄCY .wiz-todo (gdy występuje) + .wiz-actions
           danego kroku kreatora — nie kopiuje przycisków, tylko przenosi kontener
           (dokładnie jeden na stronę, bo tylko jeden krok jest .step--now naraz).
           Na szerokim ekranie ZERO zmiany — .m-fixed to zwykły, niestylowany <div>
           (żadnych reguł poza @media 767px, sekcja POWŁOKA niżej), więc marginesy
           .wiz-todo/.wiz-actions ZOSTAJĄ bez zmian na desktopie; reset margin-top
           (gap paska zastępuje odstęp) jest TYLKO w tej samej sekcji 767px, przy
           regułach pozycjonujących. */

        /* ===== Krok 4 (deploy/ux/krok4-spojnosc.html) — spójność z krokiem 2
           i widokiem po wysyłce. Zastąpiła `.wiz-review` (tabelka
           „Kontrakt/Numer księgowy/Twoja część"): ZGŁOSZONE PO WDROŻENIU,
           że powielała to, co mówią już zwinięte kroki 1 i 2 wyżej — jeden
           wiersz zbiorczy zamiast tabelki. */
        .headline {
            display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap;
            padding: 0.7rem 0.85rem; background: var(--hover-bg); border-radius: 8px;
            font-size: 0.86rem; margin-bottom: 0.9rem;
        }
        .headline .k { color: var(--muted); font-size: 0.78rem; font-weight: 600; }
        .headline .v { font-weight: 650; }
        .headline .r { margin-left: auto; font-weight: 700; white-space: nowrap; }
        .headline .num2 { font-family: ui-monospace, Menlo, monospace; font-weight: 700; }

        /* Nagłówek grupy kontraktowej w kroku 4 — przy KILKU celach, jeden na
           kontrakt, złączony wizualnie z listą pod nim (`+ .poz-lista`
           niżej) w jedną ramkę. Numer księgowy TU, nie osobnym wierszem — stoi
           przy celu, którego dotyczy (patrz `.section-header h2 .num2` w
           widoku po wysyłce — ten sam numer, inne miejsce, bo tam jest jeden
           cel na całą kartę, nie jeden na kontrakt). */
        .cgrouphead {
            display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap;
            padding: 0.6rem 0.8rem; background: var(--th-bg); border: 1px solid var(--border);
            border-radius: 8px 8px 0 0; border-bottom: 0; font-size: 0.85rem;
        }
        .cgrouphead .kind { font-size: 0.66rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--link); }
        .cgrouphead .nm { font-weight: 650; }
        .cgrouphead .k { color: var(--muted); font-size: 0.78rem; font-weight: 600; }
        .cgrouphead .num2 { font-family: ui-monospace, Menlo, monospace; font-weight: 700; }
        .cgrouphead .r { margin-left: auto; color: var(--muted); font-size: 0.8rem; white-space: nowrap; }
        .cgrouphead + .poz-lista { border-top-left-radius: 0; border-top-right-radius: 0; margin-top: -1px; }

        /* „Uwaga dla księgowości" jako blok, nie goły odnośnik — adresat MUSI
           je zobaczyć. Wyróżniona bursztynem po wypełnieniu (`--filled`),
           ta sama paleta co `.wiz-todo`. */
        .note-block { border: 1px dashed var(--input-border); border-radius: 8px; padding: 0.7rem 0.85rem; margin-top: 0.9rem; }
        .note-block .t { font-size: 0.85rem; font-weight: 650; display: flex; align-items: center; gap: 0.5rem; }
        .note-block .t a { color: var(--link); text-decoration: none; font-size: 0.8rem; margin-left: auto; font-weight: 600; }
        .note-block .d { font-size: 0.78rem; color: var(--muted); margin-top: 0.25rem; }
        .note-block--filled { border-style: solid; border-color: #eddfae; background: #fef3c7; }
        .note-block--filled .t { color: #92400e; }
        .note-block--filled .t a { color: #92400e; }
        .note-block--filled .v {
            font-size: 0.85rem; color: var(--text); margin-top: 0.35rem;
            background: var(--card-bg); border-radius: 6px; padding: 0.5rem 0.7rem; white-space: pre-wrap;
        }
        .note-block textarea { margin-top: 0.6rem; }

        .wiz-poslist { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
        .wiz-posrow { display: flex; align-items: center; gap: 0.75rem; padding: 0.7rem 0.9rem; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
        .wiz-posrow:last-child { border-bottom: 0; }
        .wiz-posrow .n { color: var(--muted); font-size: 0.78rem; width: 14px; flex: none; }
        .wiz-posrow .nm { font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1 1 160px; }
        .wiz-posrow .amt { color: var(--muted); font-size: 0.8rem; white-space: nowrap; }
        .wiz-posrow-detail { flex: 1 1 100%; padding-top: 0.6rem; }

        .items-preview { margin-top: 1rem; background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius-card); padding: 1rem 1.25rem; }   /* płaska jak .card */
        .items-preview h3 { margin: 0 0 0.25rem; font-size: 0.95rem; font-weight: 650; }

        /* Panel podglądu pozycji w kroku 3, tryb "cała faktura": kierownik
           dotąd opisywał zakup, nie widząc co na fakturze jest. Tylko do
           odczytu, zwijany (bez tego długa faktura spycha pole opisu poza
           ekran). */
        .peek { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; margin-bottom: 1.1rem; }
        .peek > summary { list-style: none; cursor: pointer; padding: 0.6rem 0.85rem; background: var(--th-bg); display: flex; align-items: center; gap: 0.6rem; font-size: 0.85rem; font-weight: 600; }
        .peek > summary::-webkit-details-marker { display: none; }
        .peek > summary .caret { transition: transform 0.15s ease; color: var(--muted); flex: none; }
        .peek[open] > summary .caret { transform: rotate(90deg); }
        .peek > summary .cnt { margin-left: auto; font-weight: 600; color: var(--muted); font-size: 0.78rem; }
        .peek-body { padding: 0.2rem 0.85rem 0.7rem; overflow-x: auto; }
        .peek table { width: 100%; border-collapse: collapse; font-size: 0.85rem; min-width: 420px; }
        .peek th { font-size: 0.68rem; letter-spacing: 0.05em; text-transform: uppercase; color: var(--muted); font-weight: 700; text-align: right; padding: 0.5rem 0.5rem; border-bottom: 1px solid var(--border); }
        .peek th.l { text-align: left; }
        /* font-size wprost — ta sama pułapka co w tabeli faktur (globalne `th, td`
           0.92rem bije dziedziczone 0.85rem z `.peek table`). */
        .peek td { padding: 0.55rem 0.5rem; text-align: right; border-bottom: 1px solid var(--border); font-size: 13.2px; }
        .peek tr:last-child td { border-bottom: 0; }
        .peek td.l { text-align: left; }
        .peek td.n { color: var(--muted); width: 1.4rem; }
        .peek td .nm { font-weight: 600; }
        .peek tfoot td { font-weight: 700; border-top: 1px solid var(--input-border); border-bottom: 0; }

        /* ===== Runda 4 — pasek stanu nad kreatorem (jeden komponent, trzy
           warianty: wysłana/zaakceptowana/do poprawy), blok odbioru domknięty,
           kłódka kroków 1-2 w stanie "do poprawy". ===== */
        .statebar { display: flex; align-items: flex-start; gap: 0.75rem; border-radius: 8px; padding: 0.85rem 1.1rem; margin-bottom: 1rem; border: 1px solid; }
        .statebar .txt { min-width: 0; flex: 1; }
        .statebar .lead { font-weight: 650; font-size: 0.9rem; }
        .statebar .sub { font-size: 0.8rem; margin-top: 0.2rem; opacity: 0.9; }
        .statebar .when { font-size: 0.78rem; white-space: nowrap; opacity: 0.75; flex: none; }
        .statebar--wait { background: var(--th-bg); border-color: var(--border); color: var(--text); }
        .statebar--wait .sub { color: var(--muted); }
        .statebar--ok { background: #d1fae5; border-color: #a7e6c8; color: #065f46; }
        .statebar--ok .sub { color: #065f46; opacity: 0.85; }
        .statebar--fix { background: #fef3c7; border-color: #eddfae; color: #92400e; }

        .comment { background: var(--card-bg); border: 1px solid #eddfae; border-radius: 6px; padding: 0.6rem 0.8rem; margin-top: 0.6rem; color: var(--text); font-size: 0.85rem; }
        .comment .who { font-size: 0.75rem; color: var(--muted); font-weight: 600; margin-bottom: 0.25rem; }

        .padlock { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.75rem; color: var(--muted); font-weight: 600; }

        .receipt { display: flex; gap: 0.6rem; align-items: flex-start; border: 1px solid var(--accent); background: var(--hover-bg); border-radius: 6px; padding: 0.6rem 0.8rem; margin-top: 0.6rem; }
        .receipt--manual { border-color: #e3dcc4; }
        .receipt .lead { font-size: 0.85rem; font-weight: 600; }
        .receipt .sub { font-size: 0.78rem; color: var(--muted); margin-top: 0.15rem; }
        /* Ikona — dodana w kroku 3 dla bloku potwierdzenia POKAZANEGO OD RAZU po
           zaznaczeniu „biorę odbiór na siebie" (ZGŁOSZONE PO WDROŻENIU: dawniej
           checkbox się odhaczał i nic więcej). `_wizard_receipt_summary.html`
           (podsumowanie w kroku 4/po wysłaniu) nie ma ikony i działa dalej bez
           zmian — sam `<div>` wewnątrz nie robi za flex item bez `.ic`. */
        .receipt .ic { flex: none; color: var(--link); margin-top: 0.1rem; }
        /* Miniatura skanu — zwykły <img> wskazujący na podgląd, bez żadnej
           biblioteki. Kierownik ma od razu zobaczyć, czy zdjęcie z telefonu
           jest czytelne. */
        .sl-thumb {
            width: 2.2rem; height: 2.2rem; object-fit: cover; border-radius: 4px;
            border: 1px solid var(--border);
        }
        .filechip { display: inline-flex; align-items: center; gap: 0.4rem; margin-top: 0.4rem; background: var(--card-bg); border: 1px solid var(--border); border-radius: 6px; padding: 0.3rem 0.55rem; font-size: 0.78rem; text-decoration: none; color: var(--text); font-weight: 600; }
        .filechip:hover { border-color: var(--accent); color: var(--link); }
        .filechip .sz { color: var(--muted); font-weight: 400; }
        /* Kwadrat ikony dla PDF-a — ten sam wzorzec co `.fic` w kroku 3, żeby
           plik wyglądał tak samo na obu ekranach jednego przepływu. */
        .filechip .fi {
            width: 1.35rem; height: 1.35rem; border-radius: 5px; display: grid; place-items: center;
            flex: none; background: var(--hover-bg); color: var(--link);
        }

        .quoted { background: var(--th-bg); border-left: 3px solid var(--border); padding: 0.55rem 0.8rem; border-radius: 0 6px 6px 0; margin-top: 0.5rem; font-size: 0.85rem; white-space: pre-wrap; }

        @media (max-width: 767px) {
            /* Cel dotknięcia >=44px (zadanie, sekcja Mobile) — kierownik często
               na telefonie w terenie. Próg poszerzony z 700 na 767px w kroku 8.2
               (ten sam próg co .m-top/.m-fixed niżej) — sticky dolny pasek na
               główny przycisk kroku, dawniej tu celowo pominięty, jest teraz
               tym paskiem: bez wyrównania progów przycisk w pasku między 701 a
               767px miałby gwarancję 44px z jednej reguły, ale nie z tej. */
            /* `inline-flex`, nie samo `min-height`: na kotwicy (`<a class=
               "btn-outline">Zapisz i wyjdź`) wysokość rozciągała ramkę, ale
               tekst zostawał przyklejony do góry — wyglądało to jak pusty,
               za wysoki prostokąt z podpisem u szczytu. */
            .step .wiz-actions .btn, .step .wiz-actions button, .step .wiz-actions .btn-outline {
                min-height: 44px; display: inline-flex; align-items: center; justify-content: center;
                /* `border-box`, inaczej `min-height` dolicza się DO paddingu i
                   z celu 44 px robi się 59 px — przycisk odstaje od reszty. */
                box-sizing: border-box;
            }
            .mode-card { min-height: 44px; }
        }

        /* ===== Modal (Runda 3) — pierwszy prawdziwy modal w tej aplikacji,
           reużywalny komponent na data-atrybutach (data-modal-open/-close),
           patrz JS w invoices/_wizard.html. Generyczny, nie tylko dla
           kreatora — kolejne funkcje mogą go reużyć bez nowego CSS. ===== */
        .modal-overlay { position: fixed; inset: 0; background: rgba(17, 24, 39, 0.55); display: flex; align-items: center; justify-content: center; padding: 1rem; z-index: 1000; }
        .modal { background: var(--card-bg); border-radius: 8px; box-shadow: var(--card-shadow); max-width: 460px; width: 100%; padding: 1.25rem 1.5rem; max-height: 90vh; overflow-y: auto; }
        .modal h3 { margin: 0 0 0.5rem; font-size: 1.05rem; }
        .modal p { margin: 0 0 0.9rem; }
        .modal .field-row:last-of-type { margin-bottom: 0; }
        .modal-actions { display: flex; justify-content: flex-end; gap: 0.6rem; margin-top: 1.1rem; }
        /* Okno „Przygotuj przelew" (deploy/25) — szersze od bazowych 460px
           (28-znakowy numer rachunku z odstępami + przycisk kopiowania
           łamały się w dwie linie) i jedyny modal z wydzieloną głową/stopką:
           `display:flex;flex-direction:column` + `overflow:hidden` na
           kontenerze, `overflow-y:auto` tylko na `.modal-body` — głowa i
           stopka (z przyciskiem „Oznacz jako opłaconą") zostają widoczne przy
           przewijaniu długiej listy rachunków, zamiast znikać razem z resztą.
           Scoped pod `.modal--wide`, nie pod gołe `.modal`, żeby prostsze
           modale potwierdzeń (dodanie celu, odrzucenie przydziału) zostały
           przy dotychczasowym, płaskim wyglądzie. */
        .modal--wide { max-width: 640px; padding: 0; display: flex; flex-direction: column; overflow: hidden; }
        .modal--wide .modal-head { display: flex; align-items: center; gap: 0.75rem; padding: 0.9rem 1.25rem; border-bottom: 1px solid var(--border); background: var(--th-bg); flex: none; }
        .modal--wide .modal-head h3 { margin: 0; font-size: 1rem; }
        .modal--wide .modal-body { padding: 1.1rem 1.25rem; overflow-y: auto; }
        .modal--wide .modal-body .field-row:last-of-type { margin-bottom: 0; }
        /* Etykiety pól — małe wersaliki wygaszonym kolorem, żeby nie
           konkurowały wagą z wartością pod nimi (jak w referencji UX).
           Scoped pod `.modal--wide .field-row`, NIE globalnie: te same klasy
           w `_wizard.html` (np. „Wiesz, do kogo powinna trafić?") to pełne
           zdania — wersaliki zrobiłyby z nich coś nieczytelnego. */
        /* `:not(.acc-option)` konieczne — `.acc-option` to TEŻ `<label>`
           (wiersz wyboru rachunku), a nie podpis pola: bez wykluczenia
           `display:block` stąd wywracał jego `display:flex`, a
           `text-transform:uppercase` dziedziczył się na numer/opis rachunku. */
        .modal--wide .field-row label:not(.acc-option) { display: block; text-transform: uppercase; letter-spacing: 0.04em; font-weight: 700; font-size: 0.68rem; color: var(--muted); margin-bottom: 0.35rem; }
        .modal--wide .modal-foot { display: flex; align-items: center; gap: 0.65rem; flex-wrap: wrap; padding: 0.85rem 1.25rem; border-top: 1px solid var(--border); background: var(--th-bg); flex: none; }
        /* Kontekst faktury nad polami: górny wiersz numer/sprzedawca/termin
           (termin dosunięty do prawej), dolny wiersz numer KSeF + linki do
           źródła (też do prawej) — dwie odrębne linijki zamiast jednego
           zbitego akapitu. */
        .przelew-ctx { padding-bottom: 0.8rem; margin-bottom: 1rem; border-bottom: 1px solid var(--border); }
        .przelew-ctx-top { display: flex; align-items: baseline; gap: 0.65rem; flex-wrap: wrap; font-size: 0.85rem; }
        .przelew-ctx .fnum { font-family: ui-monospace, "Cascadia Mono", "Segoe UI Mono", Menlo, monospace; font-size: 0.75rem; background: var(--th-bg); border: 1px solid var(--border); padding: 0.2rem 0.5rem; border-radius: 5px; }
        .przelew-ctx .seller { font-weight: 650; min-width: 0; }
        .przelew-ctx .due { margin-left: auto; color: var(--muted); white-space: nowrap; }
        .przelew-ctx-bottom { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin-top: 0.5rem; }
        .przelew-ctx .ksef-wrap { display: flex; align-items: baseline; gap: 0.4rem; min-width: 0; }
        .przelew-ctx .ksef-label { font-size: 0.65rem; letter-spacing: 0.04em; text-transform: uppercase; font-weight: 700; color: var(--muted); flex: none; }
        .przelew-ctx .ksef-val { font-family: ui-monospace, "Cascadia Mono", "Segoe UI Mono", Menlo, monospace; font-size: 0.72rem; color: var(--muted); word-break: break-all; }
        .przelew-ctx .ctx-links { margin-left: auto; display: flex; align-items: center; gap: 0.6rem; flex: none; }
        .przelew-ctx .ctx-links a { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.78rem; font-weight: 600; color: var(--link); text-decoration: none; white-space: nowrap; }
        .przelew-ctx .ctx-links a:hover { text-decoration: underline; }
        .przelew-ctx .ctx-links a.sec { color: var(--muted); font-weight: 500; }
        .przelew-ctx .ctx-links a.sec:hover { color: var(--text); }
        .przelew-ctx .ctx-links .sep { color: var(--border); }
        /* Pola „wartość + Kopiuj" w oknie przelewu. Wartość wygląda jak input
           tylko-do-odczytu (księgowa przekleja ją do banku), ale jest divem —
           nie da się jej przypadkiem nadpisać. Ramka `--input-border` (nie
           `--border`, subtelniejsza) — inaczej pole ledwo odróżniało się od
           tła modala. */
        .copy-row { display: flex; align-items: center; gap: 0.5rem; }
        .copy-row .copy-val { flex: 1; padding: 0.5rem 0.65rem; background: var(--th-bg); border: 1px solid var(--input-border); border-radius: 6px; font-size: 0.88rem; overflow-wrap: anywhere; }
        .copy-row input.copy-val { background: var(--input-bg); width: auto; }
        .mono { font-family: ui-monospace, "Cascadia Mono", "Segoe UI Mono", Menlo, monospace; }
        /* Przyciski kopiowania — ikona, nie tekst (referencja UX): kwadrat
           `.copy-icon` przy każdym polu, mały `.copy-mini` przy numerze KSeF
           i NIP-ie w kontekście/hincie. Stan „skopiowano" (klasa `.copied`,
           dodawana w JS w base.html) zamienia oba warianty na tę samą
           zieloną pigułkę z tekstem — inaczej nie było wiadomo, czy kliknięcie
           coś zrobiło. */
        .copy-btn { flex: none; border: 1px solid var(--input-border); border-radius: 6px; background: var(--card-bg); color: var(--muted); cursor: pointer; padding: 0; display: inline-grid; place-items: center; vertical-align: middle; }
        .copy-btn:hover { color: var(--link); border-color: var(--accent); }
        .copy-btn svg { display: block; }
        .copy-btn.copy-icon { width: 44px; align-self: stretch; }
        .copy-btn.copy-icon svg { width: 15px; height: 15px; }
        .copy-btn.copy-mini { width: 26px; height: 24px; }
        .copy-btn.copy-mini svg { width: 11px; height: 11px; }
        .copy-btn.copied { width: auto; height: auto; align-self: auto; padding: 0 0.65rem; font-size: 0.68rem; font-weight: 700; white-space: nowrap; color: var(--ok); border-color: var(--ok-line); background: var(--ok-bg); }
        /* Wybór rachunku przy fakturze z kilkoma numerami — radio na wiersz. */
        .acc-option { display: flex; align-items: flex-start; gap: 0.55rem; padding: 0.45rem 0.6rem; border: 1px solid var(--border); border-radius: 6px; margin-bottom: 0.4rem; cursor: pointer; }
        .acc-option input[type="radio"] { width: auto; margin-top: 0.2rem; }
        .acc-option .acc-desc { display: block; font-size: 0.78rem; color: var(--muted); }
        /* Próg białej listy VAT — jedyny hint z konsekwencją
           podatkową, więc dostaje wagę `.warnbox` (ta sama paleta co inne
           ostrzeżenia w apce), a nie kolejną linijkę zwykłego `.hint`. */
        .field-row .warnbox { margin-top: 0.5rem; }
        /* `.hint` bez zakresu (poza `.receipt-row`/`.sub-actions`, jedyne
           dotąd miejsca z tą klasą) dziedziczył domyślny rozmiar tekstu —
           stąd zbyt duża czcionka pod polami w tym modalu. */
        .modal--wide .hint { font-size: 0.76rem; color: var(--muted); margin-top: 0.3rem; }
        .modal--wide .hint a { color: var(--link); font-weight: 600; text-decoration: none; }
        .modal--wide .hint a:hover { text-decoration: underline; }

        @media (max-width: 700px) {
            .wiz-review-row { grid-template-columns: 1fr; gap: 0.2rem; }
        }

        .field-row { margin-bottom: 0.9rem; }
        .field-row label { display: block; font-weight: 600; margin-bottom: 0.25rem; font-size: 0.85rem; color: var(--muted); }
        /* Siatka formularza "Dodaj usługę" (widok Usługi) — 4 kolumny, rząd 1
           to 4 pojedyncze pola, rząd 2 to opis (span 3) + kwota (span 1,
           dopełnia ten sam rząd automatycznie) — stąd proporcja ok. 3:1 bez
           osobnego szablonu kolumn dla drugiego rzędu. .field-row wewnątrz
           bez zmian, tylko inny kontener wokół niego. */
        .service-form-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0 0.75rem; }
        .service-form-description { grid-column: span 3; }
        .two-col-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
        input, select, textarea {
            width: 100%; padding: 0.45rem 0.6rem; border: 1px solid var(--input-border);
            border-radius: var(--radius-field); font-size: 13.2px; box-sizing: border-box;   /* makieta .inp */
            font-family: inherit; background: var(--input-bg); color: var(--text);
        }
        button, .btn {
            /* Poprawka po pełnym porównaniu z makietą (21.09.2026): wysokość/padding/waga/
               rozmiar/promień/odstęp z sekcji 0 (.btn: 13,4px / 650 / wys. 36 / 0 15px / gap
               6px). inline-flex + wysokość WPROST (nie z paddingu) — jak makieta, bo
               wysokość z samego paddingu pionowego zależy od line-height kroju i nie daje
               powtarzalnej liczby pikseli. */
            display: inline-flex; align-items: center; justify-content: center; gap: 6px;
            height: 36px; padding: 0 15px; border: 1px solid transparent; border-radius: var(--radius-field);
            font-size: 13.4px; font-weight: 650; cursor: pointer; text-decoration: none; margin-right: 0.5rem;
            /* <button> NIE dziedziczy kroju (arkusz przeglądarki: font: -webkit-small-control)
               — bez tej linii przyciski stały w Arialu, gdy reszta strony była już w Segoe UI
               (znalezisko z 21.09.2026, aktualne niezależnie od tego, który krój jest na body
               — zmierzone najpierw przy Interze, ten sam mechanizm dotyczy Segoe UI). Makieta:
               .btn ma font: inherit. */
            font-family: inherit;
        }
        /* Bez tego zablokowany przycisk (np. "Dalej" w kreatorze, dopóki
           opisy niekompletne) wygląda identycznie jak aktywny — użytkownik
           klika, nic się nie dzieje, i nie wie czy to jego pomyłka czy błąd
           aplikacji. Wyłapane wizualnie, nie przez test (atrybut `disabled`
           w HTML jest poprawny, wygląd nie). */
        button:disabled, .btn:disabled {
            opacity: 0.5; cursor: not-allowed;
        }
        /* Przycisk GŁÓWNY dostaje kolor SOLID, nie przezroczystość: akcent
           w 50% nadal czyta się jak "zielony, tylko inny", a nie jak
           "nieaktywny" — a to jedyny sygnał, że kliknięcie nic nie da.
           Kolory z referencji (deploy/kreator-uproszczony-pozycje.html). */
        .btn-primary:disabled {
            background: #cdd6d1; color: #f2f6f4; opacity: 1;
        }
        /* Referencyjne makiety nie mają trybu ciemnego, a #cdd6d1 na tle
           #111827 jest JAŚNIEJSZY od otoczenia i wraca do wyglądu aktywnego. */
        @media (prefers-color-scheme: dark) {
            :root:not([data-theme="light"]) .btn-primary:disabled {
                background: var(--th-bg); color: var(--muted); border: 1px solid var(--border);
            }
        }
        :root[data-theme="dark"] .btn-primary:disabled {
            background: var(--th-bg); color: var(--muted); border: 1px solid var(--border);
        }
        .btn-primary { background: var(--accent-fill, var(--accent)); color: var(--accent-fg); }
        .btn-success { background: #059669; color: #fff; }
        .btn-danger { background: #dc2626; color: #fff; }
        .btn-secondary { background: #6b7280; color: #fff; }
        .messages { list-style: none; padding: 0; margin: 0 0 1rem; }
        .messages li { padding: 0.6rem 1rem; border-radius: 4px; margin-bottom: 0.5rem; }
        .messages .success { background: #d1fae5; color: #065f46; }
        .messages .error { background: #fee2e2; color: #991b1b; }
        .filters { margin-bottom: 0.75rem; padding: 0.6rem 0.75rem; border-radius: var(--radius-card); }   /* dziedziczyła 8px z .card na .filters.card w markupie; jawnie na wypadek zmiany znaczników */
        .filters label {
            font-weight: 700; font-size: 0.68rem; color: var(--muted);
            text-transform: uppercase; letter-spacing: 0.04em;
        }
        .filters select, .filters input:not(.grow) { width: auto; min-width: 150px; border-radius: 6px; }
        .date-range-sep { color: var(--muted); flex-shrink: 0; }
        /* Runda 30, krok 4: belka filtrów w JEDNYM wierszu (zamiast 300,5 px
           zawsze rozwiniętej siatki — §11.1 rozpoznania rundy 30, największa
           pojedyncza wygrana kroku: 2/1/0 wierszy listy widocznych bez
           przewijania przy 100/125/150%). Reszta filtrów pod "Więcej
           filtrów" (<details>, natywny — bez JS do samego zwijania). */
        .filters-row-1, .filters-row-1b { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
        .filters-row-1b { margin-top: 0.5rem; }
        .filters-row-1 .grow { flex: 1 1 180px; min-width: 0; }
        .filters-row-1 input, .filters-row-1 select, .filters-row-1b select { height: 34px; }
        .filters-row-1 .period-label { font-size: 0.75rem; white-space: nowrap; }
        /* Krok 8.3, usterka 2.5.2 (po [ODCZYT]): przyczyna — 302px treści na
           telefonie mieści etykietę "Okres" obok pola "Numer" (nie obok dat),
           a dwa pola daty (min-width 150px + gap) NIE mieszczą się razem
           (150+150+8 > 302), więc każde wchodzi na WŁASNĄ linię, bez etykiety
           obok — stąd wrażenie dwóch pustych, nieopisanych kontrolek. Prosta
           poprawka CSS (bez zmiany etykiet/znaczników): etykieta na WŁASNEJ
           linii nad parą pól (ten sam wzorzec flex-basis:100% co
           .filters-row-2 niżej), pola dzielą dostępną szerokość PO RÓWNO
           zamiast sztywnego 150px, separator "–" odsłonięty (dotąd chowany
           regułą sprzed rundy 30 kroku 4, .date-range-sep, @media 640px). */
        @media (max-width: 767px) {
            .filters-row-1 .period-label { flex-basis: 100%; }
            .filters-row-1 input[type="date"] { flex: 1 1 0; min-width: 0; }
            .filters-row-1 .date-range-sep { display: inline; }
        }
        .filters-row-1b .sp { flex: 1 1 auto; }
        /* `<details>` bez własnego pudełka: jego dzieci (`<summary>` I treść po
           rozwinięciu) stają się bezpośrednimi elementami flexa .filters-row-1b —
           `<summary>` siedzi W WIERSZU 2 (obok Etapu/Oddziału), a `.filters-row-2`
           (rozwinięta treść) dostaje flex-basis: 100% niżej, więc w kolapsie nie
           zajmuje miejsca, a po rozwinięciu łamie się na własną linię POD wierszem
           2 zamiast wpychać się między jego elementy. */
        .filters-row-1b > details.filters-more { display: contents; }
        .quick { display: inline-flex; gap: 0.3rem; }
        .quick button {
            height: 26px; padding: 0 0.6rem; font: inherit; font-size: 0.75rem; font-weight: 600;
            border-radius: 999px; border: 1px solid var(--input-border); background: transparent;
            color: var(--muted); cursor: pointer;
        }
        .quick button.on { background: var(--hover-bg); color: var(--link); border-color: var(--link); }
        .filters-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.5rem; }
        .chip {
            display: inline-flex; align-items: center; gap: 0.4rem; height: 26px; padding: 0 0.5rem;
            border-radius: 999px; background: var(--hover-bg); color: var(--text); font-size: 12px; font-weight: 600;   /* makieta .chip */
        }
        .chip a { color: var(--muted); text-decoration: none; font-weight: 700; }
        .chip a:hover { color: var(--danger); }
        .filters-more summary {
            cursor: pointer; font-size: 0.8rem; font-weight: 600; color: var(--muted);
            list-style: none;
        }
        .filters-more summary::-webkit-details-marker { display: none; }
        .filters-more summary::before { content: "▸ "; }
        .filters-more[open] summary::before { content: "▾ "; }
        /* flex-basis: 100% na elemencie wewnątrz `.filters-row-1b` (flex-wrap: wrap)
           to ustalony punkt łamania — technika "pełna szerokość wymusza nową
           linię" — nie przypadkowe zawijanie zależne od tego, ile miejsca akurat
           zostało po innych elementach wiersza 2. */
        .filters-row-2 { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: flex-end; flex-basis: 100%; margin-top: 0.6rem; }
        .filters-row-2 > div { display: flex; flex-direction: column; gap: 0.3rem; min-width: 150px; }
        /* Usterka #9 rozpoznania rundy 30: `.filters label` (element+klasa,
           specyficzność wyższa niż sama `.check-inline`) bił font-size i
           text-transform etykiety checkboxa — WERSALIKI 10,88px zamiast
           13,6px, "zamiast obok pola". Podniesienie specyficzności DO
           `.filters .check-inline` (dwie klasy) wygrywa jawnie, zamiast
           polegać na kolejności reguł w arkuszu. */
        .filters .check-inline {
            text-transform: none; font-weight: 500; font-size: 0.85rem; letter-spacing: normal;
        }
        /* Runda 30, krok 8.3: zwijanie panelu filtrów + kart "Faktura"/
           "Płatność" na telefonie (`.m-collapse`, wspólne dla obu). Znacznik
           trójkąta widoczny na KAŻDEJ szerokości (decyzja właściciela: "bez
           zmian na szerokim" znaczy stan przy pierwszym wejściu, nie zakaz
           kliknięcia) — inaczej nagłówek sugerowałby, że nic się nie da
           zrobić, choć się da. Otwarcie/zamknięcie steruje `app.js`
           (`syncCollapse`) zgodnie z `matchMedia('(min-width:768px)')`, ten
           sam próg co `.m-top`/`.m-fixed` z kroku 8.2. */
        .m-collapse > summary { cursor: pointer; list-style: none; }
        .m-collapse > summary::-webkit-details-marker { display: none; }
        .m-collapse > summary::after { content: "▸"; margin-left: 6px; color: var(--muted); font-size: 0.75rem; }
        .m-collapse[open] > summary::after { content: "▾"; }
        .m-filters-toggle {
            display: inline-flex; align-items: center; gap: 8px;
            height: 34px; padding: 0 12px; margin-bottom: 0.5rem;
            border: 1px solid var(--input-border); border-radius: 999px;
            font-size: 0.8rem; font-weight: 650; color: var(--text); background: var(--card-bg);
        }
        .m-filters-count {
            display: inline-flex; align-items: center; justify-content: center;
            min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px;
            background: var(--link); color: #fff; font-size: 11px; font-weight: 700;
        }
        /* Plakietki WSZYSTKICH aktywnych filtrów (`all_filter_chips`) —
           widoczne wyłącznie na telefonie, wyłącznie gdy panel jest zwinięty.
           `row2_chips`/.filters-chips (istniejące, wyżej) zostają bez zmian
           i widoczne na desktopie jak dotąd — te dwa mechanizmy się nie
           nakładają: ten tu jest zawsze `display:none` od 768px w górę. */
        .m-filters-chips { display: none; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.5rem; }
        @media (max-width: 767px) {
            .m-filters:not([open]) + .m-filters-chips { display: flex; }
        }
        /* Karty "Faktura"/"Płatność" zwinięte na telefonie — podsumowanie
           w `<summary>` (kwota brutto / termin kolorowany jak na liście).
           Puste `display:none` na desktopie: element w ogóle nie wchodzi
           do flexa `.section-header`, więc nagłówek karty wygląda tak jak
           dziś (poza trójkątem z reguły ogólnej wyżej). `margin-left:auto`
           na telefonie odsuwa hint od tytułu/plakietek zamiast pozwolić
           `justify-content:space-between` rodzica rozstrzelić wszystkie
           elementy równomiernie. */
        .m-collapse-hint { display: none; }
        @media (max-width: 767px) {
            .m-collapse-hint {
                display: inline-flex; align-items: center; margin-left: auto;
                font-weight: 650; font-size: 0.85rem; white-space: nowrap;
            }
        }
        .link-btn {
            background: none; border: none; padding: 0; margin: 0; cursor: pointer;
            font-size: 0.85rem; color: var(--muted); text-decoration: underline;
        }
        .link-btn:hover { color: var(--text); }
        .back-link { display: inline-block; margin-bottom: 1rem; color: var(--link); text-decoration: none; }
        .preview-link-row { margin-top: 0.9rem; font-size: 0.85rem; }
        .muted { color: var(--muted); }
        .inline-form { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
        .inline-form select, .inline-form input { width: auto; min-width: 140px; }
        .cell-ellipsis { max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

        /* ===== Runda 30, krok 4: lista faktur jako tabela =====
           Zastępuje .invoice-row/.col-* (jedyny widok, który ich używał —
           usunięte razem z tym blokiem, §5.1 rozpoznania rundy 30).
           Container query na .invoice-results, NIE media query na oknie:
           zwinięte menu (krok 5) zmienia dostępne miejsce, próg musi to
           widzieć niezależnie od szerokości okna. */
        .invoice-results { container-type: inline-size; }
        /* `overflow: clip`, NIE `hidden` (runda 30, krok 5 — jedyna zmiana w kodzie
           wdrożonym krokiem 4, celowa): `hidden` czyni kartę kontenerem
           przewijania, więc przyklejony nagłówek tabeli (niżej) liczyłby się
           względem karty, nie okna, i nigdy by się nie przykleił. `clip` klipsuje
           zaokrąglone rogi tak samo, kontenera nie tworzy. */
        .tablecard { background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius-card); overflow: clip; }
        table.invoice-table {
            width: 100%; border-collapse: collapse; table-layout: fixed; font-size: 0.85rem;
            box-sizing: border-box;
        }
        table.invoice-table * { box-sizing: border-box; }
        /* font-size TU, nie tylko na <table>: globalne `th, td` (wyżej w arkuszu)
           deklaruje 0.92rem WPROST na komórce, a deklaracja na elemencie bije
           dziedziczenie z `table.invoice-table` — komórki miały 14,72 px zamiast
           13,6, o 12 % więcej niż szerokości kolumn liczone pod 13,6 (przegląd
           produkcyjny kroku 5, zmierzone getComputedStyle). Ta sama pułapka
           naprawiona w `.peek td` niżej.
           line-height LICZBĄ (1.5 = 20,4 px), nie z metryki czcionki. Sama
           line-height NIE ustawia jednak wysokości wiersza: plakietki (.badge,
           .nr-badge) to inline-blocki wyrównane do linii bazowej, dziedziczą
           line-height i podnoszą wiersz skokowo (zmierzone: 43,4 przy 1.4,
           44,7 przy 1.5, 55,1 przy 1.87 — nigdy 44). Dlatego wysokość wiersza
           daje `height: 44px` na <td> z `vertical-align: middle` i zerowym
           paddingiem pionowym — dokładnie tak, jak plik referencyjny
           (`table.list td { height: 44px; padding: 0 10px }`), a plakietki
           w komórkach dostają własną `line-height: 1.2`, żeby nie dziedziczyły
           1.5 z komórki. Zmierzone: 44,0 px w każdym wierszu, w obu motywach. */
        table.invoice-table th, table.invoice-table td {
            padding: 0.55rem 0.65rem; border-bottom: 1px solid var(--border);
            white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
            font-size: 13.2px; line-height: 1.5;
        }
        table.invoice-table td { height: 44px; padding-top: 0; padding-bottom: 0; vertical-align: middle; }
        table.invoice-table td .nr-badge { line-height: 1.2; }   /* .badge ma line-height 1 globalnie (makieta) */
        /* Przyklejony do GÓRY OKNA (runda 30, krok 5): przewija się dokument,
           belka mobilna nie jest przyklejona, więc top: 0 na każdej szerokości.
           Tło konieczne — bez niego wiersze prześwitują przez nagłówek.
           z-index: 2 nad `.stretched-link::after` (z-index: 1): nagłówek
           zasłania nakładki wierszy, klikalność wiersza i Tab bez zmian.
           Sticky na <th>, nie na <thead> — przeglądarki przyklejają komórki,
           nie grupę wierszy. */
        /* Bez wersalików — plik referencyjny: „Nr faktury", 11,2 px, waga 650
           (przegląd produkcyjny kroku 5). Globalne `th` innych tabel bez zmian. */
        table.invoice-table thead th {
            position: sticky; top: 0; z-index: 2; background: var(--card-bg);
            font-size: 0.7rem; font-weight: 650; text-transform: none;
            letter-spacing: normal; color: var(--muted); text-align: left;
            white-space: normal;
        }
        table.invoice-table tbody tr:hover { background: var(--hover-bg); }
        table.invoice-table tbody tr:last-child td { border-bottom: none; }
        table.invoice-table td.r, table.invoice-table th.r { text-align: right; }
        /* Wiersz klikalny przez odnośnik rozciągnięty na CAŁY <tr> — jeden
           realny <a> (w pierwszej komórce), nie onclick na <tr>: lista musi
           dać się przejść Tabem i otworzyć Enterem. <a> nie może wrapować
           <tr> (HTML nieprawidłowy przy prawdziwej tabeli, w odróżnieniu od
           makiety opartej na divach) — stąd "stretched link": pseudoelement
           linku rozciągnięty na najbliższego pozycjonowanego przodka. */
        table.invoice-table tbody tr { position: relative; }
        .stretched-link { text-decoration: none; color: inherit; }
        .stretched-link::after { content: ""; position: absolute; inset: 0; z-index: 1; }
        table.invoice-table thead th a.th-sort {
            color: inherit; text-decoration: none; display: inline-flex; align-items: center; gap: 3px;
        }
        table.invoice-table thead th.sorted { color: var(--text); }
        .sort-arrow { color: var(--link); font-size: 0.85em; }

        /* Szerokości MINIMALNE — jeden zestaw (nie dwa jak w pierwszej wersji
           pliku referencyjnego; drugi, "roomy", zestaw odrzucony w rundzie 30
           krok 4 — jeden zestaw minimalny oddaje nadwyżkę sprzedawcy zamiast
           rozlewać ją po wszystkich kolumnach). Sprzedawca bez `width` w
           <colgroup> bierze resztę automatycznie (table-layout: fixed). */
        /* 184px, nie 150 (poprawka po przeglądzie produkcyjnym, decyzja właściciela
           21.09.2026): rozkład długości numerów na produkcji (353 faktury) —
           43 numery ponad 18 znaków, jedna na osiem; przy tej częstości
           wielokropek przestaje być wyjątkiem, a numer to klucz, po którym
           księgowa porównuje wpis z papierem. Próg zależy od szerokości kolumny
           W PIKSELACH, nie od kroju monospace — 184px trzyma próg ośmiu kolumn
           na 1242px niezależnie od tego, czy przeglądarka rozwiąże `ui-monospace`
           na Menlo/SFMono (macOS), Consolas (Windows) czy krój systemowy Linuksa;
           różnica między nimi przesuwa wyłącznie to, ILE znaków numeru się
           mieści (policzone w stan-techniczny.md), nie sam próg układu. Dłuższe
           numery: wielokropek + `title` na odnośniku wiersza (patrz `.nr-badge`
           i `title` w invoice_list.html — ten SAM odnośnik pokrywa całą komórkę,
           więc `title` na nim jest osiągalny myszą, w odróżnieniu od komórek
           Sprzedawca/Przypisanie/Etap, gdzie nakładka innego odnośnika go
           zasłania — patrz §5 „title nieosiągalny myszą”). */
        .c-nr { width: 184px; }
        /* 104px, nie 92 — zmierzone: "dd.mm.rrrr" (10 znaków, cyfry o stałej
           szerokości w tym stosie fontów) potrzebuje 79,4px, a 92px przy
           obecnym paddingu komórki (border-box, 0,65rem z każdej strony)
           dawało tylko 71,2px treści — ucinało KAŻDĄ datę, nie tylko
           przypadek testowy (zgłoszone w przeglądzie przed odbiorem kroku
           4). 104px → 83,2px treści, z zapasem. 12px dodatkowych wzięte
           z nadwyżki sprzedawcy (bez width w <colgroup>, rośnie/maleje
           automatycznie), nie z innej kolumny stałej. */
        .c-date { width: 104px; }
        .c-amt { width: 120px; }
        td.c-amt { font-weight: 600; }   /* makieta .amt: kwota pogrubiona, cyfry stałe (globalnie na body) */
        .c-due { width: 104px; }
        .c-tgt { width: 180px; }
        /* 142px, nie 118 (poprawka po przeglądzie) — 118px skopiowane z pliku
           referencyjnego, policzone na zmyślonych etykietach makiety, bez
           sprawdzenia, że `.col-rozliczenie` sprzed tej przebudowy miała
           180px Z KOMENTARZEM tłumaczącym dlaczego ("Zwrócona przez
           kierownika"). Granicą nie jest najdłuższa etykieta w ogóle, tylko
           najdłuższa CZĘSTA: etap to zamknięty zbiór kilku wartości,
           odczytywany wzrokiem, nie czytany — "Zaakce···" obok
           "Zaakceptowane" w sąsiednim wierszu wygląda na dwa różne stany,
           a "Zaakceptowane" (100,92px zmierzone) trafia się w połowie
           wierszy, więc musi mieścić się w całości. "Zwrócone przez
           kierownika" (160,69px) to ścieżka WYJĄTKU (jeden status, ten sam
           tekst dla obu ról) — tam ellipsis+title wystarcza, ten sam wzorzec
           co długie nazwy kontraktów w Przypisaniu. 142px = 100,92 (etykieta)
           + 4 (gap) + 16 (.gap-marker) + padding komórki, z zapasem. */
        .c-stage { width: 142px; }
        /* 168px, nie 126 (ta sama poprawka): zamknięty zbiór czterech etykiet
           odczytywany wzrokiem, więc reguła z Etapu — najdłuższa mieści się
           w całości. Zmierzone przy plakietce 11,6 px: „zapłacona u wystawcy”
           146px (najdłuższa — widzi ją każdy bez `can_mark_paid`, więc realnie
           się renderuje), „do potwierdzenia” 115, „po terminie” 81, „opłacona” 70;
           146 + 2×10,4 padding = 166,8 → 168px. */
        .c-pay { width: 168px; }
        col.c-seller { width: auto; }
        td.c-seller { min-width: 0; }   /* nazwa sprzedawcy zwykłą grubością — makieta; pogrubienie zdjęte 21.09.2026 */
        /* Etap niesie plakietkę stanu + (rzadko) znacznik "niedomknięta" obok.
           Stan zostaje PLAKIETKĄ (czytelna etykieta, własny ellipsis+title z
           `.badge` na wypadek rzadkiego "Zwrócone przez kierownika" — patrz
           komentarz przy `.c-stage`); "niedomknięta" NIE jest już drugą
           plakietką tekstową (dwie pełne plakietki w 118px dawały
           "Zaa···"/"ni···" — nieczytelne obie, poprawka po przeglądzie przed
           odbiorem kroku 4), tylko zwartym znacznikiem stałego rozmiaru
           (`.gap-marker`), więc nigdy nie zabiera miejsca potrzebnego
           plakietce stanu. */
        .stage-cell { display: flex; align-items: center; gap: 4px; min-width: 0; width: 100%; }
        /* „Nieprzypisana" w kolumnie Etap i na karcie: wtopiona w tło, nie stan
           aktywny z ramką — przy kilkuset nieprzypisanych to kilkaset plakietek
           przyciągających wzrok do informacji, która nic nie wnosi (plik
           referencyjny: .badge domyślna = szare tło + tekst wygaszony). Zawężone
           do tych dwóch miejsc, bo `.badge-unassigned` niesie też stany, które
           coś mówią („zapłacona u wystawcy", „Inne urządzenie"). Kontrast
           --muted na --hover-bg: 4,63:1 jasny, 5,31:1 ciemny (policzone).
           `.m-card-l2 .stage-badge`, NIE goły `.m-card-l2` (poprawka 21.09.2026,
           próg kart zależny od kolumn): od tej zmiany `.m-card-l2` niesie DRUGĄ
           plakietkę tej samej klasy `.badge-unassigned` — stan płatności
           „zapłacona u wystawcy" (has-payment) — który MA wyglądać jak zwykła
           plakietka, nie wtopiony w tło jak nieprzypisany Etap; bez zawężenia
           do `.stage-badge` dzieliłby wygaszenie, na które nie zasługuje. */
        .stage-cell .badge-unassigned, .m-card-l2 .stage-badge.badge-unassigned { background: var(--hover-bg); color: var(--muted); }
        /* Tekst wyłącznie dla czytnika ekranu (myślnik „brak przypisania" w kolumnie
           Przypisanie): aria-label na <span> o roli generycznej czytniki ignorują,
           więc słowo stoi w DOM, a widoczny myślnik ma aria-hidden. */
        .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
        .stage-cell .badge { min-width: 0; flex-shrink: 1; }
        td.c-tgt .badge { border-radius: 5px; }   /* makieta .badge--target: plakietka celu prostokątna, reszta 999px */
        /* Znacznik "niedomknięta" — kółko z wykrzyknikiem, ok. 16px, z pełnym
           tekstem w `title`/`aria-label`. Ten sam znacznik w tabeli
           (`.stage-cell .gap-marker`) i na kartach (`.m-card-l2 .gap-marker`)
           — bez niego alarm znikał z telefonu, gdzie kierownik na budowie
           korzysta z aplikacji najwięcej. */
        /* Wypełnienie/tekst jak `.badge-partial` (ta sama para --warn-bg/--warn
           co reszta ostrzeżeń w apce), NIE pełne tło + biały tekst: biały na
           --warn w trybie ciemnym (#fbbf24, jasny bursztyn) łamałby kontrast
           — dokładnie ten błąd, przed którym ostrzega §5 decyzje-projektowe.md
           (powierzchnia odniesienia to SKRAJNA powierzchnia motywu, nie
           założenie "ciemne tło = jasny tekst zawsze bezpieczny"). */
        .gap-marker {
            display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
            box-sizing: border-box; width: 16px; height: 16px; border-radius: 50%; background: var(--warn-bg);
            border: 1px solid var(--warn-line); color: var(--warn);
            font-size: 11px; font-weight: 700; line-height: 1; cursor: default;
        }
        .nr-badge {
            font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.2px; font-weight: 600;   /* makieta .fnum */
            background: var(--th-bg); color: var(--text); padding: 0.25rem 0.6rem; border-radius: 6px;
            /* Poprawka po przeglądzie 21.09.2026: łamanie na dwie linie
               (`word-break: break-all`) ukrywało koniec numeru pod nagłówkiem
               kolejnego wiersza zamiast go obcinać widocznie — zamienione na
               jednoliniowy wielokropek. */
            display: inline-block; max-width: 100%; overflow: hidden;
            white-space: nowrap; text-overflow: ellipsis; vertical-align: bottom;
            /* POPRAWKA po zgłoszeniu właściciela 21.09.2026 (produkcja): `title`
               NIE może zostać na `a.stretched-link` (jak zakładał komentarz wyżej
               do tej poprawki) — `::after` tego odnośnika pokrywa CAŁY WIERSZ
               (containing block to `<tr>`, nie ten `<a>`), więc najechanie na
               Sprzedawcę czy Przypisanie w TYM SAMYM wierszu też pokazywało numer
               faktury (potwierdzone `elementFromPoint`: element pod kursorem
               w każdej komórce wiersza to ten `<a>`). `title` przeniesiony na TĘ
               plakietkę (invoice_list.html), a ona dostaje `position: relative;
               z-index: 2` — wyżej niż `::after` (z-index: 1) tego samego `<a>`,
               więc to ONA, nie nakładka wiersza, jest teraz pod kursorem w całym
               swoim obszarze. Nadal wewnątrz `<a>`, więc klik dalej otwiera
               fakturę — to jest wariant z pozycji 9 rozpoznania, gdzie POPRZEDNIO
               title na osobnym elemencie był zasłonięty, TERAZ podniesiony nad
               nakładkę zamiast dublować ją. */
            position: relative; z-index: 2;
        }
        /* Sprzedawca i Przypisanie: własna nakładka (`.cell-title-overlay`)
           zamiast `title` na `<td>` wprost (ten sam błąd co `.nr-badge` wyżej —
           `<td>` bez pozycjonowania leży POD `::after` odnośnika wiersza,
           `elementFromPoint` w środku obu komórek zwracał `a.stretched-link`
           z numerem faktury w `title`, zgłoszenie właściciela 21.09.2026).
           `position: relative` na komórce: containing block dla `inset: 0`
           nakładki to TA komórka, nie cały wiersz (w odróżnieniu od `<tr>`,
           który jest positioned dla `::after` odnośnika). */
        td.c-seller, td.c-tgt { position: relative; }
        /* Pusta, `aria-hidden` + `tabindex="-1"` (poza drzewem dostępności i
           kolejnością Tab — jedyny DOSTĘPNY odnośnik wiersza zostaje w `.c-nr`),
           `z-index: 2` ponad `::after` odnośnika wiersza (`z-index: 1`), więc
           przechwytuje hover/klik w SWOJEJ komórce: dymek pokazuje pełny tekst,
           klik nadal otwiera fakturę (to ten sam href). */
        .cell-title-overlay { position: absolute; inset: 0; z-index: 2; }
        /* Termin: JEDNA reprezentacja — sama data, kolorem (§3 decyzje
           projektowe). tabular-nums: daty mają się układać w pion. */
        .row-termin-date { font-variant-numeric: tabular-nums; }
        .row-termin-date.is-late { color: var(--danger); font-weight: 600; }
        .row-termin-date.is-soon { color: var(--due-soon); font-weight: 600; }

        /* Chowanie kolumn: przypisanie -> wystawiona (kolejność z promptu),
           osobne progi dla zestawu Z kolumną Płatność (księgowość/dyrektor/
           administrator/podgląd, 8 kolumn) i BEZ niej (kierownik, 7 kolumn)
           — próg liczony dla FAKTYCZNEGO zestawu, nie przepisany z rundy
           poprzedzającej krok 6 (tamta liczba zakładała dodatkowo checkbox
           zaznaczania, którego tu nie ma). Wartości potwierdzone pomiarem
           Playwright, patrz raport odbioru kroku 4.

           POPRAWKA PO PRZEGLĄDZIE: `visibility: collapse` NIE oddaje
           szerokości reszcie tabeli — zmierzone (izolowany test + żywa
           strona): kolumnę faktycznie usuwa z układu, ale zgodnie z CSS2.1
           §17.6.2.1 to TABELA traci tę szerokość, nie sąsiednia kolumna.
           Skutek na 1100px treści (księgowość): tabela skurczyła się do
           918px zamiast 1100, sprzedawca dostał 196px zamiast ~376 — poniżej
           wymaganego minimum 240px, z 182px martwego miejsca obok tabeli.
           `width: 0` na <col> naprawia to (zmierzone: tabela zostaje przy
           pełnej szerokości kontenera, sprzedawca przejmuje CAŁĄ zwolnioną
           przestrzeń) — `table-layout: fixed` i tak ignoruje treść komórki
           przy liczeniu szerokości kolumn, więc 0px jest bezpieczne.

           POPRAWKA 21.09.2026 (zgłoszenie właściciela, odtworzone w Edge/Windows,
           okno 950px, księgowość, menu zwinięte): `display: none` na <th>/<td> był
           BŁĘDEM od tego samego commitu (`ae81a9e`, krok 4) — `display: none`
           WYJMUJE komórkę z siatki tabeli. Komórki PO NIEJ w tym samym wierszu
           przesuwają się o jedną pozycję w lewo, a <col> zostają na swoich
           indeksach — kolejna widoczna komórka renderuje się w SLOCIE schowanej
           kolumny (0px), nie w swoim. Ostatnie tyle kolumn, ile było schowanych,
           zostaje bez żadnej komórki: przy schowanych Przypisaniu i Wystawionej
           (142+168=310px u ról z Płatnością) widać martwy pas dokładnie tej
           szerokości — zmierzone i potwierdzone zgłoszeniem. Weryfikacja kroku 4
           tego nie złapała: mierzyła szerokość TABELI wobec kontenera (poprawnie
           pełna) i szerokość sprzedawcy — obie liczby wychodzą dobrze, bo dziura
           jest WEWNĄTRZ tabeli, w pustych slotach na końcu wiersza, nie między
           tabelą a kontenerem.

           Naprawa: komórka schowanej kolumny ZOSTAJE w siatce (żaden `display`),
           `visibility: hidden` (niewidoczna, ale slot nie przesuwa sąsiadów;
           usuwa też komórkę z drzewa dostępności — czytnik ekranu jej nie
           ogłasza, ten sam efekt co dawało `display: none`), `padding: 0` i
           `border-width: 0` (bez nich 0px szerokości `<col>` nie dawałoby
           faktycznie zerowej komórki — sam padding/ramka zostałyby), `overflow:
           hidden` (treść komórki nie wypycha jej szerzej niż 0px). Szerokość
           komórki nadal bierze się z `<col>` (table-layout: fixed).

           Progi przesunięte TRZY razy względem pierwszej wersji tego pliku:
           +12px gdy `.c-date` urosło z 92 do 104px, +24px gdy `.c-stage`
           urosło ze 118 do 142px, +34px/+42px 21.09.2026 gdy `.c-nr` urosło
           ze 150 do 184px (na WSZYSTKICH progach) i `.c-pay` ze 126 do 168px
           (tylko na progach `.has-payment`) — suma stałych kolumn, którą progi
           liczą, rośnie o tyle samo. Zmierzone po zmianie (widths.py, sesja
           21.09.2026): przy 1248px treści (laptop klienta) wszystkie osiem
           kolumn zostaje, sprzedawca dostaje 244px (> minimum 240px). */
        @container (max-width: 1242px) {
            .invoice-results.has-payment col.c-tgt { width: 0; }
            .invoice-results.has-payment th.c-tgt, .invoice-results.has-payment td.c-tgt {
                padding: 0; border-width: 0; overflow: hidden; visibility: hidden;
            }
        }
        @container (max-width: 1074px) {
            .invoice-results:not(.has-payment) col.c-tgt { width: 0; }
            .invoice-results:not(.has-payment) th.c-tgt, .invoice-results:not(.has-payment) td.c-tgt {
                padding: 0; border-width: 0; overflow: hidden; visibility: hidden;
            }
        }
        @container (max-width: 1062px) {
            .invoice-results.has-payment col.c-date { width: 0; }
            .invoice-results.has-payment th.c-date, .invoice-results.has-payment td.c-date {
                padding: 0; border-width: 0; overflow: hidden; visibility: hidden;
            }
        }
        @container (max-width: 894px) {
            .invoice-results:not(.has-payment) col.c-date { width: 0; }
            .invoice-results:not(.has-payment) th.c-date, .invoice-results:not(.has-payment) td.c-date {
                padding: 0; border-width: 0; overflow: hidden; visibility: hidden;
            }
        }

        /* Próg kart ZALEŻNY OD LICZBY KOLUMN (poprawka po przeglądzie
           produkcyjnym 21.09.2026), nie stałe 700px jak w pierwszej wersji
           kroku 4/5: stałe 700px trzymało tabelę w paśmie, gdzie sprzedawca
           (`col.c-seller`, bez ustalonej szerokości) był PONIŻEJ minimum
           240px — nie w wąskim paśmie tuż nad progiem (tak to opisywała
           wcześniejsza wersja tego komentarza), tylko w CAŁYM paśmie od
           700px do progu, w którym stałe kolumny zostawiają sprzedawcy
           mniej niż 240px. Właściciel: laptop klienta przy powiększeniu
           150% ze zwiniętym menu ma wtedy 823px treści, sprzedawca 103px —
           nieczytelne, a przy powiększeniu z powodu wzroku karty bez
           uciętych nazw są czytelniejsze niż tabela ze sprzedawcą na sto
           kilkanaście pikseli.

           Zamiast przesuwać 700px w jedno miejsce, próg liczony OSOBNO dla
           obu zestawów kolumn — dokładnie ta sama zasada co cztery progi
           chowania `.c-tgt`/`.c-date` wyżej. Zmierzone bezpośrednio
           (Playwright, krok 1px, `.invoice-results` jako kontener; NIE suma
           stałych kolumn z arytmetyki — padding/ramka `.tablecard` (1px,
           border-box) przesuwają wynik o kilka pikseli względem czystej
           sumy szerokości kolumn):
           - has-payment (8→6 widocznych po schowaniu Przypisania i Wystawionej,
             suma stałych 718px): kontener 960px → sprzedawca dokładnie 240px,
             959px → 239px. Próg: 959px.
           - bez Płatności/kierownik (7→5 widocznych, suma stałych 550px):
             kontener 792px → sprzedawca dokładnie 240px, 791px → 239px.
             Próg: 791px.
           Oba bloki (`<table>` i stos kart) renderują się zawsze (jeden
           Python-owy `for`, dwukrotnie, bo to za różne struktury, żeby dało
           się je przełączać samą zmianą CSS na tym samym markupie) — CSS
           pokazuje jeden na raz, `display:none` usuwa drugi z kolejności
           Tab bez potrzeby JS. */
        .invoice-cards { display: none; }
        .invoice-cards ul { list-style: none; margin: 0 0 0.75rem; padding: 0; }
        .invoice-cards ul:last-child { margin-bottom: 0; }
        /* Grupowanie "Do zrobienia" / "Pozostałe" — tylko kierownik (patrz
           `card_sections` w widoku): otwiera aplikację, żeby zobaczyć, co
           ma zrobić. */
        .m-sec { font-size: 0.75rem; font-weight: 700; color: var(--muted); margin: 0 0 0.4rem 0.1rem; }
        @container (max-width: 959px) {
            .invoice-results.has-payment .tablecard { display: none; }
            .invoice-results.has-payment .invoice-cards { display: block; }
        }
        @container (max-width: 791px) {
            .invoice-results:not(.has-payment) .tablecard { display: none; }
            .invoice-results:not(.has-payment) .invoice-cards { display: block; }
        }
        .m-card {
            position: relative; background: var(--card-bg); border: 1px solid var(--border);
            border-radius: 8px; padding: 0.65rem 0.8rem; margin-bottom: 0.5rem;
        }
        .m-card--late { border-color: var(--danger-line); }
        .m-card-l1 { display: flex; justify-content: space-between; gap: 0.6rem; font-size: 0.95rem; }
        .m-card-seller { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
        .m-card-amt { flex-shrink: 0; font-variant-numeric: tabular-nums; font-weight: 600; }   /* jak td.c-amt */
        .m-card-l2 {
            display: flex; align-items: center; gap: 0.5rem; margin-top: 0.3rem;
            font-size: 0.8rem; color: var(--muted); flex-wrap: wrap;
        }
        .m-card-l2 .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
        .m-card-l2 .late { color: var(--danger); font-weight: 600; }
        .m-card-l2 .soon { color: var(--due-soon); font-weight: 600; }

        /* Checkbox z etykietą w jednej linii (filtr "tylko zalegające") — pola
           formularza mają width:100%, więc sam checkbox trzeba z tego wyjąć. */
        .check-inline { display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; }
        .check-inline input[type="checkbox"] { width: auto; margin: 0; }

        /* Linia "poza wybranym okresem" — WARUNEK filtru okresu (§1/§4
           decyzje projektowe): proces jest terminowy, nie okresowy. */
        .outside {
            display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem;
            padding: 0.5rem 0.75rem; border-radius: 6px;
            background: var(--warn-bg); border: 1px solid var(--warn-line); color: var(--warn);
            font-size: 0.85rem;
        }
        /* DWA stany pustki, dwa różne teksty (§5.5 rozpoznania rundy 30) —
           pusto w bazie (filtry wyszarzone, patrz niżej) i pusto po
           filtrach (przycisk "Wyczyść filtry"). */
        .empty { padding: 2.5rem 1.5rem; text-align: center; color: var(--muted); background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; }
        .empty b { display: block; color: var(--text); font-size: 1rem; margin-bottom: 0.4rem; }
        .empty p { margin: 0 auto; max-width: 32rem; font-size: 0.85rem; }
        .empty .btn-secondary { margin-top: 0.9rem; }
        /* Pusto w bazie: filtry zostają WIDOCZNE, ale wyszarzone — znikające
           i wracające filtry to zmieniający się interfejs (§5.5). */
        #filters-form:has(input:disabled) { opacity: 0.55; }

        /* Krok 7.1 (G): .top-bar/.top-bar-seller USUNIĘTE — wyłączne źródło
           było tutaj (sprawdzone: zero innych użyć w repo), zastąpione przez
           .detail-h h1 + .detail-sub niżej. */
        .detail-h { margin: 1rem 0 1.25rem; }
        .detail-h h1 { margin: 0; font-size: 20px; font-weight: 700; letter-spacing: -0.01em; color: var(--text); }
        .detail-sub {
            display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 4px;
            font-size: 13px; color: var(--muted);
        }
        /* Krok 8.3: nagłówek szczegółów na telefonie zwarty — numer faktury
           jest już w belce `.m-top .ttl` (krok 8.2), więc na wąskim ekranie
           nie powtarzamy go tu drugi raz; mniejszy `gap`, żeby pozostałe
           dwie plakietki (przypisanie/etap) częściej mieściły się w jednym
           wierszu. Na szerokich ekranach bez zmian. */
        @media (max-width: 767px) {
            .detail-sub { gap: 6px; }
            .detail-sub .nr-badge { display: none; }
        }
        .badge--target-name { max-width: 180px; }
        .field-label { margin: 0; font-size: 0.75rem; color: var(--muted); }
        /* Krok 7.1 (E): karta "Faktura" przebudowana na .ksef/.kv/.totals
           z pliku referencyjnego w8 sekcja 4 — .details-grid/.field-value/
           .field-value-mono/.amounts-row/.amount-value USUNIĘTE, były
           wyłącznym źródłem tej karty (sprawdzone: zero innych użyć w repo
           przed usunięciem). Kontrast zmierzony na FAKTYCZNYCH
           powierzchniach (§5 decyzje-projektowe.md): --muted na --card-bg
           4,83:1 jasny / 5,78:1 ciemny (.kv dt, .totals etykieta); --text
           na --th-bg 15,70:1 jasny / 14,33:1 ciemny (.ksef wartość). */
        /* Krok 7.1 (I): tło/promień/margines dostosowane do pomiaru
           porownaj_z_makieta.py. Plik referencyjny używa `var(--gray-bg)`
           (#f3f4f6 jasny / #1f232b ciemny) — TOKENU, KTÓREGO APLIKACJA NIE
           MA. Świadomie NIE dodaję go: `--th-bg` (#f0f1f3 jasny / #111827
           ciemny) jest już ustalonym, wielokrotnie używanym w całej
           aplikacji tokenem tej samej roli (neutralny szary box — .step-num,
           .decl-row--full i kilkanaście innych miejsc); nowy, jednorazowy
           token dla samej tej karty byłby niespójny z resztą aplikacji.
           Różnica koloru tła (2,54 ΔL* jasny) NAZWANA, nie naprawiona —
           promień i margines dopasowane dokładnie (6px, 2px 0 12px). */
        .ksef {
            display: flex; align-items: center; justify-content: space-between; gap: 6px;
            margin: 2px 0 12px;
            padding: 6px 8px;
            background: var(--th-bg);
            border-radius: 6px;
            font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
            font-size: 11.8px;
            color: var(--text);
        }
        /* Krok 7.1 (J): white-space:nowrap jak w pliku referencyjnym — [POMIAR]
           340px, prawdziwy 35-znakowy numer (pk 4394): zapas ~35,5px przy
           1536 i 1920px (identyczny, kolumna ma stałą szerokość). Nawet
           38-znakowy numer testowy DEMO/03 (dłuższy niż realny format,
           artefakt danych testowych — prefiks "DEMO-KSEF-" zamiast 10-cyfrowego
           NIP) mieści się z zapasem ~18px. Zero przypadków przepełnienia
           zmierzonych — nowrap bezpieczne. */
        .ksef-val { white-space: nowrap; }
        .ksef .ksef-val { word-break: break-all; }
        /* Krok 7.1 (I): .kv poprawione na 2-kolumnową siatkę (klucz | wartość
           wyrównana do prawej) zgodnie z RZECZYWISTĄ regułą pliku
           referencyjnego (deploy/ux/uklad-aplikacji.html:494-496:
           `grid-template-columns:auto 1fr;gap:6px 14px`) — pierwsza wersja
           (E) miała dt/dd stackowane pionowo (goły <dl>), bez sprawdzenia
           przez porownaj_z_makieta.py; różnica złapana dopiero w commicie I. */
        .kv { display: grid; grid-template-columns: auto 1fr; gap: 6px 14px; font-size: 13px; margin: 0; }
        .kv dt { color: var(--muted); }
        .kv dd { margin: 0; color: var(--text); text-align: right; font-variant-numeric: tabular-nums; }
        /* Krok 7.1 (I): .totals poprawione na siatkę 3 równych kolumn
           (repeat(3,1fr)) zamiast flex-wrap — pierwsza wersja (E) zawijała
           "Brutto" na drugi wiersz przy węższej karcie (3 elementy we
           flex-wrap się nie mieściły); flex-wrap NIE jest tym, co deklaruje
           plik referencyjny (uklad-aplikacji.html:500-502). `<b>` BEZ
           font-weight: 700 to WAGA DOMYŚLNA znacznika, nie brak stylu —
           pierwsza wersja błędnie nadpisywała ją na 400 (sekcja 0 pliku
           referencyjnego: "—" w kolumnie Grubość = nienadpisane, nie "tak
           samo jak sąsiad"). Złapane porównaniem, nie przy pisaniu E. */
        .totals { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin: 12px 0 10px; padding-top: 12px; border-top: 1px solid var(--border); }
        .totals div { font-size: 11.6px; color: var(--muted); }
        .totals b { display: block; font-size: 14px; color: var(--text); font-variant-numeric: tabular-nums; }

        @media (max-width: 640px) {
            body:not(.has-shell) main { margin: 1rem auto; padding: 0 0.75rem; }
            header { margin: 0.75rem auto 0; padding: 0 0.75rem; }
            .topbar { min-height: auto; padding: 0.6rem 0.9rem; justify-content: center; }
            .nav-links { justify-content: center; width: 100%; order: 3; }
            .topbar-right { margin: 0.3rem 0; }
            /* Zawężone do .topbar (runda 30, krok 5): ta sama klasa `.user-name`
               stoi teraz też w stopce menu bocznego (i czyta ją _wizard.html) —
               gołe `.user-name { display: none }` kasowało nazwę użytkownika
               w menu wysuwanym na telefonie (zmierzone na zrzucie 390 px). */
            .topbar .user-name { display: none; }
            .page-header h1 { font-size: 1.15rem; }
            .card { padding: 1rem; }
            th, td { padding: 0.55rem 0.6rem; font-size: 0.85rem; }
            .filters select, .filters input { min-width: 100%; }
            .date-range-sep { display: none; }
            .inline-form { flex-direction: column; align-items: stretch; }
            .inline-form select, .inline-form input, .inline-form button, .inline-form .btn {
                width: 100%; max-width: none; min-width: 0; margin-right: 0;
            }
            .line-panel-row, .line-panel-bottom { flex-direction: column; align-items: stretch; }
            .mode-grid { grid-template-columns: 1fr; }
            .panel-done { flex-direction: column; }
            .target-combobox-results { left: 0; right: 0; }
            .cell-ellipsis { max-width: none; white-space: normal; }
            /* Lista faktur na wąskim ekranie: przełącznik tabela/karty to od
               rundy 30 kroku 4 container query na .invoice-results
               (max-width: 700px treści), nie ta media query na oknie —
               dawne reguły .invoice-row/.col-* usunięte razem z tymi
               klasami wyżej. */
            .service-form-grid { grid-template-columns: 1fr; }
            .service-form-description { grid-column: span 1; }
            .two-col-row { grid-template-columns: 1fr; }
            .invoice-rows-total { text-align: left; }
            .allocation-row { flex-direction: column; align-items: stretch; }
            .allocation-add-form { flex-direction: column; align-items: stretch; }
            .allocation-add-form input, .allocation-add-form .target-combobox, .allocation-add-form .btn {
                width: 100%; max-width: none;
            }
        }

        /* ===== Wzorzec "lista wierszy" — accounts/ (historia logowań, sesje, passkeye) =====
           Usunięty stąd w rundzie 30, kroku 4 (commit 96a4fc5) przy przebudowie listy
           faktur na <table> — komentarz przy usuwaniu twierdził, że invoice_list.html
           był JEDYNYM użytkownikiem tych klas. To twierdzenie było fałszywe: trzy
           szablony accounts/ nadal ich używały i renderowały się bez tego stylu od
           wdrożenia kroku 4 (eb6c6b2) do tej poprawki — regresja znaleziona w
           rozpoznaniu PO wdrożeniu na produkcję, nie w przeglądzie przed nim (patrz
           §5 decyzje-projektowe.md, zasada o przeszukiwaniu wszystkich szablonów
           wszystkich aplikacji przed usunięciem klasy CSS).

           Przywrócone Z HISTORII GITA (`git show 96a4fc5~1:static/css/app.css`), NIE
           odtworzone z pamięci — odtworzenie "mniej więcej tak samo" dałoby układ
           podobny, ale inny, i nikt by tego nie zauważył.

           Dzisiejsi użytkownicy — TYLKO te trzy pliki, nic innego (zweryfikowane
           grepem po WSZYSTKICH szablonach WSZYSTKICH aplikacji, nie tylko accounts/):
             accounts/templates/accounts/login_history.html
             accounts/templates/accounts/sessions.html
             accounts/templates/accounts/passkeys.html

           ZESTAW CELOWO NIEPEŁNY: przywrócono 12 z 20 selektorów usuniętych w kroku 4
           — tylko te z realnym dzisiejszym użytkownikiem. BRAKUJE tu (bo nie mają
           dziś żadnego użytkownika): .col-nr, .col-kwota, .col-przypisanie-sm,
           .col-rozliczenie-sm, .col-termin, .col-ilosc, .invoice-rows-total (+ jej
           `strong`), .badge-due-soon. Kto kiedyś doda czwartą stronę do tego wzorca
           i sięgnie po jeden z tych ośmiu — dostanie CISZĘ (reguła po prostu nie
           istnieje), nie błąd. Reszta czeka w tym samym commicie gita co powyższe. */
        .invoice-rows-header { display: flex; align-items: center; gap: 0.6rem; padding: 0 1rem 0.5rem; }
        .invoice-rows-header span {
            font-size: 0.7rem; color: var(--muted); text-transform: uppercase;
            font-weight: 700; letter-spacing: 0.04em;
        }
        .invoice-rows { display: flex; flex-direction: column; gap: 0.5rem; }
        .invoice-row {
            display: flex; align-items: center; gap: 0.6rem; padding: 0.85rem 1rem;
            border: 1px solid var(--border); border-radius: 8px; background: var(--card-bg);
            text-decoration: none; color: inherit;
        }
        .invoice-row:hover { border-color: var(--input-border); background: var(--hover-bg); }
        .col-main { flex: 1 1 200px; min-width: 0; display: flex; flex-direction: column; gap: 0.15rem; }
        .row-seller { font-size: 0.9rem; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        .row-date { font-size: 0.78rem; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        .row-amount { font-size: 0.95rem; font-weight: 600; color: var(--text); display: block; }
        .row-amount-label { font-size: 0.75rem; color: var(--muted); }
        .col-przypisanie { width: 200px; flex-shrink: 0; display: flex; justify-content: flex-end; }
        .col-rozliczenie { width: 180px; flex-shrink: 0; display: flex; justify-content: flex-end; }
        .col-actions { width: 150px; flex-shrink: 0; display: flex; gap: 0.4rem; justify-content: flex-end; }
        .col-data { width: 150px; flex-shrink: 0; text-align: right; }
        @media (max-width: 640px) {
            .invoice-rows-header { display: none; }
            .invoice-row { flex-direction: column; align-items: stretch; gap: 0.4rem; }
            .col-main, .col-przypisanie, .col-rozliczenie, .col-actions, .col-data { width: auto; }
            /* flex: 1 1 200px na .col-main (wyżej) ustawia flex-basis 200px — w układzie
               wierszowym to szerokość (działa zgodnie z zamiarem), ale .invoice-row jest tu
               flex-direction:column, więc ta sama podstawa czyta się jako WYSOKOŚĆ: 200px
               pustego miejsca pod krótką treścią. Dokładnie błąd nazwany i naprawiony w
               invoice_list.html przy przebudowie kroku 4 (§12.2 usterka #4 rozpoznania rundy
               30) — przywrócenie tej reguły z historii gita bez tej jednej linii wprowadzałoby
               go z powrotem do trzech stron accounts/, mimo że w tym samym kroku uznano go za
               błąd gdzie indziej. Patrz też §5 decyzje-projektowe.md. */
            .col-main { flex-basis: auto; }
            .col-data { text-align: left; }
            .col-przypisanie, .col-rozliczenie, .col-actions { justify-content: flex-start; }
        }

        /* ===== Runda 30, krok 5: POWŁOKA — menu boczne zamiast górnego paska =====
           deploy/ux/uklad-aplikacji.html, sekcje 3 i 5 + „Uwagi implementacyjne".
           Jeden <aside class="side"> (base.html) w TRZECH widokach sterowanych
           wyłącznie CSS-em — nie trzy kopie w szablonie:
             ≥ 1280 px okna   rozwinięte, --sidebar-w (232 px)
             768–1279 px      pasek ikon, --rail-w (64 px)
             < 768 px         belka .m-top (56 px, przewija się z treścią —
                              krok 8.2, dawniej .mobile-bar)
                              + menu wysuwane (.side.is-open)
           RĘCZNY WYBÓR MA PIERWSZEŃSTWO PRZED PROGIEM: html[data-side="rail"]
           wymusza pasek przy każdej szerokości ≥ 768; html[data-side="expanded"]
           trzyma menu rozwinięte także poniżej 1280 px — wtedy NAKŁADA SIĘ na
           treść (szerokość .side rośnie, --side-now NIE), bo człowiek, który
           powiększa przeglądarkę do 125/150 %, powiększa, żeby czytać podpisy,
           nie żeby je stracić (§3 decyzje-projektowe.md). Atrybut nakłada blok
           anty-FOUC w <head> PRZED arkuszem; brak klucza w localStorage = brak
           atrybutu = decyduje sam próg. Zmiana wyłącznie przyciskiem „Zwiń/
           Rozwiń menu" — kliknięcie obok nigdy nie zwija.

           PRZEWIJA SIĘ DOKUMENT, menu jest position:fixed (top/bottom, bez
           100vh) — na telefonie pasek adresu chowa się przy przewijaniu, a
           sticky nagłówka tabeli liczy się względem okna. Wszystko, co
           pozycjonuje się względem menu (przyszły pasek zbiorczy, krok 6),
           liczy od --side-now, nie od --sidebar-w.

           Wygląd paska ikon to CONTAINER QUERY na własnej szerokości .side
           (≤ 100 px) — nie osobny zestaw selektorów na data-side i osobny na
           media query: rozwinięcie ręczne poniżej 1280 px (nakładka 232 px)
           dostaje podpisy automatycznie, bo .side jest wtedy szerokie. */
        :root {
            --sidebar-w: 232px;
            --rail-w: 64px;
            --side-now: var(--sidebar-w);   /* AKTUALNA szerokość zajęta przez menu */
            --m-top-h: 56px;
            /* krok 8.2 — zmierzone (Playwright, 360/412px, DEMO/01, 02, 04, 05):
               najwyższy realny wariant to ostrzeżenie (.wiz-todo, 2 wiersze,
               66,8px) + jeden wiersz przycisków (44px) + gap 8px + padding
               2×10px = 138,8px (kroki 2/3) — WYŻEJ niż dwa wiersze przycisków
               bez ostrzeżenia (100px + padding 20px = 120px, kroki 1/4 przy
               dłuższych etykietach typu „Wyślij ponownie do księgowości").
               138,8 zaokrąglone w górę z zapasem na Segoe UI (DEV nie ma tego
               kroju, R7 rozpoznania — wagi 550/650 renderują się tam szerzej).

               Krok 8.3, usterka 2.5.5: rezerwacja pod paskiem liczyła ZAWSZE
               wariant z ostrzeżeniem, nawet na krokach bez niego (1 i 4) —
               niepotrzebny pusty pas pod paskiem. Dwie zmienne: domyślna
               (bez ostrzeżenia, 120px) i `--mfixed-h-warn` (z ostrzeżeniem,
               150px, bez zmian) — modyfikator `.m-fixed--warn` dodawany w
               szablonie DOKŁADNIE tam, gdzie kod już wie, że `.wiz-todo` się
               pokaże (te same warunki, które dziś je renderują). Min-height
               samego paska zostaje przy mniejszej wartości — to tylko dolna
               granica, realną wysokość i tak wyznacza zawartość flexa. */
            --mfixed-h: 120px;
            --mfixed-h-warn: 150px;
        }
        /* WSZYSTKO przez `:root`, nie `html`: `:root` to pseudoklasa (0,1,0)
           i wygrywa z samym `html` (0,0,1) niezależnie od kolejności — pierwsza
           wersja tych reguł na `html` przegrywała z domyślnym `:root` wyżej
           i telefon dostawał treść przesuniętą o 232 px (zmierzone). Para
           `:root, :root[data-side]` daje przy braku atrybutu równą wagę
           (rozstrzyga kolejność — telefon ostatni), a przy atrybucie (0,2,0)
           bije `:root[data-side="rail"]`. Poniżej 1280 px --side-now to ZAWSZE
           pasek, także przy data-side="expanded" — wtedy rośnie tylko szerokość
           .side (nakładka), a treść zostaje na miejscu. */
        @media (max-width: 1279px) {
            :root, :root[data-side] { --side-now: var(--rail-w); }
        }
        :root[data-side="rail"] { --side-now: var(--rail-w); }
        @media (max-width: 767px) {
            :root, :root[data-side] { --side-now: 0px; }
        }
        .side {
            position: fixed; top: 0; bottom: 0; left: 0; z-index: 40;
            width: var(--side-now); box-sizing: border-box;
            display: flex; flex-direction: column; min-height: 0;
            background: var(--card-bg); border-right: 1px solid var(--border);
            container-type: inline-size;
        }
        html[data-side="expanded"] .side { width: var(--sidebar-w); } /* nakładka < 1280 px */
        body.has-shell main { margin-left: var(--side-now); }

        .side-brand {
            display: flex; align-items: center; gap: 0.5rem; height: 64px; flex: none;
            padding: 0 14px; border-bottom: 1px solid var(--border);
        }
        .side-brand .brand { min-width: 0; overflow: hidden; }
        .side-brand .brand .nav-label { overflow: hidden; text-overflow: ellipsis; }
        .brand-mark {
            width: 30px; height: 30px; border-radius: 7px; flex: none;
            background: var(--accent-fill, var(--accent)); color: var(--accent-fg);
            display: grid; place-items: center; font-weight: 800; font-size: 0.9rem;
        }
        .side-close { display: none; }
        .side-nav { flex: 1 1 auto; overflow-y: auto; min-height: 0; padding: 12px 10px; }
        .nav-item {
            position: relative; display: flex; align-items: center; gap: 11px;
            height: 38px; padding: 0 10px; border-radius: 8px; margin-bottom: 2px;
            color: var(--muted); text-decoration: none; font-weight: 550; font-size: 13.6px;   /* makieta .nav; 550 renderuje się jako 600 w Segoe UI — zatwierdzone (21.09.2026) */
            white-space: nowrap;
        }
        .nav-item:hover { background: var(--hover-bg); color: var(--text); }
        /* Wzór z makiety (21.09.2026, po porównaniu): tło ZABARWIONE, nie pełne
           wypełnienie — color-mix() liczony przez CSS, nie w Pythonie (panel
           zostaje źródłem prawdy dla samego --accent, nic tu nie jest wyliczane
           poza przeglądarką). Tekst --link: ten sam akcent w roli tekstu,
           z gwarantowaną czytelnością (branding/colors.py), NIE --accent wprost.
           Kontrast --link na tym tle zmierzony w obu motywach dla akcentu
           produkcyjnego (#ff6a1a) i DEV (seed): patrz stan-techniczny.md. */
        .nav-item.is-active { background: color-mix(in srgb, var(--accent) 12%, var(--card-bg)); color: var(--link); font-weight: 650; }   /* makieta .nav.on */
        .side .ic { width: 18px; height: 18px; flex: none; stroke: currentColor; fill: none; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
        .nav-count {
            margin-left: auto; min-width: 22px; height: 20px; padding: 0 6px; border-radius: 999px;
            background: var(--accent-fill, var(--accent)); color: var(--accent-fg);
            font-size: 0.72rem; font-weight: 700; display: inline-grid; place-items: center;
            font-variant-numeric: tabular-nums;
        }
        /* Odwrócenie NIEAKTUALNE od 21.09.2026: powstało, bo aktywny wiersz miał
           PEŁNE wypełnienie akcentem, tym samym kolorem co domyślne tło plakietki
           — bez odwrócenia plakietka znikała w tle. Aktywny wiersz ma teraz
           zabarwione tło (color-mix 12%), więc plakietka (nadal pełny --accent-fill)
           kontrastuje z nim naturalnie — plakietka jak wszędzie, bez wyjątku
           (zgodnie z makietą: .nav .cnt nie ma osobnego stanu aktywnego). */

        .side-foot { flex: none; border-top: 1px solid var(--border); padding: 10px; }
        .side-foot .user-menu { position: relative; }
        /* Zakres .side-foot: goła .who kolidowała z .alloc-head .who (karta
           „Pozycje faktury" księgowości) i .comment .who (kreator, komentarz
           zwrotny) — obie nie deklarują display/width, więc dziedziczyły je
           z tej reguły (SS5 decyzje-projektowe.md: reguła powłoki z ogólną
           nazwą klasy musi mieć zakres kontenera powłoki). Paczka przed
           krokiem 7, commit 5/6 — błąd byl już na produkcji od kroku 5
           (0d1b567), nie regresja tej paczki. */
        .side-foot .who {
            display: flex; align-items: center; gap: 10px; width: 100%; padding: 6px 8px; margin: 0;
            border: 0; border-radius: 8px; background: none; color: var(--text); font: inherit;
            text-align: left; cursor: pointer;
        }
        .side-foot .who:hover { background: var(--hover-bg); }
        .side-foot .who .avatar { width: 30px; height: 30px; font-size: 0.75rem; }
        .who-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.2; }
        .who-name { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }   /* makieta .who .n */
        .who-role { font-size: 0.75rem; line-height: 1.45; color: var(--muted); }   /* line-height: makieta zostawia naturalną interlinię (17,4px przy 11,6px), tu jawnie zamiast dziedziczonej ciaśniejszej */
        .side-foot .who .chevron { margin-left: auto; }
        /* Menu użytkownika rozwija się W GÓRĘ — stopka jest na dole ekranu; dotąd
           kotwiczyło się do .topbar{position:relative}, którego w powłoce nie ma
           (§2.1 rozpoznania rundy 30). */
        .dropdown--up { top: auto; bottom: calc(100% + 0.4rem); left: 0; right: 0; }
        .side-exit {
            display: flex; align-items: center; gap: 11px; height: 32px; padding: 0 10px;
            margin-top: 6px; border-radius: 8px; color: var(--muted); text-decoration: none;
            font-size: 0.8rem; white-space: nowrap;
        }
        .side-exit:hover { background: var(--hover-bg); color: var(--text); }
        .side-tools { display: flex; gap: 6px; margin-top: 6px; }
        .side-tool {
            flex: 1 1 0; display: inline-flex; align-items: center; justify-content: center; gap: 6px;
            height: 30px; padding: 0 8px; margin: 0; border-radius: 7px;
            /* tło przezroczyste (dawniej --card-bg — białe pudełko na białym menu, widoczne
               tylko przez ramkę) — decyzja właściciela 21.09.2026, po porównaniu z makietą. */
            border: 1px solid var(--border); background: transparent; color: var(--muted);
            font: inherit; font-size: 0.75rem; font-weight: 600; cursor: pointer; white-space: nowrap;
        }
        .side-tool:hover { background: var(--hover-bg); color: var(--text); }
        .side-tool .icon-moon svg { display: block; }
        .side-ver { font-size: 0.68rem; color: var(--muted); text-align: center; margin-top: 8px; }

        /* Pasek ikon — .side ma ≤ 100 px (rail 64 px): podpisy znikają, ikony
           do środka, licznik jako mała plakietka na ikonie. Każda pozycja ma
           title i aria-label (base.html), więc ikona bez podpisu jest nazwana. */
        @container (max-width: 100px) {
            .nav-label { display: none; }
            .side-brand { justify-content: center; padding: 0; }
            .nav-item, .side-exit { justify-content: center; padding: 0; }
            .nav-count {
                position: absolute; top: 2px; right: 4px; margin: 0;
                min-width: 16px; height: 16px; padding: 0 4px; font-size: 0.62rem;
            }
            .side-foot .who { justify-content: center; padding: 6px 0; }
            .side-tools { flex-direction: column; }
            .side-tool { flex: none; width: 100%; }
            .dropdown--up { left: 0; right: auto; min-width: 200px; }
            /* Jedyną funkcją przycisku jest teraz ROZWINIĘCIE — strzałka w prawo
               (odbicie tej samej ikony). Przy ręcznym rozwinięciu poniżej 1280 px
               menu jest szerokie, więc container query nie łapie i zostaje „<". */
            #side-toggle .ic { transform: scaleX(-1); }
        }

        /* Telefon: belka 56 px PRZEWIJA SIĘ z dokumentem (pion jest tu najdroższy,
           §1 decyzje-projektowe.md), menu wysuwane z lewej, zamykane tłem,
           krzyżykiem albo Escape; pozycje 46 px pod palec.
           Krok 8.2: .m-top zastępuje dawny .mobile-bar — tytuł strony zamiast
           nazwy/loga firmy (ta zostaje w .side-brand, góra wysuwanego menu,
           jedyne miejsce od tej zmiany). Typografia z §0 pliku referencyjnego
           (deploy/ux/uklad-aplikacji.html:721, „Belka — tytuł"): 15px/650. */
        .m-top { display: none; }
        .side-backdrop { display: none; }
        @media (max-width: 767px) {
            .m-top {
                display: flex; align-items: center; gap: 10px; height: var(--m-top-h);
                padding: 0 12px; background: var(--card-bg); border-bottom: 1px solid var(--border);
            }
            .m-top .ttl {
                font-size: 15px; font-weight: 650; overflow: hidden; text-overflow: ellipsis;
                white-space: nowrap; min-width: 0;
            }
            .burger {
                width: 38px; height: 38px; padding: 0; margin: 0; border-radius: 9px; flex: none;
                border: 1px solid var(--border); background: var(--card-bg); color: var(--text);
                display: grid; place-items: center; cursor: pointer;
            }
            .burger .ic { width: 20px; height: 20px; stroke: currentColor; fill: none; stroke-width: 1.6; stroke-linecap: round; }
            .side {
                width: var(--sidebar-w); max-width: 85vw; transform: translateX(-100%);
                box-shadow: none; z-index: 60;
            }
            html[data-side="expanded"] .side { width: var(--sidebar-w); }
            .side.is-open { transform: none; box-shadow: 8px 0 30px rgba(0, 0, 0, 0.2); }
            .side-close {
                display: grid; place-items: center; margin-left: auto; width: 34px; height: 34px;
                padding: 0; border: 0; border-radius: 8px; background: none; color: var(--muted); cursor: pointer;
            }
            .side-close .ic { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 1.6; stroke-linecap: round; }
            .side-backdrop { display: block; position: fixed; inset: 0; z-index: 50; background: rgba(10, 12, 16, 0.45); }
            .side-backdrop[hidden] { display: none; }
            .nav-item { height: 46px; font-size: 0.95rem; }
            .side-tool#side-toggle { display: none; } /* zwijanie nie ma sensu w menu wysuwanym */

            /* Krok 8.2: pasek akcji kroku kreatora, przyklejony do dołu — ten
               sam próg co .m-top wyżej ("jeden próg dla belki i paska").
               Ancestor check (§3.2 promptu, pierwsza rzecz do sprawdzenia):
               main.page--detail ma container-type:inline-size (patrz sekcja
               SZEROKOŚCI I TYPY STRON niżej w pliku) — teoretycznie containment
               tworzy containing block dla position:fixed tak jak transform/
               filter/contain jawne. POMIAREM (Playwright, krok 8.2,
               deploy/30-krok8-2-raport.md) potwierdzone, że w Chromium
               container-type samo w sobie NIE łapie fixed — .m-fixed kotwiczy
               się poprawnie do realnego viewportu (bbox.y stały przy scrollu
               0→1085px). Zero innych przodków z transform/filter/contain/
               will-change (sprawdzone grepem) — bez pomiaru nie było to pewne. */
            .m-fixed {
                position: fixed; left: 0; right: 0; bottom: 0; z-index: 45;
                background: var(--card-bg); border-top: 1px solid var(--border);
                display: flex; flex-direction: column; gap: 8px; box-sizing: border-box;
                padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
                min-height: var(--mfixed-h);
            }
            /* Odstęp .wiz-todo/.wiz-actions od siebie liczy `gap` paska wyżej,
               nie ich własny `margin-top` (0,9rem/1rem, myślany na desktopowy
               odstęp od karty NAD nimi — w pasku byłby podwójny). Reset tylko
               tutaj, wewnątrz 767px, żeby desktop zachował oryginalny odstęp. */
            .m-fixed .wiz-todo, .m-fixed .wiz-actions { margin-top: 0; }
            /* Odstęp treści = wysokość paska w NAJWYŻSZYM wariancie (ostrzeżenie
               dwuwierszowe + przyciski zawinięte do dwóch wierszy) — czasem
               trochę pustego miejsca pod krótszym paskiem, nigdy zasłonięcia
               (decyzja właściciela, plan kroku 8.2, punkt 3 — odstępstwo od
               dosłownego "równy" z promptu, przyjęte świadomie). */
            /* `body.has-shell main.page:has(...)`, NIE goły `main.page:has(...)`:
               ta sama specyficzność (0,1,2) co `body.has-shell main.page { padding:
               12px }` niżej w pliku (sekcja SZEROKOŚCI I TYPY STRON) — bez dopisania
               `body.has-shell` przegrywałaby przy remisie kolejnością źródłową
               (odkryte pomiarem: bez tego dopisku padding-bottom wracał do 12px). */
            body.has-shell main.page:has(.m-fixed) { padding-bottom: calc(var(--mfixed-h) + env(safe-area-inset-bottom)); }
            /* `:has(.m-fixed.m-fixed--warn)`, NIE `:has(.m-fixed--warn)` samo —
               druga forma miałaby TĘ SAMĄ specyficzność co regułę bazową
               wyżej (jedna klasa w argumencie :has() w obu przypadkach) i
               wygrywałaby wyłącznie kolejnością źródłową — dokładnie pułapka
               remisu z kroku 8.2 (§5 decyzje-projektowe.md). Selektor
               złożony z DWÓCH klas w :has() daje specyficzność (0,4,2),
               jednoznacznie wyżej niż baza (0,3,2), więc wygrywa niezależnie
               od pozycji w pliku. */
            body.has-shell main.page:has(.m-fixed.m-fixed--warn) { padding-bottom: calc(var(--mfixed-h-warn) + env(safe-area-inset-bottom)); }
            /* Klawiatura: pole opisu pozycji (.poz-edycja textarea) jest w tym
               samym .step-content co .m-fixed — chowamy pasek na czas edycji,
               bez JS, żeby nie zasłaniał pola ani lokalnego przycisku "Zapisz"
               tego wiersza (R4: dziś działa to dobrze wyłącznie dlatego, że nic
               nie jest fixed — po tej zmianie trzeba było to odtworzyć świadomie). */
            .step-content:has(textarea:focus) .m-fixed { display: none; }

            /* Krok 8.4 (2.1): przerywana ramka strefy wrzucania WZ znika na
               telefonie — nie ma czego przeciągać (uklad-aplikacji.html §6,
               `.drop .drag-hint` już gaśnie pod `pointer:coarse`), a odstępy
               ramki liczone są pod desktop, więc przy pełnej szerokości
               przycisków (krok 8.1) wypychają je poza obrys karty. Zostaje
               sama para `.wz-pick`, z marginesem karty zamiast własnego
               paddingu ramki. `.drop` używany WYŁĄCZNIE w
               `_wizard_wz_documents.html` (sprawdzone grepem) — zawężanie
               do innych miejsc niepotrzebne. */
            .drop {
                padding: 0; margin-top: 0.6rem; border: 0; background: none;
            }
            .drop--compact { padding: 0; }

            /* Krok 8.4 (2.2, wariant A): na telefonie znacznik kroku wraca do
               przepływu — `position:absolute` z bazy (patrz komentarz przy
               `.step-num` wyżej w pliku) był TYLKO po to, żeby odtworzyć lewą
               kolumnę na szerokim ekranie; tutaj kolumna znika, więc num staje
               się zwykłym pierwszym elementem wiersza tytułu, przed tekstem.
               Pionowej linii łączącej kroki nie było w CSS przed tą zmianą —
               nie ma czego usuwać. */
            .step-body { padding-left: 0; }
            .step-num { position: static; margin: 0 0.6rem 0 0; }
            .step-title { display: flex; align-items: center; flex-wrap: wrap; }
        }

        /* ===== Runda 30, krok 5: SZEROKOŚCI I TYPY STRON =====
           Koniec `max-width: 1100px` na powłoce (było w TRZECH miejscach:
           header, main, inline w <footer> — §1.1 A rozpoznania rundy 30).
           Zamiast tego każdy widok DEKLARUJE typ strony blokiem
           `{% block page_type %}` na <main class="page …"> (base.html) —
           nie zgadujemy z treści:
             page--list    pełna szerokość do --list-max (lista faktur, pulpit,
                           trzy listy ustawień konta)
             page--detail  szczegóły faktury / kreator — w kroku 5 JEDNA
                           kolumna o szerokości --read-max; dwie kolumny
                           (--read-max + --side-w, próg --detail-min liczony
                           container query na obszarze treści) to krok 7
             page--form    jedna kolumna, --read-max (zmiana hasła, podgląd FV
                           bez XML / z błędem; domyślna w base.html)
           --read-max = 1100 px, NIE 960 z pliku referencyjnego: makieta
           oznaczyła tę liczbę „do potwierdzenia pomiarem dzisiejszej
           szerokości kart", pomiar (§1.2 rozpoznania) dał 1100 px, a kryterium
           odbioru kroku brzmi „żaden widok nie ma mniej miejsca niż dziś"
           (decyzja użytkownika przy planie). Krok 7 może przeliczyć.
           --list-max 1760: limit na monitory > 2K; na 1920 nieaktywny
           (obszar treści ma tam 1632 px). Obszar treści na laptopie 1536:
           1536 − 232 − 2·28 = 1248 px — próg container query kolumny
           „Przypisanie" (1166 px, krok 4) jest wtedy PRZEKROCZONY, więc
           kolumna wraca sama, bez dotykania listy. */
        :root {
            --page-pad-x: 28px;
            --page-pad-y: 22px;
            --read-max: 1100px;
            --side-w: 340px;
            --detail-min: 1120px;
            --list-max: 1760px;
        }
        /* Pasmo paska ikon (768–1279 px, a więc też laptop przy 125/150 %
           powiększenia): odstęp boczny 24 px zamiast 28 — dokładnie rachunek
           z pliku referencyjnego („kontener 912 px zamiast dzisiejszych 976
           przy 1024 px": 1024 − 64 − 2·24). Sam pasek 64 px kosztuje w tym
           paśmie 64 px szerokości treści względem stanu sprzed kroku 5 (main
           bez menu miało w − 48) — to jest cena menu bocznego przewidziana
           w makiecie, nie skutek odstępów; kryterium „nie mniej miejsca przy
           żadnej szerokości" jest tu z definicji nieosiągalne i zostaje
           nazwane w raporcie kroku. */
        @media (max-width: 1279px) {
            :root, :root[data-side] { --page-pad-x: 24px; }
        }
        body.has-shell main.page { padding: var(--page-pad-y) var(--page-pad-x); min-width: 0; box-sizing: border-box; }
        body.has-shell .page--list { max-width: calc(var(--list-max) + 2 * var(--page-pad-x)); }
        /* Krok 7: strona szczegółów musi zmieścić DWIE kolumny (--read-max +
           --side-w + 18px odstępu), nie tylko jedną — inaczej .detail nigdy
           nie osiągnąłby container-query progu --detail-min (1120px), bo
           jego dostępna szerokość byłaby z góry przycięta do --read-max. */
        body.has-shell .page--detail { max-width: calc(var(--read-max) + var(--side-w) + 18px + 2 * var(--page-pad-x)); }
        body.has-shell .page--form { max-width: calc(var(--read-max) + 2 * var(--page-pad-x)); }
        /* Bez powłoki (logowanie, MFA, wymuszona zmiana hasła): dzisiejszy
           układ wyśrodkowany — header i main na TYCH SAMYCH liczbach (patrz
           komentarz przy `header` wyżej), tylko token zamiast literału. */
        .site-footer { max-width: var(--read-max); margin: 1rem auto 1.5rem; padding: 0 1.5rem; text-align: center; font-size: 0.75rem; color: var(--muted); }
        @media (max-width: 767px) {
            body.has-shell main.page { padding: 12px; }
        }
        /* Nagłówek strony scalony z dawną górną linią: jedna linia — tytuł po
           lewej, po prawej to, co dotyczy CAŁEJ strony (synchronizacja KSeF,
           akcja strony). Odnośnik powrotny stoi NAD tytułem (`.back-link`),
           nie w osobnym pasku. Dawny górny pasek 64 px + padding 22 px + osobny
           nagłówek = ~110 px pionu na trzykrotne powtórzenie tego samego słowa. */
        body.has-shell .page-header { margin-bottom: 14px; }
        body.has-shell .page-header h1 { font-size: 22px; letter-spacing: -0.01em; }
        body.has-shell .back-link { margin-bottom: 4px; font-size: 0.85rem; }

        /* Krok 7.1 (runda 30): szczegóły faktury — dwie kolumny od --detail-min
           (1120px) OBSZARU TREŚCI .detail, container query, NIE media query na
           oknie (zwinięte menu zmienia dostępne miejsce, patrz próg 1166px
           kolumny "Przypisanie" wyżej w pliku — ten sam wzorzec).
           @container nie przyjmuje var(), więc próg stoi tu literalnie —
           1120px = wartość tokenu --detail-min (patrz :root wyżej).
           .aside jest ZAWSZE flex-column z gap (nigdy margin-bottom na karcie,
           żeby nie dublować odstępu z .aside w trybie "obok siebie" niżej). */
        .detail .aside { display: flex; flex-direction: column; gap: 18px; }
        .detail .aside > .card { margin-bottom: 0; }
        .detail .work, .detail .hist { min-width: 0; }

        /* container-type na .page--detail, NIE na .detail samym w sobie —
           element z container-type staje się kontenerem zapytań dla SWOICH
           DZIECI, nie odpytuje własnego rozmiaru (odkryte pomiarem 7.1: przy
           container-type na .detail reguła "@container (min-width: 1120px)
           { .detail { display: grid } }" nigdy się nie uruchamiała, mimo że
           .detail miało realnie 1458px). main.page.page--detail ma własny
           padding i .detail jest jego bezpośrednim dzieckiem bez marginesów,
           więc szerokość kontenera = szerokość .detail. */
        body.has-shell .page--detail { container-type: inline-size; }

        /* < 1120px: jedna kolumna, kolejność z DOM (żaden order/display:contents).
           "Faktura" i "Płatność" obok siebie (3:2) w paśmie drugi_próg..1120px —
           poniżej drugiego progu spadają z powrotem do jednej pod drugą (reguła
           bazowa .aside wyżej, flex-direction: column, już to daje domyślnie). */
        @container (min-width: 720px) and (max-width: 1119.98px) {
            /* 720px — ZMIERZONE (commit D2, po E i F, na docelowej strukturze
               .kv/.ksef/.totals i pionowej "Płatność"), NIE już tylko szacunek
               z pliku referencyjnego. Metoda: wstrzyknięty `.detail{max-width:
               Wpx}`, przemiatanie 1119→404px co 5px (okno 1920×1000), na 3
               stanach (księgowość/0107128368 z realnym 35-znakowym numerem
               KSeF, księgowość/DBG-1 zapłacona u wystawcy, dyrektor/
               0107128368 plakietka neutralna) — sprawdzane: zawijanie
               .ksef-val/.amount-value/.totals b (wysokość > 1,4×line-height)
               i przepełnienie (scrollWidth>clientWidth) całej karty
               "Płatność". WYNIK: zero defektów w CAŁYM przemiatanym zakresie,
               aż do 404px — próg 720px ma więc DUŻY zapas, nie jest granicą
               techniczną w żadną stronę. 720px pozostaje bez zmian: brak
               powodu, żeby zejść niżej (żaden zmierzony defekt tego nie
               wymaga), a schodzenie niżej niepotrzebnie zwiększyłoby zakres
               trybu "obok siebie" bez korzyści. Przy okazji: `.work` (poza
               zakresem 7.1, karta "Przypisanie", `.row2`) ma WŁASNY,
               niezależny próg złamania między 600 a 720px (nakładające się
               etykiety pól — poza zakresem, nie naprawiane tu, zgłoszone w
               raporcie) — 720px koresponduje z tym, że CAŁY układ jednej
               kolumny (nie tylko .aside) jest tam jeszcze wizualnie
               nienaruszony, czysty zbieg okoliczności potwierdzony pomiarem,
               nie przesłanka projektowa. */
            .detail .aside { flex-direction: row; }
            .detail .aside > .card:first-child { flex: 3 1 0; min-width: 0; }
            .detail .aside > .card:last-child { flex: 2 1 0; min-width: 0; }
        }

        /* >= 1120px: dwie kolumny. .aside zajmuje OBIE kolejki prawej strony
           (nazwa obszaru "side" powtórzona w obu wierszach template-areas —
           grid przypisuje span automatycznie, bez jawnego "span 2"). */
        @container (min-width: 1120px) {
            .detail {
                display: grid;
                grid-template-columns: minmax(0, var(--read-max)) var(--side-w);
                grid-template-areas: "work side" "hist side";
                column-gap: 18px;
                row-gap: 18px;
            }
            .detail .work { grid-area: work; }
            .detail .hist { grid-area: hist; }
            /* align-self: start (nie domyślne stretch) — .aside inaczej rozciąga
               się do wysokości .work + .hist razem (pomiar 7.1, sekcja 3), a
               przyklejanie niżej ma działać na wysokości WŁASNEJ treści. */
            .detail .aside { grid-area: side; flex-direction: column; align-self: start; }

            /* Krok 7.2 (runda 30): przyklejanie prawej kolumny — TYLKO gdy okno
               jest dość wysokie. 720px = najwyższa zmierzona wysokość TREŚCI
               .aside po kroku 7.1: 687,5px (księgowość, DBG-1, raport 7.1,
               sekcja "Po oględzinach (J)", pkt 7) + 14px odstępu od góry +
               zapas. Warunek dwuczęściowy: ta reguła stoi już wewnątrz
               @container (min-width: 1120px), więc poniżej progu dwóch kolumn
               nic tu się nie uruchamia — w jednej kolumnie nic się nie przykleja. */
            @media (min-height: 720px) {
                .detail .aside {
                    position: sticky;
                    top: 14px;
                    /* zabezpieczenie: gdyby niezmierzony stan przekroczył okno,
                       dół kolumny zostaje osiągalny jej własnym przewijaniem,
                       zamiast zniknąć poza dołem ekranu. */
                    max-height: calc(100vh - 28px);
                    overflow-y: auto;
                }
            }
        }
