/* ═══════════════════════════════════════════════════════════════════════
   HVMBAK MOCKUPS — warstwa wizualna

   Pracownia jak w HUMBAK EFFECTS: górny pasek, po lewej ustawienia, w środku
   płótno, po prawej tła. Cały system (przyciski „Cięcie", typografia Archivo
   Narrow, faktura arkusza, ziarno na błękicie, toast) jest przeniesiony
   z EFFECTS — z jedną zasadniczą różnicą.

   ODWRÓCENIE (decyzja Kuby): tam czerń i jasne elementy, tutaj papier i CZARNE
   elementy. To nie jest nowa paleta, tylko ta sama przełożona na drugą stronę:
   te same trzy kolory (papier, czerń, błękit), ta sama geometria, te same
   odstępy. Powód jest prosty: mockup ocenia się na jasnym tle, bo na jasnym
   tle ludzie go potem oglądają — w prezentacji, na stronie, w poście.

   RUNDA 4 dociąga tę myśl do końca: scena i kadr są CZYSTĄ BIELĄ, a kremowy
   ton zostaje wyłącznie panelom (dock, rail, pasek). Podgląd przestał więc
   być wgłębieniem, a stał się najjaśniejszym punktem ekranu — bo to on
   udaje miejsce, w którym mockup potem zamieszka. Szczegóły przy tokenach
   pięter w :root.

   RUNDA 5 (feedback Kuby 04.08.2026) zmienia trzy rzeczy:
   1. INTERFEJS WRACA NA POLSKI. Apka idzie do polskich kursantów, więc
      angielskie etykiety wypadły. Komentarze w kodzie jak zawsze po polsku.
   2. KADR TO BIAŁA KARTKA Z JEDNYM CIENIEM. Wcześniej krawędź kadru rysowały
      dwie konkurujące kreski (jasny pierścień na .viewer i ciemna kreska
      inset na .dropzone). Przy niecałkowitym devicePixelRatio kreska inset
      wychodziła tylko przy górnej i lewej krawędzi, więc rama wyglądała jak
      przesunięty duplikat i cały kadr sprawiał wrażenie krzywego. Teraz
      jest jedna powierzchnia: biel, zaokrąglone rogi (--frame-radius)
      i jeden miękki cień pod całością.
   3. KADR MIERZY SIĘ POJEMNIKIEM, NIE OKNEM. Stary zapis brał 62vw z całego
      okna, choć siedzi w kolumnie węższej o dwa panele — przy zwężaniu
      wyjeżdżał poza scenę. Miarę daje teraz jednostka kontenerowa sceny
      (100cqw), szczegóły przy .dropzone.
   ═══════════════════════════════════════════════════════════════════════ */

:root {
  /* BŁĘKIT WYPADŁ W RUNDZIE 15 (decyzja Kuby: „w ogóle pozbądźmy się, póki co,
     tego koloru"). Do tej pory jeden akcent — jasny błękit — dostawał
     powierzchnie: obwódkę wybranego kafla, punkt trzymanego suwaka, pasek
     postępu, chwyty osi czasu, hover pełnego przycisku i toast błędu. Na
     papierze czytał się jako „baby blue" i psuł czarno-białą surowość reszty.
     Zastępuje go CZERŃ MARKI: ta sama rola, ten sam rysunek, tylko bez barwy.
     Tam, gdzie czerń na czerni byłaby niewidoczna (hover pełnego klina),
     akcentem jest jaśniejszy grafit.
     Nazwy zostają na jednej zmiennej --akcent, żeby ewentualny powrót koloru
     był jedną linijką, a nie polowaniem po arkuszu. */
  --akcent: #0D0D0D;
  --akcent-hover: #333030;
  --akcent-cichy: rgba(13, 13, 13, 0.42);

  --black: #0D0D0D;
  --black-soft: #1A1A1A;

  /* PIĘTRA — ODWRÓCONE W RUNDZIE 4 (decyzja Kuby 30.07.2026).
     Wcześniej scena była wgłębieniem (ciemniejszy papier), a panele stały
     wyżej. Teraz jest odwrotnie i z konkretnego powodu: mockup ocenia się na
     BIELI, bo na bieli ląduje — w prezentacji, na stronie, w poście. Każdy
     kremowy odcień pod podglądem kłamał o kolorach eksportu.

     Nowy porządek: scena i kadr = czysta biel (#FFFFFF), panele = ledwo
     kremowe (#F8F5F1, różnica ~3% jasności — wystarczy, żeby kolumny
     odkleiły się od podglądu, za mało, żeby zaczęły wyglądać na żółte),
     kafle w panelu wracają na biel (piętro nad panelem), a wgłębienia
     w panelu (miniatury animacji) idą o piętro niżej niż panel. */
  --paper: #F8F5F1;           /* ton paneli; też „jasny atrament" na czerni */
  --panel: #F8F5F1;
  --panel-lift: #FFFFFF;      /* kafel tła, kafel stylu: piętro nad panelem */
  --panel-sink: #EFEBE5;      /* wgłębienie w panelu: kadr miniatury animacji */
  --stage-bg: #FFFFFF;        /* scena i podgląd: biel bez wyjątków */

  /* Atrament w czterech mocach — od tytułu do ledwo widocznej podpowiedzi. */
  --ink: #0D0D0D;
  --ink-2: rgba(13, 13, 13, 0.66);
  /* RUNDA 6, KONTRAST. --ink-3 stało na 0.44, co na papierze --panel daje
     2,98:1 — poniżej progu 4,5:1 dla zwykłego tekstu, a to jest ton WSZYSTKICH
     etykiet sekcji, podpowiedzi, podpisów kafli i nieaktywnych klinów, czyli
     połowy tekstu w apce. 0.58 daje 4,6:1 i nadal jest wyraźnie cichsze od
     --ink-2 (6,2:1), więc hierarchia zostaje ta sama, tylko dolne piętro
     przestaje być szare na szarym. --ink-4 zostaje przy kreskach i wygaszeniach
     (nie przy tekście — patrz .grid-caption). */
  --ink-3: rgba(13, 13, 13, 0.58);
  --ink-4: rgba(13, 13, 13, 0.28);
  --line: rgba(13, 13, 13, 0.10);
  --line-strong: rgba(13, 13, 13, 0.15);

  /* NIEUŻYWANE od 30.07.2026. Ziarno na błękicie wypadło (decyzja Kuby: na
     jasnym motywie szum robił z akcentu brudną plamę). Zmienna zostaje, bo
     tekstura jest częścią języka HVMBAK i może wrócić na innej powierzchni —
     ale NIE wolno jej wieszać na błękicie. Aktywnej faktury szukaj w --sheet. */
  --grain: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%27140%27%20height%3D%27140%27%3E%3Cfilter%20id%3D%27n%27%20filterUnits%3D%27userSpaceOnUse%27%20x%3D%270%27%20y%3D%270%27%20width%3D%27140%27%20height%3D%27140%27%3E%3CfeTurbulence%20type%3D%27fractalNoise%27%20baseFrequency%3D%270.62%27%20numOctaves%3D%273%27%20seed%3D%2711%27%20stitchTiles%3D%27stitch%27%2F%3E%3CfeColorMatrix%20type%3D%27saturate%27%20values%3D%270%27%2F%3E%3CfeComponentTransfer%3E%3CfeFuncR%20type%3D%27linear%27%20slope%3D%271.35%27%20intercept%3D%27-0.175%27%2F%3E%3CfeFuncG%20type%3D%27linear%27%20slope%3D%271.35%27%20intercept%3D%27-0.175%27%2F%3E%3CfeFuncB%20type%3D%27linear%27%20slope%3D%271.35%27%20intercept%3D%27-0.175%27%2F%3E%3CfeFuncA%20type%3D%27linear%27%20slope%3D%270%27%20intercept%3D%271%27%2F%3E%3C%2FfeComponentTransfer%3E%3C%2Ffilter%3E%3Crect%20width%3D%27140%27%20height%3D%27140%27%20filter%3D%27url(%23n)%27%2F%3E%3C%2Fsvg%3E");
  --grain-size: 120px;

  /* Faktura arkusza: poszarpana, ledwo widoczna siatka kresek. Ta z EFFECTS,
     tylko kreska przemalowana z papieru na atrament — na jasnym tle jasna
     kreska nie istnieje. */
  --sheet: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%27300%27%20height%3D%27300%27%3E%20%3Cfilter%20id%3D%27j%27%20x%3D%27-30%25%27%20y%3D%27-30%25%27%20width%3D%27160%25%27%20height%3D%27160%25%27%3E%20%3CfeTurbulence%20type%3D%27fractalNoise%27%20baseFrequency%3D%270.012%200.9%27%20numOctaves%3D%272%27%20seed%3D%277%27%20result%3D%27n%27%2F%3E%20%3CfeDisplacementMap%20in%3D%27SourceGraphic%27%20in2%3D%27n%27%20scale%3D%273.2%27%2F%3E%20%3CfeGaussianBlur%20stdDeviation%3D%270.45%27%2F%3E%20%3C%2Ffilter%3E%20%3Cg%20filter%3D%27url%28%23j%29%27%20stroke%3D%27%230D0D0D%27%20stroke-opacity%3D%270.07%27%20stroke-width%3D%271%27%20fill%3D%27none%27%3E%20%3Cpath%20d%3D%27M0%20150%20H300%27%2F%3E%3Cpath%20d%3D%27M150%200%20V300%27%2F%3E%20%3C%2Fg%3E%3C%2Fsvg%3E");

  /* Szachownica przezroczystości: dwa tony bieli, ledwo się różnią. Ma
     powiedzieć „tu nic nie ma", a nie krzyczeć w szachy. Na białej scenie
     stara krata (#E3DDD5) była już plamą, nie szeptem — pole ciemniejsze
     zjechało do #F0EDE9, czyli mniej więcej tam, gdzie stoi panel. */
  --checker: conic-gradient(from 90deg,
    #F0EDE9 0 25%, #FFFFFF 0 50%, #F0EDE9 0 75%, #FFFFFF 0);
  --checker-size: 16px 16px;

  --bar-h: 60px;
  /* Wcięcia systemowe (Dynamic Island, notch w poziomie). Na desktopie i bez
     viewport-fit=cover env() daje zero, więc doliczanie nic nie psuje —
     a w zainstalowanej PWA pasek przestaje wjeżdżać pod zegar. */
  --safe-top: env(safe-area-inset-top, 0px);
  --stage-foot: 56px;   /* ile miejsca pod płótnem zjada pasek akcji */
  --col: 340px;         /* szerokość kolumn bocznych */
  --dock-pad: 18px;     /* boczny oddech w kolumnach; czyta go też sekcja eksportu */
  --dock-pad-b: 26px;   /* dolny oddech kolumny; przyklejony eksport musi go znać */
  --radius: 14px;

  /* Kadr podglądu: zaokrąglenie i wysokość w JEDNYM miejscu, bo trzymają je
     cztery elementy naraz (.viewer, .viewer-clip, canvas, strefa upuszczania)
     i muszą być co do piksela zgodne — inaczej wraca efekt „przesuniętej
     ramki". Rogi są wyraźne (apka do mockupów, sam kadr też ma wyglądać jak
     mockup), ale nie na tyle, żeby zjadać narożnik wgranego zrzutu.
     --frame-max-h to wysokość, jaka zostaje po pasku górnym, odstępach sceny
     i pasku akcji pod płótnem; mobile podmienia ją niżej na ułamek ekranu. */
  --frame-radius: 16px;
  /* OŚ CZASU TEŻ ZJADA WYSOKOŚĆ (runda 15). To był ten błąd ze zrzutu Kuby:
     w trybie wideo pod kadrem stoi jeszcze oś (tor 34 px + stopka + odstępy
     = 92 px razem z przerwą w .stage-col), a sufit kadru jej NIE odejmował.
     Kolumna sceny wychodziła wyższa od swojej komórki gridu, a że <body> ma
     overflow: hidden, pasek z „Wgraj swój plik" i sterowaniem nagrania po
     prostu wypadał pod krawędź ekranu — bez żadnej możliwości przewinięcia.
     Wyglądało to na zawieszony podgląd („zamroził się kompletnie cały
     podgląd, przez co nie mogę zobaczyć przycisków niżej").
     Zmienna --os-h jest zerem w trybie zdjęcia i realną wysokością w wideo.
     UWAGA: --frame-max-h musi być PRZEDEKLAROWANE tam, gdzie zmienia się
     --os-h (patrz body.mode-wideo niżej) — var() w wartości własności
     niestandardowej rozwiązuje się na elemencie, na którym ta własność jest
     ZADEKLAROWANA, więc samo ustawienie --os-h na <body> nic by nie dało. */
  --os-h: 0px;
  --frame-max-h: calc(100svh - var(--bar-h) - var(--safe-top) - 28px - var(--stage-foot) - var(--os-h));

  /* PASSE-PARTOUT KADRU (runda 6). Do tej pory zaokrąglone rogi miał SAM
     canvas, więc podgląd obiecywał coś, czego w pliku nie ma: PNG wychodzi
     prostokątny, bo tłem maluje się całe płótno. Zaokrąglenie zostaje, ale
     przenosimy je z obrazu na OPRAWĘ: biała ramka o stałej szerokości,
     zaokrąglona jak dotąd, a w niej płótno z OSTRYMI rogami. Wtedy widać
     jednym rzutem oka, gdzie kończy się okno podglądu, a gdzie zaczyna plik —
     i podgląd przestaje kłamać. Wysokość samego płótna musi znać tę ramkę,
     stąd druga zmienna. */
  --frame-pad: 10px;
  --frame-inner-h: calc(var(--frame-max-h) - 2 * var(--frame-pad));

  /* Geometria klina przycisków „Cięcie" (BEZ tych zmiennych clip-path jest
     nieważny i przyciski wychodzą płaskimi prostokątami). */
  --pinch: 9%;   /* zwężenie w środku muszki switcha */
  --lean: 7%;    /* pochylenie krawędzi pojedynczego przycisku */
  --bw: 1.5px;   /* grubość udawanego obrysu */

  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  --ease-io: cubic-bezier(0.65, 0, 0.35, 1);
}

* { margin: 0; padding: 0; box-sizing: border-box; }

/* Własne display z arkusza bije regułę [hidden] z przeglądarki. */
[hidden] { display: none !important; }

/* Strona otwarta z dysku (file://) nie dostanie modułów ES, więc żadna
   z kontrolek nic nie robi — patrz skrypt na końcu index.html. Wygaszamy je,
   żeby nikt nie klikał w martwe panele; komunikat stoi w środku kadru. */
.bez-modulow .dock,
.bez-modulow .rail,
.bez-modulow .stage-bar,
.bez-modulow .bar .tp-switch { opacity: .4; pointer-events: none; }

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

body {
  /* Strona jest biała jak scena — kremowe są tylko panele. Widać to w dwóch
     miejscach: przy odbiciu przewijania na mobile i wszędzie tam, gdzie
     kolumny nie dochodzą do krawędzi. */
  background: var(--stage-bg);
  color: var(--ink);
  font-family: 'Archivo Narrow', system-ui, 'DM Sans', sans-serif;
  font-feature-settings: 'ss01', 'cv11';
  -webkit-font-smoothing: antialiased;
  height: 100svh;
  overflow: hidden;
}

.label {
  font-family: 'Archivo Narrow', system-ui, sans-serif;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--ink-3);
}

/* Archivo Narrow jest niższy niż SF Pro Compressed. Delikatny scaleY podciąga
   litery w pionie, dając ten sam „wysoki", posterowy charakter — bez zmiany
   szerokości ani układu (transform nie rusza pudełka layoutu). Tylko czysty
   tekst, bez pigułek/tła, żeby nic się nie rozjechało. */
.label, .tp-label, .control-name, .bg-tile-name {
  display: inline-block;
  transform: scaleY(1.09);
  transform-origin: center;
}

/* ═══════════════════════════════ POWŁOKA ═════════════════════════════════ */

.workspace {
  height: 100svh;
  overflow: hidden;
  display: grid;
  grid-template-rows: calc(var(--bar-h) + var(--safe-top)) minmax(0, 1fr);
  grid-template-columns: var(--col) minmax(0, 1fr) var(--col);
  /* Boczne wcięcia (notch przy telefonie w poziomie) — na desktopie zero. */
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
  grid-template-areas:
    "bar  bar   bar"
    "dock stage rail";
}

/* Wejście: kolumny wsuwają się z lekkim opóźnieniem względem siebie. */
.workspace > * { animation: rise .6s var(--ease-out) backwards; }
.bar   { animation-delay: .02s; }
.dock  { animation-delay: .16s; }
.stage { animation-delay: .08s; }
.rail  { animation-delay: .16s; }
@keyframes rise {
  from { opacity: 0; transform: translateY(12px); }
}

/* ── pasek ──────────────────────────────────────────────────────────────── */

/* PASEK ZOSTAJE JASNY (runda 16, korekta Kuby). Runda 15 przemalowała go na
   czarno „dla jednolitości z EFFECTS" — Kuba odkręcił to po zobaczeniu na
   żywo: ma być biały z czarnym znakiem, jak przedtem. Z tamtej rundy zostaje
   WYŁĄCZNIE ROZMIAR: --bar-h 60 px i znak liczony wysokością atramentu
   (patrz .bar-brand), bo to akurat zgadza się z EFFECTS i było poprawką,
   o którą chodziło. */
.bar {
  grid-area: bar;
  background: var(--panel);
  color: var(--ink);
  /* Trzy kolumny: logo po lewej, switch dokładnie na osi ekranu, po prawej
     puste pole tej samej miary. Flexem switch stałby „mniej więcej". */
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  padding: var(--safe-top) 16px 0 14px;
  gap: 16px;
  border-bottom: 1px solid var(--line);
  z-index: 20;
}
.bar-brand {
  /* WYSOKOŚĆ ZNAKU LICZONA Z ATRAMENTU, NIE Z PUDEŁKA (runda 15).
     W EFFECTS `.bar-logo` ma height: 30px, ale tamten plik (3000×648) ma wokół
     liter margines: sam atrament zajmuje 446 z 648 px, czyli 68,8% wysokości
     obrazu — na ekranie wychodzi 20,6 px wersalika. Nasz lockup jest przycięty
     do bounding boxu (runda 14: 759×120, atrament 112 px = 93,3%), więc przy
     tych samych 30 px pudełka litery byłyby o 36% wyższe niż w EFFECTS. I tak
     to wyglądało — stąd uwaga Kuby, że logo ma być tej samej wielkości.
     Ustawiamy więc WYSOKOŚĆ ATRAMENTU i dzielimy przez jego udział w pliku.
     Obie liczby są ZMIERZONE (dekoder PNG + bbox po alfie), nie dobrane okiem;
     przy podmianie pliku znaku trzeba zmierzyć na nowo. */
  --logo-ink: 20.6px;
  --logo-fill: 0.9333;
  --logo-h: calc(var(--logo-ink) / var(--logo-fill));
  justify-self: start;
  display: flex;
  align-items: center;
  /* Odstęp między członami znaku, nie między przyciskami: mniejszy niż
     światło wyrazowe w samym logotypie, żeby czytało się jako jeden znak. */
  gap: calc(var(--logo-h) * 0.12);
  background: none;
  border: 0;
  cursor: pointer;
  padding: 6px 8px 6px 4px;
  border-radius: 8px;
  transition: background .3s var(--ease-out);
}
.bar-brand:hover { background: rgba(13, 13, 13, 0.06); }
.bar-brand:focus-visible { outline: 2px solid var(--black); outline-offset: 1px; }
.bar-spacer { justify-self: end; }

/* ── ZNAK: „HVMBAK" z rysunku + „MOCKUPS" z fontu ──────────────────────────
   W katalogu leży jeden logotyp: HVMBAK EFFECTS. Nad aplikacją do mockupów
   mówił nieprawdę, a pliku nie wolno nam podmienić. Rozwiązanie z warsztatu,
   nie z kodu: bierzemy z rysunku sam wyraz HVMBAK i składamy drugi człon
   fontem — dokładnie tak, jak robi się warianty logotypu w identyfikacji.

   Kadr liczy się proporcją, nie pikselami, więc trzyma się przy każdej
   wysokości znaku. Miara wzięta z rysunku (1163×251 px): atrament zaczyna
   się w 41. kolumnie, „HVMBAK" kończy w 602., przerwa do „EFFECTS" trwa do
   642. Tniemy w środku przerwy, czyli na 622 px → 622/251 = 2.478. */
/* Runda 14: pełny lockup „HMBAK MOCKUPS" w jednym pliku (1265×200) — koniec
   z przycinaniem starego znaku EFFECTS do 622/251 i dosztukowywanym tekstem. */
.bar-mark {
  display: block;
  flex: none;
  height: var(--logo-h, 30px);
}
.bar-logo {
  height: 100%;
  width: auto;
  max-width: none;
  display: block;
  user-select: none;
  -webkit-user-drag: none;
}
/* MIARY WZIĘTE Z RYSUNKU, NIE Z OKA. Wersalik logotypu zajmuje 170 z 251 px
   wysokości znaku (0.677), a linia pisma leży na 210/251 (0.837). Archivo
   Narrow ma wersalik na 0.698 firetu (zmierzone TextMetrics na wczytanym
   pliku, nie z tabeli), więc stopień pisma = 0.677 / 0.698 ≈ 0.97 wysokości
   znaku — wtedy M w MOCKUPS jest dokładnie tak wysokie jak H w HVMBAK.
   Przesunięcie w dół dosadza linię pisma tekstu do linii pisma rysunku
   (oba pudełka są wyśrodkowane w pionie, a ich linie pisma nie leżą w tym
   samym miejscu). Waga lżejsza od logotypu: pierwszy człon ma zostać znakiem,
   drugi tylko go doprecyzowuje. */
.bar-word {
  font-family: 'Archivo Narrow', system-ui, sans-serif;
  font-size: calc(var(--logo-h, 30px) * 0.97);
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.005em;
  text-transform: uppercase;
  color: var(--ink);
  /* Dosunięcie do linii pisma rysunku. Wartość jest ZMIERZONA (pusty
     inline-block dosuwa się do linii pisma, rysunek ma ją na 0.837 swojej
     wysokości), nie dobrana na oko — po korekcie obie linie pisma stoją
     w tym samym punkcie co do setnej piksela. */
  transform: translateY(calc(var(--logo-h, 30px) * -0.012));
  white-space: nowrap;
}

/* ═══════════════ PRZYCISKI — system „Cięcie" (pochylony die-cut) ═══════════
   Kształt siedzi na ::before (obrys) i ::after (wypełnienie z odsunięciem =
   udawana krawędź, którą border nie umie obejść po skosie). Sam <button> jest
   prostokątny, więc hit-area i focus-ring nie są przycięte. Rozciągnięcie
   posterowe (scaleY) siedzi na .tp-label, żeby stany :active go nie kasowały.
   Wszystkie kształty symetryczne względem poziomej osi → tekst centruje się sam.
   GEOMETRIA BEZ ZMIAN względem EFFECTS; odwrócone są tylko skiny (niżej). */
.tp {
  position: relative; isolation: isolate;
  border: 0; background: transparent;
  min-height: 36px; padding: 0 20px;
  display: inline-flex; align-items: center; justify-content: center;
  gap: 7px;
  cursor: pointer;
  font-family: 'Archivo Narrow', system-ui, sans-serif;
  font-size: 10px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--ink); -webkit-tap-highlight-color: transparent;
  /* Bez heurystyki podwójnego stuknięcia: Safari nie wstrzymuje i nie gubi
     tapnięć w przyciski. */
  touch-action: manipulation;
  transition: transform .3s var(--ease-out), color .25s;
}
.tp-label { pointer-events: none; }
.tp-ico { width: 15px; height: 15px; flex: none; pointer-events: none; }
.tp--ico { padding: 0 13px; min-width: 44px; }
.tp:active { transform: translateY(1px); }
/* Błękit na papierze ma za mało kontrastu na obwódkę focusa, więc pierścień
   jest czarny. To jedyne miejsce, gdzie odchodzimy od akcentu z EFFECTS. */
.tp:focus-visible { outline: 2px solid var(--black); outline-offset: 3px; }
.tp[disabled] { opacity: .5; cursor: default; }
.tp::before, .tp::after {
  content: ''; position: absolute; inset: 0; z-index: -1;
  clip-path: var(--shape);
  transition: background .25s var(--ease-out);
}
.tp::after { clip-path: var(--shape-in, var(--shape)); }

/* Geometrie klina (--lean pojedyncze, --pinch muszka switcha). */
.tp--single {
  --shape: polygon(0 var(--lean), 100% 0, 100% 100%, 0 calc(100% - var(--lean)));
  --shape-in: polygon(var(--bw) calc(var(--lean) + var(--bw)), calc(100% - var(--bw)) var(--bw), calc(100% - var(--bw)) calc(100% - var(--bw)), var(--bw) calc(100% - var(--lean) - var(--bw)));
}
.tp--single-r {
  --shape: polygon(0 0, 100% var(--lean), 100% calc(100% - var(--lean)), 0 100%);
  --shape-in: polygon(var(--bw) var(--bw), calc(100% - var(--bw)) calc(var(--lean) + var(--bw)), calc(100% - var(--bw)) calc(100% - var(--lean) - var(--bw)), var(--bw) calc(100% - var(--bw)));
}
.tp--seg-l {
  --shape: polygon(0 0, 100% var(--pinch), 100% calc(100% - var(--pinch)), 0 100%);
  --shape-in: polygon(var(--bw) var(--bw), calc(100% - var(--bw)) calc(var(--pinch) + var(--bw)), calc(100% - var(--bw)) calc(100% - var(--pinch) - var(--bw)), var(--bw) calc(100% - var(--bw)));
}
.tp--seg-r {
  --shape: polygon(0 var(--pinch), 100% 0, 100% 100%, 0 calc(100% - var(--pinch)));
  --shape-in: polygon(var(--bw) calc(var(--pinch) + var(--bw)), calc(100% - var(--bw)) var(--bw), calc(100% - var(--bw)) calc(100% - var(--bw)), var(--bw) calc(100% - var(--pinch) - var(--bw)));
}

.tp-switch { position: relative; isolation: isolate; display: inline-flex; gap: 2px; }
/* Muszka pod parą zakładek: płytka, na której leżą oba kliny. */
.tp-switch::before {
  content: ''; position: absolute; inset: 0; z-index: -2;
  clip-path: polygon(0 0, 50% var(--pinch), 100% 0, 100% 100%, 50% calc(100% - var(--pinch)), 0 100%);
  background: rgba(13, 13, 13, 0.07);
}
.tp-switch .tp { min-width: 82px; }

/* ── skiny „Cięcie", odwrócone ──────────────────────────────────────────────
   cut-ghost: ciemny obrys na papierze, środek w kolorze podłoża (dlatego
   ::after musi znać podłoże — stąd osobna reguła dla sceny).
   cut-solid: pełna czerń z papierowym tekstem, hover na błękit (tekst czarny).
   cut-seg: przezroczysty, aktywny czernieje. */
.cut-ghost::before { background: rgba(13, 13, 13, 0.30); }
.cut-ghost::after { background: var(--panel); }
/* Na scenie klin leży na ciemniejszym papierze, nie na panelu. */
.stage .cut-ghost::after { background: var(--stage-bg); }
.cut-ghost { color: var(--ink-2); }
.cut-ghost:hover::before { background: rgba(13, 13, 13, 0.75); }
.cut-ghost:hover { color: var(--ink); }

.cut-solid::before, .cut-solid::after { background: var(--black); }
.cut-solid { color: var(--paper); }
.cut-solid:hover::before, .cut-solid:hover::after { background: var(--akcent-hover); }
.cut-solid:hover { color: var(--paper); }

.cut-seg::before, .cut-seg::after { background: transparent; }
.cut-seg { color: var(--ink-3); }
.cut-seg:hover { color: var(--ink); }
.cut-seg.is-on::before, .cut-seg.is-on::after { background: var(--black); }
.cut-seg.is-on { color: var(--paper); }

/* Compact — mniejsze przyciski (reset, formaty, sterowanie wideo). */
.tp--sm { min-height: 28px; padding: 0 12px; font-size: 9px; letter-spacing: 0.04em; }
.tp--sm .tp-ico { width: 13px; height: 13px; }
.tp-switch--sm .tp { min-width: 44px; min-height: 28px; padding: 0 10px; font-size: 9px; }
.tp--ico.tp--sm { min-width: 34px; padding: 0 9px; }

/* Kręciołek w przycisku (eksport w toku). */
.btn-spin {
  position: absolute;
  top: 50%; right: 12px;
  width: 12px; height: 12px;
  margin-top: -6px;
  border: 2px solid rgba(248, 245, 241, 0.35);
  border-top-color: var(--paper);
  border-radius: 50%;
  opacity: 0;
  transition: opacity .2s;
}
.tp.is-busy { padding-right: 32px; }
.tp.is-busy .btn-spin { opacity: 1; animation: spin .7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ═══════════════════════════════ ŚRODEK ══════════════════════════════════ */

.stage {
  grid-area: stage;
  position: relative;
  display: grid;
  place-items: center;
  /* Dolny margines równy górnemu: pasek akcji ma siedzieć w połowie odstępu
     między dolną krawędzią płótna a dołem strony, nie kleić się do kadru. */
  padding: 26px 30px;
  min-width: 0;
  min-height: 0;
  /* Scena jest POJEMNIKIEM ZAPYTAŃ: pusty kadr bierze z niej szerokość przez
     100cqw. Bez tego musiałby liczyć się z vw, czyli z całym oknem — a między
     nim a oknem stoją dwie kolumny po --col. Kolumna gridu i tak jest
     minmax(0, 1fr), więc containment niczego tu nie zmienia poza tym, że
     udostępnia jednostkę. */
  container-type: inline-size;
  container-name: scena;
  /* Runda 4: scena jest CZYSTĄ BIELĄ, bez faktury arkusza. Kreska --sheet
     przetrwała w planszach XY (tam jest częścią „technicznego płótna"), ale
     pod podglądem każda tekstura zmienia odczyt kolorów mockupu — a to
     jedyne miejsce w apce, gdzie ocenia się kolory. */
  background: var(--stage-bg);
}

/* Kolumna sceny: płótno i pasek akcji pod nim. Szerokość bierze się z
   szerszego dziecka, więc przy szerokim kadrze pasek trzyma się krawędzi
   płótna, a przy wąskim (pion) rozpycha się do swojej miary. */
.stage-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 22px;
  max-width: 100%;
  min-width: 0;
  min-height: 0;
}

.stage-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  width: 100%;
}
.stage-bar-group { display: flex; align-items: center; gap: 8px; flex: none; }

.viewer {
  position: relative;
  max-width: 100%;
  max-height: var(--frame-max-h);
  border-radius: var(--frame-radius);
  /* JEDEN cień na cały kadr (runda 5). Wcześniej krawędź rysowały trzy
     warstwy naraz plus kreska inset ze strefy upuszczania i przy ułamkowym
     dpr rozjeżdżały się o pół piksela — kadr wyglądał na krzywy. Zostaje
     miękki cień pod całością: bliska warstwa sadza kartkę na scenie, daleka
     daje jej głębię. Żadnego pierścienia, żadnej kreski w środku. */
  box-shadow:
    0 3px 10px -3px rgba(13, 13, 13, 0.10),
    0 20px 44px -14px rgba(13, 13, 13, 0.26);
  animation: viewerIn .8s var(--ease-out) backwards;
}
@keyframes viewerIn {
  from { opacity: 0; transform: scale(0.965) translateY(10px); }
}

/* Kadr: canvas i strefa upuszczania w JEDNEJ komórce gridu, jedno nad drugim.
   Pusty stan podaje miarę, dopóki nie ma pliku; potem [hidden] wyjmuje go
   z układu i miarę podaje canvas. Szachownica leży na tle kadru, więc widać
   ją przez przezroczyste piksele canvasa. */
.viewer-clip {
  position: relative;
  display: grid;
  /* minmax(0, max-content) zamiast domyślnego auto: kolumna MOŻE zejść poniżej
     rozmiaru zawartości (obrót telefonu / zwężenie okna — canvas ma jeszcze
     stary rozmiar w px, a max-width: 100% skaluje go w dół bez rozpychania
     przyklejonej sceny). PUŁAPKA, która położyła produkcję 30.07: wariant
     z MIN-content zapadał kolumnę DO ZERA — wkład min-content canvasa
     z procentowym max-width w cyklicznej kolumnie liczy się jako 0, więc
     canvas renderował się poprawnie, ale wyświetlał z offsetWidth = 0
     (obraz „znikał" po wgraniu). max-content nie ma tej dziury: kolumna
     bierze szerokość canvasa, a kurczy się tylko, gdy brakuje miejsca. */
  grid-template-columns: minmax(0, max-content);
  max-width: 100%;
  min-width: 0;
  border-radius: var(--frame-radius);
  overflow: hidden;
  max-height: var(--frame-max-h);
  /* Ramka podglądu: biała kartka, na której leży płótno (patrz --frame-pad).
     Szachownica przezroczystości zeszła stąd na sam canvas — ma pokazywać
     dziury W PLIKU, a nie podkładać się pod oprawę okna. */
  padding: var(--frame-pad);
  background: var(--panel-lift);
}
.viewer-clip > * { grid-area: 1 / 1; min-width: 0; }

/* Płótno ma OSTRE rogi — bo taki jest plik. Zaokrąglenie należy do oprawy
   (.viewer-clip), nie do obrazu; wcześniej canvas nosił te same rogi co kadr
   i podgląd obiecywał zaokrąglony PNG. Szachownica leży POD pikselami płótna,
   więc widać ją dokładnie tam, gdzie w pliku będzie dziura. */
.viewer-canvas {
  display: block;
  justify-self: center;
  align-self: center;
  max-width: 100%;
  max-height: var(--frame-inner-h);
  width: auto; height: auto;
  border-radius: 0;
  background-image: var(--checker);
  background-size: var(--checker-size);
}

/* ── pusty stan ─────────────────────────────────────────────────────────── */

/* Strefa upuszczania jest przyciskiem, bo klik ma otwierać wybór pliku.
   To ona nadaje płótnu rozmiar, póki nic nie wgrano.

   ROZMIAR (runda 5). Stary zapis brał `min-width: min(880px, 62vw)`, czyli
   ułamek CAŁEGO okna — a strefa siedzi w kolumnie węższej o dwa panele
   po --col. Przy zwężaniu okna min-width przerastał dostępne miejsce i kadr
   wyjeżdżał poza scenę (skarga Kuby: „kwadracik się ucina"). Teraz miarę daje
   sama scena przez 100cqw, więc pojemnik nie może przerosnąć pojemnika:
     • 100cqw   — cała szerokość sceny, bez jej paddingu (to content box),
     • 880px    — sufit, żeby na szerokim monitorze kadr nie rozlał się w płot,
     • --frame-max-h × 1,6 — sufit z WYSOKOŚCI: przy niskim oknie kadr zwęża
       się razem z wysokością, zamiast dać się przyciąć u dołu.
   Proporcja 16:10 zamiast pary min-width/min-height: jedna liczba zamiast
   dwóch, więc kadr nie ma jak zgubić kształtu przy żadnej szerokości.
   min-width pilnuje tylko, żeby nie zapadł się do zera. */
.dropzone {
  position: relative;
  z-index: 2;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 16px;
  /* BEZ SUFITU 880 px (runda 8). Sufit sprawiał, że na szerokim monitorze
     pusty kadr miał 880 px, a płótno po wgraniu pliku 1240 — czyli podgląd
     skakał o 360 px, a zanim skoczył, płótno (szersze) rozpychało komórkę
     gridu i z prawej strony pustego kadru wychodził pasek tła. Teraz miarę
     dają te same dwie liczby, co płótnu: szerokość sceny i sufit z wysokości.
     Zmierzone po zmianie: pusty kadr = płótno co do piksela na 2000, 1700,
     1440, 1200, 860 i 390 px szerokości okna. */
  /* KWADRAT wysokości sceny (runda 14, decyzja Kuby): pusty kadr ma być
     duży i kwadratowy, nie płaskim paskiem 16:10 z pustką nad i pod nim. */
  width: min(calc(100cqw - 2 * var(--frame-pad)), var(--frame-inner-h));
  min-width: 168px;
  max-width: 100%;
  aspect-ratio: 1 / 1;
  /* Przy wąskiej kolumnie 16:10 robi z kadru pasek, a nad nim i pod nim
     zostaje pusta scena. Dolna granica wysokości pozwala kadrowi zabrać to
     miejsce (wychodzi wtedy kształt bliższy kwadratu), ale nigdy więcej, niż
     scena naprawdę ma — stąd min() z --frame-max-h. */
  min-height: min(var(--frame-inner-h), 420px);
  /* Padding równy ze wszystkich stron: treść i tak stoi w optycznym środku
     (place-content: center), ale przy dwuwierszowym tekście to on decyduje,
     ile powietrza zostaje przy krawędzi. */
  padding: 32px;
  border: 0;
  /* Czysta biała kartka. Krawędź rysuje wyłącznie cień na .viewer — żadnej
     kreski inset, bo to ona robiła z kadru przesunięty duplikat. */
  background: var(--panel-lift);
  color: var(--ink-3);
  cursor: pointer;
  text-align: center;
  transition: background .3s var(--ease-out), box-shadow .3s var(--ease-out), color .25s;
}
/* Przeglądarka bez zapytań o pojemnik (Chrome < 105, Safari < 16) wyrzuca całą
   deklarację z 100cqw i zostawia kadr bez szerokości — zapadłby się do miary
   tekstu. Rezerwowa miara liczy się wtedy z okna pomniejszonego o obie kolumny
   panelu i o padding sceny: mniej dokładne niż 100cqw (nie wie o marginesach
   wewnętrznych), ale trzyma kadr w scenie i nie tnie go przy zwężaniu. */
@supports not (width: 1cqw) {
  .dropzone {
    width: min(calc(100vw - 2 * var(--col) - 60px - 2 * var(--frame-pad)), var(--frame-inner-h));
  }
  @media (max-width: 860px), (max-height: 500px) and (pointer: coarse) {
    .dropzone { width: min(calc(100vw - 32px), calc(var(--frame-inner-h) * 1.45)); }
  }
}

/* Najechanie bez ramki: kartka lekko cieplej, znak i tekst ciemnieją. Tyle
   wystarczy, żeby było widać, że to się klika. */
.dropzone:hover { background: #FCFAF7; color: var(--ink-2); }
.dropzone:hover .drop-ico { opacity: 1; }
.dropzone:focus-visible { outline: 2px solid var(--black); outline-offset: -6px; }
/* Plik nad kadrem — klasę zapala JS na dragenter. Tu obwódka MA być, bo to
   stan chwilowy i mówi „puść tutaj"; kreska idzie po zaokrąglonym narożniku
   kadru, więc nie kłóci się z jego kształtem. */
.dropzone.is-over {
  background: var(--panel-sink);
  box-shadow: inset 0 0 0 2px var(--black);
  color: var(--black);
}
.dropzone.is-over .drop-ico { opacity: 1; }
/* Przeciąganie pliku nad kadr z już wgranym medium: dropzone jest wtedy
   schowany, więc podświetlamy sam kadr. */
.viewer.is-drop .viewer-clip { box-shadow: inset 0 0 0 2px var(--akcent); }
.drop-ico { width: 40px; height: 40px; opacity: .55; transition: opacity .25s; }
.drop-text {
  /* Miara mierzona na najdłuższym zdaniu, jakie tu wchodzi („Wrzuć zrzut
     ekranu, wklej ze schowka albo kliknij i wybierz plik"): łamie się na dwa
     wiersze mniej więcej równej długości, bez sieroty w drugim. */
  max-width: 34ch;
  font-family: 'DM Sans', system-ui, sans-serif;
  font-size: 13.5px;
  line-height: 1.5;
  letter-spacing: 0;
  text-transform: none;
  text-wrap: balance;
}

/* ── pasek wideo ────────────────────────────────────────────────────────── */

.video-bar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 9px;
  flex: 1 1 auto;
  min-width: 0;
}

/* ── TWOJE USTAWIENIA ───────────────────────────────────────────────────────
   Lista zapamiętanych wyglądów. Każdy wpis to para: przycisk z nazwą
   (przywraca) i krzyżyk (kasuje) — jeden element z dwoma polami trafienia,
   bo zagnieżdżony przycisk w przycisku jest nieprawidłowym HTML-em
   i przeglądarka rozrywa taki markup. */

.zapisy { display: flex; flex-wrap: wrap; gap: 6px; }
.zapisy:empty { display: none; }

.zapis {
  display: inline-flex;
  align-items: stretch;
  border-radius: 6px;
  overflow: hidden;
  box-shadow: inset 0 0 0 1px var(--line);
  background: var(--panel-lift);
}
.zapis-nazwa {
  border: 0;
  background: none;
  padding: 5px 8px;
  font-family: 'Archivo Narrow', system-ui, sans-serif;
  font-size: 10.5px;
  letter-spacing: 0.02em;
  color: var(--ink-2);
  cursor: pointer;
  max-width: 150px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.zapis-nazwa:hover { color: var(--black); background: rgba(13, 13, 13, 0.04); }
.zapis-kasuj {
  border: 0;
  background: none;
  padding: 0 7px;
  font-size: 12px;
  line-height: 1;
  color: var(--ink-3);
  cursor: pointer;
  box-shadow: inset 1px 0 0 var(--line);
}
.zapis-kasuj:hover { color: var(--black); background: rgba(13, 13, 13, 0.06); }
.zapis-nazwa:focus-visible,
.zapis-kasuj:focus-visible { outline: 2px solid var(--black); outline-offset: -2px; }

/* ── OŚ CZASU Z PRZYCINANIEM ────────────────────────────────────────────────
   Pełna szerokość sceny, wysoki tor i duże chwyty — przy nagraniu do 3 minut
   wąski suwak dawał mniej niż piksel na sekundę. Kolejność warstw w torze:
   przesłony poza zakresem → zakres → głowica → chwyty (na wierzchu, bo to
   w nie się celuje). */

.os {
  width: 100%;
  /* Leży POD kadrem, nad paskiem z przyciskami — nie w jego rzędzie.
     W rzędzie zabierała miejsce przyciskowi „Wgraj swój plik" i podpis
     urywał się w połowie. */
  margin-top: 16px;
  display: flex;
  flex-direction: column;
  gap: 7px;
}

.os-tor {
  position: relative;
  height: 34px;
  border-radius: 7px;
  background: var(--panel-sink);
  box-shadow: inset 0 0 0 1px var(--line);
  cursor: pointer;
  touch-action: none;     /* przeciąganie po torze nie ma przewijać strony */
  overflow: hidden;
}

/* Materiał poza zaznaczeniem: przygaszony, ale nadal widoczny — użytkownik
   ma wiedzieć, ile odcina, a nie tylko co zostaje. */
/* Materiał poza zaznaczeniem musi być WYRAŹNIE ciemniejszy — przy 0,10 krycia
   nie dało się rozpoznać, gdzie kończy się fragment. */
.os-poza {
  position: absolute;
  top: 0;
  bottom: 0;
  background: rgba(13, 13, 13, 0.22);
  pointer-events: none;
  z-index: 2;
}
.os-poza--lewa { left: 0; }
.os-poza--prawa { right: 0; }

/* Zaznaczony fragment. Był błękitny (runda 11) — jedyne kolorowe pole
   w środku ekranu i dokładnie to, co Kuba nazwał „baby blue". Teraz jest
   szarością atramentu: różnica względem przygaszonego otoczenia bierze się
   z JASNOŚCI, a nie z barwy. */
.os-zakres {
  position: absolute;
  top: 0;
  bottom: 0;
  z-index: 1;
  background: rgba(13, 13, 13, 0.07);
  box-shadow: inset 0 0 0 1px rgba(13, 13, 13, 0.28);
  pointer-events: none;
}

.os-glowica {
  position: absolute;
  top: 0;
  bottom: 0;
  z-index: 3;
  width: 2px;
  margin-left: -1px;
  background: var(--black);
  /* Wskaźnik dalej przechodzi na tor (klik w tor przewija), ale głowica jest
     fokusowalna: strzałki przewijają do momentu — jedyna rzecz na osi, która
     nie miała odpowiednika z klawiatury. */
  pointer-events: none;
}
.os-glowica:focus-visible { outline: 2px solid var(--black); outline-offset: 3px; }

/* Chwyt: szeroki obszar trafienia (18 px) z widoczną kreską pośrodku —
   sam pasek 4 px byłby nie do złapania palcem. */
.os-uchwyt {
  position: absolute;
  top: 0;
  bottom: 0;
  z-index: 4;
  width: 18px;
  margin-left: -9px;
  cursor: ew-resize;
  touch-action: none;
}
.os-uchwyt::before {
  content: '';
  position: absolute;
  top: 3px;
  bottom: 3px;
  left: 50%;
  width: 4px;
  margin-left: -2px;
  border-radius: 2px;
  background: var(--akcent-cichy);
  transition: background .2s var(--ease-out), transform .2s var(--ease-out);
}
.os-uchwyt:hover::before,
.os-uchwyt.is-ciagniety::before { background: var(--black); transform: scaleX(1.35); }
.os-uchwyt:focus-visible { outline: 2px solid var(--black); outline-offset: 1px; }

.os-stopka {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.os-info {
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  color: var(--ink-3);
}
.os-caly {
  border: 0;
  padding: 0;
  background: none;
  font-family: 'Archivo Narrow', system-ui, sans-serif;
  font-size: 10px;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--ink-3);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.os-caly:hover { color: var(--black); }

.mode-foto .os { display: none !important; }
body.no-media .os { display: none !important; }
body.is-exporting .os-tor { pointer-events: none; opacity: .5; }

.video-time {
  flex: none;
  font-family: 'JetBrains Mono', ui-monospace, monospace;
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
  color: var(--ink-3);
  white-space: nowrap;
}

/* Dwie ikony w jednym przycisku: widoczną wybiera stan. JS może użyć klasy
   (is-playing / is-muted) albo aria-pressed — obsługujemy oba zapisy, żeby
   moduł wideo nie musiał się domyślać, czego tu oczekujemy. */
.ico-pause, .ico-mute { display: none; }
#btnPlay.is-playing .ico-play,
#btnPlay[data-state="playing"] .ico-play,
#btnPlay[aria-pressed="true"] .ico-play { display: none; }
#btnPlay.is-playing .ico-pause,
#btnPlay[data-state="playing"] .ico-pause,
#btnPlay[aria-pressed="true"] .ico-pause { display: block; }
#btnMute.is-muted .ico-sound,
#btnMute[data-state="muted"] .ico-sound,
#btnMute[aria-pressed="true"] .ico-sound { display: none; }
#btnMute.is-muted .ico-mute,
#btnMute[data-state="muted"] .ico-mute,
#btnMute[aria-pressed="true"] .ico-mute { display: block; }

/* ══════════════════════ KOLUMNY BOCZNE (wspólne) ═════════════════════════ */

.dock, .rail {
  min-height: 0;
  overflow: hidden;
  background: var(--panel);
}
/* Lewa kolumna jest siatką „przewijana treść + stałe dno" (runda 5): eksport
   i stopka nie należą do przewijania, więc nie mają jak przykryć kafli. */
.dock {
  grid-area: dock;
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
}
.rail { grid-area: rail; border-left: 1px solid var(--line); }

/* Lewa kolumna jest długa (sześć paneli), więc przewijanie musi być cichą
   częścią motywu, a nie systemowym pasem na pół ekranu: włos w tonie atramentu,
   bez szyny, widoczny mocniej dopiero pod kursorem. Firefox obsługuje
   scrollbar-width/color, WebKit własne pseudoelementy — trzeba dać oba. */
.dock-scroll, .rail-scroll {
  height: 100%;
  /* W kolumnie-flexie treść musi móc zejść poniżej swojej wysokości, inaczej
     przewijanie przenosi się na całą kolumnę i stałe dno ucieka poza ekran. */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 20px var(--dock-pad) var(--dock-pad-b);
  /* Tabulator przewija kolumnę sam. Bez zapasu fokus lądowałby dokładnie
     pod rozmytą krawędzią (patrz maska niżej) i obwódka wybranego kafla
     byłaby wyblakła — a to jedyny znak, gdzie się właśnie jest. */
  scroll-padding: 26px 0 52px;
  display: flex;
  flex-direction: column;
  gap: 22px;
  scrollbar-width: thin;
  scrollbar-color: rgba(13, 13, 13, 0.18) transparent;
}
/* ── CO ZOSTAŁO POD KRAWĘDZIĄ (runda 6) ────────────────────────────────────
   Lewa kolumna ma siedem sekcji i ~2000 px wysokości, a na ekranie 1440×900
   widać z niej 660 px. Do tej rundy nie mówiło tego NIC: pasek przewijania
   na macOS jest nakładkowy (w spoczynku niewidzialny), więc treść po prostu
   urywała się w połowie kafla urządzenia, tuż nad kreską sekcji eksportu.
   Wyglądało to na błąd rysowania, a pięć sekcji (Pochylenie, Animacja, Cień,
   Prywatność, Płótno) po prostu nie istniało dla kogoś, kto widzi apkę
   pierwszy raz.

   Krawędź jest teraz ROZMYTA MASKĄ: treść nie urywa się, tylko gaśnie —
   a gasnący rząd kafli to najstarszy i najczytelniejszy znak „przewiń dalej".
   Maska siedzi na pudełku przewijania, nie na treści, więc nie jedzie razem
   z nią. Zapala ją JS (main.js: pilnujKrawedzi) i tylko wtedy, gdy naprawdę
   jest co pokazać — na końcu listy krawędź wraca ostra, żeby nie udawać, że
   dalej coś jest. */
.dock-scroll, .rail-scroll {
  --fade-t: 0px;
  --fade-b: 0px;
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0, #000 var(--fade-t),
    #000 calc(100% - var(--fade-b)), transparent 100%);
  mask-image: linear-gradient(to bottom,
    transparent 0, #000 var(--fade-t),
    #000 calc(100% - var(--fade-b)), transparent 100%);
}
.dock-scroll.ma-gore, .rail-scroll.ma-gore { --fade-t: 20px; }
.dock-scroll.ma-dol, .rail-scroll.ma-dol { --fade-b: 42px; }
/* Stopka kolumny podnosi się o cień, gdy treść pod nią wjeżdża — to samo
   zdanie co maska, tylko od drugiej strony. */
.dock.ma-dol .dock-export { box-shadow: 0 -12px 20px -14px rgba(13, 13, 13, 0.5); }

.dock-scroll::-webkit-scrollbar,
.rail-scroll::-webkit-scrollbar { width: 9px; }
.dock-scroll::-webkit-scrollbar-track,
.rail-scroll::-webkit-scrollbar-track { background: transparent; }
.dock-scroll::-webkit-scrollbar-thumb,
.rail-scroll::-webkit-scrollbar-thumb {
  /* Przezroczysty border + background-clip: content-box daje wrażenie
     odsuniętego, cieńszego pasa — inaczej włos kleiłby się do krawędzi. */
  background-color: rgba(13, 13, 13, 0.16);
  background-clip: content-box;
  border: 3px solid transparent;
  border-radius: 999px;
}
.dock-scroll:hover::-webkit-scrollbar-thumb,
.rail-scroll:hover::-webkit-scrollbar-thumb { background-color: rgba(13, 13, 13, 0.3); }

.rail-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
}

.block { display: flex; flex-direction: column; gap: 12px; }
.block-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  min-height: 28px;
  font-family: 'Archivo Narrow', system-ui, sans-serif;
  font-size: 10px;
  letter-spacing: 0.025em;
}
/* Kreska oddzielająca sekcje w kolumnie. */
.block--sep {
  padding-top: 16px;
  border-top: 1px solid var(--line);
}

/* Para przycisków w nagłówku sekcji (reset + kostka). Klin ikonowy dosuwa się
   do resetu na tyle blisko, żeby czytały się jako jedno narzędzie. */
.head-actions { display: flex; align-items: center; gap: 4px; }
.head-actions .tp--ico.tp--sm { min-width: 30px; padding: 0 8px; }
.head-actions .tp--sm .tp-ico { width: 13px; height: 13px; }

/* ── LEWA: suwaki ───────────────────────────────────────────────────────── */

.controls { display: flex; flex-direction: column; gap: 15px; }

.control { display: flex; flex-direction: column; gap: 7px; }
.control-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-family: 'Archivo Narrow', system-ui, sans-serif;
  font-size: 10px;
  letter-spacing: 0.025em;
}
.control-name {
  color: var(--ink-2);
  font-weight: 600;
}
/* W EFFECTS liczba była błękitna na czerni. Na papierze błękit jako TEKST
   znika (kontrast ~1.5:1), więc liczba jest czarna, a błękit dostaje punkt
   suwaka pod palcem — akcent zostaje w tym samym miejscu ekranu. */
.control-value {
  color: var(--ink);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  -webkit-user-select: none;
  user-select: none;
}
/* Plansza XY nie ma liczby, ma gest. W miejscu, gdzie przy suwaku stoi
   wartość, stoi więc słowo — małą literą i ciszej, bo to podpowiedź, a nie
   odczyt. Bez niego pusty kwadrat z rombem nie mówi, że się go ciągnie. */
.control-value--cichy {
  font-weight: 500;
  font-size: 9px;
  letter-spacing: 0.06em;
  text-transform: lowercase;
  color: var(--ink-3);
}

.slider {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 22px;
  background: transparent;
  cursor: pointer;
  /* Pion należy do strony: kolumna ustawień to kilkanaście pełnej szerokości
     suwaków i palec zaczynający przewijanie na którymś z nich ciągnął wartość
     zamiast listy. Oś gestu rozstrzyga JS (bindSliderDotyk w main.js):
     poziomy ruch steruje suwakiem, pionowy puszcza natywne przewijanie. */
  touch-action: pan-y;
}
.slider::-webkit-slider-runnable-track {
  height: 2px; border-radius: 2px; background: rgba(13, 13, 13, 0.18);
}
.slider::-moz-range-track {
  height: 2px; border-radius: 2px; background: rgba(13, 13, 13, 0.18);
}

/* Punkt suwaka „Klin": zwężający się ku dołowi klin (echo klina przycisków).
   Na jasnym pasku jest ciemny — odwrotnie niż w EFFECTS. */
.slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 6px; height: 17px;
  margin-top: -7.5px;
  border: 0; border-radius: 0;
  background: linear-gradient(180deg, var(--black), #333030);
  clip-path: polygon(0 0, 100% 0, 73% 100%, 27% 100%);
  box-shadow: 0 1px 4px rgba(13, 13, 13, 0.25);
  transition: transform .3s var(--ease-spring), background .25s;
}
.slider::-moz-range-thumb {
  width: 6px; height: 17px; border: 0; border-radius: 0;
  background: linear-gradient(180deg, var(--black), #333030);
  clip-path: polygon(0 0, 100% 0, 73% 100%, 27% 100%);
  box-shadow: 0 1px 4px rgba(13, 13, 13, 0.25);
}
/* Najechanie tylko tam, gdzie naprawdę jest kursor: na dotyku przeglądarka
   trzyma :hover na ostatnio dotkniętym elemencie i punkt zostawałby błękitny. */
@media (hover: hover) {
  .slider:hover::-webkit-slider-thumb { transform: scaleY(1.12); }
}
/* Trzymanie: na myszy :active, na dotyku klasa z JS (tam :active bywa
   przyznawany dopiero po chwili albo wcale). */
.slider:active::-webkit-slider-thumb,
.slider.is-held::-webkit-slider-thumb { transform: scaleY(1.2); }
.slider:focus-visible { outline: 2px solid var(--black); outline-offset: 2px; }

/* ── LEWA: rzędy klinów (format płótna, rodzaj cienia) ──────────────────── */

/* Kliny w rzędzie, wszystkie pochylone w tę samą stronę: czyta się jak pasek
   oderwanych biletów. Muszka (seg-l + seg-r) działa tylko w parze, przy pięciu
   sztukach robiłaby wężyk. Pole nieaktywnego klina jest ledwo zaznaczone, żeby
   było widać, że w ogóle jest do klikania.

   auto-fit zamiast repeat(5, 1fr): przy naprawdę wąskim panelu rząd ma prawo
   złamać się na dwa wiersze, zamiast ucinać litery.

   MIARA TORU: 42 px, nie 52. Przy kolumnie 252 px (okno ≤1100) pięć klinów
   mieści się w jednym rzędzie dopiero od tej wartości — przy 52 px wychodziły
   cztery kliny i jeden sierota w drugim wierszu. Same podpisy i tak łamią się
   w środku klina (patrz niżej), więc węższy tor niczego nie ucina. */
.wedge-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(42px, 1fr));
  gap: 3px;
}
.wedge-row .tp {
  --lean: 12%;
  min-width: 0;
  padding: 0 4px;
  font-size: 8.5px;
  letter-spacing: 0.02em;
}
/* Podpis w klinie łamie się, zamiast dostać wielokropek. Polskie nazwy są
   dłuższe od angielskich („Przy podłożu" zamiast „Grounded") i w kolumnie
   340 px nie mieszczą się w jednym wierszu; ucięte „Przy pod…" nic nie mówi,
   a dwa wiersze czytają się normalnie. Klin ma stałą wysokość na dwa wiersze,
   żeby rząd nie skakał, gdy jeden podpis się złamie, a drugi nie. */
.wedge-row .tp {
  min-height: 34px;
  align-items: center;
}
.wedge-row .tp-label {
  max-width: 100%;
  white-space: normal;
  line-height: 1.18;
  text-align: center;
  overflow-wrap: break-word;
}
.wedge-row .cut-seg::after { background: rgba(13, 13, 13, 0.055); }
.wedge-row .cut-seg:hover::after { background: rgba(13, 13, 13, 0.11); }
.wedge-row .cut-seg.is-on::before,
.wedge-row .cut-seg.is-on::after { background: var(--black); }

/* ── LEWA: kafle stylu ──────────────────────────────────────────────────────
   Miniatura jest POWIĘKSZONYM lewym górnym narożnikiem materiału — tak samo
   jak w shots.so i z tego samego powodu: styl mockupu widać wyłącznie na
   krawędzi, więc pokazujemy krawędź, a nie całą karteczkę w skali 1:20.

   Geometria: .styl-art to okienko z gradientem, a materiał (::after) to
   zaokrąglony prostokąt, którego lewy górny róg wypada w środku okienka,
   a pozostałe trzy wyjeżdżają za kadr. ::before to arkusze pod materiałem
   (stos). Cała różnica między stylami siedzi w box-shadow materiału. */

.styl-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px 8px;
}

.styl-tile {
  display: flex;
  flex-direction: column;
  gap: 5px;
  border: 0;
  padding: 0;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform .4s var(--ease-spring);
}
.styl-tile:hover { transform: translateY(-2px); }
.styl-tile:active { transform: translateY(0) scale(0.985); }
.styl-tile:focus-visible { outline: 2px solid var(--black); outline-offset: 2px; }

.styl-art {
  position: relative;
  display: block;
  /* 3:2 zamiast kwadratu (runda 5): dziewięć ramek plus dwie obudowy zjadało
     kwadratami całą wysokość kolumny i obudowy wpadały pod krawędź ekranu.
     Miniatura pokazuje NAROŻNIK materiału, więc niższy kafel niczego nie gubi,
     a cała lista wyboru mieści się bez przewijania. */
  aspect-ratio: 3 / 2;
  border-radius: 8px;
  overflow: hidden;
  /* Srebrny papier: gradient daje miniaturze głębię, bez niego biały materiał
     na białym kaflu byłby niewidzialny. Po przejściu na biały motyw ton
     podjechał o pół piętra w górę, żeby kafel nie był ciemniejszy od panelu,
     na którym leży — ale różnica jasności wewnątrz gradientu ZOSTAJE, bo to
     ona rysuje krawędź materiału. */
  background: linear-gradient(155deg, #EAE4DC 0%, #FFFFFF 52%, #ECE6DF 100%);
  box-shadow: inset 0 0 0 1px var(--line);
  transition: box-shadow .3s var(--ease-out);
}
/* Materiał: lewy górny róg w środku okienka, resztą wyjeżdża za kadr. */
.styl-art::after {
  content: '';
  position: absolute;
  left: 32%; top: 32%; right: -34%; bottom: -34%;
  border-radius: 15px;
  background: #FFFFFF;
  box-shadow: -2px -2px 7px rgba(13, 13, 13, 0.22);
}

.styl-name {
  font-family: 'Archivo Narrow', system-ui, sans-serif;
  font-size: 8.5px;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  text-align: center;
  color: var(--ink-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: color .25s;
}

/* Wybrany kafel: czarna obwódka plus ciemny włos na zewnątrz (do rundy 15
   obwódka była błękitna — kolor wypadł z całej apki).
   Ta sama obwódka obsługuje obie siatki kafli: styl i urządzenie. */
.styl-tile.is-on .styl-art,
.styl-tile[aria-pressed="true"] .styl-art {
  box-shadow:
    inset 0 0 0 2.5px var(--akcent),
    0 0 0 1px rgba(13, 13, 13, 0.22);
}
.styl-tile.is-on .styl-name,
.styl-tile[aria-pressed="true"] .styl-name { color: var(--ink); }

/* ── warianty stylu (kolejność jak w data-styl) ─────────────────────────── */

/* Szkło: gruby półprzezroczysty pierścień po zewnętrznej krawędzi. */
.styl-tile[data-styl="glass-light"] .styl-art::after {
  box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.62), -2px -2px 7px rgba(13, 13, 13, 0.2);
}
.styl-tile[data-styl="glass-dark"] .styl-art::after {
  box-shadow: 0 0 0 4px rgba(13, 13, 13, 0.42), -2px -2px 7px rgba(13, 13, 13, 0.2);
}
/* Kontur: cienka ciemna linia z papierowym prześwitem. */
.styl-tile[data-styl="outline"] .styl-art::after {
  box-shadow: 0 0 0 3px #FFFFFF, 0 0 0 4.2px rgba(13, 13, 13, 0.5);
}
/* Rama: solidna czerń. */
.styl-tile[data-styl="border"] .styl-art::after {
  box-shadow: 0 0 0 4px var(--black);
}
/* Karta: białe passe-partout, oddzielone od materiału włosem. */
.styl-tile[data-styl="card"] .styl-art::after {
  box-shadow:
    0 0 0 1px rgba(13, 13, 13, 0.13),
    0 0 0 8px #FFFFFF,
    0 0 0 9px rgba(13, 13, 13, 0.10),
    -3px -3px 9px rgba(13, 13, 13, 0.18);
}
/* Stos: arkusze pod materiałem. Pierwszy arkusz kładzie ::before, drugi jego
   własny box-shadow z odsunięciem — jeden pseudoelement wystarcza na dwie
   warstwy pod materiałem. (Wariant z JEDNYM arkuszem, data-styl="stack",
   wypadł z UI w rundzie 4 — w stanie zostaje jako wartość legacy.) */
.styl-tile[data-styl="stack2"] .styl-art::before {
  content: '';
  position: absolute;
  left: 25%; top: 25%; right: -27%; bottom: -27%;
  border-radius: 15px;
  background: #FFFFFF;
  box-shadow: inset 0 0 0 1px rgba(13, 13, 13, 0.10);
}
.styl-tile[data-styl="stack2"] .styl-art::before {
  box-shadow:
    inset 0 0 0 1px rgba(13, 13, 13, 0.10),
    -7px -7px 0 0 #FFFFFF,
    -7px -7px 0 1px rgba(13, 13, 13, 0.10);
}
/* Retro: czarna rama i twardy cień-blok, bez rozmycia. */
.styl-tile[data-styl="retro"] .styl-art::after {
  box-shadow: 0 0 0 3px var(--black), -7px -7px 0 0 rgba(13, 13, 13, 0.5);
}
/* Okno: pasek u góry i trzy kropki. Wszystko idzie w background-image
   materiału (cztery warstwy: trzy kropki + pasek z kreską), bo ::before jest
   już zajęte przez arkusze stosu, a ::after przykryłoby dowolną nakładkę.
   Kolory kropek i ton paska są WPROST z render.js (OKNO_KROPKI, fill
   #ECE7E1) — miniatura ma pokazywać to, co naprawdę się narysuje. */
.styl-tile[data-styl="okno"] .styl-art::after {
  background-image:
    radial-gradient(circle 1.7px at 10px 9.5px, #FF5F57 98%, transparent 0),
    radial-gradient(circle 1.7px at 16px 9.5px, #FEBC2E 98%, transparent 0),
    radial-gradient(circle 1.7px at 22px 9.5px, #28C840 98%, transparent 0),
    linear-gradient(#ECE7E1 0 19px, rgba(13, 13, 13, 0.10) 19px 20px, transparent 20px);
  background-repeat: no-repeat;
}

/* ── LEWA: kafle urządzeń (#deviceGroup) ────────────────────────────────────
   Ta sama siatka i ten sam kafel co w stylu (klasy .styl-tile / .styl-art —
   wybór urządzenia to dokładnie ta sama decyzja co wybór ramki, więc ma
   wyglądać tak samo). Różni się miniatura: zamiast POWIĘKSZONEGO NAROŻNIKA
   pokazujemy CAŁĄ bryłę, bo o urządzeniu decyduje sylwetka, a nie krawędź.

   Rysunek robi SVG w markupie, nie pseudoelementy (runda 8) — powód i liczby
   przy samym SVG w index.html. Tutaj zostaje tylko kasowanie narożnika
   dziedziczonego ze stylu i paleta. */

.dev-art::after { content: none; }

.dev-art > svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

/* Korpus, ekran, podstawa. Te same trzy tony w obu obudowach, żeby kafle
   czytały się jako jedna rodzina. */
.dev-korpus { fill: var(--black); }
.dev-ekran { fill: #FFFFFF; }
/* Podstawa musi być CIEMNIEJSZA od jasnego rogu gradientu na kaflu (#FFFFFF
   w 52%), inaczej znika i laptop wygląda, jakby stał na niczym. */
.dev-baza { fill: #CBC4BA; }
.dev-wciecie { fill: rgba(13, 13, 13, 0.30); }

/* Cień pod bryłą — jedyne, co odkleja sprzęt od srebrnego tła kafla. */
.dev-cien { filter: drop-shadow(0 1.4px 2.2px rgba(13, 13, 13, 0.26)); }

/* ── LEWA: kafle ruchu (#ruchGroup) ─────────────────────────────────────────
   RUSZAJĄCA SIĘ MINIATURA. Nazwa nie mówi, czym „Obrót" różni się od
   „Kołysania" — a kafel, który sam się kołysze, mówi to bez jednego słowa.
   Miniatura to biała karteczka w okienku takim samym jak przy stylach, tylko
   kwadratowym: tu liczy się RUCH, nie krawędź, więc karteczka jest cała
   widoczna i ma miejsce, żeby się poruszyć.

   Ruch idzie CSS-em, nie canvasem: to cztery keyframes na transformie, koszt
   bliski zeru, a canvas wymagałby pętli rAF w kolumnie, która i tak przewija
   się obok pracującego podglądu. Fazy są celowo te same co w silniku
   (js/render.js/RUCH_PROFILE): sinus w poziomie, sinus o podwójnej
   częstotliwości w pionie — czyli ósemka, nie wahadło.

   Wszystkie animacje stoją przy prefers-reduced-motion (blok na końcu pliku
   wyłącza je globalnie). */

.ruch-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px 8px;
  margin-bottom: 16px;
}

.ruch-tile {
  display: flex;
  flex-direction: column;
  gap: 5px;
  border: 0;
  padding: 0;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform .4s var(--ease-spring);
}
.ruch-tile:hover { transform: translateY(-2px); }
.ruch-tile:active { transform: translateY(0) scale(0.985); }
.ruch-tile:focus-visible { outline: 2px solid var(--black); outline-offset: 2px; }

.ruch-art {
  position: relative;
  display: block;
  aspect-ratio: 3 / 2;
  border-radius: 8px;
  overflow: hidden;
  background: linear-gradient(155deg, #EAE4DC 0%, #FFFFFF 52%, #ECE6DF 100%);
  box-shadow: inset 0 0 0 1px var(--line);
  transition: box-shadow .3s var(--ease-out);
  /* Perspektywa na pojemniku, żeby rotateY w środku był rzutem, a nie
     zwykłym spłaszczeniem. */
  perspective: 220px;
}
/* Karteczka: prostokąt 9:16 wyśrodkowany w okienku — sylwetka zrzutu
   z telefonu, czyli tego, co najczęściej tu ląduje. */
.ruch-art::after {
  content: '';
  position: absolute;
  left: 50%; top: 50%;
  width: 24%; height: 62%;
  margin: 0;
  transform: translate(-50%, -50%);
  border-radius: 2.5px;
  background: #FFFFFF;
  box-shadow: 0 1.5px 4px rgba(13, 13, 13, 0.28);
}

.ruch-name {
  font-family: 'Archivo Narrow', system-ui, sans-serif;
  font-size: 8.5px;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  text-align: center;
  color: var(--ink-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: color .25s;
}

.ruch-tile.is-on .ruch-art,
.ruch-tile[aria-pressed="true"] .ruch-art {
  box-shadow:
    inset 0 0 0 2.5px var(--akcent),
    0 0 0 1px rgba(13, 13, 13, 0.22);
}
.ruch-tile.is-on .ruch-name,
.ruch-tile[aria-pressed="true"] .ruch-name { color: var(--ink); }

/* „Brak" to jedyny kafel, który stoi — i to jest jego cały opis. */
.ruch-tile[data-ruch="kolysanie"] .ruch-art::after { animation: ruchKolysanie 3.4s var(--ease-io) infinite; }
.ruch-tile[data-ruch="unoszenie"] .ruch-art::after { animation: ruchUnoszenie 3s var(--ease-io) infinite; }
.ruch-tile[data-ruch="obrot"] .ruch-art::after { animation: ruchObrot 3.8s var(--ease-io) infinite; }
.ruch-tile[data-ruch="zblizenie"] .ruch-art::after { animation: ruchZblizenie 3.4s var(--ease-io) infinite; }

@keyframes ruchKolysanie {
  0%, 100% { transform: translate(-50%, -50%) rotateY(0deg) rotate(0deg); }
  25%      { transform: translate(-50%, -50%) rotateY(20deg) rotate(1.6deg); }
  75%      { transform: translate(-50%, -50%) rotateY(-20deg) rotate(-1.6deg); }
}
@keyframes ruchUnoszenie {
  0%, 100% { transform: translate(-50%, -50%) translateY(0); }
  50%      { transform: translate(-50%, -50%) translateY(-13%); }
}
@keyframes ruchObrot {
  0%, 100% { transform: translate(-50%, -50%) rotateY(0deg) rotate(0deg); }
  25%      { transform: translate(-50%, -50%) rotateY(46deg) rotate(3deg); }
  75%      { transform: translate(-50%, -50%) rotateY(-46deg) rotate(-3deg); }
}
@keyframes ruchZblizenie {
  0%, 100% { transform: translate(-50%, -50%) scale(1); }
  50%      { transform: translate(-50%, -50%) scale(1.16); }
}

/* ── LEWA: plansze XY (pochylenie, kierunek cienia) ─────────────────────────
   Plansza i uchwyt-romb przeniesione z EFFECTS (app.css ~1247–1310), przełożone
   na jasny motyw: płótno na bieli z fakturą arkusza, krzyż osi ciemny, uchwyt
   ciemny z papierową obwódką (musi się czytać i na środku, i przy krawędzi).
   Obsługę wskaźnika dokłada main.js (wspólny padController). */

/* Plansza stoi pod swoim podpisem, a nie na środku pustej szerokości: cała
   kolumna czyta się od lewej krawędzi (etykiety, suwaki, kliny), więc
   wyśrodkowany kwadrat wypadał z tej osi. */
.pad-wrap { display: flex; justify-content: flex-start; }

/* ZASIĘG CIENIA (tylko plansza kierunku). Odsunięcie cienia jest liczone
   z wektora, którego DŁUGOŚĆ klamrujemy do 1 (patrz shadowGeom w render.js),
   więc realnie osiągalny obszar to elipsa wpisana w planszę, a nie cały
   prostokąt. Rysujemy ją kreską: bez niej rogi planszy wyglądają na miejsce,
   w którym cień jedzie najdalej, a tam nic już się nie zmienia. */
.pad--kierunek::after {
  content: '';
  position: absolute;
  inset: 8px;
  border: 1px dashed rgba(13, 13, 13, 0.16);
  border-radius: 50%;
  pointer-events: none;
}

/* PROSTOKĄT NA CAŁĄ SZEROKOŚĆ PANELU (runda 15, prośba Kuby). Do tej pory
   plansza była kwadratem 168 px — na 340-pikselowej kolumnie zostawiała
   połowę szerokości pustą, a każdy piksel ruchu myszą przesuwał wartość
   o 1/168 zakresu. Przy cieniu narożnym to było za grubo: „wystarczy ruszyć
   o jeden piksel, a cień przemieszcza się na drugą stronę mockupu".
   Teraz plansza bierze całą szerokość (304 px przy --col 340), a proporcja
   8:5 daje 190 px wysokości — czyli OBIE osie są dokładniejsze niż przedtem,
   nie tylko pozioma. Na mobile zostaje kwadrat (patrz sekcja TELEFON): tam
   kolumna jest wąska, a wysokość ekranu jest walutą, której nie ma na zbyciu. */
.pad {
  position: relative;
  width: 100%;
  max-width: none;
  aspect-ratio: 8 / 5;
  border: 1px solid var(--line-strong);
  /* Ostre rogi i faktura technicznego płótna: miejsce pracy, nie panel
     z zabawkami. */
  border-radius: 0;
  background:
    linear-gradient(rgba(13, 13, 13, 0.10), rgba(13, 13, 13, 0.10)) 50% 0 / 1px 100% no-repeat,
    linear-gradient(rgba(13, 13, 13, 0.10), rgba(13, 13, 13, 0.10)) 0 50% / 100% 1px no-repeat,
    var(--sheet) repeat,
    var(--panel-lift);
  cursor: crosshair;
  /* Ciągnięcie palcem nie może przewijać kolumny. */
  touch-action: none;
  -webkit-tap-highlight-color: transparent;
  transition: border-color .25s;
}
.pad:hover { border-color: rgba(13, 13, 13, 0.3); }
.pad:focus-visible { outline: 2px solid var(--black); outline-offset: 2px; }

/* Uchwyt to ROMB (obrócony kwadrat), nie kółko — spójnie z klinami. */
.pad-dot {
  position: absolute;
  left: 50%; top: 50%;
  width: 13px;
  height: 13px;
  transform: translate(-50%, -50%) rotate(45deg);
  background: var(--black);
  border: 2px solid var(--paper);
  box-shadow: 0 0 0 1px rgba(13, 13, 13, 0.45), 0 1px 5px rgba(13, 13, 13, 0.25);
  pointer-events: none;
}
/* W trakcie ciągnięcia (klasa z main.js) romb robi się błękitny. */
.pad.is-held .pad-dot { background: var(--akcent-hover); }

/* ── LEWA: eksport ──────────────────────────────────────────────────────── */

/* Eksport i stopka są STAŁYM dnem kolumny, poza pudełkiem przewijania (patrz
   komentarz w index.html). Wcześniej sekcja stała `position: sticky` w środku
   przewijanej treści i przy oknie ~900 px przykrywała dolny rząd kafli
   urządzeń: kafle urywały się w połowie i wyglądało to na błąd rysowania.
   Kreska u góry oddziela dno od treści, która pod nie wjeżdża. */
.dock-export {
  flex: none;
  z-index: 12;
  background: var(--panel);
  border-top: 1px solid var(--line);
  padding: 16px var(--dock-pad) 12px;
}
.dock-export .block-head { margin-bottom: 12px; }

.export-row {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
/* Format zostaje po lewej, „Pobierz" dosuwa się do prawej krawędzi kolumny.
   Auto-margines zamiast space-between: po wypadnięciu kopiowania (runda 5)
   w wierszu bywa jeden element i space-between nie miałoby czego rozstawiać. */
.export-row #btnDownload { margin-left: auto; }

/* ZNACZEK FORMATU WIDEO (.fmt-badge) WYPADŁ W RUNDZIE 15 (uwaga Kuby:
   „niepotrzebny jest przycisk MP4 przy eksporcie wideo, skoro nie ma innej
   opcji"). Format wybiera przeglądarka, nie użytkownik — a plakietka, która
   niczego nie wybiera, wygląda jak zepsuty przycisk. Gdy plik wychodzi
   czymś innym niż MP4, mówi o tym toast po eksporcie (js/wideo.js). */

/* Pasek postępu: cienka kreska na całą szerokość kolumny. Szerokość
   wypełnienia bierze --p (JS ustawia je na pasku), a inline width na dziecku
   też zadziała — inline bije zmienną. */
.export-progress {
  position: relative;
  height: 3px;
  background: rgba(13, 13, 13, 0.12);
  overflow: hidden;
}
.export-fill {
  height: 100%;
  width: var(--p, 0%);
  background-color: var(--akcent);
  transition: width .18s linear;
}

.export-eta {
  margin: 8px 0 0;
  font-family: 'Archivo Narrow', system-ui, sans-serif;
  font-size: 11px;
  letter-spacing: 0.02em;
  color: var(--ink-3);
}

/* Stopka: dwa zdania, które mają dać się przeczytać, a nie tylko zaznaczyć
   swoją obecność. Przy 9 px i 28% krycia były na granicy widoczności. */
.dock-foot {
  /* Od rundy 5 stopka leży poza pudełkiem przewijania, razem z eksportem, więc
     boczny padding musi wziąć na siebie (kolumna go już nie daje). Kreski
     u góry nie ma: oddziela już sekcja eksportu tuż nad nią. */
  flex: none;
  padding: 4px var(--dock-pad) var(--dock-pad-b);
  font-family: 'Archivo Narrow', system-ui, sans-serif;
  font-size: 10px;
  line-height: 1.7;
  letter-spacing: 0.02em;
  color: var(--ink-3);
}

/* ── PRAWA: tła w trzech kategoriach ────────────────────────────────────────
   Strukturę buduje JS: .bg-cats > .bg-cat > (.bg-cat-head z tytułem
   i chevronem .bg-cat-more) + .bg-cat-grid z kaflami .bg-tile.

   Zwinięta kategoria pokazuje JEDEN rząd, czyli trzy kafle — reszta czeka pod
   chevronem. Dziesięć gradientów plus kolory plus efekty w jednej płachcie
   byłoby dłuższe niż cała reszta aplikacji, a i tak zawsze wybiera się jedno.
   Chevron obraca się o 90° (wzór: rozkładane kategorie na mobile w EFFECTS);
   obracamy CAŁY przycisk, a nie ikonę w środku, żeby zadziałało niezależnie
   od tego, czy JS wsadzi tam <svg>, czy zwykły znak. */

.bg-cats { display: flex; flex-direction: column; gap: 16px; }

.bg-cat { display: flex; flex-direction: column; gap: 9px; }

.bg-cat-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  /* Cały nagłówek jest chwytem: klikanie w sam chevron wymagałoby celowania. */
  cursor: pointer;
  border: 0;
  background: none;
  padding: 0;
  font-family: 'Archivo Narrow', system-ui, sans-serif;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-3);
  transition: color .25s;
}
.bg-cat-head:hover { color: var(--ink); }
.bg-cat-head:focus-visible { outline: 2px solid var(--black); outline-offset: 2px; }

.bg-cat-more {
  display: grid;
  place-items: center;
  flex: none;
  width: 26px;
  height: 22px;
  border: 0;
  padding: 0;
  background: none;
  color: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: transform .25s var(--ease-out), color .25s;
}
.bg-cat-more svg { width: 13px; height: 13px; display: block; }
.bg-cat-more:focus-visible { outline: 2px solid var(--black); outline-offset: 1px; }
.bg-cat.is-rozwinieta .bg-cat-more { transform: rotate(90deg); }

.bg-cat-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px 8px;
}
.bg-cat-grid > * { min-width: 0; }
/* Zwinięta: zostaje pierwszy rząd. */
.bg-cat:not(.is-rozwinieta) .bg-cat-grid > *:nth-child(n + 4) { display: none; }

/* Kontrolka wygaszona przez JS (klasa .is-locked). Suwak zostaje w układzie,
   ale widać, że nic nie zmienia — tak samo jak przy „Rogach" pod obudową. */
.control.is-locked { opacity: .42; }
.control.is-locked .slider { pointer-events: none; }

/* ── PRAWA: TWOJE TŁO ───────────────────────────────────────────────────────
   Kafle dziedziczą po `.bg-tile`, więc kształt, promień, cień zaznaczenia
   i podpis są DOKŁADNIE takie jak w kategoriach niżej — sekcja ma się czytać
   jako pierwsza z nich. Tu zostają tylko różnice. */

.bg-cat--wlasne { margin-bottom: 2px; }

/* Nagłówek bez chevronu: to <p>, nie przycisk, bo nie ma czego rozwijać.
   Kasujemy więc wskaźnik i odstęp, które `.bg-cat-head` bierze pod kliknięcie. */
.bg-cat-head--static { margin: 0; cursor: default; }

/* Kafel wgrywania: dwie kolumny szerokości, wysokość z rzędu (kwadratowa
   próbka koloru ją wyznacza) — dlatego kasujemy odziedziczone `aspect-ratio`.
   Selektor DWUKLASOWY, bo blok `.bg-tile` stoi w pliku niżej i przy równej
   specyficzności to on by wygrał — kafel wychodził wtedy dwa razy za wysoki. */
.bg-tile.bg-drop {
  grid-column: span 2;
  aspect-ratio: auto;
  align-items: center;
  /* Przerywana krawędź MUSI być outline, nie box-shadow — cień nie zna kreski.
     Do środka, żeby nie rozpychała siatki. */
  outline: 1.5px dashed rgba(13, 13, 13, 0.28);
  outline-offset: -3px;
  box-shadow: none;
  color: var(--ink-3);
  transition: outline-color .25s var(--ease-out), color .25s var(--ease-out),
              transform .4s var(--ease-spring);
}
.bg-tile.bg-drop:hover,
.bg-tile.bg-drop.is-over { outline-color: var(--akcent); color: var(--ink-2); }
.bg-tile.bg-drop.is-on { outline-color: var(--akcent); box-shadow: none; }

/* Ikona siedzi w środku wolnego pola NAD podpisem — podpis stoi tam, gdzie
   we wszystkich kaflach tła, czyli przy dolnej krawędzi. */
.bg-drop-srodek {
  position: absolute;
  inset: 0 0 18px;
  display: grid;
  place-items: center;
  pointer-events: none;
}
.bg-drop-ico { width: 21px; height: 21px; display: block; }

/* Podgląd wgranego tła — pod ikoną i podpisem, na całym kafelku. */
.bg-drop-podglad {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
}
/* Z obrazem podpis i ikona muszą być czytelne na czymkolwiek — stąd przesłona. */
.bg-tile.bg-drop.ma-obraz { color: #FFFFFF; outline-color: rgba(255, 255, 255, 0.7); }
.bg-drop.ma-obraz .bg-drop-podglad::after {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(13, 13, 13, 0.34);
}
.bg-tile.bg-drop.ma-obraz:hover { outline-color: #FFFFFF; }

/* Próbka koloru: natywny input rozciągnięty na cały kafelek — tylko on otwiera
   systemowy próbnik, więc to on musi być polem kliknięcia. */
.bg-tile.bg-kolor { cursor: pointer; padding: 5px; }
.bg-kolor input[type="color"] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  cursor: pointer;
  /* Chrome daje próbce własny margines i ramkę — kasujemy, żeby kolor
     wypełniał kafelek co do piksela. */
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
}
.bg-kolor input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
.bg-kolor input[type="color"]::-webkit-color-swatch { border: 0; border-radius: 8px; }

.bg-wlasne-kasuj {
  display: block;
  margin: 2px 0 0;
  padding: 0;
  border: 0;
  background: none;
  font-family: 'Archivo Narrow', system-ui, sans-serif;
  font-size: 10px;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--ink-3);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.bg-wlasne-kasuj:hover { color: var(--black); }

.bg-tile {
  position: relative;
  isolation: isolate;
  aspect-ratio: 1;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  /* Plakietka centruje się i kurczy do treści (patrz .bg-tile-name) —
     rozciągnięta na całą szerokość stykała się z obwódką wyboru. */
  align-items: center;
  border: 0;
  /* 6 px zamiast 5: obwódka wyboru ma 3 px i przy 5 zostawały 2 px prześwitu
     do plakietki — czytało się jako sklejone. */
  padding: 6px;
  border-radius: 8px;
  overflow: hidden;
  background: var(--panel-lift);
  box-shadow: inset 0 0 0 1px var(--line);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform .4s var(--ease-spring), box-shadow .3s var(--ease-out);
  opacity: 0;
  animation: tileIn .5s var(--ease-out) forwards;
}
@keyframes tileIn {
  from { opacity: 0; transform: translateY(10px) scale(0.95); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
.bg-tile:hover { transform: translateY(-2px); }
.bg-tile:active { transform: translateY(0) scale(0.985); }
.bg-tile:focus-visible { outline: 2px solid var(--black); outline-offset: 2px; }

/* Wrapper podglądu: na desktopie przezroczysty dla układu, na mobile robi
   pełną szerokość i kotwicę dla kafli akcji (blok mobile niżej). */
.viewer-wrap { display: contents; }

/* Kafle akcji przy kadrze, lupa na kadrze i X pełnego podglądu istnieją
   tylko na mobile — bazowo schowane. */
.side-actions { display: none; }
.kadr-lupa { display: none; }
.podglad-zamknij { display: none; }
body.podglad-duzy .podglad-zamknij {
  display: grid;
  place-items: center;
  position: fixed;
  top: calc(var(--safe-top) + 10px);
  right: 10px;
  z-index: 50;
  width: 42px;
  height: 42px;
  border: 0;
  border-radius: 50%;
  background: rgba(13, 13, 13, 0.82);
  color: var(--paper);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.podglad-zamknij svg { width: 18px; height: 18px; }

/* Rzeczy, które na telefonie tylko kradną wysokość podglądu. */
.desktop-only { display: block; }

/* Kafel przezroczysty: szachownicę rysuje pseudoelement, nie tło. JS ustawia
   tła kafli w inline style, a inline `background` skasowałoby obrazek — do
   ::after inline nie dosięgnie. */
.bg-tile--trans::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: var(--checker);
  background-size: 12px 12px;
}

/* Podpis: papierowa plakietka. Siedzi na dole kafla, gdy JS wsadzi ją do
   środka (selektor z >), i sprawdza się też jako osobny wiersz pod kaflem —
   nie wiemy, którą wersję zbuduje moduł, a plakietka działa w obu. */
.bg-tile-name {
  display: block;
  max-width: 100%;
  padding: 2px 5px;
  background: rgba(255, 255, 255, 0.92);
  color: var(--ink);
  font-family: 'Archivo Narrow', system-ui, sans-serif;
  font-size: 8px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.bg-tile > .bg-tile-name { position: relative; z-index: 1; }

/* Wybrany kafel: gruba błękitna obwódka (rysowana na ::before — patrz koniec
   arkusza) i plakietka przemalowana na czysty błękit, żeby wybór był widoczny
   także w miniaturze. Obsługujemy .is-on i aria-pressed, bo w EFFECTS kafle
   chodziły na aria-pressed. */
.bg-tile.is-on,
.bg-tile[aria-pressed="true"] { transform: translateY(-2px); }
.bg-tile.is-on .bg-tile-name,
.bg-tile[aria-pressed="true"] .bg-tile-name {
  background-color: var(--akcent);
  color: var(--paper);
}

.note {
  font-family: 'DM Sans', system-ui, sans-serif;
  font-size: 12px;
  line-height: 1.6;
  letter-spacing: 0;
  color: var(--ink-3);
  text-wrap: pretty;
}
/* Podpowiedź pod narzędziami (Privacy): mówi, gdzie wykonać gest, więc stoi
   blisko kafelków i jest cichsza od notki w kolumnie teł. */
.note--hint {
  margin-top: -3px;
  font-size: 11.5px;
  line-height: 1.5;
  /* Ton --ink-4 (28% krycia) czytał się jako tekst wyłączony, a to jest
     instrukcja obsługi narzędzia — musi dać się przeczytać za pierwszym
     razem. Cichszy od etykiet, głośniejszy od stopki. */
  color: var(--ink-3);
}

/* ── komunikat ──────────────────────────────────────────────────────────── */

/* KLIN, NIE PIGUŁKA (runda 15, decyzja Kuby). Komunikat był czarną pastylką,
   a przy błędzie robił się jasnoniebieski — jedyne miejsce w apce, gdzie
   pojawiał się kolor, i akurat to, które ma być najspokojniejsze. Teraz jest
   BIAŁY KLIN Z CZARNYM NAPISEM, czyli ten sam kształt co przyciski „Cięcie":
   komunikat wygląda na część systemu, a nie na powiadomienie z innej aplikacji.

   Kształt idzie clip-path — tym samym wielokątem co .tp--single, tylko
   z mocniejszym pochyleniem (toast jest niski i szeroki, więc przy --lean 7%
   ścięcie byłoby ledwo widoczne). Cień nie może iść po box-shadow: pod
   clip-path box-shadow jest ODCINANY razem z kształtem. Kładziemy go więc
   filtrem na pojemniku — drop-shadow liczy się z sylwetki PO przycięciu,
   więc obrysowuje klin, a nie prostokąt. */
.toast {
  position: fixed;
  left: 50%; bottom: 26px;
  transform: translate(-50%, 24px);
  background: #FFFFFF;
  color: var(--black);
  font-family: 'Archivo Narrow', system-ui, sans-serif;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  max-width: min(560px, calc(100vw - 32px));
  text-align: center;
  padding: 13px 26px;
  border-radius: 0;
  clip-path: polygon(0 9px, 100% 0, 100% 100%, 0 calc(100% - 9px));
  filter: drop-shadow(0 2px 4px rgba(13, 13, 13, 0.22))
          drop-shadow(0 12px 34px rgba(13, 13, 13, 0.28));
  opacity: 0;
  pointer-events: none;
  z-index: 60;
  transition: opacity .35s var(--ease-out), transform .45s var(--ease-spring);
}
.toast.is-on { opacity: 1; transform: translate(-50%, 0); }
/* Błąd wygląda tak samo jak każdy inny komunikat — treść mówi, co się stało,
   i to wystarczy. Reguła zostaje pusta z premedytacją: JS dalej wiesza klasę
   .is-error, a tu jest jedno miejsce, w którym można by jej kiedyś użyć. */
/* Toast aktualizacji: OSOBNY element (skrypt SW w index.html), bo zwykły
   #toast ma timer, który by go zgasił. Wisi piętro nad zwykłym toastem,
   jest przyciskiem i jako jedyny łapie wskaźnik. */
.toast--update {
  bottom: 74px;
  border: 0;
  pointer-events: auto;
  cursor: pointer;
}

/* ═══════════════════════════════ TRYBY ═══════════════════════════════════
   Klasę mode-foto / mode-wideo przełącza JS na <body>.

   Za pokazywanie i chowanie odpowiada arkusz, nie atrybut (tak stoi
   w ARCHITEKTURA.md), dlatego reguły są mocniejsze od [hidden]: w trybie
   zdjęcia pasek wideo nie wyjdzie nigdy, a w trybie wideo wyjdzie nawet
   wtedy, gdy nikt nie zdjął atrybutu z HTML-a.

   Furtka dla modułu wideo: jeśli pasek ma być schowany, dopóki nie ma
   wgranego nagrania, wystarczy dołożyć <body> klasę `no-media` — wtedy pasek
   znika niezależnie od trybu. */

/* Eksport wideo leci w czasie rzeczywistym z żywego podglądu — na ten czas
   wygaszamy panele (main.js zapala .is-exporting na <body>). Pasek postępu
   i przycisk Pobierz siedzą w .dock-export, który zostaje klikalny. */
body.is-exporting .dock .block:not(.dock-export),
body.is-exporting .rail,
body.is-exporting .stage-bar,
body.is-exporting .bar .tp-switch {
  pointer-events: none;
  opacity: 0.45;
  transition: opacity .3s var(--ease-out);
}

/* ══════════════ KONFLIKTY FUNKCJI: CO WŁAŚNIE ZAJĘŁO TĘ GAŁKĘ ═════════════
   Renderer ma kilka twardych zasad, których do rundy 5 nie było widać:
     • obudowa urządzenia ZASTĘPUJE ramkę stylu i narzuca własne rogi
       (body.device-on),
     • styl Retro rysuje własny cień, więc panel cienia przestaje mieć wpływ
       (body.cien-wymuszony), tak samo jak przy rodzaju cienia „Brak"
       (body.cien-brak),
   Klasy wiesza main.js. Wszędzie wygląda to tak samo: kontrolka gaśnie i pod
   sekcją wychodzi jedno zdanie, kto ją zajął. Bez tego apka wygląda na
   zepsutą, bo suwak chodzi, a mockup stoi.

   Wygaszamy WYGLĄDEM, nie blokadą kliknięć tam, gdzie kliknięcie coś załatwia:
   kafel stylu MUSI zostać klikalny, bo to on zdejmuje obudowę urządzenia.
   Suwaki nie mają czym oddać takiego gestu, więc im odbieramy zdarzenia
   wskaźnika — dopiero na samym <input>, żeby kursor „not-allowed" z kontrolki
   miał się gdzie pokazać (#ctlRadius dostaje do tego `disabled` od JS).

   Klasa .is-locked robi to samo na dowolnej sekcji, gdyby JS wolał sterować
   ręcznie zamiast klasą na <body>. */
/* Podpis nad siatką kafli („Ramki", „Obudowy"): dzieli jedną listę wyboru na
   dwie rodziny, nie na dwie decyzje. Dlatego jest cichszy od nagłówka sekcji —
   ma porządkować, a nie zapowiadać nowy panel. */
.grid-caption {
  margin: 2px 0 -2px;
  font-family: 'Archivo Narrow', system-ui, sans-serif;
  font-size: 9.5px;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  /* Było --ink-4 (0.28 = 1,9:1 przy 9,5 px) — podpis „Ramki"/„Obudowy" był
     praktycznie niewidoczny, a to on dzieli listę wyboru na dwie rodziny.
     Cichszy od nagłówka sekcji robi się rozstrzeleniem i stopniem, nie
     wygaszeniem do progu widoczności. */
  color: var(--ink-3);
}
.styl-grid + .grid-caption { margin-top: 14px; }

/* Sekcja stylu NIE jest już wygaszana przy włączonej obudowie (runda 5): ramki
   i obudowy stoją w jednej liście z jednym zaznaczeniem, więc nie ma sytuacji
   „kafel świeci się, ale nic nie robi" — zaznaczenie po prostu przeskakuje. */
body.device-on .control--rogi,
.control--rogi.is-locked,
body.cien-wymuszony .block--cien .control--kierunek,
body.cien-wymuszony .block--cien .controls--cien,
body.cien-brak .block--cien .control--kierunek,
body.cien-brak .block--cien .controls--cien,
.control--kierunek.is-locked,
.controls--cien.is-locked {
  opacity: .38;
  cursor: not-allowed;
  transition: opacity .3s var(--ease-out);
}
body.device-on .control--rogi .slider,
.control--rogi.is-locked .slider,
body.cien-wymuszony .block--cien .slider,
body.cien-wymuszony .block--cien .pad,
body.cien-brak .block--cien .slider,
body.cien-brak .block--cien .pad,
.control--kierunek.is-locked .pad,
.controls--cien.is-locked .slider { pointer-events: none; }

/* Rodzaj cienia zostaje czynny nawet przy Retro: to jedyny klin, którym da się
   z tego stanu wyjść (tak samo jak kafel stylu przy obudowie). */
body.cien-wymuszony #shadowTypeGroup,
body.cien-brak #shadowTypeGroup { pointer-events: auto; opacity: 1; }

/* Zdania wyjaśniające: każde ma własny wyzwalacz, żeby przy urządzeniu nie
   wyskakiwało zdanie o cieniu. .is-locked zostaje jako furtka dla JS. */
.note--lock {
  display: none;
  margin-top: -2px;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--ink-3);
}
body.cien-wymuszony .note--lock--retro,
body.cien-brak .note--lock--bezcienia,
/* Runda 16: zdanie o rogach nie wychodziło NIGDY — .note--lock jest domyślnie
   schowane, a wyzwalacza dla obudowy nie było. Efekt: kursant włączał iPhone'a,
   suwak „Rogi" gasł bez słowa wyjaśnienia, a jedyne, co mówiło dlaczego,
   siedziało w atrybucie `title` (na dotyku niedostępnym). */
body.device-on .note--lock--urzadzenie,
.is-locked > .note--lock { display: block; }
/* Retro wymusza cień, więc „bez cienia" nie ma prawa wyjść razem z nim. */
body.cien-wymuszony .note--lock--bezcienia { display: none; }

/* Przycisk wygaszony przez JS (klasa .is-off + aria-disabled). Zostaje
   klikalny, bo to JS decyduje, co powiedzieć na stuknięcie — zabieramy mu
   tylko reakcje na kursor, żeby nie udawał czynnego. */
.tp.is-off { opacity: .45; cursor: not-allowed; }
.tp.is-off:active { transform: none; }
.cut-seg.is-off:hover { color: var(--ink-3); }
.cut-seg.is-off.is-on:hover { color: var(--paper); }
.cut-seg.is-off:not(.is-on):hover::before,
.cut-seg.is-off:not(.is-on):hover::after { background: transparent; }
.wedge-row .cut-seg.is-off:not(.is-on):hover::after { background: rgba(13, 13, 13, 0.055); }

.mode-foto #videoBar { display: none !important; }

/* WYSOKOŚĆ, KTÓRĄ ZABIERA OŚ CZASU (patrz --os-h przy :root).
   92 px = przerwa .stage-col (22) + margines osi (16) + tor (34) + odstęp (7)
   + stopka osi (~13). Liczby są z jednego miejsca — gdy któraś się zmieni,
   trzeba poprawić i tam, i tu.
   --frame-max-h i --frame-inner-h muszą zostać PRZELICZONE na <body>: token
   z :root ma już podstawione stare --os-h (patrz komentarz przy :root). */
body.mode-wideo {
  --os-h: 92px;
  --frame-max-h: calc(100svh - var(--bar-h) - var(--safe-top) - 28px - var(--stage-foot) - var(--os-h));
  --frame-inner-h: calc(var(--frame-max-h) - 2 * var(--frame-pad));
}
/* Bez wgranego nagrania osi nie ma (patrz .os wyżej), więc i wysokości nie
   zabiera — inaczej pusty kadr w trybie wideo byłby bez powodu niższy. */
body.mode-wideo.no-media { --os-h: 0px; }

/* RUCH ISTNIEJE TYLKO W WIDEO. W zdjęciu eksport jest nieruchomym PNG, więc
   sekcja nie ma czego obiecywać — chowamy ją całą, zamiast wygaszać. */
.mode-foto .block--ruch { display: none !important; }
.mode-wideo .fmt-switch { display: none !important; }
/* Kopiowanie do schowka: tylko zdjęcie (dla wideo schowek nie ma formatu). */
.mode-wideo #btnCopy { display: none !important; }
/* Wyrównanie do „Pobierz": tp--sm ma 28 px, a dwa różne wzrosty w jednym
   wierszu wyglądały jak błąd. */
.export-row #btnCopy { min-height: 36px; }

/* Animacja mockupu wypadła w rundzie 8 (decyzja Kuby: ruch był tak łagodny,
   że nie było go widać, a w zamian komplikował eksport — zdjęcie z animacją
   zapisywało się jako wideo). Razem z nią wyleciały: sekcja z kaflami,
   moduł js/anim-eksport.js i cała gałąź eksportu do MP4 z pojedynczego zrzutu. */

/* Kopiowanie do schowka wypadło w rundzie 5 (decyzja Kuby): schowek systemowy
   i tak nie przyjmował nagrania, a w trybie zdjęcia dwie akcje końcowe obok
   siebie rozmywały to, po co się tu przychodzi. Została jedna: Pobierz. */

.mode-wideo #videoBar { display: flex !important; }
body.no-media #videoBar { display: none !important; }

/* ═══════════════════════════════ TABLET ══════════════════════════════════ */

/* Węższe okno tylko zwęża kolumny boczne — układ zostaje ten sam, bo
   ustawienia po lewej i tła po prawej to sedno tego widoku. */
@media (max-width: 1100px) {
  :root { --col: 252px; --dock-pad: 14px; --dock-pad-b: 24px; }
  .dock-scroll, .rail-scroll { padding: 18px var(--dock-pad) var(--dock-pad-b); }
  /* W zwężonej kolumnie format i „Pobierz" nie mieszczą się w jednym wierszu,
     więc każde bierze własny: muszka PNG|JPG u góry, pod nią pełnej szerokości
     przycisk pobierania. Wychodzi z tego czytelna kolejność „w czym zapisuję,
     a potem zapisuję". */
  /* Format NIE rozciąga się na całą szerokość: muszka to inline-flex
     z płytką ::before na inset 0, więc flex-basis 100% malował długą pustą
     płytkę obok dwóch małych klinów (reklamacja Kuby z mobile). Wiersze
     łamie pełnoszerokościowy „Pobierz", a kopiowanie i format zostają obok
     siebie w naturalnej szerokości. */
  .export-row .fmt-switch { flex: 0 0 auto; }
  .export-row #btnDownload { flex: 1 0 100%; margin-left: 0; }
  .bg-cat-grid { gap: 8px 6px; }
  .styl-grid { gap: 8px 6px; }
  .wedge-row .tp { padding: 0 2px; font-size: 8px; }
  .ruch-grid { gap: 8px 6px; }
  /* Plansza dalej bierze całą szerokość kolumny (runda 15) — przy --col 252
     wychodzi 224×140 px, czyli i tak więcej pola do manewru niż dawny
     kwadrat 148 px. */
  .pad { aspect-ratio: 8 / 5; }
}

/* ═══════════════════════════════ MOBILE ═══════════════════════════════════
   ≤860 px. Trzy kolumny nie mają tu sensu, więc aplikacja składa się w jeden
   słupek. Trzy zasady, przeniesione z EFFECTS razem z powodami, dla których
   tam powstały:

   1. PODGLĄD ZAWSZE NA OKU. Pasek i scena są przyklejone u góry ekranu,
      przewija się tylko to, co pod nimi. Każde tło, każdy suwak widać
      od razu — bez odjeżdżania palcem w górę po każdej zmianie.
   2. NAJPIERW WYGLĄD, POTEM LICZBY. Zaraz pod sceną idą TŁA, dopiero za nimi
      ustawienia (na desktopie jest odwrotnie, bo tam widać wszystko naraz).
      Na telefonie kolejność sekcji JEST nawigacją: pierwsze pod kciukiem ma
      być to, co zmienia mockup najmocniej i wybiera się jednym stuknięciem,
      a nie suwak zaokrąglenia.
   3. PALEC MA CZTERDZIEŚCI PIKSELI, nie sześć. Pola trafienia rosną, układ
      zostaje — nadmiar oddajemy ujemnym marginesem.

   overflow-x: clip zamiast hidden, bo hidden robi z elementu kontener
   przewijania i psuje position: sticky na iOS.

   Drugi warunek listy: TELEFON W POZIOMIE. Pro Max ma w landscape 932 px
   szerokości, więc sam próg szerokości wpuszczał go w układ trzykolumnowy
   ściśnięty do ~430 px wysokości — kadr wychodził szczątkowy, a kolumny
   przewijały się spod notcha. Dotykowy wskaźnik odróżnia telefon od niskiego
   okna na laptopie, które ma zostać przy desktopie.
   ═══════════════════════════════════════════════════════════════════════ */

@media (max-width: 860px), (max-height: 500px) and (pointer: coarse) {
  html { overflow-x: clip; overscroll-behavior-x: none; }

  /* Wracamy do normalnego przepływu dokumentu. Na desktopie <body> ma
     height:100svh i overflow:hidden, bo tam cała pracownia mieści się
     w jednym ekranie i przewijają się same kolumny. Tutaj przewija się
     STRONA — i tylko wtedy sticky liczy się względem viewportu. */
  body {
    height: auto;
    min-height: 100svh;
    overflow: visible;
    overflow-x: clip;
    max-width: 100vw;
    /* Palec ma prawo do pionu i zoomu. Bez tego ruch odchylony o kilka stopni
       od pionu ciągnął całą stronę w bok i odsłaniał marginesy. */
    touch-action: pan-y pinch-zoom;
    overscroll-behavior-x: none;
  }

  /* Kadr oddaje wysokość, bo pod nim ma się jeszcze zmieścić kciuk i początek
     listy teł. Z --frame-max-h liczy się też sufit szerokości strefy
     upuszczania, więc wystarczy podmienić ją w jednym miejscu. */
  :root {
    --bar-h: 40px;
    --stage-foot: 0px;
    /* 54svh zamiast 46 (reklamacja Kuby: „podgląd w chuj za mały") — resztę
       pikseli oddały odstępy sekcji niżej, nie lista teł.
       Runda 16: 57svh. Trzy zaoszczędzone miejsca: plakietka działu „Tło"
       zamiast osobnej linijki z pustym pasem pod nią, niższe nagłówki
       kategorii (36 → 30 px) i ciaśniejsza góra kolumny. */
    --frame-max-h: 57svh;
    --frame-radius: 14px;
    --os-h: 0px;
    /* Na telefonie kadr ma dojść do krawędzi ekranu, więc oprawa jest
       cieńsza — ma tylko powiedzieć „to okno, nie plik". */
    --frame-pad: 6px;
  }

  /* Reguła z desktopu (body.mode-wideo odejmuje wysokość osi czasu) ma
     wyższą specyficzność niż :root, więc bez tego przywrócenia przebiłaby
     mobilne 54svh. A tu odejmować nie ma czego: strona się PRZEWIJA, więc
     nic nie wypada pod krawędź ekranu. */
  body.mode-wideo {
    --os-h: 0px;
    --frame-max-h: 57svh;
    --frame-inner-h: calc(var(--frame-max-h) - 2 * var(--frame-pad));
  }

  .workspace {
    display: flex;
    flex-direction: column;
    height: auto;
    min-height: 100svh;
    overflow: visible;
    grid-template-columns: none;
    grid-template-rows: none;
    grid-template-areas: none;
  }

  /* ── pasek: 44 px, logo i tryb ──────────────────────────────────────── */
  .bar {
    order: 0;
    position: sticky;
    top: 0;
    z-index: 30;
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* Wcięcie systemowe doliczone do wysokości: w zainstalowanej PWA pasek
       zaczyna się POD zegarem i Dynamic Island, nie pod nimi w środku. */
    height: calc(var(--bar-h) + var(--safe-top));
    padding: var(--safe-top) 10px 0;
    gap: 8px;
  }
  /* Logo ma prawo się skurczyć, przełącznik trybu NIE. Bez tego przy 320 px
     szeroki znak (1163×251) rozpychałby pasek i space-between wypchnęłoby
     switch za prawą krawędź. Kolejność ważności jest tu jednoznaczna: bez
     logo apka działa, bez przełącznika trybu nie. */
  /* Ta sama miara co na desktopie, tylko mniejsza: podajemy WYSOKOŚĆ
     ATRAMENTU (patrz .bar-brand), a nie pudełka. 18,7 px to dokładnie tyle,
     ile znak miał tu przed rundą 15 (pudełko 20 px × 0,9333). */
  .bar-brand { --logo-ink: 18.7px; min-width: 0; flex: 0 1 auto; overflow: hidden; padding: 8px 6px; }
  /* Pusta komórka gridu trzymała switch na osi; we flexie nie ma czego
     równoważyć, a zabierałaby miejsce. */
  .bar-spacer { display: none; }
  .bar .tp-switch { flex: none; }
  /* Przełącznik trybu: 30 px wysokości i węższe kliny (runda 14, uwaga Kuby:
     był nieproporcjonalnie duży względem loga). Cały pasek jest jego polem
     trafienia w pionie, więc palec i tak ma swoje 40 px. */
  .bar .tp-switch .tp { min-width: 58px; min-height: 30px; font-size: 8.5px; padding: 0 10px; }

  /* ── scena: przyklejona POD paskiem ─────────────────────────────────────
     Pasek i scena to dwa piętra tej samej „głowy" aplikacji: pasek stoi na
     zerze, scena podjeżdża pod niego i tam zostaje. Dlatego scena klei się
     na wysokości paska, a nie na zerze — nic się nie przykrywa. */
  .stage {
    order: 1;
    position: sticky;
    top: calc(var(--bar-h) + var(--safe-top));
    z-index: 20;
    padding: 8px 10px 10px;
    border-bottom: 1px solid var(--line);
    /* Cień mówi, że pod spodem coś się przesuwa. */
    box-shadow: 0 14px 22px rgba(13, 13, 13, 0.09);
  }

  /* ── KAFLE AKCJI PRZY KADRZE (wzorzec HVMBAK EFFECTS / EFFECTS-VIDEO) ───
     Pionowa kolumna trapezów przy prawej krawędzi PASA podglądu: wrapper ma
     pełną szerokość, więc przy pionowym zrzucie kafle stoją OBOK kadru,
     a przy poziomym wchodzą na niego szkłem, jak w EFFECTS. Zastępują na
     telefonie „Wgraj swój plik", przyklejony eksport i przycisk kopiowania. */
  .viewer-wrap {
    display: grid;
    place-items: center;
    position: relative;
    width: 100%;
  }
  /* Słupek kafli wychodzi, gdy JEST plik BIEŻĄCEGO trybu albo gdy nie ma go
     wcale (wtedy zostaje samo „Wgraj"). Drugi warunek był na `no-media`,
     czyli „nie ma NAGRANIA" — a to zostawiało dziurę: tryb zdjęcia bez zrzutu,
     ale z wgranym wcześniej wideo, chował cały słupek razem z kaflem
     wgrywania (runda 16, ta sama pomyłka klas co przy „Pobierz" niżej). */
  body.ma-zrodlo .side-actions,
  body:not(.ma-zrodlo) .side-actions {
    display: flex;
    flex-direction: column;
    gap: 5px;
    position: absolute;
    right: 4px;
    top: 50%;
    transform: translateY(-50%);
    z-index: 6;
  }
  /* Bez pliku zostaje samo „wgraj" — reszta nie ma na czym pracować.
     BŁĄD Z RUNDY 14, NAPRAWIONY W 16: warunkiem było `body.no-media`, a ta
     klasa znaczy „nie ma NAGRANIA", nie „nie ma pliku" (main.js:
     `toggle('no-media', !state.sources.wideo)`). W trybie ZDJĘCIA nagrania
     nigdy nie ma, więc na telefonie kafel „Pobierz" — i do rundy 16 także
     powiększenie — były schowane ZAWSZE, mimo wgranego zrzutu. Jedyną drogą
     do pliku zostawał eksport na samym dole kolumny.
     Właściwy warunek to `ma-zrodlo` (źródło BIEŻĄCEGO trybu). */
  body:not(.ma-zrodlo) #saDownload { display: none; }

  /* ── LUPA NA KADRZE (runda 16) ──────────────────────────────────────────
     Siedzi w prawym DOLNYM rogu podglądu, czyli tam, gdzie nie zasłania ani
     paska stanu na zrzucie, ani nazwy pliku. Papierowe szkło jak kafle akcji,
     tylko mniejsze — to podpowiedź, nie akcja główna. Klin ścięty od dołu,
     żeby czytał się jako część rodziny „Cięcie", a nie jako guzik systemowy.
     Bez pliku znika: nie ma czego powiększać. */
  body.ma-zrodlo .viewer .kadr-lupa {
    display: grid;
    place-items: center;
    position: absolute;
    right: 8px;
    bottom: 8px;
    z-index: 7;
    width: 34px;
    height: 34px;
    border: 0;
    border-radius: 0;
    background: rgba(245, 240, 235, 0.88);
    box-shadow: 0 2px 10px rgba(13, 13, 13, 0.20);
    color: var(--ink);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
    clip-path: polygon(0 0, 100% 0, 100% calc(100% - 16%), 0 100%);
  }
  .viewer .kadr-lupa svg { width: 17px; height: 17px; transform: translateY(-2.7px); }
  .viewer .kadr-lupa:active { background: rgba(13, 13, 13, 0.16); }
  /* W pełnym podglądzie lupa znika — z tego widoku wychodzi się X-em w rogu
     ekranu, stuknięciem w tło, Esc albo „wstecz". */
  body.podglad-duzy .kadr-lupa { display: none !important; }
  /* ...i znika też przy włączonym narzędziu prywatności: leży NAD płótnem,
     więc zaznaczenie zaczęte w prawym dolnym rogu zrzutu otwierałoby pełny
     podgląd zamiast rysować prostokąt. A akurat w tym rogu siedzi to, co
     zasłania się najczęściej. */
  body.redact-on .kadr-lupa { display: none !important; }
  .sa-btn {
    position: relative;
    width: 38px;
    height: 38px;
    display: grid;
    place-items: center;
    border: 0;
    border-radius: 0;
    /* Papierowe szkło: czytelne i na tle sceny, i na obrazie pod spodem. */
    background: rgba(245, 240, 235, 0.85);
    box-shadow: 0 2px 10px rgba(13, 13, 13, 0.14);
    color: var(--ink);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
    --sk: 18%;
  }
  .sa-btn:active { background: rgba(13, 13, 13, 0.16); }
  .sa-btn svg { width: 18px; height: 18px; }
  /* Środek ikony liczony od KRÓTSZEGO boku trapezu (18% z 38 px / 2 ≈ 3,4 px). */
  .sa-btn--gora { clip-path: polygon(0 var(--sk), 100% 0, 100% 100%, 0 100%); }
  .sa-btn--gora svg { transform: translateY(3.4px); }
  .sa-btn--dol { clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--sk)), 0 100%); }
  .sa-btn--dol svg { transform: translateY(-3.4px); }

  /* ── PEŁNY PODGLĄD ──────────────────────────────────────────────────────
     Scena na cały ekran, sam kadr: bez kafli, paska i osi. Wyjścia: X w rogu
     EKRANU, stuknięcie w tło obok kadru, Esc, systemowy „wstecz" (JS wpina
     się w historię — cofnięcie zamyka podgląd, nie stronę). */
  body.podglad-duzy { overflow: hidden; height: 100svh; }
  body.podglad-duzy .stage {
    position: fixed;
    inset: 0;
    z-index: 45;
    padding: calc(var(--safe-top) + 10px) 12px calc(12px + env(safe-area-inset-bottom, 0px));
    border-bottom: 0;
    box-shadow: none;
    background: var(--stage-bg);
    display: grid;
    place-items: center;
    --frame-max-h: calc(100svh - var(--safe-top) - 76px);
  }
  body.podglad-duzy .side-actions { display: none !important; }
  body.podglad-duzy .stage-bar,
  body.podglad-duzy .os { display: none !important; }
  /* Kolumna sceny bierze CAŁĄ szerokość: pasek akcji ma trzymać się krawędzi
     ekranu, a nie krawędzi wąskiego kadru (zrzut z telefonu jest pionowy
     i „Wgraj swoje" lądowałoby w rurce na środku). Sam kadr dalej centruje
     się w scenie. */
  .stage-col { width: 100%; gap: 8px; }

  /* Pusty stan: szerokość dalej z pojemnika (100cqw = szerokość sceny bez jej
     paddingu), więc przy każdym telefonie kadr kończy się dokładnie tam, gdzie
     kończy się scena. Proporcja bliżej kwadratu, bo pionowy ekran nie ma czym
     obsłużyć płaskiego kadru 16:10. */
  .dropzone {
    width: min(calc(100cqw - 2 * var(--frame-pad)), calc(var(--frame-inner-h) * 1.45));
    aspect-ratio: 3 / 2;
    min-width: 140px;
    /* Na telefonie wolnej wysokości nie ma: kadr ma zostać poziomy, żeby pod
       nim zmieściły się jeszcze tła. Dlatego dolna granica z desktopu schodzi
       do wartości, której proporcja i tak zwykle nie dotyka. */
    min-height: min(var(--frame-inner-h), 200px);
    padding: 22px;
    gap: 12px;
  }
  .drop-ico { width: 32px; height: 32px; }
  .drop-text { font-size: 12.5px; max-width: 26ch; }

  /* Pasek pod kadrem łamie się na dwa wiersze: „Wgraj swoje" zostaje po
     lewej, sterowanie nagraniem schodzi pod spód na całą szerokość, dzięki
     czemu scrub jest długi i da się w niego trafić. */
  .stage-bar { flex-wrap: wrap; row-gap: 10px; }
  .video-bar { flex: 1 1 100%; justify-content: flex-start; }
  .video-bar .slider--scrub { flex: 1 1 90px; min-width: 60px; }

  /* ── pod sceną: najpierw TŁA, potem ustawienia ──────────────────────── */
  .rail { order: 2; border-left: 0; }
  .dock { order: 3; border-right: 0; border-top: 1px solid var(--line); }

  /* Kolumny przestają być własnymi kontenerami przewijania — od teraz
     przewija się dokument, a one po prostu mają swoją wysokość. */
  .dock, .rail { overflow: visible; }
  .dock-scroll, .rail-scroll {
    height: auto;
    overflow: visible;
    overscroll-behavior: auto;
    /* Ciaśniej niż na desktopie (16/22/20): każdy piksel odstępu w słupku
       to piksel zabrany podglądowi, a sekcje i tak rozdzielają nagłówki. */
    padding: 12px var(--dock-pad) 18px;
    gap: 14px;
  }
  .block { gap: 9px; }
  .bg-cat { gap: 6px; }
  /* Rail jeszcze ciaśniej (runda 14): „Tło" → „Twoje" → „Kolory" bez pustych
     pasów — każdy z nich kosztował wysokość podglądu.
     Runda 16: góra kolumny 12 → 8 px, odstęp 8 → 6 — plakietka działu (niżej)
     sama oddziela nagłówek od reszty, więc pusty pas przestał być potrzebny. */
  .rail-scroll { gap: 6px; padding-top: 8px; }
  .bg-cats { gap: 10px; }
  .bg-cat--wlasne { margin-bottom: 0; }
  /* W słupku nie ma „dołu kolumny": eksport i stopka wracają do zwykłego
     biegu treści, więc biorą taki sam boczny padding jak reszta kolumny. */
  .dock { display: block; }
  .dock-export { padding: 22px var(--dock-pad) 0; }
  .dock-foot { padding: 18px var(--dock-pad) 22px; }

  /* ── EKSPORT NA TELEFONIE (runda 14) ────────────────────────────────────
     Przyklejony pasek „Pobierz" WYPADŁ (decyzja Kuby: za duży, zjadał
     miejsce) — akcję końcową przejął kafel pobierania przy podglądzie,
     który zapisuje w formacie ze stanu (bazowo PNG). Sekcja Eksport wraca
     do zwykłego przepływu na dole słupka: muszka PNG|JPG przy lewej,
     mniejsza; kto chce JPG, ten ją znajdzie. Kopiowanie do schowka jest
     rzeczą desktopową (wklejanie do Figmy/Slacka) — na dotyku znika. */
  #btnReplace { display: none; }
  .export-row #btnCopy { display: none; }
  .mode-foto .stage-bar { display: none; }
  .desktop-only { display: none !important; }
  .tp-switch--sm .tp { min-height: 32px; min-width: 48px; }

  /* ── pola trafienia ─────────────────────────────────────────────────────
     Suwak rośnie do wysokości palca, a układ zostaje ten sam: to, co
     przybyło, oddajemy ujemnym marginesem. Ścieżka i punkt siedzą w pionie
     na środku, więc nic się nie przesuwa (trik z EFFECTS). */
  .slider { height: 38px; margin-block: -8px; }
  .slider::-webkit-slider-thumb { width: 7px; height: 20px; margin-top: -9px; }
  .slider::-moz-range-thumb { width: 7px; height: 20px; }

  /* POLA TRAFIENIA NA DOTYKU (runda 6). Wszystko, co się klika, schodzi
     najniżej do 36 px — poniżej tej miary palec zaczyna trafiać obok, a przy
     eksporcie i wgrywaniu pliku pudło kosztuje najwięcej. „Pobierz" i „Wgraj
     swój plik" dostają pełne 44 px, bo to dwa końce całej ścieżki. */
  .tp--sm { min-height: 36px; padding: 0 14px; }
  .tp--ico.tp--sm { min-width: 44px; padding: 0 12px; }
  .tp--sm .tp-ico { width: 15px; height: 15px; }
  .head-actions .tp--ico.tp--sm { min-width: 40px; padding: 0 10px; }
  .tp-switch--sm .tp { min-height: 36px; min-width: 60px; }
  #btnDownload, #btnReplace { min-height: 44px; }

  /* Kliny (płótno, cień, animacja) dostają pełną wysokość palca i mogą
     spokojnie złamać się na dwa wiersze — auto-fit robi to sam. */
  .wedge-row { gap: 4px; }
  .wedge-row .tp { min-height: 36px; font-size: 9px; padding: 0 6px; }

  /* Plansze XY: minimum 150 px, żeby dało się celować kciukiem, i romb
     większy o dwa piksele. Na telefonie zostaje KWADRAT — prostokąt
     z desktopu (runda 15) kupuje precyzję za wysokość, a tu wysokość jest
     walutą, której nie ma na zbyciu: pod planszą ma się jeszcze zmieścić
     reszta panelu. */
  .pad { min-width: 150px; max-width: 230px; aspect-ratio: 1; }
  .pad-dot { width: 15px; height: 15px; }

  /* Kafle tła i stylu zostają w TRZECH kolumnach: „zwinięta kategoria =
     pierwszy rząd" trzyma się na liczbie kolumn, więc zmiana siatki
     rozjechałaby chowanie kafli. Ograniczamy za to szerokość siatki, żeby
     przy szerszym oknie kafle nie urosły na pół ekranu. Przy 320 px ekranu
     kafel ma i tak ~88 px, czyli dwa razy więcej niż minimum dla palca. */
  .bg-cat-grid, .styl-grid { max-width: 430px; gap: 8px 6px; }
  .styl-name, .bg-tile-name { font-size: 8px; }

  /* ── NAGŁÓWEK DZIAŁU „TŁO" (runda 16, uwaga Kuby) ──────────────────────
     „Tło" jest DZIAŁEM, a „Twoje", „Kolory", „Gradienty" i „Efekty" są jego
     CZĘŚCIAMI — tyle że dostawały ten sam krój, ten sam stopień i ten sam
     ton, więc hierarchia znikała: „zlewa się z podkategoriami". Do tego
     dzielił je pusty pas na kilkanaście pikseli, których na telefonie
     nie ma na zbyciu.
     Nagłówek działu jest teraz PLAKIETKĄ w języku reszty apki: czarny klin
     z papierowym napisem, czyli ten sam kształt co przyciski „Cięcie".
     Podkategorie zostają ciche i przez to od razu widać, co jest czym.
     Odzyskane piksele idą do podglądu (--frame-max-h niżej: 54 → 57svh). */
  .rail-head {
    align-items: center;
    margin-bottom: 2px;
  }
  .rail-head .label {
    display: inline-flex;
    align-items: center;
    min-height: 22px;
    padding: 0 13px;
    background: var(--black);
    color: var(--paper);
    font-size: 9.5px;
    letter-spacing: 0.08em;
    /* Ten sam klin co .tp--single, tylko liczony w pikselach: plakietka jest
       niska, a przy --lean 7% ścięcie byłoby ledwie widoczne. */
    clip-path: polygon(0 4px, 100% 0, 100% 100%, 0 calc(100% - 4px));
    /* scaleY z .label rozciągnąłby też tło plakietki — na klinie widać to
       jako rozjechane ścięcie, więc tutaj wracamy do jedynki. */
    transform: none;
  }

  /* ── POSTĘP EKSPORTU NA TELEFONIE (runda 16) ────────────────────────────
     Eksport wideo leci w CZASIE RZECZYWISTYM — trzyminutowy klip to trzy
     minuty czekania. Pasek postępu i licznik „zostało około" siedzą
     w `.dock-export`, czyli na samym dole przewijanego słupka, a akcję
     odpala kafel przy kadrze — więc na telefonie użytkownik odpalał eksport
     i przez trzy minuty patrzył na przygaszone panele bez jednej informacji.
     Wyglądało to na zawieszoną apkę.
     Teraz na czas eksportu pasek i licznik wskakują na dno EKRANU jako
     przyklejony pas. Nie dokładamy kontrolki: te same dwa elementy, tylko
     w miejscu, w którym użytkownik faktycznie patrzy. */
  body.is-exporting .dock-export {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 48;
    padding: 12px var(--dock-pad) calc(12px + env(safe-area-inset-bottom, 0px));
    background: var(--panel);
    box-shadow: 0 -8px 24px rgba(13, 13, 13, 0.16);
  }
  /* Z przyklejonego pasa zostaje samo to, co mówi „ile jeszcze": nagłówek
     „Eksport" i rząd z formatem/pobieraniem są w tym momencie bezużyteczne,
     a zabierają połowę wysokości. */
  body.is-exporting .dock-export .block-head,
  body.is-exporting .dock-export .export-row { display: none; }
  body.is-exporting .export-eta { margin-top: 10px; text-align: center; }

  /* Kafle akcji przy kadrze też gasną na czas eksportu — inaczej wyglądają
     na czynne, a stuknięcie w „Pobierz" wpada w cichy `return`. */
  body.is-exporting .side-actions,
  body.is-exporting .kadr-lupa {
    pointer-events: none;
    opacity: 0.45;
  }

  /* Nagłówek kategorii jest chwytem na palec, nie linijką tekstu.
     36 px zeszło na 30: cztery nagłówki po 6 px to 24 px podglądu, a chwyt
     i tak zostaje wyraźnie nad progiem dla palca. */
  .bg-cat-head { min-height: 30px; }
  .bg-cat-more { width: 38px; height: 34px; }
  .bg-cat-more svg { width: 14px; height: 14px; }

  /* Trzy cele dotykowe, które wypadły z bloku „pola trafienia": tekstowy
     „Zaznacz całość" pod osią oraz dwa krzyżyki kasowania. Wygląd zostaje,
     rośnie tylko pole (padding + ujemny margines tam, gdzie układ by się
     przesunął). */
  .os-caly { padding: 12px 6px; margin: -12px -6px; }
  .zapis-kasuj { min-width: 36px; min-height: 36px; }
  .bg-wlasne-kasuj { padding: 10px 8px; margin: -10px -8px; }
}

/* Bardzo niskie ekrany (telefon w poziomie): przyklejona scena zjadłaby cały
   widok i nie zostałoby nic do przewijania. Kadr oddaje wtedy część wysokości. */
@media (max-width: 860px) and (max-height: 620px), (max-height: 500px) and (pointer: coarse) {
  :root { --frame-max-h: 38svh; }
}

/* Kto wyłączył ruch w systemie, dostaje to samo bez animacji. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ═════════════════════════ OBWÓDKA WYBRANEGO KAFLA ════════════════════════
   Ziarno na błękicie WYPADŁO (decyzja Kuby 30.07.2026): błękit ma być czysty,
   bez tekstury — na jasnym motywie szum robił z akcentu brudną plamę. Została
   po nim tylko zmienna --grain w :root, nieużywana.

   Obwódkę rysuje pseudoelement, nie inset box-shadow: kafel przezroczysty ma
   szachownicę na ::after (z-index -1), która przykryłaby cień wewnętrzny —
   pierścień musi lecieć NAD treścią kafla. Maska z ziarnem przestała być
   potrzebna, więc zamiast niej jest zwykły border. */
.bg-tile.is-on::before,
.bg-tile[aria-pressed="true"]::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: inherit;
  border: 3px solid var(--akcent);
  pointer-events: none;
}

/* ═══════════ HUMBAK STUDIO — promo rodziny (dodane 18.08.2026) ═══════════
   Kafelek i belka to jedyne animowane elementy interfejsu: kolory zostają
   w rodzinie (błękit landingu), wyróżnia je ruch pikseli, nie kolor. */
.bar-spacer { justify-self: end; }
.hub-btn { text-decoration: none; }

.studio-tile {
  position: relative;
  flex: none;              /* dziecko flexowej kolumny z przewijaniem: bez tego kurczy się do zera */
  display: block; overflow: hidden;
  border-radius: 10px; text-decoration: none;
  background: linear-gradient(160deg, #EFF9FF, #8CC7EE);
  box-shadow: inset 0 0 0 1px var(--line-strong);
}
.studio-px {
  position: absolute; inset: 0; width: 100%; height: 100%;
  display: block; image-rendering: pixelated;
}
.studio-tile-in {
  position: relative; z-index: 1;
  display: flex; flex-direction: column; align-items: flex-start; gap: 5px;
  padding: 15px 16px 14px;
  color: #0D0D0D;
}
.studio-tile-brand {
  font-family: 'Archivo Narrow', sans-serif;   /* w tej apce nie ma Archivo Black */
  font-weight: 600; text-transform: uppercase;
  font-size: 13px; letter-spacing: 0.02em;
}
.studio-tile-line {
  font-family: 'DM Sans', sans-serif;
  font-size: 11px; line-height: 1.5;
  color: rgba(13, 13, 13, 0.72);
}
.studio-tile-cta {
  margin-top: 3px;
  font-size: 10px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase;
}
.studio-tile:hover .studio-tile-cta { text-decoration: underline; }

.studio-belka { display: none; }

@media (max-width: 860px), (max-height: 500px) and (pointer: coarse) {
  .hub-btn { display: none; }
  .studio-belka {
    order: 1;
    position: relative; overflow: hidden;
    display: flex; align-items: center;
    margin: 8px 10px 0;
    padding: 13px 14px;
    border-radius: 10px;
    text-decoration: none;
    background: linear-gradient(160deg, #EFF9FF, #8CC7EE);
  }
  .studio-belka-in {
    position: relative; z-index: 1;
    font-family: 'DM Sans', sans-serif;
    font-size: 11.5px; color: rgba(13, 13, 13, 0.8);
    display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap;
  }
  .studio-belka-in strong {
    font-family: 'Archivo Narrow', sans-serif;
    font-weight: 600; font-size: 12px; color: #0D0D0D;
  }
  .studio-belka-arrow { margin-left: 2px; }
}
