PageSourceSearch

https://chmurka.ai/ecommerce.html

html chmurka.ai collected 2026-10-05 15:15:13 UTC 315,042 bytes, 4,656 lines download raw bytes

1<!DOCTYPE html>
2<html lang="pl" data-theme="dark">
3<head>
4<!-- Anty-FOUC pixcloud: kurtyna do końca auto-tłumaczenia (en-tr-done z page-en-translate.js), failsafe 900ms -->
5<script>(function(){if(location.hostname.indexOf('pixcloud')===-1)return;var d=document.documentElement;d.classList.add('is-pixcloud');setTimeout(function(){d.classList.add('en-tr-done')},900);})();</script>
5
6<style>html.is-pixcloud:not(.en-tr-done) body{opacity:0!important}html.is-pixcloud.en-tr-done body{opacity:1;transition:opacity .12s ease-out}</style>
7<meta charset="UTF-8">
8<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
9<meta name="theme-color" content="#07080f">
10<title>Marketing Studio — Chmurka.ai</title>
11<link rel="icon" href="/img/favicon.ico?v=2">
12
13
14<!-- Unbounded + Manrope = skóra 2.0 (oba z latin-ext); Inter/Space Grotesk/Cormorant/Instrument zostają dla starszych komponentów -->
15<style type="text/css">@font-face {font-family:'Unbounded';font-style:normal;font-weight:500;src:url(/cf-fonts/v/unbounded/5.2.8/latin-ext/wght/normal.woff2);unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:'Unbounded';font-style:normal;font-weight:500;src:url(/cf-fonts/v/unbounded/5.2.8/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:'Unbounded';font-style:normal;font-weight:500;src:url(/cf-fonts/v/unbounded/5.2.8/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:'Unbounded';font-style:normal;font-weight:500;src:url(/cf-fonts/v/unbounded/5.2.8/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:'Unbounded';font-style:normal;font-weight:500;src:url(/cf-fonts/v/unbounded/5.2.8/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:'Unbounded';font-style:normal;font-weight:600;src:url(/cf-fonts/v/unbounded/5.2.8/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:'Unbounded';font-style:normal;font-weight:600;src:url(/cf-fonts/v/unbounded/5.2.8/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:'Unbounded';font-style:normal;font-weight:600;src:url(/cf-fonts/v/unbounded/5.2.8/latin-ext/wght/normal.woff2);unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:'Unbounded';font-style:normal;font-weight:600;src:url(/cf-fonts/v/unbounded/5.2.8/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:'Unbounded';font-style:normal;font-weight:600;src:url(/cf-fonts/v/unbounded/5.2.8/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:'Unbounded';font-style:normal;font-weight:700;src:url(/cf-fonts/v/unbounded/5.2.8/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:'Unbounded';font-style:normal;font-weight:700;src:url(/cf-fonts/v/unbounded/5.2.8/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:'Unbounded';font-style:normal;font-weight:700;src:url(/cf-fonts/v/unbounded/5.2.8/cyrillic/wght/normal.woff2);
15unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:'Unbounded';font-style:normal;font-weight:700;src:url(/cf-fonts/v/unbounded/5.2.8/latin-ext/wght/normal.woff2);unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:'Unbounded';font-style:normal;font-weight:700;src:url(/cf-fonts/v/unbounded/5.2.8/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:400;src:url(/cf-fonts/v/manrope/5.2.8/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:400;src:url(/cf-fonts/v/manrope/5.2.8/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:400;src:url(/cf-fonts/v/manrope/5.2.8/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:400;src:url(/cf-fonts/v/manrope/5.2.8/latin-ext/wght/normal.woff2);unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:400;src:url(/cf-fonts/v/manrope/5.2.8/greek/wght/normal.woff2);unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:400;src:url(/cf-fonts/v/manrope/5.2.8/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:500;src:url(/cf-fonts/v/manrope/5.2.8/latin-ext/wght/normal.woff2);unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:500;src:url(/cf-fonts/v/manrope/5.2.8/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:500;src:url(/cf-fonts/v/manrope/5.2.8/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:500;src:url(/cf-fonts/v/manrope/5.2.8/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:500;src:url(/cf-fonts/v/manrope/5.2.8/greek/wght/normal.woff2);unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:500;src:url(/cf-fonts/v/manrope/5.2.8/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:600;src:url(/cf-fonts/v/manrope/5.2.8/latin/wght/normal.woff2);
15unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:600;src:url(/cf-fonts/v/manrope/5.2.8/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:600;src:url(/cf-fonts/v/manrope/5.2.8/latin-ext/wght/normal.woff2);unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:600;src:url(/cf-fonts/v/manrope/5.2.8/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:600;src:url(/cf-fonts/v/manrope/5.2.8/greek/wght/normal.woff2);unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:600;src:url(/cf-fonts/v/manrope/5.2.8/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:700;src:url(/cf-fonts/v/manrope/5.2.8/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:700;src:url(/cf-fonts/v/manrope/5.2.8/greek/wght/normal.woff2);unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:700;src:url(/cf-fonts/v/manrope/5.2.8/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:700;src:url(/cf-fonts/v/manrope/5.2.8/latin-ext/wght/normal.woff2);unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:700;src:url(/cf-fonts/v/manrope/5.2.8/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:700;src:url(/cf-fonts/v/manrope/5.2.8/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:800;src:url(/cf-fonts/v/manrope/5.2.8/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:800;src:url(/cf-fonts/v/manrope/5.2.8/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:800;src:url(/cf-fonts/v/manrope/5.2.8/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:800;src:url(/cf-fonts/v/manrope/5.2.8/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:800;src:url(/cf-fonts/v/manrope/5.2.8/latin-ext/wght/normal.woff2);
15unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:'Manrope';font-style:normal;font-weight:800;src:url(/cf-fonts/v/manrope/5.2.8/greek/wght/normal.woff2);unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.2.8/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.2.8/latin-ext/wght/normal.woff2);unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.2.8/greek/wght/normal.woff2);unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.2.8/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.2.8/greek-ext/wght/normal.woff2);unicode-range:U+1F00-1FFF;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.2.8/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:400;src:url(/cf-fonts/v/inter/5.2.8/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:500;src:url(/cf-fonts/v/inter/5.2.8/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:500;src:url(/cf-fonts/v/inter/5.2.8/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:500;src:url(/cf-fonts/v/inter/5.2.8/greek/wght/normal.woff2);unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:500;src:url(/cf-fonts/v/inter/5.2.8/latin-ext/wght/normal.woff2);unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:500;src:url(/cf-fonts/v/inter/5.2.8/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:500;src:url(/cf-fonts/v/inter/5.2.8/greek-ext/wght/normal.woff2);unicode-range:U+1F00-1FFF;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:500;src:url(/cf-fonts/v/inter/5.2.8/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.2.8/latin-ext/wght/normal.woff2);
15unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.2.8/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.2.8/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.2.8/greek-ext/wght/normal.woff2);unicode-range:U+1F00-1FFF;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.2.8/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.2.8/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:600;src:url(/cf-fonts/v/inter/5.2.8/greek/wght/normal.woff2);unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.2.8/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.2.8/greek-ext/wght/normal.woff2);unicode-range:U+1F00-1FFF;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.2.8/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.2.8/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.2.8/latin-ext/wght/normal.woff2);unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.2.8/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:700;src:url(/cf-fonts/v/inter/5.2.8/greek/wght/normal.woff2);unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:800;src:url(/cf-fonts/v/inter/5.2.8/latin-ext/wght/normal.woff2);unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:800;src:url(/cf-fonts/v/inter/5.2.8/greek/wght/normal.woff2);unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:800;src:url(/cf-fonts/v/inter/5.2.8/vietnamese/wght/normal.woff2);
15unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:800;src:url(/cf-fonts/v/inter/5.2.8/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:800;src:url(/cf-fonts/v/inter/5.2.8/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:800;src:url(/cf-fonts/v/inter/5.2.8/greek-ext/wght/normal.woff2);unicode-range:U+1F00-1FFF;font-display:swap;}@font-face {font-family:'Inter';font-style:normal;font-weight:800;src:url(/cf-fonts/v/inter/5.2.8/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:'Space Grotesk';font-style:normal;font-weight:500;src:url(/cf-fonts/v/space-grotesk/5.2.10/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:'Space Grotesk';font-style:normal;font-weight:500;src:url(/cf-fonts/v/space-grotesk/5.2.10/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:'Space Grotesk';font-style:normal;font-weight:500;src:url(/cf-fonts/v/space-grotesk/5.2.10/latin-ext/wght/normal.woff2);unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:'Space Grotesk';font-style:normal;font-weight:600;src:url(/cf-fonts/v/space-grotesk/5.2.10/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:'Space Grotesk';font-style:normal;font-weight:600;src:url(/cf-fonts/v/space-grotesk/5.2.10/latin-ext/wght/normal.woff2);unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:'Space Grotesk';font-style:normal;font-weight:600;src:url(/cf-fonts/v/space-grotesk/5.2.10/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:'Space Grotesk';font-style:normal;font-weight:700;src:url(/cf-fonts/v/space-grotesk/5.2.10/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:'Space Grotesk';font-style:normal;font-weight:700;src:url(/cf-fonts/v/space-grotesk/5.2.10/latin-ext/wght/normal.woff2);unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:'Space Grotesk';font-style:normal;font-weight:700;src:url(/cf-fonts/v/space-grotesk/5.2.10/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:'Instrument Serif';font-style:normal;font-weight:400;src:url(/cf-fonts/s/instrument-serif/5.2.8/latin-ext/400/normal.woff2);
15unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:'Instrument Serif';font-style:normal;font-weight:400;src:url(/cf-fonts/s/instrument-serif/5.2.8/latin/400/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:'Instrument Serif';font-style:italic;font-weight:400;src:url(/cf-fonts/s/instrument-serif/5.2.8/latin/400/italic.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:'Instrument Serif';font-style:italic;font-weight:400;src:url(/cf-fonts/s/instrument-serif/5.2.8/latin-ext/400/italic.woff2);unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:'Cormorant Garamond';font-style:normal;font-weight:600;src:url(/cf-fonts/v/cormorant-garamond/5.2.11/latin/wght/normal.woff2);unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;font-display:swap;}@font-face {font-family:'Cormorant Garamond';font-style:normal;font-weight:600;src:url(/cf-fonts/v/cormorant-garamond/5.2.11/cyrillic/wght/normal.woff2);unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;font-display:swap;}@font-face {font-family:'Cormorant Garamond';font-style:normal;font-weight:600;src:url(/cf-fonts/v/cormorant-garamond/5.2.11/latin-ext/wght/normal.woff2);unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;font-display:swap;}@font-face {font-family:'Cormorant Garamond';font-style:normal;font-weight:600;src:url(/cf-fonts/v/cormorant-garamond/5.2.11/vietnamese/wght/normal.woff2);unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;font-display:swap;}@font-face {font-family:'Cormorant Garamond';font-style:normal;font-weight:600;src:url(/cf-fonts/v/cormorant-garamond/5.2.11/cyrillic-ext/wght/normal.woff2);unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;font-display:swap;}</style>
16<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
17<!-- Tailwind CDN — wymagane przez pricing-modal.html (używa Tailwind utility classes) -->
18<script src="https://cdn.tailwindcss.com"></script>
vendor: 1 bytes, line 18
18
19<script src="/js/i18n-standalone.js"></script>
19
20
21<style>
22  :root {
23    --bg: #07070b;
24    --bg2: #0d0d12;
25    --card: rgba(255,255,255,0.035);
26    --card-hi: rgba(255,255,255,0.06);
27    --border: rgba(255,255,255,0.06);
28    --border-hi: rgba(255,255,255,0.12);
29    --text: #fafafb;
30    --muted: rgba(250,250,251,0.62);
31    --muted2: rgba(250,250,251,0.35);
32    --accent: #a6f1f8;
33    --accent2: #d4bff9;
34    --accent3: #f4c7e7;
35    --danger: #f87171;
36    --grad: linear-gradient(135deg, #a6f1f8 0%, #d4bff9 50%, #f4c7e7 100%);
37    --grad-soft: linear-gradient(135deg, rgba(166,241,248,0.15), rgba(212,191,249,0.15) 50%, rgba(244,199,231,0.15));
38    --blur: saturate(180%) blur(28px);
39    --shadow-sm: 0 2px 8px rgba(0,0,0,0.15);
40    --shadow-md: 0 12px 32px rgba(0,0,0,0.28);
41    --shadow-lg: 0 28px 70px rgba(0,0,0,0.5);
42    --shadow-glow: 0 30px 80px rgba(166,241,248,0.18);
43    --eo: cubic-bezier(0.16, 1, 0.3, 1);
44    --es: cubic-bezier(0.34, 1.56, 0.64, 1);
45  }
46  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
47  * { -webkit-tap-highlight-color: transparent; }
48  html, body { background: var(--bg); color: var(--text); font-family: 'Inter', -apple-system, 'SF Pro Display', sans-serif; font-size: 14px; line-height: 1.5; overflow: hidden; overflow-x: hidden; height: 100vh; max-width: 100vw; -webkit-font-smoothing: antialiased; font-feature-settings: 'cv11', 'ss01'; }
49  .serif { font-family: 'Instrument Serif', serif; font-style: italic; }
50
51  /* Ambient background glow */
52  body::before { content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 0; background:
53    radial-gradient(800px 600px at 15% -10%, rgba(166,241,248,0.06), transparent 60%),
54    radial-gradient(600px 500px at 85% 110%, rgba(244,199,231,0.05), transparent 60%),
55    radial-gradient(500px 400px at 50% 50%, rgba(212,191,249,0.03), transparent 70%);
56  }
57
58  /* ════ LAYOUT ════ */
59  .app { position: relative; z-index: 1; display: flex; flex-direction: column; height: 100vh; }
60  .topbar { display: flex; align-items: center; gap: 32px; padding: 16px 32px; border-bottom: 1px solid var(--border); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur); background: rgba(7,7,11,0.75); z-index: 20; }
61  .main { flex: 1; overflow: hidden; display: flex; }
62  .center { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 48px 48px 200px; position: relative; scroll-behavior: smooth; }
63  .right-col { width: 340px; border-left: 1px solid var(--border); padding: 32px 28px 200px; overflow-y: auto; flex-shrink: 0; transition: width 0.3s var(--eo); }
64  body.is-landing .right-col { display: none; }
65  body.is-landing .center { padding: 48px 0 0; }
66  body.is-landing .dock { display: none !important; }
67
68  /* Scrollbar */
69  .center::-webkit-scrollbar, .right-col::-webkit-scrollbar, .mention-dd::-webkit-scrollbar { width: 8px; }
70  .center::-webkit-scrollbar-thumb, .right-col::-webkit-scrollbar-thumb, .mention-dd::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 8px; }
71  .center::-webkit-scrollbar-thumb:hover, .right-col::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.15); }
72
73  /* ════ TOPBAR ════ */
74  .logo { display: flex; align-items: center; gap: 12px; text-decoration: none; color: inherit; font-size: 15px; font-weight: 600; letter-spacing: -0.025em; }
75  .logo-dot { width: 32px; height: 32px; border-radius: 10px; background: var(--grad); display: flex; align-items: center; justify-content: center; color: #07070b; font-size: 15px; box-shadow: 0 6px 18px rgba(166,241,248,0.22); }
76  .logo-brand { display: flex; align-items: baseline; gap: 6px; font-family: 'Cormorant Garamond', serif; font-size: 18px; line-height: 1; font-weight: 500; }
77  .logo-brand-main { color: var(--text); transition: color 0.2s; }
78  .logo-brand-main:hover { color: var(--accent); }
79  .logo-sep { color: var(--muted); opacity: 0.6; font-size: 12px; padding: 0 2px; }
80  .logo-brand-sub { color: var(--accent); font-style: italic; font-weight: 600; font-size: 16px; letter-spacing: -0.02em; }
81  .topup-btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; border-radius: 999px; background: linear-gradient(135deg, rgba(166,241,248,0.18), rgba(166,241,248,0.06)); color: var(--accent); border: 1px solid rgba(166,241,248,0.3); font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.2s var(--eo); font-family: inherit; }
82  .topup-btn:hover { background: rgba(166,241,248,0.22); border-color: var(--accent); transform: translateY(-1px); }
83  .tabs { display: flex; gap: 2px; flex: 1; }
84  .tab { padding: 9px 18px; border-radius: 12px; color: var(--muted); font-size: 13.5px; font-weight: 500; cursor: pointer; letter-spacing: -0.015em; transition: all 0.25s var(--eo); border: none; background: transparent; font-family: inherit; }
85  .tab:hover { color: var(--text); background: var(--card); }
86  .tab.on { color: var(--text); background: var(--card-hi); font-weight: 600; }
87  .tb-right { display: flex; align-items: center; gap: 12px; }
88  .credits { display: inline-flex; align-items: center; gap: 7px; padding: 9px 14px; border-radius: 999px; background: var(--card); border: 1px solid var(--border-hi); font-size: 12.5px; font-weight: 600; letter-spacing: -0.015em; }
89  .credits i { color: var(--accent); font-size: 10px; }
90  .home-btn { width: 36px; height: 36px; border-radius: 10px; background: var(--card); color: var(--muted); display: inline-flex; align-items: center; justify-content: center; text-decoration: none; transition: all 0.2s var(--eo); }
91  .home-btn:hover { background: var(--card-hi); color: var(--text); }
92  .avatar { width: 34px; height: 34px; border-radius: 50%; background: var(--grad-soft); display: flex; align-items: center; justify-content: center; color: var(--text); font-size: 13px; font-weight: 600; cursor: pointer; transition: transform 0.2s var(--es); }
93  .avatar:hover { transform: scale(1.06); }
94
95  /* ════ PAGE HEADER ════ */
96  .page-head { margin-bottom: 40px; max-width: 1400px; }
97  h1 { font-size: 52px; font-weight: 600; letter-spacing: -0.045em; line-height: 1; margin-bottom: 12px; }
98  h1 .serif { font-weight: 400; color: var(--accent2); }
99  .sub { color: var(--muted); font-size: 15px; letter-spacing: -0.015em; max-width: 620px; }
100
101  /* ════ VIEW CONTROLS (cats + segment) ════ */
102  .controls { display: flex; align-items: center; justify-content: space-between; gap: 24px; margin-bottom: 32px; max-width: 1400px; flex-wrap: wrap; }
103  .cats { display: flex; gap: 4px; flex-wrap: wrap; }
104  .cat { padding: 9px 16px; border-radius: 999px; background: transparent; color: var(--muted); font-size: 13px; font-weight: 500; letter-spacing: -0.01em; cursor: pointer; transition: all 0.25s var(--eo); border: 1px solid transparent; font-family: inherit; }
105  .cat:hover { color: var(--text); background: var(--card); }
106  .cat.on { background: #fff; color: #07070b; font-weight: 600; }
107  .cat.cta-request { background: rgba(166,241,248,0.1); color: var(--accent); border-color: rgba(166,241,248,0.3); font-weight: 600; }
108  .cat.cta-request:hover { background: rgba(166,241,248,0.2); border-color: rgba(166,241,248,0.6); }
109  .segment { display: flex; padding: 3px; background: var(--card); border-radius: 12px; }
110  .seg { padding: 7px 16px; border-radius: 9px; font-size: 12px; font-weight: 500; color: var(--muted); cursor: pointer; transition: all 0.2s var(--eo); letter-spacing: -0.01em; border: none; background: transparent; font-family: inherit; }
111  .seg.on { background: rgba(255,255,255,0.1); color: var(--text); font-weight: 600; }
112
113  /* ════ STUDIO TILES — KIVE-STYLE ════ */
114  .studios { display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap: 20px; max-width: 1400px; }
115  @media (min-width: 1600px) { .studios { grid-template-columns: repeat(3, 1fr); } }
116  .tile { position: relative; border-radius: 28px; overflow: hidden; cursor: pointer; background: var(--bg2); aspect-ratio: 4/5; transition: transform 0.5s var(--eo), box-shadow 0.5s var(--eo); will-change: transform; }
117  .tile::after { content: ''; position: absolute; inset: 0; border-radius: inherit; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.06); pointer-events: none; transition: box-shadow 0.3s var(--eo); z-index: 3; }
118  .tile:hover { transform: translateY(-6px); box-shadow: var(--shadow-lg); }
119  .tile:hover::after { box-shadow: inset 0 0 0 1px rgba(255,255,255,0.14); }
120  .tile:hover .tile-img { transform: scale(1.06); }
121  .tile.on { transform: translateY(-6px); box-shadow: var(--shadow-lg), var(--shadow-glow); }
122  .tile.on::after { box-shadow: inset 0 0 0 2px rgba(166,241,248,0.6); }
123  .tile-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; transition: transform 1s var(--eo); background: var(--grad-soft); }
124  .tile-overlay { position: absolute; inset: 0; background: linear-gradient(to top, rgba(7,7,11,0.96) 0%, rgba(7,7,11,0.55) 35%, rgba(7,7,11,0.08) 60%, transparent 85%); pointer-events: none; z-index: 1; }
125  .tile-info { position: absolute; bottom: 0; left: 0; right: 0; padding: 28px 28px 26px; z-index: 2; }
126  .tile-name { font-size: 24px; font-weight: 600; letter-spacing: -0.03em; color: #fff; margin-bottom: 6px; line-height: 1.05; }
127  .tile-meta { font-size: 11.5px; color: rgba(255,255,255,0.65); text-transform: uppercase; letter-spacing: 0.14em; font-weight: 500; }
128  .tile-check { position: absolute; top: 18px; right: 18px; width: 36px; height: 36px; border-radius: 50%; background: rgba(255,255,255,0.95); backdrop-filter: blur(12px); display: flex; align-items: center; justify-content: center; color: #07070b; font-size: 13px; opacity: 0; transform: scale(0.4); transition: opacity 0.25s var(--eo), transform 0.35s var(--es); z-index: 4; }
129  .tile.on .tile-check { opacity: 1; transform: scale(1); }
130  /* Hover CTA — visual hint że tile jest klikalny */
131  .tile-use-hint {
132    position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) translateY(8px);
133    background: var(--accent); color: #07070b; font-weight: 600; font-size: 12.5px;
134    padding: 10px 18px; border-radius: 999px; pointer-events: none;
135    opacity: 0; transition: opacity 0.3s var(--eo), transform 0.35s var(--es);
136    box-shadow: 0 10px 30px rgba(0,0,0,0.45); white-space: nowrap; z-index: 3;
137    display: inline-flex; align-items: center; gap: 6px;
138  }
139  .tile:hover .tile-use-hint { opacity: 1; transform: translate(-50%, -50%) translateY(0); }
140  .tile.on .tile-use-hint { display: none; }
141  /* Serduszko "Dodaj do Moich" — widoczne na hover public tile, solid gdy już sklonowany */
142  .tile-heart { position: absolute; top: 18px; left: 18px; width: 36px; height: 36px; border-radius: 50%; background: rgba(13,13,18,0.78); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.16); display: flex; align-items: center; justify-content: center; color: rgba(255,255,255,0.85); font-size: 14px; cursor: pointer; opacity: 0; transform: scale(0.7); transition: opacity 0.25s var(--eo), transform 0.3s var(--es), background 0.2s, color 0.2s; z-index: 5; padding: 0; }
143  .tile:hover .tile-heart { opacity: 1; transform: scale(1); }
144  .tile-heart:hover { background: rgba(255,71,133,0.95); color: #fff; border-color: rgba(255,71,133,0.95); transform: scale(1.08); }
145  .tile-heart.saved { opacity: 1; transform: scale(1); background: rgba(255,71,133,0.95); color: #fff; border-color: rgba(255,71,133,0.95); }
146  .tile-heart.saved:hover { background: rgba(255,71,133,1); }
147  @keyframes heartPop { 0% { transform: scale(1); } 40% { transform: scale(1.3); } 100% { transform: scale(1); } }
148  .tile-heart.pop { animation: heartPop 0.5s var(--es); }
149
150  /* ════ BRAND KIT ("Mój Styl") ════ */
151  .bk-empty { padding: 60px 30px; text-align: center; background: var(--card); border: 1px dashed var(--border-hi); border-radius: 24px; }
152  .bk-empty i { font-size: 42px; background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent; margin-bottom: 18px; display: block; }
153  .bk-empty h3 { font-size: 22px; font-weight: 600; letter-spacing: -0.02em; margin-bottom: 10px; }
154  .bk-empty p { color: var(--muted); font-size: 14px; line-height: 1.55; max-width: 500px; margin: 0 auto 22px; letter-spacing: -0.01em; }
155  .bk-drop { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; padding: 22px; background: var(--bg2); border: 2px dashed var(--border-hi); border-radius: 20px; margin-bottom: 16px; transition: all 0.2s; }
156  .bk-drop.drag-over { border-color: var(--accent); background: rgba(166,241,248,0.04); }
157  .bk-slot { aspect-ratio: 1/1; border-radius: 14px; background: var(--card); border: 1px dashed var(--border); display: flex; align-items: center; justify-content: center; color: var(--muted2); cursor: pointer; transition: all 0.2s; position: relative; overflow: hidden; }
158  .bk-slot:hover { border-color: var(--accent); color: var(--accent); background: rgba(166,241,248,0.04); }
159  .bk-slot img { width: 100%; height: 100%; object-fit: cover; }
160  .bk-slot .bk-rm { position: absolute; top: 6px; right: 6px; width: 24px; height: 24px; border-radius: 50%; background: rgba(7,7,11,0.85); color: #fff; border: none; cursor: pointer; font-size: 10px; display: none; align-items: center; justify-content: center; }
161  .bk-slot:hover .bk-rm { display: flex; }
162  .bk-fingerprint { padding: 24px; background: var(--card); border: 1px solid var(--border-hi); border-radius: 20px; margin-top: 18px; }
163  .bk-fingerprint h4 { font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.14em; color: var(--muted2); margin-bottom: 14px; }
164  .bk-palette { display: flex; gap: 8px; margin-bottom: 18px; }
165  .bk-palette .chip { width: 36px; height: 36px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.1); box-shadow: 0 2px 8px rgba(0,0,0,0.3); position: relative; }
166  .bk-palette .chip::after { content: attr(data-hex); position: absolute; bottom: -22px; left: 50%; transform: translateX(-50%); font-size: 9.5px; color: var(--muted2); font-family: ui-monospace, monospace; letter-spacing: 0.02em; }
167  .bk-mood { display: inline-block; padding: 6px 12px; border-radius: 999px; background: rgba(166,241,248,0.12); color: var(--accent); font-size: 12px; font-weight: 600; letter-spacing: -0.01em; margin-bottom: 20px; }
168  .bk-text { background: var(--bg2); border: 1px solid var(--border); border-radius: 14px; padding: 14px 16px; font-size: 13px; line-height: 1.6; color: var(--text); letter-spacing: -0.01em; margin-bottom: 12px; white-space: pre-wrap; }
169  .bk-text-label { font-size: 11px; color: var(--muted2); text-transform: uppercase; letter-spacing: 0.12em; font-weight: 600; margin-bottom: 6px; }
170
171  /* ════ INFLU CHAT MODAL ════ */
172  .ic-modal { background: var(--bg2); border: 1px solid var(--border-hi); border-radius: 24px; max-width: 580px; width: 100%; max-height: 86vh; display: flex; flex-direction: column; overflow: hidden; box-shadow: var(--shadow-lg); animation: modalIn 0.35s var(--es); }
173  .ic-head { padding: 20px 26px 14px; border-bottom: 1px solid var(--border); }
174  .ic-head h3 { font-size: 20px; font-weight: 600; letter-spacing: -0.02em; display: flex; align-items: center; gap: 10px; }
175  .ic-head h3 i { background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent; font-size: 22px; }
176  .ic-head .sub { font-size: 12.5px; color: var(--muted); margin-top: 4px; letter-spacing: -0.01em; }
177  .ic-body { flex: 1; overflow-y: auto; padding: 18px 22px; display: flex; flex-direction: column; gap: 10px; }
178  .ic-msg { padding: 10px 14px; border-radius: 14px; font-size: 13.5px; line-height: 1.5; letter-spacing: -0.01em; max-width: 85%; }
179  .ic-msg.ai { background: var(--card); color: var(--text); align-self: flex-start; border: 1px solid var(--border); }
180  .ic-msg.user { background: rgba(166,241,248,0.14); color: var(--text); align-self: flex-end; border: 1px solid rgba(166,241,248,0.25); }
181  .ic-msg.system { background: transparent; color: var(--muted); text-align: center; font-size: 12px; align-self: center; }
182  .ic-typing { display: inline-flex; align-items: center; gap: 4px; padding: 10px 16px; border-radius: 14px; background: var(--card); border: 1px solid var(--border); align-self: flex-start; }
183  .ic-typing span { width: 6px; height: 6px; border-radius: 50%; background: var(--muted2); animation: icDot 1.2s infinite; }
184  .ic-typing span:nth-child(2) { animation-delay: 0.2s; }
185  .ic-typing span:nth-child(3) { animation-delay: 0.4s; }
186  @keyframes icDot { 0%,80%,100% { opacity: 0.3; transform: scale(0.8); } 40% { opacity: 1; transform: scale(1); } }
187  .ic-input-wrap { padding: 14px 22px 18px; border-top: 1px solid var(--border); display: flex; gap: 10px; }
188  .ic-input { flex: 1; padding: 11px 14px; background: var(--card); border: 1px solid var(--border); border-radius: 14px; color: var(--text); font-family: inherit; font-size: 13.5px; outline: none; resize: none; min-height: 42px; max-height: 120px; letter-spacing: -0.01em; }
189  .ic-input:focus { border-color: rgba(166,241,248,0.35); }
190  .ic-send { width: 42px; height: 42px; border-radius: 50%; background: var(--grad); color: #07070b; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 13px; transition: all 0.2s var(--es); flex-shrink: 0; }
191  .ic-send:hover:not(:disabled) { transform: scale(1.06); }
192  .ic-send:disabled { opacity: 0.4;
192 cursor: not-allowed; }
193  .ic-generating { padding: 24px; text-align: center; background: var(--card); border: 1px solid var(--border); border-radius: 18px; margin-top: 12px; }
194  .ic-generating .spin { width: 36px; height: 36px; border: 3px solid rgba(255,255,255,0.1); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; margin: 0 auto 12px; }
195  .ic-done { padding: 20px; background: var(--card); border: 1px solid var(--success); border-radius: 18px; margin-top: 12px; }
196  .ic-done img { width: 100%; aspect-ratio: 9/16; object-fit: cover; border-radius: 14px; margin-bottom: 14px; }
197
198  /* ════ EDITOR MODAL (premium soft look) ════ */
199  .ed-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.82); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); z-index: 200; display: none; align-items: stretch; padding: 20px; animation: fadeIn 0.2s var(--eo); }
200  .ed-backdrop.show { display: flex; }
201  .ed-modal { background: var(--bg2); border: 1px solid var(--border-hi); border-radius: 28px; flex: 1; display: flex; flex-direction: column; overflow: hidden; box-shadow: var(--shadow-lg); max-width: 1600px; margin: 0 auto; animation: modalIn 0.4s var(--es); }
202  .ed-head { padding: 20px 26px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--border); }
203  .ed-head h3 { font-size: 18px; font-weight: 600; letter-spacing: -0.02em; }
204  .ed-head .ed-sub { font-size: 12px; color: var(--muted); margin-top: 2px; letter-spacing: -0.01em; }
205  .ed-close { width: 36px; height: 36px; border-radius: 12px; background: var(--card); border: none; color: var(--text); cursor: pointer; transition: all 0.2s; font-size: 14px; display: flex; align-items: center; justify-content: center; }
206  .ed-close:hover { background: var(--card-hi); }
207  .ed-body { flex: 1; display: grid; grid-template-columns: 200px 1fr 320px; gap: 1px; background: var(--border); overflow: hidden; min-height: 0; }
208  @media (max-width: 1200px) { .ed-body { grid-template-columns: 180px 1fr 280px; } }
209  @media (max-width: 900px) { .ed-body { grid-template-columns: 1fr; } }
210  .ed-tools { background: var(--bg2); padding: 16px; display: flex; flex-direction: column; gap: 4px; overflow-y: auto; }
211  .ed-tool { padding: 11px 14px; border-radius: 12px; background: transparent; border: 1px solid transparent; color: var(--muted); font-size: 13px; font-weight: 500; cursor: pointer; text-align: left; display: flex; align-items: center; gap: 10px; transition: all 0.2s var(--eo); font-family: inherit; letter-spacing: -0.01em; }
212  .ed-tool i { width: 16px; text-align: center; opacity: 0.7; font-size: 12px; }
213  .ed-tool:hover { background: var(--card); color: var(--text); }
214  .ed-tool.on { background: var(--card-hi); color: var(--text); font-weight: 600; border-color: rgba(255,255,255,0.06); }
215  .ed-tool.on i { opacity: 1; color: var(--accent); }
216  .ed-canvas { background: var(--bg); position: relative; display: flex; align-items: center; justify-content: center; overflow: hidden; min-height: 400px; }
217  .ed-canvas::before { content: ''; position: absolute; inset: 0; background-image: repeating-conic-gradient(rgba(255,255,255,0.018) 0% 25%, transparent 0% 50%); background-size: 22px 22px; opacity: 0.5; pointer-events: none; }
218  .ed-canvas img { max-width: 94%; max-height: 94%; border-radius: 16px; box-shadow: 0 20px 60px rgba(0,0,0,0.5); position: relative; z-index: 1; object-fit: contain; transition: filter 0.15s ease; }
219
220  /* ═══ TEXT TOOL — draggable layers nad obrazem ═══ */
221  #ed-text-stage {
222    position: absolute; inset: 0;
223    display: none;
224    align-items: center; justify-content: center;
225    z-index: 5; pointer-events: none;
226    overflow: auto; padding: 16px;
227  }
228  #ed-text-stage.active { display: flex; pointer-events: auto; }
229  #ed-text-frame {
230    position: relative;
231    display: inline-block;
232    flex-shrink: 0;
233    margin: auto;
234    border-radius: 16px;
235    cursor: text;
236    box-shadow: 0 20px 60px rgba(0,0,0,0.5);
237    line-height: 0;  /* żeby frame nie miał extra spacingu pod img */
238  }
239  #ed-text-frame img {
240    display: block;
241    max-width: 90vw;
242    max-height: calc(100vh - 320px);
243    width: auto; height: auto;
244    object-fit: contain;
245    border-radius: 16px;
246    pointer-events: none;
247  }
248  .ed-text-inline {
249    position: absolute;
250    background: rgba(0,0,0,0.65); color: #fff;
251    border: 2px solid rgba(166,241,248,0.85);
252    padding: 8px 14px; border-radius: 8px;
253    text-align: center; outline: none;
254    backdrop-filter: blur(8px);
255    font-family: inherit; font-weight: 600;
256    z-index: 12;
257    box-shadow: 0 8px 32px rgba(0,0,0,0.4);
258    box-sizing: border-box;
259    line-height: 1.2;
260  }
261  /* Drag-to-select rectangle preview */
262  .ed-text-selrect {
263    position: absolute;
264    border: 2px dashed var(--accent);
265    background: rgba(166,241,248,0.12);
266    pointer-events: none;
267    z-index: 11;
268    border-radius: 4px;
269  }
270  .ed-tlayer {
271    position: absolute; user-select: none; cursor: move; padding: 4px 10px;
272    border: 1px dashed transparent; border-radius: 4px;
273    transition: border-color 0.15s, background 0.15s;
274    transform: translate(-50%, -50%);
275    line-height: 1.15; white-space: pre;
276    z-index: 8;
277    text-shadow: 0 1px 4px rgba(0,0,0,0.4);
278  }
279  .ed-tlayer:hover { border-color: rgba(166,241,248,0.4); background: rgba(0,0,0,0.06); }
280  .ed-tlayer.selected { border-color: var(--accent); background: rgba(166,241,248,0.12); }
281  .ed-tlayer-del {
282    position: absolute; top: -10px; right: -10px;
283    width: 22px; height: 22px; border-radius: 50%;
284    background: var(--danger); color: #fff; border: none;
285    font-size: 10px; cursor: pointer; display: none;
286    align-items: center; justify-content: center;
287  }
288  .ed-tlayer.selected .ed-tlayer-del { display: flex; }
289  .ed-text-color-row { display: flex; gap: 8px; flex-wrap: wrap; }
290  .ed-color-pill {
291    width: 28px; height: 28px; border-radius: 50%; cursor: pointer;
292    border: 2px solid rgba(255,255,255,0.1); transition: all 0.15s var(--eo);
293  }
294  .ed-color-pill:hover { transform: scale(1.1); }
295  .ed-color-pill.on { border-color: var(--accent); transform: scale(1.15); box-shadow: 0 0 0 3px rgba(166,241,248,0.15); }
296  .ed-text-fonts { display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; }
297  .ed-text-font {
298    background: var(--card); border: 1px solid rgba(255,255,255,0.06);
299    color: var(--text); padding: 8px 10px; border-radius: 8px;
300    cursor: pointer; font-size: 12px; transition: all 0.15s var(--eo);
301  }
302  .ed-text-font:hover { background: var(--card-hi); }
303  .ed-text-font.on { border-color: var(--accent); background: rgba(166,241,248,0.08); color: var(--accent); }
304  .ed-loading { position: absolute; inset: 0; background: rgba(13,13,18,0.78); backdrop-filter: blur(8px); display: none; align-items: center; justify-content: center; flex-direction: column; gap: 14px; z-index: 10; }
305  .ed-loading.show { display: flex; }
306  .ed-loading .spin { width: 40px; height: 40px; border: 3px solid rgba(255,255,255,0.1); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; }
307  .ed-loading-text { font-size: 13px; color: var(--muted); letter-spacing: -0.01em; }
308
309  .ed-panel { background: var(--bg2); padding: 22px; overflow-y: auto; display: flex; flex-direction: column; gap: 18px; }
310  .ed-panel h4 { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.14em; color: var(--muted2); margin-bottom: 4px; }
311  .ed-panel label { display: block; font-size: 11px; font-weight: 600; color: var(--muted2); text-transform: uppercase; letter-spacing: 0.12em; margin-bottom: 8px; }
312  .ed-panel .hint-txt { font-size: 12px; color: var(--muted); line-height: 1.55; letter-spacing: -0.01em; padding: 10px 14px; background: var(--card); border-radius: 12px; border: 1px solid var(--border); }
313  .ed-panel .hint-txt i { color: var(--accent); margin-right: 6px; }
314  .ed-ta { width: 100%; padding: 12px 14px; background: var(--card); border: 1px solid var(--border); border-radius: 14px; color: var(--text); font-family: inherit; font-size: 13px; resize: vertical; min-height: 90px; outline: none; transition: all 0.2s; letter-spacing: -0.01em; line-height: 1.5; }
315  .ed-ta:focus { border-color: rgba(166,241,248,0.35); background: var(--card-hi); }
316  .ed-ratios { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
317  .ed-ratio-btn { padding: 10px 6px; border-radius: 10px; background: var(--card); border: 1px solid var(--border); color: var(--muted); font-size: 11.5px; font-weight: 600; cursor: pointer; transition: all 0.2s var(--eo); font-family: inherit; letter-spacing: -0.01em; }
318  .ed-ratio-btn:hover { background: var(--card-hi); color: var(--text); }
319  .ed-ratio-btn.on { background: rgba(166,241,248,0.14); border-color: rgba(166,241,248,0.4); color: var(--accent); }
320  .ed-scale { display: flex; gap: 8px; }
321  .ed-scale-btn { flex: 1; padding: 12px 8px; border-radius: 12px; background: var(--card); border: 1px solid var(--border); color: var(--muted); font-size: 14px; font-weight: 700; cursor: pointer; transition: all 0.2s var(--eo); font-family: inherit; }
322  .ed-scale-btn:hover { background: var(--card-hi); color: var(--text); }
323  .ed-scale-btn.on { background: rgba(166,241,248,0.14); border-color: rgba(166,241,248,0.4); color: var(--accent); }
324  .ed-slider-row { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
325  .ed-slider-head { display: flex; justify-content: space-between; align-items: center; }
326  .ed-slider-head label { margin: 0; font-size: 11px; color: var(--muted); text-transform: none; letter-spacing: -0.01em; font-weight: 500; }
327  .ed-slider-head .val { font-size: 11.5px; color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
328  .ed-slider { -webkit-appearance: none; appearance: none; width: 100%; height: 4px; border-radius: 2px; background: rgba(255,255,255,0.08); outline: none; cursor: pointer; }
329  .ed-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--accent); cursor: pointer; box-shadow: 0 0 12px rgba(166,241,248,0.4); border: 2px solid var(--bg2); }
330  .ed-slider::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%; background: var(--accent); cursor: pointer; border: 2px solid var(--bg2); }
331  .ed-cam-presets { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin-top: 6px; }
332  .ed-cam-preset { padding: 7px 4px; border-radius: 8px; background: var(--card); border: 1px solid var(--border); color: var(--muted); font-size: 10.5px; font-weight: 500; cursor: pointer; transition: all 0.2s; font-family: inherit; letter-spacing: -0.01em; }
333  .ed-cam-preset:hover { background: var(--card-hi); color: var(--text); }
334  .ed-status { font-size: 12px; color: var(--muted); text-align: center; padding: 8px; border-radius: 10px; letter-spacing: -0.01em; min-height: 20px; }
335  .ed-status.ok { color: var(--success); }
336  .ed-status.err { color: var(--danger); }
337  .ed-actions { display: flex; flex-direction: column; gap: 8px; margin-top: auto; padding-top: 14px; border-top: 1px solid var(--border); }
338  .ed-go { padding: 13px 18px; border-radius: 14px; background: var(--grad); color: #07070b; border: none; cursor: pointer; font-weight: 700; font-size: 13.5px; letter-spacing: -0.01em; transition: all 0.25s var(--es); font-family: inherit; display: inline-flex; align-items: center; justify-content: center; gap: 8px; }
339  .ed-go:hover:not(:disabled) { transform: scale(1.02); box-shadow: 0 14px 34px rgba(166,241,248,0.25); }
340  .ed-go:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
341  .ed-foot { padding: 14px 22px; display: flex; gap: 10px; justify-content: space-between; align-items: center; border-top: 1px solid var(--border); background: var(--bg2); }
342  .ed-foot-left { display: flex; gap: 8px; }
343  .ed-foot-btn { padding: 10px 16px; border-radius: 12px; background: var(--card); border: 1px solid var(--border); color: var(--text); cursor: pointer; font-size: 12.5px; font-weight: 600; transition: all 0.2s; font-family: inherit; display: inline-flex; align-items: center; gap: 7px; letter-spacing: -0.01em; }
344  .ed-foot-btn:hover { background: var(--card-hi); }
345  .ed-foot-btn.primary { background: #fff; color: #07070b; border: none; }
346  .ed-foot-btn.primary:hover { transform: scale(1.03); }
347
348  /* ════ RIGHT COLUMN (History + Library quicklinks) ════ */
349  .col-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
350  .col-title { font-size: 12px; text-transform: uppercase; letter-spacing: 0.14em; color: var(--muted2); font-weight: 600; }
351  .col-count { font-size: 11px; color: var(--muted); padding: 2px 8px; border-radius: 999px; background: var(--card); font-weight: 600; }
352  .hist { display: flex; flex-direction: column; gap: 14px; }
353  .hist-item { position: relative; border-radius: 18px; overflow: hidden; background: var(--card); cursor: pointer; transition: all 0.3s var(--eo); }
354  .hist-item::after { content: ''; position: absolute; inset: 0; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.05); border-radius: inherit; pointer-events: none; }
355  .hist-item:hover { transform: translateY(-2px); }
356  .hist-item img, .hist-item video { width: 100%; aspect-ratio: 1/1; object-fit: cover; display: block; }
357  .hist-item.pending { aspect-ratio: 1/1; display: flex; align-items: center; justify-content: center; background: var(--grad-soft); }
358  .hist-item.pending .spin { width: 28px; height: 28px; border: 2px solid rgba(255,255,255,0.1); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; }
359  .hist-meta { padding: 10px 14px 12px; font-size: 11px; color: var(--muted); line-height: 1.4; }
360  .hist-prompt { color: var(--text); font-weight: 500; font-size: 12.5px; letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 3px; }
361  .hist-time { color: var(--muted2); font-size: 10.5px; }
362  @keyframes spin { to { transform: rotate(360deg); } }
363
364  /* ════ HOVER ACTIONS overlay na hist-item (premium soft look) ════ */
365  .hist-actions { position: absolute; top: 10px; right: 10px; display: flex; gap: 6px; opacity: 0; transform: translateY(-4px); transition: opacity 0.25s var(--eo), transform 0.3s var(--es); z-index: 5; pointer-events: none; }
366  .hist-item:hover .hist-actions { opacity: 1; transform: translateY(0); pointer-events: auto; }
367  .ha-btn { width: 32px; height: 32px; border-radius: 11px; background: rgba(13,13,18,0.72); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); border: 1px solid rgba(255,255,255,0.1); color: rgba(255,255,255,0.88); font-size: 11.5px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s var(--eo); padding: 0; }
368  .ha-btn:hover { background: rgba(255,255,255,0.95); color: #07070b; border-color: rgba(255,255,255,0.95); transform: scale(1.08); }
369  .ha-btn.danger:hover { background: var(--danger); color: #fff; border-color: var(--danger); }
370  .ha-btn.accent:hover { background: var(--accent); color: #07070b; border-color: var(--accent); }
371  .ha-btn.primary { background: rgba(125,249,255,0.92); color: #07070b; border-color: rgba(125,249,255,0.92); }
372  .ha-btn.primary:hover { background: #fff; color: #07070b; transform: scale(1.08); }
373  .hist-bot-actions { position: absolute; left: 10px; bottom: 10px; display: flex; gap: 6px; opacity: 0; transform: translateY(4px); transition: opacity 0.25s var(--eo), transform 0.3s var(--es); z-index: 5; pointer-events: none; }
374  .hist-item:hover .hist-bot-actions { opacity: 1; transform: translateY(0); pointer-events: auto; }
375
376  .empty { padding: 48px 20px; text-align: center; color: var(--muted); }
377  .empty i { font-size: 32px; color: var(--muted2); margin-bottom: 14px; display: block; }
378  .empty h4 { font-size: 14px; font-weight: 600; color: var(--text); margin-bottom: 6px; letter-spacing: -0.01em; }
379  .empty p { font-size: 12.5px; line-height: 1.5; }
380
381  .lib-quick { margin-top: 32px; padding: 16px; background: var(--card); border-radius: 18px; border: 1px solid var(--border); }
382  .lib-quick h4 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.14em; color: var(--muted2); font-weight: 600; margin-bottom: 10px; }
383  .lib-links { display: flex; flex-direction: column; gap: 2px; }
384  .lib-link { display: flex; align-items: center; justify-content: space-between; padding: 9px 12px; border-radius: 10px; color: var(--muted); font-size: 13px; cursor: pointer; transition: all 0.2s var(--eo); background: transparent; border: none; text-align: left; font-family: inherit; width: 100%; letter-spacing: -0.01em; }
385  .lib-link:hover { background: var(--card-hi); color: var(--text); }
386  .lib-link i { opacity: 0.6; font-size: 11px; }
387  .lib-link .cnt { font-size: 11px; font-weight: 600; color: var(--accent); background: rgba(166,241,248,0.1); padding: 2px 7px; border-radius: 999px; }
388
389  /* ════ LIBRARY VIEW ════ */
390  .lib-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 32px; max-width: 1400px; }
391  .lib-tabs { display: flex; padding: 4px; background: var(--card); border-radius: 14px; }
392  .lib-tab { padding: 9px 18px; border-radius: 10px; font-size: 13px; font-weight: 500; color: var(--muted); cursor: pointer; transition: all 0.2s var(--eo); letter-spacing: -0.01em; border: none; background: transparent; font-family: inherit; }
393  .lib-tab.on { background: rgba(255,255,255,0.08); color: var(--text); font-weight: 600; }
394  .btn-add { padding: 10px 18px; border-radius: 12px; background: #fff; color: #07070b; font-size: 13px; font-weight: 600; cursor: pointer; border: none; font-family: inherit; display: inline-flex; align-items: center; gap: 7px; transition: all 0.2s var(--es); letter-spacing: -0.01em; }
395  .btn-add:hover { transform: scale(1.03); box-shadow: 0 10px 30px rgba(255,255,255,0.1); }
396  .assets-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; max-width: 1400px; }
397  .asset { position: relative; border-radius: 20px; overflow: hidden; background: var(--card); cursor: pointer; transition: all 0.3s var(--eo); }
398  .asset::after { content: ''; position: absolute; inset: 0; box-shadow: inset 0 0 0 1px var(--border); border-radius: inherit; pointer-events: none; transition: box-shadow 0.3s var(--eo); }
399  .asset:hover { transform: translateY(-3px); }
400  .asset:hover::after { box-shadow: inset 0 0 0 1px var(--border-hi); }
401  .asset img { width: 100%; aspect-ratio: 1/1; object-fit: cover; display: block; }
402  .asset-info { padding: 12px 14px 14px; }
403  .asset-name { font-size: 13.5px; font-weight: 600; letter-spacing: -0.01em; }
404  .asset-name::before { content: '@'; color: var(--accent); margin-right: 2px; }
405  .asset-desc { font-size: 10.5px; color: var(--muted); margin-top: 3px; letter-spacing: -0.01em; line-height: 1.35; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
406  .asset-badge { position: absolute; top: 10px; left: 10px; padding: 4px 9px; border-radius: 999px; background: rgba(125,249,255,0.15); backdrop-filter: blur(10px); color: var(--accent); font-size: 9.5px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; display: inline-flex; align-items: center; gap: 4px;
406 border: 1px solid rgba(125,249,255,0.25); }
407  .asset.public::after { background: linear-gradient(135deg, rgba(125,249,255,0.03), transparent 50%); }
408  .asset-del { position: absolute; top: 10px; right: 10px; width: 28px; height: 28px; border-radius: 10px; background: rgba(7,7,11,0.7); backdrop-filter: blur(10px); color: var(--text); border: none; cursor: pointer; opacity: 0; transition: all 0.2s var(--eo); font-size: 11px; display: flex; align-items: center; justify-content: center; }
409  .asset:hover .asset-del { opacity: 1; }
410  .asset-del:hover { background: var(--danger); }
411
412  /* ════ INPUT DOCK ════ */
413  .dock { position: fixed; bottom: 28px; left: 50%; transform: translateX(-50%); width: min(720px, calc(100vw - 64px - 340px)); background: rgba(13,13,18,0.85); backdrop-filter: var(--blur); -webkit-backdrop-filter: var(--blur); border: 1px solid rgba(255,255,255,0.08); border-radius: 28px; padding: 8px; z-index: 40; box-shadow: 0 -6px 40px rgba(0,0,0,0.3), 0 40px 100px rgba(0,0,0,0.5); transition: all 0.35s var(--eo); }
414  .dock:focus-within { border-color: rgba(166,241,248,0.25); box-shadow: 0 -6px 40px rgba(0,0,0,0.4), 0 40px 100px rgba(0,0,0,0.55), 0 0 0 6px rgba(166,241,248,0.05); }
415  .dock.drag-over { border-color: rgba(166,241,248,0.6); background: rgba(13,13,18,0.95); box-shadow: 0 -6px 40px rgba(0,0,0,0.5), 0 40px 100px rgba(0,0,0,0.6), 0 0 0 8px rgba(166,241,248,0.12); transform: translateX(-50%) scale(1.01); }
416  .dock.drag-over::before { content: 'Upuść zdjęcie aby dodać do promptu'; position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: rgba(166,241,248,0.08); border-radius: 28px; font-size: 13.5px; font-weight: 600; color: var(--accent); pointer-events: none; letter-spacing: -0.01em; z-index: 2; }
417  .dock-inner { padding: 12px 16px 8px; }
418  .dock-refs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }
419  .dock-refs:empty { display: none; }
420  .chip { display: inline-flex; align-items: center; gap: 8px; padding: 3px 12px 3px 3px; border-radius: 999px; background: var(--card); border: 1px solid var(--border); font-size: 12.5px; letter-spacing: -0.01em; animation: pop 0.3s var(--es); }
421  @keyframes pop { from { opacity: 0; transform: translateY(6px) scale(0.85); } to { opacity: 1; transform: translateY(0) scale(1); } }
422  .chip img { width: 24px; height: 24px; border-radius: 50%; object-fit: cover; }
423  .chip .t { color: var(--muted2); font-size: 9px; text-transform: uppercase; letter-spacing: 0.1em; font-weight: 600; }
424  .chip-x { cursor: pointer; color: var(--muted); font-size: 10px; padding: 3px 5px; border-radius: 5px; transition: all 0.15s; }
425  .chip-x:hover { background: rgba(248,113,113,0.15); color: var(--danger); }
426  .p-input { width: 100%; background: transparent; border: none; outline: none; color: var(--text); font-family: inherit; font-size: 15.5px; resize: none; min-height: 44px; max-height: 180px; padding: 4px 0; line-height: 1.5; letter-spacing: -0.015em; }
427  .p-input::placeholder { color: var(--muted2); letter-spacing: -0.015em; }
428  .dock-bar { display: flex; align-items: center; gap: 6px; padding: 8px 4px 0; flex-wrap: wrap; }
429  .pill { display: inline-flex; align-items: center; gap: 6px; padding: 8px 12px; border-radius: 12px; background: transparent; color: var(--muted); font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.2s var(--eo); letter-spacing: -0.01em; border: none; font-family: inherit; }
430  .pill-help-icon { font-size: 10px; opacity: 0.5; margin-left: 2px; }
431  .pill-with-help { position: relative; }
432  .pill-with-help:hover .pill-help-icon { opacity: 1; }
433  .pill-with-help::after { content: attr(data-help); position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%); background: rgba(15,18,28,0.96); color: #e6e9ef; padding: 10px 14px; border-radius: 10px; font-size: 11.5px; font-weight: 400; line-height: 1.45; letter-spacing: -0.01em; width: 280px; max-width: 90vw; text-align: left; opacity: 0; pointer-events: none; transition: opacity 0.2s; z-index: 100; border: 1px solid rgba(255,255,255,0.08); box-shadow: 0 8px 24px rgba(0,0,0,0.4); white-space: normal; }
434  .pill-with-help:hover::after { opacity: 1; }
435  .pill:hover { background: var(--card); color: var(--text); }
436  .pill.studio { background: rgba(212,191,249,0.14); color: var(--accent2); padding-right: 10px; }
437  .pill.studio:hover { background: rgba(212,191,249,0.2); }
438  .pill.add-btn { background: rgba(255,255,255,0.04); color: var(--muted); border: 1px dashed rgba(255,255,255,0.12); }
439  .pill.add-btn:hover { background: rgba(125,249,255,0.08); color: var(--accent); border-color: rgba(125,249,255,0.35); border-style: solid; }
440  .pill.add-btn.glow { animation: glowPulse 1.5s ease-in-out infinite; border-color: rgba(212,191,249,0.5); color: var(--accent2); }
441  @keyframes glowPulse { 0%,100% { box-shadow: 0 0 0 0 rgba(212,191,249,0.3); } 50% { box-shadow: 0 0 0 6px rgba(212,191,249,0); } }
442
443  /* ════ ASSET PICKER (drawer) ════ */
444  .picker-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(14px); z-index: 90; display: none; align-items: center; justify-content: center; padding: 24px; }
445  .picker-backdrop.show { display: flex; animation: fadeIn 0.2s var(--eo); }
446  .picker { background: var(--bg2); border: 1px solid var(--border-hi); border-radius: 28px; max-width: 920px; width: 100%; max-height: 85vh; display: flex; flex-direction: column; box-shadow: var(--shadow-lg); overflow: hidden; animation: modalIn 0.35s var(--es); }
447  .picker-head { padding: 22px 28px 0; display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
448  .picker-head h3 { font-size: 22px; font-weight: 600; letter-spacing: -0.03em; }
449  .picker-head .picker-sub { font-size: 13px; color: var(--muted); margin-top: 2px; letter-spacing: -0.01em; }
450  .picker-close { width: 36px; height: 36px; border-radius: 12px; background: var(--card); border: none; color: var(--text); cursor: pointer; transition: all 0.2s; font-size: 14px; display: flex; align-items: center; justify-content: center; }
451  .picker-close:hover { background: var(--card-hi); }
452  .picker-tabs { padding: 16px 28px 0; display: flex; gap: 4px; flex-wrap: wrap; align-items: center; }
453  .picker-tab { padding: 8px 16px; border-radius: 10px; background: transparent; color: var(--muted); font-size: 12.5px; font-weight: 500; cursor: pointer; transition: all 0.2s var(--eo); border: 1px solid transparent; font-family: inherit; letter-spacing: -0.01em; }
454  .picker-tab:hover { color: var(--text); background: var(--card); }
455  .picker-tab.on { background: var(--card-hi); color: var(--text); font-weight: 600; }
456  .picker-upload-btn { margin-left: auto; padding: 8px 16px; border-radius: 10px; background: #fff; color: #07070b; font-size: 12px; font-weight: 600; border: none; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; font-family: inherit; transition: all 0.2s var(--es); }
457  .picker-upload-btn:hover { transform: scale(1.03); }
458  .picker-body { flex: 1; overflow-y: auto; padding: 20px 28px 28px; }
459  .picker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 14px; }
460  .p-tile { position: relative; border-radius: 16px; overflow: hidden; background: var(--card); cursor: pointer; transition: all 0.3s var(--eo); aspect-ratio: 1/1; }
461  .p-tile::after { content: ''; position: absolute; inset: 0; box-shadow: inset 0 0 0 1px var(--border); border-radius: inherit; pointer-events: none; transition: box-shadow 0.25s; }
462  .p-tile:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }
463  .p-tile:hover::after { box-shadow: inset 0 0 0 2px rgba(125,249,255,0.6); }
464  .p-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
465  .p-tile-info { position: absolute; bottom: 0; left: 0; right: 0; padding: 8px 12px 10px; background: linear-gradient(to top, rgba(7,7,11,0.95), transparent); z-index: 2; }
466  .p-tile-name { font-size: 12px; font-weight: 600; color: #fff; letter-spacing: -0.01em; }
467  .p-tile-name::before { content: '@'; color: var(--accent); margin-right: 1px; }
468  .p-tile-badge { position: absolute; top: 8px; right: 8px; padding: 3px 8px; border-radius: 999px; background: rgba(125,249,255,0.92); color: #07070b; font-size: 9px; font-weight: 700; letter-spacing: 0.08em; z-index: 3; display: flex; align-items: center; gap: 3px; }
469  .p-tile-desc { font-size: 9.5px; color: rgba(255,255,255,0.6); margin-top: 2px; letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
470
471  .p-empty { padding: 60px 20px; text-align: center; color: var(--muted); }
472  .p-empty i { font-size: 36px; color: var(--muted2); margin-bottom: 14px; display: block; }
473  .p-empty h4 { font-size: 15px; font-weight: 600; color: var(--text); margin-bottom: 6px; letter-spacing: -0.01em; }
474  .p-empty p { font-size: 13px; line-height: 1.5; max-width: 360px; margin: 0 auto 16px; }
475  .pill i { font-size: 10.5px; }
476  .spacer { flex: 1; }
477  .cost { font-size: 11.5px; color: var(--muted); letter-spacing: -0.01em; margin-right: 6px; }
478  .send { width: 46px; height: 46px; border-radius: 16px; background: var(--grad); border: none; color: #07070b; font-size: 16px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: transform 0.25s var(--es), box-shadow 0.25s var(--eo); box-shadow: 0 10px 24px rgba(166,241,248,0.25); }
479  .send:hover { transform: scale(1.08); box-shadow: 0 14px 34px rgba(166,241,248,0.35); }
480  .send:active { transform: scale(0.95); }
481  .send:disabled { opacity: 0.4;
481 cursor: not-allowed; transform: none; box-shadow: none; }
482  .send .fa-spinner { animation: spin 0.6s linear infinite; }
483
484  /* Mention dropdown */
485  .mention-dd { position: absolute; bottom: calc(100% + 12px); left: 0; right: 0; background: rgba(13,13,18,0.96); backdrop-filter: var(--blur); border: 1px solid var(--border-hi); border-radius: 18px; padding: 6px; max-height: 320px; overflow-y: auto; box-shadow: var(--shadow-lg); display: none; z-index: 60; }
486  .mention-dd.show { display: block; animation: pop 0.2s var(--es); }
487  .mi { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 12px; cursor: pointer; transition: all 0.15s var(--eo); border: none; background: transparent; width: 100%; text-align: left; font-family: inherit; color: var(--text); }
488  .mi:hover, .mi.active { background: var(--card-hi); }
489  .mi img { width: 40px; height: 40px; border-radius: 12px; object-fit: cover; }
490  .mi-name { font-size: 13.5px; font-weight: 600; letter-spacing: -0.01em; }
491  .mi-type { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.1em;
491 margin-top: 1px; font-weight: 500; }
492
493  /* ════ MODAL ════ */
494  .backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(12px); z-index: 100; display: none; align-items: center; justify-content: center; padding: 24px; }
495  .backdrop.show { display: flex; animation: fadeIn 0.2s var(--eo); }
496  @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
497  .modal { background: var(--bg2); border: 1px solid var(--border-hi); border-radius: 28px; max-width: 480px; width: 100%; padding: 32px; animation: modalIn 0.35s var(--es); box-shadow: var(--shadow-lg); }
498  @keyframes modalIn { from { opacity: 0; transform: scale(0.92) translateY(20px); } to { opacity: 1; transform: scale(1) translateY(0); } }
499  .modal h3 { font-size: 22px; font-weight: 600; letter-spacing: -0.03em; margin-bottom: 8px; line-height: 1.15; }
500  .modal p { color: var(--muted); font-size: 13.5px; margin-bottom: 24px; letter-spacing: -0.01em; line-height: 1.5; }
501  .fld-label { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.14em; color: var(--muted2); font-weight: 600; margin-bottom: 8px; display: block; }
502  .fld { width: 100%; padding: 13px 16px; background: var(--card); border: 1px solid var(--border); border-radius: 14px; color: var(--text); font-family: inherit; font-size: 14px; outline: none; margin-bottom: 14px; transition: all 0.2s var(--eo); letter-spacing: -0.01em; }
503  .fld:focus { border-color: rgba(166,241,248,0.35); background: var(--card-hi); }
504  .drop { border: 2px dashed var(--border-hi); border-radius: 20px; padding: 40px 20px; text-align: center; cursor: pointer; transition: all 0.25s var(--eo); margin-bottom: 16px; position: relative; background: transparent; }
505  .drop:hover, .drop.dragging { border-color: var(--accent); background: rgba(166,241,248,0.025); }
506  .drop i { font-size: 28px; color: var(--muted); display: block; margin-bottom: 10px; }
507  .drop img { max-width: 100%; max-height: 180px; border-radius: 12px; }
508  .drop-txt { font-size: 13px; color: var(--muted); letter-spacing: -0.01em; }
509  .m-actions { display: flex; gap: 10px; justify-content: flex-end; }
510  .btn { padding: 11px 20px; border-radius: 14px; border: 1px solid var(--border-hi); background: var(--card); color: var(--text); font-size: 13.5px; font-weight: 500; cursor: pointer; transition: all 0.2s var(--eo); font-family: inherit; letter-spacing: -0.01em; }
511  .btn:hover { background: var(--card-hi); }
512  .btn-pr { background: #fff; color: #07070b; border: none; font-weight: 600; }
513  .btn-pr:hover { transform: scale(1.02); box-shadow: 0 10px 24px rgba(255,255,255,0.15); }
514  .btn-pr:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; }
515
516  /* Toast */
517  .toast { position: fixed; bottom: 140px; left: 50%; transform: translateX(-50%); padding: 12px 20px; background: rgba(13,13,18,0.95); backdrop-filter: var(--blur); border: 1px solid var(--border-hi); border-radius: 14px; font-size: 13px; z-index: 200; box-shadow: var(--shadow-lg); display: none; letter-spacing: -0.01em; }
518  .toast.show { display: block; animation: slideUp 0.3s var(--es); }
519  @keyframes slideUp { from { opacity: 0; transform: translate(-50%, 20px); } to { opacity: 1; transform: translate(-50%, 0); } }
520
521  /* Hidden view default */
522  .view { display: none; }
523  .view.on { display: block; animation: viewIn 0.35s var(--eo); }
524
525  /* ═══ LANDING — PREMIUM ═══ */
526  .land-wrap { max-width: 1280px; margin: 0 auto; padding: 0 24px; position: relative; z-index: 1; }
527
528  /* (mesh + noise overlay zastąpione tłem chmurka.ai — aurora + dots na body::before/::after) */
529  body.is-landing { position: relative; }
530  body.is-landing .center { position: relative; z-index: 2; }
531
532  /* Reveal-on-scroll */
533  .reveal { opacity: 0; transform: translateY(24px); transition: opacity 0.7s cubic-bezier(0.2, 0.7, 0.2, 1), transform 0.7s cubic-bezier(0.2, 0.7, 0.2, 1); }
534  .reveal.in { opacity: 1; transform: translateY(0); }
535  .reveal-delay-1 { transition-delay: 0.08s; }
536  .reveal-delay-2 { transition-delay: 0.16s; }
537  .reveal-delay-3 { transition-delay: 0.24s; }
538  .land-hero { padding: 96px 0 64px; text-align: center; position: relative; overflow: hidden; }
539  .land-hero::before {
540    content: ''; position: absolute; inset: -80px -200px 0 -200px;
541    background:
542      radial-gradient(ellipse at 50% 30%, rgba(166,241,248,0.14), transparent 60%),
543      radial-gradient(ellipse at 30% 70%, rgba(186,148,255,0.08), transparent 55%);
544    pointer-events: none; z-index: 0;
545    animation: heroGlow 8s ease-in-out infinite alternate;
546  }
547  @keyframes heroGlow {
548    0%, 100% { opacity: 0.85; transform: scale(1); }
549    50%      { opacity: 1;    transform: scale(1.08); }
550  }
551  .land-eyebrow {
552    display: inline-flex; align-items: center; gap: 8px;
553    font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--accent);
554    padding: 7px 16px; border-radius: 999px; margin-bottom: 28px; position: relative; z-index: 1; font-weight: 600;
555    background: linear-gradient(135deg, rgba(166,241,248,0.10), rgba(186,148,255,0.08));
556    border: 1px solid rgba(166,241,248,0.25); backdrop-filter: blur(6px);
557    box-shadow: 0 4px 20px rgba(166,241,248,0.08), inset 0 1px 0 rgba(255,255,255,0.04);
558  }
559  .land-eyebrow::before {
560    content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--accent);
561    box-shadow: 0 0 8px var(--accent);
562    animation: blink 2s ease-in-out infinite;
563  }
564  @keyframes blink {
565    0%, 100% { opacity: 1; }
566    50% { opacity: 0.35; }
567  }
568  .land-title { font-family: 'Cormorant Garamond', serif; font-size: clamp(40px, 7vw, 88px); line-height: 1.02; letter-spacing: -0.028em; font-weight: 500; color: var(--text); margin: 0 0 22px; position: relative; z-index: 1; }
569  .land-title em {
570    font-style: italic;
571    background: linear-gradient(135deg, var(--accent) 0%, #c5a0ff 100%);
572    -webkit-background-clip: text; background-clip: text;
573    -webkit-text-fill-color: transparent;
574    color: transparent;
575  }
576  .land-sub { font-size: clamp(15px, 1.6vw, 19px); line-height: 1.55; color: var(--muted); max-width: 620px; margin: 0 auto 32px; position: relative; z-index: 1; font-weight: 300; }
577  .land-cta-row { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; position: relative; z-index: 1; }
578  .land-cta-primary {
579    background: linear-gradient(135deg, #ffffff 0%, #e8e8f0 100%);
580    color: #07070b; font-weight: 600; font-size: 15px;
581    padding: 15px 30px; border: none; border-radius: 999px; cursor: pointer;
582    font-family: inherit; transition: all 0.3s var(--eo);
583    display: inline-flex; align-items: center; gap: 8px;
584    position: relative; overflow: hidden;
585    box-shadow: 0 4px 16px rgba(0,0,0,0.18), 0 0 0 1px rgba(255,255,255,0.05) inset;
586  }
587  .land-cta-primary::before {
588    content: ''; position: absolute; inset: 0;
589    background: linear-gradient(120deg, transparent 30%, rgba(166,241,248,0.4) 50%, transparent 70%);
590    transform: translateX(-100%); transition: transform 0.7s var(--eo);
591  }
592  .land-cta-primary:hover { transform: translateY(-2px); box-shadow: 0 16px 44px rgba(166,241,248,0.28), 0 0 0 1px rgba(166,241,248,0.4); }
593  .land-cta-primary:hover::before { transform: translateX(100%); }
594  .land-cta-primary > * { position: relative; z-index: 1; }
595  .land-cta-secondary {
596    background: rgba(255,255,255,0.02); color: var(--text); font-weight: 500; font-size: 15px;
597    padding: 14px 26px; border: 1px solid rgba(255,255,255,0.08); border-radius: 999px; cursor: pointer;
598    font-family: inherit; transition: all 0.25s var(--eo); backdrop-filter: blur(8px);
599  }
600  .land-cta-secondary:hover { border-color: rgba(166,241,248,0.5); color: var(--accent); background: rgba(166,241,248,0.04); transform: translateY(-1px); }
601
602  /* Trust strip */
603  .land-trust { display: flex; gap: 32px; justify-content: center; margin-top: 40px; flex-wrap: wrap; opacity: 0.55; font-size: 12px; color: var(--muted); letter-spacing: 0.05em; position: relative; z-index: 1; }
604  .land-trust span { display: flex; align-items: center; gap: 6px; }
605
606  /* Section heading */
607  .land-section { padding: 72px 0 24px; }
608  .land-section-head { text-align: center; margin-bottom: 36px; }
609  .land-section-eyebrow { font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--muted); margin-bottom: 8px; font-weight: 600; }
610  .land-section-title { font-family: 'Cormorant Garamond', serif; font-size: clamp(28px, 4vw, 44px); line-height: 1.1; letter-spacing: -0.015em; font-weight: 500; margin: 0 0 10px; }
611  .land-section-sub { font-size: 14px; color: var(--muted); max-width: 540px; margin: 0 auto; line-height: 1.5; }
612
613  /* Showcase grid */
614  .land-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
615  @media (max-width: 900px) { .land-grid { grid-template-columns: repeat(2, 1fr); } }
616  .land-tile {
617    aspect-ratio: 3 / 4; border-radius: 18px; overflow: hidden; background: var(--card);
618    position: relative; transition: transform 0.5s var(--eo), box-shadow 0.5s var(--eo); cursor: default;
619    box-shadow: 0 4px 20px rgba(0,0,0,0.25);
620  }
621  .land-tile::after {
622    content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
623    box-shadow: inset 0 0 0 1px rgba(255,255,255,0.06);
624    transition: box-shadow 0.5s var(--eo);
625  }
626  .land-tile:hover { transform: translateY(-6px); box-shadow: 0 24px 60px rgba(0,0,0,0.5), 0 0 0 1px rgba(166,241,248,0.2); }
627  .land-tile:hover::after { box-shadow: inset 0 0 0 1px rgba(166,241,248,0.18), inset 0 0 30px rgba(166,241,248,0.08); }
628  .land-tile img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.7s var(--eo); }
629  .land-tile:hover img { transform: scale(1.06); }
630  .land-tile-label { position: absolute; bottom: 0; left: 0; right: 0; padding: 18px 14px 12px; background: linear-gradient(to top, rgba(0,0,0,0.75), transparent); color: #fff; font-size: 12px; font-weight: 500; letter-spacing: 0.02em; pointer-events: none; }
631  .land-tile-empty { display: flex; align-items: center; justify-content: center; color: var(--muted); font-size: 12px; opacity: 0.4; }
632  .land-tile-cta {
633    position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
634    background: rgba(0,0,0,0.55); backdrop-filter: blur(2px);
635    opacity: 0; transition: opacity 0.3s var(--eo); cursor: pointer; padding: 14px;
636  }
637  .land-tile:hover .land-tile-cta { opacity: 1; }
638  .land-tile-btn {
639    background: var(--accent); color: #07070b; font-weight: 600; font-size: 13px;
640    padding: 11px 20px; border: none; border-radius: 999px; cursor: pointer;
641    font-family: inherit; display: inline-flex; align-items: center; gap: 6px;
642    transform: translateY(8px); transition: transform 0.3s var(--eo);
643    box-shadow: 0 8px 20px rgba(0,0,0,0.4);
644  }
645  .land-tile:hover .land-tile-btn { transform: translateY(0); }
646
647  /* 3 cards section */
648  .land-features { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; margin-top: 16px; }
649  @media (max-width: 900px) { .land-features { grid-template-columns: 1fr; } }
650  .land-feature {
651    background: linear-gradient(180deg, rgba(255,255,255,0.025) 0%, rgba(255,255,255,0.005) 100%);
652    border: 1px solid rgba(255,255,255,0.06); border-radius: 22px;
653    padding: 32px 28px; transition: all 0.4s var(--eo); position: relative; overflow: hidden;
654    backdrop-filter: blur(12px);
655  }
656  .land-feature::before {
657    content: ''; position: absolute; inset: 0; border-radius: inherit; padding: 1px;
658    background: linear-gradient(135deg, transparent, rgba(166,241,248,0.3), transparent 70%);
659    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
660    mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
661    -webkit-mask-composite: xor; mask-composite: exclude;
662    opacity: 0; transition: opacity 0.4s var(--eo); pointer-events: none;
663  }
664  .land-feature:hover { transform: translateY(-4px); background: linear-gradient(180deg, rgba(166,241,248,0.05) 0%, rgba(255,255,255,0.005) 100%); }
665  .land-feature:hover::before { opacity: 1; }
666  .land-feature-icon {
667    width: 48px; height: 48px; border-radius: 14px;
668    background: linear-gradient(135deg, rgba(166,241,248,0.18), rgba(186,148,255,0.10));
669    color: var(--accent); display: flex; align-items: center; justify-content: center;
670    font-size: 22px; margin-bottom: 18px;
671    box-shadow: 0 4px 16px rgba(166,241,248,0.12), inset 0 1px 0 rgba(255,255,255,0.08);
672  }
673  .land-feature h3 { font-family: 'Cormorant Garamond', serif; font-size: 22px; line-height: 1.2; margin: 0 0 8px; font-weight: 600; }
674  .land-feature p { font-size: 13.5px; line-height: 1.55; color: var(--muted); margin: 0; }
675  .land-feature .soon { display: inline-block; margin-top: 10px; font-size: 10.5px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--accent); padding: 3px 8px; border: 1px solid rgba(166,241,248,0.3); border-radius: 6px; font-weight: 600; }
676
677  /* How it works */
678  .land-steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 32px; margin-top: 16px; }
679  @media (max-width: 900px) { .land-steps { grid-template-columns: 1fr; gap: 24px; } }
680  /* Rozszerzona wersja — 6 kroków, 2 rzędy po 3 */
681  .land-steps-detailed { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; margin-top: 24px; max-width: 1100px; margin-left: auto; margin-right: auto; }
682  @media (max-width: 900px) { .land-steps-detailed { grid-template-columns: 1fr; gap: 18px; } }
683  .land-step-detailed {
684    background: linear-gradient(180deg, rgba(255,255,255,0.03) 0%, rgba(255,255,255,0.005) 100%);
685    border: 1px solid rgba(255,255,255,0.06);
686    border-radius: 18px;
687    padding: 28px 24px;
688    transition: all 0.3s var(--eo);
689    text-align: left;
690    position: relative;
691  }
692  .land-step-detailed:hover { border-color: rgba(166,241,248,0.25); transform: translateY(-3px); }
693  .land-step-detailed-head { display: flex; align-items: center; gap: 14px; margin-bottom: 14px; }
694  .land-step-detailed-num {
695    width: 38px; height: 38px; border-radius: 12px; flex-shrink: 0;
696    background: linear-gradient(135deg, var(--accent), #c5a0ff);
697    color: #07070b; font-weight: 700; font-size: 17px;
698    display: flex; align-items: center; justify-content: center;
699    box-shadow: 0 6px 18px rgba(166,241,248,0.25);
700  }
701  .land-step-detailed h4 { margin: 0; font-size: 17px; font-weight: 600; line-height: 1.2; }
702  .land-step-detailed p { margin: 0; color: var(--muted); font-size: 13.5px; line-height: 1.55; }
703  .land-step-detailed .badge {
704    display: inline-block; margin-top: 10px;
705    font-size: 10.5px; letter-spacing: 0.12em; text-transform: uppercase;
706    color: var(--accent); padding: 3px 9px;
707    border: 1px solid rgba(166,241,248,0.3); border-radius: 6px; font-weight: 600;
708  }
709  .land-step { text-align: center; }
710  .land-step-num {
711    font-family: 'Cormorant Garamond', serif; font-size: 64px; line-height: 1; margin-bottom: 12px;
712    font-weight: 500; font-style: italic;
713    background: linear-gradient(135deg, var(--accent) 0%, #c5a0ff 100%);
714    -webkit-background-clip: text; background-clip: text;
715    -webkit-text-fill-color: transparent;
716    filter: drop-shadow(0 4px 16px rgba(166,241,248,0.25));
717  }
718  .land-step h4 { font-size: 16px; font-weight: 600; margin: 0 0 6px; }
719  .land-step p { font-size: 13px; color: var(--muted); margin: 0; line-height: 1.5; }
720
721  /* Use cases */
722  .land-cases { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; max-width: 1100px; margin: 0 auto; }
723  @media (max-width: 1024px) { .land-cases { grid-template-columns: repeat(2, 1fr); } }
724  @media (max-width: 600px) { .land-cases { grid-template-columns: 1fr; } }
725  .land-case {
726    background: var(--card); border: 1px solid var(--line);
727    border-radius: 16px; padding: 22px 20px;
728    transition: all 0.3s var(--eo);
729  }
730  .land-case:hover { border-color: rgba(166,241,248,0.3); transform: translateY(-3px); }
731  .land-case-emoji { font-size: 32px; margin-bottom: 10px; line-height: 1; }
732  .land-case h4 { font-size: 15px; font-weight: 600; margin: 0 0 6px; }
733  .land-case p { font-size: 12.5px; line-height: 1.5; color: var(--muted); margin: 0; }
734
735  /* ROI */
736  .land-roi-box {
737    max-width: 760px; margin: 0 auto;
738    background: linear-gradient(135deg, rgba(166,241,248,0.08), rgba(186,148,255,0.05)
738);
739    border: 1px solid rgba(166,241,248,0.2);
740    border-radius: 22px; padding: 36px 32px; text-align: center;
741  }
742  .land-roi-box h3 { font-family: 'Cormorant Garamond', serif; font-size: 26px; line-height: 1.2; margin: 0 0 22px; font-weight: 500; }
743  .land-roi-grid { display: grid; grid-template-columns: 1fr auto 1fr; gap: 20px; align-items: center; }
744  @media (max-width: 600px) { .land-roi-grid { grid-template-columns: 1fr; } }
745  .land-roi-side { padding: 16px; }
746  .land-roi-side h5 { font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); margin: 0 0 10px; font-weight: 600; }
747  .land-roi-num { font-family: 'Cormorant Garamond', serif; font-size: 36px; font-weight: 600; line-height: 1; margin: 0 0 6px; }
748  .land-roi-side.old .land-roi-num { color: var(--muted); text-decoration: line-through; opacity: 0.7; }
749  .land-roi-side.new .land-roi-num { background: linear-gradient(135deg, var(--accent), #c5a0ff); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
750  .land-roi-label { font-size: 12px; color: var(--muted); }
751  .land-roi-arrow { font-size: 24px; color: var(--accent); }
752
753  /* FAQ */
754  .land-faq { max-width: 760px; margin: 0 auto; display: flex; flex-direction: column; gap: 10px; }
755  .land-faq-item {
756    background: var(--card); border: 1px solid var(--line);
757    border-radius: 14px; overflow: hidden; transition: border-color 0.2s;
758  }
759  .land-faq-item:hover { border-color: rgba(255,255,255,0.12); }
760  .land-faq-item[open] { border-color: rgba(166,241,248,0.25); }
761  .land-faq-q {
762    padding: 16px 20px; cursor: pointer; font-weight: 500; font-size: 14.5px;
763    list-style: none; display: flex; justify-content: space-between; align-items: center;
764    transition: color 0.2s;
765  }
766  .land-faq-q::-webkit-details-marker { display: none; }
767  .land-faq-q::after {
768    content: '+'; font-size: 22px; color: var(--muted); font-weight: 300;
769    transition: transform 0.3s var(--eo);
770  }
771  .land-faq-item[open] .land-faq-q::after { transform: rotate(45deg); color: var(--accent); }
772  .land-faq-a { padding: 0 20px 18px; font-size: 13.5px; line-height: 1.6; color: var(--muted); }
773
774  /* Bottom CTA */
775  .land-bottom-cta { text-align: center; padding: 96px 24px 120px; border-top: 1px solid rgba(166,241,248,0.12); margin-top: 80px; position: relative; overflow: hidden; }
776  .land-bottom-cta::before {
777    content: ''; position: absolute; inset: -1px 0 0 0;
778    background:
779      radial-gradient(ellipse at 50% 0%, rgba(166,241,248,0.18), transparent 60%),
780      radial-gradient(ellipse at 30% 100%, rgba(186,148,255,0.10), transparent 50%);
781    pointer-events: none; z-index: 0;
782    animation: heroGlow 10s ease-in-out infinite alternate;
783  }
784  .land-bottom-cta > * { position: relative; z-index: 1; }
785  .land-bottom-cta h2 { font-family: 'Cormorant Garamond', serif; font-size: clamp(28px, 4.5vw, 48px); line-height: 1.05; margin: 0 0 16px; font-weight: 500; }
786  .land-bottom-cta p { font-size: 15px; color: var(--muted); max-width: 480px; margin: 0 auto 24px; line-height: 1.5; }
787
788  /* ═══ Process showcase: input → input → output ═══ */
789  .land-process { display: flex; align-items: center; justify-content: center; gap: 16px; max-width: 1100px; margin: 0 auto; flex-wrap: wrap; }
790  .land-process-step { flex: 0 0 auto; text-align: center; }
791  .land-process-img, .land-process-video {
792    width: 200px; aspect-ratio: 9/16; border-radius: 18px; overflow: hidden;
793    background: var(--card); position: relative;
794    box-shadow: 0 12px 40px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.04);
795    transition: transform 0.5s var(--eo);
796  }
797  .land-process-step:hover .land-process-img,
798  .land-process-step:hover .land-process-video { transform: translateY(-4px) scale(1.02); }
799  .land-process-img img, .land-process-video video {
800    width: 100%; height: 100%; object-fit: cover; display: block;
801  }
802  .land-process-result .land-process-video {
803    width: 240px; box-shadow: 0 16px 50px rgba(166,241,248,0.18), 0 0 0 2px rgba(166,241,248,0.25);
804  }
805  .land-process-result .land-process-video::before {
806    content: 'Wynik AI'; position: absolute; top: 12px; left: 12px;
807    padding: 4px 10px; background: rgba(166,241,248,0.95); color: #07070b;
808    font-size: 10.5px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
809    border-radius: 999px; z-index: 2;
810  }
811  .land-process-label {
812    margin-top: 12px; font-size: 12px; color: var(--muted); letter-spacing: 0.01em;
813    font-family: ui-monospace, 'SF Mono', monospace;
814  }
815  .land-process-label b { color: var(--accent); font-weight: 600; }
816  .land-process-op {
817    font-size: 28px; color: var(--muted); font-weight: 200;
818    font-family: 'Cormorant Garamond', serif; letter-spacing: -0.02em;
819    padding: 0 4px; align-self: center;
820  }
821  .land-process-op.arrow { color: var(--accent); font-size: 32px; font-weight: 300; }
822
823  .land-process-prompt {
824    max-width: 760px; margin: 32px auto 0; padding: 18px 22px;
825    background: var(--card); border: 1px solid var(--border);
826    border-radius: 16px; text-align: center;
827    font-family: ui-monospace, 'SF Mono', monospace; font-size: 13px; line-height: 1.55;
828    color: var(--text); letter-spacing: -0.01em;
829    position: relative;
830  }
831  .land-process-prompt::before {
832    content: 'Twój prompt'; position: absolute; top: -10px; left: 22px;
833    padding: 3px 10px; background: var(--bg2); border: 1px solid var(--border);
834    border-radius: 6px; font-size: 10px; font-weight: 600; letter-spacing: 0.1em;
835    text-transform: uppercase; color: var(--muted); font-family: inherit;
836  }
837  .land-process-prompt .acc { color: var(--accent); font-weight: 600; }
838
839  @media (max-width: 768px) {
840    .land-process { flex-direction: column; gap: 8px; }
841    .land-process-img, .land-process-video { width: 160px; }
842    .land-process-result .land-process-video { width: 180px; }
843    .land-process-op { font-size: 22px; }
844    .land-process-op.arrow { font-size: 26px; transform: rotate(90deg); }
845    .land-process-op:not(.arrow) { transform: rotate(0deg); }
846    .land-process-prompt { font-size: 12px; padding: 16px 18px; margin: 20px 12px 0; }
847  }
848  @keyframes viewIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
849
850  /* Mobile */
851  @media (max-width: 1024px) {
852    .right-col { display: none; }
853    .center { padding: 24px 24px 180px; }
854    h1 { font-size: 36px; }
855    .studios { grid-template-columns: 1fr; }
856    .dock { left: 16px; right: 16px; transform: none; width: auto; bottom: 16px; }
857    .topbar { padding: 12px 16px; gap: 12px; }
858    .tabs { overflow-x: auto; scrollbar-width: none; }
859    .tabs::-webkit-scrollbar { display: none; }
860  }
861
862  /* ════════════ MOBILE (≤768px) ════════════ */
863  @media (max-width: 768px) {
864    /* Topbar — kompakt */
865    .topbar { padding: 8px 12px; gap: 8px; }
866    .logo span { display: none; }
867    .tab { padding: 6px 12px; font-size: 12px; white-space: nowrap; }
868    .tab[disabled] { display: none; }
869    .tb-right { gap: 8px; }
870    .credits { font-size: 12px; padding: 4px 10px; }
871    .home-btn, .avatar { width: 32px; height: 32px; }
872    .avatar { font-size: 12px; }
873
874    /* Center */
875    .center { padding: 16px 16px 220px; }
876    h1 { font-size: 26px; }
877    .sub { font-size: 13px; }
878    .page-head { margin-bottom: 22px; }
879
880    /* Controls + cats — full width scrollable */
881    .controls { gap: 12px; margin-bottom: 22px; }
882    .cats { display: flex; gap: 6px; overflow-x: auto; scrollbar-width: none; padding-bottom: 4px; -webkit-overflow-scrolling: touch; }
883    .cats::-webkit-scrollbar { display: none; }
884    .cat { padding: 7px 12px; font-size: 12px; white-space: nowrap; flex-shrink: 0; }
885    .segment { width: 100%; }
886    .seg { flex: 1; padding: 8px 12px; font-size: 12px; }
887
888    /* Studios — 1 kolumna ale mniejszy aspect */
889    .studios { gap: 14px; }
890    .tile { aspect-ratio: 4/3.5; border-radius: 22px; }
891    .tile-info { padding: 18px 18px 16px; }
892    .tile-name { font-size: 17px; }
893    .tile-meta { font-size: 11px; }
894
895    /* Library */
896    .lib-head { flex-wrap: wrap; gap: 12px; margin-bottom: 20px; }
897    .lib-tabs { width: 100%; order: -1; }
898    .lib-tab { flex: 1; padding: 8px 10px; font-size: 12px; }
899    .btn-add { padding: 8px 14px; font-size: 12px; }
900    .assets-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; }
901
902    /* Dock — kompaktowy */
903    .dock { left: 12px; right: 12px; bottom: 80px; padding: 4px; border-radius: 22px; }
904    .dock-inner { padding: 8px 10px 6px; }
905    .p-input { font-size: 14px; padding: 10px 12px; }
906    .dock-bar { gap: 4px; padding: 6px 2px 0; flex-wrap: wrap; }
907    .pill { padding: 7px 10px; font-size: 11px; gap: 4px; }
908    .pill .fas, .pill .fa { font-size: 11px; }
909    .send { width: 36px; height: 36px; }
910    .cost { font-size: 11px; }
911
912    /* Modals — full screen na mobile */
913    .modal { max-width: calc(100vw - 16px) !important; padding: 22px 18px !important; border-radius: 22px !important; }
914    .ic-modal { max-width: 100% !important; max-height: 92vh !important; width: 100% !important; border-radius: 22px 22px 0 0 !important; margin-top: auto; }
915    .ic-head { padding: 16px 18px 12px; }
916    .ic-body { padding: 14px 18px; }
917    .picker { max-width: 100% !important; max-height: 92vh !important; width: 100% !important; border-radius: 22px 22px 0 0 !important; margin-top: auto; }
918    .ed-modal { max-width: 100% !important; border-radius: 0 !important; max-height: 100vh !important; }
919    .ed-body { grid-template-columns: 1fr !important; }
920    .ed-tools { display: flex; flex-direction: row; padding: 8px; overflow-x: auto; scrollbar-width: none; gap: 6px; max-height: 56px; flex-shrink: 0; border-bottom: 1px solid var(--border); }
921    .ed-tools::-webkit-scrollbar { display: none; }
922    .ed-tool { flex-shrink: 0; padding: 8px 14px; font-size: 12px; white-space: nowrap; }
923    .ed-panel { max-height: 240px; padding: 14px; }
924
925    /* Backdrops on mobile — bottom sheet style */
926    .backdrop { align-items: flex-end !important; padding: 0 !important; }
927    .backdrop > .modal { width: 100% !important; max-width: 100% !important; border-radius: 22px 22px 0 0 !important; max-height: 92vh !important; overflow-y: auto; margin: 0 !important; }
928
929    /* Landing — mobile typo */
930    .land-wrap { padding: 0 16px; }
931    .land-hero { padding: 32px 0 24px; }
932    .land-eyebrow { font-size: 10px; padding: 5px 12px; }
933    .land-trust { gap: 12px; flex-direction: column; align-items: center; margin-top: 24px; }
934    .land-section { padding: 48px 0 12px; }
935    .land-section-title { font-size: 26px; }
936    .land-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; }
937    .land-tile { aspect-ratio: 3 / 4; border-radius: 12px; }
938    .land-tile-label { padding: 14px 10px 8px; font-size: 11px; }
939    .land-tile-cta { opacity: 1; background: linear-gradient(to top, rgba(0,0,0,0.65) 0%, transparent 50%); }  /* na mobile zawsze widoczny CTA */
940    .land-tile-btn { transform: translateY(0); padding: 8px 14px; font-size: 11px; }
941    .land-features { gap: 12px; }
942    .land-feature { padding: 22px 18px; border-radius: 16px; }
943    .land-feature h3 { font-size: 18px; }
944    .land-feature p { font-size: 12.5px; }
945    .land-step-num { font-size: 44px; }
946    .land-bottom-cta { padding: 56px 16px 72px; margin-top: 40px; }
947    .land-cta-primary, .land-cta-secondary { padding: 12px 22px; font-size: 14px; }
948
949    /* Mobile: floating "Historia" button */
950    .m-history-fab { display: flex !important; }
951
952    /* Hide tile-use-hint na mobile (nie ma hover) */
953    .tile-use-hint { display: none !important; }
954    .land-tile-cta { pointer-events: auto; }
955  }
956
957  /* Floating history button — visible only on mobile (display: none default) */
958  .m-history-fab {
959    display: none;
960    position: fixed;
961    bottom: 16px;
962    right: 16px;
963    width: 48px;
964    height: 48px;
965    border-radius: 50%;
966    background: rgba(13,13,18,0.92);
967    backdrop-filter: blur(14px);
968    -webkit-backdrop-filter: blur(14px);
969    border: 1px solid rgba(255,255,255,0.12);
970    color: var(--text);
971    font-size: 16px;
972    cursor: pointer;
973    z-index: 50;
974    align-items: center;
975    justify-content: center;
976    box-shadow: 0 12px 30px rgba(0,0,0,0.5);
977    transition: all 0.25s var(--eo);
978  }
979  .m-history-fab:hover, .m-history-fab:active { background: var(--accent); color: #07070b; }
980  .m-history-fab .badge {
981    position: absolute; top: -2px; right: -2px;
982    min-width: 18px; height: 18px; padding: 0 4px;
983    border-radius: 999px; background: var(--accent); color: #07070b;
984    font-size: 10px; font-weight: 700; display: flex; align-items: center; justify-content: center;
985  }
986  body.is-landing .m-history-fab { display: none !important; }
987
988  /* Mobile history sheet (bottom sheet) */
989  .m-history-sheet {
990    position: fixed; left: 0; right: 0; bottom: 0;
991    max-height: 75vh; background: var(--bg2);
992    border-top: 1px solid var(--border-hi); border-radius: 22px 22px 0 0;
993    padding: 16px 18px 24px; overflow-y: auto;
994    z-index: 60; transform: translateY(100%);
995    transition: transform 0.3s var(--eo);
996    box-shadow: 0 -20px 60px rgba(0,0,0,0.6);
997  }
998  .m-history-sheet.show { transform: translateY(0); }
999  .m-history-sheet-handle { width: 40px; height: 4px; border-radius: 2px; background: rgba(255,255,255,0.2); margin: 0 auto 14px; }
1000  .m-history-sheet-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
1001  .m-history-sheet-head h4 { margin: 0; font-size: 14px; font-weight: 600; }
1002  .m-history-sheet-close { background: transparent; color: var(--muted); border: none; cursor: pointer; padding: 4px 8px; font-size: 14px; }
1003  .m-history-backdrop {
1004    position: fixed; inset: 0; background: rgba(0,0,0,0.5);
1005    z-index: 55; opacity: 0; pointer-events: none; transition: opacity 0.25s var(--eo);
1006  }
1007  .m-history-backdrop.show { opacity: 1; pointer-events: auto; }
1008  /* ═══════════ REDESIGN v2 — brand look: Space Grotesk + neon #00f2ff + WebGL shader (jak agent/spaider), scoped do landingu ═══════════ */
1009  #ec-shader{position:fixed;inset:0;width:100%;height:100%;z-index:0;display:block;pointer-events:none}
1010  .app{position:relative;z-index:1}
1011  #view-landing{--accent:#00f2ff;--font-disp:'Space Grotesk',system-ui,sans-serif;
1012    --neon-grad:linear-gradient(100deg,#00f2ff,#0066ff,#bc13fe,#00f2ff);--neon-grad2:linear-gradient(100deg,#00f2ff,#ff3366);}
1013  #view-landing .land-title,#view-landing .land-section-title,#view-landing .land-feature h3,
1014  #view-landing .land-roi-box h3,#view-landing .land-roi-num,#view-landing .land-bottom-cta h2{font-family:var(--font-disp)!important;font-weight:700!important;letter-spacing:-.02em}
1015  #view-landing .land-title em{font-family:'Instrument Serif',Georgia,serif!important;font-style:italic;font-weight:400;
1016    background:var(--neon-grad2);background-size:200% 100%;-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;animation:ecSlide 5s linear infinite}
1017  @keyframes ecSlide{to{background-position:200% 0}}
1018  #view-landing .land-eyebrow{color:#00f2ff;background:rgba(0,242,255,.06);border-color:rgba(0,242,255,.22)}
1019  #view-landing .land-section-eyebrow{color:#00f2ff}
1020  #view-landing .land-trust i{color:#00f2ff}
1021  #view-landing .land-cta-primary{background:var(--neon-grad)!important;background-size:200% 100%!important;color:#04121a!important;animation:ecSlide 6s linear infinite;box-shadow:0 14px 44px rgba(0,242,255,.3)!important}
1022  #view-landing .land-feature-icon{background:rgba(0,242,255,.08)!important;border:1px solid rgba(0,242,255,.2);color:#00f2ff!important;box-shadow:0 4px 16px rgba(0,242,255,.14)!important}
1023  #view-landing .land-step-detailed-num{background:linear-gradient(135deg,#00f2ff,#0066ff)!important;color:#04121a!important;box-shadow:0 6px 18px rgba(0,242,255,.28)!important}
1024  #view-landing .land-step-num{background:var(--neon-grad2)!important;-webkit-background-clip:text!important;background-clip:text!important;font-family:var(--font-disp)!important;font-style:normal!important;font-weight:700!important;filter:drop-shadow(0 4px 16px rgba(0,242,255,.25))}
1025  #view-landing .land-roi-box{background:radial-gradient(120% 140% at 0 0,rgba(0,242,255,.08),transparent 55%),radial-gradient(120% 140% at 100% 100%,rgba(188,19,254,.1),transparent 55%),rgba(255,255,255,.02)!important;border-color:rgba(0,242,255,.2)!important}
1026  #view-landing .land-roi-side.new{background:rgba(0,242,255,.05)!important;border:1px solid rgba(0,242,255,.28)!important;border-radius:16px}
1027  #view-landing .land-roi-side.new .land-roi-num{background:var(--neon-grad2)!important;-webkit-background-clip:text;background-clip:text}
1028  #view-landing .land-roi-side.old .land-roi-num{text-decoration-color:#ff3366}
1029  #view-landing .land-roi-arrow{color:#00f2ff}
1030  #view-landing .land-feature{overflow:hidden}
1031  #view-landing .land-feature::after{content:'';position:absolute;top:0;left:-60%;width:55%;height:100%;transform:skewX(-18deg);background:linear-gradient(90deg,transparent,rgba(0,242,255,.12),transparent);transition:left .65s ease;pointer-events:none;z-index:2}
1032  #view-landing .land-feature:hover::after{left:135%}
1033  #view-landing .land-feature:hover{box-shadow:0 22px 55px rgba(0,0,0,.55),0 0 0 1px rgba(0,242,255,.18)}
1034  #view-landing .land-process-result .land-process-video{box-shadow:0 16px 50px rgba(0,242,255,.18),0 0 0 2px rgba(0,242,255,.25)!important}
1035  #view-landing .land-process-result .land-process-video::before{background:rgba(0,242,255,.95)!important;color:#04121a!important}
1036  #view-landing .land-bottom-cta{border-color:rgba(0,242,255,.12)}
1037  #view-landing .land-bottom-cta::before{background:radial-gradient(ellipse at 50% 0%,rgba(0,242,255,.18),transparent 60%),radial-gradient(ellipse at 30% 100%,rgba(188,19,254,.12),transparent 50%)!important}
1038  /* scroll-reveal — JS dokłada .ec-js (na #view-landing) i .ec-in (na sekcje). Bez JS: wszystko widoczne. */
1039  #view-landing.ec-js .land-hero,#view-landing.ec-js .land-section,#view-landing.ec-js .land-bottom-cta{opacity:0;transform:translateY(32px);transition:opacity .9s cubic-bezier(.16,1,.3,1),transform .9s cubic-bezier(.16,1,.3,1)}
1040  #view-landing.ec-js .ec-in{opacity:1!important;transform:none!important}
1041</style>
1042<!-- ═══ SKÓRA 2.0: Marketing Studio (2026-08-25) — warstwa NAD oryginalnym CSS ═══ -->
1043<style>
1044/* ═══════════════════════════════════════════════════════════════════════════
1045   MARKETING STUDIO — SKÓRA 2.0 (2026-08-25)
1046   Warstwa NAKŁADANA po oryginalnym CSS: przejmuje tokeny (stare 
1046komponenty —
1047   edytor, picker, czaty — przemalowują się same) i przeprojektowuje powierzchnie
1048   pierwszego kontaktu: tło, topbar, nagłówki, kafle galerii, dock, historię.
1049   Układ i identyfikatory BEZ ZMIAN — stały user ma trafić tam, gdzie zawsze.
1050   ═══════════════════════════════════════════════════════════════════════════ */
1051
1052/* ── TOKENY: głębsza noc, ostrzejszy cyjan chmurki, cieplejszy gradient ── */
1053:root {
1054  --bg: #07080f;
1055  --bg2: #0b0d17;
1056  --card: rgba(255,255,255,0.04);
1057  --card-hi: rgba(255,255,255,0.075);
1058  --border: rgba(255,255,255,0.07);
1059  --border-hi: rgba(125,243,255,0.28);
1060  --text: #f6f7fb;
1061  --muted: rgba(246,247,251,0.6);
1062  --muted2: rgba(246,247,251,0.34);
1063  --accent: #59e8ff;
1064  --accent2: #b9a8ff;
1065  --accent3: #ff9ecf;
1066  --danger: #ff7a7a;
1067  --grad: linear-gradient(135deg, #59e8ff 0%, #b9a8ff 52%, #ff9ecf 100%);
1068  --font-display: 'Unbounded', 'Space Grotesk', sans-serif;
1069  --font-body: 'Manrope', 'Inter', system-ui, sans-serif;
1070}
1071
1072/* ── ATMOSFERA: mesh + ziarno zamiast płaskiej czerni ── */
1073body {
1074  font-family: var(--font-body);
1075  background:
1076    radial-gradient(1100px 520px at 12% -6%, rgba(89,232,255,0.075), transparent 60%),
1077    radial-gradient(900px 480px at 88% 4%, rgba(185,168,255,0.065), transparent 55%),
1078    radial-gradient(1200px 700px at 50% 118%, rgba(255,158,207,0.05), transparent 60%),
1079    var(--bg);
1080  background-attachment: fixed;
1081}
1082body::before {
1083  content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 0; opacity: 0.5;
1084  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E");
1085}
1086
1087/* ── TOPBAR: smuklejszy, markowy ── */
1088.topbar {
1089  background: rgba(7,8,15,0.82);
1090  backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);
1091  border-bottom: 1px solid var(--border);
1092}
1093.logo-dot {
1094  background: var(--grad); border-radius: 12px;
1095  box-shadow: 0 0 22px rgba(89,232,255,0.35);
1096}
1097.logo-brand-main { font-family: var(--font-display); font-weight: 700; letter-spacing: -0.02em; }
1098.logo-brand-sub {
1099  font-family: var(--font-display); font-size: 11px; font-weight: 600;
1100  background: var(--grad); -webkit-background-clip: text; background-clip: text;
1101  -webkit-text-fill-color: transparent; color: transparent; letter-spacing: 0.02em;
1102}
1103.tabs { gap: 4px; }
1104.tab { border-radius: 999px; font-weight: 600; transition: all .18s ease; }
1105.tab.on { background: var(--card-hi); box-shadow: inset 0 0 0 1px var(--border-hi); color: var(--text); }
1106.credits { font-weight: 700; }
1107.credits i { color: var(--accent); }
1108.topup-btn {
1109  background: var(--grad); color: #07080f; font-weight: 800; border: none;
1110  box-shadow: 0 4px 18px rgba(89,232,255,0.28);
1111  transition: transform .15s ease, box-shadow .15s ease;
1112}
1113.topup-btn:hover { transform: translateY(-1px); box-shadow: 0 6px 24px rgba(89,232,255,0.42); }
1114.avatar { background: var(--card-hi); border: 1px solid var(--border-hi); font-weight: 800; }
1115
1116/* ── NAGŁÓWKI WIDOKÓW ── */
1117.page-head h1 {
1118  font-family: var(--font-display); font-weight: 700; letter-spacing: -0.03em;
1119}
1120.page-head h1 .serif {
1121  font-family: var(--font-display); font-style: normal;
1122  background: var(--grad); -webkit-background-clip: text; background-clip: text;
1123  -webkit-text-fill-color: transparent; color: transparent;
1124}
1125.page-head .sub { color: var(--muted); }
1126
1127/* ── PIGUŁKI KATEGORII I SEGMENT ── */
1128.cat {
1129  border-radius: 999px; border: 1px solid var(--border);
1130  background: var(--card); font-weight: 600; transition: all .16s ease;
1131}
1132.cat:hover { border-color: var(--border-hi); transform: translateY(-1px); }
1133.cat.on {
1134  background: var(--card-hi); border-color: var(--border-hi); color: var(--text);
1135  box-shadow: 0 0 16px rgba(89,232,255,0.14);
1136}
1137.cat.cta-request { border-style: dashed; color: var(--accent); }
1138.seg.on { background: var(--card-hi); box-shadow: inset 0 0 0 1px var(--border-hi); }
1139
1140/* ── KAFLE GALERII: głębia, unoszenie, zoom obrazka ── */
1141.studios { gap: 14px; }
1142.tile {
1143  border-radius: 18px; overflow: hidden; border: 1px solid var(--border);
1144  background: var(--bg2);
1145  transition: transform .22s cubic-bezier(.2,.8,.2,1), border-color .22s ease, box-shadow .22s ease;
1146}
1147.tile:hover {
1148  transform: translateY(-4px);
1149  border-color: var(--border-hi);
1150  box-shadow: 0 14px 38px rgba(0,0,0,0.5), 0 0 24px rgba(89,232,255,0.10);
1151}
1152.tile img.tile-img { transition: transform .35s cubic-bezier(.2,.8,.2,1); }
1153.tile:hover img.tile-img { transform: scale(1.045); }
1154.tile.on { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent), 0 12px 34px rgba(0,0,0,0.5); }
1155.tile-overlay { background: linear-gradient(180deg, transparent 42%, rgba(5,6,11,0.92) 100%); }
1156.tile-name { font-family: var(--font-display); font-size: 12.5px; font-weight: 600; letter-spacing: -0.01em; }
1157.tile-meta { color: var(--muted2); }
1158.tile-heart { transition: transform .15s ease; }
1159.tile-heart:hover { transform: scale(1.15); }
1160.tile-heart.saved { color: var(--accent3); }
1161.tile-use-hint { background: rgba(7,8,15,0.75); backdrop-filter: blur(8px); border: 1px solid var(--border-hi); border-radius: 999px; }
1162.tile-check { background: var(--grad); color: #07080f; }
1163/* Badge WIDEO — kafle szablonów wideo (modes zawiera 'video') */
1164.tile-badge-video {
1165  position: absolute; top: 10px; left: 10px; z-index: 2;
1166  display: inline-flex; align-items: center; gap: 5px;
1167  padding: 4px 10px; border-radius: 999px;
1168  background: rgba(7,8,15,0.78); backdrop-filter: blur(8px);
1169  border: 1px solid var(--border-hi);
1170  font-size: 9.5px; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase;
1171  color: var(--accent);
1172}
1173.tile-badge-video i { font-size: 8.5px; }
1174
1175/* Wejście kafli: kaskadowe wypłynięcie przy renderze */
1176@keyframes tileIn { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); } }
1177.studios .tile { animation: tileIn .5s cubic-bezier(.2,.8,.2,1) both; }
1178.studios .tile:nth-child(2)  { animation-delay: .03s; }
1179.studios .tile:nth-child(3)  { animation-delay: .06s; }
1180.studios .tile:nth-child(4)  { animation-delay: .09s; }
1181.studios .tile:nth-child(5)  { animation-delay: .12s; }
1182.studios .tile:nth-child(6)  { animation-delay: .15s; }
1183.studios .tile:nth-child(7)  { animation-delay: .18s; }
1184.studios .tile:nth-child(8)  { animation-delay: .21s; }
1185.studios .tile:nth-child(n+9){ animation-delay: .24s; }
1186
1187/* ── DOCK: szklana konsola z poświatą ── */
1188.dock { background: transparent; }
1189.dock-inner {
1190  background: rgba(10,12,22,0.86);
1191  backdrop-filter: blur(22px); -webkit-backdrop-filter: blur(22px);
1192  border: 1px solid var(--border);
1193  border-radius: 22px;
1194  box-shadow: 0 18px 50px rgba(0,0,0,0.55), 0 0 0 1px rgba(255,255,255,0.02) inset;
1195  transition: border-color .2s ease, box-shadow .2s ease;
1196}
1197.dock-inner:focus-within {
1198  border-color: var(--border-hi);
1199  box-shadow: 0 18px 50px rgba(0,0,0,0.55), 0 0 30px rgba(89,232,255,0.12);
1200}
1201.p-input { font-family: var(--font-body); font-size: 14px; }
1202.p-input::placeholder { color: var(--muted2); }
1203.pill {
1204  border-radius: 999px; border: 1px solid var(--border); background: var(--card);
1205  font-weight: 600; transition: all .15s ease;
1206}
1207.pill:hover { border-color: var(--border-hi); background: var(--card-hi); }
1208.pill.studio { border-color: var(--border-hi); color: var(--accent); background: rgba(89,232,255,0.07); }
1209.cost { font-family: var(--font-display); font-size: 11.5px; font-weight: 700; color: var(--muted); }
1210.send {
1211  background: var(--grad); border-radius: 14px;
1212  box-shadow: 0 4px 18px rgba(89,232,255,0.3);
1213  transition: transform .15s ease, box-shadow .15s ease;
1214}
1215.send:hover { transform: translateY(-1px) scale(1.03); box-shadow: 0 6px 26px rgba(89,232,255,0.45); }
1216.send:disabled { opacity: 0.5; transform: none; box-shadow: none; }
1217.chip { border-radius: 999px; border: 1px solid var(--border-hi); background: rgba(89,232,255,0.06); }
1218.mention-dd { border-radius: 16px; border: 1px solid var(--border-hi); background: rgba(10,12,22,0.96); backdrop-filter: blur(20px); box-shadow: 0 16px 44px rgba(0,0,0,0.6); }
1219.mi.active, .mi:hover { background: var(--card-hi); }
1220
1221/* ── HISTORIA ── */
1222.right-col { border-left: 1px solid var(--border); background: rgba(7,8,15,0.4); }
1223.col-title { font-family: var(--font-display); font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
1224.col-count { background: var(--card-hi); border-radius: 999px; font-weight: 800; color: var(--accent); }
1225.hist-item { border-radius: 16px; border: 1px solid var(--border); overflow: hidden; transition: border-color .18s ease, transform .18s ease; }
1226.hist-item:hover { border-color: var(--border-hi); transform: translateY(-1px); }
1227.ha-btn { border-radius: 10px; transition: all .14s ease; }
1228.ha-btn.primary { background: var(--grad); color: #07080f; }
1229.ha-btn:hover { transform: translateY(-1px); }
1230.lib-quick { border-top: 1px solid var(--border); }
1231.lib-quick h4 { font-family: var(--font-display); font-size: 10.5px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
1232.lib-link { border-radius: 12px; transition: background .15s ease; }
1233.lib-link:hover { background: var(--card); }
1234.lib-link .cnt { background: var(--card-hi); border-radius: 999px; color: var(--accent); font-weight: 800; }
1235
1236/* ── BIBLIOTEKA ── */
1237.lib-tab { border-radius: 999px; font-weight: 600; }
1238.lib-tab.on { background: var(--card-hi); box-shadow: inset 0 0 0 1px var(--border-hi); }
1239.btn-add { border-radius: 999px; font-weight: 700; }
1240.asset { border-radius: 16px; border: 1px solid var(--border); overflow: hidden; transition: transform .2s ease, border-color .2s ease; }
1241.asset:hover { transform: translateY(-3px); border-color: var(--border-hi); }
1242.asset-name { font-weight: 700; }
1243
1244/* ── MODALE I FORMULARZE (dziedziczą tokeny, tu tylko szlif) ── */
1245.modal, .picker, .ic-modal, .ed-modal {
1246  border: 1px solid var(--border); border-radius: 22px;
1247  background: linear-gradient(180deg, #0d0f1a, #090b13);
1248  box-shadow: 0 30px 90px rgba(0,0,0,0.7);
1249}
1250.modal h3, .picker-head h3, .ic-head h3, .ed-head h3 { font-family: var(--font-display); font-weight: 600; letter-spacing: -0.02em; }
1251.fld { border-radius: 12px; }
1252.fld:focus { border-color: var(--border-hi); box-shadow: 0 0 0 3px rgba(89,232,255,0.1); }
1253.btn-pr { background: var(--grad); color: #07080f; font-weight: 800; border: none; }
1254.btn-pr:hover { filter: brightness(1.08); }
1255.drop:hover { border-color: var(--border-hi); background: rgba(89,232,255,0.04); }
1256.toast { border-radius: 14px; border: 1px solid var(--border-hi); background: rgba(10,12,22,0.94); backdrop-filter: blur(16px); font-weight: 600; }
1257
1258/* ── SEKCJA "JAK TO DZIAŁA" (statyczna, na dole create) ── */
1259.ms-how {
1260  margin: 56px auto 90px; max-width: 860px; padding: 0 8px;
1261  display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px;
1262}
1263.ms-how-card {
1264  background: var(--card); border: 1px solid var(--border); border-radius: 18px;
1265  padding: 20px 18px;
1266}
1267.ms-how-card b {
1268  display: flex; align-items: center; gap: 10px; margin-bottom: 8px;
1269  font-family: var(--font-display); font-size: 12.5px; font-weight: 600; color: var(--text);
1270}
1271.ms-how-card b i { color: var(--accent); font-size: 13px; }
1272.ms-how-card p { margin: 0; font-size: 12px; line-height: 1.6; color: var(--muted); }
1273@media (max-width: 768px) { .ms-how { grid-template-columns: 1fr; margin: 36px auto 120px; } }
1274
1275/* Mobile: nic nie ruszamy strukturalnie — tylko dock i sheet dziedziczą nową skórę */
1276.m-history-sheet { background: linear-gradient(180deg, #0d0f1a, #090b13); border-top: 1px solid var(--border-hi); }
1277.m-history-fab { background: var(--grad); color: #07080f; box-shadow: 0 8px 26px rgba(89,232,255,0.35); }
1278
1279</style>
1280<!-- ═══ DOCK 2.0 (Higgsfield-style konsola): tryb Obraz/Wideo + sloty + Generuj z kosztem ═══ -->
1281<style>
1282.mk-dock-row { display: grid; grid-template-columns: auto 1fr auto; gap: 14px; align-items: stretch; }
1283.mk-modes { display: flex; flex-direction: column; gap: 6px; justify-content: center; padding-right: 12px; border-right: 1px solid var(--border); }
1284.mk-mode {
1285  display: flex; flex-direction: column; align-items: center; gap: 4px;
1286  min-width: 58px; padding: 9px 10px; border-radius: 14px; cursor: pointer;
1287  background: transparent; border: 1px solid transparent; color: var(--muted2);
1288  font-family: var(--font-body); font-size: 10px; font-weight: 700; transition: all .15s ease;
1289}
1290.mk-mode i { font-size: 14px; }
1291.mk-mode:hover { color: var(--muted); background: var(--card); }
1292.mk-mode.on { color: var(--text); background: var(--card-hi); border-color: var(--border-hi); box-shadow: 0 0 14px rgba(89,232,255,0.1); }
1293.mk-center { min-width: 0; display: flex; flex-direction: column; justify-content: center; }
1294.mk-slots { display: flex; align-items: center; gap: 10px; padding-left: 12px; border-left: 1px solid var(--border); }
1295/* Specyficzność 0,3,0 (bije .pill.add-btn z oryginalnego CSS) i ZERO !important —
1296   updateDockHints chowa sloty inline'owym display:none zależnie od presetu
1297   (np. Motion Design bez modelki), a !important z arkusza by to blokował. */
1298.mk-slots .pill.mk-slot {
1299  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
1300  width: 64px; height: 64px; border-radius: 16px;
1301  border: 1.5px dashed var(--border-hi); background: var(--card);
1302  color: var(--muted); font-size: 10px; font-weight: 700;
1303  transition: all .15s ease;
1304}
1305.mk-slots .pill.mk-slot i { font-size: 13px; color: var(--accent); }
1306.mk-slots .pill.mk-slot:hover { background: var(--card-hi); transform: translateY(-1px); color: var(--text); }
1307.mk-slot .mk-slot-label { text-transform: uppercase; letter-spacing: 0.06em; font-size: 8.5px; }
1308/* Preset woła modela → podświetlenie slotu (glow nadaje updateDockHints) */
1309.mk-slots .pill.mk-slot.glow { border-color: var(--accent); box-shadow: 0 0 18px rgba(89,232,255,0.35); color: var(--text); }
1310.mk-generate {
1311  display: flex; align-items: center; gap: 10px; padding: 8px 8px 8px 16px;
1312  border-radius: 18px; background: var(--grad);
1313  box-shadow: 0 6px 24px rgba(89,232,255,0.3);
1314}
1315.mk-generate .cost { color: rgba(7,8,15,0.85); font-weight: 800; font-size: 12px; white-space: nowrap; }
1316.mk-generate .send { background: rgba(7,8,15,0.22); box-shadow: none; border-radius: 12px; }
1317.mk-generate .send:hover { background: rgba(7,8,15,0.34); transform: none; }
1318.mk-generate .send:disabled { opacity: 0.45; }
1319@media (max-width: 860px) {
1320  .mk-dock-row { grid-template-columns: 1fr; gap: 10px; }
1321  .mk-modes { flex-direction: row; justify-content: flex-start;
1321 border-right: none; border-bottom: 1px solid var(--border); padding: 0 0 8px; }
1322  .mk-mode { flex-direction: row; min-width: 0; padding: 7px 14px; }
1323  .mk-slots { border-left: none; padding-left: 0; justify-content: space-between; }
1324  .mk-slot { flex: 0 0 auto; width: 56px; height: 56px; }
1325  .mk-generate { flex: 1; justify-content: space-between; }
1326}
1327
1328/* ═══ MOZAIKA GALERII (wzór z referencji: kafle różnych rozmiarów, ciasno, pełna
1329   szerokość — koniec sztywnej siatki). Wzór rozmiarów łamie się co 10 kafli, więc
1330   długa galeria nie wygląda jak tapeta. Czysty CSS na istniejącym renderStudios. ═══ */
1331#studios.studios {
1332  display: grid;
1333  grid-template-columns: repeat(auto-fill, minmax(236px, 1fr));
1334  grid-auto-rows: 212px;
1335  grid-auto-flow: dense;
1336  gap: 3px;
1337  max-width: none;
1338  margin: 0 -48px;              /* pełny wylew do krawędzi .center */
1339}
1340.studios .tile { border-radius: 0; width: 100%; height: 100%; animation: none; }
1341.studios .tile img.tile-img, .studios .tile div.tile-img { width: 100%; height: 100%; object-fit: cover; display: block; }
1342.studios .tile:hover { transform: none; box-shadow: inset 0 0 0 2px var(--accent); z-index: 2; }
1343.studios .tile.on { box-shadow: inset 0 0 0 2px var(--accent); }
1344/* Wzór mozaiki: duży bohater, dwa pionowe, jeden szeroki na każde 10 kafli */
1345.studios .tile:nth-child(10n+1) { grid-column: span 2; grid-row: span 2; }
1346.studios .tile:nth-child(10n+4) { grid-row: span 2; }
1347.studios .tile:nth-child(10n+7) { grid-column: span 2; }
1348.studios .tile:nth-child(10n+8) { grid-row: span 2; }
1349.studios .tile-info { padding: 14px 14px 12px; }
1350.studios .tile-name { font-size: 11.5px; }
1351.studios .tile-meta { font-size: 9.5px; }
1352/* Żywe podglądy wideo w kaflach */
1353.studios .tile video.tile-img { width: 100%; height: 100%; object-fit: cover; display: block; }
1354/* FORMAT FILMU dyktuje kształt kafla — te reguły są PO wzorze nth-child, więc wygrywają */
1355.studios .tile.tile-v916 { grid-row: span 2; grid-column: span 1; }
1356.studios .tile.tile-v169 { grid-column: span 2; grid-row: span 1; }
1357@media (max-width: 860px) {
1358  #studios.studios { margin: 0 -20px; grid-template-columns: repeat(2, 1fr); grid-auto-rows: 128px; }
1359  .studios .tile:nth-child(10n+7) { grid-column: span 2; }
1360}
1361
1362/* ═══ NAGŁÓWEK PRO (ms-head): eyebrow + wielki tytuł z gradientowym akcentem.
1363   data-i18n siedzi na WEWNĘTRZNYCH spanach, nie na h1 — tłumaczenie nie zjada markupu. ═══ */
1364.ms-head { margin-bottom: 26px; }
1365.ms-eyebrow {
1366  font-family: var(--font-display); font-size: 10.5px; font-weight: 600;
1367  letter-spacing: 0.24em; text-transform: uppercase; color: var(--accent);
1368  margin-bottom: 10px;
1369}
1370.ms-head h1 {
1371  font-family: var(--font-display); font-size: clamp(30px, 4vw, 46px);
1372  font-weight: 700; letter-spacing: -0.03em; line-height: 1.06; margin: 0 0 10px;
1373  text-wrap: balance;
1374}
1375.ms-head .ms-grad {
1376  background: var(--grad); -webkit-background-clip: text; background-clip: text;
1377  -webkit-text-fill-color: transparent; color: transparent;
1378}
1379.ms-head .sub { font-size: 14px; color: var(--muted); margin: 0; }
1380
1381/* ═══ FILTR GALERII: Wszystko / Obrazy / Filmy — pierwsze skrzypce nad kategoriami ═══ */
1382.mk-filtr { display: flex; gap: 4px; padding: 4px; margin-bottom: 12px; width: max-content; border-radius: 999px; background: var(--card); border: 1px solid var(--border); }
1383.mk-filtr button {
1384  display: inline-flex; align-items: center; gap: 7px;
1385  padding: 8px 18px; border-radius: 999px; border: none; cursor: pointer;
1386  background: transparent; color: var(--muted);
1387  font-family: var(--font-body); font-size: 12.5px; font-weight: 700;
1388  transition: all .16s ease;
1389}
1390.mk-filtr button i { font-size: 11px; }
1391.mk-filtr button:hover { color: var(--text); }
1392.mk-filtr button.on { background: var(--card-hi); color: var(--text); box-shadow: inset 0 0 0 1px var(--border-hi), 0 0 14px rgba(89,232,255,0.12); }
1393.mk-filtr button.on i { color: var(--accent); }
1394</style>
1395</head>
1396<body>
1397<div class="app">
1398  <!-- ═══ TOPBAR ═══ -->
1399  <div class="topbar">
1400    <div class="logo">
1401      <div class="logo-dot"><i class="fas fa-wand-magic-sparkles"></i></div>
1402      <div class="logo-brand">
1403        <a href="/" class="logo-brand-main" data-i18n-title="nav.back_home" title="Wróć na chmurka.ai">Chmurka.ai</a>
1404        <span class="logo-sep">·</span>
1405        <span class="logo-brand-sub">Marketing Studio</span>
1406      </div>
1407    </div>
1408    <nav class="tabs" id="tabs">
1409      <button class="tab on" data-view="create" data-i18n="nav.tab.creator">Kreator</button>
1410      <button class="tab" data-view="library" data-i18n="nav.tab.library">Biblioteka</button>
1411    </nav>
1412    <div class="tb-right">
1413      <div class="credits"><i class="fas fa-bolt"></i> <span id="credits-num">—</span></div>
1414      <button class="topup-btn" onclick="ecOpenPricing()" data-i18n-title="topup.title" title="Doładuj kredyty"><i class="fas fa-plus"></i> <span data-i18n="topup.btn">Doładuj</span></button>
1415      <a href="/" class="home-btn" data-i18n-title="nav.home" title="Strona główna"><i class="fas fa-arrow-right-to-bracket" style="transform:rotate(180deg);font-size:13px"></i></a>
1416      <div class="avatar" id="avatar">?</div>
1417    </div>
1418  </div>
1419
1420  <!-- ═══ MAIN ═══ -->
1421  <div class="main">
1422    <!-- CENTER -->
1423    <section class="center">
1424
1425      <!-- CREATE VIEW -->
1426      <div id="view-create" class="view on">
1427        <div class="page-head ms-head">
1428          <div class="ms-eyebrow">Marketing Studio</div>
1429          <h1><span data-i18n="ecommerce.creator.ms1">Twój produkt.</span> <span class="ms-grad" data-i18n="ecommerce.creator.ms2">Gotowa kampania.</span></h1>
1430          <p class="sub" data-i18n="ecommerce.creator.msSub">Wybierz szablon z galerii, dodaj zdjęcie produktu, resztę zrobi AI.</p>
1431        </div>
1432        <div class="controls">
1433          <div class="mk-filtr">
1434            <button type="button" class="on" data-f="all" onclick="mkFiltruj('all')"><i class="fas fa-border-all"></i> <span data-i18n="ecommerce.filtr.all">Wszystko</span></button>
1435            <button type="button" data-f="image" onclick="mkFiltruj('image')"><i class="far fa-image"></i> <span data-i18n="ecommerce.filtr.images">Obrazy</span></button>
1436            <button type="button" data-f="video" onclick="mkFiltruj('video')"><i class="fas fa-clapperboard"></i> <span data-i18n="ecommerce.filtr.videos">Filmy</span></button>
1437          </div>
1438          <div class="cats">
1439            <button class="cat on" data-cat="" data-i18n="ecommerce.cat.all">Wszystkie</button>
1440            <button class="cat" data-cat="fashion" data-i18n="ecommerce.cat.fashion">Moda</button>
1441            <button class="cat" data-cat="beauty" data-i18n="ecommerce.cat.beauty">Uroda</button>
1442            <button class="cat" data-cat="supplements" data-i18n="ecommerce.cat.supplements">Suplementy</button>
1443            <button class="cat" data-cat="home" data-i18n="ecommerce.cat.home">Dom</button>
1444            <button class="cat" data-cat="jewelry" data-i18n="ecommerce.cat.jewelry">Biżuteria</button>
1445            <button class="cat" data-cat="food" data-i18n="ecommerce.cat.food">Jedzenie</button>
1446            <button class="cat" data-cat="coffee" data-i18n="ecommerce.cat.coffee">Kawa/Herbata</button>
1447            <button class="cat" data-cat="candles" data-i18n="ecommerce.cat.candles">Świeczki</button>
1448            <button class="cat" data-cat="sport" data-i18n="ecommerce.cat.sport">Sport</button>
1449            <button class="cat" data-cat="kids" data-i18n="ecommerce.cat.kids">Dzieci</button>
1450            <button class="cat" data-cat="pets" data-i18n="ecommerce.cat.pets">Zwierzęta</button>
1451            <button class="cat" data-cat="alcohol" data-i18n="ecommerce.cat.alcohol">Alkohole</button>
1452            <button class="cat" data-cat="garden" data-i18n="ecommerce.cat.garden">Ogród</button>
1453            <button class="cat" data-cat="handmade" data-i18n="ecommerce.cat.handmade">Handmade</button>
1454            <button class="cat" data-cat="other" data-i18n="ecommerce.cat.other">Inne</button>
1455            <button type="button" class="cat cta-request" onclick="openCategoryRequestModal()"><i class="fas fa-plus" style="font-size:11px;margin-right:4px"></i><span data-i18n="ecommerce.cat.request">Nie ma Twojej kategorii? Kliknij</span></button>
1456          </div>
1457          <div class="segment">
1458            <button class="seg on" data-seg="discover" data-i18n="ecommerce.seg.discover">Odkrywaj</button>
1459            <button class="seg" data-seg="my" data-i18n="ecommerce.seg.my">Moje</button>
1460          </div>
1461        </div>
1462        <div id="studios" class="studios"></div>
1463        <div class="ms-how">
1464          <div class="ms-how-card"><b><i class="fas fa-box"></i>
1464<span data-i18n="ecommerce.how.step1_title">Dodaj produkt</span></b><p data-i18n="ecommerce.how.step1_desc">Wgraj zdjęcie swojego produktu do Biblioteki — wystarczy raz, potem wołasz go przez @nazwa.</p></div>
1465          <div class="ms-how-card"><b><i class="fas fa-palette"></i><span data-i18n="ecommerce.how.step2_title">Wybierz szablon</span></b><p data-i18n="ecommerce.how.step2_desc">Kliknij gotową scenę z galerii albo opisz własną w pasku na dole.</p></div>
1466          <div class="ms-how-card"><b><i class="fas fa-wand-magic-sparkles"></i><span data-i18n="ecommerce.how.step3_title">Generuj</span></b><p data-i18n="ecommerce.how.step3_desc">AI wstawi Twój produkt w scenę. Wynik masz w Historii po prawej — stamtąd edycja, napisy i pobranie.</p></div>
1467        </div>
1468
1469      </div>
1470
1471      <!-- LIBRARY VIEW -->
1472      <div id="view-library" class="view">
1473        <div class="page-head">
1474          <h1><span class="serif" data-i18n="ecommerce.library.page_title">Biblioteka</span></h1>
1475          <p class="sub" data-i18n="ecommerce.library.page_sub">Wszystkie Twoje produkty i modele — dostępne jako @nazwa w generatorze.</p>
1476        </div>
1477        <div class="lib-head">
1478          <div class="lib-tabs">
1479            <button class="lib-tab on" data-lib="product" data-i18n="ecommerce.lib.tab.products">Produkty</button>
1480            <button class="lib-tab" data-lib="character" data-i18n="ecommerce.lib.tab.models">Modele</button>
1481            <!-- Brand Kit "Mój Styl" tymczasowo schowany — odsłonić gdy gotowe.
1482            <button class="lib-tab" data-lib="brandkit">Mój Styl ✨</button>
1483            -->
1484          </div>
1485          <button class="btn-add" id="btn-create-influ" style="display:none;background:var(--grad);color:#07070b;margin-right:8px" onclick="openInfluChat()"><i class="fas fa-wand-magic-sparkles"></i> <span data-i18n="ecommerce.lib.create_influ">Stwórz influ AI</span></button>
1486          <button class="btn-add" id="btn-add-asset"><i class="fas fa-plus"></i> <span id="btn-add-label" data-i18n="ecommerce.lib.add_product">Dodaj produkt</span></button>
1487        </div>
1488        <div id="assets" class="assets-grid"></div>
1489        <div id="brandkit-view" style="display:none; max-width:1000px"></div>
1490      </div>
1491    </section>
1492
1493    <!-- RIGHT COLUMN -->
1494    <aside class="right-col">
1495      <div class="col-header">
1496        <div class="col-title" data-i18n="ecommerce.sidebar.history">Historia</div>
1497        <div class="col-count" id="hist-count">0</div>
1498      </div>
1499      <div id="hist" class="hist"></div>
1500      <div class="lib-quick">
1501        <h4 data-i18n="ecommerce.sidebar.lib_title">Biblioteka</h4>
1502        <div class="lib-links">
1503          <button class="lib-link" onclick="go('library');setLib('product')">
1504            <span><i class="fas fa-box"></i>&nbsp;&nbsp;<span data-i18n="ecommerce.lib.tab.products">Produkty</span></span>
1505            <span class="cnt" id="cnt-products">0</span>
1506          </button>
1507          <button class="lib-link" onclick="go('library');setLib('character')">
1508            <span><i class="fas fa-user"></i>&nbsp;&nbsp;<span data-i18n="ecommerce.lib.tab.models">Modele</span></span>
1509            <span class="cnt" id="cnt-characters">0</span>
1510          </button>
1511        </div>
1512      </div>
1513    </aside>
1514  </div>
1515</div>
1516
1517<!-- ═══ DOCK ═══ -->
1518<div class="dock" id="dock">
1519  <div class="dock-inner">
1520   <div class="mk-dock-row">
1521    <!-- Tryb: Obraz / Wideo (dock 2.0) -->
1522    <div class="mk-modes">
1523      <button type="button" class="mk-mode on" id="mk-mode-image" onclick="mkSetTryb('image')"><i class="far fa-image"></i><span data-i18n="ecommerce.dock.mode_image">Obraz</span></button>
1524      <button type="button" class="mk-mode" id="mk-mode-video" onclick="mkSetTryb('video')"><i class="fas fa-clapperboard"></i><span data-i18n="ecommerce.dock.mode_video">Wideo</span></button>
1525    </div>
1526    <!-- Środek: referencje + prompt + chipy ustawień -->
1527    <div class="mk-center">
1528    <div id="dock-refs" class="dock-refs"></div>
1529    <div style="position:relative">
1530      <div id="mention-dd" class="mention-dd"></div>
1531      <textarea id="p-input" class="p-input" data-i18n-placeholder="ecommerce.dock.placeholder" placeholder="Opisz scenę, albo wpisz @ żeby dodać produkt..." rows="1"></textarea>
1532    </div>
1533    <div class="dock-bar">
1534      <button class="pill add-btn" onclick="document.getElementById('dock-file-input').click()" data-i18n-title="ecommerce.dock.attach_photo" title="Dołącz zdjęcie (do edycji przez AI)"><i class="fas fa-paperclip"></i> <span data-i18n="ecommerce.dock.photo_btn">Zdjęcie</span></button>
1535      <input type="file" id="dock-file-input" accept="image/*" multiple style="display:none" onchange="onDockFiles(this)">
1536      <button class="pill studio" id="studio-pill" style="display:none" onclick="clearStudio()">
1537        <i class="fas fa-palette"></i>
1538        <span id="studio-pill-name"></span>
1539        <i class="fas fa-xmark" style="opacity:0.5;margin-left:2px;font-size:9px"></i>
1540      </button>
1541      <button class="pill" onclick="cycleRatio()"><i class="fas fa-crop-simple"></i> <span id="ratio-label">1:1</span></button>
1542      <button class="pill pill-with-help" id="pose-toggle" onclick="togglePosePreserve()" data-help="Zachowaj pozycję / orientację / kąt produktu z referki. Włącz to dla opakowań z czytelnym tekstem, logo lub etykietą — AI nie obróci ich i nie ukryje napisów. Wyłącz dla zwykłych produktów żeby AI mogła ustawiać je w nowych kompozycjach."><i class="fas fa-square" id="pose-icon"></i> <span data-i18n="ecommerce.dock.preserve_pose">Zachowaj pozycję</span> <i class="fas fa-circle-question pill-help-icon"></i></button>
1543      <button class="pill pill-with-help" id="bulk-toggle" onclick="toggleBulkMode()" data-help-pl="Bulk: każdy produkt w docku zostanie użyty w OSOBNEJ generacji zamiast wszystkie razem na jednym zdjęciu. Max 2 generacje równolegle, kolejka. Idealnie gdy masz 5-10 produktów i chcesz dla każdego osobne zdjęcie w tym samym stylu/Studio." data-help-en="Bulk: each product in the dock will be used in a SEPARATE generation instead of all together in one photo. Max 2 parallel generations, queued. Ideal when you have 5-10 products and want individual photos in the same style/Studio." data-help="Bulk: każdy produkt w docku zostanie użyty w OSOBNEJ generacji zamiast wszystkie razem na jednym zdjęciu. Max 2 generacje równolegle, kolejka. Idealnie gdy masz 5-10 produktów i chcesz dla każdego osobne zdjęcie w tym samym stylu/Studio." style="display:none"><i class="fas fa-square" id="bulk-icon"></i> <span id="bulk-label" data-i18n="ecommerce.dock.bulk">Bulk (każdy osobno)</span> <i class="fas fa-circle-question pill-help-icon"></i></button>
1544      <span class="spacer"></span>
1545    </div>
1546    <div id="dock-hint" style="display:none;padding:8px 12px 2px;font-size:11.5px;color:var(--accent2);letter-spacing:-0.01em">
1547      <i class="fas fa-circle-info" style="margin-right:6px"></i>
1548      <span id="dock-hint-text" data-i18n="ecommerce.dock.hintModel">Ten preset potrzebuje modela — kliknij "Model" żeby wybrać</span>
1549    </div>
1550    </div>
1551    <!-- Prawa: sloty Produkt/Model + Generuj z kosztem (dock 2.0) -->
1552    <div class="mk-slots">
1553      <button class="pill add-btn mk-slot" onclick="openAssetPicker('product')" data-i18n-title="ecommerce.dock.add_product" title="Dodaj produkt"><i class="fas fa-plus"></i><span class="mk-slot-label" data-i18n="ecommerce.dock.product_btn">Produkt</span></button>
1554      <button class="pill add-btn mk-slot" id="add-model-btn" onclick="openAssetPicker('character')" data-i18n-title="ecommerce.dock.add_model" title="Dodaj model"><i class="fas fa-plus"></i><span class="mk-slot-label" data-i18n="ecommerce.dock.model_btn">Model</sp
1554an></button>
1555      <div class="mk-generate">
1556        <span class="cost" id="cost">—</span>
1557        <button class="send" id="btn-gen" onclick="doGenerate()" data-i18n-title="btn.generate" title="Generuj"><i class="fas fa-arrow-up"></i></button>
1558      </div>
1559    </div>
1560   </div>
1561  </div>
1562</div>
1563
1564<!-- ═══ ASSET MODAL ═══ -->
1565<div id="modal" class="backdrop" onclick="if(event.target===this)closeModal()">
1566  <div class="modal">
1567    <h3 id="m-title" data-i18n="ecommerce.modal.add_product">Dodaj produkt</h3>
1568    <p id="m-sub" data-i18n="ecommerce.modal.add_product_sub">Wgraj zdjęcie. Możesz nadać krótką nazwę bez spacji (np. <code style="background:var(--card);padding:2px 7px;border-radius:6px">Koszulka</code>) — albo zostawić puste, system wygeneruje automatycznie.</p>
1569    <label class="fld-label"><span data-i18n="ecommerce.modal.name_label">Nazwa</span> <span style="color:var(--muted2);font-weight:400;font-size:11px" data-i18n="ecommerce.modal.optional">(opcjonalna)</span></label>
1570    <input type="text" class="fld" id="m-name" data-i18n-placeholder="ecommerce.modal.name_placeholder" placeholder="np. Koszulka_red lub zostaw puste" maxlength="60">
1571    <label class="fld-label" data-i18n="ecommerce.modal.photo_label">Zdjęcie</label>
1572    <div class="drop" id="m-drop" onclick="document.getElementById('m-file').click()">
1573      <i class="fas fa-cloud-arrow-up"></i>
1574      <div class="drop-txt" data-i18n="ecommerce.modal.drop_txt">Kliknij lub przeciągnij plik<br><span style="color:var(--muted2);font-size:11.5px">max 10MB · JPG / PNG / WEBP</span></div>
1575      <img id="m-preview" style="display:none">
1576      <input type="file" id="m-file" accept="image/*" style="display:none" onchange="onModalFile(this)">
1577    </div>
1578    <label class="fld-label" data-i18n="ecommerce.modal.desc_label">Opis (opcjonalny)</label>
1579    <input type="text" class="fld" id="m-desc" data-i18n-placeholder="ecommerce.modal.desc_placeholder" placeholder="Np. bawełna, rozmiar M, czerwona" maxlength="500">
1580    <div class="m-actions">
1581      <button class="btn" onclick="closeModal()" data-i18n="btn.cancel">Anuluj</button>
1582      <button class="btn btn-pr" id="btn-save" onclick="saveAsset()" data-i18n="btn.add">Dodaj</button>
1583    </div>
1584  </div>
1585</div>
1586
1587<!-- ═══ PROMPT PREVIEW MODAL ═══ -->
1588<div id="preview-modal" class="backdrop" onclick="if(event.target===this)closePreview()">
1589  <div class="modal" style="max-width:600px">
1590    <h3 data-i18n="ecommerce.preview.title">Finalny prompt</h3>
1591    <p data-i18n="ecommerce.preview.sub">Chmurka AI połączyła scenę ze stylu z Twoim opisem i @nazwami. Możesz edytować przed wysłaniem do generatora.</p>
1592    <div style="position:relative;margin-bottom:14px">
1593      <div id="preview-badge" style="position:absolute;top:10px;right:12px;padding:3px 10px;border-radius:999px;background:var(--card-hi);font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em;font-weight:600;display:none">
1594        <i class="fas fa-sparkles" style="color:var(--accent)"></i> <span id="preview-badge-text">Chmurka AI</span>
1595      </div>
1596      <textarea id="preview-text" class="fld" rows="8" style="font-size:13.5px;line-height:1.55;padding:16px;margin-bottom:0;font-family:ui-monospace,'SF Mono',Menlo,monospace"></textarea>
1597    </div>
1598    <div style="display:flex;align-items:center;gap:8px;margin-bottom:18px;padding:10px 14px;background:var(--card);border-radius:12px;font-size:11.5px;color:var(--muted);letter-spacing:-0.01em">
1599      <i class="fas fa-circle-info" style="color:var(--accent);font-size:12px"></i>
1600      <span><span data-i18n="ecommerce.preview.model_label">Model:</span> <b id="preview-model" style="color:var(--text)">nano-banana-2</b> · <span data-i18n="ecommerce.preview.ratio_label">Proporcje:</span> <b id="preview-ratio" style="color:var(--text)">1:1</b> · Refs: <b id="preview-refs" style="color:var(--text)">0</b></span>
1601    </div>
1602    <div class="m-actions">
1603      <button class="btn" onclick="closePreview()" data-i18n="btn.cancel">Anuluj</button>
1604      <button class="btn btn-pr" id="btn-confirm-gen" onclick="confirmGenerate()"><i class="fas fa-arrow-up" style="margin-right:6px"></i> <span data-i18n="btn.generate">Generuj</span></button>
1605    </div>
1606  </div>
1607</div>
1608
1609<!-- ═══ ASSET PICKER DRAWER ═══ -->
1610<div id="picker-backdrop" class="picker-backdrop" onclick="if(event.target===this)closePicker()">
1611  <div class="picker">
1612    <div class="picker-head">
1613      <div>
1614        <h3 id="picker-title" data-i18n="ecommerce.picker.title">Wybierz produkt</h3>
1615        <div class="picker-sub" id="picker-sub" data-i18n="ecommerce.picker.sub">Z Biblioteki albo Chmurkowi</div>
1616      </div>
1617      <button class="picker-close" onclick="closePicker()"><i class="fas fa-xmark"></i></button>
1618    </div>
1619    <div class="picker-tabs">
1620      <button class="picker-tab on" data-picker-tab="mine" data-i18n="ecommerce.picker.tab_mine">Moje</button>
1621      <button class="picker-tab" data-picker-tab="chmurka" id="picker-tab-chmurka">Chmurkowi ✨</button>
1622      <button class="picker-upload-btn" id="picker-upload-btn"><i class="fas fa-plus"></i> <span data-i18n="ecommerce.picker.add_new">Dodaj nowy</span></button>
1623    </div>
1624    <div class="picker-body">
1625      <div id="picker-grid" class="picker-grid"></div>
1626    </div>
1627  </div>
1628</div>
1629
1630<!-- ═══ INFLU CHAT MODAL ═══ -->
1631<div id="influ-modal" class="backdrop" onclick="if(event.target===this)closeInfluChat()">
1632  <div class="ic-modal">
1633    <div class="ic-head">
1634      <h3><i class="fas fa-wand-magic-sparkles"></i> <span data-i18n="ecommerce.influ.title">Stwórz swojego influ</span></h3>
1635      <div class="sub" data-i18n="ecommerce.influ.sub">AI zada kilka pytań, potem wygeneruje Twojego modela. Koszt: 20 kr. Zawsze biały studio background 9:16.</div>
1636    </div>
1637    <div class="ic-body" id="ic-body"></div>
1638    <div class="ic-input-wrap" id="ic-input-wrap">
1639      <textarea class="ic-input" id="ic-input" data-i18n-placeholder="ecommerce.influ.input_placeholder" placeholder="Odpowiedz..." rows="1" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();icSendMsg();}"></textarea>
1640      <button class="ic-send" id="ic-send-btn" onclick="icSendMsg()"><i class="fas fa-arrow-up"></i></button>
1641    </div>
1642  </div>
1643</div>
1644
1645<!-- ═══ EDITOR MODAL ═══ -->
1646<div id="editor" class="ed-backdrop">
1647  <div class="ed-modal">
1648    <div class="ed-head">
1649      <div>
1650        <h3 data-i18n="ecommerce.editor.title">Edytor</h3>
1651        <div class="ed-sub" id="ed-subtitle" data-i18n="ecommerce.editor.subtitle_default">Wybierz narzędzie po lewej</div>
1652      </div>
1653      <button class="ed-close" onclick="closeEditor()"><i class="fas fa-xmark"></i></button>
1654    </div>
1655    <div class="ed-body">
1656      <aside class="ed-tools">
1657        <button class="ed-tool on" data-tool="ai-edit" onclick="edSelectTool('ai-edit')"><i class="fas fa-wand-magic-sparkles"></i> <span data-i18n="ecommerce.editor.tool.ai_edit">Edytuj AI</span></button>
1658        <button class="ed-tool" data-tool="reframe" onclick="edSelectTool('reframe')"><i class="fas fa-expand"></i> <span data-i18n="ecommerce.editor.tool.reframe">Rozszerz</span></button>
1659        <button class="ed-tool" data-tool="camera" onclick="edSelectTool('camera')"><i class="fas fa-camera-rotate"></i> <span data-i18n="ecommerce.editor.tool.camera">Kąt kamery</span></button>
1660        <button class="ed-tool" data-tool="filter" onclick="edSelectTool('filter')"><i class="fas fa-sliders"></i> <span data-i18n="ecommerce.editor.tool.filter">Jasność / Kolor</span></button>
1661        <button class="ed-tool" data-tool="text" onclick="edSelectTool('text')"><i class="fas fa-font"></i> <span data-i18n="ecommerce.editor.tool.text">Dodaj napisy</span></button>
1662      </aside>
1663      <main class="ed-canvas" id="ed-canvas">
1664        <img id="ed-img" src="" alt="" referrerpolicy="no-referrer">
1665        <div id="ed-text-stage">
1666          <div id="ed-text-frame">
1667            <img id="ed-text-img" src="" alt="" referrerpolicy="no-referrer">
1668            <!-- text layers wstrzykiwane dynamicznie -->
1669          </div>
1670        </div>
1671        <div class="ed-loading" id="ed-loading">
1672          <div class="spin"></div>
1673          <div class="ed-loading-text" id="ed-loading-text" data-i18n="ecommerce.editor.processing">Przetwarzanie...</div>
1674        </div>
1675      </main>
1676      <aside class="ed-panel" id="ed-panel"></aside>
1677    </div>
1678    <div class="ed-foot">
1679      <div class="ed-foot-left">
1680        <button class="ed-foot-btn" onclick="edDownload()"><i class="fas fa-download"></i> <span data-i18n="btn.download">Pobierz</span></button>
1681        <button class="ed-foot-btn" onclick="edAddAsRef()"><i class="fas fa-plus"></i> <span data-i18n="ecommerce.editor.use_as_ref">Użyj jako referencję</span></button>
1682      </div>
1683      <button class="ed-foot-btn primary" onclick="closeEditor()" data-i18n="btn.close">Zamknij</button>
1684    </div>
1685  </div>
1686</div>
1687
1688<!-- ═══ CATEGORY REQUEST MODAL ═══ -->
1689<div id="cat-request-modal" class="backdrop" onclick="if(event.target===this)closeCategoryRequestModal()">
1690  <div class="modal" style="max-width:520px">
1691    <div style="padding:24px 24px 8px">
1692      <h3 style="margin:0 0 8px;font-size:20px;font-family:'Cormorant Garamond',serif;font-weight:600" data-i18n="ecommerce.cat_request.title">Zaproponuj kategorię</h3>
1693      <p style="margin:0 0 4px;color:var(--muted);font-size:13px;line-height:1.5" data-i18n="ecommerce.cat_request.sub">Nie znalazłeś kategorii dla swojego biznesu? Powiedz nam o czym potrzebujesz szablonów.</p>
1694      <p style="margin:0 0 16px;color:var(--accent);font-size:13px;font-weight:600"><i class="fas fa-bolt" style="font-size:11px;margin-right:4px"></i><span data-i18n="ecommerce.cat_request.promise">Dodajemy nowe presety w max 24 godziny</span></p>
1695    </div>
1696    <div style="padding:0 24px 16px">
1697      <label style="display:block;font-size:12px;color:var(--muted);margin-bottom:6px;font-weight:500" data-i18n="ecommerce.cat_request.email_label">Twój email</label>
1698      <input id="cr-email" type="email" placeholder="[email protected]" style="width:100%;background:var(--card);border:1px solid var(--line);color:var(--text);padding:10px 12px;border-radius:8px;font-size:14px;font-family:inherit;margin-bottom:12px" />
1699      <label style="display:block;font-size:12px;color:var(--muted);margin-bottom:6px;font-weight:500" data-i18n="ecommerce.cat_request.message_label">O jakiej kategorii / produkcie?</label>
1700      <textarea id="cr-message" rows="4" data-i18n-placeholder="ecommerce.cat_request.message_placeholder" placeholder="Np. sprzedaję świece sojowe w słoikach, potrzebowałbym szablonów typu spa, hygge, scandynawski minimal..." style="width:100%;background:var(--card);border:1px solid var(--line);color:var(--text);padding:10px 12px;border-radius:8px;font-size:14px;font-family:inherit;resize:vertical;min-height:100px"></textarea>
1701    </div>
1702    <div style="padding:0 24px 24px;display:flex;gap:8px;justify-content:flex-end">
1703      <button onclick="closeCategoryRequestModal()" style="background:transparent;color:var(--muted);border:1px solid var(--line);padding:10px 18px;border-radius:8px;cursor:pointer;font-family:inherit;font-size:14px" data-i18n="btn.cancel">Anuluj</button>
1704      <button id="cr-submit-btn" onclick="submitCategoryRequest()" style="background:var(--accent);color:#07070b;border:none;padding:10px 18px;border-radius:8px;cursor:pointer;font-weight:600;font-family:inherit;font-size:14px" data-i18n="ecommerce.cat_request.submit">Wyślij propozycję</button>
1705    </div>
1706  </div>
1707</div>
1708
1709<!-- Mobile: Floating "Historia" button + bottom sheet (visible tylko na ≤768px) -->
1710<button class="m-history-fab" id="m-history-fab" onclick="openMobileHistory()" title="Historia generacji">
1711  <i class="fas fa-clock-rotate-left"></i>
1712  <span class="badge" id="m-history-badge" style="display:none">0</span>
1713</button>
1714
1715<div class="m-history-backdrop" id="m-history-backdrop" onclick="closeMobileHistory()"></div>
1716<div class="m-history-sheet" id="m-history-sheet">
1717  <div class="m-history-sheet-handle"></div>
1718  <div class="m-history-sheet-head">
1719    <h4 data-i18n="ecommerce.mobile.history_title">Historia generacji</h4>
1720    <button class="m-history-sheet-close" onclick="closeMobileHistory()"><i class="fas fa-xmark"></i></button>
1721  </div>
1722  <div id="m-hist-list" class="hist"></div>
1723</div>
1724
1725<!-- Pricing modal container (loaded async via fetch — same flow as index.html) -->
1726<div id="pricing-modal-container"></div>
1727
1728<div id="toast" class="toast"></div>
1729
1730
1731<script>
1732(function(){
1733  const API = window.location.origin;
1734  const S = {
1735    token: localStorage.getItem('token'),
1736    view: 'landing',
1737    seg: 'discover',
1738    cat: '',
1739    lib: 'product',
1740    assets: [],
1741    studios: [],
1742    currentStudio: null,
1743    activeRefs: [],
1744    modalType: 'product',
1745    modalFile: null,
1746    ratios: ['1:1','4:5','3:4','9:16','16:9'],
1747    ratioIdx: 0,
1748    pendingTasks: new Set(),  // wiele tasków naraz (zamiast single pendingTaskId)
1749    polling: false,
1750    preservePose: false,  // toggle: zachowaj pozycję produktu z referki (dla opakowań z czytelnym tekstem)
1751    bulkMode: false,      // toggle: każdy ref w docku = osobny task (concurrency 2)
1752  };
1753
1754  const $ = (id) => document.getElementById(id);
1755  const authH = (json=true) => {
1756    const h = {};
1757    if (json) h['Content-Type'] = 'application/json';
1758    if (S.token && S.token !== 'cookie-auth') h.Authorization = 'Bearer ' + S.token;
1759    return h;
1760  };
1761  // Centralna funkcja do update kredytów (4 elementy + localStorage + event)
1762  window.setCredits = function(n) {
1763    const num = Math.floor(Number(n));
1764    if (Number.isNaN(num)) return;
1765    localStorage.setItem('credits', num);
1766    const el = document.getElementById('credits-num');
1767    if (el) el.textContent = num.toLocaleString('pl');
1768    try { window.dispatchEvent(new CustomEvent('credits-changed', { detail: { credits: num } })); } catch (_) {}
1769  };
1770
1771  const api = async (path, opts = {}) => {
1772    const r = await fetch(API + path, { ...opts, headers: { ...authH(), ...(opts.headers||{}) }, credentials: 'include' });
1773    const ct = r.headers.get('content-type') || '';
1774    const d = ct.includes('json') ? await r.json() : await r.text();
1775    if (!r.ok) throw new Error(d?.error || `HTTP ${r.status}`);
1776    // Auto-update credits z każdej response gdzie są
1777    if (d && typeof d === 'object') {
1778      if (d.credits !== undefined) window.setCredits(d.credits);
1779      else if (d.user?.credits !== undefined) window.setCredits(d.user.credits);
1780    }
1781    return d;
1782  };
1783
1784  // Polling fallback co 60s — łapie refundy z tła (Dreamina retry, RunBlob fail, etc.)
1785  setInterval(() => {
1786    if (S.token) api('/me').catch(() => {});
1787  }, 60_000);
1788  const toast = (m, ms=2500) => {
1789    if (typeof m === 'string' && location.hostname.indexOf('pixcloud') !== -1) {
1790      m = m.replace(/ i podpięto jako referencję/, ' and attached as reference').replace(/ do promptu/, ' to prompt').replace(/^Dodano do Moich ♥$/, 'Added to Mine ♥').replace(/^Masz już w Moich$/, 'Already in Mine').replace(/^Dodano /, 'Added ');
1791    }
1792    const t = $('toast'); t.textContent = m; t.classList.add('show');
1793    clearTimeout(toast._t); toast._t = setTimeout(()=>t.classList.remove('show'), ms);
1794  };
1795
1796  // ═══ AUTH ═══
1797  async function boot() {
1798    if (!S.token) {
1799      try {
1800        const r = await fetch(API + '/auth/me', { credentials: 'include' });
1801        if (r.ok) {
1802          const d = await r.json();
1803          if (d.user) {
1804            S.token = 'cookie-auth';
1805            localStorage.setItem('token','cookie-auth');
1806            localStorage.setItem('credits', d.user.credits||0);
1807            localStorage.setItem('nickname', d.user.nickname||'');
1808          }
1809        }
1810      } catch(_){}
1811    }
1812    // Niezalogowany — pozwalamy zobaczyć stronę. Klik "Generuj" wywoła requireLogin().
1813    if (!S.token) {
1814      $('credits-num').textContent = '—';
1815      $('avatar').textContent = '?';
1816      return false;
1817    }
1818    try {
1819      const me = await api('/me');
1820      $('credits-num').textContent = (me.credits||0).toLocaleString('pl');
1821      $('avatar').textContent = (me.nickname||'U')[0].toUpperCase();
1822    } catch(e){ console.warn('/me', e); }
1823    return true;
1824  }
1825
1826  // Wywołać przy każdej akcji wymagającej zalogowania (generate, save, etc.)
1827  function requireLogin(featureLabel) {
1828    if (S.token) return true;
1829    toast(`Zaloguj się, żeby ${featureLabel || 'kontynuować'} →`, 3000);
1830    setTimeout(() => { window.location.href = '/?redirect=/ecommerce'; }, 600);
1831    return false;
1832  }
1833  window.requireLogin = requireLogin;
1834
1835  // ═══ ROUTING ═══
1836  window.go = function(v) {
1837    S.view = v;
1838    document.querySelectorAll('.view').forEach(el => el.classList.remove('on'));
1839    $('view-' + v).classList.add('on');
1840    document.querySelectorAll('#tabs .tab').forEach(t => t.classList.toggle('on', t.dataset.view === v));
1841    document.body.classList.toggle('is-landing', v === 'landing');
1842    if (v === 'landing') loadShowcase();
1843    if (v === 'create') loadStudios();
1844    if (v === 'library') loadAssets();
1845    if (v !== 'landing') loadHistory();
1846    window.scrollTo({ top: 0, behavior: 'smooth' });
1847  };
1848
1849  // ═══ LANDING SHOWCASE — różnorodne presety z różnych kategorii ═══
1850  let _showcaseLoaded = false;
1851  async function loadShowcase() {
1852    if (_showcaseLoaded) return;
1853    const host = $('land-showcase');
1854    host.innerHTML = Array.from({ length: 8 }).map(() =>
1855      `<div class="land-tile land-tile-empty"><i class="fas fa-spinner fa-spin"></i></div>`
1856    ).join('');
1857    try {
1858      const data = await api('/api/brand/studios');
1859      const withThumb = (data || []).filter(s => s.thumbnail && !s.ownerId);  // tylko publiczne
1860      if (!withThumb.length) {
1861        host.innerHTML = `<div class="land-tile land-tile-empty" style="grid-column:1/-1;aspect-ratio:auto;padding:48px 24px;text-align:center">${window.t?window.t('ecom.soonTemplates'):'Wkrótce nowe szablony'}</div>`;
1862        return;
1863      }
1864      // Różnorodność: pierwsze tile z każdej kategorii (do 8 unikalnych kategorii)
1865      const seenCats = new Set();
1866      const diverse = [];
1867      for (const s of withThumb) {
1868        const cat = s.category || 'other';
1869        if (!seenCats.has(cat) && diverse.length < 8) {
1870          diverse.push(s);
1871          seenCats.add(cat);
1872        }
1873      }
1874      // Dopełnij do 8 jeśli mało kategorii
1875      if (diverse.length < 8) {
1876        for (const s of withThumb) {
1877          if (!diverse.includes(s) && diverse.length < 8) diverse.push(s);
1878        }
1879      }
1880      host.innerHTML = diverse.map(s => `
1881        <div class="land-tile" data-slug="${s.slug || ''}">
1882          <img src="${s.thumbnail}" alt="${(s.name || '').replace(/"/g,'&quot;')}" loading="lazy" referrerpolicy="no-referrer">
1883          <div class="land-tile-label">${s.name || ''}</div>
1884          <div class="land-tile-cta" onclick="useShowcaseTile('${s.slug || ''}')">
1885            <button class="land-tile-btn" type="button"><i class="fas fa-wand-magic-sparkles"></i> Użyj szablonu</button>
1886          </div>
1887        </div>
1888      `).join('');
1889      _showcaseLoaded = true;
1890    } catch(e) {
1891      host.innerHTML = `<div class="land-tile land-tile-empty" style="grid-column:1/-1;aspect-ratio:auto;padding:48px 24px;text-align:center">${window.t?window.t('ecom.loadFail'):'Nie udało się załadować inspiracji'}</div>`;
1892    }
1893  }
1894  window.useShowcaseTile = function(slug) {
1895    if (slug) sessionStorage.setItem('preselectStudioSlug', slug);
1896    go('create');
1897  };
1898  document.querySelectorAll('#tabs .tab').forEach(t => { if (!t.disabled) t.onclick = () => go(t.dataset.view); });
1899
1900  // ═══ STUDIOS ═══
1901  async function loadStudios() {
1902    try {
1903      // Preselect z landing — kategoria
1904      const preselectCat = sessionStorage.getItem('preselectCategory');
1905      if (preselectCat) {
1906        S.cat = preselectCat;
1907        sessionStorage.removeItem('preselectCategory');
1908        // aktualizuj UI cat-pillów
1909        document.querySelectorAll('.cats .cat').forEach(c => c.classList.toggle('on', c.dataset.cat === preselectCat));
1910      }
1911      // Galerią rządzi FILTR (Wszystko/Obrazy/Filmy), nie tryb docka — wejście pokazuje
1912      // wszystko wymieszane w mozaice, badge WIDEO odróżnia filmy.
1913      const _f = window._mkFiltr || 'all';
1914      const url = '/api/brand/studios' + (_f === 'all'
1915        ? (S.cat ? '?category=' + S.cat : '')
1916        : ('?mode=' + _f + (S.cat ? '&category=' + S.cat : '')));
1917      const data = await api(url);
1918      S.allStudios = data;
1919      S.ownedSlugs = new Set(data.filter(s => s.ownerId).map(s => s.slug));
1920      const isMy = S.seg === 'my';
1921      S.studios = data.filter(s => isMy ? s.ownerId : !s.ownerId);
1922      renderStudios();
1923      // Preselect z landing — konkretne studio przez slug
1924      const preselectSlug = sessionStorage.getItem('preselectStudioSlug');
1925      if (preselectSlug) {
1926        sessionStorage.removeItem('preselectStudioSlug');
1927        const target = (data || []).find(s => s.slug === preselectSlug);
1928        if (target) setTimeout(() => pickStudio(target._id), 150);
1929      }
1930    } catch(e) { console.warn('studios', e); }
1931  }
1932  // ── OSZCZĘDNY STREAMING WIDEO (25.08): autoplay na kaflach i w historii ściągał
1933  // WSZYSTKIE mp4 naraz (1 GB+ transferu przy pełnej historii). Żywa ściana zostaje,
1934  // ale film gra TYLKO w kadrze: wejście w viewport = podepnij src i graj; wyjście =
1935  // pauza i zwolnienie bufora (removeAttribute+load). Powrót gra znów, już z cache.
1936  const vidIO = new IntersectionObserver((wpisy) => {
1937    for (const w of wpisy) {
1938      const v = w.target;
1939      if (w.isIntersecting) {
1940        if (!v.getAttribute('src') && v.dataset.src) v.setAttribute('src', v.dataset.src);
1941        const p = v.play(); if (p && p.catch) p.catch(() => {});
1942      } else if (v.getAttribute('src')) {
1943        try { v.pause(); } catch (_) {}
1944        v.removeAttribute('src'); v.load();
1945      }
1946    }
1947  }, { rootMargin: '160px' });
1948  function uzbrojLazyWideo(kontener) {
1949    (kontener || document).querySelectorAll('video[data-src]').forEach(v => {
1950      if (!v._lazyVid) { v._lazyVid = true; vidIO.observe(v); }
1951    });
1952  }
1953
1954  function renderStudios() {
1955    const el = $('studios');
1956    if (!S.studios.length) {
1957      const _en = location.hostname.indexOf('pixcloud') !== -1;
1958      el.innerHTML = `<div class="empty" style="grid-column:1/-1;padding:80px 20px"><i class="fas fa-${S.seg==='my'?'wand-magic-sparkles':'palette'}"></i><h4>${S.seg==='my'?(_en?'Nothing saved yet':'Jeszcze nic nie zapisałeś'):(_en?'No styles in this category':'Brak stylów w tej kategorii')}</h4><p>${S.seg==='my'?(_en?'Your own styles will appear here.':'Twoje własne style pojawią się tutaj.'):(_en?'Pick another category or explore Discover.':'Wybierz inną kategorię albo Odkrywaj.')}</p></div>`;
1959      return;
1960    }
1961    const isPublicView = S.seg !== 'my';
1962    el.innerHTML = S.studios.map(s => {
1963      const isSaved = isPublicView && S.ownedSlugs?.has(s.slug);
1964      // UWAGA: <span>, nie <button> — nested buttons psują strukturę DOM (browser auto-zamyka outer .tile button)
1965      const heartBtn = isPublicView ? `<span role="button" tabindex="0" class="tile-heart ${isSaved?'saved':''}" data-slug="${s.slug}" onclick="event.stopPropagation();cloneStudio('${s._id}',this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();event.stopPropagation();cloneStudio('${s._id}',this);}" title="${isSaved?'Masz już w Moich':'Dodaj do Moich'}"><i class="fa${isSaved?'s':'r'} fa-heart"></i></span>` : '';
1966      // Szablony wideo: ŻYWY podgląd (pętla bez dźwięku) zamiast statycznego thumba,
1967      // a kształt kafla w mozaice bierze się z formatu filmu (9:16 pion, 16:9 szeroki).
1968      const jestWideo = Array.isArray(s.modes) && s.modes.includes('video');
1969      const aspektKafla = jestWideo ? ((s.videoRecipe && s.videoRecipe.aspekt) || s.defaultAspectRatio || '9:16') : '';
1970      const klasaAspektu = jestWideo ? (aspektKafla === '16:9' ? ' tile-v169' : ' tile-v916') : '';
1971      const medium = (jestWideo && s.preview)
1972        ? `<video data-src="${s.preview}" class="tile-img" muted loop playsinline preload="none"${s.thumbnail ? ` poster="${s.thumbnail}"` : ''}></video>`
1973        : s.thumbnail ? `<img src="${s.thumbnail}" class="tile-img" alt="" loading="lazy" decoding="async" referrerpolicy="no-referrer">` : '<div class="tile-img"></div>';
1974      // Kafle wideo mówią userowi CO ROBI szablon i CO DODAĆ („chuj wie do którego co
1975      // dodać" — owner 25.08): opis z bazy (pole opis{pl,en,cs}) + plakietka wymagań.
1976      const _lng = (document.documentElement.lang || 'pl').slice(0, 2);
1977      const _chip = { pl: ['📷 dodaj zdjęcie produktu', '✍️ z haseł, logo opcjonalnie'], en: ['📷 add a product photo', '✍️ from claims, logo optional'], cs: ['📷 přidejte fotku produktu', '✍️ ze sloganů, logo volitelně'] }[_lng] || [];
1978      const opisWideo = jestWideo
1979        ? `${s.opis && s.opis[_lng] ? `<div style="font-size:10.5px;line-height:1.35;color:rgba(255,255,255,0.82);margin:2px 0 4px;text-transform:none;letter-spacing:0">${s.opis[_lng]}</div>` : ''}<div class="tile-meta" style="text-transform:none;letter-spacing:0.02em">${(s.videoRecipe && s.videoRecipe.bezProduktu) ? (_chip[1] || '') : (_chip[0] || '')}</div>`
1980        : '';
1981      return `
1982      <button class="tile${klasaAspektu} ${S.currentStudio?._id===s._id?'on':''}" onclick="pickStudio('${s._id}')" style="padding:0;font-family:inherit">
1983        ${medium}
1984        <div class="tile-overlay"></div>
1985        ${Array.isArray(s.modes) && s.modes.includes('video') ? '<span class="tile-badge-video"><i class="fas fa-play"></i>WIDEO</span>' : ''}
1986        ${heartBtn}
1987        <div class="tile-info">
1988          <div class="tile-name">${s.name}</div>
1989          ${jestWideo ? opisWideo : `<div class="tile-meta">${s.category} · ${s.defaultAspectRatio || '1:1'}</div>`}
1990        </div>
1991        <span class="tile-use-hint"><i class="fas fa-wand-magic-sparkles"></i> Użyj szablonu</span>
1992        <div class="tile-check"><i class="fas fa-check"></i></div>
1993      </button>
1994    `;
1995    }).join('');
1996    uzbrojLazyWideo(el);
1997  }
1998  window.cloneStudio = async function(id, btn) {
1999    if (btn?.classList.contains('saved')) { toast('Masz już w Moich'); return; }
2000    try {
2001      const r = await api('/api/brand/studios/' + id + '/clone', { method: 'POST' });
2002      if (btn) {
2003        btn.classList.add('saved', 'pop');
2004        btn.querySelector('i').className = 'fas fa-heart';
2005        btn.setAttribute('title', 'Masz już w Moich');
2006        setTimeout(() => btn.classList.remove('pop'), 500);
2007      }
2008      const slug = r.studio?.slug;
2009      if (slug) S.ownedSlugs?.add(slug);
2010      toast(r.wasExisting ? 'Masz już w Moich' : 'Dodano do Moich ♥');
2011    } catch (e) {
2012      toast('Nie udało się dodać');
2013      console.warn('clone', e);
2014    }
2015  };
2016  window.pickStudio = function(id) {
2017    const s = S.studios.find(x => x._id === id);
2018    if (!s) return;
2019    S.currentStudio = s;
2020    // Mozaika miesza obrazy i filmy — kafel SAM przestawia tryb docka pod swój typ,
2021    // user nie musi wiedzieć, że tryby istnieją (mkSetTryb nie przeładowuje galerii).
2022    try { window.mkSetTryb && window.mkSetTryb(Array.isArray(s.modes) && s.modes.includes('video') ? 'video' : 'image', s); } catch(_) {}
2023    // Toggle class na pojedynczych tile'ach zamiast pełnego re-render — żeby nie migały images.
2024    document.querySelectorAll('#studios .tile').forEach(t => t.classList.remove('on'));
2025    const target = document.querySelector(`#studios .tile[onclick*="${id}"]`);
2026    if (target) target.classList.add('on');
2027    $('studio-pill').style.display = 'inline-flex';
2028    $('studio-pill-name').textContent = s.name;
2029    if (s.defaultAspectRatio) {
2030      const i = S.ratios.indexOf(s.defaultAspectRatio);
2031      if (i >= 0) { S.ratioIdx = i; $('ratio-label').textContent = S.ratios[i]; }
2032    }
2033    updateCost();
2034    updateDockHints();
2035    $('p-input').focus();
2036  };
2037
2038  // Pokaż hint i glow przy "Model" button jeśli preset wymaga character.
2039  // Ukryj "Model" gdy preset jest product-only (kind='product') — user mial: "w chuj confusing jest".
2040  function updateDockHints() {
2041    const studio = S.currentStudio;
2042    const hintEl = $('dock-hint');
2043    const modelBtn = $('add-model-btn');
2044    const productBtn = document.querySelector('.pill.add-btn[onclick*="product"]');
2045
2046    // Widoczność przycisków wg presetu
2047    if (!studio) {
2048      // Bez presetu — oba widoczne
2049      if (modelBtn) modelBtn.style.display = '';
2050      if (productBtn) productBtn.style.display = '';
2051    } else {
2052      const needsModel = studio.kind === 'model' || /\{character\}/.test(studio.promptTemplate || '');
2053      const needsProduct = studio.kind !== 'model' || /\{product\}/.test(studio.promptTemplate || '');  // model-preset może też wymagać produktu
2054      if (modelBtn) modelBtn.style.display = needsModel ? '' : 'none';
2055      if (productBtn) productBtn.style.display = needsProduct ? '' : 'none';
2056    }
2057
2058    // Tryb Wideo (dock 2.0): hint prowadzi przez rurę i nie znika po wyborze presetu —
2059    // bez tego user widział przycisk bez wyjaśnienia, co się stanie po kliku.
2060    if (window._mkTryb === 'video') {
2061      const wt = (k, z) => (typeof window.t === 'function' && window.t(k)) || z;
2062      hintEl.style.display = 'block';
2063      $('dock-hint-text').textContent = (studio && studio.videoRecipe)
2064        ? wt('ecommerce.wideo.hintReady', 'Szablon wideo: najpierw klatki (5 x 20 kr), potem animacja (25 kr). Kliknij Generuj.')
2065        : wt('ecommerce.wideo.pickTemplate', 'Wybierz szablon wideo z galerii.');
2066      modelBtn?.classList.remove('glow');
2067      return;
2068    }
2069    // Hint + glow (bez zmian w logice)
2070    if (!studio) {
2071      hintEl.style.display = 'none';
2072      modelBtn?.classList.remove('glow');
2073      return;
2074    }
2075    const needsModel = studio.kind === 'model' || /\{character\}/.test(studio.promptTemplate || '');
2076    const hasCharacterRef = S.activeRefs.some(r => r.type === 'character');
2077    const hasProductRef = S.activeRefs.some(r => r.type === 'product');
2078    const needsProduct = /\{product\}/.test(studio.promptTemplate || '');
2079    if (needsModel && !hasCharacterRef) {
2080      hintEl.style.display = 'block';
2081      $('dock-hint-text').textContent = `Preset "${studio.name}" potrzebuje modela — kliknij przycisk "Model" żeby wybrać.`;
2082      modelBtn?.classList.add('glow');
2083    } else if (needsProduct && !hasProductRef) {
2084      hintEl.style.display = 'block';
2085      $('dock-hint-text').textContent = `Preset "${studio.name}" potrzebuje produktu — kliknij "Produkt" żeby wybrać.`;
2086      modelBtn?.classList.remove('glow');
2087    } else {
2088      hintEl.style.display = 'none';
2089      modelBtn?.classList.remove('glow');
2090    }
2091  }
2092  window.clearStudio = function() {
2093    S.currentStudio = null;
2094    $('studio-pill').style.display = 'none';
2095    renderStudios();
2096    updateCost();
2097    updateDockHints();
2098  };
2099
2100  // ═══ ASSET PICKER (drawer z gridem modelów/produktów) ═══
2101  S.picker = { type: 'product', tab: 'mine', assets: [] };
2102
2103  window.openAssetPicker = async function(type) {
2104    S.picker.type = type;
2105    S.picker.tab = 'mine';
2106    const _en = location.hostname.indexOf('pixcloud') !== -1;
2107    $('picker-title').textContent = type === 'character' ? (_en ? 'Select model' : 'Wybierz model') : (_en ? 'Select product' : 'Wybierz produkt');
2108    $('picker-sub').textContent = type === 'character'
2109      ? (_en ? 'From your Library or Pixcloud influencers (ready)' : 'Z Twojej Biblioteki albo Chmurkowi influ (gotowi)')
2110      : (_en ? 'From your Library (products you uploaded)' : 'Z Twojej Biblioteki (produkty które wgrałeś)');
2111    // Chmurka tab tylko dla character — i NIE na pixcloud (ukryte na pixcloud)
2112    $('picker-tab-chmurka').style.display = (type === 'character' && !_en) ? '' : 'none';
2113    $('picker-upload-btn').onclick = () => { closePicker(); openModal(type); };
2114    $('picker-upload-btn').textContent = ''; // reset
2115    $('picker-upload-btn').innerHTML = `<i class="fas fa-plus"></i> ${type === 'character' ? (_en ? 'Add influencer' : 'Dodaj model') : (_en ? 'Add product' : 'Dodaj produkt')}`;
2116    document.querySelectorAll('.picker-tab').forEach(t => t.classList.toggle('on', t.dataset.pickerTab === 'mine'));
2117    $('picker-backdrop').classList.add('show');
2118    await loadPickerAssets();
2119  };
2120
2121  window.closePicker = () => $('picker-backdrop').classList.remove('show');
2122
2123  document.querySelectorAll('.picker-tab').forEach(t => {
2124    t.onclick = () => {
2125      S.picker.tab = t.dataset.pickerTab;
2126      document.querySelectorAll('.picker-tab').forEach(x => x.classList.toggle('on', x === t));
2127      renderPicker();
2128    };
2129  });
2130
2131  async function loadPickerAssets() {
2132    try {
2133      const all = await api('/api/brand/assets?type=' + S.picker.type);
2134      S.picker.assets = all;
2135      renderPicker();
2136    } catch(e) { console.warn('picker load', e); }
2137  }
2138
2139  function renderPicker() {
2140    const grid = $('picker-grid');
2141    const { tab, type, assets } = S.picker;
2142    const filtered = tab === 'chmurka'
2143      ? assets.filter(a => a.isPublic)
2144      : assets.filter(a => !a.isPublic);
2145    if (!filtered.length) {
2146      grid.innerHTML = `<div class="p-empty" style="grid-column:1/-1">
2147        <i class="fas fa-${type==='character'?'user':'box'}"></i>
2148        <h4>Brak ${type==='character'?'modeli':'produktów'}${tab==='chmurka'?' Chmurkowi':''}</h4>
2149        <p>${tab==='mine'
2150          ? 'Dodaj swoje zdjęcia przyciskiem "Dodaj ' + (type==='character'?'model':'produkt') + '" powyżej.'
2151          : 'Wybierz zakładkę Moje żeby zobaczyć swoje.'}</p>
2152      </div>`;
2153      return;
2154    }
2155    grid.innerHTML = filtered.map(a => `
2156      <div class="p-tile" onclick="pickAsset('${a._id}')">
2157        <img src="${a.imageUrl}" alt="${a.name}" loading="lazy">
2158        ${a.isPublic ? '<div class="p-tile-badge"><i class="fas fa-sparkles"></i> CHMURKA</div>' : ''}
2159        <div class="p-tile-info">
2160          <div class="p-tile-name">${a.name}</div>
2161          ${a.description ? `<div class="p-tile-desc">${a.description}</div>` : ''}
2162        </div>
2163      </div>
2164    `).join('');
2165  }
2166
2167  window.pickAsset = function(id) {
2168    const a = S.picker.assets.find(x => x._id === id);
2169    if (!a) return;
2170    // Add mention to input + active refs
2171    if (!S.activeRefs.find(r => r.assetId === a._id)) {
2172      S.activeRefs.push({ name: a.name, assetId: a._id, imageUrl: a.imageUrl, type: a.type });
2173      renderRefs();
2174      // Add @name do prompta (na końcu, ze spacją)
2175      const inp = $('p-input');
2176      const existing = inp.value.trim();
2177      inp.value = (existing ? existing + ' ' : '') + '@' + a.name + ' ';
2178      autoGrow(inp);
2179    }
2180    closePicker();
2181    updateCost();
2182    updateDockHints();
2183    $('p-input').focus();
2184    toast('Dodano @' + a.name);
2185  };
2186  document.querySelectorAll('.cat').forEach(c => {
2187    if (c.classList.contains('cta-request')) return;  // skip CTA "Nie ma kategorii?" — ma własny onclick
2188    c.onclick = () => {
2189      document.querySelectorAll('.cat').forEach(x => { if (!x.classList.contains('cta-request')) x.classList.remove('on'); });
2190      c.classList.add('on'); S.cat = c.dataset.cat; loadStudios();
2191    };
2192  });
2193  document.querySelectorAll('.seg').forEach(s => s.onclick = () => {
2194    document.querySelectorAll('.seg').forEach(x => x.classList.remove('on'));
2195    s.classList.add('on'); S.seg = s.dataset.seg; loadStudios();
2196  });
2197
2198  // ═══ RATIO ═══
2199  window.cycleRatio = function() {
2200    S.ratioIdx = (S.ratioIdx + 1) % S.ratios.length;
2201    $('ratio-label').textContent = S.ratios[S.ratioIdx];
2202  };
2203  window.togglePosePreserve = function() {
2204    S.preservePose = !S.preservePose;
2205    const btn = $('pose-toggle');
2206    const ico = $('pose-icon');
2207    if (S.preservePose) {
2208      // ON: wypełnione, jasny accent
2209      ico.className = 'fas fa-square-check';
2210      btn.style.background = 'rgba(166,241,248,0.22)';
2211      btn.style.borderColor = 'rgba(166,241,248,0.6)';
2212      btn.style.color = 'var(--accent)';
2213    } else {
2214      // OFF: pusty kwadrat, neutralny
2215      ico.className = 'fas fa-square';
2216      btn.style.background = '';
2217      btn.style.borderColor = '';
2218      btn.style.color = '';
2219    }
2220  };
2221  window.toggleBulkMode = function() {
2222    S.bulkMode = !S.bulkMode;
2223    _renderBulkPill();
2224    updateCost();
2225  };
2226  function _renderBulkPill() {
2227    const btn = $('bulk-toggle');
2228    const ico = $('bulk-icon');
2229    const lbl = $('bulk-label');
2230    if (!btn) return;
2231    const n = S.activeRefs.filter(r => r.type === 'product').length;
2232    if (S.bulkMode) {
2233      ico.className = 'fas fa-square-check';
2234      btn.style.background = 'rgba(244,114,182,0.22)';
2235      btn.style.borderColor = 'rgba(244,114,182,0.6)';
2236      btn.style.color = '#f472b6';
2237      lbl.textContent = 'Bulk (' + n + ' osobno)';
2238    } else {
2239      ico.className = 'fas fa-square';
2240      btn.style.background = '';
2241      btn.style.borderColor = '';
2242      btn.style.color = '';
2243      lbl.textContent = 'Bulk (każdy osobno)';
2244    }
2245  }
2246  function updateBulkVisibility() {
2247    // Pokaż toggle bulk gdy w docku są ≥2 produkty
2248    const productCount = S.activeRefs.filter(r => r.type === 'product').length;
2249    const btn = $('bulk-toggle');
2250    if (!btn) return;
2251    if (productCount >= 2) {
2252      btn.style.display = '';
2253      _renderBulkPill();
2254    } else {
2255      btn.style.display = 'none';
2256      if (S.bulkMode) { S.bulkMode = false; _renderBulkPill(); }  // off auto gdy <2 produkty
2257    }
2258  }
2259  function updateCost() {
2260    const m = S.currentStudio?.defaultModel;
2261    const perGen = (m === 'seedream-4' ? 15 : 10);
2262    if (S.bulkMode) {
2263      const n = S.activeRefs.filter(r => r.type === 'product').length;
2264      $('cost').textContent = (perGen * n) + ' kr (' + n + '× po ' + perGen + ')';
2265    } else {
2266      $('cost').textContent = perGen + ' kr';
2267    }
2268  }
2269
2270  // ═══ ASSETS ═══
2271  async function loadAssets() {
2272    try {
2273      const [p, c] = await Promise.all([api('/api/brand/assets?type=product'), api('/api/brand/assets?type=character')]);
2274      S.assets = S.lib === 'product' ? p : c;
2275      $('cnt-products').textContent = p.length;
2276      $('cnt-characters').textContent = c.length;
2277      renderAssets();
2278    } catch(e) { console.warn('assets', e); }
2279  }
2280  function renderAssets() {
2281    const el = $('assets');
2282    if (!S.assets.length) {
2283      el.innerHTML = `<div class="empty" style="grid-column:1/-1;padding:60px 20px"><i class="fas fa-${S.lib==='product'?'box':'user'}"></i><h4>Brak ${S.lib==='product'?'produktów':'modeli'}</h4><p>Wgraj zdjęcia aby oznaczać przez @nazwa.</p></div>`;
2284      return;
2285    }
2286    el.innerHTML = S.assets.map(a => {
2287      const isPublic = !!a.isPublic;
2288      const badge = isPublic ? '<div class="asset-badge"><i class="fas fa-sparkles"></i> Chmurka</div>' : '';
2289      const delBtn = isPublic ? '' : `<button class="asset-del" onclick="delAsset('${a._id}', event)"><i class="fas fa-trash"></i></button>`;
2290      const payload = JSON.stringify({ kind: 'asset', assetId: a._id, name: a.name, imageUrl: a.imageUrl, type: a.type });
2291      return `
2292        <div class="asset ${isPublic?'public':''}" draggable="true" ondragstart="onAssetDragStart(event, ${JSON.stringify(payload).replace(/"/g, '&quot;')})">
2293          <img src="${a.imageUrl}" alt="${a.name}" loading="lazy" draggable="false">
2294          ${badge}
2295          ${delBtn}
2296          <div class="asset-info"><div class="asset-name">${a.name}</div>${a.description?`<div class="asset-desc">${a.description}</div>`:''}</div>
2297        </div>`;
2298    }).join('');
2299  }
2300  document.querySelectorAll('.lib-tab').forEach(t => t.onclick = () => setLib(t.dataset.lib));
2301  window.setLib = function(tab) {
2302    S.lib = tab;
2303    document.querySelectorAll('.lib-tab').forEach(x => x.classList.toggle('on', x.dataset.lib === tab));
2304    const isBk = tab === 'brandkit';
2305    $('assets').style.display = isBk ? 'none' : 'grid';
2306    $('brandkit-view').style.display = isBk ? 'block' : 'none';
2307    $('btn-add-asset').style.display = isBk ? 'none' : 'inline-flex';
2308    if (isBk) {
2309      const btnInflu = $('btn-create-influ'); if (btnInflu) btnInflu.style.display = 'none';
2310      loadBrandKit();
2311      return;
2312    }
2313    $('btn-add-label').textContent = tab === 'product' ? 'Dodaj produkt' : 'Dodaj modela';
2314    const btnInflu = $('btn-create-influ');
2315    if (btnInflu) btnInflu.style.display = tab === 'character' ? 'inline-flex' : 'none';
2316    loadAssets();
2317  };
2318
2319  // ═══ BRAND KIT ("Mój Styl") ═══
2320  S.bk = { slots: [null, null, null, null, null], fingerprint: null, saved: null };
2321
2322  async function loadBrandKit() {
2323    try {
2324      const kit = await api('/api/brand/kit');
2325      S.bk.saved = kit;
2326      renderBrandKit();
2327    } catch(e) { console.warn('brand kit load', e); renderBrandKit(); }
2328  }
2329
2330  function renderBrandKit() {
2331    const el = $('brandkit-view');
2332    const saved = S.bk.saved;
2333    if (saved?.universalSummary && !S.bk.editing) {
2334      // User has kit — show preview
2335      const palette = (saved.palette || []).map(h => `<div class="chip" data-hex="${h}" style="background:${h}"></div>`).join('');
2336      el.innerHTML = `
2337        <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;gap:16px;flex-wrap:wrap">
2338          <div>
2339            <h3 style="font-size:26px;font-weight:600;letter-spacing:-0.02em">Twój Brand Kit</h3>
2340            <p style="color:var(--muted);font-size:13px;margin-top:4px;letter-spacing:-0.01em">Styl doklejany automatycznie do każdej generacji · ${saved.sourceImageUrls?.length || 0} zdjęć · ${new Date(saved.analyzedAt).toLocaleDateString('pl')}</p>
2341          </div>
2342          <div style="display:flex;gap:8px">
2343            <button class="btn" onclick="bkRestart()" style="padding:10px 16px;border-radius:12px;background:var(--card);border:1px solid var(--border);color:var(--text);cursor:pointer;font-size:12.5px;font-weight:600;font-family:inherit"><i class="fas fa-rotate" style="margin-right:6px"></i> Przerób na nowo</button>
2344            <button class="btn" onclick="bkDelete()" style="padding:10px 16px;border-radius:12px;background:var(--card);border:1px solid var(--border);color:var(--danger);cursor:pointer;font-size:12.5px;font-weight:600;font-family:inherit"><i class="fas fa-trash" style="margin-right:6px"></i> Usuń</button>
2345          </div>
2346        </div>
2347        <div class="bk-fingerprint">
2348          ${saved.mood ? `<span class="bk-mood">${saved.mood}</span>` : ''}
2349          ${palette ? `<div class="bk-palette" style="margin-bottom:30px">${palette}</div>` : ''}
2350          <div class="bk-text-label">Uniwersalny styl (dokleja się zawsze)</div>
2351          <div class="bk-text">${saved.universalSummary}</div>
2352          ${saved.contextualSummary ? `<div class="bk-text-label" style="margin-top:14px">Preferowany kontekst (dokleja się gdy brak Studio presetu)</div><div class="bk-text">${saved.contextualSummary}</div>` : ''}
2353          ${saved.sourceImageUrls?.length ? `<div class="bk-text-label" style="margin-top:14px">Wzory referencyjne</div><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px">${saved.sourceImageUrls.map(u => `<img src="${u}" style="width:100%;aspect-ratio:1/1;object-fit:cover;border-radius:12px" referrerpolicy="no-referrer">`).join('')}</div>` : ''}
2354        </div>`;
2355      return;
2356    }
2357    // Empty or creating — upload flow
2358    const slots = S.bk.slots.map((s, i) => `
2359      <div class="bk-slot" onclick="document.getElementById('bk-file-${i}').click()" data-slot="${i}">
2360        ${s ? `<img src="${s.url}" referrerpolicy="no-referrer"><button class="bk-rm" onclick="event.stopPropagation();bkRemoveSlot(${i})"><i class="fas fa-xmark"></i></button>` : `<i class="fas fa-plus" style="font-size:16px;opacity:0.5"></i>`}
2361        <input type="file" id="bk-file-${i}" accept="image/*" style="display:none" onchange="bkOnFile(${i},this)">
2362      </div>`).join('');
2363    const filledCount = S.bk.slots.filter(Boolean).length;
2364    el.innerHTML = `
2365      <div class="bk-empty">
2366        <i class="fas fa-wand-magic-sparkles"></i>
2367        <h3>Naucz AI swojego stylu</h3>
2368        <p>Wgraj 2-5 swoich zdjęć — AI wyciągnie z nich <b>DNA stylu</b> (paleta, mood, oświetlenie, ziarno) i automatycznie dodaje ten sam vibe do każdej przyszłej generacji. Twoje posty będą wyglądać <b>jak z jednej sesji</b>.</p>
2369      </div>
2370      <div class="bk-drop" id="bk-drop">${slots}</div>
2371      <div style="display:flex;justify-content:flex-end;gap:10px">
2372        <button class="btn" onclick="bkAnalyze()" ${filledCount < 2 ? 'disabled' : ''} style="padding:12px 22px;border-radius:14px;background:${filledCount<2?'var(--card)':'var(--grad)'};color:${filledCount<2?'var(--muted)':'#07070b'};border:none;cursor:${filledCount<2?'not-allowed':'pointer'};font-size:13.5px;font-weight:700;font-family:inherit;letter-spacing:-0.01em;transition:all 0.25s var(--es)"><i class="fas fa-sparkles" style="margin-right:6px"></i> ${filledCount<2?'Wgraj min. 2 zdjęcia':'Analizuj styl ('+filledCount+')'}</button>
2373      </div>
2374      ${S.bk.fingerprint ? `
2375        <div class="bk-fingerprint">
2376          <h4>Podgląd fingerprintu</h4>
2377          ${S.bk.fingerprint.mood ? `<span class="bk-mood">${S.bk.fingerprint.mood}</span>` : ''}
2378          ${S.bk.fingerprint.palette?.length ? `<div class="bk-palette" style="margin-bottom:30px">${S.bk.fingerprint.palette.map(h => `<div class="chip" data-hex="${h}" style="background:${h}"></div>`).join('')}</div>` : ''}
2379          <div class="bk-text-label">Uniwersalny styl</div>
2380          <div class="bk-text">${S.bk.fingerprint.universalSummary}</div>
2381          ${S.bk.fingerprint.contextualSummary ? `<div class="bk-text-label" style="margin-top:14px">Preferowany kontekst</div><div class="bk-text">${S.bk.fingerprint.contextualSummary}</div>` : ''}
2382          <div style="display:flex;gap:10px;margin-top:18px;justify-content:flex-end">
2383            <button class="btn" onclick="S.bk.fingerprint=null;renderBrandKit()" style="padding:11px 18px;border-radius:12px;background:var(--card);border:1px solid var(--border);color:var(--text);cursor:pointer;font-size:12.5px;font-weight:600;font-family:inherit">Anuluj</button>
2384            <button class="btn" onclick="bkSave()" style="padding:11px 18px;border-radius:12px;background:var(--grad);color:#07070b;border:none;cursor:pointer;font-size:12.5px;font-weight:700;font-family:inherit"><i class="fas fa-check" style="margin-right:6px"></i> Zapisz mój styl</button>
2385          </div>
2386        </div>
2387      ` : ''}`;
2388  }
2389
2390  window.bkOnFile = async function(idx, inputEl) {
2391    const f = inputEl.files?.[0];
2392    if (!f) return;
2393    if (f.size > 10 * 1024 * 1024) return toast('Plik > 10MB');
2394    // Upload do R2 przez /upload-temp
2395    const fd = new FormData(); fd.append('file', f);
2396    try {
2397      const up = await fetch(API + '/upload-temp', { method: 'POST', headers: authH(false), credentials: 'include', body: fd });
2398      const d = await up.json();
2399      if (!d.url) throw new Error('upload fail');
2400      S.bk.slots[idx] = { url: d.url, file: f };
2401      renderBrandKit();
2402    } catch(e) { toast('Błąd wgrywania'); }
2403  };
2404
2405  window.bkRemoveSlot = function(idx) { S.bk.slots[idx] = null; renderBrandKit(); };
2406
2407  window.bkAnalyze = async function() {
2408    const urls = S.bk.slots.filter(Boolean).map(s => s.url);
2409    if (urls.length < 2) return toast('Wgraj min. 2 zdjęcia');
2410    toast('AI analizuje styl... (15-30s)');
2411    try {
2412      const r = await api('/api/brand/kit/analyze', { method: 'POST', body: JSON.stringify({ imageUrls: urls }) });
2413      S.bk.fingerprint = r.fingerprint;
2414      renderBrandKit();
2415      toast('Gotowe — sprawdź i zapisz');
2416    } catch(e) { toast('Błąd: ' + e.message); }
2417  };
2418
2419  window.bkSave = async function() {
2420    if (!S.bk.fingerprint) return;
2421    try {
2422      const r = await api('/api/brand/kit', { method: 'POST', body: JSON.stringify(S.bk.fingerprint) });
2423      S.bk.saved = r.brandKit;
2424      S.bk.fingerprint = null;
2425      S.bk.slots = [null, null, null, null, null];
2426      S.bk.editing = false;
2427      renderBrandKit();
2428      toast('Zapisano — Twój styl aktywny');
2429    } catch(e) { toast('Błąd: ' + e.message); }
2430  };
2431
2432  window.bkRestart = function() {
2433    if (!confirm('Przerobić od nowa? Stary fingerprint zostanie zastąpiony.')) return;
2434    S.bk.slots = [null, null, null, null, null];
2435    S.bk.fingerprint = null;
2436    S.bk.editing = true;
2437    renderBrandKit();
2438  };
2439
2440  // ═══ CATEGORY REQUEST — user może poprosić o nową kategorię szablonów ═══
2441  window.openCategoryRequestModal = function() {
2442    const userEmail = (window.currentUser && window.currentUser.email) || '';
2443    $('cr-email').value = userEmail;
2444    $('cr-message').value = '';
2445    $('cat-request-modal').classList.add('show');
2446    setTimeout(() => $('cr-message').focus(), 100);
2447  };
2448  window.closeCategoryRequestModal = function() {
2449    $('cat-request-modal').classList.remove('show');
2450  };
2451  window.submitCategoryRequest = async function() {
2452    const email = $('cr-email').value.trim();
2453    const message = $('cr-message').value.trim();
2454    if (!email || !email.includes('@')) return toast('Podaj poprawny email');
2455    if (!message || message.length < 10) return toast('Opisz proszę swoją kategorię (min. 10 znaków)');
2456    const btn = $('cr-submit-btn');
2457    btn.disabled = true;
2458    btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Wysyłanie...';
2459    try {
2460      const r = await fetch(`${API}/api/category-request`, {
2461        method: 'POST',
2462        headers: { 'Content-Type': 'application/json', 'Authorization': S.token ? `Bearer ${S.token}` : '' },
2463        credentials: 'include',
2464        body: JSON.stringify({ email, message })
2465      });
2466      if (!r.ok) {
2467        const e = await r.json().catch(() => ({}));
2468        throw new Error(e.error || 'Błąd serwera');
2469      }
2470      closeCategoryRequestModal();
2471      toast('Dzięki! Dodamy Twoją kategorię w ciągu 24h ✨', 4000);
2472    } catch(e) {
2473      toast('Błąd: ' + e.message, 4000);
2474    } finally {
2475      btn.disabled = false;
2476      btn.innerHTML = 'Wyślij propozycję';
2477    }
2478  };
2479
2480  // ═══ INFLU CHAT — rozmowa z AI żeby stworzyć własnego influ ═══
2481  S.influ = { messages: [], generating: false, finalPrompt: null, suggestedName: null };
2482
2483  window.openInfluChat = function() {
2484    S.influ = { messages: [], generating: false, finalPrompt: null, suggestedName: null };
2485    $('ic-body').innerHTML = '';
2486    $('ic-input').value = '';
2487    $('influ-modal').classList.add('show');
2488    // Inicjalna wiadomość AI — zachęcamy do pełnego opisu w 1 wiadomości (mniej turns)
2489    const greet = 'Cześć! Opisz swojego influencera w 1-2 zdaniach. Np. "kobieta 25 lat, długie blond włosy, niebieskie oczy, casualowa w białej koszulce". Im więcej szczegółów podasz, tym mniej będę dopytywał.';
2490    icAddMsg('ai', greet);
2491    S.influ.messages.push({ role: 'assistant', content: greet });
2492    setTimeout(() => $('ic-input').focus(), 200);
2493  };
2494
2495  window.closeInfluChat = function() {
2496    if (S.influ.generating) return toast('Zaczekaj aż skończy się generacja');
2497    $('influ-modal').classList.remove('show');
2498  };
2499
2500  function icAddMsg(role, text) {
2501    const div = document.createElement('div');
2502    div.className = 'ic-msg ' + role;
2503    div.textContent = text;
2504    $('ic-body').appendChild(div);
2505    $('ic-body').scrollTop = $('ic-body').scrollHeight;
2506  }
2507
2508  function icAddTyping() {
2509    const div = document.createElement('div');
2510    div.className = 'ic-typing';
2511    div.id = 'ic-typing-bubble';
2512    div.innerHTML = '<span></span><span></span><span></span>';
2513    $('ic-body').appendChild(div);
2514    $('ic-body').scrollTop = $('ic-body').scrollHeight;
2515  }
2516  function icRemoveTyping() { $('ic-typing-bubble')?.remove(); }
2517
2518  window.icSendMsg = async function() {
2519    const input = $('ic-input');
2520    const txt = input.value.trim();
2521    if (!txt) return;
2522    if (S.influ.generating) return;
2523    input.value = '';
2524    icAddMsg('user', txt);
2525    S.influ.messages.push({ role: 'user', content: txt });
2526    $('ic-send-btn').disabled = true;
2527    icAddTyping();
2528    try {
2529      const r = await api('/api/brand/influencer/chat', { method: 'POST', body: JSON.stringify({ messages: S.influ.messages }) });
2530      icRemoveTyping();
2531      if (r.done && r.finalPrompt) {
2532        // AI decyzja: mamy dość info. Pokaż message, rozpocznij generację.
2533        icAddMsg('ai', r.message || 'Generuję...');
2534        S.influ.messages.push({ role: 'assistant', content: r.message || '' });
2535        S.influ.finalPrompt = r.finalPrompt;
2536        S.influ.suggestedName = r.suggestedName || 'Model';
2537        await icStartGenerate();
2538      } else {
2539        icAddMsg('ai', r.message);
2540        S.influ.messages.push({ role: 'assistant', content: r.message });
2541        $('ic-send-btn').disabled = false;
2542        input.focus();
2543      }
2544    } catch (e) {
2545      icRemoveTyping();
2546      icAddMsg('system', 'Błąd: ' + e.message);
2547      $('ic-send-btn').disabled = false;
2548    }
2549  };
2550
2551  async function icStartGenerate() {
2552    S.influ.generating = true;
2553    $('ic-input-wrap').style.display = 'none';
2554    // System message with spinner
2555    const div = document.createElement('div');
2556    div.id = 'ic-generating-box';
2557    div.className = 'ic-generating';
2558    div.innerHTML = `<div class="spin"></div><div style="font-size:13px;color:var(--muted);letter-spacing:-0.01em">Generuję <b>@${S.influ.suggestedName}</b>... (20-40s)</div>`;
2559    $('ic-body').appendChild(div);
2560    $('ic-body').scrollTop = $('ic-body').scrollHeight;
2561    try {
2562      const r = await api('/api/brand/influencer/generate', { method: 'POST', body: JSON.stringify({
2563        prompt: S.influ.finalPrompt,
2564        name: S.influ.suggestedName
2565      })});
2566      $('ic-generating-box')?.remove();
2567      const doneDiv = document.createElement('div');
2568      doneDiv.className = 'ic-done';
2569      const promptText = r.asset?.description || S.influ.finalPrompt || '';
2570      doneDiv.innerHTML = `
2571        <img src="${r.asset.imageUrl}" alt="${r.asset.name}" referrerpolicy="no-referrer">
2572        <div style="text-align:center;margin-bottom:14px">
2573          <div style="font-size:17px;font-weight:600;letter-spacing:-0.02em;margin-bottom:3px">@${r.asset.name} gotowy!</div>
2574          <div style="font-size:12.5px;color:var(--muted);letter-spacing:-0.01em">Użyj go w promptcie przez <code style="background:var(--card-hi);padding:2px 7px;border-radius:6px">@${r.asset.name}</code></div>
2575        </div>
2576        <details style="margin-bottom:14px;background:var(--bg2);border:1px solid var(--border);border-radius:12px;padding:10px 14px">
2577          <summary style="cursor:pointer;font-size:11.5px;color:var(--muted2);text-transform:uppercase;letter-spacing:0.12em;font-weight:600">Pokaż prompt (co AI zinterpretowało z rozmowy)</summary>
2578          <div style="margin-top:10px;font-size:12px;color:var(--muted);line-height:1.55;letter-spacing:-0.01em;white-space:pre-wrap;font-family:ui-monospace,'SF Mono',monospace">${promptText.replace(/</g,'&lt;')}</div>
2579        </details>
2580        <button class="btn-add" style="width:100%;justify-content:center;background:var(--grad);color:#07070b" onclick="closeInfluChat();setLib('character');loadAssets()">Świetnie, zamknij</button>`;
2581      $('ic-body').appendChild(doneDiv);
2582      $('ic-body').scrollTop = $('ic-body').scrollHeight;
2583    } catch (e) {
2584      $('ic-generating-box')?.remove();
2585      icAddMsg('system', 'Błąd generacji: ' + e.message + ' (kredyty zwrócone)');
2586      $('ic-input-wrap').style.display = 'flex';
2587      $('ic-send-btn').disabled = false;
2588    }
2589    S.influ.generating = false;
2590  }
2591
2592  window.bkDelete = async function() {
2593    if (!confirm('Usunąć Brand Kit? Kolejne generacje nie będą już miały Twojego stylu.')) return;
2594    try {
2595      await api('/api/brand/kit', { method: 'DELETE' });
2596      S.bk.saved = null;
2597      S.bk.editing = false;
2598      renderBrandKit();
2599      toast('Usunięto');
2600    } catch(e) { toast('Błąd: ' + e.message); }
2601  };
2602  $('btn-add-asset').onclick = () => openModal(S.lib);
2603  window.delAsset = async function(id, ev) {
2604    ev.stopPropagation();
2605    if (!confirm('Usunąć?')) return;
2606    try { await api('/api/brand/assets/'+id, { method: 'DELETE' }); toast('Usunięto'); loadAssets(); }
2607    catch(e){ toast('Błąd: '+e.message); }
2608  };
2609
2610  // ═══ MODAL ═══
2611  window.openModal = function(type) {
2612    S.modalType = type; S.modalFile = null;
2613    const _en = location.hostname.indexOf('pixcloud') !== -1;
2614    $('m-title').textContent = type === 'product' ? (_en ? 'Add product' : 'Dodaj produkt') : (_en ? 'Add influencer' : 'Dodaj modela');
2615    $('m-sub').innerHTML = type === 'product'
2616      ? (_en ? 'Upload a product photo and give it a name (e.g. <code style="background:var(--card);padding:2px 7px;border-radius:6px">Shirt_red</code>).' : 'Wgraj zdjęcie produktu i nadaj nazwę (np. <code style="background:var(--card);padding:2px 7px;border-radius:6px">Koszulka_red</code>).')
2617      : (_en ? 'Upload a model/influencer photo and give it a name (e.g. <code style="background:var(--card);padding:2px 7px;border-radius:6px">Kate</code>).' : 'Wgraj zdjęcie modela/influencerki i nadaj nazwę (np. <code style="background:var(--card);padding:2px 7px;border-radius:6px">Kasia</code>).');
2618    $('m-name').value = ''; $('m-desc').value = ''; $('m-preview').style.display = 'none';
2619    $('m-drop').querySelector('i').style.display = '';
2620    $('m-drop').querySelector('.drop-txt').style.display = '';
2621    $('modal').classList.add('show');
2622    setTimeout(() => $('m-name').focus(), 300);
2623  };
2624  window.closeModal = () => $('modal').classList.remove('show');
2625  window.onModalFile = function(inp) {
2626    const f = inp.files[0];
2627    if (!f) return;
2628    if (f.size > 10 * 1024 * 1024) return toast('Plik > 10MB');
2629    S.modalFile = f;
2630    const p = $('m-preview');
2631    p.src = URL.createObjectURL(f); p.style.display = 'block';
2632    $('m-drop').querySelector('i').style.display = 'none';
2633    $('m-drop').querySelector('.drop-txt').style.display = 'none';
2634  };
2635  window.saveAsset = async function() {
2636    let name = $('m-name').value.trim();
2637    const desc = $('m-desc').value.trim();
2638    // Auto-generate name jeśli puste — typ + krótki timestamp (sortowalne, unikalne)
2639    if (!name) {
2640      const ts = Date.now().toString(36).slice(-5).toUpperCase();
2641      name = (S.modalType === 'product' ? 'Produkt_' : 'Model_') + ts;
2642    }
2643    if (!S.modalFile) return toast('Wybierz plik');
2644    const btn = $('btn-save'); btn.disabled = true; btn.textContent = 'Wysyłam...';
2645    try {
2646      const fd = new FormData();
2647      fd.append('file', S.modalFile); fd.append('type', S.modalType); fd.append('name', name);
2648      if (desc) fd.append('description', desc);
2649      const r = await fetch(API + '/api/brand/assets', { method: 'POST', headers: authH(false), credentials: 'include', body: fd });
2650      const d = await r.json();
2651      if (!r.ok) throw new Error(d.error || 'Błąd');
2652      toast('Dodano @' + d.name + ' i podpięto jako referencję');
2653      // ═══ AUTO-ATTACH: nowy upload zastępuje stare chipy w docku ═══
2654      // Bez tego stare produkty zostawały podczepione i AI mieszała je z nowymi.
2655      S.activeRefs = [{
2656        name: d.name, assetId: d._id, imageUrl: d.imageUrl, type: d.type
2657      }];
2658      renderRefs(); updateDockHints?.();
2659      closeModal(); loadAssets();
2660    } catch(e) { toast('Błąd: ' + e.message); }
2661    finally { btn.disabled = false; btn.textContent = 'Dodaj'; }
2662  };
2663  const md = $('m-drop');
2664  md.ondragover = e => { e.preventDefault(); md.classList.add('dragging'); };
2665  md.ondragleave = () => md.classList.remove('dragging');
2666  md.ondrop = e => { e.preventDefault(); md.classList.remove('dragging'); if (e.dataTransfer.files[0]) { $('m-file').files = e.dataTransfer.files; onModalFile($('m-file')); } };
2667
2668  // ═══ INPUT + @mentions ═══
2669  const input = $('p-input');
2670  const dd = $('mention-dd');
2671  let mentionStart = -1;
2672  input.addEventListener('input', async () => {
2673    autoGrow(input); updateCost();
2674    const pos = input.selectionStart;
2675    const text = input.value.slice(0, pos);
2676    const m = text.match(/@(\w*)$/);
2677    if (m) { mentionStart = pos - m[0].length; await showMention(m[1]); }
2678    else hideMention();
2679  });
2680  input.addEventListener('keydown', e => {
2681    if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); doGenerate(); return; }
2682    if (!dd.classList.contains('show')) return;
2683    if (e.key === 'ArrowDown') { e.preventDefault(); moveSel(1); }
2684    if (e.key === 'ArrowUp') { e.preventDefault(); moveSel(-1); }
2685    if (e.key === 'Enter') { const a = dd.querySelector('.mi.active'); if (a) { e.preventDefault(); a.click(); } }
2686    if (e.key === 'Escape') hideMention();
2687  });
2688  function moveSel(dir) {
2689    const items = [...dd.querySelectorAll('.mi')];
2690    if (!items.length) return;
2691    let idx = items.findIndex(i => i.classList.contains('active'));
2692    idx = (idx + dir + items.length) % items.length;
2693    items.forEach((i,n) => i.classList.toggle('active', n === idx));
2694    items[idx].scrollIntoView({ block: 'nearest' });
2695  }
2696  async function showMention(q) {
2697    try {
2698      const all = await api('/api/brand/assets');
2699      const qL = q.toLowerCase();
2700      const matches = all.filter(a => a.name.toLowerCase().startsWith(qL)).slice(0, 10);
2701      if (!matches.length) {
2702        dd.innerHTML = `<div style="padding:20px;text-align:center;color:var(--muted);font-size:12.5px">Brak @${q||'...'}. <a href="#" onclick="go('library');hideMention();return false" style="color:var(--accent)">Dodaj w Bibliotece →</a></div>`;
2703      } else {
2704        dd.innerHTML = matches.map((a,i) => `
2705          <button class="mi ${i===0?'active':''}" data-name="${a.name}" data-id="${a._id}" data-url="${a.imageUrl}" data-type="${a.type}">
2706            <img src="${a.imageUrl}" alt="">
2707            <div style="flex:1;min-width:0">
2708              <div class="mi-name">@${a.name}${a.isPublic?' <span style="font-size:9px;color:var(--accent);letter-spacing:0.08em;font-weight:600;margin-left:4px"><i class="fas fa-sparkles"></i> CHMURKA</span>':''}</div>
2709              <div class="mi-type">${a.type}${a.description?' · '+a.description.slice(0,40):''}</div>
2710            </div>
2711          </button>`).join('');
2712        dd.querySelectorAll('.mi').forEach(el => el.onclick = () => insertMention({ name: el.dataset.name, assetId: el.dataset.id, imageUrl: el.dataset.url, type: el.dataset.type }));
2713      }
2714      dd.classList.add('show');
2715    } catch(e){ console.warn(e); }
2716  }
2717  function hideMention() { dd.classList.remove('show'); mentionStart = -1; }
2718  // Renderowany link w mention-dd woła hideMention() inline — bez tego eksportu
2719  // inline onclick rzucał ReferenceError (funkcja żyła tylko w closure).
2720  window.hideMention = hideMention;
2721  function insertMention({ name, assetId, imageUrl, type }) {
2722    if (mentionStart < 0) return;
2723    const before = input.value.slice(0, mentionStart);
2724    const afterMatch = input.value.slice(input.selectionStart).match(/^(\w*)/);
2725    const after = input.value.slice(input.selectionStart + (afterMatch?afterMatch[0].length:0));
2726    input.value = before + '@' + name + ' ' + after;
2727    const pos = (before + '@' + name + ' ').length;
2728    input.focus(); input.setSelectionRange(pos, pos);
2729    hideMention();
2730    if (!S.activeRefs.find(r => r.assetId === assetId)) {
2731      S.activeRefs.push({ name, assetId, imageUrl, type }); renderRefs();
2732    }
2733    autoGrow(input); updateCost(); updateDockHints();
2734  }
2735  function autoGrow(el){ el.style.height = 'auto'; el.style.height = Math.min(el.scrollHeight, 180) + 'px'; }
2736  // Ref może pochodzić z: (1) Library asset — ma assetId+imageUrl, (2) hist gallery — fromHistory+imageUrl, (3) lokalny plik — file+objectUrl
2737  // Unikalny klucz: refId (zawsze generujemy przy wrzucie).
2738  function refKey(r) { return r.refId || r.assetId || r.imageUrl; }
2739  function renderRefs() {
2740    const el = $('dock-refs');
2741    el.innerHTML = S.activeRefs.map(r => {
2742      const key = refKey(r);
2743      const src = r.imageUrl || r.thumbnailUrl || '';
2744      const badge = r.file ? 'Lokalny' : (r.fromHistory ? 'Z galerii' : r.type);
2745      return `
2746      <div class="chip">
2747        <img src="${src}" referrerpolicy="no-referrer">
2748        <span>@${r.name}</span>
2749        <span class="t">${badge}</span>
2750        <span class="chip-x" onclick="removeRef('${key}')"><i class="fas fa-xmark"></i></span>
2751      </div>`;
2752    }).join('');
2753    updateBulkVisibility();
2754    updateCost();
2755  }
2756  window.removeRef = function(key) {
2757    const r = S.activeRefs.find(a => refKey(a) === key);
2758    S.activeRefs = S.activeRefs.filter(x => refKey(x) !== key);
2759    if (r) {
2760      if (r.file && r.imageUrl?.startsWith('blob:')) { try { URL.revokeObjectURL(r.imageUrl); } catch(_){} }
2761      if (r.name) input.value = input.value.replace(new RegExp('@'+r.name+'\\s*','g'),'');
2762    }
2763    renderRefs(); updateCost(); updateDockHints();
2764  };
2765
2766  // Dock: user dołącza lokalne zdjęcie(a) → ref'y dla NB2 (generator mode)
2767  window.onDockFiles = function(inputEl) {
2768    const files = Array.from(inputEl.files || []);
2769    if (!files.length) return;
2770    for (const f of files) {
2771      if (!f.type.startsWith('image/')) { toast('Tylko obrazki'); continue; }
2772      if (f.size > 10 * 1024 * 1024) { toast(f.name + ' > 10MB — pomijam'); continue; }
2773      if (S.activeRefs.length >= 10) { toast('Max 10 referencji'); break; }
2774      const blobUrl = URL.createObjectURL(f);
2775      const refId = 'r_' + Math.random().toString(36).slice(2, 10);
2776      const name = 'zdjecie' + (S.activeRefs.filter(r => r.file).length + 1);
2777      S.activeRefs.push({ refId, type: 'product', name, imageUrl: blobUrl, thumbnailUrl: blobUrl, file: f, local: true });
2778    }
2779    inputEl.value = '';  // reset input żeby można było wybrać ten sam plik ponownie
2780    renderRefs(); updateCost(); updateDockHints();
2781    toast('Dołączono — opisz co zmienić i klik Generuj');
2782  };
2783
2784  // ═══ GENERATE (2-step: compose → preview → confirm) ═══
2785  // Trzymamy "zamówienie" (pending generation) gotowe do wysłania po zatwierdzeniu preview.
2786  S.pending = null;
2787
2788  window.doGenerate = async function() {
2789    const rawPrompt = input.value.trim();
2790    if (!rawPrompt && !S.currentStudio) return toast('Wybierz scenę lub wpisz prompt');
2791    // Bez blokady single-task — user może puszczać wiele równolegle. Backend ma swoje rate limity.
2792
2793    // 1) Expand mentions z promptu + MERGE z już wrzuconymi refs z docka (dropnute/załączone/picker).
2794    //    Source of truth: S.activeRefs (chipsy w docku). @mentions to dodatkowy sposób dodania z library.
2795    let refs = [...S.activeRefs];
2796    let userExtra = rawPrompt;
2797    try {
2798      const exp = await api('/api/brand/expand-mentions', { method: 'POST', body: JSON.stringify({ prompt: rawPrompt }) });
2799      // Dodaj tylko te z @mentions które jeszcze nie są w refs (po assetId lub name)
2800      for (const m of (exp.refs || [])) {
2801        const dup = refs.some(r => (r.assetId && r.assetId === m.assetId) || (r.name && r.name === m.name));
2802        if (!dup) refs.push(m);
2803      }
2804      if (exp.unknown?.length) toast('Nieznane @' + exp.unknown.join(', @'));
2805      userExtra = rawPrompt.replace(/@\w+/g, '').replace(/\s+/g, ' ').trim();
2806    } catch(_) {}
2807
2808    // 2) Compose via Gemini (albo fallback)
2809    const btn = $('btn-gen'); btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner"></i>';
2810    let composedPrompt = '', isFallback = false;
2811    try {
2812      if (S.currentStudio) {
2813        const cp = await api('/api/brand/compose-prompt', { method: 'POST', body: JSON.stringify({
2814          studioTemplate: S.currentStudio.promptTemplate,
2815          studioName: S.currentStudio.name,
2816          userExtra,
2817          refs: refs.map(r => ({ type: r.type, name: r.name })),
2818          preservePose: !!S.preservePose
2819        })});
2820        composedPrompt = cp.prompt || '';
2821        isFallback = !!cp.fallback;
2822      } else if (refs.length > 0) {
2823        // Brak Studio, ALE są referencje → tryb edycji. User pisze "zmień X" i potrzebuje
2824        // wyraźnej instrukcji żeby AI zachowało resztę obrazu. Inaczej NB2 generuje coś od zera.
2825        const userText = (userExtra || rawPrompt).trim();
2826        const refCount = refs.length;
2827        const refPhrase = refCount === 1 ? 'the reference image' : `reference images 1-${refCount}`;
2828        composedPrompt = `Using ${refPhrase} as the base, apply this change: "${userText}". Preserve everything else from the reference (identity, face, clothing, background, composition, lighting, pose, camera angle) exactly 1:1. Only modify what the user explicitly requested.`;
2829      } else {
2830        // Brak Studio, brak refs — text-to-image od zera
2831        composedPrompt = userExtra || rawPrompt;
2832      }
2833    } catch(e) {
2834      toast('Błąd composera: ' + e.message);
2835      composedPrompt = userExtra;  // final fallback
2836    } finally {
2837      btn.disabled = false; btn.innerHTML = '<i class="fas fa-arrow-up"></i>';
2838    }
2839
2840    // 3) Pomijamy preview modal — od razu generuj. (user nie chce zatwierdzania promptu)
2841    // userExtra + rawPrompt zapisujemy — bulk worker potrzebuje recomposeować osobno per produkt.
2842    S.pending = { prompt: composedPrompt, refs, ratio: S.ratios[S.ratioIdx], userExtra, rawPrompt };
2843    $('preview-text').value = composedPrompt; // wartość czytana przez confirmGenerate
2844    confirmGenerate();
2845  };
2846
2847  window.closePreview = () => {
2848    $('preview-modal').classList.remove('show');
2849    S.pending = null;
2850  };
2851
2852  // Pojedynczy generate request — używany przez confirmGenerate (single) i bulk loop.
2853  async function _sendGeneration(refsForThisJob, finalPrompt, ratio, opts = {}) {
2854    // 'nano-banana-2' → NB2 router (NB2_PROVIDER=pixverse w .env).
2855    // Strip 'runblob/' prefix gdy zapisany w DB studia — pozwala routerowi wybrać provider z env.
2856    const rawModel = S.currentStudio?.defaultModel || 'nano-banana-2';
2857    const modelName = rawModel.replace(/^runblob\//, '');
2858    const fd = new FormData();
2859    fd.append('type', 'image');
2860    fd.append('modelName', modelName);
2861    fd.append('prompt', finalPrompt);
2862    fd.append('aspectRatio', ratio);
2863    fd.append('generationType', refsForThisJob.length ? 'reference' : 'text');
2864    fd.append('modalSource', 'ecommerce');
2865    // userExtra/rawPrompt — czyste user input zapisywane w paramsSnapshot żeby galeria mogła
2866    // odzyskać oryginalną frazę przez "Użyj prompt" (bez Gemini-wrappera composera).
2867    if (opts.userExtra) fd.append('userExtra', String(opts.userExtra).slice(0, 2000));
2868    if (opts.rawPrompt) fd.append('rawPrompt', String(opts.rawPrompt).slice(0, 2000));
2869    if (S.currentStudio?._id) fd.append('studioId', String(S.currentStudio._id));
2870    if (refsForThisJob.length) {
2871      const urls = [];
2872      let fileIdx = 0;
2873      for (const r of refsForThisJob) {
2874        if (r.file) { fd.append(`imageFile${fileIdx}`, r.file); fileIdx++; }
2875        else if (r.imageUrl) urls.push(r.imageUrl);
2876      }
2877      if (urls.length) fd.append('imageUrls', JSON.stringify(urls));
2878    }
2879    const r = await fetch(API + '/generate', { method: 'POST', headers: authH(false), credentials: 'include', body: fd });
2880    const d = await r.json();
2881    if (!r.ok) throw new Error(d.error || 'Błąd');
2882    const taskId = d.data?.taskId || d.taskId;
2883    if (!taskId) throw new Error('Brak taskId');
2884    return taskId;
2885  }
2886
2887  window.confirmGenerate = async function() {
2888    if (!requireLogin('wygenerować')) return;
2889    if (!S.pending) return;
2890    const finalPrompt = $('preview-text').value.trim();
2891    if (!finalPrompt) return toast('Prompt nie może być pusty');
2892
2893    const { refs, ratio } = S.pending;
2894    const btn = $('btn-confirm-gen');
2895    btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin" style="margin-right:6px"></i> Wysyłam...';
2896
2897    try {
2898      // ═══ BULK MODE: każdy produkt → osobny task z DEDYKOWANYM promptem (composed per product) ═══
2899      if (S.bulkMode) {
2900        const products = refs.filter(r => r.type === 'product');
2901        const characters = refs.filter(r => r.type === 'character');
2902        const others = refs.filter(r => r.type !== 'product' && r.type !== 'character');
2903        if (products.length < 2) {
2904          // mniej niż 2 produkty — bulk niepotrzebny, fallback na single
2905          const taskId = await _sendGeneration(refs, finalPrompt, ratio, { userExtra: S.pending?.userExtra, rawPrompt: S.pending?.rawPrompt });
2906          S.pendingTasks.add(taskId); addPending(taskId); startPolling();
2907        } else {
2908          toast(`Bulk: wysyłam ${products.length} generacji (po 2 naraz)...`);
2909          // Każdy produkt dostaje SWÓJ prompt — composer przelatuje fresh z 1 ref'em produktu.
2910          // Bez tego pierwszy task miałby prompt o "image 1, 2 AND 3" ale tylko 1 obraz → AI generuje kopie.
2911          let fired = 0;
2912          let failed = 0;
2913          const queue = products.slice();
2914          // Czysty user-extra (bez @mentions), oryginalny rawPrompt usera — composer go zwiąże z 1 produktem na raz.
2915          const userExtraForBulk = (S.pending?.userExtra ?? '').trim();
2916          const rawPromptCached = (S.pending?.rawPrompt ?? '').trim();
2917          async function composeForProduct(product) {
2918            const refsForJob = [...characters, product, ...others];
2919            // Bez Studio → fallback prosty (preserve block z 1 produktem)
2920            if (!S.currentStudio) return { prompt: userExtraForBulk || rawPromptCached || product.name, refsForJob };
2921            try {
2922              const cp = await api('/api/brand/compose-prompt', { method: 'POST', body: JSON.stringify({
2923                studioTemplate: S.currentStudio.promptTemplate,
2924                studioName: S.currentStudio.name,
2925                userExtra: userExtraForBulk,
2926                refs: refsForJob.map(r => ({ type: r.type, name: r.name })),
2927                preservePose: !!S.preservePose
2928              })});
2929              return { prompt: cp.prompt || userExtraForBulk, refsForJob };
2930            } catch(e) {
2931              console.warn('compose fail for @' + product.name + ':', e.message);
2932              return { prompt: userExtraForBulk || product.name, refsForJob };
2933            }
2934          }
2935          async function worker() {
2936            while (queue.length) {
2937              const product = queue.shift();
2938              try {
2939                const { prompt, refsForJob } = await composeForProduct(product);
2940                const taskId = await _sendGeneration(refsForJob, prompt, ratio, { userExtra: userExtraForBulk, rawPrompt: rawPromptCached });
2941                S.pendingTasks.add(taskId);
2942                addPending(taskId);
2943                fired++;
2944                if (fired === 1) startPolling();
2945              } catch (e) {
2946                failed++;
2947                console.warn('bulk fail @' + (product.name||'?') + ':', e.message);
2948              }
2949            }
2950          }
2951          await Promise.all([worker(), worker()]);
2952          toast(`Bulk: ${fired} wysłanych${failed ? `, ${failed} błędów` : ''}`);
2953        }
2954      } else {
2955        // ═══ SINGLE MODE: stary flow, wszystkie refy w jednym tasku ═══
2956        const taskId = await _sendGeneration(refs, finalPrompt, ratio, { userExtra: S.pending?.userExtra, rawPrompt: S.pending?.rawPrompt });
2957        S.pendingTasks.add(taskId); addPending(taskId); startPolling();
2958        toast('Wysłano do generatora');
2959      }
2960      // Cleanup: zamknij preview, wyczyść input (+ revoke blob URLs)
2961      closePreview();
2962      for (const r of S.activeRefs) { if (r.file && r.imageUrl?.startsWith('blob:')) { try { URL.revokeObjectURL(r.imageUrl); } catch(_){} } }
2963      input.value = ''; autoGrow(input); S.activeRefs = []; renderRefs();
2964    } catch(e) {
2965      toast('Błąd: ' + e.message);
2966    } finally {
2967      btn.disabled = false; btn.innerHTML = '<i class="fas fa-arrow-up" style="margin-right:6px"></i> Generuj';
2968    }
2969  };
2970
2971  function addPending(taskId) {
2972    const el = $('hist');
2973    el.querySelector('.empty')?.remove();
2974    const item = document.createElement('div');
2975    item.className = 'hist-item pending'; item.id = 'h-' + taskId;
2976    item.innerHTML = '<div class="spin"></div>';
2977    el.insertBefore(item, el.firstChild);
2978    bumpCount(+1);
2979  }
2980  function bumpCount(n) {
2981    const el = $('hist-count'); el.textContent = Math.max(0, (+el.textContent||0) + n);
2982    syncMobileHistoryBadge();
2983  }
2984  function syncMobileHistoryBadge() {
2985    const n = +($('hist-count')?.textContent || 0);
2986    const badge = $('m-history-badge');
2987    if (badge) {
2988      badge.style.display = n > 0 ? 'flex' : 'none';
2989      badge.textContent = n > 99 ? '99+' : n;
2990    }
2991  }
2992  // Mobile history sheet — przesuwamy DOM node #hist między right-col a mobile sheet
2993  window.openMobileHistory = function() {
2994    const histEl = $('hist');
2995    const sheetTarget = $('m-hist-list');
2996    if (histEl && sheetTarget && histEl.parentElement !== sheetTarget) {
2997      sheetTarget.appendChild(histEl);
2998    }
2999    $('m-history-backdrop').classList.add('show');
3000    $('m-history-sheet').classList.add('show');
3001  };
3002  window.closeMobileHistory = function() {
3003    $('m-history-backdrop').classList.remove('show');
3004    $('m-history-sheet').classList.remove('show');
3005    // Po animacji wracamy #hist do right-col (żeby loadHistory go znajdował)
3006    setTimeout(() => {
3007      const histEl = $('hist');
3008      const rightCol = document.querySelector('.right-col');
3009      const libQuick = rightCol?.querySelector('.lib-quick');
3010      if (histEl && rightCol && histEl.parentElement !== rightCol) {
3011        if (libQuick) rightCol.insertBefore(histEl, libQuick);
3012        else rightCol.appendChild(histEl);
3013      }
3014    }, 320);
3015  };
3016  // Touch swipe-down żeby zamknąć sheet
3017  (function() {
3018    const sheet = document.getElementById('m-history-sheet');
3019    if (!sheet) return;
3020    let startY = 0, currentY = 0, dragging = false;
3021    sheet.addEventListener('touchstart', (e) => {
3022      // Tylko gdy scroll-top sheet'a 0 (na górze listy)
3023      if (sheet.scrollTop > 5) return;
3024      startY = e.touches[0].clientY; dragging = true; sheet.style.transition = 'none';
3025    }, { passive: true });
3026    sheet.addEventListener('touchmove', (e) => {
3027      if (!dragging) return;
3028      currentY = e.touches[0].clientY - startY;
3029      if (currentY > 0) sheet.style.transform = `translateY(${currentY}px)`;
3030    }, { passive: true });
3031    sheet.addEventListener('touchend', () => {
3032      if (!dragging) return;
3033      dragging = false; sheet.style.transition = '';
3034      sheet.style.transform = '';
3035      if (currentY > 80) closeMobileHistory();
3036      currentY = 0;
3037    });
3038  })();
3039  function renderHist(t) {
3040    const prev = $('h-' + t.taskId);
3041    const promptPreview = (t.prompt||'').slice(0, 60);
3042    const time = new Date(t.createdAt).toLocaleTimeString('pl', { hour:'2-digit', minute:'2-digit' });
3043    const isReady = t.status === 'success' && t.resultUrl;
3044    const isFail = t.status === 'failed';
3045    const safeUrl = (t.resultUrl || '').replace(/"/g, '&quot;');
3046    // Film w historii: własny rendering (<video>, autoodtwarzanie bez dźwięku) i TYLKO
3047    // Pobierz + Usuń — akcje obrazkowe (referencja/edytor/prompt/regen) dla wideo nie
3048    // mają sensu i tylko myliły (decyzja ownera 25.08).
3049    const isVideo = t.type === 'video' || /\.(mp4|webm|mov)(\?|$)/i.test(t.resultUrl || '');
3050    const actions = !isReady ? '' : isVideo ? `
3051      <div class="hist-actions">
3052        <button class="ha-btn" onclick="event.stopPropagation();histDownload('${t.taskId}')" title="Pobierz"><i class="fas fa-download"></i></button>
3053      </div>
3054      <div class="hist-bot-actions">
3055        <button class="ha-btn danger" onclick="event.stopPropagation();histDelete('${t.taskId}')" title="Usuń"><i class="fas fa-trash"></i></button>
3056      </div>` : `
3057      <div class="hist-actions">
3058        <button class="ha-btn primary" onclick="event.stopPropagation();histAddRef('${t.taskId}')" title="Dodaj jako referencję do promptu"><i class="fas fa-plus"></i></button>
3059        <button class="ha-btn accent" onclick="event.stopPropagation();histEdit('${t.taskId}')" title="Edytuj"><i class="fas fa-pen"></i></button>
3060        <button class="ha-btn ha-btn-prompt" onclick="event.stopPropagation();histUsePrompt('${t.taskId}')" title="Użyj tego promptu" style="background:linear-gradient(135deg,#7c3aed,#bc13fe) !important;color:#fff !important;border:1px solid rgba(255,255,255,0.25) !important;font-weight:700;font-size:12.5px;"><i class="fa-solid fa-paste"></i></button>
3061        <button class="ha-btn" onclick="event.stopPropagation();histDownload('${t.taskId}')" title="Pobierz"><i class="fas fa-download"></i></button>
3062      </div>
3063      <div class="hist-bot-actions">
3064        <button class="ha-btn" onclick="event.stopPropagation();histRegen('${t.taskId}')" title="Regeneruj z tymi samymi parametrami"><i class="fas fa-rotate"></i></button>
3065        <button class="ha-btn danger" onclick="event.stopPropagation();histDelete('${t.taskId}')" title="Usuń"><i class="fas fa-trash"></i></button>
3066      </div>`;
3067    const media = isVideo
3068      ? `<video data-src="${safeUrl}" muted loop playsinline preload="none" style="width:100%;display:block;min-height:140px;background:#000"></video>`
3069      : `<img src="${safeUrl}" alt="" loading="lazy" decoding="async">`;
3070    const html = isReady
3071      ? `${media}${actions}<div class="hist-meta"><div class="hist-prompt">${promptPreview}</div><div class="hist-time">${time}</div></div>`
3072      : isFail
3073        ? `<div style="aspect-ratio:1/1;display:flex;align-items:center;justify-content:center;color:var(--danger);font-size:12px;padding:16px;text-align:center">${(t.error||'Błąd')}</div>`
3074        : `<div class="spin"></div>`;
3075    const item = prev || document.createElement('div');
3076    item.className = 'hist-item' + ((!isReady && !isFail) ? ' pending' : '');
3077    item.id = 'h-' + t.taskId;
3078    item.innerHTML = html;
3079    uzbrojLazyWideo(item);
3080    if (isReady && isVideo) {
3081      // Lightbox jest obrazkowy — film otwieramy w nowej karcie; bez przeciągania
3082      // (payload przeciągania to imageUrl, film by udawał obrazek).
3083      item.onclick = () => window.open(t.resultUrl, '_blank');
3084      item.draggable = false;
3085    } else if (isReady) {
3086      item.onclick = () => openLightbox(t.resultUrl);
3087      item.draggable = true;
3088      item.ondragstart = (e) => {
3089        const payload = JSON.stringify({ kind: 'task', taskId: t.taskId, imageUrl: t.resultUrl, thumbnailUrl: t.thumbnailUrl || t.resultUrl });
3090        e.dataTransfer.effectAllowed = 'copy';
3091        e.dataTransfer.setData('application/x-chmurka', payload);
3092        e.dataTransfer.setData('text/plain', t.resultUrl);
3093      };
3094    } else {
3095      item.draggable = false;
3096    }
3097    if (!prev) $('hist').appendChild(item);
3098    // Zapisz task w cache do akcji (regen potrzebuje paramsSnapshot)
3099    S.taskCache = S.taskCache || {};
3100    S.taskCache[t.taskId] = t;
3101  }
3102
3103  // Drag-start na asset tiles (wołane inline z renderAssets)
3104  window.onAssetDragStart = function(e, payloadJson) {
3105    e.dataTransfer.effectAllowed = 'copy';
3106    e.dataTransfer.setData('application/x-chmurka', payloadJson);
3107    // Fallback żeby NIE wkleiło URL do textarea (browser default drop handler textarea wykorzystuje text/plain + text/uri-list)
3108    e.dataTransfer.setData('text/plain', '');
3109  };
3110
3111  // ═══ SMART LIGHTBOX (zoom widoku na klik tile) ═══
3112  window.openLightbox = function(url) {
3113    if (!url) return;
3114    const bd = document.createElement('div');
3115    bd.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.92);backdrop-filter:blur(20px);z-index:500;display:flex;align-items:center;justify-content:center;padding:40px;cursor:zoom-out;animation:fadeIn 0.25s var(--eo)';
3116    bd.onclick = () => bd.remove();
3117    const img = document.createElement('img');
3118    img.src = url;
3119    img.referrerPolicy = 'no-referrer';
3120    img.style.cssText = 'max-width:100%;max-height:100%;border-radius:16px;box-shadow:0 30px 80px rgba(0,0,0,0.6);object-fit:contain';
3121    bd.appendChild(img);
3122    document.body.appendChild(bd);
3123  };
3124
3125  // ═══ HIST ACTIONS ═══
3126  window.histAddRef = function(taskId) {
3127    const t = S.taskCache?.[taskId];
3128    if (!t?.resultUrl) return toast('Brak obrazka');
3129    if (!S.activeRefs) S.activeRefs = [];
3130    if (S.activeRefs.length >= 10) return toast('Max 10 referencji');
3131    if (S.activeRefs.some(r => r.imageUrl === t.resultUrl)) return toast('Już dodane');
3132    const refId = 'r_' + Math.random().toString(36).slice(2, 10);
3133    const name = 'ref' + (S.activeRefs.length + 1);
3134    S.activeRefs.push({ refId, type: 'product', name, imageUrl: t.resultUrl, thumbnailUrl: t.thumbnailUrl || t.resultUrl, fromHistory: true });
3135    renderRefs(); updateDockHints();
3136    toast('Dodano @' + name + ' do promptu');
3137    $('p-input').focus();
3138  };
3139
3140  // Smart download z 3-stopniowym fallbackiem: direct fetch → /api/image-proxy → <a download>
3141  async function smartDownload(url, filename) {
3142    if (!url) { toast('Brak obrazka'); return; }
3143    const name = filename || (url.split('/').pop().split('?')[0] || 'chmurka-file');
3144    async function fetchBlob(u, hdrs) {
3145      const r = await fetch(u, { mode: 'cors', referrerPolicy: 'no-referrer', credentials: hdrs ? 'include' : 'omit', headers: hdrs || {} });
3146      if (!r.ok) throw new Error('HTTP ' + r.status);
3147      return r.blob();
3148    }
3149    // 0) NASZ BUCKET → direct z R2, z pominięciem Railway (2026-07-21) — media.chmurka.ai
3150    //    ma ustawiony CORS, więc przepychanie pliku przez nasz serwer to czysty koszt egressu.
3151    const _ourMedia = (() => {
3152      try { const h = new URL(url, location.href).hostname.toLowerCase();
3153            return h === 'media.chmurka.ai' || h === 'media.pixcloud.app'; } catch { return false; }
3154    })();
3155    if (_ourMedia) {
3156      try {
3157        triggerBlobDownload(await fetchBlob(url), name);
3158        toast('Pobrano');
3159        return;
3160      } catch(e0) { console.warn('[download] direct R2 failed, fallback na proxy:', e0.message); }
3161    }
3162    // 1) FALLBACK: /api/image-proxy?download=1 — same-origin blob, Content-Disposition: attachment.
3163    try {
3164      const blob = await fetchBlob('/api/image-proxy?download=1&url=' + encodeURIComponent(url), authH());
3165      triggerBlobDownload(blob, name);
3166      toast('Pobrano');
3167      return;
3168    } catch(e1) { console.warn('[download] proxy failed:', e1.message); }
3169    // 2) FALLBACK: direct fetch (jeśli zewnętrzny CDN ma CORS)
3170    try {
3171      const blob = await fetchBlob(url);
3172      triggerBlobDownload(blob, name);
3173      toast('Pobrano');
3174      return;
3175    } catch(e2) { console.warn('[download] direct fetch failed:', e2.message); }
3176    // 3) <a download> — ostatni fallback (może zapytać o nazwę / pokazać preview w tab'ie)
3177    const a = document.createElement('a');
3178    a.href = url;
3178 a.download = name; a.target = '_blank';
3179    document.body.appendChild(a); a.click(); a.remove();
3180    toast('Otwieram w nowej karcie');
3181  }
3182  function triggerBlobDownload(blob, name) {
3183    const bu = URL.createObjectURL(blob);
3184    const a = document.createElement('a');
3185    a.href = bu; a.download = name;
3186    document.body.appendChild(a); a.click(); a.remove();
3187    setTimeout(() => URL.revokeObjectURL(bu), 1000);
3188  }
3189
3190  window.histDownload = function(taskId) {
3191    const t = S.taskCache?.[taskId];
3192    if (!t?.resultUrl) return toast('Brak obrazka');
3193    const isVideo = /\.(mp4|webm|mov)($|\?)/i.test(t.resultUrl);
3194    const ext = isVideo ? 'mp4' : (t.resultUrl.match(/\.([a-z0-9]+)(\?|$)/i)?.[1] || 'jpg');
3195    smartDownload(t.resultUrl, `chmurka-${taskId.slice(0, 8)}.${ext}`);
3196  };
3197
3198  window.histRegen = function(taskId) {
3199    const t = S.taskCache?.[taskId];
3200    if (!t) return toast('Brak taska w cache');
3201    if (t.prompt) $('p-input').value = t.prompt;
3202    // Aspect ratio ze snapshot
3203    const ar = t.paramsSnapshot?.aspectRatio;
3204    if (ar && S.ratios.includes(ar)) { S.ratioIdx = S.ratios.indexOf(ar); $('ratio-label').textContent = ar; }
3205    autoGrow($('p-input'));
3206    $('p-input').focus();
3207    toast('Wczytano parametry — klik Generuj');
3208  };
3209
3210  // Wkleja CZYSTY user input (userExtra) z poprzedniej generacji do prompt fielda — bez Gemini-wrappera.
3211  // Composer doklei wrapper świeżo z aktualnego studia gdy klikniesz Generuj. Stare taski (przed
3212  // savem userExtra) → fallback na task.prompt z toastem ostrzegawczym (może zawierać wrapper).
3213  window.histUsePrompt = function(taskId) {
3214    const t = S.taskCache?.[taskId];
3215    if (!t) return toast('Brak taska w cache');
3216    const userExtra = t.paramsSnapshot?.userExtra || t.paramsSnapshot?.rawPrompt || '';
3217    if (userExtra) {
3218      $('p-input').value = userExtra;
3219      autoGrow($('p-input'));
3220      $('p-input').focus();
3221      toast('Wklejono prompt — możesz dopisać i kliknąć Generuj');
3222    } else if (t.prompt) {
3223      $('p-input').value = t.prompt;
3224      autoGrow($('p-input'));
3225      $('p-input').focus();
3226      toast('Stary task — wklejony cały prompt z wrapperem. Edytuj przed generacją.', 5000);
3227    } else {
3228      toast('Brak zapisanego promptu');
3229    }
3230  };
3231
3232  window.histDelete = async function(taskId) {
3233    if (!confirm('Usunąć generację?')) return;
3234    try {
3235      await api('/api/brand/tasks/' + taskId, { method: 'DELETE' });
3236      $('h-' + taskId)?.remove();
3237      bumpCount(-1);
3238      toast('Usunięto');
3239    } catch(e) { toast('Błąd: ' + e.message); }
3240  };
3241
3242  window.histEdit = function(taskId) {
3243    const t = S.taskCache?.[taskId];
3244    if (!t?.resultUrl) return toast('Brak obrazka');
3245    openImageEditor({ imageUrl: t.resultUrl, taskId: t.taskId });
3246  };
3247
3248  // ═══════════════════════════════════════════════════════════════════════
3249  //  IMAGE EDITOR — 5 narzędzi w premium soft stylu ecommerce
3250  //  State: _ed = { url, taskId, tool, filters, cam }
3251  // ═══════════════════════════════════════════════════════════════════════
3252  const _ed = {
3253    url: null, taskId: null, tool: 'ai-edit',
3254    filters: { brightness: 100, contrast: 100, saturation: 100, temperature: 0 },
3255    cam: { azimuth: 0, elevation: 0, zoom: 0 },
3256  };
3257
3258  window.openImageEditor = function(opts) {
3259    _ed.url = opts?.imageUrl || null;
3260    _ed.imageUrl = opts?.imageUrl || null;  // ← reset z poprzedniego taska text-transform (inaczej kolejne otwarcie wysyła stare zdjęcie)
3261    _ed.taskId = opts?.taskId || null;
3262    _ed.filters = { brightness: 100, contrast: 100, saturation: 100, temperature: 0 };
3263    _ed.cam = { azimuth: 0, elevation: 0, zoom: 0 };
3264    _ed.text = { layers: [], font: 'Inter', color: '#FFFFFF', size: 36, selectedId: null };  // ← reset napisów
3265    _ed.tool = 'ai-edit';
3266    const img = $('ed-img');
3267    img.src = _ed.url;
3268    img.style.filter = 'none';
3269    img.style.opacity = '1';
3270    // Sync text-img z aktualnym zdjęciem (na wypadek przełączenia na tool=text później)
3271    const textImg = document.getElementById('ed-text-img');
3272    if (textImg) textImg.src = _ed.url;
3273    // Wyczyść stare text layers z poprzedniego edytora
3274    const frame = document.getElementById('ed-text-frame');
3275    if (frame) frame.querySelectorAll('.ed-tlayer, .ed-text-inline, .ed-text-selrect').forEach(n => n.remove());
3276    $('editor').classList.add('show');
3277    edSelectTool('ai-edit');
3278  };
3279
3280  window.closeEditor = function() {
3281    $('editor').classList.remove('show');
3282    edHideLoading();
3283  };
3284
3285  window.edSelectTool = function(name) {
3286    _ed.tool = name;
3287    document.querySelectorAll('.ed-tool').forEach(b => b.classList.toggle('on', b.dataset.tool === name));
3288    // Resetuj preview filter jeśli nie w trybie filter
3289    const img = $('ed-img');
3290    if (name !== 'filter') {
3291      img.style.filter = 'none';
3292    } else {
3293      edApplyFiltersCSS();
3294    }
3295    // Toggle text-stage overlay
3296    const textStage = $('ed-text-stage');
3297    if (textStage) {
3298      if (name === 'text') {
3299        $('ed-text-img').src = $('ed-img').src;
3300        textStage.classList.add('active');
3301        $('ed-img').style.opacity = '0';
3302      } else {
3303        textStage.classList.remove('active');
3304        $('ed-img').style.opacity = '1';
3305      }
3306    }
3307    edRenderPanel();
3308    $('ed-subtitle').textContent = {
3309      'ai-edit': 'Opisz co zmienić — AI przerobi',
3310      'upscale': 'Podnieś rozdzielczość 2×',
3311      'reframe': 'Rozszerz tło — wybierz nowe proporcje',
3312      'camera': 'Zmień kąt widzenia na tym samym obiekcie',
3313      'filter': 'Jasność, kontrast, nasycenie, temperatura',
3314      'text': 'Kliknij na zdjęciu aby dodać napis. AI zamieni go na profesjonalną typografię.'
3315    }[name] || '';
3316  };
3317
3318  function edRenderPanel() {
3319    const p = $('ed-panel');
3320    const tool = _ed.tool;
3321    if (tool === 'ai-edit') {
3322      p.innerHTML = `
3323        <div>
3324          <label>Co zmienić?</label>
3325          <textarea class="ed-ta" id="ed-prompt" placeholder="np. zmień tło na białe studio, dodaj odbicia, usuń człowieka z tyłu..."></textarea>
3326        </div>
3327        <div class="hint-txt"><i class="fas fa-circle-info"></i>AI zachowa produkt/osobę ze zdjęcia 1:1 i zmieni tylko to co opisałeś.</div>
3328        <div id="ed-status-ai" class="ed-status"></div>
3329        <div class="ed-actions">
3330          <button class="ed-go" id="ed-go-ai" onclick="edRunAiEdit()"><i class="fas fa-wand-magic-sparkles"></i> Edytuj (2 kr)</button>
3331        </div>`;
3332    } else if (tool === 'upscale') {
3333      p.innerHTML = `
3334        <div>
3335          <label>Skala</label>
3336          <div class="ed-scale">
3337            <button class="ed-scale-btn on" data-scale="2" onclick="edSetScale(2,this)">2×</button>
3338          </div>
3339        </div>
3340        <div class="hint-txt"><i class="fas fa-circle-info"></i>Powiększa rozdzielczość zachowując detale. Max 8192 px.</div>
3341        <div id="ed-status-up" class="ed-status"></div>
3342        <div class="ed-actions">
3343          <button class="ed-go" id="ed-go-up" onclick="edRunUpscale()"><i class="fas fa-up-right-and-down-left-from-center"></i> Powiększ (2 kr)</button>
3344        </div>`;
3345      _ed.scale = 2;
3346    } else if (tool === 'reframe') {
3347      const ratios = ['1:1','4:5','3:4','9:16','16:9'];
3348      p.innerHTML = `
3349        <div>
3350          <label>Nowe proporcje</label>
3351          <div class="ed-ratios">
3352            ${ratios.map(r => `<button class="ed-ratio-btn ${r==='1:1'?'on':''}" data-ratio="${r}" onclick="edSetRatio('${r}',this)">${r}</button>`).join('')}
3353          </div>
3354        </div>
3355        <div>
3356          <label>Prompt (opcjonalny)</label>
3357          <textarea class="ed-ta" id="ed-reframe-prompt" placeholder="np. dorzuć więcej nieba, mglista poranka..." rows="3"></textarea>
3358        </div>
3359        <div class="hint-txt"><i class="fas fa-circle-info"></i>AI dorysowuje nowe obszary żeby zmieścić obraz w wybranych proporcjach.</div>
3360        <div id="ed-status-rf" class="ed-status"></div>
3361        <div class="ed-actions">
3362          <button class="ed-go" id="ed-go-rf" onclick="edRunReframe()"><i class="fas fa-expand"></i> Rozszerz (4 kr)</button>
3363        </div>`;
3364      _ed.reframeRatio = '1:1';
3365    } else if (tool === 'camera') {
3366      p.innerHTML = `
3367        <div>
3368          <div class="ed-slider-row">
3369            <div class="ed-slider-head"><label>Obrót (lewo/prawo)</label><span class="val" id="ed-v-az">${_ed.cam.azimuth}°</span></div>
3370            <input type="range" class="ed-slider" min="-90" max="90" value="${_ed.cam.azimuth}" oninput="edCam('azimuth',this.value)">
3371          </div>
3372          <div class="ed-slider-row">
3373            <div class="ed-slider-head"><label>Nachylenie (góra/dół)</label><span class="val" id="ed-v-el">${_ed.cam.elevation}</span></div>
3374            <input type="range" class="ed-slider" min="-1" max="1" step="1" value="${_ed.cam.elevation}" oninput="edCam('elevation',this.value)">
3375          </div>
3376          <div class="ed-slider-row">
3377            <div class="ed-slider-head"><label>Przybliżenie</label><span class="val" id="ed-v-zm">${_ed.cam.zoom}</span></div>
3378            <input type="range" class="ed-slider" min="0" max="10" step="1" value="${_ed.cam.zoom}" oninput="edCam('zoom',this.value)">
3379          </div>
3380        </div>
3381        <div>
3382          <label>Presety</label>
3383          <div class="ed-cam-presets">
3384            <button class="ed-cam-preset" onclick="edCamPreset(45,0,0)">Prawo 45°</button>
3385            <button class="ed-cam-preset" onclick="edCamPreset(-45,0,0)">Lewo 45°</button>
3386            <button class="ed-cam-preset" onclick="edCamPreset(0,1,0)">Z góry</button>
3387            <button class="ed-cam-preset" onclick="edCamPreset(0,-1,0)">Z dołu</button>
3388            <button class="ed-cam-preset" onclick="edCamPreset(0,0,5)">Z bliska</button>
3389            <button class="ed-cam-preset" onclick="edCamPreset(90,0,0)">Z boku</button>
3390          </div>
3391        </div>
3392        <div class="hint-txt"><i class="fas fa-circle-info"></i>AI pokaże ten sam obiekt pod innym kątem.</div>
3393        <div id="ed-status-cam" class="ed-status"></div>
3394        <div class="ed-actions">
3395          <button class="ed-go" id="ed-go-cam" onclick="edRunCamera()"><i class="fas fa-camera-rotate"></i> Zmień kąt (4 kr)</button>
3396        </div>`;
3397    } else if (tool === 'filter') {
3398      const f = _ed.filters;
3399      p.innerHTML = `
3400        <div>
3401          <div class="ed-slider-row">
3402            <div class="ed-slider-head"><label>Jasność</label><span class="val" id="ed-v-br">${f.brightness}%</span></div>
3403            <input type="range" class="ed-slider" min="0" max="200" value="${f.brightness}" oninput="edFilter('brightness',this.value)">
3404          </div>
3405          <div class="ed-slider-row">
3406            <div class="ed-slider-head"><label>Kontrast</label><span class="val" id="ed-v-co">${f.contrast}%</span></div>
3407            <input type="range" class="ed-slider" min="0" max="200" value="${f.contrast}
3407" oninput="edFilter('contrast',this.value)">
3408          </div>
3409          <div class="ed-slider-row">
3410            <div class="ed-slider-head"><label>Nasycenie</label><span class="val" id="ed-v-sa">${f.saturation}%</span></div>
3411            <input type="range" class="ed-slider" min="0" max="200" value="${f.saturation}" oninput="edFilter('saturation',this.value)">
3412          </div>
3413          <div class="ed-slider-row">
3414            <div class="ed-slider-head"><label>Temperatura</label><span class="val" id="ed-v-te">${f.temperature}</span></div>
3415            <input type="range" class="ed-slider" min="-50" max="50" value="${f.temperature}" oninput="edFilter('temperature',this.value)">
3416          </div>
3417        </div>
3418        <div class="hint-txt"><i class="fas fa-circle-info"></i>Filtry są podglądem — kliknij Zastosuj żeby zapisać zmiany w obrazku.</div>
3419        <div id="ed-status-f" class="ed-status"></div>
3420        <div class="ed-actions">
3421          <button class="ed-go" id="ed-go-f" onclick="edResetFilters()" style="background:var(--card);color:var(--muted);border:1px solid var(--border)"><i class="fas fa-arrow-rotate-left"></i> Reset</button>
3422          <button class="ed-go" onclick="edApplyFiltersAndSave()"><i class="fas fa-check"></i> Zastosuj (zapisz zmiany)</button>
3423        </div>`;
3424    } else if (tool === 'text') {
3425      const t = _ed.text || (_ed.text = { layers: [], font: 'Inter', color: '#FFFFFF', size: 36, selectedId: null });
3426      p.innerHTML = `
3427        <div style="background:linear-gradient(135deg,rgba(166,241,248,0.08),rgba(166,241,248,0.02));border:1px solid rgba(166,241,248,0.18);border-radius:12px;padding:12px 14px;font-size:12.5px;line-height:1.5;color:var(--text)">
3428          <div style="display:flex;gap:8px;align-items:flex-start"><i class="fas fa-wand-magic-sparkles" style="color:var(--accent);font-size:13px;margin-top:2px"></i><div>
3429            <b>Zaznacz obszar</b> przeciągając myszką po zdjęciu → wpisz tekst → <b>Enter</b>. AI zamieni napisy w profesjonalną typografię pasującą do stylu fotografii.<br>
3430            <span style="color:var(--muted);font-size:11.5px">⚠️ AI może popełnić błąd — jeśli wynik nie pasuje, spróbuj ponownie.</span>
3431          </div></div>
3432        </div>
3433        <div>
3434          <label>Font (dla nowych napisów)</label>
3435          <div class="ed-text-fonts">
3436            ${['Inter','Helvetica','Cormorant Garamond','Bebas Neue','Playfair Display'].map(f =>
3437              `<button class="ed-text-font ${t.font===f?'on':''}" onclick="edSetTextFont('${f}')" style="font-family:'${f}',sans-serif">${f}</button>`).join('')}
3438          </div>
3439        </div>
3440        <div>
3441          <label>Kolor</label>
3442          <div class="ed-text-color-row">
3443            ${['#FFFFFF','#0D0D12','#A6F1F8','#E5C07B','#FF6B9D'].map(c =>
3444              `<button class="ed-color-pill ${t.color===c?'on':''}" style="background:${c}" onclick="edSetTextColor('${c}')"></button>`).join('')}
3445            <input type="color" value="${t.color}" oninput="edSetTextColor(this.value)" style="width:28px;height:28px;border:none;background:transparent;cursor:pointer;border-radius:50%;overflow:hidden">
3446          </div>
3447        </div>
3448        <div>
3449          <div class="ed-slider-head"><label>Rozmiar</label><span class="val" id="ed-v-tsz">${t.size}px</span></div>
3450          <input type="range" class="ed-slider" min="14" max="120" value="${t.size}" oninput="edSetTextSize(this.value)">
3451        </div>
3452        <div class="hint-txt"><i class="fas fa-circle-info"></i>Przeciągnij napis aby przesunąć. Kliknij napis i × aby usunąć. Max 5 napisów.</div>
3453        <div id="ed-status-t" class="ed-status"></div>
3454        <div class="ed-actions">
3455          <button class="ed-go" id="ed-go-t" onclick="edRunTextTransform()"><i class="fas fa-wand-magic-sparkles"></i> Wygeneruj profesjonalną grafikę (10 kr)</button>
3456        </div>`;
3457    }
3458  }
3459
3460  // ── Controls (setters wołane z UI) ──
3461  window.edSetScale = (s, btn) => { _ed.scale = s; btn.parentElement.querySelectorAll('.ed-scale-btn').forEach(b => b.classList.toggle('on', b === btn)); };
3462  window.edSetRatio = (r, btn) => { _ed.reframeRatio = r; btn.parentElement.querySelectorAll('.ed-ratio-btn').forEach(b => b.classList.toggle('on', b === btn)); };
3463  window.edCam = (prop, val) => {
3464    _ed.cam[prop] = Number(val);
3465    const mapId = { azimuth: 'ed-v-az', elevation: 'ed-v-el', zoom: 'ed-v-zm' };
3466    const suffix = prop === 'azimuth' ? '°' : '';
3467    const el = document.getElementById(mapId[prop]); if (el) el.textContent = val + suffix;
3468  };
3469  window.edCamPreset = (az, el, zm) => {
3470    _ed.cam = { azimuth: az, elevation: el, zoom: zm };
3471    edRenderPanel();  // re-render żeby suwaki się odświeżyły
3472  };
3473  window.edFilter = (prop, val) => {
3474    _ed.filters[prop] = Number(val);
3475    const mapId = { brightness: 'ed-v-br', contrast: 'ed-v-co', saturation: 'ed-v-sa', temperature: 'ed-v-te' };
3476    const suffix = prop === 'temperature' ? '' : '%';
3477    const labelEl = document.getElementById(mapId[prop]); if (labelEl) labelEl.textContent = val + suffix;
3478    edApplyFiltersCSS();
3479  };
3480  window.edResetFilters = () => {
3481    _ed.filters = { brightness: 100, contrast: 100, saturation: 100, temperature: 0 };
3482    $('ed-img').style.filter = 'none';
3483    edRenderPanel();
3484  };
3485  function edApplyFiltersCSS() {
3486    const f = _ed.filters;
3487    let temp = '';
3488    if (f.temperature > 0) temp = ` sepia(${f.temperature}%) hue-rotate(-10deg)`;
3489    else if (f.temperature < 0) temp = ` hue-rotate(${f.temperature * 2}deg)`;
3490    $('ed-img').style.filter = `brightness(${f.brightness}%) contrast(${f.contrast}%) saturate(${f.saturation}%)${temp}`;
3491  }
3492
3493  // ═══ TEXT TOOL — draggable layers + export ═══
3494  window.edSetTextFont = (f) => { (_ed.text || (_ed.text={})).font = f; edRenderPanel(); };
3495  window.edSetTextColor = (c) => { (_ed.text || (_ed.text={})).color = c; edRenderPanel(); _edUpdateSelectedLayer(); };
3496  window.edSetTextSize = (s) => { (_ed.text || (_ed.text={})).size = Number(s); document.getElementById('ed-v-tsz').textContent = s + 'px'; _edUpdateSelectedLayer(); };
3497
3498  function _edUpdateSelectedLayer() {
3499    if (!_ed.text?.selectedId) return;
3500    const layer = _ed.text.layers.find(l => l.id === _ed.text.selectedId);
3501    if (!layer) return;
3502    layer.color = _ed.text.color;
3503    layer.size = _ed.text.size;
3504    layer.font = _ed.text.font;
3505    _edRenderTextLayers();
3506  }
3507
3508  window.edAddTextLayer = function() {
3509    const txt = document.getElementById('ed-text-input')?.value?.trim();
3510    if (!txt) return toast('Wpisz najpierw treść napisu');
3511    const t = _ed.text || (_ed.text = { layers: [], font: 'Inter', color: '#F
3511FFFFF', size: 36, selectedId: null });
3512    if (t.layers.length >= 5) return toast('Maks 5 napisów');
3513    const id = 'tl-' + Date.now();
3514    t.layers.push({ id, text: txt, x: 50, y: 50, font: t.font, color: t.color, size: t.size });
3515    t.selectedId = id;
3516    document.getElementById('ed-text-input').value = '';
3517    _edRenderTextLayers();
3518  };
3519
3520  function _edRenderTextLayers() {
3521    const frame = document.getElementById('ed-text-frame');
3522    if (!frame) return;
3523    // remove existing
3524    frame.querySelectorAll('.ed-tlayer').forEach(n => n.remove());
3525    const t = _ed.text;
3526    if (!t?.layers) return;
3527    for (const layer of t.layers) {
3528      const el = document.createElement('div');
3529      el.className = 'ed-tlayer' + (layer.id === t.selectedId ? ' selected' : '');
3530      el.dataset.id = layer.id;
3531      el.style.left = layer.x + '%';
3532      el.style.top = layer.y + '%';
3533      el.style.fontFamily = `'${layer.font}', sans-serif`;
3534      el.style.color = layer.color;
3535      el.style.fontSize = layer.size + 'px';
3536      el.style.fontWeight = ['Bebas Neue','Inter','Helvetica'].includes(layer.font) ? '700' : '500';
3537      el.innerHTML = `${layer.text.replace(/</g,'&lt;').replace(/\n/g,'<br>')}<button class="ed-tlayer-del" onclick="event.stopPropagation();edDeleteTextLayer('${layer.id}')"><i class="fas fa-xmark"></i></button>`;
3538      _attachLayerDrag(el, layer);
3539      frame.appendChild(el);
3540    }
3541  }
3542
3543  function _attachLayerDrag(el, layer) {
3544    let dragging = false, startX = 0, startY = 0, startLayerX = 0, startLayerY = 0;
3545    const frame = document.getElementById('ed-text-frame');
3546    el.addEventListener('mousedown', (e) => {
3547      e.stopPropagation();
3548      e.preventDefault();
3549      // Update selection bez full re-render (re-render kasuje el i drag się rwie)
3550      document.querySelectorAll('.ed-tlayer.selected').forEach(n => n.classList.remove('selected'));
3551      el.classList.add('selected');
3552      _ed.text.selectedId = layer.id;
3553      dragging = true;
3554      startX = e.clientX; startY = e.clientY;
3555      startLayerX = layer.x; startLayerY = layer.y;
3556    });
3557    const onMove = (e) => {
3558      if (!dragging) return;
3559      const rect = frame.getBoundingClientRect();
3560      const dx = ((e.clientX - startX) / rect.width) * 100;
3561      const dy = ((e.clientY - startY) / rect.height) * 100;
3562      layer.x = Math.max(0, Math.min(100, startLayerX + dx));
3563      layer.y = Math.max(0, Math.min(100, startLayerY + dy));
3564      el.style.left = layer.x + '%';
3565      el.style.top = layer.y + '%';
3566    };
3567    const onUp = () => { dragging = false; };
3568    document.addEventListener('mousemove', onMove);
3569    document.addEventListener('mouseup', onUp);
3570  }
3571
3572  window.edDeleteTextLayer = function(id) {
3573    if (!_ed.text) return;
3574    _ed.text.layers = _ed.text.layers.filter(l => l.id !== id);
3575    if (_ed.text.selectedId === id) _ed.text.selectedId = null;
3576    _edRenderTextLayers();
3577  };
3578
3579  // ═══ Drag-to-select rectangle na zdjęciu → input dopasowany do obszaru ═══
3580  document.addEventListener('DOMContentLoaded', () => {
3581    const frame = document.getElementById('ed-text-frame');
3582    if (!frame) return;
3583
3584    let dragSel = null;
3585    let selRect = null;
3586
3587    frame.addEventListener('mousedown', (e) => {
3588      // Ignoruj klik na istniejący napis lub input
3589      if (e.target.closest('.ed-tlayer')) return;
3590      if (e.target.closest('.ed-text-inline')) return;
3591      e.preventDefault();
3592      const rect = frame.getBoundingClientRect();
3593      const x = ((e.clientX - rect.left) / rect.width) * 100;
3594      const y = ((e.clientY - rect.top) / rect.height) * 100;
3595      dragSel = { startX: x, startY: y, endX: x, endY: y };
3596      // Pokaż preview rectangle
3597      selRect = document.createElement('div');
3598      selRect.className = 'ed-text-selrect';
3599      _updateSelRect(selRect, dragSel);
3600      frame.appendChild(selRect);
3601    });
3602
3603    document.addEventListener('mousemove', (e) => {
3604      if (!dragSel || !selRect) return;
3605      const rect = frame.getBoundingClientRect();
3606      dragSel.endX = Math.max(0, Math.min(100, ((e.clientX - rect.left) / rect.width) * 100));
3607      dragSel.endY = Math.max(0, Math.min(100, ((e.clientY - rect.top) / rect.height) * 100));
3608      _updateSelRect(selRect, dragSel);
3609    });
3610
3611    document.addEventListener('mouseup', () => {
3612      if (!dragSel) return;
3613      const sel = dragSel; dragSel = null;
3614      if (selRect) { selRect.remove(); selRect = null; }
3615      const w = Math.abs(sel.endX - sel.startX);
3616      const h = Math.abs(sel.endY - sel.startY);
3617      const cx = (sel.startX + sel.endX) / 2;
3618      const cy = (sel.startY + sel.endY) / 2;
3619      // Single click (no drag) → punkt
3620      if (w < 2 && h < 2) {
3621        _edShowInlineInput(cx, cy, null, null);
3622      } else {
3623        // Drag area → wymiary rectangle
3624        _edShowInlineInput(cx, cy, w, h);
3625      }
3626    });
3627  });
3628
3629  function _updateSelRect(el, sel) {
3630    const left = Math.min(sel.startX, sel.endX);
3631    const top = Math.min(sel.startY, sel.endY);
3632    const w = Math.abs(sel.endX - sel.startX);
3633    const h = Math.abs(sel.endY - sel.startY);
3634    el.style.left = left + '%';
3635    el.style.top = top + '%';
3636    el.style.width = w + '%';
3637    el.style.height = h + '%';
3638  }
3639
3640  function _edShowInlineInput(x, y, areaW, areaH) {
3641    const frame = document.getElementById('ed-text-frame');
3642    if (!frame) return;
3643    frame.querySelectorAll('.ed-text-inline').forEach(n => n.remove());
3644
3645    const t = _ed.text || (_ed.text = { layers: [], font: 'Inter', color: '#F
3645FFFFF', size: 36, selectedId: null });
3646    if (t.layers.length >= 5) { toast('Max 5 napisów'); return; }
3647
3648    const inp = document.createElement('input');
3649    inp.type = 'text';
3650    inp.className = 'ed-text-inline';
3651    inp.placeholder = 'wpisz napis i naciśnij Enter…';
3652    inp.style.color = t.color;
3653    inp.style.fontFamily = `'${t.font}', sans-serif`;
3654
3655    if (areaW != null && areaH != null) {
3656      // Drag-area mode — input wypełnia zaznaczony rectangle
3657      const left = x - areaW / 2;
3658      const top = y - areaH / 2;
3659      inp.style.left = left + '%';
3660      inp.style.top = top + '%';
3661      inp.style.width = areaW + '%';
3662      inp.style.height = areaH + '%';
3663      inp.style.minWidth = '120px';
3664      inp.style.minHeight = '40px';
3665      // Auto-fit font size do wysokości obszaru
3666      const frameH = frame.getBoundingClientRect().height;
3667      const fitSize = Math.max(14, Math.min(120, Math.round((areaH / 100) * frameH * 0.6)));
3668      inp.style.fontSize = fitSize + 'px';
3669      t._pendingSize = fitSize;
3670      inp.style.transform = 'none';
3671    } else {
3672      // Punkt — input wycentrowany
3673      inp.style.left = x + '%';
3674      inp.style.top = y + '%';
3675      inp.style.transform = 'translate(-50%, -50%)';
3676      inp.style.fontSize = Math.min(t.size, 32) + 'px';
3677      inp.style.minWidth = '160px';
3678      t._pendingSize = t.size;
3679    }
3680
3681    const commit = () => {
3682      const txt = inp.value.trim();
3683      inp.remove();
3684      if (!txt) return;
3685      const id = 'tl-' + Date.now();
3686      t.layers.push({
3687        id, text: txt, x, y,
3688        font: t.font, color: t.color,
3689        size: t._pendingSize || t.size
3690      });
3691      t.selectedId = id;
3692      _edRenderTextLayers();
3693    };
3694
3695    inp.addEventListener('keydown', (e) => {
3696      e.stopPropagation();
3697      if (e.key === 'Enter') { e.preventDefault(); commit(); }
3698      else if (e.key === 'Escape') { inp.remove(); }
3699    });
3700    inp.addEventListener('blur', commit);
3701    frame.appendChild(inp);
3702    setTimeout(() => inp.focus(), 30);
3703  }
3704
3705  window.edRunTextTransform = async function() {
3706    const layers = _ed.text?.layers || [];
3707    if (!layers.length) { edSetStatus('ed-status-t', 'Zaznacz obszar i wpisz napis', 'err'); return; }
3708    edSetStatus('ed-status-t', 'Generuję profesjonalną typografię...');
3709    edShowLoading('Generowanie profesjonalnej typografii...');
3710    try {
3711      const headers = { 'Content-Type': 'application/json' };
3712      if (S.token) headers['Authorization'] = 'Bearer ' + S.token;
3713      // Wysyłamy JSON z pozycjami napisów (% względem obrazu) — backend renderuje sketch server-side
3714      const r = await fetch(`${API}/api/ecommerce/text-transform`, {
3715        method: 'POST', headers, credentials: 'include',
3716        body: JSON.stringify({
3717          originalImageUrl: _ed.imageUrl || $('ed-img').src,
3718          layers: layers.map(l => ({
3719            text: l.text,
3720            x: l.x, y: l.y,
3721            font: l.font,
3722            color: l.color,
3723            size: l.size
3724          }))
3725        })
3726      });
3727      const data = await r.json();
3728      if (!r.ok || !data.resultUrl) throw new Error(data.error || 'Błąd serwera');
3729      _ed.imageUrl = data.resultUrl;
3730      $('ed-img').src = data.resultUrl;
3731      $('ed-text-img').src = data.resultUrl;
3732      _ed.text.layers = [];
3733      _edRenderTextLayers();
3734      edSetStatus('ed-status-t', 'Gotowe! Możesz dalej edytować lub pobrać.', 'ok');
3735      toast('Profesjonalna typografia wygenerowana ✨');
3736      loadHistory();
3737    } catch (err) {
3738      edSetStatus('ed-status-t', 'Błąd: ' + err.message, 'err');
3739    } finally {
3740      edHideLoading();
3741    }
3742  };
3743
3744  function edShowLoading(txt) {
3745    $('ed-loading-text').textContent = txt || 'Przetwarzanie...';
3746    $('ed-loading').classList.add('show');
3747  }
3748  function edHideLoading() { $('ed-loading').classList.remove('show'); }
3749  function edSetStatus(id, msg, kind) {
3750    const el = $(id); if (!el) return;
3751    el.textContent = msg || '';
3752    el.className = 'ed-status' + (kind === 'ok' ? ' ok' : kind === 'err' ? ' err' : '');
3753  }
3754
3755  // ── Polling: czeka aż task status zmieni się na success/failed ──
3756  async function edPollTask(taskId, { maxWait = 180_000, interval = 2500 } = {}) {
3757    const start = Date.now();
3758    while (Date.now() - start < maxWait) {
3759      await new Promise(r => setTimeout(r, interval));
3760      try {
3761        const res = await api('/api/brand/tasks?limit=30');
3762        const t = (res.tasks || []).find(x => x.taskId === taskId);
3763        if (t?.status === 'success' && t.resultUrl) return t.resultUrl;
3764        if (t?.status === 'failed') throw new Error(t.error || 'Task failed');
3765      } catch (e) {
3766        if (e.message && e.message !== 'HTTP 404') throw e;
3767      }
3768    }
3769    throw new Error('Timeout');
3770  }
3771
3772  // ── AI Edit (nano-banana-edit) ──
3773  window.edRunAiEdit = async function() {
3774    if (!requireLogin('edytować obraz')) return;
3775    const prompt = $('ed-prompt').value.trim();
3776    if (!prompt) return edSetStatus('ed-status-ai', 'Napisz co zmienić', 'err');
3777    if (!_ed.url) return toast('Brak obrazka');
3778    const btn = $('ed-go-ai'); btn.disabled = true;
3779    edShowLoading('Edytuję obraz...');
3780    edSetStatus('ed-status-ai', 'Wysyłam...');
3781    try {
3782      const fd = new FormData();
3783      fd.append('type', 'image');
3784      fd.append('modelName', 'nano-banana-2'); // NB2 router → pixverse (env NB2_PROVIDER)
3785      fd.append('prompt', prompt);
3786      fd.append('generationType', 'reference');
3787      fd.append('modalSource', 'ecommerce');
3788      fd.append('imageUrls', JSON.stringify([_ed.url]));
3789      const r = await fetch(API + '/generate', { method: 'POST', headers: authH(false), credentials: 'include', body: fd });
3790      const d = await r.json();
3791      if (!r.ok) throw new Error(d.error || 'Błąd');
3792      const taskId = d.data?.taskId || d.taskId;
3793      if (!taskId) throw new Error('Brak taskId');
3794      edSetStatus('ed-status-ai', 'Przetwarzanie (15-40s)...');
3795      const resultUrl = await edPollTask(taskId);
3796      _ed.url = resultUrl; _ed.taskId = taskId;
3797      $('ed-img').src = resultUrl;
3798      edSetStatus('ed-status-ai', 'Gotowe!', 'ok');
3799      loadHistory();
3800    } catch(e) { edSetStatus('ed-status-ai', e.message, 'err'); }
3801    finally { btn.disabled = false; edHideLoading(); }
3802  };
3803
3804  // ── Upscale 2× (runway) ──
3805  window.edRunUpscale = async function() {
3806    if (!_ed.url) return toast('Brak obrazka');
3807    const btn = $('ed-go-up'); btn.disabled = true;
3808    edShowLoading('Powiększam...');
3809    edSetStatus('ed-status-up', 'Przetwarzanie...');
3810    try {
3811      const d = await api('/api/studio/upscale-runway', { method: 'POST', body: JSON.stringify({ imageUrl: _ed.url, scale: _ed.scale || 2 }) });
3812      const resultUrl = d.resultUrl || d.url;
3813      if (!resultUrl) throw new Error('Brak wyniku');
3814      _ed.url = resultUrl;
3815      $('ed-img').src = resultUrl;
3816      edSetStatus('ed-status-up', 'Powiększone!', 'ok');
3817      loadHistory();
3818    } catch(e) { edSetStatus('ed-status-up', e.message, 'err'); }
3819    finally { btn.disabled = false; edHideLoading(); }
3820  };
3821
3822  // ── Reframe (luma photon) ──
3823  window.edRunReframe = async function() {
3824    if (!_ed.url) return toast('Brak obrazka');
3825    const btn = $('ed-go-rf'); btn.disabled = true;
3826    const prompt = $('ed-reframe-prompt')?.value?.trim() || '';
3827    edShowLoading('Rozszerzam...');
3828    edSetStatus('ed-status-rf', 'Przetwarzanie...');
3829    try {
3830      const d = await api('/api/studio/reframe', { method: 'POST', body: JSON.stringify({ imageUrl: _ed.url, aspectRatio: _ed.reframeRatio || '1:1', prompt, model: 'photon-flash-1' }) });
3831      const resultUrl = d.resultUrl || d.url;
3832      if (!resultUrl) throw new Error('Brak wyniku');
3833      _ed.url = resultUrl;
3834      $('ed-img').src = resultUrl;
3835      edSetStatus('ed-status-rf', 'Gotowe!', 'ok');
3836      loadHistory();
3837    } catch(e) { edSetStatus('ed-status-rf', e.message, 'err'); }
3838    finally { btn.disabled = false; edHideLoading(); }
3839  };
3840
3841  // ── Camera angle (qwen-edit-multiangle) ──
3842  window.edRunCamera = async function() {
3843    if (!requireLogin('zmienić kąt')) return;
3844    if (!_ed.url) return toast('Brak obrazka');
3845    const az = Math.max(-90, Math.min(90, Math.round(_ed.cam.azimuth)));
3846    const el = Math.max(-1, Math.min(1, Math.round(_ed.cam.elevation)));
3847    const zm = Math.max(0, Math.min(10, Math.round(_ed.cam.zoom)));
3848    const btn = $('ed-go-cam'); btn.disabled = true;
3849    edShowLoading('Zmieniam kąt...');
3850    edSetStatus('ed-status-cam', 'Wysyłam...');
3851    try {
3852      const fd = new FormData();
3853      fd.append('type', 'image');
3854      fd.append('modelName', 'qwen/qwen-edit-multiangle');
3855      fd.append('prompt', '');
3856      fd.append('imageUrls', JSON.stringify([_ed.url]));
3857      fd.append('rotateDegrees', az);
3858      fd.append('verticalTilt', el);
3859      fd.append('moveForward', zm);
3860      fd.append('useWideAngle', 'false');
3861      fd.append('modalSource', 'ecommerce');
3862      const r = await fetch(API + '/generate', { method: 'POST', headers: authH(false), credentials: 'include', body: fd });
3863      const d = await r.json();
3864      if (!r.ok) throw new Error(d.error || 'Błąd');
3865      const taskId = d.data?.taskId || d.taskId;
3866      if (!taskId) throw new Error('Brak taskId');
3867      edSetStatus('ed-status-cam', 'Przetwarzanie...');
3868      const resultUrl = await edPollTask(taskId);
3869      _ed.url = resultUrl; _ed.taskId = taskId;
3870      $('ed-img').src = resultUrl;
3871      edSetStatus('ed-status-cam', 'Gotowe!', 'ok');
3872      loadHistory();
3873    } catch(e) { edSetStatus('ed-status-cam', e.message, 'err'); }
3874    finally { btn.disabled = false; edHideLoading(); }
3875  };
3876
3877  // ── Filters save (canvas → /upload-temp) ──
3878  window.edApplyFiltersAndSave = async function() {
3879    if (!_ed.url) return toast('Brak obrazka');
3880    edShowLoading('Zapisuję...');
3881    edSetStatus('ed-status-f', 'Przetwarzanie...');
3882    try {
3883      // 1) Fetch image bytes (przez proxy jeśli cross-origin)
3884      const isLocal = _ed.url.startsWith('blob:') || _ed.url.startsWith('data:') || _ed.url.startsWith('/');
3885      const fetchUrl = isLocal ? _ed.url : '/api/image-proxy?url=' + encodeURIComponent(_ed.url);
3886      const resp = await fetch(fetchUrl, { credentials: 'include', headers: isLocal ? {} : authH() });
3887      if (!resp.ok) throw new Error('Fetch failed');
3888      const blob = await resp.blob();
3889      const bmpUrl = URL.createObjectURL(blob);
3890      // 2) Load into <img>
3891      const tmp = new Image();
3892      await new Promise((res, rej) => { tmp.onload = res; tmp.onerror = () => rej(new Error('Load failed')); tmp.src = bmpUrl; });
3893      URL.revokeObjectURL(bmpUrl);
3894      // 3) Canvas manipulation (Safari-safe — manual pixel math zamiast ctx.filter)
3895      const c = document.createElement('canvas');
3896      const w = tmp.naturalWidth, h = tmp.naturalHeight;
3897      c.width = w; c.height = h;
3898      const ctx = c.getContext('2d');
3899      ctx.drawImage(tmp, 0, 0);
3900      const f = _ed.filters;
3901      const d = ctx.getImageData(0, 0, w, h).data;
3902      const br = f.brightness / 100, co = f.contrast / 100, sa = f.saturation / 100;
3903      const coF = (259 * (co * 255 + 255)) / (255 * (259 - co * 255 + 255));
3904      for (let i = 0; i < d.length; i += 4) {
3905        let r = d[i], g = d[i + 1], b = d[i + 2];
3906        r *= br; g *= br; b *= br;
3907        r = coF * (r - 128) + 128; g = coF * (g - 128) + 128; b = coF * (b - 128) + 128;
3908        const gray = 0.2126 * r + 0.7152 * g + 0.0722 * b;
3909        r = gray + (r - gray) * sa; g = gray + (g - gray) * sa; b = gray + (b - gray) * sa;
3910        if (f.temperature > 0) { r += f.temperature * 0.5; b -= f.temperature * 0.3; }
3911        else if (f.temperature < 0) { b += Math.abs(f.temperature) * 0.5; r -= Math.abs(f.temperature) * 0.3; }
3912        d[i] = Math.max(0, Math.min(255, r));
3913        d[i + 1] = Math.max(0, Math.min(255, g));
3914        d[i + 2] = Math.max(0, Math.min(255, b));
3915      }
3916      ctx.putImageData(new ImageData(d, w, h), 0, 0);
3917      // 4) toBlob → POST /upload-temp
3918      const outBlob = await new Promise(r => c.toBlob(r, 'image/png'));
3919      if (!outBlob) throw new Error('Canvas blob null');
3920      const fd = new FormData();
3921      fd.append('file', outBlob, 'edited.png');
3922      const up = await fetch(API + '/upload-temp', { method: 'POST', credentials: 'include', headers: authH(false), body: fd });
3923      const upJson = await up.json();
3924      if (!upJson.url) throw new Error('Upload failed');
3925      _ed.url = upJson.url;
3926      _ed.filters = { brightness: 100, contrast: 100, saturation: 100, temperature: 0 };
3927      $('ed-img').src = upJson.url;
3928      $('ed-img').style.filter = 'none';
3929      edRenderPanel();
3930      edSetStatus('ed-status-f', 'Zapisane!', 'ok');
3931    } catch(e) { edSetStatus('ed-status-f', e.message, 'err'); }
3932    finally { edHideLoading(); }
3933  };
3934
3935  window.edDownload = function() {
3936    if (!_ed.url) return toast('Brak obrazka');
3937    const ext = _ed.url.match(/\.([a-z0-9]+)(\?|$)/i)?.[1] || 'jpg';
3938    smartDownload(_ed.url, `chmurka-edit-${Date.now().toString(36)}.${ext}`);
3939  };
3940
3941  window.edAddAsRef = function() {
3942    if (!_ed.url) return toast('Brak obrazka');
3943    if (S.activeRefs.length >= 10) return toast('Max 10 referencji');
3944    if (S.activeRefs.some(r => r.imageUrl === _ed.url)) return toast('Już dodane');
3945    const refId = 'r_' + Math.random().toString(36).slice(2, 10);
3946    const name = 'ref' + (S.activeRefs.length + 1);
3947    S.activeRefs.push({ refId, type: 'product', name, imageUrl: _ed.url, thumbnailUrl: _ed.url, fromHistory: true });
3948    renderRefs(); updateDockHints();
3949    toast('Dodano @' + name);
3950    closeEditor();
3951  };
3952  async function startPolling() {
3953    if (S.polling) return;
3954    S.polling = true;
3955    while (S.pendingTasks.size > 0) {
3956      await new Promise(r => setTimeout(r, 3000));
3957      try {
3958        const res = await api('/api/brand/tasks?limit=50');
3959        const tasks = res.tasks || [];
3960        // Sprawdź każdy pending task — jeśli skończył się (success/failed) → renderuj i usuń z setu
3961        for (const taskId of Array.from(S.pendingTasks)) {
3962          const t = tasks.find(x => x.taskId === taskId);
3963          if (t && (t.status === 'success' || t.status === 'failed')) {
3964            renderHist(t);
3965            S.pendingTasks.delete(taskId);
3966          }
3967        }
3968      } catch(e) { console.warn('poll', e.message); }
3969    }
3970    S.polling = false;
3971  }
3972  async function loadHistory() {
3973    try {
3974      const res = await api('/api/brand/tasks?limit=50');
3975      const tasks = res.tasks || [];
3976      const el = $('hist');
3977      $('hist-count').textContent = tasks.length;
3978      syncMobileHistoryBadge();
3979      if (!tasks.length) {
3980        el.innerHTML = '<div class="empty"><i class="fas fa-images"></i><h4>' + (window.t?window.t('ecom.empty'):'Pusto') + '</h4><p>' + (window.t?window.t('ecom.emptyDesc'):'Twoje generacje pojawią się tutaj.') + '</p></div>';
3981        return;
3982      }
3983      el.innerHTML = '';
3984      let foundPending = false;
3985      tasks.forEach(t => {
3986        renderHist(t);
3987        if (t.status === 'pending' || t.status === 'processing') {
3988          S.pendingTasks.add(t.taskId);
3989          foundPending = true;
3990        }
3991      });
3992      if (foundPending) startPolling();
3993    } catch(e){ console.warn('history', e); }
3994  }
3995
3996  // ═══ DRAG & DROP — asset tile / hist tile / desktop file → dock ═══
3997  // Rejestrujemy w capture:true na document żeby wyprzedzić domyślną obsługę textarea
3998  // (która wklejałaby URL jako tekst). Reagujemy tylko jeśli drop celuje w dock.
3999  (function setupDragDrop() {
4000    const dock = $('dock');
4001    let dragDepth = 0;
4002    const hasDropData = (e) => {
4003      const types = Array.from(e.dataTransfer?.types || []);
4004      return types.includes('application/x-chmurka') || types.includes('Files');
4005    };
4006    const targetsDock = (e) => !!(e.target && dock.contains(e.target));
4007
4008    document.addEventListener('dragenter', (e) => {
4009      if (!hasDropData(e) || !targetsDock(e)) return;
4010      e.preventDefault();
4011      dragDepth++;
4012      dock.classList.add('drag-over');
4013    }, true);
4014    document.addEventListener('dragover', (e) => {
4015      if (!hasDropData(e) || !targetsDock(e)) return;
4016      e.preventDefault();
4017      e.stopPropagation();  // żeby textarea default nie doszedł
4018      e.dataTransfer.dropEffect = 'copy';
4019    }, true);
4020    document.addEventListener('dragleave', (e) => {
4021      if (!targetsDock(e)) return;
4022      dragDepth = Math.max(0, dragDepth - 1);
4023      if (dragDepth === 0) dock.classList.remove('drag-over');
4024    }, true);
4025    document.addEventListener('drop', (e) => {
4026      if (!hasDropData(e) || !targetsDock(e)) return;
4027      e.preventDefault();
4028      e.stopPropagation();
4029      dragDepth = 0;
4030      dock.classList.remove('drag-over');
4031
4032      // 1) Z naszych tile'ów (asset / hist)
4033      const cm = e.dataTransfer.getData('application/x-chmurka');
4034      if (cm) {
4035        try {
4036          const p = JSON.parse(cm);
4037          if (S.activeRefs.length >= 10) return toast('Max 10 referencji');
4038          if (p.kind === 'asset') {
4039            if (S.activeRefs.some(r => r.assetId === p.assetId)) return toast('Już dodane');
4040            S.activeRefs.push({ assetId: p.assetId, name: p.name, imageUrl: p.imageUrl, type: p.type });
4041            renderRefs(); updateDockHints();
4042            toast('Dodano @' + p.name);
4043          } else if (p.kind === 'task') {
4044            if (S.activeRefs.some(r => r.imageUrl === p.imageUrl)) return toast('Już dodane');
4045            const refId = 'r_' + Math.random().toString(36).slice(2, 10);
4046            const name = 'ref' + (S.activeRefs.length + 1);
4047            S.activeRefs.push({ refId, type: 'product', name, imageUrl: p.imageUrl, thumbnailUrl: p.thumbnailUrl || p.imageUrl, fromHistory: true });
4048            renderRefs(); updateDockHints();
4049            toast('Dodano @' + name);
4050          }
4051        } catch (err) { console.warn('drop parse', err); }
4052        return;
4053      }
4054
4055      // 2) Pliki z desktopu
4056      const files = Array.from(e.dataTransfer.files || []).filter(f => f.type.startsWith('image/'));
4057      if (files.length) onDockFiles({ files, value: '' });
4058    }, true);
4059  })();
4060
4061  // ═══ INIT ═══
4062  // SKÓRA 2.0 (Marketing Studio): landing wyleciał, wchodzimy prosto w narzędzie.
4063  // loadShowcase/renderLandingCats/setupRevealOnScroll zostają w pliku jako martwe —
4064  // ich kontenery zniknęły z markupu, więc wywołanie któregokolwiek = TypeError.
4065  (async () => {
4066    if (!(await boot())) {
4067      // Gość: galeria szablonów to teraz strona wejściowa (landing wyleciał) — pokazujemy
4068      // publiczne presety bez logowania. Endpoint jest na optionalAuth, więc 401 nie ma.
4069      loadStudios();
4070      return;
4071    }
4072    loadStudios(); loadAssets(); loadHistory();
4073    loadPricingModal();
4074  })();
4075
4076  // Wczytanie pricing-modal (async) — jak w index.html
4077  let _pricingLoaded = false;
4078  let _pricingLoadingPromise = null;
4079  function loadPricingModal() {
4080    if (_pricingLoaded) return Promise.resolve();
4081    if (_pricingLoadingPromise) return _pricingLoadingPromise;
4082    // Sync globals które pricing-modal czyta
4083    window.userToken = S.token;
4084    window.currentLang = 'pl';
4085    _pricingLoadingPromise = fetch('/components/pricing-modal.html?v=popup20260930a')
4086      .then(r => r.ok ? r.text() : Promise.reject('Failed'))
4087      .then(html => {
4088        const c = document.getElementById('pricing-modal-container');
4089        if (!c) return;
4090        c.innerHTML = html;
4091        // Wykonaj zagnieżdżone 
4091<script> sekwencyjnie + poczekaj aż openPricingModal będzie dostępne
4092        const scripts = Array.from(c.querySelectorAll('script'));
4093        for (const s of scripts) {
4094          const ns = document.createElement('script');
4095          ns.textContent = s.textContent;
4096          document.body.appendChild(ns);
4097        }
4098        _pricingLoaded = true;
4099      })
4100      .catch(e => { _pricingLoadingPromise = null; console.warn('pricing-modal load failed:', e); });
4101    return _pricingLoadingPromise;
4102  }
4103  // Powrót z płatności Revolut (?revolut_purchase=): modal od razu, bo to on pokazuje wynik
4104  if (/[?&]revolut_purchase=/.test(location.search)) loadPricingModal();
4105
4106  // Wrapper który gwarantuje że pricing-modal jest załadowany przed otwarciem
4107  window.ecOpenPricing = async function(reason) {
4108    window.userToken = S.token;
4109    window.currentLang = 'pl';
4110    if (!_pricingLoaded) {
4111      toast('Ładuję cennik…');
4112      await loadPricingModal();
4113    }
4114    if (typeof window.openPricingModal === 'function') {
4115      window.openPricingModal(reason);
4116    } else {
4117      toast('Nie udało się wczytać cennika. Odśwież stronę.', 4000);
4118    }
4119  };
4120
4121  // ═══ Premium scroll-reveal — fade-in sekcji landing ═══
4122  function setupRevealOnScroll() {
4123    const sections = document.querySelectorAll('#view-landing .land-section, #view-landing .land-bottom-cta');
4124    sections.forEach((sec, i) => {
4125      sec.classList.add('reveal');
4126      // Też dzieci sekcji
4127      const children = sec.querySelectorAll('.land-section-head, .land-grid > *, .land-features > *, .land-steps > *, .land-process > *, .land-process-prompt');
4128      children.forEach((c, j) => {
4129        c.classList.add('reveal');
4130        if (j < 4) c.classList.add('reveal-delay-' + (j + 1));
4131      });
4132    });
4133
4134    if ('IntersectionObserver' in window) {
4135      const io = new IntersectionObserver((entries) => {
4136        entries.forEach(en => {
4137          if (en.isIntersecting) {
4138            en.target.classList.add('in');
4139            io.unobserve(en.target);
4140          }
4141        });
4142      }, { threshold: 0.12, rootMargin: '0px 0px -10% 0px' });
4143      document.querySelectorAll('#view-landing .reveal').forEach(el => io.observe(el));
4144    } else {
4145      document.querySelectorAll('.reveal').forEach(el => el.classList.add('in'));
4146    }
4147  }
4148
4149  // ═══ LANDING — chip'y kategorii ═══
4150  function renderLandingCats() {
4151    const cats = [
4152      { v: 'fashion', label: '👗 Moda' },
4153      { v: 'beauty', label: '💄 Uroda' },
4154      { v: 'supplements', label: '💊 Suplementy' },
4155      { v: 'home', label: '🏠 Dom' },
4156      { v: 'jewelry', label: '💎 Biżuteria' },
4157      { v: 'food', label: '🍽️ Jedzenie' },
4158      { v: 'coffee', label: '☕ Kawa/Herbata' },
4159      { v: 'candles', label: '🕯️ Świeczki' },
4160      { v: 'sport', label: '💪 Sport' },
4161      { v: 'kids', label: '🧸 Dzieci' },
4162      { v: 'pets', label: '🐾 Zwierzęta' },
4163      { v: 'alcohol', label: '🍷 Alkohole' },
4164      { v: 'garden', label: '🌿 Ogród' },
4165      { v: 'handmade', label: '🧶 Handmade' },
4166      { v: 'other', label: '✨ Inne' },
4167    ];
4168    const host = $('land-cats');
4169    if (!host) return;
4170    const _ecEN = location.hostname.indexOf('pixcloud') !== -1;
4171    const _catEN = { fashion:'👗 Fashion', beauty:'💄 Beauty', supplements:'💊 Supplements', home:'🏠 Home', jewelry:'💎 Jewelry', food:'🍽️ Food', coffee:'☕ Coffee/Tea', candles:'🕯️ Candles', sport:'💪 Sports', kids:'🧸 Kids', pets:'🐾 Pets', alcohol:'🍷 Alcohol', garden:'🌿 Garden', handmade:'🧶 Handmade', other:'✨ Other' };
4172    host.innerHTML = cats.map(c =>
4173      `<button class="cat" type="button" onclick="useShowcaseCategory('${c.v}')" style="cursor:pointer">${_ecEN && _catEN[c.v] ? _catEN[c.v] : c.label}</button>`
4174    ).join('') + `<button class="cat cta-request" type="button" onclick="openCategoryRequestModal()" style="cursor:pointer"><i class="fas fa-plus" style="font-size:11px;margin-right:4px"></i>${_ecEN ? 'Missing yours? Tell us' : 'Brak Twojej? Powiedz nam'}</button>`;
4175  }
4176  window.useShowcaseCategory = function(cat) {
4177    sessionStorage.setItem('preselectCategory', cat);
4178    go('create');
4179  };
4180
4181  // ═══ MOSTEK DLA MODUŁU AGENTA (SKÓRA 2.0) ═══
4182  // Moduł agenta żyje w OSOBNEJ IIFE (świadomie: zero ryzyka dla działającej logiki),
4183  // a potrzebuje auth, toastów i wpięcia zadań w historię. Wystawiamy minimum, nie całość.
4184  window._mk = {
4185    api, authH, toast,
4186    refy: () => S.activeRefs,
4187    czekajace: S.pendingTasks,
4188    addPending, bumpCount, startPolling, loadHistory, loadStudios,
4189    studio: () => S.currentStudio,
4190    token: () => S.token,
4191  };
4192})();
4193
4194</script>
vendor: 1 bytes, line 4194
4194
4195<script>
4196/* ═══ DOCK 2.0: tryb Obraz/Wideo + RURA WIDEO (Motion Design) ═══
4197   Osobna IIFE (świadomie poza główną — zero ryzyka dla działającej logiki obrazkowej).
4198   Tryb wideo: preset z videoRecipe → klatki gpt-image (i2i z hero, jak zmierzony flow
4199   ownera na „gumo") → akceptacja usera → omni reference-to-video 10 s z klatkami jako
4200   referencjami. Każdy krok to zwykły task /generate: normalne opłaty, normalne zwroty,
4201   wszystko widać w Historii. Zero nowej logiki kredytowej.                            */
4202(function(){
4203  const $ = (id) => document.getElementById(id);
4204  const tt = (klucz, zapas) => (typeof window.t === 'function' && window.t(klucz)) || zapas;
4205
4206  // ── OCHRONA MODALI PRZED PRZYPADKOWYM ZAMKNIĘCIEM (owner 25.08) ──
4207  // Zaznaczanie tekstu w polu karty kończące się NAD TŁEM przeglądarka liczy jako
4208  // klik w tło (target kliknięcia = wspólny przodek press/release) i modal znikał z całą
4209  // robotą usera. Reguła: zamknięcie przez tło TYLKO gdy naciśnięcie I puszczenie
4210  // były na samym tle. Capture-phase łyka fałszywy klik zanim dotrze do handlerów
4211  // wszystkich modali (nowych i starych) — jedna poprawka chroni całą stronę.
4212  let _mkPress = null;
4213  document.addEventListener('pointerdown', (e) => { _mkPress = e.target; }, true);
4214  document.addEventListener('click', (e) => {
4215    const t = e.target;
4216    const czyTlo = t && t.classList && (t.classList.contains('backdrop') || t.classList.contains('picker-backdrop') || t.classList.contains('ed-backdrop') || t.classList.contains('m-history-backdrop'));
4217    if (!czyTlo) return;
4218    if (_mkPress === t) return;   // prawdziwy klik w tło — przechodzi
4219    e.stopPropagation();          // zaznaczenie puszczone nad tłem — połknięte
4220  }, true);
4221
4222  window._mkTryb = 'image';
4223  window.mkSetTryb = function(tryb, studio){
4224    // Podpowiedź per szablon: user od razu wie, czy dodać zdjęcie produktu, czy
4225    // wystarczą hasła/logo. Działa też przy zmianie szablonu w obrębie trybu wideo.
4226    const hintSzablonu = () => {
4227      if (tryb !== 'video') return;
4228      const bezProduktu = studio && studio.videoRecipe && studio.videoRecipe.bezProduktu;
4229      // Slot „Produkt" zmienia znaczenie: szablony typograficzne biorą tu LOGO.
4230      const slotP = document.querySelector('.mk-slot[onclick*="product"] .mk-slot-label');
4231      if (slotP) slotP.textContent = bezProduktu ? tt('ecommerce.dock.logo_btn', 'Logo (opcja)') : tt('ecommerce.dock.product_btn', 'Produkt');
4232      if (studio && studio.videoRecipe) {
4233        mkHint(bezProduktu
4234          ? tt('ecommerce.wideo.hintBez', 'Wpisz w polu markę i hasła, opcjonalnie dodaj logo w slocie Logo, potem kliknij Generuj.')
4235          : tt('ecommerce.wideo.hintProdukt', 'Dodaj zdjęcie produktu z Biblioteki, dopisz w polu na czym Ci zależy, potem kliknij Generuj.'));
4236      } else mkHint(tt('ecommerce.wideo.pickTemplate', 'Wybierz szablon wideo z galerii.'));
4237    };
4238    // Pigułki obrazkowe nie mają sensu w rurze wideo — wisiały i myliły („przyciski
4239    // nie znikają" — owner). Chowamy z pamięcią poprzedniego stanu (bulk sam steruje
4240    // swoją widocznością, więc przy powrocie oddajemy mu jego wartość).
4241    const przelacz = (id, pokaz) => {
4242      const el = $(id); if (!el) return;
4243      if (!pokaz) { if (el.dataset.mkPrev === undefined) el.dataset.mkPrev = el.style.display || ''; el.style.display = 'none'; }
4244      else if (el.dataset.mkPrev !== undefined) { el.style.display = el.dataset.mkPrev; delete el.dataset.mkPrev; }
4245    };
4246    if (tryb === window._mkTryb) { hintSzablonu(); return; }
4247    window._mkTryb = tryb;
4248    for (const id of ['pose-toggle', 'bulk-toggle', 'add-model-btn']) przelacz(id, tryb !== 'video');
4249    $('mk-mode-image')?.classList.toggle('on', tryb === 'image');
4250    $('mk-mode-video')?.classList.toggle('on', tryb === 'video');
4251    const gen = $('btn-gen');
4252    const ratioPill = $('ratio-label') && $('ratio-label').closest('.pill');
4253    if (tryb === 'video') {
4254      // Generuj w trybie wideo prowadzi do rury — NIE do obrazkowego doGenerate.
4255      if (gen) { gen.disabled = false; gen.onclick = window.mkGenerujWideo; }
4256      // Wideo robimy WYŁĄCZNIE w 9:16 albo 16:9 (limit silnika) — chip proporcji
4257      // przełącza tylko te dwie, zamiast pełnej karuzeli obrazkowej.
4258      if ($('ratio-label')) $('ratio-label').textContent = '9:16';
4259      if (ratioPill) ratioPill.onclick = function(){
4260        const l = $('ratio-label');
4261        if (l) l.textContent = l.textContent.trim() === '9:16' ? '16:9' : '9:16';
4262      };
4263      hintSzablonu();
4264      const c = $('cost'); if (c) c.textContent = tt('ecommerce.wideo.cost', '5x10 + 25 kr');
4265      // Brief zamiast promptu: w trybie wideo pole nie opisuje sceny (scenę ma szablon),
4266      // tylko karmi agenta — placeholder mówi wprost, co tu wpisać.
4267      const inp = $('p-input');
4268      if (inp) inp.placeholder = tt('ecommerce.dock.placeholderVideo', 'Napisz, co to za produkt i co podkreślić, a ja ułożę teksty reklamy...');
4269    } else {
4270      if (gen) { gen.disabled = false; gen.onclick = () => window.doGenerate(); }
4271      if (ratioPill) ratioPill.onclick = () => window.cycleRatio();
4272      mkHint(null);
4273      const inp = $('p-input');
4274      if (inp) inp.placeholder = tt('ecommerce.dock.placeholder', 'Opisz scenę, albo wpisz @ żeby dodać produkt...');
4275    }
4276    // Tryb dotyczy TYLKO docka — galerię filtruje mkFiltruj (Wszystko/Obrazy/Filmy),
4277    // więc przełączenie trybu NIE przeładowuje mozaiki (klik w kafel by nią migał).
4278  };
4279
4280  // ── FILTR GALERII: Wszystko / Obrazy / Filmy ──
4281  window._mkFiltr = 'all';
4282  window.mkFiltruj = function(f){
4283    if (f === window._mkFiltr) return;
4284    window._mkFiltr = f;
4285    document.querySelectorAll('.mk-filtr button').forEach(b => b.classList.toggle('on', b.dataset.f === f));
4286    try { window._mk && window._mk.loadStudios && window._mk.loadStudios(); } catch(_) {}
4287  };
4288
4289  function mkHint(tekst) {
4290    const hint = $('dock-hint'), ht = $('dock-hint-text');
4291    if (!hint || !ht) return;
4292    if (tekst) { ht.textContent = tekst; hint.style.display = 'block'; }
4293    else hint.style.display = 'none';
4294  }
4295
4296  // ── RURA ──────────────────────────────────────────────────────────────────
4297  let mkWTrakcie = false;
4298
4299  async function generacja(cialo, zrodlo) {
4300    const wyslij = async (src) => {
4301      const fd = new FormData();
4302      for (const [k, v] of Object.entries(cialo)) fd.append(k, v);
4303      fd.append('modalSource', src);
4304      const r = await fetch('/generate', { method: 'POST', body: fd, headers: window._mk.authH(false), credentials: 'include' });
4305      const d = await r.json();
4306      return { ok: r.ok, d };
4307    };
4308    let w = await wyslij(zrodlo || 'ecommerce');
4309    // Serwer sprzed restartu nie zna jeszcze 'ecommerce-klatka' (enum) — wtedy klatka
4310    // idzie po staremu jako 'ecommerce'. Po deployu fallback przestaje się odpalać sam.
4311    if (!w.ok && zrodlo === 'ecommerce-klatka') w = await wyslij('ecommerce');
4312    if (!w.ok) throw new Error(w.d.error || 'generacja odrzucona');
4313    return (w.d.data && w.d.data.taskId) || w.d.taskId;
4314  }
4315
4316  // ── BRAMKA WIERNOŚCI ──────────────────────────────────────────────────────
4317  // Po każdej klatce wizja sprawdza: tylko teksty ze specyfikacji + wierny produkt.
4318  // Nie gra → JEDNA powtórka z wykrytą wadą dopisaną do prompta. Kontrola nigdy
4319  // nie blokuje (błąd = przepuść) i działa tylko przy bramkaWiernosci !== false.
4320  async function mkKontrola(klatkaUrl, produktUrl, teksty, scena, powtorzenia) {
4321    try {
4322      const r = await fetch('/api/marketing/kontrola', {
4323        method: 'POST', credentials: 'include',
4324        headers: Object.assign({ 'Content-Type': 'application/json' }, window._mk.authH()),
4325        body: JSON.stringify({ klatka: klatkaUrl, produkt: produktUrl || '', teksty, scena, powtorzenia: !!powtorzenia }),
4326      });
4327      if (!r.ok) return { ok: true };
4328      return await r.json();
4329    } catch (_) { return { ok: true }; }
4330  }
4331
4332  // Teksty, które MAJĄ prawo być na tej klatce = wartości pól użytych w jej promptcie.
4333  function mkTekstyKlatki(k, pola, d, przepis) {
4334    // tekstyNarastaja (np. Stemple): kompozycja SKŁADA SIĘ plansza po planszy, więc
4335    // teksty WCZEŚNIEJSZYCH plansz legalnie stoją na późniejszych — lista dozwolonych
4336    // narasta razem z filmem (bez tego bramka zgłaszała „KONKRET, którego nie ma
4337    // w specyfikacji" na planszy 2). Szablony o rozłącznych planszach (Karta Tech)
4338    // zostają przy liście per plansza.
4339    const klatki = (przepis && przepis.klatki) || [];
4340    const idx = klatki.indexOf(k);
4341    const zakres = (przepis && przepis.tekstyNarastaja && idx >= 0) ? klatki.slice(0, idx + 1) : [k];
4342    const teksty = [];
4343    for (const p of pola) {
4344      // Pola OPISOWE (kolory/tła/rekwizyty/lokacje) i pola-stery NIE są napisami do
4345      // wyrenderowania — trafiały na listę dozwolonych tekstów i lądowały NA PLAKACIE
4346      // („black cyan", „deep crimson and ink black"). Decyduje JAWNA flaga `opisowe`
4347      // (heurystyka po treści opisu przepuszczała pola bez markera „PO ANGIELSKU").
4348      if (p.opisowe === true || p.tylkoDlaAgenta || /PO ANGIELSKU/i.test(p.opis || '')) continue;
4349      if (d[p.id] && !teksty.includes(d[p.id]) && zakres.some(kl => (kl.prompt || '').includes('{' + p.id + '}'))) teksty.push(d[p.id]);
4350    }
4351    for (const kl of zakres) {
4352      const stale = (kl.prompt || '').match(/fixed label "([^"]+)"/);   // np. "SKALA 1:1"
4353      if (stale && !teksty.includes(stale[1])) teksty.push(stale[1]);
4354    }
4355    return teksty;
4356  }
4357
4358  async function czekaj(taskId, limitMs) {
4359    const start = Date.now();
4360    for (;;) {
4361      if (Date.now() - start > limitMs) throw new Error('timeout');
4362      await new Promise(r => setTimeout(r, 4000));
4363      try {
4364        const r = await fetch('/studio/nodes/task/' + encodeURIComponent(taskId), { headers: window._mk.authH(), credentials: 'include' });
4365        if (!r.ok) continue;
4366        const t = await r.json();
4367        if (t.status === 'success' && t.resultUrl) return t.resultUrl;
4368        if (t.status === 'failed') throw new Error(t.failedReason || 'frame_failed');
4369      } catch (e) { if (e.message !== 'Failed to fetch') throw e; }
4370    }
4371  }
4372
4373  window.mkGenerujWideo = async function(){
4374    if (mkWTrakcie) return;
4375    const S = window._mk;
4376    const studio = S.studio && S.studio();
4377    const przepis = studio && studio.videoRecipe;
4378    if (!przepis) { S.toast(tt('ecommerce.wideo.pickTemplate', 'Wybierz szablon wideo z galerii.')); return; }
4379    // Szablony bezProduktu: KAŻDY dodany obrazek traktujemy jako LOGO (user wrzucał
4380    // logo, a rura filtrowała po type==='product' i logo NIGDY nie dolatywało — refs:0
4381    // w każdym runie typograficznym 25.08).
4382    const produkt = (S.refy() || []).find(r => /^https?:\/\//.test(r.imageUrl || '') && (r.type === 'product' || przepis.bezProduktu));
4383    // Szablony typograficzne (bezProduktu) nie potrzebują zdjęcia — klatki to plansze.
4384    if (!produkt && !przepis.bezProduktu) { S.toast(tt('ecommerce.wideo.needProduct', 'Dodaj produkt z Biblioteki, żeby zrobić wideo.')); return; }
4385
4386    mkWTrakcie = true;
4387    const gen = $('btn-gen');
4388    if (gen) { gen.disabled = true; gen.innerHTML = '<i class="fas fa-spinner fa-spin"></i>'; }
4389    try {
4390      // KARTA REŻYSERA: agent dopasowuje hasła/CTA/tło do produktu; user może poprawić.
4391      // Endpoint nigdy nie blokuje (błąd = domyślne z szablonu + ręczna edycja), więc
4392      // rura działa także na serwerze sprzed restartu.
4393      // Pola karty DEKLARUJE SZABLON (videoRecipe.pola) — różne szablony, różne pola,
4394      // ten sam kod. Brak deklaracji = standardowa czwórka (stare przepisy).
4395      const POLA_STD = [
4396        { id: 'haslo1', etykieta: { pl: 'Hasło, linia 1' } }, { id: 'haslo2', etykieta: { pl: 'Hasło, linia 2' } },
4397        { id: 'cta', etykieta: { pl: 'Wezwanie do działania' } }, { id: 'tlo', etykieta: { pl: 'Tło sceny (po angielsku)' } },
4398      ];
4399      const pola = (Array.isArray(przepis.pola) && przepis.pola.length ? przepis.pola : POLA_STD);
4400      const domyslne = {};
4401      for (const p of pola) domyslne[p.id] = (przepis.domyslne || {})[p.id] || '';
4402      let propozycja = domyslne, zFallbacku = true;
4403      try {
4404        const r = await fetch('/api/marketing/karta', {
4405          method: 'POST', credentials: 'include',
4406          headers: Object.assign({ 'Content-Type': 'application/json' }, window._mk.authH()),
4407          body: JSON.stringify({
4408            lang: (document.documentElement.lang || 'pl').slice(0, 2),
4409            produktNazwa: (produkt && produkt.name) || '', produktOpis: (produkt && produkt.description) || '',
4410            produktImageUrl: (produkt && produkt.imageUrl) || '',   // oczy agenta: wizja gdy opis pusty
4411            stylOpis: przepis.stylOpis || '', extra: ($('p-input') && $('p-input').value || '').slice(0, 500),
4412            pola, domyslne,
4413          }),
4414        });
4415        if (r.ok) { const j = await r.json(); if (j && j.karta) { propozycja = Object.assign({}, domyslne, j.karta); zFallbacku = !!j.fallback; } }
4416      } catch (_) {}
4417      const d = await mkKarta(propozycja, zFallbacku, pola);
4418      if (!d) return;   // user zamknął kartę — nic nie wygenerowano, nic nie pobrano
4419
4420      // Proporcje z chipa (w trybie wideo tylko 9:16 albo 16:9 — limit silnika).
4421      const aspekt = ($('ratio-label')?.textContent || '').trim() === '16:9' ? '16:9' : '9:16';
4422      const podmien = (p) => pola.reduce(
4423        (tekst, pl) => tekst.replace(new RegExp('\\{' + pl.id + '\\}', 'g'), d[pl.id] || ''),
4424        p.replace(/\{product\}/g, 'product')
4425      // Przepisy pisane pod pion — przy 16:9 podmieniamy kadrowanie w promptach.
4426      ).replace(/Vertical 9:16 framing/g, aspekt === '16:9' ? 'Horizontal 16:9 framing' : 'Vertical 9:16 framing')
4427       .replace(/vertical 9:16/g, aspekt === '16:9' ? 'horizontal 16:9' : 'vertical 9:16');
4428      // Model klatek WPROST z przepisu (pełne ID!). UWAGA: krótkie 'gpt-image-2' to w
4429      // backendzie RunBlob BEZ 9:16/16:9 — dlatego zero skracania po '/'; Pro=pixverse.
4430      const model = przepis.modelKlatek || 'pixverse/gpt-image-2';
4431
4432      // BUDOWANIE WSTECZNE (metoda ownera 25.08): dla szablonów plakatowych zamiast
4433      // 4 plansz robimy JEDNĄ — gotowy projekt — a omni go SKŁADA od pustego tła,
4434      // kończąc dokładnie na nim. Krótki polski prompt bije angielskie eseje (zmierzone).
4435      // 1 klatka + film = 35 kr zamiast ~90, zero przenoszenia tekstu między planszami.
4436      const klatkiWszystkie = przepis.klatki || [];
4437      const budowanie = przepis.budowanieWsteczne === true;
4438      const klatki = budowanie
4439        ? [klatkiWszystkie.find(k => k.rola === (przepis.klatkaFinalowa || 'final')) || klatkiWszystkie[klatkiWszystkie.length - 1] || klatkiWszystkie[0]].filter(Boolean)
4440        : klatkiWszystkie;
4441      const hero = klatki.find(k => k.rola === 'hero') || klatki[0];
4442      const wyniki = {};
4443      mkHint(tt('ecommerce.wideo.frames', 'Klatka {n} z {total}...').replace('{n}', 1).replace('{total}', klatki.length));
4444      // Klatki NIE wchodzą do historii (decyzja ownera) — user śledzi je w liczniku
4445      // w docku, a w historii ląduje dopiero finalny film.
4446      // INWENTARZ TEKSTÓW (25.08): silniki obrazkowe renderują frazy z prompta (nazwy
4447      // kolorów „black"/„orange" na plakacie). Na KOŃCU prompta każdej klatki ląduje
4448      // wyliczony automatycznie KOMPLETNY spis napisów tej klatki — koniec zgadywania.
4449      const inwentarz = (k) => {
4450        const t = mkTekstyKlatki(k, pola, d, przepis);
4451        return '\nTEXT INVENTORY — apart from the product\'s own label, the COMPLETE list of every text in this image'
4452          + (przepis.powtorzeniaOk ? ' (each may repeat along its marquee row)' : '')
4453          + ': ' + (t.length ? t.map(x => '"' + x + '"').join(', ') : 'NONE — this image contains NO readable text at all')
4454          + '. Nothing else is written anywhere.';
4455      };
4456      const heroCialo = { type: 'image', modelName: model, prompt: podmien(hero.prompt) + inwentarz(hero), aspectRatio: aspekt, detail: 'standard', resolution: '1440p' };
4457      if (produkt) heroCialo.imageUrls = JSON.stringify([produkt.imageUrl]);   // bezProduktu → czyste t2i
4458      const heroTask = await generacja(heroCialo, 'ecommerce-klatka');
4459      wyniki[hero.rola] = await czekaj(heroTask, 8 * 60_000);
4460
4461      // 2. Pozostałe klatki, jedna po drugiej. Referencje HYBRYDOWO (decyzja 25.08):
4462      // [poprzednia klatka + hero + oryginalne zdjęcie usera] — poprzednia daje ciągłość
4463      // scenografii między sąsiadami (omni interpoluje właśnie między nimi), hero kotwiczy
4464      // serię przed dryfem jak w głuchym telefonie, oryginał pilnuje etykiety.
4465      const reszta = klatki.filter(k => k !== hero);
4466      let poprzednia = wyniki[hero.rola];
4467      const wady = {};   // rola -> problemy, których bramka NIE zdołała usunąć (jawne w akceptacji)
4468      // Hero też przechodzi bramkę (to kotwica całej serii — jego wada dziedziczy się dalej).
4469      if (przepis.bramkaWiernosci !== false) {
4470        const b = await mkBramka(wyniki[hero.rola], heroCialo, produkt && produkt.imageUrl, mkTekstyKlatki(hero, pola, d, przepis), hero.rola, przepis.powtorzeniaOk);
4471        wyniki[hero.rola] = b.url; if (b.wada) wady[hero.rola] = b.wada;
4472        poprzednia = wyniki[hero.rola];
4473      }
4474      for (let i = 0; i < reszta.length; i++) {
4475        const k = reszta[i];
4476        mkHint(tt('ecommerce.wideo.frames', 'Klatka {n} z {total}...').replace('{n}', i + 2).replace('{total}', klatki.length));
4477        const refy = k.uzyjHeroJakoRef
4478          ? [...new Set([poprzednia, wyniki[hero.rola], produkt && produkt.imageUrl].filter(Boolean))]
4479          : [produkt && produkt.imageUrl].filter(Boolean);
4480        const cialo = { type: 'image', modelName: model, prompt: podmien(k.prompt) + inwentarz(k), aspectRatio: aspekt, detail: 'standard', resolution: '1440p' };
4481        if (refy.length) cialo.imageUrls = JSON.stringify(refy);
4482        const tid = await generacja(cialo, 'ecommerce-klatka');
4483        wyniki[k.rola] = await czekaj(tid, 8 * 60_000);
4484        // Bramka wierności 2.0: kontrola → naprawa → druga kontrola → ewentualna JAWNA wada.
4485        // k.produktUkryty (np. zamknięta paczka w Odpakowaniu): klatka celowo NIE pokazuje
4486        // produktu — bramka sprawdza wtedy tylko teksty, bez porównania z referencją.
4487        if (przepis.bramkaWiernosci !== false) {
4488          const b = await mkBramka(wyniki[k.rola], cialo, !k.produktUkryty && produkt && produkt.imageUrl, mkTekstyKlatki(k, pola, d, przepis), k.rola, przepis.powtorzeniaOk);
4489          wyniki[k.rola] = b.url; if (b.wada) wady[k.rola] = b.wada;
4490        }
4491        poprzednia = wyniki[k.rola];
4492      }
4493
4494      // 3. Akceptacja usera — klatki tanie, wideo drogie, więc próg PRZED animacją
4495      mkHint(null);
4496      const kolejnosc = klatki.map(k => k.rola);
4497      const zgoda = await mkAkceptacja(kolejnosc.map(r => wyniki[r]), kolejnosc.map(r => wady[r] || null));
4498      if (!zgoda) { S.toast(tt('btn.cancel', 'Anulowano')); return; }
4499
4500      // 4. Animacja: omni reference-to-video z klatkami jako referencjami.
4501      // Przy budowaniu wstecznym leci krótki polski prompt składania zamiast reżyserii scen.
4502      const BUDOWANIE_PROMPT = 'stwórz motion design tego projektu: film zaczyna się od samego pustego tła, bez napisów i bez logo, a potem wszystkie elementy pojawiają się po kolei od góry do dołu, wbijane jak pieczątki, bez żadnych dodatkowych elementów spoza referencji. Na końcu film wygląda IDENTYCZNIE jak obrazek referencyjny, co do litery i co do układu. Żaden napis nie zmienia treści ani pisowni.';
4503      const wideoTask = await generacja({
4504        type: 'video', modelName: przepis.silnik || 'omni-flash-reference-to-video',
4505        prompt: budowanie ? (przepis.animacjaBudowanie || BUDOWANIE_PROMPT) : podmien(przepis.animacja || ''),
4506        aspectRatio: aspekt, duration: '10',   // ŻELAZNA ZASADA ownera (25.08): wideo ZAWSZE 10 s
4507        imageUrls: JSON.stringify(kolejnosc.map(r => wyniki[r])),
4508      });
4509      // Film — jedyny wpis w historii z tej rury. czekajace.add() jest KLUCZOWE:
4510      // startPolling kręci pętlę tylko póki ten zbiór jest niepusty; bez wpisu kafel
4511      // wisiał ze spinnerem na zawsze (nikt nie odpytywał backendu o status).
4512      S.addPending(wideoTask); S.czekajace.add(wideoTask); S.bumpCount(1); S.startPolling();
4513      S.toast(tt('ecommerce.wideo.queued', 'Animacja ruszyła. Wynik pojawi się w Historii.'), 4000);
4514    } catch (e) {
4515      console.warn('[mk-wideo]', e);
4516      S.toast(e.message === 'frame_failed'
4517        ? tt('ecommerce.wideo.frameFail', 'Klatka nie wyszła. Kredyty wróciły, spróbuj ponownie.')
4518        : ('Wideo: ' + e.message), 4500);
4519    } finally {
4520      mkWTrakcie = false;
4521      if (gen) { gen.disabled = false; gen.innerHTML = '<i class="fas fa-arrow-up"></i>'; }
4522    }
4523  };
4524
4525  // Karta reżysera: pola renderowane Z DEKLARACJI SZABLONU (etykiety w 3 językach
4526  // siedzą w przepisie). null = user zrezygnował. Klasy .backdrop/.modal → skóra.
4527  function mkKarta(wart, zFallbacku, pola) {
4528    return new Promise((oddaj) => {
4529      const lang = (document.documentElement.lang || 'pl').slice(0, 2);
4530      const el = document.createElement('div');
4531      el.className = 'backdrop show';
4532      el.style.zIndex = '260';
4533      const wiersz = (p) => {
4534        const et = (p.etykieta && (p.etykieta[lang] || p.etykieta.pl)) || p.id;
4535        return '<label class="fld-label">' + et + (p.wymagane ? ' <span style="color:var(--accent)">*</span>' : '') + '</label>'
4536          + '<input type="text" class="fld" id="mk-k-' + p.id + '" maxlength="80" data-wymagane="' + (p.wymagane ? '1' : '') + '" value="' + String(wart[p.id] || '').replace(/"/g, '&quot;') + '">';
4537      };
4538      el.innerHTML = '<div class="modal" style="max-width:460px">'
4539        + '<h3>' + tt('ecommerce.karta.title', 'Karta reżysera') + '</h3>'
4540        + '<p>' + (zFallbacku ? tt('ecommerce.karta.subFallback', 'Wpisz teksty pod swój produkt.') : tt('ecommerce.karta.sub', 'AI dopasował teksty do Twojego produktu. Popraw co chcesz i klikaj Start.')) + '</p>'
4541        + (wart.nastroj ? '<p style="color:var(--accent);font-size:12px;margin-top:-6px"><i class="fas fa-wand-magic-sparkles" style="margin-right:6px"></i>' + wart.nastroj + '</p>' : '')
4542        + pola.map(wiersz).join('')
4543        + '<div class="m-actions">'
4544        + '<button class="btn" data-mk="nie">
4544' + tt('btn.cancel', 'Anuluj') + '</button>'
4545        + '<button class="btn btn-pr" data-mk="tak"><i class="fas fa-arrow-right" style="margin-right:6px"></i>' + tt('ecommerce.karta.start', 'Start') + '</button>'
4546        + '</div></div>';
4547      el.addEventListener('click', (e) => {
4548        const mk = e.target.closest && e.target.closest('[data-mk]');
4549        if (!mk) { if (e.target === el) { el.remove(); oddaj(null); } return; }
4550        if (mk.dataset.mk !== 'tak') { el.remove(); oddaj(null); return; }
4551        const w = {};
4552        for (const p of pola) w[p.id] = (el.querySelector('#mk-k-' + p.id)?.value || '').trim();
4553        // Pola wymagane (np. marka do logo) muszą być wypełnione PRZED wydaniem grosza.
4554        const brak = pola.find(p => p.wymagane && !w[p.id]);
4555        if (brak) {
4556          const inp = el.querySelector('#mk-k-' + brak.id);
4557          if (inp) { inp.style.borderColor = 'var(--danger)'; inp.focus(); }
4558          return;   // karta zostaje otwarta
4559        }
4560        el.remove(); oddaj(w);
4561      });
4562      document.body.appendChild(el);
4563    });
4564  }
4565
4566  // Prosty overlay akceptacji (istniejące klasy .backdrop/.modal → dziedziczy skórę)
4567  // ── BRAMKA 2.0 (25.08): kontrola → naprawa → PONOWNA kontrola → ewentualna jawna wada.
4568  // Błąd TYLKO tekstowy (zgubione Ć w „LEKKOŚĆ") naprawiamy CHIRURGICZNIE modelem
4569  // edycyjnym na gotowej klatce (kompozycja zostaje 1:1, taniej niż losowanie od nowa);
4570  // błąd produktu/kompozycji = pełna regeneracja z opisem wady. Jeśli po poprawce wada
4571  // została — NIE ukrywamy jej: klatka dostaje czerwone ostrzeżenie w oknie akceptacji.
4572  async function mkBramka(url, cialo, produktUrl, teksty, scena, powtorzenia) {
4573    const w = await mkKontrola(url, produktUrl, teksty, scena, powtorzenia);
4574    if (!w || w.ok !== false || !(w.problemy && w.problemy.length)) return { url, wada: null };
4575    // Filtr halucynacji kontrolera: „zawiera napis 'X'... powinno być 'X'" (identyczne
4576    // brzmienia) to NIE problem — 25.08 taki werdykt wysłał IDEALNĄ klatkę do naprawy.
4577    w.problemy = w.problemy.filter(p => {
4578      const m = p.match(/["']([^"']+)["'][^"']*?["']([^"']+)["']/);
4579      return !(m && m[1].trim().toLowerCase() === m[2].trim().toLowerCase());
4580    });
4581    if (!w.problemy.length) return { url, wada: null };
4582    mkHint(tt('ecommerce.wideo.kontrola', 'Kontrola jakości wykryła wadę, poprawiam klatkę...'));
4583    const tylkoTekst = w.teksty_ok === false && w.produkt_ok !== false;
4584    let poprawiona = null;
4585    try {
4586      if (tylkoTekst) {
4587        // Naprawa TYM SAMYM silnikiem co klatki (decyzja ownera 25.08) — nano-edit
4588        // przerysowywał styl gpt-image (inny tłok liter, inna czcionka).
4589        const naprawa = {
4590          type: 'image', modelName: cialo.modelName,
4591          prompt: 'RESTORATION of this exact advertising frame (attached as reference). Reproduce it 1:1 — composition, colors, lighting, textures, product and layout stay pixel-identical — and fix ONLY these text errors: ' + w.problemy.join('; ')
4592            + '. The texts must read EXACTLY: ' + (teksty || []).map(t => '"' + t + '"').join(', ')
4593            + '. Polish diacritics must be letter-perfect. This is a repair, NOT a new picture.',
4594          aspectRatio: cialo.aspectRatio, detail: cialo.detail, resolution: cialo.resolution,
4595          imageUrls: JSON.stringify([url]),
4596        };
4597        poprawiona = await czekaj(await generacja(naprawa, 'ecommerce-klatka'), 8 * 60_000);
4598      } else {
4599        const cialoP = Object.assign({}, cialo, { prompt: cialo.prompt + '\nQC FEEDBACK — the previous attempt FAILED quality control because: ' + w.problemy.join('; ') + '. Fix exactly these issues.' });
4600        poprawiona = await czekaj(await generacja(cialoP, 'ecommerce-klatka'), 8 * 60_000);
4601      }
4602    } catch (_) {}
4603    if (!poprawiona) return { url, wada: w.problemy };
4604    const w2 = await mkKontrola(poprawiona, produktUrl, teksty, scena, powtorzenia);
4605    if (w2 && w2.ok === false && w2.problemy && w2.problemy.length) return { url: poprawiona, wada: w2.problemy };
4606    return { url: poprawiona, wada: null };
4607  }
4608
4609  function mkAkceptacja(urls, wady) {
4610    wady = wady || [];
4611    const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
4612    const maWady = wady.some(Boolean);
4613    return new Promise((zgoda) => {
4614      const el = document.createElement('div');
4615      el.className = 'backdrop show';
4616      el.style.zIndex = '260';
4617      el.innerHTML = '<div class="modal" style="max-width:640px">'
4618        + '<h3>' + tt('ecommerce.wideo.acceptTitle', 'Klatki Twojej reklamy') + '</h3>'
4619        + '<p>' + tt('ecommerce.wideo.acceptSub', 'Sprawdź, czy produkt wygląda jak Twój. Animacja ruszy po akceptacji.') + '</p>'
4620        + (maWady ? '<p style="color:var(--danger,#dc2626);font-size:12.5px;margin:4px 0 0">' + tt('ecommerce.wideo.wady', 'Kontrola jakości nie zdołała usunąć wszystkich wad — sprawdź oznaczone klatki przed animacją.') + '</p>' : '')
4621        + '<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin:14px 0">'
4622        + urls.map((u, i) => '<div style="position:relative">'
4623            + '<img src="' + u + '" style="width:100%;aspect-ratio:9/16;object-fit:cover;border-radius:10px;border:2px solid ' + (wady[i] ? 'var(--danger,#dc2626)' : 'var(--border)') + '">'
4624            + (wady[i] ? '<div title="' + esc(wady[i].join('; ')) + '" style="position:absolute;left:4px;right:4px;bottom:4px;background:rgba(220,38,38,0.92);color:#fff;font-size:9.5px;line-height:1.25;padding:3px 6px;border-radius:7px;overflow:hidden;max-height:44px">&#9888; ' + esc(wady[i][0]) + '</div>' : '')
4625            + '</div>').join('')
4626        + '</div><div class="m-actions">'
4627        + '<button class="btn" data-mk="nie">' + tt('btn.cancel', 'Anuluj') + '</button>'
4628        + '<button class="btn btn-pr" data-mk="tak"><i class="fas fa-play" style="margin-right:6px"></i>' + tt('ecommerce.wideo.animate', 'Animuj (25 kr)') + '</button>'
4629        + '</div></div>';
4630      el.addEventListener('click', (e) => {
4631        const mk = e.target.closest && e.target.closest('[data-mk]');
4632        if (mk) { el.remove(); zgoda(mk.dataset.mk === 'tak'); }
4633        else if (e.target === el) { el.remove(); zgoda(false); }
4634      });
4635      document.body.appendChild(el);
4636    });
4637  }
4638})();
4639</script>
vendor: 1 bytes, line 4639
4639
4640<script>window.__EN_DICT = {
4641  "WIDEO":"VIDEO",
4642  "Wszystkie":"All","Moda":"Fashion","Uroda":"Beauty","Suplementy":"Supplements","Dom":"Home","Biżuteria":"Jewelry","Jedzenie":"Food","Kawa/Herbata":"Coffee/Tea","Świeczki":"Candles","Sport":"Sports","Dzieci":"Kids","Zwierzęta":"Pets","Alkohole":"Alcohol","Ogród":"Garden","Inne":"Other",
4643  "Nie ma Twojej kategorii? Kliknij":"Not your category? Click","Brak Twojej? Powiedz nam":"Missing yours? Tell us",
4644  "Twoja branża":"Your industry","15+ kategorii dla małych sklepów":"15+ categories for small shops","Każda kategoria ma dziesiątki gotowych szablonów. Brakuje Twojej? Powiedz nam — dodamy w 24h.":"Every category has dozens of ready-made templates. Missing yours? Tell us — we'll add it within 24h.",
4645  "Zaproponuj kategorię":"Suggest a category","Nie znalazłeś kategorii dla swojego biznesu? Powiedz nam o czym potrzebujesz szablonów.":"Didn't find a category for your business? Tell us what templates you need.","Dodajemy nowe presety w max 24 godziny":"We add new presets within 24 hours","Twój email":"Your email","O jakiej kategorii / produkcie?":"Which category / product?","Wyślij propozycję":"Send suggestion",
4646  "Użyj szablonu":"Use template","Wgraj zdjęcia aby oznaczać przez @nazwa.":"Upload photos to tag with @name.","Brak produktów":"No products","Brak modeli":"No models",
4647  "Wgraj zdjęcia produktów raz — AI pamięta wygląd i wstawia do każdej sceny. Idealne dla sklepów z 5+ pozycjami.":"Upload product photos once — AI remembers the look and drops them into every scene. Perfect for shops with 5+ items.",
4648  "From 2 PLN / generation":"From £0.40 / generation","PLN 2,000 — 5,000":"£400 — 1,000","~PLN 30 / month":"~£6 / month","Wkrótce nowe szablony":"New templates coming soon","Kurtka @IMG_1":"Jacket @IMG_1","Modelka @IMG_2":"Model @IMG_2","Wybierz produkt":"Select product","Wybierz model":"Select model","Z Twojej Biblioteki (produkty które wgrałeś)":"From your Library (products you uploaded)","Z Twojej Biblioteki albo Chmurkowi influ (gotowi)":"From your Library or Pixcloud influencers (ready)","Dodaj produkt":"Add product","Dodaj modela":"Add model","Produkt":"Product",
4649  "Aksamitna Tajemnica":"Velvet Mystery","Poranna Rosa":"Morning Dew","Słoneczny Urok":"Sunny Charm","Marmurowy Sen":"Marble Dream","Złoty Szept Glamour":"Golden Whisper Glamour","Energia Treningu":"Workout Energy","Siła i Skupienie":"Strength & Focus","Naturalna Energia":"Natural Energy","Moc i Elegancja":"Power & Elegance","Światło Ziół":"Herbal Light","Czysta Harmonia":"Pure Harmony","Ciemna Elegancja":"Dark Elegance","Poranne Światło":"Morning Light","Aksamitna Czerń":"Velvet Black","Głębia Smaku":"Depth of Flavor","Kulinarna Elegancja":"Culinary Elegance","Słoneczny Dywanik":"Sunny Rug","Rustykalny Szept":"Rustic Whisper","Słoneczny Poranek":"Sunny Morning","Miejski Rytuał":"Urban Ritual","Poranny Dotyk":"Morning Touch","Zmysłowy Relaks":"Sensual Relax","Nocna Harmonia":"Night Harmony","Blask Poranka":"Morning Glow","Jedwabny Szept":"Silk Whisper","Mroczny Aksamit":"Dark Velvet","Poranny Trening":"Morning Workout","Kryształowy Szron":"Crystal Frost","Obsydianowy Blask":"Obsidian Glow","Antyczny Sekret":"Ancient Secret","Promienny Dotyk Natury":"Radiant Touch of Nature","Poranny Świt":"Morning Dawn","Czystość Poranka":"Morning Purity","Delikatny Dotyk":"Gentle Touch","Świetliste Kontury":"Luminous Contours","Dynamiczny Kontrast":"Dynamic Contrast","Czysta Esencja":"Pure Essence"
4650};</script>
vendor: 1 bytes, line 4650
4650
4651<script>window.__CS_DICT={"WIDEO":"VIDEO","Wszystkie":"Vše","Moda":"Móda","Uroda":"Krása","Suplementy":"Doplňky","Dom":"Domácnost","Biżuteria":"Šperky","Jedzenie":"Jídlo","Kawa/Herbata":"Káva/Čaj","Świeczki":"Svíčky","Sport":"Sport","Dzieci":"Děti","Zwierzęta":"Mazlíčci","Alkohole":"Alkohol","Ogród":"Zahrada","Inne":"Ostatní","Nie ma Twojej kategorii? Kliknij":"Nevidíte svou kategorii? Klikněte","Brak Twojej? Powiedz nam":"Chybí ta vaše? Řekněte nám","Twoja branża":"Váš obor","15+ kategorii dla małych sklepów":"15+ kategorií pro malé obchody","Każda kategoria ma dziesiątki gotowych szablonów. Brakuje Twojej? Powiedz nam — dodamy w 24h.":"Každá kategorie má desítky hotových šablon. Chybí ta vaše? Řekněte nám — přidáme ji do 24 h.","Zaproponuj kategorię":"Navrhněte kategorii","Nie znalazłeś kategorii dla swojego biznesu? Powiedz nam o czym potrzebujesz szablonów.":"Nenašli jste kategorii pro své podnikání? Řekněte nám, jaké šablony potřebujete.","Dodajemy nowe presety w max 24 godziny":"Nové presety přidáváme do 24 hodin","Twój email":"Váš e-mail","O jakiej kategorii / produkcie?":"Jaká kategorie / produkt?","Wyślij propozycję":"Odeslat návrh","Użyj szablonu":"Použít šablonu","Wgraj zdjęcia aby oznaczać przez @nazwa.":"Nahrajte fotky, abyste je mohli tagovat přes @jméno.","Brak produktów":"Žádné produkty","Brak modeli":"Žádné modely","Wgraj zdjęcia produktów raz — AI pamięta wygląd i wstawia do każdej sceny. Idealne dla sklepów z 5+ pozycjami.":"Nahrajte fotky produktů jednou — AI si vzhled zapamatuje a vloží ho do každé scény. Ideální pro obchody s 5+ položkami.","From 2 PLN / generation":"Od 10 Kč / generace","PLN 2,000 — 5,000":"10 000 – 25 000 Kč","~PLN 30 / month":"~150 Kč / měsíc","Wkrótce nowe szablony":"Nové šablony již brzy","Kurtka @IMG_1":"Bunda @IMG_1","Modelka @IMG_2":"Modelka @IMG_2","Wybierz produkt":"Vyberte produkt","Wybierz model":"Vyberte model","Z Twojej Biblioteki (produkty które wgrałeś)":"Z vaší knihovny (produkty, které jste nahráli)","Z Twojej Biblioteki albo Chmurkowi influ (gotowi)":"Z vaší knihovny nebo Pixcloud influenceři (hotoví)","Dodaj produkt":"Přidat produkt","Dodaj modela":"Přidat modelku","Produkt":"Produkt","Aksamitna Tajemnica":"Sametové tajemství","Poranna Rosa":"Ranní rosa","Słoneczny Urok":"Sluneční kouzlo","Marmurowy Sen":"Mramorový sen","Złoty Szept Glamour":"Zlatý šepot glamour","Energia Treningu":"Energie tréninku","Siła i Skupienie":"Síla a soustředění","Naturalna Energia":"Přirozená energie","Moc i Elegancja":"Síla a elegance","Światło Ziół":"Světlo bylin","Czysta Harmonia":"Čistá harmonie","Ciemna Elegancja":"Temná elegance","Poranne Światło":"Ranní světlo","Aksamitna Czerń":"Sametová čerň","Głębia Smaku":"Hloubka chuti","Kulinarna Elegancja":"Kulinářská elegance","Słoneczny Dywanik":"Sluneční kobereček","Rustykalny Szept":"Rustikální šepot","Słoneczny Poranek":"Slunečné ráno","Miejski Rytuał":"Městský rituál","Poranny Dotyk":"Ranní dotek","Zmysłowy Relaks":"Smyslný relax","Nocna Harmonia":"Noční harmonie","Blask Poranka":"Záře rána","Jedwabny Szept":"Hedvábný šepot","Mroczny Aksamit":"Temný samet","Poranny Trening":"Ranní trénink","Kryształowy Szron":"Křišťálový mráz","Obsydianowy Blask":"Obsidiánová záře","Antyczny Sekret":"Antické tajemství","Promienny Dotyk Natury":"Zářivý dotek přírody","Poranny Świt":"Ranní úsvit","Czystość Poranka":"Čistota rána","Delikatny Dotyk":"Jemný dotek","Świetliste Kontury":"Světelné kontury","Dynamiczny Kontrast":"Dynamický kontrast","Czysta Esencja":"Čistá esence"};</script>
4651
4652
4653
4654<script src="/js/page-en-translate.js?v=5"></script>
vendor: 1 bytes, line 4654
4654
4655<script>(function(){function c(){var b=a.contentDocument||(a.contentWindow&&a.contentWindow.document);if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'a45d616949d706e9',t:'MTc5MTIxMzMxMw=='};var a=document.createElement('script');a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script>
4655</body>
4656</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.