import animate from 'tailwindcss-animate'; /** * Hand-written, never CLI-generated. `shadcn add` rewrites this file and * strips every comment in it, so if you run the CLI, diff this file * afterwards and put the comments back. * * The palette is PIG's, inlined as literals rather than injected at runtime: * this site has no user-chosen accent, so a runtime theme layer would be a * moving part that buys nothing. Every value here is `hsl(var(--token))` โ€” * the triples themselves, with their measured contrast ratios, live in * src/index.css. Nothing in this file is a raw colour. */ /** @type {import('tailwindcss').Config} */ export default { darkMode: ['class', '[data-theme="dark"]'], content: ['./index.html', './src/**/*.{ts,tsx}'], theme: { extend: { colors: { bg: 'hsl(var(--bg))', background: 'hsl(var(--bg))', surface: 'hsl(var(--surface))', 'surface-2': 'hsl(var(--surface-2))', 'surface-3': 'hsl(var(--surface-3))', border: 'hsl(var(--border))', // The only boundary token that clears 3:1 as a non-text signal. Inputs, // selects, outline chips and icons at rest use it; `border` is decorative. 'border-strong': 'hsl(var(--border-strong))', input: 'hsl(var(--border))', ring: 'hsl(var(--ring))', fg: 'hsl(var(--fg))', foreground: 'hsl(var(--fg))', 'fg-2': 'hsl(var(--fg-2))', muted: 'hsl(var(--muted))', 'muted-foreground': 'hsl(var(--muted))', // shadcn's `accent` is its SUBTLE hover surface, not the brand. Mapping // it to the brand paints a full-strength block on every hover state. accent: 'hsl(var(--accent-subtle))', 'accent-foreground': 'hsl(var(--accent-fg))', brand: 'hsl(var(--accent))', 'accent-fg': 'hsl(var(--accent-fg))', 'accent-on': 'hsl(var(--accent-on))', 'accent-edge': 'hsl(var(--accent-edge))', 'accent-subtle': 'hsl(var(--accent-subtle))', // The tab bar's 2px state bar: accent in dark, accent-fg in light, so // the trigger needs one class and no theme variant. 'tab-underline': 'hsl(var(--tab-underline))', primary: { DEFAULT: 'hsl(var(--accent))', foreground: 'hsl(var(--accent-on))' }, secondary: { DEFAULT: 'hsl(var(--surface-2))', foreground: 'hsl(var(--fg))' }, card: { DEFAULT: 'hsl(var(--surface))', foreground: 'hsl(var(--fg))' }, popover: { DEFAULT: 'hsl(var(--surface))', foreground: 'hsl(var(--fg))' }, // `danger-on`, not white: white on the dark theme's red is 3.63:1. destructive: { DEFAULT: 'hsl(var(--danger))', foreground: 'hsl(var(--danger-on))' }, positive: 'hsl(var(--positive))', warning: 'hsl(var(--warning))', 'warning-on': 'hsl(var(--warning-on))', 'warning-subtle': 'hsl(var(--warning-subtle))', danger: 'hsl(var(--danger))', 'danger-on': 'hsl(var(--danger-on))', 'danger-subtle': 'hsl(var(--danger-subtle))', info: 'hsl(var(--info))', // The board's three tile states. Named by MEANING, not by colour, so // the high-contrast palette can swap the values without renaming. tile: { exact: 'hsl(var(--tile-exact))', 'exact-fg': 'hsl(var(--tile-exact-fg))', present: 'hsl(var(--tile-present))', 'present-fg': 'hsl(var(--tile-present-fg))', absent: 'hsl(var(--tile-absent))', 'absent-fg': 'hsl(var(--tile-absent-fg))', // An unplayed tile's edge (`border-tile-empty`). empty: 'hsl(var(--tile-empty-border))', }, }, fontFamily: { sans: ['Geist Variable', 'Inter', 'ui-sans-serif', 'system-ui', 'sans-serif'], mono: ['Geist Mono Variable', 'ui-monospace', 'SFMono-Regular', 'Menlo', 'monospace'], }, /* * The type scale (spec ยง3). Sans unless the name says mono; the face is * still applied by `font-mono` on the element, these only fix size, * leading, weight and tracking. Responsive steps are the `-lg` / `-xl` * siblings (`text-display lg:text-display-lg xl:text-display-xl`). * Weight ceiling is 600 everywhere. */ fontSize: { display: ['2.25rem', { lineHeight: '1.05', fontWeight: '600', letterSpacing: '-0.02em' }], 'display-lg': ['3rem', { lineHeight: '1.05', fontWeight: '600', letterSpacing: '-0.02em' }], 'display-xl': ['3.5rem', { lineHeight: '1.05', fontWeight: '600', letterSpacing: '-0.02em' }], h1: ['1.75rem', { lineHeight: '1.15', fontWeight: '600', letterSpacing: '-0.015em' }], 'h1-lg': ['2rem', { lineHeight: '1.15', fontWeight: '600', letterSpacing: '-0.015em' }], h2: ['1.5rem', { lineHeight: '1.15', fontWeight: '600', letterSpacing: '-0.015em' }], 'h2-lg': ['1.875rem', { lineHeight: '1.15', fontWeight: '600', letterSpacing: '-0.015em' }], h3: ['1.125rem', { lineHeight: '1.3', fontWeight: '600', letterSpacing: '-0.01em' }], lede: ['1.125rem', { lineHeight: '1.5', fontWeight: '400' }], 'lede-lg': ['1.25rem', { lineHeight: '1.5', fontWeight: '400' }], // The tab claim: never mono, never uppercase, never shortened. claim: ['1.125rem', { lineHeight: '1.35', fontWeight: '500' }], 'claim-lg': ['1.25rem', { lineHeight: '1.35', fontWeight: '500' }], // The education floor. Nothing that explains is ever smaller. prose: ['1rem', { lineHeight: '1.6', fontWeight: '400' }], ui: ['0.875rem', { lineHeight: '1.4' }], small: ['0.8125rem', { lineHeight: '1.45', fontWeight: '400' }], caption: ['0.75rem', { lineHeight: '1.4' }], // mono, uppercase โ€” pair with `font-mono uppercase`. label: ['0.6875rem', { lineHeight: '1.2', fontWeight: '500', letterSpacing: '0.04em' }], 'mono-data': ['0.8125rem', { lineHeight: '1.4', fontWeight: '400' }], stat: ['0.8125rem', { lineHeight: '1.4', fontWeight: '600' }], metric: ['2rem', { lineHeight: '1.1', fontWeight: '600', letterSpacing: '-0.01em' }], // The landing's 0% โ†’ ~60% figure. figure: ['2.5rem', { lineHeight: '1', fontWeight: '500', letterSpacing: '-0.02em' }], 'figure-lg': ['3.5rem', { lineHeight: '1', fontWeight: '500', letterSpacing: '-0.02em' }], code: ['0.8125rem', { lineHeight: '1.6' }], }, letterSpacing: { label: '0.04em' }, // A real four-step scale. 4px tiles/cells, 6px controls, 8px inner blocks, // 12px surfaces. Named aliases resolve to the same CSS variables index.css // declares, so a component can say what it means (`rounded-control`). borderRadius: { sm: '0.25rem', md: '0.375rem', lg: '0.5rem', xl: '0.75rem', tile: 'var(--radius-tile)', control: 'var(--radius-control)', surface: 'var(--radius-surface)', }, // Overlays only. Flat components carry no shadow; borders do the work. boxShadow: { overlay: 'var(--shadow-overlay)' }, maxWidth: { canvas: '80rem', measure: 'var(--measure)', 'measure-wide': 'var(--measure-wide)' }, transitionDuration: { 1: 'var(--dur-1)', 2: 'var(--dur-2)', 3: 'var(--dur-3)' }, transitionTimingFunction: { enter: 'var(--ease-out)', exit: 'var(--ease-in)' }, }, }, plugins: [animate], };