feat(landing): update cyber hero visuals and theme sync

This commit is contained in:
777genius 2026-05-17 23:49:32 +03:00
parent 50d4b6b0ca
commit 9eff462422
33 changed files with 1151 additions and 320 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

View file

@ -14,17 +14,218 @@
--cyber-muted: #9ba8c7;
--cyber-border-cyan: rgba(0, 234, 255, 0.42);
--cyber-border-magenta: rgba(255, 43, 255, 0.42);
--cyber-panel-bg:
linear-gradient(135deg, rgba(5, 14, 31, 0.9), rgba(3, 10, 22, 0.64));
--cyber-panel-shadow:
0 0 0 1px rgba(47, 125, 255, 0.12) inset,
0 0 24px rgba(0, 234, 255, 0.12);
--cyber-hero-bg:
radial-gradient(circle at 76% 30%, rgba(0, 234, 255, 0.14), transparent 30%),
radial-gradient(circle at 86% 70%, rgba(255, 43, 255, 0.14), transparent 34%),
linear-gradient(180deg, var(--cyber-bg-0), var(--cyber-bg-1) 58%, var(--cyber-bg-0));
--cyber-monterey-bg:
radial-gradient(circle at 76% 22%, rgba(138, 47, 255, 0.76), transparent 30%),
radial-gradient(circle at 18% 76%, rgba(37, 8, 128, 0.72), transparent 36%),
linear-gradient(180deg, #180061 0%, #3200a2 46%, #130042 100%);
--cyber-monterey-before-bg:
radial-gradient(circle at 18% 34%, rgba(2, 5, 13, 0.62), rgba(2, 5, 13, 0.14) 34%, transparent 62%),
linear-gradient(90deg, rgba(2, 5, 13, 0.48) 0%, rgba(2, 5, 13, 0.17) 42%, rgba(2, 5, 13, 0.04) 64%, rgba(2, 5, 13, 0.3) 100%);
--cyber-monterey-after-bg:
linear-gradient(180deg, rgba(2, 5, 13, 0.92) 0%, rgba(2, 5, 13, 0.62) 15%, rgba(2, 5, 13, 0.08) 44%, rgba(2, 5, 13, 0.68) 100%),
radial-gradient(circle at 64% 42%, transparent 0 26%, rgba(2, 5, 13, 0.24) 70%, rgba(2, 5, 13, 0.54) 100%);
--cyber-monterey-canvas-opacity: 1;
--cyber-monterey-canvas-filter: blur(4px) saturate(1.22) brightness(1.08) contrast(1.08);
--cyber-monterey-canvas-blend: normal;
--cyber-background-bg:
radial-gradient(circle at 72% 28%, rgba(0, 234, 255, 0.1), transparent 30%),
radial-gradient(circle at 88% 62%, rgba(255, 43, 255, 0.1), transparent 32%),
linear-gradient(90deg, transparent 0 64px, rgba(0, 234, 255, 0.045) 65px 66px, transparent 67px 160px),
linear-gradient(180deg, rgba(2, 5, 13, 0.66) 0%, rgba(2, 5, 13, 0.2) 38%, rgba(2, 5, 13, 0.78) 100%);
--cyber-background-opacity: 0.58;
--cyber-background-before-bg:
linear-gradient(90deg, transparent 0 8%, rgba(0, 234, 255, 0.14) 8.1% 8.22%, transparent 8.34% 18%, rgba(255, 43, 255, 0.12) 18.1% 18.22%, transparent 18.34% 31%, rgba(0, 234, 255, 0.11) 31.1% 31.24%, transparent 31.36% 44%, rgba(47, 125, 255, 0.12) 44.1% 44.2%, transparent 44.34% 62%, rgba(255, 43, 255, 0.1) 62.1% 62.22%, transparent 62.34% 78%, rgba(0, 234, 255, 0.12) 78.1% 78.22%, transparent 78.34%),
repeating-linear-gradient(90deg, transparent 0 78px, rgba(0, 234, 255, 0.05) 80px 82px, transparent 84px 116px),
linear-gradient(to top, rgba(3, 12, 27, 0.66) 0%, rgba(3, 12, 27, 0.42) 8%, rgba(3, 12, 27, 0.16) 17%, transparent 28%),
linear-gradient(to top, rgba(5, 20, 44, 0.5) 0%, rgba(5, 20, 44, 0.28) 12%, rgba(5, 20, 44, 0.12) 24%, transparent 36%),
linear-gradient(to top, rgba(4, 17, 38, 0.38) 0%, rgba(4, 17, 38, 0.22) 18%, rgba(4, 17, 38, 0.08) 34%, transparent 48%);
--cyber-background-before-opacity: 0.62;
--cyber-background-before-blend: screen;
--cyber-background-after-bg:
repeating-linear-gradient(90deg, transparent 0 34px, rgba(0, 234, 255, 0.12) 35px 36px, transparent 37px 110px),
repeating-linear-gradient(180deg, transparent 0 28px, rgba(255, 43, 255, 0.1) 29px 30px, transparent 31px 78px),
linear-gradient(90deg, transparent, rgba(0, 234, 255, 0.08), transparent);
--cyber-background-after-opacity: 0.26;
--cyber-background-after-blend: screen;
--cyber-wash-bg:
radial-gradient(circle at 18% 44%, rgba(2, 5, 13, 0.48), rgba(2, 5, 13, 0.22) 34%, transparent 58%),
linear-gradient(90deg, rgba(2, 5, 13, 0.42) 0%, rgba(2, 5, 13, 0.28) 34%, rgba(2, 5, 13, 0.08) 66%, rgba(2, 5, 13, 0.3) 100%),
linear-gradient(180deg, rgba(2, 5, 13, 0.18), rgba(2, 5, 13, 0.08) 58%, rgba(2, 5, 13, 0.92));
--cyber-gridlines-bg:
linear-gradient(rgba(0, 234, 255, 0.055) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 234, 255, 0.045) 1px, transparent 1px);
--cyber-gridlines-opacity: 0.16;
--cyber-scanlines-bg: repeating-linear-gradient(
to bottom,
rgba(255, 255, 255, 0.08) 0,
rgba(255, 255, 255, 0.08) 1px,
transparent 1px,
transparent 4px
);
--cyber-scanlines-opacity: 0.11;
--cyber-copy-aura: radial-gradient(circle at 28% 38%, rgba(2, 5, 13, 0.82), rgba(2, 5, 13, 0.36) 62%, transparent 78%);
--cyber-title-color: rgba(244, 247, 255, 0.96);
--cyber-description-color: rgba(222, 229, 255, 0.84);
--cyber-action-primary-color: var(--cyber-bg-0);
--cyber-action-secondary-bg: rgba(3, 10, 22, 0.56);
--cyber-action-secondary-hover-bg: rgba(0, 234, 255, 0.08);
--cyber-release-color: rgba(244, 247, 255, 0.62);
--cyber-scene-floor-bg:
radial-gradient(ellipse at 58% 84%, rgba(255, 43, 255, 0.24), transparent 18%),
radial-gradient(ellipse at 56% 84%, rgba(0, 234, 255, 0.18), transparent 32%),
repeating-radial-gradient(ellipse at 58% 84%, rgba(0, 234, 255, 0.1) 0 1px, transparent 1px 20px);
--cyber-scene-floor-opacity: 0.48;
--cyber-scene-foreground-bg:
linear-gradient(90deg, transparent 0 4%, rgba(0, 234, 255, 0.08) 4.1%, transparent 4.4%),
linear-gradient(180deg, transparent 0 88%, rgba(255, 43, 255, 0.08));
--cyber-scene-foreground-opacity: 0.72;
--cyber-video-frame-bg: rgba(2, 6, 16, 0.82);
--cyber-video-content-bg: rgba(2, 6, 16, 0.94);
--cyber-card-text: rgba(244, 247, 255, 0.84);
--cyber-card-muted: rgba(222, 229, 255, 0.72);
--cyber-card-subtle: rgba(222, 229, 255, 0.62);
--cyber-card-inset: rgba(255, 255, 255, 0.06);
--cyber-feature-shell-bg:
radial-gradient(ellipse at 50% 62%, rgba(0, 234, 255, 0.08), transparent 56%),
radial-gradient(ellipse at 78% 52%, rgba(255, 43, 255, 0.08), transparent 48%),
linear-gradient(180deg, transparent 0%, rgba(5, 17, 40, 0.08) 30%, rgba(5, 14, 31, 0.18) 52%, rgba(5, 17, 40, 0.08) 72%, transparent 100%);
--cyber-feature-rail-bg:
linear-gradient(180deg, rgba(3, 10, 22, 0.34) 0%, rgba(5, 14, 31, 0.74) 30%, rgba(5, 14, 31, 0.72) 70%, rgba(3, 10, 22, 0.34) 100%),
linear-gradient(135deg, rgba(5, 14, 31, 0.58), rgba(3, 10, 22, 0.42));
--cyber-feature-rail-shadow:
0 0 0 1px rgba(47, 125, 255, 0.1) inset,
0 -28px 46px rgba(2, 5, 13, 0.14),
0 30px 58px rgba(2, 5, 13, 0.18),
0 0 24px rgba(0, 234, 255, 0.1);
--cyber-feature-divider: rgba(0, 234, 255, 0.16);
--cyber-feature-title: rgba(244, 247, 255, 0.94);
--cyber-feature-text: rgba(222, 229, 255, 0.62);
--cyber-radius-xs: 4px;
--cyber-radius-sm: 6px;
--cyber-radius-md: 8px;
--cyber-frame-cut: 18px;
}
.v-theme--light .cyber-hero {
--cyber-bg-0: #f8fcff;
--cyber-bg-1: #eaf7fb;
--cyber-panel-weak: rgba(255, 255, 255, 0.68);
--cyber-panel: rgba(255, 255, 255, 0.78);
--cyber-panel-strong: rgba(255, 255, 255, 0.92);
--cyber-cyan: #008fb3;
--cyber-blue: #2563eb;
--cyber-magenta: #b832d8;
--cyber-violet: #7c3aed;
--cyber-text: #132238;
--cyber-muted: #596b83;
--cyber-border-cyan: rgba(8, 145, 178, 0.34);
--cyber-border-magenta: rgba(184, 50, 216, 0.28);
--cyber-panel-bg:
linear-gradient(135deg, rgba(255, 255, 255, 0.9), rgba(237, 249, 252, 0.68));
--cyber-panel-shadow:
0 0 0 1px rgba(8, 145, 178, 0.1) inset,
0 18px 48px rgba(8, 145, 178, 0.12),
0 0 22px rgba(184, 50, 216, 0.07);
--cyber-hero-bg:
radial-gradient(circle at 76% 30%, rgba(0, 178, 214, 0.14), transparent 32%),
radial-gradient(circle at 86% 70%, rgba(184, 50, 216, 0.13), transparent 36%),
linear-gradient(180deg, #fbfdff 0%, #edf8fb 56%, #f8fcff 100%);
--cyber-monterey-bg:
radial-gradient(circle at 78% 24%, rgba(113, 185, 255, 0.28), transparent 31%),
radial-gradient(circle at 22% 72%, rgba(221, 170, 255, 0.24), transparent 38%),
radial-gradient(circle at 8% 32%, rgba(101, 218, 255, 0.18), transparent 34%),
linear-gradient(180deg, #f8fcff 0%, #eaf7fb 48%, #fbf7ff 100%);
--cyber-monterey-before-bg:
radial-gradient(circle at 18% 34%, rgba(255, 255, 255, 0.46), rgba(255, 255, 255, 0.14) 34%, transparent 62%),
linear-gradient(90deg, rgba(255, 255, 255, 0.22) 0%, rgba(255, 255, 255, 0.08) 42%, rgba(255, 255, 255, 0.03) 64%, rgba(237, 247, 252, 0.2) 100%);
--cyber-monterey-after-bg:
linear-gradient(180deg, rgba(248, 252, 255, 0.52) 0%, rgba(248, 252, 255, 0.22) 18%, rgba(248, 252, 255, 0.04) 48%, rgba(248, 252, 255, 0.58) 100%),
radial-gradient(circle at 64% 42%, transparent 0 26%, rgba(255, 255, 255, 0.16) 70%, rgba(235, 247, 252, 0.42) 100%);
--cyber-monterey-canvas-opacity: 1;
--cyber-monterey-canvas-filter: blur(1px) saturate(1.34) brightness(1.12) contrast(1.16);
--cyber-monterey-canvas-blend: multiply;
--cyber-background-bg:
radial-gradient(circle at 72% 28%, rgba(0, 178, 214, 0.12), transparent 31%),
radial-gradient(circle at 88% 62%, rgba(184, 50, 216, 0.1), transparent 33%),
linear-gradient(90deg, transparent 0 64px, rgba(8, 145, 178, 0.06) 65px 66px, transparent 67px 160px),
linear-gradient(180deg, rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0.08) 38%, rgba(237, 247, 252, 0.64) 100%);
--cyber-background-opacity: 0.5;
--cyber-background-before-bg:
linear-gradient(90deg, transparent 0 8%, rgba(8, 145, 178, 0.16) 8.1% 8.22%, transparent 8.34% 18%, rgba(184, 50, 216, 0.12) 18.1% 18.22%, transparent 18.34% 31%, rgba(8, 145, 178, 0.13) 31.1% 31.24%, transparent 31.36% 44%, rgba(37, 99, 235, 0.12) 44.1% 44.2%, transparent 44.34% 62%, rgba(184, 50, 216, 0.1) 62.1% 62.22%, transparent 62.34% 78%, rgba(8, 145, 178, 0.12) 78.1% 78.22%, transparent 78.34%),
repeating-linear-gradient(90deg, transparent 0 78px, rgba(8, 145, 178, 0.06) 80px 82px, transparent 84px 116px),
linear-gradient(to top, rgba(8, 145, 178, 0.12) 0%, rgba(8, 145, 178, 0.07) 13%, transparent 31%),
linear-gradient(to top, rgba(184, 50, 216, 0.1) 0%, rgba(184, 50, 216, 0.05) 16%, transparent 38%),
linear-gradient(to top, rgba(37, 99, 235, 0.09) 0%, rgba(37, 99, 235, 0.04) 20%, transparent 48%);
--cyber-background-before-opacity: 0.58;
--cyber-background-before-blend: multiply;
--cyber-background-after-bg:
repeating-linear-gradient(90deg, transparent 0 34px, rgba(8, 145, 178, 0.1) 35px 36px, transparent 37px 110px),
repeating-linear-gradient(180deg, transparent 0 28px, rgba(184, 50, 216, 0.08) 29px 30px, transparent 31px 78px),
linear-gradient(90deg, transparent, rgba(8, 145, 178, 0.07), transparent);
--cyber-background-after-opacity: 0.18;
--cyber-background-after-blend: multiply;
--cyber-wash-bg:
radial-gradient(circle at 18% 44%, rgba(255, 255, 255, 0.28), rgba(255, 255, 255, 0.12) 36%, transparent 60%),
linear-gradient(90deg, rgba(255, 255, 255, 0.18) 0%, rgba(237, 248, 252, 0.1) 36%, rgba(255, 255, 255, 0.03) 68%, rgba(251, 247, 255, 0.16) 100%),
linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(248, 252, 255, 0.04) 58%, rgba(248, 252, 255, 0.72));
--cyber-gridlines-bg:
linear-gradient(rgba(8, 145, 178, 0.055) 1px, transparent 1px),
linear-gradient(90deg, rgba(8, 145, 178, 0.045) 1px, transparent 1px);
--cyber-gridlines-opacity: 0.2;
--cyber-scanlines-bg: repeating-linear-gradient(
to bottom,
rgba(8, 35, 50, 0.035) 0,
rgba(8, 35, 50, 0.035) 1px,
transparent 1px,
transparent 4px
);
--cyber-scanlines-opacity: 0.08;
--cyber-copy-aura: radial-gradient(circle at 28% 38%, rgba(255, 255, 255, 0.92), rgba(255, 255, 255, 0.38) 58%, transparent 78%);
--cyber-title-color: rgba(19, 34, 56, 0.98);
--cyber-description-color: rgba(50, 65, 88, 0.82);
--cyber-action-primary-color: #061722;
--cyber-action-secondary-bg: rgba(255, 255, 255, 0.62);
--cyber-action-secondary-hover-bg: rgba(8, 145, 178, 0.08);
--cyber-release-color: rgba(50, 65, 88, 0.68);
--cyber-scene-floor-bg:
radial-gradient(ellipse at 58% 84%, rgba(184, 50, 216, 0.16), transparent 18%),
radial-gradient(ellipse at 56% 84%, rgba(8, 145, 178, 0.14), transparent 32%),
repeating-radial-gradient(ellipse at 58% 84%, rgba(8, 145, 178, 0.08) 0 1px, transparent 1px 20px);
--cyber-scene-floor-opacity: 0.38;
--cyber-scene-foreground-bg:
linear-gradient(90deg, transparent 0 4%, rgba(8, 145, 178, 0.07) 4.1%, transparent 4.4%),
linear-gradient(180deg, transparent 0 88%, rgba(184, 50, 216, 0.06));
--cyber-scene-foreground-opacity: 0.48;
--cyber-video-frame-bg: rgba(255, 255, 255, 0.7);
--cyber-video-content-bg: rgba(8, 20, 34, 0.9);
--cyber-card-text: rgba(19, 34, 56, 0.88);
--cyber-card-muted: rgba(67, 82, 105, 0.76);
--cyber-card-subtle: rgba(67, 82, 105, 0.64);
--cyber-card-inset: rgba(255, 255, 255, 0.62);
--cyber-feature-shell-bg: transparent;
--cyber-feature-rail-bg: transparent;
--cyber-feature-rail-shadow:
0 0 0 1px rgba(8, 145, 178, 0.12) inset,
0 -1px 0 rgba(8, 145, 178, 0.18),
0 1px 0 rgba(8, 145, 178, 0.16);
--cyber-feature-divider: rgba(8, 145, 178, 0.18);
--cyber-feature-title: rgba(19, 34, 56, 0.94);
--cyber-feature-text: rgba(67, 82, 105, 0.72);
}
.cyber-panel {
position: relative;
border: 1px solid var(--cyber-border-cyan);
background:
linear-gradient(135deg, rgba(5, 14, 31, 0.9), rgba(3, 10, 22, 0.64));
background: var(--cyber-panel-bg);
clip-path: polygon(
var(--cyber-frame-cut) 0,
100% 0,
@ -33,9 +234,7 @@
0 100%,
0 var(--cyber-frame-cut)
);
box-shadow:
0 0 0 1px rgba(47, 125, 255, 0.12) inset,
0 0 24px rgba(0, 234, 255, 0.12);
box-shadow: var(--cyber-panel-shadow);
}
.cyber-hero {
@ -53,13 +252,11 @@
isolation: isolate;
overflow: clip;
color: var(--cyber-text);
background:
radial-gradient(circle at 76% 30%, rgba(0, 234, 255, 0.14), transparent 30%),
radial-gradient(circle at 86% 70%, rgba(255, 43, 255, 0.14), transparent 34%),
linear-gradient(180deg, var(--cyber-bg-0), var(--cyber-bg-1) 58%, var(--cyber-bg-0));
background: var(--cyber-hero-bg);
}
.cyber-hero__background,
.cyber-hero__monterey,
.cyber-hero__wash,
.cyber-hero__gridlines,
.cyber-hero__scanlines {
@ -68,32 +265,75 @@
pointer-events: none;
}
.cyber-hero__monterey {
z-index: -5;
overflow: hidden;
background: var(--cyber-monterey-bg);
}
.cyber-hero__monterey::before,
.cyber-hero__monterey::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
}
.cyber-hero__monterey::before {
z-index: 2;
background: var(--cyber-monterey-before-bg);
}
.cyber-hero__monterey::after {
z-index: 3;
background: var(--cyber-monterey-after-bg);
}
.cyber-hero__monterey-canvas {
position: absolute;
inset: 0;
z-index: 1;
width: 100%;
height: 100%;
opacity: 0;
filter: var(--cyber-monterey-canvas-filter);
mix-blend-mode: var(--cyber-monterey-canvas-blend);
transform: scale(1.035);
transition: opacity 1.65s cubic-bezier(0.22, 0.72, 0.2, 1);
}
.cyber-hero__monterey--live .cyber-hero__monterey-canvas {
opacity: var(--cyber-monterey-canvas-opacity);
}
.cyber-hero__monterey a[data-n] {
left: 14px !important;
right: auto !important;
bottom: 12px !important;
z-index: 5 !important;
padding: 0 !important;
color: rgba(244, 247, 255, 0.46) !important;
font: 700 10px/1 var(--font-family-mono, monospace) !important;
letter-spacing: 0.14em !important;
opacity: 0.32 !important;
text-shadow: none !important;
}
.cyber-hero__background {
z-index: -4;
inset: -40px -40px -80px;
background:
radial-gradient(circle at 70% 26%, rgba(0, 234, 255, 0.16), transparent 30%),
radial-gradient(circle at 86% 62%, rgba(255, 43, 255, 0.16), transparent 32%),
linear-gradient(90deg, transparent 0 64px, rgba(0, 234, 255, 0.055) 65px 66px, transparent 67px 160px),
linear-gradient(180deg, transparent 0 55%, rgba(255, 43, 255, 0.05) 55.2% 55.4%, transparent 55.6%),
linear-gradient(to top, rgba(3, 10, 24, 0.88) 0%, rgba(3, 10, 24, 0.7) 12%, rgba(3, 10, 24, 0.34) 25%, transparent 38%),
linear-gradient(to top, rgba(5, 17, 40, 0.42) 0%, rgba(5, 17, 40, 0.28) 24%, rgba(5, 17, 40, 0.12) 43%, transparent 62%),
linear-gradient(180deg, #02050d 0%, #061124 54%, #02050d 100%);
background: var(--cyber-background-bg);
background-size:
auto,
auto,
220px 100%,
auto,
260px 100%,
180px 100%,
auto;
background-position:
0 0,
center,
24px 0,
0 0,
center;
opacity: 0.92;
opacity: var(--cyber-background-opacity);
transform: translate3d(
0,
calc(var(--hero-scroll) * 0.035px),
@ -106,12 +346,7 @@
content: "";
position: absolute;
inset: 12% 0 0;
background:
linear-gradient(90deg, transparent 0 8%, rgba(0, 234, 255, 0.14) 8.1% 8.22%, transparent 8.34% 18%, rgba(255, 43, 255, 0.12) 18.1% 18.22%, transparent 18.34% 31%, rgba(0, 234, 255, 0.11) 31.1% 31.24%, transparent 31.36% 44%, rgba(47, 125, 255, 0.12) 44.1% 44.2%, transparent 44.34% 62%, rgba(255, 43, 255, 0.1) 62.1% 62.22%, transparent 62.34% 78%, rgba(0, 234, 255, 0.12) 78.1% 78.22%, transparent 78.34%),
repeating-linear-gradient(90deg, transparent 0 78px, rgba(0, 234, 255, 0.05) 80px 82px, transparent 84px 116px),
linear-gradient(to top, rgba(3, 12, 27, 0.66) 0%, rgba(3, 12, 27, 0.42) 8%, rgba(3, 12, 27, 0.16) 17%, transparent 28%),
linear-gradient(to top, rgba(5, 20, 44, 0.5) 0%, rgba(5, 20, 44, 0.28) 12%, rgba(5, 20, 44, 0.12) 24%, transparent 36%),
linear-gradient(to top, rgba(4, 17, 38, 0.38) 0%, rgba(4, 17, 38, 0.22) 18%, rgba(4, 17, 38, 0.08) 34%, transparent 48%);
background: var(--cyber-background-before-bg);
background-size:
auto,
auto,
@ -124,8 +359,8 @@
16px bottom,
72px bottom,
120px bottom;
opacity: 0.62;
mix-blend-mode: screen;
opacity: var(--cyber-background-before-opacity);
mix-blend-mode: var(--cyber-background-before-blend);
mask-image: linear-gradient(180deg, transparent 0, black 22%, black 78%, transparent 100%);
}
@ -133,12 +368,9 @@
content: "";
position: absolute;
inset: 0;
background:
repeating-linear-gradient(90deg, transparent 0 34px, rgba(0, 234, 255, 0.12) 35px 36px, transparent 37px 110px),
repeating-linear-gradient(180deg, transparent 0 28px, rgba(255, 43, 255, 0.1) 29px 30px, transparent 31px 78px),
linear-gradient(90deg, transparent, rgba(0, 234, 255, 0.08), transparent);
opacity: 0.26;
mix-blend-mode: screen;
background: var(--cyber-background-after-bg);
opacity: var(--cyber-background-after-opacity);
mix-blend-mode: var(--cyber-background-after-blend);
mask-image:
linear-gradient(180deg, transparent 0 10%, black 22%, black 82%, transparent 100%),
linear-gradient(90deg, transparent 0 8%, black 24%, black 94%, transparent 100%);
@ -146,32 +378,21 @@
.cyber-hero__wash {
z-index: -3;
background:
radial-gradient(circle at 18% 44%, rgba(2, 5, 13, 0.48), rgba(2, 5, 13, 0.22) 34%, transparent 58%),
linear-gradient(90deg, rgba(2, 5, 13, 0.42) 0%, rgba(2, 5, 13, 0.28) 34%, rgba(2, 5, 13, 0.08) 66%, rgba(2, 5, 13, 0.3) 100%),
linear-gradient(180deg, rgba(2, 5, 13, 0.18), rgba(2, 5, 13, 0.08) 58%, rgba(2, 5, 13, 0.92));
background: var(--cyber-wash-bg);
}
.cyber-hero__gridlines {
z-index: -2;
opacity: 0.16;
background-image:
linear-gradient(rgba(0, 234, 255, 0.055) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 234, 255, 0.045) 1px, transparent 1px);
opacity: var(--cyber-gridlines-opacity);
background-image: var(--cyber-gridlines-bg);
background-size: 72px 72px;
mask-image: linear-gradient(180deg, transparent, black 12%, black 72%, transparent);
}
.cyber-hero__scanlines {
z-index: 8;
opacity: 0.11;
background-image: repeating-linear-gradient(
to bottom,
rgba(255, 255, 255, 0.08) 0,
rgba(255, 255, 255, 0.08) 1px,
transparent 1px,
transparent 4px
);
opacity: var(--cyber-scanlines-opacity);
background-image: var(--cyber-scanlines-bg);
mix-blend-mode: overlay;
}
@ -179,7 +400,8 @@
position: relative;
z-index: 2;
width: min(1580px, calc(100vw - 56px));
max-width: none !important;
max-width: min(1580px, calc(100vw - 56px)) !important;
margin-inline: auto;
}
.cyber-hero__layout {
@ -198,12 +420,7 @@
}
.cyber-hero__copy::before {
content: "";
position: absolute;
inset: -64px -64px -42px -36px;
z-index: -1;
background: radial-gradient(circle at 28% 38%, rgba(2, 5, 13, 0.82), rgba(2, 5, 13, 0.36) 62%, transparent 78%);
pointer-events: none;
display: none;
}
.cyber-hero__brand-lockup {
@ -241,7 +458,7 @@
line-height: 1;
font-weight: 900;
letter-spacing: 0;
color: rgba(244, 247, 255, 0.96);
color: var(--cyber-title-color);
white-space: nowrap;
}
@ -269,7 +486,7 @@
.cyber-hero__description {
max-width: 560px;
margin: 0 0 30px;
color: rgba(222, 229, 255, 0.84);
color: var(--cyber-description-color);
font-size: clamp(1rem, 1.08vw, 1.22rem);
line-height: 1.7;
}
@ -302,7 +519,7 @@
}
.cyber-hero__action--primary.v-btn {
color: var(--cyber-bg-0) !important;
color: var(--cyber-action-primary-color) !important;
background: linear-gradient(135deg, var(--cyber-cyan), var(--cyber-magenta)) !important;
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.16) inset,
@ -314,14 +531,14 @@
.cyber-hero__action--docs.v-btn {
color: var(--cyber-text) !important;
border-color: rgba(0, 234, 255, 0.46) !important;
background: rgba(3, 10, 22, 0.56) !important;
background: var(--cyber-action-secondary-bg) !important;
}
.cyber-hero__action--watch.v-btn:hover,
.cyber-hero__action--docs.v-btn:hover {
color: var(--cyber-cyan) !important;
border-color: rgba(0, 234, 255, 0.74) !important;
background: rgba(0, 234, 255, 0.08) !important;
background: var(--cyber-action-secondary-hover-bg) !important;
}
.cyber-hero__terminal-note {
@ -345,10 +562,14 @@
.cyber-hero__release {
flex: 0 0 auto;
color: rgba(244, 247, 255, 0.62);
color: var(--cyber-release-color);
white-space: nowrap;
}
.cyber-hero__release-date {
color: var(--cyber-card-subtle);
}
.cyber-hero__scene {
min-width: 0;
width: clamp(940px, 60vw, 1220px);
@ -384,12 +605,9 @@
.cyber-scene__floor {
z-index: 0;
background:
radial-gradient(ellipse at 58% 84%, rgba(255, 43, 255, 0.24), transparent 18%),
radial-gradient(ellipse at 56% 84%, rgba(0, 234, 255, 0.18), transparent 32%),
repeating-radial-gradient(ellipse at 58% 84%, rgba(0, 234, 255, 0.1) 0 1px, transparent 1px 20px);
background: var(--cyber-scene-floor-bg);
filter: blur(8px);
opacity: 0.48;
opacity: var(--cyber-scene-floor-opacity);
mask-image: radial-gradient(ellipse at 58% 84%, black 0 26%, rgba(0, 0, 0, 0.52) 40%, transparent 62%);
}
@ -427,11 +645,9 @@
.cyber-scene__foreground {
z-index: 6;
background:
linear-gradient(90deg, transparent 0 4%, rgba(0, 234, 255, 0.08) 4.1%, transparent 4.4%),
linear-gradient(180deg, transparent 0 88%, rgba(255, 43, 255, 0.08));
background: var(--cyber-scene-foreground-bg);
mix-blend-mode: screen;
opacity: 0.72;
opacity: var(--cyber-scene-foreground-opacity);
mask-image: linear-gradient(180deg, transparent 0 18%, black 28%, black 78%, transparent 100%);
}
@ -441,7 +657,7 @@
position: relative;
aspect-ratio: 16 / 9;
border: 1px solid rgba(0, 234, 255, 0.66);
background: rgba(2, 6, 16, 0.82);
background: var(--cyber-video-frame-bg);
clip-path: polygon(20px 0, 100% 0, 100% calc(100% - 20px), calc(100% - 20px) 100%, 0 100%, 0 20px);
box-shadow:
0 0 0 1px rgba(47, 125, 255, 0.2) inset,
@ -484,7 +700,7 @@
z-index: 2;
overflow: hidden;
border-radius: var(--cyber-radius-sm);
background: rgba(2, 6, 16, 0.94);
background: var(--cyber-video-content-bg);
clip-path: polygon(14px 0, 100% 0, 100% calc(100% - 14px), calc(100% - 14px) 100%, 0 100%, 0 14px);
}
@ -494,7 +710,7 @@
border: 0;
border-radius: 0;
box-shadow: none;
background: rgba(2, 6, 16, 0.95);
background: var(--cyber-video-content-bg);
}
.cyber-video-frame__content .hero-video__player {
@ -635,13 +851,13 @@
left: 66%;
width: 274px;
padding: 12px 13px;
color: rgba(244, 247, 255, 0.84);
color: var(--cyber-card-text);
font-family: var(--at-font-mono);
font-size: 1.08rem;
line-height: 1.28;
border-color: color-mix(in srgb, var(--agent-accent), transparent 44%);
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.06) inset,
0 0 0 1px var(--cyber-card-inset) inset,
0 0 24px var(--agent-accent-soft);
}
@ -691,7 +907,7 @@
.cyber-agent__tasks li {
position: relative;
padding-left: 15px;
color: rgba(222, 229, 255, 0.72);
color: var(--cyber-card-muted);
}
.cyber-agent__tasks li::before {
@ -709,7 +925,7 @@
display: flex;
gap: 6px;
margin-top: 7px;
color: rgba(222, 229, 255, 0.62);
color: var(--cyber-card-subtle);
}
.cyber-agent__status strong {
@ -744,13 +960,13 @@
.cyber-agent[data-agent="planner"] .cyber-agent__card {
display: block;
top: 12%;
right: 108%;
top: 10%;
right: 106%;
left: auto;
width: 280px;
padding: 12px 13px;
font-size: 1.14rem;
transform: translate(110px, -10%);
width: 258px;
padding: 11px 12px;
font-size: 1.04rem;
transform: translate(-28px, -8%);
}
.cyber-agent[data-agent="lead"] .cyber-agent__card {
@ -763,15 +979,29 @@
.cyber-agent[data-agent="developer"] .cyber-agent__card {
top: -14%;
left: 102%;
left: 98%;
width: 274px;
transform: translateY(-12px);
}
@media (min-width: 1101px) {
.cyber-agent[data-agent="planner"] {
left: calc(var(--agent-x) * 1% + 20px);
top: calc(var(--agent-y) * 1% - 26px);
}
.cyber-agent[data-agent="lead"],
.cyber-agent[data-agent="developer"] {
top: calc(var(--agent-y) * 1% + 12px);
top: calc(var(--agent-y) * 1% - 4px);
}
.cyber-agent[data-agent="lead"] .cyber-agent__float,
.cyber-agent[data-agent="developer"] .cyber-agent__float {
top: 10px;
}
.cyber-agent[data-agent="planner"] .cyber-agent__float {
top: 70px;
}
}
@ -797,6 +1027,10 @@
position: absolute;
left: calc(var(--bubble-x) * 1%);
top: calc(var(--bubble-y) * 1%);
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
max-width: 168px;
min-height: 30px;
padding: 7px 11px;
@ -919,35 +1153,35 @@
@keyframes cyberFeatureReviewerSpeechFloat {
0%,
100% {
transform: translate3d(0, 0, 0) rotate(-4deg);
transform: translateX(calc(50% + var(--reviewer-bubble-center-shift, 0px))) translate3d(0, 0, 0) rotate(-4deg);
}
50% {
transform: translate3d(0, -3px, 0) rotate(-3deg);
transform: translateX(calc(50% + var(--reviewer-bubble-center-shift, 0px))) translate3d(0, -3px, 0) rotate(-3deg);
}
}
@keyframes cyberFeatureReviewerSpeechPop {
0% {
opacity: 0;
transform: translate3d(12px, 12px, 0) scale(0.54) rotate(-12deg);
transform: translateX(calc(50% + var(--reviewer-bubble-center-shift, 0px))) translate3d(12px, 12px, 0) scale(0.54) rotate(-12deg);
}
62% {
opacity: 1;
transform: translate3d(-2px, -3px, 0) scale(1.08) rotate(-3deg);
transform: translateX(calc(50% + var(--reviewer-bubble-center-shift, 0px))) translate3d(-2px, -3px, 0) scale(1.08) rotate(-3deg);
}
100% {
opacity: 1;
transform: translate3d(0, 0, 0) scale(1) rotate(-4deg);
transform: translateX(calc(50% + var(--reviewer-bubble-center-shift, 0px))) translate3d(0, 0, 0) scale(1) rotate(-4deg);
}
}
@keyframes cyberFeatureReviewerSpeechExit {
to {
opacity: 0;
transform: translate3d(8px, 8px, 0) scale(0.86) rotate(-8deg);
transform: translateX(calc(50% + var(--reviewer-bubble-center-shift, 0px))) translate3d(8px, 8px, 0) scale(0.86) rotate(-8deg);
}
}
@ -964,10 +1198,7 @@
z-index: -1;
inset: -118px -3vw -96px;
pointer-events: none;
background:
radial-gradient(ellipse at 50% 62%, rgba(0, 234, 255, 0.08), transparent 56%),
radial-gradient(ellipse at 78% 52%, rgba(255, 43, 255, 0.08), transparent 48%),
linear-gradient(180deg, transparent 0%, rgba(5, 17, 40, 0.08) 30%, rgba(5, 14, 31, 0.18) 52%, rgba(5, 17, 40, 0.08) 72%, transparent 100%);
background: var(--cyber-feature-shell-bg);
opacity: 0.86;
mask-image: radial-gradient(ellipse at 50% 54%, black 0 48%, rgba(0, 0, 0, 0.5) 62%, transparent 78%);
}
@ -980,97 +1211,70 @@
gap: 0;
width: 100%;
padding: 17px 18px;
background:
linear-gradient(180deg, rgba(3, 10, 22, 0.34) 0%, rgba(5, 14, 31, 0.74) 30%, rgba(5, 14, 31, 0.72) 70%, rgba(3, 10, 22, 0.34) 100%),
linear-gradient(135deg, rgba(5, 14, 31, 0.58), rgba(3, 10, 22, 0.42));
box-shadow:
0 0 0 1px rgba(47, 125, 255, 0.1) inset,
0 -28px 46px rgba(2, 5, 13, 0.14),
0 30px 58px rgba(2, 5, 13, 0.18),
0 0 24px rgba(0, 234, 255, 0.1);
background: var(--cyber-feature-rail-bg);
box-shadow: var(--cyber-feature-rail-shadow);
}
.cyber-feature-rail__collaboration {
position: absolute;
left: 50%;
bottom: calc(100% - 16px);
z-index: 2;
width: clamp(132px, 11vw, 168px);
height: auto;
pointer-events: none;
user-select: none;
filter:
drop-shadow(0 18px 24px rgba(0, 0, 0, 0.5))
drop-shadow(0 0 20px rgba(120, 58, 255, 0.18))
drop-shadow(0 0 18px rgba(255, 45, 64, 0.14));
transform: translate3d(-50%, 0, 0);
}
.cyber-feature-rail__reviewer {
--reviewer-robot-width: clamp(76px, 5.4vw, 96px);
position: absolute;
z-index: 3;
right: clamp(34px, 5vw, 86px);
bottom: calc(100% - 8px);
right: clamp(52px, 5vw, 82px);
bottom: calc(100% - 6px);
display: flex;
align-items: flex-end;
gap: 10px;
gap: 8px;
pointer-events: none;
}
.cyber-feature-rail__reviewer--active .cyber-feature-rail__reviewer-card {
border-color: rgba(255, 43, 255, 0.86);
border-color: rgba(0, 234, 255, 0.86);
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.08) inset,
0 0 28px rgba(255, 43, 255, 0.28);
0 0 28px rgba(0, 234, 255, 0.26);
}
.cyber-feature-rail__reviewer--active .cyber-feature-rail__robot {
filter:
drop-shadow(0 16px 24px rgba(0, 0, 0, 0.5))
drop-shadow(0 0 22px rgba(255, 43, 255, 0.42));
drop-shadow(0 0 22px rgba(0, 234, 255, 0.36));
}
.cyber-feature-rail__reviewer-bubble {
--speech-fill: rgba(255, 246, 159, 0.9);
--reviewer-bubble-center-shift: 3px;
position: absolute;
right: clamp(98px, 6.5vw, 116px);
bottom: calc(100% - 44px);
left: auto;
top: auto;
right: calc(var(--reviewer-robot-width) / 2);
bottom: calc(100% + 10px);
z-index: 6;
display: inline-flex;
align-items: center;
min-height: 30px;
width: max-content;
max-width: 158px;
padding: 7px 11px;
color: #0b1020;
font-family: var(--at-font-mono);
font-size: 0.64rem;
font-weight: 900;
line-height: 1.1;
letter-spacing: 0;
text-align: center;
white-space: nowrap;
background:
radial-gradient(circle at 28% 24%, rgba(255, 255, 255, 0.9), var(--speech-fill) 64%, rgba(255, 224, 88, 0.92) 100%);
border: 2px solid #050816;
border-radius: 999px;
box-shadow:
0 0 0 1px rgba(255, 215, 0, 0.34),
0 5px 0 rgba(0, 0, 0, 0.2),
0 0 14px rgba(255, 215, 0, 0.18);
text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.62);
transform: rotate(-4deg);
transform-origin: right bottom;
white-space: normal;
overflow-wrap: anywhere;
text-wrap: balance;
transform: translateX(calc(50% + var(--reviewer-bubble-center-shift))) translate3d(0, 0, 0) rotate(-4deg);
transform-origin: center bottom;
animation: cyberFeatureReviewerSpeechFloat 2.8s ease-in-out 0.45s infinite;
}
.cyber-feature-rail__reviewer-bubble::before,
.cyber-feature-rail__reviewer-bubble::after {
position: absolute;
top: 52%;
content: "";
clip-path: polygon(0 0, 100% 50%, 0 100%);
transform: translateY(-50%);
}
.cyber-feature-rail__reviewer-bubble::before {
right: -21px;
width: 23px;
height: 17px;
background: #050816;
}
.cyber-feature-rail__reviewer-bubble::after {
right: -16px;
width: 18px;
height: 11px;
background: rgba(255, 226, 78, 0.96);
}
.cyber-feature-bubble-enter-active,
.cyber-feature-bubble-leave-active {
transition:
@ -1095,13 +1299,13 @@
.cyber-feature-rail__robot {
position: relative;
top: 9px;
width: clamp(74px, 5.6vw, 96px);
width: var(--reviewer-robot-width);
height: auto;
transform: rotate(3deg);
transform-origin: center bottom;
filter:
drop-shadow(0 14px 22px rgba(0, 0, 0, 0.48))
drop-shadow(0 0 16px rgba(255, 43, 255, 0.26));
drop-shadow(0 0 16px rgba(0, 234, 255, 0.22));
pointer-events: none;
user-select: none;
animation: cyberRobotBob 6.4s ease-in-out infinite;
@ -1109,16 +1313,17 @@
.cyber-feature-rail__reviewer-card {
position: relative;
width: clamp(190px, 15.5vw, 248px);
padding: 10px 12px;
color: rgba(244, 247, 255, 0.84);
width: clamp(146px, 10.2vw, 160px);
padding: 7px 8px;
color: var(--cyber-card-text);
font-family: var(--at-font-mono);
font-size: clamp(0.56rem, 0.65vw, 0.72rem);
line-height: 1.32;
border-color: rgba(255, 43, 255, 0.5);
font-size: clamp(0.48rem, 0.46vw, 0.54rem);
line-height: 1.28;
border-color: rgba(0, 234, 255, 0.52);
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.06) inset,
0 0 24px rgba(255, 43, 255, 0.2);
0 0 0 1px var(--cyber-card-inset) inset,
0 0 24px rgba(0, 234, 255, 0.18);
transform: translateY(-34px);
}
.cyber-feature-rail__reviewer-card::after {
@ -1126,15 +1331,15 @@
position: absolute;
left: calc(100% - 1px);
top: 56%;
width: 18px;
width: 13px;
height: 1px;
background: linear-gradient(90deg, rgba(255, 43, 255, 0.72), transparent);
box-shadow: 0 0 12px rgba(255, 43, 255, 0.34);
background: linear-gradient(90deg, rgba(0, 234, 255, 0.72), transparent);
box-shadow: 0 0 12px rgba(0, 234, 255, 0.3);
}
.cyber-feature-rail__reviewer-label {
margin-bottom: 7px;
color: var(--cyber-magenta);
margin-bottom: 5px;
color: var(--cyber-cyan);
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.07em;
@ -1142,7 +1347,7 @@
.cyber-feature-rail__reviewer-tasks {
display: grid;
gap: 4px;
gap: 3px;
margin: 0;
padding: 0;
list-style: none;
@ -1150,8 +1355,8 @@
.cyber-feature-rail__reviewer-tasks li {
position: relative;
padding-left: 15px;
color: rgba(222, 229, 255, 0.72);
padding-left: 12px;
color: var(--cyber-card-muted);
}
.cyber-feature-rail__reviewer-tasks li::before {
@ -1159,21 +1364,21 @@
position: absolute;
left: 0;
top: 0.52em;
width: 6px;
height: 6px;
border: 1px solid var(--cyber-magenta);
width: 5px;
height: 5px;
border: 1px solid var(--cyber-cyan);
transform: rotate(45deg);
}
.cyber-feature-rail__reviewer-status {
display: flex;
gap: 6px;
margin-top: 7px;
color: rgba(222, 229, 255, 0.62);
gap: 5px;
margin-top: 5px;
color: var(--cyber-card-subtle);
}
.cyber-feature-rail__reviewer-status strong {
color: var(--cyber-magenta);
color: var(--cyber-cyan);
font-weight: 800;
}
@ -1184,7 +1389,7 @@
gap: 12px;
min-width: 0;
padding: 0 18px;
border-right: 1px solid rgba(0, 234, 255, 0.16);
border-right: 1px solid var(--cyber-feature-divider);
}
.cyber-feature-rail__item:last-child {
@ -1204,13 +1409,13 @@
.cyber-feature-rail__title {
margin-bottom: 3px;
color: rgba(244, 247, 255, 0.94);
color: var(--cyber-feature-title);
font-weight: 800;
font-size: 0.92rem;
}
.cyber-feature-rail__text {
color: rgba(222, 229, 255, 0.62);
color: var(--cyber-feature-text);
font-size: 0.8rem;
line-height: 1.45;
}
@ -1329,7 +1534,15 @@
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.cyber-feature-rail__collaboration {
left: 50%;
bottom: calc(100% - 14px);
width: 132px;
}
.cyber-feature-rail__reviewer {
--reviewer-robot-width: 78px;
right: 56px;
gap: 10px;
}
@ -1340,7 +1553,7 @@
}
.cyber-feature-rail__robot {
width: 78px;
width: var(--reviewer-robot-width);
}
.cyber-feature-rail__item:nth-child(3) {
@ -1363,6 +1576,14 @@
transform: scale(1.02);
}
.cyber-hero__monterey {
opacity: 0.58;
}
.cyber-hero__monterey-canvas {
display: none;
}
.cyber-hero__container {
width: min(100% - 32px, 680px);
}
@ -1484,6 +1705,10 @@
padding: 16px;
}
.cyber-feature-rail__collaboration {
display: none;
}
.cyber-feature-rail__reviewer {
display: none;
}
@ -1510,6 +1735,7 @@
}
.cyber-hero__background,
.cyber-hero__monterey,
.cyber-scene,
.cyber-scene__video,
.cyber-agent {

View file

@ -6,10 +6,11 @@ const { isDark, toggleTheme } = useBrowserTheme();
const { trackThemeToggle } = useAnalytics();
const tooltip = computed(() => isDark.value ? t('theme.light') : t('theme.dark'));
const icon = computed(() => isDark.value ? mdiWeatherNight : mdiWeatherSunny);
const onToggle = () => {
toggleTheme();
trackThemeToggle(isDark.value ? 'dark' : 'light');
const theme = toggleTheme();
trackThemeToggle(theme);
};
</script>
@ -19,7 +20,7 @@ const onToggle = () => {
<template #activator="{ props }">
<v-btn
v-bind="props"
:icon="isDark ? mdiWeatherSunny : mdiWeatherNight"
:icon="icon"
variant="text"
size="small"
:aria-label="tooltip"

View file

@ -7,6 +7,7 @@ import {
mdiMonitorDashboard,
} from "@mdi/js";
import {
heroCollaborationFeature,
heroFeatureRail,
heroReviewerFeatureCard,
type HeroMessage,
@ -48,6 +49,14 @@ const reviewerBubbleText = computed(() => {
<template>
<div class="cyber-feature-rail-shell">
<img
class="cyber-feature-rail__collaboration"
:src="heroCollaborationFeature.asset"
alt=""
loading="lazy"
decoding="async"
aria-hidden="true"
>
<div
class="cyber-feature-rail__reviewer"
:class="{
@ -58,12 +67,13 @@ const reviewerBubbleText = computed(() => {
aria-hidden="true"
>
<Transition name="cyber-feature-bubble">
<div
<CyberHeroSpeechBubble
v-if="reviewerBubbleText"
class="cyber-feature-rail__reviewer-bubble"
role="reviewer"
>
{{ reviewerBubbleText }}
</div>
</CyberHeroSpeechBubble>
</Transition>
<div class="cyber-feature-rail__reviewer-card cyber-panel">
<div class="cyber-feature-rail__reviewer-label">{{ heroReviewerFeatureCard.label }}</div>

View file

@ -28,29 +28,29 @@ const showReceiver = computed(() =>
<template>
<div class="cyber-messages" aria-hidden="true">
<Transition name="cyber-bubble">
<div
<CyberHeroSpeechBubble
v-if="showSender && message && !reducedMotion"
class="cyber-message cyber-message--sender"
:class="`cyber-message--role-${message.from}`"
:style="senderStyle"
variant="sender"
:role="message.from"
:bubble-style="senderStyle"
>
{{ message.text }}
</div>
</CyberHeroSpeechBubble>
</Transition>
<Transition name="cyber-bubble">
<div
<CyberHeroSpeechBubble
v-if="showReceiver && message && !reducedMotion"
class="cyber-message cyber-message--receiver"
:class="`cyber-message--role-${message.to}`"
:style="receiverStyle"
variant="receiver"
:role="message.to"
:bubble-style="receiverStyle"
>
{{ message.response }}
</div>
</CyberHeroSpeechBubble>
</Transition>
<div v-if="reducedMotion" class="cyber-message cyber-message--static cyber-panel">
<CyberHeroSpeechBubble v-if="reducedMotion" class="cyber-panel" variant="static">
Agents coordinate work automatically.
</div>
</CyberHeroSpeechBubble>
</div>
</template>

View file

@ -0,0 +1,174 @@
<script setup lang="ts">
import type { NeatConfig, NeatController } from "@firecms/neat";
const canvasRef = ref<HTMLCanvasElement | null>(null);
const isLive = ref(false);
let gradient: NeatController | null = null;
let heroObserver: IntersectionObserver | null = null;
let motionQuery: MediaQueryList | null = null;
let mobileQuery: MediaQueryList | null = null;
let isVisible = false;
let isInitializing = false;
let initToken = 0;
let revealTimer: number | null = null;
const montereyConfig: NeatConfig = {
colors: [
{ color: "#130437", enabled: true },
{ color: "#B34BD0", enabled: true },
{ color: "#210751", enabled: true },
{ color: "#3511A5", enabled: true },
{ color: "#8F3E8D", enabled: false },
{ color: "#FF9A9E", enabled: false },
],
speed: 4.8,
horizontalPressure: 7,
verticalPressure: 3,
waveFrequencyX: 0,
waveFrequencyY: 0,
waveAmplitude: 0,
shadows: 4,
highlights: 0,
colorBrightness: 1.92,
colorSaturation: 2.18,
wireframe: false,
colorBlending: 9,
backgroundColor: "#030012",
backgroundAlpha: 1,
grainScale: 6,
grainSparsity: 0,
grainIntensity: 0.1,
grainSpeed: 0,
resolution: 0.32,
yOffset: 150,
flowDistortionA: 0.4,
flowDistortionB: 10,
flowScale: 3.3,
flowEase: 0.37,
enableProceduralTexture: false,
textureVoidLikelihood: 0.06,
textureVoidWidthMin: 10,
textureVoidWidthMax: 500,
textureBandDensity: 0.8,
textureColorBlending: 0.06,
textureSeed: 333,
textureEase: 0.38,
proceduralBackgroundColor: "#003FFF",
textureShapeTriangles: 20,
textureShapeCircles: 15,
textureShapeBars: 15,
textureShapeSquiggles: 10,
yOffsetWaveMultiplier: 4.5,
yOffsetColorMultiplier: 4.8,
yOffsetFlowMultiplier: 5.2,
flowEnabled: true,
};
function supportsWebGl() {
try {
const canvas = document.createElement("canvas");
const context = canvas.getContext("webgl2") || canvas.getContext("webgl");
const isSupported = Boolean(context);
context?.getExtension("WEBGL_lose_context")?.loseContext();
return isSupported;
} catch {
return false;
}
}
function shouldUseLiveGradient() {
return Boolean(
canvasRef.value &&
isVisible &&
!motionQuery?.matches &&
!mobileQuery?.matches &&
supportsWebGl(),
);
}
function destroyGradient() {
initToken += 1;
if (revealTimer !== null) {
window.clearTimeout(revealTimer);
revealTimer = null;
}
gradient?.destroy();
gradient = null;
isLive.value = false;
}
async function initGradient() {
if (gradient || isInitializing || !shouldUseLiveGradient()) return;
const token = initToken;
isInitializing = true;
try {
const { NeatGradient } = await import("@firecms/neat");
if (token !== initToken || !canvasRef.value || !shouldUseLiveGradient()) return;
gradient = new NeatGradient({
ref: canvasRef.value,
...montereyConfig,
resolution: window.devicePixelRatio > 1 ? 0.24 : 0.34,
});
revealTimer = window.setTimeout(() => {
revealTimer = null;
if (token === initToken && gradient && shouldUseLiveGradient()) {
isLive.value = true;
}
}, 180);
} catch (error) {
console.warn("Monterey hero background is unavailable", error);
destroyGradient();
} finally {
isInitializing = false;
}
}
function syncGradient() {
if (shouldUseLiveGradient()) {
void initGradient();
return;
}
destroyGradient();
}
onMounted(() => {
motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
mobileQuery = window.matchMedia("(max-width: 700px)");
motionQuery.addEventListener("change", syncGradient);
mobileQuery.addEventListener("change", syncGradient);
heroObserver = new IntersectionObserver(
([entry]) => {
isVisible = Boolean(entry?.isIntersecting);
syncGradient();
},
{ rootMargin: "160px 0px", threshold: 0.01 },
);
const target = canvasRef.value?.closest(".cyber-hero");
if (target) heroObserver.observe(target);
});
onBeforeUnmount(() => {
heroObserver?.disconnect();
motionQuery?.removeEventListener("change", syncGradient);
mobileQuery?.removeEventListener("change", syncGradient);
destroyGradient();
});
</script>
<template>
<div
class="cyber-hero__monterey"
:class="{ 'cyber-hero__monterey--live': isLive }"
aria-hidden="true"
>
<canvas ref="canvasRef" class="cyber-hero__monterey-canvas" />
</div>
</template>

View file

@ -0,0 +1,25 @@
<script setup lang="ts">
import type { CSSProperties } from "vue";
const props = withDefaults(defineProps<{
variant?: "sender" | "receiver" | "static";
role?: string | null;
bubbleStyle?: CSSProperties;
}>(), {
variant: "sender",
role: null,
bubbleStyle: undefined,
});
const bubbleClasses = computed(() => [
"cyber-message",
`cyber-message--${props.variant}`,
props.role ? `cyber-message--role-${props.role}` : null,
]);
</script>
<template>
<div :class="bubbleClasses" :style="bubbleStyle">
<slot />
</div>
</template>

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import robotAvatarCyan from "~/assets/images/hero/robots/robot-avatar-cyan-v1.webp";
import robotLeadLounge from "~/assets/images/footer/robot-lead-lounge-v1.webp";
const { t, locale } = useI18n();
const { repoUrl } = useGithubRepo();
@ -13,14 +13,30 @@ const docsHref = computed(() => {
<template>
<footer class="app-footer">
<img
class="app-footer__robot"
:src="robotAvatarCyan"
alt=""
loading="lazy"
decoding="async"
aria-hidden="true"
>
<div class="app-footer__robot-stage">
<span class="app-footer__robot-bubble">
<svg
class="app-footer__robot-bubble-shape"
viewBox="0 0 92 62"
aria-hidden="true"
focusable="false"
>
<path
class="app-footer__robot-bubble-fill"
d="M18 5H58C73 5 84 14 84 27C84 40 73 47 59 47H52L61 58L39 47H18C9 47 4 38 4 26C4 14 9 5 18 5Z"
/>
</svg>
<span class="app-footer__robot-bubble-text">{{ t('footer.robotBubble') }}</span>
</span>
<img
class="app-footer__robot"
:src="robotLeadLounge"
alt=""
loading="lazy"
decoding="async"
draggable="false"
>
</div>
<v-container class="app-footer__inner">
<span class="app-footer__copy"
>{{ t('footer.copyright', { year }) }} · {{ t('footer.tagline') }}</span
@ -44,20 +60,73 @@ const docsHref = computed(() => {
isolation: isolate;
}
.app-footer__robot {
.app-footer__robot-stage {
position: absolute;
right: clamp(22px, 9vw, 148px);
bottom: calc(100% - 4px);
right: clamp(24px, 7vw, 112px);
bottom: calc(100% - 5px);
z-index: 2;
width: clamp(76px, 6.2vw, 112px);
height: auto;
width: clamp(178px, 16vw, 236px);
pointer-events: none;
user-select: none;
transform: translateY(14px) rotate(-2deg);
transform-origin: center bottom;
transform: translateY(3px) rotate(-1deg);
transform-origin: 54% bottom;
filter:
drop-shadow(0 16px 22px rgba(0, 0, 0, 0.54))
drop-shadow(0 0 18px rgba(0, 234, 255, 0.26));
drop-shadow(0 14px 18px rgba(0, 0, 0, 0.52))
drop-shadow(0 0 14px rgba(130, 255, 0, 0.2));
}
.app-footer__robot {
display: block;
width: 100%;
height: auto;
}
.app-footer__robot-bubble {
position: absolute;
top: -28px;
left: -18px;
z-index: 3;
display: block;
width: 72px;
height: 49px;
color: #07111d;
font-family: var(--at-font-mono);
font-size: 0.62rem;
font-weight: 900;
line-height: 1;
letter-spacing: 0;
white-space: nowrap;
text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.62);
transform: rotate(-2deg);
transform-origin: 72% 74%;
filter:
drop-shadow(0 3px 0 rgba(0, 0, 0, 0.18))
drop-shadow(0 0 9px rgba(255, 215, 0, 0.14));
}
.app-footer__robot-bubble-shape {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
overflow: visible;
}
.app-footer__robot-bubble-fill {
fill: #fff09a;
stroke: #050816;
stroke-width: 4.6;
stroke-linejoin: round;
stroke-linecap: round;
}
.app-footer__robot-bubble-text {
position: absolute;
top: 11px;
left: 0;
z-index: 3;
width: 54px;
text-align: center;
}
.app-footer__inner {
@ -119,7 +188,7 @@ const docsHref = computed(() => {
}
@media (max-width: 600px) {
.app-footer__robot {
.app-footer__robot-stage {
display: none;
}

View file

@ -2,6 +2,9 @@
import robotAvatarCyan from "~/assets/images/hero/robots/robot-avatar-cyan-v1.webp";
const { t } = useI18n()
const comparisonRobotRef = ref<HTMLElement | null>(null)
const showComparisonRobotBubble = ref(false)
let comparisonRobotObserver: IntersectionObserver | null = null
interface CellValue {
status: string
@ -245,6 +248,30 @@ const sourceLinks = [
{ label: 'Claude pricing', href: 'https://claude.com/pricing' },
]
onMounted(() => {
if (!comparisonRobotRef.value) return
comparisonRobotObserver = new IntersectionObserver(
([entry]) => {
if (!entry?.isIntersecting) return
showComparisonRobotBubble.value = true
comparisonRobotObserver?.disconnect()
comparisonRobotObserver = null
},
{
rootMargin: '0px 0px -12% 0px',
threshold: 0.35,
},
)
comparisonRobotObserver.observe(comparisonRobotRef.value)
})
onUnmounted(() => {
comparisonRobotObserver?.disconnect()
comparisonRobotObserver = null
})
function getCellClass(cell: CellValue): string {
switch (cell.status) {
case 'yes': return 'comparison-table__cell--yes'
@ -284,14 +311,28 @@ function getStatusIcon(status: string): string {
</div>
<div class="comparison-table__wrap">
<img
<span
ref="comparisonRobotRef"
class="comparison-table__robot"
:src="robotAvatarCyan"
alt=""
loading="lazy"
decoding="async"
aria-hidden="true"
>
<Transition name="comparison-robot-bubble">
<span
v-if="showComparisonRobotBubble"
class="comparison-table__robot-bubble"
>
{{ t("comparison.robotBubble") }}
</span>
</Transition>
<img
class="comparison-table__robot-image"
:src="robotAvatarCyan"
alt=""
loading="lazy"
decoding="async"
draggable="false"
>
</span>
<table class="comparison-table">
<thead>
<tr>
@ -422,17 +463,139 @@ function getStatusIcon(status: string): string {
height: auto;
pointer-events: none;
user-select: none;
transform: translateY(14px) scaleX(-1) rotate(2deg);
transform: translateY(4px) rotate(-0.5deg);
transform-origin: center bottom;
animation: comparisonRobotIdle 5.2s ease-in-out infinite;
filter:
drop-shadow(0 18px 22px rgba(0, 0, 0, 0.5))
drop-shadow(0 0 18px rgba(0, 234, 255, 0.26));
}
.comparison-table__robot-image {
display: block;
width: 100%;
height: auto;
transform:
scaleX(-1)
rotate(2deg);
transform-origin: center bottom;
user-select: none;
}
.comparison-table__robot::selection {
background: transparent;
}
.comparison-table__robot-bubble {
position: absolute;
top: 10px;
right: calc(100% + 12px);
z-index: 5;
display: inline-flex;
align-items: center;
min-height: 30px;
padding: 6px 10px;
color: #07111d;
font-family: var(--at-font-mono);
font-size: 0.66rem;
font-weight: 900;
line-height: 1;
letter-spacing: 0;
white-space: nowrap;
background:
radial-gradient(circle at 26% 22%, rgba(255, 255, 255, 0.88), rgba(255, 244, 168, 0.86) 66%, rgba(255, 215, 0, 0.84) 100%);
border: 2px solid #050816;
border-radius: 999px;
box-shadow:
0 0 0 1px rgba(255, 215, 0, 0.28),
0 5px 0 rgba(0, 0, 0, 0.2),
0 0 12px rgba(255, 215, 0, 0.14);
text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.62);
transform: rotate(-5deg);
transform-origin: right bottom;
animation: comparisonRobotBubbleFloat 2.6s ease-in-out 0.42s infinite;
}
.comparison-table__robot-bubble::before {
position: absolute;
top: 52%;
right: -30px;
width: 32px;
height: 18px;
content: "";
background: #050816;
clip-path: polygon(0 0, 100% 50%, 0 100%);
transform: translateY(-50%);
}
.comparison-table__robot-bubble::after {
position: absolute;
top: 52%;
right: -24px;
width: 26px;
height: 12px;
content: "";
background: rgba(255, 226, 78, 0.96);
clip-path: polygon(0 0, 100% 50%, 0 100%);
transform: translateY(-50%);
}
.comparison-robot-bubble-enter-active,
.comparison-robot-bubble-leave-active {
transition:
opacity 0.26s ease,
filter 0.26s ease;
}
.comparison-robot-bubble-enter-active {
animation: comparisonRobotBubblePop 0.52s cubic-bezier(0.18, 0.9, 0.2, 1.24);
}
.comparison-robot-bubble-enter-from,
.comparison-robot-bubble-leave-to {
opacity: 0;
filter: blur(2px);
}
@keyframes comparisonRobotIdle {
0%,
100% {
transform: translate3d(0, 4px, 0) rotate(-0.55deg);
}
50% {
transform: translate3d(1px, 3px, 0) rotate(0.75deg);
}
}
@keyframes comparisonRobotBubblePop {
0% {
opacity: 0;
transform: translate3d(14px, 18px, 0) scale(0.48) rotate(-13deg);
}
58% {
opacity: 1;
transform: translate3d(-3px, -4px, 0) scale(1.1) rotate(-4deg);
}
100% {
opacity: 1;
transform: translate3d(0, 0, 0) scale(1) rotate(-5deg);
}
}
@keyframes comparisonRobotBubbleFloat {
0%,
100% {
transform: translate3d(0, 0, 0) rotate(-5deg);
}
50% {
transform: translate3d(0, -2px, 0) rotate(-4deg);
}
}
.comparison-section__sources {
max-width: 1040px;
margin: 18px auto 0;

View file

@ -9,7 +9,7 @@ const { t, locale } = useI18n();
const downloadStore = useDownloadStore();
const { data: releaseData, resolve } = useReleaseDownloads();
const { trackDownloadClick } = useAnalytics();
const { repoUrl, releaseDownloadUrl } = useGithubRepo();
const { releaseDownloadUrl } = useGithubRepo();
const isMounted = ref(false);
const showLinuxRobotMessage = ref(false);
const showFallingLinuxRobot = ref(false);
@ -21,6 +21,7 @@ let linuxRobotObserver: IntersectionObserver | null = null;
let linuxRobotFallRaf = 0;
let linuxRobotFallTimer: number | null = null;
let lastLinuxRobotScrollY = 0;
let faqLandingResizeObserver: ResizeObserver | null = null;
function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value));
@ -64,8 +65,8 @@ function getLinuxRobotFallMetrics() {
const target = getPageRect(faqTarget);
const robotWidth = clamp(sourceViewport.width, 92, 112);
const robotHeight = sourceViewport.height * (robotWidth / sourceViewport.width);
const landedPageX = target.left + target.width * 0.3;
const landedPageY = target.top + target.height * 0.08 - robotHeight * 0.62;
const landedPageX = target.left + target.width * 0.5 - robotWidth * 0.5;
const landedPageY = target.top - robotHeight * 0.58;
return {
sourceViewport,
@ -151,6 +152,11 @@ function updateLinuxRobotFall() {
const viewportHeight = window.innerHeight;
const startScroll = metrics.download.bottom - viewportHeight * 0.72;
if (linuxRobotFlightState.value === 'landed') {
fallingLinuxRobotStyle.value = getLinuxRobotLandedStyle(metrics);
return;
}
if (currentScrollY < startScroll) {
resetLinuxRobotFall({ keepSourceHidden: hasLinuxRobotDeparted.value });
return;
@ -165,10 +171,7 @@ function updateLinuxRobotFall() {
if (linuxRobotFlightState.value === 'idle') {
launchLinuxRobotFall(metrics);
return;
}
if (linuxRobotFlightState.value === 'landed') return;
}
onMounted(() => {
@ -177,6 +180,7 @@ onMounted(() => {
nextTick(() => {
const linuxCard = document.querySelector<HTMLElement>('[data-download-os="linux"]');
const faqTarget = document.querySelector<HTMLElement>('[data-faq-landing-target]');
if (!linuxCard) return;
linuxRobotObserver = new IntersectionObserver(
@ -193,21 +197,31 @@ onMounted(() => {
);
linuxRobotObserver.observe(linuxCard);
if (faqTarget) {
faqLandingResizeObserver = new ResizeObserver(scheduleLinuxRobotFallUpdate);
faqLandingResizeObserver.observe(faqTarget);
}
scheduleLinuxRobotFallUpdate();
});
window.addEventListener('scroll', scheduleLinuxRobotFallUpdate, { passive: true });
window.addEventListener('resize', scheduleLinuxRobotFallUpdate);
window.visualViewport?.addEventListener('resize', scheduleLinuxRobotFallUpdate);
});
onUnmounted(() => {
linuxRobotObserver?.disconnect();
linuxRobotObserver = null;
faqLandingResizeObserver?.disconnect();
faqLandingResizeObserver = null;
if (linuxRobotFallRaf) window.cancelAnimationFrame(linuxRobotFallRaf);
linuxRobotFallRaf = 0;
clearLinuxRobotFallTimer();
window.removeEventListener('scroll', scheduleLinuxRobotFallUpdate);
window.removeEventListener('resize', scheduleLinuxRobotFallUpdate);
window.visualViewport?.removeEventListener('resize', scheduleLinuxRobotFallUpdate);
});
const platformIcons: Record<string, string> = {
@ -262,12 +276,6 @@ const releaseDate = computed(() => {
});
});
const devBranchUrl = computed(() => `${repoUrl.value}/tree/dev`);
const devBranchNote = computed(() =>
locale.value === 'ru'
? 'Самая свежая версия доступна в ветке dev - можно развернуть локально.'
: 'Freshest version is available on the dev branch - clone and run it locally.',
);
</script>
<template>
@ -367,15 +375,6 @@ const devBranchNote = computed(() =>
</div>
</div>
<a
class="download-section__dev-note"
:href="devBranchUrl"
target="_blank"
rel="noopener"
>
{{ devBranchNote }}
</a>
<p v-if="isMounted && releaseVersion" class="download-section__release-info">
v{{ releaseVersion }} · {{ releaseDate }}
</p>
@ -578,7 +577,7 @@ const devBranchNote = computed(() =>
opacity: 0.96;
transform:
translate3d(var(--fall-x), var(--fall-y), 0)
rotate(-5deg)
rotate(355deg)
scale(0.95);
}
}
@ -887,38 +886,6 @@ const devBranchNote = computed(() =>
font-family: 'JetBrains Mono', monospace;
}
.download-section__dev-note {
display: flex;
width: fit-content;
max-width: min(620px, calc(100vw - 32px));
margin: 18px auto 0;
padding: 8px 12px;
border: 1px solid rgba(0, 240, 255, 0.12);
border-radius: 10px;
background: rgba(0, 240, 255, 0.035);
color: #00f0ff;
font-family: 'JetBrains Mono', monospace;
font-size: 0.76rem;
line-height: 1.55;
text-align: center;
text-decoration: none;
opacity: 0.82;
position: relative;
z-index: 1;
transition:
border-color 0.2s ease,
background 0.2s ease,
color 0.2s ease,
opacity 0.2s ease;
}
.download-section__dev-note:hover {
border-color: rgba(57, 255, 20, 0.24);
background: rgba(57, 255, 20, 0.045);
color: #39ff14;
opacity: 1;
}
@keyframes downloadFadeUp {
from {
opacity: 0;

View file

@ -25,6 +25,14 @@ const withBase = (path: string) => `${baseURL.replace(/\/?$/, "/")}${path.replac
useCyberHeroParallax(heroRef);
const releaseVersion = computed(() => releaseData.value?.version || null);
const releaseDate = computed(() => {
if (!releaseData.value?.pubDate) return "";
return new Date(releaseData.value.pubDate).toLocaleDateString(locale.value, {
year: "numeric",
month: "short",
day: "numeric",
});
});
const activeHeroMessage = computed(() => heroMessages[activeHeroMessageIndex.value] ?? null);
const heroDownloadUrl = computed(() => {
@ -114,6 +122,7 @@ onUnmounted(() => {
<template>
<section id="hero" ref="heroRef" class="hero-section cyber-hero section anchor-offset" data-cyber-hero>
<CyberHeroMontereyBackground />
<div class="cyber-hero__background" aria-hidden="true" />
<div class="cyber-hero__wash" aria-hidden="true" />
<div class="cyber-hero__gridlines" aria-hidden="true" />
@ -163,6 +172,9 @@ onUnmounted(() => {
>
<span class="cyber-hero__release">
v{{ releaseVersion }}
<span v-if="releaseDate" class="cyber-hero__release-date">
· {{ releaseDate }}
</span>
</span>
</p>
</div>

View file

@ -1,7 +1,9 @@
import { computed, getCurrentInstance, onUnmounted, watch } from "vue";
import { computed, getCurrentInstance, onMounted, onUnmounted, ref, watch } from "vue";
import type { Ref } from "vue";
import { useThemeStore } from "~/stores/theme";
type ThemeName = "light" | "dark";
type VuetifyThemeInstance = {
global: {
name: Ref<string>;
@ -10,40 +12,109 @@ type VuetifyThemeInstance = {
change?: (name: string) => void;
};
function isThemeName(value: string | null | undefined): value is ThemeName {
return value === "dark" || value === "light";
}
export const useBrowserTheme = () => {
const themeStore = useThemeStore();
const { $vuetifyTheme } = useNuxtApp();
const vuetifyTheme = $vuetifyTheme as VuetifyThemeInstance | null;
const documentTheme = ref<ThemeName | null>(null);
let mediaQueryHandler: ((event: MediaQueryListEvent) => void) | null = null;
let mediaQuery: MediaQueryList | null = null;
let themeClassObserver: MutationObserver | null = null;
const applyVuetifyTheme = (name: "light" | "dark") => {
if (!vuetifyTheme) return;
if (typeof vuetifyTheme.change === "function") {
vuetifyTheme.change(name);
} else {
vuetifyTheme.global.name.value = name;
}
const getDocumentTheme = (): ThemeName | null => {
if (!import.meta.client) return null;
const appClass = document.querySelector(".v-application")?.classList;
if (appClass?.contains("v-theme--dark")) return "dark";
if (appClass?.contains("v-theme--light")) return "light";
return null;
};
const applyTheme = (name: "light" | "dark") => {
themeStore.setTheme(name, true);
const refreshDocumentTheme = () => {
documentTheme.value = getDocumentTheme();
return documentTheme.value;
};
const applyDocumentTheme = (name: ThemeName) => {
if (!import.meta.client) return;
document.querySelectorAll(".v-application").forEach((app) => {
app.classList.toggle("v-theme--dark", name === "dark");
app.classList.toggle("v-theme--light", name === "light");
});
documentTheme.value = name;
};
const getAppliedTheme = (): ThemeName => {
const domTheme = getDocumentTheme() ?? documentTheme.value;
if (domTheme) return domTheme;
const vuetifyName = vuetifyTheme?.global.name.value;
if (isThemeName(vuetifyName)) return vuetifyName;
return themeStore.current;
};
const syncStoreFromAppliedTheme = () => {
const appliedTheme = getAppliedTheme();
if (themeStore.current !== appliedTheme) {
themeStore.setTheme(appliedTheme, false);
}
return appliedTheme;
};
const applyVuetifyTheme = (name: ThemeName) => {
if (!vuetifyTheme) return;
if (vuetifyTheme.change) {
vuetifyTheme.change(name);
return;
}
vuetifyTheme.global.name.value = name;
};
const applyTheme = (name: ThemeName, fromUser = true) => {
applyVuetifyTheme(name);
applyDocumentTheme(name);
themeStore.setTheme(name, fromUser);
return name;
};
const observeDocumentTheme = () => {
if (!import.meta.client || themeClassObserver) return;
const app = document.querySelector(".v-application");
if (!app) return;
refreshDocumentTheme();
themeClassObserver = new MutationObserver(() => {
refreshDocumentTheme();
});
themeClassObserver.observe(app, { attributes: true, attributeFilter: ["class"] });
};
const initTheme = () => {
if (!import.meta.client) return;
const initialTheme = themeStore.getInitialTheme();
themeStore.setTheme(initialTheme, false);
applyVuetifyTheme(initialTheme);
applyTheme(initialTheme, false);
if (mediaQuery && mediaQueryHandler) {
mediaQuery.removeEventListener("change", mediaQueryHandler);
mediaQuery = null;
mediaQueryHandler = null;
}
if (!themeStore.userSelected) {
mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
mediaQueryHandler = (event: MediaQueryListEvent) => {
if (!themeStore.userSelected) {
const newTheme = event.matches ? "dark" : "light";
themeStore.setTheme(newTheme, false);
applyVuetifyTheme(newTheme);
applyTheme(newTheme, false);
}
};
mediaQuery.addEventListener("change", mediaQueryHandler);
@ -51,27 +122,54 @@ export const useBrowserTheme = () => {
};
const toggleTheme = () => {
applyTheme(themeStore.current === "dark" ? "light" : "dark");
const appliedTheme = syncStoreFromAppliedTheme();
return applyTheme(appliedTheme === "dark" ? "light" : "dark");
};
if (getCurrentInstance()) {
onMounted(() => {
refreshDocumentTheme();
observeDocumentTheme();
});
onUnmounted(() => {
if (mediaQuery && mediaQueryHandler) {
mediaQuery.removeEventListener("change", mediaQueryHandler);
}
themeClassObserver?.disconnect();
});
}
watch(
() => themeStore.current,
(value) => {
applyVuetifyTheme(value as "light" | "dark");
applyVuetifyTheme(value);
}
);
if (vuetifyTheme) {
watch(
() => vuetifyTheme.global.name.value,
(value) => {
if (isThemeName(value) && themeStore.current !== value) {
themeStore.setTheme(value, false);
}
}
);
}
const currentTheme = computed(() => {
if (documentTheme.value) return documentTheme.value;
const vuetifyName = vuetifyTheme?.global.name.value;
return isThemeName(vuetifyName) ? vuetifyName : themeStore.current;
});
const isDark = computed(() => currentTheme.value === "dark");
return {
currentTheme: computed(() => themeStore.current),
isDark: computed(() => themeStore.current === "dark"),
currentTheme,
isDark,
initTheme,
toggleTheme
};

View file

@ -1,6 +1,8 @@
import robotAvatarCyan from "~/assets/images/hero/robots/robot-avatar-cyan-v1.webp";
import robotAvatarMagenta from "~/assets/images/hero/robots/robot-avatar-magenta-v1.webp";
import robotAvatarCyan from "~/assets/images/hero/robots/robot-avatar-cyan-cat-v1.webp";
import robotAvatarReviewerTeal from "~/assets/images/hero/robots/robot-avatar-reviewer-teal-v1.webp";
import robotAvatarSeatedMagenta from "~/assets/images/hero/robots/robot-avatar-seated-magenta-v1.webp";
import robotAvatarYellow from "~/assets/images/hero/robots/robot-avatar-yellow-star-v1.webp";
import robotRedPurpleHandshake from "~/assets/images/hero/robots/robot-red-purple-handshake-v1.webp";
export const HERO_SCENE_BREAKPOINTS = {
desktop: 1200,
@ -95,8 +97,8 @@ export const heroAgents: readonly HeroAgent[] = [
{
id: "developer",
label: "Developer",
asset: robotAvatarMagenta,
accent: "magenta",
asset: robotAvatarYellow,
accent: "amber",
facing: 1,
lean: -1,
priority: true,
@ -110,12 +112,16 @@ export const heroAgents: readonly HeroAgent[] = [
export const heroReviewerFeatureCard = {
label: "Reviewer",
asset: robotAvatarMagenta,
accent: "magenta",
asset: robotAvatarReviewerTeal,
accent: "cyan",
status: "Reviewing",
tasks: ["Review code", "Check quality", "Request changes"],
} as const;
export const heroCollaborationFeature = {
asset: robotRedPurpleHandshake,
} as const;
export const heroMessages: readonly HeroMessage[] = [
{
id: "plan-ready",

View file

@ -64,6 +64,7 @@
"sectionTitle": "كيف نقارن",
"sectionSubtitle": "مقارنة تفصيلية للمميزات مع أدوات البرمجة بالذكاء الاصطناعي الأخرى.",
"feature": "الميزة",
"robotBubble": "احكم بنفسك",
"features": {
"crossTeam": "التواصل بين الفرق",
"agentMessaging": "مراسلة بين الوكلاء",
@ -98,6 +99,7 @@
"footer": {
"copyright": "© {year} Agent Teams",
"tagline": "تنسيق وكلاء الذكاء الاصطناعي للمطورين",
"robotBubble": "أنا أنتظر",
"links": {
"github": "GitHub",
"docs": "التوثيق"

View file

@ -64,6 +64,7 @@
"sectionTitle": "Wie wir im Vergleich abschneiden",
"sectionSubtitle": "Funktionsvergleich mit anderen KI-Coding-Tools.",
"feature": "Funktion",
"robotBubble": "Urteile selbst",
"features": {
"crossTeam": "Teamübergreifende Kommunikation",
"agentMessaging": "Agent-zu-Agent-Messaging",
@ -98,6 +99,7 @@
"footer": {
"copyright": "© {year} Agent Teams",
"tagline": "KI-Agenten-Orchestrierung für Entwickler",
"robotBubble": "Ich warte",
"links": {
"github": "GitHub",
"docs": "Dokumentation"

View file

@ -64,6 +64,7 @@
"sectionTitle": "How we compare",
"sectionSubtitle": "Feature-by-feature comparison with other AI coding tools.",
"feature": "Feature",
"robotBubble": "Judge for yourself",
"features": {
"crossTeam": "Cross-team communication",
"agentMessaging": "Agent-to-agent messaging",
@ -98,6 +99,7 @@
"footer": {
"copyright": "© {year} Agent Teams",
"tagline": "AI agent orchestration for developers",
"robotBubble": "I'm waiting",
"links": {
"github": "GitHub",
"docs": "Documentation"

View file

@ -64,6 +64,7 @@
"sectionTitle": "Cómo nos comparamos",
"sectionSubtitle": "Comparación detallada de funciones con otras herramientas de programación con IA.",
"feature": "Función",
"robotBubble": "Juzga tú",
"features": {
"crossTeam": "Comunicación entre equipos",
"agentMessaging": "Mensajería entre agentes",
@ -98,6 +99,7 @@
"footer": {
"copyright": "© {year} Agent Teams",
"tagline": "Orquestación de agentes IA para desarrolladores",
"robotBubble": "Estoy esperando",
"links": {
"github": "GitHub",
"docs": "Documentación"

View file

@ -64,6 +64,7 @@
"sectionTitle": "Comment nous nous comparons",
"sectionSubtitle": "Comparaison fonctionnalité par fonctionnalité avec d'autres outils IA.",
"feature": "Fonctionnalité",
"robotBubble": "Juge par toi-même",
"features": {
"crossTeam": "Communication inter-équipes",
"agentMessaging": "Messagerie entre agents",
@ -98,6 +99,7 @@
"footer": {
"copyright": "© {year} Agent Teams",
"tagline": "Orchestration d'agents IA pour développeurs",
"robotBubble": "J'attends",
"links": {
"github": "GitHub",
"docs": "Documentation"

View file

@ -64,6 +64,7 @@
"sectionTitle": "तुलना करें",
"sectionSubtitle": "अन्य AI कोडिंग टूल्स के साथ सुविधा-दर-सुविधा तुलना।",
"feature": "सुविधा",
"robotBubble": "खुद फैसला करें",
"features": {
"crossTeam": "क्रॉस-टीम संचार",
"agentMessaging": "एजेंट-टू-एजेंट मैसेजिंग",
@ -98,6 +99,7 @@
"footer": {
"copyright": "© {year} Agent Teams",
"tagline": "डेवलपर्स के लिए AI एजेंट ऑर्केस्ट्रेशन",
"robotBubble": "मैं इंतज़ार कर रहा हूँ",
"links": {
"github": "GitHub",
"docs": "दस्तावेज़"

View file

@ -64,6 +64,7 @@
"sectionTitle": "他ツールとの比較",
"sectionSubtitle": "他のAIコーディングツールとの機能比較。",
"feature": "機能",
"robotBubble": "自分で判断して",
"features": {
"crossTeam": "チーム間コミュニケーション",
"agentMessaging": "エージェント間メッセージング",
@ -98,6 +99,7 @@
"footer": {
"copyright": "© {year} Agent Teams",
"tagline": "開発者向けAIエージェントオーケストレーション",
"robotBubble": "待ってるよ",
"links": {
"github": "GitHub",
"docs": "ドキュメント"

View file

@ -64,6 +64,7 @@
"sectionTitle": "Como nos comparamos",
"sectionSubtitle": "Comparação detalhada de recursos com outras ferramentas de programação com IA.",
"feature": "Recurso",
"robotBubble": "Julgue você",
"features": {
"crossTeam": "Comunicação entre equipes",
"agentMessaging": "Mensagens entre agentes",
@ -98,6 +99,7 @@
"footer": {
"copyright": "© {year} Agent Teams",
"tagline": "Orquestração de agentes IA para desenvolvedores",
"robotBubble": "Estou esperando",
"links": {
"github": "GitHub",
"docs": "Documentação"

View file

@ -64,6 +64,7 @@
"sectionTitle": "Сравнение с конкурентами",
"sectionSubtitle": "Подробное сравнение возможностей с другими AI-инструментами для разработки.",
"feature": "Возможность",
"robotBubble": "Суди сам",
"features": {
"crossTeam": "Межкомандная коммуникация",
"agentMessaging": "Обмен сообщениями между агентами",
@ -98,6 +99,7 @@
"footer": {
"copyright": "© {year} Agent Teams",
"tagline": "Оркестрация ИИ-агентов для разработчиков",
"robotBubble": "Я жду",
"links": {
"github": "GitHub",
"docs": "Документация"

View file

@ -64,6 +64,7 @@
"sectionTitle": "功能对比",
"sectionSubtitle": "与其他 AI 编程工具的逐项功能对比。",
"feature": "功能",
"robotBubble": "你来判断",
"features": {
"crossTeam": "跨团队通信",
"agentMessaging": "智能体间消息",
@ -98,6 +99,7 @@
"footer": {
"copyright": "© {year} Agent Teams",
"tagline": "面向开发者的 AI 智能体编排",
"robotBubble": "我在等你",
"links": {
"github": "GitHub",
"docs": "文档"

View file

@ -6,6 +6,7 @@
"": {
"name": "agent-teams-landing",
"dependencies": {
"@firecms/neat": "^0.8.0",
"@mdi/js": "^7.4.47",
"@nuxtjs/i18n": "^9.5.6",
"@pinia/nuxt": "^0.11.3",
@ -1314,6 +1315,12 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@firecms/neat": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@firecms/neat/-/neat-0.8.0.tgz",
"integrity": "sha512-gwvYd63voJa+ZtEt6SW3toJwVx9smisKuXE7vsXvZtlGPzsWpR1lzaltIsijuPkg8Qj/ybS2tEdsttWE7g2KsA==",
"license": "MIT AND Commons-Clause"
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",

View file

@ -18,6 +18,7 @@
"format:fix": "prettier . --write"
},
"dependencies": {
"@firecms/neat": "^0.8.0",
"@mdi/js": "^7.4.47",
"@nuxtjs/i18n": "^9.5.6",
"@pinia/nuxt": "^0.11.3",

View file

@ -5,7 +5,8 @@ export default defineNuxtPlugin({
const { initTheme } = useBrowserTheme();
const { initLocale } = useLocation();
// Run after hydration to avoid SSR/CSR mismatches.
initTheme();
nuxtApp.hook("app:mounted", () => {
initTheme();
initLocale();

View file

@ -2,6 +2,8 @@ import "vuetify/styles";
import { createVuetify } from "vuetify";
import { aliases, mdi } from "vuetify/iconsets/mdi-svg";
type ThemeName = "light" | "dark";
const brand = {
cyan: "#00f0ff",
magenta: "#ff00ff",
@ -11,9 +13,28 @@ const brand = {
darkSurface: "#12121a"
};
function isThemeName(value: string | null | undefined): value is ThemeName {
return value === "dark" || value === "light";
}
function resolveInitialTheme(cookieTheme: ThemeName | null): ThemeName {
if (import.meta.client) {
const saved = localStorage.getItem("theme");
if (isThemeName(saved)) return saved;
if (cookieTheme) return cookieTheme;
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
return cookieTheme ?? "light";
}
export default defineNuxtPlugin({
name: "vuetify",
setup(nuxtApp) {
const themeCookie = useCookie<ThemeName | null>("theme");
const cookieTheme = isThemeName(themeCookie.value) ? themeCookie.value : null;
const defaultTheme = resolveInitialTheme(cookieTheme);
const vuetify = createVuetify({
icons: {
defaultSet: "mdi",
@ -21,7 +42,7 @@ export default defineNuxtPlugin({
sets: { mdi }
},
theme: {
defaultTheme: "dark",
defaultTheme,
themes: {
light: {
colors: {

View file

@ -1,30 +1,60 @@
import { defineStore } from "pinia";
type ThemeName = "light" | "dark";
const themeCookieName = "theme";
function isThemeName(value: string | null | undefined): value is ThemeName {
return value === "dark" || value === "light";
}
function getCookieTheme(): ThemeName | null {
if (!import.meta.client) return null;
const cookie = document.cookie
.split("; ")
.find((item) => item.startsWith(`${themeCookieName}=`));
const value = cookie ? decodeURIComponent(cookie.split("=").slice(1).join("=")) : null;
return isThemeName(value) ? value : null;
}
function persistTheme(theme: ThemeName) {
localStorage.setItem(themeCookieName, theme);
document.cookie = `${themeCookieName}=${theme}; Path=/; Max-Age=31536000; SameSite=Lax`;
}
export const useThemeStore = defineStore("theme", {
state: () => ({
current: "dark" as ThemeName,
current: "light" as ThemeName,
userSelected: false
}),
actions: {
getInitialTheme(): ThemeName {
if (!import.meta.client) return "dark";
const saved = localStorage.getItem("theme");
if (saved === "dark" || saved === "light") {
if (!import.meta.client) return "light";
const saved = localStorage.getItem(themeCookieName);
if (isThemeName(saved)) {
this.userSelected = true;
persistTheme(saved);
return saved;
}
const cookieTheme = getCookieTheme();
if (cookieTheme) {
this.userSelected = true;
persistTheme(cookieTheme);
return cookieTheme;
}
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark";
}
return "dark";
return "light";
},
setTheme(theme: ThemeName, fromUser: boolean) {
this.current = theme;
if (import.meta.client && fromUser) {
this.userSelected = true;
localStorage.setItem("theme", theme);
persistTheme(theme);
}
}
}