---
phase: 14
slug: auth-pages
status: draft
shadcn_initialized: false
preset: none
created: 2026-05-16
---

# Phase 14 — UI Design Contract

> Visual and interaction contract for the auth-pages restyle. Generated by gsd-ui-researcher.
> Source of truth for: gsd-planner, gsd-executor, gsd-ui-auditor.

---

## Design System

| Property | Value |
|----------|-------|
| Tool | none (custom CSS + design tokens) |
| Preset | not applicable |
| Component library | Phase 13 design system (`@ui.Input`, `@ui.FormField`, `@ui.Button`, `@ui.Card`) |
| Icon library | none (Google SVG inline; no general icon library in use) |
| Font | ui-sans-serif system stack (body); "Roboto", Arial (Google button label only) |

**Source:** CONTEXT.md decisions D-FI01, D-FI02, D-FI03; CLAUDE.md tech stack; `backend/internal/web/ui/base.css`.

No shadcn gate applies — this is a Go + HTMX project with a custom CSS design system, not React/Next.js/Vite.

---

## Page Structure Contract

### AuthLayout shell

AuthLayout replaces `@Layout(...)` for both `/login` and `/signup`. It renders a standalone HTML document with no navigation header or footer.

| Layer | CSS class | Description |
|-------|-----------|-------------|
| Outer shell | `.login-screen` | Full-viewport flex center; `background: var(--gradient-shell)`; `overflow: hidden; position: relative` |
| Animated background | `.background-layer` | `position: absolute; inset: 0; overflow: hidden; pointer-events: none` — contains 35 logo elements |
| Card wrapper | `.card-wrap` | `max-width: 32rem; width: 100%; position: relative; z-index: 1` |
| Card glow | `.card-glow` | `background: var(--gradient-card-glow); border-radius: 1rem; filter: blur(24px); position: absolute; inset: 0; z-index: -1` — decorative only |
| Card shell | `.auth-card-shell` | `backdrop-filter: blur(12px); background: var(--color-surface-card); border: 1px solid var(--color-border-default); border-radius: 1rem; box-shadow: var(--shadow-auth-card); padding: 1.25rem; position: relative` |

Note on `.auth-card-shell` padding: `1.25rem` (20px) is ported verbatim from the go-backend reference as a port-fidelity constraint. It is not a design system spacing token and does not appear in the Spacing Scale table.

**Background gradient for `.login-screen`:** use `var(--gradient-shell)` which resolves to:
```
linear-gradient(135deg, var(--overlay-brand-muted), transparent 30%),
linear-gradient(160deg, var(--overlay-dark-soft), transparent 42%),
linear-gradient(to bottom right, var(--overlay-dark-border), var(--color-surface-page), var(--overlay-brand-faint))
```
This is already defined as a token in `base.css`. Do not hardcode color stops.

**Source:** D-L01, D-L02; go-backend `.login-screen` + `.auth-card-shell` CSS (lines 1063-1179 of `go-backend/static/styles.css`).

### Element order within `.auth-card-shell`

```
1. .auth-card-topbar       — (empty in Phase 14; reserved placeholder div, no back-link or theme toggle)
2. .brand-header           — logo image: <img src="/static/logo_dark.png" class="brand-logo" alt="Xtablo">
3. .title-group            — <h1> page title  ← PRIMARY VISUAL ANCHOR (brand logo + h1 together)
4. .auth-body              — wraps: Google button → divider → form → nav link
   ├── Google button       (.gsi-material-button as <a> or <button>)
   ├── Divider             (.divider-row → .divider-line + .divider-pill + .divider-line)
   ├── Form                (LoginFormFragment / SignupFormFragment with @ui.FormField + @ui.Input)
   └── Nav link            (.signup-copy + .signup-link)
```

The `.brand-header` (logo image) and `.title-group` (`<h1>`) together form the **primary visual anchor** of the auth card. They are the first elements a user sees and establish brand identity before interacting with the form.

**Source:** D-AC02; go-backend `AuthScreen` templ + `auth_components.templ` structure; D-AB04 (AnimatedBackground separate component).

---

## Animated Background Contract

### Component: `AnimatedBackground`

- Lives in `backend/templates/` (not `backend/internal/web/ui/`)
- 35 logo elements exactly, matching go-backend `AnimatedBackground` templ verbatim
- Each element: `<div class="background-logo bg-NN animate-*">` containing `<img class="logo-asset size-NN animate-*" src="/static/logo_dark.png" alt="Xtablo">`
- Phase 14 uses `logo_dark.png` only — no dark-mode swap (`light-only`/`dark-only` classes deferred per DEFERRED section)
- `alt="Xtablo"` on each img; parent `.background-layer` carries `aria-hidden="true"`

### Asset requirement

`backend/static/logo_dark.png` must exist before template compiles. Copy from `go-backend/static/logo_dark.png` as part of Phase 14.

### CSS classes to port into `backend/internal/web/ui/auth.css`

From `go-backend/static/styles.css` (extract only, do not import the file directly):

| Class group | Source lines | Purpose |
|-------------|-------------|---------|
| `.background-layer` | ~1081 | Absolute full-viewport container |
| `.background-logo` | ~1088 | Absolute positioning base for each logo |
| `.logo-asset` | ~1092 | `display: block; object-fit: contain` |
| `.size-06` through `.size-20` | ~1099–1111 | Logo size scale (rem-based) |
| `.opacity-02` through `.opacity-05` | ~1113–1116 | Per-element opacity |
| `.bg-01` through `.bg-35` | ~1118–1155 | Fixed positions (top/left/right/bottom percentages) |
| All `animate-*` utility classes | ~2883–2930+ | Animation assignments |
| All `@keyframes` definitions | below animate-* | The actual keyframe definitions for every named animation |

**Source:** D-AB01, D-AB03.

---

## Spacing Scale

Multiples of 4 only. All values from existing token usage in go-backend auth CSS.

| Token | Value | Auth usage |
|-------|-------|------------|
| xs | 4px | Divider gap between line and pill |
| sm | 8px | AnimatedBackground element gaps; `.auth-provider-stack` gap |
| md | 16px | `.auth-body` flex gap; auth-provider separator margin |
| lg | 24px | `.auth-card-topbar` margin-bottom; `.title-group` margin-bottom; `.brand-header` margin-bottom |
| xl | 32px | `.login-screen` padding |
| 2xl | 48px | — |

**Out-of-grid values (not tokens):** `.auth-card-shell` padding is `1.25rem` (20px) — ported verbatim from go-backend as a port-fidelity constraint; see Page Structure Contract. Screen horizontal padding is `1rem` (16px) which falls on the grid.

---

## Typography

| Role | Size | Weight | Line Height | Notes |
|------|------|--------|-------------|-------|
| Auth heading | clamp(1.5rem, 4vw, 1.875rem) | 700 | 1.2 | `.title-group h1` — "Sign in to Xtablo", "Create your account" |
| Body | 14px (0.875rem) | 400 | 1.4 | Field labels, divider pill text, nav link copy, `.signup-copy` muted paragraph |
| Google button label | 14px (0.875rem) | 500 | — | `.gsi-material-button-contents` only — Material Design spec; `font-family: "Roboto", Arial, sans-serif; letter-spacing: 0.25px`; must not propagate outside `.gsi-material-button` |

**Rules:**
- 2 weights for page-authored elements: **400** (regular) and **700** (bold). Weight 500 is used only inside `.gsi-material-button-contents` per the Material Design Google Sign-In specification.
- 2 effective sizes for page-authored elements: `clamp(1.5rem–1.875rem)` for the heading, `0.875rem` for all supporting text.
- Body font: inherited from `base.css` (`ui-sans-serif, system-ui, -apple-system, ...`).
- The scoped exception for the Google button (`font-family: "Roboto"`, `font-weight: 500`, `letter-spacing: 0.25px`) is contained entirely within the `.gsi-material-button` CSS block and does not propagate to any other element.

**Source:** go-backend CSS `.title-group h1`, `.field-stack label`, `.signup-copy`, `.gsi-material-button-contents`.

---

## Color

| Role | Token | Resolved value | Usage |
|------|-------|----------------|-------|
| Dominant (60%) | `--color-surface-page` / `--color-surface-card` | `#ffffff` / `rgba(255,255,255,0.8)` | Auth card shell background (glassmorphism) |
| Secondary (30%) | `--gradient-shell` via `--overlay-brand-muted`, `--overlay-dark-soft`, `--overlay-dark-border`, `--overlay-brand-faint` | See gradient definition above | `.login-screen` full-viewport background |
| Accent (10%) | `--color-brand-primary` | `#804eec` | Reserved for: `.signup-link` text color (inherited from `--foreground` + hover state via `--accent`); focus rings on inputs (`--color-focus-ring: rgba(124,58,237,0.2)`) |
| Destructive | `--color-status-danger-*` | `#dc2626` / banner tokens | Error state banners (`@GeneralError`, `@FieldError`) and `.status-error` banner |
| Muted text | `--color-text-muted` / `--muted-foreground` | `hsl(0 0% 43.5%)` | `.signup-copy`, `.divider-pill`, placeholder text |
| Google button | `--color-surface-default` bg / `--color-border-google` border / `--color-text-google` text | `#ffffff` / `#747775` / `#1f1f1f` | `.gsi-material-button` only |

**Accent reserved for:**
- Input focus ring (via `--color-focus-ring`)
- `.signup-link` hover background (via `--accent` = `var(--color-surface-subtle)`)
- No use of brand purple on any other auth page element

**Source:** go-backend auth CSS; `backend/internal/web/ui/base.css` token definitions.

---

## Component Contracts

### Brand logo

```
.brand-header > img.brand-logo
  src="/static/logo_dark.png"
  alt="Xtablo"
  width: 4rem; height: 4rem; object-fit: contain
```

### Auth card topbar

Empty `<div class="auth-card-topbar">` in Phase 14. No back-link, no theme toggle. Preserve the class for future phases.

### Title group

```
.title-group > h1
  Login page:  "Sign in to Xtablo"
  Signup page: "Create your account"
  font-size: clamp(1.5rem, 4vw, 1.875rem); font-weight: 700; margin: 0; text-align: center
```

### Google button (`.gsi-material-button`)

Element: `<a>` (when configured) or `<button disabled>` (when unconfigured), wrapping:

```
.gsi-material-button
  └── .gsi-material-button-state        (hover/active overlay)
  └── .gsi-material-button-content-wrapper
      ├── .gsi-material-button-icon     (20×20 Google SVG, 4-path multicolor)
      ├── .gsi-material-button-contents  "Sign in with Google"  ← English label (D-G03)
      └── .visually-hidden               "Sign in with Google"  ← screen reader duplicate
```

The full Google SVG (4 colored paths — `#EA4335`, `#4285F4`, `#FBBC05`, `#34A853`) is inlined as in go-backend `GoogleButton` templ.

Disabled state: `cursor: not-allowed; opacity: 0.6` (or Tailwind equivalent); `aria-disabled="true"`.

**Source:** D-G01, D-G02, D-G03; go-backend `GoogleButton` templ.

### Divider

```
.divider-row
  ├── .divider-line   (flex: 1; border-top: 1px solid var(--border))
  ├── .divider-pill   "or"  ← English (not "Ou continuer avec")
  └── .divider-line
```

**Source:** go-backend `AuthDivider` templ; English convention per D-G03 pattern.

### Form fields

Replace all raw `<input>` in `LoginFormFragment` and `SignupFormFragment` with `@ui.FormField(ui.FormFieldProps{...})` wrapping `@ui.Input(ui.InputProps{...})`:

```
@ui.FormField(ui.FormFieldProps{
    Label: "Email address",
    For:   "email",
    Field: ui.Input(ui.InputProps{
        ID:           "email",
        Name:         "email",
        Type:         "email",
        Placeholder:  "you@example.com",
        Value:        form.Email,
        Required:     true,
        Attrs:        templ.Attributes{"autocomplete": "email"},
    }),
    Error: errs.Email,  // replaces @FieldError(errs.Email)
})
```

The `@GeneralError(errs.General)` call remains at the top of the form, outside `@ui.FormField`.

### Navigation links

```
login page below form:
  <p class="signup-copy">
    Don't have an account?
    <a class="signup-link" href="/signup">Sign up</a>
  </p>

signup page below form:
  <p class="signup-copy">
    Already have an account?
    <a class="signup-link" href="/login">Sign in</a>
  </p>
```

**Source:** D-NL01, D-NL02, D-NL03; go-backend `AuthScreenFooter` templ pattern.

---

## Copywriting Contract

| Element | Copy |
|---------|------|
| Login page title | Sign in to Xtablo |
| Signup page title | Create your account |
| Login CTA (submit button) | Sign in to Xtablo |
| Signup CTA (submit button) | Create account |
| Google button label | Sign in with Google |
| Divider label | or |
| Login nav link | Don't have an account? [Sign up] |
| Signup nav link | Already have an account? [Sign in] |
| Email field label | Email address |
| Email placeholder | you@example.com |
| Password field label (login) | Password |
| Password placeholder (login) | Your password |
| Password field label (signup) | Password |
| Password placeholder (signup) | 12 characters minimum |
| General error state | [inline `@GeneralError` — existing component; copy determined by server error message] |
| Field error state | [inline `@FieldError` wired through `FormFieldProps.Error` — existing component] |
| Google button disabled label | Sign in with Google (provider unavailable) |

No empty state: auth pages always show the form. No destructive actions on these pages.

**Source:** D-G03 (English label); existing backend templates copy; cross-page nav copy from D-NL01, D-NL02.

---

## Interaction Contract

### HTMX behavior — preserved exactly, not changed

| Form | `hx-post` | `hx-target` | `hx-swap` |
|------|-----------|-------------|-----------|
| Login form (`#login-form`) | `/login` | `#login-form` | `outerHTML` |
| Signup form (`#signup-form`) | `/signup` | `#signup-form` | `outerHTML` |

**Phase 14 is visual only. No HTMX behavior changes.**

### Focus management

- Tab order: Google button → email input → password input → submit button → nav link
- Focus ring: `box-shadow: 0 0 0 2px var(--color-focus-ring)` on inputs (via `ui-input` class from Phase 13 design system)
- Google button focus: `.gsi-material-button-state` opacity 0.12 on `:focus` (via ported CSS)

### States

| Element | States |
|---------|--------|
| Google button (configured) | default, hover (box-shadow), focus (state overlay 0.12), active (state overlay 0.12) |
| Google button (unconfigured) | disabled (cursor: not-allowed; no hover effect) |
| Input | default, focus (ring), error (error class from `@ui.FormField`) |
| Submit button | default, hover (opacity 0.9 via `@ui.Button`), disabled during submission (`aria-busy`) |
| Nav link | default, hover (background: `var(--accent)`, color transitions to `--foreground`) |
| Animated background logos | 35 independent CSS animations, `pointer-events: none`, `aria-hidden="true"` |

---

## Accessibility Contract

- `.background-layer` has `aria-hidden="true"` — decorative only
- Each `<img>` in `AnimatedBackground` has `alt="Xtablo"` — deferred to remove once `aria-hidden` is confirmed propagated by all browsers; acceptable for now
- Google button includes `.visually-hidden` span with "Sign in with Google" for screen readers
- Form inputs have explicit `<label for="...">` via `@ui.FormField`
- CSRF hidden field is preserved in both forms
- Disabled Google button uses `disabled` attribute + `aria-disabled="true"`

---

## CSS Files Contract

| File | Action | What changes |
|------|--------|-------------|
| `backend/internal/web/ui/auth.css` | Replace entirely | Port full auth CSS from go-backend: `.login-screen`, `.background-layer`/`.background-logo`/sizing/opacity/position classes, all `animate-*` utilities, all `@keyframes`, `.auth-card-shell` (padding: 1.25rem — ported verbatim from go-backend reference; not a design system spacing token), `.auth-card-topbar`, `.brand-header`, `.brand-logo`, `.title-group`, `.auth-body`, `.login-form` (replaced by `@ui.FormField` but keep for compat), `.divider-row`/`.divider-line`/`.divider-pill`, `.signup-copy`, `.signup-link`, `.gsi-material-button` and sub-classes, `.card-wrap`, `.card-glow`, `.status-slot`, `.status-banner`, `.status-success`, `.status-error` |
| `backend/tailwind.input.css` | No change | `auth.css` path is unchanged; import remains valid |
| `backend/internal/web/ui/base.css` | No change | All required tokens already defined |

The existing `.auth-provider-stack`, `.auth-provider-button`, `.auth-provider-separator` rules in `auth.css` are superseded by the Google button restyle — but keep them for any non-Google controls. The `auth-provider-button` is replaced by `gsi-material-button` for Google specifically.

---

## Template Files Contract

| File | Action |
|------|--------|
| `backend/templates/auth_layout.templ` | **Create new** — standalone HTML shell: `<!DOCTYPE html>`, head with `tailwind.css` + `htmx.min.js`, body with `.login-screen` wrapper, `@AnimatedBackground()`, `.card-wrap`, `.card-glow`, `.auth-card-shell` |
| `backend/templates/auth_components.templ` | **Create new** — `AnimatedBackground` component (35 elements); optionally `AuthDivider` component |
| `backend/templates/auth_login.templ` | **Update** — switch from `@Layout(...)` to `@AuthLayout(...)`; replace `@ui.Card(...)` wrapper with auth card structure; migrate inputs to `@ui.FormField`/`@ui.Input`; add nav link |
| `backend/templates/auth_signup.templ` | **Update** — same changes as login |

Handler signatures (`templates.LoginPage(...)`, `templates.SignupPage(...)`) must not change — the `AuthLayout` detail is internal to the template.

---

## Registry Safety

| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none | not applicable |
| third-party | none | not applicable |

This project uses no component registry. All CSS is hand-authored. The only third-party visual asset is the Google SVG (4 colored paths), which is inlined verbatim from the Material Design specification and already present in go-backend source.

---

## Pre-Population Sources

| Decision | Source |
|----------|--------|
| AuthLayout gradient: `var(--gradient-shell)` | `base.css` token + go-backend `.login-screen` CSS |
| Auth card shell CSS (padding, radius, shadow, backdrop) | go-backend `styles.css` lines 1174–1182 |
| AnimatedBackground 35 elements | go-backend `auth_components.templ` verbatim |
| Logo asset path `/static/logo_dark.png` | D-AB02; go-backend static |
| Google button structure and all CSS | D-G01, D-G02; go-backend `GoogleButton` templ + styles |
| Google button label "Sign in with Google" | D-G03 |
| `@ui.FormField` / `@ui.Input` migration | D-FI01, D-FI02, D-FI03; Phase 13 design system |
| Nav link copy and CSS classes | D-NL01, D-NL02, D-NL03; go-backend `AuthScreenFooter` + `.signup-copy`/`.signup-link` |
| HTMX swap patterns preserved unchanged | CONTEXT.md code_context section |
| Dark mode logo deferred | CONTEXT.md deferred section (DARK-01) |
| Forgot password link deferred | CONTEXT.md deferred section |
| Responsive layout deferred | CONTEXT.md deferred section (RESP-01..03) |

---

## Checker Sign-Off

- [ ] Dimension 1 Copywriting: PASS
- [ ] Dimension 2 Visuals: PASS
- [ ] Dimension 3 Color: PASS
- [ ] Dimension 4 Typography: PASS
- [ ] Dimension 5 Spacing: PASS
- [ ] Dimension 6 Registry Safety: PASS

**Approval:** pending
