FeedGine launch
21
.claude/settings.local.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(wc -l \"/c/projekt/Profice WebSite\"/style/*.css \"/c/projekt/Profice WebSite\"/scripts/*.js)",
|
||||
"Bash(find '/c/projekt/Profice WebSite' -name *.js -o -name *.css)",
|
||||
"Bash(xargs wc:*)",
|
||||
"Bash(grep -E \"\\\\.html$\")",
|
||||
"Bash(cp 'C:/projekt/Profice WebSite/images/logo/logo-01-complete.png' C:/Users/mvita/Desktop/Alles/FEEDGINE/images/logo/)",
|
||||
"Bash(cp 'C:/projekt/Profice WebSite/images/logo/Appicon 1024X1024-01.png' C:/Users/mvita/Desktop/Alles/FEEDGINE/images/logo/)",
|
||||
"Bash(cp 'C:/projekt/Profice WebSite/images/icons/spider.png' C:/Users/mvita/Desktop/Alles/FEEDGINE/images/icons/)",
|
||||
"Bash(cp \"C:/projekt/Profice WebSite/images/icons/email_white.png\" \"C:/Users/mvita/Desktop/Alles/FEEDGINE/images/icons/\")",
|
||||
"Bash(cp \"C:/projekt/Profice WebSite/images/icons/instagram.png\" \"C:/Users/mvita/Desktop/Alles/FEEDGINE/images/icons/\")",
|
||||
"Bash(cp \"C:/projekt/Profice WebSite/images/icons/facebook.png\" \"C:/Users/mvita/Desktop/Alles/FEEDGINE/images/icons/\")",
|
||||
"Bash(cp \"C:/projekt/Profice WebSite/images/icons/KI.png\" \"C:/Users/mvita/Desktop/Alles/FEEDGINE/images/icons/\")",
|
||||
"Bash(cp \"C:/projekt/Profice WebSite/images/additional/cursor.png\" \"C:/Users/mvita/Desktop/Alles/FEEDGINE/images/additional/\")",
|
||||
"Bash(cp \"C:/projekt/Profice WebSite/scripts/hex-background.js\" \"C:/Users/mvita/Desktop/Alles/FEEDGINE/scripts/\")",
|
||||
"Bash(cp \"C:/projekt/Profice WebSite/scripts/feed-calculator.js\" \"C:/Users/mvita/Desktop/Alles/FEEDGINE/scripts/\")",
|
||||
"Bash(cp:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
51
.htaccess
Normal file
@@ -0,0 +1,51 @@
|
||||
# Feedgine — Apache Configuration
|
||||
|
||||
# Security: disable directory listing
|
||||
Options -Indexes
|
||||
|
||||
# Flowise chat proxy
|
||||
RewriteEngine On
|
||||
RewriteRule ^api/v1/prediction/(.*)$ scripts/add/flowise-proxy.php?path=$1 [L,QSA]
|
||||
|
||||
# HTTPS redirect (enable in production)
|
||||
# RewriteCond %{HTTPS} off
|
||||
# RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||
|
||||
# GZIP compression
|
||||
<IfModule mod_deflate.c>
|
||||
AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json
|
||||
AddOutputFilterByType DEFLATE image/svg+xml font/ttf font/woff font/woff2
|
||||
</IfModule>
|
||||
|
||||
# Browser caching
|
||||
<IfModule mod_expires.c>
|
||||
ExpiresActive On
|
||||
ExpiresByType text/css "access plus 1 month"
|
||||
ExpiresByType application/javascript "access plus 1 month"
|
||||
ExpiresByType image/png "access plus 1 month"
|
||||
ExpiresByType image/jpeg "access plus 1 month"
|
||||
ExpiresByType image/svg+xml "access plus 1 month"
|
||||
ExpiresByType font/ttf "access plus 1 month"
|
||||
ExpiresByType font/woff2 "access plus 1 month"
|
||||
ExpiresByType text/html "access plus 1 hour"
|
||||
</IfModule>
|
||||
|
||||
# Security headers
|
||||
<IfModule mod_headers.c>
|
||||
Header always set X-Content-Type-Options "nosniff"
|
||||
Header always set X-XSS-Protection "1; mode=block"
|
||||
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||
# X-Frame-Options set per-page where needed
|
||||
</IfModule>
|
||||
|
||||
# Protect config file
|
||||
<Files "config.php">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</Files>
|
||||
|
||||
# Protect data directory
|
||||
<FilesMatch "\.json$">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</FilesMatch>
|
||||
7
Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
COPY . /usr/share/nginx/html
|
||||
|
||||
EXPOSE 80
|
||||
68
config.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/**
|
||||
* Feedgine — Centralized Credentials & Configuration
|
||||
* ALL SENSITIVE DATA IS STORED HERE — NOT VISIBLE TO CLIENT
|
||||
*
|
||||
* Include this file in send.php: require_once dirname(__DIR__, 2) . '/config.php';
|
||||
*/
|
||||
|
||||
// ============================================================
|
||||
// ENVIRONMENT
|
||||
// ============================================================
|
||||
define('USE_PRODUCTION', false); // set to true on live server
|
||||
define('DEBUG_MODE', true); // set to false on live server
|
||||
|
||||
// ============================================================
|
||||
// N8N WEBHOOKS
|
||||
// ============================================================
|
||||
define('FEEDGINE_WEBHOOK_TEST', 'https://n8n.support-space.de/webhook-test/REPLACE_WITH_FEEDGINE_WEBHOOK');
|
||||
define('FEEDGINE_WEBHOOK_PROD', 'https://n8n.support-space.de/webhook/REPLACE_WITH_FEEDGINE_WEBHOOK');
|
||||
define('FEEDGINE_WEBHOOK_URL', USE_PRODUCTION ? FEEDGINE_WEBHOOK_PROD : FEEDGINE_WEBHOOK_TEST);
|
||||
|
||||
// KI Chat Webhook
|
||||
define('KI_CHAT_WEBHOOK_TEST', 'https://n8n.support-space.de/webhook-test/8a25bce2-ff83-4676-a3a2-a0e1174fcffe');
|
||||
define('KI_CHAT_WEBHOOK_PROD', 'https://n8n.support-space.de/webhook/8a25bce2-ff83-4676-a3a2-a0e1174fcffe');
|
||||
define('KI_CHAT_WEBHOOK_URL', USE_PRODUCTION ? KI_CHAT_WEBHOOK_PROD : KI_CHAT_WEBHOOK_TEST);
|
||||
|
||||
// ============================================================
|
||||
// GOOGLE
|
||||
// ============================================================
|
||||
define('GA_MEASUREMENT_ID', 'G-XXXXXXXXXX'); // Google Analytics 4 Measurement ID
|
||||
define('GA_API_SECRET', ''); // GA4 Measurement Protocol API Secret
|
||||
define('GTM_CONTAINER_ID', 'GTM-XXXXXXX'); // Google Tag Manager container ID
|
||||
define('GADS_CONVERSION_ID', 'AW-XXXXXXXXXX'); // Google Ads Conversion ID
|
||||
define('GADS_CONVERSION_LABEL', ''); // Google Ads Conversion Label
|
||||
|
||||
// Flowise Chatbot
|
||||
define('FLOWISE_CHATFLOW_ID', 'd63d3d02-b5fa-482c-9161-c21c615fb625');
|
||||
define('FLOWISE_API_HOST', 'https://flowise.profice.de');
|
||||
|
||||
// ============================================================
|
||||
// META / FACEBOOK
|
||||
// ============================================================
|
||||
define('FB_PIXEL_ID', ''); // Facebook Pixel ID
|
||||
define('FB_ACCESS_TOKEN', ''); // Facebook Conversions API Access Token
|
||||
|
||||
// ============================================================
|
||||
// LINKEDIN
|
||||
// ============================================================
|
||||
define('LINKEDIN_PARTNER_ID', '');
|
||||
|
||||
// ============================================================
|
||||
// GENERAL API SECURITY
|
||||
// ============================================================
|
||||
define('API_SECRET_KEY', 'REPLACE_WITH_STRONG_RANDOM_KEY');
|
||||
define('RATE_LIMIT_REQUESTS', 100); // max requests per window
|
||||
define('RATE_LIMIT_WINDOW', 3600); // window in seconds (1 hour)
|
||||
|
||||
// ============================================================
|
||||
// ALLOWED ORIGINS (CORS)
|
||||
// ============================================================
|
||||
define('ALLOWED_ORIGINS', serialize([
|
||||
'https://feedgine.de',
|
||||
'https://www.feedgine.de',
|
||||
'https://profice.ai',
|
||||
'https://www.profice.ai',
|
||||
'http://localhost',
|
||||
'http://127.0.0.1',
|
||||
]));
|
||||
BIN
fonts/montserrat-bold.ttf
Normal file
BIN
fonts/montserrat-regular.ttf
Normal file
BIN
images/additional/cursor.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
images/icons/KI.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
images/icons/email_white.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
images/icons/facebook.png
Normal file
|
After Width: | Height: | Size: 7.4 MiB |
BIN
images/icons/instagram.png
Normal file
|
After Width: | Height: | Size: 8.5 MiB |
BIN
images/icons/spider.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
images/logo/Appicon 1024X1024-01.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
images/logo/logo-01-complete.png
Normal file
|
After Width: | Height: | Size: 322 KiB |
908
index.html
Normal file
@@ -0,0 +1,908 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
|
||||
<!-- Cookiebot — consent management -->
|
||||
<script id="Cookiebot" src="https://consent.cookiebot.com/uc.js"
|
||||
data-cbid="d6499c25-b3c2-41ac-8edf-60a4209f74c9"
|
||||
data-blockingmode="auto" type="text/javascript"></script>
|
||||
<script>
|
||||
window.addEventListener('CookiebotOnDialogDisplay', function () {
|
||||
var d = document.getElementById('CybotCookiebotDialog');
|
||||
if (d) {
|
||||
d.style.setProperty('position', 'fixed', 'important');
|
||||
d.style.setProperty('bottom', '0', 'important');
|
||||
d.style.setProperty('top', 'auto', 'important');
|
||||
d.style.setProperty('left', '0', 'important');
|
||||
d.style.setProperty('right', '0', 'important');
|
||||
d.style.setProperty('width', '100%', 'important');
|
||||
d.style.setProperty('transform','none', 'important');
|
||||
d.style.setProperty('margin', '0', 'important');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Google Tag Manager -->
|
||||
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer','GTM-PNJK5FN7');</script>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#2E2929">
|
||||
<meta name="description" content="Feedgine — Profit Intelligence Platform. Verbindet JTL-Wawi-Margen mit Google Shopping, Meta & Microsoft Ads. POAS statt ROAS. Server-Side Tracking. Ab €999/Monat.">
|
||||
<meta name="keywords" content="Feedgine, POAS, Profit Intelligence, Google Shopping, Server Side Tracking, Feed Optimierung, E-Commerce">
|
||||
<meta name="author" content="Profice GmbH">
|
||||
<meta property="og:title" content="Feedgine — Profit Intelligence Platform">
|
||||
<meta property="og:description" content="ROAS lügt. Profit zählt. Feedgine berechnet echte Produktmargen und optimiert Feeds für POAS. +31% profitabler Umsatz bei lkw-teile24.de.">
|
||||
<meta property="og:type" content="website">
|
||||
<meta name="robots" content="index, follow">
|
||||
<title>Feedgine — Profit Intelligence Platform by Profice</title>
|
||||
|
||||
<!-- Favicons -->
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="images/logo/Appicon 1024X1024-01.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="images/logo/Appicon 1024X1024-01.png">
|
||||
<link rel="shortcut icon" href="images/logo/Appicon 1024X1024-01.png">
|
||||
|
||||
<!-- Fonts: local Montserrat + Google Fonts -->
|
||||
<link rel="stylesheet" href="style/fonts.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Raleway:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ================================================================
|
||||
FEEDGINE — Profice Home Design System
|
||||
Palette: Warm Dark · Amber · Teal · Green
|
||||
================================================================ */
|
||||
|
||||
/* ─── ROOT ─── */
|
||||
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
||||
:root{
|
||||
--charcoal:#4F4747;--slate:#777764;--ivory:#EBEBDE;
|
||||
--amber:#F57C00;--teal:#26A69A;--green:#66BB6A;
|
||||
--bg:#0e0d0c;--bg2:#161514;--bg3:#1c1b19;
|
||||
--text:#d4d0c8;--text-dim:#7a7668;--text-bright:#f5f2ea;
|
||||
--mono:'Space Mono','Courier New',monospace;
|
||||
--head:'Montserrat','Segoe UI',sans-serif;
|
||||
--body:'Raleway','Montserrat',sans-serif;
|
||||
}
|
||||
html{scroll-behavior:smooth;font-size:16px}
|
||||
body{background:var(--bg);color:var(--text);font-family:var(--body);overflow-x:hidden;-webkit-font-smoothing:antialiased}
|
||||
::selection{background:var(--amber);color:var(--bg)}
|
||||
|
||||
/* ─── GRAIN OVERLAY ─── */
|
||||
body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:9999;
|
||||
background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
|
||||
opacity:.35}
|
||||
|
||||
/* ─── NAV ─── */
|
||||
nav{position:fixed;top:0;left:0;right:0;z-index:100;padding:0.6rem 3rem;display:flex;justify-content:space-between;align-items:center;
|
||||
backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
|
||||
background:rgba(14,13,12,.75);border-bottom:1px solid rgba(235,235,222,.05)}
|
||||
.nav-logo{text-decoration:none;display:flex;align-items:center}
|
||||
.nav-logo img{height:44px;display:block;width:auto}
|
||||
nav ul{list-style:none;display:flex;gap:2rem;align-items:center}
|
||||
nav a{color:var(--text-dim);text-decoration:none;font-family:var(--head);font-size:.78rem;letter-spacing:.1em;text-transform:uppercase;transition:color .3s}
|
||||
nav a:hover{color:var(--amber)}
|
||||
.nav-cta{color:var(--amber)!important;border:1px solid var(--amber);padding:.45rem 1.2rem;transition:all .3s}
|
||||
.nav-cta:hover{background:var(--amber);color:var(--bg)!important}
|
||||
|
||||
/* Hamburger */
|
||||
.hamburger{display:none;flex-direction:column;gap:5px;background:transparent;border:1px solid rgba(235,235,222,.25);
|
||||
border-radius:6px;cursor:pointer;padding:8px 10px;width:42px;height:42px;transition:border-color .2s}
|
||||
.hamburger:hover{border-color:var(--amber)}
|
||||
.hamburger span{display:block;width:18px;height:2px;background:var(--text);border-radius:1px;transition:transform .3s,opacity .3s;transform-origin:center}
|
||||
.hamburger.open span:nth-child(1){transform:translateY(7px) rotate(45deg)}
|
||||
.hamburger.open span:nth-child(2){opacity:0}
|
||||
.hamburger.open span:nth-child(3){transform:translateY(-7px) rotate(-45deg)}
|
||||
|
||||
/* Mobile nav */
|
||||
.mobile-nav{display:none;position:fixed;top:62px;left:0;right:0;background:rgba(14,13,12,.97);
|
||||
backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
|
||||
z-index:99;flex-direction:column;gap:0;border-bottom:1px solid rgba(235,235,222,.08);
|
||||
padding:1rem 1.5rem 1.5rem}
|
||||
.mobile-nav.open{display:flex}
|
||||
.mobile-nav a{display:block;padding:.8rem .5rem;color:var(--text-dim);text-decoration:none;font-family:var(--head);
|
||||
font-size:.85rem;letter-spacing:.08em;text-transform:uppercase;border-bottom:1px solid rgba(235,235,222,.04);transition:color .2s}
|
||||
.mobile-nav a:hover{color:var(--amber)}
|
||||
.mobile-nav .mobile-cta{margin-top:.75rem;text-align:center;border:1px solid var(--amber);
|
||||
color:var(--amber)!important;border-bottom:1px solid var(--amber)!important;padding:.8rem!important}
|
||||
.overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:90}
|
||||
.overlay.active{display:block}
|
||||
|
||||
/* ─── HERO ─── */
|
||||
.hero{min-height:100vh;display:grid;grid-template-columns:1fr 400px;gap:4rem;align-items:center;
|
||||
padding:8rem 3rem 4rem;max-width:1240px;margin:0 auto;position:relative}
|
||||
.hero::before{content:'';position:absolute;top:-20%;right:-10%;width:70vw;height:70vw;
|
||||
background:radial-gradient(circle,rgba(245,124,0,.05) 0%,transparent 65%);pointer-events:none}
|
||||
.hero-label{font-family:var(--head);font-size:.7rem;letter-spacing:.3em;text-transform:uppercase;color:var(--amber);
|
||||
margin-bottom:2rem;display:flex;align-items:center;gap:1rem}
|
||||
.hero-label::before{content:'';width:3rem;height:1px;background:var(--amber)}
|
||||
.hero h1{font-family:var(--head);font-weight:900;font-size:clamp(2.8rem,7vw,5.5rem);line-height:1.05;
|
||||
color:var(--text-bright);max-width:14ch;margin-bottom:2rem}
|
||||
.hero h1 em{font-style:normal;color:var(--amber);position:relative}
|
||||
.hero h1 em::after{content:'';position:absolute;bottom:.08em;left:0;right:0;height:3px;background:var(--amber);opacity:.35}
|
||||
.hero-sub{font-family:var(--body);font-size:1.1rem;line-height:1.8;color:var(--text-dim);max-width:44ch;margin-bottom:2.5rem;font-weight:300}
|
||||
.hero-sub strong{color:var(--text);font-weight:600}
|
||||
.hero-actions{display:flex;gap:1rem;flex-wrap:wrap}
|
||||
|
||||
/* Buttons */
|
||||
.cta-btn{display:inline-block;font-family:var(--head);font-weight:700;font-size:.82rem;letter-spacing:.12em;text-transform:uppercase;
|
||||
text-decoration:none;color:var(--bg);background:var(--amber);padding:1rem 2.5rem;transition:all .3s;border:none;cursor:pointer}
|
||||
.cta-btn:hover{background:var(--text-bright);color:var(--bg)}
|
||||
.cta-btn::after{content:'→';margin-left:.75rem;transition:transform .3s;display:inline-block}
|
||||
.cta-btn:hover::after{transform:translateX(4px)}
|
||||
.btn-ghost{display:inline-block;font-family:var(--head);font-weight:600;font-size:.82rem;letter-spacing:.1em;text-transform:uppercase;
|
||||
text-decoration:none;color:var(--text-dim);border:1px solid rgba(235,235,222,.2);padding:1rem 2rem;transition:all .3s}
|
||||
.btn-ghost:hover{color:var(--amber);border-color:var(--amber)}
|
||||
|
||||
/* ─── TERMINAL CARD (hero right) ─── */
|
||||
.hero-terminal{background:var(--bg2);border:1px solid rgba(235,235,222,.08);padding:1.75rem;border-radius:4px}
|
||||
.t-header{display:flex;align-items:center;gap:.45rem;margin-bottom:1.5rem;padding-bottom:1rem;border-bottom:1px solid rgba(235,235,222,.06)}
|
||||
.t-dot{width:8px;height:8px;border-radius:50%}
|
||||
.t-dot.r{background:#FF5F57}.t-dot.y{background:#FEBC2E}.t-dot.g{background:#28C840}
|
||||
.t-label{font-family:var(--mono);font-size:.62rem;color:var(--text-dim);margin-left:auto;letter-spacing:.08em}
|
||||
.t-row{display:flex;justify-content:space-between;align-items:center;padding:.65rem 0;
|
||||
border-bottom:1px solid rgba(235,235,222,.04);font-family:var(--mono);font-size:.73rem}
|
||||
.t-row:last-of-type{border-bottom:none}
|
||||
.t-key{color:var(--text-dim)}.t-val{font-weight:700;color:var(--text)}
|
||||
.t-val.g{color:var(--green)}.t-val.a{color:var(--amber)}.t-val.r{color:#ef9a9a}
|
||||
.t-tag{font-family:var(--mono);font-size:.58rem;padding:2px 7px;font-weight:700;letter-spacing:.04em;margin-left:6px;border-radius:2px}
|
||||
.t-tag.pos{background:rgba(102,187,106,.12);color:var(--green)}
|
||||
.t-tag.warn{background:rgba(245,124,0,.12);color:var(--amber)}
|
||||
.t-tag.neg{background:rgba(239,154,154,.12);color:#ef9a9a}
|
||||
.t-footer{margin-top:1.25rem;padding-top:1rem;border-top:1px solid rgba(235,235,222,.06);
|
||||
display:flex;justify-content:space-between;font-family:var(--mono);font-size:.65rem}
|
||||
.t-footer .key{color:var(--text-dim)}.t-footer .val{color:var(--teal);font-weight:700}
|
||||
|
||||
/* ─── PROOF STRIP ─── */
|
||||
.proof-strip{background:var(--bg3);border-top:1px solid rgba(235,235,222,.04);border-bottom:1px solid rgba(235,235,222,.04);padding:4rem 3rem}
|
||||
.proof-inner{max-width:1240px;margin:0 auto;display:grid;grid-template-columns:repeat(4,1fr);gap:2rem}
|
||||
.proof-label{width:100%;text-align:center;font-family:var(--mono);font-size:.68rem;color:var(--text-dim);
|
||||
margin-bottom:1.5rem;letter-spacing:.08em;text-transform:uppercase;grid-column:1/-1}
|
||||
.proof-stat{}
|
||||
.proof-num{font-family:var(--head);font-size:2.4rem;font-weight:900;color:var(--amber);letter-spacing:-.03em;line-height:1;margin-bottom:.4rem}
|
||||
.proof-desc{font-family:var(--body);font-size:.83rem;color:var(--text-dim);line-height:1.5;font-weight:300}
|
||||
|
||||
/* ─── DIVIDER ─── */
|
||||
hr.divider{border:none;border-top:1px solid rgba(235,235,222,.06);margin:0}
|
||||
|
||||
/* ─── SECTIONS ─── */
|
||||
.section{max-width:1240px;margin:0 auto;padding:6rem 3rem}
|
||||
.label-tag{font-family:var(--head);font-size:.68rem;letter-spacing:.3em;text-transform:uppercase;color:var(--amber);
|
||||
margin-bottom:1.5rem;display:flex;align-items:center;gap:1rem}
|
||||
.label-tag::before{content:'';width:3rem;height:1px;background:var(--amber)}
|
||||
.section-title{font-family:var(--head);font-weight:800;font-size:clamp(1.8rem,4vw,3rem);color:var(--text-bright);
|
||||
line-height:1.15;margin-bottom:1.25rem}
|
||||
.section-title em{font-style:normal;color:var(--amber)}
|
||||
.section-lead{font-family:var(--body);font-size:1.05rem;color:var(--text-dim);max-width:52ch;line-height:1.8;
|
||||
font-weight:300;margin-bottom:3.5rem}
|
||||
|
||||
/* ─── PROBLEM / PROFILE CARDS ─── */
|
||||
.problem-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:2rem}
|
||||
.prob-card{border-left:2px solid var(--slate);padding:2rem 2rem 2rem 2.5rem;transition:border-color .3s}
|
||||
.prob-card:hover{border-color:var(--amber)}
|
||||
.prob-num{font-family:var(--head);font-size:.65rem;letter-spacing:.2em;text-transform:uppercase;color:var(--amber);
|
||||
opacity:.75;margin-bottom:1.25rem}
|
||||
.prob-icon{width:52px;height:52px;border-radius:6px;background:rgba(245,124,0,.06);border:1px solid rgba(245,124,0,.15);
|
||||
display:flex;align-items:center;justify-content:center;margin-bottom:1.25rem;color:var(--amber)}
|
||||
.prob-title{font-family:var(--head);font-weight:700;font-size:1.1rem;color:var(--text-bright);margin-bottom:.75rem}
|
||||
.prob-text{font-family:var(--body);font-size:.88rem;color:var(--text-dim);line-height:1.75;font-weight:300}
|
||||
|
||||
/* ─── HOW IT WORKS ─── */
|
||||
.how-grid{display:grid;grid-template-columns:1fr 360px;gap:4rem;align-items:start;margin-top:3rem}
|
||||
.how-step{display:flex;gap:1.5rem;padding:1.25rem 0;border-bottom:1px solid rgba(235,235,222,.05);transition:border-color .2s}
|
||||
.how-step:last-child{border-bottom:none}
|
||||
.how-step:hover{border-color:rgba(245,124,0,.2)}
|
||||
.step-num{font-family:var(--mono);font-size:.62rem;color:var(--amber);min-width:22px;padding-top:4px;letter-spacing:.06em}
|
||||
.step-title{font-family:var(--head);font-weight:700;font-size:.95rem;color:var(--text-bright);margin-bottom:.35rem}
|
||||
.step-desc{font-family:var(--body);font-size:.87rem;color:var(--text-dim);line-height:1.65;font-weight:300}
|
||||
|
||||
/* Visual / POAS panel */
|
||||
.how-visual{background:var(--bg2);border:1px solid rgba(235,235,222,.07);padding:2rem;position:sticky;top:5rem}
|
||||
.vis-label{font-family:var(--mono);font-size:.6rem;color:var(--text-dim);letter-spacing:.1em;text-transform:uppercase;margin-bottom:1.5rem}
|
||||
.poas-wrap{margin-bottom:1.1rem}
|
||||
.poas-bar-label{display:flex;justify-content:space-between;font-family:var(--mono);font-size:.68rem;color:var(--text-dim);margin-bottom:.4rem}
|
||||
.poas-bar-label span:last-child{color:var(--text)}
|
||||
.poas-track{height:4px;background:rgba(235,235,222,.07);overflow:hidden;border-radius:2px}
|
||||
.poas-fill{height:100%;border-radius:2px;transition:width 1.4s cubic-bezier(.16,1,.3,1)}
|
||||
.poas-fill.g{background:var(--green)}
|
||||
.poas-fill.a{background:var(--amber)}
|
||||
.poas-fill.r{background:#ef9a9a;width:18%!important}
|
||||
.vis-divider{height:1px;background:rgba(235,235,222,.06);margin:1.5rem 0}
|
||||
.vis-row{display:flex;justify-content:space-between;padding:.38rem 0;font-family:var(--mono);font-size:.7rem}
|
||||
.vis-k{color:var(--text-dim)}.vis-v{color:var(--text);font-weight:700}
|
||||
.vis-v.g{color:var(--green)}.vis-v.r{color:#ef9a9a}
|
||||
|
||||
/* ─── MODULES ─── */
|
||||
.modules-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:rgba(235,235,222,.06)}
|
||||
.module-card{background:var(--bg);padding:3rem 2.5rem;position:relative;transition:background .3s}
|
||||
.module-card:hover{background:var(--bg2)}
|
||||
.module-num{font-family:var(--head);font-size:.62rem;letter-spacing:.2em;text-transform:uppercase;color:var(--amber);opacity:.7;margin-bottom:1.5rem}
|
||||
.module-stack-num{font-family:var(--head);font-weight:800;font-size:3rem;color:rgba(245,124,0,.08);
|
||||
position:absolute;top:1.5rem;right:1.5rem;line-height:1;user-select:none}
|
||||
.module-title{font-family:var(--head);font-weight:700;font-size:1.1rem;color:var(--text-bright);margin-bottom:.75rem}
|
||||
.module-desc{font-family:var(--body);font-size:.87rem;color:var(--text-dim);line-height:1.75;font-weight:300;margin-bottom:1.25rem}
|
||||
.module-features{list-style:none;display:flex;flex-direction:column;gap:.4rem}
|
||||
.module-features li{font-family:var(--mono);font-size:.75rem;color:var(--text-dim);padding-left:1.25rem;position:relative;font-weight:400}
|
||||
.module-features li::before{content:'→';position:absolute;left:0;color:var(--amber)}
|
||||
|
||||
/* ─── COMPARE TABLE ─── */
|
||||
.compare-wrap{background:var(--bg2);border:1px solid rgba(235,235,222,.06);overflow:hidden;overflow-x:auto;-webkit-overflow-scrolling:touch}
|
||||
.compare-table{width:100%;border-collapse:collapse;font-size:.87rem;min-width:420px}
|
||||
.compare-table th{padding:1rem 1.5rem;text-align:left;font-family:var(--head);font-size:.65rem;font-weight:700;
|
||||
color:var(--text-dim);letter-spacing:.1em;text-transform:uppercase;border-bottom:1px solid rgba(235,235,222,.06);background:rgba(235,235,222,.02)}
|
||||
.compare-table th.accent{color:var(--amber);background:rgba(245,124,0,.04)}
|
||||
.compare-table td{padding:1rem 1.5rem;border-bottom:1px solid rgba(235,235,222,.04);color:var(--text-dim);font-size:.85rem}
|
||||
.compare-table tr:last-child td{border-bottom:none}
|
||||
.compare-table td:first-child{color:var(--text);font-family:var(--head);font-weight:600}
|
||||
.compare-table td.accent-col{background:rgba(245,124,0,.03)}
|
||||
.chk{color:var(--green);font-weight:700}.crs{color:#ef9a9a;font-weight:700}
|
||||
|
||||
/* ─── PRICING ─── */
|
||||
.pricing-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:2rem}
|
||||
.price-card{border-left:2px solid var(--slate);padding:2.5rem 2rem;position:relative;transition:border-color .3s}
|
||||
.price-card:hover{border-color:var(--text-dim)}
|
||||
.price-card.featured{border-left-color:var(--amber)}
|
||||
.price-badge{position:absolute;top:0;right:0;background:var(--amber);color:var(--bg);
|
||||
font-family:var(--head);font-size:.6rem;font-weight:800;padding:4px 10px;letter-spacing:.08em;text-transform:uppercase}
|
||||
.price-tier{font-family:var(--head);font-size:.65rem;font-weight:700;color:var(--text-dim);
|
||||
letter-spacing:.2em;text-transform:uppercase;margin-bottom:1.25rem}
|
||||
.price-amount{font-family:var(--head);font-size:3rem;font-weight:900;letter-spacing:-.03em;color:var(--text-bright);line-height:1;margin-bottom:.2rem}
|
||||
.price-period{font-family:var(--mono);font-size:.68rem;color:var(--text-dim);margin-bottom:.4rem}
|
||||
.price-setup{font-family:var(--mono);font-size:.68rem;color:var(--amber);margin-bottom:2rem}
|
||||
.price-features{list-style:none;display:flex;flex-direction:column;gap:.55rem;margin-bottom:2rem}
|
||||
.price-features li{font-family:var(--body);font-size:.85rem;color:var(--text-dim);display:flex;gap:.6rem;align-items:flex-start;font-weight:300}
|
||||
.price-features li span:first-child{color:var(--green);font-weight:700;flex-shrink:0;margin-top:1px}
|
||||
.price-cta{display:block;text-align:center;padding:.85rem;font-family:var(--head);font-size:.75rem;font-weight:700;
|
||||
letter-spacing:.1em;text-transform:uppercase;text-decoration:none;border:1px solid rgba(235,235,222,.15);
|
||||
color:var(--text-dim);transition:all .3s}
|
||||
.price-cta:hover{border-color:var(--amber);color:var(--amber)}
|
||||
.price-card.featured .price-cta{border-color:var(--amber);color:var(--amber)}
|
||||
.price-card.featured .price-cta:hover{background:var(--amber);color:var(--bg)}
|
||||
|
||||
/* ─── CALCULATOR ─── */
|
||||
.calculator-wrapper{max-width:1100px;margin:0 auto;padding:6rem 3rem}
|
||||
.calculator-section{display:grid;grid-template-columns:1fr 1fr;gap:2.5rem;align-items:start;margin-top:2.5rem}
|
||||
.calc-inputs{background:var(--bg2);border:1px solid rgba(235,235,222,.07);padding:2.5rem 2rem}
|
||||
.calc-inputs h3{font-family:var(--head);font-size:1rem;font-weight:700;color:var(--text-bright);margin-bottom:2rem}
|
||||
.calc-field{margin-bottom:1.5rem}
|
||||
.calc-field:last-child{margin-bottom:0}
|
||||
.calc-field label{display:flex;justify-content:space-between;align-items:baseline;font-family:var(--head);
|
||||
font-size:.78rem;font-weight:600;color:var(--text-dim);margin-bottom:.6rem;letter-spacing:.02em}
|
||||
.calc-field label span{font-size:.82rem;font-weight:700;color:var(--text-bright)}
|
||||
.calc-slider-row{display:flex;align-items:center;gap:.75rem}
|
||||
.calc-slider-row input[type=range]{flex:1;-webkit-appearance:none;appearance:none;height:3px;border-radius:2px;
|
||||
background:rgba(235,235,222,.1);outline:none;cursor:pointer}
|
||||
.calc-slider-row input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;border-radius:50%;
|
||||
background:var(--amber);cursor:pointer;border:2px solid rgba(0,0,0,.3)}
|
||||
.calc-slider-row input[type=range]::-moz-range-thumb{width:16px;height:16px;border-radius:50%;
|
||||
background:var(--amber);cursor:pointer;border:2px solid rgba(0,0,0,.3)}
|
||||
.calc-num-input{width:80px;padding:6px 10px;border:1px solid rgba(235,235,222,.12);font-size:.82rem;font-weight:600;
|
||||
color:var(--text-bright);text-align:right;background:rgba(235,235,222,.04);outline:none;font-family:inherit}
|
||||
.calc-num-input:focus{border-color:var(--amber)}
|
||||
.calc-outputs{display:flex;flex-direction:column;gap:1rem}
|
||||
.calc-out-card{background:var(--bg2);border:1px solid rgba(235,235,222,.07);padding:1.75rem}
|
||||
.calc-out-card.highlight{background:var(--bg3);border-color:rgba(38,166,154,.3)}
|
||||
.calc-out-label{font-family:var(--head);font-size:.62rem;font-weight:700;letter-spacing:.15em;text-transform:uppercase;
|
||||
color:var(--text-dim);margin-bottom:.5rem}
|
||||
.calc-out-card.highlight .calc-out-label{color:var(--teal)}
|
||||
.calc-out-value{font-family:var(--head);font-size:2.2rem;font-weight:700;color:var(--text-bright);line-height:1.1;margin-bottom:.25rem}
|
||||
.calc-out-card.highlight .calc-out-value{color:var(--teal)}
|
||||
.calc-out-sub{font-size:.8rem;color:var(--text-dim)}
|
||||
.calc-hebel-row{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}
|
||||
.calc-hebel-chip{display:inline-flex;align-items:center;background:rgba(102,187,106,.08);border:1px solid rgba(102,187,106,.2);
|
||||
padding:5px 12px;font-size:.87rem;font-weight:700;color:var(--green)}
|
||||
|
||||
/* ─── CTA SECTION ─── */
|
||||
.cta-section{padding:8rem 3rem;text-align:center;position:relative;background:var(--bg3)}
|
||||
.cta-section::before{content:'';position:absolute;bottom:0;left:50%;transform:translateX(-50%);width:60vw;height:60vw;
|
||||
background:radial-gradient(circle,rgba(245,124,0,.04) 0%,transparent 60%);pointer-events:none}
|
||||
.cta-section h2{font-family:var(--head);font-weight:900;font-size:clamp(2rem,5vw,3.2rem);color:var(--text-bright);
|
||||
margin-bottom:1.5rem;line-height:1.1}
|
||||
.cta-section h2 em{font-style:normal;color:var(--amber)}
|
||||
.cta-section p{font-size:1rem;color:var(--text-dim);max-width:44ch;margin:0 auto 3rem;line-height:1.8;font-weight:300}
|
||||
.cta-eyebrow{font-family:var(--head);font-size:.65rem;letter-spacing:.3em;text-transform:uppercase;color:var(--amber);margin-bottom:1.5rem}
|
||||
|
||||
/* ─── FOOTER ─── */
|
||||
footer{background:var(--bg3);border-top:1px solid rgba(235,235,222,.05);padding:4rem 3rem 2rem}
|
||||
.footer-grid{max-width:1240px;margin:0 auto;display:grid;grid-template-columns:repeat(4,1fr);gap:3rem;margin-bottom:3rem}
|
||||
.footer-brand img{height:40px;display:block;filter:brightness(0) invert(1);opacity:.8;margin-bottom:1rem}
|
||||
.footer-brand p{font-family:var(--body);font-size:.85rem;color:var(--text-dim);line-height:1.6;font-weight:300}
|
||||
.footer-col h4{font-family:var(--head);font-size:.65rem;letter-spacing:.2em;text-transform:uppercase;color:var(--amber);margin-bottom:1.25rem;font-weight:700}
|
||||
.footer-col a{display:block;color:var(--text-dim);text-decoration:none;font-size:.85rem;margin-bottom:.6rem;transition:color .3s}
|
||||
.footer-col a:hover{color:var(--amber)}
|
||||
.footer-contact-item{display:flex;align-items:center;gap:.75rem;margin-bottom:.75rem;font-size:.85rem;color:var(--text-dim)}
|
||||
.footer-contact-item a{color:var(--text-dim);text-decoration:none;transition:color .3s}
|
||||
.footer-contact-item a:hover{color:var(--amber)}
|
||||
.social-icons{display:flex;gap:1rem;margin-top:.5rem}
|
||||
.social-link{color:var(--text-dim);transition:color .3s,transform .3s;opacity:.6}
|
||||
.social-link:hover{color:var(--amber);opacity:1;transform:translateY(-2px)}
|
||||
.footer-bottom{max-width:1240px;margin:0 auto;padding-top:2rem;border-top:1px solid rgba(235,235,222,.05);
|
||||
display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:.75rem}
|
||||
.footer-bottom p{font-size:.78rem;color:var(--text-dim);opacity:.5}
|
||||
.footer-bottom-links{display:flex;gap:1.5rem}
|
||||
.footer-bottom-links a{font-size:.78rem;color:var(--text-dim);text-decoration:none;transition:color .3s}
|
||||
.footer-bottom-links a:hover{color:var(--amber)}
|
||||
|
||||
/* ─── REVEAL ANIMATIONS ─── */
|
||||
.reveal{opacity:0;transform:translateY(28px);transition:opacity .8s ease,transform .8s ease}
|
||||
.reveal.visible{opacity:1;transform:translateY(0)}
|
||||
.reveal-delay-1{transition-delay:.1s}
|
||||
.reveal-delay-2{transition-delay:.2s}
|
||||
|
||||
/* ─── RESPONSIVE ─── */
|
||||
@media(max-width:1024px){
|
||||
nav ul{gap:1.2rem}
|
||||
.how-grid{grid-template-columns:1fr;gap:2.5rem}
|
||||
.how-visual{position:static}
|
||||
.footer-grid{grid-template-columns:repeat(2,1fr);gap:2rem}
|
||||
}
|
||||
@media(max-width:900px){
|
||||
nav{padding:.6rem 1.5rem}
|
||||
nav ul{display:none}
|
||||
.hamburger{display:flex}
|
||||
.hero{grid-template-columns:1fr;gap:2.5rem;padding:7rem 1.5rem 3rem}
|
||||
.proof-strip{padding:3rem 1.5rem}
|
||||
.proof-inner{grid-template-columns:repeat(2,1fr)}
|
||||
.section{padding:4rem 1.5rem}
|
||||
.problem-grid{grid-template-columns:1fr;gap:1.25rem}
|
||||
.modules-grid{grid-template-columns:1fr;gap:1px}
|
||||
.pricing-grid{grid-template-columns:1fr;gap:1.25rem}
|
||||
}
|
||||
@media(max-width:768px){
|
||||
nav{padding:.5rem 1.25rem}
|
||||
.hero{padding:6.5rem 1.25rem 3rem}
|
||||
.hero h1{font-size:clamp(2.2rem,8vw,3.5rem)}
|
||||
.section{padding:3.5rem 1.25rem}
|
||||
.calculator-wrapper{padding:3.5rem 1.25rem}
|
||||
.calculator-section{grid-template-columns:1fr;gap:1.25rem}
|
||||
.cta-section{padding:5rem 1.25rem}
|
||||
footer{padding:3rem 1.25rem 1.5rem}
|
||||
.footer-grid{grid-template-columns:1fr 1fr;gap:1.75rem}
|
||||
.footer-bottom{flex-direction:column;align-items:flex-start}
|
||||
}
|
||||
@media(max-width:480px){
|
||||
.proof-inner{grid-template-columns:1fr 1fr}
|
||||
.hero-actions{flex-direction:column;align-items:stretch}
|
||||
.hero-actions .cta-btn,.hero-actions .btn-ghost{text-align:center}
|
||||
.footer-grid{grid-template-columns:1fr;gap:1.5rem}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Google Tag Manager (noscript) -->
|
||||
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PNJK5FN7"
|
||||
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
||||
|
||||
<!-- ── NAV ── -->
|
||||
<nav>
|
||||
<a href="https://profice.ai/" class="nav-logo" title="Profice GmbH">
|
||||
<img src="images/logo/logo-01-complete.png" alt="Profice">
|
||||
</a>
|
||||
<ul>
|
||||
<li><a href="#problem">Problem</a></li>
|
||||
<li><a href="#solution">Lösung</a></li>
|
||||
<li><a href="#modules">Module</a></li>
|
||||
<li><a href="#pricing">Pricing</a></li>
|
||||
<li><a href="#calculator">Kalkulator</a></li>
|
||||
<li><a href="https://profice.ai/">↗ Profice</a></li>
|
||||
<li><a href="https://termine.profice.de" target="_blank" rel="noopener" class="nav-cta">Profit-Analyse buchen</a></li>
|
||||
</ul>
|
||||
<button class="hamburger" id="hamburger" aria-label="Menü öffnen">
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- Mobile Nav -->
|
||||
<div class="mobile-nav" id="mobileNav">
|
||||
<a href="#problem">Problem</a>
|
||||
<a href="#solution">Lösung</a>
|
||||
<a href="#modules">Module</a>
|
||||
<a href="#pricing">Pricing</a>
|
||||
<a href="#calculator">Kalkulator</a>
|
||||
<a href="https://profice.ai/" target="_blank" rel="noopener">↗ KI Systeme (Profice)</a>
|
||||
<a href="https://termine.profice.de" target="_blank" rel="noopener" class="mobile-cta">Profit-Analyse buchen →</a>
|
||||
</div>
|
||||
<div class="overlay" id="overlay"></div>
|
||||
|
||||
<!-- ── HERO ── -->
|
||||
<section>
|
||||
<div class="hero">
|
||||
<div class="hero-left">
|
||||
<div class="hero-label">Profit Intelligence Platform</div>
|
||||
<h1>ROAS lügt.<br><em>Profit zählt.</em></h1>
|
||||
<p class="hero-sub">
|
||||
Feedgine verbindet deine <strong>JTL-Wawi-Margen</strong> mit Google Shopping, Meta und Microsoft Ads. Ergebnis bei lkw-teile24.de: <strong>+31 % profitabler Umsatz</strong>, −23 % verschwendetes Budget.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a href="https://termine.profice.de" target="_blank" rel="noopener" class="cta-btn">Profit-Analyse buchen</a>
|
||||
<a href="#solution" class="btn-ghost">Wie es funktioniert</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-terminal">
|
||||
<div class="t-header">
|
||||
<div class="t-dot r"></div>
|
||||
<div class="t-dot y"></div>
|
||||
<div class="t-dot g"></div>
|
||||
<span class="t-label">LIVE · feedgine.engine</span>
|
||||
</div>
|
||||
<div class="t-row">
|
||||
<span class="t-key">Produkt A — Werkzeug</span>
|
||||
<span class="t-val g">POAS 4.2 <span class="t-tag pos">SKALIEREN</span></span>
|
||||
</div>
|
||||
<div class="t-row">
|
||||
<span class="t-key">Produkt B — Zubehör</span>
|
||||
<span class="t-val a">POAS 1.1 <span class="t-tag warn">PRÜFEN</span></span>
|
||||
</div>
|
||||
<div class="t-row">
|
||||
<span class="t-key">Produkt C — Sonder</span>
|
||||
<span class="t-val r">POAS 0.6 <span class="t-tag neg">PAUSIEREN</span></span>
|
||||
</div>
|
||||
<div class="t-row">
|
||||
<span class="t-key">Produkt D — OEM-Teil</span>
|
||||
<span class="t-val g">POAS 3.8 <span class="t-tag pos">+BUDGET</span></span>
|
||||
</div>
|
||||
<div class="t-footer">
|
||||
<span class="key">Tracking Coverage (SST)</span>
|
||||
<span class="val">97.4 %</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── PROOF STRIP ── -->
|
||||
<div class="proof-strip">
|
||||
<div class="proof-inner">
|
||||
<div class="proof-label">Live-Referenz: lkw-teile24.de — 230.000 Produkte · JTL-Wawi · Google PMax</div>
|
||||
<div class="proof-stat">
|
||||
<div class="proof-num">230k</div>
|
||||
<div class="proof-desc">Produkte live in Referenz-Deployment lkw-teile24.de</div>
|
||||
</div>
|
||||
<div class="proof-stat">
|
||||
<div class="proof-num">97%</div>
|
||||
<div class="proof-desc">Tracking-Coverage durch Server Side Tracking</div>
|
||||
</div>
|
||||
<div class="proof-stat">
|
||||
<div class="proof-num">−23%</div>
|
||||
<div class="proof-desc">Ads-Budget auf unprofitable Produkte nach POAS-Umstellung</div>
|
||||
</div>
|
||||
<div class="proof-stat">
|
||||
<div class="proof-num">+31%</div>
|
||||
<div class="proof-desc">Umsatz mit profitablen Produkten im selben Zeitraum</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="divider">
|
||||
|
||||
<!-- ── PROBLEM ── -->
|
||||
<section id="problem" class="section">
|
||||
<div class="label-tag reveal">Das Problem</div>
|
||||
<h2 class="section-title reveal">Dein ROAS <em>lügt dich an.</em></h2>
|
||||
<p class="section-lead reveal">Klassische Tools zeigen dir Umsatz — nicht Marge. Du weißt nicht welche Produkte wirklich Geld verdienen, und welche dein Budget verbrennen.</p>
|
||||
|
||||
<div class="problem-grid">
|
||||
<div class="prob-card reveal">
|
||||
<div class="prob-num">01 — PROBLEM</div>
|
||||
<div class="prob-icon">
|
||||
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>
|
||||
</div>
|
||||
<div class="prob-title">Feed-Tools kennen keinen EK</div>
|
||||
<p class="prob-text">Channable, DataFeedWatch & Co. syndizieren Daten. Sie wissen nichts über deine Einkaufspreise, Versandkosten oder echte Marge pro Produkt.</p>
|
||||
</div>
|
||||
<div class="prob-card reveal reveal-delay-1">
|
||||
<div class="prob-num">02 — PROBLEM</div>
|
||||
<div class="prob-icon">
|
||||
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="1" y1="1" x2="23" y2="23"/><path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"/><path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"/><path d="M10.71 5.05A16 16 0 0 1 22.56 9"/><path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg>
|
||||
</div>
|
||||
<div class="prob-title">Tracking bricht täglich</div>
|
||||
<p class="prob-text">Adblocker, ITP, Cookie-Verweigerer. Client-Side Tracking verliert 20–40% der Conversions. Du optimierst auf Phantomdaten.</p>
|
||||
</div>
|
||||
<div class="prob-card reveal reveal-delay-2">
|
||||
<div class="prob-num">03 — PROBLEM</div>
|
||||
<div class="prob-icon">
|
||||
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="9" x2="19" y2="9"/><line x1="5" y1="15" x2="19" y2="15"/><line x1="18" y1="4" x2="6" y2="20"/></svg>
|
||||
</div>
|
||||
<div class="prob-title">ROAS ≠ Profitabilität</div>
|
||||
<p class="prob-text">ROAS 5 auf einem Produkt mit 8% Marge. Minus. Du weißt es nicht — und Google bietet weiter drauf. Das ist das eigentliche Problem.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divider">
|
||||
|
||||
<!-- ── SOLUTION ── -->
|
||||
<section id="solution" class="section">
|
||||
<div class="label-tag reveal">Die Lösung</div>
|
||||
<h2 class="section-title reveal">Profit-Intelligenz,<br><em>nicht Feed-Sync.</em></h2>
|
||||
|
||||
<div class="how-grid">
|
||||
<div class="how-steps reveal">
|
||||
<div class="how-step">
|
||||
<span class="step-num">01</span>
|
||||
<div>
|
||||
<div class="step-title">Datenbasis aus deinem WaWi</div>
|
||||
<p class="step-desc">Feedgine liest EK, Versandkosten und alle relevanten Margen-Felder direkt aus JTL-Wawi. Keine manuelle Pflege.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="how-step">
|
||||
<span class="step-num">02</span>
|
||||
<div>
|
||||
<div class="step-title">Bereinigung & Margenkalkulation</div>
|
||||
<p class="step-desc">Jedes Produkt bekommt eine echte Deckungsbeitragsberechnung. Daten werden bereinigt, normiert und POAS-tauglich gemacht.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="how-step">
|
||||
<span class="step-num">03</span>
|
||||
<div>
|
||||
<div class="step-title">POAS Custom Labels</div>
|
||||
<p class="step-desc">Produkte werden automatisch in Profit-Tiers eingeteilt. Google Shopping und PMax bieten auf Basis echter Marge — nicht Umsatz.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="how-step">
|
||||
<span class="step-num">04</span>
|
||||
<div>
|
||||
<div class="step-title">Server Side Tracking</div>
|
||||
<p class="step-desc">Cookie-optional. First-Party. Tracking-Coverage über 95% — unabhängig von Browser, Adblocker oder Consent-Rate.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="how-step">
|
||||
<span class="step-num">05</span>
|
||||
<div>
|
||||
<div class="step-title">Multi-Channel Ausgabe</div>
|
||||
<p class="step-desc">Google Shopping, Meta CAPI und Microsoft Ads — täglich aktualisiert, ohne Drittanbieter-Abo.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="how-visual reveal">
|
||||
<div class="vis-label">Profit-Analyse · Live</div>
|
||||
<div class="poas-wrap">
|
||||
<div class="poas-bar-label"><span>Top-Seller → Werkzeuge</span><span>POAS 4.2</span></div>
|
||||
<div class="poas-track"><div class="poas-fill g" style="width:84%"></div></div>
|
||||
</div>
|
||||
<div class="poas-wrap">
|
||||
<div class="poas-bar-label"><span>Mittelfeld → Zubehör</span><span>POAS 2.8</span></div>
|
||||
<div class="poas-track"><div class="poas-fill a" style="width:56%"></div></div>
|
||||
</div>
|
||||
<div class="poas-wrap">
|
||||
<div class="poas-bar-label"><span>Verlust → Sonderartikel</span><span>POAS 0.6</span></div>
|
||||
<div class="poas-track"><div class="poas-fill r"></div></div>
|
||||
</div>
|
||||
<div class="vis-divider"></div>
|
||||
<div class="vis-row"><span class="vis-k">Tracking vorher</span><span class="vis-v r">61%</span></div>
|
||||
<div class="vis-row"><span class="vis-k">Tracking nachher (SST)</span><span class="vis-v g">97%</span></div>
|
||||
<div class="vis-row"><span class="vis-k">Ads-Budget eingespart</span><span class="vis-v g">−23%</span></div>
|
||||
<div class="vis-row"><span class="vis-k">Profit. Umsatz</span><span class="vis-v g">+31%</span></div>
|
||||
<div class="vis-divider"></div>
|
||||
<div style="font-family:var(--mono);font-size:.6rem;color:var(--text-dim);line-height:1.6">
|
||||
Referenz: lkw-teile24.de<br>230.000 Produkte · JTL-Wawi · Google PMax
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divider">
|
||||
|
||||
<!-- ── MODULES ── -->
|
||||
<section id="modules" class="section">
|
||||
<div class="label-tag reveal">Was enthalten ist</div>
|
||||
<h2 class="section-title reveal">Drei Module.<br><em>Ein System.</em></h2>
|
||||
<p class="section-lead reveal">Kein Flickenteppich aus Einzellösungen. Feedgine verbindet Profitdaten, Feed-Intelligence und Tracking in einer Pipeline.</p>
|
||||
|
||||
<div class="modules-grid">
|
||||
<div class="module-card reveal">
|
||||
<div class="module-stack-num">01</div>
|
||||
<div class="module-num">MODUL 01 — PROFIT ENGINE</div>
|
||||
<div class="module-title">Margenkalkulation</div>
|
||||
<p class="module-desc">EK, Versand, Gebühren — alles berücksichtigt. POAS Custom Labels für Google Shopping und PMax auf Basis echter Deckungsbeiträge.</p>
|
||||
<ul class="module-features">
|
||||
<li>EK-Daten aus JTL-Wawi / MSSQL</li>
|
||||
<li>Automatische DB-Berechnung</li>
|
||||
<li>POAS-Tiering (Skala 1–5)</li>
|
||||
<li>Täglicher Update-Zyklus</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="module-card reveal reveal-delay-1">
|
||||
<div class="module-stack-num">02</div>
|
||||
<div class="module-num">MODUL 02 — FEED INTELLIGENCE</div>
|
||||
<div class="module-title">Datenpipeline</div>
|
||||
<p class="module-desc">Datenbereinigungs-Pipeline für Google, Meta und Microsoft. Kein Channable. Direkt aus deiner Datenbasis — täglich, automatisch.</p>
|
||||
<ul class="module-features">
|
||||
<li>Google Shopping / PMax</li>
|
||||
<li>Meta Product Catalog (CAPI)</li>
|
||||
<li>Microsoft Shopping Ads</li>
|
||||
<li>OE/OEM Nummer-Handling</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="module-card reveal reveal-delay-2">
|
||||
<div class="module-stack-num">03</div>
|
||||
<div class="module-num">MODUL 03 — SERVER SIDE TRACKING</div>
|
||||
<div class="module-title">First-Party Tracking</div>
|
||||
<p class="module-desc">Cookie-optionales Tracking auf EU-Infrastruktur. Kein Datenverlust durch Adblocker oder ITP. DSGVO-konform by design.</p>
|
||||
<ul class="module-features">
|
||||
<li>Google Ads Conversions (enhanced)</li>
|
||||
<li>Meta Conversion API</li>
|
||||
<li>GA4 Server Container</li>
|
||||
<li>Consent Mode v2 ready</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divider">
|
||||
|
||||
<!-- ── COMPARISON ── -->
|
||||
<section class="section">
|
||||
<div class="label-tag reveal">Marktvergleich</div>
|
||||
<h2 class="section-title reveal">Was andere kosten.<br><em>Was du bekommst.</em></h2>
|
||||
|
||||
<div class="compare-wrap reveal" style="margin-top:2.5rem">
|
||||
<table class="compare-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Feature</th>
|
||||
<th>Klassische Agentur</th>
|
||||
<th class="accent">Feedgine</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Echte Margenkalkulation (EK)</td>
|
||||
<td><span class="crs">✕</span></td>
|
||||
<td class="accent-col"><span class="chk">✓</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>POAS Custom Labels</td>
|
||||
<td>Manuell</td>
|
||||
<td class="accent-col"><span class="chk">✓ Automatisch</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Server Side Tracking</td>
|
||||
<td>Extra Kosten</td>
|
||||
<td class="accent-col"><span class="chk">✓ Enthalten</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Multi-Channel (Google + Meta + MS)</td>
|
||||
<td>Teilweise</td>
|
||||
<td class="accent-col"><span class="chk">✓</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>JTL-Wawi Integration</td>
|
||||
<td><span class="crs">✕</span></td>
|
||||
<td class="accent-col"><span class="chk">✓ Nativ</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>EU-Hosting / DSGVO</td>
|
||||
<td>Teilweise</td>
|
||||
<td class="accent-col"><span class="chk">✓ 100%</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Monatliche Kosten</td>
|
||||
<td>€2.000–5.000+</td>
|
||||
<td class="accent-col" style="color:var(--amber);font-weight:700">ab €999</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divider">
|
||||
|
||||
<!-- ── PRICING ── -->
|
||||
<section id="pricing" class="section">
|
||||
<div class="label-tag reveal">Pricing</div>
|
||||
<h2 class="section-title reveal">Kein Lock-in.<br><em>Kein Bullshit.</em></h2>
|
||||
<p class="section-lead reveal">Managed oder Self-Hosted — du entscheidest. Alle Preise netto zzgl. MwSt.</p>
|
||||
|
||||
<div class="pricing-grid">
|
||||
<div class="price-card featured reveal">
|
||||
<div class="price-badge">Empfohlen</div>
|
||||
<div class="price-tier">Gewinn-Check</div>
|
||||
<div class="price-amount">€1.000</div>
|
||||
<div class="price-period">einmalig</div>
|
||||
<ul class="price-features">
|
||||
<li><span>✓</span><span>Analyse deiner Ads & Daten</span></li>
|
||||
<li><span>✓</span><span>Identifikation von Verlusten</span></li>
|
||||
<li><span>✓</span><span>konkrete Maßnahmen</span></li>
|
||||
</ul>
|
||||
<a href="https://termine.profice.de" target="_blank" rel="noopener" class="price-cta">Jetzt starten →</a>
|
||||
</div>
|
||||
<div class="price-card reveal reveal-delay-1">
|
||||
<div class="price-tier">Feedgine Setup</div>
|
||||
<div class="price-amount">€4.999</div>
|
||||
<div class="price-period">einmalig</div>
|
||||
<ul class="price-features">
|
||||
<li><span>✓</span><span>Feed + POAS Setup</span></li>
|
||||
<li><span>✓</span><span>Tracking Fix</span></li>
|
||||
<li><span>✓</span><span>Margensteuerung</span></li>
|
||||
</ul>
|
||||
<a href="https://termine.profice.de" target="_blank" rel="noopener" class="price-cta">Setup anfragen →</a>
|
||||
</div>
|
||||
<div class="price-card reveal reveal-delay-2">
|
||||
<div class="price-tier">Feedgine Betrieb</div>
|
||||
<div class="price-amount">ab €1.500</div>
|
||||
<div class="price-period">/Monat</div>
|
||||
<ul class="price-features">
|
||||
<li><span>✓</span><span>Monitoring</span></li>
|
||||
<li><span>✓</span><span>Optimierung</span></li>
|
||||
<li><span>✓</span><span>Skalierung</span></li>
|
||||
</ul>
|
||||
<a href="https://termine.profice.de" target="_blank" rel="noopener" class="price-cta">Starten →</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divider">
|
||||
|
||||
<!-- ── TARGET AUDIENCE ── -->
|
||||
<section class="section">
|
||||
<div class="label-tag reveal">Zielgruppe</div>
|
||||
<h2 class="section-title reveal">Feedgine ist gebaut für<br><em>E-Commerce mit Ambition.</em></h2>
|
||||
|
||||
<div class="problem-grid" style="margin-top:2rem">
|
||||
<div class="prob-card reveal">
|
||||
<div class="prob-num">PROFIL 01</div>
|
||||
<div class="prob-title">JTL-Shop mit >10k Produkten</div>
|
||||
<p class="prob-text">Du nutzt JTL-Wawi und hast einen aktiven Google-Shopping-Feed. Du weißt, dass dein Feed besser sein könnte — aber dir fehlt die Marge-Transparenz.</p>
|
||||
</div>
|
||||
<div class="prob-card reveal reveal-delay-1">
|
||||
<div class="prob-num">PROFIL 02</div>
|
||||
<div class="prob-title">Adspend >€5k/Monat</div>
|
||||
<p class="prob-text">Du investierst signifikant in Google Ads oder Meta — und willst endlich wissen, welche Produkte echten Profit liefern und welche Budget verbrennen.</p>
|
||||
</div>
|
||||
<div class="prob-card reveal reveal-delay-2">
|
||||
<div class="prob-num">PROFIL 03</div>
|
||||
<div class="prob-title">ROAS-Reporting reicht nicht mehr</div>
|
||||
<p class="prob-text">Du hast das Gefühl, dass deine Agentur auf Umsatz optimiert statt auf Gewinn. Du brauchst Transparenz — keine Dashboards mit Vanity-Metriken.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divider">
|
||||
|
||||
<!-- ── CALCULATOR ── -->
|
||||
<section id="calculator">
|
||||
<div class="calculator-wrapper">
|
||||
<div class="label-tag reveal">Kalkulator</div>
|
||||
<h2 class="section-title reveal">Ihr persönlicher <em>Gewinn-Rechner</em></h2>
|
||||
<p class="section-lead reveal" style="margin-bottom:0">Geben Sie Ihre Zahlen ein — und sehen Sie sofort, wie viel mehr Profit durch optimierte Feed-Steuerung möglich ist.</p>
|
||||
|
||||
<div class="calculator-section">
|
||||
<div class="calc-inputs">
|
||||
<h3>Ihre Kennzahlen</h3>
|
||||
<div class="calc-field">
|
||||
<label>Werbebudget <span id="lbl-budget">10.000 €</span></label>
|
||||
<div class="calc-slider-row">
|
||||
<input type="range" id="sl-budget" min="1000" max="100000" step="500" value="10000">
|
||||
<input type="number" class="calc-num-input" id="num-budget" min="1000" max="100000" step="500" value="10000">
|
||||
</div>
|
||||
</div>
|
||||
<div class="calc-field">
|
||||
<label>Monatlicher Umsatz <span id="lbl-umsatz">50.000 €</span></label>
|
||||
<div class="calc-slider-row">
|
||||
<input type="range" id="sl-umsatz" min="5000" max="500000" step="1000" value="50000">
|
||||
<input type="number" class="calc-num-input" id="num-umsatz" min="5000" max="500000" step="1000" value="50000">
|
||||
</div>
|
||||
</div>
|
||||
<div class="calc-field">
|
||||
<label>Bruttomarge <span id="lbl-marge">30 %</span></label>
|
||||
<div class="calc-slider-row">
|
||||
<input type="range" id="sl-marge" min="10" max="80" step="1" value="30">
|
||||
<input type="number" class="calc-num-input" id="num-marge" min="10" max="80" step="1" value="30">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calc-outputs">
|
||||
<div class="calc-out-card">
|
||||
<div class="calc-out-label">Bisheriger Profit</div>
|
||||
<div class="calc-out-value" id="out-profit-alt">5.000 €</div>
|
||||
<div class="calc-out-sub">Vor der Feed-Optimierung</div>
|
||||
</div>
|
||||
<div class="calc-out-card highlight">
|
||||
<div class="calc-out-label">Neuer Profit</div>
|
||||
<div class="calc-out-value" id="out-profit-neu">—</div>
|
||||
<div class="calc-out-sub">Mit optimiertem Feed</div>
|
||||
</div>
|
||||
<div class="calc-out-card">
|
||||
<div class="calc-out-label">Ihr Hebel</div>
|
||||
<div class="calc-hebel-row">
|
||||
<div class="calc-hebel-chip" id="out-hebel-eur">—</div>
|
||||
<div class="calc-hebel-chip" id="out-hebel-pct">—</div>
|
||||
</div>
|
||||
<div class="calc-out-sub" style="margin-top:10px">Mehr Gewinn — gleiches Budget</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── CTA ── -->
|
||||
<div id="contact" class="cta-section">
|
||||
<div class="cta-eyebrow">Bereit?</div>
|
||||
<h2>Wisse endlich, welche<br>Produkte <em>Geld verdienen.</em></h2>
|
||||
<p>Kein Sales-Funnel. Kein Massen-Onboarding. Wir arbeiten mit ausgewählten E-Commerce-Betreibern — direkt, konkret, auf Augenhöhe.</p>
|
||||
<a href="https://termine.profice.de" target="_blank" rel="noopener" class="cta-btn" style="margin-top:2rem">Profit-Analyse buchen</a>
|
||||
</div>
|
||||
|
||||
<!-- ── FOOTER ── -->
|
||||
<footer>
|
||||
<div class="footer-grid">
|
||||
<div class="footer-brand">
|
||||
<img src="images/logo/logo-01-complete.png" alt="Profice Logo">
|
||||
<p>Profit Intelligence für E-Commerce.<br>POAS · Feed · Server-Side Tracking.</p>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>Feedgine</h4>
|
||||
<a href="#problem">Das Problem</a>
|
||||
<a href="#solution">Die Lösung</a>
|
||||
<a href="#modules">Module</a>
|
||||
<a href="#pricing">Pricing</a>
|
||||
<a href="#calculator">POAS-Kalkulator</a>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>Rechtliches</h4>
|
||||
<a href="https://profice.ai/sites/datenschutz.html" target="_blank" rel="noopener">Datenschutzerklärung</a>
|
||||
<a href="https://profice.ai/sites/impressum.html" target="_blank" rel="noopener">Impressum</a>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<h4>Kontakt</h4>
|
||||
<div class="footer-contact-item">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>
|
||||
<a href="mailto:hello@profice.ai">hello@profice.ai</a>
|
||||
</div>
|
||||
<div class="social-icons">
|
||||
<a href="https://instagram.com" target="_blank" rel="noopener noreferrer" class="social-link" title="Instagram">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>
|
||||
</a>
|
||||
<a href="https://facebook.com" target="_blank" rel="noopener noreferrer" class="social-link" title="Facebook">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 Profice GmbH. Alle Rechte vorbehalten.</p>
|
||||
<div class="footer-bottom-links">
|
||||
<a href="https://profice.ai/sites/datenschutz.html" target="_blank" rel="noopener">Datenschutz</a>
|
||||
<a href="https://profice.ai/sites/impressum.html" target="_blank" rel="noopener">Impressum</a>
|
||||
<a href="https://profice.ai/" target="_blank" rel="noopener">profice.ai</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- ── SCRIPTS ── -->
|
||||
<script src="scripts/feed-calculator.js" defer></script>
|
||||
|
||||
<script>
|
||||
// Reveal on scroll
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('visible') });
|
||||
}, { threshold: 0.12 });
|
||||
document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
|
||||
|
||||
// Mobile nav toggle
|
||||
const hamburger = document.getElementById('hamburger');
|
||||
const mobileNav = document.getElementById('mobileNav');
|
||||
const overlay = document.getElementById('overlay');
|
||||
|
||||
function toggleNav() {
|
||||
hamburger.classList.toggle('open');
|
||||
mobileNav.classList.toggle('open');
|
||||
overlay.classList.toggle('active');
|
||||
}
|
||||
|
||||
hamburger?.addEventListener('click', toggleNav);
|
||||
overlay?.addEventListener('click', toggleNav);
|
||||
mobileNav?.querySelectorAll('a').forEach(a => a.addEventListener('click', () => {
|
||||
if (mobileNav.classList.contains('open')) toggleNav();
|
||||
}));
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
25
nginx.conf
Normal file
@@ -0,0 +1,25 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Redirect /index.html to /
|
||||
location = /index.html {
|
||||
return 301 /;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Cache static assets
|
||||
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/javascript image/svg+xml;
|
||||
}
|
||||
196
scripts/add/send.php
Normal file
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
/**
|
||||
* Feedgine Web API Handler
|
||||
* ALL SENSITIVE DATA IS IN config.php — NOT HERE
|
||||
* Handles: contact forms, KI chat, cookie consent, tracking events
|
||||
*/
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/config.php';
|
||||
|
||||
// ============================================================
|
||||
// SECURITY HEADERS
|
||||
// ============================================================
|
||||
header('Content-Type: application/json');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('X-Frame-Options: DENY');
|
||||
header('X-XSS-Protection: 1; mode=block');
|
||||
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||
|
||||
// CORS
|
||||
$allowedOrigins = unserialize(ALLOWED_ORIGINS);
|
||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||
if (in_array($origin, $allowedOrigins)) {
|
||||
header("Access-Control-Allow-Origin: $origin");
|
||||
} else {
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
}
|
||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
header('Access-Control-Max-Age: 86400');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================================
|
||||
|
||||
function sendResponse($success, $message, $data = null, $statusCode = 200) {
|
||||
http_response_code($statusCode);
|
||||
$response = ['success' => $success, 'message' => $message, 'timestamp' => date('c')];
|
||||
if ($data !== null && (!USE_PRODUCTION || DEBUG_MODE)) {
|
||||
$response['data'] = $data;
|
||||
}
|
||||
echo json_encode($response);
|
||||
exit();
|
||||
}
|
||||
|
||||
function sanitizeInput($input) {
|
||||
return htmlspecialchars(trim($input ?? ''), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function getClientIP() {
|
||||
foreach (['HTTP_CF_CONNECTING_IP','HTTP_X_FORWARDED_FOR','HTTP_X_REAL_IP','REMOTE_ADDR'] as $key) {
|
||||
if (!empty($_SERVER[$key])) {
|
||||
$ip = explode(',', $_SERVER[$key])[0];
|
||||
return filter_var(trim($ip), FILTER_VALIDATE_IP) ?: 'unknown';
|
||||
}
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function checkRateLimit($ip) {
|
||||
$file = __DIR__ . '/data/rate_limits.json';
|
||||
$limits = file_exists($file) ? json_decode(file_get_contents($file), true) : [];
|
||||
$now = time();
|
||||
// Clean old entries
|
||||
foreach ($limits as $k => $v) {
|
||||
if ($now - $v['first'] > RATE_LIMIT_WINDOW) unset($limits[$k]);
|
||||
}
|
||||
if (!isset($limits[$ip])) {
|
||||
$limits[$ip] = ['count' => 1, 'first' => $now];
|
||||
} else {
|
||||
$limits[$ip]['count']++;
|
||||
if ($limits[$ip]['count'] > RATE_LIMIT_REQUESTS) {
|
||||
file_put_contents($file, json_encode($limits));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
file_put_contents($file, json_encode($limits));
|
||||
return true;
|
||||
}
|
||||
|
||||
function sendToWebhook($data, $webhookUrl) {
|
||||
$payload = json_encode($data);
|
||||
if (function_exists('curl_init')) {
|
||||
$ch = curl_init($webhookUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Content-Length: ' . strlen($payload)],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
]);
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
return ['success' => $httpCode >= 200 && $httpCode < 300, 'http_code' => $httpCode, 'response' => $response, 'error' => $error];
|
||||
}
|
||||
// Fallback
|
||||
$ctx = stream_context_create(['http' => [
|
||||
'method' => 'POST',
|
||||
'header' => "Content-Type: application/json\r\nContent-Length: " . strlen($payload) . "\r\n",
|
||||
'content' => $payload,
|
||||
'timeout' => 15,
|
||||
]]);
|
||||
$result = @file_get_contents($webhookUrl, false, $ctx);
|
||||
return ['success' => $result !== false, 'response' => $result, 'method' => 'file_get_contents'];
|
||||
}
|
||||
|
||||
function storeLead($data) {
|
||||
$file = __DIR__ . '/data/leads.json';
|
||||
$leads = file_exists($file) ? json_decode(file_get_contents($file), true) : [];
|
||||
$leads[] = array_merge($data, ['timestamp' => date('c'), 'ip' => getClientIP()]);
|
||||
if (count($leads) > 200) $leads = array_slice($leads, -200);
|
||||
file_put_contents($file, json_encode($leads, JSON_PRETTY_PRINT));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// REQUEST HANDLERS
|
||||
// ============================================================
|
||||
|
||||
function handleContactForm($data) {
|
||||
$name = sanitizeInput($data['name'] ?? '');
|
||||
$contact = sanitizeInput($data['contact'] ?? '');
|
||||
$message = sanitizeInput($data['message'] ?? '');
|
||||
$company = sanitizeInput($data['company'] ?? '');
|
||||
|
||||
if (empty($name) || empty($contact)) {
|
||||
sendResponse(false, 'Name und Kontakt sind Pflichtfelder.', null, 400);
|
||||
}
|
||||
|
||||
$payload = ['source' => 'feedgine.de', 'name' => $name, 'contact' => $contact,
|
||||
'message' => $message, 'company' => $company, 'timestamp' => date('c'), 'ip' => getClientIP()];
|
||||
storeLead($payload);
|
||||
sendToWebhook($payload, FEEDGINE_WEBHOOK_URL);
|
||||
sendResponse(true, 'Anfrage erfolgreich übermittelt. Wir melden uns in Kürze.');
|
||||
}
|
||||
|
||||
function handleChatMessage($data) {
|
||||
$message = sanitizeInput($data['message'] ?? '');
|
||||
$session_id = sanitizeInput($data['session_id'] ?? '');
|
||||
|
||||
if (empty($message)) {
|
||||
sendResponse(false, 'Nachricht darf nicht leer sein.', null, 400);
|
||||
}
|
||||
|
||||
$payload = ['message' => $message, 'session_id' => $session_id, 'source' => 'feedgine.de'];
|
||||
$result = sendToWebhook($payload, KI_CHAT_WEBHOOK_URL);
|
||||
$botReply = 'Vielen Dank für Ihre Nachricht. Unser Team meldet sich schnellstmöglich.';
|
||||
|
||||
if ($result['success'] && !empty($result['response'])) {
|
||||
$decoded = json_decode($result['response'], true);
|
||||
foreach (['message','output','text','response','answer','result'] as $key) {
|
||||
if (!empty($decoded[$key])) { $botReply = $decoded[$key]; break; }
|
||||
}
|
||||
}
|
||||
sendResponse(true, 'OK', ['message' => $botReply, 'session_id' => $session_id]);
|
||||
}
|
||||
|
||||
function handleCookieConsent($data) {
|
||||
$file = __DIR__ . '/data/cookie_consent.json';
|
||||
$records = file_exists($file) ? json_decode(file_get_contents($file), true) : [];
|
||||
$records[] = ['consent' => $data, 'timestamp' => date('c'), 'ip' => getClientIP()];
|
||||
if (count($records) > 1000) $records = array_slice($records, -1000);
|
||||
file_put_contents($file, json_encode($records));
|
||||
sendResponse(true, 'Consent gespeichert.');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ROUTER
|
||||
// ============================================================
|
||||
|
||||
$ip = getClientIP();
|
||||
if (!checkRateLimit($ip)) {
|
||||
sendResponse(false, 'Zu viele Anfragen. Bitte warten.', null, 429);
|
||||
}
|
||||
|
||||
$raw = file_get_contents('php://input');
|
||||
$body = json_decode($raw, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE || empty($body)) {
|
||||
sendResponse(false, 'Ungültige Anfrage.', null, 400);
|
||||
}
|
||||
|
||||
$type = sanitizeInput($body['type'] ?? '');
|
||||
|
||||
switch ($type) {
|
||||
case 'contact': handleContactForm($body); break;
|
||||
case 'chat': handleChatMessage($body); break;
|
||||
case 'cookie_consent': handleCookieConsent($body); break;
|
||||
default: sendResponse(false, "Unbekannter Typ: $type", null, 400);
|
||||
}
|
||||
256
scripts/cursor.js
Normal file
@@ -0,0 +1,256 @@
|
||||
// cursor.js — Venom/Spider Cursor · Space Edition (white + neon)
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
if (window.matchMedia("(pointer: coarse)").matches) return;
|
||||
|
||||
/* ── CONFIG ── */
|
||||
const CONFIG = {
|
||||
tentacleCount: 10,
|
||||
triggerDist: 8,
|
||||
maxLength: 300,
|
||||
connectionDist: 150,
|
||||
prediction: 3.5,
|
||||
|
||||
// Idle: cold silver-white
|
||||
idleStroke: { r: 210, g: 228, b: 255 },
|
||||
idleGlow: { r: 160, g: 200, b: 255 },
|
||||
|
||||
// Hover: neon orange (matches site accent)
|
||||
hoverStroke: { r: 255, g: 102, b: 0 },
|
||||
hoverGlow: { r: 255, g: 80, b: 0 },
|
||||
};
|
||||
|
||||
/* ── toggle ── */
|
||||
const toggleBtn = document.getElementById('cursorToggle');
|
||||
const body = document.body;
|
||||
|
||||
let isCursorDisabled = localStorage.getItem('venomCursorDisabled') !== 'false';
|
||||
|
||||
function updateCursorState() {
|
||||
if (isCursorDisabled) {
|
||||
body.classList.add('system-cursor');
|
||||
document.documentElement.style.cursor = '';
|
||||
if (toggleBtn) {
|
||||
toggleBtn.classList.remove('active');
|
||||
const icon = toggleBtn.querySelector('.cursor-icon');
|
||||
if (icon && icon.tagName === 'IMG') {
|
||||
icon.src = 'images/icons/spider.png';
|
||||
icon.alt = 'Spider Cursor';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
body.classList.remove('system-cursor');
|
||||
document.documentElement.style.cursor = 'none';
|
||||
if (toggleBtn) {
|
||||
toggleBtn.classList.add('active');
|
||||
const icon = toggleBtn.querySelector('.cursor-icon');
|
||||
if (icon && icon.tagName === 'IMG') {
|
||||
icon.src = 'images/additional/cursor.png';
|
||||
icon.alt = 'Custom Cursor';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateCursorState();
|
||||
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
isCursorDisabled = !isCursorDisabled;
|
||||
localStorage.setItem('venomCursorDisabled', isCursorDisabled);
|
||||
updateCursorState();
|
||||
});
|
||||
}
|
||||
|
||||
/* ── canvas ── */
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.id = 'venom-cursor';
|
||||
document.body.appendChild(canvas);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
function resize() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
|
||||
/* ── state ── */
|
||||
const mouse = { x: 0, y: 0 };
|
||||
const oldMouse = { x: 0, y: 0 };
|
||||
let isHover = false;
|
||||
let colorT = 0; // 0 = idle, 1 = hover (lerped)
|
||||
|
||||
const tentacles = [];
|
||||
let rotation = 0; // reticle ring slow spin
|
||||
|
||||
/* ── hover selector ── */
|
||||
const HOVER_SEL = [
|
||||
'a', 'button', '[role="button"]', 'select', 'label',
|
||||
'.nav-link', '.btn-primary', '.btn-ghost', '.nav-demo-btn',
|
||||
'.price-cta', '.prob-card', '.module-card', '.how-step',
|
||||
'.dropdown-item', '.mobile-nav-link', '.mobile-nav-cta',
|
||||
'[onclick]', '.clickable'
|
||||
].join(',');
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
mouse.x = e.clientX;
|
||||
mouse.y = e.clientY;
|
||||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||
isHover = !!el && (el.matches(HOVER_SEL) || !!el.closest(HOVER_SEL));
|
||||
}, { passive: true });
|
||||
|
||||
/* ── colour helper ── */
|
||||
function lerpColor(a, b, t) {
|
||||
return {
|
||||
r: a.r + (b.r - a.r) * t | 0,
|
||||
g: a.g + (b.g - a.g) * t | 0,
|
||||
b: a.b + (b.b - a.b) * t | 0,
|
||||
};
|
||||
}
|
||||
|
||||
/* ── Tentacle ── */
|
||||
class Tentacle {
|
||||
constructor(targetX, targetY) {
|
||||
this.anchor = { x: targetX, y: targetY };
|
||||
this.dead = false;
|
||||
this.dist = 0;
|
||||
this.age = 0;
|
||||
}
|
||||
|
||||
update() {
|
||||
const dx = mouse.x - this.anchor.x;
|
||||
const dy = mouse.y - this.anchor.y;
|
||||
this.dist = Math.sqrt(dx * dx + dy * dy);
|
||||
this.age++;
|
||||
if (this.dist > CONFIG.maxLength) this.dead = true;
|
||||
}
|
||||
|
||||
draw(stroke, glow) {
|
||||
if (this.dead) return;
|
||||
|
||||
const tension = Math.min(this.dist / CONFIG.maxLength, 1);
|
||||
|
||||
// Fade in over first 10 frames
|
||||
const fadeIn = Math.min(this.age / 10, 1);
|
||||
const lineAlpha = fadeIn * (1 - tension * 0.85) * 0.70;
|
||||
const dotAlpha = fadeIn * (1 - tension) * 0.90;
|
||||
const lw = Math.max(0.2, 1.1 * (1 - tension * 0.80));
|
||||
|
||||
// Glow pass (wide, soft)
|
||||
ctx.save();
|
||||
ctx.shadowColor = `rgba(${glow.r},${glow.g},${glow.b},${lineAlpha * 0.55})`;
|
||||
ctx.shadowBlur = 6;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(mouse.x, mouse.y);
|
||||
ctx.lineTo(this.anchor.x, this.anchor.y);
|
||||
ctx.strokeStyle = `rgba(${stroke.r},${stroke.g},${stroke.b},${lineAlpha})`;
|
||||
ctx.lineWidth = lw;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
|
||||
// Anchor dot
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.anchor.x, this.anchor.y, 1.8 * (1 - tension), 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(${stroke.r},${stroke.g},${stroke.b},${dotAlpha})`;
|
||||
ctx.shadowColor = `rgba(${glow.r},${glow.g},${glow.b},${dotAlpha * 0.6})`;
|
||||
ctx.shadowBlur = 4;
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── render ── */
|
||||
function render() {
|
||||
if (isCursorDisabled) {
|
||||
requestAnimationFrame(render);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Lerp color T
|
||||
colorT += (isHover ? 1 : 0 - colorT) * 0.12;
|
||||
|
||||
const stroke = lerpColor(CONFIG.idleStroke, CONFIG.hoverStroke, colorT);
|
||||
const glow = lerpColor(CONFIG.idleGlow, CONFIG.hoverGlow, colorT);
|
||||
|
||||
/* spawn tentacle on movement */
|
||||
const moved = Math.hypot(mouse.x - oldMouse.x, mouse.y - oldMouse.y);
|
||||
if (moved > CONFIG.triggerDist) {
|
||||
const vx = mouse.x - oldMouse.x;
|
||||
const vy = mouse.y - oldMouse.y;
|
||||
const tx = mouse.x + vx * CONFIG.prediction + (Math.random() - 0.5) * 60;
|
||||
const ty = mouse.y + vy * CONFIG.prediction + (Math.random() - 0.5) * 60;
|
||||
tentacles.push(new Tentacle(tx, ty));
|
||||
oldMouse.x = mouse.x;
|
||||
oldMouse.y = mouse.y;
|
||||
}
|
||||
if (tentacles.length > CONFIG.tentacleCount) tentacles.shift();
|
||||
|
||||
/* update + draw tentacles */
|
||||
for (let i = tentacles.length - 1; i >= 0; i--) {
|
||||
tentacles[i].update();
|
||||
if (tentacles[i].dead) tentacles.splice(i, 1);
|
||||
else tentacles[i].draw(stroke, glow);
|
||||
}
|
||||
|
||||
/* web connections between close anchor pairs */
|
||||
for (let i = 0; i < tentacles.length; i++) {
|
||||
for (let j = i + 1; j < tentacles.length; j++) {
|
||||
const a = tentacles[i].anchor;
|
||||
const b = tentacles[j].anchor;
|
||||
const d = Math.hypot(a.x - b.x, a.y - b.y);
|
||||
if (d < CONFIG.connectionDist) {
|
||||
const alpha = (1 - d / CONFIG.connectionDist) * 0.28;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(a.x, a.y);
|
||||
ctx.lineTo(b.x, b.y);
|
||||
ctx.strokeStyle = `rgba(${stroke.r},${stroke.g},${stroke.b},${alpha})`;
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* cursor shape — segmented reticle ring */
|
||||
rotation += isHover ? 0.040 : 0.016;
|
||||
|
||||
const ringR = isHover ? 9 : 7;
|
||||
const gapHalf = 0.28; // gap half-angle in radians at each cardinal point
|
||||
const segments = [
|
||||
[rotation + gapHalf, rotation + Math.PI * 0.5 - gapHalf],
|
||||
[rotation + Math.PI * 0.5 + gapHalf, rotation + Math.PI - gapHalf],
|
||||
[rotation + Math.PI + gapHalf, rotation + Math.PI * 1.5 - gapHalf],
|
||||
[rotation + Math.PI * 1.5 + gapHalf, rotation + Math.PI * 2.0 - gapHalf],
|
||||
];
|
||||
|
||||
ctx.save();
|
||||
ctx.shadowColor = `rgba(${glow.r},${glow.g},${glow.b},0.75)`;
|
||||
ctx.shadowBlur = isHover ? 14 : 8;
|
||||
ctx.strokeStyle = `rgba(${stroke.r},${stroke.g},${stroke.b},0.90)`;
|
||||
ctx.lineWidth = isHover ? 1.4 : 1.1;
|
||||
ctx.lineCap = 'round';
|
||||
for (const [start, end] of segments) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(mouse.x, mouse.y, ringR, start, end);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
// Center dot — exact cursor tip
|
||||
ctx.save();
|
||||
ctx.shadowColor = `rgba(${glow.r},${glow.g},${glow.b},0.90)`;
|
||||
ctx.shadowBlur = isHover ? 10 : 6;
|
||||
ctx.beginPath();
|
||||
ctx.arc(mouse.x, mouse.y, isHover ? 2.2 : 1.6, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(${stroke.r},${stroke.g},${stroke.b},1)`;
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
|
||||
render();
|
||||
});
|
||||
85
scripts/feed-calculator.js
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Gewinn-Calculator — Feed Page
|
||||
* Real-time profit calculator with synchronized sliders and number inputs.
|
||||
*/
|
||||
(function () {
|
||||
function fmt(n) {
|
||||
return n.toLocaleString('de-DE', { maximumFractionDigits: 0 }) + ' €';
|
||||
}
|
||||
|
||||
function fmtPct(n) {
|
||||
return (n >= 0 ? '+' : '') + n.toLocaleString('de-DE', { maximumFractionDigits: 0 }) + ' %';
|
||||
}
|
||||
|
||||
var WASTE = 0.20; // Anteil Budget-Fresser — fixed at 20%
|
||||
|
||||
function calc() {
|
||||
var B = +document.getElementById('sl-budget').value;
|
||||
var U = +document.getElementById('sl-umsatz').value;
|
||||
var m = +document.getElementById('sl-marge').value / 100;
|
||||
var w = WASTE;
|
||||
|
||||
var P_alt = U * m - B;
|
||||
var roas_core = U / (B * (1 - w));
|
||||
var U_neu = U + B * w * roas_core;
|
||||
var P_neu = U_neu * m - B;
|
||||
var wachstum = P_alt !== 0 ? ((P_neu - P_alt) / Math.abs(P_alt)) * 100 : 0;
|
||||
var hebel = P_neu - P_alt;
|
||||
|
||||
document.getElementById('out-profit-alt').textContent = fmt(Math.round(P_alt));
|
||||
document.getElementById('out-profit-neu').textContent = fmt(Math.round(P_neu));
|
||||
document.getElementById('out-hebel-eur').textContent = (hebel >= 0 ? '+' : '') + fmt(Math.round(hebel)).replace(' €', '') + ' €';
|
||||
document.getElementById('out-hebel-pct').textContent = fmtPct(Math.round(wachstum));
|
||||
}
|
||||
|
||||
function syncFromSlider(slId, numId, lblId, unit) {
|
||||
var sl = document.getElementById(slId);
|
||||
var num = document.getElementById(numId);
|
||||
var lbl = document.getElementById(lblId);
|
||||
|
||||
function updateLbl(v) {
|
||||
if (unit === '€') {
|
||||
lbl.textContent = Number(v).toLocaleString('de-DE') + ' €';
|
||||
} else {
|
||||
lbl.textContent = v + ' %';
|
||||
}
|
||||
}
|
||||
|
||||
sl.addEventListener('input', function () {
|
||||
num.value = sl.value;
|
||||
updateLbl(sl.value);
|
||||
calc();
|
||||
});
|
||||
|
||||
num.addEventListener('input', function () {
|
||||
var v = Math.min(+num.max, Math.max(+num.min, +num.value || +num.min));
|
||||
sl.value = v;
|
||||
updateLbl(v);
|
||||
calc();
|
||||
});
|
||||
|
||||
num.addEventListener('change', function () {
|
||||
var v = Math.min(+num.max, Math.max(+num.min, +num.value || +num.min));
|
||||
num.value = v;
|
||||
sl.value = v;
|
||||
updateLbl(v);
|
||||
calc();
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (!document.getElementById('sl-budget')) return;
|
||||
|
||||
syncFromSlider('sl-budget', 'num-budget', 'lbl-budget', '€');
|
||||
syncFromSlider('sl-umsatz', 'num-umsatz', 'lbl-umsatz', '€');
|
||||
syncFromSlider('sl-marge', 'num-marge', 'lbl-marge', '%');
|
||||
|
||||
calc();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
131
scripts/feedgine.js
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* feedgine.js — Main page interactions
|
||||
* - Scroll animations (IntersectionObserver)
|
||||
* - Bar animation for the profit visual
|
||||
* - Terminal pulse effect
|
||||
* - Smooth scroll for anchor links
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ---- SCROLL ANIMATIONS ----
|
||||
const obs = new IntersectionObserver((entries) => {
|
||||
entries.forEach(e => {
|
||||
if (e.isIntersecting) {
|
||||
e.target.classList.add('visible');
|
||||
obs.unobserve(e.target);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.1 });
|
||||
|
||||
document.querySelectorAll('.animate-in').forEach(el => obs.observe(el));
|
||||
|
||||
// ---- BAR ANIMATION ----
|
||||
const barObs = new IntersectionObserver((entries) => {
|
||||
entries.forEach(e => {
|
||||
if (e.isIntersecting) {
|
||||
e.target.querySelectorAll('.poas-fill').forEach(bar => {
|
||||
const w = bar.style.width;
|
||||
bar.style.width = '0%';
|
||||
setTimeout(() => { bar.style.width = w; }, 150);
|
||||
});
|
||||
barObs.unobserve(e.target);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.3 });
|
||||
|
||||
document.querySelectorAll('.how-visual').forEach(el => barObs.observe(el));
|
||||
|
||||
// ---- TERMINAL PULSE ----
|
||||
setInterval(() => {
|
||||
document.querySelectorAll('.t-val').forEach(v => {
|
||||
v.style.transition = 'opacity 0.3s';
|
||||
v.style.opacity = '0.4';
|
||||
setTimeout(() => { v.style.opacity = '1'; }, 300);
|
||||
});
|
||||
}, 3500);
|
||||
|
||||
// ---- HAMBURGER MENU ----
|
||||
const hamburger = document.getElementById('hamburger');
|
||||
const mobileNav = document.getElementById('mobileNav');
|
||||
|
||||
function closeMobileNav() {
|
||||
if (!hamburger || !mobileNav) return;
|
||||
hamburger.classList.remove('open');
|
||||
hamburger.setAttribute('aria-expanded', 'false');
|
||||
mobileNav.classList.remove('open');
|
||||
mobileNav.setAttribute('aria-hidden', 'true');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
if (hamburger && mobileNav) {
|
||||
hamburger.addEventListener('click', () => {
|
||||
const isOpen = hamburger.classList.contains('open');
|
||||
if (isOpen) {
|
||||
closeMobileNav();
|
||||
} else {
|
||||
hamburger.classList.add('open');
|
||||
hamburger.setAttribute('aria-expanded', 'true');
|
||||
mobileNav.classList.add('open');
|
||||
mobileNav.setAttribute('aria-hidden', 'false');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
});
|
||||
|
||||
// Close on any nav link click
|
||||
mobileNav.querySelectorAll('a').forEach(a => {
|
||||
a.addEventListener('click', closeMobileNav);
|
||||
});
|
||||
|
||||
// Close on overlay click or Escape key
|
||||
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeMobileNav(); });
|
||||
}
|
||||
|
||||
// ---- SMOOTH SCROLL ----
|
||||
document.querySelectorAll('a[href^="#"]').forEach(a => {
|
||||
a.addEventListener('click', (e) => {
|
||||
const href = a.getAttribute('href');
|
||||
if (!href || href === '#') return;
|
||||
const target = document.querySelector(href);
|
||||
if (!target) return;
|
||||
e.preventDefault();
|
||||
const offset = 100;
|
||||
const top = target.getBoundingClientRect().top + window.pageYOffset - offset;
|
||||
window.scrollTo({ top, behavior: 'smooth' });
|
||||
});
|
||||
});
|
||||
|
||||
// ---- CONTACT FORM SUBMIT ----
|
||||
const form = document.getElementById('contactForm');
|
||||
if (form) {
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const btn = form.querySelector('button[type="submit"]');
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Wird gesendet…'; }
|
||||
|
||||
const data = {
|
||||
type: 'contact',
|
||||
name: form.querySelector('[name="name"]')?.value || '',
|
||||
contact: form.querySelector('[name="contact"]')?.value || '',
|
||||
message: form.querySelector('[name="message"]')?.value || '',
|
||||
company: form.querySelector('[name="company"]')?.value || '',
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('scripts/add/send.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const json = await res.json();
|
||||
const msg = form.querySelector('.form-success');
|
||||
if (msg) { msg.style.display = 'block'; }
|
||||
form.reset();
|
||||
} catch (err) {
|
||||
console.error('Form error:', err);
|
||||
} finally {
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Demo anfragen →'; }
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
531
scripts/hex-background.js
Normal file
@@ -0,0 +1,531 @@
|
||||
/**
|
||||
* Space Background — Glass Shards · Animated Nebula · Drifting Stars · Shooting Stars
|
||||
* No hex grid. Cursor scatters floating glass fragments.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
let canvas, ctx;
|
||||
let W = 0, H = 0;
|
||||
let mouse = { x: -2000, y: -2000 };
|
||||
let rafId;
|
||||
let initialized = false;
|
||||
let time = 0;
|
||||
|
||||
/* ──────────────────────────────────────────────
|
||||
NEBULA BLOBS (animated radial gradients) — dimmed
|
||||
────────────────────────────────────────────── */
|
||||
const NEBULAS = [
|
||||
{ px: 0.12, py: 0.25, pr: 0.50, cr: 255, cg: 80, cb: 0, a: 0.040, spx: 0.00014, spy: 0.00007, phase: 0.0 },
|
||||
{ px: 0.82, py: 0.55, pr: 0.55, cr: 0, cg: 255, cb: 136, a: 0.028, spx:-0.00009, spy: 0.00011, phase: 2.1 },
|
||||
{ px: 0.50, py: 0.88, pr: 0.42, cr: 0, cg: 180, cb: 255, a: 0.022, spx: 0.00007, spy:-0.00009, phase: 4.3 },
|
||||
{ px: 0.72, py: 0.12, pr: 0.38, cr: 160, cg: 0, cb: 255, a: 0.016, spx:-0.00010, spy: 0.00005, phase: 1.5 },
|
||||
{ px: 0.35, py: 0.65, pr: 0.32, cr: 255, cg: 140, cb: 0, a: 0.016, spx: 0.00005, spy:-0.00006, phase: 3.7 },
|
||||
];
|
||||
|
||||
function drawNebulas() {
|
||||
for (let i = 0; i < NEBULAS.length; i++) {
|
||||
const n = NEBULAS[i];
|
||||
const ox = Math.sin(time * n.spx * 800 + n.phase) * W * 0.10;
|
||||
const oy = Math.cos(time * n.spy * 800 + n.phase + 1.2) * H * 0.10;
|
||||
const cx = n.px * W + ox;
|
||||
const cy = n.py * H + oy;
|
||||
const rad = n.pr * Math.min(W, H);
|
||||
const pa = n.a * (0.65 + 0.35 * Math.sin(time * 0.28 + n.phase));
|
||||
|
||||
const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, rad);
|
||||
g.addColorStop(0, `rgba(${n.cr},${n.cg},${n.cb},${pa})`);
|
||||
g.addColorStop(0.45,`rgba(${n.cr},${n.cg},${n.cb},${pa * 0.35})`);
|
||||
g.addColorStop(1, `rgba(${n.cr},${n.cg},${n.cb},0)`);
|
||||
ctx.fillStyle = g;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, rad, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────
|
||||
DRIFTING STARS — dimmed halos
|
||||
────────────────────────────────────────────── */
|
||||
const STAR_COUNT = 220;
|
||||
let stars = [];
|
||||
|
||||
function initStars() {
|
||||
stars = [];
|
||||
for (let i = 0; i < STAR_COUNT; i++) {
|
||||
stars.push({
|
||||
x: Math.random() * W,
|
||||
y: Math.random() * H,
|
||||
r: 0.35 + Math.random() * 1.65,
|
||||
vx: (Math.random() - 0.5) * 0.055,
|
||||
vy: 0.018 + Math.random() * 0.055,
|
||||
phase: Math.random() * Math.PI * 2,
|
||||
twinkleSpeed: 0.4 + Math.random() * 1.6,
|
||||
// 0=white 1=neon-green 2=neon-teal
|
||||
type: Math.random() < 0.78 ? 0 : (Math.random() < 0.5 ? 1 : 2),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function drawStars() {
|
||||
for (let i = 0; i < stars.length; i++) {
|
||||
const s = stars[i];
|
||||
const tw = 0.25 + 0.75 * (0.5 + 0.5 * Math.sin(time * s.twinkleSpeed + s.phase));
|
||||
|
||||
s.x += s.vx;
|
||||
s.y += s.vy;
|
||||
if (s.y > H + 2) { s.y = -2; s.x = Math.random() * W; }
|
||||
if (s.x < -2) s.x = W + 2;
|
||||
if (s.x > W + 2) s.x = -2;
|
||||
|
||||
const clr = s.type === 0
|
||||
? `rgba(255,255,255,${tw * 0.70})`
|
||||
: s.type === 1
|
||||
? `rgba(0,255,136,${tw * 0.50})`
|
||||
: `rgba(0,220,255,${tw * 0.50})`;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
|
||||
ctx.fillStyle = clr;
|
||||
ctx.fill();
|
||||
|
||||
// Halo only on larger stars, dimmed
|
||||
if (s.r > 1.1) {
|
||||
const halo = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, s.r * 3.5);
|
||||
const ha = tw * (s.type === 0 ? 0.14 : 0.10);
|
||||
halo.addColorStop(0, s.type === 0
|
||||
? `rgba(255,255,255,${ha})`
|
||||
: s.type === 1
|
||||
? `rgba(0,255,136,${ha})`
|
||||
: `rgba(0,220,255,${ha})`);
|
||||
halo.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
ctx.beginPath();
|
||||
ctx.arc(s.x, s.y, s.r * 3.5, 0, Math.PI * 2);
|
||||
ctx.fillStyle = halo;
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────
|
||||
SHOOTING STARS
|
||||
────────────────────────────────────────────── */
|
||||
const MAX_SHOOTING_STARS = 3;
|
||||
let shootingStars = [];
|
||||
let nextShootingStarAt = 0; // time value when next star spawns
|
||||
|
||||
function spawnShootingStar() {
|
||||
// Spawn from top or right edge, travel down-left or down-right
|
||||
const fromRight = Math.random() < 0.5;
|
||||
const startX = fromRight ? W * (0.5 + Math.random() * 0.6) : W * Math.random() * 0.7;
|
||||
const startY = Math.random() * H * 0.45;
|
||||
const angle = (Math.PI / 4) + (Math.random() - 0.5) * 0.6; // ~45° downward
|
||||
const speed = 6 + Math.random() * 9;
|
||||
const length = 80 + Math.random() * 160;
|
||||
|
||||
shootingStars.push({
|
||||
x: startX,
|
||||
y: startY,
|
||||
vx: Math.cos(angle) * speed * (fromRight ? -1 : 1),
|
||||
vy: Math.sin(angle) * speed,
|
||||
length,
|
||||
alpha: 0,
|
||||
fadeIn: true,
|
||||
life: 0,
|
||||
maxLife: (length / speed) * 1.6, // frames to live
|
||||
});
|
||||
}
|
||||
|
||||
function updateDrawShootingStars() {
|
||||
// Possibly spawn a new one
|
||||
if (shootingStars.length < MAX_SHOOTING_STARS && time > nextShootingStarAt) {
|
||||
spawnShootingStar();
|
||||
// Next star between 4–14 seconds of time units (time += 0.016/frame)
|
||||
nextShootingStarAt = time + 4 + Math.random() * 10;
|
||||
}
|
||||
|
||||
for (let i = shootingStars.length - 1; i >= 0; i--) {
|
||||
const s = shootingStars[i];
|
||||
s.life++;
|
||||
|
||||
// Fade in quickly, fade out near end
|
||||
if (s.life < 8) {
|
||||
s.alpha = s.life / 8;
|
||||
} else if (s.life > s.maxLife - 10) {
|
||||
s.alpha = Math.max(0, (s.maxLife - s.life) / 10);
|
||||
} else {
|
||||
s.alpha = 1;
|
||||
}
|
||||
|
||||
s.x += s.vx;
|
||||
s.y += s.vy;
|
||||
|
||||
// Draw trail
|
||||
const tailX = s.x - (s.vx / Math.hypot(s.vx, s.vy)) * s.length;
|
||||
const tailY = s.y - (s.vy / Math.hypot(s.vx, s.vy)) * s.length;
|
||||
|
||||
const grad = ctx.createLinearGradient(s.x, s.y, tailX, tailY);
|
||||
grad.addColorStop(0, `rgba(255,255,255,${s.alpha * 0.90})`);
|
||||
grad.addColorStop(0.15,`rgba(220,235,255,${s.alpha * 0.55})`);
|
||||
grad.addColorStop(1, `rgba(180,210,255,0)`);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(s.x, s.y);
|
||||
ctx.lineTo(tailX, tailY);
|
||||
ctx.strokeStyle = grad;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
|
||||
// Bright head dot
|
||||
ctx.beginPath();
|
||||
ctx.arc(s.x, s.y, 1.4, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(255,255,255,${s.alpha * 0.95})`;
|
||||
ctx.fill();
|
||||
|
||||
// Remove if expired or off-screen
|
||||
if (s.life >= s.maxLife || s.x < -50 || s.x > W + 50 || s.y > H + 50) {
|
||||
shootingStars.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────
|
||||
GLASS SHARDS — fixed physics
|
||||
────────────────────────────────────────────── */
|
||||
const SHARD_COUNT = 30;
|
||||
const SHARD_RADIUS = 230; // mouse influence radius
|
||||
const SCATTER_FORCE = 3.0;
|
||||
const ROT_V_MAX = 0.018; // cap on rotation speed
|
||||
|
||||
const SHARD_COLORS = [
|
||||
{ r: 255, g: 102, b: 0 }, // neon orange
|
||||
{ r: 0, g: 255, b: 136 }, // neon green
|
||||
{ r: 0, g: 220, b: 255 }, // neon teal
|
||||
{ r: 255, g: 255, b: 255 }, // white
|
||||
{ r: 255, g: 180, b: 0 }, // amber
|
||||
];
|
||||
|
||||
let shards = [];
|
||||
|
||||
function buildVerts(size, sides) {
|
||||
const verts = [];
|
||||
const base = Math.random() * Math.PI * 2;
|
||||
for (let i = 0; i < sides; i++) {
|
||||
const a = base + (Math.PI * 2 * i / sides) + (Math.random() - 0.5) * 0.65;
|
||||
const r = size * (0.55 + Math.random() * 0.45);
|
||||
verts.push([Math.cos(a) * r, Math.sin(a) * r]);
|
||||
}
|
||||
return verts;
|
||||
}
|
||||
|
||||
function initShards() {
|
||||
shards = [];
|
||||
for (let i = 0; i < SHARD_COUNT; i++) {
|
||||
const size = 11 + Math.random() * 42;
|
||||
const sides = 3 + Math.floor(Math.random() * 3); // 3–5 sides
|
||||
const col = SHARD_COLORS[Math.floor(Math.random() * SHARD_COLORS.length)];
|
||||
shards.push({
|
||||
x: Math.random() * W,
|
||||
y: Math.random() * H,
|
||||
vx: (Math.random() - 0.5) * 0.22,
|
||||
vy: (Math.random() - 0.5) * 0.22,
|
||||
rot: Math.random() * Math.PI * 2,
|
||||
rotV: (Math.random() - 0.5) * 0.003, // very gentle initial spin
|
||||
verts: buildVerts(size, sides),
|
||||
size, col,
|
||||
alpha: 0.18 + Math.random() * 0.24,
|
||||
alphaTarget: 0.18 + Math.random() * 0.24,
|
||||
svx: 0, svy: 0,
|
||||
phase: Math.random() * Math.PI * 2,
|
||||
floatSpeed: 0.20 + Math.random() * 0.45, // slower float
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function drawShard(s) {
|
||||
ctx.save();
|
||||
ctx.translate(s.x, s.y);
|
||||
ctx.rotate(s.rot);
|
||||
|
||||
const { r, g, b } = s.col;
|
||||
const al = s.alpha;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(s.verts[0][0], s.verts[0][1]);
|
||||
for (let i = 1; i < s.verts.length; i++) ctx.lineTo(s.verts[i][0], s.verts[i][1]);
|
||||
ctx.closePath();
|
||||
|
||||
// Glass fill
|
||||
const fill = ctx.createLinearGradient(-s.size, -s.size, s.size * 0.6, s.size * 0.6);
|
||||
fill.addColorStop(0, `rgba(${r},${g},${b},${al * 0.18})`);
|
||||
fill.addColorStop(0.45,`rgba(255,255,255,${al * 0.08})`);
|
||||
fill.addColorStop(1, `rgba(${r},${g},${b},${al * 0.03})`);
|
||||
ctx.fillStyle = fill;
|
||||
ctx.fill();
|
||||
|
||||
// Neon outline — dimmed
|
||||
ctx.shadowColor = `rgba(${r},${g},${b},0.45)`;
|
||||
ctx.shadowBlur = 7;
|
||||
ctx.strokeStyle = `rgba(${r},${g},${b},${al * 0.70})`;
|
||||
ctx.lineWidth = 1.0;
|
||||
ctx.stroke();
|
||||
|
||||
// Soft outer glow — dimmed
|
||||
ctx.shadowBlur = 14;
|
||||
ctx.strokeStyle = `rgba(${r},${g},${b},${al * 0.18})`;
|
||||
ctx.lineWidth = 2.0;
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Inner highlight
|
||||
if (s.verts.length >= 2) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(s.verts[0][0] * 0.55, s.verts[0][1] * 0.55);
|
||||
ctx.lineTo(s.verts[1][0] * 0.55, s.verts[1][1] * 0.55);
|
||||
ctx.strokeStyle = `rgba(255,255,255,${al * 0.40})`;
|
||||
ctx.lineWidth = 0.8;
|
||||
ctx.shadowBlur = 3;
|
||||
ctx.shadowColor = 'rgba(255,255,255,0.3)';
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function updateShards() {
|
||||
const infSq = SHARD_RADIUS * SHARD_RADIUS;
|
||||
|
||||
for (let i = 0; i < shards.length; i++) {
|
||||
const s = shards[i];
|
||||
|
||||
// Gentle float bob
|
||||
s.vy += Math.sin(time * s.floatSpeed + s.phase) * 0.002;
|
||||
|
||||
// Mouse scatter
|
||||
if (mouse.x > -1000) {
|
||||
const dx = s.x - mouse.x;
|
||||
const dy = s.y - mouse.y;
|
||||
const distSq = dx * dx + dy * dy;
|
||||
|
||||
if (distSq < infSq) {
|
||||
const dist = Math.sqrt(distSq);
|
||||
const force = (1 - dist / SHARD_RADIUS) * SCATTER_FORCE;
|
||||
const ang = Math.atan2(dy, dx);
|
||||
s.svx += Math.cos(ang) * force * 0.065;
|
||||
s.svy += Math.sin(ang) * force * 0.065;
|
||||
// Small nudge to rotation — NOT multiplicative
|
||||
s.rotV += (Math.random() - 0.5) * 0.004 * (1 - dist / SHARD_RADIUS);
|
||||
// Brighten on interaction
|
||||
s.alphaTarget = Math.min(0.75, s.alphaTarget + 0.03);
|
||||
}
|
||||
}
|
||||
|
||||
// Rotation damping — keeps shards from spinning endlessly
|
||||
s.rotV *= 0.96;
|
||||
// Hard cap on rotation speed
|
||||
if (s.rotV > ROT_V_MAX) s.rotV = ROT_V_MAX;
|
||||
if (s.rotV < -ROT_V_MAX) s.rotV = -ROT_V_MAX;
|
||||
|
||||
// Scatter velocity damping
|
||||
s.svx *= 0.92;
|
||||
s.svy *= 0.92;
|
||||
s.vx *= 0.998;
|
||||
s.vy *= 0.998;
|
||||
|
||||
// Apply
|
||||
s.x += s.vx + s.svx;
|
||||
s.y += s.vy + s.svy;
|
||||
s.rot += s.rotV;
|
||||
|
||||
// Fade alpha toward target
|
||||
s.alpha += (s.alphaTarget - s.alpha) * 0.025;
|
||||
// Slowly restore target
|
||||
const base = 0.18;
|
||||
if (s.alphaTarget > base) s.alphaTarget -= 0.006;
|
||||
|
||||
// Edge wrap
|
||||
const m = s.size + 10;
|
||||
if (s.x < -m) s.x = W + m;
|
||||
if (s.x > W + m) s.x = -m;
|
||||
if (s.y < -m) s.y = H + m;
|
||||
if (s.y > H + m) s.y = -m;
|
||||
}
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────
|
||||
CURSOR AURA — dimmed
|
||||
────────────────────────────────────────────── */
|
||||
function drawCursorAura() {
|
||||
const r = 130 + Math.sin(time * 1.8) * 22;
|
||||
const aura = ctx.createRadialGradient(mouse.x, mouse.y, 0, mouse.x, mouse.y, r);
|
||||
aura.addColorStop(0, 'rgba(255,102,0,0.04)');
|
||||
aura.addColorStop(0.5, 'rgba(255,102,0,0.012)');
|
||||
aura.addColorStop(1, 'rgba(255,102,0,0)');
|
||||
ctx.fillStyle = aura;
|
||||
ctx.beginPath();
|
||||
ctx.arc(mouse.x, mouse.y, r, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────
|
||||
MAIN LOOP
|
||||
────────────────────────────────────────────── */
|
||||
function animate() {
|
||||
if (!initialized) return;
|
||||
|
||||
time += 0.016;
|
||||
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
|
||||
drawNebulas();
|
||||
drawStars();
|
||||
updateDrawShootingStars();
|
||||
updateShards();
|
||||
for (let i = 0; i < shards.length; i++) drawShard(shards[i]);
|
||||
if (mouse.x > -1000) drawCursorAura();
|
||||
|
||||
rafId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────
|
||||
MOBILE — one-shot static render
|
||||
────────────────────────────────────────────── */
|
||||
function initMobileStatic() {
|
||||
const c = document.getElementById('hexCanvas');
|
||||
if (!c) return;
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
const w = window.innerWidth;
|
||||
const h = window.innerHeight;
|
||||
c.width = w * dpr;
|
||||
c.height = h * dpr;
|
||||
c.style.width = w + 'px';
|
||||
c.style.height = h + 'px';
|
||||
const cx = c.getContext('2d');
|
||||
cx.scale(dpr, dpr);
|
||||
|
||||
// Nebula blobs — dimmed
|
||||
const g1 = cx.createRadialGradient(w*0.15, h*0.3, 0, w*0.15, h*0.3, w*0.5);
|
||||
g1.addColorStop(0, 'rgba(255,80,0,0.04)'); g1.addColorStop(1, 'rgba(255,80,0,0)');
|
||||
cx.fillStyle = g1; cx.fillRect(0, 0, w, h);
|
||||
|
||||
const g2 = cx.createRadialGradient(w*0.82, h*0.65, 0, w*0.82, h*0.65, w*0.45);
|
||||
g2.addColorStop(0, 'rgba(0,255,136,0.03)'); g2.addColorStop(1, 'rgba(0,255,136,0)');
|
||||
cx.fillStyle = g2; cx.fillRect(0, 0, w, h);
|
||||
|
||||
const g3 = cx.createRadialGradient(w*0.5, h*0.85, 0, w*0.5, h*0.85, w*0.4);
|
||||
g3.addColorStop(0, 'rgba(0,180,255,0.025)'); g3.addColorStop(1, 'rgba(0,180,255,0)');
|
||||
cx.fillStyle = g3; cx.fillRect(0, 0, w, h);
|
||||
|
||||
// Stars
|
||||
for (let i = 0; i < 130; i++) {
|
||||
const sx = Math.random() * w;
|
||||
const sy = Math.random() * h;
|
||||
const sr = 0.35 + Math.random() * 1.5;
|
||||
const sa = 0.20 + Math.random() * 0.55;
|
||||
cx.beginPath();
|
||||
cx.arc(sx, sy, sr, 0, Math.PI * 2);
|
||||
cx.fillStyle = `rgba(255,255,255,${sa})`;
|
||||
cx.fill();
|
||||
}
|
||||
|
||||
// Glass shards
|
||||
const MCOLS = [[255,102,0],[0,255,136],[0,220,255],[255,255,255],[255,180,0]];
|
||||
for (let i = 0; i < 14; i++) {
|
||||
const sx = Math.random() * w;
|
||||
const sy = Math.random() * h;
|
||||
const ss = 10 + Math.random() * 35;
|
||||
const sides = 3 + Math.floor(Math.random() * 3);
|
||||
const [cr,cg,cb] = MCOLS[Math.floor(Math.random() * MCOLS.length)];
|
||||
cx.save();
|
||||
cx.translate(sx, sy);
|
||||
cx.rotate(Math.random() * Math.PI * 2);
|
||||
cx.beginPath();
|
||||
for (let j = 0; j < sides; j++) {
|
||||
const a = (Math.PI * 2 * j / sides) + (Math.random()-0.5)*0.6;
|
||||
const r = ss * (0.55 + Math.random() * 0.45);
|
||||
j === 0 ? cx.moveTo(Math.cos(a)*r, Math.sin(a)*r)
|
||||
: cx.lineTo(Math.cos(a)*r, Math.sin(a)*r);
|
||||
}
|
||||
cx.closePath();
|
||||
cx.fillStyle = `rgba(${cr},${cg},${cb},0.04)`;
|
||||
cx.strokeStyle = `rgba(${cr},${cg},${cb},0.38)`;
|
||||
cx.lineWidth = 1;
|
||||
cx.fill();
|
||||
cx.shadowColor = `rgba(${cr},${cg},${cb},0.45)`;
|
||||
cx.shadowBlur = 6;
|
||||
cx.stroke();
|
||||
cx.shadowBlur = 0;
|
||||
cx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────
|
||||
INIT
|
||||
────────────────────────────────────────────── */
|
||||
function init() {
|
||||
if (window.innerWidth <= 768 || ('ontouchstart' in window)) {
|
||||
initMobileStatic();
|
||||
return;
|
||||
}
|
||||
|
||||
canvas = document.getElementById('hexCanvas');
|
||||
if (!canvas) return;
|
||||
|
||||
ctx = canvas.getContext('2d', { alpha: true });
|
||||
resize();
|
||||
|
||||
window.addEventListener('resize', debounce(resize, 200));
|
||||
document.addEventListener('mousemove', e => {
|
||||
mouse.x = e.clientX;
|
||||
mouse.y = e.clientY;
|
||||
}, { passive: true });
|
||||
document.addEventListener('mouseleave', () => {
|
||||
mouse.x = -2000;
|
||||
mouse.y = -2000;
|
||||
});
|
||||
|
||||
// First shooting star after a short delay
|
||||
nextShootingStarAt = 3 + Math.random() * 5;
|
||||
|
||||
initialized = true;
|
||||
animate();
|
||||
}
|
||||
|
||||
function resize() {
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
W = window.innerWidth;
|
||||
H = window.innerHeight;
|
||||
canvas.width = W * dpr;
|
||||
canvas.height = H * dpr;
|
||||
canvas.style.width = W + 'px';
|
||||
canvas.style.height = H + 'px';
|
||||
canvas.style.position = 'fixed';
|
||||
canvas.style.top = '0';
|
||||
canvas.style.left = '0';
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.scale(dpr, dpr);
|
||||
initStars();
|
||||
initShards();
|
||||
}
|
||||
|
||||
function debounce(fn, ms) {
|
||||
let t;
|
||||
return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); };
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
initialized = false;
|
||||
stars = []; shards = []; shootingStars = [];
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
window.HexBackground = { init, destroy };
|
||||
})();
|
||||
194
scripts/ki-chat-bubble.js
Normal file
@@ -0,0 +1,194 @@
|
||||
// KI Chat Bubble — Flowise embed (Feedgine-branded)
|
||||
// To remove: delete this file and its <script> tag in index.html.
|
||||
|
||||
import Chatbot from "https://cdn.jsdelivr.net/npm/flowise-embed/dist/web.js";
|
||||
|
||||
// Read from config - on localhost talk directly to Flowise, on prod use proxy.
|
||||
const isLocal = ['localhost', '127.0.0.1'].includes(window.location.hostname);
|
||||
|
||||
Chatbot.init({
|
||||
chatflowid: isLocal ? "d63d3d02-b5fa-482c-9161-c21c615fb625" : "chat",
|
||||
apiHost: isLocal ? "https://flowise.profice.de" : window.location.origin,
|
||||
theme: {
|
||||
button: {
|
||||
backgroundColor: "#05050d",
|
||||
right: 24,
|
||||
bottom: 24,
|
||||
size: "medium",
|
||||
iconColor: "#ff6600"
|
||||
},
|
||||
chatWindow: {
|
||||
showTitle: true,
|
||||
title: "Feedgine Assistent",
|
||||
titleBackgroundColor: "#05050d",
|
||||
titleTextColor: "#e8e8f2",
|
||||
welcomeMessage: "Hallo! Ich bin der KI-Assistent von Feedgine.\n\nIch beantworte Ihre Fragen rund um Profit Intelligence, POAS-Optimierung und Server-Side Tracking.",
|
||||
backgroundColor: "#0d0d1a",
|
||||
fontSize: 15,
|
||||
showAgentMessages: true,
|
||||
poweredByTextColor: "#0d0d1a",
|
||||
botMessage: {
|
||||
backgroundColor: "rgba(255,255,255,0.07)",
|
||||
textColor: "#e8e8f2",
|
||||
showAvatar: true,
|
||||
avatarSrc: "/images/icons/KI.png"
|
||||
},
|
||||
userMessage: {
|
||||
backgroundColor: "rgba(255,102,0,0.18)",
|
||||
textColor: "#e8e8f2",
|
||||
showAvatar: false
|
||||
},
|
||||
textInput: {
|
||||
placeholder: "Ihre Nachricht...",
|
||||
backgroundColor: "rgba(255,255,255,0.04)",
|
||||
textColor: "#e8e8f2",
|
||||
sendButtonColor: "#ff6600"
|
||||
},
|
||||
footer: {
|
||||
textColor: "#8888a8",
|
||||
text: "KI-System · Keine sensiblen Daten eingeben.",
|
||||
company: " ",
|
||||
companyLink: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Inject orange border around the chat header via shadow DOM
|
||||
function injectHeaderStyle() {
|
||||
const flowise = document.querySelector('flowise-chatbot');
|
||||
if (!flowise?.shadowRoot) return false;
|
||||
|
||||
const shadow = flowise.shadowRoot;
|
||||
if (shadow.querySelector('#feedgine-header-applied')) return true;
|
||||
|
||||
const all = [...shadow.querySelectorAll('*')];
|
||||
const header = all.find(el => {
|
||||
const bg = getComputedStyle(el).backgroundColor;
|
||||
return bg === 'rgb(5, 5, 13)' && el.textContent.includes('Feedgine Assistent');
|
||||
});
|
||||
|
||||
if (!header) return false;
|
||||
|
||||
header.style.borderTop = '1px solid rgba(255,102,0,0.5)';
|
||||
header.style.borderLeft = '1px solid rgba(255,102,0,0.5)';
|
||||
header.style.borderRight = '1px solid rgba(255,102,0,0.5)';
|
||||
header.style.borderBottom = '1px solid rgba(255,102,0,0.25)';
|
||||
header.style.boxShadow = '0 0 10px rgba(255,102,0,0.28), 0 0 24px rgba(255,102,0,0.10)';
|
||||
header.style.boxSizing = 'border-box';
|
||||
|
||||
const chatWindow = header.parentElement;
|
||||
if (chatWindow) {
|
||||
chatWindow.style.borderBottomLeftRadius = '12px';
|
||||
chatWindow.style.borderBottomRightRadius = '12px';
|
||||
chatWindow.style.border = '1px solid rgba(255,102,0,0.25)';
|
||||
chatWindow.style.boxShadow = '0 0 30px rgba(255,102,0,0.08)';
|
||||
chatWindow.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
const marker = document.createElement('span');
|
||||
marker.id = 'feedgine-header-applied';
|
||||
marker.style.display = 'none';
|
||||
shadow.appendChild(marker);
|
||||
return true;
|
||||
}
|
||||
|
||||
let attempts = 0;
|
||||
const interval = setInterval(() => {
|
||||
if (injectHeaderStyle() || ++attempts > 20) clearInterval(interval);
|
||||
}, 300);
|
||||
|
||||
// Keep shadow DOM cursor in sync with the main-page cursor toggle.
|
||||
// CSS from the outer document cannot penetrate shadow DOM, so we inject
|
||||
// a <style> tag directly and update it whenever body's class changes.
|
||||
function updateShadowCursor(shadow) {
|
||||
let style = shadow.querySelector('#feedgine-cursor-style');
|
||||
if (!style) {
|
||||
style = document.createElement('style');
|
||||
style.id = 'feedgine-cursor-style';
|
||||
shadow.appendChild(style);
|
||||
}
|
||||
style.textContent = document.body.classList.contains('system-cursor')
|
||||
? ''
|
||||
: '* { cursor: none !important; }';
|
||||
}
|
||||
|
||||
function initShadowCursorSync() {
|
||||
const host = document.querySelector('flowise-chatbot');
|
||||
if (!host?.shadowRoot) return false;
|
||||
|
||||
updateShadowCursor(host.shadowRoot);
|
||||
|
||||
new MutationObserver(() => updateShadowCursor(host.shadowRoot))
|
||||
.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
let cursorAttempts = 0;
|
||||
const cursorInterval = setInterval(() => {
|
||||
if (initShadowCursorSync() || ++cursorAttempts > 20) clearInterval(cursorInterval);
|
||||
}, 300);
|
||||
|
||||
// Global helper: open the Flowise chat and automatically send a message.
|
||||
// Called by CTA buttons across the page.
|
||||
window.openChatWithMessage = function (message) {
|
||||
// Defer by one tick so Flowise's own click-outside handler runs first
|
||||
// (otherwise it would close the chat immediately after we open it).
|
||||
setTimeout(() => {
|
||||
const chatbot = document.querySelector('flowise-chatbot');
|
||||
if (!chatbot || !chatbot.shadowRoot) return;
|
||||
|
||||
const shadow = chatbot.shadowRoot;
|
||||
|
||||
// Open chat window if not yet visible
|
||||
if (!shadow.querySelector('textarea')) {
|
||||
const toggleBtn = shadow.querySelector('button[part="button"]') || shadow.querySelector('button');
|
||||
if (toggleBtn) toggleBtn.click();
|
||||
}
|
||||
|
||||
// Poll until the textarea is available, then fill + send via Enter key
|
||||
let tries = 0;
|
||||
const poll = setInterval(() => {
|
||||
if (++tries > 30) { clearInterval(poll); return; }
|
||||
|
||||
const ta = shadow.querySelector('textarea');
|
||||
if (!ta) return;
|
||||
clearInterval(poll);
|
||||
|
||||
// Use execCommand('insertText') — fires a real, trusted InputEvent
|
||||
// that Solid.js signal tracking actually picks up (unlike synthetic events).
|
||||
ta.focus();
|
||||
ta.select();
|
||||
document.execCommand('delete', false);
|
||||
document.execCommand('insertText', false, message);
|
||||
|
||||
// Find and click the send button.
|
||||
// Walk UP from the textarea; the send button is a sibling in the
|
||||
// same input-row container. Fall back to the last non-toggle button
|
||||
// in the entire shadow DOM (send button is always last in source order).
|
||||
setTimeout(() => {
|
||||
const toggleBtn = shadow.querySelector('button[part="button"]');
|
||||
|
||||
let sendBtn = null;
|
||||
let el = ta;
|
||||
for (let i = 0; i < 6 && !sendBtn; i++) {
|
||||
el = el.parentElement;
|
||||
if (!el) break;
|
||||
const candidate = [...el.querySelectorAll('button')]
|
||||
.find(b => b !== toggleBtn);
|
||||
if (candidate) sendBtn = candidate;
|
||||
}
|
||||
|
||||
// Fallback: the send button is the last button in the shadow DOM
|
||||
if (!sendBtn) {
|
||||
const all = [...shadow.querySelectorAll('button')]
|
||||
.filter(b => b !== toggleBtn);
|
||||
sendBtn = all[all.length - 1] || null;
|
||||
}
|
||||
|
||||
if (sendBtn) sendBtn.click();
|
||||
}, 300);
|
||||
}, 150);
|
||||
}, 200); // 200 ms lets the click-outside event fully propagate before we open
|
||||
};
|
||||
64
scripts/scroll-header.js
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Scroll Header — adapted from Profice WebSite
|
||||
* Handles sticky header state changes on scroll.
|
||||
*/
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const topBanner = document.querySelector('.top-banner');
|
||||
if (!topBanner) return;
|
||||
|
||||
const scrollThreshold = 50;
|
||||
const rafDelay = 8;
|
||||
|
||||
let isScrolled = false;
|
||||
let lastScrollY = 0;
|
||||
let rafId = null;
|
||||
let lastUpdateTime = 0;
|
||||
let lastScrollTime = 0;
|
||||
|
||||
function calculateVelocity(currentScrollY, currentTime) {
|
||||
if (lastScrollTime === 0) { lastScrollTime = currentTime; return 0; }
|
||||
const timeDelta = currentTime - lastScrollTime;
|
||||
const scrollDelta = Math.abs(currentScrollY - lastScrollY);
|
||||
lastScrollTime = currentTime;
|
||||
return scrollDelta / timeDelta;
|
||||
}
|
||||
|
||||
function updateHeaderState(scrolled, velocity = 0) {
|
||||
if (scrolled === isScrolled) return;
|
||||
if (velocity > 5) {
|
||||
topBanner.classList.add('fast-scroll');
|
||||
} else {
|
||||
topBanner.classList.remove('fast-scroll');
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
if (scrolled) {
|
||||
topBanner.classList.add('scrolled');
|
||||
} else {
|
||||
topBanner.classList.remove('scrolled');
|
||||
}
|
||||
});
|
||||
isScrolled = scrolled;
|
||||
}
|
||||
|
||||
function handleScroll(currentTime) {
|
||||
if (currentTime - lastUpdateTime < rafDelay) {
|
||||
rafId = requestAnimationFrame(handleScroll);
|
||||
return;
|
||||
}
|
||||
lastUpdateTime = currentTime;
|
||||
|
||||
const currentScrollY = window.pageYOffset || document.documentElement.scrollTop;
|
||||
const velocity = calculateVelocity(currentScrollY, currentTime);
|
||||
|
||||
updateHeaderState(currentScrollY > scrollThreshold, velocity);
|
||||
lastScrollY = currentScrollY;
|
||||
rafId = null;
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (!rafId) rafId = requestAnimationFrame(handleScroll);
|
||||
}, { passive: true });
|
||||
|
||||
// Initial state
|
||||
if (window.pageYOffset > scrollThreshold) updateHeaderState(true);
|
||||
});
|
||||
103
style/cursor.css
Normal file
@@ -0,0 +1,103 @@
|
||||
/* cursor.css — Venom/Spider Cursor · Space Edition */
|
||||
|
||||
/* ── Default: system cursor ── */
|
||||
body.system-cursor,
|
||||
body.system-cursor *,
|
||||
body.system-cursor a,
|
||||
body.system-cursor button,
|
||||
body.system-cursor input,
|
||||
body.system-cursor textarea,
|
||||
body.system-cursor select,
|
||||
body.system-cursor label,
|
||||
body.system-cursor [role="button"],
|
||||
body.system-cursor [onclick],
|
||||
body.system-cursor .clickable {
|
||||
cursor: auto !important;
|
||||
}
|
||||
|
||||
/* ── Custom cursor: hide native cursor everywhere ── */
|
||||
body:not(.system-cursor),
|
||||
body:not(.system-cursor) * {
|
||||
cursor: none !important;
|
||||
}
|
||||
|
||||
/* Flowise toggle button is exposed via part="button" — pierce shadow DOM */
|
||||
body:not(.system-cursor) flowise-chatbot::part(button) {
|
||||
cursor: none !important;
|
||||
}
|
||||
|
||||
/* ── Canvas overlay ── */
|
||||
#venom-cursor {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 2147483647;
|
||||
display: none;
|
||||
}
|
||||
|
||||
body:not(.system-cursor) #venom-cursor {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body.system-cursor #venom-cursor {
|
||||
display: none !important;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ── Touch devices: revert ── */
|
||||
@media (pointer: coarse) {
|
||||
body, html, a, button, input, textarea, select {
|
||||
cursor: auto !important;
|
||||
}
|
||||
#venom-cursor {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Cursor toggle button ── */
|
||||
#cursorToggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255,255,255,0.18);
|
||||
color: rgba(255,255,255,0.70);
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 10px;
|
||||
cursor: auto;
|
||||
transition: border-color 0.25s, background 0.25s, box-shadow 0.25s;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#cursorToggle:hover {
|
||||
border-color: rgba(255,255,255,0.40);
|
||||
background: rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
#cursorToggle.active {
|
||||
border-color: rgba(255,102,0,0.45);
|
||||
background: rgba(255,102,0,0.06);
|
||||
box-shadow: 0 0 8px rgba(255,102,0,0.12);
|
||||
}
|
||||
|
||||
#cursorToggle .cursor-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
filter: brightness(0) invert(1);
|
||||
opacity: 0.65;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
#cursorToggle:hover .cursor-icon,
|
||||
#cursorToggle.active .cursor-icon {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
1964
style/design.css
Normal file
16
style/fonts.css
Normal file
@@ -0,0 +1,16 @@
|
||||
/* Local Montserrat fonts — loaded before any external request */
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
src: url('../fonts/montserrat-regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
src: url('../fonts/montserrat-bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||