style-extractor
Extract evidence-based style guides and motion appendices from websites or web apps. Use when Codex needs reusable visual language, semantic tokens, component/state rules, runtime animation evidence, or style references that preserve design signal while stripping product-specific structure and content.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install lucent-snow-style-extractor
Repository
Extract evidence-based style guides and motion appendices from websites or web apps. Use when Codex needs reusable visual language, semantic tokens, component/state rules, runtime animation evidence, or style references that preserve design signal while stripping product-specific structure and content.
Open repositoryBest for
Primary workflow: Write Technical Docs.
Technical facets: Full Stack, Tech Writer, Designer.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: Lucent-Snow.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install style-extractor into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://www.skillhub.club/skills/lucent-snow-style-extractor before adding style-extractor to shared team environments
- Use style-extractor for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: style-extractor
description: Extract evidence-based style guides and motion appendices from websites or web apps. Use when Codex needs reusable visual language, semantic tokens, component/state rules, runtime animation evidence, or style references that preserve design signal while stripping product-specific structure and content.
---
# Style Extractor (Web Style + Motion)
Extract a reusable design system from **web UIs**: colors, typography, spacing, components, states, and, when relevant, motion (timings, keyframes, delay chains, JS-driven behavior).
Core principle:
- Extract **visual language and interaction rules**, not the source product's information architecture.
- Keep what is reusable: tokens, component patterns, state changes, layout tendencies, motion cadence.
- Strip what is product-specific: business copy, app logic, navigation tree, feature taxonomy, marketing claims.
## Output location (REQUIRED)
- Save all generated deliverables under: `%USERPROFILE%\\style-extractor\\`
- Never write generated outputs under the skill folder (`.codex/skills/...`)
- Never dump screenshots, CSS, JS, and Markdown side-by-side in the root output directory
Each extraction must create a dedicated project folder:
```text
%USERPROFILE%\style-extractor\
<project>-<style>\
guides\
style-guide.md
motion-guide.md
evidence-manifest.md
evidence\
screenshots\
assets\
notes\
```
Required deliverables for every extraction:
- `%USERPROFILE%\\style-extractor\\<project>-<style>\\guides\\style-guide.md`
- `%USERPROFILE%\\style-extractor\\<project>-<style>\\guides\\evidence-manifest.md`
- `%USERPROFILE%\\style-extractor\\<project>-<style>\\evidence\\`
Required when motion is meaningful:
- `%USERPROFILE%\\style-extractor\\<project>-<style>\\guides\\motion-guide.md`
Subdirectory rules:
- `guides/` only for final Markdown deliverables
- `evidence/screenshots/` only for PNG/JPG/WebP captures
- `evidence/assets/` only for downloaded CSS/JS/SVG/font stylesheets
- `evidence/notes/` only for raw dumps, runtime notes, traces, or helper text files
- Root project folder should contain folders only; no loose screenshots or loose guide files
Use these reference files:
- `references/output-contract.md` — required output structure and anti-patterns
- `references/style-guide-template.md` — required sections for the style guide
- `references/motion-guide-template.md` — required sections for the motion appendix
- `references/evidence-manifest-template.md` — required sections for the evidence manifest
## References (quality bar)
- `references/9nine-visual-novel/` — best-practice **style + motion** reference package
- `references/motherduck-design-system-reference/` — strong **static style** reference package
What to learn from these references:
- keep the document dense enough to feel like a real reference manual
- preserve semantic token tables with representative raw values
- preserve component state matrices with concrete observed values
- preserve copy-paste examples and practical implementation notes when they materially help reuse
What not to copy blindly:
- long generic filler or repeated product intro
- raw-value dumping without semantic grouping
- excessive product-specific branding or IA detail
- sections that exist only to make the document look long, without adding reuse value
## Required reference read-through (MANDATORY)
Before starting any extraction, read in this order:
1) `references/output-contract.md`
2) `references/style-guide-template.md`
3) `references/motion-guide-template.md`
4) `references/evidence-manifest-template.md`
5) all files under `references/9nine-visual-novel/guides/`
6) all files under `references/motherduck-design-system-reference/guides/`
Reference package selection rule:
- motion-heavy, media-heavy, or section-transition-driven source → use `references/9nine-visual-novel/` as the primary quality bar
- static-structure-heavy source → read `references/motherduck-design-system-reference/`
Do not start evidence collection, screenshotting, script execution, or drafting until both reference packages have been reviewed.
Extraction is incomplete if the final deliverable does not explicitly state:
- that both reference packages were reviewed before extraction
- which package was used as the primary quality bar for the current target
---
## Workflow
### Phase 0 — Inputs
1) Project name + style/variant name
2) Sources: URL / web repo / both
3) Motion assessment: does the site have meaningful motion? (determines whether Phase 1B is needed)
4) Reuse target: web app / marketing site / desktop app / design reference only
5) Primary reference package selected: 9nine / MotherDuck
Before gathering evidence, decide which source traits belong in the reusable style and which should be excluded. Use this simple filter:
- Keep: tokens, spacing rhythm, typography hierarchy, density, component language, state treatment, motion cadence
- Adapt carefully: layout primitives, panel structure, navigation patterns, content density
- Discard: product copy, feature-specific flows, brand story, app-specific IA, domain-specific labels
### Phase 0.5 — Read the reference package (REQUIRED)
Complete all of the following before Phase 1A:
- read `references/output-contract.md`
- read `references/style-guide-template.md`
- read `references/motion-guide-template.md`
- read `references/evidence-manifest-template.md`
- read `references/9nine-visual-novel/guides/style-guide.md`
- read `references/9nine-visual-novel/guides/motion-guide.md`
- read `references/9nine-visual-novel/guides/evidence-manifest.md`
- read `references/motherduck-design-system-reference/guides/style-guide.md`
- read `references/motherduck-design-system-reference/guides/motion-guide.md`
- read `references/motherduck-design-system-reference/guides/evidence-manifest.md`
- choose one of the two packages as the primary quality bar for the current extraction
If both packages are not read first, the extraction is invalid.
### Phase 1A — Static evidence gathering
#### Step 1 — Open page & screenshots
Use Chrome MCP tools:
- `new_page` / `select_page` / `navigate_page`
- `take_screenshot` (fullPage when helpful)
Minimum screenshot set:
1) baseline (full page/section)
2) navigation visible + active state
3) primary CTA: default + hover + pressed (if possible)
4) form controls: default + focus-visible (+ invalid if present)
5) modal/dialog open (if any)
#### Step 2 — Extract computed styles
Use `evaluate_script` to pull:
- CSS Variables (`:root` and scoped)
- Colors, typography, spacing from `getComputedStyle`
- Component state matrices (default / hover / active / focus-visible / disabled)
#### Step 3 — Pull CSS/JS bodies
- `list_network_requests` / `get_network_request` to download CSS files
- Useful for extracting `@keyframes`, CSS variables, and media queries offline
Minimum static evidence to keep in `...\\evidence\\`:
- screenshots under `evidence/screenshots/`
- downloaded CSS/JS under `evidence/assets/`
- notes and selector inventories under `evidence/notes/`
---
### Phase 1B — Motion evidence gathering (when site has meaningful motion)
> Static and motion are independent paths. Skip this phase if the site has no meaningful animation.
Follow these steps **in priority order**. Each level captures more; stop when you have enough evidence.
#### Level 1 — @keyframes extraction (no interaction needed)
Paste `scripts/transition-scanner.js`, then call:
```js
__seTransition.keyframes()
```
Returns all `@keyframes` definitions from stylesheets. This is deterministic and complete—no timing issues.
#### Level 2 — CSS transition scan (no interaction needed)
Same script, call:
```js
__seTransition.scan() // scan entire page
__seTransition.scan('.my-section') // scan specific section
```
Returns every element's `transition-property / duration / easing / delay` + clusters by pattern. This shows **what can animate** even if nothing is animating yet.
#### Level 3 — Interaction diff (requires triggering interactions)
This captures the dominant pattern on modern sites: **JS sets inline style → CSS transition interpolates**.
Paste `scripts/interaction-diff.js`, then:
```js
// 1. Watch elements you care about
__seDiff.watch([
'[class*="sectionContainer"]',
'[class*="illustration"]',
'[class*="navItem"]',
'button, a[class*="cta"], a[class*="CTA"]'
])
// 2. Trigger interaction and capture diff
__seDiff.triggerAndCapture(
() => document.querySelector('.nav-item-2').click(),
{ captureAt: 50, settleAt: 500 }
)
```
Returns:
- `diffs` — per-element before/after inline style + computed style changes
- `duringAnimations` — `document.getAnimations()` captured within the 50ms transition window
- `afterAnimations` — what's still running after settle
**Key insight**: `document.getAnimations()` only returns results during active CSS transitions. The capture window is typically <300ms. This script captures within that window automatically.
Repeat for 3+ key interactions (section change, component switch, hover, scroll).
#### Level 4 — JS library extraction
Paste `scripts/library-detect.js` to detect third-party animation libraries.
Returns `{ globals, instances, dom, fingerprints, assets }`.
Key: many modern sites bundle libraries as modules, so `window.Swiper` etc. may be undefined. The script also checks:
- `el.swiper` — Swiper instance on DOM elements (works when bundled)
- `[data-aos-*]` — AOS attributes on elements
- `[data-framer-*]` — Framer Motion attributes
- Asset URL hints (script/stylesheet URLs containing library names)
When instances are found, configs are extracted directly (Swiper params, AOS settings, etc.).
#### Level 5 — rAF sampling (fallback for opaque JS motion)
When the above levels don't capture enough (e.g., hand-written `requestAnimationFrame` loops), use `scripts/motion-tools.js`:
```js
__seMotion.sample('.animated-element', { durationMs: 800, include: ['transform', 'opacity'] })
```
Records computed style values every frame. Useful for inferring duration and property ranges, but cannot capture easing or intent.
#### Level 6 — Performance trace (optional, complex motion)
For very complex orchestrated animations:
- `performance_start_trace` / `performance_stop_trace`
- Analyze via `performance_analyze_insight`
---
### Phase 2 — Abstraction and de-productization (REQUIRED)
Do not write the guide as a product teardown. Convert evidence into reusable rules.
For each major finding, classify it explicitly:
- `Reusable`: can be copied directly as a token, component rule, state rule, or motion primitive
- `Adapted`: useful idea, but needs reshaping for the target product type
- `Discarded`: source-specific structure or content that should not be copied
Examples:
- A cloud-blue surface palette: `Reusable`
- A docs sidebar + article rail layout: `Adapted`
- An AI chat app's prompt-library IA: `Discarded`
- A marketing hero with video background: `Adapted` or `Discarded`, depending on target
### Phase 3 — Semantic tokenization (REQUIRED)
Do not stop at raw values. Convert repeated values into **semantic tokens**:
1) cluster repeated values (colors/radii/durations/easings/shadows)
2) map usage (CTA/text/border/overlay/active/etc.)
3) name by intent (e.g., `--color-accent`, `--motion-300`, `nav.switch.iconColor`)
4) keep evidence alongside tokens (raw values + element/selector/screenshot)
### Phase 4 — Write the deliverables
Start from the reference templates. Do not improvise the top-level structure unless the source clearly requires one.
#### Deliverable A — Style guide (REQUIRED)
Follow `references/style-guide-template.md`.
This file must answer:
- what the style feels like
- which tokens define it
- how components behave across states
- what layout tendencies are reusable
- how to rebuild the visual language without copying the product itself
#### Deliverable B — Motion guide (REQUIRED when dynamic)
Follow `references/motion-guide-template.md`.
This file must answer:
- what moves
- what triggers motion
- durations, delays, easing, and keyframes
- whether motion is CSS-driven, transition-driven, or JS/library-driven
- which motion primitives are reusable in another product
#### Deliverable C — Evidence manifest (REQUIRED)
Follow `references/evidence-manifest-template.md`.
Include:
- source URLs / repo refs / date captured
- screenshot list
- downloaded CSS/JS files or notes
- scripts used
- interactions tested
- gaps, blockers, and confidence notes
- reference package reviewed and which package files were read before extraction
Component state matrix must include at least:
- default / hover / active(pressed) / focus-visible / disabled
- include loading / selected / invalid when those states exist
---
## Scripts reference
| Script | Namespace | Purpose |
|--------|-----------|---------|
| `scripts/transition-scanner.js` | `__seTransition` | Scan CSS transitions + extract @keyframes |
| `scripts/interaction-diff.js` | `__seDiff` | Before/after inline style diff + instant getAnimations |
| `scripts/motion-tools.js` | `__seMotion` | getAnimations snapshot + rAF sampling |
| `scripts/library-detect.js` | (returns directly) | Library detection + instance config extraction |
| `scripts/extract-keyframes.py` | CLI | Offline @keyframes extraction from downloaded CSS files |
---
## Quality checklist
### Static
- [ ] tokens include usage intent (not just lists)
- [ ] examples are copy-pasteable (HTML+CSS)
- [ ] 5+ copy-paste component examples
- [ ] reusable vs adapted vs discarded source traits are called out
- [ ] product-specific IA/copy is stripped from the final guide
### Motion (when dynamic)
- [ ] all @keyframes extracted from stylesheets (Level 1)
- [ ] CSS transition patterns documented with durations/easing (Level 2)
- [ ] 3+ key interactions with before/after diff evidence (Level 3)
- [ ] JS libraries detected and configs extracted when present (Level 4)
- [ ] at least one documented "delay chain" if present
- [ ] motion semantic tokens defined (duration scale + easing scale)
### Self-check (run after completing the guide)
1. Does every color token have a usage mapping? (not just a hex list)
2. Does every component have a state matrix with actual computed values?
3. Can someone reproduce the motion from the documentation alone?
4. Are all copy-paste examples self-contained (HTML + CSS in one block)?
5. If the original product disappeared, would this guide still be useful as a design reference?
6. Did the guide extract style without accidentally cloning the source product's information model?
### Failure conditions
Do not consider the extraction complete if any of the following are true:
- output is only one prose-heavy markdown file with no evidence manifest
- tokens are raw lists without semantic naming or usage mapping
- the document describes the source product more than the reusable style
- dynamic sites have no motion appendix despite meaningful motion
- examples cannot be implemented directly
- screenshots and downloaded assets are dumped loose in the project root instead of grouped into subfolders
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/output-contract.md
```markdown
# Output Contract
Use this contract for every extraction. Treat it as a required deliverable spec, not a suggestion.
## Required folder shape
Always produce one dedicated extraction directory:
```text
<project>-<style>/
guides/
style-guide.md
motion-guide.md
evidence-manifest.md
evidence/
screenshots/
assets/
notes/
```
Produce when motion is meaningful:
- `guides/motion-guide.md`
Never place final guide files, screenshots, CSS, or JS loose in the extraction root.
## File purposes
### `guides/style-guide.md`
Capture the reusable visual language:
- design philosophy
- semantic tokens
- typography
- spacing
- components + state matrix
- layout tendencies
- responsive behavior
- copy-paste examples
This file is for designers or engineers who want to rebuild the style in another product.
### `guides/motion-guide.md`
Capture the reusable motion system:
- interaction triggers
- durations / delays / easing
- transition patterns
- keyframes
- JS-library evidence
- reusable motion primitives
This file is required whenever motion is part of the source's identity or usability.
### `guides/evidence-manifest.md`
Capture how the guide was derived:
- source URL or repo
- capture date
- screenshots and what each one proves
- downloaded CSS/JS or notes
- scripts used
- interactions tested
- confidence notes and unresolved gaps
### `evidence/screenshots/`
Store screenshots only:
- baseline captures
- state captures
- sequence captures
### `evidence/assets/`
Store downloaded assets only:
- CSS
- JS
- SVG references
- font stylesheets
### `evidence/notes/`
Store raw support notes only:
- traces
- runtime notes
- selector inventories
- dumps / helper text
## Anti-patterns
Reject outputs that:
- spend more space describing the original product than the reusable style
- copy the source app's IA, feature taxonomy, or content structure
- list raw colors, timings, or selectors without semantic naming
- provide no evidence trail
- mix static style and motion into one unstructured document
- dump all files flat into one directory with no folder hierarchy
- read like a thin summary when the evidence clearly supports a denser reference
- replace observed values with vague adjectives such as “soft”, “clean”, or “premium” without evidence
- bloat the document with generic implementation advice that is not grounded in the source
- dump exhaustive weak-value lists with no semantic grouping or prioritization
## Reuse filter
For each major trait, decide which bucket it belongs to:
- `Reusable`: can be copied directly
- `Adapted`: useful idea, but must be reshaped
- `Discarded`: product-specific, should not be copied
Examples:
- Reusable: accent color system, card radius, button states, motion duration scale
- Adapted: sidebar layout, hero composition, section rhythm
- Discarded: pricing table logic, chat workflow, product copy, page IA
## Minimum evidence bar
Static:
- 1 baseline screenshot in `evidence/screenshots/`
- 3 or more component/state captures in `evidence/screenshots/`
- computed style evidence for core tokens in `evidence/notes/`
- enough raw evidence to support border/radius/layering notes when those traits appear important
Dynamic:
- 3 or more key interactions documented in `guides/evidence-manifest.md`
- transition evidence or animation evidence in `evidence/notes/`
- keyframes when present in `evidence/assets/` or `evidence/notes/`
- JS-library notes when relevant in `evidence/notes/`
## Density expectations
The guide should feel like a reusable reference, not a short memo.
Expected qualities:
- token tables contain representative raw values and usage mapping
- component sections include real state evidence, not only narrative summaries
- implementation notes appear when they materially help another engineer recreate the style
- border / radius / opacity / layering are called out explicitly when visually important
- copy-paste examples are substantial enough to be implementation starting points
Do not chase length for its own sake.
Do preserve the “reference manual” feeling when the source supports it.
```
### references/style-guide-template.md
```markdown
# Style Guide Template
Use this as the top-level structure for `<project>-<style>-style-guide.md`.
Quality intent:
- Keep the document dense enough to function as a real reference, not just a summary.
- Favor semantically grouped evidence over raw-value dumping.
- Include practical implementation guidance where it materially helps reuse.
- Avoid long generic filler, repeated product description, or exhaustive weak-value lists with no prioritization.
## 1. Overview
- Project / variant / source
- Type: marketing site / web app / docs / dashboard / desktop-web
- Visual summary in 3-6 bullets
- 1 short paragraph describing the dominant visual read
Optional:
- A short table of contents when the document is long enough to benefit from one
## 2. Design Philosophy
State the style principles using evidence, not adjectives alone.
For each principle:
- what it is
- which tokens/components prove it
- whether it is `Reusable`, `Adapted`, or `Discarded`
## 3. Semantic Tokens
Required groups:
- color tokens
- typography tokens
- spacing tokens
- radius / border / shadow tokens
- opacity / layering tokens
Use semantic names and map each token to usage.
For each important token cluster, include:
- semantic token name
- representative raw value(s)
- usage mapping
- confidence or evidence note when helpful
## 4. Color Palette + Usage Mapping
Include:
- brand/accent colors
- neutrals
- semantic colors
- usage notes
Do not stop at a swatch list. Explain where each color belongs.
Good pattern to copy from strong references:
- separate core colors from semantic usage
- include direct implementation examples for CTA / text / surface / border roles
## 5. Typography
Include:
- font families
- size scale
- weight scale
- line-height and letter-spacing tendencies
- mixed-language notes when relevant
When useful, include:
- representative CSS snippets
- fallback and loading notes
- guidance on where display typography stops and body typography begins
## 6. Spacing + Density
Include:
- spacing scale
- content density
- module rhythm
- container or gutter patterns
## 7. Component System
Cover 5 or more meaningful components.
For each component, include:
- role
- visual rule
- state matrix
- reusable notes
- actual raw values when captured (color, border, transition, radius, etc.)
Minimum state matrix:
- default
- hover
- active
- focus-visible
- disabled
Include selected / invalid / loading when they exist.
Good pattern to preserve from strong references:
- state tables with observed values
- note clearly when a state was not styled or not evidenced
Avoid:
- vague “hover gets brighter” prose without values or evidence
## 8. Border, Radius, Shadow, and Layering
Document separately when these are meaningfully part of the visual language.
Include:
- border widths
- border color strategy
- radius scale
- shadow / elevation strategy
- opacity / transparency usage
- z-index / layer ordering
If the source has minimal shadows or minimal layering, say so explicitly instead of inventing a heavy system.
## 9. Layout Tendencies
Document:
- page composition patterns
- primary/secondary region balance
- card vs panel behavior
- navigation style
Mark clearly which layout traits should only be adapted, not copied literally.
## 10. Responsive Behavior
Document breakpoints, collapse behavior, and simplifications.
When possible include:
- container widths
- layout shifts by breakpoint
- touch/hover conditional behavior
## 11. CSS Variables / Theme Sources
Include when available:
- root variable declarations
- scoped theme variables
- source-of-truth notes for tokens coming from CSS vars, inline styles, or JS
Do not dump the entire stylesheet blindly. Curate the variables that matter to reuse.
## 12. Copy-Paste Examples
Provide 5 or more self-contained examples with HTML + CSS in one block.
Examples should express the extracted style, not recreate the original product.
Good examples usually include:
- primary CTA
- secondary action
- nav or marker element
- card / panel
- one motion-driven or layered component when relevant
## 13. Implementation Notes
Include practical notes when they materially improve reuse:
- font loading
- reset assumptions
- accessibility gaps in the source
- performance caveats
- when JS is required instead of pure CSS
Keep this grounded in evidence. Do not add generic frontend advice.
## 14. Adaptation Notes
Include:
- what to keep when porting the style to another product
- what to soften or simplify
- what to avoid copying
Good adaptation notes are especially important when the source is a marketing site or highly domain-specific app.
```
### references/motion-guide-template.md
```markdown
# Motion Guide Template
Use this as the top-level structure for `<project>-<style>-motion-guide.md`.
## 1. Motion Summary
Summarize:
- overall motion character
- where motion matters most
- whether the system is transition-led, keyframe-led, or JS-led
## 2. Motion Tokens
Required groups:
- duration scale
- delay scale
- easing scale
- motion property families
Name tokens semantically, not only by raw value.
## 3. Trigger Matrix
List 3 or more key interactions.
For each interaction, capture:
- trigger
- target elements
- properties that change
- duration
- delay
- easing
- evidence source
## 4. Transition Patterns
Document recurring patterns such as:
- opacity + translate entrance
- icon color switch
- panel expand/collapse
- overlay sweep
- active-state highlight shift
## 5. Keyframes
For important animations, include:
- keyframe name
- purpose
- full `@keyframes` block when available
- observed bindings (duration, easing, iteration, fill)
## 6. JS-Driven Motion
Document:
- detected libraries
- config evidence
- whether motion is visible via `document.getAnimations()`
- when rAF sampling or trace was needed
## 7. Delay Chains / Sequencing
Call out any staged entrances or chained reveals.
For each chain, list:
- order
- delay ladder
- what visual effect the sequence creates
## 8. Reusable Motion Primitives
Translate the source motion into reusable primitives such as:
- `content.fadeSlideIn`
- `nav.activeSwitch`
- `card.hoverLift`
- `cta.breathingAccent`
Mark each as `Reusable`, `Adapted`, or `Discarded`.
## 9. Evidence Notes
Include:
- which scripts were used
- what interactions were tested
- what evidence is partial or inferred
- any blind spots
```
### references/evidence-manifest-template.md
```markdown
# Evidence Manifest Template
Use this as the top-level structure for `<project>-<style>-evidence-manifest.md`.
## 1. Source Summary
- Project / variant
- URL or repo
- capture date
- device or viewport
- theme / locale / login state if relevant
## 2. Screenshots
List each screenshot with:
- filename
- folder path (normally `evidence/screenshots/...`)
- what it captures
- why it matters
## 3. Downloaded Assets / Notes
List:
- CSS files under `evidence/assets/`
- JS files under `evidence/assets/`
- inline snippets or dumps under `evidence/notes/`
- traces under `evidence/notes/`
- any extra notes under `evidence/notes/`
## 4. Scripts Used
Record which skill scripts were used and for what:
- `transition-scanner.js`
- `interaction-diff.js`
- `library-detect.js`
- `motion-tools.js`
- `extract-keyframes.py`
## 5. Interactions Tested
For each tested interaction, record:
- trigger
- target area
- evidence captured
- whether results were complete, partial, or inferred
## 6. Coverage Assessment
Split findings into:
- static coverage
- motion coverage
- state coverage
## 7. Gaps and Confidence
Include:
- blockers
- cross-origin limitations
- protected states you could not enter
- anything inferred rather than directly observed
- confidence level for the overall extraction
```
### references/9nine-visual-novel/guides/style-guide.md
```markdown
# 9-nine- Visual Novel Portal — Style Guide
> **Source:** https://9-nine-project.com/
> **Date captured:** 2026-03-13
> **Reuse target:** Visual novel / anime IP promotional website, design reference
---
## 1. Design Personality
**Elegant, serene, and premium Japanese visual-novel branding.** The site blends classical serif typography (Cormorant Infant) with clean Japanese sans-serif (Noto Sans JP), anchored by a single dominant deep-blue hue. The overall impression is calm sophistication — crystal-clear white space, diamond/geometric decorative motifs, and restrained animation that feels like pages turning in a storybook.
**Key adjectives (evidence-grounded):**
- **Serene** — 0.4s universal transition speed; no jarring motion anywhere
- **Premium** — 120–264px section gaps; bimodal spacing (tiny or huge, nothing in between)
- **Crystalline** — diamond-shaped carousel dots (`transform: rotate(45deg)`), diamond-tile footer overlay, canvas particle system
- **Editorial** — Cormorant Infant serif at 40px display size with 3px letter-spacing; bilingual heading pattern
- **Borderless** — zero `border-radius` site-wide; zero `box-shadow` on interactive elements; depth via spacing and color only
---
## 2. Color Tokens
### 2.1 Primary Palette
| Token | Hex | RGB | Observed usage |
|-------|-----|-----|----------------|
| `--color-primary` | `#004789` | `rgb(0, 71, 137)` | Nav links, heading `.en`, button bg, footer bg, tab dots, border accents, decorative lines |
| `--color-primary-hover` | `#0c569a` | `rgb(12, 86, 154)` | `.sw-Button:hover` background (observed via computed style diff) |
| `--color-primary-light` | `#005299` | `rgb(0, 82, 153)` | Character name plates, character CV text |
| `--color-accent-sky` | `#0084ff` | `rgb(0, 132, 255)` | Section background wash at 5% opacity; bright accent highlight |
| `--color-accent-sky-light` | `#72cafc` | `rgb(114, 202, 252)` | Lighter sky accent (background element) |
### 2.2 Neutral Palette
| Token | Hex | RGB | Observed usage |
|-------|-----|-----|----------------|
| `--color-text` | `#333` | `rgb(51, 51, 51)` | `body` color, default text |
| `--color-text-secondary` | `#343434` | `rgb(52, 52, 52)` | News title text, slightly variant body |
| `--color-text-inverse` | `#fff` | `rgb(255, 255, 255)` | Text on `--color-primary` bg (footer nav, buttons, mobile nav) |
| `--color-surface` | `#fff` | `rgb(255, 255, 255)` | Page background, card surfaces |
| `--color-surface-blue-tint` | `#eff4fd` | `rgb(239, 244, 253)` | Subtle blue-tinted border/divider |
| `--color-black` | `#000` | `rgb(0, 0, 0)` | Rare — hamburger icon, pure-black decorations |
### 2.3 Overlay & Shadow
| Token | Value | Observed usage |
|-------|-------|----------------|
| `--overlay-tag` | `rgba(225, 228, 232, 0.8)` | Frosted tag/label backgrounds |
| `--overlay-blue-wash` | `rgba(0, 132, 255, 0.05)` | Subtle blue section background wash |
| `--overlay-white-80` | `rgba(255, 255, 255, 0.8)` | White overlay panel |
| `--overlay-white-20` | `rgba(255, 255, 255, 0.2)` | Subtle white mist |
| `--shadow-card` | `rgba(102, 113, 152, 0.1) 0 0 10px 0` | Only box-shadow on entire site — soft card/panel glow |
| `--shadow-character` | `drop-shadow(rgba(0, 71, 136, 0.1) 20px 0 0)` | Character art animated drop-shadow (slides right via keyframe) |
| `--glow-white` | `drop-shadow(rgba(255, 255, 255, 0.4) 0 0 22px)` | White glow filter on decorative elements |
### 2.4 Color Philosophy
The palette is intentionally monochromatic. `#004789` is the only chromatic hue used for UI elements. All visual hierarchy comes from:
- **Opacity variation** (tab dots: 0.3 inactive → 1.0 active)
- **Lightness steps** (`#004789` → `#005299` → `#0084ff` → `#72cafc`)
- **White space** rather than color fills to separate sections
No warm colors, no grays beyond `#333`/`#343434`. The site has zero CSS custom properties in `:root` — all values are hardcoded in the stylesheet.
---
## 3. Typography
### 3.1 Font Stack
| Role | Family | Weight(s) | Source |
|------|--------|-----------|--------|
| **Display / English headings** | `Cormorant Infant` | 600, 700 | Google Fonts |
| **English UI labels** | `Oswald` | 400 | Google Fonts |
| **Japanese body** | `Noto Sans JP` + `YakuHanJP` | 400, 500, 700 | Google Fonts + CDN (v3.3.1) |
| **Japanese headings / names** | `Noto Serif JP` | 500, 700 | Google Fonts |
**Full body stack (observed on `<body>`):**
```css
font-family: YakuHanJP, "Noto Sans JP", "Hiragino Kaku Gothic ProN",
"Hiragino Kaku Gothic Pro", "MS ゴシック", sans-serif;
```
### 3.2 Type Scale
| Token | Size | Line-height | Weight | Observed usage |
|-------|------|-------------|--------|----------------|
| `--text-display` | `40px` | `40px` (1:1) | 600 | Section heading English label (`.sw-Subtitle .en`) — "NEWS", "CHARACTER", etc. |
| `--text-xl` | `28px` | — | — | Large feature text |
| `--text-lg` | `23px` | — | — | Sub-feature text |
| `--text-footer-nav` | `18px` | `18px` (1:1) | 400 | Footer navigation links (Cormorant Infant) |
| `--text-nav` | `16px` | `16px` (1:1) | 400 | Header nav links (Cormorant Infant), news date (Oswald), news title (Noto Sans JP at `26px` line-height) |
| `--text-button` | `14px` | `64px` (line-height = button height) | 400 | "VIEW MORE" button label (Oswald) |
| `--text-body` | `12px` | `12px` (1:1) | 400 | Body default, heading `.ja` subtitle, character names |
| `--text-caption` | `10px` | — | 400 | Copyright, small labels |
### 3.3 Letter Spacing
| Token | Value | Observed on |
|-------|-------|-------------|
| `--ls-display` | `3px` (0.075em at 40px) | `.sw-Subtitle .en` — Cormorant Infant headings |
| `--ls-nav` | `1.6px` (0.1em at 16px) | Header nav links, news date |
| `--ls-footer-nav` | `1.8px` (0.1em at 18px) | Footer navigation links |
| `--ls-button` | `1.4px` (0.1em at 14px) | `.sw-Button` label |
| `--ls-subtitle-ja` | `0.6px` (0.05em at 12px) | `.sw-Subtitle .ja` Japanese subtitle |
| `--ls-copyright` | `1px` | Footer copyright text |
### 3.4 Bilingual Section Heading Pattern
Every major section uses a consistent bilingual heading — the site's most distinctive typographic pattern:
```css
/* Container */
.sw-Subtitle {
display: flex;
align-items: center; /* or block with margin-top on .ja for stacked variant */
}
/* English label — large serif */
.sw-Subtitle .en {
font-family: "Cormorant Infant";
font-weight: 600;
font-size: 40px;
line-height: 40px;
color: #004789;
letter-spacing: 3px;
}
/* Japanese label — small sans-serif */
.sw-Subtitle .ja {
font-family: "Noto Sans JP";
font-weight: 500;
font-size: 12px;
line-height: 12px;
color: #004789;
letter-spacing: 0.6px;
margin-left: 20px; /* horizontal variant */
}
```
**Reusable pattern:** Large serif English label + small sans-serif secondary label, same accent color, horizontally aligned (or stacked with `margin-top: 12px` for wider sections).
### 3.5 Typography Rules
1. **1:1 line-height ratio** is used everywhere except news titles (`16px / 26px`). This creates extremely tight, compact text blocks — a deliberate Japanese web design convention.
2. **Letter-spacing is always 0.1em** on Cormorant Infant and Oswald text. This is the site's typographic signature.
3. **Font weight is minimal** — only 400 (body) and 500–600 (headings). No bold body text.
4. **Noto Serif JP** is reserved exclusively for character name plates — it never appears in UI or navigation.
---
## 4. Layout
### 4.1 Content Width
| Token | Value | Observed usage |
|-------|-------|----------------|
| `--width-page` | `1280px` | `body` computed width, full-bleed sections |
| `--width-content` | `1200px` | Inner content containers (`margin: 0 40px` on 1280px body) |
| `--width-footer-inner` | `960px` | Footer inner content (`margin: 0 160px`) |
| `--width-hero` | `920px` | Hero visual area (1280px - 2×180px padding) |
### 4.2 Section Spacing (Observed)
| Section | Top padding | Bottom padding | Pattern |
|---------|------------|----------------|---------|
| Mainvisual (hero) | `120px` | — | Hero area with blur-reveal |
| Pickup (carousel) | `150px` | — | First content section |
| News | `150px` | `142px` | Standard content section |
| Channel (video) | `264px` | — | Extra-tall gap (visual break) |
| Game | `190px` | `200px` | Largest symmetric gap |
| Character | `133px` | — | Gallery section |
| Goods | `225px` | `140px` | Tallest top padding |
| Special | `135px` | — | Standard |
| Official (SNS) | `222px` | `160px` | Pre-footer section |
**Spacing philosophy:** The site uses a **bimodal distribution** — either tiny (0–20px for internal component spacing) or massive (120–264px for section gaps). There is almost nothing in the 30–100px range. This creates a gallery-like, high-end experience.
### 4.3 Spacing Tokens
| Token | Value | Usage |
|-------|-------|-------|
| `--space-section-xl` | `222–264px` | Extra-large section gaps (Channel, Official, Goods) |
| `--space-section-lg` | `150–200px` | Standard section gaps (News, Game, Pickup) |
| `--space-section-md` | `133–142px` | Moderate section gaps (Character, News bottom) |
| `--space-gutter` | `40px` | Page horizontal margins (1280px → 1200px content) |
| `--space-footer-gutter` | `160px` | Footer horizontal margins (1280px → 960px content) |
| `--space-nav-top` | `52px` | Nav link top padding (creates vertical rhythm) |
| `--space-heading-gap` | `20px` | Gap between `.en` and `.ja` in bilingual headings |
| `--space-button-margin-top` | `67px` | Gap above "VIEW MORE" buttons |
### 4.4 Grid Philosophy
The layout uses **no CSS Grid** — it relies entirely on flexbox and absolute positioning. Sections stack vertically with generous vertical spacing. Horizontal layouts use `display: flex` with fixed widths. The character gallery uses overlapping absolute positioning for a staggered depth effect.
---
## 5. Components
### 5.1 Button — "VIEW MORE"
The primary CTA button. Distinctive for its extending decorative line with arrow tip.
```css
/* Default state */
.sw-Button {
display: block;
width: 260px;
height: 64px;
line-height: 64px;
margin: 67px auto 0;
color: #fff;
background: #004789;
font-family: Oswald;
font-size: 14px;
font-weight: 400;
letter-spacing: 1.4px;
text-align: center;
text-decoration: none;
border: none;
border-radius: 0;
position: relative;
transition: 0.4s;
cursor: pointer;
}
/* Decorative line extending right */
.sw-Button::before {
content: "";
position: absolute;
top: 32px; /* vertically centered */
right: -50px;
width: 80px;
height: 2px;
border-top: 0.67px solid #004789;
border-bottom: 0.67px solid #fff;
transition: 0.4s;
}
/* Arrow tip */
.sw-Button::after {
content: "";
position: absolute;
top: 50%;
right: -50px;
width: 10px;
height: 1px;
background: #004789;
transform: rotate(20deg);
transform-origin: right;
transition: 0.4s;
}
```
**State matrix (observed):**
| State | Background | Color | `::before` right | `::after` transform | Transition |
|-------|-----------|-------|------------------|---------------------|------------|
| Default | `#004789` | `#fff` | `-50px` | `rotate(20deg)` | — |
| Hover | `#0c569a` | `#fff` | `-60px` (slides out 10px) | `rotate(180deg)` (arrow flips) | `0.4s` |
| Focus-visible | Not styled | — | — | — | — |
### 5.2 Navigation Link (Header)
```css
/* Default */
.st-Header_Static nav ul li a {
display: block;
padding: 52px 25px 0;
font-family: "Cormorant Infant";
font-size: 16px;
font-weight: 400;
line-height: 16px;
letter-spacing: 1.6px;
color: #004789;
text-decoration: none;
position: relative;
cursor: pointer;
height: 120px; /* total nav bar height */
}
/* Underline indicator (::before) — vertical line that grows on hover */
.st-Header_Static nav ul li a::before {
content: "";
position: absolute;
bottom: 78px;
left: 50%;
width: 1px;
height: 0; /* hidden by default */
background: #004789;
transition: 0.4s;
}
```
**State matrix (observed):**
| State | Color | `::before` height | `::before` opacity | Animation |
|-------|-------|-------------------|-------------------|-----------|
| Default | `#004789` | `0` | — | — |
| Hover | `#004789` | `42px` | `0.6` | `transition: 0.4s` |
| Current (active page) | `#004789` | `42px` | `1` | `header-line 0.5s ease 0.8s` (animated in on page load) |
### 5.3 Navigation Link (Footer)
```css
.st-Footer nav a {
display: block;
padding: 52px 25px 0;
font-family: "Cormorant Infant";
font-size: 18px; /* 2px larger than header nav */
font-weight: 400;
line-height: 18px;
letter-spacing: 1.8px;
color: #fff; /* inverse on primary bg */
height: 120px;
position: relative;
cursor: pointer;
}
/* Same ::before underline pattern as header, with transition: 0.4s */
```
### 5.4 Tab Dot Indicator (Carousel)
```css
/* Diamond-shaped dots — rotated 45° squares */
[role="tab"] {
display: block;
width: 8px;
height: 8px;
background: #004789;
border: none;
border-radius: 0; /* NOT circles — sharp squares */
transform: rotate(45deg); /* creates diamond shape */
opacity: 0.3;
cursor: pointer;
transition: opacity 0.4s;
padding: 0;
margin: 0;
}
[role="tab"][aria-selected="true"] {
opacity: 1;
}
```
**Reusable insight:** The site uses **diamond-shaped** pagination dots (rotated squares), not circles — consistent with the crystalline/geometric motif throughout.
### 5.5 News List Item
| Element | Font | Size | Weight | Color | Letter-spacing |
|---------|------|------|--------|-------|---------------|
| Date | Oswald | 16px | 400 | `#004789` | 1.6px |
| Category tag | — | — | — | — | (not rendered as separate element in computed styles) |
| Title | Noto Sans JP | 16px | 400 | `#343434` | normal |
```css
/* Thumbnail container */
.idx-News_Inner .thumb {
width: 380px;
height: 214px;
overflow: hidden;
border-radius: 0;
}
.idx-News_Inner .thumb img {
object-fit: cover;
transition: transform 0.4s;
}
/* Hover: subtle zoom */
.idx-News_Inner li a:hover .thumb img {
transform: scale(1.05);
}
/* Hover: title color change */
.idx-News_Inner li a:hover .title {
color: #004789; /* text → primary on hover */
transition: color 0.4s;
}
/* Hover: overlay appears on thumbnail */
.idx-News_Inner .thumb::before {
content: "";
/* overlay that fades in on hover */
transition: 0.4s;
}
```
### 5.6 Character Name Plate
```css
.character-name {
font-family: "Noto Serif JP";
font-size: 12px;
font-weight: 500;
color: #005299; /* --color-primary-light */
}
.character-cv {
font-family: "Noto Serif JP";
font-size: 12px;
color: #005299;
}
```
### 5.7 Footer
```css
.st-Footer {
background: #004789;
position: relative;
z-index: 1;
}
/* Decorative wavy top border — repeating background image */
.st-Footer::before {
content: "";
position: absolute;
top: -1px;
left: 50%;
transform: translateX(-50%);
width: 100%;
height: 23px;
background: url(deco_footer_line.png) repeat-x top center / 300px 23px;
}
/* Diamond pattern overlay */
.st-Footer::after {
content: "";
position: absolute;
top: 0; left: 0;
width: 100%;
height: 423px;
background: url(bg_footer_dia.png) repeat-x top center / 107px 389px;
}
/* Footer inner */
.st-Footer_Inner {
width: 960px;
padding: 66px 0 40px;
margin: 0 160px;
}
/* Footer logo */
.st-Footer_Inner .logo img {
width: 167px;
height: 74.5px;
}
/* Copyright */
.st-Footer .copyright {
font-family: "Noto Sans JP";
font-size: 10px;
color: #fff;
letter-spacing: 1px;
}
```
### 5.8 Page Top Button
```css
.st-Pagetop a {
width: 101px;
height: 182px;
font-family: "Cormorant Infant";
font-size: 16px;
color: #fff;
background: transparent;
border: none;
border-radius: 0;
}
/* ::before and ::after create decorative arrow, transition: 0.4s */
```
---
## 6. Decorative Motifs
### 6.1 Diamond / Crystal Pattern
The signature visual element is a **diamond (rhombus) geometric pattern**, used as:
- **Footer background overlay** — repeating diamond tile (`bg_footer_dia.png`, 107×389px tile)
- **Carousel pagination** — 8×8px squares rotated 45° (`transform: rotate(45deg)`)
- **Section separator accents** — positioned absolutely as decorative corners
- **Wavy footer border** — repeating wave pattern (`deco_footer_line.png`, 300×23px tile)
### 6.2 Particle System
The hero section uses a `<canvas id="particles">` particle effect, creating floating specs that suggest a crystalline/magical atmosphere. CSS transition `0.2s` is applied for opacity fading.
### 6.3 SVG Stroke Drawing
Section heading decorations use SVG stroke-draw animation — lines that "write themselves" on scroll via `svg-stroke` keyframe (2s ease-out, `stroke-dashoffset: 1000px → 0`).
### 6.4 Section Backgrounds
Each major section uses full-bleed decorative background images (subtle geometric/diamond watermarks) positioned behind content using pseudo-elements.
---
## 7. Border & Radius
### 7.1 Border Radius
**All border radius values are exactly `0px` across the entire site.** Buttons, images, cards, containers, navigation — everything is sharp-cornered. This is a deliberate design choice creating a unified angular aesthetic consistent with the diamond/geometric motif.
### 7.2 Border Usage
The design philosophy is **borderless**. No card borders, no container borders, no button borders.
**When borders appear (rare):**
- Active nav indicator: `::before` pseudo-element with `background: #004789` (not a CSS border)
- Button decorative line: `border-top: 0.67px solid #004789` + `border-bottom: 0.67px solid #fff` (creates a double-line effect)
- Subtle section divider: `border-color: #eff4fd` (very light blue, barely visible)
---
## 8. Responsive Strategy
### 8.1 Breakpoints (Extracted from CSS)
| Breakpoint | Usage |
|-----------|-------|
| `max-width: 767px` | Mobile / SP — primary mobile breakpoint |
| `min-width: 768px` | Desktop / PC — primary desktop breakpoint |
| `min-width: 768px and max-width: 1280px` | Small desktop |
| `min-width: 768px and max-width: 1560px` | Medium desktop |
| `min-width: 1561px` | Large desktop |
| `min-width: 1921px` | Extra-large desktop (caps fluid sizing) |
### 8.2 Responsive Patterns
- **Mobile uses `vw` units extensively** for fluid sizing (e.g., `6.5vw` for heading size)
- **Desktop uses fixed `px` values**
- **Transitions are desktop-only** — all `transition: 0.4s` rules are inside `min-width: 768px` media queries
- **Character gallery stagger delays differ** between mobile and desktop (mobile delays are slightly longer)
---
## 9. Copy-Paste Examples
### 9.1 Bilingual Section Heading
```html
<h2 class="section-heading">
<span class="en">SECTION TITLE</span>
<span class="ja">セクションタイトル</span>
</h2>
<style>
.section-heading {
display: flex;
align-items: center;
}
.section-heading .en {
font-family: "Cormorant Infant", serif;
font-weight: 600;
font-size: 40px;
line-height: 40px;
color: #004789;
letter-spacing: 3px;
}
.section-heading .ja {
font-family: "Noto Sans JP", sans-serif;
font-weight: 500;
font-size: 12px;
line-height: 12px;
color: #004789;
letter-spacing: 0.6px;
margin-left: 20px;
}
</style>
```
### 9.2 CTA Button with Extending Line
```html
<a href="#" class="cta-button">VIEW MORE</a>
<style>
.cta-button {
display: block;
width: 260px;
height: 64px;
line-height: 64px;
margin: 0 auto;
color: #fff;
background: #004789;
font-family: Oswald, sans-serif;
font-size: 14px;
letter-spacing: 1.4px;
text-align: center;
text-decoration: none;
border: none;
border-radius: 0;
position: relative;
transition: 0.4s;
}
.cta-button::before {
content: "";
position: absolute;
top: 50%;
right: -50px;
width: 80px;
height: 2px;
border-top: 0.67px solid #004789;
border-bottom: 0.67px solid #fff;
transition: 0.4s;
}
.cta-button::after {
content: "";
position: absolute;
top: 50%;
right: -50px;
width: 10px;
height: 1px;
background: #004789;
transform: rotate(20deg);
transform-origin: right;
transition: 0.4s;
}
.cta-button:hover {
background: #0c569a;
}
.cta-button:hover::before,
.cta-button:hover::after {
right: -60px;
}
.cta-button:hover::after {
transform: rotate(180deg);
}
</style>
```
### 9.3 Diamond Pagination Dots
```html
<div class="dot-nav" role="tablist">
<button role="tab" aria-selected="true" class="dot active">1</button>
<button role="tab" class="dot">2</button>
<button role="tab" class="dot">3</button>
</div>
<style>
.dot-nav {
display: flex;
gap: 12px;
justify-content: center;
align-items: center;
}
.dot {
display: block;
width: 8px;
height: 8px;
padding: 0;
background: #004789;
border: none;
border-radius: 0;
transform: rotate(45deg);
opacity: 0.3;
cursor: pointer;
transition: opacity 0.4s;
color: transparent;
font-size: 0;
}
.dot.active,
.dot[aria-selected="true"] {
opacity: 1;
}
</style>
```
### 9.4 Navigation Bar with Underline Reveal
```html
<nav class="nav-bar">
<a href="#" class="nav-link active">HOME</a>
<a href="#" class="nav-link">NEWS</a>
<a href="#" class="nav-link">ABOUT</a>
</nav>
<style>
.nav-bar {
display: flex;
height: 120px;
}
.nav-link {
display: block;
padding: 52px 25px 0;
font-family: "Cormorant Infant", serif;
font-size: 16px;
font-weight: 400;
line-height: 16px;
letter-spacing: 1.6px;
color: #004789;
text-decoration: none;
position: relative;
}
.nav-link::before {
content: "";
position: absolute;
bottom: 78px;
left: 50%;
width: 1px;
height: 0;
background: #004789;
transition: 0.4s;
}
.nav-link:hover::before {
height: 42px;
opacity: 0.6;
}
.nav-link.active::before {
height: 42px;
opacity: 1;
}
</style>
```
### 9.5 News Card with Hover Zoom
```html
<a href="#" class="news-card">
<div class="news-thumb">
<img src="thumbnail.jpg" alt="">
</div>
<div class="news-meta">
<span class="news-date">2026.03.13</span>
</div>
<p class="news-title">Article title goes here</p>
</a>
<style>
.news-card {
display: block;
text-decoration: none;
}
.news-thumb {
width: 380px;
height: 214px;
overflow: hidden;
border-radius: 0;
}
.news-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.4s;
}
.news-card:hover .news-thumb img {
transform: scale(1.05);
}
.news-date {
font-family: Oswald, sans-serif;
font-size: 16px;
color: #004789;
letter-spacing: 1.6px;
}
.news-title {
font-family: "Noto Sans JP", sans-serif;
font-size: 16px;
font-weight: 400;
color: #343434;
line-height: 26px;
transition: color 0.4s;
}
.news-card:hover .news-title {
color: #004789;
}
</style>
```
---
## 10. Classification Summary
| Source Trait | Classification | Notes |
|-------------|---------------|-------|
| Deep-blue monochrome palette (`#004789` family) | **Reusable** | Single-hue system easily remapped to any brand color |
| Cormorant Infant + Noto Sans JP bilingual pairing | **Reusable** | Elegant serif/sans contrast for any dual-language site |
| Bilingual section heading pattern (`.en` + `.ja`) | **Reusable** | Works for any dual-language or label+subtitle pattern |
| 0.4s universal hover transition | **Reusable** | Clean, consistent, unhurried feel |
| "VIEW MORE" button with extending line + arrow | **Reusable** | Distinctive and fully copy-pasteable |
| Diamond-shaped pagination dots | **Reusable** | Simple, on-brand alternative to circles |
| Zero border-radius site-wide | **Reusable** | Strong geometric identity choice |
| Bimodal spacing (tiny or huge) | **Reusable** | Creates premium gallery feel |
| 1:1 line-height ratio | **Adapted** | Works for Japanese text; may need adjustment for Latin-heavy content |
| Diamond geometric motif (footer tiles, decorations) | **Adapted** | Specific to crystal/magical theme but geometric concept is universal |
| Canvas particle system | **Adapted** | Mood-specific; concept reusable for atmospheric effects |
| Character staggered gallery with depth delays | **Adapted** | Visual novel specific layout; stagger concept is reusable |
| Footer with decorative wave border | **Adapted** | Technique reusable, imagery is IP-specific |
| SVG stroke-draw heading decorations | **Reusable** | Works for any decorative line art |
| Scroll-triggered class-toggle animation system | **Reusable** | Lightweight AOS alternative |
| Product branding, logo, copy | **Discarded** | Product-specific |
| News/game/character content structure | **Discarded** | Content-specific IA |
---
## 11. Implementation Notes
### 11.1 Font Loading
```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Infant:wght@600;700&family=Noto+Sans+JP:wght@400;500;700&family=Noto+Serif+JP:wght@500;700&family=Oswald&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/yakuhanjp.min.css" rel="stylesheet">
```
### 11.2 Base Reset
```css
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: YakuHanJP, "Noto Sans JP", "Hiragino Kaku Gothic ProN",
"Hiragino Kaku Gothic Pro", sans-serif;
font-size: 12px;
line-height: 12px;
color: #333;
background: #fff;
-webkit-font-smoothing: antialiased;
}
a { color: inherit; text-decoration: none; }
button { font: inherit; border: none; background: none; cursor: pointer; padding: 0; }
img { display: block; max-width: 100%; border-radius: 0; }
```
### 11.3 Accessibility Gaps
- **12px base font** is below WCAG 2.1 recommended minimum (16px)
- **1:1 line-height** is very tight for readability
- **No `:focus-visible` styles** observed — keyboard navigation has no visual indicator
- **Color contrast is strong:** `#004789` on `#fff` = ~8.5:1 ratio (AAA); `#333` on `#fff` = ~12.6:1 (AAA)
- **Tab dots use `color: transparent`** — needs `aria-label` for screen readers
### 11.4 Adaptation Notes
**Keep when porting:**
- The bilingual heading pattern (works for any label+subtitle)
- The 0.4s transition speed (creates calm, deliberate feel)
- The monochromatic palette approach (swap `#004789` for your brand color)
- The button with extending line (unique, memorable)
- Diamond dots instead of circles (distinctive)
**Adjust when porting:**
- Increase base font to 14–16px for Latin-heavy content
- Add `:focus-visible` outlines for accessibility
- Consider `line-height: 1.5` for body text readability
- The 120–264px section gaps may be excessive for content-dense applications
```
### references/9nine-visual-novel/guides/motion-guide.md
```markdown
# 9-nine- Visual Novel Portal — Motion Guide
> **Source:** https://9-nine-project.com/
> **Date captured:** 2026-03-13
> **Motion complexity:** Moderate — CSS keyframes + scroll-triggered class toggling + canvas particles
---
## 1. Motion Personality
**Gentle, choreographed reveals.** Motion is used to create a sense of pages gracefully unfolding — elements fade in, slide upward, or blur-to-sharp as the user scrolls. The overall tempo is unhurried (0.4s–2s), with staggered delays that create a cascading "one after another" entrance feel. There is no jarring or rapid motion anywhere.
**Key motion adjectives (evidence-grounded):**
- **Graceful** — `fade-up` keyframe uses only 20px translateY, not dramatic slides
- **Staggered** — nav items enter with 0.1s step delays; characters with 0.05s steps
- **Crystalline** — blur-to-sharp hero reveal (`filter: blur(10px) → blur(0)`)
- **Scroll-triggered** — all content animations fire via Intersection Observer class toggling
- **Desktop-only hover** — all `transition: 0.4s` rules are inside `min-width: 768px` media queries
---
## 2. Duration Scale
| Token | Value | Observed usage |
|-------|-------|----------------|
| `--duration-instant` | `0.2s` | Canvas particle opacity |
| `--duration-fast` | `0.3s` | Loading inner reveal |
| `--duration-base` | `0.4s` | **Dominant duration** — buttons, links, nav, body opacity, fade-up (menu), all hover transitions |
| `--duration-loading-transition` | `0.4s + 0.2s delay` | Loading screen opacity fade (with delay) |
| `--duration-nav-open` | `0.5s` | Fixed nav container opacity |
| `--duration-nav-stagger` | `0.8s` | Header nav item entrance animation |
| `--duration-content` | `1s` | Scroll-triggered fade-up, opacity reveals, character image entrance |
| `--duration-hero` | `1s + 0.2s delay` | Hero blur-to-sharp reveal |
| `--duration-logo` | `1s` | Header logo container transform |
| `--duration-svg` | `2s` | SVG stroke drawing animation |
| `--duration-loading-mask` | `6s` | Loading screen mask wipe (long, cinematic) |
**Dominant duration: `0.4s`** — used for nearly all hover transitions, UI state changes, and menu animations. This is significantly slower than modern web standards (typically 0.15–0.25s), creating a deliberate, unhurried feel.
---
## 3. Easing Scale
| Token | Value | Observed usage |
|-------|-------|----------------|
| `--ease-default` | `ease-out` | Scroll-triggered fade/fade-up, hero blur reveal, SVG stroke draw |
| `--ease-nav` | `ease` | Header nav stagger entrance, character image entrance, header-line |
| `--ease-loading` | `ease-in-out` | Loading screen transitions, menu open fade-up |
| `--ease-loading-mask` | `linear` | Loading mask wipe (constant speed) |
| `--ease-share` | `linear` | Share button wiggle |
---
## 4. @keyframes Inventory
### 4.1 `load-opacity` — Simple Fade In
```css
@keyframes load-opacity {
0% { opacity: 0; }
100% { opacity: 1; }
}
```
**Bindings:**
- `.anim-sc.animFade.animated` → `1s ease-out forwards` (scroll-triggered content)
- `.anim-ld.animFade.animated` → `1s ease-out forwards` (load-triggered content)
- `.st-Loading::before`, `::after`, `_Inner` → `0.4s ease-in-out forwards` (loading screen)
### 4.2 `fade-up` — Fade In + Slide Up
```css
@keyframes fade-up {
0% { opacity: 0; transform: translateY(20px); }
100% { opacity: 1; transform: translateY(0); }
}
```
**Bindings:**
- `.anim-sc.animFadeup.animated` → `1s ease-out forwards` (scroll-triggered content)
- `.st-Header_Fixed_Inner nav.open .logo, .menu, .official` → `0.4s ease-in-out forwards` (menu open)
### 4.3 `load-blur` — Blur to Sharp
```css
@keyframes load-blur {
0% { filter: blur(10px); }
100% { filter: blur(0); }
}
```
**Binding:** `.idx-Mainvisual_Inner .wrap.animated` → `1s ease-out 0.2s forwards` (hero image starts heavily blurred and sharpens after 0.2s delay)
### 4.4 `svg-stroke` — SVG Line Draw
```css
@keyframes svg-stroke {
0% { stroke-dashoffset: 1000px; }
100% { stroke-dashoffset: 0; }
}
```
**Binding:** `svg.anim-sc.animated` → `2s ease-out forwards` (decorative SVG elements "draw themselves" on scroll)
### 4.5 `header-nav` — Nav Item Entrance (Fade Up, Smaller)
```css
@keyframes header-nav {
0% { opacity: 0; transform: translateY(10px); }
100% { opacity: 1; transform: translateY(0); }
}
```
**Bindings:**
- `.st-Header_Static.animated li` → `0.8s ease forwards` (with staggered delays per item)
- `.idx-Character_Inner .wrap.animated .chara-image li img` → `1s ease forwards` (character images reuse this keyframe)
### 4.6 `header-line` — Nav Underline Grow
```css
@keyframes header-line {
0% { top: 0; height: 0; }
100% { top: 0; height: 42px; }
}
```
**Binding:** `.st-Header_Static.animated li a.current::before` → `0.5s ease 0.8s forwards` (current page underline grows after nav items settle)
### 4.7 `chara-shadow` — Character Drop Shadow Slide
```css
@keyframes chara-shadow {
0% { filter: drop-shadow(rgba(0, 71, 136, 0.1) 0 0 0); }
100% { filter: drop-shadow(rgba(0, 71, 136, 0.1) 20px 0 0); }
}
```
**Binding:** Character portrait images — a blue-tinted shadow slides 20px to the right, creating depth.
### 4.8 `load-fade` / `load-fade-sp` — Loading Mask Wipe
```css
@keyframes load-fade {
0% { -webkit-mask-position-y: 0; }
100% { -webkit-mask-position-y: -500px; }
}
```
**Binding:** `.st-Loading_Inner::after` → `6s linear forwards` (PC) / `load-fade-sp` variant for mobile
### 4.9 `post-share` — Share Button Wiggle
```css
@keyframes post-share {
0% { transform: rotate(0deg); }
50% { transform: rotate(15deg); }
100% { transform: rotate(0deg); }
}
```
**Binding:** `.sw-Share ul li a:hover` → `0.4s linear forwards` (desktop only — attention-drawing wiggle)
### 4.10 `rotate-y` / `rotate-x` — Continuous Rotation
```css
@keyframes rotate-y {
0% { transform: translateY(-50%) rotate(0deg); }
100% { transform: translateY(-50%) rotate(360deg); }
}
@keyframes rotate-x {
0% { transform: translateX(-50%) rotate(0deg); }
100% { transform: translateX(-50%) rotate(360deg); }
}
```
**Binding:** Decorative spinning geometric ornaments (likely diamond shapes).
---
## 5. Transition Patterns
### 5.1 Universal Hover Transition
```css
transition: 0.4s;
```
Applied to nearly all interactive elements, but **only on desktop** (`min-width: 768px`). This creates a consistent, unhurried feel across all hover states.
**Elements using this pattern (60+ selectors observed):**
- `.sw-Button` + `::before` + `::after`
- `.st-Header_Static nav ul li a::before` (underline)
- `.st-Footer_Inner nav ul li a::before`
- `.sw-Breadcrumb ul li a`
- `.sw-Pagination ul li a`
- `.sw-Body a` and `a img`
- `.sw-Share ul li a` and `a img`
- `.sw-Modal` and `.sw-Modal_Close span`
- `.idx-News_Inner .wrap ul li a .thumb::before` (overlay)
- `.idx-Channel_Inner .wrap .item ul li a .thumb img`
- `.idx-Character_Inner .wrap .chara-image li` and `li img`
- `.idx-Character_Inner .wrap .chara-button li a` and `::before`
- `.idx-Special_Inner .wrap ul li a::before`, `::after`, `.thumb::before`
- `.idx-Official_Inner .wrap ul li a`
- `.st-Header_Fixed_Inner .hamburger button` + `::before` + `::after`
- `.st-Header_Fixed_Inner nav .logo, .menu, .official`
- `.st-Footer_Inner .middle .logo a`, `.share ul li a::before`, `.share ul li a img`
- `.st-Footer_Inner .official ul li a`, `.bottom a`
- `.st-Pagetop a::before`, `a::after`
### 5.2 Transform Transitions (0.4s)
```css
transition: transform 0.4s, -webkit-transform 0.4s;
```
Used for image zoom effects:
- `.idx-Pickup_Inner .wrap ul li a img` — carousel image zoom
- `.idx-News_Inner .wrap ul li a .thumb img` — news thumbnail zoom
- `.idx-Goods_Inner .wrap ul li a img` — goods image zoom
- `.idx-Special_Inner .wrap ul li a .thumb img` — special thumbnail zoom
### 5.3 Header Logo Transform (Layered Timing)
```css
.st-Header_Fixed_Inner .logos {
transition: transform 1s; /* container: slow */
}
.st-Header_Fixed_Inner .logos .logo {
transition: transform 0.4s; /* logo image: fast */
}
.st-Header_Fixed_Inner .logos .text {
transition: opacity 0.4s; /* text: fast fade */
}
```
The header logo area uses a slower 1s transform for the container and 0.4s for individual items — creating a layered, slightly delayed feel when the fixed header transforms.
### 5.4 Nav Menu Open
```css
.st-Header_Fixed_Inner nav {
transition: opacity 0.5s; /* container fades in */
}
.st-Header_Fixed_Inner nav .logo,
.st-Header_Fixed_Inner nav .menu,
.st-Header_Fixed_Inner nav .official {
transition: 0.4s; /* items ready for transition */
}
/* Open state uses fade-up keyframe */
.st-Header_Fixed_Inner nav.open .logo,
.st-Header_Fixed_Inner nav.open .menu,
.st-Header_Fixed_Inner nav.open .official {
animation: fade-up 0.4s ease-in-out forwards;
}
```
### 5.5 Specific Property Transitions
```css
/* Nav menu underline width */
.st-Header_Fixed_Inner nav .menu li a::before {
transition: width 0.4s;
}
/* Official link icon transform */
.st-Header_Fixed_Inner > .official ul li a::before {
transition: transform 0.4s;
}
/* News title color */
.idx-News_Inner .wrap ul li a .title {
transition: color 0.4s;
}
/* Tab dot opacity */
.idx-Pickup_Inner #slider-nav_dots .slick-dots li button {
transition: opacity 0.4s;
}
/* Body opacity (page load) */
body { transition: opacity 0.4s; }
/* Loading screen */
.st-Loading { transition: opacity 0.4s 0.2s; } /* 0.2s delay */
```
---
## 6. Delay Chains
### 6.1 Header Nav Stagger (Page Load)
Nav items enter sequentially on page load with `header-nav 0.8s ease forwards`:
| Item | Delay | Cumulative |
|------|-------|------------|
| 1st (HOME) | `0.1s` | 0.1s |
| 2nd (NEWS) | `0.2s` | 0.2s |
| 3rd (ABOUT) | `0.3s` | 0.3s |
| 4th (GAME) | `0.4s` | 0.4s |
| 5th (ANIME) | `0.5s` | 0.5s |
| 6th (CHARACTER) | `0.6s` | 0.6s |
| 7th (GOODS) | `0.7s` | 0.7s |
| 8th (SPECIAL) | `0.8s` | 0.8s |
**Step: 0.1s** — linear stagger, left to right.
### 6.2 Character Gallery Stagger (Scroll-Triggered)
Character portraits enter with staggered delays using `header-nav 1s ease forwards`. The stagger is **not linear** — it's grouped by visual depth:
**Desktop (min-width: 768px):**
| Position | Character | Delay | Visual layer |
|----------|-----------|-------|-------------|
| 1st | Front-left | `0.3s` | Foreground |
| 2nd | Front-center-left | `0.35s` | Foreground |
| 3rd | Front-center | `0.4s` | Foreground |
| 4th | Front-center-right | `0.45s` | Foreground |
| 5th | Front-right | `0.5s` | Foreground |
| 6th | Back-left | `50ms` | **Background (enters first)** |
| 7th | Back-center-left | `0.1s` | Background |
| 8th | Back-center | `0.15s` | Background |
| 9th | Back-center-right | `0.2s` | Background |
| 10th | Back-right | `0.25s` | Background |
**Key insight:** Background characters (6th–10th) enter **before** foreground characters (1st–5th), creating a depth-perception effect where the back row appears first and the front row cascades in on top.
### 6.3 Hero Entrance Choreography
```
body opacity: 0.4s (immediate)
└── hero blur: 1s ease-out (0.2s delay)
└── nav items: 0.8s each (0.1s stagger starting at 0.1s)
└── current nav underline: 0.5s ease (0.8s delay — after nav settles)
```
Total entrance sequence: ~1.3s from page load to fully settled state.
---
## 7. Scroll-Triggered Animation System
The site uses a **class-toggling scroll observer** pattern (no third-party library):
```
.anim-sc → element is scroll-observable (starts invisible)
.animFade → will use load-opacity keyframe
.animFadeup → will use fade-up keyframe
.animated → class added when element enters viewport
```
```css
/* Before scroll trigger: invisible */
.anim-sc {
opacity: 0;
}
/* After scroll trigger: animate in */
.anim-sc.animFade.animated {
animation: load-opacity 1s ease-out forwards;
}
.anim-sc.animFadeup.animated {
animation: fade-up 1s ease-out forwards;
}
/* SVG variant */
svg.anim-sc.animated {
animation: svg-stroke 2s ease-out forwards;
}
```
This is a lightweight alternative to AOS — an Intersection Observer adds `.animated` class, CSS handles the rest. No JavaScript animation runtime needed.
**Load-triggered variant:** `.anim-ld` uses the same keyframes but triggers on page load instead of scroll.
---
## 8. Canvas Particle System
The hero section includes `<canvas id="particles">` with floating particle specs creating a magical/crystalline atmosphere. CSS transition `0.2s` is applied for opacity fading. The canvas implementation is JavaScript-driven (not extracted — would require JS file download).
---
## 9. Motion Primitives (Reusable)
| Primitive | Keyframe | Duration | Easing | Trigger | Copy-paste ready |
|-----------|----------|----------|--------|---------|-----------------|
| Fade in | `load-opacity` | 0.4s–1s | ease-out / ease-in-out | Scroll / load | Yes |
| Fade up | `fade-up` | 0.4s–1s | ease-out / ease-in-out | Scroll / menu open | Yes |
| Blur reveal | `load-blur` | 1s | ease-out | Page load (hero) | Yes |
| Line draw | `svg-stroke` | 2s | ease-out | Scroll | Yes |
| Shadow slide | `chara-shadow` | — | — | Scroll (character) | Yes |
| Underline grow | `header-line` | 0.5s | ease | Page load (nav) | Yes |
| Hover transition | CSS transition | 0.4s | default | Hover (desktop) | Yes |
| Stagger chain | delay increment | 0.1s step (nav) / 0.05s step (chars) | — | Load / scroll | Pattern |
| Wiggle | `post-share` | 0.4s | linear | Hover | Yes |
| Continuous spin | `rotate-y` / `rotate-x` | infinite | — | Always | Yes |
---
## 10. Motion Classification
| Source Motion | Classification | Notes |
|--------------|---------------|-------|
| 0.4s universal hover transition | **Reusable** | Clean, consistent feel; swap duration to taste |
| Fade-up scroll reveal (20px translateY) | **Reusable** | Standard modern pattern |
| Blur-to-sharp hero reveal | **Reusable** | Elegant loading effect; works for any hero |
| SVG stroke draw | **Reusable** | Works for any decorative line art |
| Nav stagger delays (0.1s step) | **Reusable** | Copy the step pattern for any sequential entrance |
| Nav underline grow animation | **Reusable** | Elegant active-state indicator |
| Share button wiggle | **Reusable** | Simple attention draw |
| Scroll-triggered class-toggle system | **Reusable** | Lightweight AOS alternative |
| Character depth-stagger (back-first) | **Adapted** | Gallery-specific but depth concept is reusable |
| Canvas particles | **Adapted** | Mood-specific; concept reusable for atmospheric effects |
| Loading mask wipe (6s) | **Adapted** | Technique reusable; timing is site-specific |
| Continuous rotation ornaments | **Adapted** | Decorative; concept reusable |
```
### references/9nine-visual-novel/guides/evidence-manifest.md
```markdown
# 9-nine- Visual Novel Portal — Evidence Manifest
> **Extraction date:** 2026-03-13
> **Extractor:** Claude Code / Style Extractor Skill
> **Source URL:** https://9-nine-project.com/
---
## 1. Source Information
| Field | Value |
|-------|-------|
| URL | https://9-nine-project.com/ |
| Site type | Visual novel / anime IP promotional portal |
| CMS | WordPress (custom theme: `nine-project`) |
| Pages analyzed | Homepage only (index) |
| Viewport | 1280px desktop |
| Date captured | 2026-03-13 |
---
## 2. Screenshots
| File | Description | What it captures |
|------|-------------|-----------------|
| `evidence/screenshots/01-homepage-viewport.png` | Homepage initial viewport (above fold) | Hero area, header nav, particle canvas |
| `evidence/screenshots/02-homepage-fullpage.png` | Full page screenshot (all sections) | Complete page layout, section spacing, vertical rhythm |
| `evidence/screenshots/03-nav-hover.png` | Header navigation with hover on "NEWS" | Nav underline reveal (::before height: 42px, opacity: 0.6) |
| `evidence/screenshots/04-button-hover.png` | "VIEW MORE" button hover state | Background change #004789 → #0c569a, line slides to right: -60px, arrow flips |
| `evidence/screenshots/05-footer.png` | Footer section | Blue bg, diamond overlay, wave border, nav links, copyright |
| `evidence/screenshots/06-character-section.png` | Character gallery section | Staggered character portraits, name plates, depth layout |
| `evidence/screenshots/07-news-section.png` | News listing section | Thumbnail cards, date/category/title typography, "VIEW MORE" button |
---
## 3. Downloaded Assets
| File | Source | Description |
|------|--------|-------------|
| `evidence/assets/style.css` | `nine-project/assets/css/index/style.css` | Main stylesheet (minified) — all selectors, keyframes, transitions, media queries |
| `evidence/assets/google-fonts.css` | Google Fonts API | Font-face declarations for Cormorant Infant (600, 700), Noto Sans JP (400, 500, 700), Noto Serif JP (500, 700), Oswald (400) |
| `evidence/assets/yakuhanjp.min.css` | jsDelivr CDN (v3.3.1) | YakuHanJP font-face declarations |
---
## 4. Extraction Methods
### 4.1 Static Evidence
| Method | Tool | Details |
|--------|------|---------|
| Page navigation | `mcp__chrome-devtools__navigate_page` | Navigated to homepage, waited for full load |
| DOM snapshot | `mcp__chrome-devtools__take_snapshot` | Full a11y tree — 256 nodes, all interactive elements identified |
| Screenshots | `mcp__chrome-devtools__take_screenshot` | 7 screenshots (viewport, fullpage, 5 component-level) |
| Computed styles — global | `evaluate_script` | Extracted all unique text colors (6), bg colors (6), border colors (7), font families (5), font sizes (10), box-shadows (1), filters (2) from every DOM element |
| Computed styles — components | `evaluate_script` | `getComputedStyle` on body, nav links (4), section headings (4 with .en/.ja), buttons (2 with ::before/::after), footer (with ::before/::after), tab dots (3), character name plates (3), news items |
| Hover state — nav | `mcp__chrome-devtools__hover` on uid `1_5` + `evaluate_script` | Captured ::before height change (0 → 42px), opacity (→ 0.6) |
| Hover state — button | `mcp__chrome-devtools__hover` on uid `1_112` + `evaluate_script` | Captured bg change (#004789 → #0c569a), ::before right (-50px → -60px), ::after transform flip |
| Section layout | `evaluate_script` | Extracted padding/margin/width for all 18 `[class*="idx-"]` sections and their `_Inner` containers |
| Header/footer layout | `evaluate_script` | Header dimensions, nav display, logo size, footer inner width/padding/margin, copyright styles |
### 4.2 Motion Evidence
| Method | Details |
|--------|---------|
| @keyframes extraction | Parsed all `CSSKeyframesRule` from stylesheets — 11 unique keyframes (some duplicated across PC/SP sheets) |
| Transition scan | Extracted all `transition` properties from CSS rules — 60+ transition declarations catalogued |
| Animation binding scan | Extracted all `animation` / `animationName` declarations with duration, delay, easing — 15 animation bindings |
| Delay chain extraction | Regex-matched `nth-of-type` selectors with `animationDelay` — 8 header nav delays, 20 character delays (10 PC + 10 SP) |
| Breakpoint extraction | Collected all `CSSMediaRule.conditionText` — 15 unique media query breakpoints |
| CSS variable scan | Scanned all `:root` rules — confirmed zero custom properties (all values hardcoded) |
### 4.3 Tools NOT Used
- `scripts/transition-scanner.js` — Not injected; CSS rule parsing via `evaluate_script` was sufficient for complete transition inventory
- `scripts/interaction-diff.js` — Not needed; site uses scroll-trigger classes (`.animated`), not inline-style mutations
- `scripts/library-detect.js` — Not needed; no third-party animation library detected (pure CSS keyframes + class toggling)
- `scripts/motion-tools.js` — Not needed; no opaque JS-driven motion requiring rAF sampling
- Performance trace — Not needed for this complexity level
---
## 5. Interactions Tested
| Interaction | Method | Evidence | Completeness |
|-------------|--------|----------|-------------|
| Nav link hover ("NEWS") | `hover` on uid `1_5` + computed style capture | Screenshot 03, ::before height 0→42px, opacity→0.6 | Full |
| "VIEW MORE" button hover | `hover` on uid `1_112` + computed style capture | Screenshot 04, bg #004789→#0c569a, line right -50px→-60px, arrow flips | Full |
| Carousel tab dots | DOM snapshot observation | `[role="tab"]` with `aria-selected`, opacity 0.3/1.0, transform rotate(45deg) | Observed (no click test) |
| Character gallery | Computed style extraction | Staggered animation delays extracted, depth ordering confirmed | Delays only (scroll trigger not fired) |
| News thumbnail hover | CSS rule extraction | `transform: scale(1.05)` on img, `color: #004789` on title, overlay ::before | CSS rules only (not visually tested) |
---
## 6. Component State Matrix
### Navigation Link
| State | Color | `::before` height | `::before` opacity | Trigger | Evidence |
|-------|-------|-------------------|-------------------|---------|----------|
| Default | `#004789` | `0` | — | — | Computed styles |
| Hover | `#004789` | `42px` | `0.6` | Mouse hover | Screenshot 03 + computed diff |
| Current | `#004789` | `42px` | `1` | Page match | `header-line 0.5s ease 0.8s` keyframe |
### "VIEW MORE" Button
| State | Background | Color | `::before` right | `::after` transform | Evidence |
|-------|-----------|-------|------------------|---------------------|----------|
| Default | `#004789` | `#fff` | `-50px` | `rotate(20deg)` | Computed styles |
| Hover | `#0c569a` | `#fff` | `-60px` | `rotate(180deg)` | Screenshot 04 + computed diff |
| Focus-visible | Not styled | — | — | — | No `:focus-visible` rules found |
### Carousel Tab Dot
| State | Background | Opacity | Shape | Evidence |
|-------|-----------|---------|-------|----------|
| Default | `#004789` | `0.3` | 8×8px diamond (rotate 45°) | Computed styles |
| Selected | `#004789` | `1` | 8×8px diamond | DOM `aria-selected="true"` |
### News Item
| State | Thumbnail | Title color | Overlay | Evidence |
|-------|-----------|------------|---------|----------|
| Default | `scale(1)` | `#343434` | Hidden | Computed styles |
| Hover | `scale(1.05)` | `#004789` | Fades in | CSS transition rules |
---
## 7. Confidence Assessment
### High Confidence
- **Color palette** — Complete extraction from computed styles across all DOM elements (6 text colors, 6 bg colors, 7 border colors)
- **Typography** — All four font families confirmed via Google Fonts request + computed styles; all 10 font sizes catalogued
- **@keyframes** — Complete extraction (11 unique keyframes with full CSS text)
- **Transition patterns** — Complete inventory (60+ transition rules from CSS)
- **Button/nav hover states** — Directly observed via hover + computed style diff
- **Delay chains** — Complete extraction from CSS nth-of-type rules (8 nav + 20 character)
- **Breakpoints** — All 15 media query conditions extracted from CSS
- **Section spacing** — All 18 sections measured via computed styles
### Medium Confidence
- **Character gallery layout** — Delays and image dimensions extracted, but absolute positioning coordinates not fully mapped
- **Loading screen sequence** — Keyframes and timings extracted from CSS, but actual loading behavior not observed (page loaded too fast)
- **Footer decorative images** — Background image URLs and sizes extracted from ::before/::after computed styles, but actual tile patterns only visible in screenshots
### Gaps / Not Captured
- **Mobile layout** — Only desktop viewport (1280px) analyzed. Mobile uses extensive `vw` units and different character stagger delays
- **Inner pages** — Only homepage analyzed. NEWS detail, CHARACTER detail, GAME pages may have additional components
- **JavaScript behavior** — No JS files downloaded. Scroll observer implementation is inferred from CSS class patterns (`.anim-sc` → `.animated`)
- **Focus-visible states** — The site does not have custom `:focus-visible` styling
- **Loading screen real-time** — The full loading screen choreography (6s mask wipe) was not observed
- **Canvas particle implementation** — Canvas code not extracted; only CSS transition property captured
- **News category tag** — `.cat` element returned null in computed styles (may be conditionally rendered or differently structured)
---
## 8. Third-Party Dependencies
| Library | Version | Purpose |
|---------|---------|---------|
| YakuHanJP | 3.3.1 | Japanese punctuation-optimized font subset |
| Google Fonts | — | Cormorant Infant, Noto Sans JP, Noto Serif JP, Oswald |
| WordPress | — | CMS (custom theme `nine-project`) |
| Slick Slider | — | Carousel (inferred from `.slick-next`, `.slick-prev`, `.slick-dots` selectors in CSS) |
| No animation libraries | — | All motion is pure CSS keyframes + Intersection Observer class toggling |
```
### references/motherduck-design-system-reference/guides/style-guide.md
```markdown
# MotherDuck Design System — Style Guide
> **Source:** https://motherduck.com/
> **Date captured:** 2026-03-13
> **Viewport:** 1280px desktop
> **CMS:** Next.js (styled-components)
---
## 1. Design Personality
**Technical product made approachable.** MotherDuck's visual language is built on monospace typography, hard 2px corners, strong charcoal borders, and a warm cream canvas. The result feels like a well-designed developer tool that doesn't take itself too seriously — sharp and systemized, but never cold.
**Key personality traits (evidence-grounded):**
- **Monospace-led** — `Aeonik Mono` is used for body, nav, buttons, and headings alike, giving the entire site a console/terminal feel
- **Border-disciplined** — `2px solid rgb(56, 56, 56)` is the universal structural element; shadows are rare
- **Warm neutral base** — cream `rgb(244, 239, 234)` instead of cold gray or white
- **Single-radius system** — `2px` on nearly every component; no radius variety
- **Accent-role clarity** — yellow owns CTA, teal owns secondary/success, blue owns informational; they never compete
- **Retro offset shadow** — `rgb(56, 56, 56) -6px 6px 0px 0px` on featured cards, creating a flat-but-lifted effect
---
## 2. Semantic Color Tokens
### 2.1 Accent colors
| Token | Value | Usage |
|---|---|---|
| `--color-accent-primary` | `rgb(255, 222, 0)` | Primary CTA background ("TRY 7 DAYS FREE"), persona badge (Software Engineers), section highlights |
| `--color-accent-secondary` | `rgb(83, 219, 201)` | Secondary persona badge (Data Scientists), success/support accents |
| `--color-accent-info` | `rgb(111, 194, 255)` | Tertiary persona badge (Data Engineers), "START FREE" nav button background |
| `--color-accent-coral` | `rgb(255, 113, 105)` | Newsletter "SUBSCRIBE" badge background |
| `--color-accent-lime` | `rgb(249, 238, 62)` | Subtle yellow variant (observed on some interactive states) |
### 2.2 Text colors
| Token | Value | Usage |
|---|---|---|
| `--color-text-primary` | `rgb(56, 56, 56)` | Headings, body text, borders, button text — the dominant text color |
| `--color-text-body` | `rgb(0, 0, 0)` | Form input text, some body copy |
| `--color-text-muted` | `rgb(161, 161, 161)` | Disabled button text, muted labels, input borders (default) |
| `--color-text-subtle` | `rgb(129, 129, 129)` | Secondary metadata |
| `--color-text-inverse` | `rgb(255, 255, 255)` | Text on dark backgrounds (footer, dark badges, persona cards active state) |
| `--color-text-soft` | `rgb(192, 192, 192)` | Very light labels |
### 2.3 Surface colors
| Token | Value | Usage |
|---|---|---|
| `--color-bg-cream` | `rgb(244, 239, 234)` | Page background, secondary button background |
| `--color-bg-soft` | `rgb(248, 248, 247)` | Form input background (70% opacity), disabled button background, featured card background |
| `--color-bg-card` | `rgb(255, 255, 255)` | Cards, content panels |
| `--color-bg-dark` | `rgb(56, 56, 56)` | Footer background, "How We Scale" badge, active persona card state, dark tags |
| `--color-bg-mid` | `rgb(90, 90, 90)` | Subtle dark variant |
| `--color-bg-muted` | `rgb(215, 215, 215)` | Neutral dividers |
### 2.4 Overlay colors
| Token | Value | Usage |
|---|---|---|
| `--overlay-dark` | `rgba(0, 0, 0, 0.7)` | Modal/overlay backdrops |
### 2.5 Badge-specific tinted backgrounds
These appear on ecosystem category badges:
| Token | Value |
|---|---|
| `--badge-bg-blue` | `rgb(235, 249, 255)` |
| `--badge-bg-green` | `rgb(232, 245, 233)` |
| `--badge-bg-purple` | `rgb(247, 241, 255)` |
| `--badge-bg-lime` | `rgb(249, 251, 231)` |
| `--badge-bg-yellow` | `rgb(255, 253, 231)` |
| `--badge-bg-gray` | `rgb(236, 239, 241)` |
| `--badge-bg-indigo` | `rgb(234, 240, 255)` |
| `--badge-bg-red` | `rgb(255, 235, 233)` |
| `--badge-bg-orange` | `rgb(253, 237, 218)` |
Matching border colors: `rgb(84, 180, 222)`, `rgb(56, 193, 176)`, `rgb(178, 145, 222)`, `rgb(179, 196, 25)`, `rgb(225, 196, 39)`, `rgb(132, 166, 188)`, `rgb(117, 151, 238)`, `rgb(243, 142, 132)`, `rgb(245, 177, 97)`.
### 2.6 Implementation-ready CSS variables
```css
:root {
/* Accent */
--color-accent-primary: rgb(255, 222, 0);
--color-accent-secondary: rgb(83, 219, 201);
--color-accent-info: rgb(111, 194, 255);
--color-accent-coral: rgb(255, 113, 105);
/* Text */
--color-text-primary: rgb(56, 56, 56);
--color-text-body: rgb(0, 0, 0);
--color-text-muted: rgb(161, 161, 161);
--color-text-inverse: rgb(255, 255, 255);
/* Surfaces */
--color-bg-cream: rgb(244, 239, 234);
--color-bg-soft: rgb(248, 248, 247);
--color-bg-card: rgb(255, 255, 255);
--color-bg-dark: rgb(56, 56, 56);
}
```
**Transferable rules:**
- Use warm off-whites instead of pure cold grays for the main canvas
- Keep heavy text and borders in charcoal rather than pure black
- Let accent yellow own primary CTA duty exclusively
- Use secondary accent colors for semantic variation (persona, category), not for every button
---
## 3. Typography
### 3.1 Font stack
| Token | Value | Usage |
|---|---|---|
| `--font-ui` | `"Aeonik Mono", sans-serif` | Primary: body, nav, buttons, headings, labels — the dominant face |
| `--font-display` | `"Aeonik Fono", "Aeonik Mono"` | Display emphasis (marquee section dividers) |
| `--font-body` | `Inter, Arial, sans-serif` | Hero subtitle, persona descriptions, form inputs — softer reading face |
| `--font-brand` | `Aeonik, sans-serif` | Small brand labels (e.g., "MotherDuck AI" badge, weight 600) |
### 3.2 Type scale
| Token | Size | Line height | Weight | Usage |
|---|---|---|---|---|
| `--type-display` | `56px` | `67.2px` (1.2) | `400` | Hero H1, uppercase, letter-spacing `1.12px` |
| `--type-section-lg` | `48px` | `57.6px` (1.2) | `400` | Large section headings (e.g., "ECOSYSTEM") |
| `--type-section` | `32px` | `44.8px` (1.4) | `400` | Standard section headings, uppercase |
| `--type-module` | `24px` | `28.8px` (1.2) | `400–500` | Card headings, sub-section titles, uppercase |
| `--type-subtitle` | `20px` | `28px` (1.4) | `300` | Hero subtitle (Inter), descriptive paragraphs |
| `--type-badge` | `18px` | `25.2px` (1.4) | `400` | Persona badges, category labels, uppercase |
| `--type-body` | `16px` | `16px` (1.0) | `400` | Default body, nav links, buttons |
| `--type-small` | `14px` | `19.6px` (1.4) | `300` | Persona descriptions, card metadata (Inter) |
| `--type-caption` | `13px` | — | `400` | Small labels |
| `--type-micro` | `12px` | — | `400` | Fine print, badges |
| `--type-nano` | `11px` | — | `400` | Smallest observed text |
### 3.3 Typography patterns
- **All headings are uppercase** via `text-transform: uppercase`
- **Weight stays near 400** — the site is crisp, not loud. Only the brand label "MotherDuck AI" uses 600
- **Letter-spacing on H1** — `1.12px` (0.02em) adds subtle tracking to the hero
- **Nav links** — `16px`, `letter-spacing: 0.32px`, `line-height: 19.2px`
- **Single-family discipline** — Aeonik Mono carries nav, buttons, headings, and body. Inter is reserved for softer reading contexts (subtitle, descriptions, form inputs)
- **Bilingual note** — `"Noto Sans SC"` is loaded for Chinese content support
---
## 4. Spacing + Density
### 4.1 Layout variables (from CSS)
| Token | Value | Usage |
|---|---|---|
| `--header-desktop` | `90px` | Desktop header height |
| `--header-mobile` | `70px` | Mobile header height |
| `--eyebrow-desktop` | `55px` | Desktop eyebrow bar height |
| `--eyebrow-mobile` | `70px` | Mobile eyebrow bar height |
### 4.2 Spacing tokens (observed)
| Token | Value | Usage |
|---|---|---|
| `--space-xs` | `4px` | Badge padding vertical, tight icon gaps |
| `--space-sm` | `8px` | Button groups, badge H2 vertical padding, small gaps |
| `--space-md` | `16px` | Badge padding horizontal, card internal spacing, form input horizontal padding |
| `--space-lg` | `24px` | Card padding, module spacing, badge H2 horizontal padding |
| `--space-xl` | `32px` | Section internal padding |
| `--space-2xl` | `56px` | Section vertical rhythm |
| `--space-section` | `90px` | Footer top padding |
| `--space-hero` | `127px` | Large section gaps |
### 4.3 Density philosophy
This is a spacious but not luxurious system. Space creates clarity, not drama.
- Section gaps are generous (~127px between major sections)
- Internal component spacing is tight (8–24px)
- Nav bar: 70px height with 20px vertical padding
- The duality matters: if everything is spacious, the product feel is lost; if everything is tight, the marketing layer disappears
---
## 5. Component System
### 5.1 Primary CTA Button
The hero "TRY 7 DAYS FREE" button. Blue background, charcoal border, retro hover lift.
| Property | Default | Hover |
|---|---|---|
| background | `rgb(111, 194, 255)` | `rgb(111, 194, 255)` (unchanged) |
| color | `rgb(56, 56, 56)` | `rgb(56, 56, 56)` |
| border | `2px solid rgb(56, 56, 56)` | `2px solid rgb(56, 56, 56)` |
| border-radius | `2px` | `2px` |
| padding | `16.5px 22px` | `16.5px 22px` |
| font | `Aeonik Mono`, `16px`, `400`, uppercase | — |
| transform | `none` | `translate(7px, -7px)` |
| box-shadow | `none` | `none` (shadow is a sibling pseudo-element) |
| transition | `transform 0.12s ease-in-out` | — |
**Hover mechanism:** The button translates `7px` right and `7px` up, revealing a charcoal offset shadow (`-6px 6px`) underneath. This creates a "press to push back" affordance.
### 5.2 Secondary CTA Button
The "BOOK A DEMO" button. Cream background matching the page, same border and hover pattern.
| Property | Default | Hover |
|---|---|---|
| background | `rgb(244, 239, 234)` | `rgb(244, 239, 234)` |
| color | `rgb(56, 56, 56)` | `rgb(56, 56, 56)` |
| border | `2px solid rgb(56, 56, 56)` | `2px solid rgb(56, 56, 56)` |
| padding | `16.5px 22px` | `16.5px 22px` |
| transform | `none` | `translate(7px, -7px)` |
| transition | `transform 0.12s ease-in-out` | — |
### 5.3 Nav Button ("START FREE")
Smaller padding variant, blue background, same border system.
| Property | Value |
|---|---|
| background | `rgb(111, 194, 255)` |
| color | `rgb(56, 56, 56)` |
| border | `2px solid rgb(56, 56, 56)` |
| border-radius | `2px` |
| padding | `11.5px 18px` |
| font | `Aeonik Mono`, `16px`, `400`, uppercase |
| transition | `transform 0.12s ease-in-out` |
### 5.4 Disabled / Submit Button
| Property | Value |
|---|---|
| background | `rgb(248, 248, 247)` |
| color | `rgb(161, 161, 161)` |
| border | `2px solid rgb(161, 161, 161)` |
| border-radius | `2px` |
| padding | `17.25px 23px` |
| cursor | `pointer` (still pointer even when disabled) |
| transition | `transform 0.12s ease-in-out` |
Transferable idea: disabled buttons remain structurally present — they don't ghost away. The muted palette signals inactivity without breaking the layout.
### 5.5 Nav Link
| Property | Value |
|---|---|
| font | `Aeonik Mono`, `16px`, `400` |
| color | `rgb(56, 56, 56)` |
| letter-spacing | `0.32px` |
| border | `0.667px solid transparent` |
| border-radius | `2px` |
| display | `flex`, gap `6px` |
| transition | `all` (browser default) |
Hover effect is understated: the transparent border becomes visible rather than the text wildly changing.
### 5.6 Text Link ("READ MORE", "LEARN MORE")
| Property | Value |
|---|---|
| font | `Aeonik Mono`, `16px`, `400` |
| color | `rgb(56, 56, 56)` |
| border | `0.667px solid transparent` |
| text-decoration | `none` |
| transition | `all` |
### 5.7 Card (Content Panel)
White background, charcoal border, no radius. Used for feature cards ("Hypertenancy", "MCP Server").
| Property | Value |
|---|---|
| background | `rgb(255, 255, 255)` |
| border | `2px solid rgb(56, 56, 56)` |
| border-radius | `0px` |
| display | `flex` |
| overflow | `hidden` |
### 5.8 Featured Card (with retro shadow)
The AI demo card uses the retro offset shadow pattern.
| Property | Value |
|---|---|
| background | `rgb(248, 248, 247)` |
| border | `2px solid rgb(56, 56, 56)` |
| box-shadow | `rgb(56, 56, 56) -6px 6px 0px 0px` |
### 5.9 Form Input
| Property | Value |
|---|---|
| font | `Inter`, `16px`, `400` |
| color | `rgb(0, 0, 0)` |
| background | `rgba(248, 248, 247, 0.7)` |
| border | `2px solid rgb(56, 56, 56)` |
| border-radius | `2px` |
| padding | `16px 40px 16px 24px` |
| letter-spacing | `0.32px` |
| transition | `0.2s ease-in-out, padding` |
The chat-style input variant uses:
| Property | Value |
|---|---|
| font | `Aeonik Mono`, `14px` |
| border | `2px solid rgb(56, 56, 56)` |
| border-radius | `0px` |
| padding | `0px 16px` |
| height | `44px` |
| transition | `box-shadow 0.15s` |
### 5.10 Persona Badge (H3 with colored background)
Used in the "Who is it for?" section. Each persona gets a distinct accent color.
| Persona | Background | Color | Border |
|---|---|---|---|
| Software Engineers | `rgb(255, 222, 0)` | `rgb(56, 56, 56)` | `2px solid rgb(56, 56, 56)` |
| Data Scientists | `rgb(83, 219, 201)` | `rgb(56, 56, 56)` | `2px solid rgb(56, 56, 56)` |
| Data Engineers | `rgb(111, 194, 255)` | `rgb(56, 56, 56)` | `2px solid rgb(56, 56, 56)` |
Common properties: `padding: 4px 16px`, `font-size: 18px`, `font-weight: 400`, `text-transform: uppercase`, `border-radius: 0px`.
Active/selected state inverts: `background: rgb(56, 56, 56)`, `color: rgb(255, 255, 255)`, with `transition: background-color 0.3s, color 0.3s`.
### 5.11 Section Badge Heading (H2 with background)
A distinctive pattern: section headings rendered as inline badges with colored backgrounds.
| Heading | Background | Color | Padding |
|---|---|---|---|
| "How We Scale" | `rgb(56, 56, 56)` | `rgb(255, 255, 255)` | `8px 24px` |
| "SUBSCRIBE" | `rgb(255, 113, 105)` | `rgb(56, 56, 56)` | `8px 30px` |
Common: `font-size: 32px`, `font-weight: 400`, `text-transform: uppercase`, `border-radius: 0px`.
### 5.12 Persona Card (Clickable)
| Property | Default | Hover (implied) |
|---|---|---|
| transform | `scale(0.9)` | `scale(1)` |
| transition | `transform 0.3s` | — |
| border | none | — |
| background | transparent | — |
Cards start at 90% scale and grow to full size on hover/interaction.
### 5.13 Community Social Card
Profile picture: `border-radius: 50%`, `width: 46px`. Cards are minimal — no border, no background, no shadow. The content (quote text + profile) carries the visual weight.
---
## 6. Border, Radius, Shadow, and Layering
| Trait | Pattern |
|---|---|
| Border strategy | Borders do most of the structural work. `2px solid rgb(56, 56, 56)` is the universal border. |
| Radius scale | Essentially one value: `2px`. Circles (`50%`) only for avatars. `11px` observed once (likely third-party). |
| Shadow strategy | Rare. The retro offset shadow `rgb(56, 56, 56) -6px 6px 0px 0px` is the only shadow pattern. No soft drop shadows. |
| Hover shadow | Buttons don't gain shadows on hover — they translate to reveal a pre-existing offset shadow underneath. |
| Opacity | Modest. `rgba(248, 248, 247, 0.7)` on form inputs. No heavy transparency layers. |
| Layering | Conventional product-site layering. No theatrical z-index stacking. |
**Key insight:** This system proves how much identity can come from border discipline alone. The single radius, single border weight, and single shadow pattern create a cohesive visual language without any complexity.
---
## 7. Layout Tendencies
### 7.1 Page structure
- **Eyebrow bar** — announcement strip at top (`55px` desktop, `70px` mobile)
- **Header** — `70px` height, `20px` vertical padding, transparent background
- **Main content** — full-width sections with internal containers
- **Footer** — `rgb(56, 56, 56)` dark background, `padding: 90px 0 72px`
### 7.2 Section patterns
The homepage uses several distinct section types:
1. **Hero** — large H1 + subtitle + dual CTA buttons
2. **Interactive demo** — embedded AI chat card with retro shadow
3. **Marquee divider** — repeating text strip ("DATA WAREHOUSE + AI", "USE CASES") as section separators
4. **Feature cards** — white bordered cards with image + text split layout
5. **Persona grid** — three clickable persona cards with colored badges
6. **Use case tabs** — tabbed content with duck illustrations
7. **Scaling diagram** — duckling size cards with illustrations
8. **Ecosystem hub** — category badges linking to integration pages
9. **Testimonial carousel** — horizontal scrolling quote cards
10. **Community wall** — social media post cards in a masonry-like grid
11. **Slack CTA** — full-width banner with illustration
12. **Newsletter** — yellow background section with email form
13. **Footer** — dark background with link columns
### 7.3 Reusable layout patterns
- Centered content containers with generous padding
- Clean grid sections with obvious reading order
- Flat cards with strong border separation
- Marquee text strips as section dividers (not just decorative — they establish section identity)
- Badge headings that break out of the normal heading hierarchy
### 7.4 Adapt carefully
- Warm marketing hero for tools that need denser utility-first landing pages
- Playful accent pairings in serious enterprise contexts
- Marquee dividers may feel too casual for some products
### 7.5 Discard
- Exact product nav, pricing flow, and copy hierarchy
- Duck-themed language and illustrations
- Specific persona/use-case content structure
---
## 8. Responsive Behavior
### 8.1 Breakpoints (from CSS media queries)
| Breakpoint | Usage |
|---|---|
| `max-width: 480px` | Small mobile |
| `min-width: 480px` | Mobile landscape |
| `min-width: 556px` | Small tablet |
| `min-width: 728px` | Tablet |
| `max-width: 768px` | Mobile-first max |
| `min-width: 960px` | Desktop start |
| `max-width: calc(1301px)` | Below wide desktop |
| `min-width: 1302px` | Wide desktop |
| `min-width: 1440px` | Large desktop |
| `min-width: 1600px` | Extra-wide |
### 8.2 Responsive patterns
- Container padding reduces on mobile
- Header height shifts: `90px` desktop → `70px` mobile
- Eyebrow bar: `55px` desktop → `70px` mobile (taller on mobile for touch targets)
- Multi-column areas collapse to single column
- The token system stays consistent across breakpoints — only layout changes, not visual language
---
## 9. Copy-Paste Examples
### 9.1 Primary CTA with retro shadow
```html
<div class="md-cta-wrap">
<a href="#" class="md-btn md-btn--primary">TRY 7 DAYS FREE</a>
</div>
```
```css
.md-cta-wrap {
position: relative;
display: inline-block;
}
.md-cta-wrap::after {
content: '';
position: absolute;
inset: 0;
background: rgb(56, 56, 56);
z-index: -1;
}
.md-btn {
position: relative;
display: inline-flex;
align-items: center;
gap: 8px;
font-family: "Aeonik Mono", sans-serif;
font-size: 16px;
font-weight: 400;
text-transform: uppercase;
padding: 16.5px 22px;
border: 2px solid rgb(56, 56, 56);
border-radius: 2px;
text-decoration: none;
transition: transform 0.12s ease-in-out;
}
.md-btn--primary {
background: rgb(111, 194, 255);
color: rgb(56, 56, 56);
}
.md-btn--primary:hover {
transform: translate(7px, -7px);
}
```
### 9.2 Secondary CTA
```css
.md-btn--secondary {
background: rgb(244, 239, 234);
color: rgb(56, 56, 56);
}
.md-btn--secondary:hover {
transform: translate(7px, -7px);
}
```
### 9.3 Disabled button
```css
.md-btn--disabled {
background: rgb(248, 248, 247);
color: rgb(161, 161, 161);
border-color: rgb(161, 161, 161);
cursor: not-allowed;
}
```
### 9.4 Flat card
```html
<article class="md-card">
<h3 class="md-card__title">Feature Title</h3>
<p class="md-card__body">Supporting copy.</p>
</article>
```
```css
.md-card {
background: rgb(255, 255, 255);
border: 2px solid rgb(56, 56, 56);
border-radius: 0px;
overflow: hidden;
display: flex;
}
.md-card__title {
font-family: "Aeonik Mono", sans-serif;
font-size: 24px;
font-weight: 400;
text-transform: uppercase;
color: rgb(56, 56, 56);
}
```
### 9.5 Persona badge
```html
<span class="md-badge md-badge--yellow">Software Engineers</span>
<span class="md-badge md-badge--teal">Data Scientists</span>
<span class="md-badge md-badge--blue">Data Engineers</span>
```
```css
.md-badge {
display: inline-block;
font-family: "Aeonik Mono", sans-serif;
font-size: 18px;
font-weight: 400;
text-transform: uppercase;
padding: 4px 16px;
border: 2px solid rgb(56, 56, 56);
border-radius: 0px;
color: rgb(56, 56, 56);
transition: background-color 0.3s, color 0.3s;
}
.md-badge--yellow { background: rgb(255, 222, 0); }
.md-badge--teal { background: rgb(83, 219, 201); }
.md-badge--blue { background: rgb(111, 194, 255); }
.md-badge.is-active {
background: rgb(56, 56, 56);
color: rgb(255, 255, 255);
}
```
### 9.6 Section badge heading
```html
<h2 class="md-section-badge md-section-badge--dark">How We Scale</h2>
```
```css
.md-section-badge {
display: inline-block;
font-family: "Aeonik Mono", sans-serif;
font-size: 32px;
font-weight: 400;
text-transform: uppercase;
line-height: 44.8px;
border-radius: 0px;
}
.md-section-badge--dark {
background: rgb(56, 56, 56);
color: rgb(255, 255, 255);
padding: 8px 24px;
}
.md-section-badge--coral {
background: rgb(255, 113, 105);
color: rgb(56, 56, 56);
padding: 8px 30px;
border: 2px solid rgb(56, 56, 56);
}
```
### 9.7 Form input
```html
<input class="md-input" placeholder="Enter value" />
```
```css
.md-input {
width: 100%;
font-family: Inter, Arial, sans-serif;
font-size: 16px;
font-weight: 400;
letter-spacing: 0.32px;
padding: 16px 24px;
border: 2px solid rgb(56, 56, 56);
border-radius: 2px;
background: rgba(248, 248, 247, 0.7);
color: rgb(0, 0, 0);
transition: 0.2s ease-in-out;
}
.md-input:focus {
outline: none;
border-color: rgb(56, 56, 56);
}
```
---
## 10. Classification Summary
| Trait | Classification | Notes |
|---|---|---|
| Monospace-led typography | `Reusable` | Single-family discipline creates strong coherence |
| Warm cream canvas | `Reusable` | Approachable alternative to cold grays |
| 2px border + 2px radius system | `Reusable` | Extreme simplicity, high identity |
| Charcoal-not-black text | `Reusable` | Softer than pure black, still authoritative |
| Retro offset shadow hover | `Reusable` | Distinctive interaction pattern |
| `0.12s ease-in-out` button transition | `Reusable` | Snappy, responsive feel |
| Badge heading pattern | `Reusable` | Inline colored headings as section markers |
| Persona color coding | `Reusable` | Three accent colors for three audience segments |
| Marquee section dividers | `Adapted` | Repeating text strips; concept reusable, execution is brand-specific |
| Ecosystem badge palette | `Adapted` | 9 tinted backgrounds; pattern reusable, exact colors are category-specific |
| Duck illustrations | `Discarded` | Brand-specific visual content |
| Product nav and IA | `Discarded` | Pricing/product/community/company structure |
| Testimonial carousel | `Adapted` | Layout pattern reusable, content is product-specific |
| Newsletter yellow section | `Adapted` | Full-width accent section; concept reusable |
---
## 11. Implementation Notes
- **Preserve the one-radius discipline.** Adding multiple radii weakens the system quickly. If you need softer corners for a specific context, use a separate token rather than inflating the base.
- **Prefer border-led structure over shadow-led structure.** The retro offset shadow is the only shadow pattern — don't add soft drop shadows.
- **Font loading:** Aeonik Mono is a commercial font. If unavailable, `"IBM Plex Mono", "SF Mono", monospace` is a reasonable fallback that preserves the console feel.
- **Accessibility gap:** The site does not have custom `:focus-visible` styles. If reusing this system, add clear focus indicators consistent with the border logic (e.g., `outline: 2px solid rgb(111, 194, 255); outline-offset: 2px`).
- **Keep accent color ownership clear.** Yellow = primary CTA. Teal = secondary/success. Blue = informational/nav. Coral = attention/newsletter. Don't let them compete.
- **The `0.12s` transition is intentionally fast.** This is snappier than the typical `0.2–0.3s` web standard, giving the site a responsive, tool-like feel.
---
## 12. Adaptation Notes
- **Keep:** typographic coherence (mono everywhere), sharp geometry (2px), warm neutral base, bordered components, retro shadow hover, badge heading pattern
- **Soften:** brand playfulness if the destination is more serious; reduce accent color count if the product doesn't have three distinct personas
- **Avoid copying:** duck-specific language, full marketing composition, literal CTA copy, exact product navigation structure
```
### references/motherduck-design-system-reference/guides/motion-guide.md
```markdown
# MotherDuck Design System — Motion Guide
> **Source:** https://motherduck.com/
> **Date captured:** 2026-03-13
> **Motion complexity:** Low-moderate — CSS transitions + styled-components keyframes + scroll-triggered transforms
---
## 1. Motion Personality
**Snappy, functional, restrained.** Motion on MotherDuck serves utility, not spectacle. Transitions are fast (0.12s–0.3s), easing is standard, and there are no cinematic entrance sequences. The dominant motion pattern is the retro offset shadow hover — buttons translate to reveal a shadow underneath, creating a satisfying "click me" affordance.
**Key motion adjectives (evidence-grounded):**
- **Snappy** — `0.12s ease-in-out` is the dominant button transition, faster than typical web standards
- **Functional** — motion signals interactivity (hover, focus, state change), not decoration
- **Restrained** — no scroll-triggered entrance animations on the main content
- **Consistent** — the same `0.12s ease-in-out` pattern applies to all button variants
- **Scroll-driven** — some sections use `transform 1s, opacity 1s` for scroll-triggered reveals
---
## 2. Duration Scale
| Token | Value | Observed usage |
|---|---|---|
| `--duration-snap` | `30ms` | Active/press state transitions |
| `--duration-fast` | `0.12s` | **Dominant duration** — all button hover transitions |
| `--duration-base` | `0.15s` | Input focus, SVG icon transforms, opacity fades |
| `--duration-ui` | `0.2s` | Border, background, height, opacity state changes |
| `--duration-state` | `0.3s` | Persona badge color swap, nav menu, persona card scale, scroll-triggered transforms |
| `--duration-reveal` | `0.4s` | Section opacity reveals, card entrance fades |
| `--duration-entrance` | `0.5s` | Opacity fade-in with delay |
| `--duration-scroll` | `1s` | Scroll-triggered section transforms + opacity |
**Dominant duration: `0.12s`** — used for all button hover transitions. This is notably faster than the typical `0.2–0.3s` web standard, giving the site a tool-like responsiveness.
---
## 3. Easing Scale
| Token | Value | Observed usage |
|---|---|---|
| `--ease-default` | `ease-in-out` | Buttons, inputs, most UI transitions |
| `--ease-reveal` | `ease-out` | Opacity reveals, section entrances |
| `--ease-entrance` | `ease-in` | Fade-out animations |
| `--ease-spring` | `linear(0 0%, ... 1.003 65.1%, 1 100%)` | Spring-like scroll transform (custom linear easing with overshoot) |
The custom `linear()` easing function creates a spring-like overshoot effect — the element slightly overshoots its target position (peaks at `1.003`) before settling. Used for scroll-triggered card transforms.
---
## 4. Transition Patterns
### 4.1 Button Hover — Retro Offset Lift
```css
transition: transform 0.12s ease-in-out;
```
On hover, buttons translate `translate(7px, -7px)` to reveal a charcoal offset shadow underneath. This is the site's signature interaction.
**Elements using this pattern:**
- "TRY 7 DAYS FREE" (primary CTA)
- "BOOK A DEMO" (secondary CTA)
- "START FREE" (nav CTA)
- "SUBMIT" buttons
- All button variants with `2px solid` borders
### 4.2 Active/Press State
```css
transition: transform 30ms ease-in-out;
```
On `:active`, the transition shortens to `30ms` for immediate press feedback. The button snaps back faster than it lifts.
### 4.3 Persona Badge State Change
```css
transition: background-color 0.3s, color 0.3s;
```
When a persona tab is selected, the badge smoothly inverts from colored-background/dark-text to dark-background/white-text.
### 4.4 Persona Card Scale
```css
transition: transform 0.3s;
```
Persona cards start at `scale(0.9)` and grow to `scale(1)` on hover/interaction.
### 4.5 Input Focus
```css
/* Chat input */
transition: box-shadow 0.15s;
/* Form input */
transition: 0.2s ease-in-out, padding;
```
### 4.6 Section Scroll Reveal
```css
transition: transform 1s, opacity 1s;
```
Combined transform + opacity transition triggered by scroll position. The `1s` duration creates a gentle reveal.
### 4.7 Spring Scroll Transform
```css
transition: transform 1s linear(0 0%, 0.006 1.15%, 0.022 2.3%, 0.091 5.1%,
0.18 7.6%, 0.508 16.3%, 0.607 19.325%, 0.691 22.35%, 0.762 25.375%,
0.822 28.4%, 0.872 31.75%, 0.912 35.1%, 0.944 38.9%, 0.968 43%,
0.985 47.6%, 0.996 53.1%, 1.001 58.4%, 1.003 65.1%, 1 100%);
```
Custom `linear()` easing with spring-like overshoot for scroll-triggered card transforms.
### 4.8 Opacity Fade Scale
```css
/* Fast UI fade */
transition: opacity 0.15s ease-in-out;
/* Medium reveal */
transition: opacity 0.2s ease-in-out;
/* Section reveal */
transition: opacity 0.3s ease-out;
/* Slow entrance */
transition: opacity 0.4s ease-out;
```
### 4.9 Height/Collapse
```css
transition: height 0.15s ease-in-out;
transition: height 0.2s ease-in-out, margin-top 0.2s ease-in-out;
transition: max-height 1s ease-in;
```
### 4.10 SVG Icon Transform
```css
/* Arrow rotation */
transition: transform 0.2s ease-in-out;
/* Icon path morph */
transition: 100ms ease-in-out; /* on path elements */
```
### 4.11 Nav/Dropdown
```css
/* Dropdown border reveal */
transition: border-bottom 0.2s ease-in-out, background-color 0.2s ease-in-out;
/* Dropdown content */
transition: right 0.2s ease-in-out, opacity 0.5s ease-in-out;
```
---
## 5. @keyframes Inventory
### 5.1 Site-Owned Keyframes
#### `hCmOrq` — Fade Up (Toast/Notification Enter)
```css
@keyframes hCmOrq {
0% { transform: translateY(1rem); opacity: 0; }
100% { transform: translateY(0); opacity: 1; }
}
```
**Binding:** `200ms ease-out forwards`
#### `iBNjHa` — Fade Down (Toast/Notification Exit)
```css
@keyframes iBNjHa {
0% { transform: translateY(0); opacity: 1; }
100% { transform: translateY(1rem); opacity: 0; }
}
```
**Binding:** `200ms ease-in-out forwards`
#### `idkxa` — Scale Up (Tooltip/Popover Enter)
```css
@keyframes idkxa {
0% { opacity: 0; transform: translateY(calc(-50% + 1px)) scale(0.8); }
49.9% { opacity: 0; }
50% { opacity: 1; }
100% { opacity: 1; transform: translateY(calc(-50% + 1px)) scale(1); }
}
```
Two-phase animation: invisible scale-up in the first half, then snap to visible and continue scaling.
#### `jLKMiS` — Scale Down (Tooltip/Popover Exit)
Reverse of `idkxa` — scales from 1 to 0.8 while fading out.
### 5.2 Third-Party Keyframes (Toastify — Discarded)
22 Toastify keyframes are present (bounce, zoom, flip, slide variants in all directions). These are from the `react-toastify` library and are **not part of the design system**. Also present: `swiper-preloader-spin` from Swiper.
---
## 6. Complete Transition Inventory
41 transition rules extracted from CSS. Grouped by pattern:
| Pattern | Count | Representative selectors |
|---|---|---|
| `transform 0.12s ease-in-out` | ~8 | `.hYweFU`, `.kIgTrP`, `.fQiulQ` (button variants) |
| `transform 30ms ease-in-out` | ~3 | `.hYweFU:active`, `.kIgTrP:active` (press states) |
| `transform 0.2s ease-in-out` | ~3 | `.hAfsSP`, `.lmjJiJ` (icon/arrow transforms) |
| `transform 0.3s` | ~3 | `.fAbQfd`, `.comCta`, `.fzXPOx` (card/persona scale) |
| `opacity 0.Xs ease-*` | ~6 | Various opacity fades (0.15s–0.4s) |
| `height/margin 0.Xs` | ~3 | `.dmzMdf`, `.hNJpCO` (collapse/expand) |
| `background-color 0.3s, color 0.3s` | 1 | `.dqBFzp` (persona badge swap) |
| `border-bottom 0.2s, bg 0.2s` | 1 | `.boOfwU` (nav dropdown) |
| `box-shadow 0.15s` | 1 | `.hCMCpH` (input focus) |
| `transform 1s, opacity 1s` | 1 | `.bLJbyM` (scroll reveal) |
| Spring `linear()` 1s | 1 | `.go1656994552` (scroll card) |
| `max-height 1s ease-in` | 1 | `.go3128134379` (large collapse) |
---
## 7. Motion Primitives (Reusable)
| Primitive | Duration | Easing | Trigger | Copy-paste ready |
|---|---|---|---|---|
| Button retro lift | `0.12s` | `ease-in-out` | Hover | Yes |
| Button press snap | `30ms` | `ease-in-out` | Active | Yes |
| Badge color swap | `0.3s` | default | State change | Yes |
| Card scale | `0.3s` | default | Hover | Yes |
| Input focus | `0.15s` | default | Focus | Yes |
| Opacity fade (UI) | `0.15s–0.2s` | `ease-in-out` | State change | Yes |
| Opacity fade (reveal) | `0.3s–0.4s` | `ease-out` | Scroll | Yes |
| Section scroll reveal | `1s` | `ease-out` or spring | Scroll | Yes |
| Height collapse | `0.15s–0.2s` | `ease-in-out` | Toggle | Yes |
| Toast enter | `200ms` | `ease-out` | Notification | Pattern |
| Toast exit | `200ms` | `ease-in-out` | Notification | Pattern |
---
## 8. Motion Classification
| Source Motion | Classification | Notes |
|---|---|---|
| `0.12s` button retro lift | **Reusable** | Signature interaction; works for any bordered button system |
| `30ms` active press | **Reusable** | Snappy press feedback; pair with any hover lift |
| Badge color swap `0.3s` | **Reusable** | Clean tab/toggle state transition |
| Card scale `0.3s` | **Reusable** | Subtle hover affordance |
| Input focus transitions | **Reusable** | Standard form interaction |
| Section scroll reveal `1s` | **Reusable** | Gentle content entrance |
| Spring `linear()` easing | **Adapted** | Custom easing; concept reusable, exact curve is site-specific |
| Toastify animations | **Discarded** | Third-party library, not part of design system |
| Tooltip scale animation | **Adapted** | Two-phase opacity+scale; pattern reusable |
| Nav dropdown transitions | **Reusable** | Border + background reveal on hover |
---
## 9. Implementation Notes
- **Motion should stay subordinate to typography and borders.** This is a static-heavy system where motion serves utility, not personality.
- **The `0.12s` duration is intentionally fast.** Don't slow it down to `0.2s` — the snappiness is part of the tool-like feel.
- **The `30ms` active state is key.** It creates a satisfying "click" feel by making the press response nearly instant.
- **No `prefers-reduced-motion` handling observed.** If reusing this system, add `@media (prefers-reduced-motion: reduce)` to disable transforms and reduce transitions to opacity-only.
- **The spring `linear()` easing is a modern CSS feature.** Check browser support if targeting older browsers. Fallback: `ease-out`.
```
### references/motherduck-design-system-reference/guides/evidence-manifest.md
```markdown
# MotherDuck Design System — Evidence Manifest
> **Extraction date:** 2026-03-13
> **Extractor:** Claude Code / Style Extractor Skill
> **Source URL:** https://motherduck.com/
---
## 1. Source Information
| Field | Value |
|---|---|
| URL | https://motherduck.com/ |
| Site type | SaaS product marketing site (cloud data warehouse) |
| Framework | Next.js with styled-components |
| Pages analyzed | Homepage only (index) |
| Viewport | 1280px desktop |
| Date captured | 2026-03-13 |
---
## 2. Screenshots
| File | Description | What it captures |
|---|---|---|
| `evidence/screenshots/01-homepage-viewport.png` | Homepage initial viewport (above fold) | Hero heading, dual CTA buttons, nav bar, eyebrow announcement |
| `evidence/screenshots/03-hero-cta-hover.png` | Hero CTA button hover state | "TRY 7 DAYS FREE" button with retro offset lift (translate 7px, -7px) |
| `evidence/screenshots/04-who-is-it-for.png` | "Who is it for?" persona section | Three persona cards with colored badges, scale interaction |
| `evidence/screenshots/05-how-we-scale.png` | "How We Scale" section | Badge heading (dark bg), duckling size cards with illustrations |
| `evidence/screenshots/06-ecosystem.png` | Ecosystem section | Category badges, integration hub layout |
| `evidence/screenshots/07-footer.png` | Footer and newsletter section | Dark footer, yellow newsletter section, link columns |
---
## 3. Downloaded Assets
| File | Source | Description |
|---|---|---|
| (none downloaded) | — | Site uses styled-components; CSS is generated at runtime, not available as static files |
---
## 4. Extraction Methods
### 4.1 Static Evidence
| Method | Tool | Details |
|---|---|---|
| Page navigation | `mcp__chrome-devtools__navigate_page` | Navigated to homepage, waited for full load |
| DOM snapshot | `mcp__chrome-devtools__take_snapshot` | Full a11y tree — 500+ nodes, all interactive elements identified |
| Screenshots | `mcp__chrome-devtools__take_screenshot` | 6 screenshots (viewport, hover state, 4 section-level) |
| Computed styles — global | `evaluate_script` | Extracted all unique text colors (8), bg colors (21), border colors (14), font families (11), font sizes (13), box-shadows (1), border-radii (3), transitions (30+) |
| Computed styles — components | `evaluate_script` | `getComputedStyle` on body, H1, hero subtitle, all H2s (12), all H3s (10), CTA buttons (3 variants), nav items (8), cards (3), inputs (3), persona badges, footer |
| Hover state — CTA button | `hover` on uid `3_74` + `evaluate_script` | Captured transform change: `none` → `matrix(1, 0, 0, 1, 7, -7)` = `translate(7px, -7px)` |
| Hover state — secondary CTA | `hover` on uid `3_76` + `evaluate_script` | Same transform pattern confirmed on "BOOK A DEMO" |
| Section layout | `evaluate_script` | Extracted H2 positions (y-coordinates), section heights, padding, margins, backgrounds |
| Badge heading pattern | `evaluate_script` | Identified 8 badge-style headings with colored backgrounds |
| Persona card structure | `evaluate_script` | Extracted card dimensions, badge styles, description typography, scale transforms |
### 4.2 Motion Evidence
| Method | Details |
|---|---|
| CSS variable scan | Scanned all `:root` rules — found `--header-desktop`, `--header-mobile`, `--eyebrow-desktop`, `--eyebrow-mobile` + Toastify/Swiper third-party vars |
| @keyframes extraction | Parsed all `CSSKeyframesRule` from stylesheets — 31 total (22 Toastify, 4 site-owned, 1 Swiper, 4 duplicates) |
| Transition scan | Extracted all `transition` properties from CSS rules — 41 transition declarations catalogued |
| Animation binding scan | Extracted all `animation` declarations — 4 animation bindings (all Toastify-related) |
| Media query extraction | Collected all `CSSMediaRule.conditionText` — 12 unique breakpoints |
### 4.3 Tools NOT Used
- `scripts/transition-scanner.js` — Not needed; styled-components CSS rules were accessible via `evaluate_script`
- `scripts/interaction-diff.js` — Not needed; hover states captured via direct hover + computed style diff
- `scripts/library-detect.js` — Not needed; framework identified from DOM structure (Next.js + styled-components class patterns)
- `scripts/motion-tools.js` — Not needed; no opaque JS-driven motion requiring rAF sampling
- Performance trace — Not needed for this complexity level
---
## 5. Interactions Tested
| Interaction | Method | Evidence | Completeness |
|---|---|---|---|
| "TRY 7 DAYS FREE" hover | `hover` on uid `3_74` + computed style capture | Screenshot 03, transform `translate(7px, -7px)` | Full |
| "BOOK A DEMO" hover | `hover` on uid `3_76` + computed style capture | Same transform pattern confirmed | Full |
| Persona badge active state | Computed style extraction | `bg: rgb(56,56,56)`, `color: #fff`, `transition: background-color 0.3s, color 0.3s` | CSS rules (not visually tested) |
| Persona card scale | Computed style extraction | `transform: scale(0.9)`, `transition: transform 0.3s` | CSS rules (not visually tested) |
| Nav link hover | DOM snapshot observation | `border: 0.667px solid transparent` → visible on hover | CSS rules only |
| Input focus | Computed style extraction | `transition: box-shadow 0.15s` / `0.2s ease-in-out` | CSS rules only |
| Popup close | `click` on uid `3_502` | HubSpot popup CTA dismissed | Functional |
---
## 6. Component State Matrix
### Primary CTA Button ("TRY 7 DAYS FREE")
| State | Background | Transform | Box-shadow | Trigger | Evidence |
|---|---|---|---|---|---|
| Default | `rgb(111, 194, 255)` | `none` | `none` | — | Computed styles |
| Hover | `rgb(111, 194, 255)` | `translate(7px, -7px)` | `none` (shadow is sibling) | Mouse hover | Screenshot 03 + computed diff |
| Active | `rgb(111, 194, 255)` | snap back | — | Mouse press | CSS `:active` rule (30ms transition) |
| Focus-visible | Not styled | — | — | — | No `:focus-visible` rules found |
### Secondary CTA Button ("BOOK A DEMO")
| State | Background | Transform | Evidence |
|---|---|---|---|
| Default | `rgb(244, 239, 234)` | `none` | Computed styles |
| Hover | `rgb(244, 239, 234)` | `translate(7px, -7px)` | Computed diff |
### Disabled Button ("SUBMIT")
| State | Background | Color | Border | Evidence |
|---|---|---|---|---|
| Disabled | `rgb(248, 248, 247)` | `rgb(161, 161, 161)` | `2px solid rgb(161, 161, 161)` | Computed styles |
### Persona Badge
| State | Background | Color | Border | Evidence |
|---|---|---|---|---|
| Default (yellow) | `rgb(255, 222, 0)` | `rgb(56, 56, 56)` | `2px solid rgb(56, 56, 56)` | Computed styles |
| Default (teal) | `rgb(83, 219, 201)` | `rgb(56, 56, 56)` | `2px solid rgb(56, 56, 56)` | Computed styles |
| Default (blue) | `rgb(111, 194, 255)` | `rgb(56, 56, 56)` | `2px solid rgb(56, 56, 56)` | Computed styles |
| Active/Selected | `rgb(56, 56, 56)` | `rgb(255, 255, 255)` | `2px solid rgb(56, 56, 56)` | Computed styles |
---
## 7. Confidence Assessment
### High Confidence
- **Color palette** — Complete extraction from computed styles across all DOM elements (8 text colors, 21 bg colors, 14 border colors)
- **Typography** — All font families confirmed via computed styles; all 13 font sizes catalogued
- **Border/radius system** — Universal `2px` radius and `2px solid rgb(56, 56, 56)` border confirmed across all components
- **Button hover mechanism** — Directly observed via hover + computed style diff (translate 7px, -7px)
- **Transition patterns** — Complete inventory (41 transition rules from CSS)
- **Breakpoints** — All 12 media query conditions extracted from CSS
- **Badge heading pattern** — All 8 badge-style headings identified with exact styles
### Medium Confidence
- **Retro offset shadow** — Observed on AI demo card (`-6px 6px 0px 0px`); button hover reveals this shadow via translate, but the shadow implementation (pseudo-element vs sibling) was inferred, not directly inspected
- **Persona card interaction** — Scale transform `0.9` → `1` observed in computed styles, but the exact trigger (hover vs scroll vs intersection) was not confirmed
- **Community social cards** — Minimal styling observed (no border, no bg); may have hover states not captured
- **Marquee section dividers** — Text content detected but exact animation (CSS animation vs JS scroll) not confirmed
### Gaps / Not Captured
- **Mobile layout** — Only desktop viewport (1280px) analyzed
- **Inner pages** — Only homepage analyzed. Product, pricing, blog pages may have additional components
- **Full-page screenshot** — Failed due to page size (14648px height); only viewport screenshots captured
- **Nav dropdown interaction** — Nav buttons (PRODUCT, COMMUNITY, COMPANY) were not interactive during hover testing
- **Testimonial carousel** — Horizontal scroll behavior not tested; only static card styles observed
- **Toast notification** — Toastify is loaded but no toast was triggered during extraction
- **Dark mode** — No dark mode toggle or `prefers-color-scheme` media query observed
- **Focus-visible states** — The site does not have custom `:focus-visible` styling
---
## 8. Third-Party Dependencies
| Library | Version | Purpose |
|---|---|---|
| Next.js | — | React framework (SSR/SSG) |
| styled-components | — | CSS-in-JS (class name patterns: `.hYweFU`, `.kIgTrP`, etc.) |
| react-toastify | — | Toast notifications (22 keyframes, CSS variables) |
| Swiper | — | Carousel/slider (`--swiper-theme-color`, `swiper-preloader-spin` keyframe) |
| Inter | — | Body/description font (Google Fonts or self-hosted) |
| Aeonik Mono | — | Primary UI font (commercial, self-hosted) |
| Aeonik Fono | — | Display font variant (commercial, self-hosted) |
| Aeonik | — | Brand label font (commercial, self-hosted) |
| Noto Sans SC | — | Chinese language support |
| HubSpot | — | Popup CTA forms (iframe-based) |
```
### scripts/transition-scanner.js
```javascript
// Style Extractor: CSS transition scanner (paste into evaluate_script)
//
// Scans all elements for CSS transition declarations and clusters results.
// Returns structured data: per-element transition configs + clustered patterns.
//
// Exposes:
// - window.__seTransition.scan(root?) — scan all elements (default: document)
// - window.__seTransition.keyframes() — extract all @keyframes from stylesheets
//
// This file is intentionally framework-agnostic and safe to paste as an IIFE.
(() => {
if (window.__seTransition?.installed) return;
function cssPath(el) {
if (!el || el.nodeType !== 1) return null;
if (el.id) return `#${CSS.escape(el.id)}`;
const parts = [];
let cur = el;
let depth = 0;
while (cur && cur.nodeType === 1 && depth < 6) {
let part = cur.tagName.toLowerCase();
if (cur.classList && cur.classList.length) {
part += Array.from(cur.classList).slice(0, 2).map(c => `.${CSS.escape(c)}`).join('');
}
const parent = cur.parentElement;
if (parent) {
const same = Array.from(parent.children).filter(c => c.tagName === cur.tagName);
if (same.length > 1) part += `:nth-of-type(${same.indexOf(cur) + 1})`;
}
parts.unshift(part);
if (parent?.id) { parts.unshift(`#${CSS.escape(parent.id)}`); break; }
cur = parent;
depth++;
}
return parts.join(' > ');
}
/**
* Scan all elements under `root` for CSS transition declarations.
* Returns { elements, patterns }.
* elements — per-element details (capped at opts.limit, default 80)
* patterns — clustered by (property + duration + easing) signature
*/
function scan(root, opts) {
const node = root
? (typeof root === 'string' ? document.querySelector(root) : root)
: document;
if (!node) return { error: 'root not found' };
const limit = opts?.limit ?? 80;
const all = node.querySelectorAll('*');
const elements = [];
const patternMap = {};
for (const el of all) {
const s = getComputedStyle(el);
const tp = s.transitionProperty;
const td = s.transitionDuration;
if (!tp || tp === 'none' || tp === 'all 0s' || !td || td === '0s') continue;
// Parse multi-value transition shorthand
const props = tp.split(',').map(p => p.trim());
const durs = td.split(',').map(d => d.trim());
const easings = s.transitionTimingFunction.split(',').map(e => e.trim());
const delays = s.transitionDelay.split(',').map(d => d.trim());
const transitions = props.map((p, i) => ({
property: p,
duration: durs[i % durs.length],
easing: easings[i % easings.length],
delay: delays[i % delays.length] !== '0s' ? delays[i % delays.length] : null
}));
// Skip elements where all durations are 0s
if (transitions.every(t => t.duration === '0s')) continue;
const sig = transitions.map(t => `${t.property}|${t.duration}|${t.easing}`).join(';');
patternMap[sig] = patternMap[sig] || { transitions: transitions, count: 0, examples: [] };
patternMap[sig].count++;
if (patternMap[sig].examples.length < 3) {
patternMap[sig].examples.push({
tag: el.tagName.toLowerCase(),
className: String(el.className || '').slice(0, 120),
path: cssPath(el)
});
}
if (elements.length < limit) {
elements.push({
tag: el.tagName.toLowerCase(),
className: String(el.className || '').slice(0, 120),
path: cssPath(el),
transitions,
inlineStyle: el.getAttribute('style')?.slice(0, 200) || null
});
}
}
// Sort patterns by count descending
const patterns = Object.values(patternMap).sort((a, b) => b.count - a.count);
return {
totalScanned: all.length,
totalWithTransitions: elements.length + Object.values(patternMap).reduce((s, p) => s + Math.max(0, p.count - 3), 0),
elements,
patterns
};
}
/**
* Extract all @keyframes definitions from stylesheets.
* Returns array of { name, frames: [{ keyText, cssText }] }.
* Handles CORS-blocked sheets gracefully.
*/
function keyframes() {
const result = [];
const seen = new Set();
for (const sheet of document.styleSheets) {
let rules;
try { rules = sheet.cssRules || sheet.rules; } catch { continue; }
if (!rules) continue;
for (const rule of rules) {
if (rule.type === CSSRule.KEYFRAMES_RULE && !seen.has(rule.name)) {
seen.add(rule.name);
const frames = [];
for (let i = 0; i < rule.cssRules.length; i++) {
frames.push({
keyText: rule.cssRules[i].keyText,
cssText: rule.cssRules[i].style.cssText.slice(0, 400)
});
}
result.push({ name: rule.name, frameCount: frames.length, frames });
}
}
}
return result;
}
window.__seTransition = {
installed: true,
scan,
keyframes
};
})();
```
### scripts/interaction-diff.js
```javascript
// Style Extractor: Interaction diff (paste into evaluate_script)
//
// Records inline styles + computed motion properties before/after an interaction,
// and captures document.getAnimations() within the transition window.
//
// Exposes:
// - window.__seDiff.watch(selectors) — start watching elements
// - window.__seDiff.triggerAndCapture(fn, opts) — trigger interaction, capture diff
// - window.__seDiff.snapshot(label) — manual snapshot of watched elements
//
// Usage (in evaluate_script):
// 1. Paste this file
// 2. __seDiff.watch(['.my-btn', '.hero-title', '[class*="section"]'])
// 3. __seDiff.triggerAndCapture(() => document.querySelector('.nav-item').click())
//
// This file is intentionally framework-agnostic and safe to paste as an IIFE.
(() => {
if (window.__seDiff?.installed) return;
const MOTION_PROPS = ['opacity', 'transform', 'color', 'backgroundColor', 'borderColor',
'boxShadow', 'width', 'height', 'top', 'left', 'filter', 'clipPath'];
let watchedSelectors = [];
let watchedElements = [];
function cssPath(el) {
if (!el || el.nodeType !== 1) return null;
if (el.id) return `#${CSS.escape(el.id)}`;
const parts = [];
let cur = el;
let depth = 0;
while (cur && cur.nodeType === 1 && depth < 6) {
let part = cur.tagName.toLowerCase();
if (cur.classList && cur.classList.length) {
part += Array.from(cur.classList).slice(0, 2).map(c => `.${CSS.escape(c)}`).join('');
}
const parent = cur.parentElement;
if (parent) {
const same = Array.from(parent.children).filter(c => c.tagName === cur.tagName);
if (same.length > 1) part += `:nth-of-type(${same.indexOf(cur) + 1})`;
}
parts.unshift(part);
if (parent?.id) { parts.unshift(`#${CSS.escape(parent.id)}`); break; }
cur = parent;
depth++;
}
return parts.join(' > ');
}
function describeEl(el) {
return {
tag: el.tagName.toLowerCase(),
className: String(el.className || '').slice(0, 120),
path: cssPath(el),
text: (el.innerText || '').trim().slice(0, 40) || null
};
}
function captureState(el) {
const s = getComputedStyle(el);
const computed = {};
for (const p of MOTION_PROPS) computed[p] = s[p];
return {
inlineStyle: el.getAttribute('style')?.slice(0, 300) || null,
computed
};
}
function captureAnimations() {
const anims = document.getAnimations({ subtree: true });
return anims.map(a => {
const effect = a.effect;
const timing = effect?.getTiming?.() ?? null;
const target = (() => { try { return effect?.target ?? null; } catch { return null; } })();
const keyframes = (() => { try { return effect?.getKeyframes?.() ?? null; } catch { return null; } })();
const props = keyframes ? [...new Set(keyframes.flatMap(kf =>
Object.keys(kf).filter(k => !['offset', 'easing', 'composite', 'computedOffset'].includes(k))
))] : [];
return {
type: a.constructor?.name ?? null,
animationName: a.animationName ?? null,
playState: a.playState,
timing: timing ? {
duration: timing.duration,
delay: timing.delay,
easing: timing.easing,
fill: timing.fill,
iterations: timing.iterations
} : null,
properties: props,
target: target?.nodeType === 1 ? describeEl(target) : null
};
});
}
/**
* Register selectors to watch. Each selector can match multiple elements.
*/
function watch(selectors) {
watchedSelectors = Array.isArray(selectors) ? selectors : [selectors];
watchedElements = [];
for (const sel of watchedSelectors) {
const els = document.querySelectorAll(sel);
for (const el of els) watchedElements.push({ selector: sel, el });
}
return { watching: watchedElements.length, selectors: watchedSelectors };
}
/**
* Take a labeled snapshot of all watched elements.
*/
function snapshot(label) {
return {
label,
at: Date.now(),
elements: watchedElements.map(({ selector, el }) => ({
selector,
...describeEl(el),
...captureState(el)
})),
animations: captureAnimations()
};
}
/**
* Trigger an interaction and capture before/after diff.
*
* @param {Function} triggerFn — the interaction to trigger (e.g., () => el.click())
* @param {Object} opts
* @param {number} opts.captureAt — ms after trigger to capture (default 50)
* @param {number} opts.settleAt — ms to wait for settle snapshot (default 500)
*/
async function triggerAndCapture(triggerFn, opts) {
const captureAt = opts?.captureAt ?? 50;
const settleAt = opts?.settleAt ?? 500;
// Before
const before = snapshot('before');
// Trigger
triggerFn();
// During — capture within the transition window
await new Promise(r => setTimeout(r, captureAt));
const during = snapshot('during');
// After — wait for transitions to settle
await new Promise(r => setTimeout(r, settleAt - captureAt));
const after = snapshot('after');
// Compute diffs
const diffs = watchedElements.map(({ selector, el }, i) => {
const b = before.elements[i]?.computed || {};
const a = after.elements[i]?.computed || {};
const changes = {};
for (const p of MOTION_PROPS) {
if (b[p] !== a[p]) changes[p] = { from: b[p], to: a[p] };
}
const inlineChanged = before.elements[i]?.inlineStyle !== after.elements[i]?.inlineStyle;
return {
selector,
...describeEl(el),
inlineChanged,
inlineBefore: before.elements[i]?.inlineStyle,
inlineAfter: after.elements[i]?.inlineStyle,
computedChanges: changes,
hasChanges: Object.keys(changes).length > 0 || inlineChanged
};
}).filter(d => d.hasChanges);
return {
trigger: triggerFn.toString().slice(0, 200),
timing: { captureAt, settleAt },
diffs,
duringAnimations: during.animations,
afterAnimations: after.animations
};
}
window.__seDiff = {
installed: true,
watch,
snapshot,
triggerAndCapture
};
})();
```
### scripts/library-detect.js
```javascript
// Style Extractor: library + fingerprint detection (paste into evaluate_script)
//
// Detects animation libraries and extracts configs when possible.
// Detection strategies: global objects → DOM instances → DOM fingerprints → asset hints.
//
// Returns { globals, instances, dom, fingerprints, assets }.
(() => {
const globals = {
Swiper: typeof window.Swiper !== 'undefined',
gsap: typeof window.gsap !== 'undefined',
ScrollTrigger: typeof window.ScrollTrigger !== 'undefined',
anime: typeof window.anime !== 'undefined',
THREE: typeof window.THREE !== 'undefined',
lottie: typeof window.lottie !== 'undefined',
AOS: typeof window.AOS !== 'undefined'
};
// ── DOM instance extraction (works even when globals are bundled away) ──
const instances = {};
// Swiper: read config from el.swiper
const swiperEls = document.querySelectorAll('.swiper');
const swiperInstances = [];
for (const el of swiperEls) {
const inst = el.swiper;
if (inst) {
swiperInstances.push({
container: String(el.className).slice(0, 120),
slides: inst.slides?.length ?? null,
params: {
slidesPerView: inst.params?.slidesPerView,
spaceBetween: inst.params?.spaceBetween,
speed: inst.params?.speed,
loop: inst.params?.loop,
effect: inst.params?.effect,
direction: inst.params?.direction,
autoplay: inst.params?.autoplay ? {
delay: inst.params.autoplay.delay,
disableOnInteraction: inst.params.autoplay.disableOnInteraction
} : false,
freeMode: inst.params?.freeMode?.enabled ?? inst.params?.freeMode ?? false,
pagination: !!inst.params?.pagination?.el,
navigation: !!(inst.params?.navigation?.nextEl || inst.params?.navigation?.prevEl)
}
});
}
}
if (swiperInstances.length) instances.swiper = swiperInstances;
// AOS: read config from data-aos-* attributes
const aosEls = document.querySelectorAll('[data-aos]');
if (aosEls.length) {
instances.aos = [...aosEls].slice(0, 20).map(el => ({
element: el.tagName.toLowerCase() + (el.className ? '.' + String(el.className).split(' ')[0].slice(0, 40) : ''),
animation: el.dataset.aos,
duration: el.dataset.aosDuration || null,
delay: el.dataset.aosDelay || null,
easing: el.dataset.aosEasing || null,
once: el.dataset.aosOnce || null,
offset: el.dataset.aosOffset || null
}));
}
// Framer Motion: read data-framer-* attributes
const framerEls = document.querySelectorAll('[data-framer-appear-id], [data-framer-component-type]');
if (framerEls.length) {
instances.framerMotion = {
count: framerEls.length,
examples: [...framerEls].slice(0, 5).map(el => ({
tag: el.tagName.toLowerCase(),
className: String(el.className || '').slice(0, 80),
appearId: el.dataset.framerAppearId || null,
componentType: el.dataset.framerComponentType || null
}))
};
}
// ── DOM fingerprints ──
const dom = {
swiper: swiperEls.length,
swiperWithInstance: swiperInstances.length,
aos: aosEls.length,
framer: framerEls.length,
video: document.querySelectorAll('video').length,
canvas: document.querySelectorAll('canvas').length,
svg: document.querySelectorAll('svg').length,
lottiePlayer: document.querySelectorAll('lottie-player, dotlottie-player').length
};
// ── Asset + CSS variable hints ──
const scriptUrls = Array.from(document.scripts).map(s => s.src).filter(Boolean);
const styleUrls = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).map(l => l.href).filter(Boolean);
const allUrls = scriptUrls.concat(styleUrls);
function hasKeyword(urls, kw) {
const k = kw.toLowerCase();
return urls.some(u => String(u).toLowerCase().includes(k));
}
const fingerprints = {
hasSwiperThemeVar: (() => {
try {
const v = getComputedStyle(document.documentElement).getPropertyValue('--swiper-theme-color');
return Boolean(v && v.trim());
} catch { return false; }
})(),
assetHints: {
swiper: hasKeyword(allUrls, 'swiper'),
gsap: hasKeyword(scriptUrls, 'gsap'),
lottie: hasKeyword(scriptUrls, 'lottie'),
three: hasKeyword(scriptUrls, 'three'),
anime: hasKeyword(scriptUrls, 'anime'),
framer: hasKeyword(scriptUrls, 'framer'),
aos: hasKeyword(allUrls, 'aos')
}
};
const assets = {
scripts: scriptUrls,
stylesheets: styleUrls
};
return { globals, instances, dom, fingerprints, assets };
})();
```
### scripts/motion-tools.js
```javascript
// Style Extractor: Motion tools (paste into evaluate_script when extracting dynamic UIs)
//
// Exposes:
// - window.__seMotion.install(): installs helpers (idempotent)
// - window.__seMotion.capture(label): captures document.getAnimations() snapshot
// - window.__seMotion.sample(el, opts): samples computed styles per rAF for JS-driven motion
//
// This file is intentionally framework-agnostic and safe to paste as an IIFE.
(() => {
if (window.__seMotion?.installed) return;
function cssPath(el) {
if (!el || el.nodeType !== 1) return null;
if (el.id) return `#${CSS.escape(el.id)}`;
const parts = [];
let cur = el;
let depth = 0;
while (cur && cur.nodeType === 1 && depth < 6) {
let part = cur.tagName.toLowerCase();
if (cur.classList && cur.classList.length) {
part += Array.from(cur.classList).slice(0, 2).map(c => `.${CSS.escape(c)}`).join('');
}
const parent = cur.parentElement;
if (parent) {
const same = Array.from(parent.children).filter(c => c.tagName === cur.tagName);
if (same.length > 1) part += `:nth-of-type(${same.indexOf(cur) + 1})`;
}
parts.unshift(part);
if (parent?.id) {
parts.unshift(`#${CSS.escape(parent.id)}`);
break;
}
cur = parent;
depth++;
}
return parts.join(' > ');
}
function summarizeKeyProps(kfs) {
if (!Array.isArray(kfs) || kfs.length === 0) return null;
const omit = new Set(['offset', 'easing', 'composite', 'computedOffset']);
const props = new Set();
for (const kf of kfs) for (const p of Object.keys(kf)) if (!omit.has(p)) props.add(p);
const out = {};
for (const p of props) {
let first = null;
let last = null;
let firstOffset = null;
let lastOffset = null;
for (const kf of kfs) {
if (kf[p] == null) continue;
if (first == null) {
first = kf[p];
firstOffset = kf.offset ?? kf.computedOffset ?? null;
}
last = kf[p];
lastOffset = kf.offset ?? kf.computedOffset ?? null;
}
out[p] = { from: first, to: last, fromOffset: firstOffset, toOffset: lastOffset };
}
return out;
}
function describeTarget(el) {
if (!el || el.nodeType !== 1) return null;
const s = getComputedStyle(el);
const r = el.getBoundingClientRect();
return {
tag: el.tagName.toLowerCase(),
id: el.id || null,
className: el.className ? String(el.className).slice(0, 200) : null,
text: (el.innerText || el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 80) || null,
path: cssPath(el),
rect: { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) },
computed: {
opacity: s.opacity,
transform: s.transform,
filter: s.filter,
clipPath: s.clipPath,
willChange: s.willChange
}
};
}
function capture(label) {
const anims = document.getAnimations({ subtree: true });
return {
label,
at: Date.now(),
url: location.href,
scrollY: Math.round(scrollY),
animationCount: anims.length,
animations: anims.map(a => {
const effect = a.effect;
const timing = effect?.getTiming?.() ?? null;
const target = (() => { try { return effect?.target ?? null; } catch { return null; } })();
const keyframes = (() => { try { return effect?.getKeyframes?.() ?? null; } catch { return null; } })();
return {
type: a.constructor?.name ?? null,
playState: a.playState,
currentTime: a.currentTime ?? null,
animationName: a.animationName ?? null,
timing,
keyProps: summarizeKeyProps(keyframes),
target: describeTarget(target)
};
})
};
}
async function sample(el, opts) {
const target = typeof el === 'string' ? document.querySelector(el) : el;
if (!target) return { ok: false, reason: 'target not found' };
const ms = Math.max(100, opts?.durationMs ?? 800);
const include = opts?.include ?? ['transform', 'opacity'];
const out = [];
const start = performance.now();
await new Promise(resolve => {
function step() {
const t = performance.now();
const s = getComputedStyle(target);
const row = { t: Math.round(t - start) };
for (const k of include) row[k] = s[k];
out.push(row);
if (t - start >= ms) return resolve();
requestAnimationFrame(step);
}
requestAnimationFrame(step);
});
return {
ok: true,
target: describeTarget(target),
durationMs: ms,
samples: out
};
}
window.__seMotion = {
installed: true,
install: () => true,
capture,
sample
};
})();
```
### scripts/extract-keyframes.py
```python
"""
Style Extractor: extract @keyframes blocks from downloaded CSS files.
Usage:
python scripts/extract-keyframes.py <folder> [--out out.md] [--limit 50]
Notes:
- This is a helper for evidence gathering when you can pull stylesheet bodies
(e.g., via Chrome MCP get_network_request) and want complete keyframes.
"""
from __future__ import annotations
import argparse
import glob
import os
import re
from dataclasses import dataclass
@dataclass(frozen=True)
class KeyframesBlock:
name: str
file: str
css: str
def read_text(path: str) -> str:
with open(path, "r", encoding="utf-8", errors="ignore") as f:
return f.read()
def extract_keyframes_blocks(css_text: str, source_file: str) -> list[KeyframesBlock]:
blocks: list[KeyframesBlock] = []
for match in re.finditer(r"@keyframes\s+([^\s{]+)\s*\{", css_text):
name = match.group(1)
i = match.end() - 1
depth = 0
while i < len(css_text):
c = css_text[i]
if c == "{":
depth += 1
elif c == "}":
depth -= 1
if depth == 0:
end = i + 1
blocks.append(KeyframesBlock(name=name, file=source_file, css=css_text[match.start() : end]))
break
i += 1
return blocks
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("folder", help="Folder containing .css files")
ap.add_argument("--out", default="", help="Write markdown output to file")
ap.add_argument("--limit", type=int, default=200, help="Max keyframes blocks to output")
args = ap.parse_args()
css_files = sorted(glob.glob(os.path.join(args.folder, "*.css")))
all_blocks: list[KeyframesBlock] = []
for path in css_files:
text = read_text(path)
all_blocks.extend(extract_keyframes_blocks(text, os.path.basename(path)))
# Deduplicate by name keeping first appearance (minified CSS often repeats)
dedup: dict[str, KeyframesBlock] = {}
for b in all_blocks:
dedup.setdefault(b.name, b)
names = sorted(dedup.keys())
names = names[: max(0, args.limit)]
out_lines: list[str] = []
out_lines.append(f"# Keyframes Extraction\n")
out_lines.append(f"- folder: `{os.path.abspath(args.folder)}`")
out_lines.append(f"- keyframes (unique): {len(dedup)}")
out_lines.append("")
for name in names:
b = dedup[name]
out_lines.append(f"## `{name}`")
out_lines.append(f"- source: `{b.file}`")
out_lines.append("")
out_lines.append("```css")
out_lines.append(b.css)
out_lines.append("```")
out_lines.append("")
output = "\n".join(out_lines)
if args.out:
with open(args.out, "w", encoding="utf-8") as f:
f.write(output)
else:
print(output)
return 0
if __name__ == "__main__":
raise SystemExit(main())
```