diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index ab200dc2..f015805f 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -1,25 +1,71 @@ /** * Root layout for @gonext/docs. * - * Renders the persistent shell: top nav (Docs / ADRs / API Reference / - * Proposals), the section sidebar (slot via `children`), and the search - * bar. The sidebar is itself rendered by individual section layouts so - * that pages outside the doc tree (landing, API reference) can render - * full-bleed. + * Renders the persistent shell for every docs route: the topbar with + * brand wordmark, primary nav, and global search; everything below the + * topbar is owned by the route — sidebars, articles, and TOC come from + * the per-section layouts under `app/docs/` and `app/adr/`. + * + * Brand fonts (Living-Systems handoff): Archivo for display headlines, + * Geist for UI/body, Geist Mono for code, Instrument Serif for the + * signature italic accents that swap in inside tags. They load via + * `next/font/google` so the URLs are self-hosted, layout shift is + * suppressed by next/font's reserved-space metric, and the CSS + * variables surface for both Tailwind utilities and raw selectors in + * `styles/docs.css` / `src/styles/tokens.css`. */ import type { Metadata } from 'next'; import type { ReactElement, ReactNode } from 'react'; import Link from 'next/link'; +import { + Archivo, + Geist, + Geist_Mono, + Instrument_Serif, +} from 'next/font/google'; import { SearchBar } from '@/components/SearchBar'; import { buildSearchIndex } from '@/lib/content'; import '@/styles/docs.css'; +const archivo = Archivo({ + subsets: ['latin'], + weight: ['500', '600', '700', '800', '900'], + variable: '--font-display', + display: 'swap', +}); + +const geist = Geist({ + subsets: ['latin'], + weight: ['400', '500', '600', '700'], + variable: '--font-sans', + display: 'swap', +}); + +const geistMono = Geist_Mono({ + subsets: ['latin'], + weight: ['400', '500'], + variable: '--font-mono', + display: 'swap', +}); + +const instrumentSerif = Instrument_Serif({ + subsets: ['latin'], + weight: ['400'], + style: ['normal', 'italic'], + variable: '--font-serif', + display: 'swap', +}); + export const metadata: Metadata = { title: { default: 'GoNext Documentation', template: '%s · GoNext Docs', }, - description: 'Documentation, ADRs, and architectural references for the GoNext platform.', + description: + 'Documentation, ADRs, and architectural references for the GoNext platform — a modular CMS built on Go and Next.js.', + icons: { + icon: '/favicon.svg', + }, }; export default async function RootLayout({ @@ -30,18 +76,35 @@ export default async function RootLayout({ // The search index is built once per build and inlined into the layout // chunk. Keeps the index out of `_next/data` round-trips. const entries = await buildSearchIndex(); + const fontVariables = [ + archivo.variable, + geist.variable, + geistMono.variable, + instrumentSerif.variable, + ].join(' '); return ( - + -
- GoNext - - +
+
+ + + Go + Next + + Docs + + +
+
+ + v1.0 +
{children} diff --git a/apps/docs/app/page.tsx b/apps/docs/app/page.tsx index 641720e3..48529fce 100644 --- a/apps/docs/app/page.tsx +++ b/apps/docs/app/page.tsx @@ -1,15 +1,25 @@ /** - * Landing page. + * Landing page — the docs site front door. * - * Three pieces, ordered top-down on the page: - * 1. A hero with the project name, one-line value prop, and a Getting - * Started CTA that links into the first doc. - * 2. A feature grid summarising what the docs cover. + * Three sections, ordered top-down: + * 1. A hero with a giant Archivo headline ("Docs that *grow* with you.") + * where the italic emerald `grow` ties to the brand's + * italic-accent rule. Beneath it sits a Geist subtitle and two + * prominent CTAs — emerald primary (Read the docs) and a cream + * secondary (API reference). + * 2. A forest "alive band" with organic radial-glow gradients in + * emerald + lavender. Inside the band lives the feature grid of + * subsystem entry points (each card jumps into a specific doc). * 3. A direct path into the ADR list for readers who came here looking * for design decisions, not how-to. + * + * The italic emphasis (`grow`) follows HANDOFF.md "The italic + * accent rule": one italic word per headline, max two. It is emphasis + * not decoration. */ import Link from 'next/link'; import type { ReactElement } from 'react'; +import { ArrowRight, BookOpen } from 'lucide-react'; const FEATURES = [ { @@ -48,33 +58,50 @@ export default function LandingPage(): ReactElement { return (
-

GoNext Documentation

+ Living documentation +

+ Docs that grow with you. +

- A modern, modular CMS built on Go and Next.js. Familiar mental model, - modern stack, plugin and theme ecosystems engineered for safety. + Subsystem guides, architectural decisions, and the API reference for the + GoNext platform. One coherent surface for the whole stack — Go backend, + Next.js frontend, plugin and theme ecosystems.

- - Start with the architecture overview → - +
+ +
-
+
+
By subsystem
+

+ One product for everything you used five for. +

+

+ Each guide is self-contained — the architecture overview is the entry + point, but you can drop into any subsystem on its own. +

{FEATURES.map((f) => ( -

{f.title}

+

{f.title}

{f.body}

))}
-
-

- Looking for design decisions? Read the{' '} - Architecture Decision Records. -

-
+

+ Looking for design decisions? Read the{' '} + Architecture Decision Records. +

); } diff --git a/apps/docs/components/Callout.tsx b/apps/docs/components/Callout.tsx index e7e099ed..bc11d14f 100644 --- a/apps/docs/components/Callout.tsx +++ b/apps/docs/components/Callout.tsx @@ -4,6 +4,11 @@ * Used by first-party MDX pages. Filesystem-sourced markdown gets folded * into
by the renderer; we may later promote `> [!NOTE]` * patterns into this component, but for now the two layers stay separate. + * + * Visual: emerald-soft tint for note/tip, warning-soft for warning, + * danger-soft for danger. Each carries a circular ink-on-cream icon + * (Archivo glyph) with an uppercase emerald-deep title. Mirrors the + * .callout treatment in docs/design/ui_kits/docs/index.html. */ import type { ReactElement, ReactNode } from 'react'; diff --git a/apps/docs/components/SearchBar.tsx b/apps/docs/components/SearchBar.tsx index a46ba3c9..b5fdc15f 100644 --- a/apps/docs/components/SearchBar.tsx +++ b/apps/docs/components/SearchBar.tsx @@ -5,6 +5,11 @@ * no body text) so we don't have to ship the entire docs corpus to the * client. Fuse.js handles the fuzzy match; results land in a small dropdown. * + * Visual: paper-3 pill with a leading Lucide `search` icon and a trailing + * ⌘K keycap. On focus the field swaps to paper background with an + * emerald border + emerald-tinted focus halo (`--sh-focus`). Matches + * the .search-mini pill in docs/design/ui_kits/docs/index.html. + * * No analytics, no remote calls — this is a deliberate non-feature: the * search index is part of the static bundle and works offline. */ @@ -12,6 +17,7 @@ import Link from 'next/link'; import Fuse from 'fuse.js'; +import { Search } from 'lucide-react'; import { useMemo, useState, type ReactElement } from 'react'; import type { SearchEntry } from '@/lib/content'; @@ -37,16 +43,20 @@ export function SearchBar({ entries }: { entries: SearchEntry[] }): ReactElement return (
- setQuery(e.target.value)} - onFocus={() => setOpen(true)} - onBlur={() => setTimeout(() => setOpen(false), 150)} - aria-label="Search documentation" - /> +
+
{open && results.length > 0 && (
    {results.map((r) => { diff --git a/apps/docs/package.json b/apps/docs/package.json index abbf5bee..d319a83e 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -21,6 +21,7 @@ "@next/mdx": "^15.0.0", "fuse.js": "^7.0.0", "gray-matter": "^4.0.3", + "lucide-react": "^0.469.0", "next": "^15.0.0", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -38,9 +39,12 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitest/coverage-v8": "^1.6.0", + "autoprefixer": "^10.4.20", "eslint": "^8.57.0", "eslint-config-next": "^15.0.0", "jsdom": "^24.0.0", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", "typescript": "^5.6.0", "vitest": "^1.6.0" } diff --git a/apps/docs/postcss.config.mjs b/apps/docs/postcss.config.mjs new file mode 100644 index 00000000..022ba41f --- /dev/null +++ b/apps/docs/postcss.config.mjs @@ -0,0 +1,17 @@ +/** + * PostCSS config — required for Next.js to pick up Tailwind v3. + * + * Mirrors `apps/admin/postcss.config.mjs`. Next.js auto-detects this + * file at the app root and runs the listed plugins over every imported + * `.css` file. We import the global stylesheet (which lives at + * `styles/docs.css`, kept under that name for backwards-compatible + * import paths) from the root layout; without the postcss config the + * `@tailwind` directives at the top of that file would be left as + * literal text. + */ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/docs/public/favicon.ico b/apps/docs/public/favicon.ico new file mode 100644 index 00000000..8be9d61c Binary files /dev/null and b/apps/docs/public/favicon.ico differ diff --git a/apps/docs/public/favicon.svg b/apps/docs/public/favicon.svg new file mode 100644 index 00000000..0b025a84 --- /dev/null +++ b/apps/docs/public/favicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/apps/docs/public/logo-mark.svg b/apps/docs/public/logo-mark.svg new file mode 100644 index 00000000..84b37f60 --- /dev/null +++ b/apps/docs/public/logo-mark.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/apps/docs/public/logo-wordmark.svg b/apps/docs/public/logo-wordmark.svg new file mode 100644 index 00000000..6e3e2ec3 --- /dev/null +++ b/apps/docs/public/logo-wordmark.svg @@ -0,0 +1,7 @@ + + + + + Go + Next + \ No newline at end of file diff --git a/apps/docs/src/styles/tokens.css b/apps/docs/src/styles/tokens.css new file mode 100644 index 00000000..321df481 --- /dev/null +++ b/apps/docs/src/styles/tokens.css @@ -0,0 +1,298 @@ +/* ============================================================= + GoNext — Design Tokens + "Living systems" — biology-meets-software. + Cream paper · forest ink · emerald + lavender accents. + Heavy grotesque display + editorial italic serif accents. + + NOTE: Fonts are self-hosted via `next/font/google` in + `app/layout.tsx`. The CSS variables (--font-display, --font-sans, + --font-mono, --font-serif) are injected on the element by + next/font; the rules below reference them so the same token names + from the design handoff resolve at runtime to the real font + families. Do not re-add the @import — it would double-fetch and + defeat next/font's self-hosting + preloading. + ============================================================= */ + +:root { + /* ─── Surface — warm cream paper ────────────────────────── */ + --paper: #F5F2EA; /* page background — warm cream */ + --paper-2: #EFEBE0; /* card / panel surface */ + --paper-3: #E6E1D2; /* sunken, hover row, inputs at rest */ + --paper-4: #DAD3BD; /* inset wells */ + + /* ─── Surface — deep forest dark ────────────────────────── */ + --forest: #0E1A14; /* primary dark surface (sidebar, dark hero) */ + --forest-2: #18261E; /* lifted dark surface */ + --forest-3: #22322A; /* dark hover */ + --forest-border: #2C3D33; /* hairline on dark */ + + /* ─── Foreground ────────────────────────────────────────── */ + --ink: #0E1A14; /* primary text — same as forest, "alive" black */ + --ink-soft: #1F2D26; /* secondary text on cream */ + --fg-muted: #4A5C52; /* tertiary text on cream */ + --fg-subtle: #6B7B72; /* labels, captions */ + --fg-faint: #94A199; /* placeholder, disabled */ + --fg-on-forest: #F0EAD8; /* primary text on dark forest */ + --fg-on-forest-muted: #A8B5AC; + + /* ─── Border ────────────────────────────────────────────── */ + --border: #D9D2C0; /* default hairline on cream */ + --border-strong: #B8B09A; /* focused / emphasized on cream */ + --border-subtle: #E8E2D1; /* very faint, table rows */ + + /* ─── Accent — vivid emerald (primary brand) ────────────── */ + --emerald: #10B981; /* fresh, alive green — primary CTAs, focus */ + --emerald-bright: #34D399; /* lighter, used on dark surfaces */ + --emerald-deep: #047857; /* hover, text-on-cream emerald */ + --emerald-soft: #D1FAE5; /* tinted surface */ + --emerald-ink: #022C22; /* text on emerald */ + + /* ─── Accent — lavender (secondary, data viz) ───────────── */ + --lavender: #A78BFA; /* secondary accent — data viz, tags */ + --lavender-deep: #7C3AED; + --lavender-soft: #EDE9FE; + + /* ─── Semantic ──────────────────────────────────────────── */ + --success: #059669; + --success-soft: #D1FAE5; + --warning: #D97706; + --warning-soft: #FEF3C7; + --danger: #DC2626; + --danger-soft: #FEE2E2; + + /* ─── Typography ────────────────────────────────────────── */ + /* These four variables are *re-declared* on by next/font with the + real, hashed self-hosted family names. The fallback chains below only + apply if next/font hasn't loaded yet (FOUT window) or in environments + where next/font isn't running (e.g. raw vitest jsdom snapshots). */ + --font-display: 'Archivo', system-ui, sans-serif; /* heavy grotesque headlines */ + --font-sans: 'Geist', -apple-system, system-ui, sans-serif; /* body, UI */ + --font-serif: 'Instrument Serif', Georgia, serif; /* italic accents */ + --font-mono: 'Geist Mono', ui-monospace, monospace; + /* legacy alias */ + --font-ui: var(--font-sans); + + /* type scale */ + --t-2xs: 11px; + --t-xs: 12px; + --t-sm: 13px; + --t-base: 14px; + --t-md: 15px; + --t-lg: 17px; + --t-xl: 20px; + --t-2xl: 24px; + --t-3xl: 32px; + --t-4xl: 44px; + --t-5xl: 64px; + --t-6xl: 96px; + + /* line heights */ + --lh-tight: 1.0; + --lh-snug: 1.15; + --lh-normal: 1.5; + + /* tracking */ + --track-tight: -0.03em; + --track-normal: -0.005em; + --track-wide: 0.04em; + + /* ─── Spacing (4-base) ──────────────────────────────────── */ + --s-1: 4px; + --s-2: 8px; + --s-3: 12px; + --s-4: 16px; + --s-5: 20px; + --s-6: 24px; + --s-7: 32px; + --s-8: 48px; + --s-9: 64px; + --s-10: 96px; + + /* ─── Radii ─────────────────────────────────────────────── */ + --r-xs: 4px; + --r-sm: 6px; + --r-md: 8px; + --r-lg: 12px; + --r-xl: 16px; + --r-pill: 999px; + + /* ─── Shadows (soft, organic — not hard offsets) ────────── */ + --sh-xs: 0 1px 2px rgba(14, 26, 20, 0.04); + --sh-sm: 0 1px 3px rgba(14, 26, 20, 0.06), 0 1px 2px rgba(14, 26, 20, 0.04); + --sh-md: 0 6px 14px -4px rgba(14, 26, 20, 0.08), 0 2px 6px -2px rgba(14, 26, 20, 0.04); + --sh-lg: 0 16px 32px -10px rgba(14, 26, 20, 0.14), 0 4px 10px -4px rgba(14, 26, 20, 0.06); + --sh-focus: 0 0 0 3px rgba(16, 185, 129, 0.22); + + /* ─── Motion ────────────────────────────────────────────── */ + --ease: cubic-bezier(0.2, 0.7, 0.2, 1); + --dur-fast: 100ms; + --dur: 160ms; + --dur-slow: 260ms; +} + +/* ─── Reset ───────────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; } +html, body { height: 100%; } +body { + margin: 0; + font-family: var(--font-sans); + font-size: var(--t-base); + line-height: var(--lh-normal); + color: var(--ink); + background: var(--paper); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + font-feature-settings: 'cv11', 'ss01'; +} + +/* ─── Display headings ────────────────────────────────────── */ +.display, h1.display { + font-family: var(--font-display); font-weight: 800; + font-size: clamp(var(--t-4xl), 6.5vw, var(--t-6xl)); + line-height: var(--lh-tight); letter-spacing: var(--track-tight); + color: var(--ink); margin: 0; +} +.display em, h1.display em, .italic-accent { + font-family: var(--font-serif); font-weight: 400; + font-style: italic; letter-spacing: -0.01em; + font-size: 1.05em; +} + +h1, .h1 { + font-family: var(--font-display); font-weight: 800; + font-size: var(--t-4xl); line-height: var(--lh-tight); letter-spacing: var(--track-tight); + margin: 0; color: var(--ink); +} +h1 em, .h1 em { font-family: var(--font-serif); font-weight: 400; font-style: italic; font-size: 1.05em; } + +h2, .h2 { + font-family: var(--font-display); font-weight: 700; + font-size: var(--t-3xl); line-height: var(--lh-snug); letter-spacing: var(--track-tight); + margin: 0; color: var(--ink); +} +h2 em, .h2 em { font-family: var(--font-serif); font-weight: 400; font-style: italic; font-size: 1.05em; } + +h3, .h3 { + font-family: var(--font-sans); font-weight: 600; + font-size: var(--t-xl); line-height: var(--lh-snug); letter-spacing: var(--track-normal); + margin: 0; color: var(--ink); +} + +h4, .h4 { + font-family: var(--font-sans); font-weight: 600; + font-size: var(--t-lg); line-height: var(--lh-snug); + margin: 0; color: var(--ink); +} + +p, .body { + font-family: var(--font-sans); font-weight: 400; + font-size: var(--t-base); line-height: var(--lh-normal); + margin: 0; color: var(--ink-soft); + text-wrap: pretty; +} +.lead { font-size: var(--t-md); line-height: var(--lh-normal); color: var(--fg-muted); } +small, .small { font-size: var(--t-sm); color: var(--fg-muted); } + +.label { + font-family: var(--font-sans); font-weight: 500; + font-size: var(--t-xs); line-height: 1.4; + color: var(--fg-subtle); +} + +.eyebrow { + display: inline-block; + font-family: var(--font-sans); font-weight: 500; + font-size: var(--t-xs); letter-spacing: 0.12em; text-transform: uppercase; + color: var(--emerald-deep); +} + +code, .mono { font-family: var(--font-mono); font-size: 0.92em; } + +/* ─── Tags ────────────────────────────────────────────────── */ +.tag { + display: inline-flex; align-items: center; gap: 4px; + padding: 2px 8px; + font-family: var(--font-sans); font-weight: 500; font-size: var(--t-xs); + background: var(--paper-3); color: var(--fg-muted); + border: 1px solid var(--border); + border-radius: var(--r-sm); + line-height: 1.5; +} +.tag--emerald { background: var(--emerald-soft); color: var(--emerald-deep); border-color: transparent; } +.tag--lavender { background: var(--lavender-soft); color: var(--lavender-deep); border-color: transparent; } +.tag--success { background: var(--success-soft); color: var(--success); border-color: transparent; } +.tag--warning { background: var(--warning-soft); color: var(--warning); border-color: transparent; } +.tag--danger { background: var(--danger-soft); color: var(--danger); border-color: transparent; } +.tag--ink { background: var(--ink); color: var(--paper); border-color: transparent; } +.tag--dot::before { content: ''; width: 6px; height: 6px; border-radius: 999px; background: currentColor; opacity: 0.9; } + +/* ─── Buttons ─────────────────────────────────────────────── */ +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: 6px; + padding: 9px 16px; + font-family: var(--font-sans); font-weight: 500; + font-size: var(--t-sm); line-height: 1; + color: var(--ink); + background: var(--paper-2); + border: 1px solid var(--border); + border-radius: var(--r-md); + cursor: pointer; text-decoration: none; + transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease), color var(--dur) var(--ease), box-shadow var(--dur) var(--ease); + box-shadow: var(--sh-xs); +} +.btn:hover { background: var(--paper-3); border-color: var(--border-strong); } +.btn:focus-visible { outline: none; box-shadow: var(--sh-focus); } + +.btn--primary { background: var(--ink); color: var(--paper); border-color: var(--ink); } +.btn--primary:hover { background: var(--forest-2); border-color: var(--forest-2); } + +.btn--emerald { background: var(--emerald); color: var(--emerald-ink); border-color: var(--emerald); } +.btn--emerald:hover { background: var(--emerald-deep); color: var(--paper); border-color: var(--emerald-deep); } + +.btn--ghost { background: transparent; border-color: transparent; box-shadow: none; color: var(--fg-muted); } +.btn--ghost:hover { background: var(--paper-3); color: var(--ink); } + +.btn--sm { padding: 5px 10px; font-size: var(--t-xs); border-radius: var(--r-sm); } +.btn--lg { padding: 12px 20px; font-size: var(--t-base); } + +/* ─── Input ───────────────────────────────────────────────── */ +.input { + display: block; width: 100%; + padding: 8px 12px; + font-family: var(--font-sans); font-size: var(--t-sm); + color: var(--ink); + background: var(--paper); + border: 1px solid var(--border); + border-radius: var(--r-md); + outline: none; + transition: border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease); +} +.input:hover { border-color: var(--border-strong); } +.input:focus { border-color: var(--emerald); box-shadow: var(--sh-focus); } +.input::placeholder { color: var(--fg-faint); } + +/* ─── Card ────────────────────────────────────────────────── */ +.card { + background: var(--paper-2); + border: 1px solid var(--border); + border-radius: var(--r-lg); + padding: var(--s-6); + box-shadow: var(--sh-xs); +} + +/* ─── Keyboard hint ───────────────────────────────────────── */ +.kbd { + display: inline-flex; align-items: center; justify-content: center; + min-width: 18px; height: 18px; padding: 0 5px; + font-family: var(--font-mono); font-size: 10.5px; + color: var(--fg-subtle); + background: var(--paper); + border: 1px solid var(--border); + border-radius: var(--r-xs); +} + +/* ─── Brand wordmark helpers ──────────────────────────────── */ +.wordmark { display: inline-flex; align-items: baseline; gap: 1px; line-height: 1; letter-spacing: var(--track-tight); } +.wordmark .wm-go { font-family: var(--font-display); font-weight: 800; color: var(--ink); } +.wordmark .wm-next { font-family: var(--font-serif); font-weight: 400; font-style: italic; color: var(--ink); } +.wordmark.on-dark .wm-go, .wordmark.on-dark .wm-next { color: var(--fg-on-forest); } diff --git a/apps/docs/src/test/landing.test.tsx b/apps/docs/src/test/landing.test.tsx new file mode 100644 index 00000000..306f8fd4 --- /dev/null +++ b/apps/docs/src/test/landing.test.tsx @@ -0,0 +1,73 @@ +/** + * Landing page — brand-contract snapshot. + * + * We pin the brand contract (display headline + italic accent + two + * CTAs + forest band + feature grid) rather than the raw HTML. The + * actual rendering is straight JSX, so the assertions read like the + * design handoff: + * + * 1. The hero headline carries the giant Archivo class hooks and an + * with the italic-accent emerald token. + * 2. There are exactly two primary CTAs — emerald "Read the docs" + * pointing into the architecture overview, and a paper-2 + * "API reference" secondary. + * 3. The forest band has its own headline (also with an italic + * accent) and exposes a card-per-subsystem feature grid. + * + * The CSS rules that turn these class names into pixels are unit- + * tested implicitly by the typecheck / build pass that compiles + * Tailwind; here we only assert the JSX contract. + */ +import { render, screen, within } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import LandingPage from '@/app/page'; + +describe('Landing page', () => { + it('renders the hero headline with the brand italic accent', () => { + render(); + + const heading = screen.getByRole('heading', { level: 1 }); + expect(heading).toHaveTextContent(/Docs that\s+grow\s+with you/); + expect(heading.className).toContain('landing__title'); + // The italic-accent rule lives inside . + const accent = heading.querySelector('em'); + expect(accent).not.toBeNull(); + expect(accent?.textContent).toBe('grow'); + }); + + it('renders both primary and secondary CTAs', () => { + render(); + + const primary = screen.getByRole('link', { name: /read the docs/i }); + expect(primary.getAttribute('href')).toBe('/docs/00-architecture-overview'); + expect(primary.className).toContain('landing__cta--primary'); + + const secondary = screen.getByRole('link', { name: /api reference/i }); + expect(secondary.getAttribute('href')).toBe('/api'); + expect(secondary.className).toContain('landing__cta--secondary'); + }); + + it('renders the forest band with its own italic-accent headline', () => { + render(); + + const bandHeading = screen.getByRole('heading', { level: 2 }); + expect(bandHeading).toHaveTextContent(/One product for everything you used\s+five\s+for/); + expect(bandHeading.className).toContain('landing__band-title'); + const accent = bandHeading.querySelector('em'); + expect(accent?.textContent).toBe('five'); + + // Subsystem grid is rendered inside the band, with one link per feature. + const band = bandHeading.closest('section'); + expect(band).not.toBeNull(); + const featureLinks = within(band as HTMLElement).getAllByRole('link'); + expect(featureLinks.length).toBeGreaterThanOrEqual(6); + }); + + it('exposes a path into the ADR list as a footnote', () => { + render(); + + const adrLink = screen.getByRole('link', { name: /architecture decision records/i }); + expect(adrLink.getAttribute('href')).toBe('/adr'); + }); +}); diff --git a/apps/docs/src/test/search-bar.test.tsx b/apps/docs/src/test/search-bar.test.tsx new file mode 100644 index 00000000..75e33ec9 --- /dev/null +++ b/apps/docs/src/test/search-bar.test.tsx @@ -0,0 +1,60 @@ +/** + * SearchBar — brand-contract snapshot. + * + * The search field is the most visible piece of chrome on every docs + * page. We pin the visual contract: + * + * 1. The field carries the `.search-bar__field` class so the paper-3 + * pill, hover, and focus states defined in styles/docs.css apply. + * 2. A Lucide search icon (svg) leads the field. + * 3. A ⌘K kbd hint trails the field. + * 4. The input is the `search` role, with the documented placeholder. + * + * Functional behaviour (fuzzy matching, dropdown opening on focus, + * keyboard navigation) is already covered by Fuse.js and our own + * unit tests over `buildSearchIndex`. Here we only guard the markup + * shape that the brand CSS depends on. + */ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; + +import { SearchBar } from '@/components/SearchBar'; +import type { SearchEntry } from '@/lib/content'; + +const ENTRIES: SearchEntry[] = [ + { section: 'docs', slug: '00-architecture-overview', title: 'Architecture Overview' }, + { section: 'adr', slug: '0001-licensing', title: 'Licensing' }, +]; + +describe('', () => { + it('renders the paper-3 pill with a Lucide icon and a kbd hint', () => { + const { container } = render(); + + // Wrapper has the .search-bar landmark. + const wrapper = container.querySelector('.search-bar'); + expect(wrapper).not.toBeNull(); + expect(wrapper?.getAttribute('role')).toBe('search'); + + // The pill itself. + const field = wrapper?.querySelector('.search-bar__field'); + expect(field).not.toBeNull(); + + // Lucide renders an with a stable class name we can spot. + const icon = field?.querySelector('svg'); + expect(icon).not.toBeNull(); + expect(icon?.classList.contains('search-bar__icon')).toBe(true); + + // ⌘K kbd hint. + const kbd = field?.querySelector('.search-bar__kbd'); + expect(kbd?.textContent).toBe('⌘K'); + }); + + it('renders an accessible search input with the docs placeholder', () => { + render(); + + const input = screen.getByRole('searchbox', { name: /search documentation/i }); + expect(input).toBeInTheDocument(); + expect(input.getAttribute('placeholder')).toMatch(/search docs/i); + expect(input.className).toContain('search-bar__input'); + }); +}); diff --git a/apps/docs/styles/docs.css b/apps/docs/styles/docs.css index 00c892c8..d465ce03 100644 --- a/apps/docs/styles/docs.css +++ b/apps/docs/styles/docs.css @@ -1,38 +1,65 @@ -/** - * Documentation site stylesheet. +/* + * @gonext/docs — global stylesheet. * - * Design constraints: - * - Reading width capped at 72ch — the published research on long-form - * reading comfort is unambiguous and our docs are dense. - * - Two-column shell on desktop, single column under 1024px. We do not - * ship a hamburger menu in v1; sidebars collapse to inline blocks. - * - Light-on-dark for code, light theme for body. We intentionally do - * not implement a theme toggle yet — the docs corpus is read more - * often in daylight settings (offices) than dark ones. - * - All colors as CSS custom properties so a dark-mode pass later is - * a single `@media (prefers-color-scheme: dark)` block. + * Loads the brand design tokens (cream-paper / forest-ink / emerald + + * lavender accents) from the shared mirror, then the Tailwind v3 + * directives, then the docs-specific shell selectors used by the + * topbar, sidebar, article body, TOC, code blocks, callouts, and pager. + * + * Architecture mirrors apps/admin/src/app/globals.css: + * 1. Import design tokens — the contract with `docs/design/`. + * 2. Tailwind base/components/utilities so JSX components can + * compose utilities directly. + * 3. Legacy aliases for the pre-brand `--color-*` variable names so + * existing class-based selectors keep resolving (and to ease the + * grep-and-replace if any third-party CSS lands). + * 4. The docs-specific class selectors (topbar, sidebar, article, + * TOC, etc.) that the React tree emits. We keep these class-based + * rather than convert every selector to Tailwind so the markdown + * renderer's output — which has no Tailwind utility classes — + * inherits the brand without rewriting the markdown pipeline. + * + * IMPORTANT: token names (`--paper`, `--emerald`, `--font-display`, + * etc.) are the contract with the design handoff. Do not redefine them + * here — change them at `src/styles/tokens.css` (or upstream in + * `docs/design/colors_and_type.css`) and let this file consume them. */ + +@import '../src/styles/tokens.css'; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* --------------------------------------------------------------------------- + * Legacy color aliases. + * + * The pre-brand scaffold used `--color-bg`, `--color-text`, etc. Keep + * them resolving to the brand tokens so any helper class that still + * references the old names picks up the right colour without a sweep. + * Prefer the brand tokens in new code. + * ------------------------------------------------------------------------- */ :root { - --color-bg: #ffffff; - --color-bg-muted: #f7f8fa; - --color-text: #1a1a1a; - --color-text-muted: #5a6068; - --color-border: #e5e7eb; - --color-link: #0b62d6; - --color-link-hover: #084ba6; - --color-accent: #0b62d6; - --color-code-bg: #0d1117; - --color-callout-note: #eef4ff; - --color-callout-warning: #fff7ed; - --color-callout-danger: #fef2f2; - --color-callout-tip: #ecfdf5; - - --font-body: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - --font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; - - --content-max: 72ch; - --sidebar-width: 260px; + --color-bg: var(--paper); + --color-bg-muted: var(--paper-2); + --color-text: var(--ink); + --color-text-muted: var(--fg-muted); + --color-border: var(--border); + --color-link: var(--emerald-deep); + --color-link-hover: var(--ink); + --color-accent: var(--emerald); + --color-code-bg: var(--forest); + --color-callout-note: var(--emerald-soft); + --color-callout-warning: var(--warning-soft); + --color-callout-danger: var(--danger-soft); + --color-callout-tip: var(--emerald-soft); + --font-body: var(--font-sans); + + /* Layout constants — match docs/design/ui_kits/docs/index.html. */ + --content-max: 760px; + --sidebar-width: 240px; --toc-width: 220px; + --topbar-height: 65px; } * { @@ -42,75 +69,169 @@ html, body { margin: 0; padding: 0; - background: var(--color-bg); - color: var(--color-text); - font-family: var(--font-body); - font-size: 16px; - line-height: 1.6; + background: var(--paper); + color: var(--ink-soft); + font-family: var(--font-sans), Geist, system-ui, -apple-system, sans-serif; + font-size: 14px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; } a { - color: var(--color-link); + color: var(--emerald-deep); text-decoration: none; } a:hover { - color: var(--color-link-hover); + color: var(--ink); text-decoration: underline; } -/* ---- App shell ---- */ +/* --------------------------------------------------------------------------- + * Topbar — sticky, blurred cream backdrop, brand wordmark + nav + search. + * + * Pixel measurements mirror docs/design/ui_kits/docs/index.html .topbar. + * The blur is a `position: sticky` + `backdrop-filter: blur(10px)` over + * a translucent paper — same trick the marketing nav uses. + * ------------------------------------------------------------------------- */ -.docs-shell { - display: grid; - grid-template-columns: var(--sidebar-width) 1fr; - min-height: 100vh; +.docs-topbar { + position: sticky; + top: 0; + z-index: 20; + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 14px 32px; + background: rgba(245, 242, 234, 0.85); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border-bottom: 1px solid var(--border); + height: var(--topbar-height); } -.docs-shell__main { +.docs-topbar__left { display: flex; - flex-direction: column; + align-items: center; + gap: 28px; min-width: 0; } -.docs-shell__header { - display: flex; +.docs-topbar__brand { + display: inline-flex; align-items: center; - gap: 24px; - padding: 12px 32px; - border-bottom: 1px solid var(--color-border); - background: var(--color-bg); - position: sticky; - top: 0; - z-index: 10; + gap: 12px; + color: var(--ink); + line-height: 1; } -.docs-shell__brand { - font-weight: 700; - font-size: 18px; +.docs-topbar__brand:hover { text-decoration: none; } + +.docs-topbar__brand-tag { + padding-left: 12px; + border-left: 1px solid var(--border); + font-size: var(--t-xs); + color: var(--fg-subtle); + letter-spacing: 0.06em; + text-transform: uppercase; + font-weight: 500; } -.docs-shell__nav { +.docs-topbar__nav { display: flex; - gap: 20px; - flex: 1; + gap: 22px; } -.docs-shell__nav-link { - color: var(--color-text); - font-size: 14px; +.docs-topbar__nav-link { + font-family: var(--font-sans), Geist, system-ui, sans-serif; + font-size: var(--t-sm); + color: var(--fg-muted); + font-weight: 500; + letter-spacing: -0.005em; + transition: color 160ms cubic-bezier(0.2, 0.7, 0.2, 1); } -.docs-shell__nav-link[data-active="true"] { - color: var(--color-accent); - font-weight: 600; +.docs-topbar__nav-link:hover { + color: var(--ink); + text-decoration: none; +} + +.docs-topbar__nav-link[data-active="true"] { + color: var(--emerald-deep); +} + +.docs-topbar__right { + display: flex; + align-items: center; + gap: 12px; +} + +.docs-topbar__version { + font-family: var(--font-mono), 'Geist Mono', ui-monospace, monospace; + font-size: 11px; + padding: 4px 10px; + background: var(--emerald-soft); + color: var(--emerald-deep); + border-radius: var(--r-pill); + font-weight: 500; + letter-spacing: 0; +} + +/* --------------------------------------------------------------------------- + * Wordmark — "Go" in Archivo 800, "Next" in Instrument Serif italic, + * no space between, baseline-aligned. Matches the brand handoff. + * ------------------------------------------------------------------------- */ + +.wordmark { + display: inline-flex; + align-items: baseline; + gap: 1px; + line-height: 1; + letter-spacing: -0.03em; +} + +.wordmark .wm-go { + font-family: var(--font-display), Archivo, system-ui, sans-serif; + font-weight: 800; + font-size: 20px; + color: inherit; +} + +.wordmark .wm-next { + font-family: var(--font-serif), 'Instrument Serif', Georgia, serif; + font-weight: 400; + font-style: italic; + font-size: 22px; + color: inherit; + margin-left: 1px; +} + +/* --------------------------------------------------------------------------- + * Docs shell — three-column layout (sidebar / article / TOC) anchored + * to the topbar. The shell is rendered by the per-section pages; this + * file only owns the visual rules. + * ------------------------------------------------------------------------- */ + +.docs-shell { + display: grid; + grid-template-columns: var(--sidebar-width) 1fr; + min-height: calc(100vh - var(--topbar-height)); + background: var(--paper); +} + +.docs-shell__main { + display: flex; + flex-direction: column; + min-width: 0; } .docs-shell__content { display: grid; grid-template-columns: 1fr var(--toc-width); - gap: 48px; - padding: 40px 32px; + gap: 64px; + padding: 40px 56px 96px; max-width: 1280px; margin: 0 auto; width: 100%; @@ -127,36 +248,47 @@ a:hover { } .docs-shell__content { grid-template-columns: 1fr; + padding: 32px 24px 64px; + gap: 32px; } .toc { display: none; } .sidebar { position: static; + height: auto; border-right: 0; - border-bottom: 1px solid var(--color-border); + border-bottom: 1px solid var(--border); } } -/* ---- Sidebar ---- */ +/* --------------------------------------------------------------------------- + * Sidebar — forest-dark surface with emerald-bright hover, matching the + * admin sidebar treatment. The handoff puts the docs sidenav on cream; + * we go forest-dark for the docs site to create the high-contrast + * "two surfaces" rhythm the brand asks for and to differentiate the + * docs reader from the marketing site. + * ------------------------------------------------------------------------- */ .sidebar { - border-right: 1px solid var(--color-border); - background: var(--color-bg-muted); - padding: 20px 12px; + background: var(--forest); + color: var(--fg-on-forest); + border-right: 1px solid var(--forest-border); + padding: 28px 20px 80px; position: sticky; - top: 0; - height: 100vh; + top: var(--topbar-height); + height: calc(100vh - var(--topbar-height)); overflow-y: auto; } .sidebar__heading { - font-size: 11px; + font-family: var(--font-display), Archivo, system-ui, sans-serif; + font-size: var(--t-xs); font-weight: 700; text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-text-muted); - padding: 0 8px 12px; + letter-spacing: 0.08em; + color: var(--emerald-bright); + padding: 0 4px 16px; } .sidebar__list { @@ -166,117 +298,225 @@ a:hover { } .sidebar__item { - padding: 2px 0; + padding: 1px 0; } .sidebar__link { display: block; - padding: 4px 8px; - border-radius: 4px; - color: var(--color-text); - font-size: 13px; + padding: 5px 8px; + border-radius: var(--r-sm); + color: var(--fg-on-forest-muted); + font-size: var(--t-sm); + line-height: 1.5; + border-left: 2px solid transparent; + transition: color 160ms cubic-bezier(0.2, 0.7, 0.2, 1), + background 160ms cubic-bezier(0.2, 0.7, 0.2, 1), + border-color 160ms cubic-bezier(0.2, 0.7, 0.2, 1); } .sidebar__link:hover { - background: rgba(11, 98, 214, 0.06); + color: var(--emerald-bright); + background: var(--forest-2); text-decoration: none; } .sidebar__link[data-active="true"] { - background: rgba(11, 98, 214, 0.12); - color: var(--color-accent); + color: var(--paper); + background: var(--forest-2); + border-left-color: var(--emerald); font-weight: 600; } +.sidebar__group { + padding: 8px 0 4px; + list-style: none; +} + .sidebar__group-title { - padding: 8px 8px 2px; - font-size: 11px; + padding: 12px 4px 6px; + font-family: var(--font-display), Archivo, system-ui, sans-serif; + font-size: var(--t-xs); font-weight: 700; - color: var(--color-text-muted); + color: var(--fg-on-forest); text-transform: uppercase; - letter-spacing: 0.04em; + letter-spacing: 0.06em; } -/* ---- Article body ---- */ +/* --------------------------------------------------------------------------- + * Article body — paper surface, Archivo for h1/h2 with italic emerald + * accents, Instrument Serif italic for blockquotes, Geist for prose, + * Geist Mono for code, Shiki dark theme tuned via the .shiki block at + * the end of this file. + * ------------------------------------------------------------------------- */ +.docs-shell__article { + counter-reset: h2-counter; +} + +.docs-shell__article > header h1, .docs-shell__article h1 { - font-size: 32px; - font-weight: 700; - margin: 0 0 8px; - letter-spacing: -0.01em; + font-family: var(--font-display), Archivo, system-ui, sans-serif; + font-weight: 800; + font-size: clamp(40px, 5vw, 56px); + letter-spacing: -0.03em; + line-height: 1; + margin: 8px 0 16px; + color: var(--ink); +} + +.docs-shell__article h1 em, +.docs-shell__article h1 i { + font-family: var(--font-serif), 'Instrument Serif', Georgia, serif; + font-weight: 400; + font-style: italic; + color: var(--emerald-deep); + font-size: 1.05em; +} + +.docs-shell__article > header > p { + font-family: var(--font-sans), Geist, system-ui, sans-serif; + font-size: 19px; + line-height: 1.55; + color: var(--fg-muted); + margin: 0 0 36px; + max-width: 620px; } .docs-shell__article h2 { - font-size: 24px; - font-weight: 600; - margin: 32px 0 12px; - padding-top: 8px; - border-top: 1px solid var(--color-border); + font-family: var(--font-display), Archivo, system-ui, sans-serif; + font-weight: 800; + font-size: 30px; + letter-spacing: -0.025em; + line-height: 1.1; + margin: 56px 0 14px; + color: var(--ink); + counter-increment: h2-counter; +} + +.docs-shell__article h2::before { + content: counter(h2-counter, decimal-leading-zero) '. '; + font-family: var(--font-serif), 'Instrument Serif', Georgia, serif; + font-weight: 400; + font-style: italic; + font-size: 0.7em; + color: var(--emerald-deep); + margin-right: 4px; +} + +.docs-shell__article h2 em, +.docs-shell__article h2 i { + font-family: var(--font-serif), 'Instrument Serif', Georgia, serif; + font-weight: 400; + font-style: italic; + color: var(--emerald-deep); + font-size: 1.05em; } .docs-shell__article h3 { - font-size: 18px; + font-family: var(--font-sans), Geist, system-ui, sans-serif; font-weight: 600; - margin: 24px 0 8px; + font-size: 19px; + letter-spacing: -0.005em; + margin: 32px 0 10px; + color: var(--ink); } .docs-shell__article h4 { - font-size: 15px; + font-family: var(--font-sans), Geist, system-ui, sans-serif; font-weight: 600; - margin: 20px 0 6px; + font-size: var(--t-lg); + margin: 24px 0 8px; + color: var(--ink); } .docs-shell__article p { - margin: 12px 0; + font-size: 16px; + line-height: 1.7; + margin: 0 0 16px; + color: var(--ink-soft); +} + +.docs-shell__article a { + color: var(--emerald-deep); + text-decoration: underline; + text-underline-offset: 3px; + text-decoration-thickness: 1px; +} + +.docs-shell__article a:hover { + color: var(--ink); } .docs-shell__article ul, .docs-shell__article ol { - padding-left: 24px; - margin: 12px 0; + padding-left: 22px; + margin: 0 0 16px; } .docs-shell__article li { - margin: 4px 0; + font-size: 16px; + line-height: 1.65; + margin-bottom: 6px; + color: var(--ink-soft); } .docs-shell__article blockquote { - border-left: 3px solid var(--color-accent); - padding: 8px 16px; - margin: 16px 0; - background: var(--color-callout-note); - color: var(--color-text); + font-family: var(--font-serif), 'Instrument Serif', Georgia, serif; + font-style: italic; + font-weight: 400; + font-size: 19px; + line-height: 1.5; + color: var(--ink-soft); + border-left: 3px solid var(--emerald); + padding: 6px 18px; + margin: 20px 0; + background: var(--paper-2); + border-radius: 0 var(--r-md) var(--r-md) 0; +} + +.docs-shell__article blockquote p { + font-family: inherit; + font-style: inherit; + font-size: inherit; + margin: 0; + color: inherit; } .docs-shell__article hr { border: 0; - border-top: 1px solid var(--color-border); - margin: 32px 0; + border-top: 1px solid var(--border); + margin: 40px 0; } -.docs-shell__article code { - font-family: var(--font-mono); - font-size: 0.9em; +.docs-shell__article p code, +.docs-shell__article li code, +.docs-shell__article td code, +.docs-shell__article a code { + font-family: var(--font-mono), 'Geist Mono', ui-monospace, monospace; + font-size: 13px; + background: var(--paper-3); + border: 1px solid var(--border); + border-radius: var(--r-xs); padding: 1px 6px; - border-radius: 4px; - background: var(--color-bg-muted); - color: var(--color-text); + color: var(--ink); } .docs-shell__article pre { - background: var(--color-code-bg); - color: #e6edf3; - border-radius: 8px; - padding: 16px 20px; + background: var(--forest); + color: var(--fg-on-forest); + border-radius: var(--r-md); + border: 1px solid var(--forest-border); + padding: 16px 18px; overflow-x: auto; - font-family: var(--font-mono); + font-family: var(--font-mono), 'Geist Mono', ui-monospace, monospace; font-size: 13px; - line-height: 1.6; - margin: 16px 0; + line-height: 1.65; + margin: 18px 0 24px; } .docs-shell__article pre code { background: transparent; + border: 0; padding: 0; color: inherit; font-size: inherit; @@ -285,56 +525,72 @@ a:hover { .docs-shell__article table { width: 100%; border-collapse: collapse; - margin: 16px 0; + margin: 20px 0; font-size: 14px; + border: 1px solid var(--border); + border-radius: var(--r-md); + overflow: hidden; } .docs-shell__article th, .docs-shell__article td { text-align: left; - padding: 8px 12px; - border-bottom: 1px solid var(--color-border); + padding: 10px 14px; + border-bottom: 1px solid var(--border-subtle); } .docs-shell__article th { - background: var(--color-bg-muted); + background: var(--paper-2); font-weight: 600; + color: var(--ink); +} + +.docs-shell__article tr:last-child td { + border-bottom: 0; } .heading-anchor { - color: var(--color-text-muted); + color: var(--fg-subtle); margin-left: 8px; opacity: 0; - transition: opacity 120ms; + transition: opacity 160ms cubic-bezier(0.2, 0.7, 0.2, 1); + text-decoration: none; + font-weight: 400; } -h1:hover .heading-anchor, -h2:hover .heading-anchor, -h3:hover .heading-anchor, -h4:hover .heading-anchor { +.docs-shell__article h1:hover .heading-anchor, +.docs-shell__article h2:hover .heading-anchor, +.docs-shell__article h3:hover .heading-anchor, +.docs-shell__article h4:hover .heading-anchor { opacity: 1; } -/* ---- Table of contents ---- */ +/* --------------------------------------------------------------------------- + * TOC — sticky right rail, paper-3 surface with emerald accent on the + * active item. Mirrors docs/design/ui_kits/docs/index.html .toc. + * ------------------------------------------------------------------------- */ .toc { position: sticky; - top: 80px; - align-self: flex-start; - padding-left: 16px; - border-left: 1px solid var(--color-border); - font-size: 13px; - max-height: calc(100vh - 100px); + top: calc(var(--topbar-height) + 40px); + align-self: start; + padding: 18px 16px; + background: var(--paper-3); + border: 1px solid var(--border); + border-radius: var(--r-lg); + max-height: calc(100vh - var(--topbar-height) - 80px); overflow-y: auto; + box-shadow: var(--sh-xs); } .toc__heading { - font-size: 11px; + font-family: var(--font-display), Archivo, system-ui, sans-serif; + font-size: var(--t-xs); font-weight: 700; text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-text-muted); - margin-bottom: 8px; + letter-spacing: 0.08em; + color: var(--ink); + margin-bottom: 12px; } .toc__list { @@ -344,62 +600,127 @@ h4:hover .heading-anchor { } .toc__item { - padding: 3px 0; + padding: 0; } -.toc__item--depth-3 { - padding-left: 12px; - font-size: 12px; +.toc__item--depth-3 .toc__link { + padding-left: 24px; + font-size: var(--t-xs); } .toc__link { - color: var(--color-text-muted); + display: block; + padding: 4px 0 4px 12px; + font-size: var(--t-sm); + color: var(--fg-muted); + line-height: 1.4; + border-left: 1px solid var(--border); + margin-left: -1px; + transition: color 160ms cubic-bezier(0.2, 0.7, 0.2, 1), + border-color 160ms cubic-bezier(0.2, 0.7, 0.2, 1); } .toc__link:hover { - color: var(--color-accent); + color: var(--ink); text-decoration: none; } -/* ---- Search ---- */ +.toc__link[data-active="true"] { + color: var(--ink); + font-weight: 500; + border-left-color: var(--emerald); +} + +/* --------------------------------------------------------------------------- + * Search — paper-3 pill with a Lucide search icon. Focused border lifts + * to border-strong; the dropdown sits below in paper-2 with the same + * soft-shadow + border treatment as other cards. + * ------------------------------------------------------------------------- */ .search-bar { position: relative; margin-left: auto; - width: 280px; + width: 320px; +} + +.search-bar__field { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + background: var(--paper-3); + border: 1px solid transparent; + border-radius: var(--r-md); + transition: background 160ms cubic-bezier(0.2, 0.7, 0.2, 1), + border-color 160ms cubic-bezier(0.2, 0.7, 0.2, 1), + box-shadow 160ms cubic-bezier(0.2, 0.7, 0.2, 1); +} + +.search-bar__field:hover { + background: var(--paper); + border-color: var(--border); +} + +.search-bar__field:focus-within { + background: var(--paper); + border-color: var(--emerald); + box-shadow: var(--sh-focus); +} + +.search-bar__icon { + width: 14px; + height: 14px; + color: var(--fg-subtle); + flex-shrink: 0; } .search-bar__input { - width: 100%; - padding: 6px 10px; - border: 1px solid var(--color-border); - border-radius: 6px; - font-size: 13px; - background: var(--color-bg); - color: var(--color-text); - font-family: inherit; + flex: 1; + border: none; + outline: none; + background: transparent; + font: inherit; + font-family: var(--font-sans), Geist, system-ui, sans-serif; + font-size: var(--t-sm); + color: var(--ink); + min-width: 0; +} + +.search-bar__input::placeholder { + color: var(--fg-faint); } -.search-bar__input:focus { - outline: 2px solid var(--color-accent); - outline-offset: -1px; +.search-bar__kbd { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + padding: 0 5px; + font-family: var(--font-mono), 'Geist Mono', ui-monospace, monospace; + font-size: 10.5px; + color: var(--fg-subtle); + background: var(--paper); + border: 1px solid var(--border); + border-radius: var(--r-xs); + flex-shrink: 0; } .search-bar__results { position: absolute; - top: calc(100% + 4px); + top: calc(100% + 6px); left: 0; right: 0; - background: var(--color-bg); - border: 1px solid var(--color-border); - border-radius: 6px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08); + background: var(--paper-2); + border: 1px solid var(--border); + border-radius: var(--r-md); + box-shadow: var(--sh-md); list-style: none; margin: 0; - padding: 4px; + padding: 6px; max-height: 320px; overflow-y: auto; - z-index: 20; + z-index: 30; } .search-bar__result { @@ -410,36 +731,44 @@ h4:hover .heading-anchor { display: flex; flex-direction: column; padding: 8px 10px; - border-radius: 4px; - color: var(--color-text); + border-radius: var(--r-sm); + color: var(--ink); + gap: 2px; } .search-bar__result-link:hover { - background: var(--color-bg-muted); + background: var(--paper-3); text-decoration: none; + color: var(--ink); } .search-bar__result-section { + font-family: var(--font-display), Archivo, system-ui, sans-serif; font-size: 10px; font-weight: 700; text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-text-muted); + letter-spacing: 0.08em; + color: var(--emerald-deep); } .search-bar__result-title { - font-size: 13px; + font-size: var(--t-sm); + color: var(--ink); } -/* ---- Page nav ---- */ +/* --------------------------------------------------------------------------- + * Page nav — prev/next pager. Two-up cards, paper-2 fill, hover lifts + * to paper-3 with a stronger border. The "Next" cell aligns right and + * picks up the same italic-accent rule as headlines for emphasis. + * ------------------------------------------------------------------------- */ .page-nav { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; - margin-top: 48px; + margin-top: 64px; padding-top: 24px; - border-top: 1px solid var(--color-border); + border-top: 1px solid var(--border); } .page-nav__cell--next { @@ -447,17 +776,23 @@ h4:hover .heading-anchor { } .page-nav__link { - display: inline-flex; + display: flex; flex-direction: column; - padding: 12px 16px; - border: 1px solid var(--color-border); - border-radius: 8px; - color: var(--color-text); + gap: 4px; + padding: 16px 18px; + background: var(--paper-2); + border: 1px solid var(--border); + border-radius: var(--r-md); + color: var(--ink); + transition: background 160ms cubic-bezier(0.2, 0.7, 0.2, 1), + border-color 160ms cubic-bezier(0.2, 0.7, 0.2, 1); } .page-nav__link:hover { - border-color: var(--color-accent); + background: var(--paper-3); + border-color: var(--border-strong); text-decoration: none; + color: var(--ink); } .page-nav__link--next { @@ -465,138 +800,488 @@ h4:hover .heading-anchor { } .page-nav__label { - font-size: 11px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-text-muted); + font-family: var(--font-sans), Geist, system-ui, sans-serif; + font-size: var(--t-xs); + color: var(--fg-subtle); } .page-nav__title { - font-size: 14px; - font-weight: 600; + font-family: var(--font-display), Archivo, system-ui, sans-serif; + font-weight: 700; + font-size: var(--t-lg); + letter-spacing: -0.02em; + margin-top: 2px; +} + +.page-nav__title em, +.page-nav__title i { + font-family: var(--font-serif), 'Instrument Serif', Georgia, serif; + font-weight: 400; + font-style: italic; + color: var(--emerald-deep); } -/* ---- Landing ---- */ +/* --------------------------------------------------------------------------- + * Landing page — giant Archivo headline with italic-accent emerald, + * Geist subtitle, two prominent CTAs. The hero is centered with an + * organic forest-glow band underneath that hosts the feature grid in + * paper-2 cards. + * ------------------------------------------------------------------------- */ .landing { - max-width: 960px; + max-width: 1200px; margin: 0 auto; - padding: 64px 32px; + padding: 64px 32px 96px; } .landing__hero { text-align: center; - padding: 24px 0 48px; + padding: 32px 0 48px; + max-width: 880px; + margin: 0 auto; +} + +.landing__eyebrow { + display: inline-flex; + align-items: center; + gap: 8px; + font-family: var(--font-sans), Geist, system-ui, sans-serif; + font-size: var(--t-xs); + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--emerald-deep); + margin-bottom: 24px; +} + +.landing__eyebrow::before { + content: ''; + width: 6px; + height: 6px; + border-radius: 999px; + background: var(--emerald); + box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.18); + animation: pulse 2.4s cubic-bezier(0.2, 0.7, 0.2, 1) infinite; +} + +@keyframes pulse { + 0%, 100% { box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.18); } + 50% { box-shadow: 0 0 0 8px rgba(16, 185, 129, 0); } } .landing__title { - font-size: 48px; + font-family: var(--font-display), Archivo, system-ui, sans-serif; font-weight: 800; - letter-spacing: -0.02em; - margin: 0 0 12px; + font-size: clamp(48px, 7vw, 96px); + letter-spacing: -0.03em; + line-height: 1; + color: var(--ink); + margin: 0 0 24px; +} + +.landing__title em, +.landing__title i { + font-family: var(--font-serif), 'Instrument Serif', Georgia, serif; + font-weight: 400; + font-style: italic; + font-size: 1.05em; + color: var(--emerald-deep); + letter-spacing: -0.01em; } .landing__subtitle { - font-size: 18px; - color: var(--color-text-muted); - margin: 0 auto 32px; - max-width: 720px; + font-family: var(--font-sans), Geist, system-ui, sans-serif; + font-size: 19px; + line-height: 1.55; + color: var(--fg-muted); + margin: 0 auto 36px; + max-width: 640px; +} + +.landing__ctas { + display: inline-flex; + flex-wrap: wrap; + gap: 12px; + justify-content: center; } .landing__cta { - display: inline-block; - padding: 12px 24px; - background: var(--color-accent); - color: white; - border-radius: 8px; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 14px 24px; + font-family: var(--font-sans), Geist, system-ui, sans-serif; font-weight: 600; + font-size: 15px; + letter-spacing: -0.005em; + border-radius: var(--r-md); + text-decoration: none; + transition: background 160ms cubic-bezier(0.2, 0.7, 0.2, 1), + border-color 160ms cubic-bezier(0.2, 0.7, 0.2, 1), + color 160ms cubic-bezier(0.2, 0.7, 0.2, 1), + box-shadow 160ms cubic-bezier(0.2, 0.7, 0.2, 1); + box-shadow: var(--sh-xs); +} + +.landing__cta--primary { + background: var(--emerald); + color: var(--emerald-ink); + border: 1px solid var(--emerald); } -.landing__cta:hover { - background: var(--color-link-hover); - color: white; +.landing__cta--primary:hover { + background: var(--emerald-deep); + border-color: var(--emerald-deep); + color: var(--paper); text-decoration: none; + box-shadow: var(--sh-md); +} + +.landing__cta--secondary { + background: var(--paper-2); + color: var(--ink); + border: 1px solid var(--border); +} + +.landing__cta--secondary:hover { + background: var(--paper-3); + border-color: var(--border-strong); + color: var(--ink); + text-decoration: none; +} + +.landing__cta-icon { + width: 16px; + height: 16px; +} + +.landing__band { + margin: 64px 0 32px; + padding: 64px 56px; + position: relative; + background: var(--forest); + color: var(--fg-on-forest); + border-radius: var(--r-xl); + overflow: hidden; + isolation: isolate; +} + +.landing__band::before, +.landing__band::after { + content: ''; + position: absolute; + width: 720px; + height: 720px; + border-radius: 999px; + pointer-events: none; + z-index: -1; + filter: blur(40px); +} + +.landing__band::before { + top: -280px; + left: -240px; + background: radial-gradient(circle, rgba(16, 185, 129, 0.18), transparent 60%); +} + +.landing__band::after { + bottom: -280px; + right: -240px; + background: radial-gradient(circle, rgba(167, 139, 250, 0.14), transparent 60%); +} + +.landing__band-eyebrow { + font-family: var(--font-sans), Geist, system-ui, sans-serif; + font-size: var(--t-xs); + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--emerald-bright); + margin-bottom: 12px; +} + +.landing__band-title { + font-family: var(--font-display), Archivo, system-ui, sans-serif; + font-weight: 800; + font-size: clamp(28px, 3.5vw, 40px); + letter-spacing: -0.025em; + line-height: 1.1; + color: var(--fg-on-forest); + margin: 0 0 8px; + max-width: 580px; +} + +.landing__band-title em, +.landing__band-title i { + font-family: var(--font-serif), 'Instrument Serif', Georgia, serif; + font-weight: 400; + font-style: italic; + color: var(--emerald-bright); +} + +.landing__band-sub { + font-family: var(--font-sans), Geist, system-ui, sans-serif; + color: var(--fg-on-forest-muted); + font-size: 16px; + max-width: 560px; + margin: 0 0 32px; } .feature-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; - margin-top: 48px; +} + +.landing__band .feature-grid { + margin-top: 16px; } .feature-card { - padding: 20px; - border: 1px solid var(--color-border); - border-radius: 12px; - background: var(--color-bg); + display: block; + padding: 24px; + border: 1px solid var(--border); + border-radius: var(--r-lg); + background: var(--paper-2); + color: var(--ink); + box-shadow: var(--sh-xs); + transition: background 160ms cubic-bezier(0.2, 0.7, 0.2, 1), + border-color 160ms cubic-bezier(0.2, 0.7, 0.2, 1), + transform 160ms cubic-bezier(0.2, 0.7, 0.2, 1), + box-shadow 160ms cubic-bezier(0.2, 0.7, 0.2, 1); +} + +.feature-card:hover { + background: var(--paper-3); + border-color: var(--border-strong); + transform: translateY(-2px); + box-shadow: var(--sh-md); + text-decoration: none; + color: var(--ink); +} + +.landing__band .feature-card { + background: var(--forest-2); + border-color: var(--forest-border); + color: var(--fg-on-forest); +} + +.landing__band .feature-card:hover { + background: var(--forest-3); + border-color: var(--emerald); + color: var(--fg-on-forest); } .feature-card__title { - font-size: 16px; - font-weight: 600; + font-family: var(--font-display), Archivo, system-ui, sans-serif; + font-weight: 700; + font-size: var(--t-lg); + letter-spacing: -0.02em; margin: 0 0 8px; + color: inherit; +} + +.feature-card__title em, +.feature-card__title i { + font-family: var(--font-serif), 'Instrument Serif', Georgia, serif; + font-weight: 400; + font-style: italic; + color: var(--emerald-deep); +} + +.landing__band .feature-card__title em, +.landing__band .feature-card__title i { + color: var(--emerald-bright); } .feature-card__body { font-size: 14px; - color: var(--color-text-muted); + line-height: 1.55; + color: var(--fg-muted); margin: 0; } -/* ---- Callout ---- */ +.landing__band .feature-card__body { + color: var(--fg-on-forest-muted); +} + +.landing__footnote { + text-align: center; + margin-top: 48px; + font-size: 14px; + color: var(--fg-muted); +} + +/* --------------------------------------------------------------------------- + * Callout — emerald-tinted info box. Warning variant swaps to the + * warning tokens. Mirrors the .callout used in the design handoff. + * ------------------------------------------------------------------------- */ .callout { display: flex; - gap: 12px; - padding: 12px 16px; - border-radius: 8px; - margin: 16px 0; - border: 1px solid var(--color-border); + gap: 14px; + padding: 16px 18px; + border-radius: var(--r-md); + margin: 18px 0 24px; + border: 1px solid transparent; +} + +.callout--note, +.callout--tip { + background: var(--emerald-soft); + border-left: 3px solid var(--emerald); +} + +.callout--warning { + background: var(--warning-soft); + border-left: 3px solid var(--warning); } -.callout--note { background: var(--color-callout-note); } -.callout--tip { background: var(--color-callout-tip); } -.callout--warning { background: var(--color-callout-warning); } -.callout--danger { background: var(--color-callout-danger); } +.callout--danger { + background: var(--danger-soft); + border-left: 3px solid var(--danger); +} .callout__icon { - font-weight: 700; - width: 20px; - text-align: center; + font-family: var(--font-display), Archivo, system-ui, sans-serif; + font-weight: 800; + width: 24px; + height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + background: var(--ink); + color: var(--paper); + font-size: 12px; flex-shrink: 0; } +.callout--warning .callout__icon { background: var(--warning); } +.callout--danger .callout__icon { background: var(--danger); } + .callout__title { - font-weight: 600; - margin-bottom: 4px; + font-family: var(--font-display), Archivo, system-ui, sans-serif; + font-size: var(--t-xs); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--emerald-deep); + margin-bottom: 6px; +} + +.callout--warning .callout__title { color: var(--warning); } +.callout--danger .callout__title { color: var(--danger); } + +.callout__content { + font-size: var(--t-sm); + line-height: 1.55; + color: var(--ink-soft); } -/* ---- Code block (MDX wrapper) ---- */ +.callout__content p { margin: 0; } +.callout__content p + p { margin-top: 6px; } + +/* --------------------------------------------------------------------------- + * Code block wrapper (MDX) — forest header bar with language label and + * a copy button. Mirrors docs/design/ui_kits/docs/index.html pre.code. + * ------------------------------------------------------------------------- */ .code-block { - margin: 16px 0; - border-radius: 8px; + margin: 18px 0 24px; + border-radius: var(--r-md); overflow: hidden; - border: 1px solid var(--color-border); + border: 1px solid var(--forest-border); + background: var(--forest); } .code-block__bar { display: flex; align-items: center; justify-content: space-between; - padding: 6px 12px; - background: var(--color-bg-muted); - font-size: 12px; - color: var(--color-text-muted); + padding: 8px 14px; + background: var(--forest-2); + border-bottom: 1px solid var(--forest-border); + font-family: var(--font-mono), 'Geist Mono', ui-monospace, monospace; + font-size: var(--t-xs); + color: var(--fg-on-forest-muted); + letter-spacing: 0.04em; +} + +.code-block__filename { + color: var(--emerald-bright); + font-weight: 500; } .code-block__copy { - border: 1px solid var(--color-border); - background: var(--color-bg); - padding: 2px 10px; - border-radius: 4px; - font-size: 12px; + background: transparent; + border: 1px solid var(--forest-border); + border-radius: var(--r-sm); + padding: 3px 10px; + color: var(--fg-on-forest); + font: inherit; cursor: pointer; + font-size: 11px; + transition: background 160ms cubic-bezier(0.2, 0.7, 0.2, 1), + border-color 160ms cubic-bezier(0.2, 0.7, 0.2, 1); +} + +.code-block__copy:hover { + background: var(--forest-3); + border-color: var(--emerald); +} + +.code-block__body { + padding: 0; +} + +.code-block__body pre { + margin: 0; + border-radius: 0; + border: 0; +} + +/* --------------------------------------------------------------------------- + * Shiki dark theme tuning. + * + * Shiki renders `github-dark` token classes that mostly look fine on + * the forest surface; we re-tone a handful of token kinds toward the + * brand palette (emerald for keywords, lavender for variables, warm + * amber for strings) so code blocks inherit the brand without + * swapping the whole theme. Selectors are scoped to `.shiki` so they + * don't bleed into regular `
    ` content.
    + * ------------------------------------------------------------------------- */
    +
    +.docs-shell__article pre.shiki,
    +.code-block .shiki {
    +  background: var(--forest) !important;
    +  color: var(--fg-on-forest);
    +}
    +
    +.shiki .line { display: block; }
    +
    +/* Keywords, control flow → emerald-bright */
    +.shiki .token.keyword,
    +.shiki .token.storage,
    +.shiki .token.tag {
    +  color: var(--emerald-bright);
    +}
    +
    +/* Variables, identifiers → lavender */
    +.shiki .token.variable,
    +.shiki .token.parameter {
    +  color: var(--lavender);
    +}
    +
    +/* Strings → warm amber so they read clearly on the deep forest */
    +.shiki .token.string,
    +.shiki .token.attr-value {
    +  color: #FFD173;
    +}
    +
    +/* Comments → muted forest-foreground */
    +.shiki .token.comment,
    +.shiki .token.prolog {
    +  color: var(--fg-on-forest-muted);
    +  font-style: italic;
     }
    diff --git a/apps/docs/tailwind.config.ts b/apps/docs/tailwind.config.ts
    new file mode 100644
    index 00000000..d5f812d4
    --- /dev/null
    +++ b/apps/docs/tailwind.config.ts
    @@ -0,0 +1,146 @@
    +/**
    + * Tailwind v3 configuration for @gonext/docs.
    + *
    + * Mirrors the admin app's Tailwind extension so the same Living-Systems
    + * tokens (cream paper, forest ink, emerald + lavender) resolve to the
    + * same Tailwind utilities. The single source of truth for values is
    + * `docs/design/colors_and_type.css`; the mirror lives at
    + * `src/styles/tokens.css`. When those change, this file must change too.
    + *
    + * Font families resolve to the CSS custom properties that
    + * `next/font/google` injects on the  element from
    + * `app/layout.tsx`. The fallback chain inside each entry is what
    + * appears before the self-hosted face is ready, and in JSDOM tests
    + * where next/font is not running.
    + */
    +import type { Config } from 'tailwindcss';
    +
    +const config: Config = {
    +  content: [
    +    './app/**/*.{ts,tsx,mdx}',
    +    './components/**/*.{ts,tsx}',
    +    './lib/**/*.{ts,tsx}',
    +    './src/**/*.{ts,tsx}',
    +  ],
    +  theme: {
    +    extend: {
    +      colors: {
    +        paper: {
    +          DEFAULT: '#F5F2EA',
    +          '2': '#EFEBE0',
    +          '3': '#E6E1D2',
    +          '4': '#DAD3BD',
    +        },
    +        forest: {
    +          DEFAULT: '#0E1A14',
    +          '2': '#18261E',
    +          '3': '#22322A',
    +          border: '#2C3D33',
    +        },
    +        ink: {
    +          DEFAULT: '#0E1A14',
    +          soft: '#1F2D26',
    +        },
    +        fg: {
    +          muted: '#4A5C52',
    +          subtle: '#6B7B72',
    +          faint: '#94A199',
    +          'on-forest': '#F0EAD8',
    +          'on-forest-muted': '#A8B5AC',
    +        },
    +        border: {
    +          DEFAULT: '#D9D2C0',
    +          strong: '#B8B09A',
    +          subtle: '#E8E2D1',
    +        },
    +        emerald: {
    +          DEFAULT: '#10B981',
    +          bright: '#34D399',
    +          deep: '#047857',
    +          soft: '#D1FAE5',
    +          ink: '#022C22',
    +        },
    +        lavender: {
    +          DEFAULT: '#A78BFA',
    +          deep: '#7C3AED',
    +          soft: '#EDE9FE',
    +        },
    +        success: {
    +          DEFAULT: '#059669',
    +          soft: '#D1FAE5',
    +        },
    +        warning: {
    +          DEFAULT: '#D97706',
    +          soft: '#FEF3C7',
    +        },
    +        danger: {
    +          DEFAULT: '#DC2626',
    +          soft: '#FEE2E2',
    +        },
    +      },
    +      fontFamily: {
    +        display: ['var(--font-display)', 'Archivo', 'system-ui', 'sans-serif'],
    +        sans: ['var(--font-sans)', 'Geist', 'system-ui', 'sans-serif'],
    +        serif: ['var(--font-serif)', 'Instrument Serif', 'Georgia', 'serif'],
    +        mono: ['var(--font-mono)', 'Geist Mono', 'ui-monospace', 'monospace'],
    +      },
    +      fontSize: {
    +        '2xs': ['11px', { lineHeight: '1.4' }],
    +        xs: ['12px', { lineHeight: '1.4' }],
    +        sm: ['13px', { lineHeight: '1.5' }],
    +        base: ['14px', { lineHeight: '1.5' }],
    +        md: ['15px', { lineHeight: '1.5' }],
    +        lg: ['17px', { lineHeight: '1.4' }],
    +        xl: ['20px', { lineHeight: '1.4' }],
    +        '2xl': ['24px', { lineHeight: '1.2' }],
    +        '3xl': ['32px', { lineHeight: '1.15' }],
    +        '4xl': ['44px', { lineHeight: '1.05' }],
    +        '5xl': ['64px', { lineHeight: '1.0' }],
    +        '6xl': ['96px', { lineHeight: '1.0' }],
    +      },
    +      letterSpacing: {
    +        tight: '-0.03em',
    +        normal: '-0.005em',
    +        wide: '0.04em',
    +      },
    +      spacing: {
    +        '1': '4px',
    +        '2': '8px',
    +        '3': '12px',
    +        '4': '16px',
    +        '5': '20px',
    +        '6': '24px',
    +        '7': '32px',
    +        '8': '48px',
    +        '9': '64px',
    +        '10': '96px',
    +      },
    +      borderRadius: {
    +        xs: '4px',
    +        sm: '6px',
    +        md: '8px',
    +        lg: '12px',
    +        xl: '16px',
    +        pill: '999px',
    +      },
    +      boxShadow: {
    +        xs: '0 1px 2px rgba(14, 26, 20, 0.04)',
    +        sm: '0 1px 3px rgba(14, 26, 20, 0.06), 0 1px 2px rgba(14, 26, 20, 0.04)',
    +        md: '0 6px 14px -4px rgba(14, 26, 20, 0.08), 0 2px 6px -2px rgba(14, 26, 20, 0.04)',
    +        lg: '0 16px 32px -10px rgba(14, 26, 20, 0.14), 0 4px 10px -4px rgba(14, 26, 20, 0.06)',
    +        focus: '0 0 0 3px rgba(16, 185, 129, 0.22)',
    +      },
    +      transitionTimingFunction: {
    +        brand: 'cubic-bezier(0.2, 0.7, 0.2, 1)',
    +      },
    +      transitionDuration: {
    +        fast: '100ms',
    +        DEFAULT: '160ms',
    +        slow: '260ms',
    +      },
    +    },
    +  },
    +  plugins: [],
    +};
    +
    +export default config;
    diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
    index ac4cbfc4..df1bc12e 100644
    --- a/pnpm-lock.yaml
    +++ b/pnpm-lock.yaml
    @@ -141,6 +141,9 @@ importers:
           gray-matter:
             specifier: ^4.0.3
             version: 4.0.3
    +      lucide-react:
    +        specifier: ^0.469.0
    +        version: 0.469.0(react@19.2.6)
           next:
             specifier: ^15.0.0
             version: 15.5.18(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
    @@ -187,6 +190,9 @@ importers:
           '@vitest/coverage-v8':
             specifier: ^1.6.0
             version: 1.6.1(vitest@1.6.1(@types/node@22.19.19)(jsdom@24.1.3))
    +      autoprefixer:
    +        specifier: ^10.4.20
    +        version: 10.5.0(postcss@8.5.14)
           eslint:
             specifier: ^8.57.0
             version: 8.57.1
    @@ -196,6 +202,12 @@ importers:
           jsdom:
             specifier: ^24.0.0
             version: 24.1.3
    +      postcss:
    +        specifier: ^8.4.49
    +        version: 8.5.14
    +      tailwindcss:
    +        specifier: ^3.4.17
    +        version: 3.4.19(tsx@4.22.2)
           typescript:
             specifier: ^5.6.0
             version: 5.9.3