cd6e70449d
ensureLicence() en tete d'init(), avant tout le reste : un ecran d'authentification qui s'affiche puis se fait recouvrir donnerait l'impression que le coffre etait accessible. L'ecran reprend les classes de l'ecran d'authentification — zero style nouveau hormis un z-index. Le statut est demande deux fois avant de le montrer : un demarrage a froid lent manquerait la fenetre de reponse, et le silence se lirait comme un refus. Une cle refusee ne dit pas pourquoi, et le journal note le verdict sans jamais la cle. ATTENTION : tant que LicencePublicKeyBlob est a zero, l'ecran est infranchissable. Poser la cle publique avant de deployer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3200 lines
95 KiB
CSS
3200 lines
95 KiB
CSS
/* ============================================================
|
|
Vault — UI V2 (Linear/Notion design system)
|
|
Dark default, cyan/violet accent, glassmorphism subtil.
|
|
============================================================ */
|
|
|
|
/* ---- 1. RESET & TOKENS ----------------------------------- */
|
|
|
|
*,*::before,*::after { box-sizing: border-box; }
|
|
html, body { margin: 0; padding: 0; height: 100%; }
|
|
body {
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
color: var(--text);
|
|
background: var(--bg);
|
|
-webkit-font-smoothing: antialiased;
|
|
overflow: hidden;
|
|
}
|
|
|
|
button { font-family: inherit; cursor: pointer; }
|
|
input, select, textarea { font-family: inherit; font-size: inherit; }
|
|
kbd {
|
|
font-family: 'JetBrains Mono', 'SF Mono', Consolas, monospace;
|
|
font-size: 11px;
|
|
padding: 2px 6px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
background: var(--bg-elev);
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
:root, [data-theme="dark"] {
|
|
/* Slightly lighter surfaces + warmer tones, brighter text-dim/faint
|
|
and more visible borders. Less "black hole" feel. */
|
|
--bg: #15151c;
|
|
--bg-elev: #1d1d26;
|
|
--bg-elev-2: #292932;
|
|
--bg-elev-3: #34343f; /* one step above elev-2 — hover / raised bits */
|
|
--bg-glass: rgba(29,29,38,0.78);
|
|
--border: #353541;
|
|
--border-soft: #25252e;
|
|
--text: #f1f1f5;
|
|
--text-dim: #b8b8c4;
|
|
--text-faint: #82828f;
|
|
--accent: #06b6d4;
|
|
--accent-hover: #22d3ee;
|
|
--accent-soft: rgba(6,182,212,0.12);
|
|
--accent-fg: #ffffff; /* text/icon colour on an accent background */
|
|
--accent-2: #a78bfa;
|
|
--danger: #f43f5e;
|
|
--danger-soft: rgba(244,63,94,0.12);
|
|
--success: #10b981;
|
|
--warning: #f59e0b;
|
|
--shadow: 0 8px 24px rgba(0,0,0,0.4);
|
|
--shadow-lg: 0 24px 48px rgba(0,0,0,0.55);
|
|
--blur: saturate(140%) blur(20px);
|
|
--radius: 12px;
|
|
--radius-sm: 8px;
|
|
--radius-lg: 16px;
|
|
--t-fast: 120ms cubic-bezier(0.16,1,0.3,1);
|
|
--t-base: 200ms cubic-bezier(0.16,1,0.3,1);
|
|
}
|
|
|
|
[data-theme="light"] {
|
|
--bg: #f7f7f5;
|
|
--bg-elev: #ffffff;
|
|
--bg-elev-2: #efefec;
|
|
--bg-elev-3: #e3e3dd; /* one step above elev-2 — hover / raised bits */
|
|
--bg-glass: rgba(255,255,255,0.92);
|
|
--border: #dcdcd7;
|
|
--border-soft: #ebebe6;
|
|
--text: #1a1a1f;
|
|
--text-dim: #57575e;
|
|
--text-faint: #8e8e96;
|
|
--accent: #0891b2; /* cyan-600 — darker for better contrast on white */
|
|
--accent-hover: #0e7490;
|
|
--accent-soft: rgba(8,145,178,0.10);
|
|
--accent-2: #7c3aed;
|
|
--shadow: 0 4px 16px rgba(0,0,0,0.06);
|
|
--shadow-lg: 0 16px 36px rgba(0,0,0,0.08);
|
|
--blur: saturate(160%) blur(16px);
|
|
}
|
|
[data-theme="light"] .auth-screen {
|
|
background:
|
|
radial-gradient(ellipse at top, rgba(8,145,178,0.08), transparent 50%),
|
|
radial-gradient(ellipse at bottom right, rgba(124,58,237,0.06), transparent 50%),
|
|
var(--bg);
|
|
}
|
|
|
|
svg { width: 16px; height: 16px; flex-shrink: 0; }
|
|
|
|
/* ---- 2. UTILITIES ---------------------------------------- */
|
|
|
|
.is-hidden { display: none !important; }
|
|
.hint { color: var(--text-faint); font-weight: 400; font-size: 12px; margin-left: 4px; }
|
|
|
|
/* ---- 3. BUTTONS ------------------------------------------ */
|
|
|
|
.btn {
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
gap: 6px;
|
|
padding: 8px 14px;
|
|
border: 1px solid transparent;
|
|
border-radius: var(--radius-sm);
|
|
font-size: 13px; font-weight: 500; line-height: 1;
|
|
transition: all var(--t-fast);
|
|
white-space: nowrap;
|
|
}
|
|
.btn-primary { background: var(--accent); color: white; }
|
|
.btn-primary:hover { background: var(--accent-hover); }
|
|
.btn.is-danger { background: var(--danger); color: white; }
|
|
.btn.is-danger:hover { background: #e11d48; }
|
|
.btn-ghost {
|
|
background: transparent;
|
|
color: var(--text-dim);
|
|
border-color: var(--border);
|
|
}
|
|
.btn-ghost:hover { background: var(--bg-elev-2); color: var(--text); }
|
|
.btn-block { width: 100%; }
|
|
.btn-sm { padding: 6px 10px; font-size: 12px; }
|
|
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
.btn-link {
|
|
background: transparent;
|
|
color: var(--text-dim);
|
|
border-color: transparent;
|
|
text-decoration: underline;
|
|
text-underline-offset: 2px;
|
|
}
|
|
.btn-link:hover { color: var(--accent); background: transparent; }
|
|
|
|
.icon-btn {
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
width: 32px; height: 32px;
|
|
background: transparent;
|
|
border: 1px solid transparent;
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text-dim);
|
|
transition: all var(--t-fast);
|
|
}
|
|
.icon-btn:hover { background: var(--bg-elev-2); color: var(--text); }
|
|
.icon-btn-sm { width: 24px; height: 24px; }
|
|
.icon-btn-sm svg { width: 14px; height: 14px; }
|
|
|
|
/* ---- 4. FORM CONTROLS ------------------------------------ */
|
|
|
|
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
|
|
.field > span {
|
|
font-size: 12px; font-weight: 500; color: var(--text-dim);
|
|
display: flex; align-items: center;
|
|
}
|
|
.field input[type="text"],
|
|
.field input[type="password"],
|
|
.field input[type="search"],
|
|
.field input[type="email"],
|
|
.field select {
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text);
|
|
transition: all var(--t-fast);
|
|
}
|
|
.field input:focus, .field select:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
background: var(--bg);
|
|
box-shadow: 0 0 0 3px var(--accent-soft);
|
|
}
|
|
|
|
.input-with-action { position: relative; display: flex; align-items: center; gap: 4px; }
|
|
.input-with-action input { padding-right: 80px; }
|
|
.input-with-action .icon-btn {
|
|
position: absolute;
|
|
top: 50%; transform: translateY(-50%);
|
|
}
|
|
.input-with-action .icon-btn:nth-of-type(1) { right: 38px; }
|
|
.input-with-action .icon-btn:nth-of-type(2) { right: 4px; }
|
|
|
|
.strength-bar {
|
|
height: 4px;
|
|
background: var(--border);
|
|
border-radius: 2px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
margin-top: 4px;
|
|
}
|
|
.strength-bar::after {
|
|
content: '';
|
|
position: absolute; inset: 0;
|
|
width: var(--strength, 0%);
|
|
background: linear-gradient(90deg, var(--danger), var(--warning) 50%, var(--success));
|
|
transition: width var(--t-base);
|
|
}
|
|
|
|
input[type="range"] {
|
|
-webkit-appearance: none; appearance: none;
|
|
width: 100%; height: 4px;
|
|
background: var(--border);
|
|
border-radius: 2px;
|
|
}
|
|
input[type="range"]::-webkit-slider-thumb {
|
|
-webkit-appearance: none; appearance: none;
|
|
width: 16px; height: 16px;
|
|
background: var(--accent);
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.check {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
padding: 6px 10px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
transition: all var(--t-fast);
|
|
}
|
|
.check:has(input:checked) {
|
|
background: var(--accent-soft);
|
|
border-color: var(--accent);
|
|
color: var(--accent);
|
|
}
|
|
.check input { display: none; }
|
|
|
|
/* ---- 5. AUTH SCREEN -------------------------------------- */
|
|
|
|
.auth-screen {
|
|
position: fixed; inset: 0;
|
|
display: grid; place-items: center;
|
|
padding: 24px;
|
|
background:
|
|
radial-gradient(ellipse at top, var(--accent-soft), transparent 50%),
|
|
radial-gradient(ellipse at bottom right, rgba(167,139,250,0.08), transparent 50%),
|
|
var(--bg);
|
|
}
|
|
#licenceScreen { z-index: 300; }
|
|
.auth-card {
|
|
width: 100%; max-width: 400px;
|
|
padding: 32px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
.auth-brand { text-align: center; margin-bottom: 24px; }
|
|
.auth-brand-icon {
|
|
width: 40px; height: 40px;
|
|
padding: 10px;
|
|
background: var(--accent-soft);
|
|
color: var(--accent);
|
|
border-radius: var(--radius);
|
|
margin-bottom: 12px;
|
|
}
|
|
.auth-brand h1 { font-size: 20px; font-weight: 600; margin: 0 0 4px; }
|
|
.auth-brand p { color: var(--text-dim); font-size: 13px; margin: 0; }
|
|
.auth-tabs {
|
|
display: flex;
|
|
padding: 4px;
|
|
background: var(--bg);
|
|
border-radius: var(--radius-sm);
|
|
margin-bottom: 20px;
|
|
}
|
|
.auth-tab {
|
|
flex: 1;
|
|
padding: 8px 12px;
|
|
background: transparent;
|
|
border: none;
|
|
border-radius: 6px;
|
|
color: var(--text-dim);
|
|
font-size: 13px; font-weight: 500;
|
|
transition: all var(--t-fast);
|
|
}
|
|
.auth-tab.is-active {
|
|
background: var(--bg-elev);
|
|
color: var(--text);
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
}
|
|
.auth-form { display: flex; flex-direction: column; }
|
|
.auth-form .btn-block { margin-top: 8px; }
|
|
|
|
.auth-hint {
|
|
padding: 10px 12px;
|
|
margin-bottom: 14px;
|
|
background: var(--accent-soft);
|
|
border: 1px solid var(--accent);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--accent);
|
|
font-size: 12px;
|
|
text-align: center;
|
|
}
|
|
.auth-hint:empty { display: none; }
|
|
|
|
/* Tag chip input */
|
|
.chip-input {
|
|
display: flex; flex-wrap: wrap; align-items: center; gap: 4px;
|
|
padding: 6px 8px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
min-height: 38px;
|
|
transition: all var(--t-fast);
|
|
}
|
|
.chip-input:focus-within {
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 3px var(--accent-soft);
|
|
background: var(--bg);
|
|
}
|
|
/* Existing-tag autocomplete dropdown anchored under the chip-input. */
|
|
.tag-suggest {
|
|
position: absolute;
|
|
top: 100%; left: 0; right: 0;
|
|
margin-top: 4px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
box-shadow: var(--shadow-lg);
|
|
z-index: 60;
|
|
overflow: hidden;
|
|
max-height: 240px;
|
|
overflow-y: auto;
|
|
}
|
|
.tag-suggest.is-hidden { display: none; }
|
|
.tag-suggest-item {
|
|
display: flex; align-items: center; gap: 8px;
|
|
width: 100%;
|
|
padding: 7px 10px;
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text);
|
|
font-size: 13px;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
}
|
|
.tag-suggest-item:hover { background: var(--bg-elev-2); }
|
|
.tag-suggest-item svg { width: 12px; height: 12px; color: var(--text-faint); }
|
|
|
|
.chip-input input {
|
|
flex: 1;
|
|
min-width: 80px;
|
|
background: transparent;
|
|
border: none;
|
|
outline: none;
|
|
color: var(--text);
|
|
font-size: 13px;
|
|
padding: 2px 4px;
|
|
}
|
|
.chip {
|
|
display: inline-flex; align-items: center; gap: 4px;
|
|
padding: 2px 4px 2px 8px;
|
|
background: var(--accent-soft);
|
|
color: var(--accent);
|
|
border-radius: 10px;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
}
|
|
.chip button {
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
width: 16px; height: 16px;
|
|
background: transparent;
|
|
border: none;
|
|
border-radius: 50%;
|
|
color: inherit;
|
|
cursor: pointer;
|
|
opacity: 0.6;
|
|
}
|
|
.chip button:hover { opacity: 1; background: rgba(0,0,0,0.1); }
|
|
.chip button svg { width: 10px; height: 10px; }
|
|
|
|
|
|
/* Settings rows */
|
|
.setting-row {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 10px 12px;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border-soft);
|
|
border-radius: var(--radius-sm);
|
|
margin-bottom: 6px;
|
|
font-size: 13px;
|
|
}
|
|
.setting-row > span { display: flex; flex-direction: column; gap: 2px; }
|
|
.setting-hint {
|
|
color: var(--text-faint);
|
|
font-size: 11px;
|
|
font-weight: normal;
|
|
line-height: 1.35;
|
|
max-width: 320px;
|
|
}
|
|
.setting-row select {
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
color: var(--text);
|
|
padding: 4px 8px;
|
|
font-size: 12px;
|
|
}
|
|
.setting-row select:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
/* iOS-style toggle switch */
|
|
.toggle {
|
|
position: relative;
|
|
display: inline-block;
|
|
width: 36px; height: 20px;
|
|
flex-shrink: 0;
|
|
}
|
|
.toggle input { opacity: 0; width: 0; height: 0; }
|
|
.toggle-slider {
|
|
position: absolute; inset: 0;
|
|
background: var(--bg-elev-2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
cursor: pointer;
|
|
transition: all var(--t-fast);
|
|
}
|
|
.toggle-slider::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 2px;
|
|
top: 50%;
|
|
width: 14px; height: 14px;
|
|
background: var(--text-dim);
|
|
border-radius: 50%;
|
|
transform: translateY(-50%);
|
|
transition: all var(--t-fast);
|
|
}
|
|
.toggle input:checked + .toggle-slider {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
}
|
|
.toggle input:checked + .toggle-slider::before {
|
|
background: white;
|
|
transform: translate(16px, -50%);
|
|
}
|
|
|
|
/* ---- 6. APP SHELL ---------------------------------------- */
|
|
|
|
.app-shell {
|
|
display: grid;
|
|
grid-template-columns: 240px 1fr;
|
|
height: 100vh;
|
|
background: var(--bg);
|
|
}
|
|
|
|
/* ---- 7. SIDEBAR ------------------------------------------ */
|
|
|
|
.sidebar {
|
|
display: flex; flex-direction: column;
|
|
padding: 16px 12px;
|
|
background: var(--bg-elev);
|
|
border-right: 1px solid var(--border);
|
|
overflow-y: auto;
|
|
}
|
|
.sidebar-brand {
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 6px 8px 18px;
|
|
font-size: 15px; font-weight: 600;
|
|
}
|
|
.sidebar-brand svg { color: var(--accent); width: 18px; height: 18px; }
|
|
|
|
.sidebar-nav { display: flex; flex-direction: column; gap: 2px; }
|
|
.nav-item {
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 7px 10px;
|
|
background: transparent;
|
|
border: none;
|
|
border-radius: 6px;
|
|
color: var(--text-dim);
|
|
font-size: 13px; font-weight: 500;
|
|
text-align: left;
|
|
width: 100%;
|
|
transition: all var(--t-fast);
|
|
}
|
|
.nav-item:hover { background: var(--bg-elev-2); color: var(--text); }
|
|
.nav-item.is-active {
|
|
background: var(--accent-soft);
|
|
color: var(--accent);
|
|
}
|
|
.nav-item.is-active svg { color: var(--accent); }
|
|
.nav-item svg { color: var(--text-faint); }
|
|
.nav-item > span:first-of-type { flex: 1; }
|
|
.nav-count {
|
|
font-size: 11px;
|
|
color: var(--text-faint);
|
|
background: var(--bg-elev-2);
|
|
padding: 1px 6px;
|
|
border-radius: 8px;
|
|
min-width: 18px;
|
|
text-align: center;
|
|
}
|
|
.nav-item.is-active .nav-count { background: var(--accent); color: white; }
|
|
|
|
.sidebar-section { margin-top: 18px; }
|
|
.sidebar-section-header {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 4px 10px 6px;
|
|
font-size: 11px; font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
color: var(--text-faint);
|
|
}
|
|
.section-toggle {
|
|
flex: 1;
|
|
display: flex; align-items: center; gap: 6px;
|
|
background: none; border: none; padding: 2px 0;
|
|
font: inherit; color: inherit; text-transform: inherit;
|
|
letter-spacing: inherit;
|
|
cursor: pointer;
|
|
text-align: left;
|
|
}
|
|
.section-toggle:hover { color: var(--text-dim); }
|
|
.section-count {
|
|
min-width: 16px;
|
|
padding: 1px 6px;
|
|
border-radius: 9px;
|
|
background: var(--bg);
|
|
color: var(--text-dim);
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
text-align: center;
|
|
letter-spacing: 0;
|
|
}
|
|
/* Push everything after the count toward the right of the toggle button
|
|
so the +/etc. action button stays at the row's edge. */
|
|
.section-toggle { gap: 6px; }
|
|
.section-chevron {
|
|
width: 12px; height: 12px;
|
|
transition: transform 0.18s ease;
|
|
flex-shrink: 0;
|
|
}
|
|
.sidebar-section.is-collapsed .section-chevron {
|
|
transform: rotate(-90deg);
|
|
}
|
|
.sidebar-section.is-collapsed .section-body {
|
|
display: none;
|
|
}
|
|
.sidebar-bottom {
|
|
margin-top: auto;
|
|
padding-top: 12px;
|
|
border-top: 1px solid var(--border-soft);
|
|
display: flex; flex-direction: column; gap: 2px;
|
|
}
|
|
|
|
.folder-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
.nav-item.drag-over {
|
|
background: var(--accent-soft);
|
|
box-shadow: inset 0 0 0 2px var(--accent);
|
|
}
|
|
/* Folder reorder: insertion line above or below the hovered item. */
|
|
.nav-item.drop-before { box-shadow: inset 0 2px 0 0 var(--accent); }
|
|
.nav-item.drop-after { box-shadow: inset 0 -2px 0 0 var(--accent); }
|
|
.nav-item.is-dragging { opacity: 0.4; }
|
|
.nav-item[draggable="true"] { cursor: grab; }
|
|
.nav-item[draggable="true"]:active { cursor: grabbing; }
|
|
|
|
/* ---- 8. MAIN --------------------------------------------- */
|
|
|
|
.main { display: flex; flex-direction: column; overflow: hidden; }
|
|
.topbar {
|
|
display: flex; align-items: center; gap: 12px;
|
|
padding: 12px 24px;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--bg-glass);
|
|
backdrop-filter: var(--blur);
|
|
-webkit-backdrop-filter: var(--blur);
|
|
position: relative;
|
|
/* backdrop-filter creates a stacking context, so child z-indexes (e.g.
|
|
.user-dropdown : 50) are confined here. Without an explicit z-index
|
|
the topbar lands behind .slideover (z-index 30), clipping the
|
|
dropdown when an entry detail panel is open. 40 puts the topbar
|
|
(and its children) above the slideover; modals at 200+ still win. */
|
|
z-index: 40;
|
|
}
|
|
.search { flex: 1; max-width: 480px; position: relative; display: flex; align-items: center; }
|
|
.search svg {
|
|
position: absolute; left: 10px;
|
|
color: var(--text-faint);
|
|
pointer-events: none;
|
|
}
|
|
.search input {
|
|
width: 100%;
|
|
padding: 8px 70px 8px 34px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text);
|
|
transition: all var(--t-fast);
|
|
}
|
|
.search input:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 3px var(--accent-soft);
|
|
}
|
|
/* Replace the native (blue/grey) search-cancel button with a custom one
|
|
tinted via background-color so it picks up the accent on hover and
|
|
matches the other X buttons in the app. mask-image keeps it crisp. */
|
|
.search input::-webkit-search-cancel-button {
|
|
appearance: none;
|
|
-webkit-appearance: none;
|
|
width: 16px; height: 16px;
|
|
margin-right: 8px;
|
|
cursor: pointer;
|
|
background-color: var(--accent);
|
|
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><path d='M18 6 6 18M6 6l12 12'/></svg>");
|
|
mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><path d='M18 6 6 18M6 6l12 12'/></svg>");
|
|
-webkit-mask-size: contain;
|
|
mask-size: contain;
|
|
-webkit-mask-repeat: no-repeat;
|
|
mask-repeat: no-repeat;
|
|
opacity: 0.85;
|
|
transition: opacity var(--t-fast);
|
|
}
|
|
.search input::-webkit-search-cancel-button:hover {
|
|
opacity: 1;
|
|
}
|
|
.search kbd { position: absolute; right: 8px; }
|
|
|
|
/* Search history dropdown */
|
|
.search-history {
|
|
position: absolute;
|
|
top: calc(100% + 4px);
|
|
left: 0;
|
|
right: 0;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
box-shadow: var(--shadow-lg);
|
|
z-index: 50;
|
|
overflow: hidden;
|
|
}
|
|
.search-history.is-hidden { display: none; }
|
|
.search-history-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 6px 10px;
|
|
border-bottom: 1px solid var(--border-soft);
|
|
font-size: 11px;
|
|
color: var(--text-faint);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.4px;
|
|
}
|
|
.search-history-clear {
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text-dim);
|
|
font-size: 11px;
|
|
cursor: pointer;
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
}
|
|
.search-history-clear:hover { color: var(--danger); background: var(--danger-soft); }
|
|
.search-history-row {
|
|
display: flex;
|
|
align-items: stretch;
|
|
}
|
|
.search-history-row:hover { background: var(--bg-elev-2); }
|
|
.search-history-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex: 1;
|
|
min-width: 0;
|
|
padding: 8px 12px;
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text);
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
text-align: left;
|
|
}
|
|
.search-history-item svg { width: 13px; height: 13px; color: var(--text-faint); }
|
|
.search-history-del {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 32px;
|
|
flex-shrink: 0;
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text-dim);
|
|
cursor: pointer;
|
|
transition: color var(--t-fast), background var(--t-fast);
|
|
}
|
|
.search-history-del {
|
|
font-size: 18px;
|
|
line-height: 1;
|
|
font-weight: 400;
|
|
}
|
|
.search-history-row:hover .search-history-del { color: var(--text); }
|
|
.search-history-del:hover { color: var(--danger); background: var(--danger-soft); }
|
|
.topbar-actions { display: flex; align-items: center; gap: 8px; margin-left: auto; }
|
|
|
|
/* View toggle (Cards / List) */
|
|
.view-toggle {
|
|
display: inline-flex;
|
|
padding: 2px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
.view-btn {
|
|
width: 28px; height: 28px;
|
|
display: grid; place-items: center;
|
|
background: transparent;
|
|
border: none;
|
|
border-radius: 6px;
|
|
color: var(--text-dim);
|
|
transition: all var(--t-fast);
|
|
}
|
|
.view-btn:hover { color: var(--text); }
|
|
.view-btn.is-active {
|
|
background: var(--accent-soft);
|
|
color: var(--accent);
|
|
}
|
|
|
|
[data-theme="dark"] .theme-icon-light { display: none; }
|
|
[data-theme="light"] .theme-icon-dark { display: none; }
|
|
|
|
.user-menu { position: relative; }
|
|
.user-chip {
|
|
display: inline-flex; align-items: center; gap: 8px;
|
|
padding: 5px 12px 5px 6px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: 20px;
|
|
color: var(--text);
|
|
font-size: 12px;
|
|
transition: all var(--t-fast);
|
|
}
|
|
.user-chip:hover { background: var(--bg-elev-2); }
|
|
/* Round initials avatar. Background colour is set inline from a hash of
|
|
the username (deterministic — same user, same colour every render).
|
|
Falls back to a background-image when a custom picture is set. */
|
|
.user-avatar {
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
width: 30px; height: 30px;
|
|
margin-left: 0;
|
|
border-radius: 50%;
|
|
font-size: 13px; font-weight: 600;
|
|
color: #fff;
|
|
text-transform: uppercase;
|
|
background-size: cover; background-position: center;
|
|
flex-shrink: 0;
|
|
user-select: none;
|
|
}
|
|
/* Larger preview variant shown in Settings → Account. */
|
|
.user-avatar-lg {
|
|
width: 48px; height: 48px;
|
|
margin-left: 0;
|
|
font-size: 20px;
|
|
}
|
|
.user-dropdown {
|
|
position: absolute; top: calc(100% + 6px); right: 0;
|
|
min-width: 180px;
|
|
padding: 4px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
box-shadow: var(--shadow);
|
|
z-index: 50;
|
|
}
|
|
.dropdown-item {
|
|
display: flex; align-items: center; gap: 8px;
|
|
width: 100%;
|
|
padding: 8px 10px;
|
|
background: transparent;
|
|
border: none;
|
|
border-radius: 6px;
|
|
color: var(--text);
|
|
font-size: 13px;
|
|
text-align: left;
|
|
}
|
|
.dropdown-item:hover { background: var(--bg-elev-2); }
|
|
.dropdown-item svg { color: var(--text-dim); }
|
|
|
|
/* No padding-top: the sticky .content-header owns it, so its box starts flush
|
|
with the scrollport and nothing can scroll through above it. */
|
|
.content { flex: 1; padding: 0 32px 24px; overflow-y: auto; }
|
|
/* Sticky title row — carries .content's top padding so its opaque box reaches
|
|
the scrollport edge. Everything else that sticks (batch bar, pagination,
|
|
table header) offsets by --hdr-h. */
|
|
.content-header {
|
|
display: flex; align-items: baseline; gap: 12px;
|
|
/* padding, NOT margin: offsetHeight (which feeds --hdr-h, where the sticky
|
|
pagination parks) ignores margins, so the 20px gap vanished as soon as
|
|
the header stuck. As padding it is measured AND painted — the gap stays
|
|
constant and stays opaque instead of letting rows scroll through it. */
|
|
padding-bottom: 20px;
|
|
/* FIXED, not min-height: 24 top padding + 38 bar + 20 bottom padding. A
|
|
minimum still lets a child grow the row — a couple of stray pixels from
|
|
the selection bar were enough to push the whole grid down in table and
|
|
list view. With a fixed height nothing inside can move what follows. */
|
|
height: 82px;
|
|
position: sticky; top: 0; z-index: 12;
|
|
background: var(--bg);
|
|
padding-top: 24px;
|
|
}
|
|
.content-header h2 { font-size: 22px; font-weight: 600; margin: 0; }
|
|
.content-meta { color: var(--text-faint); font-size: 12px; }
|
|
|
|
/* ---- 9. ENTRY GRID --------------------------------------- */
|
|
|
|
.entry-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 12px;
|
|
position: relative;
|
|
user-select: none; /* avoid native text-selection during marquee */
|
|
}
|
|
|
|
/* Marquee rectangle for rubber-band selection */
|
|
|
|
.entry-card {
|
|
position: relative;
|
|
padding: 14px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
transition: all var(--t-fast);
|
|
cursor: pointer;
|
|
}
|
|
.entry-card:hover {
|
|
transform: translateY(-1px);
|
|
border-color: var(--accent);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
.entry-card.is-selected {
|
|
border-color: var(--accent);
|
|
background: var(--accent-soft);
|
|
}
|
|
.entry-card.is-checked {
|
|
border-color: var(--accent);
|
|
background: var(--accent-soft);
|
|
box-shadow: inset 0 0 0 2px var(--accent);
|
|
}
|
|
/* Keyboard cursor (j/k nav) — distinct from selected so a focused card
|
|
stays visible after the slideover closes. */
|
|
.entry-card.is-cursor,
|
|
.entry-row.is-cursor {
|
|
outline: 2px solid var(--accent);
|
|
outline-offset: -2px;
|
|
}
|
|
|
|
/* === List view layout === */
|
|
/* Cards are flattened into a single horizontal row using flexbox + `order`.
|
|
`.entry-head` and `.entry-pw-row` get display:contents so their children
|
|
participate directly in the card flex container — no wrapping, ever. */
|
|
.entry-grid.is-list {
|
|
grid-template-columns: 1fr;
|
|
gap: 4px;
|
|
}
|
|
.entry-grid.is-list .entry-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 8px 12px;
|
|
}
|
|
.entry-grid.is-list .entry-card:hover { transform: none; }
|
|
|
|
.entry-grid.is-list .entry-head { display: contents; }
|
|
.entry-grid.is-list .entry-pw-row { display: contents; }
|
|
.entry-grid.is-list .entry-note-row { display: contents; }
|
|
.entry-grid.is-list .entry-note-placeholder {
|
|
order: 3;
|
|
/* Same track as .entry-pw so note rows line their copy button up with
|
|
the password rows'. Full text on hover via the title attr. */
|
|
flex: 0 0 110px;
|
|
min-width: 0;
|
|
font-size: 11px;
|
|
color: var(--text-dim);
|
|
font-style: italic;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
padding: 4px 8px;
|
|
background: var(--bg);
|
|
border-radius: 6px;
|
|
border: 1px solid var(--border-soft);
|
|
}
|
|
.entry-grid.is-list .entry-note-row .icon-btn {
|
|
order: 4;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.entry-grid.is-list .entry-avatar {
|
|
order: 1;
|
|
width: 28px; height: 28px;
|
|
font-size: 12px;
|
|
margin: 0;
|
|
}
|
|
.entry-grid.is-list .entry-title {
|
|
order: 2;
|
|
/* Fixed track (not flex:1) so the password field that follows starts at
|
|
the SAME x on every row — otherwise variable trailing content
|
|
(tags/folder/icons) let the title grow by different amounts and the
|
|
pw pill drifted left/right per row. Short names leave whitespace;
|
|
long names ellipsise. */
|
|
flex: 0 0 240px;
|
|
min-width: 0;
|
|
display: flex; flex-direction: column;
|
|
}
|
|
.entry-grid.is-list .entry-title b { font-size: 13px; }
|
|
.entry-grid.is-list .entry-title small { font-size: 11px; }
|
|
|
|
.entry-grid.is-list .entry-pw {
|
|
order: 3;
|
|
font-size: 11px;
|
|
/* Full shorthand, not width: the base .entry-pw is `flex: 1` (basis 0),
|
|
which ignores width and let the pill stretch across the whole row. */
|
|
flex: 0 0 110px;
|
|
padding: 4px 8px;
|
|
background: var(--bg);
|
|
border-radius: 6px;
|
|
border: 1px solid var(--border-soft);
|
|
}
|
|
.entry-grid.is-list .entry-pw-row .icon-btn {
|
|
order: 4;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.entry-grid.is-list .entry-meta {
|
|
order: 5;
|
|
/* Absorb the free space freed up by the now-fixed title, pushing tags/
|
|
folder + the order-6 actions to the far right while pw stays put.
|
|
Sole auto margin in the row — see the note below the order-6 block. */
|
|
margin: 0 0 0 auto;
|
|
flex-shrink: 0;
|
|
max-width: 220px;
|
|
overflow: hidden;
|
|
flex-wrap: nowrap;
|
|
}
|
|
.entry-grid.is-list .entry-meta .entry-chip {
|
|
flex-shrink: 0;
|
|
font-size: 10px;
|
|
}
|
|
|
|
/* Right-side action buttons: favorite + delete (normal view),
|
|
restore + purge (trash view), or kebab (compact mode). Same order slot. */
|
|
.entry-grid.is-list .entry-fav,
|
|
.entry-grid.is-list .entry-del,
|
|
.entry-grid.is-list .entry-dup,
|
|
.entry-grid.is-list .entry-kebab-wrap,
|
|
.entry-grid.is-list .entry-head .icon-btn {
|
|
order: 6;
|
|
flex-shrink: 0;
|
|
}
|
|
/* NOTE: no margin-left:auto here. .entry-meta is always in the DOM (empty
|
|
when the entry has no chips) and already carries the auto, so it absorbs
|
|
all the free space. A second auto margin would SPLIT the free space
|
|
between the two, drifting the chips left/right depending on row width. */
|
|
/* Selection checkbox overlay — nested INSIDE the avatar (.entry-avatar
|
|
is position:relative) so it covers the avatar 1:1, no overlap with the
|
|
avatar's footprint. Hidden by default; appears on card hover OR when
|
|
the entry is already checked. The avatar's initials stay visible
|
|
underneath through the checkbox's transparent-then-solid transition. */
|
|
.entry-check {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: grid; place-items: center;
|
|
background: var(--bg);
|
|
border: 1.5px solid var(--accent);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text);
|
|
font-size: 14px;
|
|
line-height: 1;
|
|
cursor: pointer;
|
|
opacity: 0;
|
|
transition: opacity var(--t-fast),
|
|
background var(--t-fast), border-color var(--t-fast);
|
|
z-index: 2;
|
|
}
|
|
.entry-card:hover .entry-check,
|
|
.entry-check.is-checked {
|
|
opacity: 1;
|
|
}
|
|
.entry-check.is-checked {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
color: white;
|
|
}
|
|
/* Standalone variant used in the table view's check column. The default
|
|
rule above is absolute+inset:0 so the box sizes itself from its avatar
|
|
parent (card view). When the checkbox lives on its own in a <td>, no
|
|
parent provides a size and the box collapses to a thin line. This
|
|
modifier restores explicit dimensions and always-on visibility. */
|
|
.entry-check.entry-check-static {
|
|
position: static;
|
|
width: 18px; height: 18px;
|
|
border-radius: 4px;
|
|
border-width: 1.5px;
|
|
opacity: 1;
|
|
font-size: 11px;
|
|
}
|
|
|
|
/* Batch action bar (appears when selection is non-empty). It lives INSIDE
|
|
.content-header, to the right of the title: as a block of its own it pushed
|
|
the whole list down on every selection. The header reserves its height (see
|
|
.content-header min-height) so appearing/disappearing shifts nothing. */
|
|
.batch-bar {
|
|
flex: 1;
|
|
align-self: center;
|
|
/* EXACTLY the height .content-header reserves. Left to grow, the bar was a
|
|
few pixels taller than the reservation and pushed the whole grid down as
|
|
soon as anything was selected — the shift the sticky header was meant to
|
|
prevent in the first place. */
|
|
height: 38px;
|
|
display: flex; align-items: center; gap: 8px;
|
|
padding: 0 10px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--accent);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow);
|
|
animation: toastIn var(--t-base);
|
|
}
|
|
.batch-bar-count {
|
|
font-weight: 600;
|
|
color: var(--accent);
|
|
margin-right: 8px;
|
|
}
|
|
.batch-bar select {
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
color: var(--text);
|
|
padding: 4px 8px;
|
|
font-size: 12px;
|
|
}
|
|
.batch-bar .grow { flex: 1; }
|
|
|
|
.entry-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
|
.entry-avatar {
|
|
position: relative; /* anchor for .entry-check overlay */
|
|
width: 32px; height: 32px;
|
|
display: grid; place-items: center;
|
|
background: var(--bg-elev-2);
|
|
border-radius: var(--radius-sm);
|
|
font-weight: 600; font-size: 14px;
|
|
color: var(--text);
|
|
flex-shrink: 0;
|
|
overflow: hidden;
|
|
}
|
|
.entry-avatar-img {
|
|
width: 100%; height: 100%;
|
|
object-fit: contain;
|
|
padding: 4px;
|
|
box-sizing: border-box;
|
|
image-rendering: -webkit-optimize-contrast; /* sharper 16x16 icons */
|
|
}
|
|
.entry-title { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
|
.entry-title b {
|
|
font-size: 14px; font-weight: 600;
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
}
|
|
.entry-title small {
|
|
color: var(--text-dim);
|
|
font-size: 12px;
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
}
|
|
.entry-fav, .entry-del, .entry-dup, .entry-pin {
|
|
color: var(--text-faint);
|
|
background: transparent; border: none;
|
|
padding: 4px;
|
|
transition: all var(--t-fast);
|
|
cursor: pointer;
|
|
}
|
|
.entry-fav.is-on { color: var(--warning); }
|
|
.entry-fav:hover { color: var(--warning); transform: scale(1.1); }
|
|
.entry-pin.is-on { color: var(--accent); }
|
|
.entry-pin:hover { color: var(--accent); transform: scale(1.1); }
|
|
.entry-dup {
|
|
color: var(--text-dim);
|
|
opacity: 0;
|
|
}
|
|
.entry-card:hover .entry-dup { opacity: 0.8; }
|
|
.entry-dup:hover {
|
|
opacity: 1;
|
|
color: var(--accent);
|
|
background: var(--accent-soft);
|
|
border-radius: 6px;
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
/* Delete button — always visible, brightens on hover */
|
|
.entry-del {
|
|
color: var(--text-dim);
|
|
opacity: 0.8;
|
|
}
|
|
.entry-del:hover {
|
|
opacity: 1;
|
|
color: var(--danger);
|
|
background: var(--danger-soft);
|
|
border-radius: 6px;
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
/* Username row with inline copy button (visible on card hover) */
|
|
.entry-user-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
.entry-user-row > span {
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
}
|
|
.entry-copy-user {
|
|
background: transparent; border: none;
|
|
padding: 2px;
|
|
color: var(--text-faint);
|
|
opacity: 0;
|
|
cursor: pointer;
|
|
transition: all var(--t-fast);
|
|
border-radius: 4px;
|
|
}
|
|
.entry-copy-user svg { width: 11px; height: 11px; }
|
|
.entry-card:hover .entry-copy-user { opacity: 0.7; }
|
|
.entry-copy-user:hover {
|
|
opacity: 1 !important;
|
|
color: var(--accent);
|
|
background: var(--accent-soft);
|
|
}
|
|
|
|
/* Kebab (⋯) menu */
|
|
.entry-kebab-wrap {
|
|
position: relative;
|
|
}
|
|
.entry-kebab {
|
|
background: transparent; border: none;
|
|
padding: 4px;
|
|
color: var(--text-dim);
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: all var(--t-fast);
|
|
}
|
|
.entry-kebab:hover { color: var(--text); background: var(--bg-elev-2); }
|
|
|
|
.entry-kebab-menu {
|
|
position: absolute;
|
|
top: calc(100% + 4px);
|
|
right: 0;
|
|
min-width: 180px;
|
|
padding: 4px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
box-shadow: var(--shadow);
|
|
z-index: 60;
|
|
display: none;
|
|
animation: modalIn var(--t-fast);
|
|
}
|
|
.entry-kebab-menu.is-open { display: block; }
|
|
/* Lift the card containing an open kebab menu above its siblings so the
|
|
dropdown isn't clipped by the next card in DOM order. */
|
|
.entry-card:has(.entry-kebab-menu.is-open) { z-index: 100; }
|
|
.entry-row:has(.entry-kebab-menu.is-open) { position: relative; z-index: 100; }
|
|
|
|
.kebab-item {
|
|
display: flex; align-items: center; gap: 10px;
|
|
width: 100%;
|
|
padding: 8px 10px;
|
|
background: transparent;
|
|
border: none;
|
|
border-radius: 6px;
|
|
color: var(--text);
|
|
font-size: 13px;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
transition: all var(--t-fast);
|
|
}
|
|
.kebab-item:hover { background: var(--bg-elev-2); }
|
|
.kebab-item svg { color: var(--text-dim); }
|
|
.kebab-item.is-danger { color: var(--danger); }
|
|
.kebab-item.is-danger:hover { background: var(--danger-soft); }
|
|
.kebab-item.is-danger svg { color: var(--danger); }
|
|
|
|
.entry-pw-row {
|
|
display: flex; align-items: center; gap: 6px;
|
|
padding: 6px 8px;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border-soft);
|
|
border-radius: 6px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.entry-pw {
|
|
flex: 1;
|
|
font-family: 'JetBrains Mono', 'SF Mono', Consolas, monospace;
|
|
font-size: 12px;
|
|
color: var(--text-dim);
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
user-select: none;
|
|
}
|
|
.entry-pw.is-revealed { color: var(--text); }
|
|
.entry-pw.is-error { color: var(--danger); }
|
|
|
|
.entry-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
.entry-chip {
|
|
display: inline-flex; align-items: center; gap: 4px;
|
|
padding: 2px 8px;
|
|
background: var(--bg-elev-2);
|
|
border-radius: 10px;
|
|
font-size: 11px;
|
|
color: var(--text-dim);
|
|
}
|
|
.entry-chip svg { width: 11px; height: 11px; }
|
|
.entry-chip.is-folder { color: var(--accent); background: var(--accent-soft); }
|
|
.entry-chip.is-pwned {
|
|
color: #fff;
|
|
background: #dc2626; /* solid red — high-attention signal */
|
|
font-weight: 600;
|
|
}
|
|
.entry-chip.is-pwned svg { stroke: #fff; }
|
|
.entry-chip.is-2fa {
|
|
color: var(--accent);
|
|
background: var(--accent-soft);
|
|
font-weight: 600;
|
|
}
|
|
.entry-chip.is-aged {
|
|
color: #b45309;
|
|
background: rgba(245, 158, 11, 0.15);
|
|
font-weight: 600;
|
|
}
|
|
.entry-chip.is-aged svg { stroke: #b45309; }
|
|
|
|
/* TOTP live-code panel (inside slide-over Two-factor field) */
|
|
.totp-panel {
|
|
display: flex; align-items: center; gap: 12px;
|
|
margin-top: 8px;
|
|
padding: 10px 12px;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border-soft);
|
|
border-radius: var(--radius-sm);
|
|
min-height: 36px;
|
|
}
|
|
.totp-code {
|
|
flex: 1;
|
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
font-size: 22px;
|
|
letter-spacing: 2px;
|
|
color: var(--accent);
|
|
font-weight: 600;
|
|
user-select: all; /* triple-click selects the code */
|
|
}
|
|
.totp-code.is-invalid {
|
|
color: var(--text-faint);
|
|
font-size: 13px;
|
|
letter-spacing: normal;
|
|
font-style: italic;
|
|
font-weight: normal;
|
|
}
|
|
.totp-bar-wrap {
|
|
margin-top: 4px;
|
|
height: 3px;
|
|
background: var(--bg);
|
|
border-radius: 2px;
|
|
overflow: hidden;
|
|
}
|
|
.totp-bar {
|
|
height: 100%;
|
|
background: var(--accent);
|
|
width: 0%;
|
|
transition: width 0.8s linear, background 0.2s;
|
|
}
|
|
|
|
/* ---- Authenticator view (TOTP grid) ---------------------- */
|
|
.entry-grid.is-auth {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
gap: 12px;
|
|
}
|
|
.auth-card {
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
padding: 14px 16px;
|
|
cursor: pointer;
|
|
transition: border-color 0.15s, transform 0.05s;
|
|
}
|
|
.auth-card:hover { border-color: var(--accent); }
|
|
.auth-card:active { transform: scale(0.99); }
|
|
.auth-card-head { margin-bottom: 8px; }
|
|
.auth-card-title {
|
|
font-weight: 600;
|
|
color: var(--text);
|
|
font-size: 14px;
|
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
}
|
|
.auth-card-sub {
|
|
font-size: 12px;
|
|
color: var(--text-dim);
|
|
margin-top: 2px;
|
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
}
|
|
.auth-card-code-row {
|
|
display: flex; align-items: center; gap: 8px;
|
|
}
|
|
.auth-card-code {
|
|
flex: 1;
|
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
font-size: 26px;
|
|
letter-spacing: 3px;
|
|
color: var(--accent);
|
|
font-weight: 600;
|
|
user-select: all;
|
|
}
|
|
.auth-card-bar-wrap {
|
|
margin-top: 8px;
|
|
height: 3px;
|
|
background: var(--bg);
|
|
border-radius: 2px;
|
|
overflow: hidden;
|
|
}
|
|
.auth-card-bar {
|
|
height: 100%;
|
|
background: var(--accent);
|
|
width: 100%;
|
|
transition: width 0.8s linear, background 0.2s;
|
|
}
|
|
.auth-card-bar.is-warning { background: #f59e0b; }
|
|
|
|
/* ---- Vault health dashboard ----------------------------- */
|
|
.entry-grid.is-health {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
.health-loading {
|
|
color: var(--text-dim);
|
|
text-align: center;
|
|
padding: 40px;
|
|
font-size: 14px;
|
|
}
|
|
.health-header {
|
|
display: flex; align-items: center; gap: 20px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
padding: 20px;
|
|
}
|
|
.health-score {
|
|
width: 110px; height: 110px;
|
|
border-radius: 50%;
|
|
display: flex; flex-direction: column;
|
|
align-items: center; justify-content: center;
|
|
flex-shrink: 0;
|
|
border: 4px solid var(--accent);
|
|
background: var(--bg);
|
|
}
|
|
.health-score.is-ok { border-color: #10b981; color: #10b981; }
|
|
.health-score.is-fair { border-color: var(--accent); color: var(--accent); }
|
|
.health-score.is-warn { border-color: #f59e0b; color: #f59e0b; }
|
|
.health-score.is-danger { border-color: #ef4444; color: #ef4444; }
|
|
.health-score-num {
|
|
font-size: 36px; font-weight: 700;
|
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
}
|
|
.health-score-lbl {
|
|
font-size: 11px; font-weight: 600;
|
|
text-transform: uppercase; letter-spacing: 0.5px;
|
|
margin-top: 2px;
|
|
}
|
|
/* Score dial + its per-category breakdown, stacked. */
|
|
.health-score-col {
|
|
display: flex; flex-direction: column;
|
|
align-items: center; gap: 10px;
|
|
flex-shrink: 0;
|
|
}
|
|
.health-score-breakdown {
|
|
display: flex; flex-direction: column;
|
|
gap: 2px;
|
|
font-size: 11px;
|
|
color: var(--text-dim);
|
|
white-space: nowrap;
|
|
}
|
|
/* Each row keeps its own box: a display:contents row would have none, and the
|
|
title tooltip explaining the cap would never appear. */
|
|
.health-bd-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr auto auto;
|
|
gap: 0 10px;
|
|
cursor: help;
|
|
}
|
|
.health-bd-n { text-align: right; font-variant-numeric: tabular-nums; }
|
|
.health-bd-pts {
|
|
text-align: right;
|
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
color: var(--text-faint);
|
|
}
|
|
/* Nothing wrong in this category — dim it so the eye goes to what needs work. */
|
|
.health-bd-row.is-clear .health-bd-lbl,
|
|
.health-bd-row.is-clear .health-bd-n,
|
|
.health-bd-row.is-clear .health-bd-pts { opacity: 0.45; }
|
|
|
|
.health-intro { flex: 1; }
|
|
.health-intro h3 { margin: 0 0 6px; font-size: 16px; }
|
|
.health-intro p {
|
|
margin: 0 0 10px;
|
|
color: var(--text-dim);
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
}
|
|
.health-card {
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
padding: 16px 18px;
|
|
}
|
|
.health-card-head {
|
|
display: flex; align-items: center; gap: 10px;
|
|
margin-bottom: 6px;
|
|
}
|
|
.health-card-head h4 {
|
|
margin: 0;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
}
|
|
.health-badge {
|
|
min-width: 22px;
|
|
padding: 2px 8px;
|
|
border-radius: 11px;
|
|
background: var(--accent);
|
|
color: white;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
text-align: center;
|
|
}
|
|
.health-badge.is-empty {
|
|
background: var(--bg);
|
|
color: var(--text-dim);
|
|
}
|
|
.health-hint {
|
|
margin: 0 0 10px;
|
|
font-size: 12px;
|
|
color: var(--text-dim);
|
|
line-height: 1.4;
|
|
}
|
|
.health-empty {
|
|
margin: 0;
|
|
font-size: 12px;
|
|
color: var(--text-faint);
|
|
font-style: italic;
|
|
}
|
|
.health-list {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: flex; flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
.health-item {
|
|
display: flex; align-items: center; gap: 8px;
|
|
padding: 6px 10px;
|
|
background: var(--bg);
|
|
border-radius: var(--radius-sm);
|
|
font-size: 13px;
|
|
}
|
|
.health-item-label {
|
|
flex: 1;
|
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
color: var(--text);
|
|
}
|
|
.health-more {
|
|
margin: 6px 0 0;
|
|
color: var(--text-faint);
|
|
font-size: 11px;
|
|
font-style: italic;
|
|
text-align: center;
|
|
}
|
|
.health-more-btn {
|
|
display: block;
|
|
margin: 8px auto 0;
|
|
}
|
|
|
|
/* ---- Slideover custom icon field ------------------------ */
|
|
.so-icon-field {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
.so-icon-preview {
|
|
width: 56px; height: 56px;
|
|
display: grid; place-items: center;
|
|
background: var(--bg-elev-2);
|
|
border-radius: var(--radius-sm);
|
|
overflow: hidden;
|
|
font-weight: 600;
|
|
font-size: 20px;
|
|
color: var(--text);
|
|
flex-shrink: 0;
|
|
}
|
|
.so-icon-preview img {
|
|
width: 100%; height: 100%;
|
|
object-fit: contain;
|
|
padding: 6px;
|
|
box-sizing: border-box;
|
|
}
|
|
.so-icon-actions {
|
|
display: flex; flex-direction: column; gap: 6px;
|
|
}
|
|
|
|
/* ---- Password history modal --------------------- */
|
|
.so-history-wrap {
|
|
margin-top: -8px;
|
|
margin-bottom: 4px;
|
|
}
|
|
.so-history-wrap .btn { padding: 4px 8px; font-size: 11px; }
|
|
|
|
/* Make the history body actually scrollable. The .modal-body's default
|
|
doesn't fix the height, so the panel grew to fit all rows → no scroll. */
|
|
#historyBody {
|
|
max-height: 65vh;
|
|
overflow-y: auto;
|
|
padding: 12px 16px;
|
|
}
|
|
.history-loading, .history-empty {
|
|
padding: 24px;
|
|
text-align: center;
|
|
color: var(--text-dim);
|
|
font-size: 13px;
|
|
}
|
|
.history-list {
|
|
list-style: none;
|
|
margin: 0; padding: 0;
|
|
display: flex; flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
.history-row {
|
|
background: var(--bg);
|
|
border: 1px solid var(--border-soft);
|
|
border-radius: var(--radius-sm);
|
|
padding: 10px 12px;
|
|
display: flex; flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
.history-meta {
|
|
display: flex; align-items: center; gap: 8px;
|
|
}
|
|
.history-date {
|
|
font-size: 11px;
|
|
color: var(--text-dim);
|
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
}
|
|
.history-preview {
|
|
display: flex; align-items: flex-start; gap: 6px;
|
|
min-width: 0;
|
|
}
|
|
.history-value {
|
|
flex: 1 1 0;
|
|
min-width: 0;
|
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
font-size: 12px;
|
|
color: var(--text);
|
|
/* Long revealed passwords wrap to multiple lines (no ellipsis) so
|
|
the user sees the full value. break-all is needed because real
|
|
passwords have no whitespace for the browser to break on. */
|
|
white-space: normal;
|
|
word-break: break-all;
|
|
overflow-wrap: anywhere;
|
|
line-height: 1.4;
|
|
}
|
|
.history-row { min-width: 0; }
|
|
.history-actions {
|
|
display: flex; justify-content: flex-end;
|
|
}
|
|
|
|
/* ---- Cheatsheet overlay --------------------------- */
|
|
.cheatsheet-panel { max-width: 720px; width: 100%; }
|
|
.cheatsheet-body {
|
|
max-height: 60vh;
|
|
overflow-y: auto;
|
|
padding: 18px 22px;
|
|
}
|
|
.cheatsheet-group { margin-bottom: 18px; }
|
|
.cheatsheet-group h4 {
|
|
margin: 0 0 8px;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
color: var(--text-dim);
|
|
}
|
|
.cheatsheet-list {
|
|
display: flex; flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
.cheatsheet-row {
|
|
display: grid;
|
|
grid-template-columns: 180px 1fr;
|
|
gap: 16px;
|
|
align-items: start;
|
|
padding: 4px 0;
|
|
font-size: 13px;
|
|
}
|
|
.cheatsheet-desc {
|
|
color: var(--text-dim);
|
|
line-height: 1.4;
|
|
}
|
|
.cheatsheet-keys {
|
|
display: flex; align-items: center; gap: 4px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.cheatsheet-keys kbd {
|
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
font-size: 11px;
|
|
padding: 2px 7px;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
box-shadow: 0 1px 0 var(--border-soft);
|
|
}
|
|
.cheatsheet-plus {
|
|
color: var(--text-faint);
|
|
font-size: 10px;
|
|
}
|
|
.cheatsheet-icon-chip {
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
width: 24px; height: 22px;
|
|
padding: 2px;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
box-shadow: 0 1px 0 var(--border-soft);
|
|
color: var(--text);
|
|
}
|
|
.cheatsheet-icon-chip svg { width: 14px; height: 14px; }
|
|
|
|
/* ---- Slideover custom-fields editor ------------------ */
|
|
.so-custom-list {
|
|
display: flex; flex-direction: column;
|
|
gap: 6px;
|
|
margin-top: 6px;
|
|
}
|
|
.so-custom-row {
|
|
/* label | value (eye inside) | row menu */
|
|
display: grid;
|
|
grid-template-columns: 1fr 1.4fr auto;
|
|
gap: 4px;
|
|
align-items: center;
|
|
}
|
|
/* The reveal eye sits inside the field, like the TOTP one. */
|
|
.so-custom-value-wrap { position: relative; display: flex; min-width: 0; }
|
|
.so-custom-value-wrap > .so-input,
|
|
.so-custom-value-wrap > .so-combo { width: 100%; }
|
|
.so-custom-value-wrap > .so-input { padding-right: 28px; }
|
|
.so-custom-value-eye {
|
|
position: absolute;
|
|
right: 3px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
}
|
|
/* Multiline: pin it to the top corner, clear of the resize grip — and inside
|
|
the scrollbar, which appears as soon as the content passes the auto-grow cap.
|
|
scrollbar-gutter keeps that space reserved so the eye doesn't shift when the
|
|
scrollbar shows up. */
|
|
.so-custom-value-wrap.is-multiline .so-custom-value-eye {
|
|
top: 3px;
|
|
right: 16px;
|
|
transform: none;
|
|
}
|
|
.so-custom-value-wrap.is-multiline > .so-input {
|
|
padding-right: 44px;
|
|
scrollbar-gutter: stable;
|
|
}
|
|
/* The row menu is absolutely positioned inside its wrap; without this the
|
|
slideover clips it or it lands under the next row. */
|
|
.so-custom-row .entry-kebab-wrap { position: relative; }
|
|
.so-custom-row .entry-kebab-menu { right: 0; }
|
|
.so-custom-row .so-input {
|
|
padding: 4px 8px;
|
|
font-size: 12px;
|
|
}
|
|
/* Multiline custom field (Notes). The row stays a grid, so the textarea just
|
|
grows in its own column and the buttons keep their places. */
|
|
.so-custom-multiline {
|
|
min-height: 56px;
|
|
resize: vertical;
|
|
align-self: stretch;
|
|
font-family: inherit;
|
|
line-height: 1.5;
|
|
overflow-y: auto; /* JS caps the auto-grow; past that it scrolls */
|
|
}
|
|
/* Editable combobox for option-backed custom fields (card brand, expiry
|
|
year…). Input fills the cell; the arrow drops a menu of ALL options. */
|
|
.so-combo { position: relative; display: flex; align-items: center; }
|
|
.so-combo-input { flex: 1; padding-right: 24px !important; width: 100%; }
|
|
.so-combo-arrow {
|
|
position: absolute; right: 2px; top: 50%;
|
|
transform: translateY(-50%);
|
|
display: flex; align-items: center; justify-content: center;
|
|
width: 20px; height: 20px;
|
|
background: transparent; border: 0; padding: 0;
|
|
color: var(--text-faint); cursor: pointer;
|
|
}
|
|
.so-combo-arrow svg { width: 14px; height: 14px; }
|
|
.so-combo-arrow:hover { color: var(--text); }
|
|
.so-combo-menu {
|
|
position: absolute; top: calc(100% + 2px); left: 0; right: 0;
|
|
z-index: 20;
|
|
max-height: 200px; overflow-y: auto;
|
|
background: var(--bg-elev-2);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
box-shadow: var(--shadow);
|
|
padding: 4px;
|
|
}
|
|
.so-combo-menu.is-hidden { display: none; }
|
|
.so-combo-item {
|
|
padding: 6px 10px;
|
|
font-size: 12px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
}
|
|
.so-combo-item:hover { background: var(--accent); color: #fff; }
|
|
/* The eye carries the secret state itself — a second toggle beside it read as
|
|
a duplicate of the reveal. Flipping the flag lives in the row menu. */
|
|
/* No marker for "stored as secret": the eye only exists on a secret field —
|
|
Make public removes it — so its mere presence already says so. */
|
|
.so-custom-add {
|
|
margin-top: 8px;
|
|
}
|
|
.so-custom-add .btn { padding: 4px 10px; font-size: 12px; }
|
|
|
|
/* ---- Audit log viewer -------------------------------- */
|
|
.entry-grid.is-audit {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
.audit-loading, .audit-empty {
|
|
color: var(--text-dim);
|
|
padding: 24px;
|
|
text-align: center;
|
|
font-size: 13px;
|
|
}
|
|
.audit-header {
|
|
display: flex; gap: 8px;
|
|
align-items: center;
|
|
}
|
|
.audit-search {
|
|
flex: 1;
|
|
padding: 6px 12px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text);
|
|
font-size: 13px;
|
|
}
|
|
.audit-search:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 3px var(--accent-soft);
|
|
}
|
|
.audit-note {
|
|
margin: 0;
|
|
font-size: 11px;
|
|
color: var(--text-faint);
|
|
font-style: italic;
|
|
}
|
|
.audit-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
background: var(--bg-elev);
|
|
max-height: 70vh;
|
|
overflow-y: auto;
|
|
}
|
|
.audit-row {
|
|
display: grid;
|
|
grid-template-columns: 160px 1fr 120px;
|
|
gap: 12px;
|
|
padding: 8px 14px;
|
|
font-size: 12px;
|
|
border-bottom: 1px solid var(--border-soft);
|
|
align-items: center;
|
|
}
|
|
.audit-row:last-child { border-bottom: none; }
|
|
.audit-row:hover { background: var(--bg); }
|
|
.audit-date {
|
|
color: var(--text-dim);
|
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
white-space: nowrap;
|
|
}
|
|
.audit-action {
|
|
color: var(--text);
|
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
}
|
|
.audit-ip {
|
|
color: var(--text-faint);
|
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
text-align: right;
|
|
}
|
|
|
|
/* ---- Filters dropdown + chips ------------------------ */
|
|
.filters-wrap { position: relative; }
|
|
.filters-count {
|
|
position: absolute;
|
|
top: -2px; right: -4px;
|
|
background: var(--accent);
|
|
color: white;
|
|
font-size: 9px;
|
|
font-weight: 700;
|
|
padding: 1px 5px;
|
|
border-radius: 8px;
|
|
line-height: 1.3;
|
|
pointer-events: none;
|
|
}
|
|
.filters-menu {
|
|
position: absolute;
|
|
top: calc(100% + 4px); right: 0;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
box-shadow: var(--shadow-lg);
|
|
z-index: 60;
|
|
min-width: 240px;
|
|
padding: 6px;
|
|
}
|
|
.filters-menu-title {
|
|
padding: 4px 8px 6px;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
color: var(--text-faint);
|
|
}
|
|
.filters-menu-row {
|
|
display: flex; align-items: center; gap: 8px;
|
|
padding: 6px 8px;
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
font-size: 13px;
|
|
color: var(--text);
|
|
}
|
|
.filters-menu-row:hover { background: var(--accent-soft); }
|
|
.filters-menu-row svg { width: 14px; height: 14px; color: var(--text-dim); }
|
|
.filters-menu-cb {
|
|
margin: 0;
|
|
accent-color: var(--accent);
|
|
cursor: pointer;
|
|
}
|
|
.filters-menu-clear {
|
|
width: 100%;
|
|
margin-top: 6px;
|
|
padding: 6px 8px;
|
|
background: none;
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
color: var(--text-dim);
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
}
|
|
.filters-menu-clear:hover {
|
|
color: var(--text);
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.filter-chips {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 8px 0;
|
|
margin-bottom: 4px;
|
|
}
|
|
.filter-chip {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
background: var(--accent-soft);
|
|
color: var(--accent);
|
|
border: 1px solid var(--accent);
|
|
border-radius: 14px;
|
|
padding: 3px 4px 3px 10px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
}
|
|
.filter-chip svg { width: 12px; height: 12px; }
|
|
.filter-chip-x {
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
width: 16px; height: 16px;
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
color: var(--accent);
|
|
border-radius: 50%;
|
|
padding: 0;
|
|
}
|
|
.filter-chip-x:hover { background: var(--accent); color: white; }
|
|
.filter-chip-x svg { width: 10px; height: 10px; }
|
|
.filter-chips-clear {
|
|
background: none;
|
|
border: none;
|
|
color: var(--text-faint);
|
|
font-size: 11px;
|
|
cursor: pointer;
|
|
padding: 3px 8px;
|
|
text-decoration: underline;
|
|
}
|
|
.filter-chips-clear:hover { color: var(--text-dim); }
|
|
|
|
/* ---- Note cards (kind=note) -------------------------- */
|
|
.entry-note-row {
|
|
display: flex; align-items: center;
|
|
padding: 4px 0 8px;
|
|
color: var(--text-dim);
|
|
font-size: 12px;
|
|
font-style: italic;
|
|
}
|
|
.entry-note-placeholder {
|
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
}
|
|
|
|
/* Note marker in table-view "Name" cell + faint placeholder in user cell */
|
|
.kind-badge {
|
|
display: inline-block;
|
|
padding: 1px 6px;
|
|
margin-left: 6px;
|
|
border-radius: 3px;
|
|
background: var(--accent-soft);
|
|
color: var(--accent);
|
|
font-size: 9px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
vertical-align: middle;
|
|
}
|
|
.col-user-note {
|
|
color: var(--text-faint);
|
|
font-style: italic;
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* ---- Slideover note textarea ------------------------- */
|
|
.so-note-body {
|
|
min-height: 220px;
|
|
resize: vertical;
|
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
font-size: 13px;
|
|
line-height: 1.55;
|
|
white-space: pre-wrap;
|
|
}
|
|
/* A textarea has no type="password"; -webkit-text-security is the Chromium
|
|
equivalent (WebView2 is Chromium, so it's always available here). */
|
|
.so-note-body.is-masked,
|
|
.so-custom-multiline.is-masked { -webkit-text-security: disc; }
|
|
.so-note-label { display: flex; align-items: center; justify-content: space-between; }
|
|
|
|
/* ---- + New dropdown ---------------------------------- */
|
|
.new-entry-wrap {
|
|
position: relative;
|
|
display: flex;
|
|
gap: 1px;
|
|
}
|
|
.new-entry-caret {
|
|
padding-left: 6px;
|
|
padding-right: 6px;
|
|
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
|
}
|
|
.new-entry-wrap > #newEntryBtn {
|
|
border-radius: var(--radius-sm) 0 0 var(--radius-sm);
|
|
}
|
|
.new-entry-menu {
|
|
position: absolute;
|
|
top: calc(100% + 4px); right: 0;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
box-shadow: var(--shadow-lg);
|
|
z-index: 60;
|
|
min-width: 160px;
|
|
padding: 4px;
|
|
}
|
|
.new-entry-menu .dropdown-item {
|
|
width: 100%;
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 8px 10px;
|
|
background: none; border: none;
|
|
color: var(--text);
|
|
font: inherit;
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
text-align: left;
|
|
}
|
|
.new-entry-menu .dropdown-item:hover { background: var(--accent-soft); }
|
|
.new-entry-menu .dropdown-item svg { width: 14px; height: 14px; }
|
|
.new-entry-menu .dropdown-sep {
|
|
height: 1px;
|
|
background: var(--border-soft);
|
|
margin: 4px 0;
|
|
}
|
|
.new-entry-menu .dropdown-section {
|
|
padding: 4px 10px 2px;
|
|
font-size: 10px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
color: var(--text-faint);
|
|
}
|
|
|
|
/* Table column picker dropdown — only rendered in table view. */
|
|
.cols-wrap {
|
|
position: relative; /* anchor for the absolutely-positioned menu */
|
|
display: inline-flex;
|
|
vertical-align: middle;
|
|
}
|
|
.cols-menu {
|
|
position: absolute;
|
|
top: calc(100% + 4px);
|
|
right: 0;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
box-shadow: var(--shadow-lg);
|
|
z-index: 60;
|
|
min-width: 160px;
|
|
padding: 4px;
|
|
overflow: hidden;
|
|
}
|
|
.cols-menu.is-hidden { display: none; }
|
|
.cols-menu-item {
|
|
display: flex; align-items: center; gap: 8px;
|
|
padding: 7px 10px;
|
|
font-size: 13px;
|
|
color: var(--text);
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
}
|
|
.cols-menu-item:hover { background: var(--bg-elev-2); }
|
|
.cols-menu-item input { cursor: pointer; }
|
|
|
|
/* ---- Quick search modal (tray menu) ------------------ */
|
|
.quick-search-panel {
|
|
padding: 0;
|
|
overflow: hidden;
|
|
max-width: 520px;
|
|
width: 100%;
|
|
}
|
|
.quick-search-input {
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 14px 18px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.quick-search-input svg {
|
|
width: 18px; height: 18px;
|
|
color: var(--text-dim);
|
|
flex-shrink: 0;
|
|
}
|
|
.quick-search-input input {
|
|
flex: 1;
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text);
|
|
font-size: 15px;
|
|
outline: none;
|
|
}
|
|
.quick-search-input kbd {
|
|
font-size: 10px;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
background: var(--bg);
|
|
color: var(--text-dim);
|
|
border: 1px solid var(--border);
|
|
}
|
|
.quick-search-results {
|
|
max-height: 360px;
|
|
overflow-y: auto;
|
|
padding: 6px 0;
|
|
}
|
|
.quick-search-row {
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 8px 18px;
|
|
cursor: pointer;
|
|
}
|
|
.quick-search-row.is-selected {
|
|
background: var(--accent-soft);
|
|
}
|
|
.quick-search-row:hover { background: var(--accent-soft); }
|
|
.quick-search-avatar {
|
|
width: 32px; height: 32px;
|
|
border-radius: var(--radius-sm);
|
|
background: var(--bg-elev-2);
|
|
display: grid; place-items: center;
|
|
overflow: hidden;
|
|
font-weight: 600;
|
|
font-size: 13px;
|
|
flex-shrink: 0;
|
|
}
|
|
.quick-search-avatar img {
|
|
width: 100%; height: 100%;
|
|
object-fit: contain;
|
|
padding: 4px;
|
|
box-sizing: border-box;
|
|
}
|
|
.quick-search-main {
|
|
flex: 1; min-width: 0;
|
|
}
|
|
.quick-search-name {
|
|
font-size: 14px; font-weight: 600;
|
|
color: var(--text);
|
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
}
|
|
.quick-search-sub {
|
|
font-size: 12px;
|
|
color: var(--text-dim);
|
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
}
|
|
.quick-search-empty {
|
|
padding: 24px 18px;
|
|
text-align: center;
|
|
color: var(--text-dim);
|
|
font-size: 13px;
|
|
font-style: italic;
|
|
}
|
|
.quick-search-hint {
|
|
border-top: 1px solid var(--border);
|
|
padding: 8px 18px;
|
|
font-size: 11px;
|
|
color: var(--text-faint);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* Slideover title — tint the "+ New entry" prefix in accent colour so
|
|
the mode is unambiguous without changing the header layout. */
|
|
#slideoverTitle.is-new-mode { color: var(--accent); }
|
|
#slideoverTitle.is-edit-mode { color: var(--text); }
|
|
.btn-xs {
|
|
font-size: 11px;
|
|
padding: 3px 10px;
|
|
}
|
|
|
|
/* Brief attention pulse on the slideover generator button after Fix */
|
|
@keyframes attentionPulse {
|
|
0%, 100% { box-shadow: 0 0 0 0 var(--accent); }
|
|
50% { box-shadow: 0 0 0 6px transparent; }
|
|
}
|
|
.icon-btn.is-pulse {
|
|
animation: attentionPulse 0.7s ease-out 3;
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
|
|
/* ---- 10. SLIDE-OVER -------------------------------------- */
|
|
|
|
.slideover {
|
|
position: fixed; top: 0; right: 0; bottom: 0;
|
|
width: 420px;
|
|
background: var(--bg-elev);
|
|
border-left: 1px solid var(--border);
|
|
box-shadow: var(--shadow-lg);
|
|
transform: translateX(100%);
|
|
transition: transform var(--t-base);
|
|
display: flex; flex-direction: column;
|
|
/* Above the topbar (z-index 40) — otherwise the topbar's
|
|
backdrop-filter stacking context clips the slideover header
|
|
(where the title / close button live) under it. */
|
|
z-index: 50;
|
|
}
|
|
.slideover.is-open { transform: translateX(0); }
|
|
/* Settings needs more width than the entry editor — the left tab column eats
|
|
~100px. Only #settingsPanel; #slideover stays 420. (Center mode already
|
|
sets its own 520.) */
|
|
#settingsPanel { width: 800px; }
|
|
/* Suppress the slide animation when the panel is closed — otherwise
|
|
changing editor-position animates the off-screen position from right
|
|
to left and the panel briefly crosses the viewport. */
|
|
.slideover:not(.is-open) { transition: none; }
|
|
/* Push the app shell to the left when the entry slideover is open,
|
|
so the table, pagination etc. stay visible. Only applies to the
|
|
entry editor (#slideover) — NOT #settingsPanel which would visually
|
|
re-flow the whole app. Width matches .slideover (420px). */
|
|
body[data-editor-position="right"]:has(#slideover.is-open) #appShell { margin-right: 420px; }
|
|
#appShell { transition: margin-right var(--t-base), margin-left var(--t-base); }
|
|
|
|
/* ---- Editor position: LEFT (mirrored slideover) ---- */
|
|
body[data-editor-position="left"] #slideover,
|
|
body[data-editor-position="left"] #settingsPanel {
|
|
left: 0; right: auto;
|
|
border-left: none;
|
|
border-right: 1px solid var(--border);
|
|
transform: translateX(-100%);
|
|
}
|
|
body[data-editor-position="left"] #slideover.is-open,
|
|
body[data-editor-position="left"] #settingsPanel.is-open { transform: translateX(0); }
|
|
body[data-editor-position="left"]:has(#slideover.is-open) #appShell {
|
|
margin-left: 420px;
|
|
}
|
|
|
|
/* ---- Editor position: CENTER (modal-style) ---- */
|
|
body[data-editor-position="center"] #slideover,
|
|
body[data-editor-position="center"] #settingsPanel {
|
|
top: 50%; left: 50%; right: auto; bottom: auto;
|
|
width: 520px;
|
|
max-height: 90vh;
|
|
border-radius: var(--radius-lg);
|
|
border-left: 1px solid var(--border);
|
|
transform: translate(-50%, -50%) scale(0.96);
|
|
opacity: 0;
|
|
/* Hidden panels in center mode live INSIDE the viewport (opacity 0
|
|
instead of off-screen translate), so without this guard the
|
|
buttons would stay clickable through the invisible panel. */
|
|
pointer-events: none;
|
|
transition: transform var(--t-base), opacity var(--t-base);
|
|
}
|
|
/* Settings gets a FIXED height in centered mode so switching tabs (short vs
|
|
long) doesn't resize + re-center the modal. Entry #slideover stays
|
|
content-sized (grows with the form). */
|
|
body[data-editor-position="center"] #settingsPanel { width: 800px; height: min(94vh, 720px); }
|
|
body[data-editor-position="center"] #slideover.is-open,
|
|
body[data-editor-position="center"] #settingsPanel.is-open {
|
|
transform: translate(-50%, -50%) scale(1);
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
}
|
|
/* Center mode = true modal: dim + blur backdrop, click anywhere
|
|
outside the panel dismisses it (handled in JS). The backdrop is
|
|
painted via a body pseudo-element so it covers everything but
|
|
the active panel. */
|
|
body[data-editor-position="center"]:has(#slideover.is-open)::before,
|
|
body[data-editor-position="center"]:has(#settingsPanel.is-open)::before {
|
|
content: '';
|
|
position: fixed; inset: 0;
|
|
background: rgba(0, 0, 0, 0.45);
|
|
backdrop-filter: blur(4px);
|
|
-webkit-backdrop-filter: blur(4px);
|
|
z-index: 45; /* above topbar (40), below .slideover (50) */
|
|
pointer-events: auto; /* swallow clicks so cards/sidebar don't see them */
|
|
}
|
|
.slideover-header {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 16px 20px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.slideover-header h3 {
|
|
font-size: 15px; font-weight: 600;
|
|
margin: 0;
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
}
|
|
.slideover-body { padding: 20px; overflow-y: auto; flex: 1; }
|
|
.slideover-field { margin-bottom: 16px; }
|
|
/* Settings panel search — sits below the header, above the body. Only
|
|
present inside #settingsPanel. */
|
|
.settings-search-wrap {
|
|
position: relative;
|
|
padding: 10px 16px;
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex; align-items: center;
|
|
}
|
|
.settings-search-icon {
|
|
position: absolute; left: 26px; top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 14px; height: 14px;
|
|
color: var(--text-faint);
|
|
pointer-events: none;
|
|
}
|
|
.settings-search-input {
|
|
flex: 1;
|
|
padding: 7px 30px 7px 32px;
|
|
background: var(--bg-elev-2);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text);
|
|
font-size: 13px;
|
|
outline: none;
|
|
transition: border-color var(--t-fast);
|
|
}
|
|
.settings-search-input:focus { border-color: var(--accent); }
|
|
.settings-search-clear {
|
|
position: absolute; right: 22px; top: 50%;
|
|
transform: translateY(-50%);
|
|
background: transparent; border: 0; padding: 4px;
|
|
cursor: pointer;
|
|
color: var(--text-faint);
|
|
border-radius: 4px;
|
|
display: none;
|
|
}
|
|
.settings-search-clear svg { width: 12px; height: 12px; }
|
|
.settings-search-clear:hover { color: var(--text); background: var(--bg-elev-2); }
|
|
.settings-search-wrap.has-query .settings-search-clear { display: inline-flex; }
|
|
/* Settings tabs — vertical column on the left; JS toggles .is-tab-hidden. */
|
|
.settings-main { display: flex; flex: 1; min-height: 0; }
|
|
.settings-tabs {
|
|
display: flex; flex-direction: column; gap: 4px; padding: 12px 8px;
|
|
flex: 0 0 auto; border-right: 1px solid var(--border); overflow-y: auto;
|
|
}
|
|
.settings-tab {
|
|
display: flex; align-items: center; gap: 8px;
|
|
text-align: left; width: 100%; padding: 7px 12px; font-size: 12px; cursor: pointer;
|
|
color: var(--text-dim); background: transparent; border: 1px solid transparent;
|
|
border-radius: 6px; white-space: nowrap;
|
|
}
|
|
.settings-tab svg { width: 15px; height: 15px; flex: 0 0 auto; }
|
|
.settings-tab:hover { background: var(--bg-elev-2); }
|
|
.settings-tab.is-active { color: var(--accent-fg); background: var(--accent); border-color: var(--accent); }
|
|
.slideover-field.is-tab-hidden { display: none; }
|
|
/* Guided tour (spotlight + bubble). Backdrop blocks app clicks; the spot's
|
|
huge box-shadow is the dimmer (no separate overlay needed). */
|
|
.tour-backdrop { position: fixed; inset: 0; z-index: 300; }
|
|
.tour-spot {
|
|
position: fixed; z-index: 301; pointer-events: none; border-radius: 8px;
|
|
border: 2px solid var(--accent);
|
|
box-shadow: 0 0 0 9999px rgba(0, 0, 0, .6);
|
|
transition: top .2s ease, left .2s ease, width .2s ease, height .2s ease;
|
|
}
|
|
.tour-bubble {
|
|
position: fixed; z-index: 302; max-width: 300px;
|
|
background: var(--bg-elev-2, var(--bg-2)); color: var(--text);
|
|
border: 1px solid var(--border); border-radius: 10px;
|
|
padding: 14px 16px; box-shadow: 0 8px 30px rgba(0, 0, 0, .45);
|
|
}
|
|
.tour-bubble-title { font-weight: 700; font-size: 14px; margin-bottom: 4px; }
|
|
.tour-bubble-body { font-size: 12px; color: var(--text-dim); line-height: 1.5; }
|
|
.tour-bubble-foot {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
gap: 12px; margin-top: 12px;
|
|
}
|
|
.tour-bubble-count { font-size: 11px; color: var(--text-faint); }
|
|
.tour-bubble-btns { display: flex; gap: 6px; }
|
|
/* Hidden section + per-row + no-results banner driven by JS. */
|
|
.slideover-field.is-search-hidden,
|
|
.setting-row.is-search-hidden { display: none; }
|
|
.settings-no-results {
|
|
padding: 20px;
|
|
color: var(--text-faint);
|
|
font-size: 13px;
|
|
text-align: center;
|
|
display: none;
|
|
}
|
|
.settings-no-results.is-visible { display: block; }
|
|
/* Global busy overlay — spinner + text for long ops (export, big saves). */
|
|
.busy-overlay {
|
|
position: fixed; inset: 0;
|
|
z-index: 200;
|
|
display: flex; align-items: center; justify-content: center;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
backdrop-filter: blur(3px);
|
|
-webkit-backdrop-filter: blur(3px);
|
|
}
|
|
.busy-overlay.is-hidden { display: none; }
|
|
.busy-box {
|
|
display: flex; flex-direction: column; align-items: center; gap: 14px;
|
|
padding: 24px 32px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
.busy-spinner {
|
|
width: 34px; height: 34px;
|
|
border: 3px solid var(--border);
|
|
border-top-color: var(--accent);
|
|
border-radius: 50%;
|
|
animation: busy-spin 0.7s linear infinite;
|
|
}
|
|
@keyframes busy-spin { to { transform: rotate(360deg); } }
|
|
/* Breach-scan progress. Non-blocking: no backdrop, no pointer capture.
|
|
Lives INSIDE .toast-container, so it stacks with the toasts in the same
|
|
corner instead of floating over the cards (pinned to the left edge it also
|
|
covered the sidebar navigation). column-reverse puts it above them. */
|
|
.hibp-progress {
|
|
align-self: flex-end;
|
|
display: flex; align-items: center; gap: 8px;
|
|
padding: 8px 12px;
|
|
border-radius: 8px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
|
|
font-size: 12px; color: var(--text-dim);
|
|
pointer-events: none;
|
|
}
|
|
.hibp-progress-spinner {
|
|
width: 14px; height: 14px;
|
|
border: 2px solid var(--border);
|
|
border-top-color: var(--accent);
|
|
border-radius: 50%;
|
|
animation: busy-spin 0.7s linear infinite;
|
|
flex-shrink: 0;
|
|
}
|
|
.busy-text { font-size: 13px; color: var(--text-dim); text-align: center; }
|
|
.slideover-field-label { font-size: 11px; font-weight: 500; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
|
|
.slideover-field-value {
|
|
display: flex; align-items: center; gap: 6px;
|
|
padding: 10px 12px;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border-soft);
|
|
border-radius: var(--radius-sm);
|
|
font-size: 13px;
|
|
word-break: break-all;
|
|
}
|
|
.so-input {
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text);
|
|
font-size: 13px;
|
|
transition: all var(--t-fast);
|
|
}
|
|
.so-input:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
background: var(--bg);
|
|
box-shadow: 0 0 0 3px var(--accent-soft);
|
|
}
|
|
.so-pw-row {
|
|
display: flex; align-items: center; gap: 4px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
padding: 2px 4px 2px 10px;
|
|
}
|
|
.so-pw-row:focus-within {
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 3px var(--accent-soft);
|
|
}
|
|
.so-pw-row .so-input {
|
|
background: transparent;
|
|
border: none;
|
|
padding: 8px 4px;
|
|
}
|
|
.so-pw-row .so-input:focus { box-shadow: none; }
|
|
/* Sticky action row — stays glued to the bottom of the slideover body
|
|
while it scrolls, so Save is reachable without scrolling all the way
|
|
down on entries with lots of custom fields / attachments. */
|
|
.slideover-actions {
|
|
display: flex; gap: 8px;
|
|
position: sticky;
|
|
bottom: -20px; /* cancel the .slideover-body padding-bottom */
|
|
margin: 24px -20px -20px; /* extend to the edges */
|
|
padding: 12px 20px;
|
|
background: var(--bg-elev);
|
|
border-top: 1px solid var(--border);
|
|
z-index: 2;
|
|
}
|
|
|
|
/* ---- 11. MODALS ------------------------------------------ */
|
|
|
|
.modal {
|
|
position: fixed; inset: 0;
|
|
z-index: 100;
|
|
display: grid; place-items: center;
|
|
padding: 24px;
|
|
}
|
|
.modal-backdrop {
|
|
position: absolute; inset: 0;
|
|
background: rgba(0,0,0,0.5);
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
.modal-panel {
|
|
position: relative;
|
|
width: 100%; max-width: 480px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-lg);
|
|
overflow: hidden;
|
|
animation: modalIn var(--t-base);
|
|
}
|
|
.modal-panel-sm { max-width: 360px; }
|
|
@keyframes modalIn {
|
|
from { opacity: 0; transform: scale(0.96) translateY(8px); }
|
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
|
}
|
|
.modal-header {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 16px 20px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.modal-header h3 { font-size: 15px; font-weight: 600; margin: 0; }
|
|
.modal-body { padding: 20px; }
|
|
.modal-footer {
|
|
display: flex; gap: 8px; justify-content: flex-end;
|
|
padding: 14px 20px;
|
|
background: var(--bg);
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.gen-preview {
|
|
padding: 12px;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 13px;
|
|
word-break: break-all;
|
|
margin-bottom: 16px;
|
|
min-height: 42px;
|
|
display: flex; align-items: center;
|
|
}
|
|
.gen-options { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
|
|
|
/* ---- 12. COMMAND PALETTE -------------------------------- */
|
|
|
|
.cmd-palette {
|
|
position: fixed; inset: 0;
|
|
z-index: 200;
|
|
display: grid; place-items: start center;
|
|
padding-top: 15vh;
|
|
}
|
|
.cmd-panel {
|
|
position: relative;
|
|
width: 100%; max-width: 560px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-lg);
|
|
overflow: hidden;
|
|
animation: modalIn var(--t-base);
|
|
}
|
|
.cmd-input {
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 14px 16px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.cmd-input svg { color: var(--text-faint); }
|
|
.cmd-input input {
|
|
flex: 1;
|
|
background: transparent; border: none; outline: none;
|
|
color: var(--text);
|
|
font-size: 15px;
|
|
}
|
|
.cmd-results { max-height: 360px; overflow-y: auto; padding: 8px; }
|
|
.cmd-item {
|
|
display: flex; align-items: center; gap: 10px;
|
|
padding: 10px 12px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
color: var(--text);
|
|
font-size: 13px;
|
|
}
|
|
.cmd-item:hover, .cmd-item.is-active { background: var(--bg-elev-2); }
|
|
|
|
/* ---- 13. EMPTY STATE ------------------------------------ */
|
|
|
|
.empty-state {
|
|
display: flex; flex-direction: column; align-items: center; gap: 8px;
|
|
padding: 80px 20px;
|
|
color: var(--text-dim);
|
|
animation: fadeIn var(--t-base);
|
|
}
|
|
.empty-illustration {
|
|
width: 140px; height: 140px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.empty-icon {
|
|
width: 48px; height: 48px;
|
|
padding: 12px;
|
|
background: var(--bg-elev);
|
|
border-radius: var(--radius);
|
|
color: var(--text-faint);
|
|
}
|
|
.empty-state h3 { margin: 8px 0 0; font-size: 18px; color: var(--text); font-weight: 600; }
|
|
.empty-state p { margin: 0; font-size: 13px; max-width: 320px; text-align: center; }
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(8px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
/* ---- Skeleton loaders ----------------------------------- */
|
|
.skeleton-card {
|
|
padding: 14px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
.skeleton-card::after {
|
|
content: '';
|
|
position: absolute; inset: 0;
|
|
background: linear-gradient(90deg, transparent, var(--bg-elev-2), transparent);
|
|
animation: shimmer 1.4s infinite;
|
|
}
|
|
.skeleton-line {
|
|
height: 12px;
|
|
background: var(--bg-elev-2);
|
|
border-radius: 4px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.skeleton-line.w-40 { width: 40%; }
|
|
.skeleton-line.w-60 { width: 60%; }
|
|
.skeleton-line.w-80 { width: 80%; }
|
|
.skeleton-row { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
|
.skeleton-circle {
|
|
width: 32px; height: 32px;
|
|
background: var(--bg-elev-2);
|
|
border-radius: var(--radius-sm);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
@keyframes shimmer {
|
|
from { transform: translateX(-100%); }
|
|
to { transform: translateX(100%); }
|
|
}
|
|
|
|
/* ---- Idle warning ---------------------------------------- */
|
|
.idle-warning {
|
|
position: fixed;
|
|
top: 50%; left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
z-index: 400;
|
|
width: 320px;
|
|
padding: 24px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--warning);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-lg);
|
|
text-align: center;
|
|
animation: modalIn var(--t-base);
|
|
}
|
|
.idle-warning .idle-icon {
|
|
width: 36px; height: 36px;
|
|
padding: 8px;
|
|
background: rgba(245,158,11,0.12);
|
|
color: var(--warning);
|
|
border-radius: 50%;
|
|
margin: 0 auto 8px;
|
|
display: block;
|
|
}
|
|
.idle-warning h3 { font-size: 16px; font-weight: 600; margin: 0 0 4px; }
|
|
.idle-warning p { color: var(--text-dim); font-size: 13px; margin: 0 0 16px; }
|
|
.idle-warning b { color: var(--warning); font-size: 20px; }
|
|
|
|
/* ---- 14. TOASTS ----------------------------------------- */
|
|
|
|
.toast-container {
|
|
position: fixed; bottom: 20px; right: 20px;
|
|
z-index: 300;
|
|
display: flex; flex-direction: column-reverse; gap: 8px;
|
|
pointer-events: none;
|
|
}
|
|
.toast {
|
|
pointer-events: auto;
|
|
display: flex; align-items: center; gap: 8px;
|
|
padding: 10px 14px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-left: 3px solid var(--accent);
|
|
border-radius: var(--radius-sm);
|
|
box-shadow: var(--shadow);
|
|
font-size: 13px;
|
|
min-width: 240px;
|
|
animation: toastIn var(--t-base);
|
|
}
|
|
/* Errors / warnings linger — let the user clear them instead of waiting. */
|
|
.toast.is-dismissable { cursor: pointer; }
|
|
/* Settings entry point when a hotkey is dead: a dot the user meets in the
|
|
normal UI, not only inside the panel they may never open. */
|
|
#settingsBtn.has-alert { position: relative; }
|
|
#settingsBtn.has-alert::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 6px; right: 8px;
|
|
width: 7px; height: 7px;
|
|
border-radius: 50%;
|
|
background: var(--warning, #e0a800);
|
|
}
|
|
.toast.is-success { border-left-color: var(--success); }
|
|
.toast.is-error { border-left-color: var(--danger); }
|
|
.toast.is-warning { border-left-color: var(--warning); }
|
|
@keyframes toastIn {
|
|
from { opacity: 0; transform: translateX(20px); }
|
|
to { opacity: 1; transform: translateX(0); }
|
|
}
|
|
|
|
/* ---- 15. SCROLLBARS ------------------------------------- */
|
|
|
|
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
::-webkit-scrollbar-track { background: transparent; }
|
|
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
|
::-webkit-scrollbar-thumb:hover { background: var(--text-faint); }
|
|
|
|
/* ---- 16. RESPONSIVE ------------------------------------- */
|
|
|
|
@media (max-width: 900px) {
|
|
.app-shell { grid-template-columns: 60px 1fr; }
|
|
.sidebar { padding: 12px 6px; }
|
|
.sidebar-brand span,
|
|
.nav-item > span:first-of-type,
|
|
.nav-count,
|
|
.sidebar-section-header { display: none; }
|
|
.nav-item { justify-content: center; padding: 8px; }
|
|
.topbar { padding: 12px 16px; }
|
|
.content { padding: 0 16px 16px; }
|
|
.content-header { padding-top: 16px; height: 74px; } /* 16 + 38 + 20 */
|
|
.slideover { width: 100%; }
|
|
/* On a narrow viewport the slideover fills the screen — no point
|
|
pushing the shell, the table isn't visible anyway. */
|
|
body:has(.slideover.is-open) #appShell { margin-right: 0; margin-left: 0; }
|
|
}
|
|
|
|
/* ---- 17. AUTOFILL PICKER -------------------------------- */
|
|
|
|
.autofill-pick-btn {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
width: 100%;
|
|
padding: 10px 16px;
|
|
background: none;
|
|
border: none;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
transition: background var(--t-fast);
|
|
}
|
|
.autofill-pick-btn:hover { background: var(--bg-elev-2); }
|
|
.autofill-pick-site {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--text);
|
|
}
|
|
.autofill-pick-user {
|
|
font-size: 11px;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
/* ---- Hotkey capture buttons (Settings → Autofill) ---- */
|
|
.hotkey-capture-btn {
|
|
font-family: ui-monospace, "Cascadia Code", Menlo, monospace;
|
|
font-size: 11px;
|
|
min-width: 140px;
|
|
text-align: center;
|
|
}
|
|
/* Combo refused by Windows (another app holds it) — the hotkey is dead, so the
|
|
row has to say so long after the toast is gone. ⚠ via ::after so no markup
|
|
change is needed when the state flips. */
|
|
.hotkey-capture-btn.is-conflict {
|
|
border-color: var(--warning, #e0a800);
|
|
color: var(--warning, #e0a800);
|
|
}
|
|
.hotkey-capture-btn.is-conflict::after {
|
|
content: " ⚠";
|
|
}
|
|
.hotkey-capture-btn.is-capturing {
|
|
background: var(--accent);
|
|
color: var(--accent-fg, #fff);
|
|
animation: pulse-capture 1.2s ease-in-out infinite;
|
|
}
|
|
@keyframes pulse-capture {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.6; }
|
|
}
|
|
|
|
/* ---- Card display title + subtitle (when entry.title set) ---- */
|
|
.entry-subtitle {
|
|
display: block;
|
|
font-size: 11px;
|
|
color: var(--text-faint);
|
|
margin-top: 1px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* ---- Sidebar "(no folder)" pseudo-entry ---- */
|
|
.nav-item.is-uncategorized {
|
|
color: var(--text-faint);
|
|
font-style: italic;
|
|
}
|
|
.nav-item.is-uncategorized:hover { color: var(--text-dim); }
|
|
.nav-item.is-uncategorized.is-active { color: var(--text); font-style: normal; }
|
|
/* Spacer occupies the same footprint as the SVG folder icon so labels
|
|
stay vertically aligned across real-folder rows and "(no folder)". */
|
|
.nav-icon-spacer {
|
|
display: inline-block;
|
|
width: 16px;
|
|
height: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ============================================================
|
|
Table view
|
|
============================================================ */
|
|
.entry-grid.is-table { display: block; padding: 0; }
|
|
.entry-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 13px;
|
|
}
|
|
.entry-table thead th {
|
|
text-align: left;
|
|
font-weight: 500;
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
color: var(--text-faint);
|
|
padding: 10px 12px;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--bg-elev);
|
|
user-select: none;
|
|
position: sticky; top: calc(var(--hdr-h, 0px) + var(--pag-h, 0px));
|
|
/* Above .entry-check (2), below .pagination (4) — the row checkboxes were
|
|
painted over this header for the same reason as in card view. */
|
|
z-index: 3;
|
|
}
|
|
.entry-table th.is-sortable { cursor: pointer; }
|
|
.entry-table th.is-sortable:hover { color: var(--text-dim); }
|
|
.entry-table th.is-active { color: var(--accent); }
|
|
.entry-table th .sort-arrow {
|
|
margin-left: 4px;
|
|
font-size: 10px;
|
|
vertical-align: middle;
|
|
}
|
|
.entry-table tbody td {
|
|
padding: 8px 12px;
|
|
border-bottom: 1px solid var(--border-soft);
|
|
color: var(--text);
|
|
vertical-align: middle;
|
|
}
|
|
.entry-row { cursor: pointer; transition: background var(--t-fast); }
|
|
.entry-row:hover { background: var(--bg-elev); }
|
|
.entry-row.is-selected { background: var(--accent-soft); }
|
|
.entry-row.is-checked { background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); }
|
|
.col-check { width: 36px; }
|
|
/* NAME stops growing past 360px so it doesn't push Username/Folder/
|
|
Updated all the way to the right. Long names ellipsise on a single
|
|
line so the avatar never falls under the title. */
|
|
.col-name {
|
|
min-width: 180px;
|
|
width: auto; /* shrinks to short names, grows to fit
|
|
long ones — no fixed cap, the column
|
|
matches its widest entry. */
|
|
white-space: nowrap;
|
|
padding-right: 32px; /* extra breathing room so the longest
|
|
title never butts up against Username. */
|
|
}
|
|
.col-name .cell-name-wrap {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
vertical-align: middle;
|
|
}
|
|
.col-name .entry-avatar-sm {
|
|
width: 22px; height: 22px;
|
|
display: inline-grid; place-items: center;
|
|
font-size: 10px;
|
|
margin-right: 8px;
|
|
vertical-align: middle;
|
|
}
|
|
.col-name .cell-name-wrap { display: inline-flex; align-items: center; gap: 6px; }
|
|
.col-name .fav-dot { color: var(--warning); font-size: 12px; }
|
|
.col-site, .col-user { color: var(--text-dim); }
|
|
.col-site { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.col-user { max-width: 200px; }
|
|
.col-user .icon-btn { margin-left: 4px; opacity: 0; transition: opacity var(--t-fast); }
|
|
.entry-row:hover .col-user .icon-btn { opacity: 0.7; }
|
|
.col-folder { color: var(--text-dim); white-space: nowrap; }
|
|
.col-updated { color: var(--text-faint); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
/* Soak the remaining horizontal space so NAME/Username/Folder stay
|
|
close to each other on the left rather than spreading out. */
|
|
.col-actions { width: 100%; min-width: 80px; text-align: right; white-space: nowrap; }
|
|
.col-actions .icon-btn { opacity: 0; transition: opacity var(--t-fast); }
|
|
.entry-row:hover .col-actions .icon-btn { opacity: 0.7; }
|
|
.col-actions .entry-kebab-wrap { display: inline-block; vertical-align: middle; }
|
|
|
|
/* Hide Edge/IE native password reveal eye (we ship our own toggle). */
|
|
input[type="password"]::-ms-reveal,
|
|
input[type="password"]::-ms-clear {
|
|
display: none !important;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
|
|
/* Login screen pw toggle — only one action button, override the
|
|
2-buttons layout from .input-with-action. */
|
|
.input-with-action:has(> .icon-btn:only-of-type) input {
|
|
padding-right: 40px;
|
|
}
|
|
.input-with-action:has(> .icon-btn:only-of-type) .icon-btn:only-of-type {
|
|
right: 4px;
|
|
}
|
|
|
|
/* Custom right-click menu (replaces native Edge contextmenu in inputs). */
|
|
.custom-ctxmenu {
|
|
position: fixed;
|
|
z-index: 9999;
|
|
min-width: 160px;
|
|
padding: 4px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
.custom-ctxmenu-item {
|
|
display: block;
|
|
width: 100%;
|
|
text-align: left;
|
|
padding: 7px 12px;
|
|
font-size: 13px;
|
|
background: transparent;
|
|
border: none;
|
|
border-radius: 4px;
|
|
color: var(--text);
|
|
cursor: pointer;
|
|
}
|
|
.custom-ctxmenu-item:hover:not(.is-disabled) { background: var(--bg-elev-2); }
|
|
.custom-ctxmenu-item.is-disabled { color: var(--text-faint); cursor: default; }
|
|
|
|
/* Folder delete X button — visible on row hover only. */
|
|
.nav-item .folder-delete {
|
|
margin-left: auto;
|
|
width: 20px; height: 20px;
|
|
padding: 0;
|
|
line-height: 0;
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
background: transparent; border: none;
|
|
color: var(--text-faint);
|
|
border-radius: 4px;
|
|
opacity: 0;
|
|
transition: opacity var(--t-fast), color var(--t-fast), background var(--t-fast);
|
|
cursor: pointer;
|
|
}
|
|
.nav-item .folder-delete svg { width: 14px; height: 14px; display: block; }
|
|
.nav-item:hover .folder-delete { opacity: 0.7; }
|
|
.nav-item .folder-delete:hover {
|
|
opacity: 1;
|
|
color: var(--danger);
|
|
background: var(--danger-soft);
|
|
}
|
|
.nav-item:hover .nav-count { display: none; }
|
|
|
|
/* Folder edit (pencil) button — same hover-reveal as the delete X. */
|
|
.nav-item .folder-edit {
|
|
width: 20px; height: 20px;
|
|
padding: 0;
|
|
line-height: 0;
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
background: transparent; border: none;
|
|
color: var(--text-faint);
|
|
border-radius: 4px;
|
|
opacity: 0;
|
|
transition: opacity var(--t-fast), color var(--t-fast), background var(--t-fast);
|
|
cursor: pointer;
|
|
}
|
|
.nav-item .folder-edit svg { width: 13px; height: 13px; display: block; }
|
|
.nav-item:hover .folder-edit { opacity: 0.7; }
|
|
.nav-item .folder-edit:hover {
|
|
opacity: 1;
|
|
color: var(--accent);
|
|
background: var(--accent-soft);
|
|
}
|
|
/* When folder-edit is rendered before folder-delete in the DOM, push delete
|
|
to the far right so the layout stays {icon} {label} … {edit} {delete}. */
|
|
.nav-item .folder-edit + .folder-delete { margin-left: 2px; }
|
|
|
|
/* ============================================================
|
|
FOLDER CUSTOMIZATION MODAL
|
|
============================================================ */
|
|
#folderModal .form-field { margin-bottom: 14px; }
|
|
#folderModal .form-field label {
|
|
display: block;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
color: var(--text-faint);
|
|
margin-bottom: 4px;
|
|
}
|
|
#folderModal .so-input { width: 100%; }
|
|
|
|
/* ============================================================
|
|
ATTACHMENTS list inside the slideover
|
|
============================================================ */
|
|
.so-attach-list {
|
|
margin-top: 8px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
.so-attach-empty {
|
|
font-size: 12px;
|
|
color: var(--text-faint);
|
|
padding: 6px 2px;
|
|
font-style: italic;
|
|
}
|
|
.so-attach-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 10px;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
}
|
|
.so-attach-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.so-attach-name {
|
|
font-size: 13px;
|
|
color: var(--text);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.so-attach-meta {
|
|
font-size: 11px;
|
|
color: var(--text-faint);
|
|
margin-top: 1px;
|
|
}
|
|
.so-attach-row .icon-btn { flex-shrink: 0; }
|
|
.folder-swatch-row,
|
|
.folder-icon-row {
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
margin-top: 6px;
|
|
}
|
|
.folder-swatch {
|
|
width: 26px; height: 26px;
|
|
border-radius: 50%;
|
|
border: 2px solid transparent;
|
|
background: var(--bg-elev);
|
|
cursor: pointer;
|
|
padding: 0;
|
|
transition: transform var(--t-fast), border-color var(--t-fast);
|
|
}
|
|
.folder-swatch:hover { transform: scale(1.1); }
|
|
.folder-swatch.is-active {
|
|
border-color: var(--text);
|
|
box-shadow: 0 0 0 2px var(--bg);
|
|
}
|
|
.folder-swatch.is-default {
|
|
background:
|
|
linear-gradient(135deg, transparent 47%, var(--danger) 47% 53%, transparent 53%),
|
|
var(--bg-elev);
|
|
}
|
|
.folder-icon-btn {
|
|
width: 36px; height: 36px;
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
border: 1px solid var(--border);
|
|
background: var(--bg-elev);
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
color: var(--text-dim);
|
|
transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
|
|
}
|
|
.folder-icon-btn svg { width: 18px; height: 18px; }
|
|
.folder-icon-btn:hover { background: var(--bg-elev-2); color: var(--text); }
|
|
.folder-icon-btn.is-active {
|
|
border-color: var(--accent);
|
|
color: var(--accent);
|
|
background: var(--accent-soft);
|
|
}
|
|
.folder-preview {
|
|
display: flex; align-items: center; gap: 10px;
|
|
margin: 16px 0 4px;
|
|
padding: 12px 14px;
|
|
background: var(--bg-elev);
|
|
border-radius: 6px;
|
|
color: var(--text);
|
|
font-weight: 500;
|
|
}
|
|
.folder-preview svg { width: 18px; height: 18px; }
|
|
|
|
/* Remember username checkbox row on auth screen. */
|
|
.remember-row {
|
|
display: flex; align-items: center; gap: 8px;
|
|
margin: -4px 0 12px;
|
|
font-size: 12px;
|
|
color: var(--text-dim);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
.remember-row input[type="checkbox"] {
|
|
width: 14px; height: 14px;
|
|
accent-color: var(--accent);
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Just-added entry feedback — pulse background fades out over 2.5s. */
|
|
@keyframes entryFlash {
|
|
0% { background: var(--accent-soft); box-shadow: 0 0 0 2px var(--accent), 0 8px 24px rgba(6,182,212,0.3); }
|
|
40% { background: var(--accent-soft); box-shadow: 0 0 0 2px var(--accent), 0 8px 24px rgba(6,182,212,0.2); }
|
|
100% { background: var(--bg-elev); box-shadow: 0 0 0 0 transparent; }
|
|
}
|
|
.entry-card.is-flash { animation: entryFlash 2.5s ease-out; }
|
|
.entry-row.is-flash { animation: entryFlash 2.5s ease-out; }
|
|
/* Le meme battement pour un bloc de reglage vers lequel on vient d'etre
|
|
conduit : "Sync now" sans configuration ouvre ce panneau et descend ici, et
|
|
l'animation dit lequel des blocs on est venu chercher. Reutilisee plutot que
|
|
redefinie - deux pulsations differentes pour le meme sens se remarqueraient. */
|
|
.slideover-field.is-flash { animation: entryFlash 2.5s ease-out; }
|
|
|
|
/* Pagination bar (all view modes). Sticky so the page buttons stay reachable
|
|
while scrolling a long page; z-index beats the sticky table header, which
|
|
is pushed down by --pag-h (measured in renderEntries). */
|
|
.pagination {
|
|
position: sticky;
|
|
top: var(--hdr-h, 0px);
|
|
/* Sticky layers, bottom to top: .entry-check 2 · table header 3 ·
|
|
pagination 4 · .content-header 12. At EQUAL z-index the later element in
|
|
the DOM wins, and the cards come after this bar — which is why the
|
|
checkboxes of the rows scrolling underneath used to cover it. */
|
|
z-index: 4;
|
|
background: var(--bg);
|
|
display: flex; align-items: center; gap: 6px;
|
|
padding: 14px 12px 10px;
|
|
flex-wrap: wrap;
|
|
font-size: 13px;
|
|
color: var(--text-dim);
|
|
grid-column: 1 / -1;
|
|
}
|
|
.pagination-info {
|
|
margin-right: auto;
|
|
color: var(--text-faint);
|
|
font-size: 12px;
|
|
}
|
|
.pagination-btn {
|
|
min-width: 32px;
|
|
padding: 5px 9px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
color: var(--text-dim);
|
|
cursor: pointer;
|
|
transition: all var(--t-fast);
|
|
}
|
|
.pagination-btn:hover:not(.is-active):not(.is-disabled) {
|
|
border-color: var(--accent);
|
|
color: var(--text);
|
|
}
|
|
.pagination-btn.is-active {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
color: white;
|
|
cursor: default;
|
|
}
|
|
.pagination-btn.is-disabled {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
}
|
|
.pagination-ellipsis { padding: 0 4px; color: var(--text-faint); }
|
|
.pagination-size {
|
|
margin-left: 8px;
|
|
padding: 5px 8px;
|
|
background: var(--bg-elev);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
color: var(--text-dim);
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* ============================================================
|
|
PRINTABLE RECOVERY CODE — only renders on the printed page
|
|
============================================================ */
|
|
#printRecoveryArea { display: none; }
|
|
|
|
@media print {
|
|
/* Hide the running app entirely; show only the printable sheet. */
|
|
body > *:not(#printRecoveryArea) { display: none !important; }
|
|
#printRecoveryArea {
|
|
display: block !important;
|
|
position: static;
|
|
background: #fff !important;
|
|
color: #000 !important;
|
|
}
|
|
.prc-sheet {
|
|
font-family: Georgia, 'Times New Roman', serif;
|
|
padding: 40px;
|
|
max-width: 700px;
|
|
margin: 0 auto;
|
|
}
|
|
.prc-sheet h1 {
|
|
font-size: 22px;
|
|
margin: 0 0 18px;
|
|
border-bottom: 2px solid #000;
|
|
padding-bottom: 8px;
|
|
}
|
|
.prc-meta {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 12px;
|
|
margin-bottom: 28px;
|
|
}
|
|
.prc-code {
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 32px;
|
|
letter-spacing: 6px;
|
|
text-align: center;
|
|
padding: 24px;
|
|
border: 2px dashed #000;
|
|
margin: 28px 0;
|
|
font-weight: bold;
|
|
}
|
|
.prc-instructions {
|
|
font-size: 13px;
|
|
line-height: 1.6;
|
|
}
|
|
.prc-instructions ol { padding-left: 22px; }
|
|
@page { margin: 1.5cm; }
|
|
}
|