design-system
This skill extracts a complete design system from a live website using browser automation. It navigates the rendered DOM, captures every visual design decision, and outputs ready-to-use design tokens that plug directly into the frontend-design skill.
The user provides: the URL of the website to extract from, and optionally the output directory (default: design-system/).
Why Extract a Design System
Manually inspecting DevTools to extract colors, fonts, spacing, and other design tokens is slow and error-prone. When rebuilding a site or building a demo that must match a client's existing brand, you need every visual decision captured accurately:
- Colors — not just the primary brand color, but every neutral, accent, and semantic color in use
- Typography — exact font families, sizes for each heading level, weights, line heights
- Spacing — the rhythm of padding and gaps that makes a design feel cohesive
- Components — button styles, card radii, shadow depths that define the UI language
This skill automates the full extraction in minutes instead of hours.
What Gets Extracted
| Category | Details |
|---|---|
| Logo | SVG/PNG from header, footer, favicon — downloaded to assets/ |
| Colors | CSS custom properties + computed background/text/border/fill colors, classified into primary/secondary/accent/neutral/semantic roles |
| Typography | Font families (+ Google Fonts/font-face source URLs), sizes for h1-h6/body/small, weights, line heights, letter spacing |
| Border radius | All unique values from buttons, cards, inputs, containers |
| Shadows | box-shadow values from cards, dropdowns, modals, buttons |
| Spacing | Common padding, gap, and margin patterns |
| Buttons | Full button style specs — colors, radius, padding, font, hover states, variants |
| Icons | Library detection (Font Awesome, Material, Heroicons, Lucide, Phosphor, etc.) |
| Layout | Max-width, container padding, grid/flex usage, breakpoints |
| Dark mode | Detect and extract dark theme overrides if present |
Extraction Process
Step 1: Navigate and Prepare
1. Navigate to the target URL with agent-browser (fallback: Playwright MCP browser_navigate)
2. Wait for full page load (networkidle)
3. Dismiss cookie consent banners — look for "Reject all", "Accept", or close buttons
4. Scroll the full page to trigger lazy-loaded content and fonts
5. Wait 2 seconds for web fonts to finish loading
Step 2: Extract CSS Custom Properties
Extract all CSS custom properties from :root and theme stylesheets:
// Execute via agent-browser evaluate or Playwright browser_evaluate
(() => {
const vars = {};
// From inline styles on :root
const root = document.documentElement;
const rootStyles = getComputedStyle(root);
// From stylesheets
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule.selectorText === ':root' || rule.selectorText === ':root, :host') {
for (const prop of rule.style) {
if (prop.startsWith('--')) {
vars[prop] = rule.style.getPropertyValue(prop).trim();
}
}
}
}
} catch (e) { /* CORS stylesheet — skip */ }
}
return vars;
})()
This captures design tokens the site already defines (Tailwind, CSS-in-JS theme vars, custom property systems).
Step 3: Extract Colors from Computed Styles
Scan the rendered DOM to find every color actually in use:
(() => {
const colors = { background: new Set(), text: new Set(), border: new Set(), fill: new Set() };
const elements = document.querySelectorAll('*');
const sample = Array.from(elements).slice(0, 2000); // Cap for performance
for (const el of sample) {
const s = getComputedStyle(el);
const bg = s.backgroundColor;
const tc = s.color;
const bc = s.borderColor;
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') colors.background.add(bg);
if (tc) colors.text.add(tc);
if (bc && bc !== 'rgba(0, 0, 0, 0)' && bc !== bg) colors.border.add(bc);
// SVG fills
if (el.tagName === 'svg' || el.closest('svg')) {
const fill = s.fill;
if (fill && fill !== 'none' && fill !== 'rgba(0, 0, 0, 0)') colors.fill.add(fill);
}
}
return {
background: [...colors.background],
text: [...colors.text],
border: [...colors.border],
fill: [...colors.fill]
};
})()
Step 4: Extract Typography
Capture all font families, sizes, weights, and line heights:
(() => {
const typography = {
families: new Set(),
headings: {},
body: null,
small: null
};
// Heading styles
for (let i = 1; i <= 6; i++) {
const el = document.querySelector(`h${i}`);
if (el) {
const s = getComputedStyle(el);
typography.headings[`h${i}`] = {
fontFamily: s.fontFamily,
fontSize: s.fontSize,
fontWeight: s.fontWeight,
lineHeight: s.lineHeight,
letterSpacing: s.letterSpacing,
textTransform: s.textTransform
};
typography.families.add(s.fontFamily.split(',')[0].trim().replace(/['"]/g, ''));
}
}
// Body text
const body = document.querySelector('p');
if (body) {
const s = getComputedStyle(body);
typography.body = {
fontFamily: s.fontFamily,
fontSize: s.fontSize,
fontWeight: s.fontWeight,
lineHeight: s.lineHeight,
letterSpacing: s.letterSpacing
};
typography.families.add(s.fontFamily.split(',')[0].trim().replace(/['"]/g, ''));
}
// Small text
const small = document.querySelector('small, .text-sm, .text-xs, figcaption, .caption');
if (small) {
const s = getComputedStyle(small);
typography.small = {
fontFamily: s.fontFamily,
fontSize: s.fontSize,
fontWeight: s.fontWeight,
lineHeight: s.lineHeight
};
}
// Detect font sources
const fontLinks = [];
document.querySelectorAll('link[href*="fonts.googleapis.com"], link[href*="fonts.bunny.net"]').forEach(link => {
fontLinks.push(link.href);
});
// Check @font-face rules
const fontFaces = [];
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule instanceof CSSFontFaceRule) {
fontFaces.push({
family: rule.style.getPropertyValue('font-family').replace(/['"]/g, ''),
src: rule.style.getPropertyValue('src'),
weight: rule.style.getPropertyValue('font-weight'),
style: rule.style.getPropertyValue('font-style')
});
}
}
} catch (e) { /* CORS */ }
}
return {
families: [...typography.families],
headings: typography.headings,
body: typography.body,
small: typography.small,
fontSources: fontLinks,
fontFaces
};
})()
Step 5: Extract Border Radius, Spacing, and Shadows
(() => {
const radii = new Set();
const shadows = new Set();
const paddings = new Set();
const gaps = new Set();
const interactive = document.querySelectorAll(
'button, a, input, select, textarea, [class*="card"], [class*="Card"], ' +
'[class*="btn"], [class*="Btn"], [class*="modal"], [class*="Modal"], ' +
'[class*="dropdown"], [class*="Dropdown"], [role="dialog"], [role="button"]'
);
for (const el of interactive) {
const s = getComputedStyle(el);
const r = s.borderRadius;
if (r && r !== '0px') radii.add(r);
const shadow = s.boxShadow;
if (shadow && shadow !== 'none') shadows.add(shadow);
}
// Spacing from containers and sections
const containers = document.querySelectorAll(
'section, main, article, [class*="container"], [class*="wrapper"], ' +
'[class*="Container"], [class*="Wrapper"], header, footer, nav'
);
for (const el of containers) {
const s = getComputedStyle(el);
const p = s.padding;
if (p && p !== '0px') paddings.add(p);
const g = s.gap;
if (g && g !== 'normal' && g !== '0px') gaps.add(g);
}
return {
borderRadius: [...radii].sort(),
boxShadows: [...shadows],
paddings: [...paddings],
gaps: [...gaps]
};
})()
Step 6: Extract Button Styles and Component Patterns
(() => {
const buttons = [];
const btnElements = document.querySelectorAll(
'button, a[class*="btn"], a[class*="Btn"], a[class*="button"], a[class*="Button"], ' +
'[role="button"], input[type="submit"]'
);
const seen = new Set();
for (const el of btnElements) {
const s = getComputedStyle(el);
const key = `${s.backgroundColor}|${s.color}|${s.borderRadius}|${s.fontSize}|${s.fontWeight}|${s.padding}`;
if (seen.has(key)) continue;
seen.add(key);
buttons.push({
text: el.textContent?.trim().substring(0, 50),
backgroundColor: s.backgroundColor,
color: s.color,
borderRadius: s.borderRadius,
border: s.border,
padding: s.padding,
fontSize: s.fontSize,
fontWeight: s.fontWeight,
fontFamily: s.fontFamily,
textTransform: s.textTransform,
letterSpacing: s.letterSpacing,
boxShadow: s.boxShadow,
cursor: s.cursor
});
}
return buttons;
})()
Step 7: Extract Logos and Detect Icon Libraries
(() => {
const logos = [];
// Header/footer logos
const logoSelectors = [
'header img[class*="logo"], header img[alt*="logo"], header svg[class*="logo"]',
'footer img[class*="logo"], footer img[alt*="logo"], footer svg[class*="logo"]',
'a[class*="logo"] img, a[class*="Logo"] img',
'a[class*="logo"] svg, a[class*="Logo"] svg',
'[class*="brand"] img, [class*="Brand"] img',
'header a:first-child img, header a:first-child svg'
];
for (const selector of logoSelectors) {
document.querySelectorAll(selector).forEach(el => {
if (el.tagName === 'IMG') {
logos.push({ type: 'img', src: el.src, alt: el.alt, width: el.naturalWidth, height: el.naturalHeight });
} else if (el.tagName === 'svg' || el.tagName === 'SVG') {
logos.push({ type: 'svg', html: el.outerHTML.substring(0, 5000) });
}
});
}
// Favicon
const favicon = document.querySelector('link[rel="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"]');
if (favicon) logos.push({ type: 'favicon', href: favicon.href });
// Icon library detection
const iconLibraries = [];
const checks = [
{ name: 'Font Awesome', test: () => !!document.querySelector('[class*="fa-"], [class*="fas "], [class*="far "], [class*="fab "]') },
{ name: 'Material Icons', test: () => !!document.querySelector('.material-icons, .material-symbols-outlined, [class*="mdi-"]') },
{ name: 'Heroicons', test: () => !!document.querySelector('[class*="heroicon"], [data-slot="icon"]') },
{ name: 'Lucide', test: () => !!document.querySelector('[class*="lucide-"], .lucide') },
{ name: 'Phosphor', test: () => !!document.querySelector('[class*="ph-"]') },
{ name: 'Bootstrap Icons', test: () => !!document.querySelector('[class*="bi-"]') },
{ name: 'Feather', test: () => !!document.querySelector('[class*="feather-"]') },
{ name: 'Tabler Icons', test: () => !!document.querySelector('[class*="tabler-"], [class*="ti-"]') },
{ name: 'Inline SVG icons', test: () => document.querySelectorAll('svg[viewBox]').length > 5 }
];
for (const check of checks) {
try { if (check.test()) iconLibraries.push(check.name); } catch (e) {}
}
return { logos, iconLibraries };
})()
Step 8: Extract Layout Tokens
(() => {
const layout = {};
// Max-width of main container
const containers = document.querySelectorAll(
'main, [class*="container"], [class*="Container"], [class*="wrapper"], [class*="Wrapper"]'
);
const maxWidths = new Set();
for (const el of containers) {
const s = getComputedStyle(el);
if (s.maxWidth && s.maxWidth !== 'none') maxWidths.add(s.maxWidth);
}
layout.maxWidths = [...maxWidths];
// Container padding
const mainEl = document.querySelector('main') || document.querySelector('[class*="container"]');
if (mainEl) {
const s = getComputedStyle(mainEl);
layout.containerPadding = { left: s.paddingLeft, right: s.paddingRight };
}
// Grid usage
const gridElements = [];
document.querySelectorAll('*').forEach(el => {
const s = getComputedStyle(el);
if (s.display === 'grid' || s.display === 'inline-grid') {
gridElements.push({
tag: el.tagName.toLowerCase(),
className: el.className?.toString().substring(0, 100),
gridTemplateColumns: s.gridTemplateColumns,
gap: s.gap
});
}
});
layout.grids = gridElements.slice(0, 10);
// Breakpoints from media queries
const breakpoints = new Set();
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule instanceof CSSMediaRule) {
const match = rule.conditionText?.match(/(\d+)px/g);
if (match) match.forEach(bp => breakpoints.add(bp));
}
}
} catch (e) { /* CORS */ }
}
layout.breakpoints = [...breakpoints].sort((a, b) => parseInt(a) - parseInt(b));
return layout;
})()
Step 9: Detect Dark Mode
(() => {
const darkMode = { detected: false, method: null, tokens: {} };
// Check for prefers-color-scheme media queries
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule instanceof CSSMediaRule && rule.conditionText?.includes('prefers-color-scheme: dark')) {
darkMode.detected = true;
darkMode.method = 'prefers-color-scheme';
// Extract dark mode custom properties
for (const innerRule of rule.cssRules) {
if (innerRule.selectorText === ':root' || innerRule.selectorText?.includes(':root')) {
for (const prop of innerRule.style) {
if (prop.startsWith('--')) {
darkMode.tokens[prop] = innerRule.style.getPropertyValue(prop).trim();
}
}
}
}
}
}
} catch (e) { /* CORS */ }
}
// Check for .dark or [data-theme="dark"] class-based toggle
if (!darkMode.detected) {
const hasDarkClass = !!document.querySelector('[class*="dark:"], .dark');
const hasDarkData = !!document.querySelector('[data-theme="dark"], [data-mode="dark"]');
if (hasDarkClass || hasDarkData) {
darkMode.detected = true;
darkMode.method = 'class-based';
}
}
// Check for a dark mode toggle button
const toggleSelectors = [
'[class*="theme-toggle"], [class*="dark-mode"], [class*="darkMode"]',
'[aria-label*="dark"], [aria-label*="theme"], [aria-label*="mode"]',
'button[class*="moon"], button[class*="sun"]'
];
for (const sel of toggleSelectors) {
if (document.querySelector(sel)) {
darkMode.toggleFound = true;
break;
}
}
return darkMode;
})()
Step 10: Take Visual Reference Screenshots
Capture key sections for visual reference:
1. Full-page screenshot → screenshots/full-page.png
2. Navigation/header at desktop width → screenshots/navigation.png
3. Hero section (first screen) → screenshots/hero.png
4. A card or grid component (if present) → screenshots/cards.png
5. Buttons (find a section with CTAs) → screenshots/buttons.png
6. Footer → screenshots/footer.png
Use agent-browser screenshot or Playwright browser_take_screenshot with element selectors.
Step 11: Validate on Inner Pages
Visit 2-3 inner pages (e.g., an about page, a detail page, a blog post) and re-run Steps 3-6 to confirm design consistency. Note any deviations — some sites use different color schemes or typography on certain page types.
For each inner page:
1. Navigate to the page
2. Re-extract colors, typography, and button styles
3. Compare with homepage extraction
4. Note differences in the output (e.g., "Blog pages use a narrower max-width")
Color Classification Strategy
Raw computed colors need to be classified into semantic roles for the design tokens. Use this strategy:
Automatic Classification Rules
| Role | Detection method |
|---|---|
| Background | Most common background-color on body, main, large sections |
| Foreground | Most common color on body, p, span (the main text color) |
| Primary | Color used on primary buttons, main CTAs, active nav items, links |
| Secondary | Second most prominent brand color — secondary buttons, badges, accents |
| Accent | Highlight color for hover states, focus rings, decorative elements |
| Muted | Low-contrast text (captions, placeholders, disabled states), subtle borders |
| Muted foreground | Text color used on muted backgrounds |
| Card | Background color of card components (often slightly off-white or a tinted neutral) |
| Border | Most common border-color on inputs, cards, dividers |
| Destructive | Red/error color — form errors, delete buttons, warning alerts |
| Success | Green — success messages, checkmarks, positive indicators |
| Warning | Yellow/amber — warning alerts, pending states |
Classification Process
- Convert all collected colors to HSL for comparison
- Group similar colors (within ΔE < 5) to deduplicate near-identical values
- Rank by frequency of occurrence in the DOM
- Apply the detection rules above based on where each color appears
- If a role can't be auto-detected, leave it blank and note it in the output
Output File Specifications
The skill produces these files in the output directory:
design-system/
├── design-tokens.md # Human-readable design system documentation
├── tokens.css # CSS custom properties ready for globals.css
├── tailwind-theme.ts # Tailwind v4 @theme block + v3 config fallback
├── screenshots/ # Visual references
│ ├── full-page.png
│ ├── navigation.png
│ ├── hero.png
│ ├── cards.png
│ ├── buttons.png
│ └── footer.png
└── assets/ # Downloaded logos and favicons
├── logo.svg (or logo.png)
├── logo-footer.svg
└── favicon.ico
design-tokens.md
# Design System — {domain}
**Extracted**: {date}
**Source URL**: {url}
**Pages analyzed**: {count}
## Colors
### Brand
| Role | Value | Preview |
|------|-------|---------|
| Primary | #8e375c | 🟣 |
| Secondary | #2d5a7b | 🔵 |
| Accent | #d4a853 | 🟡 |
### Neutrals
| Role | Value | Preview |
|------|-------|---------|
| Background | #f8f7f5 | ⬜ |
| Foreground | #1c1917 | ⬛ |
| Muted | #a8a29e | 🔘 |
| Border | #e7e5e4 | ▫️ |
### Semantic
| Role | Value |
|------|-------|
| Destructive | #dc2626 |
| Success | #16a34a |
| Warning | #d97706 |
### Dark Mode
| Role | Light | Dark |
|------|-------|------|
| Background | #f8f7f5 | #1c1917 |
| Foreground | #1c1917 | #f5f5f4 |
| Primary | #8e375c | #c76b94 |
| Muted | #a8a29e | #78716c |
## Typography
### Font Families
| Role | Family | Source |
|------|--------|--------|
| Headings | Playfair Display | Google Fonts |
| Body | DM Sans | Google Fonts |
### Scale
| Element | Size | Weight | Line Height | Letter Spacing |
|---------|------|--------|-------------|----------------|
| h1 | 48px | 700 | 1.2 | -0.02em |
| h2 | 36px | 700 | 1.25 | -0.01em |
| h3 | 24px | 600 | 1.3 | 0 |
| h4 | 20px | 600 | 1.4 | 0 |
| h5 | 18px | 600 | 1.4 | 0 |
| h6 | 16px | 600 | 1.5 | 0 |
| Body | 16px | 400 | 1.6 | 0 |
| Small | 14px | 400 | 1.5 | 0 |
## Border Radius
| Usage | Value |
|-------|-------|
| Buttons | 8px |
| Cards | 12px |
| Inputs | 6px |
| Full round | 9999px |
## Shadows
| Usage | Value |
|-------|-------|
| Card | 0 1px 3px rgba(0,0,0,0.1) |
| Dropdown | 0 4px 12px rgba(0,0,0,0.15) |
| Modal | 0 8px 30px rgba(0,0,0,0.2) |
## Spacing
| Pattern | Value |
|---------|-------|
| Section padding | 80px (py-20) |
| Card padding | 24px |
| Container max-width | 1280px |
| Container padding | 16px mobile / 32px desktop |
| Grid gap | 24px |
## Buttons
| Variant | BG | Text | Radius | Padding | Font |
|---------|-----|------|--------|---------|------|
| Primary | #8e375c | #ffffff | 8px | 12px 24px | DM Sans 500 |
| Secondary | transparent | #8e375c | 8px | 12px 24px | DM Sans 500 |
| Ghost | transparent | #1c1917 | 8px | 12px 24px | DM Sans 500 |
## Icons
- Library: Lucide React
- Style: Outline, 24px default
## Layout
- Max width: 1280px
- Container padding: 16px (mobile) / 32px (desktop)
- Grid: CSS Grid, 3-column at desktop
- Breakpoints: 640px, 768px, 1024px, 1280px
tokens.css
The CSS variable names match the frontend-design skill's @theme convention so the output can be pasted directly into globals.css:
/* Design System — {domain}
* Extracted: {date}
* Source: {url}
*/
@import "tailwindcss";
@theme {
/* Colors */
--color-background: {background};
--color-foreground: {foreground};
--color-primary: {primary};
--color-primary-foreground: {primary-foreground};
--color-secondary: {secondary};
--color-secondary-foreground: {secondary-foreground};
--color-accent: {accent};
--color-accent-foreground: {accent-foreground};
--color-muted: {muted};
--color-muted-foreground: {muted-foreground};
--color-card: {card};
--color-card-foreground: {card-foreground};
--color-border: {border};
--color-destructive: {destructive};
/* Typography */
--font-heading: "{heading-font}", serif;
--font-body: "{body-font}", sans-serif;
/* Border Radius */
--radius-sm: {radius-sm};
--radius-md: {radius-md};
--radius-lg: {radius-lg};
--radius-full: 9999px;
/* Shadows */
--shadow-sm: {shadow-sm};
--shadow-md: {shadow-md};
--shadow-lg: {shadow-lg};
}
/* Dark mode overrides */
@media (prefers-color-scheme: dark) {
:root {
--color-background: {dark-background};
--color-foreground: {dark-foreground};
--color-primary: {dark-primary};
--color-muted: {dark-muted};
--color-muted-foreground: {dark-muted-foreground};
--color-card: {dark-card};
--color-border: {dark-border};
}
}
tailwind-theme.ts
/* Design System — {domain}
* Extracted: {date}
*
* Tailwind v4: Copy the @theme block from tokens.css into globals.css.
* Tailwind v3: Use this config object in tailwind.config.ts.
*/
// Tailwind v3 fallback config
import type { Config } from "tailwindcss";
export default {
theme: {
extend: {
colors: {
background: "{background}",
foreground: "{foreground}",
primary: {
DEFAULT: "{primary}",
foreground: "{primary-foreground}",
},
secondary: {
DEFAULT: "{secondary}",
foreground: "{secondary-foreground}",
},
accent: {
DEFAULT: "{accent}",
foreground: "{accent-foreground}",
},
muted: {
DEFAULT: "{muted}",
foreground: "{muted-foreground}",
},
card: {
DEFAULT: "{card}",
foreground: "{card-foreground}",
},
border: "{border}",
destructive: "{destructive}",
},
fontFamily: {
heading: ["{heading-font}", "serif"],
body: ["{body-font}", "sans-serif"],
},
borderRadius: {
sm: "{radius-sm}",
md: "{radius-md}",
lg: "{radius-lg}",
},
boxShadow: {
sm: "{shadow-sm}",
md: "{shadow-md}",
lg: "{shadow-lg}",
},
},
},
} satisfies Partial<Config>;
Agent Team Integration
This skill runs in Phase 1 of website-refactor, in parallel with website-analysis:
Phase 1: Extract + Plan
├── website-analysis → structure + content (pages, text, images)
├── design-system → visual tokens (colors, fonts, spacing, components)
└── Lead plans spec, asks user questions
Phase 2: Project Setup (Lead scaffolds, applies design tokens to globals.css)
Phase 3: Design (designer uses extracted tokens from design-system/)
Phase 4+5: Verify + Audit
The designer teammate receives the extracted tokens:
"Design tokens are in design-system/tokens.css — copy the @theme block
into globals.css. Reference design-system/design-tokens.md for the full
type scale, spacing, and button specs. Visual references are in
design-system/screenshots/."
Standalone Usage
- Client demos: "Extract their brand's design system so our rebuild matches exactly"
- Competitor analysis: "What design tokens does competitor.com use?"
- Brand audits: "Document the current design system before we propose changes"
- Design handoff: "Extract the live site's design system for the design team"
- Migration prep: "Capture all visual decisions before rebuilding on a new stack"
Troubleshooting
Cookie Consent Banners
Always dismiss cookie banners first — they overlay the page and can block element inspection. Look for "Reject all", "Accept all", or close (×) buttons. Some banners use iframes; check inside iframes if the main document has no banner buttons.
CORS-Restricted Stylesheets
External stylesheets (Google Fonts, CDN-hosted CSS) may throw CORS errors when reading cssRules. The extraction snippets catch these errors and skip those sheets. Font information is still captured from computed styles and <link> elements.
Lazy-Loaded Fonts
Some sites load fonts only when specific sections scroll into view. Scroll the full page before extracting typography to ensure all fonts are loaded. Check document.fonts.status === 'loaded' or wait for document.fonts.ready.
Single-Page Applications (SPAs)
SPAs render content client-side. Always use agent-browser (real browser) — never curl or fetch(). Wait for networkidle after navigation. For React/Vue/Angular sites, content may render after initial load; add a short delay.
CSS-in-JS (Styled Components, Emotion)
Sites using CSS-in-JS generate styles at runtime. The computed style extraction (Steps 3-6) captures these correctly since it reads getComputedStyle() from the rendered DOM, not from stylesheets. CSS custom properties defined in JS theme objects will also appear in the :root extraction.
Tailwind CSS Sites
Tailwind sites often have no CSS custom properties — all styles are utility classes. The computed style extraction still works because it reads final rendered values. Look for a tailwind.config or @theme block in the page source for additional token definitions.
Dark Mode Toggle
If the site has a class-based dark mode toggle (.dark class), click the toggle button and re-run the color extraction to capture dark mode values. Report both light and dark tokens. If using prefers-color-scheme, the media query extraction (Step 9) captures it automatically.
Sites Behind Authentication
If the site requires login, note it and ask the user to provide credentials or a session cookie. Some extractions can still be done on public pages (login page, marketing pages).
Very Large Sites
The DOM sampling in Step 3 caps at 2000 elements for performance. For very large pages, this captures the most important elements (they appear first in DOM order). Run the extraction on the homepage and 2-3 key inner pages rather than every page.