Skip to content
2 changes: 1 addition & 1 deletion app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default function About() {
</div>
</div>
{/* Bottom border accent with gradient */}
<div className="h-1.5 bg-gradient-to-r from-[#c85200] via-[#fc7d0b] to-[#1170aa] rounded-b-lg" />
<div className="h-1.5 bg-gradient-to-r from-accent-orange-hover via-accent-orange to-accent rounded-b-lg" />
</div>
</div>

Expand Down
4 changes: 2 additions & 2 deletions app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export default async function BlogPost({ params }: { params: Promise<{ slug: str
href={post.mediumUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-4 py-2 bg-black text-white rounded-lg hover:bg-gray-800 transition-colors text-sm font-semibold"
className="inline-flex items-center gap-2 px-4 py-2 bg-accent-orange text-on-accent-orange rounded-lg hover:bg-accent-orange-hover transition-colors text-sm font-semibold"
>
<FaMedium className="w-4 h-4" />
Read on Medium
Expand Down Expand Up @@ -103,7 +103,7 @@ export default async function BlogPost({ params }: { params: Promise<{ slug: str
href={post.mediumUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 bg-black text-white rounded-lg hover:bg-gray-800 transition-colors font-semibold"
className="inline-flex items-center gap-2 px-6 py-3 bg-accent-orange text-on-accent-orange rounded-lg hover:bg-accent-orange-hover transition-colors font-semibold"
>
<FaMedium className="w-5 h-5" />
Continue reading on Medium
Expand Down
2 changes: 1 addition & 1 deletion app/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export default async function Blog() {
title="Read on Medium"
aria-label="Read on Medium"
>
<FaMedium className="w-5 h-5 text-text-secondary hover:text-black transition-colors" />
<FaMedium className="w-5 h-5 text-text-secondary hover:text-accent-orange transition-colors" />
</a>
)}
</div>
Expand Down
4 changes: 2 additions & 2 deletions app/contact/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ export default function Contact() {
<ScrollReveal delayMs={100}>
<div className="glass-card shine-effect rounded-xl p-6">
<div className="flex items-center gap-4 mb-4">
<div className="w-12 h-12 bg-gradient-to-br from-[#0077b5] to-[#005582] rounded-xl flex items-center justify-center icon-bounce shadow-lg">
<FaLinkedin className="w-6 h-6 text-white" />
<div className="w-12 h-12 bg-gradient-to-br from-accent-orange to-accent-orange-hover rounded-xl flex items-center justify-center icon-bounce shadow-lg">
<FaLinkedin className="w-6 h-6 text-on-accent-orange" />
</div>
<h2 className="text-2xl font-semibold text-text-primary">LinkedIn</h2>
</div>
Expand Down
96 changes: 58 additions & 38 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@
theme, so dark text is always the right call for it. */
--on-accent: #ffffff;
--on-accent-orange: #1a1a1a;
/* Fixed (non-theme-dependent) blue used for the small number of buttons
that are deliberately "the blue button" as opposed to "the theme's
highlight color" (e.g. distinguishing two CTAs from each other on the
same page). Unlike --accent, this never changes with the theme -
Xanga's --accent is bright orange, so reusing --accent here would make
a "blue" button render orange and become indistinguishable from an
orange one. Deliberately not overridden in the dark/Xanga blocks below
so every theme gets the same value. Dark enough that white text always
has sufficient contrast on it. */
--accent-blue: #1170aa;
--accent-blue-hover: #0d5a88;
--on-accent-blue: #ffffff;
/* Shared "primary CTA" color, used by every solid gradient button
(.gradient-btn) so all of them agree on one hue per theme instead of
mixing blue and orange buttons on the same page. Light mode's --accent
is a dark, saturated blue that reads well with white text, so primary
buttons stay blue here; dark and Xanga both lean on the bright orange
instead (see overrides below), since dark mode's --accent is a lighter
blue that needs a dark-text workaround, and Xanga's --accent already
*is* the orange. */
--btn-primary-from: var(--accent);
--btn-primary-to: var(--accent-hover);
--btn-primary-text: var(--on-accent);
--btn-primary-shadow-rgb: 17, 112, 170;
--border: #c8d0d9; /* Very Light Gray from palette */
--link: #1170aa; /* Dark Blue for links */
--link-hover: #fc7d0b; /* Bright Orange for link hover */
Expand All @@ -58,6 +58,12 @@
contrast - use dark text instead. */
--on-accent: #0b1220;
--on-accent-orange: #1a1a1a;
/* Primary buttons switch to orange in dark mode - see the --btn-primary-*
comment in :root above. */
--btn-primary-from: var(--accent-orange);
--btn-primary-to: var(--accent-orange-hover);
--btn-primary-text: var(--on-accent-orange);
--btn-primary-shadow-rgb: 252, 125, 11;
--border: #57606c; /* Dark Gray from palette */
--link: #5fa2ce; /* Medium Blue for links */
--link-hover: #fc7d0b; /* Bright Orange for link hover */
Expand Down Expand Up @@ -86,6 +92,12 @@
needs dark text too. */
--on-accent: #0a0a0a;
--on-accent-orange: #1a1a1a;
/* Primary buttons stay orange in Xanga too - it's already the theme's
dominant highlight color, same rationale as dark mode above. */
--btn-primary-from: var(--accent);
--btn-primary-to: var(--accent-hover);
--btn-primary-text: var(--on-accent-orange);
--btn-primary-shadow-rgb: 252, 125, 11;
--border: #fc7d0b; /* Tableau Bright Orange */
--border-navy: #1170aa; /* Tableau Dark Blue */
--link: #5fa2ce; /* Tableau Medium Blue */
Expand Down Expand Up @@ -270,15 +282,20 @@ html[data-theme="xanga"] body header nav a:focus-visible * {
on top of that background. This is deliberately not scoped to "header"
only: the mobile nav menu is a sibling of <nav> rather than nested inside
it, and the same bg-accent-on-a-link pattern shows up outside the header
too. */
html[data-theme="xanga"] body a[class*="bg-accent"],
html[data-theme="xanga"] body a[class*="bg-accent"]:hover,
html[data-theme="xanga"] body a[class*="bg-accent"]:focus-visible {
too.
The :not([class*="hover:bg-accent"]) guard matters: [class*="bg-accent"]
is a substring match, so without it this would also match links whose
ONLY accent-flavored class is a hover-prefixed one (e.g. the inactive
mobile-nav links' "hover:bg-accent-orange") and force --on-accent text
onto them even while at rest, over their actual resting text color. */
html[data-theme="xanga"] body a[class*="bg-accent"]:not([class*="hover:bg-accent"]),
html[data-theme="xanga"] body a[class*="bg-accent"]:not([class*="hover:bg-accent"]):hover,
html[data-theme="xanga"] body a[class*="bg-accent"]:not([class*="hover:bg-accent"]):focus-visible {
color: var(--on-accent) !important;
}
html[data-theme="xanga"] body a[class*="bg-accent"] *,
html[data-theme="xanga"] body a[class*="bg-accent"]:hover *,
html[data-theme="xanga"] body a[class*="bg-accent"]:focus-visible * {
html[data-theme="xanga"] body a[class*="bg-accent"]:not([class*="hover:bg-accent"]) *,
html[data-theme="xanga"] body a[class*="bg-accent"]:not([class*="hover:bg-accent"]):hover *,
html[data-theme="xanga"] body a[class*="bg-accent"]:not([class*="hover:bg-accent"]):focus-visible * {
color: var(--on-accent) !important;
}

Expand Down Expand Up @@ -362,10 +379,23 @@ html[data-theme="xanga"] body [class*="text-text-secondary"] {
color: var(--text-secondary) !important;
}

html[data-theme="xanga"] body [class*="text-accent"] {
/* [class*="text-accent"] is a substring match, so without the :not() guards
it would also catch "text-accent-hover" and "text-accent-orange" (both
contain "text-accent") and force them to var(--accent) too, instead of
their own distinct shade - the same class of bug fixed above for
a[class*="bg-accent"]. */
html[data-theme="xanga"] body [class*="text-accent"]:not([class*="text-accent-hover"]):not([class*="text-accent-orange"]) {
color: var(--accent) !important;
}

html[data-theme="xanga"] body [class*="text-accent-hover"] {
color: var(--accent-hover) !important;
}

html[data-theme="xanga"] body [class*="text-accent-orange"] {
color: var(--accent-orange) !important;
}

/* Input and form elements */
html[data-theme="xanga"] body input,
html[data-theme="xanga"] body textarea,
Expand Down Expand Up @@ -688,42 +718,32 @@ html[data-theme="xanga"] body ::-webkit-scrollbar-thumb:hover {
box-shadow: 0 16px 48px rgba(252, 125, 11, 0.25);
}

/* Gradient Button Style */
/* Gradient Button Style. All solid CTA buttons share this one style so they
agree on a single hue per theme (blue in light mode, orange in dark and
Xanga - see the --btn-primary-* tokens above) instead of some buttons
being permanently blue and others permanently orange on the same page. */
.gradient-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 14px 28px;
border-radius: 12px;
border: none;
background: linear-gradient(135deg, var(--accent-orange) 0%, var(--accent-orange-hover) 100%);
background: linear-gradient(135deg, var(--btn-primary-from) 0%, var(--btn-primary-to) 100%);
/* !important: in Xanga mode, the generic "body a" link-color rule has
higher selector specificity than this class alone and would otherwise
win even without !important on its side. */
color: var(--on-accent-orange) !important;
color: var(--btn-primary-text) !important;
font-weight: 600;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 20px rgba(252, 125, 11, 0.4);
box-shadow: 0 4px 20px rgba(var(--btn-primary-shadow-rgb), 0.4);
text-decoration: none;
}

.gradient-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(252, 125, 11, 0.5);
}

.gradient-btn-blue {
/* Uses the fixed --accent-blue tokens (not --accent) so this button stays
visually blue - and distinct from the orange .gradient-btn - even in
Xanga mode, where --accent itself is the bright orange highlight color. */
background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-blue-hover) 100%);
color: var(--on-accent-blue) !important;
box-shadow: 0 4px 20px rgba(17, 112, 170, 0.4);
}

.gradient-btn-blue:hover {
box-shadow: 0 8px 30px rgba(17, 112, 170, 0.5);
box-shadow: 0 8px 30px rgba(var(--btn-primary-shadow-rgb), 0.5);
}

/* Glow Effect */
Expand Down
4 changes: 2 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export default function Home() {
</div>
</div>
{/* Bottom border accent with gradient */}
<div className="h-1.5 bg-gradient-to-r from-[#c85200] via-[#fc7d0b] to-[#1170aa] rounded-b-lg" />
<div className="h-1.5 bg-gradient-to-r from-accent-orange-hover via-accent-orange to-accent rounded-b-lg" />
</div>
</div>
</div>
Expand Down Expand Up @@ -238,7 +238,7 @@ export default function Home() {
</p>
<a
href="mailto:hello@thomaslbohn.com"
className="gradient-btn-blue gradient-btn"
className="gradient-btn"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
Expand Down
11 changes: 7 additions & 4 deletions components/projects/ProjectArtifact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ const typeLabels: Record<ProjectArtifactType['type'], string> = {
example: 'Example',
}

// Uses theme-aware tokens (instead of hardcoded Tableau hex values) so these
// badges shift with the active theme like everything else, rather than
// staying frozen at their light-mode colors in dark/Xanga.
const typeColors: Record<ProjectArtifactType['type'], string> = {
artifact: 'bg-[#1170aa]/10 text-[#1170aa] border-[#1170aa]/20', // Tableau Dark Blue
deliverable: 'bg-[#5fa2ce]/10 text-[#5fa2ce] border-[#5fa2ce]/20', // Tableau Medium Blue
phase: 'bg-[#57606c]/10 text-[#57606c] border-[#57606c]/20', // Tableau Dark Gray
example: 'bg-[#fc7d0b]/10 text-[#fc7d0b] border-[#fc7d0b]/20', // Tableau Bright Orange
artifact: 'bg-accent/10 text-accent border-accent/20',
deliverable: 'bg-accent-hover/10 text-accent-hover border-accent-hover/20',
phase: 'bg-text-secondary/10 text-text-secondary border-text-secondary/20',
example: 'bg-accent-orange/10 text-accent-orange border-accent-orange/20',
}

export function ProjectArtifact({ artifact }: ProjectArtifactProps) {
Expand Down
44 changes: 28 additions & 16 deletions tailwind.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import type { Config } from 'tailwindcss'

// Our theme colors are plain CSS custom properties (e.g. `var(--accent)`)
// rather than static hex/rgb values, since their actual value changes per
// theme (light/dark/xanga). Tailwind can only generate opacity-modifier
// utilities (e.g. `bg-accent/10`, `border-accent-orange/40`) for colors it
// can decompose into RGB channels at build time - a bare `var(--x)` doesn't
// qualify, so those classes were silently generating no CSS at all (fully
// transparent, invisible borders) anywhere they were used. Wrapping each
// color in `color-mix()` with the `<alpha-value>` placeholder lets Tailwind
// substitute the requested opacity at build time while still resolving the
// actual color from the CSS variable at runtime. See:
// https://github.com/tailwindlabs/tailwindcss/discussions/12824
function withOpacity(cssVariable: string) {
return `color-mix(in srgb, var(${cssVariable}) calc(<alpha-value> * 100%), transparent)`
}

const config: Config = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
Expand All @@ -9,22 +24,19 @@ const config: Config = {
theme: {
extend: {
colors: {
'bg-primary': 'var(--bg-primary)',
'bg-secondary': 'var(--bg-secondary)',
'text-primary': 'var(--text-primary)',
'text-secondary': 'var(--text-secondary)',
'accent': 'var(--accent)',
'accent-hover': 'var(--accent-hover)',
'accent-orange': 'var(--accent-orange)',
'accent-orange-hover': 'var(--accent-orange-hover)',
'accent-blue': 'var(--accent-blue)',
'accent-blue-hover': 'var(--accent-blue-hover)',
'on-accent': 'var(--on-accent)',
'on-accent-orange': 'var(--on-accent-orange)',
'on-accent-blue': 'var(--on-accent-blue)',
'border': 'var(--border)',
'link': 'var(--link)',
'link-hover': 'var(--link-hover)',
'bg-primary': withOpacity('--bg-primary'),
'bg-secondary': withOpacity('--bg-secondary'),
'text-primary': withOpacity('--text-primary'),
'text-secondary': withOpacity('--text-secondary'),
'accent': withOpacity('--accent'),
'accent-hover': withOpacity('--accent-hover'),
'accent-orange': withOpacity('--accent-orange'),
'accent-orange-hover': withOpacity('--accent-orange-hover'),
'on-accent': withOpacity('--on-accent'),
'on-accent-orange': withOpacity('--on-accent-orange'),
'border': withOpacity('--border'),
'link': withOpacity('--link'),
'link-hover': withOpacity('--link-hover'),
},
},
},
Expand Down
Loading