---
id: M016
title: "Visual Identity & Reading Experience"
status: complete
completed_at: 2026-04-03T06:26:29.854Z
key_decisions:
  - Kept .search-container at 36rem and .home-hero__value-prop at 32rem for readability — only content sections widened to 42rem
  - Generated PNG assets programmatically using Python stdlib (struct+zlib) to avoid external image tool dependencies
  - Used favicon mark without background rect for inline header logo — decorative with aria-hidden
  - ToC only renders in sidebar for v2-format pages; v1 pages have no ToC
  - rootMargin 0px 0px -70% 0px triggers active state when section enters top 30% of viewport
  - Hide section name on mobile (<600px) in reading header rather than truncating both fields
  - pointer-events:none when reading header is hidden so invisible bar doesn't block clicks
  - Made useCountUp generic over element type for React 18 ref compatibility
  - Kept section-specific CSS classes as empty shells for future BEM overrides rather than deleting them
key_files:
  - frontend/src/App.css
  - frontend/src/App.tsx
  - frontend/index.html
  - frontend/src/components/ReadingHeader.tsx
  - frontend/src/components/TableOfContents.tsx
  - frontend/src/pages/TechniquePage.tsx
  - frontend/src/pages/Home.tsx
  - frontend/src/hooks/useCountUp.ts
  - frontend/src/pages/AdminPipeline.tsx
  - frontend/public/favicon.svg
  - frontend/public/og-image.png
lessons_learned:
  - Lifted state pattern for scroll-spy: when multiple components need the same IntersectionObserver data (ToC + reading header), lift the observer to the parent page component and pass activeId down as a prop. Avoids duplicate observers and keeps them in sync.
  - Programmatic PNG generation via Python stdlib (struct + zlib) works for simple brand assets but times out for large images with naive pixel-by-pixel generation — use scanline-based approach with bounding-box culling for 1200x630+ sizes.
  - CSS grid-template-rows 0fr/1fr animation and IntersectionObserver scroll-spy are now established patterns in this codebase — both used in multiple components.
---

# M016: Visual Identity & Reading Experience

**Modernized the public site's visual identity and reading experience with CSS bug fixes, brand assets, ToC modernization with scroll-spy, sticky reading header, and homepage personality polish.**

## What Happened

M016 delivered six slices of frontend polish across the public site and pipeline admin.

S01 fixed four CSS bugs on the homepage: unified max-width to 42rem across content sections, removed border-image from the featured card (restoring border-radius per the KNOWLEDGE.md rule), constrained the CTA button on mobile, and standardized section spacing to 2rem.

S02 addressed five pipeline admin UI issues: collapse toggle arrow styling, mobile card layout (grid to single-column stack with text truncation), stage chevrons replacing line separators, filter bar right-alignment via margin-left:auto, and creator dropdown visibility fix (>= 1 instead of > 1).

S03 established brand baseline: SVG favicon with cyan arc+dot mark, 32px PNG fallback, apple-touch-icon, 1200x630 OG social image (all generated programmatically via Python stdlib to avoid external dependencies), OG/Twitter meta tags in index.html, and inline SVG logo mark in the header brand area.

S04 modernized the technique page Table of Contents: relocated from main prose column to sidebar, replaced CSS counters with left accent bar, added "On this page" heading, converted to unordered list, and implemented IntersectionObserver-based scroll-spy with rootMargin triggering active state when a section enters the top 30% of the viewport.

S05 added a sticky reading header that slides in when the article H1 leaves the viewport. The IntersectionObserver scroll-spy state was lifted from TableOfContents to TechniquePage, making activeId available to both the ToC and ReadingHeader. Mobile hides the section name to keep the title readable at 375px. pointer-events:none when hidden prevents click interception.

S06 polished homepage personality: useCountUp hook for animated stat numbers (rAF + ease-out, triggered by IntersectionObserver), unified .section-heading utility class replacing four duplicate heading styles, tighter above-fold spacing, and header border-bottom accent line in brand cyan.

## Success Criteria Results

The roadmap's vision defined six deliverables, each corresponding to a slice:

- **Fix landing page CSS bugs** — ✅ S01: 42rem max-width unification, border-image removal, CTA mobile sizing, spacing normalization. Build verified.
- **Fix pipeline admin UI issues** — ✅ S02: Collapse toggle, mobile cards, chevrons, filter alignment, creator dropdown. Build verified.
- **Establish brand baseline (logo, favicon, OG tags)** — ✅ S03: favicon.svg, favicon-32.png, apple-touch-icon.png, og-image.png in dist/. OG/Twitter meta tags in index.html. Inline SVG logo in header.
- **Modernize technique page ToC with active section tracking** — ✅ S04: Accent bar, no counters, "On this page" heading, IntersectionObserver scroll-spy, sticky in sidebar.
- **Add sticky reading header** — ✅ S05: ReadingHeader component with slide-in/out, title + current section, mobile support, pointer-events:none when hidden.
- **Polish homepage personality** — ✅ S06: Animated count-up, unified section headings, tighter above-fold, header brand accent.

## Definition of Done Results

- **All slices complete:** ✅ S01–S06 all marked ✅ in roadmap
- **All slice summaries exist:** ✅ S01-SUMMARY.md through S06-SUMMARY.md all present
- **Code changes verified:** ✅ 23 files changed, 1016 insertions across frontend and backend
- **Frontend build passes:** ✅ Confirmed in all 6 slice summaries (npm run build, zero errors)
- **Cross-slice integration:** ✅ S05 correctly consumed S04's IntersectionObserver pattern (lifted activeId state). S06 correctly built on S01's 42rem baseline and S03's brand assets.

## Requirement Outcomes

- **R037 (Landing Page Visual Consistency):** active → validated. S01 delivered 42rem max-width, border-image fix, CTA sizing, spacing normalization. S06 added animated count-up and unified section headings. Build verified in both slices.
- **R038 (Pipeline Admin UI Fixes):** active → validated. S02 delivered all five fixes: collapse toggle, mobile cards, chevrons, filter alignment, creator dropdown. Build verified.
- **R039 (Brand Minimum):** active → validated. S03 delivered favicon (SVG + PNG), apple-touch-icon, OG image, OG/Twitter meta tags, inline SVG logo. All assets confirmed in dist/ build output. All meta tags confirmed in built index.html.
- **R040 (ToC Modernization):** active → validated. S04 delivered accent bar, no counters, heading, scroll-spy, sticky sidebar. Build verified.
- **R041 (Sticky Reading Header):** active → validated. S05 delivered ReadingHeader component with slide animation, title + section display, mobile support. Build verified.

## Deviations

No significant deviations from plan. Minor adjustments: T01 in S03 switched from flat-list to scanline-based PNG generation after timeout. T01 in S05 used non-null assertion after .sort() chain for TypeScript narrowing. T01 in S06 made useCountUp generic over HTMLElement type for React 18 strict ref typing.

## Follow-ups

Replace programmatic PNG brand assets with professionally designed assets when brand identity matures. Add per-page OG tags (technique pages should have unique og:title/og:description). Consider adding GET /review/moments/{id} if admin tools need single-moment fetching beyond the current list-and-filter pattern.
