---
name: helpcenter
description: Generate help center screenshots and articles with full multi-language support (DE, EN, FR, IT), Playwright element capture, annotation overlays, and structured help article creation
---

# /helpcenter Workflow

Generate professional, zoomed-in screenshots of UI elements and compose them into structured help articles for the in-app Help Center.

> [!IMPORTANT]
> This workflow produces **static screenshot-based guides**, not videos. Screenshots are zoomed/cropped to the relevant UI element (usually a dialog or modal), never showing empty page background. The `FeedbackWidget` must be hidden in all captures.

> [!CAUTION]
> **Writing style rules (mandatory):**
> - **NEVER** use ` —` (em dash with space before it) anywhere in any text, file, or article content
> - **Avoid** using `-` (hyphens) wherever possible; use colons, commas, or restructure the sentence instead
> - These rules apply to workflow text, i18n translation keys, commit messages, article content, code comments, and all generated documentation

// turbo-all

---

## 🔴 GOLDEN RULE: German-First Proof of Concept

> [!CAUTION]
> **EVERY new help article MUST follow the German-first workflow.** This is non-negotiable.

**The process is:**

1. **Create the GERMAN version first** (screenshots + locale content + HelpCenter.js config)
2. **Present it to the user for review** in the browser at `http://localhost:3000/help/{category}/{slug}`
3. **Iterate until the user confirms** that all screenshots and text are correct
4. **Only AFTER user confirmation**, create the EN, FR, and IT versions
5. **Never create all 4 languages simultaneously** before the German version is approved

**Why:** Screenshots and text descriptions easily drift out of sync. Step ordering can be wrong, selectors can miss, and content can be inaccurate. It is far cheaper to fix one language than four. The German version serves as the proof of concept that validates the entire flow.

---

## Overview

### Architecture

```
e2e/tests/help-screenshots/          ← Playwright capture scripts (one per feature)
e2e/helpers/help-screenshot-helpers.js ← Shared utilities for element capture (supports lang param)
client/public/help/{feature}/          ← Output PNGs served as static files
client/public/help/{feature}/{lang}/   ← Language-specific PNGs (de, en, fr, it)
client/src/pages/HelpCenter.js         ← React page (index + article + step, all Tailwind inline)
client/src/locales/{lang}/help.json    ← Article text (step titles, descriptions, tips)
e2e/scripts/upload-help-screenshots.js ← Cloudinary upload script (writes manifest)
e2e/scripts/cleanup-help-screenshots.js ← Cloudinary cleanup script (removes orphaned assets)
client/src/data/help-images.json       ← CDN manifest (auto-generated, DO NOT edit manually)
```

> [!IMPORTANT]
> **Cloudinary integration is live.** After capturing screenshots and getting user approval:
> 1. Run `node e2e/scripts/upload-help-screenshots.js --feature {feature-name}`
> 2. This uploads all PNGs (including language subfolders) to Cloudinary and writes `help-images.json`
> 3. `HelpCenter.js` resolves images via `getHelpImage(feature, label, lang)` with CDN → local fallback
> 4. **Multi-language screenshots** are MANDATORY: every step must have screenshots in all 4 languages (de, en, fr, it)
> 5. The upload script handles language subfolders automatically

### Key Principles

1. **German-first, then expand**: Create and validate the German version before producing other languages
2. **Element-level capture**: Crop to the dialog/modal/section, never full-page screenshots with empty whitespace
3. **No FeedbackWidget**: Always hide the feedback bug button (`z-[60]`) before capturing
4. **No login noise**: Use API for data setup, browser only for navigating to the target state
5. **Red highlight rings mandatory**: Every screenshot for steps 2+ MUST have a red highlight ring (`#ef4444`) on the relevant element
6. **Source-first selectors**: Read the actual component file to find selectors, never guess
7. **Full language coverage**: ALL steps must be captured in ALL 4 languages (after DE is approved)
8. **Regional code awareness**: `i18n.language` can return `en-US`, `de-CH`, etc. The resolver normalizes with `lang.split('-')[0]`
9. **Process completeness**: Articles MUST cover all process flows including edge cases, not just the happy path
10. **Cross-article linking**: When a user action leads to a substantial secondary process (refunds, reschedules, disputes), link to the dedicated article covering that flow

---

## Step 0: Component Analysis & Process Flow Mapping

> **Goal**: Deeply understand the feature by reading ALL related source code, then map the COMPLETE process including ALL decision paths, edge cases, and secondary flows BEFORE writing any capture script or article text.

> [!CAUTION]
> **This is the most critical step. Skipping or rushing this phase leads to wrong step counts, incorrect screenshots, mismatched descriptions, and days of painful rework.** The create-offer experience proved that understanding the component source is the difference between a 1-hour task and a 2-day nightmare.

### 0a. Read ALL Related Components

Before writing a single line of spec code or article text, you MUST read:

1. **The primary UI component** (e.g., `OfferCreateEditModal.js`, `ManageAvailabilityModal.js`)
   - Understand every form field, every wizard step, every conditional section
   - Note all required fields and validation logic (e.g., `isStepXValid`)
   - Identify all buttons, toggles, dropdowns, and their exact labels

2. **The detail/display component** (e.g., `OfferDetailPage.js`, `BookingDetailsModal.js`)
   - Understand what the user sees AFTER creation
   - Note status-dependent UI changes (different buttons per status)

3. **The dashboard/list component** (e.g., `OffersDashboardTab.js`)
   - Understand the dashboard view, filters, metrics, and card layout

4. **The data model** (e.g., `server/models/Offer.js`)
   - Understand all fields, statuses, and lifecycle transitions
   - This tells you what states are possible and what paths exist

5. **The API/controller** (e.g., `server/controllers/offerController.js`)
   - Understand what happens on the backend for each action
   - This reveals hidden flows (e.g., automatic expiration, background jobs)

6. **The locale files** (e.g., `client/src/locales/de/{feature}.json`)
   - Know the exact German labels for every button, tab, and section
   - These are your selector reference for text-based Playwright locators

7. **Related services** (e.g., `offerAPI.js`, `offerPdfGenerator.js`)
   - Understand supporting functionality like PDF generation, payment flows

**Create a COMPONENT ANALYSIS SHEET:**

```
COMPONENT ANALYSIS: Create Offer
═══════════════════════════════════════════
Files Read:
  ✅ OfferCreateEditModal.js (wizard: 3 steps, 11 screenshot-worthy moments)
  ✅ OfferDetailPage.js (post-creation view, status banners)
  ✅ OffersDashboardTab.js (metrics, filters, card layout)
  ✅ Offer.js model (statuses: draft, sent, accepted, paid, ...)
  ✅ offerController.js (create, send, accept, decline, ...)
  ✅ de/offers.json (all German labels)
  ✅ offerAPI.js (API endpoints)
  ✅ offerPdfGenerator.js (PDF feature)

Wizard Steps (from component source):
  Step 1 "Kunde": Client selector, title, description, valid-until date
  Step 2 "Positionen": Service chips import, custom line items, quantities
  Step 3 "Prüfen": Client price box, coach payout box, AGB section, send/draft buttons

Screenshot-Worthy Moments (mapped from component):
  1. Dashboard overview (with metrics)
  2. Click "Create Offer" button
  3. Client selector (Step 1)
  4. Title + description + date fields (Step 1)
  5. Service chips (Step 2)
  6. Custom line items (Step 2)
  7. Edit line items (Step 2)
  8. Client price summary (Step 3)
  9. Coach payout summary (Step 3)
  10. AGB section (Step 3)
  11. Send/draft buttons (Step 3)

Conditional UI:
  - Service chips only visible if coach has services configured
  - "An Kunde senden" disabled if no line items added
  - Valid-until date defaults to 14 days from now
```

> [!IMPORTANT]
> **The step count comes FROM the component analysis, not from guessing.** If the wizard has 3 pages with 4 important sections each, that's ~12 screenshot steps. Count them BEFORE writing the spec. The create-offer flow has 11 steps because the AGB section and send button on Step 3 each deserve their own screenshot.

### 0b. Realistic Sample Content

> [!CAUTION]
> **NEVER capture empty states when demonstrating how to fill out a form or upload media.** 
> - If a step shows image or video uploading, you MUST use the `generate_image` tool to create a generic, high-quality, relevant sample image and upload it in the spec.
> - If a step shows text input or rich-text editing, you MUST type realistic, professional sample content (e.g., "Stress Management Coaching" instead of "Test Program").
> - Screenshots must look like a real user is actively setting up their product.

### 0c. Map the Feature Lifecycle

For each feature, create a **decision tree** that covers every possible flow:

1. **Identify the primary (happy) path**: The most common user journey
2. **Identify ALL secondary paths**: What happens when the user takes a different action at each decision point
3. **Identify edge cases**: Error states, permission differences (coach vs client), conditional UI elements

```
PROCESS FLOW MAP: {Feature}
═══════════════════════════════════
Primary Flow (Happy Path):
  Step 1 → Step 2 → Step 3 → Success

Decision Points:
  After Step 2, user can:
  ├── Confirm → Step 3 (happy path)
  ├── Decline → Decline flow (document inline or link to separate article)
  ├── Reschedule → Links to "Reschedule Bookings" article
  └── Cancel → Links to "Cancel & Refund" article

Edge Cases:
  - What if user has no bookings? → Empty state UI
  - What if booking is expired? → Different action buttons
  - Coach view vs Client view → Different permissions and options
  - Conditional tabs/sections → Document when they appear and why
```

### 0d. Decide Article Scope vs Cross-Links

For each secondary flow, decide:

| Secondary Flow Size | Action | Example |
|---|---|---|
| **Small** (1-2 steps) | Document inline within the current article | "Click the toggle to enable" |
| **Medium** (3-5 steps) | Add a collapsible section within the article | "How to apply a discount code" |
| **Large** (5+ steps, own lifecycle) | Create a separate article and add a cross-link | "For refund details, see: Refund & Cancellation" |

### 0e. Cross-Link Data Model

Cross-links are defined in the `helpArticles` config in `HelpCenter.js`:

```javascript
{
  id: 'manage-bookings',
  feature: 'manage-bookings',
  // ... standard fields ...
  relatedArticles: [
    { articleId: 'cancel-booking', contextKey: 'articles.manageBookings.related.cancelBooking' },
    { articleId: 'reschedule-booking', contextKey: 'articles.manageBookings.related.reschedule' },
    { articleId: 'refund-booking', contextKey: 'articles.manageBookings.related.refund' },
  ],
}
```

In `help.json`, the context text explains WHY this link is relevant:
```json
{
  "articles": {
    "manageBookings": {
      "related": {
        "cancelBooking": "Need to cancel? Learn about cancellation policies and refund tiers.",
        "reschedule": "Want to change the date? See how to propose, accept, or decline a new time.",
        "refund": "Looking for a refund? Understand the refund process and dispute escalation."
      }
    }
  }
}
```

> [!IMPORTANT]
> **When to create a cross-link:** If during article creation you discover that an action button or decision leads to a flow that requires 5+ steps to fully explain, it MUST become its own article with a cross-link. Never try to cram a full lifecycle (like refunds) into a single step of another article.

### 0f. Document Conditional UI

Many components render different UI based on state. **Document ALL conditions:**

```
CONDITIONAL UI MAP: BookingDetailsModal
═══════════════════════════════════
Tab: "Notes"
  Visible when: canViewNotes = sessionLinkSessionId exists AND user is coach or client
  Hidden when: No linked session (e.g., pending requests)

Tab: "Reschedule"
  Visible when: booking.status is 'confirmed' AND NOT past date

Action: "Cancel Booking"
  Visible when: booking.status in ['confirmed', 'pending']
  Hidden when: booking.status in ['cancelled', 'completed']

Action: "Confirm Booking"
  Visible when: booking.status === 'requested' AND user is coach
  Hidden when: All other states
```

This map drives BOTH the article text (explaining when features appear) and the test setup (ensuring the right booking state for each screenshot).

### 0f. Plan Step Count and Image Labels

**Before any coding**, write down the exact step list with image labels:

```
STEP PLAN: Create Offer (11 steps)
═══════════════════════════════════
 Step  1: step-1-dashboard         → Dashboard overview with "Create Offer" button
 Step  2: step-2-create-button     → Click the button (red highlight on button)
 Step  3: step-3-client-select     → Client selector (red highlight on dropdown)
 Step  4: step-4-title-description → Title, description, valid-until date fields
 Step  5: step-5-service-chips     → Service import chips (red highlight on chips)
 Step  6: step-6-custom-items      → Custom line item form
 Step  7: step-7-edit-items        → Line item cards with edit/delete
 Step  8: step-8-summary-client    → Client price box (red highlight)
 Step  9: step-9-summary-coach     → Coach payout box (red highlight)
 Step 10: step-10-agb              → AGB section (red highlight)
 Step 11: step-11-send-buttons     → Send/Draft buttons (red highlight)
```

> [!WARNING]
> **The step plan must match EXACTLY between:** the Playwright spec (image labels), `HelpCenter.js` (article config), and `help.json` (step content). If any of these three are out of sync, the article breaks. The create-offer bug where step 10 showed "validity date" instead of "AGB" was caused by the locale file not being updated when the step order changed.

---

## Step 1: Capture Script (Multi-Language)

> **Goal**: Write the Playwright screenshot script with full multi-language support.

### 1a. File Structure

Create TWO files:

1. **Single-language (German only, for rapid iteration):**
   `e2e/tests/help-screenshots/help-{feature}.spec.js`

2. **Multi-language (all 4 languages, for production):**
   `e2e/tests/help-screenshots/help-{feature}-multilang.spec.js`

> [!CAUTION]
> The multi-language spec MUST capture **ALL steps** in **ALL languages**. Never leave any step out. If the original spec has 9 steps, the multi-language spec must also have 9 steps per language (= 36 tests for 4 languages).

### 1b. Multi-Language Spec Template

```javascript
// @ts-check
const { test, expect } = require('@playwright/test');
const { login } = require('../../helpers/auth');
const { goToDashboard, clickDashboardTab } = require('../../helpers/navigation');
const {
  hideHelpScreenshotNoise,
  captureElement,
  captureElementWithHighlight,
  captureFullDialog,
  captureDialogSection,
} = require('../../helpers/help-screenshot-helpers');

const FEATURE = '{feature}';

// Language-specific selectors
// CRITICAL: Read ALL translation files before writing these!
// Check: client/src/locales/{lang}/managesessions.json
// Check: client/src/locales/{lang}/header.json
// Check: client/src/locales/{lang}/userdashboard.json
const LANG_CONFIG = {
  de: {
    sessionsTab: 'Sitzungen',
    availabilityBtn: 'Verfügbarkeit',
    priceLabel: 'Preis',
  },
  en: {
    sessionsTab: 'Manage Sessions',
    availabilityBtn: 'Availability',
    priceLabel: 'Price',
  },
  fr: {
    sessionsTab: 'sessions',        // partial match for "Gérer les sessions"
    availabilityBtn: 'disponibilité',
    priceLabel: 'Prix',
  },
  it: {
    sessionsTab: 'sessioni',        // partial match for "Gestisci sessioni"
    availabilityBtn: 'disponibilità',
    priceLabel: 'Prezzo',
  },
};

const LANGUAGES = Object.keys(LANG_CONFIG);

// Set the app language via localStorage before the page loads.
// i18next-browser-languagedetector reads `i18nextLng` from localStorage.
async function setLanguage(page, lang) {
  await page.addInitScript((lng) => {
    localStorage.setItem('i18nextLng', lng);
  }, lang);
}

for (const lang of LANGUAGES) {
  test.describe(`Help Screenshots: {Feature} [${lang.toUpperCase()}]`, () => {
    test.beforeEach(async ({ page }) => {
      await setLanguage(page, lang);
    });

    // ALL steps go here, passing `lang` to every capture function
    test(`[${lang}] Step 1: Description`, async ({ page }) => {
      await login(page, 'coach');
      // ... navigate to target state
      await captureElementWithHighlight(page, {
        target: 'button:has-text("...")',
        container: 'main',
        label: 'step-1-something',
        feature: FEATURE,
        lang,  // <-- ALWAYS pass lang!
      });
    });

    // Repeat for ALL steps...
  });
}
```

### 1c. Language Switching: How It Works

> [!IMPORTANT]
> **The language is set via `localStorage` using `addInitScript`, which runs BEFORE the page loads.**
> This is critical because `i18next-browser-languagedetector` reads `i18nextLng` from localStorage on initialization. Setting it after page load would not switch the language properly.

```javascript
async function setLanguage(page, lang) {
  // addInitScript runs in the page context BEFORE any other scripts
  await page.addInitScript((lng) => {
    localStorage.setItem('i18nextLng', lng);
  }, lang);
}
```

**Common pitfalls:**
- Do NOT use `page.evaluate()` to set language (runs too late, after React renders)
- Do NOT use URL params like `?lng=en` (not supported by our i18n setup)
- `addInitScript` must be called BEFORE `page.goto()` or `login()`

### 1d. Language-Specific Selectors: Best Practices

| Approach | When to use | Example |
|---|---|---|
| **Partial text match** | Buttons, tabs, labels | `has-text("sessions")` matches "Manage Sessions" |
| **ID selectors** | Switches, inputs, form fields | `#availableForInstantBooking` (language-independent) |
| **Role selectors** | Dialogs, modals | `[role="dialog"]` (universal) |
| **CSS class selectors** | Layout containers | `.grid.grid-cols-2` (universal) |

> [!TIP]
> Prefer non-text selectors (IDs, roles, CSS classes) where possible. They work across all languages without configuration. Only use text-based selectors when there is no structural alternative.

### 1e. Helper Utilities

The `help-screenshot-helpers.js` provides these core functions. **All capture functions accept an optional `lang` parameter** that controls the output subdirectory:

#### `hideHelpScreenshotNoise(page)`
Hides UI elements that shouldn't appear in help screenshots:
- FeedbackWidget (the floating bug button at bottom-right)
- Toast notifications
- Any dev-only overlays

#### `captureElement(page, { selector, label, feature, lang, padding })`
Takes a screenshot of exactly one element, with optional padding.
When `lang` is provided, saves to `client/public/help/{feature}/{lang}/{label}.png`

#### `captureElementWithHighlight(page, { target, label, feature, lang, highlightColor, padding })`
Same as `captureElement` but first injects a CSS highlight ring around the target element.

#### `captureFullDialog(page, { label, feature, lang, padding })`
Captures the entire visible `[role="dialog"]` element.

#### `captureDialogSection(page, { section, highlight, label, feature, lang, padding })`
Captures a specific section within a dialog, with optional highlight on a sub-element.

### 1f. Capture Best Practices

| Technique | When to Use | How |
|---|---|---|
| **Element crop** | Modals, dialogs, forms | `captureFullDialog` or `captureElement` with `[role="dialog"]` |
| **Highlight ring** | Showing "click here" | `captureElementWithHighlight` with a red/cyan glow |
| **Section zoom** | Specific part of a dialog | `captureElement` targeting the inner section container |
| **Two-state comparison** | Toggle on/off, select change | Capture before → interact → capture after |
| **Full page (rare)** | Only for context/navigation overview | `page.screenshot()` with tight viewport |

### 1g. Resolution and Quality

| Setting | Value | Reason |
|---|---|---|
| **Viewport** | 1440×900 | Matches desktop experience |
| **Device scale factor** | 2 | Retina-quality captures |
| **Format** | PNG | Lossless, sharp UI screenshots |
| **Element padding** | 24px | Clean border around captured elements |

---

## Step 2: Run Captures

### 2a. Add Playwright Project (if not already added)

Ensure `playwright.config.js` has the `help-screenshots` project:

```javascript
{
  name: 'help-screenshots',
  testDir: './tests/help-screenshots',
  use: {
    browserName: 'chromium',
    ignoreHTTPSErrors: true,
    screenshot: 'off',        // We manage screenshots manually
    video: 'off',             // No video needed
    viewport: { width: 1440, height: 900 },
    deviceScaleFactor: 2,      // Retina quality
  },
},
```

### 2b. Run the Multi-Language Script

```bash
cd e2e
npx playwright test tests/help-screenshots/help-{feature}-multilang.spec.js --project=help-screenshots --reporter=list --retries=1
```

> [!WARNING]
> Multi-language runs can take 15-40 minutes depending on step count (e.g., 32 tests at ~1m each). Plan accordingly. Use `--retries=1` to handle transient login timeouts.

### 2c. Verify Output

Check `client/public/help/{feature}/` for the generated PNGs:
- Each language subfolder (de/, en/, fr/, it/) must contain ALL steps
- Each PNG should be cropped to the relevant UI element
- No FeedbackWidget visible
- Highlight rings clearly visible on target elements
- Text is sharp and readable at 100% zoom
- **Verify that each language screenshot actually shows the correct language** (not defaulting to German)

---

## Step 3: Screenshot Validation (Cross-Check Text vs Image)

> **Goal**: Systematically validate that every article step's text accurately describes what is shown in the corresponding screenshot. Fix any mismatches by re-capturing with the browser.

> [!IMPORTANT]
> **This is a TWO-PASS process.** Pass 1 generates screenshots via the Playwright spec (fast, automated). Pass 2 validates each screenshot against the article text and manually corrects any mismatches using the browser tool.

### 3a. Automated Cross-Check (File Level)

> [!NOTE]
> This is a preliminary cross-check. The final mandatory validation happens in Step 6 directly in the browser.

For EVERY step in EVERY language, perform this validation:

1. **View the screenshot** using the `view_file` tool
2. **Read the corresponding article text** from `help.json`
3. **Cross-check** each of these criteria:

| Check | What to verify | Fix if wrong |
|---|---|---|
| **Language match** | Screenshot UI text matches the article language | Re-run spec for that language |
| **Content match** | The screenshot shows the exact UI state described by the step text | Update step text OR re-capture screenshot |
| **UI element visibility** | Every element mentioned in the step text is visible in the screenshot | Adjust the spec to scroll/expand/click before capture |
| **Action buttons** | If the text mentions actions (e.g., "Click Cancel"), those buttons must be visible | Navigate to the correct state before capture |
| **Tab/section match** | If the text says "Basis Info tab", the screenshot must show that tab as active | Fix the spec's tab navigation |
| **Loading state** | Screenshot must NOT show spinners, skeleton loaders, or loading indicators | Add wait logic (see Learnings) |
| **Overlays** | No cookie banners, toast notifications, or feedback widgets visible | Add dismissal/hide logic (see Learnings) |

### 3b. Manual Browser Correction

When a cross-check fails and the fix is not a simple spec adjustment:

1. **Open the browser** to the relevant page using the browser subagent
2. **Navigate to the exact state** needed for the screenshot
3. **Dismiss any overlays** (cookie banners, modals, tooltips)
4. **Capture the correct screenshot** using the browser's screenshot capability
5. **Save** the corrected screenshot to the correct path: `client/public/help/{feature}/{lang}/{step-label}.png`

```
VALIDATION LOG: {Feature} [{Lang}]
═══════════════════════════════════
Step 1: ✅ Text matches screenshot
Step 2: ✅ Text matches screenshot
Step 3: ❌ MISMATCH: Text says "Filter by status" but screenshot shows unfiltered list
  → FIX: Re-captured via browser with filter dropdown open
Step 4: ✅ Text matches screenshot
Step 5: ❌ MISMATCH: Screenshot shows loading spinner instead of Notes tab
  → FIX: Added wait for .rbc-event elements before capture
Step 6: ✅ Text matches screenshot
```

### 3c. Validation Checklist (Per Step)

For each step, verify ALL of these:

- [ ] Screenshot is NOT a loading/spinner state
- [ ] Screenshot is NOT showing a cookie consent banner
- [ ] Screenshot language matches the article language
- [ ] All UI elements mentioned in the step text are visible in the screenshot
- [ ] The active tab/section in the screenshot matches what the text describes
- [ ] Action buttons mentioned in the text are visible in the screenshot
- [ ] The screenshot is properly cropped (no excessive whitespace)
- [ ] The screenshot shows real data (not empty states unless that's the point)

### 3d. When to Update Text vs Re-Capture

| Situation | Action |
|---|---|
| Screenshot is correct but text description is vague or misleading | Update the `help.json` text |
| Text is correct but screenshot doesn't show the right state | Re-capture the screenshot (spec fix or manual) |
| Both are wrong | Fix both: update text AND re-capture |
| UI element is conditionally rendered and not visible | Ensure test setup creates the right conditions for visibility |

---

## Step 4: Help Article Data

> **Goal**: Create the structured article data in locale files with complete process coverage.

### 4a. Article Data Structure

Each article is defined with **nested JSON keys** in `client/src/locales/{lang}/help.json`:

```json
{
  "articles": {
    "coachAvailability": {
      "title": "Verfügbarkeit erstellen",
      "description": "So legst du fest, wann Kunden ...",
      "timeToRead": "5 Min.",
      "steps": {
        "1": {
          "title": "1. Navigiere zu «Sitzungen Verwalten»",
          "text": "Öffne dein Dashboard und klicke ...",
          "tip": "Dieser Button ist nur für Coaches sichtbar ..."
        }
      },
      "related": {
        "cancelBooking": "Buchung stornieren? Erfahre mehr über die Stornierungsbedingungen.",
        "reschedule": "Termin verschieben? So schlägst du einen neuen Termin vor."
      }
    }
  }
}
```

> [!IMPORTANT]
> **All 4 language files must be kept in sync.** When adding or modifying a help article:
> 1. Write the content in German first (primary language)
> 2. Translate to EN, FR, IT immediately
> 3. Verify that all keys exist in all locale files
> 4. Never leave placeholder text like "TODO" or untranslated English in non-EN files

The article's **image labels** go in the article data object in `HelpCenter.js` (not in locale files):
```javascript
{ titleKey: 'articles.coachAvailability.steps.1.title', imageLabel: 'step-1-button' }
```
`getHelpImage(feature, imageLabel, lang)` resolves: CDN (lang-specific → default) → local `/help/` fallback.

> [!IMPORTANT]
> **Regional language code handling:** The `getHelpImage` function normalizes regional codes (e.g., `en-US` → `en`, `de-CH` → `de`) using `lang.split('-')[0]`. This is critical because the browser's `i18n.language` can return regional codes, while the manifest uses base codes. If this normalization is ever removed, language-specific images will silently fall back to default (German).

### 4b. Article Content: Complete Process Documentation

> [!CAUTION]
> **Sunshine-case-only articles are UNACCEPTABLE.** Every article must cover the actions, options, and outcomes a user may encounter, including edge cases.

For each article:

1. **Cover ALL action buttons** visible in the UI, not just the primary one
2. **Explain conditional elements** (e.g., "The Notes tab appears once a session has been linked")
3. **Document different user perspectives** when relevant (coach vs client view)
4. **Explain status-dependent behavior** (e.g., buttons that change based on booking status)
5. **Add cross-links** for flows that lead to substantial secondary processes

Example of comprehensive step text (vs minimal):

```
❌ BAD (sunshine only):
"Click 'Confirm' to accept the booking."

✅ GOOD (all paths):
"From the booking detail view, you can take several actions depending on the booking status:
 • Confirm: Accept the booking request. The client will receive a confirmation notification.
 • Decline: Reject the request. You can optionally propose an alternative date.
 • Reschedule: Suggest a new date and time. See also: 'Reschedule a Booking'.
 • Cancel: Cancel a confirmed booking. Refund policies apply. See also: 'Cancellation & Refunds'."
```

### 4c. Cross-Link Implementation in HelpCenter.js

Add `relatedArticles` to the article config:

```javascript
{
  id: 'manage-bookings',
  feature: 'manage-bookings',
  category: 'sessions',
  // ... standard fields ...
  relatedArticles: [
    { articleId: 'cancel-booking', contextKey: 'articles.manageBookings.related.cancelBooking' },
    { articleId: 'reschedule-booking', contextKey: 'articles.manageBookings.related.reschedule' },
  ],
}
```

The `HelpArticle` component renders these at the bottom of the article as a "Related Guides" section.

### 4d. Article Categories

Articles are organized by category:

| Category | Icon | Features |
|---|---|---|
| `sessions` | 📅 | Availability, Bookings, Cancellations, Reschedule |
| `programs` | 📚 | Program creation, Enrollment, Progress |
| `offers` | 📨 | Offer creation, Acceptance, Payment |
| `events` | 🗓️ | Event creation, Registration, Management |
| `finance` | 💰 | Billing, Invoices, Payouts, Discounts |
| `profile` | 👤 | Profile setup, Connections |

---

## Step 5: Help Center Page

> **Goal**: Build the React page components.

### Page Structure

All components are in a **single file** `client/src/pages/HelpCenter.js`:
- `HelpCenter`: Main index with hero, search, category sidebar, article grid
- `HelpArticle`: Step by step article renderer with hero banner and white card container
- `HelpStep`: Single step (image, text, tip)

**Styling:** All Tailwind utilities inline. No separate CSS file. Uses `bg-card` containers for content contrast.

### Key Design Requirements

1. **Tailwind only**: No separate CSS files, all styling co-located in JSX
2. **Dark mode**: All elements use `dark:` variants
3. **Responsive**: Mobile-first, sidebar collapses to horizontal pills
4. **i18n**: All text via `useTranslation('help')`, namespace `help`
5. **White card containers**: Article content wrapped in `bg-card` with `border`, `shadow-sm`, `rounded-2xl` for clear contrast against the background
6. **Hero banner**: Teal gradient hero at the top of article pages with breadcrumbs and metadata
7. **Search**: Client-side across article + step text
8. **Breadcrumbs**: Help → Category → Article
9. **Related Guides section**: Rendered at the bottom of each article if `relatedArticles` is defined

### Route Setup

Add to `App.js`:
```jsx
<Route path="/help" element={<HelpCenter />} />
<Route path="/help/:category/:articleSlug" element={<HelpArticle />} />
```

---

## Step 6: AI In-Browser Validation & Review

> [!IMPORTANT]
> **AI AGENT BROWSER VALIDATION (MANDATORY)**
> After the German version is completed, you (**the AI agent**) MUST open the browser using your browser subagent and navigate to `http://localhost:3000/help/{category}/{slug}`. 
> 
> You must validate **every single point** on the actual rendered page. Check if the screenshot exactly displays what the text communicates. If there is ANY mismatch between the text description and the UI shown in the screenshot, you must **adjust it** (either fix the text or re-capture the screenshot) BEFORE presenting it to the user. Do not rely on just reading the markdown/JSON files—you must view the final assembled page in the browser!

### 6a. Quality Checklist

For each screenshot:
- [ ] Cropped to the relevant UI element (no wasted whitespace)
- [ ] No FeedbackWidget visible
- [ ] Highlight ring clearly marks the target element
- [ ] Text is sharp and readable
- [ ] Dialog content is fully visible (not cut off)
- [ ] Screenshot shows the CORRECT language (verify by reading button labels, titles)
- [ ] No loading spinners, cookie banners, or toast notifications visible

For each article:
- [ ] Step titles match the actual UI labels
- [ ] Step descriptions explain the *purpose* of each action
- [ ] ALL action buttons and options are documented, not just the primary action
- [ ] Edge cases and conditional UI are explained
- [ ] Cross-links to related articles are present where flows branch significantly
- [ ] Tips provide genuinely helpful context
- [ ] Images load correctly from Cloudinary CDN
- [ ] Article flows logically from start to finish
- [ ] All 4 language files have complete, non-placeholder translations
- [ ] Every switch, toggle, and option is explained in detail

### 6b. Process Coverage Checklist

For each article, verify coverage of:
- [ ] Primary (happy) path documented end-to-end
- [ ] All secondary paths documented or cross-linked
- [ ] Different user roles/perspectives covered (coach vs client)
- [ ] Status-dependent behavior explained (e.g., buttons change based on booking status)
- [ ] Conditional UI elements explained (when they appear, when they don't)
- [ ] Empty/error states mentioned where relevant
- [ ] Cross-links to related articles added for any branching flow with 5+ steps

### 6c. Screenshot Recapture

If the UI changes, rerun the capture script:
```bash
npx playwright test tests/help-screenshots/help-{feature}-multilang.spec.js --project=help-screenshots --retries=1
```
New screenshots automatically overwrite the old ones. Then reupload to Cloudinary (Step 7).

---

## Step 7: Upload to Cloudinary

> **Goal**: Upload all approved screenshots to Cloudinary CDN and generate the image manifest.

### 7a. Upload All Screenshots (All Languages)

After the user has reviewed and approved the screenshots:

```bash
node e2e/scripts/upload-help-screenshots.js --feature {feature-name}
```

This uploads:
- All PNGs from `client/public/help/{feature}/` (default set)
- All PNGs from `client/public/help/{feature}/de/` (German)
- All PNGs from `client/public/help/{feature}/en/` (English)
- All PNGs from `client/public/help/{feature}/fr/` (French)
- All PNGs from `client/public/help/{feature}/it/` (Italian)

And writes `client/src/data/help-images.json` with all CDN URLs organized by language.

### 7b. Expected Manifest Structure

After upload, the manifest should contain entries for ALL steps in ALL languages:

```json
{
  "coach-availability": {
    "default": {
      "step-1-availability-button": "https://res.cloudinary.com/...",
      "step-2-dialog-overview": "https://res.cloudinary.com/...",
      "step-3-date-selection": "https://res.cloudinary.com/...",
      "...all steps..."
    },
    "de": { "step-1-availability-button": "...", "...all steps..." },
    "en": { "step-1-availability-button": "...", "...all steps..." },
    "fr": { "step-1-availability-button": "...", "...all steps..." },
    "it": { "step-1-availability-button": "...", "...all steps..." }
  }
}
```

> [!CAUTION]
> **Every language section must have the SAME number of entries as default.** If any step is missing from a language, the user will see German fallback images for that step, creating a mixed-language experience. This is unacceptable.

### 7c. Verify CDN Images

After upload:
- [ ] `help-images.json` exists with entries for all steps in all languages
- [ ] Open the help article in the browser
- [ ] Switch to each language (DE, EN, FR, IT) and verify ALL screenshots change
- [ ] No mixed-language screenshots (e.g., German calendar in an English article)
- [ ] Fallback works: unknown languages fall back to `default`

---

## Learnings and Critical Notes

> [!WARNING]
> **These are hard-won lessons. Read and follow them strictly.**

### Why Playwright Specs Alone Fail for Wizard Dialogs

> [!CAUTION]
> **The #1 breakthrough in this workflow:** Playwright specs that run separate `test()` blocks per step CANNOT capture wizard dialogs correctly. Each `test()` starts with a fresh page context, so the wizard resets to step 1. This means steps 3-12 all produce identical screenshots (the wizard's initial view).

**The problem:** Standard Playwright approach writes one `test()` per step:
```javascript
// ❌ BAD: Wizard resets between tests — all screenshots look the same
test('Step 3: client selection', async ({ page }) => { /* ... */ });
test('Step 4: offer title', async ({ page }) => { /* ... */ });
test('Step 5: service chips', async ({ page }) => { /* ... */ });
// All three screenshots show the wizard's step 1 because the dialog is gone
```

**The solution:** Use a SINGLE `test()` block that walks through the entire wizard sequentially, capturing each screenshot at the right moment:
```javascript
// ✅ GOOD: Single test preserves wizard state across all steps
test('All steps', async ({ page }) => {
  test.setTimeout(180_000); // 3 minutes for 11 steps
  await login(page, 'coach');
  await navigateToOffersDashboard(page);
  
  // Step 1: Dashboard overview
  await screenshotDialog(page, 'step-1-dashboard');
  
  // Step 2: Click create button
  await addHighlight(page, 'button:has-text("Offerte erstellen")');
  await screenshotDialog(page, 'step-2-create-button');
  await removeHighlight(page, 'button:has-text("Offerte erstellen")');
  await page.click('button:has-text("Offerte erstellen")');
  await page.waitForTimeout(2000);
  
  // Step 3: Client selection (wizard is still open!)
  await addHighlight(page, '.client-selector');
  await screenshotDialog(page, 'step-3-client-select');
  // ... fill fields, click Next, continue (wizard stays open)
  
  // Step 4-11: Continue in the same test...
});
```

### Browser Subagent as Primary Capture Method

When Playwright specs fail or produce incorrect results, the **browser subagent** is the reliable fallback:

1. **Login** using the `/test` workflow's login approach
2. **Navigate** to the target state step by step
3. **Fill real data** into forms (not placeholder text)
4. **Inject CSS highlights** via `execute_browser_javascript`
5. **Capture screenshots** at each moment
6. **Save to** `client/public/help/{feature}/de/{step-label}.png`

**When to use browser subagent vs Playwright spec:**

| Scenario | Use |
|---|---|
| Simple page-level captures (dashboard, list views) | Playwright spec |
| Multi-step wizard dialogs (create offer, create program) | Playwright spec with single-test approach |
| Debugging/fixing individual screenshots that failed | Browser subagent |
| Quick proof-of-concept for a new article | Browser subagent |
| Production multi-language captures | Playwright spec (all 4 languages) |

### Dialog Keep-Alive: Never Close the Dialog Between Steps

> [!IMPORTANT]
> **The wizard dialog must stay open for the ENTIRE duration of screenshot capture.** If you close it (click X, click Cancel, navigate away), you lose all wizard state and must start over.

Critical patterns for keeping the dialog alive:

1. **Never click the close (X) button** between captures
2. **Never navigate away** from the current URL (no `page.goto()` after the dialog is open)
3. **Use "Next"/"Weiter" to advance** wizard steps, not "Back" (going back can reset form state)
4. **Do NOT submit the form** (clicking "Send" or "Save as Draft" closes the dialog). Capture the send button screenshot BEFORE clicking it
5. **Handle modal overlays carefully**: If clicking a button opens a sub-modal (e.g., confirmation dialog), capture it, then close the sub-modal (not the parent wizard)

### Dialog Scroll Management

Wizard dialogs often have content that extends below the visible area. The dialog has its own scrollable container, separate from the page scroll:

```javascript
// ❌ WRONG: Page scroll does nothing for dialog content
await page.evaluate(() => window.scrollTo(0, 500));

// ✅ CORRECT: Scroll the dialog's inner overflow container
const dialog = page.locator('[role="dialog"]').first();
const scrollContainer = dialog.locator('[class*="overflow-y"]').first();
await scrollContainer.evaluate(el => { el.scrollTop = el.scrollHeight; });
await page.waitForTimeout(500); // Wait for scroll to settle

// ✅ ALSO CORRECT: Scroll a specific element into view within the dialog
const targetSection = dialog.locator('.agb-section');
await targetSection.evaluate(el => el.scrollIntoView({ behavior: 'instant', block: 'center' }));
```

**Common scroll scenarios:**
- Review step (Step 3 in offers) has AGB section below the fold
- Price summary boxes may need scrolling to show both client and coach views
- Line item lists can grow beyond the visible area

### Login Pattern for Playwright Help Specs

Use the shared `login()` helper from `e2e/helpers/auth.js`, same as the e2e test specs:

```javascript
const { login } = require('../../helpers/auth');

// Login as coach (most help articles are from coach perspective)
await login(page, 'coach');

// Login as client (for client-facing articles like accepting an offer)
await login(page, 'client');
```

This helper handles the full login flow including waiting for the dashboard to load. See `@[.agent/workflows/test.md]` for the complete login pattern and available test accounts.

> [!TIP]
> **Read the test workflow** (`.agent/workflows/test.md`) for login credentials, navigation helpers, and data setup patterns. Many help screenshot scripts reuse the same setup logic.

### Direct URL Navigation (Skip UI Clicks)

Navigate directly to the target page via URL with query parameters instead of clicking through menus. This saves time and avoids flaky menu interactions:

```javascript
// ✅ GOOD: Direct navigation to the offers dashboard tab
await page.goto('/dashboard?tab=offers');
await page.waitForLoadState('domcontentloaded');
await page.waitForTimeout(3000);

// ❌ BAD: Click Dashboard → Click Offers tab → Wait for tab switch
await page.click('text=Dashboard');
await page.click('[data-value="offers"]');
```

This works for any dashboard tab. Check `App.js` routes and the dashboard component for supported query params.

### Force-Click for Obscured Elements

When a button is partially covered by another element (e.g., a tooltip, overlay, or the dialog backdrop), use `{ force: true }`:

```javascript
// Normal click may fail if the element is partially obscured
await createBtn.click({ force: true });
```

Use `force: true` sparingly — only when you know the element exists and is the right target but Playwright refuses to click because something overlaps it. Common cases: floating action buttons near dialog edges, buttons behind tooltip overlays.

### Loading Spinner and Data Wait Strategy

The most common screenshot issue is capturing a loading state instead of actual content. Follow this pattern:

```javascript
// 1. Dismiss cookie consent banner FIRST
try {
  const cookieBtn = page.locator(
    'button:has-text("Alle akzeptieren"), button:has-text("Accept all")'
  ).first();
  if (await cookieBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
    await cookieBtn.click();
    await page.waitForTimeout(500);
  }
} catch { /* no cookie banner */ }

// 2. Wait for spinner to APPEAR (confirms loading started)
try {
  await page.waitForSelector('.animate-spin', { state: 'visible', timeout: 10000 });
  // 3. Wait for spinner to DISAPPEAR (data loaded)
  await page.waitForSelector('.animate-spin', { state: 'hidden', timeout: 60000 });
} catch {
  // Spinner never appeared (cached data) — continue
  await page.waitForTimeout(3000);
}

// 4. Wait for ACTUAL CONTENT to render (not just the container)
try {
  await page.waitForSelector('.rbc-event', { state: 'visible', timeout: 10000 });
} catch { /* may have no events */ }

// 5. Settle time for animations
await page.waitForTimeout(3000);

// 6. Hide UI noise
await hideHelpScreenshotNoise(page);

// 7. CSS-hide cookie banners (in case they reappeared)
await page.addStyleTag({
  content: `
    [class*="cookie"], [class*="consent"], [id*="cookie"], [id*="consent"],
    .cc-window, .cc-banner { display: none !important; }
  `
});
```

> [!CAUTION]
> **Waiting for the container is NOT enough.** `.rbc-calendar` appearing does NOT mean the events have loaded. You MUST wait for child elements like `.rbc-event` that contain the actual data. The spinner disappearing only means the API call returned, not that React has finished rendering the data.

### Screenshot Path Strategy

**ALWAYS use absolute paths** via the `ensureOutputDir` helper or `path.resolve`:

```javascript
const path = require('path');
const { ensureOutputDir } = require('../../helpers/help-screenshot-helpers');

function screenshotPath(lang, filename) {
  const dir = ensureOutputDir(FEATURE, lang);
  return path.join(dir, filename);
}

// Usage:
await page.screenshot({ path: screenshotPath(lang, 'step-1-calendar.png') });
```

Relative paths silently resolve to the wrong directory when Playwright runs specs from a different working directory.

### Language Code Normalization
- `i18n.language` can return regional codes like `en-US`, `de-CH`, `fr-FR`, `it-CH`
- The manifest uses base codes: `en`, `de`, `fr`, `it`
- `getHelpImage()` normalizes with `lang.split('-')[0]`
- If you add language features elsewhere, always normalize the code first

### Setting Language in Playwright
- Use `page.addInitScript()` to set `localStorage.setItem('i18nextLng', lang)` BEFORE login
- This MUST happen before `page.goto()` because `i18next-browser-languagedetector` reads it on init
- `page.evaluate()` is too late (React has already rendered)
- URL params (`?lng=en`) are NOT supported

### Multi-Language Selectors
- Always check ALL 4 translation files before writing selectors
- Partial text matches work well for navigation (e.g., `has-text("sessions")`)
- ID selectors (`#availableForInstantBooking`) are language-independent and preferred
- Dialog/role selectors (`[role="dialog"]`) work universally

> [!WARNING]
> **Never assume translations — always read the JSON files.** Common locale pitfalls from real sessions:
> - EN often uses **lowercase** where you'd expect Title Case: `"Show list"` not `"Show List"`, `"Show calendar"` not `"Show Calendar"`
> - IT frequently uses different verbs than expected: `"Visualizza"` not `"Mostra"` for "show", `"Cancella"` not `"Annulla"` for "cancel"
> - FR uses different phrasing: `"Proposer une heure"` not `"Proposer un horaire"`
> - Always grep all 4 locale files for the exact key before adding to `LANG_CONFIG`

### Conditional Tab/Section Rendering
- Many modal tabs are conditionally rendered based on data state (e.g., Notes tab requires `canViewNotes`)
- Before writing the spec, read the component source to understand ALL conditions
- Ensure your test data creates the right state for each tab to be visible
- If a tab/section is truly optional, document in the article text WHEN it appears

### Tab Navigation in Booking Detail Modals
- Try `data-value` attribute first: `[role="dialog"] [role="tab"][data-value="basicInfo"]`
- Fallback to text matching with regex: iterate over `[role="tab"]` elements and check `textContent()`
- Known tab values: `overview`, `basicInfo`, `notes`, `recap`, `reschedule`
- The Recap tab only appears for completed sessions with recordings
- The Reschedule tab only appears for confirmed (not past) bookings with active reschedule requests

### Dual-Dialog Cancellation Modal Capture
- When clicking "Cancel" in BookingDetailsModal, a CancellationModal opens ON TOP of it
- This creates TWO `[role="dialog"]` elements. Always capture the LAST (topmost) dialog: `page.locator('[role="dialog"]').last()`
- Wait 2-3 seconds after clicking Cancel for the API call to return financial data
- After capture, close the cancellation modal by clicking "Keep Booking" / "Buchung beibehalten" to avoid state corruption

> [!TIP]
> **Use deep links instead of list-item clicks to open booking detail modals.** List-item clicks are unreliable in Playwright due to scroll positioning, stale element references, and CRA hot-reload context destruction. The deep link approach is deterministic:
> ```javascript
> // 1. Extract auth token from localStorage (quick page.evaluate)
> const token = await page.evaluate(() => localStorage.getItem('token'));
>
> // 2. Decode JWT in Node context (NOT page context — immune to HMR)
> const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
> const userId = payload.user?.id || payload.id; // JWT nests under { user: { id } }
>
> // 3. Fetch bookings via Playwright request API (Node context)
> const res = await page.request.get(`http://localhost:3000/api/bookings/${userId}/bookings`, {
>   headers: { 'Authorization': `Bearer ${token}` }
> });
> const data = await res.json();
> const bookingId = data.regularBookings?.find(
>   b => b.status === 'confirmed' && !b.isAvailability
> )?._id;
>
> // 4. Navigate with deep link — modal auto-opens
> await page.goto(`/manage-sessions/${userId}?bookingId=${bookingId}`);
> await page.locator('[role="dialog"]').first()
>   .waitFor({ state: 'visible', timeout: 30_000 });
> ```
> **Key: Steps 1-3 happen in Node context**, completely immune to CRA hot-reloads that destroy the page's execution context.

### relatedArticles Rendering
- `relatedArticles` is defined in the `HelpCenter.js` article config but **no frontend rendering code exists yet**
- The data model is ready (config + help.json context keys) but the `HelpArticle` component does not render the section
- When implementing: add a "Related Guides" / "Ähnliche Anleitungen" section at the bottom of each article
- Each related article should show: article title, context text (from `contextKey`), and a link to the article

### Cookie Consent Banner
- Cookie banners can overlay screenshots even after dismissal (they may reappear on navigation)
- Always dismiss via click AND inject CSS to hide by class/ID
- This is especially important for Step 1 screenshots where the user lands fresh on the page

### Complete Coverage is Mandatory
- Every step in the original spec must also exist in the multi-language spec
- If the original has 9 steps, the multi-language spec must have 9 × 4 = 36 tests
- Partial coverage creates a broken mixed-language experience
- Always verify that each language folder has the same file count as the default folder

### Cloudinary Note
- The platform uses a single Cloudinary cloud across dev, staging, and prod
- Uploads from dev are immediately accessible in prod via the same CDN URLs
- No need to re-upload when deploying

### Wizard Dialog Capture (CRA Apps)

> [!CAUTION]
> **CRA's Hot Module Replacement (HMR) WebSocket causes frame navigations that destroy Playwright's execution context mid-test.** This is the #1 cause of "Execution context was destroyed" errors in wizard dialog captures.

**Fix: Block HMR WebSocket connections before navigating:**

```javascript
// 1. Abort WebSocket upgrade requests at the route level
await page.route(/\/(ws|sockjs-node)/, route => route.abort());

// 2. Override window.WebSocket to prevent CRA dev client connections
await page.addInitScript(() => {
  const Orig = window.WebSocket;
  window.WebSocket = function(url, ...args) {
    if (/sockjs-node|\/ws/.test(url)) {
      return { close(){}, send(){}, addEventListener(){}, removeEventListener(){} };
    }
    return new Orig(url, ...args);
  };
});
```

Both steps are required. Route interception alone misses WebSocket upgrade requests; `addInitScript` alone misses connections initiated before the script runs.

### Dialog Screenshot Method

> [!IMPORTANT]
> **Always use `page.screenshot({ clip })` with the dialog's bounding box, NOT `dialog.screenshot()`.** When HMR causes context destruction, `dialog.screenshot()` throws because the locator's execution context is already gone. Capturing via `page.screenshot({ clip })` is resilient because the page-level context survives.

```javascript
async function screenshotDialog(page, label, lang) {
  const dialog = page.locator('[role="dialog"]').first();
  await dialog.waitFor({ state: 'visible', timeout: 15_000 });
  const box = await dialog.boundingBox();
  if (!box) return;
  await page.screenshot({
    path: path.join(dir, `${label}.png`),
    clip: { x: Math.floor(box.x), y: Math.floor(box.y), width: Math.ceil(box.width), height: Math.ceil(box.height) },
    animations: 'disabled',
  });
}
```

### Selector Pitfalls in Wizard Forms

| Pitfall | Example | Fix |
|---|---|---|
| **Wrong parent class** | `.space-y-3 input` when actual class is `.space-y-2.5` | Inspect source code for exact class name; use broader selector like `dialog.locator('input[type="text"]')` |
| **First input matches search box** | `dialog.locator('input[type="text"]').first()` grabs client search, not title | Target inputs inside a specific section: `.border-t input.first()` |
| **Finding empty inputs** | Need to fill the LAST empty text input (e.g., custom line item title) | Iterate all `input[type="text"]` backwards, check `inputValue()`, fill first empty one |
| **Escaped selectors** | `.border-t.border-border\\/40` | Playwright requires double-escaping in CSS selectors |

### Wizard Navigation (Next/Weiter Button)

```javascript
// Find the Next button by localized text (works across DE/EN/FR/IT)
const nextBtn = dialog.locator('button')
  .filter({ hasText: /Weiter|Next|Suivant|Avanti/ })
  .first();

// Always check visibility AND disabled state before clicking
if (await nextBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
  const isDisabled = await nextBtn.isDisabled();
  if (!isDisabled) {
    await nextBtn.click();
    await page.waitForTimeout(2000); // Wait for step transition animation
  } else {
    console.log('Next button disabled: form validation not met');
  }
}
```

> [!WARNING]
> If the Next button stays disabled, it means a required field is empty or invalid. Check the component source for `isStepXValid` logic. Common causes: empty line item title, missing client selection, or `unitPrice.amount === 0`.

### Red Highlight Rings on Screenshots

> [!IMPORTANT]
> **Every help screenshot that shows a clickable element, input field, or important UI section MUST have a red highlight ring (`#ef4444`) injected via CSS before capture.** This makes it immediately clear to the user what the step is referring to.

**Inline injection pattern (for wizard dialogs where you control state):**

```javascript
// Add red highlight ring to a target element
async function addHighlight(page, selector) {
  const el = page.locator(selector).first();
  if (await el.isVisible().catch(() => false)) {
    await el.evaluate(el => {
      el.style.setProperty('box-shadow', '0 0 0 3px #ef4444, 0 0 16px #ef444466', 'important');
      el.style.setProperty('border-radius', '6px', 'important');
      el.style.setProperty('position', 'relative', 'important');
      el.style.setProperty('z-index', '999', 'important');
    });
  }
}

// Remove highlight after screenshot
async function removeHighlight(page, selector) {
  const el = page.locator(selector).first();
  await el.evaluate(el => {
    el.style.removeProperty('box-shadow');
    el.style.removeProperty('border-radius');
    el.style.removeProperty('position');
    el.style.removeProperty('z-index');
  }).catch(() => {});
}
```

Or use the helper function `captureElementWithHighlight` from `help-screenshot-helpers.js` (default color is `#ef4444`).

### Blue Focus Ring Prevention

> [!CAUTION]
> **The browser's blue focus outline appears on the controlled window and on focused form inputs.** This is NOT a CSS focus ring; it's the OS-level window focus indicator. To prevent it from appearing in screenshots:

```javascript
// Always blur the active element before taking a screenshot
await page.evaluate(() => {
  /** @type {HTMLElement} */ (document.activeElement)?.blur();
});
await page.waitForTimeout(200);
```

This must be called after ANY interaction (click, fill, type) and before the screenshot.

### Sequential Wizard Capture

For multi-step wizard flows (like Create Offer with 3 wizard steps containing 11+ screenshot steps):

1. **Run ALL steps in a single test**, not separate tests. This preserves wizard state.
2. **Use generous timeouts** (3 min total for 12 steps): `test.setTimeout(180_000);`
3. **Wait for animations** after each step transition: `await page.waitForTimeout(2000);`
4. **Scroll the dialog's inner scrollable container**, not the page:
   ```javascript
   const scroll = dialog.locator('[class*="overflow-y"]').first();
   await scroll.evaluate(el => { el.scrollTop = el.scrollHeight; });
   ```
5. **Log each step** for debugging: `console.log(\`[${lang}] ✓ step N: label\`);`

### Graceful Fallback Chains (Never Hard-Fail)

> [!IMPORTANT]
> **Every interaction in a wizard capture spec MUST be wrapped in `.catch(() => false)` guards.** A single hard failure aborts the entire 11-step capture, wasting minutes of setup time. Use fallback chains instead:

```javascript
// ✅ GOOD: Graceful fallback with logging
const chipContainer = dialog.locator('.flex.flex-wrap.gap-1\\.5').first();
if (await chipContainer.isVisible().catch(() => false)) {
  await highlightLocator(chipContainer);
  console.log(`  [${lang}]   ✓ highlighted service chips`);
} else {
  console.log(`  [${lang}]   ⚠ service chip container not found, trying fallback`);
  // Fallback: try a different selector
  const chipSection = dialog.locator('text=AUS DEINEN DIENSTLEISTUNGEN >> xpath=..').first();
  if (await chipSection.isVisible().catch(() => false)) {
    await highlightLocator(chipSection);
  }
}

// ❌ BAD: Hard failure aborts the entire run
const chipContainer = dialog.locator('.flex.flex-wrap.gap-1\\.5').first();
await chipContainer.waitFor({ state: 'visible' }); // Throws on timeout!
```

### Text-Based Element Identification

When selectors are ambiguous (e.g., multiple `.rounded-xl.border` cards), identify elements by scanning their text content:

```javascript
// Find the coach billing card among multiple similar-looking cards
const allCards = dialog.locator('.rounded-xl.border');
const cardCount = await allCards.count();
let targetCard = null;
for (let i = 0; i < cardCount; i++) {
  const text = await allCards.nth(i).textContent().catch(() => '');
  if (text && (text.includes('ABRECHNUNG') || text.includes('Payout'))) {
    targetCard = allCards.nth(i);
    break;
  }
}
// Fallback to positional index if text search fails
if (!targetCard && cardCount >= 3) {
  targetCard = allCards.nth(2);
}
```

This is more resilient than relying on CSS class combinations, which change often.

### Reverse-Iteration for Empty Inputs

When a wizard dynamically adds input fields (e.g., custom line items), the new empty input is the LAST one. Scan backwards to find it:

```javascript
const allTextInputs = dialog.locator('input[type="text"]');
const count = await allTextInputs.count();
for (let i = count - 1; i >= 0; i--) {
  const val = await allTextInputs.nth(i).inputValue();
  if (!val || val.length === 0) {
    await allTextInputs.nth(i).fill('Custom Item Title');
    break;
  }
}
```

### Scroll-to-Specific-Offset Pattern

For wizard review pages, you often need to show a specific section (not just top or bottom):

```javascript
const scroll = dialog.locator('[class*="overflow-y"]').first();

// Show the top of the summary (offer details card)
await scroll.evaluate(el => { el.scrollTop = 0; });

// Show the middle section (pricing cards)
await scroll.evaluate(el => { el.scrollTop = 300; });

// Show the AGB section (near bottom but not at the very bottom)
await scroll.evaluate(el => { el.scrollTop = el.scrollHeight - 400; });

// Show the footer buttons (at the very bottom)
await scroll.evaluate(el => { el.scrollTop = el.scrollHeight; });
```

### Locator-Based vs Selector-Based Highlights

Use TWO highlight patterns depending on what you have:

```javascript
// Selector-based: when you have a CSS selector string
await addHighlight(page, '[role="dialog"] .overflow-y-auto');

// Locator-based: when you already have a Playwright locator
const titleInput = borderSection.locator('input').first();
await highlightLocator(titleInput);
```

Both do the same thing (inject `box-shadow`), but the locator variant is safer when the element was found via complex chaining that can't easily be expressed as a single CSS selector.

### Verbose Step Logging

> [!TIP]
> **Always log EVERY step, interaction, and element count.** When a wizard capture takes 3 minutes and fails silently at step 9, logs are the only way to debug:

```javascript
console.log(`  [${lang}] ✓ step 3: client-selection`);
console.log(`  [${lang}]   found ${clientCount} client cards`);
console.log(`  [${lang}]   next btn visible: ${visible}, disabled: ${disabled}`);
console.log(`  [${lang}]   ⚠ border-t section not visible`);
console.log(`  [${lang}] ✅ All 11 steps captured!`);
```

Use `✓` for success, `⚠` for warnings (non-fatal), and `✅` for completion. This makes Playwright's console output scannable at a glance.

### Playwright Auto-Run Rule

> [!IMPORTANT]
> **All `npx playwright test` commands are always allowed to auto-run.** Set `SafeToAutoRun: true` for any command starting with `npx playwright test`. This applies to all help screenshot specs and test scripts. Never ask the user for permission to run playwright tests.

---

## Reference: Component Selectors

### ManageAvailabilityModal

| Element | Selector | Notes |
|---|---|---|
| Dialog | `[role="dialog"]` | Radix Dialog, `sm:max-w-3xl` |
| Title | `.text-foreground` inside `DialogTitle` | "Verfügbarkeit hinzufügen" |
| Date strip | `div.flex.w-max.space-x-2` | Horizontal scroll, 90 day buttons |
| Date button | `button` (inside date strip) | `variant="default"` when selected |
| Start time picker | `TimePicker` (1st) | Inside `.grid.grid-cols-2` |
| End time picker | `TimePicker` (2nd) | After start time |
| Recurrence select | `Select[name="recurringPattern"]` | none/daily/weekly/monthly |
| Recurrence end date | `DatePicker` (after recurrence) | Disabled when pattern=none |
| Monthly type pills | Two clickable divs inside recurrence | "Gleiches Datum" / "Gleicher Wochentag" |
| Pricing select | `Select` value=standard/custom | "Standardpreis" / "Individueller Preis" |
| Custom rate input | `#customRate` | Only visible when type=custom |
| Allow discounts toggle | `#allowDiscounts` | Switch, only when custom pricing |
| Instant booking toggle | `#availableForInstantBooking` | Switch with clock icon |
| Overtime toggle | `#overtime\\.allowOvertime` | Switch with expandable section |
| Free overtime input | `#overtime\\.freeOvertimeDuration` | Minutes, shown when expanded |
| Paid overtime input | `#overtime\\.paidOvertimeDuration` | Minutes |
| Overtime rate input | `#overtime\\.overtimeRate` | Rate value |
| Cancel button | Button "Abbrechen" | `variant="outline"` |
| Delete button | Button "Löschen" | Only in edit mode |
| Save button | Button "Speichern" | `type="submit"` |

### ManageSessions

| Element | Selector | Notes |
|---|---|---|
| Green availability button | `button[variant="action-green"]:has-text("Verfügbarkeit")` | Desktop only |
| Add session button | `button:has-text("Sitzung hinzufügen")` | Primary button |
| Calendar container | `.rbc-calendar` | react-big-calendar |
| Calendar events | `.rbc-event` | Wait for these to confirm data loaded |
| List toggle | Button with List/Calendar icon | Toggles view |

### BookingDetailsModal

| Element | Selector | Notes |
|---|---|---|
| Dialog | `[role="dialog"]` | Full-screen modal |
| Tab: Overview | `button[value="overview"]` | Default active tab |
| Tab: Basis Info | `button[value="basis"]` | Editable fields |
| Tab: Advanced | `button[value="advanced"]` | Extended options |
| Tab: Notes | `button[value="notes"]` | Conditional: requires `canViewNotes` |
| Tab: Reschedule | `button[value="reschedule"]` | Conditional: confirmed bookings only |
| Action buttons | `.booking-actions, [class*="BookingActions"]` | Status-dependent |
| Status badge | `.badge, [class*="status"]` | Shows booking state |

---

## Reference: Hiding FeedbackWidget

The FeedbackWidget renders only when `REACT_APP_SHOW_FEEDBACK_WIDGET=true`. For screenshots, we hide it via CSS injection rather than env vars to avoid needing a separate build:

```javascript
// Inject CSS to hide the feedback widget
await page.addStyleTag({
  content: `
    /* FeedbackWidget: floating button at bottom-right, z-[60] */
    button.fixed.rounded-full.shadow-\\[0_8px_32px_rgba\\(0\\,0\\,0\\,0\\.2\\)\\].z-\\[60\\] {
      display: none !important;
    }
    /* Also target by the bug icon inside */
    button.fixed:has(svg.lucide-bug) {
      display: none !important;
    }
  `
});
```

---

## Reference: Element-Level Screenshot Approach

### Why element capture vs full page

| Full page screenshot | Element level capture |
|---|---|
| Shows empty sidebar, header, footer | Shows ONLY the dialog/control |
| Small dialog lost in large viewport | Dialog fills the image |
| Inconsistent framing | Consistent, predictable framing |
| Hard to see details | Details are clear at any zoom |

### Implementation: Bounding Box Crop

```javascript
async function captureElement(page, { selector, label, feature, lang, padding = 24 }) {
  const element = page.locator(selector).first();
  await element.waitFor({ state: 'visible', timeout: 10_000 });

  const box = await element.boundingBox();
  if (!box) throw new Error(`Element not found: ${selector}`);

  const clip = {
    x: Math.max(0, box.x - padding),
    y: Math.max(0, box.y - padding),
    width: box.width + padding * 2,
    height: box.height + padding * 2,
  };

  // lang param creates language subdirectory: {feature}/{lang}/{label}.png
  const dir = lang
    ? path.join(__dirname, '../../client/public/help', feature, lang)
    : path.join(__dirname, '../../client/public/help', feature);
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });

  await page.screenshot({
    path: path.join(dir, `${label}.png`),
    clip,
  });
}
```

---

## Workflow Summary: End-to-End Sequence

```
═══════════════════════════════════════════════════════════════
  PHASE 1: ANALYSIS (before writing any code)
═══════════════════════════════════════════════════════════════

Step 0: COMPONENT ANALYSIS & PROCESS FLOW MAPPING
  ├── Read ALL related source files (component, model, controller, API, locales)
  ├── Create component analysis sheet (fields, wizard steps, validations)
  ├── Map all user journeys (primary + secondary paths)
  ├── Identify edge cases and conditional UI
  ├── Decide inline docs vs cross-article links
  ├── Create conditional UI map for dynamic components
  ├── Plan exact step count and image labels
  └── Verify step count matches between spec, HelpCenter.js, and help.json

═══════════════════════════════════════════════════════════════
  PHASE 2: GERMAN PROOF OF CONCEPT (iterate until approved)
═══════════════════════════════════════════════════════════════

Step 1: PLAYWRIGHT SPEC (German only)
  ├── Read component source for selectors
  ├── Read German translation file for text selectors
  ├── Write single-language (DE) spec with all steps
  ├── Include red highlight rings on all steps (2+)
  ├── Include blue focus ring prevention (blur before capture)
  ├── Include FeedbackWidget hiding
  └── Include cookie/spinner/overlay handling

Step 2: RUN GERMAN CAPTURES
  ├── Execute DE-only spec
  ├── Verify all steps pass (0 failures)
  └── Check that each screenshot shows correct content

Step 3: GERMAN ARTICLE DATA
  ├── Write help.json steps in German ONLY
  ├── Document ALL process flows (not just happy path)
  ├── Add cross-links to related articles
  └── Update HelpCenter.js article config

Step 4: AI IN-BROWSER VALIDATION (MANDATORY)
  ├── 🔴 IMPORTANT: Open the browser subagent to http://localhost:3000/help/{category}/{slug}
  ├── For each step on the rendered page:
  │   ├── Validate EVERY point: does the screenshot display EXACTLY what the text communicates?
  │   └── Do not rely solely on markdown/JSON files—view the final assembled page in the browser
  ├── Fix mismatches:
  │   ├── Update help.json text if screenshot is correct but text is inaccurate
  │   ├── Re-capture via browser subagent if text is correct but screenshot is wrong
  │   └── Fix both if both are wrong
  └── Log validation results per step

Step 5: USER REVIEW (German)
  ├── Present the article at http://localhost:3000/help/{category}/{slug}
  ├── User reviews screenshots and text
  ├── Iterate until ALL steps are confirmed correct
  └── DO NOT proceed to multi-language until user approves

═══════════════════════════════════════════════════════════════
  PHASE 3: MULTI-LANGUAGE EXPANSION (after German is approved)
═══════════════════════════════════════════════════════════════

Step 6: EXPAND TO EN, FR, IT
  ├── Read ALL 3 remaining translation files for text selectors
  ├── Expand spec to multi-language (all 4 languages)
  ├── Run multi-language spec
  ├── Verify all steps pass in all languages
  └── Check file count per language folder matches

Step 7: TRANSLATE ARTICLE DATA
  ├── Translate help.json steps to EN, FR, IT
  ├── Verify all keys exist in all locale files
  ├── Never leave placeholder text or untranslated content
  └── Keep step numbers and structure identical across languages

Step 8: MULTI-LANGUAGE VALIDATION
  ├── Verify each language screenshot shows correct language
  ├── No mixed-language screenshots
  ├── Text matches screenshots in all languages
  └── Cross-check step count consistency

═══════════════════════════════════════════════════════════════
  PHASE 4: FINALIZE
═══════════════════════════════════════════════════════════════

Step 9: UPLOAD TO CLOUDINARY
  ├── Upload all screenshots: node e2e/scripts/upload-help-screenshots.js --feature {name}
  ├── Verify CDN manifest (help-images.json)
  ├── Test in all languages in the browser
  └── Clean up orphaned assets: node e2e/scripts/cleanup-help-screenshots.js --feature {name}
```

---

## Cloudinary Cleanup

> **Goal**: Remove orphaned Cloudinary assets from previous screenshot iterations.

After multiple iterations of screenshot capture, old unused images accumulate on Cloudinary. Use the cleanup script:

```bash
# Dry run (shows what would be deleted, deletes nothing)
node e2e/scripts/cleanup-help-screenshots.js

# Dry run for a specific feature
node e2e/scripts/cleanup-help-screenshots.js --feature create-offer

# Actually delete orphaned assets
node e2e/scripts/cleanup-help-screenshots.js --delete

# Delete orphaned assets for a specific feature
node e2e/scripts/cleanup-help-screenshots.js --feature create-offer --delete
```

The script compares Cloudinary assets in the `help/` folder against the `help-images.json` manifest and identifies assets that exist on Cloudinary but are NOT referenced in the manifest. Always do a dry run first.

---

## Critical Lessons Learned (from Create-Offer Experience)

> [!WARNING]
> **These lessons were learned the hard way. Violating them WILL cause rework.**

### 1. Step Count Consistency is Everything

The step count must be identical across THREE sources:
- `HelpCenter.js` article config (step entries with `imageLabel`)
- `help.json` locale file (step content with `title`, `text`, `tip`)
- The Playwright spec (screenshots labeled `step-N-{label}`)

If you reorder, add, or remove steps, **ALL THREE must be updated simultaneously**. The create-offer bug where step 10 said "validity date" but the screenshot showed "AGB" happened because the step order was changed in the spec and HelpCenter.js but NOT in help.json.

### 2. Validity Date / Similar Field Placement

When a form field appears on a wizard page alongside other fields, it should be captured as part of the screenshot for that wizard page, NOT as a separate step. Example: the valid-until date in the create-offer wizard appears on wizard Step 1 alongside title and description, so it belongs in the same screenshot as those fields (merged into article step 4), not as a standalone step 10.

### 3. French Locale Has Simplified Articles

The French `help.json` has simplified versions of some articles (e.g., createOffer has only 5 steps vs 11 in DE/EN/IT). When updating step content across locales, **always check the actual step count in each language file** before making changes. Do not assume all files have the same structure.

### 4. Screenshot Content Must Match Description, Not Just Step Number

Never rely on step numbers alone. Always verify that the CONTENT of the screenshot (what UI elements are visible, what is highlighted) matches what the step TEXT describes. A step titled "AGB und Anhänge" must show the AGB section highlighted, regardless of what step number it is.

### 5. API-Driven Test Data for Status-Dependent UI

When screenshots require a specific offer/booking status (e.g., "sent" to show Withdraw/Extend buttons), **create the test data via API before navigating to the UI**. Clicking through existing offers on the dashboard is unreliable because their status may be "completed" or "paid", which hides the relevant action buttons.

**Pattern: Create a fresh sent offer via Node `fetch`:**

```javascript
const API_BASE = 'http://localhost:5000';

async function createFreshSentOffer(lang) {
  // 1. Login via API to get auth token
  const loginRes = await fetch(`${API_BASE}/api/users/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email: 'coach@example.com', password: 'password' }),
  });
  const { token } = await loginRes.json();

  // 2. Get connected client ID
  const connRes = await fetch(`${API_BASE}/api/connections/user`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  const connData = await connRes.json();
  const connections = connData.connections || connData; // API wraps in { connections: [...] }
  const accepted = connections.find(c => c.status === 'accepted');
  const clientId = typeof accepted.otherUser === 'object'
    ? (accepted.otherUser._id || accepted.otherUser.id)
    : accepted.otherUser; // otherUser, NOT client/coach!

  // 3. Create + send offer
  const offerRes = await fetch(`${API_BASE}/api/offers`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
    body: JSON.stringify({
      client: clientId,
      title: 'Test Offer',
      lineItems: [{ title: 'Service', quantity: 1, unitPrice: { amount: 100, currency: 'CHF' } }],
      validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
      sendImmediately: true,
    }),
  });
  const offer = await offerRes.json();
  return offer._id || offer.id;
}
```

> [!CAUTION]
> **Use Node's native `fetch`, NOT Playwright's `request` fixture.** Playwright's `request` inherits `baseURL` from playwright.config.js (port 3000 = frontend), and absolute URLs are still routed through it. Node `fetch` targets the backend directly (port 5000).

> [!WARNING]
> **The connections API returns `otherUser`, NOT `client` or `coach`.** The `/api/connections/user` endpoint returns `{ connections: [...] }` where each connection has `{ _id, status, otherUser, initiatedByMe }`. The `otherUser` field contains the connected party's ID or populated object.

### 6. Step Ordering: Group Actions After Hero

When documenting a detail page with multiple action buttons (Withdraw, Extend, Clone, PDF, Edit), **group all action steps immediately after the hero section**, before content sections (line items, price summary, cover letter). This mirrors the natural UI flow: the user sees the hero with buttons first, then scrolls down to content.

**Correct order (manage-offers example):**
```
Steps 1-4: Dashboard navigation (overview, filters, search, card)
Step 5:    Hero section with status + action buttons
Steps 6-9: Actions (withdraw dialog, extend dialog, clone, PDF)
Steps 10-12: Content sections (line items, price summary, cover letter/AGB)
Steps 13-14: History and notes
```

**Wrong order** (actions scattered among content):
```
Steps 6-8: Line items, price, cover letter  ← content before actions
Steps 9-12: Withdraw, extend, clone, PDF    ← user already scrolled past buttons
```

### 7. Login Endpoint is `/api/users/login`

The login API is mounted at `/api/users` (from `userRoutes.js`), not `/api/auth`. Always use `POST /api/users/login` for API-based authentication. Check `server/server.js` for route mount paths when unsure about endpoint prefixes.

### 8. Mandatory Pre-Scripting Source Verification (Manage Bookings Experience)

> [!CAUTION]
> **Never guess API routes, locale strings, or JWT structures. Always verify from source code BEFORE writing the spec.** Every failure in the manage-bookings session came from assumptions that were wrong.

**Checklist before writing ANY help screenshot spec:**

1. **API routes** — Read `client/src/services/*API.js` to find the exact endpoint URLs:
   ```javascript
   // ❌ WRONG: Guessed route
   const res = await page.request.get(`/api/bookings/coach/${userId}`);
   
   // ✅ CORRECT: Verified from bookingAPI.js line 67
   const res = await page.request.get(`/api/bookings/${userId}/bookings`);
   ```

2. **JWT payload structure** — Read `server/controllers/userController.js` to see the signing call:
   ```javascript
   // Server signs: { user: { id: user._id, role: user.role, version: user.tokenVersion } }
   
   // ❌ WRONG: Flat payload assumed
   const userId = payload.id || payload.userId;
   
   // ✅ CORRECT: Nested user object
   const userId = payload.user?.id || payload.id || payload.userId || payload.sub;
   ```

3. **Locale strings** — Read ALL 4 `client/src/locales/{de,en,fr,it}/*.json` files for exact text:
   ```javascript
   // ❌ WRONG: Guessed capitalization and word choice
   en: { listBtn: 'Show List', proposeTimeText: 'Propose Time' }
   it: { calendarBtn: 'Mostra calendario', cancelBtnText: 'Annulla prenotazione' }
   
   // ✅ CORRECT: Verified from locale JSON files
   en: { listBtn: 'Show list', proposeTimeText: 'Suggest Time' }
   it: { calendarBtn: 'Visualizza calendario', cancelBtnText: 'Cancella prenotazione' }
   ```

4. **Deep link parameters** — Read the target component to verify URL param handling:
   ```javascript
   // ManageSessions.js uses useSearchParams():
   const deepLinkBookingId = searchParams.get('bookingId');
   // → Navigate to /manage-sessions/{coachId}?bookingId={bookingId}
   ```

> [!TIP]
> **Run these grep commands before writing ANY new spec:**
> ```bash
> # 1. Find API route
> grep -rn "getCoachSessions\|bookings.*coach" client/src/services/
> # 2. Find JWT structure
> grep -n "jwt.sign" server/controllers/userController.js
> # 3. Find locale strings (replace KEY with your button/label key)
> grep -rn "showList\|cancelBooking" client/src/locales/*/bookings.json
> # 4. Find deep link handler
> grep -n "searchParams\|bookingId" client/src/components/ManageSessions.js
> ```

### 9. Single-Test Pattern for ALL Specs (Not Just Wizards)

> [!CAUTION]
> **The single `test()` pattern is mandatory for ALL help screenshot specs, not just wizard dialogs.** This was proven when rewriting the accept-offer, manage-offers, and offer-fulfillment specs. With separate tests per step, each step re-logins and re-navigates, turning a 1.2-minute run into 8+ minutes and introducing flaky navigation failures.

```javascript
// ❌ BAD: Separate tests — each re-logins, re-navigates (slow, flaky)
test('Step 1: dashboard', async ({ page }) => { await login(page, 'coach'); /* ... */ });
test('Step 2: filters', async ({ page }) => { await login(page, 'coach'); /* ... */ });
test('Step 3: search', async ({ page }) => { await login(page, 'coach'); /* ... */ });

// ✅ GOOD: Single test — login once, state carries across all steps (~1 min total)
test('All 10 steps', async ({ page }) => {
  test.setTimeout(240_000);
  await login(page, 'coach');
  // Step 1: dashboard (already here from login)
  await page.screenshot({ path: screenshotPath(lang, 'step-1-dashboard.png') });
  // Step 2: filters (no re-login needed)
  await addHighlight(filterBtn);
  await page.screenshot({ path: screenshotPath(lang, 'step-2-filters.png') });
  // Steps 3-10: continue sequentially...
});
```

This applies to dashboard views, detail pages, multi-role flows, and any spec with 3+ steps. The only exception is if steps genuinely require independent page states that conflict with each other.

### 10. Lucide Icon Selectors for Language-Independent Button Targeting

> [!TIP]
> **Use `button:has(svg.lucide-{icon-name})` selectors to target action buttons across ALL languages.** These are completely language-independent and more stable than text selectors, which break when translations differ.

| Selector | Button |
|---|---|
| `button:has(svg.lucide-check-circle)` | Accept |
| `button:has(svg.lucide-x-circle)` | Decline |
| `button:has(svg.lucide-rotate-ccw)` | Request revision |
| `button:has(svg.lucide-play)` | Start work |
| `button:has(svg.lucide-package-check)` | Mark fulfilled |
| `button:has(svg.lucide-clipboard-check)` | Confirm delivery / Complete |
| `button:has(svg.lucide-calendar-plus)` | Extend validity |
| `button:has(svg.lucide-copy)` | Duplicate |
| `button:has(svg.lucide-download)` | Download PDF |
| `button:has(svg.lucide-filter)` | Filter dropdown |
| `button:has(svg.lucide-bell)` | Notification bell |
| `button:has(svg.lucide-send)` | Send |
| `button:has(svg.lucide-trash-2)` | Delete |
| `button:has(svg.lucide-edit)` | Edit |
| `button:has(svg.lucide-undo)` | Withdraw |
| `button:has(svg.lucide-plus)` | Create / Add |

**When to use text selectors instead:** Only when two buttons share the same icon but have different text labels, or when targeting non-button elements like tabs or links.

### 11. Multi-Role Capture with `browser.newContext()`

> [!IMPORTANT]
> **When a spec needs both coach and client perspectives (e.g., offer-fulfillment), use `browser.newContext()` to create a second browser context instead of logging out and back in.** This preserves the first context's state, allowing you to return to it for later steps.

```javascript
test('All 7 steps', async ({ page, browser }) => {
  // Steps 1-4: Coach perspective
  await login(page, 'coach');
  // ... capture coach steps ...

  // Step 5: Switch to client perspective (new context)
  const clientContext = await browser.newContext({
    viewport: { width: 1440, height: 900 },
    deviceScaleFactor: 2,
  });
  const clientPage = await clientContext.newPage();
  await clientPage.addInitScript((lng) => {
    localStorage.setItem('i18nextLng', lng);
  }, lang);
  await blockHmr(clientPage);
  await login(clientPage, 'client');
  // ... capture client step ...
  await clientContext.close();

  // Steps 6-7: Back to coach context (page is still alive!)
  await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
  // ... capture remaining coach steps ...
});
```

> [!WARNING]
> **You must re-apply `blockHmr()`, `setLanguage()`, and `hideHelpScreenshotNoise()` on the new context's page.** These are per-page settings and do not carry over from the original context.

### 12. Highlight Color Standardization: Red Only

> [!WARNING]
> **ALL highlight rings MUST use `#ef4444` (red), regardless of the button's semantic meaning.** Do NOT use green (`#10b981`) for "accept" buttons or blue (`#3b82f6`) for "start" buttons. The highlight ring's purpose is to POINT AT the element, not to convey its action. Mixed colors create visual inconsistency across articles.

```javascript
// ❌ BAD: Color matches button semantics (inconsistent across articles)
captureElementWithHighlight(page, { target: acceptBtn, highlightColor: '#10b981' }); // green
captureElementWithHighlight(page, { target: startBtn,  highlightColor: '#3b82f6' }); // blue
captureElementWithHighlight(page, { target: deleteBtn, highlightColor: '#ef4444' }); // red

// ✅ GOOD: Always red, regardless of button meaning
captureElementWithHighlight(page, { target: acceptBtn, highlightColor: '#ef4444' }); // red
captureElementWithHighlight(page, { target: startBtn,  highlightColor: '#ef4444' }); // red
captureElementWithHighlight(page, { target: deleteBtn, highlightColor: '#ef4444' }); // red
```

The `captureElementWithHighlight` helper defaults to `#ef4444`, so omitting `highlightColor` is the correct approach. Never override it.

### 13. XPath Ancestor Traversal for Inline Status Banners

> [!TIP]
> **Status banners (paid, in_progress, fulfilled, completed) are inline within the hero section, not separate components.** To find and highlight them, locate a text node inside the banner, then traverse UP to the containing flex row using XPath:

```javascript
// Find the "Bezahlung abgeschlossen" text, then highlight its parent row
const paidText = page.locator('text=Bezahlung abgeschlossen').first();
if (await paidText.isVisible({ timeout: 2000 }).catch(() => false)) {
  const bannerRow = paidText
    .locator('xpath=ancestor::div[contains(@class, "flex")]')
    .first();
  if (await bannerRow.isVisible().catch(() => false)) {
    await addHighlight(bannerRow);
  }
}
```

**Known status banner text (German):**

| Status | Banner text | Color theme |
|---|---|---|
| `accepted` (payment pending) | `Zahlung ausstehend` | Amber |
| `paid` | `Bezahlung abgeschlossen` | Green |
| `in_progress` | `In Bearbeitung` | Blue |
| `fulfilled` | `Leistung erbracht` | Amber |
| `completed` | `Abgeschlossen` | Emerald |
| `revision_requested` | `Revision angefordert` | Amber |
| `declined` | `abgelehnt` | Red |

This pattern works for any inline UI element that doesn't have a unique CSS class or ID but does have unique visible text.
