create-foliokit 1.0.0 → 1.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.
@@ -15,6 +15,7 @@
15
15
  --elevated: #090909;
16
16
  --secondary: #0b0b0b;
17
17
  --lime: #e5cf9c;
18
+ --social-hover: #00e676;
18
19
  --pink: #ec4899;
19
20
  --violet: #a78bfa;
20
21
  --warm-white: #ffffffde;
@@ -26,6 +27,8 @@
26
27
  --frame-dot-hover: #3a352b;
27
28
  --content-width: 880px;
28
29
  --page-gutter: 16px;
30
+ --grid-size: 40px;
31
+ --grid-line: rgba(255, 255, 255, .18);
29
32
  --section-space: 20px;
30
33
  --section-space-end: 22px;
31
34
  --panel-inset: 12px;
@@ -44,41 +47,21 @@
44
47
 
45
48
  * { box-sizing: border-box; }
46
49
  html, body { overflow-x: clip; }
47
- body { min-width: 320px; margin: 0; color: var(--warm-white); background: #000000; isolation: isolate; }
50
+ body { position: relative; min-width: 320px; margin: 0; color: var(--warm-white); background: #000000; isolation: isolate; }
48
51
  body::before {
49
- position: fixed;
52
+ position: absolute;
50
53
  z-index: -1;
51
54
  inset: 0;
52
55
  pointer-events: none;
53
- background: url('/stars-landscape.svg') center / cover no-repeat;
54
- opacity: .9;
55
- content: '';
56
- }
57
- body::after {
58
- --near-stars: url('/stars-foreground-landscape.svg');
59
- position: fixed;
60
- z-index: -1;
61
- inset: -2%;
62
- pointer-events: none;
56
+ background-color: var(--obsidian);
63
57
  background-image:
64
- radial-gradient(ellipse at 16% 26%, rgba(255, 255, 255, .004), transparent 43%),
65
- radial-gradient(ellipse at 82% 70%, rgba(255, 255, 255, .003), transparent 44%),
66
- radial-gradient(ellipse at 50% 100%, rgba(255, 255, 255, .002), transparent 48%),
67
- var(--near-stars);
68
- background-position: center;
69
- background-size: 100% 100%, 100% 100%, 100% 100%, cover;
70
- background-repeat: no-repeat;
71
- opacity: .96;
72
- mix-blend-mode: screen;
73
- transform-origin: center;
74
- animation: galaxy-depth 36s ease-in-out infinite alternate;
75
- will-change: transform;
58
+ linear-gradient(var(--grid-line) 1px, transparent 1px),
59
+ linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
60
+ background-position: center center, center center;
61
+ background-size: var(--grid-size) var(--grid-size);
62
+ background-repeat: repeat;
76
63
  content: '';
77
64
  }
78
- @keyframes galaxy-depth {
79
- from { transform: translate3d(-.45%, .3%, 0) scale(1); }
80
- to { transform: translate3d(.35%, -.25%, 0) scale(1.025); }
81
- }
82
65
  a { color: inherit; text-decoration: none; transition: color var(--interaction-duration) var(--interaction-ease); }
83
66
  a:hover, a:focus-visible { color: var(--lime); }
84
67
  button, a { -webkit-tap-highlight-color: transparent; }
@@ -86,11 +69,12 @@ button:focus-visible, a:focus-visible { outline: 2px solid var(--lime); outline-
86
69
  h1, h2, h3, p { margin-top: 0; }
87
70
  h1, h2, h3 { font-family: var(--heading-font); }
88
71
  .mono { font-family: var(--mono-font); }
89
- .section-wrap { width: min(var(--content-width), calc(100% - var(--page-gutter) - var(--page-gutter))); margin-inline: auto; }
72
+ .section-wrap { width: min(var(--content-width), calc(100% - var(--page-gutter) - var(--page-gutter))); margin-inline: auto; background: var(--obsidian); }
90
73
  .dotted-frame { position: relative; }
91
74
  .dotted-frame::before { position: absolute; z-index: 0; inset: 0; pointer-events: none; background-image: repeating-linear-gradient(90deg, var(--frame-dot) 0 1px, transparent 1px 5px), repeating-linear-gradient(90deg, var(--frame-dot) 0 1px, transparent 1px 5px), repeating-linear-gradient(0deg, var(--frame-dot) 0 1px, transparent 1px 5px), repeating-linear-gradient(0deg, var(--frame-dot) 0 1px, transparent 1px 5px); background-repeat: no-repeat; background-position: center top, center bottom, left center, right center; background-size: calc(100% - 16px) 1px, calc(100% - 16px) 1px, 1px calc(100% - 16px), 1px calc(100% - 16px); content: ''; transition: background-image .18s ease; }
92
75
  .dotted-frame > * { position: relative; z-index: 1; }
93
76
 
77
+
94
78
  .site-header {
95
79
  position: sticky;
96
80
  top: 0;
@@ -124,7 +108,6 @@ h1, h2, h3 { font-family: var(--heading-font); }
124
108
  a.text-interaction:hover, a.text-interaction:focus-visible,
125
109
  a:hover .text-interaction, a:focus-visible .text-interaction,
126
110
  .project-card:hover .text-interaction, .project-card:focus-within .text-interaction,
127
- .portfolio-history-row:hover .text-interaction, .portfolio-history-row:focus-within .text-interaction,
128
111
  .contact-submit:hover .text-interaction, .contact-submit:focus-visible .text-interaction { color: var(--lime); }
129
112
  .main-nav .nav-link.is-active { color: var(--lime); }
130
113
  .main-nav .nav-link { transition: color var(--interaction-duration) var(--interaction-ease); }
@@ -211,13 +194,9 @@ a:not(.social-link):not(.contact-social-link):hover > img, a:not(.social-link):n
211
194
  .profile-photo-stage { position: relative; width: 100px; height: 96px; flex: 0 0 100px; }
212
195
  .profile-photo { position: absolute; top: 6px; left: 8px; z-index: 2; display: block; width: 84px; height: 84px; border: 1px solid color-mix(in srgb, var(--warm-white) 26%, transparent); border-radius: 50%; object-fit: cover; object-position: center; }
213
196
  .hero h1 { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-start; gap: 7px; margin: 0; color: var(--warm-white); font: 600 25px/1.15 var(--heading-font); letter-spacing: -.05em; }
214
- .name-heart-wrap { position: relative; display: inline-flex; flex: 0 0 auto; align-items: center; }
215
- .name-heart-icon { display: block; width: 20px; height: 20px; object-fit: contain; animation: heart-beat 1.15s ease-in-out infinite; transform-origin: center; }
197
+ .verified-mark { display: inline-flex; width: 20px; height: 20px; flex: 0 0 20px; align-self: center; align-items: center; justify-content: center; color: #fff; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .45)); }
198
+ .verified-mark svg { display: block; width: 100%; height: 100%; }
216
199
  .skill-name, .site-tooltip { padding: 6px 9px; border: 1px dotted var(--frame-dot-hover); border-radius: 4px; color: var(--warm-white); background: var(--elevated); font: 10px/1.3 var(--body-font); font-weight: 400; letter-spacing: 0; text-align: center; white-space: nowrap; box-shadow: 0 4px 14px rgba(0, 0, 0, .16); }
217
- .heart-message { position: absolute; top: 50%; left: calc(100% + 10px); z-index: 6; width: max-content; max-width: min(190px, calc(100vw - 24px)); padding: 7px 10px; border: 1px dotted var(--frame-dot-hover); border-radius: 4px; color: var(--warm-white); background: var(--elevated); font: 11px/1.35 var(--body-font); letter-spacing: .01em; text-align: center; white-space: nowrap; box-shadow: 0 4px 14px rgba(0, 0, 0, .16); opacity: 0; visibility: hidden; pointer-events: none; transform: translate(-3px, -50%); transition: opacity 180ms var(--interaction-ease), transform 180ms var(--interaction-ease), visibility 180ms; }
218
- @media (hover: hover) and (pointer: fine) {
219
- .name-heart-wrap:hover .heart-message { opacity: 1; visibility: visible; transform: translate(0, -50%); }
220
- }
221
200
  .site-tooltip { position: fixed; z-index: 100; max-width: min(220px, calc(100vw - 24px)); overflow: hidden; text-overflow: ellipsis; opacity: 0; pointer-events: none; transform: translate(-50%, -100%) scale(.98); animation: tooltip-enter .16s ease both; }
222
201
  .site-tooltip.is-below { transform: translate(-50%, 0) scale(.98); }
223
202
  @keyframes tooltip-enter { from { opacity: 0; transform: translate(-50%, -100%) scale(.97); } to { opacity: 1; transform: translate(-50%, -100%) scale(1); } }
@@ -268,30 +247,36 @@ a:not(.social-link):not(.contact-social-link):hover > img, a:not(.social-link):n
268
247
  .testimonial-card .portfolio-content-organization { margin-bottom: 0; }
269
248
  [hidden] { display: none !important; }
270
249
  .portfolio-content-card a { display: inline-block; margin: 2px 10px 2px 0; color: var(--lime); font-size: var(--text-sm); }
271
- .portfolio-history-panel { padding: 0; }
272
- .portfolio-history-panel > header { margin: 0; padding: 16px; border-bottom: 1px dashed var(--border); }
250
+ .portfolio-history-panel { overflow: hidden; margin: 4px 0; padding: 0; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); }
251
+ .portfolio-history-panel > header { margin: 0; padding: 16px; border-bottom: 1px solid var(--border); background: color-mix(in srgb, var(--surface) 70%, var(--graphite)); }
273
252
  .portfolio-history-panel > header h2 { margin: 0; }
274
- .portfolio-history-list { display: grid; }
275
- .portfolio-history-row { display: grid; grid-template-columns: auto minmax(0, 1fr) minmax(100px, 22%); align-items: center; gap: 16px; min-width: 0; padding: 18px 16px; border-bottom: 1px dashed var(--frame-dot); transition: border-color var(--interaction-duration) var(--interaction-ease); }
253
+ .portfolio-history-list { display: grid; gap: 7px; padding: 12px; }
254
+ .portfolio-history-row { position: relative; display: grid; grid-template-columns: auto minmax(0, 1fr) minmax(100px, 22%); align-items: center; gap: 16px; min-width: 0; padding: 16px; border: 1px dashed var(--frame-dot); border-radius: 4px; background: transparent; transition: border-color var(--interaction-duration) var(--interaction-ease); }
276
255
  .portfolio-history-row.is-education-row { grid-template-columns: minmax(0, 1fr) minmax(100px, 22%); }
277
- .portfolio-history-row:last-child { border-bottom: 0; }
278
256
  .portfolio-history-row:hover, .portfolio-history-row:focus-within { border-color: color-mix(in srgb, var(--lime) 58%, var(--frame-dot)); }
279
- .portfolio-history-logo { width: 56px; height: 56px; margin: 0; border: 1px solid var(--border); border-radius: 12px; background: var(--surface); object-fit: contain; }
280
- .portfolio-history-logo-placeholder { display: grid; place-items: center; color: var(--text-muted); font: 500 10px/1 var(--body-font); }
257
+ .portfolio-history-logo { width: 56px; height: 56px; margin: 0; border: 1px solid var(--border); border-radius: 8px; background: var(--card); object-fit: contain; }
281
258
  .portfolio-history-main { min-width: 0; }
259
+ .portfolio-history-main h3, .portfolio-content-organization { overflow-wrap: anywhere; }
282
260
  .portfolio-history-main h3 { margin: 0 0 4px; color: var(--warm-white); font: 600 var(--text-md)/1.35 var(--heading-font); }
283
261
  .portfolio-history-row .portfolio-content-organization, .portfolio-history-date, .portfolio-history-description { margin: 0; color: var(--text-muted); font-size: var(--text-sm); line-height: 1.5; }
284
- .portfolio-history-date { justify-self: end; text-align: right; }
262
+ .portfolio-history-meta { display: grid; min-width: 0; max-width: 100%; justify-items: end; align-content: center; gap: 6px; }
263
+ .portfolio-history-date { justify-self: end; width: fit-content; min-width: 0; max-width: 100%; padding: 4px 8px; border: 1px solid var(--border); border-radius: 3px; background: var(--card); color: var(--muted); text-align: right; white-space: normal; overflow-wrap: anywhere; }
264
+ .portfolio-history-location { display: inline-flex; align-items: center; gap: 4px; margin: 0; color: var(--text-muted); font-size: 10px; line-height: 1.3; text-align: right; }
265
+ .portfolio-history-location { min-width: 0; max-width: 100%; flex-wrap: wrap; overflow-wrap: anywhere; }
266
+ .portfolio-history-location svg { flex: 0 0 auto; }
267
+ .portfolio-organization-link { display: inline-flex; align-items: center; gap: 4px; color: inherit; text-decoration: underline dotted var(--frame-dot); text-underline-offset: 3px; transition: color var(--interaction-duration) var(--interaction-ease), text-decoration-color var(--interaction-duration) var(--interaction-ease); }
268
+ .portfolio-organization-link:hover, .portfolio-organization-link:focus-visible { color: var(--lime); text-decoration-color: var(--lime); }
285
269
  .portfolio-history-link { display: inline-flex; margin-left: 8px; color: var(--text-muted); font-size: 1.15em; text-decoration: none; transition: color .2s ease, transform .2s ease; }
286
270
  .portfolio-history-link:hover, .portfolio-history-link:focus-visible { color: var(--lime); transform: translate(2px, -2px); }
287
271
  .portfolio-history-description { margin-top: 5px; }
288
272
  @media (max-width: 600px) {
289
273
  .portfolio-history-panel > header { padding: 14px 12px; }
290
- .portfolio-history-row { grid-template-columns: auto minmax(0, 1fr); gap: 12px; padding: 15px 12px; }
274
+ .portfolio-history-row { grid-template-columns: auto minmax(0, 1fr); gap: 12px; padding: 14px 12px; }
291
275
  .portfolio-history-row.is-education-row { grid-template-columns: minmax(0, 1fr); }
292
276
  .portfolio-history-logo { width: 46px; height: 46px; border-radius: 10px; }
293
- .portfolio-history-date { grid-column: 2; justify-self: start; text-align: left; }
294
- .portfolio-history-row.is-education-row .portfolio-history-date { grid-column: 1; }
277
+ .portfolio-history-meta { grid-column: 2; justify-items: start; }
278
+ .portfolio-history-date, .portfolio-history-location { justify-self: start; text-align: left; }
279
+ .portfolio-history-row.is-education-row .portfolio-history-meta { grid-column: 1; }
295
280
  }
296
281
  .metallic-shimmer-text { color: #D6B77A; }
297
282
  @supports ((background-clip: text) or (-webkit-background-clip: text)) {
@@ -314,15 +299,13 @@ a:not(.social-link):not(.contact-social-link):hover > img, a:not(.social-link):n
314
299
  .social-links { display: flex; max-width: 100%; flex-wrap: wrap; justify-content: flex-start; gap: 7px; margin-top: 14px; }
315
300
  .social-link { display: inline-flex; width: auto; min-width: 0; height: 35px; flex: 0 0 auto; align-items: center; justify-content: flex-start; gap: 7px; padding: 6px 9px; border: 1px dotted var(--frame-dot); border-radius: 5px; color: var(--muted); background: var(--surface); font: 10px/1.2 var(--body-font); transition: transform var(--interaction-duration) var(--interaction-ease), color var(--interaction-duration) var(--interaction-ease), border-color var(--interaction-duration) var(--interaction-ease), background var(--interaction-duration) var(--interaction-ease), opacity var(--interaction-duration) var(--interaction-ease); }
316
301
  .social-link:hover, .social-link:focus-visible,
317
- .contact-social-link:hover, .contact-social-link:focus-visible { color: var(--lime); border-color: color-mix(in srgb, var(--lime) 58%, var(--frame-dot)); }
318
- .social-link:hover .brand-icon, .social-link:focus-visible .brand-icon,
319
- .contact-social-link:hover > img, .contact-social-link:focus-visible > img { filter: brightness(0) saturate(100%) invert(86%) sepia(17%) saturate(560%) hue-rotate(356deg) brightness(96%) contrast(90%); }
320
- .social-link:hover .social-icon, .social-link:focus-visible .social-icon, .contact-social-link:hover > svg, .contact-social-link:focus-visible > svg, .contact-social-link:hover > img, .contact-social-link:focus-visible > img { transform: translateY(-2px); opacity: 1; }
321
- .social-link:active .social-icon, .contact-social-link:active > svg, .contact-social-link:active > img { transform: translateY(0); opacity: .72; }
302
+ .contact-social-link:hover, .contact-social-link:focus-visible { color: var(--social-hover); border-color: var(--frame-dot); }
303
+ .social-link:hover .social-icon, .social-link:focus-visible .social-icon, .contact-social-link:hover > svg, .contact-social-link:focus-visible > svg, .contact-social-link:hover > img, .contact-social-link:focus-visible > img { opacity: 1; }
304
+ .social-link:hover .social-icon, .social-link:focus-visible .social-icon, .contact-social-link:hover > img, .contact-social-link:focus-visible > img { filter: brightness(0) saturate(100%) invert(72%) sepia(99%) saturate(1037%) hue-rotate(82deg) brightness(102%) contrast(104%); }
305
+ .contact-social-link:hover > svg, .contact-social-link:focus-visible > svg { color: var(--social-hover); }
322
306
  .social-link.is-unavailable { cursor: default; }
323
- .social-icon { display: block; width: 16px; height: 16px; object-fit: contain; opacity: .82; transition: opacity var(--interaction-duration) var(--interaction-ease), transform var(--interaction-duration) var(--interaction-ease); }
324
- .brand-icon-fallback { display: inline-grid; flex: 0 0 auto; place-items: center; overflow: hidden; border: 1px solid color-mix(in srgb, var(--lime) 38%, var(--frame-dot)); border-radius: 3px; color: var(--muted); background: color-mix(in srgb, var(--surface) 82%, transparent); font: 700 6px/1 var(--mono-font); letter-spacing: -.04em; }
325
- .social-link:hover .brand-icon-fallback, .social-link:focus-visible .brand-icon-fallback, .contact-social-link:hover .brand-icon-fallback, .contact-social-link:focus-visible .brand-icon-fallback { border-color: var(--lime); color: var(--lime); }
307
+ .social-icon { display: block; width: 16px; height: 16px; object-fit: contain; opacity: .82; transition: opacity var(--interaction-duration) var(--interaction-ease), transform var(--interaction-duration) var(--interaction-ease), filter var(--interaction-duration) var(--interaction-ease); }
308
+ .brand-icon-fallback { display: inline-grid; flex: 0 0 auto; place-items: center; overflow: hidden; border: 1px solid var(--frame-dot); border-radius: 3px; color: var(--muted); background: color-mix(in srgb, var(--surface) 82%, transparent); font: 700 6px/1 var(--mono-font); letter-spacing: -.04em; }
326
309
  .resume-icon { color: var(--muted); }
327
310
 
328
311
  .section-rule { display: flex; align-items: center; gap: 12px; color: var(--muted); font-size: 8px; letter-spacing: .09em; }
@@ -351,7 +334,7 @@ a:not(.social-link):not(.contact-social-link):hover > img, a:not(.social-link):n
351
334
  .project-actions .project-action-placeholder { max-width: 190px; color: var(--text-muted); font-family: var(--body-font); cursor: pointer; }
352
335
  .project-actions a:hover, .project-actions a:focus-visible, .project-actions .project-action-placeholder:hover { color: var(--lime); }
353
336
  .project-actions a > svg, .project-actions a > img, .project-actions .project-action-placeholder > svg { display: block; width: 14px; height: 14px; flex: 0 0 14px; object-fit: contain; transition: transform var(--interaction-duration) var(--interaction-ease), filter var(--interaction-duration) var(--interaction-ease), color var(--interaction-duration) var(--interaction-ease); }
354
- .project-actions a:focus-visible, .project-actions .project-action-placeholder:focus-visible { outline: 1px solid var(--lime); outline-offset: 1px; }
337
+ .project-actions a:focus-visible, .project-actions .project-action-placeholder:focus-visible { outline: 1px solid var(--frame-dot-hover); outline-offset: 1px; }
355
338
 
356
339
  .skills, .contact-section { position: relative; padding: var(--section-space) 0 var(--section-space-end); }
357
340
  .contact-section::before { position: absolute; top: 0; right: 0; left: 0; height: 1px; background: repeating-linear-gradient(90deg, var(--stitch) 0 4px, transparent 4px 8px); content: ''; }
@@ -363,7 +346,7 @@ a:not(.social-link):not(.contact-social-link):hover > img, a:not(.social-link):n
363
346
  .skill-tile { position: relative; display: grid; width: 42px; height: 42px; flex: 0 0 42px; place-items: center; padding: 0; border: 1px solid transparent; border-radius: 4px; color: var(--warm-white); background: transparent; }
364
347
  .skill-tile:hover { z-index: 2; }
365
348
  .skill-logo { width: 32px; height: 32px; object-fit: contain; }
366
- .skill-logo-fallback { display: grid; width: 32px; height: 32px; place-items: center; border: 1px solid color-mix(in srgb, var(--lime) 38%, var(--frame-dot)); border-radius: 4px; color: var(--lime); font: 700 8px/1 var(--mono-font); letter-spacing: -.02em; }
349
+ .skill-logo-fallback { display: grid; width: 32px; height: 32px; place-items: center; border: 1px solid var(--frame-dot); border-radius: 4px; color: var(--muted); font: 700 8px/1 var(--mono-font); letter-spacing: -.02em; }
367
350
  .skill-logo-fallback[hidden] { display: none; }
368
351
  .skill-name { position: absolute; top: calc(100% + 7px); left: 50%; z-index: 8; min-width: max-content; opacity: 0; pointer-events: none; transform: translate(-50%, -3px) scale(.97); transition: opacity var(--interaction-duration) var(--interaction-ease), transform var(--interaction-duration) var(--interaction-ease); }
369
352
  .skill-tile:hover .skill-name { opacity: 1; transform: translate(-50%, 0) scale(1); }
@@ -392,10 +375,11 @@ a:not(.social-link):not(.contact-social-link):hover > img, a:not(.social-link):n
392
375
  .contact-social-panel { min-width: 0; padding: 10px 1px 0 14px; border-left: 1px dotted var(--frame-dot); }
393
376
  .contact-social-links { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 5px; margin-top: 9px; }
394
377
  .contact-social-link { display: flex; min-width: 0; min-height: 34px; align-items: center; justify-content: center; gap: 7px; padding: 6px 8px; border: 1px dotted var(--frame-dot); border-radius: 3px; color: var(--muted); background: transparent; font: 10px/1.25 var(--body-font); transition: color var(--interaction-duration) var(--interaction-ease), border-color var(--interaction-duration) var(--interaction-ease); }
395
- .contact-social-link:hover, .contact-social-link:focus-visible { color: var(--lime); }
378
+ .contact-social-link:hover, .contact-social-link:focus-visible { color: var(--social-hover); }
379
+ .social-link:focus-visible, .contact-social-link:focus-visible, .skill-tile:focus-visible { outline-color: var(--frame-dot-hover); }
396
380
  .contact-social-link > svg, .contact-social-link > img, .contact-brand-icon { display: block; width: 16px; height: 16px; flex: 0 0 16px; object-fit: contain; }
397
381
  .contact-social-link > svg { color: var(--muted); }
398
- .contact-social-link > svg, .contact-social-link > img { opacity: .82; transition: transform var(--interaction-duration) var(--interaction-ease), opacity var(--interaction-duration) var(--interaction-ease); }
382
+ .contact-social-link > svg, .contact-social-link > img { opacity: .82; transition: transform var(--interaction-duration) var(--interaction-ease), opacity var(--interaction-duration) var(--interaction-ease), filter var(--interaction-duration) var(--interaction-ease), color var(--interaction-duration) var(--interaction-ease); }
399
383
  .contact-brand-icon { fill: currentColor; }
400
384
  .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; clip-path: inset(50%); }
401
385
  .site-footer { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 17px var(--page-gutter) 20px; border-top: 1px dotted var(--frame-dot); color: var(--muted); text-align: center; }
@@ -403,9 +387,7 @@ a:not(.social-link):not(.contact-social-link):hover > img, a:not(.social-link):n
403
387
  .footer-copyright, .footer-credit, .footer-quote { margin: 0; font-family: var(--body-font); }
404
388
  .footer-copyright { color: var(--text-muted); font-size: 10px; line-height: 1.45; }
405
389
  .footer-credit { color: var(--muted); font-size: 10px; line-height: 1.45; }
406
- .footer-heart { display: inline-block; width: 14px; height: 14px; margin: 0 2px; object-fit: contain; vertical-align: -3px; animation: footer-heart-pulse 1.8s ease-in-out infinite; }
407
390
  .footer-quote { width: min(100%, var(--content-width)); margin-top: 5px; padding-top: 9px; border-top: 1px dotted color-mix(in srgb, var(--frame-dot) 72%, transparent); color: var(--text-muted); font-size: 10px; font-style: italic; line-height: 1.45; letter-spacing: .01em; }
408
- @keyframes footer-heart-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.16); } }
409
391
 
410
392
  @keyframes wave-hand {
411
393
  0%, 62%, 100% { transform: rotate(0); }
@@ -421,35 +403,21 @@ a:not(.social-link):not(.contact-social-link):hover > img, a:not(.social-link):n
421
403
  from { text-shadow: 0 0 3px rgba(229, 207, 156, .2); }
422
404
  to { text-shadow: 0 0 5px rgba(229, 207, 156, .32); }
423
405
  }
424
- @keyframes heart-beat {
425
- 0%, 10%, 25%, 42%, 100% { transform: scale(1); filter: drop-shadow(0 0 0 rgba(229, 207, 156, 0)); }
426
- 14% { transform: scale(1.14); filter: drop-shadow(0 0 3px rgba(229, 207, 156, .48)); }
427
- 19% { transform: scale(1.03); }
428
- 31% { transform: scale(1.2); filter: drop-shadow(0 0 5px rgba(229, 207, 156, .62)); }
429
- 37% { transform: scale(1.05); }
430
- }
431
406
  @keyframes role-metallic-shimmer {
432
407
  from { background-position: 0 0, 100% 50%; }
433
408
  to { background-position: 0 0, 0% 50%; }
434
409
  }
435
410
 
436
411
  @media (prefers-reduced-motion: reduce) {
437
- .name-heart-icon, .footer-heart, .profile-intro .profile-role, .metallic-shimmer-text { animation: none; }
412
+ .profile-intro .profile-role, .metallic-shimmer-text { animation: none; }
438
413
  }
439
414
 
440
415
  @media (max-width: 760px) {
441
416
  :root { --page-gutter: 14px; }
442
- body::before { opacity: .2; }
443
- body::after { display: none; animation: none; will-change: auto; }
444
- .heart-message { top: auto; bottom: calc(100% + 5px); left: 50%; transform: translate(-50%, 3px); }
445
- @media (hover: hover) and (pointer: fine) {
446
- .name-heart-wrap:hover .heart-message { transform: translate(-50%, 0); }
447
- }
448
417
  .main-nav { gap: 12px; }
449
418
  .project-grid { gap: 8px; }
450
419
  }
451
420
  @media (max-width: 600px) {
452
- body::before { background-image: url('/stars-portrait.svg'); opacity: .16; }
453
421
  :root { --page-gutter: 12px; --section-space: 16px; --section-space-end: 18px; --panel-inset: 10px; }
454
422
  .site-header { min-height: 44px; }
455
423
  .wordmark { font-size: 13px; }
@@ -466,10 +434,10 @@ a:not(.social-link):not(.contact-social-link):hover > img, a:not(.social-link):n
466
434
  .hero h1 { margin-top: 7px; font-size: 22px; }
467
435
  .profile-intro { font-size: 13px; }
468
436
  .profile-intro .profile-role { font-size: 14px; }
469
- .profile-metadata { flex: 1 1 100%; flex-direction: row; align-items: center; justify-content: flex-start; gap: 12px; font-size: 10px; }
437
+ .profile-metadata { min-width: 0; flex: 1 1 100%; flex-direction: row; flex-wrap: wrap; align-items: center; justify-content: flex-start; gap: 12px; font-size: 10px; }
470
438
  .profile-bio { max-width: 100%; font-size: 12px; line-height: 1.55; }
471
439
  .social-links { gap: 6px; margin-top: 12px; }
472
- .social-link { min-height: 36px; padding: 7px 10px; }
440
+ .social-link { min-height: 44px; padding: 7px 10px; }
473
441
  .projects { padding: var(--section-space) 0 var(--section-space-end) 12px; }
474
442
  .projects-intro { gap: 4px; padding: 5px 0 9px; }
475
443
  .projects-intro h2 { font-size: 22px; }
@@ -479,7 +447,7 @@ a:not(.social-link):not(.contact-social-link):hover > img, a:not(.social-link):n
479
447
  .project-info { gap: 7px; }
480
448
  .project-info > h3 { font-size: 14px; }
481
449
  .project-info > p { font-size: 10px; }
482
- .project-actions a { min-height: 32px; padding: 5px 10px; font-size: 11px; }
450
+ .project-actions a { min-height: 44px; padding: 5px 10px; font-size: 11px; }
483
451
  .skills-panel { padding: 9px 10px 11px; }
484
452
  .skills-panel > h2 { margin-bottom: 7px; padding-bottom: 6px; font-size: 18px; }
485
453
  .skills-grid { gap: 4px; }
@@ -490,12 +458,9 @@ a:not(.social-link):not(.contact-social-link):hover > img, a:not(.social-link):n
490
458
  .contact-content { grid-template-columns: minmax(0, 1fr); }
491
459
  .contact-form { padding: 9px 0 10px; }
492
460
  .contact-social-panel { padding: 9px 0 0; border-top: 1px dotted var(--frame-dot); border-left: 0; }
493
- .contact-social-link { min-height: 38px; }
461
+ .contact-submit, .contact-social-link { min-height: 44px; }
494
462
  .site-footer { padding: 16px var(--page-gutter) 18px; }
495
463
  }
496
- @media (max-width: 600px) and (hover: none) {
497
- .heart-message { display: none; }
498
- }
499
464
  @media (max-width: 380px) {
500
465
  :root { --page-gutter: 10px; }
501
466
  .profile-metadata { gap: 4px; font-size: 9px; }
@@ -513,7 +478,7 @@ main > section.section-wrap::before,
513
478
  main > section.section-wrap::after { display: none; }
514
479
  .dotted-frame::before { display: none; }
515
480
  .skills-panel, .contact-panel { border: 0; border-radius: 0; }
516
- .project-card, .portfolio-history-row { border: 1px dashed var(--frame-dot); border-radius: 0; }
481
+ .project-card { border: 1px dashed var(--frame-dot); border-radius: 0; }
517
482
  .project-card {
518
483
  transition: border-color var(--interaction-duration) var(--interaction-ease);
519
484
  }
@@ -32,8 +32,8 @@ test('portfolio content, optional sections, section ordering and custom sections
32
32
  assert.ok(aboutSection)
33
33
  assert.equal([...aboutSection.matchAll(/<p>/g)].length, 3, 'bio content renders as three paragraphs')
34
34
  await access(resolve(templateRoot, 'public', config.profile.profileImage.slice(1)))
35
- assert.ok(config.projects.every(({ image }) => image === '/project-placeholder.svg'))
36
- await access(resolve(templateRoot, 'public', 'project-placeholder.svg'))
35
+ assert.ok(config.projects.every(({ image }) => image === '/project-placeholder.png'))
36
+ await access(resolve(templateRoot, 'public', 'project-placeholder.png'))
37
37
  const styles = await readFile(resolve(templateRoot, 'src/styles.css'), 'utf8')
38
38
  assert.match(styles, /\.profile-photo\s*\{[^}]*border-radius:\s*50%/)
39
39
  assert.deepEqual(config.skills.map(({ name }) => name), [
@@ -0,0 +1,73 @@
1
+ export interface Skill {
2
+ name: string
3
+ icon: string
4
+ category?: string
5
+ }
6
+
7
+ export interface Project {
8
+ name: string
9
+ category?: string
10
+ description: string
11
+ image?: string
12
+ imageAlt?: string
13
+ technologies: Skill[]
14
+ liveUrl?: string
15
+ githubUrl?: string
16
+ featured?: boolean
17
+ }
18
+
19
+ export interface ContentItem {
20
+ title: string
21
+ organization?: string
22
+ organizationUrl?: string
23
+ date?: string
24
+ location?: string
25
+ description?: string
26
+ image?: string
27
+ imageAlt?: string
28
+ imagePlaceholder?: string
29
+ links?: { label: string; url: string }[]
30
+ }
31
+
32
+ export interface Testimonial {
33
+ name: string
34
+ role: string
35
+ company?: string
36
+ message: string
37
+ image?: string
38
+ }
39
+
40
+ export interface SocialIcon {
41
+ label: string
42
+ icon: string
43
+ url?: string
44
+ color?: string
45
+ }
46
+
47
+ export interface PortfolioConfig {
48
+ profile: {
49
+ name: string
50
+ headline: string
51
+ introduction: string
52
+ about: string
53
+ location?: string
54
+ timeZone?: string
55
+ timeZoneLabel?: string
56
+ profileImage?: string
57
+ resumeUrl?: string
58
+ quote?: string
59
+ }
60
+ socialIcons: SocialIcon[]
61
+ experience: ContentItem[]
62
+ education: ContentItem[]
63
+ skills: Skill[]
64
+ projects: Project[]
65
+ testimonials: Testimonial[]
66
+ contact: { email?: string }
67
+ seo: {
68
+ title: string
69
+ description: string
70
+ siteUrl?: string
71
+ image?: string
72
+ }
73
+ }
@@ -1,20 +0,0 @@
1
- node_modules/
2
- dist/
3
- .vite/
4
- .netlify/
5
- .vercel/
6
- coverage/
7
- *.tsbuildinfo
8
- *.log
9
- npm-debug.log*
10
- .DS_Store
11
- Thumbs.db
12
- .env
13
- .env.*
14
- secrets/
15
- *.pem
16
- *.key
17
- *.p12
18
- *.pfx
19
- *.credentials
20
- *.secret
@@ -1,10 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" role="img" aria-labelledby="title description">
2
- <title id="title">Avatar placeholder</title>
3
- <desc id="description">A neutral circular placeholder for a 400 by 400 profile image.</desc>
4
- <circle cx="200" cy="200" r="192" fill="#29292c"/>
5
- <circle cx="200" cy="200" r="176" fill="#e7e7e9" stroke="#45454a" stroke-width="12"/>
6
- <g fill="#77777d" font-family="Arial, sans-serif" text-anchor="middle">
7
- <text x="200" y="188" font-size="30" font-weight="700">Avatar</text>
8
- <text x="200" y="224" font-size="25" font-weight="600">400 x 400</text>
9
- </g>
10
- </svg>
@@ -1,10 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 675" role="img" aria-labelledby="title description">
2
- <title id="title">Project image placeholder</title>
3
- <desc id="description">A neutral 1200 by 675 project thumbnail placeholder.</desc>
4
- <rect width="1200" height="675" fill="#050505"/>
5
- <rect x="24" y="24" width="1152" height="627" rx="34" fill="#e7e7e9"/>
6
- <g fill="#77777d" font-family="Arial, sans-serif" text-anchor="middle">
7
- <text x="600" y="310" font-size="58" font-weight="700">Project image</text>
8
- <text x="600" y="376" font-size="52" font-weight="600">1200 x 675</text>
9
- </g>
10
- </svg>