From cd51cb5632ecb591ccfe56a62a4459e512f7917d Mon Sep 17 00:00:00 2001 From: Yuriy Panov Date: Sat, 29 Aug 2026 11:51:56 +0600 Subject: [PATCH] Fix horizontal truncation across the phone layer Four defects, all of them real clipping rather than styling taste. The header's two action pills were `shrink-0` at 288px inside a 343px container, so flexbox took the difference out of the logo, which carried `min-w-0`. The brand mark rendered 37px wide at 375px and 0px at 320px, and the CTA ran past the right edge. The wordmark stays legible now and the call button goes square and icon-only below 760px; the CTA moves to a new MobileActionBar, which has room for it and puts it in the thumb zone. It waits for the hero's own buttons to scroll away and drops back down while the form is on screen, so it never covers the fields it points at. `.scroll-cards` bleeds to the viewport edge with a 16px inline padding and a matching negative margin, but had no `scroll-padding-inline`, so `scroll-snap-align: start` resolved to scroll position 16 and mandatory snapping forced that scroll on load: the first card sat on the bezel with no gutter, and one row had drifted a full card over. Matching the padding fixes the resting position on all seven rows. Card tracks move from a percentage of the row to `calc(100vw - 62px)`. A percentage peek grows with the screen, and at 82-90% it exposed ~68px of the next card -- enough legible half-words to read as broken text. The value stays in the markup as `auto-cols-*` so it keeps its sort position against `sm:grid-still`, which has to win at the breakpoint. The market stat card pinned its source line to the card bottom while the longest label wraps to four lines at phone width, running text through text by 75px. The source is in flow under `mt-auto` instead. The header CTA switches on `max-md` rather than the `narrow` variant: `narrow` is max-width 760 and the bar is min-width 760, so at exactly 760px both would have hidden and the page would have had no CTA at all. Verified at 320 / 375 / 759 / 760 / 800 and desktop: no horizontal page scroll, no in-flow overflow, desktop grids unchanged. Co-Authored-By: Claude Opus 5 --- medcenter/src/App.tsx | 3 + medcenter/src/components/EquipmentSection.tsx | 2 +- medcenter/src/components/LaunchSection.tsx | 4 +- medcenter/src/components/MarketSection.tsx | 13 ++-- medcenter/src/components/MobileActionBar.tsx | 66 +++++++++++++++++++ medcenter/src/components/ProgramsSection.tsx | 2 +- medcenter/src/components/ProjectSection.tsx | 2 +- medcenter/src/components/SiteFooter.tsx | 6 +- medcenter/src/components/SiteHeader.tsx | 31 +++++++-- medcenter/src/components/WhyExoSection.tsx | 2 +- medcenter/src/index.css | 13 ++++ 11 files changed, 126 insertions(+), 18 deletions(-) create mode 100644 medcenter/src/components/MobileActionBar.tsx diff --git a/medcenter/src/App.tsx b/medcenter/src/App.tsx index 5acd51b..efcadc9 100644 --- a/medcenter/src/App.tsx +++ b/medcenter/src/App.tsx @@ -2,6 +2,7 @@ import { EquipmentSection } from './components/EquipmentSection' import { Hero } from './components/Hero' import { LaunchSection } from './components/LaunchSection' import { LossEconomySection } from './components/LossEconomySection' +import { MobileActionBar } from './components/MobileActionBar' import { MarketSection } from './components/MarketSection' import { ProgramsSection } from './components/ProgramsSection' import { ProjectSection } from './components/ProjectSection' @@ -32,6 +33,8 @@ export default function App() { + + ) } diff --git a/medcenter/src/components/EquipmentSection.tsx b/medcenter/src/components/EquipmentSection.tsx index b4ce0fa..abfffd8 100644 --- a/medcenter/src/components/EquipmentSection.tsx +++ b/medcenter/src/components/EquipmentSection.tsx @@ -33,7 +33,7 @@ export function EquipmentSection() {
{devices.map((device) => (
{formats.map((format) => (
{timeline.map((step) => (
diff --git a/medcenter/src/components/MarketSection.tsx b/medcenter/src/components/MarketSection.tsx index 91081da..6f248b3 100644 --- a/medcenter/src/components/MarketSection.tsx +++ b/medcenter/src/components/MarketSection.tsx @@ -28,25 +28,28 @@ export function MarketSection() {
{marketStats.map((stat) => (
diff --git a/medcenter/src/components/MobileActionBar.tsx b/medcenter/src/components/MobileActionBar.tsx new file mode 100644 index 0000000..373fe85 --- /dev/null +++ b/medcenter/src/components/MobileActionBar.tsx @@ -0,0 +1,66 @@ +import { useEffect, useState } from 'react' +import { contacts } from '../data/content' +import { cx } from '../lib/cx' +import { ButtonLink, buttonClass } from './Button' +import { PhoneIcon } from './Icons' + +/** + * The primary CTA's home below 760px, where the header cannot hold it without + * crushing the logo. See `SiteHeader`. + * + * It waits for the hero — whose own two buttons say the same thing — to scroll + * away, and steps back down while the form is on screen, so it never sits on + * top of the fields it points at. That also keeps it clear of the submit + * button, which is the one control it could plausibly be mistaken for. + */ +export function MobileActionBar() { + const [shown, setShown] = useState(false) + + useEffect(() => { + const hero = document.getElementById('top') + const form = document.getElementById('proposal') + if (!hero || !form) return + + let heroOnScreen = true + let formOnScreen = false + + const observer = new IntersectionObserver((entries) => { + for (const entry of entries) { + if (entry.target === hero) heroOnScreen = entry.isIntersecting + if (entry.target === form) formOnScreen = entry.isIntersecting + } + setShown(!heroOnScreen && !formOnScreen) + }) + + observer.observe(hero) + observer.observe(form) + return () => observer.disconnect() + }, []) + + return ( +
+
+ + + + + Получить предложение + +
+
+ ) +} diff --git a/medcenter/src/components/ProgramsSection.tsx b/medcenter/src/components/ProgramsSection.tsx index d553a57..8683981 100644 --- a/medcenter/src/components/ProgramsSection.tsx +++ b/medcenter/src/components/ProgramsSection.tsx @@ -15,7 +15,7 @@ export function ProgramsSection() { lead="Действующие процедуры, ЛФК, массаж и EXO-технологии объединяются в курс под состояние пациента, этап реабилитации и утверждённую клиническую модель." /> -
+
{programs.map((program) => ( ))} diff --git a/medcenter/src/components/ProjectSection.tsx b/medcenter/src/components/ProjectSection.tsx index f97792b..be7b4cc 100644 --- a/medcenter/src/components/ProjectSection.tsx +++ b/medcenter/src/components/ProjectSection.tsx @@ -20,7 +20,7 @@ export function ProjectSection() {
diff --git a/medcenter/src/components/SiteFooter.tsx b/medcenter/src/components/SiteFooter.tsx index 4056f1e..684900e 100644 --- a/medcenter/src/components/SiteFooter.tsx +++ b/medcenter/src/components/SiteFooter.tsx @@ -3,7 +3,11 @@ import { Container } from './Layout' export function SiteFooter() { return ( -