create-authenik8-app 2.4.14 → 2.4.15

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 (227) hide show
  1. package/README.md +34 -516
  2. package/dist/src/bin/cli.d.ts +3 -0
  3. package/dist/src/bin/cli.d.ts.map +1 -0
  4. package/dist/src/bin/cli.js +112 -0
  5. package/dist/src/bin/cli.js.map +1 -0
  6. package/dist/src/bin/index.js +108 -23
  7. package/dist/src/bin/index.js.map +1 -1
  8. package/dist/src/commands/add/apply.d.ts +6 -0
  9. package/dist/src/commands/add/apply.d.ts.map +1 -0
  10. package/dist/src/commands/add/apply.js +123 -0
  11. package/dist/src/commands/add/apply.js.map +1 -0
  12. package/dist/src/commands/add/ci.d.ts +6 -0
  13. package/dist/src/commands/add/ci.d.ts.map +1 -0
  14. package/dist/src/commands/add/ci.js +108 -0
  15. package/dist/src/commands/add/ci.js.map +1 -0
  16. package/dist/src/commands/add/context.d.ts +5 -0
  17. package/dist/src/commands/add/context.d.ts.map +1 -0
  18. package/dist/src/commands/add/context.js +19 -0
  19. package/dist/src/commands/add/context.js.map +1 -0
  20. package/dist/src/commands/add/diff.d.ts +4 -0
  21. package/dist/src/commands/add/diff.d.ts.map +1 -0
  22. package/dist/src/commands/add/diff.js +114 -0
  23. package/dist/src/commands/add/diff.js.map +1 -0
  24. package/dist/src/commands/add/index.d.ts +6 -0
  25. package/dist/src/commands/add/index.d.ts.map +1 -0
  26. package/dist/src/commands/add/index.js +61 -0
  27. package/dist/src/commands/add/index.js.map +1 -0
  28. package/dist/src/commands/add/output.d.ts +5 -0
  29. package/dist/src/commands/add/output.d.ts.map +1 -0
  30. package/dist/src/commands/add/output.js +58 -0
  31. package/dist/src/commands/add/output.js.map +1 -0
  32. package/dist/src/commands/add/plan.d.ts +6 -0
  33. package/dist/src/commands/add/plan.d.ts.map +1 -0
  34. package/dist/src/commands/add/plan.js +93 -0
  35. package/dist/src/commands/add/plan.js.map +1 -0
  36. package/dist/src/commands/add/registry.d.ts +4 -0
  37. package/dist/src/commands/add/registry.d.ts.map +1 -0
  38. package/dist/src/commands/add/registry.js +37 -0
  39. package/dist/src/commands/add/registry.js.map +1 -0
  40. package/dist/src/commands/add/types.d.ts +37 -0
  41. package/dist/src/commands/add/types.d.ts.map +1 -0
  42. package/dist/src/commands/add/types.js +2 -0
  43. package/dist/src/commands/add/types.js.map +1 -0
  44. package/dist/src/commands/doctor/checks.d.ts +3 -0
  45. package/dist/src/commands/doctor/checks.d.ts.map +1 -0
  46. package/dist/src/commands/doctor/checks.js +353 -0
  47. package/dist/src/commands/doctor/checks.js.map +1 -0
  48. package/dist/src/commands/doctor/context.d.ts +5 -0
  49. package/dist/src/commands/doctor/context.d.ts.map +1 -0
  50. package/dist/src/commands/doctor/context.js +134 -0
  51. package/dist/src/commands/doctor/context.js.map +1 -0
  52. package/dist/src/commands/doctor/index.d.ts +9 -0
  53. package/dist/src/commands/doctor/index.d.ts.map +1 -0
  54. package/dist/src/commands/doctor/index.js +80 -0
  55. package/dist/src/commands/doctor/index.js.map +1 -0
  56. package/dist/src/commands/doctor/output.d.ts +4 -0
  57. package/dist/src/commands/doctor/output.d.ts.map +1 -0
  58. package/dist/src/commands/doctor/output.js +53 -0
  59. package/dist/src/commands/doctor/output.js.map +1 -0
  60. package/dist/src/commands/doctor/postGeneration.d.ts +7 -0
  61. package/dist/src/commands/doctor/postGeneration.d.ts.map +1 -0
  62. package/dist/src/commands/doctor/postGeneration.js +22 -0
  63. package/dist/src/commands/doctor/postGeneration.js.map +1 -0
  64. package/dist/src/commands/doctor/services.d.ts +5 -0
  65. package/dist/src/commands/doctor/services.d.ts.map +1 -0
  66. package/dist/src/commands/doctor/services.js +80 -0
  67. package/dist/src/commands/doctor/services.js.map +1 -0
  68. package/dist/src/commands/doctor/types.d.ts +69 -0
  69. package/dist/src/commands/doctor/types.d.ts.map +1 -0
  70. package/dist/src/commands/doctor/types.js +2 -0
  71. package/dist/src/commands/doctor/types.js.map +1 -0
  72. package/dist/src/commands/upgrade/context.d.ts +5 -0
  73. package/dist/src/commands/upgrade/context.d.ts.map +1 -0
  74. package/dist/src/commands/upgrade/context.js +57 -0
  75. package/dist/src/commands/upgrade/context.js.map +1 -0
  76. package/dist/src/commands/upgrade/index.d.ts +7 -0
  77. package/dist/src/commands/upgrade/index.d.ts.map +1 -0
  78. package/dist/src/commands/upgrade/index.js +35 -0
  79. package/dist/src/commands/upgrade/index.js.map +1 -0
  80. package/dist/src/commands/upgrade/migrations.d.ts +4 -0
  81. package/dist/src/commands/upgrade/migrations.d.ts.map +1 -0
  82. package/dist/src/commands/upgrade/migrations.js +22 -0
  83. package/dist/src/commands/upgrade/migrations.js.map +1 -0
  84. package/dist/src/commands/upgrade/output.d.ts +4 -0
  85. package/dist/src/commands/upgrade/output.d.ts.map +1 -0
  86. package/dist/src/commands/upgrade/output.js +52 -0
  87. package/dist/src/commands/upgrade/output.js.map +1 -0
  88. package/dist/src/commands/upgrade/plan.d.ts +3 -0
  89. package/dist/src/commands/upgrade/plan.d.ts.map +1 -0
  90. package/dist/src/commands/upgrade/plan.js +119 -0
  91. package/dist/src/commands/upgrade/plan.js.map +1 -0
  92. package/dist/src/commands/upgrade/types.d.ts +48 -0
  93. package/dist/src/commands/upgrade/types.d.ts.map +1 -0
  94. package/dist/src/commands/upgrade/types.js +2 -0
  95. package/dist/src/commands/upgrade/types.js.map +1 -0
  96. package/dist/src/lib/args.d.ts +21 -0
  97. package/dist/src/lib/args.d.ts.map +1 -1
  98. package/dist/src/lib/args.js +119 -1
  99. package/dist/src/lib/args.js.map +1 -1
  100. package/dist/src/lib/constants.d.ts +1 -1
  101. package/dist/src/lib/constants.d.ts.map +1 -1
  102. package/dist/src/lib/constants.js +3 -1
  103. package/dist/src/lib/constants.js.map +1 -1
  104. package/dist/src/lib/expressOAuth.d.ts +10 -0
  105. package/dist/src/lib/expressOAuth.d.ts.map +1 -0
  106. package/dist/src/lib/expressOAuth.js +164 -0
  107. package/dist/src/lib/expressOAuth.js.map +1 -0
  108. package/dist/src/lib/interruption.d.ts +14 -0
  109. package/dist/src/lib/interruption.d.ts.map +1 -0
  110. package/dist/src/lib/interruption.js +16 -0
  111. package/dist/src/lib/interruption.js.map +1 -0
  112. package/dist/src/lib/nonInteractive.d.ts +8 -0
  113. package/dist/src/lib/nonInteractive.d.ts.map +1 -0
  114. package/dist/src/lib/nonInteractive.js +111 -0
  115. package/dist/src/lib/nonInteractive.js.map +1 -0
  116. package/dist/src/lib/oauth.d.ts +9 -0
  117. package/dist/src/lib/oauth.d.ts.map +1 -0
  118. package/dist/src/lib/oauth.js +62 -0
  119. package/dist/src/lib/oauth.js.map +1 -0
  120. package/dist/src/lib/onboarding.d.ts +8 -0
  121. package/dist/src/lib/onboarding.d.ts.map +1 -0
  122. package/dist/src/lib/onboarding.js +35 -0
  123. package/dist/src/lib/onboarding.js.map +1 -0
  124. package/dist/src/lib/packageManagerCommands.d.ts +11 -0
  125. package/dist/src/lib/packageManagerCommands.d.ts.map +1 -0
  126. package/dist/src/lib/packageManagerCommands.js +34 -0
  127. package/dist/src/lib/packageManagerCommands.js.map +1 -0
  128. package/dist/src/lib/process.d.ts.map +1 -1
  129. package/dist/src/lib/process.js +9 -0
  130. package/dist/src/lib/process.js.map +1 -1
  131. package/dist/src/lib/projectManifest.d.ts +67 -0
  132. package/dist/src/lib/projectManifest.d.ts.map +1 -0
  133. package/dist/src/lib/projectManifest.js +134 -0
  134. package/dist/src/lib/projectManifest.js.map +1 -0
  135. package/dist/src/lib/release.d.ts +9 -0
  136. package/dist/src/lib/release.d.ts.map +1 -0
  137. package/dist/src/lib/release.js +48 -0
  138. package/dist/src/lib/release.js.map +1 -0
  139. package/dist/src/lib/rootCommand.d.ts +15 -0
  140. package/dist/src/lib/rootCommand.d.ts.map +1 -0
  141. package/dist/src/lib/rootCommand.js +17 -0
  142. package/dist/src/lib/rootCommand.js.map +1 -0
  143. package/dist/src/lib/schemas.d.ts +15 -6
  144. package/dist/src/lib/schemas.d.ts.map +1 -1
  145. package/dist/src/lib/schemas.js +8 -1
  146. package/dist/src/lib/schemas.js.map +1 -1
  147. package/dist/src/lib/semver.d.ts +10 -0
  148. package/dist/src/lib/semver.d.ts.map +1 -0
  149. package/dist/src/lib/semver.js +51 -0
  150. package/dist/src/lib/semver.js.map +1 -0
  151. package/dist/src/lib/types.d.ts +5 -1
  152. package/dist/src/lib/types.d.ts.map +1 -1
  153. package/dist/src/steps/configurePrisma.d.ts.map +1 -1
  154. package/dist/src/steps/configurePrisma.js +18 -23
  155. package/dist/src/steps/configurePrisma.js.map +1 -1
  156. package/dist/src/steps/configureReadme.d.ts +4 -0
  157. package/dist/src/steps/configureReadme.d.ts.map +1 -0
  158. package/dist/src/steps/configureReadme.js +22 -0
  159. package/dist/src/steps/configureReadme.js.map +1 -0
  160. package/dist/src/steps/createProject.d.ts.map +1 -1
  161. package/dist/src/steps/createProject.js +16 -170
  162. package/dist/src/steps/createProject.js.map +1 -1
  163. package/dist/src/steps/finalSetup.d.ts +3 -2
  164. package/dist/src/steps/finalSetup.d.ts.map +1 -1
  165. package/dist/src/steps/finalSetup.js +14 -9
  166. package/dist/src/steps/finalSetup.js.map +1 -1
  167. package/dist/src/steps/installDeps.d.ts +1 -0
  168. package/dist/src/steps/installDeps.d.ts.map +1 -1
  169. package/dist/src/steps/installDeps.js +11 -0
  170. package/dist/src/steps/installDeps.js.map +1 -1
  171. package/dist/src/steps/prompts.d.ts.map +1 -1
  172. package/dist/src/steps/prompts.js +32 -13
  173. package/dist/src/steps/prompts.js.map +1 -1
  174. package/dist/src/utils/output.d.ts.map +1 -1
  175. package/dist/src/utils/output.js +23 -11
  176. package/dist/src/utils/output.js.map +1 -1
  177. package/package.json +5 -4
  178. package/templates/express-auth/README.md +21 -12
  179. package/templates/express-auth/package.json +11 -7
  180. package/templates/express-auth/src/config/redis.ts +109 -0
  181. package/templates/express-auth/src/server.ts +2 -0
  182. package/templates/express-auth+/README.md +25 -18
  183. package/templates/express-auth+/package.json +11 -7
  184. package/templates/express-auth+/src/auth/auth.ts +2 -0
  185. package/templates/express-auth+/src/auth/controllers/password.controller.ts +12 -1
  186. package/templates/express-auth+/src/auth/routes/password.route.ts +1 -0
  187. package/templates/express-auth+/src/config/redis.ts +109 -0
  188. package/templates/express-auth+/src/utils/security.ts +12 -0
  189. package/templates/express-base/README.md +21 -12
  190. package/templates/express-base/package.json +11 -8
  191. package/templates/express-base/src/.env.example +1 -1
  192. package/templates/express-base/src/config/redis.ts +109 -0
  193. package/templates/express-base/src/server.ts +2 -0
  194. package/templates/fullstack/.env.example +3 -2
  195. package/templates/fullstack/PRESET_CONTRACT.md +7 -3
  196. package/templates/fullstack/README.md +45 -7
  197. package/templates/fullstack/apps/api/package.json +6 -4
  198. package/templates/fullstack/apps/api/prisma/migrations/20260721000000_init/migration.sql +163 -0
  199. package/templates/fullstack/apps/api/prisma/migrations/migration_lock.toml +3 -0
  200. package/templates/fullstack/apps/api/src/auth/authenik8.ts +13 -4
  201. package/templates/fullstack/apps/api/src/config/env.ts +9 -1
  202. package/templates/fullstack/apps/api/src/config/logger.ts +21 -11
  203. package/templates/fullstack/apps/api/tests/logger.test.ts +43 -0
  204. package/templates/fullstack/apps/web/package.json +3 -1
  205. package/templates/fullstack/apps/web/src/App.tsx +1 -1
  206. package/templates/fullstack/apps/web/src/auth/RouteGuards.tsx +1 -1
  207. package/templates/fullstack/apps/web/src/components/AppShell.tsx +90 -45
  208. package/templates/fullstack/apps/web/src/components/AuthShell.tsx +10 -7
  209. package/templates/fullstack/apps/web/src/components/Page.tsx +6 -3
  210. package/templates/fullstack/apps/web/src/features/projects/ProjectPages.tsx +1 -1
  211. package/templates/fullstack/apps/web/src/main.tsx +13 -6
  212. package/templates/fullstack/apps/web/src/pages/DashboardPage.tsx +163 -23
  213. package/templates/fullstack/apps/web/src/pages/StatusPages.tsx +1 -1
  214. package/templates/fullstack/apps/web/src/pages/auth/LoginPage.tsx +93 -17
  215. package/templates/fullstack/apps/web/src/pages/auth/OAuthCallbackPage.tsx +1 -1
  216. package/templates/fullstack/apps/web/src/pages/auth/RecoveryPages.tsx +1 -1
  217. package/templates/fullstack/apps/web/src/pages/auth/RegisterPage.tsx +1 -1
  218. package/templates/fullstack/apps/web/src/pages/auth/VerifyEmailPage.tsx +1 -1
  219. package/templates/fullstack/apps/web/src/styles.css +119 -43
  220. package/templates/fullstack/apps/web/vite.config.ts +12 -0
  221. package/templates/fullstack/docker-compose.yml +2 -2
  222. package/templates/fullstack/gitignore.template +1 -0
  223. package/templates/fullstack/package.json +17 -8
  224. package/templates/fullstack/scripts/run-local.mjs +157 -0
  225. package/templates/prisma/postgresql/.env.example +1 -2
  226. package/templates/prisma/sqlite/.env.example +1 -2
  227. package/templates/prisma/sqlite/client.ts +2 -2
@@ -1,48 +1,188 @@
1
+ import type { ReactNode } from "react";
1
2
  import { useQuery } from "@tanstack/react-query";
2
- import { Activity, ArrowRight, CheckCircle2, FolderKanban, MonitorSmartphone, ShieldCheck } from "lucide-react";
3
- import { Link } from "react-router-dom";
4
- import { Badge, Card, EmptyState, Spinner } from "@authenik8/ui";
3
+ import {
4
+ Activity,
5
+ ArrowRight,
6
+ CheckCircle2,
7
+ FolderKanban,
8
+ MonitorSmartphone,
9
+ Plus,
10
+ ShieldCheck,
11
+ } from "lucide-react";
12
+ import { Badge } from "@astryxdesign/core/Badge";
13
+ import { Button } from "@astryxdesign/core/Button";
14
+ import { Card } from "@astryxdesign/core/Card";
15
+ import { StatusDot } from "@astryxdesign/core/StatusDot";
16
+ import { Link, useNavigate } from "react-router";
17
+ import { EmptyState, Spinner } from "@authenik8/ui";
5
18
  import { accountApi, healthApi, projectApi } from "@authenik8/api-client";
6
- import { PageHeader, ErrorNotice } from "../components/Page";
19
+ import { ErrorNotice } from "../components/Page";
7
20
  import { useAuth } from "../auth/AuthProvider";
8
21
 
9
22
  export function DashboardPage() {
10
23
  const { user } = useAuth();
24
+ const navigate = useNavigate();
11
25
  const projects = useQuery({ queryKey: ["projects"], queryFn: projectApi.list });
12
26
  const sessions = useQuery({ queryKey: ["sessions"], queryFn: accountApi.sessions });
13
27
  const health = useQuery({ queryKey: ["health"], queryFn: healthApi.status, refetchInterval: 60_000 });
28
+ const firstName = user?.name.trim().split(/\s+/)[0] || "there";
29
+ const projectCount = projects.data?.projects.length;
30
+ const sessionCount = sessions.data?.sessions.length;
31
+ const healthLabel = health.isPending
32
+ ? "Checking"
33
+ : health.isError
34
+ ? "Unavailable"
35
+ : health.data?.status ?? "Unknown";
14
36
 
15
37
  return (
16
- <>
17
- <PageHeader title={`Good to see you, ${user?.name.split(" ")[0]}`} description="Your workspace and identity services at a glance." />
38
+ <div className="dashboard-page">
39
+ <header className="dashboard-hero">
40
+ <div className="hero-copy">
41
+ <div className="eyebrow">
42
+ <StatusDot
43
+ variant={health.isError ? "error" : health.isPending ? "warning" : "success"}
44
+ label={`API status: ${healthLabel}`}
45
+ isPulsing={health.isPending}
46
+ />
47
+ <span>Protected workspace</span>
48
+ </div>
49
+ <h1>Good to see you, {firstName}</h1>
50
+ <p>Your projects, active identity sessions, and account security in one place.</p>
51
+ </div>
52
+ <div className="hero-actions">
53
+ <Button
54
+ label="Security settings"
55
+ variant="secondary"
56
+ onClick={() => navigate("/settings/security")}
57
+ />
58
+ <Button
59
+ label="New project"
60
+ variant="primary"
61
+ icon={<Plus size={17} />}
62
+ onClick={() => navigate("/projects/new")}
63
+ />
64
+ </div>
65
+ </header>
66
+
18
67
  <div className="stat-grid">
19
- <Card className="stat-card"><span className="stat-icon cyan"><FolderKanban size={19} /></span><div><span>Projects</span><strong>{projects.data?.projects.length ?? "-"}</strong></div></Card>
20
- <Card className="stat-card"><span className="stat-icon violet"><MonitorSmartphone size={19} /></span><div><span>Active sessions</span><strong>{sessions.data?.sessions.length ?? "-"}</strong></div></Card>
21
- <Card className="stat-card"><span className="stat-icon green"><ShieldCheck size={19} /></span><div><span>Email security</span><strong>{user?.verified ? "Verified" : "Pending"}</strong></div></Card>
22
- <Card className="stat-card"><span className="stat-icon amber"><Activity size={19} /></span><div><span>API status</span><strong>{health.data?.status ?? "Checking"}</strong></div></Card>
68
+ <MetricCard
69
+ icon={<FolderKanban size={20} />}
70
+ tone="cyan"
71
+ label="Projects"
72
+ value={projectCount ?? "-"}
73
+ detail={projectCount === 1 ? "Owned resource" : "Owned resources"}
74
+ />
75
+ <MetricCard
76
+ icon={<MonitorSmartphone size={20} />}
77
+ tone="violet"
78
+ label="Active sessions"
79
+ value={sessionCount ?? "-"}
80
+ detail="Across your devices"
81
+ />
82
+ <MetricCard
83
+ icon={<ShieldCheck size={20} />}
84
+ tone="green"
85
+ label="Email security"
86
+ value={user?.verified ? "Verified" : "Pending"}
87
+ detail={user?.verified ? "Identity confirmed" : "Action required"}
88
+ />
89
+ <MetricCard
90
+ icon={<Activity size={20} />}
91
+ tone="amber"
92
+ label="API status"
93
+ value={healthLabel}
94
+ detail="Checked every minute"
95
+ />
23
96
  </div>
24
97
 
25
98
  <div className="dashboard-grid">
26
- <section className="panel">
27
- <div className="panel-heading"><div><h2>Recent projects</h2><p>Latest workspace activity</p></div><Link className="text-link" to="/projects">View all <ArrowRight size={15} /></Link></div>
28
- {projects.isPending ? <div className="panel-loading"><Spinner /></div> : projects.error ? <ErrorNotice error={projects.error} /> : projects.data!.projects.length === 0 ? (
29
- <EmptyState icon={<FolderKanban />} title="No projects yet" action={<Link className="button button-primary" to="/projects/new">Create project</Link>}>Create the first owned resource in this workspace.</EmptyState>
99
+ <Card padding={0} className="dashboard-panel">
100
+ <div className="panel-heading">
101
+ <div><h2>Recent projects</h2><p>Your latest workspace activity</p></div>
102
+ <Link className="text-link" to="/projects">View all <ArrowRight size={15} /></Link>
103
+ </div>
104
+ {projects.isPending ? (
105
+ <div className="panel-loading"><Spinner /></div>
106
+ ) : projects.error ? (
107
+ <div className="panel-error"><ErrorNotice error={projects.error} /></div>
108
+ ) : projects.data!.projects.length === 0 ? (
109
+ <EmptyState
110
+ icon={<FolderKanban />}
111
+ title="No projects yet"
112
+ action={<Link className="button button-primary" to="/projects/new">Create project</Link>}
113
+ >
114
+ Create the first owned resource in this workspace.
115
+ </EmptyState>
30
116
  ) : (
31
- <div className="table-wrap"><table><thead><tr><th>Name</th><th>Status</th><th>Updated</th></tr></thead><tbody>
32
- {projects.data!.projects.slice(0, 5).map((project) => <tr key={project.id}><td><Link className="table-link" to={`/projects/${project.id}`}>{project.name}</Link></td><td><Badge tone={project.status === "ACTIVE" ? "success" : project.status === "ARCHIVED" ? "neutral" : "warning"}>{project.status}</Badge></td><td>{new Date(project.updatedAt).toLocaleDateString()}</td></tr>)}
33
- </tbody></table></div>
117
+ <div className="table-wrap">
118
+ <table>
119
+ <caption className="sr-only">Recently updated projects</caption>
120
+ <thead><tr><th>Name</th><th>Status</th><th>Updated</th></tr></thead>
121
+ <tbody>
122
+ {projects.data!.projects.slice(0, 5).map((project) => (
123
+ <tr key={project.id}>
124
+ <td><Link className="table-link" to={`/projects/${project.id}`}>{project.name}</Link></td>
125
+ <td><Badge variant={projectStatusVariant(project.status)} label={formatLabel(project.status)} /></td>
126
+ <td>{new Date(project.updatedAt).toLocaleDateString()}</td>
127
+ </tr>
128
+ ))}
129
+ </tbody>
130
+ </table>
131
+ </div>
34
132
  )}
35
- </section>
36
- <aside className="panel security-panel">
37
- <div className="panel-heading"><div><h2>Security posture</h2><p>Account checks</p></div></div>
133
+ </Card>
134
+
135
+ <Card padding={0} className="dashboard-panel security-panel">
136
+ <div className="panel-heading">
137
+ <div><h2>Security posture</h2><p>Live account checks</p></div>
138
+ <Badge variant={user?.verified ? "success" : "warning"} label={user?.verified ? "Protected" : "Review"} />
139
+ </div>
38
140
  <ul className="check-list">
39
141
  <li><CheckCircle2 /><span><strong>Rotating session</strong><small>Refresh cookie protected</small></span></li>
40
142
  <li className={user?.verified ? "" : "pending"}><CheckCircle2 /><span><strong>Email verification</strong><small>{user?.verified ? "Identity confirmed" : "Action required"}</small></span></li>
41
143
  <li><CheckCircle2 /><span><strong>API authorization</strong><small>Role and owner policies active</small></span></li>
42
144
  </ul>
43
- <Link className="button button-secondary full-width" to="/settings/security">Review account security</Link>
44
- </aside>
145
+ <div className="security-action">
146
+ <Button
147
+ label="Review account security"
148
+ variant="secondary"
149
+ width="100%"
150
+ onClick={() => navigate("/settings/security")}
151
+ />
152
+ </div>
153
+ </Card>
45
154
  </div>
46
- </>
155
+ </div>
47
156
  );
48
157
  }
158
+
159
+ interface MetricCardProps {
160
+ icon: ReactNode;
161
+ tone: "cyan" | "violet" | "green" | "amber";
162
+ label: string;
163
+ value: string | number;
164
+ detail: string;
165
+ }
166
+
167
+ function MetricCard({ icon, tone, label, value, detail }: MetricCardProps) {
168
+ return (
169
+ <Card padding={0} className="stat-card">
170
+ <span className={`stat-icon ${tone}`}>{icon}</span>
171
+ <div className="stat-copy">
172
+ <span>{label}</span>
173
+ <strong>{value}</strong>
174
+ <small>{detail}</small>
175
+ </div>
176
+ </Card>
177
+ );
178
+ }
179
+
180
+ function projectStatusVariant(status: string): "success" | "neutral" | "warning" {
181
+ if (status === "ACTIVE") return "success";
182
+ if (status === "ARCHIVED") return "neutral";
183
+ return "warning";
184
+ }
185
+
186
+ function formatLabel(value: string) {
187
+ return value.toLowerCase().replaceAll("_", " ");
188
+ }
@@ -1,5 +1,5 @@
1
1
  import { ArrowLeft, LockKeyhole, SearchX } from "lucide-react";
2
- import { Link } from "react-router-dom";
2
+ import { Link } from "react-router";
3
3
 
4
4
  function StatusPage({ icon, title, message }: { icon: React.ReactNode; title: string; message: string }) {
5
5
  return <main className="status-page"><div className="status-icon">{icon}</div><h1>{title}</h1><p>{message}</p><Link className="button button-primary" to="/"><ArrowLeft size={17} /> Return to dashboard</Link></main>;
@@ -1,12 +1,27 @@
1
- import { useState, type FormEvent } from "react";
2
- import { Github, KeyRound } from "lucide-react";
3
- import { Link, useLocation, useNavigate, useSearchParams } from "react-router-dom";
4
- import { Button, Input } from "@authenik8/ui";
1
+ import {
2
+ useState,
3
+ type ComponentProps,
4
+ type ComponentType,
5
+ type FormEvent,
6
+ type InputHTMLAttributes,
7
+ } from "react";
8
+ import { Github, KeyRound, LockKeyhole, Mail } from "lucide-react";
9
+ import { Banner } from "@astryxdesign/core/Banner";
10
+ import { Button } from "@astryxdesign/core/Button";
11
+ import { TextInput } from "@astryxdesign/core/TextInput";
12
+ import { Link, useLocation, useNavigate, useSearchParams } from "react-router";
5
13
  import { AuthShell } from "../../components/AuthShell";
6
- import { ErrorNotice, Field } from "../../components/Page";
14
+ import { getErrorMessage } from "../../components/Page";
7
15
  import { useAuth } from "../../auth/AuthProvider";
8
16
  import { enabledOAuthProviders } from "../../auth/providers";
9
17
 
18
+ type CredentialInputProps = ComponentProps<typeof TextInput> &
19
+ Pick<InputHTMLAttributes<HTMLInputElement>, "autoComplete">;
20
+
21
+ // Astryx 0.1.7 forwards native input attributes at runtime but does not yet
22
+ // declare autoComplete. Keep that browser and password-manager contract here.
23
+ const CredentialInput = TextInput as ComponentType<CredentialInputProps>;
24
+
10
25
  export function LoginPage() {
11
26
  const { login } = useAuth();
12
27
  const navigate = useNavigate();
@@ -33,19 +48,80 @@ export function LoginPage() {
33
48
  }
34
49
 
35
50
  return (
36
- <AuthShell title="Welcome back" subtitle="Sign in to your workspace" footer={<>New to Authenik8? <Link to="/register">Create an account</Link></>}>
37
- {params.has("oauthError") && <ErrorNotice error={new Error("OAuth sign-in could not be completed")} />}
38
- {error !== undefined && <ErrorNotice error={error} />}
39
- <form className="form-stack" onSubmit={submit}>
40
- <Field label="Email address" htmlFor="email"><Input id="email" type="email" autoComplete="email" value={email} onChange={(event) => setEmail(event.target.value)} required /></Field>
41
- <Field label="Password" htmlFor="password"><Input id="password" type="password" autoComplete="current-password" value={password} onChange={(event) => setPassword(event.target.value)} required /></Field>
42
- <div className="form-row end"><Link to="/forgot-password">Forgot password?</Link></div>
43
- <Button type="submit" disabled={pending}><KeyRound size={17} /> {pending ? "Signing in..." : "Sign in"}</Button>
51
+ <AuthShell
52
+ title="Welcome back"
53
+ subtitle="Sign in to your workspace"
54
+ footer={<>New to Authenik8? <Link to="/register">Create an account</Link></>}
55
+ >
56
+ {params.has("oauthError") && (
57
+ <Banner
58
+ className="login-alert"
59
+ status="error"
60
+ title="OAuth sign-in failed"
61
+ description="The provider could not complete sign-in. Please try again."
62
+ />
63
+ )}
64
+ {error !== undefined && (
65
+ <Banner
66
+ className="login-alert"
67
+ status="error"
68
+ title="Unable to sign in"
69
+ description={getErrorMessage(error)}
70
+ />
71
+ )}
72
+ <form className="form-stack login-form" onSubmit={submit}>
73
+ <CredentialInput
74
+ label="Email address"
75
+ type="email"
76
+ htmlName="email"
77
+ autoComplete="email"
78
+ value={email}
79
+ onChange={setEmail}
80
+ startIcon={<Mail size={17} />}
81
+ size="lg"
82
+ width="100%"
83
+ isRequired
84
+ />
85
+ <CredentialInput
86
+ label="Password"
87
+ type="password"
88
+ htmlName="password"
89
+ autoComplete="current-password"
90
+ value={password}
91
+ onChange={setPassword}
92
+ startIcon={<LockKeyhole size={17} />}
93
+ size="lg"
94
+ width="100%"
95
+ isRequired
96
+ />
97
+ <div className="login-recovery"><Link to="/forgot-password">Forgot password?</Link></div>
98
+ <Button
99
+ type="submit"
100
+ label="Sign in"
101
+ variant="primary"
102
+ icon={<KeyRound size={17} />}
103
+ width="100%"
104
+ isLoading={pending}
105
+ isDisabled={pending}
106
+ />
44
107
  </form>
45
- {enabledOAuthProviders.length > 0 && <><div className="divider"><span>or continue with</span></div>
46
- <div className="oauth-grid">
47
- {enabledOAuthProviders.map((provider) => <Button key={provider} variant="secondary" onClick={() => window.location.assign(`/api/auth/oauth/${provider}`)}>{provider === "google" ? <span className="provider-g">G</span> : <Github size={18} />} {provider === "google" ? "Google" : "GitHub"}</Button>)}
48
- </div></>}
108
+ {enabledOAuthProviders.length > 0 && (
109
+ <>
110
+ <div className="divider"><span>or continue with</span></div>
111
+ <div className="oauth-grid">
112
+ {enabledOAuthProviders.map((provider) => (
113
+ <Button
114
+ key={provider}
115
+ label={provider === "google" ? "Google" : "GitHub"}
116
+ variant="secondary"
117
+ width="100%"
118
+ icon={provider === "google" ? <span className="provider-g">G</span> : <Github size={18} />}
119
+ onClick={() => window.location.assign(`/api/auth/oauth/${provider}`)}
120
+ />
121
+ ))}
122
+ </div>
123
+ </>
124
+ )}
49
125
  </AuthShell>
50
126
  );
51
127
  }
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
- import { useNavigate, useSearchParams } from "react-router-dom";
2
+ import { useNavigate, useSearchParams } from "react-router";
3
3
  import { Spinner } from "@authenik8/ui";
4
4
  import { useAuth } from "../../auth/AuthProvider";
5
5
  import { AuthShell } from "../../components/AuthShell";
@@ -1,6 +1,6 @@
1
1
  import { useState, type FormEvent } from "react";
2
2
  import { ArrowLeft, Mail, RotateCcw } from "lucide-react";
3
- import { Link, useSearchParams } from "react-router-dom";
3
+ import { Link, useSearchParams } from "react-router";
4
4
  import { Button, Input } from "@authenik8/ui";
5
5
  import { authApi } from "@authenik8/api-client";
6
6
  import { AuthShell } from "../../components/AuthShell";
@@ -1,6 +1,6 @@
1
1
  import { useState, type FormEvent } from "react";
2
2
  import { UserPlus } from "lucide-react";
3
- import { Link } from "react-router-dom";
3
+ import { Link } from "react-router";
4
4
  import { registerSchema, type RegisterInput } from "@authenik8/contracts";
5
5
  import { ApiError, authApi } from "@authenik8/api-client";
6
6
  import { Button, Input } from "@authenik8/ui";
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from "react";
2
- import { Link, useSearchParams } from "react-router-dom";
2
+ import { Link, useSearchParams } from "react-router";
3
3
  import { Spinner } from "@authenik8/ui";
4
4
  import { authApi } from "@authenik8/api-client";
5
5
  import { AuthShell } from "../../components/AuthShell";
@@ -20,6 +20,37 @@
20
20
  --amber-light: #fff1ce;
21
21
  --red: #bd384c;
22
22
  --red-light: #fde8ec;
23
+ --color-background-surface: #ffffff;
24
+ --color-background-body: #f3f5f2;
25
+ --color-background-card: #ffffff;
26
+ --color-background-popover: #ffffff;
27
+ --color-background-muted: #eef2ee;
28
+ --color-accent: #218653;
29
+ --color-accent-muted: #e4f4e9;
30
+ --color-neutral: #edf1ee;
31
+ --color-overlay: rgba(11, 35, 23, .58);
32
+ --color-overlay-hover: rgba(18, 53, 36, .06);
33
+ --color-overlay-pressed: rgba(18, 53, 36, .11);
34
+ --color-text-primary: #27302b;
35
+ --color-text-secondary: #69736c;
36
+ --color-text-disabled: #98a099;
37
+ --color-text-accent: #176a46;
38
+ --color-on-accent: #ffffff;
39
+ --color-icon-primary: #27302b;
40
+ --color-icon-secondary: #69736c;
41
+ --color-icon-accent: #218653;
42
+ --color-success: #246a43;
43
+ --color-success-muted: #e2f3e7;
44
+ --color-warning: #9a6512;
45
+ --color-warning-muted: #fff1ce;
46
+ --color-error: #bd384c;
47
+ --color-error-muted: #fde8ec;
48
+ --color-border: #dce3dd;
49
+ --color-border-emphasized: #bdc9bf;
50
+ --radius-element: 6px;
51
+ --radius-container: 10px;
52
+ --radius-page: 14px;
53
+ --shadow-low: 0 1px 2px rgba(18, 53, 36, .05), 0 6px 18px rgba(18, 53, 36, .04);
23
54
  }
24
55
 
25
56
  * { box-sizing: border-box; letter-spacing: 0; }
@@ -98,56 +129,90 @@ code { overflow-wrap: anywhere; color: #246a43; font-family: "SFMono-Regular", C
98
129
  .screen-loader { min-height: 50vh; display: grid; place-items: center; }
99
130
  .panel-loading, .center-status { min-height: 180px; display: grid; place-items: center; }
100
131
 
101
- .app-layout { min-height: 100vh; display: grid; grid-template-columns: 236px minmax(0, 1fr); }
102
- .sidebar, .mobile-sidebar { background: var(--navy); color: #e2eee6; }
103
- .sidebar { position: sticky; top: 0; height: 100vh; padding: 22px 14px 16px; display: flex; flex-direction: column; }
104
- .brand { min-width: 0; display: flex; align-items: center; gap: 10px; color: #fff; font-weight: 800; }
105
- .brand:hover { color: #fff; }
106
- .brand img { width: 44px; height: 44px; margin: -5px; flex: 0 0 auto; }
107
- .brand span { min-width: 0; display: grid; font-size: 15px; }
108
- .brand small { color: #93b5a1; font-size: 10px; font-weight: 600; }
109
- .side-nav { margin-top: 32px; display: grid; gap: 4px; }
110
- .nav-label { margin: 18px 10px 5px; color: #769583; font-size: 10px; font-weight: 800; text-transform: uppercase; }
111
- .side-nav a { min-height: 42px; padding: 10px 11px; border-radius: 5px; display: flex; align-items: center; gap: 11px; color: #b3c9bb; font-size: 13px; font-weight: 650; }
112
- .side-nav a:hover { color: #fff; background: #19472f; }
113
- .side-nav a.active { color: #fff; background: #20563a; box-shadow: inset 3px 0 0 #7ce8aa; }
114
- .sidebar-account { min-width: 0; margin-top: auto; padding: 13px 7px 0; border-top: 1px solid #28533a; display: flex; align-items: center; gap: 10px; }
115
- .avatar { width: 34px; height: 34px; flex: 0 0 auto; border-radius: 6px; display: grid; place-items: center; color: #17492f; background: #a7dfbf; font-size: 11px; font-weight: 900; }
132
+ .workspace-shell { background: var(--navy); }
133
+ .workspace-topbar { min-height: 68px; border-bottom: 1px solid var(--line); background: rgba(255, 255, 255, .96); backdrop-filter: blur(14px); }
134
+ .brand-logo { width: 43px; height: 43px; margin: -6px; display: block; }
135
+ .current-section { min-width: 0; display: flex; align-items: center; gap: 8px; color: #626d65; font-size: 13px; font-weight: 700; }
136
+ .current-section svg { color: var(--green); }
137
+ .workspace-sidebar {
138
+ --color-background-surface: var(--navy);
139
+ --color-background-muted: var(--navy-soft);
140
+ --color-text-primary: #dcebe1;
141
+ --color-text-secondary: #8fad9a;
142
+ --color-text-disabled: #64806e;
143
+ --color-icon-primary: #dcebe1;
144
+ --color-icon-secondary: #9eb9a8;
145
+ --color-icon-accent: #7ce8aa;
146
+ --color-neutral: #20563a;
147
+ --color-overlay-hover: #19472f;
148
+ --color-overlay-pressed: #20563a;
149
+ --color-border: #28533a;
150
+ background: var(--navy);
151
+ border-right: 1px solid #28533a;
152
+ }
153
+ .workspace-sidebar button:focus-visible { outline: 2px solid #7ce8aa; outline-offset: -2px; }
154
+ .sidebar-account { min-width: 0; display: flex; align-items: center; gap: 10px; }
116
155
  .sidebar-account > div:last-child { min-width: 0; display: grid; gap: 2px; }
117
156
  .sidebar-account strong, .sidebar-account span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
118
157
  .sidebar-account strong { color: #f2faf5; font-size: 12px; }
119
158
  .sidebar-account span { color: #8fad9a; font-size: 10px; }
120
- .mobile-sidebar, .mobile-scrim { display: none; }
121
-
122
- .app-main { min-width: 0; }
123
- .topbar { height: 64px; padding: 0 28px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 14px; background: rgba(255,255,255,.95); position: sticky; top: 0; z-index: 20; }
124
- .breadcrumb { min-width: 0; display: flex; align-items: center; gap: 8px; color: #626d65; font-size: 13px; font-weight: 700; }
125
- .breadcrumb svg { color: var(--green); }
126
- .topbar > .icon-button:last-child { margin-left: auto; }
127
- .menu-button { display: none; }
128
159
  .page { width: 100%; max-width: 1480px; margin: 0 auto; padding: 32px; }
129
160
  .page-header { min-height: 62px; margin-bottom: 24px; display: flex; justify-content: space-between; align-items: flex-start; gap: 20px; }
130
161
  .page-header p { margin-bottom: 0; font-size: 14px; }
131
162
  .page-actions, .action-row { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 9px; }
132
163
 
133
- .stat-grid { margin-bottom: 22px; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
134
- .stat-card { min-height: 92px; padding: 17px; display: flex; align-items: center; gap: 14px; }
135
- .stat-card > div { min-width: 0; display: grid; gap: 5px; }
136
- .stat-card span { color: #717c74; font-size: 12px; font-weight: 650; }
137
- .stat-card strong { overflow-wrap: anywhere; color: #27302b; font-size: 21px; line-height: 1.1; }
138
- .stat-icon { width: 38px; height: 38px; flex: 0 0 auto; border-radius: 6px; display: grid; place-items: center; }
164
+ .dashboard-page { display: grid; gap: 22px; }
165
+ .dashboard-hero {
166
+ min-height: 210px;
167
+ padding: 34px 38px;
168
+ position: relative;
169
+ overflow: hidden;
170
+ border: 1px solid #285f41;
171
+ border-radius: 12px;
172
+ display: flex;
173
+ align-items: flex-end;
174
+ justify-content: space-between;
175
+ gap: 28px;
176
+ color: #ffffff;
177
+ background:
178
+ radial-gradient(circle at 86% 8%, rgba(124, 232, 170, .2), transparent 34%),
179
+ linear-gradient(135deg, #123524 0%, #16452d 68%, #1a5134 100%);
180
+ box-shadow: 0 18px 45px rgba(18, 53, 36, .14);
181
+ --color-accent: #a7dfbf;
182
+ --color-on-accent: #123524;
183
+ --color-text-primary: #123524;
184
+ --color-border: rgba(255, 255, 255, .42);
185
+ }
186
+ .dashboard-hero::after { content: ""; width: 190px; height: 190px; position: absolute; top: -112px; right: 14%; border: 1px solid rgba(255,255,255,.14); border-radius: 50%; box-shadow: 0 0 0 38px rgba(255,255,255,.025), 0 0 0 78px rgba(255,255,255,.02); pointer-events: none; }
187
+ .hero-copy, .hero-actions { position: relative; z-index: 1; }
188
+ .hero-copy { max-width: 700px; }
189
+ .hero-copy h1 { margin-bottom: 10px; color: #ffffff; font-size: clamp(28px, 3vw, 38px); letter-spacing: -.025em; }
190
+ .hero-copy p { max-width: 610px; margin-bottom: 0; color: #b9cebf; font-size: 15px; }
191
+ .eyebrow { margin-bottom: 17px; display: flex; align-items: center; gap: 8px; color: #9fc5ac; font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
192
+ .hero-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 9px; }
193
+ .hero-actions button:first-child { color: #eef8f1; border-color: rgba(255,255,255,.28); background: rgba(255,255,255,.08); }
194
+ .hero-actions button:first-child:hover { background: rgba(255,255,255,.14); }
195
+
196
+ .stat-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
197
+ .stat-card { min-height: 116px; padding: 18px; display: flex; align-items: center; gap: 14px; box-shadow: var(--shadow-low); }
198
+ .stat-copy { min-width: 0; display: grid; gap: 4px; }
199
+ .stat-copy > span { color: #717c74; font-size: 12px; font-weight: 650; }
200
+ .stat-copy strong { overflow-wrap: anywhere; color: #27302b; font-size: 23px; line-height: 1.1; letter-spacing: -.02em; }
201
+ .stat-copy small { margin-top: 2px; overflow: hidden; color: #8a938c; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
202
+ .stat-icon { width: 42px; height: 42px; flex: 0 0 auto; border-radius: 8px; display: grid; place-items: center; }
139
203
  .stat-icon.cyan { color: #1d7549; background: var(--cyan-light); }
140
204
  .stat-icon.violet { color: #46647b; background: var(--violet-light); }
141
205
  .stat-icon.green { color: #246a43; background: var(--green-light); }
142
206
  .stat-icon.amber { color: #8a5a0f; background: var(--amber-light); }
143
207
  .dashboard-grid { display: grid; grid-template-columns: minmax(0, 1.65fr) minmax(280px, .7fr); gap: 18px; align-items: start; }
208
+ .dashboard-panel { min-width: 0; overflow: hidden; box-shadow: var(--shadow-low); }
144
209
  .panel { min-width: 0; overflow: hidden; }
145
210
  .panel-heading { min-height: 72px; padding: 18px 20px; border-bottom: 1px solid #e8eaf0; display: flex; justify-content: space-between; align-items: center; gap: 16px; }
146
211
  .panel-heading h2, .panel-heading p { margin-bottom: 0; }
147
212
  .panel-heading p { margin-top: 2px; font-size: 12px; }
148
213
  .text-link, .back-link { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 750; }
149
214
  .back-link { margin-bottom: 13px; }
150
- .security-panel { padding-bottom: 18px; }
215
+ .panel-error { padding: 18px 20px; }
151
216
  .check-list { margin: 0; padding: 8px 20px 18px; list-style: none; }
152
217
  .check-list li { padding: 14px 0; border-bottom: 1px solid #eceef3; display: flex; align-items: center; gap: 11px; }
153
218
  .check-list svg { color: var(--green); }
@@ -155,7 +220,7 @@ code { overflow-wrap: anywhere; color: #246a43; font-family: "SFMono-Regular", C
155
220
  .check-list span { display: grid; gap: 3px; }
156
221
  .check-list strong { font-size: 13px; }
157
222
  .check-list small { color: var(--muted); font-size: 11px; }
158
- .security-panel > .button { margin: 0 20px; width: calc(100% - 40px); }
223
+ .security-action { padding: 0 20px 20px; }
159
224
 
160
225
  .table-wrap { max-width: 100%; overflow-x: auto; }
161
226
  table { width: 100%; border-collapse: collapse; font-size: 13px; }
@@ -210,6 +275,11 @@ tbody tr:hover { background: #fbfcfd; }
210
275
 
211
276
  .auth-layout { min-height: 100vh; display: grid; grid-template-columns: minmax(360px, .8fr) minmax(500px, 1.2fr); background: #fff; }
212
277
  .auth-brand-panel { min-height: 100vh; padding: 34px 42px; position: relative; overflow: hidden; display: flex; flex-direction: column; color: #fff; background: var(--navy); border-right: 1px solid #286342; }
278
+ .auth-top-brand { min-width: 0; display: flex; align-items: center; gap: 10px; color: #ffffff; font-size: 15px; font-weight: 800; }
279
+ .auth-top-brand:hover { color: #ffffff; }
280
+ .auth-top-brand img { width: 44px; height: 44px; margin: -5px; flex: 0 0 auto; }
281
+ .auth-top-brand span { min-width: 0; display: grid; }
282
+ .auth-top-brand small { color: #93b5a1; font-size: 10px; font-weight: 600; }
213
283
  .auth-brand-copy { max-width: 470px; margin: auto 0; position: relative; z-index: 1; }
214
284
  .auth-logo { width: 220px; height: 220px; margin: 0 0 2px -34px; display: block; }
215
285
  .security-mark { margin-bottom: 18px; display: block; color: #7ce8aa; font-family: "SFMono-Regular", Consolas, monospace; font-size: 11px; font-weight: 800; }
@@ -217,14 +287,18 @@ tbody tr:hover { background: #fbfcfd; }
217
287
  .auth-brand-copy p { max-width: 390px; margin: 17px 0 0; color: #b4cbbb; font-size: 15px; }
218
288
  .auth-signal { display: flex; align-items: center; gap: 8px; color: #93b29f; font-size: 11px; }
219
289
  .signal-dot { width: 8px; height: 8px; border-radius: 50%; background: #7ce8aa; box-shadow: 0 0 0 4px rgba(124,232,170,.12); }
220
- .auth-form-panel { min-width: 0; padding: 50px 7vw; display: grid; place-items: center; }
221
- .auth-form-wrap { width: min(100%, 440px); }
290
+ .auth-form-panel { min-width: 0; padding: 50px 7vw; display: grid; place-items: center; background: radial-gradient(circle at 100% 0%, rgba(33, 134, 83, .08), transparent 32%), #f8faf8; }
291
+ .auth-form-card { width: min(100%, 480px); box-shadow: 0 20px 55px rgba(18, 53, 36, .1); }
292
+ .auth-form-wrap { width: 100%; padding: 34px; }
222
293
  .auth-form-wrap > header { margin-bottom: 27px; }
223
294
  .auth-form-wrap > header h2 { margin-bottom: 6px; font-size: 26px; }
224
295
  .auth-form-wrap > header p { margin: 0; font-size: 14px; }
225
296
  .auth-form-wrap > footer { margin-top: 24px; color: #71788a; font-size: 13px; text-align: center; }
226
297
  .auth-form-wrap > footer a { display: inline-flex; align-items: center; gap: 5px; font-weight: 700; }
227
298
  .auth-form-wrap .notice { margin-bottom: 17px; }
299
+ .login-alert { margin-bottom: 17px; }
300
+ .login-form { gap: 18px; }
301
+ .login-recovery { margin-top: -7px; display: flex; justify-content: flex-end; font-size: 13px; font-weight: 700; }
228
302
  .divider { margin: 22px 0; display: flex; align-items: center; gap: 12px; color: #9197a5; font-size: 11px; }
229
303
  .divider::before, .divider::after { content: ""; height: 1px; flex: 1; background: #e2e5eb; }
230
304
  .oauth-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
@@ -240,21 +314,19 @@ tbody tr:hover { background: #fbfcfd; }
240
314
  .auth-brand-copy h1 { font-size: 32px; }
241
315
  }
242
316
 
243
- @media (max-width: 760px) {
244
- .app-layout { display: block; }
245
- .sidebar { display: none; }
246
- .mobile-scrim { display: block; position: fixed; inset: 0; z-index: 39; background: rgba(11,35,23,.58); }
247
- .mobile-sidebar { width: min(84vw, 280px); padding: 20px 14px; position: fixed; inset: 0 auto 0 0; z-index: 40; display: flex; flex-direction: column; transform: translateX(-105%); transition: transform 180ms ease; }
248
- .mobile-sidebar.is-open { transform: translateX(0); }
249
- .topbar { height: 58px; padding: 0 16px; }
250
- .menu-button { display: inline-flex; }
317
+ @media (max-width: 768px) {
318
+ .workspace-topbar { min-height: 58px; }
319
+ .current-section { display: none; }
251
320
  .page { padding: 22px 16px 36px; }
252
321
  .page-header { min-height: 0; margin-bottom: 18px; display: grid; }
253
322
  .page-header h1 { font-size: 24px; }
254
323
  .page-actions { justify-content: flex-start; }
324
+ .dashboard-page { gap: 16px; }
325
+ .dashboard-hero { min-height: 230px; padding: 26px 24px; display: grid; align-content: end; }
326
+ .hero-actions { justify-content: flex-start; }
255
327
  .stat-grid { grid-template-columns: 1fr 1fr; gap: 10px; }
256
- .stat-card { min-height: 86px; padding: 13px; }
257
- .stat-card strong { font-size: 17px; }
328
+ .stat-card { min-height: 104px; padding: 14px; }
329
+ .stat-copy strong { font-size: 19px; }
258
330
  .settings-layout { grid-template-columns: 1fr; gap: 16px; }
259
331
  .settings-nav { grid-template-columns: 1fr 1fr; }
260
332
  .settings-nav a { border-left: 0; border-bottom: 3px solid transparent; text-align: center; }
@@ -270,11 +342,15 @@ tbody tr:hover { background: #fbfcfd; }
270
342
  .auth-signal { display: none; }
271
343
  .security-mark { margin-bottom: 10px; }
272
344
  .auth-form-panel { min-height: calc(100vh - 220px); padding: 34px 20px; align-items: start; }
345
+ .auth-form-wrap { padding: 28px 24px; }
273
346
  th, td { padding: 12px 14px; }
274
347
  }
275
348
 
276
349
  @media (max-width: 480px) {
277
350
  .stat-grid { grid-template-columns: 1fr; }
351
+ .dashboard-hero { padding: 24px 20px; }
352
+ .hero-copy h1 { font-size: 27px; }
353
+ .hero-actions { display: grid; }
278
354
  .oauth-grid { grid-template-columns: 1fr; }
279
355
  .form-panel, .settings-panel { padding: 18px; }
280
356
  .form-actions, .action-row { justify-content: stretch; }