peach_faithful_spec.md

35.4 KB · raw

rendered · source

1# Peach — Every Surface Faithful to the Demo2_v1.4.1 target_34---56## Purpose78Every 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.910The demo's design language is established and non-negotiable:11- **Cormorant Garamond** (italic, weight 300–400) for display text and numbers12- **IBM Plex Mono** (weight 300–400) for codes, passwords, mono values13- **Inter Variable** for all UI chrome, labels, buttons14- Amber bone palette: `--peach-accent: #D96830`, `--t0: #F0E8D8`, `--t1`, `--t2`, `--t3` scale15- Zero `border-radius` on the popup root shell; small radii (6–12px) inside surfaces only16- Muted amber glow on interactive elements; no harsh white or saturated colors17- All copy lowercase or sentence-case — never ALL-CAPS labels except in `.peach-label`1819---2021## Files Modified2223| File | Type |24|------|------|25| `src/popup/styles/popup.css` | CSS additions + fixes |26| `src/popup/views/Entry.tsx` | TOTP ring trigger; regen panel CSS classes |27| `src/popup/views/PasskeyApproval.tsx` | Full inline-style → CSS class migration |28| `src/popup/views/PasskeyEntry.tsx` | `window.confirm()` replacement; badge CSS |29| `src/popup/views/Vault.tsx` | Passkey badge CSS class |30| `src/popup/views/Settings.tsx` | `.peach-info-notice`, sync row CSS, color literal fix |31| `src/popup/views/Export.tsx` | Input class fix |32| `src/popup/views/CJKExport.tsx` | Inline style → CSS class migration |33| `src/popup/views/CJKImport.tsx` | Inline style → CSS class migration |34| `src/popup/views/Lock.tsx` | Restore link wrapper CSS class |35| `src/popup/components/TOTP.tsx` | Class rename to `peach-` prefix |3637**No changes to `detect.ts`, `inject.ts`, `fill.ts`, `save.ts`, background files, or the Rust sync host.**3839---4041## Issue 1 — TOTP font: missing `font-style: italic`4243### Root cause4445`popup.css` declares:46```css47.peach-totp-code {48  font-family: "Cormorant Garamond", "Times New Roman", serif;49  font-size: 38px; color: var(--peach-accent);50  line-height: 0.95; min-height: 1em;51}52```5354No `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.5556### Fix — `popup.css`5758```css59/* BEFORE */60.peach-totp-code {61  margin: 2px 0 0;62  font-family: "Cormorant Garamond", "Times New Roman", serif;63  font-size: 38px; color: var(--peach-accent);64  line-height: 0.95; min-height: 1em;65}6667/* AFTER */68.peach-totp-code {69  margin: 2px 0 0;70  font-family: "Cormorant Garamond", "Times New Roman", serif;71  font-style: italic;72  font-weight: 300;73  font-size: 38px; color: var(--peach-accent);74  line-height: 0.95; min-height: 1em;75}76```7778`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.7980---8182## Issue 2 — TOTP ring does not fire on code refresh8384### Root cause8586`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).8788### Fix — `Entry.tsx`8990Add 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.9192```tsx93// After the existing totpState memo, add:9495// Trigger the accent ring every time the TOTP code changes (new period)96let prevTotpCode: string | undefined;97createEffect(() => {98  const state = totpState();99  if (!state) { prevTotpCode = undefined; return; }100  if (prevTotpCode !== undefined && state.code !== prevTotpCode) {101    triggerRing();102  }103  prevTotpCode = state.code;104});105```106107This 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.108109---110111## Issue 3 — `TOTP.tsx` standalone component has unstyled class names112113The 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.114115### Fix — `TOTP.tsx`116117Rename classes to the established `peach-` convention:118119```tsx120export function TOTPDisplay(props: TOTPProps) {121  const [totp, setTotp] = createSignal(generateTOTP(props.secret));122  const interval = setInterval(() => { setTotp(generateTOTP(props.secret)); }, 1000);123  onCleanup(() => clearInterval(interval));124125  return (126    <div class="peach-totp-band">127      <div>128        <span class="peach-label">2-factor</span>129        <p class="peach-totp-code">130          <span class="peach-totp-code-text">131            {totp().code.slice(0, 3)} {totp().code.slice(3)}132          </span>133        </p>134      </div>135      <div class="peach-totp-meta">136        <span class="peach-label">expires</span>137        <div class="peach-totp-countdown">{totp().remaining}s</div>138      </div>139    </div>140  );141}142```143144This reuses the exact same structure and CSS classes as the full `Entry.tsx` TOTP band, ensuring visual consistency wherever this simpler component is used.145146---147148## Issue 4 — `PasskeyApproval.tsx`: all layout via inline styles149150### Root cause151152Every 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.153154### New CSS classes — add to `popup.css` after the existing overlay section155156```css157/* ── Passkey approval overlay ─────────────────────────────── */158.peach-passkey-approval {159  display: flex;160  flex-direction: column;161  align-items: center;162  padding: 28px 22px 22px;163  gap: 14px;164  text-align: center;165}166167.peach-passkey-approval-icon {168  width: 46px;169  height: 46px;170  border-radius: 12px;171  background: var(--peach-s2);172  border: 1px solid var(--peach-t3);173  display: flex;174  align-items: center;175  justify-content: center;176  flex-shrink: 0;177}178179.peach-passkey-approval-icon img,180.peach-passkey-approval-icon svg {181  width: 28px;182  height: 28px;183  border-radius: 6px;184}185186.peach-passkey-approval-rp {187  font-family: var(--font-ui);188  font-size: 12px;189  color: var(--peach-t2);190  letter-spacing: 0.03em;191}192193.peach-passkey-approval-action {194  font-family: var(--font-display);195  font-style: italic;196  font-weight: 300;197  font-size: 26px;198  color: var(--peach-t0);199  line-height: 1;200}201202.peach-passkey-approval-user {203  font-family: var(--font-mono);204  font-size: 11px;205  color: var(--peach-t1);206  background: var(--peach-s2);207  border: 1px solid var(--peach-t3);208  border-radius: 4px;209  padding: 3px 8px;210}211212.peach-passkey-approval-notice {213  font-size: 10px;214  color: var(--peach-t2);215  line-height: 1.5;216  max-width: 220px;217}218219.peach-passkey-approval-actions {220  display: flex;221  gap: 8px;222  width: 100%;223  margin-top: 6px;224}225226.peach-passkey-approval-actions .peach-btn {227  flex: 1;228}229230/* Ensure peach-overlay-card is defined (also used in Entry.tsx QR modal) */231.peach-overlay-card {232  background: var(--peach-s1);233  border: 1px solid var(--peach-t3);234  border-radius: 12px;235  overflow: hidden;236  width: 100%;237}238```239240### Fix — `PasskeyApproval.tsx`241242Replace the inline-styled structure with the new classes:243244```tsx245<div class="peach-overlay-view" role="dialog" aria-modal="true">246  <div class="peach-overlay-card">247    <div class="peach-passkey-approval">248249      <div class="peach-passkey-approval-icon">250        <Show when={props.request.iconUrl} fallback={<PasskeyGlyph />}>251          <img src={props.request.iconUrl} alt={props.request.rpId} />252        </Show>253      </div>254255      <div class="peach-passkey-approval-rp">{props.request.rpId}</div>256257      <div class="peach-passkey-approval-action">258        {props.request.type === "create" ? "create passkey" : "sign in"}259      </div>260261      <Show when={props.request.userName}>262        <div class="peach-passkey-approval-user">{props.request.userName}</div>263      </Show>264265      <div class="peach-passkey-approval-notice">266        {props.request.type === "create"267          ? "Peach will generate and store a passkey for this site."268          : "Peach will authenticate you using your stored passkey."}269      </div>270271      <div class="peach-passkey-approval-actions">272        <button class="peach-btn peach-btn--ghost peach-btn--sm" onClick={props.onDeny}>273          Deny274        </button>275        <button class="peach-btn peach-btn--accent peach-btn--sm" onClick={props.onApprove}>276          {props.request.type === "create" ? "Create" : "Sign in"}277        </button>278      </div>279280    </div>281  </div>282</div>283```284285---286287## Issue 5 — `PasskeyEntry.tsx`: `window.confirm()` for delete288289### Root cause290291`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.292293### Fix — In-popup confirmation pattern294295Add a `deleteConfirmPending` signal. When the user clicks "Remove passkey", show an inline confirmation row instead of calling `window.confirm()`:296297```tsx298const [deleteConfirmPending, setDeleteConfirmPending] = createSignal(false);299300// Replace the delete button with:301<Show302  when={deleteConfirmPending()}303  fallback={304    <button305      class="peach-btn peach-btn--ghost peach-btn--sm peach-passkey-delete-btn"306      onClick={() => setDeleteConfirmPending(true)}307    >308      Remove passkey309    </button>310  }311>312  <div class="peach-passkey-delete-confirm">313    <span class="peach-passkey-delete-confirm-copy">Remove this passkey?</span>314    <button315      class="peach-btn peach-btn--ghost peach-btn--sm"316      onClick={() => setDeleteConfirmPending(false)}317    >318      Cancel319    </button>320    <button321      class="peach-btn peach-btn--ghost peach-btn--sm peach-passkey-delete-btn--confirm"322      onClick={() => {323        setDeleteConfirmPending(false);324        props.onDelete();325      }}326    >327      Remove328    </button>329  </div>330</Show>331```332333### New CSS classes — add to `popup.css`334335```css336/* ── Passkey entry view ───────────────────────────────────── */337.peach-passkey-badge {338  margin-left: 8px;339  font-size: 10px;340  letter-spacing: 0.06em;341  text-transform: uppercase;342  color: var(--peach-t2);343  font-family: var(--font-ui);344  vertical-align: middle;345}346347.peach-passkey-delete-btn {348  color: var(--peach-breach);349  border-color: rgba(229, 85, 85, 0.25);350}351352.peach-passkey-delete-confirm {353  display: flex;354  align-items: center;355  gap: 8px;356  padding: 8px 10px;357  background: rgba(229, 85, 85, 0.06);358  border: 1px solid rgba(229, 85, 85, 0.2);359  border-radius: 6px;360}361362.peach-passkey-delete-confirm-copy {363  flex: 1;364  font-size: 11px;365  color: var(--peach-t1);366}367368.peach-passkey-delete-btn--confirm {369  color: var(--peach-breach);370  border-color: rgba(229, 85, 85, 0.4);371}372```373374### Fix — `PasskeyEntry.tsx`: inline badge in domain row375376Replace the inline badge style:377```tsx378/* BEFORE */379<span style="margin-left:8px;font-size:10px;...">passkey</span>380381/* AFTER */382<span class="peach-passkey-badge">passkey</span>383```384385Replace the inline delete button style:386```tsx387/* BEFORE */388<button style="color:#e55;border-color:rgba(229,85,85,0.25)">Remove passkey</button>389390/* AFTER */391<button class="peach-btn peach-btn--ghost peach-btn--sm peach-passkey-delete-btn">Remove passkey</button>392```393394The `.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).395396---397398## Issue 6 — `Vault.tsx`: passkey badge inline styles399400The passkey badge in the vault list uses a long inline style string. Replace with the same `.peach-passkey-badge` class defined above:401402```tsx403/* BEFORE */404<span style="margin-left:6px;font-size:10px;letter-spacing:0.06em;text-transform:uppercase;color:var(--t2)">passkey</span>405406/* AFTER */407<span class="peach-passkey-badge">passkey</span>408```409410---411412## Issue 7 — `Settings.tsx`: semantic misuse of `.peach-restore-warning` and inline styles413414### `.peach-restore-warning` misused for non-destructive notices415416`.peach-restore-warning` has a red-tinted border and background — visually communicating danger. It is used in Settings for:4171. "Only one passkey provider can be active at a time" — informational, not dangerous4182. The passkey private key storage notice — informational419420These should use a new neutral notice class.421422### New CSS class — add to `popup.css`423424```css425/* ── Info notice (neutral, non-destructive) ───────────────── */426.peach-info-notice {427  font-size: 11px;428  color: var(--peach-t2);429  line-height: 1.5;430  background: rgba(240, 232, 216, 0.04);431  border: 1px solid var(--peach-t3);432  border-radius: 6px;433  padding: 8px 10px;434}435```436437Replace the passkey provider conflict notice and the private key storage notice with `class="peach-info-notice"` instead of `class="peach-restore-warning"`.438439### Sync section: color literals and inline styles440441The sync section in `Settings.tsx` uses:442- Literal color strings: `"#5e5"`, `"#dd0"`, `"#888"`, `"#e55"` in `getStatusColor()`443- Inline `style="..."` on every device and network row444445**Fix for `getStatusColor()`** — replace literals with CSS vars defined in `vars.css`:446447Add to `vars.css`:448```css449--peach-sync-ok:   #5ebd5e;450--peach-sync-busy: #c8a840;451--peach-sync-off:  var(--peach-t2);452--peach-sync-err:  var(--peach-breach);453```454455Update `getStatusColor()`:456```ts457function getStatusColor(status: SyncStatus): string {458  switch (status) {459    case "synced":      return "var(--peach-sync-ok)";460    case "syncing":     return "var(--peach-sync-busy)";461    case "no_peers":    return "var(--peach-sync-off)";462    case "not_trusted": return "var(--peach-sync-err)";463    case "offline":     return "var(--peach-sync-off)";464    default:            return "var(--peach-sync-off)";465  }466}467```468469**New CSS classes for sync device/network rows** — add to `popup.css`:470471```css472/* ── Settings sync rows ───────────────────────────────────── */473.peach-sync-section-label {474  font-size: 11px;475  color: var(--peach-t2);476  text-transform: uppercase;477  letter-spacing: 0.05em;478}479480.peach-sync-device-row {481  display: flex;482  justify-content: space-between;483  align-items: center;484  padding: 6px 0;485  border-bottom: 1px solid var(--peach-t3);486}487488.peach-sync-device-info {489  display: flex;490  flex-direction: column;491  gap: 2px;492}493494.peach-sync-device-name {495  font-size: 13px;496  color: var(--peach-t0);497}498499.peach-sync-device-meta {500  font-size: 11px;501  color: var(--peach-t2);502}503504.peach-sync-status-dot {505  display: inline-block;506  width: 8px;507  height: 8px;508  border-radius: 50%;509  vertical-align: middle;510}511512.peach-sync-remove-btn {513  color: var(--peach-breach);514}515```516517**Fix — `Settings.tsx`**: Replace all inline-styled device/network row elements with these classes. The `style={...}` template literals on device rows become:518519```tsx520<div class="peach-sync-device-row">521  <div class="peach-sync-device-info">522    <span class="peach-sync-device-name">{device.displayName}</span>523    <span class="peach-sync-device-meta">524      Last seen: {formatRelativeTime(device.lastSeenAt)} · Last synced: {formatRelativeTime(device.lastSyncAt)}525    </span>526  </div>527  <button528    class="peach-btn peach-btn--ghost peach-btn--sm peach-sync-remove-btn"529    onClick={() => void handleRemoveDevice(device.deviceId)}530  >531    Remove532  </button>533</div>534```535536The status row inline dot becomes:537```tsx538<span539  class="peach-sync-status-dot"540  style={{ background: getStatusColor(syncStatus()) }}541/>542```543544(The single `style={{ background }}` is acceptable here since it's driven by a dynamic value. All structural layout moves to CSS.)545546---547548## Issue 8 — `Entry.tsx`: regen panel inline styles549550The regen panel, breach badge, canary badge, and canary suppress row all use inline styles. These should be CSS classes.551552### New CSS classes — add to `popup.css`553554```css555/* ── Breach badge ─────────────────────────────────────────── */556.peach-breach-badge {557  display: flex;558  position: relative;559  z-index: 1;560  align-items: center;561  gap: 8px;562  font-size: 11px;563  color: var(--peach-breach);564  padding: 6px 0;565  margin-bottom: 4px;566}567568/* ── Canary badge ─────────────────────────────────────────── */569.peach-canary-badge {570  display: flex;571  position: relative;572  z-index: 1;573  align-items: center;574  gap: 5px;575  padding: 4px 10px;576  background: rgba(217, 160, 48, 0.12);577  border: 1px solid rgba(217, 160, 48, 0.35);578  border-radius: 6px;579  margin-bottom: 6px;580  width: fit-content;581}582583.peach-canary-badge-label {584  font-size: 10px;585  letter-spacing: 0.06em;586  color: rgba(217, 160, 48, 0.9);587  font-family: var(--font-ui);588  text-transform: uppercase;589}590591/* ── Canary suppress row ──────────────────────────────────── */592.peach-canary-suppress-row {593  display: flex;594  align-items: center;595  gap: 8px;596  padding: 6px 10px;597  background: rgba(217, 160, 48, 0.08);598  border-radius: 6px;599  margin-bottom: 8px;600}601602.peach-canary-suppress-label {603  font-size: 11px;604  color: rgba(217, 160, 48, 0.85);605  cursor: pointer;606  font-family: var(--font-ui);607  letter-spacing: 0.04em;608}609610/* ── Regen panel ──────────────────────────────────────────── */611.peach-regen-panel {612  position: relative;613  z-index: 1;614  background: rgba(217, 104, 48, 0.08);615  border: 1px solid rgba(217, 104, 48, 0.25);616  border-radius: 8px;617  padding: 12px;618  margin-bottom: 8px;619}620621.peach-regen-panel-header {622  display: flex;623  align-items: center;624  justify-content: space-between;625  margin-bottom: 10px;626}627628.peach-regen-panel-title {629  font-size: 11px;630  font-weight: 600;631  letter-spacing: 0.06em;632  text-transform: uppercase;633  color: rgba(217, 104, 48, 0.9);634}635636.peach-regen-output {637  background: rgba(0, 0, 0, 0.2);638  border-radius: 6px;639  padding: 10px;640  margin-bottom: 10px;641  font-family: var(--font-mono);642  font-size: 13px;643  word-break: break-all;644  color: var(--peach-t1);645}646647.peach-regen-actions {648  display: flex;649  gap: 6px;650}651652.peach-regen-success {653  position: relative;654  z-index: 1;655  background: rgba(40, 180, 80, 0.1);656  border: 1px solid rgba(40, 180, 80, 0.3);657  border-radius: 8px;658  padding: 10px 12px;659  margin-bottom: 8px;660  display: flex;661  align-items: center;662  gap: 8px;663  font-size: 11px;664  color: rgba(40, 180, 80, 0.9);665}666```667668**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).669670---671672## Issue 9 — `Lock.tsx`: restore link wrapper inline style673674The restore link buttons under the onboarding create-vault panel are wrapped in a div with inline style:675```tsx676<div style="display:flex;flex-direction:column;gap:2px;padding:0 2px;">677```678679### New CSS class — add to `popup.css`680681```css682.peach-onboarding-alt-actions {683  display: flex;684  flex-direction: column;685  gap: 2px;686  padding: 0 2px;687}688```689690### Fix — `Lock.tsx`691692```tsx693/* BEFORE */694<div style="display:flex;flex-direction:column;gap:2px;padding:0 2px;">695  ...696</div>697698/* AFTER */699<div class="peach-onboarding-alt-actions">700  ...701</div>702```703704---705706## Issue 10 — `Export.tsx`: password input class mismatch707708`Export.tsx` uses `peach-search-input` for the export password field and CJK description text:709```tsx710<input class="peach-search-input" style="width:100%;box-sizing:border-box;padding:8px 10px;..."711```712713`.peach-search-input` is the vault search bar — it's styled as an italic Cormorant Garamond field with no border. Using it for a password input with additional inline overrides is wrong.714715### Fix — `Export.tsx` and `CJKExport.tsx` / `CJKImport.tsx`716717Replace all instances of `peach-search-input` used for password/text inputs with a proper class:718719### New CSS class — add to `popup.css`720721```css722/* ── Settings-context text/password input ─────────────────── */723.peach-settings-input {724  width: 100%;725  box-sizing: border-box;726  padding: 8px 10px;727  background: var(--peach-s2);728  border: 1px solid var(--peach-t3);729  border-radius: 6px;730  color: var(--peach-t0);731  font-family: var(--font-ui);732  font-size: 12px;733  outline: none;734  transition: border-color 120ms ease;735}736737.peach-settings-input:focus {738  border-color: var(--peach-accent);739}740741.peach-settings-input[type="password"] {742  font-family: var(--font-mono);743  letter-spacing: 0.04em;744}745```746747Replace all:748```tsx749class="peach-search-input" style="width:100%;box-sizing:border-box;padding:8px 10px;background:var(--s2);border:1px solid var(--t3);border-radius:6px;color:var(--t0);font-size:12px"750```751with:752```tsx753class="peach-settings-input"754```755756This applies to `Export.tsx` (export password), `CJKExport.tsx` (passphrase + confirm), and `CJKImport.tsx` (passphrase + master password inputs).757758---759760## Issue 11 — `CJKExport.tsx` and `CJKImport.tsx`: inline-styled shells761762### Strength meter763764The strength meter bar in `CJKExport.tsx` is built with inline styles. Add CSS classes:765766```css767/* ── CJK export passphrase strength ──────────────────────── */768.peach-strength-row {769  display: flex;770  align-items: center;771  gap: 8px;772  margin-bottom: 10px;773}774775.peach-strength-track {776  flex: 1;777  height: 4px;778  background: var(--peach-s2);779  border-radius: 2px;780  overflow: hidden;781}782783.peach-strength-fill {784  height: 100%;785  border-radius: 2px;786  transition: width 120ms ease, background 120ms ease;787}788789.peach-strength-label {790  font-size: 10px;791  font-weight: 500;792  min-width: 64px;793  text-align: right;794}795```796797Replace the inline strength meter in `CJKExport.tsx > PassphraseScreen`:798799```tsx800<Show when={props.passphrase.length > 0}>801  <div class="peach-strength-row">802    <div class="peach-strength-track">803      <div804        class="peach-strength-fill"805        style={{806          width: `${barFillPercent()}%`,807          background: STRENGTH_COLOUR[props.strengthTier],808        }}809      />810    </div>811    <span812      class="peach-strength-label"813      style={{ color: STRENGTH_COLOUR[props.strengthTier] }}814    >815      {STRENGTH_LABEL[props.strengthTier]}816    </span>817  </div>818</Show>819```820821### Warning block822823Both `CJKExport.tsx` and `Export.tsx` use an inline-styled red notice block. Add:824825```css826.peach-danger-notice {827  background: rgba(220, 80, 60, 0.08);828  border: 1px solid rgba(220, 80, 60, 0.2);829  border-radius: 6px;830  padding: 9px 11px;831  font-size: 11px;832  color: var(--peach-t1);833  line-height: 1.5;834}835```836837Replace:838```tsx839<div style="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(--t1);margin:12px 0">840```841with:842```tsx843<div class="peach-danger-notice" style="margin:12px 0">844```845846### Spinner (generating/restoring screens)847848Both CJK views declare `@keyframes peach-spin` inline inside a `<style>` tag inside the component. This duplicates the animation and pollutes the style sheet at runtime. Extract to `popup.css`:849850```css851/* ── Spinner (shared) ─────────────────────────────────────── */852@keyframes peach-spin {853  to { transform: rotate(360deg); }854}855856.peach-spinner {857  width: 32px;858  height: 32px;859  border: 2px solid var(--peach-s2);860  border-top-color: var(--peach-accent);861  border-radius: 50%;862  margin: 0 auto 16px;863  animation: peach-spin 0.8s linear infinite;864}865866.peach-progress-screen {867  text-align: center;868  padding: 32px 16px;869}870871.peach-progress-screen-title {872  font-size: 13px;873  color: var(--peach-t0);874  margin-bottom: 16px;875}876877.peach-progress-screen-stage {878  font-size: 11px;879  color: var(--peach-t2);880  min-height: 14px;881}882883.peach-progress-screen-note {884  font-size: 10px;885  color: var(--peach-t2);886  margin-top: 8px;887}888```889890Remove the inline `<style>` blocks from `GeneratingScreen` and `RestoringScreen`. Replace the spinner `<div>` and surrounding layout with:891892```tsx893function GeneratingScreen(props: GeneratingScreenProps) {894  return (895    <div class="peach-settings-group peach-progress-screen">896      <div class="peach-progress-screen-title">Creating your backup phrase…</div>897      <div class="peach-spinner" aria-label="Working" />898      <div class="peach-progress-screen-stage">{PROGRESS_COPY[props.stage]}</div>899      <Show when={props.highSecurity}>900        <div class="peach-progress-screen-note">Using High Security — this takes about 3 seconds.</div>901      </Show>902    </div>903  );904}905```906907### CJK QR overlay908909The QR overlay in `CJKExport.tsx` is entirely inline-styled. Replace with the `.peach-overlay-view` + `.peach-overlay-card` pattern already used throughout the extension:910911```tsx912<Show when={props.showQrOverlay && props.qrDataUrl}>913  <div class="peach-overlay-view" role="dialog" aria-modal="true" onClick={props.onCloseQr}>914    <div class="peach-overlay-card peach-cjk-qr-card" onClick={(e) => e.stopPropagation()}>915      <div class="peach-overlay-header">916        <span class="peach-overlay-title">scan with Peach</span>917        <div class="peach-overlay-spacer" />918        <button class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn" onClick={props.onCloseQr} aria-label="Close">919          <CloseGlyph />920        </button>921      </div>922      <div class="peach-cjk-qr-body">923        <p class="peach-cjk-qr-hint">Open Peach → Import → Restore from CJK phrase</p>924        <img src={props.qrDataUrl!} alt="Vault QR code" class="peach-cjk-qr-image" width="320" height="320" />925      </div>926    </div>927  </div>928</Show>929```930931New CSS:932```css933.peach-cjk-qr-card {934  max-width: 400px;935  margin: auto;936}937938.peach-cjk-qr-body {939  padding: 0 20px 20px;940  text-align: center;941}942943.peach-cjk-qr-hint {944  font-size: 11px;945  color: var(--peach-t2);946  margin: 0 0 14px;947}948949.peach-cjk-qr-image {950  border-radius: 4px;951  display: block;952  margin: 0 auto;953  width: 100%;954  max-width: 320px;955  height: auto;956}957```958959---960961## Issue 12 — `SyncPairing.tsx`: verify `peach-pairing-*` CSS coverage962963`SyncPairing.tsx` is well-structured — it uses `peach-pairing-*` class names throughout with no inline styles. Verify that all referenced classes exist in `popup.css`:964965| Class | Expected |966|-------|----------|967| `.peach-pairing-code-display` | Container for the 6-digit pairing code |968| `.peach-pairing-code-digit` | Individual digit cell — mono, large |969| `.peach-pairing-countdown-ring` | SVG ring wrapper |970| `.peach-pairing-ring-track` | SVG circle background |971| `.peach-pairing-ring-progress` | SVG circle fill |972| `.peach-pairing-countdown-num` | Countdown number overlay |973| `.peach-pairing-content` | Centered flex column |974| `.peach-pairing-body` | Scrollable body |975| `.peach-pairing-headline` | Large display text |976| `.peach-pairing-subline` | Secondary copy |977| `.peach-pairing-status` | Spinner + status text row |978| `.peach-pairing-error` | Error text |979| `.peach-pairing-error-msg` | Error message paragraph |980| `.peach-pairing-code-inputs` | 6-box code input grid |981| `.peach-pairing-code-box` | Individual code input |982| `.peach-pairing-spinner` | CSS spinner |983| `.peach-pairing-success-icon` | Success checkmark wrapper |984| `.peach-pairing-error-icon` | Error X wrapper |985986If any of these are missing from `popup.css`, add them. The critical ones for visual coherence:987988```css989/* ── Sync pairing ─────────────────────────────────────────── */990.peach-pairing-body {991  flex: 1;992  overflow-y: auto;993  display: flex;994  flex-direction: column;995}996997.peach-pairing-content {998  flex: 1;999  display: flex;1000  flex-direction: column;1001  align-items: center;1002  justify-content: center;1003  gap: 14px;1004  padding: 28px 20px;1005  text-align: center;1006}10071008.peach-pairing-headline {1009  font-family: var(--font-display);1010  font-style: italic;1011  font-weight: 300;1012  font-size: 28px;1013  color: var(--peach-t0);1014  margin: 0;1015  line-height: 1;1016}10171018.peach-pairing-subline {1019  font-size: 12px;1020  color: var(--peach-t2);1021  max-width: 220px;1022  line-height: 1.5;1023}10241025.peach-pairing-subline--center {1026  text-align: center;1027}10281029.peach-pairing-code-display {1030  display: flex;1031  gap: 6px;1032}10331034.peach-pairing-code-digit {1035  display: flex;1036  align-items: center;1037  justify-content: center;1038  width: 36px;1039  height: 48px;1040  background: var(--peach-s2);1041  border: 1px solid var(--peach-t3);1042  border-radius: 6px;1043  font-family: var(--font-mono);1044  font-size: 22px;1045  color: var(--peach-t0);1046  letter-spacing: 0;1047}10481049.peach-pairing-countdown-ring {1050  position: relative;1051  display: inline-flex;1052  align-items: center;1053  justify-content: center;1054}10551056.peach-pairing-countdown-num {1057  position: absolute;1058  font-family: var(--font-mono);1059  font-size: 14px;1060  color: var(--peach-t1);1061}10621063.peach-pairing-ring-track {1064  fill: none;1065  stroke: var(--peach-t3);1066  stroke-width: 2;1067}10681069.peach-pairing-ring-progress {1070  fill: none;1071  stroke: var(--peach-accent);1072  stroke-width: 2;1073  stroke-linecap: round;1074  transform: rotate(-90deg);1075  transform-origin: 50% 50%;1076  transition: stroke-dashoffset 0.5s linear;1077}10781079.peach-pairing-code-inputs {1080  display: flex;1081  gap: 8px;1082}10831084.peach-pairing-code-box {1085  width: 36px;1086  height: 48px;1087  text-align: center;1088  background: var(--peach-s2);1089  border: 1px solid var(--peach-t3);1090  border-radius: 6px;1091  font-family: var(--font-mono);1092  font-size: 20px;1093  color: var(--peach-t0);1094  outline: none;1095  transition: border-color 120ms ease;1096}10971098.peach-pairing-code-box:focus {1099  border-color: var(--peach-accent);1100}11011102.peach-pairing-code-box.has-error {1103  border-color: var(--peach-breach);1104}11051106.peach-pairing-status {1107  display: flex;1108  align-items: center;1109  gap: 8px;1110  font-size: 13px;1111  color: var(--peach-t1);1112}11131114.peach-pairing-error {1115  font-size: 11px;1116  color: var(--peach-breach);1117}11181119.peach-pairing-error-msg {1120  color: var(--peach-breach);1121}11221123.peach-pairing-success-icon,1124.peach-pairing-error-icon {1125  display: flex;1126  align-items: center;1127  justify-content: center;1128}11291130.peach-pairing-spinner {1131  width: 20px;1132  height: 20px;1133  border: 1.5px solid var(--peach-t3);1134  border-top-color: var(--peach-accent);1135  border-radius: 50%;1136  animation: peach-spin 0.8s linear infinite;1137  flex-shrink: 0;1138}1139```11401141---11421143## Issue 13 — `GeneratorDrawer.tsx`: verify CSS coverage11441145`GeneratorDrawer.tsx` uses `peach-generator-drawer`, `peach-generator-drawer-inner`, `peach-generator-drawer-content`, `peach-pwgen-display-row`, `peach-pwgen-output`, `peach-pwgen-length-row`, `peach-pwgen-slider`, `peach-pwgen-len-num`, `peach-pwgen-toggles`, `peach-pwgen-toggle-row`, `peach-pwgen-toggle-label`. Verify all are in `popup.css`. The drawer animation uses `grid-template-rows: 0fr → 1fr` for the smooth open. Confirm:11461147```css1148.peach-generator-drawer {1149  display: grid;1150  grid-template-rows: 0fr;1151  transition: grid-template-rows 240ms var(--ease-out);1152}11531154.peach-generator-drawer.is-open {1155  grid-template-rows: 1fr;1156}11571158.peach-generator-drawer-inner {1159  overflow: hidden;1160}1161```11621163The password output (`peach-pwgen-output`) must use `var(--font-mono)`:11641165```css1166.peach-pwgen-output {1167  font-family: var(--font-mono);1168  font-size: 13px;1169  color: var(--peach-t0);1170  word-break: break-all;1171  flex: 1;1172}1173```11741175---11761177## Summary of CSS additions to `popup.css`11781179| New class | Purpose |1180|-----------|---------|1181| `.peach-passkey-approval` | Passkey approval overlay layout |1182| `.peach-passkey-approval-icon` | Icon wrapper with border |1183| `.peach-passkey-approval-rp` | RP domain text |1184| `.peach-passkey-approval-action` | Large italic action verb |1185| `.peach-passkey-approval-user` | Mono username chip |1186| `.peach-passkey-approval-notice` | Informational fine print |1187| `.peach-passkey-approval-actions` | Button row |1188| `.peach-overlay-card` | Shared overlay card shell (if missing) |1189| `.peach-passkey-badge` | "passkey" chip in Vault + PasskeyEntry |1190| `.peach-passkey-delete-btn` | Red-tinted delete trigger |1191| `.peach-passkey-delete-confirm` | Inline confirm row |1192| `.peach-passkey-delete-confirm-copy` | Confirm text |1193| `.peach-passkey-delete-btn--confirm` | Confirm action button |1194| `.peach-info-notice` | Neutral informational notice |1195| `.peach-sync-section-label` | Devices/networks section label |1196| `.peach-sync-device-row` | Device/network row |1197| `.peach-sync-device-info` | Device name + meta column |1198| `.peach-sync-device-name` | Device name text |1199| `.peach-sync-device-meta` | Last seen / last synced text |1200| `.peach-sync-status-dot` | Status indicator dot |1201| `.peach-sync-remove-btn` | Red remove button |1202| `.peach-breach-badge` | Breach warning row in Entry |1203| `.peach-canary-badge` | Canary indicator |1204| `.peach-canary-badge-label` | Canary label text |1205| `.peach-canary-suppress-row` | Session suppress checkbox row |1206| `.peach-canary-suppress-label` | Suppress label text |1207| `.peach-regen-panel` | Password regen overlay |1208| `.peach-regen-panel-header` | Regen header row |1209| `.peach-regen-panel-title` | Regen title text |1210| `.peach-regen-output` | Password preview (IBM Plex Mono) |1211| `.peach-regen-actions` | Regen button row |1212| `.peach-regen-success` | Success confirmation row |1213| `.peach-onboarding-alt-actions` | Restore link column on onboarding |1214| `.peach-settings-input` | Settings-context text/password input |1215| `.peach-strength-row` | Passphrase strength meter row |1216| `.peach-strength-track` | Strength bar background |1217| `.peach-strength-fill` | Strength bar fill |1218| `.peach-strength-label` | Strength tier label |1219| `.peach-danger-notice` | Red-tinted destructive warning block |1220| `.peach-spinner` | Loading spinner |1221| `.peach-progress-screen` | Centered progress screen wrapper |1222| `.peach-progress-screen-title` | Progress title |1223| `.peach-progress-screen-stage` | Stage label |1224| `.peach-progress-screen-note` | Supplemental note |1225| `.peach-cjk-qr-card` | CJK QR overlay card width |1226| `.peach-cjk-qr-body` | QR body padding |1227| `.peach-cjk-qr-hint` | QR hint text |1228| `.peach-cjk-qr-image` | QR image |1229| `@keyframes peach-spin` | Shared spinner keyframe (extracted from JSX) |1230| `.peach-pairing-*` (11 classes) | Full SyncPairing visual coverage |12311232---12331234## Summary of `vars.css` additions12351236```css1237--peach-sync-ok:   #5ebd5e;1238--peach-sync-busy: #c8a840;1239--peach-sync-off:  var(--peach-t2);1240--peach-sync-err:  var(--peach-breach);1241```12421243---12441245## What this does NOT change12461247- The autofill selector, passkey proxy, or any content script1248- The vault encryption or crypto layer1249- The Ring.tsx component itself (it works; we're only adding the TOTP trigger)1250- Health.tsx (correctly styled with `peach-health-*` classes; no changes needed)1251- Vault.tsx list layout (correctly styled; only the passkey badge inline style changes)1252- The overall layout shell, nav, overlay entry/exit animations