/* ===== CZCIONKA (Roboto, self-hosted) =====
   Dwa pliki (nie osiem) dla czterech wag (300/400/500/700) używanych w
   aplikacji (patrz --font-sans niżej) — Roboto v51 to font zmiennoprzecinkowy
   (variable font): JEDEN plik .woff2 obejmuje cały zakres wag na osi "wght",
   więc zamiast czterech pinowanych deklaracji wystarczy jedna z zakresem
   `font-weight: 300 700`, a przeglądarka sama dobiera interpolowaną wagę.
   Dwa pliki to podział na podzbiory Unicode (nie na wagę): "latin" (podstawowe
   znaki łacińskie) i "latin-ext" (polskie znaki diakrytyczne — ą ć ę ł ń ó ś
   ź ż leżą w zakresie U+0100-02BA). Pobrane lokalnie z Google Fonts (ten sam
   plik, ta sama licencja Apache 2.0) i wyłącznie stąd serwowane — jedyny
   dotąd kontakt aplikacji z serwerem trzeciej strony (fonts.googleapis.com/
   fonts.gstatic.com) ujawniał adres IP użytkownika Google przy każdym
   otwarciu; teraz font-src w CSP wskazuje wyłącznie 'self'. Zgłoszone wprost
   przez użytkownika po analizie bezpieczeństwa aplikacji. */
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('../fonts/roboto-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('../fonts/roboto-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ===== ZMIENNE ===== */
:root {
  --color-bg: #f8f7f4;
  --color-bg-elevated: #ffffff;
  --color-bg-muted: #f0eeea;
  --color-border: #e2dfd8;
  --color-border-light: #eceae4;
  --color-text: #2c2a26;
  --color-text-secondary: #6b6560;
  --color-text-muted: #9a948c;
  --color-primary: #3d6b4f;
  --color-primary-hover: #2f5540;
  --color-primary-light: #e8f0eb;
  --color-accent: #8b6914;
  --color-danger: #c0392b;
  --color-alive: #3d6b4f;
  --color-deceased: #8b7355;
  --color-male: #4a7c9b;
  --color-female: #9b5a7c;
  --color-union: #c4a035;
  --color-parent-child: #5a8f6e;
  /* Wyraźnie inny odcień niebieskiego niż --color-male niżej (ten jest
     bardziej stonowany/szaroniebieski, przez co przy tym samym kolorze
     linia adopcji myliłaby się wzrokowo z odznaką płci na karcie) — linia
     relacji rodzic-dziecko przez adopcję (patrz Connections.drawParentChild,
     Wizard.FLOWS.adoption). Zgłoszone wprost przez użytkownika. */
  --color-adoption: #3b6ea5;
  --color-highlight: #3d6b4f;
  --color-dim: rgba(248, 247, 244, 0.75);
  --shadow-sm: 0 1px 3px rgba(44, 42, 38, 0.06);
  --shadow-md: 0 4px 16px rgba(44, 42, 38, 0.08);
  --shadow-lg: 0 8px 32px rgba(44, 42, 38, 0.12);
  /* "Szkło" — półprzezroczyste tło + rozmycie tego, co jest pod spodem
     (backdrop-filter), użyte na przyciskach/panelach/modalach/kartach.
     Dwa poziomy krycia: -bg (przyciski, karty — ma być widać, że coś pod
     spodem faktycznie prześwituje) i -bg-strong (modale/tooltipy z dłuższym
     tekstem — potrzebują większego kontrastu, żeby tekst został czytelny).
     -hover to lekko bardziej kryjąca wersja -bg, na potrzeby stanu :hover
     (samo osłabienie/wzmocnienie blur na hover wygląda migotliwie). */
  --glass-bg: rgba(255, 255, 255, 0.28);
  --glass-bg-hover: rgba(255, 255, 255, 0.45);
  --glass-bg-strong: rgba(255, 255, 255, 0.5);
  /* Obwódki CELOWO ciemne (nie białe) w jasnym motywie — biała, przezroczysta
     obwódka (tak jak w :root[data-theme="dark"] niżej, gdzie dobrze się
     odcina od ciemnego tła) na jasnym tle strony (--color-bg) praktycznie
     znika, bo biały-na-prawie-białym nie daje kontrastu niezależnie od
     krycia. rgba na bazie --color-text (2c,2a,26) daje widoczną, stonowaną
     szarą obwódkę. Zgłoszone wprost przez użytkownika. */
  --glass-border: rgba(44, 42, 38, 0.18);
  --glass-highlight: rgba(44, 42, 38, 0.14);
  --glass-blur: 18px;
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 16px;
  --font-sans: 'Roboto', 'Segoe UI', system-ui, -apple-system, sans-serif;
  --header-btn-height: 40px;
  --transition: 0.25s cubic-bezier(0.4, 0, 0.2, 1);
  --header-height: 64px;
  /* 3:4 dokładnie (210/280 = 0.75) — karta sama w sobie ma teraz kształt
     zdjęcia, żeby to ono mogło wypełnić cały kafelek (patrz .person-card
     niżej), zamiast być małą miniaturką obok tekstu. */
  --card-width: 210px;
  --card-height: 280px;
  --row-empty-height: 28px;
  --row-padding: 16px;
}

:root[data-theme="dark"] {
  --color-bg: #1c1b18;
  --color-bg-elevated: #252420;
  --color-bg-muted: #2b2a25;
  --color-border: #3d3b34;
  --color-border-light: #343228;
  --color-text: #eae7e0;
  --color-text-secondary: #b0aa9f;
  --color-text-muted: #7d786f;
  --color-primary: #5a9873;
  --color-primary-hover: #6ba884;
  --color-primary-light: #24352b;
  --color-accent: #c9a13f;
  --color-danger: #e0685a;
  --color-alive: #5a9873;
  --color-deceased: #b09a7c;
  --color-male: #6fa3c4;
  --color-female: #c084a8;
  --color-union: #d4b24a;
  --color-parent-child: #7bb392;
  --color-adoption: #6fa8d8;
  --color-highlight: #5a9873;
  --color-dim: rgba(28, 27, 24, 0.75);
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.2);
  --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.3);
  --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.4);
  --glass-bg: rgba(37, 36, 32, 0.22);
  --glass-bg-hover: rgba(58, 56, 50, 0.4);
  --glass-bg-strong: rgba(30, 29, 26, 0.42);
  --glass-border: rgba(255, 255, 255, 0.1);
  --glass-highlight: rgba(255, 255, 255, 0.08);
}

/* ===== RESET ===== */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  font-size: 16px;
  scroll-behavior: smooth;
}

body {
  font-family: var(--font-sans);
  background: var(--color-bg);
  color: var(--color-text);
  line-height: 1.5;
  overflow: hidden;
  height: 100vh;
}

.hidden { display: none !important; }

/* Ekran ładowania — patrz komentarz przy #app-loading-screen w index.html.
   z-index 1000: ponad WSZYSTKIM innym w aplikacji (nawet ponad .pin-lock-
   overlay, 150) — to pierwsza rzecz, jaką strona pokazuje, więc musi
   wygrać ze wszystkiego, co JS mógłby zdążyć wyrenderować pod spodem, zanim
   App.init() zdąży ją schować. */
.app-loading-screen {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 28px;
  background: var(--color-bg);
}

/* .connect-folder-logo ma domyślnie margin-bottom:20px (myślany pod zwykły,
   blokowy kontekst z tekstem POD logo) — tu jedyny "tekst" pod spodem to
   spinner w osobnym flex-gapie kontenera, więc bez wyzerowania odstęp
   podwoiłby się (20px marginesu + 28px gapu). */
.app-loading-screen .connect-folder-logo {
  margin-bottom: 0;
}

.app-loading-spinner {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 3px solid var(--color-border);
  border-top-color: var(--color-primary);
  animation: loading-spin 0.8s linear infinite;
}

/* ===== PASKI PRZEWIJANIA (dopasowane do motywu) =====
   Kolory przez zmienne (--color-border/--color-text-muted), które już same
   przełączają się jasny/ciemny w :root[data-theme="dark"] — więc nie trzeba
   osobnego zestawu reguł dla trybu ciemnego, ten sam kod działa w obu. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--color-border) transparent;
}

*::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

*::-webkit-scrollbar-track {
  background: transparent;
}

*::-webkit-scrollbar-thumb {
  background-color: var(--color-border);
  border-radius: 10px;
  border: 2px solid transparent;
  background-clip: padding-box;
}

*::-webkit-scrollbar-thumb:hover {
  background-color: var(--color-text-muted);
}

/* ===== NAGŁÓWEK ===== */
.app-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: var(--header-height);
  background: transparent;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 24px;
  z-index: 100;
  /* Sam pasek jest transparentny i w większości pusty — mimo to, bez tego,
     jego niewidoczny box (position:fixed na całą szerokość ekranu) i tak
     przechwytywałby najechanie/kliknięcie nad kartami/liniami/etykietami
     przewijanymi pod nim, nawet tam gdzie nie ma żadnego przycisku.
     pointer-events:none na kontenerze + auto z powrotem na faktycznych
     dzieciach-przyciskach (patrz niżej) rozwiązuje to tym samym trikiem co
     .connections-layer nad kartami. */
  pointer-events: none;
}

.app-header > * {
  pointer-events: auto;
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  /* "Dane" i żarówka opuściły ten kontener na rzecz rogów ekranu
     (.corner-btn), a ołówek/przełącznik grupowy/cofnij/reset na rzecz
     .side-toolbar — .header-actions to teraz JEDYNE nieprzypięte dziecko
     .app-header (flex), więc bez tego wylądowałoby po lewej (space-between
     z jednym elementem = flex-start), zamiast po prawej jak dotąd. */
  margin-left: auto;
  /* align-self:flex-start + margin-top:20px (nie domyślne wyśrodkowanie z
     .app-header { align-items:center }) — inaczej te przyciski (wysokość
     40px) lądowały wyśrodkowane w 64px nagłówku, czyli 12px od góry, a
     "Dane" (.corner-top-left) ma na sztywno top:20px. 20px tutaj to ta sama
     wartość, żeby obie strony nagłówka faktycznie siedziały w jednej linii. */
  align-self: flex-start;
  margin-top: 20px;
}

.header-actions .btn {
  height: var(--header-btn-height);
}

/* ===== ROZWIJANA LISTA (DANE) ===== */
/* Grupa "Dane" + skrót zapisu (patrz #btn-quick-save niżej) — ten sam wzorzec
   co .corner-bottom-left-group (przełącznik motywu + prywatność + kłódka
   PIN-u). Pozycjonowanie w rogu (fixed/top/left) dziedziczy z .corner-btn/
   .corner-top-left na tym samym elemencie, tu tylko układ w poziomie. */
.corner-top-left-group {
  display: flex;
  align-items: center;
  gap: 10px;
}

.dropdown {
  position: relative;
}

.dropdown-menu {
  position: absolute;
  /* Zero odstępu od przycisku (bez margin-top) — jakikolwiek odstęp tworzy
     "martwą strefę" pod przyciskiem: kursor zjeżdżający na skos w dół, w
     stronę menu, na chwilę opuszcza obszar :hover (przycisk skończył się,
     menu jeszcze się nie zaczęło), więc menu znika, zanim kursor do niego
     dotrze. Przy zerowym odstępie obszary stykają się, więc :hover nigdy
     się nie przerywa. */
  top: 100%;
  left: 0;
  min-width: 200px;
  background: var(--glass-bg-strong);
  backdrop-filter: blur(24px) saturate(180%);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  padding: 6px;
  display: none;
  flex-direction: column;
  gap: 2px;
  z-index: 110;
}

.dropdown:hover .dropdown-menu,
.dropdown:focus-within .dropdown-menu {
  display: flex;
}

.dropdown-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 8px 10px;
  border: none;
  background: none;
  border-radius: var(--radius-sm);
  font-family: inherit;
  font-size: 0.875rem;
  color: var(--color-text);
  cursor: pointer;
  /* Patrz komentarz przy .btn w tym pliku (obejście podwójnego stuknięcia na
     iOS) — ten przycisk też nie ma tamtej klasy. */
  touch-action: manipulation;
  white-space: nowrap;
}

.dropdown-item:hover {
  background: var(--color-bg-muted);
}

/* "Zapisz"/"Zapisz z nowym szyfrowaniem" — wyszarzone, gdy nie ma czego
   zapisać (puste drzewo) albo (samo "Zapisz") gdy nie ma jeszcze ustalonego
   hasła z poprzedniego zapisu/importu (patrz App.updateSaveButtonsState w
   app.js). Zgłoszone wprost przez użytkownika. */
.dropdown-item:disabled {
  opacity: 0.4;
  pointer-events: none;
  cursor: not-allowed;
}

.dropdown-item:disabled:hover {
  background: none;
}

.dropdown-divider {
  margin: 6px 4px;
  border-top: 1px solid var(--color-border-light);
}

/* Wyszarzona informacja o ostatnio zapisanym/zaimportowanym pliku (patrz
   #dropdown-file-info w index.html i App.updateDataDependentUI) — sama
   nazwa pliku na osobnej linii pod etykietą, żeby długie nazwy (z doklejoną
   datą, patrz Backup.formatDateSuffix) nie musiały się mieścić w jednej
   linii z resztą tekstu. */
.dropdown-file-info {
  padding: 6px 10px 8px;
  font-size: 0.8125rem;
  color: var(--color-text-muted);
  line-height: 1.4;
}

.dropdown-file-info-label,
.dropdown-file-info-name {
  display: block;
}

.dropdown-file-info-name {
  font-weight: 500;
  word-break: break-all;
}

/* Nieodwracalna operacja (patrz Storage.clearAll) — czerwony, żeby wyróżnić
   się od reszty pozycji menu, które są bezpieczne do przypadkowego kliknięcia. */
.dropdown-item-danger {
  color: var(--color-danger);
}

.dropdown-item-danger:hover {
  background: #fdecea;
}

:root[data-theme="dark"] .dropdown-item-danger:hover {
  background: rgba(74, 43, 40, 0.35);
}

/* ===== PRZYCISKI ZAKOTWICZONE W ROGACH / KOLUMNIE (poza przepływem nagłówka) ===== */
.corner-btn {
  /* Wygrywa z `.dropdown { position: relative }` dzięki kolejności w
     kaskadzie (ta reguła jest zdefiniowana niżej) — element "Dane" łączy obie
     klasy: potrzebuje position:fixed względem viewportu (róg ekranu), a
     jednocześnie musi zostać punktem odniesienia dla `.dropdown-menu`
     (position:absolute), co fixed równie dobrze zapewnia co relative. */
  position: fixed;
  z-index: 100;
}

.corner-top-left {
  top: 20px;
  left: 24px;
}

.corner-top-left .btn {
  height: var(--header-btn-height);
}

.corner-bottom-left {
  bottom: 20px;
  left: 24px;
}

/* Ta sama wysokość co .header-actions/.corner-top-left/.side-toolbar .btn
   wyżej — bez tego przycisk prywatności (SVG oka, 18px) wyliczał wysokość
   tylko z zawartości i wychodził niższy (36px) niż reszta przycisków
   nagłówka (40px), więc wizualnie wyglądał na "mniejszy". Zgłoszone wprost
   przez użytkownika. */
.corner-bottom-left-group .btn {
  height: var(--header-btn-height);
}

.corner-bottom-left-group {
  display: flex;
  align-items: center;
  gap: 10px;
}

.corner-bottom-right {
  bottom: 20px;
  right: 24px;
}

/* Grupa "Zablokuj teraz" + odznaka logo/wersji — ten sam wzorzec co
   .corner-bottom-left-group wyżej (wysokość na .btn potomnych, bo inaczej
   przycisk z samą ikoną SVG wyliczałby wysokość tylko z zawartości i
   wychodziłby niższy niż odznaka obok). */
.corner-bottom-right-group .btn {
  height: var(--header-btn-height);
}

/* align-items:flex-end (nie center) — "Zablokuj teraz" ma stać w linii z
   odznaką logo/wersji (dolny wiersz kolumny obok, patrz
   .corner-bottom-right-stack niżej), a nie wyśrodkowany względem CAŁEJ
   dwuwierszowej kolumny (co wizualnie wypychało go w górę, bliżej Ustawień
   niż odznaki). Zgłoszone wprost przez użytkownika. */
.corner-bottom-right-group {
  display: flex;
  align-items: flex-end;
  gap: 10px;
}

/* Kolumna: przycisk Ustawień NAD odznaką logo/wersji, oba dosunięte do
   prawej krawędzi (patrz index.html) — flex-end na osi poprzecznej (pozioma,
   bo kolumna) odpowiada za wyrównanie do prawej. */
.corner-bottom-right-stack {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
}

.app-brand-logo {
  height: 18px;
  width: auto;
  display: block;
  margin-right: 12px;
}

/* Domyślnie (motyw jasny) wariant z ciemnym tekstem, ukryty wariant z
   białym — :root[data-theme="dark"] (patrz App.initTheme w app.js) je
   zamienia. Ten sam mechanizm co logo w oknie powitalnym
   (.connect-folder-logo). */
.app-brand-logo-dark {
  display: none;
}

:root[data-theme="dark"] .app-brand-logo-light {
  display: none;
}

:root[data-theme="dark"] .app-brand-logo-dark {
  display: block;
}

.app-brand-version {
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--color-text-secondary);
}

.btn-icon-eye svg {
  /* Bez display:block inline <svg> zostawia mały odstęp pod sobą (jak
     zwykły znak tekstowy z zejściem poniżej linii bazowej), przez co ikona
     wyglądałaby na nierówną z sąsiednią żarówką-emoji w tym samym rzędzie. */
  display: block;
}

/* Ten sam powód co .btn-icon-eye svg wyżej. */
.btn-icon-lock svg {
  display: block;
}

/* Ten sam powód co .btn-icon-eye svg wyżej. */
.btn-icon-power svg {
  display: block;
}

/* Skrót zapisu obok przycisku "Dane" (patrz App.updateDataDependentUI w
   app.js) — domyślnie wyszarzona/przygaszona ikona (nic do zapisania),
   pełny kolor + świecąca poświata w klasie .needs-save (są niezapisane
   zmiany). transition na filter/text-shadow, żeby przejście między stanami
   było płynne, nie skokowe. Zgłoszone wprost przez użytkownika. */
#btn-quick-save .btn-icon {
  filter: grayscale(1) opacity(0.55);
  transition: filter 0.2s ease;
}

#btn-quick-save.needs-save .btn-icon {
  filter: none;
}

#btn-quick-save.needs-save {
  box-shadow: 0 0 0 1px var(--color-primary);
}

.side-toolbar {
  position: fixed;
  top: 50%;
  right: 24px;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  z-index: 100;
}

.side-toolbar .btn {
  height: var(--header-btn-height);
}

/* Odstęp mniej więcej wielkości jednego przycisku między "cofnij" a "reset" */
.side-toolbar-gap {
  height: var(--header-btn-height);
}

/* ===== PRZYCISKI ===== */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 8px 16px;
  border: none;
  border-radius: var(--radius-sm);
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  cursor: pointer;
  transition: all var(--transition);
  white-space: nowrap;
  /* Nowocześniejsze, bardziej niezawodne uzupełnienie pustego nasłuchu
     touchstart na document (patrz App.init w app.js) — ten sam cel
     (zniwelowanie starego zachowania Safari na iOS, gdzie element z regułą
     :hover wymagał dwóch stuknięć: pierwsze tylko "podświetlało", dopiero
     drugie klikało), ale ustawione wprost na elemencie. Sam touchstart na
     document okazał się niewystarczający dla części przycisków (np.
     "Edytuj" w profilu) — zgłoszone wprost przez użytkownika po teście na
     prawdziwym urządzeniu. */
  touch-action: manipulation;
}

/* Przyciski nagłówka/paska bocznego BEZ etykiety tekstowej (sama ikona —
   emoji w <span class="btn-icon"> albo inline SVG) — stała szerokość, taka
   sama jak wysokość (--header-btn-height), zamiast domyślnego auto-width z
   samego paddingu. Bez tego szerokość zależała od tego, CZYM akurat jest
   ikona: emoji renderowały się szersze (~58px) niż inline SVG o explicit
   width="16"/"18" (~50-52px) — te same z pozoru "kwadratowe" przyciski
   wychodziły różnej szerokości. padding:0, bo flex-centering z .btn
   wystarcza do wyśrodkowania ikony w stałym boksie. Zgłoszone wprost przez
   użytkownika. */
.btn-icon-only {
  width: var(--header-btn-height);
  padding: 0;
}

.btn-primary {
  /* Deklaracja-fallback PRZED color-mix() — jeśli przeglądarka nie
     rozumie color-mix (nie dotyczy Chrome/Edge, na które ta aplikacja i tak
     jest nastawiona, patrz File System Access API w storage.js/backup.js),
     cała druga deklaracja jest nieważna i zostaje ta pierwsza, więc przycisk
     nie zostaje bez tła. */
  background: var(--color-primary);
  /* 85% (nie 58%) w jasnym motywie — przy niższym kryciu zieleń wymieszana
     z jasnym tłem strony (--color-bg) robiła się wyraźnie bledsza niż sam
     --color-primary. 58% wraca w ciemnym motywie (patrz nadpisanie w sekcji
     "TRYB CIEMNY" niżej w pliku) — tam ten sam efekt szkła dobrze wygląda.
     Zgłoszone wprost przez użytkownika. */
  background: color-mix(in srgb, var(--color-primary) 85%, transparent);
  backdrop-filter: blur(var(--glass-blur)) saturate(180%);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(180%);
  color: white;
  width: 44px;
  height: 44px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--glass-highlight);
  font-size: 1.5rem;
  line-height: 1;
  box-shadow: var(--shadow-md);
}

.btn-primary:hover {
  background: var(--color-primary-hover);
  /* 90% w jasnym motywie, ten sam powód co bazowe .btn-primary wyżej —
     58%/65% z ciemnego motywu wraca przez nadpisanie w sekcji "TRYB
     CIEMNY". */
  background: color-mix(in srgb, var(--color-primary-hover) 90%, transparent);
  transform: scale(1.05);
}

.btn-primary.btn-labeled {
  width: auto;
  height: auto;
  padding: 8px 18px;
  border-radius: var(--radius-sm);
  font-size: 0.875rem;
  font-weight: 500;
  /* .btn-primary (dla okrągłej odmiany bez etykiety) ustawia line-height:1
     — bez zresetowania tu ten przycisk dziedziczyłby tę samą, dużo
     ciaśniejszą wysokość wiersza (14px zamiast 21px z body) niż KAŻDY inny
     przycisk w nagłówku, przesuwając wyśrodkowany tekst o ok. 1px względem
     reszty. Zgłoszone wprost przez użytkownika. */
  line-height: 1.5;
  gap: 6px;
}

/* .btn-large resetuje rozmiar/kształt tak samo jak .btn-labeled (patrz
   niżej) — bez powtórzenia go tu przyciski typu "Stwórz nowe drzewo"
   (.btn-primary.btn-large, bez .btn-labeled) dziedziczyłyby powiększenie na
   hover myślane dla okrągłego, pływającego przycisku-ikony bez etykiety.
   Zgłoszone wprost przez użytkownika. */
.btn-primary.btn-labeled:hover,
.btn-primary.btn-large:hover {
  transform: none;
}

.header-actions .btn-primary.btn-labeled {
  height: var(--header-btn-height);
}

.btn-secondary {
  background: var(--glass-bg);
  backdrop-filter: blur(var(--glass-blur)) saturate(160%);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
  color: var(--color-text);
  border: 1px solid var(--glass-border);
}

.btn-secondary:hover {
  background: var(--glass-bg-hover);
  border-color: var(--color-text-muted);
}

.btn-secondary.active {
  background: rgba(253, 236, 234, 0.45);
  border-color: var(--color-danger);
  color: var(--color-danger);
}

.btn-secondary.active:hover {
  background: rgba(251, 220, 217, 0.55);
}

/* Aktywny tryb przesuwania elementów — celowo NIE czerwień .active (ta jest
   zarezerwowana dla trybu usuwania), tylko ten sam bursztynowy odcień co
   .move-mode-bar/.move-mode-tint, żeby kolor przycisku od razu kojarzył się
   z tym samym trybem co żółty komunikat/poświata na stronie. */
.btn-secondary.active-move {
  background: rgba(251, 243, 222, 0.45);
  border-color: #c4a035;
  color: #8b6914;
}

.btn-secondary.active-move:hover {
  background: rgba(247, 235, 201, 0.55);
}

/* Przycisk trybu przeciągania (grupowe/pojedyncze) — celowo NIE używa
   .active/czerwieni jak inne przełączniki: ma być wyszarzony i niedostępny
   POZA trybem edycji (domyślnie), a w pełni klikalny (bez wyróżnienia
   kolorem) dopiero gdy tryb edycji jest włączony — patrz
   .app-header.editing-locked niżej. Przycisk resetu pozycji dzieli teraz
   dokładnie tę samą regułę: ma sens tylko W trakcie edycji, więc też
   domyślnie wyszarzony i odblokowywany dopiero przez editing-locked.*/
#btn-drag-group-toggle,
.side-toolbar #btn-reset-positions {
  opacity: 0.4;
  filter: grayscale(0.6);
  pointer-events: none;
  cursor: not-allowed;
}

/* Przyciski cofania/przywracania przesunięć — dostępność każdego zależy od
   DWÓCH rzeczy naraz (tryb edycji WŁĄCZONY i jego historia niepusta), więc
   sterowana jest wprost atrybutem `disabled` z JS (patrz
   Timeline.updateHistoryButtonsState), a nie samą obecnością
   .editing-locked jak przy pozostałych przyciskach. Ten ogólny styl
   `:disabled` obowiązuje niezależnie od TEGO, KTÓRY z dwóch warunków akurat
   go wyłącza. */
.btn:disabled {
  opacity: 0.4;
  filter: grayscale(0.6);
  pointer-events: none;
  cursor: not-allowed;
}

/* Aktywny tryb przesuwania elementów: reszta przycisków (poza samym
   przełącznikiem trybu, przełącznikiem grupowe/pojedyncze i cofnij — te
   trzy teraz mieszkają razem w `.side-toolbar`) staje się wyszarzona i
   niedostępna — sens kliknięcia w cokolwiek innego w tym momencie byłby
   mylący (np. otwarcie kreatora połączeń w trakcie przeciągania karty).
   Wymienione wprost, bo po rozbiciu dawnego wspólnego `.header-actions` na
   `.header-actions` / `.side-toolbar` / `.corner-btn` nie ma już jednego
   wspólnego kontenera, po którym dałoby się to wykluczyć negacją. Rozwijane
   menu "Dane" jest wymuszenie ukrywane, bo samo pojawia się na CSS :hover,
   niezależnie od tego, czy przycisk je otwierający jest "klikalny". */
.app-header.editing-locked .header-actions .btn,
.app-header.editing-locked #btn-theme-toggle,
.app-header.editing-locked #btn-data {
  opacity: 0.4;
  filter: grayscale(0.6);
  pointer-events: none;
  cursor: not-allowed;
}

.app-header.editing-locked #btn-drag-group-toggle,
.app-header.editing-locked .side-toolbar #btn-reset-positions {
  opacity: 1;
  filter: none;
  pointer-events: auto;
  cursor: pointer;
}

.app-header.editing-locked .dropdown-menu {
  display: none !important;
}

/* Aktywny tryb usuwania elementów: analogicznie do .editing-locked wyżej —
   reszta przycisków (poza samym przełącznikiem trybu usuwania) wyszarza się
   i przestaje reagować, żeby nie dało się w trakcie usuwania np. otworzyć
   kreatora połączeń albo wejść w tryb przesuwania. Cofnij/przywróć są tu
   wyjątkiem (patrz .app-header.delete-locked .side-toolbar niżej) — usuwanie
   osób i połączeń dokłada wpisy do TEJ SAMEJ historii co ręczne
   przesunięcia (patrz Timeline.dragHistory), więc te przyciski muszą
   działać także w trybie usuwania. */
.app-header.delete-locked .header-actions .btn:not(#btn-delete-elements),
.app-header.delete-locked .side-toolbar .btn:not(#btn-undo-move):not(#btn-redo-move),
.app-header.delete-locked #btn-theme-toggle,
.app-header.delete-locked #btn-data {
  opacity: 0.4;
  filter: grayscale(0.6);
  pointer-events: none;
  cursor: not-allowed;
}

.app-header.delete-locked .dropdown-menu {
  display: none !important;
}

.btn-text {
  background: none;
  color: var(--color-text-secondary);
  padding: 4px 8px;
}

.btn-text:hover { color: var(--color-danger); }

.btn-danger {
  background: var(--color-danger);
  color: white;
  border: 1px solid var(--color-danger);
}

.btn-danger:hover {
  filter: brightness(0.92);
}

.btn-large {
  width: auto;
  height: auto;
  padding: 12px 24px;
  border-radius: var(--radius-md);
  font-size: 1rem;
}

/* Emoji-czcionka renderuje glif przesunięty względem własnego pudełka znaku
   inaczej niż zwykły tekst — mimo że flexbox (.btn: align-items: center)
   poprawnie centruje SAME kontenery ikony i etykiety, wizualnie ikona i tak
   wychodziła inaczej niż tekst obok. `:has(+ .btn-label)` celowo ogranicza
   poprawkę TYLKO do ikon stojących obok etykiety tekstowej — same ikony bez
   etykiety (patrz .btn-icon-only) są już poprawnie wyśrodkowane same w
   sobie, nie potrzebują przesunięcia. `:not(.btn-icon-glyph)` wyklucza
   zwykłe znaki tekstowe (np. "+" przy "Dodaj członka rodziny") — TE są już
   poprawnie wyśrodkowane jak zwykły tekst i przesunięcie zrobione pod kątem
   emoji tylko by je popsuło. Zgłoszone wprost przez użytkownika. */
.btn-icon { font-size: 1.1rem; }

.btn-icon:has(+ .btn-label):not(.btn-icon-glyph) {
  transform: translateY(-2px);
}

/* Powyższe -2px jest wykalibrowane pod emoji-czcionkę Windows/Androida
   (Segoe UI Emoji/Noto Color Emoji) — na iPadOS/iOS (Apple Color Emoji, z
   INNYMI metrykami glifu w pudełku znaku) ta sama poprawka nadkompensowuje i
   ikona wychodzi za wysoko, mimo że etykieta obok JEST widoczna (to nie jest
   przypadek z ukrytym .btn-label na wąskim ekranie — patrz osobna poprawka w
   media query 768px niżej w pliku; ikony na PC i Androidzie wypadają
   poprawnie, tylko iPadOS ma inne metryki). @supports
   (-webkit-touch-callout: none) to standardowy, szeroko używany sposób
   celowania WYŁĄCZNIE w Safari na iOS/iPadOS (ta właściwość istnieje tylko
   tam — ani w desktopowym Safari, ani w żadnej przeglądarce na Androidzie) —
   nie ma innego niezawodnego selektora CSS rozróżniającego platformy.
   Zgłoszone wprost przez użytkownika (test na iPadzie, po stronie Androida
   i PC już poprawne). */
@supports (-webkit-touch-callout: none) {
  .btn-icon:has(+ .btn-label):not(.btn-icon-glyph) {
    transform: none;
  }
}

/* ===== DRZEWO GENEALOGICZNE / OŚ CZASU ===== */
.timeline-container {
  /* top:0 (nie var(--header-height)) — drzewo ma sięgać POD przezroczysty
     nagłówek, żeby przez niego prześwitywały karty/linie zamiast gołego
     tła strony. Nagłówek i tak zostaje na wierzchu (z-index:100), więc
     górny skrawek drzewa bywa częściowo zasłonięty przyciskami — to
     świadomy kompromis przezroczystego, pływającego nagłówka. */
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  overflow: hidden;
}

.timeline-wrapper {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  cursor: grab;
  touch-action: none;
}

.timeline-wrapper.dragging {
  cursor: grabbing;
}

/* Poziome linie lat w tle (patrz Timeline.render — .year-separator) — POD
   liniami relacji (.connections-layer, z-index:1), nie razem z kartami
   wewnątrz .timeline-world (z-index:2, samo ustanawia stacking context,
   więc z-index samego .year-separator i tak nigdy nie wyszedłby POD
   .connections-layer, dopóki żyje w środku). Osobna warstwa na własnym,
   niższym z-index to jedyny sposób, żeby linie relacji faktycznie rysowały
   się NAD siatką lat. Zgłoszone wprost przez użytkownika. */
.year-separators-layer {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 0;
  pointer-events: none;
  transform-origin: 0 0;
}

.connections-layer {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
  z-index: 1;
  overflow: visible;
  transform-origin: 0 0;
  /* CELOWO bez will-change/backface-visibility — próbowaliśmy tego jako
     obejścia migotania linii na iPadzie (patrz [[connections_ipad_zoom_flicker]]),
     ale to nie pomogło, a przy dużym drzewie (ta warstwa ma rozmiar CAŁEGO
     świata, potencjalnie kilkanaście tysięcy pikseli) wymuszenie promocji do
     osobnej warstwy GPU zaczęło zamiast tego psuć renderowanie NA PC —
     prawdopodobnie limit rozmiaru tekstury GPU. Zgłoszone wprost przez
     użytkownika. */
}

/* W trybie usuwania warstwa linii musi wysunąć się przed .timeline-world —
   inaczej ten (niewidoczny, ale obecny) kontener kart przechwytuje kliknięcia
   nad każdym miejscem, gdzie linia biegnie poza obrysem karty, i .connection-hit
   pod spodem nigdy ich nie dostaje. Poza obszarem samych linii warstwa
   pozostaje przezroczysta dla kliknięć (pointer-events: none), więc karty
   nadal działają normalnie. */
.connections-layer.delete-mode {
  z-index: 3;
}

.timeline-world {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 2;
  transform-origin: 0 0;
}

.year-separator {
  position: absolute;
  height: 1px;
  background: var(--color-border-light);
  pointer-events: none;
}

/* ===== KOLUMNA LAT (zawsze widoczna) ===== */
.year-labels-bg {
  position: absolute;
  top: 0;
  left: 0;
  width: 64px;
  height: 100%;
  z-index: 3;
  pointer-events: none;
  background: linear-gradient(to right, var(--color-bg) 75%, transparent);
}

.year-labels {
  position: absolute;
  top: 0;
  left: 0;
  width: 64px;
  z-index: 4;
  pointer-events: none;
  transform-origin: 0 0;
}

.year-label-row {
  position: absolute;
  left: 0;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding-right: 10px;
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
  opacity: 0.6;
}

.year-label-row.populated {
  font-size: 0.8125rem;
  font-weight: 700;
  color: var(--color-text);
  opacity: 1;
}

/* ===== KARTA OSOBY =====
   Zdjęcie wypełnia CAŁY kafelek z małym, 6px marginesem (patrz inset:6px w
   .person-card-photo/.person-card-photo-placeholder niżej), tekst
   (imię/nazwisko + daty) jest nakładką na dole zdjęcia, odznaka płci w
   rogu. Karta ma więc stałą wysokość (nie min-height jak dawniej) — to
   zdjęcie wyznacza kształt karty, nie odwrotnie — i zero paddingu/flex:
   wszystkie dzieci pozycjonują się przez position:absolute względem niej. */
.person-card {
  width: var(--card-width);
  height: var(--card-height);
  /* BEZ backdrop-filter — tło pod kartami to i tak niemal zawsze jednolity
     kolor strony (--color-bg), więc efekt szkła nie dawał żadnej widocznej
     różnicy, tylko kosztował GPU (przeliczany na każdej klatce pan/zoomu,
     zwłaszcza dotkliwe na słabszym mobilnym GPU). --glass-bg (już
     półprzezroczyste samo w sobie) zostaje. Zgłoszone wprost przez
     użytkownika: zacinanie się przy oddalaniu drzewa na telefonie. */
  background: var(--glass-bg);
  border: 1.5px solid var(--glass-border);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: all var(--transition);
  box-shadow: var(--shadow-sm);
  position: absolute;
  z-index: 3;
  /* Karty się przeciąga (patrz Timeline pointerdown/move w timeline.js) —
     bez tego przypadkowe "złapanie" myszką za zdjęcie/tekst przy szybkim
     ruchu zaznaczało je na niebiesko jak zwykły tekst strony. */
  user-select: none;
  -webkit-user-select: none;
}

.person-card:hover {
  border-color: var(--color-primary);
  box-shadow: var(--shadow-md);
  /* CELOWO bez transform:translateY — karta wizualnie "odjeżdżała" od
     linii relacji (rysowanych osobno, w SVG, bez pojęcia o tym przesunięciu
     pojedynczej karty), więc przy najechaniu linia wyglądała, jakby się
     odklejała od zdjęcia. Zgłoszone wprost przez użytkownika. */
  /* Podniesione nad zwykłe sąsiednie karty (z-index:3) — inaczej panel z
     danymi dodatkowymi (patrz .person-card-details niżej), wysuwający się
     z prawej strony, chowałby się POD kartą sąsiada zamiast nad nią. */
  z-index: 15;
}

/* Kursor "chwytu" podpowiada możliwość przeciągania TYLKO w aktywnym trybie
   przesuwania elementów (przycisk-ołówek) — poza nim karta zachowuje się jak
   zwykły, klikalny element (cursor: pointer). */
.timeline-world.move-mode .person-card {
  cursor: grab;
}

/* Ręczne przeciąganie karty w poziomie (patrz Timeline.clampDragX) — bez
   `transition` na czas ruchu, żeby karta podążała za kursorem od razu, a nie
   dojeżdżała do niego z opóźnieniem właściwym dla .person-card (transition:
   all). Podniesiona nad resztę (z-index), żeby wizualnie przechodziła nad
   sąsiadami podczas przesuwania. */
.person-card.card-dragging {
  transition: none;
  cursor: grabbing;
  box-shadow: var(--shadow-lg);
  z-index: 20;
}

/* Pierścień podświetlenia jako OUTLINE, nie border — outline żyje poza
   modelem pudełkowym (nie wlicza się do box-sizing), więc w przeciwieństwie
   do wcześniejszej wersji (grubszy border-width) w ogóle nie zmniejsza
   wciętej procentowo ramki zdjęcia (patrz .person-card-photo-frame) —
   można go więc swobodnie pogrubiać bez ryzyka powrotu tamtego "skoku"
   zdjęcia przy najechaniu. Sam border karty (1.5px, patrz .person-card)
   zostaje bez zmian we WSZYSTKICH stanach. calc(.../var(--zoom-scale)) —
   ten sam mechanizm co przy .connection-line: stała grubość na EKRANIE
   niezależnie od zoomu (patrz Timeline.applyTransform w timeline.js).
   Powiększone (3px, było efektywnie 2.5px) i mniej przezroczyste (pełny,
   kryjący kolor outline zamiast półprzezroczystej obwódki) — zgłoszone
   wprost przez użytkownika. */
.person-card.highlighted {
  background: var(--color-primary-light);
  outline: calc(3px / var(--zoom-scale, 1)) solid var(--color-highlight);
  /* Większy, wyraźniejszy pierścień — było 3px/0.25, zgłoszone wprost przez
     użytkownika jako za słabo widoczne. */
  box-shadow: 0 0 0 6px rgba(61, 107, 79, 0.35), var(--shadow-md);
  z-index: 10;
}

/* Najechanie na linię MAŁŻEŃSTWA/PARTNERSTWA (patrz
   Connections.highlightRelation, flaga isUnion) — obwódka i poświata karty
   mają być żółte (kolor linii unii, --color-union), a nie zielone jak przy
   reszcie relacji. Nadpisuje outline-color/box-shadow z .highlighted wyżej —
   musi więc być PO nim w arkuszu, obie klasy są nadawane razem w JS.
   Zgłoszone wprost przez użytkownika. */
.person-card.union-hover-highlighted {
  outline-color: var(--color-union);
  box-shadow: 0 0 0 6px rgba(196, 160, 53, 0.35), var(--shadow-md);
}

/* Najechanie na ADOPTOWANE dziecko (patrz Connections.setHighlight/
   Timeline.onPersonHover, Models.isAdoptedChild) — obwódka i poświata karty
   (i cała linia przodków, patrz .connection-line.adoption-theme niżej) mają
   być niebieskie (--color-adoption), a nie zielone jak przy reszcie relacji
   — linia przodków przy takim najechaniu śledzi już wyłącznie stronę
   biologiczną (patrz Models.getAncestors), więc niebieski tu sygnalizuje
   "to jest adoptowana osoba", nie "to jest linia adopcyjna". Nadpisuje
   outline-color/box-shadow z .highlighted wyżej — musi więc być PO nim w
   arkuszu, obie klasy są nadawane razem w JS. Zgłoszone wprost przez
   użytkownika. */
.person-card.adoption-theme {
  outline-color: var(--color-adoption);
  box-shadow: 0 0 0 6px rgba(59, 110, 165, 0.35), var(--shadow-md);
}

.person-card.dimmed {
  opacity: 0.25;
  filter: grayscale(0.4);
}

.person-card.secondary-highlight {
  border-color: rgba(61, 107, 79, 0.5);
  background: rgba(232, 240, 235, 0.6);
  opacity: 0.85;
  z-index: 5;
}

/* Tryb usuwania: kursor sygnalizuje klikalność wszystkich kart, samo
   podświetlenie na czerwono (jak przy linii-celu usuwania) pojawia się
   dopiero na konkretnej karcie pod kursorem. */
.timeline-world.delete-mode .person-card {
  cursor: pointer;
}

.person-card.delete-target {
  border-color: var(--color-danger);
  background: #fdecea;
  box-shadow: 0 0 0 3px rgba(192, 57, 43, 0.25), var(--shadow-md);
  z-index: 10;
}

/* Zdjęcie (albo placeholder) wypełnia całą kartę poza wąskim marginesem
   (inset) — dzięki temu, że sama karta ma już format 3:4 (--card-width/
   --card-height), zdjęcie w tym samym stosunku wypełnia niemal cały
   kafelek zamiast być małą, osobną miniaturą obok tekstu. object-fit:cover
   przycina realne zdjęcie do tego kształtu bez zniekształceń, niezależnie
   od proporcji oryginalnego pliku. */
/* Ramka przejmuje pozycjonowanie/inset dawnego .person-card-photo (patrz
   niżej) i przycina (overflow:hidden) — potrzebne, odkąd samo zdjęcie może
   być większym, wykadrowanym w JS elementem (patrz Utils.applyPhotoCrop),
   a nie tylko zwykłym object-fit:cover na całej ramce. */
.person-card-photo-frame {
  /* inset W PROCENTACH (nie stałe 6px z obu stron) — karta ma proporcje 3:4,
     NIE kwadrat, więc odjęcie tej samej liczby pikseli od szerokości i
     wysokości psuło te proporcje (np. na karcie 210x280 dawało ramkę
     198x268 ≈ 0,739 zamiast 0,75). Utils.applyPhotoCrop pozycjonuje
     wykadrowane zdjęcie w PROCENTACH względem ramki, zakładając, że ta jest
     DOKŁADNIE 3:4 — tak samo jak okno narzędzia kadrowania (patrz
     .photo-crop-viewport) — każde odchylenie od 3:4 rozciągało/przekrzywiało
     zdjęcie ("rozjechane"), tym mocniej im mniejsza karta (mobilne warianty
     --card-width/--card-height). Procent działa poprawnie, bo CSS liczy
     top/bottom względem WYSOKOŚCI, a left/right względem SZEROKOŚCI — ta
     sama wartość skaluje więc obie strony proporcjonalnie i zachowuje 3:4. */
  position: absolute;
  inset: 2.15%;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--color-bg-muted);
}

.person-card-photo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.person-card-photo-placeholder {
  /* Ten sam procentowy inset co .person-card-photo-frame wyżej — czysto dla
     spójności marginesu, niezależnie od tego, czy dana karta ma zdjęcie. */
  position: absolute;
  inset: 2.15%;
  border-radius: var(--radius-sm);
  background: var(--color-bg-muted);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2.5rem;
  color: var(--color-text-muted);
}

/* Nakładka z imieniem/nazwiskiem na dole zdjęcia — gradient zawsze ciemny i
   tekst zawsze biały, NIEZALEŻNIE od motywu strony, bo tło pod spodem to
   dowolne, nieprzewidywalne zdjęcie (a nie var(--color-bg)); ten sam
   kontrast, który dobrze czyta się na jasnym motywie, musi też działać na
   czarno-białym albo bardzo jasnym zdjęciu. Wysokość nakładki to tylko
   wysokość tekstu (brak top — box rośnie od dołu wraz z zawartością), ale
   left/right:0 (nie 6px) — w poziomie ma sięgać PEŁNEJ szerokości zdjęcia,
   od samej krawędzi karty, a nie zostawiać wąski, nieprzysłonięty pas po
   bokach. Daty/miejsce zamieszkania przeniosły się do osobnego panelu
   widocznego po najechaniu (patrz .person-card-details niżej) — sama
   etykieta ma pozostać krótka i czytelna: tylko zdjęcie i imię/nazwisko (z
   ewentualnym tytułem, patrz Utils.getShortName). */
.person-card-overlay {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  /* Padding górny większy niż dolny/boczne — box (a razem z nim gradient)
     ma się kończyć kawałek NAD tekstem, nie ściśle przy jego górnej krawędzi. */
  padding: 26px 10px 8px;
  /* left/right/bottom:0 liczą się od KRAWĘDZI WEWNĘTRZNEJ obwódki karty
     (padding-box, nie border-box) — czyli ten box jest wcięty o grubość
     obwódki (1.5px) względem prawdziwego, zewnętrznego rogu karty. Promień
     TEGO SAMEGO co --radius-md (czyli promień samej, zewnętrznej krawędzi)
     był więc za duży jak na już wcięty box — rozjeżdżał się z krzywizną
     obwódki i zostawiał widoczną, jasną szczelinę w dolnych rogach (prześwit
     szklanego tła karty). calc() odejmuje dokładnie tyle, o ile ten box jest
     wcięty, żeby obie krzywizny były współśrodkowe. Osobna, większa wartość
     przy .highlighted niżej — tam obwódka jest grubsza (2.5px). Zgłoszone
     wprost przez użytkownika. */
  border-radius: 0 0 calc(var(--radius-md) - 1.5px) calc(var(--radius-md) - 1.5px);
  background: linear-gradient(to top, rgba(0, 0, 0, 0.78), rgba(0, 0, 0, 0.4) 65%, transparent);
  z-index: 2;
  min-width: 0;
}

.person-card-name {
  font-size: 1.0625rem;
  font-weight: 700;
  color: #fff;
  line-height: 1.25;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
  /* Imię/nazwisko WOLNO zawijać (do 2 linii) — ucinanie długich (np.
     złożonych) nazwisk wielokropkiem byłoby gorsze niż jedna dodatkowa
     linia. */
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

/* Nazwisko rodowe/panieńskie i pseudonim — pod imieniem i nazwiskiem, każde
   w osobnej linii (patrz Timeline.createPersonCard). Mniejsza czcionka +
   wielkie litery odróżniają je od głównego imienia/nazwiska, zamiast
   konkurować z nim o uwagę. Ten sam text-shadow co .person-card-name — leżą
   na tym samym, nieprzewidywalnym zdjęciu w tle. Zgłoszone wprost przez
   użytkownika. */
.person-card-maiden-name,
.person-card-nickname {
  font-size: 0.6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  line-height: 1.3;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}


/* Delikatnie wyszarzone — półprzezroczysta biel (nie szary), bo leży na tym
   samym ciemnym gradiencie co .person-card-name, gdzie zwykły szary wypadłby
   albo za ciemno (nieczytelnie), albo niespójnie z motywem zdjęcia. */
.person-card-maiden-name {
  color: rgba(255, 255, 255, 0.65);
}

.person-card-nickname {
  color: rgba(255, 255, 255, 0.85);
}

/* Panel danych dodatkowych (data urodzenia/zgonu, miejsce zamieszkania,
   zawód, wykształcenie, wyznanie) —
   domyślnie niewidoczny i nieklikalny, wysuwa się z prawej strony karty
   dopiero po najechaniu (patrz .person-card:hover niżej). Wycentrowany W
   PIONIE względem karty (top: 50% + translateY(-50%) w bazowym transformie
   niżej), a NIE przyklejony do jej górnej krawędzi jak dawniej — środek
   panelu wypada na wysokości środka karty niezależnie od tego, ile wierszy
   danych akurat zawiera. W przeciwieństwie do nakładki na zdjęciu ma zwykłe,
   "kartowe" tło zależne od motywu — nie leży już na nieprzewidywalnym
   zdjęciu, więc nie potrzebuje ciemnego gradientu/białego tekstu na stałe. */
.person-card-details {
  position: absolute;
  top: 50%;
  left: calc(100% + 10px);
  z-index: 5;
  min-width: 150px;
  max-width: 220px;
  padding: 10px 12px;
  border-radius: var(--radius-md);
  background: var(--glass-bg-strong);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border: 1px solid var(--glass-border);
  box-shadow: var(--shadow-lg);
  opacity: 0;
  transform: translateY(-50%) translateX(-6px);
  pointer-events: none;
  transition: opacity var(--transition), transform var(--transition);
}

/* .hover-target — dokładany TYLKO do bezpośredniego celu najechania/
   stuknięcia (patrz Timeline.onPersonHover), nie do całej podświetlonej
   linii przodków (.highlighted, obejmuje też przodków). Na myszce pokrywa
   się z :hover; na dotyku (gdzie :hover nie istnieje, patrz click w
   createPersonCard w timeline.js) to jedyny sposób pokazania panelu dla
   TEJ jednej karty: pierwsze stuknięcie podświetla linię przodków i
   pokazuje ten panel, dopiero DRUGIE stuknięcie (już podświetlonej karty)
   otwiera profil. Zgłoszone wprost przez użytkownika. */
.person-card:hover .person-card-details,
.person-card.hover-target .person-card-details {
  opacity: 1;
  transform: translateY(-50%) translateX(0);
}

/* Daty NIGDY się nie zawijają (white-space: nowrap) — karta jest
   wystarczająco szeroka, a panel wystarczająco luźny, żeby pełna polska
   data ("3 października 2003") zmieściła się w jednej linii. Miejsce
   zamieszkania WOLNO uciąć wielokropkiem — nazwy miejscowości (zwłaszcza z
   dopiskiem dzielnicy/regionu) nie mają tak przewidywalnej długości. */
.person-card-detail-row {
  font-size: 0.8125rem;
  color: var(--color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.person-card-detail-row + .person-card-detail-row {
  margin-top: 5px;
}

.person-card-detail-death {
  color: var(--color-danger);
}

/* ===== TRYB PRYWATNOŚCI (przycisk-oczko obok żarówki) =====
   Zamazuje nazwiska i inne dane wrażliwe na kartach osi i w profilu —
   sama struktura strony (etykiety pól, karty, przyciski) zostaje czytelna,
   rozmywają się tylko wartości. Imię (i ewentualny tytuł naukowy przed nim)
   CELOWO zostają czytelne nawet w tym trybie — to jedyny sposób, żeby dało
   się rozpoznać, która karta/wpis jest której osoby, bez ujawniania pełnej
   tożsamości (nazwiska). Stąd rozbicie na dwa poziomy:
   1) .person-card-name/#profile-name/.profile-field-value same w sobie
      NIE są już rozmywane — Utils.shortNameHtml/fullNameHtml (patrz
      utils.js) owija w nich tylko nazwisko w <span class="privacy-sensitive">,
      a profile.js oznacza pola Imię/Tytuł naukowy jako
      .profile-field-value-visible.
   2) .privacy-sensitive (nazwisko, dzień+miesiąc — ale NIE rok — daty
      urodzenia, nazwisko rodowe/panieńskie, pseudonim, data zawarcia
      związku) i reszta pól/paneli poniżej rozmywają się w całości.
   Klikalność pod spodem (np. linki rodziny w profilu) celowo zostaje
   nietknięta — filter:blur nie wyłącza wskaźnika. Promień 10px (nie 6px)
   CELOWO: krótkie, pogrubione fragmenty (samo nazwisko) przy słabszym
   rozmyciu wciąż dało się odgadnąć z kształtu liter. */
body.privacy-mode .privacy-sensitive,
/* Pokolenie i pełna data urodzenia (patrz .person-card-detail-row-visible w
   Timeline.createPersonCard) — same w sobie nikogo nie identyfikują, w
   przeciwieństwie do reszty tego panelu, więc zostają czytelne. Zgłoszone
   wprost przez użytkownika. */
body.privacy-mode .person-card-detail-row:not(.person-card-detail-row-visible),
body.privacy-mode .profile-field-value:not(.profile-field-value-visible),
body.privacy-mode .profile-text-block {
  filter: blur(10px);
  user-select: none;
}

/* Formularz dodawania/edycji — dane osoby wchodzą do pól jako zwykła
   wartość inputu/textarea (patrz Forms.openEdit), więc filter:blur na
   .profile-field-value tam nie sięga: to zupełnie inne elementy. Wyjątek
   :not(:focus) jest konieczny — bez niego nie dałoby się DOKOŃCZYĆ edycji
   akurat wykadrowanego, zamazanego pola. Pola kategoryczne (select, radio)
   celowo pominięte — "mężczyzna"/"wykształcenie wyższe" nie identyfikuje
   nikogo tak jak nazwisko czy adres. Imię/drugie imię (#firstName,
   #middleNames) też celowo pominięte, tak samo jak w reszcie aplikacji. */
body.privacy-mode .person-form input[type="text"]:not(:focus):not(#firstName):not(#middleNames),
body.privacy-mode .person-form input[type="date"]:not(:focus),
body.privacy-mode .person-form textarea:not(:focus) {
  filter: blur(10px);
}

/* Tryb prywatności to tryb TYLKO DO PRZEGLĄDANIA — te same wyszarzone/
   nieklikalne przyciski co .editing-locked/.delete-locked wyżej, ale
   niezależnie od nich: nie da się dodać osoby (nagłówek i pusty stan),
   usunąć elementu, dodać połączenia, wejść w tryb przesuwania (edycja
   położenia kart) ani otworzyć edycji danych z profilu. App.initPrivacy
   dodatkowo od razu przerywa którykolwiek z tych trybów, gdyby był już
   aktywny w chwili włączenia trybu prywatności. */
body.privacy-mode #btn-add-person,
body.privacy-mode #btn-add-first,
body.privacy-mode #btn-delete-elements,
body.privacy-mode #btn-connections,
body.privacy-mode #btn-move-mode,
body.privacy-mode #btn-edit-person {
  opacity: 0.4;
  filter: grayscale(0.6);
  pointer-events: none;
  cursor: not-allowed;
}

/* Odznaka płci — prawy górny róg karty, NAD wszystkim innym (photo,
   overlay), stąd position:absolute względem samej .person-card (ta już ma
   position:absolute do własnego pozycjonowania w drzewie, więc naturalnie
   pełni też rolę kontenera pozycjonującego dla tej odznaki). Zwykłe kolorowe
   kółeczko zamiast ikony — kolor niesie informację o płci (niebieski/
   różowy/szary), obramowanie w kolorze tła karty odcina je wizualnie od
   zdjęcia pod spodem, żeby było widoczne na każdym tle. */
.person-card-gender-badge {
  position: absolute;
  top: -4px;
  right: -4px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 2.5px solid var(--color-bg-elevated);
  box-shadow: var(--shadow-sm);
  z-index: 4;
}

.person-card-gender-badge.male { background: var(--color-male); }
.person-card-gender-badge.female { background: var(--color-female); }
.person-card-gender-badge.other { background: var(--color-text-muted); }

/* ===== LINIE POŁĄCZEŃ ===== */
.connection-line {
  fill: none;
  /* calc(2px / var(--zoom-scale)) — TEN SAM mechanizm co border-width przy
     .person-card.highlighted (patrz tam i Timeline.applyTransform w
     timeline.js, ustawia --zoom-scale na :root): dzieląc przez bieżącą
     skalę z góry "zawyżamy" grubość w układzie ŚWIATA dokładnie o tyle, o
     ile świat zaraz zostanie pomniejszony transformacją CSS, więc na
     EKRANIE zawsze wychodzi ta sama, stała grubość. Samo
     vector-effect:non-scaling-stroke niżej NIE wystarczyło — bywa
     niewiarygodne, gdy scale() jest nałożony CSS-em na sam KORZEŃ <svg>
     (tak jak tu, patrz Connections.draw), zamiast na element wewnątrz
     niego — różni się między przeglądarkami. Zostawione jako dodatkowe
     zabezpieczenie, nie jedyny mechanizm. stroke-width CELOWO poza
     transition niżej — ma nadążać za zoomem 1:1, bez opóźnienia płynnej
     animacji. Zgłoszone wprost przez użytkownika. */
  stroke-width: calc(2px / var(--zoom-scale, 1));
  transition: stroke-opacity var(--transition);
  vector-effect: non-scaling-stroke;
}

.connection-line.union {
  stroke: var(--color-union);
  stroke-dasharray: none;
}

.connection-line.parent-child {
  stroke: var(--color-parent-child);
}

/* Odnoga KONKRETNEGO adoptowanego dziecka do rodziców (patrz
   Connections.drawParentChild — flaga isAdoption dodawana tylko do "feedera"
   tego jednego dziecka, NIE do wspólnego pnia/odnóg rodziców, które mogą być
   dzielone też z jego biologicznym rodzeństwem w tej samej grupie) —
   specyficzność .parent-child.adoption (3 klasy) przebija samo .parent-child
   (2 klasy) niezależnie od kolejności w arkuszu. Zgłoszone wprost przez
   użytkownika. */
.connection-line.parent-child.adoption {
  stroke: var(--color-adoption);
}

/* Cała linia przodków przy najechaniu na ADOPTOWANE dziecko (patrz
   Connections.getLineClass/setHighlight, Models.isAdoptedChild) — śledzi
   już wyłącznie stronę biologiczną (Models.getAncestors pomija relacje
   adopcyjne przy wspinaniu), więc to NIE to samo co .adoption wyżej (kolor
   TEJ KONKRETNEJ relacji adopcyjnej) — tu chodzi o zabarwienie całej
   podświetlonej (biologicznej) linii na niebiesko, bo najechana osoba jest
   adoptowana. 4 klasy — wygrywa z .parent-child.adoption (3 klasy)
   niezależnie od kolejności w arkuszu. Zgłoszone wprost przez użytkownika. */
.connection-line.parent-child.highlighted.adoption-theme {
  stroke: var(--color-adoption);
}

.connection-line.dimmed {
  stroke-opacity: 0.08;
}

.connection-line.normal {
  stroke-opacity: 0.55;
}

/* Musi być PRZED "hover-dimmed" (patrz niżej), za to PO normal/dimmed — przy
   najechaniu na linię dopisujemy klasę "highlighted" do już istniejącej
   "normal" albo "dimmed" (nadanej wcześniej przy rysowaniu, wg podświetlenia
   osoby), więc przy takiej samej specyficzności wygrywa kolejność w
   arkuszu, nie kolejność dodania klas w JS. */
.connection-line.highlighted {
  /* Bez własnego stroke-width — zostaje ta sama, stała grubość co bazowa
     .connection-line (2px), tylko w pełni kryjąca (stroke-opacity niżej).
     Było 4.5px (grubsza), zgłoszone wprost przez użytkownika, że ma
     zostawać tej samej wielkości, nie powiększać się. */
  stroke-opacity: 1;
}

/* Reszta linii przy najechaniu na jedną konkretną — celowo osobna klasa, nie
   "dimmed" (ta jest już zajęta przez podświetlanie linii przodków przy
   najechaniu na OSOBĘ, patrz wyżej, i miałaby tę samą specyficzność co
   "normal"). Musi być OSTATNIA w tej grupie, żeby zawsze wygrywać. */
.connection-line.hover-dimmed {
  stroke-opacity: 0.08;
}

/* SAMA najechana linia (patrz Connections.highlightRelation) — bez tego
   najechanie na linię tylko gasiło resztę, a ta pod kursorem zostawała w
   swoim dotychczasowym stanie ("normal"/"dimmed" z podświetlenia przodków),
   zamiast się wyróżnić. Wzajemnie wykluczające się z hover-dimmed (JS nigdy
   nie dodaje obu naraz), ale i tak OSTATNIA w tej grupie z tego samego
   powodu co hover-dimmed wyżej — ma zawsze wygrywać. Zgłoszone wprost przez
   użytkownika. */
.connection-line.hover-highlighted {
  /* Patrz komentarz przy .connection-line.highlighted wyżej — ten sam
     powód: zostaje bazowa grubość, nie powiększa się. */
  stroke-opacity: 1;
}

/* Najechanie na relację ADOPCYJNĄ (patrz Connections.highlightRelation,
   flaga isAdoption) — cały jej układ, w tym wspólny pień/odgałęzienia do
   rodziców, ma się podświetlić na niebiesko, nie zielono. Pień sam w sobie
   NIE ma wypalonej klasy ".adoption" na stałe (patrz .connection-line.
   parent-child.adoption wyżej — dostaje ją tylko odnoga SAMEGO dziecka), więc
   bez tej reguły zostałby zielony mimo pełnej nieprzezroczystości z
   .hover-highlighted powyżej (ta klasa zmienia tylko stroke-opacity, nie
   kolor). 3 klasy — wygrywa z .parent-child (2 klasy) niezależnie od
   kolejności w arkuszu. Zgłoszone wprost przez użytkownika. */
.connection-line.hover-highlighted.adoption-theme {
  stroke: var(--color-adoption);
}

/* Znacznik z datą związku i liczbą wspólnych lat (patrz
   Connections.drawUnionDateBadge) — żyje w foreignObject WEWNĄTRZ tego
   samego SVG co linia unii, więc skaluje się z nią razem przy zoomie/panie.
   -wrap wypełnia cały (celowo szerszy niż potrzeba) box foreignObject i tylko
   centruje flexboksem właściwą "pigułkę" (.union-date-badge, szerokość
   auto do treści) — bez tego trzeba by z góry znać dokładną szerokość
   tekstu, żeby poprawnie ustawić x samego foreignObject. */
.union-date-badge-wrap {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}

.union-date-badge {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  padding: 4px 10px;
  border-radius: var(--radius-md);
  background: var(--glass-bg-strong);
  backdrop-filter: blur(16px) saturate(180%);
  -webkit-backdrop-filter: blur(16px) saturate(180%);
  border: 1px solid var(--color-union);
  box-shadow: var(--shadow-sm);
  white-space: nowrap;
  transition: opacity var(--transition);
  /* TYLKO ta pigułka (nie cały, szerszy -wrap) ma łapać mysz — patrz
     Connections.bindRelationHover — żeby najechanie na jej widoczny
     prostokąt podświetlało relację tak samo jak najechanie na samą linię
     (.connection-hit ma tylko wąski, niewidoczny pasek wzdłuż trasy). */
  pointer-events: auto;
}

.union-date-badge-date {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--color-text);
  line-height: 1.3;
}

.union-date-badge-years {
  font-size: 0.6875rem;
  color: var(--color-text-muted);
  line-height: 1.3;
}

.union-date-badge.dimmed {
  opacity: 0.08;
}

/* Znacznik daty adopcji (patrz Connections.drawAdoptionDateBadge) używa TEJ
   SAMEJ pigułki co data związku (.union-date-badge), tylko z niebieską
   obwódką zamiast żółtej — spójnie z niebieską linią samej relacji
   (--color-adoption, patrz .connection-line.parent-child.adoption wyżej).
   Nie ma odpowiednika .union-date-badge-years pod spodem — "X lat razem" ma
   sens dla związku dwojga żyjących razem ludzi, nie dla adopcji. Zgłoszone
   wprost przez użytkownika. */
.union-date-badge.adoption {
  border-color: var(--color-adoption);
}

/* Najechanie na linię unii (albo samą tabliczkę) podświetla oboje razem —
   patrz Connections.highlightRelation/drawUnionDateBadge (dopisuje samą
   siebie do highlightEls linii). Ten sam żółty poświata-pierścień co
   .person-card.union-hover-highlighted, dla spójności kolorystycznej.
   Zgłoszone wprost przez użytkownika. */
.union-date-badge.hover-highlighted {
  box-shadow: 0 0 0 4px rgba(196, 160, 53, 0.35), var(--shadow-md);
}

/* .adoption.hover-highlighted (3 klasy) musi przebić samo .hover-highlighted
   (2 klasy) wyżej, niezależnie od kolejności w arkuszu — niebieska poświata
   zamiast żółtej, ten sam odcień co --color-adoption. */
.union-date-badge.adoption.hover-highlighted {
  box-shadow: 0 0 0 4px rgba(59, 110, 165, 0.35), var(--shadow-md);
}

.connections-layer.delete-mode .union-date-badge {
  cursor: pointer;
}

.connection-crossing {
  fill: var(--color-bg);
  stroke: var(--color-border);
  stroke-width: 1.5;
  pointer-events: none;
}

/* ===== PUSTY STAN ===== */
.empty-state {
  position: fixed;
  top: var(--header-height);
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-bg);
  z-index: 50;
}

.empty-state-content {
  text-align: center;
  max-width: 400px;
  padding: 32px;
}

.empty-state-icon {
  font-size: 4rem;
  margin-bottom: 16px;
}

.empty-state-content h2 {
  font-family: var(--font-sans);
  font-size: 1.5rem;
  margin-bottom: 8px;
  color: var(--color-text);
}

.empty-state-content p {
  color: var(--color-text-secondary);
  margin-bottom: 24px;
}

/* ===== POŁĄCZ FOLDER (WYMAGANE) ===== */
.connect-folder-content {
  text-align: center;
  padding: 32px 28px;
}

.connect-folder-logo {
  width: 170px;
  max-width: 80%;
  height: auto;
  margin: 0 auto 20px;
  display: block;
  /* Przesunięcie w prawo o dosłownie 2px względem wyśrodkowania —
     zgłoszone wprost przez użytkownika. */
  transform: translateX(2px);
}

/* Domyślnie (motyw jasny) wariant z ciemnym tekstem, ukryty wariant z
   białym — :root[data-theme="dark"] (patrz App.initTheme w app.js) je
   zamienia. */
.connect-folder-logo-dark {
  display: none;
}

:root[data-theme="dark"] .connect-folder-logo-light {
  display: none;
}

:root[data-theme="dark"] .connect-folder-logo-dark {
  display: block;
}

.connect-folder-content h2 {
  font-family: var(--font-sans);
  font-size: 1.35rem;
  margin-bottom: 12px;
  color: var(--color-text);
}

.connect-folder-content p {
  color: var(--color-text-secondary);
  font-size: 0.875rem;
  line-height: 1.6;
  margin-bottom: 24px;
}

/* Przypomnienie o regularnym zapisie (patrz index.html) — wyróżnione na
   czerwono, tym samym wzorcem co .zip-password-tip, żeby nie zlewało się z
   resztą, neutralnie szarego tekstu w tym modalu. Powtarzamy tu
   ".connect-folder-content p" — bez tego ta reguła miałaby TĘ SAMĄ
   specyficzność co reguła szarego tekstu wyżej i przegrałaby z nią kolejnością
   w arkuszu. Zgłoszone wprost przez użytkownika. */
.connect-folder-content p.connect-folder-warning {
  background: #fdecea;
  border: 1px solid rgba(192, 57, 43, 0.35);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  color: var(--color-danger);
  font-weight: 600;
}

:root[data-theme="dark"] .connect-folder-warning {
  background: rgba(74, 43, 40, 0.35);
  border-color: rgba(224, 104, 90, 0.4);
}

.connect-folder-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* ===== MODALE ===== */
.modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(44, 42, 38, 0.4);
  backdrop-filter: blur(4px);
  animation: fadeIn 0.2s ease;
}

.modal-content {
  position: relative;
  background: var(--glass-bg-strong);
  backdrop-filter: blur(28px) saturate(180%);
  -webkit-backdrop-filter: blur(28px) saturate(180%);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  width: 100%;
  max-width: 560px;
  max-height: 90vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  animation: slideUp 0.3s ease;
}

.modal-large { max-width: 680px; }
.modal-small { max-width: 400px; }
.modal-profile { max-width: 720px; }

.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20px 24px;
  border-bottom: 1px solid var(--color-border);
  flex-shrink: 0;
}

.modal-header h2 {
  font-family: var(--font-sans);
  font-size: 1.25rem;
  font-weight: 600;
}

.modal-close {
  background: none;
  border: none;
  font-size: 1.5rem;
  color: var(--color-text-muted);
  cursor: pointer;
  /* Patrz komentarz przy .btn w tym pliku — ten przycisk nie ma tamtej
     klasy, więc dostaje touch-action osobno. */
  touch-action: manipulation;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  transition: all var(--transition);
}

.modal-close:hover {
  background: var(--color-bg-muted);
  color: var(--color-text);
}

.confirm-modal-body {
  padding: 20px 24px 24px;
}

/* Zwykłe, osobne akapity w treści modali (np. zip-password-modal,
   zip-import-password-modal — patrz index.html) — #confirm-modal-message
   niżej to osobny, bardziej specyficzny przypadek (dynamiczna treść z
   Utils.confirm), więc ta ogólna reguła musi zostać OBOK niej, nie
   zastępować jej — inaczej te "zwykłe" akapity w innych modalach tracą
   odstęp od kolejnego elementu (np. pola z hasłem tuż pod nimi). Zgłoszone
   wprost przez użytkownika. */
.confirm-modal-body p {
  color: var(--color-text-secondary);
  font-size: 0.875rem;
  line-height: 1.6;
  margin-bottom: 20px;
}

/* #confirm-modal-message to <div> (nie <p>) — może dostać albo zwykły tekst
   (textContent, patrz Utils.confirm), albo złożony markup z akapitami i
   listą (patrz .confirm-summary-list niżej, użycie w Backup.importZip);
   <ul> jako dziecko <p> byłoby nieprawidłowym HTML-em i przeglądarka mogłaby
   je po swojemu "naprawić". Style tekstu wiszą więc na samym kontenerze
   (dziedziczą je zwykłe teksty), a odstępy między jego bezpośrednimi
   dziećmi (gdy jest ich kilka) ustawia reguła "> *" niżej. */
.confirm-modal-body #confirm-modal-message {
  color: var(--color-text-secondary);
  font-size: 0.875rem;
  line-height: 1.6;
  margin-bottom: 20px;
}

.confirm-modal-body #confirm-modal-message > * {
  margin: 0 0 12px;
}

.confirm-modal-body #confirm-modal-message > *:last-child {
  margin-bottom: 0;
}

/* Liczniki osób/połączeń/zdjęć przy potwierdzeniu importu (patrz
   Backup.importZip) — każdy na osobnej linii z ikoną, zamiast wtopiony w
   zdanie, żeby dało się je wychwycić jednym rzutem oka. Zgłoszone wprost
   przez użytkownika. */
.confirm-summary-list {
  list-style: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.confirm-summary-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--color-text);
  font-size: 0.9375rem;
  font-weight: 600;
}

.confirm-summary-icon {
  font-size: 1.125rem;
}

/* Wiersz w modalu Ustawień (patrz #settings-modal w index.html) — etykieta z
   ikoną po lewej, przycisk stanu po prawej. Jeden wiersz na razie
   (biometria), ale nazwa klasy zakłada, że kolejne ustawienia będą
   dokładane w tym samym wzorcu. */
.settings-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.settings-row-label {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--color-text);
  font-size: 0.9375rem;
}

.settings-row + .settings-row {
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--color-border-light);
}

.settings-select {
  padding: 6px 10px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-bg-elevated);
  color: var(--color-text);
  font-family: inherit;
  font-size: 0.875rem;
}

/* "O aplikacji" — osobna sekcja pod wierszami ustawień, ten sam wzorzec
   nagłówka co .form-section (wielkie litery, cienka linia pod spodem). */
.settings-section {
  margin-top: 20px;
  padding-top: 16px;
  border-top: 1px solid var(--color-border-light);
}

.settings-section h3 {
  font-size: 0.8125rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-text-secondary);
  margin-bottom: 8px;
}

.settings-section p {
  color: var(--color-text-secondary);
  font-size: 0.8125rem;
  line-height: 1.6;
}

.zip-password-value {
  font-family: monospace;
  font-size: 1rem;
  letter-spacing: 0.03em;
}

.zip-password-warning {
  color: var(--color-danger);
  font-size: 0.8125rem;
}

/* Pole hasła przy imporcie — 4 osobne segmenty (NP+, 000, 000, 000) z
   myślnikami między nimi, odzwierciedlające grupowanie hasła generowanego
   w Backup.generatePassword. Specyficzność ".password-segments
   .password-segment" musi przebić ".form-group input" (padding/szerokość),
   bo inaczej ta ogólna reguła formularzowa by je nadpisała. Tło/obramowanie/
   kolor tekstu zadeklarowane TU wprost (nie tylko dziedziczone z ".form-
   group input") — pola na ekranie blokady (patrz #app-lock-segments w
   index.html) NIE siedzą w żadnym .form-group, więc bez tego dostawałyby
   domyślny, natywny wygląd przeglądarki (białe tło, czarny tekst) zamiast
   stylu reszty aplikacji, i to WIDOCZNIE źle w ciemnym motywie. Zgłoszone
   wprost przez użytkownika. */
.password-segments {
  display: flex;
  align-items: center;
  gap: 6px;
}

.password-segments .password-segment {
  width: 48px;
  padding: 8px 4px;
  text-align: center;
  text-transform: uppercase;
  font-family: monospace;
  font-size: 0.9375rem;
  letter-spacing: 0.03em;
  background: var(--color-bg-elevated);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  transition: border-color var(--transition);
}

.password-segments .password-segment:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(61, 107, 79, 0.12);
}

.password-segment-dash {
  color: var(--color-text-muted);
  font-weight: 600;
}

.pin-error {
  margin-top: 8px;
  color: var(--color-danger);
  font-size: 0.8125rem;
}

/* Nakładka blokady hasłem — CELOWO bez .modal (patrz komentarz przy
   #app-lock-overlay w index.html: musi przetrwać globalny Escape). Tło w
   pełni nieprzezroczyste (var(--color-bg), nie półprzezroczysta czerń jak
   .modal-backdrop) — ma całkowicie zasłonić drzewo pod spodem. z-index 150:
   wyżej niż reszta aplikacji (nagłówek/toolbar to 100), ale CELOWO NIŻEJ niż
   .modal (200+) — okna typu "Wyczyść dane przeglądarki" otwierane WPROST z
   tej nakładki (patrz AppLock.wireLockScreen) mają wyskakiwać PONAD nią,
   jako osobne okno, a nie chować się pod nią. AppLock.setInert wyłącza
   `inert` na .modal tak samo jak na tej nakładce, więc taki modal zostaje
   klikalny. Zgłoszone wprost przez użytkownika. */
.pin-lock-overlay {
  position: fixed;
  inset: 0;
  z-index: 150;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-bg);
}

/* Na zainstalowanej aplikacji (PWA, display-mode:standalone) tło TEJ
   nakładki jest zawsze ciemne — NIEZALEŻNIE od jasny/ciemny przełącznika
   motywu w samej aplikacji (ten dalej rządzi kartą/logo/tekstem w środku,
   patrz .pin-lock-box niżej — logo dobiera właściwy wariant do
   :root[data-theme] tak jak zawsze, więc nadal ma dobry kontrast na
   jasnej karcie, gdyby motyw aplikacji akurat był jasny). Zgłoszone wprost
   przez użytkownika. */
@media (display-mode: standalone) {
  .pin-lock-overlay {
    background: #1c1b18;
  }
}

/* Karta w tym samym stylu co reszta "uniesionych" powierzchni aplikacji
   (border+cień+var(--color-bg-elevated), patrz np. pola formularza) —
   NIE efekt szkła jak .modal-content: ten polega na rozmyciu TREŚCI za
   sobą, a za tą kartą jest tylko płaskie, jednolite tło (.pin-lock-overlay
   musi być w pełni nieprzezroczyste, patrz komentarz przy nim), więc blur
   nie miałby czego rozmywać — wyszłaby ledwo widoczna, prawie przezroczysta
   plama zamiast wyraźnej karty. .connect-folder-content (na tym samym
   elemencie, patrz index.html) dokłada padding/text-align/typografię
   nagłówka i opisu — tu tylko wygląd samej karty i jej szerokość. */
.pin-lock-box {
  display: flex;
  flex-direction: column;
  align-items: center;
  max-width: 400px;
  width: 100%;
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}

.pin-lock-box .password-segments {
  justify-content: center;
  margin-bottom: 20px;
}

/* "Odblokuj" + odcisk palca obok siebie (patrz index.html) — ikona
   biometrii jest icon-only (btn-icon-only), więc dziedziczy kwadratowy
   kształt zamiast rozciągać się do wysokości .btn-large tekstowego
   sąsiada; align-items:center wyśrodkowuje ją mimo to w tym samym rzędzie.
   width:100% (na rzędzie) + flex:1 (na samym "Odblokuj") — rząd rozciąga
   się na całą dostępną szerokość karty, "Odblokuj" wypełnia w nim resztę
   miejsca obok wąskiej ikony, więc wychodzi TAK SAMO szeroki jak "Wyczyść
   dane przeglądarki" niżej (ta sama pełna szerokość karty). Zgłoszone
   wprost przez użytkownika. */
.app-lock-unlock-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin-top: 12px;
  width: 100%;
}

.app-lock-unlock-row #app-lock-unlock {
  flex: 1;
}

.app-lock-unlock-row .btn-icon-only {
  height: 100%;
}

.pin-lock-box .pin-error {
  margin-top: 12px;
}

.pin-lock-box #app-lock-reset {
  margin-top: 20px;
  width: 100%;
}

/* Potrząśnięcie polami hasła po nieudanej próbie odblokowania (patrz
   AppLock.attemptUnlock) — czysto wizualna informacja zwrotna, dodatkowa do
   samego komunikatu błędu. */
@keyframes pin-shake {
  10%, 90% { transform: translateX(-1px); }
  20%, 80% { transform: translateX(2px); }
  30%, 50%, 70% { transform: translateX(-4px); }
  40%, 60% { transform: translateX(4px); }
}

.pin-shake {
  animation: pin-shake 0.4s ease;
}

.zip-password-tip {
  margin-top: 12px;
  padding: 10px 12px;
  background: #fdecea;
  border: 1px solid rgba(192, 57, 43, 0.35);
  border-radius: var(--radius-sm);
  color: var(--color-text-secondary);
  font-size: 0.8125rem;
  line-height: 1.5;
}

.profile-header-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* ===== FORMULARZ ===== */
.person-form {
  overflow-y: auto;
  padding: 0 24px 24px;
}

.form-section {
  margin-top: 24px;
}

.form-section h3 {
  font-size: 0.8125rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-text-secondary);
  margin-bottom: 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--color-border-light);
}

.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}

/* Biografia/Notatki/Dodatkowe informacje — bez tego stały tuż pod ostatnim
   wierszem siatki pól (.form-grid) powyżej, praktycznie bez odstępu, zamiast
   czytelnie zaczynać nowy blok. Zgłoszone wprost przez użytkownika. */
.form-group-textblock {
  margin-top: 16px;
}

.form-group {
  display: flex;
  flex-direction: column;
  gap: 4px;
  /* Bez tego element siatki (.form-grid to grid, TO jest jego dziecko) ma
     domyślne min-width:auto, czyli nie może skurczyć się poniżej intrinsic
     szerokości swojej zawartości — a długi <select> (patrz niżej) i tak
     nadal by wypychał kolumnę poza 1fr, wymuszając poziome przewijanie
     CAŁEGO formularza. Zgłoszone wprost przez użytkownika. */
  min-width: 0;
}

.form-group label {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--color-text-secondary);
}

.required { color: var(--color-danger); }

.form-group input,
.form-group select,
.form-group textarea {
  padding: 8px 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  font-family: inherit;
  font-size: 0.875rem;
  background: var(--color-bg-elevated);
  color: var(--color-text);
  transition: border-color var(--transition);
  /* Bez tego <select> mierzy się wg NAJDŁUŻSZEJ opcji (np. długie nazwy
     stopni/tytułów, patrz militaryRank/officialTitle) zamiast wg dostępnej
     szerokości kolumny — właśnie to wypychało formularz w poziomą
     przewijalność. */
  width: 100%;
  min-width: 0;
}

/* Jawna wysokość dla pól JEDNOWIERSZOWYCH (nie .form-group textarea — ta ma
   własną, wielowierszową wysokość przez min-height niżej). Powód: Safari na
   iPadOS/iOS liczy wewnętrzną wysokość <input type="date"> inaczej niż dla
   zwykłego tekstu, mimo IDENTYCZNEGO paddingu/font-size (patrz reguła
   wyżej) — samo pole (ten prostokąt, który trzeba dotknąć, żeby otworzyć
   kalendarz — NIE sam kalendarz, ten to już natywny popup systemu, którego
   CSS w ogóle nie dotyczy) wychodziło zauważalnie większe niż sąsiednie
   pola w tym samym wierszu siatki. Sama zmiana line-height (poprzednia
   próba) NIE wystarczyła — potwierdzone wprost przez użytkownika na
   realnym iPadzie: Safari nadal liczył inną wysokość dla segmentowego
   kontrolka daty niezależnie od line-height. Jawna wysokość w pikselach
   (box-sizing: border-box z resetu na górze pliku, więc wlicza
   padding/border) wymusza IDENTYCZNY rozmiar wszystkich pól, niezależnie od
   typu i wewnętrznego renderowania danej przeglądarki — mocniejsze
   ograniczenie niż sam line-height. 40px = ta sama wysokość co
   --header-btn-height (przyciski nagłówka), więc spójna skala w całej
   aplikacji. Zgłoszone wprost przez użytkownika.
   :not([type="radio"]):not([type="checkbox"]) — inaczej ta sama reguła
   nadałaby 40x40px też przyciskom radio (patrz "Żyje"/"Zmarła" niżej w
   formularzu, .radio-label input[type="radio"] MA wprawdzie wyższą
   specyficzność i i tak by wygrała, ale bez tego wykluczenia polegalibyśmy
   na tym przypadkiem, nie świadomie). */
.form-group input:not([type="radio"]):not([type="checkbox"]),
.form-group select {
  height: 40px;
}

/* height:40px wyżej OKAZAŁO SIĘ NIEWYSTARCZAJĄCE dla <input type="date"> na
   iPadOS — potwierdzone zrzutem ekranu wprost od użytkownika: pole nadal
   wychodziło wyraźnie wyższe (i szersze/"bryłowate") niż sąsiednie pola text.
   Powód: przy domyślnym -webkit-appearance (auto) Safari renderuje datę
   WŁASNYM, natywnym segmentowym kontrolkiem (dzień/miesiąc/rok jako osobne
   "kołowrotki") o WŁASNEJ wymuszonej minimalnej wysokości wewnętrznej,
   której zwykłe CSS `height` na zewnętrznym boksie nie potrafi obciąć —
   inaczej niż w przypadku zwykłego <input type="text">, gdzie treścią jest
   zwykły tekst bez takiego wewnętrznego layoutu. appearance:none wyłącza tę
   natywną, "bryłowatą" prezentację na rzecz zwykłego, stylowalnego pola
   tekstowego (JAK input[type=text]) — dotknięcie w dowolne miejsce pola
   dalej otwiera natywny kalendarz Safari (samo pole zostaje zwykłym,
   klikalnym/fokusowalnym <input>, appearance:none zmienia TYLKO jego
   zamknięty wygląd, nie zdejmuje mu funkcji). Jedyny znany kompromis: może
   zniknąć domyślna ikonka kalendarza po prawej stronie pola (rysowana przez
   tę samą natywną prezentację, którą właśnie wyłączamy) — do sprawdzenia na
   żywym iPadzie. Zgłoszone wprost przez użytkownika. */
.form-group input[type="date"] {
  -webkit-appearance: none;
  appearance: none;
}

.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(61, 107, 79, 0.12);
}

.form-group input.error,
.form-group select.error {
  border-color: var(--color-danger);
}

.form-group textarea {
  resize: vertical;
  min-height: 80px;
}

.radio-group {
  display: flex;
  gap: 24px;
}

.radio-label {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  font-size: 0.875rem;
}

.radio-label input[type="radio"] {
  accent-color: var(--color-primary);
  width: 16px;
  height: 16px;
}

.form-errors {
  margin-top: 16px;
  padding: 12px;
  background: #fdf0ef;
  border: 1px solid #f5c6cb;
  border-radius: var(--radius-sm);
  color: var(--color-danger);
  font-size: 0.8125rem;
}

.form-errors ul {
  list-style: none;
  padding: 0;
}

.form-errors li + li { margin-top: 4px; }

.form-actions {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
  margin-top: 24px;
  padding-top: 16px;
  border-top: 1px solid var(--color-border-light);
}

/* ===== SCALANIE ARCHIWÓW: EKRAN ROZBIEŻNOŚCI =====
   Patrz Backup.renderMergeConflictsModal (js/backup.js) — treść budowana w
   JS, te reguły tylko ją stylują. Ten sam wzorzec przewijanego wnętrza
   modala co .person-form/.profile-body (modal-content ma overflow:hidden +
   flex-column, więc to ten element faktycznie przewija długą listę, a
   nagłówek modala zostaje na miejscu). */
.merge-conflicts-body {
  overflow-y: auto;
  padding: 20px 24px 24px;
}

.merge-conflicts-intro {
  color: var(--color-text-secondary);
  font-size: 0.875rem;
  line-height: 1.6;
  margin-bottom: 16px;
}

.merge-conflicts-bulk {
  display: flex;
  gap: 12px;
  margin-bottom: 20px;
}

.merge-conflict-person {
  padding: 14px 16px;
  margin-bottom: 14px;
  background: var(--color-bg-muted);
  border: 1px solid var(--color-border-light);
  border-radius: var(--radius-md);
}

/* Wyróżnia karty prawdopodobnych duplikatów (patrz Backup.mergeZip/
   personMatchKey) — mają dodatkowo pytanie "czy to ta sama osoba?" i to od
   niego zależy, czy reszta karty (pola konfliktu) w ogóle się liczy, więc
   dostają wyraźniejszą, bardziej "aktywną" obwódkę niż zwykłe konflikty pól
   dopasowań po ID. */
.merge-conflict-person-probable {
  border-color: var(--color-primary);
  border-width: 1.5px;
}

.merge-conflict-person h3 {
  font-size: 0.9375rem;
  font-weight: 700;
  margin-bottom: 10px;
}

.merge-conflict-duplicate-question {
  padding-bottom: 14px;
  margin-bottom: 14px;
  border-bottom: 1px solid var(--color-border-light);
}

.merge-conflict-duplicate-question .merge-conflict-option {
  display: inline-flex;
  flex-direction: row;
  width: auto;
  margin-right: 10px;
}

.merge-conflicts-no-fields {
  color: var(--color-text-secondary);
  font-size: 0.8125rem;
  font-style: italic;
}

/* Gdy duplikat odrzucony ("nie, to dwie różne osoby") — patrz
   updateDuplicateCardState w Backup.renderMergeConflictsModal — wybór pól
   poniżej i tak jest ignorowany przy scalaniu, wyszarzenie tylko czyni to
   jasnym na pierwszy rzut oka. */
.merge-conflict-fields-disabled {
  opacity: 0.4;
  pointer-events: none;
}

.merge-conflict-field + .merge-conflict-field {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--color-border-light);
}

.merge-conflict-label {
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-secondary);
  margin-bottom: 8px;
}

/* grid (nie flex) — dwie opcje mają zawsze równą szerokość obok siebie,
   niezależnie od długości tekstu wewnątrz każdej z nich. */
.merge-conflict-field {
  display: grid;
  grid-template-columns: 1fr;
  gap: 8px;
}

.merge-conflict-option {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: all var(--transition);
}

.merge-conflict-option:hover {
  background: var(--color-bg-elevated);
}

/* :has — wspierane we wszystkich przeglądarkach, na których działa reszta
   aplikacji (backdrop-filter itp. i tak już tego wymaga) — podświetla całą
   etykietę-kartę wybranej opcji, nie tylko sam radiobutton w jej środku. */
.merge-conflict-option:has(input:checked) {
  border-color: var(--color-primary);
  background: var(--color-primary-light);
}

.merge-conflict-option input[type="radio"] {
  margin-top: 3px;
  flex-shrink: 0;
}

.merge-conflict-option-source {
  flex-shrink: 0;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--color-text-secondary);
  width: 92px;
}

.merge-conflict-option-value {
  font-size: 0.875rem;
  color: var(--color-text);
  word-break: break-word;
}

@media (min-width: 560px) {
  .merge-conflict-field { grid-template-columns: 1fr 1fr; }
  .merge-conflict-option { flex-direction: column; }
  .merge-conflict-option-source { width: auto; }
}

/* ===== ZDJĘCIE W FORMULARZU ===== */
.photo-upload {
  display: flex;
  align-items: flex-start;
  gap: 20px;
}

.photo-preview {
  position: relative;
  width: 120px;
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-md);
  overflow: hidden;
  border: 2px dashed var(--color-border);
  flex-shrink: 0;
}

.photo-preview img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.photo-placeholder {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: var(--color-bg-muted);
  color: var(--color-text-muted);
  font-size: 0.75rem;
}

.photo-placeholder span { font-size: 2rem; margin-bottom: 4px; }

.photo-actions {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-top: 8px;
}

/* ===== KADROWANIE ZDJĘCIA ===== */
.photo-crop-body {
  padding: 20px 24px 24px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

.photo-crop-viewport {
  position: relative;
  width: 260px;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  border-radius: var(--radius-md);
  background: var(--color-bg-muted);
  box-shadow: var(--shadow-md);
  cursor: grab;
  touch-action: none;
  user-select: none;
}

.photo-crop-viewport:active {
  cursor: grabbing;
}

.photo-crop-viewport img {
  position: absolute;
  top: 50%;
  left: 50%;
  max-width: none;
  pointer-events: none;
}

.photo-crop-hint {
  font-size: 0.8rem;
  color: var(--color-text-muted);
  text-align: center;
  margin: 0;
}

.photo-crop-zoom-row {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
}

.photo-crop-zoom-icon {
  font-size: 0.9rem;
  opacity: 0.7;
}

/* Suwak przybliżenia — natywny wygląd zastąpiony własnym, żeby uchwyt był
   zawsze wyraźnie widoczny (grubsza kropka niż tor) i spójny w obu motywach
   zamiast dziedziczyć wygląd systemowy przeglądarki (w trybie ciemnym bywał
   ledwo widoczny). Szary uchwyt = przybliżenie nie jest jeszcze zastosowane
   (100%, patrz klasa .is-zoomed nadawana w PhotoCrop przy wartości > 100%),
   zielony = aktywne przybliżenie. */
.photo-crop-zoom-row input[type="range"] {
  flex: 1;
  -webkit-appearance: none;
  appearance: none;
  height: 20px;
  background: transparent;
  cursor: pointer;
}

.photo-crop-zoom-row input[type="range"]:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
  border-radius: 9px;
}

.photo-crop-zoom-row input[type="range"]::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: 2px;
  background: var(--color-border);
}

.photo-crop-zoom-row input[type="range"]::-moz-range-track {
  height: 4px;
  border-radius: 2px;
  background: var(--color-border);
}

.photo-crop-zoom-row input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 18px;
  height: 18px;
  margin-top: -7px;
  border-radius: 50%;
  background: var(--color-text-muted);
  border: 2px solid var(--color-bg-elevated);
  box-shadow: var(--shadow-sm);
  cursor: grab;
  transition: background var(--transition);
}

.photo-crop-zoom-row input[type="range"]::-moz-range-thumb {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--color-text-muted);
  border: 2px solid var(--color-bg-elevated);
  box-shadow: var(--shadow-sm);
  cursor: grab;
  transition: background var(--transition);
}

.photo-crop-zoom-row input[type="range"]:active::-webkit-slider-thumb,
.photo-crop-zoom-row input[type="range"]:active::-moz-range-thumb {
  cursor: grabbing;
}

.photo-crop-zoom-row input[type="range"].is-zoomed::-webkit-slider-thumb {
  background: var(--color-primary);
}

.photo-crop-zoom-row input[type="range"].is-zoomed::-moz-range-thumb {
  background: var(--color-primary);
}

.photo-crop-body .form-actions {
  width: 100%;
}

/* ===== PROFIL ===== */
.profile-body {
  overflow-y: auto;
  padding: 24px;
}

.profile-layout {
  display: grid;
  grid-template-columns: 300px 1fr;
  gap: 32px;
}

.profile-photo-frame {
  position: relative;
  width: 300px;
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-md);
  overflow: hidden;
  box-shadow: var(--shadow-md);
  flex-shrink: 0;
}

.profile-photo-large {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.profile-photo-placeholder-large {
  width: 300px;
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-md);
  background: linear-gradient(135deg, var(--color-bg-muted), var(--color-border-light));
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: var(--color-text-muted);
  box-shadow: var(--shadow-sm);
}

.profile-photo-placeholder-large span {
  font-size: 3rem;
  margin-bottom: 8px;
  opacity: 0.5;
}

.profile-section {
  margin-bottom: 24px;
}

.profile-family-section {
  margin-top: 40px;
}

.profile-section h3 {
  font-size: 0.8125rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-primary);
  margin-bottom: 12px;
}

/* Nagłówki "Dane podstawowe" i "Rodzina" wyróżnione na tle zwykłych tytułów
   sekcji wyżej (Informacje dodatkowe, Biografia, Notatki...) — większa
   czcionka, WIELKIE litery (jak reszta, tylko bardziej wyraziste przy takim
   rozmiarze). Selektor złożony (.profile-section.profile-section-title-lg
   h3, nie sam .profile-section-title-lg h3) celowo dla wyższej
   specyficzności: przy takiej samej jak ".profile-section h3" o wygranej
   decydowałaby tylko kolejność w arkuszu, a to łatwo przypadkiem odwrócić
   przy kolejnej edycji. */
.profile-section.profile-section-title-lg h3 {
  font-size: 1.375rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.profile-field {
  display: flex;
  gap: 8px;
  margin-bottom: 8px;
  font-size: 0.875rem;
}

.profile-field-label {
  color: var(--color-text-secondary);
  min-width: 140px;
  flex-shrink: 0;
}

.profile-field-value {
  color: var(--color-text);
}

.profile-text-block {
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--color-text);
  white-space: pre-wrap;
}

.profile-family-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 24px;
}

.profile-family-group {
  /* Kafelki (spany .profile-family-link) w tej samej grupie stają się
     bezpośrednimi dziećmi flexa — bez tego, jako zwykłe blockowe <span>,
     stykałyby się ze sobą bez żadnego odstępu. */
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.profile-family-group h4 {
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
  margin-bottom: 2px;
}

.profile-family-link {
  display: block;
  padding: 8px 12px;
  background: var(--glass-bg);
  backdrop-filter: blur(10px) saturate(150%);
  -webkit-backdrop-filter: blur(10px) saturate(150%);
  border-radius: var(--radius-sm);
  font-size: 0.8125rem;
  color: var(--color-primary);
  cursor: pointer;
  transition: all var(--transition);
  border: 1px solid transparent;
}

.profile-family-link:hover {
  background: var(--color-primary-light);
  border-color: var(--color-primary);
}

.profile-empty {
  color: var(--color-text-muted);
  font-size: 0.8125rem;
  font-style: italic;
}

/* ===== POŁĄCZENIA MENU ===== */
.connections-menu {
  padding: 16px 24px 24px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.connection-option {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 16px;
  background: var(--glass-bg);
  backdrop-filter: blur(10px) saturate(150%);
  -webkit-backdrop-filter: blur(10px) saturate(150%);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: all var(--transition);
  text-align: left;
  font-family: inherit;
}

.connection-option:hover {
  border-color: var(--color-primary);
  background: var(--color-primary-light);
}

.connection-icon { font-size: 1.5rem; }

.connection-label {
  font-weight: 600;
  font-size: 0.9375rem;
  color: var(--color-text);
  display: block;
}

.connection-desc {
  font-size: 0.8125rem;
  color: var(--color-text-secondary);
  display: block;
  margin-top: 2px;
}

/* ===== PASEK WYBORU NA OSI (tworzenie połączenia) ===== */
.selection-bar {
  position: fixed;
  top: calc(var(--header-height) + 16px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 10px 16px 10px 20px;
  background: var(--glass-bg-strong);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  max-width: calc(100vw - 48px);
}

.selection-bar-info {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.selection-bar-title {
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.selection-bar-instruction {
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--color-text);
  white-space: nowrap;
}

.selection-bar-steps {
  display: flex;
  gap: 6px;
}

.wizard-step-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-border);
  transition: background var(--transition);
}

.wizard-step-dot.active { background: var(--color-primary); }
.wizard-step-dot.completed { background: var(--color-primary); opacity: 0.5; }

.person-card.selectable {
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(61, 107, 79, 0.3), var(--shadow-md);
  animation: selectable-pulse 1.6s ease-in-out infinite;
  z-index: 10;
}

.person-card.not-selectable {
  opacity: 0.25;
  filter: grayscale(0.5);
  pointer-events: none;
}

@keyframes selectable-pulse {
  0%, 100% { box-shadow: 0 0 0 3px rgba(61, 107, 79, 0.3), var(--shadow-md); }
  50% { box-shadow: 0 0 0 6px rgba(61, 107, 79, 0.12), var(--shadow-md); }
}

/* ===== TRYB USUWANIA ELEMENTÓW ===== */
.delete-mode-bar {
  position: fixed;
  /* Ta sama pozycja co komunikaty (.toast-container) — POD przyciskiem
     "Dane", nie na nim, żeby wszystkie tego typu komunikaty (toasty, ten
     pasek, pasek trybu przesuwania) pojawiały się w jednym, spójnym miejscu.
     z-index musi przebić .corner-btn (100). Zgłoszone wprost przez
     użytkownika. */
  top: calc(20px + var(--header-btn-height) + 20px);
  left: 24px;
  z-index: 105;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 20px;
  background: rgba(253, 236, 234, 0.45);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border: 1px solid rgba(192, 57, 43, 0.35);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  /* Czysto informacyjny pasek (sam tekst/ikona, bez niczego klikalnego w
     środku) — na wąskim telefonie max-width niżej potrafi sięgnąć niemal
     całej szerokości ekranu i wizualnie zachodzić na przycisk WYŁĄCZAJĄCY
     ten właśnie tryb (#btn-delete-elements, w .header-actions po prawej —
     ten SAM pasek zasłaniał więc sam siebie, blokując jedyne wyjście z
     trybu usuwania. pointer-events:none przepuszcza dotyk/kliknięcie na to,
     co jest POD spodem, niezależnie od wizualnego nachodzenia. Zgłoszone
     wprost przez użytkownika. */
  pointer-events: none;
  max-width: calc(100vw - 48px);
}

/* ===== TRYB PRZESUWANIA ELEMENTÓW ===== */
.move-mode-bar {
  position: fixed;
  /* Analogicznie do .delete-mode-bar wyżej — ta sama pozycja co komunikaty
     (.toast-container), POD przyciskiem "Dane". */
  top: calc(20px + var(--header-btn-height) + 20px);
  left: 24px;
  z-index: 105;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 20px;
  background: rgba(251, 243, 222, 0.45);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border: 1px solid rgba(196, 160, 53, 0.4);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  /* Patrz identyczny komentarz przy .delete-mode-bar wyżej — ten sam problem
     (zasłanianie przycisku #btn-move-mode na wąskim ekranie) i to samo
     rozwiązanie. */
  pointer-events: none;
  max-width: calc(100vw - 48px);
}

.move-mode-bar .delete-mode-title {
  color: #8b6914;
}

/* Delikatna czerwona poświata na całym tle strony, sygnalizująca aktywny
   tryb usuwania — nieklikalna (pointer-events: none), więc nie przeszkadza
   w trafianiu w karty ani linie pod spodem. Poniżej nagłówka (który ma
   przezroczyste tło, więc poświata i tak przez niego prześwituje) i poniżej
   modali/toastów. */
.delete-mode-tint {
  position: fixed;
  inset: 0;
  z-index: 90;
  pointer-events: none;
  background: rgba(192, 57, 43, 0.05);
}

/* Ten sam pomysł co .delete-mode-tint, tylko żółto — sygnalizuje aktywny
   tryb przesuwania elementów. */
.move-mode-tint {
  position: fixed;
  inset: 0;
  z-index: 90;
  pointer-events: none;
  background: rgba(196, 160, 53, 0.06);
}

.delete-mode-icon {
  font-size: 1.25rem;
  flex-shrink: 0;
}

.delete-mode-text {
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.delete-mode-title {
  font-size: 0.6875rem;
  font-weight: 700;
  color: var(--color-danger);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.delete-mode-desc {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--color-text);
}

/* Niewidoczny, szerszy obszar wzdłuż linii — ułatwia trafienie myszką w
   cienką linię połączenia. Najechanie działa zawsze (podświetlenie); samo
   kliknięcie (usunięcie) pozostaje aktywne tylko w trybie usuwania — stąd
   kursor "pointer" tylko wtedy, żeby nie sugerować klikalności poza nim. */
.connection-hit {
  fill: none;
  stroke: transparent;
  stroke-width: 14;
  pointer-events: stroke;
}

.connections-layer.delete-mode .connection-hit {
  cursor: pointer;
}

.connection-line.delete-target {
  stroke: var(--color-danger);
  stroke-width: calc(4.5px / var(--zoom-scale, 1));
  stroke-opacity: 1;
}

/* ===== TOAST ===== */
.toast-container {
  position: fixed;
  /* POD przyciskiem "Dane" (.corner-top-left: top:20px, wysokość
     --header-btn-height), z tym samym 20px odstępem od dołu przycisku, jaki
     przycisk ma od góry ekranu — nie na tej samej wysokości co on (tak było
     wcześniej, patrz historia niżej), tylko wyraźnie pod nim, żeby komunikat
     nie nachodził na przycisk/rozwijane z niego menu. Zgłoszone wprost przez
     użytkownika. */
  top: calc(20px + var(--header-btn-height) + 20px);
  left: 24px;
  z-index: 300;
  display: flex;
  flex-direction: column;
  gap: 8px;
  /* Ta sama strefa co rozwinięte menu ".dropdown-menu" pod przyciskiem
     "Dane" (oba pod tym samym przyciskiem, patrz komentarz wyżej) — bez
     tego wysoki z-index (300, ponad menu ze 110) sprawiał, że najechanie na
     toast fizycznie PRZEJMOWAŁO hover z ".dropdown:hover", więc CSS-owe
     menu rozwijane samym :hover znikało, gdy kursor mijał komunikat po
     drodze do jego pozycji. Toasty są czysto informacyjne (sam tekst, znikają
     same po 3s, patrz Utils.showToast) — nie mają niczego klikalnego do
     stracenia, więc przepuszczenie wskaźnika na to, co jest pod spodem, jest
     bezpieczne. Ten sam mechanizm co .delete-mode-bar/.move-mode-bar niżej.
     Zgłoszone wprost przez użytkownika. */
  pointer-events: none;
}

.toast {
  padding: 12px 20px;
  /* Fallback PRZED color-mix(), patrz komentarz przy .btn-primary. */
  background: var(--color-text);
  background: color-mix(in srgb, var(--color-text) 65%, transparent);
  backdrop-filter: blur(16px) saturate(160%);
  -webkit-backdrop-filter: blur(16px) saturate(160%);
  border: 1px solid var(--glass-highlight);
  color: white;
  border-radius: var(--radius-md);
  font-size: 0.875rem;
  box-shadow: var(--shadow-lg);
  animation: slideInLeft 0.3s ease;
  max-width: 360px;
}

.toast.success {
  background: var(--color-primary);
  background: color-mix(in srgb, var(--color-primary) 65%, transparent);
}
.toast.error {
  background: var(--color-danger);
  background: color-mix(in srgb, var(--color-danger) 65%, transparent);
}

/* ===== NAKŁADKA WCZYTYWANIA =====
   Patrz Utils.showLoading/hideLoading (js/utils.js) — pokazywana na czas
   odczytu/zapisu archiwum ZIP (Backup.importZip/mergeZip), operacji zbyt
   wolnych (kilkaset ms do kilku sekund przy większym drzewie/wielu
   zdjęciach), żeby zostawić użytkownika bez ŻADNEJ informacji, że coś się
   dzieje. z-index WYŻEJ niż modale (200) i toasty (300) — pojawia się
   samodzielnie (nigdy jednocześnie z modalem/oknem hasła, patrz
   Backup.importZip/mergeZip: nakładka chowana tuż PRZED każdym pytaniem do
   użytkownika), ale gdyby się kiedyś nałożyła, ma wygrać — w końcu to ona
   mówi "cała reszta interfejsu jest teraz zablokowana". */
.loading-overlay {
  position: fixed;
  inset: 0;
  z-index: 400;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  background: rgba(44, 42, 38, 0.45);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

.loading-spinner {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 3px solid rgba(255, 255, 255, 0.3);
  border-top-color: #fff;
  animation: loading-spin 0.8s linear infinite;
}

@keyframes loading-spin {
  to { transform: rotate(360deg); }
}

.loading-message {
  color: #fff;
  font-size: 0.9375rem;
  font-weight: 500;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}

/* ===== BANER INSTALACJI PWA ===== */
/* Zachęta do dodania aplikacji do ekranu głównego — pokazywana wyłącznie na
   urządzeniach mobilnych (patrz js/pwa.js), więc bez dodatkowych reguł
   @media chowających ją na desktopie. */
.pwa-install-banner {
  position: fixed;
  left: 12px;
  right: 12px;
  bottom: 12px;
  z-index: 150;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 16px;
  background: var(--color-text);
  background: color-mix(in srgb, var(--color-text) 70%, transparent);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border: 1px solid var(--glass-highlight);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  color: white;
  animation: slideUp 0.3s ease;
}

.pwa-install-banner.hidden {
  display: none;
}

.pwa-install-icon {
  font-size: 1.5rem;
  flex-shrink: 0;
}

.pwa-install-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
}

.pwa-install-title {
  font-size: 0.875rem;
  font-weight: 700;
}

.pwa-install-desc {
  font-size: 0.75rem;
  opacity: 0.85;
  line-height: 1.4;
}

.pwa-install-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}

.pwa-install-close {
  background: none;
  border: none;
  color: white;
  opacity: 0.75;
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
  padding: 4px;
}

.pwa-install-close:hover {
  opacity: 1;
}

@media (max-width: 480px) {
  .pwa-install-banner {
    flex-wrap: wrap;
  }
  .pwa-install-actions {
    width: 100%;
    justify-content: flex-end;
  }
}

/* ===== ANIMACJE ===== */
@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes slideUp {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: translateY(0); }
}

@keyframes slideInRight {
  from { opacity: 0; transform: translateX(40px); }
  to { opacity: 1; transform: translateX(0); }
}

@keyframes slideInLeft {
  from { opacity: 0; transform: translateX(-40px); }
  to { opacity: 1; transform: translateX(0); }
}

/* ===== RESPONSYWNOŚĆ ===== */
@media (max-width: 768px) {
  :root {
    /* 3:4 dokładnie, tak jak na desktopie (patrz --card-width/--card-height
       w :root głównym) — tylko mniejsze, żeby zmieściło się na wąskim
       ekranie. */
    --card-width: 150px;
    --card-height: 200px;
  }

  .app-header { padding: 0 16px; }
  /* Te trzy są zakotwiczone w rogach ekranu NIEZALEŻNIE od paddingu
     .app-header (position:fixed, nie flex-child) — bez tego dopasowania
     zostałyby przy stałych 24px, podczas gdy "Dodaj członka rodziny" (w
     .header-actions, odległość = sam padding nagłówka) zbliżyłby się do
     16px, psując wyrównanie względem krawędzi ekranu. */
  .corner-top-left,
  .corner-bottom-left {
    left: 16px;
  }
  .side-toolbar,
  .corner-bottom-right {
    right: 16px;
  }
  .btn-label { display: none; }

  /* .btn-icon:has(+ .btn-label) (wyżej w pliku) przesuwa ikonę o 2px w górę,
     żeby wyrównać emoji względem TEKSTU etykiety obok — ale :has() sprawdza
     tylko obecność .btn-label w DOM, nie jego widoczność. Tekst znika tu
     wyżej (display:none), element jednak zostaje w DOM, więc to przesunięcie
     nadal by się stosowało mimo braku etykiety, którą miało kompensować —
     ikona wizualnie "uciekała" za wysoko (Dane/Usuń elementy/Dodaj
     połączenie — każdy z emoji + .btn-label obok). Cofamy je z powrotem tu,
     w tym samym breakpoincie, w którym etykieta faktycznie znika. Zgłoszone
     wprost przez użytkownika. */
  .btn-icon:has(+ .btn-label):not(.btn-icon-glyph) {
    transform: none;
  }

  /* .delete-mode-bar/.move-mode-bar mają max-width liczony jako "prawie cała
     szerokość ekranu" (patrz reguły bazowe niżej w pliku), co na telefonie
     wystarcza, by pasek fizycznie sięgał pod .side-toolbar (kolumna przycisków
     wyśrodkowana pionowo po prawej, m.in. przycisk ołówka #btn-move-mode) —
     a że opis w pasku jest długi i na wąskim ekranie zawija się na wiele linii,
     pasek robi się na tyle wysoki, że na typowym telefonie w pionie realnie
     zasłania ten przycisk. Rezerwujemy więc po prawej miejsce dokładnie na
     szerokość kolumny .side-toolbar (16px odstęp od krawędzi + szerokość
     przycisku, ta sama co --header-btn-height) plus mały margines — tak pasek
     nigdy nie wchodzi w tę kolumnę, niezależnie od wysokości ekranu czy
     długości tekstu. Zgłoszone wprost przez użytkownika. */
  .delete-mode-bar,
  .move-mode-bar {
    max-width: calc(100vw - 24px - 16px - var(--header-btn-height) - 12px);
  }

  .form-grid { grid-template-columns: 1fr; }
  .profile-layout { grid-template-columns: 1fr; }
  .profile-photo-frame,
  .profile-photo-placeholder-large {
    width: 100%;
    max-width: 300px;
    margin: 0 auto;
  }

  .modal { padding: 12px; }
  .modal-content { max-height: 95vh; }
}

@media (max-width: 480px) {
  :root {
    /* Kolejny stopień pomniejszenia karty (dalej dokładnie 3:4) na wąskich
       telefonach — POPRZEDNIA reguła w tym miejscu ustawiała
       .person-card { width: 100% }, co nadpisywało stałą szerokość z
       --card-width pikselami "100% szerokości świata" (.timeline-world,
       rozciągniętego na CAŁE, przewijane drzewo, nie samego viewportu) —
       Timeline.render() i tak liczy pozycje kart w pikselach na podstawie
       --card-width (patrz getComputedStyle w timeline.js), więc taka karta
       urosłaby do absurdalnych rozmiarów i nachodziła na resztę drzewa.
       Zamiast tego po prostu dalej zmniejszamy tę samą zmienną. */
    --card-width: 120px;
    --card-height: 160px;
  }

  .photo-crop-viewport {
    /* 260px z założenia mieści się w modal-small (max-width:400px) nawet po
       odjęciu paddingu, ale na NAJWĘŻSZYCH telefonach (~320px logicznej
       szerokości) modal ma jeszcze węższy margines (.modal padding:12px
       ustawiony wyżej w tym pliku) — min() pozwala kadrowi skurczyć się
       zamiast wystawać poza tło modala. */
    width: min(260px, 100%);
  }
}

/* ===== TRYB CIEMNY: nadpisania kolorów wpisanych na sztywno (nie przez zmienne) ===== */
/* Przywraca niższe krycie z przed poprawki jasnego motywu (patrz
   .btn-primary/.btn-primary:hover wyżej w pliku) — na ciemnym tle ten
   efekt szkła dobrze wygląda, więc tu zostaje bez zmian. */
:root[data-theme="dark"] .btn-primary {
  background: color-mix(in srgb, var(--color-primary) 58%, transparent);
}

:root[data-theme="dark"] .btn-primary:hover {
  background: color-mix(in srgb, var(--color-primary-hover) 65%, transparent);
}

:root[data-theme="dark"] .btn-secondary.active {
  background: rgba(74, 43, 40, 0.4);
  border-color: var(--color-danger);
  color: var(--color-danger);
}

:root[data-theme="dark"] .btn-secondary.active:hover {
  background: rgba(90, 51, 48, 0.55);
}

:root[data-theme="dark"] .btn-secondary.active-move {
  background: rgba(58, 49, 32, 0.4);
  border-color: #d4b24a;
  color: #d4b24a;
}

:root[data-theme="dark"] .btn-secondary.active-move:hover {
  background: rgba(69, 58, 38, 0.55);
}

:root[data-theme="dark"] .form-errors {
  background: #3a2422;
  border-color: #6b3a35;
}

:root[data-theme="dark"] .delete-mode-bar {
  background: rgba(58, 36, 34, 0.45);
  border-color: rgba(224, 104, 90, 0.4);
}

:root[data-theme="dark"] .person-card.delete-target {
  background: rgba(74, 43, 40, 0.35);
}

:root[data-theme="dark"] .zip-password-tip {
  background: rgba(74, 43, 40, 0.35);
  border-color: rgba(224, 104, 90, 0.4);
}

:root[data-theme="dark"] .move-mode-bar {
  background: rgba(58, 49, 32, 0.45);
  border-color: rgba(212, 178, 74, 0.4);
}

:root[data-theme="dark"] .move-mode-bar .delete-mode-title {
  color: #d4b24a;
}
