Skip to content

Commit f03f44f

Browse files
authored
feat(templates): migrate portfolio to the semantic theme-token architecture (emdash-cms#1733)
* feat(templates): migrate portfolio to the semantic theme-token architecture Same treatment as blog/marketing: tokens move to src/styles/tokens.css as @layer base defaults with light-dark() colors and a @supports fallback for pre-light-dark browsers; theme.css becomes a pure override file; the theme switcher pins color-scheme instead of maintaining triplicated palettes. Semantic names: --font-heading (was --font-serif, incl. astro.config cssVariable in both variants), --font-body (was --font-sans), --color-brand (was --color-accent) with --color-on-brand and --color-brand-ring replacing hardcoded white and the frozen violet focus ring, and --color-danger fixing form errors that ignored the theme toggle. Adds weight/leading/tracking tokens, drops the unused --color-accent-muted and --font-mono, and strips ProjectCard's inline var() fallbacks that would mask a broken token. * fix(templates): guard portfolio color-scheme pins behind @supports Same fix as on the blog/marketing branch: without the guard, browsers on the plain-light fallback palette get dark UA form controls when the toggle sets the dark cookie.
1 parent 92fd412 commit f03f44f

23 files changed

Lines changed: 494 additions & 494 deletions

File tree

templates/portfolio-cloudflare/AGENTS.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,30 +71,34 @@ The `gallery` field on `projects` is a JSON field, not an EmDash image field. It
7171

7272
## Visual character
7373

74-
Typography is the design. The display face is **Playfair Display** (serif) on the `--font-serif` CSS variable; the body face is the system sans stack on `--font-sans`. The serif is used for the site title, hero titles, project titles, page titles, and contact column labels. Everything else is the sans.
74+
Typography is the design. The display face is **Playfair Display** (serif) on the `--font-heading` CSS variable; the body face is the system sans stack on `--font-body`. The serif is used for the site title, hero titles, project titles, page titles, and contact column labels. Everything else is the sans. Serif weight is calm on purpose (`--font-weight-heading` and `--font-weight-display` both default to 500).
7575

76-
The accent colour is barely visible by design -- the only saturated colour on the page should be inside images. The default `--color-accent` (`#7c3aed`) is used sparingly for link hover and focus states.
76+
The brand colour is barely visible by design -- the only saturated colour on the page should be inside images. The default `--color-brand` (`#7c3aed`) is used sparingly for link hover and focus states.
7777

7878
Whitespace is generous. Sections breathe. Don't fight that.
7979

8080
## Customisation
8181

82-
`src/styles/theme.css` is the only file to edit for visual changes. Every CSS variable from `Base.astro` is listed there as a commented default -- uncomment and change to override. The dark mode palette is defined inside `Base.astro` itself; light-mode overrides in `theme.css` won't affect dark mode. To customise dark mode, add `@media (prefers-color-scheme: dark)` and `:root.dark` rules in `theme.css`.
82+
Design tokens live in `src/styles/tokens.css` with their default values. To restyle the site, override tokens in `src/styles/theme.css` -- declarations there are unlayered, so they always beat the `@layer base` defaults. Don't edit `tokens.css` or `Base.astro` for visual changes.
8383

84-
Fonts are configured in `astro.config.mjs` under `fonts:` (the Astro Fonts API). To change the display face, swap the `name:` for any Google Fonts serif and keep `cssVariable: "--font-serif"`. Good pairings: Cormorant Garamond, Fraunces, EB Garamond, DM Serif Display. Avoid changing the body font unless you have a reason -- system sans is deliberately quiet here.
84+
Colours are defined with `light-dark(<light>, <dark>)`, so each token carries both modes. Overriding with a plain colour changes light and dark at once; use `light-dark()` in the override to keep them distinct. There is no separate dark palette to maintain.
8585

86-
CSS variables worth knowing:
86+
The display face is configured in `astro.config.mjs` under `fonts:` (the Astro Fonts API). To change it, swap the `name:` for any Google Fonts serif and keep `cssVariable: "--font-heading"`. Good pairings: Cormorant Garamond, Fraunces, EB Garamond, DM Serif Display. The body face (`--font-body`) is a plain token in `tokens.css` -- system sans, deliberately quiet; override it in `theme.css` only if you have a reason.
8787

88-
- `--color-accent` / `--color-accent-muted` -- the single accent, used very sparingly
88+
CSS variables worth knowing (see `tokens.css` for the full list):
89+
90+
- `--color-brand`, `--color-on-brand`, `--color-brand-ring` -- the single accent, used very sparingly
8991
- `--color-bg`, `--color-surface`, `--color-text`, `--color-muted`, `--color-border` -- neutral palette
90-
- `--font-serif`, `--font-sans` -- bound to the Fonts API entries in `astro.config.mjs`
92+
- `--color-danger` -- form errors
93+
- `--font-heading` (Fonts API entry in `astro.config.mjs`), `--font-body` (token)
94+
- `--font-weight-heading` / `--font-weight-display` (both 500) -- raise for a heavier serif voice
9195
- `--font-size-4xl` -- the size of the homepage title and project titles
9296
- `--max-width` (720px), `--wide-width` (1200px) -- column widths
9397

9498
## What not to do
9599

96100
- Don't introduce gradients, drop shadows on cards, or coloured section backgrounds. The template's voice is calm and editorial; those break it.
97-
- Don't change `--font-sans` to a display font. Two display faces fight each other.
101+
- Don't change `--font-body` to a display font. Two display faces fight each other.
98102
- Don't add more than one accent colour.
99103
- Don't write generic copy like "Welcome to my portfolio" or "Crafting beautiful experiences". The work should speak; the words should be specific (a client name, a discipline, a year).
100104
- Don't pack the home page with every project. The "Selected Work" framing is intentional -- 3-6 is plenty.

templates/portfolio-cloudflare/astro.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export default defineConfig({
2222
{
2323
provider: fontProviders.google(),
2424
name: "Playfair Display",
25-
cssVariable: "--font-serif",
25+
cssVariable: "--font-heading",
2626
weights: [400, 500, 600, 700],
2727
fallbacks: ["serif"],
2828
},

templates/portfolio-cloudflare/src/components/ProjectCard.astro

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -56,23 +56,23 @@ const allTags = [...(categories || []), ...(tags || [])];
5656
.card-link {
5757
display: flex;
5858
flex-direction: column;
59-
gap: var(--spacing-lg, 1.5rem);
59+
gap: var(--spacing-lg);
6060
text-decoration: none;
6161
color: inherit;
6262
}
6363

6464
.card-image {
6565
position: relative;
6666
overflow: hidden;
67-
border-radius: var(--radius, 4px);
67+
border-radius: var(--radius);
6868
}
6969

7070
.card-image img {
7171
width: 100%;
7272
height: auto;
7373
aspect-ratio: 4 / 3;
7474
object-fit: cover;
75-
transition: transform var(--transition-slow, 300ms ease);
75+
transition: transform var(--transition-slow);
7676
}
7777

7878
.card-overlay {
@@ -82,21 +82,22 @@ const allTags = [...(categories || []), ...(tags || [])];
8282
align-items: center;
8383
justify-content: center;
8484
background: rgba(0, 0, 0, 0);
85-
transition: background var(--transition-base, 200ms ease);
85+
transition: background var(--transition-base);
8686
}
8787

88+
/* Over-image chip: white regardless of theme */
8889
.card-view {
89-
font-family: var(--font-serif, Georgia, serif);
90-
font-size: var(--font-size-sm, 0.875rem);
90+
font-family: var(--font-heading);
91+
font-size: var(--font-size-sm);
9192
color: white;
92-
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
93+
padding: var(--spacing-sm) var(--spacing-md);
9394
border: 1px solid white;
94-
border-radius: var(--radius, 4px);
95+
border-radius: var(--radius);
9596
opacity: 0;
9697
transform: translateY(10px);
9798
transition:
98-
opacity var(--transition-base, 200ms ease),
99-
transform var(--transition-base, 200ms ease);
99+
opacity var(--transition-base),
100+
transform var(--transition-base);
100101
}
101102

102103
.project-card:hover .card-image img {
@@ -115,33 +116,33 @@ const allTags = [...(categories || []), ...(tags || [])];
115116
.card-content {
116117
display: flex;
117118
flex-direction: column;
118-
gap: var(--spacing-xs, 0.25rem);
119+
gap: var(--spacing-xs);
119120
}
120121

121122
.card-title {
122-
font-family: var(--font-serif, Georgia, serif);
123-
font-size: var(--font-size-xl, 1.5rem);
124-
font-weight: 500;
125-
transition: color var(--transition-fast, 150ms ease);
123+
font-family: var(--font-heading);
124+
font-size: var(--font-size-xl);
125+
font-weight: var(--font-weight-heading);
126+
transition: color var(--transition-fast);
126127
}
127128

128129
.project-card:hover .card-title {
129-
color: var(--color-accent, #7c3aed);
130+
color: var(--color-brand);
130131
}
131132

132133
.card-meta {
133-
font-size: var(--font-size-sm, 0.875rem);
134-
color: var(--color-muted, #6b7280);
134+
font-size: var(--font-size-sm);
135+
color: var(--color-muted);
135136
}
136137

137138
.card-separator {
138-
margin: 0 var(--spacing-xs, 0.25rem);
139+
margin: 0 var(--spacing-xs);
139140
}
140141

141142
.card-summary {
142-
font-size: var(--font-size-sm, 0.875rem);
143-
color: var(--color-muted, #6b7280);
144-
line-height: 1.6;
143+
font-size: var(--font-size-sm);
144+
color: var(--color-muted);
145+
line-height: var(--leading-normal);
145146
display: -webkit-box;
146147
-webkit-line-clamp: 2;
147148
-webkit-box-orient: vertical;
@@ -151,17 +152,17 @@ const allTags = [...(categories || []), ...(tags || [])];
151152
.card-categories {
152153
display: flex;
153154
flex-wrap: wrap;
154-
gap: var(--spacing-sm, 0.5rem);
155-
margin-top: var(--spacing-sm, 0.5rem);
155+
gap: var(--spacing-sm);
156+
margin-top: var(--spacing-sm);
156157
}
157158

158159
.card-category {
159-
font-size: 0.75rem;
160+
font-size: var(--font-size-xs);
160161
text-transform: uppercase;
161-
letter-spacing: 0.05em;
162-
color: var(--color-muted, #6b7280);
163-
padding: var(--spacing-xs, 0.25rem) var(--spacing-sm, 0.5rem);
164-
border: 1px solid var(--color-border, #e5e7eb);
165-
border-radius: var(--radius, 4px);
162+
letter-spacing: var(--tracking-wide);
163+
color: var(--color-muted);
164+
padding: var(--spacing-xs) var(--spacing-sm);
165+
border: 1px solid var(--color-border);
166+
border-radius: var(--radius);
166167
}
167168
</style>

0 commit comments

Comments
 (0)