Files
pad/docs/brand.md
T
xarmian 94c6d3cc52 fix(brand): the favicon's gaps tighten to match the mark (TASK-3974) (#2248)
pad-mark-small moves from 4px tiles with 2px gaps (gap/tile 0.50) to 4px
tiles with 1px gaps and a 1px inset (0.25), the grid pad-favicon-tile
already used. It stays pixel-aligned: the master's own 0.19 renders its
sub-pixel gaps as smeared half-tones at 16px 1x in Chromium and WebKit,
and this grid stays crisp at 1x, 2x and 3x. icon.svg, favicon-16/32 and
favicon.ico (16/32/48) are re-exported with export.mjs; the outputs of
unchanged masters came out byte-identical, so the export reproduces the
committed set. docs/brand.md records the change and why.


Claude-Session: https://claude.ai/code/session_012tVNjc1RAdLdRN1ae77u4U

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 20:32:39 -04:00

486 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Pad Brand Spec
**Audience:** developers working on `pad-web` (`../pad-web`, the `getpad.dev`
marketing site) and `web/` (the SvelteKit app embedded in this repo's Go
binary, served at `app.getpad.dev` in Cloud mode and at `localhost:7777`
or operator domains in self-hosted mode).
**Purpose:** define the visual contract for surfaces that **border**
between marketing and product, so the two codebases can converge
intentionally rather than drift accidentally.
**This doc is _not_ a full design system.** It only covers what is
actually shared. The interior of the Pad app (the workspace shell, item
detail pages, board views, etc.) keeps its own conventions and is
explicitly out of scope here.
> Tracked under [PLAN-900] (Cohesive UX between getpad.dev and Pad
> Cloud), TASK-904. When you change anything in this doc, update the
> citations in the source files listed under [How both repos cite this
> doc](#how-both-repos-cite-this-doc).
---
## 1. Where parity matters / where divergence is fine
This is the most important decision in this document. Read this section
first.
| Surface | Parity? | Notes |
|---|---|---|
| `getpad.dev` marketing pages (home, docs, blog, legal) | — | The reference. Other surfaces converge toward this. |
| `app.getpad.dev` auth-page family (login, register, forgot-password, reset-password, join, OAuth-error) **in Cloud mode** | **Full parity** | Header + footer + tokens match `getpad.dev`. See PLAN-900 / TASK-902 / TASK-903. |
| `app.getpad.dev` 404 / 500 error pages **in Cloud mode** | **Full parity** | Same chrome as auth pages. See TASK-906. |
| Transactional emails (invite, password reset, etc.) **in Cloud mode** | **Full parity** | Header + footer treatment + token palette. See TASK-907. |
| In-app **Resources** menu (links to docs / changelog / GitHub / support) | Brand-consistent text + icon treatment, but lives inside the workspace shell — does not import marketing chrome. See TASK-905. |
| Self-hosted auth pages, error pages, emails | **Neutral — no parity** | Operators ship Pad under their own brand. Self-hosted UI must NOT carry getpad.dev branding. Gated on the existing `cloud_mode` flag (see [Cloud-mode flag](#cloud-mode-flag)). |
| App workspace shell (TopBar, sidebar, item detail, board, list views) | **Divergent — keep as-is** | Tool aesthetic. Don't homogenize. |
**Two-line summary:** Cohesion applies at the seams between marketing
and product, and only when the user is on Cloud. Inside the app and on
self-hosted, leave things alone.
---
## 2. Cloud-mode flag
Every parity decision in this doc is conditional on Cloud mode. The
flag is already plumbed end-to-end and **no new env var should be
introduced**:
- **Server:** `cloud_mode` is returned on `/api/v1/auth/session`
(see `internal/server/handlers_auth.go`).
- **Client:** `cloud_mode?: boolean` in `web/src/lib/api/client.ts`
(lines 119, 127). Auth pages already read it
(`web/src/routes/login/+page.svelte` line 18,
`cloudMode = $state(false)`).
- **Components:** `LegalFooter` and `SupportFooter` accept a
`cloudMode` prop today and switch behavior on it
(`web/src/lib/components/auth/`).
Reuse this signal everywhere. If a new component needs to know whether
it's on Cloud, plumb the same prop — do not add a parallel mechanism.
---
## 3. Color tokens
The marketing site (`pad-web/src/app.css`) is the canonical source
for bordering surfaces. The accent palette is already aligned across
both repos; the structural tokens (bg / text / border) currently
diverge and the convergence direction is **app moves toward marketing**,
not the other way around.
### Canonical values (bordering surfaces only)
```
--color-bg #111113 /* primary surface */
--color-bg-raised #1a1a1e /* card surface */
--color-bg-surface #222226 /* nested surface */
--color-bg-hover #2a2a30
--color-text #ededef /* body text */
--color-text-secondary #a0a0a8 /* nav links, secondary copy */
--color-text-muted #8a8a93 /* footer links, captions ← AA-passing */
--color-border #2a2a30 /* card borders, dividers */
--color-border-subtle #1f1f24 /* header / footer separators */
--color-accent #4a9eff /* primary action, focus ring */
--color-accent-hover #3b8de6
--color-green #4ade80
--color-amber #fbbf24
--color-purple #a78bfa
--color-red #f87171
```
### Accent palette parity
The accent values (blue, green, amber, purple) are **already identical**
between `pad-web/src/app.css` and `web/src/app.css`. Treat them as fixed
and do not re-pick them.
### Known drift to clean up (out of scope for this doc)
- `web/src/app.css` defines `--text-muted: #666666`, which gives
≈3.2:1 against `--bg-primary: #1a1a1a` and **fails WCAG AA for
body-size text**. The marketing site uses `#8a8a93` (≈4.7:1) which
passes. When the auth-page header/footer convergence work
(TASK-902 / TASK-903) lands, surfaces that consume the marketing
palette automatically inherit the AA-passing value. The deep-app
fix is a separate concern — file a follow-up if it bites.
---
## 4. Type scale
### Font families
Bordering surfaces use:
```
--font-sans 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif
--font-mono 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace
```
The deep app (`web/src/app.css`) currently uses system-ui as primary —
that's intentional for the workspace shell (system feel inside a tool)
and stays as-is. **Bordering surfaces only** should pull in Inter +
JetBrains Mono so they feel continuous with the marketing site.
If Inter / JetBrains Mono aren't already self-hosted on the app side,
loading them costs an HTTP request — fine for auth pages, error pages,
and emails (low-frequency surfaces); not justifiable inside the
workspace shell.
### Sizes (Tailwind utility names — both repos use Tailwind)
| Use | Class | Computed |
|---|---|---|
| Logo wordmark | `text-lg font-bold tracking-tight` | 18px / bold / tightened tracking |
| Nav link | `text-sm` | 14px |
| Body copy | base (no class) | 16px |
| Footer copy | `text-sm` | 14px |
| Footer muted (legal) | `text-sm text-text-muted` | 14px in `--color-text-muted` |
| Copyright line | `text-sm text-text-muted` | 14px |
---
## 5. Spacing & layout
### Container
```
mx-auto max-w-6xl px-6
```
`max-w-6xl` = 72rem = 1152px. Same on both repos. Use this on every
header / footer / page-content container that's part of the bordering
surfaces.
### Header
```
fixed top-0 z-50 w-full border-b border-border-subtle bg-bg/80 backdrop-blur-xl
```
- Fixed to top of viewport, full width, stays above page content.
- 80% opacity background plus `backdrop-blur-xl` for the soft-glass
effect. Don't drop the blur — the bordering surfaces match each other
via this exact treatment.
- `border-b border-border-subtle` is the only divider.
- Vertical padding `py-4` (16px), horizontal `px-6` (24px) inside the
`max-w-6xl` row.
### Main content offset
`<main class="pt-16">` — 64px top padding so content doesn't slide
under the fixed header. Every bordering surface needs this.
### Footer
```
border-t border-border-subtle
```
Inside container:
```
flex flex-col items-center justify-between gap-4 px-6 py-8 sm:flex-row
```
- Stacks vertically on mobile, lays out horizontally at the `sm:`
breakpoint.
- Link row uses `flex flex-wrap items-center justify-center gap-x-6 gap-y-3`.
### Mobile breakpoint
Tailwind's `md:` (768px). Above this, full nav; below, hamburger menu.
Both repos must use the same breakpoint to avoid awkward jumps when a
user opens the app from a marketing link on a tablet.
### Hamburger spec
- Closed: 24x24 SVG, two horizontal lines (`y=8` and `y=16`).
- Open: 24x24 SVG, X mark (lines from corner to corner).
- Stroke width 2, round caps and joins, `currentColor` so it inherits.
- Toggling the menu reveals an inline panel with the same nav links
stacked, `border-t border-border-subtle px-6 py-4` for the panel.
(Pulled from the existing `pad-web/src/routes/+layout.svelte`
implementation — see lines 60–127. The auth-page header in this repo
must match this byte-for-byte for the SVG path so the visual
transition is identical.)
---
## 6. Header pattern
### Anatomy (left to right)
1. **Wordmark** — `<a href="/" class="text-lg font-bold tracking-tight text-text">pad</a>`.
Lowercase. Always links to root of the current property
(`getpad.dev/` on marketing, `app.getpad.dev/login` on auth pages
pre-auth, etc.).
2. **Spacer** — `flex items-center justify-between` distributes.
3. **Link row (desktop)** — `hidden items-center gap-8 md:flex` containing
nav links. Each link `text-sm text-text-secondary transition-colors hover:text-text`.
4. **Hamburger button (mobile)** — `flex items-center justify-center md:hidden`,
aria-labelled "Toggle menu", `aria-expanded` reflects state, `aria-controls="mobile-menu"`.
### Link list — what goes in the header
The header link list is **not the same** between marketing and auth
pages. Don't try to homogenize it.
| Surface | Header links |
|---|---|
| `getpad.dev` (marketing) | Docs, Blog, GitHub, **Login** *(added by TASK-901)* |
| `app.getpad.dev` auth pages, Cloud mode | Docs, Blog, GitHub *(but linking to `https://getpad.dev/...`)* |
| `app.getpad.dev` auth pages, self-hosted | **No header at all** — render `null` |
Marketing nav adds a Login CTA because that's its job. Auth-page nav
omits it because the user is already on /login. Both nav surfaces
otherwise mirror each other so the user feels they're on the same
property.
### External links
Anything pointing off the current property opens in a new tab:
```svelte
<a href={link.href} target="_blank" rel="noopener noreferrer" ...>
```
`rel="noopener noreferrer"` is required — both for security
(`noopener` prevents the new page from accessing `window.opener`) and
because Tailwind/Svelte tooling will warn without it.
---
## 7. Footer pattern
### Anatomy
```
<footer class="border-t border-border-subtle">
<div class="mx-auto flex max-w-6xl flex-col items-center justify-between gap-4 px-6 py-8 sm:flex-row">
<p class="text-sm text-text-muted">
© 2026 Pad
<span class="mx-1">·</span>
<a href="https://perpetualsoftware.org" target="_blank" rel="noopener noreferrer" class="text-text-muted/60 transition-colors hover:text-text-secondary">
Perpetual Software LLC
</a>
</p>
<div class="flex flex-wrap items-center justify-center gap-x-6 gap-y-3">
<!-- link list — see canonical order below -->
</div>
</div>
</footer>
```
### Canonical link order
Always the same order. This is the contract:
1. GitHub *(external)*
2. Community *(external — the repo's GitHub Discussions; TASK-2888)*
3. Docs
4. Tutorials *(TASK-3452: in-app on Pad Cloud, getpad.dev/learn elsewhere; the app's user menu carries it, the marketing footer omits it while /learn is unlisted)*
5. Changelog
6. Contribute
7. FAQ
8. Security
9. Privacy
10. Terms
11. Sub-processors
If a surface omits a link (e.g. self-hosted footer skips Changelog
because there's no Changelog page on a self-hosted install), keep the
*relative* order intact — don't reshuffle.
### Link styling
```
text-sm text-text-muted transition-colors hover:text-text-secondary
```
Hover lifts from `--color-text-muted` (`#8a8a93`) to
`--color-text-secondary` (`#a0a0a8`). Subtle by design — the footer is
secondary navigation, not a CTA strip.
### Copyright line
- Format: `© <current year> Pad · Perpetual Software LLC`
- The middot is `&middot;` (`·`). Not `|`, not `-`, not `/`.
- "Perpetual Software LLC" links to `https://perpetualsoftware.org` with
the muted treatment shown above.
- Year is the current calendar year. If you're updating this doc, also
audit the inline `2026` in `pad-web/src/routes/+layout.svelte` line 142.
### Self-hosted footer
When `cloud_mode === false`, render only the legal-essential subset:
- Privacy *(if the operator has hosted a privacy doc)*
- Terms *(if applicable)*
- A copyright line that says the operator's name, not Pad's (this is
out of scope for the immediate cohesion work — for now keep the
current self-hosted footer behavior; flag for the operator-branding
follow-up plan).
---
## 8. How both repos cite this doc
When you change tokens, header structure, or footer link list in this
doc, update the source files that follow this contract. To make drift
auditable, each consuming file carries a one-line comment pointing
back here.
**Source of truth: `pad-web/src/routes/+layout.svelte`**
```svelte
<!-- Visual contract: docs/brand.md (Pad repo). When changing tokens,
header structure, or footer link list, update both this file and
the auth-page header/footer in the Pad repo to match. -->
```
**App-side consumers (each gets a similar comment):**
- `web/src/lib/components/auth/AuthHeader.svelte` *(new in TASK-902)*
- `web/src/lib/components/auth/LegalFooter.svelte`
- `web/src/lib/components/auth/SupportFooter.svelte`
- `web/src/routes/+error.svelte` *(touched by TASK-906)*
- Email templates in `internal/email/` *(touched by TASK-907)*
The comment doesn't need to be elaborate — one line citing the path is
enough. The point is that anyone touching one file can grep for the
others and keep them in sync.
---
## 9. The Pad mark
> **Adopted (Dave, 2026-10-09; TASK-3514), with a FLAT app icon:** a solid
> dark tile, no edge glow and no tile sheen. The rollout runs repo by repo
> (plan on TASK-3514); a surface not yet switched still shows its old mark.
The mark is a 3x3 grid of rounded tiles. It replaces both earlier marks: the
purple "P" (the app's favicons and app icons) and the raster nine-square
icons (mobile, video, marketing), which came from one bitmap and drifted in
crop, glow and tile size. The SVGs in `docs/brand/mark/` are the source. Every
PNG anywhere is an export of one of them, never a hand-edited copy.
### The master set (`docs/brand/mark/`)
| File | What it is | Use it for |
|---|---|---|
| `pad-mark.svg` | The mark, full colour, no background | Headers, the README, any lockup at about 20px and up |
| `pad-mark-small.svg` | Pixel-aligned at 16px: 4px tiles, 1px gaps, 1px inset (the favicon tile's grid) | Favicons at 16, 32 and 48px, and `icon.svg`; scale by whole multiples only |
| `pad-favicon-tile.svg` | The small mark on a dark tile | Places that need a solid backing: PWA maskable, Windows tiles |
| `pad-app-icon.svg` | The mark on a solid dark tile (`#0a0b16`), full-bleed | iOS AppIcon and `apple-touch-icon` (iOS masks it; no transparency) |
| `pad-app-icon-shaped.svg` | The same tile, transparent outside its rounded shape | PWA `icon-192/512`, Android legacy launcher, in-app logos (iOS `PadIcon`, Android `pad_logo`), video lockups, OG and store art |
| `pad-org-avatar.svg` | The mark, smaller, on a full-bleed dark tile with no edge ring; every tile clears a circle crop | The PerpetualSoftware GitHub org avatar, which GitHub's default social card shows |
| `pad-mark-mono.svg` | One colour (`currentColor`), with the two slate tiles at 40% | One-colour contexts: print, embossing, a single-ink badge |
### Geometry and colour
- Grid: tiles of 200, gaps of 38, tile corners of 37, on a 676 square,
measured from the mobile app icon (`icon_1024.png`). In the app icon the
grid sits at 174..850 on a 1024 canvas, and the tile's own corner radius
is 222.
- Tile colours, row by row:
- `#007bfc` `#2f2ffb` `#8406fb`
- `#01bc81` `#fdb600` `#45496c`
- `#0090fd` `#004ffb` `#424668`
- The two slate tiles are part of the mark, as in the mobile icon. Keep
them slate; do not recolour them to the accent.
- App-icon tile: solid `#0a0b16`, corner radius 222 of 1024 when shaped.
Flat by ruling: the mobile icon's edge glow and tile sheen were mocked and
declined.
### Rules
- On light and dark backgrounds alike, use `pad-mark.svg` as is. No
outline, shadow or glow anywhere, the app icon included.
- Below 20px, use `pad-mark-small.svg`. Drawn at 16px, the full mark's
38/200 gaps come out under one pixel wide, and at 1x density they render
as smeared half-tones in both Chromium and WebKit. The small variant
snaps every edge to a pixel, so its gaps stay crisp. Its gaps were 2px
(gap/tile 0.50) at first; TASK-3974 tightened them to 1px with a 1px
inset (0.25, near the master's 0.19), the grid `pad-favicon-tile.svg`
already used. That reads as the same mark as the full one and stays
crisp at 1x, 2x and 3x (the TASK-3974 comparison renders).
A 2x2 simplification was tried and dropped: it loses the identity
(TASK-3514 mockups).
- The lockup is the mark followed by the lowercase wordmark `pad` (section
6). In the mockups the mark is about 1.2x the wordmark's font size (22px
beside an 18px wordmark). In a header the mark sits beside the wordmark
and never replaces it.
### GitHub social preview
**Ruled (Dave, 2026-10-09):** keep GitHub's default card, which keeps the live
counts, and make the mark the org avatar (`pad-org-avatar-1024.png`, uploaded
in the org settings). The custom card below is parked, kept in case it is
wanted later.
`docs/brand/social/render.mjs` renders it in GitHub's own card layout, with
the mark where the org avatar goes:
- `a`: durable facts (license, stack, hosting, agent access) in place of counts.
- `b`: a snapshot of the counts.
GitHub has no API for the social preview: an owner uploads the PNG under
Settings › Social preview, and it stays as uploaded. Version b's numbers are
therefore as of the render. Run `node docs/brand/social/render.mjs b` and
re-upload to refresh them.
### Exporting
`node docs/brand/mark/export.mjs <static-dir> [--og]` writes the web icon set
from the masters: `favicon-16x16.png`, `favicon-32x32.png`, `favicon.ico`
(16/32/48), `icon.svg` (the small mark, which a browser prefers in a tab),
`pad-mark.svg`, `icon-192.png` and `icon-512.png` (shaped), `apple-touch-icon.png`
(full-bleed), `padicon.png` (shaped, for links to the old name), and with
`--og` the 1200x630 `og-card.png`. This repo runs it on `web/static --og`;
pad-web runs it on its `static/`. Never edit an exported PNG: change the SVG
and export again.
### Where it goes
Each surface gets an export from the master set. The rollout plan by repo
is on TASK-3514.
---
## 10. What this doc explicitly does NOT do
- It is not a full design system. The deep app (workspace shell, item
views, etc.) has its own conventions and stays as-is.
- It does not extract a shared component package. Both repos
hand-implement to this contract. The cost of an `@pad/ui` package
isn't justified by the small surface area being shared.
- It does not define operator-customizable branding for self-hosted.
That's a separate, larger plan to be opened after PLAN-900 ships.
- It does not address mobile / responsive behavior inside the
workspace shell — only at the bordering surfaces.
---
## 11. References
- [PLAN-900] — Cohesive UX between getpad.dev and Pad Cloud
- [IDEA-888] — Original idea this plan implements
- `pad-web/src/app.css` — canonical token values
- `pad-web/src/routes/+layout.svelte` — canonical header + footer
reference implementation
- `web/src/app.css` — app-side tokens (currently divergent on
bg / text / border / fonts; aligned on accent palette)
- `web/src/lib/components/auth/` — auth-page chrome components
- `docs/brand/mark/` — the mark's master SVGs (section 9)
[PLAN-900]: # "tracked in Pad workspace"
[IDEA-888]: # "tracked in Pad workspace"