careermate 0.0.1

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 (137) hide show
  1. package/AGENTS.md +33 -0
  2. package/INSTALL.md +230 -0
  3. package/LICENSE +21 -0
  4. package/README.md +532 -0
  5. package/THIRD_PARTY_NOTICES.md +57 -0
  6. package/bin/careermate.mjs +77 -0
  7. package/dist/career-os/README.md +78 -0
  8. package/dist/career-os/eop/cover-letter.md +46 -0
  9. package/dist/career-os/eop/fit-analysis.md +48 -0
  10. package/dist/career-os/eop/interview-prep.md +39 -0
  11. package/dist/career-os/eop/job-analysis.md +42 -0
  12. package/dist/career-os/eop/profile-extraction.md +37 -0
  13. package/dist/career-os/knowledge/ats.md +131 -0
  14. package/dist/career-os/knowledge/company-research.md +137 -0
  15. package/dist/career-os/knowledge/cover-letter.md +135 -0
  16. package/dist/career-os/knowledge/fit-matching.md +121 -0
  17. package/dist/career-os/knowledge/human-writing.md +118 -0
  18. package/dist/career-os/knowledge/interview-behavioral.md +132 -0
  19. package/dist/career-os/knowledge/interview-technical.md +126 -0
  20. package/dist/career-os/knowledge/linkedin-profile.md +152 -0
  21. package/dist/career-os/knowledge/networking-referrals.md +166 -0
  22. package/dist/career-os/knowledge/offer-evaluation-decision.md +121 -0
  23. package/dist/career-os/knowledge/onboarding-first-90-days.md +126 -0
  24. package/dist/career-os/knowledge/portfolio.md +121 -0
  25. package/dist/career-os/knowledge/recruiter-screen.md +105 -0
  26. package/dist/career-os/knowledge/rejection-triage-iteration.md +115 -0
  27. package/dist/career-os/knowledge/resume.md +123 -0
  28. package/dist/career-os/knowledge/salary-negotiation.md +123 -0
  29. package/dist/career-os/knowledge/verifiers/ats-compat.md +64 -0
  30. package/dist/career-os/knowledge/verifiers/consistency.md +61 -0
  31. package/dist/career-os/knowledge/verifiers/human-voice.md +82 -0
  32. package/dist/career-os/knowledge/verifiers/recency-staleness.md +72 -0
  33. package/dist/career-os/knowledge/verifiers/responsiveness-on-target.md +82 -0
  34. package/dist/career-os/knowledge/verifiers/truthfulness.md +67 -0
  35. package/dist/doctor.mjs +79094 -0
  36. package/dist/mcp.mjs +96108 -0
  37. package/dist/migrate.mjs +4735 -0
  38. package/dist/pdf.worker.min.mjs +28 -0
  39. package/dist/public/app.js +150 -0
  40. package/dist/public/fonts/PretendardVariable.woff2 +0 -0
  41. package/dist/public/icon-180.png +0 -0
  42. package/dist/public/index.html +52 -0
  43. package/dist/public/lib.js +394 -0
  44. package/dist/public/pages/applications.js +107 -0
  45. package/dist/public/pages/documents.js +467 -0
  46. package/dist/public/pages/home.js +261 -0
  47. package/dist/public/pages/interview.js +227 -0
  48. package/dist/public/pages/jobs.js +493 -0
  49. package/dist/public/pages/profile.js +556 -0
  50. package/dist/public/pages/settings.js +240 -0
  51. package/dist/public/styles.css +498 -0
  52. package/dist/site/analytics.js +97 -0
  53. package/dist/site/favicon.svg +1 -0
  54. package/dist/site/fonts/PretendardVariable.woff2 +0 -0
  55. package/dist/site/icon-180.png +0 -0
  56. package/dist/site/icon-512.png +0 -0
  57. package/dist/site/index.html +598 -0
  58. package/dist/site/landing.js +54 -0
  59. package/dist/site/llms-install.txt +217 -0
  60. package/dist/site/robots.txt +4 -0
  61. package/dist/site/sitemap.xml +21 -0
  62. package/dist/site/start.html +326 -0
  63. package/dist/site/style.css +550 -0
  64. package/dist/site/vercel.json +20 -0
  65. package/dist/web.mjs +46852 -0
  66. package/docs/FAQ.md +101 -0
  67. package/docs/MCP_TOOLS.md +311 -0
  68. package/docs/README.md +28 -0
  69. package/docs/START_WORKFLOW.md +137 -0
  70. package/docs/SUPPORTED_AI_APPS.md +112 -0
  71. package/docs/WORKFLOWS.md +202 -0
  72. package/docs/career-os/README.md +78 -0
  73. package/docs/career-os/eop/cover-letter.md +46 -0
  74. package/docs/career-os/eop/fit-analysis.md +48 -0
  75. package/docs/career-os/eop/interview-prep.md +39 -0
  76. package/docs/career-os/eop/job-analysis.md +42 -0
  77. package/docs/career-os/eop/profile-extraction.md +37 -0
  78. package/docs/career-os/knowledge/ats.md +131 -0
  79. package/docs/career-os/knowledge/company-research.md +137 -0
  80. package/docs/career-os/knowledge/cover-letter.md +135 -0
  81. package/docs/career-os/knowledge/fit-matching.md +121 -0
  82. package/docs/career-os/knowledge/human-writing.md +118 -0
  83. package/docs/career-os/knowledge/interview-behavioral.md +132 -0
  84. package/docs/career-os/knowledge/interview-technical.md +126 -0
  85. package/docs/career-os/knowledge/linkedin-profile.md +152 -0
  86. package/docs/career-os/knowledge/networking-referrals.md +166 -0
  87. package/docs/career-os/knowledge/offer-evaluation-decision.md +121 -0
  88. package/docs/career-os/knowledge/onboarding-first-90-days.md +126 -0
  89. package/docs/career-os/knowledge/portfolio.md +121 -0
  90. package/docs/career-os/knowledge/recruiter-screen.md +105 -0
  91. package/docs/career-os/knowledge/rejection-triage-iteration.md +115 -0
  92. package/docs/career-os/knowledge/resume.md +123 -0
  93. package/docs/career-os/knowledge/salary-negotiation.md +123 -0
  94. package/docs/career-os/knowledge/verifiers/ats-compat.md +64 -0
  95. package/docs/career-os/knowledge/verifiers/consistency.md +61 -0
  96. package/docs/career-os/knowledge/verifiers/human-voice.md +82 -0
  97. package/docs/career-os/knowledge/verifiers/recency-staleness.md +72 -0
  98. package/docs/career-os/knowledge/verifiers/responsiveness-on-target.md +82 -0
  99. package/docs/career-os/knowledge/verifiers/truthfulness.md +67 -0
  100. package/package.json +89 -0
  101. package/packages/core/src/context.ts +74 -0
  102. package/packages/core/src/index.ts +8 -0
  103. package/packages/core/src/onboarding.ts +81 -0
  104. package/packages/core/src/services.ts +187 -0
  105. package/packages/core/src/summary.ts +104 -0
  106. package/packages/db/src/connection.ts +76 -0
  107. package/packages/db/src/index.ts +23 -0
  108. package/packages/db/src/paths.ts +52 -0
  109. package/packages/db/src/repositories.ts +891 -0
  110. package/packages/db/src/runtime.ts +58 -0
  111. package/packages/db/src/schema.ts +307 -0
  112. package/packages/exporters/src/html.ts +110 -0
  113. package/packages/exporters/src/index.ts +364 -0
  114. package/packages/exporters/src/markdown.ts +178 -0
  115. package/packages/knowledge/src/index.ts +168 -0
  116. package/packages/mcp-tools/src/bridge.ts +106 -0
  117. package/packages/mcp-tools/src/index.ts +17 -0
  118. package/packages/mcp-tools/src/result.ts +49 -0
  119. package/packages/mcp-tools/src/tools.ts +747 -0
  120. package/packages/mcp-tools/src/update.ts +330 -0
  121. package/packages/parsers/src/documents.ts +533 -0
  122. package/packages/parsers/src/html.ts +86 -0
  123. package/packages/parsers/src/index.ts +231 -0
  124. package/packages/parsers/src/keywords.ts +151 -0
  125. package/packages/prompts/src/humanize.ts +59 -0
  126. package/packages/prompts/src/index.ts +82 -0
  127. package/packages/prompts/src/install.ts +43 -0
  128. package/packages/prompts/src/onboarding.ts +35 -0
  129. package/packages/prompts/src/system.ts +57 -0
  130. package/packages/shared/src/build-info.ts +22 -0
  131. package/packages/shared/src/enums.ts +142 -0
  132. package/packages/shared/src/index.ts +19 -0
  133. package/packages/shared/src/schemas.ts +453 -0
  134. package/packages/workflows/src/definitions.ts +109 -0
  135. package/packages/workflows/src/index.ts +39 -0
  136. package/scripts/init.ts +371 -0
  137. package/tsconfig.json +32 -0
@@ -0,0 +1,498 @@
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
+ /* Warm paper neutrals (light) — unified with the public landing page */
23
+ --bg: #faf8f4;
24
+ --bg-subtle: #f2efe8;
25
+ --bg-sunken: #ece8df;
26
+ --surface: #ffffff;
27
+ --surface-hover: #f5f2ec;
28
+ --border: #e6e1d6;
29
+ --border-strong: #d6d0c2;
30
+ --text: #1c1a16;
31
+ --text-secondary: #595449;
32
+ --text-tertiary: #8b867a;
33
+ --text-on-accent: #ffffff;
34
+
35
+ /* Accent — deep emerald (single flat accent, no gradient) */
36
+ --accent: #0f7256;
37
+ --accent-hover: #0b5b44;
38
+ --accent-soft: #e7f1ec;
39
+ --accent-border: #bcdccd;
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: #14130f;
71
+ --bg-subtle: #201d17;
72
+ --bg-sunken: #100f0b;
73
+ --surface: #1b1915;
74
+ --surface-hover: #262219;
75
+ --border: #2e2a22;
76
+ --border-strong: #3e3930;
77
+ --text: #f2eee5;
78
+ --text-secondary: #aaa599;
79
+ --text-tertiary: #79746a;
80
+ --accent: #34d399;
81
+ --accent-hover: #6ee7b7;
82
+ --accent-soft: #11281d;
83
+ --accent-border: #1f4f3c;
84
+ --text-on-accent: #05241a;
85
+ --green-soft:#0e2a1a; --teal-soft:#0c2826; --blue-soft:#11203f; --violet-soft:#1e1638;
86
+ --amber-soft:#2a2008; --red-soft:#2c1414; --slate-soft:#1f242b;
87
+ /* lighter semantic foregrounds so status badges stay legible on dark soft bg */
88
+ --green:#4ade80; --teal:#2dd4bf; --blue:#60a5fa; --violet:#a78bfa; --amber:#fbbf24; --red:#ef4444; --slate:#94a3b8;
89
+ --green-border:#1f4f3c; --teal-border:#155e57; --blue-border:#1e3a6b; --violet-border:#3b2f6b; --amber-border:#5c4410; --red-border:#5c2020; --slate-border:#3a4250;
90
+ --shadow-lg: 0 12px 40px rgba(0,0,0,.5);
91
+ }
92
+ }
93
+ :root[data-theme='dark'] {
94
+ --bg:#14130f; --bg-subtle:#201d17; --bg-sunken:#100f0b; --surface:#1b1915; --surface-hover:#262219;
95
+ --border:#2e2a22; --border-strong:#3e3930; --text:#f2eee5; --text-secondary:#aaa599; --text-tertiary:#79746a;
96
+ --accent:#34d399; --accent-hover:#6ee7b7; --accent-soft:#11281d; --accent-border:#1f4f3c; --text-on-accent:#05241a;
97
+ --green-soft:#0e2a1a; --teal-soft:#0c2826; --blue-soft:#11203f; --violet-soft:#1e1638;
98
+ --amber-soft:#2a2008; --red-soft:#2c1414; --slate-soft:#1f242b;
99
+ --green:#4ade80; --teal:#2dd4bf; --blue:#60a5fa; --violet:#a78bfa; --amber:#fbbf24; --red:#ef4444; --slate:#94a3b8;
100
+ --green-border:#1f4f3c; --teal-border:#155e57; --blue-border:#1e3a6b; --violet-border:#3b2f6b; --amber-border:#5c4410; --red-border:#5c2020; --slate-border:#3a4250;
101
+ --shadow-lg:0 12px 40px rgba(0,0,0,.5);
102
+ }
103
+
104
+ * { box-sizing: border-box; }
105
+ /* Icons must never stretch inside flex containers. */
106
+ svg { flex: none; }
107
+ html, body { height: 100%; }
108
+ body {
109
+ margin: 0; font-family: var(--font); color: var(--text); background: var(--bg-subtle);
110
+ font-size: 15px; line-height: 1.55; -webkit-font-smoothing: antialiased;
111
+ text-rendering: optimizeLegibility;
112
+ }
113
+ a { color: var(--accent); text-decoration: none; }
114
+ a:hover { text-decoration: underline; }
115
+ h1,h2,h3,h4 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }
116
+ ::selection { background: var(--accent-soft); }
117
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
118
+
119
+ /* ----------------------------------------------------------------- layout */
120
+ .app { display: grid; grid-template-columns: var(--sidebar-w) 1fr; min-height: 100vh; }
121
+
122
+ .sidebar {
123
+ background: var(--bg); border-right: 1px solid var(--border);
124
+ display: flex; flex-direction: column; position: sticky; top: 0; height: 100vh;
125
+ }
126
+ .sidebar__brand { display: flex; align-items: center; gap: 10px; padding: 18px 18px 14px; }
127
+ .sidebar__logo {
128
+ width: 30px; height: 30px; border-radius: 8px; flex: none;
129
+ background: var(--accent);
130
+ display: grid; place-items: center; color: var(--text-on-accent); font-weight: 800; font-size: 15px;
131
+ }
132
+ .sidebar__title { font-weight: 700; font-size: 15px; letter-spacing: -.01em; }
133
+ .sidebar__subtitle { font-size: 12px; color: var(--text-tertiary); margin-top: 1px; }
134
+ .nav { padding: 6px 10px; display: flex; flex-direction: column; gap: 1px; flex: 1; overflow-y: auto; }
135
+ .nav__label { font-size: 11.5px; font-weight: 600; color: var(--text-tertiary); padding: 12px 10px 4px; text-transform: uppercase; letter-spacing: .04em; }
136
+ .nav__item {
137
+ display: flex; align-items: center; gap: 10px; padding: 7px 10px; border-radius: var(--radius-sm);
138
+ color: var(--text-secondary); font-weight: 500; cursor: pointer; user-select: none; position: relative;
139
+ }
140
+ .nav__item:hover { background: var(--surface-hover); color: var(--text); text-decoration: none; }
141
+ .nav__item.is-active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
142
+ .nav__item svg { width: 17px; height: 17px; flex: none; }
143
+ .nav__badge { margin-left: auto; font-size: 11.5px; background: var(--bg-sunken); color: var(--text-secondary);
144
+ padding: 1px 7px; border-radius: 999px; font-weight: 600; }
145
+ .nav__item.is-active .nav__badge { background: var(--accent); color: var(--text-on-accent); }
146
+ .sidebar__foot { padding: 12px 16px; border-top: 1px solid var(--border); font-size: 12px; color: var(--text-tertiary); }
147
+ .sidebar__foot .dot { display:inline-block; width:7px; height:7px; border-radius:999px; background: var(--green); margin-right:6px; }
148
+
149
+ .main { display: flex; flex-direction: column; min-width: 0; }
150
+ .topbar {
151
+ height: var(--topbar-h); border-bottom: 1px solid var(--border); background: color-mix(in srgb, var(--bg) 88%, transparent);
152
+ backdrop-filter: blur(8px); position: sticky; top: 0; z-index: 20;
153
+ display: flex; align-items: center; gap: 12px; padding: 0 24px;
154
+ }
155
+ .topbar__title { font-size: 15px; font-weight: 650; }
156
+ .topbar__crumb { color: var(--text-tertiary); }
157
+ .topbar__spacer { flex: 1; }
158
+ /* mobile nav: hamburger (hidden ≥900px) + off-canvas drawer scrim */
159
+ .nav-toggle { display: none; width: 34px; height: 34px; flex: none; place-items: center;
160
+ border: 1px solid var(--border); background: var(--surface); border-radius: var(--radius-sm);
161
+ color: var(--text-secondary); cursor: pointer; margin-right: 4px; }
162
+ .nav-toggle:hover { background: var(--surface-hover); color: var(--text); }
163
+ .nav-toggle svg { width: 20px; height: 20px; }
164
+ .nav-scrim { display: none; position: fixed; inset: 0; z-index: 40; background: rgba(0,0,0,.4); }
165
+ .view { padding: 24px; max-width: var(--maxw); width: 100%; margin: 0 auto; }
166
+ .view--wide { max-width: 1280px; }
167
+
168
+ .page-head { display: flex; align-items: flex-start; gap: 16px; margin-bottom: 20px; }
169
+ .page-head__text h1 { font-size: 22px; }
170
+ .page-head__text p { margin: 4px 0 0; color: var(--text-secondary); }
171
+ .page-head__actions { margin-left: auto; display: flex; gap: 8px; flex: none; }
172
+
173
+ /* ----------------------------------------------------------------- grid */
174
+ .grid { display: grid; gap: 16px; }
175
+ .grid--2 { grid-template-columns: repeat(2, 1fr); }
176
+ .grid--3 { grid-template-columns: repeat(3, 1fr); }
177
+ .grid--4 { grid-template-columns: repeat(4, 1fr); }
178
+ @media (max-width: 900px){
179
+ .grid--2,.grid--3,.grid--4{ grid-template-columns: 1fr; }
180
+ .app{ grid-template-columns: 1fr; }
181
+ /* sidebar slides in as an off-canvas drawer; the topbar hamburger toggles it */
182
+ .sidebar{ position: fixed; top: 0; left: 0; bottom: 0; height: 100%; z-index: 50;
183
+ transform: translateX(-100%); transition: transform .22s ease; box-shadow: var(--shadow-lg); }
184
+ .app.nav-open .sidebar{ transform: none; }
185
+ .app.nav-open .nav-scrim{ display: block; }
186
+ .nav-toggle{ display: inline-grid; }
187
+ }
188
+
189
+ /* ----------------------------------------------------------------- card */
190
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); }
191
+ .card__head { padding: 14px 18px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 10px; }
192
+ .card__head h3 { font-size: 15px; }
193
+ .card__head .sub { color: var(--text-tertiary); font-size: 13px; }
194
+ .card__head .right { margin-left: auto; display: flex; gap: 6px; }
195
+ .card__body { padding: 18px; }
196
+ .card__body--tight { padding: 0; }
197
+ /* a light bordered block for grouping inside a card/modal — never nest a full .card */
198
+ .subcard { border: 1px solid var(--border); border-radius: var(--radius); padding: 14px; }
199
+ .card.is-clickable { cursor: pointer; transition: border-color .12s, box-shadow .12s, transform .12s; }
200
+ .card.is-clickable:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); }
201
+
202
+ /* stat tiles */
203
+ .stat { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 16px 18px; }
204
+ .stat.is-clickable { cursor: pointer; transition: border-color .12s, box-shadow .12s; }
205
+ .stat.is-clickable:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); }
206
+ .stat__label { font-size: 13px; color: var(--text-secondary); display:flex; align-items:center; gap:7px; }
207
+ .stat__label svg { width: 15px; height: 15px; color: var(--text-tertiary); }
208
+ .stat__value { font-size: 26px; font-weight: 700; letter-spacing: -.02em; margin-top: 6px; }
209
+ .stat__hint { font-size: 12.5px; color: var(--text-tertiary); margin-top: 2px; }
210
+ /* borderless count strip (settings 내 데이터) — flat counts, never a card-in-card */
211
+ .countstrip { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px 24px; }
212
+ .countstrip__item { display: flex; flex-direction: column; gap: 2px; }
213
+ .countstrip__num { font-size: 24px; font-weight: 700; letter-spacing: -.02em; }
214
+ .countstrip__label { font-size: 13px; color: var(--text-secondary); }
215
+ @media (max-width: 560px){ .countstrip { grid-template-columns: repeat(2, 1fr); } }
216
+
217
+ /* ----------------------------------------------------------------- buttons */
218
+ .btn {
219
+ display: inline-flex; align-items: center; gap: 7px; height: 34px; padding: 0 13px;
220
+ border-radius: var(--radius-sm); border: 1px solid var(--border-strong); background: var(--surface);
221
+ color: var(--text); font-weight: 550; font-size: 14px; cursor: pointer; font-family: inherit;
222
+ transition: background .12s, border-color .12s, opacity .12s; white-space: nowrap; user-select: none;
223
+ }
224
+ .btn:hover { background: var(--surface-hover); }
225
+ .btn:disabled { opacity: .5; cursor: not-allowed; }
226
+ .btn svg { width: 15px; height: 15px; }
227
+ .btn--primary { background: var(--accent); border-color: var(--accent); color: var(--text-on-accent); }
228
+ .btn--primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
229
+ .btn--ghost { border-color: transparent; background: transparent; }
230
+ .btn--ghost:hover { background: var(--surface-hover); }
231
+ .btn--danger { color: var(--red); border-color: var(--red-border); background: var(--red-soft); }
232
+ .btn--danger:hover { background: var(--red); color:#fff; border-color: var(--red); }
233
+ .btn--sm { height: 28px; padding: 0 10px; font-size: 13px; }
234
+ .btn--block { width: 100%; justify-content: center; }
235
+ .icon-btn { width: 30px; height: 30px; padding: 0; justify-content: center; }
236
+ /* a ghost button marked as the selected option (segmented toggle), e.g. theme picker */
237
+ .btn.is-selected { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-border); }
238
+
239
+ /* ----------------------------------------------------------------- forms */
240
+ .field { margin-bottom: 14px; }
241
+ .field > label { display: block; font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; }
242
+ .field .hint { font-size: 12px; color: var(--text-tertiary); margin-top: 5px; }
243
+ .input, .textarea, .select {
244
+ width: 100%; font-family: inherit; font-size: 14px; color: var(--text);
245
+ background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
246
+ padding: 8px 11px; transition: border-color .12s, box-shadow .12s;
247
+ }
248
+ .input:focus, .textarea:focus, .select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
249
+ /* keyboard focus must be clearly visible (the soft ring alone fails WCAG 1.4.11/2.4.11) */
250
+ .input:focus-visible, .textarea:focus-visible, .select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; box-shadow: none; }
251
+ /* skip link — hidden until focused, then drops into the top-left */
252
+ .skip-link { position: fixed; top: 8px; left: 8px; z-index: 200; transform: translateY(-200%);
253
+ background: var(--accent); color: var(--text-on-accent); padding: 8px 14px; border: 0;
254
+ border-radius: var(--radius-sm); font: inherit; font-weight: 600; cursor: pointer; transition: transform .15s; }
255
+ .skip-link:focus { transform: none; outline: 2px solid var(--text-on-accent); outline-offset: 2px; }
256
+ .textarea { resize: vertical; min-height: 90px; line-height: 1.6; }
257
+ .textarea--lg { min-height: 280px; font-size: 15px; }
258
+ .input::placeholder, .textarea::placeholder { color: var(--text-tertiary); }
259
+ .row { display: flex; gap: 10px; }
260
+ .row > * { flex: 1; }
261
+ /* compact inline input for the topbar/toolbar (search boxes) */
262
+ .input--inline { width: 220px; max-width: 46vw; }
263
+
264
+ /* ----------------------------------------------------------------- badges */
265
+ .badge { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; font-weight: 600;
266
+ padding: 2px 9px; border-radius: 999px; border: 1px solid var(--border); background: var(--bg-sunken); color: var(--text-secondary); white-space: nowrap; }
267
+ .badge .dot { width: 6px; height: 6px; border-radius: 999px; background: currentColor; }
268
+ .badge--draft { color: var(--slate); background: var(--slate-soft); border-color: var(--slate-border); }
269
+ .badge--planned { color: var(--blue); background: var(--blue-soft); border-color: var(--blue-border); }
270
+ .badge--applied { color: var(--violet); background: var(--violet-soft); border-color: var(--violet-border); }
271
+ .badge--document_passed { color: var(--teal); background: var(--teal-soft); border-color: var(--teal-border); }
272
+ .badge--interview { color: var(--amber); background: var(--amber-soft); border-color: var(--amber-border); }
273
+ .badge--final_passed { color: var(--green); background: var(--green-soft); border-color: var(--green-border); }
274
+ .badge--rejected { color: var(--red); background: var(--red-soft); border-color: var(--red-border); }
275
+ .badge--on_hold { color: var(--slate); background: var(--slate-soft); border-color: var(--slate-border); }
276
+ .badge--accent { color: var(--accent); background: var(--accent-soft); border-color: var(--accent-border); }
277
+ .badge--score { font-variant-numeric: tabular-nums; }
278
+
279
+ .chip { display:inline-flex; align-items:center; gap:6px; font-size:12px; padding:3px 9px; border-radius:7px;
280
+ background: var(--bg-sunken); color: var(--text-secondary); border:1px solid var(--border); }
281
+ .chip--accent { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-border); }
282
+ .chips { display:flex; flex-wrap:wrap; gap:6px; }
283
+
284
+ /* ----------------------------------------------------------------- table */
285
+ .table { width: 100%; border-collapse: collapse; }
286
+ .table th { text-align: left; font-size: 12px; font-weight: 600; color: var(--text-tertiary); text-transform: uppercase;
287
+ letter-spacing: .03em; padding: 10px 16px; border-bottom: 1px solid var(--border); }
288
+ .table td { padding: 12px 16px; border-bottom: 1px solid var(--border); vertical-align: middle; }
289
+ .table tr:last-child td { border-bottom: none; }
290
+ .table tbody tr.is-clickable { cursor: pointer; }
291
+ .table tbody tr.is-clickable:hover { background: var(--surface-hover); }
292
+ .table .muted { color: var(--text-tertiary); }
293
+ .table .strong { font-weight: 600; }
294
+ /* keyboard-reachable lead-cell link that looks identical to plain text */
295
+ .cell-link { color: inherit; text-decoration: none; }
296
+ .cell-link:hover { text-decoration: none; }
297
+
298
+ /* ----------------------------------------------------------------- progress */
299
+ .progress { height: 7px; background: var(--bg-sunken); border-radius: 999px; overflow: hidden; }
300
+ .progress__bar { height: 100%; background: var(--accent); border-radius: 999px; transition: width .4s ease; }
301
+
302
+ /* ----------------------------------------------------------------- empty state */
303
+ .empty { text-align: center; padding: 44px 24px; color: var(--text-secondary); }
304
+ .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); }
305
+ .empty__icon svg { width: 22px; height: 22px; }
306
+ .empty h3 { font-size: 15px; color: var(--text); margin-bottom: 6px; }
307
+ .empty p { max-width: 420px; margin: 0 auto 16px; font-size: 13.5px; }
308
+
309
+ /* ----------------------------------------------------------------- timeline / versions */
310
+ .timeline { display: flex; flex-direction: column; }
311
+ .tl-item { display: grid; grid-template-columns: 22px 1fr; gap: 12px; padding-bottom: 4px; }
312
+ .tl-item__rail { display: flex; flex-direction: column; align-items: center; }
313
+ .tl-item__dot { width: 11px; height: 11px; border-radius: 999px; background: var(--surface); border: 2px solid var(--border-strong); margin-top: 4px; flex: none; }
314
+ .tl-item.is-current .tl-item__dot { background: var(--accent); border-color: var(--accent); }
315
+ .tl-item__line { width: 2px; flex: 1; background: var(--border); margin: 4px 0; }
316
+ .tl-item:last-child .tl-item__line { display: none; }
317
+ .tl-item__body { padding-bottom: 16px; }
318
+
319
+ /* ----------------------------------------------------------------- kanban */
320
+ .board { display: grid; grid-auto-flow: column; grid-auto-columns: 272px; gap: 14px; overflow-x: auto; padding-bottom: 8px; }
321
+ .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); }
322
+ .board__col-head { padding: 11px 14px; display: flex; align-items: center; gap: 8px; position: sticky; top: 0; }
323
+ .board__col-head .count { margin-left: auto; font-size: 12.5px; color: var(--text-tertiary); font-weight: 600; }
324
+ .board__cards { padding: 0 10px 12px; display: flex; flex-direction: column; gap: 8px; overflow-y: auto; }
325
+ .board-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 11px 12px; cursor: pointer; box-shadow: var(--shadow-sm); }
326
+ .board-card:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); }
327
+ .board-card__company { font-weight: 600; font-size: 14px; }
328
+ .board-card__role { font-size: 13px; color: var(--text-secondary); margin-top: 1px; }
329
+ .board-card__meta { display: flex; align-items: center; gap: 8px; margin-top: 9px; }
330
+
331
+ /* ----------------------------------------------------------------- list row (shared)
332
+ One primitive behind the home action lane, recent-job list, and any dense list.
333
+ Leading slot → title + muted sub → right-aligned trailing (badges/score). */
334
+ .list-row { display: flex; align-items: center; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--border); }
335
+ .list-row:last-child { border-bottom: none; }
336
+ .list-row.is-clickable { cursor: pointer; transition: background .12s; }
337
+ .list-row.is-clickable:hover { background: var(--surface-hover); }
338
+ .list-row__lead { flex: none; display: grid; place-items: center; }
339
+ .list-row__lead--chip { width: 30px; height: 30px; border-radius: 8px; background: var(--bg-sunken); color: var(--text-tertiary); }
340
+ .list-row__lead--chip svg { width: 16px; height: 16px; }
341
+ .list-row__dot { width: 9px; height: 9px; border-radius: 999px; flex: none; }
342
+ .list-row__main { flex: 1; min-width: 0; }
343
+ .list-row__title { font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
344
+ .list-row__sub { font-size: 13px; color: var(--text-tertiary); margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
345
+ .list-row__trail { flex: none; display: flex; align-items: center; gap: 8px; margin-left: auto; white-space: nowrap; }
346
+
347
+ /* applications: status-group section header (reads clearly as a section title) */
348
+ .app-group__head { display: flex; align-items: center; gap: 9px; padding-bottom: 8px; margin-bottom: 2px; border-bottom: 1px solid var(--border); }
349
+ .app-group__dot { width: 9px; height: 9px; border-radius: 999px; flex: none; }
350
+ .app-group__title { font-weight: 650; font-size: 15px; letter-spacing: -.01em; }
351
+ .app-group__count { margin-left: auto; color: var(--text-tertiary); font-size: 13px; }
352
+
353
+ /* row-level action (e.g. status select) — hidden until the row is hovered or
354
+ focused, so grouped lists stay calm. Always shown where hover isn't available. */
355
+ .list-row .row-action { opacity: 0; pointer-events: none; transition: opacity .12s; }
356
+ .list-row:hover .row-action,
357
+ .list-row:focus-within .row-action { opacity: 1; pointer-events: auto; }
358
+ @media (hover: none) { .list-row .row-action { opacity: 1; pointer-events: auto; } }
359
+
360
+ /* ----------------------------------------------------------------- pipeline bar + funnel
361
+ Visualizes the application status_breakdown. Segment color == badge--{status} hue,
362
+ so a colour means the same stage everywhere (board, badges, here). */
363
+ .pipebar { display: flex; height: 14px; border-radius: 999px; overflow: hidden; background: var(--bg-sunken); gap: 2px; }
364
+ .pipebar__seg { min-width: 8px; cursor: pointer; transition: flex-grow .4s ease, opacity .12s; }
365
+ .pipebar__seg:hover { opacity: .8; }
366
+ .pipefunnel { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-top: 18px; }
367
+ .pipefunnel__tile { cursor: pointer; }
368
+ .pipefunnel__label { font-size: 13px; color: var(--text-secondary); display: flex; align-items: center; gap: 6px; }
369
+ .pipefunnel__dot { width: 8px; height: 8px; border-radius: 999px; flex: none; }
370
+ .pipefunnel__value { font-size: 24px; font-weight: 700; letter-spacing: -.02em; margin-top: 5px; line-height: 1.1; }
371
+ .pipefunnel__conv { font-size: 12px; color: var(--text-tertiary); margin-top: 2px; }
372
+ @media (max-width: 560px){ .pipefunnel { grid-template-columns: repeat(2, 1fr); } }
373
+
374
+ /* ----------------------------------------------------------------- check row (onboarding)
375
+ The ONE place setup guidance lives. Outcome rows auto-detect completion. */
376
+ .check-row { display: flex; align-items: center; gap: 10px; padding: 7px 0; }
377
+ .check-row__dot { width: 16px; height: 16px; border: 2px solid var(--border-strong); border-radius: 999px; flex: none; box-sizing: border-box; }
378
+ .check-row__icon { width: 16px; height: 16px; flex: none; color: var(--green); }
379
+ .check-row__label { font-size: 14px; }
380
+ .check-row.is-done .check-row__label { color: var(--text-tertiary); text-decoration: line-through; }
381
+ .check-row__label.is-link { color: var(--accent); cursor: pointer; }
382
+ .check-row__label.is-link:hover { text-decoration: underline; }
383
+
384
+ /* ----------------------------------------------------------------- activity feed (day-bucketed) */
385
+ .feed-group { font-size: 11.5px; font-weight: 600; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: .04em; padding: 14px 0 6px; }
386
+ .feed-group:first-child { padding-top: 0; }
387
+ .feed-item { display: flex; gap: 12px; align-items: flex-start; padding: 6px 0; }
388
+ .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); }
389
+ .feed-item__icon svg { width: 16px; height: 16px; }
390
+ .feed-item__body { flex: 1; min-width: 0; }
391
+ .feed-item__text { font-size: 13.5px; line-height: 1.5; }
392
+ .feed-item__time { display: block; font-size: 12.5px; color: var(--text-tertiary); margin-top: 1px; }
393
+
394
+ /* ----------------------------------------------------------------- first-run hero
395
+ Day one: a single getting-started card centered on a quiet dot canvas, instead
396
+ of a wall of empty cards. */
397
+ .firstrun-hero { display: grid; place-items: start center; padding: 28px 0;
398
+ background-image: radial-gradient(var(--border) 1px, transparent 1px); background-size: 24px 24px; border-radius: var(--radius-lg); }
399
+ .firstrun-hero > .card { width: 100%; max-width: 540px; }
400
+
401
+ /* ----------------------------------------------------------------- doc preview */
402
+ .doc-preview { white-space: pre-wrap; word-break: break-word; font-size: 15px; line-height: 1.75; color: var(--text);
403
+ background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 22px 26px; max-height: 60vh; overflow-y: auto; }
404
+ .kv { display: grid; grid-template-columns: 132px 1fr; gap: 8px 16px; }
405
+ .kv dt { color: var(--text-tertiary); font-size: 13px; }
406
+ /* min-width:0 overrides a grid item's default min-width:auto so the 1fr column
407
+ can shrink below its content — otherwise a long path/value overflows the grid. */
408
+ .kv dd { margin: 0; min-width: 0; }
409
+
410
+ /* ----------------------------------------------------------------- modal */
411
+ .modal-root { position: fixed; inset: 0; z-index: 60; display: none; }
412
+ .modal-root.is-open { display: block; }
413
+ .modal__scrim { position: absolute; inset: 0; background: rgba(16,16,20,.42); backdrop-filter: blur(2px); animation: fade .15s; }
414
+ .modal { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); width: min(640px, calc(100vw - 32px));
415
+ max-height: calc(100vh - 48px); background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
416
+ box-shadow: var(--shadow-lg); display: flex; flex-direction: column; animation: pop .16s ease; }
417
+ .modal--lg { width: min(880px, calc(100vw - 32px)); }
418
+ .modal__head { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; }
419
+ .modal__head h3 { font-size: 15px; }
420
+ .modal__body { padding: 20px; overflow-y: auto; }
421
+ .modal__error { margin: 0 20px 4px; padding: 10px 12px; border-radius: var(--radius); border: 1px solid var(--red-border);
422
+ background: var(--red-soft); color: var(--red); font-size: 13px; line-height: 1.5; }
423
+ .modal__error[hidden] { display: none; }
424
+ .modal__foot { padding: 14px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 8px; }
425
+ @keyframes pop { from { transform: translate(-50%,-46%); opacity: 0; } to { transform: translate(-50%,-50%); opacity: 1; } }
426
+ @keyframes fade { from { opacity: 0; } to { opacity: 1; } }
427
+
428
+ /* ----------------------------------------------------------------- toast */
429
+ .toasts { position: fixed; bottom: 20px; right: 20px; z-index: 80; display: flex; flex-direction: column; gap: 10px; }
430
+ .toast { background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--accent); border-radius: var(--radius);
431
+ box-shadow: var(--shadow-md); padding: 12px 16px; min-width: 260px; max-width: 380px; animation: slidein .2s ease; font-size: 14px; }
432
+ .toast--success { border-left-color: var(--green); }
433
+ .toast--error { border-left-color: var(--red); }
434
+ .toast__title { font-weight: 600; margin-bottom: 2px; }
435
+ .toast__body { color: var(--text-secondary); font-size: 13px; }
436
+ @keyframes slidein { from { transform: translateX(20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
437
+
438
+ /* ----------------------------------------------------------------- tabs */
439
+ .tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); margin-bottom: 18px; }
440
+ .tab { padding: 9px 14px; font-size: 14px; font-weight: 550; color: var(--text-secondary); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; }
441
+ .tab:hover { color: var(--text); }
442
+ .tab.is-active { color: var(--accent); border-bottom-color: var(--accent); }
443
+
444
+ /* ----------------------------------------------------------------- utils */
445
+ .muted { color: var(--text-tertiary); }
446
+ .text-sm { font-size: 13px; }
447
+ .text-secondary { color: var(--text-secondary); }
448
+ .stack { display: flex; flex-direction: column; }
449
+ .stack-2 > * + * { margin-top: 8px; } .stack-3 > * + * { margin-top: 12px; } .stack-4 > * + * { margin-top: 16px; }
450
+ .flex { display: flex; align-items: center; } .flex-col{ display:flex; flex-direction:column; }
451
+ .gap-2 { gap: 8px; } .gap-3 { gap: 12px; } .gap-4 { gap: 16px; }
452
+ .between { justify-content: space-between; } .center { align-items: center; } .wrap { flex-wrap: wrap; }
453
+ .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;}
454
+ .tnum { font-variant-numeric: tabular-nums; }
455
+ /* min-width:0 lets a truncating element actually shrink inside a flex row;
456
+ without it a long unbreakable value (e.g. a data-folder path) forces the
457
+ flex item to its content width and overflows the viewport on narrow screens. */
458
+ .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; min-width: 0; }
459
+ .divider { height: 1px; background: var(--border); margin: 16px 0; }
460
+ .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; }
461
+ @keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }
462
+ .hide { display: none !important; }
463
+ .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
464
+
465
+ /* score color helper */
466
+ .score-strong { color: var(--green); } .score-mid { color: var(--amber); } .score-weak { color: var(--red); }
467
+
468
+ /* banner / callout */
469
+ .callout { display: flex; gap: 12px; padding: 14px 16px; border-radius: var(--radius); border: 1px solid var(--accent-border); background: var(--accent-soft); }
470
+ .callout svg { flex: none; width: 18px; height: 18px; color: var(--accent); margin-top: 1px; }
471
+ .callout--privacy { border-color: var(--border); background: var(--bg-subtle); }
472
+ .callout--privacy svg { color: var(--text-tertiary); }
473
+ .callout__title { font-weight: 600; margin-bottom: 2px; }
474
+ .callout__body { color: var(--text-secondary); font-size: 13px; }
475
+
476
+ /* ----------------------------------------------------------------- phone layout */
477
+ /* Placed last so these win over the base widths (media queries add no specificity). */
478
+ @media (max-width: 640px){
479
+ .topbar{ padding: 0 12px; gap: 8px; }
480
+ /* title takes the room between hamburger and actions and truncates instead of
481
+ collapsing to vertical text; the flex spacer isn't needed at this width */
482
+ .topbar__title{ flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
483
+ .topbar__spacer{ display: none; }
484
+ .input--inline{ width: 110px; max-width: 32vw; }
485
+ .view{ padding: 16px; }
486
+ /* jobs table: drop the secondary 위치·마감일 column so 회사·직무·적합도·상태 all fit */
487
+ .table--jobs th:nth-child(2), .table--jobs td:nth-child(2){ display: none; }
488
+ /* keep the 적합도 score ("88점") on one line in the narrow column */
489
+ .table--jobs th:nth-child(3), .table--jobs td:nth-child(3){ white-space: nowrap; }
490
+ .table--jobs td:nth-child(3) .tnum{ font-size: 13px; }
491
+ /* any remaining wide table can still scroll instead of crushing columns */
492
+ .table-scroll{ overflow-x: auto; -webkit-overflow-scrolling: touch; }
493
+ /* stack key/value so long emails·paths·URLs aren't clipped by the fixed label column */
494
+ .kv{ grid-template-columns: 1fr; gap: 0; }
495
+ .kv dt{ margin-top: 10px; }
496
+ .kv dd{ margin: 0 0 2px; }
497
+ .page-head{ flex-wrap: wrap; }
498
+ }
@@ -0,0 +1,97 @@
1
+ // =============================================================================
2
+ // CareerMate marketing-site analytics — careermate.life ONLY.
3
+ //
4
+ // SCOPE: this file loads on the PUBLIC marketing/demo site (site/) that
5
+ // Vercel serves. It must NEVER be loaded by the local dashboard (apps/web, bound
6
+ // to 127.0.0.1) — that would break the core promise in AGENTS.md:
7
+ // "절대 사용자 데이터를 외부로 업로드하지 마라."
8
+ // There is no user career data on this site; only anonymous page/traffic signals.
9
+ //
10
+ // What runs here:
11
+ // 1) Vercel Web Analytics — cookieless, no PII, no consent banner required.
12
+ // 2) Vercel Speed Insights — Core Web Vitals, also cookieless.
13
+ // 3) Google Analytics 4 — OPTIONAL. Off until GA_MEASUREMENT_ID is filled in.
14
+ // Runs under Google Consent Mode v2 with analytics_storage DENIED by default,
15
+ // i.e. cookieless pings, so it needs no consent banner and stays consistent
16
+ // with the local-first / privacy-first brand. (Flip consent to 'granted'
17
+ // later only if you add a real consent UI.)
18
+ //
19
+ // The marketing pages (index.html, start.html) are classic multi-page navigations,
20
+ // so a single pageview on load is enough. The demo (/demo/) is a hash-router SPA,
21
+ // so we emit a virtual pageview on every hashchange — otherwise every sub-route
22
+ // (#/jobs, #/applications, …) would collapse into one "/demo" view.
23
+ // =============================================================================
24
+ (function () {
25
+ 'use strict';
26
+
27
+ // ── 1. Vercel Web Analytics (cookieless) ───────────────────────────────────
28
+ // Queue shim per Vercel's HTML snippet; the injected script drains window.vaq.
29
+ window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };
30
+ loadScript('/_vercel/insights/script.js');
31
+
32
+ // ── 2. Vercel Speed Insights (cookieless) — OFF by default ──────────────────
33
+ // Vercel's Hobby plan allows Speed Insights on only ONE project at a time, and it's
34
+ // more valuable on the real app than on this rarely-changing static site (use the free
35
+ // PageSpeed Insights for ad-hoc checks here). Flip to true to load it (needs Enable in
36
+ // the Vercel dashboard too); leaving it false avoids a dead 404 script request.
37
+ var ENABLE_SPEED_INSIGHTS = false;
38
+ if (ENABLE_SPEED_INSIGHTS) {
39
+ window.si = window.si || function () { (window.siq = window.siq || []).push(arguments); };
40
+ loadScript('/_vercel/speed-insights/script.js');
41
+ }
42
+
43
+ // ── 3. Google Analytics 4 (optional, cookieless via Consent Mode v2) ────────
44
+ // CareerMate's GA4 measurement ID — a public, client-side identifier (not a secret).
45
+ // Set to '' to disable GA entirely; the Vercel analytics above still runs.
46
+ var GA_MEASUREMENT_ID = 'G-HK9KWTYCBD';
47
+ if (/^G-[A-Z0-9]+$/.test(GA_MEASUREMENT_ID)) initGoogleAnalytics(GA_MEASUREMENT_ID);
48
+
49
+ // ── Demo SPA: report hash route changes to Vercel as a custom event ─────────
50
+ // Vercel's script auto-tracks History API navigations but not hashchange.
51
+ if (location.pathname.indexOf('/demo') === 0) {
52
+ window.addEventListener('hashchange', function () {
53
+ window.va('event', { name: 'demo_route', data: { route: currentRoute() } });
54
+ });
55
+ }
56
+
57
+ // ── helpers ─────────────────────────────────────────────────────────────────
58
+ function loadScript(src) {
59
+ var s = document.createElement('script');
60
+ s.src = src;
61
+ s.defer = true;
62
+ document.head.appendChild(s);
63
+ }
64
+
65
+ function currentRoute() {
66
+ return (location.hash || '').replace(/^#\/?/, '').split('/')[0] || 'home';
67
+ }
68
+
69
+ function initGoogleAnalytics(id) {
70
+ window.dataLayer = window.dataLayer || [];
71
+ function gtag() { window.dataLayer.push(arguments); }
72
+ window.gtag = gtag;
73
+ // Deny storage by default → GA runs cookieless. No banner required.
74
+ gtag('consent', 'default', {
75
+ ad_storage: 'denied',
76
+ ad_user_data: 'denied',
77
+ ad_personalization: 'denied',
78
+ analytics_storage: 'denied',
79
+ });
80
+ gtag('js', new Date());
81
+ // send_page_view:false so we control pageviews (incl. the demo's hash routes).
82
+ gtag('config', id, { send_page_view: false, anonymize_ip: true });
83
+ loadScript('https://www.googletagmanager.com/gtag/js?id=' + id);
84
+
85
+ sendGaPageview();
86
+ window.addEventListener('hashchange', sendGaPageview);
87
+ }
88
+
89
+ function sendGaPageview() {
90
+ if (!window.gtag) return;
91
+ window.gtag('event', 'page_view', {
92
+ page_path: location.pathname + location.search + location.hash,
93
+ page_location: location.href,
94
+ page_title: document.title,
95
+ });
96
+ }
97
+ })();
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="24" height="24" rx="6" fill="#0f7256"/><text x="12" y="17" font-size="14" text-anchor="middle" fill="#fff" font-family="Segoe UI,sans-serif" font-weight="bold">C</text></svg>
Binary file
Binary file