Three layout bugs and a mobile experience that had no wayfinding. The `#proposal` section was clipped, not merely cramped: the submit button carried `whitespace-nowrap` from the shared button base, giving it a min-content width of 358px, which sized the section's implicit `auto` grid track to 406px inside a 343px container. `overflow-hidden` then sliced the copy and every form field off the right edge of a 375px screen. Fixed at the root — `whitespace-nowrap` moves out of `base` and into `headerSize`, where buttons are sized by their label — plus an explicit `grid-cols-[minmax(0,1fr)]` on the section. The market stat cards put their source line in absolute position against a guessed min-height, so the longest label ran underneath it. It is in the flow now, pinned by `mt-auto`. Below 980px the header nav is not rendered, which left seventeen screens of scroll with no way to navigate them. The header gives that width back to the page — logo and call button only — and <ThumbBar /> takes over: scroll progress, a live section indicator that opens a section sheet, and one short CTA, hidden over the hero and over the form. Card rows now say what they are. Rows of unordered peers keep scrolling sideways and gain a measured segment indicator (<Rail />), replacing the two prose "swipe sideways" hints. The launch timeline goes the other way: sideways scrolling hid week 3-5 from someone reading week 1-2, so below 900px it becomes four disclosures on a spine — which also recovers the scroll length the taller hero spent. Below 560px the type scale steps down half a level and the display tracking opens back up, and the hero photo zooms past `cover` onto the wave wall, since fitting by height left only ceiling tiles above the copy. Also: the route card inside the market section split into two 165px columns at 760px, so that split now waits for 980px. Desktop above 980px is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
128 lines
5.3 KiB
TypeScript
128 lines
5.3 KiB
TypeScript
import { marketNotes, marketStats, routeWith, routeWithout } from '../data/content'
|
|
import { useReveal } from '../hooks/useReveal'
|
|
import { cx } from '../lib/cx'
|
|
import { Container, Eyebrow, Section, SectionHead, SmallNote } from './Layout'
|
|
import { Rail } from './Rail'
|
|
|
|
export function MarketSection() {
|
|
const stats = useReveal<HTMLDivElement>()
|
|
const gap = useReveal<HTMLElement>()
|
|
|
|
return (
|
|
<Section id="market">
|
|
<Container>
|
|
<SectionHead
|
|
split
|
|
eyebrow="Рыночный контекст"
|
|
title="Спрос на реабилитацию растёт. Пациент уже находится внутри вашего центра"
|
|
lead="Забудьте о расходах на привлечение новых клиентов. Мы встраиваем готовое направление физиотерапии в вашу структуру, превращая лояльную базу пациентов и текущий штат врачей в источник новой прибыли."
|
|
/>
|
|
|
|
<div className="grid grid-cols-[minmax(0,1fr)] gap-[24px] md:grid-cols-[minmax(0,1.08fr)_minmax(360px,0.92fr)] md:items-stretch">
|
|
<Rail
|
|
outerRef={stats.ref}
|
|
wrapperClassName={stats.revealClass}
|
|
label="Рыночные показатели"
|
|
className="scroll-cards auto-cols-[minmax(255px,82%)] gap-[12px] sm:grid-still sm:grid-cols-2"
|
|
>
|
|
{marketStats.map((stat) => (
|
|
/* The source line used to be absolutely positioned against the
|
|
card's min-height, so the longest label ran underneath it.
|
|
It is in the flow now, pinned to the bottom by `mt-auto`. */
|
|
<article
|
|
key={stat.label}
|
|
className="relative flex min-h-[185px] flex-col overflow-hidden rounded-lg border border-navy/[0.12] bg-white p-[24px] shadow-[0_16px_48px_rgb(4_29_46/0.06)]
|
|
phone:min-h-[168px] phone:p-[20px] sm:min-h-[205px]"
|
|
>
|
|
<span
|
|
aria-hidden="true"
|
|
className="teal-bloom-soft absolute -right-[36px] -bottom-[48px] size-[150px] rounded-full"
|
|
/>
|
|
<strong className="relative mb-[8px] block text-[clamp(34px,5vw,54px)] leading-none font-[830] tracking-[-0.06em] text-navy">
|
|
{stat.value}
|
|
</strong>
|
|
<p className="relative max-w-[240px] text-[14px] leading-[1.45] text-muted">{stat.label}</p>
|
|
<span className="relative mt-auto pt-[14px] text-[10px] tracking-[0.09em] text-[#7E93A3] uppercase">
|
|
{stat.source}
|
|
</span>
|
|
</article>
|
|
))}
|
|
</Rail>
|
|
|
|
<article
|
|
ref={gap.ref}
|
|
className={cx(
|
|
'gap-card-surface relative grid gap-[16px] overflow-hidden rounded-xl p-[27px] text-white shadow-deep compact:p-[20px]',
|
|
gap.revealClass,
|
|
)}
|
|
>
|
|
<span
|
|
aria-hidden="true"
|
|
className="teal-bloom absolute -right-[160px] -bottom-[200px] size-[480px] rounded-full"
|
|
/>
|
|
|
|
<div className="relative z-1">
|
|
<Eyebrow tone="dark">Исходная точка</Eyebrow>
|
|
<h3 className="max-w-[600px] text-[clamp(27px,3.5vw,42px)] compact:text-[22px] compact:tracking-[-0.02em]">
|
|
Маршрут пациента: как перестать отдавать прибыль конкурентам.
|
|
</h3>
|
|
</div>
|
|
|
|
{/* Not `md:` — at 760 the card is already sharing the row with the
|
|
stats, so splitting it again gives two 165px columns of
|
|
four-word lines. It waits for 980, where the card is wide. */}
|
|
<div className="relative z-1 grid gap-[12px] lg:grid-cols-2">
|
|
<RouteColumn sign="—" title="Пока физиотерапии нет" items={routeWithout} />
|
|
<RouteColumn sign="+" title="После запуска направления" items={routeWith} accent />
|
|
</div>
|
|
</article>
|
|
</div>
|
|
|
|
<SmallNote className="mt-[16px]">{marketNotes[0]}</SmallNote>
|
|
<SmallNote className="mt-[8px]">{marketNotes[1]}</SmallNote>
|
|
</Container>
|
|
</Section>
|
|
)
|
|
}
|
|
|
|
function RouteColumn({
|
|
sign,
|
|
title,
|
|
items,
|
|
accent = false,
|
|
}: {
|
|
sign: string
|
|
title: string
|
|
items: string[]
|
|
accent?: boolean
|
|
}) {
|
|
return (
|
|
<div
|
|
className={cx(
|
|
'rounded-[20px] border p-[20px]',
|
|
accent ? 'border-teal/[0.34] bg-teal/[0.13]' : 'border-white/[0.13] bg-white/[0.07]',
|
|
)}
|
|
>
|
|
<div className="mb-[12px] flex items-center gap-[10px] font-[820]">
|
|
<span
|
|
aria-hidden="true"
|
|
className="grid size-[28px] place-items-center rounded-full bg-white/[0.12] text-[13px] text-teal-bright"
|
|
>
|
|
{sign}
|
|
</span>
|
|
{title}
|
|
</div>
|
|
<ul className="grid gap-[9px]">
|
|
{items.map((item) => (
|
|
<li
|
|
key={item}
|
|
className="relative pl-[19px] text-[14px] text-white/[0.72] before:absolute before:top-[0.65em] before:left-0 before:size-[7px] before:rounded-full before:bg-teal before:content-['']"
|
|
>
|
|
{item}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)
|
|
}
|