# Gradeful site structure

Public inventory of every student-facing page, button, and flow.
HTML hub: /connects
This dump is the same markdown as docs/site-structure/ in the repo.
It updates whenever Gradeful is deployed.

================================================================================
FILE: 00-how-everything-connects.md
HTML: /connects/00-how-everything-connects
RAW: /connects/raw/00-how-everything-connects
================================================================================
# How everything connects

This is the map. Detail for every button lives in `02`–`06`. Marketing CTAs that start these flows are in `01-marketing-pages.md`.

Access words used below:

- **public** — no login
- **logged-in** — Supabase Auth session
- **onboarded** — `profiles.onboarding_completed_at` set, or name + AUB/LAU already filled (major and campus are optional)
- **subscribed** — at least one `user_access` row with `payment_status` in `paid`/`confirmed` and `expires_at` null or in the future (admins skip this for `/my-courses`)

`proxy.ts` gates:

- `/` — if a Supabase Auth cookie is present **and** `userHasActiveSubscription` is true (`user_access` paid/confirmed, `expires_at` null or in the future) → `/dashboard`. Anonymous visitors and logged-in users with no active access still get the marketing homepage. Other public routes (`/pricing`, `/courses`, legal) are not redirected.
- `/login`, `/signup` — if already logged in → `/auth/home` (with safe `next` when present)
- `/dashboard`, `/welcome`, `/admin`, `/my-courses` — if anonymous → `/login?next=…`
- `/my-courses/{slug}` and `/dashboard/course/{slug}` — also require subscribed (else `/pricing`); `/dashboard` and the `/my-courses` home alias do **not**
- `/checkout` — session refresh; the page itself sends anonymous users to `/login?next=/checkout?…` and incomplete profiles to `/welcome?next=…`

**Active access** is not “logged in” and not a historical order. Authoritative read: `user_access` where `user_id` matches, `payment_status` in `paid`/`confirmed`, and `expires_at` is null or `> now()`. Expired or revoked rows do not count. Canonical student home is `/dashboard`.

---

## 1. Registration and first login

```mermaid
flowchart TD
  A[Anonymous: /signup or /login] --> B{Method}
  B -->|Google| C["signInWithOAuth → Google"]
  B -->|Email signup| D["signUp → auth.users"]
  B -->|Email login| E["signInWithPassword"]
  C --> G["/auth/callback?code=&next="]
  D --> H[Check your email]
  H --> G
  E --> I["/auth/home?next="]
  G --> I
  D -.->|trigger handle_new_user| P[(profiles insert)]
  I --> J{profileNeedsOnboarding?}
  J -->|yes| K["/welcome?next="]
  J -->|no| L["active access → /dashboard; else / or preserved next"]
  K --> M["completeOnboarding writes name + university"]
  M --> L
```

**Tables**

| Step | Writes | Reads |
|---|---|---|
| Google / email signup | `auth.users`; trigger inserts `profiles` (`id`, `email`, `full_name`, `avatar_url`, `nickname`; role defaults to `student`) | — |
| Email login | Auth session cookies only | `auth.users` |
| `/auth/callback` | Session cookies; optional Resend welcome email if `profiles.created_at` ≤ 5s | `profiles.created_at` |
| `/auth/home` | — | `profiles.full_name`, `university`, `onboarding_completed_at` |
| `/welcome` submit | `profiles.full_name`, `university`, optional campus/major/programs, `nickname`, `onboarding_completed_at`, `updated_at` | same fields as prefill |

**Where login is forced**

- Any `/dashboard`, `/welcome`, `/admin`, `/my-courses` hit while logged out → `/login?next=…`
- `/checkout` without a session → `/login?next=/checkout?plan=…`
- Workspace layout and checkout bounce incomplete name/university to `/welcome?next=…`

**After login, where you land**

- Default `next` is `/auth/home`
- `/auth/home` → `/welcome?next=…` or the preserved course/checkout path; default home is `/dashboard` only when `userHasActiveSubscription` is true, otherwise `/`
- Course-preview signup uses `/signup?next=/checkout?plan=focus&course={uuid}`

Phone signup is **not** offered. Phone sign-in was removed from `/login` (zero hosted identities).

Full page inventory: [02-auth-onboarding.md](./02-auth-onboarding.md)
Product rules: [../onboarding.md](../onboarding.md)

---

## 2. Checkout and access grant

```mermaid
flowchart TD
  M[Marketing: Get Focus / Get Balance / Cover my semester / Prep for Finals] --> C["/checkout?plan=focus|balance|fullload|crunch"]
  Course["/courses/slug Get access"] --> C2["/checkout?plan=focus&course=uuid"]
  C2 --> C
  C --> Auth{Signed in?}
  Auth -->|no| Login["/login?next=/checkout?…"]
  Login --> C
  Auth -->|yes| S1[Step 1 pick courses]
  S1 --> S2[Step 2 name university WhatsApp uni email terms]
  S2 --> API["POST /api/payments/whish/create"]
  API --> ORD[(orders pending)]
  API --> W[Whish collect URL]
  W --> R["/checkout/payment/success or failure?orderId="]
  R --> ST["GET /api/payments/whish/status"]
  ST --> REC[reconcileWhishOrder]
  REC --> UA[(user_access paid)]
  UA --> Packs["Go to my courses → /dashboard"]
```

**Plan slugs on the URL vs database ids**

| URL `?plan=` | Display name | `PLANS` / `orders.plan` / `user_access.plan_type` |
|---|---|---|
| `focus` | Focus | `single_course` (also accepted as `focus`) |
| `balance` | Balance | `two_courses` |
| `fullload` | Full Load | `semester_access` |
| `crunch` | Crunch | `exam_crunch` |

Catalog: `lib/payments/catalog.ts`. Full Load is up to 6 courses; Crunch is up to 4. New grants do not write `schedule_verifications`.

**Live writes**

| Moment | Table |
|---|---|
| Continue to secure payment | `orders` (`status=pending`, `payment_provider=whish`, `course_ids`, `registered_credits`, `whatsapp`, `email` = uni email, `amount`, `external_id`, `collect_url`) |
| Whish paid (status poll or callback) | `orders.payment_status=paid`; `grantAccess` → `user_access` (`payment_status=paid`, `course_ids`, `expires_at`, `payment_reference`). New grants do **not** insert `schedule_verifications`. |
| `payment_events` | Server audit around create/reconcile (no UI) |

Entitlement **reads** use `user_access.course_ids`. Junction tables may be dual-written and are not the read source of truth.

**Not the live CTA path (removed)**

- Legacy reservation modal / `POST /api/reservations`
- Legacy `POST /api/orders`
- Placeholder payment sessions

`profiles` is **not** updated at checkout. University on the form is stored on `orders`, not on the profile.

Paid course lists still read `user_access.course_ids` / `orders.course_ids`. Junction tables (`access_grant_courses`, `order_courses`) are dual-written when present and are **not** the read source of truth yet.

Full page inventory: [03-checkout.md](./03-checkout.md)

---

## 3. After pay: dashboard vs my-courses

```mermaid
flowchart TD
  Pay[user_access row exists] --> D["/dashboard — student home"]
  Pay --> Alias["/my-courses and /dashboard/my-packs redirect to /dashboard"]
  D --> Course["/my-courses/{slug}"]
  Course --> Study["?module=&tab=notes|formula_sheet|flashcards|quiz|exam_review|mistake_fixes|exercises"]
  D --> Lib["/dashboard/library"]
  D --> Prog["/dashboard/progress"]
  D --> Sem["/dashboard/semester-plan"]
  D --> Set["?settings=profile|notifications|billing|security|danger|help"]
```

`/dashboard/course/{slug}/…` redirects to `/my-courses/{slug}`.  
`/my-courses/library|progress|semester-plan` redirect to the `/dashboard/…` twins.

Someone who is logged in but **has not paid** can still open `/dashboard` (empty onboarding banner). `/my-courses` without a slug redirects to `/dashboard`. Course URLs `/my-courses/{slug}` still require a paid grant.

Full page inventory: [04-dashboard-and-workspace.md](./04-dashboard-and-workspace.md)

---

## 4. Settings (every tab)

Opened over the workspace (not a standalone `/settings` page). Gear, account flyout **Settings**, or `/dashboard?settings={tab}`. Query is stripped after open. Legacy `/settings/*` redirects into these tabs.

| Tab id | Label | What it does |
|---|---|---|
| `profile` | Profile | Edit name, nickname, photo, university/campus/major; badge display |
| `notifications` | Notifications | Four email/reminder toggles on `profiles.notification_preferences` |
| `billing` | Plan & billing | Active `user_access` + Whish `orders` history; **Upgrade or change plan** → `/pricing` |
| `security` | Security | Password, Google link/unlink, phone display, backup password, this-device time |
| `danger` | Danger zone | Sign out → `/`; delete account (type `DELETE`) |
| `help` | Help & contact | WhatsApp, request a course, policy links |

Dark mode and extra languages are not shipped. Appearance and Language panes and server actions were removed. `/settings/appearance` and `/settings/language` still redirect to profile.

Full inventory: [05-settings.md](./05-settings.md)

---

## 5. Study: how a workspace is used

```mermaid
flowchart TD
  Card[Dashboard course card] --> OV["/my-courses/{slug} Overview"]
  OV --> Notes["Notes markdown"]
  OV --> Form["Formula sheet"]
  OV --> FC["Flashcards"]
  OV --> Quiz["Quiz / Exam"]
  OV --> ER["Exam review"]
  OV --> MF["Mistake fixes"]
  OV --> EX["Exercises"]
  Notes --> CP[(content_progress)]
  FC --> FS[(flashcard_sessions / card_states / review_logs)]
  Quiz --> QA[(quiz_attempts + content_progress)]
  Notes --> Lib[(library_items save)]
  Heartbeat[CourseStudyHeartbeat] --> SS[(study_sessions)]
  SS --> Streak[Sidebar streak / Progress]
```

Course settings (`/my-courses/{slug}/settings`) writes `flashcard_deck_settings` and can upload `syllabuses`. Exam dates are read from `course_events`.

Full inventory: [06-study-views.md](./06-study-views.md)

---

## 6. End-to-end student path (one sitting)

1. Land on `/` or `/pricing` → **Get Focus** (or another plan CTA).
2. `/checkout?plan=focus` → if logged out, **Sign in** / **Create account** with `next` back to checkout.
3. New account: verify email if needed → `/auth/home` → `/welcome` (name, AUB/LAU, major) → `/dashboard` (still no courses).
4. Return to checkout (`next`) → pick course(s) → details → **Continue to secure payment** → Whish OTP.
5. `/checkout/payment/success` polls until **Payment confirmed** → **Go to my courses**.
6. Dashboard shows the course card → open `/my-courses/{slug}` → Notes / Flashcards / Quiz.
7. Gear → Settings: nickname, photo, notification toggles, billing (active access), security.
8. Sidebar Progress / Library / Semester Plan read the same study writes (`content_progress`, `study_sessions`, `library_items`, `exam_schedules`, `course_events`). Syllabus upload is optional and private.

---

## 7. Connection cheat sheet

| From | To | How |
|---|---|---|
| Header **Get access** / **Add more courses** | `/checkout` | `navCheckoutHref` |
| Header **Sign in** | `/login` | — |
| Header **Dashboard** (mobile logged-in) | `/dashboard` | — |
| Account flyout **Upgrade plan** | `/pricing` | then back into checkout |
| Account flyout **Settings** / **Get help** | Settings modal or `/dashboard?settings=` | workspace vs marketing header |
| Checkout success **Go to my courses** | `/dashboard` | after `user_access` grant |
| Dashboard course card | `/my-courses/{slug}` | `buildCourseOverviewHref` |
| Course **Get access** (public catalog) | `/checkout?plan=focus&course={id}` | preselects that course |
| Non-subscriber `/my-courses` | `/dashboard` (free-state home) | alias redirect; course slugs still gated |
| Non-subscriber `/my-courses/{slug}` | `/pricing` | proxy |
| Incomplete profile any workspace page | `/welcome` | layout |
| Logout | `/` | Auth sign-out |
| Delete account | `/` (or `/login` if unauthorized) | `auth.admin.deleteUser` |

---

## 8. Tables the student product actually uses

| Table | Written by | Read by |
|---|---|---|
| `auth.users` | signup, password change, delete | login, settings security |
| `profiles` | signup trigger, welcome, profile/avatar/notifications, nickname | header, dashboard greeting, settings, `/auth/home` |
| `orders` | Whish checkout create + reconcile | payment return status, settings billing |
| `user_access` | `grantAccess` after paid Whish | dashboard courses, billing, proxy `/my-courses`, nav CTA |
| `courses` / `course_packs` / `universities` / `course_modules` / `module_pack_items` | admin | catalog, checkout picker, study |
| `content_progress` | notes, quiz complete, flashcard scheduled review | overview, resume, progress |
| `flashcard_sessions`, `flashcard_card_states`, `flashcard_review_logs`, `flashcard_deck_settings` | flashcard UI / course settings | flashcards + progress |
| `quiz_attempts` | quiz complete | progress quiz notes |
| `study_sessions` | study heartbeat | streak, study hours, progress |
| `library_items` | save note / upload | library |
| `syllabuses` | syllabus modal | semester plan, course settings |
| `course_events` | admin / syllabus parse | exam banner, semester plan, course settings |
| `notifications` | server triggers | dashboard bell |
| `user_badges` | streak/badge jobs | settings Profile “Showing up” |
| `user_streaks` | streak compute | sidebar streak card |

================================================================================
FILE: 01-marketing-pages.md
HTML: /connects/01-marketing-pages
RAW: /connects/raw/01-marketing-pages
================================================================================
# Marketing pages

Pilot inventory of Gradeful’s public marketing, catalog, legal, and contact routes.

Live student-facing copy is the source of truth in the app. Marketing copy was restored from Git `a7de9a9` (the commit immediately before the tutoring-platform rewrite), with factual/legal exceptions documented in `07-release-status.md` and `08-known-limitations.md`.

This file covers unauthenticated marketing surfaces only.

Related inventories:

- [00-how-everything-connects.md](./00-how-everything-connects.md) — registration, checkout, dashboard, settings, study
- [02-auth-onboarding.md](./02-auth-onboarding.md)
- [03-checkout.md](./03-checkout.md)
- [04-dashboard-and-workspace.md](./04-dashboard-and-workspace.md)
- [05-settings.md](./05-settings.md)
- [06-study-views.md](./06-study-views.md)

Admin (`/admin/*`) is still not in this set.

## Scope

Included:

- `/` homepage
- `/pricing`
- `/courses` listing
- `/courses/[slug]` public course preview
- `/about`
- `/contact`
- `/support`
- `/tutoring`
- `/policies`
- `/privacy`
- `/terms`
- `/refunds`
- `/academic-integrity`
- `/site-map`
- Redirects: `/faq`, `/access`, `/content-standards`
- `/markitup` (public cinematic site; not Gradeful marketing chrome)

Excluded from this file: `/login`, `/signup`, `/forgot-password`, `/reset-password`, `/welcome`, `/checkout`, `/dashboard`, `/my-courses`, `/admin`, `/maintenance`, `/dev`.

There is no App Router `(marketing)` group. These pages live at the app root and share `SiteHeader` + `SiteFooter` except `/markitup`.

Normal access for every Gradeful page below is **public** (no login required) except `/`: logged-in users with **active** `user_access` are redirected to `/dashboard` before the marketing homepage HTML is rendered (`proxy.ts` + `app/page.tsx`). Logged-in users without active access still see `/`. Active users may still open `/pricing`, `/courses`, public course pages, and legal/help on purpose. If `platform_settings` key `maintenance_mode` is `"true"`, non-admins are sent to `/maintenance` instead.

---

## Shared chrome

Used on every Gradeful marketing page in this file except `/markitup`. Documented once. Each PAGE still lists header then footer in render order and points here, plus any page-specific exception.

### Site header (`components/site/SiteHeader.tsx` + `NavAuthButton.tsx`)

SECTION: Site header
  Copy: Logo image alt `Gradeful`. Desktop and mobile nav labels `Courses`, `Pricing`, `About`. Logged-in extras documented on elements. Logged-out CTA `Get access` and `Sign in`. Mobile control `Open menu` / `Close menu`.
  Data shown: Current path (underline active nav). Auth session from Supabase Auth. Profile fields `profiles.avatar_url`, `profiles.full_name`, `profiles.university`, `profiles.campus`, `profiles.major`, `profiles.second_major`, `profiles.role`. Active paid access from `user_access.plan_type`, `user_access.payment_status`, `user_access.expires_at` (rows with `payment_status` in `paid` or `confirmed` and unexpired). Header is sticky except homepage (`sticky={false}`).

  ELEMENT: Gradeful (logo)
    Action: navigate
    Destination: `/`
    Writes to: none
    Reads from: none

  ELEMENT: Courses
    Action: navigate
    Destination: `/courses`
    Writes to: none
    Reads from: none

  ELEMENT: Pricing
    Action: navigate
    Destination: `/pricing`
    Writes to: none
    Reads from: none

  ELEMENT: About
    Action: navigate
    Destination: `/about`
    Writes to: none
    Reads from: none

  ELEMENT: My courses
    Action: navigate
    Destination: `/my-courses`
    Writes to: none
    Reads from: Visible only when logged in and at least one active `user_access` row (`payment_status` in `paid`/`confirmed`, `expires_at` null or in the future)

  ELEMENT: Admin
    Action: navigate
    Destination: `/admin`
    Writes to: none
    Reads from: Visible only when `profiles.role` is `admin`

  ELEMENT: Open menu / Close menu
    Action: toggle
    Destination: mobile nav panel `#site-mobile-menu`
    Writes to: none
    Reads from: none (mobile only)

  ELEMENT: Get access
    Action: navigate
    Destination: `/checkout`
    Writes to: none
    Reads from: Shown when logged out, except on `/pricing` and `/checkout`. When logged in with no paid plan (`getNavAccessCtaLabel` tier `none`), same label. Hidden while auth is still loading.

  ELEMENT: Add more courses
    Action: navigate
    Destination: `/checkout`
    Writes to: none
    Reads from: Shown when logged in with an active paid `user_access.plan_type` (tier `partial`). Desktop header uses avatar-only layout, so this CTA appears in the **mobile** menu, not the desktop bar.

  ELEMENT: Sign in
    Action: navigate
    Destination: `/login`
    Writes to: none
    Reads from: Shown when logged out

  ELEMENT: Dashboard
    Action: navigate
    Destination: `/dashboard`
    Writes to: none
    Reads from: Shown when logged in, in the **mobile** `NavAuthButton` full layout only (desktop logged-in header is avatar-only)

  ELEMENT: Open settings (gear)
    Action: navigate
    Destination: `/dashboard?settings=profile`
    Writes to: none
    Reads from: Shown when logged in (`AccountChrome`)

  ELEMENT: Open account menu (avatar / initials)
    Action: open modal
    Destination: account flyout menu
    Writes to: none
    Reads from: `profiles.avatar_url`, `profiles.full_name`, auth email; initials derived from name/email

Account flyout (opens from avatar; also used from header on marketing pages):

  ELEMENT: Settings
    Action: navigate
    Destination: `/dashboard?settings=profile`
    Writes to: none
    Reads from: none

  ELEMENT: Get help
    Action: navigate
    Destination: `/dashboard?settings=help`
    Writes to: none
    Reads from: none

  ELEMENT: Upgrade plan
    Action: navigate
    Destination: `/pricing`
    Writes to: none
    Reads from: none

  ELEMENT: Dashboard
    Action: navigate
    Destination: `/dashboard`
    Writes to: none
    Reads from: none

  ELEMENT: Learn more
    Action: toggle
    Destination: Learn more submenu
    Writes to: none
    Reads from: none

  ELEMENT: About Gradeful
    Action: navigate
    Destination: `/about`
    Writes to: none
    Reads from: none

  ELEMENT: Privacy policy
    Action: navigate
    Destination: `/privacy`
    Writes to: none
    Reads from: none

  ELEMENT: Terms of service
    Action: navigate
    Destination: `/terms`
    Writes to: none
    Reads from: none

  ELEMENT: Refund policy
    Action: navigate
    Destination: `/refunds`
    Writes to: none
    Reads from: none

  ELEMENT: Academic integrity
    Action: navigate
    Destination: `/academic-integrity`
    Writes to: none
    Reads from: none

  ELEMENT: All policies
    Action: navigate
    Destination: `/policies`
    Writes to: none
    Reads from: none

  ELEMENT: Log out / Logging out…
    Action: submit
    Destination: `/` after `supabase.auth.signOut()`
    Writes to: clears Auth session cookies (no Gradeful table write)
    Reads from: none

### Site footer (`SiteFooter` in `components/site/PublicPageShell.tsx`)

SECTION: Site footer
  Copy: Logo alt `Gradeful`. Body: `Course-specific study packs for AUB and LAU students. Built for focused self-study.` Copyright: `© 2026 Gradeful.` Column headings `Platform` and `Policies`.
  Data shown: static

  ELEMENT: Home
    Action: navigate
    Destination: `/`
    Writes to: none
    Reads from: none

  ELEMENT: Courses
    Action: navigate
    Destination: `/courses`
    Writes to: none
    Reads from: none

  ELEMENT: Pricing
    Action: navigate
    Destination: `/pricing`
    Writes to: none
    Reads from: none

  ELEMENT: Privacy Policy
    Action: navigate
    Destination: `/privacy`
    Writes to: none
    Reads from: none

  ELEMENT: Terms of Service
    Action: navigate
    Destination: `/terms`
    Writes to: none
    Reads from: none

  ELEMENT: Refund Policy
    Action: navigate
    Destination: `/refunds`
    Writes to: none
    Reads from: none

  ELEMENT: Academic Integrity
    Action: navigate
    Destination: `/academic-integrity`
    Writes to: none
    Reads from: none

---

PAGE: /
Purpose: Search-first marketing homepage for course-specific study packs (notes, formula sheets, flashcards, exam review) and plan CTAs.
Access: public for anonymous and logged-in users with no active plan. Logged-in users with active `user_access` are redirected to `/dashboard` (no homepage flash).
Entry points: Direct URL; footer Home; header logo; site map; post-logout redirect (`AccountFlyout` signs out to `/`); external links to gradeful.app.

SECTION: Site header
  Copy: Shared chrome. Homepage header is not sticky.
  Data shown: Shared chrome
  ELEMENT: all Shared chrome elements apply

SECTION: Hero
  Copy: `Course-specific study packs`
  `Stop studying from scattered sources that weren't built for your course.`
  `Notes, formula sheets, flashcards, and exam review, all organized around your exact syllabus. Currently serving AUB and LAU students.`
  Data shown: static

SECTION: Course search
  Copy: Screen-reader label `Search courses`. Input `aria-label` `Search courses`. Placeholder: `Search by course code or name, e.g. CHEM201, Calculus III`
  Empty suggestions: `No matching course yet.`
  Suggestion row secondary label (sm+): `Open`
  Data shown: Suggestion rows are dynamic from `getCourses()`: `courses.code`, `courses.title`, `courses.slug`, `universities.short_name`, pack count from related `course_packs` rows. Catalog limited to `courses.is_active = true`, non-sample, and `course_packs.length > 0`. Query uses first 50 courses.

  ELEMENT: Search form (no visible submit button; submitting the field)
    Action: submit
    Destination: If a highlighted suggestion is active → `/courses/[courses.slug]`. Else if query non-empty → `/courses?q={typed query}`. Else → `/courses`. Note: `/courses` does not currently read `?q=`; the listing search state is client-only.
    Writes to: none
    Reads from: `courses.*` / `universities.short_name` / `course_packs` as above

  ELEMENT: [university short name] [course code] [course title] (each suggestion)
    Action: navigate
    Destination: `/courses/[courses.slug]`
    Writes to: none
    Reads from: `universities.short_name`, `courses.code`, `courses.title`, `courses.slug`

  ELEMENT: Search all courses
    Action: navigate
    Destination: `/courses?q={typed query}`
    Writes to: none
    Reads from: Visible when the dropdown is open and no suggestion matches

  ELEMENT: Request this course
    Action: navigate
    Destination: `/courses#course-request`
    Writes to: none
    Reads from: Visible when the dropdown is open and no suggestion matches

SECTION: Hero course marquee
  Copy: Each tile shows a compatibility university short name (when display mode is `compatibility`) and a display course code. Pack counts are not shown.
  Data shown: Rendered only when `heroCourses.length > 0` and `platform_settings` key `show_course_catalog` is true (default true). Courses from `getCourses()` as above.

  ELEMENT: [university] [code]
    Action: navigate
    Destination: `/courses/[courses.slug]`
    Writes to: none
    Reads from: `universities.short_name` via `formatUniversityShortName`, `courses.code`, `courses.slug`. Visibility also reads `platform_settings` key `show_course_catalog`.

SECTION: Request prompt under search
  Copy: `Don't see your course?` then link `Request it →`
  Data shown: static

  ELEMENT: Request it →
    Action: navigate
    Destination: `/courses#course-request`
    Writes to: none
    Reads from: none

SECTION: Available now
  Copy: `Available now`
  `Admin-selected study packs built for your exact course at AUB and LAU.`
  Slider helper (mobile): `Swipe sideways or use the arrows to see more courses.`
  End card: badge `Full catalog`; `Check more`; `Find your next course pack.`; `Browse the full Gradeful catalog and open every course available for AUB and LAU.`; `Browse all courses`
  Data shown: Section omitted when no featured courses. Cards from `getHomepageCourses()`: `courses` where `is_active = true` and `is_homepage_featured = true`, ordered by `courses.homepage_sort_order` then `courses.code`, max 10, then `isListedCatalogCourse`. Card fields: `universities.short_name` (compatibility display), `courses.code`, `courses.title`, `courses.description`, `courses.semester_label`, `courses.slug`; pack count from `course_packs`. Instructor names stay off unless `NEXT_PUBLIC_GRADEFUL_SHOW_INSTRUCTOR_NAMES=true`. Badge: `1 pack` / `{n} packs`; `Access required` if any related pack is locked, else `Preview open`. Label `Semester:` is a static prefix.

  ELEMENT: Scroll course previews left
    Action: toggle
    Destination: none (scrolls the slider)
    Writes to: none
    Reads from: none

  ELEMENT: Scroll course previews right
    Action: toggle
    Destination: none (scrolls the slider)
    Writes to: none
    Reads from: none

  ELEMENT: [course title] (card heading link)
    Action: navigate
    Destination: `/courses/[courses.slug]`
    Writes to: none
    Reads from: `courses.title`, `courses.slug`

  ELEMENT: Preview pack
    Action: navigate
    Destination: `/courses/[courses.slug]`
    Writes to: none
    Reads from: `courses.slug`; `aria-label` is dynamically `View pack for {courses.code} {courses.title}`

  ELEMENT: Check more courses / Browse all courses (end card)
    Action: navigate
    Destination: `/courses`
    Writes to: none
    Reads from: none

  ELEMENT: Browse all courses →
    Action: navigate
    Destination: `/courses`
    Writes to: none
    Reads from: none

SECTION: Testimonials
  Copy: Testimonials section removed. Hardcoded quotes are not shipped. `lib/reviews.ts` is an empty list until consented testimonials exist.
  Data shown: none

SECTION: Comparison
  Copy: `The usual way`
  `WhatsApp PDF from last semester. No solutions.`
  `YouTube videos that skip the course you are actually taking.`
  `Classmate notes from a different section.`
  `Practice with no answer key.`
  `Googling at 2am the night before.`
  `With Gradeful`
  `Notes built for your exact course.`
  `Formula sheet organized by chapter. Printable.`
  `Practice quiz written for this course.`
  `Exam review for the week before finals.`
  `One place. Everything ready. Nothing missing.`
  Data shown: static

SECTION: Pack tools
  Copy: `Everything you need. Nothing you don't.`
  `Everything you'd normally chase across different sources, organized once, for your exact course.`
  Cards:
  Notes — `Built for your exact chapter. Not a generic overview.`
  Formula sheets — `One page. Everything organized. Print-ready.`
  Flashcards — `Drill the ideas until they stick.`
  Practice quizzes — `Original questions to check your understanding.`
  Mock exams — `Timed Gradeful practice under exam-like pressure.`
  Exam review — `Condensed version for finals week.`
  Mistake fixes — `What students get wrong in this course, fixed.`
  AI explanations — `Ask anything. Grounded in your pack.` Badge: `Coming soon`
  Data shown: static

  ELEMENT: Preview a sample pack
    Action: navigate
    Destination: `/courses`
    Writes to: none
    Reads from: none

SECTION: Pricing
  Copy: `Straightforward access.`
  `Starting at {formatStartingPrice(plans)}. Less than one textbook chapter.` from `getPaymentPlans()` (catalog `lib/payments/catalog.ts` plus optional `plan_overrides`).
  Per card: plan display name, price, unit label, duration, benefits, and CTA from the same live plan record. Default names: Focus, Balance, Full Load, Crunch. Price via `formatPlanPrice`. Unit/duration from catalog helpers (`/1 course`, `/2 courses`, `/up to 6 courses`, `/up to 4 courses`; semester window or `{n}-day sprint`). Badge `Recommended` on Full Load. Badge `{n} days` on Crunch.
  Data shown: `getPaymentPlans()` → `lib/payments/catalog.ts` merged with `platform_settings.plan_overrides`.

  ELEMENT: Get Focus
    Action: navigate
    Destination: `/checkout?plan=focus`
    Writes to: none
    Reads from: none

  ELEMENT: Get Balance
    Action: navigate
    Destination: `/checkout?plan=balance`
    Writes to: none
    Reads from: none

  ELEMENT: Cover my semester
    Action: navigate
    Destination: `/checkout?plan=fullload`
    Writes to: none
    Reads from: none

  ELEMENT: Prep for Finals
    Action: navigate
    Destination: `/checkout?plan=crunch`
    Writes to: none
    Reads from: none

  ELEMENT: Compare full features →
    Action: navigate
    Destination: `/pricing#compare`
    Writes to: none
    Reads from: none

SECTION: Closing CTA
  Copy: `Finals are coming. Find your course before the panic starts.`
  `Search available courses or request the one you need next.`
  Data shown: static. WhatsApp URL is hardcoded `https://wa.me/96179057533` (not `platform_settings.whatsapp_contact`).

  ELEMENT: Find your course pack →
    Action: navigate
    Destination: `/courses`
    Writes to: none
    Reads from: none

  ELEMENT: WhatsApp →
    Action: external link
    Destination: `https://wa.me/96179057533`
    Writes to: none
    Reads from: none

SECTION: Site footer
  Copy: Shared chrome
  Data shown: static
  ELEMENT: all Shared chrome footer elements apply

---

PAGE: /pricing
Purpose: Compare Focus / Balance / Full Load / Crunch and send the student into checkout.
Access: public
Entry points: Header/footer Pricing; homepage Compare full features; about is not a source; contact secondary “How access works” goes to `/access` which redirects here; `/faq` redirect; `/access` redirect; account flyout Upgrade plan; site map.

SECTION: Site header
  Copy: Shared chrome. Exception: logged-out `Get access` is hidden on this path.
  Data shown: Shared chrome
  ELEMENT: Shared chrome except `Get access`

SECTION: Hero
  Copy: `Pricing for AUB and LAU course packs`
  `Pick the access that matches your semester.`
  `Choose a plan and get instant access to course-specific study packs.`
  Data shown: static

  ELEMENT: View plans
    Action: navigate
    Destination: in-page `#plans` (smooth scroll)
    Writes to: none
    Reads from: none

SECTION: Plans
  Copy: Eyebrow `Plans`
  `Simple pricing. Clear coverage.`
  `Focus on one course, cover two difficult classes, unlock your semester load, or use Crunch for finals pressure.`
  Cards use `getPaymentPlans()` so admin overlays apply. Names, prices, unit labels, duration, benefits, and CTAs come from `lib/payments/catalog.ts`.
  Data shown: live plan records, not hardcoded dollar amounts.

  ELEMENT: Get Focus
    Action: navigate
    Destination: `/checkout?plan=focus`
    Writes to: none
    Reads from: none

  ELEMENT: Get Balance
    Action: navigate
    Destination: `/checkout?plan=balance`
    Writes to: none
    Reads from: none

  ELEMENT: Cover my semester
    Action: navigate
    Destination: `/checkout?plan=fullload`
    Writes to: none
    Reads from: none

  ELEMENT: Prep for Finals
    Action: navigate
    Destination: `/checkout?plan=crunch`
    Writes to: none
    Reads from: none

SECTION: Compare plans
  Copy: Eyebrow `Compare plans`
  `The full picture before you choose.`
  `Every plan includes the same course tutoring tools. The difference is how many courses you cover and how long access lasts.`
  Toggle closed label `Compare all features`; open label `Hide comparison`.
  Table headers: `Outcome` plus live plan names
  Rows from live plans + `tutoringMessagesLabels()`:
  Courses covered — catalog `coursesCoveredLabel` (1 / 2 / up to 6 / up to 4)
  Access duration — catalog `durationShortLabel`
  Lessons + reference sheets — checkmark (sr-only `Included`) all plans
  Flashcards + quizzes — Included all plans
  Exam prep + common traps — Included all plans
  AI tutor — fair-use tutoring message counts from `lib/payments/economics.ts` (not a credit wallet)
  Best for — catalog `comparisonBestFor`
  Footnote: tutoring messages are a fair-use allowance, not credits; core lessons/practice/review do not consume it.
  Data shown: `getPaymentPlans()` plus economics labels. Hash `#compare` auto-opens the table.

  ELEMENT: Compare all features / Hide comparison
    Action: toggle
    Destination: `#pricing-feature-table` (show/hide)
    Writes to: none
    Reads from: none

SECTION: Which plan
  Copy: `Which plan should I choose?`
  `Match the plan to the pressure.`
  `The right plan depends on how many courses need structure, not on a generic subscription tier.`
  Choose Focus — `Not sure if Gradeful is worth it? Try one course first. If it helps, you'll know before spending more.`
  Choose Balance — `Tried one course and it worked. Now cover the other one. Two courses for $7 more than Focus.`
  Choose Full Load — live copy from `choiceGuidance` (up to 6 courses at checkout).
  Choose Crunch — live copy from `choiceGuidance` (up to 4 courses for `{durationDays}` days).
  Data shown: `choiceGuidance(plans)` from `lib/payments/catalog.ts`

SECTION: FAQ
  Copy: Eyebrow `FAQ`
  `Answers before you choose a plan.`
  `What is inside, how access works, and what to do if your course is not listed yet.`
  Accordion from `PricingFaq` + live plans:
  `What is inside each pack?` / worked notes, formula sheet, flashcards, practice quizzes, exam review, mistake fixes for the specific AUB or LAU course
  `How long does access last?` / Focus, Balance, Full Load `{durationDays}` days; Crunch `{durationDays}` days
  `Can I share my account with friends?` / No, one student per account
  `Is there a free preview?` / Yes — open any course to see the pack structure before buying
  `What if my course is not listed?` / Request it on the courses page
  `Can I switch or upgrade my plan?` / Focus can move to Balance or Full Load by paying the difference via WhatsApp; self-serve upgrades are not live
  `Do I pick my courses when I buy?` / Focus 1, Balance 2, Full Load up to 6, Crunch up to 4. No schedule upload to buy.
  `Can I access on my phone?` / Yes
  `What is the refund policy?` / `REFUND_POLICY.studentCopy` from `lib/payments/transitions.ts`
  Footer: `Didn't find your answer?` `Ask directly and get a straight answer.`
  Data shown: live plans + refund policy module

  ELEMENT: [each FAQ question]
    Action: toggle
    Destination: that item’s answer region
    Writes to: none
    Reads from: none

  ELEMENT: Ask on WhatsApp
    Action: external link
    Destination: `https://wa.me/96179057533`
    Writes to: none
    Reads from: none

SECTION: Refund footnote
  Copy: `Refunds are reviewed for documented course withdrawals or drops with university confirmation. Contact us before requesting access if you are unsure about your enrollment.` then `Read our refund policy →`
  Data shown: static

  ELEMENT: Read our refund policy →
    Action: navigate
    Destination: `/refunds`
    Writes to: none
    Reads from: none

SECTION: Site footer
  Copy: Shared chrome
  Data shown: static
  ELEMENT: all Shared chrome footer elements apply

---

PAGE: /courses
Purpose: Browse AUB/LAU course packs and request a missing course.
Access: public
Entry points: Header/footer Courses; homepage search, marquee, Browse all courses, Preview a sample pack, Find your course pack; contact Browse courses; support Request a course; about Request a course; site map.

SECTION: Site header
  Copy: Shared chrome
  Data shown: Shared chrome
  ELEMENT: all Shared chrome elements apply

SECTION: Hero
  Copy: `AUB + LAU course packs`
  `Study smarter with packs made for your course.`
  `Access organized notes, formula sheets, flashcards, and exam review for the AUB and LAU courses you take.` plus the course compatibility statement.
  Image alt: `Gradeful study pack preview showing lecture notes, formula sheets, flashcards, and exam review`
  Data shown: static (`/assets/courses-hero-graphic.png`)

SECTION: Catalog error (only if `getCourses` fails)
  Copy: `Course catalog is being built. Check back soon.`
  `The public catalog is not available yet. The page is ready and will show courses once Supabase is connected and seeded.`
  Data shown: static strings from `catalogUnavailable()` in `lib/catalog.ts` (not database content)

SECTION: Filters (when catalog loads without issue)
  Copy: Screen-reader `Search courses by code, title, or university`. Placeholder: `Search by course code or name, e.g. MATH 201`. Group `aria-label` `Filter by university compatibility`. Tab labels `All` plus each `universities.short_name` present in offerings.
  Data shown: URL state `q` and `university` (stable short_name slug). Listing dataset is `getCourses({ limit: 200 })` then client ranker. Sample slug/title/code stay hidden. Active in-development courses are listed with status, not hidden for empty packs.

  ELEMENT: Search courses (input)
    Action: toggle
    Destination: `/courses?q=` via replaceState (shareable; does not break back/forward)
    Writes to: none
    Reads from: compact course codes, titles, offering aliases on `course_universities`, university names, topic keywords. Instructor names are secondary only.

  ELEMENT: All
    Action: toggle
    Destination: clears `university` query param via pushState
    Writes to: none
    Reads from: none

  ELEMENT: University compatibility tabs
    Action: toggle
    Destination: `/courses?university={short_name}` via pushState
    Writes to: none
    Reads from: offerings on `course_universities` plus primary `courses.university_id` fallback

  ELEMENT: Clear filters
    Action: toggle
    Destination: `/courses` via pushState
    Writes to: none
    Reads from: Visible when a university filter or search is set

SECTION: Empty catalog (loaded, zero listed courses)
  Copy: `Course catalog is being built. Check back soon.`
  `Published courses will appear here once the catalog is seeded.`
  Data shown: static (this empty state is separate from the fetch-issue banner)

SECTION: Filtered empty
  Copy: `No courses found for "{search}"` (search preserved) then `Keep your search. Request the course so Gradeful can prioritize it.`
  Data shown: `{search}` is the typed filter string.

  ELEMENT: Request this course →
    Action: navigate
    Destination: in-page `#course-request` (opens request collapsible and prefills from search)
    Writes to: none
    Reads from: current search string used as prefill

SECTION: Course table
  Copy: Column headers `Course` `Compatibility` `Coverage` `Last reviewed` `Action`
  Row: course title and `courses.code`; compatibility badges from `course_universities` (neutral text, no logos); availability (`Available` / `Preview available` / `In development`) and coverage (`Full support` / `Core topics` / `Early access`) derived from published module items; last reviewed from max `module_pack_items.updated_at`; one action (`Open course` / `Preview` / `Notify me`).
  Data shown: `courses.id`, `courses.slug`, `courses.code`, `courses.title`, offering codes, computed availability/coverage.

  ELEMENT: Open course / Preview (row link)
    Action: navigate
    Destination: `/courses/[courses.slug]`
    Writes to: none
    Reads from: `courses.slug`

  ELEMENT: Notify me
    Action: toggle
    Destination: in-page `#course-request`
    Writes to: none
    Reads from: course code prefill

SECTION: Pagination
  Copy: `Show` `{10|20|50}` `per page` then `{start}–{end} of {totalItems}`. Ellipsis `…`. Controls `aria-label` `Previous page` / `Next page` / `Courses per page` / `Course list pagination`.
  Data shown: Counts from the filtered in-memory list, not a SQL offset.

  ELEMENT: 10 / 20 / 50
    Action: toggle
    Destination: none (changes page size)
    Writes to: none
    Reads from: none

  ELEMENT: Previous page
    Action: toggle
    Destination: none (previous page)
    Writes to: none
    Reads from: disabled on page 1

  ELEMENT: [page number]
    Action: toggle
    Destination: none (that page)
    Writes to: none
    Reads from: none

  ELEMENT: Next page
    Action: toggle
    Destination: none (next page)
    Writes to: none
    Reads from: disabled on last page

SECTION: Course request collapsible
  Copy: Closed header: `Don't see your course?` `Request it and we'll prioritize by demand.` trailing `Request a course`
  Open eyebrow: `Course request`
  Fields: `Course code` placeholder `MATH201`; `Course name` placeholder `Calculus III`; `University` with `Select university` / `AUB` / `LAU` / `Other`; `Your email` placeholder `your@university.edu`; `Notes` placeholder `Instructor name, section, anything specific about what you need`
  Submit: `Send request` or `Sending...`
  Success: `Request sent.` `We prioritize by demand.`
  Error body uses `formError` when insert fails: `Request could not be sent yet. The public request table may still be setting up.` Field errors include `Enter a valid email address.` and `Add a university, course code, or course title.` and `Keep this under {n} characters.`
  Data shown: Success is client state. Insert writes `course_requests`. Prefill may copy the catalog search string into code or title.

  ELEMENT: Request a course (header button)
    Action: toggle
    Destination: expands the form (`aria-expanded`)
    Writes to: none
    Reads from: none

  ELEMENT: Send request / Sending...
    Action: submit
    Destination: none (stays on `/courses`; success state in place)
    Writes to: `course_requests.id`, `course_requests.email`, `course_requests.university_name`, `course_requests.course_code`, `course_requests.course_title`, `course_requests.instructor_name` (hidden empty), `course_requests.notes`
    Reads from: none

SECTION: Site footer
  Copy: Shared chrome
  Data shown: static
  ELEMENT: all Shared chrome footer elements apply

---

PAGE: /courses/[slug]
Purpose: Public preview of one course pack (what’s included, locked module outline) and checkout CTA.
Access: public (page itself). `View formula sheet` may send a logged-out user toward a protected study URL.
Entry points: Homepage search/marquee/cards; courses table; direct/shared URL; internal course links.

SECTION: Site header
  Copy: Shared chrome
  Data shown: Shared chrome
  ELEMENT: all Shared chrome elements apply

SECTION: Load error
  Copy: `Back to courses`
  `Couldn't load this course right now. Try refreshing.`
  Data shown: static. Shown when `getCourseBySlug` throws/returns an issue.

  ELEMENT: Back to courses
    Action: navigate
    Destination: `/courses`
    Writes to: none
    Reads from: none

(Unknown slug uses Next `notFound()` — custom 404, not this page.)

SECTION: Course header
  Copy: Breadcrumb `Courses` / `{subject}` / `{courses.title}` where subject is mapped from course-code prefix (`BIOL`→`Biology`, `CHEM`→`Chemistry`, `ECON`→`Economics`, `MATH`/`MTH`→`Mathematics`, `MECH`→`Mechanical Engineering`, `PHYS`→`Physics`, else the letter prefix or `Course`).
  Badges: `{universities.short_name} · {courseCode}` from `getUniversityCourses`.
  Heading: `courses.title`
  Optional body: `courses.description`
  Data shown: `courses.title`, `courses.description`, `courses.code`, `courses.slug`, `universities.short_name`, related university course codes. JSON-LD BreadcrumbList uses those same fields.

  ELEMENT: Courses
    Action: navigate
    Destination: `/courses`
    Writes to: none
    Reads from: none

  ELEMENT: Get access →
    Action: navigate
    Destination: `/checkout?plan=focus&course={courses.id}`
    Writes to: none
    Reads from: `courses.id`

  ELEMENT: View formula sheet
    Action: navigate
    Destination: `/my-courses/{courses.slug}?module={course_modules.id}&tab=formula_sheet` when a module has `course_modules.is_free_preview` and a `formula_sheet` pack item
    Writes to: none
    Reads from: `course_modules.is_free_preview`, `module_pack_items.item_type`, `courses.slug`

  ELEMENT: Unlock formula sheet
    Action: navigate
    Destination: `/pricing`
    Writes to: none
    Reads from: Shown when no free-preview formula sheet exists

SECTION: What's included
  Copy: `What's included`
  Cards: `Notes` `Formula sheet` `Flashcards` `Practice quiz` `Exam review` `Mistake fixes`
  Data shown: static labels (not read from `module_pack_items` on this grid)

SECTION: Course modules
  Copy: `Course modules`
  Shared-track rows: `course_modules.title`; status chips `Not published yet` (no visible pack items) or `Up next` when `course_modules.status === "up_next"`; expanded chips `{module_pack_items.label}` optionally ` · {module_pack_items.count}` (label omitted when stored as empty/`default`).
  Divider: `Tracks split by school from here`
  Columns: `AUB {aubCourse.courseCode ?? courses.code} track` and `LAU {lauCourse.courseCode ?? courses.code} track` listing locked `course_modules.title` for `track` `aub` / `lau`.
  Data shown: `course_modules.title`, `course_modules.sort_order`, `course_modules.status`, `course_modules.track`, `course_modules.is_free_preview`, `module_pack_items.label`, `module_pack_items.count`, `module_pack_items.item_type` (flagged items hidden). Modules with zero student-visible items treated as unpublished.

  ELEMENT: [module title] (shared-track row)
    Action: toggle
    Destination: expands that module’s pack-item chips
    Writes to: none
    Reads from: `course_modules.title`; first shared module defaults open

  ELEMENT: Get access to unlock all modules →
    Action: navigate
    Destination: `/checkout?plan=focus&course={courses.id}`
    Writes to: none
    Reads from: `courses.id`

SECTION: Site footer
  Copy: Shared chrome
  Data shown: static
  ELEMENT: all Shared chrome footer elements apply

---

PAGE: /about
Purpose: Founder story, product beliefs, independence disclaimer, and contact.
Access: public
Entry points: Header About; account Learn more → About Gradeful; site map.

SECTION: Site header
  Copy: Shared chrome
  Data shown: Shared chrome
  ELEMENT: all Shared chrome elements apply

SECTION: Why Gradeful exists
  Copy: `Why Gradeful exists`
  `Built by an engineering student who couldn't make sense of Dynamics.`
  `The rules weren't hard. The textbook made them hard.`
  `So I built a pack for Dynamics. Worked notes. A formula sheet that organized the rules by type. Practice problems that escalated. It made the course click — not because it leaked an exam, but because the ideas finally had a path.`
  `That was the idea. If one pack could do that for Dynamics, the same approach could work for every course at AUB and LAU.`
  Data shown: static

SECTION: What we believe
  Copy: `What we believe`
  `01` `Generic material fails specific courses.` `MATH201 is not the same as a random Calculus III playlist. Your course has a scope. Your workspace should follow that scope — without pretending to be the university.`
  `02` `Organized beats comprehensive.` `A 200-page textbook is not the problem. Not knowing which ideas to practice first is. Gradeful sequences lessons, drills, and review.`
  `03` `Practice should feel like pressure.` `Original quizzes and practice exams train the method. They are not copies of anyone's live assessment.`
  Data shown: static

SECTION: Independence
  Copy: `Independence`
  Canonical independence statement from `lib/brand/copy.ts`.
  Data shown: static

SECTION: Get in touch
  Copy: `Get in touch`
  `Questions about a specific course, a workspace you think is missing, or anything else. Reach out directly.`
  Data shown: static

  ELEMENT: WhatsApp
    Action: external link
    Destination: `https://wa.me/96179057533`
    Writes to: none
    Reads from: none

  ELEMENT: Request a course
    Action: navigate
    Destination: `/courses#course-request`
    Writes to: none
    Reads from: none

SECTION: Site footer
  Copy: Shared chrome
  Data shown: static
  ELEMENT: all Shared chrome footer elements apply

---

PAGE: /contact
Purpose: Public intake for course requests and support topics.
Access: public
Entry points: Policies Contact Gradeful; checkout/support copy linking `/contact`; direct URL.

SECTION: Site header
  Copy: Shared chrome
  Data shown: Shared chrome
  ELEMENT: all Shared chrome elements apply

SECTION: Hero
  Copy: Eyebrow `Contact Gradeful`
  `Requests, corrections, and launch questions in one place.`
  `The most useful message is specific: university, course code, course title, instructor if relevant, and the kind of material that would help.`
  Badges: `Course requests` `Corrections` `Access questions`
  Decorative visual label `contact`
  Data shown: static

  ELEMENT: Browse courses
    Action: navigate
    Destination: `/courses`
    Writes to: none
    Reads from: none

  ELEMENT: How access works
    Action: navigate
    Destination: `/access` (immediately redirects to `/pricing`)
    Writes to: none
    Reads from: none

SECTION: What to send
  Copy: Eyebrow `What to send`
  `Make the request specific enough to act on.`
  `Short, precise course details are more useful than broad feedback. If you are reporting an error, include the course and section where you saw it.`
  Course requests — `Tell Gradeful which AUB or LAU course, instructor, or topic should be prioritized next.`
  Access questions — `Ask about pricing, payment confirmation, or course coverage.`
  Corrections — `Flag unclear explanations, possible mistakes, missing formulas, or outdated course details.`
  Campus partners — `Students, ambassadors, tutors, and reviewers can reach out when campus programs open.`
  Support band: `Support` `Send the course details Gradeful needs to help.` `For fastest support, include your university, course code, instructor if relevant, and the specific material or access question.`
  Data shown: static

SECTION: Request a course form
  Copy: `Request a course`
  `Tell Gradeful what you need next.`
  `Share the course code, university, and anything specific your section needs. We use requests to prioritize what gets built next.`
  Labels: `Email` placeholder `you@example.com`; `University` placeholder `AUB or LAU`; `Course code` placeholder `MATH201`; `Course title` placeholder `Calculus III`; `Instructor` placeholder `Instructor name, if relevant`; `Notes` placeholder `Tell us what materials would help.`
  Submit: `Submit request` / `Sending...`
  Success: `Request sent` `Gradeful has your course request.` `Reference number: {course_requests.id}` `Gradeful can use this to prioritize courses.`
  Fail banner: `Request could not be sent yet. The public request table may still be setting up.`
  Data shown: Success reference is the generated `course_requests.id`.

  ELEMENT: Submit request / Sending...
    Action: submit
    Destination: none (success state on `/contact`)
    Writes to: `course_requests.id`, `course_requests.email`, `course_requests.university_name`, `course_requests.course_code`, `course_requests.course_title`, `course_requests.instructor_name`, `course_requests.notes`
    Reads from: none

SECTION: Site footer
  Copy: Shared chrome
  Data shown: static
  ELEMENT: all Shared chrome footer elements apply

---

PAGE: /support
Purpose: Public help landing (same Help & contact content used in settings).
Access: public
Entry points: Direct URL; account scheme `Get help` href is `/support` but the header flyout currently opens `/dashboard?settings=help` instead; site map does not list it.

SECTION: Site header
  Copy: Shared chrome
  Data shown: Shared chrome
  ELEMENT: all Shared chrome elements apply

SECTION: Help & contact
  Copy: `Help & contact`
  `Reach out or browse common answers`
  Chat with us — `Usually responds within a day`
  Request a missing course — `We prioritize by demand`
  Policy links: `Privacy Policy` `Terms of Service` `Refund Policy` `Academic Integrity`
  Data shown: static

  ELEMENT: Open WhatsApp
    Action: external link
    Destination: `https://wa.me/96179057533`
    Writes to: none
    Reads from: none

  ELEMENT: Request a course
    Action: navigate
    Destination: `/courses#course-request`
    Writes to: none
    Reads from: none

  ELEMENT: Privacy Policy
    Action: navigate
    Destination: `/privacy`
    Writes to: none
    Reads from: none

  ELEMENT: Terms of Service
    Action: navigate
    Destination: `/terms`
    Writes to: none
    Reads from: none

  ELEMENT: Refund Policy
    Action: navigate
    Destination: `/refunds`
    Writes to: none
    Reads from: none

  ELEMENT: Academic Integrity
    Action: navigate
    Destination: `/academic-integrity`
    Writes to: none
    Reads from: none

SECTION: Site footer
  Copy: Shared chrome
  Data shown: static
  ELEMENT: all Shared chrome footer elements apply

---

PAGE: /tutoring
Purpose: Public tutoring/help request intake (stored as a course request).
Access: public
Entry points: Direct URL (not in main nav or footer).

SECTION: Site header
  Copy: Shared chrome
  Data shown: Shared chrome
  ELEMENT: all Shared chrome elements apply

SECTION: Hero
  Copy: `Need help beyond a study pack?`
  `Need a tutor for your course?`
  `Request help for an AUB or LAU course when you need more than a pack: a difficult chapter, a topic explanation, exam prep, or a course that is not covered yet.`
  Side card: `How it works` `This is a request path, not a full tutoring marketplace.` `Gradeful collects the course and topic details first. If help is available, the team can follow up with next steps. If not, the request still helps prioritize future packs and support.`
  Data shown: static

  ELEMENT: Request help
    Action: navigate
    Destination: in-page `#request`
    Writes to: none
    Reads from: none

  ELEMENT: View study pack plans
    Action: navigate
    Destination: `/pricing`
    Writes to: none
    Reads from: none

SECTION: Use cases
  Copy: Eyebrow `Use cases`
  `Ask for specific help, not vague tutoring.`
  `The better the course and topic details, the easier it is to tell whether Gradeful can help.`
  You are stuck before an exam — `Send the course, topic, and deadline so Gradeful can understand what kind of help would actually matter.`
  One chapter is blocking everything — `Ask for help with a specific chapter instead of searching through generic videos and old group chats.`
  You need a topic explained — `Use the request form for a focused explanation path connected to the course you are taking.`
  Your course is not covered yet — `Requests help Gradeful see which AUB and LAU courses need packs or support next.`
  Data shown: static

SECTION: Request form
  Copy: Eyebrow `Request`
  `Tell us the course, topic, and kind of help you want.`
  `This uses Gradeful's existing safe public request system. No payment is collected here, and no tutor match is promised before Gradeful reviews the request.`
  Form: `Request help` `Tell us what you are stuck on.` `This is an intake request. It does not promise a tutor match, but it gives Gradeful the details needed to respond clearly.`
  Labels: `Email` placeholder `you@example.com`; `University` `Select university` / `AUB` / `LAU` / `Other`; `Course code` placeholder `MATH201`; `Topic or chapter` placeholder `Sequences, organic mechanisms, circuits...`; `Preferred help type` options `Exam prep` / `Specific chapter` / `Topic explanation` / `Course not covered yet`; `Details` placeholder `Tell us what you need help understanding and when your exam or quiz is.`
  Submit: `Send help request` / `Sending...`
  Success: `Request sent` `Gradeful has your help request.` `Reference number: {id}. The team can use this to understand what course support students need next.`
  Data shown: Success `{id}` is `course_requests.id`. Topic is stored in `course_requests.course_title` (fallback `Tutoring request`). Notes are composed as `Tutoring/help request` plus preferred help type, topic, and details (max 500 chars).

  ELEMENT: Send help request / Sending...
    Action: submit
    Destination: none (success state on `/tutoring`)
    Writes to: `course_requests.id`, `course_requests.email`, `course_requests.university_name`, `course_requests.course_code`, `course_requests.course_title`, `course_requests.instructor_name` (empty), `course_requests.notes`
    Reads from: none

SECTION: Site footer
  Copy: Shared chrome
  Data shown: static
  ELEMENT: all Shared chrome footer elements apply

---

PAGE: /policies
Purpose: Index of legal/trust pages.
Access: public
Entry points: Account flyout All policies; direct URL.

SECTION: Site header
  Copy: Shared chrome
  Data shown: Shared chrome
  ELEMENT: all Shared chrome elements apply

SECTION: Hero
  Copy: Eyebrow `Policies`
  `The trust layer behind the study packs.`
  `These pages set public expectations for privacy, access, refunds, academic integrity, and original study material.`
  Badges: `Privacy` `Terms` `Academic integrity`
  Data shown: static

  ELEMENT: Contact Gradeful
    Action: navigate
    Destination: `/contact`
    Writes to: none
    Reads from: none

  ELEMENT: Integrity rules
    Action: navigate
    Destination: `/academic-integrity`
    Writes to: none
    Reads from: none

SECTION: Policy library
  Copy: Eyebrow `Policy library`
  `Legal and trust pages students can actually scan.`
  `These policies are written for the public site and paid course-pack access. They should be reviewed by counsel before a major launch.`
  Rows from `policyLinks`: `Privacy Policy` note `Student, schedule, usage, and support data.`; `Terms of Service` note `Rules for course packs and platform use.`; `Refund Policy` note `Course drops, withdrawals, and access issues.`; `Academic Integrity` note `Honest studying, practice, AI expectations, and original content.` Each row CTA `Read policy`.
  Data shown: static

  ELEMENT: Privacy Policy / Read policy
    Action: navigate
    Destination: `/privacy`
    Writes to: none
    Reads from: none

  ELEMENT: Terms of Service / Read policy
    Action: navigate
    Destination: `/terms`
    Writes to: none
    Reads from: none

  ELEMENT: Refund Policy / Read policy
    Action: navigate
    Destination: `/refunds`
    Writes to: none
    Reads from: none

  ELEMENT: Academic Integrity / Read policy
    Action: navigate
    Destination: `/academic-integrity`
    Writes to: none
    Reads from: none

SECTION: Operating principles
  Copy: Eyebrow `Operating principles` `The short version.` `Every policy should reinforce the same product posture: useful study help, honest access, original content, and no university-affiliation implication.`
  Independent — `Gradeful is not affiliated with AUB, LAU, or any instructor.`
  Original — `Published study material should be independently created and reviewed.`
  Student-safe — `Policies explain data, access, refunds, and fair-use expectations in plain language.`
  Data shown: static

SECTION: Site footer
  Copy: Shared chrome
  Data shown: static
  ELEMENT: all Shared chrome footer elements apply

---

PAGE: /privacy
Purpose: Privacy policy.
Access: public
Entry points: Footer; policy nav; policies index; account Learn more; support list; site map.

SECTION: Site header
  Copy: Shared chrome
  Data shown: Shared chrome
  ELEMENT: all Shared chrome elements apply

SECTION: Policy nav
  Copy: `Privacy Policy` `Terms of Service` `Refund Policy` `Academic Integrity`
  Data shown: Active path styles the current pill.

  ELEMENT: Privacy Policy
    Action: navigate
    Destination: `/privacy`
    Writes to: none
    Reads from: `usePathname()` for active state

  ELEMENT: Terms of Service
    Action: navigate
    Destination: `/terms`
    Writes to: none
    Reads from: pathname

  ELEMENT: Refund Policy
    Action: navigate
    Destination: `/refunds`
    Writes to: none
    Reads from: pathname

  ELEMENT: Academic Integrity
    Action: navigate
    Destination: `/academic-integrity`
    Writes to: none
    Reads from: pathname

SECTION: Title
  Copy: `Privacy Policy`
  `Last updated: June 2026`
  Data shown: static

SECTION: Overview
  Copy: `Overview`
  `Gradeful is an independent study platform for AUB and LAU students. We collect only the information needed to run course previews, course requests, support, safety, and paid course-pack features.`
  `Gradeful is not affiliated with AUB, LAU, or any instructor. Do not upload university documents, professor materials, exam files, or private third-party content unless you have the right to share them.`
  Data shown: static

SECTION: Information we may collect
  Copy: `Information we may collect`
  `Contact details, such as email address or support messages.`
  `Course request details, such as university, course code, course title, instructor name, and notes.`
  `Account details when accounts are active, such as login method, course access, and subscription status.`
  `Schedule or enrollment proof if required to verify course access.`
  `Payment and access records when paid access is active.`
  `Usage, device, and security data, such as pages visited, feature usage, IP address, browser, and device identifiers.`
  `Content feedback, correction reports, and academic-integrity reports.`
  Data shown: static

SECTION: How we use information
  Copy: `How we use information`
  `To provide course previews, course packs, and student support.`
  `To prioritize new AUB and LAU course content.`
  `To verify access, payment status, and course eligibility.`
  `To prevent abuse, account sharing abuse, scraping, or cheating.`
  `To improve explanations, quizzes, and platform reliability.`
  `To comply with legal, safety, and academic-integrity obligations.`
  Data shown: static

SECTION: AI and study assistance
  Copy: `AI and study assistance`
  `If Gradeful offers AI study assistance, prompts and interactions may be processed to provide answers, improve safety, prevent abuse, and debug the product. AI features should be grounded in Gradeful course material and are not a substitute for university rules or instructor guidance.`
  Data shown: static

SECTION: Sharing information
  Copy: `Sharing information`
  `Gradeful may share information with service providers that help run the platform, including hosting, database, analytics, email, payment, support, security, and AI providers. We may also disclose information when required by law, to protect rights and safety, or to investigate misuse.`
  `Gradeful does not sell student personal information as a standalone data product.`
  Data shown: static

SECTION: Retention and deletion
  Copy: `Retention and deletion`
  `We keep information for as long as needed to provide the platform, keep records, prevent abuse, resolve disputes, and comply with legal obligations. Students may request deletion of personal data, subject to records Gradeful must keep for security, payment, academic-integrity, or legal reasons.`
  Data shown: static

SECTION: Student responsibilities
  Copy: `Student responsibilities`
  `Students should not submit confidential, copyrighted, or restricted course materials. If you believe content on Gradeful includes protected material or personal information, contact Gradeful with the course, page, and details so it can be reviewed.`
  Data shown: static

SECTION: Site footer
  Copy: Shared chrome
  Data shown: static
  ELEMENT: all Shared chrome footer elements apply

---

PAGE: /terms
Purpose: Terms of service.
Access: public
Entry points: Footer; policy nav; policies index; account Learn more; support; site map.

SECTION: Site header
  Copy: Shared chrome
  Data shown: Shared chrome
  ELEMENT: all Shared chrome elements apply

SECTION: Policy nav
  Copy: Same four pills as `/privacy`
  Data shown: pathname for active pill
  ELEMENT: same four policy nav links as `/privacy`

SECTION: Title
  Copy: `Terms of Service`
  `Last updated: June 2026`
  Data shown: static

SECTION: Independent platform
  Copy: `Independent platform`
  `Gradeful is an independent academic support platform. It is not affiliated with, sponsored by, or endorsed by AUB, LAU, any university, or any instructor.`
  Data shown: static

SECTION: Educational use
  Copy: `Educational use`
  `Gradeful provides study materials, previews, practice tools, and course-pack features to support self-study. Gradeful does not guarantee grades, exam results, course outcomes, admission outcomes, or academic standing.`
  `Students remain responsible for attending class, reading official course material, following instructor requirements, and obeying university policies.`
  Data shown: static

SECTION: Accounts and access
  Copy: `Accounts and access`
  `Each account is intended for the approved student or approved access group only. Gradeful may limit access, revoke access, or request verification if it detects misuse, suspicious activity, or account sharing outside the allowed plan.`
  Data shown: static

SECTION: Payments and refunds
  Copy: `Payments and refunds`
  `Paid access and payment confirmation are subject to the plan shown at purchase. Refunds are handled under the Refund Policy.`
  Data shown: static

SECTION: Academic integrity
  Copy: `Academic integrity`
  `Students may not use Gradeful to cheat, plagiarize, submit generated or copied work as their own, bypass exam rules, or violate university policy. Gradeful may remove users or reports connected to misuse.`
  Data shown: static

SECTION: Content ownership and restrictions
  Copy: `Content ownership and restrictions`
  `Gradeful content may not be copied, resold, scraped, uploaded to other platforms, or redistributed without permission. Students may not upload professor slides, exams, copyrighted books, paid materials, or private university files unless they have permission.`
  Data shown: static

SECTION: Availability and changes
  Copy: `Availability and changes`
  `Gradeful may change course availability, pricing, features, policies, and access rules as the platform develops. Public catalog pages may show setup states when course data is unavailable.`
  Data shown: static

SECTION: Limitation of responsibility
  Copy: `Limitation of responsibility`
  `Gradeful is provided as a study support service. To the maximum extent allowed by law, Gradeful is not responsible for indirect losses, academic decisions, missed deadlines, exam performance, or university disciplinary outcomes.`
  Data shown: static

SECTION: Site footer
  Copy: Shared chrome
  Data shown: static
  ELEMENT: all Shared chrome footer elements apply

---

PAGE: /refunds
Purpose: Refund policy.
Access: public
Entry points: Footer; policy nav; pricing footnote; account Learn more; support; site map.

SECTION: Site header
  Copy: Shared chrome
  Data shown: Shared chrome
  ELEMENT: all Shared chrome elements apply

SECTION: Policy nav
  Copy: Same four pills as `/privacy`
  Data shown: pathname
  ELEMENT: same four policy nav links as `/privacy`

SECTION: Title
  Copy: `Refund Policy`
  `Last updated: June 2026`
  Data shown: static

SECTION: General rule
  Copy: `General rule`
  `Gradeful generally issues refunds only for official course withdrawal or course drop cases that are verified with university documentation.`
  `Refund requests are reviewed under this policy. Submitting a request does not guarantee approval.`
  Data shown: static

SECTION: Usually eligible for review
  Copy: `Usually eligible for review`
  `Official withdrawal from the course connected to the paid pack.`
  `Official course drop with documentation showing the relevant course and date.`
  `Duplicate payment for the same student and same access period.`
  `A Gradeful-side access issue that prevents use and cannot be resolved within a reasonable support window.`
  Data shown: static

SECTION: Usually not refundable
  Copy: `Usually not refundable`
  `Change of mind after access is approved.`
  `Partial semester use or low usage.`
  `Exam failure, grade dissatisfaction, or course difficulty.`
  `Missing university deadlines or ignoring official course rules.`
  `Account removal for cheating, redistribution, abuse, or other terms violations.`
  Data shown: static

SECTION: How to request a refund
  Copy: `How to request a refund`
  `Send the student name or account email, course code, plan purchased, payment reference if available, and official university drop or withdrawal documentation. Gradeful may ask for additional verification before approving a refund.`
  Data shown: static

SECTION: Processing
  Copy: `Processing`
  `Approved refunds are normally returned through the original payment path when possible. Processing time may depend on the payment method, bank, wallet, or gateway.`
  Data shown: static

SECTION: Site footer
  Copy: Shared chrome
  Data shown: static
  ELEMENT: all Shared chrome footer elements apply

---

PAGE: /academic-integrity
Purpose: Academic integrity and original-content rules.
Access: public
Entry points: Footer; policy nav; policies Integrity rules; account Learn more; support; `/content-standards` permanent redirect; site map.

SECTION: Site header
  Copy: Shared chrome
  Data shown: Shared chrome
  ELEMENT: all Shared chrome elements apply

SECTION: Policy nav
  Copy: Same four pills as `/privacy`
  Data shown: pathname
  ELEMENT: same four policy nav links as `/privacy`

SECTION: Title
  Copy: `Academic Integrity`
  `Last updated: June 2026`
  Data shown: static

SECTION: What Gradeful supports
  Copy: `What Gradeful supports`
  `Reviewing concepts before class, quizzes, exams, or finals.`
  `Practicing with original questions and worked examples.`
  `Learning from mistakes and improving problem-solving habits.`
  `Using flashcards, formula sheets, and summaries for recall.`
  `Asking for explanations that help you understand the method.`
  Data shown: static

SECTION: What Gradeful does not allow
  Copy: `What Gradeful does not allow`
  `Using Gradeful during an exam, quiz, or assessment if prohibited.`
  `Submitting Gradeful explanations or AI output as your own work.`
  `Uploading professor slides, private exams, answer keys, or restricted files.`
  `Requesting solutions to active graded assignments in a dishonest way.`
  `Redistributing paid Gradeful materials outside the approved access plan.`
  Data shown: static

SECTION: AI study help
  Copy: `AI study help`
  `If AI features are available, they should explain concepts, diagnose mistakes, and guide practice. They should not be used to bypass learning, complete prohibited work, or violate course rules.`
  Data shown: static

SECTION: Student contributions
  Copy: `Student contributions`
  `Students, tutors, or reviewers who contribute material must only provide content they have the right to share. Contributions should be original, consented, and free of protected institutional material.`
  Data shown: static

SECTION: Original study material
  Copy: `Original study material`
  `Gradeful materials should be independently created from student knowledge, tutor expertise, public concepts, and original synthesis. Content should explain ideas in Gradeful's own words and examples.`
  `Gradeful should not publish professor slides, private course files, copyrighted textbooks, restricted exams, paid answer keys, or institutional materials without permission.`
  Data shown: static

SECTION: Review and corrections
  Copy: `Review and corrections`
  `Collect student pain points and common confusion areas.`
  `Create notes, examples, and practice from original synthesis.`
  `Review for accuracy with tutors or subject-matter reviewers.`
  `Use AI-assisted checks for consistency where helpful.`
  `Publish with a correction pathway for students.`
  `Students should be able to report unclear explanations, errors, outdated course details, or suspected protected material. Reports should include the course code, pack, section, and a clear description of the issue.`
  Data shown: static

SECTION: Removal requests
  Copy: `Removal requests`
  `If someone believes Gradeful content infringes rights or includes restricted material, they should send enough detail for review. Gradeful may remove or revise content while the report is investigated.`
  Data shown: static

SECTION: Reports and enforcement
  Copy: `Reports and enforcement`
  `Gradeful may remove content, restrict access, investigate reports, or suspend accounts connected to cheating, copyright misuse, harassment, scraping, redistribution, or academic-integrity violations.`
  Data shown: static

SECTION: Site footer
  Copy: Shared chrome
  Data shown: static
  ELEMENT: all Shared chrome footer elements apply

---

PAGE: /site-map
Purpose: Internal public route tracker (not linked from header/footer).
Access: public
Entry points: Direct URL only.

SECTION: Site header
  Copy: Shared chrome
  Data shown: Shared chrome
  ELEMENT: all Shared chrome elements apply

SECTION: Hero
  Copy: Eyebrow `Route dashboard`
  `Every public Gradeful route in one polished tracker.`
  `Use this page to check the live homepage sections, public pages, policies, and account routes.`
  Badges: `Public pages` `Account` `Policies`
  Data shown: static

  ELEMENT: Back home
    Action: navigate
    Destination: `/`
    Writes to: none
    Reads from: none

SECTION: Counts
  Copy: Number `{publicRoutes.length}` then `Public pages`. Number `{accountRoutes.length}` then `Account routes`. Number `{policyLinks.length}` then `Policy routes`.
  Data shown: Counts are `publicRoutes.length` (Homepage, Sign in, Sign up, Forgot password, Reset password, plus `Courses`/`Pricing`/`About` from `siteNavLinks`), `accountRoutes.length` (1), `policyLinks.length` (4). Not database.

SECTION: Public pages
  Copy: Heading `Public pages` and `{n} routes`. Each card: label, path, note, badge `Open`.
  Homepage — `/` — `Main public search homepage.`
  Sign in — `/login` — `Email, social, and phone authentication entry point.`
  Sign up — `/signup` — `Student account creation and email verification entry point.`
  Forgot password — `/forgot-password` — `Password recovery email request form.`
  Reset password — `/reset-password` — `Secure new-password form for recovery links.`
  Courses — `/courses` (no note on `siteNavLinks`)
  Pricing — `/pricing`
  About — `/about`
  Data shown: static lists in `app/site-map/page.tsx` + `siteNavLinks`

  ELEMENT: Open (each row)
    Action: navigate
    Destination: that row’s `href`
    Writes to: none
    Reads from: none

SECTION: Account
  Copy: `Account` `{n} routes`
  My courses — `/my-courses` — `Protected dashboard for active student course access.`
  Data shown: static

  ELEMENT: Open
    Action: navigate
    Destination: `/my-courses` (middleware sends logged-out users to login)
    Writes to: none
    Reads from: none

SECTION: Policies
  Copy: `Policies` `{n} routes` plus the four `policyLinks` labels/notes from Shared chrome / `PublicPageShell`
  Data shown: static `policyLinks`

  ELEMENT: Open
    Action: navigate
    Destination: `/privacy` `/terms` `/refunds` `/academic-integrity`
    Writes to: none
    Reads from: none

SECTION: Homepage sections
  Copy: `Homepage sections` `{n} routes`
  Homepage hero — `/` — `Search-first landing hero.`
  What's inside — `/` — `Homepage tutoring tools overview.`
  Data shown: static

  ELEMENT: Open
    Action: navigate
    Destination: `/`
    Writes to: none
    Reads from: none

SECTION: Site footer
  Copy: Shared chrome
  Data shown: static
  ELEMENT: all Shared chrome footer elements apply

---

PAGE: /faq
Purpose: Legacy FAQ URL; not a rendered page.
Access: public
Entry points: Old bookmarks/external links.

SECTION: Redirect
  Copy: none (no page body)
  Data shown: none
  ELEMENT: none — `next.config.ts` redirects `/faq` → `/pricing` (temporary)

---

PAGE: /access
Purpose: Legacy “how access works” URL; not a rendered page.
Access: public
Entry points: Contact hero `How access works`; leftover `/access` links.

SECTION: Redirect
  Copy: none (no page body)
  Data shown: none
  ELEMENT: none — `redirect("/pricing")`

---

PAGE: /content-standards
Purpose: Legacy content-standards URL; not a rendered page.
Access: public
Entry points: Old bookmarks/external links.

SECTION: Redirect
  Copy: none (no page body)
  Data shown: none
  ELEMENT: none — `permanentRedirect("/academic-integrity")`

---

PAGE: /markitup
Purpose: Separate public cinematic site for MarkItUp (Beirut marketing studio). Does not use Gradeful header/footer or brand tokens.
Access: public
Entry points: Direct URL only (not in Gradeful nav/footer).

SECTION: Preloader (skipped when `prefers-reduced-motion: reduce`)
  Copy: `Loading`
  Skip control uses `Skip the sequence`
  Giant numeric progress `00`–`100` (dynamic)
  `Lighting the street`
  `markitup.lb`
  Data shown: Progress is client animation, not a database.

  ELEMENT: Skip the sequence
    Action: toggle
    Destination: none (dismisses preloader)
    Writes to: none
    Reads from: none

SECTION: Fixed header
  Copy: Wordmark `MARKIT` + `U` (rocket glyph). Instagram chip `@{markitup.handle}` → `@markitup.lb`
  Data shown: static `lib/markitup/content.ts`

  ELEMENT: @markitup.lb
    Action: external link
    Destination: `https://www.instagram.com/markitup.lb/`
    Writes to: none
    Reads from: none

SECTION: Scroll film
  Copy: Screen-reader h1: `We don’t just market. We move brands.`
  Hint: `Scroll`
  Chapter copy is dynamic from scroll progress (`lib/markitup/film.ts` `filmChapters`), not a CMS:
  `01 — Beirut, at night` / `A quiet brand is already falling.` / `Lights off. Nobody looking. The rocket waits in the window like a thing that never launched.`
  `02 — The mark` / `We don’t just market.` / `A gold line is drawn. The object becomes a mark. The night starts to move.`
  `03 — Consulting` / `We sit with the business first.` / `Through the glass, onto the table. Maps, the gap, the rocket in the centre.`
  `04 — Strategy` / `Every post is a move.` / `The rocket lifts. Gold lines connect. Noise becomes a system.`
  `05 — Full-service` / `We move brands.` / `The street is the same. The window is not. The rocket leaves the sill.`
  `06 — MarkItUp` / `DM us to get started.` / `Consulting. Strategy. Full-service packages. The work begins in a message.`
  Data shown: static chapter table; visible chapter depends on scroll position.

  ELEMENT: @markitup.lb (shown on last chapter)
    Action: external link
    Destination: `https://www.instagram.com/markitup.lb/`
    Writes to: none
    Reads from: Visible when `chapter.showCta` is true (signal chapter)

---

## Notes

- `/courses?q=` and `?university=` hydrate catalog search. University filters use `universities.short_name` slugs.
- Course request forms use `#course-request`. Tutoring help on `/tutoring` keeps its own `#request` form.
- Pricing cards use code `PLANS`; homepage prices can follow `platform_settings.plan_overrides`.
- `platform_settings.show_pricing_publicly` is stored in admin but does not gate `/pricing`.
- Course request + tutoring request both insert `course_requests` (no tutoring table).
- Marketing **Get access** / plan CTAs enter [03-checkout.md](./03-checkout.md); **Sign in** enters [02-auth-onboarding.md](./02-auth-onboarding.md).

================================================================================
FILE: 02-auth-onboarding.md
HTML: /connects/02-auth-onboarding
RAW: /connects/raw/02-auth-onboarding
================================================================================
# Auth & onboarding

Registration, sign-in, password recovery, and name/university setup. Product rules: [../onboarding.md](../onboarding.md). How this hooks to checkout and dashboard: [00-how-everything-connects.md](./00-how-everything-connects.md).

---

PAGE: /login
Purpose: Sign in with Google or email/password.
Access: public (logged-in visitors are redirected by proxy to `/auth/home` with safe `next`)
Entry points: Header **Sign in**; protected-route bounce with `next`; signup **Sign in →**; forgot/reset return; checkout `buildLoginHref`; `/login?next=`; `/login?message=password_updated`; `/login?error=` from `/auth/callback`.

SECTION: Brand panel (desktop)
  Copy: `Your exam is coming.` `Your course is ready.` `Course-specific study packs.`
  Data shown: static

  ELEMENT: Gradeful (logo, aria-label `Gradeful homepage`)
    Action: navigate
    Destination: `/`
    Writes to: none
    Reads from: none

SECTION: Status banner
  Copy: `Password updated. Please sign in.`
  Data shown: query `message=password_updated` only

SECTION: Callback error
  Copy: allowlisted human messages from `lib/auth/callback-errors.ts` (`auth_failed`, `access_denied`, `otp_expired`, …). Raw IdP errors are never shown.
  Data shown: query `error`

SECTION: Form header
  Copy: `Welcome back` `Sign in with Google or the email you used to create your account.`
  Data shown: static

  ELEMENT: Continue with Google
    Action: submit
    Destination: Google OAuth then `/auth/callback?next=` (safe)
    Writes to: `auth.users` / session; new Google users also get `profiles` via trigger
    Reads from: none
    Errors: `Social sign-in is unavailable. Please try again.` `Unable to connect. Please check your connection.`

SECTION: Divider
  Copy: `or email`
  Data shown: static

SECTION: Email panel
  Copy: Label `Account email` placeholder `you@email.com`. Label `Password`.
  Data shown: form state

  ELEMENT: Forgot password?
    Action: navigate
    Destination: `/forgot-password`
    Writes to: none
    Reads from: none

  ELEMENT: Sign in
    Action: submit
    Destination: `/auth/home?next=` (safe)
    Writes to: Auth session only
    Reads from: none
    Errors: `Email or password is incorrect.` `Unable to connect. Please check your connection.`

SECTION: Footer
  Copy: `Don't have an account?`

  ELEMENT: Sign up →
    Action: navigate
    Destination: `/signup` or `/signup?next=…`
    Writes to: none
    Reads from: `next` query if present

Phone OTP is not offered. Hosted Gradeful had zero phone identities.

---

PAGE: /signup
Purpose: Create a student account (Google or email).
Access: public (logged-in redirected by proxy)
Entry points: Login **Sign up →**; marketing; course page **Start this course** (`buildSignupHref` to checkout); `/signup?next=`.

SECTION: Form header
  Copy: `Create your account` `Google or email. Then your name and university — enough to open the right course.`
  Data shown: static

  ELEMENT: Continue with Google
    Action: submit
    Destination: Google then `/auth/callback?next=…`
    Writes to: `auth.users`; `profiles` via `private.handle_new_user` (role stays `student`)
    Reads from: none
    Errors: `Social sign-up is unavailable. Please try again.` `Unable to connect. Please check your connection.`

SECTION: Email signup
  Copy: Label `Email` placeholder `you@email.com`. Label `Password`. Help `Use at least 8 characters. Avoid passwords that have shown up in a data breach.` Label `Confirm password`.
  Data shown: form state

  ELEMENT: Create account
    Action: submit
    Destination: in-place “Check your email”
    Writes to: `auth.users`; `profiles` via trigger
    Reads from: none
    Errors: `Enter a valid email address.` `Passwords do not match.` `We could not create your account. Try signing in instead.` `Unable to connect. Please check your connection.`

SECTION: Footer
  ELEMENT: Sign in →
    Destination: `/login` or `/login?next=…`

SECTION: Check your email
  Copy: `Check your email` `We sent a verification link to {submittedEmail}` `Didn't receive it?`
  ELEMENT: Resend → Success: `Verification email sent.`
  ELEMENT: Back to sign in → `/login` (+ optional `next`)

---

PAGE: /forgot-password
Purpose: Request a password-reset email.
Access: public
Entry points: Login **Forgot password?**

SECTION: Card
  Copy: `Reset your password` `Enter your email and we'll send you a reset link.` Label `Email` placeholder `you@email.com`.
  ELEMENT: ← Back to login → `/login`
  ELEMENT: Send reset link / Sending...
    Destination: none (`redirectTo` `/reset-password`)
    Success: `Check your email for a reset link.`
    Errors: `We could not send the reset link. Please try again.` `Unable to connect. Please check your connection.`

---

PAGE: /reset-password
Purpose: Set a new password from the email recovery link.
Access: public (needs recovery session or `?code=`)
Entry points: Reset email link.

SECTION: Invalid
  Copy: `This reset link is invalid or has expired.`
  ELEMENT: Request a new reset link → `/forgot-password`

SECTION: Ready form
  Copy: Label `New password`. Help `Use at least 8 characters.` Label `Confirm password`.
  ELEMENT: Update password / Updating...
    Destination: `/login?message=password_updated` after `signOut`
    Writes to: `auth.users` password hash
    Errors: `Passwords do not match.` `We could not update your password. Try again.` `Unable to connect. Please check your connection.`

---

PAGE: /auth/callback
Purpose: Exchange OAuth/email `code` for a session; optional welcome email; send the student through `/auth/home` with safe `next`.
Access: public (session is created here)
Entry points: Google OAuth; email verification link; Auth redirects with `?code=&next=`.

SECTION: No UI
  Copy: none. On failure: `/login?error={allowlisted}` plus preserved safe `next`.
  ELEMENT: none (route handler)
    Action: navigate
    Destination: `/auth/home?next=` on success; login error URL on failure
    Writes to: Auth session cookies. Welcome email via Resend if `profiles.created_at` within 5 seconds.
    Reads from: `profiles.created_at`, `profiles.full_name`

---

PAGE: /auth/home
Purpose: After login, send incomplete name/university profiles to `/welcome?next=` and everyone else to the preserved destination (course, checkout, or `/dashboard`).
Access: logged-in (anonymous → `/login`)
Entry points: Default after login/signup/OAuth; proxy redirect from `/login`/`/signup` when already logged in.

SECTION: No UI
  ELEMENT: none (route handler)
    Action: navigate
    Destination: `resolveLoggedInDestination`
    Reads from: `profiles.full_name`, `university`, `onboarding_completed_at`

---

PAGE: /welcome
Purpose: Required onboarding only: who are you (name) and which university offering (AUB/LAU). Preserve `next`.
Access: logged-in; incomplete onboarding. Anonymous → `/login?next=/welcome`. Already complete → preserved `next`, `/dashboard` if active access, otherwise `/`.
Entry points: `/auth/home`; workspace layout; checkout; direct URL.

SECTION: Brand panel (desktop)
  Copy: `Your university.` `Your courses.` `Twenty seconds. Then you're in — not a marketing page.`

SECTION: Intro
  Copy: `Almost in` `Tell us where you study.` `Name, university, major. We use this to show the right packs — nothing to verify here.`

SECTION: Who are you?
  Copy: Label `Your name` placeholder `Maya Haddad`. `How Gradeful greets you — not a public username.`

SECTION: What course do you need?
  Copy: `Which university?` `AUB and LAU use different course codes. This picks the matching offering.`
  ELEMENT: AUB / LAU cards
  If `next` is checkout/course: `We'll send you back to checkout for the course you already picked.` / `We'll send you back to that course.`
  If homepage: `After this you can pick a course. Major and campus can wait.` plus continue to `/courses`.

SECTION: Submit
  Copy: `Continue to checkout` / `Continue to your course` / `Find a course`
  Writes to: `profiles.full_name`, `university`, `onboarding_completed_at` (optional campus/major if present)
  Toasts: `Add the name you use at uni.` `Pick AUB or LAU so we show the right packs.` `Could not save that. Try again.`

Major, minors, and LAU campus are not on this form.

================================================================================
FILE: 03-checkout.md
HTML: /connects/03-checkout
RAW: /connects/raw/03-checkout
================================================================================
# Checkout

Live path is Whish. Homepage/pricing CTAs go to `/checkout?plan=`. How this grants `user_access` and lands on dashboard: [00-how-everything-connects.md](./00-how-everything-connects.md).

Plans (source `lib/payments/catalog.ts`; live overlays via `getPaymentPlans()` apply to homepage, pricing, and checkout charges):

| URL slug | Name | Price | Courses | Days |
|---|---|---|---|---|
| `focus` | Focus | $10 | 1 | 120 |
| `balance` | Balance | $17 | 2 | 120 |
| `fullload` | Full Load | $25 | up to 6 | 120 |
| `crunch` | Crunch | $14 | up to 4 | 21 |

---

PAGE: /checkout
Purpose: Pick courses, collect details, start Whish payment.
Access: logged-in. Missing/invalid `?plan=` → `/pricing`. No session → `/login?next=/checkout?plan=…`.
Entry points: Homepage **Get Focus** / **Get Balance** / **Cover my semester** / **Prep for Finals**; pricing same labels; course detail **Get access →** with `&course={courses.id}`; payment failure **Return to checkout**.

SECTION: Header
  Copy: Logo alt `Gradeful`
  Data shown: static

  ELEMENT: Gradeful (logo)
    Action: navigate
    Destination: `/`
    Writes to: none
    Reads from: none

  ELEMENT: Contact us
    Action: navigate
    Destination: `/contact`
    Writes to: none
    Reads from: none

  ELEMENT: Back to plans
    Action: navigate
    Destination: `/pricing`
    Writes to: none
    Reads from: none

SECTION: Step indicator
  Copy: `Your course` `Your details` `Payment`
  Data shown: current step (1–2 in UI; payment is Whish, not a third in-app step)

SECTION: Step 1 — course picker
  Copy by plan: `coursePickerCopy` in `lib/payments/catalog.ts`
  Focus: `Which course do you need?` `You're getting 1 course — choose the one you need most.`
  Balance: `Which courses do you need?` `You're getting 2 courses — pick them now.`
  Full Load: `Which courses are you taking?` `Pick up to 6 courses. The plan covers what you select — not the whole catalog.`
  Crunch: `Which courses are you sitting?` `Pick up to 4 exam courses. You don't need to upload a schedule.`
  Search placeholder: `Search by code — e.g. MATH201`
  Status: `Loading courses…` `No courses found.` `Courses couldn't be loaded. Refresh and try again.`
  Data shown: `courses` (`is_active=true`) with `course_packs` and `universities`; optional `initialCourseId` from `?course=`

  ELEMENT: Search by code (input)
    Action: toggle
    Destination: none (filters list)
    Writes to: none
    Reads from: `courses.code`, `courses.title`

  ELEMENT: Selected course chip (code + remove)
    Action: toggle
    Destination: none (deselect)
    Writes to: none
    Reads from: `courses.code`

  ELEMENT: Course row (university, code, optional `{n} cr`, title)
    Action: toggle
    Destination: none (select/deselect)
    Writes to: none
    Reads from: `courses.*`, `universities.short_name`, credit hours on the course
    Client limits: `You can only pick 1 course on Focus` `You can only pick 2 courses on Balance` `You can pick up to {n} courses.`

  ELEMENT: Try again
    Action: toggle
    Destination: none (reload courses)
    Writes to: none
    Reads from: visible when load failed

  ELEMENT: Continue
    Action: toggle
    Destination: step 2
    Writes to: none (client state + sessionStorage `gradeful.checkout.plan`)
    Reads from: selection validity

SECTION: Step 2 — details
  Copy: Order summary shows plan name, included line, price, duration, selected courses. Empty `No courses selected`. No registered-credits or screenshot fields.
  Included lines: Focus `1 course · all content types` `/1 course` `Full semester · 120 days`; Balance `2 courses · all content types` `/2 courses`; Full Load / Crunch `The courses you pick · all content types` `/your courses`; Crunch duration `21-day sprint`.
  Fields: `Full name` placeholder `Your name` (prefilled from profile, editable, does not write profiles); `Signed-in email` readonly `This is the email on your Gradeful account. It can't be changed here.` empty `No email on this account`; `University` hidden/read-only when profile already has AUB/LAU, else `Select university` / `AUB` / `LAU` / `Other`; `WhatsApp (optional)` `+961 XX XXX XXX` `Optional, for account support. Payment uses the phone you enter on Whish.`; institution email hidden when login already matches campus, else shown for receipt; checkbox personal tutoring-access sentence plus `CHECKOUT_INDEPENDENCE_ACK`.
  Footnote: `You'll continue to a secure payment page to complete the purchase.` `Need help?`
  Data shown: Auth email; selected courses; `PLANS` price

  ELEMENT: Change courses
    Action: toggle
    Destination: step 1
    Writes to: none
    Reads from: none

  ELEMENT: Full name
    Action: toggle
    Destination: none
    Writes to: `orders.name`
    Reads from: none
    Error: `Full name is required.`

  ELEMENT: Signed-in email (readonly)
    Action: none
    Destination: none
    Writes to: none
    Reads from: Auth email
    Error: `Sign in to continue checkout.`

  ELEMENT: University
    Action: toggle
    Destination: none
    Writes to: `orders.university`
    Reads from: none
    Error: `Select your university.`

  ELEMENT: WhatsApp number
    Action: toggle
    Destination: none
    Writes to: `orders.whatsapp`
    Reads from: none
    Error: `WhatsApp number is required.`

  ELEMENT: Your uni email
    Action: toggle
    Destination: none
    Writes to: `orders.email` (this is the uni email, not Auth email)
    Reads from: none
    Errors: `Your uni email is required.` `Use a university email ending in .edu.`

  ELEMENT: I understand this material is for personal study only...
    Action: toggle
    Destination: none
    Writes to: none (must be true to submit)
    Reads from: none
    Error: `You must agree to continue.`

  ELEMENT: Continue to secure payment / Preparing secure payment…
    Action: submit
    Destination: Whish `collectUrl` (`window.location.assign`)
    Writes to: `orders` via `POST /api/payments/whish/create` (`user_id`, `name`, `email`, `university`, `plan`, `course_ids`, `registered_credits` null, `whatsapp`, `status=pending`, `payment_provider=whish`, `payment_status=pending`, `external_id`, `amount` from live plan, `currency=USD`, `collect_url`, `provider_metadata.durationDays`, `provider_metadata.catalogPrice`)
    Reads from: form + Auth session
    Errors: `An order was already submitted recently. Please wait and try again.` `Checkout is temporarily unavailable. Please try again shortly.` `Payment could not be started. Please try again.` `Unable to connect. Please check your connection.` plus server `error` ≤160 chars. 401 → login with checkout `next`.

  ELEMENT: Contact us
    Action: navigate
    Destination: `/contact`
    Writes to: none
    Reads from: none

API errors from create (also shown when returned): `Sign in to continue checkout.` `Invalid request.` `Checkout details are incomplete.` `Use your AUB or LAU email for this payment. It is not your login.` `Selected courses are not available.` `One or more selected courses are not available for purchase.` `You already have access to those courses. Open them from My courses instead of paying again.` `Order could not be created. Try again.` `Something went wrong. Please try again.` Entitlement: `Remove duplicate courses and try again.` `Select at least one course for this plan.` `Select 1 course for this plan.` `Select {n} courses for this plan.` `You can pick up to {n} courses.`

---

PAGE: /checkout/payment/success
Purpose: Return from Whish; poll until paid/failed/timeout. Arriving here does not mean paid by itself.
Access: logged-in (else login with `next` = this URL). Query `orderId` (UUID).
Entry points: Whish redirect after collect.

SECTION: PaymentReturnStatus (variant success)
  Copy by state:
  invalid-order: `We couldn't find that payment` `Open checkout again from your plan, or return to pricing to start over.`
  paid: `Payment confirmed` `Access is ready. You can open your courses now.` `Reference {8-char}`
  failed: `Payment wasn't completed` `No access was granted. You can try payment again, or return to checkout to start over.`
  pending: `We're confirming your payment` `Whish is still processing your payment. Keep this page open. If you haven't finished paying, you can return to Whish. This is not a failed payment.`
  unavailable (timeout): `We couldn't confirm the payment yet` `We haven't confirmed this payment yet. That doesn't mean it failed. If you already paid, wait a minute and refresh, or contact support. If you haven't finished on Whish, you can continue there.`
  unavailable (other): `We could not verify your payment right now. That doesn't mean it failed. Continue on Whish if you still need to pay, or contact support if you were charged.`
  checking: `Checking payment status` `Hang tight — we're verifying with the payment provider.`
  Hint: `Arriving here doesn't confirm payment by itself — we verify status securely.`
  Data shown: `GET /api/payments/whish/status?orderId=` → `orders`; reconcile may write `user_access`

  ELEMENT: Return to pricing
    Action: navigate
    Destination: `/pricing`
    Writes to: none
    Reads from: none

  ELEMENT: Go to my courses
    Action: navigate
    Destination: `/dashboard`
    Writes to: none
    Reads from: shown when `accessGranted` / paid

  ELEMENT: Continue payment on Whish
    Action: external link
    Destination: Whish `collectUrl` / `resumePaymentUrl`
    Writes to: none
    Reads from: `orders.collect_url`

  ELEMENT: Return to checkout
    Action: navigate
    Destination: `/checkout` with remembered plan
    Writes to: none
    Reads from: sessionStorage plan

  ELEMENT: Contact Whish support
    Action: external link
    Destination: `https://www.whish.money/contact-us`
    Writes to: none
    Reads from: none

Poll may call `reconcileWhishOrder` → `grantAccess` → `user_access`. New grants do not insert `schedule_verifications`.

---

PAGE: /checkout/payment/failure
Purpose: Same status poll as success; copy hint differs.
Access: logged-in
Entry points: Whish failure redirect.

SECTION: PaymentReturnStatus (variant failure)
  Copy: Same state machine as success. Hint: `Arriving here doesn't mean the payment permanently failed — we verify status securely.`
  Data shown: same as success
  ELEMENT: same buttons as `/checkout/payment/success`

================================================================================
FILE: 04-dashboard-and-workspace.md
HTML: /connects/04-dashboard-and-workspace
RAW: /connects/raw/04-dashboard-and-workspace
================================================================================
# Dashboard & workspace

Student home after login. Course study tabs are in [06-study-views.md](./06-study-views.md). Settings overlay is [05-settings.md](./05-settings.md). Flows: [00-how-everything-connects.md](./00-how-everything-connects.md).

**Access:** Workspace layout requires logged-in + onboarded (else `/welcome` or `/login?next=/dashboard`).  
`/dashboard` is the only student home. Course workspaces (`/my-courses/{slug}`) stay entitlement-gated (proxy → `/pricing` if unpaid). Admins count as subscribed for that gate. Marketing `/` is separate: only users with **current active access** are redirected there from `/`.

**Canonical home:** `/dashboard`. `/my-courses` and `/dashboard/my-packs` redirect here (query `q` and `error=no_access` preserved). Course study URLs stay at `/my-courses/{slug}`.

**Redirects:** `/my-courses/library|progress|semester-plan` → `/dashboard/…`. `/dashboard/course/[slug]/…` → `/my-courses/[slug]`.

---

## Shared chrome (every workspace page)

SECTION: Desktop sidebar
  Copy: Expanded wordmark `Gradeful`; collapsed `G`. Nav labels below. Streak card `This week` plus a dynamic rhythm title. Account chip shows `profiles.full_name` or `Your account`, campus line or email.
  Data shown: `profiles.*`; streak from `user_streaks` / `study_sessions`; feature flag `aiTutorEnabled`; `profiles.role` for Admin

  ELEMENT: Gradeful / G (brand)
    Action: navigate
    Destination: `/dashboard`
    Writes to: none
    Reads from: none

  ELEMENT: Collapse / Expand sidebar
    Action: toggle
    Destination: none
    Writes to: cookie via `setWorkspaceSidebarCollapsed` (except on course routes: local only)
    Reads from: cookie

  ELEMENT: Dashboard
    Action: navigate
    Destination: `/dashboard`
    Writes to: none
    Reads from: active on `/dashboard` (aliases `/my-courses`, `/dashboard/my-packs` redirect here)

  ELEMENT: Library
    Action: navigate
    Destination: `/dashboard/library`
    Writes to: none
    Reads from: none

  ELEMENT: Progress
    Action: navigate
    Destination: `/dashboard/progress`
    Writes to: none
    Reads from: none

  ELEMENT: Semester Plan
    Action: navigate
    Destination: `/dashboard/semester-plan`
    Writes to: none
    Reads from: none

  ELEMENT: AI Tutor
    Action: navigate
    Destination: On a course: same course URL + `?panel=ai`; else `/dashboard/ai-tutor`
    Writes to: none
    Reads from: Visible when `aiTutorEnabled`

  ELEMENT: Admin dashboard
    Action: navigate
    Destination: `/admin`
    Writes to: none
    Reads from: `profiles.role` is admin

  ELEMENT: Streak card / flame (collapsed)
    Action: open modal
    Destination: calendar dialog (month grid; legend `Streak day` `Missed` `Today`)
    Writes to: none
    Reads from: `user_streaks`, `study_sessions`, `profiles`, platform `weeklyStudyGoalMinutes`

  ELEMENT: Account chip (opens flyout)
    Action: open modal
    Destination: account menu
    Writes to: none
    Reads from: `profiles.avatar_url`, `full_name`, campus fields, Auth email

  ELEMENT: Settings (flyout)
    Action: open modal
    Destination: Settings tab `profile`
    Writes to: none
    Reads from: none

  ELEMENT: Get help (flyout)
    Action: open modal
    Destination: Settings tab `help`
    Writes to: none
    Reads from: none

  ELEMENT: Upgrade plan (flyout)
    Action: navigate
    Destination: `/pricing`
    Writes to: none
    Reads from: none

  ELEMENT: Dashboard (flyout)
    Action: navigate
    Destination: `/dashboard`
    Writes to: none
    Reads from: none

  ELEMENT: Learn more (flyout)
    Action: toggle
    Destination: submenu
    Writes to: none
    Reads from: none

  ELEMENT: About Gradeful / Privacy policy / Terms of service / Refund policy / Academic integrity / All policies
    Action: navigate
    Destination: `/about` `/privacy` `/terms` `/refunds` `/academic-integrity` `/policies`
    Writes to: none
    Reads from: none

  ELEMENT: Log out / Logging out…
    Action: submit
    Destination: `/`
    Writes to: clears session
    Reads from: none

SECTION: Mobile header
  Copy: Logo `Gradeful`
  Data shown: same as sidebar account

  ELEMENT: Open workspace menu (hamburger)
    Action: toggle
    Destination: drawer with full sidebar (`showCollapseToggle=false`)
    Writes to: none
    Reads from: none

  ELEMENT: Close menu / backdrop / Escape
    Action: toggle
    Destination: closes drawer
    Writes to: none
    Reads from: none

  ELEMENT: Gradeful
    Action: navigate
    Destination: `/dashboard`
    Writes to: none
    Reads from: none

  ELEMENT: Open settings (gear)
    Action: open modal
    Destination: Settings `profile`
    Writes to: none
    Reads from: none

  ELEMENT: Open account menu
    Action: open modal
    Destination: same flyout as sidebar
    Writes to: none
    Reads from: `profiles.*`

SECTION: Course right panel (course player only)
  Copy: Collapsed `Open stats` / `Open AI tutor`. Expanded tabs `Stats` `AI Tutor`. Stats labels `Notes` `Quizzes` `Exam prep` plus notes %, flashcards due, quiz scores, exam days. AI loading `Loading AI tutor...` Modes `Explain` `Guide me` `Practice me` `Review my mistake` `Quiz me`. Suggestions `Explain this concept` `Help me practice` `What should I review for my upcoming exam?`
  Data shown: `content_progress`, flashcard due counts, quiz scores, `course_events`

  ELEMENT: Open stats / Open AI tutor / Collapse panel
    Action: toggle
    Destination: none
    Writes to: cookie `setWorkspaceAiPanelExpanded`
    Reads from: cookie; `aiTutorEnabled`

  ELEMENT: Stats / AI Tutor tabs
    Action: toggle
    Destination: none
    Writes to: none
    Reads from: none

  ELEMENT: Module list row
    Action: navigate
    Destination: `/my-courses/{slug}?module=&tab=notes`
    Writes to: none
    Reads from: `course_modules`

  ELEMENT: View all chapters →
    Action: navigate
    Destination: course overview
    Writes to: none
    Reads from: none

  ELEMENT: AI suggestion chips / chat send
    Action: submit
    Destination: none
    Writes to: POST `/api/ai-tutor` with course/module IDs (usage metrics only; no transcript table)
    Reads from: entitled Gradeful pack items, student progress, student-entered exam dates

  ELEMENT: Ask AI Tutor (floating)
    Action: toggle
    Destination: opens AI panel
    Writes to: none
    Reads from: `aiTutorEnabled`

---

PAGE: /dashboard
Purpose: Restored pre-redesign student home (Git `89d0991`): greeting, resume card, exam-mode banner, course cards, what needs attention, Today calendar, study hours, recent activity. Not the later next-study-action redesign.
Access: logged-in + onboarded (not subscriber-gated). Unpaid accounts see the empty/onboarding pack state, not a marketing homepage inside the workspace.
Entry points: `/` when the user has active access; `/auth/home`; welcome submit; login `next`; sidebar Dashboard; public header Dashboard; checkout **Go to my courses** → `/dashboard`

SECTION: Top bar
  Copy: Greeting `Good morning, {firstName}`. Search `Search your courses...`
  Data shown: `profiles.full_name` first name

  ELEMENT: Search
    Action: submit
    Destination: paid `/dashboard?q=…`; free `/courses?q=…`
    Writes to: none
    Reads from: query `q` filters owned courses on the paid home

  ELEMENT: Notification bell
    Action: toggle
    Destination: notification panel
    Writes to: `notifications.is_read` (`markAllNotificationsRead`; auto after 2s open)
    Reads from: `notifications` (limit 20) + realtime INSERT
    Empty: `No notifications yet`
    Badge: unread count
    Row: navigates `action_url` when set
    Note: evening streak-risk notifications are not generated

  ELEMENT: Mark all read
    Action: submit
    Destination: none
    Writes to: `notifications.is_read`
    Reads from: unread rows

  ELEMENT: Open settings (md+)
    Action: open modal
    Destination: Settings `profile`
    Writes to: none
    Reads from: none

  ELEMENT: AccountChrome (mobile top bar)
    Action: open modal
    Destination: account flyout / settings
    Writes to: none
    Reads from: `profiles.*`

SECTION: Access error
  Copy: `We couldn't open that course. Make sure it's in your plan, or try opening it again from the list below.`
  Data shown: query `error=no_access`

SECTION: Resume where you left off
  Copy: `Resume where you left off` plus last content type / chapter.
  Data shown: latest `content_progress.last_accessed_at` among owned modules
  ELEMENT: Resume CTA
    Action: navigate
    Destination: `/my-courses/{slug}?module=&tab=`
    Writes to: none
    Reads from: `content_progress`

SECTION: Exam mode banner
  Copy: `Exam mode` plus nearest exam headline. CTA into that course.
  Data shown: `exam_schedules` / `course_events` on owned courses
  ELEMENT: Open course
    Action: navigate
    Destination: `/my-courses/{slug}`
    Writes to: none
    Reads from: exam-mode builder

SECTION: Your courses
  Copy: `Your courses` or `Search your courses`. Cards: progress bar, `Content coming soon`, flashcards due, exam badge. Empty: `Your study space is waiting.` / `No courses yet` with `Browse the catalog and get access to your first pack.`
  Data shown: `user_access.course_ids` via `getCachedUserOwnedCourses`; `content_progress`; flashcard due counts
  ELEMENT: Course card
    Action: navigate
    Destination: `/my-courses/{slug}`
    Writes to: none
    Reads from: owned courses
  ELEMENT: View all courses / Show fewer / Clear search
    Action: toggle / navigate
    Destination: `/dashboard` for clear search
    Writes to: none
    Reads from: owned course count

SECTION: What needs attention
  Copy: `What needs attention` / `You're all caught up for now.` Rows for due flashcards, quizzes, exams, unpublished content.
  Data shown: `lib/workspace/attention-data.ts` plus flashcard due/leech counts
  ELEMENT: Attention row
    Action: navigate
    Destination: study tab href when set
    Writes to: none
    Reads from: attention items
  ELEMENT: View all
    Action: navigate
    Destination: `/dashboard/progress`
    Writes to: none
    Reads from: attention total count

SECTION: Today (right column)
  Copy: `Today` plus month label, week strip, `You're all caught up` / `{n} items need attention`. Link `Open semester plan`.
  Data shown: calendar from now; `stats.tasksDueToday`
  ELEMENT: Open semester plan
    Action: navigate
    Destination: `/dashboard/semester-plan`
    Writes to: none
    Reads from: none

SECTION: Study hours
  Copy: weekly hours vs goal, 7-day bars.
  Data shown: `study_sessions.active_minutes`; platform `weeklyStudyGoalMinutes`

SECTION: Recent activity
  Copy: `Recent activity` / `Activity appears after you open lessons, quizzes, or flashcards.` (tool names follow restored Notes / Quiz / Flashcards labels.)
  Data shown: recent `content_progress` rows

Public weekly rank / “students studying now” are **not** shown (leaderboards stay retired). Schedule-confirm and syllabus-upload gates stay retired.

---

PAGE: /my-courses
Purpose: Alias of `/dashboard` (redirect, query preserved).
Access: logged-in + onboarded. Home path is not subscriber-gated.
Entry points: old bookmarks; emails
ELEMENT: none — redirect only. `/my-courses/{slug}` remains the course workspace.

---

PAGE: /dashboard/my-packs
Purpose: Alias of `/dashboard` (redirect, query preserved).
Access: logged-in + onboarded
Entry points: old checkout CTA
ELEMENT: none — redirect only

---

PAGE: /dashboard/library
Purpose: Saved notes, favorited flashcards/quizzes, personal uploads.
Access: logged-in + onboarded
Entry points: Sidebar Library. Alias `/my-courses/library` redirects here.

SECTION: Header
  Copy: `My Library` `Saved notes, flashcards, and your own materials`
  Data shown: static

  ELEMENT: All / Notes / Flashcards / Uploads
    Action: toggle
    Destination: none (filter)
    Writes to: none
    Reads from: `library_items.item_type`

SECTION: Cards
  Copy: Empty `Nothing saved here yet`. Uploads empty `No uploads yet`. Upload zone `Upload your own materials` PDF·images·Word ≤10MB.
  Data shown: `library_items` + course/module joins

  ELEMENT: View →
    Action: navigate
    Destination: notes study href
    Writes to: none
    Reads from: saved note target

  ELEMENT: Review →
    Action: navigate
    Destination: flashcards (+ optional `&card=`)
    Writes to: none
    Reads from: flashcard library item

  ELEMENT: Retake →
    Action: navigate
    Destination: quiz
    Writes to: none
    Reads from: quiz library item

  ELEMENT: Delete
    Action: submit
    Destination: none
    Writes to: deletes `library_items` (`deleteLibraryItem`)
    Reads from: item id

  ELEMENT: Upload file
    Action: submit
    Destination: none
    Writes to: `library_items` via `uploadLibraryFile`
    Reads from: file

---

PAGE: /dashboard/progress
Purpose: Weekly streak objective, course breakdown, 12-week heatmap, quiz notes.
Access: logged-in + onboarded
Entry points: Sidebar Progress. Alias `/my-courses/progress` redirects here.

SECTION: This week / Progress
  Copy: `This week` `Progress` plus objective copy (weekly goal + miss budget). Summary: all-time screen time, current streak, course completion. Course bars: Notes / Flashcards / Quizzes. `Last 12 weeks`. Quiz notes table headers Module, Attempts, Best, Latest, Direction. How streaks work (explanatory copy in the view).
  Data shown: `profiles`, owned courses, `course_modules`, `study_sessions`, `course_xp`, `quiz_attempts`, `course_events`, streak dates

  ELEMENT: Keep studying
    Action: navigate
    Destination: first course overview or `/courses`
    Writes to: none
    Reads from: owned courses

  ELEMENT: Open course →
    Action: navigate
    Destination: `/my-courses/{slug}`
    Writes to: none
    Reads from: `courses.slug`

---

PAGE: /dashboard/semester-plan
Purpose: Upcoming assessments, recommended prep, course progress. Not a giant calendar. Syllabus is optional and private.
Access: logged-in + onboarded
Entry points: Sidebar Semester Plan. Alias `/my-courses/semester-plan` redirects here.

SECTION: Header
  Copy: `Semester Plan` `Upcoming assessments, a short prep list, and course progress.` CTA `Add exam` session length `20 min` `45 min` `90 min` `Exam cram`
  Data shown: cookie `gradeful_study_session`; owned courses

  ELEMENT: Add exam
    Action: open dialog
    Destination: AddExamDateDialog
    Writes to: `exam_schedules` (`addStudentExamDate`)
    Reads from: owned courses

  ELEMENT: Session length
    Action: submit
    Destination: none
    Writes to: cookie via `setStudySessionLength`
    Reads from: cookie

SECTION: Upcoming assessments
  Copy: empty `No assessments yet` `Add an exam in a few seconds.` list shows course, title, when, optional topics, source when dates disagree
  Data shown: merged `exam_schedules` + `course_events` (`lib/workspace/assessments.ts`)

  ELEMENT: Remove
    Action: submit
    Destination: none
    Writes to: deletes student `exam_schedules` row
    Reads from: student-owned assessment id

SECTION: Recommended prep
  Copy: session titles from `generateStudyPlan`; empty `Add an exam or open a course`
  Data shown: assessments, progress, flashcard due counts
  ELEMENT: Open →
    Action: navigate
    Destination: course overview or study href
    Writes to: none
    Reads from: session.href

SECTION: Course progress
  Copy: `{code}` `{n}% complete`
  Data shown: `content_progress`
  ELEMENT: Open →
    Action: navigate
    Destination: `/my-courses/{slug}`

SECTION: Syllabus (optional)
  Copy: `Private to you.` `Upload` / `Delete`
  Data shown: `syllabuses`
  ELEMENT: Upload
    Action: open modal
    Destination: SyllabusUploadModal (skippable)
    Writes to: `syllabuses`
  ELEMENT: Delete
    Action: submit
    Destination: none
    Writes to: deletes storage object + `syllabuses` row

---

PAGE: /dashboard/ai-tutor
Purpose: Standalone AI page when opened off-course.
Access: logged-in; if `aiTutorEnabled` false → `/dashboard`
Entry points: Sidebar AI Tutor when not in a course.

SECTION: Help
  Copy: `AI Tutor` plus instruction to open a course and use the right AI panel
  Data shown: static

  ELEMENT: Back to dashboard
    Action: navigate
    Destination: `/dashboard`
    Writes to: none
    Reads from: none

---

PAGE: /my-courses/[courseSlug]
Purpose: Course overview + in-place study player (`?module=&tab=&variant=&chapter=&panel=`).
Access: logged-in + onboarded; must own the course else `/dashboard?error=no_access`; anonymous → login with `next`
Entry points: dashboard cards, resume, attention, progress, library, right panel, public catalog formula-sheet preview (may bounce to login)

SECTION: Course shell header
  Copy: Breadcrumb `Dashboard` / `{courseCode}` / compact chapter picker / current study tool / progress. Tabs: `Overview` `Notes` `Formula sheet` `Flashcards` `Quiz` `Exam` `Exam review` `Mistake fixes` `Exercises` `Ask Tutor` (Ask Tutor hidden unless `aiTutorEnabled`). Locked title `Not published yet`. Picker trigger `Ch. {n} · {title}` or `Select chapter`. Layout: restored pre-redesign tab strip + chapter picker (Git `c7fc162`). No Learn / Practice / Review strip and no course-outline column. Right AI panel is a full-screen drawer on small screens.
  Data shown: `courses.code`, `course_modules`, available `module_pack_items` mapped through `lib/workspace/learning-intent.ts`

  ELEMENT: Dashboard
    Action: navigate
    Destination: `/dashboard`
    Writes to: none
    Reads from: none

  ELEMENT: {courseCode}
    Action: navigate
    Destination: `/my-courses/{slug}` overview
    Writes to: none
    Reads from: `courses.slug`

  ELEMENT: Course settings (gear)
    Action: navigate
    Destination: `/my-courses/{slug}/settings`
    Writes to: none
    Reads from: none

  ELEMENT: Overview / Notes / Formula sheet / Flashcards / Quiz / Exam / Exam review / Mistake fixes / Exercises
    Action: navigate
    Destination: overview, or `/my-courses/{slug}?module={id}&tab={item_type}`. Ask Tutor still uses `?panel=ai`. Legacy `?intent=learn` URLs may still resolve to the first matching tab, but Learn / Practice / Review is not the student IA.
    Writes to: none
    Reads from: published `module_pack_items` for the chapter

  ELEMENT: Chapter option `Ch. {n} · {title}`
    Action: navigate
    Destination: same intent, new module
    Writes to: none
    Reads from: `course_modules`

Study body for each tab: [06-study-views.md](./06-study-views.md).

---

PAGE: /my-courses/[courseSlug]/settings
Purpose: Per-course flashcard scheduler, syllabus, exam dates.
Access: logged-in; must own course else public `/courses/{slug}`
Entry points: Course shell settings gear. `/dashboard/course/{slug}/settings` redirects here.

SECTION: Header
  Copy: `{code} settings`
  Data shown: `courses.code`, `courses.title`

  ELEMENT: ← Back to course
    Action: navigate
    Destination: course overview
    Writes to: none
    Reads from: none

SECTION: Flashcard study
  Copy: `Flashcard study` `How well should cards stick?` `New cards per day` `More options` `Maximum reviews per day` placeholder `No limit` `Study order` options mixed / `Reviews first` / `New cards first` related-cards checkbox `Timezone` `Developer settings`
  Data shown: `flashcard_deck_settings`. Raw scheduler parameter vectors are not shown.

  ELEMENT: Save flashcard settings / Saving…
    Action: submit
    Destination: none
    Writes to: `flashcard_deck_settings` (`updateFlashcardDeckSettings`)
    Reads from: form
    Toast: `Flashcard settings saved.`

SECTION: Syllabus
  Copy: `Syllabus` `Optional and private.` empty `No syllabus uploaded.`
  Data shown: `syllabuses`

  ELEMENT: Upload / Replace
    Action: open modal
    Destination: SyllabusUploadModal
    Writes to: `syllabuses`
    Reads from: existing row

  ELEMENT: Delete syllabus
    Action: submit
    Destination: none
    Writes to: deletes storage object + `syllabuses` row

SECTION: Exams
  Copy: `Exams` empty `No exams yet.` `Add exam`
  Data shown: merged `exam_schedules` + `course_events`
  ELEMENT: Add exam
    Action: open dialog
    Destination: AddExamDateDialog
    Writes to: `exam_schedules`

================================================================================
FILE: 05-settings.md
HTML: /connects/05-settings
RAW: /connects/raw/05-settings
================================================================================
# Settings

Account settings modal over the workspace. Opened from the gear, account flyout **Settings** / **Get help**, or `/dashboard?settings={tab}` (also recognized on `/my-courses?settings=`). `buildSettingsHref` always points at `/dashboard?settings=…`. After open, the query is stripped. Legacy `/settings`, `/settings/profile`, `/settings/security`, `/settings/notifications`, `/settings/billing`, `/settings/help`, `/settings/danger` redirect here. `/settings/appearance` and `/settings/language` redirect to profile.

PAGE: /dashboard?settings={tab}
Purpose: Every student account setting (identity, notifications, billing, security, delete, help).
Access: logged-in + onboarded workspace. `GET /api/workspace/settings` returns 401 if anonymous.
Entry points: Sidebar/header gear; flyout **Settings**; flyout **Get help** (help tab); security **Manage account deletion →**; marketing header gear → `/dashboard?settings=profile`.

SECTION: Modal chrome
  Copy: Dialog title (sr-only) `Settings`. Nav aria-label `Settings sections`. Load skeleton aria-label `Loading settings`. Error `Could not load settings. Please try again.` Billing placeholder `Loading settings...`
  Data shown: `GET /api/workspace/settings` → `{ settings, billing }`

  ELEMENT: Profile
    Action: toggle
    Destination: profile pane (`settings=profile`)
    Writes to: none
    Reads from: none

  ELEMENT: Notifications
    Action: toggle
    Destination: notifications pane
    Writes to: none
    Reads from: none

  ELEMENT: Plan & billing
    Action: toggle
    Destination: billing pane
    Writes to: none
    Reads from: none

  ELEMENT: Security
    Action: toggle
    Destination: security pane
    Writes to: none
    Reads from: none

  ELEMENT: Danger zone
    Action: toggle
    Destination: danger pane
    Writes to: none
    Reads from: none

  ELEMENT: Help & contact
    Action: toggle
    Destination: help pane
    Writes to: none
    Reads from: none

---

SECTION: Profile
  Copy: `Profile` `Correct name, university, and optional programs here. Paid access stays on the courses you bought — not these labels.`
  Data shown: `profiles.*`, `user_badges.*`

### Where you study
  Copy: `Where you study` `University matches AUB or LAU course offerings. Changing it does not change courses you already bought.`
  Copy: `Which university?` `AUB and LAU use different course codes. This picks the matching offering.`
  Campus (optional, LAU only): `Campus (optional)` `Beirut or Byblos does not change which Gradeful courses you can open.`

  ELEMENT: AUB / LAU
    Action: toggle
    Destination: none
    Writes to: `profiles.university` on Save changes
    Reads from: `profiles.university`

  ELEMENT: Beirut / Byblos (optional)
    Action: toggle
    Destination: none
    Writes to: `profiles.campus` on Save changes
    Reads from: `profiles.campus`

### Programs (optional)
  Copy: `Programs (optional)` `Major and minors are for later recommendations. They are not required to open a course.`
  ELEMENT: Major / Second major / Minors (typeaheads, not required)
    Writes to: `profiles.major`, `second_major`, `minors` on Save changes

### You
  Copy: `You` `How Gradeful greets you in the workspace.` Labels `Profile photo` `Full name` `Nickname` `{n}/20` `Used in your workspace greeting. Letters, numbers, and underscores only.` `Account email` `This is your Gradeful login and receipts. It is not overwritten by an AUB or LAU address.` `Institution email` `Optional, and separate from login. Checkout may ask for your AUB or LAU address as a payment contact.`
  Data shown: `profiles.full_name`, `nickname`, `avatar_url`; Auth email

  ELEMENT: Change photo
    Action: open modal
    Destination: crop dialog `Crop your photo` `Drag to reposition and use the slider to zoom. We save a square crop.`
    Writes to: none until Save photo
    Reads from: `profiles.avatar_url`

  ELEMENT: Zoom (range 1–3)
    Action: toggle
    Destination: none
    Writes to: none
    Reads from: none

  ELEMENT: Cancel
    Action: toggle
    Destination: closes crop
    Writes to: none
    Reads from: none

  ELEMENT: Save photo / Uploading...
    Action: submit
    Destination: none
    Writes to: Storage `avatars/{userId}/avatar.jpg`; `profiles.avatar_url`; `profiles.updated_at`
    Reads from: cropped blob
    Success: `Profile photo updated`
    Errors: `Upload a JPG, PNG, or WebP image.` `Photo must be 5MB or smaller.` `You must be signed in to change your photo.` `Choose a photo to upload.` `Your photo could not be uploaded. Please try again.` `Your photo uploaded but could not be saved. Please try again.`

  ELEMENT: Full name
    Action: toggle
    Destination: none
    Writes to: `profiles.full_name` on Save changes (max 100)
    Reads from: `profiles.full_name`

  ELEMENT: Nickname
    Action: toggle
    Destination: none
    Writes to: `profiles.nickname` (2–20, `[a-zA-Z0-9_]+`)
    Reads from: `profiles.nickname`
    Errors: `Enter a nickname.` `Use at least 2 characters.` `Use at most 20 characters.` `Use letters, numbers, and underscores only.`

  ELEMENT: Email (disabled)
    Action: none
    Destination: none
    Writes to: none
    Reads from: `auth.users.email`

  ELEMENT: Contact support
    Action: external link
    Destination: `https://wa.me/96179057533`
    Writes to: none
    Reads from: none

  ELEMENT: Save changes / Saving...
    Action: submit
    Destination: toast + refresh
    Writes to: `profiles.full_name`, `profiles.nickname`, `profiles.updated_at`
    Reads from: form
    Success: `Profile updated`
    Error: `Something went wrong`

### Showing up
  Copy: `Showing up` `Quiet milestones for coming back. Not trophies for never missing a day.`
  Cards: `One week` `Showed up seven days in a row`; `Two weeks` `A fortnight of showing up`; `One month` `A month of making this a habit`; `Two months` `Sixty days of quiet consistency`; `One hundred days` `A home you kept returning to`. Earned subtitle formatted `MMM D, YYYY`.
  Data shown: `user_badges.badge_type`, `user_badges.earned_at`

  ELEMENT: none (display only)
    Action: none
    Destination: none
    Writes to: none
    Reads from: `user_badges`

---

SECTION: Notifications
  Copy: `Notifications` `Choose what you hear about from us. Study reminders stay quiet and optional.`
  Data shown: `profiles.notification_preferences` JSON. Defaults: `access_granted` true, `new_course` true, `announcements` false, `study_reminders` true. Key `reservation_updates` exists in JSON (default true) with **no toggle**.

  ELEMENT: Access granted emails
    Action: toggle
    Destination: none (saves immediately)
    Writes to: `profiles.notification_preferences.access_granted`, `profiles.updated_at`
    Reads from: same
    Description: `Get an email when your course access is activated.`

  ELEMENT: New course available
    Action: toggle
    Destination: none
    Writes to: `profiles.notification_preferences.new_course`
    Reads from: same
    Description: `Hear when a new course pack is added for your university.`

  ELEMENT: Platform announcements
    Action: toggle
    Destination: none
    Writes to: `profiles.notification_preferences.announcements`
    Reads from: same
    Description: `Occasional updates about Gradeful features and changes.`

  ELEMENT: Study reminders
    Action: toggle
    Destination: none
    Writes to: `profiles.notification_preferences.study_reminders`
    Reads from: same
    Description: `A gentle evening nudge if you haven't opened a pack yet. Never a guilt trip.`
    Toasts: `Preferences updated` `Something went wrong`

---

SECTION: Plan & billing
  Copy: `Plans & access` `Your active courses and payment history. Change plan anytime.` `Active access` `Payment history` columns `Date` `Plan` `Amount` `Status`
  Data shown: Active cards from `user_access` (`plan_type`, `course_ids`, `expires_at`, statuses paid/confirmed/pending, not expired) joined to `courses.code`. History from Whish `orders` for the signed-in user (`plan`, `amount`, `payment_status`, `created_at`). Plan names Focus / Balance / Full Load / Crunch else `Course access`. Status from `commercialOrderStatus()`. `Courses covered: {codes | Selected courses | No courses selected}` `Expires {date | Not set}` badge `Expiring soon` if ≤14 days. Empty access `No active access yet`. Empty history `No payment history yet`. Amount uses `orders.amount`.

  ELEMENT: Upgrade or change plan
    Action: navigate
    Destination: `/pricing`
    Writes to: none
    Reads from: none

  ELEMENT: Browse plans →
    Action: navigate
    Destination: `/pricing`
    Writes to: none
    Reads from: visible when no active access

---

SECTION: Security
  Copy: `Security` `Manage how you sign in to Gradeful` loading `Loading sign-in methods…`
  Data shown: Supabase Auth identities and session (`last_sign_in_at` / `updated_at` as last active)

### Change password (email identity only)
  Copy: `Change password` labels `Current password` `New password` `Confirm new password`

  ELEMENT: Update password / Updating...
    Action: submit
    Destination: none
    Writes to: `auth.users` password (`signInWithPassword` then `updateUser`)
    Reads from: email prop + form
    Toasts: `New password must be at least 8 characters` `New passwords do not match` `Current password is incorrect` `Something went wrong` `Password updated`

### Phone sign-in (phone identity)
  Copy: `Phone sign-in` `You sign in with your phone number ending in ••••{lastFour}.`
  Data shown: Auth phone identity
  ELEMENT: none (display)

### Google
  Copy: `Google` `You sign in with Google.` status `Connected` / `Not connected` check aria-label `Connected`

  ELEMENT: Connect / Working...
    Action: submit
    Destination: `/auth/callback?next=/dashboard?settings=security`
    Writes to: Auth identity (`linkIdentity` Google)
    Reads from: identities
    Toast: `Google could not be connected`

  ELEMENT: Disconnect / Working...
    Action: submit
    Destination: none
    Writes to: Auth (`unlinkIdentity`)
    Reads from: blocked if only one identity
    Toasts: `Google could not be disconnected` `Add another sign-in method before disconnecting Google` `Google disconnected`

### Add password backup (Google-only)
  Copy: `Add password backup` `Add a password so you can still sign in if Google is unavailable.`
  ELEMENT: Password / Confirm password / Add backup / Saving...
    Action: submit
    Destination: none
    Writes to: Auth `updateUser({ password })`
    Reads from: none
    Toasts: `Password must be at least 8 characters` `Passwords do not match` `We could not add a password backup. Try again.` `Password backup added`

### Add email and password backup (phone-only)
  Copy: `Add email and password backup` `Add an email and password so you have another way back in.` After send: `We sent a confirmation link to {email}. Confirm it to finish adding your backup sign-in method.`
  ELEMENT: Email / Password / Confirm password / Add backup / Saving...
    Action: submit
    Destination: none
    Writes to: Auth email+password
    Reads from: none
    Toasts: `Enter a valid email address.` `We could not add email and password backup. Try again.` `Check your email to confirm the backup sign-in method.` plus password match/length toasts above

### This device
  Copy: `This device` `Last active {Just now | Recently | formatted datetime}`
  ELEMENT: none (display)
    Reads from: Auth `last_sign_in_at` / `updated_at`

  ELEMENT: Manage account deletion →
    Action: toggle
    Destination: Danger zone tab (fallback `/dashboard?settings=danger`)
    Writes to: none
    Reads from: none

---

SECTION: Danger zone
  Copy: `Danger zone` `Account actions that cannot be undone`

### Sign out
  Copy: `Sign out` `End your session on this device.`

  ELEMENT: Sign out / Signing out...
    Action: submit
    Destination: `/`
    Writes to: clears Auth session
    Reads from: none

### Delete your account
  Copy: `Delete your account` `This permanently removes your profile, access records, and account data. This cannot be undone.` Dialog `Delete your account?` `This action is permanent. Type DELETE below to confirm.` Label `Confirmation` placeholder `DELETE`

  ELEMENT: Delete account
    Action: open modal
    Destination: confirm dialog
    Writes to: none
    Reads from: none

  ELEMENT: Cancel
    Action: toggle
    Destination: closes dialog
    Writes to: none
    Reads from: none

  ELEMENT: Permanently delete / Deleting...
    Action: submit
    Destination: `/` on success; `/login` if unauthorized
    Writes to: deletes Auth user (`auth.admin.deleteUser`); related rows cascade
    Reads from: confirmation must equal `DELETE`
    Errors: `You must be signed in to delete your account.` `We couldn't delete your account right now. Please contact support.`

---

SECTION: Help & contact
  Copy: `Help & contact` `Reach out or browse common answers` `Chat with us` `Usually responds within a day` `Request a missing course` `We prioritize by demand`

  ELEMENT: Open WhatsApp
    Action: external link
    Destination: `https://wa.me/96179057533`
    Writes to: none
    Reads from: none

  ELEMENT: Request a course
    Action: navigate
    Destination: `/courses#course-request`
    Writes to: none
    Reads from: none

  ELEMENT: Privacy Policy
    Action: navigate
    Destination: `/privacy`
    Writes to: none
    Reads from: none

  ELEMENT: Terms of Service
    Action: navigate
    Destination: `/terms`
    Writes to: none
    Reads from: none

  ELEMENT: Refund Policy
    Action: navigate
    Destination: `/refunds`
    Writes to: none
    Reads from: none

  ELEMENT: Academic Integrity
    Action: navigate
    Destination: `/academic-integrity`
    Writes to: none
    Reads from: none

---

## Not in the modal (removed half-features)

Dark mode and extra languages are not shipped. The orphaned Appearance and Language panes were removed so settings only shows working tabs. `/settings/appearance` and `/settings/language` still redirect to profile. `profiles.theme_preference` and `profiles.language_preference` remain in the schema for a later full feature — do not expose “coming soon” UI until then.

================================================================================
FILE: 06-study-views.md
HTML: /connects/06-study-views
RAW: /connects/raw/06-study-views
================================================================================
# Study views

Inside `/my-courses/{slug}` after Overview. Shell tabs and chapter picker are in [04-dashboard-and-workspace.md](./04-dashboard-and-workspace.md). Heartbeat `CourseStudyHeartbeat` writes `study_sessions` while a study tab is open (not a labeled button).

Student IA (restored from Git `c7fc162`): **Overview** plus content tabs **Notes**, **Formula sheet**, **Flashcards**, **Quiz**, **Exam**, **Exam review**, **Mistake fixes**, **Exercises**. `module_pack_items.item_type` stays on `tab=` URLs. Learn / Practice / Review is not the primary navigation.

Ask Tutor remains the course-grounded AI panel (`?panel=ai`). Quiz/flashcard/exam **viewers** keep later backend (mastery, FSRS, exam timer). Course outline column from the later redesign is not shown.

Shared empty / gating copy (`StudyWorkspaceState`):

- `No study content yet` / `Return to the course overview or check back later.`
- `Not published yet` / `This chapter does not have any published study material yet.` / `Pick another chapter…`
- `{Tool} is not in this chapter` / `Open {tool} instead.` / `Return to the course overview.`
- `Loading study tool` / `Preparing this study tool.` / `Hang tight — it will appear in a moment.`
- `{Tool} for this chapter is being prepared.`
- `Content is empty` / `Content unavailable` / `This study tool could not be loaded.` / `Try another tab…`

Shared reader chrome (markdown tabs): `Version` select; `Save to library` / `Remove from library`; `Print`; reader chapters list is mobile / below `lg`; `On this page`; empty headings `Headings from this page will appear here as you read.`; mobile `Chapters` drawer title `Chapters` / `Close contents`.

---

PAGE: /my-courses/{slug} (Overview)
Purpose: Next recommended action, course roadmap, progress, upcoming assessment, flashcard (weak-area) summary.
Access: logged-in + owns course
Entry points: course card; breadcrumb course code; locked-tab fallbacks

SECTION: Continue
  Copy: `Start here` or `Pick up where you left off`. Empty `No study content is ready yet. Check back when new notes are published.` CTA `Start learning →` / `Continue learning →`.
  Data shown: `content_progress`, next playable `module_pack_items`

  ELEMENT: Start learning → / Continue learning →
    Action: navigate
    Destination: study href for that tab (`tab=` remains the content identity)
    Writes to: none
    Reads from: next content item

SECTION: Course roadmap
  Copy: `Course roadmap`. Row `Chapter {n}` status `Not started` / `{pct}%` / `Done`. Content-type availability uses restored tab names (`Notes`, `Quiz`, …). Fallback `Study the core ideas and practice problems for {title}.`
  Data shown: `course_modules`, chapter progress, pack item types

  ELEMENT: Chapter row
    Action: toggle
    Destination: expands available study tools for that chapter
    Writes to: none
    Reads from: `course_modules.title`

  ELEMENT: Notes / Formula sheet / Flashcards / Quiz / Exam / Exam review / Mistake fixes / Exercises
    Action: navigate
    Destination: first matching `tab=` href for that type
    Writes to: none
    Reads from: pack item types

SECTION: Your progress
  Copy: `Your progress` `{n}% complete` `{started} of {total} chapter(s) started`
  Data shown: aggregated chapter / content progress
  ELEMENT: none (display)

SECTION: Flashcard cue (embedded)
  Copy: `Flashcards` `{n} due · ~{m} min` `Review now`
  Data shown: due count + session duration estimate (median when enough history, otherwise ~30s/card). Does not duplicate the flashcard landing dashboard.

  ELEMENT: Review now
    Action: navigate
    Destination: flashcards `?scope=course&autostart=scheduled`
    Writes to: none until session starts
    Reads from: due count

Empty course: `No published study material yet` + back to dashboard.

---

PAGE: /my-courses/{slug}?tab=notes
Purpose: Chapter notes markdown (Learn).
Access: logged-in + owns course
Entry points: Overview, Learn tab, resume **Resume lessons**. Deep link `tab=notes` survives.

SECTION: Notes reader
  Copy: Empty `Notes for this chapter are being prepared.` Type label `Notes`.
  Data shown: `module_pack_items.content_data` (markdown); `content_progress` (`content_type=notes`)

  ELEMENT: Version
    Action: navigate
    Destination: same tab with `variant=`
    Writes to: none
    Reads from: pack variants

  ELEMENT: Save to library / Remove from library
    Action: submit
    Destination: none
    Writes to: `library_items` (`item_type=saved_note`)
    Reads from: saved-note status

  ELEMENT: Print
    Action: toggle
    Destination: none (`window.print`)
    Writes to: none
    Reads from: none

  ELEMENT: Chapters list `Ch. {n} · {title}`
    Action: navigate
    Destination: notes href for that module (flushes notes progress first)
    Writes to: `content_progress` (notes, debounced)
    Reads from: `course_modules`

  ELEMENT: On this page heading
    Action: navigate
    Destination: `#headingId`
    Writes to: localStorage notes resume heading
    Reads from: markdown headings

  ELEMENT: Previous / Next
    Action: toggle
    Destination: in-page section
    Writes to: `content_progress` (notes)
    Reads from: heading outline

  ELEMENT: Practice this concept →
    Action: navigate
    Destination: `tab=exercises` else `tab=quiz`
    Writes to: none
    Reads from: available practice types

  ELEMENT: Open reference →
    Action: navigate
    Destination: `tab=formula_sheet` (Review intent; contextual from Learn)
    Writes to: none
    Reads from: none

---

PAGE: /my-courses/{slug}?tab=formula_sheet
Purpose: Printable formula markdown.
Access: logged-in + owns course (public catalog may deep-link a free-preview module; unauthenticated users hit login)

SECTION: Formula sheet reader
  Copy: Type `Formula sheet`. Empty `Formula sheet for this chapter is being prepared.` Same Version / Save / Print / Chapters / On this page as notes. Lesson Previous/Next **does not** write `content_progress`.
  Data shown: `module_pack_items.content_data`

  ELEMENT: Version / Save to library / Print / Chapters / On this page / Previous / Next
    Action: same as Notes except progress write
    Destination: formula_sheet href / print / in-page
    Writes to: `library_items` on save only
    Reads from: content_data

---

PAGE: /my-courses/{slug}?tab=flashcards
Purpose: Spaced-repetition + practice/browse.
Access: logged-in + owns course
Entry points: tab strip; Overview **Review {n} due**; library **Review →**; resume **Review flashcards**

SECTION: Landing
  Copy: `{n} due · ~{m} min` `Review due cards` `Learn new cards` `More options` `Browse all` `Needs attention` `Flashcard settings` chips `Due` `Learning` `New` caught-up `You're caught up` `Next review {time}` empty `Nothing to study right now.` loading `Loading your flashcard deck...` `Loading course flashcard schedule...` `Could not load course flashcard schedule.` `Flashcards for this chapter are being prepared.`
  Hidden from default UI: mature cards, leeches, FSRS, raw retention, coverage dashboard.
  Data shown: `flashcard_card_states`, `flashcard_deck_settings`, review logs

  ELEMENT: Review due cards
    Action: submit
    Destination: reviewing phase (mode `scheduled`)
    Writes to: `flashcard_sessions` insert
    Reads from: due queue

  ELEMENT: Learn new cards
    Action: submit
    Destination: reviewing (`learn_new`)
    Writes to: `flashcard_sessions`
    Reads from: new-card budget

  ELEMENT: Browse all
    Action: toggle
    Destination: browse phase
    Writes to: none
    Reads from: cards

  ELEMENT: Needs attention
    Action: submit
    Destination: reviewing (`practice`)
    Writes to: `flashcard_sessions`
    Reads from: weakCardIds + `is_leech` / `attention_flagged_at`

  ELEMENT: Back to course
    Action: navigate
    Destination: overview
    Writes to: none
    Reads from: empty state

SECTION: Browse
  Copy: `Browse all cards` `Unscheduled browsing — ratings will not change your schedule.` badge `Needs attention`
  Data shown: cards + session flagged set

  ELEMENT: Back
    Action: toggle
    Destination: landing
    Writes to: none
    Reads from: none

  ELEMENT: Card row (front text)
    Action: toggle
    Destination: reviewing that card
    Writes to: may start browse session
    Reads from: card front

SECTION: Session header
  Copy: `Scheduled review` / `Learn new` / `Practice` / `Browse`. Counters `Learning {n}` `Review {n}` `New {n}`
  Data shown: session queue

  ELEMENT: Undo
    Action: submit
    Destination: previous card
    Writes to: `undo_last_flashcard_rating` → `flashcard_card_states` / review logs
    Reads from: last rated card
    SR: `Last rating undone.`

  ELEMENT: End session
    Action: submit
    Destination: landing or completion
    Writes to: `flashcard_sessions` update
    Reads from: session stats

SECTION: Reviewer
  Copy: Labels `Question` `Answer` `Hint`. Helper `Again: missed it. Hard: recalled with effort. Good: knew it. Easy: too easy.` Group aria `Rate your recall`. Keyboard `Space reveals · 1–4 rate…` Banner `Needs attention`. Link `Review this in the lesson`. Confirm `Reset this card's scheduling?…`
  Data shown: current card; coarse interval previews on rating buttons

  ELEMENT: Card actions
    Action: open modal
    Destination: menu
    Writes to: none
    Reads from: none

  ELEMENT: Bury for today
    Action: submit
    Destination: next card
    Writes to: `bury_flashcard_card` → `flashcard_card_states`
    Reads from: card id
    Toast: `Card buried for today.`

  ELEMENT: Needs attention / Remove flag
    Action: submit
    Destination: none
    Writes to: `flag_flashcard_attention` → `flashcard_card_states.attention_flagged_at`
    Reads from: flagged set + `is_leech`

  ELEMENT: Report issue
    Action: submit
    Destination: report dialog
    Writes to: `study_content_reports` (fallback `content_reports`)
    Reads from: card front/back + category
    Toast: `Thanks — we logged this card for review.`

  ELEMENT: Advanced → Reset scheduling
    Action: submit
    Destination: next card
    Writes to: `reset_flashcard_scheduling`
    Reads from: card
    Toast: `Card scheduling reset.`

  ELEMENT: Advanced → Suspend card
    Action: submit
    Destination: next / complete
    Writes to: `suspend_flashcard_card`
    Reads from: card
    Toast: `Card suspended.`

  ELEMENT: Hint / Hide hint
    Action: toggle
    Destination: none
    Writes to: none
    Reads from: `card.hint`

  ELEMENT: Show answer
    Action: toggle
    Destination: none
    Writes to: none
    Reads from: none

  ELEMENT: Review this in the lesson
    Action: navigate
    Destination: notes `#section-{key}` (stable content anchor; hash omitted if heading is gone)
    Writes to: none
    Reads from: `sourceHeadingId` / `notesSource` / `conceptIds`

  ELEMENT: Again / Hard / Good / Easy (`1 · {interval}` …)
    Action: submit
    Destination: next card / waiting / complete
    Writes to: Scheduled/learn: `commit_flashcard_review` → `flashcard_card_states`, `flashcard_review_logs`, then `content_progress` (flashcards). Practice/browse: no schedule write.
    Reads from: rating previews

SECTION: Waiting
  Copy: `Waiting for learning card` + countdown

  ELEMENT: Finish for now
    Action: submit
    Destination: completion
    Writes to: `flashcard_sessions`
    Reads from: stats

  ELEMENT: Study ahead
    Action: submit
    Destination: reviewing (+30m ahead)
    Writes to: may continue `flashcard_sessions`
    Reads from: queue

SECTION: Completion
  Copy: `Session complete` / `Practice complete` `{n} cards reviewed` `Recall` `Time spent` `Ratings` `Still due` `Next review {time}` `{n} card(s) marked needs attention this session` `Does not change your schedule` (practice again)

  ELEMENT: Back to flashcards
    Action: toggle
    Destination: landing
    Writes to: none (refresh reads)
    Reads from: none

  ELEMENT: Return to course
    Action: navigate
    Destination: module flashcards or overview
    Writes to: none
    Reads from: backHref

  ELEMENT: Practice again
    Action: submit
    Destination: reviewing (practice)
    Writes to: `flashcard_sessions`
    Reads from: none

Session errors (toasts): could not load/open/start/save/undo/bury/suspend/reset; `Nothing to study in this mode right now.`

---

PAGE: /my-courses/{slug}?tab=quiz
Purpose: Multiple-choice practice with immediate explanation.
Access: logged-in + owns course

SECTION: Intro
  Copy: Eyebrow `{moduleTitle}` `Practice quiz` `{n} questions · Immediate feedback` `Finishing this checks it off. Passing and mastery are separate.` Empty `Quiz for this chapter is being prepared.`
  Data shown: `module_pack_items.content_data.questions`

  ELEMENT: Start quiz →
    Action: toggle
    Destination: answering (server presentation order when signed in)
    Writes to: `quiz_attempts` in_progress (`client_attempt_id`)
    Reads from: pack questions

SECTION: Answering / review
  Copy: `Question {i} of {n}` explanation after select. Keys `Keys 1-4 select / Enter continues / R retakes`
  Data shown: current question options

  ELEMENT: Option 1…n
    Action: toggle
    Destination: review state for that question
    Writes to: draft + `saveQuizProgress` until complete
    Reads from: `question.options`

  ELEMENT: Next question →
    Action: toggle
    Destination: next question
    Writes to: none
    Reads from: none

  ELEMENT: See results
    Action: submit
    Destination: complete
    Writes to: `quiz_attempts` (idempotent `client_attempt_id`); `quiz_attempt_answers`; `content_progress` completed at 100% on submit regardless of score; merged `user_concept_mastery`; optional `user_mistake_patterns`. Pass if score ≥ 80 (`lib/study/quiz-policy.ts`). Mastery is concept-level, not quiz completion.
    Reads from: answers array; pack item questions on the server

SECTION: Complete
  Copy: `Quiz submitted` `{n} / {total} correct` plus honest percent. `Attempted is not the same as mastered.` Pass copy from `attemptHeadline`. Concept summary from merged evidence: Strong / Developing / Needs work. One recommended next action. Per-question reasoning, not only the correct option.

  ELEMENT: Retake quiz
    Action: toggle
    Destination: answering (new attempt id, server shuffle)
    Writes to: new `quiz_attempts` row on start
    Reads from: none

  ELEMENT: Practice this weakness
    Action: toggle then submit
    Destination: follow-up question for that concept
    Writes to: practice concept outcomes (not a new full attempt)
    Reads from: question bank

---

PAGE: /my-courses/{slug}?tab=exam
Purpose: Practice exam. Answers after submit. Optional timer.
Access: logged-in + owns course

SECTION: Intro
  Copy: `Practice exam` `{n} questions · Answers after you submit`
  ELEMENT: Start untimed / Timed
    Writes to: `quiz_attempts` `mode=exam`
    Browser payload has no answer keys (`stripExamAnswerKey`).

SECTION: Complete
  Copy: `Exam submitted` plus score. Same mastery / attempted language as quiz.
  Scoring against pack item on the server. Retake starts a new UUID.

---

PAGE: /my-courses/{slug}?tab=exam_review
Purpose: Exam-week markdown.
Access: logged-in + owns course

SECTION: Reader
  Copy: Type `Exam review`. Empty `Exam review for this chapter is being prepared.`
  Data shown: `module_pack_items.content_data`

  ELEMENT: Version / Save to library / Print / Chapters / On this page
    Action: same as formula sheet
    Destination: exam_review href / print
    Writes to: `library_items` on save only
    Reads from: content_data

---

PAGE: /my-courses/{slug}?tab=mistake_fixes
Purpose: Recurring mistake patterns from tagged distractors, plus chapter trap markdown.
Access: logged-in + owns course

SECTION: Your mistake patterns
  Copy: `Your mistake patterns` empty `After a few quizzes, recurring slips show up here.`
  Data shown: `user_mistake_patterns`, `user_concept_mastery`

SECTION: Reader
  Copy: Type `Common traps`. Empty `Common traps for this chapter are being prepared.`
  ELEMENT: Version / Save to library / Print / Chapters / On this page
    Action: same as exam review
    Destination: mistake_fixes href
    Writes to: `library_items` on save
    Reads from: content_data

---

PAGE: /my-courses/{slug}?tab=exercises
Purpose: Practice-problem markdown.
Access: logged-in + owns course

SECTION: Reader
  Copy: Type `Exercises`. Empty `Exercises for this chapter are being prepared.`
  ELEMENT: Version / Save to library / Print / Chapters / On this page
    Action: same as exam review
    Destination: exercises href
    Writes to: `library_items` on save
    Reads from: content_data

---

## Write cheat sheet

| Surface | Writes |
|---|---|
| Notes scroll/sections | `content_progress` (notes); localStorage resume heading; optional `library_items` |
| Formula / exam review / mistake fixes / exercises | optional `library_items` only |
| Flashcards scheduled/learn ratings | `flashcard_sessions`, `flashcard_card_states`, `flashcard_review_logs`, `content_progress` (flashcards) |
| Flashcards bury/suspend/reset/undo | RPCs on card states / logs |
| Flashcards practice/browse ratings | session row; no schedule |
| Quiz/Exam complete | `quiz_attempts`, `content_progress` (quiz) |
| Open study tab | `study_sessions` via heartbeat |
| Course settings Save flashcard settings | `flashcard_deck_settings` |

================================================================================
FILE: 07-release-status.md
HTML: /connects/07-release-status
RAW: /connects/raw/07-release-status
================================================================================
# Release status

Public snapshot of what Gradeful actually ships. This page is the product map, not an attack guide. It updates when the site is deployed.

Live student copy in the app always wins over older inventories in `01`–`06`.

Live app SHA: `111936e`. App rollback SHA if this release must be reverted: `640eff4`. Database migrations are additive and are not reverted by rolling the app back.

## What students can do

- Search for a course on `/` and `/courses` (anonymous and logged-in users with no active plan)
- Logged-in users with active `user_access` hitting `/` go to `/dashboard` (server-side, no homepage flash)
- Preview original notes and practice on `/courses/[slug]`
- Pay with Whish on `/checkout` → `orders` → `user_access`
- Study from `/dashboard` into `/my-courses/{slug}` (restored pre-redesign dashboard and course tabs)
- Manage account from Settings (profile, notifications, billing, security, help, danger)
- Read this public map at `/connects` (no login)

`/access` is a leftover URL. It redirects to `/pricing`.

## Sources of truth

| Question | Source |
|---|---|
| Display price | `lib/payments/catalog.ts` via `getPaymentPlans()` |
| Amount charged | `orders.amount` written by the server |
| Entitlement (which courses) | `user_access.course_ids` (and `orders.course_ids` on the order) |
| Student home | `/dashboard` |
| Study URL | `/my-courses/{slug}` |

Junction tables `order_courses` and `access_grant_courses` may exist for dual-write. They are **not** the read source of truth. Invoker RLS helpers such as `student_has_paid_course` must read `user_access.course_ids` only. Do not cut over reads to the junctions in this release.

## Do not reintroduce

- Fake testimonials or review marquees
- Reservation checkout, reservation lookup, or `/api/reservations`
- Manual `/api/orders` inserts
- Sandbox OTP hints on live payment pages
- XP, public leaderboards, tournaments, boss battles, live presence
- Campus petitions or schedule-upload lockout
- Appearance / language settings UI until a real theme and i18n ship
- Public `user_badges` SELECT (`USING true`)

## Known accepted items

- AI Tutor stays **Coming soon** on the homepage while `workspace_ai_tutor_enabled` is off
- Practice-exam / mock-exam cards describe Gradeful practice, not university exams
- Auth leaked-password protection is an Auth dashboard setting, not something this site toggles in SQL
- Payment audit events are server-only by design
- Some historic tables keep RLS on with no client policies because grants were revoked (server-only / archived)
- Multiple permissive SELECT policies (admin + public/own) are an accepted pattern
- Exact pre-redesign copy that would be false or legally unsafe was not restored (see UI / copy restore in this file)

Full list: [08-known-limitations.md](./08-known-limitations.md)

## UI / copy restore (this change)

Visual and copy revert from Git history. Not a database rollback.

| Concern | Git reference |
|---|---|
| General dashboard UI | `89d0991` (parent of `88fc130` next-study-action redesign) |
| Course workspace UI | `c7fc162` (parent of `4c91035` Learn/Practice/Review) |
| Marketing/product copy + AUB/LAU tag colors | `a7de9a9` (parent of `d7f3973` tutoring-platform rewrite) |

Canonical dashboard route: `/dashboard`. Authoritative active access: `user_access` paid/confirmed, unexpired.

Copy that was **not** restored exactly (factual/legal):

- Fake testimonials / review marquees / a “what students say” section
- Welcome “Then your dashboard” (free users now land on `/` or `/courses`, not a paid dashboard)
- Plan durations still use live `durationDays` rather than a hardcoded “full semester” that can drift from catalog
- AI tutor / analytics / syllabus / exam-date privacy language that describes data the live system actually collects
- Checkout independence acknowledgement (legal) kept next to the restored personal-study checkbox
- `/access` links stay pointed at `/pricing` (legacy URL is a redirect)
- Sandbox OTP hints
- The old homepage affiliation pill (compatibility-only framing is used instead: `Course-specific study packs`)
- Exam-insider and professor-match claims from the old feature cards
- Unverifiable prestige / “top student” social proof
- Reservation / schedule-lockout / public leaderboard / XP copy
- Plan prices still come from `getPaymentPlans()` (not hardcoded dollar amounts in the homepage body)

AUB/LAU tags: catalog cards and homepage marquee use filled `brand-pine` / `brand-peach`; `UniversityBadge` / course-page chips use the bordered pine/peach tokens from `getUniversityBadgeStyles`.

## Hosted schema (additive, no table drops)

Applied on the live Gradeful database through `20260910080000` (array-SOT entitlement helper) and `20260910075625` (drop public badge count, drop duplicate `analytics_events_name_idx`). Entitlement **reads** still use `course_ids` arrays. Junctions are dual-write only.

## Rollback

1. Revert the Vercel production deployment to SHA `640eff4`, or `git revert` the app commit on `main` and push. Do **not** `git push --force`.
2. Leave hosted SQL in place. Additive migrations (`student_has_paid_course` array SOT, badge policy drop, index drop) are safe to keep.
3. Do not run `supabase db reset` or drop archived tables to roll back.

## Monitoring

- Vercel production deployment for **branch `main`** only (not a `cursor/*` preview)
- `bash scripts/verify-live.sh`
- Supabase Auth dashboard: enable leaked-password protection (HaveIBeenPwned)
- Supabase advisors: `rls_enabled_no_policy` on archived/server-only tables is accepted
- Whish reconcile: daily cron on hobby; payment webhook still required for live grants

## Legacy tables (kept, not dropped)

Historic rows may still exist for support dumps: `reservations`, `campus_petitions`, `schedule_verifications`, `course_xp`, `tournaments`, `payment_sessions`. The product does not write them anymore.

================================================================================
FILE: 08-known-limitations.md
HTML: /connects/08-known-limitations
RAW: /connects/raw/08-known-limitations
================================================================================
# Known limitations

Public list of accepted gaps, dual-write warnings, and things this release does not ship. Not an attack guide.

## Entitlements

- **Read source of truth is `user_access.course_ids`.** `orders.course_ids` records what was purchased. Junctions `order_courses` and `access_grant_courses` are dual-write only.
- Cutting invoker RLS over to `access_grant_courses` caused permission denied on entitled course loads. Do not repeat that cutover until a dedicated migration grants the right reads.
- `public.student_has_paid_course(uuid)` is `SECURITY INVOKER` and must keep reading the array.

## Auth

- Leaked-password protection is off until it is toggled in the Supabase Auth dashboard (HaveIBeenPwned). SQL cannot enable it.
- Phone signup is not offered.
- Dark mode and i18n are not shipped. Appearance and language settings UI stay removed.

## Payments

- Live checkout is Whish. There is no reservation path and no placeholder provider.
- Refunds are course withdrawal/drop with university documentation only.
- Hobby Vercel uses a daily Whish reconcile cron; webhooks are still the live grant path.

## Routing

- Anonymous `/` → marketing homepage
- Logged-in, no active `user_access` → marketing homepage
- Logged-in, pending/abandoned/expired/revoked access → marketing homepage
- Logged-in, any currently valid paid plan → `/dashboard` (proxy + `app/page.tsx`, no homepage flash)
- Active users may still open `/pricing`, `/courses`, public course pages, and legal pages on purpose

## Product flags

- Homepage AI Tutor stays Coming soon while `workspace_ai_tutor_enabled` is off.
- Public leaderboards, XP, tournaments, boss battles, campus petitions, and schedule-upload lockout are retired. Tables may still exist with grants revoked.

## SEO

- `/robots.txt` disallows `/dashboard`, `/my-courses`, `/admin`, `/checkout`, `/welcome`, `/api/`, and auth/dev routes.
- `/connects` stays indexable. `/sitemap.xml` lists marketing pages, catalog courses, and connects docs.

## Database advisors (accepted)

- INFO `rls_enabled_no_policy` on archived or server-only tables (`orders`, `payment_events`, `order_courses`, reservations, petitions, XP, tournaments). Grants were revoked.
- WARN `multiple_permissive_policies` on public catalog tables (admin SELECT plus public/own SELECT).
- INFO unused indexes: do not drop or add indexes on archived tables in this pass.
- WARN `auth_leaked_password_protection`: dashboard toggle, documented above.

## Error catalog (student-facing)

| Situation | What the student sees |
|---|---|
| Catalog / Supabase outage | Clean empty or retry copy. Raw PostgREST messages are never shown. |
| Anonymous `/dashboard` | Redirect to `/login?next=…` |
| No paid access on a course workspace URL | Redirect to `/pricing` |
| Whish payment failure | `/checkout/payment/failure` |
| Maintenance flag | `/maintenance` except login, auth, connects |

## Not in this release

- Junction-table entitlement cutover
- Dropping `course_ids` arrays or archived tables
- Enabling AI Tutor on the homepage without the platform flag
- Dark mode / language switching
- Playwright suites or new npm packages

================================================================================
FILE: README.md
HTML: /connects
RAW: /connects/raw/README
================================================================================
# Gradeful site structure

Inventory of every public and student-facing surface: copy, buttons, data, and how flows connect.

These files are published on the public site (no login):

- Hub: [/connects](/connects)
- All files as one markdown dump (paste into an AI): [/connects/raw](/connects/raw)
- One file: `/connects/raw/00-how-everything-connects` (and the other filenames)

Production URLs after this ships on `main`:

- https://gradeful.app/connects
- https://gradeful.app/connects/raw

The live pages read these markdown files from the repo, so they stay in sync on every deploy.

Start with **how everything connects**, then use the page inventories for verbatim copy and every control.

| File | What it covers |
|---|---|
| [00-how-everything-connects.md](./00-how-everything-connects.md) | Registration, checkout, dashboard, settings, study — numbered flows, tables, and how they hook together |
| [07-release-status.md](./07-release-status.md) | What is live, sources of truth, rollback, monitoring |
| [08-known-limitations.md](./08-known-limitations.md) | Dual-write warning, accepted advisors, student error catalog |
| [01-marketing-pages.md](./01-marketing-pages.md) | Homepage, pricing, courses, about, contact, policies, etc. |
| [02-auth-onboarding.md](./02-auth-onboarding.md) | Login, signup, forgot/reset password, `/welcome`, auth callback |
| [03-checkout.md](./03-checkout.md) | `/checkout`, Whish payment, success/failure, `orders` → `user_access` |
| [04-dashboard-and-workspace.md](./04-dashboard-and-workspace.md) | Sidebar, dashboard home, library, progress, semester plan, course shell |
| [05-settings.md](./05-settings.md) | Settings modal: every tab, field, toggle, and write |
| [docs/pricing.md](../pricing.md) | Unit economics, catalog source of truth, plan transitions (not a page inventory) |

Format on page files:

```
PAGE: /route
Purpose:
Access: public / logged-in / onboarded / admin-only
Entry points:

SECTION: [name]
  Copy: [verbatim]
  Data shown: [table.column or static]

  ELEMENT: [label verbatim]
    Action: navigate / submit / toggle / open modal / external link
    Destination:
    Writes to:
    Reads from:
```

Admin (`/admin/*`) is not in this set.
