CtrlK
BlogDocsLog inGet started
Tessl Logo

pantheon-ai/website-theme-porter

Port the visual theme and styling from a live website to a React/Tailwind CSS project. Extracts colours, typography, spacing, and component styles — via agent-browser automation, manual inspection, curl/wget, or direct source reading — writes structured documentation and all artifacts under .context/artifacts/{website}/ with timestamps, applies findings as Tailwind v4 CSS tokens, then verifies by visually diffing the original site against the local or deployed version. Use when cloning a brand, replicating a design system, matching a reference site, migrating visual identity, copying a style guide, or porting a theme from any live URL into a React codebase.

95

1.44x
Quality

94%

Does it follow best practices?

Impact

98%

1.44x

Average score across 5 eval scenarios

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

tailwind-mapping.mdreferences/

CSS to Tailwind Token Mapping

Translate raw CSS computed values extracted from a live site into Tailwind CSS variables and utility classes.


Colours

rgb() / rgba() → HSL CSS variable

Tailwind v4 uses HSL for semantic tokens. Convert extracted rgb() values:

Extracted (rgb)HexHSLToken
rgb(13, 148, 136)#0D9488174 90% 31%--primary
rgb(255, 255, 255)#FFFFFF0 0% 100%--background
rgb(17, 24, 39)#111827221 39% 11%--foreground
rgb(107, 114, 128)#6B7280220 9% 46%--muted-foreground
rgb(229, 231, 235)#E5E7EB220 14% 96%--border

Quick HSL converter (run in browser):

function toHsl(rgb) {
  const [r, g, b] = rgb.match(/\d+/g).map(Number).map(n => n / 255);
  const max = Math.max(r, g, b), min = Math.min(r, g, b);
  let h, s, l = (max + min) / 2;
  if (max === min) { h = s = 0; }
  else {
    const d = max - min;
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    switch (max) {
      case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
      case g: h = ((b - r) / d + 2) / 6; break;
      case b: h = ((r - g) / d + 4) / 6; break;
    }
  }
  return `${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
}

Semantic token assignment

Map extracted colours to these standard tokens:

TokenDescriptionTypical source element
--backgroundPage backgroundbody bg
--foregroundDefault textbody color
--primaryMain brand / CTAPrimary button bg
--primary-foregroundText on primaryPrimary button text
--secondarySecondary actionsSecondary button bg
--secondary-foregroundText on secondarySecondary button text
--mutedSubtle backgroundsCard, sidebar bg
--muted-foregroundDe-emphasised textCaptions, placeholders
--accentHighlight/hoverLink hover, tag bg
--accent-foregroundText on accent
--borderDividers, outlinesborder-color of inputs
--ringFocus ring:focus outline
--destructiveErrors, deleteError text / button
--destructive-foregroundText on destructive
--cardCard surfaceCard component bg
--card-foregroundText on cardCard text color

Token Decision Tree

When a colour does not obviously map to a single token, walk this tree:

Is it the most prominent interactive colour (CTA button bg, main link)?
  YES → --primary
  NO ↓

Is it used on a second type of button or a less prominent interactive element?
  YES → --secondary
  NO ↓

Is it used for hover states, tag backgrounds, or inline highlights only?
  YES → --accent
  NO ↓

Is it a subtle background (sidebar, card surface, input field bg)?
  YES → --muted  (text on it → --muted-foreground)
  NO ↓

Is it used on borders, dividers, or form input outlines?
  YES → --border
  NO ↓

Is it the page-level background?
  YES → --background  (text on it → --foreground)
  NO → define a custom token (e.g. --surface, --panel, --hero-bg)

Tie-breaking rules:

  • --secondary vs --accent: prefer --accent for decorative hover/highlight effects; use --secondary when the element has an interactive affordance (click, focus, etc.).
  • :hover colours: getComputedStyle may silently return the resting state if the pointer is not physically over the element. Always verify against the base state.
  • Dark mode: if extraction was performed with the OS in dark mode, all background/foreground values will be dark-mode variants. Re-extract in light mode or document which mode was active in docs/theme/overview.md.
  • Colours appearing only in @media (prefers-color-scheme: dark) blocks belong in docs/theme/dark-mode.md, not the main token map.

Typography

Font family

Extracted valueTailwind tokenCSS variable
"Inter", sans-seriffont-sans--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif
"Playfair Display", seriffont-serif--font-serif: 'Playfair Display', ui-serif, Georgia, serif
"JetBrains Mono", monospacefont-mono--font-mono: 'JetBrains Mono', ui-monospace, monospace

Define in @theme inline:

@theme inline {
  --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
}

Font size scale

Map extracted px values to Tailwind size tokens:

pxremTailwind class
12px0.75remtext-xs
14px0.875remtext-sm
16px1remtext-base
18px1.125remtext-lg
20px1.25remtext-xl
24px1.5remtext-2xl
30px1.875remtext-3xl
36px2.25remtext-4xl
48px3remtext-5xl
60px3.75remtext-6xl

If the site uses a non-standard scale, define custom tokens:

@theme inline {
  --text-display: 3.5rem;
}

Font weight

ExtractedTailwind class
300font-light
400font-normal
500font-medium
600font-semibold
700font-bold
800font-extrabold
900font-black

Line height

ExtractedTailwind class
1leading-none
1.25leading-tight
1.375leading-snug
1.5leading-normal
1.625leading-relaxed
2leading-loose

Spacing

Map extracted px values to Tailwind spacing scale (1 unit = 0.25rem = 4px):

pxTailwindCommon use
4pxp-1Tight padding
8pxp-2Small padding
12pxp-3
16pxp-4Default padding
20pxp-5
24pxp-6Section padding
32pxp-8Card padding
40pxp-10
48pxp-12Section gap
64pxp-16Large section
80pxp-20Hero padding
96pxp-24Extra large
128pxp-32

For non-standard values, use CSS variables:

@theme inline {
  --spacing-section: 5rem; /* 80px */
}

Border Radius

ExtractedTailwind class
2pxrounded-sm
4pxrounded
6pxrounded-md
8pxrounded-lg
12pxrounded-xl
16pxrounded-2xl
9999pxrounded-full

Define a project-wide radius token:

:root {
  --radius: 0.5rem; /* matches the site's rounded-lg */
}
@theme inline {
  --radius-default: var(--radius);
}

Shadows

DescriptionTailwind class
Noneshadow-none
Subtle cardshadow-sm
Card / panelshadow or shadow-md
Elevated modalshadow-lg
Overlay / dropdownshadow-xl
Dramaticshadow-2xl

For custom shadows, extract the exact box-shadow value and define:

@theme inline {
  --shadow-card: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
}

Complete src/index.css Template

@import "tailwindcss";

/* ── Semantic tokens ── */
:root {
  /* Colours (fill from extraction) */
  --background:           hsl(0 0% 100%);
  --foreground:           hsl(221 39% 11%);
  --primary:              hsl(174 90% 31%);
  --primary-foreground:   hsl(0 0% 100%);
  --secondary:            hsl(210 40% 96%);
  --secondary-foreground: hsl(222 47% 11%);
  --muted:                hsl(210 40% 96%);
  --muted-foreground:     hsl(215 16% 47%);
  --accent:               hsl(210 40% 96%);
  --accent-foreground:    hsl(222 47% 11%);
  --destructive:          hsl(0 84% 60%);
  --destructive-foreground: hsl(0 0% 100%);
  --border:               hsl(214 32% 91%);
  --ring:                 hsl(174 90% 31%);
  --card:                 hsl(0 0% 100%);
  --card-foreground:      hsl(221 39% 11%);

  /* Shape */
  --radius: 0.5rem;
}

/* ── Map to Tailwind utilities ── */
@theme inline {
  --color-background:           var(--background);
  --color-foreground:           var(--foreground);
  --color-primary:              var(--primary);
  --color-primary-foreground:   var(--primary-foreground);
  --color-secondary:            var(--secondary);
  --color-secondary-foreground: var(--secondary-foreground);
  --color-muted:                var(--muted);
  --color-muted-foreground:     var(--muted-foreground);
  --color-accent:               var(--accent);
  --color-accent-foreground:    var(--accent-foreground);
  --color-destructive:          var(--destructive);
  --color-destructive-foreground: var(--destructive-foreground);
  --color-border:               var(--border);
  --color-ring:                 var(--ring);
  --color-card:                 var(--card);
  --color-card-foreground:      var(--card-foreground);

  /* Typography */
  --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
}

/* ── Base styles ── */
@layer base {
  *, *::before, *::after { box-sizing: border-box; }

  body {
    background-color: var(--background);
    color: var(--foreground);
    font-family: var(--font-sans);
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
  }
}

SKILL.md

tile.json