2376 lines
92 KiB
HTML
2376 lines
92 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Frontier Algorithmics – KI & Technology Consulting</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--fa-navy: #1A1F5C;
|
||
--fa-navy-deep: #0F1340;
|
||
--fa-blue: #6B8FD4;
|
||
--fa-blue-light: #A8C0E8;
|
||
--fa-blue-pale: #EEF3FB;
|
||
--fa-off-white: #F7F8FC;
|
||
--fa-mid: #6B7280;
|
||
--fa-border: rgba(26,31,92,0.1);
|
||
/* Horizontal content padding: scales with viewport,
|
||
caps content at ~1500px on huge screens */
|
||
--content-pad: max(1.25rem, calc((100vw - 1500px) / 2));
|
||
}
|
||
@media (min-width: 720px) { :root { --content-pad: max(2.5rem, calc((100vw - 1500px) / 2)); } }
|
||
@media (min-width: 1100px) { :root { --content-pad: max(3.5rem, calc((100vw - 1500px) / 2)); } }
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
html { scroll-behavior: smooth; scroll-padding-top: 70px; }
|
||
body {
|
||
font-family: 'DM Sans', sans-serif;
|
||
color: var(--fa-navy);
|
||
background: #fff;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
/* ── NAV ── */
|
||
nav.fa-nav {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 1.1rem var(--content-pad);
|
||
border-bottom: 0.5px solid var(--fa-border);
|
||
background: rgba(255,255,255,0.97);
|
||
position: sticky; top: 0; z-index: 200;
|
||
backdrop-filter: blur(8px);
|
||
gap: 1.5rem;
|
||
}
|
||
.fa-logo-area { display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
|
||
.fa-logo-mark { font-family:'Syne',sans-serif; font-weight:800; font-size:24px; color:var(--fa-blue); letter-spacing:-1px; line-height:1; }
|
||
.fa-logo-divider { width:1px; height:30px; background:var(--fa-border); }
|
||
.fa-logo-text { font-family:'Syne',sans-serif; font-weight:600; font-size:12px; color:var(--fa-navy); text-transform:uppercase; letter-spacing:2.5px; line-height:1.3; }
|
||
.fa-nav-links { display:flex; gap:0.5rem; list-style:none; align-items: center; }
|
||
.fa-nav-links a { font-size:14px; font-weight:400; color:var(--fa-navy); text-decoration:none; letter-spacing:0.3px; opacity:0.65; transition:opacity 0.2s; white-space: nowrap; padding:0.6rem 0.75rem; display:inline-flex; align-items:center; min-height:44px; }
|
||
.fa-nav-links a:hover { opacity:1; }
|
||
.fa-nav-links a.lang { opacity: 0.35; font-family: 'Syne', sans-serif; font-size: 12px; font-weight: 600; letter-spacing: 1.5px; text-transform: uppercase; cursor: default; }
|
||
.fa-nav-links a.lang:hover { opacity: 0.5; }
|
||
.fa-nav-links a.lang .active { color: var(--fa-blue); opacity: 1; }
|
||
.fa-nav-cta {
|
||
font-family:'DM Sans',sans-serif; font-size:14px; font-weight:500;
|
||
color:#fff; background:var(--fa-navy); border:none;
|
||
padding:0.65rem 1.4rem; cursor:pointer; transition:background 0.2s;
|
||
text-decoration: none; white-space: nowrap; display: inline-flex; align-items: center; gap: 8px;
|
||
flex-shrink: 0;
|
||
}
|
||
.fa-nav-cta:hover { background:var(--fa-blue); }
|
||
.fa-nav-cta::before {
|
||
content: ''; width: 6px; height: 6px; border-radius: 50%;
|
||
background: #6FD49A; box-shadow: 0 0 0 2px rgba(111,212,154,0.25);
|
||
}
|
||
/* Mobile menu button */
|
||
.fa-nav-menu {
|
||
display: none; background: none; border: none; cursor: pointer;
|
||
padding: 8px; gap: 4px; flex-direction: column;
|
||
}
|
||
.fa-nav-menu span {
|
||
display: block; width: 22px; height: 1.5px;
|
||
background: var(--fa-navy); transition: transform 0.25s, opacity 0.25s;
|
||
}
|
||
.fa-nav.menu-open .fa-nav-menu span:nth-child(1) { transform: translateY(5.5px) rotate(45deg); }
|
||
.fa-nav.menu-open .fa-nav-menu span:nth-child(2) { opacity: 0; }
|
||
.fa-nav.menu-open .fa-nav-menu span:nth-child(3) { transform: translateY(-5.5px) rotate(-45deg); }
|
||
|
||
/* ── HERO ── */
|
||
.fa-hero {
|
||
min-height: 88vh;
|
||
display: grid;
|
||
grid-template-columns: 1.15fr 0.85fr;
|
||
align-items: center;
|
||
padding: 5rem var(--content-pad) 4.5rem;
|
||
position: relative;
|
||
overflow: hidden;
|
||
background: #fff;
|
||
}
|
||
.fa-hero.no-visual {
|
||
grid-template-columns: 1fr;
|
||
padding: 6rem var(--content-pad) 5.5rem;
|
||
}
|
||
.fa-hero.no-visual .fa-hero-content {
|
||
padding-right: 0;
|
||
max-width: 880px;
|
||
}
|
||
.fa-hero.no-visual h1 {
|
||
font-size: clamp(2.8rem, 5.4vw, 5rem);
|
||
}
|
||
.fa-hero.no-visual .fa-hero-sub {
|
||
max-width: 540px; font-size: 17px;
|
||
}
|
||
/* Canvas background */
|
||
#hero-canvas, #hero-canvas-gl {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 100%; height: 100%;
|
||
z-index: 0;
|
||
}
|
||
#hero-canvas { opacity: 1; }
|
||
#hero-canvas-gl { opacity: 0; transition: opacity 0.35s ease; pointer-events: none; }
|
||
#hero-canvas-gl.active { opacity: 1; }
|
||
/* Mask: fade the GL canvas away on the left side where the headline sits */
|
||
#hero-canvas-gl.active {
|
||
-webkit-mask-image: linear-gradient(to right,
|
||
transparent 0%, transparent 18%,
|
||
rgba(0,0,0,0.18) 30%, rgba(0,0,0,0.55) 45%,
|
||
rgba(0,0,0,0.9) 60%, #000 75%);
|
||
mask-image: linear-gradient(to right,
|
||
transparent 0%, transparent 18%,
|
||
rgba(0,0,0,0.18) 30%, rgba(0,0,0,0.55) 45%,
|
||
rgba(0,0,0,0.9) 60%, #000 75%);
|
||
}
|
||
/* Soft white wash behind headline for extra legibility when GL is active */
|
||
.fa-hero.gl-active::before {
|
||
content: ''; position: absolute; inset: 0;
|
||
background: linear-gradient(to right,
|
||
rgba(255,255,255,0.88) 0%,
|
||
rgba(255,255,255,0.78) 22%,
|
||
rgba(255,255,255,0.45) 42%,
|
||
rgba(255,255,255,0.1) 60%,
|
||
rgba(255,255,255,0) 75%);
|
||
z-index: 1; pointer-events: none;
|
||
transition: opacity 0.35s ease;
|
||
}
|
||
/* When GL is off, no wash */
|
||
#hero-canvas.hidden { opacity: 0; }
|
||
.fa-hero-content { position:relative; z-index:2; padding-right:3.5rem; max-width: 720px; }
|
||
.fa-eyebrow {
|
||
font-size:11px; font-weight:500; letter-spacing:3px; text-transform:uppercase;
|
||
color:var(--fa-blue); margin-bottom:1.75rem;
|
||
display:flex; align-items:center; gap:12px;
|
||
}
|
||
.fa-eyebrow::before { content:''; display:block; width:36px; height:1px; background:var(--fa-blue); }
|
||
|
||
/* AI badge in hero */
|
||
.fa-ai-badge {
|
||
display: inline-flex; align-items: center; gap: 8px;
|
||
background: var(--fa-navy); color: #fff;
|
||
font-size: 11px; font-weight: 500; letter-spacing: 1.5px; text-transform: uppercase;
|
||
padding: 0.45rem 1rem 0.45rem 0.7rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
.fa-ai-dot {
|
||
width: 7px; height: 7px; border-radius: 50%; background: var(--fa-blue);
|
||
animation: pulse 2s ease-in-out infinite;
|
||
}
|
||
@keyframes pulse {
|
||
0%,100% { opacity:1; transform:scale(1); }
|
||
50% { opacity:0.5; transform:scale(0.7); }
|
||
}
|
||
|
||
.fa-hero h1 {
|
||
font-family:'Syne',sans-serif;
|
||
font-size: clamp(2.6rem, 4.4vw, 4.2rem);
|
||
font-weight:700; line-height:0.98;
|
||
color:var(--fa-navy); letter-spacing:-2.5px; margin-bottom:2.25rem;
|
||
text-wrap: balance;
|
||
}
|
||
.fa-hero h1 em {
|
||
font-style:normal; color:var(--fa-blue);
|
||
position: relative; display: inline-block;
|
||
/* Scroll progress drives saturation → use a color blend toward a richer blue */
|
||
color: color-mix(in oklch,
|
||
var(--fa-blue),
|
||
oklch(0.55 0.22 255) calc(var(--hero-progress, 0) * 100%)
|
||
);
|
||
transition: color 0.05s linear;
|
||
}
|
||
@keyframes faUnderlineDraw {
|
||
0% { transform: scaleX(0); }
|
||
100% { transform: scaleX(1); }
|
||
}
|
||
/* Base underline (faded) — draws in on load */
|
||
.fa-hero h1 em.underline::after {
|
||
content:''; position:absolute; left:0; right:0; bottom:0.06em;
|
||
height: 0.08em; background: var(--fa-blue); opacity: 0.35;
|
||
transform-origin: left center;
|
||
animation: faUnderlineDraw 1.2s cubic-bezier(0.22, 0.61, 0.36, 1) 0.6s both;
|
||
}
|
||
/* Brighter overlay underline — grows with scroll progress */
|
||
.fa-hero h1 em.underline::before {
|
||
content:''; position:absolute; left:0; bottom:0.04em;
|
||
height: 0.14em;
|
||
width: calc(var(--hero-progress, 0) * 100%);
|
||
background: linear-gradient(90deg,
|
||
oklch(0.62 0.18 250) 0%,
|
||
oklch(0.70 0.22 260) 100%);
|
||
opacity: 0.85;
|
||
border-radius: 1px;
|
||
box-shadow: 0 0 12px oklch(0.65 0.22 255 / 0.4);
|
||
transition: width 0.05s linear;
|
||
pointer-events: none;
|
||
}
|
||
.fa-hero h1.flat em { color: var(--fa-navy); }
|
||
.fa-hero h1.flat em::after, .fa-hero h1.flat em::before { display: none; }
|
||
.fa-hero-sub {
|
||
font-size:16px; font-weight:300; line-height:1.8;
|
||
color:var(--fa-mid); max-width:460px; margin-bottom:3rem;
|
||
}
|
||
.fa-hero-actions { display:flex; align-items:center; gap:1.75rem; }
|
||
.fa-btn-primary {
|
||
font-family:'DM Sans',sans-serif; font-size:14px; font-weight:500;
|
||
color:#fff; background:var(--fa-navy); border:none;
|
||
padding:0.9rem 2.25rem; cursor:pointer; transition:background 0.2s;
|
||
text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem;
|
||
white-space: nowrap;
|
||
}
|
||
.fa-btn-primary:hover { background:var(--fa-blue); }
|
||
.fa-btn-ghost {
|
||
font-family:'DM Sans',sans-serif; font-size:14px; font-weight:400;
|
||
color:var(--fa-navy); background:none; border:none; cursor:pointer; opacity:0.6; transition:opacity 0.2s;
|
||
text-decoration: none; display: inline-flex; align-items: center;
|
||
}
|
||
.fa-btn-ghost:hover { opacity:1; }
|
||
|
||
/* Hero right side – capability stack */
|
||
.fa-hero-visual {
|
||
position:relative; z-index:2;
|
||
display:flex; flex-direction:column; align-items:stretch;
|
||
max-width: 380px; margin-left: auto;
|
||
}
|
||
.fa-cap-card {
|
||
background: var(--fa-navy); color: #fff;
|
||
border: 0.5px solid rgba(107,143,212,0.25);
|
||
}
|
||
.fa-cap-head {
|
||
padding: 1.1rem 1.5rem;
|
||
border-bottom: 0.5px solid rgba(107,143,212,0.18);
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
font-family:'Syne',sans-serif; font-size:11px; font-weight:600;
|
||
letter-spacing:2.5px; text-transform:uppercase;
|
||
color: rgba(255,255,255,0.68);
|
||
}
|
||
.fa-cap-head-mark {
|
||
display:flex; align-items:center; gap:6px;
|
||
color: var(--fa-blue-light);
|
||
}
|
||
.fa-cap-head-mark::before {
|
||
content:''; width:5px; height:5px; border-radius:50%;
|
||
background: var(--fa-blue-light);
|
||
animation: pulse 2.4s ease-in-out infinite;
|
||
}
|
||
.fa-cap-row {
|
||
display: grid; grid-template-columns: 44px 1fr;
|
||
padding: 1.25rem 1.5rem;
|
||
border-bottom: 0.5px solid rgba(107,143,212,0.12);
|
||
gap: 1rem; align-items: center;
|
||
transition: background 0.25s;
|
||
}
|
||
.fa-cap-row:last-of-type { border-bottom: none; }
|
||
.fa-cap-row:hover { background: rgba(107,143,212,0.06); }
|
||
.fa-cap-tag {
|
||
font-family:'Syne',sans-serif;
|
||
font-size: 12px; font-weight: 700; letter-spacing: 1.5px;
|
||
color: var(--fa-blue-light);
|
||
border: 0.5px solid rgba(168,192,232,0.35);
|
||
padding: 6px 0; text-align: center;
|
||
}
|
||
.fa-cap-body {}
|
||
.fa-cap-title {
|
||
font-family:'Syne',sans-serif; font-size: 15px; font-weight: 600;
|
||
color: #fff; letter-spacing: -0.2px;
|
||
margin-bottom: 4px;
|
||
}
|
||
.fa-cap-keys {
|
||
font-size: 12px; font-weight: 300; line-height: 1.5;
|
||
color: rgba(255,255,255,0.68);
|
||
letter-spacing: 0.2px;
|
||
}
|
||
.fa-cap-foot {
|
||
padding: 0.85rem 1.5rem;
|
||
background: rgba(0,0,0,0.18);
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
font-size: 11px; letter-spacing: 1.8px; text-transform: uppercase;
|
||
color: rgba(255,255,255,0.65);
|
||
}
|
||
.fa-cap-foot-dot {
|
||
display:inline-flex; align-items:center; gap:8px;
|
||
color: rgba(255,255,255,0.7);
|
||
}
|
||
.fa-cap-foot-dot::before {
|
||
content:''; width:6px; height:6px; border-radius:50%;
|
||
background: #6FD49A;
|
||
box-shadow: 0 0 0 3px rgba(111,212,154,0.18);
|
||
}
|
||
|
||
/* Original stat cards (kept for tweak toggle) */
|
||
.fa-stat-card {
|
||
background:var(--fa-navy); color:#fff;
|
||
padding:1.75rem 2.25rem; width:290px;
|
||
}
|
||
.fa-stat-card .num {
|
||
font-family:'Syne',sans-serif; font-size:2rem; font-weight:800;
|
||
letter-spacing:-0.5px; color:var(--fa-blue-light); line-height:1; margin-bottom:0.4rem;
|
||
}
|
||
.fa-stat-card .label { font-size:13px; font-weight:300; opacity:0.65; line-height:1.5; }
|
||
.fa-stat-card.light {
|
||
background:rgba(238,243,251,0.9); color:var(--fa-navy);
|
||
border:0.5px solid var(--fa-border); margin-right:2rem;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
.fa-stat-card.light .num { color:var(--fa-navy); }
|
||
.fa-stat-card.light .label { opacity:0.55; color:var(--fa-navy); }
|
||
.fa-hero-badge {
|
||
background:var(--fa-blue); color:#fff;
|
||
font-size:11px; font-weight:500; letter-spacing:2px; text-transform:uppercase;
|
||
padding:0.55rem 1.1rem; align-self:flex-start; margin-left:1rem;
|
||
}
|
||
.fa-hero-visual.legacy { gap:1.25rem; align-items:flex-end; max-width: none; }
|
||
|
||
/* ── TICKER / EXPERTISE BANNER ── */
|
||
.fa-ticker-wrap {
|
||
background: var(--fa-navy);
|
||
padding: 0;
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
.fa-ticker-inner {
|
||
display: flex;
|
||
width: max-content;
|
||
animation: ticker 75s linear infinite;
|
||
padding: 1.6rem 0;
|
||
}
|
||
.fa-ticker-wrap:hover .fa-ticker-inner { animation-play-state: paused; }
|
||
@keyframes ticker {
|
||
0% { transform: translateX(0); }
|
||
100% { transform: translateX(-50%); }
|
||
}
|
||
.fa-ticker-item {
|
||
display: flex; align-items: center; gap: 1.8rem;
|
||
padding: 0 2.2rem;
|
||
font-family: 'Syne', sans-serif;
|
||
font-size: 16px; font-weight: 600;
|
||
letter-spacing: 3px; text-transform: uppercase;
|
||
color: rgba(255,255,255,0.65);
|
||
white-space: nowrap;
|
||
}
|
||
.fa-ticker-dot {
|
||
width: 5px; height: 5px; border-radius: 50%;
|
||
background: var(--fa-blue); flex-shrink: 0;
|
||
}
|
||
/* Mobile: keep compact + a bit faster so it's not boring on small screens */
|
||
@media (max-width: 720px) {
|
||
.fa-ticker-inner { animation-duration: 45s; padding: 1.1rem 0; }
|
||
.fa-ticker-item { font-size: 11px; letter-spacing: 2px; padding: 0 1.5rem; gap: 1.25rem; }
|
||
}
|
||
/* Fade edges */
|
||
.fa-ticker-wrap::before,
|
||
.fa-ticker-wrap::after {
|
||
content: '';
|
||
position: absolute; top: 0; bottom: 0; width: 120px; z-index: 2;
|
||
pointer-events: none;
|
||
}
|
||
.fa-ticker-wrap::before { left:0; background: linear-gradient(to right, var(--fa-navy), transparent); }
|
||
.fa-ticker-wrap::after { right:0; background: linear-gradient(to left, var(--fa-navy), transparent); }
|
||
|
||
/* ── SERVICES ── */
|
||
.fa-services { padding:7rem var(--content-pad); background:var(--fa-off-white); }
|
||
.fa-section-header {
|
||
display:grid; grid-template-columns:1fr 1fr;
|
||
gap:2rem; margin-bottom:4.5rem; align-items:end;
|
||
}
|
||
.fa-section-tag {
|
||
font-size:11px; font-weight:500; letter-spacing:3px; text-transform:uppercase;
|
||
color:var(--fa-blue); margin-bottom:1rem;
|
||
display:flex; align-items:center; gap:10px;
|
||
}
|
||
.fa-section-tag::before { content:''; display:block; width:24px; height:1px; background:var(--fa-blue); }
|
||
.fa-section-header h2 {
|
||
font-family:'Syne',sans-serif;
|
||
font-size:clamp(2rem,3vw,2.8rem); font-weight:700;
|
||
line-height:1.1; letter-spacing:-1.2px; color:var(--fa-navy);
|
||
}
|
||
.fa-section-desc {
|
||
font-size:15px; font-weight:300; line-height:1.8;
|
||
color:var(--fa-mid); max-width:420px; align-self:end;
|
||
}
|
||
|
||
/* (KI pills removed — cleaner cards) */
|
||
|
||
.fa-services-grid {
|
||
display:grid; grid-template-columns:repeat(4,1fr);
|
||
gap:1px; background:var(--fa-border); border:0.5px solid var(--fa-border);
|
||
}
|
||
.fa-service-card {
|
||
background:#fff; padding:2.25rem 2rem;
|
||
cursor:pointer; transition:background 0.25s; position:relative;
|
||
}
|
||
.fa-service-card:hover { background:var(--fa-navy); }
|
||
.fa-service-card:hover .fa-sc-num { color:var(--fa-blue-light); }
|
||
.fa-service-card:hover .fa-sc-title,
|
||
.fa-service-card:hover .fa-sc-desc { color:rgba(255,255,255,0.85); }
|
||
.fa-service-card:hover .fa-sc-tags span { color:rgba(255,255,255,0.6); border-color:rgba(255,255,255,0.15); }
|
||
.fa-sc-num {
|
||
font-family:'Syne',sans-serif; font-size:12px; font-weight:700;
|
||
letter-spacing:2px; color:var(--fa-blue); margin-bottom:1.25rem; transition:color 0.25s;
|
||
}
|
||
.fa-sc-title {
|
||
font-family:'Syne',sans-serif; font-size:16px; font-weight:700;
|
||
color:var(--fa-navy); margin-bottom:0.75rem; line-height:1.25; transition:color 0.25s;
|
||
}
|
||
.fa-sc-desc {
|
||
font-size:14px; font-weight:300; line-height:1.7;
|
||
color:var(--fa-mid); margin-bottom:1.25rem; transition:color 0.25s;
|
||
}
|
||
.fa-sc-tags { display:flex; flex-wrap:wrap; gap:5px; }
|
||
.fa-sc-tags span {
|
||
font-size:11px; letter-spacing:0.5px; color:var(--fa-navy);
|
||
border:0.5px solid var(--fa-border); padding:3px 9px; opacity:0.7;
|
||
transition:color 0.25s, border-color 0.25s;
|
||
}
|
||
|
||
/* ── KI HIGHLIGHT SECTION ── */
|
||
.fa-ai-section {
|
||
padding: 7rem var(--content-pad);
|
||
background: var(--fa-navy);
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 5rem;
|
||
align-items: center;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
.fa-ai-section::before {
|
||
content: 'AI';
|
||
font-family: 'Syne', sans-serif;
|
||
font-size: 22rem;
|
||
font-weight: 800;
|
||
color: rgba(107,143,212,0.04);
|
||
position: absolute;
|
||
right: -2rem; bottom: -4rem;
|
||
line-height: 1;
|
||
pointer-events: none;
|
||
letter-spacing: -10px;
|
||
}
|
||
.fa-ai-content { position: relative; z-index: 1; }
|
||
.fa-ai-tag {
|
||
font-size:11px; font-weight:500; letter-spacing:3px; text-transform:uppercase;
|
||
color:var(--fa-blue-light); margin-bottom:1rem;
|
||
display:flex; align-items:center; gap:10px;
|
||
}
|
||
.fa-ai-tag::before { content:''; display:block; width:24px; height:1px; background:var(--fa-blue-light); }
|
||
.fa-ai-content h2 {
|
||
font-family:'Syne',sans-serif;
|
||
font-size:clamp(1.8rem,2.8vw,2.6rem); font-weight:700;
|
||
letter-spacing:-1px; line-height:1.1; color:#fff; margin-bottom:1.5rem;
|
||
}
|
||
.fa-ai-content h2 em { font-style:normal; color:var(--fa-blue-light); }
|
||
.fa-ai-content p {
|
||
font-size:15px; font-weight:300; line-height:1.8;
|
||
color:rgba(255,255,255,0.72); margin-bottom:2.5rem; max-width:440px;
|
||
}
|
||
.fa-ai-capabilities {
|
||
display: grid; grid-template-columns: 1fr 1fr; gap: 1px;
|
||
background: rgba(107,143,212,0.15);
|
||
border: 0.5px solid rgba(107,143,212,0.2);
|
||
position: relative; z-index: 1;
|
||
}
|
||
.fa-ai-cap {
|
||
background: rgba(26,31,92,0.6);
|
||
padding: 1.75rem;
|
||
transition: background 0.2s;
|
||
}
|
||
.fa-ai-cap:hover { background: rgba(107,143,212,0.12); }
|
||
.fa-ai-cap-icon {
|
||
width: 32px; height: 32px;
|
||
background: rgba(107,143,212,0.15);
|
||
border: 0.5px solid rgba(107,143,212,0.3);
|
||
display: flex; align-items: center; justify-content: center;
|
||
margin-bottom: 0.85rem;
|
||
font-size: 14px; color: var(--fa-blue-light);
|
||
}
|
||
.fa-ai-cap h4 {
|
||
font-family:'Syne',sans-serif; font-size:14px; font-weight:700;
|
||
color:#fff; margin-bottom:0.4rem; letter-spacing:-0.2px;
|
||
}
|
||
.fa-ai-cap p {
|
||
font-size:13px; font-weight:300; line-height:1.6;
|
||
color:rgba(255,255,255,0.65); margin-bottom:0;
|
||
}
|
||
|
||
/* ── WHY ── */
|
||
.fa-why {
|
||
padding:7rem var(--content-pad);
|
||
display:grid; grid-template-columns:1fr 1fr;
|
||
gap:5rem; align-items:center; background:#fff;
|
||
}
|
||
.fa-why-visual {
|
||
position: relative;
|
||
aspect-ratio: 2 / 3;
|
||
overflow: hidden;
|
||
}
|
||
.fa-why-img {
|
||
position: absolute; inset: 0;
|
||
width: 100%; height: 100%;
|
||
object-fit: cover;
|
||
object-position: center;
|
||
display: block;
|
||
}
|
||
/* Subtle gradient so text overlays stay readable */
|
||
.fa-why-visual::after {
|
||
content: ''; position: absolute; inset: 0;
|
||
background: linear-gradient(to bottom,
|
||
rgba(15,19,64,0.0) 30%,
|
||
rgba(15,19,64,0.18) 65%,
|
||
rgba(15,19,64,0.55) 100%);
|
||
pointer-events: none;
|
||
}
|
||
.fa-why-block {
|
||
position: absolute;
|
||
left: 1.5rem; right: 1.5rem; bottom: 1.5rem;
|
||
background: var(--fa-navy);
|
||
padding: 2rem 2.25rem;
|
||
color: #fff;
|
||
z-index: 2;
|
||
box-shadow: 0 20px 50px -20px rgba(15,19,64,0.45);
|
||
}
|
||
.fa-why-block-quote {
|
||
font-family:'Syne',sans-serif; font-size:1.25rem; font-weight:600;
|
||
line-height:1.35; letter-spacing:-0.3px; color:#fff; margin-bottom:1rem;
|
||
}
|
||
.fa-why-block-quote em { font-style:normal; color:var(--fa-blue-light); }
|
||
.fa-why-block-sub {
|
||
font-size:13px; font-weight:300; line-height:1.7;
|
||
color:rgba(255,255,255,0.72);
|
||
}
|
||
.fa-accent-bar {
|
||
position: absolute;
|
||
top: 1.5rem; right: 1.5rem;
|
||
width: 110px; height: 110px;
|
||
background: rgba(255,255,255,0.92);
|
||
backdrop-filter: blur(6px);
|
||
-webkit-backdrop-filter: blur(6px);
|
||
border: 0.5px solid rgba(26,31,92,0.08);
|
||
display: flex; align-items: center; justify-content: center;
|
||
flex-direction: column; gap: 3px;
|
||
z-index: 2;
|
||
}
|
||
.fa-accent-bar .city {
|
||
font-family:'Syne',sans-serif; font-size:13px; font-weight:800;
|
||
color:var(--fa-navy); letter-spacing:2.5px; text-transform:uppercase;
|
||
}
|
||
.fa-accent-bar .tagline {
|
||
font-size:9px; color:var(--fa-mid);
|
||
letter-spacing:1.5px; text-transform:uppercase;
|
||
}
|
||
.fa-photo-credit {
|
||
position: absolute; bottom: 6px; right: 10px; z-index: 2;
|
||
font-family: 'DM Sans', sans-serif;
|
||
font-size: 9px; letter-spacing: 1px;
|
||
color: rgba(255,255,255,0.7);
|
||
text-shadow: 0 1px 4px rgba(0,0,0,0.6);
|
||
text-decoration: none;
|
||
}
|
||
.fa-photo-credit:hover { color: #fff; }
|
||
.fa-why-content h2 {
|
||
font-family:'Syne',sans-serif; font-size:clamp(1.8rem,2.8vw,2.4rem);
|
||
font-weight:700; letter-spacing:-0.8px; line-height:1.15; color:var(--fa-navy); margin-bottom:2.75rem;
|
||
}
|
||
.fa-points { display:flex; flex-direction:column; gap:1.75rem; }
|
||
.fa-point { display:grid; grid-template-columns:2.5rem 1fr; gap:1rem; align-items:start; }
|
||
.fa-point-num { font-family:'Syne',sans-serif; font-size:12px; font-weight:700; color:var(--fa-blue); letter-spacing:1px; padding-top:3px; }
|
||
.fa-point-text strong { display:block; font-size:15px; font-weight:500; color:var(--fa-navy); margin-bottom:5px; }
|
||
.fa-point-text p { font-size:14px; font-weight:300; line-height:1.7; color:var(--fa-mid); }
|
||
|
||
/* ── BOOKING SECTION (Beratungstermin) ── */
|
||
.fa-booking {
|
||
padding: 6rem var(--content-pad);
|
||
background: #fff;
|
||
border-top: 0.5px solid var(--fa-border);
|
||
border-bottom: 0.5px solid var(--fa-border);
|
||
}
|
||
.fa-booking-inner {
|
||
display: grid; grid-template-columns: 1fr 1.05fr;
|
||
gap: 4rem; align-items: stretch;
|
||
}
|
||
.fa-booking-content { display: flex; flex-direction: column; }
|
||
.fa-booking-tag {
|
||
display: inline-flex; align-items: center; gap: 10px;
|
||
font-size:11px; font-weight:500; letter-spacing:3px; text-transform:uppercase;
|
||
color:var(--fa-blue); margin-bottom:1rem;
|
||
}
|
||
.fa-booking-tag::before { content:''; display:block; width:24px; height:1px; background:var(--fa-blue); }
|
||
.fa-booking-content h2 {
|
||
font-family:'Syne',sans-serif;
|
||
font-size: clamp(1.7rem, 2.6vw, 2.4rem); font-weight:700;
|
||
line-height:1.1; letter-spacing:-1px; color:var(--fa-navy);
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
.fa-booking-content h2 em { font-style:normal; color:var(--fa-blue); }
|
||
.fa-booking-content > p {
|
||
font-size:15px; font-weight:300; line-height:1.75;
|
||
color:var(--fa-mid); margin-bottom:2.25rem; max-width:440px;
|
||
}
|
||
.fa-booking-list {
|
||
list-style: none; display: flex; flex-direction: column; gap: 1.5rem;
|
||
margin-top: auto;
|
||
}
|
||
.fa-booking-list li { display: grid; grid-template-columns: 2.5rem 1fr; gap: 1rem; }
|
||
.fa-booking-li-num {
|
||
font-family:'Syne',sans-serif; font-size:12px; font-weight:700;
|
||
color:var(--fa-blue); letter-spacing:1.5px; padding-top:3px;
|
||
}
|
||
.fa-booking-list strong {
|
||
display:block; font-size:15px; font-weight:500;
|
||
color:var(--fa-navy); margin-bottom:5px;
|
||
}
|
||
.fa-booking-list p { font-size:14px; font-weight:300; line-height:1.65; color:var(--fa-mid); }
|
||
/* Right column: calendar embed slot */
|
||
.fa-booking-cal {
|
||
background: var(--fa-navy);
|
||
color: #fff;
|
||
min-height: 460px;
|
||
position: relative;
|
||
overflow: hidden;
|
||
display: flex;
|
||
}
|
||
.fa-booking-cal iframe { width: 100%; height: 100%; border: 0; }
|
||
.fa-cal-placeholder {
|
||
flex: 1; display: flex; flex-direction: column;
|
||
align-items: flex-start; justify-content: center;
|
||
padding: 3rem 2.75rem; gap: 0.5rem;
|
||
position: relative;
|
||
}
|
||
.fa-cal-placeholder::before {
|
||
/* Subtle grid pattern */
|
||
content: ''; position: absolute; inset: 0;
|
||
background-image:
|
||
linear-gradient(rgba(168,192,232,0.06) 1px, transparent 1px),
|
||
linear-gradient(90deg, rgba(168,192,232,0.06) 1px, transparent 1px);
|
||
background-size: 40px 40px;
|
||
pointer-events: none;
|
||
}
|
||
.fa-cal-icon {
|
||
width: 44px; height: 44px;
|
||
border: 0.5px solid rgba(168,192,232,0.4);
|
||
display: flex; align-items: center; justify-content: center;
|
||
margin-bottom: 1.25rem;
|
||
position: relative; z-index: 1;
|
||
}
|
||
.fa-cal-icon svg { width: 22px; height: 22px; stroke: var(--fa-blue-light); }
|
||
.fa-cal-label {
|
||
font-family:'Syne',sans-serif; font-size:11px; font-weight:600;
|
||
letter-spacing:2.5px; text-transform:uppercase;
|
||
color: var(--fa-blue-light); position: relative; z-index: 1;
|
||
}
|
||
.fa-cal-title {
|
||
font-family:'Syne',sans-serif; font-size: 1.6rem; font-weight: 700;
|
||
color: #fff; letter-spacing: -0.5px; line-height: 1.1;
|
||
margin: 0.25rem 0 0.75rem; position: relative; z-index: 1;
|
||
}
|
||
.fa-cal-sub {
|
||
font-size: 14px; font-weight: 300; color: rgba(255,255,255,0.72);
|
||
line-height: 1.6; max-width: 320px; margin-bottom: 1.75rem;
|
||
position: relative; z-index: 1;
|
||
}
|
||
.fa-cal-actions {
|
||
display: flex; flex-direction: column; gap: 0.75rem;
|
||
position: relative; z-index: 1;
|
||
}
|
||
.fa-btn-cal {
|
||
font-family:'DM Sans',sans-serif; font-size:13px; font-weight:500;
|
||
color: var(--fa-navy); background: #fff; border: none;
|
||
padding: 0.85rem 1.6rem; cursor: pointer; transition: background 0.2s;
|
||
text-decoration: none; display: inline-flex; align-items: center; gap: 8px;
|
||
white-space: nowrap;
|
||
}
|
||
.fa-btn-cal:hover { background: var(--fa-blue-light); }
|
||
.fa-cal-tiny {
|
||
font-size: 12px; color: rgba(255,255,255,0.65);
|
||
letter-spacing: 0.3px; line-height: 1.5;
|
||
}
|
||
.fa-cal-tiny a { color: var(--fa-blue-light); text-decoration: none; border-bottom: 0.5px solid rgba(168,192,232,0.3); }
|
||
|
||
/* ── KONTORION ── */
|
||
.fa-showcase {
|
||
padding:7rem var(--content-pad); background:var(--fa-off-white);
|
||
display:grid; grid-template-columns:1fr 1fr; gap:5rem; align-items:center;
|
||
}
|
||
.fa-showcase-label { font-size:10px; font-weight:500; letter-spacing:3px; text-transform:uppercase; color:var(--fa-mid); margin-bottom:0.9rem; }
|
||
.fa-showcase h2 { font-family:'Syne',sans-serif; font-size:clamp(1.7rem,2.5vw,2.2rem); font-weight:700; letter-spacing:-0.8px; line-height:1.15; color:var(--fa-navy); margin-bottom:1.25rem; }
|
||
.fa-showcase > div > p { font-size:15px; font-weight:300; line-height:1.8; color:var(--fa-mid); margin-bottom:2.25rem; max-width:430px; }
|
||
.fa-kontorion-card { background:var(--fa-navy); padding:3rem; color:#fff; position:relative; overflow:hidden; }
|
||
.fa-kontorion-card::before { content:'K'; font-family:'Syne',sans-serif; font-size:16rem; font-weight:800; color:rgba(107,143,212,0.06); position:absolute; right:-1.5rem; top:-2rem; line-height:1; pointer-events:none; }
|
||
.fa-k-label { font-size:11px; font-weight:500; letter-spacing:3px; text-transform:uppercase; color:var(--fa-blue-light); margin-bottom:1rem; opacity:0.9; }
|
||
.fa-k-title { font-family:'Syne',sans-serif; font-size:1.8rem; font-weight:700; letter-spacing:-0.5px; margin-bottom:0.75rem; color:#fff; }
|
||
.fa-k-desc { font-size:14px; font-weight:300; line-height:1.75; color:rgba(255,255,255,0.68); margin-bottom:1.75rem; max-width:310px; }
|
||
.fa-k-pills { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:2rem; }
|
||
.fa-k-pill { font-size:11px; letter-spacing:1.5px; text-transform:uppercase; color:var(--fa-blue-light); border:0.5px solid rgba(107,143,212,0.35); padding:4px 10px; }
|
||
.fa-k-link { display:inline-flex; align-items:center; gap:8px; font-size:13px; font-weight:500; letter-spacing:1px; text-transform:uppercase; color:#fff; text-decoration:none; border-bottom:1px solid rgba(255,255,255,0.2); padding-bottom:3px; transition:border-color 0.2s; }
|
||
.fa-k-link:hover { border-color:rgba(255,255,255,0.6); }
|
||
|
||
/* ── CTA ── */
|
||
.fa-cta { background:var(--fa-navy); padding:7rem var(--content-pad); display:grid; grid-template-columns:1fr auto; align-items:center; gap:4rem; }
|
||
.fa-cta h2 { font-family:'Syne',sans-serif; font-size:clamp(1.8rem,3vw,2.8rem); font-weight:700; letter-spacing:-1.2px; line-height:1.08; color:#fff; }
|
||
.fa-cta h2 em { font-style:normal; color:var(--fa-blue-light); }
|
||
.fa-cta-sub { font-size:14px; font-weight:300; color:rgba(255,255,255,0.65); margin-top:0.9rem; }
|
||
.fa-cta-actions { display:flex; flex-direction:column; gap:0.9rem; align-items:flex-end; }
|
||
.fa-btn-white { font-family:'DM Sans',sans-serif; font-size:14px; font-weight:500; color:var(--fa-navy); background:#fff; border:none; padding:0.9rem 2.25rem; cursor:pointer; white-space:nowrap; transition:background 0.2s; text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem; }
|
||
.fa-btn-white:hover { background:var(--fa-blue-light); }
|
||
.fa-cta-loc { font-size:11px; color:rgba(255,255,255,0.55); letter-spacing:2px; text-transform:uppercase; }
|
||
|
||
/* ── FOOTER ── */
|
||
footer.fa-footer { background:var(--fa-navy-deep); padding:2rem var(--content-pad); display:flex; align-items:center; justify-content:space-between; gap: 1.5rem; flex-wrap: wrap; }
|
||
.fa-footer-left { font-family:'Syne',sans-serif; font-size:13px; font-weight:600; letter-spacing:2px; text-transform:uppercase; color:rgba(255,255,255,0.55); }
|
||
.fa-footer-links { display:flex; gap:1.75rem; }
|
||
.fa-footer-links a { font-size:13px; color:rgba(255,255,255,0.55); text-decoration:none; letter-spacing:0.5px; transition:color 0.2s; }
|
||
.fa-footer-links a:hover { color:rgba(255,255,255,0.9); }
|
||
|
||
/* ── SCROLL REVEAL ── */
|
||
[data-reveal] {
|
||
opacity: 0;
|
||
transform: translateY(22px);
|
||
transition: opacity 0.5s cubic-bezier(0.22, 0.61, 0.36, 1),
|
||
transform 0.5s cubic-bezier(0.22, 0.61, 0.36, 1);
|
||
transition-delay: var(--reveal-delay, 0ms);
|
||
}
|
||
[data-reveal].is-visible {
|
||
opacity: 1;
|
||
transform: none;
|
||
}
|
||
/* Don't animate on reduced-motion */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
[data-reveal] { opacity: 1; transform: none; transition: none; }
|
||
}
|
||
|
||
/* ── RESPONSIVE ── */
|
||
|
||
/* Small laptops & narrow desktops (1024-1280): keep paddings tighter so content breathes */
|
||
@media (max-width: 1280px) {
|
||
.fa-hero, .fa-services, .fa-ai-section, .fa-why, .fa-showcase, .fa-booking, .fa-cta {
|
||
padding-top: 5.5rem; padding-bottom: 5.5rem;
|
||
}
|
||
.fa-services-grid { grid-template-columns: repeat(2, 1fr); }
|
||
.fa-booking-inner { gap: 3rem; }
|
||
.fa-section-header { gap: 1.5rem; margin-bottom: 3.5rem; }
|
||
}
|
||
|
||
/* Tablet & smaller (≤ 900px): stack 2-col layouts */
|
||
@media (max-width: 900px) {
|
||
.fa-hero, .fa-why, .fa-showcase, .fa-ai-section, .fa-booking-inner, .fa-cta {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.fa-hero { min-height: auto; padding: 4rem var(--content-pad); }
|
||
.fa-hero-content { padding-right: 0; }
|
||
.fa-hero-visual { display: none; }
|
||
.fa-section-header { grid-template-columns: 1fr; }
|
||
.fa-why, .fa-showcase, .fa-ai-section, .fa-booking { padding: 4.5rem var(--content-pad); gap: 3rem; }
|
||
.fa-services { padding: 4.5rem var(--content-pad); }
|
||
.fa-cta { padding: 4.5rem var(--content-pad); }
|
||
.fa-cta-actions { align-items: flex-start; }
|
||
.fa-ai-capabilities { grid-template-columns: 1fr; }
|
||
.fa-why-visual { aspect-ratio: 4 / 5; max-height: 70vh; }
|
||
/* Reverse stacking order so photo comes after content on mobile? Keep visual-first; user can reorder later */
|
||
.fa-booking-cal { min-height: 380px; }
|
||
}
|
||
|
||
/* Small tablet / large phone (≤ 720px) */
|
||
@media (max-width: 720px) {
|
||
nav.fa-nav { padding: 0.9rem var(--content-pad); gap: 0.75rem; }
|
||
.fa-nav-links { display: none; }
|
||
.fa-nav-links.menu-open {
|
||
display: flex; flex-direction: column;
|
||
position: fixed; top: 56px; left: 0; right: 0; bottom: 0;
|
||
background: rgba(255,255,255,0.98); backdrop-filter: blur(10px);
|
||
padding: 2rem var(--content-pad); gap: 1.5rem;
|
||
align-items: flex-start; z-index: 199;
|
||
}
|
||
.fa-nav-links.menu-open a { font-size: 18px; opacity: 0.85; }
|
||
.fa-nav-menu { display: flex; }
|
||
.fa-logo-text { font-size: 10px; letter-spacing: 1.5px; }
|
||
.fa-nav-cta { padding: 0.55rem 1rem; font-size: 12px; }
|
||
}
|
||
|
||
/* Phone (≤ 480px) */
|
||
@media (max-width: 480px) {
|
||
.fa-hero h1 { font-size: clamp(1.85rem, 8.5vw, 2.6rem); letter-spacing: -1.2px; }
|
||
.fa-hero .fa-eyebrow { font-size: 10px; letter-spacing: 2px; margin-bottom: 1.25rem; }
|
||
.fa-hero-sub { font-size: 15px; margin-bottom: 2rem; }
|
||
.fa-hero-actions { flex-wrap: wrap; gap: 1rem; }
|
||
.fa-btn-primary { padding: 0.85rem 1.6rem; }
|
||
.fa-hero { padding: 3rem var(--content-pad) 3.5rem; }
|
||
|
||
.fa-services-grid { grid-template-columns: 1fr; }
|
||
.fa-section-header h2 { font-size: clamp(1.6rem, 7vw, 2rem); }
|
||
.fa-service-card { padding: 1.75rem 1.5rem; }
|
||
|
||
.fa-ai-section { padding: 4rem var(--content-pad); gap: 2rem; }
|
||
.fa-ai-section::before { font-size: 12rem; right: -1rem; bottom: -2rem; }
|
||
.fa-ai-content h2 { font-size: clamp(1.5rem, 6.5vw, 2rem); }
|
||
|
||
.fa-why-block { left: 0.75rem; right: 0.75rem; bottom: 0.75rem; padding: 1.5rem 1.5rem; }
|
||
.fa-why-block-quote { font-size: 1.05rem; }
|
||
.fa-why-block-sub { font-size: 11px; }
|
||
.fa-accent-bar { top: 0.75rem; right: 0.75rem; width: 88px; height: 88px; }
|
||
.fa-accent-bar .city { font-size: 11px; letter-spacing: 2px; }
|
||
.fa-accent-bar .tagline { font-size: 8px; }
|
||
.fa-why-content h2 { font-size: clamp(1.6rem, 7vw, 2rem); }
|
||
|
||
.fa-showcase h2 { font-size: clamp(1.5rem, 6.5vw, 1.9rem); }
|
||
.fa-kontorion-card { padding: 2rem 1.75rem; }
|
||
.fa-kontorion-card::before { font-size: 11rem; }
|
||
|
||
.fa-booking-cal { min-height: 320px; }
|
||
.fa-cal-placeholder { padding: 2rem 1.75rem; }
|
||
.fa-cal-title { font-size: 1.3rem; }
|
||
|
||
.fa-cta h2 { font-size: clamp(1.6rem, 7vw, 2.2rem); }
|
||
.fa-cta-actions { width: 100%; }
|
||
.fa-btn-white { width: 100%; text-align: center; justify-content: center; }
|
||
|
||
footer.fa-footer { padding: 1.5rem var(--content-pad); flex-direction: column; gap: 1rem; align-items: flex-start; }
|
||
.fa-footer-links { flex-wrap: wrap; gap: 1rem 1.5rem; }
|
||
|
||
/* Hero canvases: don't dominate phones */
|
||
#hero-canvas, #hero-canvas-gl { opacity: 0.5; }
|
||
}
|
||
|
||
/* Touch devices: cursor effects fall back to ambient motion (already handled by mouseSmooth drifting to center). Hide tweaks panel auto-hint for hover. */
|
||
@media (hover: none) and (pointer: coarse) {
|
||
.fa-service-card:hover { background: #fff; } /* avoid sticky hover state on touch */
|
||
.fa-service-card:hover .fa-sc-num { color: var(--fa-blue); }
|
||
.fa-service-card:hover .fa-sc-title,
|
||
.fa-service-card:hover .fa-sc-desc { color: var(--fa-navy); }
|
||
.fa-service-card:hover .fa-sc-tags span { color: var(--fa-navy); border-color: var(--fa-border); }
|
||
.fa-service-card:hover .fa-sc-desc { color: var(--fa-mid); }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- NAV -->
|
||
<nav class="fa-nav" id="fa-nav">
|
||
<a href="#top" class="fa-logo-area" style="text-decoration:none;">
|
||
<div class="fa-logo-mark">FA</div>
|
||
<div class="fa-logo-divider"></div>
|
||
<div class="fa-logo-text">Frontier<br>Algorithmics</div>
|
||
</a>
|
||
<ul class="fa-nav-links" id="fa-nav-links">
|
||
<li><a href="#services">Leistungen</a></li>
|
||
<li><a href="#ki">KI & Daten</a></li>
|
||
<li><a href="#kontorion">Kontorion</a></li>
|
||
<li><a href="#why">Über uns</a></li>
|
||
<li><a href="#" class="lang" aria-disabled="true" title="English version coming soon"><span class="active">DE</span> / EN</a></li>
|
||
</ul>
|
||
<button class="fa-nav-menu" id="fa-nav-menu" aria-label="Menu" aria-expanded="false">
|
||
<span></span><span></span><span></span>
|
||
</button>
|
||
<a href="#kontakt" class="fa-nav-cta">Beratungstermin</a>
|
||
</nav>
|
||
|
||
<!-- HERO -->
|
||
<section class="fa-hero" id="top">
|
||
<canvas id="hero-canvas"></canvas>
|
||
<canvas id="hero-canvas-gl"></canvas>
|
||
|
||
<div class="fa-hero-content">
|
||
<div class="fa-ai-badge">
|
||
<div class="fa-ai-dot"></div>
|
||
KI-getriebene Technologieberatung
|
||
</div>
|
||
<div class="fa-eyebrow">Regensburg · Bayern · International</div>
|
||
<h1 id="fa-headline">Komplexe Systeme.<br><em class="underline">Klare Lösungen.</em><br>Echter Fortschritt.</h1>
|
||
<p class="fa-hero-sub">Von intelligenten KI-Agenten bis zur regulierten Medizinsoftware – wir verbinden modernste Algorithmen mit der Präzision, die kritische Systeme verlangen.</p>
|
||
<div class="fa-hero-actions">
|
||
<a href="#kontakt" class="fa-btn-primary">Beratungstermin vereinbaren</a>
|
||
<a href="#kontorion" class="fa-btn-ghost">Kontorion ansehen ↗</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="fa-hero-visual" id="fa-hero-visual">
|
||
<!-- Capability stack (default) -->
|
||
<div class="fa-cap-card" id="fa-cap-card">
|
||
<div class="fa-cap-head">
|
||
<span>Kern-Disziplinen</span>
|
||
<span class="fa-cap-head-mark">Live</span>
|
||
</div>
|
||
<div class="fa-cap-row">
|
||
<div class="fa-cap-tag">AI</div>
|
||
<div class="fa-cap-body">
|
||
<div class="fa-cap-title">Generative KI & Agents</div>
|
||
<div class="fa-cap-keys">LLM · RAG · Computer Vision · MLOps</div>
|
||
</div>
|
||
</div>
|
||
<div class="fa-cap-row">
|
||
<div class="fa-cap-tag">MD</div>
|
||
<div class="fa-cap-body">
|
||
<div class="fa-cap-title">Regulierte Software</div>
|
||
<div class="fa-cap-keys">MDR · IEC 62304 · ISO 13485 · FHIR / DICOM</div>
|
||
</div>
|
||
</div>
|
||
<div class="fa-cap-row">
|
||
<div class="fa-cap-tag">SW</div>
|
||
<div class="fa-cap-body">
|
||
<div class="fa-cap-title">Engineering & Architektur</div>
|
||
<div class="fa-cap-keys">Apps · Backend · APIs · Tech Consulting</div>
|
||
</div>
|
||
</div>
|
||
<div class="fa-cap-foot">
|
||
<span class="fa-cap-foot-dot">Regensburg · DE</span>
|
||
<span>Seit 2020</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Original stat cards (toggle via Tweaks) -->
|
||
<div id="fa-stat-stack" style="display:none; flex-direction:column; gap:1.25rem; align-items:flex-end; width:100%;">
|
||
<div class="fa-stat-card">
|
||
<div class="num">LLM</div>
|
||
<div class="label">Agents · RAG · Fine-Tuning<br>Generative KI für Ihr Unternehmen</div>
|
||
</div>
|
||
<div class="fa-stat-card light">
|
||
<div class="num">MDR</div>
|
||
<div class="label">IEC 62304 · HL7 · FHIR · DICOM<br>Regulatorische Expertise</div>
|
||
</div>
|
||
<div class="fa-hero-badge">Seit Regensburg</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- TICKER -->
|
||
<div class="fa-ticker-wrap" aria-hidden="true">
|
||
<div class="fa-ticker-inner" id="ticker">
|
||
<!-- Items doubled for seamless loop -->
|
||
<span class="fa-ticker-item">KI-Strategie<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">LLM-Agents<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">Computer Vision<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">Predictive Analytics<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">RAG-Systeme<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">App-Entwicklung<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">Regulatory Affairs<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">HL7 / FHIR<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">DICOM<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">Software-Architektur<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">Agile Methoden<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">Data Science<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">MLOps<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">MDR / IEC 62304<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">Backend & APIs<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">Technologieberatung<div class="fa-ticker-dot"></div></span>
|
||
<!-- duplicate for seamless -->
|
||
<span class="fa-ticker-item">KI-Strategie<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">LLM-Agents<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">Computer Vision<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">Predictive Analytics<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">RAG-Systeme<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">App-Entwicklung<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">Regulatory Affairs<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">HL7 / FHIR<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">DICOM<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">Software-Architektur<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">Agile Methoden<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">Data Science<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">MLOps<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">MDR / IEC 62304<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">Backend & APIs<div class="fa-ticker-dot"></div></span>
|
||
<span class="fa-ticker-item">Technologieberatung<div class="fa-ticker-dot"></div></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SERVICES -->
|
||
<section class="fa-services" id="services">
|
||
<div class="fa-section-header">
|
||
<div>
|
||
<div class="fa-section-tag" data-reveal>Was wir tun</div>
|
||
<h2 data-reveal style="--reveal-delay:80ms">Vier Disziplinen.<br>Ein Partner.</h2>
|
||
</div>
|
||
<p class="fa-section-desc" data-reveal style="--reveal-delay:160ms">KI ist kein Add-on – sie ist der Antrieb hinter jedem unserer Leistungsbereiche. Wir decken den gesamten Projektzyklus ab, von der ersten Idee bis zur regulierten Lösung.</p>
|
||
</div>
|
||
<div class="fa-services-grid">
|
||
<div class="fa-service-card" data-reveal style="--reveal-delay:0ms">
|
||
<div class="fa-sc-num">01</div>
|
||
<div class="fa-sc-title">Data Science & KI</div>
|
||
<p class="fa-sc-desc">LLM-Agents, RAG-Systeme, Computer Vision und Predictive Analytics – von der Datenpipeline bis zum produktiven KI-System.</p>
|
||
<div class="fa-sc-tags">
|
||
<span>LLM / RAG</span><span>Agents</span><span>Computer Vision</span><span>Forecasting</span>
|
||
</div>
|
||
</div>
|
||
<div class="fa-service-card" data-reveal style="--reveal-delay:80ms">
|
||
<div class="fa-sc-num">02</div>
|
||
<div class="fa-sc-title">Regulatorik</div>
|
||
<p class="fa-sc-desc">Medizinproduktesoftware nach MDR, ISO 13485 und IEC 62304 – von der technischen Dokumentation bis zur erfolgreichen Zulassung.</p>
|
||
<div class="fa-sc-tags">
|
||
<span>MDR</span><span>HL7 / FHIR</span><span>DICOM</span><span>IEC 62304</span>
|
||
</div>
|
||
</div>
|
||
<div class="fa-service-card" data-reveal style="--reveal-delay:160ms">
|
||
<div class="fa-sc-num">03</div>
|
||
<div class="fa-sc-title">App-Entwicklung</div>
|
||
<p class="fa-sc-desc">Native und Cross-Platform Apps, Backend-Systeme und Web-Frontends – präzise implementiert, skalierbar gebaut, KI-ready von Anfang an.</p>
|
||
<div class="fa-sc-tags">
|
||
<span>Cross-Platform</span><span>Backend</span><span>Frontend</span><span>APIs</span>
|
||
</div>
|
||
</div>
|
||
<div class="fa-service-card" data-reveal style="--reveal-delay:240ms">
|
||
<div class="fa-sc-num">04</div>
|
||
<div class="fa-sc-title">Software Engineering</div>
|
||
<p class="fa-sc-desc">Systemarchitektur, Agile Methoden und technische Beratung für Projekte, bei denen Struktur über Erfolg oder Scheitern entscheidet.</p>
|
||
<div class="fa-sc-tags">
|
||
<span>Architektur</span><span>Scrum / SAFe</span><span>Tech Consulting</span><span>Reviews</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- BOOKING / BERATUNGSTERMIN -->
|
||
<section class="fa-booking" id="kontakt">
|
||
<div class="fa-booking-inner">
|
||
<div class="fa-booking-content">
|
||
<div class="fa-booking-tag" data-reveal>Beratungstermin · 30 Min</div>
|
||
<h2 data-reveal style="--reveal-delay:80ms">In 30 Minuten sprechen wir<br>über <em>Ihren Use Case</em>.</h2>
|
||
<p data-reveal style="--reveal-delay:160ms">Kein Pitch, kein Verkaufsgespräch. Wir hören zu, stellen Fragen, und sagen Ihnen ehrlich, ob wir der richtige Partner sind – oder nicht.</p>
|
||
<ul class="fa-booking-list" data-reveal style="--reveal-delay:240ms">
|
||
<li>
|
||
<span class="fa-booking-li-num">01</span>
|
||
<div>
|
||
<strong>Konkrete Machbarkeit</strong>
|
||
<p>Wir schätzen Aufwand, Risiken und Zeitrahmen Ihres Projekts ein – nicht in Phrasen, sondern in Wochen.</p>
|
||
</div>
|
||
</li>
|
||
<li>
|
||
<span class="fa-booking-li-num">02</span>
|
||
<div>
|
||
<strong>Erste Architektur-Skizze</strong>
|
||
<p>Welche Methoden, Modelle und Werkzeuge wir für Ihre Anforderungen einsetzen würden.</p>
|
||
</div>
|
||
</li>
|
||
<li>
|
||
<span class="fa-booking-li-num">03</span>
|
||
<div>
|
||
<strong>Klare nächste Schritte</strong>
|
||
<p>Mit oder ohne uns weiter – Sie verlassen das Gespräch mit einer Empfehlung, die unabhängig vom Auftrag funktioniert.</p>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<!-- CALENDAR EMBED SLOT
|
||
To activate: replace the .fa-cal-placeholder block below with your booking iframe, e.g.
|
||
<iframe src="https://cal.com/your-handle/30min" style="width:100%;height:100%;border:0;"></iframe>
|
||
-->
|
||
<div class="fa-booking-cal" data-reveal style="--reveal-delay:120ms">
|
||
<div class="fa-cal-placeholder">
|
||
<div class="fa-cal-icon" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||
<rect x="3" y="5" width="18" height="16" rx="1"></rect>
|
||
<line x1="3" y1="10" x2="21" y2="10"></line>
|
||
<line x1="8" y1="3" x2="8" y2="7"></line>
|
||
<line x1="16" y1="3" x2="16" y2="7"></line>
|
||
<circle cx="8" cy="15" r="1" fill="currentColor"></circle>
|
||
<circle cx="12" cy="15" r="1" fill="currentColor"></circle>
|
||
<circle cx="16" cy="15" r="1" fill="currentColor"></circle>
|
||
</svg>
|
||
</div>
|
||
<div class="fa-cal-label">Termin wählen</div>
|
||
<div class="fa-cal-title">30-Minuten-Slot</div>
|
||
<div class="fa-cal-sub">Hier läuft Ihr Booking-Embed (Cal.com, Calendly, o.ä.). Bis dahin schreiben Sie uns direkt – wir antworten innerhalb eines Werktags.</div>
|
||
<div class="fa-cal-actions">
|
||
<a href="mailto:contact@frontieralgorithmics.com?subject=Beratungstermin%20–%20Frontier%20Algorithmics" class="fa-btn-cal">Per E-Mail anfragen ↗</a>
|
||
<div class="fa-cal-tiny">
|
||
oder direkt: <a href="mailto:contact@frontieralgorithmics.com">contact@frontieralgorithmics.com</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- KI HIGHLIGHT -->
|
||
<section class="fa-ai-section" id="ki">
|
||
<div class="fa-ai-content">
|
||
<div class="fa-ai-tag" data-reveal>KI-Expertise</div>
|
||
<h2 data-reveal style="--reveal-delay:80ms">Intelligente Systeme,<br>die <em>echte Arbeit</em> leisten.</h2>
|
||
<p data-reveal style="--reveal-delay:160ms">Wir entwickeln keine Demos – wir bauen KI-Systeme, die in produktiven Umgebungen funktionieren. Von autonomen Agenten bis zur regulierten medizinischen Bildanalyse.</p>
|
||
<a href="#kontakt" class="fa-btn-primary" data-reveal style="--reveal-delay:240ms; background:#fff; color:var(--fa-navy); text-decoration:none;">Beratungstermin vereinbaren</a>
|
||
</div>
|
||
<div class="fa-ai-capabilities" data-reveal style="--reveal-delay:200ms">
|
||
<div class="fa-ai-cap">
|
||
<div class="fa-ai-cap-icon">⬡</div>
|
||
<h4>LLM-Agents & RAG</h4>
|
||
<p>Autonome Agenten und retrieval-gestützte Systeme für komplexe Unternehmensanwendungen.</p>
|
||
</div>
|
||
<div class="fa-ai-cap">
|
||
<div class="fa-ai-cap-icon">◎</div>
|
||
<h4>Computer Vision</h4>
|
||
<p>Bildanalyse und Objekterkennung – auch in regulierten Bereichen wie der Medizintechnik.</p>
|
||
</div>
|
||
<div class="fa-ai-cap">
|
||
<div class="fa-ai-cap-icon">⟁</div>
|
||
<h4>Predictive Analytics</h4>
|
||
<p>Forecasting-Modelle und Anomalieerkennung auf Basis realer Produktionsdaten.</p>
|
||
</div>
|
||
<div class="fa-ai-cap">
|
||
<div class="fa-ai-cap-icon">⊞</div>
|
||
<h4>MLOps & Deployment</h4>
|
||
<p>Vom Notebook zur skalierbaren Produktionsinfrastruktur – zuverlässig und wartbar.</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- WHY -->
|
||
<section class="fa-why" id="why">
|
||
<div class="fa-why-visual" data-reveal>
|
||
<img src="assets/regensburg.jpg"
|
||
alt="Regensburg — Steinerne Brücke und Dom St. Peter"
|
||
class="fa-why-img"
|
||
loading="lazy">
|
||
<div class="fa-accent-bar">
|
||
<div class="city">RGN</div>
|
||
<div class="tagline">Bayern · DE</div>
|
||
</div>
|
||
<div class="fa-why-block">
|
||
<div class="fa-why-block-quote">„Wir bauen nicht nur Software – wir tragen <em>Verantwortung</em> für das System dahinter."</div>
|
||
<div class="fa-why-block-sub">Frontier Algorithmics verbindet Ingenieursdenken mit unternehmerischer Verantwortung. Regensburg ist unser Zuhause – Europa und darüber hinaus unser Markt.</div>
|
||
</div>
|
||
<a class="fa-photo-credit"
|
||
href="https://unsplash.com/de/@lecreusois"
|
||
target="_blank" rel="noopener">Foto · L. Niederwimmer / Unsplash</a>
|
||
</div>
|
||
<div class="fa-why-content">
|
||
<h2 data-reveal style="--reveal-delay:120ms">Warum Frontier<br>Algorithmics?</h2>
|
||
<div class="fa-points">
|
||
<div class="fa-point" data-reveal style="--reveal-delay:200ms">
|
||
<div class="fa-point-num">01</div>
|
||
<div class="fa-point-text">
|
||
<strong>KI mit Substanz, nicht mit Buzzwords</strong>
|
||
<p>Wir implementieren LLM-Agents, RAG-Systeme und Computer-Vision-Lösungen, die in der Praxis funktionieren – nicht nur in der Demo.</p>
|
||
</div>
|
||
</div>
|
||
<div class="fa-point" data-reveal style="--reveal-delay:280ms">
|
||
<div class="fa-point-num">02</div>
|
||
<div class="fa-point-text">
|
||
<strong>Regulatorik trifft Technologie</strong>
|
||
<p>Wir sind eine der wenigen Firmen, die KI-Expertise und regulatorisches Know-how (MDR, IEC 62304) unter einem Dach vereinen.</p>
|
||
</div>
|
||
</div>
|
||
<div class="fa-point" data-reveal style="--reveal-delay:360ms">
|
||
<div class="fa-point-num">03</div>
|
||
<div class="fa-point-text">
|
||
<strong>Beweis statt Versprechen: Kontorion</strong>
|
||
<p>Wir haben ein eigenes Produkt entwickelt und am Markt platziert. Wir denken wie Unternehmer, nicht wie Dienstleister.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- KONTORION -->
|
||
<section class="fa-showcase" id="kontorion">
|
||
<div>
|
||
<div class="fa-showcase-label" data-reveal>Eigenes Produkt · Referenz</div>
|
||
<h2 data-reveal style="--reveal-delay:80ms">Kontorion – unsere Billing-Plattform für den EU-Markt.</h2>
|
||
<p data-reveal style="--reveal-delay:160ms">Kontorion ist der Beweis unserer Leistungsfähigkeit: eine vollständig intern entwickelte SaaS-Plattform für Subscription-Billing, EU-Steuern, SEPA und Revenue Recognition – gebaut für die regulatorische Realität von 27 Mitgliedsstaaten.</p>
|
||
<a href="https://kontorion.eu" target="_blank" rel="noopener" class="fa-btn-primary" data-reveal style="--reveal-delay:240ms">Mehr über Kontorion ↗</a>
|
||
</div>
|
||
<div class="fa-kontorion-card" data-reveal style="--reveal-delay:120ms">
|
||
<div class="fa-k-label">Produkt · kontorion.eu</div>
|
||
<div class="fa-k-title">Kontorion</div>
|
||
<p class="fa-k-desc">Subscription-Billing, EU-VAT/OSS, XRechnung, SEPA und IFRS 15 in einer Plattform. Nativ für Europa gebaut, nicht nachgerüstet.</p>
|
||
<div class="fa-k-pills">
|
||
<span class="fa-k-pill">EU-VAT / OSS</span>
|
||
<span class="fa-k-pill">SEPA</span>
|
||
<span class="fa-k-pill">XRechnung</span>
|
||
<span class="fa-k-pill">IFRS 15</span>
|
||
</div>
|
||
<a class="fa-k-link" href="https://kontorion.eu" target="_blank">kontorion.eu besuchen →</a>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- CTA -->
|
||
<section class="fa-cta">
|
||
<div>
|
||
<h2 data-reveal>Bereit, das <em>nächste<br>komplexe Projekt</em> anzugehen?</h2>
|
||
<p class="fa-cta-sub" data-reveal style="--reveal-delay:100ms">Wir hören zu, bevor wir reden. Kein Proposal ohne Verständnis.</p>
|
||
</div>
|
||
<div class="fa-cta-actions" data-reveal style="--reveal-delay:200ms">
|
||
<a href="#kontakt" class="fa-btn-white">Beratungstermin vereinbaren</a>
|
||
<div class="fa-cta-loc">Regensburg · Bayern · International</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- FOOTER -->
|
||
<footer class="fa-footer">
|
||
<div class="fa-footer-left">© 2025 Frontier Algorithmics · Regensburg</div>
|
||
<div class="fa-footer-links">
|
||
<a href="#">Impressum</a>
|
||
<a href="#">Datenschutz</a>
|
||
<a href="#">LinkedIn</a>
|
||
<a href="#">kontorion.eu</a>
|
||
</div>
|
||
</footer>
|
||
|
||
<script>
|
||
/* ============================================================
|
||
MOBILE NAV MENU TOGGLE
|
||
============================================================ */
|
||
(function() {
|
||
const nav = document.getElementById('fa-nav');
|
||
const btn = document.getElementById('fa-nav-menu');
|
||
const links = document.getElementById('fa-nav-links');
|
||
if (!nav || !btn || !links) return;
|
||
btn.addEventListener('click', () => {
|
||
const open = nav.classList.toggle('menu-open');
|
||
links.classList.toggle('menu-open', open);
|
||
btn.setAttribute('aria-expanded', String(open));
|
||
});
|
||
// Close on link click
|
||
links.querySelectorAll('a').forEach(a => {
|
||
a.addEventListener('click', () => {
|
||
nav.classList.remove('menu-open');
|
||
links.classList.remove('menu-open');
|
||
btn.setAttribute('aria-expanded', 'false');
|
||
});
|
||
});
|
||
})();
|
||
|
||
/* ============================================================
|
||
HERO SCROLL PROGRESS
|
||
Drives --hero-progress (0..1) as the user scrolls through the hero.
|
||
Used by the headline emphasis (underline grows, color saturates).
|
||
============================================================ */
|
||
(function() {
|
||
const hero = document.querySelector('.fa-hero');
|
||
if (!hero) return;
|
||
const root = document.documentElement;
|
||
let ticking = false;
|
||
|
||
function update() {
|
||
const rect = hero.getBoundingClientRect();
|
||
const h = rect.height;
|
||
// 0 when hero top hits viewport top; 1 when hero bottom hits viewport top.
|
||
// Begin advancing only after a small lead-in (~12%) so the underline
|
||
// entrance animation finishes cleanly before scroll takes over.
|
||
let p = -rect.top / h;
|
||
p = Math.max(0, Math.min(1, (p - 0.12) / 0.7));
|
||
root.style.setProperty('--hero-progress', p.toFixed(3));
|
||
ticking = false;
|
||
}
|
||
function onScroll() {
|
||
if (!ticking) { requestAnimationFrame(update); ticking = true; }
|
||
}
|
||
window.addEventListener('scroll', onScroll, { passive: true });
|
||
window.addEventListener('resize', onScroll);
|
||
update();
|
||
})();
|
||
|
||
/* ============================================================
|
||
HERO CANVAS — two modes
|
||
- 'constellation' (default refined): 4 anchored hub nodes with
|
||
real labels, drifting around their anchors; sparse filler nodes;
|
||
edges only to nearest 2 neighbors.
|
||
- 'network' (original): bouncing dots, all-pairs short edges.
|
||
============================================================ */
|
||
(function() {
|
||
const canvas = document.getElementById('hero-canvas');
|
||
const ctx = canvas.getContext('2d');
|
||
|
||
const NAVY = '26, 31, 92';
|
||
const BLUE = '107, 143, 212';
|
||
const BLUE_LIGHT = '168, 192, 232';
|
||
|
||
let W, H, nodes = [], hubs = [];
|
||
let mode = 'constellation';
|
||
let mouse = { x: -9999, y: -9999 };
|
||
let rafId = null;
|
||
let running = true;
|
||
|
||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||
|
||
function resize() {
|
||
const rect = canvas.parentElement.getBoundingClientRect();
|
||
W = rect.width; H = rect.height;
|
||
canvas.width = W * dpr;
|
||
canvas.height = H * dpr;
|
||
canvas.style.width = W + 'px';
|
||
canvas.style.height = H + 'px';
|
||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||
}
|
||
|
||
function rand(a, b) { return a + Math.random() * (b - a); }
|
||
|
||
/* ------- CONSTELLATION MODE ------- */
|
||
function initConstellation() {
|
||
hubs = [
|
||
{ ax: 0.18, ay: 0.30, label: 'LLM · RAG' },
|
||
{ ax: 0.62, ay: 0.18, label: 'COMPUTER VISION' },
|
||
{ ax: 0.78, ay: 0.62, label: 'MDR · 62304' },
|
||
{ ax: 0.32, ay: 0.82, label: 'FHIR · DICOM' },
|
||
].map((h, i) => ({
|
||
...h,
|
||
x: h.ax * W, y: h.ay * H,
|
||
vx: 0, vy: 0,
|
||
r: 4.5,
|
||
phase: i * 1.7,
|
||
}));
|
||
|
||
nodes = [];
|
||
const count = Math.max(28, Math.floor((W * H) / 22000));
|
||
for (let i = 0; i < count; i++) {
|
||
nodes.push({
|
||
x: rand(0, W), y: rand(0, H),
|
||
vx: rand(-0.18, 0.18),
|
||
vy: rand(-0.15, 0.15),
|
||
r: rand(1.2, 2.4),
|
||
});
|
||
}
|
||
}
|
||
|
||
function updateConstellation(t) {
|
||
// Hubs drift around their anchor (spring physics)
|
||
for (const h of hubs) {
|
||
const targetX = h.ax * W + Math.sin(t * 0.0004 + h.phase) * 18;
|
||
const targetY = h.ay * H + Math.cos(t * 0.0005 + h.phase * 1.3) * 14;
|
||
h.vx += (targetX - h.x) * 0.008;
|
||
h.vy += (targetY - h.y) * 0.008;
|
||
h.vx *= 0.92; h.vy *= 0.92;
|
||
h.x += h.vx; h.y += h.vy;
|
||
}
|
||
// Filler nodes drift with edge-wrapping
|
||
for (const n of nodes) {
|
||
n.x += n.vx; n.y += n.vy;
|
||
if (n.x < -10) n.x = W + 10;
|
||
if (n.x > W + 10) n.x = -10;
|
||
if (n.y < -10) n.y = H + 10;
|
||
if (n.y > H + 10) n.y = -10;
|
||
}
|
||
}
|
||
|
||
function nearestK(node, others, k) {
|
||
const arr = [];
|
||
for (const o of others) {
|
||
if (o === node) continue;
|
||
const dx = node.x - o.x, dy = node.y - o.y;
|
||
arr.push({ o, d: Math.sqrt(dx*dx + dy*dy) });
|
||
}
|
||
arr.sort((a, b) => a.d - b.d);
|
||
return arr.slice(0, k);
|
||
}
|
||
|
||
function drawConstellation() {
|
||
ctx.clearRect(0, 0, W, H);
|
||
|
||
// 1. Edges from each filler node to its nearest 2 neighbors
|
||
ctx.lineWidth = 0.9;
|
||
for (const n of nodes) {
|
||
const nn = nearestK(n, nodes, 2);
|
||
for (const { o, d } of nn) {
|
||
if (d > 160) continue;
|
||
const a = (1 - d / 160) * 0.25;
|
||
ctx.strokeStyle = `rgba(${BLUE}, ${a})`;
|
||
ctx.beginPath();
|
||
ctx.moveTo(n.x, n.y);
|
||
ctx.lineTo(o.x, o.y);
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
|
||
// 2. Stronger edges between hubs and their nearest filler nodes
|
||
ctx.lineWidth = 1.3;
|
||
for (const h of hubs) {
|
||
const nn = nearestK(h, nodes, 6);
|
||
for (const { o, d } of nn) {
|
||
if (d > 260) continue;
|
||
const a = (1 - d / 260) * 0.55;
|
||
ctx.strokeStyle = `rgba(${BLUE}, ${a})`;
|
||
ctx.beginPath();
|
||
ctx.moveTo(h.x, h.y);
|
||
ctx.lineTo(o.x, o.y);
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
|
||
// 3. Mouse pulls a bright connection to the 4 nearest nodes
|
||
if (mouse.x > -9000) {
|
||
const closest = nearestK({ x: mouse.x, y: mouse.y }, nodes.concat(hubs), 4);
|
||
ctx.lineWidth = 1.4;
|
||
for (const { o, d } of closest) {
|
||
if (d > 220) continue;
|
||
const a = (1 - d / 220) * 0.7;
|
||
ctx.strokeStyle = `rgba(${BLUE}, ${a})`;
|
||
ctx.beginPath();
|
||
ctx.moveTo(mouse.x, mouse.y);
|
||
ctx.lineTo(o.x, o.y);
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
|
||
// 4. Filler dots — blue-tinted for visibility
|
||
for (const n of nodes) {
|
||
const g2 = ctx.createRadialGradient(n.x, n.y, 0, n.x, n.y, n.r * 3);
|
||
g2.addColorStop(0, `rgba(${BLUE}, 0.18)`);
|
||
g2.addColorStop(1, `rgba(${BLUE}, 0)`);
|
||
ctx.beginPath();
|
||
ctx.arc(n.x, n.y, n.r * 3, 0, Math.PI * 2);
|
||
ctx.fillStyle = g2;
|
||
ctx.fill();
|
||
ctx.beginPath();
|
||
ctx.arc(n.x, n.y, n.r, 0, Math.PI * 2);
|
||
ctx.fillStyle = `rgba(${BLUE}, 0.45)`;
|
||
ctx.fill();
|
||
}
|
||
|
||
// 5. Hub nodes — halo + core + double pulse ring + label
|
||
const t = performance.now();
|
||
for (const h of hubs) {
|
||
// Outer pulse ring (slow, large)
|
||
const pulseT = ((t * 0.0007 + h.phase) % 1);
|
||
const pulseR = 8 + pulseT * 52;
|
||
ctx.beginPath();
|
||
ctx.arc(h.x, h.y, pulseR, 0, Math.PI * 2);
|
||
ctx.strokeStyle = `rgba(${BLUE}, ${(1 - pulseT) * 0.65})`;
|
||
ctx.lineWidth = 1.2;
|
||
ctx.stroke();
|
||
|
||
// Inner pulse ring (faster, smaller, offset phase)
|
||
const pulseT2 = ((t * 0.0011 + h.phase + 0.5) % 1);
|
||
const pulseR2 = 5 + pulseT2 * 28;
|
||
ctx.beginPath();
|
||
ctx.arc(h.x, h.y, pulseR2, 0, Math.PI * 2);
|
||
ctx.strokeStyle = `rgba(${BLUE}, ${(1 - pulseT2) * 0.45})`;
|
||
ctx.lineWidth = 0.8;
|
||
ctx.stroke();
|
||
|
||
// Glow halo
|
||
const g = ctx.createRadialGradient(h.x, h.y, 0, h.x, h.y, 42);
|
||
g.addColorStop(0, `rgba(${BLUE}, 0.45)`);
|
||
g.addColorStop(0.5, `rgba(${BLUE}, 0.12)`);
|
||
g.addColorStop(1, `rgba(${BLUE}, 0)`);
|
||
ctx.beginPath();
|
||
ctx.arc(h.x, h.y, 42, 0, Math.PI * 2);
|
||
ctx.fillStyle = g;
|
||
ctx.fill();
|
||
|
||
// Core dot
|
||
ctx.beginPath();
|
||
ctx.arc(h.x, h.y, h.r, 0, Math.PI * 2);
|
||
ctx.fillStyle = `rgba(${BLUE}, 1)`;
|
||
ctx.fill();
|
||
// Inner ring
|
||
ctx.beginPath();
|
||
ctx.arc(h.x, h.y, h.r + 3.5, 0, Math.PI * 2);
|
||
ctx.strokeStyle = `rgba(${BLUE}, 0.75)`;
|
||
ctx.lineWidth = 1.2;
|
||
ctx.stroke();
|
||
// Outer ring
|
||
ctx.beginPath();
|
||
ctx.arc(h.x, h.y, h.r + 8, 0, Math.PI * 2);
|
||
ctx.strokeStyle = `rgba(${BLUE}, 0.3)`;
|
||
ctx.lineWidth = 0.8;
|
||
ctx.stroke();
|
||
|
||
// Label tag
|
||
ctx.font = '700 11px "Syne", sans-serif';
|
||
const tw = ctx.measureText(h.label).width;
|
||
const lx = h.x + h.r + 12;
|
||
const ly = h.y - 5;
|
||
// tag background pill
|
||
ctx.fillStyle = `rgba(${BLUE}, 0.12)`;
|
||
ctx.fillRect(lx - 4, ly - 2, tw + 8, 16);
|
||
// tag border
|
||
ctx.strokeStyle = `rgba(${BLUE}, 0.4)`;
|
||
ctx.lineWidth = 0.6;
|
||
ctx.strokeRect(lx - 4, ly - 2, tw + 8, 16);
|
||
// label text
|
||
ctx.fillStyle = `rgba(${NAVY}, 0.95)`;
|
||
ctx.fillText(h.label, lx, ly + 9);
|
||
}
|
||
}
|
||
|
||
/* ------- ORIGINAL NETWORK MODE ------- */
|
||
function initNetwork() {
|
||
nodes = [];
|
||
const count = Math.max(50, Math.floor((W * H) / 14000));
|
||
for (let i = 0; i < count; i++) {
|
||
nodes.push({
|
||
x: rand(0, W), y: rand(0, H),
|
||
vx: rand(-0.25, 0.25),
|
||
vy: rand(-0.2, 0.2),
|
||
r: rand(2, 5),
|
||
isKey: Math.random() < 0.12,
|
||
label: ['LLM','RAG','CV','AI','ML','API','MDR'][Math.floor(Math.random()*7)]
|
||
});
|
||
}
|
||
hubs = [];
|
||
}
|
||
function updateNetwork() {
|
||
for (const n of nodes) {
|
||
n.x += n.vx; n.y += n.vy;
|
||
if (n.x < 0 || n.x > W) n.vx *= -1;
|
||
if (n.y < 0 || n.y > H) n.vy *= -1;
|
||
}
|
||
}
|
||
function drawNetwork() {
|
||
ctx.clearRect(0, 0, W, H);
|
||
for (let i = 0; i < nodes.length; i++) {
|
||
for (let j = i + 1; j < nodes.length; j++) {
|
||
const dx = nodes[i].x - nodes[j].x;
|
||
const dy = nodes[i].y - nodes[j].y;
|
||
const d = Math.sqrt(dx*dx + dy*dy);
|
||
if (d < 130) {
|
||
const a = (1 - d / 130) * 0.18;
|
||
ctx.strokeStyle = `rgba(${BLUE}, ${a})`;
|
||
ctx.lineWidth = 0.6;
|
||
ctx.beginPath();
|
||
ctx.moveTo(nodes[i].x, nodes[i].y);
|
||
ctx.lineTo(nodes[j].x, nodes[j].y);
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
const mdx = nodes[i].x - mouse.x;
|
||
const mdy = nodes[i].y - mouse.y;
|
||
const md = Math.sqrt(mdx*mdx + mdy*mdy);
|
||
if (md < 160) {
|
||
const a = (1 - md / 160) * 0.35;
|
||
ctx.strokeStyle = `rgba(${BLUE}, ${a})`;
|
||
ctx.lineWidth = 0.8;
|
||
ctx.beginPath();
|
||
ctx.moveTo(nodes[i].x, nodes[i].y);
|
||
ctx.lineTo(mouse.x, mouse.y);
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
for (const n of nodes) {
|
||
const g = ctx.createRadialGradient(n.x, n.y, 0, n.x, n.y, n.r * 4);
|
||
g.addColorStop(0, `rgba(${BLUE}, 0.12)`);
|
||
g.addColorStop(1, `rgba(${BLUE}, 0)`);
|
||
ctx.beginPath();
|
||
ctx.arc(n.x, n.y, n.r * 4, 0, Math.PI * 2);
|
||
ctx.fillStyle = g;
|
||
ctx.fill();
|
||
ctx.beginPath();
|
||
ctx.arc(n.x, n.y, n.r, 0, Math.PI * 2);
|
||
ctx.fillStyle = n.isKey ? `rgba(${BLUE}, 0.75)` : `rgba(${NAVY}, 0.35)`;
|
||
ctx.fill();
|
||
if (n.isKey) {
|
||
ctx.font = '600 9px "Syne", sans-serif';
|
||
ctx.fillStyle = `rgba(${BLUE}, 0.55)`;
|
||
ctx.fillText(n.label, n.x + n.r + 4, n.y + 3);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ------- FLOW FIELD MODE ------- */
|
||
let flowParticles = [];
|
||
function initFlowfield() {
|
||
flowParticles = [];
|
||
const count = Math.max(160, Math.floor((W * H) / 4500));
|
||
for (let i = 0; i < count; i++) {
|
||
flowParticles.push({
|
||
x: rand(0, W), y: rand(0, H),
|
||
px: 0, py: 0,
|
||
life: rand(0, 200),
|
||
maxLife: 220 + rand(0, 280),
|
||
speed: 0.35 + rand(0, 0.7),
|
||
});
|
||
}
|
||
// Paint a fresh white so we don't carry over residue from another mode
|
||
ctx.fillStyle = '#fff';
|
||
ctx.fillRect(0, 0, W, H);
|
||
}
|
||
function flowAngle(x, y, t) {
|
||
const a = Math.sin(x * 0.0045 + t * 0.00025);
|
||
const b = Math.cos(y * 0.0042 - t * 0.00030);
|
||
const c = Math.sin((x + y) * 0.0023 + t * 0.00040);
|
||
return (a + b + c) * Math.PI;
|
||
}
|
||
function updateFlowfield(t) {
|
||
for (const p of flowParticles) {
|
||
p.px = p.x; p.py = p.y;
|
||
const ang = flowAngle(p.x, p.y, t);
|
||
p.x += Math.cos(ang) * p.speed;
|
||
p.y += Math.sin(ang) * p.speed;
|
||
|
||
// Mouse repulsion
|
||
if (mouse.x > -9000) {
|
||
const dx = p.x - mouse.x, dy = p.y - mouse.y;
|
||
const d = Math.sqrt(dx*dx + dy*dy);
|
||
if (d < 130 && d > 0.1) {
|
||
const f = (1 - d / 130) * 1.6;
|
||
p.x += (dx / d) * f;
|
||
p.y += (dy / d) * f;
|
||
}
|
||
}
|
||
|
||
p.life++;
|
||
if (p.life > p.maxLife || p.x < -5 || p.x > W + 5 || p.y < -5 || p.y > H + 5) {
|
||
p.x = rand(0, W); p.y = rand(0, H);
|
||
p.life = 0;
|
||
p.px = p.x; p.py = p.y;
|
||
}
|
||
}
|
||
}
|
||
function drawFlowfield() {
|
||
// Soft fade of previous frame instead of clear -> trails
|
||
ctx.fillStyle = 'rgba(255,255,255,0.018)';
|
||
ctx.fillRect(0, 0, W, H);
|
||
|
||
ctx.lineWidth = 1.0;
|
||
ctx.lineCap = 'round';
|
||
for (const p of flowParticles) {
|
||
const lr = p.life / p.maxLife;
|
||
const a = Math.sin(lr * Math.PI) * 0.7; // fade in & out
|
||
// Mix navy + blue based on life position
|
||
const useBlue = ((p.x + p.y) | 0) % 3 !== 0;
|
||
ctx.strokeStyle = useBlue
|
||
? `rgba(${BLUE}, ${a})`
|
||
: `rgba(${NAVY}, ${a * 0.7})`;
|
||
ctx.beginPath();
|
||
ctx.moveTo(p.px, p.py);
|
||
ctx.lineTo(p.x, p.y);
|
||
ctx.stroke();
|
||
}
|
||
}
|
||
|
||
/* ------- TOPOGRAPHY MODE ------- */
|
||
const TOPO_RES = 26;
|
||
let topoField = null, topoCols = 0, topoRows = 0;
|
||
function initTopo() {
|
||
topoCols = Math.ceil(W / TOPO_RES) + 2;
|
||
topoRows = Math.ceil(H / TOPO_RES) + 2;
|
||
topoField = new Float32Array(topoCols * topoRows);
|
||
}
|
||
function topoNoise(x, y, t) {
|
||
return Math.sin(x * 0.011 + t * 0.00028)
|
||
+ Math.cos(y * 0.013 - t * 0.00022)
|
||
+ 0.6 * Math.sin((x + y) * 0.0075 + t * 0.00036)
|
||
+ 0.4 * Math.cos((x - y) * 0.0098 - t * 0.00042);
|
||
}
|
||
function updateTopo(t) {
|
||
const mx = mouse.x, my = mouse.y, hasMouse = mx > -9000;
|
||
for (let j = 0; j < topoRows; j++) {
|
||
const y = j * TOPO_RES;
|
||
for (let i = 0; i < topoCols; i++) {
|
||
const x = i * TOPO_RES;
|
||
let v = topoNoise(x, y, t);
|
||
if (hasMouse) {
|
||
const dx = x - mx, dy = y - my;
|
||
const d2 = dx*dx + dy*dy;
|
||
if (d2 < 60000) v += 1.6 * Math.exp(-d2 / 9000);
|
||
}
|
||
topoField[j * topoCols + i] = v;
|
||
}
|
||
}
|
||
}
|
||
function drawTopo() {
|
||
ctx.clearRect(0, 0, W, H);
|
||
const thresholds = [-1.6, -1.0, -0.4, 0.2, 0.8, 1.4, 2.0];
|
||
for (let ti = 0; ti < thresholds.length; ti++) {
|
||
const thr = thresholds[ti];
|
||
// Center thresholds slightly stronger
|
||
const mid = (thresholds.length - 1) / 2;
|
||
const closeness = 1 - Math.abs(ti - mid) / mid;
|
||
const a = 0.13 + closeness * 0.18;
|
||
ctx.strokeStyle = `rgba(${BLUE}, ${a})`;
|
||
ctx.lineWidth = closeness > 0.7 ? 0.95 : 0.55;
|
||
ctx.beginPath();
|
||
for (let j = 0; j < topoRows - 1; j++) {
|
||
for (let i = 0; i < topoCols - 1; i++) {
|
||
const tl = topoField[j * topoCols + i];
|
||
const tr = topoField[j * topoCols + i + 1];
|
||
const br = topoField[(j + 1) * topoCols + i + 1];
|
||
const bl = topoField[(j + 1) * topoCols + i];
|
||
let idx = 0;
|
||
if (tl > thr) idx |= 1;
|
||
if (tr > thr) idx |= 2;
|
||
if (br > thr) idx |= 4;
|
||
if (bl > thr) idx |= 8;
|
||
if (idx === 0 || idx === 15) continue;
|
||
const x = i * TOPO_RES, y = j * TOPO_RES, s = TOPO_RES;
|
||
// Linear-interpolated mid-edge crossings
|
||
const tx = x + s * (thr - tl) / (tr - tl);
|
||
const rx = x + s, ry = y + s * (thr - tr) / (br - tr);
|
||
const bx = x + s * (thr - bl) / (br - bl), by = y + s;
|
||
const lx = x, ly = y + s * (thr - tl) / (bl - tl);
|
||
switch (idx) {
|
||
case 1: case 14: ctx.moveTo(lx, ly); ctx.lineTo(tx, y); break;
|
||
case 2: case 13: ctx.moveTo(tx, y); ctx.lineTo(rx, ry); break;
|
||
case 3: case 12: ctx.moveTo(lx, ly); ctx.lineTo(rx, ry); break;
|
||
case 4: case 11: ctx.moveTo(rx, ry); ctx.lineTo(bx, by); break;
|
||
case 5:
|
||
ctx.moveTo(lx, ly); ctx.lineTo(tx, y);
|
||
ctx.moveTo(rx, ry); ctx.lineTo(bx, by); break;
|
||
case 6: case 9: ctx.moveTo(tx, y); ctx.lineTo(bx, by); break;
|
||
case 7: case 8: ctx.moveTo(lx, ly); ctx.lineTo(bx, by); break;
|
||
case 10:
|
||
ctx.moveTo(lx, ly); ctx.lineTo(bx, by);
|
||
ctx.moveTo(tx, y); ctx.lineTo(rx, ry); break;
|
||
}
|
||
}
|
||
}
|
||
ctx.stroke();
|
||
}
|
||
|
||
// Subtle elevation markers at field peaks (just a few small dots)
|
||
ctx.fillStyle = `rgba(${BLUE}, 0.55)`;
|
||
for (let j = 2; j < topoRows - 2; j += 4) {
|
||
for (let i = 2; i < topoCols - 2; i += 4) {
|
||
const v = topoField[j * topoCols + i];
|
||
if (v > 1.8) {
|
||
ctx.beginPath();
|
||
ctx.arc(i * TOPO_RES, j * TOPO_RES, 2, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ------- LOOP & PUBLIC API ------- */
|
||
function loop() {
|
||
if (!running) return;
|
||
const t = performance.now();
|
||
if (mode === 'constellation') {
|
||
updateConstellation(t);
|
||
drawConstellation();
|
||
} else if (mode === 'network') {
|
||
updateNetwork();
|
||
drawNetwork();
|
||
} else if (mode === 'flow') {
|
||
updateFlowfield(t);
|
||
drawFlowfield();
|
||
} else if (mode === 'topo') {
|
||
updateTopo(t);
|
||
drawTopo();
|
||
}
|
||
rafId = requestAnimationFrame(loop);
|
||
}
|
||
|
||
function setMode(m) {
|
||
mode = m;
|
||
const cnv2d = document.getElementById('hero-canvas');
|
||
const cnvGL = document.getElementById('hero-canvas-gl');
|
||
const hero = document.querySelector('.fa-hero');
|
||
|
||
// GL-driven modes
|
||
if (m === 'plasma' || m === 'mercury' || m === 'crystal') {
|
||
running = false;
|
||
cancelAnimationFrame(rafId); rafId = null;
|
||
ctx.clearRect(0, 0, W, H);
|
||
cnv2d.classList.add('hidden');
|
||
cnvGL.classList.add('active');
|
||
hero.classList.add('gl-active');
|
||
if (window.__faSetGLMode) window.__faSetGLMode(m);
|
||
return;
|
||
}
|
||
|
||
// 2D modes
|
||
cnv2d.classList.remove('hidden');
|
||
cnvGL.classList.remove('active');
|
||
hero.classList.remove('gl-active');
|
||
if (window.__faSetGLMode) window.__faSetGLMode('off');
|
||
|
||
if (m === 'off') {
|
||
running = false;
|
||
cancelAnimationFrame(rafId);
|
||
rafId = null;
|
||
ctx.clearRect(0, 0, W, H);
|
||
cnv2d.classList.add('hidden');
|
||
return;
|
||
}
|
||
if (m === 'constellation') initConstellation();
|
||
else if (m === 'network') initNetwork();
|
||
else if (m === 'flow') initFlowfield();
|
||
else if (m === 'topo') initTopo();
|
||
running = true;
|
||
cancelAnimationFrame(rafId);
|
||
rafId = null;
|
||
loop();
|
||
}
|
||
window.__faSetCanvasMode = setMode;
|
||
|
||
window.addEventListener('resize', () => {
|
||
resize();
|
||
if (mode === 'constellation') initConstellation();
|
||
else if (mode === 'network') initNetwork();
|
||
else if (mode === 'flow') initFlowfield();
|
||
else if (mode === 'topo') initTopo();
|
||
});
|
||
canvas.parentElement.addEventListener('mousemove', e => {
|
||
const rect = canvas.getBoundingClientRect();
|
||
mouse.x = e.clientX - rect.left;
|
||
mouse.y = e.clientY - rect.top;
|
||
});
|
||
canvas.parentElement.addEventListener('mouseleave', () => {
|
||
mouse.x = -9999; mouse.y = -9999;
|
||
});
|
||
|
||
resize();
|
||
initConstellation();
|
||
loop();
|
||
})();
|
||
|
||
/* ============================================================
|
||
HERO WEBGL — Plasma + Mercury, brand-tuned
|
||
Renders into a SECOND canvas overlaid on the 2D one.
|
||
Outputs RGBA (alpha-blended) so effects sit over the white hero.
|
||
============================================================ */
|
||
(function() {
|
||
const canvas = document.getElementById('hero-canvas-gl');
|
||
const gl = canvas.getContext('webgl', { antialias: false, alpha: true, premultipliedAlpha: false });
|
||
if (!gl) { window.__faSetGLMode = () => {}; return; }
|
||
|
||
const VS_SRC = `
|
||
attribute vec2 a_pos;
|
||
void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }
|
||
`;
|
||
|
||
const FRAG_HEADER = `
|
||
precision highp float;
|
||
uniform vec2 uRes;
|
||
uniform float uTime;
|
||
uniform vec2 uMouse;
|
||
|
||
float hash21(vec2 p) {
|
||
p = fract(p * vec2(123.34, 456.21));
|
||
p += dot(p, p + 45.32);
|
||
return fract(p.x * p.y);
|
||
}
|
||
float vnoise(vec2 p) {
|
||
vec2 i = floor(p), f = fract(p);
|
||
vec2 u = f*f*(3.0-2.0*f);
|
||
float a = hash21(i);
|
||
float b = hash21(i + vec2(1.0, 0.0));
|
||
float c = hash21(i + vec2(0.0, 1.0));
|
||
float d = hash21(i + vec2(1.0, 1.0));
|
||
return mix(mix(a,b,u.x), mix(c,d,u.x), u.y);
|
||
}
|
||
float fbm(vec2 p) {
|
||
float v = 0.0, a = 0.5;
|
||
mat2 r = mat2(0.8, -0.6, 0.6, 0.8);
|
||
for (int i = 0; i < 4; i++) { v += a * vnoise(p); p = r * p * 2.02; a *= 0.5; }
|
||
return v;
|
||
}
|
||
`;
|
||
|
||
// ── PLASMA HERO (navy/blue glow following cursor over white) ──
|
||
const FS_PLASMA = `
|
||
void main() {
|
||
vec2 frag = gl_FragCoord.xy;
|
||
float scale = min(uRes.x, uRes.y);
|
||
vec2 uv = (frag - 0.5 * uRes) / scale;
|
||
vec2 m = (uMouse - 0.5 * uRes) / scale;
|
||
|
||
// Orb anchored softly to cursor (drift to center if cursor leaves)
|
||
vec2 center = m * 0.55;
|
||
vec2 p = uv - center;
|
||
float r = length(p);
|
||
float a = atan(p.y, p.x);
|
||
|
||
// Corona: polar-domain warped noise
|
||
float corona = 0.0;
|
||
for (int i = 0; i < 3; i++) {
|
||
float fi = float(i);
|
||
corona += fbm(vec2(a * (3.0 + fi) + uTime * (0.18 + fi * 0.07),
|
||
r * 4.5 - uTime * 0.25)) / (fi + 1.0);
|
||
}
|
||
corona /= 1.6;
|
||
|
||
float core = exp(-pow(r * 4.0, 2.0));
|
||
float outer = exp(-r * 2.8) * (0.55 + 0.5 * corona);
|
||
|
||
// Solar arc filaments (subtle)
|
||
float fil = smoothstep(0.6, 0.92, fbm(vec2(a * 6.0, r * 7.0 + uTime * 0.4)));
|
||
fil *= smoothstep(0.08, 0.3, r) * smoothstep(0.7, 0.25, r);
|
||
|
||
// Brand palette
|
||
vec3 navy = vec3(0.102, 0.122, 0.361);
|
||
vec3 blue = vec3(0.420, 0.561, 0.831);
|
||
vec3 lblue = vec3(0.659, 0.753, 0.910);
|
||
|
||
// Color: deep navy at core, blue corona, light-blue filaments
|
||
vec3 col = mix(blue, navy, smoothstep(0.0, 0.25, r));
|
||
col = mix(col, lblue, fil);
|
||
|
||
// Intensity controls alpha (so the white hero shows through)
|
||
float a1 = core * 0.65 + outer * 0.55 + fil * 0.45;
|
||
a1 = clamp(a1, 0.0, 0.85);
|
||
|
||
gl_FragColor = vec4(col, a1);
|
||
}
|
||
`;
|
||
|
||
// ── CRYSTAL HERO (voronoi lattice tuned to brand on white) ──
|
||
const FS_CRYSTAL = `
|
||
vec2 hash22b(vec2 p) {
|
||
vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));
|
||
p3 += dot(p3, p3.yzx + 33.33);
|
||
return fract((p3.xx + p3.yz) * p3.zy);
|
||
}
|
||
vec3 voronoiB(vec2 p) {
|
||
vec2 i = floor(p), f = fract(p);
|
||
float d1 = 1e9, d2 = 1e9;
|
||
vec2 ic = vec2(0.0);
|
||
for (int y = -1; y <= 1; y++) {
|
||
for (int x = -1; x <= 1; x++) {
|
||
vec2 g = vec2(float(x), float(y));
|
||
vec2 o = hash22b(i + g);
|
||
o = 0.5 + 0.5 * sin(uTime * 0.5 + 6.28 * o);
|
||
vec2 r = g + o - f;
|
||
float d = dot(r, r);
|
||
if (d < d1) { d2 = d1; d1 = d; ic = i + g; }
|
||
else if (d < d2) { d2 = d; }
|
||
}
|
||
}
|
||
return vec3(sqrt(d1), sqrt(d2), hash21(ic));
|
||
}
|
||
|
||
void main() {
|
||
vec2 frag = gl_FragCoord.xy;
|
||
float scale = min(uRes.x, uRes.y);
|
||
vec2 uv = frag / scale;
|
||
vec2 m = uMouse / scale;
|
||
|
||
vec2 d = uv - m;
|
||
float warp = 0.45 * exp(-length(d) * 2.2);
|
||
vec2 uvw = uv + d * warp;
|
||
|
||
vec3 vd = voronoiB(uvw * 3.5);
|
||
float d1 = vd.x, d2 = vd.y, cellId = vd.z;
|
||
float edge = d2 - d1;
|
||
|
||
vec3 navy = vec3(0.102, 0.122, 0.361);
|
||
vec3 blue = vec3(0.420, 0.561, 0.831);
|
||
vec3 lblue = vec3(0.659, 0.753, 0.910);
|
||
vec3 pale = vec3(0.85, 0.90, 0.97);
|
||
|
||
vec3 fill = mix(pale, lblue, cellId * 0.7);
|
||
float boundary = smoothstep(0.0, 0.35, d1);
|
||
fill = mix(fill, blue, boundary * 0.55);
|
||
|
||
float edgeMask = 1.0 - smoothstep(0.0, 0.035, edge);
|
||
vec3 edgeCol = mix(navy, blue, sin(uTime * 0.3 + cellId * 8.0) * 0.5 + 0.5);
|
||
|
||
vec3 mvd = voronoiB(m * 3.5);
|
||
float hovered = step(abs(mvd.z - cellId), 0.001);
|
||
|
||
float caustic = smoothstep(0.5, 0.95, sin(d1 * 28.0 + uTime + cellId * 14.0));
|
||
caustic *= (1.0 - edgeMask) * 0.3;
|
||
|
||
vec3 col = fill + edgeCol * edgeMask * 1.0 + lblue * caustic;
|
||
col = mix(col, mix(blue, lblue, 0.4), hovered * (1.0 - edgeMask) * 0.45);
|
||
|
||
float fillAlpha = 0.18 + boundary * 0.18 + hovered * 0.12;
|
||
float a1 = max(fillAlpha, edgeMask * 0.85);
|
||
a1 = clamp(a1, 0.0, 0.85);
|
||
|
||
gl_FragColor = vec4(col, a1);
|
||
}
|
||
`;
|
||
|
||
// ── MERCURY HERO (navy/blue metaballs over white) ──
|
||
const FS_MERCURY = `
|
||
float metaball(vec2 p, vec2 c, float r) {
|
||
return r / max(length(p - c), 0.0001);
|
||
}
|
||
void main() {
|
||
vec2 frag = gl_FragCoord.xy;
|
||
float scale = min(uRes.x, uRes.y);
|
||
vec2 uv = (frag - 0.5 * uRes) / scale;
|
||
vec2 m = (uMouse - 0.5 * uRes) / scale;
|
||
|
||
// Six drifting metaballs + one tied to cursor + one mirror
|
||
vec2 b0 = vec2(sin(uTime * 0.27) * 0.42, cos(uTime * 0.23) * 0.28);
|
||
vec2 b1 = vec2(cos(uTime * 0.35) * 0.48, sin(uTime * 0.21) * 0.26);
|
||
vec2 b2 = vec2(sin(uTime * 0.19 + 2.0) * 0.5, cos(uTime * 0.31 + 1.0) * 0.36);
|
||
vec2 b3 = vec2(cos(uTime * 0.25 + 4.0) * 0.36, sin(uTime * 0.17 + 3.0) * 0.44);
|
||
vec2 b4 = m;
|
||
vec2 b5 = -m * 0.4;
|
||
|
||
float f = 0.0;
|
||
f += metaball(uv, b0, 0.16);
|
||
f += metaball(uv, b1, 0.18);
|
||
f += metaball(uv, b2, 0.13);
|
||
f += metaball(uv, b3, 0.15);
|
||
f += metaball(uv, b4, 0.22); // cursor blob — biggest
|
||
f += metaball(uv, b5, 0.10);
|
||
|
||
float surface = smoothstep(2.3, 2.85, f);
|
||
float halo = smoothstep(1.6, 2.3, f) - surface;
|
||
|
||
// Normal approximation via finite difference
|
||
vec2 e = vec2(0.004, 0.0);
|
||
float fx = (
|
||
metaball(uv + e.xy, b0, 0.16) + metaball(uv + e.xy, b1, 0.18)
|
||
+ metaball(uv + e.xy, b2, 0.13) + metaball(uv + e.xy, b3, 0.15)
|
||
+ metaball(uv + e.xy, b4, 0.22) + metaball(uv + e.xy, b5, 0.10)
|
||
) - f;
|
||
float fy = (
|
||
metaball(uv + e.yx, b0, 0.16) + metaball(uv + e.yx, b1, 0.18)
|
||
+ metaball(uv + e.yx, b2, 0.13) + metaball(uv + e.yx, b3, 0.15)
|
||
+ metaball(uv + e.yx, b4, 0.22) + metaball(uv + e.yx, b5, 0.10)
|
||
) - f;
|
||
vec2 n = normalize(vec2(fx, fy) + vec2(1e-5));
|
||
|
||
// Brand palette
|
||
vec3 navy = vec3(0.102, 0.122, 0.361);
|
||
vec3 blue = vec3(0.420, 0.561, 0.831);
|
||
vec3 lblue = vec3(0.659, 0.753, 0.910);
|
||
vec3 pale = vec3(0.93, 0.95, 0.99);
|
||
|
||
// Iridescence: navy → blue → light-blue based on normal angle
|
||
float ang = atan(n.y, n.x);
|
||
float t1 = sin(ang * 2.0 + uTime * 0.5) * 0.5 + 0.5;
|
||
float t2 = sin(ang * 3.0 - uTime * 0.4) * 0.5 + 0.5;
|
||
vec3 surfCol = mix(navy, blue, t1);
|
||
surfCol = mix(surfCol, lblue, t2 * 0.55);
|
||
|
||
// Fresnel-ish edge highlight
|
||
float fres = pow(1.0 - abs(n.y), 1.6);
|
||
surfCol = mix(surfCol, pale, fres * 0.5);
|
||
|
||
// Spec highlight
|
||
float spec = pow(max(dot(n, normalize(vec2(0.5, 0.8))), 0.0), 18.0);
|
||
surfCol += vec3(spec) * 0.4;
|
||
|
||
// Edge ring (subtle blue glow around the merge surface)
|
||
vec3 haloCol = mix(blue, lblue, 0.6);
|
||
|
||
// Composition: opaque over white where surface is solid; soft halo around
|
||
vec3 col = mix(haloCol, surfCol, surface);
|
||
float a1 = surface * 0.92 + halo * 0.35;
|
||
a1 = clamp(a1, 0.0, 0.95);
|
||
|
||
gl_FragColor = vec4(col, a1);
|
||
}
|
||
`;
|
||
|
||
function compile(type, src) {
|
||
const s = gl.createShader(type);
|
||
gl.shaderSource(s, src);
|
||
gl.compileShader(s);
|
||
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
|
||
console.error('GL compile:', gl.getShaderInfoLog(s));
|
||
return null;
|
||
}
|
||
return s;
|
||
}
|
||
function makeProgram(fsSrc) {
|
||
const vs = compile(gl.VERTEX_SHADER, VS_SRC);
|
||
const fs = compile(gl.FRAGMENT_SHADER, FRAG_HEADER + '\n' + fsSrc);
|
||
if (!vs || !fs) return null;
|
||
const p = gl.createProgram();
|
||
gl.attachShader(p, vs); gl.attachShader(p, fs);
|
||
gl.linkProgram(p);
|
||
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
|
||
console.error('GL link:', gl.getProgramInfoLog(p));
|
||
return null;
|
||
}
|
||
return p;
|
||
}
|
||
|
||
const buf = gl.createBuffer();
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
||
-1,-1, 1,-1, -1,1, -1,1, 1,-1, 1,1
|
||
]), gl.STATIC_DRAW);
|
||
|
||
const programs = {};
|
||
function getProgram(name) {
|
||
if (programs[name]) return programs[name];
|
||
let src;
|
||
if (name === 'plasma') src = FS_PLASMA;
|
||
else if (name === 'mercury') src = FS_MERCURY;
|
||
else if (name === 'crystal') src = FS_CRYSTAL;
|
||
else return null;
|
||
const p = makeProgram(src);
|
||
if (!p) return null;
|
||
programs[name] = {
|
||
prog: p,
|
||
uRes: gl.getUniformLocation(p, 'uRes'),
|
||
uTime: gl.getUniformLocation(p, 'uTime'),
|
||
uMouse: gl.getUniformLocation(p, 'uMouse'),
|
||
a_pos: gl.getAttribLocation(p, 'a_pos'),
|
||
};
|
||
return programs[name];
|
||
}
|
||
|
||
gl.enable(gl.BLEND);
|
||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||
|
||
let mode = 'off';
|
||
let running = false;
|
||
let rafId = null;
|
||
|
||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||
const mouse = { x: -9999, y: -9999 };
|
||
const mouseSmooth = { x: 0, y: 0 };
|
||
|
||
function resize() {
|
||
const rect = canvas.parentElement.getBoundingClientRect();
|
||
const w = Math.floor(rect.width * dpr);
|
||
const h = Math.floor(rect.height * dpr);
|
||
if (canvas.width !== w || canvas.height !== h) {
|
||
canvas.width = w; canvas.height = h;
|
||
canvas.style.width = rect.width + 'px';
|
||
canvas.style.height = rect.height + 'px';
|
||
gl.viewport(0, 0, w, h);
|
||
}
|
||
}
|
||
|
||
canvas.parentElement.addEventListener('mousemove', e => {
|
||
const rect = canvas.getBoundingClientRect();
|
||
mouse.x = (e.clientX - rect.left) * dpr;
|
||
// GL origin is bottom-left
|
||
mouse.y = (rect.height - (e.clientY - rect.top)) * dpr;
|
||
});
|
||
canvas.parentElement.addEventListener('mouseleave', () => {
|
||
mouse.x = -9999; mouse.y = -9999;
|
||
});
|
||
// Initialize smooth target to center so first frame isn't off-screen
|
||
function initCenterMouse() {
|
||
const rect = canvas.parentElement.getBoundingClientRect();
|
||
if (mouse.x < -9000) {
|
||
mouseSmooth.x = (rect.width / 2) * dpr;
|
||
mouseSmooth.y = (rect.height / 2) * dpr;
|
||
}
|
||
}
|
||
|
||
const startMs = performance.now();
|
||
let lastFrame = startMs;
|
||
|
||
function loop() {
|
||
if (!running) return;
|
||
resize();
|
||
const now = performance.now();
|
||
const dt = (now - lastFrame) / 1000;
|
||
lastFrame = now;
|
||
const t = (now - startMs) / 1000;
|
||
|
||
// Smooth mouse (or drift toward center when out)
|
||
const target = mouse.x < -9000
|
||
? { x: canvas.width / 2, y: canvas.height / 2 }
|
||
: mouse;
|
||
mouseSmooth.x += (target.x - mouseSmooth.x) * Math.min(1, dt * 5);
|
||
mouseSmooth.y += (target.y - mouseSmooth.y) * Math.min(1, dt * 5);
|
||
|
||
const p = getProgram(mode);
|
||
if (!p) { rafId = requestAnimationFrame(loop); return; }
|
||
|
||
gl.useProgram(p.prog);
|
||
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||
gl.enableVertexAttribArray(p.a_pos);
|
||
gl.vertexAttribPointer(p.a_pos, 2, gl.FLOAT, false, 0, 0);
|
||
gl.uniform2f(p.uRes, canvas.width, canvas.height);
|
||
gl.uniform1f(p.uTime, t);
|
||
gl.uniform2f(p.uMouse, mouseSmooth.x, mouseSmooth.y);
|
||
|
||
gl.clearColor(0, 0, 0, 0);
|
||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||
|
||
rafId = requestAnimationFrame(loop);
|
||
}
|
||
|
||
function setGLMode(m) {
|
||
if (m === mode) return;
|
||
mode = m;
|
||
if (m === 'off') {
|
||
running = false;
|
||
cancelAnimationFrame(rafId); rafId = null;
|
||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||
return;
|
||
}
|
||
if (m === 'plasma' || m === 'mercury' || m === 'crystal') {
|
||
getProgram(m); // compile if needed
|
||
initCenterMouse();
|
||
if (!running) {
|
||
running = true;
|
||
lastFrame = performance.now();
|
||
cancelAnimationFrame(rafId);
|
||
loop();
|
||
}
|
||
}
|
||
}
|
||
window.__faSetGLMode = setGLMode;
|
||
|
||
window.addEventListener('resize', resize);
|
||
resize();
|
||
})();
|
||
|
||
/* ============================================================
|
||
TWEAKS PANEL
|
||
============================================================ */
|
||
(function() {
|
||
const DEFAULTS = /*EDITMODE-BEGIN*/{
|
||
"canvasMode": "mercury",
|
||
"visualMode": "none",
|
||
"emphasis": "underline"
|
||
}/*EDITMODE-END*/;
|
||
|
||
const state = { ...DEFAULTS };
|
||
|
||
// Panel
|
||
const panel = document.createElement('div');
|
||
panel.id = 'fa-tweaks';
|
||
panel.style.cssText = `
|
||
position: fixed; bottom: 24px; right: 24px; z-index: 9999;
|
||
width: 280px; background: #fff; color: #1A1F5C;
|
||
border: 0.5px solid rgba(26,31,92,0.15);
|
||
box-shadow: 0 20px 60px -20px rgba(26,31,92,0.35), 0 4px 12px -4px rgba(26,31,92,0.1);
|
||
font-family: 'DM Sans', sans-serif;
|
||
display: none;
|
||
`;
|
||
panel.innerHTML = `
|
||
<div style="display:flex; align-items:center; justify-content:space-between; padding:14px 18px; border-bottom:0.5px solid rgba(26,31,92,0.1); cursor:move;" id="fa-tweaks-head">
|
||
<div style="font-family:'Syne',sans-serif; font-size:11px; font-weight:700; letter-spacing:2.5px; text-transform:uppercase; color:#1A1F5C;">Tweaks</div>
|
||
<button id="fa-tweaks-close" style="background:none; border:none; cursor:pointer; font-size:18px; line-height:1; color:#6B7280; padding:0;">×</button>
|
||
</div>
|
||
<div style="padding: 16px 18px; display:flex; flex-direction:column; gap:18px;">
|
||
<div>
|
||
<div style="font-size:10px; font-weight:500; letter-spacing:2px; text-transform:uppercase; color:#6B7280; margin-bottom:8px;">Hero Canvas</div>
|
||
<div class="fa-seg" data-key="canvasMode" style="display:grid; grid-template-columns:1fr 1fr; gap:1px; background:rgba(26,31,92,0.1); border:0.5px solid rgba(26,31,92,0.1);">
|
||
<button data-v="constellation">Constellation</button>
|
||
<button data-v="network">Network</button>
|
||
<button data-v="flow">Flow field</button>
|
||
<button data-v="topo">Topografie</button>
|
||
<button data-v="plasma">Plasma</button>
|
||
<button data-v="mercury">Mercury</button>
|
||
<button data-v="crystal" style="grid-column: span 2;">Crystal</button>
|
||
<button data-v="off" style="grid-column: span 2;">Off</button>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="font-size:10px; font-weight:500; letter-spacing:2px; text-transform:uppercase; color:#6B7280; margin-bottom:8px;">Right Panel</div>
|
||
<div class="fa-seg" data-key="visualMode" style="display:grid; grid-template-columns:1fr 1fr 1fr; gap:1px; background:rgba(26,31,92,0.1); border:0.5px solid rgba(26,31,92,0.1);">
|
||
<button data-v="stack">Stack</button>
|
||
<button data-v="stats">Cards</button>
|
||
<button data-v="none">Keine</button>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="font-size:10px; font-weight:500; letter-spacing:2px; text-transform:uppercase; color:#6B7280; margin-bottom:8px;">Headline Emphasis</div>
|
||
<div class="fa-seg" data-key="emphasis" style="display:grid; grid-template-columns:1fr 1fr 1fr; gap:1px; background:rgba(26,31,92,0.1); border:0.5px solid rgba(26,31,92,0.1);">
|
||
<button data-v="underline">Blue+line</button>
|
||
<button data-v="blue">Blue</button>
|
||
<button data-v="flat">All navy</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<style>
|
||
#fa-tweaks .fa-seg button {
|
||
background: #fff; border: none; padding: 9px 6px;
|
||
font-family: 'DM Sans', sans-serif; font-size: 11px; font-weight: 500;
|
||
color: #6B7280; cursor: pointer; transition: all 0.15s;
|
||
letter-spacing: 0.3px;
|
||
}
|
||
#fa-tweaks .fa-seg button:hover { color: #1A1F5C; }
|
||
#fa-tweaks .fa-seg button.active { background: #1A1F5C; color: #fff; }
|
||
</style>
|
||
`;
|
||
document.body.appendChild(panel);
|
||
|
||
function apply() {
|
||
// Canvas
|
||
if (window.__faSetCanvasMode) window.__faSetCanvasMode(state.canvasMode);
|
||
// Visual
|
||
const cap = document.getElementById('fa-cap-card');
|
||
const stats = document.getElementById('fa-stat-stack');
|
||
const visual = document.getElementById('fa-hero-visual');
|
||
const hero = document.querySelector('.fa-hero');
|
||
hero.classList.remove('no-visual');
|
||
if (state.visualMode === 'stack') {
|
||
cap.style.display = 'block';
|
||
stats.style.display = 'none';
|
||
visual.style.display = 'flex';
|
||
visual.classList.remove('legacy');
|
||
} else if (state.visualMode === 'stats') {
|
||
cap.style.display = 'none';
|
||
stats.style.display = 'flex';
|
||
visual.style.display = 'flex';
|
||
visual.classList.add('legacy');
|
||
} else { // none
|
||
visual.style.display = 'none';
|
||
hero.classList.add('no-visual');
|
||
}
|
||
// Trigger canvas resize so it spans the new hero width
|
||
window.dispatchEvent(new Event('resize'));
|
||
// Emphasis
|
||
const h1 = document.getElementById('fa-headline');
|
||
const em = h1.querySelector('em');
|
||
h1.classList.remove('flat');
|
||
em.classList.remove('underline');
|
||
if (state.emphasis === 'underline') em.classList.add('underline');
|
||
else if (state.emphasis === 'flat') h1.classList.add('flat');
|
||
// Highlight active buttons
|
||
panel.querySelectorAll('.fa-seg').forEach(seg => {
|
||
const key = seg.dataset.key;
|
||
seg.querySelectorAll('button').forEach(b => {
|
||
b.classList.toggle('active', b.dataset.v === state[key]);
|
||
});
|
||
});
|
||
}
|
||
|
||
panel.querySelectorAll('.fa-seg button').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const seg = btn.closest('.fa-seg');
|
||
const key = seg.dataset.key;
|
||
state[key] = btn.dataset.v;
|
||
apply();
|
||
try {
|
||
window.parent.postMessage({ type: '__edit_mode_set_keys', edits: { [key]: btn.dataset.v } }, '*');
|
||
} catch(e) {}
|
||
});
|
||
});
|
||
|
||
// Close button
|
||
document.getElementById('fa-tweaks-close').addEventListener('click', () => {
|
||
panel.style.display = 'none';
|
||
try { window.parent.postMessage({ type: '__edit_mode_dismissed' }, '*'); } catch(e){}
|
||
});
|
||
|
||
// Drag
|
||
(function makeDraggable() {
|
||
const head = document.getElementById('fa-tweaks-head');
|
||
let dragging = false, sx = 0, sy = 0, px = 0, py = 0;
|
||
head.addEventListener('mousedown', e => {
|
||
if (e.target.id === 'fa-tweaks-close') return;
|
||
dragging = true;
|
||
sx = e.clientX; sy = e.clientY;
|
||
const r = panel.getBoundingClientRect();
|
||
px = r.left; py = r.top;
|
||
panel.style.right = 'auto'; panel.style.bottom = 'auto';
|
||
e.preventDefault();
|
||
});
|
||
window.addEventListener('mousemove', e => {
|
||
if (!dragging) return;
|
||
panel.style.left = (px + e.clientX - sx) + 'px';
|
||
panel.style.top = (py + e.clientY - sy) + 'px';
|
||
});
|
||
window.addEventListener('mouseup', () => dragging = false);
|
||
})();
|
||
|
||
// Edit-mode protocol
|
||
window.addEventListener('message', e => {
|
||
if (!e.data) return;
|
||
if (e.data.type === '__activate_edit_mode') panel.style.display = 'block';
|
||
if (e.data.type === '__deactivate_edit_mode') panel.style.display = 'none';
|
||
});
|
||
try { window.parent.postMessage({ type: '__edit_mode_available' }, '*'); } catch(e){}
|
||
|
||
// Apply initial state on load
|
||
// (canvas already initialized; this aligns visual + emphasis)
|
||
apply();
|
||
})();
|
||
|
||
/* ============================================================
|
||
SCROLL REVEAL — IntersectionObserver
|
||
============================================================ */
|
||
(function() {
|
||
const observer = new IntersectionObserver(function(entries) {
|
||
entries.forEach(function(entry) {
|
||
if (entry.isIntersecting) {
|
||
entry.target.classList.add('is-visible');
|
||
observer.unobserve(entry.target);
|
||
}
|
||
});
|
||
}, { threshold: 0.1, rootMargin: '0px 0px -40px 0px' });
|
||
|
||
document.querySelectorAll('[data-reveal]').forEach(function(el) {
|
||
observer.observe(el);
|
||
});
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|