diff --git a/.gitignore b/.gitignore
index 7f2dbf3..d67fcf0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ node_modules
/.svelte-kit
/build
+
# OS
.DS_Store
Thumbs.db
diff --git a/AGENTS.md b/AGENTS.md
index 6462ca9..cddbb45 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -18,11 +18,15 @@
## Auth
-| Role | Auth | Session |
-| ---------------- | -------------------------- | --------------------------- |
-| Super admin (me) | PB email+pass | 24hr JWT |
-| Fam admin | PB email+pass | 24hr JWT, scoped to own fam |
-| Member | Invite code + device token | localStorage, no expiry |
+| Role | Auth | Session | Record in |
+| ---------------- | -------------------------- | --------------------------- | -------------------- |
+| Admin (parent) | PB email+pass | 24hr JWT | `fam_admins` |
+| Member (child) | Invite code + device token | `device_token` cookie only | `members` |
+
+- **Admins** (parents) have a PB auth record + `fam_admins` record. They authenticate via email/password login, get a session cookie (`session`) with `{ famId, userId, famSlug, memberName, role: "parent" }`.
+- **Members** (children) exist only in the `members` collection. They authenticate via invite code + device token (SHA-256 hashed). The `device_token` cookie is set on join; no session cookie.
+- **Platform superuser** (`_superusers`) used only server-side by `pb-admin.ts` for cross-family queries (e.g. `/admin` stats dashboard). Not an app role.
+- The layout (`[fam]/+layout.server.ts`) derives `isParent` and `role` centrally from the session cookie — child pages use `page.data.isParent` or `page.data.role` from `$app/state`.
## PB Collections (all scoped by `famId`)
@@ -42,9 +46,15 @@
/ Landing (SaaS marketing)
/admin Admin panel - statistic dashboard, and any donations made
/join/:code Member invite code
+/join/:code/:member Member invite with pre-selected member
/{fam} Fam dashboard
/{fam}/admin Admin panel
-/{fam}/:username Member kanban (?token= for auth)
+/{fam}/:username Member kanban & admin dashboard (role determined by session)
+/{fam}/:username/preferences User preferences (admin→fam_admins, member→members)
+/{fam}/:username/settings Family admin settings (session required)
+/{fam}/:username/chores Chore templates & assignment grid
+/{fam}/:username/rewards Rewards overview
+/{fam}/:username/bonuses Bonus configs & evaluation
/api/* Hono proxy (webhooks, CRON)
```
@@ -58,11 +68,82 @@
- **WhatsApp:** Deferred — Hono CRON handler has pluggable notification interface
- **UI reactivity:** Svelte `$state` / `$derived` / `$effect` — no PB SDK `.subscribe()` / SSE
+## Env Vars (SvelteKit 3.0.0-next.4)
+
+Env vars must be declared in `frontend/src/env.ts` using `defineEnvVars` from `@sveltejs/kit/hooks`:
+
+```ts
+import { defineEnvVars } from '@sveltejs/kit/hooks';
+export const variables = defineEnvVars({
+ DEBUG_RECORD_ID: {},
+ SERVER_IP: {public: true},
+ PB_PORT: {public: true}
+});
+```
+
+Only vars with `{public: true}` are exposed to client-side code via `$app/env/public`. If you need a new client-side env var (e.g., `PUBLIC_PB_URL`), you must:
+1. Add it to `.env` with `PUBLIC_` prefix
+2. Add it to `frontend/src/env.ts` with `{public: true}`
+
+The actual values come from `.env` (symlinked from project root at `frontend/.env -> ../.env`). Server-only env vars (no `{public}` flag) are available via `$app/env/private` but only in server modules.
+
+## ⚠️ CRITICAL: Reactivity Pattern (NEVER use `$effect` to sync from famStore)
+
+**Do NOT do this:**
+```svelte
+
+let items = $state(data.items);
+$effect(() => {
+ if (data.items?.length) items = data.items;
+ if (famStore.initialized && famStore.items.length) items = famStore.items;
+});
+```
+
+**Do this instead:**
+```svelte
+
+let items = $state(famStore.initialized ? famStore.items : (data.items || []));
+```
+
+**Form action callbacks must call `famStore.applyRecord()`:**
+```ts
+// In use:enhance callback:
+famStore.applyRecord('collection_name', record, 'create' | 'update' | 'delete');
+```
+
+The `famStore` has reactive `$state` properties and an `applyRecord()` method designed for instant UI feedback from form actions. Calling `applyRecord()` mutates the store directly — the UI updates immediately without waiting for PB SSE. The SSE subscription is a backup for multi-user sync only. Do NOT add `$effect` watchers to bridge the gap between form actions and reactive state.
+
+## UI Component Conventions
+
+All admin and member pages use the following pattern:
+
+```svelte
+
+
+
+
+
+
+```
+
+- **Layout shell** lives in `[fam]/+layout.svelte` — Sidebar, TopNav, Footer. All fam-scoped routes inherit it.
+- **Sidebar** role-aware: shows admin CTAs on `/admin/*`, member CTAs on `/[username]/*`.
+- **TopNav** has `announcement` (center slot) and `actions` (right slot).
+- **`Card` `cols` prop**: `1 | 2 | 3` — spans that many columns in the 3-column `CardGrid`.
+- **`Button`** for all CTAs: `