diff --git a/.agents/skills/better-ui/SKILL.md b/.agents/skills/better-ui/SKILL.md
new file mode 100644
index 0000000..1922230
--- /dev/null
+++ b/.agents/skills/better-ui/SKILL.md
@@ -0,0 +1,112 @@
+---
+name: better-ui
+description: Polishes and improves the UI in your project. Covers concentric border radius, optical alignment, surface depth, contextual icons, hit areas and more.
+---
+
+# UI polish
+
+Polish comes from a pile of small details that compound. This skill is the reference for which are worth having and what values they take.
+
+When reviewing, slow the interface down. What feels off at 10% speed is what is subtly wrong at full speed.
+
+Keep the project's component library, tokens and density, and match its motion language except where a rule below prescribes an exact interaction.
+
+Every duration, curve, scale and blur below is a specific value, not a range to approximate. `cubic-bezier(0.2, 0, 0, 1)` is not `cubic-bezier(0.4, 0, 0.2, 1)`, and `0.96` is not `0.95`. Use what is written.
+
+Text wrapping, font rendering, tabular numbers and text spacing belong to `better-typography`. Hit areas, focus, keyboard support, ARIA and reduced motion belong to `better-accessibility`. Grouping, section spacing, breakpoints and spatial RTL belong to `better-layout`.
+
+## Concentric border radius
+
+Outer radius = inner radius + padding. Mismatched radii on nested elements is the most common thing that makes an interface feel off. Radius, shadow and outline recipes are in [surfaces.md](surfaces.md).
+
+## Optical over geometric alignment
+
+When geometric centering looks off, align optically. Buttons with icons, play triangles and asymmetric icons all need a manual nudge.
+
+## Shadows for elevation, borders for structure
+
+Where a border exists only to create depth, prefer layered transparent `box-shadow` values. Keep borders that communicate structure or state: dividers, separators and selected or focus states.
+
+## Interruptible animations
+
+Use CSS transitions for interactive state changes, because they can be interrupted mid-animation. Reserve keyframes for staged sequences that run once.
+
+## Split and stagger enter animations
+
+For an infrequent staged entrance where sequence communicates hierarchy, break the content into semantic chunks and stagger them by ~100ms. Animating one container gets you less for the same cost. Leave high-frequency interactions unstaggered. See [enter-exit.md](enter-exit.md).
+
+## Subtle exit animations
+
+Use a small fixed `translateY` rather than full height. Exits should be softer than enters. Use `ease-out` for both directions.
+
+## Contextual icon animations
+
+Animate icons with `opacity`, `scale` and `blur` rather than toggling visibility. Use exactly these values: scale `0.25` to `1`, opacity `0` to `1`, blur `4px` to `0px`.
+
+With a motion library (`motion` or `framer-motion` in `package.json`), match that package's import path, or nearby imports where both exist. Use `transition: { type: "spring", duration: 0.3, bounce: 0 }`. Bounce is always `0`.
+
+Without one, keep both icons in the DOM with one absolutely positioned, and cross-fade with `cubic-bezier(0.2, 0, 0, 1)`. That gives you enter and exit with no dependency. Both recipes are in [icon-transitions.md](icon-transitions.md).
+
+## Image outlines
+
+Give images a `1px` outline at low opacity for consistent depth. Pure black in light mode (`oklch(0 0 0 / 0.1)`), pure white in dark (`oklch(1 0 0 / 0.1)`). Never a near-black like slate or zinc and never a tinted neutral. A tinted outline picks up the surface underneath and reads as dirt on the image edge.
+
+## Scale on press
+
+A `scale(0.96)` on click gives a button tactile feedback. Always `0.96`; anything below `0.95` feels exaggerated. Add a `static` prop to switch it off where motion would distract. See [recipes for CSS, Tailwind and Motion](animations.md#scale-on-press).
+
+## Skip animation on page load
+
+Use `initial={false}` on `AnimatePresence` to keep enter animations off the first render. Check that it leaves intentional page entrances intact.
+
+## Suppress transitions on theme switch
+
+A theme flip changes color, background, border and shadow on nearly every element at once. Every transition on those properties fires together and the switch smears instead of snapping. Inject `*,*::before,*::after{transition:none !important}`, force a reflow, then remove it on the next frame. See the [recipe](animations.md#suppress-transitions-on-theme-switch).
+
+## Transition only what changes
+
+Always name the exact properties: `transition-property: scale, opacity`. Tailwind's `transition-transform` covers `transform, translate, scale, rotate`.
+
+## Use `will-change` sparingly
+
+Only for `transform`, `opacity` and `filter`, which the GPU can composite. Never `will-change: all`. Add it when you see first-frame stutter, not before. See [performance.md](performance.md).
+
+## Match icon stroke to text weight
+
+An icon next to text carries the text's optical weight: `1.5px` stroke beside regular (400) text, `2px` beside semibold (600). One stroke weight per icon set and one icon library per surface. Sizing and RTL flipping are in [icons.md](icons.md).
+
+## One SVG, recolored per state
+
+Icons use `currentColor` and take hover, selected and disabled states from CSS color and opacity, never from separate assets. Outline is the default variant; fill marks the active state.
+
+## Motion restraint
+
+Give high-frequency interactions instant feedback, or a transition of `150ms` or less on opacity and color. A custom animation there charges its attention cost on every trigger.
+
+Every animated state change also needs a static cue: color, an icon, or a label. Motion is never the only feedback channel.
+
+## Before you finish
+
+| Mistake | Fix |
+| --- | --- |
+| Icons look off-center | Nudge optically with padding, or fix the SVG |
+| Jarring staged entrance or exit | Stagger infrequent entrances; keep exits subtle |
+| Theme toggle crossfades the whole page | Disable transitions for the swap, force a reflow, restore on the next frame |
+| `transition: all` on elements | Specify exact properties |
+| First-frame animation stutter | Add `will-change: transform` (sparingly) |
+| Hairline icon beside bold text | Match the stroke width to the text weight |
+
+## Reporting
+
+**Severity.** `HIGH` breaks an interaction, makes motion unusable, or leaves a state change visible only while the animation runs. `MEDIUM` is a visible inconsistency in surfaces, icons, or motion. `LOW` is isolated polish.
+
+**Verification.** Without a browser: every state the component defines, meaning hover, focus, active, loading and empty, plus motion durations and easings read from the code. With one: walk each state, and replay motion at 10% speed in the browser's Animations panel. Report every check you could not run as `Not verified`.
+
+**Format.** Group findings under the principle each violates, ordered by severity, one row per root cause listing every location it appears in:
+
+| Severity | Location | Before | After | Why |
+| --- | --- | --- | --- | --- |
+
+`Location` is `path/to/file:line`. `Why` names the principle and the user impact.
+
+End with `Block` when any `HIGH` remains, `Approve` otherwise, leaving the rest in the table as work to do. Never `Approve` coverage you did not inspect. With nothing to report, state "No actionable UI-polish findings" and report verification.
diff --git a/.agents/skills/better-ui/agents/openai.yaml b/.agents/skills/better-ui/agents/openai.yaml
new file mode 100644
index 0000000..913f200
--- /dev/null
+++ b/.agents/skills/better-ui/agents/openai.yaml
@@ -0,0 +1,3 @@
+interface:
+ display_name: "Better UI"
+ short_description: "Design engineering details for polished interfaces"
diff --git a/.agents/skills/better-ui/animations.md b/.agents/skills/better-ui/animations.md
new file mode 100644
index 0000000..91430e6
--- /dev/null
+++ b/.agents/skills/better-ui/animations.md
@@ -0,0 +1,205 @@
+# Animations
+
+Interruptible transitions, press feedback and the restraint that decides whether to animate at all. Staged entrances and exits live in [enter-exit.md](enter-exit.md); icon swaps in [icon-transitions.md](icon-transitions.md).
+
+## Interruptible animations
+
+Users change intent mid-interaction. Animations that cannot be interrupted make the interface feel broken.
+
+### CSS transitions vs. keyframes
+
+| | CSS Transitions | CSS Keyframe Animations |
+| --- | --- | --- |
+| **Behavior** | Interpolate toward latest state | Run on a fixed timeline |
+| **Interruptible** | Yes, retargets mid-animation | No, restarts from beginning |
+| **Use for** | Interactive state changes (hover, toggle, open/close) | Staged sequences that run once (enter animations, loading) |
+| **Duration** | Fixed; retargets the value mid-flight, not the timeline | Fixed timeline, restarts from the beginning |
+
+```css
+/* Good: interruptible transition for a toggle */
+.drawer {
+ transform: translateX(-100%);
+ transition: transform 200ms ease-out;
+}
+.drawer.open {
+ transform: translateX(0);
+}
+
+/* Clicking again mid-animation smoothly reverses, no jank */
+```
+
+```css
+/* Bad: keyframe animation for interactive element */
+.drawer.open {
+ animation: slideIn 200ms ease-out forwards;
+}
+
+/* Closing mid-animation snaps or restarts, feels broken */
+```
+
+Prefer CSS transitions for interactive elements. Reserve keyframes for one-shot sequences.
+
+## Scale on press
+
+A subtle scale-down on click gives buttons tactile feedback. Always `scale(0.96)`, never below `0.95`, which feels exaggerated. Use CSS transitions so a release mid-press returns smoothly.
+
+Not every button needs it. Add a `static` prop that disables the scale where the motion would distract.
+
+### CSS example
+
+```css
+.button {
+ transition-property: scale;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+
+.button:active {
+ scale: 0.96;
+}
+```
+
+### Tailwind example
+
+```tsx
+
+```
+
+### Motion example
+
+```tsx
+
+ Click me
+
+```
+
+### Static prop pattern
+
+Extract the scale class into a variable and apply it conditionally on a `static` prop:
+
+```tsx
+const tapScale = "active:not-disabled:scale-[0.96]";
+
+function Button({ static: isStatic, className, children, ...props }) {
+ return (
+
+ );
+}
+
+// Usage
+ {/* scales on press */}
+ {/* no scale */}
+```
+
+## Skip animation on page load
+
+Use `initial={false}` on `AnimatePresence` to stop enter animations firing on first render. An element already in its default state animates on later state changes, not on page load.
+
+### When it works
+
+```tsx
+// Good: icon doesn't animate in on mount, only on state change
+
+
+
+
+
+```
+
+Works well for icon swaps, toggles, tabs and segmented controls, anything with a default state on page load.
+
+### When it breaks
+
+Never use `initial={false}` where the component relies on its `initial` prop for a first-time enter animation, such as a staggered page hero or a loading state. Removing it skips the entire entrance.
+
+```tsx
+// Bad: initial={false} would skip the staggered page enter entirely
+
+
+ ...
+
+
+```
+
+Verify the component still looks right on a full page refresh before applying this.
+
+## Suppress transitions on theme switch
+
+Flipping the theme changes `color`, `background-color`, `border-color` and `box-shadow` on nearly every element at once. Everything carrying a transition on those properties animates together, so the switch reads as a slow smear rather than an instant change. Disable transitions for the swap and restore them right after.
+
+Inject a stylesheet that turns off every transition, force a reflow so the new colors commit while it still applies, then drop it on the next frame:
+
+```tsx
+"use client";
+
+import { useEffect } from "react";
+
+export function DisableThemeTransitions() {
+ useEffect(() => {
+ const mql = window.matchMedia("(prefers-color-scheme: dark)");
+
+ const handleChange = () => {
+ const style = document.createElement("style");
+ style.append(
+ document.createTextNode(
+ "*,*::before,*::after{transition:none !important}"
+ )
+ );
+ document.head.append(style);
+
+ const _flushReflow = document.body.offsetHeight;
+
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => style.remove());
+ });
+ };
+
+ mql.addEventListener("change", handleChange);
+ return () => mql.removeEventListener("change", handleChange);
+ }, []);
+
+ return null;
+}
+```
+
+`document.body.offsetHeight` is read for its side effect, forcing a synchronous style flush so the new theme resolves while the override is still in the document and no transition starts. The nested `requestAnimationFrame` removes the override only after that paint, restoring transitions before the next interaction.
+
+That covers the OS-level change. An in-app toggle needs the same treatment around its own flip: apply the override, change the theme, flush, remove. `next-themes` ships this as `disableTransitionOnChange`.
+
+## Motion restraint
+
+Motion is a budget, not a garnish. Three rules decide whether an animation belongs at all:
+
+- **Give high-frequency interactions instant feedback instead of animation.** Every keystroke, every list-row hover, every tab switch in a work tool. Reserve expressive motion for infrequent moments (first load of a view, success states, empty states); high-frequency interactions get instant feedback or the subtlest possible transition (`opacity`/`background-color` at ≤150ms).
+- **Motion is never the only feedback channel.** Every state change an animation communicates stays visible when it doesn't run: a color change, an icon swap, a label. Users with reduced motion enabled and anyone who blinked still need to see what happened.
+- **Brief and precise beats prominent.** Where a shorter, smaller animation says the same thing, use it. When in doubt cut the duration, not the clarity.
+
+```css
+/* Good: high-frequency hover gets a minimal transition */
+.row:hover {
+ background-color: var(--surface-hover);
+ transition: background-color 100ms ease-out;
+}
+
+/* Bad: every hover replays a full entrance */
+.row:hover .row-icon {
+ animation: bounceIn 500ms;
+}
+```
+
diff --git a/.agents/skills/better-ui/enter-exit.md b/.agents/skills/better-ui/enter-exit.md
new file mode 100644
index 0000000..6d353d7
--- /dev/null
+++ b/.agents/skills/better-ui/enter-exit.md
@@ -0,0 +1,147 @@
+# Enter and exit animations
+
+Staged entrances and the exits that follow them. For interactive state feedback see [animations.md](animations.md); for icon swaps see [icon-transitions.md](icon-transitions.md).
+
+## Enter animations: split and stagger
+
+Use this for infrequent staged entrances where sequence communicates hierarchy: the first load of a page hero, a success state, an empty state. Break a large container into semantic chunks and animate each one. Never stagger routine interactions such as row hovers, keystrokes, or repeated tab changes.
+
+### Step by step
+
+1. **Split** into logical groups (title, description, buttons)
+2. **Stagger** with ~100ms delay between groups
+3. **For titles**, consider splitting into individual words with ~80ms stagger
+4. **Combine** `opacity`, `blur` and `translateY` for the enter effect
+
+### Code example
+
+```tsx
+// Motion (Framer Motion): staggered enter
+function PageHeader() {
+ return (
+
+
+ Welcome
+
+
+
+ A description of the page.
+
+
+
+
+
+
+ );
+}
+```
+
+### CSS-only stagger
+
+```css
+.stagger-item {
+ opacity: 0;
+ transform: translateY(12px);
+ filter: blur(4px);
+ animation: fadeInUp 400ms ease-out forwards;
+}
+
+.stagger-item:nth-child(1) { animation-delay: 0ms; }
+.stagger-item:nth-child(2) { animation-delay: 100ms; }
+.stagger-item:nth-child(3) { animation-delay: 200ms; }
+
+@keyframes fadeInUp {
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ filter: blur(0);
+ }
+}
+```
+
+## Exit animations
+
+Exits are softer and less attention-grabbing than enters. The user's focus is moving to the next thing, so do not fight for it.
+
+### Subtle exit (recommended)
+
+```tsx
+// Small fixed translateY: indicates direction without drama
+
+ {content}
+
+```
+
+### Full exit (when context matters)
+
+```tsx
+// Slide fully out: use when spatial context is important
+// (e.g., a card returning to a list, a drawer closing)
+
+ {content}
+
+```
+
+### Good vs. bad
+
+```css
+/* Good: subtle exit */
+.item-exit {
+ opacity: 0;
+ transform: translateY(-12px);
+ transition: opacity 150ms ease-out, transform 150ms ease-out;
+}
+
+/* Bad: dramatic exit that steals focus */
+.item-exit {
+ opacity: 0;
+ transform: translateY(-100%) scale(0.5);
+ transition: all 400ms ease-out;
+}
+
+/* Sometimes correct: remove immediately when motion adds no context */
+.item-exit {
+ display: none;
+}
+```
+
+**Key points:**
+- Use a small fixed `translateY`, say `-12px`, rather than the full container height
+- Keep some directional movement to indicate where the element went
+- Exit duration should be shorter than enter duration (150ms vs 300ms)
+- Use a subtle exit when it preserves spatial context. Remove immediately when motion adds no information, the interaction repeats frequently, or reduced motion is requested.
+
diff --git a/.agents/skills/better-ui/icon-transitions.md b/.agents/skills/better-ui/icon-transitions.md
new file mode 100644
index 0000000..fe266de
--- /dev/null
+++ b/.agents/skills/better-ui/icon-transitions.md
@@ -0,0 +1,102 @@
+# Icon transitions
+
+Cross-fading an icon when it changes contextually or by state, with and without a motion library. Icon weight, color and direction live in [icons.md](icons.md).
+
+## Contextual icon animations
+
+When icons appear or disappear contextually, on hover or a state change, animate them with `opacity`, `scale` and `blur` rather than toggling visibility.
+
+### Motion example
+
+This uses the `motion` package. Where the project has `framer-motion`, import the same APIs from `"framer-motion"`. Never mix an installed package with the other's import path.
+
+```tsx
+import { AnimatePresence, motion } from "motion/react";
+
+function IconButton({ isActive, icon: Icon }) {
+ return (
+
+ );
+}
+```
+
+### CSS transition approach (no Motion)
+
+Without Motion or Framer Motion, keep both icons in the DOM and cross-fade with CSS transitions. Neither unmounts, so enter and exit both animate smoothly.
+
+One icon is absolutely positioned on top of the other. Toggling state cross-fades them, the entering icon scaling up from `0.25` while the exiting one scales down to `0.25`, both with opacity and blur.
+
+```tsx
+function IconButton({ isActive, ActiveIcon, InactiveIcon }) {
+ return (
+
+ );
+}
+```
+
+The non-absolute icon, `InactiveIcon`, defines the layout size. The absolute one, `ActiveIcon`, overlays it without affecting flow.
+
+### Choosing between Motion and CSS
+
+| | Motion (Framer Motion) | CSS transitions (both icons in DOM) |
+| --- | --- | --- |
+| **Enter animation** | Yes | Yes |
+| **Exit animation** | Yes (via `AnimatePresence`) | Yes (cross-fade, icon never unmounts) |
+| **Spring physics** | Yes | No, use `cubic-bezier(0.2, 0, 0, 1)` as approximation |
+| **When to use** | Project already uses `motion` or `framer-motion` | No motion dependency, or keeping bundle small |
+
+Check the project's `package.json`. Import from `"motion/react"` when `motion` is installed, or `"framer-motion"` when that is. Where both exist, follow the imports the component or its nearest peers already use. Where neither is present, use the CSS cross-fade and never add a dependency just for icon transitions.
+
+### When to animate icons
+
+| Animate | Don't animate |
+| --- | --- |
+| Icons that appear on hover (action buttons) | Static navigation icons |
+| State change icons (play → pause, like → liked) | Decorative icons |
+| Icons in contextual toolbars | Icons that are always visible |
+| Loading/success state indicators | Icon labels (text next to icon) |
+
+Use exactly these values for contextual icon animations. Do not deviate:
+- `scale`: `0.25` → `1` (never use `0.5` or `0.6`)
+- `opacity`: `0` → `1`
+- `filter`: `"blur(4px)"` → `"blur(0px)"`
+- `transition`: `{ type: "spring", duration: 0.3, bounce: 0 }`; **bounce must always be `0`**, never `0.1` or any other value
+
diff --git a/.agents/skills/better-ui/icons.md b/.agents/skills/better-ui/icons.md
new file mode 100644
index 0000000..e425fba
--- /dev/null
+++ b/.agents/skills/better-ui/icons.md
@@ -0,0 +1,110 @@
+# Icons
+
+Icon weight, states, sizing and direction, the details that make icons sit naturally in an interface.
+
+## Match icon stroke to text weight
+
+A hairline icon beside semibold text reads as broken; a heavy icon beside regular text shouts.
+
+| Adjacent text | Icon stroke width (24px grid) |
+| --- | --- |
+| Regular (400), 14–16px | `1.5px` |
+| Medium/Semibold (500–600) | `2px` |
+| Bold (700), or emphasized standalone | `2.5px` |
+
+```html
+
+
+
+
+
+```
+
+Two related consistency rules:
+
+- **One optical strategy per surface.** Never mix icon libraries with incompatible stroke conventions on one toolbar. Where the library supports stroke variants, match them to adjacent text as above; otherwise keep the set's native stroke and use size or color for emphasis.
+- **Size icons relative to the text's cap height**, typically `1em`–`1.25em` when inline with text, so the pair scales together.
+
+## One SVG, recolored per state
+
+Never ship separate assets for default, hover, selected and disabled states. Use one SVG drawn with `currentColor` and let CSS state drive the color:
+
+```html
+
+
+```
+
+```css
+.icon-button { color: oklch(0.552 0.016 285.938); }
+.icon-button:hover { color: oklch(0.21 0.006 285.885); }
+.icon-button[aria-pressed="true"] { color: oklch(0.623 0.188 259.815); }
+.icon-button:disabled { opacity: 0.4; }
+```
+
+```html
+
+
+```
+
+Hardcoded fills inside the SVG, such as `fill="#666"`, break this. Strip them to `currentColor` when importing icons.
+
+## Outline default, fill active
+
+Where an icon set offers outline and filled variants, use them as a state pair, never interchangeably:
+
+| Variant | Use for |
+| --- | --- |
+| Outline | Default state: toolbars, list rows, inline with text |
+| Fill | Selected/active state: the active tab, a toggled bookmark, a liked heart |
+
+```tsx
+// Good: variant communicates state
+
+
+// Bad: filled icons everywhere, so the active tab has no state signal
+
+```
+
+The swap between variants is a contextual icon animation. Use the exact cross-fade values in [icon-transitions.md](icon-transitions.md).
+
+## Design at render size
+
+An icon that looks great at 48px collapses into mush at 16px. Thin interior lines, tight counters and fine texture all blur or alias when small.
+
+- Test every icon at the smallest size it will render, often `16px`. It must stay recognizable there.
+- Prefer simplified glyphs for small contexts over scaling down detailed artwork.
+- Keep icons on the pixel grid at their render size. A 16px icon drawn on a 24px grid with fractional scaling renders soft, so use the set's native grid sizes (`16`, `20`, `24`) rather than arbitrary scales.
+- Always SVG, never raster, so the same asset stays crisp at every density.
+
+## Icons in RTL
+
+Under `dir="rtl"`, flip icons whose meaning is tied to reading direction, and leave the rest alone:
+
+| Flip | Don't flip |
+| --- | --- |
+| Back/forward arrows, chevrons in navigation | Logos and brand marks |
+| Text-block glyphs (alignment, lists, indent) | Checkmarks |
+| Speaker/volume waves (emanate in reading direction) | Physical objects: clocks, cups, pencils |
+| "Send" style directional glyphs | Media playback (play/rewind refer to tape direction, convention keeps them LTR) |
+
+```css
+/* Good: mirror only direction-dependent icons */
+[dir="rtl"] .icon-directional {
+ scale: -1 1;
+}
+```
+
+```html
+
+
+```
+
+Analyze composite icons part by part. A badge or slash overlay may keep its position even when the base glyph flips. Accessible names for icon-only buttons belong to `better-accessibility`.
diff --git a/.agents/skills/better-ui/performance.md b/.agents/skills/better-ui/performance.md
new file mode 100644
index 0000000..e32ce47
--- /dev/null
+++ b/.agents/skills/better-ui/performance.md
@@ -0,0 +1,88 @@
+# Performance
+
+Transition specificity and GPU compositing hints.
+
+## Transition only what changes
+
+Never use `transition: all` or Tailwind's `transition-all`. Always name the exact properties that change. Tailwind's bare `transition` maps to a curated list of colors, opacity, shadow and transforms rather than `all`, and naming what changes is still better.
+
+### Why
+
+- `transition: all` forces the browser to watch every property for changes
+- Causes unexpected transitions on properties you didn't intend to animate (colors, padding, shadows)
+- Prevents browser optimizations
+
+### CSS example
+
+```css
+/* Good: only transition what changes */
+.button {
+ transition-property: scale, background-color;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+
+/* Bad: transition everything */
+.button {
+ transition: all 150ms ease-out;
+}
+```
+
+### Tailwind
+
+```tsx
+// Good: explicit properties
+