/* ==========================================================================
   WARSTWA WSPÓLNA  —  typografia, tokeny, układ, komponenty.
   ==========================================================================

   Kolory bazowe (cztery na paletę) mieszkają w `themes.css`. Tutaj są tylko dwa
   rodzaje wartości barwnych:

     * POCHODNE — wyliczone z czterech kolorów palety przez `color-mix()`;
     * SEMANTYCZNE (`--danger`, `--success`) — IDENTYCZNE we wszystkich paletach.

   Mieszamy `in srgb`, a nie `in oklab`: oklab daje ładniejsze przejścia, ale
   wynik trudno policzyć na kartce. W srgb mieszanie jest zwykłą średnią ważoną
   składowych, więc każdy współczynnik poniżej dało się dobrać pod konkretny
   próg kontrastu i zweryfikować liczbowo, a nie „na oko". Współczynniki stoją
   na styk minimum — zmiana któregokolwiek wymaga ponownego przeliczenia.

   Rozmiary pisma pochodzą WYŁĄCZNIE ze skali `--text-*` (proporcja 1.25).
   Żadnego `font-size` z wartością wpisaną wprost; pilnuje tego test
   `test_arkusz_uzywa_wylacznie_skali_typograficznej`.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Skale — wartości niezależne od palety.

   Kroje pisma (`@font-face`, `--font-sans`, `--font-serif`) mieszkają
   w `fonts.css`: potrzebuje ich także `/admin/`, do którego ten arkusz nie
   wchodzi (ADR-064).
   -------------------------------------------------------------------------- */

:root {
  color-scheme: light;

  /* Skala modularna, proporcja 1.25, podstawa 1rem. Kolejne poziomy to
     kolejne potęgi proporcji — dzięki temu rozmiary są ze sobą w stałym
     stosunku, a nie dobierane po jednym „na czuja". */
  --text-caption: 0.8rem; /* 1 / 1.25    — podpisy, podpowiedzi, błędy pola */
  --text-body: 1rem; /*                  — treść */
  --text-lead: 1.25rem; /* 1.25          — wyróżniony akapit, legenda sekcji */
  --text-title: 1.5625rem; /* 1.25²      — nagłówek karty */
  --text-page: 1.9531rem; /* 1.25³       — nagłówek strony */

  /* 1.25⁴ — stopień DISPLAY. Sufit skali, nie kolejny nagłówek: obowiązuje
     w całym projekcie, a używają go DWA miejsca, oba w ZAPROSZENIU — tytuł
     wydarzenia w hero Gościa i inicjał otwierający „naszą historię" (#17,
     profil editorialowy). Nagłówki panelu zostają na `--text-page` i nie wolno
     ich tym podmieniać — panel ogląda jedna osoba przez godziny, zaproszenie
     kilkaset osób przez chwilę, i tylko to drugie ma prawo krzyczeć.

     Drugie użycie jest po TEJ SAMEJ stronie tej granicy i dlatego wolno je było
     dołożyć: inicjał nie jest nagłówkiem, jest literą, a stoi na ekranie, który
     z założenia ma przyciągać. Trzeciego użycia w panelu Organizatora nie będzie.

     PŁYNNOŚĆ SIEDZI W TOKENIE, nie w regule. Gdyby `clamp()` stał przy
     `font-size`, każde kolejne użycie tego stopnia powtarzałoby trzy liczby
     i pierwsza rozbieżność między nimi rozjechałaby skalę po cichu. Tu
     wartość jest jedna i cała skala pozostaje jednym miejscem do zmiany.

     Sufit to dokładnie 1.25⁴ (2.4414rem), czyli kolejna potęga proporcji —
     nie liczba dobrana pod tytuł, który akurat mamy w danych. Podłoga to
     `--text-page` (1.9531rem): przy 390 px tytuł schodzi do stopnia nagłówka
     strony i ani o krok niżej, bo poniżej przestaje być najważniejszą rzeczą
     na ekranie. Człon zmienny domyka oba końce: podłoga wypada dokładnie
     przy 390 px (nasz cel mobilny), sufit przy 1200 px.

     Człon stały jest w `rem`, a nie zerowy, i to nie jest kosmetyka:
     `font-size` złożony z samych `vw` nie reaguje na powiększenie tekstu
     w przeglądarce, więc osoba, która ustawiła sobie większy krój, dostaje
     tytuł dokładnie tej samej wielkości co wszyscy (WCAG 1.4.4). */
  --text-display: clamp(1.9531rem, 1.718rem + 0.96vw, 2.4414rem); /* 1.25⁴ */

  --leading-tight: 1.2; /* nagłówki */
  --leading-normal: 1.55; /* treść */

  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;

  --radius: 10px;
  --radius-sm: 6px;

  /* Czasy przejść — górna granica z widełek 120–200 ms. */
  --dur-fast: 120ms;
  --dur: 180ms;

  --measure: 40rem; /* komfortowa długość wiersza tekstu */
  --card-max: 26rem; /* karta logowania i pozostałe ekrany tożsamości */
  --card-max-wide: 34rem; /* formularz rejestracji (trzy sekcje) */
  --legal-max: 46rem; /* dokument prawny — listy zagnieżdżone i tabele (#163) */
  --panel-width: 68rem;
  --control-height: 2.75rem; /* jednolita wysokość wszystkich kontrolek */
  --field-max: 24rem; /* pole nie rozciąga się na całą kartę */

  /* --------------------------------------------------------------------
     Tokeny SEMANTYCZNE — te same we wszystkich paletach, świadomie POZA
     blokami `[data-theme]`.

     Palety mają cztery kolory i żaden z nich nie jest czerwony ani zielony,
     więc barwy sygnalizacyjnej nie da się z nich wyliczyć. Powód, dla którego
     musi istnieć, nie jest estetyczny: przy zgodzie RODO w formularzu RSVP
     gość musi jednoznacznie zobaczyć, że zgoda NIE została zapisana — sama
     ikona i pogrubienie to za słaby sygnał na telefonie, w słońcu, na weselu.
     Czerwień w palecie `blush` i w `graphite` jest identyczna, bo znaczenie
     „błąd" nie zależy od tego, jakie wesele obsługujemy.

     Sygnał barwny UZUPEŁNIA rozróżnienia nietekstowe (ikona, grubość
     krawędzi, pogrubienie), nigdy ich nie zastępuje — WCAG 1.4.1.
     -------------------------------------------------------------------- */
  --danger: #b3261e;
  --danger-bg: #fdecea;
  --success: #146c43;
  --success-bg: #e7f4ec;
  /* TRZECI sygnał, dołożony przy skanerze (#9, etap 3). „Już wpuszczony" nie
     jest ani błędem (gość wraca z zewnątrz), ani sukcesem (nikogo właśnie nie
     wpuszczono) — a ten sam bilet drugi raz może znaczyć, że ktoś przesłał kod
     dalej. Bez własnej barwy stan wypadał neutralnie i nie zatrzymywał wzroku
     tam, gdzie jest to jedyne zadanie karty.

     Bursztyn, bo zieleń i czerwień są zajęte, a te trzy znaczenia („w porządku",
     „popatrz", „nie") są w tej kolejności czytelne bez instrukcji.

     KONTRAST POLICZONY, nie oszacowany: #8a4b00 daje 6.11:1 na własnym
     tle, 6.80:1 na bieli i 6.26:1 na najciemniejszej powierzchni karty
     w paletach — próg AA dla tekstu to 4.5:1. Ten token, jak `--danger`
     i `--success`, mieszka w warstwie WSPÓLNEJ: znaczenie „popatrz" nie zależy
     od tego, jakie wesele obsługujemy. */
  --attention: #8a4b00;
  --attention-bg: #fdf1e3;
}

/* Awaryjna wartość kroju nagłówkowego, gdyby dokument trafił tu bez palety.
   `:where()` zeruje wagę selektora — inaczej ten wpis (późniejszy w kolejności
   wczytywania, o tej samej specyficzności co `[data-theme="…"]`) przykryłby
   szeryfowy krój palet weselnych. */
:where(:root) {
  --font-display: var(--font-sans);
}

/* --------------------------------------------------------------------------
   Tokeny pochodne — wyliczane z czterech kolorów aktywnej palety.

   Selektor obejmuje TAKŻE `[data-theme]`, nie tylko `:root`. Wartość zmiennej
   niestandardowej wylicza się na elemencie, na którym ją ZADEKLAROWANO, a niżej
   już tylko dziedziczy. Gdyby ta lista wisiała wyłącznie na `:root`, paleta
   nałożona na fragment strony zmieniłaby cztery kolory bazowe, ale pochodne
   (przyciski, komunikaty, obramowania) zostałyby policzone z palety dokumentu —
   i sekcja wyszłaby w dwóch zestawach barw naraz. Sprawdzone na podglądzie.
   -------------------------------------------------------------------------- */

:root,
[data-theme] {
  /* Trzy role powierzchni. Pomieszanie ich to najczęstszy sposób, w jaki jasny
     interfejs staje się ciężki:
       --surface-card  tło KARTY — najjaśniejszy ton palety, czyli `--surface`
                       WPROST, bez rozjaśniania,
       --surface-page  tło STRONY — ten sam ton przyciemniony domieszką
                       `--surface-alt`,
       --surface-alt   pasy sekcji, nagłówek nawigacji, naprzemienne wiersze
                       tabel — NIGDY wypełnienie karty.

     Kierunek mieszania jest tu całą rzeczą. Pierwsza wersja rozjaśniała kartę
     bielą (`color-mix(#fff 88%, var(--surface))`) i wychodziło ~#FFFEFE
     w KAŻDEJ palecie: `--surface` jest już najjaśniejszym tonem, więc karta
     nie ma dokąd się rozjaśnić, a motyw znikał ze wszystkiego poza akcentami.
     Barwę niesie więc strona, a karta jest od niej jaśniejsza „za darmo".

     Domieszka 15 % (N = 85, dolna granica ustalonego zakresu 85–92): rozdział
     karta/strona jest tu najsłabszym ogniwem — przy N = 92 różnica jasności
     spada do ΔL* ≈ 0.6–1.0, czyli poniżej progu dostrzegalności na typowym
     ekranie. Przy N = 85 wychodzi ΔL* ≈ 1.3–1.9 i do tego wyraźna różnica
     ODCIENIA, bo strona ciągnie w stronę `--surface-alt`. */
  --surface-card: var(--surface);
  --surface-page: color-mix(in srgb, var(--surface) 85%, var(--surface-alt));

  /* Tekst pomocniczy: 84 % atramentu. Niżej zejść się nie da — na `--surface-alt`
     w palecie `blush` byłoby poniżej 4.5:1. */
  --muted: color-mix(in srgb, var(--ink) 84%, var(--surface-card));

  /* Surowy `--accent` NIE nadaje się ani na tekst, ani na tło pod tekstem:
     w paletach weselnych daje 2.50–3.12:1, czyli poniżej AA (`blush` z niemal
     białym napisem to 2.80:1). Jest kolorem identyfikacji palety — pierścień
     ogniska, podkreślenia, plamy dekoracyjne. Wszystko, co niesie treść,
     bierze przyciemniony wariant: 6.04–13.77:1. */
  --accent-strong: color-mix(in srgb, var(--accent) 37%, var(--ink));
  --accent-hover: color-mix(in srgb, var(--accent-strong) 78%, var(--ink));
  --accent-active: color-mix(in srgb, var(--accent-strong) 58%, var(--ink));
  --accent-soft: color-mix(in srgb, var(--accent) 16%, var(--surface-card));

  /* --------------------------------------------------------------------
     POWIERZCHNIA HERO — tokeny liczone względem PARY z `themes.css`
     (`--hero-surface` / `--hero-ink`), nie względem `--ink`.

     Wszystkie tokeny wyżej zakładają jasne tło i to założenie jest w nich
     ukryte: mieszają się KU `--surface-card`. Hero bywa polem ciemnym
     (palety firmowe) albo jasnym (weselne, ADR-068), więc jeden kierunek
     mieszania nie wystarczy — a nie jest to brak wartości, tylko brak
     drugiego KIERUNKU.

     TU JEST CAŁA SZTUCZKA: kierunku nie wybiera ten arkusz ani reguła
     komponentu, tylko paleta — przez to, CZYM jest jej `--hero-ink`. Każdy
     token niżej miesza się w stronę tego atramentu, więc ten sam wzór daje
     ton jasny na ciemnym polu i ciemny na jasnym. Bez tego każda barwa hero
     istniałaby w dwóch wariantach, a reguła komponentu musiałaby wiedzieć,
     który wziąć.

     Dlaczego nie surowy `--accent`: na ciemnym hero daje 1.45–3.60:1, czyli
     w trzech paletach z sześciu poniżej progu 3:1 dla elementu NIETEKSTOWEGO
     (WCAG 1.4.11) — ornament w `slate` byłby granatem na prawie czarnym.
     To ta sama wada co przy `--accent` pod tekstem na karcie (ADR-027).

     Współczynniki są WSPÓLNE dla wszystkich palet. Jedyna wartość zależna
     od schematu (`--hero-accent-share`) stoi przy schemacie, w `themes.css`,
     i ma tam swoje uzasadnienie — sześć ręcznie dobranych wyjątków to sześć
     miejsc do zapomnienia przy siódmej palecie.
     -------------------------------------------------------------------- */

  /* Akcent doprowadzony do czytelności NA HERO: mieszany ku atramentowi tego
     pola, czyli ku bieli na ciemnym i ku ciemnemu na jasnym. Ten sam token
     unosi ornament, nadkreślnik nad tytułem i płatki. Zmierzone 4.81–8.97:1,
     minimum w `blush`. */
  --accent-on-hero: color-mix(in srgb, var(--accent) var(--hero-accent-share), var(--hero-ink));

  /* Tekst drugorzędny na hero (termin, miejsce, czas pozostały). To jest
     DOKŁADNIE `--muted` policzony na powierzchni hero — ten sam współczynnik
     84 %, bo pytanie jest to samo: o ile przygasić atrament, żeby został
     szeptem, a nie zniknął. Zmierzone 5.02–9.24:1. */
  --on-hero-muted: color-mix(in srgb, var(--hero-ink) 84%, var(--hero-surface));

  /* Podkładka pod znakiem Organizatora — barwa PRZECIWNA do hero, bo jej
     jedyne zadanie to unieść znak, który z tłem hero się zlewa. Wzór jest
     ten sam co przy `--accent-soft` (16 % akcentu w powierzchni), tylko
     powierzchnią jest tu atrament hero: na ciemnym polu wychodzi z tego
     jasna przepaska, na jasnym — ciemna. Odwrócenie schematu odwraca ją
     samo, bez ani jednej reguły więcej (ADR-068). */
  --hero-plate: color-mix(in srgb, var(--accent) 16%, var(--hero-ink));

  /* Ramka włosowa: niski udział atramentu. Rola dekoracyjna (obrys karty,
     separator) — nie identyfikuje komponentu, więc nie podlega progowi 3:1. */
  --border: color-mix(in srgb, var(--ink) 28%, var(--surface-card));

  /* Granica KONTROLKI. Tu próg 3:1 obowiązuje (WCAG 1.4.11): obrys pola jest
     jedyną rzeczą, po której widać, gdzie się kliknie. 58 % to minimum, przy
     którym warunek jest spełniony w każdej palecie względem karty I strony. */
  --border-control: color-mix(in srgb, var(--ink) 58%, var(--surface-card));
  --border-control-hover: color-mix(in srgb, var(--ink) 78%, var(--surface-card));
}

/* --------------------------------------------------------------------------
   Reset (minimalny — tyle, ile trzeba, żeby nie walczyć z przeglądarką)
   -------------------------------------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

/* `hidden` MUSI WYGRYWAĆ Z KAŻDĄ REGUŁĄ UKŁADU — i to jest poprawka błędu,
   nie ostrożność na wyrost. Przeglądarka realizuje ten atrybut regułą
   `[hidden] { display: none }` w SWOIM arkuszu, a każda nasza deklaracja
   `display` bije arkusz przeglądarki niezależnie od specyficzności. Skutek
   był widoczny na ekranie recepcji: bloki `.message` (czyli `display: flex`)
   stały odsłonięte WSZYSTKIE naraz, mimo `hidden` w znaczniku — ekran witał
   trzema ostrzeżeniami przy działającej kamerze.

   Wada tej klasy wraca przy KAŻDYM elemencie chowanym atrybutem, któremu
   ktoś kiedyś nada `display` — dlatego reguła stoi w resecie, a nie przy
   jednym komponencie. `!important` jest tu treścią: „ukryty" nie jest stanem
   do negocjacji z układem. */
[hidden] {
  display: none !important;
}

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  min-height: 100vh;
  background: var(--surface-page);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--text-body);
  line-height: var(--leading-normal);
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
}

img,
svg {
  max-width: 100%;
}

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}

fieldset {
  margin: 0;
  padding: 0;
  border: 0;
}

/* --------------------------------------------------------------------------
   Typografia — wyłącznie ze skali `--text-*`.

   Waga 700 nie jest przypadkowa: wysyłamy Inter w 400 i 700 oraz Cormorant
   Garamond w 600. Prośba o wagę, której nie ma w pliku, kończyłaby się doborem
   najbliższej (mamy `font-synthesis-weight: none`, więc bez pogrubiania
   syntetycznego) — deklarujemy więc to, co realnie leży w `static/fonts/`.
   -------------------------------------------------------------------------- */

h1,
h2,
h3 {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: var(--leading-tight);
  margin: 0 0 var(--space-4);
  text-wrap: balance;
}

h1 {
  font-size: var(--text-title);
  letter-spacing: -0.01em;
}

h2 {
  font-size: var(--text-lead);
}

h3 {
  font-size: var(--text-body);
}

/* Nagłówek STRONY — o stopień wyżej niż nagłówek karty. Karta na ekranie
   logowania ma nagłówek proporcjonalny do siebie, nie do przeglądarki. */
.page-title {
  font-size: var(--text-page);
  /* Nagłówek strony NIESIE CUDZY TEKST: tytuł wydarzenia, a od #100 także imię
     i nazwisko osoby na ekranie weryfikacji biletu. W największym stopniu pisma
     na ekranie jedno długie słowo bez spacji rozpycha stronę najszybciej —
     a wtedy przeglądarka oddala CAŁY dokument (patrz uzasadnienie przy
     `FREE_TEXT_SELECTORS` w `tests/test_presentation_guardrails.py`). */
  overflow-wrap: anywhere;
}

p {
  margin: 0 0 var(--space-4);
  max-width: var(--measure);

  /* SIEROTA W OSTATNIM WIERSZU — `pretty` przesuwa łamanie tak, żeby na końcu
     akapitu nie zostawało jedno krótkie słowo. Nagłówki mają `balance` od
     początku; akapity nie miały nic i przy mierze 40 rem widać to wprost:
     zdanie o 105 znakach zostawiało na drugim wierszu samo „i Ty.".

     `pretty`, nie `balance`: `balance` wyrównuje DŁUGOŚĆ wszystkich wierszy
     i jest pomyślane dla dwóch–trzech wierszy nagłówka. W akapicie robi
     szkodę — przy sześciu wierszach przeglądarka i tak przestaje go stosować,
     a tam, gdzie go stosuje, skraca wiersze, które nie miały problemu.

     PRZEGLĄDARKA BEZ TEJ WARTOŚCI ŁAMIE JAK DZIŚ, więc reguła nie jest
     warunkiem poprawności układu — jest poprawką typograficzną. Treść, która
     nie ma prawa się rozjechać, musi to zapewnić SAMA (krótszym zdaniem albo
     `nowrap` na frazie, jak `.waiting__when strong`). */
  text-wrap: pretty;
}

:where(p, li):last-child {
  margin-bottom: 0;
}

a {
  color: var(--accent-strong);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}

a:hover {
  color: var(--accent-hover);
}

small,
.text-muted {
  color: var(--muted);
  font-size: var(--text-caption);
}

/* --------------------------------------------------------------------------
   Dostępność — ognisko klawiatury, łącze pomijające, tekst dla czytnika
   -------------------------------------------------------------------------- */

/* Pierścień DWUTONOWY i to nie jest ozdoba.

   Sam `--accent` nie wystarcza: jako element nietekstowy podlega progowi 3:1
   (WCAG 1.4.11), a w paletach weselnych daje 3.02–3.22 na karcie, 2.88–3.10 na
   tle strony i tylko 2.17–2.49 na `--surface-alt` — czyli na nagłówku nawigacji,
   gdzie stoi przycisk wylogowania. Wcześniejszy pomiar sprawdzał wyłącznie tło
   karty i dlatego wypadł pozytywnie.

   Zamiast odbierać `--accent` tę rolę (albo przyciemniać paletę, co przeciągnęłoby
   tożsamość weselną w brąz), pierścień dostaje drugą warstwę: wewnętrzny
   `box-shadow` niesie kolor palety, a zewnętrzna linia w `--ink` gwarantuje
   kontrast na każdym z trzech teł (9.53–15.56:1). Wskaźnik złożony z dwóch
   warstw, z których jedna spełnia próg, jest zgodny z 1.4.11. */
:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
  box-shadow: 0 0 0 3px var(--accent);
  border-radius: var(--radius-sm);
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Widoczne dopiero po sfokusowaniu tabulatorem — pierwszy element strony. */
.skip-link {
  position: absolute;
  left: var(--space-4);
  top: 0;
  z-index: 10;
  transform: translateY(-150%);
  padding: var(--space-2) var(--space-4);
  background: var(--surface-card);
  color: var(--accent-strong);
  border: 2px solid var(--border-control);
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  text-decoration: none;
  transition: transform var(--dur-fast) ease-out;
}

.skip-link:focus {
  transform: translateY(0);
}

/* --------------------------------------------------------------------------
   Znak graficzny (logo albo tekstowa nazwa produktu)
   -------------------------------------------------------------------------- */

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  text-decoration: none;
  color: var(--ink);
}

/* Lockup poziomy. 2,75rem = 44 px to wysokość ZNAKU w lockupie i jest to PRÓG,
   nie upodobanie: poniżej ~40 px siatka dni w kalendarzu zlewa się w plamę
   (ADR-119). Tam, gdzie nie ma na tyle miejsca, wstawiamy nazwę tekstem,
   a nie mniejszy rysunek. */
.brand__lockup {
  display: block;
  height: 2.75rem;
  width: auto;
}

.brand__name {
  font-family: var(--font-display);
  font-size: var(--text-lead);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* Lockup pionowy na ekranach tożsamości. Wyżej napis zaczyna dominować nad
   formularzem, a lockup jest szeroki, więc na telefon nie trzeba go zmniejszać. */
.brand-wordmark {
  display: block;
  height: 7rem;
  width: auto;
}

/* Ścieżka BEZ pliku znaku — nazwa niesie wtedy całą tożsamość sama, więc
   dostaje podkreślenie i ma wyglądać jak decyzja typograficzna, a nie jak
   zasób, którego nie udało się wczytać. Ze znakiem obok byłaby to druga
   kreska w jednym miejscu: znak już domyka kompozycję. */
.brand--text .brand__name {
  border-bottom: 2px solid var(--accent);
  padding-bottom: 2px;
}

/* --------------------------------------------------------------------------
   Układ: strony tożsamości (`base.html`) i panel (`base_panel.html`)
   -------------------------------------------------------------------------- */

.page {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center; /* oddech nad i pod kartą */
  gap: var(--space-6);
  min-height: 100vh;
  padding: var(--space-7) var(--space-4);
}

.page__body {
  width: 100%;
  max-width: var(--card-max);
}

.page__body--wide {
  max-width: var(--card-max-wide);
}

.panel {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

/* Nagłówek nawigacji — jedno z trzech dozwolonych zastosowań `--surface-alt`. */
.panel__header {
  background: var(--surface-alt);
  border-bottom: 1px solid var(--border);
}

.panel__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  width: 100%;
  max-width: var(--panel-width);
  margin: 0 auto;
  padding: var(--space-4);
}

.panel__nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-5);
}

.panel__main {
  width: 100%;
  max-width: var(--panel-width);
  margin: 0 auto;
  padding: var(--space-6) var(--space-4) var(--space-7);
}

/* Karta: prawie białe tło + ramka włosowa + promień. Bez cienia — cień na
   jasnym tle albo nie jest widoczny, albo brudzi barwę palety. */
.card {
  background: var(--surface-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-6);
}

.stack > * + * {
  margin-top: var(--space-6);
}

/* --------------------------------------------------------------------------
   Ikony — sprite SVG, kolor dziedziczony przez `currentColor`
   -------------------------------------------------------------------------- */

.icon {
  width: 1.15em;
  height: 1.15em;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  vertical-align: -0.2em;
}

/* --------------------------------------------------------------------------
   GRAFIKA NIE WCHODZI DO ZAZNACZENIA

   Obrazek ani ikona nie są tekstem, ale przeciągnięcie myszą przez akapit
   zabiera je razem z nim: znak firmowy i ikonki podświetlają się wtedy na
   niebiesko, a próba zaznaczenia zdania kończy się złapaniem obrazka
   i rozpoczęciem jego przeciągania. Na ekranie zaproszenia, gdzie znak
   Organizatora stoi tuż pod podpisem, wygląda to na usterkę.

   REGUŁA JEST GLOBALNA, bo problem jest globalny — `app.css` wchodzi na KAŻDEJ
   stronie, także w warstwie Gościa (`base_public.html` rozszerza `base.html`),
   więc jedna deklaracja obejmuje panel, ekrany tożsamości i zaproszenie.

   CZEGO TA REGUŁA NIE ROBI i nie ma robić: nie chroni obrazków przed pobraniem.
   Menu podręczne, „zapisz obrazek jako", narzędzia deweloperskie i zwykły adres
   pliku działają dalej — plik w magazynie publicznym jest publiczny z definicji
   (ADR-056). To jest poprawka ERGONOMII zaznaczania tekstu, nie zabezpieczenie,
   i nie wolno jej opisywać jako zabezpieczenia.

   Tekst zostaje zaznaczalny wszędzie tam, gdzie był — reguła dotyczy wyłącznie
   elementów graficznych. Wyjątek `user-select: all` na kodzie dostępu (niżej
   w tym pliku) jest niezależny i nietknięty.
   -------------------------------------------------------------------------- */
img,
svg {
  user-select: none;
  /* Przeciąganie obrazka to osobne zachowanie od zaznaczania i osobny atrybut:
     bez tego złapanie znaku myszą nadal startuje „przeciągnij i upuść",
     nawet gdy zaznaczenie jest już wyłączone. Własność jest niestandardowa
     (tylko WebKit/Blink), a w Firefoksie tę samą rolę pełni `draggable="false"`
     na elemencie — nie dokładamy go, bo w tej przeglądarce przeciąganie nie
     przerywa zaznaczania tekstu i nie ma czego naprawiać. */
  -webkit-user-drag: none;
}

/* --------------------------------------------------------------------------
   Przyciski i łącza akcji
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--control-height);
  padding: 0 var(--space-5);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: var(--accent-strong);
  color: var(--surface-card);
  font-weight: 700;
  line-height: var(--leading-tight);
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--dur-fast) ease-out;
}

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

.btn:active {
  background: var(--accent-active);
}

/* Przycisk, który NIE MA CZEGO ZROBIĆ (dziś: zapis odpowiedzi bez zmiany —
   `guest.js`). Wygaszamy go BARWĄ, nie przezroczystością: `opacity` przygasza
   też tło pod spodem i przy nakładających się warstwach daje wynik zależny od
   tego, co akurat leży niżej.
   `cursor: not-allowed` mówi to samo wskaźnikowi, a `:hover` musi zostać
   zdjęty jawnie — inaczej nieaktywny przycisk rozjaśnia się pod kursorem
   i wygląda na klikalny. */
.btn:disabled,
.btn:disabled:hover {
  background: var(--border);
  color: var(--muted);
  cursor: not-allowed;
}

/* Drugorzędny: bez wypełnienia. Tekst w `--accent-strong`, nie w surowym
   `--accent` — ten drugi nie przechodzi 4.5:1 w paletach weselnych. */
.btn--ghost {
  background: transparent;
  border-color: var(--border-control);
  color: var(--accent-strong);
}

.btn--ghost:hover {
  background: var(--accent-soft);
  border-color: var(--border-control-hover);
  color: var(--accent-hover);
}

.btn--ghost:active {
  color: var(--accent-active);
}

/* Przycisk DRUGORZĘDNY WOBEC TREŚCI, przy której stoi — dziś „Jak dojechać"
   pod adresem (#147). Niższy i drobniejszy od `.btn`, bo nie jest akcją tego
   ekranu: panel otwiera się po zaproszenie, a nawigacja jest przy nim
   udogodnieniem. Pełnowymiarowy przycisk przeciągał na siebie wzrok z adresu,
   czyli z tego, co ma być przeczytane.

   36 px WYSOKOŚCI JEST DOLNĄ GRANICĄ, nie wartością dobraną pod oko: cel
   dotykowy ma minimum 24 px (WCAG 2.5.8) i tyle samo musi zostać po odjęciu
   obramowania. Pełne 44 px z `--control-height` należą się akcjom głównym
   (WCAG 2.5.5) i tam zostają. */
.btn--compact {
  min-height: 2.25rem;
  padding: 0 var(--space-3);
  font-size: var(--text-caption);
}

/* Przycisk z samą ikoną. Kwadrat o boku `--control-height` (44 px) — to nie
   jest wybór estetyczny, tylko minimalny cel dotykowy z WCAG 2.5.5; ikona ma
   1.15em i sama w sobie dałaby cel wielkości paznokcia. Nazwę przycisku niesie
   tekst w `.visually-hidden` (patrz `_agenda.html`): ikona bez nazwy jest dla
   czytnika ekranu przyciskiem bez etykiety. */
.btn--icon {
  min-width: var(--control-height);
  padding: 0;
}

.btn--icon .icon {
  width: 1.25em;
  height: 1.25em;
  vertical-align: 0;
}

/* Akcja niszcząca: sygnał barwny dopiero na najechanie i na ognisko. Stale
   czerwony przycisk przy każdym wierszu zamieniłby listę w ścianę ostrzeżeń,
   a wtedy czerwień przestaje cokolwiek znaczyć (ta sama zasada co przy
   `message--danger`). Barwa UZUPEŁNIA nazwę „Usuń punkt", nie zastępuje jej. */
.btn--danger:hover {
  background: var(--danger-bg);
  border-color: var(--danger);
  color: var(--danger);
}

.btn--danger:focus-visible {
  border-color: var(--danger);
  color: var(--danger);
}

/* --------------------------------------------------------------------------
   Pole formularza — struktura z `templates/partials/_field.html`
   -------------------------------------------------------------------------- */

.field + .field {
  margin-top: var(--space-5);
}

.field__label {
  display: block;
  font-weight: 700;
  margin-bottom: var(--space-2);
}

/* Podpowiedź stoi POD etykietą, przed kontrolką: użytkownik czyta ją, zanim
   zacznie pisać, a nie po tym, jak skończył. */
.field__help {
  margin: 0 0 var(--space-2);
  max-width: var(--field-max);
  color: var(--muted);
  font-size: var(--text-caption);
}

/* Wyjątki dla pól wyboru siedzą WEWNĄTRZ `:where()`, nie obok. `:not()` liczy
   specyficzność swojego argumentu, więc `:not([type="checkbox"])` dołożyłoby tu
   klasę wagi — i reguła stanu błędu niżej (o jedną klasę słabsza) przestałaby
   działać. Objaw: czerwony komunikat pod polem, ale ramka pola bez zmian.
   Wyszło na zrzucie ekranu formularza rejestracji z błędami.

   `:not(.visually-hidden)` dołożone przy #7 etap 2 i to jest wyjątek o INNYM
   powodzie niż dwa poprzednie. `.visually-hidden` ma dokładnie taką samą wagę
   co ta reguła (`.field` to jedna klasa, `:where()` zeruje swój argument), więc
   o wyniku decydowała KOLEJNOŚĆ W PLIKU — a ta reguła stoi niżej i wygrywała.
   Kontrolka pliku schowana przez `file-drop.js` dostawała wtedy `width: 100%`
   zamiast `1px`, zostając przy tym `position: absolute` z `clip-path`: element
   NIEWIDOCZNY, o szerokości całego ekranu, wystający poza prawą krawędź.
   Objaw był jeden i tylko na telefonie — ekran importu dawał się przesunąć
   w bok, czego nie robi żaden inny. Zmierzone sondą po `getBoundingClientRect()`
   na layoucie 380 px, nie zgadnięte. */
.field
  :where(input, select, textarea):where(
    :not([type="checkbox"]):not([type="radio"]):not(.visually-hidden)
  ) {
  display: block;
  width: 100%;
  max-width: var(--field-max);
  min-height: var(--control-height);
  padding: 0 var(--space-3);
  background: var(--surface-card);
  color: var(--ink);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-sm);
  transition: border-color var(--dur-fast) ease-out;
}

/* `resize: vertical` daje uchwyt w rogu kontrolki. Bez `max-height` ten uchwyt
   nie ma GÓRNEJ granicy: pole da się przeciągnąć na dowolną wysokość, wypychając
   przyciski formularza daleko poza ekran. `min-height` bez `max-height` wygląda
   na komplet i nim nie jest — druga granica jest tak samo potrzebna. 30rem to
   mniej więcej ekran laptopa; tekst dłuższy niż tyle przewija się w środku pola. */
.field textarea {
  padding: var(--space-2) var(--space-3);
  line-height: var(--leading-normal);
  resize: vertical;
  min-height: 7rem;
  max-height: 30rem;
}

/* Licznik znaków. Wstawia go `field-counter.js` (warstwa nieobowiązkowa) —
   bez skryptu limit nadal działa, tylko nie widać, ile go zostało. Stąd brak
   tego elementu w `_field.html`: pusty licznik przy wyłączonym JS byłby polem
   informacyjnym, które nigdy nie niesie informacji. */
.field__counter {
  margin: var(--space-2) 0 0;
  max-width: var(--field-max);
  color: var(--muted);
  font-size: var(--text-caption);
  /* Cyfry o stałej szerokości — inaczej licznik drga przy każdym znaku, bo
     „1" jest węższa od „8". Ruch w kącie oka podczas pisania rozprasza. */
  font-variant-numeric: tabular-nums;
}

/* Blisko limitu licznik przestaje być tłem i staje się ostrzeżeniem. Kolor NIE
   jest tu jedynym nośnikiem (WCAG 1.4.1): treść i tak podaje liczbę, a czytnik
   ekranu dostaje osobny komunikat z `role="status"`. */
.field__counter--near-limit {
  color: var(--danger);
  font-weight: 700;
}

/* Lista wyboru bez systemowej strzałki. Natywna kontrolka rysuje ją inaczej
   w każdej przeglądarce i to ona najbardziej zdradza „domyślny formularz".
   Strzałkę rysujemy DWOMA gradientami, a nie obrazkiem: `url()` w arkuszu jest
   zabroniony (test `test_stylesheet_does_not_reach_for_external_assets` łapie
   każdy `//`, a `data:` z SVG-iem niesie `xmlns` z adresem). Kolor bierze
   `currentColor`, więc strzałka blednie razem z tekstem podpowiedzi. */
.field select {
  appearance: none;
  padding-right: var(--space-7);
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 1.35rem center, right 1rem center;
  background-size: 0.35rem 0.35rem;
  background-repeat: no-repeat;
}

/* Pusty wybór („Wybierz typ wydarzenia") ma wyglądać jak PODPOWIEDŹ, nie jak
   wybrana wartość — inaczej formularz twierdzi, że użytkownik już zdecydował.
   Stan bierze się z `:invalid` na polu wymaganym, czyli z tego samego atrybutu,
   który i tak steruje walidacją; osobna klasa byłaby drugim źródłem prawdy. */
.field select:required:invalid {
  color: var(--muted);
}

.field :where(input, select, textarea):hover {
  border-color: var(--border-control-hover);
}

/* KONTROLKA WYŁĄCZONA MUSI WYGLĄDAĆ NA WYŁĄCZONĄ — znalezione zrzutem przy #299.

   Przeglądarka szarzy `:disabled` sama, ale robi to TŁEM — a tło nadajemy tu
   jawnie (`--surface-card`), więc jej styl przepada. Do #299 nie miało to
   objawu, bo zamrożone były OBA pola terminu naraz i nie było ich z czym
   porównać. Od #299 obok wyłączonej daty stoi CZYNNA godzina: dwie kontrolki
   wyglądające identycznie, z których jedna po cichu nie przyjmuje wpisu, to
   jest ekran wyglądający na zepsuty.

   Kursor i brak reakcji na `:hover` też są tu treścią, nie ozdobą: to one
   odpowiadają na pytanie „czy ja czegoś nie klikam nie tak". */
.field :where(input, select, textarea):disabled {
  background: var(--surface-page);
  color: var(--muted);
  border-style: dashed;
  cursor: not-allowed;
}

.field :where(input, select, textarea):disabled:hover {
  border-color: var(--border-control);
}

/* Stan błędu jedzie na atrybucie, który Django i tak wystawia dla czytników
   ekranu — nie ma drugiego źródła prawdy, które mogłoby się rozjechać. */
.field [aria-invalid="true"] {
  border-color: var(--danger);
  border-width: 2px;
}

/* POLE WYBORU I PRZYCISK RADIOWY NIE MAJĄ OBRAMOWANIA, KTÓRE DAŁOBY SIĘ
   POMALOWAĆ — i to jest dziura, nie niuans (znaleziona przy #21).

   Reguła bazowa pól tekstowych wyklucza `[type="checkbox"]` i `[type="radio"]`,
   więc kontrolka zostaje NATYWNA: przeglądarka rysuje ją sama, a `border` na
   `appearance: auto` po prostu ignoruje. Deklaracja wyżej wykonuje się wtedy
   bez żadnego skutku widocznego — czyli deklaracja martwa, tylko z innej
   strony niż zwykle: tam obramowanie ukrywała specyficzność, tu nie ma
   czego ukrywać.

   Ekran potwierdzenia anonimizacji (#21) składa się WYŁĄCZNIE z takich pól,
   więc jego stan błędu nie miał ani jednego sygnału poza czerwonym zdaniem pod
   spodem. `outline` działa na kontrolce natywnej i nie rusza układu (nie zajmuje
   miejsca), a barwa UZUPEŁNIA komunikat przy polu, nigdy go nie zastępuje
   (WCAG 1.4.1). Reguła jest OGÓLNA, bo dziura jest ogólna — dotyczy każdego
   wymaganego pola wyboru w projekcie, także zgód w formularzu rejestracji. */
.field :where(input[type="checkbox"], input[type="radio"])[aria-invalid="true"] {
  outline: 2px solid var(--danger);
  outline-offset: 2px;
}

.field__errors {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
  max-width: var(--field-max);
  color: var(--danger);
  font-size: var(--text-caption);
  font-weight: 700;
}

/* Podświetlenie miejsca błędu na czas jednej animacji (#196). Klasę zakłada
   i zdejmuje `error-focus.js`; w HTML-u nie stoi nigdy, bo opisuje ZDARZENIE
   (formularz właśnie wrócił z błędem), a nie stan pola.

   `outline`, a nie ramka ani tło: nie zajmuje miejsca, więc podświetlenie
   niczym nie przesuwa w kolumnie pól — a to jest warunek, skoro dzieje się
   dokładnie w chwili, w której użytkownik czyta, gdzie się pomylił. Barwa
   UZUPEŁNIA komunikat pod polem, nigdy go nie zastępuje (WCAG 1.4.1).

   JEDNA deklaracja klatki, bez `to` — stanem po animacji jest stan
   SPOCZYNKOWY, czyli obrys przezroczysty (ta sama zasada co przy
   `scan-result-in`). Dzięki temu pole nie ma jak zostać obrysowane na czerwono,
   gdy animacja nie dojdzie do końca albo zostanie wygaszona przez
   `prefers-reduced-motion` — a wygaszenie jest tu poprawnym zachowaniem:
   przewinięcie do błędu zostaje, miga wyłącznie ozdoba. */
.flagged {
  outline: 2px solid transparent;
  outline-offset: var(--space-2);
  animation: error-flag 1.4s ease-out;
}

@keyframes error-flag {
  from,
  60% {
    outline-color: var(--danger);
  }
}

/* Zgoda / pole wyboru: etykieta OBOK kontrolki, nie nad nią — nad polem
   wyboru wisiałaby w powietrzu, oderwana od kwadracika, który opisuje. */
.field__inline {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
}

.field__inline input[type="checkbox"] {
  width: 1.15rem;
  height: 1.15rem;
  margin: 0.28rem 0 0;
  flex: none;
  accent-color: var(--accent-strong);
}

.field__inline .field__label {
  margin-bottom: 0;
  font-weight: 400;
}

/* Grupa przycisków radiowych (widget z `use_fieldset`) — od #99.

   `<fieldset>` przychodzi z domyślną ramką i domyślnymi marginesami przeglądarki,
   których nie da się „nie zauważyć": bez tego zerowania każda grupa kontrolek
   dostaje obwódkę, jakiej nie ma żaden inny element formularza. Legenda jest
   zwykłą `.field__label`, więc etykieta grupy wygląda dokładnie tak samo jak
   etykieta pojedynczego pola — różnica jest w semantyce (`<legend>` opisuje
   WSZYSTKIE kontrolki sekcji), nie w wyglądzie.

   Django renderuje grupę jako `<div>` z jednym `<div>` na opcję, a w nim
   `<label>` obejmującą kontrolkę i napis. Selektory celują w tę strukturę
   WPROST (`> div`, `> div > div`), zamiast prosić o dodatkowe klasy: znaczniki
   generuje Django, więc klas w nich nie ma i nie będzie. */
.field__group {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

/* Opcje w jednym wierszu, dopóki się mieszczą. Trzy krótkie odpowiedzi obok
   siebie czyta się jak zbiór do wyboru; jedna pod drugą — jak listę zadań.

   ODSTĘP MIĘDZY WIERSZAMI ZEROWY, i to nie jest ściśnięcie na siłę: każda opcja
   ma już `min-height: var(--control-height)` (44 px, cel dotykowy WCAG 2.5.5),
   więc rozdziela je czterdzieści kilka pikseli własnej wysokości. Dołożony
   `row-gap` sumował się z tym do ponad pięćdziesięciu i grupa trzech odpowiedzi
   zajmowała pół ekranu telefonu — a przy pytaniu, które ma się przeczytać
   jednym rzutem oka, to jest różnica między „wybór" a „formularz".

   Odstęp POZIOMY zostaje: tam nic nie rozdziela opcji poza nim. */
.field__group > div {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--space-5);
}

/* Cel dotykowy pełnej wysokości (WCAG 2.5.5): sam kwadracik kontrolki ma
   ~1.1 rem, czyli bok wielkości paznokcia. Powiększamy OBSZAR — a obszarem jest
   etykieta obejmująca kontrolkę razem z napisem, nie sama kontrolka. Przycisk
   radiowy narysowany na 44 px wygląda jak element z innej epoki. */
.field__group > div > div > label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--control-height);
  /* Etykieta OPCJI nie jest etykietą POLA. `.field__label` (czyli `<legend>`)
     zostaje jedynym pogrubionym napisem sekcji, bo to on mówi, czego dotyczy
     wybór. */
  font-weight: 400;
}

.field__group input[type="radio"] {
  width: 1.15rem;
  height: 1.15rem;
  margin: 0;
  flex: none;
  accent-color: var(--accent-strong);
}

/* Sekcja formularza. `<fieldset>`/`<legend>` to semantyka, nie ozdoba: czytnik
   ekranu ogłasza legendę przy każdym polu sekcji, więc „Hasło" w sekcji
   „Dostęp" nie myli się z „Hasło" w żadnej innej. */
.fieldset {
  margin-top: var(--space-6);
}

/* Przerwa MIĘDZY sekcjami większa niż nad pierwszą: to ona ma nieść podział. */
.fieldset + .fieldset {
  margin-top: var(--space-7);
}

/* Zdanie wprowadzające WEWNĄTRZ sekcji — dziś jedno, nad zgodami przy
   rejestracji (#86). Odstęp z góry większy niż z dołu, bo należy do pól pod
   sobą, nie do tych nad. */
.fieldset__note {
  margin-top: var(--space-6);
  margin-bottom: var(--space-3);
  color: var(--muted);
  font-size: var(--text-caption);
}

.fieldset__legend {
  padding: 0;
  margin-bottom: var(--space-4);
  font-family: var(--font-display);
  font-size: var(--text-lead);
  font-weight: 700;
  line-height: var(--leading-tight);
}

/* --------------------------------------------------------------------------
   PODGLĄD PALETY POD LISTĄ WYBORU (#70)

   Jedyne miejsce w panelu, w którym barwy WARSTWY PUBLICZNEJ pokazują się
   Organizatorowi — i jedyne, w którym mają prawo się pokazać. Panel zostaje
   neutralny (ADR-027): paleta jest tu TREŚCIĄ karty podglądu, a nie
   identyfikacją ekranu, dlatego `data-theme` siada na tej karcie, nigdy wyżej.

   ANI JEDNEJ WARTOŚCI KOLORU W TEJ SEKCJI. Wszystko poniżej bierze tokeny,
   które blok `:root, [data-theme]` wylicza NA KARCIE — czyli siódma paleta
   pokaże się tu sama, bez dopisywania reguł.

   DWA STANY TEJ SAMEJ SEKCJI. Bez skryptu widać wszystkie dopuszczone palety,
   jedna pod drugą i bez ruchu — bo serwer nie wie, co Organizator wybierze.
   Klasę `--live` nakłada dopiero `theme-preview.js` i dopiero ona zwija sekcję
   do jednej karty. Kolejność jest tu wymuszona: gdyby chowanie stało w regule
   bazowej, przeglądarka bez skryptu nie pokazałaby ŻADNEJ palety.
   -------------------------------------------------------------------------- */

.theme-preview {
  display: grid;
  gap: var(--space-3);
  margin-top: var(--space-3);
}

/* Karta stoi na tle karty panelu, więc obrys jest jedyną rzeczą oddzielającą
   jasną powierzchnię palety od jasnej powierzchni panelu. `--border` liczy się
   z atramentu TEJ palety (28 % na jej powierzchni), więc kreska trzyma się
   w każdym z sześciu zestawów — sprawdzone na zrzutach dla wszystkich. */
.theme-preview__card {
  display: grid;
  gap: var(--space-3);
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-page);
  color: var(--ink);
}

/* Nazwa palety KROJEM tej palety — to jest połowa odpowiedzi na pytanie „czym
   różni się zestaw weselny od firmowego". Sam kolor jej nie daje: szeryfy widać
   dopiero na napisie. */
.theme-preview__name {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-lead);
  line-height: var(--leading-tight);
}

.theme-preview__swatches {
  display: flex;
  gap: var(--space-2);
}

/* Próbka koloru NIE JEST kontrolką i nie da się jej kliknąć — cała sekcja jest
   `aria-hidden`, a wyboru dokonuje się na liście wyżej. Obrys jest tu po to,
   żeby najjaśniejsza próbka nie zlewała się z powierzchnią karty. */
.theme-preview__swatch {
  width: 2rem;
  height: 2rem;
  border: 1px solid var(--border);
  border-radius: 50%;
}

.theme-preview__swatch--surface {
  background: var(--surface);
}

.theme-preview__swatch--alt {
  background: var(--surface-alt);
}

/* Surowy `--accent` wolno tu postawić WYŁĄCZNIE dlatego, że jest próbką samego
   siebie — nic na nim nie stoi i nic z niego nie czyta. Wszędzie indziej niesie
   treść dopiero `--accent-strong` (patrz tokeny pochodne wyżej). */
.theme-preview__swatch--accent {
  background: var(--accent);
}

.theme-preview__swatch--ink {
  background: var(--ink);
}

/* Makieta zaproszenia: powierzchnia KARTY na tle STRONY, czyli ta sama para co
   u Gościa. Zawija się na wąskim ekranie, bo napis „Potwierdzam obecność" przy
   nagłówku nie mieści się w jednym wierszu na 380 px. */
.theme-preview__sample {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4);
  border-radius: var(--radius-sm);
  background: var(--surface-card);
}

.theme-preview__heading {
  font-family: var(--font-display);
  font-size: var(--text-title);
  line-height: var(--leading-tight);
}

.theme-preview__button {
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-sm);
  background: var(--accent-strong);
  color: var(--surface-card);
  font-size: var(--text-caption);
}

/* --- Ze skryptem: jedna karta i wejście z ruchem ------------------------- */

.theme-preview--live .theme-preview__card {
  display: none;
}

.theme-preview--live .theme-preview__card.is-current {
  display: grid;
  animation: theme-preview-in var(--dur) ease-out both;
}

@keyframes theme-preview-in {
  from {
    opacity: 0;
    transform: translateY(-0.25rem);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

/* Kolory wskakują PO KOLEI, bo to one są odpowiedzią na pytanie zadane przy
   liście — a cztery próbki pojawiające się naraz czyta się jak jeden prostokąt.
   Opóźnienie idzie regułami `:nth-child`, nie atrybutem `style` w szablonie:
   inline blokuje ścisłe CSP (ta sama zasada co przy wejściu listy gości). */
@keyframes theme-preview-pop {
  from {
    opacity: 0;
    transform: scale(0.4);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

.theme-preview--live .is-current .theme-preview__swatch {
  animation: theme-preview-pop var(--dur) ease-out both;
}

.theme-preview--live .is-current .theme-preview__swatch:nth-child(1) {
  animation-delay: 60ms;
}

.theme-preview--live .is-current .theme-preview__swatch:nth-child(2) {
  animation-delay: 120ms;
}

.theme-preview--live .is-current .theme-preview__swatch:nth-child(3) {
  animation-delay: 180ms;
}

.theme-preview--live .is-current .theme-preview__swatch:nth-child(4) {
  animation-delay: 240ms;
}

/* --------------------------------------------------------------------------
   Formularz panelu — siatka pól

   Pola panelu NIE mogą zostać przy `--field-max` (24 rem): karta panelu ma do
   68 rem, więc kolumna kontrolek zajmowałaby jedną trzecią, a reszta byłaby
   pustką. To ten sam formularz co na ekranie logowania, tylko w zupełnie innym
   pojemniku — stąd osobna siatka zamiast szerszej wartości globalnej.

   O tym, co jest pełnej szerokości, decyduje TYP KONTROLKI (`:has()`), bo
   szablon renderuje pola pętlą i nie zna ich nazw. Domyślnie pole zajmuje cały
   wiersz; wyjątkiem są daty — „Początek" i „Koniec" należą do siebie i czyta się
   je razem.
   -------------------------------------------------------------------------- */

.card--form {
  max-width: 48rem;
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-5);
}

.form-grid > .field {
  grid-column: 1 / -1;
}

/* PÓŁ WIERSZA DLA KAŻDEJ DATY. Selektor wymienia dwa kształty kontrolki, bo od
   #288 termin wydarzenia jest polem ZŁOŻONYM (`<fieldset class="field__group">`
   z datą i osobną godziną), a termin zapisów został przy jednym wybieraku
   `datetime-local`. Trzy pola, dwa kształty, jedna reguła układu. */
.form-grid > .field:has(input[type="datetime-local"]),
.form-grid > .field:has(.datetime) {
  grid-column: auto;
}

/* KONTROLKI DATY STOJĄ NA JEDNYM POZIOMIE, cokolwiek stoi nad nimi. Bez tego
   „Początek" z czterema wierszami podpowiedzi spycha swoje pole niżej niż
   „Koniec" z trzema — a to jedna para, którą czyta się razem, i porównanie obu
   dat zaczyna się od szukania drugiego pola wzrokiem.

   Wyrównanie robi `subgrid`: oba pola dzielą CZTERY wiersze siatki nadrzędnej
   (etykieta, podpowiedź, kontrolka, błędy), więc wysokość każdego wiersza
   wyznacza wyższe z pól, a niższe dostaje pustkę w swoim.

   Wiersze są przypisane JAWNIE, bo pole bez podpowiedzi wciągnęłoby kontrolkę
   do wiersza podpowiedzi sąsiada — czyli dokładnie tam, gdzie jej nie ma stać.

   Odrzucone: dosunięcie kontrolki do dołu pola (`margin-top: auto` przy
   rozciągniętym `.field`). Wygląda tak samo, dopóki żadne z pól nie ma błędu —
   a błąd („koniec musi być późniejszy niż początek") wisi POD kontrolką
   i przesuwa ją w górę tylko w jednym z pól. Rozjazd wracałby więc w tym
   jedynym momencie, w którym obie daty ogląda się naprawdę uważnie.

   `row-gap: 0` — odstępy niosą marginesy etykiety i podpowiedzi, te same co
   poza siatką; odziedziczony `gap` dołożyłby do nich 1,5 rem na każdy wiersz. */
@supports (grid-template-rows: subgrid) {
  .form-grid > .field:has(input[type="datetime-local"]),
  .form-grid > .field:has(.datetime) {
    display: grid;
    grid-template-rows: subgrid;
    grid-row: span 4;
    row-gap: 0;
  }

  .form-grid > .field:has(input[type="datetime-local"]) > .field__label {
    grid-row: 1;
  }

  .form-grid > .field:has(input[type="datetime-local"]) > .field__help {
    grid-row: 2;
  }

  .form-grid > .field > input[type="datetime-local"] {
    grid-row: 3;
  }

  .form-grid > .field:has(input[type="datetime-local"]) > .field__errors {
    grid-row: 4;
  }

  /* TO SAMO WYRÓWNANIE DLA POLA ZŁOŻONEGO (#288), tylko o piętro głębiej.
     Etykieta i podpowiedź stoją tu WEWNĄTRZ `<fieldset>` (`partials/_field.html`
     daje im `<legend>`, bo `<label for>` wskazałaby jedną z dwóch kontrolek),
     więc do wierszy siatki nadrzędnej sięgają przez ZAGNIEŻDŻONY `subgrid`:
     `.field` bierze cztery wiersze, `<fieldset>` trzy pierwsze z nich, a błędy
     zostają w czwartym — po staremu, poza sekcją. Bez tego „Początek" z dłuższą
     podpowiedzią spychałby swoje kontrolki niżej niż „Koniec", a to jest para,
     którą czyta się jednym rzutem oka. */
  .form-grid > .field:has(.datetime) > .field__group {
    display: grid;
    grid-template-rows: subgrid;
    grid-row: 1 / 4;
    row-gap: 0;
  }

  .form-grid > .field:has(.datetime) > .field__group > .field__label {
    grid-row: 1;
  }

  .form-grid > .field:has(.datetime) > .field__group > .field__help {
    grid-row: 2;
  }

  .form-grid > .field:has(.datetime) > .field__group > .datetime {
    grid-row: 3;
  }

  .form-grid > .field:has(.datetime) > .field__errors {
    grid-row: 4;
  }
}

/* --------------------------------------------------------------------------
   Termin: data i osobna, opcjonalna godzina (#288)

   DWIE KONTROLKI OBOK SIEBIE, bo to jedna odpowiedź podana w dwóch krokach —
   jedna pod drugą czytałyby się jak dwa różne pytania. Godzina jest węższa:
   „18:00" ma cztery znaki, a data dziesięć, więc równe kolumny dawałyby przy
   godzinie pół pola pustki.

   ZAWIJANIE ZOSTAJE (`flex-wrap`) i jest tu jedynym zabezpieczeniem układu na
   wąskim ekranie: przy 390 px pole terminu ma pełną szerokość wiersza, ale przy
   powiększonej czcionce systemowej dwie kontrolki i tak przestają się mieścić —
   wtedy godzina schodzi pod datę zamiast wypychać kartę w poziom.

   SELEKTOR JEST DWUCZŁONOWY CELOWO. Wyżej stoi `.field__group > div` (opcje
   w grupie przycisków radiowych), które ma specyficzność 0-1-1 — czyli WYŻSZĄ
   niż samo `.datetime`, mimo że stoi wcześniej w pliku. Sama klasa przegrałaby
   więc o `gap` i `flex-wrap` z regułą napisaną dla zupełnie innej kontrolki. */
.field__group > .datetime {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: end;
}

.datetime__part {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.datetime__part--date {
  flex: 1 1 10rem;
}

.datetime__part--time {
  flex: 0 1 7rem;
}

/* Podpis części, nie etykieta pola: `<legend>` mówi „Początek" i to on zostaje
   jedynym pogrubionym napisem sekcji. Ta sama zasada co przy opcjach w grupie
   przycisków radiowych wyżej. */
.datetime__label {
  color: var(--muted);
  font-size: var(--text-caption);
  font-weight: 400;
}

/* Odstęp niesie `gap`, nie margines sąsiedztwa — inaczej pola w drugim wierszu
   dostałyby przerwę podwójną. */
.form-grid > .field + .field {
  margin-top: 0;
}

/* W siatce szerokość wyznacza kolumna, a nie `--field-max`. Podpowiedź i błędy
   idą razem z kontrolką, inaczej łamałyby się węziej niż pole, które opisują. */
.form-grid .field :where(input, select, textarea),
.form-grid .field :where(.field__help, .field__errors) {
  max-width: 100%;
}

@media (max-width: 40rem) {
  .form-grid {
    grid-template-columns: 1fr;
  }

  .form-grid > .field:has(input[type="datetime-local"]),
  .form-grid > .field:has(.datetime) {
    grid-column: 1 / -1;
  }
}

.form__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  margin-top: var(--space-6);
}

/* Rząd akcji na dole karty — dziś jedna pozycja, ale rząd, bo druga akcja
   dołoży się bez zmiany układu.

   `wrap` i `gap` są tu na tę drugą pozycję, nie na ozdobę: przy 380 px dwa
   przyciski nie mieszczą się w jednym wierszu, a przycięty do połowy jest
   gorszy niż stojący pod spodem.

   CZEGO W TYM RZĘDZIE NIE MA I DLACZEGO: plakietek z liczbami. Pigułka
   postawiona przy obrysowanym przycisku czyta się jak drugi przycisk o innym
   kształcie — ta sama forma w dwóch różnych rolach. Liczby należą do zdania
   nad listą, gdzie stoją pozostałe liczby tej karty; poszerzanie odstępu
   między nimi a przyciskiem problemu nie rozwiązywało, bo nie był w odstępie.

   Odstęp od treści karty jest MNIEJSZY niż w `.form__actions`: tam pod spodem
   kończy się formularz i pauza oddziela go od reszty strony, tutaj rząd należy
   do karty, w której stoi. */
.card__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-4);
}

/* Pojemnika NIE MA, gdy nie ma czego w nim postawić — rozstrzyga o tym warunek
   w SZABLONIE, a nie reguła `:empty`. Ta druga nie zadziałałaby tutaj i jest to
   pułapka warta zapamiętania: warunek szablonu zostawia w środku znaki nowej
   linii, więc element z samymi wcięciami NIE JEST pusty w rozumieniu CSS-u —
   a pusty rząd flex nadal niesie swój `margin-top`. */

.form--inline {
  display: inline;
}

/* --------------------------------------------------------------------------
   Komunikaty — `django.contrib.messages` oraz błędy niezwiązane z polem
   -------------------------------------------------------------------------- */

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

.messages:last-child {
  margin-bottom: 0;
}

.message {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-left: 4px solid var(--accent-strong);
  border-radius: var(--radius-sm);
}

/* Barwa UZUPEŁNIA ikonę i grubszą krawędź, nie zastępuje ich (WCAG 1.4.1). */
.message.error,
.message.warning,
.message--danger {
  background: var(--danger-bg);
  border-color: color-mix(in srgb, var(--danger) 30%, var(--danger-bg));
  border-left-color: var(--danger);
  border-left-width: 6px;
  font-weight: 700;
}

.message.success,
.message--success {
  background: var(--success-bg);
  border-color: color-mix(in srgb, var(--success) 30%, var(--success-bg));
  border-left-color: var(--success);
}

.message.error .message__icon,
.message.warning .message__icon,
.message--danger .message__icon {
  color: var(--danger);
}

.message.success .message__icon,
.message--success .message__icon {
  color: var(--success);
}

.message__icon {
  margin-top: 0.2em;
}

/* --------------------------------------------------------------------------
   Ekrany listowe i szczegółowe panelu (#6)
   -------------------------------------------------------------------------- */

/* Nagłówek strony i akcja główna w jednym rzędzie; przy wąskim ekranie akcja
   schodzi pod nagłówek zamiast go ściskać. */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
}

/* Blok nagłówkowy: okruszek, tytuł, status. Odstępy niesie `gap`, a nie
   marginesy akapitów — te są w tym miejscu zerowane przez regułę „ostatni
   akapit bez marginesu" i wiersz statusu przyklejał się do zejścia liter
   tytułu. Widać to dopiero na ekranie, nie w HTML-u. */
.page-header {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.page-header > * {
  margin: 0;
}

/* Nagłówek bywa opakowany w blok razem z okruszkiem i statusem — stąd selektor
   potomka, nie dziecka. */
.toolbar :where(h1, h2) {
  margin: 0;
}

/* Przewijanie w poziomie należy do TABELI, nie do strony: cztery kolumny nie
   mieszczą się na telefonie, a strona jadąca w bok zabiera ze sobą nagłówek
   i nawigację. */
.table__scroll {
  overflow-x: auto;
}

.table {
  width: 100%;
  border-collapse: collapse;
  text-align: left;
}

.table :where(th, td) {
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}

.table thead th {
  border-bottom-width: 2px;
  white-space: nowrap;
}

.table tbody tr:last-child :where(th, td) {
  border-bottom: 0;
}

/* Wiersze naprzemienne — jedno z trzech dozwolonych zastosowań `--surface-alt`. */
.table tbody tr:nth-child(even) {
  background: var(--surface-alt);
}

/* Znacznik statusu. Barwa jest tu WYŁĄCZNIE tłem tekstu, który i tak niesie całą
   treść („Szkic", „Opublikowane") — status czytelny sam z siebie nie łamie
   WCAG 1.4.1, bo nie ma informacji przekazanej samym kolorem. */
.badge {
  display: inline-block;
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--border-control);
  border-radius: 999px;
  background: var(--accent-soft);
  font-size: var(--text-caption);
  font-weight: 700;
  white-space: nowrap;
}

/* Plakietka WIELOCZŁONOWA (#99: „potwierdzone · odmowa · bez odpowiedzi").
   `white-space: nowrap` z reguły bazowej jest słuszny przy dwóch liczbach
   i szkodliwy przy trzech: na telefonie plakietka ma wtedy 394 px przy 345 px
   kolumny i wypycha CAŁY pasek poza ekran — czyli rozjeżdża się także ekran
   Organizatora, który tych liczb nie czyta. Zmierzone w ramce 360 px,
   nie oszacowane. */
.badge--wrap {
  white-space: normal;
}

.pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
}

/* Zakres listy wydarzeń: „Wydarzenia (3)" ↔ „Archiwum (2)" (#258).

   NIE PRZYCISKI, mimo że to nawigacja: przyciski w tym panelu znaczą CZYNNOŚĆ
   („Nowe wydarzenie", „Opublikuj"), a to jest zmiana widoku tej samej listy.
   Rząd obrysowanych kontrolek nad tabelą przykryłby przy okazji jedyny
   prawdziwy przycisk tego ekranu.

   Bieżący zakres jest TEKSTEM, nie wyszarzonym łączem: łącze prowadzące tam,
   gdzie się już jest, to jedyny odnośnik na stronie, który nic nie robi. */
.list-scopes {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin-bottom: var(--space-4);
  font-size: var(--text-caption);
}

.list-scopes__current {
  font-weight: 600;
}

/* Okruszek nad nagłówkiem strony — kontekst („Wydarzenia / Wesele”) bez
   zabierania miejsca nagłówkowi.

   Niesie TYTUŁ WYDARZENIA, czyli tekst od użytkownika — stąd `overflow-wrap`
   z tego samego powodu co przy osi agendy (patrz komentarz przy
   `.timeline__title`). Okruszek jest na każdym ekranie panelu, więc bez tego
   jedna nazwa bez spacji rozpycha w bok wszystkie naraz. */
.eyebrow {
  margin: 0 0 var(--space-1);
  color: var(--muted);
  font-size: var(--text-caption);
  overflow-wrap: anywhere;
}

/* Dane wydarzenia w BLOKACH, nie w dwóch kolumnach.

   Etykieta stoi NAD wartością i jest drobna: adres albo termin bywają długie,
   a w układzie „wąska kolumna etykiet + reszta" łamały się na ćwiartce
   szerokości karty, przy pustym wierszu obok. Bloki układają się w rzędzie po
   tyle, ile się mieści, i schodzą pod siebie same — bez progu w media query. */
.detail-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: var(--space-5) var(--space-6);
  margin: 0;
}

.detail-item dt {
  margin-bottom: var(--space-1);
  color: var(--muted);
  font-size: var(--text-caption);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.detail-item dd {
  margin: 0;
}

/* Pasek pod liczbami w tej samej karcie (#311) — dziś przekroczenie limitu na
   ekranie galerii. `detail-grid` ma zerowe marginesy (odstępy niesie `gap`
   między komórkami), a pasek jest akapitem bez marginesu górnego, więc bez tej
   reguły czerwony blok przyklejał się do ostatniego wiersza liczb, choć opisuje
   je wszystkie. Selektor sąsiedztwa, nie klasa na akapicie: każdy przyszły
   komunikat postawiony pod siatką wartości potrzebuje dokładnie tego samego. */
.detail-grid + .message {
  margin-top: var(--space-5);
  margin-bottom: 0;
}

/* Druga linia wartości (adres pod nazwą miejsca) — ten sam blok, niższa ranga. */
.detail-item__secondary {
  display: block;
  color: var(--muted);
  font-size: var(--text-caption);
}

/* Akcja pod wartością — dziś „Jak dojechać" pod adresem (#147). Własny wiersz,
   nie doklejenie do adresu: cel dotykowy przyklejony do tekstu trafia się
   razem z nim. Kształt i rozmiar przychodzą z `.btn--ghost.btn--compact`. */
.detail-item__action {
  margin-top: var(--space-3);
}

/* Brak terminu w wydarzeniu, które ma być opublikowane, to nie jest neutralna
   informacja — to jedyna rzecz blokująca publikację. Kolor UZUPEŁNIA treść
   komunikatu, nie zastępuje jej (WCAG 1.4.1): tekst mówi wprost, czego brakuje. */
.text-warning {
  color: var(--danger);
  font-weight: 700;
}

/* Akcja nieodwracalna: osobno, na końcu, jako łącze. Przycisk obok „Edytuj"
   sugerowałby, że obie akcje są tej samej wagi. */
.danger-zone {
  margin-top: var(--space-6);
  font-size: var(--text-caption);
}

.danger-zone a {
  color: var(--danger);
}

/* --------------------------------------------------------------------------
   Agenda wydarzenia (#6, etap 2) — OŚ CZASU

   Trzecie podejście do tego ekranu i jedyne, które odpowiada na pytanie, po co
   ktoś tu w ogóle zagląda. Dwa poprzednie (goły tekst, potem kafle z ramkami)
   pokazywały punkty jako niezależne rekordy — a agendę czyta się po to, żeby
   zobaczyć NASTĘPSTWO: co po czym i ile trwa. Stąd jedna ciągła linia, węzły
   na niej i godziny w stałej kolumnie po lewej.

   Cztery kolumny wiersza siedzą w zmiennych, bo tę samą siatkę musi trzymać
   pięć rzeczy naraz: wiersz, węzeł „dodaj", linia osi (pozycjonowana bezwzględnie
   względem tych szerokości), wariant podglądu i układ mobilny. Rozsypanie tych
   liczb po regułach kończy się osią biegnącą obok kropek zamiast przez nie.
   -------------------------------------------------------------------------- */

/* Nagłówek sekcji: tytuł i licznik w jednym rzędzie, oddzielone od osi kreską
   na pełną szerokość karty. Kreska nie jest ozdobą — bez niej „Harmonogram"
   i pierwszy nagłówek dnia czytają się jak jedna dwupoziomowa linia. */
/* Karta agendy jest WĘŻSZA od panelu (68rem). Na pełnej szerokości między
   tytułem punktu a jego przyciskami zostawało ~800 px pustego pola — wiersz
   przestawał się czytać jako jeden obiekt i trzeba było wodzić wzrokiem, żeby
   sprawdzić, którego punktu dotyczy kosz. To była pierwsza uwaga z przeglądu
   i nie da się jej naprawić samym stylem przycisku. */
.agenda {
  max-width: 56rem;
}

.agenda__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-6);
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--border);
}

.agenda__bar > * {
  margin: 0;
}

.agenda__day + .agenda__day {
  margin-top: var(--space-6);
}

/* Nagłówek dnia PRZYKLEJA SIĘ do góry ekranu przy przewijaniu: przy agendzie na
   trzy doby po kilkunastu punktach przestaje być wiadomo, którą się czyta.
   Tło jest obowiązkowe — element przyklejony bez tła nachodzi na przewijaną
   pod nim treść i obie warstwy stają się nieczytelne.

   Dzień jest widoczny ZAWSZE, także przy agendzie jednodniowej. Wcześniej data
   pokazywała się przy godzinie i tylko wtedy, gdy punkty wypadały w różnych
   dniach — harmonogram jednodniowy nie mówił, o który dzień chodzi. */
.agenda__date {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin: 0 0 var(--space-4);
  padding: var(--space-2) 0;
  background: var(--surface-card);
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.agenda__date::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

/* --------------------------------------------------------------------------
   Oś: siatka wiersza i linia
   -------------------------------------------------------------------------- */

.timeline {
  --time-col: 4rem; /* mieści „11:26" w stopniu `--text-lead` */
  --rail-col: 1.5rem;

  /* ODSTĘP MIĘDZY KOLUMNAMI — godzina, oś i treść rozdzielone SIATKĄ, a nie
     dopełnieniem na poszczególnych elementach.

     Wersja z `padding-right` na godzinie i `padding-left` na treści wyglądała
     identycznie, dopóki godzina mieściła się w swojej kolumnie. Gdy przestawała
     (szerszy krój, większy stopień, „11:26" zamiast „9:00"), tekst zjadał własne
     dopełnienie i dosuwał się do kropki na styk — a kolumna osi zostawała na
     miejscu, bo jej pozycję wyznacza siatka, nie treść. Objaw był widoczny
     wyłącznie w widoku Gościa i wyłącznie przy wąskim ekranie, czyli tam, gdzie
     nikt nie porównuje obu agend obok siebie.

     `gap` leży POZA ścieżkami siatki, więc żadna treść nie ma jak go zabrać. */
  --col-gap: var(--space-3);

  /* POZIOME dopełnienie wiersza — pas podświetlenia ma być wcięty, a nie sięgać
     krawędzi karty. Bez niego przyciski akcji dotykają prawej krawędzi, a opis
     punktu dochodzi do nich bez odstępu: przy długim tekście litery kończą się
     tam, gdzie zaczyna się ikona. Ta sama zasada, na której stoi karta grupy
     na liście gości (`.guest-group` z własnym `padding`) — dwa ekrany tego
     samego panelu mają wyglądać na zrobione według jednej reguły. */
  --row-pad: var(--space-3);

  /* PIONOWE dopełnienie wiersza — powietrze między przyciskiem akcji a krawędzią
     pasa podświetlenia. Domyślnie zero: pas widać wyłącznie tam, gdzie w wierszu
     jest co kliknąć (patrz `.agenda .timeline` niżej).

     Zero Z JEDNOSTKĄ, i to nie jest ozdoba: wartość wchodzi do `calc()` przy
     `--rail-cap`, a dodawanie bezwymiarowego zera do długości jest w `calc()`
     błędem — cała deklaracja przepada wtedy po cichu razem z zakończeniem osi. */
  --row-pad-y: 0rem;

  /* Odstęp do NASTĘPNEGO punktu. Leży poza pasem podświetlenia — pas ma obejmować
     punkt, a nie punkt i lukę pod nim (powód przy `.timeline__row` niżej). */
  --row-gap: var(--space-6);

  /* Oś węzła przesuwa się RAZEM z siatką: `::before` jest pozycjonowany
     absolutnie względem pola dopełnienia wiersza, więc bez `--row-pad` w tym
     rachunku kreska zostałaby na miejscu, a kropki odjechałyby w prawo. Objaw:
     linia biegnąca obok węzłów zamiast przez nie.

     `--col-gap` wchodzi do rachunku RAZ — między kolumną godziny a kolumną osi.
     Pominięcie go przesunęłoby kreskę o pełny odstęp w lewo względem kropek. */
  --rail-x: calc(
    var(--row-pad) + var(--time-col) + var(--col-gap) + var(--rail-col) / 2
  );
  --rail-w: 1px;

  /* WSPÓLNA wysokość pierwszego wiersza tekstu — godzina, węzeł i tytuł mają
     leżeć w jednej linii, a każde z nich ma inny stopień pisma. Przy zwykłym
     `line-height` liczonym mnożnikiem (20 px × 1.2 kontra 16 px × 1.55) pola
     wierszowe wychodzą różnej wysokości i wszystko trzeba by dosuwać ręcznie
     przy każdej zmianie skali. Podana DŁUGOŚĆ daje obu polom tę samą wysokość,
     a tekst centruje się w nich sam. */
  --row-line: 1.75rem;

  /* WYSOKOŚĆ PASA PIERWSZEGO WIERSZA — tego, w którym leżą godzina, węzeł, tytuł
     i przyciski akcji. Domyślnie tyle, co sam wiersz tekstu; tam, gdzie obok
     stoją przyciski, podnosi się do ICH wysokości (`.agenda .timeline` niżej),
     bo inaczej tekst trzyma się góry pasa, a przyciski jego środka — i punkt
     agendy czyta się jako dwie rzeczy postawione obok siebie na chybił trafił.

     `--row-lift` to dopełnienie, którym tekst dosuwa się do środka tego pasa;
     przy pasie równym wierszowi tekstu wychodzi zero, więc nic nie robi. */
  --row-head: var(--row-line);
  --row-lift: calc((var(--row-head) - var(--row-line)) / 2);

  /* Od góry WIERSZA (razem z jego dopełnieniem) do ŚRODKA węzła — stąd liczy się
     początek i koniec kreski, która ma zaczynać się i kończyć w kropce. */
  --rail-cap: calc(var(--row-pad-y) + var(--row-head) / 2);

  list-style: none;
  margin: 0;
  padding: 0;
}

/* Oś Z AKCJAMI — pełny ekran agendy Organizatora i tylko on. Podgląd na ekranie
   wydarzenia (`--preview`) i plan dnia Gościa (`.invite`) przycisków nie mają,
   więc pas pod kursorem im się nie zapala i nie ma tam czego z czym równać;
   dokładanie im tej samej wysokości wiersza rozepchnęłoby wypis bez powodu.

   Dwie wartości i obie wynikają z przycisku: pas pierwszego wiersza ma jego
   wysokość (żeby tekst stanął w jednej linii ze środkiem ikony), a dopełnienie
   daje mu powietrze — bez niego przycisk dotyka górnej i dolnej krawędzi
   podświetlenia i wygląda, jakby się w nim nie mieścił. */
.agenda .timeline {
  --row-head: var(--control-height);
  --row-pad-y: var(--space-2);
}

/* Odstęp do następnego punktu niesie MARGINES WIERSZA, a nie dopełnienie treści
   ani `gap` listy. Decyduje o tym pas podświetlenia: ma obejmować punkt, i tylko
   punkt. Odstęp schowany w dopełnieniu (tak było do #132) wchodzi do pasa, więc
   pod tekstem zostawał pusty ogon wysokości luki — a przyciski akcji,
   wyśrodkowane w takim pasie, siedziały wyraźnie pod wierszem, którego dotyczą.

   `gap` na liście dałby to samo co margines, ale URWAŁBY OŚ: pseudo-element
   z kreską sięga tylko granic wiersza, więc linia rozpadłaby się na odcinki.
   Margines nie ma tej wady — kreskę wypuszczamy w niego jawnie (niżej).

   Dopełnienie pionowe jest SYMETRYCZNE i dlatego `align-self: center` na akcjach
   trafia w środek pasa: środek ścieżki siatki i środek prostokąta tła to przy
   równym dopełnieniu ten sam punkt. */
.timeline__row {
  position: relative;
  display: grid;
  grid-template-columns: var(--time-col) var(--rail-col) minmax(0, 1fr) auto;
  column-gap: var(--col-gap);
  align-items: start;
  padding: var(--row-pad-y) var(--row-pad);
  margin-bottom: var(--row-gap);
  border-radius: var(--radius-sm);
  transition: background-color var(--dur-fast) ease-out;
}

/* `--rail-x` to OŚ węzła (środek kropki), a nie jego lewa krawędź — więc linia
   musi zostać wyśrodkowana WZGLĘDEM tej osi, a nie od niej zaczynać. Wersja
   z samym `left: var(--rail-x)` odsuwała kreskę o pół piksela w prawo, a że
   przeglądarka dociąga włosową linię do całego piksela urządzenia, przy zwykłej
   gęstości ekranu wychodziło z tego całe PIKSEL obok kropki (zmierzone: kropka
   miała środek w 213,5, a kreska zajmowała kolumnę 214–215). Efekt widać
   wyłącznie na ekranie i tylko przy powiększeniu — stąd `--rail-w` jako osobny
   token: szerokość linii i jej wyśrodkowanie muszą pochodzić z jednej wartości,
   inaczej zmiana jednego rozjedzie drugie.

   W PIONIE kreska wychodzi poza wiersz, na całą wysokość odstępu pod nim —
   inaczej oś miałaby przerwę w każdej luce między punktami, czyli wszędzie tam,
   gdzie ma właśnie prowadzić wzrok. Odstęp leży w marginesie (powód przy
   `.timeline__row` wyżej), a margines jest poza prostokątem, do którego odnosi
   się `bottom: 0`. */
.timeline__row::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: calc(-1 * var(--row-gap));
  left: calc(var(--rail-x) - var(--rail-w) / 2);
  width: var(--rail-w);
  background: var(--border);
}

/* Oś ZACZYNA SIĘ i KOŃCZY w węźle, a nie na krawędzi listy: kreska wystająca
   nad pierwszą kropkę wygląda jak urwany fragment czegoś większego. */
.timeline__row:first-child::before {
  top: var(--rail-cap);
}

.timeline__row:last-child::before {
  bottom: auto;
  height: var(--rail-cap);
}

/* Jedyny punkt dnia nie ma z czym się łączyć — linia byłaby wtedy kreską
   wystającą spod kropki donikąd. Reguła musi stać PO obu powyższych, bo ma
   z nimi tę samą wagę selektora. */
.timeline__row:first-child:last-child::before {
  content: none;
}

/* Ostatni wiersz nie potrzebuje odstępu na linię do następnego punktu. */
.timeline > .timeline__row:last-child {
  margin-bottom: 0;
}

/* Godzina. Odstęp do osi niesie `column-gap` siatki, NIE dopełnienie tego
   elementu — patrz `--col-gap` wyżej.

   `tabular-nums` jest tu warunkiem, nie ozdobą: przy cyfrach o zmiennej
   szerokości „11:26" i „9:00" mają różną długość i kolumna czasu skacze
   z wiersza na wiersz. Krój ZAWSZE bezszeryfowy, także w warstwie Gościa —
   Cormorant Garamond nie ma zestawu `tnum`, więc godziny złożone nim
   rozjeżdżałyby się dokładnie tam, gdzie ta deklaracja ma temu zapobiec.
   Tożsamość palety niesie na tym ekranie tytuł i węzeł osi, nie kolumna
   liczb do skanowania wzrokiem. */
.timeline__time {
  grid-column: 1;
  grid-row: 1;
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--text-lead);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: var(--row-line);
  letter-spacing: -0.01em;
  white-space: nowrap;
  padding-top: var(--row-lift);
}

/* Węzeł na osi. `position: relative` jest tu konieczne, żeby kropka rysowała
   się NAD linią — inaczej kreska przechodzi przez jej środek. */
.timeline__mark {
  position: relative;
  grid-column: 2;
  grid-row: 1;
  justify-self: center;
  width: 0.6875rem;
  height: 0.6875rem;
  /* Od góry TREŚCI wiersza, więc bez `--row-pad-y` — dopełnienie odsuwa węzeł
     razem z całą siatką. `--rail-cap` liczy to samo od góry wiersza i o tyle
     właśnie się od tej wartości różni. */
  margin-top: calc(var(--row-head) / 2 - 0.34375rem);
  border: 2px solid var(--border-control);
  border-radius: 50%;
  background: var(--surface-card);
  transition:
    background-color var(--dur-fast) ease-out,
    border-color var(--dur-fast) ease-out;
}

/* Odsunięcie treści od węzła niesie `column-gap` siatki — bez niego tytuł
   przykleja się do kropki i oś przestaje być osią, robi się z niej punktor
   listy. Dopełnienie pionowe to `--row-lift`: dosuwa pierwszy wiersz tytułu do
   środka pasa, w którym stoją godzina, węzeł i przyciski. Po obu stronach tyle
   samo, żeby punkt z opisem zostawał w pasie wyśrodkowany. */
.timeline__body {
  grid-column: 3;
  grid-row: 1;
  min-width: 0;
  padding-block: var(--row-lift);
}

/* `overflow-wrap` na OBU polach niosących tekst użytkownika, nie tylko na opisie.

   Ciąg bez spacji (wklejony link, „aaaaaa…", nazwa pliku) nie ma gdzie się złamać
   i wychodzi POZA kartę. Na szerokim ekranie to brzydki wąs, na telefonie —
   dwie osobne awarie z jednej przyczyny, bo przeglądarka mobilna, której treść
   nie mieści się w szerokości, ODDALA CAŁĄ STRONĘ, żeby ją zmieścić:

     * ekran przewija się w bok, a kolumna godzin ucieka poza kadr;
     * pod treścią pojawia się pusty pas na kilkaset pikseli — wysokość okna
       liczona po oddaleniu (zmierzone: strona 956 px szeroka przy ekranie
       390 px dawała dokument wysoki na 2069 px zamiast 1246 px).

   Drugi objaw nie wygląda na problem z tekstem i dlatego szukałoby się go
   w zupełnie innym miejscu. `min-width: 0` na `.timeline__body` już jest —
   bez niego ścieżka siatki i tak nie zeszłaby poniżej najdłuższego słowa. */
.timeline__title {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--text-body);
  line-height: var(--row-line);
  overflow-wrap: anywhere;
}

.timeline__note {
  margin: var(--space-1) 0 0;
  color: var(--muted);
  max-width: var(--measure);
  overflow-wrap: anywhere;
}

/* Odstęp do następnego punktu. Stoi NA WYSOKOŚCI linii między węzłami i jest
   celowo drobny: to miara odczytana z sąsiednich godzin, a nie osobna dana,
   którą ktoś wpisał. Bez wersalików — „1 H" czyta się jak skrót od czegoś
   innego niż godzina, a cała wartość i tak jest krótka. */
.timeline__gap {
  margin: var(--space-3) 0 0;
  color: var(--muted);
  font-size: var(--text-caption);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
}

/* Akcje na ŚRODKU wiersza w pionie, nie przy jego górnej krawędzi: podświetlenie
   na najechanie obejmuje cały wiersz, a przyciski dosunięte do góry wyglądają
   w tym pasie na zgubione. Środek ścieżki siatki jest zarazem środkiem pasa,
   bo dopełnienie wiersza jest symetryczne, a odstęp do następnego punktu leży
   w marginesie, czyli poza pasem (patrz `.timeline__row` wyżej). */
/* Odstęp od TREŚCI niesie margines akcji, a nie `column-gap` siatki: `gap`
   rozsunąłby również godzinę od osi, a `--rail-x` liczy pozycję kreski
   z założenia, że kolumna osi zaczyna się dokładnie za `--time-col`. Jedna
   właściwość na jednej kolumnie zamiast rachunku w dwóch miejscach. */
.timeline__actions {
  grid-column: 4;
  grid-row: 1;
  align-self: center;
  display: flex;
  gap: var(--space-2);
  margin-left: var(--space-4);
}

/* Punkt pod kursorem podświetla się CAŁYM WIERSZEM (razem z węzłem), a nie samą
   ramką przycisku: przy dwunastu punktach i myszy gdzieś w połowie ekranu to
   jedyne, co mówi, którego wiersza dotyczą akcje po prawej. */
.timeline__row:hover {
  background: var(--accent-soft);
}

.timeline__row:hover .timeline__mark {
  border-color: var(--accent);
  background: var(--accent);
}

/* Akcje pokazują się na najechanie i na ognisko klawiatury, ale na ekranie
   dotykowym (`hover: none`) zostają widoczne ZAWSZE — tam „najechać" nie ma
   czym, a ukryty przycisk jest przyciskiem, którego nie ma. */
@media (hover: hover) {
  .timeline__actions {
    opacity: 0;
    transition: opacity var(--dur-fast) ease-out;
  }

  .timeline__row:hover .timeline__actions,
  .timeline__actions:focus-within {
    opacity: 1;
  }
}

/* --------------------------------------------------------------------------
   Punkt trwający TERAZ — wyróżnienie dokładane przez `agenda.js`
   -------------------------------------------------------------------------- */

/* Wyróżnienie jest DWUCZĘŚCIOWE: wypełniony węzeł w kolorze akcentu ORAZ
   podpis „trwa teraz" przy godzinie. Sam kolor nie wystarcza (WCAG 1.4.1) —
   a przy palecie o niskim kontraście akcentu bywa wręcz niewidoczny. */
.timeline__row--now .timeline__mark {
  border-color: var(--accent-strong);
  background: var(--accent-strong);
  box-shadow: 0 0 0 4px var(--accent-soft);
}

.timeline__row--now .timeline__time {
  color: var(--accent-strong);
}

/* Znacznik przy tytule, nie przy godzinie: kolumna godzin ma szerokość „11:34"
   i każdy dłuższy napis wychodzi z niej wprost na sąsiedni wiersz. */
.timeline__now {
  display: inline-block;
  margin-left: var(--space-3);
  padding: 0 var(--space-2);
  border-radius: 999px;
  background: var(--accent-soft);
  font-size: var(--text-caption);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  vertical-align: 0.05em;
  color: var(--accent-strong);
}

/* --------------------------------------------------------------------------
   Wiersz w trybie edycji i węzeł „dodaj punkt"
   -------------------------------------------------------------------------- */

/* Wiersz zamieniony w formularz przestaje być siatką czterech kolumn —
   formularz zajmuje całą szerokość, żeby pola miały gdzie stanąć. Wyróżnienie
   robi akcent na krawędzi, a nie wypełnienie `--surface-alt`: ta powierzchnia
   należy do pasów sekcji, a w karcie daje ciężki prostokąt. */
.timeline__row--editing {
  display: block;
  padding: var(--space-4) 0 var(--space-6) var(--space-5);
  /* Odstęp do następnego punktu niesie tu dopełnienie, bo wiersz w edycji nie ma
     osi (`::before` wyłączony) i nie ma czego przez margines przepuszczać. */
  margin-bottom: 0;
  border-left: 3px solid var(--accent);
  border-radius: 0;
}

.timeline__row--editing::before {
  content: none;
}

.timeline__row--editing:hover {
  background: transparent;
}

/* Dodawanie punktu jest OSTATNIM WĘZŁEM OSI: kropka z plusem stoi na tej samej
   linii co punkty, więc czyta się jako „tu dopiszesz następny", a nie jako
   osobne pudełko pod listą. */
.timeline--new {
  margin-top: var(--space-6);
}

/* Zwinięty `<details>` bez własnego marginesu przykleja się do ostatniego
   punktu; rozwinięty musi dać formularzowi powietrze. */
.timeline--new[open] .timeline__add {
  margin-bottom: var(--space-5);
}

/* Trójkącik znacznika zdejmujemy DWOMA deklaracjami: `list-style` dla
   większości przeglądarek i `::-webkit-details-marker` dla Safari, który sam
   `list-style` ignoruje. */
.timeline__add {
  list-style: none;
  cursor: pointer;
  /* Węzeł „dodaj" jest jedynym wierszem, pod którym nie ma następnego punktu —
     odstęp do formularza daje `.timeline--new[open]`, a w stanie zwiniętym nie
     ma go po co zostawiać. */
  margin-bottom: 0;
}

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

/* Węzeł „dodaj" NIE dostaje linii: stoi w osobnym `<details>`, poza listą dnia,
   więc kreska nad nim nie łączyłaby się z ostatnią kropką — wyglądałaby jak
   urwany kawałek osi. */
.timeline__add::before {
  content: none;
}

/* Węzeł „dodaj" jest większy od zwykłego i przerywany — pusty okrąg z plusem
   czyta się jako miejsce do wypełnienia, a nie jako istniejący punkt. Element
   siatki może wystawać poza swoją kolumnę; `justify-self: center` (dziedziczone
   z `.timeline__mark`) trzyma go wtedy symetrycznie na osi. */
.timeline__mark--add {
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  margin-top: calc(var(--row-head) / 2 - 0.875rem);
  border-style: dashed;
  color: var(--accent-strong);
}

/* Etykieta stoi w tej samej linii co tytuły punktów — ten sam `--row-line`
   i to samo dosunięcie do środka pasa, dziedziczone z `.timeline__body`. */
.timeline__add-label {
  line-height: var(--row-line);
  font-weight: 700;
  color: var(--accent-strong);
}

.timeline__add:hover .timeline__mark--add {
  border-style: solid;
  border-color: var(--accent-strong);
  background: var(--accent-soft);
}

.timeline__add:hover {
  background: transparent;
}

/* Ognisko klawiatury na `<summary>` musi być widoczne tak samo jak na
   przycisku — bez tego węzeł „dodaj" jest jedynym elementem sterującym na
   ekranie, po którym nie widać, że się na nim stoi. */
.timeline__add:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Formularz DODAWANIA jest wcięty do kolumny treści, żeby stanął pod tytułami
   punktów, a nie pod godzinami. Formularz EDYCJI wcięcia nie dostaje — jego
   wiersz ma już własne (`.timeline__row--editing`), a dwa złożyłyby się w jedno
   głębokie i pola wyjechałyby poza kartę na wąskim ekranie. */
.timeline--new .timeline__form {
  padding-left: calc(var(--time-col) + var(--rail-col));
}

/* --------------------------------------------------------------------------
   Warianty: podgląd na ekranie wydarzenia i układ mobilny
   -------------------------------------------------------------------------- */

/* Podgląd to WYPIS, nie narzędzie: ciaśniej, bez akcji, bez podświetlania
   wiersza — kliknięcie i tak nic tu nie robi, więc reakcja na kursor
   obiecywałaby coś, czego nie ma. */
.timeline--preview {
  --time-col: 3.5rem;
  --row-gap: var(--space-5);
}

.timeline--preview .timeline__time {
  font-size: var(--text-body);
}

.timeline--preview .timeline__row:hover {
  background: transparent;
}

.timeline--preview .timeline__row:hover .timeline__mark {
  border-color: var(--border-control);
  background: var(--surface-card);
}

/* Ostatni wiersz podglądu nie ma odstępu pod sobą (linia osi kończy się
   w kropce), więc dopowiedzenie „…i 3 dalej" przyklejałoby się do tytułu. */
.agenda__more {
  margin-top: var(--space-4);
}

@media (max-width: 40rem) {
  /* Na telefonie cztery kolumny zostawiają na tytuł kilkanaście znaków. Oś
     i godzina ZOSTAJĄ (bez nich harmonogram przestaje być harmonogramem), a pod
     spód schodzą akcje — jedyna kolumna, którą da się oddać bez straty treści. */
  .timeline {
    --time-col: 3.25rem;
    --rail-col: 1.25rem;
  }

  .timeline__row {
    grid-template-columns: var(--time-col) var(--rail-col) minmax(0, 1fr);
  }

  .timeline__time {
    font-size: var(--text-body);
  }

  /* Akcje stoją tu POD treścią, we własnym wierszu siatki — wyśrodkowanie
     w pionie traci sens, a pas podświetlenia obejmuje oba wiersze. Odstępu do
     następnego punktu nie trzeba nigdzie przenosić: siedzi w marginesie wiersza
     i luka między opisem a przyciskami tego samego punktu go nie dotyczy. */
  .timeline__actions {
    grid-column: 3;
    grid-row: 2;
    align-self: start;
    margin-top: var(--space-3);
    /* Odstęp POZIOMY był potrzebny, dopóki akcje stały obok treści. Tutaj stoją
       POD nią, w tej samej kolumnie — margines wciąłby je głębiej niż tekst,
       który mają dotyczyć. */
    margin-left: 0;
  }

  /* Na dotyku akcje są widoczne zawsze (patrz `@media (hover: hover)` wyżej),
     więc wiersz w edycji nie może ich chować pod wcięciem formularza. */
  .timeline--new .timeline__form {
    padding-left: 0;
  }
}

/* Lista skutków na ekranach POTWIERDZENIA — rozliczenia wydarzenia (#33)
   i usunięcia grupy gości (#7). Jedna klasa na oba, bo to ta sama rola: co
   dokładnie zniknie, gdy kliknę. Znaczniki ZOSTAJĄ (`list-style` nietknięty):
   to wyliczenie konsekwencji, a nie nawigacja — punktory pomagają przeczytać
   je jako osobne pozycje. */
.consequences {
  margin: 0 0 var(--space-5);
  padding-left: var(--space-5);
  max-width: var(--measure);
}

.consequences li + li {
  margin-top: var(--space-3);
}

/* --------------------------------------------------------------------------
   Goście wydarzenia (#7, etap 1) — KARTA GRUPY

   Układ jest kartą, a nie tabelą, i to jest jedyna decyzja, z której wynika
   cała reszta. Tabela „grupa | telefon | osoby | akcje" nie mieści się na
   380 px, a przewijana w bok gubi pierwszą kolumnę — czyli dokładnie tę, po
   której wiadomo, czyj wiersz się właśnie edytuje. Karta ma naturalną
   hierarchię (nazwa → telefon grupy → osoby), która na wąskim ekranie po
   prostu się wydłuża, zamiast rozjeżdżać w poziomie.

   Telefon stoi PRZY GRUPIE, nad listą osób, a nie w wierszu osoby. To nie jest
   układ graficzny, tylko model danych narysowany wprost: numer i token należą
   do grupy (ADR-011). Osoba nie ma tu miejsca, w którym numer mógłby stanąć —
   i o to chodzi.
   -------------------------------------------------------------------------- */

/* Wyszukiwarka nad listą gości (#88).

   Pole i przyciski stoją w JEDNYM rzędzie z wyrównaniem do DOŁU (`flex-end`):
   nad polem jest etykieta, nad przyciskami nie ma nic, więc wyrównanie do góry
   ustawiłoby przyciski przy etykiecie, a nie przy kontrolce, do której należą.
   `align-items: baseline` też nie — podpowiedź pod polem jest częścią `.field`,
   więc linia bazowa wypada w połowie bloku.

   `flex-wrap` z `flex: 1 1 18rem` na polu daje zawijanie na wąskim ekranie bez
   punktu przełamania w media query: przyciski schodzą pod pole dopiero wtedy,
   gdy naprawdę się nie mieszczą. Zmierzone na 380 px — tam są już pod spodem. */
/* Odstęp MIĘDZY LINIAMI jest większy niż między elementami w linii
   (`row-gap` > `column-gap`) i to jest cała treść tej pary wartości. Odkąd rząd
   ma trzy linie — fraza, filtry, przyciski — wspólny odstęp 0.75 rem zlepiał
   nagłówek „Odpowiedź osoby" z polem frazy nad nim: etykieta sekcji stała
   bliżej cudzej kontrolki niż własnej. Sąsiedztwo w pionie znaczy tu „następny
   krok", a w poziomie „to samo pytanie". */
.guests__search {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-5) var(--space-3);
  margin-top: var(--space-5);
}

/* `flex-grow: 0` przy `flex-basis: --field-max` — pole NIE rozciąga się poza
   szerokość kontrolki, którą i tak ogranicza `--field-max`. Z `grow: 1` rosło
   samo pudełko `.field`, a `<input>` w środku zostawał przy 24 rem: przycisk
   „Szukaj" odjeżdżał wtedy na drugi koniec karty i przestawał wyglądać na
   należący do pola obok. Widać to wyłącznie na zrzucie — HTML jest w obu
   wariantach ten sam.

   Selektor DZIECKA, nie potomka: od dołożenia filtrów w rzędzie stoi
   pojemnik z własnymi polami, a te mają być węższe. Potomek objąłby je razem
   z polem frazy i zawężenie niżej byłoby wtedy nadpisywaniem reguły o tej samej
   wadze, czyli zależnością od kolejności w pliku. */
.guests__search > .field {
  flex: 0 1 var(--field-max);
  margin: 0;
}

/* Filtry listy gości (gałąź `fix/guest-list-filters`): odpowiedź osoby i „tylko bez kodu".

   `flex-basis: 100%` — filtry stoją POD polem frazy, na własnej linii, ZAWSZE
   i niezależnie od szerokości ekranu. To jest decyzja o czytelności, nie skutek
   zawijania: pole frazy niesie trzy linie podpowiedzi, filtr odpowiedzi dwie,
   więc postawione obok siebie dają dwie kolumny o różnej wysokości, z etykietami
   i kontrolkami na różnych poziomach — pasek wygląda wtedy na złożony
   z przypadku. Jedna kolumna czyta się z góry na dół: co szukam, czym zawężam,
   zatwierdź.

   Zawijanie tego NIE ZAŁATWIA i próba na tym poległa: przy `flex-basis: auto`
   o miejscu filtrów decyduje suma szerokości kontrolek, więc każde ich zwężenie
   (choćby dla czytelności listy wyboru) po cichu wciąga je z powrotem do rzędu
   frazy. Układ ma wynikać z decyzji, a nie z tego, ile akurat zostało miejsca.

   `align-items: flex-end`: nad listą wyboru stoi etykieta z podpowiedzią, nad
   polem wyboru nie stoi nic, więc wyrównanie do góry ustawiłoby kwadracik przy
   etykiecie sąsiada zamiast przy jego kontrolce. */
.guests__filters {
  display: flex;
  flex: 1 1 100%;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-3) var(--space-5);
}

/* SZEROKOŚĆ POLA FILTRU WYZNACZA JEGO KONTROLKA, NIE JEGO PODPOWIEDŹ — i to
   jest cała treść tej reguły. `.field__help` niesie własne
   `max-width: var(--field-max)` (24 rem), więc bez tego ograniczenia to
   PODPOWIEDŹ rozpychała pudełko pola: pole wyboru odjeżdżało o ok. 380 px od
   listy wyboru, z którą tworzy jedną grupę, i wyglądało na kontrolkę niczyją.
   Zmierzone na zrzucie, nie oszacowane — w HTML-u oba warianty są identyczne. */
.guests__filters .field {
  flex: 0 1 16rem;
  margin: 0;
}

/* Kontrolka wypełnia pudełko pola, którego szerokość wyznacza reguła wyżej.
   Wartość wpisana tutaj wprost (np. `14rem`) byłaby DRUGĄ liczbą opisującą tę
   samą szerokość — i to ona ucinała „Wszystkie odpowiedzi" w połowie słowa. */
.guests__filters select {
  width: 100%;
}

/* Pole wyboru stoi na OSI listy wyboru obok, nie przy jej dolnej krawędzi.
   Rząd wyrównuje doły pudełek (`flex-end`), a pudełko pola wyboru jest wysokie
   na jedną linię tekstu — kwadracik lądował przez to poniżej środka sąsiada
   i para wyglądała na złożoną z dwóch niezależnych rzeczy. Wysokość kontrolki
   na pojemniku daje wspólną podstawę, centrowanie w pionie ustawia zawartość
   na tej samej osi.

   `margin-top` kwadracika trzeba przy tym wyzerować: reguła bazowa
   `.field__inline` opuszcza go o 0.28 rem, bo tam wyrównanie idzie do GÓRY
   i chodzi o zrównanie z pierwszą linią wieloliniowej etykiety. Przy
   centrowaniu ten sam margines przesuwa go pod oś. */
.guests__filters .field__inline {
  min-height: var(--control-height);
  align-items: center;
}

.guests__filters .field__inline input[type="checkbox"] {
  margin-top: 0;
}

/* Rząd przycisków wyszukiwarki nie ma odstępu od góry, który `.form__actions`
   dokłada w formularzach pionowych — odstęp niesie tu `gap` rzędu.

   Od dołożenia filtrów przyciski schodzą na WŁASNĄ linię, pod komplet kontrolek, i to jest
   jedyny układ, w którym „Szukaj" stoi PO tym, co zatwierdza. Trzy kontrolki
   plus dwa przyciski nie mieszczą się w jednym rzędzie na karcie panelu
   (zmierzone: ok. 1040 px potrzebne przy 1050 px karty na ekranie 1280 px),
   a rząd zawinięty w przypadkowym miejscu ustawia przyciski obok pola wyboru
   — czyli sugeruje, że należą do niego.

   Do LEWEJ, nie do prawej: rząd „Importuj z pliku CSV / Dodaj gościa" pod
   spodem jest dosunięty do prawej, a dwa rzędy przycisków przy tej samej
   krawędzi czytają się jak jeden blok czterech akcji o różnej wadze. */
.guests__search .form__actions {
  flex: 1 1 100%;
  justify-content: flex-start;
  margin-top: 0;
}

/* Komunikat o wydarzeniu ROZLICZONYM stoi tuż pod wyszukiwarką i bez tej reguły
   dotyka jej przyciskiem — `.message` nie niesie własnego marginesu, bo w innych
   miejscach odstęp daje mu przepływ dokumentu. Tutaj poprzednik jest kontenerem
   `flex` z `gap`, czyli marginesy sąsiadów się NIE zbiegają i odstęp trzeba
   podać. Zawężone do listy gości: reguła ogólna przestawiłaby komunikaty na
   ekranach, których ta zmiana nie dotyczy. */
.guests > .message {
  margin-top: var(--space-6);
}

/* Nawigacja stron stoi WEWNĄTRZ karty listy, tuż pod ostatnią grupą — inaczej
   niż na liście wydarzeń, gdzie jest osobnym elementem strony i odstęp daje jej
   przepływ dokumentu. Bez tej reguły przyciski dotykają ostatniej karty. */
.guests .pagination {
  margin-top: var(--space-6);
}

/* Zdanie o pustym wyniku (brak trafień frazy, brak trafień filtru, pusta lista)
   stoi DOKŁADNIE tam, gdzie stałaby lista — więc dostaje ten sam odstęp od góry
   co `.guest-groups`. Bez tej reguły przykleja się do rzędu przycisków nad sobą:
   akapit ma `margin-top: 0` (reguła bazowa `p`), a poprzednik jest kontenerem
   `flex` z `gap`, więc nie ma tu marginesu, który mógłby się z czymkolwiek
   zbiec. Ta sama klasa problemu co `.guests > .message` wyżej i tak samo widać
   ją wyłącznie na ekranie — HTML jest poprawny w obu wariantach.

   Selektor DZIECKA: `.text-muted` występuje w tej sekcji jeszcze dwa razy
   (pusta grupa, numer strony w stronicowaniu) i tam odstęp niesie już układ. */
.guests > p {
  margin-top: var(--space-6);
}

/* Dwa wejścia na listę: „Importuj z pliku CSV" i „Dodaj gościa" (#7, etap 2).
   Stoją w JEDNYM rzędzie, dosunięte do prawej — dwa osobne rzędy przycisków
   jeden nad drugim czyta się jak dwa poziomy ważności, a te akcje są równorzędne.

   `flex-wrap` z `flex-basis: 100%` na ROZWINIĘTYM `<details>` to cała mechanika
   tego układu: zwinięty formularz jest zwykłym elementem rzędu, a rozwinięty
   zeskakuje na własną, pełną linię. Bez tego pola formularza ściskałyby się do
   szerokości przycisku obok. */
.guests__entry {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: flex-end;
  gap: var(--space-3);
  margin-top: var(--space-5);
}

.guests__new[open] {
  flex: 1 1 100%;
}

/* Przycisk „Dodaj gościa" stoi POD paskiem, dosunięty do prawej. W samym pasku
   stanąć nie może: `<details>` rozwija treść pod swoim `<summary>`, więc
   formularz otwierałby się w jego wąskiej kolumnie. `margin-left: auto`
   wymaga elementu blokowego — `.btn` jest `inline-flex`, stąd podmiana; przy
   rozwiniętym formularzu (pełna linia rzędu) to ono trzyma `<summary>` przy
   prawej krawędzi. */
.guests__add {
  display: flex;
  width: fit-content;
  margin-left: auto;
  list-style: none;
  cursor: pointer;
}

/* Trójkącik znacznika zdejmujemy DWOMA deklaracjami: `list-style` dla
   większości przeglądarek i `::-webkit-details-marker` dla Safari, który sam
   `list-style` ignoruje. */
.guests__add::-webkit-details-marker {
  display: none;
}

.guests__new[open] .guests__add {
  margin-bottom: var(--space-5);
}

/* Formularz ma JEDNĄ prawą krawędź. Bez tego pola pojedyncze zatrzymują się na
   `--field-max` (24 rem), a para „imię + nazwisko" rozciąga się na całą
   szerokość formularza — i kolumna kontrolek wychodzi poszarpana. Widać to
   dopiero na zrzucie, bo każde pole z osobna wygląda poprawnie. */
.guests__form {
  max-width: 32rem;
}

.guests__form .field :where(input, select, textarea) {
  max-width: 100%;
}

.guest-groups {
  display: grid;
  gap: var(--space-5);
  margin: var(--space-6) 0 0;
  padding: 0;
  list-style: none;
}

/* Karta grupy: obrys, bez wypełnienia. `--surface-alt` należy do pasów sekcji
   i w karcie panelu daje ciężki, ciemny prostokąt — a tych
   prostokątów jest tu tyle, ile grup. */
.guest-group {
  padding: var(--space-5);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: background-color var(--dur) ease-out;
}

/* Grupa pod kursorem podświetla się CAŁĄ KARTĄ, a nie samą ramką przycisku:
   przy kilkunastu grupach i myszy gdzieś w połowie ekranu to jedyne, co mówi,
   której grupy dotyczą akcje po prawej. */
.guest-group:hover {
  background: var(--accent-soft);
}

/* Podświetlenie po podmianie fragmentu. Klasę `htmx-added` HTMX dokłada sam
   na wstawianą treść i sam ją zdejmuje, więc przejście odpala się przy
   ZDEJMOWANIU — karta zaczyna podświetlona i gaśnie.

   ŚWIADOMY KOSZT: podmieniamy CAŁĄ listę (reguła 2 w `guests/views.py`), więc
   klasa ląduje na wszystkich kartach naraz. Sygnał znaczy tu „lista się
   odświeżyła", a nie „ta karta jest nowa" — i tak ma być czytany. Wyróżnienie
   jednej karty wymagałoby podmiany pojedynczego wiersza, czyli złamania reguły,
   która pilnuje sortowania. */
.guest-group.htmx-added {
  background: var(--accent-soft);
}

/* Wejście listy: przesunięcie o `0.25rem` i zanikanie, opóźnienie SCHODKOWE.
   Opóźnienie idzie regułami `:nth-child(-n+K)`, a nie `style="animation-delay"`
   w szablonie — atrybut inline blokuje ścisłe CSP i zapala
   `test_template_respects_csp_bans`.

   Reguły mają tę samą wagę, więc rozstrzyga KOLEJNOŚĆ: każdy kolejny zapis
   jest węższy od poprzedniego i dlatego stoi niżej. Karty od jedenastej
   w górę zostają przy wartości bazowej — czekanie sekundy na dwudziestą
   kartę nie jest już efektem wejścia, tylko opóźnieniem. */
@keyframes guest-group-in {
  from {
    opacity: 0;
    transform: translateY(0.25rem);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

.guest-groups > .guest-group {
  animation: guest-group-in var(--dur) ease-out both;
  animation-delay: 150ms;
}

.guest-groups > .guest-group:nth-child(-n + 8) {
  animation-delay: 120ms;
}

.guest-groups > .guest-group:nth-child(-n + 6) {
  animation-delay: 90ms;
}

.guest-groups > .guest-group:nth-child(-n + 4) {
  animation-delay: 60ms;
}

.guest-groups > .guest-group:nth-child(-n + 2) {
  animation-delay: 30ms;
}

.guest-groups > .guest-group:nth-child(1) {
  animation-delay: 0ms;
}

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

/* `align-items: start`, nie `center`: etykieta grupy ma 255 znaków limitu, więc
   bywa wielowierszowa — a wyśrodkowana pionowo ikona wisi wtedy w POŁOWIE bloku
   tekstu, jakby należała do trzeciego wiersza. Widać to wyłącznie na długich
   danych, czyli na stanie spoza happy path.

   `min-width: 0` i `overflow-wrap` są tu obowiązkowe, nie ozdobne: element
   elastyczny bez `min-width: 0` nie kurczy się poniżej swojej treści i wypycha
   przyciski poza kartę. */
.guest-group__name {
  display: flex;
  align-items: start;
  gap: var(--space-2);
  min-width: 0;
  margin: 0;
  font-size: var(--text-lead);
  line-height: var(--leading-tight);
  overflow-wrap: anywhere;
}

/* Optyczne wyrównanie do PIERWSZEGO wiersza: pole wierszowe jest wyższe od
   ikony (1.2 × rozmiar pisma kontra 1.15 em), więc ikona dosunięta do góry
   stoi ociupinę za wysoko. */
.guest-group__icon {
  flex: none;
  margin-top: 0.1em;
  color: var(--muted);
}

.guest-group__actions {
  display: flex;
  flex: none;
  gap: var(--space-2);
}

.guest-group__phone {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-3) 0 0;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* Osoby wiszą na pionowej kresce pod nazwą grupy — ten sam pomysł co oś agendy:
   kreska mówi „to należy do tego, co wyżej", a przy dwóch grupach pod rząd
   pozwala odróżnić, gdzie kończy się jedna lista osób, a zaczyna druga. */
.guest-list {
  margin: var(--space-4) 0 0;
  padding: 0 0 0 var(--space-4);
  border-left: 1px solid var(--border);
  list-style: none;
}

/* Wysokość wiersza to `--control-height` (44 px, WCAG 2.5.5) — cel dotykowy
   przycisków akcji, a nie odstęp dobrany na oko. */
.guest {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: var(--control-height);
}

/* Nazwisko i odpowiedź (#99) jako JEDNA całość wiersza.

   Dwa powody naraz, oba wynikające z tego, że `.guest` jest `space-between`
   z celem dotykowym 44 px:

     * POZIOMO — trzecie dziecko rozpycha się między pozostałe, więc plakietka
       wylądowałaby dokładnie w połowie wiersza, oderwana od osoby, której
       dotyczy (ten sam błąd układu co plakietki w pasku listy, #98).
       `margin-right: auto` zabiera wolną przestrzeń PRZED podziałem.
     * PIONOWO — wiersz wyrównuje dzieci do ŚRODKA, a napis mniejszym stopniem
       wyśrodkowany względem większego nie stoi z nim w jednej linii pisma:
       unosi się o kilka pikseli i wygląda jak wklejony. Wewnątrz pojemnika
       wyrównanie idzie po LINII BAZOWEJ, czyli tak, jak czyta je oko. */
.guest__person {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-3);
  min-width: 0;
  margin-right: auto;
}

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

.guest__rsvp {
  flex: none;
  font-size: var(--text-caption);
  font-weight: 700;
}

/* Trzy stany, trzy traktowania — a rozróżnienia NIE niesie sam kolor
   (WCAG 1.4.1): napis jest inny w każdym z nich, klasa tylko go wzmacnia.

   „Bez odpowiedzi" jest przygaszone, bo to stan wyjściowy każdej nowo wpisanej
   osoby i przy pierwszym otwarciu listy dotyczy WSZYSTKICH; wyróżniony dawałby
   ekran złożony z samych ostrzeżeń. Odmowa bierze `--danger`, ale wyłącznie
   jako barwę tekstu — to nie jest błąd Organizatora, tylko informacja, której
   nie wolno przeoczyć przy zamawianiu. */
.guest__rsvp--pending {
  color: var(--muted);
  font-weight: 400;
}

.guest__rsvp--confirmed {
  color: var(--accent-strong);
}

.guest__rsvp--declined {
  color: var(--danger);
}

/* „po terminie" (#115) — DOPISEK przy odmowie, nie druga odznaka stanu.
   Odmowa po zamknięciu zapisów jest dla Organizatora inną wiadomością niż
   odmowa zwykła (catering już zamówiony), ale wciąż tą samą ODPOWIEDZIĄ —
   dlatego stoi w tej samej linii, mniejszym stopniem i bez własnego tła.
   Osobna kolorowa plakietka konkurowałaby wzrokowo ze statusem, o którym mówi. */
.guest__rsvp-late {
  color: var(--muted);
  font-size: var(--text-caption);
  font-weight: 400;
  white-space: nowrap;
}

/* Ekran weryfikacji biletu (#100) czyta się Z ODLEGŁOŚCI RĘKI, przy wejściu,
   często w biegu — odpowiedź na zaproszenie dostaje tam stopień o dwa większy
   niż w wierszu listy gości. Klasa stanu (barwa) i napis zostają te same, bo
   „Potwierdzone" nie ma prawa znaczyć wizualnie czegoś innego na dwóch
   ekranach tej samej aplikacji. */
.ticket-check .guest__rsvp {
  font-size: var(--text-lead);
}

/* ODPRAWA NA EKRANIE WEJŚCIA (#9) — jedna oczywista akcja, na całą szerokość.

   Ten ekran otwiera się przy drzwiach, w biegu, i klika się go kciukiem: cel
   dotykowy jest tu CAŁYM paskiem, a nie kwadratem 44 px gdzieś w rogu karty.
   Kreska nad sekcją oddziela „kto to jest" od „co z tym zrobić" — bez niej
   przycisk czyta się jak czwarta pozycja listy danych. */
.ticket-check__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-5);
  padding-top: var(--space-5);
  border-top: 1px solid var(--border);
}

/* Szerokość musi dostać FORMULARZ, nie sam przycisk: formularz jest tu
   elementem układu flex, więc kurczy się do treści i `width: 100%` na przycisku
   liczy się wtedy od tej skurczonej szerokości. Wada widoczna wyłącznie na
   zrzucie — HTML jest poprawny, a przycisk działa. */
.ticket-check__admit-form {
  flex: 1 1 100%;
}

.ticket-check__admit {
  width: 100%;
}

/* Stan „już wpuszczony" bierze stopień `--text-lead`, tak samo jak odpowiedź na
   zaproszenie wyżej i z tego samego powodu: przy powtórnym skanie to jest
   JEDYNA rzecz, którą obsługa musi odczytać z odległości ręki. `margin-right:
   auto` odsuwa od niego „Cofnij" — akcja korygująca nie ma stać przy samym
   napisie, w który celuje kciuk. */
.ticket-check__state {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0 auto 0 0;
  color: var(--accent-strong);
  font-size: var(--text-lead);
  font-weight: 700;
}

/* WEJŚCIE (#9) — dopisek przy osobie, nie trzecia plakietka stanu.

   Stopień i waga jak przy „po terminie": ten wiersz ma już jedną informację
   wyróżnioną barwą (odpowiedź na zaproszenie), a druga równie mocna zmusiłaby
   oko do wyboru między nimi. Kolor akcentu, bo „wszedł" jest stanem
   OSIĄGNIĘTYM, a nie brakiem do uzupełnienia — ale rozróżnienia nie niesie sam
   kolor (WCAG 1.4.1): niosą je napis i ikona.

   `nowrap` na całości: „Wpuszczono 18:42" złamane po pierwszym wyrazie czyta
   się jak dwie osobne dane. Dwa wyrazy zawsze mieszczą się w kolumnie —
   inaczej niż plakietka trzech liczb z 20uu1, która nowrapu mieć nie może. */
/* ŚWIADOMIE BEZ `inline-flex` — i to jest cała treść tej reguły.

   Wiersz osoby (`.guest__person`) wyrównuje dzieci PO LINII BAZOWEJ, a element
   `flex` oddaje jako swoją linię bazową linię swojego PIERWSZEGO DZIECKA. Tym
   dzieckiem jest tu ikona, która żadnej linii pisma nie ma — więc cały napis
   „Wpuszczono 20:15" siadał obok nazwiska krzywo, przy poprawnym HTML-u
   i zielonych testach. Objaw wyłącznie na ekranie.

   Bez `display` pozostaje zwykły blok (element flex i tak blokuje `inline`),
   którego linią bazową jest linia jego TEKSTU — czyli ta sama, co przy
   nazwisku i przy odpowiedzi na zaproszenie obok. Odstęp przy ikonie robi
   wtedy zwykła spacja i `vertical-align` z klasy `.icon`, dokładnie jak
   w każdym innym miejscu tego projektu. */
.guest__check-in {
  flex: none;
  color: var(--accent-strong);
  font-size: var(--text-caption);
  white-space: nowrap;
}

/* DANE USUNIĘTE (#21) — dopisek przy osobie, ten sam stopień i to samo miejsce
   co „Wpuszczono" wyżej. ŚWIADOMIE BEZ `inline-flex` z dokładnie tego samego
   powodu: element flex oddaje jako swoją linię bazową linię pierwszego dziecka,
   a tym dzieckiem jest ikona, która żadnej linii pisma nie ma.

   Przygaszony, nie wyróżniony: przy liście, na której wymazano kilkanaście
   osób, wyróżnienie zamieniłoby ekran w ścianę ostrzeżeń — a to jest stan
   POPRAWNY, wynik wykonanego żądania. Kursywa odróżnia dopisek od nazwy wiersza
   — a od domknięcia #21 nazwa jest już JEDNYM słowem („Zanonimizowano”), nie
   powtórzonym dwa razy: regułę niesie `Guest.__str__`, nie szablon. */
.guest__anonymised {
  flex: none;
  color: var(--muted);
  font-size: var(--text-caption);
  font-style: italic;
  white-space: nowrap;
}

/* Wiadomość od Gościa: CAŁY wiersz pod nazwiskiem. `flex-basis: 100%` łamie
   wiersz w kontenerze `flex-wrap`, więc zdanie nie ściska kolumny akcji —
   a bywa długie (limit 500 znaków). */
.guest__note {
  flex-basis: 100%;
  margin-top: calc(var(--space-1) * -1);
  /* Wcięcie mówi „to należy do wiersza WYŻEJ". Bez niego zdanie stoi na tej
     samej osi co nazwiska i przy trzech osobach w grupie nie widać, do której
     z nich się odnosi — a odnosi się do tej NAD nim, nie pod. */
  padding-left: var(--space-4);
  color: var(--muted);
  font-size: var(--text-caption);
  overflow-wrap: anywhere;
}

/* DIETA (#15) — cały wiersz nad wiadomością, ta sama mechanika co przy niej
   (`flex-basis: 100%` łamie wiersz w kontenerze `flex-wrap`) i to samo wcięcie,
   które mówi „to należy do wiersza WYŻEJ".

   RÓŻNI SIĘ BARWĄ I TO JEST CAŁA RÓŻNICA MIĘDZY TYMI DWOMA WIERSZAMI. Wiadomość
   jest przygaszona, bo bywa uprzejmością („dojedziemy po 18"); dieta jest
   pozycją do przepisania na listę dla kuchni, a przygaszona ginie na karcie
   rodziny, w której trzy osoby napisały coś miłego. Rozróżnienia nie niesie
   jednak sam kolor — niosą je ikona i słowo „Dieta:" (WCAG 1.4.1). */
.guest__diet {
  flex-basis: 100%;
  margin-top: calc(var(--space-1) * -1);
  padding-left: var(--space-4);
  font-size: var(--text-caption);
  overflow-wrap: anywhere;
}

/* Odstęp przy ikonie robiony marginesem, nie `gap`: element ma zostać liniowy,
   żeby oddał linię bazową TEKSTU. Jako `inline-flex` oddałby linię pierwszego
   dziecka, czyli ikony, która żadnej linii pisma nie ma — ta sama pułapka co
   przy „Wpuszczono" i „Dane usunięte" wyżej. */
.guest__diet .icon,
.guest__table .icon {
  margin-right: var(--space-1);
}

/* STÓŁ (#16) — plakietka PRZY nazwisku, nie wiersz pod nim: napis jest krótki,
   a Organizator czyta go skanując listę w pionie.

   `flex: none` i `white-space: nowrap` z tego samego powodu co przy
   „Wpuszczono": to jest etykieta, a nie zdanie — złamana na dwa wiersze
   przestaje się skanować wzrokiem i rozpycha wiersz osoby w pionie.

   Przygaszona: plan sali jest tłem pracy nad listą, a nie stanem wymagającym
   reakcji. Wyróżnienie zabierałoby uwagę odpowiedziom, po które ten ekran
   otwiera się przed weselem. */
.guest__table {
  flex: none;
  color: var(--muted);
  font-size: var(--text-caption);
  white-space: nowrap;
}

.guest__actions {
  display: flex;
  flex: none;
  gap: var(--space-2);
}

.guest--empty {
  font-size: var(--text-caption);
}

/* Akcje pokazują się na najechanie i na ognisko klawiatury, ale na ekranie
   dotykowym (`hover: none`) zostają widoczne ZAWSZE — tam „najechać" nie ma
   czym, a ukryty przycisk jest przyciskiem, którego nie ma. */
@media (hover: hover) {
  .guest-group__actions,
  .guest__actions {
    opacity: 0;
    transition: opacity var(--dur-fast) ease-out;
  }

  .guest-group:hover .guest-group__actions,
  .guest-group__actions:focus-within,
  .guest:hover .guest__actions,
  .guest__actions:focus-within {
    opacity: 1;
  }
}

/* WĄSKI EKRAN — wiersz osoby przestaje być rzędem i staje się BLOKIEM.

   Telefon jest tu głównym nośnikiem, a nie wariantem awaryjnym: lista gości
   otwiera się na sali, w biegu. Na 390 px wiersz osoby nie mieści się w jednej
   linii, więc łamie się sam — i to złamanie było źródłem wszystkich trzech wad
   widocznych na zrzucie, z których żadnej nie widać na ekranie
   szerokim:

     * odpowiedź na zaproszenie stała OBOK krótkiego nazwiska, a POD długim —
       ta sama informacja w dwóch miejscach, zależnie od tego, ile liter ma
       czyjeś nazwisko. Tutaj nazwisko dostaje własny wiersz zawsze, więc
       odpowiedź stoi zawsze pod nim;
     * rząd przycisków spadał pod nazwisko i przyklejał się do NASTĘPNEJ osoby
       — wyglądał jak jej akcje, bo dzieliła je z nią zwykła przerwa flexa.
       Kreska nad każdą kolejną osobą mówi, gdzie kończy się jedna, a zaczyna
       druga (ten sam zabieg co pionowa oś `.guest-list`, tym samym tokenem);
     * wiadomość od gościa wypadała POD przyciskami, czyli między osobą, której
       dotyczy, a osobą następną. `order` przestawia ją przed akcje.

   `order` jest tu dozwolony: przestawiany element nie zawiera
   niczego, co da się zogniskować, a jedyne kontrolki wiersza — `.guest__actions`
   — zostają na końcu w obu porządkach, więc kolejność tabulacji nadal zgadza
   się z widzianą (WCAG 2.4.3). Warunek przestaje obowiązywać w chwili, gdy
   w wiadomości albo przy odpowiedzi stanie łącze.

   Próg to ten sam `40rem` co przy agendzie — jedna szerokość na cały panel,
   a nie osobna liczba dobrana pod ten ekran. */
@media (max-width: 40rem) {
  .guest__name {
    flex-basis: 100%;
  }

  .guest__actions {
    order: 1;
    flex-basis: 100%;
    margin-top: var(--space-1);
  }

  /* Wcięcie wiadomości mówiło „to należy do wiersza WYŻEJ" i było potrzebne,
     dopóki wiadomość stała pod rzędem przycisków. Tutaj stoi bezpośrednio pod
     nazwiskiem, wewnątrz bloku odciętego kreską — a wcięta zostawiała blok
     z trzema różnymi krawędziami lewej strony. */
  .guest__note,
  .guest__diet {
    margin-top: 0;
    padding-left: 0;
  }

  .guest-list > .guest + .guest {
    margin-top: var(--space-3);
    padding-top: var(--space-3);
    border-top: 1px solid var(--border);
  }
}

/* Karta w trybie edycji albo potwierdzania usunięcia: wyróżnienie robi akcent
   NA KRAWĘDZI, a nie wypełnienie. Obie reguły muszą stać PO `.guest-group:hover`
   — mają z nią tę samą wagę selektora, więc rozstrzyga kolejność, a karta
   zmieniająca barwę pod kursorem w trakcie wpisywania danych rozprasza. */
.guest-group--editing,
.guest-group--confirming {
  border-left: 3px solid var(--accent);
}

.guest-group--editing:hover,
.guest-group--confirming:hover {
  background: transparent;
}

/* Wiersz osoby zamieniony w formularz przestaje być rzędem „nazwa + akcje" —
   pola potrzebują całej szerokości, żeby miały gdzie stanąć. */
.guest--editing {
  display: block;
  padding: var(--space-3) 0;
}

/* Para pól, które czyta się razem (imię i nazwisko). Domyślnie każde pole
   siatki zajmuje cały wiersz — tu odwrotnie, bo rozdzielenie tych dwóch na
   osobne wiersze rozciąga formularz osoby na wysokość całej karty grupy. */
.form-grid--pairs > .field {
  grid-column: auto;
}

/* Odpowiedź na zaproszenie w formularzu osoby (#115) — ODKLEJONA od pary
   imię/nazwisko nad nią. Siatka par ma własny odstęp wewnętrzny, więc pole
   dostawione pod nią lądowało w tej samej odległości co dwa pola tej samej
   pary — a to jest inna rzecz: tamte dwa opisują KOGO, to jedno opisuje
   ODPOWIEDŹ i ma przy sobie dwa wiersze podpowiedzi. */
.guest__form-answer {
  margin-top: var(--space-5);
}

/* RYTM PIONOWY FORMULARZA OSOBY (#15, #226) — dokładnie ten sam odstęp, który
   `.field + .field` daje polom stojącym obok siebie.

   Reguła bazowa tych przejść NIE ŁAPIE i to jest cała przyczyna: między polami
   stoją POJEMNIKI — siatka par (imię/nazwisko, stół/miejsce) i blok odpowiedzi
   — a `+` łączy wyłącznie rodzeństwo tego samego rodzaju. Dopóki formularz miał
   trzy pola, nie było tego widać; dieta i para „stół/miejsce" siadły wprost na
   kontrolce nad sobą, więc nagłówek pola dotykał cudzego pola.

   Selektory są ZAWĘŻONE do dwóch formularzy osoby, a nie napisane globalnie:
   `.form-grid` używa też formularz wydarzenia, który ma własny rytm (pary dat
   w `subgrid`) i którego ta reguła nie ma prawa ruszyć.

   DZIECI BEZPOŚREDNIE (`>`), nie potomkowie, i to jest warunek poprawności,
   nie porządek: zapis potomny łapałby także parę pól WEWNĄTRZ siatki, gdzie
   odstęp rysuje `gap`, a `margin-top` jest jawnie zerowany
   (`.form-grid > .field + .field`). Przy równej specyficzności wygrywałaby
   reguła stojąca niżej w pliku — czyli ta — i para „Imię / Nazwisko"
   rozjechałaby się w pionie. */
:is(.guest__form, .guest-add__form)
  > :is(.field, .form-grid, .guest__form-answer)
  + :is(.field, .form-grid) {
  margin-top: var(--space-5);
}

/* Blok „dodaj osobę" pod listą osób w karcie grupy. Występuje w dwóch
   postaciach pod tym samym `id`: jako ŁĄCZE (stan spoczynkowy, jedyny na
   liście) i jako FORMULARZ dociągnięty w jego miejsce (`.guest-add--open`).
   Odstęp od listy osób jest wspólny, więc podmiana nie przesuwa karty. */
.guest-add,
.guest-add__link {
  margin-top: var(--space-4);
}

/* Łącze wygląda jak element sterujący, bo nim jest — ale zostaje `<a>`,
   żeby bez JavaScriptu prowadziło na pełny ekran dodawania osoby (#81). */
.guest-add__link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--control-height);
  color: var(--accent-strong);
  font-size: var(--text-caption);
  font-weight: 700;
  text-decoration: none;
}

.guest-add__link:hover {
  text-decoration: underline;
}

/* Ognisko klawiatury musi być widoczne tak samo jak na przycisku — bez tego
   „Dodaj osobę" jest elementem sterującym, po którym nie widać, że się na nim
   stoi. Domyślny `outline` łącza znika razem z `text-decoration`. */
.guest-add__link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* Zdanie w miejscu łącza „Dodaj osobę", gdy grupa jest pełna (#161). Wygaszone
   i mniejsze od łącza, które zastępuje: to nie jest czynność do wykonania, tylko
   powód, dla którego jej tu nie ma. Ten sam odstęp od listy osób co `.guest-add`,
   żeby karta pełnej grupy nie stała inaczej niż każda inna. */
.guest-add__note {
  margin-top: var(--space-4);
  color: var(--muted);
  font-size: var(--text-caption);
}

/* USUNIĘCIE DANYCH OSOBOWYCH (#21) — łącze na dole karty, pod „Dodaj osobę".

   Wygaszone i mniejsze niż akcje obok, bo to jest czynność RZADKA i ciężka:
   realizacja żądania z art. 17 RODO zdarza się raz na wiele wesel, a wygląd
   równie mocny co „Dodaj osobę do grupy" stawiałby ją w jednym rzędzie
   z czynnością wykonywaną co chwilę. Barwa ostrzegawcza wchodzi dopiero pod
   kursorem i na ognisku — stale czerwone łącze przy każdej karcie zamieniłoby
   listę w ścianę ostrzeżeń (ta sama zasada co przy `.btn--danger`).

   Kreska nad łączem oddziela „co jeszcze mogę zrobić z tą grupą" od „co mogę
   z niej usunąć": bez niej akcja niszcząca czyta się jak trzecia pozycja listy
   osób. */
.guest-group__erase {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--control-height);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
  width: 100%;
  color: var(--muted);
  font-size: var(--text-caption);
  text-decoration: none;
}

.guest-group__erase:hover,
.guest-group__erase:focus-visible {
  color: var(--danger);
  text-decoration: underline;
}

/* Ognisko klawiatury widoczne tak samo jak na przycisku — z tego samego powodu
   co przy „Dodaj osobę": domyślny `outline` łącza znika razem z podkreśleniem. */
.guest-group__erase:focus-visible {
  outline: 2px solid var(--danger);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* Nagłówek formularza zastępuje napis, który wcześniej niosło `<summary>`.
   Ten sam rozmiar i waga co łącze — podmiana nie ma zmieniać rytmu karty. */
.guest-add__title {
  margin: 0;
  color: var(--accent-strong);
  font-size: var(--text-caption);
  font-weight: 700;
}

.guest-add__form {
  margin-top: var(--space-4);
}

/* Podgląd na ekranie wydarzenia: WYPIS, nie narzędzie — bez akcji, bez
   reakcji na kursor, bez telefonu. Numer jest daną osobową i nie ma powodu,
   żeby stał na ekranie, na którym nic się z nim nie robi. */
.guest-preview {
  display: grid;
  gap: var(--space-3);
  margin: var(--space-4) 0 0;
  padding: 0;
  list-style: none;
}

.guest-preview__row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-3);
}

.guest-preview__name {
  font-weight: 700;
  overflow-wrap: anywhere;
}

/* NAZWA REKORDU, Z KTÓREGO WYMAZANO DANE — jedna reguła na cztery powierzchnie
   (lista gości: grupa i osoba, karta wydarzenia, skaner recepcji).

   „Zanonimizowano" nie jest niczyim nazwiskiem, tylko wartością zastępczą, więc
   ma się od nazwisk RÓŻNIĆ — inaczej lista wygląda jak lista gości o tym samym
   nazwisku. Kursywa (`font-style: italic`) niesie tu dokładnie to, co niesie
   w tekście drukowanym: „to jest napis o innym statusie niż sąsiednie".
   Przygaszenie (`--muted`, ten sam token co przy dopisku „Dane usunięte")
   odsuwa go na drugi plan, bo w wierszu ważniejsza jest odpowiedź RSVP
   i informacja o wejściu.

   ŚWIADOMIE BEZ CZERWIENI, mimo że to pierwszy odruch. Czerwony w tym systemie
   znaczy „błąd albo akcja nieodwracalna do rozważenia" (`.btn--danger`), a
   wymazanie danych jest stanem POPRAWNYM i już wykonanym — na liście po
   retencji (#22) czerwone byłyby wtedy wszystkie wiersze naraz, czyli ekran
   krzyczałby o tym, że zadziałał zgodnie z prawem. To ta sama zasada, którą
   opisuje komentarz przy `.guest__anonymised`.

   Znaczenia NIE NIESIE SAM WYGLĄD (WCAG 1.4.1): obok stoi napis „Dane usunięte"
   z datą, a tutaj sama treść jest słowem „Zanonimizowano". Kursywa i szarość
   tylko to wzmacniają. */
.guest__name--erased,
.guest-group__name--erased,
.guest-preview__name--erased,
.scan-result__name--erased,
.event-title--erased {
  color: var(--muted);
  font-style: italic;
}

.guest-preview__people {
  min-width: 0;
  color: var(--muted);
  font-size: var(--text-caption);
  overflow-wrap: anywhere;
}

/* --------------------------------------------------------------------------
   KOD DOSTĘPU GOŚCIA (#98)

   Trzy rzeczy, wszystkie na jednej zasadzie: stan kodu jest INFORMACJĄ, a sam
   kod — zdarzeniem jednorazowym. Stąd odznaka na karcie mówi wyłącznie „czy",
   a ekran z kodem jest jedynym miejscem, które pokazuje „jaki", i wygląda na
   coś, co się zaraz skończy.
   -------------------------------------------------------------------------- */

/* Plakietki paska listy w jednym pojemniku. Pasek jest `space-between`, więc
   trzecie dziecko rozpycha się MIĘDZY nagłówek a liczby — licznik „bez kodu"
   siadał wtedy na środku, oderwany od plakietki, z którą tworzy jedną
   informację. Zawinięcie w pojemnik przywraca paskowi dwa dzieci i pozwala
   plakietkom łamać się razem, gdy zabraknie szerokości. */
.toolbar__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-2);
}

/* Zerowanie marginesów jest tu WARUNKIEM działania, a nie porządkami — i to
   jest pułapka warta zapamiętania. `align-items: center` centruje MARGINES
   ZEWNĘTRZNY elementu, nie jego ramkę. Akapit `.badge` ma domyślne
   `margin-bottom: 1em`, zdejmowane wyłącznie ostatniemu (`:where(p, li)
   :last-child`), więc plakietka STOJĄCA PIERWSZA wnosiła 16 px marginesu
   pod spodem i jej ramka wypadała o 8 px WYŻEJ niż ramka sąsiadki. Dopóki
   plakietka była w pasku jedna, problemu nie było — jest zawsze ostatnia.

   Odstęp między plakietkami niesie `gap`, jak wszędzie indziej w tym arkuszu.
   Ten sam zabieg co `.page-header > *`. */
.toolbar__meta > * {
  margin: 0;
}

/* Licznik „bez kodu". Wariant plakietki bez wypełnienia: obok „Grupy: 12 ·
   Osoby: 34" dwie wypełnione plamy czytają się jak jedna liczba rozbita na
   dwie części. Rozróżnienia nie niesie sam kolor — niesie je napis i ikona
   (WCAG 1.4.1), a barwa je tylko wzmacnia.

   `inline-flex` zamiast odziedziczonego `inline-block`: bez tego ikona stoi
   na linii bazowej tekstu i plakietka rośnie o jej zejście, czyli o kilka
   pikseli więcej niż sąsiadka obok. */
.badge--attention {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  border-color: var(--accent-strong);
  background: transparent;
  color: var(--accent-strong);
}

/* Stan kodu w karcie grupy — ten sam rytm co wiersz z telefonem, bo to ta sama
   klasa informacji: co wiem o tej grupie, zanim ją otworzę. */
.guest-group__code {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-2) 0 0;
  color: var(--muted);
  font-size: var(--text-caption);
}

/* Brak kodu to LUKA DO UZUPEŁNIENIA, nie błąd — dlatego akcent, a nie
   `--danger`. Grupa bez kodu jest normalnym stanem świeżo wpisanej listy,
   a czerwień zarezerwowana dla błędów przestaje znaczyć „błąd", jeśli maluje
   się nią również rzeczy, które po prostu czekają na swoją kolej. */
.guest-group__code--missing {
  color: var(--accent-strong);
  font-weight: 700;
}

/* DANE GRUPY USUNIĘTE (#21) — trzeci stan tego wiersza, celowo wyglądający
   inaczej niż dwa pozostałe. „Bez kodu" jest luką do uzupełnienia (akcent),
   „kod wydany" stanem załatwionym (przygaszone), a ten jest STANEM
   KOŃCOWYM: nie ma tu nic do zrobienia i nie będzie. Wygaszony bardziej niż
   pozostałe, bo karta zanonimizowanej grupy nie ma prawa przyciągać wzroku
   na liście, na której reszta rodzin czeka na kody i odpowiedzi.

   Bez `--danger` i to jest decyzja tej samej klasy co przy „bez kodu": nic się
   nie zepsuło, wykonano żądanie osoby. Rozróżnienia nie niesie zresztą kolor —
   niosą je napis i ikona ostrzeżenia (WCAG 1.4.1). */
.guest-group__code--erased {
  color: var(--muted);
  font-style: italic;
}

/* Stan zgody (#99) — ten sam wiersz co stan kodu i celowo wygląda tak samo:
   obie informacje odpowiadają na pytanie „czy z tą grupą wszystko załatwione",
   a nie na dwa różne pytania. */
.guest-group__consent {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-2) 0 0;
  color: var(--muted);
  font-size: var(--text-caption);
}

/* Brak zgody NIE jest błędem i nie dostaje `--danger`: zgoda jest dobrowolna,
   więc grupa, która jej nie udzieliła, jest w stanie całkowicie poprawnym.
   Wyróżnienie mówi „tu nic nie masz", a nie „tu coś zepsułeś" — dokładnie ta
   sama decyzja co przy grupie bez kodu wyżej. */
.guest-group__consent--missing {
  color: var(--accent-strong);
  font-weight: 700;
}

/* POTRZEBY GRUPY (#15) — ten sam wiersz co stan kodu i stan zgody wyżej,
   celowo wyglądający tak samo: wszystkie trzy odpowiadają na jedno pytanie
   („czy z tą rodziną wszystko załatwione"), więc trzeci wygląd byłby trzecim
   pytaniem, którego nie ma.

   BEZ WYRÓŻNIENIA STANU „bez odpowiedzi" — inaczej niż przy zgodzie. Zgody
   brakuje wtedy, gdy Gość świadomie jej nie dał; tutaj brak odpowiedzi znaczy
   najczęściej „jeszcze nie zdążyli", a lista, na której każda rodzina świeci
   akcentem, przestaje wyróżniać cokolwiek. */
.guest-group__need {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-2) 0 0;
  color: var(--muted);
  font-size: var(--text-caption);
}

/* Komunikat otwierający ekran kodu — ten pod samym nagłówkiem.

   Odstęp z reguły nagłówkowej (16 px) wystarcza AKAPITOWI i nie wystarcza
   PUDEŁKU: ramka komunikatu domyka się optycznie tuż pod zejściem liter tytułu
   i całość czyta się jak jeden blok. Marginesy sąsiadów się zbiegają, więc
   24 px tutaj daje 24 px w sumie, a nie 40.

   Reguła jest ŚWIADOMIE ZAWĘŻONA do ekranów kodu, choć problem jest ogólny
   („komunikat zaraz pod nagłówkiem"). Wersja globalna (`h1 + .message`)
   przestawiłaby też agendę i listę gości — czyli ekrany, których to zadanie
   nie dotyczy i których nikt przy tej zmianie nie ogląda. */
.message--lead {
  margin-top: var(--space-5);
}

/* Karta z wydanym kodem, wstawiona w miejsce grupy. Ten sam zabieg co przy
   edycji i potwierdzaniu usunięcia (akcent NA KRAWĘDZI, bez wypełnienia),
   więc trzy różne stany jednej karty wyglądają jak jedna rodzina. */
.guest-group--issued {
  border-left: 3px solid var(--accent);
}

.guest-group--issued:hover {
  background: transparent;
}

/* Pole z kodem. Wyróżnione powierzchnią, bo to jedyna rzecz na tym ekranie,
   po którą Organizator przyszedł — i jedyna, której nie da się odzyskać. */
.code-card {
  margin: var(--space-5) 0;
  padding: var(--space-4) var(--space-5);
  border: 1px solid var(--border-control);
  border-radius: var(--radius);
  background: var(--surface-alt);
}

.code-card__label {
  margin: 0;
  color: var(--muted);
  font-size: var(--text-caption);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* Rodzina `monospace` jest tu NOŚNIKIEM TREŚCI, tak samo jak przy nazwach
   kolumn w imporcie CSV: kod bywa dyktowany przez telefon i przepisywany
   ręcznie, a w zapisie proporcjonalnym grupy po cztery znaki tracą równą
   długość, czyli jedyną wskazówkę, gdzie się jest w środku kodu.

   `overflow-wrap: anywhere` zamiast przewijania w bok: przy 380 px zapis
   XXXX-XXXX-XXXX-XXXX nie mieści się w jednym wierszu, a kod schowany za
   krawędzią ekranu jest kodem, którego nie widać przy przepisywaniu.

   `user-select: all` robi z całego pola jeden zaznaczalny obiekt — kliknięcie
   zaznacza kod w całości, także wtedy, gdy przycisku kopiowania nie ma
   (brak JavaScriptu albo brak schowka w przeglądarce). */
.code-card__code {
  margin: var(--space-2) 0 0;
  font-family: monospace;
  font-size: var(--text-title);
  line-height: var(--leading-tight);
  letter-spacing: 0.06em;
  overflow-wrap: anywhere;
  user-select: all;
}

/* Przycisk dokłada `panel.js` — w HTML-u go nie ma. Reguła jest tu mimo to,
   bo miejsce na przyszły element przygotowuje się REGUŁAMI, nie pustym
   pojemnikiem. */
.code-card__copy {
  margin-top: var(--space-3);
}

/* Link do zaproszenia i adresat. `<dl>`, bo to pary „nazwa — wartość", a nie
   akapity: czytnik ekranu ogłasza wtedy, czego dotyczy odczytywany adres. */
.code-facts {
  margin: 0 0 var(--space-5);
  max-width: var(--measure);
}

.code-facts dt {
  color: var(--muted);
  font-size: var(--text-caption);
}

/* Adres zaproszenia jest długi i nie ma w nim spacji — bez tego rozciąga
   kartę poza szerokość ekranu telefonu. */
.code-facts dd {
  margin: var(--space-1) 0 var(--space-4);
  overflow-wrap: anywhere;
}

/* --------------------------------------------------------------------------
   IMPORT GOŚCI Z PLIKU CSV (#7, etap 2)

   Dwa ekrany bez ani jednego atrybutu HTMX: wgranie pliku i podgląd przed
   zapisem. Świadomie pełne strony — podgląd pięciuset wierszy wstawiony
   w podmieniany fragment listy płaciłby się przy KAŻDEJ mutacji listy.
   -------------------------------------------------------------------------- */

/* Nazwa kolumny i przykładowa treść pliku. Rodzina `monospace` jest tu
   NOŚNIKIEM TREŚCI, a nie ozdobą: w zapisie proporcjonalnym `imie;nazwisko`
   nie widać, gdzie kończy się jedna kolumna, a średnik gubi się w interpunkcji
   zdania obok. Font systemowy, bo żaden krój z `static/` nie ma odmiany
   o stałej szerokości — a ściągnięcie takiego z CDN-u jest zakazane. */
code {
  padding: 0 var(--space-1);
  border-radius: var(--radius-sm);
  background: var(--surface-alt);
  font-family: monospace;
  font-size: var(--text-caption);
  overflow-wrap: anywhere;
}

/* Przykładowy plik. Przewijanie należy do BLOKU, nie do strony: wiersz
   `Anna;Nowak;+48 602 345 678;` nie mieści się na 380 px, a strona jadąca
   w bok zabiera ze sobą nagłówek i nawigację. */
.code-block {
  margin: var(--space-4) 0;
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-alt);
  overflow-x: auto;
}

/* Wnętrze `<pre>` ma już własne tło i dopełnienie — drugi prostokąt wokół
   każdej linii wyglądałby jak zaznaczenie fragmentu tekstu. */
.code-block code {
  padding: 0;
  background: none;
}

/* Strefa upuszczania pliku. Cała karta jest celem — ramka przerywana mówi
   „tu można coś położyć" bez ani jednego słowa, a przy okazji nie wygląda jak
   pole do wpisania tekstu, którym nie jest.

   Wysokość bierze się z `min-height`, a nie z dopełnienia: strefa musi być
   widocznie większa od przycisku obok, żeby czytało się to jako obszar, a nie
   jako kolejna kontrolka. Na telefonie ta sama liczba daje cel dotykowy
   kilkanaście razy większy od minimum z WCAG 2.5.5 — i to jest jedyna droga
   wyboru pliku na dotyku, bo przeciągania tam nie ma. */
.dropzone {
  display: grid;
  gap: var(--space-3);
}

.dropzone__area {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 11rem;
  padding: var(--space-6) var(--space-4);
  border: 2px dashed var(--border-control);
  border-radius: var(--radius);
  background: var(--surface-alt);
  text-align: center;
  cursor: pointer;
  transition: border-color var(--dur-fast) ease-out, background var(--dur-fast) ease-out;
}

.dropzone__area:hover {
  border-color: var(--border-control-hover);
  background: var(--accent-soft);
}

/* Ognisko stoi na STREFIE, a nie na kontrolce: po włączeniu skryptu kontrolka
   jest schowana wizualnie, więc jej własny pierścień nie miałby czego obrysować.
   `:focus-within` przenosi go na element, który użytkownik widzi — bez tego
   nawigacja klawiaturą przechodzi przez niewidzialny punkt. */
.dropzone:focus-within .dropzone__area {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

/* Plik wisi nad strefą. Linia ciągła zamiast przerywanej to sygnał NIEZALEŻNY
   od barwy — samo pociemnienie tła znika w trybie wysokiego kontrastu
   i u osób nierozróżniających barw (WCAG 1.4.1). */
.dropzone--over .dropzone__area {
  border-style: solid;
  border-color: var(--accent-strong);
  background: var(--accent-soft);
}

.dropzone__icon {
  width: 2.5rem;
  height: 2.5rem;
  color: var(--accent-strong);
}

.dropzone__title {
  font-size: var(--text-lead);
  font-weight: 700;
}

.dropzone__hint {
  max-width: 28rem;
  color: var(--muted);
  font-size: var(--text-caption);
}

/* Nazwa pliku i podpowiedź pod strefą są WYŚRODKOWANE i to nie jest ozdoba.
   Strefa zajmuje całą szerokość karty i ma treść na osi środka; akapit
   przyklejony do lewej krawędzi, urwany na 24 rem (`--field-max` z reguły
   `.field__help`), wyglądał pod nią jak resztka innego układu.

   Granica długości wiersza zostaje — inna, bo inna jest jej rola: tam chodziło
   o kolumnę pól, tu o miarę wiersza, którą da się przeczytać. Bez niej zdanie
   ciągnęłoby się na całą szerokość karty.

   Selektor jest ZŁOŻONY, nie owinięty w `:where()`, i to celowo: `:where()`
   zeruje wagę swojego argumentu, więc reguła miałaby dokładnie tę samą
   specyficzność co `.field__help` i rozstrzygałaby ją wyłącznie kolejność
   w pliku. Wystarczyłoby przenieść sekcję wyżej, żeby cicho przestała
   działać. */
.dropzone .dropzone__file,
.dropzone .field__help {
  max-width: 42rem;
  margin-inline: auto;
  text-align: center;
}

.dropzone__file {
  margin-block: 0;
  font-weight: 700;
  overflow-wrap: anywhere;
}

/* OBECNY PLIK — wiersz pod strefą, nie w środku niej (#123).

   Miejsce jest treścią: strefa to CEL UPUSZCZENIA, a to jest INFORMACJA
   ZWROTNA. Wsadzone do środka konkurowałoby o to samo miejsce co napis
   „przeciągnij plik tutaj", a przy okazji stałoby w elemencie, który
   `file-drop.js` podświetla przy przeciąganiu.

   Miniatura ma wymiary w atrybutach `<img>` (policzone z kolumn `logo_width`
   / `logo_height`, patrz `events.forms.LogoFileInput`), więc układ nie skacze
   przy jej wczytaniu. Arkusz stawia wyłącznie GÓRNĄ granicę — na wypadek
   wiersza sprzed #123, który wymiarów nie ma i przyszedłby bez atrybutów. */
.dropzone__current {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  flex-wrap: wrap;
}

.dropzone__thumb {
  display: inline-flex;
  padding: var(--space-2);
  border: 1px solid var(--border-control);
  border-radius: var(--radius-sm);
  /* Szachownica rysowana gradientami, bez pliku: logo bywa przezroczyste,
     a znak w jednolitej bieli na białym tle wygląda jak brak pliku. */
  background-color: var(--surface-card);
  background-image:
    linear-gradient(45deg, var(--surface-alt) 25%, transparent 25%),
    linear-gradient(-45deg, var(--surface-alt) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, var(--surface-alt) 75%),
    linear-gradient(-45deg, transparent 75%, var(--surface-alt) 75%);
  background-size: 12px 12px;
  background-position: 0 0, 0 6px, 6px -6px, -6px 0;
}

.dropzone__thumb img {
  display: block;
  max-width: 6rem;
  max-height: 6rem;
  object-fit: contain;
}

/* Zapowiedź usunięcia razem z drogą powrotną (#132). Ten sam układ co
   `.dropzone__current` wyżej, bo staje dokładnie w jego miejscu — pojemnik
   podglądu jest wtedy `hidden`, więc dwa bloki nigdy nie stoją naraz.
   Chowanie niesie atrybut `hidden`, który wygrywa z tym `display` regułą
   z resetu (`[hidden] { display: none !important }`). */
.dropzone__pending {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin: 0;
}

/* Etykieta OBOK pola wyboru, nie nad nim — ten sam wariant co `.field__inline`
   w `partials/_field.html` i z tego samego powodu: napis nad kwadracikiem jest
   oderwany od tego, co opisuje. */
.dropzone__current-clear {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

/* Natywnej kontrolki NIE chowa ten arkusz i to jest cała umowa o ścieżce bez
   JavaScriptu: bez skryptu kontrolka zostaje widoczna i to ona pokazuje nazwę
   wybranego pliku. Chowa ją dopiero `file-drop.js`, dokładając jej klasę
   `.visually-hidden` — czyli TĘ SAMĄ, której używa reszta projektu. Kopia tych
   ośmiu deklaracji pod własną nazwą byłaby drugą definicją „schowane wizualnie,
   ale nadal w drzewie ogniska" i rozjechałaby się przy pierwszej poprawce
   tamtej. */

/* Opis formatu pliku: akapity, tabela, ramka z ostrzeżeniem, przykład i lista.
   Rytm pionowy MUSI być tu zadeklarowany, bo połowa tych elementów nie ma
   własnych marginesów — `.table__scroll` i `.message` to zwykłe pojemniki,
   więc ostrzeżenie o telefonie przyklejało się do ostatniego wiersza tabeli
   i wyglądało jak jej stopka. Widać to wyłącznie na zrzucie.

   Nagłówek dostaje odstęp WIĘKSZY niż akapit — inaczej „Przykład" należy
   wzrokowo do sekcji, która się właśnie skończyła, a nie do tej, którą otwiera.
   Reguła stoi PO regule ogólnej i ma tę samą wagę (`:where()` zeruje swój
   argument), więc rozstrzyga kolejność w pliku, nie specyficzność. */
.import-help > * + * {
  margin-top: var(--space-4);
}

.import-help > * + :where(h2, h3) {
  margin-top: var(--space-6);
}

/* Nazwa kolumny NIE ŁAMIE SIĘ w środku wyrazu. Bez tego `overflow-wrap: anywhere`
   z reguły `code` (potrzebne przy długich wartościach w zdaniu) zawija na 380 px
   „nazwisko" na „nazwisk" + „o" — czyli pokazuje nazwę kolumny w postaci,
   w której nikt jej w pliku nie wpisze. Tabela ma własne przewijanie
   (`.table__scroll`), więc szerokości nie brakuje — brakowało zakazu. */
.import-help .table code {
  white-space: nowrap;
}

.import-preview__meta {
  margin-top: var(--space-4);
}

.import-preview {
  display: grid;
  gap: var(--space-4);
  margin: var(--space-5) 0 0;
  padding: 0;
  list-style: none;
}

/* Karta grupy do założenia: obrys, bez wypełnienia — ta sama zasada co przy
   liście gości. `--surface-alt` należy do pasów sekcji i przy kilkuset
   grupach dałoby ścianę ciemnych prostokątów. */
.import-preview__group {
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.import-preview__name {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  font-weight: 700;
  overflow-wrap: anywhere;
}

.import-preview__phone {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-2) 0 0;
  color: var(--muted);
  font-size: var(--text-caption);
}

.import-preview__people,
.import-preview__known {
  margin: var(--space-3) 0 0;
  padding-left: var(--space-5);
  overflow-wrap: anywhere;
}

/* --------------------------------------------------------------------------
   ODMOWA PO PRZEKROCZENIU LIMITU PRÓB — ekran 429 (#23)

   Karta wyśrodkowana, bez akcji. To jedyny ekran w projekcie, na którym NIE MA
   nic do kliknięcia — i tak ma zostać: każdy przycisk prowadziłby z powrotem
   pod tę samą blokadę, a ekran jest odpowiedzią „wróć za chwilę", nie miejscem
   pracy.

   Barwa ikony i czasu idzie z `--accent-strong`, nie z `--danger`: to nie jest
   błąd użytkownika ani awaria, tylko chwila przerwy. Czerwień komunikowałaby
   coś, co się zepsuło.
   -------------------------------------------------------------------------- */

.waiting {
  max-width: var(--card-max);
  margin-inline: auto;
  text-align: center;
}

/* Ikona jako element kompozycji, a nie znaczek przy tekście — stąd rozmiar
   z jawnym mnożnikiem `em` na własnym stopniu pisma. Wysokość linii zerowana:
   `.icon` stoi na `vertical-align`, więc w bloku o wysokości linii treści
   dokłada pod sobą kilka pikseli i nagłówek przestaje być równo odsunięty. */
.waiting__icon {
  width: 2.5em;
  height: 2.5em;
  font-size: var(--text-lead);
  line-height: 0;
  color: var(--accent-strong);
}

/* Zdanie z czasem oczekiwania. `text-wrap: balance` nie wystarcza — chodzi
   o to, żeby „za 22 minuty" nie łamało się MIĘDZY liczbą a jednostką, bo
   liczba zostaje wtedy sama na końcu wiersza i czyta się jak część zdania
   powyżej. Nierozdzielność niesie `nowrap` na samej frazie, nie na akapicie:
   akapit musi mieć prawo się złamać, inaczej wychodzi poza kartę przy 380 px. */
.waiting__when {
  font-size: var(--text-lead);
}

.waiting__when strong {
  white-space: nowrap;
  color: var(--accent-strong);
}

/* --------------------------------------------------------------------------
   WARSTWA PUBLICZNA — mieszka w `static/css/guest.css` (#102)

   Kompozycja zaproszenia Gościa (`.invite*`, `.hero*`, `.gate`, `.people*`,
   `.leave*` i nadpisania osi czasu dla Gościa) wyprowadziła się STĄD do
   osobnego arkusza, wpiętego wyłącznie w `guests/public/base_public.html`.

   Powód jest jeden i jest to powód pielęgnacyjny: ten ekran jest jedyną
   powierzchnią produktu oglądaną przez osoby postronne i będzie poprawiany
   częściej niż cała reszta arkusza razem wzięta. Dopóki leżał tutaj, każda
   poprawka wyglądu zaproszenia była zmianą w pliku, który wchodzi także do
   panelu Organizatora i na ekran logowania — czyli za każdym razem trzeba
   było udowodnić, że NIE ruszyła tamtych. Teraz to wynika z wpięcia.

   ZOSTAJE TUTAJ, i to celowo:
     * wszystkie TOKENY, także te używane wyłącznie przez warstwę Gościa
       (`--text-display`, `--accent-on-hero`, `--on-hero-muted`) — warstwa
       wspólna jest jedynym miejscem, w którym powstają wartości pochodne
       (ADR-027), i rozdział arkuszy tego nie zmienia;
     * baza osi czasu (`.timeline*`) i nagłówek dnia (`.agenda__date`) —
       współdzielone z agendą Organizatora. Gość dostaje NADPISANIA tych
       samych klas w `guest.css`, nie drugi zestaw znaczników.
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   HTMX — przejścia przy podmianie fragmentów
   -------------------------------------------------------------------------- */

/* Zanikanie działa zawsze: klasę `htmx-swapping` HTMX DODAJE, więc przejście
   odpala się samo. */
.htmx-swapping {
  opacity: 0;
  transform: translateY(-0.25rem);
  transition: opacity var(--dur-fast) ease-out, transform var(--dur-fast) ease-out;
}

/* Pojawianie wymaga przejścia zadeklarowanego NA elemencie, bo `htmx-settling`
   jest ZDEJMOWANA — dlatego cel podmiany oznaczamy dodatkowo klasą `hx-swap`. */
.hx-swap {
  transition: opacity var(--dur) ease-in, transform var(--dur) ease-in;
}

.hx-swap.htmx-settling {
  opacity: 0;
  transform: translateY(0.25rem);
}

/* --------------------------------------------------------------------------
   EKRAN RECEPCJI (#9, etap 3) — skaner biletów przy wejściu.

   Cały ten blok jest projektowany pod JEDEN scenariusz: telefon trzymany
   w jednej ręce, przy drzwiach, w półmroku albo w słońcu, a odpowiedź czytana
   w ćwierć sekundy. Stąd duże cele dotykowe i podgląd kamery zajmujący tyle
   miejsca, ile się da — celowanie w kod jest tu czynnością manualną.
   -------------------------------------------------------------------------- */

/* Odstęp WEWNĄTRZ sekcji jest mniejszy niż odstęp od tytułu i od „drugiej
   drogi" pod spodem — inaczej komunikat stanu i podgląd kamery czytają się
   jak dwie niezwiązane sekcje, a są jedną odpowiedzią na jedno pytanie.
   Wyszło z przeglądu na komputerze: bloki stały „bez sensu oddalone". */
.scanner {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-top: var(--space-4);
}

/* Przycisk startowy na całą szerokość: uruchamia się go kciukiem, patrząc na
   kolejkę, a nie na ekran. To jedyna akcja tego ekranu przed pierwszym skanem
   (D8 — kamera nie rusza z wczytania strony). */
.scanner__enable {
  width: 100%;
}

/* Podgląd ma STAŁE proporcje i własne tło: bez nich układ podskakuje w chwili,
   w której strumień dojdzie i przeglądarka pozna wymiary klatki — czyli
   dokładnie wtedy, gdy obsługa celuje już w kod.

   `object-fit: cover` zamiast rozciągania: zniekształcony obraz to gorszy
   odczyt, a nie tylko brzydszy podgląd. */
.scanner__viewport {
  position: relative;
  overflow: hidden;
  aspect-ratio: 3 / 4;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--ink);
}

.scanner__video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Szeroki ekran nie potrzebuje podglądu na pół strony — kod trzyma się wtedy
   przed kamerą laptopa, z odległości ręki, a pionowy kadr telefonu przestaje
   mieć sens. */
@media (min-width: 40rem) {
  .scanner__viewport {
    aspect-ratio: 4 / 3;
    max-height: 60vh;
  }
}

/* Licznik stoi NAD skanerem, bo to jedyna liczba, którą recepcja sprawdza
   sama z siebie — a nie odpowiedź na skan, więc nie ma jej co mieszać z kartą
   wyniku. `align-self` trzyma plakietkę przy lewej krawędzi: rozciągnięta na
   całą szerokość udawałaby pasek stanu. */
.scanner__count {
  align-self: flex-start;
  margin: var(--space-4) 0 0;
}

.scanner__fallback {
  margin-top: var(--space-5);
}

/* --------------------------------------------------------------------------
   KARTA WYNIKU SKANU (#9, etap 3) — jedyna rzecz, na którą patrzy recepcja.

   Obowiązek nadrzędny jest jeden: CZYTELNOŚĆ Z ODLEGŁOŚCI RĘKI, W PÓŁMROKU,
   W ĆWIERĆ SEKUNDY. Wszystko poniżej wynika z tego zdania, a nie z upodobań.

   KOLOR NIGDY NIE JEST JEDYNYM NOŚNIKIEM STANU (D9, WCAG 1.4.1): każdy stan ma
   własny ZNAK i własny NAPIS, a barwa je wzmacnia. Telefon w słońcu gubi
   różnicę odcieni, a osoba nierozróżniająca kolorów nie ma jej wcale.
   -------------------------------------------------------------------------- */

/* Pusty pojemnik nie zajmuje miejsca — inaczej ekran przed pierwszym skanem
   miałby dziurę wielkości karty, a podgląd kamery zjechałby poza widok.
   `:empty` jest tu poprawne dokładnie dlatego, że serwer wstawia CAŁĄ kartę
   albo nic; stanu „pusta karta z ramką" nie ma. */
.scan-result:empty {
  display: none;
}

.scan-result {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-left: 4px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-card);
  /* Wejście krótkie i BEZ opóźnienia: karta ma być czytelna, zanim obsługa
     zdąży podnieść wzrok, a następny gość stoi już przy drzwiach. 160 ms to
     tyle, żeby oko zauważyło ZMIANĘ (a nie tylko inną treść w tym samym
     miejscu) i ani chwili dłużej. */
  animation: scan-result-in 160ms ease-out;
}

/* JEDNA deklaracja `from`, bez `to` — stanem po animacji jest stan SPOCZYNKOWY
   elementu (ta sama zasada co w warstwie ruchu Gościa).
   Dzięki temu karta nie ma jak zostać przezroczysta, gdy animacja nie dojdzie
   do końca albo zostanie wygaszona przez `prefers-reduced-motion`. */
@keyframes scan-result-in {
  from {
    opacity: 0;
    transform: translateY(0.5rem);
  }
}

/* Plakietka stanu: ZNAK plus NAPIS, w tej kolejności, w jednej linii pisma.
   To ona jest czytana pierwsza — nazwisko dopiero potwierdza, kogo dotyczy. */
.scan-result__badge {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  font-size: var(--text-lead);
  font-weight: 700;
}

/* Znak większy niż w wierszu listy: przy 1.15em ginie obok napisu w stopniu
   `--text-lead`, a to on niesie rozróżnienie tam, gdzie barwa zawodzi. */
.scan-result__icon {
  width: 1.6em;
  height: 1.6em;
  flex: none;
}

/* Nazwisko w stopniu NAGŁÓWKA STRONY — większe niż cokolwiek innego na tym
   ekranie. `overflow-wrap` z tego samego powodu co przy `.page-title`: to jest
   cudzy tekst, a jedno długie nazwisko bez spacji rozpycha kartę najszybciej. */
.scan-result__name {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-page);
  font-weight: 700;
  line-height: var(--leading-tight);
  overflow-wrap: anywhere;
}

.scan-result__rsvp,
.scan-result__lead {
  margin: 0;
}

/* STÓŁ NA KARCIE SKANERA (#16) — zdanie, które obsługa mówi gościowi na głos,
   więc stopień JAK TEKST KARTY, nie jak podpis: czyta się je z odległości ręki,
   przy drzwiach, tak samo jak nazwisko nad nim.

   `flex` z `gap` — karta i tak jest blokiem na całą szerokość, więc nie ma tu
   pułapki z linią bazową, która wymusza układ liniowy na plakietce przy
   nazwisku na liście gości. */
.scan-result__table {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-2) 0 0;
}

.scan-result__lead {
  color: var(--muted);
  font-size: var(--text-caption);
}

.scan-result__undo {
  margin-top: var(--space-2);
}

/* WPUSZCZONO — zielone, bo to jedyny stan, po którym nie trzeba nic robić.
   Barwa tekstu z `--success`, tło z `--success-bg`: oba są tokenami
   SYGNALIZACYJNYMI spoza palet, więc „wpuszczono" wygląda tak samo na weselu
   i na konferencji. */
.scan-result--admitted {
  border-color: color-mix(in srgb, var(--success) 30%, var(--success-bg));
  border-left-color: var(--success);
  background: var(--success-bg);
}

.scan-result--admitted .scan-result__badge {
  color: var(--success);
}

/* JUŻ WPUSZCZONY — bursztyn, świadomie NIE zielony i NIE czerwony.
   To nie jest błąd (gość wraca z zewnątrz) ani sukces (nikogo właśnie nie
   wpuszczono): ten sam bilet drugi raz może znaczyć, że ktoś przesłał kod
   dalej, więc stan ma ZATRZYMAĆ WZROK, a nie uspokoić.

   Pierwsza wersja brała tu akcent panelu i wypadała prawie tak samo jak stan
   „cofnięto" — dwa różne zdarzenia w jednej barwie, przy karcie czytanej
   w ćwierć sekundy. Stąd trzeci token sygnalizacyjny (uzasadnienie i policzony
   kontrast przy jego definicji). */
.scan-result--repeat {
  border-color: color-mix(in srgb, var(--attention) 30%, var(--attention-bg));
  border-left-color: var(--attention);
  background: var(--attention-bg);
}

.scan-result--repeat .scan-result__badge {
  color: var(--attention);
}

/* BILET NIEWAŻNY — czerwone i bez nazwiska, bo nie ma czyjego pokazać.
   Karta jest wtedy krótsza i to też jest sygnał: inny KSZTAŁT widać wcześniej
   niż inny kolor. */
.scan-result--invalid {
  border-color: color-mix(in srgb, var(--danger) 30%, var(--danger-bg));
  border-left-color: var(--danger);
  background: var(--danger-bg);
}

.scan-result--invalid .scan-result__badge {
  color: var(--danger);
}

/* COFNIĘTO — neutralne. Potwierdzenie, że korekta się udała, nie ostrzeżenie:
   pomyłka przy automatycznej odprawie jest normalną częścią wieczoru, a nie
   zdarzeniem, które trzeba podkreślić na czerwono. */
.scan-result--undone {
  border-left-color: var(--muted);
  background: var(--surface-alt);
}

.scan-result--undone .scan-result__badge {
  color: var(--muted);
}

/* --------------------------------------------------------------------------
   Galeria zdjęć (#18)

   JEDEN KOMPONENT DLA OBU POWIERZCHNI — panelu Organizatora i zaproszenia
   Gościa. Stoi w `app.css`, a nie w `guest.css`, bo `app.css` wchodzi wszędzie,
   a `guest.css` wyłącznie do warstwy Gościa. To ta sama konstrukcja co przy osi
   czasu (`.timeline*`): znaczniki i siatka są wspólne, a `guest.css` mógłby
   dołożyć własną SKÓRĘ, gdyby jej potrzebował. Dziś nie potrzebuje — kafelek
   zdjęcia wygląda tak samo po obu stronach i w obu profilach prezentacji.
   Jedyne, co warstwa Gościa dokłada osobno, to `order` sekcji `#gallery`.

   SIATKA `auto-fill`, NIE STAŁA LICZBA KOLUMN. Kafelki mają rosnąć od 390 px do
   1440 px bez ani jednego zapytania medialnego: `minmax(8rem, 1fr)` daje dwie
   kolumny na telefonie, cztery–pięć na laptopie i tyle samo w obu profilach.
   Zapytanie medialne byłoby tu drugą listą progów obok tej, którą ma już
   `.detail-grid`, i rozjeżdżałoby się z nią przy pierwszej korekcie.

   KWADRATOWE KAFELKI (`aspect-ratio`) i `object-fit: cover`. Zdjęcia przychodzą
   w dwóch orientacjach naraz — pionowe z telefonów, poziome z lustrzanki —
   a siatka z wierszami różnej wysokości czyta się jak awaria układu. Kadrowanie
   dotyczy WYŁĄCZNIE miniatury: pod adresem leży pełny obraz w proporcjach
   oryginału, a pobranie oddaje właśnie jego.

   ATRYBUTY `width`/`height` NA `<img>` ZOSTAJĄ MIMO `aspect-ratio` — nie są
   nadmiarowe. Wymiary z wiersza dają przeglądarce proporcję ZANIM wczyta się
   arkusz i zanim dojedzie obrazek; bez nich pierwsze wejście na galerię przesuwa
   stronę pod palcem przy każdym doczytanym kafelku (CLS).
   -------------------------------------------------------------------------- */

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Pojemnik pozycji jest kontekstem pozycjonowania dla odnośnika „Usuń" — ten
   stoi NA zdjęciu, w rogu, bo pod zdjęciem zabierałby wysokość każdemu wierszowi
   siatki, także tym bez własnych zdjęć. */
.gallery__item {
  position: relative;
}

.gallery__link {
  display: block;
  border-radius: var(--radius-sm);
}

.gallery__image {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: var(--radius-sm);
  /* Plama pod obrazkiem, widoczna ZANIM ten się wczyta. Bez niej siatka jest
     przez chwilę zbiorem dziur w karcie — a przy dwudziestu czterech kafelkach
     dojeżdżających po kolei ta chwila trwa całe przewinięcie. */
  background: var(--border);
}

.gallery__link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ODNOŚNIK „USUŃ" MA WŁASNE TŁO, a nie samą barwę tekstu. Leży na zdjęciu,
   czyli na tle, o którym nie wiemy NIC — biała suknia i czarny garnitur trafiają
   się w tej samej siatce. Półprzezroczysta plama pod napisem jest jedyną
   konstrukcją, która trzyma kontrast w obu przypadkach; sam kolor tekstu
   znikałby na połowie zdjęć.

   Pole kliknięcia dobite `padding`-iem do celu wielkości kciuka — to jest
   kontrolka klikana na telefonie, jedną ręką, w tłumie. */
.gallery__remove {
  position: absolute;
  top: var(--space-1);
  right: var(--space-1);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-sm);
  background: rgb(0 0 0 / 62%);
  color: #fff;
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  text-decoration: none;
}

.gallery__remove:hover {
  background: rgb(0 0 0 / 80%);
}

.gallery__remove:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}

.gallery__pages {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

.gallery__form {
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}

/* Podgląd na ekranie potwierdzenia — TU KADROWANIA NIE MA i to jest różnica
   względem kafelka. Pytanie brzmi „czy to na pewno TO zdjęcie", więc obcięcie
   połowy kadru byłoby dokładnie tą informacją, której nie wolno zabrać.
   Wysokość ograniczona jednostką `vh`, żeby przyciski zostały w kadrze telefonu
   razem z obrazkiem. */
.gallery__preview {
  display: block;
  width: 100%;
  max-height: 50vh;
  height: auto;
  margin: var(--space-4) 0;
  object-fit: contain;
  border-radius: var(--radius-sm);
}

/* --------------------------------------------------------------------------
   Podgląd zdjęć WYBRANYCH do wgrania (#18)

   Rysuje go `gallery-upload.js` z adresów `blob:`, więc bez skryptu ta lista
   jest pusta i `hidden` — reguły niżej nie mają wtedy czego ostylować i nie
   zajmują ani piksela.

   Kafelki są MNIEJSZE niż w galerii i to jest celowe: to nie jest treść, tylko
   kontrola tego, co zaraz poleci. Mniejszy moduł mieści dwadzieścia miniatur
   bez spychania przycisku „Dodaj zdjęcia" poza ekran telefonu.
   -------------------------------------------------------------------------- */

/* PASEK ROZCIĄGA SIĘ NA OBIE KOLUMNY FORMULARZA — bez tego siedzi w jednej
   i marnuje połowę karty. `.form-grid` ma dwie kolumny, a `<ul>` nie jest
   `.field`, więc reguła obok (`.form-grid > .field`) go nie obejmowała: pasek
   dostawał ~340 px zamiast ~680 i mieścił trzy kafelki w rzędzie zamiast sześciu.
   Zmierzone na zrzucie, nie przewidziane. */
.form-grid > .picker {
  grid-column: 1 / -1;
}

.picker {
  display: grid;
  /* 6 rem, nie 5: przy pełnej szerokości karty mniejszy moduł dawałby kafelki
     wielkości znaczka, na których nie da się rozpoznać zdjęcia — a po to ten
     pasek istnieje. Sześć kafelków w rzędzie na laptopie, trzy na telefonie. */
  grid-template-columns: repeat(auto-fill, minmax(6rem, 1fr));
  gap: var(--space-2);
  margin: var(--space-4) 0 0;
  padding: 0;
  list-style: none;
}

.picker__item {
  position: relative;
}

.picker__image {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: var(--radius-sm);
  background: var(--border);
}

/* Ten sam wzorzec co `.gallery__remove`: plama pod napisem, bo tłem jest cudze
   zdjęcie, o którego jasności nie wiemy nic. */
.picker__remove {
  position: absolute;
  top: var(--space-1);
  right: var(--space-1);
  padding: 0 var(--space-1);
  border: 0;
  border-radius: var(--radius-sm);
  background: rgb(0 0 0 / 62%);
  color: #fff;
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  cursor: pointer;
}

.picker__remove:hover {
  background: rgb(0 0 0 / 80%);
}

.picker__remove:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}

/* --------------------------------------------------------------------------
   Przeglądarka zdjęć (#18)

   Element `<dialog>` tworzy `gallery.js`; bez skryptu nic z tego nie powstaje,
   a kafelek zostaje zwykłym łączem do pobrania.

   `<dialog>` OTWARTY MODALNIE DAJE Z PUDEŁKA trzy rzeczy, których nie
   odtwarzamy ręcznie: warstwę nad całą stroną (`::backdrop`), pułapkę ogniska
   i zamykanie klawiszem Escape. To jest cały powód, dla którego skrypt kończy
   pracę, gdy przeglądarka tego elementu nie zna — własna nakładka byłaby drugą
   implementacją modala, z których jedna zawsze gubi dostępność.
   -------------------------------------------------------------------------- */

/* WYŚWIETLANIE WIĄŻE SIĘ ZE STANEM `[open]`, NIGDY z samą klasą — i to jest
   tu najważniejsza linia w całym bloku.

   Przeglądarka chowa zamknięty `<dialog>` regułą `dialog { display: none }`
   o swoistości (0,0,1). Zwykłe `.lightbox { display: grid }` ma (0,1,0), więc
   ją WYGRYWA — a wtedy okno nie chowa się nigdy: ani po kliknięciu „Zamknij",
   ani przed pierwszym otwarciem, gdzie wisi jako czarny prostokąt na końcu
   dokumentu. Objaw zgłoszony z przeglądu brzmiał „klikam zamknij i zostaje
   tekst zdjęcia": okno zostawało otwarte, a `src` był już zdjęty.

   Reguła zamykająca należy więc do STANU, a nie do komponentu. Pozostałe
   deklaracje mogą zostać na klasie — przy `display: none` nie robią nic.
   Pilnuje tego bramka `test_the_lightbox_is_only_displayed_when_open`. */
.lightbox[open] {
  display: grid;
}

.lightbox {
  /* GEST „WSTECZ" PRZEGLĄDARKI BLOKUJE `overscroll-behavior`, nie sam
     `preventDefault()` w skrypcie — i to jest naprawa zgłoszenia „swipe w lewo
     wychodzi z ekranu zdjęć". Poziome przesunięcie dwoma palcami jest na macOS
     skrótem do nawigacji wstecz; przeglądarka rozstrzyga o nim ZANIM strumień
     zdarzeń `wheel` w ogóle dojdzie do naszego nasłuchu, więc obrona po stronie
     JavaScriptu bywa spóźniona. `contain` zatrzymuje łańcuch przewijania na tym
     elemencie, a `overflow: hidden` czyni go pojemnikiem przewijania, bez czego
     ta deklaracja nie robiłaby nic.

     `dialog` ma własne domyślne marginesy i szerokość dobraną do treści —
     tutaj ma wypełnić ekran, więc oba trzeba znieść jawnie. */
  overflow: hidden;
  overscroll-behavior: contain;
  width: 100%;
  max-width: 100%;
  height: 100%;
  max-height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  grid-template-rows: 1fr auto;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  background: rgb(0 0 0 / 92%);
  color: #fff;
}

.lightbox::backdrop {
  background: rgb(0 0 0 / 80%);
}

/* `height: 100%` I `overflow: hidden` RAZEM, nie osobno. Bez wysokości scena
   bierze rozmiar od obrazu, więc `max-height: 100%` na `<img>` liczy się od
   niczego i zdjęcie wjeżdża pod pasek przycisków — zmierzone na zrzucie, nie
   przewidziane. `overflow` domyka to dla obrazów o skrajnych proporcjach. */
.lightbox__stage {
  display: flex;
  align-items: center;
  justify-content: center;
  grid-row: 1;
  grid-column: 2;
  min-width: 0;
  min-height: 0;
  height: 100%;
  padding: var(--space-3);
  overflow: hidden;

  /* PRZESUWANIE PALCEM (#18). `--drag` ustawia `gallery.js` w trakcie gestu;
     domyślne zero znaczy „nic się nie dzieje", więc bez skryptu ta deklaracja
     nie robi nic. Skrypt podaje LICZBĘ, `transform` stoi tutaj — wygląd należy
     do arkusza, tak jak wszędzie w tym projekcie.

     `touch-action: pan-y pinch-zoom` oddaje przeglądarce przewijanie w pionie
     i przybliżanie dwoma palcami, a zostawia nam POZIOM. Wartość `none`
     zabrałaby powiększanie, czyli jedyny sposób obejrzenia szczegółu zdjęcia
     na telefonie. */
  --drag: 0px;

  transform: translateX(var(--drag));
  transition: transform 180ms ease-out;
  touch-action: pan-y pinch-zoom;
}

/* W TRAKCIE gestu bez przejścia — inaczej obraz wlecze się za palcem zamiast
   iść z nim. Przejście wraca po puszczeniu i odgrywa powrót na miejsce albo
   dojście do zera przy zmianie zdjęcia. Globalny blok `prefers-reduced-motion`
   (koniec tego pliku) wygasza je bez ani jednej gałęzi w skrypcie. */
.lightbox__stage.is-dragging {
  transition: none;
}

/* KADROWANIA TU NIE MA — odwrotnie niż w kafelku. Podgląd ma pokazać CAŁE
   zdjęcie w jego proporcjach, bo po to się go otwiera; `contain` razem
   z ograniczeniem obu wymiarów trzyma je w kadrze bez przycinania. */
.lightbox__image {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

/* PRZEJŚCIE MIĘDZY ZDJĘCIAMI — klasę zakłada `gallery.js` przy każdej zmianie,
   a `--from` niesie stronę, z której zdjęcie „przyjeżdża". Bez tego podmiana
   `src` jest przeskokiem: oko nie wie, w którą stronę poszła galeria.

   Krótko (200 ms) i BEZ ruchu w drugą stronę: animacja ma pokazać kierunek,
   a nie opóźnić oglądanie. Wygaszenie ruchu (`prefers-reduced-motion`, koniec
   tego pliku) skraca ją do 1 ms — zdjęcie zmienia się wtedy natychmiast i to
   jest zachowanie POPRAWNE, nie zubożone. */
.lightbox__image.is-entering {
  animation: photo-enter 200ms ease-out;
}

@keyframes photo-enter {
  from {
    opacity: 0;
    transform: translateX(var(--from, 0));
  }
}

.lightbox__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  grid-row: 2;
  grid-column: 1 / -1;
  padding: var(--space-4);
}

.lightbox__counter {
  margin: 0;
  color: #fff;
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-variant-numeric: tabular-nums;
}

/* Przyciski w pasku stoją na CIEMNYM tle, więc wariant `--ghost` z palety
   panelu byłby tu nieczytelny: jego barwa jest liczona dla jasnej karty. */
.lightbox__close {
  border-color: rgb(255 255 255 / 55%);
  background: transparent;
  color: #fff;
}

.lightbox__close:hover {
  border-color: #fff;
  background: rgb(255 255 255 / 12%);
}

/* Strzałki są CELAMI DOTYKOWYMI, nie ikonkami: szerokość dobrana pod kciuk
   (WCAG 2.5.5), wysokość duża, ale OGRANICZONA. Pełna wysokość dawała cel
   wygodny i obrys ogniska ciągnący się przez cały ekran — a obrys ma pokazywać,
   gdzie jest ognisko, nie zakreślać połowę okna. `min()` trzyma jedno i drugie:
   dziesięć rem na laptopie, cała wysokość na niskim ekranie. */
.lightbox__nav {
  display: flex;
  align-items: center;
  justify-content: center;
  align-self: center;
  grid-row: 1;
  width: 3rem;
  height: min(100%, 10rem);
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: #fff;
  /* Ze SKALI, nie z oka — pilnuje tego bramka `test_stylesheet_uses_only_the_type_scale`,
     i słusznie: złapała tu wpisane `2.5rem`. */
  font-size: var(--text-page);
  line-height: 1;
  cursor: pointer;
}

.lightbox__nav--previous {
  grid-column: 1;
}

.lightbox__nav--next {
  grid-column: 3;
}

.lightbox__nav:hover {
  background: rgb(255 255 255 / 10%);
}

.lightbox__nav:focus-visible {
  outline: 2px solid #fff;
  outline-offset: -4px;
}

/* --------------------------------------------------------------------------
   Materiały do pobrania — ekran Organizatora (#125)

   Lista wierszy „nazwa + metryka + Usuń", zbudowana tym samym rytmem co
   pozostałe listy panelu: kreska włosowa między pozycjami, akcja po prawej,
   treść po lewej. Na wąskim ekranie wiersz się zawija, a akcja schodzi pod
   nazwę — nie ma tu tabeli, więc nie ma czego przewijać w bok.

   Sekcja jest krótka i taka ma zostać: cały ekran to lista plików i jeden
   formularz, a wszystko poza tą listą składa się z komponentów, które panel
   już ma (`.card`, `.form-grid`, `.btn`, `.text-muted`).
   -------------------------------------------------------------------------- */

.materials-admin {
  margin: 0;
  padding: 0;
  list-style: none;
}

.materials-admin__item {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--border);
}

.materials-admin__item:first-child {
  padding-top: 0;
}

.materials-admin__item:last-child {
  padding-bottom: 0;
  border-bottom: 0;
}

/* Nazwa i metryka jedna pod drugą — metryka jest podpisem nazwy, a nie
   osobną kolumną. Przy nazwie długiej na dwie linie kolumnowy układ zostawiał
   „PDF · 2,1 MB" wiszące samotnie po środku wiersza. */
.materials-admin__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.materials-admin__name {
  /* Nazwa pliku bywa długa i bywa bez spacji — bez tego rozpycha kartę. */
  overflow-wrap: anywhere;
  font-weight: 600;
}

.materials-admin__remove {
  flex: none;
}

/* Nazwa i metryka na ekranie potwierdzenia usunięcia — ten sam układ,
   jeden egzemplarz, więc bez listy. */
.material-summary {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-bottom: var(--space-5);
}

/* Komunikat o odpowiedzialności za treść materiałów (#125, D10). Wyróżniony
   plamą tła i kreską z boku, bo ma być PRZECZYTANY przed wgraniem, a nie
   zauważony po fakcie — ale w barwie neutralnej, nie ostrzegawczej: to jest
   warunek korzystania, a nie ostrzeżenie o błędzie. */
.notice {
  margin: 0 0 var(--space-5);
  padding: var(--space-4);
  max-width: var(--measure);
  border-left: 3px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-alt);
  color: var(--muted);
  font-size: var(--text-caption);
}

/* --------------------------------------------------------------------------
   DOKUMENTY PRAWNE (#163, ADR-066)

   Kolumna jest SZERSZA niż karta formularza i węższa niż strona: dokument ma
   zagnieżdżone listy i tabele, więc `--card-max-wide` (34rem) łamałby wiersz
   tabeli, a pełna szerokość dałaby miarę, przy której oko gubi początek linii.

   NUMERACJA IDZIE LICZNIKAMI, nie `list-style-type`. Powód jest jeden i twardy:
   dokument cytuje sam siebie („ust. 5 i 6", „lit. a"), a wersja `.tex` numeruje
   `1.` i `a)`. `list-style-type: lower-alpha` daje `a.` — czyli znacznik, który
   przy przepisywaniu odsyłacza z PDF-a do strony wygląda na inny.
   -------------------------------------------------------------------------- */

.page__body--legal {
  max-width: var(--legal-max);
}

/* NA SZEROKIM EKRANIE DOKUMENT SIĘ ROZCIĄGA. Miara dobra dla prozy czytanej
   linearnie jest tu za wąska: dokument prawny SKANUJE się po numerach, a każdy
   punkt to jedno–dwa zdania, więc dłuższy wiersz nie gubi czytelnika, za to
   skraca stronę o kilka ekranów przewijania. */
@media (min-width: 75rem) {
  :root {
    --legal-max: 60rem;
  }
}

/* KRESKA POD TYTUŁEM POTRZEBUJE ODDECHU Z OBU STRON. Sama `padding-bottom`
   odsuwała ją od tytułu, ale zostawiała przyklejoną do pierwszego rozdziału —
   linia czytała się wtedy jak podkreślenie nagłówka, który stoi POD nią. */
.legal__header {
  padding-bottom: var(--space-5);
  margin-bottom: var(--space-7);
  border-bottom: 1px solid var(--border);
}

.legal__section + .legal__section,
.legal__chapter {
  margin-top: var(--space-6);
}

/* Metryka dokumentu — numer wersji. Stoi na końcu i ma się nie mierzyć
   z treścią: to informacja, po którą sięga się świadomie (przy sporze o to,
   na co ktoś się zgodził), a nie nagłówek strony. */
.legal__colophon {
  margin-top: var(--space-7);
  padding-top: var(--space-5);
  border-top: 1px solid var(--border);
}

/* Rozdział Regulaminu — środkowany, wersalikami, jak w wersji do druku. */
.legal__chapter {
  text-align: center;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-size: var(--text-body);
}

.legal__section h2 {
  font-size: var(--text-lead);
}

/* Regulamin ma o jeden poziom więcej od reszty zestawu: paragrafy siedzą
   w rozdziałach, więc nagłówek `§` jest tam `<h3>`, a nie `<h2>`. Klasa
   wyrównuje jego STOPIEŃ PISMA do paragrafu w dokumentach bez rozdziałów —
   hierarchia znaczników zostaje poprawna w każdym dokumencie z osobna. */
.legal__heading {
  font-size: var(--text-lead);
}

.legal__section h3 {
  font-size: var(--text-body);
}

.legal ol,
.legal ul {
  padding-left: 0;
  list-style: none;
}

.legal li + li {
  margin-top: var(--space-3);
}

.legal__clauses,
.legal__points {
  counter-reset: legal-item;
}

.legal__clauses > li,
.legal__points > li,
.legal__bullets > li {
  position: relative;
  padding-left: var(--space-6);
}

.legal__clauses > li::before,
.legal__points > li::before,
.legal__bullets > li::before {
  position: absolute;
  left: 0;
  color: var(--muted);
}

.legal__clauses > li::before {
  counter-increment: legal-item;
  content: counter(legal-item) ".";
}

.legal__points > li::before {
  counter-increment: legal-item;
  content: counter(legal-item, lower-alpha) ")";
}

.legal__bullets > li::before {
  content: "•";
}

/* Lista zagnieżdżona zaczyna WŁASNĄ numerację i własny odstęp od treści
   punktu, w którym siedzi — inaczej litery ustawiałyby się pod cyframi. */
.legal li > .legal__points,
.legal li > .legal__bullets {
  margin-top: var(--space-3);
}

/* Ramka nad dokumentem, który NIE JEST dokumentem Operatora — dziś wyłącznie
   wzór klauzuli, którą wypełnia Organizator. Bez niej strona wygląda jak
   zobowiązanie Platformy, a jest formularzem do przepisania. */
/* --------------------------------------------------------------------------
   Gotowa klauzula do skopiowania (#166)

   Blok stoi na górze strony wzoru i ma wyglądać na COŚ DO ZABRANIA, a nie na
   kolejny akapit dokumentu — stąd tło, ramka i monospace na samej treści.
   -------------------------------------------------------------------------- */

/* FLEX Z `gap`, NIE `.stack`. Rząd akcji bywa PUSTY — przycisk kopiowania stoi
   w HTML-u jako `hidden` i odsłania go dopiero skrypt. Pusty blok o zerowej
   wysokości zwija swój margines górny z dolnym i z marginesem sąsiada, więc
   odstęp nad tekstem klauzuli nie rósł, a odsłonięty przycisk lądował
   przyklejony do ramki poniżej. `gap` nie zwija się nigdy. */
.clause-block {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  margin-bottom: var(--space-7);
  padding: var(--space-5);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-alt);
}

.clause-block__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

/* `pre` z ZAWIJANIEM: to proza, a nie kod, więc poziomy pasek przewijania
   byłby tu wadą. Zachowujemy natomiast podziały akapitów — klauzula wklejona
   w jednym bloku jest nieczytelna, a Organizator wkleja ją tak, jak stoi. */
.clause-block__text {
  margin: 0;
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-card);
  font-family: inherit;
  font-size: var(--text-caption);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Łącze do dokumentu pod polem zgody (#86). Wcięcie równa je z etykietą stojącą
   obok kwadracika, żeby czytało się jako jej ciąg dalszy, a nie jako osobny
   akapit formularza. */
.field__consent {
  margin-top: var(--space-2);
  padding-left: var(--space-6);
  font-size: var(--text-caption);
}

.field__consent .text-muted {
  display: block;
}

/* Rząd akcji pod kontrolką — przycisk „Wstaw wzór" i łącze do pełnego wzoru. */
.field__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-4);
  margin-top: var(--space-3);
  font-size: var(--text-caption);
}

.legal__notice {
  /* Odstęp PIONOWY jest częścią tej ramki, nie dodatkiem: wstawka wchodzi
     w środek toku dokumentu, więc bez marginesu przykleja się do akapitu nad
     nią i pod nią, a wtedy przestaje się czytać jak wtrącenie. */
  margin: var(--space-6) 0;
  padding: var(--space-4);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--surface-alt);
}

/* Dane strony umowy — etykieta nad wartością na wąskim ekranie, para obok
   siebie od 40rem. `<dl>`, a nie tabela: to nie są wiersze zestawienia, tylko
   pary nazwa–wartość, i czytnik ekranu ma je tak zapowiedzieć. */
.legal__details {
  margin: var(--space-4) 0;
}

.legal__details dt {
  color: var(--muted);
  font-size: var(--text-caption);
}

.legal__details dd {
  margin: 0 0 var(--space-3);
}

/* OD 40rem PARA STOI OBOK SIEBIE, nie jedna pod drugą. Etykieta nad wartością
   to układ MOBILNY — na szerokim ekranie rozciąga siedem pól kontaktowych na
   czternaście wierszy i każe wodzić okiem w pionie po samych nagłówkach.
   Siatka dwukolumnowa układa `dt` i `dd` naprzemiennie bez ani jednej klasy
   więcej, bo oba są bezpośrednimi dziećmi listy. */
@media (min-width: 40rem) {
  .legal__details {
    display: grid;
    grid-template-columns: minmax(7rem, 11rem) 1fr;
    column-gap: var(--space-5);
    row-gap: var(--space-3);
    align-items: baseline;
  }

  .legal__details dd {
    margin: 0;
  }
}

.legal__annex {
  margin-top: var(--space-7);
  padding-top: var(--space-5);
  border-top: 1px solid var(--border);
}

/* TABELA PRZEWIJA SIĘ SAMA, a nie rozpycha stronę. Przy 390 px czterokolumnowy
   rejestr cookies nie ma jak się zmieścić, a strona szersza od okna każe
   przeglądarce oddalić CAŁĄ treść (uzasadnienie przy `overflow-wrap` wyżej). */
.legal__table-wrap {
  overflow-x: auto;
}

/* `min-width` JEST WARUNKIEM przewijania, nie ozdobą. Sama `width: 100%`
   ściska kolumny do szerokości rodzica — wtedy nie ma czego przewijać, a przy
   390 px `sessionid` łamie się na „sessi/onid" i tabela wygląda na zepsutą.
   Dopiero szerokość większa niż kadr daje wrapperowi zawartość do przesunięcia. */
.legal__table {
  width: 100%;
  min-width: 34rem;
  border-collapse: collapse;
  font-size: var(--text-caption);
}

/* Nazwa ciasteczka i atrybut to IDENTYFIKATORY — łamane w środku przestają
   być tym, co użytkownik zobaczy w ustawieniach przeglądarki. */
.legal__table code {
  white-space: nowrap;
}

.legal__table th,
.legal__table td {
  padding: var(--space-3);
  border-bottom: 1px solid var(--border);
  text-align: left;
  vertical-align: top;
}

.legal__table th {
  color: var(--muted);
}

/* --------------------------------------------------------------------------
   Stopka z dokumentami — ta sama w trzech warstwach, drobnym drukiem
   -------------------------------------------------------------------------- */

.legal-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-2) var(--space-4);
  font-size: var(--text-caption);
}

.panel__footer {
  margin-top: auto;
  padding: var(--space-5) var(--space-4);
  border-top: 1px solid var(--border);
  color: var(--muted);
  font-size: var(--text-caption);
  text-align: center;
}

.panel__footer .legal-links {
  margin-top: var(--space-2);
}

/* --------------------------------------------------------------------------
   Okno panelu: zakładki, ramka okna oraz treść pomocy i ustawień (#93, #233)

   Treść jest tu ZAWSZE ta sama — różni się wyłącznie oprawa. Okno tworzy
   `modal.js`; bez skryptu te same poradniki i te same ustawienia są zwykłymi
   stronami panelu, więc `.guide` i `.settings` muszą wyglądać poprawnie w obu
   miejscach i nie mogą zakładać, że stoją w `<dialog>`.
   -------------------------------------------------------------------------- */

/* Zakładki w nagłówku panelu. Ikona i napis w jednym wierszu, bo sam znak
   zapytania nie mówi, co się otworzy, a sam napis gubi się między „Wydarzenia"
   a przyciskiem wylogowania. */
.panel__tab {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

/* WYŚWIETLANIE WIĄŻE SIĘ ZE STANEM `[open]`, NIGDY z samą klasą — dokładnie ta
   sama pułapka co przy przeglądarce zdjęć i to samo uzasadnienie: reguła
   przeglądarki `dialog { display: none }` ma swoistość (0,0,1), więc
   `.panel-modal { display: flex }` wygrywałaby z nią i okno nie chowałoby się
   nigdy — wisiałoby na końcu dokumentu jako biały prostokąt jeszcze przed
   pierwszym kliknięciem. */
.panel-modal[open] {
  display: flex;
}

.panel-modal {
  flex-direction: column;

  /* Okno rośnie do treści, ale nie dalej niż na tyle, ile czyta się wygodnie:
     szerokość jest miarą wiersza prozy z zapasem na marginesy, a nie ułamkiem
     ekranu. Na telefonie zostaje margines, żeby było widać, że pod spodem coś
     jest. */
  width: min(48rem, 100% - 2 * var(--space-4));
  max-height: min(85vh, 56rem);
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-card);
  color: var(--ink);
}

.panel-modal::backdrop {
  background: rgb(0 0 0 / 55%);
}

/* Pasek z wyjściem STOI NAD TREŚCIĄ, nie pod nią: pierwszą rzeczą, której
   szuka się w oknie otwartym przez pomyłkę, jest sposób jego zamknięcia —
   a poradnik ma dwieście wierszy, więc przycisk na dole byłby po drugiej
   stronie przewijania. */
.panel-modal__bar {
  display: flex;
  justify-content: flex-end;
  flex: none;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
}

/* PRZEWIJA SIĘ TREŚĆ, NIE OKNO. `overscroll-behavior` zatrzymuje łańcuch
   przewijania na tym elemencie — bez tego dojechanie do końca poradnika
   przewija stronę pod spodem, czyli ekran, na który Organizator zaraz wróci. */
.panel-modal__body {
  flex: 1 1 auto;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-5) var(--space-6) var(--space-6);
}

/* --------------------------------------------------------------------------
   Spis poradników
   -------------------------------------------------------------------------- */

.help-index {
  margin: 0;
  padding: 0;
  list-style: none;
}

.help-index li + li {
  margin-top: var(--space-3);
}

/* CAŁA POZYCJA JEST CELEM KLIKNIĘCIA, nie sam tytuł: zdanie pod tytułem jest
   tym, po czym wybiera się poradnik, więc kliknięcie w nie musi otwierać to
   samo co kliknięcie w nazwę. */
.help-index__link {
  display: block;
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: inherit;
  text-decoration: none;
}

.help-index__link:hover {
  border-color: var(--border-control-hover);
  background: var(--accent-soft);
}

.help-index__title {
  display: block;
  font-family: var(--font-display);
  font-weight: 700;
}

.help-index__summary {
  display: block;
  margin-top: var(--space-1);
  color: var(--muted);
  font-size: var(--text-caption);
}

/* --------------------------------------------------------------------------
   Treść poradnika

   Miara wiersza JEST TU REGUŁĄ, nie ozdobą: poradnik to kilkaset słów prozy
   czytanej linearnie, a nie dokument skanowany po numerach (stąd inaczej niż
   przy `.legal`, gdzie wiersz jest celowo dłuższy).
   -------------------------------------------------------------------------- */

.guide {
  max-width: var(--measure);
}

/* KARTA PORADNIKA NIE ROZCIĄGA SIĘ NA CAŁY PANEL. Miarę wiersza niesie `.guide`
   wyżej, więc bez tego ograniczenia karta na ekranie 1440 px jest dwa razy
   szersza od tekstu w środku — proza siedzi przy lewej krawędzi, a prawa połowa
   karty świeci pustką i wygląda na urwany układ. Zmierzone na zrzucie, nie
   przewidziane.

   Szerokość WYPROWADZONA z miary wiersza, nie dobrana osobno: karta ma być
   dokładnie tekstem plus jej własne marginesy, więc obie liczby nie mają jak
   się rozjechać. Okno pomocy tej klasy nie potrzebuje — ma własną szerokość. */
.help-page {
  max-width: calc(var(--measure) + 2 * var(--space-6));
  margin-inline: auto;
}

/* Sekcja poradnika zaczyna się ODDECHEM, nie kreską: poziomych linii jest na
   tym ekranie już dość (karta, pasek okna), a nagłówek w innym kroju i tak
   odcina się od akapitu nad nim. */
/* WYRÓŻNIONE OSTRZEŻENIE W PROZIE MA NORMALNĄ GRUBOŚĆ PISMA — i to jest
   naprawa ze zrzutu, nie przeczucie. `.message--danger` pogrubia CAŁĄ treść
   i na ekranie panelu jest to poprawne: stoi tam jedno zdanie nad przyciskiem,
   który zaraz coś zrobi. W poradniku ten sam komponent niesie czterdziestowyrazowy
   akapit — a akapit pogrubiony w całości nie jest wyróżniony, tylko trudniejszy
   do przeczytania, bo wyróżnia się w nim wszystko.

   Barwa, ikona i gruba krawędź zostają: to one niosą znaczenie (WCAG 1.4.1),
   a `<strong>` w środku dalej pogrubia to jedno zdanie, które ma się wybić. */
.guide .message {
  font-weight: 400;
}

.guide h2 {
  margin-top: var(--space-7);
}

.guide h3 {
  margin-top: var(--space-6);
}

.guide ul,
.guide ol {
  margin: 0 0 var(--space-4);
  padding-left: var(--space-5);
  max-width: var(--measure);
}

.guide li + li {
  margin-top: var(--space-2);
}

/* TABELA PRZEWIJA SIĘ SAMA, a nie rozpycha okna — ta sama reguła i to samo
   uzasadnienie co przy rejestrze cookies w dokumentach prawnych. */
.guide__table-wrap {
  margin-bottom: var(--space-4);
  overflow-x: auto;
}

.guide__table {
  width: 100%;
  min-width: 28rem;
  border-collapse: collapse;
  font-size: var(--text-caption);
}

.guide__table th,
.guide__table td {
  padding: var(--space-3);
  border-bottom: 1px solid var(--border);
  text-align: left;
  vertical-align: top;
}

.guide__table th {
  color: var(--muted);
}

/* --------------------------------------------------------------------------
   Ustawienia konta (#233)

   Trzy formularze jeden pod drugim — w oknie panelu i na pełnej stronie tak
   samo. Karta dostaje tę samą miarę co poradnik: to są pola tekstowe czytane
   z etykietami, a nie tabela, więc rozciągnięcie ich na 1440 px oddala etykietę
   od kontrolki na całą szerokość ekranu.
   -------------------------------------------------------------------------- */

.settings {
  max-width: calc(var(--measure) + 2 * var(--space-6));
  margin-inline: auto;
}

/* KRESKA MIĘDZY SEKCJAMI, nie sam odstęp. Trzy formularze z własnymi
   przyciskami „Zapisz" muszą być rozpoznawalne jako trzy — inaczej przycisk
   pod ostatnim polem wygląda na przycisk całego ekranu, a to jest dokładnie ta
   pomyłka, przez którą ktoś zmieni hasło, chcąc poprawić telefon. */
.settings__section + .settings__section {
  margin-top: var(--space-7);
  padding-top: var(--space-6);
  border-top: 1px solid var(--border);
}

.settings__heading {
  margin-bottom: var(--space-2);
}

/* --------------------------------------------------------------------------
   Pakiety (#291)

   Trzy karty cennika obok siebie i podsumowanie zamówienia pod nimi. Siatka
   `auto-fit` schodzi pod siebie sama, bez progu w media query — dokładnie tak
   jak `.detail-grid`, i z tego samego powodu: kart są trzy, a nie „trzy albo
   cztery zależnie od ekranu".

   MIARA KARTY (18rem) jest szersza niż w `.detail-grid` (14rem), bo w karcie
   stoi lista wyprowadzonych limitów, a nie jedna wartość. Węższa łamałaby
   „300 zdjęć od Gości" w połowie na telefonie.
   -------------------------------------------------------------------------- */

.packages {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  gap: var(--space-5);
}

/* Karta pakietu jest KOLUMNĄ, żeby przycisk stał równo w każdej z trzech —
   `margin-top: auto` na akcji dociska go do dołu niezależnie od tego, ile
   wierszy ma lista limitów nad nim. Bez tego przyciski układają się schodkami
   i wyglądają jak trzy różne kontrolki. */
.package {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* CENA JEST NAJWIĘKSZĄ LICZBĄ NA KARCIE i to jest treść, nie ozdoba: człowiek
   porównuje tu trzy oferty, więc kwota musi dać się przeczytać jednym rzutem
   oka. Liczba Gości stoi w nagłówku karty, czyli o jeden poziom wyżej. */
.package__price {
  font-size: var(--text-title);
  font-weight: 700;
}

/* Lista tego, co wchodzi w cenę — WSPÓLNA dla cennika i dla podsumowania
   zamówienia. Znak ✓ rysuje `::before`, więc nie ma go w treści: czytnik ekranu
   dostaje zwykłą listę, a nie serię „ptaszek". Odstęp idzie MIĘDZY pozycjami,
   bo wiersze bywają dwuwierszowe i `line-height` sklejałby wtedy sąsiadów. */
.package__limits {
  margin: 0;
  padding: 0;
  list-style: none;
}

.package__limits li {
  position: relative;
  margin-bottom: var(--space-3);
  padding-left: var(--space-5);
}

.package__limits li::before {
  content: "✓";
  position: absolute;
  left: 0;
  color: var(--accent-strong);
  font-weight: 700;
}

/* Akcja karty dociśnięta do dołu (`margin-top: auto` w kolumnie), a przycisk na
   CAŁĄ jej szerokość: trzy karty cennika stoją obok siebie, więc przycisk
   przyklejony do lewej krawędzi każdej z nich układał się w schodki i czytał
   jak trzy różne kontrolki. */
.package__actions {
  margin-top: auto;
}

.package__actions .btn {
  width: 100%;
}

/* Ile miejsc w tym pakiecie już masz — wiersz nad przyciskiem, nie akapit obok:
   to jest liczba, którą porównuje się z ceną, a nie zdanie do przeczytania. */
.package__slots {
  margin: 0 0 var(--space-3);
  color: var(--muted);
  font-size: var(--text-caption);
}

/* EKRAN ZAMÓWIENIA MA WŁASNĄ, WĘŻSZĄ MIARĘ (#307). Panel ma 68 rem, a na tym
   ekranie stoją dwie karty z kilkoma wierszami każda — rozciągnięte na pełną
   szerokość zostawiały pół okna puste i to była pierwsza rzecz, którą widać
   po wejściu. 54 rem trzyma kolumnę treści przy komfortowej długości wiersza,
   a kolumnę rachunku przy szerokości, w której kwota i przycisk stoją obok
   siebie bez łamania. */
.order-page {
  max-width: 54rem;
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}

/* Dwie kolumny: po lewej to, za co się płaci, po prawej rachunek i akcja.
   Kolumna rachunku jest WĄSKA I STAŁA, bo w niej stoi pole na kod rabatowy
   (#292): rabat zmienia kwotę, więc stoi przy niej, a nie pod listą tego, co
   wchodzi w pakiet. Na telefonie obie schodzą pod siebie i kolejność zostaje
   ta sama co w kodzie. */
.order {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-5);
  align-items: start;
}

@media (min-width: 48rem) {
  .order {
    grid-template-columns: 1fr minmax(17rem, 20rem);
  }
}

/* Warunek zakupu ODDZIELONY od listy pełnym odstępem — sklejony z nią czytał
   się jak jej piąta pozycja. */
.order__terms {
  margin: var(--space-5) 0 0;
  max-width: none;
}

/* RACHUNEK, nie sama cena: plama tła oddziela go od kolumny treści, a wnętrze
   ma własny rytm — pozycje, kreska, kwota, akcja. */
.order__summary {
  background: var(--surface-alt);
}

.order__summary-title {
  margin: 0 0 var(--space-4);
  font-size: var(--text-lead);
}

.order__lines {
  margin: 0;
}

.order__line {
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
  color: var(--muted);
  font-size: var(--text-caption);
}

.order__line dd {
  margin: 0;
  color: var(--ink);
  font-weight: 600;
}

/* KRESKA NAD KWOTĄ, nie sam odstęp: kwota jest tu wynikiem tego, co stoi wyżej,
   a nie kolejnym wierszem listy. `flex-wrap` zrzuca ją do własnego wiersza
   w całości, zamiast łamać napis „Do zapłaty" na dwie linie na telefonie. */
.order__total {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-2) var(--space-3);
  margin: var(--space-4) 0 0;
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}

.order__total-label {
  font-weight: 600;
}

/* `margin-left: auto` trzyma kwotę przy PRAWEJ krawędzi także wtedy, gdy
   z przekreśloną ceną obok zawija się pod etykietę — bez tego przeskakiwała po
   „Zastosuj kod" na lewą stronę kolumny. */
.order__total-amount {
  margin-left: auto;
  font-size: var(--text-title);
  font-weight: 700;
}

/* Cena z cennika PRZEKREŚLONA obok kwoty po rabacie — bez niej klient widzi
   inną liczbę niż na cenniku i nie wie, czy kod zadziałał. */
.order__total-was {
  margin-right: var(--space-2);
  color: var(--muted);
  font-size: var(--text-body);
  font-weight: 400;
}

/* „Zamawiam" na CAŁĄ szerokość kolumny — jest główną akcją tego ekranu, a
   `inline-flex` z `.btn` zostawiał go przyklejonego do lewej krawędzi rachunku,
   jakby był jednym z kilku. „Zastosuj kod" zostaje w naturalnej szerokości:
   obsługuje pole nad sobą, nie zamówienie. */
.order__form {
  margin-top: var(--space-5);
}

.order__apply {
  margin-top: var(--space-3);
}

.order__submit {
  width: 100%;
  margin-top: var(--space-5);
}

.order__note {
  margin: var(--space-4) 0 0;
  color: var(--muted);
  font-size: var(--text-caption);
}

/* --------------------------------------------------------------------------
   Ograniczenie ruchu — to nie kosmetyka. Animacja potrafi wywołać zawroty
   głowy i mdłości u osób z wrażliwością przedsionkową, dlatego deklaracja
   systemowa użytkownika wygrywa ze wszystkim (`!important`).
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}
