style: refine landing hero presentation

Adjust the landing hero, header, demo video, and Monterey background styling for the updated cyberpunk presentation without mixing in dependency or workflow changes.
This commit is contained in:
777genius 2026-05-24 00:23:23 +03:00
parent edcb0a7433
commit ec60c244a3
4 changed files with 318 additions and 18 deletions

View file

@ -1819,10 +1819,6 @@
scale(var(--agent-tablet-scale));
}
.cyber-agent__card {
display: none;
}
.cyber-feature-rail-shell {
width: 100%;
}
@ -1864,6 +1860,88 @@
}
}
@media (min-width: 768px) and (max-width: 1100px) {
.cyber-agent[data-agent="planner"] {
transform:
translate3d(-50%, -100%, 0)
scale(var(--agent-tablet-scale));
}
.cyber-agent[data-agent="planner"] .cyber-agent__float {
top: 4px;
transform-origin: center bottom;
}
.cyber-agent[data-agent="planner"] .cyber-agent__image {
transform:
scaleX(var(--agent-face))
rotate(var(--agent-lean));
}
.cyber-agent[data-agent="planner"] .cyber-agent__contact {
left: 17%;
right: 17%;
bottom: 18px;
height: 14px;
}
.cyber-agent__card {
display: block;
width: 138px;
padding: 8px 9px;
font-size: 0.62rem;
line-height: 1.24;
}
.cyber-agent[data-agent="planner"] .cyber-agent__card,
.cyber-agent[data-agent="lead"] .cyber-agent__card,
.cyber-agent[data-agent="developer"] .cyber-agent__card {
padding: 8px 9px;
font-size: 0.62rem;
}
.cyber-agent__label {
margin-bottom: 4px;
}
.cyber-agent__tasks {
display: none;
}
.cyber-agent__status {
gap: 3px 5px;
margin-top: 4px;
white-space: nowrap;
}
.cyber-agent[data-agent="planner"] .cyber-agent__card {
top: 35%;
right: auto;
left: 100%;
width: 138px;
transform: translate(8px, -12%) scale(2.27);
transform-origin: left top;
}
.cyber-agent[data-agent="lead"] .cyber-agent__card {
top: 12%;
right: auto;
left: 100%;
width: 138px;
transform: translate(8px, -12%) scale(2.38);
transform-origin: left top;
}
.cyber-agent[data-agent="developer"] .cyber-agent__card {
top: 12%;
right: auto;
left: 100%;
width: 128px;
transform: translate(8px, -12%) scale(2.5);
transform-origin: left top;
}
}
@media (max-width: 767px) {
.cyber-hero {
padding: 84px 0 36px;
@ -1889,12 +1967,14 @@
.cyber-hero__layout {
min-width: 0;
gap: 0;
overflow: hidden;
}
.cyber-hero__copy {
width: 100%;
max-width: 100%;
padding-bottom: 0;
}
.cyber-hero__brand-lockup {
@ -1970,14 +2050,14 @@
.cyber-scene {
min-height: auto;
aspect-ratio: auto;
padding: 92px 0 12px;
padding: 96px 0 14px;
transform: none;
}
.cyber-hero__scene {
width: 100%;
max-width: 100%;
margin-top: 18px;
margin-top: 8px;
overflow: hidden;
}
@ -1996,18 +2076,20 @@
.cyber-scene__robots {
inset: 0 0 auto;
height: 92px;
height: 96px;
display: flex;
align-items: flex-end;
justify-content: center;
gap: 10px;
gap: clamp(18px, 6vw, 34px);
}
.cyber-agent {
position: relative;
flex: 0 0 auto;
display: none;
left: auto;
top: auto;
width: 76px;
width: clamp(58px, 18vw, 74px);
transform: none;
}
@ -2015,11 +2097,46 @@
display: block;
}
.cyber-agent[data-agent="planner"] {
z-index: auto;
width: clamp(58px, 18vw, 74px);
transform: none;
}
.cyber-agent[data-agent="planner"] .cyber-agent__float {
top: 4px;
transform-origin: center bottom;
}
.cyber-agent[data-agent="planner"] .cyber-agent__image {
transform:
scaleX(var(--agent-face))
rotate(var(--agent-lean));
}
.cyber-agent[data-agent="planner"] .cyber-agent__contact {
left: 17%;
right: 17%;
bottom: 18px;
height: 14px;
}
.cyber-agent[data-agent="lead"] .cyber-agent__float {
top: 3px;
}
.cyber-agent[data-agent="developer"] .cyber-agent__float {
top: 0;
}
.cyber-agent__float {
animation-duration: 6s;
}
.cyber-agent__card,
.cyber-agent .cyber-agent__card,
.cyber-agent[data-agent="planner"] .cyber-agent__card,
.cyber-agent[data-agent="lead"] .cyber-agent__card,
.cyber-agent[data-agent="developer"] .cyber-agent__card,
.cyber-agent__eyes {
display: none;
}
@ -2034,6 +2151,33 @@
padding: 0 4px;
}
.cyber-feature-rail-shell {
margin-top: clamp(104px, 24vw, 128px);
}
.cyber-feature-rail__collaboration {
left: 31%;
bottom: calc(100% + 8px);
width: clamp(96px, 30vw, 124px);
}
.cyber-feature-rail__reviewer {
--reviewer-robot-width: clamp(58px, 18vw, 72px);
right: clamp(6px, 3vw, 16px);
bottom: calc(100% + 8px);
gap: 0;
}
.cyber-feature-rail__reviewer-card,
.cyber-feature-rail__reviewer-bubble {
display: none;
}
.cyber-feature-rail__robot {
top: 4px;
}
.cyber-feature-rail__item {
grid-template-columns: 48px 44px minmax(0, 1fr);
grid-template-rows: auto;

View file

@ -173,6 +173,7 @@ function shouldUseBackgroundVideo() {
backgroundPlaybackId.value &&
isVisible &&
!motionQuery?.matches &&
!mobileQuery?.matches &&
!hasBackgroundVideoError.value,
);
}
@ -248,7 +249,7 @@ onMounted(() => {
motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
mobileQuery = window.matchMedia("(max-width: 700px)");
motionQuery.addEventListener("change", syncMotionState);
mobileQuery.addEventListener("change", syncGradient);
mobileQuery.addEventListener("change", syncMotionState);
heroObserver = new IntersectionObserver(
([entry]) => {
@ -267,7 +268,7 @@ onMounted(() => {
onBeforeUnmount(() => {
heroObserver?.disconnect();
motionQuery?.removeEventListener("change", syncMotionState);
mobileQuery?.removeEventListener("change", syncGradient);
mobileQuery?.removeEventListener("change", syncMotionState);
stopBackgroundVideo();
destroyGradient();
});

View file

@ -9,6 +9,8 @@ const menuOpen = ref(false);
const withBase = (path: string) => `${baseURL.replace(/\/?$/, '/')}${path.replace(/^\/+/, '')}`;
const docsHref = computed(() => withBase(locale.value === 'ru' ? 'docs/ru/' : 'docs/'));
const isRu = computed(() => locale.value === 'ru');
const openMenuLabel = computed(() => (isRu.value ? 'Открыть меню' : 'Open menu'));
const closeMenuLabel = computed(() => (isRu.value ? 'Закрыть меню' : 'Close menu'));
const navItems = computed(() => [
{ href: '#screenshots', label: t('nav.screenshots'), shortLabel: isRu.value ? 'Скрины' : 'Shots' },
@ -134,7 +136,7 @@ const navItems = computed(() => [
<ThemeToggle />
</div>
<div class="app-header__mobile-actions">
<v-btn :icon="mdiMenu" variant="text" @click="menuOpen = true" />
<v-btn :icon="mdiMenu" variant="text" :aria-label="openMenuLabel" @click="menuOpen = true" />
<Teleport to="body">
<Transition name="mobile-menu-fade">
<div v-if="menuOpen" class="mobile-menu-overlay" @click.self="menuOpen = false">
@ -142,7 +144,13 @@ const navItems = computed(() => [
<div class="mobile-menu__header">
<AppLogo />
<div style="flex: 1" />
<v-btn :icon="mdiClose" variant="text" @click="menuOpen = false" />
<v-btn
:icon="mdiClose"
variant="text"
class="mobile-menu__close"
:aria-label="closeMenuLabel"
@click="menuOpen = false"
/>
</div>
<hr class="mobile-menu__divider">
<nav class="mobile-menu__list">
@ -825,10 +833,43 @@ const navItems = computed(() => [
}
.mobile-menu__header {
position: sticky;
top: 0;
z-index: 2;
display: flex;
align-items: center;
gap: 12px;
padding-bottom: 12px;
padding-bottom: 14px;
background: transparent;
}
.mobile-menu__close {
width: 52px !important;
min-width: 52px !important;
height: 52px !important;
color: var(--cyber-cyan) !important;
border: 1px solid rgba(0, 234, 255, 0.82) !important;
border-radius: 50% !important;
background:
radial-gradient(circle at 50% 50%, rgba(0, 234, 255, 0.18), transparent 58%),
rgba(2, 10, 24, 0.94) !important;
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.08) inset,
0 0 18px rgba(0, 234, 255, 0.38),
0 0 36px rgba(0, 234, 255, 0.16);
}
.mobile-menu__close :deep(.v-icon) {
font-size: 30px;
filter: drop-shadow(0 0 10px rgba(0, 234, 255, 0.6));
}
.mobile-menu__close:hover {
color: #ffffff !important;
border-color: rgba(255, 255, 255, 0.86) !important;
background:
radial-gradient(circle at 50% 50%, rgba(0, 234, 255, 0.28), transparent 62%),
rgba(0, 234, 255, 0.16) !important;
}
.mobile-menu__divider {

View file

@ -29,10 +29,29 @@ const muxPlayerUrl = computed(() => {
url.searchParams.set("video-title", muxVideoTitle.value);
return url.toString();
});
const muxPosterUrl = computed(() => {
if (!muxPlaybackId.value) return "";
const url = new URL(`https://image.mux.com/${encodeURIComponent(muxPlaybackId.value)}/thumbnail.webp`);
url.searchParams.set("time", "0.1");
url.searchParams.set("width", "900");
url.searchParams.set("fit_mode", "preserve");
return url.toString();
});
const isLoaded = ref(false);
const hasError = ref(false);
const isMobileViewport = ref(false);
const playerActivated = ref(false);
const shouldShowMobilePoster = computed(() => (
Boolean(muxPlayerUrl.value) &&
!hasError.value &&
isMobileViewport.value &&
!playerActivated.value
));
const shouldShowPlayer = computed(() => Boolean(muxPlayerUrl.value) && !hasError.value && !shouldShowMobilePoster.value);
let loadFallbackTimer: ReturnType<typeof setTimeout> | null = null;
let mobileQuery: MediaQueryList | null = null;
function clearLoadFallback() {
if (!loadFallbackTimer) return;
@ -51,11 +70,25 @@ function markError() {
clearLoadFallback();
}
function syncMobileViewport() {
isMobileViewport.value = Boolean(mobileQuery?.matches);
}
function activatePlayer() {
playerActivated.value = true;
}
onMounted(() => {
mobileQuery = window.matchMedia("(max-width: 700px)");
syncMobileViewport();
mobileQuery.addEventListener("change", syncMobileViewport);
loadFallbackTimer = setTimeout(markLoaded, 2500);
});
onUnmounted(clearLoadFallback);
onUnmounted(() => {
mobileQuery?.removeEventListener("change", syncMobileViewport);
clearLoadFallback();
});
</script>
<template>
@ -70,7 +103,7 @@ onUnmounted(clearLoadFallback);
<ClientOnly>
<iframe
v-if="muxPlayerUrl && !hasError"
v-if="shouldShowPlayer"
class="hero-video__player"
:class="{ 'hero-video__player--loaded': isLoaded }"
:src="muxPlayerUrl"
@ -94,7 +127,21 @@ onUnmounted(clearLoadFallback);
</template>
</ClientOnly>
<div v-if="!isLoaded && !hasError && muxPlayerUrl" class="hero-video__skeleton">
<button
v-if="shouldShowMobilePoster"
type="button"
class="hero-video__poster"
:style="{ '--hero-video-poster': muxPosterUrl ? `url(${muxPosterUrl})` : 'url(/screenshots/2.jpg)' }"
:aria-label="videoTitle"
@click="activatePlayer"
>
<span class="hero-video__poster-play">
<v-icon :icon="mdiPlay" size="40" />
</span>
<span class="hero-video__poster-label">{{ t("hero.watchDemo") }}</span>
</button>
<div v-if="!isLoaded && !hasError && shouldShowPlayer" class="hero-video__skeleton">
<div class="hero-video__skeleton-pulse" />
<div class="hero-video__skeleton-content">
<div class="hero-video__skeleton-spinner" />
@ -265,6 +312,69 @@ onUnmounted(clearLoadFallback);
opacity: 1;
}
.hero-video__poster {
position: relative;
z-index: 2;
display: flex;
width: 100%;
height: 100%;
min-height: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
border: none;
border-radius: 14px;
color: rgba(230, 251, 255, 0.94);
background:
linear-gradient(90deg, rgba(2, 6, 16, 0.18), rgba(2, 6, 16, 0.36)),
linear-gradient(180deg, rgba(0, 234, 255, 0.06), rgba(255, 43, 255, 0.08)),
var(--hero-video-poster) center / cover;
cursor: pointer;
}
.hero-video__poster::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(circle at 50% 50%, rgba(0, 240, 255, 0.16), transparent 34%),
repeating-linear-gradient(to bottom, rgba(255, 255, 255, 0.08) 0 1px, transparent 1px 4px);
mix-blend-mode: screen;
opacity: 0.38;
pointer-events: none;
}
.hero-video__poster-play,
.hero-video__poster-label {
position: relative;
z-index: 1;
}
.hero-video__poster-play {
display: grid;
width: 70px;
height: 70px;
place-items: center;
border: 1px solid rgba(0, 240, 255, 0.58);
border-radius: 50%;
color: #ffffff;
background: rgba(2, 10, 24, 0.68);
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.08) inset,
0 0 24px rgba(0, 240, 255, 0.3);
}
.hero-video__poster-label {
font-size: 12px;
font-weight: 800;
color: rgba(0, 240, 255, 0.9);
font-family: "JetBrains Mono", monospace;
letter-spacing: 0.05em;
text-transform: uppercase;
text-shadow: 0 0 16px rgba(0, 240, 255, 0.42);
}
.hero-video__skeleton {
position: absolute;
inset: 0;
@ -398,6 +508,10 @@ onUnmounted(clearLoadFallback);
border-radius: 10px;
}
.hero-video__poster {
border-radius: 10px;
}
.hero-video__edge {
left: 12px;
right: 12px;