fix(mobile): hero — картинка как фон секции, текст поверх

- HeroImage: на мобилке картинка позиционируется фоном за текстом, opacity 0.45
- Сильный mask-gradient к левому-нижнему углу, чтобы заголовок читался
- Софт-вуаль поверх фона (background var(--bg) с убывающей прозрачностью)
- Секция hero: min-h-[88vh] на мобиле, текст по центру по вертикали
- В тёмной теме мобильная opacity ещё ниже (0.32)
- Десктоп без изменений
This commit is contained in:
Alexey Pavlov
2026-05-31 10:00:25 +03:00
parent 6aff8cd6d9
commit 3154b47578
2 changed files with 46 additions and 28 deletions
+2 -2
View File
@@ -31,9 +31,9 @@ export default async function HomePage() {
<Header /> <Header />
{/* Hero */} {/* Hero */}
<section className="relative overflow-hidden"> <section className="relative overflow-hidden min-h-[88vh] sm:min-h-0 flex items-center sm:block">
<HeroImage /> <HeroImage />
<div className="container-wide pt-8 pb-12 sm:pt-20 sm:pb-24 lg:pt-28 lg:pb-32 relative"> <div className="container-wide pt-6 pb-10 sm:pt-20 sm:pb-24 lg:pt-28 lg:pb-32 relative z-10 w-full">
<Reveal> <Reveal>
<div className="max-w-xl lg:max-w-2xl reveal"> <div className="max-w-xl lg:max-w-2xl reveal">
<div <div
+44 -26
View File
@@ -2,8 +2,9 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
/** /**
* Hero иллюстрация с лёгким parallax при скролле + плавный fade с левого края, * Hero иллюстрация — фон секции. Поверх неё рендерится текст.
* чтобы картинка органично «вытаивала» в фон под текстом. * Desktop: справа, fade к центру, parallax при скролле.
* Mobile: фон ВО ВСЮ секцию, текст поверх с дополнительным фейдом.
*/ */
export default function HeroImage() { export default function HeroImage() {
const ref = useRef(null); const ref = useRef(null);
@@ -15,7 +16,6 @@ export default function HeroImage() {
cancelAnimationFrame(raf); cancelAnimationFrame(raf);
raf = requestAnimationFrame(() => { raf = requestAnimationFrame(() => {
const y = window.scrollY; const y = window.scrollY;
// едет вверх медленнее, чем страница
const offset = Math.min(60, y * 0.15); const offset = Math.min(60, y * 0.15);
if (ref.current) ref.current.style.transform = `translateY(-${offset}px)`; if (ref.current) ref.current.style.transform = `translateY(-${offset}px)`;
}); });
@@ -29,7 +29,7 @@ export default function HeroImage() {
<picture> <picture>
<source <source
srcSet="/uploads/hero-1920.webp 1920w, /uploads/hero-1280.webp 1280w, /uploads/hero-800.webp 800w" srcSet="/uploads/hero-1920.webp 1920w, /uploads/hero-1280.webp 1280w, /uploads/hero-800.webp 800w"
sizes="(min-width: 1024px) 65vw, 100vw" sizes="(min-width: 768px) 70vw, 100vw"
type="image/webp" type="image/webp"
/> />
<img <img
@@ -46,11 +46,10 @@ export default function HeroImage() {
<style jsx>{` <style jsx>{`
.hero-image-wrap { .hero-image-wrap {
position: absolute; position: absolute;
top: 0; right: 0; bottom: 0; inset: 0;
width: 100%;
pointer-events: none; pointer-events: none;
overflow: hidden; overflow: hidden;
z-index: -1; z-index: 0;
} }
.hero-image { .hero-image {
position: absolute; position: absolute;
@@ -63,8 +62,6 @@ export default function HeroImage() {
object-position: right top; object-position: right top;
opacity: 0.85; opacity: 0.85;
will-change: transform; will-change: transform;
transition: opacity 0.4s ease;
/* плавный фейд от центра к левому краю — картинка тает */
-webkit-mask-image: linear-gradient( -webkit-mask-image: linear-gradient(
to right, to right,
transparent 0%, transparent 0%,
@@ -93,27 +90,48 @@ export default function HeroImage() {
background: linear-gradient(to bottom, transparent, rgb(var(--bg))); background: linear-gradient(to bottom, transparent, rgb(var(--bg)));
pointer-events: none; pointer-events: none;
} }
/* На мобилке картинка сверху, под ней — текст */
/* === MOBILE: картинка фоном, текст поверх === */
@media (max-width: 767px) { @media (max-width: 767px) {
.hero-image-wrap {
position: relative;
width: 100vw;
margin-left: -1rem;
margin-right: -1rem;
height: 240px;
margin-bottom: 1rem;
}
.hero-image { .hero-image {
top: 0; top: -20px;
right: 0; right: -30%; /* выезжает за правый край — видна только часть */
height: 100%; height: 100%;
width: 100%; width: auto;
object-position: right center; max-width: 160%;
opacity: 0.95; opacity: 0.45; /* приглушаем, чтобы текст читался */
-webkit-mask-image: linear-gradient(to bottom, black 60%, transparent 100%); -webkit-mask-image: linear-gradient(
mask-image: linear-gradient(to bottom, black 60%, transparent 100%); to bottom right,
transparent 0%,
rgba(0,0,0,0.15) 30%,
rgba(0,0,0,0.6) 60%,
black 100%
);
mask-image: linear-gradient(
to bottom right,
transparent 0%,
rgba(0,0,0,0.15) 30%,
rgba(0,0,0,0.6) 60%,
black 100%
);
}
:global(.dark) .hero-image {
opacity: 0.32;
}
/* софт-вуаль для контраста заголовка */
.hero-image-wrap::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
to bottom,
rgb(var(--bg) / 0.4) 0%,
rgb(var(--bg) / 0.2) 40%,
rgb(var(--bg)) 100%
);
z-index: 1;
pointer-events: none;
} }
.hero-image-wrap::after { display: none; }
} }
`}</style> `}</style>
</div> </div>