--- paths: - "app/components/**/*.tsx" - "app/_ui/**/*.tsx" --- # Component Rules ## SSR animation safety (critical) **Never** use `initial={{ opacity: 0 }}` on server-rendered elements. This bakes `style="opacity:0"` into HTML — content is invisible if hydration fails. Use `ScrollFadeIn` instead: ```tsx import ScrollFadeIn from "@/app/components/ScrollFadeIn" ``` `AnimatePresence` is fine for modals and overlays that only appear after user interaction. ## Design system (Plakat) - Colors: only the Plakat tokens `signal`, `ink`, `paper`, `paper-2`, `ok` — no hex, no raw Tailwind palette, no `liquid-*` (left over only in admin and 404) - Ink and paper swap roles in dark mode on their own; `signal` stays red - Fonts: `font-display` (Bricolage, `[font-variation-settings:'wdth'_80]` for posters), `font-sans` (Geist), `font-mono` (Geist Mono) - Cards: `rounded-[18px] border-2 border-ink` + hard offset shadow `shadow-[6px_6px_0_rgb(var(--plakat-ink))]` - Pills: `rounded-full` with `shadow-[inset_0_0_0_1.5px_rgb(var(--plakat-ink))]`, filled variant `bg-ink text-paper` - Labels: `font-mono text-[13px]`; nothing below 12px - Subpages: wrap in `app/_ui/SubpageShell.tsx`, reuse `SubpageIntro`, `btn`, `ghost`, `card` from `app/_ui/subpage.tsx`; long text in `.plakat-prose` - Motion: `ease-out` / `ease-drawer`, press feedback `active:scale-[0.97]`, never `transition-all` ## Async components Every component that fetches data must have a Skeleton loading state shown while data loads. ## i18n - Client: `useTranslations("namespace")` from `next-intl` - Server: `getTranslations("namespace")` from `next-intl/server` - New client sections need a wrapper in `ClientWrappers.tsx` with scoped `NextIntlClientProvider` ## TypeScript - No `any` — define interfaces in `lib/directus.ts` or `types/` - No emojis in code