/* Komponenty interfejsu.

   Nazwy klas opisuja rzecz, a nie strone, na ktorej stoi: `.card`, a nie
   `.machine-details-card`. Stary front mial ten sam kafelek pod szescioma
   nazwami w szesciu arkuszach, wiec poprawka w jednym miejscu nie schodzila
   na pozostale.

   Uwaga o hakach dla skryptow: czesc elementow nosi obok klasy wygladu takze
   dawna nazwe (np. `class="card transparent-card"`). Ta druga nie styluje
   niczego - szukaja po niej skrypty. Docelowo powinna dostac przedrostek
   `js-`; do tego czasu obowiazuje zasada, ze arkusz nie odwoluje sie do
   nazw z tej grupy, a skrypty nie odwoluja sie do nazw wygladu.
*/

@layer components {
    /* ==================================================================
       KARTA
       Podstawowy pojemnik na tresc. Sekcja strony, kafelek danych, okno
       modalne i pasek filtrow to wszystko ta sama karta.
    ================================================================== */
    .card {
        display: flex;
        flex-direction: column;
        gap: var(--space-sm);
        padding: var(--card-pad);
        background: var(--surface);
        border: 1px solid var(--border);
        border-radius: var(--radius-lg);
    }

    .card--raised {
        background: var(--surface-raised);
        box-shadow: var(--shadow-md);
    }

    .card--flush {
        padding: 0;
        overflow: hidden;
    }

    /* Tytul sekcji z wlosem pod spodem. Wlos idzie przez cala szerokosc
       karty, a nie tylko pod napis - stad ujemne marginesy boczne. */
    .card__title {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-sm);
        margin: calc(var(--card-pad) * -1) calc(var(--card-pad) * -1) 0;
        padding: var(--space-sm) var(--card-pad);
        color: var(--text);
        font-size: var(--text-md);
        font-weight: var(--weight-medium);
        border-bottom: 1px solid var(--hairline);
    }

    .card__title > :last-child:not(:only-child) {
        margin-inline-start: auto;
    }

    .card--flush .card__title {
        margin: 0;
    }

    .card__footer {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-xs);
        margin: 0 calc(var(--card-pad) * -1) calc(var(--card-pad) * -1);
        padding: var(--space-sm) var(--card-pad);
        border-top: 1px solid var(--hairline);
    }

    /* Na telefonie glowna akcja stopki formularza zajmuje caly rzad -
       latwiejszy cel i bez pustego pasa obok przycisku. */
    @media (max-width: 48rem) {
        .card__footer.cluster--end {
            justify-content: stretch;
        }

        .card__footer.cluster--end > .btn,
        .card__footer.cluster--end > button {
            flex: 1 1 auto;
        }
    }

    /* ==================================================================
       POLE DANYCH
       Para podpis-wartosc. Buduje kazda karte z danymi: frezarke, firme,
       profil, model. Podpis jest maly i przygaszony, wartosc czytelna -
       dzieki temu karte z dwunastoma polami skanuje sie wzrokiem.
    ================================================================== */
    .field {
        display: flex;
        flex-direction: column;
        gap: var(--space-3xs);
        min-width: 0;
    }

    .field__label {
        color: var(--text-faint);
        font-size: var(--label-size);
        font-weight: var(--weight-medium);
        letter-spacing: var(--label-spacing);
        text-transform: var(--label-transform);
    }

    .field__value {
        min-width: 0;
        color: var(--text);
        font-size: var(--text-md);
        overflow-wrap: break-word;
    }

    /* Numery seryjne, PLC, wersje, wspolrzedne. */
    .field__value--mono {
        font-family: var(--font-mono);
        font-variant-numeric: tabular-nums;
        font-size: var(--text-sm);
    }

    /* Podpowiedz pod polem - `help_text` z formularza Django. */
    .field__help {
        color: var(--text-faint);
        font-size: var(--text-xs);
    }

    /* Prefiks kraju przed wartoscia: numer telefonu albo NIP. To dwie
       kontrolki, ktore sa jedna wartoscia, wiec stoja obok siebie - tak sie
       je czyta i tak dyktuje.

       Z golego `<div>`, w ktorym stoja w szablonach rejestracji, wychodzilo
       co innego: select i input maja `width: 100%` z warstwy `base`, wiec
       kazde bralo cala linie i prefiks wygladal jak osobne pole bez podpisu.

       Prefiks nie ma szerokosci z gory znanej i to jest tu cala trudnosc.
       Lista nie pokazuje "+48", tylko "Polska (+48)" (a przy NIP-ie "Polska
       (PL)"), wiec potrzebuje okolo 115 px. Do tego `customSelects.js`
       podmienia kazdy `<select>` na wlasny widget, wiec dzieckiem tego
       pojemnika bywa `div.custom-select`, a nie `select` - regula musi
       lapac oba, inaczej nie lapie nic i widget bierze cala szerokosc,
       zostawiajac numerowi 26 px.

       Obie kontrolki maja wiec wielkosc wyjsciowa i wolno im rosnac, a caly
       pojemnik moze zawijac: gdy oba wymiary wyjsciowe nie mieszcza sie
       w karcie - a na telefonie sie nie mieszcza - kazda wchodzi na wlasny
       wiersz i wypelnia go. Podpis stoi wtedy nad para, wiec nadal widac,
       ze to jedno pole. `min-width: 0` jest konieczne przy obu: bez niego
       pole flexa nie zwezy sie ponizej swojej tresci. */
    .prefixed-field {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-xs);
    }

    .prefixed-field > select,
    .prefixed-field > .custom-select {
        flex: 1 1 9rem;
        min-width: 0;
    }

    .prefixed-field > input {
        flex: 2 1 10rem;
        min-width: 0;
    }

    /* Para zajmuje dwa tory siatki `.fields`, bo w jednym stoi ciasno: tor ma
       tam 12rem, a przy szerokiej karcie i tak nie wiecej niz 198 px - po
       odjeciu prefiksu na wartosc zostaje mniej, niz zajmuje "601 234 567".

       Zapytanie o szerokosc jest tu konieczne i nie jest ostroznoscia na
       wyrost. `span 2` nie schodzi do jednej kolumny, gdy tylko jedna sie
       miesci - przeciwnie, ZMUSZA siatke do zrobienia drugiej. Na telefonie
       wychodzily z tego dwa tory po 192 i 101 px, czyli caly formularz
       raportu sciesniony do polowy szerokosci karty. Ponizej 30rem karta ma
       na tyle malo miejsca, ze siatka i tak jest jednokolumnowa, a para
       zawija sie w niej sama (`flex-wrap` wyzej). */
    @media (min-width: 30rem) {
        .fields > .field:has(> .prefixed-field) {
            grid-column: span 2;
        }
    }

    /* Brak danych ma byc widoczny jako brak, a nie jako tresc. */
    .field__value.is-empty,
    .is-empty {
        color: var(--text-faint);
        font-weight: var(--weight-normal);
    }

    /* Karta z dwiema postaciami pola: wartoscia do czytania i widgetem do
       pisania. Klase `editing` zaklada skrypt po klikniciu "Edytuj", a poza
       nia widoczna jest tylko wartosc.

       Widget stoi w znaczniku zawsze - takze poza trybem edycji - bo to on
       niesie dane formularza. Gdyby wchodzil dopiero po kliknieciu, po bledzie
       walidacji nie bylo by czego pokazac.

       Regula celuje wylacznie w pola, ktore maja obie postacie naraz
       (`:has(> .field__value)`). Bez tego zawezenia chowalaby pola w kazdej
       karcie z formularzem w calej aplikacji - a wiekszosc formularzy zadnej
       wartosci do czytania nie ma i zostalyby same podpisy. */
    .card:not(.editing) .field:has(> .field__value) :where(input, select, textarea, .custom-select),
    .card:not(.editing) .field:has(> .field__value) .errorlist {
        display: none;
    }

    .card.editing .field > .field__value {
        display: none;
    }

    /* Pole edytowalne dwuklikiem (dane techniczne modelu) - kursor i tlo
       maja to zdradzic przed klikniciem. */
    .is-editable .field__value {
        margin: -3px -7px;
        padding: 3px 7px;
        border-radius: var(--radius-xs);
        cursor: pointer;
        transition: background var(--dur-fast) var(--ease);
    }

    .is-editable .field__value:hover {
        background: var(--surface-active);
    }

    /* ==================================================================
       PRZYCISK
       Jedna wysokosc z polami formularza, wiec rzad "pole + przycisk"
       stoi rowno bez poprawek.
    ================================================================== */
    .btn {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: var(--space-2xs);
        min-height: var(--control-height);
        padding: 0 var(--space-sm);
        color: var(--text);
        font-size: var(--text-sm);
        font-weight: var(--weight-medium);
        line-height: 1;
        white-space: nowrap;
        text-decoration: none;
        background: var(--surface-raised);
        border: 1px solid var(--border);
        border-radius: var(--radius-sm);
        cursor: pointer;
        transition: background var(--dur-fast) var(--ease),
                    border-color var(--dur-fast) var(--ease),
                    color var(--dur-fast) var(--ease);
    }

    /* Najechanie idzie kryciem, a nie wpisanym odcieniem. Przycisk stoi raz
       na plotnie, raz na karcie, raz w oknie, a kazde z tych tel ma inna
       jasnosc - jedna wpisana wartosc bylaby na czesci z nich ciemniejsza
       od spoczynkowego tla przycisku i przycisk gaslby przy najechaniu
       zamiast sie rozjasniac. Krycie sklada sie z tym, co pod spodem, wiec
       rozjasnia zawsze i o tyle samo. */
    .btn:hover {
        color: var(--text);
        background: var(--surface-active);
        border-color: var(--border-strong);
    }

    .btn:active {
        transform: translateY(1px);
    }

    .btn:disabled,
    .btn[aria-disabled="true"] {
        opacity: 0.5;
        pointer-events: none;
    }

    /* Akcja glowna strony - na kazdym ekranie najwyzej jedna. */
    .btn--primary {
        color: var(--text-on-accent);
        background: var(--accent);
        border-color: var(--accent);
    }

    .btn--primary:hover {
        color: var(--text-on-accent);
        background: var(--accent-strong);
        border-color: var(--accent-strong);
    }

    /* Usuwanie. Czerwien pelna, a nie sam czerwony napis - to jedyne
       akcje w aplikacji, ktorych nie da sie cofnac. */
    .btn--danger {
        color: #fff;
        background: var(--danger-solid);
        border-color: var(--danger-solid);
    }

    .btn--danger:hover {
        color: #fff;
        background: var(--danger-solid-hover);
        border-color: var(--danger-solid-hover);
    }

    /* Akcja drugorzedna, bez wlasnego tla - do rzedow, gdzie przyciskow
       jest duzo (komorki tabeli, stopki kart). */
    .btn--ghost {
        background: transparent;
        border-color: transparent;
        color: var(--text-muted);
    }

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

    .btn--sm {
        min-height: var(--control-height-sm);
        padding: 0 var(--space-xs);
        font-size: var(--text-xs);
    }

    .btn--icon {
        width: var(--control-height);
        padding: 0;
    }

    .btn--icon.btn--sm {
        width: var(--control-height-sm);
    }

    .btn--block {
        width: 100%;
    }

    /* Przelacznik widoku: filtry paska frezarek, zakresy czasu w SPC.
       Wlaczony stan niesie `aria-pressed` albo `aria-current`, nie klasa -
       dzieki temu czytnik ekranu tez o nim wie. */
    .btn--tab[aria-pressed="true"],
    .btn--tab[aria-current] {
        color: var(--accent-soft);
        background: var(--surface-active);
        border-color: var(--accent-line);
    }

    /* Liczba przy nazwie filtru ("Wszystkie 16"). `muted`, a nie `faint`:
       przycisk pod kursorem jasnieje o `--surface-active`, a na tym tle
       `faint` schodzi do 4.1:1. Poza tym jest to liczba do odczytania,
       a nie podpis. */
    .btn__count {
        color: var(--text-muted);
        font-family: var(--font-mono);
        font-size: var(--text-2xs);
        font-variant-numeric: tabular-nums;
    }

    .btn--tab[aria-pressed="true"] .btn__count,
    .btn--tab[aria-current] .btn__count {
        color: var(--accent-soft);
    }

    .pager .btn {
        min-width: 6rem;
    }

    /* ==================================================================
       ODZNAKA I KROPKA STANU
       Cztery znaczenia dziedziny: aktualne, zbliza sie termin, po terminie,
       informacja. Kolor nigdy nie idzie sam - zawsze towarzyszy mu napis
       albo `title`, bo na samym kolorze nie da sie oprzec dostepnosci.
    ================================================================== */
    .badge {
        display: inline-flex;
        align-items: center;
        gap: var(--space-2xs);
        padding: 0.15em var(--space-xs);
        font-size: var(--text-2xs);
        font-weight: var(--weight-medium);
        line-height: 1.6;
        white-space: nowrap;
        border: 1px solid transparent;
        border-radius: var(--radius-pill);
    }

    .badge--ok      { color: var(--ok);     background: var(--ok-quiet);     border-color: var(--ok-line); }
    .badge--warn    { color: var(--warn);   background: var(--warn-quiet);   border-color: var(--warn-line); }
    .badge--danger  { color: var(--danger); background: var(--danger-quiet); border-color: var(--danger-line); }
    .badge--info    { color: var(--info);   background: var(--info-quiet);   border-color: var(--info-line); }
    .badge--neutral { color: var(--text-muted); background: var(--surface-active); border-color: var(--border); }

    /* Stan zgloszenia. Kolor bierze sie z atrybutu, nie z klasy dolozonej
       w szablonie - tak samo, jak `data-level` przy komunikatach. Dzieki temu
       wartosc jest jedna: ta sama, ktora stoi w bazie. */
    .badge[data-status="waiting"]     { color: var(--warn);       background: var(--warn-quiet);     border-color: var(--warn-line); }
    .badge[data-status="in_progress"] { color: var(--info);       background: var(--info-quiet);     border-color: var(--info-line); }
    .badge[data-status="closed"]      { color: var(--text-muted); background: var(--surface-active); border-color: var(--border); }

    .status-dot {
        flex: none;
        width: 0.5rem;
        height: 0.5rem;
        border-radius: 50%;
        background: var(--dot-color, var(--text-faint));
    }

    .status-dot--ok     { --dot-color: var(--ok); }
    .status-dot--warn   { --dot-color: var(--warn); }
    .status-dot--danger { --dot-color: var(--danger); }

    /* ==================================================================
       NAWIGACJA GLOWNA
       Wiersze menu bocznego. Ten sam `.nav-link` sluzy pozycji glownej,
       przelacznikowi grupy i pozycji podmenu - roznia sie modyfikatorem,
       nie osobnym komponentem.
    ================================================================== */
    .nav-list {
        display: flex;
        flex-direction: column;
        gap: var(--space-3xs);
    }

    .nav-link {
        display: flex;
        width: 100%;
        align-items: center;
        gap: var(--space-xs);
        padding: var(--space-xs) var(--space-sm);
        color: var(--text-muted);
        font-size: var(--text-md);
        text-align: start;
        text-decoration: none;
        border-radius: var(--radius-sm);
        cursor: pointer;
        transition: color var(--dur-fast) var(--ease),
                    background var(--dur-fast) var(--ease);
    }

    .nav-link:hover {
        color: var(--text);
        background: var(--surface-hover);
    }

    /* Biezaca strona. Selektor celuje w atrybut, bo to on niesie te
       informacje takze czytnikowi ekranu - klasa `active-link` mowila ja
       wylacznie widzacym.

       `page` to strona, na ktorej stoimy, `true` - grupa albo sekcja, ktora
       ja zawiera. Kreska przy krawedzi nalezy sie tylko tej pierwszej.

       Tlo jest bialym szklem, nie poswiata czerwieni firmowej - tak samo,
       jak bylo wczesniej (`#ffffff1c` pod pozycja paska gornego). Czerwien
       zostaje w kresce przy krawedzi: znak marki wystarczy postawic raz,
       a rozlana na cale tlo pozycji czytala sie jak ostrzezenie. Ten sam
       zestaw dostaja pozycje menu bocznego, profil u dolu i wlaczony
       przelacznik widoku. */
    .nav-link[aria-current] {
        color: var(--text);
        background: var(--surface-active);
    }

    .nav-link[aria-current="page"] {
        box-shadow: inset 2px 0 0 var(--accent);
    }

    .nav-link__icon {
        flex: none;
        width: 1.125rem;
        color: var(--text-faint);
        font-size: var(--text-md);
        line-height: 1;
        text-align: center;
        transition: color var(--dur-fast) var(--ease);
    }

    .nav-link:hover .nav-link__icon,
    .nav-link[aria-current] .nav-link__icon {
        color: var(--accent-soft);
    }

    /* --- znacznik nieprzeczytanych --------------------------------------
       Kropka z liczba siedzi na ikonie, a nie przy prawej krawedzi wiersza:
       wiersz menu jest szeroki, wiec liczba postawiona przy jego koncu
       czytala sie jak licznik calej grupy, a nie powiadomien.

       Pojemnik musi byc `relative`. Bez niego znacznik szukal najblizszego
       ustawionego rodzica i znajdowal `.app-sidebar` (jest `sticky`) -
       ladowal wiec w gornym rogu menu, na znaku firmowym, kilkaset pikseli
       nad dzwonkiem. */
    .nav-icon-badge {
        position: relative;
        display: inline-flex;
        flex: none;
    }

    /* Znacznik wychodzi w gore i lekko w bok, ale nie poza wlasny odstep -
       miedzy ikona a napisem jest `--space-xs`, wiec dwucyfrowa liczba
       wypchnieta dalej dotykalaby slowa "Powiadomienia". Obwodka w kolorze
       plotna odcina go od samego dzwonka. */
    .nav-badge {
        position: absolute;
        top: -0.3rem;
        inset-inline-end: -0.2rem;
        min-width: 0.95rem;
        padding: 0 0.2rem;
        color: var(--text-on-accent);
        font-size: var(--text-2xs);
        font-weight: var(--weight-bold);
        line-height: 0.95rem;
        text-align: center;
        background: var(--accent);
        border-radius: var(--radius-pill);
        box-shadow: 0 0 0 0.125rem var(--canvas);
    }

    /* Sam dzwonek tez sie rozswietla - w ciemnej kolumnie mala plamka przy
       wygaszonej ikonie ginie, zwlaszcza gdy menu jest przewiniete. */
    .nav-icon-badge:has(.nav-badge:not([hidden])) .nav-link__icon {
        color: var(--accent-soft);
    }

    /* Ta sama kropka na dzwonku w pasku gornym. Przycisk jest wiekszy od
       swojej ikony, wiec znacznik wchodzi do srodka zamiast wychodzic poza
       obrys - poza nim wjezdzalby na krawedz paska i na krawedz ekranu. */
    .app-bar__action .nav-badge {
        top: 0.25rem;
        inset-inline-end: 0.2rem;
    }

    .app-bar__action:has(.nav-badge:not([hidden])) {
        color: var(--text);
    }

    /* Nazwy sekcji bywaja dlugie ("Frezarki firm", "Kalibracje frezarek"),
       a menu ma stala szerokosc - wiec skracaja sie wielokropkiem zamiast
       lamac wiersz albo rozpychac kolumne. */
    .nav-link__text {
        flex: 1;
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* --- grupa z podmenu ------------------------------------------------
       Sekcje obu hubow stoja wprost w menu, a nie dopiero na stronie huba.
       O tym, czy grupa jest otwarta, mowi `aria-expanded` na przycisku -
       jedna wartosc dla arkusza i dla czytnika ekranu naraz. */
    .nav-group__chevron {
        flex: none;
        margin-inline-start: auto;
        color: var(--text-faint);
        font-size: var(--text-2xs);
        transition: transform var(--dur-med) var(--ease);
    }

    .nav-group__toggle[aria-expanded="true"] .nav-group__chevron {
        transform: rotate(180deg);
    }

    .nav-sub {
        display: flex;
        flex-direction: column;
        gap: var(--space-3xs);
        margin-block: 0;
        margin-inline-start: var(--space-lg);
        padding-inline-start: var(--space-xs);
        border-inline-start: 1px solid var(--hairline);
        max-height: 0;
        overflow: hidden;
        opacity: 0;
        visibility: hidden;
        transform: translateY(-0.25rem);
        transition:
            max-height var(--dur-med) var(--ease),
            margin-block var(--dur-med) var(--ease),
            opacity var(--dur-med) var(--ease),
            transform var(--dur-med) var(--ease),
            visibility 0s linear var(--dur-med);
    }

    .nav-group__toggle[aria-expanded="true"] + .nav-sub {
        max-height: 40rem;
        margin-block: var(--space-3xs) var(--space-2xs);
        opacity: 1;
        visibility: visible;
        transform: translateY(0);
        transition:
            max-height var(--dur-med) var(--ease),
            margin-block var(--dur-med) var(--ease),
            opacity var(--dur-med) var(--ease),
            transform var(--dur-med) var(--ease),
            visibility 0s linear 0s;
    }

    #nav-sub-notifications {
        scrollbar-width: thin;
        scrollbar-color: var(--border-strong) transparent;
    }

    .nav-group__toggle[aria-expanded="true"] + #nav-sub-notifications {
        max-height: max(8rem, calc(100dvh - 30rem));
        overflow-y: auto;
        overscroll-behavior: contain;
    }

    @media (prefers-reduced-motion: reduce) {
        .nav-sub,
        .nav-group__chevron {
            transition: none;
        }
    }

    .nav-link--sub {
        padding-block: var(--space-2xs);
        color: var(--text-faint);
        font-size: var(--text-xs);
    }

    .nav-notification-item {
        display: flex;
        align-items: center;
        gap: var(--space-3xs);
    }

    .nav-notification-item .notification-item-link {
        min-width: 0;
        flex: 1 1 auto;
    }

    .nav-notification-item--unread .notification-item-link {
        color: var(--text);
    }

    .nav-notification-item form {
        flex: 0 0 auto;
    }

    .nav-notification-remove {
        padding: var(--space-3xs);
        color: var(--text-faint);
    }

    .nav-notification-clear {
        display: flex;
        justify-content: flex-start;
        padding-block-end: var(--space-3xs);
        border-block-end: 1px solid var(--hairline);
    }

    .nav-notification-clear form {
        width: 100%;
    }

    .nav-notification-clear__button {
        width: 100%;
        padding-inline: var(--space-2xs);
        color: var(--text-faint);
        font-size: var(--text-2xs);
        opacity: 0.75;
    }

    .nav-link--sub[aria-current] {
        color: var(--accent-soft);
    }

    /* --- profil u dolu menu ---------------------------------------------- */
    .nav-profile {
        display: flex;
        align-items: center;
        gap: var(--space-xs);
        margin-top: var(--space-2xs);
        padding: var(--space-2xs) var(--space-sm);
        color: var(--text-muted);
        font-size: var(--text-sm);
        text-decoration: none;
        border-radius: var(--radius-sm);
        transition: color var(--dur-fast) var(--ease),
                    background var(--dur-fast) var(--ease);
    }

    .nav-profile:hover {
        color: var(--text);
        background: var(--surface-hover);
    }

    .nav-profile[aria-current] {
        color: var(--text);
        background: var(--surface-active);
        box-shadow: inset 2px 0 0 var(--accent);
    }

    .nav-profile .avatar {
        --avatar-size: 1.75rem;
        border: 1px solid var(--border);
    }

    .nav-profile__text {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* ==================================================================
       MENU BOCZNE
       Lista sekcji obok tresci: podstrony frezarki, profil, "Moja firma",
       "Zarzadzaj". Jeden komponent zamiast czterech osobnych arkuszy.
    ================================================================== */
    .side-menu {
        display: flex;
        flex-direction: column;
        gap: var(--space-3xs);
        padding: var(--space-xs);
        background: var(--surface);
        border: 1px solid var(--border);
        border-radius: var(--radius-lg);
    }

    .side-menu__link {
        display: flex;
        align-items: center;
        gap: var(--space-xs);
        padding: var(--space-xs) var(--space-sm);
        color: var(--text-muted);
        font-size: var(--text-sm);
        text-decoration: none;
        border-radius: var(--radius-sm);
        transition: color var(--dur-fast) var(--ease),
                    background var(--dur-fast) var(--ease);
    }

    .side-menu__link:hover {
        color: var(--text);
        background: var(--surface-hover);
    }

    .side-menu__link[aria-current] {
        color: var(--text);
        background: var(--surface-active);
        box-shadow: inset 2px 0 0 var(--accent);
    }

    /* "Kalibruj" - jedyna pozycja, ktora nie przelacza widoku, tylko
       zaczyna prace. Wyglada wiec jak akcja, a nie jak zakladka. */
    .side-menu__link--primary {
        color: var(--text-on-accent);
        background: var(--accent);
        font-weight: var(--weight-medium);
        justify-content: center;
    }

    .side-menu__link--primary:hover {
        color: var(--text-on-accent);
        background: var(--accent-strong);
    }

    /* Pozycja wyprowadzajaca poza panel (statystyki zaawansowane). */
    .side-menu__link--external {
        color: var(--info);
    }

    .side-menu__sep {
        height: 1px;
        margin: var(--space-2xs) var(--space-xs);
        background: var(--hairline);
    }

    /* Ponizej tego progu menu sekcji nie ma juz wlasnej kolumny i kladzie sie
       w poziomie nad trescia.

       Zawija sie przy tym, a nie przewija. Przewijalo sie do tej pory i to
       bylo najgorsze miejsce w calej aplikacji: menu frezarki ma siedem
       pozycji, ktore przy 375 px skladaja sie na 712 px - widac bylo dwie,
       a o piesciu pozostalych (w tym o "Statystykach zaawansowanych"
       i o "Kalibruj") nie bylo skad sie dowiedziec, bo pasek nie mial ani
       cienia, ani strzalki. Nawigacja, ktorej nie widac, nie jest
       nawigacja; trzy rzedy pigulek kosztuja 80 px i pokazuja wszystko. */
    @media (max-width: 60rem) {
        .side-menu {
            flex-direction: row;
            flex-wrap: wrap;
        }

        .side-menu__link {
            white-space: nowrap;
        }

        .side-menu__link[aria-current] {
            box-shadow: inset 0 -2px 0 var(--accent);
        }

        .side-menu__sep {
            width: 1px;
            height: auto;
            margin: var(--space-2xs);
        }
    }

    /* ==================================================================
       TABELA W KARCIE
       Sama tabela jest juz ostylowana w warstwie `base`. Tutaj tylko
       pojemnik, ktory pozwala jej przewijac sie w poziomie, zamiast
       rozpychac strone - listy frezarek maja po osiem kolumn.
    ================================================================== */
    .table-wrap {
        overflow-x: auto;
        overscroll-behavior-x: contain;
    }

    /* Tabela wypelnia pojemnik, a przewija sie dopiero wtedy, gdy kolumny
       naprawde sie nie miesza - i teraz robi dokladnie to, co ten opis mowi.

       Stalo tu wczesniej `min-width: max-content` i to byl blad: razem
       z `width: 1%` kolumny akcji tworzylo to zapetlona zaleznosc -
       szerokosc tabeli zalezy od komorek, a komorka od szerokosci tabeli.
       Przegladarka rozwiazywala ja absurdem: tabela firm z szescioma
       krotkimi kolumnami rozdymala sie do 8641 px w pojemniku szerokim na
       1126 px i cala lista jechala w bok.

       Zastapila je stala granica 44rem - i ta petli nie tworzyla, ale
       przewijanie w bok zamieniala z ostatecznosci w regule. Kazda tabela
       stawala sie szeroka na 704 px, takze wtedy, gdy jej tresc miescila sie
       w polowie tego: pracownicy firmy (szesc kolumn) potrzebuja przy 1180 px
       dokladnie tyle, ile ma ich kolumna - 597 px - a granica kazala im
       jechac w bok o 107. Tolerancje osi na stronie frezarki to cztery
       krotkie kolumny w karcie szerokiej na 370 px i przewijaly sie o 333 px
       przy KAZDEJ szerokosci okna, takze na komputerze.

       Bez granicy szerokosc tabeli wyznacza jej wlasna tresc: przegladarka
       i tak nie zwezi kolumny ponizej najdluzszego slowa, wiec nic sie nie
       urywa - tekst w komorkach zawija sie i tyle. Przewijaja sie juz tylko
       te tabele, ktore naprawde sa szersze niz miejsce, ktore maja: lista
       frezarek firm z osmioma kolumnami, w tym trzema z przyciskami.

       Sam `width: 100%` zostaje - to on daje `width: 1%` przy kolumnie akcji
       punkt odniesienia, dzieki ktoremu ta zwija sie do tresci. */
    .table-wrap table {
        width: 100%;
    }

    /* Adres e-mail i numer telefonu stoja na listach uzytkownikow jako
       przyciski - to odsylacze `mailto:` i `tel:`. Przycisk z zasady nie
       lamie napisu (`.btn` nizej), a adres nie ma spacji, wiec kolumna
       z "j.kowalski@densub.pl" zadala 160 px i to ona rozpychala cala tabele
       pracownikow: przy 1024 px potrzebowala 646 px w kolumnie szerokiej na
       449. W komorce tabeli ten zakaz wiec nie obowiazuje - napis zawija sie,
       gdy kolumna jest ciasna, a przy szerokim oknie i tak zostaje w jednej
       linii, bo miejsca nie brakuje.

       Kolumna akcji jest z tego wyjeta i musi byc. Ma `width: 1%`, czyli
       "zwin sie do tresci", a `overflow-wrap: anywhere` sprowadza tresc
       przycisku do jednej litery: kolumna liczyla wtedy 51 px, a "Otworz"
       i "Usun frezarki" staly w niej pionowo, litera pod litera, podczas gdy
       reszta tabeli miala miejsca w nadmiarze. Adresow ani numerow w tej
       kolumnie nie ma - stoja w niej same przyciski o krotkich napisach,
       wiec zakaz lamania nic tu nie rozpycha. */
    .table-wrap :where(td, th):not(.col-actions) .btn {
        white-space: normal;
        overflow-wrap: anywhere;
    }

    /* Alias frezarki wpisuje sie wprost w komorce tabeli - skrypt podmienia
       jej tresc na formularz z tym polem. Pole ma byc czescia wiersza, a nie
       kontrolka rozpychajaca kolumne, wiec dostaje wysokosc mniejszej
       kontrolki i szerokosc z gory znana. */
    .alias-input {
        width: 11rem;
        min-height: var(--control-height-sm);
        padding-block: 0;
    }

    /* Cztery kolumny szczegolow zgloszenia sa schowane, dopoki przycisk
       "Pokaz szczegoly" ich nie pokaze - inaczej tabela ma trzynascie kolumn
       i nie miesci sie w zadnym oknie. Skrypt przelacza `show`, tak samo jak
       na menu rozwijanym. */
    /* Napis otwierajacy profil i wartosc pola edytowana dwuklikiem sa celem
       klikniecia, choc nie sa przyciskami - kursor ma to powiedziec. */
    .user-requested-link,
    .field__value.field-content {
        cursor: pointer;
    }

    .details-col {
        display: none;
    }

    .details-col.show {
        display: table-cell;
    }

    /* Kolumna z akcjami trzyma sie prawej krawedzi i nie zawija. `width: 1%`
       przy tabeli o okreslonej szerokosci znaczy "zwin sie do tresci" -
       dziala, bo `width: 100%` wyzej daje te szerokosc. */
    .col-actions {
        width: 1%;
        white-space: nowrap;
        text-align: right;
    }

    /* --- tabela na telefonie --------------------------------------------
       Wiersz przestaje byc wierszem i staje sie kafelkiem: kazda komorka to
       para "podpis - wartosc", ta sama, z ktorej zlozone sa karty danych
       (`.field` wyzej). Dzieki temu lista firm na telefonie wyglada jak
       reszta aplikacji, a nie jak tabela ogladana przez dziurke.

       Powod jest arytmetyczny. Lista frezarek ma do dziesieciu kolumn - na
       ekranie szerokim na 375 px nie ma dla nich sposobu. Zwezanie do tresci
       nie pomaga: na kolumne wypada wtedy okolo 35 px, wiec numer seryjny
       lamie sie na cztery wiersze, a nazwa modelu na trzy. Przewijanie w bok
       tez nie: odjezdza w nim najpierw kolumna pierwsza, czyli ta, po ktorej
       wiersz sie rozpoznaje.

       Podpis przy kazdej wartosci bierze sie z naglowka kolumny -
       `responsiveTables.js` przepisuje jego tresc do `data-label`. Ten sam
       skrypt nadaje `data-wide` i robi to dopiero od piatej kolumny:
       tabela czterokolumnowa (osie kalibracji, pliki frezarki) miesci sie
       na telefonie w calosci albo niewiele jej brakuje, a rozlozona na
       kafelki bylaby trzy razy dluzsza, niz musi.

       Bez skryptu nic sie nie psuje - `data-wide` wtedy nie ma, wiec tabela
       zostaje tabela i zwezaja ja same kolumny.
    ---------------------------------------------------------------------- */
    @media (max-width: 48rem) {
        /* Tabela, ktora nie schodzi do kafelkow, ma sie na telefonie zmiescic
           w calosci - i brakuje jej do tego niewiele. Wypelnienie boczne
           komorki jest jedynym miejscem, z ktorego da sie to wziac bez
           ruszania tresci: cztery kolumny po 12 px z kazdej strony to 96 px
           oddane na sam odstep, czyli ponad cwierc ekranu. Przy 4 px
           tolerancje osi mieszcza sie w 343 px z zapasem, przy 12 px
           wystawaly o cztery. Wypelnienie pionowe zostaje - to ono rozdziela
           wiersze.

           Kafelkow to nie dotyczy: tam komorka jest wierszem karty i ma
           swoje wlasne wypelnienie, ustawione nizej. */
        .table-wrap table:not([data-wide]) :where(th, td) {
            padding-inline: var(--space-2xs);
        }

        /* Kafelki oddziela odstep, a nie wlos, wiec potrzebuja marginesu -
           tabela stoi zwykle w `.card--flush`, czyli w karcie bez
           wypelnienia. */
        .table-wrap:has(table[data-wide]) {
            padding: var(--space-sm);
        }

        /* Naglowek znika z oczu, ale nie z drzewa dostepnosci: podpisy
           kolumn sa jedyna rzecza, ktora wiaze komorke z jej znaczeniem,
           a czytnik ekranu bierze je stad, a nie z `data-label`. */
        table[data-wide] thead {
            position: absolute;
            width: 1px;
            height: 1px;
            overflow: hidden;
            clip-path: inset(50%);
            white-space: nowrap;
        }

        table[data-wide],
        table[data-wide] tbody {
            display: block;
        }

        table[data-wide] tbody tr {
            display: block;
            padding: var(--space-2xs) 0;
            background: var(--surface-raised);
            border: 1px solid var(--border);
            border-radius: var(--radius-md);
        }

        table[data-wide] tbody tr + tr {
            margin-top: var(--space-xs);
        }

        /* Podpis dostaje staly tor, a nie szerokosc wlasnej tresci: wartosci
           maja stac w kafelku w jednej linii, tak samo jak stoja w kolumnie
           tabeli. Gorna granica jest procentowa, bo podpisy sa tlumaczone
           i po niemiecku bywaja dwa razy dluzsze niz po polsku. */
        table[data-wide] tbody :where(td, th) {
            display: grid;
            grid-template-columns: minmax(0, 40%) minmax(0, 1fr);
            gap: var(--space-sm);
            align-items: baseline;
            padding: var(--space-3xs) var(--space-sm);
            border: 0;
            text-align: start;
        }

        table[data-wide] tbody :where(td, th)::before {
            content: attr(data-label);
            color: var(--text-faint);
            font-size: var(--label-size);
            font-weight: var(--weight-medium);
            letter-spacing: var(--label-spacing);
            text-transform: var(--label-transform);
        }

        /* Wartosc zaczyna sie przy lewej krawedzi swojego toru, zamiast
           rozciagac sie na cala jego szerokosc. Widac to po przyciskach:
           komorka jest siatka, a dziecko siatki domyslnie wypelnia swoj tor,
           wiec "Pobierz" na liscie frezarek rozlewalo sie w pas przez pol
           kafelka - podczas gdy w tej samej kolumnie tabeli ma szerokosc
           swojego napisu.

           Zwykly tekst wyglada tak samo jak przedtem: zawija sie na
           szerokosci toru, bo dziecko siatki nie przekracza jej granicy.
           Kolumna akcji ustawia sobie `end` nizej i to ona wygrywa. */
        table[data-wide] tbody :where(td, th) {
            justify-items: start;
        }

        /* Kolumna, ktorej naglowek nic nie mowi, nie zajmuje w kafelku toru
           na podpis. */
        table[data-wide] tbody :where(td, th):not([data-label]) {
            grid-template-columns: minmax(0, 1fr);
        }

        table[data-wide] tbody :where(td, th):not([data-label])::before {
            content: none;
        }

        /* Akcje sa tym, co sie z kafelkiem robi, a nie kolejna jego dana -
           stoja wiec pod kreska, przez cala szerokosc, jak stopka karty. */
        table[data-wide] tbody .col-actions {
            width: auto;
            grid-template-columns: minmax(0, 1fr);
            justify-items: end;
            margin-top: var(--space-2xs);
            padding-top: var(--space-xs);
            border-top: 1px solid var(--hairline);
        }

        table[data-wide] tbody .col-actions::before {
            content: none;
        }

        /* Kolumny szczegolow zgloszenia doklada przycisk "Pokaz szczegoly".
           W kafelku sa zwykla para podpis-wartosc, wiec `table-cell`
           z warstwy wyzej trzeba zdjac. */
        table[data-wide] tbody .details-col.show {
            display: grid;
        }

        /* Pole aliasu wpisuje sie wprost w komorce - w kafelku ma tyle
           miejsca, ile zostalo po podpisie, a nie stale 11rem. */
        .alias-input {
            width: 100%;
        }
    }

    /* ==================================================================
       KAFEL ROZDZIELACZA
       Odsylacz-kafelek na panelu menedzera. Caly jest celem klikniecia,
       a nie sam napis w srodku.
    ================================================================== */
    .tile {
        display: flex;
        align-items: center;
        min-height: 3.5rem;
        padding: var(--space-sm);
        color: var(--text-muted);
        font-size: var(--text-sm);
        text-decoration: none;
        background: var(--surface-raised);
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
        transition: color var(--dur-fast) var(--ease),
                    background var(--dur-fast) var(--ease),
                    border-color var(--dur-fast) var(--ease);
    }

    @media (hover: hover) {
        .tile:hover {
            color: var(--text);
            background: var(--surface-hover);
            border-color: var(--accent-line);
        }
    }

    .section-icon {
        width: 1.25rem;
        height: 1.25rem;
        object-fit: contain;
        opacity: 0.8;
    }

    /* ==================================================================
       PAGINATOR
    ================================================================== */
    .pager {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        justify-content: center;
        gap: var(--space-xs);
    }

    /* ==================================================================
       PUSTY STAN
    ================================================================== */
    .empty-state {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: var(--space-xs);
        min-height: 12rem;
        padding: var(--space-xl);
        color: var(--text-muted);
        text-align: center;
    }

    .empty-state__icon {
        font-size: var(--text-3xl);
        color: var(--text-faint);
        line-height: 1;
    }

    /* ==================================================================
       WYSZUKIWARKA I PASEK FILTROW
    ================================================================== */
    .search {
        display: flex;
        align-items: center;
        gap: var(--space-2xs);
        min-width: 0;
    }

    .search input {
        min-width: 0;
    }

    /* Lupa jest plikiem PNG w pelnej rozdzielczosci - bez rozmiaru wypelnia
       caly przycisk. */
    #search-icon {
        width: 1rem;
        height: 1rem;
        object-fit: contain;
        opacity: 0.7;
    }

    .filter-bar {
        display: flex;
        flex-wrap: wrap;
        align-items: flex-end;
        gap: var(--space-sm);
    }

    /* Pola filtrow maja gorna granice szerokosci: bez niej ostatnie pole,
       ktore zawinelo sie do nastepnego wiersza, rozciagalo sie na cala
       szerokosc paska i wygladalo jak inny rodzaj kontrolki niz sasiednie. */
    .filter-bar .field,
    .filter-bar__field {
        flex: 1 1 10rem;
        min-width: 0;
        max-width: 16rem;
    }

    .filter-bar__actions {
        display: flex;
        gap: var(--space-xs);
        margin-inline-start: auto;
    }

    /* Na waskim ekranie pola zawijaja sie po jednym albo po dwa w wierszu
       i wtedy gorna granica szerokosci zaczyna szkodzic zamiast pomagac:
       pole, ktore zostalo w wierszu samo, konczy sie w polowie paska,
       a reszta wiersza jest pusta. Dolna granica (`flex-basis: 10rem`)
       zostaje - to ona decyduje, ile pol wejdzie w wiersz, wiec na tablecie
       jest ich nadal kilka, a na telefonie jedno.

       Przyciski schodza pod pola przez cala szerokosc paska, ale nie
       rozciagaja sie na nia: para "Filtruj / Wyczysc filtry" ma wygladac
       tak samo, jak wszedzie indziej w aplikacji, a wysokosc celu daje jej
       juz `--control-height` z zapytania o wskaznik. */
    @media (max-width: 48rem) {
        .filter-bar .field,
        .filter-bar__field {
            max-width: none;
        }

        .filter-bar__actions {
            width: 100%;
            margin-inline-start: 0;
        }
    }

    /* ==================================================================
       AWATAR
    ================================================================== */
    .avatar {
        flex: none;
        width: var(--avatar-size, 2rem);
        height: var(--avatar-size, 2rem);
        object-fit: cover;
        border-radius: 50%;
        background: var(--surface-raised);
    }

    .avatar--lg { --avatar-size: 4rem; }

    /* Naglowek profilu: zdjecie i akcje obok niego. Pole wyboru pliku lezy
       na zdjeciu, wiec kontener jest punktem odniesienia dla jego pozycji. */
    .profile-head {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-md);
    }

    .profile-head__photo {
        position: relative;
        flex: none;
    }

    .profile-head__upload {
        position: absolute;
        inset: 0;
        display: grid;
        place-items: center;
        overflow: hidden;
        background: #000000a6;
        border-radius: 50%;
        opacity: 0;
        transition: opacity var(--dur-fast) var(--ease);
    }

    .profile-head__photo:hover .profile-head__upload,
    .profile-head__upload:focus-within {
        opacity: 1;
    }

    .profile-head__upload input[type="file"] {
        width: 100%;
        font-size: 0;
        cursor: pointer;
    }

    /* Zastepnik, gdy uzytkownik nie ma zdjecia - ikona w kole. */
    .avatar-icon {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: var(--avatar-size, 2rem);
        height: var(--avatar-size, 2rem);
        color: var(--text-faint);
        font-size: calc(var(--avatar-size, 2rem) * 0.9);
        line-height: 1;
    }

    /* Awatar i zapasowa ikona podmieniaja sie atrybutem `hidden`: ikona stoi
       schowana, a `onerror` zdjecia chowa zdjecie i odslania ja
       (`components/user_avatar.html`). `[hidden]` z arkusza przegladarki
       przegrywa jednak z kazda regula `display` autora - a `display` maja
       tu obie strony: `img` z zerowania i `.bi` z sekcji ikon. Zaden z tych
       elementow nie dawal sie wiec schowac i w kazdym wierszu listy dzialu
       wsparcia staly dwa awatary naraz: zdjecie i ikona obok niego.

       Regula jest tutaj, a nie globalnie na `[hidden]`, bo globalne
       `!important` odwrocilo by pierwszenstwo tam, gdzie skrypty pokazuja
       element przez `style.display`, zostawiajac `hidden` w znaczniku
       (pasek postepu, podpowiedz wykresu - patrz 06-overlays.css). */
    .avatar[hidden],
    .avatar-icon[hidden] {
        display: none;
    }

    /* ==================================================================
       PASEK POSTEPU
       Czas do nastepnej kalibracji i do serwisu. Kolor bierze sie ze
       stanu, nie z procentu - o progach decyduje widok.
    ================================================================== */
    .progress {
        position: relative;
        height: 0.375rem;
        overflow: hidden;
        background: var(--surface-active);
        border-radius: var(--radius-pill);
    }

    .progress__bar {
        height: 100%;
        background: var(--progress-color, var(--accent));
        border-radius: inherit;
        transition: width var(--dur-med) var(--ease);
    }

    .progress--ok     { --progress-color: var(--ok); }
    .progress--warn   { --progress-color: var(--warn); }
    .progress--danger { --progress-color: var(--danger); }

    /* Licznik do nastepnej kalibracji i do serwisu liczy skrypt i podaje
       wynik przez `--progress` - stad szerokosc, a nie z atrybutu. */
    #timer-bar-fill {
        width: var(--progress, 0%);
    }

    /* ==================================================================
       NAGLOWEK FREZARKI
       Zdjecie modelu obok nazwy i numeru seryjnego. Zdjecie ma stala
       proporcje, wiec model bez pliku nie zmienia wysokosci naglowka.
    ================================================================== */
    .machine-header {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-md);
    }

    /* Zdjecia modeli to wyciete PNG-i z przezroczystym tlem ("bz cienia"),
       wiec kadr zostaje bez wypelnienia - z panelem pod spodem kazda frezarka
       stala na jasniejszym prostokacie zamiast na karcie. Ramka zostaje, bo
       trzyma stale miejsce na zdjecie takze wtedy, gdy modelu nie wgrano. */
    .machine-header__image {
        flex: none;
        width: 7rem;
        aspect-ratio: 4 / 3;
        padding: var(--space-2xs);
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
    }

    .machine-header__image img {
        width: 100%;
        height: 100%;
        object-fit: contain;
    }

    /* Licznik terminu stoi po przeciwnej stronie naglowka niz zdjecie.
       Dolna granica szerokosci jest po to, zeby pasek postepu nie zwezal sie
       do napisu obok - a gorna, zeby nie rozciagal sie przez pol strony. */
    .machine-header__state {
        flex: 1 1 12rem;
        max-width: 22rem;
        margin-inline-start: auto;
    }

    /* Po zawinieciu naglowka licznik nie ma juz stac polowa szerokosci
       przy prawej krawedzi - tak samo, jak akcje w `.page-head`. */
    @media (max-width: 48rem) {
        .machine-header__state {
            width: 100%;
            max-width: none;
            margin-inline-start: 0;
        }
    }

    .machine-header__last {
        color: var(--text-muted);
        font-size: var(--text-sm);
    }

    .machine-header__timer {
        display: flex;
        align-items: baseline;
        gap: var(--space-2xs);
        margin: var(--space-2xs) 0;
        font-size: var(--text-sm);
    }

    /* Rzad pod naglowkiem frezarki na stronie serwisow: wybor rodzaju serwisu
       i przycisk prosby o serwis. Podpis ma nad soba tylko lista, wiec
       domyslne `center` z `.cluster` stawialo przycisk na srodku pary
       podpis-lista - czyli o pol podpisu wyzej niz sama lista. `end` zrownuje
       dolne krawedzie listy i przycisku, a podpis zostaje nad nimi. */
    #select-service-container {
        align-items: end;
    }

    /* ==================================================================
       KAFELEK FREZARKI
       Pasek na stronie glownej. Kafelki dziela szerokosc karty po rowno -
       liczbe torow podaje widok w `--machines-per-row`.
    ================================================================== */
    .machine-strip {
        display: grid;
        /* Ile kafelkow ma stac w rzedzie. Wartosc przychodzi ze stylu w linii
           (`company_machines_list.html`, `MACHINES_PER_PAGE`), a styl w linii
           bije kazda regule arkusza - takze te z zapytania o szerokosc.
           Dlatego czytamy ja raz i dalej idzie juz `--strip-cols`, ktorej
           styl w linii nie ustawia: to ja nadpisuja reguly nizej. Nazwy
           w szablonie nie ruszamy - liczba frezarek na stronie jest
           wielkoscia widoku, a nie wygladem. */
        --strip-cols: var(--machines-per-row, 4);

        /* Szerokosc kafelka: tyle, ile daje podzial karty na `--strip-cols`,
           ale nie mniej niz 13rem. Ponizej tej wartosci para dat "ostatnia /
           nastepny termin" nie miesci sie juz w kafelku obok siebie. */
        --strip-tile: max(13rem, calc((100% - (var(--strip-cols) - 1) * var(--space-sm)) / var(--strip-cols)));

        /* Kafelki, ktore sie nie miesza, schodza do nastepnego wiersza -
           pasek nie przewija sie w bok. To jest ta zmiana, o ktora chodzi:
           przewijanie w poziomie znaczylo, ze czesc frezarek nie istnieje,
           dopoki ktos nie zgadnie, ze trzeba pociagnac w bok. Najgorzej
           wypadal laptop, nie telefon: przy 1180 px karta ma 729 px, a
           cztery kafelki - 868, wiec czwarty stal poza kadrem.

           `auto-fill`, nie `auto-fit`: przy `auto-fit` przegladarka zwija
           tory bez zawartosci, wiec jedna frezarka znaleziona wyszukiwarka
           rozciagalaby sie na cala szerokosc karty. Kafelek ma miec zawsze
           te sama szerokosc, niezaleznie od tego, ile ich stoi.

           Liczba torow sama nie przekroczy `--strip-cols`: gdy podzial karty
           daje wiecej niz 13rem, `--strip-tile` jest rowne dokladnie temu
           podzialowi i miesci sie ich tyle, co dotad - ani jednego wiecej.
           Na szerokim ekranie ta regula nie zmienia wiec niczego. */
        grid-template-columns: repeat(auto-fill, minmax(min(var(--strip-tile), 100%), 1fr));
        gap: var(--space-sm);
    }

    .machine-tile {
        display: flex;
        flex-direction: column;
        gap: var(--space-2xs);
        padding: var(--space-sm);
        color: inherit;
        text-decoration: none;
        background: var(--surface-raised);
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
        transition: border-color var(--dur-fast) var(--ease),
                    background var(--dur-fast) var(--ease);
    }

    /* Podswietlenie pod kursorem. Na dotyku nie ma czego podswietlac,
       a dotkniety kafelek zostawal jasny jeszcze dlugo po tym, jak palec
       zszedl - i wygladal na zaznaczony. */
    @media (hover: hover) {
        .machine-tile:hover {
            background: var(--surface-hover);
            border-color: var(--border-strong);
        }
    }

    /* Bez wypelnienia, tak jak w naglowku frezarki - zdjecie ma przezroczyste
       tlo i ma stac wprost na kafelku. */
    .machine-tile__image {
        aspect-ratio: 16 / 10;
        object-fit: contain;
        border-radius: var(--radius-sm);
    }

    .machine-tile__name {
        color: var(--text);
        font-size: var(--text-sm);
        font-weight: var(--weight-medium);
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .machine-tile__meta {
        color: var(--text-faint);
        font-family: var(--font-mono);
        font-size: var(--text-2xs);
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .machine-tile__alias {
        color: var(--accent-soft);
        font-size: var(--text-2xs);
        cursor: pointer;
    }

    /* Dwie daty obok siebie: ostatnia kalibracja i nastepny termin.
       Staly podzial na pol, zeby drugi wiersz stal w kazdym kafelku na tej
       samej wysokosci mimo roznej dlugosci pierwszego. */
    .machine-tile__dates {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: var(--space-sm);
        margin-top: auto;
        padding-top: var(--space-2xs);
        border-top: 1px solid var(--hairline);
    }

    /* Kreska miedzy datami, a nie sam odstep. Oba podpisy sa dluzsze niz
       kolumna, ktora dostaja (99 px w kafelku szerokim na 235), wiec kazdy
       lamie sie na dwa wiersze - a te cztery wiersze staly wczesniej cztery
       piksele od siebie i czytalo sie je w poprzek: "OSTATNIA NASTEPNY",
       "KALIBRACJA TERMIN". Skrocic podpisow nie mozna, bo sa tlumaczone;
       kreska z odstepem po obu stronach mowi, ktore slowo nalezy do ktorej
       daty, i nie zmienia wysokosci kafelka - siatka dat ma jeden wiersz,
       wiec wiekszy `gap` idzie wylacznie w poziom. */
    .machine-tile__dates > * + * {
        padding-inline-start: var(--space-sm);
        border-inline-start: 1px solid var(--hairline);
    }

    .machine-tile__status {
        display: inline-flex;
        align-items: center;
        gap: var(--space-2xs);
        color: var(--text-muted);
        font-size: var(--text-2xs);
    }

    .machine-image-placeholder {
        opacity: 0.35;
        padding: var(--space-sm);
    }

    /* Pasek: strzalka, kafelki, strzalka. Strzalki nie wchodza w przewijana
       liste - stoja poza nia, wiec nie odjezdzaja razem z kafelkami. */
    .strip {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr) auto;
        align-items: center;
        gap: var(--space-xs);
    }

    /* --- pasek na telefonie ----------------------------------------------
       Kafelek klada sie na bok: zdjecie w waskiej kolumnie z lewej, reszta
       obok. Stojac, kafelek zajmuje cala szerokosc telefonu, wiec zdjecie
       ma w nim 193 px wysokosci - cztery frezarki to prawie tysiac pikseli
       samych zdjec. Polozony mierzy 239 px na frezarke i mowi to samo.

       Strzalki wracaja i to nie jest kosmetyka: pasek niesie jedna strone
       parku (`MACHINES_PER_PAGE`), a na nastepna przenosi wylacznie ten
       przycisk. Schowany na telefonie zamykal caly park na pierwszych
       czterech frezarkach. Ida pod kafelki, w oba dolne rogi - w zasiegu
       kciuka i bez zabierania szerokosci samym kafelkom. */
    @media (max-width: 48rem) {
        .strip {
            grid-template-columns: repeat(2, minmax(0, 1fr));
            gap: var(--space-sm) var(--space-xs);
        }

        .machine-strip {
            grid-column: 1 / -1;
            grid-row: 1;
        }

        .arrow-left-btn  { grid-column: 1; grid-row: 2; justify-self: start; }
        .arrow-right-btn { grid-column: 2; grid-row: 2; justify-self: end; }

        .machine-tile {
            display: grid;
            grid-template-columns: 6rem minmax(0, 1fr);
            column-gap: var(--space-sm);
            align-items: start;
        }

        /* Zdjecie stoi w pierwszej kolumnie, reszta - nazwa, firma, numer,
           daty, stan - idzie kolejno w drugiej. Kolumny sa przypisane
           wprost, a nie zostawione automatowi, bo dzieci kafelka jest raz
           szescioro, a raz osmioro: firme widzi tylko pracownik, a pole
           aliasu tylko klient.

           `span 3`, a nie `1 / -1`: `-1` wskazuje ostatnia linie siatki
           JAWNEJ, a ta nie ma tu ani jednego wiersza z `grid-template-rows`
           - wszystkie sa domyslne. `1 / -1` znaczy wiec "od pierwszej linii
           do pierwszej", czyli jeden wiersz: zdjecie rozpychalo przez to
           wiersz z nazwa do swoich 60 px i pod nazwa zostawala dziura.

           Trzy wiersze sa bezpieczne w obie strony. W kolumnie obok stoja
           zawsze co najmniej cztery rzeczy, wiec zdjecie nie tworzy pustego
           toru; a najkrotsze trzy - nazwa, firma i numer seryjny - skladaja
           sie na 67 px, czyli i tak wiecej niz 60 px zdjecia. */
        .machine-tile > a:first-child {
            grid-column: 1;
            grid-row: 1 / span 3;
            align-self: start;
        }

        .machine-tile > :not(a:first-child) {
            grid-column: 2;
        }

        /* Na kafelku polozonym na bok daty maja do dyspozycji nie 209 px,
           tylko 176 - a wtedy kolumna wychodzi wezsza niz samo slowo
           "KALIBRACJA" i podpis wychodzi poza kreske. Ida wiec jedna pod
           druga, przez cala szerokosc: podpis miesci sie w jednym wierszu,
           a kreska z pionowej robi sie pozioma.

           Nie doklejaja sie tez juz do dolu kafelka: przy ukladzie w poziomie
           `margin-top: auto` zepchnelby je pod zdjecie, ktore jest teraz
           wyzsze od nich. */
        .machine-tile__dates {
            grid-template-columns: minmax(0, 1fr);
            gap: var(--space-2xs);
            margin-top: var(--space-2xs);
        }

        .machine-tile__dates > * + * {
            padding-inline-start: 0;
            padding-block-start: var(--space-2xs);
            border-inline-start: 0;
            border-block-start: 1px solid var(--hairline);
        }
    }

    /* ==================================================================
       WSKAZNIK PRACY
    ================================================================== */
    .spinner {
        width: 1.75rem;
        height: 1.75rem;
        border: 2px solid var(--surface-active);
        border-top-color: var(--accent);
        border-radius: 50%;
        animation: spin 0.8s linear infinite;
    }

    @keyframes spin {
        to { transform: rotate(1turn); }
    }

    /* htmx zapala wskaznik, gdy zapytanie jest w drodze. */
    .htmx-indicator {
        opacity: 0;
        transition: opacity var(--dur-med) var(--ease);
    }

    .htmx-request .htmx-indicator,
    .htmx-request.htmx-indicator {
        opacity: 1;
    }

    /* ==================================================================
       KOMENTARZ I HISTORIA
       Rozmowa i slad zdarzen na zgloszeniu problemu. Oba to lista wpisow
       "kto, kiedy, co" - roznia sie tym, ze komentarz pisze czlowiek,
       a zdarzenie zapisuje aplikacja.
    ================================================================== */
    .comment {
        display: flex;
        flex-direction: column;
        gap: var(--space-2xs);
        padding: var(--space-sm);
        background: var(--surface-sunken);
        border: 1px solid var(--hairline);
        border-radius: var(--radius-md);
    }

    .comment__meta {
        color: var(--text-faint);
        font-size: var(--text-xs);
    }

    .timeline {
        display: flex;
        flex-direction: column;
        gap: var(--space-xs);
        margin: 0;
        padding: 0;
        list-style: none;
    }

    /* Kreska po lewej wiaze wpisy w jeden ciag - bez niej lista zdarzen
       czyta sie jak zbior niepowiazanych zdan. */
    .timeline__item {
        display: flex;
        flex-direction: column;
        gap: var(--space-3xs);
        padding-inline-start: var(--space-sm);
        border-inline-start: 2px solid var(--border);
        font-size: var(--text-sm);
    }

    .timeline__head {
        display: flex;
        flex-wrap: wrap;
        align-items: baseline;
        gap: var(--space-2xs);
        color: var(--text);
    }

    .timeline__note {
        color: var(--text-muted);
        font-size: var(--text-xs);
    }

    /* ==================================================================
       LISTA WARUNKOW (wymagania hasla)
    ================================================================== */
    .rule-list {
        display: flex;
        flex-direction: column;
        gap: var(--space-3xs);
        margin: var(--space-2xs) 0 0;
        padding: 0;
        list-style: none;
        color: var(--text-muted);
        font-size: var(--text-xs);
    }

    /* ==================================================================
       EKRAN POWITALNY
    ================================================================== */
    .welcome {
        display: grid;
        grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);
        align-items: center;
        gap: var(--space-2xl);
        /* Szerokosc wprost - z tego samego powodu, co przy `.page`: `.app-main`
           jest siatka, a marginesy `auto` wylaczaja w niej rozciagniecie
           dziecka na caly tor. Pion zalatwia teraz `.app-main`, wiec zeszla
           stad `min-height: calc(100dvh - 8rem)`, czyli zgadniete 8rem na
           pasek gorny i stopke. */
        width: 100%;
        max-width: var(--content-max);
        margin-inline: auto;
        padding: var(--content-pad);
    }

    .welcome__intro {
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        gap: var(--space-md);
    }

    .welcome__logo {
        width: 5rem;
        height: 5rem;
        object-fit: contain;
    }

    .welcome__title {
        display: flex;
        align-items: baseline;
        gap: var(--space-xs);
        font-size: var(--text-3xl);
        letter-spacing: -0.02em;
    }

    .welcome__art {
        width: 100%;
        object-fit: contain;
    }

    @media (max-width: 60rem) {
        .welcome {
            grid-template-columns: minmax(0, 1fr);
            gap: var(--space-lg);
        }
    }

    /* ==================================================================
       KALENDARZ
       Siatke miesiaca rysuje `calendar.HTMLCalendar` po stronie Pythona
       (`calibration_history/calendar_words_converter.py`), wiec znacznik jest
       z gory dany i nie da sie w nim dolozyc klas: <table class="month">,
       wiersz tytulu (th.month), wiersz skrotow dni (th.mon..th.sun) i komorki
       dni (td.mon..td.sun, td.noday dla pustych). Dlatego jako jedyny
       komponent trzyma sie tych selektorow zamiast wlasnych nazw.

       Warstwa `base` styluje kazda tabele jak tabele danych: lepki naglowek,
       wlos pod kazdym wierszem, podswietlenie calego wiersza pod kursorem.
       Dla kalendarza to nieprawda - tydzien nie jest rekordem, a komorka nie
       jest polem - wiec ta sekcja to zdejmuje i buduje siatke kafelkow. Klasy
       wewnatrz komorek (`cell-content`, `logs-link`, kropki frezarek) doklada
       skrypt po wczytaniu logow.
    ================================================================== */
    #calendar-container {
        display: flex;
        flex-direction: column;
        gap: var(--space-md);
    }

    table.month {
        table-layout: fixed;
        border-collapse: separate;
        border-spacing: var(--space-2xs);
    }

    /* Wyrownanie do srodka i brak obrysu sa wspolne dla obu wierszy
       naglowka - tytulu miesiaca i skrotow dni. */
    table.month th {
        position: static;
        padding: var(--space-2xs);
        color: var(--text-muted);
        font-size: var(--label-size);
        letter-spacing: var(--label-spacing);
        text-transform: var(--label-transform);
        text-align: center;
        white-space: nowrap;
        background: var(--surface-raised);
        border: 0;
        border-radius: var(--radius-sm);
    }

    /* Tytul miesiaca jest pierwszym wierszem tabeli, a nie <caption> -
       tak go generuje `HTMLCalendar`. Ma czytac sie jak naglowek sekcji,
       wiec nie bierze ani tla, ani kapitalikow od reszty naglowka. */
    table.month th.month {
        padding: 0 0 var(--space-2xs);
        color: var(--text);
        font-size: var(--text-lg);
        font-weight: var(--weight-medium);
        letter-spacing: normal;
        text-transform: none;
        background: none;
    }

    /* Dzien to kafelek, nie komorka listy: tresc trzyma sie gornej krawedzi,
       bo skrypt dokleda pod numerem odsylacz do kalibracji i kropki frezarek.
       Wysokosc jest stala, zeby miesiac nie zmienial ksztaltu w zaleznosci od
       tego, ktore dni maja wpisy. */
    table.month td {
        height: 4.5rem;
        padding: var(--space-2xs) var(--space-xs);
        font-size: var(--text-sm);
        font-variant-numeric: tabular-nums;
        text-align: left;
        vertical-align: top;
        background: var(--surface);
        border: 1px solid var(--hairline);
        border-radius: var(--radius-sm);
    }

    /* Dni sasiedniego miesiaca - widac, ze naleza do siatki, ale nie do
       miesiaca, ktory sie oglada. */
    table.month td.noday {
        background: var(--surface-sunken);
        border-color: transparent;
    }

    /* Podswietla sie kafelek pod kursorem, a nie caly tydzien. */
    table.month tbody tr:hover {
        background: none;
    }

    @media (hover: hover) {
        table.month td:not(.noday):hover {
            background: var(--surface-hover);
        }
    }

    /* --- kalendarz na telefonie -----------------------------------------
       Miesiac ma sens tylko w calosci: rzecza, po ktora sie tu wchodzi, jest
       rozklad kalibracji w czasie, a nie pojedynczy dzien. Przewijany w bok
       kalendarz tego nie pokazuje, wiec siatka ma sie zmiescic - a zmiesci
       sie, bo `table-layout: fixed` dzieli szerokosc po rowno, a skrocone
       nazwy dni doklada `changeWeekDayName.js` juz ponizej 1024 px.

       Dolna granica jest po to, zeby przy 320 px komorka nie zeszla ponizej
       numeru dnia ze strzalka obok; ponizej tej szerokosci siatka wraca do
       przewijania.

       Napis "kalibracje" w komorce schodzi: przy siedmiu kolumnach na
       telefonie nie ma dla niego miejsca, a prowadzi tam, gdzie i tak
       prowadzi przycisk "wszystkie kalibracje" z rozwijanego menu dnia -
       `mobileUseHandlers.js` doklada go do kazdej komorki ponizej 1024 px
       wlasnie dlatego. */
    @media (max-width: 48rem) {
        /* `min(20rem, 100%)`, nie stale 20rem: przy 320 px CSS z wypelnieniem
           strony 20rem wystaje poza pojemnik i ciagnie poziomy pasek
           przewijania calego dokumentu. */
        .table-wrap table.month {
            min-width: min(20rem, 100%);
        }

        table.month {
            border-spacing: var(--space-3xs);
        }

        table.month th {
            padding: var(--space-3xs);
        }

        table.month td {
            height: 3.25rem;
            padding: var(--space-3xs);
            font-size: var(--text-xs);
        }

        .logs-link {
            display: none;
        }

        /* Strzalka rozwijajaca liste kalibracji dnia jest na telefonie
           jedynym celem w komorce, wiec dostaje wiecej niz swoje 16 px:
           obrys zostaje ten sam, rosnie sam obszar dotkniecia. */
        .dropdown-btn {
            padding: var(--space-2xs);
            box-sizing: content-box;
        }
    }

    /* Wiersz w komorce: numer dnia po lewej, odsylacz i menu logow po
       prawej. */
    .cell-content {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: var(--space-2xs);
    }

    .logs-link {
        color: var(--accent-soft);
        font-size: var(--text-2xs);
        white-space: nowrap;
    }

    .dropdown-btn,
    .empty-dropdown-btn {
        width: 1rem;
        height: 1rem;
        object-fit: contain;
        opacity: 0.7;
        cursor: pointer;
    }

    /* --- znacznik dnia --------------------------------------------------
       Ptaszek na dniu, w ktorym byla kalibracja albo serwis. Skrypt wstawia
       go do KAZDEJ komorki z numerem dnia - tej bez wpisu z klasa
       `hidden-icon` - zeby wszystkie dni mialy te sama wysokosc niezaleznie
       od tego, czy cos sie tego dnia wydarzylo.

       Regula na `hidden-icon` jest wiec obowiazkowa, nie kosmetyczna: bez
       niej ptaszek stoi na wszystkich dniach miesiaca i dzien z kalibracja
       niczym nie rozni sie od pustego.

       `visibility`, a nie `color: transparent`: chodzi o zniknieciu calej
       ikony przy zachowaniu miejsca po niej. Skrypt strony serwisow zdejmuje
       te klase przy najechaniu i pokazuje ptaszek przygaszony - jako
       zapowiedz "kliknij, zeby zapisac tu serwis". */
    .cell-content .bi-check2 {
        color: var(--ok);
        font-size: var(--text-lg);
        line-height: 1;
    }

    .cell-content .bi-exclamation-circle {
        font-size: var(--text-lg);
        line-height: 1;
    }

    .hidden-icon {
        visibility: hidden;
    }

    /* Dzien bez serwisu jest na stronie serwisow celem klikniecia - zapisuje
       serwis pod ta data. Obrys mowi, ze to nie jest zwykla komorka.

       Tak samo dzien od dzisiaj wzwyz w oknie zgloszenia serwisu: skrypt
       nadaje mu `select-event-date`, a wybranemu - `selected-event-date`. */
    table.month td.no-service-day,
    table.month td.select-event-date {
        cursor: pointer;
    }

    table.month td.no-service-day:hover,
    table.month td.select-event-date:hover {
        border-color: var(--border-strong);
    }

    /* Zaznaczenie wybranego dnia idzie obwodka wewnetrzna, a nie tlem: dni
       z zajetymi godzinami skrypt maluje tlem w stylu liniowym, a to bije
       kazda regule stad. Obwodka widac na obu rodzajach dni i nie trzeba do
       niej `!important`. */
    table.month td.selected-event-date {
        box-shadow: inset 0 0 0 2px var(--accent);
    }

    /* Ikona zajetych godzin stoi na srodku komorki, nie przy krawedzi. */
    .events-cell-content .dropdown {
        text-align: center;
    }

    /* Dopisek "- biezacy" przy nazwie miesiaca. Kolor nadawal skrypt stylem
       liniowym - jedyny zahardkodowany kolor, jaki zostal w skryptach
       kalendarza. */
    .current-month-label {
        display: inline;
        color: var(--accent-soft);
    }

    /* --- pasek frezarek nad kalendarzem ---------------------------------
       Legenda: kazda frezarka ma swoj kolor, ten sam co kropki w komorkach
       dni. Najechanie na kropke rozswietla odpowiadajaca jej pigulke. */
    #milling-machines-msg,
    #machines-states-data {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        justify-content: center;
        gap: var(--space-xs);
        color: var(--text-muted);
        font-size: var(--text-sm);
    }

    #machines-container,
    .machines-container {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        justify-content: center;
        gap: var(--space-xs);
        min-width: 0;
    }

    .machine-container {
        display: inline-flex;
        align-items: center;
        gap: var(--space-2xs);
        padding: var(--space-3xs) var(--space-xs);
        color: var(--text-muted);
        font-size: var(--text-xs);
        white-space: nowrap;
        background: var(--surface);
        border: 1px solid var(--border);
        border-radius: var(--radius-pill);
    }

    #no-calibrated-machines {
        color: var(--text-muted);
        font-size: var(--text-sm);
        text-align: center;
    }

    /* Kolor kropki nadaje skrypt stylem liniowym - jeden na frezarke. */
    .machine-dot,
    .machine-dot-link {
        flex: none;
        width: 0.625rem;
        height: 0.625rem;
        border-radius: 50%;
    }

    .machine-dot-link {
        cursor: pointer;
    }

    .machines-dots-container {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-3xs);
    }

    /* Puls przy najechaniu - laczy kropke w komorce dnia z pigulka
       frezarki w legendzie. `--dot-color` ustawia skrypt. */
    .machine-dot-clicked {
        animation: dot-glow 1.4s infinite ease-in-out;
    }

    @keyframes dot-glow {
        0%, 100% { box-shadow: 0 0 2px 0 var(--dot-color, var(--accent)); }
        50%      { box-shadow: 0 0 16px 6px var(--dot-color, var(--accent)); }
    }

    /* Frezarka bez ani jednej kalibracji - obrys mruga, bo to stan do
       zalatwienia, a nie zwykla pozycja listy. */
    .no-logs-machine {
        animation: no-logs-blink 1.6s infinite;
    }

    @keyframes no-logs-blink {
        0%, 100% { border-color: var(--border-strong); }
        50%      { border-color: transparent; }
    }

    /* Wykrzyknik przy frezarce po terminie kalibracji. Rysowany z trzech
       elementow, bo skrypt tak go sklada - kolo, kreska, kropka. */
    .alert-icon {
        position: relative;
        display: inline-block;
        flex: none;
        width: 1.125rem;
        height: 1.125rem;
        background: var(--warn);
        border-radius: 50%;
    }

    .alert-line,
    .alert-dot {
        position: absolute;
        left: 50%;
        width: 0.1875rem;
        background: var(--canvas);
        transform: translateX(-50%);
    }

    .alert-line {
        top: 0.1875rem;
        height: 0.5625rem;
        border-radius: var(--radius-pill);
    }

    .alert-dot {
        bottom: 0.1875rem;
        height: 0.1875rem;
        border-radius: 50%;
    }

    /* ==================================================================
       IKONY
       Bootstrap Icons wchodzi z CDN. Tutaj tylko wyrownanie: ikona ma stac
       w linii z tekstem obok, a nie na linii bazowej pisma.
    ================================================================== */
    .bi {
        display: inline-block;
        vertical-align: -0.125em;
        line-height: 1;
    }
}
