/**
* inject.ts — autofill icon + selector overlay, injected into pages.
*
* Runs as a content script. Injects the Peach icon next to detected login
* fields, anchors it via event-driven positioning (no perpetual rAF — see
* Projects.md §4.7), and renders the inline autofill selector in a shadow
* DOM. Never holds decrypted vault data directly: all entry lookups go
* through `chrome.runtime.sendMessage`, and only the minimum fields needed
* to fill come back across the boundary.
*/
import { ACCENT_COLORS, type PasswordPolicy, type SavedIdentity } from "../shared/types";
import { generatePassword } from "../shared/generate";
import { detectPasswordPolicy } from "./policy";
let accentHex = "#E8956D";
let popupTheme: "dark" | "light" = "dark";
chrome.runtime.sendMessage({ type: "GET_SETTINGS" }, (resp: { ok: boolean; data?: { accentColor?: string; theme?: string } } | undefined) => {
const data = resp?.data;
const name = data?.accentColor ?? "peach";
accentHex = ACCENT_COLORS.find(c => c.name === name)?.hex ?? "#E8956D";
popupTheme = data?.theme === "light" ? "light" : "dark";
});
// 8-digit hex alpha helpers for the accent-derived shadow tokens.
// `#E8956D` + alpha `28` → `#E8956D28`. The browser parses the trailing
// two hex digits as the alpha channel.
function withAlpha(hex: string, alpha: string): string {
// Defensive: only 6-digit hex inputs are supported here. ACCENT_COLORS
// always exposes 6-digit hex, so this is a guard against future changes
// shipping shorthand colors that would silently break the var injection.
return /^#[0-9a-f]{6}$/i.test(hex) ? hex + alpha : hex;
}
export interface SelectorEntry {
id: string;
title: string;
username?: string;
email?: string;
url?: string;
hasTotp?: boolean;
/** True when the entry also has a passkey credential attached. */
hasPasskey?: boolean;
}
interface SelectorOptions {
domain?: string;
searchableEntries?: SelectorEntry[];
/** True when shown on a signup form — changes header label and entry-click behaviour */
isSignup?: boolean;
/**
* Derived identity list (emails then usernames) for signup mode (State 10).
* When present, renderIdentityHelper() is used instead of renderList().
*/
savedIdentities?: SavedIdentity[];
/**
* Called after the user picks a saved identity (signup mode only) or
* after successfully saving a new entry on a signup form.
* Lets the caller fill the visible username + password fields.
*/
onFillFields?: (username: string, password: string) => void;
/**
* When true, show an inline unlock form instead of the entries list.
* Used when the vault is locked so the user can unlock without opening
* the popup.
*/
locked?: boolean;
/**
* Called after a successful inline unlock — the caller should re-query
* entries and re-open the selector with real data.
*/
onUnlocked?: () => void;
/**
* Called when the user explicitly dismisses the selector (Escape, Tab, or
* click outside) — NOT when an entry is selected or on auto-close.
* Used by the caller to suppress spurious reopens on the same field.
*/
onDismiss?: () => void;
}
function getDomainFromUrl(url: string): string {
try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return ""; }
}
function keySvgStr(): string {
return ``;
}
function plusSvgStr(): string {
return ``;
}
function searchSvgStr(): string {
return ``;
}
function backSvgStr(): string {
return ``;
}
function peachIconSvg(): string {
// Peach body stays brand-peach regardless of `accentHex` — a sage or
// sky accent must not turn the autofill icon green or blue. Accent
// color still tints surrounding glow/hover halos below.
return ``;
}
function checkSvgStr(): string {
return ``;
}
function smartUrl(): { origin: string; title: string; loginSubdomain: boolean } {
const { hostname, origin } = location;
const parts = hostname.split(".");
const AUTH_PREFIXES = ["accounts", "auth", "login", "signin", "sign-in", "sso",
"id", "identity", "secure", "portal", "my", "am", "idp", "connect", "oauth",
"oidc", "federation", "saml", "cas", "ldap"];
const loginSubdomain = parts.length > 2 && AUTH_PREFIXES.includes(parts[0]!.toLowerCase());
const rootName = parts.length >= 2 ? parts[parts.length - 2]! : parts[0]!;
const title = rootName.charAt(0).toUpperCase() + rootName.slice(1);
return { origin, title, loginSubdomain };
}
/**
* Shows a bottom-of-page toast after filling credentials when the entry has
* never matched the current site — offering to add the current origin to the
* entry's `additionalUrls` so future visits auto-suggest it. Closes after 9s.
*/
export function showRememberSiteToast(
pageDomain: string,
entryId: string,
): void {
const host = document.createElement("div");
host.style.cssText = "position:fixed;bottom:22px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:auto;";
host.style.setProperty("--peach-accent", accentHex);
const shadow = host.attachShadow({ mode: "closed" });
const style = document.createElement("style");
style.textContent = `
.bar {
display:flex;align-items:center;gap:10px;padding:10px 14px;
background:#131109;border:1px solid rgba(240,232,216,0.09);
border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,0.75);
font-family:Inter,sans-serif;white-space:nowrap;
animation:tf-in 180ms cubic-bezier(0.16,1,0.3,1) both;
}
@keyframes tf-in { from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none} }
.txt { font-size:11px;color:rgba(240,232,216,0.55);font-family:"IBM Plex Mono",monospace; }
.yes {
padding:5px 12px;border-radius:6px;border:none;cursor:pointer;font-size:11px;
background:var(--peach-accent);color:#fff;transition:background 100ms;font-family:Inter,sans-serif;
}
.yes:hover { background:color-mix(in srgb, var(--peach-accent) 82%, #000); }
.no {
padding:5px 10px;border-radius:6px;border:none;cursor:pointer;font-size:11px;
background:rgba(240,232,216,0.07);color:rgba(240,232,216,0.45);font-family:Inter,sans-serif;
}
`;
shadow.appendChild(style);
const bar = document.createElement("div"); bar.className = "bar";
const txt = document.createElement("span"); txt.className = "txt";
txt.textContent = `Remember ${pageDomain} for this entry?`;
const yesBtn = document.createElement("button"); yesBtn.className = "yes";
yesBtn.textContent = "Remember";
yesBtn.addEventListener("click", async () => {
await chrome.runtime.sendMessage({
type: "ADD_ADDITIONAL_URL",
payload: { id: entryId, url: location.origin },
});
host.remove();
});
const noBtn = document.createElement("button"); noBtn.className = "no";
noBtn.textContent = "Not now";
noBtn.addEventListener("click", () => host.remove());
bar.append(txt, yesBtn, noBtn);
shadow.appendChild(bar);
document.body.appendChild(host);
setTimeout(() => host.remove(), 9000);
}
interface SmartGenerator {
root: HTMLElement;
getPassword: () => string;
/** Copy the current password to the clipboard with the standard pulse UX. */
copy: () => Promise;
}
interface SmartGeneratorOptions {
/** Site-detected policy — clamps length, sets symbol toggle, exposes hint. */
policy: PasswordPolicy;
/**
* Seed value: used as the displayed password without regenerating, e.g. the
* password the user already typed into the page. The generator still
* regenerates the moment any control changes.
*/
initialPassword?: string;
/** Fires every time the password regenerates (initial + on changes). */
onChange?: (pw: string) => void;
}
/**
* Build the inline-popup smart-password generator. Mirrors the main popup's
* GeneratorDrawer (compact mode): readout row + length slider + 4 charset
* pills + a policy hint that names the constraints Peach detected on the
* page. Writing this in pure DOM rather than reusing the Solid component
* keeps the content-script bundle small.
*/
function createSmartGenerator(opts: SmartGeneratorOptions): SmartGenerator {
const policy = opts.policy ?? {};
// Start at site max (strongest password the site accepts), fall back to 64
let length = policy.maxLength ?? 64;
if (policy.minLength) length = Math.max(length, policy.minLength);
if (length < 8) length = 8;
if (length > 128) length = 128;
let upper = true;
let lower = true;
let numbers = true;
let symbols = !policy.forbidsSymbols;
if (policy.requiresSymbols) symbols = true;
const forbiddenChars = policy.forbiddenChars ?? "";
let password = "";
const root = document.createElement("div");
root.className = "pwgen-block";
const displayRow = root.appendChild(document.createElement("div"));
displayRow.className = "pwgen-display-row";
const output = displayRow.appendChild(document.createElement("input")) as HTMLInputElement;
output.type = "text";
output.className = "pwgen-output";
output.setAttribute("autocomplete", "off");
output.setAttribute("spellcheck", "false");
output.setAttribute("autocorrect", "off");
output.setAttribute("autocapitalize", "off");
const regenBtn = displayRow.appendChild(document.createElement("button"));
regenBtn.className = "pwgen-icon-btn";
regenBtn.type = "button";
regenBtn.setAttribute("aria-label", "Regenerate password");
regenBtn.title = "Regenerate";
regenBtn.innerHTML = ``;
const copyBtn = displayRow.appendChild(document.createElement("button"));
copyBtn.className = "pwgen-icon-btn";
copyBtn.type = "button";
copyBtn.setAttribute("aria-label", "Copy password");
copyBtn.title = "Copy";
copyBtn.innerHTML = ``;
const controls = root.appendChild(document.createElement("div"));
controls.className = "pwgen-controls-row";
const slider = controls.appendChild(document.createElement("input"));
slider.type = "range";
slider.className = "pwgen-slider";
slider.min = "8";
slider.max = "128";
slider.value = String(length);
if (policy.minLength) slider.min = String(Math.max(8, policy.minLength));
if (policy.maxLength) slider.max = String(Math.min(128, policy.maxLength));
const lenNum = controls.appendChild(document.createElement("span"));
lenNum.className = "pwgen-len-num pwgen-len-num--edit";
lenNum.textContent = String(length);
lenNum.addEventListener("click", () => {
const li = document.createElement("input");
li.type = "text"; li.inputMode = "numeric";
li.className = "pwgen-len-input"; li.value = String(length);
lenNum.replaceWith(li);
requestAnimationFrame(() => li.select());
function commitLenEdit() {
const n = parseInt(li.value, 10);
if (!isNaN(n)) {
length = Math.max(parseInt(slider.min, 10), Math.min(parseInt(slider.max, 10), n));
slider.value = String(length);
regenerate();
}
lenNum.textContent = String(length);
li.replaceWith(lenNum);
}
li.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); commitLenEdit(); } else if (e.key === "Escape") { li.replaceWith(lenNum); } });
li.addEventListener("blur", commitLenEdit);
});
const pills = root.appendChild(document.createElement("div"));
pills.className = "pwgen-pills";
function makePill(label: string, on: boolean, onToggle: (next: boolean) => void): HTMLButtonElement {
const b = document.createElement("button");
b.type = "button";
b.className = `pwgen-pill ${on ? "is-on" : ""}`;
b.textContent = label;
b.addEventListener("click", () => {
const next = !b.classList.contains("is-on");
b.classList.toggle("is-on", next);
onToggle(next);
});
return b;
}
pills.appendChild(makePill("A-Z", upper, (next) => { upper = next; regenerate(); }));
pills.appendChild(makePill("a-z", lower, (next) => { lower = next; regenerate(); }));
pills.appendChild(makePill("0-9", numbers, (next) => { numbers = next; regenerate(); }));
pills.appendChild(makePill("!@#", symbols, (next) => {
// The site forbids symbols — UI lets the user override but still warn.
symbols = next;
regenerate();
}));
// Policy hint — surfaces what Peach read off the site so the user knows the
// generator is already tuned to it. Only shows when something interesting
// was detected.
const hintParts: string[] = [];
if (policy.minLength) hintParts.push(`min ${policy.minLength}`);
if (policy.maxLength) hintParts.push(`max ${policy.maxLength}`);
if (policy.requiresSymbols) hintParts.push("requires symbols");
if (policy.forbidsSymbols) hintParts.push("no symbols");
if (policy.forbiddenChars) hintParts.push(`excludes ${policy.forbiddenChars.slice(0, 6)}${policy.forbiddenChars.length > 6 ? "…" : ""}`);
if (hintParts.length > 0) {
const hint = root.appendChild(document.createElement("div"));
hint.className = "pwgen-policy-hint";
const accent = document.createElement("span");
accent.className = "pwgen-hint-accent";
accent.textContent = "site rules · ";
const text = document.createTextNode(hintParts.join(" · "));
hint.appendChild(accent);
hint.appendChild(text);
}
function regenerate() {
// Falls back gracefully when the user toggles every charset off — the
// shared generator forces lowercase in that case rather than throwing.
password = generatePassword({
length,
includeUppercase: upper,
includeLowercase: lower,
includeNumbers: numbers,
includeSymbols: symbols,
forbiddenChars,
});
output.value = password;
opts.onChange?.(password);
}
output.addEventListener("input", () => {
password = output.value;
length = password.length;
slider.value = String(Math.max(8, Math.min(128, length)));
lenNum.textContent = String(length);
opts.onChange?.(password);
});
slider.addEventListener("input", () => {
length = parseInt(slider.value, 10);
lenNum.textContent = String(length);
regenerate();
});
regenBtn.addEventListener("click", regenerate);
let copyResetTimer: ReturnType | null = null;
async function copy(): Promise {
try {
await navigator.clipboard.writeText(password);
copyBtn.classList.add("is-copied");
if (copyResetTimer) clearTimeout(copyResetTimer);
copyResetTimer = setTimeout(() => copyBtn.classList.remove("is-copied"), 1400);
} catch {
// Clipboard API may be denied — silently no-op; user still has regen UI.
}
}
copyBtn.addEventListener("click", () => { void copy(); });
if (opts.initialPassword) {
password = opts.initialPassword;
output.value = password;
opts.onChange?.(password);
} else {
regenerate();
}
return {
root,
getPassword: () => password,
copy,
};
}
export function observeAnchorPosition(
anchorEl: HTMLElement,
onUpdate: () => void
): () => void {
let rafId = 0;
let burstUntil = 0;
const runUpdate = () => {
rafId = 0;
onUpdate();
if (performance.now() < burstUntil) rafId = requestAnimationFrame(runUpdate);
};
const scheduleUpdate = () => { if (!rafId) rafId = requestAnimationFrame(runUpdate); };
const scheduleBurst = (ms = 220) => {
burstUntil = Math.max(burstUntil, performance.now() + ms);
scheduleUpdate();
};
const resizeObserver = new ResizeObserver(() => scheduleBurst());
resizeObserver.observe(anchorEl);
if (anchorEl.parentElement) resizeObserver.observe(anchorEl.parentElement);
const mutationObserver = new MutationObserver(() => scheduleBurst());
mutationObserver.observe(anchorEl, { attributes: true, attributeFilter: ["class", "style", "type", "hidden"] });
if (anchorEl.parentElement) {
mutationObserver.observe(anchorEl.parentElement, { attributes: true, attributeFilter: ["class", "style", "hidden"] });
}
const onScroll = () => scheduleUpdate();
window.addEventListener("scroll", onScroll, true);
window.addEventListener("resize", onScroll);
window.visualViewport?.addEventListener("resize", onScroll);
window.visualViewport?.addEventListener("scroll", onScroll);
anchorEl.addEventListener("focus", () => scheduleBurst());
anchorEl.addEventListener("input", () => scheduleBurst());
scheduleBurst(260);
return () => {
if (rafId) cancelAnimationFrame(rafId);
resizeObserver.disconnect();
mutationObserver.disconnect();
window.removeEventListener("scroll", onScroll, true);
window.removeEventListener("resize", onScroll);
window.visualViewport?.removeEventListener("resize", onScroll);
window.visualViewport?.removeEventListener("scroll", onScroll);
};
}
export function injectFieldIcon(field: HTMLInputElement, onClick: () => void): () => void {
const button = document.createElement("button");
button.type = "button";
button.setAttribute("aria-label", "Peach autofill");
button.title = "Fill with Peach";
button.innerHTML = peachIconSvg();
// position: fixed is load-bearing — not absolute. Absolute positioning
// inherits the nearest positioned ancestor's box, which on sites with
// overflow:hidden or sticky parents clips or offsets the icon. Fixed
// anchors to the viewport and escapes all parent layout.
button.style.cssText = `
position: fixed; z-index: 2147483646;
width: 26px; height: 26px; cursor: pointer;
pointer-events: auto; border: none; padding: 3px;
display: inline-flex; align-items: center; justify-content: center;
background: transparent; border-radius: 999px;
transition: opacity 140ms ease, background 120ms ease, box-shadow 120ms ease, transform 80ms ease;
opacity: 0;
`;
button.addEventListener("mouseenter", () => {
button.style.background = `color-mix(in srgb, ${accentHex} 12%, transparent)`;
button.style.boxShadow = `0 0 0 1px color-mix(in srgb, ${accentHex} 18%, transparent), 0 0 14px color-mix(in srgb, ${accentHex} 12%, transparent)`;
button.style.transform = "scale(1.08)";
});
button.addEventListener("mouseleave", () => {
button.style.background = "transparent";
button.style.boxShadow = "none";
button.style.transform = "none";
});
button.addEventListener("pointerdown", e => { e.stopPropagation(); e.preventDefault(); });
button.addEventListener("click", e => { e.stopPropagation(); e.preventDefault(); onClick(); });
document.body.appendChild(button);
function updatePosition() {
const rect = field.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
button.style.opacity = "0"; button.style.pointerEvents = "none";
return;
}
const sz = 26;
const inset = Math.max(5, Math.min(10, rect.height * 0.22));
const top = rect.top + rect.height / 2 - sz / 2;
// Clamp left so the icon stays fully inside the field's right edge, never
// spilling onto an adjacent sibling button.
const minLeft = rect.left + 4;
const left = Math.max(minLeft, rect.right - sz - inset);
// Hide if the field is too narrow to fit the icon cleanly (< 40px)
if (rect.width < 40) {
button.style.opacity = "0"; button.style.pointerEvents = "none";
return;
}
button.style.top = `${top}px`;
button.style.left = `${left}px`;
button.style.opacity = "1"; button.style.pointerEvents = "auto";
}
updatePosition();
requestAnimationFrame(() => { button.style.opacity = "1"; });
const stopObserving = observeAnchorPosition(field, updatePosition);
return () => { stopObserving(); button.remove(); };
}
// All theme-dependent values resolve through tokens scoped to
// .peach-inline-popup[data-theme="dark|light"]. Accent variables
// (--peach-accent, --peach-accent-dim, --peach-glow, --peach-border-accent)
// are identical in both themes — only surface tokens differ. See
// Projects.md §4.14: hardcoded hex inside this stylesheet breaks the
// theme switch — only literal brand-peach in peachIconSvg may stay raw.
const SELECTOR_STYLES = `
:host { all: initial; }
*,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }
.peach-inline-popup[data-theme="dark"] {
--peach-bg: #1a1410;
--peach-bg-card: #221c16;
--peach-text: #f5f0ea;
--peach-text-dim: #8a7a6a;
--peach-text-muted: #4a3a2a;
--peach-border: rgba(255,255,255,0.07);
--peach-input-bg: rgba(255,255,255,0.05);
--peach-input-border: rgba(255,255,255,0.08);
}
.peach-inline-popup[data-theme="light"] {
--peach-bg: #fdf8f4;
--peach-bg-card: #f5ede4;
--peach-text: #1c1310;
--peach-text-dim: #6b5a4a;
--peach-text-muted: #a89080;
--peach-border: rgba(0,0,0,0.08);
--peach-input-bg: rgba(0,0,0,0.04);
--peach-input-border: rgba(0,0,0,0.10);
}
.peach-inline-popup {
background: var(--peach-bg);
border: 1px solid var(--peach-border-accent, var(--peach-border));
border-radius: 14px;
overflow: hidden;
box-shadow:
0 8px 40px var(--peach-glow, transparent),
0 2px 12px rgba(0,0,0,0.5);
font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
animation: peach-enter 200ms cubic-bezier(0.16,1,0.3,1) forwards;
transition: height 220ms cubic-bezier(0.16,1,0.3,1);
}
/* Light theme: shadow against a light page is too strong otherwise — drop both
the accent glow and the dark drop-shadow opacity to keep the popup elegant. */
.peach-inline-popup[data-theme="light"] {
box-shadow:
0 8px 40px color-mix(in srgb, var(--peach-accent) 10%, transparent),
0 2px 12px rgba(0,0,0,0.12);
}
.peach-inline-popup.is-exiting {
animation: peach-exit 130ms cubic-bezier(0.4,0,1,1) forwards;
pointer-events: none;
}
@keyframes peach-enter {
0% { opacity: 0; transform: translateY(-8px) scale(0.96); filter: blur(3px); }
65% { opacity: 1; filter: blur(0); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes peach-exit {
0% { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
100% { opacity: 0; transform: translateY(-4px) scale(0.98); filter: blur(2px); }
}
@keyframes peach-slide-in {
from { opacity: 0; transform: translateX(12px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes peach-slide-back {
from { opacity: 0; transform: translateX(-12px); }
to { opacity: 1; transform: translateX(0); }
}
.peach-content[data-dir="forward"] { animation: peach-slide-in 150ms cubic-bezier(0.16,1,0.3,1) both; }
.peach-content[data-dir="back"] { animation: peach-slide-back 150ms cubic-bezier(0.16,1,0.3,1) both; }
/* Domain header */
.domain-header {
padding: 9px 13px 7px;
border-bottom: 1px solid var(--peach-border);
display: flex; align-items: center; justify-content: space-between;
gap: 8px;
}
.domain-header-text {
display: flex; flex-direction: column; gap: 2px;
min-width: 0; flex: 1;
}
.domain-eyebrow {
font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;
color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;
}
.domain-name {
font-size: 10px; color: color-mix(in srgb, var(--peach-accent) 60%, transparent);
font-family: "IBM Plex Mono", monospace;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.domain-header-actions {
display: inline-flex; align-items: center; gap: 5px;
flex-shrink: 0;
}
.domain-add-btn,
.domain-search-btn {
flex-shrink: 0;
width: 22px; height: 22px;
display: inline-flex; align-items: center; justify-content: center;
background: color-mix(in srgb, var(--peach-accent) 12%, transparent);
border: 1px solid color-mix(in srgb, var(--peach-accent) 28%, transparent);
border-radius: 6px;
color: var(--peach-accent);
cursor: pointer; padding: 0;
transition: background 120ms, border-color 120ms, transform 80ms;
font-family: "IBM Plex Mono", monospace;
font-size: 14px; line-height: 1;
}
.domain-add-btn:hover,
.domain-search-btn:hover {
background: color-mix(in srgb, var(--peach-accent) 22%, transparent);
border-color: color-mix(in srgb, var(--peach-accent) 48%, transparent);
transform: scale(1.06);
}
.domain-add-btn:active,
.domain-search-btn:active { transform: scale(0.96); }
/* Search */
.search-wrap {
padding: 9px 12px 7px;
border-bottom: 1px solid var(--peach-border);
}
.search {
width: 100%; background: var(--peach-input-bg);
border: 1px solid var(--peach-input-border);
border-radius: 7px; color: var(--peach-text); padding: 7px 10px;
font-size: 12px; outline: none; font-family: "IBM Plex Mono", monospace;
transition: border-color 120ms, box-shadow 120ms;
}
.search:focus {
border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);
}
.search::placeholder { color: var(--peach-text-muted); }
/* Results */
.results { max-height: 256px; overflow-y: auto; scrollbar-width: none; }
.results::-webkit-scrollbar { display: none; }
/* Entry rows */
.entry {
display: flex; align-items: center; padding: 8px 12px;
cursor: pointer; border-bottom: 1px solid var(--peach-border);
transition: background 70ms; position: relative; outline: none;
}
.entry:last-child { border-bottom: none; }
.entry:hover { background: color-mix(in srgb, var(--peach-text) 4%, transparent); }
.entry.focused { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }
.entry.focused::before {
content: ""; position: absolute; left: 0; top: 3px; bottom: 3px;
width: 2px; border-radius: 2px;
background: color-mix(in srgb, var(--peach-accent) 55%, transparent);
}
.entry-icon {
width: 28px; height: 28px; border-radius: 7px; flex-shrink: 0;
background: var(--peach-input-bg); border: 1px solid var(--peach-border);
display: flex; align-items: center; justify-content: center;
overflow: hidden; margin-right: 10px;
}
.entry-icon img { width: 15px; height: 15px; border-radius: 2px; display: block; }
.entry-icon.is-new {
background: color-mix(in srgb, var(--peach-accent) 8%, transparent);
border-color: color-mix(in srgb, var(--peach-accent) 15%, transparent);
color: color-mix(in srgb, var(--peach-accent) 80%, transparent);
}
.entry-info { flex: 1; min-width: 0; }
.entry-title {
font-size: 12.5px; color: var(--peach-text);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
line-height: 1.3;
}
.entry-title.is-new { color: color-mix(in srgb, var(--peach-accent) 90%, transparent); font-size: 12px; }
.entry-sub {
font-size: 10px; color: var(--peach-text-dim);
font-family: "IBM Plex Mono", monospace;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
margin-top: 1px;
}
.totp-badge {
font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;
border-radius: 4px; background: color-mix(in srgb, var(--peach-accent) 10%, transparent);
color: color-mix(in srgb, var(--peach-accent) 75%, transparent);
font-family: "IBM Plex Mono", monospace; flex-shrink: 0; margin-left: 8px;
}
.passkey-badge {
font-size: 8px; letter-spacing: 0.04em; padding: 2px 6px;
border-radius: 4px;
background: color-mix(in srgb, var(--peach-accent) 14%, transparent);
color: color-mix(in srgb, var(--peach-accent) 85%, transparent);
border: 1px solid color-mix(in srgb, var(--peach-accent) 24%, transparent);
font-family: "IBM Plex Mono", monospace; flex-shrink: 0; margin-left: 6px;
}
.divider { height: 1px; background: var(--peach-border); margin: 2px 0; }
.empty-hint {
padding: 14px 13px; font-size: 11px; color: var(--peach-text-dim);
font-family: "IBM Plex Mono", monospace;
}
/* Create form */
.create-form-wrap {
animation: peach-slide-in 180ms cubic-bezier(0.16,1,0.3,1) both;
}
.create-head {
display: flex; align-items: center; gap: 8px;
padding: 10px 12px; border-bottom: 1px solid var(--peach-border);
}
.back-btn {
background: transparent; border: none; cursor: pointer;
color: var(--peach-text-dim); padding: 3px; border-radius: 5px;
display: flex; align-items: center; line-height: 0;
transition: color 100ms, background 100ms;
}
.back-btn:hover { color: var(--peach-text); background: var(--peach-input-bg); }
.create-label { font-size: 12px; color: var(--peach-text); font-weight: 500; flex: 1; }
/* minmax(0, 1fr) caps the grid track at the popup width — without it,
long monospace content (e.g. a 60-char generated password) would
auto-size the track wider than the popup and overflow the right edge. */
.create-body { padding: 12px; display: grid; grid-template-columns: minmax(0, 1fr); gap: 9px; }
.f-group { display: grid; grid-template-columns: minmax(0, 1fr); gap: 3px; }
.f-label {
font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;
color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;
}
.f-input {
background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);
border-radius: 7px; color: var(--peach-text); padding: 7px 10px;
font-size: 11px; font-family: "IBM Plex Mono", monospace;
outline: none; width: 100%; transition: border-color 120ms, box-shadow 120ms;
}
.f-input:focus { border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent); }
.f-input::placeholder { color: var(--peach-text-muted); }
.f-input.url-f { font-size: 10px; color: var(--peach-text-dim); letter-spacing: 0.01em; }
.f-row { display: flex; gap: 6px; }
.f-row .f-input { flex: 1; min-width: 0; }
.gen-btn {
padding: 0 10px; border-radius: 7px; border: none;
background: color-mix(in srgb, var(--peach-accent) 10%, transparent);
color: color-mix(in srgb, var(--peach-accent) 80%, transparent);
font-size: 10px; font-family: "IBM Plex Mono", monospace;
cursor: pointer; white-space: nowrap; flex-shrink: 0;
display: flex; align-items: center; gap: 4px;
transition: background 100ms;
}
.gen-btn:hover { background: color-mix(in srgb, var(--peach-accent) 18%, transparent); }
.gen-toggle-link {
background: none; border: none; padding: 2px 0;
color: color-mix(in srgb, var(--peach-accent) 60%, transparent);
font-size: 10px; font-family: "IBM Plex Mono", monospace;
cursor: pointer; text-align: left; transition: color 100ms;
}
.gen-toggle-link:hover { color: var(--peach-accent); }
.save-btn {
width: 100%; padding: 8px; border-radius: 8px; border: none;
background: var(--peach-accent); color: #fff; font-size: 12px;
font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;
transition: background 120ms, box-shadow 120ms, transform 80ms;
letter-spacing: 0.01em;
}
.save-btn:hover {
background: color-mix(in srgb, var(--peach-accent) 82%, #000);
box-shadow: 0 4px 20px var(--peach-glow, transparent);
}
.save-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.url-hint {
font-size: 9px; color: color-mix(in srgb, var(--peach-accent) 45%, transparent);
font-family: "IBM Plex Mono", monospace; font-style: italic;
}
/* Success */
.success-wrap {
display: flex; flex-direction: column; align-items: center;
padding: 24px 16px 20px; gap: 8px;
animation: su-in 280ms cubic-bezier(0.16,1,0.3,1) both;
}
@keyframes su-in { from{opacity:0;transform:scale(0.9)}to{opacity:1;transform:none} }
.success-ring {
width: 40px; height: 40px; border-radius: 50%;
background: rgba(123,144,96,0.10); border: 1.5px solid rgba(123,144,96,0.25);
display: flex; align-items: center; justify-content: center;
}
.success-text { font-size: 12.5px; color: var(--peach-text); font-weight: 500; }
.success-sub { font-size: 10px; color: var(--peach-text-dim); font-family:"IBM Plex Mono",monospace; }
/* Silver platter (State 1 — single match) */
.silver-platter {
padding: 14px 14px 12px;
display: flex; flex-direction: column; gap: 6px;
}
.sp-title {
font-size: 14px; font-weight: 500; color: var(--peach-text);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
line-height: 1.25;
}
.sp-identity-row {
display: flex; align-items: baseline; gap: 5px; margin-bottom: 3px;
}
.sp-identity-label {
font-size: 9px; font-weight: 600; letter-spacing: 0.05em;
text-transform: uppercase; color: var(--peach-text-muted); flex-shrink: 0;
}
.sp-username {
font-size: 12px; color: var(--peach-text-dim);
font-family: "IBM Plex Mono", monospace;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
margin-bottom: 0;
}
.sp-identity-row + .sp-fill-btn, .sp-username + .sp-fill-btn { margin-top: 6px; }
.sp-fill-btn {
width: 100%; padding: 11px; border-radius: 9px; border: none;
background: var(--peach-accent); color: #fff;
font-size: 13px; font-weight: 500; cursor: pointer;
font-family: Inter, sans-serif; letter-spacing: 0.01em;
display: flex; align-items: center; justify-content: center; min-height: 38px;
transition: filter 120ms ease, transform 80ms ease, box-shadow 120ms ease, background 120ms ease;
}
.sp-fill-btn:hover {
filter: brightness(1.1);
box-shadow: 0 4px 20px var(--peach-glow, transparent);
transform: translateY(-1px);
}
.sp-fill-btn:active { transform: translateY(0); filter: brightness(0.95); }
.sp-fill-btn.filled {
background: #16a34a; transform: none; filter: none;
box-shadow: 0 4px 16px rgba(22,163,74,0.35);
cursor: default;
}
.sp-fill-btn:disabled { cursor: default; }
.sp-actions-group {
margin-top: 6px;
display: flex; flex-direction: column; gap: 4px;
}
.sp-actions-label {
font-size: 8px; letter-spacing: 0.18em; text-transform: uppercase;
color: var(--peach-text-muted);
font-family: "IBM Plex Mono", monospace;
text-align: center;
}
.sp-actions {
display: flex; gap: 4px; justify-content: space-between; align-items: center;
}
.sp-action-btn {
flex: 1; height: 28px; border: none; border-radius: 6px;
background: transparent; color: var(--peach-text-dim);
font-size: 12px; cursor: pointer;
transition: background 120ms ease, color 120ms ease;
font-family: Inter, sans-serif; letter-spacing: 0.04em;
display: inline-flex; align-items: center; justify-content: center; gap: 4px;
white-space: nowrap; overflow: hidden;
}
.sp-action-btn:hover:not(:disabled) {
background: color-mix(in srgb, var(--peach-text) 5%, transparent);
color: var(--peach-text);
}
.sp-action-btn:disabled { opacity: 0.3; cursor: default; }
.sp-action-btn.flashed {
color: var(--peach-accent);
background: color-mix(in srgb, var(--peach-accent) 12%, transparent);
}
/* TOTP tray — shown below the silver-platter secondary actions */
.sp-tray {
margin-top: 8px;
padding: 10px 12px;
border-radius: 9px;
background: var(--peach-input-bg);
border: 1px solid var(--peach-input-border);
display: flex; align-items: center; gap: 12px;
animation: peach-slide-in 180ms cubic-bezier(0.16,1,0.3,1) both;
}
.sp-tray-label {
font-size: 8px; letter-spacing: 0.12em; text-transform: uppercase;
color: var(--peach-text-dim); font-family: "IBM Plex Mono", monospace;
flex-shrink: 0;
}
.sp-totp-code {
font-size: 18px; font-weight: 500;
color: var(--peach-text);
font-family: "IBM Plex Mono", monospace;
letter-spacing: 0.12em;
flex: 1;
text-align: center;
}
.sp-totp-ring {
width: 22px; height: 22px; flex-shrink: 0;
}
.sp-totp-ring .ring-track { stroke: var(--peach-input-border); }
.sp-totp-ring .ring-fill {
stroke: var(--peach-accent);
transition: stroke-dashoffset 1s linear;
transform: rotate(-90deg);
transform-origin: center;
}
.sp-totp-copy {
background: transparent; border: none; cursor: pointer;
padding: 4px 8px; border-radius: 5px;
color: var(--peach-text-dim); font-size: 11px;
font-family: Inter, sans-serif;
transition: background 120ms, color 120ms;
}
.sp-totp-copy:hover {
background: color-mix(in srgb, var(--peach-text) 5%, transparent);
color: var(--peach-text);
}
/* Card silver platter (State 7) — reuses .sp-* primary action classes */
.card-platter {
padding: 12px 14px 10px;
display: flex; flex-direction: column; gap: 4px;
}
.cp-network {
font-size: 10px; font-weight: 600;
color: var(--peach-accent);
letter-spacing: 0.08em; text-transform: uppercase;
font-family: Inter, sans-serif;
}
.cp-nickname {
font-size: 14px; font-weight: 500; color: var(--peach-text);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
line-height: 1.2;
}
.cp-last4 {
font-size: 12px; color: var(--peach-text-dim);
font-family: "IBM Plex Mono", monospace;
letter-spacing: 0.04em;
margin-top: 2px;
}
.cp-meta {
font-size: 11px; color: var(--peach-text-dim);
font-family: "IBM Plex Mono", monospace;
margin-top: 2px;
display: flex; gap: 10px; align-items: center;
}
.cp-meta-sep { opacity: 0.5; }
.cp-fill-wrap { margin-top: 10px; }
/* Locked state */
.locked-wrap {
display: flex; flex-direction: column; align-items: stretch;
padding: 14px 13px 13px; gap: 9px;
}
.locked-head { display: flex; align-items: center; gap: 8px; padding-bottom: 2px; }
.locked-icon {
width: 24px; height: 24px; border-radius: 6px; flex-shrink: 0;
background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);
display: flex; align-items: center; justify-content: center;
}
.locked-title { font-size: 11.5px; color: var(--peach-text); font-weight: 500; }
.locked-sub { font-size: 10.5px; line-height: 1.4; color: var(--peach-text-muted); text-align: center; margin: 2px 0 2px; }
.locked-input {
width: 100%; background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);
border-radius: 7px; color: var(--peach-text); padding: 7px 10px;
font-size: 11px; font-family: "IBM Plex Mono", monospace;
outline: none; transition: border-color 120ms, box-shadow 120ms;
}
.locked-input:focus { border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent); }
.locked-input::placeholder { color: var(--peach-text-muted); }
.locked-btn {
width: 100%; padding: 8px; border-radius: 8px; border: none;
background: var(--peach-accent); color: #fff; font-size: 12px;
font-family: Inter, sans-serif; cursor: pointer; font-weight: 500;
transition: background 120ms;
letter-spacing: 0.01em;
box-shadow: 0 2px 16px var(--peach-glow, transparent);
animation: peach-pulse-glow 2.4s ease-in-out infinite;
}
.locked-btn:hover { background: color-mix(in srgb, var(--peach-accent) 82%, #000); }
.locked-btn:disabled { opacity: 0.4; cursor: not-allowed; animation: none; }
.locked-error {
font-size: 10px; color: rgba(220,80,60,0.85);
font-family: "IBM Plex Mono", monospace;
}
@keyframes peach-pulse-glow {
0%, 100% { box-shadow: 0 2px 16px var(--peach-glow, transparent); }
50% { box-shadow: 0 2px 28px var(--peach-glow, transparent),
0 0 48px var(--peach-glow, transparent); }
}
/* ── State 10: identity helper ─────────────────────────────── */
.identity-list {
max-height: 192px; overflow-y: auto; padding: 4px 0;
scrollbar-width: none;
}
.identity-list::-webkit-scrollbar { display: none; }
.peach-identity-row {
display: flex; align-items: center; gap: 10px;
padding: 9px 12px; cursor: pointer; border-radius: 8px;
transition: background 100ms ease;
}
.peach-identity-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }
.peach-identity-value {
font-family: "IBM Plex Mono", monospace; font-size: 13px;
color: var(--peach-text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.peach-identity-meta { font-size: 10px; color: var(--peach-text-muted); flex-shrink: 0; }
.identity-alt-row {
display: flex; align-items: center; padding: 8px 12px; cursor: pointer;
border-radius: 8px; transition: background 100ms ease;
color: var(--peach-text-dim); font-size: 12px; font-family: Inter, sans-serif;
}
.identity-alt-row:hover { background: color-mix(in srgb, var(--peach-text) 6%, transparent); }
.identity-escape-row {
display: flex; align-items: center; padding: 8px 12px; cursor: pointer;
border-radius: 8px; transition: background 100ms ease; outline: none;
color: var(--peach-accent); font-size: 11px; font-family: Inter, sans-serif;
margin-top: 2px;
}
.identity-escape-row:hover, .identity-escape-row:focus { background: color-mix(in srgb, var(--peach-accent) 8%, transparent); }
.identity-alt-input-wrap { display: none; padding: 0 12px 8px; gap: 6px; align-items: center; }
.identity-alt-input-wrap.open { display: flex; }
.identity-alt-input {
flex: 1; height: 32px; padding: 0 10px; border-radius: 7px;
border: 1px solid var(--peach-input-border); background: var(--peach-input-bg);
color: var(--peach-text); font-family: "IBM Plex Mono", monospace; font-size: 12px;
outline: none; transition: border-color 120ms;
}
.identity-alt-input:focus {
border-color: color-mix(in srgb, var(--peach-accent) 40%, transparent);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--peach-accent) 10%, transparent);
}
.identity-alt-confirm {
height: 32px; padding: 0 12px; border-radius: 7px; border: none;
background: var(--peach-accent); color: #fff; font-size: 12px;
cursor: pointer; font-family: Inter, sans-serif; flex-shrink: 0;
}
/* ── Generator view (after identity picked) ────────────────── */
.gen-view { padding: 12px; display: flex; flex-direction: column; gap: 8px; }
.gen-confirmed {
display: flex; align-items: center; gap: 8px; padding: 7px 10px;
background: color-mix(in srgb, var(--peach-accent) 10%, transparent);
border-radius: 8px;
}
.gen-confirmed-check { color: var(--peach-accent); font-size: 13px; font-weight: 600; }
.gen-confirmed-val {
font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;
}
.gen-label {
font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
color: var(--peach-text-muted); font-family: "IBM Plex Mono", monospace;
}
.gen-pw-row { display: flex; gap: 6px; align-items: center; }
.gen-pw-val {
flex: 1; padding: 8px 10px; border-radius: 7px;
background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);
font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.gen-regen-btn {
height: 34px; padding: 0 10px; border-radius: 7px; border: none;
background: color-mix(in srgb, var(--peach-text) 8%, transparent);
color: var(--peach-text-dim); font-size: 11px; cursor: pointer;
font-family: "IBM Plex Mono", monospace; transition: background 100ms ease;
}
.gen-regen-btn:hover { background: color-mix(in srgb, var(--peach-text) 14%, transparent); }
.gen-fill-btn {
padding: 10px; border-radius: 8px; border: none;
background: var(--peach-accent); color: #fff; font-size: 13px; font-weight: 500;
cursor: pointer; font-family: Inter, sans-serif; letter-spacing: 0.01em;
transition: filter 120ms ease, transform 80ms ease;
}
.gen-fill-btn:hover { filter: brightness(1.08); transform: translateY(-1px); }
.gen-fill-btn:active { transform: translateY(0); }
/* ── Smart password generator (parity with main UI compact mode) ─── */
.pwgen-block { display: flex; flex-direction: column; gap: 8px; }
.pwgen-display-row {
display: flex; align-items: center; gap: 6px;
padding: 8px 10px; border-radius: 7px;
background: var(--peach-input-bg); border: 1px solid var(--peach-input-border);
}
.pwgen-output {
flex: 1; min-width: 0; width: 0;
font-family: "IBM Plex Mono", monospace; font-size: 12px;
color: var(--peach-text);
background: transparent; border: none; outline: none; padding: 0;
}
.pwgen-icon-btn {
flex-shrink: 0;
width: 26px; height: 26px;
display: inline-flex; align-items: center; justify-content: center;
background: transparent; border: 1px solid var(--peach-input-border);
border-radius: 6px; color: var(--peach-text-dim); cursor: pointer;
padding: 0; transition: background 100ms, color 100ms, border-color 100ms;
}
.pwgen-icon-btn:hover {
background: color-mix(in srgb, var(--peach-accent) 10%, transparent);
color: var(--peach-accent);
border-color: color-mix(in srgb, var(--peach-accent) 30%, transparent);
}
.pwgen-icon-btn.is-copied {
background: color-mix(in srgb, var(--peach-accent) 18%, transparent);
color: var(--peach-accent);
border-color: color-mix(in srgb, var(--peach-accent) 45%, transparent);
}
.pwgen-controls-row {
display: flex; align-items: center; gap: 8px;
flex-wrap: wrap;
}
.pwgen-slider {
flex: 1; min-width: 110px;
accent-color: var(--peach-accent);
cursor: pointer; height: 2px; background: transparent;
}
.pwgen-len-num {
font-family: "IBM Plex Mono", monospace; font-size: 11px;
color: var(--peach-accent); min-width: 22px; text-align: right;
}
.pwgen-len-num--edit { cursor: text; border-bottom: 1px dotted color-mix(in srgb, var(--peach-accent) 50%, transparent); }
.pwgen-len-input {
font-family: "IBM Plex Mono", monospace; font-size: 11px;
color: var(--peach-accent); width: 28px; text-align: right;
background: transparent; border: none; border-bottom: 1px solid var(--peach-accent);
outline: none; padding: 0; min-width: 22px;
}
@keyframes identity-suggest-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: none; }
}
.identity-suggest-list { display: flex; flex-direction: column; gap: 1px; margin-top: 3px;
animation: identity-suggest-in 180ms cubic-bezier(0.16,1,0.3,1) both; }
.identity-filter-row { display: flex; align-items: center; gap: 4px; padding: 2px 0 4px; }
.identity-collapse-btn {
margin-left: auto; background: transparent; border: none; cursor: pointer;
color: var(--peach-text-muted); padding: 2px 4px; display: flex; align-items: center;
line-height: 0; border-radius: 3px;
transition: color 100ms ease, transform 220ms cubic-bezier(0.16,1,0.3,1);
}
.identity-collapse-btn:hover { color: var(--peach-text); }
.identity-collapse-btn.is-collapsed { transform: rotate(-90deg); }
.identity-rows { display: grid; grid-template-rows: 1fr;
transition: grid-template-rows 220ms cubic-bezier(0.16,1,0.3,1); }
.identity-rows.is-collapsed { grid-template-rows: 0fr; }
.identity-rows-inner { overflow-y: auto; min-height: 0; max-height: 148px;
scrollbar-width: thin; scrollbar-color: var(--peach-text-muted) transparent;
mask-image: linear-gradient(to bottom, black calc(100% - 18px), transparent 100%);
-webkit-mask-image: linear-gradient(to bottom, black calc(100% - 18px), transparent 100%); }
.identity-rows-inner::-webkit-scrollbar { width: 4px; }
.identity-rows-inner::-webkit-scrollbar-track { background: transparent; }
.identity-rows-inner::-webkit-scrollbar-thumb { background: var(--peach-text-muted); border-radius: 2px; }
.identity-filter-pill {
padding: 2px 7px; border-radius: 999px;
border: 1px solid var(--peach-input-border);
background: transparent; color: var(--peach-text-muted);
font-family: "IBM Plex Mono", monospace; font-size: 9px;
cursor: pointer; transition: border-color 80ms, background 80ms, color 80ms;
}
.identity-filter-pill.is-on {
border-color: color-mix(in srgb, var(--peach-accent) 50%, transparent);
background: color-mix(in srgb, var(--peach-accent) 10%, transparent);
color: var(--peach-accent);
}
.identity-suggest-row {
display: flex; align-items: center; gap: 5px;
width: 100%; padding: 4px 6px; border-radius: 5px; border: none;
background: transparent; cursor: pointer; text-align: left;
transition: background 80ms;
}
.identity-suggest-row:hover,
.identity-suggest-row:focus { background: color-mix(in srgb, var(--peach-accent) 10%, transparent); outline: none; }
.identity-suggest-val {
font-family: "IBM Plex Mono", monospace; font-size: 10px; color: var(--peach-text);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;
}
.identity-suggest-type {
font-family: "IBM Plex Mono", monospace; font-size: 8px; font-weight: 600;
flex-shrink: 0; padding: 1px 3px; border-radius: 3px;
}
.identity-suggest-type--email {
color: color-mix(in srgb, var(--peach-accent) 70%, transparent);
background: color-mix(in srgb, var(--peach-accent) 12%, transparent);
}
.identity-suggest-type--username {
color: var(--peach-text-muted);
background: color-mix(in srgb, var(--peach-text) 8%, transparent);
}
.identity-suggest-meta {
font-family: "IBM Plex Mono", monospace; font-size: 9px;
color: var(--peach-text-muted); flex-shrink: 0;
}
.pwgen-pills { display: flex; gap: 5px; flex-wrap: wrap; }
.pwgen-pill {
padding: 3px 8px; border-radius: 999px;
border: 1px solid var(--peach-input-border);
background: transparent; color: var(--peach-text-muted);
font-family: "IBM Plex Mono", monospace; font-size: 9px;
letter-spacing: 0.06em; cursor: pointer;
transition: all 130ms ease;
}
.pwgen-pill.is-on {
border-color: color-mix(in srgb, var(--peach-accent) 50%, transparent);
background: color-mix(in srgb, var(--peach-accent) 10%, transparent);
color: var(--peach-accent);
}
.pwgen-pill:not(.is-on):hover {
color: var(--peach-text-dim);
border-color: var(--peach-text-muted);
}
.pwgen-policy-hint {
font-size: 10px; color: var(--peach-text-muted);
font-family: "IBM Plex Mono", monospace; letter-spacing: 0.04em;
line-height: 1.3;
}
.pwgen-policy-hint .pwgen-hint-accent { color: var(--peach-accent); }
/* ── Save offer (after password filled) ───────────────────── */
.save-offer { padding: 12px; display: flex; flex-direction: column; gap: 8px; }
.save-offer-title {
font-size: 12px; font-weight: 600; color: var(--peach-text); font-family: Inter, sans-serif;
}
.save-offer-row {
display: flex; align-items: center; gap: 8px; padding: 6px 8px;
border-radius: 7px; background: var(--peach-input-bg);
border: 1px solid var(--peach-input-border);
}
.save-offer-lbl {
font-size: 10px; color: var(--peach-text-muted); width: 54px; flex-shrink: 0;
font-family: "IBM Plex Mono", monospace;
}
.save-offer-val {
font-family: "IBM Plex Mono", monospace; font-size: 12px; color: var(--peach-text);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;
}
.save-offer-btns { display: flex; gap: 8px; margin-top: 4px; }
.save-offer-save {
flex: 1; padding: 9px; border-radius: 8px; border: none;
background: var(--peach-accent); color: #fff; font-size: 13px;
cursor: pointer; font-family: Inter, sans-serif; transition: filter 120ms ease;
}
.save-offer-save:hover { filter: brightness(1.08); }
.save-offer-skip {
flex: 1; padding: 9px; border-radius: 8px; border: none;
background: color-mix(in srgb, var(--peach-text) 8%, transparent);
color: var(--peach-text-dim); font-size: 13px; cursor: pointer;
font-family: Inter, sans-serif; transition: background 100ms ease;
}
.save-offer-skip:hover { background: color-mix(in srgb, var(--peach-text) 14%, transparent); }
`;
export function showAutofillSelector(
anchorEl: HTMLInputElement,
entries: SelectorEntry[],
onSelect: (id: string) => void,
options: SelectorOptions = {}
): () => void {
const host = document.createElement("div");
// When anchorEl is inside a focus-trapping modal the modal's JS focus trap fires
// when Peach's internal inputs gain focus (they are outside the modal's DOM subtree),
// immediately blurring them and closing the identity suggest list.
// Fix: detect the containing form or modal and append the host INSIDE it so focus
// traps see Peach as within the modal. Use position:fixed so the popup positions
// correctly relative to the viewport regardless of the container's CSS context —
// overflow:hidden and most transforms do not affect fixed children.
//
// Detection priority:
// 1.