vibes.diy/docs Home Discord
creator docs · reference · generated

Custom look — wear the style the person names

This is the same document the Vibes code generator reads when it builds apps — rendered from prompts/pkg/llms/custom-look.md on every deploy, so it can't drift from what the platform actually does. Read it from your terminal any time with npx vibes-diy skills.

When the person names an outside style, brand or design system — "make it look like a Claude artifact", "use Material", "plain shadcn", "neo-brutalist like Gumroad", "terminal", "newspaper", "iOS settings", "our brand colours are #…" — the app wears THAT style, built from the recipes below.

How a custom look works

The app writes the style's palette onto the platform's theme variables in its own :root block. Every component then paints through those variables, so the owner's Style tab still restyles the whole app later: a Style pick overrides the same names.

The block lives in ONE <style> element, the first child of the app's root element, holding a module-level THEME_CSS string in this order:

  1. the Google Fonts @import (an @import is only read at the top of a stylesheet);
  2. :root { … } — the light palette and every structural token;
  3. @media (prefers-color-scheme: dark) { :root { … } } — the dark palette, for a style that has one. A style that is light-only or dark-only writes the one :root and sets color-scheme to match.
jsxconst THEME_CSS = `
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
:root {
  color-scheme: light;
  --background: #ffffff; --surface: #ffffff; --surface-overlay: #ffffff;
  --primary: #18181b; --on-primary: #fafafa;
  /* …the rest of the style's tokens… */
}
@media (prefers-color-scheme: dark) {
  :root { color-scheme: dark; --background: #09090b; /* …colours only… */ }
}
`;

export default function App() {
  return (
    <div className="min-h-screen bg-background text-foreground font-sans">
      <style>{THEME_CSS}</style>
      {/* the app */}
    </div>
  );
}

The variables

Write literal values (hex, rgb(), oklch()) for these names, exactly:

  • Colours (light, and again in the dark arm): --background (the page), --surface (cards, rows, fields), --surface-overlay (opaque panels that float: menus, dialogs), --primary, --secondary, --accent, --neutral (the quiet fill), --text-primary, --text-secondary, --border.
  • Inks — each fill names the text colour that sits on it, at 4.5:1 or better: --on-primary, --on-secondary, --on-accent.
  • Structure (once, in the light :root): --font-family, --font-family-mono, --font-size-base, --radius-sm, --radius, --radius-lg, --border-width, --vibes-spacing, --vibes-shadow-sm, --vibes-shadow, --vibes-shadow-lg, --letter-spacing, --letter-spacing-wide, --font-weight, --text-transform.
  • A display face, when the style pairs two families: --font-serif, read by the font-serif utility.

The platform's shadcn names (--foreground, --card, --card-foreground, --popover, --popover-foreground, --primary-foreground, --secondary-foreground, --muted, --muted-foreground, --accent-foreground, --destructive, --input, --ring) already point at the names above, so they follow the block on their own; the block writes the canonical names and leaves the aliases to the platform.

Painting with them

Plain Tailwind utilities read the variables:

Utility Reads
bg-background text-foreground --background, --text-primary
bg-card text-card-foreground / bg-popover text-popover-foreground --surface / --surface-overlay, --text-primary
text-muted-foreground, bg-muted --text-secondary, the quiet fill
bg-primary text-primary-foreground (and secondary, accent) the fill and its --on-* ink
border border-border --border-width, --border
rounded-sm / rounded-md / rounded-lg --radius-sm / --radius / --radius-lg
shadow-xs / shadow-md / shadow-lg --vibes-shadow-sm / --vibes-shadow / -lg
font-sans / font-mono / font-serif --font-family / --font-family-mono / --font-serif
tracking-tight / tracking-wide, font-bold --letter-spacing / -wide, --font-weight

font-sans goes once on the root element and every child inherits the face.

Every component is plain JSX with Tailwind utilities, written in the app itself. A custom-look app imports no look package (@vibes.diy/pouf, @vibes.diy/look): its buttons, cards, rows and fields are a few lines of JSX each, shaped by the recipe below. Each extracted component file paints with the same utilities.

Fonts load from Google Fonts with &display=swap. Name the families in --font-family (and --font-serif / --font-family-mono) with a system fallback after them, and load exactly those families in the @import.

The look is inspired by its source; the words on screen are the app's own. Headings, the title, empty states and buttons speak about the app (a reading list, a habit, a gig), so another company's name, logo and marks stay out of the UI. "Inspired by" lives in the code comments, if anywhere.

Any other named style ("like Linear", "Bauhaus", "Y2K") gets the same treatment: start from the nearest recipe below, then set the colours, faces, radius and shadows the style is known for.

Claude-artifact-inspired

Asked as: "like a Claude artifact", "Claude style", "Anthropic-y", "warm and calm". Warm neutral ground, one terracotta accent, generous whitespace, quiet borders, a serif display face over a clean sans body.

css@import url('https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,[email protected],400;8..60,600&family=Inter:wght@400;500;600&display=swap');
:root {
  color-scheme: light;
  --background: #f5f3ec; --surface: #fdfcf8; --surface-overlay: #fdfcf8;
  --primary: #b4532f; --on-primary: #ffffff;
  --secondary: #ebe7dc; --on-secondary: #3b382f;
  --accent: #f3e2d6; --on-accent: #7a3417; --neutral: #ebe7dc;
  --text-primary: #2b2a25; --text-secondary: #6b675b; --border: #e2ded2;
  --font-family: 'Inter', system-ui, sans-serif; --font-serif: 'Source Serif 4', Georgia, serif;
  --font-family-mono: ui-monospace, Menlo, monospace; --font-size-base: 1rem;
  --radius-sm: 6px; --radius: 10px; --radius-lg: 14px; --border-width: 1px; --vibes-spacing: 1rem;
  --vibes-shadow-sm: 0 1px 1px rgb(60 50 30 / 0.04); --vibes-shadow: 0 1px 3px rgb(60 50 30 / 0.06);
  --vibes-shadow-lg: 0 8px 24px rgb(60 50 30 / 0.08);
  --letter-spacing: -0.01em; --letter-spacing-wide: 0.04em; --font-weight: 600; --text-transform: none;
}
@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;
    --background: #262522; --surface: #2f2e2a; --surface-overlay: #34332f;
    --primary: #d97757; --on-primary: #1f1e1b; --secondary: #3a3833; --on-secondary: #ece9e0;
    --accent: #4a3328; --on-accent: #f5c9b3; --neutral: #3a3833;
    --text-primary: #f0eee6; --text-secondary: #b1ada1; --border: #3e3c36;
  }
}
  • Type: headings font-serif at 600 (text-3xl page title, text-xl section), body Inter 400 at text-base with leading-relaxed, labels 500.
  • Shape: soft 10px corners, 1px quiet borders, near-invisible shadows, roomy padding (p-6, gap-6), a narrow reading column (max-w-2xl mx-auto).
jsxfunction ReadingCard({ book, onFinish }) {
  return (
    <article className="bg-card text-card-foreground border border-border rounded-md p-6 flex flex-col gap-3">
      <h3 className="font-serif text-xl">{book.title}</h3>
      <p className="text-muted-foreground leading-relaxed">{book.note}</p>
      <div className="flex items-center gap-3 pt-2">
        <button type="button" className="bg-primary text-primary-foreground rounded-md px-4 py-2 font-medium" onClick={onFinish}>
          Mark as read
        </button>
        <span className="bg-accent text-accent-foreground rounded-sm px-2 py-1 text-sm">{book.author}</span>
      </div>
    </article>
  );
}

shadcn/ui plain

Asked as: "plain shadcn", "shadcn default", "like a Vercel app", "clean zinc". Neutral zinc, near-black primary, crisp 1px borders, compact controls.

css@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap');
:root {
  color-scheme: light;
  --background: #ffffff; --surface: #ffffff; --surface-overlay: #ffffff;
  --primary: #18181b; --on-primary: #fafafa;
  --secondary: #f4f4f5; --on-secondary: #18181b;
  --accent: #f4f4f5; --on-accent: #18181b; --neutral: #f4f4f5;
  --text-primary: #09090b; --text-secondary: #71717a; --border: #e4e4e7;
  --font-family: 'Geist', system-ui, sans-serif; --font-family-mono: 'Geist Mono', ui-monospace, monospace;
  --font-size-base: 0.9375rem;
  --radius-sm: 4px; --radius: 6px; --radius-lg: 8px; --border-width: 1px; --vibes-spacing: 1rem;
  --vibes-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.05); --vibes-shadow: 0 1px 3px rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
  --vibes-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
  --letter-spacing: -0.015em; --letter-spacing-wide: 0.02em; --font-weight: 600; --text-transform: none;
}
@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;
    --background: #09090b; --surface: #111113; --surface-overlay: #18181b;
    --primary: #fafafa; --on-primary: #18181b; --secondary: #27272a; --on-secondary: #fafafa;
    --accent: #27272a; --on-accent: #fafafa; --neutral: #27272a;
    --text-primary: #fafafa; --text-secondary: #a1a1aa; --border: #27272a;
  }
}
  • Type: Geist 400 body at 15px, 600 titles with tracking-tight (text-2xl page, text-base card), text-sm controls, Geist Mono for numbers.
  • Shape: 6px corners, 1px borders on every card and field, shadow-xs on cards, h-9 buttons, tight rhythm (gap-4, p-4).
jsxfunction SettingCard({ title, hint, onSave }) {
  return (
    <div className="bg-card text-card-foreground border border-border rounded-lg shadow-xs">
      <div className="p-4 flex flex-col gap-1">
        <h3 className="font-semibold tracking-tight">{title}</h3>
        <p className="text-sm text-muted-foreground">{hint}</p>
      </div>
      <div className="border-t border-border px-4 py-3 flex justify-end gap-2">
        <button type="button" className="h-9 px-4 rounded-md border border-border text-sm font-medium hover:bg-accent">Cancel</button>
        <button type="button" className="h-9 px-4 rounded-md bg-primary text-primary-foreground text-sm font-medium" onClick={onSave}>
          Save
        </button>
      </div>
    </div>
  );
}

Material 3

Asked as: "Material", "Material 3", "Material You", "like an Android app", "Google style". Tonal surfaces in a purple family, pill buttons (filled and tonal), elevation shown as a deeper tint, large rounded cards.

css@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap');
:root {
  color-scheme: light;
  --background: #fef7ff; --surface: #f3edf7; --surface-overlay: #ece6f0;
  --primary: #6750a4; --on-primary: #ffffff;
  --secondary: #e8def8; --on-secondary: #1d192b;
  --accent: #ffd8e4; --on-accent: #31111d; --neutral: #e7e0ec;
  --text-primary: #1d1b20; --text-secondary: #49454f; --border: #cac4d0;
  --font-family: 'Roboto', system-ui, sans-serif; --font-family-mono: ui-monospace, monospace; --font-size-base: 1rem;
  --radius-sm: 8px; --radius: 12px; --radius-lg: 28px; --border-width: 1px; --vibes-spacing: 1rem;
  --vibes-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.3); --vibes-shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 1px 3px 1px rgb(0 0 0 / 0.15);
  --vibes-shadow-lg: 0 2px 3px rgb(0 0 0 / 0.3), 0 6px 10px 4px rgb(0 0 0 / 0.15);
  --letter-spacing: 0em; --letter-spacing-wide: 0.01em; --font-weight: 500; --text-transform: none;
}
@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;
    --background: #141218; --surface: #211f26; --surface-overlay: #2b2930;
    --primary: #d0bcff; --on-primary: #381e72; --secondary: #4a4458; --on-secondary: #e8def8;
    --accent: #633b48; --on-accent: #ffd8e4; --neutral: #36343b;
    --text-primary: #e6e0e9; --text-secondary: #cac4d0; --border: #49454f;
  }
}
  • Type: Roboto; titles 400 at text-2xl, list headlines 500 at text-base, supporting text 400 text-sm text-muted-foreground, button labels 500 text-sm.
  • Shape: cards on bg-card with no border and rounded-lg (28px) for sheets or rounded-md (12px) for list cards; buttons rounded-full h-10 px-6; elevation is the tonal step (bg-background → bg-card → bg-popover) and shadow-xs only on a floating action.
jsxfunction HabitCard({ habit, onCheck }) {
  return (
    <div className="bg-card text-card-foreground rounded-md p-4 flex items-center gap-4">
      <div className="h-10 w-10 rounded-full bg-secondary text-secondary-foreground grid place-items-center font-medium">
        {habit.streak}
      </div>
      <div className="flex-1">
        <p className="font-medium">{habit.name}</p>
        <p className="text-sm text-muted-foreground">{habit.streak} day streak</p>
      </div>
      <button type="button" className="h-10 px-6 rounded-full bg-secondary text-secondary-foreground text-sm font-medium">Skip</button>
      <button type="button" className="h-10 px-6 rounded-full bg-primary text-primary-foreground text-sm font-medium" onClick={onCheck}>
        Done
      </button>
    </div>
  );
}

Neo-brutalist (Gumroad-like)

Asked as: "neo-brutalist", "neo-brutalism", "like Gumroad", "chunky bold borders and hard shadows". A colour word alone ("make it brutalist") is a colourway on the platform's own look, not this recipe. Thick black borders, hard offset shadows, flat high-chroma fills on a cream ground. Light-only.

css@import url('https://fonts.googleapis.com/css2?family=Archivo:wght@500;700;900&display=swap');
:root {
  color-scheme: light;
  --background: #f4f4f0; --surface: #ffffff; --surface-overlay: #ffffff;
  --primary: #ff90e8; --on-primary: #000000;
  --secondary: #ffc900; --on-secondary: #000000;
  --accent: #23a094; --on-accent: #000000; --neutral: #e8e8e2;
  --text-primary: #000000; --text-secondary: #3d3d3d; --border: #000000;
  --font-family: 'Archivo', system-ui, sans-serif; --font-family-mono: ui-monospace, monospace; --font-size-base: 1rem;
  --radius-sm: 2px; --radius: 4px; --radius-lg: 8px; --border-width: 2px; --vibes-spacing: 1rem;
  --vibes-shadow-sm: 2px 2px 0 var(--border); --vibes-shadow: 4px 4px 0 var(--border); --vibes-shadow-lg: 8px 8px 0 var(--border);
  --letter-spacing: -0.02em; --letter-spacing-wide: 0.08em; --font-weight: 900; --text-transform: uppercase;
}
  • Type: Archivo; headings 900 with tracking-tight at text-4xl, body 500, labels 700 text-xs tracking-wide [text-transform:var(--text-transform)].
  • Shape: every card and button carries border border-border (2px black) and shadow-md (hard 4px offset, no blur); fills are flat; a press slides the element into its shadow (active:translate-x-1 active:translate-y-1 active:shadow-none).
jsxfunction GigRow({ gig, onTickets }) {
  return (
    <div className="bg-card border border-border rounded-md shadow-md p-4 flex items-center gap-4">
      <div className="bg-secondary text-secondary-foreground border border-border rounded-sm px-3 py-2 text-center">
        <p className="text-xs font-bold tracking-wide [text-transform:var(--text-transform)]">{gig.month}</p>
        <p className="text-2xl font-bold">{gig.day}</p>
      </div>
      <div className="flex-1">
        <p className="text-xl font-bold tracking-tight">{gig.venue}</p>
        <p className="text-muted-foreground">{gig.city}</p>
      </div>
      <button
        type="button"
        className="bg-primary text-primary-foreground border border-border rounded-md shadow-md px-5 py-3 font-bold active:translate-x-1 active:translate-y-1 active:shadow-none"
        onClick={onTickets}
      >
        Tickets
      </button>
    </div>
  );
}

Terminal / mono

Asked as: "terminal", "hacker", "command line", "monospace", "green screen", "retro computer". Phosphor green on near-black, one amber highlight, square corners, everything mono. Dark-only.

css@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
:root {
  color-scheme: dark;
  --background: #0a0f0b; --surface: #101812; --surface-overlay: #141e17;
  --primary: #4ade80; --on-primary: #03140a;
  --secondary: #16251a; --on-secondary: #a7f3c0;
  --accent: #fbbf24; --on-accent: #1a1200; --neutral: #16251a;
  --text-primary: #c6f6d5; --text-secondary: #6ea883; --border: #1f3527;
  --font-family: 'JetBrains Mono', ui-monospace, monospace; --font-family-mono: 'JetBrains Mono', ui-monospace, monospace;
  --font-size-base: 0.9375rem;
  --radius-sm: 0px; --radius: 0px; --radius-lg: 0px; --border-width: 1px; --vibes-spacing: 1rem;
  --vibes-shadow-sm: 0 0 #0000; --vibes-shadow: 0 0 #0000; --vibes-shadow-lg: 0 0 0 1px var(--border);
  --letter-spacing: 0em; --letter-spacing-wide: 0.1em; --font-weight: 700; --text-transform: uppercase;
}
  • Type: JetBrains Mono everywhere; one size step for headings (text-lg font-bold), labels text-xs tracking-wide in caps, a > or $ prompt glyph before headings.
  • Shape: square corners, 1px rules, no shadows, dense rows (py-2), status shown as bracketed words ([ OK ], [WARN]) in the fill colours.
jsxfunction ServiceLine({ service, onRestart }) {
  const up = service.status === "up";
  return (
    <div className="flex items-center gap-3 border-b border-border py-2">
      <span className={up ? "bg-primary text-primary-foreground px-1" : "bg-accent text-accent-foreground px-1"}>
        {up ? "[ OK ]" : "[WARN]"}
      </span>
      <span className="flex-1">{service.name}</span>
      <span className="text-muted-foreground">{service.latency}ms</span>
      <button type="button" className="border border-border px-2 hover:bg-primary hover:text-primary-foreground" onClick={onRestart}>
        restart
      </button>
    </div>
  );
}

Editorial / newspaper

Asked as: "newspaper", "editorial", "magazine", "broadsheet", "like the morning paper". Newsprint ground, black ink, one red for kickers, ruled columns, serif type. Light-only.

css@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&family=Source+Serif+4:opsz,[email protected],400;8..60,600&display=swap');
:root {
  color-scheme: light;
  --background: #f6f2e8; --surface: #fbf8f1; --surface-overlay: #fbf8f1;
  --primary: #141414; --on-primary: #f6f2e8;
  --secondary: #e9e2d0; --on-secondary: #141414;
  --accent: #a51d1d; --on-accent: #ffffff; --neutral: #e9e2d0;
  --text-primary: #141414; --text-secondary: #57524a; --border: #1c1c1c;
  --font-family: 'Source Serif 4', Georgia, serif; --font-serif: 'Playfair Display', Georgia, serif;
  --font-family-mono: ui-monospace, monospace; --font-size-base: 1.0625rem;
  --radius-sm: 0px; --radius: 0px; --radius-lg: 0px; --border-width: 1px; --vibes-spacing: 1rem;
  --vibes-shadow-sm: 0 0 #0000; --vibes-shadow: 0 0 #0000; --vibes-shadow-lg: 0 0 #0000;
  --letter-spacing: -0.01em; --letter-spacing-wide: 0.12em; --font-weight: 700; --text-transform: uppercase;
}
  • Type: headlines font-serif 900 (text-5xl masthead, text-2xl story), body Source Serif 400 at 17px leading-relaxed, kickers text-xs font-bold tracking-wide in caps and text-accent.
  • Shape: square corners, hairline column rules (border-t border-border), a double rule under the masthead (border-b-4 border-double), no shadows, columns with md:grid-cols-3 gap-8.
jsxfunction RecipeStory({ recipe, onSave }) {
  return (
    <article className="border-t border-border pt-4 flex flex-col gap-2">
      <p className="text-xs font-bold tracking-wide uppercase text-accent">{recipe.course}</p>
      <h2 className="font-serif text-2xl font-bold leading-tight">{recipe.title}</h2>
      <p className="leading-relaxed first-letter:font-serif first-letter:text-4xl first-letter:float-left first-letter:mr-1">
        {recipe.intro}
      </p>
      <p className="text-sm italic text-muted-foreground">Serves {recipe.serves}</p>
      <button type="button" className="self-start bg-primary text-primary-foreground px-4 py-2 text-sm font-bold tracking-wide uppercase" onClick={onSave}>
        Clip it
      </button>
    </article>
  );
}

iOS settings

Asked as: "iOS settings", "like an iPhone app", "Apple style", "grouped list", "native settings". Grouped inset lists on a grey ground, hairline separators, blue actions, green switches.

css@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
:root {
  color-scheme: light;
  --background: #f2f2f7; --surface: #ffffff; --surface-overlay: #ffffff;
  --primary: #0066d6; --on-primary: #ffffff;
  --secondary: #e5e5ea; --on-secondary: #000000;
  --accent: #34c759; --on-accent: #000000; --neutral: #e5e5ea;
  --text-primary: #000000; --text-secondary: #6c6c70; --border: #c6c6c8;
  --font-family: -apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif;
  --font-family-mono: ui-monospace, Menlo, monospace; --font-size-base: 1.0625rem;
  --radius-sm: 6px; --radius: 10px; --radius-lg: 14px; --border-width: 0.5px; --vibes-spacing: 1rem;
  --vibes-shadow-sm: 0 0 #0000; --vibes-shadow: 0 0 #0000; --vibes-shadow-lg: 0 8px 30px rgb(0 0 0 / 0.12);
  --letter-spacing: -0.01em; --letter-spacing-wide: 0.02em; --font-weight: 600; --text-transform: uppercase;
}
@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;
    --background: #000000; --surface: #1c1c1e; --surface-overlay: #2c2c2e;
    --primary: #0a84ff; --on-primary: #000000; --secondary: #2c2c2e; --on-secondary: #ffffff;
    --accent: #30d158; --on-accent: #000000; --neutral: #2c2c2e;
    --text-primary: #ffffff; --text-secondary: #a1a1a6; --border: #38383a;
  }
}
  • Type: the system face (Inter where it is absent); rows at 17px 400, large title text-3xl font-bold, group headers text-xs text-muted-foreground uppercase above each group, footers text-xs below.
  • Shape: each group is one bg-card rounded-md block inset from the screen edge (mx-4), rows min-h-11 px-4, hairline separators inset to the label (border-b border-border on all rows but the last), chevrons in text-muted-foreground, no shadows.
jsxfunction SettingsGroup({ title, rows }) {
  return (
    <section className="mx-4 flex flex-col gap-1">
      <p className="px-4 text-xs uppercase text-muted-foreground">{title}</p>
      <div className="bg-card text-card-foreground rounded-md overflow-hidden">
        {rows.map((row, i) => (
          <label key={row.id} className="flex items-center min-h-11 pl-4">
            <div className={`flex flex-1 items-center gap-3 pr-4 py-2 ${i < rows.length - 1 ? "border-b border-border" : ""}`}>
              <span className="flex-1">{row.label}</span>
              {row.kind === "toggle" ? (
                <input type="checkbox" checked={row.on} onChange={row.onToggle} className="h-6 w-10 accent-accent" />
              ) : (
                <span className="text-muted-foreground">{row.value} ›</span>
              )}
            </div>
          </label>
        ))}
      </div>
    </section>
  );
}

Brand kit from the maker

Asked as: "our brand colours are #…", "use our palette", "our font is …", "match our logo". Map what the person pasted onto the variables, step by step. Their hex values go into the :root exactly as they wrote them.

  1. Ground. The lightest brand colour (a cream, an off-white) is --background. With no light colour in the kit, --background is #ffffff or a 3% tint of the main colour.
  2. Surface. --surface and --surface-overlay sit one step lighter than the ground (#ffffff on a cream).
  3. Primary. The boldest brand colour is --primary; --on-primary is #ffffff or a near-black, whichever reads at 4.5:1 or better.
  4. Secondary and accent. The next brand colour is --secondary (with its ink); a pale tint of the primary is --accent, for highlights and badges.
  5. Ink. --text-primary is the darkest brand colour, or a near-black leaning toward the primary's hue; --text-secondary sits halfway to the ground and still reads at 4.5:1; --border is a light step of the ground.
  6. Font. The named font is --font-family (or --font-serif for a display face), loaded in the @import. A font outside Google Fonts gets the closest Google family in its place.
  7. Shape. Take radius and weight from the brand's feel: rounded and soft for a friendly brand, square and tight for a formal one.

Worked example — "a bake sale order form; our colours are #1b4d3e, #f4a259 and #fdf6ec, and our font is Fraunces":

App.jsx

jsximport React, { useState } from "react";
import { useFireproof } from "use-fireproof";

// Brand kit: #fdf6ec ground, #1b4d3e primary, #f4a259 secondary, Fraunces display.
const THEME_CSS = `
@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,[email protected],600;9..144,800&family=Inter:wght@400;500;600&display=swap');
:root {
  color-scheme: light;
  --background: #fdf6ec; --surface: #ffffff; --surface-overlay: #ffffff;
  --primary: #1b4d3e; --on-primary: #ffffff;
  --secondary: #f4a259; --on-secondary: #2a1a08;
  --accent: #f8dcc0; --on-accent: #1b4d3e; --neutral: #f3eadc;
  --text-primary: #1f2a26; --text-secondary: #5c6661; --border: #e8dccb;
  --font-family: 'Inter', system-ui, sans-serif; --font-serif: 'Fraunces', Georgia, serif;
  --font-family-mono: ui-monospace, monospace; --font-size-base: 1rem;
  --radius-sm: 8px; --radius: 14px; --radius-lg: 22px; --border-width: 1px; --vibes-spacing: 1rem;
  --vibes-shadow-sm: 0 1px 2px rgb(27 77 62 / 0.06); --vibes-shadow: 0 2px 8px rgb(27 77 62 / 0.08);
  --vibes-shadow-lg: 0 12px 32px rgb(27 77 62 / 0.12);
  --letter-spacing: -0.01em; --letter-spacing-wide: 0.06em; --font-weight: 600; --text-transform: none;
}
`;

const TREATS = [
  { id: "scone", name: "Apricot scone", price: 3 },
  { id: "brownie", name: "Brown butter brownie", price: 4 },
  { id: "loaf", name: "Lemon loaf slice", price: 3 },
];

function TreatRow({ treat, count, onAdd }) {
  return (
    <div className="bg-card text-card-foreground border border-border rounded-md shadow-sm p-4 flex items-center gap-4">
      <div className="flex-1">
        <p className="font-serif text-lg">{treat.name}</p>
        <p className="text-sm text-muted-foreground">${treat.price} each</p>
      </div>
      {count > 0 && <span className="bg-accent text-accent-foreground rounded-sm px-2 py-1 text-sm">{count}</span>}
      <button type="button" className="bg-secondary text-secondary-foreground rounded-md px-4 py-2 font-medium" onClick={onAdd}>
        Add
      </button>
    </div>
  );
}

export default function App() {
  const { database, useLiveQuery } = useFireproof("bake-sale");
  const { docs: orders } = useLiveQuery("type", { key: "order" });
  const [name, setName] = useState("");
  const [basket, setBasket] = useState({});

  const add = (id) => setBasket((b) => ({ ...b, [id]: (b[id] ?? 0) + 1 }));
  const total = TREATS.reduce((sum, t) => sum + (basket[t.id] ?? 0) * t.price, 0);

  const placeOrder = async (e) => {
    e.preventDefault();
    const typed = name.trim();
    const items = basket;
    setName("");
    setBasket({});
    const { ok } = await database.put({ type: "order", name: typed, items, total, created: Date.now() });
    if (!ok) {
      setName((now) => now || typed);
      setBasket((now) => (Object.keys(now).length === 0 ? items : now));
    }
  };

  const c = {
    page: "min-h-screen bg-background text-foreground font-sans",
    column: "max-w-xl mx-auto p-6 flex flex-col gap-6",
    title: "font-serif text-4xl tracking-tight",
    field: "bg-card border border-border rounded-md px-4 py-3",
    order: "flex justify-between border-b border-border py-2 text-sm",
  };

  return (
    <div className={c.page}>
      <style>{THEME_CSS}</style>
      <main className={c.column}>
        <h1 className={c.title}>Saturday bake sale</h1>
        <form onSubmit={placeOrder} className="flex flex-col gap-4">
          {TREATS.map((t) => (
            <TreatRow key={t.id} treat={t} count={basket[t.id] ?? 0} onAdd={() => add(t.id)} />
          ))}
          <input className={c.field} placeholder="Name for the order" value={name} onChange={(e) => setName(e.target.value)} />
          <button
            type="submit"
            className="bg-primary text-primary-foreground rounded-md px-6 py-3 font-semibold disabled:opacity-50"
            disabled={!name.trim() || total === 0}
          >
            Order for ${total}
          </button>
        </form>
        <section className="flex flex-col">
          <h2 className="font-serif text-xl">Orders so far</h2>
          {orders.length === 0 && <p className="text-muted-foreground">The first order lands here.</p>}
          {orders.map((o) => (
            <div key={o._id} className={c.order}>
              <span>{o.name}</span>
              <span className="text-muted-foreground">${o.total}</span>
            </div>
          ))}
        </section>
      </main>
    </div>
  );
}