diff --git a/.planning/phases/07-validation-ux-polish/07-RESEARCH.md b/.planning/phases/07-validation-ux-polish/07-RESEARCH.md
new file mode 100644
index 0000000..7f23716
--- /dev/null
+++ b/.planning/phases/07-validation-ux-polish/07-RESEARCH.md
@@ -0,0 +1,493 @@
+# Phase 7: Validation & UX Polish - Research
+
+**Researched:** 2026-03-31
+**Domain:** React / react-hook-form / Zod v4 / Tailwind CSS inline validation and tooltip patterns
+**Confidence:** HIGH
+
+
+## User Constraints (from CONTEXT.md)
+
+### Locked Decisions
+
+**Validation scope:**
+- Azure account name (`account` field, azureblob): 3-24 lowercase alphanumeric characters — validate with regex `/^[a-z0-9]{3,24}$/`
+- S3 region (`region` field, s3): valid AWS region format (lowercase letters, digits, hyphens) — validate with regex `/^[a-z][a-z0-9-]+[a-z0-9]$/`
+- GCS project number (`project_number` field, gcs): digits only — validate with regex `/^\d+$/`
+- S3-compatible endpoint: non-empty only (existing required behavior) — no URL format validation
+- S3-compatible region: skip — optional field, no universal format rule
+- No other backends/fields need format validation
+
+**Validation rule location:**
+- Add `validate?: { regex: RegExp; message: string }` to `FieldDef` in `src/schemas/registry.ts`
+- `buildZodSchema()` in `src/schemas/index.ts` reads `field.validate` and adds `.regex(...)` to the Zod string
+- Registry entries for the 3 validated fields get their `validate` rule inline
+
+**Tooltip trigger:**
+- Info icon (ⓘ) placed next to the field label — click/tap toggles explanation
+- Inline reveal: explanation expands directly below the field input, pushing content down
+- Toggle state is local to the field component (no global state needed)
+- Tooltip stays visible until ⓘ is clicked again (not dismissed on blur)
+
+**Tooltip data location:**
+- Add optional `tooltipText?: string` to `FieldDef` interface
+- `helpText` remains unchanged (brief hint, always visible below the field)
+- `tooltipText` is the longer plain-language explanation shown on ⓘ click
+- `FieldRenderer` and `PasswordField` render the ⓘ icon + inline expand when `tooltipText` is present
+
+**Fields that get tooltips:**
+- `azureblob sas_url`: explain SAS URL vs access key — SAS URL bundles endpoint + time-limited token, scoped to containers; access key is the full account credential
+- `azureblob key` (access key): explain it's the full storage account key — different from SAS, full access
+- `sftp` auth method: explain password vs key-based auth — SftpAuthToggle UI needs an ⓘ on the tab labels or above the toggle
+- `onedrive token`: explain what the JSON token is and how to obtain it via `rclone authorize "onedrive"`
+
+### Claude's Discretion
+- Exact Tailwind styling of the ⓘ icon and inline tooltip panel
+- Whether SftpAuthToggle gets its ⓘ on the tab label or as a standalone line above the tabs
+- ⓘ icon source (Heroicons, inline SVG, or text character)
+- Exact wording of tooltip content (stay close to existing helpText tone)
+
+### Deferred Ideas (OUT OF SCOPE)
+None — discussion stayed within phase scope.
+
+
+
+## Phase Requirements
+
+| ID | Description | Research Support |
+|----|-------------|-----------------|
+| VALID-01 | User sees inline validation error when a field value violates backend-specific format rules (Azure account name: 3–24 lowercase alphanumeric; S3 region: valid format; etc.) | Zod v4 `.regex()` chaining verified; `buildZodSchema()` extension pattern confirmed; error display already exists in FieldRenderer |
+| UX-01 | User can view a contextual tooltip on sensitive or complex fields (SAS token vs. access key, region codes, SFTP auth method, OneDrive token) | React `useState` toggle pattern sufficient; `FieldDef` extension with `tooltipText?` is the correct approach; PasswordField and FieldRenderer both need ⓘ treatment |
+
+
+---
+
+## Summary
+
+Phase 7 is a narrowly-scoped surgical enhancement to an already-working wizard. Two independent concerns: (1) format validation wired through the schema registry into Zod, and (2) per-field inline tooltip panels wired through the registry into the field renderer components. Both concerns touch the same three-file chain (`registry.ts` → `index.ts` → `FieldRenderer.tsx`/`PasswordField.tsx`), plus `SftpAuthToggle.tsx` for the auth-method tooltip.
+
+The project uses **Zod v4.3.6** (not v3). The `.regex()` method is available on `z.string()` and chains normally — verified by direct runtime test. However, `.optional()` in Zod v4 wraps the inner type, making `.regex()` unavailable after calling `.optional()`. The correct order when a field is optional but has a regex constraint is `z.string().regex(...).optional()`, not `z.string().optional().regex(...)`. All three validated fields (azureblob `account`, s3 `region`, gcs `project_number`) are `required: true`, so the simpler pattern applies: `z.string().min(1, ...).regex(regex, message)`.
+
+The tooltip feature requires zero new dependencies. React `useState` per field is the right tool. The CSS toggle pattern already established in `AzureAuthToggle` and `SftpAuthToggle` (div.block / div.hidden) is the exact pattern to reuse for tooltip panel visibility. All existing tests pass (147/147); new tests must be added for VALID-01 regex rejection and UX-01 tooltip toggle behavior.
+
+**Primary recommendation:** Implement as two sequential plans — Plan 01 adds validation (registry + schema + tests), Plan 02 adds tooltips (FieldDef extension + FieldRenderer + PasswordField + SftpAuthToggle + tests).
+
+---
+
+## Standard Stack
+
+### Core (already installed — no new dependencies)
+
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| zod | 4.3.6 | Schema validation | Already in use — `buildZodSchema()` already chains `.min(1)`, just extend with `.regex()` |
+| react-hook-form | 7.72.0 | Form state + validation trigger | Already wired — `mode: 'onSubmit', reValidateMode: 'onChange'` means errors appear after first submit then live |
+| @hookform/resolvers | 5.2.2 | zodResolver bridges Zod into RHF | Already wired in RemoteConfigStep |
+| react | 18.3.1 | `useState` for tooltip toggle | useState already used in PasswordField for show/hide |
+| tailwindcss | 4.2.2 | Styling | Inline tooltip panel follows existing helpText style (`text-xs`) |
+
+**No new npm packages needed for this phase.**
+
+### Alternatives Considered
+
+| Instead of | Could Use | Tradeoff |
+|------------|-----------|----------|
+| Local `useState` toggle | Global tooltip store | Local is correct here — each field is independent, no cross-field coordination |
+| CSS div.hidden / div.block pattern | `{showTooltip &&
...}` conditional render | Both work; CSS-hidden is the established project convention for toggles (AzureAuthToggle, SftpAuthToggle) — use CSS-hidden for consistency |
+| Text character ⓘ | Heroicons InformationCircleIcon | No new dependency needed; text or inline SVG both work; Claude's discretion |
+
+---
+
+## Architecture Patterns
+
+### Recommended File Modification Order
+
+```
+src/schemas/registry.ts # Step 1: extend FieldDef interface, add validate + tooltipText fields
+src/schemas/index.ts # Step 2: extend buildZodSchema() to read field.validate
+src/components/ui/FieldRenderer.tsx # Step 3: add ⓘ icon + tooltip panel for text/select renderers
+src/components/ui/PasswordField.tsx # Step 3: add ⓘ icon + tooltip panel for password renderer
+src/components/wizard/SftpAuthToggle.tsx # Step 4: add ⓘ above the tab toggle
+src/components/wizard/RemoteConfigStep.test.tsx # Tests: VALID-01 + UX-01 coverage
+```
+
+### Pattern 1: FieldDef Interface Extension
+
+**What:** Add two optional properties to the existing `FieldDef` interface.
+**When to use:** Always — this is the single source of truth for both validate and tooltipText.
+
+```typescript
+// src/schemas/registry.ts — add to FieldDef interface
+export interface FieldDef {
+ key: string;
+ label: string;
+ inputType: 'text' | 'password' | 'select' | 'toggle';
+ required: boolean;
+ placeholder?: string;
+ helpText?: string;
+ options?: { value: string; label: string }[];
+ validate?: { regex: RegExp; message: string }; // NEW: format validation rule
+ tooltipText?: string; // NEW: longer plain-language explanation
+}
+```
+
+### Pattern 2: buildZodSchema() Extension
+
+**What:** After the existing required/optional branch, chain `.regex()` if `field.validate` is present.
+**Critical detail:** For required fields, chain `.regex()` AFTER `.min(1)`. For optional fields, chain `.regex()` BEFORE `.optional()` — though all three validated fields are required, so the optional case does not apply in this phase.
+
+```typescript
+// src/schemas/index.ts — updated loop body
+for (const field of fields) {
+ let schema: z.ZodTypeAny = field.required
+ ? z.string().min(1, `${field.label} is required`)
+ : z.string();
+
+ if (field.validate) {
+ schema = (schema as z.ZodString).regex(field.validate.regex, field.validate.message);
+ }
+
+ if (!field.required) {
+ schema = (schema as z.ZodString).optional();
+ }
+
+ shape[field.key] = schema;
+}
+```
+
+**Zod v4 verification:** `z.string().min(1, ...).regex(/pattern/, 'message')` — confirmed working via runtime test. Regex fires after min(1), so empty string shows "required" error not "invalid format".
+
+### Pattern 3: Tooltip Toggle in FieldRenderer
+
+**What:** Add local `useState` for tooltip visibility; conditionally render the ⓘ button and panel.
+**When to use:** When `field.tooltipText` is present — guard with `field.tooltipText &&`.
+
+```typescript
+// src/components/ui/FieldRenderer.tsx — text case (same pattern for select)
+import { useState } from 'react';
+
+// Inside the component function:
+const [showTooltip, setShowTooltip] = useState(false);
+
+// Label with optional ⓘ icon:
+
+
+// Tooltip panel (below input, above helpText/error):
+{field.tooltipText && showTooltip && (
+
+ {field.tooltipText}
+
+)}
+```
+
+### Pattern 4: PasswordField Tooltip
+
+**What:** PasswordField currently does not receive `field: FieldDef` — it receives individual props. Two options for adding tooltipText support:
+
+**Option A (minimal change):** Add `tooltipText?: string` prop to PasswordFieldProps, same `useState` pattern inside.
+**Option B (structural change):** Pass the full `FieldDef` to PasswordField so it receives all future FieldDef additions automatically.
+
+**Recommendation:** Option A (minimal). The existing interface is stable and PasswordField is already used by both FieldRenderer and the auth toggle components — changing its interface to accept full FieldDef would require updating all call sites. Adding a single optional prop is lower risk.
+
+```typescript
+// Updated PasswordFieldProps:
+interface PasswordFieldProps {
+ id: string;
+ label: string;
+ error?: FieldError;
+ registration: UseFormRegisterReturn;
+ placeholder?: string;
+ helpText?: string;
+ tooltipText?: string; // NEW
+}
+```
+
+FieldRenderer passes `tooltipText={field.tooltipText}` when calling PasswordField. AzureAuthToggle call sites do not need tooltipText (azureblob key and sas_url get their tooltip via FieldRenderer in the registry loop — wait, see Pitfall 1 below).
+
+### Pattern 5: SftpAuthToggle Auth-Method Tooltip
+
+**What:** The auth-method tooltip is not field-level — it explains the Password vs Private Key choice. A standalone `useState` for one tooltip, placed above the segmented control.
+
+```typescript
+// SftpAuthToggle.tsx addition:
+const [showAuthTip, setShowAuthTip] = useState(false);
+
+// Before the segmented control div:
+
+ Authentication Method
+
+
+{showAuthTip && (
+
+ Password auth sends your password directly to the SFTP server.
+ Key-based auth uses a private key file (more secure — the server only stores your public key).
+ Paste the private key PEM content if using key-based auth.
+
+)}
+```
+
+### Anti-Patterns to Avoid
+
+- **Calling `.optional()` then `.regex()` in Zod v4:** `z.string().optional()` returns `ZodOptional` which has no `.regex()` method. Order must be `.regex().optional()` for optional+validated fields (not applicable here since all validated fields are required, but important to know).
+- **Adding tooltip state to a parent component:** Each field manages its own tooltip state with `useState`. No lifting state up, no global tooltip context.
+- **Rendering the ⓘ button when `tooltipText` is undefined:** Guard with `field.tooltipText &&` to avoid rendering an empty button for most fields.
+- **Passing `tooltipText` to PasswordField from AzureAuthToggle for azureblob sas_url and key fields:** These fields are rendered inside AzureAuthToggle which calls PasswordField directly — not via FieldRenderer's registry loop. The `tooltipText` for azureblob.key and azureblob.sas_url must be passed explicitly through AzureAuthToggle (see Pitfall 1).
+
+---
+
+## Don't Hand-Roll
+
+| Problem | Don't Build | Use Instead | Why |
+|---------|-------------|-------------|-----|
+| Regex validation error display | Custom error state in component | Zod `.regex()` + zodResolver | RHF already captures Zod errors and makes them available as `errors[field.key]` — FieldRenderer already renders them |
+| Tooltip positioning/overflow | CSS `position: absolute` tooltip popover | Inline expand (push content down) | Project decision: inline reveal, pushes content down — simpler, no overflow issues, no z-index conflicts |
+| Validation trigger timing | Custom onChange handlers | RHF `reValidateMode: 'onChange'` | Already configured in RemoteConfigStep — regex errors appear live after first submit attempt |
+
+---
+
+## Common Pitfalls
+
+### Pitfall 1: AzureAuthToggle bypasses FieldRenderer for sas_url and key
+
+**What goes wrong:** `azureblob.sas_url` and `azureblob.key` are not rendered through the registry loop in RemoteConfigStep — they are rendered directly by `AzureAuthToggle` which calls `PasswordField` with hardcoded props. Adding `tooltipText` to the registry entries for these fields is not enough to make the tooltip appear automatically.
+
+**Why it happens:** RemoteConfigStep uses a three-branch ternary. The `azureblob` branch renders the account field via FieldRenderer but delegates key+sas_url to `AzureAuthToggle`. AzureAuthToggle has hardcoded `helpText` strings and doesn't read from the registry.
+
+**How to avoid:** Two options — (A) look up the registry entry inside AzureAuthToggle and pass `field.tooltipText` to PasswordField, or (B) hardcode the `tooltipText` prop in AzureAuthToggle's PasswordField calls. Option A is cleaner. If using Option A: `BACKEND_REGISTRY.azureblob.fields.find(f => f.key === 'sas_url')?.tooltipText`.
+
+**Warning signs:** Registry has `tooltipText` on sas_url and key, but no tooltip appears in the Azure form. Test will catch this if asserting tooltip button presence.
+
+### Pitfall 2: PasswordField used by both FieldRenderer and auth toggles — prop update needed in all call sites
+
+**What goes wrong:** PasswordField is called in three places: FieldRenderer, AzureAuthToggle, and SftpAuthToggle. Adding `tooltipText?` to PasswordFieldProps is safe (optional prop), but only FieldRenderer and AzureAuthToggle need it populated. SftpAuthToggle's PasswordField calls (for `pass` and `key_pem`) do not need tooltipText — the auth method tooltip is above the toggle, not on each individual field.
+
+**Why it happens:** PasswordField is a shared component used in multiple contexts.
+
+**How to avoid:** Simply don't pass `tooltipText` in SftpAuthToggle's PasswordField calls — the prop is optional.
+
+### Pitfall 3: Zod v4 `.regex()` returns a new ZodString — type must be cast for chaining
+
+**What goes wrong:** In `buildZodSchema()`, the `schema` variable is typed as `z.ZodTypeAny`. After calling `.min(1, ...)` on a `z.string()`, the result type is still `ZodString`, but TypeScript may not know this through a `ZodTypeAny` variable.
+
+**Why it happens:** `ZodTypeAny` is a broad union type. The `.regex()` method is only on `ZodString`.
+
+**How to avoid:** Cast to `z.ZodString` before calling `.regex()`: `(schema as z.ZodString).regex(...)`. Alternatively, refactor the loop to keep a `ZodString` variable explicitly. Confirmed working via runtime test.
+
+### Pitfall 4: Test assertions for tooltip — input type='password' accessibility
+
+**What goes wrong:** When testing tooltip button presence for password fields (sas_url, key, onedrive token), the label must match what `getByLabelText` expects. The ⓘ button is inline with the label text, not the label itself.
+
+**Why it happens:** If the ⓘ is a `