diff --git a/skills/.claude-plugin/plugin.json b/skills/.claude-plugin/plugin.json index a384652..deed11a 100644 --- a/skills/.claude-plugin/plugin.json +++ b/skills/.claude-plugin/plugin.json @@ -14,6 +14,7 @@ "./skills/general/caveman", "./skills/general/grill-me", "./skills/general/handoff", + "./skills/general/landing-page-prototype", "./skills/general/write-a-skill" ] } diff --git a/skills/README.md b/skills/README.md index 3a7c413..0238128 100644 --- a/skills/README.md +++ b/skills/README.md @@ -12,6 +12,7 @@ No per-repo setup required — available immediately after installing the plugin | [`/formsg-oracle`](general/formsg-oracle/SKILL.md) | Answer FormSG "why" questions from prior decisions in Notion and Slack — cite every claim | | [`/grill-me`](general/grill-me/SKILL.md) | Stress-test a plan by being interviewed relentlessly, one decision at a time | | [`/handoff`](general/handoff/SKILL.md) | Compact the current conversation into a handoff doc for a fresh agent | +| [`/landing-page-prototype`](general/landing-page-prototype/SKILL.md) | Build HTML landing page prototypes by studying a reference site's design system and adapting it for a brand brief | | [`/ubiquitous-language`](general/ubiquitous-language/SKILL.md) | Extract a DDD-style glossary from the conversation and save it to `UBIQUITOUS_LANGUAGE.md` | | [`/write-a-skill`](general/write-a-skill/SKILL.md) | Create new skills with proper structure and bundled resources | | [`/humanizer`](general/humanizer/SKILL.md) | Remove signs of AI writing from text, making it more natural and human | diff --git a/skills/general/landing-page-prototype/SKILL.md b/skills/general/landing-page-prototype/SKILL.md new file mode 100644 index 0000000..74da094 --- /dev/null +++ b/skills/general/landing-page-prototype/SKILL.md @@ -0,0 +1,208 @@ +--- +name: landing-page-prototype +description: Build high-quality HTML landing page prototypes by forensically studying a reference website's design system and adapting it for a brand brief. Use when the user provides a reference URL and wants a landing page prototype, or says things like "build a prototype based on [URL]", "create a landing page inspired by [site]", "prototype this for [product]", "make a landing page like [reference]". Also use when the user mentions brand exploration, brand expression prototypes, or landing page variations. +--- + +# Landing Page Prototype Builder + +Build landing page prototypes by forensically studying a reference website and adapting its design system to a brand brief. The reference's actual CSS is the source of truth, not descriptions or screenshots. The output should feel like it belongs in the same design universe as the reference, applied to the user's product. + +## Why depth matters + +AI-generated landing pages converge on the same generic patterns because the reference study is shallow. A screenshot tells you "the heading is big and bold." The CSS tells you "the heading is Rubik at 83px, weight 400, letter-spacing -2.5px, line-height 1.1, color rgb(26, 36, 96), and everything on the page uses a single font weight with hierarchy achieved through size alone." The second produces a prototype that actually feels like the reference. The first produces another template. + +The difference between a good prototype and a generic one is almost always in the details that get skipped: the hover easing curve, the font terminal shape, the card scale direction (up vs. down), the shadow depth on hover, the specific way background colors are applied to sections vs. cards vs. body. These details compound into something that feels designed rather than generated. + +## Required tools + +- **Playwright MCP** (browser_navigate, browser_evaluate, browser_take_screenshot, browser_hover) for studying reference sites +- **Write** for creating the HTML file +- **Read** for viewing screenshots + +## Workflow + +### Step 1: Gather inputs + +**Reference URL** (required): The website to study. + +**Brand brief** (optional): Could be a file path, inline instructions, or conversation context. Extract: product name, what it does, primary color, target audience, key copy, content sections, constraints. If not provided, ask for at minimum: product name, what it does, primary color. + +### Step 2: Study the reference + +This is the most important step. Treat the reference site as source material to be forensically analyzed, not inspiration to glance at. Run **each investigation separately** since they look at different aspects. Do not combine them into a single pass. + +Load the extraction scripts from `references/extraction-scripts.md` and run them in this order: + +**2a. Screenshots first** - Take a viewport screenshot (hero feel), then scroll through the page **viewport by viewport** (scroll 600-800px, screenshot, repeat) to see every section transition. Do NOT rely on a single full-page screenshot — full-page screenshots freeze all scroll-driven animations at their initial state and hide transitions between sections (element-to-section expansions, section-to-card shrinks, parallax reveals). You need to see the page the way a user sees it: one viewport at a time, watching what changes between scroll positions. + +**2b. Typography system** (Script 1) - Extract every font, size, weight, line-height, letter-spacing, and color. After running, answer: +- How many font families? How many weights? +- Are terminals rounded or flat? Is the font geometric or humanist? +- Is hierarchy achieved through weight, size, or both? +- What color is the body text? (pure black? tinted? deep purple? dark blue?) +- **Optical weight** (check the screenshot, not just CSS): How thick do the strokes actually look at display size? A font at CSS weight 400 can look thin (Cormorant) or thick (GT Alpina, Fraunces) depending on the typeface's design. When substituting, match the visual thickness you see in the screenshot, not the CSS font-weight number. + +**2c. Color system** (Script 2) - Extract backgrounds, gradients, borders, CTA colors. After running, answer: +- Monochromatic or multicolor palette? +- How are background colors applied? (alternating sections? cards? accent blocks?) +- What's the CTA button color, border-radius, and padding? + +**2d. Layout structure** (Script 3) - Extract section order, content widths, grid/flex patterns. This reveals the page skeleton. + +**2e. Hover states** (Script 4) - Extract all :hover CSS rules and transition properties. This is commonly skipped and makes a huge difference. After running, answer: +- Do elements scale UP on hover (lift effect) or DOWN (press effect)? +- What easing function? (cubic-bezier values matter) +- Do inner elements react differently from their containers? +- What shadow changes on hover? + +**2f. Scroll animations** (Script 5) - Extract sticky elements, scroll-driven animations, animation libraries. Identify the scroll behavior pattern (parallax? sticky swap? fade-in-on-scroll? card reveal with stagger?). + +**2g. Visual elements** (Script 6) - Extract images, SVGs, videos, decorative elements. Identify the illustration style: product screenshots, flat illustrations, collage/sticker style, abstract shapes, photography, or video. + +**2h. Card deep dive** (Script 7, if card-based layout) - Extract exact card dimensions, border-radius, padding, shadows, composition. + +**2i. Structural patterns** (Script 8) - This is the highest-level analysis and catches architectural decisions that individual CSS properties don't reveal. After running, answer: +- **Nav shape**: Full-width sticky bar? Floating pill/oval centered on the page? Transparent overlay? Does it have a visible inner container with its own border-radius? +- **Section containment**: Are sections full-bleed (edge-to-edge background) or wrapped in rounded cards floating on a visible page background? This is one of the most distinctive architectural choices a page can make. "Content on a canvas" (Phantom) vs. "stacked full-width sections" (most sites) feel completely different. +- **Hero**: Full-viewport section or a card sitting on the page? +- **Footer**: Full-width or card-wrapped to match the sections? +- **Page background**: Is the body background visible between card-wrapped sections? What color is it? +- **Edge-bleed elements**: Are any background shapes or panels positioned to extend beyond the viewport edge? (e.g., a gray panel that starts mid-page and extends past the right edge, with only left corners rounded). These "flush-to-edge" panels are a distinctive spatial pattern that's invisible in individual element CSS but defines the page's feel. Check for elements with very large widths, asymmetric border-radius (like `64px 0 0 64px`), or negative margins/absolute positioning that pushes past the viewport. + +These structural patterns define the page's spatial character. A floating pill nav on a page of card-wrapped sections feels like objects on a desk. Full-width sections with a sticky bar feel like scrolling through rooms. Both are valid but they produce very different experiences. If you miss this, the prototype will default to "stacked full-width sections" regardless of what the reference actually does. + +**2j. Scroll-driven transitions** (Script 9) - Static CSS extraction can't detect properties that change based on scroll position (GSAP ScrollTrigger, Framer Motion, CSS scroll-timeline). This step detects **shape-shifting containers**: sections that change border-radius, width, or transform as you scroll. Run the scroll transition script, which scrolls the page in increments and compares element properties at each position. After running, answer: +- Do any sections **expand from a smaller element**? (e.g., a button that scales up to become a full section background) +- Do any sections **shrink into cards** as you scroll past them? (full-bleed section gains border-radius and inset margins) +- Is the **entire page content wrapped in a card** that's only revealed at the bottom? (content container has border-radius, sits on a colored page background visible at the very end) +- Do sections **slide over each other**? (sticky section stays while next section scrolls on top of it) + +These scroll-driven spatial transformations are some of the most distinctive design patterns a page can have. They're invisible to static CSS extraction and invisible in full-page screenshots. You can only see them by scrolling through viewport by viewport. + +### Step 3: Write the Reference Design Spec + +Before writing any code, produce a structured spec and share it with the user. This is not optional. It's the bridge between raw CSS data and the prototype. Format it like this: + +``` +## Reference Design Spec: [Reference Site Name] + +### Typography +- Display font: [name], [size range], weight [X], letter-spacing [X], line-height [X] +- Body font: [name], [size], weight [X] +- Text color: [exact rgb value and what it feels like - "deep navy, not black"] +- Hierarchy method: [size only / weight + size / weight only] +- Font substitute (if commercial): [Google Font name] because [specific reason it matches] + +### Color System +- Body background: [rgb] +- Section backgrounds: [list with where each is used] +- CTA: [color], border-radius [X], padding [X] +- Accent colors: [list] +- Color application pattern: [how colors are distributed across the page] + +### Layout +- Content max-width: [X]px +- Section flow: [ordered list of sections with their background colors] +- Grid/card pattern: [columns, gaps, card dimensions] + +### Structural Architecture +- Nav: [full-width sticky bar / floating pill / transparent overlay] with [border-radius, background, backdrop-filter] +- Sections: [full-bleed / card-wrapped on visible page background] +- Hero: [full-viewport section / card floating on page] +- Footer: [full-width / card-wrapped] +- Page background: [color visible between sections, if card-wrapped] +- Heading containment: [grouped (headings inside card wrappers with content) / scattered (headings naked on page bg, content cards separate)] +- Overall spatial feel: ["objects on a canvas" / "stacked rooms" / "scrolling document"] + +### Interactions +- Card hover: [scale direction, amount, easing, shadow change] +- Inner element hover: [what happens to children on container hover] +- Scroll entrance: [how elements enter - fade, scale, slide, stagger delay] +- Sticky behavior: [what sticks, at what scroll position] +- Special animations: [any distinctive interactions] + +### Visual Language +- Illustration style: [screenshots / collage / flat / abstract / hand-drawn / none] +- Decorative elements: [blobs, dots, patterns, gradients - with colors and placement] +- Card visual composition: [what's inside the cards] + +### What Makes It Distinctive +- [The 1-2 design decisions that make this site THIS site] + +### Brand Expression Thesis +- "[One sentence: what this expression of the brand brief means]" +``` + +The brand expression thesis connects the reference's design language to the user's brand. Every decision in the build should trace back to it. + +### Step 4: Build the prototype + +Write a single self-contained HTML file with embedded CSS and JS. + +**Use the Reference Design Spec as your blueprint.** Every CSS value in the prototype should trace back to something you measured in Step 2, adapted for the brand brief. If the reference uses `scale(0.985)` on hover with `cubic-bezier(0.22, 1, 0.36, 1)`, use those exact values. If the reference's heading is 96px with letter-spacing -2.4px, use those proportions. + +**Match the reference's design system, not its content.** Use the reference's typography scale, spacing rhythm, color application patterns, layout structure, and interactions, but fill them with the brand brief's content. + +**Replicate the interactions.** If the reference has a press-down hover, a staggered card reveal, a sticky scroll section, or a collage-style illustration, build it. These interactions are what makes a site feel designed rather than generated. Use vanilla JS, no libraries. + +**Build product mockups in HTML/CSS.** These should look like real product UI. Use form fields, routing flows, email previews, security badges built from actual HTML elements, not placeholder rectangles. + +**Font substitution**: When the reference uses a commercial font, choose a Google Fonts alternative that matches the **specific characteristics** you identified (terminal shape, weight range, geometric vs. humanist, character width). Don't just pick a font from the same category. + +### Step 5: Self-review + +After writing the file, navigate to it in the browser. Take a screenshot of the hero, then the full page. Place them next to your reference screenshots and answer these questions. Do not skip any. + +**Subtraction check** (catches filler): +- List every element in your prototype that does NOT appear in the reference. For each one, state why you added it. If your reason is "it felt empty" or "it needed something," delete it. Decoration without a reason traceable to the brand expression thesis is filler. + +**Visual weight check** (catches font mismatches): +- Screenshot your hero headline next to the reference hero headline. Do the strokes look equally thick? Is the visual presence the same? If your version looks thinner or lighter, your font weight is wrong regardless of what the CSS number says. + +**Section-by-section diff** (catches structural drift): +- For each major section (hero, features, CTA, footer), name one specific difference between your prototype and the reference. Decide if each difference is an intentional adaptation (for the brand brief) or an accidental deviation. Fix the accidental ones. + +**Interaction check**: +- Test every hover state and scroll animation. Do they match the reference's behavior? + +Fix issues before presenting to the user. + +### Step 6: Present and review + +Show the user a screenshot and the live URL. Tell them: +- Your brand expression thesis +- What you replicated from the reference (be specific: "96px headings with inline emoji, press-down hover at scale(0.985), staggered card reveal") +- What you adapted for the brand brief +- Any trade-offs or limitations + +## Font Substitution Guide + +Common fonts like Inter or Roboto can be used if they genuinely match the reference's typographic intent. The test: "Am I choosing this font because it matches the reference, or because it's the first thing that comes to mind?" + +When substituting, match the **specific characteristics**: +- Terminal shape (rounded vs. flat vs. tapered) +- Weight range (does the reference use one weight or many?) +- Geometric vs. humanist +- Character width (condensed, normal, wide) +- x-height (tall vs. short) + +Some distinctive Google Fonts alternatives: +- Rounded terminals: Lexend, Nunito, Quicksand, Varela Round +- Flat terminals, geometric: Rubik, Sora, Manrope +- Thick serif: Fraunces, Libre Bodoni +- Clean sans: Outfit, Karla, Work Sans +- Editorial serif: Newsreader, Source Serif 4 + +## Common mistakes + +1. **One-pass CSS extraction.** Running a single browser_evaluate and moving on. The reference study needs 5-7 separate investigations (typography, colors, layout, hover, scroll, visuals, cards). Each reveals different details. + +2. **Ignoring hover behavior.** Hover states are invisible in screenshots. If you don't extract the CSS rules, you'll default to the generic "lift on hover" when the reference might use a press-down, a color shift, or a shadow change. Always run the hover script. + +3. **Wrong font characteristics.** Picking a font from the right category but with wrong terminal shape, weight, or character width. If the reference uses a geometric sans with flat terminals and you pick a rounded sans, the whole page feels different. + +4. **Skipping the self-review.** Building the prototype and presenting it without viewing it in the browser first. Always screenshot your own output before showing the user. + +5. **Decoration without purpose.** Every visual element must serve the brand expression thesis. If you can't explain why a decorative element is there in terms of the thesis, remove it. + +6. **Optimizing for speed.** Every prototype deserves the same forensic depth. Do not cut corners on subsequent prototypes. Do not delegate reference study to agents without visual context. diff --git a/skills/general/landing-page-prototype/references/extraction-scripts.md b/skills/general/landing-page-prototype/references/extraction-scripts.md new file mode 100644 index 0000000..216d402 --- /dev/null +++ b/skills/general/landing-page-prototype/references/extraction-scripts.md @@ -0,0 +1,568 @@ +# Reference Site Extraction Scripts + +Use these browser_evaluate scripts to extract the design system from a reference site. Run each one separately - they investigate different aspects. + +## Script 1: Typography System + +Extracts font families, sizes, weights, line heights, letter spacing, and colors for all text elements. + +```javascript +() => { + const samples = []; + document.querySelectorAll('h1, h2, h3, h4, p, a, button, span, li').forEach(el => { + const s = getComputedStyle(el); + const text = el.textContent.trim(); + if (samples.length < 30 && text.length > 2 && text.length < 100 && parseFloat(s.fontSize) > 10) { + samples.push({ + tag: el.tagName, + text: text.slice(0, 60), + fontSize: s.fontSize, + fontWeight: s.fontWeight, + fontFamily: s.fontFamily.split(',')[0].trim().replace(/"/g, ''), + lineHeight: s.lineHeight, + letterSpacing: s.letterSpacing, + color: s.color, + fontFeatureSettings: s.fontFeatureSettings, + fontVariationSettings: s.fontVariationSettings + }); + } + }); + const body = getComputedStyle(document.body); + return { + bodyFont: body.fontFamily, + bodyFontSize: body.fontSize, + bodyColor: body.color, + typeSamples: samples + }; +} +``` + +After running this, analyze: +- How many font families are used? (1 = single-font system, 2+ = paired) +- How many font weights? (1 = hierarchy through size only, multiple = weight-based hierarchy) +- What's the size range? (largest heading vs. smallest body text) +- Are the terminals rounded or flat? (compare against known fonts) +- What's the text color? (pure black, near-black, dark grey, or a tinted color like deep purple?) + +## Script 2: Color System & Backgrounds + +Extracts all background colors, gradients, and accent colors. + +```javascript +() => { + const bgColors = new Set(); + const gradients = []; + const borderColors = new Set(); + document.querySelectorAll('section, div, main, header, footer, article, aside, nav').forEach(el => { + const s = getComputedStyle(el); + const bg = s.backgroundColor; + if (bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') bgColors.add(bg); + const bgImg = s.backgroundImage; + if (bgImg !== 'none' && bgImg.includes('gradient')) gradients.push(bgImg.slice(0, 200)); + const bc = s.borderColor; + if (bc && bc !== 'rgb(0, 0, 0)' && bc !== 'rgba(0, 0, 0, 0)') borderColors.add(bc); + }); + // Find CTA/accent colors from buttons and links + const ctaColors = []; + document.querySelectorAll('a, button').forEach(el => { + const s = getComputedStyle(el); + if (s.backgroundColor !== 'rgba(0, 0, 0, 0)' && s.backgroundColor !== 'transparent' && s.backgroundColor !== 'rgb(255, 255, 255)') { + ctaColors.push({ bg: s.backgroundColor, color: s.color, text: el.textContent.trim().slice(0, 30), borderRadius: s.borderRadius, padding: s.padding }); + } + }); + return { + bgColors: [...bgColors].slice(0, 15), + gradients: gradients.slice(0, 5), + borderColors: [...borderColors].slice(0, 10), + ctaColors: ctaColors.slice(0, 8) + }; +} +``` + +After running this, analyze: +- Is the page monochromatic (one hue family) or multicolor? +- How are background colors used? (alternating sections, cards vs. body, accent blocks) +- What's the CTA color and shape (border-radius, padding)? +- Are there gradients? Where? + +## Script 3: Layout Structure + +Extracts section order, content widths, grid/flex patterns, and spacing. + +```javascript +() => { + // Map the page's section flow + const sections = []; + const topLevel = document.querySelector('main') || document.body; + const directChildren = topLevel.children; + for (let i = 0; i < Math.min(directChildren.length, 20); i++) { + const el = directChildren[i]; + const s = getComputedStyle(el); + if (el.offsetHeight > 50) { + sections.push({ + tag: el.tagName, + class: el.className.toString().slice(0, 60), + height: el.offsetHeight, + bgColor: s.backgroundColor !== 'rgba(0, 0, 0, 0)' ? s.backgroundColor : 'transparent', + display: s.display, + maxWidth: s.maxWidth, + padding: s.padding, + text: el.textContent.trim().slice(0, 50) + }); + } + } + // Find content containers and their max-width + const containers = new Set(); + document.querySelectorAll('div, section, main').forEach(el => { + const mw = getComputedStyle(el).maxWidth; + if (mw !== 'none' && mw !== '100%' && parseFloat(mw) > 500) { + containers.add(mw); + } + }); + return { sections, contentWidths: [...containers] }; +} +``` + +## Script 4: Hover States & Micro-interactions + +Extracts all CSS :hover rules that involve transforms, shadows, or visual changes. + +```javascript +() => { + const hoverRules = []; + const transitionEls = []; + const sheets = Array.from(document.styleSheets); + sheets.forEach(ss => { + try { + const rules = Array.from(ss.cssRules || []); + rules.forEach(rule => { + const text = rule.cssText || ''; + if (text.includes(':hover') && ( + text.includes('scale') || text.includes('rotate') || + text.includes('translate') || text.includes('transform') || + text.includes('shadow') || text.includes('opacity') || + text.includes('background') + )) { + hoverRules.push(text.slice(0, 400)); + } + }); + } catch(e) {} + }); + // Find elements with transitions set + document.querySelectorAll('*').forEach(el => { + const s = getComputedStyle(el); + if (s.transition && !s.transition.includes('0s') && s.transition !== 'all' && el.offsetWidth > 50) { + if (transitionEls.length < 15) { + transitionEls.push({ + tag: el.tagName, + class: el.className.toString().slice(0, 50), + transition: s.transition.slice(0, 150), + cursor: s.cursor + }); + } + } + }); + return { hoverRules: hoverRules.slice(0, 15), transitionEls }; +} +``` + +After running this, identify the hover pattern: +- Do cards scale UP (common, lifts off page) or DOWN (press effect, like Phantom)? +- What easing function is used? (ease, ease-in-out, cubic-bezier?) +- Do inner elements react differently from outer containers? +- What cursor is used? (pointer, default?) + +## Script 5: Scroll Animations & Sticky Elements + +Extracts sticky positioning, scroll-driven CSS, and animation infrastructure. + +```javascript +() => { + const stickyEls = []; + const animatedEls = []; + document.querySelectorAll('*').forEach(el => { + const s = getComputedStyle(el); + if (s.position === 'sticky') { + stickyEls.push({ + tag: el.tagName, + class: el.className.toString().slice(0, 60), + top: s.top, + height: el.offsetHeight, + text: el.textContent.trim().slice(0, 40) + }); + } + if (s.animationName && s.animationName !== 'none') { + animatedEls.push({ + tag: el.tagName, + class: el.className.toString().slice(0, 50), + animation: s.animationName, + duration: s.animationDuration, + timing: s.animationTimingFunction + }); + } + }); + // Check for scroll-driven animation APIs + let hasScrollTimeline = false; + const sheets = Array.from(document.styleSheets); + sheets.forEach(ss => { + try { + Array.from(ss.cssRules || []).forEach(rule => { + const text = rule.cssText || ''; + if (text.includes('scroll-timeline') || text.includes('animation-timeline') || text.includes('view-timeline')) { + hasScrollTimeline = true; + } + }); + } catch(e) {} + }); + // Check for animation libraries in scripts + const scriptSrcs = Array.from(document.querySelectorAll('script[src]')).map(s => s.src); + const animLibs = scriptSrcs.filter(s => s.includes('gsap') || s.includes('framer') || s.includes('lenis') || s.includes('locomotive') || s.includes('aos')); + return { stickyEls, animatedEls: animatedEls.slice(0, 10), hasScrollTimeline, animLibs }; +} +``` + +## Script 6: Visual Elements & Illustration Style + +Extracts images, SVGs, videos, and decorative elements to understand the visual language. + +```javascript +() => { + const visuals = []; + document.querySelectorAll('img, svg, video').forEach(el => { + if (el.offsetWidth > 40 && el.offsetHeight > 40) { + visuals.push({ + tag: el.tagName, + width: el.offsetWidth, + height: el.offsetHeight, + src: el.tagName === 'IMG' ? (el.src || '').slice(0, 120) : (el.tagName === 'VIDEO' ? 'video-element' : 'inline-svg'), + alt: el.alt || '', + class: el.className.toString().slice(0, 50), + borderRadius: getComputedStyle(el).borderRadius + }); + } + }); + // Check for decorative elements (absolute/fixed positioned, low-content divs) + const decorative = []; + document.querySelectorAll('div, span').forEach(el => { + const s = getComputedStyle(el); + if ((s.position === 'absolute' || s.position === 'fixed') && el.children.length === 0 && el.offsetWidth > 20) { + if (decorative.length < 10) { + decorative.push({ + width: el.offsetWidth, + height: el.offsetHeight, + borderRadius: s.borderRadius, + bgColor: s.backgroundColor, + opacity: s.opacity, + filter: s.filter !== 'none' ? s.filter : null + }); + } + } + }); + return { visuals: visuals.slice(0, 15), decorativeElements: decorative }; +} +``` + +After running this, identify: +- Are visuals product screenshots, illustrations, photos, or videos? +- What's the illustration style? (flat, 3D, hand-drawn, collage, abstract shapes) +- Are there decorative blobs, dots, or background shapes? +- What border-radius do images/cards use? + +## Script 7: Card Structure Deep Dive + +When the reference uses a card-based layout, extract exact card dimensions and composition. + +```javascript +() => { + const cards = []; + document.querySelectorAll('div, article, section').forEach(el => { + const s = getComputedStyle(el); + const br = parseFloat(s.borderRadius); + const w = el.offsetWidth; + const h = el.offsetHeight; + if (br > 12 && w > 200 && h > 200 && w < 800) { + cards.push({ + width: w, + height: h, + borderRadius: s.borderRadius, + bgColor: s.backgroundColor, + padding: s.padding, + boxShadow: s.boxShadow !== 'none' ? s.boxShadow.slice(0, 80) : 'none', + overflow: s.overflow, + display: s.display, + flexDirection: s.flexDirection, + gap: s.gap, + childCount: el.children.length, + text: el.textContent.trim().slice(0, 50) + }); + } + }); + return { cards: cards.slice(0, 12) }; +} +``` + +## Script 8: Structural Patterns + +Higher-level structural analysis. The previous scripts extract CSS values, but this one identifies **how the page is architecturally composed**. Things like whether the nav floats or spans full-width, whether content sections are full-bleed or wrapped in cards, whether the hero is a card or a section. These structural decisions define the page's character just as much as colors and fonts do. + +```javascript +() => { + // NAV STRUCTURE + const nav = document.querySelector('nav') || document.querySelector('header'); + let navInfo = null; + if (nav) { + const ns = getComputedStyle(nav); + const navInner = nav.querySelector('div') || nav; + const nis = getComputedStyle(navInner); + navInfo = { + position: ns.position, + width: nav.offsetWidth, + viewportWidth: window.innerWidth, + isFullWidth: nav.offsetWidth >= window.innerWidth - 20, + borderRadius: ns.borderRadius, + innerBorderRadius: nis.borderRadius, + background: ns.backgroundColor, + backdropFilter: ns.backdropFilter, + border: ns.border, + boxShadow: ns.boxShadow !== 'none' ? ns.boxShadow.slice(0, 80) : 'none', + margin: ns.margin, + padding: ns.padding, + maxWidth: ns.maxWidth, + top: ns.top, + // Is it a floating pill? (has border-radius, not full-width, or has margin) + isFloatingPill: parseFloat(ns.borderRadius) > 20 || (parseFloat(ns.margin) > 0 && ns.position === 'fixed') + }; + // Check if nav has a visible container with its own border-radius + nav.querySelectorAll('div').forEach(el => { + const es = getComputedStyle(el); + if (parseFloat(es.borderRadius) > 20 && el.offsetWidth > 200) { + navInfo.innerContainerRadius = es.borderRadius; + navInfo.innerContainerBg = es.backgroundColor; + navInfo.innerContainerBorder = es.border; + navInfo.hasFloatingInnerContainer = true; + } + }); + } + + // SECTION CONTAINMENT: Are sections full-bleed or card-wrapped? + const sectionPatterns = []; + const mainContent = document.querySelector('main') || document.body; + const topChildren = mainContent.querySelectorAll(':scope > section, :scope > div, :scope > article'); + topChildren.forEach(el => { + if (el.offsetHeight > 100 && sectionPatterns.length < 12) { + const s = getComputedStyle(el); + const isFullWidth = el.offsetWidth >= window.innerWidth - 20; + const hasRadius = parseFloat(s.borderRadius) > 0; + const hasBg = s.backgroundColor !== 'rgba(0, 0, 0, 0)' && s.backgroundColor !== 'transparent'; + sectionPatterns.push({ + tag: el.tagName, + width: el.offsetWidth, + isFullWidth, + borderRadius: s.borderRadius, + isCardWrapped: hasRadius && !isFullWidth, + bgColor: hasBg ? s.backgroundColor : 'transparent', + margin: s.margin, + maxWidth: s.maxWidth, + boxShadow: s.boxShadow !== 'none' ? 'has-shadow' : 'none', + overflow: s.overflow, + text: el.textContent.trim().slice(0, 40) + }); + } + }); + + // HERO STRUCTURE + const heroEl = mainContent.querySelector('section:first-of-type') || mainContent.querySelector(':scope > div:first-of-type'); + let heroInfo = null; + if (heroEl) { + const hs = getComputedStyle(heroEl); + heroInfo = { + isFullViewport: heroEl.offsetHeight >= window.innerHeight * 0.8, + isFullWidth: heroEl.offsetWidth >= window.innerWidth - 20, + isCardWrapped: parseFloat(hs.borderRadius) > 0 && heroEl.offsetWidth < window.innerWidth - 20, + borderRadius: hs.borderRadius, + minHeight: hs.minHeight, + height: heroEl.offsetHeight + }; + } + + // FOOTER STRUCTURE + const footer = document.querySelector('footer'); + let footerInfo = null; + if (footer) { + const fs = getComputedStyle(footer); + footerInfo = { + isFullWidth: footer.offsetWidth >= window.innerWidth - 20, + borderRadius: fs.borderRadius, + isCardWrapped: parseFloat(fs.borderRadius) > 0 && footer.offsetWidth < window.innerWidth - 20, + bgColor: fs.backgroundColor + }; + } + + // PAGE-LEVEL CONTAINMENT + const bodyBg = getComputedStyle(document.body).backgroundColor; + const htmlBg = getComputedStyle(document.documentElement).backgroundColor; + + // CONTAINMENT ANALYSIS: Are headings grouped with their content cards, + // or do headings float independently on the page background? + // This distinguishes "grouped sections" (heading + cards in a wrapper) + // from "scattered objects" (heading naked on bg, cards naked on bg). + const containmentAnalysis = []; + const bigHeadings = document.querySelectorAll('h2'); + bigHeadings.forEach(h => { + const fontSize = parseFloat(getComputedStyle(h).fontSize); + if (fontSize > 50) { + // Walk up to find the nearest container with background or border-radius + let el = h.parentElement; + let nearestCard = null; + for (let i = 0; i < 8 && el && el !== document.body; i++) { + const s = getComputedStyle(el); + if (parseFloat(s.borderRadius) > 12 || (s.backgroundColor !== 'rgba(0, 0, 0, 0)' && s.backgroundColor !== 'transparent' && s.backgroundColor !== htmlBg && s.backgroundColor !== bodyBg)) { + nearestCard = { + tag: el.tagName, + borderRadius: s.borderRadius, + bgColor: s.backgroundColor, + width: el.offsetWidth, + depth: i + }; + break; + } + el = el.parentElement; + } + // Check if sibling cards share the same container + const headingParent = h.closest('[class]'); + const siblingCards = headingParent ? headingParent.querySelectorAll('div').length : 0; + + containmentAnalysis.push({ + headingText: h.textContent.trim().slice(0, 40), + fontSize: fontSize + 'px', + isNakedOnPageBg: nearestCard === null, + nearestCardWrapper: nearestCard, + note: nearestCard === null ? 'Heading floats directly on page background' : 'Heading is inside a card container at depth ' + nearestCard.depth + }); + } + }); + + return { + nav: navInfo, + sectionPatterns, + hero: heroInfo, + footer: footerInfo, + pageBg: { body: bodyBg, html: htmlBg }, + containmentAnalysis, + summary: { + isNavFloating: navInfo ? navInfo.isFloatingPill || (navInfo.hasFloatingInnerContainer === true) : false, + areSectionsCardWrapped: sectionPatterns.some(s => s.isCardWrapped), + isHeroCardWrapped: heroInfo ? heroInfo.isCardWrapped : false, + isFooterCardWrapped: footerInfo ? footerInfo.isCardWrapped : false, + headingsAreNaked: containmentAnalysis.every(h => h.isNakedOnPageBg) + } + }; +} +``` + +After running this, answer these structural questions: +- **Nav**: Is it a full-width sticky bar, a floating pill/oval, or a transparent overlay? Does it have its own background card within the nav? +- **Sections**: Are they full-bleed (edge-to-edge) or wrapped in rounded cards floating on a page background? +- **Hero**: Is it a full-viewport section or a card sitting on the page? +- **Footer**: Full-width or card-wrapped? +- **Page background**: Is the body/html background visible between elements? What color is it? +- **Heading containment** (this is subtle but critical): Do the big section headings live INSIDE card wrappers alongside their content, or do they float NAKED on the page background with their content cards as separate elements below? The difference: + - "Grouped": heading + cards wrapped together in a parent card = sections feel like contained panels + - "Scattered": heading naked on bg, cards as individual pieces below = page feels like objects arranged on a desk/canvas + + Phantom uses the "scattered" pattern. Most SaaS sites use the "grouped" pattern. Getting this wrong changes the entire spatial character of the page. + +These structural patterns define whether the page feels like "content on a canvas" (card-wrapped, like Phantom) vs. "stacked sections" (full-bleed, like most sites). This is one of the most distinctive architectural decisions a landing page makes and is easily overlooked. + +## Script 9: Scroll-Driven Transitions + +Static CSS extraction can't detect properties that change based on scroll position. This script scrolls the page in increments and compares element properties at each position to detect **shape-shifting containers** — sections that change border-radius, width, or transform as you scroll. + +**Important**: This script must be run using `browser_run_code_unsafe` (not `browser_evaluate`) because it needs to programmatically scroll the page and wait between positions. + +```javascript +async (page) => { + const pageHeight = await page.evaluate(() => document.body.scrollHeight); + const viewportHeight = await page.evaluate(() => window.innerHeight); + const increment = Math.round(viewportHeight * 0.75); + const positions = []; + for (let y = 0; y <= pageHeight; y += increment) { + positions.push(y); + } + // Cap at 25 positions to avoid excessive runtime + const sampledPositions = positions.length > 25 + ? positions.filter((_, i) => i % Math.ceil(positions.length / 25) === 0) + : positions; + + const snapshots = []; + for (const scrollY of sampledPositions) { + await page.evaluate((y) => window.scrollTo(0, y), scrollY); + await page.waitForTimeout(200); // let scroll-driven animations settle + + const data = await page.evaluate(() => { + const containers = []; + // Broad selector: any div large enough to be a section-level container. + // Only track elements with interesting visual properties to keep the list manageable. + document.querySelectorAll('div').forEach(el => { + if (el.offsetHeight > 300 && el.offsetWidth > 600 && containers.length < 40) { + const s = getComputedStyle(el); + const br = s.borderRadius; + const transform = s.transform; + const inlineStyle = el.getAttribute('style') || ''; + if (br !== '0px' || transform !== 'none' || inlineStyle.includes('transform') || inlineStyle.includes('border-radius') || inlineStyle.includes('scale')) { + containers.push({ + id: (el.className.toString().slice(0, 35) || el.tagName) + '_' + el.offsetTop, + width: el.offsetWidth, + height: el.offsetHeight, + borderRadius: br, + transform: transform !== 'none' ? transform.slice(0, 80) : 'none', + opacity: s.opacity + }); + } + } + }); + return containers; + }); + snapshots.push({ scrollY, containers: data }); + } + + // Compare snapshots to find elements whose properties changed + const changes = []; + const seen = new Set(); + for (let i = 1; i < snapshots.length; i++) { + const prev = snapshots[i - 1]; + const curr = snapshots[i]; + for (const cEl of curr.containers) { + const pEl = prev.containers.find(p => p.id === cEl.id); + if (pEl && !seen.has(cEl.id)) { + const diffs = []; + if (pEl.borderRadius !== cEl.borderRadius) diffs.push(`borderRadius: ${pEl.borderRadius} → ${cEl.borderRadius}`); + if (Math.abs(pEl.width - cEl.width) > 20) diffs.push(`width: ${pEl.width} → ${cEl.width}`); + if (pEl.transform !== cEl.transform) diffs.push(`transform changed`); + if (pEl.opacity !== cEl.opacity) diffs.push(`opacity: ${pEl.opacity} → ${cEl.opacity}`); + if (diffs.length > 0) { + changes.push({ + element: cEl.id, + scrollRange: `${prev.scrollY}px → ${curr.scrollY}px`, + changes: diffs + }); + seen.add(cEl.id); + } + } + } + } + return { totalPositions: snapshots.length, shapeshifters: changes }; +} +``` + +After running this, look for: +- **border-radius changes**: A section going from `0px` to `40px` means it's shrinking into a card as you scroll past it. +- **width changes**: A container getting narrower means it's pulling away from the viewport edges (section → card). +- **transform changes**: Scale or translate changes mean elements are expanding/shrinking/sliding based on scroll. +- **margin changes**: Margin appearing means the element is gaining inset from the viewport edges. + +Common patterns this catches: +- **Button-to-section expansion**: A small pill element scales up to become a full-section background +- **Section-to-card shrink**: A full-bleed section gains border-radius and margin as you scroll past, becoming a card +- **Page-is-a-card reveal**: The outermost content container has border-radius, sitting on a colored body background that's only visible at the very top and bottom of the scroll +- **Sticky overlap**: One section stays fixed while the next slides over it