a21596b3e4
Eighteen agents: three design directions judged on whether a COO on a phone actually learns what an environment is, on craft, and on landing without a rewrite; one spec; a foundation of measured tokens; six build lanes; three browser verifiers; a final gate pass. The materials are Prime Intellect's, measured from their site: near-black grounds, one green, sharp radii, mono small-caps labels, Geist and Geist Mono self-hosted because production CSP is font-src 'self'. Two of their own greys fail contrast on their own ground (#737373 is 4.02:1, #6E6E6E is 3.73:1 on #0F0F0F), so --muted is lifted and the CSS comment carries the number — or someone will 'correct' it back. Every text-on-ground pair in both themes is tabulated in src/index.css with its measured ratio. The two rules that resolved every conflict: data is mono, sentences are sans; the language wins on materials, the lesson wins on legibility. Light mode is a finished paper theme, not an inversion. What did not change: the derived-tabs contract, the honesty markers, the isolation lint, every gate. 419 contract checks, entry chunk at 74% of budget, zero horizontal overflow on any route at 390/1024/1440 in either theme. Also flips Alert Triage to status 'live' — the pipeline built it but never promoted it, so it was badged SPEC on its own playable page and the home page counted one environment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
138 lines
7.2 KiB
JavaScript
138 lines
7.2 KiB
JavaScript
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],
|
|
};
|