/* --acc-soft = akcent na tle: „to jest z rodziny akcji", ale bez wagi wypełnionego guzika.
   Był wklepany trzykrotnie jako #eaf2f0; teraz jedna nazwa, bo doszedł czwarty użytkownik. */
:root { --fg:#111; --bg:#fff; --mut:#666; --line:#e5e5e5; --acc:#0f5f57; --acc-mid:#16786c;
        --acc-soft:#eaf2f0; --due:#b4451f; }
/* Skala odstępów — JEDNO źródło dla margin/padding/gap. Powstała z policzenia wartości, które
   już były w pliku: sześć różnych odstępów rozsianych po 90 regułach znaczyło, że każdy
   komponent zgadywał własny. Nowy komponent wybiera z listy, nie wymyśla liczby. */
:root { --s1:2px; --s2:4px; --s3:6px; --s4:8px; --s5:10px; --s6:12px; --s7:16px; --s8:20px; --s9:24px; }
* { box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
html, body { height:100%; }
/* overflow:hidden = powłoka NIGDY się nie przewija; przewija się wyłącznie <main>. Bez tego
   wystarczy kilka pikseli nadmiaru, żeby cała strona dostała scroll i zjechała ze stopką —
   a w stopce są wszystkie akcje. Stopka ma być nieusuwalna, nie „zwykle widoczna".
   100dvh (nie 100vh): przy otwartej klawiaturze viewport się kurczy i stopka siada nad nią. */
body { margin:0; font:16px/1.5 system-ui,sans-serif; color:var(--fg); background:var(--bg);
       display:flex; flex-direction:column; height:100dvh; overflow:hidden; }
/* top brand bar */
header { flex:0 0 auto; display:flex; align-items:center; justify-content:space-between; gap:var(--s2);
         padding:calc(var(--s5) + env(safe-area-inset-top)) var(--s6) var(--s5); background:var(--acc); color:#fff; }
/* brand rozpycha się, więc akcje siadają razem SKRAJNIE z prawej zamiast rozjechać się
   przez space-between; min-width:0 pozwala nazwie się przyciąć, a nie wypchnąć guziki. */
header .brand { flex:1 1 auto; min-width:0; font-size:15px; font-weight:600; letter-spacing:-.01em;
                display:flex; align-items:center; gap:var(--s4); }
header .brand span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
header .brand svg { flex:0 0 auto; width:22px; height:22px; }
header button { width:auto; padding:var(--s3) var(--s4); font-size:14px; background:none; color:rgba(255,255,255,.85); border:0; }
/* email zalogowanego = wejście do profilu; przycina się, nie wypycha ikon */
header .huser { max-width:45vw; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
header #install { flex:0 0 auto; display:flex; align-items:center; padding:var(--s3) var(--s2); }
header #install svg { width:21px; height:21px; }
/* [hidden] przegrywa z display:flex — musi być jawnie */
header #install[hidden] { display:none; }
/* scrolling content */
main { flex:1 1 auto; overflow-y:auto; padding:var(--s7); max-width:520px; width:100%; margin-inline:auto; -webkit-overflow-scrolling:touch; }
/* background-upload strip — lives in the shell, NOT in main, so it survives every screen swap:
   a clip uploading in the background must stay visible after the trainer navigates away. */
#upbar { flex:0 0 auto; max-width:520px; width:100%; margin-inline:auto; padding:0 var(--s7); }
#upbar:empty { display:none; }
#upbar .up { display:flex; flex-wrap:wrap; align-items:center; gap:var(--s4) var(--s5); padding:var(--s4) var(--s5); margin-bottom:var(--s3);
             border:1px solid var(--line); border-radius:10px; font-size:13px; background:var(--bg); }
/* Sloty wiersza: jedna reguła dla wszystkich, wyjątek tylko dla tego, który rośnie.
   Reguła per-slot znaczy, że przy dodaniu kolejnego trzeba pamiętać o kolejnym wpisie. */
#upbar .up > * { flex:0 0 auto; display:flex; align-items:center; gap:var(--s4); }
#upbar .up-name { flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
#upbar .up > :empty { display:none; }
#upbar .up-state { color:var(--mut); }
/* Wysyłka padła: nazwa klipu dostaje CAŁĄ linię — komunikat plus dwa guziki ściskały ją przy
   375 px do jednego znaku i nie było wiadomo, KTÓRE nagranie nie poszło. */
#upbar .up.err .up-name { flex:1 1 100%; }
#upbar .up.err { border-color:#b3261e; }
#upbar .up.err .st { color:#b3261e; }
#upbar .up button { width:auto; padding:var(--s3) var(--s5); font-size:13px; border-radius:8px; }
#upbar .bar { position:relative; flex:0 0 64px; height:4px; border-radius:2px; background:var(--line); overflow:hidden; display:block; }
#upbar .bar > i { position:absolute; inset:0 auto 0 0; background:var(--acc); border-radius:2px; transition:width .2s; display:block; }
/* bottom action bar — thumb zone */
footer { flex:0 0 auto; border-top:1px solid var(--line); padding:var(--s6) var(--s7) calc(var(--s6) + env(safe-area-inset-bottom));
         max-width:520px; width:100%; margin-inline:auto; display:flex; flex-direction:column; gap:var(--s4); background:var(--bg); }
footer:empty { display:none; }
h1 { font-size:22px; margin:0 0 var(--s6); }
h2 { font-size:14px; text-transform:uppercase; letter-spacing:.05em; color:var(--mut); margin:var(--s9) 0 var(--s4); }
button { font:inherit; padding:var(--s7) var(--s7); border:1px solid var(--acc); background:var(--acc); color:#fff;
         border-radius:12px; width:100%; cursor:pointer; }
button.ghost { background:none; color:var(--acc); border-color:var(--acc); }
button.danger { background:none; color:#b3261e; border-color:#b3261e; }
.row > button { flex:1 1 0; min-width:0; }
.bico { width:18px; height:18px; vertical-align:-4px; margin-right:var(--s3); }
button:disabled { opacity:.5; }
input, select, textarea { font:inherit; padding:var(--s6); width:100%; border:1px solid var(--line); border-radius:10px; margin:var(--s2) 0; }
textarea { resize:vertical; min-height:64px; }
/* mic dictation button — sits next to a field; pulses red while listening */
.mic { flex:0 0 auto; width:48px; padding:0; height:46px; display:flex; align-items:center; justify-content:center; }
.mic svg { width:22px; height:22px; }
.mic.on { background:#b3261e; border-color:#b3261e; color:#fff; animation:pulse 1s ease-in-out infinite; }
@keyframes pulse { 50% { opacity:.55; } }
.fieldmic { display:flex; gap:var(--s4); align-items:flex-start; }
.fieldmic > :first-child { margin:var(--s2) 0; }
/* Karta listy — JEDNA struktura (card() w kit.js). Sloty puste znikają same, więc ten sam
   markup obsługuje wiersz pacjenta, placówki i kartę planu. Wcześniej były to dwa osobne byty
   różniące się wyłącznie tym, których części używały.
   Wygląd siedzi w `.card`, a `.tap` dokłada WYŁĄCZNIE reakcję na dotyk: karta bez akcji
   (lista zaproszonych terapeutów) ma wyglądać identycznie, ale nie udawać klikalnej —
   kursor-łapka nad martwym elementem to obietnica, której nie ma kto spełnić. */
.card { border:1px solid var(--line); border-radius:12px; padding:var(--s7); margin:var(--s4) 0;
        display:flex; flex-direction:column; }
.card.tap { cursor:pointer; }
.card.tap:active { background:#fafbfb; border-color:#d8dcdc; }
.c-row { display:flex; align-items:flex-start; gap:var(--s5); }
.c-icon:empty, .c-badge:empty, .c-sub:empty, .c-meta:empty, .c-side:empty, .c-foot:empty { display:none; }
.c-icon { flex:0 0 auto; }
.c-body { flex:1 1 auto; min-width:0; }
.c-head { display:flex; align-items:center; gap:var(--s5); }
.c-title { flex:1 1 auto; font-size:16px; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.c-badge { flex:0 0 auto; }
.c-sub { margin-top:var(--s1); font-size:13px; color:var(--mut); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* JEDYNY komponent „etykieta w kapsułce" w całej appce. Były cztery na tę samą robotę
   (.pill, .chip-d, .due-txt, .paydot) — stąd wrażenie bałaganu i niespójne statusy między
   ekranami. Warianty: bez klasy = neutralny, .on = potwierdzenie, .due = wymaga reakcji.
   Używaj go do dawki, stanu pakietu i zaległości — nie dorabiaj kolejnego. */
.pill { flex:0 0 auto; display:inline-block; padding:var(--s1) var(--s5); border-radius:999px;
        font-size:12px; font-weight:600; white-space:nowrap;
        background:#eef0f0; color:var(--mut); }
.pill.on { background:var(--acc-soft); color:var(--acc); }
.pill.due { background:#fbeae4; color:var(--due); }
.c-meta { display:flex; align-items:baseline; flex-wrap:wrap; gap:var(--s3); margin-top:var(--s4); font-size:14px; }
.c-meta .kwota { font-size:15px; }
.c-meta .sep { color:var(--line); }
/* Prawa kolumna wiersza: licznik z chevronem, nierozrywalny. */
.c-side { flex:0 0 auto; white-space:nowrap; font-size:14px; }
/* Stopka karty — termin kontroli. Odcięta linią, bo to inna kategoria niż kasa nad nią. */
.c-foot { display:flex; align-items:center; gap:var(--s3); margin-top:var(--s4); padding-top:var(--s4);
          border-top:1px solid var(--line); font-size:13px; color:var(--mut); }
.c-foot svg { flex:0 0 auto; width:15px; height:15px; }
.c-foot span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* Kropka statusu USUNIĘTA (dawne .paydot): kolorowy punkcik bez legendy nie mówi, CO znaczy —
   czerwone przy nazwisku równie dobrze czyta się jako „problem medyczny" jak „zalega z kasą".
   Status wyraża teraz .pill Z TEKSTEM. */
/* Komunikat stanu — patrz notice() w kit.js. Osiem miejsc budowało tę strukturę ręcznie
   (stąd dawna nazwa .paybox, choć połowa użyć nie dotyczy płatności: kontrola, aktywacja). */
.notice { border:1px solid var(--line); border-left:3px solid var(--acc); border-radius:10px;
          padding:var(--s6) var(--s7); margin:var(--s6) 0; font-size:14px; line-height:1.5; }
.notice.due { border-left-color:var(--due); }
.notice b { display:block; margin-bottom:var(--s2); }
.notice button { margin-top:var(--s5); padding:var(--s5) var(--s7); font-size:14px; }
/* Dodatkowa linia pod treścią (termin kontroli, podpowiedź o płatności poza appką) oraz
   wiersz guzików — dawniej wklepywane jako style inline w czterech kopiach planBox. */
.notice-extra { display:block; margin-top:var(--s3); }
.notice-row { margin-top:var(--s5); }
.row { display:flex; gap:var(--s4); align-items:center; }
.row > * { margin:0; }
/* Stacked field + standing label. A placeholder disappears the moment a value is typed, so three
   full-width boxes reading "3" / "12" / "" would say nothing about which number is which. */
.field { display:flex; align-items:center; gap:var(--s5); }
.field > span { flex:0 0 116px; font-size:14px; color:var(--mut); }
.field > * { flex:0 0 auto; }
.field > input, .field > select, .field > textarea { flex:1 1 auto; min-width:0; margin:0; }
.mut { color:var(--mut); font-size:14px; }
video { width:100%; border-radius:12px; background:#000; }
/* Pion. Telefon nagrywa pionowo, więc odtwarzacz jest pionowy — 16:9 było założeniem z desktopu.
   Wysokość ograniczona, żeby 9:16 nie zjadło całego ekranu; szerokość liczy się z proporcji. */
iframe { aspect-ratio:9/16; max-height:68vh; width:auto; max-width:100%; border:0;
         border-radius:12px; background:#000; display:block; margin-inline:auto; }
/* Wiersz z nagraniem — JEDNA struktura wszędzie (clipRow). Sloty puste znikają same, więc
   ten sam markup obsługuje kalendarz (bez checkboxa) i bank (z checkboxem), bez wariantów. */
.card.clip { display:flex; gap:var(--s6); align-items:flex-start; cursor:pointer; }
/* Sloty: nie mają WŁASNYCH reguł wyrównania — wyrównanie do góry wynika z align-items
   na wierszu i obowiązuje każdy slot tak samo. Reguła per-slot znaczyłaby, że przy dodaniu
   kolejnego trzeba pamiętać o kolejnym wpisie; tak nie trzeba pamiętać o niczym. */
.card.clip > * { flex:0 0 auto; }
.card.clip > .meta { flex:1 1 auto; min-width:0; }
.card.clip > :empty { display:none; }
.card.clip input[type=checkbox] { width:20px; height:20px; margin:0; }
.row input[type=number] { width:70px; padding:var(--s4) var(--s3); font-size:14px; }
/* week timeline — horizontal day chips, today accented, dot = has exercises */
.timeline { display:flex; gap:var(--s3); overflow-x:auto; padding:var(--s1) 0 var(--s6); -webkit-overflow-scrolling:touch; }
.daychip { flex:0 0 auto; width:50px; padding:var(--s4) 0 var(--s3); border:1px solid var(--line); border-radius:12px;
        display:flex; flex-direction:column; align-items:center; gap:var(--s1); cursor:pointer; background:#fff; }
.daychip .dow { font-size:11px; color:var(--mut); }
.daychip .num { font-size:17px; font-weight:600; line-height:1.1; }
.daychip .dot { width:6px; height:6px; border-radius:50%; background:transparent; }
.daychip .dot.on { background:var(--acc); }
.daychip.today { border-color:var(--acc); }
.daychip.sel { background:var(--acc); border-color:var(--acc); color:#fff; }
.daychip.sel .dow { color:rgba(255,255,255,.85); }
.daychip.sel .dot.on { background:#fff; }
/* exercise card = thumbnail + meta; tap thumbnail -> video screen */
/* Clip tile — three states, ONE visual family: waiting for its signed URL, still transcoding on
   Stream, or playable. The white frame + grey border stays put through all of them, so the tile
   never jumps; only the glyph inside changes. Badge geometry is shared with .play, which is what
   keeps the states reading as the same component. */
/* PION 9:16, nie 96×64. Kafel był poziomy przy pionowym wideo, więc object-fit:cover wycinał
   środek kadru — ćwiczącemu ucinało głowę i nogi, czyli dokładnie to, co fizjo chce zobaczyć.
   Ta sama proporcja co .player i iframe; patrz "PION, zawsze" w CLAUDE.md. */
.thumb { position:relative; flex:0 0 58px; width:58px; aspect-ratio:9/16; border-radius:10px; overflow:hidden;
         background:var(--bg); border:1px solid var(--line);
         display:flex; align-items:center; justify-content:center; }
.thumb img { width:100%; height:100%; object-fit:cover; }
/* Every state wears the SAME chip — that is what makes waiting, processing and playable read as
   one component instead of three unrelated marks. Only the fill and the glyph change. Not scoped
   to .thumb: the edit-screen player reuses it, so a clip looks like itself everywhere. */
.badge { position:absolute; width:30px; height:30px; border-radius:50%;
         display:flex; align-items:center; justify-content:center; }
.badge svg { width:15px; height:15px; }
/* ready: the chip lands on a frame grab, so it goes opaque white + shadow to stay legible */
.badge.play { background:rgba(255,255,255,.92); box-shadow:0 1px 6px rgba(0,0,0,.35); }
.badge.play svg { width:13px; height:13px; fill:var(--acc); stroke:none; margin-left:var(--s1); }
/* waiting / processing: same chip, tinted for the white frame it sits on rather than an image */
.badge.spin, .badge.proc { background:#eef1f0; }
.badge.spin svg, .badge.proc svg { stroke:var(--mut); fill:none; }
/* Spin the glyph, NOT the chip — a rotating circle reads as a wobble, not a loader. */
.badge.spin svg { animation:spin 900ms linear infinite; }
.badge.proc { animation:breathe 1.8s ease-in-out infinite; }
@keyframes spin { to { transform:rotate(360deg) } }
@keyframes breathe { 0%,100% { opacity:.45 } 50% { opacity:1 } }
/* Motion is decoration here — the glyph already says which state it is. */
@media (prefers-reduced-motion: reduce) {
  .badge.spin svg, .badge.proc { animation:none; }
}
.thumb.tap { cursor:pointer; }
/* Edit player: same frame language as a tile, full width. The box is reserved up front so the
   fields below never jump when the iframe lands. */
/* Pion, jak nagranie. Wysokość ograniczona do 52vh, żeby pola dawki pod odtwarzaczem zostały
   w zasięgu kciuka bez przewijania; szerokość wynika z proporcji 9:16. */
.player { position:relative; aspect-ratio:9/16; max-height:52vh; width:auto; max-width:100%;
          border-radius:12px; overflow:hidden;
          background:var(--bg); border:1px solid var(--line); margin:0 auto var(--s7);
          display:flex; align-items:center; justify-content:center; }
.player iframe { width:100%; height:100%; aspect-ratio:auto; max-height:none; border-radius:0; }
.player .badge { width:44px; height:44px; }
.player .badge svg { width:20px; height:20px; }
.meta { flex:1; min-width:0; }
.meta b { display:block; font-size:16px; line-height:1.3; overflow:hidden; text-overflow:ellipsis; }
.meta .desc { display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
              margin-top:var(--s2); font-size:13px; }
/* Dawka: liczba w kapsułce znajduje się wzrokiem szybciej niż liczba w zdaniu, a fizjo czyta
   ją w ruchu. Używa wspólnego .pill — nie ma powodu, żeby dawka miała własny wygląd. */
.dose { display:flex; flex-wrap:wrap; gap:var(--s2); margin-top:var(--s3); }
.dose-none { margin-top:var(--s3); font-size:13px; font-style:italic; }
/* page head: linia powrotu („‹ Pacjenci") NAD linią tytułu („Piotr" + ikony).
   Etykieta, nie goły chevron: dwa sąsiednie poziomy (pakiety pacjenta / kalendarz pakietu)
   miały ten sam tytuł, więc bez nazwy rodzica nic nie odróżniało jednego od drugiego. */
/* page head = [ikona typu] [tytuł + podtytuł] [ikony akcji]; „gdzie jestem" jest w #nav.
   Wszystkie trzy to elementy TEGO SAMEGO flexa — ikona typu była wcześniej wstawką inline
   w <h1> i siadała na linii bazowej tekstu, więc nie trafiała w oś ikony akcji po prawej. */
.phead { display:flex; align-items:center; gap:var(--s5); margin-bottom:var(--s6); }
.ptitle { flex:1 1 auto; min-width:0; }
.ptitle h1 { margin:0; }
/* Podtytuł: doprecyzowuje nazwę ekranu, ale NIĄ nie jest. Zakres dat pakietu schodzi tutaj,
   dzięki czemu tytuł zostaje krótki i mieści pełne nazwy miesięcy zamiast skrótów. */
.psub { margin-top:var(--s1); font-size:13px; color:var(--mut); overflow:hidden;
        text-overflow:ellipsis; white-space:nowrap; }
/* Ikona TYPU obiektu — ten sam znak na karcie i w tytule ekranu, do którego karta prowadzi.
   Kółko, nie goły glif: na karcie sąsiaduje z tekstem i bez tła gubiłby się w wierszu. */
.tico { flex:0 0 auto; display:inline-flex; align-items:center; justify-content:center;
        width:30px; height:30px; border-radius:50%; background:var(--acc-soft); color:var(--acc); }
.tico svg { width:17px; height:17px; }
.phead > .tico { width:38px; height:38px; }
.phead > .tico svg { width:20px; height:20px; }
.phead h1 { flex:1 1 auto; margin:0; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* Pasek „gdzie jestem" — element POWŁOKI między headerem a main, nie treść ekranu.
   Nie przewija się razem z main, więc poziom widać też po zjechaniu na dół długiej listy.
   Doklejony do headera bez odstępu, więc zaokrąglenie TYLKO od dołu: pasek czyta się jak
   szuflada wysunięta z zielonego nagłówka, a nie jak kolejna karta w treści.
   --acc-mid, nie blade --acc-soft: pierwsza wersja na #eaf2f0 znikała na białej stronie —
   tekst miał kontrast, ale PLAMA paska nie, więc oko go przeskakiwało. Ten odcień to
   najjaśniejszy zielony, na którym biały tekst trzyma jeszcze AA (5.3:1).
   Cień odcina pasek od białej treści — bez niego zlewa się z headerem w jeden blok. */
#nav { flex:0 0 auto; z-index:1; }
#nav:empty { display:none; }
/* Pasek kładzie się NA treści o całą wysokość swojego zaokrąglenia — inaczej w dolnych
   rogach prześwitywało białe tło strony zamiast kadru wideo. 14px = promień .navbar. */
#nav.over { margin-bottom:-var(--s7); }
.navbar { display:flex; align-items:center; gap:var(--s4); width:100%;
          max-width:520px; margin-inline:auto;
          padding:var(--s5) var(--s7); font:inherit; font-size:15px; font-weight:600; line-height:1.35;
          border:0; border-radius:0 0 14px 14px; text-align:left;
          background:var(--acc-mid); color:#fff;
          box-shadow:0 2px 10px rgba(15,95,87,.22); }
.navbar > span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* Chevron w pełnym białym kółku: sam glif tonął w zielonym tle, a to jedyny znak mówiący
   „to się klika". Kółko robi z niego guzik, którego nie da się przeoczyć. */
.navbar .chev { flex:0 0 auto; width:24px; height:24px; border-radius:50%; background:#fff;
                display:flex; align-items:center; justify-content:center; }
.navbar .chev svg { width:16px; height:16px; color:var(--acc-mid); pointer-events:none; }
button.navbar:active { background:var(--acc); }
/* Ekran korzeniowy: pasek zostaje dla ciągłości layoutu, ale bez kółka i lżejszym krojem —
   nie ma dokąd wracać, więc nic nie może obiecywać tapnięcia. */
.navbar.static { font-weight:500; background:var(--acc-mid); color:rgba(255,255,255,.92); }
/* trailing edit affordance on list rows (trainer) */
.editbtn { flex:0 0 auto; width:38px; height:38px; padding:0; display:flex; align-items:center; justify-content:center;
           background:none; border:1px solid var(--line); border-radius:10px; color:var(--mut); }
/* pointer-events:none — inaczej tap trafia w <svg>, a action() robi e.target.disabled
   i blokada podwójnego tapnięcia po cichu nic nie robi (svg nie ma .disabled). */
.editbtn svg { width:18px; height:18px; pointer-events:none; }
.editbtn:active { background:var(--acc-soft); color:var(--acc); }
/* bottom sheet — dodawanie/edycja wjeżdża z dołu, „×" zwija; ekran pod spodem zostaje */
.sheet-wrap { position:fixed; inset:0; z-index:50; }
.sheet-wrap .veil { position:absolute; inset:0; background:rgba(0,0,0,.35); opacity:0; transition:opacity .2s; }
.sheet { position:absolute; left:0; right:0; bottom:0; max-width:520px; margin-inline:auto;
         background:var(--bg); border-radius:18px 18px 0 0; box-shadow:0 -8px 30px rgba(0,0,0,.18);
         display:flex; flex-direction:column; max-height:88dvh;
         transform:translateY(100%); transition:transform .25s ease; }
.sheet-wrap.on .veil { opacity:1; }
.sheet-wrap.on .sheet { transform:none; }
/* Sekcje sheeta: stałe, poza treścią, która rośnie i przewija. Jedna reguła zamiast trzech. */
.sheet > * { flex:0 0 auto; }
.sheet .shead { display:flex; align-items:center; gap:var(--s4); padding:var(--s7) var(--s7) var(--s3); }
.sheet .shead > * { flex:0 0 auto; }
.sheet .shead b { flex:1 1 auto; min-width:0; font-size:18px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.sheet .shead .x { width:38px; height:38px; padding:0; display:flex; align-items:center; justify-content:center;
                   background:none; border:1px solid var(--line); border-radius:10px; color:var(--mut); }
.sheet .shead .x svg { width:18px; height:18px; pointer-events:none; }
.sheet .sbody { flex:1 1 auto; overflow-y:auto; padding:var(--s2) var(--s7); -webkit-overflow-scrolling:touch; }
.sheet .sbody:empty { display:none; }
/* Sheet bez stopki (np. „Dane i zgoda" — jego guziki żyją w treści, bo są destrukcyjne):
   ostatni element inaczej dotykałby krawędzi ekranu, czyli siadał dokładnie pod kciukiem.
   :has(), nie „+": sfoot stoi w DOM PO sbody, więc sąsiad następny nie sięga wstecz. */
.sheet .sbody:has(+ .sfoot:empty) { padding-bottom:calc(var(--s8) + env(safe-area-inset-bottom)); }
/* sfoot = strefa kciuka sheeta: pola, potem guziki — jak stopka ekranu */
.sheet .sfoot { display:flex; flex-direction:column; gap:var(--s4);
                padding:var(--s6) var(--s7) calc(var(--s6) + env(safe-area-inset-bottom)); border-top:1px solid var(--line); }
.sheet .sfoot:empty { display:none; }
@media (prefers-reduced-motion: reduce) {
  .sheet, .sheet-wrap .veil { transition:none; }
}
/* welcome / login screen */
.welcome h1 { font-size:26px; letter-spacing:-.02em; margin-bottom:var(--s4); }
.hero-card { position:relative; border-radius:16px; overflow:hidden; aspect-ratio:16/10; margin-bottom:var(--s8); }
.hero-card img { width:100%; height:100%; object-fit:cover; display:block; }
.hero-card .tint { position:absolute; inset:0; background:linear-gradient(180deg,rgba(15,95,87,.22) 0%,rgba(11,74,68,.78) 100%); }
.hero-card .cap { position:absolute; left:16px; right:16px; bottom:14px; color:#fff; font-weight:600; font-size:16px; line-height:1.3; text-shadow:0 1px 8px rgba(0,0,0,.4); }
.feats { list-style:none; padding:0; margin:var(--s8) 0 0; display:flex; flex-direction:column; gap:var(--s7); }
.feats li { display:flex; gap:var(--s6); align-items:center; font-size:15px; }
.feats .fic { flex:0 0 auto; width:36px; height:36px; border-radius:10px; background:var(--acc-soft); color:var(--acc); display:flex; align-items:center; justify-content:center; }
.feats .fic svg { width:20px; height:20px; }
.consent-box { border:1px solid var(--line); border-left:3px solid var(--acc); border-radius:10px;
  padding:var(--s7); margin-top:var(--s7); font-size:15px; line-height:1.6; }
.consent-box ul.co { list-style:none; padding:0; margin:var(--s6) 0 0; }
.consent-box ul.co li { padding:var(--s3) 0 var(--s3) var(--s8); position:relative; border-top:1px solid var(--line); }
.consent-box ul.co li:first-child { border-top:0; }
.consent-box ul.co li::before { content:"→"; position:absolute; left:0; color:var(--acc); }
.consent-box p + p { margin-top:var(--s6); }
.consent-box a { color:var(--acc); }
.consent-tick { display:flex; gap:var(--s6); align-items:flex-start; margin-top:var(--s8); padding:var(--s7);
  border:2px solid var(--acc); border-radius:10px; font-size:15px; line-height:1.5; cursor:pointer; }
.consent-tick input { width:24px; height:24px; flex:0 0 auto; margin:0; accent-color:var(--acc); }
.legal { margin-top:var(--s8); font-size:12px; line-height:1.6; color:var(--mut); }
.legal a { color:var(--mut); text-decoration:underline; text-underline-offset:2px; }
.legal a:hover { color:var(--acc); }
/* Zakres dat pod nazwą pakietu — pojawia się TYLKO gdy nazwa istnieje, bo bez nazwy to
   właśnie zakres jest tytułem i powtarzałby się dwa razy pod rząd. */

/* ---------- ekran podglądu nagrania ----------
   Wideo wypełnia całe pole treści. `main` ma padding 16px dla wszystkich ekranów, więc ten
   jeden odbija go ujemnym marginesem — dzięki temu reszta appki zostaje bez zmian.
   Wysokość liczona z dostępnego miejsca (100%), nie z vh: nad main siedzi header i pasek
   nawigacji, a pod nim bywa stopka i pasek uploadu — 100vh wyjechałoby pod nie. */
/* Ekran w trybie `bleed`: zero paddingu, więc odtwarzacz wypełnia pole bez kombinowania
   z ujemnymi marginesami. Pasek nawigacji podjeżdża NA niego (patrz #nav.over). */
main.bleed { padding:0; }
/* .fill = ten SAM komponent .player, tylko rozpięty na całe pole zamiast siedzieć w kolumnie
   treści. Osobny markup dla ekranu podglądu był błędem — dwa sposoby na to samo wideo. */
.player.fill { aspect-ratio:auto; max-height:none; width:100%; height:100%;
               margin:0; border:0; border-radius:0; background:#000; }
.player.fill iframe { width:100%; height:100%; max-height:none; aspect-ratio:auto;
                      border-radius:0; background:#000; }
/* Nakładka TYLKO u góry — dół iframe'a to kontrolki Stream. pointer-events:none, żeby
   tapnięcie zawsze docierało do odtwarzacza (play/pauza), a nie zatrzymywało się na tekście. */
/* padding-top większy o wysokość, jaką pasek nawigacji na nią nachodzi — inaczej tytuł
   chowałby się pod jego zaokrąglonym dołem. */
.player.fill .vinfo { position:absolute; inset:0 0 auto 0; padding:var(--s9) var(--s7) var(--s9); pointer-events:none;
         color:#fff; background:linear-gradient(180deg, rgba(0,0,0,.72) 0%, rgba(0,0,0,.45) 55%, transparent 100%);
         text-shadow:0 1px 3px rgba(0,0,0,.6); }
.player.fill .vinfo b { display:block; font-size:17px; line-height:1.3; }
.player.fill .vdose { margin-top:var(--s1); font-size:13px; font-weight:600; color:rgba(255,255,255,.92); }
/* Opis nagrania — JEDEN wygląd dla nakładki na wideo i dla sheetów edycji (clipDesc()
   w kit.js). Wcześniej sheet dawki miał go wklepanego stylem inline, więc ten sam tekst
   miał dwie definicje. pre-wrap: łamania linii są treścią, nie przypadkiem. */
.clip-desc { margin:var(--s5) 0 0; font-size:14px; line-height:1.4; white-space:pre-wrap; }
/* .clamp tylko na kadrze: opis nie może zalać obrazu, a pełny tekst i tak jest w planie. */
.clip-desc.clamp { display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden; }
.player.fill .vinfo .clip-desc { margin-top:var(--s3); }
/* Guzik „zrobione" pod kartą ćwiczenia (widok pacjenta). */
.done-slot { margin-top:var(--s5); }
/* Wiersz banku nagrań: mniejszy kafel niż na karcie ćwiczenia, bo to lista wyboru, nie podgląd. */
/* Pusty stan listy i podpowiedź w sheecie — jedno miejsce na odstęp, zamiast sześciu
   różnych zapisów tego samego <p class=mut>. */
.empty { margin:var(--s5) 0; font-size:14px; }
.shint { margin:0 0 var(--s6); font-size:14px; }

/* Klasy pochodzące z szablonów w tpl.html — tytuł wiersza nagrania i teksty komunikatu. */
.card.clip .clip-title { display:block; font-size:16px; line-height:1.3; overflow:hidden; text-overflow:ellipsis; }
.notice .n-text { display:block; }
