# Zync.is Public Marketing & Auth Site (`zync-www`)

**Spec:** 34
**Date:** 2026-05-31
**Status:** Draft
**Depends on:** `foundation-auth-rbac`, `foundation-design-system`, `foundation-monorepo`, `zync-subscription`, `system-i18n`
**Referenced by:** `foundation-auth-rbac` (signup redirect), `2026-05-31-onboarding` (post-signup entry point)

---

## Overview

`zync-www` is the public-facing Astro site at `zync.is`. It covers marketing (landing, pricing), authentication (login, signup, password reset), and invitation acceptance. It is entirely separate from `app.zync.is` (`apps/zync-app`), which is the authenticated React SPA.

All pages are rendered statically (Astro `output: 'static'`) with React islands for interactive form components. This maximises SEO crawlability — no server-rendered auth gates, no JS-blocked content for bots.

The site is Hebrew-first. `<html dir="rtl" lang="he">` on every page. All layout uses logical CSS properties (`margin-inline-start`, `padding-inline-end`, Tailwind `ms-*`/`me-*`/`ps-*`/`pe-*`).

---

## Project Scaffold

Location: `apps/zync-www/`

### Core Dependencies

```jsonc
// apps/zync-www/package.json (key deps)
{
  "dependencies": {
    "astro": "^4.x",
    "@astrojs/react": "^3.x",
    "react": "^18.x",
    "react-dom": "^18.x",
    "react-hook-form": "^7.x",
    "zod": "^3.x",
    "@hookform/resolvers": "^3.x"
  },
  "devDependencies": {
    "@zync/config": "workspace:*",
    "@zync/ui": "workspace:*",
    "tailwindcss": "^3.x",
    "typescript": "^5.x"
  }
}
```

### Astro Config

```ts
// apps/zync-www/astro.config.ts
import { defineConfig } from 'astro/config'
import react from '@astrojs/react'

export default defineConfig({
  output: 'static',
  integrations: [react()],
  i18n: {
    defaultLocale: 'he',
    locales: ['he'],
    routing: { prefixDefaultLocale: false },
  },
  vite: {
    css: {
      // tokens from @zync/ui
    },
  },
})
```

### Tailwind Config

Inherits `packages/config/tailwind.preset.ts` so OKLCH tokens and the 8px spacing grid are enforced identically to `apps/zync-app`. No additional color definitions in `zync-www`.

```ts
// apps/zync-www/tailwind.config.ts
import preset from '@zync/config/tailwind.preset'
import type { Config } from 'tailwindcss'

export default {
  presets: [preset],
  content: ['./src/**/*.{astro,ts,tsx}'],
} satisfies Config
```

---

## Route List

| Route | Page | Rendering | Purpose |
|---|---|---|---|
| `/` | Landing | Static | Marketing homepage — hero, features, pricing preview, CTA |
| `/pricing` | Pricing | Static | Full tier comparison table, monthly/annual toggle, FAQ |
| `/login` | Login | Static + React island | Email + password login → redirect to app |
| `/signup` | Signup | Static + React island | New tenant registration → creates tenant → onboarding |
| `/reset-password` | Reset Request | Static + React island | Enter email to receive reset link |
| `/reset-password/confirm` | Reset Confirm | Static + React island | Consume `?token=` param, set new password |
| `/invite/accept` | Invite Accept | Static + React island | Consume `?token=` param, accept team invite |
| `404` | Not Found | Static | Global 404 page |

---

## Environment Variables

No new secrets. Authentication is handled entirely by `apps/zync-api`.

| Variable | Description |
|---|---|
| `PUBLIC_API_BASE_URL` | e.g. `https://api.zync.is` — base URL for all `POST /api/auth/*` calls |
| `PUBLIC_APP_URL` | e.g. `https://app.zync.is` — redirect target after successful auth |

Both are `PUBLIC_` (inlined at build time by Astro, safe to expose).

---

## Design Token Reference

All values from `packages/ui/src/tokens/index.css`. No hardcoded hex/rgb/hsl anywhere in `zync-www`.

| Token | Value | Usage |
|---|---|---|
| `--bg` | `oklch(100% 0 0)` | Page background |
| `--surface` | `oklch(95% 0.016 195)` | Card, form background |
| `--ink` | `oklch(12% 0.04 240)` | Body text |
| `--ink-soft` | `oklch(22% 0.045 240)` | Secondary text, labels |
| `--accent` | `oklch(44% 0.12 195)` | Primary CTA, links |
| `--danger` | `oklch(44% 0.19 25)` | Error states |
| `--radius` | `4px` | All rounded corners |

Spacing: 8px grid. Allowed values: 8, 16, 24, 32, 48, 64, 96px only.

---

## Page 1: Landing Page (`/`)

### Purpose

Introduce the product to prospective customers. Drive signups. No logo strip. No bento grid. No centered hero. No gradient backgrounds.

### Rendering

Static. Zero client-side JavaScript outside of React islands. Astro components for all layout.

### Layout Structure

12-column grid. All sections on a `--bg` background.

#### Sections (in order)

1. `<Nav>` — sticky, 12-col, logo start + links + "התחבר" / "התחל בחינם" end
2. `<Hero>` — asymmetric two-column
3. `<FeatureSection>` × 3 — alternating left/right screenshot + copy
4. `<PricingPreview>` — 3 tier cards, links to `/pricing`
5. `<Footer>`

### ASCII Wireframe — Hero

RTL layout (start = right, end = left):

```
┌──────────────────────────────────────────────────────────────────────────┐
│ COL:  1    2    3    4    5    6    7    8    9   10   11   12           │
├──────────────────────────────────────────────────────────────────────────┤
│       ┌─────────────────────────────────┐   ┌────────────────────────┐  │
│       │                                 │   │                        │  │
│       │  H1: ניהול פרויקטים שמבין אותך  │   │  [App screenshot /     │  │
│       │      col-start-2 col-end-8      │   │   dashboard mockup]    │  │
│       │                                 │   │                        │  │
│       │  P: תיאור קצר, שני משפטים       │   │  col-start-8 col-end-12│  │
│       │     col-start-2 col-end-7       │   │                        │  │
│       │                                 │   │  No gradient overlay.  │  │
│       │  [התחל בחינם ←]                 │   │  Static PNG/WebP.      │  │
│       │                                 │   │                        │  │
│       └─────────────────────────────────┘   └────────────────────────┘  │
│                                                                          │
│       48px top padding. 96px bottom padding.                             │
└──────────────────────────────────────────────────────────────────────────┘
```

Rules:
- H1: `col-start-2 col-end-8` (6 cols wide, offset 1 from edge)
- Subheadline/body: `col-start-2 col-end-7`
- Screenshot: `col-start-8 col-end-12` (static image, no animation)
- Single CTA: "התחל בחינם" → `/signup`
- No secondary CTA stacked beneath. One CTA, full stop.

### Feature Sections

Three sections. Each alternates screenshot side.

**Section 1 — text start (right), screenshot end (left) in RTL:**

```
┌──────────────────────────────────────────────────────────────────────────┐
│  ┌────────────────────────┐   ┌──────────────────────────────────────┐  │
│  │                        │   │                                      │  │
│  │  [Screenshot]          │   │  H2: ניהול משימות                    │  │
│  │  col-start-2 col-end-6 │   │  P: תיאור קצר                       │  │
│  │                        │   │  col-start-7 col-end-12              │  │
│  └────────────────────────┘   └──────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────────────┘
```

**Section 2 — text end (left), screenshot start (right) in RTL:**

```
┌──────────────────────────────────────────────────────────────────────────┐
│  ┌──────────────────────────────────────┐   ┌────────────────────────┐  │
│  │                                      │   │                        │  │
│  │  H2: ניהול לקוחות וחיוב              │   │  [Screenshot]          │  │
│  │  P: תיאור קצר                        │   │  col-start-8 col-end-12│  │
│  │  col-start-2 col-end-7               │   │                        │  │
│  │                                      │   └────────────────────────┘  │
│  └──────────────────────────────────────┘                                │
└──────────────────────────────────────────────────────────────────────────┘
```

**Section 3 — API & Integrations pitch (Business tier):**

```
┌──────────────────────────────────────────────────────────────────────────┐
│  ┌────────────────────────┐   ┌──────────────────────────────────────┐  │
│  │                        │   │                                      │  │
│  │  [Screenshot: API key  │   │  H2: חברו את זינק לכלים שלכם       │  │
│  │   settings page /      │   │                                      │  │
│  │   code sample snippet] │   │  P: גישת REST API מלאה מאפשרת       │  │
│  │  col-start-2 col-end-6 │   │     לחבר זינק לכל מערכת חיצונית —  │  │
│  │                        │   │     ERP, כלי אוטומציה, או מערכות    │  │
│  └────────────────────────┘   │     שפיתחתם בעצמכם.                 │  │
│                                │                                      │  │
│                                │  [צפו בתיעוד ה-API →] /pricing      │  │
│                                │  col-start-7 col-end-12              │  │
│                                └──────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────────────┘
```

- H2: `col-start-7 col-end-12` (RTL: text on left, screenshot on right)
- Body copy: two sentences max. No bullet lists.
- CTA label: "צפו בתיעוד ה-API" → `/pricing` (anchors to integrations row)
- Screenshot: static PNG of API key creation screen or a code sample showing a `curl` request. No animation.
- Badge above H2 (small, `--accent-soft` bg, `--accent` text): "תוכנית עסקי ומעלה"

One CTA per feature section maximum. CTAs point to `/signup` or a deeper anchor.

### Pricing Preview Section

3 cards: Freelancer, Business (badge: "הכי פופולרי"), Enterprise. Links to `/pricing` for full details.

```
┌──────────────────────────────────────────────────────────────────────────┐
│   H2: "בחר תוכנית"                              col-start-2 col-end-8   │
│                                                                          │
│   ┌────────────────┐  ┌────────────────────┐  ┌────────────────┐        │
│   │  Freelancer    │  │  Business          │  │  Enterprise    │        │
│   │  חינם לנצח     │  │  [הכי פופולרי]     │  │  צרו קשר       │        │
│   │                │  │  89 ₪/חודש         │  │                │        │
│   │  [CTA]         │  │  [CTA]             │  │  [CTA]         │        │
│   └────────────────┘  └────────────────────┘  └────────────────┘        │
│                                                                          │
│   "לפרטים מלאים" → /pricing                                              │
└──────────────────────────────────────────────────────────────────────────┘
```

### Nav Component

```
┌──────────────────────────────────────────────────────────────────────────┐
│  col-start-1 col-end-13                                                  │
│  ┌──────────────┬──────────────────────────────────┬───────────────┐    │
│  │  [Zync logo] │  מוצר   תמחור   (empty space)   │  התחבר  |  התחל│    │
│  └──────────────┴──────────────────────────────────┴───────────────┘    │
│  bg: --surface, border-b: 1px solid var(--line)                 │
│  Sticky. No blur/frosted-glass effect.                                   │
└──────────────────────────────────────────────────────────────────────────┘
```

### Footer Component

Single row. Logo + legal links start. Copyright end. No social icons. No decorative elements.

### Landing Page Component Breakdown

| Component | Type | Location |
|---|---|---|
| `<Nav>` | Astro | `src/components/Nav.astro` |
| `<Hero>` | Astro | `src/components/landing/Hero.astro` |
| `<FeatureSection>` | Astro | `src/components/landing/FeatureSection.astro` |
| `<PricingPreview>` | Astro | `src/components/landing/PricingPreview.astro` |
| `<PricingCard>` | Astro | `src/components/landing/PricingCard.astro` |
| `<Footer>` | Astro | `src/components/Footer.astro` |

All are pure Astro (no React). No client-side JS on the landing page.

### SEO

```html
<title>Zync — ניהול פרויקטים לעצמאיים ועסקים בישראל</title>
<meta name="description" content="Zync מאחדת משימות, לקוחות, חיובים וזמן בממשק אחד. לעצמאיים ועסקים קטנים. חינם לצמיתות." />
```

---

## Page 2: Pricing Page (`/pricing`)

### Purpose

Full tier comparison. Monthly/annual toggle. Feature matrix. FAQ.

### Rendering

Static Astro page. Toggle interactivity via a minimal React island (`<PricingToggle>`). No SSR.

### ASCII Wireframe — Pricing Hero + Cards

```
┌──────────────────────────────────────────────────────────────────────────┐
│  H1: תמחור                          col-start-2 col-end-8               │
│  P: ללא כרטיס אשראי. ביטול בכל עת.                                       │
│                                                                          │
│  [ חודשי | שנתי  ] ← <PricingToggle> React island                       │
│                      "שנתי — חיסכון של חודשיים"                         │
│                                                                          │
│  ┌─────────────────┐ ┌──────────────────────┐ ┌─────────────────┐       │
│  │  Freelancer     │ │  Business            │ │  Enterprise     │       │
│  │  ─────────────  │ │  ─────────────────── │ │  ─────────────  │       │
│  │  חינם לנצח      │ │  [הכי פופולרי ★]     │ │  159 ₪/חודש    │       │
│  │                 │ │  89 ₪/חודש           │ │  (132 שנתי)     │       │
│  │  משתמש בודד     │ │  (74 ₪/חודש שנתי)   │ │                 │       │
│  │  פרויקטים ללא   │ │  עד 8 משתמשים        │ │  עד 15 משתמשים  │       │
│  │  הגבלה          │ │  כל המודולים         │ │  כל המודולים    │       │
│  │                 │ │                      │ │                 │       │
│  │  [התחל בחינם]   │ │  [שדרג עכשיו]        │ │  [צרו קשר]      │       │
│  └─────────────────┘ └──────────────────────┘ └─────────────────┘       │
└──────────────────────────────────────────────────────────────────────────┘
```

Notes:
- Annual price is monthly equivalent (89 × 10 / 12 = 74.17 → display 74 ₪/חודש, billed annually)
- Enterprise and White Label: "צרו קשר" button → `mailto:sales@zync.is`
- White Label tier: not shown in cards. Mentioned in FAQ only ("עסקים גדולים או ממותגים — צרו קשר")

### Tier Card Spec

Each card uses `<Card>` primitive from `@zync/ui`.

| Field | Freelancer | Business | Enterprise |
|---|---|---|---|
| Display name | פרילנסר | עסקי | ארגוני |
| Monthly price (monthly billing) | חינם | ₪89 | ₪159 |
| Monthly price (annual billing) | חינם | ₪74 | ₪132 |
| Annual price (lump sum) | — | ₪888/שנה | ₪1,584/שנה |
| Badge | — | "הכי פופולרי" | — |
| CTA label | התחל בחינם | שדרג עכשיו | צרו קשר |
| CTA action | `/signup` | `/signup` | `mailto:sales@zync.is` |
| Team members | 1 | עד 8 | עד 15 |

Business card: `border-color: var(--accent)`. All cards: `--radius: 4px`.

### Feature Comparison Table

Below the tier cards. Full-width. Rows are feature groups.

```
┌──────────────────────────────────────────────────────────────────────────┐
│  Feature                          │ פרילנסר │ עסקי │ ארגוני             │
│  ─────────────────────────────────┼─────────┼──────┼────────            │
│  GROUP: ניהול ליבה                │         │      │                    │
│  פרויקטים ומשימות                 │    ✓    │  ✓   │  ✓                │
│  לוח קנבן                         │    ✓    │  ✓   │  ✓                │
│  ניהול זמן                        │    ✓    │  ✓   │  ✓                │
│  ─────────────────────────────────┼─────────┼──────┼────────            │
│  GROUP: פיננסים                   │         │      │                    │
│  חשבוניות ללקוחות                 │    ✓    │  ✓   │  ✓                │
│  ניהול הוצאות                     │    ✓    │  ✓   │  ✓                │
│  תשלומים לקבלנים                  │    —    │  ✓   │  ✓                │
│  ─────────────────────────────────┼─────────┼──────┼────────            │
│  GROUP: צוות                      │         │      │                    │
│  מספר משתמשים                     │    1    │  8   │  15               │
│  הרשאות תפקידים                   │    —    │  ✓   │  ✓                │
│  לוח בקרה מנהלים                  │    —    │  ✓   │  ✓                │
│  ─────────────────────────────────┼─────────┼──────┼────────            │
│  GROUP: אינטגרציות ו-API          │         │      │                    │
│  אינטגרציות לוח שנה               │    ✓    │  ✓   │  ✓                │
│  REST API + מפתחות API            │    —    │  ✓   │  ✓                │
│  White Label / ממותג              │    —    │  —   │  בנפרד            │
│  ─────────────────────────────────┼─────────┼──────┼────────            │
│  GROUP: תמיכה                     │         │      │                    │
│  תמיכה בדוא"ל                     │    ✓    │  ✓   │  ✓                │
│  עדיפות בתמיכה                    │    —    │  ✓   │  ✓                │
│  מנהל לקוח ייעודי                 │    —    │  —   │  ✓                │
└──────────────────────────────────────────────────────────────────────────┘
```

Checkmarks: Hebrew "✓" / dash "—". No icon libraries. Plain Unicode.

### Annual Discount Logic

The `<PricingToggle>` React island holds `billingPeriod: 'monthly' | 'annual'` in local state. On toggle, it updates displayed prices. No API call on the pricing page — prices are hardcoded constants.

```ts
// Pricing constants (src/lib/pricing.ts)
export const TIERS = {
  business: { monthly: 89, annualPerMonth: 74, annualLump: 888 },
  enterprise: { monthly: 159, annualPerMonth: 132, annualLump: 1584 },
} as const
```

Annual = monthly × 10 (2 months free). annualPerMonth = annualLump / 12 (rounded down).

### FAQ Section

Below comparison table. 5 items. `<details>`/`<summary>` native HTML (no JS accordion).

Questions:
1. האם יש תקופת ניסיון? — 14 ימי ניסיון חינם בתוכנית עסקי. לא נדרש כרטיס אשראי.
2. איך מבצעים שדרוג? — מתוך הגדרות החשבון, תחת "תוכנית". שינויים מיידיים, חיוב יחסי.
3. איך מבצעים ביטול? — ניתן לבטל בכל עת מהגדרות. החשבון נשאר פעיל עד סוף תקופת החיוב.
4. מה ההבדל בין ארגוני ל-White Label? — ארגוני מיועד לצוותים גדולים. White Label מאפשר לסמן את הפלטפורמה במותג שלכם — פרטים בפנייה ישירה.
5. תשלום — אילו אמצעי תשלום מקובלים? — כרטיסי אשראי. חיוב בשקל ישראלי (₪). אין עמלות המרה.

### Pricing Page Component Breakdown

| Component | Type | Location |
|---|---|---|
| `<PricingToggle>` | React island (`client:load`) | `src/components/pricing/PricingToggle.tsx` |
| `<TierCard>` | Astro | `src/components/pricing/TierCard.astro` |
| `<ComparisonTable>` | Astro | `src/components/pricing/ComparisonTable.astro` |
| `<PricingFAQ>` | Astro | `src/components/pricing/PricingFAQ.astro` |

### SEO

```html
<title>תמחור — Zync</title>
<meta name="description" content="פרילנסר, עסקי וארגוני. החל מחינם לצמיתות. ראה מה כלול בכל תוכנית." />
```

---

## Page 3: Login Page (`/login`)

### Purpose

Authenticate existing user. Redirect to app on success.

### Rendering

Static Astro page with a single `<LoginForm>` React island (`client:load`).

### Layout

Single-column auth layout. No sidebar. Max-width 400px. Centered on page using `place-items-center`. Logo above form. No hero image.

### ASCII Wireframe

```
┌──────────────────────────────────────────────────────────────────────────┐
│                                                                          │
│                          [Zync logo]                                     │
│                                                                          │
│              ┌──────────────────────────────────────────┐               │
│              │  כניסה לחשבון                            │               │
│              │                                          │               │
│              │  דוא"ל                                   │               │
│              │  ┌────────────────────────────────────┐  │               │
│              │  │                                    │  │               │
│              │  └────────────────────────────────────┘  │               │
│              │                                          │               │
│              │  סיסמה                           שכחת?  │               │
│              │  ┌────────────────────────────────────┐  │               │
│              │  │  ••••••••                   [👁]    │  │               │
│              │  └────────────────────────────────────┘  │               │
│              │                                          │               │
│              │  ┌────────────────────────────────────┐  │               │
│              │  │             כניסה                  │  │               │
│              │  └────────────────────────────────────┘  │               │
│              │                                          │               │
│              │  אין לך חשבון?  התחל בחינם →            │               │
│              └──────────────────────────────────────────┘               │
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘
```

Note: The eye icon for show/hide password is a functional toggle, not decorative. Use Unicode `◎`/`◉` or a minimal SVG. No icon library.

### Form Schema

```ts
// src/lib/schemas/login.ts
import { z } from 'zod'

export const loginSchema = z.object({
  email: z.string().email('כתובת דוא"ל לא תקינה'),
  password: z.string().min(1, 'נדרשת סיסמה'),
})

export type LoginInput = z.infer<typeof loginSchema>
```

### Auth Flow

1. User submits form
2. `POST /api/auth/login` with `{ email, password }`
3. On success: API sets `zync_session` + `zync_refresh` httpOnly cookies on `Domain=.zync.is`
4. Client redirects to `PUBLIC_APP_URL` (`https://app.zync.is`)
5. On error (401): inline error message — "דוא\"ל או סיסמה שגויים". No field-level attribution (no enumeration).

### API Reference

```
POST /api/auth/login
Body: { email: string, password: string }
Success (200): sets httpOnly cookies zync_session, zync_refresh
  Domain=.zync.is, Path=/, Secure, SameSite=Lax
  → client redirects to https://app.zync.is
Failure (401): { error: 'invalid_credentials' }
Rate limited: 5 failures → exponential backoff; 10 failures → lockout
```

### Error States

| State | Display |
|---|---|
| Validation (empty/invalid) | Inline beneath field, `color: var(--danger)` |
| Wrong credentials | Banner above form: "דוא\"ל או סיסמה שגויים" |
| Account locked | Banner: "החשבון נעול זמנית. נסה שוב עוד כמה דקות." |
| Network error | Banner: "שגיאת תקשורת. נסה שוב." |

### Component Breakdown

| Component | Type | Location |
|---|---|---|
| `<LoginForm>` | React island (`client:load`) | `src/components/auth/LoginForm.tsx` |

### SEO

```html
<title>כניסה — Zync</title>
<meta name="robots" content="noindex" />
```

Auth pages are `noindex`. No `<meta name="description">` needed.

---

## Page 4: Signup Page (`/signup`)

### Purpose

Create new tenant + owner user. Redirect to onboarding wizard.

### Rendering

Static Astro page with `<SignupForm>` React island (`client:load`).

### ASCII Wireframe

```
┌──────────────────────────────────────────────────────────────────────────┐
│                                                                          │
│                          [Zync logo]                                     │
│                                                                          │
│              ┌──────────────────────────────────────────┐               │
│              │  יצירת חשבון חדש                         │               │
│              │                                          │               │
│              │  שם העסק                                 │               │
│              │  ┌────────────────────────────────────┐  │               │
│              │  │                                    │  │               │
│              │  └────────────────────────────────────┘  │               │
│              │                                          │               │
│              │  דוא"ל                                   │               │
│              │  ┌────────────────────────────────────┐  │               │
│              │  │                                    │  │               │
│              │  └────────────────────────────────────┘  │               │
│              │                                          │               │
│              │  סיסמה  (לפחות 8 תווים)                  │               │
│              │  ┌────────────────────────────────────┐  │               │
│              │  │  ••••••••                   [👁]    │  │               │
│              │  └────────────────────────────────────┘  │               │
│              │                                          │               │
│              │  ┌────────────────────────────────────┐  │               │
│              │  │         יצירת חשבון                 │  │               │
│              │  └────────────────────────────────────┘  │               │
│              │                                          │               │
│              │  יש לך חשבון? כנס →                     │               │
│              │                                          │               │
│              │  בלחיצה על "יצירת חשבון" אתה מסכים      │               │
│              │  לתנאי השירות ולמדיניות הפרטיות          │               │
│              └──────────────────────────────────────────┘               │
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘
```

### Form Schema

```ts
// src/lib/schemas/signup.ts
import { z } from 'zod'

export const signupSchema = z.object({
  businessName: z.string().min(2, 'שם העסק חייב להכיל לפחות 2 תווים').max(100),
  email: z.string().email('כתובת דוא"ל לא תקינה'),
  password: z.string().min(8, 'הסיסמה חייבת להכיל לפחות 8 תווים'),
})

export type SignupInput = z.infer<typeof signupSchema>
```

### Auth Flow

1. User submits form
2. `POST /api/auth/signup` with `{ businessName, email, password }`
3. API creates `User` (status: `PENDING_EMAIL`), creates `Tenant`, assigns `OWNER` role
4. API sends verification email (fire-and-forget — does not block response)
5. API sets `zync_session` + `zync_refresh` httpOnly cookies on `Domain=.zync.is`
6. Client redirects to `https://app.zync.is/onboarding`

Unverified users can access the app; a verification banner is shown by `app.zync.is` (out of scope for this spec).

### API Reference

```
POST /api/auth/signup
Body: { businessName: string, email: string, password: string }
Success (201): sets httpOnly cookies zync_session, zync_refresh
  Domain=.zync.is, Path=/, Secure, SameSite=Lax
  → client redirects to https://app.zync.is/onboarding
Failure (409): { error: 'email_already_registered' }
Failure (422): { error: 'validation_error', fields: {...} }
```

### Error States

| State | Display |
|---|---|
| Email already registered | Banner: "כתובת הדוא\"ל כבר רשומה. רצית להתחבר?" + link to `/login` |
| Validation error | Inline beneath field |
| Network error | Banner: "שגיאת תקשורת. נסה שוב." |

### Component Breakdown

| Component | Type | Location |
|---|---|---|
| `<SignupForm>` | React island (`client:load`) | `src/components/auth/SignupForm.tsx` |

### SEO

```html
<title>יצירת חשבון — Zync</title>
<meta name="robots" content="noindex" />
```

---

## Page 5: Password Reset Request (`/reset-password`)

### Purpose

User enters their email. System sends a password-reset link.

### Rendering

Static Astro page + `<ResetRequestForm>` React island.

### ASCII Wireframe

```
┌──────────────────────────────────────────────────────────────────────────┐
│                          [Zync logo]                                     │
│              ┌──────────────────────────────────────────┐               │
│              │  איפוס סיסמה                              │               │
│              │  הזן את הדוא"ל שלך ונשלח לך קישור.       │               │
│              │                                          │               │
│              │  דוא"ל                                   │               │
│              │  ┌────────────────────────────────────┐  │               │
│              │  │                                    │  │               │
│              │  └────────────────────────────────────┘  │               │
│              │                                          │               │
│              │  ┌────────────────────────────────────┐  │               │
│              │  │         שליחת קישור                 │  │               │
│              │  └────────────────────────────────────┘  │               │
│              │                                          │               │
│              │  חזרה להתחברות ←                         │               │
│              └──────────────────────────────────────────┘               │
└──────────────────────────────────────────────────────────────────────────┘
```

### Form Schema

```ts
export const resetRequestSchema = z.object({
  email: z.string().email('כתובת דוא"ל לא תקינה'),
})
```

### Auth Flow

1. User submits email
2. `POST /api/auth/reset-password` with `{ email }`
3. API always returns 200 (no enumeration — same response whether email exists or not)
4. Form replaced with success message: "אם כתובת הדוא\"ל רשומה, שלחנו לך קישור לאיפוס. בדוק את תיבת הדואר."

### API Reference

```
POST /api/auth/reset-password
Body: { email: string }
Response (200 always): { ok: true }
Side effect: if email exists in DB, sends reset email with 1h signed token
```

### Component Breakdown

| Component | Type | Location |
|---|---|---|
| `<ResetRequestForm>` | React island (`client:load`) | `src/components/auth/ResetRequestForm.tsx` |

### SEO

```html
<title>איפוס סיסמה — Zync</title>
<meta name="robots" content="noindex" />
```

---

## Page 6: Password Reset Confirm (`/reset-password/confirm`)

### Purpose

User lands here from the email link. Sets a new password. Token consumed.

URL shape: `/reset-password/confirm?token=<signed-token>`

### Rendering

Static Astro page + `<ResetConfirmForm>` React island. The island reads `token` from `window.location.search` on mount.

### States

**Token valid (default render):**

```
┌──────────────────────────────────────────────────────────────────────────┐
│                          [Zync logo]                                     │
│              ┌──────────────────────────────────────────┐               │
│              │  בחירת סיסמה חדשה                        │               │
│              │                                          │               │
│              │  סיסמה חדשה  (לפחות 8 תווים)             │               │
│              │  ┌────────────────────────────────────┐  │               │
│              │  │  ••••••••                   [👁]    │  │               │
│              │  └────────────────────────────────────┘  │               │
│              │                                          │               │
│              │  אימות סיסמה                             │               │
│              │  ┌────────────────────────────────────┐  │               │
│              │  │  ••••••••                   [👁]    │  │               │
│              │  └────────────────────────────────────┘  │               │
│              │                                          │               │
│              │  ┌────────────────────────────────────┐  │               │
│              │  │         עדכון סיסמה                 │  │               │
│              │  └────────────────────────────────────┘  │               │
│              └──────────────────────────────────────────┘               │
└──────────────────────────────────────────────────────────────────────────┘
```

**Token missing or expired (detected on mount / on API error):**

Single message: "הקישור לא תקף או פג תוקפו. ניתן לבקש קישור חדש." + button → `/reset-password`.

### Form Schema

```ts
export const resetConfirmSchema = z.object({
  password: z.string().min(8, 'הסיסמה חייבת להכיל לפחות 8 תווים'),
  confirm: z.string(),
}).refine(d => d.password === d.confirm, {
  message: 'הסיסמאות אינן תואמות',
  path: ['confirm'],
})
```

### Auth Flow

1. Island mounts, extracts `token` from URL
2. User submits new password
3. `POST /api/auth/reset-password/confirm` with `{ token, password }`
4. On success (200): cookies set, redirect to `https://app.zync.is`
5. On 400/410 (token invalid/expired): show "expired" state

### API Reference

```
POST /api/auth/reset-password/confirm
Body: { token: string, password: string }
Success (200): sets httpOnly cookies (user now logged in), { ok: true }
Failure (400): { error: 'invalid_token' }
Failure (410): { error: 'token_expired' }
```

### Component Breakdown

| Component | Type | Location |
|---|---|---|
| `<ResetConfirmForm>` | React island (`client:load`) | `src/components/auth/ResetConfirmForm.tsx` |

### SEO

```html
<title>איפוס סיסמה — Zync</title>
<meta name="robots" content="noindex" />
```

---

## Page 7: Invite Accept (`/invite/accept`)

### Purpose

Accept a team invitation. If the invitee is a new user (no existing account), they set a name and password. If existing user, they authenticate and accept.

URL shape: `/invite/accept?token=<invitation-token>`

### Rendering

Static Astro page + `<InviteAcceptForm>` React island. Island fetches invite metadata on mount to determine which sub-state to show.

### States

**1. Token invalid or expired (on mount, `GET /api/auth/invite/:token` returns 404/410):**

Message: "ההזמנה לא תקפה או פגה תוקפה. פנה למנהל הארגון לקבלת הזמנה חדשה."

**2. New user (no existing account for the invited email):**

```
┌──────────────────────────────────────────────────────────────────────────┐
│                          [Zync logo]                                     │
│              ┌──────────────────────────────────────────┐               │
│              │  הצטרפות ל-[שם הארגון]                   │               │
│              │  הוזמנת על ידי [שם המזמין]               │               │
│              │                                          │               │
│              │  שמך המלא                                │               │
│              │  ┌────────────────────────────────────┐  │               │
│              │  │                                    │  │               │
│              │  └────────────────────────────────────┘  │               │
│              │                                          │               │
│              │  בחר סיסמה                               │               │
│              │  ┌────────────────────────────────────┐  │               │
│              │  │  ••••••••                   [👁]    │  │               │
│              │  └────────────────────────────────────┘  │               │
│              │                                          │               │
│              │  ┌────────────────────────────────────┐  │               │
│              │  │         הצטרפות לצוות               │  │               │
│              │  └────────────────────────────────────┘  │               │
│              └──────────────────────────────────────────┘               │
└──────────────────────────────────────────────────────────────────────────┘
```

**3. Existing user (account found for invited email):**

```
┌──────────────────────────────────────────────────────────────────────────┐
│              ┌──────────────────────────────────────────┐               │
│              │  הצטרפות ל-[שם הארגון]                   │               │
│              │  הוזמנת על ידי [שם המזמין]               │               │
│              │                                          │               │
│              │  כניסה לאישור ההזמנה                     │               │
│              │                                          │               │
│              │  סיסמה                                   │               │
│              │  ┌────────────────────────────────────┐  │               │
│              │  │  ••••••••                   [👁]    │  │               │
│              │  └────────────────────────────────────┘  │               │
│              │                                          │               │
│              │  ┌────────────────────────────────────┐  │               │
│              │  │         אישור הצטרפות               │  │               │
│              │  └────────────────────────────────────┘  │               │
│              └──────────────────────────────────────────┘               │
└──────────────────────────────────────────────────────────────────────────┘
```

Email field is pre-filled and read-only (from invite token metadata). Not editable.

### Form Schemas

```ts
// New user
export const inviteNewUserSchema = z.object({
  fullName: z.string().min(2, 'נדרש שם מלא'),
  password: z.string().min(8, 'הסיסמה חייבת להכיל לפחות 8 תווים'),
})

// Existing user
export const inviteExistingUserSchema = z.object({
  password: z.string().min(1, 'נדרשת סיסמה'),
})
```

### Auth Flow

**On mount:**
1. Extract `token` from URL
2. `GET /api/auth/invite/:token` → returns `{ email, tenantName, inviterName, isNewUser: boolean }` or 404/410

**New user submission:**
1. `POST /api/auth/invite/accept` with `{ token, fullName, password }`
2. API creates `User` + `TenantMembership`
3. Cookies set, redirect to `https://app.zync.is`

**Existing user submission:**
1. `POST /api/auth/invite/accept` with `{ token, password }`
2. API authenticates user, creates `TenantMembership`
3. Cookies set, redirect to `https://app.zync.is`

### API Reference

```
GET /api/auth/invite/:token
Success (200): { email, tenantName, inviterName, isNewUser: boolean }
Failure (404): { error: 'not_found' }
Failure (410): { error: 'expired' }

POST /api/auth/invite/accept
Body (new user):      { token, fullName, password }
Body (existing user): { token, password }
Success (200): sets httpOnly cookies, { ok: true }
Failure (401): { error: 'invalid_password' }   — existing user wrong password
Failure (410): { error: 'token_expired' }
```

### Component Breakdown

| Component | Type | Location |
|---|---|---|
| `<InviteAcceptForm>` | React island (`client:load`) | `src/components/auth/InviteAcceptForm.tsx` |

### SEO

```html
<title>הזמנה לצוות — Zync</title>
<meta name="robots" content="noindex" />
```

---

## Auth Pages: Shared Patterns

All 5 auth pages (`/login`, `/signup`, `/reset-password`, `/reset-password/confirm`, `/invite/accept`) share:

### Shared Layout (`src/layouts/AuthLayout.astro`)

- Full-height page: `min-h-screen bg-[--bg] flex flex-col items-center justify-center`
- Max-width form container: 400px, `bg-[--surface]`, `rounded`, `p-8`
- Zync logo centered above form (links to `/`)
- `dir="rtl" lang="he"` on `<html>`
- No sidebar, no nav, no footer

### Token Delivery Model

All auth pages are **static** Astro shells. The React islands handle all auth interaction.

On successful authentication, the API sets:
- `zync_session` (access token, 1h, httpOnly, `Domain=.zync.is`, `Secure`, `SameSite=Lax`)
- `zync_refresh` (refresh token, 30d, httpOnly, same attributes)

Because cookies are set on `Domain=.zync.is`, they are readable by `app.zync.is` automatically. No `localStorage`, no token-in-URL, no JS cookie access required. The React island simply performs `window.location.href = PUBLIC_APP_URL` after the POST succeeds.

### Loading States

Submit button shows "..." and is `disabled` during in-flight requests. No skeleton loaders.

### Form Implementation Pattern

All forms use `react-hook-form` + `@hookform/resolvers/zod`. Example structure:

```tsx
const form = useForm<LoginInput>({
  resolver: zodResolver(loginSchema),
  defaultValues: { email: '', password: '' },
})
```

Error messages rendered via `form.formState.errors.field.message`. Server errors rendered in a `<Banner>` component above the form (not inline).

Every auth `<form>` element sets `method="post"` (in addition to `onSubmit={handleSubmit(...)}` and `noValidate`). Rationale: the island hydrates on `client:load`, but if the user submits in the gap before hydration, the browser performs a *native* form submit. Without `method`, a form defaults to GET — appending field values (incl. `password`, reset/invite `token`) to the URL, where they leak into browser history, the `Referer` header, and server access logs. `method="post"` keeps any pre-hydration native submit in the request body, not the URL. (After hydration, `handleSubmit` calls `preventDefault`, so the native submit never fires.)

---

## Shared Component Library

All layout primitives consumed from `@zync/ui`. Auth forms use these:

| Component | From | Usage |
|---|---|---|
| `<Button>` | `@zync/ui` | All CTAs |
| `<Input>` | `@zync/ui` | All form fields |
| `<Card>` | `@zync/ui` | Form containers, pricing cards |
| `<Badge>` | `@zync/ui` | "הכי פופולרי" on Business tier card |

No new primitives introduced by `zync-www`. If a needed primitive is missing from `@zync/ui`, it must be added there — not created locally in `zync-www`.

---

## RTL Implementation Rules

- `<html dir="rtl" lang="he">` on every page
- All directional spacing in Tailwind uses logical properties: `ms-*`/`me-*`/`ps-*`/`pe-*`
- Never use `ml-*`, `mr-*`, `pl-*`, `pr-*`, `text-left`, `text-right`
- Grid column positions in hero sections are relative to the RTL reading direction (column 1 = rightmost)
- Flexbox: `flex-row` is correct — RTL is handled by `dir` attribute, not by reversing flex direction

---

## Performance Strategy

Per-page static rendering decisions:

| Page | Output | Rationale |
|---|---|---|
| `/` | Static, no islands | Pure marketing. Full SSG. Bots see full content. |
| `/pricing` | Static + 1 React island | `<PricingToggle>` is the only interactive element; all card content is in static HTML |
| `/login` | Static + 1 React island | Form is the only dynamic part |
| `/signup` | Static + 1 React island | Form is the only dynamic part |
| `/reset-password` | Static + 1 React island | Form is the only dynamic part |
| `/reset-password/confirm` | Static + 1 React island | Reads token from URL client-side |
| `/invite/accept` | Static + 1 React island | Fetches invite metadata client-side |

Astro's `client:load` directive used on all form islands (immediate hydration required — user interaction). No `client:idle` or `client:visible` on auth forms.

Landing page has zero JS.

### Image Optimization

Marketing images (hero screenshots, feature section images) must be served in modern formats with responsive `srcset`. All images in `apps/zync-www/public/images/` go through Astro's built-in image pipeline (`@astrojs/image` or `astro:assets`):

```astro
---
import { Image } from 'astro:assets'
import heroScreenshot from '../assets/hero-screenshot.png'
---

<Image
  src={heroScreenshot}
  alt="לוח הבקרה של Zync — תצוגת קנבן עם משימות ולוח זמנים"
  widths={[640, 1024, 1440]}
  formats={['avif', 'webp', 'png']}
  loading="eager"
  decoding="async"
/>
```

Rules:
- **Format:** AVIF first, WebP fallback, PNG final fallback. Astro generates all three at build time.
- **`widths`:** 640 (mobile), 1024 (tablet), 1440 (desktop max) for hero images. Feature section images: `[480, 800]`.
- **`loading`:** `eager` for above-the-fold hero image only. All other images: `loading="lazy"`.
- **`alt`:** Hebrew alt text for Hebrew-locale pages. Must describe content meaningfully, not "תמונה" / "screenshot".
- **Max source size:** Raw input images must be ≤ 2000px wide. No upscaling.

LCP target: hero image must be the LCP element. Ensure `<link rel="preload" as="image" type="image/avif" href="...">` is emitted in `<head>` for the hero by Astro's image optimization pipeline.

Core Web Vitals targets for `zync.is` (measured via Cloudflare Web Analytics + Lighthouse CI on main branch):

| Metric | Target | Notes |
|--------|--------|-------|
| LCP | ≤ 2.5s | Hero image is LCP element on `/` |
| CLS | ≤ 0.1 | Image dimensions must be declared to prevent layout shift |
| INP | ≤ 200ms | Auth islands hydrate on `client:load`; no blocking scripts |

Dimensions **must** be declared on every `<Image>` (width + height or aspect ratio) to prevent CLS from images loading before layout is known.

---

## Foundation Delta

The `foundation-auth-rbac` spec (2026-05-30) describes self-registration as a 5-step flow where JWT is issued only *after* email verification click. This spec diverges deliberately: the signup API issues JWT and sets cookies immediately on `POST /api/auth/signup`, and the verification email is sent fire-and-forget. Unverified users access the app but see a verification banner (implemented in `app.zync.is`, out of scope here).

**Required update to `foundation-auth-rbac`:** Step 4 of the self-registration flow should be updated to reflect immediate JWT issuance + async email verification.

---

## Design Decisions

| Decision | Choice | Alternatives Rejected |
|---|---|---|
| Token storage | httpOnly cookie `Domain=.zync.is` | `localStorage` — XSS-exposed, contradicts foundation; token-in-URL query param — leaks via referrer/history |
| Astro rendering | Static + islands | Full SSR — unnecessary, adds runtime cost, no SEO benefit for auth pages; Full SPA — loses static build, breaks bot crawlability for marketing pages |
| Hero layout | Asymmetric 2-col (copy cols 2–7, screenshot cols 8–12) | Centered stacked hero — banned by layout rules; full-width image hero — loses text hierarchy |
| Feature sections | Alternating left/right | All-left or bento grid — bento banned; all-left is monotonous |
| Pricing toggle | Local React state, hardcoded constants | API-driven pricing — unnecessary complexity, coupling, cache invalidation; query-param driven — ugly URLs |
| White Label on pricing page | FAQ mention only, no card | 4th card — clutters comparison, confuses self-service buyers; no mention — omits a valid tier entirely |
| Annual pricing math | 10× monthly = 2 months free, annualPerMonth = annual/12 | 11× monthly — less compelling; discount percentage — less intuitive than "2 free months" |
| Password hashing | PBKDF2 (per foundation) | Argon2id — WASM memory risk on Workers (foundation decision) |
| Invite page route | `/invite/accept?token=` | `/invite?token=` — ambiguous; `/i/:token` — foundation spec names this path explicitly |
| Form error display | Inline per-field (validation) + banner (server errors) | Toast for errors — transient toasts miss accessibility requirements; inline-only for server errors — loses error context on multi-field forms |
| FAQ implementation | Native `<details>`/`<summary>` | JS accordion — no benefit, adds JS weight |
| "Most popular" badge | Business tier | Enterprise — price anchoring logic: Business is the upgrade target, Enterprise is the upsell from there |

---

## Anti-Patterns Explicitly Avoided

| Anti-Pattern | Status |
|---|---|
| "Trusted by X companies" / client logo strip | Not present anywhere |
| Bento box feature grid | Not present anywhere |
| Centered hero with 2 stacked CTAs over gradient | Not present; hero is asymmetric, single CTA, flat background |
| Gradient backgrounds | Not used on any page |
| Decorative icons in feature sections | Not used |
| Emoji in UI copy | Not used |
| `localStorage` for auth tokens | Rejected; httpOnly cookie used |
| Token-in-URL redirect (auth) | Rejected |
| `hex`/`rgb`/`hsl` color values | Not used; OKLCH-only |
| Off-grid spacing (non-8px-multiple) | Not used |
| `rounded-sm/md/lg/xl` (radius ladder) | Not used; `rounded` only (`--radius: 4px`) |
| Hardcoded prices in components | Prices defined in `src/lib/pricing.ts` constants only |
| `ml-*`/`mr-*`/`pl-*`/`pr-*` (physical spacing) | Not used; logical properties only |
| More than one CTA per section | Enforced: one CTA per section maximum |

---

## SEO Summary

| Page | `<title>` | `<meta description>` | `robots` |
|---|---|---|---|
| `/` | Zync — ניהול פרויקטים לעצמאיים ועסקים בישראל | ✓ | index |
| `/pricing` | תמחור — Zync | ✓ | index |
| `/login` | כניסה — Zync | — | noindex |
| `/signup` | יצירת חשבון — Zync | — | noindex |
| `/reset-password` | איפוס סיסמה — Zync | — | noindex |
| `/reset-password/confirm` | איפוס סיסמה — Zync | — | noindex |
| `/invite/accept` | הזמנה לצוות — Zync | — | noindex |

All static pages rendered server-side by Astro at build time. No content is JS-gated for crawlers.

---

## File Structure

```
apps/zync-www/
├── astro.config.ts
├── tailwind.config.ts
├── tsconfig.json
├── package.json
├── public/
│   └── favicon.svg
└── src/
    ├── layouts/
    │   ├── BaseLayout.astro          # html/head/body, dir=rtl, tokens
    │   └── AuthLayout.astro          # centered single-col, logo above
    ├── pages/
    │   ├── index.astro               # /
    │   ├── pricing.astro             # /pricing
    │   ├── login.astro               # /login
    │   ├── signup.astro              # /signup
    │   ├── reset-password/
    │   │   ├── index.astro           # /reset-password
    │   │   └── confirm.astro         # /reset-password/confirm
    │   └── invite/
    │       └── accept.astro          # /invite/accept
    ├── components/
    │   ├── Nav.astro
    │   ├── Footer.astro
    │   ├── landing/
    │   │   ├── Hero.astro
    │   │   ├── FeatureSection.astro
    │   │   ├── PricingPreview.astro
    │   │   └── PricingCard.astro
    │   ├── pricing/
    │   │   ├── PricingToggle.tsx     # React island
    │   │   ├── TierCard.astro
    │   │   ├── ComparisonTable.astro
    │   │   └── PricingFAQ.astro
    │   └── auth/
    │       ├── LoginForm.tsx         # React island
    │       ├── SignupForm.tsx        # React island
    │       ├── ResetRequestForm.tsx  # React island
    │       ├── ResetConfirmForm.tsx  # React island
    │       └── InviteAcceptForm.tsx  # React island
    └── lib/
        ├── pricing.ts                # Tier price constants
        └── schemas/
            ├── login.ts
            ├── signup.ts
            ├── reset-request.ts
            ├── reset-confirm.ts
            └── invite.ts
```
