# Peach — Every Surface Faithful to the Demo
_v1.4.1 target_
---
## Purpose
Every popup view has been read in full. This spec prescribes the exact changes needed to make every surface — including the newer ones (Passkeys, Health, Sync, Tags, Canary) that postdate the website demo — feel like they were designed together. The goal is visual confidence and coherence: one system, no escaped inline styles, no unstyled improvised surfaces.
The demo's design language is established and non-negotiable:
- **Cormorant Garamond** (italic, weight 300–400) for display text and numbers
- **IBM Plex Mono** (weight 300–400) for codes, passwords, mono values
- **Inter Variable** for all UI chrome, labels, buttons
- Amber bone palette: `--peach-accent: #D96830`, `--t0: #F0E8D8`, `--t1`, `--t2`, `--t3` scale
- Zero `border-radius` on the popup root shell; small radii (6–12px) inside surfaces only
- Muted amber glow on interactive elements; no harsh white or saturated colors
- All copy lowercase or sentence-case — never ALL-CAPS labels except in `.peach-label`
---
## Files Modified
| File | Type |
|------|------|
| `src/popup/styles/popup.css` | CSS additions + fixes |
| `src/popup/views/Entry.tsx` | TOTP ring trigger; regen panel CSS classes |
| `src/popup/views/PasskeyApproval.tsx` | Full inline-style → CSS class migration |
| `src/popup/views/PasskeyEntry.tsx` | `window.confirm()` replacement; badge CSS |
| `src/popup/views/Vault.tsx` | Passkey badge CSS class |
| `src/popup/views/Settings.tsx` | `.peach-info-notice`, sync row CSS, color literal fix |
| `src/popup/views/Export.tsx` | Input class fix |
| `src/popup/views/CJKExport.tsx` | Inline style → CSS class migration |
| `src/popup/views/CJKImport.tsx` | Inline style → CSS class migration |
| `src/popup/views/Lock.tsx` | Restore link wrapper CSS class |
| `src/popup/components/TOTP.tsx` | Class rename to `peach-` prefix |
**No changes to `detect.ts`, `inject.ts`, `fill.ts`, `save.ts`, background files, or the Rust sync host.**
---
## Issue 1 — TOTP font: missing `font-style: italic`
### Root cause
`popup.css` declares:
```css
.peach-totp-code {
font-family: "Cormorant Garamond", "Times New Roman", serif;
font-size: 38px; color: var(--peach-accent);
line-height: 0.95; min-height: 1em;
}
```
No `font-style` is set, so the browser renders Cormorant Garamond Regular (upright). The demo showed the code in **italic** — the defining characteristic of Peach's display numerals. Everything else that uses Cormorant Garamond (`.peach-entry-name`, `.peach-unlock-headline`, `.peach-wordmark`, the entry-row name) explicitly declares `font-style: italic`. The TOTP code is the only exception, making it look like a mistake.
### Fix — `popup.css`
```css
/* BEFORE */
.peach-totp-code {
margin: 2px 0 0;
font-family: "Cormorant Garamond", "Times New Roman", serif;
font-size: 38px; color: var(--peach-accent);
line-height: 0.95; min-height: 1em;
}
/* AFTER */
.peach-totp-code {
margin: 2px 0 0;
font-family: "Cormorant Garamond", "Times New Roman", serif;
font-style: italic;
font-weight: 300;
font-size: 38px; color: var(--peach-accent);
line-height: 0.95; min-height: 1em;
}
```
`font-weight: 300` (Light Italic) matches the `CormorantGaramond-LightItalic.woff2` loaded in `vars.css` and gives the numerals the same airy quality as the large display heading.
---
## Issue 2 — TOTP ring does not fire on code refresh
### Root cause
`Ring.tsx` exposes `trigger()` via `(window as any).__peachRingTrigger`. `triggerRing()` in `App.tsx` calls this, and it fires correctly on copy. But Entry.tsx never calls it when the TOTP period resets — the ring only glows when you manually copy the code, not at the natural moment of anticipation (the new code appearing).
### Fix — `Entry.tsx`
Add a `createEffect` that watches the TOTP code string itself. When the code changes after the initial render, trigger the ring. This fires once per 30-second period, at the exact instant the new code appears.
```tsx
// After the existing totpState memo, add:
// Trigger the accent ring every time the TOTP code changes (new period)
let prevTotpCode: string | undefined;
createEffect(() => {
const state = totpState();
if (!state) { prevTotpCode = undefined; return; }
if (prevTotpCode !== undefined && state.code !== prevTotpCode) {
triggerRing();
}
prevTotpCode = state.code;
});
```
This is completely passive — it reads `totpState()` (already reactive) and calls `triggerRing()` when the code flips. No new intervals needed. The ring animation in `Ring.tsx` self-terminates with a 1200ms fade, so it won't persist.
---
## Issue 3 — `TOTP.tsx` standalone component has unstyled class names
The file at `src/popup/components/TOTP.tsx` uses `totp-display`, `totp-code`, `totp-countdown` — class names without the `peach-` prefix. These are not defined in `popup.css`. If this component renders anywhere in the popup, those elements are completely unstyled.
### Fix — `TOTP.tsx`
Rename classes to the established `peach-` convention:
```tsx
export function TOTPDisplay(props: TOTPProps) {
const [totp, setTotp] = createSignal(generateTOTP(props.secret));
const interval = setInterval(() => { setTotp(generateTOTP(props.secret)); }, 1000);
onCleanup(() => clearInterval(interval));
return (
2-factor
{totp().code.slice(0, 3)} {totp().code.slice(3)}
expires
{totp().remaining}s
);
}
```
This reuses the exact same structure and CSS classes as the full `Entry.tsx` TOTP band, ensuring visual consistency wherever this simpler component is used.
---
## Issue 4 — `PasskeyApproval.tsx`: all layout via inline styles
### Root cause
Every structural element in `PasskeyApproval.tsx` uses inline `style` attributes. The class `peach-overlay-card` is used but not defined in `popup.css` (Entry.tsx uses it in the QR modal, so it may be defined there — verify and add if missing). The icon wrapper, text hierarchy, and action area have no CSS classes.
### New CSS classes — add to `popup.css` after the existing overlay section
```css
/* ── Passkey approval overlay ─────────────────────────────── */
.peach-passkey-approval {
display: flex;
flex-direction: column;
align-items: center;
padding: 28px 22px 22px;
gap: 14px;
text-align: center;
}
.peach-passkey-approval-icon {
width: 46px;
height: 46px;
border-radius: 12px;
background: var(--peach-s2);
border: 1px solid var(--peach-t3);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.peach-passkey-approval-icon img,
.peach-passkey-approval-icon svg {
width: 28px;
height: 28px;
border-radius: 6px;
}
.peach-passkey-approval-rp {
font-family: var(--font-ui);
font-size: 12px;
color: var(--peach-t2);
letter-spacing: 0.03em;
}
.peach-passkey-approval-action {
font-family: var(--font-display);
font-style: italic;
font-weight: 300;
font-size: 26px;
color: var(--peach-t0);
line-height: 1;
}
.peach-passkey-approval-user {
font-family: var(--font-mono);
font-size: 11px;
color: var(--peach-t1);
background: var(--peach-s2);
border: 1px solid var(--peach-t3);
border-radius: 4px;
padding: 3px 8px;
}
.peach-passkey-approval-notice {
font-size: 10px;
color: var(--peach-t2);
line-height: 1.5;
max-width: 220px;
}
.peach-passkey-approval-actions {
display: flex;
gap: 8px;
width: 100%;
margin-top: 6px;
}
.peach-passkey-approval-actions .peach-btn {
flex: 1;
}
/* Ensure peach-overlay-card is defined (also used in Entry.tsx QR modal) */
.peach-overlay-card {
background: var(--peach-s1);
border: 1px solid var(--peach-t3);
border-radius: 12px;
overflow: hidden;
width: 100%;
}
```
### Fix — `PasskeyApproval.tsx`
Replace the inline-styled structure with the new classes:
```tsx
{props.request.type === "create"
? "Peach will generate and store a passkey for this site."
: "Peach will authenticate you using your stored passkey."}
```
---
## Issue 5 — `PasskeyEntry.tsx`: `window.confirm()` for delete
### Root cause
`window.confirm()` opens a native OS dialog — completely outside the Peach visual system, jarring, and especially wrong in a popup context where the OS dialog usually appears behind the extension popup.
### Fix — In-popup confirmation pattern
Add a `deleteConfirmPending` signal. When the user clicks "Remove passkey", show an inline confirmation row instead of calling `window.confirm()`:
```tsx
const [deleteConfirmPending, setDeleteConfirmPending] = createSignal(false);
// Replace the delete button with:
setDeleteConfirmPending(true)}
>
Remove passkey
}
>
Remove this passkey?
```
### New CSS classes — add to `popup.css`
```css
/* ── Passkey entry view ───────────────────────────────────── */
.peach-passkey-badge {
margin-left: 8px;
font-size: 10px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--peach-t2);
font-family: var(--font-ui);
vertical-align: middle;
}
.peach-passkey-delete-btn {
color: var(--peach-breach);
border-color: rgba(229, 85, 85, 0.25);
}
.peach-passkey-delete-confirm {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
background: rgba(229, 85, 85, 0.06);
border: 1px solid rgba(229, 85, 85, 0.2);
border-radius: 6px;
}
.peach-passkey-delete-confirm-copy {
flex: 1;
font-size: 11px;
color: var(--peach-t1);
}
.peach-passkey-delete-btn--confirm {
color: var(--peach-breach);
border-color: rgba(229, 85, 85, 0.4);
}
```
### Fix — `PasskeyEntry.tsx`: inline badge in domain row
Replace the inline badge style:
```tsx
/* BEFORE */
passkey
/* AFTER */
passkey
```
Replace the inline delete button style:
```tsx
/* BEFORE */
/* AFTER */
```
The `.peach-restore-warning` used for the "private key stored encrypted" notice is semantically wrong — that class was designed for destructive action warnings. Replace it with the new `.peach-info-notice` (defined below in Issue 7).
---
## Issue 6 — `Vault.tsx`: passkey badge inline styles
The passkey badge in the vault list uses a long inline style string. Replace with the same `.peach-passkey-badge` class defined above:
```tsx
/* BEFORE */
passkey
/* AFTER */
passkey
```
---
## Issue 7 — `Settings.tsx`: semantic misuse of `.peach-restore-warning` and inline styles
### `.peach-restore-warning` misused for non-destructive notices
`.peach-restore-warning` has a red-tinted border and background — visually communicating danger. It is used in Settings for:
1. "Only one passkey provider can be active at a time" — informational, not dangerous
2. The passkey private key storage notice — informational
These should use a new neutral notice class.
### New CSS class — add to `popup.css`
```css
/* ── Info notice (neutral, non-destructive) ───────────────── */
.peach-info-notice {
font-size: 11px;
color: var(--peach-t2);
line-height: 1.5;
background: rgba(240, 232, 216, 0.04);
border: 1px solid var(--peach-t3);
border-radius: 6px;
padding: 8px 10px;
}
```
Replace the passkey provider conflict notice and the private key storage notice with `class="peach-info-notice"` instead of `class="peach-restore-warning"`.
### Sync section: color literals and inline styles
The sync section in `Settings.tsx` uses:
- Literal color strings: `"#5e5"`, `"#dd0"`, `"#888"`, `"#e55"` in `getStatusColor()`
- Inline `style="..."` on every device and network row
**Fix for `getStatusColor()`** — replace literals with CSS vars defined in `vars.css`:
Add to `vars.css`:
```css
--peach-sync-ok: #5ebd5e;
--peach-sync-busy: #c8a840;
--peach-sync-off: var(--peach-t2);
--peach-sync-err: var(--peach-breach);
```
Update `getStatusColor()`:
```ts
function getStatusColor(status: SyncStatus): string {
switch (status) {
case "synced": return "var(--peach-sync-ok)";
case "syncing": return "var(--peach-sync-busy)";
case "no_peers": return "var(--peach-sync-off)";
case "not_trusted": return "var(--peach-sync-err)";
case "offline": return "var(--peach-sync-off)";
default: return "var(--peach-sync-off)";
}
}
```
**New CSS classes for sync device/network rows** — add to `popup.css`:
```css
/* ── Settings sync rows ───────────────────────────────────── */
.peach-sync-section-label {
font-size: 11px;
color: var(--peach-t2);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.peach-sync-device-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid var(--peach-t3);
}
.peach-sync-device-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.peach-sync-device-name {
font-size: 13px;
color: var(--peach-t0);
}
.peach-sync-device-meta {
font-size: 11px;
color: var(--peach-t2);
}
.peach-sync-status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
vertical-align: middle;
}
.peach-sync-remove-btn {
color: var(--peach-breach);
}
```
**Fix — `Settings.tsx`**: Replace all inline-styled device/network row elements with these classes. The `style={...}` template literals on device rows become:
```tsx
{device.displayName}
Last seen: {formatRelativeTime(device.lastSeenAt)} · Last synced: {formatRelativeTime(device.lastSyncAt)}
```
The status row inline dot becomes:
```tsx
```
(The single `style={{ background }}` is acceptable here since it's driven by a dynamic value. All structural layout moves to CSS.)
---
## Issue 8 — `Entry.tsx`: regen panel inline styles
The regen panel, breach badge, canary badge, and canary suppress row all use inline styles. These should be CSS classes.
### New CSS classes — add to `popup.css`
```css
/* ── Breach badge ─────────────────────────────────────────── */
.peach-breach-badge {
display: flex;
position: relative;
z-index: 1;
align-items: center;
gap: 8px;
font-size: 11px;
color: var(--peach-breach);
padding: 6px 0;
margin-bottom: 4px;
}
/* ── Canary badge ─────────────────────────────────────────── */
.peach-canary-badge {
display: flex;
position: relative;
z-index: 1;
align-items: center;
gap: 5px;
padding: 4px 10px;
background: rgba(217, 160, 48, 0.12);
border: 1px solid rgba(217, 160, 48, 0.35);
border-radius: 6px;
margin-bottom: 6px;
width: fit-content;
}
.peach-canary-badge-label {
font-size: 10px;
letter-spacing: 0.06em;
color: rgba(217, 160, 48, 0.9);
font-family: var(--font-ui);
text-transform: uppercase;
}
/* ── Canary suppress row ──────────────────────────────────── */
.peach-canary-suppress-row {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
background: rgba(217, 160, 48, 0.08);
border-radius: 6px;
margin-bottom: 8px;
}
.peach-canary-suppress-label {
font-size: 11px;
color: rgba(217, 160, 48, 0.85);
cursor: pointer;
font-family: var(--font-ui);
letter-spacing: 0.04em;
}
/* ── Regen panel ──────────────────────────────────────────── */
.peach-regen-panel {
position: relative;
z-index: 1;
background: rgba(217, 104, 48, 0.08);
border: 1px solid rgba(217, 104, 48, 0.25);
border-radius: 8px;
padding: 12px;
margin-bottom: 8px;
}
.peach-regen-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.peach-regen-panel-title {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: rgba(217, 104, 48, 0.9);
}
.peach-regen-output {
background: rgba(0, 0, 0, 0.2);
border-radius: 6px;
padding: 10px;
margin-bottom: 10px;
font-family: var(--font-mono);
font-size: 13px;
word-break: break-all;
color: var(--peach-t1);
}
.peach-regen-actions {
display: flex;
gap: 6px;
}
.peach-regen-success {
position: relative;
z-index: 1;
background: rgba(40, 180, 80, 0.1);
border: 1px solid rgba(40, 180, 80, 0.3);
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
color: rgba(40, 180, 80, 0.9);
}
```
**Fix — `Entry.tsx`**: replace inline styles with these classes. The most important one is `peach-regen-output` — it currently uses `font-family:monospace` (generic), which must become `var(--font-mono)` (IBM Plex Mono).
---
## Issue 9 — `Lock.tsx`: restore link wrapper inline style
The restore link buttons under the onboarding create-vault panel are wrapped in a div with inline style:
```tsx
```
### New CSS class — add to `popup.css`
```css
.peach-onboarding-alt-actions {
display: flex;
flex-direction: column;
gap: 2px;
padding: 0 2px;
}
```
### Fix — `Lock.tsx`
```tsx
/* BEFORE */
...
/* AFTER */
...
```
---
## Issue 10 — `Export.tsx`: password input class mismatch
`Export.tsx` uses `peach-search-input` for the export password field and CJK description text:
```tsx
PassphraseScreen`:
```tsx
0}>
{STRENGTH_LABEL[props.strengthTier]}
```
### Warning block
Both `CJKExport.tsx` and `Export.tsx` use an inline-styled red notice block. Add:
```css
.peach-danger-notice {
background: rgba(220, 80, 60, 0.08);
border: 1px solid rgba(220, 80, 60, 0.2);
border-radius: 6px;
padding: 9px 11px;
font-size: 11px;
color: var(--peach-t1);
line-height: 1.5;
}
```
Replace:
```tsx
```
with:
```tsx
```
### Spinner (generating/restoring screens)
Both CJK views declare `@keyframes peach-spin` inline inside a `