5c295ba6e302

redesign chirp pairing screen — Finch+Swallow stage, chirp arcs, flight wave, connection dot, status copy/action, sr-live region; bump v3.2.2 → v3.3.0

time
2026-05-18 13:05 UTC · 5 months ago
machine
koh2
parent
60c812553e03
5 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/components/ChirpPairingFlow.tsx+465 −107src/popup/styles/popup.css+626 −102
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "3.2.2",4+  "version": "3.3.0",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",4-  "version": "3.2.2",4+  "version": "3.3.0",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": "3.2.2",3+  "version": "3.3.0",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/popup/components/ChirpPairingFlow.tsx+465 −107
@@ -1,19 +1,27 @@11 // ChirpPairingFlow — the audio pairing screen.22 //3-// Implements the three-act bird animation specified in the brief §6.2:4-//   Act 1 (Waiting) — desktop bird idles, soft pulsing chirp ring5-//   Act 2 (Contact)  — mobile bird hops in, sound-wave arcs in both6-//                       directions, head-tilt reaction7-//   Act 3 (Paired)   — sound waves resolve into a glowing connection dot,8-//                       both birds wing-flutter, copy switches to "Paired!"3+// Renders the three-act bird animation specified in the brief §6.2:4+//   Act 1 (waiting)   — desktop finch chirps, soft rings pulse from its beak5+//   Act 2 (handshake) — mobile swallow hops in, sound-wave arcs fly between6+//                        the two birds, both heads tilt toward each other7+//   Act 3 (paired)    — waves resolve into a glowing connection dot; both8+//                        birds wing-flutter and bob together99 //10-// The acts are bound to the `PairingPhase` enum from sync-types; the11-// background coordinator drives the phase forward as the audio + Noise12-// rounds make progress. The component stays a pure renderer: it does not13-// touch the microphone or the WebSocket itself.10+// Phase mapping (PairingPhase from sync-types):11+//   idle | listening | chirping → Act 112+//   handshake                   → Act 213+//   paired                      → Act 314+//   timeout                     → desktop bird droops, retry CTA15+//   mic_denied                  → static stage, browser-settings hint16+//   version_mismatch            → static stage, update hint, retry CTA17+//   handshake_failed            → retry CTA18+//19+// The orchestrator (popup/chirp-pair.ts) pushes phase updates into20+// `progress` and the component renders them. This component owns no21+// microphone or socket state of its own.1422 15-import { Component, Show, createEffect, createSignal } from "solid-js";16-import type { PairingProgress } from "../../shared/sync-types";23+import { Component, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js";24+import type { PairingPhase, PairingProgress } from "../../shared/sync-types";1725 1826 export interface ChirpPairingFlowProps {1927   progress: PairingProgress;@@ -25,69 +33,209 @@2533   onDismiss: () => void;2634 }2735 28-const PAIRED_DISMISS_MS = 2500;36+const PAIRED_DISMISS_MS = 2_500;37+// During handshake, swap which bird the flight-wave emanates from so the38+// chirp feels reciprocated. Desktop wave first, then mobile.39+const HANDSHAKE_DESKTOP_WAVE_MS = 900;40+const HANDSHAKE_MOBILE_WAVE_MS = 1_900;2941 3042 export const ChirpPairingFlow: Component<ChirpPairingFlowProps> = (props) => {3143   const [dismissArmed, setDismissArmed] = createSignal(false);44+  const [waveSide, setWaveSide] = createSignal<"desktop" | "mobile" | null>(null);3245 3346   createEffect(() => {3447     if (props.progress.phase === "paired" && !dismissArmed()) {3548       setDismissArmed(true);3649       const t = setTimeout(() => props.onDismiss(), PAIRED_DISMISS_MS);37-      // No need to clean up on unmount — onDismiss is a no-op if parent unmounted.38-      void t;50+      onCleanup(() => clearTimeout(t));3951     }4052   });4153 54+  // Swap which side the flight-wave plays from during Act 2.55+  createEffect(() => {56+    if (props.progress.phase !== "handshake") {57+      setWaveSide(null);58+      return;59+    }60+    setWaveSide("desktop");61+    const t1 = setTimeout(() => setWaveSide("mobile"), HANDSHAKE_DESKTOP_WAVE_MS);62+    const t2 = setTimeout(() => setWaveSide(null), HANDSHAKE_MOBILE_WAVE_MS);63+    onCleanup(() => { clearTimeout(t1); clearTimeout(t2); });64+  });65+66+  const phase = createMemo(() => props.progress.phase);67+  const desktopPose = createMemo(() => poseFor("desktop", phase()));68+  const mobilePose = createMemo(() => poseFor("mobile", phase()));69+70+  const showDesktopChirp = createMemo(() => {71+    const p = phase();72+    return p === "listening" || p === "chirping" || (p === "handshake" && waveSide() !== "mobile");73+  });74+  const showMobileChirp = createMemo(() => phase() === "handshake" && waveSide() === "mobile");75+4276   return (43-    <div class="peach-chirp-pair" data-phase={props.progress.phase}44-         onClick={() => props.progress.phase === "paired" && props.onDismiss()}>45-      <div class="peach-chirp-stage">46-        <DesktopBird phase={props.progress.phase} />47-        <SoundWaves phase={props.progress.phase} />48-        <MobileBird phase={props.progress.phase} />49-        <ConnectionDot phase={props.progress.phase} />77+    <div78+      class="peach-chirp-pair"79+      data-phase={phase()}80+      role="dialog"81+      aria-label="Pair a device"82+      onClick={() => phase() === "paired" && props.onDismiss()}83+    >84+      <PairingHeader onBack={() => props.onCancel()} phase={phase()} />85+86+      <div class="peach-chirp-chip-row">87+        <StatusChip phase={phase()} />5088       </div>5189 90+      <div class={`peach-chirp-stage act-${phase()}`} aria-hidden="true">91+        <div class="peach-chirp-bird-slot peach-chirp-bird-slot-desktop">92+          <DesktopBird pose={desktopPose()} />93+          <Show when={showDesktopChirp()}>94+            <ChirpArcs direction="right" />95+          </Show>96+        </div>97+        <div class="peach-chirp-bird-slot peach-chirp-bird-slot-mobile">98+          <MobileBird pose={mobilePose()} />99+          <Show when={showMobileChirp()}>100+            <ChirpArcs direction="left" />101+          </Show>102+        </div>103+        <div class="peach-chirp-flight-track">104+          <FlightWave from="desktop" visible={waveSide() === "desktop"} />105+          <FlightWave from="mobile" visible={waveSide() === "mobile"} />106+          <ConnectionDot visible={phase() === "paired"} />107+        </div>108+        <div class="peach-chirp-motes">109+          <For8>110+            {(i) => <span class="peach-chirp-mote" style={{ "--mote-i": i }} />}111+          </For8>112+        </div>113+      </div>114+52115       <PairingCopy progress={props.progress} />116+      <PairingAction117+        progress={props.progress}118+        onCancel={() => props.onCancel()}119+        onRetry={() => props.onRetry()}120+        onDone={() => props.onDismiss()}121+      />53122 54-      <Show when={canCancel(props.progress.phase)}>55-        <button class="peach-btn peach-btn-ghost peach-chirp-cancel" onClick={() => props.onCancel()}>56-          Cancel57-        </button>58-      </Show>59-      <Show when={isTerminalError(props.progress.phase)}>60-        <button class="peach-btn peach-btn-primary peach-chirp-retry" onClick={() => props.onRetry()}>61-          Try again62-        </button>63-      </Show>64-      <Show when={props.progress.phase === "mic_denied"}>123+      <Show when={phase() === "mic_denied"}>65124         <p class="peach-chirp-mic-help">66-          Update your browser's site settings to allow Peach to access the microphone, then try again.125+          Update your browser's site settings to allow Peach to access the microphone,126+          then try again.67127         </p>68128       </Show>129+130+      <PairingFooter phase={phase()} />131+132+      {/* Polite live region so the act transitions are announced to133+          assistive tech even though the stage itself is decorative. */}134+      <div class="peach-sr-only" aria-live="polite">{liveText(props.progress)}</div>69135     </div>70136   );71137 };72138 73-function canCancel(phase: PairingProgress["phase"]): boolean {74-  return phase === "listening" || phase === "chirping" || phase === "handshake";139+function poseFor(side: "desktop" | "mobile", phase: PairingPhase): string {140+  if (side === "desktop") {141+    switch (phase) {142+      case "listening":143+      case "chirping": return "chirping";144+      case "handshake": return "attentive";145+      case "paired": return "happy";146+      case "timeout":147+      case "handshake_failed":148+      case "version_mismatch": return "sad";149+      default: return "idle";150+    }151+  }152+  switch (phase) {153+    case "handshake": return "attentive";154+    case "paired": return "happy";155+    default: return "hidden";156+  }75157 }76158 77-function isTerminalError(phase: PairingProgress["phase"]): boolean {159+function liveText(p: PairingProgress): string {160+  switch (p.phase) {161+    case "listening":162+    case "chirping": return "Listening for the chirp";163+    case "handshake": return "Pairing — exchanging keys";164+    case "paired": return p.peerDeviceName ? `Paired — connected to ${p.peerDeviceName}` : "Paired";165+    case "timeout": return "Couldn't find your phone";166+    case "mic_denied": return "Microphone access needed";167+    case "version_mismatch": return "Update Peach on both devices";168+    case "handshake_failed": return "Pairing failed — try again";169+    default: return "";170+  }171+}172+173+// ---------------------------------------------------------------------------174+// Header, chip row, footer175+// ---------------------------------------------------------------------------176+177+const PairingHeader: Component<{ onBack: () => void; phase: PairingPhase }> = (props) => (178+  <header class="peach-chirp-head">179+    <button class="peach-chirp-back" aria-label="Back" onClick={() => props.onBack()}>180+      <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">181+        <path d="M10 3 L5 8 L10 13" />182+      </svg>183+    </button>184+    <div class="peach-chirp-title">Sync</div>185+    <div class="peach-chirp-head-spacer" />186+  </header>187+);188+189+const StatusChip: Component<{ phase: PairingPhase }> = (props) => {190+  const label = () => {191+    switch (props.phase) {192+      case "listening":193+      case "chirping":194+      case "idle": return "listening";195+      case "handshake": return "handshake";196+      case "paired": return "paired";197+      case "mic_denied": return "mic blocked";198+      case "version_mismatch": return "version mismatch";199+      case "timeout": return "no response";200+      case "handshake_failed": return "failed";201+    }202+  };78203   return (79-    phase === "timeout" ||80-    phase === "handshake_failed" ||81-    phase === "version_mismatch" ||82-    phase === "mic_denied"204+    <div class="peach-chirp-chip" data-phase={props.phase}>205+      <span class="peach-chirp-chip-dot" />206+      <span class="peach-chirp-chip-text">{label()}</span>207+    </div>83208   );84-}209+};85210 211+const PairingFooter: Component<{ phase: PairingPhase }> = (props) => (212+  <footer class="peach-chirp-foot">213+    <div class="peach-chirp-foot-divider" />214+    <div class="peach-chirp-foot-text">215+      {props.phase === "paired"216+        ? "Long-term sync keys exchanged over Noise NK. Stored locally on both devices."217+        : "Chirp Sync stays on your local network. No relay servers, no cloud accounts — just your devices in the same room."}218+    </div>219+  </footer>220+);221+222+// ---------------------------------------------------------------------------223+// Copy + action area (animates on phase change via key remount)224+// ---------------------------------------------------------------------------225+86226 const PairingCopy: Component<{ progress: PairingProgress }> = (props) => {227+  // Remount on phase change so the copy-in keyframe replays.228+  const phaseKey = createMemo(() => props.progress.phase);87229   return (88-    <div class="peach-chirp-copy">89-      <h2 class="peach-chirp-headline">{headline(props.progress)}</h2>90-      <p class="peach-chirp-sub">{subtext(props.progress)}</p>230+    <div class="peach-chirp-copy-host">231+      <Show when={phaseKey()} keyed>232+        {(_p) => (233+          <div class={`peach-chirp-copy peach-chirp-copy-${props.progress.phase}`}>234+            <div class="peach-chirp-headline">{headline(props.progress)}</div>235+            <div class="peach-chirp-sub">{subtext(props.progress)}</div>236+          </div>237+        )}238+      </Show>91239     </div>92240   );93241 };@@ -96,20 +244,13 @@96244   switch (p.phase) {97245     case "idle":98246     case "listening":99-    case "chirping":100-      return "Hold your phone near your laptop";101-    case "handshake":102-      return "Pairing…";103-    case "paired":104-      return "Paired!";105-    case "timeout":106-      return "Couldn’t find your phone";107-    case "mic_denied":108-      return "Microphone access needed";109-    case "version_mismatch":110-      return "Update Peach on both devices";111-    case "handshake_failed":112-      return "Something went wrong";247+    case "chirping": return "Hold your phone near your laptop";248+    case "handshake": return "Pairing…";249+    case "paired": return "Paired!";250+    case "timeout": return "Couldn’t find your phone";251+    case "mic_denied": return "Microphone access needed";252+    case "version_mismatch": return "Update Peach on both devices";253+    case "handshake_failed": return "Something went wrong";113254   }114255 }115256 @@ -117,69 +258,286 @@117258   switch (p.phase) {118259     case "idle":119260     case "listening":120-    case "chirping":121-      return "Peach will pair automatically when your phone is close";122-    case "handshake":123-      return "Hold still — exchanging keys";124-    case "paired":125-      return p.peerDeviceName ? `Connected to ${p.peerDeviceName}` : "Connected";126-    case "timeout":127-      return "Make sure Peach is open on your phone and try again";128-    case "mic_denied":129-      return "Peach uses your microphone to pair. No audio is recorded or stored.";130-    case "version_mismatch":131-      return "The other device is running a different protocol version";132-    case "handshake_failed":133-      return p.errorMessage ?? "Please try again";261+    case "chirping": return "Peach will pair automatically when your phone is close.";262+    case "handshake": return "Listening for the chirp.";263+    case "paired": return p.peerDeviceName ? `Connected to ${p.peerDeviceName}` : "Connected";264+    case "timeout": return "Make sure Peach is open on your phone and try again.";265+    case "mic_denied": return "Peach uses your microphone to pair. No audio is recorded or stored.";266+    case "version_mismatch": return "The other device is running a different protocol version.";267+    case "handshake_failed": return p.errorMessage ?? "Please try again.";134268   }135269 }136270 137-// ---------------------------------------------------------------------------138-// Inline SVG birds — kept deliberately schematic so the focus stays on the139-// motion. A future polish pass can swap these for the production140-// illustrations without changing the component shape.141-// ---------------------------------------------------------------------------142-143-const DesktopBird: Component<{ phase: PairingProgress["phase"] }> = (props) => {271+const PairingAction: Component<{272+  progress: PairingProgress;273+  onCancel: () => void;274+  onRetry: () => void;275+  onDone: () => void;276+}> = (props) => {277+  const phase = () => props.progress.phase;144278   return (145-    <svg class="peach-chirp-bird peach-chirp-bird-desktop"146-         width="92" height="92" viewBox="0 0 92 92" data-phase={props.phase}>147-      <ellipse cx="46" cy="58" rx="28" ry="22" class="peach-chirp-bird-body" />148-      <circle cx="58" cy="42" r="14" class="peach-chirp-bird-head" />149-      <circle cx="62" cy="40" r="2.2" class="peach-chirp-bird-eye" />150-      <path d="M 70 42 L 78 42 L 72 47 Z" class="peach-chirp-bird-beak" />151-      <path d="M 30 48 Q 22 56 30 64" class="peach-chirp-bird-wing" fill="none" />152-    </svg>279+    <div class="peach-chirp-action">280+      <Show when={phase() === "listening" || phase() === "chirping" || phase() === "idle"}>281+        <div class="peach-chirp-action-waiting">282+          <button class="peach-chirp-cancel peach-btn peach-btn-ghost" onClick={() => props.onCancel()}>283+            Cancel284+          </button>285+          <div class="peach-chirp-listening">286+            <span class="peach-chirp-listen-dot" />287+            <span class="peach-chirp-listen-dot" />288+            <span class="peach-chirp-listen-dot" />289+            <span class="peach-chirp-listen-label">listening</span>290+          </div>291+        </div>292+      </Show>293+294+      <Show when={phase() === "handshake"}>295+        <div class="peach-chirp-action-handshake">296+          <div class="peach-chirp-meter">297+            <span class="peach-chirp-meter-bar" />298+          </div>299+          <span class="peach-chirp-meter-label">handshake · noise NK</span>300+        </div>301+      </Show>302+303+      <Show when={phase() === "paired"}>304+        <div class="peach-chirp-action-paired">305+          <div class="peach-chirp-device-confirm">306+            <span class="peach-chirp-device-confirm-glyph" aria-hidden="true">307+              <svg viewBox="0 0 16 16" width="14" height="14">308+                <rect x="3" y="2" width="10" height="13" rx="2" fill="none" stroke="currentColor" stroke-width="1.2" />309+                <circle cx="8" cy="12" r="0.9" fill="currentColor" />310+              </svg>311+            </span>312+            <span class="peach-chirp-device-confirm-name">313+              {props.progress.peerDeviceName ?? "Paired device"}314+            </span>315+            <span class="peach-chirp-device-confirm-trust">direct</span>316+          </div>317+          <button class="peach-chirp-done peach-btn peach-btn-primary" onClick={() => props.onDone()}>318+            Done319+          </button>320+        </div>321+      </Show>322+323+      <Show when={isRetryable(phase())}>324+        <button class="peach-chirp-retry peach-btn peach-btn-primary" onClick={() => props.onRetry()}>325+          Try again326+        </button>327+      </Show>328+    </div>153329   );154330 };155331 156-const MobileBird: Component<{ phase: PairingProgress["phase"] }> = (props) => {157-  return (158-    <svg class="peach-chirp-bird peach-chirp-bird-mobile"159-         width="74" height="74" viewBox="0 0 74 74" data-phase={props.phase}>160-      <ellipse cx="36" cy="48" rx="22" ry="18" class="peach-chirp-bird-body" />161-      <circle cx="26" cy="34" r="11" class="peach-chirp-bird-head" />162-      <circle cx="22" cy="32" r="1.8" class="peach-chirp-bird-eye" />163-      <path d="M 14 34 L 7 34 L 12 38 Z" class="peach-chirp-bird-beak" />164-      <path d="M 50 40 Q 58 48 50 56" class="peach-chirp-bird-wing" fill="none" />332+function isRetryable(phase: PairingPhase): boolean {333+  return phase === "timeout" || phase === "handshake_failed" || phase === "version_mismatch" || phase === "mic_denied";334+}335+336+// ---------------------------------------------------------------------------337+// Stage children — chirp arcs, flight wave, connection dot338+// ---------------------------------------------------------------------------339+340+const ChirpArcs: Component<{ direction: "left" | "right" }> = (props) => (341+  <div class={`peach-chirp-arcs peach-chirp-arcs-${props.direction}`}>342+    <span class="peach-chirp-arc" style={{ "animation-delay": "0ms" }} />343+    <span class="peach-chirp-arc" style={{ "animation-delay": "560ms" }} />344+    <span class="peach-chirp-arc" style={{ "animation-delay": "1120ms" }} />345+  </div>346+);347+348+const FlightWave: Component<{ from: "desktop" | "mobile"; visible: boolean }> = (props) => (349+  <svg350+    class="peach-chirp-flight-wave"351+    classList={{ "is-visible": props.visible }}352+    data-from={props.from}353+    viewBox="0 0 220 80"354+    preserveAspectRatio="none"355+  >356+    <path class="peach-chirp-flight-path" d="M 10 50 Q 60 5, 110 40 T 210 30" />357+    <path class="peach-chirp-flight-path-2" d="M 10 55 Q 60 14, 110 45 T 210 35" />358+  </svg>359+);360+361+const ConnectionDot: Component<{ visible: boolean }> = (props) => (362+  <div class="peach-chirp-dot" classList={{ "is-visible": props.visible }}>363+    <span class="peach-chirp-dot-glow" />364+    <span class="peach-chirp-dot-core" />365+    <svg class="peach-chirp-dot-ring" viewBox="0 0 60 60">366+      <circle cx="30" cy="30" r="22" fill="none" stroke-width="1" stroke-dasharray="2 4" />165367     </svg>166-  );368+  </div>369+);370+371+// 8-mote helper. createMemo over a fixed array is overkill — index is enough.372+const For8: Component<{ children: (i: number) => unknown }> = (props) => (373+  <>374+    {props.children(0)}375+    {props.children(1)}376+    {props.children(2)}377+    {props.children(3)}378+    {props.children(4)}379+    {props.children(5)}380+    {props.children(6)}381+    {props.children(7)}382+  </>383+);384+385+// ---------------------------------------------------------------------------386+// Birds — Finch (desktop) and Swallow (mobile)387+// Ported from the Chirp Pairing prototype. Bird coral/apricot hexes are388+// illustration colours, not brand accent — they live alongside the brand389+// peach exception in §3.1.390+// ---------------------------------------------------------------------------391+392+const FINCH = {393+  hi: "#f4a6b6", mid: "#dc8294", lo: "#a85870", shade: "#7a3e54",394+  cream: "#f5d8c8", beak: "#f5c764", beakShade: "#c8932e", legs: "#7a3e54",167395 };396+const SWALLOW = {397+  hi: "#f4c8a0", mid: "#dc9968", lo: "#a86b3e", shade: "#7a4a26",398+  cream: "#f7e0c8", beak: "#f5c764", beakShade: "#b87a22", legs: "#5e3a1f",399+};168400 169-const SoundWaves: Component<{ phase: PairingProgress["phase"] }> = (props) => {401+const DesktopBird: Component<{ pose: string }> = (props) => {402+  const p = FINCH;170403   return (171-    <svg class="peach-chirp-waves" width="320" height="120" viewBox="0 0 320 120" data-phase={props.phase}>172-      <path class="peach-chirp-wave peach-chirp-wave-out" d="M 80 60 Q 160 20 240 60" fill="none" />173-      <path class="peach-chirp-wave peach-chirp-wave-in" d="M 240 60 Q 160 100 80 60" fill="none" />174-    </svg>404+    <div class={`peach-chirp-bird peach-chirp-bird-desktop peach-chirp-bird-pose-${props.pose}`}>405+      <svg viewBox="0 0 160 150" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMax meet">406+        <defs>407+          <radialGradient id="peach-chirp-finch-body" cx="0.42" cy="0.32" r="0.72">408+            <stop offset="0%" stop-color={p.hi} />409+            <stop offset="55%" stop-color={p.mid} />410+            <stop offset="100%" stop-color={p.lo} />411+          </radialGradient>412+          <radialGradient id="peach-chirp-finch-head" cx="0.5" cy="0.4" r="0.7">413+            <stop offset="0%" stop-color={p.hi} />414+            <stop offset="100%" stop-color={p.mid} />415+          </radialGradient>416+          <linearGradient id="peach-chirp-finch-belly" x1="0" y1="0" x2="0" y2="1">417+            <stop offset="0%" stop-color={p.cream} stop-opacity="0" />418+            <stop offset="50%" stop-color={p.cream} stop-opacity="0.85" />419+            <stop offset="100%" stop-color={p.cream} stop-opacity="0.5" />420+          </linearGradient>421+        </defs>422+        <ellipse class="peach-chirp-bird-shadow" cx="78" cy="138" rx="40" ry="3.5" fill="rgba(0,0,0,0.45)" />423+        <g class="peach-chirp-bird-aura" opacity="0">424+          <ellipse cx="76" cy="74" rx="60" ry="46" fill={p.hi} opacity="0.18" style={{ filter: "blur(8px)" }} />425+        </g>426+        <g class="peach-chirp-bird-tail">427+          <path d="M 32 92 Q 8 90, 6 80 Q 10 95, 28 100 Z" fill={p.lo} />428+          <path d="M 32 95 Q 4 99, 4 92 Q 10 102, 28 102 Z" fill={p.shade} opacity="0.85" />429+          <path d="M 32 98 Q 8 108, 12 110 Q 20 110, 30 105 Z" fill={p.lo} opacity="0.9" />430+        </g>431+        <g class="peach-chirp-bird-feet" stroke={p.legs} stroke-width="1.6" stroke-linecap="round" fill="none">432+          <line x1="62" y1="118" x2="60" y2="134" />433+          <path d="M 60 134 L 55 138 M 60 134 L 60 139 M 60 134 L 65 138" stroke-width="1.2" />434+          <line x1="84" y1="118" x2="86" y2="134" />435+          <path d="M 86 134 L 81 138 M 86 134 L 86 139 M 86 134 L 91 138" stroke-width="1.2" />436+        </g>437+        <g class="peach-chirp-bird-body">438+          <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-chirp-finch-body)" />439+          <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-chirp-finch-belly)" />440+          <g class="peach-chirp-bird-wing">441+            <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} />442+            <g stroke={p.shade} stroke-width="0.7" stroke-linecap="round" fill="none" opacity="0.55">443+              <path d="M 64 64 Q 72 70, 78 80" />444+              <path d="M 74 62 Q 82 70, 88 82" />445+              <path d="M 84 64 Q 90 72, 94 84" />446+            </g>447+            <path d="M 56 60 C 68 56, 88 60, 102 68" stroke={p.shade} stroke-width="1" fill="none" opacity="0.4" />448+          </g>449+        </g>450+        <g class="peach-chirp-bird-head-group" style={{ "transform-origin": "95px 52px" }}>451+          <g class="peach-chirp-bird-tuft">452+            <path d="M 88 22 Q 86 12, 94 12 Q 94 20, 92 26 Z" fill={p.lo} />453+            <path d="M 96 20 Q 96 10, 104 14 Q 102 22, 100 26 Z" fill={p.shade} opacity="0.95" />454+            <path d="M 104 24 Q 108 16, 114 20 Q 110 28, 106 30 Z" fill={p.lo} opacity="0.85" />455+          </g>456+          <circle cx="98" cy="48" r="26" fill="url(#peach-chirp-finch-head)" />457+          <path d="M 76 46 Q 80 30, 96 26 Q 86 32, 80 48 Z" fill={p.shade} opacity="0.35" />458+          <g class="peach-chirp-bird-eye-group">459+            <circle cx="108" cy="42" r="4.5" fill="#1f0a10" />460+            <circle cx="109.4" cy="40.5" r="1.4" fill="rgba(255,240,225,0.95)" />461+            <circle cx="106.5" cy="44" r="0.5" fill="rgba(255,240,225,0.6)" />462+          </g>463+          <g class="peach-chirp-bird-beak" style={{ "transform-origin": "120px 52px" }}>464+            <path d="M 120 48 L 138 50 L 122 56 Z" fill={p.beak} />465+            <path d="M 120 48 L 138 50 L 128 52 Z" fill={p.beakShade} opacity="0.55" />466+            <path d="M 122 52 L 134 51" stroke={p.beakShade} stroke-width="0.6" opacity="0.6" />467+          </g>468+        </g>469+      </svg>470+    </div>175471   );176472 };177473 178-const ConnectionDot: Component<{ phase: PairingProgress["phase"] }> = (props) => {474+const MobileBird: Component<{ pose: string }> = (props) => {475+  const p = SWALLOW;179476   return (180-    <div class="peach-chirp-dot" data-phase={props.phase}>181-      <span class="peach-chirp-dot-core" />182-      <span class="peach-chirp-dot-halo" />477+    <div class={`peach-chirp-bird peach-chirp-bird-mobile peach-chirp-bird-pose-${props.pose}`}>478+      <svg viewBox="0 0 160 150" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMax meet" style={{ transform: "scaleX(-1)" }}>479+        <defs>480+          <radialGradient id="peach-chirp-swallow-body" cx="0.55" cy="0.35" r="0.75">481+            <stop offset="0%" stop-color={p.hi} />482+            <stop offset="50%" stop-color={p.mid} />483+            <stop offset="100%" stop-color={p.lo} />484+          </radialGradient>485+          <radialGradient id="peach-chirp-swallow-head" cx="0.45" cy="0.4" r="0.7">486+            <stop offset="0%" stop-color={p.hi} />487+            <stop offset="100%" stop-color={p.mid} />488+          </radialGradient>489+          <linearGradient id="peach-chirp-swallow-throat" x1="0" y1="0" x2="0" y2="1">490+            <stop offset="0%" stop-color={p.cream} stop-opacity="0.95" />491+            <stop offset="100%" stop-color={p.cream} stop-opacity="0.2" />492+          </linearGradient>493+        </defs>494+        <ellipse class="peach-chirp-bird-shadow" cx="86" cy="140" rx="34" ry="3" fill="rgba(0,0,0,0.45)" />495+        <g class="peach-chirp-bird-aura" opacity="0">496+          <ellipse cx="80" cy="78" rx="60" ry="42" fill={p.hi} opacity="0.18" style={{ filter: "blur(8px)" }} />497+        </g>498+        <g class="peach-chirp-bird-tail">499+          <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} />500+          <path d="M 30 84 Q 14 78, 6 70 Q 18 84, 30 88 Z" fill={p.shade} opacity="0.7" />501+        </g>502+        <g class="peach-chirp-bird-feet" stroke={p.legs} stroke-width="1.5" stroke-linecap="round" fill="none">503+          <line x1="76" y1="118" x2="74" y2="136" />504+          <path d="M 74 136 L 69 140 M 74 136 L 74 141 M 74 136 L 79 140" stroke-width="1.1" />505+          <line x1="96" y1="118" x2="98" y2="136" />506+          <path d="M 98 136 L 93 140 M 98 136 L 98 141 M 98 136 L 103 140" stroke-width="1.1" />507+        </g>508+        <g class="peach-chirp-bird-body">509+          <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-chirp-swallow-body)" />510+          <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" />511+          <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-chirp-swallow-throat)" />512+          <g class="peach-chirp-bird-wing">513+            <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} />514+            <g fill={p.shade} opacity="0.85">515+              <path d="M 96 88 L 110 92 L 100 96 Z" />516+              <path d="M 88 92 L 102 98 L 92 100 Z" />517+              <path d="M 80 94 L 92 100 L 84 102 Z" />518+            </g>519+            <g stroke={p.shade} stroke-width="0.6" stroke-linecap="round" fill="none" opacity="0.5">520+              <path d="M 58 76 Q 70 82, 80 90" />521+              <path d="M 68 72 Q 80 80, 90 88" />522+              <path d="M 80 70 Q 90 78, 100 86" />523+            </g>524+            <path d="M 50 70 C 64 64, 84 66, 100 72" stroke={p.shade} stroke-width="1" fill="none" opacity="0.55" />525+          </g>526+        </g>527+        <g class="peach-chirp-bird-head-group" style={{ "transform-origin": "116px 64px" }}>528+          <circle cx="118" cy="64" r="20" fill="url(#peach-chirp-swallow-head)" />529+          <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" />530+          <g class="peach-chirp-bird-eye-group">531+            <circle cx="126" cy="60" r="4" fill="#1f0a10" />532+            <circle cx="127.2" cy="58.8" r="1.2" fill="rgba(255,240,225,0.95)" />533+          </g>534+          <g class="peach-chirp-bird-beak" style={{ "transform-origin": "136px 64px" }}>535+            <path d="M 136 60 L 154 62 L 138 66 Z" fill={p.beak} />536+            <path d="M 136 60 L 154 62 L 145 63 Z" fill={p.beakShade} opacity="0.5" />537+            <path d="M 138 63 L 152 63" stroke={p.beakShade} stroke-width="0.5" opacity="0.6" />538+          </g>539+        </g>540+      </svg>183541     </div>184542   );185543 };

view file

src/popup/styles/popup.css+626 −102
@@ -8725,168 +8725,692 @@87258725 }87268726 87278727 /* ──────────────────────────────────────────────────────────────────────────8728-   Chirp pairing flow — audio bootstrap (Phase 1)8728+   Chirp pairing flow — bird-stage animation (Phase 1)87298729    See src/popup/components/ChirpPairingFlow.tsx and the brief §6.2.8730-   The three-act animation is gated on the [data-phase] attribute on8731-   .peach-chirp-pair; each act adjusts opacity / transform on child layers.8730+   Three-act animation gated on the [data-phase] attribute of8731+   .peach-chirp-pair; child layers (birds, chirp arcs, flight wave, dot)8732+   each react via either pose class or visibility flag.8733+   Tokens: accent surfaces use var(--peach-accent); bird illustration8734+   colours are baked into the SVG per the brand-peach exception in §3.1.87328735    ────────────────────────────────────────────────────────────────────── */8736+87338737 .peach-chirp-pair {87348738   position: relative;87358739   display: flex;87368740   flex-direction: column;8737-  align-items: center;8738-  justify-content: center;8739-  padding: 40px 24px 32px;8740-  min-height: 380px;8741-  gap: 24px;8741+  padding: 16px 18px 14px;8742+  min-height: 100%;8743+  gap: 0;87428744   background:8743-    radial-gradient(ellipse at top, color-mix(in srgb, var(--peach-accent) 5%, transparent), transparent 65%),8745+    radial-gradient(ellipse 70% 50% at 30% 0%, color-mix(in srgb, var(--peach-accent) 8%, transparent), transparent 70%),87448746     var(--peach-bg);87458747   color: var(--peach-text);8748+  overflow: hidden;8749+  isolation: isolate;87468750 }8751+.peach-chirp-pair::before {8752+  content: '';8753+  position: absolute;8754+  inset: 0;8755+  border-radius: inherit;8756+  pointer-events: none;8757+  background: radial-gradient(ellipse 90% 30% at 50% 0%, color-mix(in srgb, var(--peach-accent) 6%, transparent), transparent 70%);8758+  z-index: 0;8759+}8760+.peach-chirp-pair > * { position: relative; z-index: 1; }8761+8762+/* Header --------------------------------------------------------------- */8763+.peach-chirp-head {8764+  display: flex; align-items: center; gap: 10px;8765+}8766+.peach-chirp-back {8767+  width: 32px; height: 32px;8768+  display: inline-flex; align-items: center; justify-content: center;8769+  background: color-mix(in srgb, var(--peach-text) 4%, transparent);8770+  border: 1px solid color-mix(in srgb, var(--peach-text) 8%, transparent);8771+  border-radius: 8px;8772+  color: var(--peach-text-dim);8773+  cursor: pointer;8774+  transition: background 0.15s ease, color 0.15s ease;8775+}8776+.peach-chirp-back:hover {8777+  background: color-mix(in srgb, var(--peach-text) 7%, transparent);8778+  color: var(--peach-text);8779+}8780+.peach-chirp-title {8781+  font-family: var(--peach-serif, Georgia, serif);8782+  font-style: italic;8783+  font-size: 22px;8784+  letter-spacing: 0.01em;8785+  color: var(--peach-text);8786+  line-height: 1;8787+}8788+.peach-chirp-head-spacer { flex: 1; }8789+8790+/* Chip row ------------------------------------------------------------- */8791+.peach-chirp-chip-row {8792+  display: flex;8793+  align-items: center;8794+  justify-content: flex-start;8795+  margin-top: 12px;8796+}8797+.peach-chirp-chip {8798+  display: inline-flex; align-items: center; gap: 8px;8799+  padding: 5px 10px 5px 8px;8800+  background: color-mix(in srgb, var(--peach-accent) 10%, transparent);8801+  border: 1px solid color-mix(in srgb, var(--peach-accent) 20%, transparent);8802+  border-radius: 999px;8803+  font-family: var(--peach-mono, ui-monospace, monospace);8804+  font-size: 10.5px;8805+  letter-spacing: 0.04em;8806+  color: var(--peach-accent);8807+}8808+.peach-chirp-chip-dot {8809+  width: 6px; height: 6px; border-radius: 50%;8810+  background: var(--peach-accent);8811+  box-shadow: 0 0 6px color-mix(in srgb, var(--peach-accent) 50%, transparent);8812+}8813+.peach-chirp-chip[data-phase="paired"] {8814+  background: color-mix(in srgb, #4f9eda 12%, transparent);8815+  border-color: color-mix(in srgb, #4f9eda 28%, transparent);8816+  color: #76b6e8;8817+}8818+.peach-chirp-chip[data-phase="paired"] .peach-chirp-chip-dot {8819+  background: #76b6e8;8820+  box-shadow: 0 0 6px rgba(118, 182, 232, 0.55);8821+}8822+.peach-chirp-chip[data-phase="timeout"],8823+.peach-chirp-chip[data-phase="mic_denied"],8824+.peach-chirp-chip[data-phase="version_mismatch"],8825+.peach-chirp-chip[data-phase="handshake_failed"] {8826+  background: color-mix(in srgb, var(--peach-warn, #d29257) 14%, transparent);8827+  border-color: color-mix(in srgb, var(--peach-warn, #d29257) 28%, transparent);8828+  color: var(--peach-warn, #d29257);8829+}8830+.peach-chirp-chip[data-phase="timeout"] .peach-chirp-chip-dot,8831+.peach-chirp-chip[data-phase="mic_denied"] .peach-chirp-chip-dot,8832+.peach-chirp-chip[data-phase="version_mismatch"] .peach-chirp-chip-dot,8833+.peach-chirp-chip[data-phase="handshake_failed"] .peach-chirp-chip-dot {8834+  background: var(--peach-warn, #d29257);8835+  box-shadow: 0 0 6px color-mix(in srgb, var(--peach-warn, #d29257) 55%, transparent);8836+}8837+8838+/* Bird stage ----------------------------------------------------------- */87478839 .peach-chirp-stage {87488840   position: relative;8749-  width: 320px;8750-  height: 130px;8841+  margin: 14px -4px 8px;8842+  height: 180px;8843+  border-radius: 12px;8844+  background:8845+    radial-gradient(ellipse 70% 60% at 50% 30%, color-mix(in srgb, var(--peach-accent) 8%, transparent), transparent 70%),8846+    radial-gradient(ellipse 90% 100% at 50% 100%, color-mix(in srgb, #d8748a 12%, transparent), transparent 70%),8847+    color-mix(in srgb, var(--peach-bg) 92%, #000);8848+  border: 1px solid color-mix(in srgb, var(--peach-text) 6%, transparent);8849+  overflow: hidden;8850+  isolation: isolate;8851+}8852+.peach-chirp-stage::after {8853+  content: '';8854+  position: absolute;8855+  left: 12%; right: 12%;8856+  bottom: 36px;8857+  height: 1px;8858+  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--peach-text) 12%, transparent), transparent);8859+}8860+8861+.peach-chirp-bird-slot {8862+  position: absolute;8863+  bottom: 8px;8864+  width: 100px;8865+  height: 110px;87518866   display: flex;87528867   align-items: flex-end;8753-  justify-content: space-between;8868+  justify-content: center;87548869 }8870+.peach-chirp-bird-slot-desktop { left: 28px; }8871+.peach-chirp-bird-slot-mobile { right: 28px; }8872+8873+/* Mobile bird hops in from the right when handshake begins */8874+.peach-chirp-bird-slot-mobile {8875+  opacity: 1;8876+  transform: translateX(-6px);8877+  transition: opacity 0.4s ease, transform 0.65s cubic-bezier(.5, 1.7, .4, 1);8878+}8879+.peach-chirp-stage.act-listening .peach-chirp-bird-slot-mobile,8880+.peach-chirp-stage.act-chirping .peach-chirp-bird-slot-mobile,8881+.peach-chirp-stage.act-idle .peach-chirp-bird-slot-mobile {8882+  opacity: 0;8883+  transform: translateX(60px) rotate(8deg);8884+}8885+.peach-chirp-stage.act-handshake .peach-chirp-bird-slot-desktop,8886+.peach-chirp-stage.act-paired .peach-chirp-bird-slot-desktop {8887+  transform: translateX(6px);8888+  transition: transform 0.5s cubic-bezier(.4, 1.4, .4, 1);8889+}8890+8891+/* Paired: both birds bob together */8892+.peach-chirp-stage.act-paired .peach-chirp-bird-slot {8893+  animation: peach-chirp-happy-bob 1.2s ease-in-out infinite;8894+}8895+.peach-chirp-stage.act-paired .peach-chirp-bird-slot-mobile { animation-delay: 0.2s; }8896+.peach-chirp-stage.act-paired .peach-chirp-bird-slot-desktop { --bb-x: 6px; }8897+.peach-chirp-stage.act-paired .peach-chirp-bird-slot-mobile { --bb-x: -6px; }8898+@keyframes peach-chirp-happy-bob {8899+  0%, 100% { transform: translateX(var(--bb-x, 0)) translateY(0); }8900+  50%      { transform: translateX(var(--bb-x, 0)) translateY(-3px); }8901+}8902+8903+/* Bird internal -------------------------------------------------------- */87558904 .peach-chirp-bird {8756-  position: relative;8757-  transition: transform 600ms cubic-bezier(0.34, 1.56, 0.64, 1), opacity 320ms ease;8905+  width: 108px;8906+  height: 108px;8907+  transform-origin: 50% 95%;87588908 }8759-.peach-chirp-bird-body { fill: color-mix(in srgb, var(--peach-accent) 75%, #2A1A0E); }8760-.peach-chirp-bird-head { fill: color-mix(in srgb, var(--peach-accent) 65%, #2A1A0E); }8761-.peach-chirp-bird-eye  { fill: var(--peach-text); }8762-.peach-chirp-bird-beak { fill: #C9A47A; }8763-.peach-chirp-bird-wing { stroke: color-mix(in srgb, var(--peach-accent) 55%, #1A0E04); stroke-width: 2.2; stroke-linecap: round; }8909+.peach-chirp-bird svg { width: 100%; height: 100%; overflow: visible; }87648910 8765-/* Idle bob + blink, applied while waiting for the peer */8766-.peach-chirp-bird-desktop[data-phase="idle"],8767-.peach-chirp-bird-desktop[data-phase="listening"],8768-.peach-chirp-bird-desktop[data-phase="chirping"] {8769-  animation: peach-chirp-bob 2.4s ease-in-out infinite;8911+.peach-chirp-bird-body {8912+  transform-origin: 50% 80%;8913+  animation: peach-chirp-bird-bob 2.4s ease-in-out infinite;87708914 }8771-.peach-chirp-bird-desktop[data-phase="timeout"] {8772-  transform: translateY(8px) rotate(-4deg);8773-  opacity: 0.7;8915+@keyframes peach-chirp-bird-bob {8916+  0%, 100% { transform: translateY(0) scale(1, 1); }8917+  50%      { transform: translateY(-1.5px) scale(1.01, 0.99); }87748918 }87758919 8776-/* The mobile bird is hidden until Act 2, then hops in from the right edge */8777-.peach-chirp-bird-mobile {8778-  opacity: 0;8779-  transform: translateX(60px) translateY(8px);8920+.peach-chirp-bird-eye-group {8921+  animation: peach-chirp-blink 5.2s steps(1, end) infinite;8922+  transform-origin: center;8923+  transform-box: fill-box;87808924 }8781-.peach-chirp-bird-mobile[data-phase="handshake"],8782-.peach-chirp-bird-mobile[data-phase="paired"] {8783-  opacity: 1;8784-  transform: translateX(0) translateY(0);8785-  animation: peach-chirp-bob 2.6s ease-in-out infinite;8925+@keyframes peach-chirp-blink {8926+  0%, 92%, 96%, 100% { transform: scaleY(1); }8927+  94%                { transform: scaleY(0.05); }87868928 }8787-.peach-chirp-bird-mobile[data-phase="handshake_failed"] {8788-  opacity: 0.7;8789-  transform: translateX(0) translateY(6px) rotate(4deg);8929+8930+/* Pose: chirping — beak opens, head bobs, aura blooms */8931+.peach-chirp-bird-pose-chirping .peach-chirp-bird-body {8932+  animation:8933+    peach-chirp-bird-bob 2.4s ease-in-out infinite,8934+    peach-chirp-squeeze 1.7s ease-in-out infinite;87908935 }8936+@keyframes peach-chirp-squeeze {8937+  0%, 70%, 100% { transform-origin: 50% 85%; }8938+  78%           { transform: scale(1.04, 0.96) translateY(1px); }8939+  86%           { transform: scale(0.98, 1.02); }8940+}8941+.peach-chirp-bird-pose-chirping .peach-chirp-bird-head-group {8942+  animation: peach-chirp-head-bob 1.7s ease-in-out infinite;8943+}8944+@keyframes peach-chirp-head-bob {8945+  0%, 70%, 100% { transform: translateY(0) rotate(0); }8946+  78%           { transform: translateY(-3px) rotate(-4deg); }8947+  86%           { transform: translateY(-1px) rotate(-2deg); }8948+}8949+.peach-chirp-bird-pose-chirping .peach-chirp-bird-beak {8950+  animation: peach-chirp-beak-open 1.7s ease-in-out infinite;8951+}8952+@keyframes peach-chirp-beak-open {8953+  0%, 70%, 100% { transform: rotate(0) scaleX(1); }8954+  78%           { transform: rotate(-14deg) scaleX(1.05) translateY(-1px); }8955+  86%           { transform: rotate(-6deg); }8956+}8957+.peach-chirp-bird-pose-chirping .peach-chirp-bird-aura {8958+  animation: peach-chirp-aura 1.7s ease-in-out infinite;8959+}8960+@keyframes peach-chirp-aura {8961+  0%, 100% { opacity: 0; }8962+  50%      { opacity: 0.8; }8963+}8964+.peach-chirp-bird-pose-chirping .peach-chirp-bird-tail {8965+  animation: peach-chirp-tail-twitch 1.7s ease-in-out infinite;8966+  transform-origin: 100% 50%;8967+}8968+@keyframes peach-chirp-tail-twitch {8969+  0%, 75%, 100% { transform: rotate(0); }8970+  82%           { transform: rotate(-3deg) translateX(-1px); }8971+}87918972 8792-/* Sound waves between the birds — pulse out during chirping, swap colors in8793-   handshake, fade and merge into the connection dot in the paired state */8794-.peach-chirp-waves {8973+/* Pose: attentive — head tilts toward partner */8974+.peach-chirp-bird-pose-attentive .peach-chirp-bird-head-group {8975+  animation: peach-chirp-head-tilt 0.6s cubic-bezier(.4, 1.6, .5, 1) both;8976+}8977+@keyframes peach-chirp-head-tilt {8978+  0%   { transform: rotate(0) translateY(0); }8979+  60%  { transform: rotate(-10deg) translateY(-1px) translateX(2px); }8980+  100% { transform: rotate(-6deg); }8981+}8982+.peach-chirp-bird-pose-attentive .peach-chirp-bird-tail {8983+  animation: peach-chirp-tail-cock 0.6s cubic-bezier(.4, 1.6, .5, 1) both;8984+  transform-origin: 100% 50%;8985+}8986+@keyframes peach-chirp-tail-cock {8987+  0%   { transform: rotate(0); }8988+  100% { transform: rotate(-4deg); }8989+}8990+8991+/* Pose: happy — wing flutter, tuft perk, tail wag */8992+.peach-chirp-bird-pose-happy .peach-chirp-bird-wing {8993+  transform-origin: 80% 60%;8994+  animation: peach-chirp-wing-flutter 0.32s ease-in-out 5;8995+}8996+@keyframes peach-chirp-wing-flutter {8997+  0%, 100% { transform: rotate(0) translateY(0); }8998+  50%      { transform: rotate(-16deg) translateY(-2px); }8999+}9000+.peach-chirp-bird-pose-happy .peach-chirp-bird-tuft {9001+  transform-origin: 0% 100%;9002+  animation: peach-chirp-tuft-perk 0.4s ease-in-out 3;9003+}9004+@keyframes peach-chirp-tuft-perk {9005+  0%, 100% { transform: rotate(0); }9006+  50%      { transform: rotate(-15deg) translateY(-1px); }9007+}9008+.peach-chirp-bird-pose-happy .peach-chirp-bird-tail {9009+  transform-origin: 100% 50%;9010+  animation: peach-chirp-tail-wag 0.5s ease-in-out 4;9011+}9012+@keyframes peach-chirp-tail-wag {9013+  0%, 100% { transform: rotate(0); }9014+  25%      { transform: rotate(6deg); }9015+  75%      { transform: rotate(-6deg); }9016+}9017+.peach-chirp-bird-pose-happy .peach-chirp-bird-head-group {9018+  animation: peach-chirp-head-perk 0.5s ease-out;9019+}9020+@keyframes peach-chirp-head-perk {9021+  0%   { transform: translateY(0); }9022+  40%  { transform: translateY(-3px) rotate(2deg); }9023+  100% { transform: translateY(0); }9024+}9025+9026+/* Pose: sad — body droops forward */9027+.peach-chirp-bird-pose-sad .peach-chirp-bird-body {9028+  transform: translateY(5px) rotate(-2deg);9029+  animation: none;9030+}9031+.peach-chirp-bird-pose-sad .peach-chirp-bird-head-group {9032+  transform: translateY(4px) rotate(8deg);9033+}9034+.peach-chirp-bird-pose-sad .peach-chirp-bird-tuft { transform: rotate(15deg) translateY(2px); }9035+.peach-chirp-bird-pose-sad .peach-chirp-bird-tail { transform: rotate(8deg) translateY(2px); }9036+9037+.peach-chirp-bird-pose-hidden { opacity: 0; }9038+9039+/* Chirp arcs ----------------------------------------------------------- */9040+.peach-chirp-arcs {87959041   position: absolute;8796-  left: 0;8797-  right: 0;8798-  top: 0;8799-  bottom: 0;8800-  margin: auto;9042+  top: 18px;9043+  width: 96px;9044+  height: 70px;88019045   pointer-events: none;88029046 }8803-.peach-chirp-wave { stroke-width: 2.5; opacity: 0; transition: opacity 240ms ease; }8804-.peach-chirp-wave-out { stroke: color-mix(in srgb, var(--peach-accent) 90%, white 0%); }8805-.peach-chirp-wave-in  { stroke: #5E8FC9; } /* desktop receives, draw in a calmer blue */9047+.peach-chirp-arcs-right { right: -60px; }9048+.peach-chirp-arcs-left  { left: -60px; transform: scaleX(-1); }9049+.peach-chirp-arc {9050+  position: absolute;9051+  top: 50%;9052+  left: 0;9053+  width: 40px;9054+  height: 40px;9055+  margin-top: -20px;9056+  border: 1.2px solid #f4d5a8;9057+  border-color: transparent #f4d5a8 transparent transparent;9058+  border-radius: 50%;9059+  opacity: 0;9060+  animation: peach-chirp-arc-expand 1.7s cubic-bezier(.2, .6, .2, 1) infinite;9061+}9062+@keyframes peach-chirp-arc-expand {9063+  0%   { transform: scale(0.4); opacity: 0; }9064+  20%  { opacity: 0.9; }9065+  100% { transform: scale(1.6); opacity: 0; }9066+}88069067 8807-.peach-chirp-waves[data-phase="listening"] .peach-chirp-wave-out,8808-.peach-chirp-waves[data-phase="chirping"] .peach-chirp-wave-out {8809-  opacity: 0.7;8810-  animation: peach-chirp-wave-pulse 1.6s ease-in-out infinite;9068+/* Flight wave ---------------------------------------------------------- */9069+.peach-chirp-flight-track {9070+  position: absolute;9071+  inset: 32px 36px 38px 36px;9072+  pointer-events: none;88119073 }8812-.peach-chirp-waves[data-phase="handshake"] .peach-chirp-wave-out,8813-.peach-chirp-waves[data-phase="handshake"] .peach-chirp-wave-in {8814-  opacity: 0.85;8815-  animation: peach-chirp-wave-pulse 0.9s ease-in-out infinite;9074+.peach-chirp-flight-wave {9075+  position: absolute;9076+  top: 8px;9077+  left: 76px;9078+  right: 76px;9079+  height: 60px;9080+  width: auto;9081+  opacity: 0;9082+  transition: opacity 0.15s ease;9083+  stroke: #f4d5a8;9084+  fill: none;88169085 }8817-.peach-chirp-waves[data-phase="handshake_failed"] .peach-chirp-wave-out {8818-  opacity: 0.5;8819-  stroke-dasharray: 4 8;9086+.peach-chirp-flight-wave[data-from="mobile"] { transform: scaleX(-1); }9087+.peach-chirp-flight-wave.is-visible { opacity: 1; }9088+.peach-chirp-flight-path,9089+.peach-chirp-flight-path-2 {9090+  stroke-dasharray: 240;9091+  stroke-dashoffset: 240;9092+  stroke-linecap: round;9093+  fill: none;88209094 }9095+.peach-chirp-flight-path   { stroke-width: 1.6; stroke: #f4d5a8; }9096+.peach-chirp-flight-path-2 { stroke-width: 1; stroke: #f4d5a8; opacity: 0.55; }9097+.peach-chirp-flight-wave.is-visible .peach-chirp-flight-path {9098+  animation: peach-chirp-fly 700ms cubic-bezier(.2,.6,.2,1) forwards;9099+}9100+.peach-chirp-flight-wave.is-visible .peach-chirp-flight-path-2 {9101+  animation: peach-chirp-fly 700ms cubic-bezier(.2,.6,.2,1) 80ms forwards;9102+}9103+@keyframes peach-chirp-fly {9104+  0%   { stroke-dashoffset: 240; opacity: 0; }9105+  20%  { opacity: 1; }9106+  100% { stroke-dashoffset: -120; opacity: 0; }9107+}88219108 8822-/* Glowing connection dot — appears for Act 3, settles after the initial pulse */9109+/* Connection dot ------------------------------------------------------- */88239110 .peach-chirp-dot {88249111   position: absolute;8825-  left: 50%;8826-  top: 50%;8827-  width: 18px;8828-  height: 18px;8829-  margin-left: -9px;8830-  margin-top: -9px;9112+  top: 50%; left: 50%;9113+  transform: translate(-50%, -50%);9114+  width: 40px; height: 40px;88319115   opacity: 0;8832-  transition: opacity 300ms ease;9116+  pointer-events: none;9117+  transition: opacity 0.3s ease;9118+  color: var(--peach-accent);88339119 }8834-.peach-chirp-dot[data-phase="paired"] { opacity: 1; }9120+.peach-chirp-dot.is-visible { opacity: 1; }9121+.peach-chirp-dot-glow {9122+  position: absolute; top: 50%; left: 50%;9123+  transform: translate(-50%, -50%);9124+  width: 38px; height: 38px;9125+  border-radius: 50%;9126+  filter: blur(10px);9127+  opacity: 0.55;9128+  background: var(--peach-accent);9129+  animation: peach-chirp-dot-bloom 1.4s ease-out;9130+}88359131 .peach-chirp-dot-core {8836-  position: absolute;8837-  inset: 4px;8838-  border-radius: 999px;9132+  position: absolute; top: 50%; left: 50%;9133+  transform: translate(-50%, -50%);9134+  width: 8px; height: 8px;9135+  border-radius: 50%;88399136   background: var(--peach-accent);8840-  box-shadow: 0 0 16px 2px color-mix(in srgb, var(--peach-accent) 60%, transparent);8841-  animation: peach-chirp-dot-pulse 1.8s ease-in-out infinite;9137+  box-shadow: 0 0 12px currentColor;9138+  animation: peach-chirp-dot-core 1.4s ease-out;88429139 }8843-.peach-chirp-dot-halo {9140+.peach-chirp-dot-ring {9141+  position: absolute; top: 50%; left: 50%;9142+  width: 60px; height: 60px;9143+  transform: translate(-50%, -50%);9144+  animation: peach-chirp-dot-ring 1.4s linear;9145+  stroke: var(--peach-accent);9146+}9147+@keyframes peach-chirp-dot-bloom {9148+  0%   { width: 4px; height: 4px; opacity: 0; }9149+  35%  { width: 60px; height: 60px; opacity: 0.7; }9150+  100% { width: 38px; height: 38px; opacity: 0.55; }9151+}9152+@keyframes peach-chirp-dot-core {9153+  0%   { width: 0; height: 0; }9154+  35%  { width: 12px; height: 12px; }9155+  100% { width: 8px; height: 8px; }9156+}9157+@keyframes peach-chirp-dot-ring {9158+  0%   { transform: translate(-50%, -50%) scale(0.4); opacity: 0; }9159+  35%  { transform: translate(-50%, -50%) scale(1.0); opacity: 0.6; }9160+  100% { transform: translate(-50%, -50%) scale(1.4); opacity: 0; }9161+}9162+9163+/* Motes — drift upward across the stage */9164+.peach-chirp-motes {88449165   position: absolute;88459166   inset: 0;8846-  border-radius: 999px;8847-  border: 2px solid color-mix(in srgb, var(--peach-accent) 40%, transparent);8848-  animation: peach-chirp-dot-halo 1.8s ease-out infinite;9167+  pointer-events: none;88499168 }9169+.peach-chirp-mote {9170+  position: absolute;9171+  width: 2px; height: 2px;9172+  background: color-mix(in srgb, var(--peach-text) 35%, transparent);9173+  border-radius: 50%;9174+  bottom: 32px;9175+  left: calc(50% + (var(--mote-i, 0) - 4) * 24px);9176+  opacity: 0;9177+  animation: peach-chirp-mote-drift 6s linear infinite;9178+  animation-delay: calc(var(--mote-i, 0) * 0.7s);9179+}9180+@keyframes peach-chirp-mote-drift {9181+  0%   { transform: translateY(0); opacity: 0; }9182+  10%  { opacity: 0.6; }9183+  90%  { opacity: 0.1; }9184+  100% { transform: translateY(-120px) translateX(8px); opacity: 0; }9185+}88509186 8851-.peach-chirp-copy { text-align: center; max-width: 360px; }9187+/* Status copy ---------------------------------------------------------- */9188+.peach-chirp-copy-host {9189+  margin: 4px 4px 10px;9190+  text-align: center;9191+  min-height: 50px;9192+}9193+.peach-chirp-copy {9194+  animation: peach-chirp-copy-in 0.35s ease both;9195+}9196+@keyframes peach-chirp-copy-in {9197+  from { opacity: 0; transform: translateY(4px); }9198+  to   { opacity: 1; transform: translateY(0); }9199+}88529200 .peach-chirp-headline {8853-  font-size: 18px;8854-  font-weight: 600;8855-  margin: 0 0 6px;9201+  font-family: var(--peach-serif, Georgia, serif);9202+  font-style: italic;9203+  font-size: 20px;9204+  line-height: 1.15;9205+  letter-spacing: 0.01em;88569206   color: var(--peach-text);9207+  margin-bottom: 5px;88579208 }8858-.peach-chirp-sub {8859-  font-size: 13px;8860-  margin: 0;9209+.peach-chirp-copy-listening .peach-chirp-headline,9210+.peach-chirp-copy-chirping .peach-chirp-headline,9211+.peach-chirp-copy-idle .peach-chirp-headline {9212+  font-family: inherit;9213+  font-style: normal;9214+  font-weight: 500;9215+  font-size: 14.5px;88619216   color: var(--peach-text-dim);9217+  letter-spacing: 0;88629218 }8863-.peach-chirp-mic-help {9219+.peach-chirp-copy-paired .peach-chirp-headline {9220+  color: var(--peach-text);9221+  font-weight: 600;9222+}9223+.peach-chirp-sub {88649224   font-size: 12px;9225+  line-height: 1.45;88659226   color: var(--peach-text-muted);8866-  text-align: center;8867-  max-width: 320px;9227+  max-width: 280px;9228+  margin: 0 auto;9229+  text-wrap: pretty;88689230 }8869-.peach-chirp-cancel,9231+9232+/* Status action -------------------------------------------------------- */9233+.peach-chirp-action {9234+  margin: 2px 4px 10px;9235+}9236+.peach-chirp-action-waiting {9237+  display: flex;9238+  align-items: center;9239+  justify-content: space-between;9240+  gap: 12px;9241+}9242+.peach-chirp-cancel {9243+  min-width: 96px;9244+}9245+.peach-chirp-listening {9246+  display: inline-flex; align-items: center; gap: 4px;9247+  font-family: var(--peach-mono, ui-monospace, monospace);9248+  font-size: 9.5px;9249+  text-transform: uppercase;9250+  letter-spacing: 0.12em;9251+  color: var(--peach-text-muted);9252+  padding: 8px 10px;9253+}9254+.peach-chirp-listen-dot {9255+  width: 4px; height: 4px;9256+  border-radius: 50%;9257+  background: color-mix(in srgb, var(--peach-accent) 55%, transparent);9258+  animation: peach-chirp-listen-pulse 1.2s ease-in-out infinite;9259+}9260+.peach-chirp-listen-dot:nth-child(2) { animation-delay: 0.2s; }9261+.peach-chirp-listen-dot:nth-child(3) { animation-delay: 0.4s; }9262+.peach-chirp-listen-label { margin-left: 6px; }9263+@keyframes peach-chirp-listen-pulse {9264+  0%, 100% { opacity: 0.25; transform: scale(0.9); }9265+  50%      { opacity: 1;     transform: scale(1.15); }9266+}9267+9268+.peach-chirp-action-handshake {9269+  display: flex;9270+  flex-direction: column;9271+  align-items: center;9272+  gap: 8px;9273+  padding: 8px 4px;9274+}9275+.peach-chirp-meter {9276+  width: 100%;9277+  max-width: 280px;9278+  height: 2px;9279+  background: color-mix(in srgb, var(--peach-text) 8%, transparent);9280+  border-radius: 2px;9281+  position: relative;9282+  overflow: hidden;9283+}9284+.peach-chirp-meter-bar {9285+  position: absolute;9286+  top: 0; bottom: 0;9287+  width: 30%;9288+  left: -30%;9289+  background: linear-gradient(90deg, transparent, var(--peach-accent), transparent);9290+  animation: peach-chirp-meter-sweep 1.4s ease-in-out infinite;9291+}9292+@keyframes peach-chirp-meter-sweep {9293+  0%   { left: -30%; }9294+  100% { left: 100%; }9295+}9296+.peach-chirp-meter-label {9297+  font-family: var(--peach-mono, ui-monospace, monospace);9298+  font-size: 9.5px;9299+  text-transform: uppercase;9300+  letter-spacing: 0.14em;9301+  color: var(--peach-text-muted);9302+}9303+9304+.peach-chirp-action-paired {9305+  display: flex;9306+  align-items: center;9307+  gap: 10px;9308+}9309+.peach-chirp-device-confirm {9310+  flex: 1;9311+  display: inline-flex;9312+  align-items: center;9313+  gap: 8px;9314+  padding: 9px 10px 9px 12px;9315+  border-radius: 10px;9316+  background: color-mix(in srgb, #76b6e8 10%, transparent);9317+  border: 1px solid color-mix(in srgb, #76b6e8 26%, transparent);9318+  font-size: 12.5px;9319+  color: var(--peach-text);9320+  min-width: 0;9321+}9322+.peach-chirp-device-confirm-glyph {9323+  display: inline-flex;9324+  color: #76b6e8;9325+}9326+.peach-chirp-device-confirm-name {9327+  flex: 1;9328+  overflow: hidden;9329+  text-overflow: ellipsis;9330+  white-space: nowrap;9331+}9332+.peach-chirp-device-confirm-trust {9333+  font-family: var(--peach-mono, ui-monospace, monospace);9334+  font-size: 9.5px;9335+  text-transform: uppercase;9336+  letter-spacing: 0.1em;9337+  color: color-mix(in srgb, #76b6e8 85%, var(--peach-text));9338+  padding: 2px 6px;9339+  border-radius: 4px;9340+  background: color-mix(in srgb, #76b6e8 12%, transparent);9341+}9342+.peach-chirp-done {9343+  min-width: 80px;9344+}88709345 .peach-chirp-retry {88719346   align-self: center;9347+  display: block;9348+  margin: 6px auto 0;88729349   min-width: 120px;88739350 }9351+.peach-chirp-mic-help {9352+  font-size: 12px;9353+  color: var(--peach-text-muted);9354+  text-align: center;9355+  max-width: 320px;9356+  margin: 0 auto;9357+}88749358 8875-@keyframes peach-chirp-bob {8876-  0%, 100% { transform: translateY(0); }8877-  50% { transform: translateY(-3px); }9359+/* Footer --------------------------------------------------------------- */9360+.peach-chirp-foot {9361+  margin: 10px -4px -2px;9362+  margin-top: auto;88789363 }8879-@keyframes peach-chirp-wave-pulse {8880-  0%, 100% { opacity: 0.3; transform: scale(0.95); }8881-  50% { opacity: 0.95; transform: scale(1.05); }9364+.peach-chirp-foot-divider {9365+  height: 1px;9366+  background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--peach-text) 14%, transparent), transparent);9367+  margin-bottom: 10px;88829368 }8883-@keyframes peach-chirp-dot-pulse {8884-  0%, 100% { transform: scale(1); }8885-  50% { transform: scale(1.18); }9369+.peach-chirp-foot-text {9370+  font-size: 11px;9371+  line-height: 1.5;9372+  color: var(--peach-text-muted);9373+  text-align: center;9374+  padding: 0 8px;9375+  text-wrap: pretty;88869376 }8887-@keyframes peach-chirp-dot-halo {8888-  0% { transform: scale(0.6); opacity: 0.6; }8889-  100% { transform: scale(2.4); opacity: 0; }9377+9378+/* Screen-reader-only live region. Kept off the visual flow but read by9379+   assistive tech as the pairing phase advances. */9380+.peach-sr-only {9381+  position: absolute;9382+  width: 1px; height: 1px;9383+  padding: 0; margin: -1px;9384+  overflow: hidden;9385+  clip: rect(0, 0, 0, 0);9386+  white-space: nowrap;9387+  border: 0;9388+}9389+9390+/* Reduced motion — stage simplifies to opacity transitions only */9391+@media (prefers-reduced-motion: reduce) {9392+  .peach-chirp-bird-body,9393+  .peach-chirp-bird-head-group,9394+  .peach-chirp-bird-beak,9395+  .peach-chirp-bird-wing,9396+  .peach-chirp-bird-tuft,9397+  .peach-chirp-bird-tail,9398+  .peach-chirp-bird-eye-group,9399+  .peach-chirp-bird-aura,9400+  .peach-chirp-arc,9401+  .peach-chirp-flight-path,9402+  .peach-chirp-flight-path-2,9403+  .peach-chirp-dot-glow,9404+  .peach-chirp-dot-core,9405+  .peach-chirp-dot-ring,9406+  .peach-chirp-mote,9407+  .peach-chirp-meter-bar,9408+  .peach-chirp-listen-dot,9409+  .peach-chirp-bird-slot,9410+  .peach-chirp-bird-slot-mobile {9411+    animation: none !important;9412+    transition: opacity 0.2s linear !important;9413+  }88909414 }88919415 88929416 /* ─────────────────────────────────────────────────────────────────

view file