The Svelte 5 SSG migration—we brought sexy back... or to it? Or something...
This commit is contained in:
640
src/app.css
Normal file
640
src/app.css
Normal file
@@ -0,0 +1,640 @@
|
||||
/* ========================================
|
||||
CSS Variables for Light/Dark Mode
|
||||
======================================== */
|
||||
|
||||
:root {
|
||||
/* Light mode colors */
|
||||
--color-bg: #ffffff;
|
||||
--color-bg-alt: #faf9ff; /* subtle violet-tinted off-white */
|
||||
--color-bg-subtle: #f3f1ff; /* soft surface */
|
||||
|
||||
--color-text: #14121a;
|
||||
--color-text-secondary: #3f3a4a;
|
||||
--color-text-tertiary: #625b70;
|
||||
|
||||
--color-border: #e4e0f2;
|
||||
--color-border-strong: #c9c1e3;
|
||||
|
||||
/* Brand accent (links, focus, highlights) */
|
||||
--color-primary: #6d28d9; /* purple */
|
||||
--color-primary-hover: #5b21b6;
|
||||
--color-primary-bg: #efe7ff;
|
||||
|
||||
--color-secondary: #3f3a4a;
|
||||
--color-secondary-hover: #14121a;
|
||||
|
||||
/* Focus */
|
||||
--color-focus: #6d28d9;
|
||||
--color-focus-outline: rgba(109, 40, 217, 0.45);
|
||||
|
||||
/* Typography */
|
||||
--font-family: 'Inter', system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
||||
--font-family-heading: 'Fraunces', ui-serif, Georgia, 'Times New Roman', serif;
|
||||
|
||||
--font-size-base: 18px;
|
||||
--font-size-small: 15px;
|
||||
--font-size-medium: 16px;
|
||||
--font-size-large: 20px;
|
||||
--font-size-xl: 32px;
|
||||
--font-size-xxl: 52px;
|
||||
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
--line-height-base: 1.75;
|
||||
--line-height-relaxed: 1.85;
|
||||
--line-height-tight: 1.65;
|
||||
--line-height-heading: 1.25;
|
||||
|
||||
/* Spacing */
|
||||
--space-xs: 0.25rem;
|
||||
--space-sm: 0.5rem;
|
||||
--space-md: 1rem;
|
||||
--space-lg: 1.5rem;
|
||||
--space-xl: 2rem;
|
||||
--space-xxl: 3rem;
|
||||
--space-xxxl: 7rem;
|
||||
|
||||
/* Layout */
|
||||
--max-width: 1100px;
|
||||
--max-narrow-width: 680px;
|
||||
--max-text-width: 70ch;
|
||||
|
||||
/* Border radius */
|
||||
--border-radius: 6px;
|
||||
--border-radius-small: 6px;
|
||||
--border-radius-medium: 10px;
|
||||
--border-radius-large: 16px;
|
||||
|
||||
/* Transition */
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-base: 250ms ease;
|
||||
|
||||
/* CTA palette (orange primary CTA; AAA in light mode with white text) */
|
||||
--accent-orange: #9a3412;
|
||||
--accent-orange-hover: #7c2d12;
|
||||
--accent-orange-soft: #fff1e7;
|
||||
|
||||
/* Button tokens */
|
||||
--btn-primary-bg: var(--accent-orange);
|
||||
--btn-primary-bg-hover: var(--accent-orange-hover);
|
||||
--btn-primary-fg: #ffffff;
|
||||
|
||||
--btn-secondary-bg: transparent;
|
||||
--btn-secondary-fg: var(--color-text);
|
||||
--btn-secondary-border: var(--color-border-strong);
|
||||
|
||||
--btn-ghost-bg-hover: rgba(0, 0, 0, 0.06);
|
||||
|
||||
/* Focus ring (purple feels more “intentional” than orange) */
|
||||
--btn-focus-ring: rgba(109, 40, 217, 0.45);
|
||||
}
|
||||
|
||||
/* Dark mode - AAA contrast optimized */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-bg: #0b0b12; /* cool slate (not pure black) */
|
||||
--color-bg-alt: #121226;
|
||||
--color-bg-subtle: #191934;
|
||||
|
||||
--color-text: #f3f2ff;
|
||||
--color-text-secondary: #c9c6e4;
|
||||
--color-text-tertiary: #a7a2c8;
|
||||
|
||||
--color-border: #2a2950;
|
||||
--color-border-strong: #3a3870;
|
||||
|
||||
/* Brand accent (purple) */
|
||||
--color-primary: #a78bfa;
|
||||
--color-primary-hover: #c4b5fd;
|
||||
--color-primary-bg: #1a1530;
|
||||
|
||||
--color-secondary: #c9c6e4;
|
||||
--color-secondary-hover: #f3f2ff;
|
||||
|
||||
--color-focus: #a78bfa;
|
||||
--color-focus-outline: rgba(167, 139, 250, 0.45);
|
||||
|
||||
/* CTA button: keep AAA in dark mode by using dark text on bright orange */
|
||||
--btn-primary-bg: #fb923c;
|
||||
--btn-primary-bg-hover: #fdba74; /* still AAA with dark text */
|
||||
--btn-primary-fg: #0b0b12;
|
||||
|
||||
--btn-secondary-fg: var(--color-text);
|
||||
--btn-secondary-border: var(--color-border-strong);
|
||||
|
||||
--btn-ghost-bg-hover: rgba(255, 255, 255, 0.08);
|
||||
--btn-focus-ring: rgba(167, 139, 250, 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Base Styles
|
||||
======================================== */
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: var(--font-size-base);
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: var(--font-family);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: var(--line-height-base);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Skip Link (Accessibility)
|
||||
======================================== */
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
clip-path: inset(100%);
|
||||
clip: rect(0 0 0 0);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -100px;
|
||||
left: 0;
|
||||
padding: var(--space-sm) var(--space-lg);
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: var(--font-size-base);
|
||||
z-index: 9999;
|
||||
border-radius: 0 0 var(--border-radius-large) 0;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
border-bottom: none;
|
||||
|
||||
&:focus {
|
||||
top: 0;
|
||||
outline: 4px solid white;
|
||||
outline-offset: 3px;
|
||||
box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
top: 0;
|
||||
outline: 4px solid white;
|
||||
outline-offset: 3px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Focus Styles (Strong, Accessible)
|
||||
======================================== */
|
||||
|
||||
/* Strong focus indicators for keyboard navigation (WCAG 2.2 AAA) */
|
||||
:focus {
|
||||
outline: 3px solid var(--color-focus);
|
||||
outline-offset: 3px;
|
||||
transition: outline-offset var(--transition-fast);
|
||||
|
||||
&:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 4px solid var(--color-focus);
|
||||
outline-offset: 4px;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 0 8px var(--color-focus-outline);
|
||||
|
||||
*& {
|
||||
outline-style: solid !important;
|
||||
}
|
||||
|
||||
img& {
|
||||
outline: 4px solid var(--color-focus);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Typography
|
||||
======================================== */
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0 0 var(--space-lg) 0;
|
||||
line-height: var(--line-height-heading);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--font-size-xxl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
/* Heading font (keeps layout intact; just typography) */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
.section-title {
|
||||
font-family: var(--font-family-heading);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 var(--space-md) 0;
|
||||
max-width: var(--max-text-width);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
text-decoration-skip-ink: auto;
|
||||
transition: color var(--transition-fast);
|
||||
border-bottom: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary-hover);
|
||||
border-bottom-color: currentColor;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 3px solid var(--color-focus);
|
||||
outline-offset: 3px;
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Layout Containers
|
||||
======================================== */
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--space-md);
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: var(--space-xxxl) 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:nth-child(even) {
|
||||
background-color: var(--color-bg-alt);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Buttons
|
||||
======================================== */
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 1rem 2rem;
|
||||
min-height: 48px;
|
||||
min-width: 120px;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
text-decoration: none;
|
||||
border-radius: var(--border-radius-large);
|
||||
transition: all var(--transition-base);
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
letter-spacing: -0.01em;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* PRIMARY CTA
|
||||
Use the CTA/button tokens (defined in BOTH modes) to guarantee contrast.
|
||||
This fixes the dark-mode purple/white contrast violation without changing your purple brand accents. */
|
||||
.btn-primary {
|
||||
background-color: var(--btn-primary-bg);
|
||||
color: var(--btn-primary-fg);
|
||||
border-color: var(--btn-primary-bg);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--btn-primary-bg-hover);
|
||||
border-color: var(--btn-primary-bg-hover);
|
||||
color: var(--btn-primary-fg);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 4px solid var(--color-focus);
|
||||
outline-offset: 4px;
|
||||
box-shadow:
|
||||
0 0 0 8px var(--color-focus-outline),
|
||||
0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
/* SECONDARY CTA
|
||||
Keep it outlined and “lighter touch” (more professional than flipping to a heavy block).
|
||||
Uses existing tokens only; works in both modes. */
|
||||
.btn-secondary {
|
||||
background-color: var(--btn-secondary-bg);
|
||||
color: var(--btn-secondary-fg);
|
||||
border-color: var(--btn-secondary-border);
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--accent-orange-soft);
|
||||
color: var(--btn-secondary-fg);
|
||||
border-color: var(--btn-primary-bg);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-color: rgba(251, 146, 60, 0.12);
|
||||
border-color: var(--btn-primary-bg);
|
||||
color: var(--btn-secondary-fg);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 4px solid var(--color-focus);
|
||||
outline-offset: 4px;
|
||||
box-shadow:
|
||||
0 0 0 8px var(--color-focus-outline),
|
||||
0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Section Titles
|
||||
======================================== */
|
||||
|
||||
.section-title {
|
||||
margin-bottom: var(--space-xl);
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text);
|
||||
letter-spacing: -0.02em;
|
||||
line-height: var(--line-height-heading);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Content Lists
|
||||
======================================== */
|
||||
|
||||
.content-list {
|
||||
max-width: var(--max-text-width);
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
& li {
|
||||
position: relative;
|
||||
padding-left: var(--space-lg);
|
||||
margin-bottom: var(--space-lg);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-relaxed);
|
||||
color: var(--color-text);
|
||||
|
||||
&::before {
|
||||
content: '→';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--color-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: 1.2em;
|
||||
line-height: 1;
|
||||
top: 0.1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Responsive Design
|
||||
======================================== */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
:root {
|
||||
--font-size-base: 17px;
|
||||
--font-size-small: 14px;
|
||||
--font-size-medium: 15px;
|
||||
--font-size-large: 19px;
|
||||
--font-size-xl: 28px;
|
||||
--font-size-xxl: 40px;
|
||||
--space-lg: 2rem;
|
||||
--space-xl: 3rem;
|
||||
--space-xxl: 4.5rem;
|
||||
--space-xxxl: 6rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: var(--space-xxl) 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
min-height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
:root {
|
||||
--font-size-base: 16px;
|
||||
--font-size-small: 13px;
|
||||
--font-size-medium: 14px;
|
||||
--font-size-large: 18px;
|
||||
--font-size-xl: 24px;
|
||||
--font-size-xxl: 34px;
|
||||
--space-xl: 2.5rem;
|
||||
--space-xxl: 3.5rem;
|
||||
--space-xxxl: 5rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 var(--space-md);
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: var(--space-xl) 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.875rem 1.5rem;
|
||||
min-height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
High Contrast Mode Support
|
||||
======================================== */
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
:root {
|
||||
--color-primary: #0047b3;
|
||||
--color-border: #000000;
|
||||
--color-text: #000000;
|
||||
--color-text-secondary: #1a1a1a;
|
||||
|
||||
/* Maintain button contrast in high contrast mode (same tokens, same names) */
|
||||
--btn-primary-bg: #000000;
|
||||
--btn-primary-bg-hover: #000000;
|
||||
--btn-primary-fg: #ffffff;
|
||||
|
||||
--btn-secondary-border: #000000;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-primary: #66b3ff;
|
||||
--color-border: #ffffff;
|
||||
--color-text: #ffffff;
|
||||
--color-text-secondary: #e0e0e0;
|
||||
|
||||
--btn-primary-bg: #ffffff;
|
||||
--btn-primary-bg-hover: #ffffff;
|
||||
--btn-primary-fg: #000000;
|
||||
|
||||
--btn-secondary-border: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-width: 3px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
:focus,
|
||||
:focus-visible {
|
||||
outline-width: 4px;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
a {
|
||||
border-bottom-width: 2px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Print Styles (Accessibility)
|
||||
======================================== */
|
||||
|
||||
@media print {
|
||||
/* Show all content clearly for printing */
|
||||
body {
|
||||
font-size: 12pt;
|
||||
line-height: 1.5;
|
||||
color: #000;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Expand all sections */
|
||||
.section {
|
||||
page-break-inside: avoid;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1pt solid #ccc;
|
||||
}
|
||||
|
||||
/* Show URLs for external links */
|
||||
a[href^='http']:after {
|
||||
content: ' (' attr(href) ')';
|
||||
font-size: 10pt;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Hide interactive elements that don't make sense in print */
|
||||
.btn,
|
||||
.cta-group {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Ensure good contrast */
|
||||
* {
|
||||
color: #000 !important;
|
||||
background: #fff !important;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
dt {
|
||||
color: #000 !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
13
src/app.d.ts
vendored
Normal file
13
src/app.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
11
src/app.html
Normal file
11
src/app.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
64
src/lib/components/EngagementsSection.svelte
Normal file
64
src/lib/components/EngagementsSection.svelte
Normal file
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import { engagements } from '$lib/data/engagements';
|
||||
</script>
|
||||
|
||||
<section id="engagements" class="section" aria-labelledby="engagements-heading">
|
||||
<div class="container">
|
||||
<h2 id="engagements-heading" class="section-title">Recent Engagements</h2>
|
||||
<dl class="engagements-list">
|
||||
{#each engagements as engagement (engagement.title)}
|
||||
<div class="engagement">
|
||||
<dt>{engagement.title}</dt>
|
||||
<dd>{engagement.description}</dd>
|
||||
</div>
|
||||
{/each}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.engagements-list {
|
||||
max-width: var(--max-text-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.engagement {
|
||||
margin-bottom: var(--space-xl);
|
||||
padding-bottom: var(--space-lg);
|
||||
padding-left: var(--space-md);
|
||||
border-left: 3px solid var(--color-border);
|
||||
transition: border-color var(--transition-base);
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
border-left-color: var(--color-primary);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding-left: var(--space-sm);
|
||||
}
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
border-left-width: 4px;
|
||||
}
|
||||
|
||||
& dt {
|
||||
margin-bottom: var(--space-sm);
|
||||
font-size: var(--font-size-large);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text);
|
||||
line-height: var(--line-height-tight);
|
||||
}
|
||||
& dd {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-relaxed);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
276
src/lib/components/ExperienceSection.svelte
Normal file
276
src/lib/components/ExperienceSection.svelte
Normal file
@@ -0,0 +1,276 @@
|
||||
<script lang="ts">
|
||||
import { experienceLogos, experienceTextList } from '$lib/data/experience';
|
||||
</script>
|
||||
|
||||
<section
|
||||
id="experience"
|
||||
class="section experience-section"
|
||||
aria-labelledby="experience-heading"
|
||||
>
|
||||
<div class="container">
|
||||
<h2 id="experience-heading" class="section-title">
|
||||
Experience includes teams at:
|
||||
</h2>
|
||||
|
||||
<div class="logo-strip" role="list" aria-label="Company logos">
|
||||
{#each experienceLogos as logo (logo.alt)}
|
||||
<div class="logo-item" role="listitem">
|
||||
<img
|
||||
src={logo.src}
|
||||
alt={logo.alt}
|
||||
loading="lazy"
|
||||
width={logo.width}
|
||||
height={logo.height}
|
||||
/>
|
||||
<span class="logo-fallback-text">{logo.alt}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<ul class="logo-text-list" aria-hidden="true">
|
||||
{#each experienceTextList as name (name)}
|
||||
<li>{name}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<p class="footnote">Logos are trademarks of their respective owners.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.experience-section {
|
||||
text-align: center;
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
.logo-strip {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-xl);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: var(--space-lg) 0;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) and (min-width: 481px) {
|
||||
gap: var(--space-lg);
|
||||
padding: var(--space-md) 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-item {
|
||||
position: relative;
|
||||
flex: 0 1 auto;
|
||||
min-width: 120px;
|
||||
max-width: 160px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-xs);
|
||||
|
||||
/* Make logo containers keyboard focusable for screen reader users */
|
||||
&:focus-within {
|
||||
outline: 3px solid var(--color-focus);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) and (min-width: 481px) {
|
||||
min-width: 110px;
|
||||
max-width: 140px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
& img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 50px;
|
||||
object-fit: contain;
|
||||
opacity: 0.75;
|
||||
transition: all var(--transition-base);
|
||||
filter: grayscale(100%) contrast(1.15);
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
opacity: 1;
|
||||
filter: grayscale(25%) contrast(1.05);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 4px solid var(--color-focus);
|
||||
outline-offset: 4px;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
&[alt]:not([src]),
|
||||
&[alt][src=''],
|
||||
&[alt]:not([src*='.svg']):not([src*='.png']):not([src*='.jpg']) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) and (min-width: 481px) {
|
||||
max-height: 45px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
&:hover,
|
||||
&:focus {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode logo adaptations */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
filter: grayscale(100%) brightness(0) invert(1) contrast(1.25);
|
||||
opacity: 0.65;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
filter: grayscale(50%) brightness(1) invert(1) contrast(1.1);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
opacity: 1;
|
||||
filter: contrast(1.6);
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
filter: brightness(0) invert(1) contrast(1.9);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
opacity: 1;
|
||||
filter: none;
|
||||
max-height: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Fallback text (shown when image fails to load or on very small screens) */
|
||||
.logo-fallback-text {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.logo-item:has(img[alt]:not([src])) .logo-fallback-text,
|
||||
.logo-item:has(img[alt][src='']) .logo-fallback-text {
|
||||
position: static;
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding: var(--space-sm);
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
clip: auto;
|
||||
white-space: normal;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg-alt);
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Text-only list (hidden by default, shown on very small screens) */
|
||||
.logo-text-list {
|
||||
display: none;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 auto;
|
||||
max-width: 400px;
|
||||
text-align: left;
|
||||
|
||||
& li {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
margin-bottom: var(--space-sm);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg-subtle);
|
||||
border-left: 3px solid var(--color-primary);
|
||||
border-radius: var(--border-radius);
|
||||
line-height: var(--line-height-base);
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
border-left-width: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media print {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
.footnote {
|
||||
margin-top: var(--space-lg);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-normal);
|
||||
color: var(--color-text-tertiary);
|
||||
font-style: italic;
|
||||
line-height: var(--line-height-base);
|
||||
max-width: 100%;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
.logo-item img {
|
||||
opacity: 1;
|
||||
filter: contrast(1.6);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.logo-item img {
|
||||
filter: brightness(0) invert(1) contrast(1.9);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-text-list li {
|
||||
border-left-width: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
82
src/lib/components/Footer.svelte
Normal file
82
src/lib/components/Footer.svelte
Normal file
@@ -0,0 +1,82 @@
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<p class="copyright">
|
||||
© <span id="copyright-year">2026</span> mifi Ventures, LLC · Boston, MA
|
||||
</p>
|
||||
<nav class="footer-links" aria-label="Social media links">
|
||||
<a
|
||||
href="https://linkedin.com/in/the-mifi"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="LinkedIn profile (opens in new tab)">LinkedIn</a
|
||||
>
|
||||
<a
|
||||
href="https://github.com/the-mifi"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="GitHub profile (opens in new tab)">GitHub</a
|
||||
>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.footer {
|
||||
padding: var(--space-xxl) 0 var(--space-xl) 0;
|
||||
text-align: center;
|
||||
background-color: var(--color-bg);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.copyright {
|
||||
margin-bottom: var(--space-md);
|
||||
font-size: var(--font-size-medium);
|
||||
font-weight: var(--font-weight-normal);
|
||||
color: var(--color-text-tertiary);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
gap: var(--space-lg);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
& a {
|
||||
font-size: var(--font-size-medium);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-text-secondary);
|
||||
text-decoration: none;
|
||||
transition: color var(--transition-fast);
|
||||
border-bottom: 1px solid transparent;
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
margin: calc(-1 * var(--space-xs)) calc(-1 * var(--space-sm));
|
||||
min-height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 4px solid var(--color-focus);
|
||||
outline-offset: 4px;
|
||||
border-bottom-color: transparent;
|
||||
box-shadow: 0 0 0 8px var(--color-focus-outline);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@media print {
|
||||
&:after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
82
src/lib/components/Hero.svelte
Normal file
82
src/lib/components/Hero.svelte
Normal file
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import Logo from './Logo.svelte';
|
||||
</script>
|
||||
|
||||
<header id="header" class="hero">
|
||||
<div class="container">
|
||||
<Logo />
|
||||
<p class="headline">Software Engineering Consulting</p>
|
||||
<p class="subhead">
|
||||
Principal: Mike Fitzpatrick — senior full-stack engineer and architect helping
|
||||
teams ship reliable, accessible, high-performance web products.
|
||||
</p>
|
||||
<div class="cta-group">
|
||||
<a
|
||||
href="https://cal.mifi.ventures/the-mifi"
|
||||
class="btn btn-primary"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Schedule a 30-minute intro call (opens in new tab)"
|
||||
>
|
||||
Schedule a 30-minute intro call
|
||||
</a>
|
||||
<a
|
||||
href="/downloads/resume.pdf"
|
||||
class="btn btn-secondary"
|
||||
download
|
||||
aria-label="Download Mike Fitzpatrick's resume as PDF"
|
||||
>
|
||||
Download resume
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
padding: var(--space-xxxl) 0 var(--space-xxl) 0;
|
||||
text-align: center;
|
||||
background-color: var(--color-bg);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: var(--space-xxl) 0 var(--space-xl) 0;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
padding: var(--space-xl) 0 var(--space-lg) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.headline {
|
||||
margin-bottom: var(--space-lg);
|
||||
font-family: var(--font-family-heading);
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.subhead {
|
||||
max-width: var(--max-narrow-width);
|
||||
margin: 0 auto var(--space-xl) auto;
|
||||
font-size: var(--font-size-large);
|
||||
font-weight: var(--font-weight-normal);
|
||||
color: var(--color-text-secondary);
|
||||
line-height: var(--line-height-relaxed);
|
||||
}
|
||||
|
||||
.cta-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-md);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: var(--space-lg);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
14
src/lib/components/HowWeWork.svelte
Normal file
14
src/lib/components/HowWeWork.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { howWeWorkItems } from '$lib/data/content';
|
||||
</script>
|
||||
|
||||
<section id="how-we-work" class="section" aria-labelledby="how-we-work-heading">
|
||||
<div class="container">
|
||||
<h2 id="how-we-work-heading" class="section-title">How We Work</h2>
|
||||
<ul class="content-list">
|
||||
{#each howWeWorkItems as item (item)}
|
||||
<li>{item}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
14
src/lib/components/ImpactSection.svelte
Normal file
14
src/lib/components/ImpactSection.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { impactItems } from '$lib/data/content';
|
||||
</script>
|
||||
|
||||
<section id="impact" class="section" aria-labelledby="impact-heading">
|
||||
<div class="container">
|
||||
<h2 id="impact-heading" class="section-title">Selected Impact</h2>
|
||||
<ul class="content-list">
|
||||
{#each impactItems as item (item)}
|
||||
<li>{item}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
18
src/lib/components/Logo.svelte
Normal file
18
src/lib/components/Logo.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import Wordmark from './Wordmark.svelte';
|
||||
</script>
|
||||
|
||||
<h1 class="logo">
|
||||
<Wordmark />
|
||||
<span class="sr-only">mifi Ventures</span>
|
||||
</h1>
|
||||
|
||||
<style>
|
||||
.logo {
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family-heading);
|
||||
margin: 0 auto;
|
||||
max-width: 350px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
35
src/lib/components/ScheduleSection.svelte
Normal file
35
src/lib/components/ScheduleSection.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<section
|
||||
id="schedule"
|
||||
class="section schedule-section"
|
||||
aria-labelledby="schedule-heading"
|
||||
>
|
||||
<div class="container">
|
||||
<h2 id="schedule-heading" class="section-title">Let's Talk</h2>
|
||||
<p class="schedule-text">Ready to discuss your project?</p>
|
||||
<a
|
||||
href="https://cal.mifi.ventures/the-mifi"
|
||||
class="btn btn-primary"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Schedule a 30-minute intro call (opens in new tab)"
|
||||
>
|
||||
Schedule a 30-minute intro call
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.schedule-section {
|
||||
text-align: center;
|
||||
background-color: var(--color-bg-subtle);
|
||||
}
|
||||
|
||||
.schedule-text {
|
||||
margin-bottom: var(--space-lg);
|
||||
font-size: var(--font-size-large);
|
||||
font-weight: var(--font-weight-normal);
|
||||
color: var(--color-text-secondary);
|
||||
line-height: var(--line-height-relaxed);
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
14
src/lib/components/WhatWeDo.svelte
Normal file
14
src/lib/components/WhatWeDo.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { whatWeDoItems } from '$lib/data/content';
|
||||
</script>
|
||||
|
||||
<section id="what-we-do" class="section" aria-labelledby="what-we-do-heading">
|
||||
<div class="container">
|
||||
<h2 id="what-we-do-heading" class="section-title">What We Do</h2>
|
||||
<ul class="content-list">
|
||||
{#each whatWeDoItems as item (item)}
|
||||
<li>{item}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
61
src/lib/components/Wordmark.svelte
Normal file
61
src/lib/components/Wordmark.svelte
Normal file
@@ -0,0 +1,61 @@
|
||||
<script lang="ts">
|
||||
let { color = 'currentColor' }: { color?: string } = $props();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 3934 513"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xml:space="preserve"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"
|
||||
><g
|
||||
><path
|
||||
fill={color}
|
||||
d="M0,504.667l0,-362.333l82.5,0l0,83.5l-9.5,-13.5c6.444,-26.222 19.75,-45.778 39.917,-58.667c20.167,-12.889 43.806,-19.333 70.917,-19.333c29.556,0 55.694,7.694 78.417,23.083c22.722,15.389 37.417,35.861 44.083,61.417l-25,2.167c11.222,-29.222 27.917,-50.972 50.083,-65.25c22.167,-14.278 47.75,-21.417 76.75,-21.417c25.667,0 48.611,5.778 68.833,17.333c20.222,11.556 36.194,27.611 47.917,48.167c11.722,20.556 17.583,44.333 17.583,71.333l0,233.5l-87.5,0l0,-212.667c0,-16.111 -2.889,-29.889 -8.667,-41.333c-5.778,-11.444 -13.833,-20.361 -24.167,-26.75c-10.333,-6.389 -22.667,-9.583 -37,-9.583c-13.889,0 -26.139,3.194 -36.75,9.583c-10.611,6.389 -18.833,15.333 -24.667,26.833c-5.833,11.5 -8.75,25.25 -8.75,41.25l0,212.667l-87.5,0l0,-212.667c0,-16.111 -2.889,-29.889 -8.667,-41.333c-5.778,-11.444 -13.833,-20.361 -24.167,-26.75c-10.333,-6.389 -22.667,-9.583 -37,-9.583c-14,0 -26.278,3.194 -36.833,9.583c-10.556,6.389 -18.75,15.333 -24.583,26.833c-5.833,11.5 -8.75,25.25 -8.75,41.25l0,212.667l-87.5,0Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/><path
|
||||
fill={color}
|
||||
d="M614.5,504.667l0,-362.333l87.5,0l0,362.333l-87.5,0Zm0,-403.333l0,-93.333l87.5,0l0,93.333l-87.5,0Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/><path
|
||||
fill={color}
|
||||
d="M823.5,504.667l0,-284.833l-63.833,0l0,-77.5l63.833,0l0,-12c0,-27.889 5.639,-51.472 16.917,-70.75c11.278,-19.278 27.194,-34.028 47.75,-44.25c20.556,-10.222 44.778,-15.333 72.667,-15.333c5.444,0 11.389,0.333 17.833,1c6.444,0.667 11.778,1.444 16,2.333l0,75.333c-4.111,-0.889 -8.083,-1.444 -11.917,-1.667c-3.833,-0.222 -7.361,-0.333 -10.583,-0.333c-19.333,0 -34.361,4.361 -45.083,13.083c-10.722,8.722 -16.083,22.25 -16.083,40.583l0,12l158.667,0l0,77.5l-158.667,0l0,284.833l-87.5,0Zm213.667,0l0,-362.333l87.5,0l0,362.333l-87.5,0Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/><path
|
||||
fill={color}
|
||||
d="M1474.74,50.297c0,-3.892 1.365,-6.915 4.096,-9.068c2.731,-2.153 6.86,-3.229 12.388,-3.229l106.333,0c5.83,0 10.083,1.052 12.76,3.156c2.677,2.104 4.016,5.056 4.016,8.854c0,2.844 -0.913,5.219 -2.74,7.125c-1.826,1.906 -5.043,3.74 -9.651,5.5l-13.286,4.479c-6.694,2.681 -12.347,6.961 -16.958,12.841c-4.611,5.88 -8.964,15.216 -13.057,28.008l-114.453,337.807c-2.128,6.608 -3.694,12.082 -4.695,16.424c-1.002,4.342 -1.503,8.846 -1.503,13.513l0,15.141c0,4.351 -1.242,7.741 -3.727,10.172c-2.484,2.431 -5.817,3.646 -9.997,3.646l-71.854,0c-4.354,0 -7.762,-1.215 -10.224,-3.646c-2.462,-2.431 -3.693,-5.965 -3.693,-10.604l0,-14.969c0,-3.542 -0.508,-7.257 -1.523,-11.146c-1.016,-3.889 -2.428,-8.453 -4.237,-13.693l-125.854,-363.062c-2.097,-6.191 -4.4,-10.66 -6.909,-13.406c-2.509,-2.747 -5.987,-4.977 -10.435,-6.693l-14.141,-4.193c-7.497,-2.809 -11.245,-7.128 -11.245,-12.958c0,-3.892 1.394,-6.915 4.182,-9.068c2.788,-2.153 7.002,-3.229 12.641,-3.229l151.687,0c5.75,0 9.968,1.076 12.654,3.229c2.686,2.153 4.029,5.175 4.029,9.068c0,3.128 -1.044,5.646 -3.133,7.552c-2.089,1.906 -5.221,3.55 -9.398,4.932l-25.328,4.427c-5.542,1.573 -8.939,4.237 -10.193,7.992c-1.253,3.755 -0.444,9.841 2.427,18.258l124.146,361.656l-26.328,20.599l125.198,-369.797c3.542,-10.618 4.107,-18.966 1.695,-25.044c-2.411,-6.078 -9.02,-10.76 -19.826,-14.044l-21.573,-4.193c-3.972,-1.382 -7.014,-2.978 -9.125,-4.789c-2.111,-1.811 -3.167,-4.327 -3.167,-7.549Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/><path
|
||||
fill={color}
|
||||
d="M1905.721,311.365c0,9.878 -2.872,17.47 -8.615,22.776c-5.743,5.306 -14.118,7.958 -25.125,7.958l-210,0l0,-21.203l147.052,0c10.229,0 15.344,-4.622 15.344,-13.865c0,-30.597 -5.845,-54.022 -17.536,-70.273c-11.691,-16.252 -27.196,-24.378 -46.516,-24.378c-15.257,0 -28.655,4.508 -40.195,13.523c-11.54,9.016 -20.561,21.96 -27.062,38.833c-6.502,16.873 -9.753,37.037 -9.753,60.492c0,43.872 10.266,76.974 30.799,99.307c20.533,22.333 47.546,33.5 81.039,33.5c21.031,0 39.325,-4.677 54.88,-14.031c15.556,-9.354 26.748,-22.125 33.578,-38.313c2.969,-3.653 5.411,-6.146 7.326,-7.479c1.915,-1.333 3.937,-2 6.065,-2c2.938,0 5.082,1.291 6.432,3.872c1.351,2.582 1.97,5.721 1.859,9.419c-1.174,18.809 -7.769,36.038 -19.786,51.688c-12.017,15.649 -28.122,28.109 -48.315,37.38c-20.193,9.271 -43.218,13.906 -69.076,13.906c-31.069,0 -58.526,-6.501 -82.37,-19.503c-23.844,-13.002 -42.481,-31.285 -55.911,-54.849c-13.431,-23.564 -20.146,-51.166 -20.146,-82.805c0,-32.92 6.497,-62.108 19.49,-87.562c12.993,-25.455 31.546,-45.48 55.659,-60.076c24.113,-14.595 52.768,-21.893 85.966,-21.893c27.938,-0 51.99,5.361 72.159,16.083c20.168,10.722 35.67,25.512 46.505,44.37c10.835,18.858 16.253,40.564 16.253,65.12Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/><path
|
||||
fill={color}
|
||||
d="M2076.711,205.234l0,253.599c0,6.017 0.997,10.479 2.99,13.385c1.993,2.906 4.998,4.97 9.016,6.193l14.234,3.453c6.413,2.337 9.62,6.03 9.62,11.078c0,7.816 -5.082,11.724 -15.245,11.724l-124.286,0c-5.035,0 -8.755,-1.004 -11.161,-3.013c-2.406,-2.009 -3.609,-4.721 -3.609,-8.138c0,-2.733 0.865,-5.056 2.596,-6.969c1.731,-1.913 4.438,-3.458 8.122,-4.635l15.234,-3.5c4.031,-1.222 7.04,-3.262 9.026,-6.12c1.986,-2.858 2.979,-7.281 2.979,-13.271l0,-204.677c0,-4.844 -0.782,-8.342 -2.346,-10.495c-1.564,-2.153 -4.126,-3.467 -7.685,-3.943l-20.542,-1c-3.559,-0.667 -6.109,-1.819 -7.651,-3.456c-1.542,-1.637 -2.312,-3.734 -2.312,-6.289c0,-2.972 0.918,-5.387 2.753,-7.245c1.835,-1.858 5.192,-3.66 10.07,-5.406l62.24,-21.5c6.941,-2.556 12.56,-4.39 16.857,-5.503c4.297,-1.113 8.263,-1.669 11.898,-1.669c5.688,0 9.977,1.576 12.867,4.729c2.891,3.153 4.336,7.375 4.336,12.667Zm-9.385,71.365l-12.724,-13.036l13.526,-11.927c26.417,-23.639 49.119,-40.515 68.107,-50.628c18.988,-10.113 37.15,-15.169 54.487,-15.169c26.26,0 46.657,8.724 61.19,26.172c14.533,17.448 23.444,41.141 26.732,71.078l20.208,174.552c0.729,6.417 2.038,11.217 3.927,14.401c1.889,3.184 4.993,5.387 9.313,6.609l13.427,3.26c3.684,1.16 6.391,2.697 8.122,4.612c1.731,1.915 2.596,4.246 2.596,6.992c0,3.417 -1.175,6.129 -3.526,8.138c-2.351,2.009 -6.115,3.013 -11.292,3.013l-125.594,0c-10.198,0 -15.297,-3.908 -15.297,-11.724c-0,-5.017 3.177,-8.71 9.531,-11.078l14.896,-3.453c4.448,-1.222 7.823,-3.425 10.125,-6.609c2.302,-3.184 3.087,-7.905 2.354,-14.161l-18.995,-162.974c-2.476,-20.635 -7.75,-36.081 -15.823,-46.336c-8.073,-10.255 -19.927,-15.383 -35.562,-15.383c-9.844,0 -20.199,2.655 -31.065,7.966c-10.866,5.311 -22.546,13.293 -35.039,23.945l-13.625,11.74Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/><path
|
||||
fill={color}
|
||||
d="M2387.845,220.714l-18.271,-4.667c-4.927,-1.479 -8.352,-3.2 -10.273,-5.161c-1.922,-1.962 -2.883,-4.276 -2.883,-6.943c0,-3.51 1.223,-6.199 3.669,-8.065c2.446,-1.866 5.704,-2.799 9.773,-2.799l21.99,0c5.101,-0 9.294,-0.87 12.581,-2.609c3.286,-1.74 6.416,-4.936 9.388,-9.589l34.552,-51.276c3.59,-4.91 7.104,-8.504 10.542,-10.784c3.438,-2.28 6.943,-3.419 10.516,-3.419c3.878,0 6.89,1.22 9.034,3.659c2.144,2.439 3.216,5.905 3.216,10.398l0,288.479c0,15.747 3.147,27.707 9.44,35.88c6.293,8.174 15.034,12.26 26.221,12.26c7.67,0 13.736,-1.373 18.198,-4.12c4.462,-2.747 8.049,-6.002 10.763,-9.766c2.714,-3.764 5.304,-7.236 7.771,-10.417c2.467,-3.181 5.447,-5.205 8.94,-6.073c2.733,-0.177 4.901,0.624 6.505,2.404c1.604,1.78 2.375,4.773 2.312,8.982c-0.507,11.427 -4.431,21.975 -11.773,31.643c-7.342,9.668 -17.384,17.448 -30.125,23.339c-12.741,5.891 -27.367,8.836 -43.878,8.836c-26.191,0 -46.827,-6.628 -61.909,-19.883c-15.082,-13.255 -22.622,-33.345 -22.622,-60.268l0,-192.13c0,-5.128 -1.021,-9.007 -3.063,-11.635c-2.042,-2.628 -5.58,-4.72 -10.615,-6.276Zm61.109,-0.854l0.281,-26.781l106.25,0c4.444,-0 7.866,0.858 10.266,2.573c2.399,1.715 3.599,4.257 3.599,7.625c0,4.733 -2.383,8.68 -7.148,11.841c-4.766,3.161 -12.326,4.742 -22.68,4.742l-90.568,0Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/><path
|
||||
fill={color}
|
||||
d="M2855.8,465.307l0,-21.885l-2.146,-1.526l0,-187.313c0,-4.847 -0.782,-8.346 -2.346,-10.497c-1.564,-2.151 -4.126,-3.464 -7.685,-3.94l-20.542,-1.005c-3.559,-0.667 -6.109,-1.818 -7.648,-3.453c-1.54,-1.635 -2.31,-3.733 -2.31,-6.292c0,-2.969 0.917,-5.382 2.75,-7.24c1.833,-1.858 5.189,-3.661 10.068,-5.411l62.24,-21.495c6.924,-2.559 12.538,-4.394 16.844,-5.505c4.306,-1.111 8.276,-1.667 11.911,-1.667c5.687,0 9.977,1.576 12.867,4.729c2.891,3.153 4.336,7.373 4.336,12.661l0,253.365c0,6.017 0.997,10.503 2.99,13.456c1.993,2.953 4.998,4.994 9.016,6.122l14.516,3.312c3.812,1.16 6.609,2.701 8.388,4.622c1.78,1.922 2.669,4.312 2.669,7.169c0,3.417 -1.223,6.129 -3.669,8.138c-2.446,2.009 -6.258,3.013 -11.435,3.013l-65.932,0c-10.229,0 -18.6,-3.578 -25.112,-10.734c-6.512,-7.156 -9.768,-16.698 -9.768,-28.625Zm-208.76,-50.839l0,-159.885c0,-4.847 -0.786,-8.346 -2.357,-10.497c-1.571,-2.151 -4.143,-3.464 -7.716,-3.94l-20.547,-1.005c-3.559,-0.667 -6.109,-1.818 -7.648,-3.453c-1.54,-1.635 -2.31,-3.733 -2.31,-6.292c0,-2.969 0.918,-5.382 2.753,-7.24c1.835,-1.858 5.19,-3.661 10.065,-5.411l62.286,-21.495c7.226,-2.653 12.964,-4.511 17.214,-5.576c4.25,-1.064 7.908,-1.596 10.974,-1.596c5.972,0 10.428,1.576 13.367,4.729c2.939,3.153 4.409,7.373 4.409,12.661l0,197.422c0,20.92 5.023,36.531 15.07,46.833c10.047,10.302 23.452,15.453 40.216,15.453c10.382,0 21.431,-2.572 33.146,-7.716c11.715,-5.144 23.986,-13.207 36.813,-24.19l13.62,-11.74l12.724,13.031l-13.526,11.927c-26.653,24.323 -49.98,41.37 -69.982,51.141c-20.002,9.771 -38.973,14.656 -56.914,14.656c-27.354,0 -49.469,-8.744 -66.344,-26.232c-16.875,-17.488 -25.313,-41.35 -25.313,-71.586Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/><path
|
||||
fill={color}
|
||||
d="M3130.82,330.943c0,-31.67 4.576,-58.287 13.729,-79.852c9.153,-21.564 21.071,-37.831 35.755,-48.799c14.684,-10.969 30.319,-16.453 46.906,-16.453c20.118,0 35.681,5.683 46.69,17.049c11.009,11.366 16.513,27.369 16.513,48.008c0,17.24 -3.655,30.185 -10.964,38.836c-7.309,8.651 -16.743,12.977 -28.302,12.977c-11.556,0 -20.418,-3.171 -26.586,-9.513c-6.168,-6.342 -9.284,-15.235 -9.346,-26.68l-0.047,-11.573c-0.111,-7.236 -1.802,-12.64 -5.073,-16.211c-3.271,-3.571 -8.644,-5.357 -16.12,-5.357c-8.635,0 -16.97,3.531 -25.003,10.594c-8.033,7.062 -14.597,17.733 -19.693,32.01c-5.095,14.278 -7.643,32.392 -7.643,54.344l-10.818,0.62Zm6.812,-125.427l4.005,81.208l0,171.87c0,5.497 1.199,9.661 3.596,12.495c2.398,2.833 6.598,4.719 12.602,5.656l29.714,4.427c4.462,0.701 7.769,2.029 9.922,3.982c2.153,1.953 3.229,4.694 3.229,8.221c0,3.51 -1.299,6.27 -3.896,8.279c-2.597,2.009 -6.398,3.013 -11.401,3.013l-147.318,0c-5.083,0 -8.836,-1.013 -11.258,-3.039c-2.422,-2.026 -3.633,-4.739 -3.633,-8.138c0,-2.747 0.885,-5.085 2.656,-7.016c1.771,-1.931 4.514,-3.483 8.229,-4.656l15.068,-3.406c4.035,-1.128 7.044,-3.137 9.029,-6.026c1.984,-2.889 2.977,-7.295 2.977,-13.219l0,-204.391c0,-4.861 -0.779,-8.379 -2.336,-10.555c-1.557,-2.175 -4.114,-3.503 -7.669,-3.982l-20.667,-1c-3.51,-0.667 -6.04,-1.818 -7.589,-3.453c-1.549,-1.635 -2.323,-3.724 -2.323,-6.266c0,-2.955 0.942,-5.393 2.826,-7.315c1.884,-1.922 5.232,-3.709 10.044,-5.362l61.26,-20.76c8.799,-3.306 15.307,-5.466 19.526,-6.482c4.219,-1.016 7.575,-1.523 10.068,-1.523c4.08,0 7.156,1.345 9.229,4.036c2.073,2.691 3.443,7.158 4.109,13.401Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/><path
|
||||
fill={color}
|
||||
d="M3622.771,311.365c0,9.878 -2.872,17.47 -8.615,22.776c-5.743,5.306 -14.118,7.958 -25.125,7.958l-210,0l0,-21.203l147.052,0c10.229,0 15.344,-4.622 15.344,-13.865c0,-30.597 -5.845,-54.022 -17.536,-70.273c-11.691,-16.252 -27.196,-24.378 -46.516,-24.378c-15.257,0 -28.655,4.508 -40.195,13.523c-11.54,9.016 -20.561,21.96 -27.063,38.833c-6.502,16.873 -9.753,37.037 -9.753,60.492c0,43.872 10.266,76.974 30.799,99.307c20.533,22.333 47.546,33.5 81.039,33.5c21.031,0 39.325,-4.677 54.88,-14.031c15.556,-9.354 26.748,-22.125 33.578,-38.313c2.969,-3.653 5.411,-6.146 7.326,-7.479c1.915,-1.333 3.937,-2 6.065,-2c2.938,0 5.082,1.291 6.432,3.872c1.351,2.582 1.97,5.721 1.859,9.419c-1.174,18.809 -7.769,36.038 -19.786,51.688c-12.017,15.649 -28.122,28.109 -48.315,37.38c-20.193,9.271 -43.218,13.906 -69.076,13.906c-31.069,0 -58.526,-6.501 -82.37,-19.503c-23.844,-13.002 -42.481,-31.285 -55.911,-54.849c-13.431,-23.564 -20.146,-51.166 -20.146,-82.805c0,-32.92 6.497,-62.108 19.49,-87.562c12.993,-25.455 31.546,-45.48 55.659,-60.076c24.113,-14.595 52.768,-21.893 85.966,-21.893c27.938,-0 51.99,5.361 72.159,16.083c20.168,10.722 35.67,25.512 46.505,44.37c10.835,18.858 16.253,40.564 16.253,65.12Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/><path
|
||||
fill={color}
|
||||
d="M3813.044,487.698c15.937,0 28.422,-4.111 37.453,-12.333c9.031,-8.222 13.547,-18.745 13.547,-31.568c0,-8.125 -1.861,-15.469 -5.583,-22.031c-3.722,-6.562 -10.543,-12.562 -20.464,-17.997c-9.92,-5.436 -24.125,-10.471 -42.615,-15.107c-31.625,-7.188 -56.268,-15.988 -73.93,-26.401c-17.661,-10.413 -30.004,-22.363 -37.029,-35.849c-7.024,-13.486 -10.536,-28.356 -10.536,-44.609c0,-29.288 10.358,-52.604 31.073,-69.948c20.715,-17.344 50.299,-26.016 88.75,-26.016c14.302,0 26.049,1.234 35.24,3.703c9.191,2.469 16.712,4.961 22.562,7.477c5.851,2.516 10.873,3.773 15.068,3.773c4.382,0 7.961,-1.258 10.737,-3.773c2.776,-2.516 5.492,-5.031 8.148,-7.547c2.656,-2.516 5.993,-3.773 10.01,-3.773c2.781,0 5.272,0.905 7.471,2.716c2.2,1.811 4.03,5.162 5.492,10.055l22.667,71.646c2.066,6.226 2.702,11.364 1.909,15.414c-0.793,4.05 -3.287,6.918 -7.482,8.602c-4.097,1.556 -7.667,1.551 -10.708,-0.013c-3.042,-1.564 -5.937,-4.451 -8.687,-8.659c-10.062,-18.646 -20.847,-33.42 -32.354,-44.323c-11.507,-10.903 -23.586,-18.714 -36.237,-23.435c-12.651,-4.72 -25.85,-7.081 -39.596,-7.081c-19.764,0 -34.641,4.182 -44.633,12.547c-9.991,8.365 -14.987,19.563 -14.987,33.594c0,8.41 2.122,16.051 6.367,22.924c4.245,6.873 12.054,13.202 23.427,18.987c11.373,5.785 27.584,11.288 48.633,16.51c27.465,6.378 49.29,14.37 65.474,23.974c16.184,9.604 27.82,21.081 34.909,34.43c7.089,13.349 10.633,28.883 10.633,46.602c0,18.205 -4.623,34.268 -13.87,48.19c-9.247,13.922 -22.141,24.768 -38.682,32.539c-16.542,7.771 -35.79,11.656 -57.745,11.656c-13.83,0 -25.069,-1.468 -33.719,-4.404c-8.649,-2.936 -15.788,-5.848 -21.417,-8.737c-5.628,-2.889 -10.825,-4.333 -15.589,-4.333c-4.257,0 -7.92,1.424 -10.99,4.273c-3.069,2.849 -6.016,5.722 -8.841,8.62c-2.825,2.898 -6.07,4.346 -9.737,4.346c-2.701,0 -5.029,-0.989 -6.982,-2.966c-1.953,-1.977 -3.39,-5.31 -4.31,-9.997l-13.906,-67.13c-1.462,-7.559 -1.775,-13.197 -0.94,-16.914c0.835,-3.717 3.119,-6.322 6.852,-7.815c3.986,-1.587 7.492,-1.376 10.518,0.633c3.026,2.009 6.143,5.641 9.352,10.898c13.649,24.583 28.663,42.171 45.042,52.763c16.378,10.592 33.123,15.888 50.234,15.888Z"
|
||||
style="fill-rule:nonzero;"
|
||||
/></g
|
||||
></svg
|
||||
>
|
||||
8
src/lib/copyright-year.test.ts
Normal file
8
src/lib/copyright-year.test.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { getCurrentYear } from './copyright-year';
|
||||
|
||||
describe('getCurrentYear', () => {
|
||||
it('returns the current calendar year', () => {
|
||||
expect(getCurrentYear()).toBe(new Date().getFullYear());
|
||||
});
|
||||
});
|
||||
7
src/lib/copyright-year.ts
Normal file
7
src/lib/copyright-year.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Returns the current calendar year (for testing and any server use).
|
||||
* The client-side footer year is updated by static/copyright-year.js.
|
||||
*/
|
||||
export function getCurrentYear(): number {
|
||||
return new Date().getFullYear();
|
||||
}
|
||||
26
src/lib/data/content.ts
Normal file
26
src/lib/data/content.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export const whatWeDoItems = [
|
||||
'Product-focused frontend and UI architecture for modern web applications, with an emphasis on clarity, scalability, and long-term maintainability.',
|
||||
'Greenfield product builds and early-stage foundations, getting new projects off the ground quickly with structures designed to grow, not be rewritten.',
|
||||
'Performance, Core Web Vitals, rendering strategy, and technical SEO optimization focused on real-world user journeys—not just lab scores.',
|
||||
'Accessibility-first engineering, ensuring WCAG-compliant interfaces with semantic markup, keyboard parity, and inclusive interaction patterns.',
|
||||
'Modernization and stabilization of existing systems, including refactors, framework upgrades, and untangling overgrown frontend codebases.',
|
||||
'End-to-end feature delivery with clear ownership and documentation, spanning frontend and supporting backend work without unnecessary complexity.',
|
||||
];
|
||||
|
||||
export const impactItems = [
|
||||
'Get new products off the ground quickly by establishing durable frontend and platform foundations—clean architecture, clear patterns, and pragmatic defaults designed to scale with teams and traffic.',
|
||||
'Improve performance, Core Web Vitals, and technical SEO on high-traffic user journeys through rendering strategy, bundle discipline, and careful attention to real-world loading behavior.',
|
||||
'Build accessibility into core UI systems, not as a retrofit—semantic markup, keyboard parity, and screen reader support baked into reusable components and design patterns.',
|
||||
'Bring order to complex or aging codebases by simplifying structure, reducing duplication, and clarifying ownership, enabling teams to ship confidently without over-engineering.',
|
||||
'Design and evolve shared component libraries and UI systems that improve consistency, velocity, and long-term maintainability across multiple teams.',
|
||||
'Partner closely with product, design, and engineering leadership (including marketing teams and non-technical organizations) to translate goals into shippable systems, balancing speed, quality, and technical risk.',
|
||||
];
|
||||
|
||||
export const howWeWorkItems = [
|
||||
'Engagements are consulting-led and senior-driven. I work directly with founders, product leaders, marketing teams, and engineering teams—including organizations without in-house technical staff—to establish direction and deliver solutions with a high degree of autonomy.',
|
||||
'Focused, pragmatic scope. Work is scoped to deliver real progress quickly, with an emphasis on building the right foundation rather than over-engineering for hypothetical futures.',
|
||||
'Async-friendly, low-friction communication. Clear written updates, documented decisions, and scheduled calls when they add value—not meetings for their own sake.',
|
||||
'Quality as a default. Accessibility, performance, and maintainability are built into the work from the start, not added later as cleanup.',
|
||||
"Flexible engagement models. Hourly or fixed-scope work depending on clarity and needs; longer-term engagements welcome when there's ongoing product momentum.",
|
||||
'Clean handoff. Code, documentation, and context are left in a state where internal teams—or future vendors—can confidently extend the work without dependency.',
|
||||
];
|
||||
27
src/lib/data/engagements.ts
Normal file
27
src/lib/data/engagements.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export const engagements = [
|
||||
{
|
||||
title: 'Atlassian — Senior UI Engineer (Enterprise SaaS)',
|
||||
description:
|
||||
'Frontend architecture and feature delivery for Confluence integrations, including React 18 migration work and standardizing end-to-end testing practices.',
|
||||
},
|
||||
{
|
||||
title: 'CarGurus — Principal UI Engineer (Consumer Marketplace)',
|
||||
description:
|
||||
'Built and maintained high-traffic frontend systems, improved Core Web Vitals and technical SEO, and developed shared UI platforms used across teams.',
|
||||
},
|
||||
{
|
||||
title: 'The TJX Companies (TJ Maxx) — UI Engineer (Enterprise Retail)',
|
||||
description:
|
||||
'Delivered UX improvements for large-scale e-commerce experiences in close partnership with design, QA, and product teams.',
|
||||
},
|
||||
{
|
||||
title: 'Timberland — Senior Interactive Developer (Global Ecommerce)',
|
||||
description:
|
||||
'Led global web initiatives across brand and e-commerce platforms, acting as a technical bridge between marketing, design, and engineering.',
|
||||
},
|
||||
{
|
||||
title: 'MFA Boston — Pro Bono Technical Lead (Nonprofit / Fundraising)',
|
||||
description:
|
||||
"Designed and built a custom auction application for the MFA's annual Young Patrons fundraiser; subsequently iterated on and supported the platform over multiple years as the event grew, until it concluded during the pandemic.",
|
||||
},
|
||||
];
|
||||
26
src/lib/data/experience.ts
Normal file
26
src/lib/data/experience.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export const experienceLogos = [
|
||||
{ src: '/assets/logos/atlassian.svg', alt: 'Atlassian', width: 2500, height: 2500 },
|
||||
{
|
||||
src: '/assets/logos/tjx.svg',
|
||||
alt: 'TJ Maxx (The TJX Companies)',
|
||||
width: 2500,
|
||||
height: 621,
|
||||
},
|
||||
{ src: '/assets/logos/cargurus.svg', alt: 'CarGurus', width: 2500, height: 398 },
|
||||
{ src: '/assets/logos/timberland.svg', alt: 'Timberland', width: 190, height: 35 },
|
||||
{ src: '/assets/logos/vf.svg', alt: 'VF Corporation', width: 190, height: 155 },
|
||||
{
|
||||
src: '/assets/logos/bottomline.svg',
|
||||
alt: 'Bottomline Technologies',
|
||||
width: 2702,
|
||||
height: 571,
|
||||
},
|
||||
{
|
||||
src: '/assets/logos/mfa-boston.svg',
|
||||
alt: 'Museum of Fine Arts Boston',
|
||||
width: 572,
|
||||
height: 88,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const experienceTextList = experienceLogos.map((l) => l.alt);
|
||||
9
src/lib/data/home-meta.ts
Normal file
9
src/lib/data/home-meta.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { PageMeta } from '$lib/seo';
|
||||
import { defaultJsonLdGraph } from './json-ld';
|
||||
|
||||
export const homeMeta: PageMeta = {
|
||||
title: 'mifi Ventures — Software Engineering Consulting | Boston, MA',
|
||||
description:
|
||||
'Boston-based software engineering consulting. Mike Fitzpatrick helps teams build reliable, accessible, high-performance web applications. Specializing in frontend architecture, performance optimization, and modern web development.',
|
||||
jsonLd: defaultJsonLdGraph,
|
||||
};
|
||||
150
src/lib/data/json-ld.ts
Normal file
150
src/lib/data/json-ld.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Default JSON-LD graph nodes (Organization, Person, WebSite, WebPage, OfferCatalog).
|
||||
* Used for the home page; other pages can add or override via meta.jsonLd.
|
||||
*/
|
||||
|
||||
const BASE = 'https://mifi.ventures';
|
||||
|
||||
export const defaultJsonLdGraph: Record<string, unknown>[] = [
|
||||
{
|
||||
'@type': 'Organization',
|
||||
'@id': `${BASE}/#organization`,
|
||||
name: 'mifi Ventures, LLC',
|
||||
legalName: 'mifi Ventures, LLC',
|
||||
url: `${BASE}/`,
|
||||
logo: { '@type': 'ImageObject', url: `${BASE}/favicon.svg` },
|
||||
description:
|
||||
'Software engineering consulting specializing in product-focused frontend architecture, performance optimization, and accessibility-first engineering.',
|
||||
founder: { '@id': `${BASE}/#principal` },
|
||||
address: {
|
||||
'@type': 'PostalAddress',
|
||||
addressLocality: 'Boston',
|
||||
addressRegion: 'MA',
|
||||
addressCountry: 'US',
|
||||
},
|
||||
geo: { '@type': 'GeoCoordinates', latitude: 42.360082, longitude: -71.05888 },
|
||||
areaServed: { '@type': 'Country', name: 'United States' },
|
||||
hasOfferCatalog: { '@id': `${BASE}/#services` },
|
||||
sameAs: ['https://www.linkedin.com/in/the-mifi', 'https://github.com/the-mifi'],
|
||||
},
|
||||
{
|
||||
'@type': 'Person',
|
||||
'@id': `${BASE}/#principal`,
|
||||
name: 'Mike Fitzpatrick',
|
||||
jobTitle: 'Principal Software Engineer and Architect',
|
||||
description:
|
||||
'Senior full-stack engineer and architect helping teams ship reliable, accessible, high-performance web products.',
|
||||
url: `${BASE}/`,
|
||||
worksFor: { '@id': `${BASE}/#organization` },
|
||||
knowsAbout: [
|
||||
'Frontend Architecture',
|
||||
'UI Architecture',
|
||||
'React Development',
|
||||
'Web Performance Optimization',
|
||||
'Core Web Vitals',
|
||||
'Technical SEO',
|
||||
'Web Accessibility (WCAG)',
|
||||
'Component Libraries',
|
||||
'Design Systems',
|
||||
'JavaScript',
|
||||
'TypeScript',
|
||||
'Modern Web Development',
|
||||
'Greenfield Product Development',
|
||||
'Legacy System Modernization',
|
||||
'Code Refactoring',
|
||||
],
|
||||
sameAs: ['https://www.linkedin.com/in/the-mifi', 'https://github.com/the-mifi'],
|
||||
},
|
||||
{
|
||||
'@type': 'WebSite',
|
||||
'@id': `${BASE}/#website`,
|
||||
url: `${BASE}/`,
|
||||
name: 'mifi Ventures',
|
||||
description: 'Software Engineering Consulting — Boston, MA',
|
||||
publisher: { '@id': `${BASE}/#organization` },
|
||||
potentialAction: {
|
||||
'@type': 'ReserveAction',
|
||||
target: {
|
||||
'@type': 'EntryPoint',
|
||||
urlTemplate: 'https://cal.mifi.ventures/the-mifi',
|
||||
},
|
||||
name: 'Schedule a 30-minute intro call',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'WebPage',
|
||||
'@id': `${BASE}/#webpage`,
|
||||
url: `${BASE}/`,
|
||||
name: 'mifi Ventures — Software Engineering Consulting | Boston, MA',
|
||||
description:
|
||||
'Boston-based software engineering consulting. Mike Fitzpatrick helps teams build reliable, accessible, high-performance web applications.',
|
||||
isPartOf: { '@id': `${BASE}/#website` },
|
||||
about: { '@id': `${BASE}/#organization` },
|
||||
mainEntity: { '@id': `${BASE}/#organization` },
|
||||
primaryImageOfPage: { '@type': 'ImageObject', url: `${BASE}/favicon.svg` },
|
||||
inLanguage: 'en-US',
|
||||
},
|
||||
{
|
||||
'@type': 'OfferCatalog',
|
||||
'@id': `${BASE}/#services`,
|
||||
name: 'Software Engineering Consulting Services',
|
||||
description: 'Consulting services offered by mifi Ventures',
|
||||
numberOfItems: 6,
|
||||
itemListElement: [
|
||||
{
|
||||
'@type': 'Offer',
|
||||
itemOffered: {
|
||||
'@type': 'Service',
|
||||
name: 'Frontend and UI Architecture',
|
||||
description:
|
||||
'Product-focused frontend and UI architecture for modern web applications, with an emphasis on clarity, scalability, and long-term maintainability.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Offer',
|
||||
itemOffered: {
|
||||
'@type': 'Service',
|
||||
name: 'Greenfield Product Development',
|
||||
description:
|
||||
'Greenfield product builds and early-stage foundations, getting new projects off the ground quickly with structures designed to grow, not be rewritten.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Offer',
|
||||
itemOffered: {
|
||||
'@type': 'Service',
|
||||
name: 'Performance Optimization',
|
||||
description:
|
||||
'Performance, Core Web Vitals, rendering strategy, and technical SEO optimization focused on real-world user journeys.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Offer',
|
||||
itemOffered: {
|
||||
'@type': 'Service',
|
||||
name: 'Accessibility Engineering',
|
||||
description:
|
||||
'Accessibility-first engineering, ensuring WCAG-compliant interfaces with semantic markup, keyboard parity, and inclusive interaction patterns.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Offer',
|
||||
itemOffered: {
|
||||
'@type': 'Service',
|
||||
name: 'System Modernization',
|
||||
description:
|
||||
'Modernization and stabilization of existing systems, including refactors, framework upgrades, and untangling overgrown frontend codebases.',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@type': 'Offer',
|
||||
itemOffered: {
|
||||
'@type': 'Service',
|
||||
name: 'End-to-End Feature Delivery',
|
||||
description:
|
||||
'End-to-end feature delivery with clear ownership and documentation, spanning frontend and supporting backend work without unnecessary complexity.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
53
src/lib/seo.ts
Normal file
53
src/lib/seo.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* SEO / meta: site-wide defaults, page-meta type, and merge helper.
|
||||
* Layout renders <head> from merged meta; each route can export meta from +page.ts.
|
||||
*/
|
||||
|
||||
export const SEO_DEFAULTS = {
|
||||
siteName: 'mifi Ventures',
|
||||
baseUrl: 'https://mifi.ventures',
|
||||
defaultOgImage: '/assets/og-image.png',
|
||||
ogImageWidth: 1200,
|
||||
ogImageHeight: 630,
|
||||
locale: 'en_US',
|
||||
twitterCard: 'summary_large_image' as const,
|
||||
themeColorLight: '#0052cc',
|
||||
themeColorDark: '#4da6ff',
|
||||
} as const;
|
||||
|
||||
export interface PageMeta {
|
||||
title: string;
|
||||
description?: string;
|
||||
canonical?: string;
|
||||
ogImage?: string;
|
||||
ogType?: string;
|
||||
twitterTitle?: string;
|
||||
twitterDescription?: string;
|
||||
/** JSON-LD graph nodes (merged with defaults in layout) */
|
||||
jsonLd?: Record<string, unknown>[];
|
||||
}
|
||||
|
||||
export interface MergedMeta extends PageMeta {
|
||||
canonical: string;
|
||||
ogImage: string;
|
||||
ogImageAlt: string;
|
||||
jsonLdGraph: Record<string, unknown>[];
|
||||
}
|
||||
|
||||
/** Merge page meta with site defaults for rendering. */
|
||||
export function mergeMeta(meta: PageMeta, path: string = '/'): MergedMeta {
|
||||
const baseUrl = SEO_DEFAULTS.baseUrl;
|
||||
const canonical = meta.canonical ?? `${baseUrl}${path === '/' ? '' : path}`;
|
||||
const ogImage = meta.ogImage?.startsWith('http')
|
||||
? meta.ogImage
|
||||
: `${baseUrl}${meta.ogImage?.startsWith('/') ? meta.ogImage : SEO_DEFAULTS.defaultOgImage}`;
|
||||
const ogImageAlt = meta.title;
|
||||
const jsonLdGraph = meta.jsonLd ?? [];
|
||||
return {
|
||||
...meta,
|
||||
canonical,
|
||||
ogImage,
|
||||
ogImageAlt,
|
||||
jsonLdGraph,
|
||||
};
|
||||
}
|
||||
141
src/routes/+layout.svelte
Normal file
141
src/routes/+layout.svelte
Normal file
@@ -0,0 +1,141 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { mergeMeta, SEO_DEFAULTS } from '$lib/seo';
|
||||
import { homeMeta } from '$lib/data/home-meta';
|
||||
|
||||
import '../app.css';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
const meta = $derived(page.data?.meta ?? homeMeta);
|
||||
const path = $derived(page.url?.pathname ?? '/');
|
||||
const merged = $derived(mergeMeta(meta, path));
|
||||
|
||||
const jsonLdScript = $derived(
|
||||
merged.jsonLdGraph.length > 0
|
||||
? JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': merged.jsonLdGraph,
|
||||
})
|
||||
: '',
|
||||
);
|
||||
const jsonLdHtml = $derived(
|
||||
jsonLdScript
|
||||
? '<script type="application/ld+json">' + jsonLdScript + '</scr' + 'ipt>'
|
||||
: '',
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{merged.title}</title>
|
||||
<meta name="description" content={merged.description ?? ''} />
|
||||
<link rel="canonical" href={merged.canonical} />
|
||||
|
||||
<link
|
||||
rel="preload"
|
||||
href="/assets/fonts/fraunces-v38-latin-600.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/assets/fonts/fraunces-v38-latin-700.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/assets/fonts/inter-v20-latin-regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/assets/fonts/inter-v20-latin-italic.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/assets/fonts/inter-v20-latin-500.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/assets/fonts/inter-v20-latin-600.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/assets/fonts/inter-v20-latin-700.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
|
||||
<meta
|
||||
name="robots"
|
||||
content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1"
|
||||
/>
|
||||
<meta name="author" content="Mike Fitzpatrick" />
|
||||
<meta name="geo.region" content="US-MA" />
|
||||
<meta name="geo.placename" content="Boston" />
|
||||
<meta name="geo.position" content="42.360082;-71.058880" />
|
||||
<meta name="ICBM" content="42.360082, -71.058880" />
|
||||
|
||||
<meta
|
||||
name="theme-color"
|
||||
content={SEO_DEFAULTS.themeColorLight}
|
||||
media="(prefers-color-scheme: light)"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content={SEO_DEFAULTS.themeColorDark}
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
|
||||
<meta property="og:type" content={merged.ogType ?? 'website'} />
|
||||
<meta property="og:url" content={merged.canonical} />
|
||||
<meta property="og:site_name" content={SEO_DEFAULTS.siteName} />
|
||||
<meta property="og:title" content={merged.twitterTitle ?? merged.title} />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={merged.twitterDescription ?? merged.description ?? ''}
|
||||
/>
|
||||
<meta property="og:image" content={merged.ogImage} />
|
||||
<meta property="og:image:width" content={String(SEO_DEFAULTS.ogImageWidth)} />
|
||||
<meta property="og:image:height" content={String(SEO_DEFAULTS.ogImageHeight)} />
|
||||
<meta property="og:image:alt" content={merged.ogImageAlt} />
|
||||
<meta property="og:locale" content={SEO_DEFAULTS.locale} />
|
||||
|
||||
<meta name="twitter:card" content={SEO_DEFAULTS.twitterCard} />
|
||||
<meta name="twitter:url" content={merged.canonical} />
|
||||
<meta name="twitter:title" content={merged.twitterTitle ?? merged.title} />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content={merged.twitterDescription ?? merged.description ?? ''}
|
||||
/>
|
||||
<meta name="twitter:image" content={merged.ogImage} />
|
||||
<meta name="twitter:image:alt" content={merged.ogImageAlt} />
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="/favicon.svg" />
|
||||
|
||||
{#if jsonLdHtml}
|
||||
{@html jsonLdHtml}
|
||||
{/if}
|
||||
|
||||
<script src="/assets/scripts/copyright-year.js" defer></script>
|
||||
</svelte:head>
|
||||
|
||||
<a href="#main" class="skip-link">Skip to main content</a>
|
||||
{@render children()}
|
||||
3
src/routes/+layout.ts
Normal file
3
src/routes/+layout.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const prerender = true;
|
||||
export const ssr = true;
|
||||
export const csr = false;
|
||||
21
src/routes/+page.svelte
Normal file
21
src/routes/+page.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import Hero from '$lib/components/Hero.svelte';
|
||||
import ExperienceSection from '$lib/components/ExperienceSection.svelte';
|
||||
import WhatWeDo from '$lib/components/WhatWeDo.svelte';
|
||||
import ImpactSection from '$lib/components/ImpactSection.svelte';
|
||||
import HowWeWork from '$lib/components/HowWeWork.svelte';
|
||||
import EngagementsSection from '$lib/components/EngagementsSection.svelte';
|
||||
import ScheduleSection from '$lib/components/ScheduleSection.svelte';
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
</script>
|
||||
|
||||
<Hero />
|
||||
<main id="main">
|
||||
<ExperienceSection />
|
||||
<WhatWeDo />
|
||||
<ImpactSection />
|
||||
<HowWeWork />
|
||||
<EngagementsSection />
|
||||
<ScheduleSection />
|
||||
</main>
|
||||
<Footer />
|
||||
6
src/routes/+page.ts
Normal file
6
src/routes/+page.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import { homeMeta } from '$lib/data/home-meta';
|
||||
|
||||
export const load: PageLoad = () => {
|
||||
return { meta: homeMeta };
|
||||
};
|
||||
Reference in New Issue
Block a user