/*
 * Warstwa wizualna przeniesiona z projektu: design/panel-b2b.html
 * Tokeny są skopiowane 1:1 - zmieniaj wartości tutaj, nie w szablonach.
 */

:root {
    --c-bg: #fbfbf9;
    --c-surface: #ffffff;
    --c-surface-sunken: #f4f3f0;
    --c-ink-900: #1b1b19;
    --c-ink-700: #4a4a45;
    --c-ink-500: #6c6c64;
    --c-line: #e6e5e1;
    --c-line-strong: #d2d0ca;
    --c-accent: oklch(0.52 0.09 250);
    --c-accent-weak: oklch(0.955 0.018 250);
    --c-ok: oklch(0.52 0.09 150);
    /* Ten sam sygnał, ale NA CIEMNYM. `--c-ok` jest liczone pod jasne tło
       panelu i na ekranie potwierdzenia (czarne tło, makieta tury 5) czyta się
       jak plama, nie jak słowo. */
    --c-ok-bright: oklch(0.88 0.15 155);
    --c-warn: oklch(0.55 0.1 65);
    --c-danger: oklch(0.52 0.14 25);
    --c-danger-weak: oklch(0.965 0.015 25);

    /* Scena produktowa: pełnoekranowe ciemne takty landingu. Panel ich nie używa. */
    --c-stage: #0d0d0c;
    --c-stage-raised: #171716;
    --c-stage-ink: #f5f4f0;
    --c-stage-ink-dim: #8a8a80;
    --c-stage-line: #2a2a27;
    --c-signal: oklch(0.66 0.19 38);
    /* Do pisma na jasnym tle - jaśniejszy sygnał nie przechodzi progu kontrastu. */
    --c-signal-ink: oklch(0.52 0.17 38);
    /* Pismo NA sygnale i jego stan najechania. */
    --c-signal-on: #17110c;
    --c-signal-hover: oklch(0.72 0.19 38);
    --c-warn-weak: oklch(0.96 0.04 65);

    --font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
    --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

    --fs-12: 12px;
    --fs-14: 14px;
    --fs-16: 16px;
    --fs-18: 18px;
    --fs-22: 22px;
    --fs-28: 28px;
    --fs-34: 34px;
    /* Rozmiary wyświetlaczowe - tylko landing. Panel kończy się na --fs-34. */
    --fs-44: clamp(34px, 5vw, 44px);
    --fs-60: clamp(38px, 7.5vw, 60px);
    --fs-84: clamp(44px, 10vw, 84px);

    /*
     * Skala EKRANU PRZY LADZIE (makieta „Karta lojalnościowa", tura 5).
     *
     * Osobna od skali wyświetlaczowej landingu, bo odpowiada na inne pytanie.
     * Tamta rośnie z szerokością OKNA, żeby nagłówek na monitorze nie wyglądał
     * jak na telefonie; ta ma być duża **właśnie na telefonie**, bo tam jest
     * czytana - obsługa patrzy na cudzy telefon z półtora metra i pod kątem.
     * Stąd większy współczynnik `vw`: przy 390 px `--fs-84` daje 44 px, czyli
     * wielkość zwykłego nagłówka, a nie napisu widocznego z drugiej strony lady.
     */
    --fs-counter-title: clamp(34px, 13vw, 60px);
    --fs-counter-prize: clamp(30px, 11.5vw, 52px);
    --fs-counter-sum: clamp(44px, 19vw, 88px);
    --fs-counter-word: clamp(52px, 22vw, 96px);

    --lh-tight: 1.2;
    --lh-snug: 1.35;
    --lh-normal: 1.55;

    --fw-regular: 400;
    --fw-medium: 500;
    --fw-semibold: 600;

    --tracking-tight: -0.01em;
    --tracking-wide: 0.06em;

    --s-1: 4px;
    --s-2: 8px;
    --s-3: 12px;
    --s-4: 16px;
    --s-5: 24px;
    --s-6: 32px;
    --s-7: 48px;
    --s-8: 64px;

    --r-0: 2px;
    --r-1: 4px;
    --r-2: 8px;
    --r-3: 12px;
    --r-4: 20px;
    --r-full: 999px;

    --sh-1: 0 1px 2px rgba(27, 27, 25, 0.06);
    --sh-2: 0 2px 8px rgba(27, 27, 25, 0.07);
    --sh-3: 0 8px 28px rgba(27, 27, 25, 0.1);

    --focus-ring: 2px solid var(--c-accent);
    --focus-offset: 2px;
    --tap-min: 44px;
    --content-max: 520px;
    --landing-max: 1120px;
    --panel-max: 1024px;
    /* Powłoka panelu: kolumna treści (--panel-max) plus kolumna nawigacji.
       Osobny token, bo `--panel-max` opisuje szerokość czytanej treści i nie
       wolno go rozciągnąć tylko dlatego, że z boku doszła nawigacja. */
    --panel-nav-width: 224px;
    --panel-shell-max: calc(var(--panel-max) + var(--panel-nav-width));

    /* Ruch. Wbudowane krzywe CSS są zbyt miękkie - te mają wyraźny start,
       bo w interfejsie liczy się pierwsza chwila po dotknięciu. */
    --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
    --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
    --dur-tap: 120ms;
    --dur-state: 180ms;
    --dur-enter: 220ms;
    /* Przejazd między krokami zamówienia. Dłuższy od pozostałych, bo pokonuje
       całą szerokość kadru - przy 220 ms czyta się jak przeskok, nie ruch. */
    --dur-slide: 340ms;

    /* Życie banera powiadomienia: wjazd, postój, zjazd. W tokenach, bo wjazd
       czyta też powiadomienie o rozpoznawaniu - to jedyne, które zostaje na
       ekranie, więc ma przynajmniej wjeżdżać identycznie jak reszta. */
    --notification-in: 340ms;
    --notification-life: 6s;
    --notification-out: 260ms;
}

* {
    box-sizing: border-box;
}

body {
    margin: 0;
    overflow-x: clip;
    background: var(--c-bg);
    color: var(--c-ink-900);
    font-family: var(--font-sans);
    font-size: var(--fs-16);
    line-height: var(--lh-normal);

    /*
     * Strona ma zawsze co najmniej wysokość okna, żeby stopka miała dokąd
     * zjechać przy krótkiej treści (spycha ją `position: sticky` na `.footer`).
     *
     * `dvh` zamiast `vh`, bo na telefonie pasek adresu chowa się przy
     * przewijaniu: przy `vh` strona byłaby wtedy wyższa od ekranu i pojawiałby
     * się pasek przewijania na pustce. Wiersz z `vh` stoi wyżej jako zapas dla
     * przeglądarek bez `dvh`.
     *
     * **`body` nie jest kolumną elastyczną i nie ma nią zostać.** Próbowane
     * 7 sierpnia 2026 i cofnięte po zrzucie z przeglądarki: `.panel` i sekcje
     * landingu mają `margin-inline: auto`, a auto-margines w kolumnie
     * elastycznej **wyłącza rozciąganie** - kolumna treści panelu skurczyła się
     * do szerokości najdłuższego wyrazu. DESIGN.md ostrzega przed tym przy
     * `.landing`; ta sama pułapka czeka piętro wyżej.
     */
    min-height: 100vh;
    min-height: 100dvh;
}

a {
    color: var(--c-ink-900);
}

/* Obwódka skupienia nie animuje się: użytkownik klawiatury ma wiedzieć
   natychmiast, gdzie jest, a nie oglądać jak obwódka dojeżdża. */
:focus-visible {
    outline: var(--focus-ring);
    outline-offset: var(--focus-offset);
}

/* Zdanie dla czytnika ekranu, niewidoczne na ekranie. Nie `display: none`
   ani `visibility: hidden` - jedno i drugie usuwa je także z odczytu. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* Zabija ~300 ms opóźnienia dotknięcia i szary prostokąt iOS - mamy własną
   reakcję na wciśnięcie i nie chcemy, żeby system dokładał swoją. */
button,
label,
a,
input[type="checkbox"],
input[type="radio"] {
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
}

/* ---------- Nagłówek ---------- */

.header {
    background: var(--c-surface);
    border-bottom: 1px solid var(--c-line);
}

.header__bar {
    height: 56px;
    padding: 0 var(--s-4);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
}

.header__brand {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    font-size: var(--fs-16);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-tight);
    text-decoration: none;
    color: var(--c-ink-900);
}

.header__toggle {
    width: var(--tap-min);
    height: var(--tap-min);
    margin-right: -10px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--c-line);
    border-radius: var(--r-2);
    background: var(--c-surface);
    cursor: pointer;
    list-style: none;
}

/* Stan menu trzyma ukryty checkbox: działa bez JS i bez zależności od
   sposobu, w jaki przeglądarka zwija <details>. */
.header__state {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

.header__state:checked ~ .header__bar .header__toggle {
    border-color: var(--c-line-strong);
    background: var(--c-surface-sunken);
}

.header__state:focus-visible ~ .header__bar .header__toggle {
    outline: var(--focus-ring);
    outline-offset: var(--focus-offset);
}

/* Rozwijanie przez grid-template-rows 0fr→1fr: da się animować, w przeciwieństwie
   do display, i nie wymaga znajomości wysokości zawartości. */
.header__dropdown {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows var(--dur-state) var(--ease-out);
}

.header__state:checked ~ .header__dropdown {
    grid-template-rows: 1fr;
}

.header__dropdown-content {
    overflow: hidden;
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    /* Menu wychodzi z paska nad sobą, więc startuje lekko podniesione -
       ruch wskazuje kierunek, z którego przychodzi. */
    transform: translateY(-6px);
    opacity: 0;
    transition:
        transform var(--dur-state) var(--ease-out),
        opacity var(--dur-state) var(--ease-out),
        padding var(--dur-state) var(--ease-out);
    padding: 0 var(--s-4);
}

.header__state:checked ~ .header__dropdown .header__dropdown-content {
    transform: translateY(0);
    opacity: 1;
    padding: var(--s-4);
    border-top: 1px solid var(--c-line);
}

.header__company-label {
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

.header__company-name {
    font-size: var(--fs-16);
    font-weight: var(--fw-medium);
}

/* Od 720 px firma i wylogowanie są zawsze widoczne, przycisk menu znika. */
@media (min-width: 720px) {
    .header__bar {
        height: 64px;
        padding: 0 var(--s-6);
    }

    .header__brand {
        font-size: var(--fs-18);
    }

    .header__toggle {
        display: none;
    }

    /* Od 720 px blok firmy wraca do paska i jest zawsze widoczny -
       niezależnie od stanu checkboksa, więc bez animacji rozwijania. */
    .header {
        position: relative;
    }

    .header__dropdown,
    .header__state:checked ~ .header__dropdown {
        position: absolute;
        top: 0;
        right: var(--s-6);
        height: 64px;
        display: flex;
        grid-template-rows: none;
        transition: none;
    }

    .header__dropdown-content,
    .header__state:checked ~ .header__dropdown .header__dropdown-content {
        flex-direction: row;
        align-items: center;
        gap: var(--s-4);
        height: 100%;
        padding: 0;
        border: 0;
        transform: none;
        opacity: 1;
        transition: none;
    }

    .header__company {
        align-items: flex-end;
        gap: 1px;
    }
}

.header__company {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

/* ---------- Układ treści ---------- */

.content {
    max-width: var(--panel-max);
    margin: 0 auto;
    padding: var(--s-5) var(--s-4) var(--s-8);
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
}

.content--narrow {
    max-width: var(--content-max);
    padding-top: var(--s-6);
    gap: var(--s-5);
}

.container {
    gap: var(--s-5);
}

.d-grid {
    display: grid;
}

.gr-2 {
    grid-template-columns: 1fr;
}

@media (min-width: 720px) {
    .content {
        padding: var(--s-6) var(--s-6) var(--s-8);
    }

    .gr-2 {
        grid-template-columns: 1fr 1fr;
    }
}

.heading {
    margin: 0;
    font-size: var(--fs-28);
    line-height: var(--lh-tight);
    letter-spacing: var(--tracking-tight);
    font-weight: var(--fw-semibold);
}

.heading--section {
    font-size: var(--fs-22);
    line-height: var(--lh-snug);
}

.list-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--s-3);
    flex-wrap: wrap;
}

/* Liczba sztuk i akcja trzymają się razem po prawej stronie nagłówka. Bez tego
   opakowania `space-between` rozstawiłoby trzy elementy po całej szerokości
   i licznik wylądowałby samotnie na środku. */
.list-header__actions {
    display: flex;
    align-items: center;
    gap: var(--s-3);
}

.list-header__count {
    font-size: var(--fs-14);
    color: var(--c-ink-500);
}

.back-link {
    display: inline-flex;
    /* Klikalny ma być napis, nie cała szerokość kolumny. `inline-flex` sam tego
       nie załatwia: w kolumnie elastycznej element rozciąga się w poprzek,
       więc „← Nośniki" łapało kliknięcia na pustym miejscu obok siebie. */
    align-self: flex-start;
    align-items: center;
    gap: var(--s-2);
    font-size: var(--fs-14);
    color: var(--c-ink-700);
    text-decoration: none;
}

/* ---------- Formularze ---------- */

.fields {
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
}

.field {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
}

.field label,
.field__label {
    font-size: var(--fs-14);
    font-weight: var(--fw-medium);
    color: var(--c-ink-700);
}

/* Etykieta pola jest podpisem, nie celem kliknięcia - decyzja właściciela
   z 6 sierpnia 2026. Domyślnie kliknięcie w napis przenosi kursor do pola
   i zaznacza tekst przy przeciągnięciu; przy formularzu wypełnianym kciukiem
   za barem częściej się w to trafia przez przypadek, niż celowo.

   Powiązanie `for` zostaje nietknięte, więc czytnik ekranu nadal czyta pole
   po nazwie. Traci się powiększony cel dotykowy - pole ma własne 44 px
   (`--tap-min`), więc nie schodzimy poniżej progu. Kafelków wyboru
   (`.choice`) i przełączników (`.toggle`) to NIE dotyczy: tam etykieta jest
   całym elementem sterującym. */
.field > label {
    pointer-events: none;
    width: fit-content;
}

/* Lista typów, nie `input` bez zawężenia: pola radio i wyboru mają własny
   wygląd i ta reguła by je zniszczyła. `number` i `tel` dopisane 6 sierpnia
   2026 - „Liczba nośników" była od początku nieostylowanym polem systemowym
   w środku ostylowanego formularza. */
.field input[type="text"],
.field input[type="email"],
.field input[type="number"],
.field input[type="password"],
.field input[type="tel"],
.field input[type="url"],
.field textarea {
    min-height: var(--tap-min);
    padding: 0 var(--s-3);
    font: inherit;
    font-size: var(--fs-16);
    color: var(--c-ink-900);
    background: var(--c-surface);
    border: 1px solid var(--c-line-strong);
    border-radius: var(--r-2);
    text-overflow: ellipsis;
}

/* Pole wielowierszowe ma treść od pierwszej linii, nie wyśrodkowaną w wysokości
   pola, więc potrzebuje odstępu w pionie i własnej interlinii. Zmiana rozmiaru
   tylko w pionie: uchwyt ciągnięty w bok rozjeżdżał kolumnę formularza. */
.field textarea {
    padding: var(--s-3);
    line-height: var(--lh-normal);
    resize: vertical;
}

.field__help {
    margin: 0;
    font-size: var(--fs-12);
    line-height: var(--lh-normal);
    color: var(--c-ink-500);
}

/*
 * Błędy renderowane POZA polem: drabinka progów rabatu i błędy całego
 * formularza. `.field ul` ich nie łapie, bo nie stoją w żadnym `.field`, więc
 * bez tej reguły komunikat wychodzi czarną listą z punktorem - wygląda jak
 * treść strony, a nie jak odmowa zapisu. Złapane okiem w przeglądarce
 * 1 września 2026 przy progach rabatu.
 *
 * Pusty kontener nie zajmuje miejsca: `:empty` na `<div>` działa, bo Twig nie
 * wstawia w środku ani jednego znaku, gdy błędów nie ma.
 */
.form-problems:empty {
    display: none;
}

.form-problems ul,
.field ul,
.field__error {
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: var(--fs-12);
    line-height: var(--lh-normal);
    color: var(--c-danger);
}

/* Dwie klasy, nie jedna, i to nie jest ozdobnik: reguła wyżej wybiera pola po
   typie (`.field input[type="text"]`), więc ma wyższą wagę niż `.field--invalid
   input`. Pole z błędem miało przez to zwykłą ramkę - czerwony był wyłącznie
   komunikat pod spodem. Zmierzone w przeglądarce 6 sierpnia 2026 przy M5.7,
   błąd jest starszy. */
.field.field--invalid input,
.field.field--invalid textarea {
    border-color: var(--c-danger);
    background: var(--c-danger-weak);
}

/* ---------- Przyciski ---------- */

.button {
    min-height: var(--tap-min);
    padding: 0 var(--s-4);
    display: flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: var(--r-2);
    background: var(--c-ink-900);
    color: var(--c-surface);
    font: inherit;
    font-size: var(--fs-16);
    font-weight: var(--fw-medium);
    text-decoration: none;
    cursor: pointer;
    transition:
        transform var(--dur-tap) var(--ease-out),
        background-color var(--dur-state) ease;
}

/* Reakcja na samo wciśnięcie, nie na puszczenie - interfejs ma potwierdzić,
   że usłyszał, zanim serwer cokolwiek odpowie. */
.button:active {
    transform: scale(0.97);
}

/* Hover tylko dla prawdziwego wskaźnika: na dotyku wywołuje się przy tapnięciu
   i zostaje po zdjęciu palca. */
@media (hover: hover) and (pointer: fine) {
    .button:hover {
        background: var(--c-ink-700);
    }
}

.button--outline {
    background: var(--c-surface);
    color: var(--c-ink-900);
    border: 1px solid var(--c-line-strong);
}

/*
 * Wariant nieodwracalny. Jedyne miejsce w panelu, gdzie kolor niesie ostrzeżenie,
 * a nie dekorację - i celowo nie jest wypełniony na czerwono: pełna plama
 * przyciągałaby kciuk do przycisku, który unieruchamia naklejki u klienta.
 */
.button--danger {
    background: var(--c-surface);
    color: var(--c-danger);
    border: 1px solid var(--c-danger);
}

@media (hover: hover) and (pointer: fine) {
    .button--danger:hover {
        background: var(--c-danger-weak);
    }
}
.button--signal {
    background: var(--c-signal);
    color: var(--c-signal-on);
    font-weight: var(--fw-semibold);
    padding: 0 var(--s-6);
    border-radius: var(--r-full);
}

.r-2 {
    border-radius: var(--r-2);
}

@media (hover: hover) and (pointer: fine) {
    .button--signal:hover {
        background: var(--c-signal-hover);
    }
}

@media (hover: hover) and (pointer: fine) {
    .button--outline:hover {
        background: var(--c-surface-sunken);
    }
}

/* ---------- Komunikaty ---------- */

.alert {
    margin: 0;
    padding: var(--s-3) var(--s-4);
    border: 1px solid var(--c-line-strong);
    border-radius: var(--r-2);
    background: var(--c-surface);
    font-size: var(--fs-14);
    line-height: var(--lh-normal);
    color: var(--c-ink-900);
    display: flex;
    gap: var(--s-3);
    align-items: flex-start;
    animation: slide-in-top var(--dur-enter) var(--ease-out) both;
}

@keyframes slide-in-top {
    from {
        opacity: 0;
        transform: translateY(-6px);
    }
}

.alert--error {
    border-color: var(--c-danger);
    background: var(--c-danger-weak);
}

.alert--ok {
    border-color: var(--c-ok);
    background: color-mix(in oklch, var(--c-ok) 8%, white);
}

/* Ostrzeżenie, nie błąd - ten sam rejestr co metka „zamilkł": coś wymaga
   uwagi w świecie fizycznym, ale w panelu nic się nie zepsuło. */
.alert--warn {
    border-color: var(--c-warn);
    background: var(--c-warn-weak);
}

/* ===========================================================================
   Ekran nośnika
   ===========================================================================

   Przebudowany 12 sierpnia 2026 z makiety „Panel Nosnika" (wariant 1b).
   Blok `.sticker-code` - wielki monospace z kodem kości i akapit o jej
   niezmienności - został stąd skasowany razem z tą przebudową: dla właściciela
   lokalu był ścianą technicznego tekstu na ekranie, po który przychodzi
   z pytaniem „dokąd ma prowadzić naklejka". Sam adres z kości został, jedną
   linijką w nagłówku (`.tag-head__url`).

   Akcent zostaje panelowy (`--c-accent`, niebieski). Makieta rysowana była
   na sygnale (pomarańcz landingu) i to jest jedyne miejsce, w którym świadomie
   od niej odchodzę: `--c-signal` należy do sceny produktowej, a panel go nie
   używa - patrz komentarz przy tokenach. */

/* Nagłówek, treść i pasek akcji są rodzeństwem w siatce, a nie zagnieżdżeniem.
   Dzięki temu ten sam znacznik daje na szerokim ekranie „Zapisz" w prawym
   górnym rogu, a na wąskim pasek przyklejony do dołu okna - bez drugiego
   przycisku w dokumencie i bez powtórzonego nagłówka. */
.tag-form {
    display: grid;
    grid-template-areas:
        'head'
        'body'
        'actions';
    gap: var(--s-4);
}

.tag-head {
    grid-area: head;
    display: flex;
    align-items: center;
    gap: var(--s-3);
    min-width: 0;
}

/* Znak nośnika bez kafla pod spodem. Kafel z ciemnym tłem miał sens przy
   ogólnej fali NFC - był wtedy jedyną ozdobą nagłówka. Odkąd stoi w nim rysunek
   konkretnej sztuki, ten sam co w wierszu listy, ma wyglądać tak samo w obu
   miejscach: właściciel klika w naklejkę na liście i widzi tę samą naklejkę
   na górze ekranu, a nie ten sam przedmiot w dwóch ubraniach.

   Grubość kreski podnoszona tu z tego samego powodu co przy `.tag__mark`:
   znak rysowany na 120 jednostkach ma ją dobraną do kafla w katalogu, a w 34 px
   kreska 3 schodzi poniżej piksela i znak blaknie. */
.tag-head__mark {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
}

.tag-head__mark svg {
    display: block;
    width: 34px;
    height: auto;
    stroke-width: 4;
}

/* `flex: 1`, żeby pole nazwy miało się gdzie rozłożyć - bez tego kolumna tytułu
   ma szerokość samego napisu i input otwiera się na 190 px. */
.tag-head__title {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.tag-head__name {
    font-size: var(--fs-22);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-tight);
    line-height: var(--lh-tight);
    overflow-wrap: anywhere;
}

.tag-head__meta {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--s-2);
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

/* Kod z kości. `user-select: all` jest tu funkcją, nie ozdobnikiem: pierwszą
   czynnością po znalezieniu nośnika bywa wklejenie kodu do zgłoszenia albo do
   wyszukiwarki na liście, a zaznaczanie ośmiu znaków po jednym jest wtedy
   dokładnie tym tarciem, którego ekran ma nie mieć.

   Pełny adres (`nfctg.pl/r/…`) stał tu jeden dzień i wyleciał 13 sierpnia 2026:
   napis do wypalenia w kości bierze się z `app:orders:codes`, a nie z panelu. */
.tag-head__code {
    font-family: var(--font-mono);
    user-select: all;
}

.tag-form__body {
    grid-area: body;
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--s-5);
    align-items: start;
}

.tag-form__fields {
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
    min-width: 0;
}

/* Pasek akcji niesie od 13 sierpnia 2026 dwie rzeczy: przełącznik aktywności
   i „Zapisz". Przełącznik siedział wcześniej w zwiniętej sekcji „Dodatkowo",
   czyli trzy kliknięcia od miejsca, w którym się o nim myśli - a wyłączony
   nośnik to martwa naklejka na drzwiach klienta. */
.tag-form__actions {
    grid-area: actions;
    display: flex;
    align-items: center;
    gap: var(--s-3);
}

/* ---------- Nazwa nośnika ---------- */

.tag-name {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

/* Oba stany mają tę samą wysokość, bo wchodzą w to samo miejsce nagłówka.
   Bez tego kliknięcie ołówka spychało w dół całą stronę razem z „Zapisz". */
.tag-name__view,
.tag-name__row {
    min-height: var(--tap-min);
}

.tag-name__view {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    min-width: 0;
}

.tag-name__pencil {
    position: relative;
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    color: var(--c-ink-500);
    background: none;
    border: 0;
    border-radius: var(--r-full);
    cursor: pointer;
    transition:
        color var(--dur-state) ease,
        background-color var(--dur-state) ease;
}

/* Ołówek ma 28 px, żeby nie konkurował z nazwą; cel dotykowy dobija do 44 px
   przezroczystą wyściółką, jak kropki dymka. */
.tag-name__pencil::after {
    content: '';
    position: absolute;
    inset: -8px;
}

.tag-name__pencil:hover {
    color: var(--c-ink-900);
    background: var(--c-surface-sunken);
}

/* Nazwa ma 60 znaków, więc pole nie ma po co rozciągać się na całą szerokość. */
.tag-name__field {
    max-width: 420px;
}

.tag-name__row {
    display: flex;
    align-items: center;
    gap: var(--s-2);
}

.tag-name__row input {
    flex: 1;
    min-width: 0;
}

.tag-name__action {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--tap-min);
    height: var(--tap-min);
    padding: 0;
    color: var(--c-ink-700);
    background: var(--c-surface);
    border: 1px solid var(--c-line-strong);
    border-radius: var(--r-2);
    cursor: pointer;
}

.tag-name__action:hover {
    background: var(--c-surface-sunken);
}

.tag-name__action--ok {
    color: var(--c-surface);
    background: var(--c-accent);
    border-color: var(--c-accent);
}

.tag-name__action--ok:hover {
    background: var(--c-ink-900);
    border-color: var(--c-ink-900);
}

/* ---------- Typ przyłożenia ---------- */

.purpose {
    margin: 0;
    padding: 0;
    border: 0;
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
}

/* Ta sama pułapka co przy `.type-picker`: legenda nie jest elementem układu
   elastycznego, więc `gap` jej nie dotyczy i odstęp musi zrobić margines. */
.purpose legend {
    padding: 0;
    margin-bottom: var(--s-2);
    font-size: var(--fs-14);
    font-weight: var(--fw-medium);
    color: var(--c-ink-700);
}

.purpose__options {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--s-2);
}

.purpose__options .choice {
    flex-direction: column;
    align-items: baseline;
}

/* Kafelkiem jest ETYKIETA, a pole radio siedzi w środku niewidoczne - dokładnie
   jak w `.choice`. Własny znacznik, bo motyw Symfony renderuje pole radio jako
   RODZEŃSTWO etykiety, więc `label:has(input:checked)` nie miałby czego trafić. */
.purpose__tile {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--s-3);
    min-height: var(--tap-min);
    padding: var(--s-3);
    background: var(--c-surface);
    border: 1px solid var(--c-line-strong);
    border-radius: var(--r-3);
    cursor: pointer;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
    transition:
        transform var(--dur-tap) var(--ease-out),
        background-color var(--dur-state) ease,
        border-color var(--dur-state) ease,
        box-shadow var(--dur-state) ease;
}

.purpose__tile:active {
    transform: scale(0.995);
}

/* Cień WEWNĘTRZNY zamiast grubszej ramki - `border-width: 2px` przesuwałoby
   treść o piksel i siatka drgałaby przy każdym przełączeniu. */
.purpose__tile:has(input:checked) {
    border-color: var(--c-accent);
    box-shadow: inset 0 0 0 1px var(--c-accent);
    background: var(--c-accent-weak);
}

.purpose__tile:has(input:focus-visible) {
    outline: var(--focus-ring);
    outline-offset: var(--focus-offset);
}

.purpose__tile input {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: 0;
    opacity: 0;
    pointer-events: none;
}

/* Miniatura tego, co zobaczy gość. Rysowana pudełkami, nie ikoną z pliku:
   ma pokazywać UKŁAD ekranu, a nie symbol, którego trzeba się domyślić. */
.purpose__art {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 46px;
    height: 34px;
    background: var(--c-surface-sunken);
    border: 1px solid var(--c-line);
    border-radius: var(--r-1);
}

.purpose__stars {
    display: flex;
    gap: 2px;
}

.purpose__star {
    width: 5px;
    height: 5px;
    border-radius: 1px;
    background: var(--c-warn);
}

.purpose__star--off {
    background: var(--c-line-strong);
}

.purpose__window {
    display: flex;
    align-items: flex-end;
    justify-content: center;
    width: 26px;
    height: 18px;
    padding-bottom: 3px;
    border: 1px solid var(--c-line-strong);
    border-top-width: 5px;
    border-radius: var(--r-0);
}

.purpose__window-bar {
    width: 12px;
    height: 3px;
    border-radius: var(--r-0);
    background: var(--c-accent);
}

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

.purpose__name {
    font-size: var(--fs-14);
    font-weight: var(--fw-medium);
}

.purpose__desc {
    font-size: var(--fs-12);
    line-height: var(--lh-snug);
    color: var(--c-ink-500);
}

/* ---------- Etykieta pola z odnośnikiem do podpowiedzi ---------- */

.field__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s-3);
    min-height: 29px;
}

/* Etykieta przestała być bezpośrednim dzieckiem `.field`, więc reguła
   `.field > label` już jej nie dotyczy - a decyzja właściciela z 6 sierpnia
   2026 („etykieta jest podpisem, nie celem kliknięcia") obowiązuje dalej. */
.field__head > label {
    pointer-events: none;
    width: fit-content;
}

/* Układ odniesienia dymka. Bez tego `position: absolute` w `.tip` skoczyłoby
   do najbliższego pozycjonowanego przodka, czyli w praktyce do okna. */
.field__hint {
    position: relative;
    flex: none;
}

.field__hint-trigger {
    flex: none;
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
    padding: var(--s-1) 0;
    font: inherit;
    font-size: var(--fs-12);
    color: var(--c-accent);
    background: none;
    border: 0;
    cursor: pointer;
}

.field__hint-mark {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 15px;
    height: 15px;
    border: 1px solid currentcolor;
    border-radius: var(--r-full);
    font-size: 9px;
    font-weight: var(--fw-semibold);
}

/* ---------- Ocena wklejonego linku ---------- */

/* Podpowiedź, nigdy walidacja. Serwer przyjmuje każdy poprawny adres - o tym,
   co jest linkiem do opinii, decyduje Google i zmienia to bez uprzedzenia.
   Kolor jest więc informacją, a nie odmową. */
.link-check {
    display: flex;
    align-items: flex-start;
    gap: var(--s-2);
    margin: 0;
    padding: var(--s-2) var(--s-3);
    border-radius: var(--r-2);
    font-size: var(--fs-12);
    line-height: var(--lh-snug);
    animation: link-check-in var(--dur-state) var(--ease-out) both;
}

.link-check__mark {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 16px;
    height: 16px;
    border-radius: var(--r-full);
    color: var(--c-surface);
    font-size: 10px;
    font-weight: var(--fw-semibold);
}

.link-check[data-state='ok'] {
    color: var(--c-ok);
    background: color-mix(in oklch, var(--c-ok) 8%, white);
}

.link-check[data-state='ok'] .link-check__mark {
    background: var(--c-ok);
}

.link-check[data-state='warn'] {
    color: var(--c-warn);
    background: var(--c-warn-weak);
}

.link-check[data-state='warn'] .link-check__mark {
    background: var(--c-warn);
}

.link-check[data-state='bad'] {
    color: var(--c-danger);
    background: var(--c-danger-weak);
}

.link-check[data-state='bad'] .link-check__mark {
    background: var(--c-danger);
}

@keyframes link-check-in {
    from {
        opacity: 0;
        transform: translateY(-4px);
    }
}

/* ---------- Podpowiedź „skąd wziąć link" ---------- */

/* Trzy kroki rysowane pudełkami, bez ani jednego zrzutu ekranu z Google.
   Zrzut starzeje się razem z ich interfejsem i nikt tego nie zauważy;
   schematyczny układ starzeje się wolniej i nie udaje dokumentacji. */
/* Dymek WISI pod przyciskiem, a nie stoi pod polem (13 sierpnia 2026, decyzja
   właściciela). Poprzednia wersja była zwykłym blokiem w kolumnie: otwarcie
   podpowiedzi rozpychało formularz na 300 px i spychało w dół „Dodatkowo",
   podgląd telefonu i przycisk zapisu, więc ekran skakał pod ręką przy każdym
   otwarciu i zamknięciu.

   Pozycjonowanie jest ZWYKŁE, `absolute` względem `.field__hint`, a nie warstwa
   wierzchnia z `popover` i kotwicami CSS-a. Powód jest praktyczny: dymek
   przypięty do przycisku przewija się razem ze stroną sam z siebie, więc nie ma
   tu ani jednego nasłuchu na `scroll` i `resize`, ani rachunku pozycji
   w JavaScripcie. Kotwice CSS-a rozwiązałyby to samo ładniej, ale nie ma ich
   jeszcze w Firefoksie, a właściciel lokalu nie ma jak wiedzieć, że jego
   przeglądarka jest tą jedną nieobsługiwaną.

   `right: 0` znaczy „prawa krawędź dymka na prawej krawędzi przycisku", czyli
   przy prawej krawędzi pola - dymek rośnie w lewo, w stronę ekranu, a nie poza
   niego. Szerokość jest ograniczona podwójnie, bo na telefonie samo 380 px
   wystawałoby poza kadr. */
.tip {
    position: absolute;
    top: calc(100% + var(--s-2));
    right: 0;
    z-index: 20;
    width: max-content;
    max-width: min(380px, calc(100vw - 2 * var(--s-4)));
    padding: var(--s-4);
    border-radius: var(--r-3);
    background: var(--c-stage);
    color: var(--c-stage-ink);
    box-shadow: var(--sh-3);
    transform-origin: top right;
    animation: tip-in var(--dur-enter) var(--ease-out) both;
}

/* Dzióbek. Bez niego dymek jest prostokątem, który przypadkiem wylądował nad
   polem adresu; z nim widać, że należy do pytania „skąd wziąć ten link?".
   Kwadrat obrócony o 45°, ten sam kolor co tło - róg tła, nie osobny kształt.
   `right` mierzone tak, żeby trafić mniej więcej w środek przycisku. */
.tip::before {
    content: '';
    position: absolute;
    right: var(--s-6);
    top: -4px;
    width: 10px;
    height: 10px;
    background: var(--c-stage);
    transform: rotate(45deg);
}

.tip__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
    margin-bottom: var(--s-3);
}

.tip__step {
    font-size: var(--fs-12);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--c-stage-ink-dim);
}

.tip__close {
    padding: 0 var(--s-1);
    font: inherit;
    font-size: var(--fs-18);
    line-height: 1;
    color: var(--c-stage-ink-dim);
    background: none;
    border: 0;
    cursor: pointer;
}

.tip__stage {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 132px;
    padding: var(--s-3);
    border-radius: var(--r-2);
    background: var(--c-stage-raised);
    overflow: hidden;
}

.tip__scene {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    width: 100%;
    animation: tip-step-in 300ms var(--ease-out) both;
}

.tip__search {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    padding: var(--s-2) var(--s-3);
    border-radius: var(--r-full);
    background: var(--c-surface);
    color: var(--c-ink-900);
    font-size: var(--fs-12);
}

.tip__caret {
    width: 1px;
    height: 12px;
    background: currentcolor;
    animation: tip-caret 1s steps(1) infinite;
}

.tip__card {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    padding: var(--s-2);
    border-radius: var(--r-2);
    background: var(--c-surface);
}

/* Ciemniejsze niż `--c-surface-sunken`, bo kwadrat leży na BIAŁEJ karcie
   w środku dymka, a nie na tle panelu. Pierwsza wersja użyła tokenu tła
   i zniknęła - widoczne dopiero w przeglądarce, nie w kodzie. */
.tip__avatar {
    flex: none;
    width: 34px;
    height: 34px;
    border-radius: var(--r-1);
    background: var(--c-line);
}

.tip__lines {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
    flex: 1;
}

.tip__line {
    width: 70%;
    height: 7px;
    border-radius: var(--r-1);
    background: var(--c-line-strong);
}

.tip__line--half {
    width: 50%;
}

.tip__line--wide {
    width: 75%;
    background: var(--c-line);
}

.tip__stars {
    display: flex;
    gap: 3px;
}

.tip__star {
    width: 7px;
    height: 7px;
    border-radius: 1px;
    background: var(--c-warn);
}

.tip__star--off {
    background: var(--c-line);
}

.tip__panel {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    padding: var(--s-3);
    border-radius: var(--r-2);
    background: var(--c-surface);
}

.tip__buttons {
    display: flex;
    gap: var(--s-2);
}

.tip__button {
    flex: 1;
    height: 26px;
    border-radius: var(--r-1);
    background: var(--c-surface-sunken);
}

.tip__button--primary {
    flex: 1.4;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--c-accent);
    color: var(--c-surface);
    font-size: 10px;
    font-weight: var(--fw-semibold);
    animation: tip-pulse 2s var(--ease-out) infinite;
}

/* Kursor przy kroku „kliknij" - jedyna animacja w tym dymku, która przedstawia
   czynność, a nie stan. Bez niej krok drugi jest nieruchomym obrazkiem i nie
   widać, że chodzi o kliknięcie. */
.tip__pointer {
    position: absolute;
    right: 14px;
    bottom: -6px;
    width: 14px;
    height: 14px;
    border: 2px solid var(--c-surface);
    border-radius: var(--r-full);
    background: var(--c-ink-900);
    animation: tip-tap 2s var(--ease-in-out) infinite;
}

.tip__row {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    padding: var(--s-2) var(--s-3);
    border-radius: var(--r-2);
    background: var(--c-surface);
}

.tip__row--ghost {
    background: rgba(255, 255, 255, 0.09);
    border: 1px dashed var(--c-stage-line);
}

.tip__url {
    flex: 1;
    font-family: var(--font-mono);
    font-size: var(--fs-12);
    color: var(--c-ink-900);
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.tip__copy {
    flex: none;
    padding: var(--s-1) var(--s-2);
    border-radius: var(--r-full);
    background: var(--c-ink-900);
    color: var(--c-surface);
    font-size: 10px;
    font-weight: var(--fw-semibold);
    animation: tip-pulse 2s var(--ease-out) infinite;
}

.tip__paste {
    font-size: var(--fs-12);
    color: var(--c-stage-ink-dim);
}

.tip__title {
    margin: var(--s-3) 0 var(--s-1);
    font-size: var(--fs-14);
    font-weight: var(--fw-semibold);
}

.tip__desc {
    margin: 0;
    font-size: var(--fs-12);
    line-height: var(--lh-normal);
    color: var(--c-stage-ink-dim);
}

.tip__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
    margin-top: var(--s-3);
}

.tip__dots {
    display: flex;
    align-items: center;
    gap: var(--s-1);
}

/* Kropka jest przyciskiem, więc ma prawdziwy cel dotykowy - sama plamka ma
   6 px, a otacza ją przezroczysta wyściółka do progu palca. */
.tip__dot {
    position: relative;
    width: 6px;
    height: 6px;
    padding: 0;
    border: 0;
    border-radius: var(--r-full);
    /* Nie `--c-stage-line`: ta linia jest o krok od czerni sceny i na dymku
       nie było widać ani jednej nieaktywnej kropki. */
    background: color-mix(in oklch, var(--c-stage-ink) 30%, transparent);
    cursor: pointer;
    transition:
        width var(--dur-state) ease,
        background-color var(--dur-state) ease;
}

.tip__dot::after {
    content: '';
    position: absolute;
    inset: -12px -6px;
}

.tip__dot--on {
    width: 18px;
    background: var(--c-stage-ink);
}

.tip__next {
    flex: none;
    padding: var(--s-2) var(--s-4);
    font: inherit;
    font-size: var(--fs-12);
    font-weight: var(--fw-semibold);
    color: var(--c-ink-900);
    background: var(--c-stage-ink);
    border: 0;
    border-radius: var(--r-full);
    cursor: pointer;
}

/* Dymek wychodzi spod przycisku, więc rośnie OD niego: punkt zaczepienia jest
   w prawym górnym rogu, a ruch idzie w dół. Do 13 sierpnia 2026 było odwrotnie
   (wjazd od dołu), bo dymek stał wtedy pod polem i nic nie wskazywał. */
@keyframes tip-in {
    from {
        opacity: 0;
        transform: translateY(-6px) scale(0.97);
    }
}

@keyframes tip-step-in {
    from {
        opacity: 0;
        transform: translateX(10px);
    }
}

@keyframes tip-caret {
    50% {
        opacity: 0;
    }
}

@keyframes tip-pulse {
    0% {
        box-shadow: 0 0 0 0 color-mix(in oklch, var(--c-accent) 35%, transparent);
    }

    70% {
        box-shadow: 0 0 0 10px transparent;
    }

    100% {
        box-shadow: 0 0 0 0 transparent;
    }
}

@keyframes tip-tap {
    0%,
    100% {
        transform: translate(0, 0);
    }

    45% {
        transform: translate(-14px, -10px);
    }

    55% {
        transform: translate(-14px, -10px) scale(0.82);
    }
}

/* ---------- Dodatkowo: grupa i harmonogram ---------- */

/* Bez skryptu to są dwa zwykłe pola z listą wyboru, jedno pod drugim - i tak
   stoją w znaczniku. W linię układają się dopiero pod klasą `settings--live`,
   dokładaną przez kontroler; ten sam wzorzec co `wizard--live` w kreatorze
   zamówienia. Arkusz nigdy sam nie chowa pola, którego nie da się inaczej
   pokazać.

   `position: relative` jest tu układem odniesienia dla rozwiniętej listy.
   Lista wisi pod CAŁYM rzędem, a nie pod swoim chipem: przy dwóch chipach
   w wierszu lista zaczepiona o drugi z nich wychodziłaby poza prawą krawędź
   kolumny na telefonie. */
.settings {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
}

.settings__title {
    display: none;
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

.settings--live {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-2);
}

.settings--live .settings__title {
    display: block;
}

/* Zdanie o wyłączonym harmonogramie łamie rząd, bo jest zdaniem, a nie metką. */
.settings__note {
    flex-basis: 100%;
}

/* ---------- Chip z wartością ---------- */

.chip {
    display: inline-flex;
    align-items: center;
    gap: var(--s-1);
    min-height: var(--tap-min);
    padding: 0 var(--s-3);
    font: inherit;
    font-size: var(--fs-12);
    color: var(--c-ink-700);
    background: var(--c-surface);
    border: 1px solid var(--c-line-strong);
    border-radius: var(--r-full);
    cursor: pointer;
    /* Chip przełącza listę całą powierzchnią, więc jest przyciskiem, nie
       napisem - zaznaczanie w nim tekstu to skutek uboczny celowania. */
    user-select: none;
}

.chip__name {
    color: var(--c-ink-500);
}

/* Nazwa grupy bywa dłuższa niż rząd. Ucięcie z wielokropkiem zamiast zawijania:
   chip ma zostać jedną linią, a pełną nazwę widać po otwarciu listy. */
.chip__value {
    max-width: 22ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: var(--fw-medium);
    color: var(--c-ink-900);
}

.chip__caret {
    flex: none;
    color: var(--c-ink-500);
}

.chip[aria-expanded='true'] {
    border-color: var(--c-accent);
}

.chip[aria-expanded='true'] .chip__caret {
    rotate: 180deg;
}

/* Nośnik w grupie: chip zostaje, bo jego wartość nadal jest odpowiedzią na
   pytanie „jaki harmonogram tu działa" - tylko ustawia ją grupa. Wyciszony
   i bez strzałki, żeby nie obiecywał wyboru, którego serwer i tak nie przyjmie. */
.chip--locked {
    color: var(--c-ink-500);
    background: var(--c-surface-sunken);
    border-style: solid;
    cursor: default;
}

.chip--locked .chip__value {
    font-weight: var(--fw-regular);
    color: var(--c-ink-500);
}

.chip--locked .chip__caret {
    display: none;
}

/* ---------- Rozwinięta lista wyboru ---------- */

/* Zamiennik listy systemowej. Pod spodem zostaje prawdziwe `<select>` i to ono
   jedzie z żądaniem - ta lista niczego nie ustala, tylko wygląda tak jak reszta
   panelu. Ognisko trzyma sama lista (`tabindex` w znaczniku), a wybraną pozycję
   niesie `aria-activedescendant`, więc klawiatura i czytnik ekranu dostają to
   samo, co miały za darmo w liście systemowej. */
.picker {
    position: absolute;
    top: calc(100% + var(--s-2));
    left: 0;
    right: 0;
    z-index: 2;
    display: flex;
    flex-direction: column;
    gap: 2px;
    /* Na telefonie lista zajmuje całą szerokość kolumny, na szerokim ekranie
       zatrzymuje się na mierze czytania - rozciągnięta na 600 px byłaby trzema
       nazwami rozrzuconymi po pustym pasie. */
    max-width: 420px;
    max-height: 320px;
    overflow-y: auto;
    padding: var(--s-1);
    background: var(--c-surface);
    border: 1px solid var(--c-line);
    border-radius: var(--r-3);
    box-shadow: var(--sh-3);
    animation: picker-in var(--dur-state) var(--ease-out) both;
}

@keyframes picker-in {
    from {
        opacity: 0;
        translate: 0 -4px;
    }
}

.picker__option {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    min-height: var(--tap-min);
    padding: var(--s-2) var(--s-3);
    border-radius: var(--r-2);
    cursor: pointer;
}

.picker__option:hover {
    background-color: var(--c-bg);
}

.picker__option--active, .picker__option--active:hover {
    background: var(--c-surface-sunken);
}

.picker__mark {
    flex: none;
    display: grid;
    place-items: center;
    width: 17px;
    height: 17px;
    border: 1.5px solid var(--c-line-strong);
    border-radius: var(--r-full);
}

.picker__option[aria-selected='true'] .picker__mark {
    border-color: var(--c-accent);
}

.picker__option[aria-selected='true'] .picker__mark::after {
    content: '';
    width: 9px;
    height: 9px;
    border-radius: var(--r-full);
    background: var(--c-accent);
}

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

.picker__option-name {
    font-size: var(--fs-14);
    font-weight: var(--fw-medium);
}

.picker__option[aria-selected='true'] .picker__option-name {
    color: var(--c-accent);
}

.picker__option-desc {
    font-size: var(--fs-12);
    line-height: var(--lh-snug);
    color: var(--c-ink-500);
    overflow-wrap: anywhere;
}

/* ---------- Podgląd: co zobaczy gość ---------- */

/* To MAKIETA, nie ekran gościa. Żadna linia nie pochodzi z `tag/review.html.twig`
   i nie ma prawa udawać, że pochodzi - odpowiada na jedno pytanie: „czy telefon
   zapyta o ocenę, czy od razu otworzy stronę". */
.preview {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    min-width: 0;
}

.preview__toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
    min-height: var(--tap-min);
    padding: var(--s-3);
    font: inherit;
    font-size: var(--fs-14);
    font-weight: var(--fw-medium);
    color: var(--c-ink-900);
    text-align: left;
    background: var(--c-surface);
    border: 1px solid var(--c-line);
    border-radius: var(--r-3);
    cursor: pointer;
}

.preview__toggle-state {
    flex: none;
    font-weight: var(--fw-regular);
    color: var(--c-accent);
}

.preview__body {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s-4);
    padding: var(--s-5) var(--s-4);
    border: 1px solid var(--c-line);
    border-radius: var(--r-3);
    background: var(--c-surface-sunken);
}

.preview__label {
    align-self: flex-start;
    margin: 0;
    font-size: var(--fs-12);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--c-ink-500);
}

.preview__phone {
    width: 196px;
    height: 340px;
    padding: 9px;
    border-radius: 26px;
    background: var(--c-stage);
    box-shadow: var(--sh-3);
}

.preview__screen {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s-3);
    width: 100%;
    height: 100%;
    padding: var(--s-5) var(--s-4);
    border-radius: 19px;
    background: var(--c-surface);
    overflow: hidden;
}

.preview__logo {
    width: 46px;
    height: 46px;
    border-radius: var(--r-3);
    background: var(--c-surface-sunken);
}

.preview__name {
    width: 120px;
    height: 9px;
    border-radius: var(--r-1);
    background: var(--c-line);
}

.preview__scene {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s-3);
    width: 100%;
    animation: preview-in var(--dur-state) var(--ease-out) both;
}

.preview__question {
    font-size: var(--fs-14);
    font-weight: var(--fw-semibold);
}

.preview__stars {
    display: flex;
    gap: 5px;
}

.preview__star {
    width: 20px;
    height: 20px;
    border-radius: var(--r-0);
    background: var(--c-warn);
}

.preview__star--off {
    background: var(--c-line);
}

.preview__bar {
    width: 100%;
    height: 34px;
    border-radius: var(--r-2);
    background: var(--c-ink-900);
}

/*
 * Kropki karty w makiecie telefonu. Okrągłe, w odróżnieniu od kwadratowych
 * gwiazdek wyżej - to jedyna różnica kształtu, po której widać z odległości
 * metra, czy nośnik pyta o ocenę, czy nabija pieczątkę.
 */
.preview__stamps {
    display: flex;
    gap: 6px;
}

.preview__stamp {
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: var(--c-accent);
}

.preview__stamp--off {
    background: var(--c-line);
}

.preview__hero {
    width: 100%;
    height: 60px;
    border-radius: var(--r-2);
    background: var(--c-accent);
}

.preview__line {
    width: 100%;
    height: 8px;
    border-radius: var(--r-1);
    background: var(--c-line);
}

.preview__line--short {
    width: 80%;
}

.preview__line--shorter {
    width: 60%;
}

.preview__caption {
    font-family: var(--font-mono);
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

.preview__note {
    margin: 0;
    font-size: var(--fs-12);
    line-height: var(--lh-normal);
    color: var(--c-ink-500);
    text-wrap: pretty;
}

@keyframes preview-in {
    from {
        opacity: 0;
    }
}

/*
 * Ekran nośnika łamie się na zapytanie o KONTENER, a nie o okno - tak samo jak
 * listy panelu i z tego samego powodu: od kiedy z lewej stoi nawigacja, szerokość
 * okna nie mówi już nic o szerokości czytanej kolumny. Progi są dobrane do
 * zawartości (makiety 2a i 2b z projektu), nie do żadnego urządzenia.
 *
 * Wyjątkiem zostaje pasek przyklejony do dołu okna niżej: jego próg to okno,
 * bo omija nawigację, która też jest przypięta do okna.
 */
@container (max-width: 559px) {
    /* Zwinięcie podglądu dotyczy WYŁĄCZNIE wąskiej kolumny, więc jest klasą
       ograniczoną tym zapytaniem, a nie atrybutem `hidden`. Do 13 sierpnia 2026
       było odwrotnie i kontroler musiał pilnować szerokości okna, żeby nie
       zostawić schowanego kawałka strony bez sposobu na odsłonięcie. */
    .preview--collapsed .preview__body {
        display: none;
    }
}

@container (min-width: 560px) {
    .purpose__options {
        grid-template-columns: 1fr 1fr;
    }

    /* Podglądu nie da się już zwinąć, bo nie ma po co: jest miejsce na jego
       ułożenie w poziomie obok opisu (układ 2a z projektu). */
    .preview__toggle {
        display: none;
    }

    .preview__body {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr);
        grid-template-areas:
            'phone label'
            'phone note';
        justify-content: start;
        align-content: start;
        column-gap: var(--s-5);
        row-gap: var(--s-2);
    }

    .preview__label {
        grid-area: label;
        align-self: end;
    }

    .preview__phone {
        grid-area: phone;
    }

    .preview__note {
        grid-area: note;
        align-self: start;
        max-width: 42ch;
    }
}

/* Kolumna z podglądem przy boku (układ 1b). Próg jest wyższy niż szerokość
   makiety 2a (820 px) i to jest cała jego treść: przy 820 px kolumna z telefonem
   zostawiała polu adresu 390 px, czyli mniej niż rzędowi chipów pod nim. Poniżej
   tego progu podgląd schodzi pod formularz i kładzie się poziomo. */
@container (min-width: 880px) {
    .tag-form__body {
        grid-template-columns: minmax(0, 1fr) 320px;
        gap: var(--s-6);
    }

    .preview__body {
        display: flex;
        flex-direction: column;
        align-items: center;
    }

    /* Wyrównanie z układu poziomego trzeba cofnąć jawnie: `align-self` na dziecku
       wygrywa z `align-items` rodzica, więc etykieta zostawiona z `end` odjeżdża
       w kolumnie do prawej krawędzi. Złapane zrzutem, nie w kodzie. */
    .preview__label {
        align-self: flex-start;
    }

    .preview__note {
        max-width: none;
    }
}

@media (min-width: 900px) {
    .tag-form {
        grid-template-areas:
            'head actions'
            'body body';
        grid-template-columns: 1fr auto;
        align-items: center;
    }

}

@media (max-width: 899px) {
    .tag-form__actions {
        position: sticky;
        bottom: calc(56px + env(safe-area-inset-bottom, 0px));
        z-index: 1;
        padding: var(--s-3) 0;
        background: color-mix(in oklch, var(--c-bg) 92%, transparent);
        backdrop-filter: blur(8px);
    }

    /* Wyłącznie bezpośrednie dziecko paska. Przyciski dymka potwierdzenia też
       są `.button`, a te mają się rozciągać z własnej reguły, nie przy okazji. */
    .tag-form__actions > .button {
        flex: 1;
    }

    .purpose__options {
        grid-template-columns: 1fr;
    }

    .tag-form__fields .picker {
        top: auto;
    }
}

@media (max-width: 899px) and (min-width: 560px) {
    .tag-form__body {
        grid-template-columns: 1fr 1fr;
    }

    .preview__body {
        display: flex;
    }

    .preview__label {
        align-self: center;
    }
}

/* Ruch w tym ekranie jest ilustracją, nie informacją: krok dymka czyta się tak
   samo bez wjazdu, a pulsujący przycisk bez pulsu. Zostaje sama treść. */
@media (prefers-reduced-motion: reduce) {
    .tip,
    .tip__scene,
    .tip__caret,
    .tip__pointer,
    .tip__button--primary,
    .tip__copy,
    .link-check,
    .picker,
    .power__confirm,
    .preview__scene {
        animation: none;
    }
}

/* ---------- Wybór typu ---------- */

.type-picker {
    margin: 0;
    padding: 0;
    border: 0;
    display: flex;
    flex-direction: column;
    /* Ten sam odstęp co między etykietą a polem w `.field` - legenda „Kupujesz
       jako" jest etykietą kafelków i ma stać tak samo blisko jak każda inna. */
    gap: var(--s-2);
}

/* Odstęp pod legendą robi MARGINES, nie `gap` zestawu pól - i to nie jest
   kwestia gustu. Legenda nie jest elementem układu elastycznego, tylko własnym
   pudełkiem `<fieldset>`, więc `gap` jej nie dotyczy: zmierzone w przeglądarce
   6 sierpnia 2026, dół legendy i góra kafelków stały na tym samym pikselu.
   Ta sama pułapka co selektory, które nie trafiają w znacznik - patrz
   „Checklist review" w CLAUDE.md. */
.type-picker legend {
    padding: 0;
    margin-bottom: var(--s-2);
    font-size: var(--fs-14);
    font-weight: var(--fw-medium);
    color: var(--c-ink-700);
}

.type-picker__options {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--s-2);
}

/* Kafelkiem jest ETYKIETA, a pole radio siedzi w środku niewidoczne.
   Przebudowane 6 sierpnia 2026 na prośbę właściciela - poprzednia wersja miała
   trzy wady naraz: trafić trzeba było albo dokładnie w napis, albo dokładnie
   w kółko (reszta kafelka nie reagowała), przeciągnięcie zaznaczało tekst,
   a samo kółko nic nie wnosiło - kafelek i tak niesie stan tłem i obramowaniem.

   Znacznik jest własny (`<label class="choice">` z `form_widget`), bo domyślny
   motyw Symfony renderuje `<div><label>…</label><input></div>`, czyli pole radio
   jako RODZEŃSTWO etykiety. Etykieta nie mogła wtedy być całym kafelkiem. */
.choice {
    position: relative;
    min-height: var(--tap-min);
    padding: var(--s-2) var(--s-3);
    border: 1px solid var(--c-line-strong);
    border-radius: var(--r-2);
    background: var(--c-surface);
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--s-2);
    font-size: var(--fs-14);
    color: var(--c-ink-900);
    cursor: pointer;
    /* Wybór jednego z dwóch to nie jest tekst do zaznaczania. Bez tego
       przeciągnięcie kciukiem po kafelku podświetla napis na niebiesko. */
    user-select: none;
    -webkit-tap-highlight-color: transparent;
    transition:
        transform var(--dur-tap) var(--ease-out),
        background-color var(--dur-state) ease,
        border-color var(--dur-state) ease,
        box-shadow var(--dur-state) ease;
}

.choice:active {
    /* Słabiej niż przycisk: kafelek jest większy, więc ten sam współczynnik
       dawałby ruch o wiele pikseli i wyglądałby na wygięcie układu. */
    transform: scale(0.99);
}

/* Zaznaczenie niesie obramowanie wzmocnione cieniem WEWNĘTRZNYM, nie grubszą
   ramką: zmiana `border-width` na 2 px przesuwałaby treść o piksel i cała
   siatka drgałaby przy każdym przełączeniu. */
.choice:has(input:checked) {
    border-color: var(--c-accent);
    box-shadow: inset 0 0 0 1px var(--c-accent);
    background: var(--c-accent-weak);
    font-weight: var(--fw-medium);
}

/* Pole zostaje w znaczniku - obsługa strzałkami, wysyłka formularza i czytnik
   ekranu tego wymagają. Niewidoczne, ale NIE `display: none`: tamto wyjęłoby
   je z kolejności tabulatora. */
.choice input {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: 0;
    opacity: 0;
    pointer-events: none;
}

/* Skupienie klawiaturą pokazuje się na kafelku, bo samego pola nie widać. */
.choice:has(input:focus-visible) {
    outline: var(--focus-ring);
    outline-offset: var(--focus-offset);
}

/* ---------- Gałęzie nabywcy w zamówieniu ----------

   Firma i osoba prywatna proszą o inne dane, ale obie gałęzie są w jednym
   formularzu i jadą jednym POST-em. Przełącza je CSS na podstawie zaznaczonego
   pola radio - nie JavaScript, bo formularz zamówienia ma się wypełniać także
   wtedy, gdy skrypt nie doszedł. Bez tego arkusza widać obie gałęzie naraz:
   brzydko, ale nikt nie utknie.

   Ukryte pola nadal się wysyłają, więc po stronie serwera pilnują tego dwie
   rzeczy: grupy walidacji (wymagalność zależy od wyboru, nie od pustki)
   i `Order::discardFieldsOfOtherBuyerType()` (nie zapisujemy danych z gałęzi,
   której klient nie wybrał). Żadne z pól gałęzi nie ma atrybutu `required` -
   przeglądarka odmówiłaby wysłania formularza z powodu pola, którego nie widać. */

/* Zwijanie zamiast zniknięcia (6 sierpnia 2026). `display: none` nie da się
   animować, więc gałąź jest siatką o jednym wierszu i przełącza się między
   `1fr` a `0fr` - jedyny sposób na płynne dojście do wysokości treści bez
   mierzenia jej JavaScriptem. Wymaga jednego opakowania z `overflow: hidden`,
   stąd `__branch-inner`. */
.order-form__branch {
    display: grid;
    grid-template-rows: 1fr;
    opacity: 1;
    margin-top: 0;
    transition:
        grid-template-rows var(--dur-enter) var(--ease-out),
        margin-top var(--dur-enter) var(--ease-out),
        opacity var(--dur-state) var(--ease-out);
}

.order-form__branch-inner {
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
    /* Oba potrzebne: `min-height: 0` pozwala wierszowi siatki zejść poniżej
       wysokości treści, `overflow` przycina to, co wystaje w trakcie.
       `clip`, nie `hidden`, z tego samego powodu co w kadrze kroków - element
       przewijalny da się przewinąć ustawieniem kursora w polu. */
    min-height: 0;
    overflow: clip;
    visibility: visible;
    /* Odstęp na obwódkę skupienia oddany z powrotem ujemnym marginesem:
       treść stoi tam, gdzie stała, a przycinanie zaczyna się 4 px dalej.
       Bez tego `overflow: hidden` zjadał obwódkę pól po bokach. */
    padding: 4px;
    margin: -4px;
}

.order-form:has(input[name="order_form[buyerType]"][value="person"]:checked) .order-form__branch--company,
.order-form:has(input[name="order_form[buyerType]"][value="company"]:checked) .order-form__branch--person {
    grid-template-rows: 0fr;
    opacity: 0;
    /* Zwinięta gałąź ma zerową wysokość, ale nadal jest elementem kolumny,
       więc odstęp liczy się DWA razy: przed nią i po niej. Ujemny margines
       zjada jeden z nich - inaczej po przełączeniu na osobę prywatną dziura
       pod kafelkami jest dwukrotna. */
    margin-top: calc(var(--s-4) * -1);
}

/* Zwinięta gałąź musi wypaść z kolejności tabulatora - inaczej klient
   przechodzi klawiszem do pola, którego nie widzi. `visibility` chowa się
   dopiero PO animacji (opóźnienie), a pokazuje natychmiast. */
.order-form:has(input[name="order_form[buyerType]"][value="person"]:checked) .order-form__branch--company .order-form__branch-inner,
.order-form:has(input[name="order_form[buyerType]"][value="company"]:checked) .order-form__branch--person .order-form__branch-inner {
    visibility: hidden;
    transition: visibility 0s linear var(--dur-enter);
}

/* ---------- Kreator zamówienia ----------

   Trzy kroki jednego formularza. **Bez JavaScriptu kroków nie ma**: sekcje stoją
   jedna pod drugą, pasek postępu i przyciski „Dalej"/„Wstecz" się nie pokazują,
   a „Zamawiam" na dole działa jak przedtem. Wszystko, co chowa treść, wisi więc
   pod klasą `wizard--live`, którą dopisuje dopiero kontroler - arkusz sam nigdy
   nie ukryje pola, którego nie da się inaczej pokazać. */

.wizard {
    display: flex;
    flex-direction: column;
    gap: var(--s-5);
}

/* Pasek postępu bez skryptu kłamałby: trzy kroki i tak widać naraz. */
.wizard__progress {
    display: none;
    gap: var(--s-1);
}

.wizard--live .wizard__progress {
    display: flex;
    /* Segment ma pełne 44 px celu dotykowego (`--tap-min`), z czego widać
       6 px paska - reszta to przezroczysty odstęp. Ujemny margines oddaje
       ten odstęp z powrotem układowi, żeby pasek nie odjechał od nagłówka
       o wysokość, której nie widać. */
    margin-block: calc((6px - var(--tap-min)) / 2);
}

.wizard__segment {
    flex: 1;
    min-height: var(--tap-min);
    padding: 0;
    border: 0;
    background: transparent;
    display: flex;
    align-items: center;
    cursor: pointer;
}

.wizard__segment-bar {
    flex: 1;
    height: 6px;
    border-radius: var(--r-full);
    background: var(--c-line);
    overflow: hidden;
    transition: transform var(--dur-state) var(--ease-out);
}

/* Powiększenie samego paska, nie przycisku: rośnie to, co widać. */
@media (hover: hover) and (pointer: fine) {
    .wizard__segment:hover .wizard__segment-bar {
        transform: scaleY(1.6);
    }
}

.wizard__segment:focus-visible {
    outline: none;
}

.wizard__segment:focus-visible .wizard__segment-bar {
    outline: var(--focus-ring);
    outline-offset: var(--focus-offset);
}

.wizard__segment-bar::after {
    content: '';
    display: block;
    height: 100%;
    border-radius: inherit;
    background: var(--c-ink-900);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform var(--dur-slide) var(--ease-in-out);
}

/* Trzy reguły zamiast wyliczania w CSS-ie - kroków jest trzy i widać to
   wprost. Czwarty krok wymaga dopisania czwartej linijki. */
.wizard[data-step="0"] .wizard__segment:nth-child(1) .wizard__segment-bar::after,
.wizard[data-step="1"] .wizard__segment:nth-child(-n+2) .wizard__segment-bar::after,
.wizard[data-step="2"] .wizard__segment .wizard__segment-bar::after {
    transform: scaleX(1);
}

.wizard[data-step="0"] { --wizard-index: 0; }
.wizard[data-step="1"] { --wizard-index: 1; }
.wizard[data-step="2"] { --wizard-index: 2; }

/*
 * `clip`, nie `hidden`, i to nie jest kosmetyka. `overflow: hidden` tworzy
 * element PRZEWIJALNY - tylko bez pasków. Ustawienie kursora w polu z błędem
 * kazało przeglądarce przewinąć ten kadr w bok, żeby pokazać pole; kadr jechał
 * więc niezależnie od ścieżki i na ekranie stał krok trzeci, podczas gdy cała
 * reszta interfejsu (pasek postępu, adres, `inert`) mówiła „krok drugi".
 * `clip` nie daje się przewinąć w ogóle. Złapane w przeglądarce 6 sierpnia 2026.
 */
.wizard--live .wizard__viewport {
    overflow: clip;
    /* Wysokość ustawia kontroler z pomiaru bieżącego kroku. */
}

/* Animowana wyłącznie przy zmianie kroku. Przy zmianie wywołanej treścią
   (rozwinięcie gałęzi nabywcy, dorysowany błąd) ramka ma nadążać co do klatki,
   więc kontroler tę klasę zdejmuje - inaczej przycina treść w trakcie. */
.wizard__viewport--animated {
    transition: height var(--dur-slide) var(--ease-in-out);
}

.wizard__track {
    display: flex;
    flex-direction: column;
    gap: var(--s-7);
}

/* Kroki obok siebie, każdy szerokości kadru. `width: 100%` na ścieżce jest
   tu istotne: bez niego `translateX(-100%)` przesuwałoby o szerokość WSZYSTKICH
   kolumn naraz, a nie o jeden krok. Kolumny wystają poza kadr i to jest
   zamierzone - kadr je przycina. */
.wizard--live .wizard__track {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 100%;
    gap: 0;
    width: 100%;
    align-items: start;
    transform: translateX(calc(var(--wizard-index, 0) * -100%));
    transition: transform var(--dur-slide) var(--ease-in-out);
}

.wizard__step {
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
}

/* Odstęp na obwódkę skupienia: 2 px grubości plus 2 px odsunięcia
   (`--focus-ring`, `--focus-offset`). Bez niego kadr obcinałby obwódkę
   przyciskowi na dole kroku. Siedzi na kroku, nie na kadrze - dzięki temu
   mieści się w mierzonej wysokości i kontroler nie musi o nim wiedzieć. */
.wizard--live .wizard__step {
    padding: 4px;
}

.wizard__step:focus {
    outline: none;
}

.wizard__actions {
    display: flex;
    gap: var(--s-3);
    margin-top: var(--s-2);
}

.wizard__actions .button {
    flex: 1;
}

.wizard__actions .wizard__back {
    flex: 0 0 auto;
}

/* Bez skryptu te przyciski nie mają dokąd prowadzić. „Zamawiam" nie ma tej
   klasy i zostaje widoczny zawsze. */
.wizard__back,
.wizard__next {
    display: none;
}

.wizard--live .wizard__back,
.wizard--live .wizard__next {
    display: flex;
}

/* ---------- Krok pierwszy: rzecz, ilość, kwota ---------- */

.order-product {
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
}

/* Ta sama ramka rysunku co na kafelku w katalogu, tylko zaokrąglona sama
   z siebie - nie siedzi tu w `.product`, które przycinało róg za nią. */
.order-product .product__scene {
    border-radius: var(--r-3);
    overflow: hidden;
}

.order-product__body {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
}

.order-product__price {
    margin: 0;
    font-size: var(--fs-18);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-tight);
}

.order-product__unit {
    font-size: var(--fs-14);
    font-weight: var(--fw-regular);
    color: var(--c-ink-500);
}

.order-config {
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
}

/* Liczba sztuk. Pole liczbowe systemowe (strzałki na 12 px) jest nie do
   trafienia kciukiem, stąd własne przyciski po bokach - każdy pełnej wysokości
   pola. Waga selektora: reguła `.field input[type="number"]` wyżej ma tę samą
   wagę co ta, więc wygrywa kolejność w pliku. Ta sekcja MUSI zostać poniżej
   sekcji formularzy - ta sama pułapka, która zjadła czerwoną ramkę pola
   z błędem (patrz komentarz przy `.field--invalid`). */
.stepper {
    display: inline-grid;
    grid-auto-flow: column;
    align-items: stretch;
    width: fit-content;
    border: 1px solid var(--c-line-strong);
    border-radius: var(--r-3);
    background: var(--c-surface);
    overflow: hidden;
}

.stepper__button {
    width: 52px;
    min-height: var(--tap-min);
    display: grid;
    place-items: center;
    border: 0;
    background: transparent;
    color: var(--c-ink-700);
    cursor: pointer;
    transition: background-color var(--dur-state) ease, color var(--dur-state) ease;
}

@media (hover: hover) and (pointer: fine) {
    .stepper__button:hover:not(:disabled) {
        background: var(--c-surface-sunken);
    }
}

.stepper__button:active:not(:disabled) {
    background: var(--c-surface-sunken);
}

/* Granica zakresu pokazana, zanim klient w nią uderzy. Kursor domyślny,
   nie „zabroniony": przycisk nie odmawia, po prostu nie ma co zrobić. */
.stepper__button:disabled {
    color: var(--c-line-strong);
    cursor: default;
}

/* Obwódka skupienia obejmuje całą pigułkę, nie samo pole. Pole nie ma
   obramowania, więc jego własna obwódka rysowała prostokąt WEWNĄTRZ pigułki -
   dwa zaokrąglenia jedno w drugim. Zdjęcie jej bez zastępstwa zostawiłoby
   klawiaturę bez informacji, gdzie jest kursor. */
.stepper:has(input:focus-visible) {
    outline: var(--focus-ring);
    outline-offset: var(--focus-offset);
}

.stepper input[type="number"]:focus-visible {
    outline: none;
}

.stepper input[type="number"] {
    width: 84px;
    min-height: var(--tap-min);
    padding: 0 var(--s-1);
    border: 0;
    border-inline: 1px solid var(--c-line);
    border-radius: 0;
    background: transparent;
    text-align: center;
    font-size: var(--fs-18);
    font-weight: var(--fw-medium);
    /* Strzałki systemowe zdejmujemy - od tego są przyciski po bokach. */
    appearance: textfield;
    font-variant-numeric: tabular-nums;
}

.stepper input[type="number"]::-webkit-outer-spin-button,
.stepper input[type="number"]::-webkit-inner-spin-button {
    appearance: none;
    margin: 0;
}

/* Kwota podglądowa. Liczona w przeglądarce, więc bez skryptu nie ma jej wcale -
   kwota zmyślona byłaby gorsza niż jej brak. Źródłem prawdy jest `Pricing`. */
.order-total {
    margin: 0;
    padding: var(--s-3) var(--s-4);
    border-radius: var(--r-2);
    background: var(--c-surface-sunken);
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s-3);
}

.order-total__label {
    font-size: var(--fs-14);
    color: var(--c-ink-700);
}

.order-total__value {
    font-size: var(--fs-22);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-tight);
    /* Cyfry tej samej szerokości: kwota zmienia się przy każdym dotknięciu
       „więcej" i nie ma prawa przy tym drgać. */
    font-variant-numeric: tabular-nums;
}

/* Notka wyróżniona delikatnie: wciśnięte tło i ikona, bez ramki i bez koloru
   stanu. Mówi, co się stanie PO zamówieniu - to nie jest ostrzeżenie, więc nie
   ma prawa wyglądać jak `.alert`. */
.note {
    margin: 0;
    padding: var(--s-3) var(--s-4);
    border-radius: var(--r-2);
    background: var(--c-surface-sunken);
    display: flex;
    align-items: flex-start;
    gap: var(--s-3);
    font-size: var(--fs-14);
    line-height: var(--lh-normal);
    color: var(--c-ink-700);
    text-wrap: pretty;
}

.note__icon {
    flex: none;
    /* Wyrównanie do pierwszej linijki tekstu, nie do góry bloku. */
    margin-top: 3px;
    color: var(--c-ink-500);
}

/* ---------- Pole zablokowane (adres e-mail zalogowanego) ---------- */

/* Pole, którego nie da się zmienić, ma **wyglądać** na niezmienne - inaczej
   klient próbuje w nie pisać i nie rozumie, czemu nic się nie dzieje.
   Wciśnięte tło zamiast białego, wyciszony tekst i kursor mówiący „to nie jest
   miejsce do pisania".

   `readonly`, nie `disabled`, więc selektor jest na atrybucie: pole zostaje
   w kolejności tabulatora i da się z niego skopiować własny adres.
   Uzasadnienie stoi przy samym polu w `shop/order.html.twig`.

   **`.field input` w selektorze nie jest ozdobą - bez tego reguła nie działa.**
   Wyżej stoi `.field input[type="email"]` o wadze (0,2,1); samo
   `.field__input--locked[readonly]` ma (0,2,0), więc przegrywa i pole zostaje
   białe. Złapane w przeglądarce zrzutem, nie testem: znacznik był poprawny,
   klasa na miejscu, a pole wyglądało jak zwykłe. Dokładnie ta klasa błędu,
   przed którą ostrzega CLAUDE.md przy `.field--invalid`. */
.field input.field__input--locked[readonly] {
    background: var(--c-surface-sunken);
    color: var(--c-ink-500);
    border-color: var(--c-line);
    cursor: default;
}

/* Obwódka skupienia zostaje - pole jest osiągalne klawiaturą, więc musi być
   widać, że kursor na nim stoi. Znika za to podświetlenie „aktywnego pola",
   bo nic tu nie zacznie się dziać. */
.field input.field__input--locked[readonly]:focus {
    border-color: var(--c-line-strong);
    box-shadow: none;
}

/* ---------- Przełącznik aktywności ---------- */

.toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-4);
    min-height: var(--tap-min);
    padding: var(--s-3) var(--s-4);
    background: var(--c-surface);
    border: 1px solid var(--c-line);
    border-radius: var(--r-2);
    cursor: pointer;
    /* Cały element przełącza stan, więc jest przyciskiem, nie akapitem -
       zaznaczanie w nim tekstu jest wyłącznie skutkiem ubocznym celowania. */
    user-select: none;
}

.toggle__description {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.toggle__name {
    font-size: var(--fs-16);
    font-weight: var(--fw-medium);
}

.toggle__help {
    font-size: var(--fs-12);
    line-height: var(--lh-snug);
    color: var(--c-ink-500);
}

/* Wariant do paska akcji: bez ramki i bez tła, bo pasek jest już powierzchnią.
   Sam przełącznik zostaje ten sam co w zamówieniu - inaczej byłyby dwa
   przełączniki o dwóch wyglądach na jednym koncie. */
.toggle--compact {
    justify-content: flex-start;
    gap: var(--s-2);
    padding: 0;
    background: none;
    border: 0;
}

/* Przełącznik rysowany od zera (6 sierpnia 2026). Wcześniej było to systemowe
   pole wyboru rozciągnięte do 44×26 px - przeglądarka i tak rysowała kwadracik,
   więc `role="switch"` obiecywał coś, czego nie było widać.

   Znacznik zostaje bez zmian: `appearance: none` odbiera polu wygląd elementu
   zastępowanego, a wtedy jego pseudoelement daje się narysować. Gdyby któraś
   przeglądarka tego nie zrobiła, zostaje sam tor - stan nadal widać po kolorze,
   więc degradacja jest łagodna. */
.toggle input[type="checkbox"] {
    flex: none;
    appearance: none;
    position: relative;
    width: 44px;
    height: 26px;
    margin: 0;
    border: 0;
    border-radius: var(--r-full);
    background: var(--c-line-strong);
    cursor: pointer;
    transition: background-color var(--dur-state) ease;
}

.toggle input[type="checkbox"]::after {
    content: '';
    position: absolute;
    inset-block: 3px;
    left: 3px;
    width: 20px;
    border-radius: var(--r-full);
    background: var(--c-surface);
    box-shadow: var(--sh-1);
    transition: translate var(--dur-state) var(--ease-out);
}

.toggle input[type="checkbox"]:checked {
    background: var(--c-accent);
}

.toggle input[type="checkbox"]:checked::after {
    translate: 18px 0;
}

/* ---------- Wyłączenie nośnika: dymek przy przełączniku ---------- */

/* Układ odniesienia dymka. Bez kotwic CSS-a (brak wsparcia w Firefoksie)
   i bez nasłuchów na `scroll` - dymek jedzie razem ze stroną, tak samo jak
   podpowiedź „skąd wziąć link". */
.power {
    position: relative;
    display: flex;
    align-items: center;
}

/* Na wąskim ekranie pasek akcji jest przyklejony do dolnej krawędzi okna,
   więc dymek otwiera się DO GÓRY. Otwarty w dół wyszedłby poza ekran razem
   z przyciskami, którymi się go zamyka. */
.power__confirm {
    position: absolute;
    z-index: 3;
    bottom: calc(100% + var(--s-2));
    left: 0;
    width: min(320px, calc(100vw - 2 * var(--s-4)));
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    padding: var(--s-4);
    background: var(--c-surface);
    border: 1px solid var(--c-warn);
    border-radius: var(--r-3);
    box-shadow: var(--sh-3);
    transform-origin: bottom left;
    animation: confirmation-in 260ms var(--ease-out) both;
}

.power__heading {
    margin: 0;
    font-size: var(--fs-14);
    font-weight: var(--fw-semibold);
}

.power__content {
    margin: 0;
    font-size: var(--fs-12);
    line-height: var(--lh-normal);
    color: var(--c-ink-700);
    text-wrap: pretty;
}

.power__actions {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
}

/* Od 900 px pasek akcji stoi w prawym górnym rogu, więc dymek otwiera się w dół
   i wyrównuje do prawej krawędzi - w lewo wchodziłby na nagłówek.

   Blok stoi TUTAJ, tuż za regułą, którą cofa, a nie przy pozostałych progach
   ekranu nośnika. Zapytanie o media nie podnosi wagi selektora: reguła zapisana
   niżej w pliku wygrywa niezależnie od tego, ile warunków ma nad sobą. Napisany
   wyżej dawał pudełko z ustawionym naraz `top` i `bottom` - czyli dymek
   spłaszczony do 34 px, wystający poza prawą krawędź okna. Zmierzone
   w przeglądarce, w kodzie wyglądało dobrze. */
@media (min-width: 900px) {
    .power__confirm {
        top: calc(100% + var(--s-2));
        bottom: auto;
        left: auto;
        right: 0;
        transform-origin: top right;
    }
}

/* ---------- Potwierdzenie wyłączenia ---------- */

/* Jedyne miejsce w panelu, które ma zwrócić uwagę: skutek wyłączenia jest
   niewidoczny w panelu, a odczuwa go klient przed drzwiami. Dlatego blok
   wjeżdża wyraźniej niż cokolwiek innego - ale nadal pod 300 ms. */
.confirmation {
    padding: var(--s-4);
    border: 1px solid var(--c-warn);
    border-radius: var(--r-3);
    background: color-mix(in oklch, var(--c-warn) 8%, white);
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    /* Blok bez skryptu nosi też klasę `tag-form__actions`, a ta ustawia
       `align-items: center` pod rząd „przełącznik plus Zapisz". W kolumnie
       oznaczałoby to akapity zwężone do własnej treści i wyśrodkowane. */
    align-items: stretch;
    transform-origin: top center;
    animation: confirmation-in 260ms var(--ease-out) both;
}

@keyframes confirmation-in {
    from {
        opacity: 0;
        /* Nigdy scale(0) - nic w świecie nie pojawia się z niczego. */
        transform: scale(0.98) translateY(-8px);
    }
}

.confirmation__heading {
    margin: 0;
    font-size: var(--fs-16);
    font-weight: var(--fw-semibold);
}

.confirmation__content {
    margin: 0;
    font-size: var(--fs-14);
    line-height: var(--lh-normal);
    color: var(--c-ink-700);
    text-wrap: pretty;
}

.confirmation__actions {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
}

/* ---------- Lista tagów ---------- */

.tags {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    margin: 0;
    padding: 0;
    list-style: none;
}

.tag {
    background: var(--c-surface);
    border: 1px solid var(--c-line);
    border-radius: var(--r-3);
    box-shadow: var(--sh-1);
    padding: var(--s-4);
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    transition:
        border-color var(--dur-state) ease,
        box-shadow var(--dur-state) ease;
}

@media (hover: hover) and (pointer: fine) {
    .tag:hover {
        border-color: var(--c-line-strong);
        box-shadow: var(--sh-2);
    }
}

.tag--inactive {
    background: var(--c-surface-sunken);
    box-shadow: none;
}

.tag__top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
}

.tag__name {
    font-size: var(--fs-16);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-tight);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tag__code {
    font-family: var(--font-mono);
    font-size: var(--fs-14);
    font-weight: var(--fw-medium);
}

/* Gdy nośnik ma nazwę, kod schodzi do roli metki - nadal jest zaczepieniem
   nazwanego przejścia, więc nie wolno go stąd usunąć, tylko wyciszyć. */
.tag__top:has(.tag__name) .tag__code {
    margin-right: auto;
    font-size: var(--fs-12);
    font-weight: var(--fw-regular);
    color: var(--c-ink-500);
}

.tag__status {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    font-size: var(--fs-12);
    color: var(--c-ink-700);
}

.tag__dot {
    width: 8px;
    height: 8px;
    border-radius: var(--r-full);
    background: var(--c-ok);
}

.tag__status--off {
    font-weight: var(--fw-medium);
    color: var(--c-ink-900);
}

.tag__status--off .tag__dot {
    background: var(--c-surface);
    border: 2px solid var(--c-line-strong);
}

/* Rodzaj nośnika rysunkiem, nie metką z napisem (7 sierpnia 2026). Ten sam
   zestaw znaków co katalog, sklep i landing - `templates/shop/_marks/`.

   Grubość kreski podnoszona w arkuszu, bo znak rysowany na 120 jednostkach ma
   ją dobraną do rozmiaru kafla w katalogu; w 26 px kreska 3 schodzi poniżej
   piksela i znak blaknie. Reguła CSS wygrywa z atrybutem `stroke-width`
   w pliku, a dzieci dziedziczą wartość, bo żadne nie ustawia własnej. */
.tag__mark {
    display: inline-flex;
    align-items: center;
    vertical-align: middle;
}

.tag__mark svg {
    display: block;
    width: 26px;
    height: auto;
    stroke-width: 5;
}

.tag__target {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.tag__target-label {
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

.tag__target-url {
    font-size: var(--fs-14);
    line-height: var(--lh-snug);
    color: var(--c-ink-500);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    word-break: break-all;
}

.tag__target-host {
    color: var(--c-ink-900);
    font-weight: var(--fw-medium);
}

.tag--inactive .tag__target-host {
    color: var(--c-ink-700);
}

/* ---------- Stan pusty ---------- */

.empty-state {
    display: flex;
    flex-direction: column;
    gap: var(--s-5);
    padding: var(--s-6) 0;
}

.empty-state__header {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
}

.empty-state__description {
    margin: 0;
    font-size: var(--fs-16);
    line-height: var(--lh-normal);
    color: var(--c-ink-700);
    text-wrap: pretty;
}

.empty-state__steps {
    margin: 0;
    padding: var(--s-4);
    list-style: none;
    background: var(--c-surface);
    border: 1px solid var(--c-line);
    border-radius: var(--r-3);
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
    counter-reset: krok;
}

.empty-state__steps li {
    display: flex;
    gap: var(--s-3);
    align-items: flex-start;
    font-size: var(--fs-14);
    line-height: var(--lh-normal);
    color: var(--c-ink-700);
    counter-increment: krok;
}

.empty-state__steps li::before {
    content: counter(krok);
    flex: none;
    width: 24px;
    height: 24px;
    border-radius: var(--r-full);
    background: var(--c-surface-sunken);
    color: var(--c-ink-700);
    font-size: var(--fs-12);
    font-weight: var(--fw-semibold);
    display: flex;
    align-items: center;
    justify-content: center;
}

/* ---------- Strony publiczne (widok z telefonu) ---------- */

/* Klient końcowy widzi tę stronę raz, po przyłożeniu telefonu. Delikatne
   wejście sprawia, że wygląda jak zaplanowana informacja, a nie jak awaria -
   ale treść jest czytelna od pierwszej klatki, bo ktoś stoi pod drzwiami. */
.public-page {
    max-width: var(--content-max);
    margin: 0 auto;
    padding: var(--s-8) var(--s-4);
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
    text-align: center;
    align-items: center;
    animation: page-in var(--dur-enter) var(--ease-out) both;
}

@keyframes page-in {
    from {
        opacity: 0;
        transform: translateY(6px);
    }
}

.public-page h1 {
    margin: 0;
    font-size: var(--fs-22);
    line-height: var(--lh-snug);
    font-weight: var(--fw-semibold);
}

.public-page p {
    margin: 0;
    font-size: var(--fs-16);
    line-height: var(--lh-normal);
    color: var(--c-ink-700);
    text-wrap: pretty;
}

/* ---------- Ograniczenie ruchu ---------- */

/*
 * Mniej ruchu, nie zero informacji: przejścia koloru i przezroczystości
 * zostają, bo pomagają zrozumieć zmianę stanu. Znika przesuwanie
 * i skalowanie, czyli to, co wywołuje chorobę lokomocyjną.
 */
@media (prefers-reduced-motion: reduce) {
    .button,
    .choice {
        transition: background-color var(--dur-state) ease, border-color var(--dur-state) ease;
    }

    .button:active,
    .choice:active {
        transform: none;
    }

    /* Krok zmienia się natychmiast, bez przejazdu w bok i bez rozwijania
       wysokości. Sam pasek postępu też przestaje sunąć - to ruch, nie kolor. */
    .wizard--live .wizard__track,
    .wizard__viewport--animated,
    .wizard__segment-bar,
    .wizard__segment-bar::after {
        transition: none;
    }

    .wizard__segment:hover .wizard__segment-bar {
        transform: none;
    }

    /* Gałąź nabywcy zwija się od razu; zostaje samo ściemnienie, bo ono niesie
       informację „to pole odeszło", a nie ruch. */
    .order-form__branch {
        transition: opacity var(--dur-state) ease;
    }

    /* Suwak przełącznika przeskakuje; stan i tak niesie kolor toru. */
    .toggle input[type="checkbox"]::after {
        transition: none;
    }

    .alert,
    .confirmation,
    .public-page {
        animation: fade-in 160ms ease both;
    }

    .header__dropdown,
    .header__dropdown-content {
        transition: opacity var(--dur-state) ease;
    }

    .header__dropdown-content {
        transform: none;
    }

    @keyframes fade-in {
        from {
            opacity: 0;
        }
    }
}

/* ---------- Wykres przyłożeń ---------- */

.chart {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    padding: var(--s-4);
    background: var(--c-surface);
    border: 1px solid var(--c-line);
    border-radius: var(--r-3);
}

.chart__header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s-3);
}

.chart__heading {
    margin: 0;
    font-size: var(--fs-14);
    font-weight: var(--fw-medium);
    color: var(--c-ink-700);
}

.chart__total {
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

.chart__empty {
    margin: 0;
    font-size: var(--fs-14);
    line-height: var(--lh-normal);
    color: var(--c-ink-500);
    text-wrap: pretty;
}

.chart__canvas {
    display: flex;
    align-items: flex-end;
    gap: 2px;
    height: 72px;
}

.chart__bar {
    flex: 1;
    height: var(--height);
    min-height: 2px;
    border-radius: var(--r-0) var(--r-0) 0 0;
    background: var(--c-accent);
    /* Słupki dorastają po wejściu na stronę: raz, 220 ms, bez opóźnień
       kaskadowych - to podsumowanie do przeczytania, nie animacja do obejrzenia. */
    animation: chart-bar-in var(--dur-enter) var(--ease-out) both;
    transform-origin: bottom;
}

.chart__bar--empty {
    background: var(--c-line);
}

@keyframes chart-bar-in {
    from {
        transform: scaleY(0.2);
        opacity: 0;
    }
}

.chart__axis {
    display: flex;
    justify-content: space-between;
    font-family: var(--font-mono);
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

.tag__taps {
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

/* Ostrzeżenie, nie błąd: nośnik prawdopodobnie zniknął ze ściany, ale nic
   się nie zepsuło i nie ma tu żadnej akcji do wykonania w panelu. */
.tag__silent {
    padding: 2px var(--s-2);
    border-radius: var(--r-full);
    background: var(--c-warn-weak);
    color: var(--c-warn);
    font-size: var(--fs-12);
    font-weight: var(--fw-medium);
}

@media (prefers-reduced-motion: reduce) {
    .chart__bar {
        animation: fade-in 160ms ease both;
    }
}

/*
 * Miesiąc, który jeszcze trwa. Ma być widocznie niepełny, a nie po prostu
 * niższy - inaczej pierwszego dnia miesiąca wykres opowiada o załamaniu
 * ruchu, którego nie ma. Pasy zamiast innego koloru, bo kolor już niesie
 * znaczenie „pusty" na sąsiednim słupku.
 */
.chart__bar--partial {
    background: repeating-linear-gradient(
        135deg,
        var(--c-accent) 0 4px,
        var(--c-surface) 4px 6px
    );
}

/* ---------- Porównania okresów ---------- */

.compare {
    display: grid;
    gap: var(--s-3);
}

/* Ta sama granica, co reszta panelu: od 900 px jest miejsce na dwie kolumny. */
@media (min-width: 900px) {
    .compare {
        grid-template-columns: 1fr 1fr;
    }
}

.compare__card {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    padding: var(--s-4);
    background: var(--c-surface-sunken);
    border-radius: var(--r-2);
}

.compare__heading {
    margin: 0;
    font-size: var(--fs-12);
    font-weight: var(--fw-medium);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--c-ink-500);
}

.compare__numbers {
    display: flex;
    align-items: flex-end;
    gap: var(--s-3);
    margin: 0;
}

.compare__side {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.compare__label {
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

.compare__value {
    font-family: var(--font-mono);
    font-size: var(--fs-22);
    line-height: 1;
    color: var(--c-ink-900);
}

.compare__value--previous {
    color: var(--c-ink-500);
}

.compare__versus {
    padding-bottom: 2px;
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

.compare__change {
    margin: 0;
    font-size: var(--fs-14);
    font-weight: var(--fw-medium);
}

.compare__change--up {
    color: var(--c-accent);
}

/*
 * Spadek jest tu informacją, nie awarią. Czerwień z komunikatów błędu
 * kazałaby właścicielowi szukać usterki tam, gdzie jej nie ma - w lipcu
 * ruch bywa mniejszy, bo lokal miał urlop.
 */
.compare__change--down,
.compare__change--flat {
    color: var(--c-ink-500);
}

.compare__note,
.compare__empty {
    margin: 0;
    font-size: var(--fs-12);
    line-height: var(--lh-normal);
    color: var(--c-ink-500);
    text-wrap: pretty;
}

/* ---------- Dni tygodnia ---------- */

.weekdays {
    display: flex;
    gap: var(--s-2);
    align-items: flex-end;
    margin: 0;
    padding: 0;
    list-style: none;
}

.weekdays__day {
    display: flex;
    flex: 1;
    flex-direction: column;
    align-items: center;
    gap: var(--s-1);
}

.weekdays__value {
    font-family: var(--font-mono);
    font-size: var(--fs-12);
    color: var(--c-ink-700);
}

.weekdays__track {
    display: flex;
    align-items: flex-end;
    width: 100%;
    height: 64px;
}

.weekdays__bar {
    width: 100%;
    height: var(--height);
    min-height: 2px;
    border-radius: var(--r-0) var(--r-0) 0 0;
    background: var(--c-accent);
    animation: chart-bar-in var(--dur-enter) var(--ease-out) both;
    transform-origin: bottom;
}

.weekdays__label {
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

@media (prefers-reduced-motion: reduce) {
    .weekdays__bar {
        animation: fade-in 160ms ease both;
    }
}

/* ---------- Zaproszenie do planu płatnego ---------- */

.plan-upsell__lead {
    margin: 0;
    font-size: var(--fs-14);
    line-height: var(--lh-normal);
    color: var(--c-ink-700);
    text-wrap: pretty;
}

.plan-upsell__list {
    margin: 0;
    padding-left: var(--s-4);
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
    font-size: var(--fs-14);
    color: var(--c-ink-700);
}

/* ---------- Pasek planu ---------- */

.plan {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
    flex-wrap: wrap;
    padding: var(--s-3) var(--s-4);
    background: var(--c-surface-sunken);
    border-radius: var(--r-2);
}

.plan__description {
    font-size: var(--fs-14);
    color: var(--c-ink-700);
}

/* ---------- Panel: powłoka z nawigacją ---------- */

/*
 * Nawigacja jest jedna, w dwóch kształtach. Tutaj stoi wariant telefonowy:
 * pasek przy dolnej krawędzi, w zasięgu kciuka. Wariant kolumnowy siedzi
 * w bloku 900 px na końcu pliku.
 *
 * Menu schowane pod hamburgerem byłoby dla tego użytkownika gorsze: wchodzi
 * raz na kilka tygodni, więc każde wejście jest pierwszym wejściem - sekcje
 * mają być widoczne, a nie do odkrycia.
 */
.panel {
    margin: 0 auto;
    max-width: var(--panel-shell-max);
}

/* Miejsca pod pasek nawigacji **nie robi już kolumna treści**, tylko stopka
   (`body:has(.nav) .footer`) - od 7 sierpnia 2026 to ona jest ostatnim
   elementem strony. Zostawiony odstęp byłby pustym pasem między listą
   a stopką, a paska i tak by nie ominął. */

.nav {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    /* Nad treścią, ale pod banerem powiadomień (100). */
    z-index: 90;
    background: var(--c-surface);
    border-top: 1px solid var(--c-line);
    padding-bottom: env(safe-area-inset-bottom, 0px);
}

.nav__list {
    display: flex;
    /* Na tablecie pasek rozciągnięty na 1000 px dawałby pięć celów po 200 px
       z ikoną samotną pośrodku. Kciuk i tak nie sięga dalej niż na telefonie. */
    max-width: 560px;
    margin: 0 auto;
    padding: 0;
    list-style: none;
}

.nav__slot {
    flex: 1 1 0;
    min-width: 0;
}

.nav__item {
    position: relative;
    min-height: 56px;
    padding: var(--s-2) 2px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    text-decoration: none;
    color: var(--c-ink-500);
    /* Poniżej skali panelu i celowo: to etykieta ikony, nie tekst do czytania.
       Pięć polskich nazw sekcji nie zmieści się w 375 px w rozmiarze 12 px.

       Od M6 sekcji jest sześć, a razem z wyjściem siedem celów w pasku.
       Zmierzone w przeglądarce przy 390 px: slot ma 56 px, więc przy odstępie
       4 px na stronę zostaje 48 px, a „Statystyki" potrzebują 54 px przy 11 px
       - czyli szósta sekcja przycięłaby napis, który mieścił się do wczoraj.
       Stąd 10 px i węższy odstęp poziomy: to daje 52 px użytecznej szerokości
       i wszystkie siedem etykiet mieści się w całości. Wysokość celu
       dotykowego (56 px) się nie zmienia - zmienia się sam napis pod ikoną. */
    font-size: 10px;
    line-height: 1.1;
    transition: color var(--dur-state) ease;
}

.nav__label {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.nav__icon {
    flex: none;
    display: block;
}

.nav__item--active {
    color: var(--c-ink-900);
    font-weight: var(--fw-medium);
}

/* Kreska przy górnej krawędzi paska: sam kolor pisma to za mało, żeby zobaczyć
   położenie kątem oka. Na szerokim ekranie znika - tam robi to tło pozycji. */
.nav__item--active::before {
    content: "";
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 24px;
    height: 2px;
    border-radius: 0 0 var(--r-full) var(--r-full);
    background: var(--c-ink-900);
}

@media (hover: hover) and (pointer: fine) {
    .nav__item:hover {
        color: var(--c-ink-900);
    }
}

/*
 * Wylogowanie w pasku sekcji. Ten sam kształt co pozycja nawigacji, ale to
 * przycisk formularza (POST z tokenem), więc trzeba zdjąć krój przycisku
 * systemowego. Rozmiar pisma zostaje z `.nav__item` - dlatego nie używamy tu
 * skrótu `font: inherit`, który wyzerowałby go razem z krojem.
 */
.logout-form--nav {
    display: flex;
    width: 100%;
}

.nav__item--exit {
    width: 100%;
    border: 0;
    background: none;
    font-family: inherit;
    cursor: pointer;
}

/* Plan należy do konta, nie do listy sekcji. Na telefonie pasek dolny mieści
   wyłącznie cele dotykowe, więc plan widać dopiero na ekranie „Konto". */
.nav__plan,
.nav__buy {
    display: none;
}

/* ---------- Panel: karta, dane, pytania, ranking ---------- */

.card {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    padding: var(--s-4);
    background: var(--c-surface);
    border: 1px solid var(--c-line);
    border-radius: var(--r-3);
}

/* Tylko bezpośrednie dziecko: przycisk wysyłki formularza siedzi w `.fields`
   i ma zostać na pełną szerokość, bo to on jest głównym celem dotykowym. */
.card > .button,
.card > .logout-form,
.card > .subscribe-form {
    align-self: flex-start;
}

/*
 * Wylogowanie i włączenie subskrypcji są formularzami POST, nie odnośnikami
 * (uzasadnienie w security.yaml i w PanelController::subscribe). Opakowanie ma
 * zniknąć z układu - na ekranie nadal jest to pojedynczy przycisk.
 */
.logout-form,
.subscribe-form {
    display: flex;
    margin: 0;
}

/* Rozdziela w karcie to, co odwracalne, od tego, co nie. */
.card__rule {
    width: 100%;
    height: 0;
    margin: var(--s-1) 0;
    border: 0;
    border-top: 1px solid var(--c-line);
}

.plain-list {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    max-width: 36rem;
    margin: 0;
    padding-left: var(--s-4);
    font-size: var(--fs-14);
    line-height: var(--lh-normal);
    color: var(--c-ink-700);
}

.data {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: baseline;
    gap: var(--s-2) var(--s-4);
    margin: 0;
}

.data__term {
    font-size: var(--fs-12);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--c-ink-500);
}

.data__value {
    margin: 0;
    font-size: var(--fs-16);
    overflow-wrap: anywhere;
}

/* Pytania stoją na <details>, więc rozwijają się bez JavaScriptu - tak samo
   jak zakładki scenariuszy na landingu i menu konta w nagłówku. */
.faq {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
}

.faq__item {
    background: var(--c-surface);
    border: 1px solid var(--c-line);
    border-radius: var(--r-3);
}

.faq__question {
    min-height: var(--tap-min);
    padding: var(--s-3) var(--s-4);
    display: flex;
    align-items: center;
    gap: var(--s-3);
    font-size: var(--fs-16);
    font-weight: var(--fw-medium);
    line-height: var(--lh-snug);
    cursor: pointer;
    /* Zdejmuje trójkąt systemowy: wygląda inaczej w każdej przeglądarce,
       a rysujemy własny znacznik jedną grubością kreski. */
    list-style: none;
}

.faq__question::-webkit-details-marker {
    display: none;
}

/* Plus, który po otwarciu zostaje minusem. Dwie kreski z gradientów,
   bez dodatkowego elementu i bez pliku ikony. */
.faq__question::after {
    content: "";
    flex: none;
    margin-left: auto;
    width: 12px;
    height: 12px;
    background:
        linear-gradient(var(--c-ink-700) 0 0) center / 12px 1.5px no-repeat,
        linear-gradient(var(--c-ink-700) 0 0) center / 1.5px 12px no-repeat;
}

.faq__item[open] .faq__question::after {
    background: linear-gradient(var(--c-ink-700) 0 0) center / 12px 1.5px no-repeat;
}

.faq__answer {
    max-width: 36rem;
    padding: 0 var(--s-4) var(--s-4);
    font-size: var(--fs-14);
    line-height: var(--lh-normal);
    color: var(--c-ink-700);
}

.faq__answer p {
    margin: 0 0 var(--s-3);
}

.faq__answer p:last-child {
    margin-bottom: 0;
}

.faq__answer ul {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    margin: 0;
    padding-left: var(--s-4);
}

.faq__answer strong {
    color: var(--c-ink-900);
}

/*
 * Ranking nośników. Słupek odniesiony do najlepszego wyniku, nie do sumy:
 * pytanie brzmi „który pracuje lepiej od którego", a nie „jaki ma udział".
 * Stąd też brak wykresu kołowego i brak procentów.
 */
.ranking {
    display: flex;
    flex-direction: column;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* Siatka, nie flex: trzy kolumny znaczą, że słupki zaczynają się w jednej linii
   i liczby kończą w jednej linii, niezależnie od długości nazwy. Przy flexie
   każdy wiersz startował gdzie indziej i porównanie oczami przestawało działać -
   a to jedyne, po co ten ranking istnieje. */
.ranking__item {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(64px, 240px) 4ch;
    align-items: center;
    gap: var(--s-3);
    min-height: var(--tap-min);
    border-bottom: 1px solid var(--c-line);
}

.ranking__item:last-child {
    border-bottom: 0;
}

.ranking__name {
    min-width: 0;
    align-self: stretch;
    display: flex;
    align-items: center;
    font-size: var(--fs-14);
    color: var(--c-ink-900);
    /* Podkreślenie zostaje - dziesięć pozycji to dziesięć odnośników i muszą
       być rozpoznawalne - ale w kolorze linii, nie pisma. */
    text-decoration-color: var(--c-line-strong);
    text-underline-offset: 3px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ranking__track {
    height: 6px;
    border-radius: var(--r-full);
    background: var(--c-surface-sunken);
    overflow: hidden;
}

.ranking__bar {
    display: block;
    width: var(--width);
    height: 100%;
    border-radius: inherit;
    background: var(--c-accent);
}

.ranking__value {
    text-align: right;
    font-family: var(--font-mono);
    font-size: var(--fs-12);
    color: var(--c-ink-700);
}

/* ---------- Połączenie z kasą: stan agenta i odebrana sprzedaż ---------- */

.pos-tills,
.pos-docs {
    display: flex;
    flex-direction: column;
    margin: 0;
    padding: 0;
    list-style: none;
}

.pos-till {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
    padding: var(--s-3) 0;
    border-bottom: 1px solid var(--c-line);
}

.pos-till:last-child {
    border-bottom: 0;
}

.pos-till__head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--s-2);
}

.pos-till__name {
    font-size: var(--fs-16);
    color: var(--c-ink-900);
}

.pos-till__venue {
    font-size: var(--fs-14);
    color: var(--c-ink-500);
}

.pos-till__state {
    margin: 0;
    font-size: var(--fs-14);
    color: var(--c-ink-700);
}

/* Cisza agenta jest jedyną rzeczą na tym ekranie, która wymaga reakcji, więc
   jest jedyną, która ma kolor. Ostrzeżenie, nie błąd: wyłączony na noc komputer
   przy kasie to normalna praca lokalu, a nie awaria. */
.pos-till--silent .pos-till__state {
    color: var(--c-warn);
}

/*
 * Wiersz sprzedaży: dwa rzędy w wąskiej kolumnie, jeden w szerokiej.
 *
 * Siatka, nie flex, i to z tego samego powodu co przy rankingu: kwoty mają
 * kończyć się w jednej linii, żeby dały się porównać oczami. Przy flexie każda
 * zaczynałaby się gdzie indziej.
 */
.pos-doc {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
        "number amount"
        "meta   time";
    align-items: baseline;
    gap: var(--s-1) var(--s-3);
    padding: var(--s-2) 0;
    border-bottom: 1px solid var(--c-line);
}

.pos-doc:last-child {
    border-bottom: 0;
}

.pos-doc__number {
    grid-area: number;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-family: var(--font-mono);
    font-size: var(--fs-14);
    color: var(--c-ink-900);
}

.pos-doc__venue {
    grid-area: meta;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

.pos-doc__time {
    grid-area: time;
    text-align: right;
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

.pos-doc__amount {
    grid-area: amount;
    text-align: right;
    font-variant-numeric: tabular-nums;
    font-size: var(--fs-16);
    font-weight: var(--fw-medium);
    color: var(--c-ink-900);
}

@container (min-width: 560px) {
    .pos-doc {
        grid-template-columns: 5.5rem minmax(0, 10rem) minmax(0, 1fr) auto;
        grid-template-areas: "time number meta amount";
        align-items: center;
    }

    .pos-doc__time {
        text-align: left;
    }
}

/* Saldo punktowe na karcie gościa. Zastępuje rząd kropek, którego przy progu
   liczonym w setkach nie da się narysować.

   Dopisany przodek NIE jest ozdobnikiem: `.public-page p` ustawia rozmiar
   i kolor każdego akapitu w środku i ma wagę 0,1,1, więc samotna klasa (0,1,0)
   przegrywa. Bez tego saldo renderowało się szesnastopunktowym pismem w kolorze
   zwykłego zdania, a próg obok - w spanie, którego tamta reguła nie dotyczy -
   był od niego WIĘKSZY. Złapane wyliczonym stylem w przeglądarce, nie okiem;
   sąsiednie reguły w tym pliku mają ten przodek od 28 sierpnia 2026, ta go nie
   miała. */
.public-page .loyalty-card__balance {
    margin: 0;
    font-size: var(--fs-44);
    font-weight: var(--fw-semibold);
    line-height: var(--lh-tight);
    font-variant-numeric: tabular-nums;
    color: var(--loyalty-accent, var(--c-accent));
}

.public-page .loyalty-card__balance-of {
    font-size: var(--fs-22);
    color: var(--c-ink-500);
}

/* Znaczek kafelka lojalnościowego: rząd pieczątek, ostatnia pusta przy
   zbieraniu i pełna przy wydaniu nagrody. Rysowany, a nie ikonką, bo cały
   ten rząd kafelków jest rysowany. */
.purpose__stamps {
    display: flex;
    gap: 3px;
}

.purpose__dot {
    width: 7px;
    height: 7px;
    border-radius: var(--r-full);
    background: var(--c-ink-700);
}

.purpose__dot--off {
    background: none;
    box-shadow: inset 0 0 0 1px var(--c-line-strong);
}

/* ---------- Panel lojalnościówki: lokale i programy ---------- */

/* Wiersz lokalu ma dwa działania obok siebie, a nie jedno - ekran przy kasie
   otwiera się codziennie, ustawienia raz na kwartał. */
.group__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-2);
}

/* Lokal ponad limit planu. Wyciszony, ale NIE ukryty: w środku są cudze
   pieczątki i właściciel ma widzieć, że czekają. */
.group--frozen .group__description strong {
    color: var(--c-ink-500);
}

/* Liczba kart do wyzerowania. Jedyne zdanie na tamtym ekranie, przy którym
   człowiek ma się zatrzymać, więc jedyne w tym rozmiarze. */
.mode-change__count {
    margin: 0;
    font-size: var(--fs-22);
    line-height: var(--lh-snug);
    color: var(--c-ink-900);
    text-wrap: pretty;
}

/* ---------- Ekran przy kasie: karta gościa, który przyłożył telefon ---------- */

/*
 * Jedyne miejsce w panelu, w którym wychodzimy poza --fs-34, i jest to wyjątek
 * świadomy, nie przeoczenie. DESIGN.md ogranicza rozmiary wyświetlaczowe do
 * landingu, bo panel czyta się z pięćdziesięciu centymetrów, pochylonym nad
 * laptopem. Ten ekran ogląda obsługa **z odległości ramienia i pod kątem**,
 * stojąc przy ladzie - dokładnie tak samo jak ekran wydania nagrody, który
 * z tej samej racji używa --fs-44.
 */
.till {
    /* Stała wysokość, żeby ekran nie skakał przy każdym przyłożeniu. Karta
       pojawia się i znika co kilka minut przez cały dzień; układ, który się
       przy tym przestawia, męczy bardziej niż pusta przestrzeń. */
    min-height: 320px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: var(--s-3);
}

.till__waiting,
.till__guest {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s-2);
}

/* Pełna szerokość, żeby baner nagrody miał się do czego rozciągnąć. Kolumna
   z `align-items: center` kurczy dzieci do ich treści, więc `width: 100%`
   na samym banerze nie wystarcza - nie ma od czego liczyć tych stu procent. */
.till__guest {
    align-self: stretch;
}

.till__waiting-lead {
    margin: 0;
    font-size: var(--fs-22);
    color: var(--c-ink-700);
}

/* Kropka mówi „ekran żyje", a nie „coś się ładuje". Bez niej stan spoczynkowy
   jest nie do odróżnienia od zawieszonej karty przeglądarki. */
.till__pulse {
    width: 10px;
    height: 10px;
    border-radius: var(--r-full);
    background: var(--c-ok);
    animation: till-pulse 2.4s ease-in-out infinite;
}

@keyframes till-pulse {
    50% {
        opacity: 0.25;
    }
}

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

.till__label {
    margin: 0;
    font-size: var(--fs-12);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--c-ink-500);
}

/*
    Kod karty przy etykiecie. Monospace, bo to napis do przeczytania znak po
    znaku na głos - proporcjonalny krój myli 0 z 8 przy dwóch metrach od lady.
*/
.till__code {
    margin-left: var(--s-2);
    padding: 0 var(--s-2);
    border-radius: var(--r-1);
    background: var(--c-surface-sunken);
    color: var(--c-ink-700);
    font-family: var(--font-mono);
    letter-spacing: var(--tracking-wide);
}

.till__code:empty {
    display: none;
}

.till__count {
    margin: 0;
    font-size: var(--fs-44);
    font-weight: var(--fw-semibold);
    line-height: var(--lh-tight);
    font-variant-numeric: tabular-nums;
    color: var(--c-ink-900);
}

.till__of {
    font-size: var(--fs-28);
    color: var(--c-ink-500);
}

.till__missing {
    margin: 0;
    font-size: var(--fs-16);
    color: var(--c-ink-700);
}

/* Nagroda przejmuje ekran, bo to jedyny stan wymagający od obsługi czynności.
   Ciemne tło, a nie kolor akcentu: ma się różnić od reszty panelu na tyle,
   żeby dało się to zobaczyć kątem oka, nie czytając. */
.till__reward {
    width: 100%;
    margin: var(--s-2) 0 0;
    padding: var(--s-5) var(--s-4);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s-3);
    border-radius: var(--r-4);
    background: var(--c-ink-900);
    color: var(--c-surface);
}

.till__reward .till__label {
    color: var(--c-surface);
    opacity: 0.7;
}

.till__reward-name {
    margin: 0;
    font-size: var(--fs-34);
    font-weight: var(--fw-semibold);
    line-height: var(--lh-tight);
    text-wrap: balance;
}

.till__reward form {
    width: 100%;
    display: flex;
    justify-content: center;
}

/* Cel dotykowy grubo ponad minimum: klika się go przy ladzie, w pośpiechu,
   często nie patrząc na ekran.

   Kolory ODWRÓCONE, i to nie jest ozdobnik: `.button` maluje się ciemnym tłem
   na jasnym panelu, a ten stoi na ciemnym banerze nagrody - czyli ciemne na
   ciemnym. Przy pierwszym zrzucie ekranu widać było sam napis, bez przycisku,
   na jedynym elemencie tego ekranu, który obsługa ma nacisnąć. Żaden test tego
   nie widzi: znacznik jest poprawny, kontrast nie. */
.till__reward .till__action {
    min-height: 56px;
    padding-inline: var(--s-6);
    font-size: var(--fs-18);
    background: var(--c-surface);
    color: var(--c-ink-900);
}

@media (hover: hover) and (pointer: fine) {
    .till__reward .till__action:hover {
        background: var(--c-surface-sunken);
    }
}

/* Reguła naliczania powiedziana wprost, na ekranie, przy karcie której
   dotyczy. Akcent, bo jest to jedyne zdanie na tym ekranie, które mówi
   o czymś, co dopiero się wydarzy. */
.till__pairing {
    margin: 0;
    font-size: var(--fs-14);
    color: var(--c-accent);
}

/* Naliczony zakup: stan dokonany, więc kolor potwierdzenia, nie akcentu. */
.till__awarded {
    margin: 0;
    font-size: var(--fs-14);
    color: var(--c-ok);
}

.till__expiry {
    margin: 0;
    font-size: var(--fs-12);
    font-variant-numeric: tabular-nums;
    color: var(--c-ink-500);
}

/*
 * Lont: ta sama liczba co wyżej, tylko widziana kątem oka.
 *
 * Kurczy się TRANSFORMACJĄ, nie szerokością, i to nie jest wybór estetyczny.
 * Animowana `width` każe przeglądarce policzyć układ strony w każdej klatce,
 * a ten pasek biegnie na komputerze przy kasie przez cały dzień, obok karty
 * przeglądarki zostawionej otwartej na tygodnie.
 */
.till__fuse {
    /* Szerokość wpisana, bo `.till__guest` jest kolumną z `align-items: center`
       i kurczy dzieci do ich treści - pusty element dostałby zero pikseli
       i zniknąłby bez śladu. Wąski celowo: to jest przypis do liczby nad nim,
       a nie pasek postępu na całą kartę. */
    width: min(220px, 100%);
    overflow: hidden;
    height: 4px;
    border-radius: 999px;
    background: var(--c-line);
}

.till__fuse-burn {
    display: block;
    height: 100%;
    border-radius: inherit;
    background: var(--c-accent);
    /* Od lewej, bo pasek ma UBYWAĆ z prawej strony - tak samo, jak ubywa
       cyfra obok. Skalowanie od środka czytałoby się jak dwa liczniki naraz. */
    transform-origin: left center;
    transition: transform 1s linear;
}

/* Skok do pełna przy nowym przyłożeniu ma być natychmiastowy. Bez tego pasek
   przez sekundę PUCHNIE w stronę pełna, czyli pokazuje ruch odwrotny do
   znaczenia - a dzieje się to dokładnie w chwili, w której obsługa patrzy. */
.till__fuse-burn--reset {
    transition: none;
}

@media (prefers-reduced-motion: reduce) {
    /* Ruch znika, ale długość paska zostaje: niesie stan, a nie ozdobę. */
    .till__fuse-burn {
        transition: none;
    }
}

/* ---------- Landing: strona produktu fizycznego ---------- */

/*
 * Landing prowadzi przedmiot, nie panel. Sekcje same trzymają swoją szerokość,
 * bo takty produktowe idą przez całą szerokość ekranu, a czytane akapity nie.
 */
.landing {
    display: block;
}

.section-heading {
    margin: 0;
    font-size: var(--fs-44);
    line-height: var(--lh-tight);
    letter-spacing: -0.02em;
    font-weight: var(--fw-semibold);
    text-wrap: balance;
}

/*
 * Na landingu nagłówek leży NA ciemnym hero - biały pasek przecinał scenę
 * poziomą krechą. Znak odwracamy filtrem, bo ma kolory zapisane w atrybutach
 * SVG i nie da się ich przejąć przez currentColor.
 */
body:has(.landing) .header {
    position: absolute;
    inset-inline: 0;
    top: 0;
    z-index: 2;
    background: transparent;
    border-bottom-color: transparent;
}

body:has(.landing) .header__brand {
    color: var(--c-stage-ink);
}

body:has(.landing) .header__brand svg {
    filter: invert(1);
}

body:has(.landing) .header__toggle {
    border-color: var(--c-stage-line);
    background: transparent;
}

/* Zalogowany użytkownik widzi landing z paskiem konta. Nazwa firmy jest
   domyślnie w kolorze atramentu, czyli na czarnym hero znika. */
body:has(.landing) .header__company-name {
    color: var(--c-stage-ink);
}

body:has(.landing) .header__company-label {
    color: var(--c-stage-ink-dim);
}

/* ---------- Hero ---------- */

.hero {
    position: relative;
    overflow: hidden;
    background: var(--c-stage);
    color: var(--c-stage-ink);
    padding: var(--s-8) var(--s-4) 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s-6);
    text-align: center;
}

.hero__type {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s-4);
    max-width: 40rem;
    position: relative;
    z-index: 1;
}

.hero__heading {
    margin: 0;
    font-size: var(--fs-84);
    line-height: 1.02;
    letter-spacing: -0.035em;
    font-weight: var(--fw-semibold);
    text-wrap: balance;
}

.hero__lead {
    margin: 0;
    font-size: var(--fs-18);
    line-height: var(--lh-normal);
    color: var(--c-stage-ink-dim);
    text-wrap: pretty;
    max-width: 32rem;
}

.hero__cta {
    margin-top: var(--s-2);
}

.hero__price {
    margin: 0;
    font-size: var(--fs-14);
    color: var(--c-stage-ink-dim);
}

/*
 * Przedmiot jest przycięty dolną krawędzią ekranu. To zaproszenie do przewinięcia,
 * a nie ozdoba: pełną naklejkę widać dopiero w scenie poniżej.
 */
.hero__object {
    width: min(78vw, 30rem);
    margin-bottom: calc(var(--s-8) * -1.5);
    filter: drop-shadow(0 40px 60px rgba(0, 0, 0, 0.78));
}

.hero__object svg {
    display: block;
    width: 100%;
    height: auto;
}

@media (min-width: 900px) {
    .hero {
        padding-top: calc(var(--s-8) * 1.6);
        gap: var(--s-7);
    }

    .hero__object {
        width: 26rem;
        margin-bottom: calc(var(--s-8) * -1.9);
    }
}

/* ---------- Scena przewijana scrollem ---------- */

/*
 * `--progress` ustawia kontroler Stimulusa (assets/controllers/stage_controller.js).
 * Rejestracja przez @property daje dwie rzeczy naraz: liczbę, na której działa
 * calc(), i wartość początkową 1 - bez JavaScriptu scena stoi na stanie końcowym.
 */
@property --progress {
    syntax: '<number>';
    inherits: true;
    initial-value: 1;
}

.stage {
    --progress: 1;

    /* Takty: naklejka osiada, telefon dojeżdża, fala, podmiana celu. */
    --t-settle: clamp(0, calc(var(--progress) / 0.28), 1);
    --t-phone: clamp(0, calc((var(--progress) - 0.26) / 0.30), 1);
    --t-tap: clamp(0, calc((var(--progress) - 0.52) / 0.14), 1);

    /* Podpisy - każdy wchodzi i wychodzi na własnym odcinku. */
    --cap-1: calc(1 - clamp(0, calc((var(--progress) - 0.22) / 0.10), 1));
    --cap-2: calc(
        clamp(0, calc((var(--progress) - 0.24) / 0.10), 1) *
        (1 - clamp(0, calc((var(--progress) - 0.58) / 0.10), 1))
    );
    --cap-3: clamp(0, calc((var(--progress) - 0.60) / 0.10), 1);

    /* Trzy cele na ekranie telefonu. */
    --dst-1: calc(1 - clamp(0, calc((var(--progress) - 0.66) / 0.08), 1));
    --dst-2: calc(
        clamp(0, calc((var(--progress) - 0.68) / 0.08), 1) *
        (1 - clamp(0, calc((var(--progress) - 0.84) / 0.08), 1))
    );
    --dst-3: clamp(0, calc((var(--progress) - 0.86) / 0.08), 1);

    position: relative;
    height: 340vh;
    background: var(--c-stage);
    color: var(--c-stage-ink);
}

.stage__viewport {
    position: sticky;
    top: 0;
    height: 100vh;
    height: 100dvh;
    display: grid;
    grid-template-rows: 1fr auto;
    align-items: center;
    overflow: hidden;
}

.stage__frame {
    position: relative;
    width: min(92vw, 34rem);
    max-height: 56vh;
    aspect-ratio: 1 / 1;
    margin: 0 auto;
    align-self: center;
}

/* Powierzchnia, na której leży naklejka - daje jej kontakt i cień. */
.stage__floor {
    position: absolute;
    left: 50%;
    translate: -50%;
    width: 100vw;
    top: 64%;
    bottom: -40%;
    background: linear-gradient(
        to bottom,
        rgba(255, 255, 255, 0.055),
        rgba(255, 255, 255, 0.014) 45%,
        transparent
    );
    border-top: 1px solid var(--c-stage-line);
    -webkit-mask-image: linear-gradient(to right, transparent, #000 16%, #000 84%, transparent);
    mask-image: linear-gradient(to right, transparent, #000 16%, #000 84%, transparent);
}

.stage__sticker {
    position: absolute;
    left: 50%;
    top: 46%;
    width: 46%;
    translate: -50% -50%;
    scale: calc(0.88 + var(--t-settle) * 0.12);
    opacity: calc(0.25 + var(--t-settle) * 0.75);
    filter: drop-shadow(0 18px 26px rgba(0, 0, 0, 0.6));
}

.stage__sticker svg {
    display: block;
    width: 100%;
    height: auto;
}

/* Fala przyłożenia - jedyne miejsce na stronie, gdzie pojawia się sygnał. */
.stage__wave {
    position: absolute;
    left: 50%;
    top: 46%;
    width: 46%;
    aspect-ratio: 1 / 1;
    translate: -50% -50%;
    border-radius: var(--r-full);
    border: 2px solid var(--c-signal);
    scale: calc(0.6 + var(--t-tap) * 1.5);
    opacity: clamp(0, calc(var(--t-tap) * (1 - var(--t-tap)) * 4), 1);
    pointer-events: none;
}

.stage__wave--late {
    scale: calc(0.6 + var(--t-tap) * 2.3);
    opacity: clamp(0, calc(var(--t-tap) * (1 - var(--t-tap)) * 2.4), 1);
    border-width: 1px;
}

/* Telefon dojeżdża z prawego dołu i zatrzymuje się przy naklejce. */
.stage__phone {
    position: absolute;
    right: 4%;
    top: 30%;
    width: 33%;
    aspect-ratio: 1 / 2.02;
    border-radius: 14% / 7%;
    background: linear-gradient(160deg, #35352f 0%, #17171५ 40%, #0f0f0e 100%);
    background: linear-gradient(160deg, #35352f 0%, #171715 40%, #0f0f0e 100%);
    box-shadow:
        0 0 0 1px rgba(255, 255, 255, 0.14),
        0 30px 50px rgba(0, 0, 0, 0.65);
    padding: 3.5%;
    translate: calc((1 - var(--t-phone)) * 34%) calc((1 - var(--t-phone)) * 52%);
    scale: calc(0.82 + var(--t-phone) * 0.18);
    opacity: var(--t-phone);
}

.stage__screen {
    position: relative;
    width: 100%;
    height: 100%;
    border-radius: 11% / 5.5%;
    background: var(--c-bg);
    overflow: hidden;
    display: flex;
    flex-direction: column;
}

.stage__bar {
    flex: none;
    padding: 8% 8% 4%;
}

.stage__bar span {
    display: block;
    height: 0.5rem;
    border-radius: var(--r-full);
    background: var(--c-line);
}

.stage__destination {
    position: absolute;
    inset: 15% 8% 8%;
    display: flex;
    flex-direction: column;
    gap: 5%;
    align-items: flex-start;
    color: var(--c-ink-900);
    text-align: left;
}

.stage__destination--1 { opacity: var(--dst-1); }
.stage__destination--2 { opacity: var(--dst-2); }
.stage__destination--3 { opacity: var(--dst-3); }

.stage__url {
    font-family: var(--font-mono);
    font-size: clamp(8px, 1.4vw, 10px);
    color: var(--c-ink-500);
    word-break: break-all;
}

.stage__title {
    font-size: clamp(11px, 2vw, 14px);
    line-height: var(--lh-tight);
    font-weight: var(--fw-semibold);
    text-wrap: balance;
}

.stage__stars {
    font-size: clamp(11px, 2vw, 14px);
    color: var(--c-warn);
    letter-spacing: 0.1em;
}

.stage__rows {
    display: flex;
    flex-direction: column;
    gap: 22%;
    width: 100%;
}

.stage__rows i {
    display: block;
    height: 0.35rem;
    border-radius: var(--r-full);
    background: var(--c-line);
}

.stage__rows i:nth-child(2) { width: 78%; }
.stage__rows i:nth-child(3) { width: 90%; }
.stage__rows i:nth-child(4) { width: 62%; }

.stage__field {
    display: block;
    width: 100%;
    height: 1.1rem;
    border-radius: var(--r-1);
    border: 1px solid var(--c-line-strong);
    background: var(--c-surface);
}

.stage__action {
    margin-top: 2%;
    padding: 4% 10%;
    border-radius: var(--r-full);
    background: var(--c-ink-900);
    color: var(--c-bg);
    font-size: clamp(9px, 1.6vw, 11px);
    font-weight: var(--fw-medium);
}

/* ---------- Podpisy taktów ---------- */

.stage__captions {
    position: relative;
    padding: var(--s-5) var(--s-4) var(--s-7);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s-4);
}

.stage__heading {
    position: relative;
    margin: 0;
    width: min(100%, 42rem);
    min-height: 7rem;
    font-size: var(--fs-16);
    font-weight: var(--fw-regular);
    line-height: var(--lh-normal);
    text-align: center;
}

.stage__caption {
    position: absolute;
    inset: 0;
    color: var(--c-stage-ink-dim);
    text-wrap: pretty;
}

.stage__caption em {
    display: block;
    font-style: normal;
    font-size: var(--fs-28);
    line-height: var(--lh-tight);
    letter-spacing: -0.02em;
    font-weight: var(--fw-semibold);
    color: var(--c-stage-ink);
    margin-bottom: var(--s-2);
    text-wrap: balance;
}

.stage__caption--1 { opacity: var(--cap-1); }
.stage__caption--2 { opacity: var(--cap-2); }
.stage__caption--3 { opacity: var(--cap-3); }

.stage__ticks {
    display: flex;
    gap: var(--s-2);
    margin: 0;
    padding: 0;
    list-style: none;
}

.stage__tick {
    width: 2.5rem;
    height: 2px;
    border-radius: var(--r-full);
    background: var(--c-stage-line);
}

.stage__tick--1 { background: color-mix(in oklab, var(--c-stage-ink) calc(var(--cap-1) * 100%), var(--c-stage-line)); }
.stage__tick--2 { background: color-mix(in oklab, var(--c-stage-ink) calc(var(--cap-2) * 100%), var(--c-stage-line)); }
.stage__tick--3 { background: color-mix(in oklab, var(--c-stage-ink) calc(var(--cap-3) * 100%), var(--c-stage-line)); }

@media (max-width: 719px) {
    .stage__phone {
        width: 40%;
        right: 1%;
        top: 26%;
    }

    .stage__sticker,
    .stage__wave {
        width: 52%;
        left: 42%;
    }
}

@media (min-width: 900px) {
    .stage__frame {
        width: min(60vw, 38rem);
    }

    .stage__heading {
        font-size: var(--fs-18);
    }

    .stage__caption em {
        font-size: var(--fs-34);
    }
}

/*
 * Ograniczenie ruchu: kontroler i tak ustawia --progress na 1, ale gdyby JS nie
 * wystartował, ta reguła gwarantuje stan końcowy zamiast pustej sceny.
 */
@media (prefers-reduced-motion: reduce) {
    .stage {
        --progress: 1;
        height: auto;
    }

    .stage__viewport {
        position: static;
        height: auto;
        padding-block: var(--s-8);
    }
}

/* ---------- Porównanie ---------- */

.split {
    max-width: var(--landing-max);
    margin: 0 auto;
    padding: var(--s-8) var(--s-4);
    display: flex;
    flex-direction: column;
    gap: var(--s-6);
}

.split__pair {
    display: grid;
    gap: var(--s-4);
}

@media (min-width: 720px) {
    .split__pair {
        grid-template-columns: 1fr 1fr;
        gap: var(--s-5);
    }
}

.split__side {
    padding: var(--s-5);
    border: 1px solid var(--c-line);
    border-radius: var(--r-3);
    background: var(--c-surface);
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
}

.split__side--ours {
    background: var(--c-stage);
    border-color: var(--c-stage);
    color: var(--c-stage-ink);
}

.split__label {
    margin: 0;
    font-size: var(--fs-12);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--c-ink-500);
}

.split__side--ours .split__label {
    color: var(--c-signal);
}

.split__claim {
    margin: 0;
    font-size: var(--fs-28);
    line-height: var(--lh-tight);
    letter-spacing: -0.02em;
    font-weight: var(--fw-semibold);
    text-wrap: balance;
}

.split__body {
    margin: 0;
    font-size: var(--fs-16);
    line-height: var(--lh-normal);
    color: var(--c-ink-700);
    text-wrap: pretty;
}

.split__side--ours .split__body {
    color: var(--c-stage-ink-dim);
}

/* ---------- Kroki ---------- */

.steps {
    max-width: var(--landing-max);
    margin: 0 auto;
    padding: var(--s-7) var(--s-4);
    display: flex;
    flex-direction: column;
    gap: var(--s-6);
}

.steps__list {
    display: grid;
    gap: var(--s-5);
    margin: 0;
    padding: 0;
    list-style: none;
}

@media (min-width: 720px) {
    .steps__list {
        grid-template-columns: repeat(4, 1fr);
        gap: var(--s-4);
    }
}

.steps__item {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    padding-top: var(--s-3);
    border-top: 1px solid var(--c-line-strong);
}

.steps__number {
    font-family: var(--font-mono);
    font-size: var(--fs-12);
    letter-spacing: var(--tracking-wide);
    color: var(--c-signal-ink);
}

.steps__text {
    margin: 0;
    font-size: var(--fs-16);
    line-height: var(--lh-snug);
    color: var(--c-ink-700);
    text-wrap: pretty;
}

/* ---------- Nośniki (scenariusze) ---------- */

.scenarios {
    max-width: var(--landing-max);
    margin: 0 auto;
    padding: var(--s-7) var(--s-4);
    display: flex;
    flex-direction: column;
    gap: var(--s-5);
}

.scenarios__header {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
}

.scenarios__description {
    margin: 0;
    font-size: var(--fs-16);
    line-height: var(--lh-normal);
    color: var(--c-ink-700);
    max-width: 36rem;
    text-wrap: pretty;
}

.scenarios__tabs {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-2);
}

.scenarios__tab {
    position: relative;
    padding: var(--s-2) var(--s-4);
    min-height: var(--tap-min);
    display: inline-flex;
    align-items: center;
    border: 1px solid var(--c-line-strong);
    border-radius: var(--r-full);
    font-size: var(--fs-14);
    font-weight: var(--fw-medium);
    color: var(--c-ink-700);
    cursor: pointer;
    background: var(--c-surface);
    transition: background var(--dur-state) var(--ease-out), color var(--dur-state) var(--ease-out);
}

.scenarios__tab input {
    position: absolute;
    opacity: 0;
    pointer-events: none;
}

.scenarios__tab:active {
    scale: 0.98;
}

.scenarios__tab:has(input:checked) {
    background: var(--c-ink-900);
    border-color: var(--c-ink-900);
    color: var(--c-bg);
}

.scenarios__tab:has(input:focus-visible) {
    outline: var(--focus-ring);
    outline-offset: var(--focus-offset);
}

.scenarios__panels {
    position: relative;
}

.scenarios__panel {
    display: none;
    gap: var(--s-4);
    align-items: center;
}

@media (min-width: 720px) {
    .scenarios__panel {
        grid-template-columns: 1.1fr 1fr;
        gap: var(--s-6);
    }
}

.scenarios__scene {
    aspect-ratio: 16 / 10;
    border-radius: var(--r-3);
    background: var(--c-stage);
    display: grid;
    place-items: center;
    overflow: hidden;
}

.scenarios__video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.scenarios__mark {
    width: 42%;
    opacity: 0.9;
}

.scenarios__mark svg {
    display: block;
    width: 100%;
    height: auto;
}

.scenarios__content {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
}

.scenarios__panel-heading {
    margin: 0;
    font-size: var(--fs-28);
    line-height: var(--lh-tight);
    letter-spacing: -0.02em;
    font-weight: var(--fw-semibold);
}

.scenarios__content p {
    margin: 0;
    font-size: var(--fs-16);
    line-height: var(--lh-normal);
    color: var(--c-ink-700);
    text-wrap: pretty;
}

.scenarios__target {
    font-size: var(--fs-12);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--c-ink-500);
}

/*
 * Powiązanie zakładki z panelem po pozycji, nie po nazwie klucza - dzięki temu
 * zmiana kluczy w PHP nie wymaga ruszania CSS-u. Obsługuje do czterech
 * scenariuszy; piąty wymaga dopisania jednej reguły.
 */
.scenarios:has(.scenarios__tabs > :nth-child(1) input:checked) .scenarios__panel:nth-child(1),
.scenarios:has(.scenarios__tabs > :nth-child(2) input:checked) .scenarios__panel:nth-child(2),
.scenarios:has(.scenarios__tabs > :nth-child(3) input:checked) .scenarios__panel:nth-child(3),
.scenarios:has(.scenarios__tabs > :nth-child(4) input:checked) .scenarios__panel:nth-child(4) {
    display: grid;
}

/* Bez obsługi :has() widoczny jest pierwszy scenariusz - strona nadal ma treść. */
@supports not selector(:has(*)) {
    .scenarios__panel:first-child {
        display: grid;
    }

    .scenarios__tabs {
        display: none;
    }
}

/* ---------- Dowód: panel ---------- */

.panel-proof {
    max-width: var(--landing-max);
    margin: 0 auto;
    padding: var(--s-7) var(--s-4);
    display: grid;
    gap: var(--s-6);
    align-items: center;
}

@media (min-width: 720px) {
    .panel-proof {
        grid-template-columns: 1fr 1fr;
        gap: var(--s-7);
    }
}

.panel-proof__type {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--s-4);
}

.panel-proof__body {
    margin: 0;
    font-size: var(--fs-16);
    line-height: var(--lh-normal);
    color: var(--c-ink-700);
    text-wrap: pretty;
}

.mock {
    padding: var(--s-5);
    border-radius: var(--r-3);
    background: var(--c-surface);
    border: 1px solid var(--c-line);
    box-shadow: var(--sh-2);
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
}

.mock__row {
    display: flex;
    align-items: center;
    gap: var(--s-2);
}

.mock__code {
    font-family: var(--font-mono);
    font-size: var(--fs-18);
    letter-spacing: 0.08em;
}

.mock__dot {
    width: 0.5rem;
    height: 0.5rem;
    border-radius: var(--r-full);
    background: var(--c-ok);
    margin-left: auto;
}

.mock__state {
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

.mock__label {
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

.mock__field {
    display: flex;
    align-items: center;
    gap: 2px;
    padding: var(--s-3);
    border: 1px solid var(--c-accent);
    border-radius: var(--r-2);
    background: var(--c-surface);
    box-shadow: 0 0 0 3px var(--c-accent-weak);
}

.mock__value {
    font-size: var(--fs-16);
}

.mock__caret {
    width: 1px;
    height: 1.1em;
    background: var(--c-ink-900);
}

.mock__save {
    align-self: flex-start;
    padding: var(--s-2) var(--s-5);
    border-radius: var(--r-full);
    background: var(--c-ink-900);
    color: var(--c-bg);
    font-size: var(--fs-14);
    font-weight: var(--fw-medium);
}

.mock__note {
    margin: 0;
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

/* ---------- Dane techniczne ---------- */

.specs {
    max-width: var(--landing-max);
    margin: 0 auto;
    padding: var(--s-7) var(--s-4);
    display: flex;
    flex-direction: column;
    gap: var(--s-5);
}

.specs__list {
    margin: 0;
    display: flex;
    flex-direction: column;
}

.specs__row {
    display: grid;
    gap: var(--s-1);
    padding: var(--s-4) 0;
    border-top: 1px solid var(--c-line);
}

@media (min-width: 720px) {
    .specs__row {
        grid-template-columns: 14rem 1fr;
        gap: var(--s-5);
        align-items: baseline;
    }
}

.specs__term {
    font-size: var(--fs-12);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--c-ink-500);
}

.specs__value {
    margin: 0;
    max-width: 36rem;
    font-size: var(--fs-16);
    line-height: var(--lh-snug);
    color: var(--c-ink-900);
    text-wrap: pretty;
}

/* ---------- Katalog nośników ---------- */

/*
 * Katalog świadomie NIE używa powłoki `.landing`. Ta klasa przełącza nagłówek
 * w tryb „leży na ciemnym hero" (`body:has(.landing) .header`), a katalog stoi
 * na jasnym tle - pod tamtą regułą znak marki wychodził biały na białym.
 */
.catalog {
    max-width: var(--landing-max);
    margin: 0 auto;
    padding: var(--s-6) var(--s-4) var(--s-8);
}

.catalog__inner {
    display: flex;
    flex-direction: column;
    gap: var(--s-6);
}

.catalog__header {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    align-items: start;
}

.catalog__lead {
    margin: 0;
    max-width: 36rem;
    font-size: var(--fs-16);
    line-height: var(--lh-normal);
    color: var(--c-ink-700);
    text-wrap: pretty;
}

.catalog__grid {
    display: grid;
    gap: var(--s-4);
}

@media (min-width: 720px) {
    .catalog__grid {
        grid-template-columns: repeat(2, 1fr);
    }
}

/*
 * Kafel produktu. `grid-template-rows` z `1fr` na treści zrównuje cenę
 * i przycisk w jednej linii we wszystkich kafelkach niezależnie od długości
 * opisu - inaczej „Zamawiam" skakałoby w pionie od kafla do kafla.
 */
.product {
    display: grid;
    grid-template-rows: auto 1fr auto;
    border: 1px solid var(--c-line);
    border-radius: var(--r-3);
    background: var(--c-surface);
    overflow: hidden;
}

/*
 * Ramka rysunku ma stały stosunek boków, żeby podmiana znaku zastępczego na
 * zdjęcie produktu nie ruszyła układu siatki. Zdjęć nie ma ani jednego
 * (PRODUCT.md, „Evidence on Hand") - to miejsce na nie, nie obietnica.
 */
.product__scene {
    aspect-ratio: 16 / 10;
    background: var(--c-stage);
    display: grid;
    place-items: center;
}

.product__mark {
    width: 38%;
    opacity: 0.9;
}

.product__mark svg {
    display: block;
    width: 100%;
    height: auto;
}

.product__body {
    padding: var(--s-4);
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
}

.product__name {
    margin: 0;
    font-size: var(--fs-22);
    line-height: var(--lh-tight);
    letter-spacing: -0.02em;
    font-weight: var(--fw-semibold);
}

.product__lead {
    margin: 0;
    font-size: var(--fs-16);
    line-height: var(--lh-normal);
    color: var(--c-ink-700);
    text-wrap: pretty;
}

.product__specs {
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
}

.product__spec {
    display: grid;
    gap: var(--s-1);
}

.product__spec dt {
    font-size: var(--fs-12);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--c-ink-500);
}

.product__spec dd {
    margin: 0;
    font-size: var(--fs-14);
    line-height: var(--lh-normal);
    color: var(--c-ink-700);
    text-wrap: pretty;
}

.product__buy {
    padding: var(--s-4);
    border-top: 1px solid var(--c-line);
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-3);
    align-items: center;
    justify-content: space-between;
}

.product__price {
    font-size: var(--fs-18);
    font-weight: var(--fw-semibold);
    letter-spacing: -0.01em;
}

.product__unit {
    font-size: var(--fs-14);
    font-weight: var(--fw-regular);
    color: var(--c-ink-500);
}

.catalog__common {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
}

.catalog__common-heading {
    margin: 0;
    font-size: var(--fs-22);
    line-height: var(--lh-tight);
    font-weight: var(--fw-semibold);
}

/* ---------- Zamknięcie ---------- */

.close {
    background: var(--c-stage);
    color: var(--c-stage-ink);
    padding: var(--s-8) var(--s-4);
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: var(--s-4);
}

.close__heading {
    margin: 0;
    font-size: var(--fs-60);
    line-height: var(--lh-tight);
    letter-spacing: -0.03em;
    font-weight: var(--fw-semibold);
}

.close__heading span {
    display: block;
    font-size: var(--fs-16);
    font-weight: var(--fw-regular);
    letter-spacing: 0;
    color: var(--c-stage-ink-dim);
    margin-top: var(--s-2);
}

.close__body {
    margin: 0;
    max-width: 34rem;
    font-size: var(--fs-16);
    line-height: var(--lh-normal);
    color: var(--c-stage-ink-dim);
    text-wrap: pretty;
}

.close__cta {
    margin-top: var(--s-2);
}

/* Zamknięcie landingu z odnośnikiem do logowania. Nazwa mówi „konto", nie
   „stopka", bo stopka jest teraz jedna i wspólna - patrz `.footer`. */
.landing__account {
    padding: var(--s-6) var(--s-4);
    text-align: center;
    font-size: var(--fs-14);
    background: var(--c-stage);
}

.landing__account a {
    color: var(--c-stage-ink-dim);
}

/* ---------- Grupy nośników ---------- */

.groups,
.group-tags {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
}

.groups__list {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    margin: 0;
    padding: 0;
    list-style: none;
}

/*
 * Opis, pod nim akcja - zawsze, niezależnie od szerokości.
 *
 * Wcześniej był to flex z zawijaniem, więc o układzie kafla decydowała długość
 * adresu docelowego: przy krótkim przycisk stawał obok opisu, przy długim
 * spadał pod niego. W jednym rzędzie listy wychodziły z tego dwa różne kafle.
 */
.group {
    display: grid;
    justify-items: start;
    gap: var(--s-3);
    padding: var(--s-3) var(--s-4);
    background: var(--c-surface);
    border: 1px solid var(--c-line);
    border-radius: var(--r-2);
}

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

.group__count {
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

.group__target {
    font-size: var(--fs-12);
    color: var(--c-ink-500);
    font-family: var(--font-mono);
    overflow-wrap: anywhere;
}

.tag__group {
    padding: 1px var(--s-2);
    border-radius: var(--r-1);
    background: var(--c-surface-sunken);
    font-size: var(--fs-12);
    color: var(--c-ink-700);
}

.group-tags__list {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-2) var(--s-4);
    margin: 0;
    padding: 0;
    list-style: none;
    font-family: var(--font-mono);
    font-size: var(--fs-14);
}

.group-tags__own {
    margin-left: var(--s-1);
    font-family: var(--font-sans);
    font-size: var(--fs-12);
    color: var(--c-warn);
}

/* ---------- Powiadomienia (baner w stylu iOS) ---------- */

.notifications {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 100;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s-2);
    /* Notch i pasek statusu na iPhonie. */
    padding: calc(env(safe-area-inset-top, 0px) + var(--s-3)) var(--s-3) 0;
    pointer-events: none;
}

.notification {
    pointer-events: auto;
    width: min(100%, 26rem);
    display: flex;
    align-items: flex-start;
    gap: var(--s-3);
    padding: var(--s-3) var(--s-4);
    border-radius: var(--r-4);
    background: color-mix(in oklch, var(--c-surface) 82%, transparent);
    backdrop-filter: blur(20px) saturate(180%);
    border: 1px solid color-mix(in oklch, var(--c-line-strong) 60%, transparent);
    box-shadow: var(--sh-3);
    font-size: var(--fs-14);
    line-height: var(--lh-snug);
    color: var(--c-ink-900);

    /* Wejście, potem samoczynne zniknięcie. Druga animacja ma opóźnienie -
       to ona chowa baner. */
    animation:
        notification-in var(--notification-in) var(--ease-out) both,
        notification-out var(--notification-out) var(--ease-out) var(--notification-life) both;
}

/* Najechanie kursorem albo wejście tabem wstrzymuje chowanie: link w środku
   nie może uciekać w trakcie sięgania po niego. Pauzujemy tylko drugą animację. */
.notification:hover,
.notification:focus-within {
    animation-play-state: running, paused;
}

@keyframes notification-in {
    from {
        /* Nigdy od scale(0): baner zjeżdża spod krawędzi, lekko spłaszczony. */
        transform: translateY(-120%) scale(0.96);
        opacity: 0;
    }
}

@keyframes notification-out {
    to {
        transform: translateY(-120%) scale(0.96);
        opacity: 0;
    }
}

.notification__icon {
    flex: none;
    width: 20px;
    height: 20px;
    margin-top: 1px;
}

.notification__icon svg {
    width: 100%;
    height: 100%;
    display: block;
}

.notification--success .notification__icon {
    color: var(--c-ok);
}

.notification--error {
    background: color-mix(in oklch, var(--c-danger-weak) 88%, transparent);
    border-color: color-mix(in oklch, var(--c-danger) 45%, transparent);
}

.notification--error .notification__icon {
    color: var(--c-danger);
}

/*
 * Rozpoznawanie nośników. Zwykły dymek w tym samym stosie, co reszta - różni
 * się dwiema rzeczami. Barwą ostrzegawczą, bo przez ten czas własne nośniki
 * nie prowadzą tam, gdzie prowadzą klientowi. I tym, że **nie chowa się sam**:
 * niesie stan trwający godzinę, a nie komunikat o zdarzeniu, więc zostaje
 * sama animacja wjazdu.
 */
.notification--identify {
    align-items: center;
    background: color-mix(in oklch, var(--c-warn-weak) 90%, transparent);
    border-color: color-mix(in oklch, var(--c-warn) 55%, transparent);
    z-index: 1;
    animation: notification-in 0ms var(--ease-out) both;
}

/* Wyjście z okna. Wyciszone do rozmiaru zdania obok, ale z pełnym celem
   dotykowym - to jedyne wyjście z rozpoznawania widoczne z każdego ekranu. */
.notification__action {
    flex: none;
    padding: 0 var(--s-3);
    border: 0;
    background: none;
    font: inherit;
    font-weight: var(--fw-medium);
    color: var(--c-ink-900);
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 3px;
    cursor: pointer;
}

.notification__content {
    min-width: 0;
    overflow-wrap: anywhere;
}

.notification__code {
    font-family: var(--font-mono);
    font-weight: var(--fw-medium);
    text-decoration-thickness: 1px;
    text-underline-offset: 2px;
}

/* Bez ruchu: baner po prostu jest, przez ten sam czas, i gaśnie kryciem. */
@media (prefers-reduced-motion: reduce) {
    .notification {
        animation:
            fade-in 160ms ease both,
            notification-out-fade 200ms ease 6s both;
    }

    @keyframes notification-out-fade {
        to {
            opacity: 0;
        }
    }

    /* Rozpoznawanie i tak się nie chowa - zostaje samo pojawienie się. */
    .notification--identify {
        animation: fade-in 160ms ease both;
    }
}

/* Przy braku obsługi przezroczystości materiał staje się pełny. Barwa wariantu
   zostaje: bez niej ostrzeżenie i błąd wyglądałyby jak zwykłe powiadomienie. */
@media (prefers-reduced-transparency: reduce) {
    .notification {
        background: var(--c-surface);
        backdrop-filter: none;
    }

    .notification--error {
        background: var(--c-danger-weak);
    }

    .notification--identify {
        background: var(--c-warn-weak);
    }
}

@keyframes notification-in-mobile {
    to {
        width: 75%;
    }
}

@media (max-width: 719px) {
    .notification, .notification--identify {
        width: 75%;
    }

    .notification--identify .notification__icon {
        display: none;
    }

    .header__dropdown-content .button {
        width: 100%;
    }

    .list-header__actions .button.button--outline {
        background: var(--c-signal);
    }
}

/*
 * Panel na szerokim ekranie.
 *
 * Zmienia się wyłącznie struktura: liczba kolumn i gęstość wiersza. Rozmiary
 * pisma zostają te same, bo użytkownik ogląda panel z tej samej odległości -
 * płynna typografia w narzędziu nie pomaga, tylko rozjeżdża skalę.
 */
@media (min-width: 900px) {
    /*
     * Pasek dolny staje się kolumną z lewej. Ten sam znacznik, inny kształt -
     * dlatego trzeba tu wprost cofnąć position: fixed i wszystko, co z niego
     * wynikało. Jawne `left/right/bottom: auto` są konieczne, bo inaczej
     * element pozycjonowany zostałby przyklejony do krawędzi okna.
     */
    .panel {
        display: grid;
        grid-template-columns: var(--panel-nav-width) minmax(0, 1fr);
        align-items: start;
    }

    .panel__main {
        min-width: 0;
    }

    /* Wąska treść trzyma się nawigacji zamiast wyśrodkowywać się w kolumnie.
       Wyśrodkowana odjeżdżała na środek ekranu i formularz wyglądał, jakby nie
       należał do sekcji, z której się go otworzyło. Miara wiersza bez zmian -
       zmienia się położenie kolumny, nie jej szerokość. */
    .panel__main .content--narrow {
        margin-left: 0;
    }

    .nav {
        position: sticky;
        top: var(--s-6);
        left: auto;
        right: auto;
        bottom: auto;
        z-index: auto;
        display: flex;
        flex-direction: column;
        gap: var(--s-5);
        margin: var(--s-6) 0 var(--s-8);
        padding: 0 0 0 var(--s-6);
        background: transparent;
        border-top: 0;
    }

    .nav__list {
        flex-direction: column;
        gap: 2px;
        max-width: none;
        margin: 0;
    }

    .nav__item {
        min-height: var(--tap-min);
        flex-direction: row;
        justify-content: flex-start;
        gap: var(--s-3);
        padding: 0 var(--s-3);
        border-radius: var(--r-2);
        /* Wraca do skali panelu: w kolumnie jest miejsce na pełną nazwę. */
        font-size: var(--fs-14);
        line-height: var(--lh-snug);
        transition:
            color var(--dur-state) ease,
            background-color var(--dur-state) ease;
    }

    .nav__item--active {
        background: var(--c-surface-sunken);
    }

    /* W kolumnie wyjście odsuwa się od sekcji kreską - w pasku dolnym nie ma
       na to miejsca i nie jest potrzebne, bo tam kolejność czyta się od razu. */
    .nav__slot--exit {
        margin-top: var(--s-3);
        padding-top: var(--s-3);
        border-top: 1px solid var(--c-line);
    }

    /* Kreska nad pozycją była wskazówką paska dolnego. W kolumnie robi to tło. */
    .nav__item--active::before {
        content: none;
    }

    @media (hover: hover) and (pointer: fine) {
        .nav__item:hover {
            background: var(--c-surface-sunken);
        }
    }

    .nav__plan {
        display: flex;
        flex-direction: column;
        gap: 2px;
        padding: var(--s-3);
        background: var(--c-surface);
        border: 1px solid var(--c-line);
        border-radius: var(--r-2);
    }

    /* Wyjście do sklepu, pod blokiem planu. Rozciągnięte na szerokość kolumny,
       bo w wąskiej kolumnie przycisk przyklejony do lewej wygląda jak urwany. */
    .nav__buy {
        display: flex;
        justify-content: center;
    }

    .nav__plan-label {
        font-size: var(--fs-12);
        letter-spacing: var(--tracking-wide);
        text-transform: uppercase;
        color: var(--c-ink-500);
    }

    .nav__plan-name {
        font-size: var(--fs-14);
        font-weight: var(--fw-medium);
    }

    /* To jest przycisk formularza, nie odnośnik - zdejmujemy wygląd przycisku
       systemowego i zostawiamy wygląd odnośnika, bo w stopce planu ma czytać
       się jako drobne przejście, a nie druga akcja obok nazwy planu. */
    .nav__plan-link {
        min-height: var(--tap-min);
        display: flex;
        align-items: center;
        padding: 0;
        border: 0;
        background: none;
        font: inherit;
        font-size: var(--fs-12);
        color: var(--c-ink-700);
        text-align: left;
        text-decoration: underline;
        text-underline-offset: 2px;
        cursor: pointer;
    }

}

/*
 * Listy panelu na dwie kolumny.
 *
 * Zapytanie o kontener, nie o okno: od kiedy z lewej stoi nawigacja, szerokość
 * okna przestała mówić cokolwiek o szerokości kolumny treści. Przy 900 px okna
 * kolumna ma nieco ponad 600 px i dwa kafle nośnika zrobiłyby się ciasne -
 * a to ten sam ekran, na którym wcześniej wyglądały dobrze na pełnej szerokości.
 *
 * Próg dobrany do zawartości kafla, nie do żadnego urządzenia: dwa razy ~330 px
 * plus odstęp to najwęższy układ, w którym adres celu nie łamie się co drugie
 * słowo.
 */
.panel__main {
    container-type: inline-size;
}

@container (min-width: 700px) {
    .tags,
    .groups__list {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: var(--s-3);
        align-items: start;
    }

    /* Kafel nośnika rozkłada się na dwie kolumny: opis po lewej, cel po prawej,
       akcja przy prawej krawędzi. Na telefonie wszystko zostaje w słupku. */
    .tag {
        display: grid;
        grid-template-columns: 1fr auto;
        grid-template-areas:
            "top    status"
            "meta   meta"
            "target target"
            "action action";
        column-gap: var(--s-4);
    }

    .tag__top { grid-area: top; }
    .tag__meta { grid-area: meta; }
    .tag__target { grid-area: target; }
    .tag > .button { grid-area: action; justify-self: start; }

    /*
     * Znak nośnika schodzi do prawego dolnego rogu kafla. Wiersz akcji ma
     * przycisk dosunięty do lewej, więc ten róg stoi pusty - na telefonie
     * nie stoi, bo przycisk zajmuje tam całą szerokość, i dlatego znak
     * zostaje wtedy w wierszu metek, dokładnie tam, gdzie był napis.
     *
     * Pozycjonowanie bezwzględne zamiast własnego pola siatki, bo znak siedzi
     * w `.tag__meta`, a nie bezpośrednio w kaflu - siatka nie przeniesie
     * elementu do innego rodzica, a przeniesienie go w znaczniku zabrałoby
     * miejsce w wierszu metek na telefonie.
     */
    .tag {
        position: relative;
    }

    .tag__mark {
        position: absolute;
        right: var(--s-4);
        bottom: var(--s-4);
    }

    .tag__mark svg {
        width: 34px;
        stroke-width: 4;
    }
}

/* ==========================================================================
   Podsumowanie zamówienia (M5.11)

   Ostatni ekran przed oddaniem pieniędzy. Do M5.11 była to lista
   etykieta-wartość, w której nazwa nabywcy i kwota miały ten sam stopień
   i tę samą wagę - a kwota jest jedyną liczbą, która ma się czytać z drugiego
   końca stołu.
   ========================================================================== */

/* Odstęp od stopki, nie od paska płatności. Miejsce na pasek robi teraz sama
   stopka (`body:has(.paybar) .footer`), bo od 7 sierpnia 2026 to ona jest
   ostatnim elementem strony - te 96 px zostawiłyby pustkę w środku, między
   podsumowaniem a stopką, a paska i tak by nie ominęły. */
.summary { padding-bottom: var(--s-6); }

.summary__item {
    display: grid;
    grid-template-columns: 64px 1fr;
    gap: var(--s-4);
    align-items: center;
}

/* Ramka rysunku ma stały rozmiar, więc podmiana znaku zastępczego na zdjęcie
   produktu nie ruszy układu - ta sama zasada co w kaflu katalogu. */
.summary__mark {
    background: var(--c-surface-sunken);
    border-radius: var(--r-2);
    padding: 6px;
}

.summary__mark svg {
    display: block;
    width: 100%;
    height: auto;
}

.summary__name {
    margin: 0;
    font-size: var(--fs-18);
    font-weight: var(--fw-semibold);
    line-height: var(--lh-tight);
}

.summary__meta {
    margin: 2px 0 0;
    font-size: var(--fs-14);
    color: var(--c-ink-500);
}

/* Wariant jako metka, nie jako zdanie: to fakt o rzeczy z paczki, a nie opis. */
.summary__variant {
    display: inline-block;
    margin-top: var(--s-1);
    padding: 2px var(--s-2);
    border-radius: var(--r-full);
    background: var(--c-accent-weak);
    color: var(--c-accent);
    font-size: var(--fs-12);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
}

.summary__lines {
    list-style: none;
    margin: var(--s-3) 0 0;
    padding: 0;
    font-size: var(--fs-14);
    color: var(--c-ink-700);
}

.summary__lines li {
    display: flex;
    justify-content: space-between;
    gap: var(--s-3);
    padding: 3px 0;
}

/* Cyfry tabelaryczne: kolumna kwot ma się kończyć w jednej linii. */
.summary__lines span:last-child { font-variant-numeric: tabular-nums; }

.summary__total {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s-3);
    margin: var(--s-4) 0 0;
    padding: var(--s-4);
    border-radius: var(--r-2);
    background: var(--c-surface-sunken);
}

.summary__total-label {
    font-size: var(--fs-12);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--c-ink-500);
}

.summary__total-value {
    font-size: var(--fs-34);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-tight);
    font-variant-numeric: tabular-nums;
}

.summary__block { margin-top: var(--s-5); }

.summary__block p { margin: 0; }

.summary__block-title {
    margin: 0 0 var(--s-2);
    font-size: var(--fs-12);
    font-weight: var(--fw-regular);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--c-ink-500);
}

.summary__dim {
    color: var(--c-ink-500);
    font-size: var(--fs-14);
}

.summary__steps {
    list-style: none;
    margin: 0;
    padding: 0;
    counter-reset: summary-step;
}

.summary__steps li {
    position: relative;
    padding: 0 0 var(--s-4) var(--s-6);
    counter-increment: summary-step;
    font-size: var(--fs-14);
    line-height: var(--lh-normal);
    color: var(--c-ink-700);
}

.summary__steps li::before {
    content: counter(summary-step);
    position: absolute;
    inset-block-start: 0;
    inset-inline-start: 0;
    display: grid;
    place-items: center;
    width: 20px;
    height: 20px;
    border-radius: var(--r-full);
    background: var(--c-surface-sunken);
    color: var(--c-ink-700);
    font-size: 11px;
    font-weight: var(--fw-semibold);
}

.summary__steps li:last-child { padding-bottom: 0; }

.summary__steps strong { color: var(--c-ink-900); }

/* Pasek płatności zawsze w zasięgu kciuka. Sklep nie ma dolnej nawigacji
   panelu, więc nie ma z czym kolidować. */
.paybar {
    position: fixed;
    inset-inline: 0;
    inset-block-end: 0;
    z-index: 10;
    display: flex;
    align-items: center;
    gap: var(--s-3);
    padding: var(--s-3) var(--s-4);
    padding-bottom: max(var(--s-3), env(safe-area-inset-bottom));
    background: var(--c-surface);
    border-top: 1px solid var(--c-line);
}

.paybar__amount {
    font-size: var(--fs-12);
    line-height: var(--lh-snug);
    color: var(--c-ink-500);
}

.paybar__amount strong {
    font-size: var(--fs-16);
    color: var(--c-ink-900);
    font-variant-numeric: tabular-nums;
}

.paybar .button {
    flex: 1;
    justify-content: center;
}

/*
 * Od 720 px pasek przestaje być przyklejony i wchodzi w tok treści, na koniec
 * kolumny podsumowania.
 *
 * Przyklejenie ma sens na telefonie, gdzie ekran jest krótszy od zamówienia,
 * a kciuk sięga tylko dolnej krawędzi. Na szerokim ekranie ta sama reguła
 * rozciąga pasek na całą szerokość okna, czyli dwa razy szerzej niż kolumna,
 * którą klient właśnie czyta - i zostawia go tam, nawet gdy całe podsumowanie
 * mieści się bez przewijania. Wtedy nie jest skrótem do przycisku, tylko
 * belką przyklejoną do niczego.
 */
@media (min-width: 720px) {
    .paybar {
        position: static;
        justify-content: space-between;
        padding: var(--s-4);
        padding-bottom: var(--s-4);
        border: 1px solid var(--c-line);
        border-radius: var(--r-3);
    }

    /* W toku treści przycisk nie musi już zajmować całej szerokości - obok
       niego stoi kwota, a nie krawędź okna. */
    .paybar .button {
        flex: 0 0 auto;
    }

    /* Nie ma czego omijać: pasek nie leży już na treści ani na stopce. */
    body:has(.paybar) .footer {
        padding-bottom: var(--s-5);
    }
}

/* ---------- Stopka prawna ---------- */

/*
 * Jedna stopka dla wszystkiego, co wychodzi z `base` - landing, katalog, sklep
 * i panel. Trzy ekrany oglądane przez gościa lokalu (404 i 410 przekierowania,
 * `/witamy`) nadpisują blok pustką.
 *
 * Wygląd jest ten sam wszędzie, także pod ciemnym zamknięciem landingu. Stopka
 * w barwach sekcji, po której akurat stoi, znaczyłaby dwa wyglądy tego samego
 * elementu i pytanie „czy to na pewno ta sama stopka" przy każdym ekranie.
 */
.footer {
    /*
     * Stopka siada na dole okna, gdy treści jest mniej niż ekran, i jedzie za
     * treścią, gdy jest jej więcej.
     *
     * `sticky` z odsunięciem o wysokość okna: element trzyma się w toku strony,
     * ale próbuje ustawić górną krawędź poniżej zagięcia, więc spada tak nisko,
     * jak pozwala mu `body` - czyli do jego dolnej krawędzi, a ta ma zawsze
     * przynajmniej wysokość okna. Zero wpływu na układ czegokolwiek nad nią,
     * w odróżnieniu od kolumny elastycznej na `body` (patrz komentarz przy
     * `body`).
     */
    position: sticky;
    top: 100dvh;
    border-top: 1px solid var(--c-line);
    background: var(--c-bg);
    padding: var(--s-5) var(--s-4);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s-3);
    text-align: center;
}

/* Lista zawijana, nie trzy pozycje rozstawione na sztywno: czwarta
   („Ustawienia prywatności", gdy dojdzie analityka) ma się zmieścić bez
   przebudowy układu. */
.footer__links {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0 var(--s-4);
    max-width: var(--landing-max);
}

.footer__links a {
    display: inline-flex;
    align-items: center;
    /* Pełny cel dotykowy: odnośnik o wysokości wiersza jest w stopce na
       telefonie trafialny wyłącznie przypadkiem. */
    min-height: var(--tap-min);
    font-size: var(--fs-14);
    color: var(--c-ink-700);
}

.footer__seller {
    margin: 0;
    max-width: 36rem;
    font-size: var(--fs-12);
    line-height: var(--lh-normal);
    color: var(--c-ink-500);
    text-wrap: pretty;
}

/*
 * Stopka jest ostatnim elementem strony, więc to ona - a nie treść nad nią -
 * odpowiada za odstęp od pasków przyklejonych do dolnej krawędzi okna.
 */
body:has(.paybar) .footer {
    padding-bottom: calc(var(--s-5) + 72px);
}

body:has(.nav) .footer {
    padding-bottom: calc(var(--s-5) + 56px + env(safe-area-inset-bottom, 0px));
}

@media (min-width: 900px) {
    /* Nawigacja panelu przestaje być paskiem przy dolnej krawędzi, więc nie ma
       już czego omijać. */
    body:has(.nav) .footer {
        padding-bottom: var(--s-5);
    }
}

/* ---------- Miejsce na treść, której jeszcze nie ma ---------- */

/*
 * Znak dziury w treści. Ma wyglądać na niedokończone, bo jest niedokończone -
 * blok, który wygląda na skończony, przestaje się rzucać w oczy po tygodniu
 * i zostaje na produkcji.
 */
.draft {
    margin: 0;
    padding: var(--s-4);
    border: 1px dashed var(--c-line-strong);
    border-radius: var(--r-2);
    background: var(--c-surface-sunken);
    font-size: var(--fs-14);
    line-height: var(--lh-normal);
    color: var(--c-ink-700);
    text-wrap: pretty;
}

.draft-tag {
    display: inline-block;
    margin-right: var(--s-2);
    padding: 2px 6px;
    border-radius: var(--r-1);
    background: var(--c-warn-weak);
    color: var(--c-warn);
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    /* Metka nie jest zdaniem, więc nie ma prawa rozjeżdżać interlinii akapitu,
       w którym stoi. */
    line-height: var(--lh-snug);
    white-space: nowrap;
}

/* ---------- Strony dokumentów ---------- */

/*
 * Typografia dokumentu ustawiona **z góry, na potomkach**. Treść wkleja radca
 * prawny jako zwykły HTML i nie ma jej opatrywać klasami - inaczej wklejenie
 * przestaje być wklejeniem.
 */
.legal__document {
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
    margin-top: var(--s-5);
}

.legal__document h2 {
    margin: var(--s-3) 0 0;
    font-size: var(--fs-18);
    font-weight: var(--fw-semibold);
    line-height: var(--lh-snug);
    color: var(--c-ink-900);
}

.legal__document p,
.legal__document li {
    margin: 0;
    font-size: var(--fs-16);
    line-height: var(--lh-normal);
    color: var(--c-ink-700);
    text-wrap: pretty;
}

.legal__document ul,
.legal__document ol {
    margin: 0;
    padding-left: var(--s-5);
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
}

/* Zdanie o dokumentach przy przycisku wysyłającym formularz. Ciszej od pól
   i od przycisku - to jest odsyłacz, nie krok do wykonania. */
.form-legal {
    margin: 0;
    font-size: var(--fs-12);
    line-height: var(--lh-normal);
    color: var(--c-ink-500);
    text-wrap: pretty;
}

.form-legal a {
    color: var(--c-ink-700);
    text-decoration: underline;
}

/* Wiersz kwoty stałej w konfiguratorze zamówienia (dziś: wysyłka). Ciszej od
   „Razem" i bez wciśniętego tła - to składnik sumy, a nie sama suma. */
.order-line {
    margin: 0;
    padding: 0 var(--s-4);
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s-3);
    font-size: var(--fs-14);
    color: var(--c-ink-700);
}

.order-line__value {
    /* Ta sama szerokość cyfr co w „Razem" - obie liczby stoją w słupku jedna
       nad drugą i rozjeżdżone przecinki widać od razu. */
    font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------------------------
   Wyszukiwanie i filtrowanie listy nośników
   --------------------------------------------------------------------------- */

/* Pasek stoi nad listą i jest zwykłym formularzem GET, więc bez JavaScriptu
   wygląda i działa tak samo, tylko przez przycisk „Zastosuj". Kontroler
   `filters` dopisuje klasę `filters--live` dopiero po wczytaniu i dopiero wtedy
   przycisk znika - strona bez skryptu nigdy nie zostaje bez sposobu na wysłanie
   formularza (ta sama zasada co przy CSRF, M5.14). */
.filters {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    padding: var(--s-3);
    background: var(--c-surface-sunken);
    border-radius: var(--r-3);
}

.filters__search {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    flex: 1 1 200px;
    min-width: 0;
    padding: 0 var(--s-3);
    background: var(--c-surface);
    border: 1px solid var(--c-line-strong);
    border-radius: var(--r-2);
}

.filters__icon {
    flex: none;
    color: var(--c-ink-500);
}

/* Pole nie ma własnej ramki - rysuje ją opakowanie razem z lupą, więc obwódka
   skupienia obejmuje jedno i drugie zamiast prostokąta w środku pigułki. */
.filters__input {
    flex: 1;
    min-width: 0;
    min-height: var(--tap-min);
    padding: 0;
    font: inherit;
    font-size: var(--fs-16);
    color: var(--c-ink-900);
    background: none;
    border: 0;
}

.filters__input:focus {
    outline: none;
}

.filters__search:focus-within {
    outline: var(--focus-ring);
    outline-offset: var(--focus-offset);
}

/* Systemowy krzyżyk pola typu search bywa niewidoczny na tle pigułki. */
.filters__input::-webkit-search-cancel-button {
    cursor: pointer;
}

.filters__fields {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--s-3);
}

.filters__field {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
    flex: 1 1 130px;
    min-width: 0;
}

.filters__label {
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

.filters__select {
    min-height: var(--tap-min);
    padding: 0 var(--s-2);
    font: inherit;
    font-size: var(--fs-14);
    color: var(--c-ink-900);
    background: var(--c-surface);
    border: 1px solid var(--c-line-strong);
    border-radius: var(--r-2);
}

/* Przycisk filtrów: wyłącznie wąski ekran i wyłącznie przy działającym
   skrypcie. Bez skryptu nie miałby czego rozwijać, bo pola są wtedy widoczne. */
.filters__toggle {
    display: none;
    align-items: center;
    justify-content: center;
    gap: var(--s-2);
    min-height: var(--tap-min);
    padding: 0 var(--s-3);
    font: inherit;
    font-size: var(--fs-14);
    color: var(--c-ink-700);
    background: var(--c-surface);
    border: 1px solid var(--c-line-strong);
    border-radius: var(--r-2);
    cursor: pointer;
}

.filters__chevron {
    transition: transform var(--dur-state) var(--ease-out);
}

.filters--open .filters__chevron {
    transform: rotate(180deg);
}

/* Kropka, gdy któraś lista zawęża wynik. Na wąskim ekranie pola są zwinięte,
   więc bez tego znaku pusty wynik wygląda jak brak nośników. */
.filters--narrowed .filters__toggle::after {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: var(--r-full);
    background: var(--c-signal-ink);
}

/* Przy sterowaniu na żywo przycisk wysyłki nie ma co robić na ekranie. */
.filters--live .filters__apply {
    display: none;
}

@media (max-width: 719px) {
    .filters {
        display: grid;
        grid-template-columns: 1fr auto;
        align-items: center;
    }

    .filters__fields {
        grid-column: 1 / -1;
    }

    .filters--live .filters__toggle {
        display: inline-flex;
    }

    /* Zwinięte pola chowa klasa, nie atrybut `hidden`: atrybut zostałby
       na elemencie także wtedy, gdy ekran urośnie, i filtry zniknęłyby
       na desktopie po obróceniu telefonu. */
    .filters--live .filters__fields {
        display: none;
    }

    .filters--live.filters--open .filters__fields {
        display: flex;
    }
}

/* Od 720 px wszystko mieści się w jednej linii: pole wyszukiwania rozpycha się,
   trzy listy mają stałą podstawę. Etykiety zostają nad listami, więc pole
   wyszukiwania równa się do ich dołu, nie do środka paska. */
@media (min-width: 720px) {
    .filters {
        flex-direction: row;
        align-items: flex-end;
        flex-wrap: nowrap;
    }

    .filters__search {
        flex: 1 1 auto;
    }

    .filters__fields {
        flex: 0 1 auto;
        flex-wrap: nowrap;
    }

    .filters__field {
        flex: 0 1 140px;
    }
}

/* Podsumowanie wyniku wyszukiwania. Stoi w ramce razem z listą, bo licznik
   w nagłówku mówi o wszystkich nośnikach i nie jest przerysowywany. */
.list-summary {
    margin: 0 0 var(--s-3);
    font-size: var(--fs-14);
    color: var(--c-ink-500);
}

.list-summary a {
    color: var(--c-ink-700);
}

/* Ramka Turbo nie jest elementem układu - bez tego wchodzi jako blok i psuje
   odstępy listy, która i tak rozkłada się sama. */
turbo-frame#tag-list {
    display: block;
}

/* ---------------------------------------------------------------------------
   Hurtowe przydzielanie nośników do grupy
   --------------------------------------------------------------------------- */

.assign {
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
}

.assign__toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
}

.assign__all,
.assign__label {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    /* Kafelkiem jest etykieta z polem w środku, więc klikalny jest cały wiersz,
       a nie sam napis - ta sama zasada co przy kafelkach nabywcy w zamówieniu. */
    cursor: pointer;
}

.assign__all {
    font-size: var(--fs-14);
    color: var(--c-ink-700);
}

.assign__list {
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
}

.assign__item {
    padding: var(--s-3) var(--s-4);
    background: var(--c-surface);
    border: 1px solid var(--c-line);
    border-radius: var(--r-2);
}

.assign__item--on {
    border-color: var(--c-accent);
    background: var(--c-accent-weak);
}

.assign__label input[type="checkbox"] {
    flex: none;
    width: 20px;
    height: 20px;
    accent-color: var(--c-accent);
}

.assign__description {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
    min-width: 0;
}

.assign__name {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    font-size: var(--fs-16);
    font-weight: var(--fw-medium);
    color: var(--c-ink-900);
}

.assign__meta {
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

/* Nośnik zabierany innej grupie. Wyróżniony, bo to jedyna zmiana z tego ekranu,
   której skutek widać gdzie indziej niż tutaj. */
.assign__other-group {
    color: var(--c-warn);
}

/* ---------------------------------------------------------------------------
   Rozpoznawanie nośników
   --------------------------------------------------------------------------- */

/* Pasek stanu w powłoce panelu jest wariantem dymka powiadomień
   (`.notification--identify`), a nie własnym kształtem - stoi wyżej, przy
   pozostałych wariantach. */

/* Włącznik pod listą nośników. Świadomie wyciszony i bez obwódki: to nie jest
   funkcja pierwszej potrzeby, tylko odpowiedź na pytanie, które przychodzi
   dopiero po przewinięciu listy. */
.identify-hint {
    margin-top: var(--s-6);
    padding-top: var(--s-5);
    border-top: 1px solid var(--c-line);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
}

.identify-hint__text {
    margin: 0;
    font-size: var(--fs-14);
    line-height: var(--lh-normal);
    color: var(--c-ink-500);
    text-wrap: pretty;
    flex: 1 1 24rem;
}

/* ---------------------------------------------------------------------------
   Odczyt zamiast pola - adres ustawiany przez grupę
   --------------------------------------------------------------------------- */

.field-readout {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
}

.field-readout__label {
    font-size: var(--fs-14);
    font-weight: var(--fw-medium);
    color: var(--c-ink-700);
}

.field-readout__value {
    padding: var(--s-3);
    font-family: var(--font-mono);
    font-size: var(--fs-14);
    color: var(--c-ink-700);
    background: var(--c-surface-sunken);
    border: 1px solid var(--c-line);
    border-radius: var(--r-2);
    overflow-wrap: anywhere;
}

.field__help--danger a {
    color: var(--c-danger);
}

/* Atrybut `hidden` musi wygrywać z układem.
   Złapane w przeglądarce 8 sierpnia 2026, nie testem: ekran nośnika w grupie
   pokazywał NARAZ pole adresu i odczyt adresu z grupy, choć `element.hidden`
   było ustawione i po stronie serwera, i przez skrypt. Przyczyna to waga
   selektorów - `[hidden] { display: none }` z arkusza przeglądarki przegrywa
   z każdą naszą regułą klasową ustawiającą `display`, a `.field`
   i `.field-readout` ustawiają `display: flex`.

   Reguła jest ogólna celowo: ta sama pułapka czeka każdy następny element,
   który chowamy atrybutem, a jest ułożony flexem albo siatką. */
[hidden] {
    display: none !important;
}

/* ---------- Harmonogramy: reguły i rozkład doby ---------- */

/*
 * Wiersz reguły. Karta, nie rząd pól, i to jest decyzja o czytelności:
 * trzy reguły w jednej płaskiej kolumnie pól zlewają się w listę sześciu
 * godzin i trzech adresów, w której nie widać, co z czym idzie w parze.
 */
.rules {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    margin: 0;
    padding: 0;
    list-style: none;
}

.rule {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    padding: var(--s-4);
    background: var(--c-surface-sunken);
    border: 1px solid var(--c-line);
    border-radius: var(--r-2);
}

/* Dwie godziny obok siebie od najwęższego ekranu: „od" i „do" czyta się jako
   jedną wartość, więc rozbite na dwa wiersze wyglądają jak dwa osobne pytania.
   Wolna przestrzeń idzie na adres niżej, nie tutaj. */
.rule__times {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-3);
}

/* Ten sam kształt, co pola godzin, i z tego samego powodu: „do kwoty"
   i „rabat" czyta się jako jedną wartość, więc rozbite na dwa wiersze
   wyglądają jak dwa osobne pytania (progi rabatu, M12 etap 5). */
.field--time,
.field--tier {
    flex: 1 1 8rem;
    min-width: 0;
}

/* Przycisk usunięcia jest odnośnikiem z wyglądu i przyciskiem z zachowania:
   POST-em musi być, bo usuwa wiersz z ekranu, ale w karcie reguły ma czytać
   się jako drobne wyjście, a nie druga akcja obok „Zapisz". */
.rule__remove {
    align-self: flex-start;
    min-height: var(--tap-min);
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    font-size: var(--fs-12);
    color: var(--c-danger);
    text-decoration: underline;
    cursor: pointer;
}

.rules__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-3);
}

/* Metka harmonogramu na liście nośników. Ten sam kształt co metka grupy,
   inny kolor - żeby dało się je rozróżnić kątem oka, gdy stoją obok siebie. */
.tag__group--schedule {
    background: var(--c-warn-weak);
    color: var(--c-warn);
}

.preview {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    margin-top: var(--s-5);
}

.preview__list {
    display: flex;
    flex-direction: column;
    gap: 1px;
    margin: 0;
    padding: 0;
    list-style: none;
    border: 1px solid var(--c-line);
    border-radius: var(--r-2);
    overflow: hidden;
}

.preview__row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--s-2) var(--s-3);
    padding: var(--s-3) var(--s-4);
    background: var(--c-surface);
    font-size: var(--fs-14);
}

.preview__hours {
    flex: none;
    font-family: var(--font-mono);
    font-weight: var(--fw-medium);
}

.preview__target {
    min-width: 0;
    color: var(--c-ink-700);
    overflow-wrap: anywhere;
}

/* Przerwa w harmonogramie wyciszona, ale nie schowana: właściciel ma ją
   zobaczyć, zanim zobaczy ją gość lokalu o 23:00. */
.preview__row--gap {
    background: var(--c-surface-sunken);
}

.preview__row--gap .preview__target {
    color: var(--c-ink-500);
    font-style: italic;
}

.preview__row--now {
    box-shadow: inset 3px 0 0 var(--c-ink-900);
}

.preview__badge {
    margin-left: auto;
    padding: 1px var(--s-2);
    border-radius: var(--r-1);
    background: var(--c-ink-900);
    color: var(--c-surface);
    font-size: var(--fs-12);
}

/* ---------- Ekran oceny (gość lokalu) ---------- */

/*
 * Gwiazdki są przyciskami wysyłającymi formularz, nie polami wyboru: ocena
 * ma być jednym dotknięciem, bez kroku „zatwierdź". Stoją w znaczniku od 5 do
 * 1 i są odwracane tutaj, bo selektor rodzeństwa `~` sięga wyłącznie w przód -
 * to jedyny sposób, żeby najechanie na trzecią podświetliło dwie wcześniejsze
 * BEZ JAVASCRIPTU. Ekran ogląda gość lokalu na losowym telefonie.
 */
.rating {
    display: flex;
    flex-direction: row-reverse;
    justify-content: center;
    gap: var(--s-1);
    margin: var(--s-4) 0 var(--s-2);
}

.rating__star {
    /* Reset przycisku: motyw przeglądarki dokłada tło, obwódkę i padding. */
    padding: var(--s-1);
    border: 0;
    background: none;
    color: var(--c-line-strong);
    cursor: pointer;
    /* Cel dotykowy nie może być mniejszy od palca, także przy małej gwiazdce. */
    min-width: 44px;
    min-height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    transition:
        color var(--dur-state) ease,
        transform var(--dur-tap) var(--ease-out);
}

.rating__star svg {
    width: 40px;
    height: 40px;
    fill: currentColor;
}

/*
 * Podświetlenie „do tej gwiazdki włącznie". `~` łapie rodzeństwo PO elemencie
 * w znaczniku, czyli - przy odwróconym kierunku - gwiazdki wizualnie WCZEŚNIEJSZE.
 */
.rating__star:hover,
.rating__star:hover ~ .rating__star,
.rating__star:focus-visible,
.rating__star:focus-visible ~ .rating__star {
    color: var(--c-signal);
}

.rating__star:active {
    transform: scale(0.92);
}

.rating__skip {
    font-size: var(--fs-14);
    color: var(--c-ink-500);
}

.review-exits {
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
    width: 100%;
    margin-top: var(--s-2);
}

/*
 * Uwaga dla właściciela jest zwinięta, ale NIE jest schowana: `summary` widać
 * od pierwszej chwili, tak samo jak przycisk opinii. Gdyby była dostępna
 * dopiero po niskiej ocenie, byłby to „review gating" - patrz komentarz
 * w `templates/review/thanks.html.twig`.
 */
.review-note summary {
    cursor: pointer;
    font-size: var(--fs-14);
    color: var(--c-ink-700);
    padding: var(--s-2);
}

.review-note form {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    margin-top: var(--s-3);
    text-align: left;
}

.review-note textarea {
    width: 100%;
    font: inherit;
    padding: var(--s-3);
    border: 1px solid var(--c-line-strong);
    border-radius: var(--r-2);
    background: var(--c-surface);
    resize: vertical;
}

/* ---------- Rozkład ocen (panel) ---------- */

.ratings {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    padding: var(--s-4);
    background: var(--c-surface);
    border: 1px solid var(--c-line);
    border-radius: var(--r-3);
}

.ratings__header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s-3);
}

.ratings__heading {
    margin: 0;
    font-size: var(--fs-14);
    font-weight: var(--fw-medium);
    color: var(--c-ink-700);
}

.ratings__average {
    font-size: var(--fs-22);
    font-weight: var(--fw-semibold);
}

.ratings__count {
    font-size: var(--fs-12);
    font-weight: var(--fw-regular);
    color: var(--c-ink-500);
}

.ratings__rows {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
}

.ratings__row {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

.ratings__track {
    flex: 1;
    height: 8px;
    border-radius: var(--r-1);
    background: var(--c-surface-sunken);
    overflow: hidden;
}

.ratings__fill {
    display: block;
    width: var(--width);
    height: 100%;
    border-radius: var(--r-1);
    background: var(--c-ink-900);
}

/* Kolumna liczb ma stałą szerokość, żeby słupki nie skakały przy 1 kontra 100. */
.ratings__value {
    min-width: 3ch;
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* =========================================================================
   Karta lojalnościowa (M10)
   =========================================================================

   Ekrany gościa lokalu, nie klienta: bez nawigacji, bez stopki, jedna decyzja
   na ekranie. Cel dotykowy nigdy poniżej `--tap-min`.

   ## Dlaczego reguły akapitów są zagnieżdżone pod `.public-page`

   Bo inaczej nie działają, i to jest ta sama pułapka, którą `CLAUDE.md` opisuje
   przy motywie formularzy Symfony: **selektor bez zrzutu ekranu to hipoteza**.
   Powłoka tych ekranów ma regułę `.public-page p` (specyficzność 0,1,1), która
   ustawia rozmiar i kolor KAŻDEGO akapitu w środku. Sama klasa `.loyalty-done`
   to 0,1,0, więc przegrywa - baner wydania nagrody renderował się przez to
   ciemnoszarym pismem na czarnym tle, czyli nieczytelnie, a nazwa nagrody na
   ekranie zza lady miała rozmiar zwykłego zdania. Złapane okiem 28 sierpnia
   2026, w przeglądarce; żaden test tego nie widzi.

   Dopisany przodek daje 0,2,0 i wygrywa uczciwie, bez `!important`.

   `--loyalty-accent` przychodzi z programu, atrybutem `style` na `.loyalty-card`.
   Jest to jedyny kolor w tym arkuszu, który NIE pochodzi z tokenów `:root`,
   i jedyny, który przychodzi od klienta - stąd wartość zapasowa w każdym
   miejscu użycia. Kształt sprawdza `LoyaltyProgram::getAccentColor()`, ale
   arkusz nie ma prawa na to liczyć: brak wartości nie może zostawić kropek
   niewidocznych na białym tle.
   ========================================================================= */

.loyalty-card {
    width: 100%;
    padding: var(--s-5) var(--s-4);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s-3);
    border: 1px solid var(--c-line);
    border-radius: var(--r-4);
    background: var(--c-surface);
    box-shadow: var(--sh-2);

    /* Pasek akcentu u góry, zamiast wypełnienia całej karty kolorem klienta.
       Kolor bywa dowolny - łącznie z takim, na którym nasz tekst przestaje być
       czytelny. Pasek pozwala klientowi zaznaczyć markę, a nam zachować
       kontrast, którego nie ustawia on. */
    border-top: 4px solid var(--loyalty-accent, var(--c-ink-900));
}

.public-page .loyalty-card__venue {
    margin: 0;
    font-size: var(--fs-18);
    font-weight: var(--fw-semibold);
    line-height: var(--lh-snug);
}

.public-page .loyalty-card__name {
    margin: 0;
    font-size: var(--fs-14);
    color: var(--c-ink-500);
}

/*
 * Pieczątki łamią się po PIĘĆ w rzędzie.
 *
 * Sama zawijana lista układa dziesięć kropek jako 8 + 2, bo mieści tyle, ile
 * mieści szerokość karty - i taki rząd-sierota czyta się jak błąd renderowania,
 * a nie jak karta. Piątkami liczy się przy tym szybciej niż ósemkami, a to jest
 * jedyna rzecz, którą gość robi na tym ekranie wzrokiem.
 *
 * Szerokość liczona, nie wpisana: pięć kropek po 28 px i cztery przerwy po
 * 8 px. Liczba pieczątek jest ustawiana przez klienta (od 2 do 30), więc przy
 * siedmiu wyjdzie 5 + 2, a przy trzydziestu sześć równych rzędów.
 */
.loyalty-card__stamps {
    margin: 0;
    padding: 0;
    list-style: none;
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: var(--s-3) var(--s-2);

    /* Siatka zamiast zawijanej listy: te same pięć w rzędzie, ale kropki
       rosną razem z szerokością, zamiast zostawiać dziurę po prawej. Sufit
       trzyma je w rozmiarze, w którym dziesięć sztuk widać jednym rzutem oka,
       a nie jako dziesięć osobnych rzeczy. */
    width: 100%;
    max-width: 264px;
    margin-inline: auto;
}

/*
 * Kształt pieczątki jest NIEREGULARNY i to nie jest ozdobnik.
 *
 * Idealne koło czyta się jak kontrolka interfejsu - kropka postępu, radio,
 * wskaźnik stanu. Odbita pieczątka nigdy nie jest okrągła co do piksela, więc
 * lekko zniekształcony owal mówi „to jest ślad stempla", zanim gość przeczyta
 * jedno słowo. Wartość promieni pochodzi z makiety.
 */
.loyalty-card__stamp {
    aspect-ratio: 1;
    border-radius: 58% 42% 47% 53% / 52% 48% 55% 45%;
    background: color-mix(in oklch, var(--c-ink-900) 4%, transparent);
    border: 1.5px dashed var(--c-line-strong);
}

.loyalty-card__stamp--filled {
    border-style: solid;
    border-color: transparent;
    background: var(--loyalty-accent, var(--c-ink-900));

    /* Jasne oczko w środku. Bez niego pełne pieczątki zlewają się w plamę,
       gdy jest ich dwadzieścia kilka - a liczenie ich wzrokiem jest jedyną
       rzeczą, którą gość na tym ekranie robi. */
    display: grid;
    place-items: center;
}

.loyalty-card__stamp--filled::after {
    content: "";
    width: 30%;
    height: 30%;
    border-radius: var(--r-full);
    background: rgb(255 255 255 / 0.75);
}

/*
 * Pieczątka sprzed chwili wjeżdża z odbiciem - jedna, nie wszystkie.
 *
 * Gość dostaje ją, nie patrząc na ekran (przykłada telefon do naklejki), więc
 * po odwróceniu telefonu musi zobaczyć, KTÓRA przybyła. Bez tego widzi samą
 * liczbę kropek i musi ją porównać z pamięcią sprzed minuty.
 */
.loyalty-card__stamp--fresh {
    animation: loyalty-pop 450ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
    box-shadow: 0 6px 14px color-mix(in oklch, var(--loyalty-accent, var(--c-ink-900)) 35%, transparent);
}

@keyframes loyalty-pop {
    from {
        transform: scale(0.2);
        opacity: 0;
    }

    55% {
        transform: scale(1.18);
        opacity: 1;
    }

    to {
        transform: scale(1);
        opacity: 1;
    }
}

/* Odbicie znika, ale cień zostaje: to on mówi, która pieczątka jest nowa,
   a ta informacja nie jest ruchem. */
@media (prefers-reduced-motion: reduce) {
    .loyalty-card__stamp--fresh {
        animation: none;
    }
}

.public-page .loyalty-card__reward {
    margin: 0;
    font-size: var(--fs-14);
    color: var(--c-ink-700);
    text-wrap: pretty;
}

/* Przycisk pieczątki i nagrody jest jedyną decyzją na ekranie, więc zajmuje
   całą szerokość kolumny. Gość trzyma telefon jedną ręką, przy ladzie. */
.loyalty-action {
    width: 100%;
    min-height: 56px;
    font-size: var(--fs-18);
}

/*
 * Potwierdzenie: wydana nagroda oraz przyjęta zgoda na odliczenie rabatu.
 *
 * Czytane przez OBSŁUGĘ z półtora metra i pod kątem, z cudzego telefonu -
 * stąd skala `--fs-counter-*` i ciemne tło całej strony (`.public-page--final`),
 * a nie kafelek w kolumnie treści. Wygląd: makieta „Karta lojalnościowa",
 * tura 5 (5 września 2026).
 *
 * ## Dlaczego reguły są zagnieżdżone pod `.public-page`
 *
 * Bo inaczej nie działają, i to jest ta sama pułapka, którą `CLAUDE.md` opisuje
 * przy motywie formularzy Symfony: **selektor bez zrzutu ekranu to hipoteza**.
 * Powłoka tych ekranów ma regułę `.public-page p` (specyficzność 0,1,1), która
 * ustawia rozmiar i kolor KAŻDEGO akapitu w środku. Sama klasa `.loyalty-done`
 * to 0,1,0, więc przegrywa - baner wydania nagrody renderował się przez to
 * ciemnoszarym pismem na czarnym tle, czyli nieczytelnie. Złapane okiem
 * 28 sierpnia 2026, w przeglądarce; żaden test tego nie widzi.
 */
.public-page--final {
    max-width: none;
    min-height: 100dvh;
    padding: var(--s-6) var(--s-4);
    gap: var(--s-4);
    text-align: left;
    align-items: stretch;
    justify-content: center;
    background: var(--c-stage);
    color: var(--c-stage-ink);
    overflow-x: clip;
}

.public-page .loyalty-done {
    width: 100%;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    text-align: left;
    color: var(--c-stage-ink);
}

/* Jedno słowo na całą szerokość ekranu: „Wydano" albo „Zgłoszone". To ono
   niesie odpowiedź, po którą obsługa sięga wzrokiem, więc jest największe
   i jedyne w kolorze. */
.public-page .loyalty-done__label {
    font-size: var(--fs-counter-word);
    font-weight: var(--fw-semibold);
    line-height: 0.95;
    letter-spacing: var(--tracking-tight);
    color: var(--c-ok-bright);
}

.public-page .loyalty-done__reward {
    font-size: var(--fs-28);
    font-weight: var(--fw-semibold);
    line-height: var(--lh-tight);
    text-wrap: balance;
}

.public-page .loyalty-done__time {
    font-family: var(--font-mono);
    font-size: var(--fs-14);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    font-variant-numeric: tabular-nums;
    color: color-mix(in srgb, currentColor 60%, transparent);
}

/*
 * Zgoda na odliczenie rabatu, przyjęta.
 *
 * Ten sam kształt co wydana nagroda, bo dla obsługi znaczy to samo: „przyjęte,
 * możesz działać". Różni je jedno - **z karty nic jeszcze nie zeszło**, więc
 * pod spodem stoi termin ważności zgody. Bez niego ekran obiecywałby stan,
 * który po czterech minutach przestaje być prawdą.
 */
.public-page .loyalty-done--consent .loyalty-done__label {
    /* Słowo jest dłuższe od „Wydano", a znaczy mniej: nie wydaliśmy jeszcze
       niczego. Stopień niżej w skali, żeby nie wychodziło poza ekran i nie
       krzyczało głośniej od samego wydania. */
    font-size: var(--fs-counter-title);
}

.public-page .loyalty-done__pill {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    margin: 0;
    padding: var(--s-3);
    border-radius: var(--r-3);
    background: color-mix(in srgb, var(--c-stage-ink) 10%, transparent);
    color: var(--c-stage-ink);
    font-size: var(--fs-16);
    font-variant-numeric: tabular-nums;
}

.loyalty-done__dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    flex: none;
    background: var(--c-ok-bright);
    animation: loyalty-breathe 1600ms ease-in-out infinite;
}

/* Kropka oddycha, bo zgoda TRWA - to jedyny element na tym ekranie, który
   mówi „coś jeszcze się dzieje". Ruch jest w miejscu i bez przesunięcia,
   więc nie wywołuje choroby lokomocyjnej. */
@keyframes loyalty-breathe {
    0%,
    100% {
        opacity: 1;
    }

    50% {
        opacity: 0.35;
    }
}

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

/*
 * Karta w portfelu telefonu (M10, etap portfela).
 *
 * Blok przycisku jest akapitem, bo stoi w kolumnie `.public-page` obok innych
 * akapitów - zerowanie marginesu robi za niego `.loyalty-wallet`, a nie
 * reguła powłoki.
 *
 * ## Stan wyłączony musi WYGLĄDAĆ na wyłączony, inaczej kłamie
 *
 * Do tej pory w arkuszu nie było ani jednej reguły dla `:disabled` na
 * `.button`, więc wyłączony przycisk renderował się identycznie jak czynny:
 * czarny, z kursorem wskazującym i reakcją na wciśnięcie. Gość naciskałby go
 * w kółko, a ekran nie robiłby nic. Reguła jest ogólna (`.button:disabled`),
 * bo to jest brak w samym przycisku, a nie w karcie lojalnościowej.
 */
.button:disabled {
    background: var(--c-surface-sunken);
    color: var(--c-ink-500);
    cursor: not-allowed;
}

/* Wciśnięcie nie ma prawa dawać odpowiedzi, skoro przycisk nic nie robi. */
.button:disabled:active {
    transform: none;
}

@media (hover: hover) and (pointer: fine) {
    .button:disabled:hover {
        background: var(--c-surface-sunken);
    }
}

.loyalty-wallet {
    width: 100%;
    margin: 0;
    display: flex;
}

/* Dopisany przodek, nie sama klasa: `.public-page p` (0,1,1) ustawiłaby temu
   zdaniu rozmiar i kolor akapitu treści, więc podpis pod przyciskiem czytałby
   się jak druga treść ekranu. Ta sama pułapka co przy `.loyalty-done`. */
.public-page .loyalty-wallet__note {
    margin: var(--s-2) 0 0;
    font-size: var(--fs-12);
    line-height: var(--lh-normal);
    color: var(--c-ink-500);
}

/* Potwierdzenie zamiast zaproszenia, gdy karta już leży w portfelu. Odnośnik
   „dodaj ponownie" jest w zdaniu, więc dziedziczy jego rozmiar - to jest
   wyjście awaryjne, nie działanie główne tego ekranu. */
.public-page .loyalty-wallet__done {
    margin: 0;
}

/*
 * Ekran przyłożenia w trybie punktowym ma dwa stany, bo formularz wysyła się
 * sam (`autosubmit_controller.js`).
 *
 * Domyślny jest stan BEZ skryptu: widać przycisk, znika wskaźnik pracy. Gdy
 * skrypt się wczyta, kontroler dokłada `is-submitting` tuż przed wysyłką
 * i stany się zamieniają. Kolejność jest istotna - stan pracy pokazany
 * najpierw kłamałby przez ułamek sekundy u kogoś, kto skryptu nie ma.
 *
 * Bez tego gość przez cały czas trwania POST-a i przekierowania patrzył na
 * przycisk „Zapisz wizytę", który naciskał za niego skrypt - a potem ekran
 * przeskakiwał na kartę. Wyglądało to na zacięcie.
 */
.loyalty-busy {
    display: none;
}

.public-page.is-submitting .loyalty-idle {
    display: none;
}

.public-page.is-submitting .loyalty-busy {
    display: block;
}

/*
 * Kafelek karty znika na czas wysyłki - i to jest cała naprawa błędu
 * zgłoszonego 4 września 2026.
 *
 * `_card.html.twig` to WYGLĄD KARTY SPRZED przebudowy z 1 września: biały
 * prostokąt z paskiem akcentu. Stał na ekranie przyłożenia w obu stanach, więc
 * gość, który przyłożył telefon, widział przez ułamek sekundy starą kartę,
 * a zaraz potem nową - i wyglądało to na usterkę renderowania, a nie na
 * wczytywanie. Na jego miejsce wchodzi teraz szkielet karty docelowej.
 *
 * Reguła jest jedna dla obu ekranów (pieczątka i nagroda zza lady), bo
 * `.loyalty-card` znaczy dokładnie „ten kafelek", a `is-submitting` dokładnie
 * „ekran jest teraz szkieletem". Klasa dopisana w dwóch szablonach robiłaby
 * to samo, tylko w dwóch miejscach do zapomnienia.
 */
.public-page.is-submitting .loyalty-card,
.public-page.is-submitting .counter-card {
    display: none;
}

.loyalty-spinner {
    width: 28px;
    height: 28px;
    border: 3px solid var(--c-line);
    border-top-color: var(--c-ink-900);
    border-radius: 50%;
    animation: loyalty-spin 700ms linear infinite;
}

@keyframes loyalty-spin {
    to { transform: rotate(360deg); }
}

/*
 * Przy wyłączonych animacjach kółko przestaje się kręcić, ale NIE znika:
 * jest jedyną rzeczą na ekranie mówiącą, że coś trwa.
 */
@media (prefers-reduced-motion: reduce) {
    .loyalty-spinner {
        animation: none;
    }
}

/*
 * Ekran karty czekającej na zakup z kasy.
 *
 * Ta sama para stanów co przy przyłożeniu: bez skryptu widać prośbę „odśwież
 * tę stronę", ze skryptem - kółko, bo odświeży się samo.
 *
 * Klasę stawia KONTROLER, nie szablon, i to jest tu cała ostrożność: gdyby
 * szła z serwera, kółko kręciłoby się w nieskończoność u kogoś, kto skryptu
 * nie ma, a prośba o odświeżenie zniknęłaby mu z oczu. Domyślny jest stan bez
 * skryptu.
 */
.public-page.is-awaiting .loyalty-idle {
    display: none;
}

/* Kółko zostaje wyłącznie na ekranie karty czekającej na paragon
   (`.loyalty-wait`). Ekran przyłożenia miał je do 4 września 2026 obok
   szkieletu - dwa wskaźniki tej samej rzeczy naraz, z których jeden jest
   dokładniejszy. */

/* =========================================================================
   Karta gościa według makiety (M10, 1 września 2026)
   =========================================================================

   Źródło: „Karta lojalnościowa" z Claude Design, tury 3a (rabat), 3b
   (pieczątki), 3c (punkty). Dotyczy WYŁĄCZNIE ekranu `/karta/{token}`;
   ekran przyłożenia i ekran zza lady zostają na kafelku `.loyalty-card`,
   bo tam karta jest elementem obok nagłówka, a nie samą stroną.

   ## Dlaczego kolor klienta wypełnia teraz cały nagłówek

   Bo pasek nad białym kafelkiem nie mówił niczyjej marki - mówił „ta aplikacja
   ma akcent". Cena jest jedna i trzeba ją było zapłacić kodem: pod kolorem
   stoją teraz napisy, a kolor wpisuje w panelu klient i bywa żółty. Pismo
   wylicza więc {@see \App\Loyalty\AccentColor} i wstawia jako
   `--loyalty-on-accent` na `<main>`.

   ## Kolory pochodne z `currentColor`, a nie z bieli

   Makieta ma pigułki i etykiety zapisane jako `rgba(255,255,255,.16)`. Na
   jasnym akcencie, gdzie pismo jest ciemne, biała mgiełka znika bez śladu.
   Wszystkie warstwy w nagłówku liczą się więc z `currentColor`, czyli
   z koloru pisma, który już zna kontrast - jeden zestaw reguł obsługuje oba
   warianty i nie da się dopisać trzeciego przez zapomnienie.
   ========================================================================= */

/*
 * Powłoka pełnoszerokościowa.
 *
 * `.public-page` jest wąską, wyśrodkowaną kolumną z odstępem po bokach - i tak
 * ma zostać wszędzie indziej. Tutaj nagłówek ma dochodzić do krawędzi telefonu,
 * więc odstęp schodzi na `<main>`, a wraca wewnątrz `.loyalty-sheet`.
 */
/*
 * Ten sam kształt bierze ekran przyłożenia w trakcie wysyłki, bo stoi na nim
 * wtedy szkielet karty - a szkielet w wąskiej, wyśrodkowanej kolumnie
 * zapowiadałby inny kształt, niż za chwilę przyjdzie, i cały jego zysk by
 * przepadł. Treść stanu spoczynku jest w tej chwili schowana
 * (`.is-submitting .loyalty-idle`), więc powłoka nie ma tam czego zepsuć.
 */
.public-page--sheet,
.public-page.is-submitting {
    padding: 0 0 var(--s-6);
    gap: 0;
    text-align: left;
    align-items: stretch;

    /* Kolumna nie ma prawa przewijać się w bok - ani przez animację wjazdu,
       ani przez długi napis w dymku naliczenia. */
    overflow-x: clip;
}

.loyalty-sheet {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    padding: var(--s-4);
}

/* ---------- Nagłówek w kolorze lokalu ---------- */

.loyalty-hero {
    padding: var(--s-5) var(--s-4) var(--s-6);
    background: var(--loyalty-accent, var(--c-ink-900));
    color: var(--loyalty-on-accent, #ffffff);
}

/*
 * Przy pieczątkach kolor gaśnie ku dołowi, zamiast kończyć się krawędzią.
 *
 * Powód jest treściowy, nie estetyczny: w tym trybie nagłówek nie niesie salda
 * (kropki są niżej), więc pełny prostokąt koloru byłby dużą płaszczyzną
 * z samą nazwą lokalu. Wygaszenie oddaje miejsce białej płycie, która jest tu
 * treścią właściwą. Ostatni przystanek to `transparent` z tym samym kolorem
 * w tle - `transparent` bez podania koloru przechodzi w niektórych silnikach
 * przez czerń i zostawia szarą smugę.
 */
.loyalty-hero--fade {
    /* Wygaszenie zaczyna się dopiero za nazwą lokalu i schodzi do zera na
       samej krawędzi. Proporcje z makiety (46% do 74%) opisują nagłówek
       wyższy od naszego - przy dwóch wierszach tekstu zostawiały pod nim
       pas bladego koloru bez treści, który wyglądał na niedokończony.
       Sprawdzone na zrzucie, nie policzone. */
    background: linear-gradient(
        180deg,
        var(--loyalty-accent, var(--c-ink-900)) 0%,
        var(--loyalty-accent, var(--c-ink-900)) 62%,
        color-mix(in srgb, var(--loyalty-accent, var(--c-ink-900)) 45%, transparent) 84%,
        color-mix(in srgb, var(--loyalty-accent, var(--c-ink-900)) 0%, transparent) 100%
    );
}

.loyalty-hero__id {
    display: flex;
    align-items: center;
    gap: var(--s-3);
}

/*
 * Znak lokalu zastępczy: inicjały na kwadracie o kolorach zamienionych
 * miejscami. Tło bierze kolor pisma nagłówka, a pismo - kolor akcentu, więc
 * kafelek działa w obu wariantach kontrastu bez drugiej reguły.
 *
 * **Tło NIE MOŻE być `currentColor`** i kosztowało to jeden zrzut ekranu.
 * `currentColor` rozwiązuje się względem `color` TEGO SAMEGO elementu, a ten
 * jest tu przed chwilą nadpisany na akcent - więc tło i pismo wychodziły
 * identyczne, a kafelek znikał, zostawiając po sobie samą pustą przerwę.
 * Odziedziczony kolor nagłówka trzeba wziąć z własności, nie ze słowa
 * kluczowego.
 */
.loyalty-hero__mark {
    flex: none;
    display: grid;
    place-items: center;
    width: 38px;
    height: 38px;
    border-radius: var(--r-3);
    background: var(--loyalty-on-accent, #ffffff);
    color: var(--loyalty-accent, var(--c-ink-900));
    font-size: var(--fs-14);
    font-weight: 700;
    letter-spacing: var(--tracking-tight);
}

.loyalty-hero__who {
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
}

/* Przodek dopisany, bo to `<h1>`: `.public-page h1` (0,1,1) bije samotną klasę
   (0,1,0) i wymusiłoby tu rozmiar zwykłego nagłówka strony razem z zerowaniem
   marginesu, którego ta klasa nie ustawia. */
.public-page .loyalty-hero__venue {
    margin: 0;
    font-size: var(--fs-18);
    font-weight: var(--fw-semibold);
    line-height: var(--lh-tight);
    text-wrap: balance;
}

.loyalty-hero__meta {
    font-family: var(--font-mono);
    font-size: var(--fs-12);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: color-mix(in srgb, currentColor 68%, transparent);
}

.public-page .loyalty-hero__label {
    display: flex;
    flex-wrap: wrap;
    gap: 0 var(--s-2);
    margin: var(--s-5) 0 0;
    font-family: var(--font-mono);
    font-size: var(--fs-12);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;

    /* `color: inherit` jest tu OBOWIĄZKOWE, nie porządkowe: `.public-page p`
       (0,1,1) ustawia każdemu akapitowi ciemnoszare pismo, a ta klasa razem
       z przodkiem daje wprawdzie 0,2,0 i wygrywa - ale tylko o te własności,
       które faktycznie wypisze. Pominięty `color` spada z powrotem na regułę
       powłoki i nagłówek dostaje ciemne pismo na kolorze klienta. */
    color: color-mix(in srgb, var(--loyalty-on-accent, #ffffff) 70%, transparent);
}

/* Przelicznik odsunięty na koniec wiersza etykiety, ale zawijalny - przy
   wąskim ekranie schodzi pod nią zamiast ją ściskać. */
.loyalty-hero__rate {
    margin-inline-start: auto;
    color: color-mix(in srgb, var(--loyalty-on-accent, #ffffff) 55%, transparent);
}

/*
 * Saldo. Jedyne miejsce poza landingiem i ekranem wydania nagrody, w którym
 * używamy rozmiaru wyświetlaczowego - i tu jest to uzasadnione tak samo:
 * gość patrzy na tę liczbę przez sekundę, jedną ręką, przy ladzie.
 */
.public-page .loyalty-hero__balance {
    display: flex;
    align-items: baseline;
    gap: var(--s-2);
    margin: var(--s-1) 0 0;
    color: inherit;
    font-size: var(--fs-44);
    font-weight: 700;
    line-height: 1.05;
    letter-spacing: -0.04em;
    font-variant-numeric: tabular-nums;
}

.loyalty-hero__of {
    font-size: var(--fs-22);
    font-weight: var(--fw-semibold);
    letter-spacing: normal;
    color: color-mix(in srgb, currentColor 55%, transparent);
}

.public-page .loyalty-hero__bar {
    height: 10px;
    margin: var(--s-4) 0 0;
    border-radius: var(--r-full);

    /* Ta sama pułapka co przy etykiecie i kosztowała drugi zrzut ekranu:
       bez `color: inherit` akapit dziedziczy ciemnoszare pismo z `.public-page p`,
       a wypełnienie paska rysuje się `currentColor` - czyli na kolorze lokalu
       pojawiał się ciemny pasek zamiast jasnego. */
    color: inherit;
    background: color-mix(in srgb, currentColor 25%, transparent);
    overflow: hidden;
}

.loyalty-hero__bar-fill {
    display: block;
    height: 100%;
    border-radius: inherit;
    background: currentColor;
}

/* Pigułka „co z tym zrobić". Mgiełka z koloru pisma, więc czytelna na jasnym
   i na ciemnym akcencie tym samym zapisem. */
.public-page .loyalty-hero__note {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    margin: var(--s-4) 0 0;
    padding: var(--s-3) var(--s-3);
    border-radius: var(--r-3);
    background: color-mix(in srgb, currentColor 16%, transparent);
    color: inherit;
    font-size: var(--fs-14);
    line-height: var(--lh-snug);
}

/* Kropka stanu tylko przy pigułce „jest co odbierać" - w pozostałych wypadkach
   zdanie mówi o przyszłości, a kropka sugerowałaby stan gotowy. */
.public-page .loyalty-hero__note--ready::before {
    content: "";
    flex: none;
    width: 8px;
    height: 8px;
    border-radius: var(--r-full);
    background: currentColor;
}

/* ---------- Stany nad białą płytą ---------- */

/* Komplet pieczątek. Zielone tło, nie akcent klienta: to jest komunikat
   naszej aplikacji o stanie, a nie element jego marki - i ma znaczyć to samo
   w każdym lokalu. */
.public-page .loyalty-flag {
    margin: 0;
    padding: var(--s-3) var(--s-4);
    border-radius: var(--r-3);
    background: color-mix(in oklch, var(--c-ok) 14%, white);
    color: var(--c-ok);
    font-size: var(--fs-14);
    font-weight: var(--fw-semibold);
    line-height: var(--lh-snug);
}

/*
 * Czekanie na paragon z kasy.
 *
 * Domyślny jest stan BEZ skryptu: widać prośbę o odświeżenie, nie widać kółka.
 * Klasę `is-awaiting` stawia kontroler po wczytaniu i stany się zamieniają -
 * kolejność jest istotna, bo kółko pokazane najpierw kręciłoby się
 * w nieskończoność u kogoś, kto skryptu nie ma.
 */
.public-page .loyalty-wait {
    margin: 0;
    padding: var(--s-3) var(--s-4);
    border: 1px solid var(--c-line);
    border-radius: var(--r-3);
    background: var(--c-surface);
    font-size: var(--fs-14);
    line-height: var(--lh-snug);
}

/* Ten sam słownik co na ekranie przyłożenia (`.loyalty-idle` kontra
   `.loyalty-busy`), tylko przełącznikiem jest `is-awaiting`, a nie
   `is-submitting`. Druga para nazw znaczących to samo byłaby zaproszeniem do
   poprawienia jednej z nich i zapomnienia o drugiej. */
.public-page.is-awaiting .loyalty-wait .loyalty-busy {
    display: flex;
    align-items: center;
    gap: var(--s-3);
}

.loyalty-wait .loyalty-spinner {
    flex: none;
    width: 20px;
    height: 20px;
    border-width: 2.5px;
    border-color: color-mix(in oklch, var(--loyalty-accent, var(--c-ink-900)) 25%, transparent);
    border-top-color: var(--loyalty-accent, var(--c-ink-900));
}

/*
 * Za co przyszło ostatnie naliczenie.
 *
 * Ciemny pasek, nie akcent klienta: to jest paragon, czyli nasza odpowiedź na
 * pytanie „skąd ta liczba", a nie element jego marki. Kolor akcentu zlewałby
 * się przy tym z nagłówkiem stojącym metr wyżej.
 */
.public-page .loyalty-gain {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    margin: 0;
    padding: var(--s-3) var(--s-4);
    border-radius: var(--r-3);
    background: var(--c-ink-900);
    color: var(--c-surface);
    font-size: var(--fs-14);

    /* Wjazd, nie odbicie - i to jest różnica praktyczna, nie gustu.
       `loyalty-pop` przeskalowuje element do 1,18 w połowie przebiegu, co przy
       kropce pieczątki jest niewidoczne, a przy pasku na całą szerokość
       kolumny **wychodzi poza ekran** i daje poziomy pasek przewijania na
       telefonie. Złapane na zrzucie 1 września 2026, w połowie animacji. */
    animation: loyalty-gain-in 320ms var(--ease-out) both;
}

@keyframes loyalty-gain-in {
    from {
        opacity: 0;
        transform: translateY(-6px);
    }
}

.loyalty-gain__value {
    flex: none;
    font-size: var(--fs-22);
    font-weight: 700;
    color: color-mix(in oklch, var(--c-ok) 45%, white);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.loyalty-gain__note {
    color: rgb(255 255 255 / 0.7);
    line-height: var(--lh-snug);
}

@media (prefers-reduced-motion: reduce) {
    .public-page .loyalty-gain {
        animation: fade-in 160ms ease both;
    }
}

/* ---------- Biała płyta pod nagłówkiem ---------- */

.loyalty-slab {
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
    padding: var(--s-5) var(--s-4);
    border-radius: var(--r-4);
    background: var(--c-surface);
    box-shadow: var(--sh-3);
}

/* Nagroda: po co to wszystko. Wiersz, a nie zdanie, bo nazwa nagrody ma tu
   wagę tytułu - to ona odpowiada na pytanie „czy warto zbierać". */
.public-page .loyalty-prize {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    margin: 0;
    padding-top: var(--s-4);
    border-top: 1px solid var(--c-line);
    color: var(--c-ink-900);
}

/* Wariant samodzielny: przy punktach nagroda jest całą płytą, bo nie ma nad
   nią pieczątek, od których miałaby się odkreślać. */
.public-page .loyalty-prize--slab {
    padding: var(--s-4);
    border-top: 0;
    border-radius: var(--r-4);
    background: var(--c-surface);
    box-shadow: var(--sh-3);
}

/* Szerokość minimalna, nie stała: próg bywa czterocyfrowy („1000 pkt"),
   a kwadrat 40 px mieści trzy znaki. */
.loyalty-prize__badge {
    flex: none;
    display: grid;
    place-items: center;
    min-width: 40px;
    height: 40px;
    padding-inline: var(--s-2);
    border-radius: var(--r-3);
    background: var(--c-surface-sunken);
    color: var(--c-ink-500);
    font-size: var(--fs-14);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.loyalty-prize__text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.loyalty-prize__label {
    font-family: var(--font-mono);
    font-size: var(--fs-12);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--c-ink-500);
}

.loyalty-prize__name {
    font-size: var(--fs-16);
    font-weight: var(--fw-semibold);
    line-height: var(--lh-snug);
    text-wrap: pretty;
}

/* ---------- Drabinka progów rabatu ---------- */

.loyalty-tiers {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
}

.public-page .loyalty-tiers__label {
    margin: 0;
    font-family: var(--font-mono);
    font-size: var(--fs-12);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--c-ink-500);
}

/*
 * Szczeble stoją w rzędzie i zawijają się przy szóstym (tyle wynosi sufit
 * `DiscountTiers::MAX_TIERS`). Odstęp jednopikselowy na tle linii rysuje
 * przegrody bez ani jednej ramki, więc zawinięty wiersz nie zostawia
 * podwójnej kreski.
 */
.loyalty-tiers__grid {
    display: flex;
    flex-wrap: wrap;
    gap: 1px;
    margin: 0;
    padding: 0;
    list-style: none;
    border-radius: var(--r-3);
    background: var(--c-line);
    overflow: hidden;
}

.loyalty-tiers__step {
    flex: 1 1 88px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s-1);
    padding: var(--s-3) var(--s-2);
    background: var(--c-surface-sunken);
    text-align: center;
}

.loyalty-tiers__rate {
    font-size: var(--fs-18);
    font-weight: 700;
    color: var(--c-ink-900);
    font-variant-numeric: tabular-nums;
}

.loyalty-tiers__range {
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--c-ink-500);
    white-space: nowrap;
}

.public-page .loyalty-tiers__note {
    margin: 0;
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

/* =========================================================================
   Szkielet karty: ekran przyłożenia w chwili, gdy wysyła się sam
   =========================================================================

   Źródło: „Karta lojalnościowa" z Claude Design, tura 4 („Stany wczytywania -
   te same trzy karty bez danych"). Znacznik: `templates/loyalty/_skeleton.html.twig`,
   i to tam stoi pełne „po co".

   Klas własnych jest tu tyle, ile jest prostokątów w miejscu napisów. Cały
   UKŁAD - nagłówek, płyta, drabinka, wiersz nagrody, przycisk - bierze się
   z klas ekranu docelowego, więc kształt jest opisany raz.

   ## Prostokąty liczą się z `currentColor`, tak samo jak nagłówek

   Ta sama racja co przy pigułce i etykietach: kolor lokalu bywa żółty, więc
   w nagłówku pismo jest ciemne, a szara mgiełka wpisana na sztywno byłaby raz
   niewidoczna, raz brudna. `currentColor` zna już kontrast (`--loyalty-on-accent`),
   więc jeden zestaw reguł obsługuje nagłówek w kolorze klienta i białą płytę
   pod nim - bez drugiego wariantu, o którym można zapomnieć. */

.loyalty-skel__block,
.loyalty-skel__mark,
.loyalty-skel__badge,
.loyalty-skel__stamp {
    display: block;
    flex: none;
    border-radius: var(--r-full);
    background-color: color-mix(in srgb, currentColor 15%, transparent);

    /* Przejaśnienie jedzie w poprzek prostokąta jako drugie tło, więc kolor
       podstawowy zostaje ten sam i nie trzeba go podawać dwa razy. */
    background-image: linear-gradient(
        90deg,
        transparent 30%,
        color-mix(in srgb, currentColor 14%, transparent) 45%,
        transparent 60%
    );
    background-size: 300% 100%;
    background-repeat: no-repeat;
    animation: loyalty-skel 1500ms ease-in-out infinite;
}

@keyframes loyalty-skel {
    from { background-position: 150% 0; }
    to { background-position: -50% 0; }
}

/*
 * Przy wyłączonych animacjach przejaśnienie znika, a prostokąty zostają.
 *
 * To jest ta sama decyzja co przy kółku: ruch jest tu ozdobą, ale sam kształt
 * niesie informację „to nie są jeszcze Twoje dane" - a ta informacja nie może
 * zależeć od ustawień ruchu.
 */
@media (prefers-reduced-motion: reduce) {
    .loyalty-skel__block,
    .loyalty-skel__mark,
    .loyalty-skel__badge,
    .loyalty-skel__stamp {
        background-image: none;
        animation: none;
    }
}

/* Znak lokalu i odznaka progu: kwadraty w miejscu kwadratów, te same wymiary
   co `.loyalty-hero__mark` i `.loyalty-prize__badge`. */
.loyalty-skel__mark {
    width: 38px;
    height: 38px;
    border-radius: var(--r-3);
}

.loyalty-skel__badge {
    width: 40px;
    height: 40px;
    border-radius: var(--r-3);
}

/* Szerokości są sufitem, nie wymiarem: `max-width` zamiast `width` trzyma
   prostokąt w kolumnie także wtedy, gdy telefon jest węższy od makiety. */
.loyalty-skel__block {
    max-width: 100%;
}

.loyalty-skel__block--venue {
    width: 148px;
    height: 15px;
}

.loyalty-skel__block--meta {
    width: 196px;
    height: 9px;
}

.loyalty-skel__block--label {
    width: 158px;
    height: 10px;
}

.loyalty-skel__block--balance {
    width: 224px;
    height: 46px;
    border-radius: var(--r-3);
}

/* Pasek postępu: wypełnienie stoi w prawdziwym `.loyalty-hero__bar`, więc
   dziedziczy jego wysokość i przycięcie. Ułamek jest zmyślony i ma taki
   zostać - prawdziwego w tej chwili nie znamy. */
.loyalty-skel__block--fill {
    width: 46%;
    height: 100%;
    background-color: color-mix(in srgb, currentColor 45%, transparent);
}

/* Pigułka „co z tym zrobić" mieści dwa wiersze, więc jej treść musi się
   ułożyć w kolumnę - prawdziwa jest jednym zdaniem w rzędzie. */
.public-page .loyalty-skel__note {
    flex-direction: column;
    align-items: stretch;
    gap: var(--s-2);
    min-height: 62px;
    justify-content: center;
}

.loyalty-skel__note .loyalty-skel__block {
    width: 100%;
    height: 9px;
}

.loyalty-skel__block--tail {
    width: 62%;
}

.loyalty-skel__block--rate {
    width: 42px;
    height: 16px;
}

.loyalty-skel__block--range {
    width: 56px;
    height: 8px;
}

.loyalty-skel__block--note {
    width: 300px;
    height: 9px;
}

.loyalty-skel__block--name {
    width: 172px;
    height: 14px;
}

/* Przycisk portfela: ta sama wysokość co `.loyalty-action`, żeby płyta nie
   podskoczyła w chwili wejścia prawdziwego. */
.loyalty-skel__block--action {
    width: 100%;
    max-width: none;
    height: 56px;
    border-radius: var(--r-3);
    background-color: color-mix(in srgb, currentColor 22%, transparent);
}

/* Kropka pieczątki: ten sam nieregularny kształt co prawdziwa, bo to on
   mówi „to jest ślad stempla", zanim cokolwiek się wczyta. */
.loyalty-skel__stamp {
    aspect-ratio: 1;
    border-radius: 58% 42% 47% 53% / 52% 48% 55% 45%;
}

/* =========================================================================
   Pinezka na mapie w ustawieniach lokalu
   =========================================================================

   Znacznik: `templates/panel/venue_edit.html.twig`, zachowanie:
   `assets/controllers/venue_map_controller.js` - i tam stoi pełne „po co".

   Cała ta sekcja jest NAKŁADKĄ: pod mapą zostają prawdziwe pola szerokości
   i długości, więc bez JavaScriptu nic z tego nie powstaje i nie ma czego
   stylować. Powłoka mapy jest dlatego `hidden` w znaczniku, a nie chowana
   regułą - `[hidden] { display: none !important }` na początku tego pliku
   pilnuje, żeby atrybut wygrywał z każdym `display` z klasy.

   Kafle i geokoder idą z OpenStreetMap. Atrybucję rysuje Leaflet i jest ona
   wymogiem licencji ODbL, więc nie wolno jej tu ukryć regułą. */
.venue-map {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
}

/* Wysokość jest wpisana liczbą, bo Leaflet nie umie zainicjować mapy
   w elemencie o zerowej wysokości - a `aspect-ratio` na kontenerze siatki
   formularza daje w Safari zero przy pierwszym rysowaniu. 260 px to tyle,
   żeby przy zoomie 17 widać było kwartał, a nie same drzwi. */
.venue-map__canvas {
    height: 260px;
    border: 1px solid var(--c-line-strong);
    border-radius: var(--r-2);
    /* Kafle są prostokątne, a narożniki mapy zaokrąglone - bez tego wystają. */
    overflow: hidden;
    background-color: var(--c-surface-sunken);
}

.venue-map__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-2);
    margin-top: var(--s-3)
}

/* Znak w miejscu pinezki: rysowany, nie obrazkowy. Domyślna ikona Leafletu
   jest plikiem PNG, którego adres biblioteka składa ze ścieżki własnego
   skryptu - a w AssetMapperze nazwa pliku niesie sumę kontrolną, więc ten
   adres wskazuje w pustkę.

   ⚠️ Obrót siedzi na PSEUDOELEMENCIE, nigdy na samej ikonie. Leaflet ustawia
   ikonie `transform: translate3d(...)` **stylem w znaczniku**, więc nasze
   `transform` z arkusza przegrywa wagą i po prostu nie działa: kształt zostaje
   nieobrócony, czyli kółkiem z jednym kwadratowym narożnikiem, i wygląda na
   niedokończony, a nie na zepsuty. Gdyby wygrało, byłoby jeszcze gorzej -
   zabrałoby pinezce położenie i wszystkie usiadłyby w lewym górnym narożniku
   mapy. Złapane wyłącznie okiem, 4 września 2026. */
.venue-map__pin {
    cursor: grab;
}

.venue-map__pin:active {
    cursor: grabbing;
}

/* Kropla: kółko z jednym narożnikiem ostrym, obrócone o 45 stopni, żeby ostry
   narożnik wypadł na dole i wskazywał punkt. Stąd `iconAnchor` na dole
   w środku kwadratu 24 × 24 - patrz `venue_map_controller.js`. */
.venue-map__pin::before {
    content: "";
    display: block;
    width: 100%;
    height: 100%;
    border: 2px solid var(--c-surface);
    border-radius: 50% 50% 50% 0;
    background-color: var(--c-accent);
    box-shadow: var(--sh-2);
    transform: rotate(-45deg);
}

/* Współrzędne pod mapą stoją w rzędzie, bo są jedną daną w dwóch polach.
   Pojedynczy wiersz mówi to samym układem; dwa osobne pola na całą szerokość
   wyglądałyby na dwa niezależne ustawienia. */
.venue-map__coordinates {
    display: grid;
    gap: var(--s-3);
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}

/* „Wyczyść" jest cichszym przyciskiem niż „Znajdź z adresu" i to jest
   celowe: puste współrzędne są poprawnym stanem, ale rzadko pożądanym.
   Własna klasa, a nie wariant `.button`, bo w panelu nie ma dziś odmiany
   „przycisk bez obwódki" i dorobienie jej tutaj rozsiałoby ją po ekranach,
   dla których nikt jej nie projektował. */
.venue-map__reset {
    align-self: center;
    min-height: var(--tap-min);
    padding: 0 var(--s-2);
    border: 0;
    background: none;
    font: inherit;
    font-size: var(--fs-14);
    color: var(--c-ink-500);
    text-decoration: underline;
    text-underline-offset: 3px;
    cursor: pointer;
}

.venue-map__reset:hover {
    color: var(--c-ink-900);
}

/* =========================================================================
   Ekran przy ladzie: nośnik „nagroda" (M10, 5 września 2026)
   =========================================================================

   Źródło: makieta „Karta lojalnościowa" z Claude Design, tura 5. Dotyczy
   WYŁĄCZNIE ekranu `/karta/nagroda/{slug}` i ekranu wyczerpanego limitu;
   ekran przyłożenia zostaje na jasnej powłoce z kafelkiem `.loyalty-card`.

   ## Dlaczego kolor lokalu wypełnia całą stronę

   Bo tego ekranu się nie czyta - najpierw się go ROZPOZNAJE. Obsługa patrzy na
   cudzy telefon z półtora metra, zwykle przez ramię gościa, i musi wiedzieć,
   czy to karta jej lokalu, zanim przeczyta pierwsze słowo. Barwa na całej
   płaszczyźnie odpowiada na to z odległości, z której napisów jeszcze nie widać.

   Cena jest ta sama co przy nagłówku karty gościa i płaci się ją tak samo:
   pod kolorem stoją napisy, a kolor wpisuje w panelu klient i bywa żółty, więc
   pismo wylicza {@see \App\Loyalty\AccentColor} i wstawia jako
   `--loyalty-on-accent`. Wszystkie warstwy pochodne liczą się z `currentColor`,
   nie z bieli - jeden zestaw reguł obsługuje jasny i ciemny akcent.
   ========================================================================= */

.public-page--counter {
    max-width: none;
    min-height: 100dvh;
    padding: var(--s-6) var(--s-4) var(--s-5);

    /* Odstęp większy niż w kolumnie treści, bo sąsiadują tu rzeczy w skali
       `--fs-counter-*`: przy 16 px kwota „63,40 zł" dotykała kafelka pod sobą. */
    gap: var(--s-5);
    text-align: left;
    align-items: stretch;
    background: var(--loyalty-accent, var(--c-ink-900));
    color: var(--loyalty-on-accent, #ffffff);

    /* Kolumna nie ma prawa przewijać się w bok - ani przez animację wjazdu,
       ani przez długą nazwę nagrody w skali `--fs-counter-prize`. */
    overflow-x: clip;
}

/*
 * Program wstrzymany i wyczerpany limit: tło ciemne, nie kolor lokalu.
 *
 * Barwa lokalu jest na tym ekranie sygnałem „działa". Pod zdaniem „nie
 * wydajemy teraz nagród" świętowałaby coś, czego nie ma - a obsługa właśnie
 * po to na ten ekran patrzy, żeby odróżnić jedno od drugiego z daleka.
 */
.public-page--counter.is-halted {
    background: var(--c-stage);
    color: var(--c-stage-ink);
}

/* Stany bez akcji („Nie widzimy karty", „Program wstrzymany") stoją w pionie
   pośrodku: nie ma pod nimi przycisku, więc trzymanie ich przy górnej
   krawędzi zostawiałoby pół ekranu pustki bez powodu. */
.loyalty-counter__mid {
    margin-block: auto;
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
}

/*
 * Nagłówek z etykietą: „Do wydania" plus nazwa nagrody, w jednym `<h1>`.
 *
 * Dopisany przodek, nie sama klasa: `.public-page h1` (0,1,1) ustawia
 * `--fs-22` każdemu nagłówkowi w środku, więc sama klasa (0,1,0) przegrałaby
 * i nazwa nagrody miałaby rozmiar zwykłego tytułu. Ta sama pułapka, która
 * kosztowała dwa zrzuty ekranu przy karcie gościa.
 */
.public-page .loyalty-counter__head {
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
}

.loyalty-counter__label {
    font-family: var(--font-mono);
    font-size: var(--fs-12);
    font-weight: var(--fw-medium);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: color-mix(in srgb, currentColor 72%, transparent);
}

.loyalty-counter__prize {
    font-size: var(--fs-counter-prize);
    font-weight: var(--fw-semibold);
    line-height: 1.02;
    letter-spacing: var(--tracking-tight);
    text-wrap: balance;
}

/* Kwota rabatu jest krótka i jest LICZBĄ, więc unosi się o stopień wyżej niż
   nazwa nagrody - obsługa czyta ją tak samo jak cenę na wyświetlaczu kasy. */
.loyalty-counter__prize--sum {
    font-size: var(--fs-counter-sum);
    font-variant-numeric: tabular-nums;

    /* Wiersz ciaśniejszy niż 1 obcinał ogon „ł" w napisie „63,40 zł" i kwota
       wchodziła na kafelek pod nią. Złapane okiem w przeglądarce - żaden test
       tego nie widzi. */
    line-height: 1.05;
}

.public-page .loyalty-counter__title {
    font-size: var(--fs-counter-title);
    font-weight: var(--fw-semibold);
    line-height: 1;
    letter-spacing: var(--tracking-tight);
    text-wrap: balance;
}

/* Dopisany przodek z tej samej racji co wyżej: `.public-page p` (0,1,1)
   ustawiłaby temu zdaniu kolor `--c-ink-700`, czyli ciemnoszary na kolorze
   lokalu - nieczytelny dokładnie w tej połowie przypadków, w której akcent
   jest ciemny. */
.public-page .loyalty-counter__lede {
    font-size: var(--fs-22);
    line-height: var(--lh-snug);
    color: color-mix(in srgb, currentColor 88%, transparent);
}

/*
 * Przycisk jest ODWRÓCENIEM kolorów ekranu: tło bierze pismo policzone dla
 * akcentu, napis bierze sam akcent.
 *
 * Makieta ma go czarnego na niebieskim i przy niebieskim to działa - ale kolor
 * wpisuje klient, więc lokal z czarnym albo granatowym akcentem dostałby
 * czarny przycisk na czarnym tle. Odwrócenie jest czytelne **z definicji**:
 * to ta sama para, którą `AccentColor` już policzył jako kontrastową.
 */
.public-page--counter .loyalty-counter__action {
    width: 100%;
    min-height: 64px;
    padding: var(--s-4);
    border-radius: var(--r-4);
    border: none;
    background: var(--loyalty-on-accent, #ffffff);
    color: var(--loyalty-accent, var(--c-ink-900));
    font-size: var(--fs-22);
    font-weight: var(--fw-semibold);
}

@media (hover: hover) and (pointer: fine) {
    .public-page--counter .loyalty-counter__action:hover {
        background: color-mix(in srgb, var(--loyalty-on-accent, #ffffff) 88%, transparent);
    }
}

/* Przycisk stoi przy dolnej krawędzi, w zasięgu kciuka trzymającego telefon
   przy ladzie - a nie tuż pod kafelkiem, gdzie wypadałby w połowie ekranu. */
.loyalty-counter__foot {
    margin-top: auto;
    width: 100%;
}

/*
 * Odnośnik ucieczki („Przejdź na stronę lokalu").
 *
 * ⚠️ **Od 5 września 2026 nie ma go w żadnym szablonie** - właściciel zdjął go
 * ze wszystkich ekranów lojalnościówki do czasu przemyślenia, dokąd ten
 * odnośnik ma naprawdę prowadzić. Reguły zostają, bo mają wrócić razem z nim,
 * i zostają **poprawione**, żeby nie wrócił razem z nimi błąd opisany niżej.
 *
 * `color: inherit` jest tu jedyną rzeczą, która sprawia, że napis jest
 * widoczny, i nie wolno go stąd zdjąć. Bez niego akapit przegrywa
 * z `.public-page p` (0,1,1) i bierze `--c-ink-700`, a `.rating__skip` liczy
 * się z `currentColor`, czyli dziedziczy ten ciemny atrament: na kolorze
 * lokalu dawało to kontrast 1,7:1, a na ciemnym tle ekranu wstrzymanego
 * i ekranu limitu 1,5:1, czyli napis niewidoczny. Zmierzone w przeglądarce
 * 5 września 2026.
 *
 * Stoi przy dolnej krawędzi - ale tylko wtedy, gdy nie ma nad nim przycisku.
 * Dwa marginesy `auto` w jednej kolumnie **dzielą** wolną przestrzeń po
 * równo, więc przycisk wylądowałby w połowie ekranu. Selektor rodzeństwa
 * działa, bo odnośnik stoi w znaczniku zawsze PO formularzu.
 */
.public-page--counter .loyalty-counter__away {
    margin: auto 0 0;
    text-align: center;
    color: inherit;
}

.public-page--counter .loyalty-counter__foot ~ .loyalty-counter__away {
    margin-top: 0;
}

.public-page--counter .rating__skip {
    color: color-mix(in srgb, currentColor 75%, transparent);
}

/* ---------- Kafelek karty na kolorze lokalu ---------- */

/*
 * Biała płyta: nazwa lokalu, numer karty i to, co w danym trybie mierzy saldo.
 *
 * Osobna od `.loyalty-card` (ekran przyłożenia), bo tamta stoi na jasnym tle -
 * uzasadnienie i termin zdjęcia tej dwoistości: `_counter_card.html.twig`.
 */
.counter-card {
    width: 100%;
    padding: var(--s-4);
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
    border-radius: var(--r-4);
    background: var(--c-surface);

    /* Kolor pisma jest tu WŁASNY, a nie odziedziczony: wewnątrz płyty tłem
       jest biel, więc `--loyalty-on-accent` z `<main>` byłby przy jasnym
       akcencie ciemnym pismem na białym (dobrze), a przy ciemnym - białym
       na białym, czyli niewidocznym. */
    color: var(--c-ink-900);
    box-shadow: var(--sh-2);
}

.counter-card__id {
    display: flex;
    align-items: center;
    gap: var(--s-3);
}

/* Inicjały lokalu w kolorze marki, z pismem, które już zna kontrast. Ta sama
   para co w nagłówku karty gościa, tylko odwrócona - tam kwadrat jest jasny
   na kolorze, tu kolorowy na bieli. */
.counter-card__mark {
    width: 36px;
    height: 36px;
    flex: none;
    display: grid;
    place-items: center;
    border-radius: var(--r-3);
    background: var(--loyalty-accent, var(--c-ink-900));
    color: var(--loyalty-on-accent, #ffffff);
    font-size: var(--fs-14);
    font-weight: var(--fw-semibold);
}

.counter-card__who {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.counter-card__venue {
    font-size: var(--fs-16);
    font-weight: var(--fw-semibold);
    line-height: var(--lh-tight);
}

.counter-card__meta {
    font-size: var(--fs-12);
    color: var(--c-ink-500);
}

/*
 * Saldo w kafelku jest CIEMNE, nie w kolorze lokalu - inaczej niż w makiecie.
 *
 * Makieta ma tu liczbę w kolorze marki i przy niebieskim to działa, ale ten
 * kolor bywa żółty: liczba na bieli zniknęłaby dokładnie tam, gdzie jest
 * jedyną informacją. Kolor marki zostaje na kropkach i pasku, bo tam jest
 * KSZTAŁTEM, a nie napisem do przeczytania.
 */
.counter-card__balance {
    margin-left: auto;
    font-size: var(--fs-22);
    font-weight: var(--fw-semibold);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.counter-card__of {
    margin-left: 2px;
    font-size: var(--fs-14);
    font-weight: var(--fw-regular);
    color: var(--c-ink-500);
}

.counter-card__bar {
    margin: 0;
    height: 10px;
    border-radius: var(--r-2);
    background: color-mix(in srgb, var(--c-ink-900) 10%, transparent);
    overflow: hidden;
}

.counter-card__bar-fill {
    display: block;
    height: 100%;
    border-radius: var(--r-2);
    background: var(--loyalty-accent, var(--c-ink-900));
}
