3f5891a8b076

onboarding cleanup: drop the bird-pair avatars from AdoptVaultView (pair and success phases) and PairDevice — the two circles on a bar looked like testicles. Trashed PairingBirds.tsx, removed its first CSS block. Drop the 'Already have a backup?' section from Lock.tsx — the welcome screen already asks, and the test for it. (v4.3.10)

time
2026-07-28 01:38 UTC · 3 months ago
machine
koh2
agent
claude-MiniMax-M3
session
sync false-failure and bug trio
parent
58c7a0bdb8f3
10 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/components/PairingBirds.tsx+0 −161src/popup/styles/popup.css+0 −264src/popup/views/AdoptVaultView.tsx+0 −8src/popup/views/Lock.tsx+0 −32src/popup/views/PairDevice.test.tsx+0 −1src/popup/views/PairDevice.tsx+0 −15src/popup/views/contracts.test.tsx+0 −38
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "4.3.9",4+  "version": "4.3.10",55   "description": "Zero-knowledge password manager",66   "browser_specific_settings": {77     "gecko": {

view file

manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "4.3.9",4+  "version": "4.3.10",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "4.3.9",3+  "version": "4.3.10",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/popup/components/PairingBirds.tsx+0 −161
@@ -1,161 +0,0 @@1-// Pairing birds — the two illustrated characters used on the pairing screens.2-//3-// These are all that remains of the removed audio-pairing animation: the flow4-// itself went with it, but the birds outlived it and still animate above the5-// code-entry, connecting, SAS and paired screens (PairDevice.tsx's bird stage).6-// The `-chirping` pose names are kept on purpose — the bird visibly chirps.7-8-import { Component } from "solid-js";9-10-const FINCH = {11-  hi: "#f4a6b6", mid: "#dc8294", lo: "#a85870", shade: "#7a3e54",12-  cream: "#f5d8c8", beak: "#f5c764", beakShade: "#c8932e", legs: "#7a3e54",13-};14-const SWALLOW = {15-  hi: "#f4c8a0", mid: "#dc9968", lo: "#a86b3e", shade: "#7a4a26",16-  cream: "#f7e0c8", beak: "#f5c764", beakShade: "#b87a22", legs: "#5e3a1f",17-};18-19-export const DesktopBird: Component<{ pose: string }> = (props) => {20-  const p = FINCH;21-  return (22-    <div class={`peach-pair-bird peach-pair-bird-desktop peach-pair-bird-pose-${props.pose}`}>23-      <svg viewBox="0 0 160 150" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMax meet">24-        <defs>25-          <radialGradient id="peach-pair-finch-body" cx="0.42" cy="0.32" r="0.72">26-            <stop offset="0%" stop-color={p.hi} />27-            <stop offset="55%" stop-color={p.mid} />28-            <stop offset="100%" stop-color={p.lo} />29-          </radialGradient>30-          <radialGradient id="peach-pair-finch-head" cx="0.5" cy="0.4" r="0.7">31-            <stop offset="0%" stop-color={p.hi} />32-            <stop offset="100%" stop-color={p.mid} />33-          </radialGradient>34-          <linearGradient id="peach-pair-finch-belly" x1="0" y1="0" x2="0" y2="1">35-            <stop offset="0%" stop-color={p.cream} stop-opacity="0" />36-            <stop offset="50%" stop-color={p.cream} stop-opacity="0.85" />37-            <stop offset="100%" stop-color={p.cream} stop-opacity="0.5" />38-          </linearGradient>39-        </defs>40-        <ellipse class="peach-pair-bird-shadow" cx="78" cy="138" rx="40" ry="3.5" fill="rgba(0,0,0,0.45)" />41-        <g class="peach-pair-bird-aura" opacity="0">42-          <ellipse cx="76" cy="74" rx="60" ry="46" fill={p.hi} opacity="0.18" style={{ filter: "blur(8px)" }} />43-        </g>44-        <g class="peach-pair-bird-tail">45-          <path d="M 32 92 Q 8 90, 6 80 Q 10 95, 28 100 Z" fill={p.lo} />46-          <path d="M 32 95 Q 4 99, 4 92 Q 10 102, 28 102 Z" fill={p.shade} opacity="0.85" />47-          <path d="M 32 98 Q 8 108, 12 110 Q 20 110, 30 105 Z" fill={p.lo} opacity="0.9" />48-        </g>49-        <g class="peach-pair-bird-feet" stroke={p.legs} stroke-width="1.6" stroke-linecap="round" fill="none">50-          <line x1="62" y1="118" x2="60" y2="134" />51-          <path d="M 60 134 L 55 138 M 60 134 L 60 139 M 60 134 L 65 138" stroke-width="1.2" />52-          <line x1="84" y1="118" x2="86" y2="134" />53-          <path d="M 86 134 L 81 138 M 86 134 L 86 139 M 86 134 L 91 138" stroke-width="1.2" />54-        </g>55-        <g class="peach-pair-bird-body">56-          <path d="M 30 88 C 24 64, 40 38, 72 36 C 105 34, 124 56, 122 84 C 120 110, 100 122, 70 120 C 44 118, 32 108, 30 88 Z" fill="url(#peach-pair-finch-body)" />57-          <path d="M 40 82 C 42 100, 60 112, 80 110 C 100 108, 110 100, 112 86 C 100 102, 80 106, 60 102 C 50 100, 44 92, 40 82 Z" fill="url(#peach-pair-finch-belly)" />58-          <g class="peach-pair-bird-wing">59-            <path d="M 56 60 C 68 56, 88 60, 102 68 C 108 76, 106 86, 96 90 C 80 92, 64 86, 54 78 C 50 70, 50 64, 56 60 Z" fill={p.lo} />60-            <g stroke={p.shade} stroke-width="0.7" stroke-linecap="round" fill="none" opacity="0.55">61-              <path d="M 64 64 Q 72 70, 78 80" />62-              <path d="M 74 62 Q 82 70, 88 82" />63-              <path d="M 84 64 Q 90 72, 94 84" />64-            </g>65-            <path d="M 56 60 C 68 56, 88 60, 102 68" stroke={p.shade} stroke-width="1" fill="none" opacity="0.4" />66-          </g>67-        </g>68-        <g class="peach-pair-bird-head-group" style={{ "transform-origin": "95px 52px" }}>69-          <g class="peach-pair-bird-tuft">70-            <path d="M 88 22 Q 86 12, 94 12 Q 94 20, 92 26 Z" fill={p.lo} />71-            <path d="M 96 20 Q 96 10, 104 14 Q 102 22, 100 26 Z" fill={p.shade} opacity="0.95" />72-            <path d="M 104 24 Q 108 16, 114 20 Q 110 28, 106 30 Z" fill={p.lo} opacity="0.85" />73-          </g>74-          <circle cx="98" cy="48" r="26" fill="url(#peach-pair-finch-head)" />75-          <path d="M 76 46 Q 80 30, 96 26 Q 86 32, 80 48 Z" fill={p.shade} opacity="0.35" />76-          <g class="peach-pair-bird-eye-group">77-            <circle cx="108" cy="42" r="4.5" fill="#1f0a10" />78-            <circle cx="109.4" cy="40.5" r="1.4" fill="rgba(255,240,225,0.95)" />79-            <circle cx="106.5" cy="44" r="0.5" fill="rgba(255,240,225,0.6)" />80-          </g>81-          <g class="peach-pair-bird-beak" style={{ "transform-origin": "120px 52px" }}>82-            <path d="M 120 48 L 138 50 L 122 56 Z" fill={p.beak} />83-            <path d="M 120 48 L 138 50 L 128 52 Z" fill={p.beakShade} opacity="0.55" />84-            <path d="M 122 52 L 134 51" stroke={p.beakShade} stroke-width="0.6" opacity="0.6" />85-          </g>86-        </g>87-      </svg>88-    </div>89-  );90-};91-92-export const MobileBird: Component<{ pose: string }> = (props) => {93-  const p = SWALLOW;94-  return (95-    <div class={`peach-pair-bird peach-pair-bird-mobile peach-pair-bird-pose-${props.pose}`}>96-      <svg viewBox="0 0 160 150" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMax meet" style={{ transform: "scaleX(-1)" }}>97-        <defs>98-          <radialGradient id="peach-pair-swallow-body" cx="0.55" cy="0.35" r="0.75">99-            <stop offset="0%" stop-color={p.hi} />100-            <stop offset="50%" stop-color={p.mid} />101-            <stop offset="100%" stop-color={p.lo} />102-          </radialGradient>103-          <radialGradient id="peach-pair-swallow-head" cx="0.45" cy="0.4" r="0.7">104-            <stop offset="0%" stop-color={p.hi} />105-            <stop offset="100%" stop-color={p.mid} />106-          </radialGradient>107-          <linearGradient id="peach-pair-swallow-throat" x1="0" y1="0" x2="0" y2="1">108-            <stop offset="0%" stop-color={p.cream} stop-opacity="0.95" />109-            <stop offset="100%" stop-color={p.cream} stop-opacity="0.2" />110-          </linearGradient>111-        </defs>112-        <ellipse class="peach-pair-bird-shadow" cx="86" cy="140" rx="34" ry="3" fill="rgba(0,0,0,0.45)" />113-        <g class="peach-pair-bird-aura" opacity="0">114-          <ellipse cx="80" cy="78" rx="60" ry="42" fill={p.hi} opacity="0.18" style={{ filter: "blur(8px)" }} />115-        </g>116-        <g class="peach-pair-bird-tail">117-          <path d="M 38 80 Q 8 70, 0 60 Q 8 78, 28 88 Q 14 90, 6 96 Q 16 94, 32 92 L 38 84 Z" fill={p.lo} />118-          <path d="M 30 84 Q 14 78, 6 70 Q 18 84, 30 88 Z" fill={p.shade} opacity="0.7" />119-        </g>120-        <g class="peach-pair-bird-feet" stroke={p.legs} stroke-width="1.5" stroke-linecap="round" fill="none">121-          <line x1="76" y1="118" x2="74" y2="136" />122-          <path d="M 74 136 L 69 140 M 74 136 L 74 141 M 74 136 L 79 140" stroke-width="1.1" />123-          <line x1="96" y1="118" x2="98" y2="136" />124-          <path d="M 98 136 L 93 140 M 98 136 L 98 141 M 98 136 L 103 140" stroke-width="1.1" />125-        </g>126-        <g class="peach-pair-bird-body">127-          <path d="M 36 82 C 32 56, 56 42, 88 44 C 116 46, 134 60, 134 80 C 134 102, 116 118, 90 118 C 60 118, 40 104, 36 82 Z" fill="url(#peach-pair-swallow-body)" />128-          <path d="M 50 64 C 60 52, 80 46, 100 48 C 116 50, 126 60, 128 70 C 120 60, 100 56, 80 60 C 64 64, 56 68, 50 64 Z" fill={p.shade} opacity="0.5" />129-          <path d="M 64 82 C 66 100, 86 114, 102 112 C 116 110, 124 100, 126 88 C 116 102, 100 108, 84 104 C 74 102, 68 94, 64 82 Z" fill="url(#peach-pair-swallow-throat)" />130-          <g class="peach-pair-bird-wing">131-            <path d="M 50 70 C 64 64, 84 66, 100 72 C 112 78, 112 90, 100 96 C 84 100, 64 96, 52 88 C 46 80, 46 74, 50 70 Z" fill={p.lo} />132-            <g fill={p.shade} opacity="0.85">133-              <path d="M 96 88 L 110 92 L 100 96 Z" />134-              <path d="M 88 92 L 102 98 L 92 100 Z" />135-              <path d="M 80 94 L 92 100 L 84 102 Z" />136-            </g>137-            <g stroke={p.shade} stroke-width="0.6" stroke-linecap="round" fill="none" opacity="0.5">138-              <path d="M 58 76 Q 70 82, 80 90" />139-              <path d="M 68 72 Q 80 80, 90 88" />140-              <path d="M 80 70 Q 90 78, 100 86" />141-            </g>142-            <path d="M 50 70 C 64 64, 84 66, 100 72" stroke={p.shade} stroke-width="1" fill="none" opacity="0.55" />143-          </g>144-        </g>145-        <g class="peach-pair-bird-head-group" style={{ "transform-origin": "116px 64px" }}>146-          <circle cx="118" cy="64" r="20" fill="url(#peach-pair-swallow-head)" />147-          <path d="M 100 60 Q 108 46, 124 46 Q 134 50, 136 60 Q 124 54, 108 60 Z" fill={p.shade} opacity="0.55" />148-          <g class="peach-pair-bird-eye-group">149-            <circle cx="126" cy="60" r="4" fill="#1f0a10" />150-            <circle cx="127.2" cy="58.8" r="1.2" fill="rgba(255,240,225,0.95)" />151-          </g>152-          <g class="peach-pair-bird-beak" style={{ "transform-origin": "136px 64px" }}>153-            <path d="M 136 60 L 154 62 L 138 66 Z" fill={p.beak} />154-            <path d="M 136 60 L 154 62 L 145 63 Z" fill={p.beakShade} opacity="0.5" />155-            <path d="M 138 63 L 152 63" stroke={p.beakShade} stroke-width="0.5" opacity="0.6" />156-          </g>157-        </g>158-      </svg>159-    </div>160-  );161-};

view file at the parent

src/popup/styles/popup.css+0 −264
@@ -7062,43 +7062,6 @@70627062   flex: 1;70637063 }70647064 7065-.peach-adopt-birds {7066-  width: 120px;7067-  height: 54px;7068-  margin-bottom: 14px;7069-  position: relative;7070-}7071-7072-.peach-adopt-birds::before {7073-  content: "";7074-  position: absolute;7075-  left: 29px;7076-  right: 29px;7077-  top: 25px;7078-  height: 1px;7079-  background: color-mix(in srgb, var(--peach-accent) 50%, transparent);7080-}7081-7082-.peach-adopt-birds span {7083-  position: absolute;7084-  top: 11px;7085-  width: 34px;7086-  height: 28px;7087-  border-radius: 50% 50% 46% 46%;7088-  background: color-mix(in srgb, var(--peach-accent) 45%, var(--peach-t0));7089-  box-shadow: 0 0 18px color-mix(in srgb, var(--peach-accent) 24%, transparent);7090-  animation: peach-adopt-bob 1.8s ease-in-out infinite;7091-}7092-7093-.peach-adopt-birds span:first-child {7094-  left: 12px;7095-}7096-7097-.peach-adopt-birds span:last-child {7098-  right: 12px;7099-  animation-delay: 0.25s;7100-}7101-71027065 .peach-adopt-sas {71037066   margin: 18px 0;71047067   padding: 14px;@@ -7111,11 +7074,6 @@71117074   text-align: center;71127075 }71137076 7114-@keyframes peach-adopt-bob {7115-  0%, 100% { transform: translateY(0); }7116-  50% { transform: translateY(-5px); }7117-}7118-71197077 .peach-onboarding-mobile-card-buttons {71207078   display: flex;71217079   flex-direction: column;@@ -9284,228 +9242,6 @@92849242   box-shadow: 0 0 6px color-mix(in srgb, var(--peach-warn, #d29257) 55%, transparent);92859243 }92869244 9287-/* Bird stage ----------------------------------------------------------- */9288-.peach-pair-stage {9289-  position: relative;9290-  /* Never let the flex column steal the stage's height to fit whatever is9291-     below it — that is what clipped the birds to their legs before. Anything9292-     that does not fit has to give somewhere else. */9293-  flex: none;9294-  margin: 14px 0 8px;9295-  height: 180px;9296-  border-radius: 12px;9297-  background:9298-    radial-gradient(ellipse 70% 60% at 50% 30%, color-mix(in srgb, var(--peach-accent) 10%, transparent), transparent 70%),9299-    radial-gradient(ellipse 90% 100% at 50% 100%, color-mix(in srgb, #d8748a 14%, transparent), transparent 70%),9300-    color-mix(in srgb, var(--peach-bg) 92%, #000);9301-  border: 1px solid color-mix(in srgb, var(--peach-t0) 8%, transparent);9302-  overflow: hidden;9303-  isolation: isolate;9304-}9305-.peach-pair-stage::after {9306-  content: '';9307-  position: absolute;9308-  left: 12%; right: 12%;9309-  bottom: 36px;9310-  height: 1px;9311-  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--peach-t0) 14%, transparent), transparent);9312-}9313-9314-.peach-pair-bird-slot {9315-  position: absolute;9316-  bottom: 8px;9317-  width: 100px;9318-  height: 110px;9319-  display: flex;9320-  align-items: flex-end;9321-  justify-content: center;9322-}9323-.peach-pair-bird-slot-desktop { left: 28px; }9324-.peach-pair-bird-slot-mobile { right: 28px; }9325-9326-/* The QR screen is the tallest in the flow: a 180px stage plus headline, sub,9327-   QR and action overflows the popup, and the stage is what gets clipped — you9328-   saw bird legs and nothing else. Shrink the stage and the birds with it here9329-   only, so the birds stay whole and the QR stays comfortably above the fold.9330-   Uses the individual `scale` property rather than `transform`, which the9331-   mobile bird's hop animation already owns. */9332-.peach-adoption-source[data-phase="invitation"] .peach-pair-stage {9333-  height: 108px;9334-  margin: 8px 0 6px;9335-}9336-.peach-adoption-source[data-phase="invitation"] .peach-pair-bird-slot {9337-  scale: 0.62;9338-  transform-origin: center bottom;9339-}9340-.peach-adoption-source[data-phase="invitation"] .peach-pair-stage::after {9341-  bottom: 20px;9342-}9343-9344-/* Mobile bird hops in from the right when handshake begins */9345-.peach-pair-bird-slot-mobile {9346-  opacity: 1;9347-  transform: translateX(-6px);9348-  transition: opacity 0.4s ease, transform 0.65s cubic-bezier(.5, 1.7, .4, 1);9349-}9350-.peach-pair-stage.act-listening .peach-pair-bird-slot-mobile,9351-.peach-pair-stage.act-chirping .peach-pair-bird-slot-mobile,9352-.peach-pair-stage.act-idle .peach-pair-bird-slot-mobile {9353-  opacity: 0;9354-  transform: translateX(60px) rotate(8deg);9355-}9356-.peach-pair-stage.act-handshake .peach-pair-bird-slot-desktop,9357-.peach-pair-stage.act-paired .peach-pair-bird-slot-desktop {9358-  transform: translateX(6px);9359-  transition: transform 0.5s cubic-bezier(.4, 1.4, .4, 1);9360-}9361-9362-/* Paired: both birds bob together */9363-.peach-pair-stage.act-paired .peach-pair-bird-slot {9364-  animation: peach-pair-happy-bob 1.2s ease-in-out infinite;9365-}9366-.peach-pair-stage.act-paired .peach-pair-bird-slot-mobile { animation-delay: 0.2s; }9367-.peach-pair-stage.act-paired .peach-pair-bird-slot-desktop { --bb-x: 6px; }9368-.peach-pair-stage.act-paired .peach-pair-bird-slot-mobile { --bb-x: -6px; }9369-@keyframes peach-pair-happy-bob {9370-  0%, 100% { transform: translateX(var(--bb-x, 0)) translateY(0); }9371-  50%      { transform: translateX(var(--bb-x, 0)) translateY(-3px); }9372-}9373-9374-/* Bird internal -------------------------------------------------------- */9375-.peach-pair-bird {9376-  width: 108px;9377-  height: 108px;9378-  transform-origin: 50% 95%;9379-}9380-.peach-pair-bird svg { width: 100%; height: 100%; overflow: visible; }9381-9382-.peach-pair-bird-body {9383-  transform-origin: 50% 80%;9384-  animation: peach-pair-bird-bob 2.4s ease-in-out infinite;9385-}9386-@keyframes peach-pair-bird-bob {9387-  0%, 100% { transform: translateY(0) scale(1, 1); }9388-  50%      { transform: translateY(-1.5px) scale(1.01, 0.99); }9389-}9390-9391-.peach-pair-bird-eye-group {9392-  animation: peach-pair-blink 5.2s steps(1, end) infinite;9393-  transform-origin: center;9394-  transform-box: fill-box;9395-}9396-@keyframes peach-pair-blink {9397-  0%, 92%, 96%, 100% { transform: scaleY(1); }9398-  94%                { transform: scaleY(0.05); }9399-}9400-9401-/* Pose: chirping — beak opens, head bobs, aura blooms */9402-.peach-pair-bird-pose-chirping .peach-pair-bird-body {9403-  animation:9404-    peach-pair-bird-bob 2.4s ease-in-out infinite,9405-    peach-pair-squeeze 1.7s ease-in-out infinite;9406-}9407-@keyframes peach-pair-squeeze {9408-  0%, 70%, 100% { transform-origin: 50% 85%; }9409-  78%           { transform: scale(1.04, 0.96) translateY(1px); }9410-  86%           { transform: scale(0.98, 1.02); }9411-}9412-.peach-pair-bird-pose-chirping .peach-pair-bird-head-group {9413-  animation: peach-pair-head-bob 1.7s ease-in-out infinite;9414-}9415-@keyframes peach-pair-head-bob {9416-  0%, 70%, 100% { transform: translateY(0) rotate(0); }9417-  78%           { transform: translateY(-3px) rotate(-4deg); }9418-  86%           { transform: translateY(-1px) rotate(-2deg); }9419-}9420-.peach-pair-bird-pose-chirping .peach-pair-bird-beak {9421-  animation: peach-pair-beak-open 1.7s ease-in-out infinite;9422-}9423-@keyframes peach-pair-beak-open {9424-  0%, 70%, 100% { transform: rotate(0) scaleX(1); }9425-  78%           { transform: rotate(-14deg) scaleX(1.05) translateY(-1px); }9426-  86%           { transform: rotate(-6deg); }9427-}9428-.peach-pair-bird-pose-chirping .peach-pair-bird-aura {9429-  animation: peach-pair-aura 1.7s ease-in-out infinite;9430-}9431-@keyframes peach-pair-aura {9432-  0%, 100% { opacity: 0; }9433-  50%      { opacity: 0.8; }9434-}9435-.peach-pair-bird-pose-chirping .peach-pair-bird-tail {9436-  animation: peach-pair-tail-twitch 1.7s ease-in-out infinite;9437-  transform-origin: 100% 50%;9438-}9439-@keyframes peach-pair-tail-twitch {9440-  0%, 75%, 100% { transform: rotate(0); }9441-  82%           { transform: rotate(-3deg) translateX(-1px); }9442-}9443-9444-/* Pose: attentive — head tilts toward partner */9445-.peach-pair-bird-pose-attentive .peach-pair-bird-head-group {9446-  animation: peach-pair-head-tilt 0.6s cubic-bezier(.4, 1.6, .5, 1) both;9447-}9448-@keyframes peach-pair-head-tilt {9449-  0%   { transform: rotate(0) translateY(0); }9450-  60%  { transform: rotate(-10deg) translateY(-1px) translateX(2px); }9451-  100% { transform: rotate(-6deg); }9452-}9453-.peach-pair-bird-pose-attentive .peach-pair-bird-tail {9454-  animation: peach-pair-tail-cock 0.6s cubic-bezier(.4, 1.6, .5, 1) both;9455-  transform-origin: 100% 50%;9456-}9457-@keyframes peach-pair-tail-cock {9458-  0%   { transform: rotate(0); }9459-  100% { transform: rotate(-4deg); }9460-}9461-9462-/* Pose: happy — wing flutter, tuft perk, tail wag */9463-.peach-pair-bird-pose-happy .peach-pair-bird-wing {9464-  transform-origin: 80% 60%;9465-  animation: peach-pair-wing-flutter 0.32s ease-in-out 5;9466-}9467-@keyframes peach-pair-wing-flutter {9468-  0%, 100% { transform: rotate(0) translateY(0); }9469-  50%      { transform: rotate(-16deg) translateY(-2px); }9470-}9471-.peach-pair-bird-pose-happy .peach-pair-bird-tuft {9472-  transform-origin: 0% 100%;9473-  animation: peach-pair-tuft-perk 0.4s ease-in-out 3;9474-}9475-@keyframes peach-pair-tuft-perk {9476-  0%, 100% { transform: rotate(0); }9477-  50%      { transform: rotate(-15deg) translateY(-1px); }9478-}9479-.peach-pair-bird-pose-happy .peach-pair-bird-tail {9480-  transform-origin: 100% 50%;9481-  animation: peach-pair-tail-wag 0.5s ease-in-out 4;9482-}9483-@keyframes peach-pair-tail-wag {9484-  0%, 100% { transform: rotate(0); }9485-  25%      { transform: rotate(6deg); }9486-  75%      { transform: rotate(-6deg); }9487-}9488-.peach-pair-bird-pose-happy .peach-pair-bird-head-group {9489-  animation: peach-pair-head-perk 0.5s ease-out;9490-}9491-@keyframes peach-pair-head-perk {9492-  0%   { transform: translateY(0); }9493-  40%  { transform: translateY(-3px) rotate(2deg); }9494-  100% { transform: translateY(0); }9495-}9496-9497-/* Pose: sad — body droops forward */9498-.peach-pair-bird-pose-sad .peach-pair-bird-body {9499-  transform: translateY(5px) rotate(-2deg);9500-  animation: none;9501-}9502-.peach-pair-bird-pose-sad .peach-pair-bird-head-group {9503-  transform: translateY(4px) rotate(8deg);9504-}9505-.peach-pair-bird-pose-sad .peach-pair-bird-tuft { transform: rotate(15deg) translateY(2px); }9506-.peach-pair-bird-pose-sad .peach-pair-bird-tail { transform: rotate(8deg) translateY(2px); }9507-9508-.peach-pair-bird-pose-hidden { opacity: 0; }95099245 95109246 /* Sound arcs ----------------------------------------------------------- */95119247 .peach-pair-arcs {

view file

src/popup/views/AdoptVaultView.tsx+0 −8
@@ -238,10 +238,6 @@238238           </Match>239239 240240           <Match when={phase() === "pair"}>241-            <div class="peach-adopt-birds" aria-hidden="true">242-              <span />243-              <span />244-            </div>245241             <h1>Pair with your other device.</h1>246242             {/* The populated source owns mesh configuration. Its invitation247243                 carries either Peach's public relay or a self-hosted relay, so@@ -317,10 +313,6 @@317313           </Match>318314 319315           <Match when={phase() === "success"}>320-            <div class="peach-adopt-birds" aria-hidden="true">321-              <span />322-              <span />323-            </div>324316             <h1>Joined your vault.</h1>325317             <p>Your passwords will sync shortly.</p>326318             <button class="peach-btn peach-btn--accent peach-btn--md" onClick={() => void props.onDone()}>

view file

src/popup/views/Lock.tsx+0 −32
@@ -256,14 +256,6 @@256256     setLoading(false);257257   }258258 259-  function openRestore(surface: Exclude<RestoreSurface, null>) {260-    if (!props.ready || loading()) return;261-    setPassword("");262-    setConfirmPassword("");263-    setError("");264-    setRestoreSurface(surface);265-  }266-267259   // ── All surfaces routed reactively via Switch/Match ───────────────────268260   return (269261     <Switch>@@ -518,30 +510,6 @@518510                 {createBlockerMessage()}519511               </Show>520512             </button>521-522-            {/* Restore lives directly under the primary CTA, not in a page523-                footer — the 520px popup clipped a bottom footer below the524-                fold, so a fresh install looked like "new vault" was the only525-                path. These must stay on-screen without scrolling. */}526-            <div class="peach-newvault-restore">527-              <p class="peach-hint">Already have a backup?</p>528-              <button529-                class="peach-lock-restore-link"530-                onClick={() => void openRestore("restore")}531-                disabled={!props.ready}532-              >533-                › restore from backup534-              </button>535-              <Show when={props.onOpenCodexRestore}>536-                <button537-                  class="peach-lock-restore-link"538-                  onClick={() => props.onOpenCodexRestore?.()}539-                  disabled={!props.ready}540-                >541-                  › restore from peach codex542-                </button>543-              </Show>544-            </div>545513 546514             <p class="peach-newvault-warning">547515               <span class="peach-newvault-warning-icon" aria-hidden="true">⚠</span>

view file

src/popup/views/PairDevice.test.tsx+0 −1
@@ -157,6 +157,5 @@157157 158158     pushFromBackground("ADOPTION_SOURCE_RESULT", { status: "complete" });159159     expect(container.textContent).toContain("Device added");160-    expect(container.querySelectorAll(".peach-pair-bird-pose-happy")).toHaveLength(2);161160   });162161 });

view file

src/popup/views/PairDevice.tsx+0 −15
@@ -21,7 +21,6 @@2121   onMount,2222 } from "solid-js";2323 import { sendMessage } from "../../shared/bridge";24-import { DesktopBird, MobileBird } from "../components/PairingBirds";2524 import { PairingQrCode } from "../components/PairingQrCode";2625 import { SyncModeSelector } from "../components/SyncModeSelector";2726 @@ -130,18 +129,6 @@130129     </svg>131130   );132131 133-  // The bird stage — always animating — shared by every pairing screen.134-  const birdStage = (phase: SharePhase) => (135-    <div class={`peach-pair-stage ${phase === "sas" || phase === "paired" ? "act-handshake" : "act-listening"}`} aria-hidden="true">136-      <div class="peach-pair-bird-slot peach-pair-bird-slot-desktop">137-        <DesktopBird pose={phase === "paired" ? "happy" : "chirping"} />138-      </div>139-      <div class="peach-pair-bird-slot peach-pair-bird-slot-mobile">140-        <MobileBird pose={phase === "paired" ? "happy" : "attentive"} />141-      </div>142-    </div>143-  );144-145132   function errorCopy(): string {146133     switch (shareError()) {147134       case "vault_locked":@@ -183,8 +170,6 @@183170         <div class="peach-pair-title">Add a new device</div>184171         <div class="peach-pair-head-spacer" />185172       </header>186-187-      {birdStage(sharePhase())}188173 189174       <Show when={sharePhase() === "opening"}>190175         <div class="peach-desktop-searching">

view file

src/popup/views/contracts.test.tsx+0 −38
@@ -368,44 +368,6 @@368368     expect(container.textContent).toContain("CSV");369369   });370370 371-  it("Lock onboarding restore options include Peach Codex", async () => {372-    const onOpenCodexRestore = vi.fn();373-    const container = mount(374-      <LockView375-        hasVault={false}376-        ready={true}377-        onUnlock={vi.fn()}378-        onOpenCodexRestore={onOpenCodexRestore}379-      />380-    );381-382-    const restoreButton = Array.from(container.querySelectorAll("button")).find(383-      (candidate) => candidate.textContent?.includes("restore from backup")384-    ) as HTMLButtonElement;385-    restoreButton.click();386-    await flushView();387-388-    expect(container.textContent).toContain("PeachScript");389-    expect(container.textContent).toContain("Codex");390-    expect(container.textContent).toContain("CSV");391-392-    const codexTab = Array.from(container.querySelectorAll("button")).find(393-      (candidate) => candidate.textContent?.trim() === "Codex"394-    ) as HTMLButtonElement;395-    codexTab.click();396-    await flushView();397-398-    expect(container.textContent).toContain("Restore from Peach Codex");399-    expect(container.textContent).toContain("Scan printed Codex pages");400-401-    const openCodexButton = Array.from(container.querySelectorAll("button")).find(402-      (candidate) => candidate.textContent?.trim() === "open Codex restore →"403-    ) as HTMLButtonElement;404-    openCodexButton.click();405-406-    expect(onOpenCodexRestore).toHaveBeenCalledTimes(1);407-  });408-409371   it("Lock can open directly on backup restore from onboarding", async () => {410372     const container = mount(() => (411373       <LockView

view file