ragevault 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +350 -0
- package/dist/assets.d.ts +14 -0
- package/dist/assets.js +47 -0
- package/dist/assets.js.map +1 -0
- package/dist/audit.d.ts +35 -0
- package/dist/audit.js +79 -0
- package/dist/audit.js.map +1 -0
- package/dist/branding.d.ts +36 -0
- package/dist/branding.js +55 -0
- package/dist/branding.js.map +1 -0
- package/dist/crypto/cipher.d.ts +74 -0
- package/dist/crypto/cipher.js +95 -0
- package/dist/crypto/cipher.js.map +1 -0
- package/dist/crypto/cipher.test.d.ts +7 -0
- package/dist/crypto/cipher.test.js +50 -0
- package/dist/crypto/cipher.test.js.map +1 -0
- package/dist/generator.d.ts +36 -0
- package/dist/generator.js +114 -0
- package/dist/generator.js.map +1 -0
- package/dist/model.d.ts +96 -0
- package/dist/model.js +43 -0
- package/dist/model.js.map +1 -0
- package/dist/porters.d.ts +35 -0
- package/dist/porters.js +175 -0
- package/dist/porters.js.map +1 -0
- package/dist/prompt.d.ts +14 -0
- package/dist/prompt.js +56 -0
- package/dist/prompt.js.map +1 -0
- package/dist/ragevault.d.ts +22 -0
- package/dist/ragevault.js +814 -0
- package/dist/ragevault.js.map +1 -0
- package/dist/server/public/app.js +420 -0
- package/dist/server/public/apple-touch-icon.png +0 -0
- package/dist/server/public/favicon.png +0 -0
- package/dist/server/public/fonts/archivo.woff2 +0 -0
- package/dist/server/public/fonts/space-grotesk.woff2 +0 -0
- package/dist/server/public/icon-192.png +0 -0
- package/dist/server/public/icon-512.png +0 -0
- package/dist/server/public/icon-maskable.png +0 -0
- package/dist/server/public/index.html +225 -0
- package/dist/server/public/manifest.webmanifest +16 -0
- package/dist/server/public/styles.css +362 -0
- package/dist/server/public/sw.js +30 -0
- package/dist/server/server.d.ts +30 -0
- package/dist/server/server.js +410 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/session.d.ts +36 -0
- package/dist/server/session.js +89 -0
- package/dist/server/session.js.map +1 -0
- package/dist/store/vaultfile.d.ts +64 -0
- package/dist/store/vaultfile.js +110 -0
- package/dist/store/vaultfile.js.map +1 -0
- package/dist/store/vaultfile.test.d.ts +5 -0
- package/dist/store/vaultfile.test.js +45 -0
- package/dist/store/vaultfile.test.js.map +1 -0
- package/dist/totp.d.ts +26 -0
- package/dist/totp.js +88 -0
- package/dist/totp.js.map +1 -0
- package/dist/totp.test.d.ts +5 -0
- package/dist/totp.test.js +41 -0
- package/dist/totp.test.js.map +1 -0
- package/dist/tui/ansi.d.ts +34 -0
- package/dist/tui/ansi.js +52 -0
- package/dist/tui/ansi.js.map +1 -0
- package/dist/tui/app.d.ts +83 -0
- package/dist/tui/app.js +607 -0
- package/dist/tui/app.js.map +1 -0
- package/dist/update/channel.d.ts +53 -0
- package/dist/update/channel.js +135 -0
- package/dist/update/channel.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
6
|
+
<title>RageVault</title>
|
|
7
|
+
<meta name="ragevault-web" content="1" />
|
|
8
|
+
<!-- RageVault web UI · SoyRage Agency — https://soyrage.es/ -->
|
|
9
|
+
<!-- Installable app (PWA) — "Add to Home Screen" on Android / iOS -->
|
|
10
|
+
<link rel="manifest" href="manifest.webmanifest" />
|
|
11
|
+
<meta name="theme-color" content="#3b9ee8" />
|
|
12
|
+
<meta name="mobile-web-app-capable" content="yes" />
|
|
13
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
14
|
+
<meta name="apple-mobile-web-app-title" content="RageVault" />
|
|
15
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
|
16
|
+
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
|
|
17
|
+
<link rel="icon" href="favicon.png" type="image/png" />
|
|
18
|
+
<link rel="stylesheet" href="styles.css" />
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
<header class="topbar">
|
|
22
|
+
<span class="logo">RAGE<span class="accent">VAULT</span></span>
|
|
23
|
+
<div class="right">
|
|
24
|
+
<span id="ver" class="muted mono"></span>
|
|
25
|
+
<button id="settings-btn" class="icon-btn hidden" title="Settings" aria-label="Settings">⚙</button>
|
|
26
|
+
<button id="lock" class="ghost hidden">Lock</button>
|
|
27
|
+
</div>
|
|
28
|
+
</header>
|
|
29
|
+
|
|
30
|
+
<main class="wrap">
|
|
31
|
+
<!-- First run — create the vault (shown when no vault exists yet) -->
|
|
32
|
+
<section id="setup" class="lockscreen hidden">
|
|
33
|
+
<div class="lock">
|
|
34
|
+
<svg viewBox="0 0 120 140" width="96" height="112" aria-hidden="true">
|
|
35
|
+
<path class="shackle" d="M35 62 V44 a25 25 0 0 1 50 0 V62" fill="none" stroke="currentColor" stroke-width="12" stroke-linecap="round"/>
|
|
36
|
+
<rect class="body" x="22" y="60" width="76" height="62" rx="12"/>
|
|
37
|
+
<circle class="keyhole" cx="60" cy="86" r="8"/><rect class="keyhole" x="56" y="90" width="8" height="18" rx="4"/>
|
|
38
|
+
</svg>
|
|
39
|
+
</div>
|
|
40
|
+
<h1 class="lock-title">Welcome to RAGE<span class="accent">VAULT</span></h1>
|
|
41
|
+
<p class="muted">Create your vault. Choose a master password you'll remember — it encrypts everything and <b>cannot be recovered</b>.</p>
|
|
42
|
+
<div class="lock-input"><input id="s-name" type="text" placeholder="Vault name (e.g. Home Lab)" /></div>
|
|
43
|
+
<div class="lock-input"><input id="s-pass" type="password" placeholder="Master password" autocomplete="new-password" /><button id="s-gen" title="Suggest a strong passphrase">⚄</button></div>
|
|
44
|
+
<div class="lock-input"><input id="s-pass2" type="password" placeholder="Confirm master password" autocomplete="new-password" /><button id="setup-btn" title="Create vault">→</button></div>
|
|
45
|
+
<p id="setup-err" class="err"></p>
|
|
46
|
+
<div class="lock-foot">
|
|
47
|
+
<div class="lf-badges">
|
|
48
|
+
<span class="lf-badge">🔑 No reset — write it down</span>
|
|
49
|
+
<span class="lf-badge">🛡 Behind your VPN</span>
|
|
50
|
+
<span class="lf-badge">⚡ No telemetry</span>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="lf-brand">RAGE<span class="accent">VAULT</span></div>
|
|
53
|
+
<div class="lf-sub">by <a href="https://soyrage.es/" target="_blank" rel="noreferrer">SoyRage Agency</a> · end-to-end encrypted · scrypt + AES-256-GCM</div>
|
|
54
|
+
</div>
|
|
55
|
+
</section>
|
|
56
|
+
|
|
57
|
+
<!-- Unlock — full-screen animated lock -->
|
|
58
|
+
<section id="unlock" class="lockscreen">
|
|
59
|
+
<div id="padlock" class="lock">
|
|
60
|
+
<svg viewBox="0 0 120 140" width="118" height="138" aria-hidden="true">
|
|
61
|
+
<path id="shackle" class="shackle" d="M35 62 V44 a25 25 0 0 1 50 0 V62" fill="none" stroke="currentColor" stroke-width="12" stroke-linecap="round"/>
|
|
62
|
+
<rect class="body" x="22" y="60" width="76" height="62" rx="12"/>
|
|
63
|
+
<circle class="keyhole" cx="60" cy="86" r="8"/>
|
|
64
|
+
<rect class="keyhole" x="56" y="90" width="8" height="18" rx="4"/>
|
|
65
|
+
</svg>
|
|
66
|
+
</div>
|
|
67
|
+
<h1 class="lock-title">RAGE<span class="accent">VAULT</span></h1>
|
|
68
|
+
<p id="lock-sub" class="muted">Enter your master password to unlock</p>
|
|
69
|
+
<div class="lock-input">
|
|
70
|
+
<input id="master" type="password" placeholder="Master password" autocomplete="current-password" />
|
|
71
|
+
<button id="unlock-btn" title="Unlock">→</button>
|
|
72
|
+
</div>
|
|
73
|
+
<p id="unlock-err" class="err"></p>
|
|
74
|
+
<div id="cop" class="cop hidden">
|
|
75
|
+
<div class="siren"></div>
|
|
76
|
+
<div class="cop-face">🚔</div>
|
|
77
|
+
<div class="cop-text">ACCESS NOT AUTHORIZED</div>
|
|
78
|
+
<div class="cop-sub">Too many failed attempts. This incident has been logged.</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="lock-foot">
|
|
81
|
+
<div class="lf-badges">
|
|
82
|
+
<span class="lf-badge">🔒 Zero plaintext on disk</span>
|
|
83
|
+
<span class="lf-badge">🛡 Reach over your VPN</span>
|
|
84
|
+
<span class="lf-badge">⚡ No telemetry</span>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="lf-brand">RAGE<span class="accent">VAULT</span></div>
|
|
87
|
+
<div class="lf-sub">by <a href="https://soyrage.es/" target="_blank" rel="noreferrer">SoyRage Agency</a> · end-to-end encrypted · scrypt + AES-256-GCM</div>
|
|
88
|
+
</div>
|
|
89
|
+
</section>
|
|
90
|
+
|
|
91
|
+
<!-- Vault -->
|
|
92
|
+
<section id="vault" class="hidden">
|
|
93
|
+
<div class="toolbar">
|
|
94
|
+
<input id="search" class="input" type="search" placeholder="Search items…" />
|
|
95
|
+
<button id="import-toggle" class="ghost">↓ Import</button>
|
|
96
|
+
<button id="add-toggle" class="primary">+ Add</button>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<form id="add-form" class="card hidden">
|
|
100
|
+
<div class="grid2">
|
|
101
|
+
<input id="f-title" class="input" placeholder="Title (e.g. GitHub)" required />
|
|
102
|
+
<input id="f-cat" class="input" placeholder="Category (optional)" />
|
|
103
|
+
<input id="f-user" class="input" placeholder="Username / email" />
|
|
104
|
+
<input id="f-url" class="input" placeholder="https://…" />
|
|
105
|
+
<input id="f-icon" class="input" placeholder="Icon — emoji (optional)" maxlength="4" />
|
|
106
|
+
<input id="f-pass" class="input" placeholder="Password" />
|
|
107
|
+
</div>
|
|
108
|
+
<div class="genrow">
|
|
109
|
+
<button type="button" id="f-gen" class="gen-black">⚡ Generate strong password</button>
|
|
110
|
+
</div>
|
|
111
|
+
<span id="f-strength" class="tiny muted center"></span>
|
|
112
|
+
<div class="row end">
|
|
113
|
+
<button type="submit" class="primary">Save item</button>
|
|
114
|
+
</div>
|
|
115
|
+
</form>
|
|
116
|
+
|
|
117
|
+
<ul id="items" class="items"></ul>
|
|
118
|
+
</section>
|
|
119
|
+
|
|
120
|
+
<!-- One-click import — “switching is easy” -->
|
|
121
|
+
<div id="import-modal" class="modal-backdrop hidden">
|
|
122
|
+
<div class="modal import-card">
|
|
123
|
+
<button id="import-close" class="modal-x" title="Close" aria-label="Close">✕</button>
|
|
124
|
+
<div class="import-badge">Switching is easy</div>
|
|
125
|
+
<h2 class="display import-title">Bring every password with you</h2>
|
|
126
|
+
<p class="muted import-lead">Export from your current manager, drop the file below, and you're done. RageVault auto-detects the format — no manual mapping, nothing leaves this machine.</p>
|
|
127
|
+
<div class="providers">
|
|
128
|
+
<div class="prov"><span class="prov-logo bw">bw</span>Bitwarden</div>
|
|
129
|
+
<div class="prov"><span class="prov-logo op">1P</span>1Password</div>
|
|
130
|
+
<div class="prov"><span class="prov-logo pp">P</span>Proton Pass</div>
|
|
131
|
+
<div class="prov"><span class="prov-logo lp">::</span>LastPass</div>
|
|
132
|
+
<div class="prov"><span class="prov-logo csv">≡</span>Any CSV</div>
|
|
133
|
+
</div>
|
|
134
|
+
<label id="dropzone" class="dropzone">
|
|
135
|
+
<input id="import-file" type="file" accept=".json,.csv,.txt" hidden />
|
|
136
|
+
<div class="dz-icon">⬆</div>
|
|
137
|
+
<div class="dz-main">Drop your export here <span class="muted">or click to browse</span></div>
|
|
138
|
+
<div class="tiny muted">.json or .csv · parsed locally, never uploaded</div>
|
|
139
|
+
</label>
|
|
140
|
+
<p id="import-status" class="import-status"></p>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<!-- Settings — themes, change master password, replay tour -->
|
|
145
|
+
<div id="settings-modal" class="modal-backdrop hidden">
|
|
146
|
+
<div class="modal settings-card">
|
|
147
|
+
<button id="settings-close" class="modal-x" title="Close" aria-label="Close">✕</button>
|
|
148
|
+
<h2 class="display settings-title">Settings</h2>
|
|
149
|
+
|
|
150
|
+
<div class="set-section">
|
|
151
|
+
<div class="set-label">Appearance</div>
|
|
152
|
+
<div class="themes" id="themes">
|
|
153
|
+
<button class="theme-swatch" data-theme="cream"><span class="sw sw-cream"></span>Cream</button>
|
|
154
|
+
<button class="theme-swatch" data-theme="dark"><span class="sw sw-dark"></span>Dark</button>
|
|
155
|
+
<button class="theme-swatch" data-theme="midnight"><span class="sw sw-midnight"></span>Midnight</button>
|
|
156
|
+
<button class="theme-swatch" data-theme="forest"><span class="sw sw-forest"></span>Forest</button>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div class="set-section">
|
|
161
|
+
<div class="set-label">Change master password</div>
|
|
162
|
+
<input id="cp-current" class="input" type="password" placeholder="Current master password" autocomplete="current-password" />
|
|
163
|
+
<input id="cp-new" class="input" type="password" placeholder="New master password" autocomplete="new-password" />
|
|
164
|
+
<input id="cp-confirm" class="input" type="password" placeholder="Confirm new password" autocomplete="new-password" />
|
|
165
|
+
<div class="row end"><button id="cp-save" class="primary">Update password</button></div>
|
|
166
|
+
<p id="cp-status" class="tiny cp-status"></p>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<div class="set-section set-about">
|
|
170
|
+
<span class="muted">RageVault <span id="set-ver" class="mono"></span> · <a href="https://soyrage.es/" target="_blank" rel="noreferrer">SoyRage Agency</a></span>
|
|
171
|
+
<button id="tour-replay" class="link-btn">↻ Replay the tour</button>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<!-- First-run interactive spotlight tour -->
|
|
177
|
+
<div id="tour" class="tour hidden" aria-hidden="true">
|
|
178
|
+
<div id="tour-hole" class="tour-hole"></div>
|
|
179
|
+
<div id="tour-pop" class="tour-pop">
|
|
180
|
+
<div class="tour-step">RageVault tour</div>
|
|
181
|
+
<div id="tour-title" class="tour-title"></div>
|
|
182
|
+
<div id="tour-body" class="tour-body"></div>
|
|
183
|
+
<div class="tour-nav">
|
|
184
|
+
<span id="tour-progress" class="tour-progress"></span>
|
|
185
|
+
<div class="tour-btns">
|
|
186
|
+
<button id="tour-skip" class="link-btn">Skip</button>
|
|
187
|
+
<button id="tour-next" class="primary tour-next">Next →</button>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</main>
|
|
193
|
+
|
|
194
|
+
<footer class="foot">
|
|
195
|
+
<div class="foot-brand">RAGE<span class="accent">VAULT</span></div>
|
|
196
|
+
<div class="foot-tag">Self-hosted · end-to-end encrypted · <span class="mono">scrypt + AES-256-GCM</span></div>
|
|
197
|
+
<div class="foot-badges">
|
|
198
|
+
<span class="fbadge">🔒 Zero plaintext on disk</span>
|
|
199
|
+
<span class="fbadge">🛡 Behind your VPN</span>
|
|
200
|
+
<span class="fbadge">⚡ No telemetry</span>
|
|
201
|
+
</div>
|
|
202
|
+
<div class="foot-links">
|
|
203
|
+
<a href="https://soyrage.es/" target="_blank" rel="noreferrer">SoyRage Agency</a>
|
|
204
|
+
<span class="dotsep">·</span>
|
|
205
|
+
<a href="https://github.com/soyrageagency/ragevault" target="_blank" rel="noreferrer">GitHub</a>
|
|
206
|
+
<span class="dotsep">·</span>
|
|
207
|
+
<a href="https://haveibeenpwned.com/Passwords" target="_blank" rel="noreferrer">Breach-checked</a>
|
|
208
|
+
</div>
|
|
209
|
+
</footer>
|
|
210
|
+
|
|
211
|
+
<!-- Unlock reveal: a black circle zooms in with the green open padlock, then bursts open -->
|
|
212
|
+
<div id="reveal" class="reveal hidden" aria-hidden="true">
|
|
213
|
+
<div class="circle">
|
|
214
|
+
<svg viewBox="0 -54 120 200" width="90" height="150">
|
|
215
|
+
<path class="rshackle" d="M35 62 V44 a25 25 0 0 1 50 0 V62" fill="none" stroke="#3ad07f" stroke-width="12" stroke-linecap="round"/>
|
|
216
|
+
<rect x="22" y="60" width="76" height="62" rx="12" fill="#3ad07f"/>
|
|
217
|
+
<circle cx="60" cy="86" r="8" fill="#0b3d22"/>
|
|
218
|
+
<rect x="56" y="90" width="8" height="18" rx="4" fill="#0b3d22"/>
|
|
219
|
+
</svg>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
<script src="app.js"></script>
|
|
224
|
+
</body>
|
|
225
|
+
</html>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "RageVault",
|
|
3
|
+
"short_name": "RageVault",
|
|
4
|
+
"description": "Your self-hosted, end-to-end-encrypted password manager.",
|
|
5
|
+
"start_url": "./",
|
|
6
|
+
"scope": "./",
|
|
7
|
+
"display": "standalone",
|
|
8
|
+
"orientation": "portrait",
|
|
9
|
+
"background_color": "#f3f1ea",
|
|
10
|
+
"theme_color": "#3b9ee8",
|
|
11
|
+
"icons": [
|
|
12
|
+
{ "src": "icon-192.png", "sizes": "192x192", "type": "image/png" },
|
|
13
|
+
{ "src": "icon-512.png", "sizes": "512x512", "type": "image/png" },
|
|
14
|
+
{ "src": "icon-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/* RageVault web UI — SoyRage minimalist (cream / ink / blue). */
|
|
2
|
+
/* Self-hosted display faces — no external request (privacy: nothing leaves the box). */
|
|
3
|
+
@font-face {
|
|
4
|
+
font-family: "Space Grotesk"; font-style: normal; font-weight: 300 700;
|
|
5
|
+
font-display: swap; src: url("fonts/space-grotesk.woff2") format("woff2");
|
|
6
|
+
}
|
|
7
|
+
@font-face {
|
|
8
|
+
font-family: "Archivo"; font-style: normal; font-weight: 600 900;
|
|
9
|
+
font-display: swap; src: url("fonts/archivo.woff2") format("woff2");
|
|
10
|
+
}
|
|
11
|
+
:root {
|
|
12
|
+
--cream: #f3f1ea; --bg: #ffffff; --bg2: #faf9f5; --ink: #111111; --muted: #8b8b86;
|
|
13
|
+
--accent: #3b9ee8; --accent-2: #6cc0ff; --accent-soft: rgba(59,158,232,0.12); --border: #e7e3da;
|
|
14
|
+
--green: #3a8f5c; --red: #c8524a; --amber: #b8892a; --radius: 16px;
|
|
15
|
+
--sans: "Inter", "Segoe UI", system-ui, sans-serif; --mono: "SFMono-Regular", Consolas, monospace;
|
|
16
|
+
--display: "Space Grotesk", "Inter", "Segoe UI", system-ui, sans-serif;
|
|
17
|
+
--wordmark: "Archivo", "Space Grotesk", "Segoe UI", system-ui, sans-serif;
|
|
18
|
+
}
|
|
19
|
+
* { box-sizing: border-box; }
|
|
20
|
+
body { margin: 0; font-family: var(--sans); color: var(--ink); background: var(--cream);
|
|
21
|
+
background-image: linear-gradient(rgba(17,17,17,.02) 1px, transparent 1px), linear-gradient(90deg, rgba(17,17,17,.02) 1px, transparent 1px);
|
|
22
|
+
background-size: 46px 46px; min-height: 100vh; position: relative; }
|
|
23
|
+
/* Creative blue "sun" glow rising from the bottom so the page never feels flat by the footer. */
|
|
24
|
+
body::after {
|
|
25
|
+
content: ""; position: fixed; left: 50%; bottom: -240px; transform: translateX(-50%);
|
|
26
|
+
width: min(1100px, 150vw); height: 480px; pointer-events: none; z-index: 0;
|
|
27
|
+
background: radial-gradient(60% 100% at 50% 100%, rgba(59,158,232,.34) 0%, rgba(108,192,255,.18) 34%, rgba(59,158,232,.06) 62%, transparent 78%);
|
|
28
|
+
filter: blur(26px); animation: sunBreathe 7s ease-in-out infinite;
|
|
29
|
+
}
|
|
30
|
+
@keyframes sunBreathe { 0%,100% { opacity: .85; transform: translateX(-50%) scale(1); } 50% { opacity: 1; transform: translateX(-50%) scale(1.06); } }
|
|
31
|
+
.topbar, .wrap, .foot, #vault, .lockscreen { position: relative; z-index: 1; }
|
|
32
|
+
h1, .logo, .lock-title, .display { font-family: var(--display); }
|
|
33
|
+
.topbar { display: flex; align-items: center; justify-content: space-between; padding: 16px 24px; border-bottom: 1px solid var(--border); background: rgba(243,241,234,.8); backdrop-filter: blur(6px); position: sticky; top: 0; }
|
|
34
|
+
.logo { font-family: var(--wordmark); font-weight: 900; letter-spacing: -.01em; font-size: 20px; }
|
|
35
|
+
.accent { color: var(--accent); }
|
|
36
|
+
.right { display: flex; align-items: center; gap: 12px; }
|
|
37
|
+
.wrap { max-width: 760px; margin: 0 auto; padding: 28px 20px 60px; }
|
|
38
|
+
.card { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 22px; box-shadow: 0 1px 2px rgba(17,17,17,.04); }
|
|
39
|
+
.narrow { max-width: 420px; margin: 8vh auto 0; display: flex; flex-direction: column; gap: 12px; }
|
|
40
|
+
h1 { font-size: 20px; margin: 0; }
|
|
41
|
+
.input { padding: 11px 13px; border: 1px solid var(--border); border-radius: 12px; background: var(--bg); font-size: 14px; outline: none; width: 100%; }
|
|
42
|
+
.input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
43
|
+
.primary { background: var(--ink); color: #fff; border: none; border-radius: 999px; padding: 10px 18px; font-weight: 600; cursor: pointer; font-size: 14px; white-space: nowrap; }
|
|
44
|
+
.primary:hover { background: #000; }
|
|
45
|
+
.ghost { background: var(--bg); border: 1px solid var(--border); border-radius: 999px; padding: 8px 14px; font-size: 13px; cursor: pointer; }
|
|
46
|
+
.ghost:hover { border-color: var(--accent); color: var(--accent); }
|
|
47
|
+
.muted { color: var(--muted); font-size: 13px; }
|
|
48
|
+
.tiny { font-size: 11px; }
|
|
49
|
+
.mono { font-family: var(--mono); }
|
|
50
|
+
.err { color: var(--red); font-size: 13px; min-height: 16px; margin: 0; }
|
|
51
|
+
.hidden { display: none; }
|
|
52
|
+
.toolbar { display: flex; gap: 10px; margin-bottom: 16px; }
|
|
53
|
+
.toolbar .input { flex: 1; }
|
|
54
|
+
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px; }
|
|
55
|
+
.row { display: flex; gap: 10px; align-items: center; }
|
|
56
|
+
.grow { flex: 1; }
|
|
57
|
+
.items { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
|
|
58
|
+
.item { display: flex; align-items: center; gap: 12px; padding: 14px 16px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); }
|
|
59
|
+
.item .avatar { position: relative; width: 34px; height: 34px; border-radius: 10px; background: var(--accent-soft); color: var(--accent); display: grid; place-items: center; font-weight: 700; overflow: hidden; }
|
|
60
|
+
/* A loaded favicon covers the letter fallback (white bg so transparent icons read). */
|
|
61
|
+
.item .avatar .fav { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; padding: 5px; background: #fff; }
|
|
62
|
+
.item .body { flex: 1; min-width: 0; }
|
|
63
|
+
.item .title { font-weight: 600; }
|
|
64
|
+
.item .sub { color: var(--muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
65
|
+
.item .secret { font-family: var(--mono); letter-spacing: 2px; color: var(--muted); }
|
|
66
|
+
.item .secret.shown { letter-spacing: 0; color: var(--accent); }
|
|
67
|
+
.item .acts { display: flex; gap: 6px; }
|
|
68
|
+
.item .acts button { border: 1px solid var(--border); background: var(--bg2); border-radius: 8px; padding: 5px 10px; font-size: 12px; cursor: pointer; }
|
|
69
|
+
.item .acts button:hover { border-color: var(--accent); color: var(--accent); }
|
|
70
|
+
.foot { display: flex; align-items: center; justify-content: space-between; max-width: 760px; margin: 0 auto; padding: 20px; color: var(--muted); font-size: 12px; }
|
|
71
|
+
.foot a { color: var(--accent); }
|
|
72
|
+
@media (max-width: 560px) { .grid2 { grid-template-columns: 1fr; } }
|
|
73
|
+
|
|
74
|
+
/* ============================ Lock screen ============================ */
|
|
75
|
+
.lockscreen {
|
|
76
|
+
position: fixed; inset: 0; z-index: 50; display: flex; flex-direction: column;
|
|
77
|
+
align-items: center; justify-content: center; gap: 14px; padding: 24px;
|
|
78
|
+
background:
|
|
79
|
+
radial-gradient(66% 46% at 50% 116%, rgba(59,158,232,.34) 0%, rgba(108,192,255,.16) 38%, rgba(59,158,232,.04) 64%, transparent 78%),
|
|
80
|
+
radial-gradient(120% 90% at 50% 0%, #fbfaf6 0%, var(--cream) 55%);
|
|
81
|
+
}
|
|
82
|
+
/* .lockscreen's own display:flex would beat the earlier .hidden rule, so scope it. */
|
|
83
|
+
.lockscreen.hidden { display: none !important; }
|
|
84
|
+
/* The glow gently breathes behind the padlock. */
|
|
85
|
+
.lockscreen::after {
|
|
86
|
+
content: ""; position: absolute; left: 50%; bottom: -160px; transform: translateX(-50%);
|
|
87
|
+
width: min(900px, 130vw); height: 360px; pointer-events: none; z-index: -1;
|
|
88
|
+
background: radial-gradient(60% 100% at 50% 100%, rgba(59,158,232,.30) 0%, rgba(108,192,255,.14) 40%, transparent 74%);
|
|
89
|
+
filter: blur(30px); animation: sunBreathe 7s ease-in-out infinite;
|
|
90
|
+
}
|
|
91
|
+
.lock { color: var(--accent); transition: color .25s ease; }
|
|
92
|
+
.lock svg .body { fill: currentColor; }
|
|
93
|
+
.lock svg .keyhole { fill: var(--cream); }
|
|
94
|
+
.lock svg .shackle { transition: transform .5s cubic-bezier(.2,.9,.2,1); transform-origin: 60px 60px; }
|
|
95
|
+
.lock-title { font-family: var(--wordmark); margin: 6px 0 0; font-size: 40px; font-weight: 900; letter-spacing: -.025em; line-height: 1; }
|
|
96
|
+
.lock-title .accent { color: var(--accent); }
|
|
97
|
+
.lock-input { display: flex; gap: 8px; margin-top: 6px; width: min(360px, 90vw); }
|
|
98
|
+
.lock-input input {
|
|
99
|
+
flex: 1; padding: 13px 16px; border: 1px solid var(--border); border-radius: 999px;
|
|
100
|
+
background: var(--bg); font-size: 15px; outline: none; text-align: center;
|
|
101
|
+
}
|
|
102
|
+
.lock-input input:focus { border-color: var(--accent); box-shadow: 0 0 0 4px var(--accent-soft); }
|
|
103
|
+
.lock-input button {
|
|
104
|
+
width: 48px; border: none; border-radius: 50%; background: var(--ink); color: #fff;
|
|
105
|
+
font-size: 20px; cursor: pointer; transition: background .2s;
|
|
106
|
+
}
|
|
107
|
+
.lock-input button:hover { background: #000; }
|
|
108
|
+
.lock-vpn { margin-top: 2px; }
|
|
109
|
+
|
|
110
|
+
/* Wrong password: turn red + shake. */
|
|
111
|
+
.lock.fail { color: var(--red); animation: shake .5s; }
|
|
112
|
+
.lock.fail svg .body { fill: var(--red); }
|
|
113
|
+
.lockscreen.fail .lock-input input { border-color: var(--red); box-shadow: 0 0 0 4px rgba(200,82,74,.18); }
|
|
114
|
+
@keyframes shake {
|
|
115
|
+
10%,90%{transform:translateX(-2px)} 20%,80%{transform:translateX(4px)}
|
|
116
|
+
30%,50%,70%{transform:translateX(-8px)} 40%,60%{transform:translateX(8px)}
|
|
117
|
+
}
|
|
118
|
+
/* Correct: shackle lifts, glow green, then the screen fades away. */
|
|
119
|
+
.lock.open { color: var(--green); }
|
|
120
|
+
.lock.open svg .body { fill: var(--green); }
|
|
121
|
+
.lock.open svg .shackle { transform: translateY(-16px); }
|
|
122
|
+
.lockscreen.open { animation: unlockOut .55s ease .35s forwards; }
|
|
123
|
+
@keyframes unlockOut { to { opacity: 0; transform: scale(1.04); visibility: hidden; } }
|
|
124
|
+
|
|
125
|
+
/* Temporary lockout. */
|
|
126
|
+
.lockscreen.locked .lock-input input, .lockscreen.locked .lock-input button { opacity: .5; pointer-events: none; }
|
|
127
|
+
|
|
128
|
+
/* ===== "ACCESS NOT AUTHORIZED" easter egg (many failures) ===== */
|
|
129
|
+
.cop {
|
|
130
|
+
position: fixed; inset: 0; z-index: 60; display: flex; flex-direction: column;
|
|
131
|
+
align-items: center; justify-content: center; gap: 14px; color: #fff;
|
|
132
|
+
background: #7a1410; animation: copIn .3s ease;
|
|
133
|
+
}
|
|
134
|
+
.cop.hidden { display: none; }
|
|
135
|
+
.cop .cop-face { font-size: 92px; animation: bob 1s ease-in-out infinite; }
|
|
136
|
+
.cop .cop-text { font-weight: 900; font-size: clamp(24px, 7vw, 54px); letter-spacing: .06em; text-shadow: 0 2px 0 rgba(0,0,0,.3); }
|
|
137
|
+
.cop .cop-sub { opacity: .85; font-size: 14px; }
|
|
138
|
+
.cop .siren { position: absolute; inset: 0; pointer-events: none; animation: siren 1s steps(1) infinite; }
|
|
139
|
+
@keyframes siren { 0%{background:radial-gradient(60% 50% at 20% 0%, rgba(255,60,60,.6), transparent 60%)} 50%{background:radial-gradient(60% 50% at 80% 0%, rgba(80,120,255,.6), transparent 60%)} }
|
|
140
|
+
@keyframes bob { 50% { transform: translateY(-10px) rotate(-4deg); } }
|
|
141
|
+
@keyframes copIn { from { opacity: 0; } }
|
|
142
|
+
|
|
143
|
+
/* ============================ Update banner ============================ */
|
|
144
|
+
#rv-update {
|
|
145
|
+
position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%); z-index: 40;
|
|
146
|
+
display: flex; align-items: center; gap: 12px; padding: 12px 16px;
|
|
147
|
+
background: var(--ink); color: #fff; border-radius: 999px; box-shadow: 0 8px 30px rgba(0,0,0,.25);
|
|
148
|
+
font-size: 13px;
|
|
149
|
+
}
|
|
150
|
+
#rv-update.hidden { display: none; }
|
|
151
|
+
#rv-update button { border: none; border-radius: 999px; padding: 7px 14px; font-weight: 600; cursor: pointer; background: var(--accent); color: #fff; }
|
|
152
|
+
#rv-update .later { background: transparent; color: rgba(255,255,255,.7); }
|
|
153
|
+
|
|
154
|
+
/* ============================ Mobile polish ============================ */
|
|
155
|
+
@media (max-width: 560px) {
|
|
156
|
+
.grid2 { grid-template-columns: 1fr; }
|
|
157
|
+
.wrap { padding: 16px 12px 80px; }
|
|
158
|
+
.topbar { padding: 12px 16px; }
|
|
159
|
+
/* Cards: title on its own line, secret + actions wrap to a second row. */
|
|
160
|
+
.item { flex-wrap: wrap; gap: 8px 10px; }
|
|
161
|
+
.item .avatar { width: 38px; height: 38px; }
|
|
162
|
+
.item .body { flex: 1 1 calc(100% - 50px); min-width: 0; }
|
|
163
|
+
.item .body .title { white-space: normal; line-height: 1.2; }
|
|
164
|
+
.item .secret { order: 3; flex: 1 1 auto; margin-left: 50px; text-align: left; }
|
|
165
|
+
.item .acts { order: 4; margin-left: auto; }
|
|
166
|
+
.item .acts button { padding: 8px 14px; font-size: 13px; } /* bigger tap targets */
|
|
167
|
+
.toolbar .input { min-width: 0; }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* ===== Creative unlock reveal (black circle → green padlock opens) =====
|
|
171
|
+
Scoped to the #reveal overlay by id — the item action buttons also use a
|
|
172
|
+
`.reveal` class, so class selectors here would collide with them. */
|
|
173
|
+
#reveal {
|
|
174
|
+
position: fixed; inset: 0; z-index: 70; display: flex; align-items: center; justify-content: center;
|
|
175
|
+
pointer-events: none;
|
|
176
|
+
/* Frosted, subtly-tinted backdrop that blurs whatever's behind. */
|
|
177
|
+
backdrop-filter: blur(16px) saturate(120%); -webkit-backdrop-filter: blur(16px) saturate(120%);
|
|
178
|
+
background: radial-gradient(circle at 50% 44%, rgba(59,158,232,.12), rgba(13,15,18,.06) 55%, rgba(13,15,18,.24));
|
|
179
|
+
}
|
|
180
|
+
#reveal.hidden { display: none; }
|
|
181
|
+
#reveal .circle {
|
|
182
|
+
width: 220px; height: 220px; border-radius: 50%; background: #0d0f12;
|
|
183
|
+
display: flex; align-items: center; justify-content: center; transform: scale(0);
|
|
184
|
+
box-shadow: 0 30px 80px rgba(0,0,0,.45);
|
|
185
|
+
}
|
|
186
|
+
#reveal .rshackle { transform-origin: 84px 60px; } /* hinge on the right leg */
|
|
187
|
+
#reveal.grow .circle { animation: revealGrow .45s cubic-bezier(.2,.9,.2,1) forwards; }
|
|
188
|
+
/* The green padlock lifts then swings its shackle open, like a real lock. */
|
|
189
|
+
#reveal.grow .rshackle { animation: shackleOpen .7s .28s cubic-bezier(.34,1.3,.4,1) forwards; }
|
|
190
|
+
/* Exit: no zoom — the circle just fades away, revealing the vault. */
|
|
191
|
+
#reveal.fade { animation: revealFade .5s ease forwards; }
|
|
192
|
+
@keyframes revealGrow { 0% { transform: scale(0) rotate(-14deg); } 68% { transform: scale(1.05) rotate(3deg); } 100% { transform: scale(1) rotate(0); } }
|
|
193
|
+
@keyframes shackleOpen { 0% { transform: translateY(0) rotate(0); } 42% { transform: translateY(-17px) rotate(0); } 100% { transform: translateY(-11px) rotate(36deg); } }
|
|
194
|
+
@keyframes revealFade { to { opacity: 0; } }
|
|
195
|
+
|
|
196
|
+
/* Staggered item entrance when the vault first opens. */
|
|
197
|
+
.items.just-opened .item { opacity: 0; animation: itemIn .4s cubic-bezier(.2,.9,.2,1) forwards; }
|
|
198
|
+
.items.just-opened .item:nth-child(1){animation-delay:.02s} .items.just-opened .item:nth-child(2){animation-delay:.07s}
|
|
199
|
+
.items.just-opened .item:nth-child(3){animation-delay:.12s} .items.just-opened .item:nth-child(4){animation-delay:.17s}
|
|
200
|
+
.items.just-opened .item:nth-child(5){animation-delay:.22s} .items.just-opened .item:nth-child(6){animation-delay:.27s}
|
|
201
|
+
.items.just-opened .item:nth-child(7){animation-delay:.32s} .items.just-opened .item:nth-child(8){animation-delay:.37s}
|
|
202
|
+
.items.just-opened .item:nth-child(n+9){animation-delay:.42s}
|
|
203
|
+
@keyframes itemIn { from { opacity: 0; transform: translateY(14px) scale(.98); } to { opacity: 1; transform: none; } }
|
|
204
|
+
|
|
205
|
+
/* The vault content eases in as the reveal overlay clears. */
|
|
206
|
+
#vault.entering { animation: vaultIn .6s cubic-bezier(.2,.9,.2,1); }
|
|
207
|
+
@keyframes vaultIn { from { opacity: 0; transform: translateY(10px) scale(.992); filter: blur(4px); } to { opacity: 1; transform: none; filter: none; } }
|
|
208
|
+
|
|
209
|
+
/* ============================ Import modal ============================ */
|
|
210
|
+
.modal-backdrop {
|
|
211
|
+
position: fixed; inset: 0; z-index: 60; display: grid; place-items: center; padding: 20px;
|
|
212
|
+
background: rgba(17,17,17,.34); backdrop-filter: blur(6px); animation: fadeIn .2s ease;
|
|
213
|
+
}
|
|
214
|
+
/* Its own display:grid would otherwise beat the earlier .hidden rule (same specificity, later wins). */
|
|
215
|
+
.modal-backdrop.hidden { display: none; }
|
|
216
|
+
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
217
|
+
.modal {
|
|
218
|
+
position: relative; width: min(560px, 96vw); background: var(--bg); border: 1px solid var(--border);
|
|
219
|
+
border-radius: 22px; padding: 30px 30px 26px; box-shadow: 0 30px 80px rgba(17,17,17,.22);
|
|
220
|
+
animation: popIn .26s cubic-bezier(.2,.9,.2,1);
|
|
221
|
+
background-image: radial-gradient(120% 70% at 50% 118%, rgba(59,158,232,.12), transparent 60%);
|
|
222
|
+
}
|
|
223
|
+
@keyframes popIn { from { opacity: 0; transform: translateY(14px) scale(.97); } to { opacity: 1; transform: none; } }
|
|
224
|
+
.modal-x { position: absolute; top: 14px; right: 16px; border: none; background: transparent; font-size: 16px; color: var(--muted); cursor: pointer; line-height: 1; }
|
|
225
|
+
.modal-x:hover { color: var(--ink); }
|
|
226
|
+
.import-badge {
|
|
227
|
+
display: inline-block; font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
|
|
228
|
+
color: var(--accent); background: var(--accent-soft); padding: 5px 11px; border-radius: 999px; margin-bottom: 12px;
|
|
229
|
+
}
|
|
230
|
+
.import-title { font-size: 27px; font-weight: 700; margin: 0 0 6px; letter-spacing: -.01em; }
|
|
231
|
+
.import-lead { margin: 0 0 18px; line-height: 1.5; }
|
|
232
|
+
.providers { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 18px; }
|
|
233
|
+
.prov { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; padding: 7px 12px 7px 8px; border: 1px solid var(--border); border-radius: 999px; background: var(--bg2); }
|
|
234
|
+
.prov-logo { width: 24px; height: 24px; border-radius: 7px; display: grid; place-items: center; font-size: 11px; font-weight: 800; color: #fff; font-family: var(--display); }
|
|
235
|
+
.prov-logo.bw { background: #175ddc; } .prov-logo.op { background: #1a70e2; }
|
|
236
|
+
.prov-logo.pp { background: #6d4aff; } .prov-logo.lp { background: #d32d27; }
|
|
237
|
+
.prov-logo.csv { background: #3a8f5c; }
|
|
238
|
+
.dropzone {
|
|
239
|
+
display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center; cursor: pointer;
|
|
240
|
+
padding: 30px 20px; border: 2px dashed var(--border); border-radius: 18px; background: var(--bg2);
|
|
241
|
+
transition: border-color .18s, background .18s, transform .18s;
|
|
242
|
+
}
|
|
243
|
+
.dropzone:hover { border-color: var(--accent); }
|
|
244
|
+
.dropzone.drag { border-color: var(--accent); background: var(--accent-soft); transform: scale(1.01); }
|
|
245
|
+
.dz-icon {
|
|
246
|
+
width: 46px; height: 46px; border-radius: 50%; display: grid; place-items: center; font-size: 20px;
|
|
247
|
+
color: #fff; background: var(--accent); box-shadow: 0 8px 22px rgba(59,158,232,.4); margin-bottom: 4px;
|
|
248
|
+
}
|
|
249
|
+
.dz-main { font-weight: 600; font-size: 15px; }
|
|
250
|
+
.import-status { min-height: 18px; margin: 14px 0 0; font-size: 13px; font-weight: 600; text-align: center; }
|
|
251
|
+
.import-status.ok { color: var(--green); } .import-status.err { color: var(--red); } .import-status.working { color: var(--muted); }
|
|
252
|
+
@media (max-width: 560px) { .modal { padding: 26px 20px 22px; } .import-title { font-size: 23px; } }
|
|
253
|
+
|
|
254
|
+
/* ============================ Themes ============================ */
|
|
255
|
+
html[data-theme="dark"] {
|
|
256
|
+
--cream: #14161b; --bg: #1e222a; --bg2: #191c23; --ink: #e9ebef; --muted: #98a0ac;
|
|
257
|
+
--accent: #4fa8ec; --accent-2: #79c1ff; --accent-soft: rgba(79,168,236,.18); --border: #2c313c;
|
|
258
|
+
--green: #52c08a; --red: #e0736b; --amber: #e0b04a;
|
|
259
|
+
}
|
|
260
|
+
html[data-theme="midnight"] {
|
|
261
|
+
--cream: #0c1022; --bg: #161c34; --bg2: #121729; --ink: #e6eaf6; --muted: #8b93b4;
|
|
262
|
+
--accent: #7aa2ff; --accent-2: #a9c2ff; --accent-soft: rgba(122,162,255,.18); --border: #26304f;
|
|
263
|
+
--green: #56cc96; --red: #ea7a86; --amber: #e6b657;
|
|
264
|
+
}
|
|
265
|
+
html[data-theme="forest"] {
|
|
266
|
+
--cream: #0e1712; --bg: #16241c; --bg2: #121e17; --ink: #e7f0ea; --muted: #8fa89a;
|
|
267
|
+
--accent: #4fc78a; --accent-2: #86e0b3; --accent-soft: rgba(79,199,138,.18); --border: #223a2d;
|
|
268
|
+
--green: #57cf90; --red: #e37c73; --amber: #e2b656;
|
|
269
|
+
}
|
|
270
|
+
/* Dark themes: soften the paper grid + let the lock screens follow the palette. */
|
|
271
|
+
html[data-theme="dark"] body, html[data-theme="midnight"] body, html[data-theme="forest"] body {
|
|
272
|
+
background-image: linear-gradient(rgba(255,255,255,.02) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.02) 1px, transparent 1px);
|
|
273
|
+
}
|
|
274
|
+
.lockscreen { background:
|
|
275
|
+
radial-gradient(66% 46% at 50% 116%, rgba(59,158,232,.30) 0%, rgba(108,192,255,.14) 38%, transparent 74%),
|
|
276
|
+
radial-gradient(120% 90% at 50% 0%, var(--bg2) 0%, var(--cream) 55%); }
|
|
277
|
+
.topbar { background: color-mix(in srgb, var(--cream) 82%, transparent); }
|
|
278
|
+
|
|
279
|
+
/* ============================ Header gear ============================ */
|
|
280
|
+
.icon-btn { width: 34px; height: 34px; border-radius: 50%; border: 1px solid var(--border); background: var(--bg);
|
|
281
|
+
color: var(--ink); font-size: 15px; cursor: pointer; line-height: 1; transition: transform .3s, border-color .15s, color .15s; }
|
|
282
|
+
.icon-btn:hover { border-color: var(--accent); color: var(--accent); transform: rotate(60deg); }
|
|
283
|
+
|
|
284
|
+
/* ============================ Add-form: centered black Generate ============================ */
|
|
285
|
+
.genrow { display: flex; justify-content: center; margin: 12px 0 4px; }
|
|
286
|
+
.gen-black { background: var(--ink); color: #fff; border: none; border-radius: 999px; padding: 11px 26px;
|
|
287
|
+
font-weight: 700; font-size: 14px; cursor: pointer; transition: background .15s, transform .1s, box-shadow .2s;
|
|
288
|
+
box-shadow: 0 8px 20px rgba(17,17,17,.14); }
|
|
289
|
+
.gen-black:hover { background: #000; box-shadow: 0 10px 26px rgba(17,17,17,.22); }
|
|
290
|
+
.gen-black:active { transform: scale(.97); }
|
|
291
|
+
.center { text-align: center; display: block; }
|
|
292
|
+
.row.end { justify-content: flex-end; }
|
|
293
|
+
|
|
294
|
+
/* ============================ Centered, richer footer ============================ */
|
|
295
|
+
.foot { flex-direction: column; align-items: center; text-align: center; gap: 8px; padding: 34px 20px 30px; }
|
|
296
|
+
.foot-brand { font-family: var(--display); font-weight: 800; font-size: 17px; letter-spacing: .02em; }
|
|
297
|
+
.foot-tag { color: var(--muted); font-size: 12px; }
|
|
298
|
+
.foot-badges { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin: 2px 0; }
|
|
299
|
+
.fbadge { font-size: 11px; font-weight: 600; color: var(--ink); background: var(--bg); border: 1px solid var(--border);
|
|
300
|
+
padding: 5px 11px; border-radius: 999px; opacity: .9; }
|
|
301
|
+
.foot-links { display: flex; align-items: center; gap: 8px; font-size: 12px; }
|
|
302
|
+
.foot-links a { color: var(--accent); text-decoration: none; font-weight: 600; }
|
|
303
|
+
.dotsep { opacity: .45; }
|
|
304
|
+
/* The rich page footer belongs to the vault view only — never floating over the
|
|
305
|
+
lock/setup screens (each of those has its own pinned strip). */
|
|
306
|
+
.foot { display: none; }
|
|
307
|
+
body.vault-open .foot { display: flex; }
|
|
308
|
+
|
|
309
|
+
/* Compact brand strip pinned to the bottom of the lock / setup screens. */
|
|
310
|
+
.lock-foot { position: absolute; left: 0; right: 0; bottom: 22px; display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 0 20px; }
|
|
311
|
+
.lock-foot .lf-badges { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin-bottom: 4px; }
|
|
312
|
+
.lock-foot .lf-badge { font-size: 10.5px; font-weight: 600; color: var(--ink); background: color-mix(in srgb, var(--bg) 70%, transparent); border: 1px solid var(--border); padding: 4px 9px; border-radius: 999px; }
|
|
313
|
+
.lock-foot .lf-brand { font-family: var(--wordmark); font-weight: 800; font-size: 13px; letter-spacing: -.01em; color: var(--ink); }
|
|
314
|
+
.lock-foot .lf-sub { font-size: 11px; color: var(--muted); }
|
|
315
|
+
.lock-foot a { color: var(--accent); text-decoration: none; font-weight: 600; }
|
|
316
|
+
|
|
317
|
+
/* ============================ Settings ============================ */
|
|
318
|
+
.settings-card { width: min(460px, 96vw); text-align: left; }
|
|
319
|
+
.settings-title { font-size: 24px; margin: 0 0 14px; }
|
|
320
|
+
.set-section { padding: 14px 0; border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: 8px; }
|
|
321
|
+
.set-section:first-of-type { border-top: none; }
|
|
322
|
+
.set-label { font-size: 12px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
|
|
323
|
+
.themes { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
324
|
+
.theme-swatch { display: flex; align-items: center; gap: 8px; padding: 7px 12px 7px 8px; border: 1px solid var(--border);
|
|
325
|
+
border-radius: 999px; background: var(--bg2); font-size: 13px; font-weight: 600; color: var(--ink); cursor: pointer; transition: border-color .15s; }
|
|
326
|
+
.theme-swatch:hover { border-color: var(--accent); }
|
|
327
|
+
.theme-swatch.active { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
328
|
+
.sw { width: 20px; height: 20px; border-radius: 6px; border: 1px solid rgba(0,0,0,.1); }
|
|
329
|
+
.sw-cream { background: linear-gradient(135deg, #f3f1ea 60%, #3b9ee8 60%); }
|
|
330
|
+
.sw-dark { background: linear-gradient(135deg, #1e222a 60%, #4fa8ec 60%); }
|
|
331
|
+
.sw-midnight { background: linear-gradient(135deg, #161c34 60%, #7aa2ff 60%); }
|
|
332
|
+
.sw-forest { background: linear-gradient(135deg, #16241c 60%, #4fc78a 60%); }
|
|
333
|
+
.cp-status { min-height: 15px; margin: 2px 0 0; font-weight: 600; }
|
|
334
|
+
.cp-status.ok { color: var(--green); } .cp-status.err { color: var(--red); }
|
|
335
|
+
.set-about { flex-direction: row; align-items: center; justify-content: space-between; }
|
|
336
|
+
.link-btn { background: none; border: none; color: var(--accent); font-weight: 600; font-size: 13px; cursor: pointer; padding: 0; }
|
|
337
|
+
|
|
338
|
+
/* ============================ Onboarding tour ============================ */
|
|
339
|
+
.tour { position: fixed; inset: 0; z-index: 90; }
|
|
340
|
+
.tour.hidden { display: none; }
|
|
341
|
+
.tour-hole { position: absolute; border-radius: 14px; box-shadow: 0 0 0 9999px rgba(10,12,18,.62); transition: all .35s cubic-bezier(.2,.9,.2,1); pointer-events: none; }
|
|
342
|
+
.tour-pop { position: absolute; width: min(320px, 90vw); background: var(--bg); border: 1px solid var(--border);
|
|
343
|
+
border-radius: 16px; padding: 18px; box-shadow: 0 24px 60px rgba(0,0,0,.3); transition: all .35s cubic-bezier(.2,.9,.2,1); }
|
|
344
|
+
.tour-step { font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--accent); }
|
|
345
|
+
.tour-title { font-family: var(--display); font-weight: 700; font-size: 18px; margin: 4px 0 6px; }
|
|
346
|
+
.tour-body { font-size: 13px; color: var(--muted); line-height: 1.5; }
|
|
347
|
+
.tour-nav { display: flex; align-items: center; justify-content: space-between; margin-top: 14px; }
|
|
348
|
+
.tour-progress { font-size: 12px; color: var(--muted); font-family: var(--mono); }
|
|
349
|
+
.tour-btns { display: flex; align-items: center; gap: 10px; }
|
|
350
|
+
.tour-next { padding: 8px 16px; font-size: 13px; }
|
|
351
|
+
|
|
352
|
+
/* Custom emoji icon on an item avatar. */
|
|
353
|
+
.item .avatar.emoji { font-size: 18px; background: var(--bg2); }
|
|
354
|
+
|
|
355
|
+
/* "Install the browser extension" tip (shown only when it isn't detected). */
|
|
356
|
+
.ext-tip { display: flex; align-items: center; gap: 12px; padding: 12px 14px; margin-bottom: 12px;
|
|
357
|
+
background: var(--accent-soft); border: 1px solid var(--accent); border-radius: 14px; animation: fadeIn .3s ease; }
|
|
358
|
+
.ext-tip .et-ico { font-size: 20px; }
|
|
359
|
+
.ext-tip .et-text { flex: 1; font-size: 13px; line-height: 1.4; }
|
|
360
|
+
.ext-tip .et-get { background: var(--ink); color: #fff; text-decoration: none; border-radius: 999px; padding: 7px 14px; font-size: 13px; font-weight: 700; white-space: nowrap; }
|
|
361
|
+
.ext-tip .et-get:hover { background: #000; }
|
|
362
|
+
.ext-tip .et-x { border: none; background: none; color: var(--muted); cursor: pointer; font-size: 15px; }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* RageVault service worker — makes the app installable and loads the shell
|
|
2
|
+
* offline. It never caches vault data or API responses (those need the server
|
|
3
|
+
* and your session), only the static shell. SoyRage Agency — https://soyrage.es/ */
|
|
4
|
+
"use strict";
|
|
5
|
+
const CACHE = "ragevault-shell-v4";
|
|
6
|
+
const SHELL = ["./", "./index.html", "./app.js", "./styles.css", "./icon-192.png", "./manifest.webmanifest", "./fonts/space-grotesk.woff2", "./fonts/archivo.woff2"];
|
|
7
|
+
|
|
8
|
+
self.addEventListener("install", (e) => {
|
|
9
|
+
e.waitUntil(caches.open(CACHE).then((c) => c.addAll(SHELL)).then(() => self.skipWaiting()));
|
|
10
|
+
});
|
|
11
|
+
self.addEventListener("activate", (e) => {
|
|
12
|
+
e.waitUntil(caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))).then(() => self.clients.claim()));
|
|
13
|
+
});
|
|
14
|
+
// The page asks us to activate the waiting version immediately (update flow).
|
|
15
|
+
self.addEventListener("message", (e) => { if (e.data && e.data.type === "SKIP_WAITING") self.skipWaiting(); });
|
|
16
|
+
|
|
17
|
+
self.addEventListener("fetch", (e) => {
|
|
18
|
+
const url = new URL(e.request.url);
|
|
19
|
+
// Never touch the API — always hit the network (and don't cache secrets).
|
|
20
|
+
if (url.pathname.startsWith("/api/")) return;
|
|
21
|
+
// Network-first for the shell (updates arrive immediately), cache as offline
|
|
22
|
+
// fallback. The server is normally reachable over your LAN/VPN.
|
|
23
|
+
e.respondWith(
|
|
24
|
+
fetch(e.request).then((res) => {
|
|
25
|
+
const copy = res.clone();
|
|
26
|
+
caches.open(CACHE).then((c) => c.put(e.request, copy)).catch(() => {});
|
|
27
|
+
return res;
|
|
28
|
+
}).catch(() => caches.match(e.request)),
|
|
29
|
+
);
|
|
30
|
+
});
|