+```
+
+### 4. Test All Breakpoints
+
+Create systematic tests for all responsive layouts to ensure they work at every breakpoint.
diff --git a/.agents/skills/tailwindcss/GENERATION.md b/.agents/skills/tailwindcss/GENERATION.md
new file mode 100644
index 0000000..2a7b051
--- /dev/null
+++ b/.agents/skills/tailwindcss/GENERATION.md
@@ -0,0 +1,5 @@
+# Generation Info
+
+- **Source:** `sources/tailwindcss`
+- **Git SHA:** `148fcb26ab87892177ec9a7eba4b31e2ee960039`
+- **Generated:** 2026-01-28
diff --git a/.agents/skills/tailwindcss/SKILL.md b/.agents/skills/tailwindcss/SKILL.md
new file mode 100644
index 0000000..0629455
--- /dev/null
+++ b/.agents/skills/tailwindcss/SKILL.md
@@ -0,0 +1,150 @@
+---
+name: tailwindcss
+description: Tailwind CSS utility-first CSS framework. Use when styling web applications with utility classes, building responsive designs, or customizing design systems with theme variables.
+metadata:
+ author: Hairyf
+ version: "2026.2.2"
+ source: Generated from https://github.com/tailwindlabs/tailwindcss.com, scripts located at https://github.com/hairyf/skills
+---
+
+# Tailwind CSS
+
+> The skill is based on Tailwind CSS v4.1.18, generated at 2026-01-28.
+
+Tailwind CSS is a utility-first CSS framework for rapidly building custom user interfaces. Instead of writing custom CSS, you compose designs using utility classes directly in your markup. Tailwind v4 introduces CSS-first configuration with theme variables, making it easier to customize your design system.
+
+## Core References
+
+| Topic | Description | Reference |
+|-------|-------------|-----------|
+| Installation | Vite, PostCSS, CLI, and CDN setup | [core-installation](references/core-installation.md) |
+| Utility Classes | Understanding Tailwind's utility-first approach and styling elements | [core-utility-classes](references/core-utility-classes.md) |
+| Theme Variables | Design tokens, customizing theme, and theme variable namespaces | [core-theme](references/core-theme.md) |
+| Responsive Design | Mobile-first breakpoints, responsive variants, and container queries | [core-responsive](references/core-responsive.md) |
+| Variants | Applying utilities conditionally with state, pseudo-class, and media query variants | [core-variants](references/core-variants.md) |
+| Preflight | Tailwind's base styles and how to extend or disable them | [core-preflight](references/core-preflight.md) |
+
+## Layout
+
+### Display & Flexbox & Grid
+
+| Topic | Description | Reference |
+|-------|-------------|-----------|
+| Display | flex, grid, block, inline, hidden, sr-only, flow-root, contents | [layout-display](references/layout-display.md) |
+| Flexbox | flex-direction, justify, items, gap, grow, shrink, wrap, order | [layout-flexbox](references/layout-flexbox.md) |
+| Grid | grid-cols, grid-rows, gap, place-items, col-span, row-span, subgrid | [layout-grid](references/layout-grid.md) |
+| Aspect Ratio | Controlling element aspect ratio for responsive media | [layout-aspect-ratio](references/layout-aspect-ratio.md) |
+| Columns | Multi-column layout for magazine-style or masonry layouts | [layout-columns](references/layout-columns.md) |
+
+### Positioning
+
+| Topic | Description | Reference |
+|-------|-------------|-----------|
+| Position | Controlling element positioning with static, relative, absolute, fixed, and sticky | [layout-position](references/layout-position.md) |
+| Inset | Controlling placement of positioned elements with top, right, bottom, left, and inset utilities | [layout-inset](references/layout-inset.md) |
+
+### Sizing
+
+| Topic | Description | Reference |
+|-------|-------------|-----------|
+| Width | Setting element width with spacing scale, fractions, container sizes, and viewport units | [layout-width](references/layout-width.md) |
+| Height | Setting element height with spacing scale, fractions, viewport units, and content-based sizing | [layout-height](references/layout-height.md) |
+| Min & Max Sizing | min-width, max-width, min-height, max-height constraints | [layout-min-max-sizing](references/layout-min-max-sizing.md) |
+
+### Spacing
+
+| Topic | Description | Reference |
+|-------|-------------|-----------|
+| Margin | Controlling element margins with spacing scale, negative values, logical properties, and space utilities | [layout-margin](references/layout-margin.md) |
+| Padding | Controlling element padding with spacing scale, logical properties, and directional utilities | [layout-padding](references/layout-padding.md) |
+
+### Overflow
+
+| Topic | Description | Reference |
+|-------|-------------|-----------|
+| Overflow | Controlling how elements handle content that overflows their container | [layout-overflow](references/layout-overflow.md) |
+
+### Images & Replaced Elements
+
+| Topic | Description | Reference |
+|-------|-------------|-----------|
+| Object Fit & Position | Controlling how images and video are resized and positioned | [layout-object-fit-position](references/layout-object-fit-position.md) |
+
+### Tables
+
+| Topic | Description | Reference |
+|-------|-------------|-----------|
+| Table Layout | border-collapse, table-auto, table-fixed | [layout-tables](references/layout-tables.md) |
+
+## Transforms
+
+| Topic | Description | Reference |
+|-------|-------------|-----------|
+| Transform Base | Base transform utilities for enabling transforms, hardware acceleration, and custom transform values | [transform-base](references/transform-base.md) |
+| Translate | Translating elements on x, y, and z axes with spacing scale, percentages, and custom values | [transform-translate](references/transform-translate.md) |
+| Rotate | Rotating elements in 2D and 3D space with degree values and custom rotations | [transform-rotate](references/transform-rotate.md) |
+| Scale | Scaling elements uniformly or on specific axes with percentage values | [transform-scale](references/transform-scale.md) |
+| Skew | Skewing elements on x and y axes with degree values | [transform-skew](references/transform-skew.md) |
+
+## Typography
+
+| Topic | Description | Reference |
+|-------|-------------|-----------|
+| Font & Text | Font size, weight, color, line-height, letter-spacing, decoration, truncate | [typography-font-text](references/typography-font-text.md) |
+| Text Align | Controlling text alignment with left, center, right, justify, and logical properties | [typography-text-align](references/typography-text-align.md) |
+| List Style | list-style-type, list-style-position for bullets and markers | [typography-list-style](references/typography-list-style.md) |
+
+## Visual
+
+| Topic | Description | Reference |
+|-------|-------------|-----------|
+| Background | Background color, gradient, image, size, position | [visual-background](references/visual-background.md) |
+| Border | Border width, color, radius, divide, ring | [visual-border](references/visual-border.md) |
+| Effects | Box shadow, opacity, mix-blend, backdrop-blur, filter | [visual-effects](references/visual-effects.md) |
+| SVG | fill, stroke, stroke-width for SVG and icon styling | [visual-svg](references/visual-svg.md) |
+
+## Effects & Interactivity
+
+| Topic | Description | Reference |
+|-------|-------------|-----------|
+| Transition & Animation | CSS transitions, animation keyframes, reduced motion | [effects-transition-animation](references/effects-transition-animation.md) |
+| Visibility & Interactivity | Visibility, cursor, pointer-events, user-select, z-index | [effects-visibility-interactivity](references/effects-visibility-interactivity.md) |
+| Form Controls | accent-color, appearance, caret-color, resize | [effects-form-controls](references/effects-form-controls.md) |
+| Scroll Snap | scroll-snap-type, scroll-snap-align for carousels | [effects-scroll-snap](references/effects-scroll-snap.md) |
+
+## Features
+
+### Dark Mode
+
+| Topic | Description | Reference |
+|-------|-------------|-----------|
+| Dark Mode | Implementing dark mode with the dark variant and custom strategies | [features-dark-mode](references/features-dark-mode.md) |
+
+### Migration
+
+| Topic | Description | Reference |
+|-------|-------------|-----------|
+| Upgrade Guide | Migrating from v3 to v4, breaking changes, rename mappings | [features-upgrade](references/features-upgrade.md) |
+
+### Customization
+
+| Topic | Description | Reference |
+|-------|-------------|-----------|
+| Custom Styles | Adding custom styles, utilities, variants, and working with arbitrary values | [features-custom-styles](references/features-custom-styles.md) |
+| Functions & Directives | Tailwind's CSS directives and functions for working with your design system | [features-functions-directives](references/features-functions-directives.md) |
+| Content Detection | How Tailwind detects classes and how to customize content scanning | [features-content-detection](references/features-content-detection.md) |
+
+## Best Practices
+
+| Topic | Description | Reference |
+|-------|-------------|-----------|
+| Utility Patterns | Managing duplication, conflicts, important modifier, when to use components | [best-practices-utility-patterns](references/best-practices-utility-patterns.md) |
+
+## Key Recommendations
+
+- **Use utility classes directly in markup** - Compose designs by combining utilities
+- **Customize with theme variables** - Use `@theme` directive to define design tokens
+- **Mobile-first responsive design** - Use unprefixed utilities for mobile, prefixed for breakpoints
+- **Use complete class names** - Never construct classes dynamically with string interpolation
+- **Leverage variants** - Stack variants for complex conditional styling
+- **Prefer CSS-first configuration** - Use `@theme`, `@utility`, and `@custom-variant` over JavaScript configs
diff --git a/.agents/skills/tailwindcss/references/best-practices-utility-patterns.md b/.agents/skills/tailwindcss/references/best-practices-utility-patterns.md
new file mode 100644
index 0000000..b0c0783
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/best-practices-utility-patterns.md
@@ -0,0 +1,87 @@
+---
+name: best-practices-utility-patterns
+description: Managing duplication, conflicts, important modifier, and when to use components
+---
+
+# Best Practices: Utility Patterns
+
+Practical patterns when building with Tailwind utilities.
+
+## Managing duplication
+
+**Use components** for repeated UI: Extract into React/Vue/Svelte components or template partials.
+
+```jsx
+function Button({ children, variant = 'primary' }) {
+ const base = 'px-4 py-2 rounded-lg font-medium'
+ const variants = {
+ primary: 'bg-blue-500 hover:bg-blue-600 text-white',
+ secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-900',
+ }
+ return
+}
+```
+
+**Use loops** when markup is repeated from data—the class list is written once.
+
+**Multi-cursor editing** for localized duplication in a single file.
+
+## Managing conflicts
+
+When two utilities target the same property, the one **later in the stylesheet** wins. Avoid adding conflicting classes:
+
+```html
+
+
+```
+
+## Important modifier
+
+Add `!` suffix to force `!important`:
+
+```html
+
Red wins
+```
+
+Use sparingly; prefer fixing specificity properly.
+
+## Important flag (global)
+
+```css
+@import "tailwindcss" important;
+```
+
+Makes all utilities `!important`. Useful when integrating into existing high-specificity CSS.
+
+## Prefix option
+
+```css
+@import "tailwindcss" prefix(tw);
+```
+
+Generates `tw:text-red-500` etc. Use when project class names conflict with Tailwind.
+
+## When to use inline styles
+
+- Values from API/database (e.g. brand colors)
+- Dynamic values that change at runtime
+- Complex arbitrary values hard to read as class names
+- Pattern: set CSS variables via inline style, reference with `bg-(--my-var)`
+
+## Key Points
+
+- Extract repeated patterns into components
+- Never add two conflicting utilities—use conditional classes
+- `!` suffix = single utility `!important`
+- `important` flag = all utilities `!important`
+- `prefix(tw)` = prefix all utilities
+- Use inline styles for dynamic values; utilities for static design
+
+
diff --git a/.agents/skills/tailwindcss/references/core-installation.md b/.agents/skills/tailwindcss/references/core-installation.md
new file mode 100644
index 0000000..d016fea
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/core-installation.md
@@ -0,0 +1,89 @@
+---
+name: core-installation
+description: Installing Tailwind CSS with Vite, PostCSS, CLI, or CDN
+---
+
+# Installation
+
+How to add Tailwind CSS to a project.
+
+## Vite (Recommended)
+
+```bash
+npm install tailwindcss @tailwindcss/vite
+```
+
+```ts
+// vite.config.ts
+import { defineConfig } from 'vite'
+import tailwindcss from '@tailwindcss/vite'
+
+export default defineConfig({
+ plugins: [tailwindcss()],
+})
+```
+
+```css
+/* style.css */
+@import "tailwindcss";
+```
+
+## PostCSS
+
+```bash
+npm install tailwindcss @tailwindcss/postcss postcss
+```
+
+```js
+// postcss.config.js
+export default {
+ plugins: {
+ '@tailwindcss/postcss': {},
+ },
+}
+```
+
+```css
+@import "tailwindcss";
+```
+
+## Tailwind CLI
+
+```bash
+npx @tailwindcss/cli -i ./src/input.css -o ./dist/output.css --watch
+```
+
+```css
+/* input.css */
+@import "tailwindcss";
+```
+
+## Play CDN (Development only)
+
+```html
+
+```
+
+Not for production: no purging, larger payload.
+
+## Framework guides
+
+- **Nuxt**: `@nuxtjs/tailwindcss` module or `@tailwindcss/vite`
+- **Next.js**: Use Vite or PostCSS with `tailwind.config.js` if needed
+- **React Router / SvelteKit / SolidJS**: Use `@tailwindcss/vite`
+
+## Key Points
+
+- Vite: `@tailwindcss/vite` plugin + `@import "tailwindcss"`
+- PostCSS: `@tailwindcss/postcss`
+- CLI: `npx @tailwindcss/cli`
+- v4 uses CSS-first config; no `tailwind.config.js` required for basics
+- Use `@theme` in CSS to customize design tokens
+
+
diff --git a/.agents/skills/tailwindcss/references/core-preflight.md b/.agents/skills/tailwindcss/references/core-preflight.md
new file mode 100644
index 0000000..bab6d82
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/core-preflight.md
@@ -0,0 +1,200 @@
+---
+name: core-preflight
+description: Understanding Tailwind's Preflight base styles and how to extend or disable them
+---
+
+# Preflight
+
+Preflight is Tailwind's opinionated set of base styles that smooth over cross-browser inconsistencies.
+
+## Overview
+
+Built on top of modern-normalize, Preflight is automatically injected when you import `tailwindcss`:
+
+```css
+@layer theme, base, components, utilities;
+
+@import "tailwindcss/theme.css" layer(theme);
+@import "tailwindcss/preflight.css" layer(base);
+@import "tailwindcss/utilities.css" layer(utilities);
+```
+
+## Key Resets
+
+### Margins Removed
+
+All default margins are removed from headings, paragraphs, blockquotes, etc:
+
+```css
+*,
+::after,
+::before {
+ margin: 0;
+ padding: 0;
+}
+```
+
+This prevents accidentally relying on browser default margins that aren't part of your spacing scale.
+
+### Border Styles Reset
+
+Borders are reset to make adding borders easier:
+
+```css
+*,
+::after,
+::before {
+ box-sizing: border-box;
+ border: 0 solid;
+}
+```
+
+Since the `border` utility only sets `border-width`, this ensures adding `border` always creates a solid `1px` border using `currentColor`.
+
+### Headings Unstyled
+
+All headings are unstyled by default:
+
+```css
+h1, h2, h3, h4, h5, h6 {
+ font-size: inherit;
+ font-weight: inherit;
+}
+```
+
+**Reasons:**
+- Avoids deviating from your type scale
+- In UI development, headings should often be visually de-emphasized
+
+### Lists Unstyled
+
+Ordered and unordered lists have no bullets or numbers:
+
+```css
+ol, ul, menu {
+ list-style: none;
+}
+```
+
+Style lists using utilities:
+
+```html
+
+```
+
+**Accessibility:** Add `role="list"` for screen readers when keeping lists unstyled:
+
+```html
+
+```
+
+### Images Are Block-Level
+
+Images and replaced elements are `display: block`:
+
+```css
+img, svg, video, canvas, audio, iframe, embed, object {
+ display: block;
+ vertical-align: middle;
+}
+```
+
+Use `inline` utility if needed:
+
+```html
+
+```
+
+### Images Are Constrained
+
+Images and videos are constrained to parent width:
+
+```css
+img, video {
+ max-width: 100%;
+ height: auto;
+}
+```
+
+Override with `max-w-none`:
+
+```html
+
+```
+
+### Hidden Attribute
+
+Elements with `hidden` attribute stay hidden:
+
+```css
+[hidden]:where(:not([hidden="until-found"])) {
+ display: none !important;
+}
+```
+
+## Extending Preflight
+
+Add base styles to the `base` layer:
+
+```css
+@layer base {
+ h1 {
+ font-size: var(--text-2xl);
+ font-weight: 600;
+ }
+
+ h2 {
+ font-size: var(--text-xl);
+ font-weight: 600;
+ }
+
+ a {
+ color: var(--color-blue-600);
+ text-decoration-line: underline;
+ }
+}
+```
+
+## Disabling Preflight
+
+Import Tailwind components individually, omitting Preflight:
+
+```css
+@layer theme, base, components, utilities;
+
+@import "tailwindcss/theme.css" layer(theme);
+/* @import "tailwindcss/preflight.css" layer(base); */ /* Omitted */
+@import "tailwindcss/utilities.css" layer(utilities);
+```
+
+## Working Around Third-Party Libraries
+
+Some libraries may conflict with Preflight. Override Preflight styles:
+
+```css
+@layer base {
+ .google-map * {
+ border-style: none;
+ }
+}
+```
+
+## Key Points
+
+- Preflight normalizes cross-browser inconsistencies
+- Margins, borders, headings, and lists are reset
+- Images are block-level and constrained by default
+- Extend Preflight with `@layer base`
+- Disable by omitting the preflight import
+- Override Preflight styles when needed for third-party libraries
+
+
diff --git a/.agents/skills/tailwindcss/references/core-responsive.md b/.agents/skills/tailwindcss/references/core-responsive.md
new file mode 100644
index 0000000..969ca2c
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/core-responsive.md
@@ -0,0 +1,163 @@
+---
+name: core-responsive
+description: Building responsive designs with Tailwind's mobile-first breakpoint system and container queries
+---
+
+# Responsive Design
+
+Every utility class in Tailwind can be applied conditionally at different breakpoints using responsive variants.
+
+## Mobile-First Breakpoints
+
+Tailwind uses a mobile-first approach. Unprefixed utilities apply to all screen sizes, while prefixed utilities apply at that breakpoint and above.
+
+```html
+
+
Content
+```
+
+## Default Breakpoints
+
+| Breakpoint | Minimum Width | CSS |
+|------------|---------------|-----|
+| `sm` | 40rem (640px) | `@media (width >= 40rem)` |
+| `md` | 48rem (768px) | `@media (width >= 48rem)` |
+| `lg` | 64rem (1024px) | `@media (width >= 64rem)` |
+| `xl` | 80rem (1280px) | `@media (width >= 80rem)` |
+| `2xl` | 96rem (1536px) | `@media (width >= 96rem)` |
+
+## Usage
+
+Prefix any utility with a breakpoint name:
+
+```html
+
+
+```
+
+## Example: Responsive Card
+
+```html
+
+
+
+
+
+
+
+
+```
+
+## Targeting Mobile Screens
+
+Use unprefixed utilities for mobile, not `sm:`. Think of `sm:` as "at the small breakpoint", not "on small screens".
+
+```html
+
+
+
+
+
+```
+
+## Breakpoint Ranges
+
+Target a specific breakpoint range by stacking responsive variants with `max-*` variants:
+
+```html
+
+
Content
+```
+
+## Max-Width Variants
+
+Tailwind generates `max-*` variants for each breakpoint:
+
+| Variant | Media Query |
+|---------|-------------|
+| `max-sm` | `@media (width < 40rem)` |
+| `max-md` | `@media (width < 48rem)` |
+| `max-lg` | `@media (width < 64rem)` |
+| `max-xl` | `@media (width < 80rem)` |
+| `max-2xl` | `@media (width < 96rem)` |
+
+## Custom Breakpoints
+
+Add custom breakpoints using `--breakpoint-*` theme variables:
+
+```css
+@theme {
+ --breakpoint-xs: 30rem;
+ --breakpoint-3xl: 120rem;
+}
+```
+
+```html
+
Content
+```
+
+## Arbitrary Breakpoints
+
+Use arbitrary values for one-off breakpoints:
+
+```html
+
+ Content
+
+```
+
+## Container Queries
+
+Style elements based on parent container size instead of viewport:
+
+```html
+
+
+```
+
+### Container Query Variants
+
+| Variant | Minimum Width |
+|---------|---------------|
+| `@3xs` | 16rem (256px) |
+| `@xs` | 20rem (320px) |
+| `@sm` | 24rem (384px) |
+| `@md` | 28rem (448px) |
+| `@lg` | 32rem (512px) |
+| `@xl` | 36rem (576px) |
+| `@2xl` | 42rem (672px) |
+| `@3xl` | 48rem (768px) |
+| ... up to `@7xl` | 80rem (1280px) |
+
+### Named Containers
+
+Name containers to target specific ones in nested structures:
+
+```html
+
+```
+
+## Key Points
+
+- Mobile-first: unprefixed = mobile, prefixed = breakpoint and up
+- Use unprefixed utilities for mobile, not `sm:`
+- Stack variants for complex responsive behavior
+- Container queries enable component-based responsive design
+- Customize breakpoints with theme variables
+
+
diff --git a/.agents/skills/tailwindcss/references/core-theme.md b/.agents/skills/tailwindcss/references/core-theme.md
new file mode 100644
index 0000000..dd54cc7
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/core-theme.md
@@ -0,0 +1,108 @@
+---
+name: core-theme
+description: Understanding theme variables, design tokens, and customizing Tailwind's default theme
+---
+
+# Theme Variables
+
+Theme variables are special CSS variables defined using the `@theme` directive that influence which utility classes exist in your project.
+
+## Overview
+
+Theme variables store design tokens like colors, fonts, spacing, shadows, and breakpoints. They're defined using the `@theme` directive:
+
+```css
+@import "tailwindcss";
+
+@theme {
+ --color-mint-500: oklch(0.72 0.11 178);
+ --font-display: "Satoshi", "sans-serif";
+ --breakpoint-3xl: 120rem;
+}
+```
+
+Now utilities like `bg-mint-500`, `font-display`, and `3xl:grid-cols-6` become available.
+
+## Why @theme Instead of :root?
+
+Theme variables aren't just CSS variables - they also instruct Tailwind to create new utility classes. Using `@theme` makes this explicit and ensures variables are defined top-level.
+
+Use `@theme` for design tokens that map to utilities. Use `:root` for regular CSS variables that shouldn't have corresponding utilities.
+
+## Theme Variable Namespaces
+
+Theme variables are organized into namespaces that map to utility classes:
+
+| Namespace | Utility Classes |
+|-----------|----------------|
+| `--color-*` | `bg-red-500`, `text-sky-300`, `border-indigo-600`, etc. |
+| `--font-*` | `font-sans`, `font-serif`, `font-mono` |
+| `--breakpoint-*` | Responsive variants like `md:`, `lg:`, `xl:` |
+| `--spacing-*` | Spacing scale for padding, margin, gap utilities |
+| `--shadow-*` | `shadow-sm`, `shadow-md`, `shadow-lg` |
+| `--ease-*` | Transition timing functions |
+
+## Extending the Default Theme
+
+Add new theme variables to extend the default theme:
+
+```css
+@import "tailwindcss";
+
+@theme {
+ /* Add new color */
+ --color-brand-500: oklch(0.65 0.2 250);
+
+ /* Add new breakpoint */
+ --breakpoint-3xl: 120rem;
+
+ /* Add new font */
+ --font-display: "Satoshi", "sans-serif";
+}
+```
+
+## Using Theme Variables
+
+Tailwind generates CSS variables for your theme variables, so you can reference them:
+
+```html
+
+
Content
+
+
+
+ Content
+
+```
+
+## Default Theme
+
+When you import `tailwindcss`, it includes default theme variables:
+
+```css
+@layer theme, base, components, utilities;
+
+@import "./theme.css" layer(theme);
+@import "./preflight.css" layer(base);
+@import "./utilities.css" layer(utilities);
+```
+
+The default theme includes:
+- Color palette (red, blue, green, etc. with 50-950 shades)
+- Font families (sans, serif, mono)
+- Spacing scale
+- Shadows
+- Breakpoints (sm, md, lg, xl, 2xl)
+
+## Key Points
+
+- Theme variables define which utilities exist in your project
+- Use `@theme` directive to define design tokens
+- Variables must be defined top-level, not nested
+- Tailwind generates both utilities and CSS variables
+- Default theme provides a solid starting point
+
+
diff --git a/.agents/skills/tailwindcss/references/core-utility-classes.md b/.agents/skills/tailwindcss/references/core-utility-classes.md
new file mode 100644
index 0000000..4c532cc
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/core-utility-classes.md
@@ -0,0 +1,59 @@
+---
+name: core-utility-classes
+description: Understanding Tailwind's utility-first approach and how to style elements with utility classes
+---
+
+# Utility Classes
+
+Tailwind CSS uses a utility-first approach where you style elements by combining many single-purpose utility classes directly in your markup.
+
+## Overview
+
+Instead of writing custom CSS, you compose designs using utility classes:
+
+```html
+
+
+
+
ChitChat
+
You have a new message!
+
+
+```
+
+## Benefits
+
+- **Faster development** - No need to come up with class names or switch between HTML and CSS files
+- **Safer changes** - Adding or removing utilities only affects that element
+- **Easier maintenance** - Find the element and change classes, no need to remember custom CSS
+- **More portable** - Copy entire UI chunks between projects easily
+- **Smaller CSS** - Utilities are reusable, so CSS doesn't grow linearly
+
+## Why Not Inline Styles?
+
+Utility classes have important advantages over inline styles:
+
+- **Design constraints** - Choose from a predefined design system instead of magic numbers
+- **State variants** - Target hover, focus, and other states with variants like `hover:bg-blue-600`
+- **Responsive design** - Use responsive variants like `md:flex` for media queries
+
+## Utility Class Structure
+
+Utility classes follow consistent naming patterns:
+
+- **Property-value**: `bg-blue-500`, `text-lg`, `rounded-xl`
+- **Responsive**: `md:flex`, `lg:text-center`
+- **State variants**: `hover:bg-blue-600`, `focus:outline-2`
+- **Arbitrary values**: `top-[117px]`, `bg-[#bada55]`
+
+## Key Points
+
+- Every utility class is single-purpose and composable
+- Utilities can be combined with variants for conditional styling
+- Use complete class names - Tailwind scans your files as plain text
+- Avoid dynamically constructing class names with string interpolation
+
+
diff --git a/.agents/skills/tailwindcss/references/core-variants.md b/.agents/skills/tailwindcss/references/core-variants.md
new file mode 100644
index 0000000..26fe1bb
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/core-variants.md
@@ -0,0 +1,156 @@
+---
+name: core-variants
+description: Using variants to apply utilities conditionally based on states, pseudo-classes, and media queries
+---
+
+# Variants
+
+Variants let you apply utility classes conditionally based on states, pseudo-classes, pseudo-elements, media queries, and more.
+
+## Overview
+
+Add a variant prefix to any utility class to apply it conditionally:
+
+```html
+
+
Save
+```
+
+Variants can be stacked to target specific situations:
+
+```html
+
+
Save
+```
+
+## Pseudo-Class Variants
+
+### Interactive States
+
+```html
+
+ Save changes
+
+```
+
+Common interactive variants:
+- `hover:` - `:hover` pseudo-class
+- `focus:` - `:focus` pseudo-class
+- `active:` - `:active` pseudo-class
+- `focus-visible:` - `:focus-visible`
+- `focus-within:` - `:focus-within`
+- `visited:` - `:visited`
+
+### Structural Variants
+
+```html
+
+ Item 1
+ Item 2
+ Item 3
+
+```
+
+Common structural variants:
+- `first:` - `:first-child`
+- `last:` - `:last-child`
+- `odd:` - `:nth-child(odd)`
+- `even:` - `:nth-child(even)`
+- `only:` - `:only-child`
+
+### Form States
+
+```html
+
+```
+
+Common form variants:
+- `required:` - `:required`
+- `optional:` - `:optional`
+- `invalid:` - `:invalid`
+- `valid:` - `:valid`
+- `disabled:` - `:disabled`
+- `enabled:` - `:enabled`
+- `checked:` - `:checked`
+
+## Pseudo-Element Variants
+
+```html
+
+```
+
+Common pseudo-element variants:
+- `before:` - `::before`
+- `after:` - `::after`
+- `placeholder:` - `::placeholder`
+- `selection:` - `::selection`
+- `first-line:` - `::first-line`
+- `first-letter:` - `::first-letter`
+
+## Media Query Variants
+
+### Responsive Variants
+
+```html
+
Responsive text
+```
+
+### Dark Mode
+
+```html
+
+ Content
+
+```
+
+By default uses `prefers-color-scheme`, but can be customized to use a class or data attribute.
+
+### Reduced Motion
+
+```html
+
+ Animated content
+
+```
+
+## Attribute Selector Variants
+
+```html
+
+
Content
+
+
+
Details
+```
+
+## Arbitrary Variants
+
+Use arbitrary variants for custom selectors:
+
+```html
+
+```
+
+## Child Selector Variants
+
+```html
+
+
Paragraph 1
+
Paragraph 2
+
+```
+
+## Key Points
+
+- Variants prefix utilities to apply them conditionally
+- Variants can be stacked: `dark:md:hover:bg-blue-600`
+- Use variants for states, pseudo-classes, media queries, and more
+- Arbitrary variants enable custom selector patterns
+- Child selector variants target descendant elements
+
+
diff --git a/.agents/skills/tailwindcss/references/effects-form-controls.md b/.agents/skills/tailwindcss/references/effects-form-controls.md
new file mode 100644
index 0000000..2dbb50e
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/effects-form-controls.md
@@ -0,0 +1,76 @@
+---
+name: effects-form-controls
+description: Form control styling with accent-color, appearance, caret-color, and resize
+---
+
+# Form Controls & Input Styling
+
+Utilities for styling form controls: accent color, native appearance, caret color, and resize behavior.
+
+## Usage
+
+### Accent color
+
+Control the accent color of checkboxes, radio buttons, range inputs, and progress:
+
+```html
+
+
+
+
+
+
+```
+
+### Appearance
+
+Remove native form control styling for custom designs:
+
+```html
+
+
+
+ Yes
+
+ ...
+
+
+
+
+```
+
+### Caret color
+
+Set the text input cursor color:
+
+```html
+
+
+```
+
+### Resize
+
+Control textarea resize behavior:
+
+```html
+
+
+
+
+```
+
+## Key Points
+
+- `accent-*` - theme colors for checkboxes, radio, range; use `accent-[#hex]` or `accent-(--var)` for custom
+- `appearance-none` - remove native styling (custom selects, checkboxes)
+- `appearance-auto` - restore default (e.g. for `forced-colors: active`)
+- `caret-*` - theme colors for input cursor; matches text color patterns
+- `resize` - both; `resize-x` - horizontal; `resize-y` - vertical; `resize-none` - no handle
+
+
diff --git a/.agents/skills/tailwindcss/references/effects-scroll-snap.md b/.agents/skills/tailwindcss/references/effects-scroll-snap.md
new file mode 100644
index 0000000..c48855e
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/effects-scroll-snap.md
@@ -0,0 +1,59 @@
+---
+name: effects-scroll-snap
+description: CSS scroll snap for carousels and scroll containers
+---
+
+# Scroll Snap
+
+Utilities for scroll snap behavior in overflow containers. Use for carousels, horizontal galleries, or paged scroll.
+
+## Usage
+
+### Container
+
+```html
+
+
+
Slide 1
+
Slide 2
+
Slide 3
+
+
+
+
+
Section 1
+
Section 2
+
+
+
+
...
+```
+
+### Strictness
+
+- `snap-mandatory` - always rest on a snap point
+- `snap-proximity` - snap only when close to a point (default)
+
+### Child alignment
+
+```html
+
+
Center snap
+
Start snap
+
End snap
+
No snap
+
+```
+
+## Key Points
+
+- `snap-x` - horizontal; `snap-y` - vertical; `snap-both` - both; `snap-none` - disable
+- `snap-mandatory` / `snap-proximity` - strictness
+- Child: `snap-center`, `snap-start`, `snap-end`, `snap-align-none`
+- Requires overflow (e.g. `overflow-x-auto`) and scroll on container
+
+
diff --git a/.agents/skills/tailwindcss/references/effects-transition-animation.md b/.agents/skills/tailwindcss/references/effects-transition-animation.md
new file mode 100644
index 0000000..ecc38f6
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/effects-transition-animation.md
@@ -0,0 +1,80 @@
+---
+name: effects-transition-animation
+description: CSS transitions, animation keyframes, and reduced motion support
+---
+
+# Transition & Animation
+
+Utilities for CSS transitions and animations.
+
+## Usage
+
+### Transition property
+
+```html
+
Transitions common properties
+
All properties
+
Colors only
+
Opacity only
+
Transform only
+
No transition
+```
+
+### Transition duration and delay
+
+```html
+
150ms (default)
+
300ms
+
500ms
+
Delay 150ms
+
Both
+```
+
+### Transition timing
+
+```html
+
Linear
+
Ease in
+
Ease out
+
Ease in-out (default)
+
Arbitrary
+```
+
+### Animation keyframes
+
+```html
+
Spinning
+
Ping effect
+
Pulse
+
Bounce
+```
+
+Built-in: `animate-spin`, `animate-ping`, `animate-pulse`, `animate-bounce`. Use `@keyframes` in custom CSS for more.
+
+### Reduced motion
+
+```html
+
+ Respects prefers-reduced-motion
+
+
Spinner hidden when reduced motion
+```
+
+Use `motion-reduce:` to disable or simplify animations when user prefers reduced motion.
+
+## Key Points
+
+- Transition: `transition`, `transition-all`, `transition-colors`, `transition-opacity`, `transition-transform`
+- Duration: `duration-{75,100,150,200,300,500,700,1000}`
+- Delay: `delay-{75,100,150,200,300,500,700,1000}`
+- Timing: `ease-{linear,in,in-out,out}`
+- Animation: `animate-spin`, `animate-ping`, `animate-pulse`, `animate-bounce`
+- Always consider `motion-reduce:` for accessibility
+
+
diff --git a/.agents/skills/tailwindcss/references/effects-visibility-interactivity.md b/.agents/skills/tailwindcss/references/effects-visibility-interactivity.md
new file mode 100644
index 0000000..b209839
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/effects-visibility-interactivity.md
@@ -0,0 +1,82 @@
+---
+name: effects-visibility-interactivity
+description: Visibility, cursor, pointer-events, user-select, and z-index
+---
+
+# Visibility & Interactivity
+
+Utilities for visibility, cursor, pointer-events, user-select, and stacking.
+
+## Usage
+
+### Visibility
+
+```html
+
Visible (default)
+
Hidden but in layout
+
Table collapse
+```
+
+Use `invisible` when you need to keep layout space; use `hidden` (display:none) to remove from flow.
+
+### Cursor
+
+```html
+
Pointer
+
Disabled
+
Loading
+
Draggable
+
Grab
+
Grabbing
+
Text select
+
Default
+
No cursor
+```
+
+### Pointer events
+
+```html
+
Ignore all pointer events
+
Default behavior
+```
+
+Useful for overlays: make overlay `pointer-events-none` so clicks pass through, or `pointer-events-none` on disabled elements.
+
+### User select
+
+```html
+
Cannot select
+
Select text (default)
+
Select all on click
+
Browser default
+```
+
+### Z-index
+
+```html
+
0
+
10
+
20
+
50
+
Auto
+
Arbitrary
+```
+
+Common: `z-0` (base), `z-10` (dropdowns), `z-20` (fixed nav), `z-50` (modal), `z-40` (overlay).
+
+## Key Points
+
+- Visibility: `visible`, `invisible`, `collapse`
+- Cursor: `cursor-{pointer,not-allowed,wait,move,grab,text,default,none}`
+- Pointer events: `pointer-events-none`, `pointer-events-auto`
+- User select: `select-none`, `select-text`, `select-all`
+- Z-index: `z-{0,10,20,30,40,50,auto}`, `z-[n]`
+
+
diff --git a/.agents/skills/tailwindcss/references/features-content-detection.md b/.agents/skills/tailwindcss/references/features-content-detection.md
new file mode 100644
index 0000000..c513b44
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/features-content-detection.md
@@ -0,0 +1,175 @@
+---
+name: features-content-detection
+description: How Tailwind detects classes in source files and how to customize content scanning
+---
+
+# Detecting Classes in Source Files
+
+Tailwind scans your project for utility classes and generates CSS based on what you've actually used.
+
+## How Classes Are Detected
+
+Tailwind treats all source files as plain text and looks for tokens that could be class names:
+
+```jsx
+export function Button({ color, children }) {
+ const colors = {
+ black: "bg-black text-white",
+ blue: "bg-blue-500 text-white",
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+```
+
+Tailwind detects `bg-black`, `text-white`, `bg-blue-500`, `rounded-full`, `px-2`, and `py-1.5` from this file.
+
+## Dynamic Class Names
+
+Tailwind scans files as plain text, so it can't understand string concatenation or interpolation.
+
+### ❌ Don't Construct Classes Dynamically
+
+```html
+
+```
+
+The strings `text-red-600` and `text-green-600` don't exist in the file, so Tailwind won't generate them.
+
+### ✅ Use Complete Class Names
+
+```html
+
+ Content
+
+```
+
+### ❌ Don't Build Classes from Props
+
+```jsx
+function Button({ color, children }) {
+ return
{children} ;
+}
+```
+
+### ✅ Map Props to Static Classes
+
+```jsx
+function Button({ color, children }) {
+ const colorVariants = {
+ blue: "bg-blue-600 hover:bg-blue-500",
+ red: "bg-red-600 hover:bg-red-500",
+ };
+
+ return
{children} ;
+}
+```
+
+## Which Files Are Scanned
+
+Tailwind scans every file in your project except:
+
+- Files in `.gitignore`
+- Files in `node_modules`
+- Binary files (images, videos, zip files)
+- CSS files
+- Common package manager lock files
+
+## Explicitly Registering Sources
+
+Use `@source` to explicitly register source paths:
+
+```css
+@import "tailwindcss";
+@source "../node_modules/@acmecorp/ui-lib";
+```
+
+This is useful for external libraries built with Tailwind that are in `.gitignore`.
+
+## Setting Base Path
+
+Set the base path for source detection:
+
+```css
+@import "tailwindcss" source("../src");
+```
+
+Useful in monorepos where build commands run from the root.
+
+## Ignoring Specific Paths
+
+Use `@source not` to ignore paths:
+
+```css
+@import "tailwindcss";
+@source not "../src/components/legacy";
+```
+
+## Disabling Automatic Detection
+
+Use `source(none)` to disable automatic detection:
+
+```css
+@import "tailwindcss" source(none);
+
+@source "../admin";
+@source "../shared";
+```
+
+Useful for projects with multiple Tailwind stylesheets.
+
+## Safelisting Utilities
+
+Force Tailwind to generate specific classes with `@source inline()`:
+
+```css
+@import "tailwindcss";
+@source inline("underline");
+```
+
+### Safelisting with Variants
+
+Generate classes with variants:
+
+```css
+@import "tailwindcss";
+@source inline("{hover:,focus:,}underline");
+```
+
+### Safelisting with Ranges
+
+Use brace expansion to generate multiple classes:
+
+```css
+@import "tailwindcss";
+@source inline("{hover:,}bg-red-{50,{100..900..100},950}");
+```
+
+This generates `bg-red-50` through `bg-red-950` with hover variants.
+
+## Explicitly Excluding Classes
+
+Use `@source not inline()` to prevent specific classes from being generated:
+
+```css
+@import "tailwindcss";
+@source not inline("{hover:,focus:,}bg-red-{50,{100..900..100},950}");
+```
+
+## Key Points
+
+- Tailwind scans files as plain text
+- Always use complete class names, never construct them dynamically
+- Map props/state to static class names
+- Use `@source` to explicitly register or ignore paths
+- Use `@source inline()` to safelist utilities
+- Brace expansion works in inline sources for ranges
+
+
diff --git a/.agents/skills/tailwindcss/references/features-custom-styles.md b/.agents/skills/tailwindcss/references/features-custom-styles.md
new file mode 100644
index 0000000..2c1223b
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/features-custom-styles.md
@@ -0,0 +1,203 @@
+---
+name: features-custom-styles
+description: Adding custom styles, utilities, variants, and working with arbitrary values
+---
+
+# Adding Custom Styles
+
+Tailwind is designed to be extensible. This guide covers customizing your theme, using arbitrary values, adding custom CSS, and extending the framework.
+
+## Customizing Your Theme
+
+Add custom design tokens using `@theme`:
+
+```css
+@import "tailwindcss";
+
+@theme {
+ --font-display: "Satoshi", "sans-serif";
+ --breakpoint-3xl: 120rem;
+ --color-brand-500: oklch(0.65 0.2 250);
+ --ease-fluid: cubic-bezier(0.3, 0, 0, 1);
+}
+```
+
+## Arbitrary Values
+
+Use square bracket notation for one-off values that don't belong in your theme:
+
+```html
+
+
+ Content
+
+
+
+
+ Content
+
+```
+
+### CSS Variables as Arbitrary Values
+
+Reference CSS variables:
+
+```html
+
+ Content
+
+```
+
+This is shorthand for `fill-[var(--my-brand-color)]`.
+
+## Arbitrary Properties
+
+Use square brackets for CSS properties Tailwind doesn't have utilities for:
+
+```html
+
+ Content
+
+
+
+
+ Content
+
+```
+
+### CSS Variables
+
+Set CSS variables with arbitrary properties:
+
+```html
+
+ Content
+
+```
+
+## Arbitrary Variants
+
+Create custom selectors on the fly:
+
+```html
+
+```
+
+## Handling Whitespace
+
+Use underscores for spaces in arbitrary values:
+
+```html
+
+ Content
+
+```
+
+Tailwind converts underscores to spaces, except in contexts where underscores are valid (like URLs).
+
+## Custom Utilities
+
+Add custom utilities with `@utility`:
+
+```css
+@import "tailwindcss";
+
+@utility tab-4 {
+ tab-size: 4;
+}
+```
+
+Now you can use `tab-4` utility class, and it works with variants:
+
+```html
+
+ Content
+
+```
+
+## Custom Variants
+
+Add custom variants with `@custom-variant`:
+
+```css
+@import "tailwindcss";
+
+@custom-variant theme-midnight (&:where([data-theme="midnight"] *));
+```
+
+Now you can use `theme-midnight:` variant:
+
+```html
+
+ Content
+
+```
+
+## Using Variants in CSS
+
+Apply Tailwind variants to custom CSS with `@variant`:
+
+```css
+.my-element {
+ background: white;
+
+ @variant dark {
+ background: black;
+ }
+
+ @variant hover {
+ background: gray;
+ }
+}
+```
+
+## Base Styles
+
+Add base styles to the `base` layer:
+
+```css
+@layer base {
+ h1 {
+ font-size: var(--text-2xl);
+ font-weight: 600;
+ }
+
+ h2 {
+ font-size: var(--text-xl);
+ font-weight: 600;
+ }
+
+ a {
+ color: var(--color-blue-600);
+ text-decoration-line: underline;
+ }
+}
+```
+
+## Component Styles
+
+Add component styles to the `components` layer:
+
+```css
+@layer components {
+ .btn-primary {
+ @apply bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-4 rounded;
+ }
+}
+```
+
+## Key Points
+
+- Use `@theme` to customize design tokens
+- Arbitrary values with `[]` for one-off values
+- Arbitrary properties for CSS properties without utilities
+- `@utility` for custom utilities
+- `@custom-variant` for custom variants
+- `@layer` for organizing base and component styles
+
+
diff --git a/.agents/skills/tailwindcss/references/features-dark-mode.md b/.agents/skills/tailwindcss/references/features-dark-mode.md
new file mode 100644
index 0000000..3bf8bd5
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/features-dark-mode.md
@@ -0,0 +1,137 @@
+---
+name: features-dark-mode
+description: Implementing dark mode with Tailwind's dark variant and custom dark mode strategies
+---
+
+# Dark Mode
+
+Tailwind includes a `dark` variant that lets you style your site differently when dark mode is enabled.
+
+## Overview
+
+Use the `dark:` variant to apply styles in dark mode:
+
+```html
+
+ Content
+
+```
+
+## Default Behavior
+
+By default, the `dark` variant uses the `prefers-color-scheme` CSS media feature:
+
+```css
+@media (prefers-color-scheme: dark) {
+ .dark\:bg-gray-800 {
+ background-color: rgb(31 41 55);
+ }
+}
+```
+
+## Manual Toggle with Class
+
+Override the `dark` variant to use a class selector:
+
+```css
+@import "tailwindcss";
+
+@custom-variant dark (&:where(.dark, .dark *));
+```
+
+Now dark mode utilities apply when the `dark` class is present:
+
+```html
+
+
+
Content
+
+
+```
+
+## Manual Toggle with Data Attribute
+
+Use a data attribute instead:
+
+```css
+@import "tailwindcss";
+
+@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
+```
+
+```html
+
+
+
Content
+
+
+```
+
+## Three-Way Theme Toggle
+
+Support light mode, dark mode, and system preference:
+
+```js
+// On page load
+document.documentElement.classList.toggle(
+ "dark",
+ localStorage.theme === "dark" ||
+ (!("theme" in localStorage) &&
+ window.matchMedia("(prefers-color-scheme: dark)").matches)
+);
+
+// Set light mode
+localStorage.theme = "light";
+document.documentElement.classList.remove("dark");
+
+// Set dark mode
+localStorage.theme = "dark";
+document.documentElement.classList.add("dark");
+
+// Respect system preference
+localStorage.removeItem("theme");
+document.documentElement.classList.toggle(
+ "dark",
+ window.matchMedia("(prefers-color-scheme: dark)").matches
+);
+```
+
+## Common Patterns
+
+### Cards
+
+```html
+
+```
+
+### Borders
+
+```html
+
+ Content
+
+```
+
+### Buttons
+
+```html
+
+ Button
+
+```
+
+## Key Points
+
+- Use `dark:` variant for dark mode styles
+- Default uses `prefers-color-scheme` media query
+- Override with `@custom-variant` for manual toggles
+- Can use class or data attribute selectors
+- Combine with responsive variants: `dark:md:bg-gray-800`
+
+
diff --git a/.agents/skills/tailwindcss/references/features-functions-directives.md b/.agents/skills/tailwindcss/references/features-functions-directives.md
new file mode 100644
index 0000000..e68c90c
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/features-functions-directives.md
@@ -0,0 +1,241 @@
+---
+name: features-functions-directives
+description: Tailwind's CSS directives (@import, @theme, @utility, @variant) and functions (--alpha, --spacing)
+---
+
+# Functions and Directives
+
+Tailwind provides custom CSS directives and functions for working with your design system.
+
+## Directives
+
+Directives are custom Tailwind-specific at-rules that offer special functionality.
+
+### @import
+
+Import CSS files, including Tailwind:
+
+```css
+@import "tailwindcss";
+```
+
+### @theme
+
+Define your project's design tokens:
+
+```css
+@theme {
+ --font-display: "Satoshi", "sans-serif";
+ --breakpoint-3xl: 120rem;
+ --color-brand-500: oklch(0.65 0.2 250);
+ --ease-fluid: cubic-bezier(0.3, 0, 0, 1);
+}
+```
+
+### @source
+
+Explicitly specify source files for class detection:
+
+```css
+@import "tailwindcss";
+@source "../node_modules/@my-company/ui-lib";
+```
+
+### @utility
+
+Add custom utilities:
+
+```css
+@utility tab-4 {
+ tab-size: 4;
+}
+```
+
+Custom utilities work with variants:
+
+```html
+
Content
+```
+
+### @variant
+
+Apply Tailwind variants to styles in your CSS:
+
+```css
+.my-element {
+ background: white;
+
+ @variant dark {
+ background: black;
+ }
+}
+```
+
+### @custom-variant
+
+Add custom variants:
+
+```css
+@custom-variant theme-midnight (&:where([data-theme="midnight"] *));
+```
+
+```html
+
Content
+```
+
+### @apply
+
+Inline existing utility classes into custom CSS:
+
+```css
+.select2-dropdown {
+ @apply rounded-b-lg shadow-md;
+}
+
+.select2-search {
+ @apply rounded border border-gray-300;
+}
+```
+
+### @reference
+
+Import stylesheet for reference without including styles (useful for Vue/Svelte components):
+
+```html
+
+```
+
+Or reference Tailwind directly:
+
+```html
+
+```
+
+### Subpath Imports
+
+Directives support subpath imports (like TypeScript path aliases):
+
+```json
+{
+ "imports": {
+ "#app.css": "./src/css/app.css"
+ }
+}
+```
+
+```html
+
+```
+
+## Functions
+
+Tailwind provides build-time functions for working with colors and spacing.
+
+### --alpha()
+
+Adjust the opacity of a color:
+
+```css
+.my-element {
+ color: --alpha(var(--color-lime-300) / 50%);
+}
+```
+
+Compiles to:
+
+```css
+.my-element {
+ color: color-mix(in oklab, var(--color-lime-300) 50%, transparent);
+}
+```
+
+### --spacing()
+
+Generate spacing values based on your theme:
+
+```css
+.my-element {
+ margin: --spacing(4);
+}
+```
+
+Compiles to:
+
+```css
+.my-element {
+ margin: calc(var(--spacing) * 4);
+}
+```
+
+Useful in arbitrary values with `calc()`:
+
+```html
+
+ Content
+
+```
+
+## Compatibility Directives
+
+For compatibility with Tailwind CSS v3.x:
+
+### @config
+
+Load a legacy JavaScript-based configuration:
+
+```css
+@config "../../tailwind.config.js";
+```
+
+### @plugin
+
+Load a legacy JavaScript-based plugin:
+
+```css
+@plugin "@tailwindcss/typography";
+```
+
+### theme()
+
+Access theme values using dot notation (deprecated):
+
+```css
+.my-element {
+ margin: theme(spacing.12);
+}
+```
+
+**Note:** Prefer using CSS theme variables instead.
+
+## Key Points
+
+- Directives are Tailwind-specific at-rules
+- `@theme` defines design tokens
+- `@utility` creates custom utilities
+- `@custom-variant` creates custom variants
+- `@apply` inlines utilities into CSS
+- `--alpha()` and `--spacing()` are build-time functions
+- Compatibility directives support v3.x configs
+
+
diff --git a/.agents/skills/tailwindcss/references/features-upgrade.md b/.agents/skills/tailwindcss/references/features-upgrade.md
new file mode 100644
index 0000000..10b477c
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/features-upgrade.md
@@ -0,0 +1,91 @@
+---
+name: features-upgrade
+description: Migrating from Tailwind CSS v3 to v4
+---
+
+# Upgrade Guide (v3 → v4)
+
+Key changes when upgrading from Tailwind CSS v3 to v4. Use the automated upgrade tool when possible.
+
+## Upgrade Tool
+
+```bash
+npx @tailwindcss/upgrade
+```
+
+Requires Node.js 20+. Run in a new branch, review diff, test. Handles most migration automatically.
+
+## Installation Changes
+
+- **PostCSS**: Use `@tailwindcss/postcss`; remove `postcss-import` and `autoprefixer` (handled by v4)
+- **Vite**: Prefer `@tailwindcss/vite` over PostCSS
+- **CLI**: Use `npx @tailwindcss/cli` instead of `npx tailwindcss`
+
+## Import Change
+
+```css
+/* v3 */
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* v4 */
+@import "tailwindcss";
+```
+
+## Renamed Utilities
+
+| v3 | v4 |
+|----|-----|
+| `shadow-sm` | `shadow-xs` |
+| `shadow` | `shadow-sm` |
+| `rounded-sm` | `rounded-xs` |
+| `rounded` | `rounded-sm` |
+| `blur-sm` | `blur-xs` |
+| `blur` | `blur-sm` |
+| `outline-none` | `outline-hidden` |
+| `ring` | `ring-3` |
+
+## Removed / Replaced
+
+- `bg-opacity-*`, `text-opacity-*`, etc. → use `bg-black/50`, `text-black/50`
+- `flex-shrink-*` → `shrink-*`
+- `flex-grow-*` → `grow-*`
+- `overflow-ellipsis` → `text-ellipsis`
+
+## Important Modifier
+
+```html
+
+
+
+
+
+```
+
+## Ring & Border Defaults
+
+- `ring` width: 3px → 1px; use `ring-3` for v3 behavior
+- `ring` / `border` default color: `currentColor` (was gray-200 / blue-500)
+- Always specify color: `ring-3 ring-blue-500`, `border border-gray-200`
+
+## Other Breaking Changes
+
+- **Space/divide selectors**: Changed from `:not([hidden]) ~ :not([hidden])` to `:not(:last-child)`; may affect layout
+- **Variant stacking**: Left-to-right in v4 (was right-to-left)
+- **Transform reset**: `transform-none` no longer resets rotate/scale/translate; use `scale-none`, `rotate-none`, etc.
+- **Hover on mobile**: `hover` only applies when device supports hover; override with `@custom-variant hover (&:hover)` if needed
+- **Arbitrary values**: Use `bg-(--var)` not `bg-[--var]` for CSS variables
+- **theme()**: Use `theme(--breakpoint-xl)` not `theme(screens.xl)`
+- **@layer utilities/components**: Use `@utility` directive instead
+- **corePlugins, safelist, separator**: Not supported; use `@source inline()` for safelisting
+- **Sass/Less/Stylus**: v4 not designed for use with CSS preprocessors
+
+## Browser Support
+
+v4 targets Safari 16.4+, Chrome 111+, Firefox 128+. For older browsers, stay on v3.4.
+
+
diff --git a/.agents/skills/tailwindcss/references/layout-aspect-ratio.md b/.agents/skills/tailwindcss/references/layout-aspect-ratio.md
new file mode 100644
index 0000000..8335987
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/layout-aspect-ratio.md
@@ -0,0 +1,39 @@
+---
+name: layout-aspect-ratio
+description: Controlling element aspect ratio for responsive media
+---
+
+# Aspect Ratio
+
+Utilities for controlling the aspect ratio of an element (e.g. video, images).
+
+## Usage
+
+```html
+
+
+
+
+
+
+
Content
+
+
+
1:1
+
+
+
Natural ratio
+```
+
+## Key Points
+
+- `aspect-video` - 16:9
+- `aspect-square` - 1:1
+- `aspect-auto` - browser default (intrinsic)
+- `aspect-[4/3]` - arbitrary ratio
+- Useful for responsive video embeds and image containers
+
+
diff --git a/.agents/skills/tailwindcss/references/layout-columns.md b/.agents/skills/tailwindcss/references/layout-columns.md
new file mode 100644
index 0000000..fb3893e
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/layout-columns.md
@@ -0,0 +1,80 @@
+---
+name: layout-columns
+description: Multi-column layout with columns utility for masonry-like or newspaper layouts
+---
+
+# Columns
+
+Utilities for CSS multi-column layout. Content flows into multiple columns within a single element.
+
+## Usage
+
+### By number of columns
+
+```html
+
+
+
+
+
+```
+
+### By column width
+
+Use container scale for ideal column width; column count adjusts automatically:
+
+```html
+
...
+
...
+
...
+```
+
+### Column gap
+
+Use `gap-*` utilities for space between columns:
+
+```html
+
...
+```
+
+### Responsive
+
+```html
+
...
+```
+
+### Custom value
+
+```html
+
...
+
...
+```
+
+### Column / page breaks
+
+Use with multi-column or print layouts:
+
+```html
+
+
Content...
+
Force break after this
+
Next column...
+
+```
+
+- `break-after-column` / `break-before-column` - column break
+- `break-after-page` / `break-before-page` - page break (print)
+- `break-after-avoid` / `break-inside-avoid` - avoid breaking
+
+## Key Points
+
+- `columns-
` - fixed number of columns (e.g. `columns-3`)
+- `columns-3xs` through `columns-7xl` - column width from container scale
+- `columns-auto` - auto columns
+- `gap-*` controls column gap (same as flex/grid gap)
+- Use for magazine-style layouts, image galleries, long text
+
+
diff --git a/.agents/skills/tailwindcss/references/layout-display.md b/.agents/skills/tailwindcss/references/layout-display.md
new file mode 100644
index 0000000..9b89d41
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/layout-display.md
@@ -0,0 +1,110 @@
+---
+name: layout-display
+description: Controlling element display type including flex, grid, block, inline, hidden, and sr-only
+---
+
+# Display
+
+Utilities for controlling the display box type of an element.
+
+## Usage
+
+### Basic display types
+
+```html
+display: inline
+display: inline-block
+display: block
+```
+
+### Flex and Grid containers
+
+```html
+
+Flex layout
+Inline flex with text
+
+
+Grid layout
+Inline grid
+```
+
+### Flow root
+
+Use `flow-root` to create a block-level element with its own block formatting context (fixes margin collapse):
+
+```html
+
+
Content with isolated BFC
+
+```
+
+### Contents
+
+Use `contents` for a "phantom" container whose children act like direct children of the parent:
+
+```html
+
+```
+
+### Table display
+
+```html
+
+```
+
+### Hidden
+
+```html
+Removed from document flow
+Visible only on md+
+```
+
+For visual-only hiding while keeping in DOM, use `invisible` or `opacity-0` instead.
+
+### Screen reader only
+
+```html
+
+
+ Settings
+
+```
+
+Use `not-sr-only` to undo: `Settings `
+
+## Key Points
+
+- `flex` / `inline-flex` - Flexbox layout
+- `grid` / `inline-grid` - CSS Grid layout
+- `block` / `inline` / `inline-block` - Basic flow
+- `hidden` - `display: none` (removes from flow)
+- `sr-only` - Visually hidden but accessible to screen readers
+- `contents` - Children participate in parent's layout
+- `flow-root` - Establishes new BFC
+- Table utilities: `table`, `table-row`, `table-cell`, `table-header-group`, etc.
+
+
diff --git a/.agents/skills/tailwindcss/references/layout-flexbox.md b/.agents/skills/tailwindcss/references/layout-flexbox.md
new file mode 100644
index 0000000..4251945
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/layout-flexbox.md
@@ -0,0 +1,110 @@
+---
+name: layout-flexbox
+description: Flexbox layout utilities including flex-direction, justify, items, gap, grow, shrink, and wrap
+---
+
+# Flexbox
+
+Utilities for building flex layouts: direction, alignment, gap, and item sizing.
+
+## Usage
+
+### Flex direction and wrap
+
+```html
+Horizontal (default)
+Reversed
+Vertical
+Vertical reversed
+
+Wrap when needed
+No wrap (default)
+Wrap reversed
+```
+
+### Justify content
+
+```html
+Start
+End
+Center
+Space between
+Space around
+Space evenly
+```
+
+### Align items
+
+```html
+Align start
+Align end
+Center (common)
+Baseline
+Stretch (default)
+```
+
+### Align self (on flex children)
+
+```html
+
+
Auto
+
Start
+
Center
+
Stretch
+
+```
+
+### Gap
+
+```html
+gap-4 (1rem)
+Different x/y gap
+No gap
+```
+
+### Flex grow, shrink, basis
+
+```html
+
+
Fixed
+
Grows and shrinks
+
Shrink only
+
Grow/shrink with initial size
+
+```
+
+- `flex-1` = `flex: 1 1 0%` - equal distribution
+- `flex-initial` = `flex: 0 1 auto` - shrink, don't grow
+- `flex-auto` = `flex: 1 1 auto` - grow/shrink from content size
+- `flex-none` = `flex: none` - no grow or shrink
+
+### Order
+
+```html
+
+
Second
+
First
+
Last
+
+```
+
+## Key Points
+
+- Use `flex` or `inline-flex` as container (see layout-display)
+- Direction: `flex-row`, `flex-col`, `flex-row-reverse`, `flex-col-reverse`
+- Justify: `justify-start`, `justify-center`, `justify-between`, `justify-evenly`
+- Align: `items-center`, `items-start`, `items-stretch`
+- Gap: `gap-{n}`, `gap-x-{n}`, `gap-y-{n}` (spacing scale)
+- Item sizing: `flex-1`, `flex-none`, `flex-auto`, `flex-initial`
+- Self alignment: `self-center`, `self-start`, etc.
+- Order: `order-{n}`, `order-first`, `order-last`
+
+
diff --git a/.agents/skills/tailwindcss/references/layout-grid.md b/.agents/skills/tailwindcss/references/layout-grid.md
new file mode 100644
index 0000000..6b7c2bb
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/layout-grid.md
@@ -0,0 +1,87 @@
+---
+name: layout-grid
+description: CSS Grid utilities including grid-cols, grid-rows, gap, place-items, and grid placement
+---
+
+# Grid
+
+Utilities for CSS Grid layouts: columns, rows, gap, and item placement.
+
+## Usage
+
+### Grid columns and rows
+
+```html
+3 equal columns
+Responsive
+No explicit tracks
+
+3 rows
+Custom rows
+```
+
+### Custom grid template
+
+```html
+Custom template
+CSS variable
+```
+
+### Subgrid
+
+```html
+
+```
+
+### Gap
+
+```html
+Uniform gap
+Different x/y
+```
+
+### Place items (align + justify)
+
+```html
+Center both
+Start both
+Equivalent
+```
+
+### Grid placement (col/row span and start)
+
+```html
+
+
Spans 2 columns
+
Starts at col 3
+
Spans 2 rows
+
Full width
+
+```
+
+## Key Points
+
+- `grid` / `inline-grid` - Grid container (see layout-display)
+- Columns: `grid-cols-{n}`, `grid-cols-none`, `grid-cols-subgrid`, `grid-cols-[...]`
+- Rows: `grid-rows-{n}`, `grid-rows-[...]`
+- Gap: `gap-{n}`, `gap-x-{n}`, `gap-y-{n}`
+- Placement: `col-span-{n}`, `col-start-{n}`, `row-span-{n}`, `row-start-{n}`
+- `col-span-full` / `row-span-full` - span all
+- `place-items-*` - shorthand for align + justify
+- Use `grid-cols-[...]` for custom templates like `minmax()` or `repeat()`
+
+
diff --git a/.agents/skills/tailwindcss/references/layout-height.md b/.agents/skills/tailwindcss/references/layout-height.md
new file mode 100644
index 0000000..4ac09c3
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/layout-height.md
@@ -0,0 +1,97 @@
+---
+name: layout-height
+description: Setting element height with spacing scale, fractions, viewport units, and content-based sizing
+---
+
+# Height
+
+Utilities for setting the height of an element.
+
+## Usage
+
+### Spacing scale
+
+Use `h-` utilities based on the spacing scale:
+
+```html
+h-24
+h-48
+h-64
+```
+
+### Percentage heights
+
+Use `h-full` or `h-` for percentage-based heights:
+
+```html
+100%
+50%
+75%
+90%
+```
+
+### Viewport units
+
+Use `h-screen` for full viewport height, or dynamic viewport units:
+
+```html
+Full viewport height (100vh)
+Dynamic viewport height
+Small viewport height
+Large viewport height
+```
+
+### Content-based heights
+
+Use `h-auto`, `h-fit`, `h-min`, `h-max` for content-based sizing:
+
+```html
+Auto height
+Fit content
+Min content
+Max content
+```
+
+### Line height
+
+Use `h-lh` to match line height:
+
+```html
+Matches line height
+```
+
+### Size utility
+
+Use `size-` to set both width and height:
+
+```html
+16x16
+24x24
+100% x 100%
+```
+
+### Custom values
+
+Use arbitrary values for custom heights:
+
+```html
+Custom pixel height
+Custom viewport height
+Custom calculation
+```
+
+## Key Points
+
+- Spacing scale: `h-0` through `h-96` (and beyond)
+- Fractions: `h-1/2`, `h-1/3`, `h-2/3`, `h-1/4`, `h-3/4`, `h-9/10`
+- Viewport units: `h-screen` (100vh), `h-dvh`, `h-svh`, `h-lvh`
+- `h-dvh` adapts to browser UI (address bar, etc.)
+- `h-svh` uses smallest viewport height
+- `h-lvh` uses largest viewport height
+- `size-*` utilities set both width and height simultaneously
+- Use `h-auto` to reset height at specific breakpoints
+
+
diff --git a/.agents/skills/tailwindcss/references/layout-inset.md b/.agents/skills/tailwindcss/references/layout-inset.md
new file mode 100644
index 0000000..f8a09bb
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/layout-inset.md
@@ -0,0 +1,91 @@
+---
+name: layout-inset
+description: Controlling placement of positioned elements with top, right, bottom, left, and inset utilities
+---
+
+# Top / Right / Bottom / Left
+
+Utilities for controlling the placement of positioned elements.
+
+## Usage
+
+### Basic positioning
+
+Use `top-`, `right-`, `bottom-`, `left-` to position elements:
+
+```html
+
+
+
+
+
+
+
+
+```
+
+### Inset utilities
+
+Use `inset-` for all sides, `inset-x-` for horizontal, `inset-y-` for vertical:
+
+```html
+Fill parent
+Span top
+Span left
+```
+
+### Negative values
+
+Prefix with a dash for negative values:
+
+```html
+
+```
+
+### Logical properties
+
+Use `start-` and `end-` for RTL-aware positioning:
+
+```html
+
+
+```
+
+### Percentage and custom values
+
+Use fractions for percentages or arbitrary values:
+
+```html
+
+ Centered
+
+
+ Custom position
+
+```
+
+## Key Points
+
+- `inset-0` sets all sides to 0 (equivalent to `top-0 right-0 bottom-0 left-0`)
+- `inset-x-0` sets left and right to 0
+- `inset-y-0` sets top and bottom to 0
+- Use `start`/`end` for logical properties that adapt to text direction
+- Negative values use dash prefix: `-top-4`, `-left-8`
+- Combine with `position` utilities (`absolute`, `fixed`, `relative`, `sticky`)
+
+
diff --git a/.agents/skills/tailwindcss/references/layout-margin.md b/.agents/skills/tailwindcss/references/layout-margin.md
new file mode 100644
index 0000000..12dfe93
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/layout-margin.md
@@ -0,0 +1,126 @@
+---
+name: layout-margin
+description: Controlling element margins with spacing scale, negative values, logical properties, and space utilities
+---
+
+# Margin
+
+Utilities for controlling an element's margin.
+
+## Usage
+
+### All sides
+
+Use `m-` to set margin on all sides:
+
+```html
+Margin on all sides
+Larger margin
+```
+
+### Individual sides
+
+Use `mt-`, `mr-`, `mb-`, `ml-`:
+
+```html
+Top margin
+Right margin
+Bottom margin
+Left margin
+```
+
+### Horizontal and vertical
+
+Use `mx-` for horizontal, `my-` for vertical:
+
+```html
+Horizontal margin
+Vertical margin
+```
+
+### Negative margins
+
+Prefix with dash for negative values:
+
+```html
+Negative top margin
+Negative horizontal margin
+```
+
+### Auto margins
+
+Use `m-auto` or directional auto margins for centering:
+
+```html
+Centered horizontally
+Pushed to right
+```
+
+### Logical properties
+
+Use `ms-` (margin-inline-start) and `me-` (margin-inline-end) for RTL support:
+
+```html
+
+
Left margin in LTR
+
Right margin in LTR
+
+
+
Right margin in RTL
+
Left margin in RTL
+
+```
+
+### Space between children
+
+Use `space-x-` or `space-y-` to add margin between children:
+
+```html
+
+
+
+```
+
+### Reversing space
+
+Use `space-x-reverse` or `space-y-reverse` with reversed flex directions:
+
+```html
+
+```
+
+### Custom values
+
+Use arbitrary values for custom margins:
+
+```html
+Custom margin
+Custom calculation
+```
+
+## Key Points
+
+- Spacing scale: `m-0` through `m-96` (and beyond)
+- Negative: prefix with dash (`-m-4`, `-mt-8`, `-mx-4`)
+- Auto: `m-auto`, `mx-auto`, `my-auto`, `mt-auto`, etc.
+- Logical: `ms-*` (start), `me-*` (end) adapt to text direction
+- Space utilities: `space-x-*`, `space-y-*` add margin to all children except last
+- Space reverse: `space-x-reverse`, `space-y-reverse` for reversed flex layouts
+- Limitations: Space utilities don't work well with grids or complex layouts - use `gap` utilities instead
+
+
diff --git a/.agents/skills/tailwindcss/references/layout-min-max-sizing.md b/.agents/skills/tailwindcss/references/layout-min-max-sizing.md
new file mode 100644
index 0000000..b36de8d
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/layout-min-max-sizing.md
@@ -0,0 +1,63 @@
+---
+name: layout-min-max-sizing
+description: Setting minimum and maximum width and height with min-w, max-w, min-h, max-h
+---
+
+# Min & Max Sizing
+
+Utilities for constraining element dimensions with minimum and maximum width/height.
+
+## Usage
+
+### Min width
+
+```html
+
+min-w-24
+min-w-64
+
+
+min-w-full
+min-w-3/4
+
+
+min-w-sm
+min-w-xl
+
+
+min-content
+max-content
+fit-content
+auto
+
+
+Custom
+```
+
+### Max width
+
+Use `max-w-` with similar scales: spacing numbers, fractions (`max-w-1/2`), container sizes (`max-w-md`), `max-w-full`, `max-w-screen`, `max-w-min`, `max-w-max`, `max-w-fit`, `max-w-none`.
+
+### Min / Max height
+
+```html
+At least full viewport height
+Allow shrinking in flex
+Scrollable with max height
+```
+
+## Key Points
+
+- min-w: spacing scale, fractions, container scale (3xs–7xl), `full`, `screen`, `min`, `max`, `fit`, `auto`
+- max-w: same options plus `none`
+- min-h / max-h: similar scales; `min-h-0` important for flex children to shrink
+- Viewport units: `min-w-screen`, `min-w-dvw`, `min-w-svw`, `min-w-lvw`
+- Container scale: `min-w-3xs` through `min-w-7xl` map to `--container-*` variables
+
+
diff --git a/.agents/skills/tailwindcss/references/layout-object-fit-position.md b/.agents/skills/tailwindcss/references/layout-object-fit-position.md
new file mode 100644
index 0000000..22bce97
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/layout-object-fit-position.md
@@ -0,0 +1,64 @@
+---
+name: layout-object-fit-position
+description: Controlling how replaced elements (images, video) are resized and positioned within their container
+---
+
+# Object Fit & Object Position
+
+Utilities for controlling how replaced elements like ` ` and `` are resized and positioned within their container.
+
+## Usage
+
+### Object fit
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Object position
+
+```html
+
+
+
+
+
+
+
+
+```
+
+### Responsive
+
+```html
+
+```
+
+## Key Points
+
+- `object-contain` - maintain aspect ratio, fit inside container
+- `object-cover` - maintain aspect ratio, cover container (may crop)
+- `object-fill` - stretch to fill container
+- `object-scale-down` - like `contain` but never upscale
+- `object-none` - original size, ignore container
+- Position: `object-top-left`, `object-top`, `object-top-right`, `object-left`, `object-center`, `object-right`, `object-bottom-left`, `object-bottom`, `object-bottom-right`
+- Custom: `object-[25%_75%]`, `object-(--custom-property)`
+
+
diff --git a/.agents/skills/tailwindcss/references/layout-overflow.md b/.agents/skills/tailwindcss/references/layout-overflow.md
new file mode 100644
index 0000000..8424da3
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/layout-overflow.md
@@ -0,0 +1,57 @@
+---
+name: layout-overflow
+description: Controlling how elements handle content that overflows their container
+---
+
+# Overflow
+
+Utilities for controlling how an element handles content that is too large for the container.
+
+## Usage
+
+### Basic overflow
+
+Use `overflow-auto`, `overflow-hidden`, `overflow-visible`, `overflow-scroll`:
+
+```html
+Scrolls if needed
+Clips overflow
+Shows overflow
+Always shows scrollbars
+```
+
+### Axis-specific overflow
+
+Use `overflow-x-*` or `overflow-y-*` for horizontal/vertical control:
+
+```html
+
+ Horizontal scroll, vertical clip
+
+
+ Horizontal always scrolls, vertical scrolls if needed
+
+```
+
+### Overflow clip
+
+Use `overflow-clip` for clip behavior (similar to hidden but different scroll behavior):
+
+```html
+Clips without creating scroll container
+```
+
+## Key Points
+
+- `overflow-auto` - shows scrollbars only when needed
+- `overflow-hidden` - clips content that overflows
+- `overflow-visible` - allows content to overflow (default for most elements)
+- `overflow-scroll` - always shows scrollbars
+- `overflow-clip` - clips without creating scroll container
+- Use `overflow-x-*` and `overflow-y-*` for axis-specific control
+- Common pattern: `overflow-hidden` for images, `overflow-auto` for scrollable content
+
+
diff --git a/.agents/skills/tailwindcss/references/layout-padding.md b/.agents/skills/tailwindcss/references/layout-padding.md
new file mode 100644
index 0000000..95f7f11
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/layout-padding.md
@@ -0,0 +1,77 @@
+---
+name: layout-padding
+description: Controlling element padding with spacing scale, logical properties, and directional utilities
+---
+
+# Padding
+
+Utilities for controlling an element's padding.
+
+## Usage
+
+### All sides
+
+Use `p-` to set padding on all sides:
+
+```html
+Padding on all sides
+Larger padding
+```
+
+### Individual sides
+
+Use `pt-`, `pr-`, `pb-`, `pl-`:
+
+```html
+Top padding
+Right padding
+Bottom padding
+Left padding
+```
+
+### Horizontal and vertical
+
+Use `px-` for horizontal, `py-` for vertical:
+
+```html
+Horizontal padding
+Vertical padding
+```
+
+### Logical properties
+
+Use `ps-` (padding-inline-start) and `pe-` (padding-inline-end) for RTL support:
+
+```html
+
+
Left padding in LTR
+
Right padding in LTR
+
+
+
Right padding in RTL
+
Left padding in RTL
+
+```
+
+### Custom values
+
+Use arbitrary values for custom padding:
+
+```html
+Custom padding
+Custom calculation
+```
+
+## Key Points
+
+- Spacing scale: `p-0` through `p-96` (and beyond)
+- Individual: `pt-*`, `pr-*`, `pb-*`, `pl-*` for specific sides
+- Axes: `px-*` (horizontal), `py-*` (vertical)
+- Logical: `ps-*` (start), `pe-*` (end) adapt to text direction
+- No negative padding - padding cannot be negative in CSS
+- Common patterns: `p-4`, `px-6`, `py-8`, `pt-2`, `pb-4`
+
+
diff --git a/.agents/skills/tailwindcss/references/layout-position.md b/.agents/skills/tailwindcss/references/layout-position.md
new file mode 100644
index 0000000..cfa0553
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/layout-position.md
@@ -0,0 +1,85 @@
+---
+name: layout-position
+description: Controlling element positioning with static, relative, absolute, fixed, and sticky utilities
+---
+
+# Position
+
+Utilities for controlling how an element is positioned in the document.
+
+## Usage
+
+### Static positioning
+
+Use `static` to position an element according to the normal flow. Offsets are ignored and it won't act as a position reference for absolutely positioned children:
+
+```html
+
+```
+
+### Relative positioning
+
+Use `relative` to position an element in normal flow. Offsets are calculated relative to the element's normal position, and it acts as a position reference for absolutely positioned children:
+
+```html
+
+```
+
+### Absolute positioning
+
+Use `absolute` to position an element outside the normal flow. Neighboring elements act as if it doesn't exist. Offsets are calculated relative to the nearest positioned parent:
+
+```html
+
+```
+
+### Fixed positioning
+
+Use `fixed` to position an element relative to the browser window. Offsets are calculated relative to the viewport:
+
+```html
+
+```
+
+### Sticky positioning
+
+Use `sticky` to position an element as `relative` until it crosses a threshold, then treat it as `fixed` until its parent is off screen:
+
+```html
+
+```
+
+## Key Points
+
+- `static` is the default - elements flow normally
+- `relative` maintains normal flow but allows offsets and becomes a positioning context
+- `absolute` removes from flow and positions relative to nearest positioned ancestor
+- `fixed` positions relative to viewport, stays in place when scrolling
+- `sticky` combines relative and fixed behavior based on scroll position
+- Always use with offset utilities like `top-0`, `right-0`, `inset-0`, etc.
+
+
diff --git a/.agents/skills/tailwindcss/references/layout-tables.md b/.agents/skills/tailwindcss/references/layout-tables.md
new file mode 100644
index 0000000..e26216d
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/layout-tables.md
@@ -0,0 +1,67 @@
+---
+name: layout-tables
+description: Table layout and border behavior with border-collapse, table-layout
+---
+
+# Table Layout
+
+Utilities for controlling table display, border behavior, and layout algorithm.
+
+## Usage
+
+### Border collapse
+
+```html
+
+
+
+
+ State
+ City
+
+
+
+
+ Indiana
+ Indianapolis
+
+
+
+
+
+
+```
+
+### Table layout
+
+```html
+
+
+
+
+
+```
+
+### Table display (from layout-display)
+
+Combine with `table`, `table-row`, `table-cell`, `table-header-group`, etc. for semantic table structure.
+
+### Responsive
+
+```html
+
+```
+
+## Key Points
+
+- `border-collapse` - adjacent borders merge (single border between cells)
+- `border-separate` - each cell displays its own borders
+- `table-auto` - column widths from content
+- `table-fixed` - fixed layout; first row sets column widths
+- In v4, `border-*` and `divide-*` default to `currentColor`; specify a color (e.g. `border-gray-200`) explicitly
+
+
diff --git a/.agents/skills/tailwindcss/references/layout-width.md b/.agents/skills/tailwindcss/references/layout-width.md
new file mode 100644
index 0000000..6242093
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/layout-width.md
@@ -0,0 +1,102 @@
+---
+name: layout-width
+description: Setting element width with spacing scale, fractions, container sizes, and viewport units
+---
+
+# Width
+
+Utilities for setting the width of an element.
+
+## Usage
+
+### Spacing scale
+
+Use `w-` utilities based on the spacing scale:
+
+```html
+w-24
+w-48
+w-64
+```
+
+### Percentage widths
+
+Use `w-full` or `w-` for percentage-based widths:
+
+```html
+
+
+100%
+```
+
+### Container scale
+
+Use container size utilities like `w-sm`, `w-md`, `w-lg`:
+
+```html
+Small container
+Medium container
+Extra large container
+```
+
+### Viewport units
+
+Use `w-screen` for full viewport width, or dynamic viewport units:
+
+```html
+Full viewport width
+Dynamic viewport width
+Small viewport width
+Large viewport width
+```
+
+### Content-based widths
+
+Use `w-auto`, `w-fit`, `w-min`, `w-max` for content-based sizing:
+
+```html
+Auto width
+Fit content
+Min content
+Max content
+```
+
+### Size utility
+
+Use `size-` to set both width and height:
+
+```html
+16x16
+24x24
+100% x 100%
+```
+
+### Custom values
+
+Use arbitrary values for custom widths:
+
+```html
+Custom pixel width
+Custom percentage
+Custom calculation
+```
+
+## Key Points
+
+- Spacing scale: `w-0` through `w-96` (and beyond)
+- Fractions: `w-1/2`, `w-1/3`, `w-2/3`, `w-1/4`, `w-3/4`, `w-1/5`, `w-4/5`, `w-1/6`, `w-5/6`
+- Container sizes: `w-3xs`, `w-2xs`, `w-xs`, `w-sm`, `w-md`, `w-lg`, `w-xl`, `w-2xl`, `w-3xl`, `w-4xl`, `w-5xl`, `w-6xl`, `w-7xl`
+- Viewport units: `w-screen` (100vw), `w-dvw`, `w-svw`, `w-lvw`
+- `size-*` utilities set both width and height simultaneously
+- Use `w-auto` to reset width at specific breakpoints
+
+
diff --git a/.agents/skills/tailwindcss/references/transform-base.md b/.agents/skills/tailwindcss/references/transform-base.md
new file mode 100644
index 0000000..94acd22
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/transform-base.md
@@ -0,0 +1,68 @@
+---
+name: transform-base
+description: Base transform utilities for enabling transforms, hardware acceleration, and custom transform values
+---
+
+# Transform
+
+Base utilities for transforming elements.
+
+## Usage
+
+### Hardware acceleration
+
+Use `transform-gpu` to force GPU acceleration for better performance:
+
+```html
+
+ GPU-accelerated transform
+
+```
+
+### CPU rendering
+
+Use `transform-cpu` to force CPU rendering if needed:
+
+```html
+
+ CPU-rendered transform
+
+```
+
+### Removing transforms
+
+Use `transform-none` to remove all transforms:
+
+```html
+
+ Skewed on mobile, normal on desktop
+
+```
+
+### Custom transforms
+
+Use arbitrary values for custom transform functions:
+
+```html
+
+ Custom matrix transform
+
+
+ Custom 3D transform
+
+```
+
+## Key Points
+
+- `transform-gpu` enables hardware acceleration with `translateZ(0)`
+- `transform-cpu` forces CPU rendering (removes GPU acceleration)
+- `transform-none` removes all transforms at once
+- Use `transform-gpu` for better animation performance
+- Custom transforms use arbitrary values: `transform-[...]`
+- Transform utilities (translate, rotate, scale, skew) automatically enable transform
+- Hardware acceleration is usually beneficial for animations
+
+
diff --git a/.agents/skills/tailwindcss/references/transform-rotate.md b/.agents/skills/tailwindcss/references/transform-rotate.md
new file mode 100644
index 0000000..c4fb5f6
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/transform-rotate.md
@@ -0,0 +1,70 @@
+---
+name: transform-rotate
+description: Rotating elements in 2D and 3D space with degree values and custom rotations
+---
+
+# Rotate
+
+Utilities for rotating elements.
+
+## Usage
+
+### 2D rotation
+
+Use `rotate-` to rotate by degrees:
+
+```html
+45 degrees
+90 degrees
+-45 degrees (counterclockwise)
+```
+
+### 3D rotation
+
+Use `rotate-x-`, `rotate-y-`, `rotate-z-` for 3D rotation:
+
+```html
+3D rotation
+Combined 3D rotation
+```
+
+### Common rotations
+
+```html
+Quarter turn
+Half turn
+Three-quarter turn
+```
+
+### Custom values
+
+Use arbitrary values for custom rotations:
+
+```html
+Custom radian rotation
+Explicit degree rotation
+```
+
+### Removing rotation
+
+Use `rotate-none` to remove rotation:
+
+```html
+Rotated on mobile only
+```
+
+## Key Points
+
+- `rotate-*` rotates in 2D plane (around z-axis)
+- `rotate-x-*` rotates around x-axis (3D)
+- `rotate-y-*` rotates around y-axis (3D)
+- `rotate-z-*` rotates around z-axis (3D, same as `rotate-*`)
+- Negative values rotate counterclockwise: `-rotate-45`
+- Common values: `45`, `90`, `180`, `270` degrees
+- Can combine multiple axes: `rotate-x-50 rotate-y-30 rotate-z-45`
+- Use `rotate-none` to remove all rotations
+
+
diff --git a/.agents/skills/tailwindcss/references/transform-scale.md b/.agents/skills/tailwindcss/references/transform-scale.md
new file mode 100644
index 0000000..5c82b24
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/transform-scale.md
@@ -0,0 +1,83 @@
+---
+name: transform-scale
+description: Scaling elements uniformly or on specific axes with percentage values
+---
+
+# Scale
+
+Utilities for scaling elements.
+
+## Usage
+
+### Uniform scaling
+
+Use `scale-` to scale on both axes (number represents percentage):
+
+```html
+75% size
+100% size (default)
+125% size
+150% size
+```
+
+### Axis-specific scaling
+
+Use `scale-x-` or `scale-y-` to scale on one axis:
+
+```html
+75% width
+125% height
+```
+
+### Negative scaling
+
+Use negative values to mirror and scale:
+
+```html
+Mirrored horizontally
+Mirrored vertically
+Mirrored both axes
+```
+
+### Hover effects
+
+Common pattern for interactive scaling:
+
+```html
+
+ Grows on hover
+
+```
+
+### Removing scale
+
+Use `scale-none` to remove scaling:
+
+```html
+Scaled on mobile only
+```
+
+### Custom values
+
+Use arbitrary values for custom scaling:
+
+```html
+Custom scale value
+Custom x-axis scale
+```
+
+## Key Points
+
+- `scale-*` scales uniformly on both axes
+- `scale-x-*` scales horizontally only
+- `scale-y-*` scales vertically only
+- Values represent percentages: `scale-75` = 75%, `scale-125` = 125%
+- `scale-100` is the default (no scaling)
+- Negative values mirror the element: `-scale-x-100` flips horizontally
+- Common for hover effects: `hover:scale-110`, `active:scale-95`
+- Use `scale-none` to remove all scaling
+
+
diff --git a/.agents/skills/tailwindcss/references/transform-skew.md b/.agents/skills/tailwindcss/references/transform-skew.md
new file mode 100644
index 0000000..4210d83
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/transform-skew.md
@@ -0,0 +1,62 @@
+---
+name: transform-skew
+description: Skewing elements on x and y axes with degree values
+---
+
+# Skew
+
+Utilities for skewing (distorting) elements.
+
+## Usage
+
+### Skewing both axes
+
+Use `skew-` to skew on both axes:
+
+```html
+Slight skew
+Moderate skew
+Strong skew
+```
+
+### Axis-specific skewing
+
+Use `skew-x-` or `skew-y-` to skew on one axis:
+
+```html
+Skewed horizontally
+Skewed vertically
+```
+
+### Negative skewing
+
+Use negative values for opposite direction:
+
+```html
+Negative skew
+Negative x-axis skew
+```
+
+### Custom values
+
+Use arbitrary values for custom skew:
+
+```html
+Custom radian skew
+Custom degree skew
+```
+
+## Key Points
+
+- `skew-*` skews on both x and y axes
+- `skew-x-*` skews horizontally only
+- `skew-y-*` skews vertically only
+- Values are in degrees: `skew-3` = 3 degrees
+- Negative values skew in opposite direction
+- Common values: `3`, `6`, `12` degrees for subtle effects
+- Use sparingly - excessive skewing can make text hard to read
+
+
diff --git a/.agents/skills/tailwindcss/references/transform-translate.md b/.agents/skills/tailwindcss/references/transform-translate.md
new file mode 100644
index 0000000..e530a38
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/transform-translate.md
@@ -0,0 +1,77 @@
+---
+name: transform-translate
+description: Translating elements on x, y, and z axes with spacing scale, percentages, and custom values
+---
+
+# Translate
+
+Utilities for translating (moving) elements.
+
+## Usage
+
+### Spacing scale
+
+Use `translate-` to translate on both axes, or `translate-x-` / `translate-y-` for single axis:
+
+```html
+Moved 2 units
+Moved -4 units
+Moved right 4 units
+Moved down 6 units
+```
+
+### Percentage translation
+
+Use `translate-` to translate by percentage of element size:
+
+```html
+Moved 50% on both axes
+Moved 25% right
+Moved 100% up
+```
+
+### Centering elements
+
+Common pattern for centering absolutely positioned elements:
+
+```html
+
+ Centered
+
+```
+
+### Z-axis translation
+
+Use `translate-z-` for 3D translation (requires `transform-3d` on parent):
+
+```html
+
+```
+
+### Custom values
+
+Use arbitrary values for custom translations:
+
+```html
+Custom translation
+Custom pixel value
+```
+
+## Key Points
+
+- `translate-*` moves on both x and y axes
+- `translate-x-*` moves horizontally (right = positive)
+- `translate-y-*` moves vertically (down = positive)
+- `translate-z-*` moves in 3D space (forward = positive)
+- Negative values use dash prefix: `-translate-4`, `-translate-x-8`
+- Percentages are relative to element's own size
+- Common centering: `top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2`
+- Z-axis requires `transform-3d` utility on parent element
+
+
diff --git a/.agents/skills/tailwindcss/references/typography-font-text.md b/.agents/skills/tailwindcss/references/typography-font-text.md
new file mode 100644
index 0000000..dd69166
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/typography-font-text.md
@@ -0,0 +1,118 @@
+---
+name: typography-font-text
+description: Font size, weight, style, text color, line height, letter spacing, and text decoration
+---
+
+# Typography: Font & Text
+
+Utilities for font styling, text color, line height, letter spacing, and decoration.
+
+## Usage
+
+### Font size
+
+```html
+Extra small
+Small
+Base (default)
+Large
+Extra large
+2xl
+Arbitrary
+```
+
+### Font weight
+
+```html
+100
+300
+400
+500
+600
+700
+
+```
+
+### Font style and family
+
+```html
+Italic
+Not italic
+Sans (default)
+Serif
+Monospace
+```
+
+### Text color
+
+```html
+Dark text
+Blue
+50% opacity
+Arbitrary color
+CSS variable
+```
+
+### Line height
+
+```html
+1
+1.25
+1.5
+1.625
+2
+Arbitrary
+```
+
+### Letter spacing
+
+```html
+-0.05em
+-0.025em
+0
+0.025em
+0.1em
+```
+
+### Text decoration
+
+```html
+Underline
+Strikethrough
+Remove
+Overline
+Custom
+```
+
+### Text transform and overflow
+
+```html
+UPPERCASE
+lowercase
+Capitalize Each
+Normal
+
+Single line ellipsis
+Ellipsis
+Clamp to 3 lines
+```
+
+## Key Points
+
+- Font size: `text-xs` through `text-9xl`, theme scale
+- Weight: `font-thin` to `font-black`
+- Color: `text-{color}-{shade}`, opacity modifier `/50`, arbitrary `text-[#hex]`
+- Line height: `leading-none`, `leading-tight`, `leading-normal`, `leading-loose`
+- Letter spacing: `tracking-tighter` to `tracking-widest`
+- Decoration: `underline`, `line-through`, `no-underline`, `decoration-*`, `underline-offset-*`
+- Overflow: `truncate` (ellipsis), `line-clamp-{n}`
+
+
diff --git a/.agents/skills/tailwindcss/references/typography-list-style.md b/.agents/skills/tailwindcss/references/typography-list-style.md
new file mode 100644
index 0000000..dcfe46c
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/typography-list-style.md
@@ -0,0 +1,65 @@
+---
+name: typography-list-style
+description: Controlling list marker style and position with list-style-type and list-style-position
+---
+
+# List Style
+
+Utilities for controlling the marker style and position of list items.
+
+## Usage
+
+### List style type
+
+```html
+
+ Disc bullets (default for ul)
+
+
+
+ Decimal numbers (default for ol)
+
+
+
+ No markers (often with custom bullets via before/after)
+
+
+
+Roman numerals
+
+```
+
+### List style position
+
+```html
+
+
+ 5 cups chopped Porcini mushrooms
+
+
+
+
+ 5 cups chopped Porcini mushrooms
+
+```
+
+### Responsive
+
+```html
+
+```
+
+## Key Points
+
+- `list-disc` - disc bullets (ul default)
+- `list-decimal` - decimal numbers (ol default)
+- `list-none` - no markers
+- Custom: `list-[upper-roman]`, `list-[lower-alpha]`, `list-(--var)`
+- `list-inside` - markers inside content box
+- `list-outside` - markers outside content box (default)
+
+
diff --git a/.agents/skills/tailwindcss/references/typography-text-align.md b/.agents/skills/tailwindcss/references/typography-text-align.md
new file mode 100644
index 0000000..1a74c1e
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/typography-text-align.md
@@ -0,0 +1,60 @@
+---
+name: typography-text-align
+description: Controlling text alignment with left, center, right, justify, and logical properties
+---
+
+# Text Align
+
+Utilities for controlling the alignment of text.
+
+## Usage
+
+### Basic alignment
+
+Use `text-left`, `text-center`, `text-right`, `text-justify`:
+
+```html
+Left aligned
+Center aligned
+Right aligned
+Justified text
+```
+
+### Logical properties
+
+Use `text-start` and `text-end` for RTL-aware alignment:
+
+```html
+
+
Left in LTR
+
Right in LTR
+
+
+
Right in RTL
+
Left in RTL
+
+```
+
+### Responsive alignment
+
+```html
+
+ Responsive alignment
+
+```
+
+## Key Points
+
+- `text-left` - aligns to left edge
+- `text-center` - centers text
+- `text-right` - aligns to right edge
+- `text-justify` - justifies text (spaces words evenly)
+- `text-start` - aligns to start (left in LTR, right in RTL)
+- `text-end` - aligns to end (right in LTR, left in RTL)
+- Use logical properties (`text-start`, `text-end`) for internationalization
+- Common pattern: `text-center` for headings, `text-left` for body text
+
+
diff --git a/.agents/skills/tailwindcss/references/visual-background.md b/.agents/skills/tailwindcss/references/visual-background.md
new file mode 100644
index 0000000..2cc9db6
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/visual-background.md
@@ -0,0 +1,76 @@
+---
+name: visual-background
+description: Background color, gradient, image, and attachment utilities
+---
+
+# Background
+
+Utilities for background color, gradients, images, and attachment.
+
+## Usage
+
+### Background color
+
+```html
+White
+Light gray
+Blue
+50% opacity
+Arbitrary
+CSS variable
+```
+
+Color palette follows theme (red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose, slate, gray, zinc, neutral, stone).
+
+### Background image and gradient
+
+```html
+Linear gradient
+Multi-stop
+
+Image
+Image with size/position
+```
+
+Gradient directions: `to-t`, `to-tr`, `to-r`, `to-br`, `to-b`, `to-bl`, `to-l`, `to-tl`.
+
+### Background size and position
+
+```html
+auto
+cover
+contain
+
+center
+top
+bottom
+left
+right
+Arbitrary
+```
+
+### Background repeat and attachment
+
+```html
+repeat (default)
+no-repeat
+repeat-x
+
+fixed (parallax)
+local
+scroll
+```
+
+## Key Points
+
+- Colors: `bg-{color}-{shade}`, opacity `/50`, arbitrary `bg-[#hex]`
+- Gradients: `bg-gradient-to-{dir}`, `from-*`, `via-*`, `to-*`
+- Image: `bg-[url('...')]`, `bg-cover`, `bg-center`, etc.
+- Size: `bg-auto`, `bg-cover`, `bg-contain`
+- Position: `bg-center`, `bg-top`, `bg-[position:...]`
+
+
diff --git a/.agents/skills/tailwindcss/references/visual-border.md b/.agents/skills/tailwindcss/references/visual-border.md
new file mode 100644
index 0000000..a1a5e48
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/visual-border.md
@@ -0,0 +1,108 @@
+---
+name: visual-border
+description: Border width, color, style, and border radius
+---
+
+# Border
+
+Utilities for border width, color, style, and radius.
+
+## Usage
+
+### Border width
+
+```html
+1px all sides
+2px
+No border
+
+Per side
+Horizontal
+Vertical
+```
+
+### Border color
+
+```html
+Gray border
+Blue
+With opacity
+```
+
+### Border style
+
+```html
+Solid (default)
+Dashed
+Dotted
+Double
+None
+```
+
+### Border radius
+
+```html
+Small (default)
+2px
+6px
+8px
+12px
+16px
+Pill/circle
+0
+
+Per corner
+Logical (start/end)
+```
+
+### Divide (between children)
+
+```html
+
+
Item 1
+
Item 2
+
Item 3
+
+
+```
+
+### Ring (focus outline)
+
+```html
+Ring
+Focus ring
+```
+
+### Outline
+
+Separate from border; used for focus states. In v4: `outline` = 1px; `outline-2`, `outline-4` for width. Use `outline-offset-2` for offset.
+
+```html
+Outline
+Focus outline
+
+Focus outline-hidden
+```
+
+v4: `outline-none` = `outline-style: none`; `outline-hidden` = invisible but shows in forced-colors mode.
+
+## Key Points
+
+- Width: `border`, `border-{0,2,4,8}`, `border-{t,r,b,l,x,y}`
+- Color: `border-{color}`, opacity modifier
+- Radius: `rounded-{size}`, `rounded-full`, `rounded-{t,r,b,l,s,e}-*`, logical `rounded-s-*`, `rounded-e-*`
+- Divide: `divide-{x,y}`, `divide-{color}` for borders between flex/grid children
+- Ring: `ring`, `ring-{n}`, `ring-{color}`, `ring-offset-{n}` (v4 default ring = 1px; use `ring-3` for 3px)
+- Outline: `outline`, `outline-{n}`, `outline-{color}`, `outline-offset-{n}`, `outline-hidden`, `outline-none`
+
+
diff --git a/.agents/skills/tailwindcss/references/visual-effects.md b/.agents/skills/tailwindcss/references/visual-effects.md
new file mode 100644
index 0000000..5685190
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/visual-effects.md
@@ -0,0 +1,90 @@
+---
+name: visual-effects
+description: Box shadow, opacity, mix-blend, and filter effects
+---
+
+# Effects
+
+Utilities for box shadow, opacity, mix-blend, and filters.
+
+## Usage
+
+### Box shadow
+
+```html
+Small
+Default
+Medium
+Large
+Extra large
+2xl
+None
+
+Colored shadow
+Arbitrary
+```
+
+### Opacity
+
+```html
+Invisible
+50%
+Full
+```
+
+### Mix blend mode
+
+```html
+Normal
+Multiply
+Screen
+Overlay
+```
+
+### Backdrop blur and filter
+
+```html
+Blur backdrop
+Medium blur
+No blur
+
+Backdrop opacity
+```
+
+### Filter (blur, brightness, contrast, etc.)
+
+```html
+Blur
+Brightness
+Contrast
+Grayscale
+Invert
+Sepia
+No filter
+```
+
+### Object fit (images/video)
+
+```html
+
+
+
+
+
+
+```
+
+## Key Points
+
+- Shadow: `shadow-{size}`, `shadow-{color}/opacity`, `shadow-none`
+- Opacity: `opacity-{0-100}`
+- Mix blend: `mix-blend-{mode}`
+- Backdrop: `backdrop-blur-*`, `backdrop-opacity-*`
+- Filter: `blur-*`, `brightness-*`, `contrast-*`, `grayscale`, `invert`, `sepia`
+
+
diff --git a/.agents/skills/tailwindcss/references/visual-svg.md b/.agents/skills/tailwindcss/references/visual-svg.md
new file mode 100644
index 0000000..6e17516
--- /dev/null
+++ b/.agents/skills/tailwindcss/references/visual-svg.md
@@ -0,0 +1,82 @@
+---
+name: visual-svg
+description: Styling SVG elements with fill, stroke, and stroke-width utilities
+---
+
+# SVG Styling
+
+Utilities for styling SVG fill and stroke. Essential when working with icon sets like Heroicons.
+
+## Usage
+
+### Fill
+
+```html
+
+...
+...
+
+
+
+ ...
+ Check for updates
+
+
+
+...
+...
+...
+
+
+...
+...
+```
+
+### Stroke
+
+```html
+
+...
+
+
+
+ ...
+ Download
+
+
+
+...
+```
+
+### Stroke width
+
+```html
+Thin stroke
+Medium stroke
+Custom width
+Custom property
+```
+
+### Combined
+
+```html
+
+
+
+```
+
+## Key Points
+
+- `fill-*` / `stroke-*` - all theme colors (e.g. `fill-red-500`)
+- `fill-current` / `stroke-current` - use current text color (common for icons in buttons)
+- `fill-none` / `stroke-none` - no fill/stroke
+- `stroke-1`, `stroke-2`, etc. - stroke width (number = px)
+- Custom: `fill-[#hex]`, `stroke-(--var)`, `stroke-[1.5]`
+- Use with variants: `hover:fill-blue-600`, `md:stroke-2`
+
+
diff --git a/.agents/skills/vercel-react-best-practices/AGENTS.md b/.agents/skills/vercel-react-best-practices/AGENTS.md
new file mode 100644
index 0000000..3bdafa1
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/AGENTS.md
@@ -0,0 +1,3254 @@
+# React Best Practices
+
+**Version 1.0.0**
+Vercel Engineering
+January 2026
+
+> **Note:**
+> This document is mainly for agents and LLMs to follow when maintaining,
+> generating, or refactoring React and Next.js codebases. Humans
+> may also find it useful, but guidance here is optimized for automation
+> and consistency by AI-assisted workflows.
+
+---
+
+## Abstract
+
+Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.
+
+---
+
+## Table of Contents
+
+1. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL**
+ - 1.1 [Defer Await Until Needed](#11-defer-await-until-needed)
+ - 1.2 [Dependency-Based Parallelization](#12-dependency-based-parallelization)
+ - 1.3 [Prevent Waterfall Chains in API Routes](#13-prevent-waterfall-chains-in-api-routes)
+ - 1.4 [Promise.all() for Independent Operations](#14-promiseall-for-independent-operations)
+ - 1.5 [Strategic Suspense Boundaries](#15-strategic-suspense-boundaries)
+2. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL**
+ - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports)
+ - 2.2 [Conditional Module Loading](#22-conditional-module-loading)
+ - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries)
+ - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components)
+ - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent)
+3. [Server-Side Performance](#3-server-side-performance) — **HIGH**
+ - 3.1 [Authenticate Server Actions Like API Routes](#31-authenticate-server-actions-like-api-routes)
+ - 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props)
+ - 3.3 [Cross-Request LRU Caching](#33-cross-request-lru-caching)
+ - 3.4 [Hoist Static I/O to Module Level](#34-hoist-static-io-to-module-level)
+ - 3.5 [Minimize Serialization at RSC Boundaries](#35-minimize-serialization-at-rsc-boundaries)
+ - 3.6 [Parallel Data Fetching with Component Composition](#36-parallel-data-fetching-with-component-composition)
+ - 3.7 [Per-Request Deduplication with React.cache()](#37-per-request-deduplication-with-reactcache)
+ - 3.8 [Use after() for Non-Blocking Operations](#38-use-after-for-non-blocking-operations)
+4. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH**
+ - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners)
+ - 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance)
+ - 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication)
+ - 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data)
+5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM**
+ - 5.1 [Calculate Derived State During Rendering](#51-calculate-derived-state-during-rendering)
+ - 5.2 [Defer State Reads to Usage Point](#52-defer-state-reads-to-usage-point)
+ - 5.3 [Do not wrap a simple expression with a primitive result type in useMemo](#53-do-not-wrap-a-simple-expression-with-a-primitive-result-type-in-usememo)
+ - 5.4 [Don't Define Components Inside Components](#54-dont-define-components-inside-components)
+ - 5.5 [Extract Default Non-primitive Parameter Value from Memoized Component to Constant](#55-extract-default-non-primitive-parameter-value-from-memoized-component-to-constant)
+ - 5.6 [Extract to Memoized Components](#56-extract-to-memoized-components)
+ - 5.7 [Narrow Effect Dependencies](#57-narrow-effect-dependencies)
+ - 5.8 [Put Interaction Logic in Event Handlers](#58-put-interaction-logic-in-event-handlers)
+ - 5.9 [Subscribe to Derived State](#59-subscribe-to-derived-state)
+ - 5.10 [Use Functional setState Updates](#510-use-functional-setstate-updates)
+ - 5.11 [Use Lazy State Initialization](#511-use-lazy-state-initialization)
+ - 5.12 [Use Transitions for Non-Urgent Updates](#512-use-transitions-for-non-urgent-updates)
+ - 5.13 [Use useRef for Transient Values](#513-use-useref-for-transient-values)
+6. [Rendering Performance](#6-rendering-performance) — **MEDIUM**
+ - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element)
+ - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists)
+ - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements)
+ - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision)
+ - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering)
+ - 6.6 [Suppress Expected Hydration Mismatches](#66-suppress-expected-hydration-mismatches)
+ - 6.7 [Use Activity Component for Show/Hide](#67-use-activity-component-for-showhide)
+ - 6.8 [Use defer or async on Script Tags](#68-use-defer-or-async-on-script-tags)
+ - 6.9 [Use Explicit Conditional Rendering](#69-use-explicit-conditional-rendering)
+ - 6.10 [Use React DOM Resource Hints](#610-use-react-dom-resource-hints)
+ - 6.11 [Use useTransition Over Manual Loading States](#611-use-usetransition-over-manual-loading-states)
+7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM**
+ - 7.1 [Avoid Layout Thrashing](#71-avoid-layout-thrashing)
+ - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups)
+ - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops)
+ - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls)
+ - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls)
+ - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations)
+ - 7.7 [Early Length Check for Array Comparisons](#77-early-length-check-for-array-comparisons)
+ - 7.8 [Early Return from Functions](#78-early-return-from-functions)
+ - 7.9 [Hoist RegExp Creation](#79-hoist-regexp-creation)
+ - 7.10 [Use flatMap to Map and Filter in One Pass](#710-use-flatmap-to-map-and-filter-in-one-pass)
+ - 7.11 [Use Loop for Min/Max Instead of Sort](#711-use-loop-for-minmax-instead-of-sort)
+ - 7.12 [Use Set/Map for O(1) Lookups](#712-use-setmap-for-o1-lookups)
+ - 7.13 [Use toSorted() Instead of sort() for Immutability](#713-use-tosorted-instead-of-sort-for-immutability)
+8. [Advanced Patterns](#8-advanced-patterns) — **LOW**
+ - 8.1 [Initialize App Once, Not Per Mount](#81-initialize-app-once-not-per-mount)
+ - 8.2 [Store Event Handlers in Refs](#82-store-event-handlers-in-refs)
+ - 8.3 [useEffectEvent for Stable Callback Refs](#83-useeffectevent-for-stable-callback-refs)
+
+---
+
+## 1. Eliminating Waterfalls
+
+**Impact: CRITICAL**
+
+Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
+
+### 1.1 Defer Await Until Needed
+
+**Impact: HIGH (avoids blocking unused code paths)**
+
+Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
+
+**Incorrect: blocks both branches**
+
+```typescript
+async function handleRequest(userId: string, skipProcessing: boolean) {
+ const userData = await fetchUserData(userId)
+
+ if (skipProcessing) {
+ // Returns immediately but still waited for userData
+ return { skipped: true }
+ }
+
+ // Only this branch uses userData
+ return processUserData(userData)
+}
+```
+
+**Correct: only blocks when needed**
+
+```typescript
+async function handleRequest(userId: string, skipProcessing: boolean) {
+ if (skipProcessing) {
+ // Returns immediately without waiting
+ return { skipped: true }
+ }
+
+ // Fetch only when needed
+ const userData = await fetchUserData(userId)
+ return processUserData(userData)
+}
+```
+
+**Another example: early return optimization**
+
+```typescript
+// Incorrect: always fetches permissions
+async function updateResource(resourceId: string, userId: string) {
+ const permissions = await fetchPermissions(userId)
+ const resource = await getResource(resourceId)
+
+ if (!resource) {
+ return { error: 'Not found' }
+ }
+
+ if (!permissions.canEdit) {
+ return { error: 'Forbidden' }
+ }
+
+ return await updateResourceData(resource, permissions)
+}
+
+// Correct: fetches only when needed
+async function updateResource(resourceId: string, userId: string) {
+ const resource = await getResource(resourceId)
+
+ if (!resource) {
+ return { error: 'Not found' }
+ }
+
+ const permissions = await fetchPermissions(userId)
+
+ if (!permissions.canEdit) {
+ return { error: 'Forbidden' }
+ }
+
+ return await updateResourceData(resource, permissions)
+}
+```
+
+This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
+
+### 1.2 Dependency-Based Parallelization
+
+**Impact: CRITICAL (2-10× improvement)**
+
+For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
+
+**Incorrect: profile waits for config unnecessarily**
+
+```typescript
+const [user, config] = await Promise.all([
+ fetchUser(),
+ fetchConfig()
+])
+const profile = await fetchProfile(user.id)
+```
+
+**Correct: config and profile run in parallel**
+
+```typescript
+import { all } from 'better-all'
+
+const { user, config, profile } = await all({
+ async user() { return fetchUser() },
+ async config() { return fetchConfig() },
+ async profile() {
+ return fetchProfile((await this.$.user).id)
+ }
+})
+```
+
+**Alternative without extra dependencies:**
+
+```typescript
+const userPromise = fetchUser()
+const profilePromise = userPromise.then(user => fetchProfile(user.id))
+
+const [user, config, profile] = await Promise.all([
+ userPromise,
+ fetchConfig(),
+ profilePromise
+])
+```
+
+We can also create all the promises first, and do `Promise.all()` at the end.
+
+Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
+
+### 1.3 Prevent Waterfall Chains in API Routes
+
+**Impact: CRITICAL (2-10× improvement)**
+
+In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
+
+**Incorrect: config waits for auth, data waits for both**
+
+```typescript
+export async function GET(request: Request) {
+ const session = await auth()
+ const config = await fetchConfig()
+ const data = await fetchData(session.user.id)
+ return Response.json({ data, config })
+}
+```
+
+**Correct: auth and config start immediately**
+
+```typescript
+export async function GET(request: Request) {
+ const sessionPromise = auth()
+ const configPromise = fetchConfig()
+ const session = await sessionPromise
+ const [config, data] = await Promise.all([
+ configPromise,
+ fetchData(session.user.id)
+ ])
+ return Response.json({ data, config })
+}
+```
+
+For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
+
+### 1.4 Promise.all() for Independent Operations
+
+**Impact: CRITICAL (2-10× improvement)**
+
+When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
+
+**Incorrect: sequential execution, 3 round trips**
+
+```typescript
+const user = await fetchUser()
+const posts = await fetchPosts()
+const comments = await fetchComments()
+```
+
+**Correct: parallel execution, 1 round trip**
+
+```typescript
+const [user, posts, comments] = await Promise.all([
+ fetchUser(),
+ fetchPosts(),
+ fetchComments()
+])
+```
+
+### 1.5 Strategic Suspense Boundaries
+
+**Impact: HIGH (faster initial paint)**
+
+Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
+
+**Incorrect: wrapper blocked by data fetching**
+
+```tsx
+async function Page() {
+ const data = await fetchData() // Blocks entire page
+
+ return (
+
+
Sidebar
+
Header
+
+
+
+
Footer
+
+ )
+}
+```
+
+The entire layout waits for data even though only the middle section needs it.
+
+**Correct: wrapper shows immediately, data streams in**
+
+```tsx
+function Page() {
+ return (
+
+
Sidebar
+
Header
+
+ }>
+
+
+
+
Footer
+
+ )
+}
+
+async function DataDisplay() {
+ const data = await fetchData() // Only blocks this component
+ return {data.content}
+}
+```
+
+Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
+
+**Alternative: share promise across components**
+
+```tsx
+function Page() {
+ // Start fetch immediately, but don't await
+ const dataPromise = fetchData()
+
+ return (
+
+
Sidebar
+
Header
+
}>
+
+
+
+
Footer
+
+ )
+}
+
+function DataDisplay({ dataPromise }: { dataPromise: Promise }) {
+ const data = use(dataPromise) // Unwraps the promise
+ return {data.content}
+}
+
+function DataSummary({ dataPromise }: { dataPromise: Promise }) {
+ const data = use(dataPromise) // Reuses the same promise
+ return {data.summary}
+}
+```
+
+Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.
+
+**When NOT to use this pattern:**
+
+- Critical data needed for layout decisions (affects positioning)
+
+- SEO-critical content above the fold
+
+- Small, fast queries where suspense overhead isn't worth it
+
+- When you want to avoid layout shift (loading → content jump)
+
+**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.
+
+---
+
+## 2. Bundle Size Optimization
+
+**Impact: CRITICAL**
+
+Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
+
+### 2.1 Avoid Barrel File Imports
+
+**Impact: CRITICAL (200-800ms import cost, slow builds)**
+
+Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).
+
+Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.
+
+**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.
+
+**Incorrect: imports entire library**
+
+```tsx
+import { Check, X, Menu } from 'lucide-react'
+// Loads 1,583 modules, takes ~2.8s extra in dev
+// Runtime cost: 200-800ms on every cold start
+
+import { Button, TextField } from '@mui/material'
+// Loads 2,225 modules, takes ~4.2s extra in dev
+```
+
+**Correct: imports only what you need**
+
+```tsx
+import Check from 'lucide-react/dist/esm/icons/check'
+import X from 'lucide-react/dist/esm/icons/x'
+import Menu from 'lucide-react/dist/esm/icons/menu'
+// Loads only 3 modules (~2KB vs ~1MB)
+
+import Button from '@mui/material/Button'
+import TextField from '@mui/material/TextField'
+// Loads only what you use
+```
+
+**Alternative: Next.js 13.5+**
+
+```js
+// next.config.js - use optimizePackageImports
+module.exports = {
+ experimental: {
+ optimizePackageImports: ['lucide-react', '@mui/material']
+ }
+}
+
+// Then you can keep the ergonomic barrel imports:
+import { Check, X, Menu } from 'lucide-react'
+// Automatically transformed to direct imports at build time
+```
+
+Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
+
+Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.
+
+Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
+
+### 2.2 Conditional Module Loading
+
+**Impact: HIGH (loads large data only when needed)**
+
+Load large data or modules only when a feature is activated.
+
+**Example: lazy-load animation frames**
+
+```tsx
+function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch> }) {
+ const [frames, setFrames] = useState (null)
+
+ useEffect(() => {
+ if (enabled && !frames && typeof window !== 'undefined') {
+ import('./animation-frames.js')
+ .then(mod => setFrames(mod.frames))
+ .catch(() => setEnabled(false))
+ }
+ }, [enabled, frames, setEnabled])
+
+ if (!frames) return
+ return
+}
+```
+
+The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
+
+### 2.3 Defer Non-Critical Third-Party Libraries
+
+**Impact: MEDIUM (loads after hydration)**
+
+Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
+
+**Incorrect: blocks initial bundle**
+
+```tsx
+import { Analytics } from '@vercel/analytics/react'
+
+export default function RootLayout({ children }) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+```
+
+**Correct: loads after hydration**
+
+```tsx
+import dynamic from 'next/dynamic'
+
+const Analytics = dynamic(
+ () => import('@vercel/analytics/react').then(m => m.Analytics),
+ { ssr: false }
+)
+
+export default function RootLayout({ children }) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+```
+
+### 2.4 Dynamic Imports for Heavy Components
+
+**Impact: CRITICAL (directly affects TTI and LCP)**
+
+Use `next/dynamic` to lazy-load large components not needed on initial render.
+
+**Incorrect: Monaco bundles with main chunk ~300KB**
+
+```tsx
+import { MonacoEditor } from './monaco-editor'
+
+function CodePanel({ code }: { code: string }) {
+ return
+}
+```
+
+**Correct: Monaco loads on demand**
+
+```tsx
+import dynamic from 'next/dynamic'
+
+const MonacoEditor = dynamic(
+ () => import('./monaco-editor').then(m => m.MonacoEditor),
+ { ssr: false }
+)
+
+function CodePanel({ code }: { code: string }) {
+ return
+}
+```
+
+### 2.5 Preload Based on User Intent
+
+**Impact: MEDIUM (reduces perceived latency)**
+
+Preload heavy bundles before they're needed to reduce perceived latency.
+
+**Example: preload on hover/focus**
+
+```tsx
+function EditorButton({ onClick }: { onClick: () => void }) {
+ const preload = () => {
+ if (typeof window !== 'undefined') {
+ void import('./monaco-editor')
+ }
+ }
+
+ return (
+
+ Open Editor
+
+ )
+}
+```
+
+**Example: preload when feature flag is enabled**
+
+```tsx
+function FlagsProvider({ children, flags }: Props) {
+ useEffect(() => {
+ if (flags.editorEnabled && typeof window !== 'undefined') {
+ void import('./monaco-editor').then(mod => mod.init())
+ }
+ }, [flags.editorEnabled])
+
+ return
+ {children}
+
+}
+```
+
+The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
+
+---
+
+## 3. Server-Side Performance
+
+**Impact: HIGH**
+
+Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
+
+### 3.1 Authenticate Server Actions Like API Routes
+
+**Impact: CRITICAL (prevents unauthorized access to server mutations)**
+
+Server Actions (functions with `"use server"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly.
+
+Next.js documentation explicitly states: "Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation."
+
+**Incorrect: no authentication check**
+
+```typescript
+'use server'
+
+export async function deleteUser(userId: string) {
+ // Anyone can call this! No auth check
+ await db.user.delete({ where: { id: userId } })
+ return { success: true }
+}
+```
+
+**Correct: authentication inside the action**
+
+```typescript
+'use server'
+
+import { verifySession } from '@/lib/auth'
+import { unauthorized } from '@/lib/errors'
+
+export async function deleteUser(userId: string) {
+ // Always check auth inside the action
+ const session = await verifySession()
+
+ if (!session) {
+ throw unauthorized('Must be logged in')
+ }
+
+ // Check authorization too
+ if (session.user.role !== 'admin' && session.user.id !== userId) {
+ throw unauthorized('Cannot delete other users')
+ }
+
+ await db.user.delete({ where: { id: userId } })
+ return { success: true }
+}
+```
+
+**With input validation:**
+
+```typescript
+'use server'
+
+import { verifySession } from '@/lib/auth'
+import { z } from 'zod'
+
+const updateProfileSchema = z.object({
+ userId: z.string().uuid(),
+ name: z.string().min(1).max(100),
+ email: z.string().email()
+})
+
+export async function updateProfile(data: unknown) {
+ // Validate input first
+ const validated = updateProfileSchema.parse(data)
+
+ // Then authenticate
+ const session = await verifySession()
+ if (!session) {
+ throw new Error('Unauthorized')
+ }
+
+ // Then authorize
+ if (session.user.id !== validated.userId) {
+ throw new Error('Can only update own profile')
+ }
+
+ // Finally perform the mutation
+ await db.user.update({
+ where: { id: validated.userId },
+ data: {
+ name: validated.name,
+ email: validated.email
+ }
+ })
+
+ return { success: true }
+}
+```
+
+Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)
+
+### 3.2 Avoid Duplicate Serialization in RSC Props
+
+**Impact: LOW (reduces network payload by avoiding duplicate serialization)**
+
+RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server.
+
+**Incorrect: duplicates array**
+
+```tsx
+// RSC: sends 6 strings (2 arrays × 3 items)
+
+```
+
+**Correct: sends 3 strings**
+
+```tsx
+// RSC: send once
+
+
+// Client: transform there
+'use client'
+const sorted = useMemo(() => [...usernames].sort(), [usernames])
+```
+
+**Nested deduplication behavior:**
+
+```tsx
+// string[] - duplicates everything
+usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
+
+// object[] - duplicates array structure only
+users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)
+```
+
+Deduplication works recursively. Impact varies by data type:
+
+- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated
+
+- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference
+
+**Operations breaking deduplication: create new references**
+
+- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`
+
+- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`
+
+**More examples:**
+
+```tsx
+// ❌ Bad
+ u.active)} />
+
+
+// ✅ Good
+
+
+// Do filtering/destructuring in client
+```
+
+**Exception:** Pass derived data when transformation is expensive or client doesn't need original.
+
+### 3.3 Cross-Request LRU Caching
+
+**Impact: HIGH (caches across requests)**
+
+`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
+
+**Implementation:**
+
+```typescript
+import { LRUCache } from 'lru-cache'
+
+const cache = new LRUCache({
+ max: 1000,
+ ttl: 5 * 60 * 1000 // 5 minutes
+})
+
+export async function getUser(id: string) {
+ const cached = cache.get(id)
+ if (cached) return cached
+
+ const user = await db.user.findUnique({ where: { id } })
+ cache.set(id, user)
+ return user
+}
+
+// Request 1: DB query, result cached
+// Request 2: cache hit, no DB query
+```
+
+Use when sequential user actions hit multiple endpoints needing the same data within seconds.
+
+**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis.
+
+**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
+
+Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
+
+### 3.4 Hoist Static I/O to Module Level
+
+**Impact: HIGH (avoids repeated file/network I/O per request)**
+
+When loading static assets (fonts, logos, images, config files) in route handlers or server functions, hoist the I/O operation to module level. Module-level code runs once when the module is first imported, not on every request. This eliminates redundant file system reads or network fetches that would otherwise run on every invocation.
+
+**Incorrect: reads font file on every request**
+
+**Correct: loads once at module initialization**
+
+**Alternative: synchronous file reads with Node.js fs**
+
+**General Node.js example: loading config or templates**
+
+**When to use this pattern:**
+
+- Loading fonts for OG image generation
+
+- Loading static logos, icons, or watermarks
+
+- Reading configuration files that don't change at runtime
+
+- Loading email templates or other static templates
+
+- Any static asset that's the same across all requests
+
+**When NOT to use this pattern:**
+
+- Assets that vary per request or user
+
+- Files that may change during runtime (use caching with TTL instead)
+
+- Large files that would consume too much memory if kept loaded
+
+- Sensitive data that shouldn't persist in memory
+
+**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** Module-level caching is especially effective because multiple concurrent requests share the same function instance. The static assets stay loaded in memory across requests without cold start penalties.
+
+**In traditional serverless:** Each cold start re-executes module-level code, but subsequent warm invocations reuse the loaded assets until the instance is recycled.
+
+### 3.5 Minimize Serialization at RSC Boundaries
+
+**Impact: HIGH (reduces data transfer size)**
+
+The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses.
+
+**Incorrect: serializes all 50 fields**
+
+```tsx
+async function Page() {
+ const user = await fetchUser() // 50 fields
+ return
+}
+
+'use client'
+function Profile({ user }: { user: User }) {
+ return {user.name}
// uses 1 field
+}
+```
+
+**Correct: serializes only 1 field**
+
+```tsx
+async function Page() {
+ const user = await fetchUser()
+ return
+}
+
+'use client'
+function Profile({ name }: { name: string }) {
+ return {name}
+}
+```
+
+### 3.6 Parallel Data Fetching with Component Composition
+
+**Impact: CRITICAL (eliminates server-side waterfalls)**
+
+React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
+
+**Incorrect: Sidebar waits for Page's fetch to complete**
+
+```tsx
+export default async function Page() {
+ const header = await fetchHeader()
+ return (
+
+ )
+}
+
+async function Sidebar() {
+ const items = await fetchSidebarItems()
+ return {items.map(renderItem)}
+}
+```
+
+**Correct: both fetch simultaneously**
+
+```tsx
+async function Header() {
+ const data = await fetchHeader()
+ return {data}
+}
+
+async function Sidebar() {
+ const items = await fetchSidebarItems()
+ return {items.map(renderItem)}
+}
+
+export default function Page() {
+ return (
+
+
+
+
+ )
+}
+```
+
+**Alternative with children prop:**
+
+```tsx
+async function Header() {
+ const data = await fetchHeader()
+ return {data}
+}
+
+async function Sidebar() {
+ const items = await fetchSidebarItems()
+ return {items.map(renderItem)}
+}
+
+function Layout({ children }: { children: ReactNode }) {
+ return (
+
+
+ {children}
+
+ )
+}
+
+export default function Page() {
+ return (
+
+
+
+ )
+}
+```
+
+### 3.7 Per-Request Deduplication with React.cache()
+
+**Impact: MEDIUM (deduplicates within request)**
+
+Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
+
+**Usage:**
+
+```typescript
+import { cache } from 'react'
+
+export const getCurrentUser = cache(async () => {
+ const session = await auth()
+ if (!session?.user?.id) return null
+ return await db.user.findUnique({
+ where: { id: session.user.id }
+ })
+})
+```
+
+Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
+
+**Avoid inline objects as arguments:**
+
+`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.
+
+**Incorrect: always cache miss**
+
+```typescript
+const getUser = cache(async (params: { uid: number }) => {
+ return await db.user.findUnique({ where: { id: params.uid } })
+})
+
+// Each call creates new object, never hits cache
+getUser({ uid: 1 })
+getUser({ uid: 1 }) // Cache miss, runs query again
+```
+
+**Correct: cache hit**
+
+```typescript
+const params = { uid: 1 }
+getUser(params) // Query runs
+getUser(params) // Cache hit (same reference)
+```
+
+If you must pass objects, pass the same reference:
+
+**Next.js-Specific Note:**
+
+In Next.js, the `fetch` API is automatically extended with request memoization. Requests with the same URL and options are automatically deduplicated within a single request, so you don't need `React.cache()` for `fetch` calls. However, `React.cache()` is still essential for other async tasks:
+
+- Database queries (Prisma, Drizzle, etc.)
+
+- Heavy computations
+
+- Authentication checks
+
+- File system operations
+
+- Any non-fetch async work
+
+Use `React.cache()` to deduplicate these operations across your component tree.
+
+Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache)
+
+### 3.8 Use after() for Non-Blocking Operations
+
+**Impact: MEDIUM (faster response times)**
+
+Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response.
+
+**Incorrect: blocks response**
+
+```tsx
+import { logUserAction } from '@/app/utils'
+
+export async function POST(request: Request) {
+ // Perform mutation
+ await updateDatabase(request)
+
+ // Logging blocks the response
+ const userAgent = request.headers.get('user-agent') || 'unknown'
+ await logUserAction({ userAgent })
+
+ return new Response(JSON.stringify({ status: 'success' }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ })
+}
+```
+
+**Correct: non-blocking**
+
+```tsx
+import { after } from 'next/server'
+import { headers, cookies } from 'next/headers'
+import { logUserAction } from '@/app/utils'
+
+export async function POST(request: Request) {
+ // Perform mutation
+ await updateDatabase(request)
+
+ // Log after response is sent
+ after(async () => {
+ const userAgent = (await headers()).get('user-agent') || 'unknown'
+ const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
+
+ logUserAction({ sessionCookie, userAgent })
+ })
+
+ return new Response(JSON.stringify({ status: 'success' }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ })
+}
+```
+
+The response is sent immediately while logging happens in the background.
+
+**Common use cases:**
+
+- Analytics tracking
+
+- Audit logging
+
+- Sending notifications
+
+- Cache invalidation
+
+- Cleanup tasks
+
+**Important notes:**
+
+- `after()` runs even if the response fails or redirects
+
+- Works in Server Actions, Route Handlers, and Server Components
+
+Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)
+
+---
+
+## 4. Client-Side Data Fetching
+
+**Impact: MEDIUM-HIGH**
+
+Automatic deduplication and efficient data fetching patterns reduce redundant network requests.
+
+### 4.1 Deduplicate Global Event Listeners
+
+**Impact: LOW (single listener for N components)**
+
+Use `useSWRSubscription()` to share global event listeners across component instances.
+
+**Incorrect: N instances = N listeners**
+
+```tsx
+function useKeyboardShortcut(key: string, callback: () => void) {
+ useEffect(() => {
+ const handler = (e: KeyboardEvent) => {
+ if (e.metaKey && e.key === key) {
+ callback()
+ }
+ }
+ window.addEventListener('keydown', handler)
+ return () => window.removeEventListener('keydown', handler)
+ }, [key, callback])
+}
+```
+
+When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.
+
+**Correct: N instances = 1 listener**
+
+```tsx
+import useSWRSubscription from 'swr/subscription'
+
+// Module-level Map to track callbacks per key
+const keyCallbacks = new Map void>>()
+
+function useKeyboardShortcut(key: string, callback: () => void) {
+ // Register this callback in the Map
+ useEffect(() => {
+ if (!keyCallbacks.has(key)) {
+ keyCallbacks.set(key, new Set())
+ }
+ keyCallbacks.get(key)!.add(callback)
+
+ return () => {
+ const set = keyCallbacks.get(key)
+ if (set) {
+ set.delete(callback)
+ if (set.size === 0) {
+ keyCallbacks.delete(key)
+ }
+ }
+ }
+ }, [key, callback])
+
+ useSWRSubscription('global-keydown', () => {
+ const handler = (e: KeyboardEvent) => {
+ if (e.metaKey && keyCallbacks.has(e.key)) {
+ keyCallbacks.get(e.key)!.forEach(cb => cb())
+ }
+ }
+ window.addEventListener('keydown', handler)
+ return () => window.removeEventListener('keydown', handler)
+ })
+}
+
+function Profile() {
+ // Multiple shortcuts will share the same listener
+ useKeyboardShortcut('p', () => { /* ... */ })
+ useKeyboardShortcut('k', () => { /* ... */ })
+ // ...
+}
+```
+
+### 4.2 Use Passive Event Listeners for Scrolling Performance
+
+**Impact: MEDIUM (eliminates scroll delay caused by event listeners)**
+
+Add `{ passive: true }` to touch and wheel event listeners to enable immediate scrolling. Browsers normally wait for listeners to finish to check if `preventDefault()` is called, causing scroll delay.
+
+**Incorrect:**
+
+```typescript
+useEffect(() => {
+ const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
+ const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
+
+ document.addEventListener('touchstart', handleTouch)
+ document.addEventListener('wheel', handleWheel)
+
+ return () => {
+ document.removeEventListener('touchstart', handleTouch)
+ document.removeEventListener('wheel', handleWheel)
+ }
+}, [])
+```
+
+**Correct:**
+
+```typescript
+useEffect(() => {
+ const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
+ const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
+
+ document.addEventListener('touchstart', handleTouch, { passive: true })
+ document.addEventListener('wheel', handleWheel, { passive: true })
+
+ return () => {
+ document.removeEventListener('touchstart', handleTouch)
+ document.removeEventListener('wheel', handleWheel)
+ }
+}, [])
+```
+
+**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
+
+**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.
+
+### 4.3 Use SWR for Automatic Deduplication
+
+**Impact: MEDIUM-HIGH (automatic deduplication)**
+
+SWR enables request deduplication, caching, and revalidation across component instances.
+
+**Incorrect: no deduplication, each instance fetches**
+
+```tsx
+function UserList() {
+ const [users, setUsers] = useState([])
+ useEffect(() => {
+ fetch('/api/users')
+ .then(r => r.json())
+ .then(setUsers)
+ }, [])
+}
+```
+
+**Correct: multiple instances share one request**
+
+```tsx
+import useSWR from 'swr'
+
+function UserList() {
+ const { data: users } = useSWR('/api/users', fetcher)
+}
+```
+
+**For immutable data:**
+
+```tsx
+import { useImmutableSWR } from '@/lib/swr'
+
+function StaticContent() {
+ const { data } = useImmutableSWR('/api/config', fetcher)
+}
+```
+
+**For mutations:**
+
+```tsx
+import { useSWRMutation } from 'swr/mutation'
+
+function UpdateButton() {
+ const { trigger } = useSWRMutation('/api/user', updateUser)
+ return trigger()}>Update
+}
+```
+
+Reference: [https://swr.vercel.app](https://swr.vercel.app)
+
+### 4.4 Version and Minimize localStorage Data
+
+**Impact: MEDIUM (prevents schema conflicts, reduces storage size)**
+
+Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data.
+
+**Incorrect:**
+
+```typescript
+// No version, stores everything, no error handling
+localStorage.setItem('userConfig', JSON.stringify(fullUserObject))
+const data = localStorage.getItem('userConfig')
+```
+
+**Correct:**
+
+```typescript
+const VERSION = 'v2'
+
+function saveConfig(config: { theme: string; language: string }) {
+ try {
+ localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
+ } catch {
+ // Throws in incognito/private browsing, quota exceeded, or disabled
+ }
+}
+
+function loadConfig() {
+ try {
+ const data = localStorage.getItem(`userConfig:${VERSION}`)
+ return data ? JSON.parse(data) : null
+ } catch {
+ return null
+ }
+}
+
+// Migration from v1 to v2
+function migrate() {
+ try {
+ const v1 = localStorage.getItem('userConfig:v1')
+ if (v1) {
+ const old = JSON.parse(v1)
+ saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })
+ localStorage.removeItem('userConfig:v1')
+ }
+ } catch {}
+}
+```
+
+**Store minimal fields from server responses:**
+
+```typescript
+// User object has 20+ fields, only store what UI needs
+function cachePrefs(user: FullUser) {
+ try {
+ localStorage.setItem('prefs:v1', JSON.stringify({
+ theme: user.preferences.theme,
+ notifications: user.preferences.notifications
+ }))
+ } catch {}
+}
+```
+
+**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled.
+
+**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.
+
+---
+
+## 5. Re-render Optimization
+
+**Impact: MEDIUM**
+
+Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
+
+### 5.1 Calculate Derived State During Rendering
+
+**Impact: MEDIUM (avoids redundant renders and state drift)**
+
+If a value can be computed from current props/state, do not store it in state or update it in an effect. Derive it during render to avoid extra renders and state drift. Do not set state in effects solely in response to prop changes; prefer derived values or keyed resets instead.
+
+**Incorrect: redundant state and effect**
+
+```tsx
+function Form() {
+ const [firstName, setFirstName] = useState('First')
+ const [lastName, setLastName] = useState('Last')
+ const [fullName, setFullName] = useState('')
+
+ useEffect(() => {
+ setFullName(firstName + ' ' + lastName)
+ }, [firstName, lastName])
+
+ return {fullName}
+}
+```
+
+**Correct: derive during render**
+
+```tsx
+function Form() {
+ const [firstName, setFirstName] = useState('First')
+ const [lastName, setLastName] = useState('Last')
+ const fullName = firstName + ' ' + lastName
+
+ return {fullName}
+}
+```
+
+Reference: [https://react.dev/learn/you-might-not-need-an-effect](https://react.dev/learn/you-might-not-need-an-effect)
+
+### 5.2 Defer State Reads to Usage Point
+
+**Impact: MEDIUM (avoids unnecessary subscriptions)**
+
+Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
+
+**Incorrect: subscribes to all searchParams changes**
+
+```tsx
+function ShareButton({ chatId }: { chatId: string }) {
+ const searchParams = useSearchParams()
+
+ const handleShare = () => {
+ const ref = searchParams.get('ref')
+ shareChat(chatId, { ref })
+ }
+
+ return Share
+}
+```
+
+**Correct: reads on demand, no subscription**
+
+```tsx
+function ShareButton({ chatId }: { chatId: string }) {
+ const handleShare = () => {
+ const params = new URLSearchParams(window.location.search)
+ const ref = params.get('ref')
+ shareChat(chatId, { ref })
+ }
+
+ return Share
+}
+```
+
+### 5.3 Do not wrap a simple expression with a primitive result type in useMemo
+
+**Impact: LOW-MEDIUM (wasted computation on every render)**
+
+When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.
+
+Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.
+
+**Incorrect:**
+
+```tsx
+function Header({ user, notifications }: Props) {
+ const isLoading = useMemo(() => {
+ return user.isLoading || notifications.isLoading
+ }, [user.isLoading, notifications.isLoading])
+
+ if (isLoading) return
+ // return some markup
+}
+```
+
+**Correct:**
+
+```tsx
+function Header({ user, notifications }: Props) {
+ const isLoading = user.isLoading || notifications.isLoading
+
+ if (isLoading) return
+ // return some markup
+}
+```
+
+### 5.4 Don't Define Components Inside Components
+
+**Impact: HIGH (prevents remount on every render)**
+
+Defining a component inside another component creates a new component type on every render. React sees a different component each time and fully remounts it, destroying all state and DOM.
+
+A common reason developers do this is to access parent variables without passing props. Always pass props instead.
+
+**Incorrect: remounts on every render**
+
+```tsx
+function UserProfile({ user, theme }) {
+ // Defined inside to access `theme` - BAD
+ const Avatar = () => (
+
+ )
+
+ // Defined inside to access `user` - BAD
+ const Stats = () => (
+
+ {user.followers} followers
+ {user.posts} posts
+
+ )
+
+ return (
+
+ )
+}
+```
+
+Every time `UserProfile` renders, `Avatar` and `Stats` are new component types. React unmounts the old instances and mounts new ones, losing any internal state, running effects again, and recreating DOM nodes.
+
+**Correct: pass props instead**
+
+```tsx
+function Avatar({ src, theme }: { src: string; theme: string }) {
+ return (
+
+ )
+}
+
+function Stats({ followers, posts }: { followers: number; posts: number }) {
+ return (
+
+ {followers} followers
+ {posts} posts
+
+ )
+}
+
+function UserProfile({ user, theme }) {
+ return (
+
+ )
+}
+```
+
+**Symptoms of this bug:**
+
+- Input fields lose focus on every keystroke
+
+- Animations restart unexpectedly
+
+- `useEffect` cleanup/setup runs on every parent render
+
+- Scroll position resets inside the component
+
+### 5.5 Extract Default Non-primitive Parameter Value from Memoized Component to Constant
+
+**Impact: MEDIUM (restores memoization by using a constant for default value)**
+
+When memoized component has a default value for some non-primitive optional parameter, such as an array, function, or object, calling the component without that parameter results in broken memoization. This is because new value instances are created on every rerender, and they do not pass strict equality comparison in `memo()`.
+
+To address this issue, extract the default value into a constant.
+
+**Incorrect: `onClick` has different values on every rerender**
+
+```tsx
+const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
+ // ...
+})
+
+// Used without optional onClick
+
+```
+
+**Correct: stable default value**
+
+```tsx
+const NOOP = () => {};
+
+const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
+ // ...
+})
+
+// Used without optional onClick
+
+```
+
+### 5.6 Extract to Memoized Components
+
+**Impact: MEDIUM (enables early returns)**
+
+Extract expensive work into memoized components to enable early returns before computation.
+
+**Incorrect: computes avatar even when loading**
+
+```tsx
+function Profile({ user, loading }: Props) {
+ const avatar = useMemo(() => {
+ const id = computeAvatarId(user)
+ return
+ }, [user])
+
+ if (loading) return
+ return {avatar}
+}
+```
+
+**Correct: skips computation when loading**
+
+```tsx
+const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
+ const id = useMemo(() => computeAvatarId(user), [user])
+ return
+})
+
+function Profile({ user, loading }: Props) {
+ if (loading) return
+ return (
+
+
+
+ )
+}
+```
+
+**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.
+
+### 5.7 Narrow Effect Dependencies
+
+**Impact: LOW (minimizes effect re-runs)**
+
+Specify primitive dependencies instead of objects to minimize effect re-runs.
+
+**Incorrect: re-runs on any user field change**
+
+```tsx
+useEffect(() => {
+ console.log(user.id)
+}, [user])
+```
+
+**Correct: re-runs only when id changes**
+
+```tsx
+useEffect(() => {
+ console.log(user.id)
+}, [user.id])
+```
+
+**For derived state, compute outside effect:**
+
+```tsx
+// Incorrect: runs on width=767, 766, 765...
+useEffect(() => {
+ if (width < 768) {
+ enableMobileMode()
+ }
+}, [width])
+
+// Correct: runs only on boolean transition
+const isMobile = width < 768
+useEffect(() => {
+ if (isMobile) {
+ enableMobileMode()
+ }
+}, [isMobile])
+```
+
+### 5.8 Put Interaction Logic in Event Handlers
+
+**Impact: MEDIUM (avoids effect re-runs and duplicate side effects)**
+
+If a side effect is triggered by a specific user action (submit, click, drag), run it in that event handler. Do not model the action as state + effect; it makes effects re-run on unrelated changes and can duplicate the action.
+
+**Incorrect: event modeled as state + effect**
+
+```tsx
+function Form() {
+ const [submitted, setSubmitted] = useState(false)
+ const theme = useContext(ThemeContext)
+
+ useEffect(() => {
+ if (submitted) {
+ post('/api/register')
+ showToast('Registered', theme)
+ }
+ }, [submitted, theme])
+
+ return setSubmitted(true)}>Submit
+}
+```
+
+**Correct: do it in the handler**
+
+```tsx
+function Form() {
+ const theme = useContext(ThemeContext)
+
+ function handleSubmit() {
+ post('/api/register')
+ showToast('Registered', theme)
+ }
+
+ return Submit
+}
+```
+
+Reference: [https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)
+
+### 5.9 Subscribe to Derived State
+
+**Impact: MEDIUM (reduces re-render frequency)**
+
+Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
+
+**Incorrect: re-renders on every pixel change**
+
+```tsx
+function Sidebar() {
+ const width = useWindowWidth() // updates continuously
+ const isMobile = width < 768
+ return
+}
+```
+
+**Correct: re-renders only when boolean changes**
+
+```tsx
+function Sidebar() {
+ const isMobile = useMediaQuery('(max-width: 767px)')
+ return
+}
+```
+
+### 5.10 Use Functional setState Updates
+
+**Impact: MEDIUM (prevents stale closures and unnecessary callback recreations)**
+
+When updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.
+
+**Incorrect: requires state as dependency**
+
+```tsx
+function TodoList() {
+ const [items, setItems] = useState(initialItems)
+
+ // Callback must depend on items, recreated on every items change
+ const addItems = useCallback((newItems: Item[]) => {
+ setItems([...items, ...newItems])
+ }, [items]) // ❌ items dependency causes recreations
+
+ // Risk of stale closure if dependency is forgotten
+ const removeItem = useCallback((id: string) => {
+ setItems(items.filter(item => item.id !== id))
+ }, []) // ❌ Missing items dependency - will use stale items!
+
+ return
+}
+```
+
+The first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.
+
+**Correct: stable callbacks, no stale closures**
+
+```tsx
+function TodoList() {
+ const [items, setItems] = useState(initialItems)
+
+ // Stable callback, never recreated
+ const addItems = useCallback((newItems: Item[]) => {
+ setItems(curr => [...curr, ...newItems])
+ }, []) // ✅ No dependencies needed
+
+ // Always uses latest state, no stale closure risk
+ const removeItem = useCallback((id: string) => {
+ setItems(curr => curr.filter(item => item.id !== id))
+ }, []) // ✅ Safe and stable
+
+ return
+}
+```
+
+**Benefits:**
+
+1. **Stable callback references** - Callbacks don't need to be recreated when state changes
+
+2. **No stale closures** - Always operates on the latest state value
+
+3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
+
+4. **Prevents bugs** - Eliminates the most common source of React closure bugs
+
+**When to use functional updates:**
+
+- Any setState that depends on the current state value
+
+- Inside useCallback/useMemo when state is needed
+
+- Event handlers that reference state
+
+- Async operations that update state
+
+**When direct updates are fine:**
+
+- Setting state to a static value: `setCount(0)`
+
+- Setting state from props/arguments only: `setName(newName)`
+
+- State doesn't depend on previous value
+
+**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.
+
+### 5.11 Use Lazy State Initialization
+
+**Impact: MEDIUM (wasted computation on every render)**
+
+Pass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.
+
+**Incorrect: runs on every render**
+
+```tsx
+function FilteredList({ items }: { items: Item[] }) {
+ // buildSearchIndex() runs on EVERY render, even after initialization
+ const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
+ const [query, setQuery] = useState('')
+
+ // When query changes, buildSearchIndex runs again unnecessarily
+ return
+}
+
+function UserProfile() {
+ // JSON.parse runs on every render
+ const [settings, setSettings] = useState(
+ JSON.parse(localStorage.getItem('settings') || '{}')
+ )
+
+ return
+}
+```
+
+**Correct: runs only once**
+
+```tsx
+function FilteredList({ items }: { items: Item[] }) {
+ // buildSearchIndex() runs ONLY on initial render
+ const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
+ const [query, setQuery] = useState('')
+
+ return
+}
+
+function UserProfile() {
+ // JSON.parse runs only on initial render
+ const [settings, setSettings] = useState(() => {
+ const stored = localStorage.getItem('settings')
+ return stored ? JSON.parse(stored) : {}
+ })
+
+ return
+}
+```
+
+Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
+
+For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
+
+### 5.12 Use Transitions for Non-Urgent Updates
+
+**Impact: MEDIUM (maintains UI responsiveness)**
+
+Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
+
+**Incorrect: blocks UI on every scroll**
+
+```tsx
+function ScrollTracker() {
+ const [scrollY, setScrollY] = useState(0)
+ useEffect(() => {
+ const handler = () => setScrollY(window.scrollY)
+ window.addEventListener('scroll', handler, { passive: true })
+ return () => window.removeEventListener('scroll', handler)
+ }, [])
+}
+```
+
+**Correct: non-blocking updates**
+
+```tsx
+import { startTransition } from 'react'
+
+function ScrollTracker() {
+ const [scrollY, setScrollY] = useState(0)
+ useEffect(() => {
+ const handler = () => {
+ startTransition(() => setScrollY(window.scrollY))
+ }
+ window.addEventListener('scroll', handler, { passive: true })
+ return () => window.removeEventListener('scroll', handler)
+ }, [])
+}
+```
+
+### 5.13 Use useRef for Transient Values
+
+**Impact: MEDIUM (avoids unnecessary re-renders on frequent updates)**
+
+When a value changes frequently and you don't want a re-render on every update (e.g., mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`. Keep component state for UI; use refs for temporary DOM-adjacent values. Updating a ref does not trigger a re-render.
+
+**Incorrect: renders every update**
+
+```tsx
+function Tracker() {
+ const [lastX, setLastX] = useState(0)
+
+ useEffect(() => {
+ const onMove = (e: MouseEvent) => setLastX(e.clientX)
+ window.addEventListener('mousemove', onMove)
+ return () => window.removeEventListener('mousemove', onMove)
+ }, [])
+
+ return (
+
+ )
+}
+```
+
+**Correct: no re-render for tracking**
+
+```tsx
+function Tracker() {
+ const lastXRef = useRef(0)
+ const dotRef = useRef(null)
+
+ useEffect(() => {
+ const onMove = (e: MouseEvent) => {
+ lastXRef.current = e.clientX
+ const node = dotRef.current
+ if (node) {
+ node.style.transform = `translateX(${e.clientX}px)`
+ }
+ }
+ window.addEventListener('mousemove', onMove)
+ return () => window.removeEventListener('mousemove', onMove)
+ }, [])
+
+ return (
+
+ )
+}
+```
+
+---
+
+## 6. Rendering Performance
+
+**Impact: MEDIUM**
+
+Optimizing the rendering process reduces the work the browser needs to do.
+
+### 6.1 Animate SVG Wrapper Instead of SVG Element
+
+**Impact: LOW (enables hardware acceleration)**
+
+Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `` and animate the wrapper instead.
+
+**Incorrect: animating SVG directly - no hardware acceleration**
+
+```tsx
+function LoadingSpinner() {
+ return (
+
+
+
+ )
+}
+```
+
+**Correct: animating wrapper div - hardware accelerated**
+
+```tsx
+function LoadingSpinner() {
+ return (
+
+
+
+
+
+ )
+}
+```
+
+This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.
+
+### 6.2 CSS content-visibility for Long Lists
+
+**Impact: HIGH (faster initial render)**
+
+Apply `content-visibility: auto` to defer off-screen rendering.
+
+**CSS:**
+
+```css
+.message-item {
+ content-visibility: auto;
+ contain-intrinsic-size: 0 80px;
+}
+```
+
+**Example:**
+
+```tsx
+function MessageList({ messages }: { messages: Message[] }) {
+ return (
+
+ {messages.map(msg => (
+
+ ))}
+
+ )
+}
+```
+
+For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).
+
+### 6.3 Hoist Static JSX Elements
+
+**Impact: LOW (avoids re-creation)**
+
+Extract static JSX outside components to avoid re-creation.
+
+**Incorrect: recreates element every render**
+
+```tsx
+function LoadingSkeleton() {
+ return
+}
+
+function Container() {
+ return (
+
+ {loading && }
+
+ )
+}
+```
+
+**Correct: reuses same element**
+
+```tsx
+const loadingSkeleton = (
+
+)
+
+function Container() {
+ return (
+
+ {loading && loadingSkeleton}
+
+ )
+}
+```
+
+This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
+
+**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.
+
+### 6.4 Optimize SVG Precision
+
+**Impact: LOW (reduces file size)**
+
+Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
+
+**Incorrect: excessive precision**
+
+```svg
+
+```
+
+**Correct: 1 decimal place**
+
+```svg
+
+```
+
+**Automate with SVGO:**
+
+```bash
+npx svgo --precision=1 --multipass icon.svg
+```
+
+### 6.5 Prevent Hydration Mismatch Without Flickering
+
+**Impact: MEDIUM (avoids visual flicker and hydration errors)**
+
+When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.
+
+**Incorrect: breaks SSR**
+
+```tsx
+function ThemeWrapper({ children }: { children: ReactNode }) {
+ // localStorage is not available on server - throws error
+ const theme = localStorage.getItem('theme') || 'light'
+
+ return (
+
+ {children}
+
+ )
+}
+```
+
+Server-side rendering will fail because `localStorage` is undefined.
+
+**Incorrect: visual flickering**
+
+```tsx
+function ThemeWrapper({ children }: { children: ReactNode }) {
+ const [theme, setTheme] = useState('light')
+
+ useEffect(() => {
+ // Runs after hydration - causes visible flash
+ const stored = localStorage.getItem('theme')
+ if (stored) {
+ setTheme(stored)
+ }
+ }, [])
+
+ return (
+
+ {children}
+
+ )
+}
+```
+
+Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
+
+**Correct: no flicker, no hydration mismatch**
+
+```tsx
+function ThemeWrapper({ children }: { children: ReactNode }) {
+ return (
+ <>
+
+ {children}
+
+
+ >
+ )
+}
+```
+
+The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
+
+This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
+
+### 6.6 Suppress Expected Hydration Mismatches
+
+**Impact: LOW-MEDIUM (avoids noisy hydration warnings for known differences)**
+
+In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these *expected* mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Don’t overuse it.
+
+**Incorrect: known mismatch warnings**
+
+```tsx
+function Timestamp() {
+ return
{new Date().toLocaleString()}
+}
+```
+
+**Correct: suppress expected mismatch only**
+
+```tsx
+function Timestamp() {
+ return (
+
+ {new Date().toLocaleString()}
+
+ )
+}
+```
+
+### 6.7 Use Activity Component for Show/Hide
+
+**Impact: MEDIUM (preserves state/DOM)**
+
+Use React's `
` to preserve state/DOM for expensive components that frequently toggle visibility.
+
+**Usage:**
+
+```tsx
+import { Activity } from 'react'
+
+function Dropdown({ isOpen }: Props) {
+ return (
+
+
+
+ )
+}
+```
+
+Avoids expensive re-renders and state loss.
+
+### 6.8 Use defer or async on Script Tags
+
+**Impact: HIGH (eliminates render-blocking)**
+
+Script tags without `defer` or `async` block HTML parsing while the script downloads and executes. This delays First Contentful Paint and Time to Interactive.
+
+- **`defer`**: Downloads in parallel, executes after HTML parsing completes, maintains execution order
+
+- **`async`**: Downloads in parallel, executes immediately when ready, no guaranteed order
+
+Use `defer` for scripts that depend on DOM or other scripts. Use `async` for independent scripts like analytics.
+
+**Incorrect: blocks rendering**
+
+```tsx
+export default function Document() {
+ return (
+
+
+
+
+
+ {/* content */}
+
+ )
+}
+```
+
+**Correct: non-blocking**
+
+```tsx
+import Script from 'next/script'
+
+export default function Page() {
+ return (
+ <>
+
+
+ >
+ )
+}
+```
+
+**Note:** In Next.js, prefer the `next/script` component with `strategy` prop instead of raw script tags:
+
+Reference: [https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer)
+
+### 6.9 Use Explicit Conditional Rendering
+
+**Impact: LOW (prevents rendering 0 or NaN)**
+
+Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
+
+**Incorrect: renders "0" when count is 0**
+
+```tsx
+function Badge({ count }: { count: number }) {
+ return (
+
+ {count && {count} }
+
+ )
+}
+
+// When count = 0, renders: 0
+// When count = 5, renders: 5
+```
+
+**Correct: renders nothing when count is 0**
+
+```tsx
+function Badge({ count }: { count: number }) {
+ return (
+
+ {count > 0 ? {count} : null}
+
+ )
+}
+
+// When count = 0, renders:
+// When count = 5, renders: 5
+```
+
+### 6.10 Use React DOM Resource Hints
+
+**Impact: HIGH (reduces load time for critical resources)**
+
+React DOM provides APIs to hint the browser about resources it will need. These are especially useful in server components to start loading resources before the client even receives the HTML.
+
+- **`prefetchDNS(href)`**: Resolve DNS for a domain you expect to connect to
+
+- **`preconnect(href)`**: Establish connection (DNS + TCP + TLS) to a server
+
+- **`preload(href, options)`**: Fetch a resource (stylesheet, font, script, image) you'll use soon
+
+- **`preloadModule(href)`**: Fetch an ES module you'll use soon
+
+- **`preinit(href, options)`**: Fetch and evaluate a stylesheet or script
+
+- **`preinitModule(href)`**: Fetch and evaluate an ES module
+
+**Example: preconnect to third-party APIs**
+
+```tsx
+import { preconnect, prefetchDNS } from 'react-dom'
+
+export default function App() {
+ prefetchDNS('https://analytics.example.com')
+ preconnect('https://api.example.com')
+
+ return {/* content */}
+}
+```
+
+**Example: preload critical fonts and styles**
+
+```tsx
+import { preload, preinit } from 'react-dom'
+
+export default function RootLayout({ children }) {
+ // Preload font file
+ preload('/fonts/inter.woff2', { as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' })
+
+ // Fetch and apply critical stylesheet immediately
+ preinit('/styles/critical.css', { as: 'style' })
+
+ return (
+
+ {children}
+
+ )
+}
+```
+
+**Example: preload modules for code-split routes**
+
+```tsx
+import { preloadModule, preinitModule } from 'react-dom'
+
+function Navigation() {
+ const preloadDashboard = () => {
+ preloadModule('/dashboard.js', { as: 'script' })
+ }
+
+ return (
+
+
+ Dashboard
+
+
+ )
+}
+```
+
+**When to use each:**
+
+| API | Use case |
+
+|-----|----------|
+
+| `prefetchDNS` | Third-party domains you'll connect to later |
+
+| `preconnect` | APIs or CDNs you'll fetch from immediately |
+
+| `preload` | Critical resources needed for current page |
+
+| `preloadModule` | JS modules for likely next navigation |
+
+| `preinit` | Stylesheets/scripts that must execute early |
+
+| `preinitModule` | ES modules that must execute early |
+
+Reference: [https://react.dev/reference/react-dom#resource-preloading-apis](https://react.dev/reference/react-dom#resource-preloading-apis)
+
+### 6.11 Use useTransition Over Manual Loading States
+
+**Impact: LOW (reduces re-renders and improves code clarity)**
+
+Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.
+
+**Incorrect: manual loading state**
+
+```tsx
+function SearchResults() {
+ const [query, setQuery] = useState('')
+ const [results, setResults] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+
+ const handleSearch = async (value: string) => {
+ setIsLoading(true)
+ setQuery(value)
+ const data = await fetchResults(value)
+ setResults(data)
+ setIsLoading(false)
+ }
+
+ return (
+ <>
+ handleSearch(e.target.value)} />
+ {isLoading && }
+
+ >
+ )
+}
+```
+
+**Correct: useTransition with built-in pending state**
+
+```tsx
+import { useTransition, useState } from 'react'
+
+function SearchResults() {
+ const [query, setQuery] = useState('')
+ const [results, setResults] = useState([])
+ const [isPending, startTransition] = useTransition()
+
+ const handleSearch = (value: string) => {
+ setQuery(value) // Update input immediately
+
+ startTransition(async () => {
+ // Fetch and update results
+ const data = await fetchResults(value)
+ setResults(data)
+ })
+ }
+
+ return (
+ <>
+ handleSearch(e.target.value)} />
+ {isPending && }
+
+ >
+ )
+}
+```
+
+**Benefits:**
+
+- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`
+
+- **Error resilience**: Pending state correctly resets even if the transition throws
+
+- **Better responsiveness**: Keeps the UI responsive during updates
+
+- **Interrupt handling**: New transitions automatically cancel pending ones
+
+Reference: [https://react.dev/reference/react/useTransition](https://react.dev/reference/react/useTransition)
+
+---
+
+## 7. JavaScript Performance
+
+**Impact: LOW-MEDIUM**
+
+Micro-optimizations for hot paths can add up to meaningful improvements.
+
+### 7.1 Avoid Layout Thrashing
+
+**Impact: MEDIUM (prevents forced synchronous layouts and reduces performance bottlenecks)**
+
+Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.
+
+**This is OK: browser batches style changes**
+
+```typescript
+function updateElementStyles(element: HTMLElement) {
+ // Each line invalidates style, but browser batches the recalculation
+ element.style.width = '100px'
+ element.style.height = '200px'
+ element.style.backgroundColor = 'blue'
+ element.style.border = '1px solid black'
+}
+```
+
+**Incorrect: interleaved reads and writes force reflows**
+
+```typescript
+function layoutThrashing(element: HTMLElement) {
+ element.style.width = '100px'
+ const width = element.offsetWidth // Forces reflow
+ element.style.height = '200px'
+ const height = element.offsetHeight // Forces another reflow
+}
+```
+
+**Correct: batch writes, then read once**
+
+```typescript
+function updateElementStyles(element: HTMLElement) {
+ // Batch all writes together
+ element.style.width = '100px'
+ element.style.height = '200px'
+ element.style.backgroundColor = 'blue'
+ element.style.border = '1px solid black'
+
+ // Read after all writes are done (single reflow)
+ const { width, height } = element.getBoundingClientRect()
+}
+```
+
+**Correct: batch reads, then writes**
+
+```typescript
+function updateElementStyles(element: HTMLElement) {
+ element.classList.add('highlighted-box')
+
+ const { width, height } = element.getBoundingClientRect()
+}
+```
+
+**Better: use CSS classes**
+
+**React example:**
+
+```tsx
+// Incorrect: interleaving style changes with layout queries
+function Box({ isHighlighted }: { isHighlighted: boolean }) {
+ const ref = useRef(null)
+
+ useEffect(() => {
+ if (ref.current && isHighlighted) {
+ ref.current.style.width = '100px'
+ const width = ref.current.offsetWidth // Forces layout
+ ref.current.style.height = '200px'
+ }
+ }, [isHighlighted])
+
+ return Content
+}
+
+// Correct: toggle class
+function Box({ isHighlighted }: { isHighlighted: boolean }) {
+ return (
+
+ Content
+
+ )
+}
+```
+
+Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.
+
+See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.
+
+### 7.2 Build Index Maps for Repeated Lookups
+
+**Impact: LOW-MEDIUM (1M ops to 2K ops)**
+
+Multiple `.find()` calls by the same key should use a Map.
+
+**Incorrect (O(n) per lookup):**
+
+```typescript
+function processOrders(orders: Order[], users: User[]) {
+ return orders.map(order => ({
+ ...order,
+ user: users.find(u => u.id === order.userId)
+ }))
+}
+```
+
+**Correct (O(1) per lookup):**
+
+```typescript
+function processOrders(orders: Order[], users: User[]) {
+ const userById = new Map(users.map(u => [u.id, u]))
+
+ return orders.map(order => ({
+ ...order,
+ user: userById.get(order.userId)
+ }))
+}
+```
+
+Build map once (O(n)), then all lookups are O(1).
+
+For 1000 orders × 1000 users: 1M ops → 2K ops.
+
+### 7.3 Cache Property Access in Loops
+
+**Impact: LOW-MEDIUM (reduces lookups)**
+
+Cache object property lookups in hot paths.
+
+**Incorrect: 3 lookups × N iterations**
+
+```typescript
+for (let i = 0; i < arr.length; i++) {
+ process(obj.config.settings.value)
+}
+```
+
+**Correct: 1 lookup total**
+
+```typescript
+const value = obj.config.settings.value
+const len = arr.length
+for (let i = 0; i < len; i++) {
+ process(value)
+}
+```
+
+### 7.4 Cache Repeated Function Calls
+
+**Impact: MEDIUM (avoid redundant computation)**
+
+Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.
+
+**Incorrect: redundant computation**
+
+```typescript
+function ProjectList({ projects }: { projects: Project[] }) {
+ return (
+
+ {projects.map(project => {
+ // slugify() called 100+ times for same project names
+ const slug = slugify(project.name)
+
+ return
+ })}
+
+ )
+}
+```
+
+**Correct: cached results**
+
+```typescript
+// Module-level cache
+const slugifyCache = new Map()
+
+function cachedSlugify(text: string): string {
+ if (slugifyCache.has(text)) {
+ return slugifyCache.get(text)!
+ }
+ const result = slugify(text)
+ slugifyCache.set(text, result)
+ return result
+}
+
+function ProjectList({ projects }: { projects: Project[] }) {
+ return (
+
+ {projects.map(project => {
+ // Computed only once per unique project name
+ const slug = cachedSlugify(project.name)
+
+ return
+ })}
+
+ )
+}
+```
+
+**Simpler pattern for single-value functions:**
+
+```typescript
+let isLoggedInCache: boolean | null = null
+
+function isLoggedIn(): boolean {
+ if (isLoggedInCache !== null) {
+ return isLoggedInCache
+ }
+
+ isLoggedInCache = document.cookie.includes('auth=')
+ return isLoggedInCache
+}
+
+// Clear cache when auth changes
+function onAuthChange() {
+ isLoggedInCache = null
+}
+```
+
+Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
+
+Reference: [https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
+
+### 7.5 Cache Storage API Calls
+
+**Impact: LOW-MEDIUM (reduces expensive I/O)**
+
+`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.
+
+**Incorrect: reads storage on every call**
+
+```typescript
+function getTheme() {
+ return localStorage.getItem('theme') ?? 'light'
+}
+// Called 10 times = 10 storage reads
+```
+
+**Correct: Map cache**
+
+```typescript
+const storageCache = new Map()
+
+function getLocalStorage(key: string) {
+ if (!storageCache.has(key)) {
+ storageCache.set(key, localStorage.getItem(key))
+ }
+ return storageCache.get(key)
+}
+
+function setLocalStorage(key: string, value: string) {
+ localStorage.setItem(key, value)
+ storageCache.set(key, value) // keep cache in sync
+}
+```
+
+Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
+
+**Cookie caching:**
+
+```typescript
+let cookieCache: Record | null = null
+
+function getCookie(name: string) {
+ if (!cookieCache) {
+ cookieCache = Object.fromEntries(
+ document.cookie.split('; ').map(c => c.split('='))
+ )
+ }
+ return cookieCache[name]
+}
+```
+
+**Important: invalidate on external changes**
+
+```typescript
+window.addEventListener('storage', (e) => {
+ if (e.key) storageCache.delete(e.key)
+})
+
+document.addEventListener('visibilitychange', () => {
+ if (document.visibilityState === 'visible') {
+ storageCache.clear()
+ }
+})
+```
+
+If storage can change externally (another tab, server-set cookies), invalidate cache:
+
+### 7.6 Combine Multiple Array Iterations
+
+**Impact: LOW-MEDIUM (reduces iterations)**
+
+Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.
+
+**Incorrect: 3 iterations**
+
+```typescript
+const admins = users.filter(u => u.isAdmin)
+const testers = users.filter(u => u.isTester)
+const inactive = users.filter(u => !u.isActive)
+```
+
+**Correct: 1 iteration**
+
+```typescript
+const admins: User[] = []
+const testers: User[] = []
+const inactive: User[] = []
+
+for (const user of users) {
+ if (user.isAdmin) admins.push(user)
+ if (user.isTester) testers.push(user)
+ if (!user.isActive) inactive.push(user)
+}
+```
+
+### 7.7 Early Length Check for Array Comparisons
+
+**Impact: MEDIUM-HIGH (avoids expensive operations when lengths differ)**
+
+When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.
+
+In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).
+
+**Incorrect: always runs expensive comparison**
+
+```typescript
+function hasChanges(current: string[], original: string[]) {
+ // Always sorts and joins, even when lengths differ
+ return current.sort().join() !== original.sort().join()
+}
+```
+
+Two O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings.
+
+**Correct (O(1) length check first):**
+
+```typescript
+function hasChanges(current: string[], original: string[]) {
+ // Early return if lengths differ
+ if (current.length !== original.length) {
+ return true
+ }
+ // Only sort when lengths match
+ const currentSorted = current.toSorted()
+ const originalSorted = original.toSorted()
+ for (let i = 0; i < currentSorted.length; i++) {
+ if (currentSorted[i] !== originalSorted[i]) {
+ return true
+ }
+ }
+ return false
+}
+```
+
+This new approach is more efficient because:
+
+- It avoids the overhead of sorting and joining the arrays when lengths differ
+
+- It avoids consuming memory for the joined strings (especially important for large arrays)
+
+- It avoids mutating the original arrays
+
+- It returns early when a difference is found
+
+### 7.8 Early Return from Functions
+
+**Impact: LOW-MEDIUM (avoids unnecessary computation)**
+
+Return early when result is determined to skip unnecessary processing.
+
+**Incorrect: processes all items even after finding answer**
+
+```typescript
+function validateUsers(users: User[]) {
+ let hasError = false
+ let errorMessage = ''
+
+ for (const user of users) {
+ if (!user.email) {
+ hasError = true
+ errorMessage = 'Email required'
+ }
+ if (!user.name) {
+ hasError = true
+ errorMessage = 'Name required'
+ }
+ // Continues checking all users even after error found
+ }
+
+ return hasError ? { valid: false, error: errorMessage } : { valid: true }
+}
+```
+
+**Correct: returns immediately on first error**
+
+```typescript
+function validateUsers(users: User[]) {
+ for (const user of users) {
+ if (!user.email) {
+ return { valid: false, error: 'Email required' }
+ }
+ if (!user.name) {
+ return { valid: false, error: 'Name required' }
+ }
+ }
+
+ return { valid: true }
+}
+```
+
+### 7.9 Hoist RegExp Creation
+
+**Impact: LOW-MEDIUM (avoids recreation)**
+
+Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.
+
+**Incorrect: new RegExp every render**
+
+```tsx
+function Highlighter({ text, query }: Props) {
+ const regex = new RegExp(`(${query})`, 'gi')
+ const parts = text.split(regex)
+ return <>{parts.map((part, i) => ...)}>
+}
+```
+
+**Correct: memoize or hoist**
+
+```tsx
+const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+
+function Highlighter({ text, query }: Props) {
+ const regex = useMemo(
+ () => new RegExp(`(${escapeRegex(query)})`, 'gi'),
+ [query]
+ )
+ const parts = text.split(regex)
+ return <>{parts.map((part, i) => ...)}>
+}
+```
+
+**Warning: global regex has mutable state**
+
+```typescript
+const regex = /foo/g
+regex.test('foo') // true, lastIndex = 3
+regex.test('foo') // false, lastIndex = 0
+```
+
+Global regex (`/g`) has mutable `lastIndex` state:
+
+### 7.10 Use flatMap to Map and Filter in One Pass
+
+**Impact: LOW-MEDIUM (eliminates intermediate array)**
+
+Chaining `.map().filter(Boolean)` creates an intermediate array and iterates twice. Use `.flatMap()` to transform and filter in a single pass.
+
+**Incorrect: 2 iterations, intermediate array**
+
+```typescript
+const userNames = users
+ .map(user => user.isActive ? user.name : null)
+ .filter(Boolean)
+```
+
+**Correct: 1 iteration, no intermediate array**
+
+```typescript
+const userNames = users.flatMap(user =>
+ user.isActive ? [user.name] : []
+)
+```
+
+**More examples:**
+
+```typescript
+// Extract valid emails from responses
+// Before
+const emails = responses
+ .map(r => r.success ? r.data.email : null)
+ .filter(Boolean)
+
+// After
+const emails = responses.flatMap(r =>
+ r.success ? [r.data.email] : []
+)
+
+// Parse and filter valid numbers
+// Before
+const numbers = strings
+ .map(s => parseInt(s, 10))
+ .filter(n => !isNaN(n))
+
+// After
+const numbers = strings.flatMap(s => {
+ const n = parseInt(s, 10)
+ return isNaN(n) ? [] : [n]
+})
+```
+
+**When to use:**
+
+- Transforming items while filtering some out
+
+- Conditional mapping where some inputs produce no output
+
+- Parsing/validating where invalid inputs should be skipped
+
+### 7.11 Use Loop for Min/Max Instead of Sort
+
+**Impact: LOW (O(n) instead of O(n log n))**
+
+Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.
+
+**Incorrect (O(n log n) - sort to find latest):**
+
+```typescript
+interface Project {
+ id: string
+ name: string
+ updatedAt: number
+}
+
+function getLatestProject(projects: Project[]) {
+ const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
+ return sorted[0]
+}
+```
+
+Sorts the entire array just to find the maximum value.
+
+**Incorrect (O(n log n) - sort for oldest and newest):**
+
+```typescript
+function getOldestAndNewest(projects: Project[]) {
+ const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
+ return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
+}
+```
+
+Still sorts unnecessarily when only min/max are needed.
+
+**Correct (O(n) - single loop):**
+
+```typescript
+function getLatestProject(projects: Project[]) {
+ if (projects.length === 0) return null
+
+ let latest = projects[0]
+
+ for (let i = 1; i < projects.length; i++) {
+ if (projects[i].updatedAt > latest.updatedAt) {
+ latest = projects[i]
+ }
+ }
+
+ return latest
+}
+
+function getOldestAndNewest(projects: Project[]) {
+ if (projects.length === 0) return { oldest: null, newest: null }
+
+ let oldest = projects[0]
+ let newest = projects[0]
+
+ for (let i = 1; i < projects.length; i++) {
+ if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
+ if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]
+ }
+
+ return { oldest, newest }
+}
+```
+
+Single pass through the array, no copying, no sorting.
+
+**Alternative: Math.min/Math.max for small arrays**
+
+```typescript
+const numbers = [5, 2, 8, 1, 9]
+const min = Math.min(...numbers)
+const max = Math.max(...numbers)
+```
+
+This works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability.
+
+### 7.12 Use Set/Map for O(1) Lookups
+
+**Impact: LOW-MEDIUM (O(n) to O(1))**
+
+Convert arrays to Set/Map for repeated membership checks.
+
+**Incorrect (O(n) per check):**
+
+```typescript
+const allowedIds = ['a', 'b', 'c', ...]
+items.filter(item => allowedIds.includes(item.id))
+```
+
+**Correct (O(1) per check):**
+
+```typescript
+const allowedIds = new Set(['a', 'b', 'c', ...])
+items.filter(item => allowedIds.has(item.id))
+```
+
+### 7.13 Use toSorted() Instead of sort() for Immutability
+
+**Impact: MEDIUM-HIGH (prevents mutation bugs in React state)**
+
+`.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation.
+
+**Incorrect: mutates original array**
+
+```typescript
+function UserList({ users }: { users: User[] }) {
+ // Mutates the users prop array!
+ const sorted = useMemo(
+ () => users.sort((a, b) => a.name.localeCompare(b.name)),
+ [users]
+ )
+ return {sorted.map(renderUser)}
+}
+```
+
+**Correct: creates new array**
+
+```typescript
+function UserList({ users }: { users: User[] }) {
+ // Creates new sorted array, original unchanged
+ const sorted = useMemo(
+ () => users.toSorted((a, b) => a.name.localeCompare(b.name)),
+ [users]
+ )
+ return {sorted.map(renderUser)}
+}
+```
+
+**Why this matters in React:**
+
+1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only
+
+2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior
+
+**Browser support: fallback for older browsers**
+
+```typescript
+// Fallback for older browsers
+const sorted = [...items].sort((a, b) => a.value - b.value)
+```
+
+`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:
+
+**Other immutable array methods:**
+
+- `.toSorted()` - immutable sort
+
+- `.toReversed()` - immutable reverse
+
+- `.toSpliced()` - immutable splice
+
+- `.with()` - immutable element replacement
+
+---
+
+## 8. Advanced Patterns
+
+**Impact: LOW**
+
+Advanced patterns for specific cases that require careful implementation.
+
+### 8.1 Initialize App Once, Not Per Mount
+
+**Impact: LOW-MEDIUM (avoids duplicate init in development)**
+
+Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead.
+
+**Incorrect: runs twice in dev, re-runs on remount**
+
+```tsx
+function Comp() {
+ useEffect(() => {
+ loadFromStorage()
+ checkAuthToken()
+ }, [])
+
+ // ...
+}
+```
+
+**Correct: once per app load**
+
+```tsx
+let didInit = false
+
+function Comp() {
+ useEffect(() => {
+ if (didInit) return
+ didInit = true
+ loadFromStorage()
+ checkAuthToken()
+ }, [])
+
+ // ...
+}
+```
+
+Reference: [https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)
+
+### 8.2 Store Event Handlers in Refs
+
+**Impact: LOW (stable subscriptions)**
+
+Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
+
+**Incorrect: re-subscribes on every render**
+
+```tsx
+function useWindowEvent(event: string, handler: (e) => void) {
+ useEffect(() => {
+ window.addEventListener(event, handler)
+ return () => window.removeEventListener(event, handler)
+ }, [event, handler])
+}
+```
+
+**Correct: stable subscription**
+
+```tsx
+import { useEffectEvent } from 'react'
+
+function useWindowEvent(event: string, handler: (e) => void) {
+ const onEvent = useEffectEvent(handler)
+
+ useEffect(() => {
+ window.addEventListener(event, onEvent)
+ return () => window.removeEventListener(event, onEvent)
+ }, [event])
+}
+```
+
+**Alternative: use `useEffectEvent` if you're on latest React:**
+
+`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
+
+### 8.3 useEffectEvent for Stable Callback Refs
+
+**Impact: LOW (prevents effect re-runs)**
+
+Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
+
+**Incorrect: effect re-runs on every callback change**
+
+```tsx
+function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
+ const [query, setQuery] = useState('')
+
+ useEffect(() => {
+ const timeout = setTimeout(() => onSearch(query), 300)
+ return () => clearTimeout(timeout)
+ }, [query, onSearch])
+}
+```
+
+**Correct: using React's useEffectEvent**
+
+```tsx
+import { useEffectEvent } from 'react';
+
+function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
+ const [query, setQuery] = useState('')
+ const onSearchEvent = useEffectEvent(onSearch)
+
+ useEffect(() => {
+ const timeout = setTimeout(() => onSearchEvent(query), 300)
+ return () => clearTimeout(timeout)
+ }, [query])
+}
+```
+
+---
+
+## References
+
+1. [https://react.dev](https://react.dev)
+2. [https://nextjs.org](https://nextjs.org)
+3. [https://swr.vercel.app](https://swr.vercel.app)
+4. [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
+5. [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
+6. [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
+7. [https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
diff --git a/.agents/skills/vercel-react-best-practices/README.md b/.agents/skills/vercel-react-best-practices/README.md
new file mode 100644
index 0000000..f283e1c
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/README.md
@@ -0,0 +1,123 @@
+# React Best Practices
+
+A structured repository for creating and maintaining React Best Practices optimized for agents and LLMs.
+
+## Structure
+
+- `rules/` - Individual rule files (one per rule)
+ - `_sections.md` - Section metadata (titles, impacts, descriptions)
+ - `_template.md` - Template for creating new rules
+ - `area-description.md` - Individual rule files
+- `src/` - Build scripts and utilities
+- `metadata.json` - Document metadata (version, organization, abstract)
+- __`AGENTS.md`__ - Compiled output (generated)
+- __`test-cases.json`__ - Test cases for LLM evaluation (generated)
+
+## Getting Started
+
+1. Install dependencies:
+ ```bash
+ pnpm install
+ ```
+
+2. Build AGENTS.md from rules:
+ ```bash
+ pnpm build
+ ```
+
+3. Validate rule files:
+ ```bash
+ pnpm validate
+ ```
+
+4. Extract test cases:
+ ```bash
+ pnpm extract-tests
+ ```
+
+## Creating a New Rule
+
+1. Copy `rules/_template.md` to `rules/area-description.md`
+2. Choose the appropriate area prefix:
+ - `async-` for Eliminating Waterfalls (Section 1)
+ - `bundle-` for Bundle Size Optimization (Section 2)
+ - `server-` for Server-Side Performance (Section 3)
+ - `client-` for Client-Side Data Fetching (Section 4)
+ - `rerender-` for Re-render Optimization (Section 5)
+ - `rendering-` for Rendering Performance (Section 6)
+ - `js-` for JavaScript Performance (Section 7)
+ - `advanced-` for Advanced Patterns (Section 8)
+3. Fill in the frontmatter and content
+4. Ensure you have clear examples with explanations
+5. Run `pnpm build` to regenerate AGENTS.md and test-cases.json
+
+## Rule File Structure
+
+Each rule file should follow this structure:
+
+```markdown
+---
+title: Rule Title Here
+impact: MEDIUM
+impactDescription: Optional description
+tags: tag1, tag2, tag3
+---
+
+## Rule Title Here
+
+Brief explanation of the rule and why it matters.
+
+**Incorrect (description of what's wrong):**
+
+```typescript
+// Bad code example
+```
+
+**Correct (description of what's right):**
+
+```typescript
+// Good code example
+```
+
+Optional explanatory text after examples.
+
+Reference: [Link](https://example.com)
+
+## File Naming Convention
+
+- Files starting with `_` are special (excluded from build)
+- Rule files: `area-description.md` (e.g., `async-parallel.md`)
+- Section is automatically inferred from filename prefix
+- Rules are sorted alphabetically by title within each section
+- IDs (e.g., 1.1, 1.2) are auto-generated during build
+
+## Impact Levels
+
+- `CRITICAL` - Highest priority, major performance gains
+- `HIGH` - Significant performance improvements
+- `MEDIUM-HIGH` - Moderate-high gains
+- `MEDIUM` - Moderate performance improvements
+- `LOW-MEDIUM` - Low-medium gains
+- `LOW` - Incremental improvements
+
+## Scripts
+
+- `pnpm build` - Compile rules into AGENTS.md
+- `pnpm validate` - Validate all rule files
+- `pnpm extract-tests` - Extract test cases for LLM evaluation
+- `pnpm dev` - Build and validate
+
+## Contributing
+
+When adding or modifying rules:
+
+1. Use the correct filename prefix for your section
+2. Follow the `_template.md` structure
+3. Include clear bad/good examples with explanations
+4. Add appropriate tags
+5. Run `pnpm build` to regenerate AGENTS.md and test-cases.json
+6. Rules are automatically sorted by title - no need to manage numbers!
+
+## Acknowledgments
+
+Originally created by [@shuding](https://x.com/shuding) at [Vercel](https://vercel.com).
diff --git a/.agents/skills/vercel-react-best-practices/SKILL.md b/.agents/skills/vercel-react-best-practices/SKILL.md
new file mode 100644
index 0000000..4417c6a
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/SKILL.md
@@ -0,0 +1,141 @@
+---
+name: vercel-react-best-practices
+description: React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
+license: MIT
+metadata:
+ author: vercel
+ version: "1.0.0"
+---
+
+# Vercel React Best Practices
+
+Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 62 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
+
+## When to Apply
+
+Reference these guidelines when:
+- Writing new React components or Next.js pages
+- Implementing data fetching (client or server-side)
+- Reviewing code for performance issues
+- Refactoring existing React/Next.js code
+- Optimizing bundle size or load times
+
+## Rule Categories by Priority
+
+| Priority | Category | Impact | Prefix |
+|----------|----------|--------|--------|
+| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
+| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
+| 3 | Server-Side Performance | HIGH | `server-` |
+| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
+| 5 | Re-render Optimization | MEDIUM | `rerender-` |
+| 6 | Rendering Performance | MEDIUM | `rendering-` |
+| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
+| 8 | Advanced Patterns | LOW | `advanced-` |
+
+## Quick Reference
+
+### 1. Eliminating Waterfalls (CRITICAL)
+
+- `async-defer-await` - Move await into branches where actually used
+- `async-parallel` - Use Promise.all() for independent operations
+- `async-dependencies` - Use better-all for partial dependencies
+- `async-api-routes` - Start promises early, await late in API routes
+- `async-suspense-boundaries` - Use Suspense to stream content
+
+### 2. Bundle Size Optimization (CRITICAL)
+
+- `bundle-barrel-imports` - Import directly, avoid barrel files
+- `bundle-dynamic-imports` - Use next/dynamic for heavy components
+- `bundle-defer-third-party` - Load analytics/logging after hydration
+- `bundle-conditional` - Load modules only when feature is activated
+- `bundle-preload` - Preload on hover/focus for perceived speed
+
+### 3. Server-Side Performance (HIGH)
+
+- `server-auth-actions` - Authenticate server actions like API routes
+- `server-cache-react` - Use React.cache() for per-request deduplication
+- `server-cache-lru` - Use LRU cache for cross-request caching
+- `server-dedup-props` - Avoid duplicate serialization in RSC props
+- `server-hoist-static-io` - Hoist static I/O (fonts, logos) to module level
+- `server-serialization` - Minimize data passed to client components
+- `server-parallel-fetching` - Restructure components to parallelize fetches
+- `server-after-nonblocking` - Use after() for non-blocking operations
+
+### 4. Client-Side Data Fetching (MEDIUM-HIGH)
+
+- `client-swr-dedup` - Use SWR for automatic request deduplication
+- `client-event-listeners` - Deduplicate global event listeners
+- `client-passive-event-listeners` - Use passive listeners for scroll
+- `client-localstorage-schema` - Version and minimize localStorage data
+
+### 5. Re-render Optimization (MEDIUM)
+
+- `rerender-defer-reads` - Don't subscribe to state only used in callbacks
+- `rerender-memo` - Extract expensive work into memoized components
+- `rerender-memo-with-default-value` - Hoist default non-primitive props
+- `rerender-dependencies` - Use primitive dependencies in effects
+- `rerender-derived-state` - Subscribe to derived booleans, not raw values
+- `rerender-derived-state-no-effect` - Derive state during render, not effects
+- `rerender-functional-setstate` - Use functional setState for stable callbacks
+- `rerender-lazy-state-init` - Pass function to useState for expensive values
+- `rerender-simple-expression-in-memo` - Avoid memo for simple primitives
+- `rerender-move-effect-to-event` - Put interaction logic in event handlers
+- `rerender-transitions` - Use startTransition for non-urgent updates
+- `rerender-use-ref-transient-values` - Use refs for transient frequent values
+- `rerender-no-inline-components` - Don't define components inside components
+
+### 6. Rendering Performance (MEDIUM)
+
+- `rendering-animate-svg-wrapper` - Animate div wrapper, not SVG element
+- `rendering-content-visibility` - Use content-visibility for long lists
+- `rendering-hoist-jsx` - Extract static JSX outside components
+- `rendering-svg-precision` - Reduce SVG coordinate precision
+- `rendering-hydration-no-flicker` - Use inline script for client-only data
+- `rendering-hydration-suppress-warning` - Suppress expected mismatches
+- `rendering-activity` - Use Activity component for show/hide
+- `rendering-conditional-render` - Use ternary, not && for conditionals
+- `rendering-usetransition-loading` - Prefer useTransition for loading state
+- `rendering-resource-hints` - Use React DOM resource hints for preloading
+- `rendering-script-defer-async` - Use defer or async on script tags
+
+### 7. JavaScript Performance (LOW-MEDIUM)
+
+- `js-batch-dom-css` - Group CSS changes via classes or cssText
+- `js-index-maps` - Build Map for repeated lookups
+- `js-cache-property-access` - Cache object properties in loops
+- `js-cache-function-results` - Cache function results in module-level Map
+- `js-cache-storage` - Cache localStorage/sessionStorage reads
+- `js-combine-iterations` - Combine multiple filter/map into one loop
+- `js-length-check-first` - Check array length before expensive comparison
+- `js-early-exit` - Return early from functions
+- `js-hoist-regexp` - Hoist RegExp creation outside loops
+- `js-min-max-loop` - Use loop for min/max instead of sort
+- `js-set-map-lookups` - Use Set/Map for O(1) lookups
+- `js-tosorted-immutable` - Use toSorted() for immutability
+- `js-flatmap-filter` - Use flatMap to map and filter in one pass
+
+### 8. Advanced Patterns (LOW)
+
+- `advanced-event-handler-refs` - Store event handlers in refs
+- `advanced-init-once` - Initialize app once per app load
+- `advanced-use-latest` - useLatest for stable callback refs
+
+## How to Use
+
+Read individual rule files for detailed explanations and code examples:
+
+```
+rules/async-parallel.md
+rules/bundle-barrel-imports.md
+```
+
+Each rule file contains:
+- Brief explanation of why it matters
+- Incorrect code example with explanation
+- Correct code example with explanation
+- Additional context and references
+
+## Full Compiled Document
+
+For the complete guide with all rules expanded: `AGENTS.md`
diff --git a/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md b/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md
new file mode 100644
index 0000000..97e7ade
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md
@@ -0,0 +1,55 @@
+---
+title: Store Event Handlers in Refs
+impact: LOW
+impactDescription: stable subscriptions
+tags: advanced, hooks, refs, event-handlers, optimization
+---
+
+## Store Event Handlers in Refs
+
+Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
+
+**Incorrect (re-subscribes on every render):**
+
+```tsx
+function useWindowEvent(event: string, handler: (e) => void) {
+ useEffect(() => {
+ window.addEventListener(event, handler)
+ return () => window.removeEventListener(event, handler)
+ }, [event, handler])
+}
+```
+
+**Correct (stable subscription):**
+
+```tsx
+function useWindowEvent(event: string, handler: (e) => void) {
+ const handlerRef = useRef(handler)
+ useEffect(() => {
+ handlerRef.current = handler
+ }, [handler])
+
+ useEffect(() => {
+ const listener = (e) => handlerRef.current(e)
+ window.addEventListener(event, listener)
+ return () => window.removeEventListener(event, listener)
+ }, [event])
+}
+```
+
+**Alternative: use `useEffectEvent` if you're on latest React:**
+
+```tsx
+import { useEffectEvent } from 'react'
+
+function useWindowEvent(event: string, handler: (e) => void) {
+ const onEvent = useEffectEvent(handler)
+
+ useEffect(() => {
+ window.addEventListener(event, onEvent)
+ return () => window.removeEventListener(event, onEvent)
+ }, [event])
+}
+```
+
+`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
diff --git a/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md b/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md
new file mode 100644
index 0000000..73ee38e
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md
@@ -0,0 +1,42 @@
+---
+title: Initialize App Once, Not Per Mount
+impact: LOW-MEDIUM
+impactDescription: avoids duplicate init in development
+tags: initialization, useEffect, app-startup, side-effects
+---
+
+## Initialize App Once, Not Per Mount
+
+Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead.
+
+**Incorrect (runs twice in dev, re-runs on remount):**
+
+```tsx
+function Comp() {
+ useEffect(() => {
+ loadFromStorage()
+ checkAuthToken()
+ }, [])
+
+ // ...
+}
+```
+
+**Correct (once per app load):**
+
+```tsx
+let didInit = false
+
+function Comp() {
+ useEffect(() => {
+ if (didInit) return
+ didInit = true
+ loadFromStorage()
+ checkAuthToken()
+ }, [])
+
+ // ...
+}
+```
+
+Reference: [Initializing the application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)
diff --git a/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md b/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md
new file mode 100644
index 0000000..9c7cb50
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md
@@ -0,0 +1,39 @@
+---
+title: useEffectEvent for Stable Callback Refs
+impact: LOW
+impactDescription: prevents effect re-runs
+tags: advanced, hooks, useEffectEvent, refs, optimization
+---
+
+## useEffectEvent for Stable Callback Refs
+
+Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
+
+**Incorrect (effect re-runs on every callback change):**
+
+```tsx
+function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
+ const [query, setQuery] = useState('')
+
+ useEffect(() => {
+ const timeout = setTimeout(() => onSearch(query), 300)
+ return () => clearTimeout(timeout)
+ }, [query, onSearch])
+}
+```
+
+**Correct (using React's useEffectEvent):**
+
+```tsx
+import { useEffectEvent } from 'react';
+
+function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
+ const [query, setQuery] = useState('')
+ const onSearchEvent = useEffectEvent(onSearch)
+
+ useEffect(() => {
+ const timeout = setTimeout(() => onSearchEvent(query), 300)
+ return () => clearTimeout(timeout)
+ }, [query])
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md b/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md
new file mode 100644
index 0000000..6feda1e
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md
@@ -0,0 +1,38 @@
+---
+title: Prevent Waterfall Chains in API Routes
+impact: CRITICAL
+impactDescription: 2-10× improvement
+tags: api-routes, server-actions, waterfalls, parallelization
+---
+
+## Prevent Waterfall Chains in API Routes
+
+In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
+
+**Incorrect (config waits for auth, data waits for both):**
+
+```typescript
+export async function GET(request: Request) {
+ const session = await auth()
+ const config = await fetchConfig()
+ const data = await fetchData(session.user.id)
+ return Response.json({ data, config })
+}
+```
+
+**Correct (auth and config start immediately):**
+
+```typescript
+export async function GET(request: Request) {
+ const sessionPromise = auth()
+ const configPromise = fetchConfig()
+ const session = await sessionPromise
+ const [config, data] = await Promise.all([
+ configPromise,
+ fetchData(session.user.id)
+ ])
+ return Response.json({ data, config })
+}
+```
+
+For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
diff --git a/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md b/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md
new file mode 100644
index 0000000..ea7082a
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md
@@ -0,0 +1,80 @@
+---
+title: Defer Await Until Needed
+impact: HIGH
+impactDescription: avoids blocking unused code paths
+tags: async, await, conditional, optimization
+---
+
+## Defer Await Until Needed
+
+Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
+
+**Incorrect (blocks both branches):**
+
+```typescript
+async function handleRequest(userId: string, skipProcessing: boolean) {
+ const userData = await fetchUserData(userId)
+
+ if (skipProcessing) {
+ // Returns immediately but still waited for userData
+ return { skipped: true }
+ }
+
+ // Only this branch uses userData
+ return processUserData(userData)
+}
+```
+
+**Correct (only blocks when needed):**
+
+```typescript
+async function handleRequest(userId: string, skipProcessing: boolean) {
+ if (skipProcessing) {
+ // Returns immediately without waiting
+ return { skipped: true }
+ }
+
+ // Fetch only when needed
+ const userData = await fetchUserData(userId)
+ return processUserData(userData)
+}
+```
+
+**Another example (early return optimization):**
+
+```typescript
+// Incorrect: always fetches permissions
+async function updateResource(resourceId: string, userId: string) {
+ const permissions = await fetchPermissions(userId)
+ const resource = await getResource(resourceId)
+
+ if (!resource) {
+ return { error: 'Not found' }
+ }
+
+ if (!permissions.canEdit) {
+ return { error: 'Forbidden' }
+ }
+
+ return await updateResourceData(resource, permissions)
+}
+
+// Correct: fetches only when needed
+async function updateResource(resourceId: string, userId: string) {
+ const resource = await getResource(resourceId)
+
+ if (!resource) {
+ return { error: 'Not found' }
+ }
+
+ const permissions = await fetchPermissions(userId)
+
+ if (!permissions.canEdit) {
+ return { error: 'Forbidden' }
+ }
+
+ return await updateResourceData(resource, permissions)
+}
+```
+
+This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
diff --git a/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md b/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md
new file mode 100644
index 0000000..0484eba
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md
@@ -0,0 +1,51 @@
+---
+title: Dependency-Based Parallelization
+impact: CRITICAL
+impactDescription: 2-10× improvement
+tags: async, parallelization, dependencies, better-all
+---
+
+## Dependency-Based Parallelization
+
+For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
+
+**Incorrect (profile waits for config unnecessarily):**
+
+```typescript
+const [user, config] = await Promise.all([
+ fetchUser(),
+ fetchConfig()
+])
+const profile = await fetchProfile(user.id)
+```
+
+**Correct (config and profile run in parallel):**
+
+```typescript
+import { all } from 'better-all'
+
+const { user, config, profile } = await all({
+ async user() { return fetchUser() },
+ async config() { return fetchConfig() },
+ async profile() {
+ return fetchProfile((await this.$.user).id)
+ }
+})
+```
+
+**Alternative without extra dependencies:**
+
+We can also create all the promises first, and do `Promise.all()` at the end.
+
+```typescript
+const userPromise = fetchUser()
+const profilePromise = userPromise.then(user => fetchProfile(user.id))
+
+const [user, config, profile] = await Promise.all([
+ userPromise,
+ fetchConfig(),
+ profilePromise
+])
+```
+
+Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
diff --git a/.agents/skills/vercel-react-best-practices/rules/async-parallel.md b/.agents/skills/vercel-react-best-practices/rules/async-parallel.md
new file mode 100644
index 0000000..64133f6
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/async-parallel.md
@@ -0,0 +1,28 @@
+---
+title: Promise.all() for Independent Operations
+impact: CRITICAL
+impactDescription: 2-10× improvement
+tags: async, parallelization, promises, waterfalls
+---
+
+## Promise.all() for Independent Operations
+
+When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
+
+**Incorrect (sequential execution, 3 round trips):**
+
+```typescript
+const user = await fetchUser()
+const posts = await fetchPosts()
+const comments = await fetchComments()
+```
+
+**Correct (parallel execution, 1 round trip):**
+
+```typescript
+const [user, posts, comments] = await Promise.all([
+ fetchUser(),
+ fetchPosts(),
+ fetchComments()
+])
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md b/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md
new file mode 100644
index 0000000..1fbc05b
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md
@@ -0,0 +1,99 @@
+---
+title: Strategic Suspense Boundaries
+impact: HIGH
+impactDescription: faster initial paint
+tags: async, suspense, streaming, layout-shift
+---
+
+## Strategic Suspense Boundaries
+
+Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
+
+**Incorrect (wrapper blocked by data fetching):**
+
+```tsx
+async function Page() {
+ const data = await fetchData() // Blocks entire page
+
+ return (
+
+
Sidebar
+
Header
+
+
+
+
Footer
+
+ )
+}
+```
+
+The entire layout waits for data even though only the middle section needs it.
+
+**Correct (wrapper shows immediately, data streams in):**
+
+```tsx
+function Page() {
+ return (
+
+
Sidebar
+
Header
+
+ }>
+
+
+
+
Footer
+
+ )
+}
+
+async function DataDisplay() {
+ const data = await fetchData() // Only blocks this component
+ return {data.content}
+}
+```
+
+Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
+
+**Alternative (share promise across components):**
+
+```tsx
+function Page() {
+ // Start fetch immediately, but don't await
+ const dataPromise = fetchData()
+
+ return (
+
+
Sidebar
+
Header
+
}>
+
+
+
+
Footer
+
+ )
+}
+
+function DataDisplay({ dataPromise }: { dataPromise: Promise }) {
+ const data = use(dataPromise) // Unwraps the promise
+ return {data.content}
+}
+
+function DataSummary({ dataPromise }: { dataPromise: Promise }) {
+ const data = use(dataPromise) // Reuses the same promise
+ return {data.summary}
+}
+```
+
+Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.
+
+**When NOT to use this pattern:**
+
+- Critical data needed for layout decisions (affects positioning)
+- SEO-critical content above the fold
+- Small, fast queries where suspense overhead isn't worth it
+- When you want to avoid layout shift (loading → content jump)
+
+**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.
diff --git a/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md b/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md
new file mode 100644
index 0000000..ee48f32
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md
@@ -0,0 +1,59 @@
+---
+title: Avoid Barrel File Imports
+impact: CRITICAL
+impactDescription: 200-800ms import cost, slow builds
+tags: bundle, imports, tree-shaking, barrel-files, performance
+---
+
+## Avoid Barrel File Imports
+
+Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).
+
+Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.
+
+**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.
+
+**Incorrect (imports entire library):**
+
+```tsx
+import { Check, X, Menu } from 'lucide-react'
+// Loads 1,583 modules, takes ~2.8s extra in dev
+// Runtime cost: 200-800ms on every cold start
+
+import { Button, TextField } from '@mui/material'
+// Loads 2,225 modules, takes ~4.2s extra in dev
+```
+
+**Correct (imports only what you need):**
+
+```tsx
+import Check from 'lucide-react/dist/esm/icons/check'
+import X from 'lucide-react/dist/esm/icons/x'
+import Menu from 'lucide-react/dist/esm/icons/menu'
+// Loads only 3 modules (~2KB vs ~1MB)
+
+import Button from '@mui/material/Button'
+import TextField from '@mui/material/TextField'
+// Loads only what you use
+```
+
+**Alternative (Next.js 13.5+):**
+
+```js
+// next.config.js - use optimizePackageImports
+module.exports = {
+ experimental: {
+ optimizePackageImports: ['lucide-react', '@mui/material']
+ }
+}
+
+// Then you can keep the ergonomic barrel imports:
+import { Check, X, Menu } from 'lucide-react'
+// Automatically transformed to direct imports at build time
+```
+
+Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
+
+Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.
+
+Reference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
diff --git a/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md b/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md
new file mode 100644
index 0000000..99d6fc9
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md
@@ -0,0 +1,31 @@
+---
+title: Conditional Module Loading
+impact: HIGH
+impactDescription: loads large data only when needed
+tags: bundle, conditional-loading, lazy-loading
+---
+
+## Conditional Module Loading
+
+Load large data or modules only when a feature is activated.
+
+**Example (lazy-load animation frames):**
+
+```tsx
+function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch> }) {
+ const [frames, setFrames] = useState (null)
+
+ useEffect(() => {
+ if (enabled && !frames && typeof window !== 'undefined') {
+ import('./animation-frames.js')
+ .then(mod => setFrames(mod.frames))
+ .catch(() => setEnabled(false))
+ }
+ }, [enabled, frames, setEnabled])
+
+ if (!frames) return
+ return
+}
+```
+
+The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
diff --git a/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md b/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md
new file mode 100644
index 0000000..db041d1
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md
@@ -0,0 +1,49 @@
+---
+title: Defer Non-Critical Third-Party Libraries
+impact: MEDIUM
+impactDescription: loads after hydration
+tags: bundle, third-party, analytics, defer
+---
+
+## Defer Non-Critical Third-Party Libraries
+
+Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
+
+**Incorrect (blocks initial bundle):**
+
+```tsx
+import { Analytics } from '@vercel/analytics/react'
+
+export default function RootLayout({ children }) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+```
+
+**Correct (loads after hydration):**
+
+```tsx
+import dynamic from 'next/dynamic'
+
+const Analytics = dynamic(
+ () => import('@vercel/analytics/react').then(m => m.Analytics),
+ { ssr: false }
+)
+
+export default function RootLayout({ children }) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md b/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md
new file mode 100644
index 0000000..60b6269
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md
@@ -0,0 +1,35 @@
+---
+title: Dynamic Imports for Heavy Components
+impact: CRITICAL
+impactDescription: directly affects TTI and LCP
+tags: bundle, dynamic-import, code-splitting, next-dynamic
+---
+
+## Dynamic Imports for Heavy Components
+
+Use `next/dynamic` to lazy-load large components not needed on initial render.
+
+**Incorrect (Monaco bundles with main chunk ~300KB):**
+
+```tsx
+import { MonacoEditor } from './monaco-editor'
+
+function CodePanel({ code }: { code: string }) {
+ return
+}
+```
+
+**Correct (Monaco loads on demand):**
+
+```tsx
+import dynamic from 'next/dynamic'
+
+const MonacoEditor = dynamic(
+ () => import('./monaco-editor').then(m => m.MonacoEditor),
+ { ssr: false }
+)
+
+function CodePanel({ code }: { code: string }) {
+ return
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md b/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md
new file mode 100644
index 0000000..7000504
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md
@@ -0,0 +1,50 @@
+---
+title: Preload Based on User Intent
+impact: MEDIUM
+impactDescription: reduces perceived latency
+tags: bundle, preload, user-intent, hover
+---
+
+## Preload Based on User Intent
+
+Preload heavy bundles before they're needed to reduce perceived latency.
+
+**Example (preload on hover/focus):**
+
+```tsx
+function EditorButton({ onClick }: { onClick: () => void }) {
+ const preload = () => {
+ if (typeof window !== 'undefined') {
+ void import('./monaco-editor')
+ }
+ }
+
+ return (
+
+ Open Editor
+
+ )
+}
+```
+
+**Example (preload when feature flag is enabled):**
+
+```tsx
+function FlagsProvider({ children, flags }: Props) {
+ useEffect(() => {
+ if (flags.editorEnabled && typeof window !== 'undefined') {
+ void import('./monaco-editor').then(mod => mod.init())
+ }
+ }, [flags.editorEnabled])
+
+ return
+ {children}
+
+}
+```
+
+The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
diff --git a/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md b/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md
new file mode 100644
index 0000000..aad4ae9
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md
@@ -0,0 +1,74 @@
+---
+title: Deduplicate Global Event Listeners
+impact: LOW
+impactDescription: single listener for N components
+tags: client, swr, event-listeners, subscription
+---
+
+## Deduplicate Global Event Listeners
+
+Use `useSWRSubscription()` to share global event listeners across component instances.
+
+**Incorrect (N instances = N listeners):**
+
+```tsx
+function useKeyboardShortcut(key: string, callback: () => void) {
+ useEffect(() => {
+ const handler = (e: KeyboardEvent) => {
+ if (e.metaKey && e.key === key) {
+ callback()
+ }
+ }
+ window.addEventListener('keydown', handler)
+ return () => window.removeEventListener('keydown', handler)
+ }, [key, callback])
+}
+```
+
+When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.
+
+**Correct (N instances = 1 listener):**
+
+```tsx
+import useSWRSubscription from 'swr/subscription'
+
+// Module-level Map to track callbacks per key
+const keyCallbacks = new Map void>>()
+
+function useKeyboardShortcut(key: string, callback: () => void) {
+ // Register this callback in the Map
+ useEffect(() => {
+ if (!keyCallbacks.has(key)) {
+ keyCallbacks.set(key, new Set())
+ }
+ keyCallbacks.get(key)!.add(callback)
+
+ return () => {
+ const set = keyCallbacks.get(key)
+ if (set) {
+ set.delete(callback)
+ if (set.size === 0) {
+ keyCallbacks.delete(key)
+ }
+ }
+ }
+ }, [key, callback])
+
+ useSWRSubscription('global-keydown', () => {
+ const handler = (e: KeyboardEvent) => {
+ if (e.metaKey && keyCallbacks.has(e.key)) {
+ keyCallbacks.get(e.key)!.forEach(cb => cb())
+ }
+ }
+ window.addEventListener('keydown', handler)
+ return () => window.removeEventListener('keydown', handler)
+ })
+}
+
+function Profile() {
+ // Multiple shortcuts will share the same listener
+ useKeyboardShortcut('p', () => { /* ... */ })
+ useKeyboardShortcut('k', () => { /* ... */ })
+ // ...
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md b/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md
new file mode 100644
index 0000000..d30a1a7
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md
@@ -0,0 +1,71 @@
+---
+title: Version and Minimize localStorage Data
+impact: MEDIUM
+impactDescription: prevents schema conflicts, reduces storage size
+tags: client, localStorage, storage, versioning, data-minimization
+---
+
+## Version and Minimize localStorage Data
+
+Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data.
+
+**Incorrect:**
+
+```typescript
+// No version, stores everything, no error handling
+localStorage.setItem('userConfig', JSON.stringify(fullUserObject))
+const data = localStorage.getItem('userConfig')
+```
+
+**Correct:**
+
+```typescript
+const VERSION = 'v2'
+
+function saveConfig(config: { theme: string; language: string }) {
+ try {
+ localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
+ } catch {
+ // Throws in incognito/private browsing, quota exceeded, or disabled
+ }
+}
+
+function loadConfig() {
+ try {
+ const data = localStorage.getItem(`userConfig:${VERSION}`)
+ return data ? JSON.parse(data) : null
+ } catch {
+ return null
+ }
+}
+
+// Migration from v1 to v2
+function migrate() {
+ try {
+ const v1 = localStorage.getItem('userConfig:v1')
+ if (v1) {
+ const old = JSON.parse(v1)
+ saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })
+ localStorage.removeItem('userConfig:v1')
+ }
+ } catch {}
+}
+```
+
+**Store minimal fields from server responses:**
+
+```typescript
+// User object has 20+ fields, only store what UI needs
+function cachePrefs(user: FullUser) {
+ try {
+ localStorage.setItem('prefs:v1', JSON.stringify({
+ theme: user.preferences.theme,
+ notifications: user.preferences.notifications
+ }))
+ } catch {}
+}
+```
+
+**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled.
+
+**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.
diff --git a/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md b/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md
new file mode 100644
index 0000000..ce39a88
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md
@@ -0,0 +1,48 @@
+---
+title: Use Passive Event Listeners for Scrolling Performance
+impact: MEDIUM
+impactDescription: eliminates scroll delay caused by event listeners
+tags: client, event-listeners, scrolling, performance, touch, wheel
+---
+
+## Use Passive Event Listeners for Scrolling Performance
+
+Add `{ passive: true }` to touch and wheel event listeners to enable immediate scrolling. Browsers normally wait for listeners to finish to check if `preventDefault()` is called, causing scroll delay.
+
+**Incorrect:**
+
+```typescript
+useEffect(() => {
+ const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
+ const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
+
+ document.addEventListener('touchstart', handleTouch)
+ document.addEventListener('wheel', handleWheel)
+
+ return () => {
+ document.removeEventListener('touchstart', handleTouch)
+ document.removeEventListener('wheel', handleWheel)
+ }
+}, [])
+```
+
+**Correct:**
+
+```typescript
+useEffect(() => {
+ const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
+ const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
+
+ document.addEventListener('touchstart', handleTouch, { passive: true })
+ document.addEventListener('wheel', handleWheel, { passive: true })
+
+ return () => {
+ document.removeEventListener('touchstart', handleTouch)
+ document.removeEventListener('wheel', handleWheel)
+ }
+}, [])
+```
+
+**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
+
+**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.
diff --git a/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md b/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md
new file mode 100644
index 0000000..2a430f2
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md
@@ -0,0 +1,56 @@
+---
+title: Use SWR for Automatic Deduplication
+impact: MEDIUM-HIGH
+impactDescription: automatic deduplication
+tags: client, swr, deduplication, data-fetching
+---
+
+## Use SWR for Automatic Deduplication
+
+SWR enables request deduplication, caching, and revalidation across component instances.
+
+**Incorrect (no deduplication, each instance fetches):**
+
+```tsx
+function UserList() {
+ const [users, setUsers] = useState([])
+ useEffect(() => {
+ fetch('/api/users')
+ .then(r => r.json())
+ .then(setUsers)
+ }, [])
+}
+```
+
+**Correct (multiple instances share one request):**
+
+```tsx
+import useSWR from 'swr'
+
+function UserList() {
+ const { data: users } = useSWR('/api/users', fetcher)
+}
+```
+
+**For immutable data:**
+
+```tsx
+import { useImmutableSWR } from '@/lib/swr'
+
+function StaticContent() {
+ const { data } = useImmutableSWR('/api/config', fetcher)
+}
+```
+
+**For mutations:**
+
+```tsx
+import { useSWRMutation } from 'swr/mutation'
+
+function UpdateButton() {
+ const { trigger } = useSWRMutation('/api/user', updateUser)
+ return trigger()}>Update
+}
+```
+
+Reference: [https://swr.vercel.app](https://swr.vercel.app)
diff --git a/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md b/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md
new file mode 100644
index 0000000..a62d84e
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md
@@ -0,0 +1,107 @@
+---
+title: Avoid Layout Thrashing
+impact: MEDIUM
+impactDescription: prevents forced synchronous layouts and reduces performance bottlenecks
+tags: javascript, dom, css, performance, reflow, layout-thrashing
+---
+
+## Avoid Layout Thrashing
+
+Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.
+
+**This is OK (browser batches style changes):**
+```typescript
+function updateElementStyles(element: HTMLElement) {
+ // Each line invalidates style, but browser batches the recalculation
+ element.style.width = '100px'
+ element.style.height = '200px'
+ element.style.backgroundColor = 'blue'
+ element.style.border = '1px solid black'
+}
+```
+
+**Incorrect (interleaved reads and writes force reflows):**
+```typescript
+function layoutThrashing(element: HTMLElement) {
+ element.style.width = '100px'
+ const width = element.offsetWidth // Forces reflow
+ element.style.height = '200px'
+ const height = element.offsetHeight // Forces another reflow
+}
+```
+
+**Correct (batch writes, then read once):**
+```typescript
+function updateElementStyles(element: HTMLElement) {
+ // Batch all writes together
+ element.style.width = '100px'
+ element.style.height = '200px'
+ element.style.backgroundColor = 'blue'
+ element.style.border = '1px solid black'
+
+ // Read after all writes are done (single reflow)
+ const { width, height } = element.getBoundingClientRect()
+}
+```
+
+**Correct (batch reads, then writes):**
+```typescript
+function avoidThrashing(element: HTMLElement) {
+ // Read phase - all layout queries first
+ const rect1 = element.getBoundingClientRect()
+ const offsetWidth = element.offsetWidth
+ const offsetHeight = element.offsetHeight
+
+ // Write phase - all style changes after
+ element.style.width = '100px'
+ element.style.height = '200px'
+}
+```
+
+**Better: use CSS classes**
+```css
+.highlighted-box {
+ width: 100px;
+ height: 200px;
+ background-color: blue;
+ border: 1px solid black;
+}
+```
+```typescript
+function updateElementStyles(element: HTMLElement) {
+ element.classList.add('highlighted-box')
+
+ const { width, height } = element.getBoundingClientRect()
+}
+```
+
+**React example:**
+```tsx
+// Incorrect: interleaving style changes with layout queries
+function Box({ isHighlighted }: { isHighlighted: boolean }) {
+ const ref = useRef(null)
+
+ useEffect(() => {
+ if (ref.current && isHighlighted) {
+ ref.current.style.width = '100px'
+ const width = ref.current.offsetWidth // Forces layout
+ ref.current.style.height = '200px'
+ }
+ }, [isHighlighted])
+
+ return Content
+}
+
+// Correct: toggle class
+function Box({ isHighlighted }: { isHighlighted: boolean }) {
+ return (
+
+ Content
+
+ )
+}
+```
+
+Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.
+
+See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.
diff --git a/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md b/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md
new file mode 100644
index 0000000..180f8ac
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md
@@ -0,0 +1,80 @@
+---
+title: Cache Repeated Function Calls
+impact: MEDIUM
+impactDescription: avoid redundant computation
+tags: javascript, cache, memoization, performance
+---
+
+## Cache Repeated Function Calls
+
+Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.
+
+**Incorrect (redundant computation):**
+
+```typescript
+function ProjectList({ projects }: { projects: Project[] }) {
+ return (
+
+ {projects.map(project => {
+ // slugify() called 100+ times for same project names
+ const slug = slugify(project.name)
+
+ return
+ })}
+
+ )
+}
+```
+
+**Correct (cached results):**
+
+```typescript
+// Module-level cache
+const slugifyCache = new Map()
+
+function cachedSlugify(text: string): string {
+ if (slugifyCache.has(text)) {
+ return slugifyCache.get(text)!
+ }
+ const result = slugify(text)
+ slugifyCache.set(text, result)
+ return result
+}
+
+function ProjectList({ projects }: { projects: Project[] }) {
+ return (
+
+ {projects.map(project => {
+ // Computed only once per unique project name
+ const slug = cachedSlugify(project.name)
+
+ return
+ })}
+
+ )
+}
+```
+
+**Simpler pattern for single-value functions:**
+
+```typescript
+let isLoggedInCache: boolean | null = null
+
+function isLoggedIn(): boolean {
+ if (isLoggedInCache !== null) {
+ return isLoggedInCache
+ }
+
+ isLoggedInCache = document.cookie.includes('auth=')
+ return isLoggedInCache
+}
+
+// Clear cache when auth changes
+function onAuthChange() {
+ isLoggedInCache = null
+}
+```
+
+Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
+
+Reference: [How we made the Vercel Dashboard twice as fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
diff --git a/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md b/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md
new file mode 100644
index 0000000..39eec90
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md
@@ -0,0 +1,28 @@
+---
+title: Cache Property Access in Loops
+impact: LOW-MEDIUM
+impactDescription: reduces lookups
+tags: javascript, loops, optimization, caching
+---
+
+## Cache Property Access in Loops
+
+Cache object property lookups in hot paths.
+
+**Incorrect (3 lookups × N iterations):**
+
+```typescript
+for (let i = 0; i < arr.length; i++) {
+ process(obj.config.settings.value)
+}
+```
+
+**Correct (1 lookup total):**
+
+```typescript
+const value = obj.config.settings.value
+const len = arr.length
+for (let i = 0; i < len; i++) {
+ process(value)
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md b/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md
new file mode 100644
index 0000000..aa4a30c
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md
@@ -0,0 +1,70 @@
+---
+title: Cache Storage API Calls
+impact: LOW-MEDIUM
+impactDescription: reduces expensive I/O
+tags: javascript, localStorage, storage, caching, performance
+---
+
+## Cache Storage API Calls
+
+`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.
+
+**Incorrect (reads storage on every call):**
+
+```typescript
+function getTheme() {
+ return localStorage.getItem('theme') ?? 'light'
+}
+// Called 10 times = 10 storage reads
+```
+
+**Correct (Map cache):**
+
+```typescript
+const storageCache = new Map()
+
+function getLocalStorage(key: string) {
+ if (!storageCache.has(key)) {
+ storageCache.set(key, localStorage.getItem(key))
+ }
+ return storageCache.get(key)
+}
+
+function setLocalStorage(key: string, value: string) {
+ localStorage.setItem(key, value)
+ storageCache.set(key, value) // keep cache in sync
+}
+```
+
+Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
+
+**Cookie caching:**
+
+```typescript
+let cookieCache: Record | null = null
+
+function getCookie(name: string) {
+ if (!cookieCache) {
+ cookieCache = Object.fromEntries(
+ document.cookie.split('; ').map(c => c.split('='))
+ )
+ }
+ return cookieCache[name]
+}
+```
+
+**Important (invalidate on external changes):**
+
+If storage can change externally (another tab, server-set cookies), invalidate cache:
+
+```typescript
+window.addEventListener('storage', (e) => {
+ if (e.key) storageCache.delete(e.key)
+})
+
+document.addEventListener('visibilitychange', () => {
+ if (document.visibilityState === 'visible') {
+ storageCache.clear()
+ }
+})
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md b/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md
new file mode 100644
index 0000000..044d017
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md
@@ -0,0 +1,32 @@
+---
+title: Combine Multiple Array Iterations
+impact: LOW-MEDIUM
+impactDescription: reduces iterations
+tags: javascript, arrays, loops, performance
+---
+
+## Combine Multiple Array Iterations
+
+Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.
+
+**Incorrect (3 iterations):**
+
+```typescript
+const admins = users.filter(u => u.isAdmin)
+const testers = users.filter(u => u.isTester)
+const inactive = users.filter(u => !u.isActive)
+```
+
+**Correct (1 iteration):**
+
+```typescript
+const admins: User[] = []
+const testers: User[] = []
+const inactive: User[] = []
+
+for (const user of users) {
+ if (user.isAdmin) admins.push(user)
+ if (user.isTester) testers.push(user)
+ if (!user.isActive) inactive.push(user)
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md b/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md
new file mode 100644
index 0000000..f46cb89
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md
@@ -0,0 +1,50 @@
+---
+title: Early Return from Functions
+impact: LOW-MEDIUM
+impactDescription: avoids unnecessary computation
+tags: javascript, functions, optimization, early-return
+---
+
+## Early Return from Functions
+
+Return early when result is determined to skip unnecessary processing.
+
+**Incorrect (processes all items even after finding answer):**
+
+```typescript
+function validateUsers(users: User[]) {
+ let hasError = false
+ let errorMessage = ''
+
+ for (const user of users) {
+ if (!user.email) {
+ hasError = true
+ errorMessage = 'Email required'
+ }
+ if (!user.name) {
+ hasError = true
+ errorMessage = 'Name required'
+ }
+ // Continues checking all users even after error found
+ }
+
+ return hasError ? { valid: false, error: errorMessage } : { valid: true }
+}
+```
+
+**Correct (returns immediately on first error):**
+
+```typescript
+function validateUsers(users: User[]) {
+ for (const user of users) {
+ if (!user.email) {
+ return { valid: false, error: 'Email required' }
+ }
+ if (!user.name) {
+ return { valid: false, error: 'Name required' }
+ }
+ }
+
+ return { valid: true }
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md b/.agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md
new file mode 100644
index 0000000..ee0edf0
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md
@@ -0,0 +1,60 @@
+---
+title: Use flatMap to Map and Filter in One Pass
+impact: LOW-MEDIUM
+impactDescription: eliminates intermediate array
+tags: javascript, arrays, flatMap, filter, performance
+---
+
+## Use flatMap to Map and Filter in One Pass
+
+**Impact: LOW-MEDIUM (eliminates intermediate array)**
+
+Chaining `.map().filter(Boolean)` creates an intermediate array and iterates twice. Use `.flatMap()` to transform and filter in a single pass.
+
+**Incorrect (2 iterations, intermediate array):**
+
+```typescript
+const userNames = users
+ .map(user => user.isActive ? user.name : null)
+ .filter(Boolean)
+```
+
+**Correct (1 iteration, no intermediate array):**
+
+```typescript
+const userNames = users.flatMap(user =>
+ user.isActive ? [user.name] : []
+)
+```
+
+**More examples:**
+
+```typescript
+// Extract valid emails from responses
+// Before
+const emails = responses
+ .map(r => r.success ? r.data.email : null)
+ .filter(Boolean)
+
+// After
+const emails = responses.flatMap(r =>
+ r.success ? [r.data.email] : []
+)
+
+// Parse and filter valid numbers
+// Before
+const numbers = strings
+ .map(s => parseInt(s, 10))
+ .filter(n => !isNaN(n))
+
+// After
+const numbers = strings.flatMap(s => {
+ const n = parseInt(s, 10)
+ return isNaN(n) ? [] : [n]
+})
+```
+
+**When to use:**
+- Transforming items while filtering some out
+- Conditional mapping where some inputs produce no output
+- Parsing/validating where invalid inputs should be skipped
diff --git a/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md b/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md
new file mode 100644
index 0000000..dae3fef
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md
@@ -0,0 +1,45 @@
+---
+title: Hoist RegExp Creation
+impact: LOW-MEDIUM
+impactDescription: avoids recreation
+tags: javascript, regexp, optimization, memoization
+---
+
+## Hoist RegExp Creation
+
+Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.
+
+**Incorrect (new RegExp every render):**
+
+```tsx
+function Highlighter({ text, query }: Props) {
+ const regex = new RegExp(`(${query})`, 'gi')
+ const parts = text.split(regex)
+ return <>{parts.map((part, i) => ...)}>
+}
+```
+
+**Correct (memoize or hoist):**
+
+```tsx
+const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+
+function Highlighter({ text, query }: Props) {
+ const regex = useMemo(
+ () => new RegExp(`(${escapeRegex(query)})`, 'gi'),
+ [query]
+ )
+ const parts = text.split(regex)
+ return <>{parts.map((part, i) => ...)}>
+}
+```
+
+**Warning (global regex has mutable state):**
+
+Global regex (`/g`) has mutable `lastIndex` state:
+
+```typescript
+const regex = /foo/g
+regex.test('foo') // true, lastIndex = 3
+regex.test('foo') // false, lastIndex = 0
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md b/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md
new file mode 100644
index 0000000..9d357a0
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md
@@ -0,0 +1,37 @@
+---
+title: Build Index Maps for Repeated Lookups
+impact: LOW-MEDIUM
+impactDescription: 1M ops to 2K ops
+tags: javascript, map, indexing, optimization, performance
+---
+
+## Build Index Maps for Repeated Lookups
+
+Multiple `.find()` calls by the same key should use a Map.
+
+**Incorrect (O(n) per lookup):**
+
+```typescript
+function processOrders(orders: Order[], users: User[]) {
+ return orders.map(order => ({
+ ...order,
+ user: users.find(u => u.id === order.userId)
+ }))
+}
+```
+
+**Correct (O(1) per lookup):**
+
+```typescript
+function processOrders(orders: Order[], users: User[]) {
+ const userById = new Map(users.map(u => [u.id, u]))
+
+ return orders.map(order => ({
+ ...order,
+ user: userById.get(order.userId)
+ }))
+}
+```
+
+Build map once (O(n)), then all lookups are O(1).
+For 1000 orders × 1000 users: 1M ops → 2K ops.
diff --git a/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md b/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md
new file mode 100644
index 0000000..8b89573
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md
@@ -0,0 +1,49 @@
+---
+title: Early Length Check for Array Comparisons
+impact: MEDIUM-HIGH
+impactDescription: avoids expensive operations when lengths differ
+tags: javascript, arrays, performance, optimization, comparison
+---
+
+## Early Length Check for Array Comparisons
+
+When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.
+
+In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).
+
+**Incorrect (always runs expensive comparison):**
+
+```typescript
+function hasChanges(current: string[], original: string[]) {
+ // Always sorts and joins, even when lengths differ
+ return current.sort().join() !== original.sort().join()
+}
+```
+
+Two O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings.
+
+**Correct (O(1) length check first):**
+
+```typescript
+function hasChanges(current: string[], original: string[]) {
+ // Early return if lengths differ
+ if (current.length !== original.length) {
+ return true
+ }
+ // Only sort when lengths match
+ const currentSorted = current.toSorted()
+ const originalSorted = original.toSorted()
+ for (let i = 0; i < currentSorted.length; i++) {
+ if (currentSorted[i] !== originalSorted[i]) {
+ return true
+ }
+ }
+ return false
+}
+```
+
+This new approach is more efficient because:
+- It avoids the overhead of sorting and joining the arrays when lengths differ
+- It avoids consuming memory for the joined strings (especially important for large arrays)
+- It avoids mutating the original arrays
+- It returns early when a difference is found
diff --git a/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md b/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md
new file mode 100644
index 0000000..4b6656e
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md
@@ -0,0 +1,82 @@
+---
+title: Use Loop for Min/Max Instead of Sort
+impact: LOW
+impactDescription: O(n) instead of O(n log n)
+tags: javascript, arrays, performance, sorting, algorithms
+---
+
+## Use Loop for Min/Max Instead of Sort
+
+Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.
+
+**Incorrect (O(n log n) - sort to find latest):**
+
+```typescript
+interface Project {
+ id: string
+ name: string
+ updatedAt: number
+}
+
+function getLatestProject(projects: Project[]) {
+ const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
+ return sorted[0]
+}
+```
+
+Sorts the entire array just to find the maximum value.
+
+**Incorrect (O(n log n) - sort for oldest and newest):**
+
+```typescript
+function getOldestAndNewest(projects: Project[]) {
+ const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
+ return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
+}
+```
+
+Still sorts unnecessarily when only min/max are needed.
+
+**Correct (O(n) - single loop):**
+
+```typescript
+function getLatestProject(projects: Project[]) {
+ if (projects.length === 0) return null
+
+ let latest = projects[0]
+
+ for (let i = 1; i < projects.length; i++) {
+ if (projects[i].updatedAt > latest.updatedAt) {
+ latest = projects[i]
+ }
+ }
+
+ return latest
+}
+
+function getOldestAndNewest(projects: Project[]) {
+ if (projects.length === 0) return { oldest: null, newest: null }
+
+ let oldest = projects[0]
+ let newest = projects[0]
+
+ for (let i = 1; i < projects.length; i++) {
+ if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
+ if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]
+ }
+
+ return { oldest, newest }
+}
+```
+
+Single pass through the array, no copying, no sorting.
+
+**Alternative (Math.min/Math.max for small arrays):**
+
+```typescript
+const numbers = [5, 2, 8, 1, 9]
+const min = Math.min(...numbers)
+const max = Math.max(...numbers)
+```
+
+This works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability.
diff --git a/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md b/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md
new file mode 100644
index 0000000..680a489
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md
@@ -0,0 +1,24 @@
+---
+title: Use Set/Map for O(1) Lookups
+impact: LOW-MEDIUM
+impactDescription: O(n) to O(1)
+tags: javascript, set, map, data-structures, performance
+---
+
+## Use Set/Map for O(1) Lookups
+
+Convert arrays to Set/Map for repeated membership checks.
+
+**Incorrect (O(n) per check):**
+
+```typescript
+const allowedIds = ['a', 'b', 'c', ...]
+items.filter(item => allowedIds.includes(item.id))
+```
+
+**Correct (O(1) per check):**
+
+```typescript
+const allowedIds = new Set(['a', 'b', 'c', ...])
+items.filter(item => allowedIds.has(item.id))
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md b/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md
new file mode 100644
index 0000000..eae8b3f
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md
@@ -0,0 +1,57 @@
+---
+title: Use toSorted() Instead of sort() for Immutability
+impact: MEDIUM-HIGH
+impactDescription: prevents mutation bugs in React state
+tags: javascript, arrays, immutability, react, state, mutation
+---
+
+## Use toSorted() Instead of sort() for Immutability
+
+`.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation.
+
+**Incorrect (mutates original array):**
+
+```typescript
+function UserList({ users }: { users: User[] }) {
+ // Mutates the users prop array!
+ const sorted = useMemo(
+ () => users.sort((a, b) => a.name.localeCompare(b.name)),
+ [users]
+ )
+ return {sorted.map(renderUser)}
+}
+```
+
+**Correct (creates new array):**
+
+```typescript
+function UserList({ users }: { users: User[] }) {
+ // Creates new sorted array, original unchanged
+ const sorted = useMemo(
+ () => users.toSorted((a, b) => a.name.localeCompare(b.name)),
+ [users]
+ )
+ return {sorted.map(renderUser)}
+}
+```
+
+**Why this matters in React:**
+
+1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only
+2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior
+
+**Browser support (fallback for older browsers):**
+
+`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:
+
+```typescript
+// Fallback for older browsers
+const sorted = [...items].sort((a, b) => a.value - b.value)
+```
+
+**Other immutable array methods:**
+
+- `.toSorted()` - immutable sort
+- `.toReversed()` - immutable reverse
+- `.toSpliced()` - immutable splice
+- `.with()` - immutable element replacement
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md b/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md
new file mode 100644
index 0000000..c957a49
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md
@@ -0,0 +1,26 @@
+---
+title: Use Activity Component for Show/Hide
+impact: MEDIUM
+impactDescription: preserves state/DOM
+tags: rendering, activity, visibility, state-preservation
+---
+
+## Use Activity Component for Show/Hide
+
+Use React's `` to preserve state/DOM for expensive components that frequently toggle visibility.
+
+**Usage:**
+
+```tsx
+import { Activity } from 'react'
+
+function Dropdown({ isOpen }: Props) {
+ return (
+
+
+
+ )
+}
+```
+
+Avoids expensive re-renders and state loss.
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md b/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md
new file mode 100644
index 0000000..646744c
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md
@@ -0,0 +1,47 @@
+---
+title: Animate SVG Wrapper Instead of SVG Element
+impact: LOW
+impactDescription: enables hardware acceleration
+tags: rendering, svg, css, animation, performance
+---
+
+## Animate SVG Wrapper Instead of SVG Element
+
+Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `` and animate the wrapper instead.
+
+**Incorrect (animating SVG directly - no hardware acceleration):**
+
+```tsx
+function LoadingSpinner() {
+ return (
+
+
+
+ )
+}
+```
+
+**Correct (animating wrapper div - hardware accelerated):**
+
+```tsx
+function LoadingSpinner() {
+ return (
+
+
+
+
+
+ )
+}
+```
+
+This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md b/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md
new file mode 100644
index 0000000..7e866f5
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md
@@ -0,0 +1,40 @@
+---
+title: Use Explicit Conditional Rendering
+impact: LOW
+impactDescription: prevents rendering 0 or NaN
+tags: rendering, conditional, jsx, falsy-values
+---
+
+## Use Explicit Conditional Rendering
+
+Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
+
+**Incorrect (renders "0" when count is 0):**
+
+```tsx
+function Badge({ count }: { count: number }) {
+ return (
+
+ {count && {count} }
+
+ )
+}
+
+// When count = 0, renders:
0
+// When count = 5, renders:
5
+```
+
+**Correct (renders nothing when count is 0):**
+
+```tsx
+function Badge({ count }: { count: number }) {
+ return (
+
+ {count > 0 ? {count} : null}
+
+ )
+}
+
+// When count = 0, renders:
+// When count = 5, renders:
5
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md b/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md
new file mode 100644
index 0000000..aa66563
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md
@@ -0,0 +1,38 @@
+---
+title: CSS content-visibility for Long Lists
+impact: HIGH
+impactDescription: faster initial render
+tags: rendering, css, content-visibility, long-lists
+---
+
+## CSS content-visibility for Long Lists
+
+Apply `content-visibility: auto` to defer off-screen rendering.
+
+**CSS:**
+
+```css
+.message-item {
+ content-visibility: auto;
+ contain-intrinsic-size: 0 80px;
+}
+```
+
+**Example:**
+
+```tsx
+function MessageList({ messages }: { messages: Message[] }) {
+ return (
+
+ {messages.map(msg => (
+
+ ))}
+
+ )
+}
+```
+
+For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md b/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md
new file mode 100644
index 0000000..32d2f3f
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md
@@ -0,0 +1,46 @@
+---
+title: Hoist Static JSX Elements
+impact: LOW
+impactDescription: avoids re-creation
+tags: rendering, jsx, static, optimization
+---
+
+## Hoist Static JSX Elements
+
+Extract static JSX outside components to avoid re-creation.
+
+**Incorrect (recreates element every render):**
+
+```tsx
+function LoadingSkeleton() {
+ return
+}
+
+function Container() {
+ return (
+
+ {loading && }
+
+ )
+}
+```
+
+**Correct (reuses same element):**
+
+```tsx
+const loadingSkeleton = (
+
+)
+
+function Container() {
+ return (
+
+ {loading && loadingSkeleton}
+
+ )
+}
+```
+
+This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
+
+**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md b/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md
new file mode 100644
index 0000000..5cf0e79
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md
@@ -0,0 +1,82 @@
+---
+title: Prevent Hydration Mismatch Without Flickering
+impact: MEDIUM
+impactDescription: avoids visual flicker and hydration errors
+tags: rendering, ssr, hydration, localStorage, flicker
+---
+
+## Prevent Hydration Mismatch Without Flickering
+
+When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.
+
+**Incorrect (breaks SSR):**
+
+```tsx
+function ThemeWrapper({ children }: { children: ReactNode }) {
+ // localStorage is not available on server - throws error
+ const theme = localStorage.getItem('theme') || 'light'
+
+ return (
+
+ {children}
+
+ )
+}
+```
+
+Server-side rendering will fail because `localStorage` is undefined.
+
+**Incorrect (visual flickering):**
+
+```tsx
+function ThemeWrapper({ children }: { children: ReactNode }) {
+ const [theme, setTheme] = useState('light')
+
+ useEffect(() => {
+ // Runs after hydration - causes visible flash
+ const stored = localStorage.getItem('theme')
+ if (stored) {
+ setTheme(stored)
+ }
+ }, [])
+
+ return (
+
+ {children}
+
+ )
+}
+```
+
+Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
+
+**Correct (no flicker, no hydration mismatch):**
+
+```tsx
+function ThemeWrapper({ children }: { children: ReactNode }) {
+ return (
+ <>
+
+ {children}
+
+
+ >
+ )
+}
+```
+
+The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
+
+This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md b/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md
new file mode 100644
index 0000000..24ba251
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md
@@ -0,0 +1,30 @@
+---
+title: Suppress Expected Hydration Mismatches
+impact: LOW-MEDIUM
+impactDescription: avoids noisy hydration warnings for known differences
+tags: rendering, hydration, ssr, nextjs
+---
+
+## Suppress Expected Hydration Mismatches
+
+In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these *expected* mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Don’t overuse it.
+
+**Incorrect (known mismatch warnings):**
+
+```tsx
+function Timestamp() {
+ return
{new Date().toLocaleString()}
+}
+```
+
+**Correct (suppress expected mismatch only):**
+
+```tsx
+function Timestamp() {
+ return (
+
+ {new Date().toLocaleString()}
+
+ )
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md b/.agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md
new file mode 100644
index 0000000..1290bef
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md
@@ -0,0 +1,85 @@
+---
+title: Use React DOM Resource Hints
+impact: HIGH
+impactDescription: reduces load time for critical resources
+tags: rendering, preload, preconnect, prefetch, resource-hints
+---
+
+## Use React DOM Resource Hints
+
+**Impact: HIGH (reduces load time for critical resources)**
+
+React DOM provides APIs to hint the browser about resources it will need. These are especially useful in server components to start loading resources before the client even receives the HTML.
+
+- **`prefetchDNS(href)`**: Resolve DNS for a domain you expect to connect to
+- **`preconnect(href)`**: Establish connection (DNS + TCP + TLS) to a server
+- **`preload(href, options)`**: Fetch a resource (stylesheet, font, script, image) you'll use soon
+- **`preloadModule(href)`**: Fetch an ES module you'll use soon
+- **`preinit(href, options)`**: Fetch and evaluate a stylesheet or script
+- **`preinitModule(href)`**: Fetch and evaluate an ES module
+
+**Example (preconnect to third-party APIs):**
+
+```tsx
+import { preconnect, prefetchDNS } from 'react-dom'
+
+export default function App() {
+ prefetchDNS('https://analytics.example.com')
+ preconnect('https://api.example.com')
+
+ return
{/* content */}
+}
+```
+
+**Example (preload critical fonts and styles):**
+
+```tsx
+import { preload, preinit } from 'react-dom'
+
+export default function RootLayout({ children }) {
+ // Preload font file
+ preload('/fonts/inter.woff2', { as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' })
+
+ // Fetch and apply critical stylesheet immediately
+ preinit('/styles/critical.css', { as: 'style' })
+
+ return (
+
+ {children}
+
+ )
+}
+```
+
+**Example (preload modules for code-split routes):**
+
+```tsx
+import { preloadModule, preinitModule } from 'react-dom'
+
+function Navigation() {
+ const preloadDashboard = () => {
+ preloadModule('/dashboard.js', { as: 'script' })
+ }
+
+ return (
+
+
+ Dashboard
+
+
+ )
+}
+```
+
+**When to use each:**
+
+| API | Use case |
+|-----|----------|
+| `prefetchDNS` | Third-party domains you'll connect to later |
+| `preconnect` | APIs or CDNs you'll fetch from immediately |
+| `preload` | Critical resources needed for current page |
+| `preloadModule` | JS modules for likely next navigation |
+| `preinit` | Stylesheets/scripts that must execute early |
+| `preinitModule` | ES modules that must execute early |
+
+Reference: [React DOM Resource Preloading APIs](https://react.dev/reference/react-dom#resource-preloading-apis)
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md b/.agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md
new file mode 100644
index 0000000..ee275ed
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md
@@ -0,0 +1,68 @@
+---
+title: Use defer or async on Script Tags
+impact: HIGH
+impactDescription: eliminates render-blocking
+tags: rendering, script, defer, async, performance
+---
+
+## Use defer or async on Script Tags
+
+**Impact: HIGH (eliminates render-blocking)**
+
+Script tags without `defer` or `async` block HTML parsing while the script downloads and executes. This delays First Contentful Paint and Time to Interactive.
+
+- **`defer`**: Downloads in parallel, executes after HTML parsing completes, maintains execution order
+- **`async`**: Downloads in parallel, executes immediately when ready, no guaranteed order
+
+Use `defer` for scripts that depend on DOM or other scripts. Use `async` for independent scripts like analytics.
+
+**Incorrect (blocks rendering):**
+
+```tsx
+export default function Document() {
+ return (
+
+
+
+
+
+ {/* content */}
+
+ )
+}
+```
+
+**Correct (non-blocking):**
+
+```tsx
+export default function Document() {
+ return (
+
+
+ {/* Independent script - use async */}
+
+ {/* DOM-dependent script - use defer */}
+
+
+ {/* content */}
+
+ )
+}
+```
+
+**Note:** In Next.js, prefer the `next/script` component with `strategy` prop instead of raw script tags:
+
+```tsx
+import Script from 'next/script'
+
+export default function Page() {
+ return (
+ <>
+
+
+ >
+ )
+}
+```
+
+Reference: [MDN - Script element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#defer)
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md b/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md
new file mode 100644
index 0000000..6d77128
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md
@@ -0,0 +1,28 @@
+---
+title: Optimize SVG Precision
+impact: LOW
+impactDescription: reduces file size
+tags: rendering, svg, optimization, svgo
+---
+
+## Optimize SVG Precision
+
+Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
+
+**Incorrect (excessive precision):**
+
+```svg
+
+```
+
+**Correct (1 decimal place):**
+
+```svg
+
+```
+
+**Automate with SVGO:**
+
+```bash
+npx svgo --precision=1 --multipass icon.svg
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md b/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md
new file mode 100644
index 0000000..0c1b0b9
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md
@@ -0,0 +1,75 @@
+---
+title: Use useTransition Over Manual Loading States
+impact: LOW
+impactDescription: reduces re-renders and improves code clarity
+tags: rendering, transitions, useTransition, loading, state
+---
+
+## Use useTransition Over Manual Loading States
+
+Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.
+
+**Incorrect (manual loading state):**
+
+```tsx
+function SearchResults() {
+ const [query, setQuery] = useState('')
+ const [results, setResults] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+
+ const handleSearch = async (value: string) => {
+ setIsLoading(true)
+ setQuery(value)
+ const data = await fetchResults(value)
+ setResults(data)
+ setIsLoading(false)
+ }
+
+ return (
+ <>
+
handleSearch(e.target.value)} />
+ {isLoading &&
}
+
+ >
+ )
+}
+```
+
+**Correct (useTransition with built-in pending state):**
+
+```tsx
+import { useTransition, useState } from 'react'
+
+function SearchResults() {
+ const [query, setQuery] = useState('')
+ const [results, setResults] = useState([])
+ const [isPending, startTransition] = useTransition()
+
+ const handleSearch = (value: string) => {
+ setQuery(value) // Update input immediately
+
+ startTransition(async () => {
+ // Fetch and update results
+ const data = await fetchResults(value)
+ setResults(data)
+ })
+ }
+
+ return (
+ <>
+
handleSearch(e.target.value)} />
+ {isPending &&
}
+
+ >
+ )
+}
+```
+
+**Benefits:**
+
+- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`
+- **Error resilience**: Pending state correctly resets even if the transition throws
+- **Better responsiveness**: Keeps the UI responsive during updates
+- **Interrupt handling**: New transitions automatically cancel pending ones
+
+Reference: [useTransition](https://react.dev/reference/react/useTransition)
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md b/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md
new file mode 100644
index 0000000..e867c95
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md
@@ -0,0 +1,39 @@
+---
+title: Defer State Reads to Usage Point
+impact: MEDIUM
+impactDescription: avoids unnecessary subscriptions
+tags: rerender, searchParams, localStorage, optimization
+---
+
+## Defer State Reads to Usage Point
+
+Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
+
+**Incorrect (subscribes to all searchParams changes):**
+
+```tsx
+function ShareButton({ chatId }: { chatId: string }) {
+ const searchParams = useSearchParams()
+
+ const handleShare = () => {
+ const ref = searchParams.get('ref')
+ shareChat(chatId, { ref })
+ }
+
+ return
Share
+}
+```
+
+**Correct (reads on demand, no subscription):**
+
+```tsx
+function ShareButton({ chatId }: { chatId: string }) {
+ const handleShare = () => {
+ const params = new URLSearchParams(window.location.search)
+ const ref = params.get('ref')
+ shareChat(chatId, { ref })
+ }
+
+ return
Share
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md b/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md
new file mode 100644
index 0000000..47a4d92
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md
@@ -0,0 +1,45 @@
+---
+title: Narrow Effect Dependencies
+impact: LOW
+impactDescription: minimizes effect re-runs
+tags: rerender, useEffect, dependencies, optimization
+---
+
+## Narrow Effect Dependencies
+
+Specify primitive dependencies instead of objects to minimize effect re-runs.
+
+**Incorrect (re-runs on any user field change):**
+
+```tsx
+useEffect(() => {
+ console.log(user.id)
+}, [user])
+```
+
+**Correct (re-runs only when id changes):**
+
+```tsx
+useEffect(() => {
+ console.log(user.id)
+}, [user.id])
+```
+
+**For derived state, compute outside effect:**
+
+```tsx
+// Incorrect: runs on width=767, 766, 765...
+useEffect(() => {
+ if (width < 768) {
+ enableMobileMode()
+ }
+}, [width])
+
+// Correct: runs only on boolean transition
+const isMobile = width < 768
+useEffect(() => {
+ if (isMobile) {
+ enableMobileMode()
+ }
+}, [isMobile])
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md b/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md
new file mode 100644
index 0000000..3d9fe40
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md
@@ -0,0 +1,40 @@
+---
+title: Calculate Derived State During Rendering
+impact: MEDIUM
+impactDescription: avoids redundant renders and state drift
+tags: rerender, derived-state, useEffect, state
+---
+
+## Calculate Derived State During Rendering
+
+If a value can be computed from current props/state, do not store it in state or update it in an effect. Derive it during render to avoid extra renders and state drift. Do not set state in effects solely in response to prop changes; prefer derived values or keyed resets instead.
+
+**Incorrect (redundant state and effect):**
+
+```tsx
+function Form() {
+ const [firstName, setFirstName] = useState('First')
+ const [lastName, setLastName] = useState('Last')
+ const [fullName, setFullName] = useState('')
+
+ useEffect(() => {
+ setFullName(firstName + ' ' + lastName)
+ }, [firstName, lastName])
+
+ return
{fullName}
+}
+```
+
+**Correct (derive during render):**
+
+```tsx
+function Form() {
+ const [firstName, setFirstName] = useState('First')
+ const [lastName, setLastName] = useState('Last')
+ const fullName = firstName + ' ' + lastName
+
+ return
{fullName}
+}
+```
+
+References: [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md b/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md
new file mode 100644
index 0000000..e5c899f
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md
@@ -0,0 +1,29 @@
+---
+title: Subscribe to Derived State
+impact: MEDIUM
+impactDescription: reduces re-render frequency
+tags: rerender, derived-state, media-query, optimization
+---
+
+## Subscribe to Derived State
+
+Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
+
+**Incorrect (re-renders on every pixel change):**
+
+```tsx
+function Sidebar() {
+ const width = useWindowWidth() // updates continuously
+ const isMobile = width < 768
+ return
+}
+```
+
+**Correct (re-renders only when boolean changes):**
+
+```tsx
+function Sidebar() {
+ const isMobile = useMediaQuery('(max-width: 767px)')
+ return
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md b/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md
new file mode 100644
index 0000000..b004ef4
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md
@@ -0,0 +1,74 @@
+---
+title: Use Functional setState Updates
+impact: MEDIUM
+impactDescription: prevents stale closures and unnecessary callback recreations
+tags: react, hooks, useState, useCallback, callbacks, closures
+---
+
+## Use Functional setState Updates
+
+When updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.
+
+**Incorrect (requires state as dependency):**
+
+```tsx
+function TodoList() {
+ const [items, setItems] = useState(initialItems)
+
+ // Callback must depend on items, recreated on every items change
+ const addItems = useCallback((newItems: Item[]) => {
+ setItems([...items, ...newItems])
+ }, [items]) // ❌ items dependency causes recreations
+
+ // Risk of stale closure if dependency is forgotten
+ const removeItem = useCallback((id: string) => {
+ setItems(items.filter(item => item.id !== id))
+ }, []) // ❌ Missing items dependency - will use stale items!
+
+ return
+}
+```
+
+The first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.
+
+**Correct (stable callbacks, no stale closures):**
+
+```tsx
+function TodoList() {
+ const [items, setItems] = useState(initialItems)
+
+ // Stable callback, never recreated
+ const addItems = useCallback((newItems: Item[]) => {
+ setItems(curr => [...curr, ...newItems])
+ }, []) // ✅ No dependencies needed
+
+ // Always uses latest state, no stale closure risk
+ const removeItem = useCallback((id: string) => {
+ setItems(curr => curr.filter(item => item.id !== id))
+ }, []) // ✅ Safe and stable
+
+ return
+}
+```
+
+**Benefits:**
+
+1. **Stable callback references** - Callbacks don't need to be recreated when state changes
+2. **No stale closures** - Always operates on the latest state value
+3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
+4. **Prevents bugs** - Eliminates the most common source of React closure bugs
+
+**When to use functional updates:**
+
+- Any setState that depends on the current state value
+- Inside useCallback/useMemo when state is needed
+- Event handlers that reference state
+- Async operations that update state
+
+**When direct updates are fine:**
+
+- Setting state to a static value: `setCount(0)`
+- Setting state from props/arguments only: `setName(newName)`
+- State doesn't depend on previous value
+
+**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md b/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md
new file mode 100644
index 0000000..4ecb350
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md
@@ -0,0 +1,58 @@
+---
+title: Use Lazy State Initialization
+impact: MEDIUM
+impactDescription: wasted computation on every render
+tags: react, hooks, useState, performance, initialization
+---
+
+## Use Lazy State Initialization
+
+Pass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.
+
+**Incorrect (runs on every render):**
+
+```tsx
+function FilteredList({ items }: { items: Item[] }) {
+ // buildSearchIndex() runs on EVERY render, even after initialization
+ const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
+ const [query, setQuery] = useState('')
+
+ // When query changes, buildSearchIndex runs again unnecessarily
+ return
+}
+
+function UserProfile() {
+ // JSON.parse runs on every render
+ const [settings, setSettings] = useState(
+ JSON.parse(localStorage.getItem('settings') || '{}')
+ )
+
+ return
+}
+```
+
+**Correct (runs only once):**
+
+```tsx
+function FilteredList({ items }: { items: Item[] }) {
+ // buildSearchIndex() runs ONLY on initial render
+ const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
+ const [query, setQuery] = useState('')
+
+ return
+}
+
+function UserProfile() {
+ // JSON.parse runs only on initial render
+ const [settings, setSettings] = useState(() => {
+ const stored = localStorage.getItem('settings')
+ return stored ? JSON.parse(stored) : {}
+ })
+
+ return
+}
+```
+
+Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
+
+For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md b/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md
new file mode 100644
index 0000000..6357049
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md
@@ -0,0 +1,38 @@
+---
+
+title: Extract Default Non-primitive Parameter Value from Memoized Component to Constant
+impact: MEDIUM
+impactDescription: restores memoization by using a constant for default value
+tags: rerender, memo, optimization
+
+---
+
+## Extract Default Non-primitive Parameter Value from Memoized Component to Constant
+
+When memoized component has a default value for some non-primitive optional parameter, such as an array, function, or object, calling the component without that parameter results in broken memoization. This is because new value instances are created on every rerender, and they do not pass strict equality comparison in `memo()`.
+
+To address this issue, extract the default value into a constant.
+
+**Incorrect (`onClick` has different values on every rerender):**
+
+```tsx
+const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
+ // ...
+})
+
+// Used without optional onClick
+
+```
+
+**Correct (stable default value):**
+
+```tsx
+const NOOP = () => {};
+
+const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
+ // ...
+})
+
+// Used without optional onClick
+
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md b/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md
new file mode 100644
index 0000000..f8982ab
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md
@@ -0,0 +1,44 @@
+---
+title: Extract to Memoized Components
+impact: MEDIUM
+impactDescription: enables early returns
+tags: rerender, memo, useMemo, optimization
+---
+
+## Extract to Memoized Components
+
+Extract expensive work into memoized components to enable early returns before computation.
+
+**Incorrect (computes avatar even when loading):**
+
+```tsx
+function Profile({ user, loading }: Props) {
+ const avatar = useMemo(() => {
+ const id = computeAvatarId(user)
+ return
+ }, [user])
+
+ if (loading) return
+ return
{avatar}
+}
+```
+
+**Correct (skips computation when loading):**
+
+```tsx
+const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
+ const id = useMemo(() => computeAvatarId(user), [user])
+ return
+})
+
+function Profile({ user, loading }: Props) {
+ if (loading) return
+ return (
+
+
+
+ )
+}
+```
+
+**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md b/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md
new file mode 100644
index 0000000..dd58a1a
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md
@@ -0,0 +1,45 @@
+---
+title: Put Interaction Logic in Event Handlers
+impact: MEDIUM
+impactDescription: avoids effect re-runs and duplicate side effects
+tags: rerender, useEffect, events, side-effects, dependencies
+---
+
+## Put Interaction Logic in Event Handlers
+
+If a side effect is triggered by a specific user action (submit, click, drag), run it in that event handler. Do not model the action as state + effect; it makes effects re-run on unrelated changes and can duplicate the action.
+
+**Incorrect (event modeled as state + effect):**
+
+```tsx
+function Form() {
+ const [submitted, setSubmitted] = useState(false)
+ const theme = useContext(ThemeContext)
+
+ useEffect(() => {
+ if (submitted) {
+ post('/api/register')
+ showToast('Registered', theme)
+ }
+ }, [submitted, theme])
+
+ return
setSubmitted(true)}>Submit
+}
+```
+
+**Correct (do it in the handler):**
+
+```tsx
+function Form() {
+ const theme = useContext(ThemeContext)
+
+ function handleSubmit() {
+ post('/api/register')
+ showToast('Registered', theme)
+ }
+
+ return
Submit
+}
+```
+
+Reference: [Should this code move to an event handler?](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md b/.agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md
new file mode 100644
index 0000000..d97592a
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md
@@ -0,0 +1,82 @@
+---
+title: Don't Define Components Inside Components
+impact: HIGH
+impactDescription: prevents remount on every render
+tags: rerender, components, remount, performance
+---
+
+## Don't Define Components Inside Components
+
+**Impact: HIGH (prevents remount on every render)**
+
+Defining a component inside another component creates a new component type on every render. React sees a different component each time and fully remounts it, destroying all state and DOM.
+
+A common reason developers do this is to access parent variables without passing props. Always pass props instead.
+
+**Incorrect (remounts on every render):**
+
+```tsx
+function UserProfile({ user, theme }) {
+ // Defined inside to access `theme` - BAD
+ const Avatar = () => (
+
+ )
+
+ // Defined inside to access `user` - BAD
+ const Stats = () => (
+
+ {user.followers} followers
+ {user.posts} posts
+
+ )
+
+ return (
+
+ )
+}
+```
+
+Every time `UserProfile` renders, `Avatar` and `Stats` are new component types. React unmounts the old instances and mounts new ones, losing any internal state, running effects again, and recreating DOM nodes.
+
+**Correct (pass props instead):**
+
+```tsx
+function Avatar({ src, theme }: { src: string; theme: string }) {
+ return (
+
+ )
+}
+
+function Stats({ followers, posts }: { followers: number; posts: number }) {
+ return (
+
+ {followers} followers
+ {posts} posts
+
+ )
+}
+
+function UserProfile({ user, theme }) {
+ return (
+
+ )
+}
+```
+
+**Symptoms of this bug:**
+- Input fields lose focus on every keystroke
+- Animations restart unexpectedly
+- `useEffect` cleanup/setup runs on every parent render
+- Scroll position resets inside the component
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md b/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md
new file mode 100644
index 0000000..59dfab0
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md
@@ -0,0 +1,35 @@
+---
+title: Do not wrap a simple expression with a primitive result type in useMemo
+impact: LOW-MEDIUM
+impactDescription: wasted computation on every render
+tags: rerender, useMemo, optimization
+---
+
+## Do not wrap a simple expression with a primitive result type in useMemo
+
+When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.
+Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.
+
+**Incorrect:**
+
+```tsx
+function Header({ user, notifications }: Props) {
+ const isLoading = useMemo(() => {
+ return user.isLoading || notifications.isLoading
+ }, [user.isLoading, notifications.isLoading])
+
+ if (isLoading) return
+ // return some markup
+}
+```
+
+**Correct:**
+
+```tsx
+function Header({ user, notifications }: Props) {
+ const isLoading = user.isLoading || notifications.isLoading
+
+ if (isLoading) return
+ // return some markup
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md b/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md
new file mode 100644
index 0000000..d99f43f
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md
@@ -0,0 +1,40 @@
+---
+title: Use Transitions for Non-Urgent Updates
+impact: MEDIUM
+impactDescription: maintains UI responsiveness
+tags: rerender, transitions, startTransition, performance
+---
+
+## Use Transitions for Non-Urgent Updates
+
+Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
+
+**Incorrect (blocks UI on every scroll):**
+
+```tsx
+function ScrollTracker() {
+ const [scrollY, setScrollY] = useState(0)
+ useEffect(() => {
+ const handler = () => setScrollY(window.scrollY)
+ window.addEventListener('scroll', handler, { passive: true })
+ return () => window.removeEventListener('scroll', handler)
+ }, [])
+}
+```
+
+**Correct (non-blocking updates):**
+
+```tsx
+import { startTransition } from 'react'
+
+function ScrollTracker() {
+ const [scrollY, setScrollY] = useState(0)
+ useEffect(() => {
+ const handler = () => {
+ startTransition(() => setScrollY(window.scrollY))
+ }
+ window.addEventListener('scroll', handler, { passive: true })
+ return () => window.removeEventListener('scroll', handler)
+ }, [])
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md b/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md
new file mode 100644
index 0000000..cf04b81
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md
@@ -0,0 +1,73 @@
+---
+title: Use useRef for Transient Values
+impact: MEDIUM
+impactDescription: avoids unnecessary re-renders on frequent updates
+tags: rerender, useref, state, performance
+---
+
+## Use useRef for Transient Values
+
+When a value changes frequently and you don't want a re-render on every update (e.g., mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`. Keep component state for UI; use refs for temporary DOM-adjacent values. Updating a ref does not trigger a re-render.
+
+**Incorrect (renders every update):**
+
+```tsx
+function Tracker() {
+ const [lastX, setLastX] = useState(0)
+
+ useEffect(() => {
+ const onMove = (e: MouseEvent) => setLastX(e.clientX)
+ window.addEventListener('mousemove', onMove)
+ return () => window.removeEventListener('mousemove', onMove)
+ }, [])
+
+ return (
+
+ )
+}
+```
+
+**Correct (no re-render for tracking):**
+
+```tsx
+function Tracker() {
+ const lastXRef = useRef(0)
+ const dotRef = useRef
(null)
+
+ useEffect(() => {
+ const onMove = (e: MouseEvent) => {
+ lastXRef.current = e.clientX
+ const node = dotRef.current
+ if (node) {
+ node.style.transform = `translateX(${e.clientX}px)`
+ }
+ }
+ window.addEventListener('mousemove', onMove)
+ return () => window.removeEventListener('mousemove', onMove)
+ }, [])
+
+ return (
+
+ )
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md b/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md
new file mode 100644
index 0000000..e8f5b26
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md
@@ -0,0 +1,73 @@
+---
+title: Use after() for Non-Blocking Operations
+impact: MEDIUM
+impactDescription: faster response times
+tags: server, async, logging, analytics, side-effects
+---
+
+## Use after() for Non-Blocking Operations
+
+Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response.
+
+**Incorrect (blocks response):**
+
+```tsx
+import { logUserAction } from '@/app/utils'
+
+export async function POST(request: Request) {
+ // Perform mutation
+ await updateDatabase(request)
+
+ // Logging blocks the response
+ const userAgent = request.headers.get('user-agent') || 'unknown'
+ await logUserAction({ userAgent })
+
+ return new Response(JSON.stringify({ status: 'success' }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ })
+}
+```
+
+**Correct (non-blocking):**
+
+```tsx
+import { after } from 'next/server'
+import { headers, cookies } from 'next/headers'
+import { logUserAction } from '@/app/utils'
+
+export async function POST(request: Request) {
+ // Perform mutation
+ await updateDatabase(request)
+
+ // Log after response is sent
+ after(async () => {
+ const userAgent = (await headers()).get('user-agent') || 'unknown'
+ const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
+
+ logUserAction({ sessionCookie, userAgent })
+ })
+
+ return new Response(JSON.stringify({ status: 'success' }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ })
+}
+```
+
+The response is sent immediately while logging happens in the background.
+
+**Common use cases:**
+
+- Analytics tracking
+- Audit logging
+- Sending notifications
+- Cache invalidation
+- Cleanup tasks
+
+**Important notes:**
+
+- `after()` runs even if the response fails or redirects
+- Works in Server Actions, Route Handlers, and Server Components
+
+Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md b/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md
new file mode 100644
index 0000000..ee82c04
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md
@@ -0,0 +1,96 @@
+---
+title: Authenticate Server Actions Like API Routes
+impact: CRITICAL
+impactDescription: prevents unauthorized access to server mutations
+tags: server, server-actions, authentication, security, authorization
+---
+
+## Authenticate Server Actions Like API Routes
+
+**Impact: CRITICAL (prevents unauthorized access to server mutations)**
+
+Server Actions (functions with `"use server"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly.
+
+Next.js documentation explicitly states: "Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation."
+
+**Incorrect (no authentication check):**
+
+```typescript
+'use server'
+
+export async function deleteUser(userId: string) {
+ // Anyone can call this! No auth check
+ await db.user.delete({ where: { id: userId } })
+ return { success: true }
+}
+```
+
+**Correct (authentication inside the action):**
+
+```typescript
+'use server'
+
+import { verifySession } from '@/lib/auth'
+import { unauthorized } from '@/lib/errors'
+
+export async function deleteUser(userId: string) {
+ // Always check auth inside the action
+ const session = await verifySession()
+
+ if (!session) {
+ throw unauthorized('Must be logged in')
+ }
+
+ // Check authorization too
+ if (session.user.role !== 'admin' && session.user.id !== userId) {
+ throw unauthorized('Cannot delete other users')
+ }
+
+ await db.user.delete({ where: { id: userId } })
+ return { success: true }
+}
+```
+
+**With input validation:**
+
+```typescript
+'use server'
+
+import { verifySession } from '@/lib/auth'
+import { z } from 'zod'
+
+const updateProfileSchema = z.object({
+ userId: z.string().uuid(),
+ name: z.string().min(1).max(100),
+ email: z.string().email()
+})
+
+export async function updateProfile(data: unknown) {
+ // Validate input first
+ const validated = updateProfileSchema.parse(data)
+
+ // Then authenticate
+ const session = await verifySession()
+ if (!session) {
+ throw new Error('Unauthorized')
+ }
+
+ // Then authorize
+ if (session.user.id !== validated.userId) {
+ throw new Error('Can only update own profile')
+ }
+
+ // Finally perform the mutation
+ await db.user.update({
+ where: { id: validated.userId },
+ data: {
+ name: validated.name,
+ email: validated.email
+ }
+ })
+
+ return { success: true }
+}
+```
+
+Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md b/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md
new file mode 100644
index 0000000..ef6938a
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md
@@ -0,0 +1,41 @@
+---
+title: Cross-Request LRU Caching
+impact: HIGH
+impactDescription: caches across requests
+tags: server, cache, lru, cross-request
+---
+
+## Cross-Request LRU Caching
+
+`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
+
+**Implementation:**
+
+```typescript
+import { LRUCache } from 'lru-cache'
+
+const cache = new LRUCache({
+ max: 1000,
+ ttl: 5 * 60 * 1000 // 5 minutes
+})
+
+export async function getUser(id: string) {
+ const cached = cache.get(id)
+ if (cached) return cached
+
+ const user = await db.user.findUnique({ where: { id } })
+ cache.set(id, user)
+ return user
+}
+
+// Request 1: DB query, result cached
+// Request 2: cache hit, no DB query
+```
+
+Use when sequential user actions hit multiple endpoints needing the same data within seconds.
+
+**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis.
+
+**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
+
+Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md b/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md
new file mode 100644
index 0000000..87c9ca3
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md
@@ -0,0 +1,76 @@
+---
+title: Per-Request Deduplication with React.cache()
+impact: MEDIUM
+impactDescription: deduplicates within request
+tags: server, cache, react-cache, deduplication
+---
+
+## Per-Request Deduplication with React.cache()
+
+Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
+
+**Usage:**
+
+```typescript
+import { cache } from 'react'
+
+export const getCurrentUser = cache(async () => {
+ const session = await auth()
+ if (!session?.user?.id) return null
+ return await db.user.findUnique({
+ where: { id: session.user.id }
+ })
+})
+```
+
+Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
+
+**Avoid inline objects as arguments:**
+
+`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.
+
+**Incorrect (always cache miss):**
+
+```typescript
+const getUser = cache(async (params: { uid: number }) => {
+ return await db.user.findUnique({ where: { id: params.uid } })
+})
+
+// Each call creates new object, never hits cache
+getUser({ uid: 1 })
+getUser({ uid: 1 }) // Cache miss, runs query again
+```
+
+**Correct (cache hit):**
+
+```typescript
+const getUser = cache(async (uid: number) => {
+ return await db.user.findUnique({ where: { id: uid } })
+})
+
+// Primitive args use value equality
+getUser(1)
+getUser(1) // Cache hit, returns cached result
+```
+
+If you must pass objects, pass the same reference:
+
+```typescript
+const params = { uid: 1 }
+getUser(params) // Query runs
+getUser(params) // Cache hit (same reference)
+```
+
+**Next.js-Specific Note:**
+
+In Next.js, the `fetch` API is automatically extended with request memoization. Requests with the same URL and options are automatically deduplicated within a single request, so you don't need `React.cache()` for `fetch` calls. However, `React.cache()` is still essential for other async tasks:
+
+- Database queries (Prisma, Drizzle, etc.)
+- Heavy computations
+- Authentication checks
+- File system operations
+- Any non-fetch async work
+
+Use `React.cache()` to deduplicate these operations across your component tree.
+
+Reference: [React.cache documentation](https://react.dev/reference/react/cache)
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md b/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md
new file mode 100644
index 0000000..fb24a25
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md
@@ -0,0 +1,65 @@
+---
+title: Avoid Duplicate Serialization in RSC Props
+impact: LOW
+impactDescription: reduces network payload by avoiding duplicate serialization
+tags: server, rsc, serialization, props, client-components
+---
+
+## Avoid Duplicate Serialization in RSC Props
+
+**Impact: LOW (reduces network payload by avoiding duplicate serialization)**
+
+RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server.
+
+**Incorrect (duplicates array):**
+
+```tsx
+// RSC: sends 6 strings (2 arrays × 3 items)
+
+```
+
+**Correct (sends 3 strings):**
+
+```tsx
+// RSC: send once
+
+
+// Client: transform there
+'use client'
+const sorted = useMemo(() => [...usernames].sort(), [usernames])
+```
+
+**Nested deduplication behavior:**
+
+Deduplication works recursively. Impact varies by data type:
+
+- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated
+- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference
+
+```tsx
+// string[] - duplicates everything
+usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
+
+// object[] - duplicates array structure only
+users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)
+```
+
+**Operations breaking deduplication (create new references):**
+
+- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`
+- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`
+
+**More examples:**
+
+```tsx
+// ❌ Bad
+ u.active)} />
+
+
+// ✅ Good
+
+
+// Do filtering/destructuring in client
+```
+
+**Exception:** Pass derived data when transformation is expensive or client doesn't need original.
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md b/.agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md
new file mode 100644
index 0000000..5b642b6
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md
@@ -0,0 +1,142 @@
+---
+title: Hoist Static I/O to Module Level
+impact: HIGH
+impactDescription: avoids repeated file/network I/O per request
+tags: server, io, performance, next.js, route-handlers, og-image
+---
+
+## Hoist Static I/O to Module Level
+
+**Impact: HIGH (avoids repeated file/network I/O per request)**
+
+When loading static assets (fonts, logos, images, config files) in route handlers or server functions, hoist the I/O operation to module level. Module-level code runs once when the module is first imported, not on every request. This eliminates redundant file system reads or network fetches that would otherwise run on every invocation.
+
+**Incorrect: reads font file on every request**
+
+```typescript
+// app/api/og/route.tsx
+import { ImageResponse } from 'next/og'
+
+export async function GET(request: Request) {
+ // Runs on EVERY request - expensive!
+ const fontData = await fetch(
+ new URL('./fonts/Inter.ttf', import.meta.url)
+ ).then(res => res.arrayBuffer())
+
+ const logoData = await fetch(
+ new URL('./images/logo.png', import.meta.url)
+ ).then(res => res.arrayBuffer())
+
+ return new ImageResponse(
+
+
+ Hello World
+
,
+ { fonts: [{ name: 'Inter', data: fontData }] }
+ )
+}
+```
+
+**Correct: loads once at module initialization**
+
+```typescript
+// app/api/og/route.tsx
+import { ImageResponse } from 'next/og'
+
+// Module-level: runs ONCE when module is first imported
+const fontData = fetch(
+ new URL('./fonts/Inter.ttf', import.meta.url)
+).then(res => res.arrayBuffer())
+
+const logoData = fetch(
+ new URL('./images/logo.png', import.meta.url)
+).then(res => res.arrayBuffer())
+
+export async function GET(request: Request) {
+ // Await the already-started promises
+ const [font, logo] = await Promise.all([fontData, logoData])
+
+ return new ImageResponse(
+
+
+ Hello World
+
,
+ { fonts: [{ name: 'Inter', data: font }] }
+ )
+}
+```
+
+**Alternative: synchronous file reads with Node.js fs**
+
+```typescript
+// app/api/og/route.tsx
+import { ImageResponse } from 'next/og'
+import { readFileSync } from 'fs'
+import { join } from 'path'
+
+// Synchronous read at module level - blocks only during module init
+const fontData = readFileSync(
+ join(process.cwd(), 'public/fonts/Inter.ttf')
+)
+
+const logoData = readFileSync(
+ join(process.cwd(), 'public/images/logo.png')
+)
+
+export async function GET(request: Request) {
+ return new ImageResponse(
+
+
+ Hello World
+
,
+ { fonts: [{ name: 'Inter', data: fontData }] }
+ )
+}
+```
+
+**General Node.js example: loading config or templates**
+
+```typescript
+// Incorrect: reads config on every call
+export async function processRequest(data: Data) {
+ const config = JSON.parse(
+ await fs.readFile('./config.json', 'utf-8')
+ )
+ const template = await fs.readFile('./template.html', 'utf-8')
+
+ return render(template, data, config)
+}
+
+// Correct: loads once at module level
+const configPromise = fs.readFile('./config.json', 'utf-8')
+ .then(JSON.parse)
+const templatePromise = fs.readFile('./template.html', 'utf-8')
+
+export async function processRequest(data: Data) {
+ const [config, template] = await Promise.all([
+ configPromise,
+ templatePromise
+ ])
+
+ return render(template, data, config)
+}
+```
+
+**When to use this pattern:**
+
+- Loading fonts for OG image generation
+- Loading static logos, icons, or watermarks
+- Reading configuration files that don't change at runtime
+- Loading email templates or other static templates
+- Any static asset that's the same across all requests
+
+**When NOT to use this pattern:**
+
+- Assets that vary per request or user
+- Files that may change during runtime (use caching with TTL instead)
+- Large files that would consume too much memory if kept loaded
+- Sensitive data that shouldn't persist in memory
+
+**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** Module-level caching is especially effective because multiple concurrent requests share the same function instance. The static assets stay loaded in memory across requests without cold start penalties.
+
+**In traditional serverless:** Each cold start re-executes module-level code, but subsequent warm invocations reuse the loaded assets until the instance is recycled.
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md b/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md
new file mode 100644
index 0000000..1affc83
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md
@@ -0,0 +1,83 @@
+---
+title: Parallel Data Fetching with Component Composition
+impact: CRITICAL
+impactDescription: eliminates server-side waterfalls
+tags: server, rsc, parallel-fetching, composition
+---
+
+## Parallel Data Fetching with Component Composition
+
+React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
+
+**Incorrect (Sidebar waits for Page's fetch to complete):**
+
+```tsx
+export default async function Page() {
+ const header = await fetchHeader()
+ return (
+
+ )
+}
+
+async function Sidebar() {
+ const items = await fetchSidebarItems()
+ return {items.map(renderItem)}
+}
+```
+
+**Correct (both fetch simultaneously):**
+
+```tsx
+async function Header() {
+ const data = await fetchHeader()
+ return {data}
+}
+
+async function Sidebar() {
+ const items = await fetchSidebarItems()
+ return {items.map(renderItem)}
+}
+
+export default function Page() {
+ return (
+
+
+
+
+ )
+}
+```
+
+**Alternative with children prop:**
+
+```tsx
+async function Header() {
+ const data = await fetchHeader()
+ return {data}
+}
+
+async function Sidebar() {
+ const items = await fetchSidebarItems()
+ return {items.map(renderItem)}
+}
+
+function Layout({ children }: { children: ReactNode }) {
+ return (
+
+
+ {children}
+
+ )
+}
+
+export default function Page() {
+ return (
+
+
+
+ )
+}
+```
diff --git a/.agents/skills/vercel-react-best-practices/rules/server-serialization.md b/.agents/skills/vercel-react-best-practices/rules/server-serialization.md
new file mode 100644
index 0000000..39c5c41
--- /dev/null
+++ b/.agents/skills/vercel-react-best-practices/rules/server-serialization.md
@@ -0,0 +1,38 @@
+---
+title: Minimize Serialization at RSC Boundaries
+impact: HIGH
+impactDescription: reduces data transfer size
+tags: server, rsc, serialization, props
+---
+
+## Minimize Serialization at RSC Boundaries
+
+The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses.
+
+**Incorrect (serializes all 50 fields):**
+
+```tsx
+async function Page() {
+ const user = await fetchUser() // 50 fields
+ return
+}
+
+'use client'
+function Profile({ user }: { user: User }) {
+ return {user.name}
// uses 1 field
+}
+```
+
+**Correct (serializes only 1 field):**
+
+```tsx
+async function Page() {
+ const user = await fetchUser()
+ return
+}
+
+'use client'
+function Profile({ name }: { name: string }) {
+ return {name}
+}
+```
diff --git a/.claude/skills/astro b/.claude/skills/astro
new file mode 120000
index 0000000..7b7fd9a
--- /dev/null
+++ b/.claude/skills/astro
@@ -0,0 +1 @@
+../../.agents/skills/astro
\ No newline at end of file
diff --git a/.claude/skills/framer-motion b/.claude/skills/framer-motion
new file mode 120000
index 0000000..1d98d85
--- /dev/null
+++ b/.claude/skills/framer-motion
@@ -0,0 +1 @@
+../../.agents/skills/framer-motion
\ No newline at end of file
diff --git a/.claude/skills/shadcn b/.claude/skills/shadcn
new file mode 120000
index 0000000..8d5af6f
--- /dev/null
+++ b/.claude/skills/shadcn
@@ -0,0 +1 @@
+../../.agents/skills/shadcn
\ No newline at end of file
diff --git a/.claude/skills/tailwindcss b/.claude/skills/tailwindcss
new file mode 120000
index 0000000..613ab54
--- /dev/null
+++ b/.claude/skills/tailwindcss
@@ -0,0 +1 @@
+../../.agents/skills/tailwindcss
\ No newline at end of file
diff --git a/.claude/skills/tailwindcss-advanced-layouts b/.claude/skills/tailwindcss-advanced-layouts
new file mode 120000
index 0000000..7112a43
--- /dev/null
+++ b/.claude/skills/tailwindcss-advanced-layouts
@@ -0,0 +1 @@
+../../.agents/skills/tailwindcss-advanced-layouts
\ No newline at end of file
diff --git a/.claude/skills/vercel-react-best-practices b/.claude/skills/vercel-react-best-practices
new file mode 120000
index 0000000..e567923
--- /dev/null
+++ b/.claude/skills/vercel-react-best-practices
@@ -0,0 +1 @@
+../../.agents/skills/vercel-react-best-practices
\ No newline at end of file
diff --git a/.codex/skills/openspec-apply-change/SKILL.md b/.codex/skills/openspec-apply-change/SKILL.md
new file mode 100644
index 0000000..d474dc1
--- /dev/null
+++ b/.codex/skills/openspec-apply-change/SKILL.md
@@ -0,0 +1,156 @@
+---
+name: openspec-apply-change
+description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.
+license: MIT
+compatibility: Requires openspec CLI.
+metadata:
+ author: openspec
+ version: "1.0"
+ generatedBy: "1.2.0"
+---
+
+Implement tasks from an OpenSpec change.
+
+**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
+
+**Steps**
+
+1. **Select the change**
+
+ If a name is provided, use it. Otherwise:
+ - Infer from conversation context if the user mentioned a change
+ - Auto-select if only one active change exists
+ - If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
+
+ Always announce: "Using change: " and how to override (e.g., `/opsx:apply `).
+
+2. **Check status to understand the schema**
+ ```bash
+ openspec status --change "" --json
+ ```
+ Parse the JSON to understand:
+ - `schemaName`: The workflow being used (e.g., "spec-driven")
+ - Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
+
+3. **Get apply instructions**
+
+ ```bash
+ openspec instructions apply --change "" --json
+ ```
+
+ This returns:
+ - Context file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
+ - Progress (total, complete, remaining)
+ - Task list with status
+ - Dynamic instruction based on current state
+
+ **Handle states:**
+ - If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change
+ - If `state: "all_done"`: congratulate, suggest archive
+ - Otherwise: proceed to implementation
+
+4. **Read context files**
+
+ Read the files listed in `contextFiles` from the apply instructions output.
+ The files depend on the schema being used:
+ - **spec-driven**: proposal, specs, design, tasks
+ - Other schemas: follow the contextFiles from CLI output
+
+5. **Show current progress**
+
+ Display:
+ - Schema being used
+ - Progress: "N/M tasks complete"
+ - Remaining tasks overview
+ - Dynamic instruction from CLI
+
+6. **Implement tasks (loop until done or blocked)**
+
+ For each pending task:
+ - Show which task is being worked on
+ - Make the code changes required
+ - Keep changes minimal and focused
+ - Mark task complete in the tasks file: `- [ ]` → `- [x]`
+ - Continue to next task
+
+ **Pause if:**
+ - Task is unclear → ask for clarification
+ - Implementation reveals a design issue → suggest updating artifacts
+ - Error or blocker encountered → report and wait for guidance
+ - User interrupts
+
+7. **On completion or pause, show status**
+
+ Display:
+ - Tasks completed this session
+ - Overall progress: "N/M tasks complete"
+ - If all done: suggest archive
+ - If paused: explain why and wait for guidance
+
+**Output During Implementation**
+
+```
+## Implementing: (schema: )
+
+Working on task 3/7:
+[...implementation happening...]
+✓ Task complete
+
+Working on task 4/7:
+[...implementation happening...]
+✓ Task complete
+```
+
+**Output On Completion**
+
+```
+## Implementation Complete
+
+**Change:**
+**Schema:**
+**Progress:** 7/7 tasks complete ✓
+
+### Completed This Session
+- [x] Task 1
+- [x] Task 2
+...
+
+All tasks complete! Ready to archive this change.
+```
+
+**Output On Pause (Issue Encountered)**
+
+```
+## Implementation Paused
+
+**Change:**
+**Schema:**
+**Progress:** 4/7 tasks complete
+
+### Issue Encountered
+
+
+**Options:**
+1.
+2.
+3. Other approach
+
+What would you like to do?
+```
+
+**Guardrails**
+- Keep going through tasks until done or blocked
+- Always read context files before starting (from the apply instructions output)
+- If task is ambiguous, pause and ask before implementing
+- If implementation reveals issues, pause and suggest artifact updates
+- Keep code changes minimal and scoped to each task
+- Update task checkbox immediately after completing each task
+- Pause on errors, blockers, or unclear requirements - don't guess
+- Use contextFiles from CLI output, don't assume specific file names
+
+**Fluid Workflow Integration**
+
+This skill supports the "actions on a change" model:
+
+- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
+- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
diff --git a/.codex/skills/openspec-archive-change/SKILL.md b/.codex/skills/openspec-archive-change/SKILL.md
new file mode 100644
index 0000000..9b1f851
--- /dev/null
+++ b/.codex/skills/openspec-archive-change/SKILL.md
@@ -0,0 +1,114 @@
+---
+name: openspec-archive-change
+description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.
+license: MIT
+compatibility: Requires openspec CLI.
+metadata:
+ author: openspec
+ version: "1.0"
+ generatedBy: "1.2.0"
+---
+
+Archive a completed change in the experimental workflow.
+
+**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
+
+**Steps**
+
+1. **If no change name provided, prompt for selection**
+
+ Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
+
+ Show only active changes (not already archived).
+ Include the schema used for each change if available.
+
+ **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
+
+2. **Check artifact completion status**
+
+ Run `openspec status --change "" --json` to check artifact completion.
+
+ Parse the JSON to understand:
+ - `schemaName`: The workflow being used
+ - `artifacts`: List of artifacts with their status (`done` or other)
+
+ **If any artifacts are not `done`:**
+ - Display warning listing incomplete artifacts
+ - Use **AskUserQuestion tool** to confirm user wants to proceed
+ - Proceed if user confirms
+
+3. **Check task completion status**
+
+ Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
+
+ Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
+
+ **If incomplete tasks found:**
+ - Display warning showing count of incomplete tasks
+ - Use **AskUserQuestion tool** to confirm user wants to proceed
+ - Proceed if user confirms
+
+ **If no tasks file exists:** Proceed without task-related warning.
+
+4. **Assess delta spec sync state**
+
+ Check for delta specs at `openspec/changes//specs/`. If none exist, proceed without sync prompt.
+
+ **If delta specs exist:**
+ - Compare each delta spec with its corresponding main spec at `openspec/specs//spec.md`
+ - Determine what changes would be applied (adds, modifications, removals, renames)
+ - Show a combined summary before prompting
+
+ **Prompt options:**
+ - If changes needed: "Sync now (recommended)", "Archive without syncing"
+ - If already synced: "Archive now", "Sync anyway", "Cancel"
+
+ If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change ''. Delta spec analysis: "). Proceed to archive regardless of choice.
+
+5. **Perform the archive**
+
+ Create the archive directory if it doesn't exist:
+ ```bash
+ mkdir -p openspec/changes/archive
+ ```
+
+ Generate target name using current date: `YYYY-MM-DD-`
+
+ **Check if target already exists:**
+ - If yes: Fail with error, suggest renaming existing archive or using different date
+ - If no: Move the change directory to archive
+
+ ```bash
+ mv openspec/changes/ openspec/changes/archive/YYYY-MM-DD-
+ ```
+
+6. **Display summary**
+
+ Show archive completion summary including:
+ - Change name
+ - Schema that was used
+ - Archive location
+ - Whether specs were synced (if applicable)
+ - Note about any warnings (incomplete artifacts/tasks)
+
+**Output On Success**
+
+```
+## Archive Complete
+
+**Change:**
+**Schema:**
+**Archived to:** openspec/changes/archive/YYYY-MM-DD-/
+**Specs:** ✓ Synced to main specs (or "No delta specs" or "Sync skipped")
+
+All artifacts complete. All tasks complete.
+```
+
+**Guardrails**
+- Always prompt for change selection if not provided
+- Use artifact graph (openspec status --json) for completion checking
+- Don't block archive on warnings - just inform and confirm
+- Preserve .openspec.yaml when moving to archive (it moves with the directory)
+- Show clear summary of what happened
+- If sync is requested, use openspec-sync-specs approach (agent-driven)
+- If delta specs exist, always run the sync assessment and show the combined summary before prompting
diff --git a/.codex/skills/openspec-bulk-archive-change/SKILL.md b/.codex/skills/openspec-bulk-archive-change/SKILL.md
new file mode 100644
index 0000000..d2f199a
--- /dev/null
+++ b/.codex/skills/openspec-bulk-archive-change/SKILL.md
@@ -0,0 +1,246 @@
+---
+name: openspec-bulk-archive-change
+description: Archive multiple completed changes at once. Use when archiving several parallel changes.
+license: MIT
+compatibility: Requires openspec CLI.
+metadata:
+ author: openspec
+ version: "1.0"
+ generatedBy: "1.2.0"
+---
+
+Archive multiple completed changes in a single operation.
+
+This skill allows you to batch-archive changes, handling spec conflicts intelligently by checking the codebase to determine what's actually implemented.
+
+**Input**: None required (prompts for selection)
+
+**Steps**
+
+1. **Get active changes**
+
+ Run `openspec list --json` to get all active changes.
+
+ If no active changes exist, inform user and stop.
+
+2. **Prompt for change selection**
+
+ Use **AskUserQuestion tool** with multi-select to let user choose changes:
+ - Show each change with its schema
+ - Include an option for "All changes"
+ - Allow any number of selections (1+ works, 2+ is the typical use case)
+
+ **IMPORTANT**: Do NOT auto-select. Always let the user choose.
+
+3. **Batch validation - gather status for all selected changes**
+
+ For each selected change, collect:
+
+ a. **Artifact status** - Run `openspec status --change "" --json`
+ - Parse `schemaName` and `artifacts` list
+ - Note which artifacts are `done` vs other states
+
+ b. **Task completion** - Read `openspec/changes//tasks.md`
+ - Count `- [ ]` (incomplete) vs `- [x]` (complete)
+ - If no tasks file exists, note as "No tasks"
+
+ c. **Delta specs** - Check `openspec/changes//specs/` directory
+ - List which capability specs exist
+ - For each, extract requirement names (lines matching `### Requirement: `)
+
+4. **Detect spec conflicts**
+
+ Build a map of `capability -> [changes that touch it]`:
+
+ ```
+ auth -> [change-a, change-b] <- CONFLICT (2+ changes)
+ api -> [change-c] <- OK (only 1 change)
+ ```
+
+ A conflict exists when 2+ selected changes have delta specs for the same capability.
+
+5. **Resolve conflicts agentically**
+
+ **For each conflict**, investigate the codebase:
+
+ a. **Read the delta specs** from each conflicting change to understand what each claims to add/modify
+
+ b. **Search the codebase** for implementation evidence:
+ - Look for code implementing requirements from each delta spec
+ - Check for related files, functions, or tests
+
+ c. **Determine resolution**:
+ - If only one change is actually implemented -> sync that one's specs
+ - If both implemented -> apply in chronological order (older first, newer overwrites)
+ - If neither implemented -> skip spec sync, warn user
+
+ d. **Record resolution** for each conflict:
+ - Which change's specs to apply
+ - In what order (if both)
+ - Rationale (what was found in codebase)
+
+6. **Show consolidated status table**
+
+ Display a table summarizing all changes:
+
+ ```
+ | Change | Artifacts | Tasks | Specs | Conflicts | Status |
+ |---------------------|-----------|-------|---------|-----------|--------|
+ | schema-management | Done | 5/5 | 2 delta | None | Ready |
+ | project-config | Done | 3/3 | 1 delta | None | Ready |
+ | add-oauth | Done | 4/4 | 1 delta | auth (!) | Ready* |
+ | add-verify-skill | 1 left | 2/5 | None | None | Warn |
+ ```
+
+ For conflicts, show the resolution:
+ ```
+ * Conflict resolution:
+ - auth spec: Will apply add-oauth then add-jwt (both implemented, chronological order)
+ ```
+
+ For incomplete changes, show warnings:
+ ```
+ Warnings:
+ - add-verify-skill: 1 incomplete artifact, 3 incomplete tasks
+ ```
+
+7. **Confirm batch operation**
+
+ Use **AskUserQuestion tool** with a single confirmation:
+
+ - "Archive N changes?" with options based on status
+ - Options might include:
+ - "Archive all N changes"
+ - "Archive only N ready changes (skip incomplete)"
+ - "Cancel"
+
+ If there are incomplete changes, make clear they'll be archived with warnings.
+
+8. **Execute archive for each confirmed change**
+
+ Process changes in the determined order (respecting conflict resolution):
+
+ a. **Sync specs** if delta specs exist:
+ - Use the openspec-sync-specs approach (agent-driven intelligent merge)
+ - For conflicts, apply in resolved order
+ - Track if sync was done
+
+ b. **Perform the archive**:
+ ```bash
+ mkdir -p openspec/changes/archive
+ mv openspec/changes/ openspec/changes/archive/YYYY-MM-DD-
+ ```
+
+ c. **Track outcome** for each change:
+ - Success: archived successfully
+ - Failed: error during archive (record error)
+ - Skipped: user chose not to archive (if applicable)
+
+9. **Display summary**
+
+ Show final results:
+
+ ```
+ ## Bulk Archive Complete
+
+ Archived 3 changes:
+ - schema-management-cli -> archive/2026-01-19-schema-management-cli/
+ - project-config -> archive/2026-01-19-project-config/
+ - add-oauth -> archive/2026-01-19-add-oauth/
+
+ Skipped 1 change:
+ - add-verify-skill (user chose not to archive incomplete)
+
+ Spec sync summary:
+ - 4 delta specs synced to main specs
+ - 1 conflict resolved (auth: applied both in chronological order)
+ ```
+
+ If any failures:
+ ```
+ Failed 1 change:
+ - some-change: Archive directory already exists
+ ```
+
+**Conflict Resolution Examples**
+
+Example 1: Only one implemented
+```
+Conflict: specs/auth/spec.md touched by [add-oauth, add-jwt]
+
+Checking add-oauth:
+- Delta adds "OAuth Provider Integration" requirement
+- Searching codebase... found src/auth/oauth.ts implementing OAuth flow
+
+Checking add-jwt:
+- Delta adds "JWT Token Handling" requirement
+- Searching codebase... no JWT implementation found
+
+Resolution: Only add-oauth is implemented. Will sync add-oauth specs only.
+```
+
+Example 2: Both implemented
+```
+Conflict: specs/api/spec.md touched by [add-rest-api, add-graphql]
+
+Checking add-rest-api (created 2026-01-10):
+- Delta adds "REST Endpoints" requirement
+- Searching codebase... found src/api/rest.ts
+
+Checking add-graphql (created 2026-01-15):
+- Delta adds "GraphQL Schema" requirement
+- Searching codebase... found src/api/graphql.ts
+
+Resolution: Both implemented. Will apply add-rest-api specs first,
+then add-graphql specs (chronological order, newer takes precedence).
+```
+
+**Output On Success**
+
+```
+## Bulk Archive Complete
+
+Archived N changes:
+- -> archive/YYYY-MM-DD-/
+- -> archive/YYYY-MM-DD-/
+
+Spec sync summary:
+- N delta specs synced to main specs
+- No conflicts (or: M conflicts resolved)
+```
+
+**Output On Partial Success**
+
+```
+## Bulk Archive Complete (partial)
+
+Archived N changes:
+- -> archive/YYYY-MM-DD-/
+
+Skipped M changes:
+- (user chose not to archive incomplete)
+
+Failed K changes:
+- : Archive directory already exists
+```
+
+**Output When No Changes**
+
+```
+## No Changes to Archive
+
+No active changes found. Create a new change to get started.
+```
+
+**Guardrails**
+- Allow any number of changes (1+ is fine, 2+ is the typical use case)
+- Always prompt for selection, never auto-select
+- Detect spec conflicts early and resolve by checking codebase
+- When both changes are implemented, apply specs in chronological order
+- Skip spec sync only when implementation is missing (warn user)
+- Show clear per-change status before confirming
+- Use single confirmation for entire batch
+- Track and report all outcomes (success/skip/fail)
+- Preserve .openspec.yaml when moving to archive
+- Archive directory target uses current date: YYYY-MM-DD-
+- If archive target exists, fail that change but continue with others
diff --git a/.codex/skills/openspec-continue-change/SKILL.md b/.codex/skills/openspec-continue-change/SKILL.md
new file mode 100644
index 0000000..a2856f0
--- /dev/null
+++ b/.codex/skills/openspec-continue-change/SKILL.md
@@ -0,0 +1,118 @@
+---
+name: openspec-continue-change
+description: Continue working on an OpenSpec change by creating the next artifact. Use when the user wants to progress their change, create the next artifact, or continue their workflow.
+license: MIT
+compatibility: Requires openspec CLI.
+metadata:
+ author: openspec
+ version: "1.0"
+ generatedBy: "1.2.0"
+---
+
+Continue working on a change by creating the next artifact.
+
+**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
+
+**Steps**
+
+1. **If no change name provided, prompt for selection**
+
+ Run `openspec list --json` to get available changes sorted by most recently modified. Then use the **AskUserQuestion tool** to let the user select which change to work on.
+
+ Present the top 3-4 most recently modified changes as options, showing:
+ - Change name
+ - Schema (from `schema` field if present, otherwise "spec-driven")
+ - Status (e.g., "0/5 tasks", "complete", "no tasks")
+ - How recently it was modified (from `lastModified` field)
+
+ Mark the most recently modified change as "(Recommended)" since it's likely what the user wants to continue.
+
+ **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
+
+2. **Check current status**
+ ```bash
+ openspec status --change "" --json
+ ```
+ Parse the JSON to understand current state. The response includes:
+ - `schemaName`: The workflow schema being used (e.g., "spec-driven")
+ - `artifacts`: Array of artifacts with their status ("done", "ready", "blocked")
+ - `isComplete`: Boolean indicating if all artifacts are complete
+
+3. **Act based on status**:
+
+ ---
+
+ **If all artifacts are complete (`isComplete: true`)**:
+ - Congratulate the user
+ - Show final status including the schema used
+ - Suggest: "All artifacts created! You can now implement this change or archive it."
+ - STOP
+
+ ---
+
+ **If artifacts are ready to create** (status shows artifacts with `status: "ready"`):
+ - Pick the FIRST artifact with `status: "ready"` from the status output
+ - Get its instructions:
+ ```bash
+ openspec instructions --change "" --json
+ ```
+ - Parse the JSON. The key fields are:
+ - `context`: Project background (constraints for you - do NOT include in output)
+ - `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
+ - `template`: The structure to use for your output file
+ - `instruction`: Schema-specific guidance
+ - `outputPath`: Where to write the artifact
+ - `dependencies`: Completed artifacts to read for context
+ - **Create the artifact file**:
+ - Read any completed dependency files for context
+ - Use `template` as the structure - fill in its sections
+ - Apply `context` and `rules` as constraints when writing - but do NOT copy them into the file
+ - Write to the output path specified in instructions
+ - Show what was created and what's now unlocked
+ - STOP after creating ONE artifact
+
+ ---
+
+ **If no artifacts are ready (all blocked)**:
+ - This shouldn't happen with a valid schema
+ - Show status and suggest checking for issues
+
+4. **After creating an artifact, show progress**
+ ```bash
+ openspec status --change ""
+ ```
+
+**Output**
+
+After each invocation, show:
+- Which artifact was created
+- Schema workflow being used
+- Current progress (N/M complete)
+- What artifacts are now unlocked
+- Prompt: "Want to continue? Just ask me to continue or tell me what to do next."
+
+**Artifact Creation Guidelines**
+
+The artifact types and their purpose depend on the schema. Use the `instruction` field from the instructions output to understand what to create.
+
+Common artifact patterns:
+
+**spec-driven schema** (proposal → specs → design → tasks):
+- **proposal.md**: Ask user about the change if not clear. Fill in Why, What Changes, Capabilities, Impact.
+ - The Capabilities section is critical - each capability listed will need a spec file.
+- **specs//spec.md**: Create one spec per capability listed in the proposal's Capabilities section (use the capability name, not the change name).
+- **design.md**: Document technical decisions, architecture, and implementation approach.
+- **tasks.md**: Break down implementation into checkboxed tasks.
+
+For other schemas, follow the `instruction` field from the CLI output.
+
+**Guardrails**
+- Create ONE artifact per invocation
+- Always read dependency artifacts before creating a new one
+- Never skip artifacts or create out of order
+- If context is unclear, ask the user before creating
+- Verify the artifact file exists after writing before marking progress
+- Use the schema's artifact sequence, don't assume specific artifact names
+- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
+ - Do NOT copy ``, ``, `` blocks into the artifact
+ - These guide what you write, but should never appear in the output
diff --git a/.codex/skills/openspec-explore/SKILL.md b/.codex/skills/openspec-explore/SKILL.md
new file mode 100644
index 0000000..ffa10ca
--- /dev/null
+++ b/.codex/skills/openspec-explore/SKILL.md
@@ -0,0 +1,288 @@
+---
+name: openspec-explore
+description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.
+license: MIT
+compatibility: Requires openspec CLI.
+metadata:
+ author: openspec
+ version: "1.0"
+ generatedBy: "1.2.0"
+---
+
+Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
+
+**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
+
+**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
+
+---
+
+## The Stance
+
+- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
+- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
+- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
+- **Adaptive** - Follow interesting threads, pivot when new information emerges
+- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
+- **Grounded** - Explore the actual codebase when relevant, don't just theorize
+
+---
+
+## What You Might Do
+
+Depending on what the user brings, you might:
+
+**Explore the problem space**
+- Ask clarifying questions that emerge from what they said
+- Challenge assumptions
+- Reframe the problem
+- Find analogies
+
+**Investigate the codebase**
+- Map existing architecture relevant to the discussion
+- Find integration points
+- Identify patterns already in use
+- Surface hidden complexity
+
+**Compare options**
+- Brainstorm multiple approaches
+- Build comparison tables
+- Sketch tradeoffs
+- Recommend a path (if asked)
+
+**Visualize**
+```
+┌─────────────────────────────────────────┐
+│ Use ASCII diagrams liberally │
+├─────────────────────────────────────────┤
+│ │
+│ ┌────────┐ ┌────────┐ │
+│ │ State │────────▶│ State │ │
+│ │ A │ │ B │ │
+│ └────────┘ └────────┘ │
+│ │
+│ System diagrams, state machines, │
+│ data flows, architecture sketches, │
+│ dependency graphs, comparison tables │
+│ │
+└─────────────────────────────────────────┘
+```
+
+**Surface risks and unknowns**
+- Identify what could go wrong
+- Find gaps in understanding
+- Suggest spikes or investigations
+
+---
+
+## OpenSpec Awareness
+
+You have full context of the OpenSpec system. Use it naturally, don't force it.
+
+### Check for context
+
+At the start, quickly check what exists:
+```bash
+openspec list --json
+```
+
+This tells you:
+- If there are active changes
+- Their names, schemas, and status
+- What the user might be working on
+
+### When no change exists
+
+Think freely. When insights crystallize, you might offer:
+
+- "This feels solid enough to start a change. Want me to create a proposal?"
+- Or keep exploring - no pressure to formalize
+
+### When a change exists
+
+If the user mentions a change or you detect one is relevant:
+
+1. **Read existing artifacts for context**
+ - `openspec/changes//proposal.md`
+ - `openspec/changes//design.md`
+ - `openspec/changes//tasks.md`
+ - etc.
+
+2. **Reference them naturally in conversation**
+ - "Your design mentions using Redis, but we just realized SQLite fits better..."
+ - "The proposal scopes this to premium users, but we're now thinking everyone..."
+
+3. **Offer to capture when decisions are made**
+
+ | Insight Type | Where to Capture |
+ |--------------|------------------|
+ | New requirement discovered | `specs//spec.md` |
+ | Requirement changed | `specs//spec.md` |
+ | Design decision made | `design.md` |
+ | Scope changed | `proposal.md` |
+ | New work identified | `tasks.md` |
+ | Assumption invalidated | Relevant artifact |
+
+ Example offers:
+ - "That's a design decision. Capture it in design.md?"
+ - "This is a new requirement. Add it to specs?"
+ - "This changes scope. Update the proposal?"
+
+4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
+
+---
+
+## What You Don't Have To Do
+
+- Follow a script
+- Ask the same questions every time
+- Produce a specific artifact
+- Reach a conclusion
+- Stay on topic if a tangent is valuable
+- Be brief (this is thinking time)
+
+---
+
+## Handling Different Entry Points
+
+**User brings a vague idea:**
+```
+User: I'm thinking about adding real-time collaboration
+
+You: Real-time collab is a big space. Let me think about this...
+
+ COLLABORATION SPECTRUM
+ ════════════════════════════════════════════
+
+ Awareness Coordination Sync
+ │ │ │
+ ▼ ▼ ▼
+ ┌────────┐ ┌────────┐ ┌────────┐
+ │Presence│ │Cursors │ │ CRDT │
+ │ "3 │ │ Multi │ │Conflict│
+ │online" │ │ select │ │ free │
+ └────────┘ └────────┘ └────────┘
+ │ │ │
+ trivial moderate complex
+
+ Where's your head at?
+```
+
+**User brings a specific problem:**
+```
+User: The auth system is a mess
+
+You: [reads codebase]
+
+ ┌─────────────────────────────────────────────┐
+ │ CURRENT AUTH FLOW │
+ └─────────────────────────────────────────────┘
+ │
+ ┌─────────────┼─────────────┐
+ ▼ ▼ ▼
+ ┌─────────┐ ┌─────────┐ ┌─────────┐
+ │ Google │ │ GitHub │ │ Email │
+ │ OAuth │ │ OAuth │ │ Magic │
+ └────┬────┘ └────┬────┘ └────┬────┘
+ │ │ │
+ └─────────────┼─────────────┘
+ ▼
+ ┌───────────┐
+ │ Session │
+ └─────┬─────┘
+ │
+ ▼
+ ┌───────────┐
+ │ Perms │
+ └───────────┘
+
+ I see three tangles. Which one's burning?
+```
+
+**User is stuck mid-implementation:**
+```
+User: /opsx:explore add-auth-system
+ The OAuth integration is more complex than expected
+
+You: [reads change artifacts]
+
+ You're on task 4: "Implement OAuth flow"
+
+ Let me trace what's involved...
+
+ [draws diagram, explores options, suggests paths]
+
+ Want to update the design to reflect this?
+ Or add a spike task to investigate?
+```
+
+**User wants to compare options:**
+```
+User: Should we use Postgres or SQLite?
+
+You: Generic answer is boring. What's the context?
+
+User: A CLI tool that tracks local dev environments
+
+You: That changes everything.
+
+ ┌─────────────────────────────────────────────────┐
+ │ CLI TOOL DATA STORAGE │
+ └─────────────────────────────────────────────────┘
+
+ Key constraints:
+ • No daemon running
+ • Must work offline
+ • Single user
+
+ SQLite Postgres
+ Deployment embedded ✓ needs server ✗
+ Offline yes ✓ no ✗
+ Single file yes ✓ no ✗
+
+ SQLite. Not even close.
+
+ Unless... is there a sync component?
+```
+
+---
+
+## Ending Discovery
+
+There's no required ending. Discovery might:
+
+- **Flow into a proposal**: "Ready to start? I can create a change proposal."
+- **Result in artifact updates**: "Updated design.md with these decisions"
+- **Just provide clarity**: User has what they need, moves on
+- **Continue later**: "We can pick this up anytime"
+
+When it feels like things are crystallizing, you might summarize:
+
+```
+## What We Figured Out
+
+**The problem**: [crystallized understanding]
+
+**The approach**: [if one emerged]
+
+**Open questions**: [if any remain]
+
+**Next steps** (if ready):
+- Create a change proposal
+- Keep exploring: just keep talking
+```
+
+But this summary is optional. Sometimes the thinking IS the value.
+
+---
+
+## Guardrails
+
+- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
+- **Don't fake understanding** - If something is unclear, dig deeper
+- **Don't rush** - Discovery is thinking time, not task time
+- **Don't force structure** - Let patterns emerge naturally
+- **Don't auto-capture** - Offer to save insights, don't just do it
+- **Do visualize** - A good diagram is worth many paragraphs
+- **Do explore the codebase** - Ground discussions in reality
+- **Do question assumptions** - Including the user's and your own
diff --git a/.codex/skills/openspec-ff-change/SKILL.md b/.codex/skills/openspec-ff-change/SKILL.md
new file mode 100644
index 0000000..d5f1204
--- /dev/null
+++ b/.codex/skills/openspec-ff-change/SKILL.md
@@ -0,0 +1,101 @@
+---
+name: openspec-ff-change
+description: Fast-forward through OpenSpec artifact creation. Use when the user wants to quickly create all artifacts needed for implementation without stepping through each one individually.
+license: MIT
+compatibility: Requires openspec CLI.
+metadata:
+ author: openspec
+ version: "1.0"
+ generatedBy: "1.2.0"
+---
+
+Fast-forward through artifact creation - generate everything needed to start implementation in one go.
+
+**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
+
+**Steps**
+
+1. **If no clear input provided, ask what they want to build**
+
+ Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
+ > "What change do you want to work on? Describe what you want to build or fix."
+
+ From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
+
+ **IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
+
+2. **Create the change directory**
+ ```bash
+ openspec new change ""
+ ```
+ This creates a scaffolded change at `openspec/changes//`.
+
+3. **Get the artifact build order**
+ ```bash
+ openspec status --change "" --json
+ ```
+ Parse the JSON to get:
+ - `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
+ - `artifacts`: list of all artifacts with their status and dependencies
+
+4. **Create artifacts in sequence until apply-ready**
+
+ Use the **TodoWrite tool** to track progress through the artifacts.
+
+ Loop through artifacts in dependency order (artifacts with no pending dependencies first):
+
+ a. **For each artifact that is `ready` (dependencies satisfied)**:
+ - Get instructions:
+ ```bash
+ openspec instructions --change "" --json
+ ```
+ - The instructions JSON includes:
+ - `context`: Project background (constraints for you - do NOT include in output)
+ - `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
+ - `template`: The structure to use for your output file
+ - `instruction`: Schema-specific guidance for this artifact type
+ - `outputPath`: Where to write the artifact
+ - `dependencies`: Completed artifacts to read for context
+ - Read any completed dependency files for context
+ - Create the artifact file using `template` as the structure
+ - Apply `context` and `rules` as constraints - but do NOT copy them into the file
+ - Show brief progress: "✓ Created "
+
+ b. **Continue until all `applyRequires` artifacts are complete**
+ - After creating each artifact, re-run `openspec status --change "" --json`
+ - Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
+ - Stop when all `applyRequires` artifacts are done
+
+ c. **If an artifact requires user input** (unclear context):
+ - Use **AskUserQuestion tool** to clarify
+ - Then continue with creation
+
+5. **Show final status**
+ ```bash
+ openspec status --change ""
+ ```
+
+**Output**
+
+After completing all artifacts, summarize:
+- Change name and location
+- List of artifacts created with brief descriptions
+- What's ready: "All artifacts created! Ready for implementation."
+- Prompt: "Run `/opsx:apply` or ask me to implement to start working on the tasks."
+
+**Artifact Creation Guidelines**
+
+- Follow the `instruction` field from `openspec instructions` for each artifact type
+- The schema defines what each artifact should contain - follow it
+- Read dependency artifacts for context before creating new ones
+- Use `template` as the structure for your output file - fill in its sections
+- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
+ - Do NOT copy ``, ``, `` blocks into the artifact
+ - These guide what you write, but should never appear in the output
+
+**Guardrails**
+- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
+- Always read dependency artifacts before creating a new one
+- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
+- If a change with that name already exists, suggest continuing that change instead
+- Verify each artifact file exists after writing before proceeding to next
diff --git a/.codex/skills/openspec-new-change/SKILL.md b/.codex/skills/openspec-new-change/SKILL.md
new file mode 100644
index 0000000..607391a
--- /dev/null
+++ b/.codex/skills/openspec-new-change/SKILL.md
@@ -0,0 +1,74 @@
+---
+name: openspec-new-change
+description: Start a new OpenSpec change using the experimental artifact workflow. Use when the user wants to create a new feature, fix, or modification with a structured step-by-step approach.
+license: MIT
+compatibility: Requires openspec CLI.
+metadata:
+ author: openspec
+ version: "1.0"
+ generatedBy: "1.2.0"
+---
+
+Start a new change using the experimental artifact-driven approach.
+
+**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
+
+**Steps**
+
+1. **If no clear input provided, ask what they want to build**
+
+ Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
+ > "What change do you want to work on? Describe what you want to build or fix."
+
+ From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
+
+ **IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
+
+2. **Determine the workflow schema**
+
+ Use the default schema (omit `--schema`) unless the user explicitly requests a different workflow.
+
+ **Use a different schema only if the user mentions:**
+ - A specific schema name → use `--schema `
+ - "show workflows" or "what workflows" → run `openspec schemas --json` and let them choose
+
+ **Otherwise**: Omit `--schema` to use the default.
+
+3. **Create the change directory**
+ ```bash
+ openspec new change ""
+ ```
+ Add `--schema ` only if the user requested a specific workflow.
+ This creates a scaffolded change at `openspec/changes//` with the selected schema.
+
+4. **Show the artifact status**
+ ```bash
+ openspec status --change ""
+ ```
+ This shows which artifacts need to be created and which are ready (dependencies satisfied).
+
+5. **Get instructions for the first artifact**
+ The first artifact depends on the schema (e.g., `proposal` for spec-driven).
+ Check the status output to find the first artifact with status "ready".
+ ```bash
+ openspec instructions --change ""
+ ```
+ This outputs the template and context for creating the first artifact.
+
+6. **STOP and wait for user direction**
+
+**Output**
+
+After completing the steps, summarize:
+- Change name and location
+- Schema/workflow being used and its artifact sequence
+- Current status (0/N artifacts complete)
+- The template for the first artifact
+- Prompt: "Ready to create the first artifact? Just describe what this change is about and I'll draft it, or ask me to continue."
+
+**Guardrails**
+- Do NOT create any artifacts yet - just show the instructions
+- Do NOT advance beyond showing the first artifact template
+- If the name is invalid (not kebab-case), ask for a valid name
+- If a change with that name already exists, suggest continuing that change instead
+- Pass --schema if using a non-default workflow
diff --git a/.codex/skills/openspec-onboard/SKILL.md b/.codex/skills/openspec-onboard/SKILL.md
new file mode 100644
index 0000000..9076b5d
--- /dev/null
+++ b/.codex/skills/openspec-onboard/SKILL.md
@@ -0,0 +1,554 @@
+---
+name: openspec-onboard
+description: Guided onboarding for OpenSpec - walk through a complete workflow cycle with narration and real codebase work.
+license: MIT
+compatibility: Requires openspec CLI.
+metadata:
+ author: openspec
+ version: "1.0"
+ generatedBy: "1.2.0"
+---
+
+Guide the user through their first complete OpenSpec workflow cycle. This is a teaching experience—you'll do real work in their codebase while explaining each step.
+
+---
+
+## Preflight
+
+Before starting, check if the OpenSpec CLI is installed:
+
+```bash
+# Unix/macOS
+openspec --version 2>&1 || echo "CLI_NOT_INSTALLED"
+# Windows (PowerShell)
+# if (Get-Command openspec -ErrorAction SilentlyContinue) { openspec --version } else { echo "CLI_NOT_INSTALLED" }
+```
+
+**If CLI not installed:**
+> OpenSpec CLI is not installed. Install it first, then come back to `/opsx:onboard`.
+
+Stop here if not installed.
+
+---
+
+## Phase 1: Welcome
+
+Display:
+
+```
+## Welcome to OpenSpec!
+
+I'll walk you through a complete change cycle—from idea to implementation—using a real task in your codebase. Along the way, you'll learn the workflow by doing it.
+
+**What we'll do:**
+1. Pick a small, real task in your codebase
+2. Explore the problem briefly
+3. Create a change (the container for our work)
+4. Build the artifacts: proposal → specs → design → tasks
+5. Implement the tasks
+6. Archive the completed change
+
+**Time:** ~15-20 minutes
+
+Let's start by finding something to work on.
+```
+
+---
+
+## Phase 2: Task Selection
+
+### Codebase Analysis
+
+Scan the codebase for small improvement opportunities. Look for:
+
+1. **TODO/FIXME comments** - Search for `TODO`, `FIXME`, `HACK`, `XXX` in code files
+2. **Missing error handling** - `catch` blocks that swallow errors, risky operations without try-catch
+3. **Functions without tests** - Cross-reference `src/` with test directories
+4. **Type issues** - `any` types in TypeScript files (`: any`, `as any`)
+5. **Debug artifacts** - `console.log`, `console.debug`, `debugger` statements in non-debug code
+6. **Missing validation** - User input handlers without validation
+
+Also check recent git activity:
+```bash
+# Unix/macOS
+git log --oneline -10 2>/dev/null || echo "No git history"
+# Windows (PowerShell)
+# git log --oneline -10 2>$null; if ($LASTEXITCODE -ne 0) { echo "No git history" }
+```
+
+### Present Suggestions
+
+From your analysis, present 3-4 specific suggestions:
+
+```
+## Task Suggestions
+
+Based on scanning your codebase, here are some good starter tasks:
+
+**1. [Most promising task]**
+ Location: `src/path/to/file.ts:42`
+ Scope: ~1-2 files, ~20-30 lines
+ Why it's good: [brief reason]
+
+**2. [Second task]**
+ Location: `src/another/file.ts`
+ Scope: ~1 file, ~15 lines
+ Why it's good: [brief reason]
+
+**3. [Third task]**
+ Location: [location]
+ Scope: [estimate]
+ Why it's good: [brief reason]
+
+**4. Something else?**
+ Tell me what you'd like to work on.
+
+Which task interests you? (Pick a number or describe your own)
+```
+
+**If nothing found:** Fall back to asking what the user wants to build:
+> I didn't find obvious quick wins in your codebase. What's something small you've been meaning to add or fix?
+
+### Scope Guardrail
+
+If the user picks or describes something too large (major feature, multi-day work):
+
+```
+That's a valuable task, but it's probably larger than ideal for your first OpenSpec run-through.
+
+For learning the workflow, smaller is better—it lets you see the full cycle without getting stuck in implementation details.
+
+**Options:**
+1. **Slice it smaller** - What's the smallest useful piece of [their task]? Maybe just [specific slice]?
+2. **Pick something else** - One of the other suggestions, or a different small task?
+3. **Do it anyway** - If you really want to tackle this, we can. Just know it'll take longer.
+
+What would you prefer?
+```
+
+Let the user override if they insist—this is a soft guardrail.
+
+---
+
+## Phase 3: Explore Demo
+
+Once a task is selected, briefly demonstrate explore mode:
+
+```
+Before we create a change, let me quickly show you **explore mode**—it's how you think through problems before committing to a direction.
+```
+
+Spend 1-2 minutes investigating the relevant code:
+- Read the file(s) involved
+- Draw a quick ASCII diagram if it helps
+- Note any considerations
+
+```
+## Quick Exploration
+
+[Your brief analysis—what you found, any considerations]
+
+┌─────────────────────────────────────────┐
+│ [Optional: ASCII diagram if helpful] │
+└─────────────────────────────────────────┘
+
+Explore mode (`/opsx:explore`) is for this kind of thinking—investigating before implementing. You can use it anytime you need to think through a problem.
+
+Now let's create a change to hold our work.
+```
+
+**PAUSE** - Wait for user acknowledgment before proceeding.
+
+---
+
+## Phase 4: Create the Change
+
+**EXPLAIN:**
+```
+## Creating a Change
+
+A "change" in OpenSpec is a container for all the thinking and planning around a piece of work. It lives in `openspec/changes//` and holds your artifacts—proposal, specs, design, tasks.
+
+Let me create one for our task.
+```
+
+**DO:** Create the change with a derived kebab-case name:
+```bash
+openspec new change ""
+```
+
+**SHOW:**
+```
+Created: `openspec/changes//`
+
+The folder structure:
+```
+openspec/changes//
+├── proposal.md ← Why we're doing this (empty, we'll fill it)
+├── design.md ← How we'll build it (empty)
+├── specs/ ← Detailed requirements (empty)
+└── tasks.md ← Implementation checklist (empty)
+```
+
+Now let's fill in the first artifact—the proposal.
+```
+
+---
+
+## Phase 5: Proposal
+
+**EXPLAIN:**
+```
+## The Proposal
+
+The proposal captures **why** we're making this change and **what** it involves at a high level. It's the "elevator pitch" for the work.
+
+I'll draft one based on our task.
+```
+
+**DO:** Draft the proposal content (don't save yet):
+
+```
+Here's a draft proposal:
+
+---
+
+## Why
+
+[1-2 sentences explaining the problem/opportunity]
+
+## What Changes
+
+[Bullet points of what will be different]
+
+## Capabilities
+
+### New Capabilities
+- ``: [brief description]
+
+### Modified Capabilities
+
+
+## Impact
+
+- `src/path/to/file.ts`: [what changes]
+- [other files if applicable]
+
+---
+
+Does this capture the intent? I can adjust before we save it.
+```
+
+**PAUSE** - Wait for user approval/feedback.
+
+After approval, save the proposal:
+```bash
+openspec instructions proposal --change "" --json
+```
+Then write the content to `openspec/changes//proposal.md`.
+
+```
+Proposal saved. This is your "why" document—you can always come back and refine it as understanding evolves.
+
+Next up: specs.
+```
+
+---
+
+## Phase 6: Specs
+
+**EXPLAIN:**
+```
+## Specs
+
+Specs define **what** we're building in precise, testable terms. They use a requirement/scenario format that makes expected behavior crystal clear.
+
+For a small task like this, we might only need one spec file.
+```
+
+**DO:** Create the spec file:
+```bash
+# Unix/macOS
+mkdir -p openspec/changes//specs/
+# Windows (PowerShell)
+# New-Item -ItemType Directory -Force -Path "openspec/changes//specs/"
+```
+
+Draft the spec content:
+
+```
+Here's the spec:
+
+---
+
+## ADDED Requirements
+
+### Requirement:
+
+
+
+#### Scenario:
+
+- **WHEN**
+- **THEN**
+- **AND**
+
+---
+
+This format—WHEN/THEN/AND—makes requirements testable. You can literally read them as test cases.
+```
+
+Save to `openspec/changes//specs//spec.md`.
+
+---
+
+## Phase 7: Design
+
+**EXPLAIN:**
+```
+## Design
+
+The design captures **how** we'll build it—technical decisions, tradeoffs, approach.
+
+For small changes, this might be brief. That's fine—not every change needs deep design discussion.
+```
+
+**DO:** Draft design.md:
+
+```
+Here's the design:
+
+---
+
+## Context
+
+[Brief context about the current state]
+
+## Goals / Non-Goals
+
+**Goals:**
+- [What we're trying to achieve]
+
+**Non-Goals:**
+- [What's explicitly out of scope]
+
+## Decisions
+
+### Decision 1: [Key decision]
+
+[Explanation of approach and rationale]
+
+---
+
+For a small task, this captures the key decisions without over-engineering.
+```
+
+Save to `openspec/changes//design.md`.
+
+---
+
+## Phase 8: Tasks
+
+**EXPLAIN:**
+```
+## Tasks
+
+Finally, we break the work into implementation tasks—checkboxes that drive the apply phase.
+
+These should be small, clear, and in logical order.
+```
+
+**DO:** Generate tasks based on specs and design:
+
+```
+Here are the implementation tasks:
+
+---
+
+## 1. [Category or file]
+
+- [ ] 1.1 [Specific task]
+- [ ] 1.2 [Specific task]
+
+## 2. Verify
+
+- [ ] 2.1 [Verification step]
+
+---
+
+Each checkbox becomes a unit of work in the apply phase. Ready to implement?
+```
+
+**PAUSE** - Wait for user to confirm they're ready to implement.
+
+Save to `openspec/changes//tasks.md`.
+
+---
+
+## Phase 9: Apply (Implementation)
+
+**EXPLAIN:**
+```
+## Implementation
+
+Now we implement each task, checking them off as we go. I'll announce each one and occasionally note how the specs/design informed the approach.
+```
+
+**DO:** For each task:
+
+1. Announce: "Working on task N: [description]"
+2. Implement the change in the codebase
+3. Reference specs/design naturally: "The spec says X, so I'm doing Y"
+4. Mark complete in tasks.md: `- [ ]` → `- [x]`
+5. Brief status: "✓ Task N complete"
+
+Keep narration light—don't over-explain every line of code.
+
+After all tasks:
+
+```
+## Implementation Complete
+
+All tasks done:
+- [x] Task 1
+- [x] Task 2
+- [x] ...
+
+The change is implemented! One more step—let's archive it.
+```
+
+---
+
+## Phase 10: Archive
+
+**EXPLAIN:**
+```
+## Archiving
+
+When a change is complete, we archive it. This moves it from `openspec/changes/` to `openspec/changes/archive/YYYY-MM-DD-/`.
+
+Archived changes become your project's decision history—you can always find them later to understand why something was built a certain way.
+```
+
+**DO:**
+```bash
+openspec archive ""
+```
+
+**SHOW:**
+```
+Archived to: `openspec/changes/archive/YYYY-MM-DD-/`
+
+The change is now part of your project's history. The code is in your codebase, the decision record is preserved.
+```
+
+---
+
+## Phase 11: Recap & Next Steps
+
+```
+## Congratulations!
+
+You just completed a full OpenSpec cycle:
+
+1. **Explore** - Thought through the problem
+2. **New** - Created a change container
+3. **Proposal** - Captured WHY
+4. **Specs** - Defined WHAT in detail
+5. **Design** - Decided HOW
+6. **Tasks** - Broke it into steps
+7. **Apply** - Implemented the work
+8. **Archive** - Preserved the record
+
+This same rhythm works for any size change—a small fix or a major feature.
+
+---
+
+## Command Reference
+
+**Core workflow:**
+
+| Command | What it does |
+|---------|--------------|
+| `/opsx:propose` | Create a change and generate all artifacts |
+| `/opsx:explore` | Think through problems before/during work |
+| `/opsx:apply` | Implement tasks from a change |
+| `/opsx:archive` | Archive a completed change |
+
+**Additional commands:**
+
+| Command | What it does |
+|---------|--------------|
+| `/opsx:new` | Start a new change, step through artifacts one at a time |
+| `/opsx:continue` | Continue working on an existing change |
+| `/opsx:ff` | Fast-forward: create all artifacts at once |
+| `/opsx:verify` | Verify implementation matches artifacts |
+
+---
+
+## What's Next?
+
+Try `/opsx:propose` on something you actually want to build. You've got the rhythm now!
+```
+
+---
+
+## Graceful Exit Handling
+
+### User wants to stop mid-way
+
+If the user says they need to stop, want to pause, or seem disengaged:
+
+```
+No problem! Your change is saved at `openspec/changes//`.
+
+To pick up where we left off later:
+- `/opsx:continue ` - Resume artifact creation
+- `/opsx:apply ` - Jump to implementation (if tasks exist)
+
+The work won't be lost. Come back whenever you're ready.
+```
+
+Exit gracefully without pressure.
+
+### User just wants command reference
+
+If the user says they just want to see the commands or skip the tutorial:
+
+```
+## OpenSpec Quick Reference
+
+**Core workflow:**
+
+| Command | What it does |
+|---------|--------------|
+| `/opsx:propose ` | Create a change and generate all artifacts |
+| `/opsx:explore` | Think through problems (no code changes) |
+| `/opsx:apply ` | Implement tasks |
+| `/opsx:archive ` | Archive when done |
+
+**Additional commands:**
+
+| Command | What it does |
+|---------|--------------|
+| `/opsx:new ` | Start a new change, step by step |
+| `/opsx:continue ` | Continue an existing change |
+| `/opsx:ff ` | Fast-forward: all artifacts at once |
+| `/opsx:verify ` | Verify implementation |
+
+Try `/opsx:propose` to start your first change.
+```
+
+Exit gracefully.
+
+---
+
+## Guardrails
+
+- **Follow the EXPLAIN → DO → SHOW → PAUSE pattern** at key transitions (after explore, after proposal draft, after tasks, after archive)
+- **Keep narration light** during implementation—teach without lecturing
+- **Don't skip phases** even if the change is small—the goal is teaching the workflow
+- **Pause for acknowledgment** at marked points, but don't over-pause
+- **Handle exits gracefully**—never pressure the user to continue
+- **Use real codebase tasks**—don't simulate or use fake examples
+- **Adjust scope gently**—guide toward smaller tasks but respect user choice
diff --git a/.codex/skills/openspec-sync-specs/SKILL.md b/.codex/skills/openspec-sync-specs/SKILL.md
new file mode 100644
index 0000000..353bfac
--- /dev/null
+++ b/.codex/skills/openspec-sync-specs/SKILL.md
@@ -0,0 +1,138 @@
+---
+name: openspec-sync-specs
+description: Sync delta specs from a change to main specs. Use when the user wants to update main specs with changes from a delta spec, without archiving the change.
+license: MIT
+compatibility: Requires openspec CLI.
+metadata:
+ author: openspec
+ version: "1.0"
+ generatedBy: "1.2.0"
+---
+
+Sync delta specs from a change to main specs.
+
+This is an **agent-driven** operation - you will read delta specs and directly edit main specs to apply the changes. This allows intelligent merging (e.g., adding a scenario without copying the entire requirement).
+
+**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
+
+**Steps**
+
+1. **If no change name provided, prompt for selection**
+
+ Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
+
+ Show changes that have delta specs (under `specs/` directory).
+
+ **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
+
+2. **Find delta specs**
+
+ Look for delta spec files in `openspec/changes//specs/*/spec.md`.
+
+ Each delta spec file contains sections like:
+ - `## ADDED Requirements` - New requirements to add
+ - `## MODIFIED Requirements` - Changes to existing requirements
+ - `## REMOVED Requirements` - Requirements to remove
+ - `## RENAMED Requirements` - Requirements to rename (FROM:/TO: format)
+
+ If no delta specs found, inform user and stop.
+
+3. **For each delta spec, apply changes to main specs**
+
+ For each capability with a delta spec at `openspec/changes//specs//spec.md`:
+
+ a. **Read the delta spec** to understand the intended changes
+
+ b. **Read the main spec** at `openspec/specs//spec.md` (may not exist yet)
+
+ c. **Apply changes intelligently**:
+
+ **ADDED Requirements:**
+ - If requirement doesn't exist in main spec → add it
+ - If requirement already exists → update it to match (treat as implicit MODIFIED)
+
+ **MODIFIED Requirements:**
+ - Find the requirement in main spec
+ - Apply the changes - this can be:
+ - Adding new scenarios (don't need to copy existing ones)
+ - Modifying existing scenarios
+ - Changing the requirement description
+ - Preserve scenarios/content not mentioned in the delta
+
+ **REMOVED Requirements:**
+ - Remove the entire requirement block from main spec
+
+ **RENAMED Requirements:**
+ - Find the FROM requirement, rename to TO
+
+ d. **Create new main spec** if capability doesn't exist yet:
+ - Create `openspec/specs//spec.md`
+ - Add Purpose section (can be brief, mark as TBD)
+ - Add Requirements section with the ADDED requirements
+
+4. **Show summary**
+
+ After applying all changes, summarize:
+ - Which capabilities were updated
+ - What changes were made (requirements added/modified/removed/renamed)
+
+**Delta Spec Format Reference**
+
+```markdown
+## ADDED Requirements
+
+### Requirement: New Feature
+The system SHALL do something new.
+
+#### Scenario: Basic case
+- **WHEN** user does X
+- **THEN** system does Y
+
+## MODIFIED Requirements
+
+### Requirement: Existing Feature
+#### Scenario: New scenario to add
+- **WHEN** user does A
+- **THEN** system does B
+
+## REMOVED Requirements
+
+### Requirement: Deprecated Feature
+
+## RENAMED Requirements
+
+- FROM: `### Requirement: Old Name`
+- TO: `### Requirement: New Name`
+```
+
+**Key Principle: Intelligent Merging**
+
+Unlike programmatic merging, you can apply **partial updates**:
+- To add a scenario, just include that scenario under MODIFIED - don't copy existing scenarios
+- The delta represents *intent*, not a wholesale replacement
+- Use your judgment to merge changes sensibly
+
+**Output On Success**
+
+```
+## Specs Synced:
+
+Updated main specs:
+
+****:
+- Added requirement: "New Feature"
+- Modified requirement: "Existing Feature" (added 1 scenario)
+
+****:
+- Created new spec file
+- Added requirement: "Another Feature"
+
+Main specs are now updated. The change remains active - archive when implementation is complete.
+```
+
+**Guardrails**
+- Read both delta and main specs before making changes
+- Preserve existing content not mentioned in delta
+- If something is unclear, ask for clarification
+- Show what you're changing as you go
+- The operation should be idempotent - running twice should give same result
diff --git a/.codex/skills/openspec-verify-change/SKILL.md b/.codex/skills/openspec-verify-change/SKILL.md
new file mode 100644
index 0000000..744a088
--- /dev/null
+++ b/.codex/skills/openspec-verify-change/SKILL.md
@@ -0,0 +1,168 @@
+---
+name: openspec-verify-change
+description: Verify implementation matches change artifacts. Use when the user wants to validate that implementation is complete, correct, and coherent before archiving.
+license: MIT
+compatibility: Requires openspec CLI.
+metadata:
+ author: openspec
+ version: "1.0"
+ generatedBy: "1.2.0"
+---
+
+Verify that an implementation matches the change artifacts (specs, tasks, design).
+
+**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
+
+**Steps**
+
+1. **If no change name provided, prompt for selection**
+
+ Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
+
+ Show changes that have implementation tasks (tasks artifact exists).
+ Include the schema used for each change if available.
+ Mark changes with incomplete tasks as "(In Progress)".
+
+ **IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
+
+2. **Check status to understand the schema**
+ ```bash
+ openspec status --change "" --json
+ ```
+ Parse the JSON to understand:
+ - `schemaName`: The workflow being used (e.g., "spec-driven")
+ - Which artifacts exist for this change
+
+3. **Get the change directory and load artifacts**
+
+ ```bash
+ openspec instructions apply --change "" --json
+ ```
+
+ This returns the change directory and context files. Read all available artifacts from `contextFiles`.
+
+4. **Initialize verification report structure**
+
+ Create a report structure with three dimensions:
+ - **Completeness**: Track tasks and spec coverage
+ - **Correctness**: Track requirement implementation and scenario coverage
+ - **Coherence**: Track design adherence and pattern consistency
+
+ Each dimension can have CRITICAL, WARNING, or SUGGESTION issues.
+
+5. **Verify Completeness**
+
+ **Task Completion**:
+ - If tasks.md exists in contextFiles, read it
+ - Parse checkboxes: `- [ ]` (incomplete) vs `- [x]` (complete)
+ - Count complete vs total tasks
+ - If incomplete tasks exist:
+ - Add CRITICAL issue for each incomplete task
+ - Recommendation: "Complete task: " or "Mark as done if already implemented"
+
+ **Spec Coverage**:
+ - If delta specs exist in `openspec/changes/