careermate 0.1.0 → 0.1.2

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 (134) hide show
  1. package/README.md +22 -17
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/apps/mcp/src/index.ts +4 -3
  4. package/apps/web/DESIGN_GUIDE.md +2 -2
  5. package/apps/web/demo/demo-shim.js +536 -0
  6. package/apps/web/demo/demo.css +75 -0
  7. package/apps/web/demo/seed.js +269 -0
  8. package/apps/web/public/app.js +34 -2
  9. package/apps/web/public/icon-180.png +0 -0
  10. package/apps/web/public/index.html +14 -3
  11. package/apps/web/public/lib.js +135 -23
  12. package/apps/web/public/pages/applications.js +18 -9
  13. package/apps/web/public/pages/documents.js +66 -50
  14. package/apps/web/public/pages/home.js +12 -11
  15. package/apps/web/public/pages/interview.js +9 -12
  16. package/apps/web/public/pages/jobs.js +39 -36
  17. package/apps/web/public/pages/profile.js +66 -86
  18. package/apps/web/public/pages/settings.js +21 -14
  19. package/apps/web/public/styles.css +105 -38
  20. package/apps/web/src/http.ts +31 -4
  21. package/apps/web/src/info.ts +9 -22
  22. package/apps/web/src/routes.ts +19 -14
  23. package/apps/web/src/server.ts +34 -29
  24. package/apps/web/src/settings.ts +35 -17
  25. package/bin/careermate.mjs +8 -5
  26. package/dist/doctor.mjs +78908 -0
  27. package/dist/install-page/.omc/state/agent-replay-55a58c0d-bb6e-43ea-8a05-4b6c93b80b34.jsonl +12 -0
  28. package/dist/install-page/.omc/state/idle-notif-cooldown.json +3 -0
  29. package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/mission-state.json +147 -0
  30. package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/pre-tool-advisory-throttle.json +22 -0
  31. package/dist/install-page/.omc/state/sessions/55a58c0d-bb6e-43ea-8a05-4b6c93b80b34/subagent-tracking-state.json +44 -0
  32. package/dist/install-page/favicon.svg +1 -0
  33. package/dist/install-page/icon-180.png +0 -0
  34. package/dist/install-page/icon-512.png +0 -0
  35. package/dist/install-page/index.html +328 -225
  36. package/dist/install-page/landing.js +54 -0
  37. package/dist/install-page/llms-install.txt +202 -0
  38. package/dist/install-page/og.png +0 -0
  39. package/dist/install-page/shots/applications-dark.png +0 -0
  40. package/dist/install-page/shots/applications-light.png +0 -0
  41. package/dist/install-page/shots/documents-dark.png +0 -0
  42. package/dist/install-page/shots/documents-light.png +0 -0
  43. package/dist/install-page/shots/home-dark.png +0 -0
  44. package/dist/install-page/shots/home-light.png +0 -0
  45. package/dist/install-page/shots/interview-dark.png +0 -0
  46. package/dist/install-page/shots/interview-light.png +0 -0
  47. package/dist/install-page/shots/jobs-dark.png +0 -0
  48. package/dist/install-page/shots/jobs-light.png +0 -0
  49. package/dist/install-page/shots/settings-dark.png +0 -0
  50. package/dist/install-page/shots/settings-light.png +0 -0
  51. package/dist/install-page/start.html +323 -0
  52. package/dist/install-page/style.css +128 -4
  53. package/dist/mcp.mjs +74225 -2007
  54. package/dist/{mcpb-stage/dist/web.mjs → migrate.mjs} +142 -2660
  55. package/dist/pdf.worker.min.mjs +28 -0
  56. package/dist/public/app.js +34 -2
  57. package/dist/public/icon-180.png +0 -0
  58. package/dist/public/index.html +14 -3
  59. package/dist/public/lib.js +135 -23
  60. package/dist/public/pages/applications.js +18 -9
  61. package/dist/public/pages/documents.js +66 -50
  62. package/dist/public/pages/home.js +12 -11
  63. package/dist/public/pages/interview.js +9 -12
  64. package/dist/public/pages/jobs.js +39 -36
  65. package/dist/public/pages/profile.js +66 -86
  66. package/dist/public/pages/settings.js +21 -14
  67. package/dist/public/styles.css +105 -38
  68. package/dist/web.mjs +39950 -369
  69. package/docs/ARCHITECTURE.md +5 -5
  70. package/docs/DECISIONS.md +1 -1
  71. package/docs/DEMO.md +6 -6
  72. package/docs/MCP_TOOLS.md +68 -9
  73. package/docs/ROADMAP.md +11 -10
  74. package/docs/RUNTIME_DISTRIBUTION_RESEARCH.md +135 -0
  75. package/docs/START_WORKFLOW.md +19 -7
  76. package/docs/SUPPORTED_AI_APPS.md +85 -33
  77. package/docs/TODO.md +1 -1
  78. package/docs/WORKFLOWS.md +8 -6
  79. package/docs/superpowers/specs/2026-06-16-demo-dashboard-deploy-design.md +64 -0
  80. package/package.json +8 -3
  81. package/packages/core/src/services.ts +42 -1
  82. package/packages/db/src/connection.ts +30 -0
  83. package/packages/db/src/index.ts +1 -0
  84. package/packages/db/src/paths.ts +11 -0
  85. package/packages/db/src/repositories.ts +109 -63
  86. package/packages/db/src/schema.ts +118 -0
  87. package/packages/exporters/src/html.ts +9 -12
  88. package/packages/mcp-tools/src/bridge.ts +55 -32
  89. package/packages/mcp-tools/src/index.ts +9 -0
  90. package/packages/mcp-tools/src/tools.ts +269 -65
  91. package/packages/mcp-tools/src/update.ts +330 -0
  92. package/packages/parsers/src/documents.ts +521 -0
  93. package/packages/parsers/src/index.ts +8 -5
  94. package/packages/prompts/src/onboarding.ts +2 -2
  95. package/packages/shared/src/build-info.ts +22 -0
  96. package/packages/shared/src/enums.ts +39 -0
  97. package/packages/shared/src/index.ts +1 -0
  98. package/packages/shared/src/schemas.ts +24 -3
  99. package/packages/workflows/src/definitions.ts +4 -2
  100. package/scripts/build-demo.mjs +130 -0
  101. package/scripts/build-dist.mjs +46 -8
  102. package/scripts/build-mcpb.mjs +60 -4
  103. package/scripts/doctor.ts +16 -3
  104. package/scripts/gen-og.mjs +86 -0
  105. package/scripts/init.ts +32 -3
  106. package/scripts/seed.ts +5 -5
  107. package/scripts/shots-seed.ts +192 -0
  108. package/scripts/shots.ts +89 -0
  109. package/tsconfig.json +3 -1
  110. package/dist/careermate.mcpb +0 -0
  111. package/dist/mcp-smoke.err +0 -3
  112. package/dist/mcpb-stage/README.md +0 -219
  113. package/dist/mcpb-stage/dist/install-page/index.html +0 -434
  114. package/dist/mcpb-stage/dist/install-page/style.css +0 -407
  115. package/dist/mcpb-stage/dist/install-page/vercel.json +0 -20
  116. package/dist/mcpb-stage/dist/mcp.mjs +0 -23704
  117. package/dist/mcpb-stage/dist/public/app.js +0 -118
  118. package/dist/mcpb-stage/dist/public/index.html +0 -41
  119. package/dist/mcpb-stage/dist/public/lib.js +0 -282
  120. package/dist/mcpb-stage/dist/public/pages/applications.js +0 -98
  121. package/dist/mcpb-stage/dist/public/pages/documents.js +0 -446
  122. package/dist/mcpb-stage/dist/public/pages/home.js +0 -263
  123. package/dist/mcpb-stage/dist/public/pages/interview.js +0 -230
  124. package/dist/mcpb-stage/dist/public/pages/jobs.js +0 -494
  125. package/dist/mcpb-stage/dist/public/pages/profile.js +0 -576
  126. package/dist/mcpb-stage/dist/public/pages/settings.js +0 -233
  127. package/dist/mcpb-stage/dist/public/styles.css +0 -420
  128. package/dist/mcpb-stage/manifest.json +0 -40
  129. package/docs/INSTALL.md +0 -148
  130. package/docs/INSTALL_AND_USAGE.md +0 -99
  131. package/install-page/index.html +0 -474
  132. package/install-page/style.css +0 -391
  133. package/install-page/vercel.json +0 -20
  134. /package/dist/{mcpb-stage/dist/public → install-page}/fonts/PretendardVariable.woff2 +0 -0
@@ -1,233 +0,0 @@
1
- // Settings — data location, connection, theme, backup/export, and danger zone.
2
- // Everything stays local; this page surfaces where data lives and how to manage it.
3
- import {
4
- el, get, post, icon, navigate, Card, Stat, Btn, IconBtn, Input,
5
- openModal, copyText, downloadUrl, fmtRelative, toastOk, toastError, mount,
6
- } from '/lib.js';
7
-
8
- const COUNT_LABELS = {
9
- profile: '프로필', experiences: '경력', projects: '프로젝트', skills: '스킬',
10
- documents: '문서', cover_letters: '자기소개서', jobs: '채용공고',
11
- applications: '지원', interview_preps: '면접 준비',
12
- };
13
-
14
- export async function render(ctx) {
15
- ctx.setActions([]);
16
- const { info, backups } = await get('/api/settings');
17
-
18
- const wrap = el('div', { class: 'stack-4' },
19
- DataLocation(info),
20
- Connection(info),
21
- Theme(),
22
- MyData(info.counts),
23
- Backup(backups, () => render(ctx)),
24
- DangerZone(ctx),
25
- );
26
-
27
- mount(ctx.view, wrap);
28
- }
29
-
30
- /* ----------------------------------------------------- 데이터 저장 위치 */
31
- function kvRow(label, value, copyable) {
32
- return [
33
- el('dt', {}, label),
34
- el('dd', {},
35
- el('div', { class: 'flex between gap-3' },
36
- el('span', { class: 'truncate tnum', style: { fontFamily: 'var(--mono, ui-monospace, monospace)' }, title: value }, value || '—'),
37
- copyable && value
38
- ? IconBtn('copy', { title: '복사', onClick: () => copyText(value) })
39
- : null)),
40
- ];
41
- }
42
-
43
- function DataLocation(info) {
44
- const kv = el('dl', { class: 'kv' },
45
- ...kvRow('데이터 폴더', info.data_dir, true),
46
- ...kvRow('데이터베이스', info.db_path, true),
47
- ...kvRow('앱 버전', info.version, false),
48
- ...kvRow('Node 버전', info.node_version, false),
49
- );
50
-
51
- const privacy = el('div', { class: 'callout callout--privacy' },
52
- icon('lock'),
53
- el('div', {},
54
- el('div', { class: 'callout__title' }, '내 컴퓨터에만 저장됩니다'),
55
- el('div', { class: 'callout__body' },
56
- '모든 데이터는 이 컴퓨터에만 저장됩니다. 외부로 전송되지 않습니다. AI에는 사용자가 직접 입력하거나 AI가 MCP로 조회한 정보만 전달됩니다.')));
57
-
58
- return Card({
59
- title: '데이터 저장 위치',
60
- body: el('div', { class: 'stack-3' }, kv, privacy),
61
- });
62
- }
63
-
64
- /* ---------------------------------------------------------- 연결 상태 */
65
- function Connection(info) {
66
- const dashRow = el('div', { class: 'flex between gap-3', style: { padding: '4px 0' } },
67
- el('div', { class: 'flex gap-3', style: { alignItems: 'center' } },
68
- el('span', { class: 'badge badge--final_passed' }, el('span', { class: 'dot' }), '실행 중'),
69
- el('div', {},
70
- el('div', { class: 'strong' }, '대시보드'),
71
- el('div', { class: 'muted text-sm tnum' }, info.url || '—'))),
72
- info.url
73
- ? Btn('새 탭에서 열기', { icon: 'external', sm: true, variant: 'ghost', onClick: () => window.open(info.url, '_blank') })
74
- : null);
75
-
76
- const mcpRow = el('div', { class: 'flex between gap-3', style: { padding: '4px 0', alignItems: 'flex-start' } },
77
- el('div', { class: 'flex gap-3', style: { alignItems: 'flex-start' } },
78
- el('div', { class: 'empty__icon', style: { width: '30px', height: '30px', margin: 0, borderRadius: '8px' } }, icon('link')),
79
- el('div', {},
80
- el('div', { class: 'strong' }, 'MCP 서버'),
81
- el('div', { class: 'muted text-sm', style: { maxWidth: '440px', lineHeight: '1.6' } },
82
- 'MCP 서버는 사용하시는 AI 클라이언트(Claude·ChatGPT·Cursor)가 직접 실행하며, 이 대시보드와 같은 데이터베이스를 공유합니다.'))),
83
- Btn('설치·연결 안내 열기', { icon: 'external', sm: true, variant: 'ghost', onClick: () => window.open('/install', '_blank') }));
84
-
85
- return Card({
86
- title: '연결 상태',
87
- body: el('div', { class: 'stack-2' }, dashRow, el('div', { class: 'divider' }), mcpRow),
88
- });
89
- }
90
-
91
- /* ------------------------------------------------------------- 테마 */
92
- function currentTheme() {
93
- const saved = localStorage.getItem('cf-theme');
94
- return saved === 'light' || saved === 'dark' ? saved : 'system';
95
- }
96
-
97
- function Theme() {
98
- const options = [
99
- { value: 'light', label: '라이트' },
100
- { value: 'dark', label: '다크' },
101
- { value: 'system', label: '시스템' },
102
- ];
103
-
104
- const row = el('div', { class: 'flex gap-2 wrap' });
105
-
106
- function applyTheme(value) {
107
- if (value === 'system') {
108
- delete document.documentElement.dataset.theme;
109
- localStorage.removeItem('cf-theme');
110
- } else {
111
- document.documentElement.dataset.theme = value;
112
- localStorage.setItem('cf-theme', value);
113
- }
114
- paint();
115
- }
116
-
117
- function paint() {
118
- const active = currentTheme();
119
- mount(row, ...options.map((o) => {
120
- const b = Btn(o.label, { variant: 'ghost', onClick: () => applyTheme(o.value) });
121
- b.setAttribute('aria-pressed', String(o.value === active));
122
- if (o.value === active) b.classList.add('is-selected');
123
- return b;
124
- }));
125
- }
126
- paint();
127
-
128
- return Card({
129
- title: '테마',
130
- sub: '시스템 설정을 따르거나 직접 고를 수 있어요',
131
- body: row,
132
- });
133
- }
134
-
135
- /* ---------------------------------------------------------- 내 데이터 */
136
- function MyData(counts = {}) {
137
- const entries = Object.entries(COUNT_LABELS)
138
- .map(([key, label]) => Stat({ label, value: counts[key] ?? 0 }));
139
- return Card({
140
- title: '내 데이터',
141
- sub: '이 컴퓨터에 저장된 항목 수',
142
- body: el('div', { class: 'grid grid--3' }, ...entries),
143
- });
144
- }
145
-
146
- /* ------------------------------------------------- 백업 / 내보내기 */
147
- function Backup(backups = [], rerender) {
148
- async function createBackup() {
149
- try {
150
- const res = await post('/api/settings/backup');
151
- toastOk('백업을 생성했습니다.');
152
- await rerender();
153
- } catch (e) {
154
- toastError(e);
155
- }
156
- }
157
-
158
- const list = backups.length
159
- ? el('table', { class: 'table' },
160
- el('thead', {}, el('tr', {},
161
- el('th', {}, '파일'), el('th', {}, '생성'), el('th', { style: { textAlign: 'right' } }, '크기'))),
162
- el('tbody', {}, ...backups.map((b) => el('tr', {},
163
- el('td', {}, el('span', { class: 'truncate', title: b.path || b.filename }, b.filename)),
164
- el('td', { class: 'muted text-sm' }, fmtRelative(b.created_at)),
165
- el('td', { class: 'muted text-sm tnum', style: { textAlign: 'right' } }, `${Math.round((b.size || 0) / 1024)} KB`)))))
166
- : el('p', { class: 'muted', style: { margin: 0 } }, '아직 백업이 없습니다. "백업 생성"으로 현재 데이터를 안전하게 보관하세요.');
167
-
168
- return Card({
169
- title: '백업 / 내보내기',
170
- actions: [
171
- Btn('백업 생성', { icon: 'copy', sm: true, variant: 'primary', onClick: createBackup }),
172
- Btn('전체 데이터 내보내기(JSON)', { icon: 'download', sm: true, variant: 'ghost', onClick: () => downloadUrl('/api/settings/export-all') }),
173
- ],
174
- body: list,
175
- });
176
- }
177
-
178
- /* -------------------------------------------------- 위험 구역 (Danger zone) */
179
- function DangerZone(ctx) {
180
- function openResetModal() {
181
- let confirmBtn;
182
- const input = Input({
183
- placeholder: 'DELETE',
184
- onInput: (e) => { if (confirmBtn) confirmBtn.disabled = e.target.value.trim() !== 'DELETE'; },
185
- onKeydown: (e) => { if (e.key === 'Enter' && e.target.value.trim() === 'DELETE' && confirmBtn && !confirmBtn.disabled) confirmBtn.click(); },
186
- });
187
-
188
- openModal({
189
- title: '모든 데이터 초기화',
190
- body: el('div', { class: 'stack-3' },
191
- el('div', { class: 'callout' },
192
- icon('info'),
193
- el('div', {},
194
- el('div', { class: 'callout__title' }, '되돌릴 수 없습니다'),
195
- el('div', { class: 'callout__body' },
196
- '프로필·공고·지원·문서·면접 준비를 포함한 모든 데이터가 영구적으로 삭제됩니다. 안전을 위해 초기화 직전에 자동으로 백업이 한 번 생성됩니다.'))),
197
- el('p', { class: 'text-secondary', style: { margin: 0, lineHeight: '1.6' } },
198
- '계속하려면 아래 입력란에 ', el('span', { class: 'strong' }, 'DELETE'), ' 를 입력하세요.'),
199
- input),
200
- footer: (close) => {
201
- confirmBtn = Btn('모든 데이터 초기화', {
202
- variant: 'danger',
203
- disabled: true,
204
- onClick: async () => {
205
- confirmBtn.disabled = true;
206
- try {
207
- await post('/api/settings/reset', { confirm: 'DELETE' });
208
- close();
209
- toastOk('모든 데이터가 초기화되었습니다.');
210
- navigate('/');
211
- await ctx.refreshNav();
212
- } catch (e) {
213
- toastError(e);
214
- confirmBtn.disabled = input.value.trim() !== 'DELETE';
215
- }
216
- },
217
- });
218
- return [Btn('취소', { onClick: close }), confirmBtn];
219
- },
220
- });
221
- }
222
-
223
- return Card({
224
- title: '위험 구역',
225
- sub: '이 작업은 되돌릴 수 없습니다',
226
- body: el('div', { class: 'flex between gap-3 wrap' },
227
- el('div', { style: { maxWidth: '440px' } },
228
- el('div', { class: 'strong' }, '모든 데이터 초기화'),
229
- el('div', { class: 'muted text-sm', style: { lineHeight: '1.6' } },
230
- '이 컴퓨터에 저장된 모든 CareerMate 데이터를 삭제합니다. 초기화 전에 자동으로 백업이 생성됩니다.')),
231
- Btn('모든 데이터 초기화', { icon: 'trash', variant: 'danger', onClick: openResetModal })),
232
- });
233
- }
@@ -1,420 +0,0 @@
1
- /* =============================================================================
2
- CareerMate design system — a calm, dense, real-productivity-tool aesthetic.
3
- No framework, no CDN (works fully offline; nothing leaves the machine).
4
- Inspired by the restraint of Linear / Notion / Raycast — not AI-flashy.
5
- ========================================================================== */
6
-
7
- /* Self-hosted brand font (offline; served from /fonts by the local server).
8
- A real loaded face — not a system fallback — so the type is intentional and
9
- identical on every machine. One variable file covers all weights. */
10
- @font-face {
11
- font-family: 'Pretendard Variable';
12
- font-weight: 45 920;
13
- font-style: normal;
14
- /* block (not swap): show no fallback-then-swap flash; the font is local so the
15
- brief invisibility is imperceptible, and it's preloaded in index.html. */
16
- font-display: block;
17
- src: local('Pretendard Variable'),
18
- url('/fonts/PretendardVariable.woff2') format('woff2');
19
- }
20
-
21
- :root {
22
- /* Neutral palette (light) */
23
- --bg: #ffffff;
24
- --bg-subtle: #f7f7f8;
25
- --bg-sunken: #f1f1f3;
26
- --surface: #ffffff;
27
- --surface-hover: #f7f7f8;
28
- --border: #e6e6e9;
29
- --border-strong: #d4d4d8;
30
- --text: #1c1c1f;
31
- --text-secondary: #5c5c66;
32
- --text-tertiary: #8a8a94;
33
- --text-on-accent: #ffffff;
34
-
35
- /* Accent — calm indigo */
36
- --accent: #4f46e5;
37
- --accent-hover: #4338ca;
38
- --accent-soft: #eef2ff;
39
- --accent-border: #c7d2fe;
40
-
41
- /* Semantic */
42
- --green: #15803d; --green-soft: #ecfdf3; --green-border: #bbf7d0;
43
- --teal: #0f766e; --teal-soft: #effcf9; --teal-border: #99f6e4;
44
- --blue: #1d4ed8; --blue-soft: #eff4ff; --blue-border: #bfdbfe;
45
- --violet: #6d28d9; --violet-soft: #f5f0ff; --violet-border: #ddd6fe;
46
- --amber: #b45309; --amber-soft: #fffbeb; --amber-border: #fde68a;
47
- --red: #b91c1c; --red-soft: #fef2f2; --red-border: #fecaca;
48
- --slate: #475569; --slate-soft: #f5f6f8; --slate-border: #dbe0e6;
49
-
50
- /* Elevation */
51
- --shadow-sm: 0 1px 2px rgba(16,16,20,.05);
52
- --shadow-md: 0 4px 16px rgba(16,16,20,.08);
53
- --shadow-lg: 0 12px 40px rgba(16,16,20,.16);
54
-
55
- /* Geometry */
56
- --radius-sm: 6px;
57
- --radius: 9px;
58
- --radius-lg: 14px;
59
- --sidebar-w: 248px;
60
- --topbar-h: 56px;
61
- --maxw: 1080px;
62
-
63
- --font: 'Pretendard Variable', Pretendard, -apple-system, BlinkMacSystemFont,
64
- 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif;
65
- --mono: 'SFMono-Regular', 'JetBrains Mono', Consolas, 'D2Coding', monospace;
66
- }
67
-
68
- @media (prefers-color-scheme: dark) {
69
- :root:not([data-theme='light']) {
70
- --bg: #161618;
71
- --bg-subtle: #1d1d20;
72
- --bg-sunken: #0f0f11;
73
- --surface: #1d1d20;
74
- --surface-hover: #26262a;
75
- --border: #2c2c31;
76
- --border-strong: #3a3a41;
77
- --text: #ededf0;
78
- --text-secondary: #a5a5b0;
79
- --text-tertiary: #74747e;
80
- --accent: #818cf8;
81
- --accent-hover: #a5b4fc;
82
- --accent-soft: #1e1b3a;
83
- --accent-border: #3730a3;
84
- --green-soft:#0e2a1a; --teal-soft:#0c2826; --blue-soft:#11203f; --violet-soft:#1e1638;
85
- --amber-soft:#2a2008; --red-soft:#2c1414; --slate-soft:#1f242b;
86
- --shadow-lg: 0 12px 40px rgba(0,0,0,.5);
87
- }
88
- }
89
- :root[data-theme='dark'] {
90
- --bg:#161618; --bg-subtle:#1d1d20; --bg-sunken:#0f0f11; --surface:#1d1d20; --surface-hover:#26262a;
91
- --border:#2c2c31; --border-strong:#3a3a41; --text:#ededf0; --text-secondary:#a5a5b0; --text-tertiary:#74747e;
92
- --accent:#818cf8; --accent-hover:#a5b4fc; --accent-soft:#1e1b3a; --accent-border:#3730a3;
93
- --green-soft:#0e2a1a; --teal-soft:#0c2826; --blue-soft:#11203f; --violet-soft:#1e1638;
94
- --amber-soft:#2a2008; --red-soft:#2c1414; --slate-soft:#1f242b; --shadow-lg:0 12px 40px rgba(0,0,0,.5);
95
- }
96
-
97
- * { box-sizing: border-box; }
98
- /* Icons must never stretch inside flex containers. */
99
- svg { flex: none; }
100
- html, body { height: 100%; }
101
- body {
102
- margin: 0; font-family: var(--font); color: var(--text); background: var(--bg-subtle);
103
- font-size: 15px; line-height: 1.55; -webkit-font-smoothing: antialiased;
104
- text-rendering: optimizeLegibility;
105
- }
106
- a { color: var(--accent); text-decoration: none; }
107
- a:hover { text-decoration: underline; }
108
- h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
109
- ::selection { background: var(--accent-soft); }
110
- :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
111
-
112
- /* ----------------------------------------------------------------- layout */
113
- .app { display: grid; grid-template-columns: var(--sidebar-w) 1fr; min-height: 100vh; }
114
-
115
- .sidebar {
116
- background: var(--bg); border-right: 1px solid var(--border);
117
- display: flex; flex-direction: column; position: sticky; top: 0; height: 100vh;
118
- }
119
- .sidebar__brand { display: flex; align-items: center; gap: 10px; padding: 18px 18px 14px; }
120
- .sidebar__logo {
121
- width: 30px; height: 30px; border-radius: 8px; flex: none;
122
- background: var(--accent);
123
- display: grid; place-items: center; color: #fff; font-weight: 800; font-size: 15px;
124
- }
125
- .sidebar__title { font-weight: 700; font-size: 15px; letter-spacing: -.01em; }
126
- .sidebar__subtitle { font-size: 12px; color: var(--text-tertiary); margin-top: 1px; }
127
- .nav { padding: 6px 10px; display: flex; flex-direction: column; gap: 1px; flex: 1; overflow-y: auto; }
128
- .nav__label { font-size: 11.5px; font-weight: 600; color: var(--text-tertiary); padding: 12px 10px 4px; text-transform: uppercase; letter-spacing: .04em; }
129
- .nav__item {
130
- display: flex; align-items: center; gap: 10px; padding: 7px 10px; border-radius: var(--radius-sm);
131
- color: var(--text-secondary); font-weight: 500; cursor: pointer; user-select: none; position: relative;
132
- }
133
- .nav__item:hover { background: var(--surface-hover); color: var(--text); text-decoration: none; }
134
- .nav__item.is-active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
135
- .nav__item svg { width: 17px; height: 17px; flex: none; }
136
- .nav__badge { margin-left: auto; font-size: 11.5px; background: var(--bg-sunken); color: var(--text-secondary);
137
- padding: 1px 7px; border-radius: 999px; font-weight: 600; }
138
- .nav__item.is-active .nav__badge { background: var(--accent); color: #fff; }
139
- .sidebar__foot { padding: 12px 16px; border-top: 1px solid var(--border); font-size: 12px; color: var(--text-tertiary); }
140
- .sidebar__foot .dot { display:inline-block; width:7px; height:7px; border-radius:999px; background: var(--green); margin-right:6px; }
141
-
142
- .main { display: flex; flex-direction: column; min-width: 0; }
143
- .topbar {
144
- height: var(--topbar-h); border-bottom: 1px solid var(--border); background: color-mix(in srgb, var(--bg) 88%, transparent);
145
- backdrop-filter: blur(8px); position: sticky; top: 0; z-index: 20;
146
- display: flex; align-items: center; gap: 12px; padding: 0 24px;
147
- }
148
- .topbar__title { font-size: 15px; font-weight: 650; }
149
- .topbar__crumb { color: var(--text-tertiary); }
150
- .topbar__spacer { flex: 1; }
151
- .view { padding: 24px; max-width: var(--maxw); width: 100%; margin: 0 auto; }
152
- .view--wide { max-width: 1280px; }
153
-
154
- .page-head { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 20px; }
155
- .page-head__text h1 { font-size: 22px; }
156
- .page-head__text p { margin: 4px 0 0; color: var(--text-secondary); }
157
- .page-head__actions { margin-left: auto; display: flex; gap: 8px; flex: none; }
158
-
159
- /* ----------------------------------------------------------------- grid */
160
- .grid { display: grid; gap: 16px; }
161
- .grid--2 { grid-template-columns: repeat(2, 1fr); }
162
- .grid--3 { grid-template-columns: repeat(3, 1fr); }
163
- .grid--4 { grid-template-columns: repeat(4, 1fr); }
164
- @media (max-width: 900px){ .grid--2,.grid--3,.grid--4{ grid-template-columns: 1fr; } .app{ grid-template-columns: 1fr; } .sidebar{ display:none; } }
165
-
166
- /* ----------------------------------------------------------------- card */
167
- .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); }
168
- .card__head { padding: 14px 18px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 10px; }
169
- .card__head h3 { font-size: 15px; }
170
- .card__head .sub { color: var(--text-tertiary); font-size: 13px; }
171
- .card__head .right { margin-left: auto; display: flex; gap: 6px; }
172
- .card__body { padding: 18px; }
173
- .card__body--tight { padding: 0; }
174
- /* a light bordered block for grouping inside a card/modal — never nest a full .card */
175
- .subcard { border: 1px solid var(--border); border-radius: var(--radius); padding: 14px; }
176
- .card.is-clickable { cursor: pointer; transition: border-color .12s, box-shadow .12s, transform .12s; }
177
- .card.is-clickable:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); }
178
-
179
- /* stat tiles */
180
- .stat { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 16px 18px; }
181
- .stat.is-clickable { cursor: pointer; transition: border-color .12s, box-shadow .12s; }
182
- .stat.is-clickable:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); }
183
- .stat__label { font-size: 13px; color: var(--text-secondary); display:flex; align-items:center; gap:7px; }
184
- .stat__label svg { width: 15px; height: 15px; color: var(--text-tertiary); }
185
- .stat__value { font-size: 26px; font-weight: 700; letter-spacing: -.02em; margin-top: 6px; }
186
- .stat__hint { font-size: 12.5px; color: var(--text-tertiary); margin-top: 2px; }
187
-
188
- /* ----------------------------------------------------------------- buttons */
189
- .btn {
190
- display: inline-flex; align-items: center; gap: 7px; height: 34px; padding: 0 13px;
191
- border-radius: var(--radius-sm); border: 1px solid var(--border-strong); background: var(--surface);
192
- color: var(--text); font-weight: 550; font-size: 14px; cursor: pointer; font-family: inherit;
193
- transition: background .12s, border-color .12s, opacity .12s; white-space: nowrap; user-select: none;
194
- }
195
- .btn:hover { background: var(--surface-hover); }
196
- .btn:disabled { opacity: .5; cursor: not-allowed; }
197
- .btn svg { width: 15px; height: 15px; }
198
- .btn--primary { background: var(--accent); border-color: var(--accent); color: #fff; }
199
- .btn--primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
200
- .btn--ghost { border-color: transparent; background: transparent; }
201
- .btn--ghost:hover { background: var(--surface-hover); }
202
- .btn--danger { color: var(--red); border-color: var(--red-border); background: var(--red-soft); }
203
- .btn--danger:hover { background: var(--red); color:#fff; border-color: var(--red); }
204
- .btn--sm { height: 28px; padding: 0 10px; font-size: 13px; }
205
- .btn--block { width: 100%; justify-content: center; }
206
- .icon-btn { width: 30px; height: 30px; padding: 0; justify-content: center; }
207
- /* a ghost button marked as the selected option (segmented toggle), e.g. theme picker */
208
- .btn.is-selected { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-border); }
209
-
210
- /* ----------------------------------------------------------------- forms */
211
- .field { margin-bottom: 14px; }
212
- .field > label { display: block; font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; }
213
- .field .hint { font-size: 12px; color: var(--text-tertiary); margin-top: 5px; }
214
- .input, .textarea, .select {
215
- width: 100%; font-family: inherit; font-size: 14px; color: var(--text);
216
- background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
217
- padding: 8px 11px; transition: border-color .12s, box-shadow .12s;
218
- }
219
- .input:focus, .textarea:focus, .select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
220
- .textarea { resize: vertical; min-height: 90px; line-height: 1.6; }
221
- .textarea--lg { min-height: 280px; font-size: 15px; }
222
- .input::placeholder, .textarea::placeholder { color: var(--text-tertiary); }
223
- .row { display: flex; gap: 10px; }
224
- .row > * { flex: 1; }
225
- /* compact inline input for the topbar/toolbar (search boxes) */
226
- .input--inline { width: 220px; max-width: 46vw; }
227
-
228
- /* ----------------------------------------------------------------- badges */
229
- .badge { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; font-weight: 600;
230
- padding: 2px 9px; border-radius: 999px; border: 1px solid var(--border); background: var(--bg-sunken); color: var(--text-secondary); white-space: nowrap; }
231
- .badge .dot { width: 6px; height: 6px; border-radius: 999px; background: currentColor; }
232
- .badge--draft { color: var(--slate); background: var(--slate-soft); border-color: var(--slate-border); }
233
- .badge--planned { color: var(--blue); background: var(--blue-soft); border-color: var(--blue-border); }
234
- .badge--applied { color: var(--violet); background: var(--violet-soft); border-color: var(--violet-border); }
235
- .badge--document_passed { color: var(--teal); background: var(--teal-soft); border-color: var(--teal-border); }
236
- .badge--interview { color: var(--amber); background: var(--amber-soft); border-color: var(--amber-border); }
237
- .badge--final_passed { color: var(--green); background: var(--green-soft); border-color: var(--green-border); }
238
- .badge--rejected { color: var(--red); background: var(--red-soft); border-color: var(--red-border); }
239
- .badge--on_hold { color: var(--slate); background: var(--slate-soft); border-color: var(--slate-border); }
240
- .badge--accent { color: var(--accent); background: var(--accent-soft); border-color: var(--accent-border); }
241
- .badge--score { font-variant-numeric: tabular-nums; }
242
-
243
- .chip { display:inline-flex; align-items:center; gap:6px; font-size:12px; padding:3px 9px; border-radius:7px;
244
- background: var(--bg-sunken); color: var(--text-secondary); border:1px solid var(--border); }
245
- .chip--accent { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-border); }
246
- .chips { display:flex; flex-wrap:wrap; gap:6px; }
247
-
248
- /* ----------------------------------------------------------------- table */
249
- .table { width: 100%; border-collapse: collapse; }
250
- .table th { text-align: left; font-size: 12px; font-weight: 600; color: var(--text-tertiary); text-transform: uppercase;
251
- letter-spacing: .03em; padding: 10px 16px; border-bottom: 1px solid var(--border); }
252
- .table td { padding: 12px 16px; border-bottom: 1px solid var(--border); vertical-align: middle; }
253
- .table tr:last-child td { border-bottom: none; }
254
- .table tbody tr.is-clickable { cursor: pointer; }
255
- .table tbody tr.is-clickable:hover { background: var(--surface-hover); }
256
- .table .muted { color: var(--text-tertiary); }
257
- .table .strong { font-weight: 600; }
258
-
259
- /* ----------------------------------------------------------------- progress */
260
- .progress { height: 7px; background: var(--bg-sunken); border-radius: 999px; overflow: hidden; }
261
- .progress__bar { height: 100%; background: var(--accent); border-radius: 999px; transition: width .4s ease; }
262
-
263
- /* ----------------------------------------------------------------- empty state */
264
- .empty { text-align: center; padding: 44px 24px; color: var(--text-secondary); }
265
- .empty__icon { width: 46px; height: 46px; border-radius: 12px; background: var(--bg-sunken); display: grid; place-items: center; margin: 0 auto 14px; color: var(--text-tertiary); }
266
- .empty__icon svg { width: 22px; height: 22px; }
267
- .empty h3 { font-size: 15px; color: var(--text); margin-bottom: 6px; }
268
- .empty p { max-width: 420px; margin: 0 auto 16px; font-size: 13.5px; }
269
-
270
- /* ----------------------------------------------------------------- timeline / versions */
271
- .timeline { display: flex; flex-direction: column; }
272
- .tl-item { display: grid; grid-template-columns: 22px 1fr; gap: 12px; padding-bottom: 4px; }
273
- .tl-item__rail { display: flex; flex-direction: column; align-items: center; }
274
- .tl-item__dot { width: 11px; height: 11px; border-radius: 999px; background: var(--surface); border: 2px solid var(--border-strong); margin-top: 4px; flex: none; }
275
- .tl-item.is-current .tl-item__dot { background: var(--accent); border-color: var(--accent); }
276
- .tl-item__line { width: 2px; flex: 1; background: var(--border); margin: 4px 0; }
277
- .tl-item:last-child .tl-item__line { display: none; }
278
- .tl-item__body { padding-bottom: 16px; }
279
-
280
- /* ----------------------------------------------------------------- kanban */
281
- .board { display: grid; grid-auto-flow: column; grid-auto-columns: 272px; gap: 14px; overflow-x: auto; padding-bottom: 8px; }
282
- .board__col { background: var(--bg-subtle); border: 1px solid var(--border); border-radius: var(--radius-lg); display: flex; flex-direction: column; max-height: calc(100vh - 200px); }
283
- .board__col-head { padding: 11px 14px; display: flex; align-items: center; gap: 8px; position: sticky; top: 0; }
284
- .board__col-head .count { margin-left: auto; font-size: 12.5px; color: var(--text-tertiary); font-weight: 600; }
285
- .board__cards { padding: 0 10px 12px; display: flex; flex-direction: column; gap: 8px; overflow-y: auto; }
286
- .board-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 11px 12px; cursor: pointer; box-shadow: var(--shadow-sm); }
287
- .board-card:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); }
288
- .board-card__company { font-weight: 600; font-size: 14px; }
289
- .board-card__role { font-size: 13px; color: var(--text-secondary); margin-top: 1px; }
290
- .board-card__meta { display: flex; align-items: center; gap: 8px; margin-top: 9px; }
291
-
292
- /* ----------------------------------------------------------------- list row (shared)
293
- One primitive behind the home action lane, recent-job list, and any dense list.
294
- Leading slot → title + muted sub → right-aligned trailing (badges/score). */
295
- .list-row { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--border); }
296
- .list-row:last-child { border-bottom: none; }
297
- .list-row.is-clickable { cursor: pointer; transition: background .12s; }
298
- .list-row.is-clickable:hover { background: var(--surface-hover); }
299
- .list-row__lead { flex: none; display: grid; place-items: center; }
300
- .list-row__lead--chip { width: 30px; height: 30px; border-radius: 8px; background: var(--bg-sunken); color: var(--text-tertiary); }
301
- .list-row__lead--chip svg { width: 16px; height: 16px; }
302
- .list-row__dot { width: 9px; height: 9px; border-radius: 999px; flex: none; }
303
- .list-row__main { flex: 1; min-width: 0; }
304
- .list-row__title { font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
305
- .list-row__sub { font-size: 13px; color: var(--text-tertiary); margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
306
- .list-row__trail { flex: none; display: flex; align-items: center; gap: 8px; margin-left: auto; white-space: nowrap; }
307
-
308
- /* applications: status-group section header (reads clearly as a section title) */
309
- .app-group { }
310
- .app-group__head { display: flex; align-items: center; gap: 9px; padding-bottom: 8px; margin-bottom: 2px; border-bottom: 1px solid var(--border); }
311
- .app-group__dot { width: 9px; height: 9px; border-radius: 999px; flex: none; }
312
- .app-group__title { font-weight: 650; font-size: 15px; letter-spacing: -.01em; }
313
- .app-group__count { margin-left: auto; color: var(--text-tertiary); font-size: 13px; }
314
-
315
- /* ----------------------------------------------------------------- pipeline bar + funnel
316
- Visualizes the application status_breakdown. Segment color == badge--{status} hue,
317
- so a colour means the same stage everywhere (board, badges, here). */
318
- .pipebar { display: flex; height: 14px; border-radius: 999px; overflow: hidden; background: var(--bg-sunken); gap: 2px; }
319
- .pipebar__seg { min-width: 8px; cursor: pointer; transition: flex-grow .4s ease, opacity .12s; }
320
- .pipebar__seg:hover { opacity: .8; }
321
- .pipefunnel { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-top: 18px; }
322
- .pipefunnel__tile { cursor: pointer; }
323
- .pipefunnel__label { font-size: 13px; color: var(--text-secondary); display: flex; align-items: center; gap: 6px; }
324
- .pipefunnel__dot { width: 8px; height: 8px; border-radius: 999px; flex: none; }
325
- .pipefunnel__value { font-size: 24px; font-weight: 700; letter-spacing: -.02em; margin-top: 5px; line-height: 1.1; }
326
- .pipefunnel__conv { font-size: 12px; color: var(--text-tertiary); margin-top: 2px; }
327
- @media (max-width: 560px){ .pipefunnel { grid-template-columns: repeat(2, 1fr); } }
328
-
329
- /* ----------------------------------------------------------------- check row (onboarding)
330
- The ONE place setup guidance lives. Outcome rows auto-detect completion. */
331
- .check-row { display: flex; align-items: center; gap: 10px; padding: 7px 0; }
332
- .check-row__dot { width: 16px; height: 16px; border: 2px solid var(--border-strong); border-radius: 999px; flex: none; box-sizing: border-box; }
333
- .check-row__icon { width: 16px; height: 16px; flex: none; color: var(--green); }
334
- .check-row__label { font-size: 14px; }
335
- .check-row.is-done .check-row__label { color: var(--text-tertiary); text-decoration: line-through; }
336
- .check-row__label.is-link { color: var(--accent); cursor: pointer; }
337
- .check-row__label.is-link:hover { text-decoration: underline; }
338
-
339
- /* ----------------------------------------------------------------- activity feed (day-bucketed) */
340
- .feed-group { font-size: 11.5px; font-weight: 600; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: .04em; padding: 14px 0 6px; }
341
- .feed-group:first-child { padding-top: 0; }
342
- .feed-item { display: flex; gap: 12px; align-items: flex-start; padding: 6px 0; }
343
- .feed-item__icon { width: 30px; height: 30px; border-radius: 8px; display: grid; place-items: center; flex: none; background: var(--bg-sunken); color: var(--text-tertiary); }
344
- .feed-item__icon svg { width: 16px; height: 16px; }
345
- .feed-item__body { flex: 1; min-width: 0; }
346
- .feed-item__text { font-size: 13.5px; line-height: 1.5; }
347
- .feed-item__time { display: block; font-size: 12.5px; color: var(--text-tertiary); margin-top: 1px; }
348
-
349
- /* ----------------------------------------------------------------- first-run hero
350
- Day one: a single getting-started card centered on a quiet dot canvas, instead
351
- of a wall of empty cards. */
352
- .firstrun-hero { display: grid; place-items: start center; padding: 28px 0;
353
- background-image: radial-gradient(var(--border) 1px, transparent 1px); background-size: 24px 24px; border-radius: var(--radius-lg); }
354
- .firstrun-hero > .card { width: 100%; max-width: 540px; }
355
-
356
- /* ----------------------------------------------------------------- doc preview */
357
- .doc-preview { white-space: pre-wrap; word-break: break-word; font-size: 15px; line-height: 1.75; color: var(--text);
358
- background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 22px 26px; max-height: 60vh; overflow-y: auto; }
359
- .kv { display: grid; grid-template-columns: 132px 1fr; gap: 8px 16px; }
360
- .kv dt { color: var(--text-tertiary); font-size: 13px; }
361
- .kv dd { margin: 0; }
362
-
363
- /* ----------------------------------------------------------------- modal */
364
- .modal-root { position: fixed; inset: 0; z-index: 60; display: none; }
365
- .modal-root.is-open { display: block; }
366
- .modal__scrim { position: absolute; inset: 0; background: rgba(16,16,20,.42); backdrop-filter: blur(2px); animation: fade .15s; }
367
- .modal { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); width: min(640px, calc(100vw - 32px));
368
- max-height: calc(100vh - 48px); background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
369
- box-shadow: var(--shadow-lg); display: flex; flex-direction: column; animation: pop .16s ease; }
370
- .modal--lg { width: min(880px, calc(100vw - 32px)); }
371
- .modal__head { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; }
372
- .modal__head h3 { font-size: 15px; }
373
- .modal__body { padding: 20px; overflow-y: auto; }
374
- .modal__foot { padding: 14px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 8px; }
375
- @keyframes pop { from { transform: translate(-50%,-46%); opacity: 0; } to { transform: translate(-50%,-50%); opacity: 1; } }
376
- @keyframes fade { from { opacity: 0; } to { opacity: 1; } }
377
-
378
- /* ----------------------------------------------------------------- toast */
379
- .toasts { position: fixed; bottom: 20px; right: 20px; z-index: 80; display: flex; flex-direction: column; gap: 10px; }
380
- .toast { background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: var(--radius);
381
- box-shadow: var(--shadow-md); padding: 12px 16px; min-width: 260px; max-width: 380px; animation: slidein .2s ease; font-size: 14px; }
382
- .toast--success { border-left-color: var(--green); }
383
- .toast--error { border-left-color: var(--red); }
384
- .toast__title { font-weight: 600; margin-bottom: 2px; }
385
- .toast__body { color: var(--text-secondary); font-size: 13px; }
386
- @keyframes slidein { from { transform: translateX(20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
387
-
388
- /* ----------------------------------------------------------------- tabs */
389
- .tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); margin-bottom: 18px; }
390
- .tab { padding: 9px 14px; font-size: 14px; font-weight: 550; color: var(--text-secondary); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; }
391
- .tab:hover { color: var(--text); }
392
- .tab.is-active { color: var(--accent); border-bottom-color: var(--accent); }
393
-
394
- /* ----------------------------------------------------------------- utils */
395
- .muted { color: var(--text-tertiary); }
396
- .text-sm { font-size: 13px; }
397
- .text-secondary { color: var(--text-secondary); }
398
- .stack { display: flex; flex-direction: column; }
399
- .stack-2 > * + * { margin-top: 8px; } .stack-3 > * + * { margin-top: 12px; } .stack-4 > * + * { margin-top: 16px; }
400
- .flex { display: flex; align-items: center; } .flex-col{ display:flex; flex-direction:column; }
401
- .gap-2 { gap: 8px; } .gap-3 { gap: 12px; } .gap-4 { gap: 16px; }
402
- .between { justify-content: space-between; } .center { align-items: center; } .wrap { flex-wrap: wrap; }
403
- .ml-auto { margin-left: auto; } .mt-2{margin-top:8px;} .mt-3{margin-top:12px;} .mt-4{margin-top:16px;} .mb-2{margin-bottom:8px;} .mb-3{margin-bottom:12px;} .mb-4{margin-bottom:16px;}
404
- .tnum { font-variant-numeric: tabular-nums; }
405
- .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
406
- .divider { height: 1px; background: var(--border); margin: 16px 0; }
407
- .skeleton { background: linear-gradient(90deg, var(--bg-sunken) 25%, var(--surface-hover) 37%, var(--bg-sunken) 63%); background-size: 400% 100%; animation: shimmer 1.4s infinite; border-radius: 6px; }
408
- @keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }
409
- .hide { display: none !important; }
410
-
411
- /* score color helper */
412
- .score-strong { color: var(--green); } .score-mid { color: var(--amber); } .score-weak { color: var(--red); }
413
-
414
- /* banner / callout */
415
- .callout { display: flex; gap: 12px; padding: 14px 16px; border-radius: var(--radius); border: 1px solid var(--accent-border); background: var(--accent-soft); }
416
- .callout svg { flex: none; width: 18px; height: 18px; color: var(--accent); margin-top: 1px; }
417
- .callout--privacy { border-color: var(--border); background: var(--bg-subtle); }
418
- .callout--privacy svg { color: var(--text-tertiary); }
419
- .callout__title { font-weight: 600; margin-bottom: 2px; }
420
- .callout__body { color: var(--text-secondary); font-size: 13px; }