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> <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> <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,'"')}" 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, '"')})"> 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,'<')}</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, '"'); 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,'<').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, '"') + '">'; 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, '&').replace(/</g, '<').replace(/"/g, '"'); 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">⚠ ' + 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.