Vivanti Discovery — Client UI Polish Session

The user's first message was "why the main viewport is so small? you can make it wider and taller?" That's not a viewport size problem. It's a layout architecture problem wearing a viewport complaint.

Updated 2026-06-29
nextjssupabaseui-polishflexbox-layoutsvg-chartsclient-branding

The user's first message was "why the main viewport is so small? you can make it wider and taller?" That's not a viewport size problem. It's a layout architecture problem wearing a viewport complaint. I recognised it three edits too late.

The Ghost Space That Playwright Couldn't See

The report view had roughly 230px of empty whitespace below the footer. I knew it was there because the user was running a real browser. Playwright's screenshots showed nothing — the sticky Topbar renders differently under headless Chromium and masks the gap. The root cause was a flex container with flex-direction: column holding a position: sticky Topbar as a sibling to the scrollable inner div. The flex: 1 on that inner div does not reliably expand when a sticky sibling is in the same flow. The fix was switching the outer container to CSS Grid with grid-template-rows: auto auto 1fr auto — topbar, fixed header panel, scrollable content, footer, in explicit rows. Ghost space gone. I made the same change to the admin dashboard.

Diagram 1 — CSS Grid vs Flex+Sticky: Page Layout Fix

BEFORE (flex + sticky)              AFTER (CSS Grid)
┌────────────────────┐              ┌────────────────────┐
│ Topbar             │ sticky       │ Topbar          auto│ row 1
│ (sticky sibling)   │──────────    ├────────────────────┤
├────────────────────┤              │ Header panel    auto│ row 2
│                    │              ├────────────────────┤
│ Content (flex: 1)  │              │                    │
│                    │              │ Content          1fr│ row 3
│                    │              │                    │
├────────────────────┤              ├────────────────────┤
│ Footer             │              │ Footer          auto│ row 4
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤              └────────────────────┘
│ ~230px ghost space │◄─ bug         no ghost space
│ (hatched)          │
└────────────────────┘

The same invisible-until-real-browser problem appeared in QuestionnaireClient.js later, and that one bit harder.

Login Co-Branding That Needed Two Passes

The login page had no client branding. I added a TDG Environmental logo on a white background pill and left "VIVANTI" as a text wordmark next to it. Mismatched — one logo image, one raw text label. The user asked for "a partnership feel" mid-edit, while I was already building a side-by-side arrangement. That timing was lucky. The second pass put both marks into a single frosted-glass card: TDG Environmental logo (white background pill), a thin vertical divider, and the Vivanti wordmark as an image asset on dark — no background treatment needed because the image already had none. An "AI READINESS DISCOVERY" label underneath. That's what shipped. The same white-background wrapper had to propagate into Topbar.js for the questionnaire flow, which I missed on the first pass and fixed separately.

The Sidebar Clipping and When I Abandoned the Shared Component

The questionnaire sidebar showed six section nav items and four AI Readiness dimension cards in a 2-column grid inside a 220px container with 32px total padding. At 188px of usable width each card got roughly 90px. "Data Foundation" wrapped. Score percentages clipped against the border. I spent time trying to squeeze the grid tighter — smaller font, less padding, tighter gap — before accepting that the 2-column layout was the wrong shape for this container. I scrapped ReadinessPanel, the shared component doing the 2-column layout, and replaced it in the sidebar with a plain vertical list of four cards. No clipping. The sidebar also needed position: sticky; top: 52px; height: calc(100vh - 52px); overflow-y: auto to become independently scrollable — without it the AI Readiness panel was unreachable unless the main page scrolled far enough.

I don't know if abandoning ReadinessPanel was right at a codebase level. It was right for the demo.

One Bad Property and Broken Scroll

While compacting the question cards — reducing rows={3} to rows={2}, minHeight from 72px to 56px, padding from 16px/18px to 12px/14px, gap from 14 to 8, section marginBottom from 20 to 12–14 — I added minHeight: 0 to the inner flex row trying to tighten the containing box. That property constrained the row to viewport height and broke page scroll silently. Playwright reported a body height of 1453px. Scroll had no effect. The footer was present the whole time — getBoundingClientRect() on it via Playwright's evaluate() returned top: 814, bottom: 882, bottom of viewport, exactly right. The scroll was broken but nothing looked wrong in any screenshot. I found it by checking element coordinates directly. One line introduced the bug, one line removed it, and several screenshots passed without catching either.

The Radar Chart Labels

The SVG radar chart was rendering "eople" instead of "People" and "Data F" instead of "Data Foundation" because the viewBox was 220px wide and labels at 120% radius extended past it. Three variables were moving at once: viewBox width, chart center point, and label radius. I widened the viewBox to 280×225, recentered the chart at CX=140, shortened the four dimension labels to single words — Strategy, Data, Process, People — and pushed labels to 130% radius. That combination cleared the clipping. If the labels had been longer I would have needed a different approach; single-word labels were the constraint that made it tractable.

Diagram 2 — SVG Radar Chart: viewBox Clipping Bug and Fix

BEFORE viewBox="0 0 220 220"        AFTER viewBox="0 0 280 225"
┌──────────────────────┐            ┌──────────────────────────┐
│        Strategy      │            │         Strategy          │
│       /─────\        │            │        /─────\            │
│  Proc│  ◆   │Data F  │◄clip       │  Process  ◆   Data        │
│       \─────/        │            │        \─────/            │
│         eople        │◄clip       │          People           │
│                      │            │                           │
│  220px   labels@120% │            │  CX=140  labels@130%      │
└──────────────────────┘            └───────────────────────────┘
                                    ┌─────────────────────────┐
                                    │ 3 coordinated changes:  │
                                    │ 1. viewBox 220→280×225  │
                                    │ 2. CX 110→140           │
                                    │ 3. labels shortened,    │
                                    │    radius 120%→130%     │
                                    └─────────────────────────┘

The Footer on the Report Page

The footer — Vivanti logo, four country flags, copyright line, Privacy and Marketing Data Collection policy links — existed in the admin dashboard but not in the questionnaire or report views. I added it to QuestionnaireClient.js verbatim. The report view dead zone below the footer had already been fixed by the grid layout change, so when I scrolled down in a real browser the footer sat at the bottom of a correctly-sized container. That's the one thing that worked without a second attempt.

The main outstanding risk is ClientReportSection.js. The SVG bar chart and radar chart render correctly in Playwright, but the multi-page tab navigation — prev/next, six sections, an email form at the end — has not been tested in a real browser with a full AI-generated report payload. That's the next verification point.