millas 0.2.12-beta-1 → 0.2.13

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 (120) hide show
  1. package/package.json +3 -2
  2. package/src/admin/ActivityLog.js +153 -52
  3. package/src/admin/Admin.js +516 -199
  4. package/src/admin/AdminAuth.js +213 -98
  5. package/src/admin/FormGenerator.js +372 -0
  6. package/src/admin/HookRegistry.js +256 -0
  7. package/src/admin/QueryEngine.js +263 -0
  8. package/src/admin/ViewContext.js +318 -0
  9. package/src/admin/WidgetRegistry.js +406 -0
  10. package/src/admin/index.js +17 -0
  11. package/src/admin/resources/AdminResource.js +393 -97
  12. package/src/admin/static/admin.css +1422 -0
  13. package/src/admin/static/date-picker.css +157 -0
  14. package/src/admin/static/date-picker.js +316 -0
  15. package/src/admin/static/json-editor.css +649 -0
  16. package/src/admin/static/json-editor.js +1429 -0
  17. package/src/admin/static/ui.js +1044 -0
  18. package/src/admin/views/layouts/base.njk +87 -1046
  19. package/src/admin/views/pages/detail.njk +56 -21
  20. package/src/admin/views/pages/error.njk +65 -0
  21. package/src/admin/views/pages/form.njk +47 -599
  22. package/src/admin/views/pages/list.njk +270 -62
  23. package/src/admin/views/partials/form-field.njk +53 -0
  24. package/src/admin/views/partials/form-footer.njk +28 -0
  25. package/src/admin/views/partials/form-readonly.njk +114 -0
  26. package/src/admin/views/partials/form-scripts.njk +480 -0
  27. package/src/admin/views/partials/form-widget.njk +297 -0
  28. package/src/admin/views/partials/icons.njk +64 -0
  29. package/src/admin/views/partials/json-dialog.njk +80 -0
  30. package/src/admin/views/partials/json-editor.njk +37 -0
  31. package/src/ai/AIManager.js +954 -0
  32. package/src/ai/AITokenBudget.js +250 -0
  33. package/src/ai/PromptGuard.js +216 -0
  34. package/src/ai/agents.js +218 -0
  35. package/src/ai/conversation.js +213 -0
  36. package/src/ai/drivers.js +734 -0
  37. package/src/ai/files.js +249 -0
  38. package/src/ai/media.js +303 -0
  39. package/src/ai/pricing.js +152 -0
  40. package/src/ai/provider_tools.js +114 -0
  41. package/src/ai/types.js +356 -0
  42. package/src/auth/Auth.js +18 -2
  43. package/src/auth/AuthUser.js +65 -44
  44. package/src/cli.js +3 -1
  45. package/src/commands/createsuperuser.js +267 -0
  46. package/src/commands/lang.js +589 -0
  47. package/src/commands/migrate.js +154 -81
  48. package/src/commands/serve.js +3 -4
  49. package/src/container/AppInitializer.js +101 -20
  50. package/src/container/Application.js +31 -1
  51. package/src/container/MillasApp.js +10 -3
  52. package/src/container/MillasConfig.js +35 -6
  53. package/src/core/admin.js +5 -0
  54. package/src/core/db.js +2 -1
  55. package/src/core/foundation.js +2 -10
  56. package/src/core/lang.js +1 -0
  57. package/src/errors/HttpError.js +32 -16
  58. package/src/facades/AI.js +411 -0
  59. package/src/facades/Hash.js +67 -0
  60. package/src/facades/Process.js +144 -0
  61. package/src/hashing/Hash.js +262 -0
  62. package/src/http/HtmlEscape.js +162 -0
  63. package/src/http/MillasRequest.js +63 -7
  64. package/src/http/MillasResponse.js +70 -4
  65. package/src/http/ResponseDispatcher.js +21 -27
  66. package/src/http/SafeFilePath.js +195 -0
  67. package/src/http/SafeRedirect.js +62 -0
  68. package/src/http/SecurityBootstrap.js +70 -0
  69. package/src/http/helpers.js +40 -125
  70. package/src/http/index.js +10 -1
  71. package/src/http/middleware/CsrfMiddleware.js +258 -0
  72. package/src/http/middleware/RateLimiter.js +314 -0
  73. package/src/http/middleware/SecurityHeaders.js +281 -0
  74. package/src/i18n/I18nServiceProvider.js +91 -0
  75. package/src/i18n/Translator.js +643 -0
  76. package/src/i18n/defaults.js +122 -0
  77. package/src/i18n/index.js +164 -0
  78. package/src/i18n/locales/en.js +55 -0
  79. package/src/i18n/locales/sw.js +48 -0
  80. package/src/logger/LogRedactor.js +247 -0
  81. package/src/logger/Logger.js +1 -1
  82. package/src/logger/formatters/JsonFormatter.js +11 -4
  83. package/src/logger/formatters/PrettyFormatter.js +103 -65
  84. package/src/logger/formatters/SimpleFormatter.js +14 -3
  85. package/src/middleware/ThrottleMiddleware.js +27 -4
  86. package/src/migrations/system/0001_users.js +21 -0
  87. package/src/migrations/system/0002_admin_log.js +25 -0
  88. package/src/migrations/system/0003_sessions.js +23 -0
  89. package/src/orm/fields/index.js +210 -188
  90. package/src/orm/migration/DefaultValueParser.js +325 -0
  91. package/src/orm/migration/InteractiveResolver.js +191 -0
  92. package/src/orm/migration/Makemigrations.js +312 -0
  93. package/src/orm/migration/MigrationGraph.js +227 -0
  94. package/src/orm/migration/MigrationRunner.js +202 -108
  95. package/src/orm/migration/MigrationWriter.js +463 -0
  96. package/src/orm/migration/ModelInspector.js +143 -74
  97. package/src/orm/migration/ModelScanner.js +225 -0
  98. package/src/orm/migration/ProjectState.js +213 -0
  99. package/src/orm/migration/RenameDetector.js +175 -0
  100. package/src/orm/migration/SchemaBuilder.js +8 -81
  101. package/src/orm/migration/operations/base.js +57 -0
  102. package/src/orm/migration/operations/column.js +191 -0
  103. package/src/orm/migration/operations/fields.js +252 -0
  104. package/src/orm/migration/operations/index.js +55 -0
  105. package/src/orm/migration/operations/models.js +152 -0
  106. package/src/orm/migration/operations/registry.js +131 -0
  107. package/src/orm/migration/operations/special.js +51 -0
  108. package/src/orm/migration/utils.js +208 -0
  109. package/src/orm/model/Model.js +81 -13
  110. package/src/process/Process.js +333 -0
  111. package/src/providers/AdminServiceProvider.js +66 -9
  112. package/src/providers/AuthServiceProvider.js +40 -5
  113. package/src/providers/CacheStorageServiceProvider.js +2 -2
  114. package/src/providers/DatabaseServiceProvider.js +3 -2
  115. package/src/providers/LogServiceProvider.js +4 -1
  116. package/src/providers/MailServiceProvider.js +1 -1
  117. package/src/providers/QueueServiceProvider.js +1 -1
  118. package/src/router/MiddlewareRegistry.js +27 -2
  119. package/src/scaffold/templates.js +80 -21
  120. package/src/validation/Validator.js +348 -607
@@ -0,0 +1,1422 @@
1
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2
+
3
+ :root {
4
+ /* ── Palette ── */
5
+ --bg: #f6f7f8;
6
+ --surface: #ffffff;
7
+ --surface2: #fafafa;
8
+ --surface3: #f0f1f2;
9
+ --border: #e5e7eb;
10
+ --border-soft: #f0f1f2;
11
+
12
+ /* ── Brand (Cloudflare Orange) ── */
13
+ --primary: #f6821f;
14
+ --primary-h: #e06d1a;
15
+ --primary-soft: #fff4e6;
16
+ --primary-dim: #ffe4c4;
17
+
18
+ /* ── Text ── */
19
+ --text: #172b4d;
20
+ --text-soft: #2c3e50;
21
+ --text-muted: #6b7280;
22
+ --text-xmuted: #9ca3af;
23
+
24
+ /* ── Semantic ── */
25
+ --success: #10b981;
26
+ --success-bg: #ecfdf5;
27
+ --success-border:#a7f3d0;
28
+ --danger: #ef4444;
29
+ --danger-bg: #fef2f2;
30
+ --danger-border:#fecaca;
31
+ --warning: #f59e0b;
32
+ --warning-bg: #fffbeb;
33
+ --warning-border:#fde68a;
34
+ --info: #3b82f6;
35
+ --info-bg: #eff6ff;
36
+ --info-border: #bfdbfe;
37
+
38
+ /* ── Shape ── */
39
+ --radius: 6px;
40
+ --radius-sm: 4px;
41
+ --radius-lg: 8px;
42
+ --shadow-sm: 0 1px 2px rgba(0,0,0,.05);
43
+ --shadow: 0 1px 3px rgba(0,0,0,.1), 0 1px 2px rgba(0,0,0,.06);
44
+ --shadow-lg: 0 10px 25px rgba(0,0,0,.1), 0 4px 10px rgba(0,0,0,.05);
45
+
46
+ /* ── Sidebar ── */
47
+ --sidebar-w: 232px;
48
+ }
49
+
50
+ body {
51
+ font-family: 'DM Sans', system-ui, sans-serif;
52
+ background: var(--bg);
53
+ color: var(--text);
54
+ display: flex;
55
+ height: 100vh;
56
+ overflow: hidden;
57
+ font-size: 14px;
58
+ line-height: 1.5;
59
+ -webkit-font-smoothing: antialiased;
60
+ }
61
+
62
+ /* ════════════════════════════════════════
63
+ ICONS (inline SVG sprite system)
64
+ ════════════════════════════════════════ */
65
+ .icon {
66
+ display: inline-flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+ flex-shrink: 0;
70
+ }
71
+ .icon svg {
72
+ width: 1em;
73
+ height: 1em;
74
+ fill: none;
75
+ stroke: currentColor;
76
+ stroke-width: 1.75;
77
+ stroke-linecap: round;
78
+ stroke-linejoin: round;
79
+ }
80
+ .icon-sm svg { stroke-width: 2; }
81
+ .icon-14 { font-size: 14px; }
82
+ .icon-15 { font-size: 15px; }
83
+ .icon-16 { font-size: 16px; }
84
+ .icon-18 { font-size: 18px; }
85
+ .icon-20 { font-size: 20px; }
86
+
87
+ /* ════════════════════════════════════════
88
+ SIDEBAR
89
+ ════════════════════════════════════════ */
90
+ #sidebar {
91
+ width: var(--sidebar-w);
92
+ min-width: var(--sidebar-w);
93
+ background: var(--surface);
94
+ border-right: 1px solid var(--border);
95
+ display: flex;
96
+ flex-direction: column;
97
+ overflow-y: auto;
98
+ overflow-x: hidden;
99
+ }
100
+
101
+ .sidebar-brand {
102
+ padding: 18px 16px 16px;
103
+ border-bottom: 1px solid var(--border-soft);
104
+ display: flex;
105
+ align-items: center;
106
+ gap: 10px;
107
+ }
108
+ .brand-logo {
109
+ width: 34px; height: 34px;
110
+ background: linear-gradient(135deg, #f6821f 0%, #ff9a3c 100%);
111
+ border-radius: 9px;
112
+ display: flex; align-items: center; justify-content: center;
113
+ color: #fff;
114
+ font-size: 16px;
115
+ flex-shrink: 0;
116
+ box-shadow: 0 2px 6px rgba(246,130,31,.25);
117
+ }
118
+ .brand-text { line-height: 1.25; overflow: hidden; }
119
+ .brand-name {
120
+ font-size: 13.5px;
121
+ font-weight: 700;
122
+ color: var(--text);
123
+ white-space: nowrap;
124
+ overflow: hidden;
125
+ text-overflow: ellipsis;
126
+ }
127
+ .brand-sub { font-size: 11px; color: var(--text-muted); }
128
+
129
+ .nav-section { padding: 10px 10px 4px; }
130
+ .nav-label {
131
+ font-size: 10.5px;
132
+ font-weight: 600;
133
+ color: var(--text-xmuted);
134
+ text-transform: uppercase;
135
+ letter-spacing: 0.7px;
136
+ padding: 0 8px 6px;
137
+ }
138
+ .nav-item {
139
+ display: flex;
140
+ align-items: center;
141
+ gap: 9px;
142
+ padding: 7px 10px;
143
+ border-radius: var(--radius-sm);
144
+ color: var(--text-muted);
145
+ text-decoration: none;
146
+ font-size: 13.5px;
147
+ font-weight: 500;
148
+ transition: background .12s, color .12s;
149
+ cursor: pointer;
150
+ border: none;
151
+ background: none;
152
+ width: 100%;
153
+ text-align: left;
154
+ }
155
+ .nav-item:hover { background: var(--surface2); color: var(--text-soft); }
156
+ .nav-item.active { background: var(--primary-soft); color: var(--primary); border-left: 3px solid var(--primary); padding-left: 7px; }
157
+ .nav-item.active .nav-icon { color: var(--primary); }
158
+
159
+ .nav-icon {
160
+ color: var(--text-xmuted);
161
+ transition: color .12s;
162
+ width: 18px;
163
+ display: flex;
164
+ align-items: center;
165
+ justify-content: center;
166
+ flex-shrink: 0;
167
+ }
168
+ .nav-item:hover .nav-icon { color: var(--text-soft); }
169
+ .nav-item.active .nav-icon { color: var(--primary); }
170
+
171
+ .nav-count {
172
+ margin-left: auto;
173
+ font-size: 11px;
174
+ font-weight: 500;
175
+ background: var(--surface3);
176
+ color: var(--text-muted);
177
+ padding: 1px 6px;
178
+ border-radius: 99px;
179
+ min-width: 20px;
180
+ text-align: center;
181
+ }
182
+
183
+ .sidebar-footer {
184
+ margin-top: auto;
185
+ padding: 12px 16px;
186
+ border-top: 1px solid var(--border-soft);
187
+ }
188
+ .sidebar-version {
189
+ font-size: 11px;
190
+ color: var(--text-xmuted);
191
+ }
192
+
193
+ /* ── User row ── */
194
+ .user-row {
195
+ display: flex; align-items: center; gap: 9px;
196
+ }
197
+ .user-avatar {
198
+ width: 30px; height: 30px; border-radius: 8px;
199
+ background: var(--primary-dim); color: var(--primary);
200
+ display: flex; align-items: center; justify-content: center;
201
+ font-size: 13px; font-weight: 700; flex-shrink: 0;
202
+ }
203
+ .user-info { flex: 1; min-width: 0; }
204
+ .user-name { font-size: 12.5px; font-weight: 600; color: var(--text-soft); }
205
+ .user-email { font-size: 11px; color: var(--text-muted); }
206
+ .logout-btn {
207
+ flex-shrink: 0; padding: 5px;
208
+ border-radius: 5px; color: var(--text-muted);
209
+ display: flex; align-items: center;
210
+ transition: background .1s, color .1s;
211
+ }
212
+ .logout-btn:hover { background: var(--surface3); color: var(--danger); }
213
+
214
+ /* ════════════════════════════════════════
215
+ MAIN AREA
216
+ ════════════════════════════════════════ */
217
+ #main {
218
+ flex: 1;
219
+ display: flex;
220
+ flex-direction: column;
221
+ overflow: hidden;
222
+ min-width: 0;
223
+ }
224
+
225
+ /* ── Topbar ── */
226
+ #topbar {
227
+ background: var(--surface);
228
+ border-bottom: 1px solid var(--border);
229
+ padding: 0 24px;
230
+ height: 54px;
231
+ display: flex;
232
+ align-items: center;
233
+ gap: 12px;
234
+ flex-shrink: 0;
235
+ box-shadow: var(--shadow-sm);
236
+ }
237
+ .topbar-title {
238
+ font-size: 15px;
239
+ font-weight: 600;
240
+ flex: 1;
241
+ color: var(--text);
242
+ display: flex;
243
+ align-items: center;
244
+ gap: 8px;
245
+ }
246
+ .topbar-actions { display: flex; gap: 8px; align-items: center; }
247
+
248
+ /* ── Content ── */
249
+ #content {
250
+ flex: 1;
251
+ overflow-y: auto;
252
+ padding: 24px;
253
+ }
254
+
255
+ /* ════════════════════════════════════════
256
+ BREADCRUMB
257
+ ════════════════════════════════════════ */
258
+ .breadcrumb {
259
+ display: flex;
260
+ align-items: center;
261
+ gap: 5px;
262
+ font-size: 12px;
263
+ color: var(--text-muted);
264
+ margin-bottom: 18px;
265
+ }
266
+ .breadcrumb a { color: var(--text-muted); text-decoration: none; }
267
+ .breadcrumb a:hover { color: var(--primary); }
268
+ .breadcrumb-sep { color: var(--border); }
269
+ .breadcrumb-current { color: var(--text-soft); font-weight: 500; }
270
+
271
+ /* ════════════════════════════════════════
272
+ ALERTS
273
+ ════════════════════════════════════════ */
274
+ .alert {
275
+ padding: 11px 16px;
276
+ border-radius: var(--radius-sm);
277
+ font-size: 13px;
278
+ margin-bottom: 18px;
279
+ display: flex;
280
+ align-items: center;
281
+ gap: 9px;
282
+ border: 1px solid transparent;
283
+ }
284
+ .alert-success { background: var(--success-bg); color: var(--success); border-color: var(--success-border); }
285
+ .alert-error { background: var(--danger-bg); color: var(--danger); border-color: var(--danger-border); }
286
+ .alert-warning { background: var(--warning-bg); color: var(--warning); border-color: var(--warning-border); }
287
+ .alert-info { background: var(--info-bg); color: var(--info); border-color: var(--info-border); }
288
+ .alert-close {
289
+ margin-left: auto;
290
+ background: none;
291
+ border: none;
292
+ cursor: pointer;
293
+ color: inherit;
294
+ opacity: .6;
295
+ padding: 0;
296
+ line-height: 1;
297
+ font-size: 16px;
298
+ }
299
+ .alert-close:hover { opacity: 1; }
300
+
301
+ /* ════════════════════════════════════════
302
+ CARDS
303
+ ════════════════════════════════════════ */
304
+ .card {
305
+ background: var(--surface);
306
+ border: 1px solid var(--border);
307
+ border-radius: var(--radius-lg);
308
+ overflow: hidden;
309
+ box-shadow: var(--shadow-sm);
310
+ }
311
+ .card-header {
312
+ padding: 14px 20px;
313
+ border-bottom: 1px solid var(--border-soft);
314
+ display: flex;
315
+ align-items: center;
316
+ justify-content: space-between;
317
+ gap: 12px;
318
+ flex-wrap: wrap;
319
+ background: var(--surface);
320
+ }
321
+ .card-title {
322
+ font-size: 13.5px;
323
+ font-weight: 600;
324
+ color: var(--text);
325
+ display: flex;
326
+ align-items: center;
327
+ gap: 7px;
328
+ }
329
+ .card-body { padding: 20px; }
330
+
331
+ /* ════════════════════════════════════════
332
+ STAT CARDS
333
+ ════════════════════════════════════════ */
334
+ .stats-grid {
335
+ display: grid;
336
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
337
+ gap: 10px;
338
+ margin-bottom: 18px;
339
+ }
340
+ .stat-card {
341
+ background: var(--surface);
342
+ border: 1px solid var(--border);
343
+ border-radius: var(--radius);
344
+ padding: 10px 12px;
345
+ display: flex;
346
+ flex-direction: column;
347
+ gap: 4px;
348
+ transition: box-shadow .15s, border-color .15s;
349
+ text-decoration: none;
350
+ box-shadow: var(--shadow-sm);
351
+ }
352
+ .stat-card:hover {
353
+ box-shadow: var(--shadow);
354
+ border-color: var(--primary-dim);
355
+ }
356
+ .stat-icon-wrap {
357
+ width: 28px; height: 28px;
358
+ border-radius: 7px;
359
+ background: linear-gradient(135deg, #fff4e6 0%, #ffe4c4 100%);
360
+ display: flex; align-items: center; justify-content: center;
361
+ color: var(--primary);
362
+ margin-bottom: 2px;
363
+ }
364
+ .stat-label {
365
+ font-size: 10.5px;
366
+ color: var(--text-muted);
367
+ font-weight: 500;
368
+ text-transform: uppercase;
369
+ letter-spacing: 0.3px;
370
+ }
371
+ .stat-value {
372
+ font-size: 20px;
373
+ font-weight: 700;
374
+ color: var(--text);
375
+ line-height: 1;
376
+ letter-spacing: -0.3px;
377
+ }
378
+ .stat-sub { font-size: 11px; color: var(--text-muted); }
379
+
380
+ /* ════════════════════════════════════════
381
+ TABLE
382
+ ════════════════════════════════════════ */
383
+ .table-wrap { overflow-x: auto; }
384
+ table { width: 100%; border-collapse: collapse; }
385
+ th {
386
+ text-align: left;
387
+ padding: 9px 16px;
388
+ font-size: 11px;
389
+ font-weight: 600;
390
+ text-transform: uppercase;
391
+ letter-spacing: 0.5px;
392
+ color: var(--text-muted);
393
+ background: var(--surface2);
394
+ border-bottom: 1px solid var(--border);
395
+ white-space: nowrap;
396
+ }
397
+ th.sortable { cursor: pointer; user-select: none; }
398
+ th.sortable:hover { color: var(--text-soft); }
399
+ th.sort-active { color: var(--primary); }
400
+ .sort-indicator { display: inline-flex; flex-direction: column; gap: 1px; margin-left: 4px; vertical-align: middle; }
401
+ .sort-indicator span { width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; opacity: .3; }
402
+ .sort-indicator .up { border-bottom: 5px solid currentColor; }
403
+ .sort-indicator .down { border-top: 5px solid currentColor; }
404
+ th.sort-asc .sort-indicator .up { opacity: 1; }
405
+ th.sort-desc .sort-indicator .down { opacity: 1; }
406
+ td {
407
+ padding: 11px 16px;
408
+ font-size: 13px;
409
+ border-bottom: 1px solid var(--border-soft);
410
+ vertical-align: middle;
411
+ color: var(--text-soft);
412
+ }
413
+ tr:last-child td { border-bottom: none; }
414
+ tr:hover td { background: var(--surface2); }
415
+ .col-check { width: 44px; }
416
+ .col-actions { width: 100px; text-align: right; }
417
+ .td-primary { color: var(--text); font-weight: 500; }
418
+
419
+ /* ── Row checkbox ── */
420
+ .row-check {
421
+ width: 16px; height: 16px;
422
+ cursor: pointer;
423
+ accent-color: var(--primary);
424
+ }
425
+
426
+ /* ── Action menu ── */
427
+ .action-menu { position: relative; display: inline-block; }
428
+ .action-menu-btn {
429
+ background: none;
430
+ border: 1px solid var(--border);
431
+ border-radius: var(--radius-sm);
432
+ padding: 4px 8px;
433
+ cursor: pointer;
434
+ color: var(--text-muted);
435
+ display: flex;
436
+ align-items: center;
437
+ gap: 3px;
438
+ font-size: 12px;
439
+ transition: all .12s;
440
+ }
441
+ .action-menu-btn:hover { background: var(--surface2); color: var(--text-soft); border-color: var(--border); }
442
+ .action-dropdown {
443
+ position: absolute;
444
+ right: 0;
445
+ top: calc(100% + 4px);
446
+ background: var(--surface);
447
+ border: 1px solid var(--border);
448
+ border-radius: var(--radius);
449
+ box-shadow: var(--shadow-lg);
450
+ min-width: 140px;
451
+ z-index: 50;
452
+ overflow: hidden;
453
+ display: none;
454
+ }
455
+ .action-dropdown.open { display: block; }
456
+ .action-dropdown a,
457
+ .action-dropdown button {
458
+ display: flex;
459
+ align-items: center;
460
+ gap: 8px;
461
+ padding: 8px 14px;
462
+ font-size: 13px;
463
+ color: var(--text-soft);
464
+ text-decoration: none;
465
+ background: none;
466
+ border: none;
467
+ width: 100%;
468
+ text-align: left;
469
+ cursor: pointer;
470
+ font-family: inherit;
471
+ transition: background .1s;
472
+ }
473
+ .action-dropdown a:hover,
474
+ .action-dropdown button:hover { background: var(--surface2); }
475
+ .action-dropdown .sep { height: 1px; background: var(--border-soft); margin: 3px 0; }
476
+ .action-dropdown .danger { color: var(--danger); }
477
+
478
+ /* ════════════════════════════════════════
479
+ BADGES
480
+ ════════════════════════════════════════ */
481
+ .badge {
482
+ display: inline-flex;
483
+ align-items: center;
484
+ gap: 4px;
485
+ padding: 2px 8px;
486
+ border-radius: 99px;
487
+ font-size: 11.5px;
488
+ font-weight: 600;
489
+ white-space: nowrap;
490
+ border: 1px solid transparent;
491
+ }
492
+ .badge-blue { background: #eff6ff; color: #2563eb; border-color: #bfdbfe; }
493
+ .badge-red { background: #fef2f2; color: #ef4444; border-color: #fecaca; }
494
+ .badge-green { background: #ecfdf5; color: #10b981; border-color: #a7f3d0; }
495
+ .badge-yellow { background: #fffbeb; color: #f59e0b; border-color: #fde68a; }
496
+ .badge-purple { background: #faf5ff; color: #a855f7; border-color: #e9d5ff; }
497
+ .badge-gray { background: #f9fafb; color: #6b7280; border-color: #e5e7eb; }
498
+ .badge-orange { background: #fff4e6; color: #f6821f; border-color: #ffe4c4; }
499
+
500
+ /* ════════════════════════════════════════
501
+ BUTTONS
502
+ ════════════════════════════════════════ */
503
+ .btn {
504
+ display: inline-flex;
505
+ align-items: center;
506
+ gap: 6px;
507
+ padding: 7px 14px;
508
+ border-radius: var(--radius-sm);
509
+ font-size: 13px;
510
+ font-weight: 500;
511
+ cursor: pointer;
512
+ border: 1px solid transparent;
513
+ transition: all .12s;
514
+ text-decoration: none;
515
+ font-family: inherit;
516
+ line-height: 1;
517
+ white-space: nowrap;
518
+ }
519
+ .btn-primary {
520
+ background: linear-gradient(135deg, #f6821f 0%, #ff9a3c 100%);
521
+ color: #fff;
522
+ border-color: var(--primary);
523
+ box-shadow: 0 1px 3px rgba(246,130,31,.25);
524
+ }
525
+ .btn-primary:hover { background: var(--primary-h); border-color: var(--primary-h); }
526
+ .btn-ghost {
527
+ background: transparent;
528
+ color: var(--text-soft);
529
+ border-color: var(--border);
530
+ }
531
+ .btn-ghost:hover { background: var(--surface2); color: var(--text); }
532
+ .btn-danger {
533
+ background: transparent;
534
+ color: var(--danger);
535
+ border-color: var(--border);
536
+ }
537
+ .btn-danger:hover { background: var(--danger-bg); border-color: var(--danger-border); }
538
+ .btn-success {
539
+ background: var(--success);
540
+ color: #fff;
541
+ border-color: var(--success);
542
+ }
543
+ .btn-success:hover { opacity: .9; }
544
+ .btn-sm { padding: 5px 10px; font-size: 12px; }
545
+ .btn-xs { padding: 3px 8px; font-size: 11px; }
546
+ .btn-icon { padding: 6px; }
547
+ .btn:disabled { opacity: .5; cursor: not-allowed; pointer-events: none; }
548
+
549
+ /* ════════════════════════════════════════
550
+ FORMS
551
+ ════════════════════════════════════════ */
552
+ .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
553
+ .form-group { display: flex; flex-direction: column; gap: 5px; }
554
+ .form-group.full { grid-column: 1 / -1; }
555
+ .form-group.w-third { grid-column: span 1; }
556
+ .form-label { font-size: 12.5px; font-weight: 500; color: var(--text-soft); }
557
+ .form-label .required { color: var(--danger); margin-left: 2px; }
558
+ .form-control {
559
+ background: var(--surface);
560
+ border: 1px solid var(--border);
561
+ color: var(--text);
562
+ border-radius: var(--radius-sm);
563
+ padding: 8px 11px;
564
+ font-size: 13.5px;
565
+ width: 100%;
566
+ outline: none;
567
+ font-family: inherit;
568
+ transition: border .12s, box-shadow .12s;
569
+ }
570
+ .form-control:hover { border-color: #c4c9d4; }
571
+ .form-control:focus {
572
+ border-color: var(--primary);
573
+ box-shadow: 0 0 0 3px var(--primary-soft);
574
+ }
575
+ .form-control.error {
576
+ border-color: var(--danger);
577
+ box-shadow: 0 0 0 3px var(--danger-bg);
578
+ }
579
+ .form-control::placeholder { color: var(--text-xmuted); }
580
+ select.form-control { cursor: pointer; }
581
+ textarea.form-control { resize: vertical; min-height: 90px; }
582
+ .form-help { font-size: 11.5px; color: var(--text-muted); }
583
+ .form-error { font-size: 11.5px; color: var(--danger); display: flex; align-items: center; gap: 4px; }
584
+
585
+ /* ── Checkbox / radio ── */
586
+ .check-group { display: flex; align-items: center; gap: 8px; }
587
+ .check-input {
588
+ width: 16px; height: 16px;
589
+ accent-color: var(--primary);
590
+ cursor: pointer;
591
+ }
592
+ .check-label { font-size: 13.5px; color: var(--text-soft); cursor: pointer; }
593
+
594
+ /* ── Search ── */
595
+ .search-wrap { position: relative; }
596
+ .search-icon-inner {
597
+ position: absolute;
598
+ left: 10px;
599
+ top: 50%;
600
+ transform: translateY(-50%);
601
+ color: var(--text-muted);
602
+ pointer-events: none;
603
+ font-size: 14px;
604
+ display: flex;
605
+ }
606
+ .search-input { padding-left: 34px !important; width: 220px; }
607
+
608
+ /* ════════════════════════════════════════
609
+ TOOLBAR (list page)
610
+ ════════════════════════════════════════ */
611
+ .toolbar {
612
+ display: flex;
613
+ align-items: center;
614
+ gap: 8px;
615
+ padding: 12px 18px;
616
+ border-bottom: 1px solid var(--border-soft);
617
+ flex-wrap: wrap;
618
+ }
619
+ .toolbar-left { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
620
+ .toolbar-right { display: flex; align-items: center; gap: 8px; }
621
+
622
+ /* ── Bulk action bar ── */
623
+ .bulk-bar {
624
+ display: none;
625
+ align-items: center;
626
+ gap: 10px;
627
+ padding: 10px 18px;
628
+ background: var(--primary-soft);
629
+ border-bottom: 1px solid var(--primary-dim);
630
+ font-size: 13px;
631
+ }
632
+ .bulk-bar.visible { display: flex; }
633
+ .bulk-count { font-weight: 600; color: var(--primary); }
634
+
635
+ /* ════════════════════════════════════════
636
+ FILTER PANEL
637
+ ════════════════════════════════════════ */
638
+ .filter-row {
639
+ display: flex;
640
+ align-items: flex-end;
641
+ gap: 10px;
642
+ padding: 12px 18px;
643
+ border-bottom: 1px solid var(--border-soft);
644
+ flex-wrap: wrap;
645
+ background: var(--surface2);
646
+ }
647
+ .filter-group { display: flex; flex-direction: column; gap: 4px; }
648
+ .filter-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .4px; color: var(--text-muted); }
649
+ .filter-control {
650
+ background: var(--surface);
651
+ border: 1px solid var(--border);
652
+ color: var(--text);
653
+ border-radius: var(--radius-sm);
654
+ padding: 6px 10px;
655
+ font-size: 12.5px;
656
+ outline: none;
657
+ font-family: inherit;
658
+ transition: border .12s;
659
+ }
660
+ .filter-control:focus { border-color: var(--primary); }
661
+
662
+ /* ════════════════════════════════════════
663
+ PAGINATION
664
+ ════════════════════════════════════════ */
665
+ .pagination {
666
+ display: flex;
667
+ align-items: center;
668
+ gap: 3px;
669
+ padding: 12px 18px;
670
+ border-top: 1px solid var(--border-soft);
671
+ flex-wrap: wrap;
672
+ }
673
+ .page-info { font-size: 12px; color: var(--text-muted); margin-left: auto; }
674
+ .page-btn {
675
+ min-width: 30px;
676
+ height: 30px;
677
+ padding: 0 6px;
678
+ display: inline-flex;
679
+ align-items: center;
680
+ justify-content: center;
681
+ border-radius: var(--radius-sm);
682
+ border: 1px solid var(--border);
683
+ background: transparent;
684
+ color: var(--text-soft);
685
+ cursor: pointer;
686
+ font-size: 12px;
687
+ font-family: inherit;
688
+ transition: all .12s;
689
+ text-decoration: none;
690
+ }
691
+ .page-btn:hover:not(:disabled) { background: var(--surface2); color: var(--text); border-color: #c4c9d4; }
692
+ .page-btn.active { background: var(--primary); border-color: var(--primary); color: #fff; }
693
+ .page-btn:disabled { opacity: .35; cursor: not-allowed; }
694
+ .page-ellipsis { color: var(--text-muted); font-size: 12px; padding: 0 3px; }
695
+
696
+ /* ════════════════════════════════════════
697
+ MODAL
698
+ ════════════════════════════════════════ */
699
+ .modal-overlay {
700
+ position: fixed;
701
+ inset: 0;
702
+ background: rgba(17,24,39,.5);
703
+ display: flex;
704
+ align-items: center;
705
+ justify-content: center;
706
+ z-index: 200;
707
+ padding: 24px;
708
+ opacity: 0;
709
+ pointer-events: none;
710
+ transition: opacity .18s;
711
+ backdrop-filter: blur(2px);
712
+ }
713
+ .modal-overlay.open { opacity: 1; pointer-events: all; }
714
+ .modal {
715
+ background: var(--surface);
716
+ border: 1px solid var(--border);
717
+ border-radius: var(--radius-lg);
718
+ width: 100%;
719
+ max-width: 520px;
720
+ max-height: 90vh;
721
+ overflow-y: auto;
722
+ transform: translateY(10px) scale(.99);
723
+ transition: transform .2s;
724
+ box-shadow: var(--shadow-lg);
725
+ }
726
+ .modal-overlay.open .modal { transform: translateY(0) scale(1); }
727
+ .modal-sm { max-width: 400px; }
728
+ .modal-lg { max-width: 700px; }
729
+ .modal-header {
730
+ padding: 18px 22px;
731
+ border-bottom: 1px solid var(--border-soft);
732
+ display: flex;
733
+ justify-content: space-between;
734
+ align-items: center;
735
+ position: sticky;
736
+ top: 0;
737
+ background: var(--surface);
738
+ z-index: 1;
739
+ }
740
+ .modal-title { font-size: 15px; font-weight: 600; color: var(--text); }
741
+ .modal-body { padding: 20px 22px; }
742
+ .modal-footer {
743
+ padding: 14px 22px;
744
+ border-top: 1px solid var(--border-soft);
745
+ display: flex;
746
+ justify-content: flex-end;
747
+ gap: 8px;
748
+ position: sticky;
749
+ bottom: 0;
750
+ background: var(--surface);
751
+ }
752
+ .close-btn {
753
+ background: none;
754
+ border: none;
755
+ color: var(--text-muted);
756
+ cursor: pointer;
757
+ padding: 4px;
758
+ line-height: 1;
759
+ border-radius: 4px;
760
+ display: flex;
761
+ align-items: center;
762
+ transition: background .1s, color .1s;
763
+ }
764
+ .close-btn:hover { background: var(--surface3); color: var(--text); }
765
+
766
+ /* ════════════════════════════════════════
767
+ EMPTY STATE
768
+ ════════════════════════════════════════ */
769
+ .empty-state {
770
+ text-align: center;
771
+ padding: 56px 24px;
772
+ color: var(--text-muted);
773
+ }
774
+ .empty-icon {
775
+ width: 52px;
776
+ height: 52px;
777
+ background: var(--surface3);
778
+ border-radius: 14px;
779
+ display: flex;
780
+ align-items: center;
781
+ justify-content: center;
782
+ margin: 0 auto 16px;
783
+ color: var(--text-xmuted);
784
+ }
785
+ .empty-title { font-size: 15px; font-weight: 600; color: var(--text-soft); margin-bottom: 7px; }
786
+ .empty-desc { font-size: 13px; max-width: 320px; margin: 0 auto 20px; line-height: 1.6; }
787
+
788
+ /* ════════════════════════════════════════
789
+ CELL TYPES
790
+ ════════════════════════════════════════ */
791
+ .bool-yes {
792
+ display: inline-flex; align-items: center; justify-content: center;
793
+ width: 20px; height: 20px; border-radius: 99px;
794
+ background: var(--success-bg); color: var(--success);
795
+ }
796
+ .bool-no {
797
+ display: inline-flex; align-items: center; justify-content: center;
798
+ width: 20px; height: 20px; border-radius: 99px;
799
+ background: var(--danger-bg); color: var(--danger);
800
+ }
801
+ .cell-muted { color: var(--text-xmuted); font-style: italic; }
802
+ .cell-mono { font-family: 'DM Mono', monospace; font-size: 12px; }
803
+ .cell-image { width: 34px; height: 34px; border-radius: 7px; object-fit: cover; border: 1px solid var(--border); }
804
+
805
+ /* ════════════════════════════════════════
806
+ TOAST
807
+ ════════════════════════════════════════ */
808
+ #toast-root {
809
+ position: fixed;
810
+ bottom: 22px;
811
+ right: 22px;
812
+ display: flex;
813
+ flex-direction: column;
814
+ gap: 8px;
815
+ z-index: 500;
816
+ pointer-events: none;
817
+ }
818
+ .toast {
819
+ background: var(--text);
820
+ color: #fff;
821
+ border-radius: var(--radius);
822
+ padding: 11px 16px;
823
+ font-size: 13px;
824
+ max-width: 320px;
825
+ box-shadow: var(--shadow-lg);
826
+ pointer-events: all;
827
+ display: flex;
828
+ align-items: center;
829
+ gap: 9px;
830
+ animation: toastIn .2s ease;
831
+ }
832
+ .toast-success .toast-dot { color: #4ade80; }
833
+ .toast-error .toast-dot { color: #f87171; }
834
+ @keyframes toastIn { from { transform: translateX(16px); opacity: 0; } }
835
+
836
+ /* ════════════════════════════════════════
837
+ UTILITIES
838
+ ════════════════════════════════════════ */
839
+ .flex { display: flex; }
840
+ .flex-col { flex-direction: column; }
841
+ .items-center { align-items: center; }
842
+ .justify-between { justify-content: space-between; }
843
+ .gap-1 { gap: 4px; }
844
+ .gap-2 { gap: 8px; }
845
+ .gap-3 { gap: 12px; }
846
+ .gap-4 { gap: 16px; }
847
+ .ml-auto { margin-left: auto; }
848
+ .text-muted { color: var(--text-muted); }
849
+ .text-sm { font-size: 12px; }
850
+ .text-xs { font-size: 11px; }
851
+ .fw-500 { font-weight: 500; }
852
+ .fw-600 { font-weight: 600; }
853
+ .mb-3 { margin-bottom: 12px; }
854
+ .mb-4 { margin-bottom: 16px; }
855
+ .mb-5 { margin-bottom: 20px; }
856
+ .mb-6 { margin-bottom: 24px; }
857
+ .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
858
+
859
+ /* ════════════════════════════════════════
860
+ RESPONSIVE
861
+ ════════════════════════════════════════ */
862
+ @media (max-width: 768px) {
863
+ #sidebar { display: none; }
864
+ .form-grid { grid-template-columns: 1fr; }
865
+ .search-input { width: 160px; }
866
+ }
867
+
868
+ /* ── Form page ────────────────────────────────────────────── */
869
+ /* ── Tab nav (form) ── */
870
+ .tab-nav {
871
+ display: flex;
872
+ gap: 0;
873
+ overflow-x: auto;
874
+ }
875
+ .tab-btn {
876
+ padding: 10px 18px;
877
+ font-size: 13.5px;
878
+ font-weight: 500;
879
+ color: var(--text-muted);
880
+ background: none;
881
+ border: none;
882
+ border-bottom: 2px solid transparent;
883
+ margin-bottom: -2px;
884
+ cursor: pointer;
885
+ font-family: inherit;
886
+ white-space: nowrap;
887
+ transition: color .12s, border-color .12s;
888
+ }
889
+ .tab-btn:hover { color: var(--text-soft); }
890
+ .tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); background: var(--primary-soft); }
891
+
892
+ .tab-form-panel { display: none; }
893
+ .tab-form-panel.active { display: block; }
894
+
895
+ /* ── Readonly ── */
896
+ .readonly-value {
897
+ font-size: 13.5px;
898
+ color: var(--text-soft);
899
+ padding: 8px 12px;
900
+ background: var(--surface2);
901
+ border: 1px solid var(--border-soft);
902
+ border-radius: var(--radius-sm);
903
+ min-height: 36px;
904
+ display: flex;
905
+ align-items: center;
906
+ gap: 8px;
907
+ }
908
+ .readonly-pre {
909
+ background: var(--surface2);
910
+ border: 1px solid var(--border);
911
+ border-radius: var(--radius-sm);
912
+ padding: 10px;
913
+ font-family: 'DM Mono', monospace;
914
+ font-size: 12px;
915
+ overflow-x: auto;
916
+ white-space: pre-wrap;
917
+ margin: 0;
918
+ color: var(--text-soft);
919
+ }
920
+
921
+ /* ── Password toggle ── */
922
+ .pw-toggle {
923
+ position: absolute;
924
+ right: 10px;
925
+ top: 50%;
926
+ transform: translateY(-50%);
927
+ background: none;
928
+ border: none;
929
+ cursor: pointer;
930
+ color: var(--text-muted);
931
+ display: flex;
932
+ align-items: center;
933
+ padding: 0;
934
+ }
935
+ .pw-toggle:hover { color: var(--text-soft); }
936
+
937
+ /* ── Richtext ── */
938
+ .richtext-wrap { border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
939
+ .richtext-toolbar {
940
+ display: flex;
941
+ align-items: center;
942
+ gap: 2px;
943
+ padding: 6px 8px;
944
+ background: var(--surface2);
945
+ border-bottom: 1px solid var(--border);
946
+ }
947
+ .richtext-toolbar button {
948
+ width: 28px; height: 28px;
949
+ display: flex; align-items: center; justify-content: center;
950
+ background: none;
951
+ border: 1px solid transparent;
952
+ border-radius: 4px;
953
+ cursor: pointer;
954
+ font-size: 13px;
955
+ color: var(--text-soft);
956
+ font-family: inherit;
957
+ transition: all .1s;
958
+ }
959
+ .richtext-toolbar button:hover { background: var(--surface3); border-color: var(--border); }
960
+ .rt-sep { width: 1px; height: 18px; background: var(--border); margin: 0 4px; }
961
+ .richtext-editor {
962
+ border: none !important;
963
+ border-radius: 0 !important;
964
+ outline: none !important;
965
+ box-shadow: none !important;
966
+ padding: 12px;
967
+ line-height: 1.6;
968
+ }
969
+ .richtext-editor:focus { outline: none; }
970
+
971
+ /* ── Field feedback ── */
972
+ .field-feedback:empty { display: none; }
973
+
974
+ @keyframes spin { to { transform: rotate(360deg); } }
975
+
976
+ /* ══════════════════════════════════════════
977
+ FK DROPDOWN WIDGET
978
+ ══════════════════════════════════════════ */
979
+ .fk-widget {
980
+ position: relative;
981
+ }
982
+
983
+ /* Trigger button — styled like form-control */
984
+ .fk-trigger {
985
+ width: 100%;
986
+ display: flex;
987
+ align-items: center;
988
+ justify-content: space-between;
989
+ gap: 8px;
990
+ text-align: left;
991
+ cursor: pointer;
992
+ background: var(--surface);
993
+ user-select: none;
994
+ padding: 0 12px;
995
+ height: 36px;
996
+ font-size: 13.5px;
997
+ color: var(--text);
998
+ transition: border-color .12s, box-shadow .12s;
999
+ }
1000
+ .fk-trigger:hover { border-color: var(--primary); }
1001
+ .fk-trigger[aria-expanded="true"] {
1002
+ border-color: var(--primary);
1003
+ box-shadow: 0 0 0 3px var(--primary-soft);
1004
+ border-bottom-left-radius: 0;
1005
+ border-bottom-right-radius: 0;
1006
+ }
1007
+ .fk-trigger-label {
1008
+ display: flex;
1009
+ align-items: center;
1010
+ gap: 6px;
1011
+ flex: 1;
1012
+ min-width: 0;
1013
+ overflow: hidden;
1014
+ white-space: nowrap;
1015
+ text-overflow: ellipsis;
1016
+ }
1017
+ .fk-placeholder { color: var(--text-muted); font-size: 13px; }
1018
+ .fk-id-chip {
1019
+ display: inline-flex;
1020
+ align-items: center;
1021
+ padding: 1px 6px;
1022
+ background: var(--surface2);
1023
+ border: 1px solid var(--border);
1024
+ border-radius: 4px;
1025
+ font-size: 11px;
1026
+ font-family: 'DM Mono', monospace;
1027
+ color: var(--text-muted);
1028
+ flex-shrink: 0;
1029
+ }
1030
+ .fk-selected-label {
1031
+ color: var(--text);
1032
+ font-size: 13.5px;
1033
+ overflow: hidden;
1034
+ text-overflow: ellipsis;
1035
+ }
1036
+ .fk-loading-label { color: var(--text-muted); font-size: 13px; font-style: italic; }
1037
+ .fk-chevron {
1038
+ flex-shrink: 0;
1039
+ color: var(--text-muted);
1040
+ transition: transform .15s;
1041
+ }
1042
+ .fk-trigger[aria-expanded="true"] .fk-chevron { transform: rotate(180deg); }
1043
+
1044
+ /* Dropdown panel — rendered in portal via UI.Dropdown, position:fixed */
1045
+ .fk-panel {
1046
+ background: var(--surface);
1047
+ border: 1px solid var(--primary);
1048
+ border-radius: var(--radius-sm);
1049
+ box-shadow: 0 8px 24px rgba(0,0,0,.12);
1050
+ overflow: hidden;
1051
+ min-width: 240px;
1052
+ }
1053
+
1054
+ /* Search bar */
1055
+ .fk-search-wrap {
1056
+ display: flex;
1057
+ align-items: center;
1058
+ gap: 8px;
1059
+ padding: 8px 10px;
1060
+ border-bottom: 1px solid var(--border-soft);
1061
+ background: var(--surface2);
1062
+ }
1063
+ .fk-search-wrap svg { color: var(--text-muted); flex-shrink: 0; }
1064
+ .fk-search {
1065
+ flex: 1;
1066
+ border: none;
1067
+ background: transparent;
1068
+ font-size: 13px;
1069
+ color: var(--text);
1070
+ outline: none;
1071
+ font-family: inherit;
1072
+ padding: 0;
1073
+ }
1074
+ .fk-search::placeholder { color: var(--text-muted); }
1075
+ .fk-search-clear {
1076
+ display: flex;
1077
+ align-items: center;
1078
+ justify-content: center;
1079
+ background: none;
1080
+ border: none;
1081
+ cursor: pointer;
1082
+ color: var(--text-muted);
1083
+ padding: 2px;
1084
+ border-radius: 3px;
1085
+ transition: color .1s, background .1s;
1086
+ }
1087
+ .fk-search-clear:hover { color: var(--text); background: var(--surface3); }
1088
+ .fk-search-clear[hidden] { display: none; }
1089
+
1090
+ /* Options list */
1091
+ .fk-list {
1092
+ max-height: 220px;
1093
+ overflow-y: auto;
1094
+ overscroll-behavior: contain;
1095
+ }
1096
+ .fk-option {
1097
+ display: flex;
1098
+ align-items: center;
1099
+ gap: 8px;
1100
+ padding: 7px 12px;
1101
+ cursor: pointer;
1102
+ font-size: 13.5px;
1103
+ color: var(--text);
1104
+ transition: background .08s;
1105
+ outline: none;
1106
+ }
1107
+ .fk-option:hover,
1108
+ .fk-option.fk-focused { background: var(--primary-soft); }
1109
+ .fk-option.fk-selected {
1110
+ background: var(--primary-soft);
1111
+ font-weight: 500;
1112
+ }
1113
+ .fk-option.fk-selected .fk-id-chip { background: var(--primary); color: #fff; border-color: var(--primary); }
1114
+ .fk-opt-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1115
+ .fk-check {
1116
+ margin-left: auto;
1117
+ color: var(--primary);
1118
+ flex-shrink: 0;
1119
+ }
1120
+
1121
+ /* States */
1122
+ .fk-empty-row {
1123
+ display: flex;
1124
+ flex-direction: column;
1125
+ align-items: center;
1126
+ justify-content: center;
1127
+ gap: 10px;
1128
+ padding: 24px 12px;
1129
+ color: var(--text-muted);
1130
+ font-size: 13px;
1131
+ text-align: center;
1132
+ line-height: 1.5;
1133
+ }
1134
+ .fk-spinner-row {
1135
+ display: flex;
1136
+ align-items: center;
1137
+ justify-content: center;
1138
+ gap: 8px;
1139
+ padding: 20px 12px;
1140
+ color: var(--text-muted);
1141
+ font-size: 13px;
1142
+ }
1143
+ @keyframes fk-spin { to { transform: rotate(360deg); } }
1144
+ .fk-spin { animation: fk-spin .7s linear infinite; }
1145
+
1146
+ /* Skeleton loader rows */
1147
+ .fk-skeleton-row {
1148
+ display: flex;
1149
+ align-items: center;
1150
+ gap: 8px;
1151
+ padding: 8px 12px;
1152
+ }
1153
+ .fk-skel {
1154
+ display: inline-block;
1155
+ border-radius: 4px;
1156
+ background: linear-gradient(90deg, var(--surface2) 25%, var(--surface3) 50%, var(--surface2) 75%);
1157
+ background-size: 200% 100%;
1158
+ animation: fk-shimmer 1.4s infinite;
1159
+ }
1160
+ .fk-skel-chip { width: 32px; height: 18px; border-radius: 4px; flex-shrink: 0; }
1161
+ .fk-skel-text { height: 13px; width: 65%; border-radius: 3px; }
1162
+ @keyframes fk-shimmer {
1163
+ 0% { background-position: 200% 0; }
1164
+ 100% { background-position: -200% 0; }
1165
+ }
1166
+
1167
+ /* Footer */
1168
+ .fk-footer {
1169
+ display: flex;
1170
+ align-items: center;
1171
+ justify-content: space-between;
1172
+ padding: 6px 10px;
1173
+ border-top: 1px solid var(--border-soft);
1174
+ background: var(--surface2);
1175
+ }
1176
+ .fk-footer[hidden] { display: none; }
1177
+ .fk-count { font-size: 11.5px; color: var(--text-muted); }
1178
+ .fk-pages { display: flex; align-items: center; gap: 4px; }
1179
+ .fk-page-btn {
1180
+ display: flex; align-items: center; justify-content: center;
1181
+ width: 24px; height: 24px;
1182
+ background: none;
1183
+ border: 1px solid var(--border);
1184
+ border-radius: var(--radius-sm);
1185
+ cursor: pointer;
1186
+ color: var(--text-soft);
1187
+ transition: all .1s;
1188
+ }
1189
+ .fk-page-btn:hover:not(:disabled) { border-color: var(--primary); color: var(--primary); background: var(--primary-soft); }
1190
+ .fk-page-btn:disabled { opacity: .35; cursor: not-allowed; }
1191
+ .fk-page-info { font-size: 11.5px; color: var(--text-muted); min-width: 40px; text-align: center; }
1192
+
1193
+ /* Clear row */
1194
+ .fk-clear-row {
1195
+ border-top: 1px solid var(--border-soft);
1196
+ padding: 6px 10px;
1197
+ background: var(--surface2);
1198
+ }
1199
+ .fk-clear-btn {
1200
+ display: flex; align-items: center; gap: 5px;
1201
+ background: none; border: none; cursor: pointer;
1202
+ font-size: 12px; color: var(--text-muted);
1203
+ font-family: inherit;
1204
+ padding: 3px 6px;
1205
+ border-radius: var(--radius-sm);
1206
+ transition: color .1s, background .1s;
1207
+ }
1208
+ .fk-clear-btn:hover { color: var(--danger, #dc2626); background: var(--danger-soft, #fef2f2); }
1209
+
1210
+ .fieldset-heading {
1211
+ font-size: 11.5px;
1212
+ font-weight: 700;
1213
+ text-transform: uppercase;
1214
+ letter-spacing: .6px;
1215
+ color: var(--text-muted);
1216
+ padding: 16px 0 10px;
1217
+ border-bottom: 1px solid var(--border-soft);
1218
+ margin-bottom: 2px;
1219
+ grid-column: 1 / -1;
1220
+ }
1221
+
1222
+ /* ── List page ────────────────────────────────────────────── */
1223
+ /* ── UI Menu (portal dropdown) ── */
1224
+ .ui-menu { position: relative; display: inline-block; }
1225
+
1226
+ .ui-menu-panel {
1227
+ background: var(--surface);
1228
+ border: 1px solid var(--border);
1229
+ border-radius: var(--radius);
1230
+ min-width: 150px;
1231
+ overflow: hidden;
1232
+ }
1233
+
1234
+ .ui-menu-item {
1235
+ display: flex;
1236
+ align-items: center;
1237
+ gap: 8px;
1238
+ padding: 8px 14px;
1239
+ font-size: 13px;
1240
+ color: var(--text-soft);
1241
+ text-decoration: none;
1242
+ background: none;
1243
+ border: none;
1244
+ width: 100%;
1245
+ text-align: left;
1246
+ cursor: pointer;
1247
+ font-family: inherit;
1248
+ transition: background .1s;
1249
+ white-space: nowrap;
1250
+ }
1251
+ .ui-menu-item:hover { background: var(--surface2); color: var(--text); }
1252
+ .ui-menu-danger { color: var(--danger); }
1253
+ .ui-menu-danger:hover { background: var(--danger-bg); color: var(--danger); }
1254
+
1255
+ .ui-menu-sep { height: 1px; background: var(--border-soft); margin: 3px 0; }
1256
+
1257
+ /* ═══════════════════════════════════════════════════════════════
1258
+ TOGGLE SWITCH (boolean / checkbox fields)
1259
+ ═══════════════════════════════════════════════════════════════ */
1260
+
1261
+ .toggle-field {
1262
+ display: flex;
1263
+ flex-direction: column;
1264
+ gap: 4px;
1265
+ padding: 6px 0;
1266
+ }
1267
+
1268
+ .toggle-wrap {
1269
+ display: inline-flex;
1270
+ align-items: center;
1271
+ gap: 10px;
1272
+ cursor: pointer;
1273
+ user-select: none;
1274
+ }
1275
+
1276
+ /* Visually hidden but still in flow so the + sibling selector works reliably */
1277
+ .toggle-input {
1278
+ position: absolute;
1279
+ opacity: 0;
1280
+ width: 1px;
1281
+ height: 1px;
1282
+ margin: -1px;
1283
+ overflow: hidden;
1284
+ clip: rect(0,0,0,0);
1285
+ white-space: nowrap;
1286
+ }
1287
+
1288
+ .toggle-track {
1289
+ position: relative;
1290
+ display: inline-block;
1291
+ width: 40px;
1292
+ height: 24px;
1293
+ border-radius: 99px;
1294
+ background: var(--border);
1295
+ transition: background .18s, box-shadow .18s;
1296
+ flex-shrink: 0;
1297
+ vertical-align: middle;
1298
+ }
1299
+
1300
+ /* :checked state — the input and track are adjacent siblings inside the label */
1301
+ .toggle-input:checked ~ .toggle-track {
1302
+ background: var(--primary);
1303
+ }
1304
+
1305
+ .toggle-thumb {
1306
+ position: absolute;
1307
+ top: 3px;
1308
+ left: 3px;
1309
+ width: 18px;
1310
+ height: 18px;
1311
+ border-radius: 50%;
1312
+ background: #fff;
1313
+ box-shadow: 0 1px 4px rgba(0,0,0,.25);
1314
+ transition: transform .18s;
1315
+ pointer-events: none;
1316
+ }
1317
+
1318
+ .toggle-input:checked ~ .toggle-track .toggle-thumb {
1319
+ transform: translateX(16px);
1320
+ }
1321
+
1322
+ .toggle-label {
1323
+ font-size: 13.5px;
1324
+ color: var(--text-soft);
1325
+ font-weight: 500;
1326
+ line-height: 1;
1327
+ }
1328
+
1329
+ /* Focus ring */
1330
+ .toggle-input:focus-visible ~ .toggle-track {
1331
+ box-shadow: 0 0 0 3px var(--primary-soft);
1332
+ }
1333
+
1334
+ /* Disabled state */
1335
+ .toggle-input:disabled ~ .toggle-track {
1336
+ opacity: .5;
1337
+ cursor: not-allowed;
1338
+ }
1339
+ .toggle-input:disabled ~ .toggle-label {
1340
+ opacity: .5;
1341
+ }
1342
+
1343
+ /* ─────────────────────────────────────────────────────────────────
1344
+ Thin themed scrollbars
1345
+ ───────────────────────────────────────────────────────────────── */
1346
+
1347
+ /* Webkit (Chrome, Edge, Safari) */
1348
+ ::-webkit-scrollbar {
1349
+ width: 5px;
1350
+ height: 5px;
1351
+ }
1352
+ ::-webkit-scrollbar-track {
1353
+ background: transparent;
1354
+ }
1355
+ ::-webkit-scrollbar-thumb {
1356
+ background: var(--primary-dim);
1357
+ border-radius: 99px;
1358
+ }
1359
+ ::-webkit-scrollbar-thumb:hover {
1360
+ background: var(--primary);
1361
+ }
1362
+ ::-webkit-scrollbar-corner {
1363
+ background: transparent;
1364
+ }
1365
+
1366
+ /* Firefox */
1367
+ * {
1368
+ scrollbar-width: thin;
1369
+ scrollbar-color: var(--primary-dim) transparent;
1370
+ }
1371
+ *:hover {
1372
+ scrollbar-color: var(--primary) transparent;
1373
+ }
1374
+
1375
+
1376
+ /* ─────────────────────────────────────────────────────────────────
1377
+ FK cells — floating arrow button appears on cell hover
1378
+ ───────────────────────────────────────────────────────────────── */
1379
+
1380
+ /* The td needs position:relative — applied via the wrapper span signal */
1381
+ td:has(.fk-cell) {
1382
+ position: relative;
1383
+ }
1384
+
1385
+ .fk-cell {
1386
+ display: block;
1387
+ /* leave room so text never sits under the arrow */
1388
+ padding-right: 24px;
1389
+ }
1390
+
1391
+ .fk-arrow-btn {
1392
+ position: absolute;
1393
+ right: 8px;
1394
+ top: 50%;
1395
+ transform: translateY(-50%) translateX(4px);
1396
+ opacity: 0;
1397
+ transition: opacity .12s ease, transform .12s ease;
1398
+ color: var(--primary);
1399
+ display: inline-flex;
1400
+ align-items: center;
1401
+ justify-content: center;
1402
+ width: 22px;
1403
+ height: 22px;
1404
+ border-radius: var(--radius-sm);
1405
+ text-decoration: none;
1406
+ background: var(--primary-soft);
1407
+ border: 1px solid var(--primary-dim);
1408
+ pointer-events: none;
1409
+ }
1410
+
1411
+ /* Show on td hover */
1412
+ td:hover .fk-arrow-btn {
1413
+ opacity: 1;
1414
+ transform: translateY(-50%) translateX(0);
1415
+ pointer-events: auto;
1416
+ }
1417
+
1418
+ /* Detail page variant */
1419
+ .fk-cell-detail .fk-arrow-btn {
1420
+ width: 24px;
1421
+ height: 24px;
1422
+ }