The brand in code
Read when: you set up a new site or app in his brand: Tailwind CSS 4, Nuxt, Nuxt UI, or a plain HTML page. Owns: how the tokens file plugs into each stack, the Tailwind theme mapping, dark mode, long-form text styles, the Nuxt UI mapping and the plain HTML starter.
His site runs Nuxt 4, Tailwind CSS 4 (4.3) and @nuxtjs/color-mode, dark by default (checked 2026-11-03). Every
value comes from ../assets/haythamasalama-brand-tokens.css; this file
only wires it in. Component templates are in components.md.
TL;DR
- Copy the tokens file into the project and import it right after Tailwind. It holds the values, the light and dark tokens, the base styles, the utilities and the animations.
- Map the tokens into Tailwind with one
@theme static inlineblock, then writebg-bg,text-fg,border-line,text-muted; never raw hex in themed UI. - Dark is the default; the light theme is
class="light"on<html>. - Hover stays neutral:
link-muted,hover:text-fg,hover:bg-surface. Neverhover:text-primary.
What the tokens file gives you
| Kind | Names |
|---|---|
| Raw values | --ink, --paper, --iris-50 … --iris-950, --azure-50 … --azure-950, --font-sans, --font-mono, --font-serif, --ease-out-soft |
| Theme tokens | --bg, --surface, --raised, --line, --edge, --chip, --fg, --soft, --muted, --faint, --mark, --underline, --accent, --accent-2, --gradient, --gradient-signature |
| Base | page colours and font, link transitions, selection colour, main p at 70ch, the focus ring |
| Utilities | .link, .link-underline, .link-muted, .eyebrow, .text-gradient, .border-gradient, .avatar-mono, .animate-rise, .animate-live |
| Components | .signature path (drawing), .go .arrow (nudging arrow), .menu-item (mobile menu stagger) |
Its rules sit in the base, components and utilities cascade layers, so Tailwind utilities and page styles
override them in the expected order.
Tailwind CSS 4
app/assets/css/main.css (Nuxt) or src/style.css (Vite), with the tokens file next to it:
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@import "@fontsource-variable/geist";
@import "@fontsource-variable/geist-mono";
@import "@fontsource/instrument-serif/latin-400-italic.css";
@import "./haythamasalama-brand-tokens.css";
@custom-variant dark (&:where(.dark, .dark *));
@custom-variant light (&:where(.light, .light *));
@theme static inline {
--color-ink: var(--ink);
--color-paper: var(--paper);
--color-iris-50: var(--iris-50); --color-iris-100: var(--iris-100); --color-iris-200: var(--iris-200);
--color-iris-300: var(--iris-300); --color-iris-400: var(--iris-400); --color-iris-500: var(--iris-500);
--color-iris-600: var(--iris-600); --color-iris-700: var(--iris-700); --color-iris-800: var(--iris-800);
--color-iris-900: var(--iris-900); --color-iris-950: var(--iris-950);
--color-azure-50: var(--azure-50); --color-azure-100: var(--azure-100); --color-azure-200: var(--azure-200);
--color-azure-300: var(--azure-300); --color-azure-400: var(--azure-400); --color-azure-500: var(--azure-500);
--color-azure-600: var(--azure-600); --color-azure-700: var(--azure-700); --color-azure-800: var(--azure-800);
--color-azure-900: var(--azure-900); --color-azure-950: var(--azure-950);
--color-bg: var(--bg); --color-surface: var(--surface); --color-raised: var(--raised);
--color-line: var(--line); --color-edge: var(--edge); --color-chip: var(--chip);
--color-fg: var(--fg); --color-soft: var(--soft); --color-muted: var(--muted);
--color-faint: var(--faint); --color-mark: var(--mark);
--color-accent: var(--accent); --color-accent-2: var(--accent-2);
--default-transition-duration: 200ms;
--default-transition-timing-function: var(--ease-out-soft);
}
inlinemakesbg-bgresolve tovar(--bg), so it flips with.light.staticalways emits the--color-*variables. Why: Nuxt UI and scripts read them at runtime, and Tailwind would otherwise emit only the ones a class uses.- The fonts need no
@themeentry: the tokens file's--font-sans,--font-monoand--font-serifoverride Tailwind's defaults, sofont-sans,font-monoandfont-serifalready use Geist and Instrument Serif. - Classes then read like the site:
bg-raised,border-line,text-soft,text-iris-400 light:text-iris-600,bg-linear-135 from-iris-500 to-azure-500,outline-iris-400.
Dark mode
Nuxt:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/color-mode'],
css: ['~/assets/css/main.css'],
colorMode: { preference: 'dark', fallback: 'dark', classSuffix: '', storageKey: 'theme' },
app: { head: { meta: [{ name: 'theme-color', content: '#0B0B0C' }] } },
})
classSuffix: '' gives .dark and .light on <html>, which is what the tokens file and the variants expect.
Without Nuxt, write <html class="dark"> and swap the class from a toggle, saving the choice under theme.
A dark-first site ignores prefers-color-scheme on purpose: dark is the brand, light is a choice.
Long-form text
Articles use Tailwind Typography with this utility, as class="prose prose-site max-w-none":
@utility prose-site {
--tw-prose-body: var(--soft);
--tw-prose-headings: var(--fg);
--tw-prose-lead: var(--soft);
--tw-prose-links: var(--fg);
--tw-prose-bold: var(--fg);
--tw-prose-counters: var(--faint);
--tw-prose-bullets: var(--chip);
--tw-prose-hr: var(--line);
--tw-prose-quotes: var(--soft);
--tw-prose-quote-borders: var(--accent-2);
--tw-prose-captions: var(--faint);
--tw-prose-code: var(--fg);
--tw-prose-pre-code: var(--fg);
--tw-prose-pre-bg: var(--raised);
--tw-prose-th-borders: var(--line);
--tw-prose-td-borders: var(--line);
& :where(h2, h3, h4) a { color: inherit; font-weight: inherit; text-decoration: none; }
& :where(h2, h3, h4) a:hover { text-decoration: underline; text-decoration-color: var(--underline); }
& :where(a):not(:where(h2, h3, h4) a) { text-decoration-color: var(--underline); text-decoration-thickness: 1px; }
& :where(a):not(:where(h2, h3, h4) a):hover { text-decoration-color: var(--fg); }
}
Code blocks take the palette in colors.md as two Shiki themes
(highlight.theme: { default: dark, light: light } in Nuxt Content).
Nuxt UI 4
His site does not use Nuxt UI, so this is a starting point, not a copy of the site: check contrast and selected states on the first page built with it. Keep the Tailwind block above and add:
@import "tailwindcss";
@import "@nuxt/ui";
/* fonts, tokens file, variants and @theme block as above */
:root,
.light,
.dark {
--ui-primary: var(--accent-2);
--ui-bg: var(--bg);
--ui-bg-muted: var(--raised);
--ui-bg-elevated: var(--surface);
--ui-bg-accented: var(--chip);
--ui-bg-inverted: var(--fg);
--ui-border: var(--line);
--ui-border-muted: var(--line);
--ui-border-accented: var(--chip);
--ui-border-inverted: var(--fg);
--ui-text-highlighted: var(--fg);
--ui-text: var(--soft);
--ui-text-toned: var(--soft);
--ui-text-muted: var(--muted);
--ui-text-dimmed: var(--faint);
--ui-text-inverted: var(--bg);
--ui-radius: 0.5rem;
}
// app/app.config.ts
export default defineAppConfig({
ui: {
colors: { primary: 'iris', secondary: 'azure', neutral: 'zinc' },
card: { slots: { root: 'shadow-none' } },
button: { defaultVariants: { color: 'neutral' } },
tabs: { defaultVariants: { color: 'neutral' } },
navigationMenu: { defaultVariants: { color: 'neutral' } },
},
})
- One block serves both themes because every
--ui-*points at a token that already flips with.light. - Neutral is the default colour. Why: the brand's main button is black on light and white on dark;
primaryis for a badge, a progress bar or one highlighted action, inside the 5% budget. - Nuxt UI turns on
@nuxtjs/color-modeitself; setcolorMode: { preference: 'dark', fallback: 'dark' }innuxt.config.tsas above. - With
@nuxt/fontsinstead of Fontsource, list Geist (400, 500, 600), Geist Mono (400, 500) and Instrument Serif (400 italic) underfonts.families.
Plain HTML
For a one-off page, a talk page or an image rendered from HTML. Copy the tokens file and the signature files beside it.
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#0B0B0C">
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&family=Instrument+Serif:ital@1&display=swap">
<link rel="stylesheet" href="haythamasalama-brand-tokens.css">
<style>
.page { max-width: 880px; margin: 0 auto; padding: 64px 24px; }
h1 { margin: 16px 0 0; font-size: 44px; line-height: 1.08; font-weight: 600; letter-spacing: -0.03em; }
.lead { font-size: 17px; line-height: 1.7; color: var(--soft); }
.word { font-family: var(--font-serif); font-style: italic; font-size: 22px; padding-right: 2px; }
.card { border: 1px solid var(--line); border-radius: 16px; padding: 22px; }
</style>
</head>
<body>
<main class="page">
<p class="eyebrow">Talk · 2026</p>
<h1>How I ship software</h1>
<p class="lead">Haytham A. Salama, <span class="word text-gradient">software engineer</span>.</p>
<img src="haythamasalama-signature-white.svg" alt="Haytham's handwritten signature" width="120" height="61">
</main>
</body>
</html>
For the light theme, swap class="dark" for class="light" and the white signature for the black one.