@tsparticles/template-login 4.1.3 → 4.2.0

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +3 -0
  3. package/package.json +4 -3
  4. package/template/angular/package.json +34 -0
  5. package/template/angular/src/app/app.component.css +151 -0
  6. package/template/angular/src/app/app.component.html +32 -0
  7. package/template/angular/src/app/app.component.ts +105 -0
  8. package/template/angular/src/app/app.module.ts +13 -0
  9. package/template/angular-confetti/package.json +36 -0
  10. package/template/angular-confetti/src/app/app.component.css +151 -0
  11. package/template/angular-confetti/src/app/app.component.html +32 -0
  12. package/template/angular-confetti/src/app/app.component.ts +105 -0
  13. package/template/angular-confetti/src/app/app.module.ts +13 -0
  14. package/template/angular-fireworks/package.json +36 -0
  15. package/template/angular-fireworks/src/app/app.component.css +151 -0
  16. package/template/angular-fireworks/src/app/app.component.html +32 -0
  17. package/template/angular-fireworks/src/app/app.component.ts +105 -0
  18. package/template/angular-fireworks/src/app/app.module.ts +13 -0
  19. package/template/astro/package.json +22 -0
  20. package/template/astro/src/pages/index.astro +173 -0
  21. package/template/ember/app/templates/application.hbs +61 -0
  22. package/template/ember/package.json +42 -0
  23. package/template/inferno/package.json +24 -0
  24. package/template/inferno/src/App.css +151 -0
  25. package/template/inferno/src/App.tsx +158 -0
  26. package/template/jquery/package.json +22 -0
  27. package/template/jquery/src/main.ts +110 -0
  28. package/template/jquery/src/style.css +151 -0
  29. package/template/lit/package.json +22 -0
  30. package/template/lit/src/my-app.ts +143 -0
  31. package/template/nextjs/package.json +25 -0
  32. package/template/nextjs/src/app/page.tsx +168 -0
  33. package/template/nextjs/src/app/providers.tsx +13 -0
  34. package/template/nuxt2/package.json +17 -0
  35. package/template/nuxt2/pages/index.vue +303 -0
  36. package/template/nuxt3/app.vue +288 -0
  37. package/template/nuxt3/package.json +21 -0
  38. package/template/nuxt4/app.vue +288 -0
  39. package/template/nuxt4/package.json +21 -0
  40. package/template/preact/package.json +23 -0
  41. package/template/preact/src/App.css +151 -0
  42. package/template/preact/src/App.tsx +140 -0
  43. package/template/qwik/package.json +22 -0
  44. package/template/qwik/src/App.tsx +139 -0
  45. package/template/react/package.json +19 -0
  46. package/template/react/src/App.css +151 -0
  47. package/template/react/src/App.tsx +140 -0
  48. package/template/riot/package.json +24 -0
  49. package/template/riot/src/app.riot +156 -0
  50. package/template/solid/package.json +18 -0
  51. package/template/solid/src/App.tsx +169 -0
  52. package/template/solid/src/index.css +151 -0
  53. package/template/solid/src/main.tsx +9 -0
  54. package/template/stencil/package.json +20 -0
  55. package/template/stencil/src/components/app-home/app-home.tsx +164 -0
  56. package/template/svelte/package.json +20 -0
  57. package/template/svelte/src/App.svelte +325 -0
  58. package/template/svelte/src/main.ts +12 -0
  59. package/template/vanilla/LICENSE +21 -0
  60. package/template/vanilla/src/main.ts +1 -1
  61. package/template/vue2/package.json +24 -0
  62. package/template/vue2/src/App.vue +287 -0
  63. package/template/vue3/package.json +19 -0
  64. package/template/vue3/src/App.vue +286 -0
  65. package/template/vue3/src/main.ts +14 -0
  66. package/template/webcomponents/package.json +21 -0
  67. package/template/webcomponents/src/main.ts +125 -0
  68. package/template/webcomponents/src/style.css +151 -0
@@ -0,0 +1,151 @@
1
+ :root {
2
+ --bg: #0f0f1a;
3
+ --card-bg: rgba(255, 255, 255, 0.05);
4
+ --text: #fff;
5
+ --border: rgba(255, 255, 255, 0.1);
6
+ --input-bg: rgba(255, 255, 255, 0.08);
7
+ --accent: #6c5ce7;
8
+ --accent-hover: #a29bfe;
9
+ --error: #e74c3c;
10
+ }
11
+
12
+ [data-theme="light"] {
13
+ --bg: #f0f0f5;
14
+ --card-bg: rgba(255, 255, 255, 0.8);
15
+ --text: #1a1a2e;
16
+ --border: rgba(0, 0, 0, 0.1);
17
+ --input-bg: rgba(0, 0, 0, 0.05);
18
+ }
19
+
20
+ * {
21
+ box-sizing: border-box;
22
+ }
23
+
24
+ body {
25
+ margin: 0;
26
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
27
+ background: var(--bg);
28
+ color: var(--text);
29
+ transition: background 0.3s, color 0.3s;
30
+ }
31
+
32
+ .auth-container {
33
+ position: absolute;
34
+ top: 50%;
35
+ left: 50%;
36
+ transform: translate(-50%, -50%);
37
+ z-index: 10;
38
+ width: 100%;
39
+ max-width: 400px;
40
+ padding: 1rem;
41
+ }
42
+
43
+ .auth-card {
44
+ background: var(--card-bg);
45
+ backdrop-filter: blur(10px);
46
+ border: 1px solid var(--border);
47
+ border-radius: 16px;
48
+ padding: 2rem;
49
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
50
+ }
51
+
52
+ .auth-header {
53
+ display: flex;
54
+ justify-content: space-between;
55
+ align-items: center;
56
+ margin-bottom: 1.5rem;
57
+ }
58
+
59
+ .auth-header h1 {
60
+ margin: 0;
61
+ font-size: 1.8em;
62
+ }
63
+
64
+ .theme-btn {
65
+ background: none;
66
+ border: 1px solid var(--border);
67
+ border-radius: 8px;
68
+ padding: 0.4em 0.6em;
69
+ font-size: 1.2em;
70
+ cursor: pointer;
71
+ transition: background 0.2s;
72
+ }
73
+
74
+ .theme-btn:hover {
75
+ background: var(--input-bg);
76
+ }
77
+
78
+ .form-group {
79
+ margin-bottom: 1rem;
80
+ }
81
+
82
+ label {
83
+ display: block;
84
+ margin-bottom: 0.3em;
85
+ font-size: 0.9em;
86
+ opacity: 0.8;
87
+ }
88
+
89
+ input {
90
+ width: 100%;
91
+ padding: 0.7em 1em;
92
+ font-size: 1em;
93
+ border: 1px solid var(--border);
94
+ border-radius: 8px;
95
+ background: var(--input-bg);
96
+ color: var(--text);
97
+ outline: none;
98
+ transition: border-color 0.2s;
99
+ }
100
+
101
+ input:focus {
102
+ border-color: var(--accent);
103
+ }
104
+
105
+ input::placeholder {
106
+ color: var(--text);
107
+ opacity: 0.4;
108
+ }
109
+
110
+ .error {
111
+ display: block;
112
+ margin-top: 0.3em;
113
+ font-size: 0.8em;
114
+ color: var(--error);
115
+ min-height: 1em;
116
+ }
117
+
118
+ button[type="submit"] {
119
+ width: 100%;
120
+ padding: 0.8em;
121
+ font-size: 1em;
122
+ font-weight: 600;
123
+ border: none;
124
+ border-radius: 8px;
125
+ background: var(--accent);
126
+ color: #fff;
127
+ cursor: pointer;
128
+ transition: background 0.2s;
129
+ margin-top: 0.5rem;
130
+ }
131
+
132
+ button[type="submit"]:hover {
133
+ background: var(--accent-hover);
134
+ }
135
+
136
+ .toggle-text {
137
+ text-align: center;
138
+ margin-top: 1.5rem;
139
+ font-size: 0.9em;
140
+ opacity: 0.7;
141
+ }
142
+
143
+ .toggle-text a {
144
+ color: var(--accent);
145
+ text-decoration: none;
146
+ margin-left: 0.3em;
147
+ }
148
+
149
+ .toggle-text a:hover {
150
+ text-decoration: underline;
151
+ }
@@ -0,0 +1,140 @@
1
+ import { useState, useEffect } from "preact/hooks";
2
+ import Particles, { initParticlesEngine } from "@tsparticles/preact";
3
+ import { loadSlim } from "@tsparticles/slim";
4
+ import type { ISourceOptions } from "@tsparticles/engine";
5
+ import "./App.css";
6
+
7
+ const options: ISourceOptions = {
8
+ background: { color: { value: "transparent" } },
9
+ fpsLimit: 60,
10
+ particles: {
11
+ number: { value: 60, density: { enable: true } },
12
+ color: { value: ["#6c5ce7", "#a29bfe", "#fd79a8", "#00cec9"] },
13
+ shape: { type: "circle" },
14
+ opacity: { value: 0.5, random: true },
15
+ size: { value: { min: 1, max: 3 }, random: true },
16
+ move: { enable: true, speed: 1, direction: "none", random: true, straight: false, outModes: { default: "out" } },
17
+ },
18
+ interactivity: {
19
+ events: { onHover: { enable: true, mode: "bubble" } },
20
+ modes: { bubble: { distance: 200, size: 6, duration: 2, opacity: 0.8 } },
21
+ },
22
+ detectRetina: true,
23
+ };
24
+
25
+ function validateEmail(email: string): boolean {
26
+ return /^[^\s@]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/.test(email);
27
+ }
28
+
29
+ export default function App() {
30
+ const [theme, setTheme] = useState(() => localStorage.getItem("theme") || "dark");
31
+ const [isLogin, setIsLogin] = useState(true);
32
+ const [email, setEmail] = useState("");
33
+ const [password, setPassword] = useState("");
34
+ const [confirm, setConfirm] = useState("");
35
+ const [emailErr, setEmailErr] = useState("");
36
+ const [passErr, setPassErr] = useState("");
37
+ const [confirmErr, setConfirmErr] = useState("");
38
+
39
+ useEffect(() => {
40
+ document.documentElement.setAttribute("data-theme", theme);
41
+ localStorage.setItem("theme", theme);
42
+ }, [theme]);
43
+
44
+ useEffect(() => {
45
+ void initParticlesEngine(async (engine) => {
46
+ await loadSlim(engine);
47
+ });
48
+ }, []);
49
+
50
+ function toggleTheme() {
51
+ setTheme((t) => (t === "dark" ? "light" : "dark"));
52
+ }
53
+
54
+ function handleSubmit(e: Event) {
55
+ e.preventDefault();
56
+ let valid = true;
57
+
58
+ if (!email || !validateEmail(email)) {
59
+ setEmailErr("Please enter a valid email address");
60
+ valid = false;
61
+ } else {
62
+ setEmailErr("");
63
+ }
64
+
65
+ if (!password || password.length < 6) {
66
+ setPassErr("Password must be at least 6 characters");
67
+ valid = false;
68
+ } else {
69
+ setPassErr("");
70
+ }
71
+
72
+ if (!isLogin) {
73
+ if (password !== confirm) {
74
+ setConfirmErr("Passwords do not match");
75
+ valid = false;
76
+ } else {
77
+ setConfirmErr("");
78
+ }
79
+ }
80
+
81
+ if (!valid) return;
82
+
83
+ if (isLogin) {
84
+ alert("Logged in successfully! (demo)");
85
+ } else {
86
+ alert("Account created successfully! (demo)");
87
+ setIsLogin(true);
88
+ }
89
+
90
+ setEmail("");
91
+ setPassword("");
92
+ setConfirm("");
93
+ }
94
+
95
+ function toggleMode(e: Event) {
96
+ e.preventDefault();
97
+ setIsLogin((m) => !m);
98
+ setEmailErr("");
99
+ setPassErr("");
100
+ setConfirmErr("");
101
+ }
102
+
103
+ return (
104
+ <div>
105
+ <div class="auth-container">
106
+ <div class="auth-card">
107
+ <div class="auth-header">
108
+ <h1>{isLogin ? "Login" : "Register"}</h1>
109
+ <button class="theme-btn" onClick={toggleTheme} aria-label="Toggle theme">{theme === "dark" ? "🌙" : "☀️"}</button>
110
+ </div>
111
+ <form onSubmit={handleSubmit} novalidate>
112
+ <div class="form-group">
113
+ <label for="email">Email</label>
114
+ <input id="email" type="email" placeholder="you@example.com" value={email} onInput={(e) => setEmail(e.currentTarget.value)} required />
115
+ <span class="error">{emailErr}</span>
116
+ </div>
117
+ <div class="form-group">
118
+ <label for="password">Password</label>
119
+ <input id="password" type="password" placeholder="Enter password" value={password} onInput={(e) => setPassword(e.currentTarget.value)} required />
120
+ <span class="error">{passErr}</span>
121
+ </div>
122
+ {!isLogin && (
123
+ <div class="form-group">
124
+ <label for="confirmPassword">Confirm Password</label>
125
+ <input id="confirmPassword" type="password" placeholder="Confirm password" value={confirm} onInput={(e) => setConfirm(e.currentTarget.value)} />
126
+ <span class="error">{confirmErr}</span>
127
+ </div>
128
+ )}
129
+ <button type="submit">{isLogin ? "Sign In" : "Create Account"}</button>
130
+ </form>
131
+ <p class="toggle-text">
132
+ <span>{isLogin ? "Don't have an account?" : "Already have an account?"}</span>
133
+ <a href="#" onClick={toggleMode}>{isLogin ? "Register" : "Login"}</a>
134
+ </p>
135
+ </div>
136
+ </div>
137
+ <Particles id="tsparticles" options={options} />
138
+ </div>
139
+ );
140
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@builder.io/qwik": "^1.12.0",
13
+ "@tsparticles/qwik": "^4.1.3",
14
+ "@tsparticles/slim": "^4.1.3",
15
+ "@tsparticles/configs": "^4.1.3",
16
+ "@tsparticles/engine": "^4.1.3"
17
+ },
18
+ "devDependencies": {
19
+ "typescript": "^5.7.2",
20
+ "vite": "^6.0.0"
21
+ }
22
+ }
@@ -0,0 +1,139 @@
1
+ import { component$, useVisibleTask$, useSignal, $ } from "@builder.io/qwik";
2
+ import { Particles, initParticlesEngine } from "@tsparticles/qwik";
3
+ import { loadSlim } from "@tsparticles/slim";
4
+ import type { ISourceOptions } from "@tsparticles/engine";
5
+
6
+ function validateEmail(val: string): boolean {
7
+ return /^[^\s@]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/.test(val);
8
+ }
9
+
10
+ const options: ISourceOptions = {
11
+ background: { color: { value: "transparent" } },
12
+ fpsLimit: 60,
13
+ particles: {
14
+ number: { value: 60, density: { enable: true } },
15
+ color: { value: ["#6c5ce7", "#a29bfe", "#fd79a8", "#00cec9"] },
16
+ shape: { type: "circle" },
17
+ opacity: { value: 0.5, random: true },
18
+ size: { value: { min: 1, max: 3 }, random: true },
19
+ move: { enable: true, speed: 1, direction: "none", random: true, straight: false, outModes: { default: "out" } },
20
+ },
21
+ interactivity: {
22
+ events: { onHover: { enable: true, mode: "bubble" } },
23
+ modes: { bubble: { distance: 200, size: 6, duration: 2, opacity: 0.8 } },
24
+ },
25
+ detectRetina: true,
26
+ };
27
+
28
+ export default component$(() => {
29
+ const isLogin = useSignal(true);
30
+ const theme = useSignal("dark");
31
+ const email = useSignal("");
32
+ const password = useSignal("");
33
+ const confirm = useSignal("");
34
+ const emailErr = useSignal("");
35
+ const passErr = useSignal("");
36
+ const confirmErr = useSignal("");
37
+
38
+ // eslint-disable-next-line qwik/no-use-visible-task
39
+ useVisibleTask$(() => {
40
+ void initParticlesEngine(async (engine) => {
41
+ await loadSlim(engine);
42
+ });
43
+ theme.value = localStorage.getItem("theme") || "dark";
44
+ document.documentElement.setAttribute("data-theme", theme.value);
45
+ });
46
+
47
+ const toggleTheme = $(() => {
48
+ theme.value = theme.value === "dark" ? "light" : "dark";
49
+ document.documentElement.setAttribute("data-theme", theme.value);
50
+ localStorage.setItem("theme", theme.value);
51
+ });
52
+
53
+ const handleSubmit = $((e: Event) => {
54
+ e.preventDefault();
55
+ let valid = true;
56
+
57
+ if (!email.value || !validateEmail(email.value)) {
58
+ emailErr.value = "Please enter a valid email address";
59
+ valid = false;
60
+ } else {
61
+ emailErr.value = "";
62
+ }
63
+
64
+ if (!password.value || password.value.length < 6) {
65
+ passErr.value = "Password must be at least 6 characters";
66
+ valid = false;
67
+ } else {
68
+ passErr.value = "";
69
+ }
70
+
71
+ if (!isLogin.value) {
72
+ if (password.value !== confirm.value) {
73
+ confirmErr.value = "Passwords do not match";
74
+ valid = false;
75
+ } else {
76
+ confirmErr.value = "";
77
+ }
78
+ }
79
+
80
+ if (!valid) return;
81
+
82
+ if (isLogin.value) {
83
+ alert("Logged in successfully! (demo)");
84
+ } else {
85
+ alert("Account created successfully! (demo)");
86
+ isLogin.value = true;
87
+ }
88
+
89
+ email.value = "";
90
+ password.value = "";
91
+ confirm.value = "";
92
+ });
93
+
94
+ const toggleMode = $((e: Event) => {
95
+ e.preventDefault();
96
+ isLogin.value = !isLogin.value;
97
+ emailErr.value = "";
98
+ passErr.value = "";
99
+ confirmErr.value = "";
100
+ });
101
+
102
+ return (
103
+ <div>
104
+ <div class="auth-container">
105
+ <div class="auth-card">
106
+ <div class="auth-header">
107
+ <h1>{isLogin.value ? "Login" : "Register"}</h1>
108
+ <button class="theme-btn" onClick$={toggleTheme} aria-label="Toggle theme">{theme.value === "dark" ? "🌙" : "☀️"}</button>
109
+ </div>
110
+ <form onSubmit$={handleSubmit} novalidate>
111
+ <div class="form-group">
112
+ <label for="email">Email</label>
113
+ <input id="email" type="email" placeholder="you@example.com" value={email.value} onInput$={(e: InputEvent) => { email.value = (e.target as HTMLInputElement).value; }} required />
114
+ <span class="error">{emailErr.value}</span>
115
+ </div>
116
+ <div class="form-group">
117
+ <label for="password">Password</label>
118
+ <input id="password" type="password" placeholder="Enter password" value={password.value} onInput$={(e: InputEvent) => { password.value = (e.target as HTMLInputElement).value; }} required />
119
+ <span class="error">{passErr.value}</span>
120
+ </div>
121
+ {!isLogin.value && (
122
+ <div class="form-group">
123
+ <label for="confirmPassword">Confirm Password</label>
124
+ <input id="confirmPassword" type="password" placeholder="Confirm password" value={confirm.value} onInput$={(e: InputEvent) => { confirm.value = (e.target as HTMLInputElement).value; }} />
125
+ <span class="error">{confirmErr.value}</span>
126
+ </div>
127
+ )}
128
+ <button type="submit">{isLogin.value ? "Sign In" : "Create Account"}</button>
129
+ </form>
130
+ <p class="toggle-text">
131
+ <span>{isLogin.value ? "Don't have an account?" : "Already have an account?"}</span>
132
+ <a href="#" onClick$={toggleMode}>{isLogin.value ? "Register" : "Login"}</a>
133
+ </p>
134
+ </div>
135
+ </div>
136
+ <Particles id="tsparticles" options={options} />
137
+ </div>
138
+ );
139
+ });
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^19.2.5",
13
+ "react-dom": "^19.2.5",
14
+ "@tsparticles/react": "^4.1.3"
15
+ },
16
+ "devDependencies": {
17
+ "@vitejs/plugin-react": "^6.0.1"
18
+ }
19
+ }
@@ -0,0 +1,151 @@
1
+ :root {
2
+ --bg: #0f0f1a;
3
+ --card-bg: rgba(255, 255, 255, 0.05);
4
+ --text: #fff;
5
+ --border: rgba(255, 255, 255, 0.1);
6
+ --input-bg: rgba(255, 255, 255, 0.08);
7
+ --accent: #6c5ce7;
8
+ --accent-hover: #a29bfe;
9
+ --error: #e74c3c;
10
+ }
11
+
12
+ [data-theme="light"] {
13
+ --bg: #f0f0f5;
14
+ --card-bg: rgba(255, 255, 255, 0.8);
15
+ --text: #1a1a2e;
16
+ --border: rgba(0, 0, 0, 0.1);
17
+ --input-bg: rgba(0, 0, 0, 0.05);
18
+ }
19
+
20
+ * {
21
+ box-sizing: border-box;
22
+ }
23
+
24
+ body {
25
+ margin: 0;
26
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
27
+ background: var(--bg);
28
+ color: var(--text);
29
+ transition: background 0.3s, color 0.3s;
30
+ }
31
+
32
+ .auth-container {
33
+ position: absolute;
34
+ top: 50%;
35
+ left: 50%;
36
+ transform: translate(-50%, -50%);
37
+ z-index: 10;
38
+ width: 100%;
39
+ max-width: 400px;
40
+ padding: 1rem;
41
+ }
42
+
43
+ .auth-card {
44
+ background: var(--card-bg);
45
+ backdrop-filter: blur(10px);
46
+ border: 1px solid var(--border);
47
+ border-radius: 16px;
48
+ padding: 2rem;
49
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
50
+ }
51
+
52
+ .auth-header {
53
+ display: flex;
54
+ justify-content: space-between;
55
+ align-items: center;
56
+ margin-bottom: 1.5rem;
57
+ }
58
+
59
+ .auth-header h1 {
60
+ margin: 0;
61
+ font-size: 1.8em;
62
+ }
63
+
64
+ .theme-btn {
65
+ background: none;
66
+ border: 1px solid var(--border);
67
+ border-radius: 8px;
68
+ padding: 0.4em 0.6em;
69
+ font-size: 1.2em;
70
+ cursor: pointer;
71
+ transition: background 0.2s;
72
+ }
73
+
74
+ .theme-btn:hover {
75
+ background: var(--input-bg);
76
+ }
77
+
78
+ .form-group {
79
+ margin-bottom: 1rem;
80
+ }
81
+
82
+ label {
83
+ display: block;
84
+ margin-bottom: 0.3em;
85
+ font-size: 0.9em;
86
+ opacity: 0.8;
87
+ }
88
+
89
+ input {
90
+ width: 100%;
91
+ padding: 0.7em 1em;
92
+ font-size: 1em;
93
+ border: 1px solid var(--border);
94
+ border-radius: 8px;
95
+ background: var(--input-bg);
96
+ color: var(--text);
97
+ outline: none;
98
+ transition: border-color 0.2s;
99
+ }
100
+
101
+ input:focus {
102
+ border-color: var(--accent);
103
+ }
104
+
105
+ input::placeholder {
106
+ color: var(--text);
107
+ opacity: 0.4;
108
+ }
109
+
110
+ .error {
111
+ display: block;
112
+ margin-top: 0.3em;
113
+ font-size: 0.8em;
114
+ color: var(--error);
115
+ min-height: 1em;
116
+ }
117
+
118
+ button[type="submit"] {
119
+ width: 100%;
120
+ padding: 0.8em;
121
+ font-size: 1em;
122
+ font-weight: 600;
123
+ border: none;
124
+ border-radius: 8px;
125
+ background: var(--accent);
126
+ color: #fff;
127
+ cursor: pointer;
128
+ transition: background 0.2s;
129
+ margin-top: 0.5rem;
130
+ }
131
+
132
+ button[type="submit"]:hover {
133
+ background: var(--accent-hover);
134
+ }
135
+
136
+ .toggle-text {
137
+ text-align: center;
138
+ margin-top: 1.5rem;
139
+ font-size: 0.9em;
140
+ opacity: 0.7;
141
+ }
142
+
143
+ .toggle-text a {
144
+ color: var(--accent);
145
+ text-decoration: none;
146
+ margin-left: 0.3em;
147
+ }
148
+
149
+ .toggle-text a:hover {
150
+ text-decoration: underline;
151
+ }