1990s Marketing Page — Hero section, feature cards, testimonials, pricing table, and responsive navigation. The classic SaaS landing page — every marketer, startup, and developer builds one. Inspired by freeCodeCamp's Responsive Web Design certification.
Hero Section
CompleteWhy learn this?
A hero section is the single highest-leverage component on any marketing page. Stripe, Linear, and Notion all invest heavily in theirs because the first 3 seconds determine whether a visitor stays or bounces. The hero has one job: communicate what the product does, why it matters, and where to click — all before the user scrolls.
What you built and why this way
min-height: 100vh vs height: 100vh. Using min-height instead of height is intentional — if the content grows (longer tagline, additional subtext), the hero expands gracefully instead of overflowing or clipping. On short screens like landscape phones, 100vh would squash everything; min-height lets the content breathe.
Flexbox centering. The flex-direction: column + justify-content: center + align-items: center triple is the standard way to center a stack of elements both horizontally and vertically. It works on every browser back to IE11 (with -ms- prefixes). An alternative would be CSS Grid with place-items: center, but flexbox is more predictable when you add a CTA button that should sit at the bottom or when the content has multiple text blocks with different line counts.
Gradient background depth. The 4-stop linear gradient (#0a0a1a → #16213e → #0f3460 → #1a1a2e) creates more depth than a 2-stop gradient because each additional stop adds a color transition point — the eye perceives multiple layers of depth. The angle (135deg) runs diagonal, which avoids the flat top-to-bottom look of 180deg. For a faster-loading alternative that still feels dimensional, a single dark color with a subtle radial-gradient overlay works in half the CSS.
Gradient text. -webkit-background-clip: text with -webkit-text-fill-color: transparent creates the gradient-accented text effect on the product name. This is a WebKit-only property (Chrome, Safari, Edge) — Firefox supports the unprefixed background-clip: text but still needs -webkit-text-fill-color: transparent for the transparent fill. On older browsers, the text falls back to the element's color — it degrades gracefully rather than breaking.
CTA button. The pill shape (border-radius: 50px) is the highest-converting CTA shape — it draws attention as a distinct visual object. The hover transition (translateY(-2px) + box-shadow) is called a "lift effect" — it signals clickability without relying on color changes, which is important for colorblind users.
Font sizing. clamp(2.5rem, 6vw, 4.5rem) means: start at 2.5rem (~40px) on small screens, scale fluidly with viewport width at 6vw, cap at 4.5rem (~72px) on large screens. The 6vw value is the sweet spot — too low and text stays small on tablets, too high and it balloons on ultrawide monitors.
Common pitfalls
100vhon iOS Safari includes the URL bar in its calculation — the hero will be taller than the visible viewport. Use100dvh(dynamic viewport height) for mobile or add a fallback with-webkit-fill-available- Gradient text with
background-clip: textwon't wrap properly in some browsers if the element is inline — usedisplay: inline-blockorblock - Large
border-radiusvalues (>10px) on buttons can make the text look off-center visually. The text appears optically centered even when mathematically centered because the rounded corners draw the eye inward
Key concepts
min-height: 100vh— Fill the viewport regardless of content height, vsheight: 100vhwhich clips overflowbackground: linear-gradient()— Multi-stop color gradients for depth. 4 stops > 2 stopsclamp(min, preferred, max)— Fluid typography without breakpoints. The middle value is the scaling formula-webkit-background-clip: text— Gradient text. WebKit-only but degrades gracefullyborder-radius: 50px— Pill-shaped buttons that convert higher than rectangular onestranslateY(-2px)+box-shadow— Lift effect hover state for interactive elements
Next up
Step 2 adds a feature card grid — the standard way SaaS products present their capabilities.
<section class="hero">
<h1>Build Smarter<br>
with <span>AI Agents</span>
</h1>
<p>Automate workflows,
integrate tools, and deploy
autonomous agents in minutes.
</p>
<a href="#" class="cta">
Start Building Free
</a>
</section>.hero {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 2rem;
background: linear-gradient(135deg,
#0a0a1a 0%, #16213e 40%,
#0f3460 70%, #1a1a2e 100%);
}
.hero h1 {
font-size: clamp(2.5rem, 6vw, 4.5rem);
font-weight: 800;
}
.hero h1 span {
background: linear-gradient(90deg, #00d4aa, #00a8cc);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero .cta {
display: inline-block; padding: 1rem 2.5rem;
background: linear-gradient(90deg, #00d4aa, #00a8cc);
color: #0a0a1a; font-weight: 700;
border-radius: 50px; text-decoration: none;
transition: transform 0.2s, box-shadow 0.2s;
}
.hero .cta:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0,212,170,0.3);
}clamp(2.5rem, 6vw, 4.5rem) for the headline so it scales from 40px on mobile to 72px on desktop. For the gradient background, 4 stops create more depth than 2: linear-gradient(135deg, #0a0a1a, #16213e, #0f3460, #1a1a2e).Feature Cards
CompleteWhy learn this?
Feature grids are the most reused layout pattern on the web. Every SaaS product — Stripe, Notion, Vercel, Linear — uses them to present capabilities in a scannable, card-based layout. The pattern is simple (icon + title + description) but the implementation details determine whether your grid looks polished or thrown together. Getting card spacing, hover states, and responsive behavior right is a foundational CSS skill.
What you built and why this way
auto-fit vs auto-fill. The grid uses repeat(auto-fit, minmax(300px, 1fr)). auto-fit collapses empty tracks when there aren't enough items to fill a row — a 6-card grid shows 3 columns on desktop, 2 on tablet, 1 on mobile. auto-fill would preserve empty column tracks, leaving visible gaps. For a feature grid where you want cards to fill the available space, auto-fit is the correct choice. For a calendar or gallery where grid position matters (March 15 should stay in column 3), use auto-fill.
Card backgrounds with rgba(). background: rgba(255,255,255,0.04) creates a barely-there white overlay on the dark page background. The 0.04 alpha means it's 4% white — subtle enough to not distract, present enough to define card boundaries. This technique works on any background color: wrap a card in a dark container and you get the same effect. Using rgba() instead of a solid hex color means the card adapts if you change the page background later.
Card symmetry rule. Every card has the same height regardless of content length. The grid's align-items: stretch default ensures all cards in a row match height. If one card has more text than others, all cards in that row grow to match — no uneven bottoms. For content that genuinely varies (like testimonials with different quote lengths), you'd switch to align-items: start and add a minimum height.
Hover lift effect. transform: translateY(-4px) paired with border-color: #00d4aa creates a lift-and-highlight effect. The 4px upward movement is subtle enough to not cause layout shifts (transforms don't affect document flow) but visible enough to signal interactivity. The border color change from a barely-visible rgba(255,255,255,0.08) to the accent green creates a clear "this is clickable" signal.
Transition performance. The transition property explicitly lists transform and border-color — not all. This matters because transition: all would animate every changeable property (including background if you add it later), potentially causing jank. Being explicit about which properties animate is a performance best practice — the browser can optimize known property transitions.
Common pitfalls
auto-fitvsauto-fillconfusion: test with 2 items on desktop to see the difference.auto-fitstretches them across the row;auto-fillleaves empty column tracks- Cards with wildly different content lengths break visual symmetry. Keep descriptions to 1-2 lines for consistency. If some cards need more text, add a
min-heightto the card - Transform hover effects on cards inside a scrollable container can trigger scrollbars — use
overflow: visibleon the parent or add negative margin to compensate
Key concepts
repeat(auto-fit, minmax(300px, 1fr))— Auto-wrapping grid.auto-fitfills rows;auto-fillkeeps column positionsrgba(255,255,255,0.04)— Subtle transparent overlay that works on any background colortransition: transform 0.2s, border-color 0.2s— Explicit property animation. Never usetransition: alltransform: translateY(-4px)— Layout-safe upward lift. Transforms don't affect document flow
Next up
Step 3 adds testimonials with pull quotes and avatar badges — social proof that converts visitors.
<div class="feature-grid">
<div class="feature-card">
<div class="icon">⚡</div>
<h3>Visual Workflows</h3>
<p>Build agent pipelines
without writing code.</p>
</div>
<!-- More cards follow the
same pattern -->
</div>.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.feature-card {
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px; padding: 2rem;
transition: transform 0.2s, border-color 0.2s;
}
.feature-card:hover {
transform: translateY(-4px);
border-color: #00d4aa;
}
.feature-card .icon { font-size: 2rem; margin-bottom: 1rem; }
.feature-card p { color: rgba(255,255,255,0.6); font-size: 0.92rem; }repeat(auto-fit, minmax(300px, 1fr)) means "create as many columns as fit, each at least 300px wide, stretch them equally." The grid wraps to fewer columns as the viewport shrinks — no @media query required.Testimonials
CompleteWhy learn this?
Social proof is the highest-converting element on a landing page. A well-written testimonial — real-sounding person, specific result, credible context — converts better than any feature list or technical spec. Dropbox, Airbnb, and Shopify all use testimonial grids because they answer the unspoken question every visitor has: "Does this actually work for real people?" Learning to build testimonial sections teaches you CSS pseudo-elements, avatar design, and quote typography.
What you built and why this way
::before for decorative quotes. The open-quote mark before each testimonial is generated with CSS ::before { content: '\201C'; } — no extra HTML elements, no icon fonts, no images. The curly left double quote (\201C) is a Unicode character that renders in every browser. Using a pseudo-element means screen readers skip it (purely decorative), and you can style it independently — size, color, position — without affecting the quote text.
Avatar circles with initials. The .avatar class creates a perfect circle (border-radius: 50%) with a gradient fill and centered initials. The 44px size follows the WCAG minimum target size recommendation — large enough to be clearly visible, small enough to not overshadow the quote. Using initials instead of photos means you don't need to manage image uploads, fallbacks, or aspect ratios. For production sites where you have real headshots, swap background: linear-gradient(...) for background-image: url(...) and keep the same border-radius.
Italic quote readability. font-style: italic combined with line-height: 1.7 makes quotes visually distinct from the surrounding body text. Italics alone are harder to read at small sizes — the increased line-height compensates by adding more vertical space between lines. For the testimonial text itself, color: rgba(255,255,255,0.8) keeps it readable but slightly muted compared to the author name, visually prioritizing the attribution.
Grid layout for testimonials. The same repeat(auto-fit, minmax(320px, 1fr)) pattern from step 2 works here too — the wider min (320px vs 300px) accommodates longer quote text without cramping. Testimonials benefit from a slightly wider minimum because quotes with different lengths look unbalanced if they're squeezed into narrow columns. On mobile, each card takes full width, stacking vertically — no horizontal scrolling.
Card structure. Unlike the feature cards, testimonial cards don't use a hover lift effect. Hover animations on testimonials can feel distracting — readers are processing the quote content, not deciding whether to click. The card still has the same subtle rgba background and border for visual consistency across the page.
Common pitfalls
::beforerequirescontent— even if empty. Withoutcontent: '', the pseudo-element doesn't render at all. The open-quote character won't appear if you forget it- Avatar circles with initials need exact width/height equality and
border-radius: 50%. A value likeborder-radius: 50pxworks at 44px but breaks if the size changes later. Use percentages for future-proof code - Italic text at font sizes below 14px becomes hard to read on low-DPI screens. Keep testimonial text at minimum 15-16px if you use italics, or skip italics and use a left border accent instead
- Screen readers may not indicate when a quote is a quote — the
<blockquote>element provides semantic meaning. Consider using<blockquote>instead of<div class="quote">for accessibility
Key concepts
::beforewithcontent: '\201C'— Purely decorative quotes generated from CSS. No HTML neededborder-radius: 50%— Perfect circles. Always use percentages over fixed valuesfont-style: italic+line-height: 1.7— Readable quote typography. Italics need more line-height<blockquote>— Semantic HTML that screen readers understand as a quotation
Next up
Step 4 builds a pricing table — the highest-stakes component on any SaaS site.
<div class="testimonial-card">
<div class="quote">We cut response
time from 4 hours to under
30 seconds.</div>
<div class="author">
<div class="avatar">SK</div>
<div>
<div class="name">
Sarah Kim</div>
</div>
</div>
</div>.testimonial-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.5rem;
}
.testimonial-card {
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px; padding: 2rem;
}
.testimonial-card .quote {
font-style: italic;
color: rgba(255,255,255,0.8);
line-height: 1.7;
}
.testimonial-card .quote::before {
content: '\201C';
font-size: 2.5rem; color: #00d4aa;
line-height: 1; display: block;
margin-bottom: 0.3rem;
}
.testimonial-card .avatar {
width: 44px; height: 44px; border-radius: 50%;
background: linear-gradient(135deg, #00d4aa, #00a8cc);
display: flex; align-items: center;
justify-content: center;
font-weight: 700; color: #0a0a1a;
}::before with content: '\201C' renders a curly open-quote that screen readers ignore (purely decorative). For the avatar, display: flex + align-items: center + justify-content: center perfectly centers the initials inside the circle.Pricing Table
CompleteWhy learn this?
The pricing table is the highest-stakes component on any SaaS site — it's where visitors decide whether to buy. A well-designed table communicates value hierarchy at a glance: which plan is for whom, what each tier includes, and which one most people choose. Figma, Linear, and Notion all use the same 3-tier pattern (Free, Pro, Enterprise) because it maps directly to the customer journey — try, buy, scale. The "Most Popular" badge is a deliberate psychological nudge, not decoration.
What you built and why this way
3-tier value hierarchy. The Starter/Pro/Enterprise structure follows the standard "good, better, best" pricing model. Starter is free (remove friction to try), Pro is the revenue driver (most users convert here), Enterprise is custom (tailored for large accounts). The Pro card is visually distinguished with transform: scale(1.03) and an accent border — this is called "decoy pricing" in conversion optimization: highlighting the middle option makes it feel like the safe, smart choice.
Sup element for prices. The <sup>$</sup> before the price number raises the dollar sign above baseline. Modern pricing pages (Linear, Vercel) use this to make the whole-dollar price the visual anchor — your eye reads "49" first, not "$49". The vertical-align: super + font-size: 0.6em styling ensures the dollar sign is proportionally smaller and raised consistently across browsers.
CSS checkmark bullets. Each feature list uses ::before { content: '\2713'; } instead of the default list-style-type: disc. The checkmark (✓) communicates "included" without needing an icon library, an image, or even an extra HTML element. Removing list-style: none and adding ::before is the standard pattern for custom bullet lists. Note: screen readers still announce the <li> count, but the checkmark is decorative — if you need to convey "included vs not included" semantically, use aria-label on each item.
scale() and layout safety. transform: scale(1.03) makes the featured card 3% larger without affecting the surrounding layout — transforms operate in the rendering layer, not the document flow. This means the scaled card overlaps adjacent cards slightly on hover, which creates visual depth. The scale is combined with the same translateY(-4px) from earlier steps for a unified interaction language across the page.
CTA button styling. Each tier has its own button style: outlined for Starter and Enterprise, filled with the accent gradient for Pro. The filled button on the featured tier uses background: linear-gradient(90deg, #00d4aa, #00a8cc) — the same gradient as the hero CTA — creating visual consistency across the page. The outlined buttons use border: 1px solid rgba(255,255,255,0.15) to stay visible without competing with the featured CTA.
Common pitfalls
transform: scale()on the featured card can cause edge clipping if the parent container hasoverflow: hidden. Add padding to the pricing grid or useoverflow: visibleon the container- The
supelement can shift the dollar sign too high depending on the font's baseline. Test across fonts — some needvertical-align: topinstead ofsuper - Custom list bullets with
::beforedon't account for multi-line items. Long feature text wraps under the bullet instead of indenting properly. Addpadding-left: 1.2emandtext-indent: -1.2emto handle wrapping correctly - Screen readers won't announce checkmarks in CSS
::before. If "included vs not included" is critical info, use visible text labels oraria-hiddenwitharia-label
Key concepts
supelement +vertical-align: super+font-size: 0.6em— Raised dollar sign without breaking the price flow::before { content: '\2713' }— CSS-only checkmark bullets. No icon libraries neededtransform: scale(1.03)— Subtle featured-card emphasis. Transforms don't shift layout- 3-tier "good, better, best" — Standard SaaS pricing model. The middle tier drives revenue
Next up
Step 5 assembles everything into a complete page with sticky nav, mobile hamburger, and accessibility polish.
<div class="pricing-card featured">
<div class="badge">
Most Popular</div>
<div class="plan">Pro</div>
<div class="price">
<sup>$</sup>49
<span>/mo</span>
</div>
<ul>
<li>Unlimited agents</li>
<li>10K task runs/mo</li>
<li>Unlimited integrations</li>
</ul>
<a href="#" class="btn">
Start Free Trial
</a>
</div>.pricing-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem; align-items: start;
}
.pricing-card {
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px; padding: 2.5rem 2rem;
text-align: center;
transition: transform 0.2s, border-color 0.2s;
}
.pricing-card.featured {
border-color: #00d4aa;
background: rgba(0,212,170,0.06);
transform: scale(1.03);
}
.pricing-card ul { list-style: none; }
.pricing-card ul li::before {
content: '\2713';
color: #00d4aa; margin-right: 0.5rem;
font-weight: 700;
}
.pricing-card.featured .btn {
background: linear-gradient(90deg, #00d4aa, #00a8cc);
color: #0a0a1a; border: none;
}sup element raises the dollar sign above baseline — style with vertical-align: super and font-size: 0.6em. For checkmarks, \2713 works in all browsers without loading an icon font. Stack transforms: transform: scale(1.03) translateY(-4px) on hover.Full Page with Responsive Nav
CompleteWhy learn this?
A landing page without navigation isn't a landing page — it's a flyer. Sticky navs with frosted-glass effects, hamburger menus for mobile, and smooth scroll anchors are the production standard for every modern SaaS site. Stripe, Linear, and Vercel all use this exact pattern. The hamburger menu is the most universally recognized mobile navigation pattern on the web — GitHub, Twitter, and Amazon all collapse their nav on small screens. This step teaches you sticky positioning, responsive breakpoints, JavaScript toggle patterns, and accessibility-first animation control.
What you built and why this way
Sticky positioning. position: sticky; top: 0 keeps the nav visible at the top of the viewport as the user scrolls. Unlike position: fixed, sticky navs stay inside their parent container's flow — if you have a top banner above the nav, the sticky nav won't overlap it. Sticky positioning has one important gotcha: it requires top, bottom, left, or right to be set. Without a threshold, the browser doesn't know when to activate the sticky behavior.
Frosted glass effect. backdrop-filter: blur(10px) combined with background: rgba(10,10,15,0.95) creates the translucent frosted-glass look. The backdrop-filter blurs everything behind the nav — page content, images, gradients — giving depth while keeping the nav readable. This is a relatively new CSS property (supported since Chrome 76, Safari 9, Firefox 103). For older browsers, the solid rgba() background is a graceful fallback since the element still has an opaque background.
Mobile hamburger strategy. The nav uses a CSS-first approach: .hamburger { display: none } on desktop, switched to display: flex at the 640px breakpoint. The .nav-links are visible by default on desktop and hidden on mobile. This progressive enhancement means the page works without JavaScript — the hamburger toggle is purely for convenience. The JS toggle itself is one line: classList.toggle('open') on the nav-links container. No frameworks, no dependencies.
Breakpoint choice. 640px is the standard "mobile" breakpoint used by Tailwind, Bootstrap, and most modern CSS frameworks. It targets phones in portrait mode (iPhone SE is 375px, iPhone 14 Pro is 390px) without affecting tablets (iPad Mini is 768px). The hamburger menu appears at 640px and below, where horizontal nav links would be too cramped. At this breakpoint, the sticky nav also switches to position: relative (iOS Safari has bugs with sticky + fixed + mobile chrome).
Unified mobile grid. All three grid sections (features, testimonials, pricing) switch to grid-template-columns: 1fr on mobile so their cards stack vertically. The .pricing-card.featured loses its scale(1.03) transform because the visual emphasis doesn't work in a single-column stack where every card is full width. On mobile, the featured card is distinguished only by its accent border.
Reduced motion. The @media (prefers-reduced-motion: reduce) query sets all animations and transitions to near-zero duration (0.01ms). This is an accessibility requirement — users with vestibular disorders can experience nausea from parallax, zoom, and slide animations. The !important is justified here because it's an accessibility override that should beat any specificity level.
Common pitfalls
position: stickywon't work if any parent element hasoverflow: hidden,overflow: auto, oroverflow: scroll— the overflow creates a new scroll container that clips the sticky behavior. This is the #1 cause of "sticky not working" bugsbackdrop-filterrequires a semi-transparent background to show the blur effect. Withoutbackground: rgba(...), the blur has nothing to blend with- Hamburger menus need the button outside the nav-links container so clicking the hamburger doesn't toggle the links themselves. Keep the button as a sibling, not a child
- Always add
aria-label="Toggle navigation"to hamburger buttons — screen reader users need to know what the button does, and a bare hamburger icon has no semantic meaning - Test sticky nav with dynamic content (images, embeds) — some browsers repaint the sticky element every frame during scroll, causing jank on low-end devices. Adding
will-change: transformcan help
Key concepts
position: sticky; top: 0— Sticky nav that follows scroll. Fails silently if parent has overflowbackdrop-filter: blur(10px)— Frosted glass effect. Semi-transparent bg required for the blur to show@media (max-width: 640px)— Standard mobile breakpoint (Tailwind/Bootstrap convention)classList.toggle('open')— One-line JS mobile menu toggle. No frameworks needed@media (prefers-reduced-motion: reduce)— Accessibility requirement, not optional. Use!importantfor safetywill-change: transform— Performance hint for sticky elements on low-end devices
Next up
All 5 steps complete. Ready for the next project: a documentation page with sidebar navigation.
<nav>
<div class="nav-inner">
<div class="logo">
Agent<span>Forge</span>
</div>
<div class="nav-links"
id="navLinks">
<a href="#">Features</a>
<a href="#">Pricing</a>
<a href="#"
class="nav-cta">
Get Started
</a>
</div>
<button class="hamburger"
id="hamburger"
aria-label="Menu">
<span></span>
<span></span>
<span></span>
</button>
</div>
</nav>nav { position: sticky; top: 0; z-index: 100;
background: rgba(10,10,15,0.95);
backdrop-filter: blur(10px); }
.nav-inner {
max-width: 1100px; margin: 0 auto;
display: flex; align-items: center;
justify-content: space-between;
padding: 1rem 2rem;
}
.hamburger { display: none; }
@media (max-width: 640px) {
nav { position: relative; }
.hamburger { display: flex; }
.nav-links {
display: none;
position: absolute; top: 100%;
flex-direction: column;
}
.nav-links.open { display: flex; }
.feature-grid,
.testimonial-grid,
.pricing-grid {
grid-template-columns: 1fr;
}
.pricing-card.featured { transform: none; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}backdrop-filter: blur(10px) needs a semi-transparent background — pair with background: rgba(10,10,15,0.85). The JS toggle is one line: document.getElementById('navLinks').classList.toggle('open'). Wrap all animations in @media (prefers-reduced-motion: reduce) for accessibility.Lessons Learned — Build Process
The AI challenges, design insights, and pipeline improvements from building Product Landing with AI.
Product landing pages are the most common web layout in existence. Every startup, SaaS company, and online store needs one. The pattern is universal: hero section (value prop + CTA), features grid (what you get), testimonials (social proof), pricing (commitment), and footer. Yahoo, Amazon, and Google all started with variations of this pattern. The transferable principle: a landing page is a persuasion engine. Every section has a job — attract attention, build desire, overcome objections, drive action. The HTML/CSS is just the medium; the psychology is the real product.
The AI produced a hero section that was too tall — 100vh with large padding made the entire feature grid invisible without scrolling. The CTA button blended into the background because the AI used the same color for the button and the hero's decorative elements. The pricing table had alignment issues: columns didn't line up because the AI used inconsistent widths across tiers.
Layout verification now includes: (1) hero section height clamped to min-height: 60vh; max-height: 80vh to ensure feature content is visible above the fold, (2) CTA contrast check — button background vs page background must differ by at least 30% luminance, (3) pricing table alignment — all columns must share the same grid-template-columns definition, not individual widths.
Each app builds on the last. The bugs found in product-landing were fixed before the next app was built — and every bug saves time on every future app.