+3
.gitignore
+3
.gitignore
-246
PROJECT_STATE.md
-246
PROJECT_STATE.md
···
1
-
# Portfolio Project - Current State Documentation
2
-
3
-
**Generated:** 2025-11-07
4
-
**Status:** Mid-Refactor
5
-
6
-
## Project Overview
7
-
8
-
React 18 + TypeScript portfolio site built with Vite, using Tailwind CSS for styling and React Router for navigation. Data-driven content from JSON files converted from CSV sources.
9
-
10
-
---
11
-
12
-
## Technology Stack
13
-
14
-
- **Framework:** React 18.3.1 + TypeScript 5.6.2
15
-
- **Build Tool:** Vite 7.1.10
16
-
- **Router:** React Router DOM 6.26.2
17
-
- **Styling:** Tailwind CSS 3.4.13 with custom fluid typography plugin
18
-
- **SEO:** react-helmet-async
19
-
- **Icons:** Heroicons & Lucide React
20
-
21
-
---
22
-
23
-
## Site Map (Current vs. Goal)
24
-
25
-
### Current Pages (3/6)
26
-
1. โ
**Home** (`/`) - Up-to-date
27
-
2. โ
**About** (`/about`) - Up-to-date
28
-
3. ๐ก **Works** (`/works`) - Structure exists, needs data integration
29
-
30
-
### Goal Pages (To Be Created)
31
-
4. โช **Writing** (`/writing`) - Will consume Bluesky PDS content
32
-
5. โช **Links** (`/links`) - Personal linktree style page
33
-
6. โช **Contact** (`/contact`) - Contact page
34
-
35
-
---
36
-
37
-
## Component Organization
38
-
39
-
### Current Structure
40
-
27 component directories with mixed organization patterns (mid-refactor)
41
-
42
-
### Target Component Structure (4-File System)
43
-
```
44
-
/ComponentName/
45
-
โโโ ComponentName.tsx # Component implementation
46
-
โโโ ComponentName.types.ts # TypeScript interfaces
47
-
โโโ ComponentName.styles.ts # Tailwind class objects
48
-
โโโ ComponentName.constants.ts # Constants & config
49
-
```
50
-
51
-
### Components Following Pattern (3-file only, NO constants yet)
52
-
- โ
Heading, Button, Link, Paragraph, Banner, Artwork, Divider
53
-
- โ
Section (with theme context provider)
54
-
- โ
PostCarousel, Skills, Tags, Events, Facts, Timeline
55
-
56
-
### Components Needing Refactor
57
-
- Card variants (multiple files in one folder)
58
-
- Navigation components (BackButton, ThemeToggle)
59
-
- Footer, Logo, Breadcrumb (single files)
60
-
- Section/Home/* components (old pattern)
61
-
- home/* components (incomplete)
62
-
63
-
---
64
-
65
-
## Data Management
66
-
67
-
### Protected Data Files (`src/data/`)
68
-
69
-
**JSON Files** (`src/data/json/`):
70
-
- `articles.json` - 8.8 KB - Publication data
71
-
- `casestudies.json` - 14.3 KB - Portfolio projects (16 items)
72
-
- `companies.json` - 6.3 KB - Client information
73
-
- `content.json` - 68.9 KB - Combined dataset
74
-
- `jobs.json` - 17.2 KB - Employment history
75
-
- `skills.json` - 4.1 KB - Professional skills
76
-
77
-
**CSV Source Files** (`src/data/csv/`):
78
-
Expected format: "Barry Prendergast - [Type].csv"
79
-
- Companies, Skills, Portfolio, Jobs, Articles
80
-
81
-
**Data Access Layer** (`src/lib/data/getData.ts`):
82
-
```typescript
83
-
getContent() // All data combined
84
-
getArticles() // Article[]
85
-
getCaseStudies() // CaseStudy[]
86
-
getCompanies() // Company[]
87
-
getJobs() // Job[]
88
-
getSkills() // Skill[]
89
-
getItemBySlug() // Generic lookup
90
-
getFeaturedArticles() // Filtered
91
-
getLatestCaseStudies() // Sorted & limited
92
-
```
93
-
94
-
**Conversion Script:**
95
-
```bash
96
-
npm run convert-content # Runs src/scripts/convertCsvToJson.ts
97
-
```
98
-
99
-
---
100
-
101
-
## Design System
102
-
103
-
### Color Palette
104
-
105
-
**Primary Colors** (from `tailwind.config.ts`):
106
-
- **Blue:** `#0000ff` (bones-blue) - Primary background
107
-
- **White:** `#ffffff` (bones-white) - Text & borders
108
-
- **Black:** `#000000` (bones-black) - Future text color
109
-
- **Yellow:** `bones-yellow` - Focus, highlights, text selection
110
-
111
-
**Neutral Palette:**
112
-
- bones-dimgray (#282828), bones-gainsboro (#DCDCDC)
113
-
- bones-gray (#808080), bones-ghostwhite (#F8F8FF)
114
-
- bones-linen (#FAF0E6), bones-floralwhite (#FFFAF0)
115
-
116
-
**Accent Colors:**
117
-
- Yellows: bones-gold, bones-goldenrod
118
-
- Blues: bones-aliceblue, bones-mediumblue, bones-midnightblue
119
-
- Reds: bones-red, bones-firebrick, bones-darkred
120
-
- Purples: bones-magenta, bones-rebeccapurple, bones-indigo
121
-
122
-
### Typography
123
-
124
-
**Fonts:**
125
-
- **Sans:** DM Sans - Body text
126
-
- **Serif:** DM Serif Display - Display text
127
-
128
-
**Fluid Type Plugin** (Custom Tailwind):
129
-
```css
130
-
/* Responsive scaling with clamp() */
131
-
.fluid-preset-h1 /* 28px โ 88px (360px โ 1200px viewport) */
132
-
.fluid-preset-h2 /* 22px โ 48px */
133
-
.fluid-preset-h3 /* 18px โ 32px */
134
-
.fluid-preset-body /* 16px โ 18px */
135
-
```
136
-
137
-
### Layout Principles
138
-
139
-
**Requirements:**
140
-
- โ
Use CSS Grid and Flexbox appropriately
141
-
- โ
NO margin - use padding and gap only
142
-
- โ
Scale text between breakpoints
143
-
- โ
Apply line-height to every text element
144
-
- โ
Tailwind utilities everywhere
145
-
146
-
---
147
-
148
-
## Current Styling Implementation
149
-
150
-
### Pattern 1: White borders on blue background
151
-
Used in: HomePage, AboutPage
152
-
- Background: `#0000ff`
153
-
- Text: White
154
-
- Borders: White `#ffffff`
155
-
- Links/Buttons: Various scales
156
-
157
-
### Pattern 2: White background (future)
158
-
- Background: White `#ffffff`
159
-
- Text: Black `#000000`
160
-
- Links: Blue `#0000ff`
161
-
162
-
### Theme System
163
-
Section component provides theme context:
164
-
- `mono`, `gray`, `yellow`, `blue`, `red`, `purple`
165
-
- Child components (Button, Heading, etc.) are theme-aware
166
-
167
-
---
168
-
169
-
## Git Workflow
170
-
171
-
**Current Branch:** `main`
172
-
**Status:** Clean working directory
173
-
174
-
**Branching Strategy:**
175
-
- โ ๏ธ **NEVER push to main without explicit approval**
176
-
- Create feature branches for all work
177
-
- Merge only when approved
178
-
179
-
**Recent Commits:**
180
-
```
181
-
47c702b Merge pull request #10
182
-
f241a4e update to prepare to merge
183
-
3fd813f GitButler Workspace Commit
184
-
9ae9db4 Merge pull request #9
185
-
```
186
-
187
-
---
188
-
189
-
## Immediate Tasks
190
-
191
-
### 1. โ
Project Documentation (this file)
192
-
193
-
### 2. Component Organization
194
-
- Split multi-component folders into individual component folders
195
-
- Add missing `.constants.ts` files where needed
196
-
- Standardize all components to 4-file system
197
-
198
-
### 3. Data Protection
199
-
- Verify `src/data/` structure integrity
200
-
- Document data dependencies
201
-
- Ensure conversion scripts work correctly
202
-
203
-
### 4. Styling Audit
204
-
- Review styling consistency across components
205
-
- Ensure no margin usage
206
-
- Verify fluid typography implementation
207
-
208
-
### 5. Page Development
209
-
- Fix WorksPage data integration
210
-
- Create Writing page (Bluesky integration)
211
-
- Create Links page
212
-
- Create Contact page
213
-
214
-
---
215
-
216
-
## Key File Locations
217
-
218
-
```
219
-
portfolio/
220
-
โโโ src/
221
-
โ โโโ components/ # 27 component directories
222
-
โ โโโ pages/ # HomePage, AboutPage, WorksPage
223
-
โ โโโ data/
224
-
โ โ โโโ json/ # Data files (protected)
225
-
โ โ โโโ csv/ # Source CSV files
226
-
โ โโโ lib/
227
-
โ โ โโโ data/ # getData.ts - data access layer
228
-
โ โ โโโ utils.ts # cn() utility
229
-
โ โโโ scripts/ # convertCsvToJson.ts
230
-
โ โโโ types/ # TypeScript definitions
231
-
โ โโโ main.tsx # Entry point with routing
232
-
โโโ plugins/ # fluidType.ts - custom Tailwind plugin
233
-
โโโ tailwind.config.ts # Design system configuration
234
-
โโโ vite.config.ts # Build configuration
235
-
```
236
-
237
-
---
238
-
239
-
## Notes
240
-
241
-
- **SEO:** All pages use Helmet with JSON-LD structured data
242
-
- **Routing:** Smart Link component handles internal/external URLs
243
-
- **Dark Mode:** Enabled via `next-themes` (class-based)
244
-
- **Icons:** Both @heroicons and lucide-react available
245
-
- **Code Quality:** ESLint + Prettier configured
246
-
- **No State Management:** Simple JSON data loading, no Redux/Zustand needed
+49
-471
package-lock.json
+49
-471
package-lock.json
···
8
8
"name": "portfolio",
9
9
"version": "0.0.0",
10
10
"dependencies": {
11
-
"@headlessui/react": "^2.1.8",
12
-
"@heroicons/react": "^2.1.5",
13
-
"@types/react-helmet-async": "^1.0.1",
14
-
"autoprefixer": "^10.4.20",
15
-
"clsx": "^2.1.1",
16
-
"lucide-react": "^0.446.0",
17
-
"next-themes": "^0.4.6",
18
-
"postcss": "^8.4.47",
19
-
"react": "^18.3.1",
20
-
"react-dom": "^18.3.1",
21
-
"react-helmet-async": "^2.0.5",
22
-
"react-router-dom": "^6.26.2",
23
-
"tailwind-merge": "^2.5.4",
24
-
"tailwindcss": "^3.4.13"
11
+
"@types/react-helmet-async": "latest",
12
+
"autoprefixer": "latest",
13
+
"clsx": "latest",
14
+
"lucide-react": "latest",
15
+
"postcss": "latest",
16
+
"react": "latest",
17
+
"react-dom": "latest",
18
+
"react-helmet-async": "latest",
19
+
"react-router-dom": "latest",
20
+
"tailwind-merge": "latest",
21
+
"tailwindcss": "latest"
25
22
},
26
23
"devDependencies": {
27
-
"@eslint/js": "^9.11.1",
28
-
"@tailwindcss/typography": "^0.5.16",
29
-
"@types/next": "^8.0.7",
30
-
"@types/node": "^22.9.0",
31
-
"@types/react": "^18.3.10",
32
-
"@types/react-dom": "^18.3.0",
33
-
"@types/react-router-dom": "^5.3.3",
34
-
"@typescript-eslint/eslint-plugin": "^8.8.0",
35
-
"@typescript-eslint/parser": "^8.8.0",
36
-
"@u3u/prettier-config": "^5.1.0",
37
-
"@vitejs/plugin-react": "^4.3.1",
38
-
"csv-parse": "^5.5.6",
39
-
"eslint": "^9.11.1",
40
-
"eslint-config-prettier": "^10.0.1",
41
-
"eslint-plugin-prettier": "^5.2.3",
42
-
"eslint-plugin-react": "^7.35.0",
43
-
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
44
-
"eslint-plugin-react-refresh": "^0.4.9",
45
-
"globals": "^15.9.0",
46
-
"prettier": "^3.5.2",
47
-
"tsx": "^4.19.2",
48
-
"typescript": "^5.6.2",
49
-
"vite": "^7.1.10"
24
+
"@eslint/js": "latest",
25
+
"@tailwindcss/typography": "latest",
26
+
"@types/node": "latest",
27
+
"@types/react": "latest",
28
+
"@types/react-dom": "latest",
29
+
"@types/react-router-dom": "latest",
30
+
"@typescript-eslint/eslint-plugin": "latest",
31
+
"@typescript-eslint/parser": "latest",
32
+
"@u3u/prettier-config": "latest",
33
+
"@vitejs/plugin-react": "latest",
34
+
"csv-parse": "latest",
35
+
"eslint": "latest",
36
+
"eslint-config-prettier": "latest",
37
+
"eslint-plugin-prettier": "latest",
38
+
"eslint-plugin-react": "latest",
39
+
"eslint-plugin-react-hooks": "latest",
40
+
"eslint-plugin-react-refresh": "latest",
41
+
"globals": "latest",
42
+
"prettier": "latest",
43
+
"tsx": "latest",
44
+
"typescript": "latest",
45
+
"vite": "latest"
50
46
}
51
47
},
52
48
"node_modules/@alloc/quick-lru": {
···
1119
1115
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1120
1116
}
1121
1117
},
1122
-
"node_modules/@floating-ui/core": {
1123
-
"version": "1.6.9",
1124
-
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
1125
-
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
1126
-
"license": "MIT",
1127
-
"dependencies": {
1128
-
"@floating-ui/utils": "^0.2.9"
1129
-
}
1130
-
},
1131
-
"node_modules/@floating-ui/dom": {
1132
-
"version": "1.6.13",
1133
-
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
1134
-
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
1135
-
"license": "MIT",
1136
-
"dependencies": {
1137
-
"@floating-ui/core": "^1.6.0",
1138
-
"@floating-ui/utils": "^0.2.9"
1139
-
}
1140
-
},
1141
-
"node_modules/@floating-ui/react": {
1142
-
"version": "0.26.28",
1143
-
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
1144
-
"integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
1145
-
"license": "MIT",
1146
-
"dependencies": {
1147
-
"@floating-ui/react-dom": "^2.1.2",
1148
-
"@floating-ui/utils": "^0.2.8",
1149
-
"tabbable": "^6.0.0"
1150
-
},
1151
-
"peerDependencies": {
1152
-
"react": ">=16.8.0",
1153
-
"react-dom": ">=16.8.0"
1154
-
}
1155
-
},
1156
-
"node_modules/@floating-ui/react-dom": {
1157
-
"version": "2.1.2",
1158
-
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
1159
-
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
1160
-
"license": "MIT",
1161
-
"dependencies": {
1162
-
"@floating-ui/dom": "^1.0.0"
1163
-
},
1164
-
"peerDependencies": {
1165
-
"react": ">=16.8.0",
1166
-
"react-dom": ">=16.8.0"
1167
-
}
1168
-
},
1169
-
"node_modules/@floating-ui/utils": {
1170
-
"version": "0.2.9",
1171
-
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
1172
-
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
1173
-
"license": "MIT"
1174
-
},
1175
-
"node_modules/@headlessui/react": {
1176
-
"version": "2.2.1",
1177
-
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.1.tgz",
1178
-
"integrity": "sha512-daiUqVLae8CKVjEVT19P/izW0aGK0GNhMSAeMlrDebKmoVZHcRRwbxzgtnEadUVDXyBsWo9/UH4KHeniO+0tMg==",
1179
-
"license": "MIT",
1180
-
"dependencies": {
1181
-
"@floating-ui/react": "^0.26.16",
1182
-
"@react-aria/focus": "^3.17.1",
1183
-
"@react-aria/interactions": "^3.21.3",
1184
-
"@tanstack/react-virtual": "^3.11.1"
1185
-
},
1186
-
"engines": {
1187
-
"node": ">=10"
1188
-
},
1189
-
"peerDependencies": {
1190
-
"react": "^18 || ^19 || ^19.0.0-rc",
1191
-
"react-dom": "^18 || ^19 || ^19.0.0-rc"
1192
-
}
1193
-
},
1194
-
"node_modules/@heroicons/react": {
1195
-
"version": "2.2.0",
1196
-
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
1197
-
"integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==",
1198
-
"license": "MIT",
1199
-
"peerDependencies": {
1200
-
"react": ">= 16 || ^19.0.0-rc"
1201
-
}
1202
-
},
1203
1118
"node_modules/@humanfs/core": {
1204
1119
"version": "0.19.1",
1205
1120
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
···
1453
1368
"dev": true,
1454
1369
"license": "Apache-2.0"
1455
1370
},
1456
-
"node_modules/@react-aria/focus": {
1457
-
"version": "3.20.1",
1458
-
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.1.tgz",
1459
-
"integrity": "sha512-lgYs+sQ1TtBrAXnAdRBQrBo0/7o5H6IrfDxec1j+VRpcXL0xyk0xPq+m3lZp8typzIghqDgpnKkJ5Jf4OrzPIw==",
1460
-
"license": "Apache-2.0",
1461
-
"dependencies": {
1462
-
"@react-aria/interactions": "^3.24.1",
1463
-
"@react-aria/utils": "^3.28.1",
1464
-
"@react-types/shared": "^3.28.0",
1465
-
"@swc/helpers": "^0.5.0",
1466
-
"clsx": "^2.0.0"
1467
-
},
1468
-
"peerDependencies": {
1469
-
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
1470
-
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
1471
-
}
1472
-
},
1473
-
"node_modules/@react-aria/interactions": {
1474
-
"version": "3.24.1",
1475
-
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.24.1.tgz",
1476
-
"integrity": "sha512-OWEcIC6UQfWq4Td5Ptuh4PZQ4LHLJr/JL2jGYvuNL6EgL3bWvzPrRYIF/R64YbfVxIC7FeZpPSkS07sZ93/NoA==",
1477
-
"license": "Apache-2.0",
1478
-
"dependencies": {
1479
-
"@react-aria/ssr": "^3.9.7",
1480
-
"@react-aria/utils": "^3.28.1",
1481
-
"@react-stately/flags": "^3.1.0",
1482
-
"@react-types/shared": "^3.28.0",
1483
-
"@swc/helpers": "^0.5.0"
1484
-
},
1485
-
"peerDependencies": {
1486
-
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
1487
-
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
1488
-
}
1489
-
},
1490
-
"node_modules/@react-aria/ssr": {
1491
-
"version": "3.9.7",
1492
-
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz",
1493
-
"integrity": "sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==",
1494
-
"license": "Apache-2.0",
1495
-
"dependencies": {
1496
-
"@swc/helpers": "^0.5.0"
1497
-
},
1498
-
"engines": {
1499
-
"node": ">= 12"
1500
-
},
1501
-
"peerDependencies": {
1502
-
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
1503
-
}
1504
-
},
1505
-
"node_modules/@react-aria/utils": {
1506
-
"version": "3.28.1",
1507
-
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.28.1.tgz",
1508
-
"integrity": "sha512-mnHFF4YOVu9BRFQ1SZSKfPhg3z+lBRYoW5mLcYTQihbKhz48+I1sqRkP7ahMITr8ANH3nb34YaMME4XWmK2Mgg==",
1509
-
"license": "Apache-2.0",
1510
-
"dependencies": {
1511
-
"@react-aria/ssr": "^3.9.7",
1512
-
"@react-stately/flags": "^3.1.0",
1513
-
"@react-stately/utils": "^3.10.5",
1514
-
"@react-types/shared": "^3.28.0",
1515
-
"@swc/helpers": "^0.5.0",
1516
-
"clsx": "^2.0.0"
1517
-
},
1518
-
"peerDependencies": {
1519
-
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
1520
-
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
1521
-
}
1522
-
},
1523
-
"node_modules/@react-stately/flags": {
1524
-
"version": "3.1.0",
1525
-
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.0.tgz",
1526
-
"integrity": "sha512-KSHOCxTFpBtxhIRcKwsD1YDTaNxFtCYuAUb0KEihc16QwqZViq4hasgPBs2gYm7fHRbw7WYzWKf6ZSo/+YsFlg==",
1527
-
"license": "Apache-2.0",
1528
-
"dependencies": {
1529
-
"@swc/helpers": "^0.5.0"
1530
-
}
1531
-
},
1532
-
"node_modules/@react-stately/utils": {
1533
-
"version": "3.10.5",
1534
-
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.5.tgz",
1535
-
"integrity": "sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==",
1536
-
"license": "Apache-2.0",
1537
-
"dependencies": {
1538
-
"@swc/helpers": "^0.5.0"
1539
-
},
1540
-
"peerDependencies": {
1541
-
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
1542
-
}
1543
-
},
1544
-
"node_modules/@react-types/shared": {
1545
-
"version": "3.28.0",
1546
-
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.28.0.tgz",
1547
-
"integrity": "sha512-9oMEYIDc3sk0G5rysnYvdNrkSg7B04yTKl50HHSZVbokeHpnU0yRmsDaWb9B/5RprcKj8XszEk5guBO8Sa/Q+Q==",
1548
-
"license": "Apache-2.0",
1549
-
"peerDependencies": {
1550
-
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
1551
-
}
1552
-
},
1553
1371
"node_modules/@remix-run/router": {
1554
1372
"version": "1.23.0",
1555
1373
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
···
1912
1730
"node": ">= 14.0.0"
1913
1731
}
1914
1732
},
1915
-
"node_modules/@swc/helpers": {
1916
-
"version": "0.5.15",
1917
-
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
1918
-
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
1919
-
"license": "Apache-2.0",
1920
-
"dependencies": {
1921
-
"tslib": "^2.8.0"
1922
-
}
1923
-
},
1924
1733
"node_modules/@tailwindcss/typography": {
1925
1734
"version": "0.5.16",
1926
1735
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
···
1951
1760
"node": ">=4"
1952
1761
}
1953
1762
},
1954
-
"node_modules/@tanstack/react-virtual": {
1955
-
"version": "3.13.6",
1956
-
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.6.tgz",
1957
-
"integrity": "sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA==",
1958
-
"license": "MIT",
1959
-
"dependencies": {
1960
-
"@tanstack/virtual-core": "3.13.6"
1961
-
},
1962
-
"funding": {
1963
-
"type": "github",
1964
-
"url": "https://github.com/sponsors/tannerlinsley"
1965
-
},
1966
-
"peerDependencies": {
1967
-
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
1968
-
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
1969
-
}
1970
-
},
1971
-
"node_modules/@tanstack/virtual-core": {
1972
-
"version": "3.13.6",
1973
-
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.6.tgz",
1974
-
"integrity": "sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg==",
1975
-
"license": "MIT",
1976
-
"funding": {
1977
-
"type": "github",
1978
-
"url": "https://github.com/sponsors/tannerlinsley"
1979
-
}
1980
-
},
1981
1763
"node_modules/@types/babel__core": {
1982
1764
"version": "7.20.5",
1983
1765
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
···
2078
1860
"dev": true,
2079
1861
"license": "MIT"
2080
1862
},
2081
-
"node_modules/@types/next": {
2082
-
"version": "8.0.7",
2083
-
"resolved": "https://registry.npmjs.org/@types/next/-/next-8.0.7.tgz",
2084
-
"integrity": "sha512-I/Gcj1YfOFmpBBX5XgBP1t1wKcFS0TGk8ytW99ujjvCp8U31QuKqM3fvvGb7+Hf1CJt3BAAgzGT0aCigqO5opQ==",
2085
-
"dev": true,
2086
-
"license": "MIT",
2087
-
"dependencies": {
2088
-
"@types/next-server": "*",
2089
-
"@types/node": "*",
2090
-
"@types/node-fetch": "*",
2091
-
"@types/react": "*"
2092
-
}
2093
-
},
2094
-
"node_modules/@types/next-server": {
2095
-
"version": "8.1.2",
2096
-
"resolved": "https://registry.npmjs.org/@types/next-server/-/next-server-8.1.2.tgz",
2097
-
"integrity": "sha512-Fm4QhAxwDlC9AHiGy23Lhv7DeTTt1O1s7tnAsyVOLPjePmYXPZVbOCrxd2oRHZnIIYWw41JelLbq4hN1B5idlQ==",
2098
-
"dev": true,
2099
-
"license": "MIT",
2100
-
"dependencies": {
2101
-
"@types/next": "*",
2102
-
"@types/node": "*",
2103
-
"@types/react": "*",
2104
-
"@types/react-loadable": "*"
2105
-
}
2106
-
},
2107
1863
"node_modules/@types/node": {
2108
1864
"version": "22.14.0",
2109
1865
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz",
···
2112
1868
"license": "MIT",
2113
1869
"dependencies": {
2114
1870
"undici-types": "~6.21.0"
2115
-
}
2116
-
},
2117
-
"node_modules/@types/node-fetch": {
2118
-
"version": "2.6.13",
2119
-
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
2120
-
"integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
2121
-
"dev": true,
2122
-
"license": "MIT",
2123
-
"dependencies": {
2124
-
"@types/node": "*",
2125
-
"form-data": "^4.0.4"
2126
1871
}
2127
1872
},
2128
1873
"node_modules/@types/prop-types": {
···
2170
1915
"@types/react-helmet": "*"
2171
1916
}
2172
1917
},
2173
-
"node_modules/@types/react-loadable": {
2174
-
"version": "5.5.11",
2175
-
"resolved": "https://registry.npmjs.org/@types/react-loadable/-/react-loadable-5.5.11.tgz",
2176
-
"integrity": "sha512-/tq2IJ853MoIFRBmqVOxnGsRRjER5TmEKzsZtaAkiXAWoDeKgR/QNOT1vd9k0p9h/F616X21cpNh3hu4RutzRQ==",
2177
-
"dev": true,
2178
-
"license": "MIT",
2179
-
"dependencies": {
2180
-
"@types/react": "*",
2181
-
"@types/webpack": "^4"
2182
-
}
2183
-
},
2184
1918
"node_modules/@types/react-router": {
2185
1919
"version": "5.1.20",
2186
1920
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
···
2204
1938
"@types/react-router": "*"
2205
1939
}
2206
1940
},
2207
-
"node_modules/@types/source-list-map": {
2208
-
"version": "0.1.6",
2209
-
"resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.6.tgz",
2210
-
"integrity": "sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g==",
2211
-
"dev": true,
2212
-
"license": "MIT"
2213
-
},
2214
-
"node_modules/@types/tapable": {
2215
-
"version": "1.0.12",
2216
-
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.12.tgz",
2217
-
"integrity": "sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q==",
2218
-
"dev": true,
2219
-
"license": "MIT"
2220
-
},
2221
-
"node_modules/@types/uglify-js": {
2222
-
"version": "3.17.5",
2223
-
"resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.5.tgz",
2224
-
"integrity": "sha512-TU+fZFBTBcXj/GpDpDaBmgWk/gn96kMZ+uocaFUlV2f8a6WdMzzI44QBCmGcCiYR0Y6ZlNRiyUyKKt5nl/lbzQ==",
2225
-
"dev": true,
2226
-
"license": "MIT",
2227
-
"dependencies": {
2228
-
"source-map": "^0.6.1"
2229
-
}
2230
-
},
2231
1941
"node_modules/@types/unist": {
2232
1942
"version": "3.0.3",
2233
1943
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
···
2235
1945
"dev": true,
2236
1946
"license": "MIT"
2237
1947
},
2238
-
"node_modules/@types/webpack": {
2239
-
"version": "4.41.40",
2240
-
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.40.tgz",
2241
-
"integrity": "sha512-u6kMFSBM9HcoTpUXnL6mt2HSzftqb3JgYV6oxIgL2dl6sX6aCa5k6SOkzv5DuZjBTPUE/dJltKtwwuqrkZHpfw==",
2242
-
"dev": true,
2243
-
"license": "MIT",
2244
-
"dependencies": {
2245
-
"@types/node": "*",
2246
-
"@types/tapable": "^1",
2247
-
"@types/uglify-js": "*",
2248
-
"@types/webpack-sources": "*",
2249
-
"anymatch": "^3.0.0",
2250
-
"source-map": "^0.6.0"
2251
-
}
2252
-
},
2253
-
"node_modules/@types/webpack-sources": {
2254
-
"version": "3.2.3",
2255
-
"resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.3.tgz",
2256
-
"integrity": "sha512-4nZOdMwSPHZ4pTEZzSp0AsTM4K7Qmu40UKW4tJDiOVs20UzYF9l+qUe4s0ftfN0pin06n+5cWWDJXH+sbhAiDw==",
2257
-
"dev": true,
2258
-
"license": "MIT",
2259
-
"dependencies": {
2260
-
"@types/node": "*",
2261
-
"@types/source-list-map": "*",
2262
-
"source-map": "^0.7.3"
2263
-
}
2264
-
},
2265
-
"node_modules/@types/webpack-sources/node_modules/source-map": {
2266
-
"version": "0.7.6",
2267
-
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
2268
-
"integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
2269
-
"dev": true,
2270
-
"license": "BSD-3-Clause",
2271
-
"engines": {
2272
-
"node": ">= 12"
2273
-
}
2274
-
},
2275
1948
"node_modules/@typescript-eslint/eslint-plugin": {
2276
1949
"version": "8.29.0",
2277
1950
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.0.tgz",
···
2799
2472
"node": ">= 0.4"
2800
2473
}
2801
2474
},
2802
-
"node_modules/asynckit": {
2803
-
"version": "0.4.0",
2804
-
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
2805
-
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
2806
-
"dev": true,
2807
-
"license": "MIT"
2808
-
},
2809
2475
"node_modules/autoprefixer": {
2810
2476
"version": "10.4.21",
2811
2477
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
···
3275
2941
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
3276
2942
"license": "MIT"
3277
2943
},
3278
-
"node_modules/combined-stream": {
3279
-
"version": "1.0.8",
3280
-
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
3281
-
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
3282
-
"dev": true,
3283
-
"license": "MIT",
3284
-
"dependencies": {
3285
-
"delayed-stream": "~1.0.0"
3286
-
},
3287
-
"engines": {
3288
-
"node": ">= 0.8"
3289
-
}
3290
-
},
3291
2944
"node_modules/commander": {
3292
2945
"version": "10.0.1",
3293
2946
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
···
3554
3207
},
3555
3208
"funding": {
3556
3209
"url": "https://github.com/sponsors/ljharb"
3557
-
}
3558
-
},
3559
-
"node_modules/delayed-stream": {
3560
-
"version": "1.0.0",
3561
-
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
3562
-
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
3563
-
"dev": true,
3564
-
"license": "MIT",
3565
-
"engines": {
3566
-
"node": ">=0.4.0"
3567
3210
}
3568
3211
},
3569
3212
"node_modules/dequal": {
···
4523
4166
"url": "https://github.com/sponsors/isaacs"
4524
4167
}
4525
4168
},
4526
-
"node_modules/form-data": {
4527
-
"version": "4.0.4",
4528
-
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
4529
-
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
4530
-
"dev": true,
4531
-
"license": "MIT",
4532
-
"dependencies": {
4533
-
"asynckit": "^0.4.0",
4534
-
"combined-stream": "^1.0.8",
4535
-
"es-set-tostringtag": "^2.1.0",
4536
-
"hasown": "^2.0.2",
4537
-
"mime-types": "^2.1.12"
4538
-
},
4539
-
"engines": {
4540
-
"node": ">= 6"
4541
-
}
4542
-
},
4543
4169
"node_modules/fraction.js": {
4544
4170
"version": "4.3.7",
4545
4171
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
···
6252
5878
"node": ">=8.6"
6253
5879
}
6254
5880
},
6255
-
"node_modules/mime-db": {
6256
-
"version": "1.52.0",
6257
-
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
6258
-
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
6259
-
"dev": true,
6260
-
"license": "MIT",
6261
-
"engines": {
6262
-
"node": ">= 0.6"
6263
-
}
6264
-
},
6265
-
"node_modules/mime-types": {
6266
-
"version": "2.1.35",
6267
-
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
6268
-
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
6269
-
"dev": true,
6270
-
"license": "MIT",
6271
-
"dependencies": {
6272
-
"mime-db": "1.52.0"
6273
-
},
6274
-
"engines": {
6275
-
"node": ">= 0.6"
6276
-
}
6277
-
},
6278
5881
"node_modules/minimatch": {
6279
5882
"version": "9.0.5",
6280
5883
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
···
6398
6001
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
6399
6002
"dev": true,
6400
6003
"license": "MIT"
6401
-
},
6402
-
"node_modules/next-themes": {
6403
-
"version": "0.4.6",
6404
-
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
6405
-
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
6406
-
"license": "MIT",
6407
-
"peerDependencies": {
6408
-
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
6409
-
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
6410
-
}
6411
6004
},
6412
6005
"node_modules/node-releases": {
6413
6006
"version": "2.0.19",
···
8059
7652
"node": ">=12.20"
8060
7653
}
8061
7654
},
8062
-
"node_modules/source-map": {
8063
-
"version": "0.6.1",
8064
-
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
8065
-
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
8066
-
"dev": true,
8067
-
"license": "BSD-3-Clause",
8068
-
"engines": {
8069
-
"node": ">=0.10.0"
8070
-
}
8071
-
},
8072
7655
"node_modules/source-map-js": {
8073
7656
"version": "1.2.1",
8074
7657
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
···
8402
7985
"url": "https://opencollective.com/synckit"
8403
7986
}
8404
7987
},
8405
-
"node_modules/tabbable": {
8406
-
"version": "6.2.0",
8407
-
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
8408
-
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
8409
-
"license": "MIT"
8410
-
},
8411
7988
"node_modules/tailwind-merge": {
8412
7989
"version": "2.6.0",
8413
7990
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
···
8579
8156
"version": "2.8.1",
8580
8157
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
8581
8158
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
8159
+
"dev": true,
8582
8160
"license": "0BSD"
8583
8161
},
8584
8162
"node_modules/tsx": {
···
8848
8426
"license": "MIT"
8849
8427
},
8850
8428
"node_modules/vite": {
8851
-
"version": "7.1.10",
8852
-
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.10.tgz",
8853
-
"integrity": "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==",
8429
+
"version": "6.4.1",
8430
+
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
8431
+
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
8854
8432
"dev": true,
8855
8433
"license": "MIT",
8856
8434
"dependencies": {
8857
8435
"esbuild": "^0.25.0",
8858
-
"fdir": "^6.5.0",
8859
-
"picomatch": "^4.0.3",
8860
-
"postcss": "^8.5.6",
8861
-
"rollup": "^4.43.0",
8862
-
"tinyglobby": "^0.2.15"
8436
+
"fdir": "^6.4.4",
8437
+
"picomatch": "^4.0.2",
8438
+
"postcss": "^8.5.3",
8439
+
"rollup": "^4.34.9",
8440
+
"tinyglobby": "^0.2.13"
8863
8441
},
8864
8442
"bin": {
8865
8443
"vite": "bin/vite.js"
8866
8444
},
8867
8445
"engines": {
8868
-
"node": "^20.19.0 || >=22.12.0"
8446
+
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
8869
8447
},
8870
8448
"funding": {
8871
8449
"url": "https://github.com/vitejs/vite?sponsor=1"
···
8874
8452
"fsevents": "~2.3.3"
8875
8453
},
8876
8454
"peerDependencies": {
8877
-
"@types/node": "^20.19.0 || >=22.12.0",
8455
+
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
8878
8456
"jiti": ">=1.21.0",
8879
-
"less": "^4.0.0",
8457
+
"less": "*",
8880
8458
"lightningcss": "^1.21.0",
8881
-
"sass": "^1.70.0",
8882
-
"sass-embedded": "^1.70.0",
8883
-
"stylus": ">=0.54.8",
8884
-
"sugarss": "^5.0.0",
8459
+
"sass": "*",
8460
+
"sass-embedded": "*",
8461
+
"stylus": "*",
8462
+
"sugarss": "*",
8885
8463
"terser": "^5.16.0",
8886
8464
"tsx": "^4.8.1",
8887
8465
"yaml": "^2.4.2"
+33
-37
package.json
+33
-37
package.json
···
12
12
"preview": "vite preview"
13
13
},
14
14
"dependencies": {
15
-
"@headlessui/react": "^2.1.8",
16
-
"@heroicons/react": "^2.1.5",
17
-
"@types/react-helmet-async": "^1.0.1",
18
-
"autoprefixer": "^10.4.20",
19
-
"clsx": "^2.1.1",
20
-
"lucide-react": "^0.446.0",
21
-
"next-themes": "^0.4.6",
22
-
"postcss": "^8.4.47",
23
-
"react": "^18.3.1",
24
-
"react-dom": "^18.3.1",
25
-
"react-helmet-async": "^2.0.5",
26
-
"react-router-dom": "^6.26.2",
27
-
"tailwind-merge": "^2.5.4",
28
-
"tailwindcss": "^3.4.13"
15
+
"@types/react-helmet-async": "latest",
16
+
"autoprefixer": "latest",
17
+
"clsx": "latest",
18
+
"lucide-react": "latest",
19
+
"postcss": "latest",
20
+
"react": "latest",
21
+
"react-dom": "latest",
22
+
"react-helmet-async": "latest",
23
+
"react-router-dom": "latest",
24
+
"tailwind-merge": "latest",
25
+
"tailwindcss": "latest"
29
26
},
30
27
"devDependencies": {
31
-
"@eslint/js": "^9.11.1",
32
-
"@tailwindcss/typography": "^0.5.16",
33
-
"@types/next": "^8.0.7",
34
-
"@types/node": "^22.9.0",
35
-
"@types/react": "^18.3.10",
36
-
"@types/react-dom": "^18.3.0",
37
-
"@types/react-router-dom": "^5.3.3",
38
-
"@typescript-eslint/eslint-plugin": "^8.8.0",
39
-
"@typescript-eslint/parser": "^8.8.0",
40
-
"@u3u/prettier-config": "^5.1.0",
41
-
"@vitejs/plugin-react": "^4.3.1",
42
-
"csv-parse": "^5.5.6",
43
-
"eslint": "^9.11.1",
44
-
"eslint-config-prettier": "^10.0.1",
45
-
"eslint-plugin-prettier": "^5.2.3",
46
-
"eslint-plugin-react": "^7.35.0",
47
-
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
48
-
"eslint-plugin-react-refresh": "^0.4.9",
49
-
"globals": "^15.9.0",
50
-
"prettier": "^3.5.2",
51
-
"tsx": "^4.19.2",
52
-
"typescript": "^5.6.2",
53
-
"vite": "^7.1.10"
28
+
"@eslint/js": "latest",
29
+
"@tailwindcss/typography": "latest",
30
+
"@types/node": "latest",
31
+
"@types/react": "latest",
32
+
"@types/react-dom": "latest",
33
+
"@types/react-router-dom": "latest",
34
+
"@typescript-eslint/eslint-plugin": "latest",
35
+
"@typescript-eslint/parser": "latest",
36
+
"@u3u/prettier-config": "latest",
37
+
"@vitejs/plugin-react": "latest",
38
+
"csv-parse": "latest",
39
+
"eslint": "latest",
40
+
"eslint-config-prettier": "latest",
41
+
"eslint-plugin-prettier": "latest",
42
+
"eslint-plugin-react": "latest",
43
+
"eslint-plugin-react-hooks": "latest",
44
+
"eslint-plugin-react-refresh": "latest",
45
+
"globals": "latest",
46
+
"prettier": "latest",
47
+
"tsx": "latest",
48
+
"typescript": "latest",
49
+
"vite": "latest"
54
50
}
55
51
}
+35
-14
plugins/fluidType.ts
+35
-14
plugins/fluidType.ts
···
15
15
* - Guards with clamp() to cap at the ends.
16
16
*/
17
17
export default function fluidType() {
18
-
return plugin(function ({ matchUtilities, theme }) {
18
+
return plugin(function ({ matchUtilities }) {
19
19
const toNum = (s: string) => Number(s.trim());
20
20
21
21
const parse = (raw: string) => {
···
76
76
{ values: {}, type: 'any' },
77
77
);
78
78
79
-
// Optional presets for common typography scales
79
+
// Typography scale presets
80
+
// Format: [minPx, maxPx, viewportMinPx, viewportMaxPx]
81
+
// Scales from mobile (360px) to desktop (1200px) viewports
80
82
const fsPresets: Record<string, [number, number, number, number]> = {
81
-
h1: [
82
-
28,
83
-
88,
83
+
// sm: Small text (captions, footnotes, labels)
84
+
sm: [
85
+
14,
86
+
16,
87
+
360,
88
+
1200,
89
+
],
90
+
// base: default text (body copy, paragraphs)
91
+
base: [
92
+
16,
93
+
32,
94
+
360,
95
+
1200,
96
+
],
97
+
// md: Medium text (body copy, paragraphs)
98
+
md: [
99
+
24,
100
+
40,
84
101
360,
85
102
1200,
86
103
],
87
-
h2: [
88
-
22,
104
+
// lg: Large text (subheadings, lead paragraphs)
105
+
lg: [
106
+
32,
89
107
48,
90
108
360,
91
109
1200,
92
110
],
93
-
h3: [
94
-
18,
95
-
32,
111
+
// xl: Extra large text (headings)
112
+
xl: [
113
+
40,
114
+
56,
96
115
360,
97
116
1200,
98
117
],
99
-
body: [
100
-
16,
101
-
18,
118
+
// 2xl: Display text (hero headings, page titles)
119
+
'2xl': [
120
+
48,
121
+
64,
102
122
360,
103
123
1200,
104
124
],
125
+
105
126
};
106
127
107
128
matchUtilities(
108
129
{
109
130
'fluid-preset': (key: string) => {
110
131
const p = fsPresets[key];
111
-
if (!p) return {};
132
+
if (!p) return null;
112
133
const [
113
134
min,
114
135
max,
+3
-1
src/App.tsx
+3
-1
src/App.tsx
···
1
1
import AboutPage from '@/pages/AboutPage';
2
2
import HomePage from '@/pages/HomePage';
3
-
import WorksPage from '@/pages/WorksPage';
3
+
import WorksPage from '@/pages/PortfolioPage';
4
+
import WorkPage from '@/pages/WorkPage';
4
5
import WritingPage from '@/pages/WritingPage';
5
6
import React, { useEffect } from 'react';
6
7
import { Route, Routes } from 'react-router-dom';
···
37
38
<Routes>
38
39
<Route path="/" element={<HomePage />} />
39
40
<Route path="/about" element={<AboutPage />} />
41
+
<Route path="/work" element={<WorkPage />} />
40
42
<Route path="/works" element={<WorksPage />} />
41
43
<Route path="/writing" element={<WritingPage />} />
42
44
{/* <Route path="/studies" element={<Studies />} /> */}
+10
src/components/Caption/Caption.styles.ts
+10
src/components/Caption/Caption.styles.ts
···
1
+
/**
2
+
* Caption Styles
3
+
*
4
+
* Uses the global typography system with fluid type scaling.
5
+
* Captions are always small text (sm size) with normal weight.
6
+
* Used for image captions, figure descriptions, etc.
7
+
*/
8
+
export const captionStyles = {
9
+
base: 'caption', // Uses the .caption preset from global CSS
10
+
} as const;
+16
src/components/Caption/Caption.tsx
+16
src/components/Caption/Caption.tsx
···
1
+
import { cn } from '@/lib/utils';
2
+
import React from 'react';
3
+
import { captionStyles } from './Caption.styles';
4
+
import { CaptionProps } from './Caption.types';
5
+
6
+
export const Caption = React.forwardRef<HTMLElement, CaptionProps>(
7
+
({ children, className = '', ...props }, ref) => {
8
+
return (
9
+
<figcaption ref={ref} className={cn(captionStyles.base, className)} {...props}>
10
+
{children}
11
+
</figcaption>
12
+
);
13
+
},
14
+
);
15
+
16
+
Caption.displayName = 'Caption';
+6
src/components/Caption/Caption.types.ts
+6
src/components/Caption/Caption.types.ts
+2
src/components/Caption/index.ts
+2
src/components/Caption/index.ts
+1
-1
src/components/CardArticle/CardArticle.styles.ts
+1
-1
src/components/CardArticle/CardArticle.styles.ts
···
3
3
export const coverImage = 'object-cover w-full h-full transition-transform group-hover:scale-105';
4
4
export const coverImageContainer = 'relative aspect-[16/9] w-full overflow-hidden bg-neutral-100 dark:bg-neutral-700';
5
5
export const date = 'text-neutral-600 dark:text-neutral-400';
6
-
export const detailContainer = 'flex flex-col gap-1';
6
+
export const detailContainer = 'flex flex-col gap-4';
7
7
export const metaContainer = 'flex items-center justify-between';
8
8
export const publicationContainer = 'flex items-center gap-2';
9
9
export const publicationIcon = 'h-8 w-8 object-contain';
+9
-9
src/components/CardArticle/CardArticle.tsx
+9
-9
src/components/CardArticle/CardArticle.tsx
···
19
19
20
20
{/* Content */}
21
21
<div className={styles.contentContainer}>
22
+
<div className={styles.detailContainer}>
23
+
{/* Title */}
24
+
<Heading level={3} size="md">
25
+
{article.title}
26
+
</Heading>
27
+
28
+
{/* Description */}
29
+
{article.subtitle && <Paragraph size="base">{article.subtitle}</Paragraph>}
30
+
</div>
22
31
{/* Publication Name (left) and Date (right) */}
23
32
<div className={styles.metaContainer}>
24
33
<div className={styles.publicationContainer}>
···
26
35
<span className={styles.publicationName}>{article.publication}</span>
27
36
</div>
28
37
<span className={styles.date}>{formattedDate}</span>
29
-
</div>
30
-
<div className={styles.detailContainer}>
31
-
{/* Title */}
32
-
<Heading level={3} style="title">
33
-
{article.title}
34
-
</Heading>
35
-
36
-
{/* Description */}
37
-
{article.subtitle && <Paragraph>{article.subtitle}</Paragraph>}
38
38
</div>
39
39
</div>
40
40
</a>
+6
src/components/CardEvent/CardEvent.styles.ts
+6
src/components/CardEvent/CardEvent.styles.ts
···
1
+
export const cardEventStyles = {
2
+
wrapper: 'px-8 py-12 bg-bones-white dark:bg-bones-black flex flex-col gap-8',
3
+
title: 'text-2xl font-bold font-dm-sans text-neutral-900',
4
+
subtitle: 'text-lg font-normal font-dm-sans text-neutral-700 italic mt-2',
5
+
meta: 'text-sm font-medium font-dm-sans text-neutral-500 uppercase mt-4',
6
+
} as const;
+32
src/components/CardEvent/CardEvent.tsx
+32
src/components/CardEvent/CardEvent.tsx
···
1
+
import { Heading } from '@/components/Heading/Heading';
2
+
import { Paragraph } from '@/components/Paragraph/Paragraph';
3
+
import { cn } from '@/lib/utils';
4
+
import React from 'react';
5
+
import { cardEventStyles } from './CardEvent.styles';
6
+
import { CardEventProps } from './CardEvent.types';
7
+
8
+
export const CardEvent: React.FC<CardEventProps> = ({
9
+
eventType,
10
+
eventTitle,
11
+
eventDescription,
12
+
eventStartYear,
13
+
eventEndYear,
14
+
eventAffiliation,
15
+
className = '',
16
+
}) => (
17
+
<div className={cn(cardEventStyles.wrapper, className)}>
18
+
<div className="flex items-center justify-between">
19
+
<Paragraph size="sm">{eventAffiliation}</Paragraph>
20
+
<Paragraph size="sm">
21
+
{eventStartYear} – {eventEndYear}
22
+
</Paragraph>
23
+
</div>
24
+
<div className="flex flex-col gap-2">
25
+
<Heading level={3} size="lg">
26
+
{eventTitle}
27
+
</Heading>
28
+
<Paragraph size="md">{eventDescription}</Paragraph>
29
+
</div>
30
+
<Paragraph size="sm">{eventType}</Paragraph>
31
+
</div>
32
+
);
+9
src/components/CardEvent/CardEvent.types.ts
+9
src/components/CardEvent/CardEvent.types.ts
+3
src/components/CardFact/CardFact.styles.ts
+3
src/components/CardFact/CardFact.styles.ts
+15
src/components/CardFact/CardFact.tsx
+15
src/components/CardFact/CardFact.tsx
···
1
+
import { Heading } from '@/components/Heading/Heading';
2
+
import { Paragraph } from '@/components/Paragraph/Paragraph';
3
+
import { cn } from '@/lib/utils';
4
+
import React from 'react';
5
+
import { cardFactStyles } from './CardFact.styles';
6
+
import { CardFactProps } from './CardFact.types';
7
+
8
+
export const CardFact: React.FC<CardFactProps> = ({ title, subtitle, className = '' }) => (
9
+
<div className={cn(cardFactStyles.wrapper, className)}>
10
+
<Heading level={3} size="lg">
11
+
{title}
12
+
</Heading>
13
+
<Paragraph size="md">{subtitle}</Paragraph>
14
+
</div>
15
+
);
+5
src/components/CardFact/CardFact.types.ts
+5
src/components/CardFact/CardFact.types.ts
+1
-1
src/components/CardNote/CardNote.tsx
+1
-1
src/components/CardNote/CardNote.tsx
+9
-1
src/components/CardRole/CardRole.styles.ts
+9
-1
src/components/CardRole/CardRole.styles.ts
···
1
-
// No custom styles needed - using Card component defaults
1
+
export const cardWrapper =
2
+
'group relative flex flex-col h-full bg-bones-white dark:bg-bones-black text-left w-full hover:bg-bones-white-10 dark:hover:bg-bones-black-10 transition-colors';
3
+
export const contentContainer = 'flex flex-col flex-grow gap-4 p-8 justify-between';
4
+
export const coverImage = 'object-cover w-full h-full transition-transform group-hover:scale-105';
5
+
export const coverImageContainer = 'relative aspect-[16/9] w-full overflow-hidden bg-neutral-100 dark:bg-neutral-700';
6
+
export const date = 'text-neutral-600 dark:text-neutral-400';
7
+
export const detailContainer = 'flex flex-col gap-4';
8
+
export const metaContainer = 'flex items-center justify-between';
9
+
export const companyName = 'font-medium text-neutral-900 dark:text-neutral-100';
+38
-13
src/components/CardRole/CardRole.tsx
+38
-13
src/components/CardRole/CardRole.tsx
···
1
-
import { CardNote } from '@/components/CardNote/CardNote';
1
+
import { Heading } from '@/components/Heading/Heading';
2
+
import { Paragraph } from '@/components/Paragraph/Paragraph';
2
3
import React from 'react';
3
-
import { useNavigate } from 'react-router-dom';
4
-
import { ROLES_BASE_PATH } from './CardRole.constants';
4
+
import * as styles from './CardRole.styles';
5
5
import { CardRoleProps } from './CardRole.types';
6
6
7
+
/**
8
+
* Format date string to readable format
9
+
*/
10
+
function formatDate(dateString: string): string {
11
+
const date = new Date(dateString);
12
+
if (isNaN(date.getTime())) return dateString;
13
+
14
+
return date.toLocaleDateString('en-US', {
15
+
year: 'numeric',
16
+
month: 'short',
17
+
});
18
+
}
19
+
7
20
export const CardRole: React.FC<CardRoleProps> = ({ role }) => {
8
-
const navigate = useNavigate();
21
+
const startDate = formatDate(role.startDate);
22
+
const endDate = role.endDate ? formatDate(role.endDate) : 'Present';
23
+
const dateRange = `${startDate} โ ${endDate}`;
9
24
10
25
return (
11
-
<CardNote
12
-
coverImage={role.coverImage}
13
-
meta={{
14
-
date: role.date,
15
-
}}
16
-
title={role.title}
17
-
description={`${role.company} | ${role.subtitle}`}
18
-
onClick={() => navigate(`${ROLES_BASE_PATH}/${role.slug}`)}
19
-
/>
26
+
<div className={styles.cardWrapper}>
27
+
{/* Content */}
28
+
<div className={styles.contentContainer}>
29
+
{/* Company Name (left) and Date (right) */}
30
+
<div className={styles.metaContainer}>
31
+
<span className={styles.companyName}>{role.company}</span>
32
+
<span className={styles.date}>{dateRange}</span>
33
+
</div>
34
+
<div className={styles.detailContainer}>
35
+
{/* Position Title */}
36
+
<Heading level={3} size="md">
37
+
{role.position}
38
+
</Heading>
39
+
40
+
{/* Description */}
41
+
{role.description && <Paragraph size="base">{role.description}</Paragraph>}
42
+
</div>
43
+
</div>
44
+
</div>
20
45
);
21
46
};
+3
-8
src/components/CardRole/CardRole.types.ts
+3
-8
src/components/CardRole/CardRole.types.ts
+14
src/components/Code/Code.styles.ts
+14
src/components/Code/Code.styles.ts
···
1
+
/**
2
+
* Code Styles
3
+
*
4
+
* Uses the global typography system with fluid type scaling.
5
+
* All code uses DM Mono font (monospace).
6
+
* Supports inline and block variants.
7
+
*/
8
+
export const codeStyles = {
9
+
base: 'font-normal font-dm-mono',
10
+
variants: {
11
+
inline: 'code-inline px-1.5 py-0.5 rounded bg-bones-black-5 dark:bg-bones-white-10',
12
+
block: 'code-block block p-4 rounded-lg bg-bones-black-5 dark:bg-bones-white-10 overflow-x-auto',
13
+
},
14
+
} as const;
+28
src/components/Code/Code.tsx
+28
src/components/Code/Code.tsx
···
1
+
import { cn } from '@/lib/utils';
2
+
import React from 'react';
3
+
import { codeStyles } from './Code.styles';
4
+
import { CodeProps } from './Code.types';
5
+
6
+
export const Code = React.forwardRef<HTMLElement, CodeProps>(
7
+
({ children, variant = 'inline', language, className = '', ...props }, ref) => {
8
+
const codeElement = (
9
+
<code
10
+
ref={ref}
11
+
className={cn(codeStyles.base, codeStyles.variants[variant], className)}
12
+
data-language={language}
13
+
{...props}
14
+
>
15
+
{children}
16
+
</code>
17
+
);
18
+
19
+
// For block variant, wrap in <pre> tag
20
+
if (variant === 'block') {
21
+
return <pre className="not-prose">{codeElement}</pre>;
22
+
}
23
+
24
+
return codeElement;
25
+
},
26
+
);
27
+
28
+
Code.displayName = 'Code';
+10
src/components/Code/Code.types.ts
+10
src/components/Code/Code.types.ts
···
1
+
import { ReactNode } from 'react';
2
+
3
+
export type CodeVariant = 'inline' | 'block';
4
+
5
+
export interface CodeProps {
6
+
children: ReactNode;
7
+
variant?: CodeVariant; // 'inline' for <code>, 'block' for <pre><code>
8
+
language?: string; // Optional language identifier for syntax highlighting
9
+
className?: string;
10
+
}
+2
src/components/Code/index.ts
+2
src/components/Code/index.ts
+2
-2
src/components/Divider/Divider.styles.ts
+2
-2
src/components/Divider/Divider.styles.ts
···
1
1
// Divider inherits color from page theme context
2
-
export const dividerHorizontal = 'border-t-[1px] border-l-0 border-r-0 border-b-0';
3
-
export const dividerVertical = 'border-l-[1px] border-t-0 border-r-0 border-b-0 inline-block h-full align-middle';
2
+
export const dividerHorizontal = 'border-t-[2px] border-l-0 border-r-0 border-b-0';
3
+
export const dividerVertical = 'border-l-[2px] border-t-0 border-r-0 border-b-0 inline-block h-full align-middle';
+1
-1
src/components/Divider/Divider.tsx
+1
-1
src/components/Divider/Divider.tsx
···
11
11
// Accent: white border
12
12
// Default: black border in light mode, white in dark
13
13
const themeClasses =
14
-
pageTheme === 'accent' ? 'border-bones-white' : 'border-bones-black-30 dark:border-bones-white-30';
14
+
pageTheme === 'accent' ? 'border-bones-white' : 'border-bones-black-20 dark:border-bones-white-20';
15
15
16
16
const orientationStyles = orientation === 'vertical' ? dividerVertical : dividerHorizontal;
17
17
+6
src/components/Event/Event.styles.ts
+6
src/components/Event/Event.styles.ts
···
1
+
export const eventStyles = {
2
+
wrapper: 'px-8 py-12 bg-bones-white dark:bg-bones-black flex flex-col gap-8',
3
+
title: 'text-2xl font-bold font-dm-sans text-neutral-900',
4
+
subtitle: 'text-lg font-normal font-dm-sans text-neutral-700 italic mt-2',
5
+
meta: 'text-sm font-medium font-dm-sans text-neutral-500 uppercase mt-4',
6
+
} as const;
+32
src/components/Event/Event.tsx
+32
src/components/Event/Event.tsx
···
1
+
import { Heading } from '@/components/Heading/Heading';
2
+
import { Paragraph } from '@/components/Paragraph/Paragraph';
3
+
import { cn } from '@/lib/utils';
4
+
import React from 'react';
5
+
import { eventStyles } from './Event.styles';
6
+
import { EventProps } from './Event.types';
7
+
8
+
export const Event: React.FC<EventProps> = ({
9
+
eventType,
10
+
eventTitle,
11
+
eventDescription,
12
+
eventStartYear,
13
+
eventEndYear,
14
+
eventAffiliation,
15
+
className = '',
16
+
}) => (
17
+
<div className={cn(eventStyles.wrapper, className)}>
18
+
<div className="flex items-center justify-between">
19
+
<Paragraph size="sm">{eventAffiliation}</Paragraph>
20
+
<Paragraph size="sm">
21
+
{eventStartYear} – {eventEndYear}
22
+
</Paragraph>
23
+
</div>
24
+
<div className="flex flex-col gap-2">
25
+
<Heading level={3} size="md">
26
+
{eventTitle}
27
+
</Heading>
28
+
<Paragraph size="md">{eventDescription}</Paragraph>
29
+
</div>
30
+
<Paragraph size="sm">{eventType}</Paragraph>
31
+
</div>
32
+
);
+9
src/components/Event/Event.types.ts
+9
src/components/Event/Event.types.ts
-6
src/components/Events/Event.styles.ts
-6
src/components/Events/Event.styles.ts
···
1
-
export const eventStyles = {
2
-
wrapper: 'px-8 py-12 bg-bones-white dark:bg-bones-black flex flex-col gap-8',
3
-
title: 'text-2xl font-bold font-dm-sans text-neutral-900',
4
-
subtitle: 'text-lg font-normal font-dm-sans text-neutral-700 italic mt-2',
5
-
meta: 'text-sm font-medium font-dm-sans text-neutral-500 uppercase mt-4',
6
-
} as const;
-32
src/components/Events/Event.tsx
-32
src/components/Events/Event.tsx
···
1
-
import { Heading } from '@/components/Heading/Heading';
2
-
import { Paragraph } from '@/components/Paragraph/Paragraph';
3
-
import { cn } from '@/lib/utils';
4
-
import React from 'react';
5
-
import { eventStyles } from './Event.styles';
6
-
import { EventProps } from './Event.types';
7
-
8
-
export const Event: React.FC<EventProps> = ({
9
-
eventType,
10
-
eventTitle,
11
-
eventDescription,
12
-
eventStartYear,
13
-
eventEndYear,
14
-
eventAffiliation,
15
-
className = '',
16
-
}) => (
17
-
<div className={cn(eventStyles.wrapper, className)}>
18
-
<div className="flex items-center justify-between">
19
-
<Paragraph size="footnote">{eventAffiliation}</Paragraph>
20
-
<Paragraph size="footnote">
21
-
{eventStartYear} – {eventEndYear}
22
-
</Paragraph>
23
-
</div>
24
-
<div className="flex flex-col gap-2">
25
-
<Heading level={3} style="body">
26
-
{eventTitle}
27
-
</Heading>
28
-
<Paragraph size="body">{eventDescription}</Paragraph>
29
-
</div>
30
-
<Paragraph size="footnote">{eventType}</Paragraph>
31
-
</div>
32
-
);
-9
src/components/Events/Event.types.ts
-9
src/components/Events/Event.types.ts
+2
-2
src/components/Events/Events.tsx
+2
-2
src/components/Events/Events.tsx
···
1
1
import { cn } from '@/lib/utils';
2
2
import React from 'react';
3
-
import { Event } from './Event';
3
+
import { CardEvent } from '@/components/CardEvent/CardEvent';
4
4
import { eventsStyles } from './Events.styles';
5
5
import { EventsProps } from './Events.types';
6
6
7
7
export const Events: React.FC<EventsProps> = ({ items, className = '' }) => (
8
8
<div className={cn(eventsStyles.wrapper, className)}>
9
9
{items.map((item) => (
10
-
<Event
10
+
<CardEvent
11
11
key={item.eventTitle}
12
12
eventType={item.eventType}
13
13
eventTitle={item.eventTitle}
+2
-2
src/components/Events/Events.types.ts
+2
-2
src/components/Events/Events.types.ts
+3
src/components/Fact/Fact.styles.ts
+3
src/components/Fact/Fact.styles.ts
+15
src/components/Fact/Fact.tsx
+15
src/components/Fact/Fact.tsx
···
1
+
import { Heading } from '@/components/Heading/Heading';
2
+
import { Paragraph } from '@/components/Paragraph/Paragraph';
3
+
import { cn } from '@/lib/utils';
4
+
import React from 'react';
5
+
import { factStyles } from './Fact.styles';
6
+
import { FactProps } from './Fact.types';
7
+
8
+
export const Fact: React.FC<FactProps> = ({ title, subtitle, className = '' }) => (
9
+
<div className={cn(factStyles.wrapper, className)}>
10
+
<Heading level={3} size="md">
11
+
{title}
12
+
</Heading>
13
+
<Paragraph size="md">{subtitle}</Paragraph>
14
+
</div>
15
+
);
+5
src/components/Fact/Fact.types.ts
+5
src/components/Fact/Fact.types.ts
-3
src/components/Facts/Fact.styles.ts
-3
src/components/Facts/Fact.styles.ts
-15
src/components/Facts/Fact.tsx
-15
src/components/Facts/Fact.tsx
···
1
-
import { Heading } from '@/components/Heading/Heading';
2
-
import { Paragraph } from '@/components/Paragraph/Paragraph';
3
-
import { cn } from '@/lib/utils';
4
-
import React from 'react';
5
-
import { factStyles } from './Fact.styles';
6
-
import { FactProps } from './Fact.types';
7
-
8
-
export const Fact: React.FC<FactProps> = ({ title, subtitle, className = '' }) => (
9
-
<div className={cn(factStyles.wrapper, className)}>
10
-
<Heading level={3} style="body">
11
-
{title}
12
-
</Heading>
13
-
<Paragraph size="body">{subtitle}</Paragraph>
14
-
</div>
15
-
);
-5
src/components/Facts/Fact.types.ts
-5
src/components/Facts/Fact.types.ts
+2
-2
src/components/Facts/Facts.tsx
+2
-2
src/components/Facts/Facts.tsx
···
1
1
import { cn } from '@/lib/utils';
2
2
import React from 'react';
3
-
import { Fact } from './Fact';
3
+
import { CardFact } from '@/components/CardFact/CardFact';
4
4
import { factsStyles } from './Facts.styles';
5
5
import { FactsProps } from './Facts.types';
6
6
···
8
8
return (
9
9
<div className={cn(factsStyles.wrapper, className)}>
10
10
{items.map((item) => (
11
-
<Fact key={item.title} title={item.title} subtitle={item.subtitle} />
11
+
<CardFact key={item.title} title={item.title} subtitle={item.subtitle} />
12
12
))}
13
13
</div>
14
14
);
+2
-2
src/components/Facts/Facts.types.ts
+2
-2
src/components/Facts/Facts.types.ts
+15
-8
src/components/Heading/Heading.styles.ts
+15
-8
src/components/Heading/Heading.styles.ts
···
1
+
/**
2
+
* Heading Styles
3
+
*
4
+
* Uses the global typography system with fluid type scaling. All headings are bold (font-weight: 700) by default.
5
+
*
6
+
* Size prop controls visual appearance independent of semantic level. Example: <Heading level={3} size="xl"> renders an
7
+
* h3 with xl styling
8
+
*/
1
9
export const HeadingStyles = {
2
-
base: 'font-bold font-dm-sans',
10
+
base: 'font-dm-sans',
3
11
sizes: {
4
-
billboard: 'italic font-medium text-6xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl leading-normal',
5
-
body: 'italic text-xl sm:text-xl md:text-2xl lg:text-3xl xl:text-3xl leading-normal',
6
-
footnote: 'italic text-base sm:text-base md:text-lg lg:text-xl xl:text-xl font-semibold leading-normal',
7
-
page: 'italic text-1xl sm:text-1xl md:text-1xl lg:text-2xl xl:text-3xl leading-normal',
8
-
section: 'italic text-3xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-5xl leading-normal',
9
-
title:
10
-
'italic text-3xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-5xl leading-tight sm:leading-tight md:leading-tight lg:leading-tight xl:leading-tight 2xl:leading-tight',
12
+
sm: 'heading-sm',
13
+
base: 'heading-base',
14
+
md: 'heading-md',
15
+
lg: 'heading-lg',
16
+
xl: 'heading-xl',
17
+
'2xl': 'heading-2xl',
11
18
},
12
19
} as const;
+6
-2
src/components/Heading/Heading.tsx
+6
-2
src/components/Heading/Heading.tsx
···
13
13
return 'h3';
14
14
case 4:
15
15
return 'h4';
16
+
case 5:
17
+
return 'h5';
18
+
case 6:
19
+
return 'h6';
16
20
default:
17
21
return 'h3';
18
22
}
19
23
};
20
24
21
25
export const Heading = React.forwardRef<HTMLHeadingElement, HeadingProps>(
22
-
({ children, level = 3, style = 'body', className = '', ...props }, ref) => {
26
+
({ children, level = 3, size = 'md', className = '', ...props }, ref) => {
23
27
const Component = getHeadingTag(level);
24
28
25
29
return React.createElement(
26
30
Component,
27
31
{
28
32
ref,
29
-
className: cn(HeadingStyles.base, HeadingStyles.sizes[style], className),
33
+
className: cn(HeadingStyles.base, HeadingStyles.sizes[size], className),
30
34
...props,
31
35
},
32
36
children,
+4
-4
src/components/Heading/Heading.types.ts
+4
-4
src/components/Heading/Heading.types.ts
···
1
1
import { ReactNode } from 'react';
2
2
3
-
export type HeadingLevel = 1 | 2 | 3 | 4;
4
-
export type HeadingStyle = 'page' | 'section' | 'body' | 'billboard' | 'footnote' | `title`;
3
+
export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
4
+
export type HeadingSize = 'sm' | 'base' | 'md' | 'lg' | 'xl' | '2xl';
5
5
6
6
export interface HeadingProps {
7
7
children: ReactNode;
8
-
level?: HeadingLevel; // Semantic HTML tag
9
-
style?: HeadingStyle; // Visual style class
8
+
level?: HeadingLevel; // Semantic HTML tag (h1-h6)
9
+
size?: HeadingSize; // Visual size (sm, md, lg, xl, 2xl)
10
10
className?: string;
11
11
}
+4
-7
src/components/Layout/Layout.styles.ts
+4
-7
src/components/Layout/Layout.styles.ts
···
3
3
4
4
export const getLayoutStyles = (theme: PageTheme = 'default') =>
5
5
cn(
6
-
'grid grid-cols-1 md:grid-cols-3 min-h-screen gap-0',
6
+
'grid grid-cols-1 md:grid-cols-[2fr_auto_1fr] min-h-screen gap-0',
7
7
// Accent theme: blue (light) / mediumblue (dark), white text
8
8
theme === 'accent' && 'bg-bones-blue dark:bg-bones-mediumblue text-bones-white',
9
9
// Default theme: white (light) / black (dark), black/white text
10
10
theme === 'default' && 'bg-bones-white dark:bg-bones-black text-bones-black dark:text-bones-white',
11
11
);
12
12
13
-
export const main = 'md:col-span-2 p-12 md:order-1';
13
+
export const main = 'p-12';
14
14
15
15
export const getAsideStyles = (theme: PageTheme = 'default') =>
16
16
cn(
17
-
'md:col-span-1 p-12 md:order-2',
18
-
// Accent theme: white border with transparency
19
-
theme === 'accent' && 'border-l border-bones-white-20',
20
-
// Default theme: black/white borders based on mode
21
-
theme === 'default' && 'border-l border-bones-black-20 dark:border-bones-white-20',
17
+
'p-12',
18
+
// Theme-specific styles (border removed - now using Divider component)
22
19
);
+15
-1
src/components/Layout/Layout.tsx
+15
-1
src/components/Layout/Layout.tsx
···
2
2
import * as styles from './Layout.styles';
3
3
import { DEFAULT_THEME } from './Layout.constants';
4
4
import { AsideProps, LayoutProps, PageTheme, MainProps } from './Layout.types';
5
+
import { Divider } from '@/components/Divider/Divider';
5
6
6
7
// Context to share page theme with child components
7
8
const PageThemeContext = createContext<PageTheme>(DEFAULT_THEME);
···
9
10
export const usePageTheme = () => useContext(PageThemeContext);
10
11
11
12
export const Layout: React.FC<LayoutProps> = ({ children, theme = DEFAULT_THEME }) => {
13
+
// Split children into Main and Aside
14
+
const childArray = React.Children.toArray(children);
15
+
const mainComponent = childArray.find(
16
+
(child) => React.isValidElement(child) && child.type === Main
17
+
);
18
+
const asideComponent = childArray.find(
19
+
(child) => React.isValidElement(child) && child.type === Aside
20
+
);
21
+
12
22
return (
13
23
<PageThemeContext.Provider value={theme}>
14
-
<div className={styles.getLayoutStyles(theme)}>{children}</div>
24
+
<div className={styles.getLayoutStyles(theme)}>
25
+
{mainComponent}
26
+
{asideComponent && <Divider orientation="vertical" className="hidden md:block" />}
27
+
{asideComponent}
28
+
</div>
15
29
</PageThemeContext.Provider>
16
30
);
17
31
};
+14
src/components/ListItem/ListItem.styles.ts
+14
src/components/ListItem/ListItem.styles.ts
···
1
+
/**
2
+
* ListItem Styles
3
+
*
4
+
* Uses the global typography system with fluid type scaling. Works with both ordered and unordered lists.
5
+
*/
6
+
7
+
export const listItemBase = 'font-dm-sans pl-4 pb-2';
8
+
9
+
export const listItemSizes = {
10
+
sm: 'paragraph-sm',
11
+
base: 'paragraph-base',
12
+
md: 'paragraph-md',
13
+
lg: 'paragraph-lg',
14
+
} as const;
+14
src/components/ListItem/ListItem.tsx
+14
src/components/ListItem/ListItem.tsx
···
1
+
import { cn } from '@/lib/utils';
2
+
import React from 'react';
3
+
import { listItemBase, listItemSizes } from './ListItem.styles';
4
+
import { ListItemProps } from './ListItem.types';
5
+
6
+
export const ListItem: React.FC<ListItemProps> = ({ children, size = 'md', className = '', ...props }) => {
7
+
return (
8
+
<li className={cn(listItemBase, listItemSizes[size], className)} {...props}>
9
+
{children}
10
+
</li>
11
+
);
12
+
};
13
+
14
+
export default ListItem;
+9
src/components/ListItem/ListItem.types.ts
+9
src/components/ListItem/ListItem.types.ts
+17
src/components/Mark/Mark.styles.ts
+17
src/components/Mark/Mark.styles.ts
···
1
+
/**
2
+
* Mark Styles
3
+
*
4
+
* Uses the global typography system with fluid type scaling.
5
+
* Highlights text with background color (default yellow).
6
+
* Inherits font family and weight from parent.
7
+
*/
8
+
export const markStyles = {
9
+
base: 'bg-bones-yellow text-bones-black',
10
+
sizes: {
11
+
sm: 'type-sm', // 14px โ 16px fluid
12
+
md: 'type-md', // 16px โ 18px fluid
13
+
lg: 'type-lg', // 18px โ 24px fluid
14
+
xl: 'type-xl', // 24px โ 36px fluid
15
+
'2xl': 'type-2xl', // 36px โ 64px fluid
16
+
},
17
+
} as const;
+16
src/components/Mark/Mark.tsx
+16
src/components/Mark/Mark.tsx
···
1
+
import { cn } from '@/lib/utils';
2
+
import React from 'react';
3
+
import { markStyles } from './Mark.styles';
4
+
import { MarkProps } from './Mark.types';
5
+
6
+
export const Mark = React.forwardRef<HTMLElement, MarkProps>(
7
+
({ children, size = 'md', className = '', ...props }, ref) => {
8
+
return (
9
+
<mark ref={ref} className={cn(markStyles.base, markStyles.sizes[size], className)} {...props}>
10
+
{children}
11
+
</mark>
12
+
);
13
+
},
14
+
);
15
+
16
+
Mark.displayName = 'Mark';
+9
src/components/Mark/Mark.types.ts
+9
src/components/Mark/Mark.types.ts
+2
src/components/Mark/index.ts
+2
src/components/Mark/index.ts
+15
src/components/OrderedList/OrderedList.styles.ts
+15
src/components/OrderedList/OrderedList.styles.ts
···
1
+
/**
2
+
* OrderedList Styles
3
+
*
4
+
* Provides different numbering styles using CSS list-style-type.
5
+
*/
6
+
7
+
export const listBase = 'list-inside space-y-2';
8
+
9
+
export const numberStyles = {
10
+
decimal: 'list-decimal',
11
+
'lower-alpha': 'list-[lower-alpha]',
12
+
'upper-alpha': 'list-[upper-alpha]',
13
+
'lower-roman': 'list-[lower-roman]',
14
+
'upper-roman': 'list-[upper-roman]',
15
+
} as const;
+19
src/components/OrderedList/OrderedList.tsx
+19
src/components/OrderedList/OrderedList.tsx
···
1
+
import { cn } from '@/lib/utils';
2
+
import React from 'react';
3
+
import { listBase, numberStyles } from './OrderedList.styles';
4
+
import { OrderedListProps } from './OrderedList.types';
5
+
6
+
export const OrderedList: React.FC<OrderedListProps> = ({
7
+
children,
8
+
numberStyle = 'decimal',
9
+
className = '',
10
+
...props
11
+
}) => {
12
+
return (
13
+
<ol className={cn(listBase, numberStyles[numberStyle], className)} {...props}>
14
+
{children}
15
+
</ol>
16
+
);
17
+
};
18
+
19
+
export default OrderedList;
+9
src/components/OrderedList/OrderedList.types.ts
+9
src/components/OrderedList/OrderedList.types.ts
+11
-16
src/components/Paragraph/Paragraph.styles.ts
+11
-16
src/components/Paragraph/Paragraph.styles.ts
···
1
+
/**
2
+
* Paragraph Styles
3
+
*
4
+
* Uses the global typography system with fluid type scaling. All paragraphs use normal font weight (400) by default.
5
+
*/
1
6
export const paragraphStyles = {
2
7
base: 'font-dm-sans',
3
8
sizes: {
4
-
body: 'text-lg sm:text-lg md:text-lg lg:text-xl xl:text-2xl leading-snug sm:leading-snug md:leading-normal lg:leading-normal xl:leading-relaxed',
5
-
blockquote:
6
-
'italic text-6xl sm:text-6xl md:text-7xl lg:text-8xl xl:text-8xl leading-tight sm:leading-tight md:leading-tight lg:leading-tight xl:leading-tight',
7
-
byline:
8
-
'font-medium text-1xl sm:text-1xl md:text-1xl lg:text-2xl xl:text-2xl leading-snug sm:leading-snug md:leading-normal lg:leading-normal xl:leading-normal',
9
-
caption:
10
-
'font-medium text-sm sm:text-sm md:text-base lg:text-base xl:text-base leading-snug sm:leading-snug md:leading-normal lg:leading-normal xl:leading-normal',
11
-
display:
12
-
'font-black text-4xl sm:text-5xl md:text-5xl lg:text-7xl xl:text-8xl leading-tight sm:leading-tight md:leading-tight lg:leading-none xl:leading-none',
13
-
footnote:
14
-
'text-sm sm:text-sm md:text-base lg:text-lg xl:text-lg leading-snug sm:leading-snug md:leading-normal lg:leading-normal xl:leading-normal',
15
-
label:
16
-
'font-medium text-1xl sm:text-1xl md:text-1xl lg:text-2xl xl:text-2xl leading-snug sm:leading-snug md:leading-normal lg:leading-normal xl:leading-normal',
17
-
lede: 'italic text-2xl sm:text-2xl md:text-2xl lg:text-3xl xl:text-4xl leading-snug sm:leading-snug md:leading-normal lg:leading-normal xl:leading-relaxed',
18
-
billboard:
19
-
'font-medium text-6xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl leading-tight sm:leading-tight md:leading-tight lg:leading-tight xl:leading-tight',
9
+
sm: 'paragraph-sm',
10
+
base: 'paragraph-base',
11
+
md: 'paragraph-md',
12
+
lg: 'paragraph-lg',
13
+
xl: 'paragraph-xl',
14
+
'2xl': 'paragraph-2xl',
20
15
},
21
16
} as const;
+1
-1
src/components/Paragraph/Paragraph.tsx
+1
-1
src/components/Paragraph/Paragraph.tsx
···
4
4
import { ParagraphProps } from './Paragraph.types';
5
5
6
6
export const Paragraph = React.forwardRef<HTMLParagraphElement, ParagraphProps>(
7
-
({ children, size = 'body', className = '' }, ref) => {
7
+
({ children, size = 'md', className = '' }, ref) => {
8
8
return (
9
9
<p ref={ref} className={cn(paragraphStyles.base, paragraphStyles.sizes[size], className)}>
10
10
{children}
+1
-1
src/components/Paragraph/Paragraph.types.ts
+1
-1
src/components/Paragraph/Paragraph.types.ts
···
1
1
import { ReactNode } from 'react';
2
2
3
-
export type ParagraphSize = 'footnote' | 'body' | 'lede' | 'label' | 'display' | 'blockquote' | 'caption' | 'byline' | 'billboard';
3
+
export type ParagraphSize = 'sm' | 'base' | 'md' | 'lg' | 'xl' | '2xl';
4
4
5
5
export interface ParagraphProps {
6
6
children: ReactNode;
+15
src/components/Quote/Quote.styles.ts
+15
src/components/Quote/Quote.styles.ts
···
1
+
/**
2
+
* Quote Styles
3
+
*
4
+
* Uses the global typography system with fluid type scaling.
5
+
* Blockquotes use DM Serif Display font and are italicized by default.
6
+
* Normal weight (400) for elegant readability.
7
+
*/
8
+
export const quoteStyles = {
9
+
base: 'font-normal font-dm-serif italic',
10
+
sizes: {
11
+
md: 'quote-md', // 18px โ 24px fluid, serif, italic
12
+
lg: 'quote-lg', // 24px โ 36px fluid, serif, italic
13
+
xl: 'quote-xl', // 36px โ 64px fluid, sans, italic
14
+
},
15
+
} as const;
+21
src/components/Quote/Quote.tsx
+21
src/components/Quote/Quote.tsx
···
1
+
import { cn } from '@/lib/utils';
2
+
import React from 'react';
3
+
import { quoteStyles } from './Quote.styles';
4
+
import { QuoteProps } from './Quote.types';
5
+
6
+
export const Quote = React.forwardRef<HTMLQuoteElement, QuoteProps>(
7
+
({ children, size = 'md', cite, className = '', ...props }, ref) => {
8
+
return (
9
+
<blockquote
10
+
ref={ref}
11
+
className={cn(quoteStyles.base, quoteStyles.sizes[size], className)}
12
+
cite={cite}
13
+
{...props}
14
+
>
15
+
{children}
16
+
</blockquote>
17
+
);
18
+
},
19
+
);
20
+
21
+
Quote.displayName = 'Quote';
+10
src/components/Quote/Quote.types.ts
+10
src/components/Quote/Quote.types.ts
+2
src/components/Quote/index.ts
+2
src/components/Quote/index.ts
+1
-1
src/components/Section/Section.styles.ts
+1
-1
src/components/Section/Section.styles.ts
+1
-1
src/components/Section/Section.tsx
+1
-1
src/components/Section/Section.tsx
···
6
6
export const Section: React.FC<SectionProps> = ({ children, className = '', ...props }) => {
7
7
return (
8
8
<section className={cn(section, className)} {...props}>
9
-
<div className="container flex flex-col gap-16 mx-auto">{children}</div>
9
+
<div className="container flex flex-col gap-8 mx-auto">{children}</div>
10
10
</section>
11
11
);
12
12
};
+3
src/components/Skill/Skill.styles.ts
+3
src/components/Skill/Skill.styles.ts
+31
src/components/Skill/Skill.tsx
+31
src/components/Skill/Skill.tsx
···
1
+
import { Artwork } from '@/components/Artwork/Artwork';
2
+
import { Heading } from '@/components/Heading/Heading';
3
+
import { Paragraph } from '@/components/Paragraph/Paragraph';
4
+
import { cn } from '@/lib/utils';
5
+
import React from 'react';
6
+
import { Tags } from '../Tags/Tags';
7
+
import { skillStyles } from './Skill.styles';
8
+
import { SkillProps } from './Skill.types';
9
+
10
+
export const Skill: React.FC<SkillProps> = ({
11
+
skillTitle,
12
+
skillDescription,
13
+
skillApproach,
14
+
skillArtwork,
15
+
skillTags,
16
+
className = '',
17
+
}) => (
18
+
<div className={cn(skillStyles.wrapper, className)}>
19
+
<div className="flex flex-col gap-8">
20
+
<Artwork name={skillArtwork} />
21
+
<div className="flex flex-col gap-2">
22
+
<Heading level={3} size="md">
23
+
{skillTitle}
24
+
</Heading>
25
+
<Paragraph size="sm">{skillDescription}</Paragraph>
26
+
<Paragraph size="md">{skillApproach}</Paragraph>
27
+
<Tags tags={skillTags} />
28
+
</div>
29
+
</div>
30
+
</div>
31
+
);
+10
src/components/Skill/Skill.types.ts
+10
src/components/Skill/Skill.types.ts
-3
src/components/Skills/Skill.styles.ts
-3
src/components/Skills/Skill.styles.ts
-31
src/components/Skills/Skill.tsx
-31
src/components/Skills/Skill.tsx
···
1
-
import { Artwork } from '@/components/Artwork/Artwork';
2
-
import { Heading } from '@/components/Heading/Heading';
3
-
import { Paragraph } from '@/components/Paragraph/Paragraph';
4
-
import { cn } from '@/lib/utils';
5
-
import React from 'react';
6
-
import { Tags } from '../Tags/Tags';
7
-
import { skillStyles } from './Skill.styles';
8
-
import { SkillProps } from './Skill.types';
9
-
10
-
export const Skill: React.FC<SkillProps> = ({
11
-
skillTitle,
12
-
skillDescription,
13
-
skillApproach,
14
-
skillArtwork,
15
-
skillTags,
16
-
className = '',
17
-
}) => (
18
-
<div className={cn(skillStyles.wrapper, className)}>
19
-
<div className="flex flex-col gap-8">
20
-
<Artwork name={skillArtwork} />
21
-
<div className="flex flex-col gap-2">
22
-
<Heading level={3} style="body">
23
-
{skillTitle}
24
-
</Heading>
25
-
<Paragraph size="byline">{skillDescription}</Paragraph>
26
-
<Paragraph size="body">{skillApproach}</Paragraph>
27
-
<Tags tags={skillTags} />
28
-
</div>
29
-
</div>
30
-
</div>
31
-
);
-10
src/components/Skills/Skill.types.ts
-10
src/components/Skills/Skill.types.ts
+1
-1
src/components/Skills/Skills.tsx
+1
-1
src/components/Skills/Skills.tsx
+1
-1
src/components/Skills/Skills.types.ts
+1
-1
src/components/Skills/Skills.types.ts
+17
src/components/Span/Span.styles.ts
+17
src/components/Span/Span.styles.ts
···
1
+
/**
2
+
* Span Styles
3
+
*
4
+
* Uses the global typography system with fluid type scaling.
5
+
* Spans inherit font-weight from parent by default.
6
+
* Use for inline text that needs specific sizing.
7
+
*/
8
+
export const spanStyles = {
9
+
base: 'font-dm-sans',
10
+
sizes: {
11
+
sm: 'type-sm', // 14px โ 16px fluid
12
+
md: 'type-md', // 16px โ 18px fluid
13
+
lg: 'type-lg', // 18px โ 24px fluid
14
+
xl: 'type-xl', // 24px โ 36px fluid
15
+
'2xl': 'type-2xl', // 36px โ 64px fluid
16
+
},
17
+
} as const;
+16
src/components/Span/Span.tsx
+16
src/components/Span/Span.tsx
···
1
+
import { cn } from '@/lib/utils';
2
+
import React from 'react';
3
+
import { spanStyles } from './Span.styles';
4
+
import { SpanProps } from './Span.types';
5
+
6
+
export const Span = React.forwardRef<HTMLSpanElement, SpanProps>(
7
+
({ children, size = 'md', className = '', ...props }, ref) => {
8
+
return (
9
+
<span ref={ref} className={cn(spanStyles.base, spanStyles.sizes[size], className)} {...props}>
10
+
{children}
11
+
</span>
12
+
);
13
+
},
14
+
);
15
+
16
+
Span.displayName = 'Span';
+9
src/components/Span/Span.types.ts
+9
src/components/Span/Span.types.ts
+2
src/components/Span/index.ts
+2
src/components/Span/index.ts
+2
-1
src/components/TLDRProfile/TLDRProfile.constants.ts
+2
-1
src/components/TLDRProfile/TLDRProfile.constants.ts
+39
-11
src/components/TLDRProfile/TLDRProfile.tsx
+39
-11
src/components/TLDRProfile/TLDRProfile.tsx
···
1
1
import { Link } from '@/components/Link/Link';
2
2
import { Tags } from '@/components/Tags/Tags';
3
-
import { specializations } from '@/data/specializations';
3
+
import { useProtopro } from '@/hooks/atproto';
4
4
import type { JSX } from 'react';
5
-
import * as styles from './TLDRProfile.styles';
5
+
import { Heading } from '../Heading/Heading';
6
+
import { Paragraph } from '../Paragraph/Paragraph';
6
7
import { CONNECT_LINKS, PROFILE, SECTIONS } from './TLDRProfile.constants';
8
+
import * as styles from './TLDRProfile.styles';
7
9
8
10
/**
9
11
* TL;DR Profile sidebar component
···
11
13
* @returns JSX element with profile photo and summary information
12
14
*/
13
15
export default function TLDRProfile(): JSX.Element {
16
+
const { data: profile } = useProtopro();
17
+
14
18
return (
15
19
<div className={styles.container}>
16
20
<div className={styles.innerContainer}>
17
21
<img src={PROFILE.avatarSrc} alt={PROFILE.name} className={styles.avatar} />
18
22
19
23
<div className={styles.infoSection}>
20
-
<h1 className={styles.name}>{PROFILE.name}</h1>
21
-
<p className={styles.detail}>{PROFILE.title}</p>
22
-
<p className={styles.detail}>{PROFILE.location}</p>
23
-
<p className={styles.detail}>{PROFILE.experience}</p>
24
+
<Heading level={3} size="md">
25
+
{PROFILE.name}
26
+
</Heading>
27
+
<Paragraph size="base">{PROFILE.location}</Paragraph>
28
+
<Paragraph size="base">{profile?.overview || PROFILE.title}</Paragraph>
29
+
{/* <Paragraph size="base">{PROFILE.experience}</Paragraph> */}
24
30
</div>
25
31
26
-
<div className={styles.section}>
27
-
<p className={styles.sectionTitle}>{SECTIONS.specializations}</p>
28
-
<Tags tags={specializations} className="flex-wrap" />
29
-
</div>
32
+
{profile?.skills && profile.skills.length > 0 && (
33
+
<div className={styles.section}>
34
+
<Heading level={4} size="base">
35
+
{SECTIONS.skills}
36
+
</Heading>
37
+
<Tags tags={[...profile.skills].sort().join(', ')} className="flex-wrap" />
38
+
</div>
39
+
)}
40
+
41
+
{profile?.languages && profile.languages.length > 0 && (
42
+
<div className={styles.section}>
43
+
<Heading level={4} size="base">
44
+
{SECTIONS.languages}
45
+
</Heading>
46
+
<Tags
47
+
tags={[...profile.languages]
48
+
.map((lang) => lang.code)
49
+
.sort()
50
+
.join(', ')}
51
+
className="flex-wrap"
52
+
/>
53
+
</div>
54
+
)}
30
55
31
56
<div className={styles.section}>
32
-
<p className={styles.sectionTitle}>{SECTIONS.connect}</p>
57
+
<Heading level={4} size="base">
58
+
{SECTIONS.connect}
59
+
</Heading>
60
+
33
61
<div className={styles.connectLinks}>
34
62
{CONNECT_LINKS.map((link) => (
35
63
<Link key={link.href} href={link.href} target="_blank" rel="noopener noreferrer">
+3
src/components/Tag/Tag.styles.ts
+3
src/components/Tag/Tag.styles.ts
+23
src/components/Tag/Tag.tsx
+23
src/components/Tag/Tag.tsx
···
1
+
import { usePageTheme } from '@/components/Layout/Layout';
2
+
import { cn } from '@/lib/utils';
3
+
import React from 'react';
4
+
import { Paragraph } from '../Paragraph/Paragraph';
5
+
import { tagStyles } from './Tag.styles';
6
+
import { TagProps } from './Tag.types';
7
+
8
+
export const Tag: React.FC<TagProps> = ({ label, className = '' }) => {
9
+
const pageTheme = usePageTheme();
10
+
11
+
// Theme-based tag styling:
12
+
// Accent: white border with semi-transparent white background
13
+
// Default: light backgrounds with dark/light text based on mode
14
+
const themeClasses = pageTheme === 'accent'
15
+
? 'border border-bones-white bg-bones-white-20 text-bones-white'
16
+
: 'bg-bones-black-5 text-bones-black dark:bg-bones-white-10 dark:text-bones-white';
17
+
18
+
return (
19
+
<div className={cn(tagStyles.base, themeClasses, className)}>
20
+
<Paragraph size="sm">{label}</Paragraph>
21
+
</div>
22
+
);
23
+
};
+4
src/components/Tag/Tag.types.ts
+4
src/components/Tag/Tag.types.ts
-3
src/components/Tags/Tag.styles.ts
-3
src/components/Tags/Tag.styles.ts
-13
src/components/Tags/Tag.tsx
-13
src/components/Tags/Tag.tsx
···
1
-
import { cn } from '@/lib/utils';
2
-
import React from 'react';
3
-
import { Paragraph } from '../Paragraph/Paragraph';
4
-
import { tagStyles } from './Tag.styles';
5
-
import { TagProps } from './Tag.types';
6
-
7
-
export const Tag: React.FC<TagProps> = ({ label, className = '' }) => {
8
-
return (
9
-
<div className={cn(tagStyles.base, className)}>
10
-
<Paragraph size="label">{label}</Paragraph>
11
-
</div>
12
-
);
13
-
};
-4
src/components/Tags/Tag.types.ts
-4
src/components/Tags/Tag.types.ts
+3
-10
src/components/Tags/Tags.tsx
+3
-10
src/components/Tags/Tags.tsx
···
1
1
import { cn } from '@/lib/utils';
2
2
import React from 'react';
3
-
import { Paragraph } from '../Paragraph/Paragraph';
4
-
5
-
interface TagProps {
6
-
label: string;
7
-
}
8
-
9
-
export const Tag: React.FC<TagProps> = ({ label }) => (
10
-
<span className="px-2 py-1 text-sm font-medium bg-gray-200 rounded">{label}</span>
11
-
);
3
+
import { Paragraph } from '@/components/Paragraph/Paragraph';
4
+
import { Tag } from '@/components/Tag/Tag';
12
5
13
6
interface TagsProps {
14
7
tags?: string;
···
23
16
{tagArray.length > 0 ? (
24
17
tagArray.map((tag, index) => <Tag key={`${tag}-${index}`} label={tag} />)
25
18
) : (
26
-
<Paragraph size="label">No tags available</Paragraph>
19
+
<Paragraph size="sm">No tags available</Paragraph>
27
20
)}
28
21
</div>
29
22
);
+2
-2
src/components/Timeline/Timeline.tsx
+2
-2
src/components/Timeline/Timeline.tsx
···
15
15
<span className={timelineStyles.date}>{item.date}</span>
16
16
</div>
17
17
<div className={timelineStyles.contentWrapper}>
18
-
<Heading level={4} style="footnote" className={timelineStyles.title}>
18
+
<Heading level={4} size="sm" className={timelineStyles.title}>
19
19
{item.title}
20
20
</Heading>
21
-
<Paragraph size="footnote" className={timelineStyles.description}>
21
+
<Paragraph size="sm" className={timelineStyles.description}>
22
22
{item.description}
23
23
</Paragraph>
24
24
</div>
+17
src/components/UnorderedList/UnorderedList.styles.ts
+17
src/components/UnorderedList/UnorderedList.styles.ts
···
1
+
/**
2
+
* UnorderedList Styles
3
+
*
4
+
* Provides different bullet styles using CSS list-style-type. Custom bullets use unicode glyphs for consistent
5
+
* cross-browser rendering.
6
+
*/
7
+
8
+
export const listBase = 'list-outside';
9
+
10
+
export const bulletStyles = {
11
+
disc: "[&>li]:list-['โข']",
12
+
circle: "[&>li]:list-['โฆ']",
13
+
square: "[&>li]:list-['โช']",
14
+
arrow: "[&>li]:list-['โ']",
15
+
dash: "[&>li]:list-['โ']",
16
+
angle: "[&>li]:list-['โบ']",
17
+
} as const;
+14
src/components/UnorderedList/UnorderedList.tsx
+14
src/components/UnorderedList/UnorderedList.tsx
···
1
+
import { cn } from '@/lib/utils';
2
+
import React from 'react';
3
+
import { bulletStyles, listBase } from './UnorderedList.styles';
4
+
import { UnorderedListProps } from './UnorderedList.types';
5
+
6
+
export const UnorderedList: React.FC<UnorderedListProps> = ({ children, bullet = 'disc', className = '', ...props }) => {
7
+
return (
8
+
<ul className={cn(listBase, bulletStyles[bullet], className)} {...props}>
9
+
{children}
10
+
</ul>
11
+
);
12
+
};
13
+
14
+
export default UnorderedList;
+9
src/components/UnorderedList/UnorderedList.types.ts
+9
src/components/UnorderedList/UnorderedList.types.ts
-3975
src/global.css
-3975
src/global.css
···
1
-
html {
2
-
-webkit-text-size-adjust: 100%;
3
-
-ms-text-size-adjust: 100%;
4
-
font-family: sans-serif;
5
-
}
6
-
7
-
body {
8
-
margin: 0;
9
-
}
10
-
11
-
article,
12
-
aside,
13
-
details,
14
-
figcaption,
15
-
figure,
16
-
footer,
17
-
header,
18
-
hgroup,
19
-
main,
20
-
menu,
21
-
nav,
22
-
section,
23
-
summary {
24
-
display: block;
25
-
}
26
-
27
-
audio,
28
-
canvas,
29
-
progress,
30
-
video {
31
-
vertical-align: baseline;
32
-
display: inline-block;
33
-
}
34
-
35
-
audio:not([controls]) {
36
-
height: 0;
37
-
display: none;
38
-
}
39
-
40
-
[hidden],
41
-
template {
42
-
display: none;
43
-
}
44
-
45
-
a {
46
-
background-color: rgba(0, 0, 0, 0);
47
-
}
48
-
49
-
a:active,
50
-
a:hover {
51
-
outline: 0;
52
-
}
53
-
54
-
abbr[title] {
55
-
border-bottom: 1px dotted;
56
-
}
57
-
58
-
b,
59
-
strong {
60
-
font-weight: bold;
61
-
}
62
-
63
-
dfn {
64
-
font-style: italic;
65
-
}
66
-
67
-
h1 {
68
-
margin: 0.67em 0;
69
-
font-size: 2em;
70
-
}
71
-
72
-
mark {
73
-
color: #000;
74
-
background: #ff0;
75
-
}
76
-
77
-
small {
78
-
font-size: 80%;
79
-
}
80
-
81
-
sub,
82
-
sup {
83
-
vertical-align: baseline;
84
-
font-size: 75%;
85
-
line-height: 0;
86
-
position: relative;
87
-
}
88
-
89
-
sup {
90
-
top: -0.5em;
91
-
}
92
-
93
-
sub {
94
-
bottom: -0.25em;
95
-
}
96
-
97
-
img {
98
-
border: 0;
99
-
}
100
-
101
-
svg:not(:root) {
102
-
overflow: hidden;
103
-
}
104
-
105
-
hr {
106
-
box-sizing: content-box;
107
-
height: 0;
108
-
}
109
-
110
-
pre {
111
-
overflow: auto;
112
-
}
113
-
114
-
code,
115
-
kbd,
116
-
pre,
117
-
samp {
118
-
font-family: monospace;
119
-
font-size: 1em;
120
-
}
121
-
122
-
button,
123
-
input,
124
-
optgroup,
125
-
select,
126
-
textarea {
127
-
color: inherit;
128
-
font: inherit;
129
-
margin: 0;
130
-
}
131
-
132
-
button {
133
-
overflow: visible;
134
-
}
135
-
136
-
button,
137
-
select {
138
-
text-transform: none;
139
-
}
140
-
141
-
button,
142
-
html input[type='button'],
143
-
input[type='reset'] {
144
-
-webkit-appearance: button;
145
-
appearance: button;
146
-
cursor: pointer;
147
-
}
148
-
149
-
button[disabled],
150
-
html input[disabled] {
151
-
cursor: default;
152
-
}
153
-
154
-
button::-moz-focus-inner,
155
-
input::-moz-focus-inner {
156
-
border: 0;
157
-
padding: 0;
158
-
}
159
-
160
-
input {
161
-
line-height: normal;
162
-
}
163
-
164
-
input[type='checkbox'],
165
-
input[type='radio'] {
166
-
box-sizing: border-box;
167
-
padding: 0;
168
-
}
169
-
170
-
input[type='number']::-webkit-inner-spin-button,
171
-
input[type='number']::-webkit-outer-spin-button {
172
-
height: auto;
173
-
}
174
-
175
-
input[type='search'] {
176
-
-webkit-appearance: none;
177
-
appearance: none;
178
-
}
179
-
180
-
input[type='search']::-webkit-search-cancel-button,
181
-
input[type='search']::-webkit-search-decoration {
182
-
-webkit-appearance: none;
183
-
}
184
-
185
-
legend {
186
-
border: 0;
187
-
padding: 0;
188
-
}
189
-
190
-
textarea {
191
-
overflow: auto;
192
-
}
193
-
194
-
optgroup {
195
-
font-weight: bold;
196
-
}
197
-
198
-
table {
199
-
border-collapse: collapse;
200
-
border-spacing: 0;
201
-
}
202
-
203
-
td,
204
-
th {
205
-
padding: 0;
206
-
}
207
-
208
-
@font-face {
209
-
font-family: webflow-icons;
210
-
src: url('data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBiUAAAC8AAAAYGNtYXDpP+a4AAABHAAAAFxnYXNwAAAAEAAAAXgAAAAIZ2x5ZmhS2XEAAAGAAAADHGhlYWQTFw3HAAAEnAAAADZoaGVhCXYFgQAABNQAAAAkaG10eCe4A1oAAAT4AAAAMGxvY2EDtALGAAAFKAAAABptYXhwABAAPgAABUQAAAAgbmFtZSoCsMsAAAVkAAABznBvc3QAAwAAAAAHNAAAACAAAwP4AZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADpAwPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAQAAAAAwACAACAAQAAQAg5gPpA//9//8AAAAAACDmAOkA//3//wAB/+MaBBcIAAMAAQAAAAAAAAAAAAAAAAABAAH//wAPAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEBIAAAAyADgAAFAAAJAQcJARcDIP5AQAGA/oBAAcABwED+gP6AQAABAOAAAALgA4AABQAAEwEXCQEH4AHAQP6AAYBAAcABwED+gP6AQAAAAwDAAOADQALAAA8AHwAvAAABISIGHQEUFjMhMjY9ATQmByEiBh0BFBYzITI2PQE0JgchIgYdARQWMyEyNj0BNCYDIP3ADRMTDQJADRMTDf3ADRMTDQJADRMTDf3ADRMTDQJADRMTAsATDSANExMNIA0TwBMNIA0TEw0gDRPAEw0gDRMTDSANEwAAAAABAJ0AtAOBApUABQAACQIHCQEDJP7r/upcAXEBcgKU/usBFVz+fAGEAAAAAAL//f+9BAMDwwAEAAkAABcBJwEXAwE3AQdpA5ps/GZsbAOabPxmbEMDmmz8ZmwDmvxmbAOabAAAAgAA/8AEAAPAAB0AOwAABSInLgEnJjU0Nz4BNzYzMTIXHgEXFhUUBw4BBwYjNTI3PgE3NjU0Jy4BJyYjMSIHDgEHBhUUFx4BFxYzAgBqXV6LKCgoKIteXWpqXV6LKCgoKIteXWpVSktvICEhIG9LSlVVSktvICEhIG9LSlVAKCiLXl1qal1eiygoKCiLXl1qal1eiygoZiEgb0tKVVVKS28gISEgb0tKVVVKS28gIQABAAABwAIAA8AAEgAAEzQ3PgE3NjMxFSIHDgEHBhUxIwAoKIteXWpVSktvICFmAcBqXV6LKChmISBvS0pVAAAAAgAA/8AFtgPAADIAOgAAARYXHgEXFhUUBw4BBwYHIxUhIicuAScmNTQ3PgE3NjMxOAExNDc+ATc2MzIXHgEXFhcVATMJATMVMzUEjD83NlAXFxYXTjU1PQL8kz01Nk8XFxcXTzY1PSIjd1BQWlJJSXInJw3+mdv+2/7c25MCUQYcHFg5OUA/ODlXHBwIAhcXTzY1PTw1Nk8XF1tQUHcjIhwcYUNDTgL+3QFt/pOTkwABAAAAAQAAmM7nP18PPPUACwQAAAAAANciZKUAAAAA1yJkpf/9/70FtgPDAAAACAACAAAAAAAAAAEAAAPA/8AAAAW3//3//QW2AAEAAAAAAAAAAAAAAAAAAAAMBAAAAAAAAAAAAAAAAgAAAAQAASAEAADgBAAAwAQAAJ0EAP/9BAAAAAQAAAAFtwAAAAAAAAAKABQAHgAyAEYAjACiAL4BFgE2AY4AAAABAAAADAA8AAMAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAADgCuAAEAAAAAAAEADQAAAAEAAAAAAAIABwCWAAEAAAAAAAMADQBIAAEAAAAAAAQADQCrAAEAAAAAAAUACwAnAAEAAAAAAAYADQBvAAEAAAAAAAoAGgDSAAMAAQQJAAEAGgANAAMAAQQJAAIADgCdAAMAAQQJAAMAGgBVAAMAAQQJAAQAGgC4AAMAAQQJAAUAFgAyAAMAAQQJAAYAGgB8AAMAAQQJAAoANADsd2ViZmxvdy1pY29ucwB3AGUAYgBmAGwAbwB3AC0AaQBjAG8AbgBzVmVyc2lvbiAxLjAAVgBlAHIAcwBpAG8AbgAgADEALgAwd2ViZmxvdy1pY29ucwB3AGUAYgBmAGwAbwB3AC0AaQBjAG8AbgBzd2ViZmxvdy1pY29ucwB3AGUAYgBmAGwAbwB3AC0AaQBjAG8AbgBzUmVndWxhcgBSAGUAZwB1AGwAYQByd2ViZmxvdy1pY29ucwB3AGUAYgBmAGwAbwB3AC0AaQBjAG8AbgBzRm9udCBnZW5lcmF0ZWQgYnkgSWNvTW9vbi4ARgBvAG4AdAAgAGcAZQBuAGUAcgBhAHQAZQBkACAAYgB5ACAASQBjAG8ATQBvAG8AbgAuAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==')
211
-
format('truetype');
212
-
font-weight: normal;
213
-
font-style: normal;
214
-
}
215
-
216
-
[class^='w-icon-'],
217
-
[class*=' w-icon-'] {
218
-
font-variant: normal;
219
-
text-transform: none;
220
-
-webkit-font-smoothing: antialiased;
221
-
-moz-osx-font-smoothing: grayscale;
222
-
font-style: normal;
223
-
font-weight: normal;
224
-
line-height: 1;
225
-
font-family: webflow-icons !important;
226
-
}
227
-
228
-
.w-icon-slider-right:before {
229
-
content: '๎';
230
-
}
231
-
232
-
.w-icon-slider-left:before {
233
-
content: '๎';
234
-
}
235
-
236
-
.w-icon-nav-menu:before {
237
-
content: '๎';
238
-
}
239
-
240
-
.w-icon-arrow-down:before,
241
-
.w-icon-dropdown-toggle:before {
242
-
content: '๎';
243
-
}
244
-
245
-
.w-icon-file-upload-remove:before {
246
-
content: '๎ค';
247
-
}
248
-
249
-
.w-icon-file-upload-icon:before {
250
-
content: '๎ค';
251
-
}
252
-
253
-
* {
254
-
box-sizing: border-box;
255
-
}
256
-
257
-
html {
258
-
height: 100%;
259
-
}
260
-
261
-
body {
262
-
color: #333;
263
-
background-color: #fff;
264
-
min-height: 100%;
265
-
margin: 0;
266
-
font-family: Arial, sans-serif;
267
-
font-size: 14px;
268
-
line-height: 20px;
269
-
}
270
-
271
-
img {
272
-
vertical-align: middle;
273
-
max-width: 100%;
274
-
display: inline-block;
275
-
}
276
-
277
-
html.w-mod-touch * {
278
-
background-attachment: scroll !important;
279
-
}
280
-
281
-
.w-block {
282
-
display: block;
283
-
}
284
-
285
-
.w-inline-block {
286
-
max-width: 100%;
287
-
display: inline-block;
288
-
}
289
-
290
-
.w-clearfix:before,
291
-
.w-clearfix:after {
292
-
content: ' ';
293
-
grid-area: 1 / 1 / 2 / 2;
294
-
display: table;
295
-
}
296
-
297
-
.w-clearfix:after {
298
-
clear: both;
299
-
}
300
-
301
-
.w-hidden {
302
-
display: none;
303
-
}
304
-
305
-
.w-button {
306
-
color: #fff;
307
-
line-height: inherit;
308
-
cursor: pointer;
309
-
background-color: #3898ec;
310
-
border: 0;
311
-
border-radius: 0;
312
-
padding: 9px 15px;
313
-
text-decoration: none;
314
-
display: inline-block;
315
-
}
316
-
317
-
input.w-button {
318
-
-webkit-appearance: button;
319
-
appearance: button;
320
-
}
321
-
322
-
html[data-w-dynpage] [data-w-cloak] {
323
-
color: rgba(0, 0, 0, 0) !important;
324
-
}
325
-
326
-
.w-code-block {
327
-
margin: unset;
328
-
}
329
-
330
-
pre.w-code-block code {
331
-
all: inherit;
332
-
}
333
-
334
-
.w-webflow-badge,
335
-
.w-webflow-badge * {
336
-
z-index: auto;
337
-
visibility: visible;
338
-
box-sizing: border-box;
339
-
float: none;
340
-
clear: none;
341
-
box-shadow: none;
342
-
opacity: 1;
343
-
direction: ltr;
344
-
font-family: inherit;
345
-
font-weight: inherit;
346
-
color: inherit;
347
-
font-size: inherit;
348
-
line-height: inherit;
349
-
font-style: inherit;
350
-
font-variant: inherit;
351
-
text-align: inherit;
352
-
letter-spacing: inherit;
353
-
-webkit-text-decoration: inherit;
354
-
text-decoration: inherit;
355
-
text-indent: 0;
356
-
text-transform: inherit;
357
-
text-shadow: none;
358
-
cursor: inherit;
359
-
white-space: inherit;
360
-
word-break: normal;
361
-
word-spacing: normal;
362
-
word-wrap: normal;
363
-
background: none;
364
-
border: 0 rgba(0, 0, 0, 0);
365
-
border-radius: 0;
366
-
width: auto;
367
-
min-width: 0;
368
-
max-width: none;
369
-
height: auto;
370
-
min-height: 0;
371
-
max-height: none;
372
-
margin: 0;
373
-
padding: 0;
374
-
list-style-type: disc;
375
-
transition: none;
376
-
display: block;
377
-
position: static;
378
-
top: auto;
379
-
bottom: auto;
380
-
left: auto;
381
-
right: auto;
382
-
overflow: visible;
383
-
transform: none;
384
-
}
385
-
386
-
.w-webflow-badge {
387
-
white-space: nowrap;
388
-
cursor: pointer;
389
-
box-shadow:
390
-
0 0 0 1px rgba(0, 0, 0, 0.1),
391
-
0 1px 3px rgba(0, 0, 0, 0.1);
392
-
visibility: visible !important;
393
-
z-index: 2147483647 !important;
394
-
color: #aaadb0 !important;
395
-
opacity: 1 !important;
396
-
background-color: #fff !important;
397
-
border-radius: 3px !important;
398
-
width: auto !important;
399
-
height: auto !important;
400
-
margin: 0 !important;
401
-
padding: 6px !important;
402
-
font-size: 12px !important;
403
-
line-height: 14px !important;
404
-
text-decoration: none !important;
405
-
display: inline-block !important;
406
-
position: fixed !important;
407
-
top: auto !important;
408
-
bottom: 12px !important;
409
-
left: auto !important;
410
-
right: 12px !important;
411
-
overflow: visible !important;
412
-
transform: none !important;
413
-
}
414
-
415
-
.w-webflow-badge > img {
416
-
visibility: visible !important;
417
-
opacity: 1 !important;
418
-
vertical-align: middle !important;
419
-
display: inline-block !important;
420
-
}
421
-
422
-
h1,
423
-
h2,
424
-
h3,
425
-
h4,
426
-
h5,
427
-
h6 {
428
-
margin-bottom: 10px;
429
-
font-weight: bold;
430
-
}
431
-
432
-
h1 {
433
-
margin-top: 20px;
434
-
font-size: 38px;
435
-
line-height: 44px;
436
-
}
437
-
438
-
h2 {
439
-
margin-top: 20px;
440
-
font-size: 32px;
441
-
line-height: 36px;
442
-
}
443
-
444
-
h3 {
445
-
margin-top: 20px;
446
-
font-size: 24px;
447
-
line-height: 30px;
448
-
}
449
-
450
-
h4 {
451
-
margin-top: 10px;
452
-
font-size: 18px;
453
-
line-height: 24px;
454
-
}
455
-
456
-
h5 {
457
-
margin-top: 10px;
458
-
font-size: 14px;
459
-
line-height: 20px;
460
-
}
461
-
462
-
h6 {
463
-
margin-top: 10px;
464
-
font-size: 12px;
465
-
line-height: 18px;
466
-
}
467
-
468
-
p {
469
-
margin-top: 0;
470
-
margin-bottom: 10px;
471
-
}
472
-
473
-
blockquote {
474
-
border-left: 5px solid #e2e2e2;
475
-
margin: 0 0 10px;
476
-
padding: 10px 20px;
477
-
font-size: 18px;
478
-
line-height: 22px;
479
-
}
480
-
481
-
figure {
482
-
margin: 0 0 10px;
483
-
}
484
-
485
-
figcaption {
486
-
text-align: center;
487
-
margin-top: 5px;
488
-
}
489
-
490
-
ul,
491
-
ol {
492
-
margin-top: 0;
493
-
margin-bottom: 10px;
494
-
padding-left: 40px;
495
-
}
496
-
497
-
.w-list-unstyled {
498
-
padding-left: 0;
499
-
list-style: none;
500
-
}
501
-
502
-
.w-embed:before,
503
-
.w-embed:after {
504
-
content: ' ';
505
-
grid-area: 1 / 1 / 2 / 2;
506
-
display: table;
507
-
}
508
-
509
-
.w-embed:after {
510
-
clear: both;
511
-
}
512
-
513
-
.w-video {
514
-
width: 100%;
515
-
padding: 0;
516
-
position: relative;
517
-
}
518
-
519
-
.w-video iframe,
520
-
.w-video object,
521
-
.w-video embed {
522
-
border: none;
523
-
width: 100%;
524
-
height: 100%;
525
-
position: absolute;
526
-
top: 0;
527
-
left: 0;
528
-
}
529
-
530
-
fieldset {
531
-
border: 0;
532
-
margin: 0;
533
-
padding: 0;
534
-
}
535
-
536
-
button,
537
-
[type='button'],
538
-
[type='reset'] {
539
-
cursor: pointer;
540
-
-webkit-appearance: button;
541
-
appearance: button;
542
-
border: 0;
543
-
}
544
-
545
-
.w-form {
546
-
margin: 0 0 15px;
547
-
}
548
-
549
-
.w-form-done {
550
-
text-align: center;
551
-
background-color: #ddd;
552
-
padding: 20px;
553
-
display: none;
554
-
}
555
-
556
-
.w-form-fail {
557
-
background-color: #ffdede;
558
-
margin-top: 10px;
559
-
padding: 10px;
560
-
display: none;
561
-
}
562
-
563
-
label {
564
-
margin-bottom: 5px;
565
-
font-weight: bold;
566
-
display: block;
567
-
}
568
-
569
-
.w-input,
570
-
.w-select {
571
-
color: #333;
572
-
background-color: #fff;
573
-
border: 1px solid #ccc;
574
-
width: 100%;
575
-
height: 38px;
576
-
margin-bottom: 10px;
577
-
padding: 8px 12px;
578
-
font-size: 14px;
579
-
line-height: 1.42857;
580
-
display: block;
581
-
}
582
-
583
-
.w-input:-moz-placeholder,
584
-
.w-select:-moz-placeholder {
585
-
color: #999;
586
-
}
587
-
588
-
.w-input::-moz-placeholder,
589
-
.w-select::-moz-placeholder {
590
-
color: #999;
591
-
opacity: 1;
592
-
}
593
-
594
-
.w-input::-webkit-input-placeholder,
595
-
.w-select::-webkit-input-placeholder {
596
-
color: #999;
597
-
}
598
-
599
-
.w-input:focus,
600
-
.w-select:focus {
601
-
border-color: #3898ec;
602
-
outline: 0;
603
-
}
604
-
605
-
.w-input[disabled],
606
-
.w-select[disabled],
607
-
.w-input[readonly],
608
-
.w-select[readonly],
609
-
fieldset[disabled] .w-input,
610
-
fieldset[disabled] .w-select {
611
-
cursor: not-allowed;
612
-
}
613
-
614
-
.w-input[disabled]:not(.w-input-disabled),
615
-
.w-select[disabled]:not(.w-input-disabled),
616
-
.w-input[readonly],
617
-
.w-select[readonly],
618
-
fieldset[disabled]:not(.w-input-disabled) .w-input,
619
-
fieldset[disabled]:not(.w-input-disabled) .w-select {
620
-
background-color: #eee;
621
-
}
622
-
623
-
textarea.w-input,
624
-
textarea.w-select {
625
-
height: auto;
626
-
}
627
-
628
-
.w-select {
629
-
background-color: #f3f3f3;
630
-
}
631
-
632
-
.w-select[multiple] {
633
-
height: auto;
634
-
}
635
-
636
-
.w-form-label {
637
-
cursor: pointer;
638
-
margin-bottom: 0;
639
-
font-weight: normal;
640
-
display: inline-block;
641
-
}
642
-
643
-
.w-radio {
644
-
margin-bottom: 5px;
645
-
padding-left: 20px;
646
-
display: block;
647
-
}
648
-
649
-
.w-radio:before,
650
-
.w-radio:after {
651
-
content: ' ';
652
-
grid-area: 1 / 1 / 2 / 2;
653
-
display: table;
654
-
}
655
-
656
-
.w-radio:after {
657
-
clear: both;
658
-
}
659
-
660
-
.w-radio-input {
661
-
float: left;
662
-
margin: 3px 0 0 -20px;
663
-
line-height: normal;
664
-
}
665
-
666
-
.w-file-upload {
667
-
margin-bottom: 10px;
668
-
display: block;
669
-
}
670
-
671
-
.w-file-upload-input {
672
-
opacity: 0;
673
-
z-index: -100;
674
-
width: 0.1px;
675
-
height: 0.1px;
676
-
position: absolute;
677
-
overflow: hidden;
678
-
}
679
-
680
-
.w-file-upload-default,
681
-
.w-file-upload-uploading,
682
-
.w-file-upload-success {
683
-
color: #333;
684
-
display: inline-block;
685
-
}
686
-
687
-
.w-file-upload-error {
688
-
margin-top: 10px;
689
-
display: block;
690
-
}
691
-
692
-
.w-file-upload-default.w-hidden,
693
-
.w-file-upload-uploading.w-hidden,
694
-
.w-file-upload-error.w-hidden,
695
-
.w-file-upload-success.w-hidden {
696
-
display: none;
697
-
}
698
-
699
-
.w-file-upload-uploading-btn {
700
-
cursor: pointer;
701
-
background-color: #fafafa;
702
-
border: 1px solid #ccc;
703
-
margin: 0;
704
-
padding: 8px 12px;
705
-
font-size: 14px;
706
-
font-weight: normal;
707
-
display: flex;
708
-
}
709
-
710
-
.w-file-upload-file {
711
-
background-color: #fafafa;
712
-
border: 1px solid #ccc;
713
-
flex-grow: 1;
714
-
justify-content: space-between;
715
-
margin: 0;
716
-
padding: 8px 9px 8px 11px;
717
-
display: flex;
718
-
}
719
-
720
-
.w-file-upload-file-name {
721
-
font-size: 14px;
722
-
font-weight: normal;
723
-
display: block;
724
-
}
725
-
726
-
.w-file-remove-link {
727
-
cursor: pointer;
728
-
width: auto;
729
-
height: auto;
730
-
margin-top: 3px;
731
-
margin-left: 10px;
732
-
padding: 3px;
733
-
display: block;
734
-
}
735
-
736
-
.w-icon-file-upload-remove {
737
-
margin: auto;
738
-
font-size: 10px;
739
-
}
740
-
741
-
.w-file-upload-error-msg {
742
-
color: #ea384c;
743
-
padding: 2px 0;
744
-
display: inline-block;
745
-
}
746
-
747
-
.w-file-upload-info {
748
-
padding: 0 12px;
749
-
line-height: 38px;
750
-
display: inline-block;
751
-
}
752
-
753
-
.w-file-upload-label {
754
-
cursor: pointer;
755
-
background-color: #fafafa;
756
-
border: 1px solid #ccc;
757
-
margin: 0;
758
-
padding: 8px 12px;
759
-
font-size: 14px;
760
-
font-weight: normal;
761
-
display: inline-block;
762
-
}
763
-
764
-
.w-icon-file-upload-icon,
765
-
.w-icon-file-upload-uploading {
766
-
width: 20px;
767
-
margin-right: 8px;
768
-
display: inline-block;
769
-
}
770
-
771
-
.w-icon-file-upload-uploading {
772
-
height: 20px;
773
-
}
774
-
775
-
.w-container {
776
-
max-width: 940px;
777
-
margin-left: auto;
778
-
margin-right: auto;
779
-
}
780
-
781
-
.w-container:before,
782
-
.w-container:after {
783
-
content: ' ';
784
-
grid-area: 1 / 1 / 2 / 2;
785
-
display: table;
786
-
}
787
-
788
-
.w-container:after {
789
-
clear: both;
790
-
}
791
-
792
-
.w-container .w-row {
793
-
margin-left: -10px;
794
-
margin-right: -10px;
795
-
}
796
-
797
-
.w-row:before,
798
-
.w-row:after {
799
-
content: ' ';
800
-
grid-area: 1 / 1 / 2 / 2;
801
-
display: table;
802
-
}
803
-
804
-
.w-row:after {
805
-
clear: both;
806
-
}
807
-
808
-
.w-row .w-row {
809
-
margin-left: 0;
810
-
margin-right: 0;
811
-
}
812
-
813
-
.w-col {
814
-
float: left;
815
-
width: 100%;
816
-
min-height: 1px;
817
-
padding-left: 10px;
818
-
padding-right: 10px;
819
-
position: relative;
820
-
}
821
-
822
-
.w-col .w-col {
823
-
padding-left: 0;
824
-
padding-right: 0;
825
-
}
826
-
827
-
.w-col-1 {
828
-
width: 8.33333%;
829
-
}
830
-
831
-
.w-col-2 {
832
-
width: 16.6667%;
833
-
}
834
-
835
-
.w-col-3 {
836
-
width: 25%;
837
-
}
838
-
839
-
.w-col-4 {
840
-
width: 33.3333%;
841
-
}
842
-
843
-
.w-col-5 {
844
-
width: 41.6667%;
845
-
}
846
-
847
-
.w-col-6 {
848
-
width: 50%;
849
-
}
850
-
851
-
.w-col-7 {
852
-
width: 58.3333%;
853
-
}
854
-
855
-
.w-col-8 {
856
-
width: 66.6667%;
857
-
}
858
-
859
-
.w-col-9 {
860
-
width: 75%;
861
-
}
862
-
863
-
.w-col-10 {
864
-
width: 83.3333%;
865
-
}
866
-
867
-
.w-col-11 {
868
-
width: 91.6667%;
869
-
}
870
-
871
-
.w-col-12 {
872
-
width: 100%;
873
-
}
874
-
875
-
.w-hidden-main {
876
-
display: none !important;
877
-
}
878
-
879
-
@media screen and (max-width: 991px) {
880
-
.w-container {
881
-
max-width: 728px;
882
-
}
883
-
884
-
.w-hidden-main {
885
-
display: inherit !important;
886
-
}
887
-
888
-
.w-hidden-medium {
889
-
display: none !important;
890
-
}
891
-
892
-
.w-col-medium-1 {
893
-
width: 8.33333%;
894
-
}
895
-
896
-
.w-col-medium-2 {
897
-
width: 16.6667%;
898
-
}
899
-
900
-
.w-col-medium-3 {
901
-
width: 25%;
902
-
}
903
-
904
-
.w-col-medium-4 {
905
-
width: 33.3333%;
906
-
}
907
-
908
-
.w-col-medium-5 {
909
-
width: 41.6667%;
910
-
}
911
-
912
-
.w-col-medium-6 {
913
-
width: 50%;
914
-
}
915
-
916
-
.w-col-medium-7 {
917
-
width: 58.3333%;
918
-
}
919
-
920
-
.w-col-medium-8 {
921
-
width: 66.6667%;
922
-
}
923
-
924
-
.w-col-medium-9 {
925
-
width: 75%;
926
-
}
927
-
928
-
.w-col-medium-10 {
929
-
width: 83.3333%;
930
-
}
931
-
932
-
.w-col-medium-11 {
933
-
width: 91.6667%;
934
-
}
935
-
936
-
.w-col-medium-12 {
937
-
width: 100%;
938
-
}
939
-
940
-
.w-col-stack {
941
-
width: 100%;
942
-
left: auto;
943
-
right: auto;
944
-
}
945
-
}
946
-
947
-
@media screen and (max-width: 767px) {
948
-
.w-hidden-main,
949
-
.w-hidden-medium {
950
-
display: inherit !important;
951
-
}
952
-
953
-
.w-hidden-small {
954
-
display: none !important;
955
-
}
956
-
957
-
.w-row,
958
-
.w-container .w-row {
959
-
margin-left: 0;
960
-
margin-right: 0;
961
-
}
962
-
963
-
.w-col {
964
-
width: 100%;
965
-
left: auto;
966
-
right: auto;
967
-
}
968
-
969
-
.w-col-small-1 {
970
-
width: 8.33333%;
971
-
}
972
-
973
-
.w-col-small-2 {
974
-
width: 16.6667%;
975
-
}
976
-
977
-
.w-col-small-3 {
978
-
width: 25%;
979
-
}
980
-
981
-
.w-col-small-4 {
982
-
width: 33.3333%;
983
-
}
984
-
985
-
.w-col-small-5 {
986
-
width: 41.6667%;
987
-
}
988
-
989
-
.w-col-small-6 {
990
-
width: 50%;
991
-
}
992
-
993
-
.w-col-small-7 {
994
-
width: 58.3333%;
995
-
}
996
-
997
-
.w-col-small-8 {
998
-
width: 66.6667%;
999
-
}
1000
-
1001
-
.w-col-small-9 {
1002
-
width: 75%;
1003
-
}
1004
-
1005
-
.w-col-small-10 {
1006
-
width: 83.3333%;
1007
-
}
1008
-
1009
-
.w-col-small-11 {
1010
-
width: 91.6667%;
1011
-
}
1012
-
1013
-
.w-col-small-12 {
1014
-
width: 100%;
1015
-
}
1016
-
}
1017
-
1018
-
@media screen and (max-width: 479px) {
1019
-
.w-container {
1020
-
max-width: none;
1021
-
}
1022
-
1023
-
.w-hidden-main,
1024
-
.w-hidden-medium,
1025
-
.w-hidden-small {
1026
-
display: inherit !important;
1027
-
}
1028
-
1029
-
.w-hidden-tiny {
1030
-
display: none !important;
1031
-
}
1032
-
1033
-
.w-col {
1034
-
width: 100%;
1035
-
}
1036
-
1037
-
.w-col-tiny-1 {
1038
-
width: 8.33333%;
1039
-
}
1040
-
1041
-
.w-col-tiny-2 {
1042
-
width: 16.6667%;
1043
-
}
1044
-
1045
-
.w-col-tiny-3 {
1046
-
width: 25%;
1047
-
}
1048
-
1049
-
.w-col-tiny-4 {
1050
-
width: 33.3333%;
1051
-
}
1052
-
1053
-
.w-col-tiny-5 {
1054
-
width: 41.6667%;
1055
-
}
1056
-
1057
-
.w-col-tiny-6 {
1058
-
width: 50%;
1059
-
}
1060
-
1061
-
.w-col-tiny-7 {
1062
-
width: 58.3333%;
1063
-
}
1064
-
1065
-
.w-col-tiny-8 {
1066
-
width: 66.6667%;
1067
-
}
1068
-
1069
-
.w-col-tiny-9 {
1070
-
width: 75%;
1071
-
}
1072
-
1073
-
.w-col-tiny-10 {
1074
-
width: 83.3333%;
1075
-
}
1076
-
1077
-
.w-col-tiny-11 {
1078
-
width: 91.6667%;
1079
-
}
1080
-
1081
-
.w-col-tiny-12 {
1082
-
width: 100%;
1083
-
}
1084
-
}
1085
-
1086
-
.w-widget {
1087
-
position: relative;
1088
-
}
1089
-
1090
-
.w-widget-map {
1091
-
width: 100%;
1092
-
height: 400px;
1093
-
}
1094
-
1095
-
.w-widget-map label {
1096
-
width: auto;
1097
-
display: inline;
1098
-
}
1099
-
1100
-
.w-widget-map img {
1101
-
max-width: inherit;
1102
-
}
1103
-
1104
-
.w-widget-map .gm-style-iw {
1105
-
text-align: center;
1106
-
}
1107
-
1108
-
.w-widget-map .gm-style-iw > button {
1109
-
display: none !important;
1110
-
}
1111
-
1112
-
.w-widget-twitter {
1113
-
overflow: hidden;
1114
-
}
1115
-
1116
-
.w-widget-twitter-count-shim {
1117
-
vertical-align: top;
1118
-
text-align: center;
1119
-
background: #fff;
1120
-
border: 1px solid #758696;
1121
-
border-radius: 3px;
1122
-
width: 28px;
1123
-
height: 20px;
1124
-
display: inline-block;
1125
-
position: relative;
1126
-
}
1127
-
1128
-
.w-widget-twitter-count-shim * {
1129
-
pointer-events: none;
1130
-
-webkit-user-select: none;
1131
-
-ms-user-select: none;
1132
-
user-select: none;
1133
-
}
1134
-
1135
-
.w-widget-twitter-count-shim .w-widget-twitter-count-inner {
1136
-
text-align: center;
1137
-
color: #999;
1138
-
font-family: serif;
1139
-
font-size: 15px;
1140
-
line-height: 12px;
1141
-
position: relative;
1142
-
}
1143
-
1144
-
.w-widget-twitter-count-shim .w-widget-twitter-count-clear {
1145
-
display: block;
1146
-
position: relative;
1147
-
}
1148
-
1149
-
.w-widget-twitter-count-shim.w--large {
1150
-
width: 36px;
1151
-
height: 28px;
1152
-
}
1153
-
1154
-
.w-widget-twitter-count-shim.w--large .w-widget-twitter-count-inner {
1155
-
font-size: 18px;
1156
-
line-height: 18px;
1157
-
}
1158
-
1159
-
.w-widget-twitter-count-shim:not(.w--vertical) {
1160
-
margin-left: 5px;
1161
-
margin-right: 8px;
1162
-
}
1163
-
1164
-
.w-widget-twitter-count-shim:not(.w--vertical).w--large {
1165
-
margin-left: 6px;
1166
-
}
1167
-
1168
-
.w-widget-twitter-count-shim:not(.w--vertical):before,
1169
-
.w-widget-twitter-count-shim:not(.w--vertical):after {
1170
-
content: ' ';
1171
-
pointer-events: none;
1172
-
border: solid rgba(0, 0, 0, 0);
1173
-
width: 0;
1174
-
height: 0;
1175
-
position: absolute;
1176
-
top: 50%;
1177
-
left: 0;
1178
-
}
1179
-
1180
-
.w-widget-twitter-count-shim:not(.w--vertical):before {
1181
-
border-width: 4px;
1182
-
border-color: rgba(117, 134, 150, 0) #5d6c7b rgba(117, 134, 150, 0) rgba(117, 134, 150, 0);
1183
-
margin-top: -4px;
1184
-
margin-left: -9px;
1185
-
}
1186
-
1187
-
.w-widget-twitter-count-shim:not(.w--vertical).w--large:before {
1188
-
border-width: 5px;
1189
-
margin-top: -5px;
1190
-
margin-left: -10px;
1191
-
}
1192
-
1193
-
.w-widget-twitter-count-shim:not(.w--vertical):after {
1194
-
border-width: 4px;
1195
-
border-color: rgba(255, 255, 255, 0) #fff rgba(255, 255, 255, 0) rgba(255, 255, 255, 0);
1196
-
margin-top: -4px;
1197
-
margin-left: -8px;
1198
-
}
1199
-
1200
-
.w-widget-twitter-count-shim:not(.w--vertical).w--large:after {
1201
-
border-width: 5px;
1202
-
margin-top: -5px;
1203
-
margin-left: -9px;
1204
-
}
1205
-
1206
-
.w-widget-twitter-count-shim.w--vertical {
1207
-
width: 61px;
1208
-
height: 33px;
1209
-
margin-bottom: 8px;
1210
-
}
1211
-
1212
-
.w-widget-twitter-count-shim.w--vertical:before,
1213
-
.w-widget-twitter-count-shim.w--vertical:after {
1214
-
content: ' ';
1215
-
pointer-events: none;
1216
-
border: solid rgba(0, 0, 0, 0);
1217
-
width: 0;
1218
-
height: 0;
1219
-
position: absolute;
1220
-
top: 100%;
1221
-
left: 50%;
1222
-
}
1223
-
1224
-
.w-widget-twitter-count-shim.w--vertical:before {
1225
-
border-width: 5px;
1226
-
border-color: #5d6c7b rgba(117, 134, 150, 0) rgba(117, 134, 150, 0);
1227
-
margin-left: -5px;
1228
-
}
1229
-
1230
-
.w-widget-twitter-count-shim.w--vertical:after {
1231
-
border-width: 4px;
1232
-
border-color: #fff rgba(255, 255, 255, 0) rgba(255, 255, 255, 0);
1233
-
margin-left: -4px;
1234
-
}
1235
-
1236
-
.w-widget-twitter-count-shim.w--vertical .w-widget-twitter-count-inner {
1237
-
font-size: 18px;
1238
-
line-height: 22px;
1239
-
}
1240
-
1241
-
.w-widget-twitter-count-shim.w--vertical.w--large {
1242
-
width: 76px;
1243
-
}
1244
-
1245
-
.w-background-video {
1246
-
color: #fff;
1247
-
height: 500px;
1248
-
position: relative;
1249
-
overflow: hidden;
1250
-
}
1251
-
1252
-
.w-background-video > video {
1253
-
object-fit: cover;
1254
-
z-index: -100;
1255
-
background-position: 50%;
1256
-
background-size: cover;
1257
-
width: 100%;
1258
-
height: 100%;
1259
-
margin: auto;
1260
-
position: absolute;
1261
-
top: -100%;
1262
-
bottom: -100%;
1263
-
left: -100%;
1264
-
right: -100%;
1265
-
}
1266
-
1267
-
.w-background-video > video::-webkit-media-controls-start-playback-button {
1268
-
-webkit-appearance: none;
1269
-
display: none !important;
1270
-
}
1271
-
1272
-
.w-background-video--control {
1273
-
background-color: rgba(0, 0, 0, 0);
1274
-
padding: 0;
1275
-
position: absolute;
1276
-
bottom: 1em;
1277
-
right: 1em;
1278
-
}
1279
-
1280
-
.w-background-video--control > [hidden] {
1281
-
display: none !important;
1282
-
}
1283
-
1284
-
.w-slider {
1285
-
text-align: center;
1286
-
clear: both;
1287
-
1288
-
background: #ddd;
1289
-
height: 300px;
1290
-
position: relative;
1291
-
}
1292
-
1293
-
.w-slider-mask {
1294
-
z-index: 1;
1295
-
white-space: nowrap;
1296
-
height: 100%;
1297
-
display: block;
1298
-
position: relative;
1299
-
left: 0;
1300
-
right: 0;
1301
-
overflow: hidden;
1302
-
}
1303
-
1304
-
.w-slide {
1305
-
vertical-align: top;
1306
-
white-space: normal;
1307
-
text-align: left;
1308
-
width: 100%;
1309
-
height: 100%;
1310
-
display: inline-block;
1311
-
position: relative;
1312
-
}
1313
-
1314
-
.w-slider-nav {
1315
-
z-index: 2;
1316
-
text-align: center;
1317
-
1318
-
height: 40px;
1319
-
margin: auto;
1320
-
padding-top: 10px;
1321
-
position: absolute;
1322
-
top: auto;
1323
-
bottom: 0;
1324
-
left: 0;
1325
-
right: 0;
1326
-
}
1327
-
1328
-
.w-slider-nav.w-round > div {
1329
-
border-radius: 100%;
1330
-
}
1331
-
1332
-
.w-slider-nav.w-num > div {
1333
-
font-size: inherit;
1334
-
line-height: inherit;
1335
-
width: auto;
1336
-
height: auto;
1337
-
padding: 0.2em 0.5em;
1338
-
}
1339
-
1340
-
.w-slider-nav.w-shadow > div {
1341
-
box-shadow: 0 0 3px rgba(51, 51, 51, 0.4);
1342
-
}
1343
-
1344
-
.w-slider-nav-invert {
1345
-
color: #fff;
1346
-
}
1347
-
1348
-
.w-slider-nav-invert > div {
1349
-
background-color: rgba(34, 34, 34, 0.4);
1350
-
}
1351
-
1352
-
.w-slider-nav-invert > div.w-active {
1353
-
background-color: #222;
1354
-
}
1355
-
1356
-
.w-slider-dot {
1357
-
cursor: pointer;
1358
-
background-color: rgba(255, 255, 255, 0.4);
1359
-
width: 1em;
1360
-
height: 1em;
1361
-
margin: 0 3px 0.5em;
1362
-
transition:
1363
-
background-color 0.1s,
1364
-
color 0.1s;
1365
-
display: inline-block;
1366
-
position: relative;
1367
-
}
1368
-
1369
-
.w-slider-dot.w-active {
1370
-
background-color: #fff;
1371
-
}
1372
-
1373
-
.w-slider-dot:focus {
1374
-
outline: none;
1375
-
box-shadow: 0 0 0 2px #fff;
1376
-
}
1377
-
1378
-
.w-slider-dot:focus.w-active {
1379
-
box-shadow: none;
1380
-
}
1381
-
1382
-
.w-slider-arrow-left,
1383
-
.w-slider-arrow-right {
1384
-
cursor: pointer;
1385
-
color: #fff;
1386
-
1387
-
-webkit-user-select: none;
1388
-
-ms-user-select: none;
1389
-
user-select: none;
1390
-
width: 80px;
1391
-
margin: auto;
1392
-
font-size: 40px;
1393
-
position: absolute;
1394
-
top: 0;
1395
-
bottom: 0;
1396
-
left: 0;
1397
-
right: 0;
1398
-
overflow: hidden;
1399
-
}
1400
-
1401
-
.w-slider-arrow-left [class^='w-icon-'],
1402
-
.w-slider-arrow-right [class^='w-icon-'],
1403
-
.w-slider-arrow-left [class*=' w-icon-'],
1404
-
.w-slider-arrow-right [class*=' w-icon-'] {
1405
-
position: absolute;
1406
-
}
1407
-
1408
-
.w-slider-arrow-left:focus,
1409
-
.w-slider-arrow-right:focus {
1410
-
outline: 0;
1411
-
}
1412
-
1413
-
.w-slider-arrow-left {
1414
-
z-index: 3;
1415
-
right: auto;
1416
-
}
1417
-
1418
-
.w-slider-arrow-right {
1419
-
z-index: 4;
1420
-
left: auto;
1421
-
}
1422
-
1423
-
.w-icon-slider-left,
1424
-
.w-icon-slider-right {
1425
-
width: 1em;
1426
-
height: 1em;
1427
-
margin: auto;
1428
-
top: 0;
1429
-
bottom: 0;
1430
-
left: 0;
1431
-
right: 0;
1432
-
}
1433
-
1434
-
.w-slider-aria-label {
1435
-
clip: rect(0 0 0 0);
1436
-
border: 0;
1437
-
width: 1px;
1438
-
height: 1px;
1439
-
margin: -1px;
1440
-
padding: 0;
1441
-
position: absolute;
1442
-
overflow: hidden;
1443
-
}
1444
-
1445
-
.w-slider-force-show {
1446
-
display: block !important;
1447
-
}
1448
-
1449
-
.w-dropdown {
1450
-
text-align: left;
1451
-
z-index: 900;
1452
-
margin-left: auto;
1453
-
margin-right: auto;
1454
-
display: inline-block;
1455
-
position: relative;
1456
-
}
1457
-
1458
-
.w-dropdown-btn,
1459
-
.w-dropdown-toggle,
1460
-
.w-dropdown-link {
1461
-
vertical-align: top;
1462
-
color: #222;
1463
-
text-align: left;
1464
-
white-space: nowrap;
1465
-
margin-left: auto;
1466
-
margin-right: auto;
1467
-
padding: 20px;
1468
-
text-decoration: none;
1469
-
position: relative;
1470
-
}
1471
-
1472
-
.w-dropdown-toggle {
1473
-
-webkit-user-select: none;
1474
-
-ms-user-select: none;
1475
-
user-select: none;
1476
-
cursor: pointer;
1477
-
padding-right: 40px;
1478
-
display: inline-block;
1479
-
}
1480
-
1481
-
.w-dropdown-toggle:focus {
1482
-
outline: 0;
1483
-
}
1484
-
1485
-
.w-icon-dropdown-toggle {
1486
-
width: 1em;
1487
-
height: 1em;
1488
-
margin: auto 20px auto auto;
1489
-
position: absolute;
1490
-
top: 0;
1491
-
bottom: 0;
1492
-
right: 0;
1493
-
}
1494
-
1495
-
.w-dropdown-list {
1496
-
background: #ddd;
1497
-
min-width: 100%;
1498
-
display: none;
1499
-
position: absolute;
1500
-
}
1501
-
1502
-
.w-dropdown-list.w--open {
1503
-
display: block;
1504
-
}
1505
-
1506
-
.w-dropdown-link {
1507
-
color: #222;
1508
-
padding: 10px 20px;
1509
-
display: block;
1510
-
}
1511
-
1512
-
.w-dropdown-link.w--current {
1513
-
color: #0082f3;
1514
-
}
1515
-
1516
-
.w-dropdown-link:focus {
1517
-
outline: 0;
1518
-
}
1519
-
1520
-
@media screen and (max-width: 767px) {
1521
-
.w-nav-brand {
1522
-
padding-left: 10px;
1523
-
}
1524
-
}
1525
-
1526
-
.w-lightbox-backdrop {
1527
-
cursor: auto;
1528
-
letter-spacing: normal;
1529
-
text-indent: 0;
1530
-
text-shadow: none;
1531
-
text-transform: none;
1532
-
visibility: visible;
1533
-
white-space: normal;
1534
-
word-break: normal;
1535
-
word-spacing: normal;
1536
-
word-wrap: normal;
1537
-
color: #fff;
1538
-
text-align: center;
1539
-
z-index: 2000;
1540
-
opacity: 0;
1541
-
-webkit-tap-highlight-color: transparent;
1542
-
background: rgba(0, 0, 0, 0.9);
1543
-
outline: 0;
1544
-
font-family:
1545
-
Helvetica Neue,
1546
-
Helvetica,
1547
-
Ubuntu,
1548
-
Segoe UI,
1549
-
Verdana,
1550
-
sans-serif;
1551
-
font-size: 17px;
1552
-
font-style: normal;
1553
-
font-weight: 300;
1554
-
line-height: 1.2;
1555
-
list-style: disc;
1556
-
position: fixed;
1557
-
top: 0;
1558
-
bottom: 0;
1559
-
left: 0;
1560
-
right: 0;
1561
-
-webkit-transform: translate(0);
1562
-
transform: translate(0);
1563
-
}
1564
-
1565
-
.w-lightbox-backdrop,
1566
-
.w-lightbox-container {
1567
-
-webkit-overflow-scrolling: touch;
1568
-
height: 100%;
1569
-
overflow: auto;
1570
-
}
1571
-
1572
-
.w-lightbox-content {
1573
-
height: 100vh;
1574
-
position: relative;
1575
-
overflow: hidden;
1576
-
}
1577
-
1578
-
.w-lightbox-view {
1579
-
opacity: 0;
1580
-
width: 100vw;
1581
-
height: 100vh;
1582
-
position: absolute;
1583
-
}
1584
-
1585
-
.w-lightbox-view:before {
1586
-
content: '';
1587
-
height: 100vh;
1588
-
}
1589
-
1590
-
.w-lightbox-group,
1591
-
.w-lightbox-group .w-lightbox-view,
1592
-
.w-lightbox-group .w-lightbox-view:before {
1593
-
height: 86vh;
1594
-
}
1595
-
1596
-
.w-lightbox-frame,
1597
-
.w-lightbox-view:before {
1598
-
vertical-align: middle;
1599
-
display: inline-block;
1600
-
}
1601
-
1602
-
.w-lightbox-figure {
1603
-
margin: 0;
1604
-
position: relative;
1605
-
}
1606
-
1607
-
.w-lightbox-group .w-lightbox-figure {
1608
-
cursor: pointer;
1609
-
}
1610
-
1611
-
.w-lightbox-img {
1612
-
width: auto;
1613
-
max-width: none;
1614
-
height: auto;
1615
-
}
1616
-
1617
-
.w-lightbox-image {
1618
-
float: none;
1619
-
max-width: 100vw;
1620
-
max-height: 100vh;
1621
-
display: block;
1622
-
}
1623
-
1624
-
.w-lightbox-group .w-lightbox-image {
1625
-
max-height: 86vh;
1626
-
}
1627
-
1628
-
.w-lightbox-caption {
1629
-
text-align: left;
1630
-
text-overflow: ellipsis;
1631
-
white-space: nowrap;
1632
-
background: rgba(0, 0, 0, 0.4);
1633
-
padding: 0.5em 1em;
1634
-
position: absolute;
1635
-
bottom: 0;
1636
-
left: 0;
1637
-
right: 0;
1638
-
overflow: hidden;
1639
-
}
1640
-
1641
-
.w-lightbox-embed {
1642
-
width: 100%;
1643
-
height: 100%;
1644
-
position: absolute;
1645
-
top: 0;
1646
-
bottom: 0;
1647
-
left: 0;
1648
-
right: 0;
1649
-
}
1650
-
1651
-
.w-lightbox-control {
1652
-
cursor: pointer;
1653
-
background-position: center;
1654
-
background-repeat: no-repeat;
1655
-
background-size: 24px;
1656
-
width: 4em;
1657
-
transition: all 0.3s;
1658
-
position: absolute;
1659
-
top: 0;
1660
-
}
1661
-
1662
-
.w-lightbox-left {
1663
-
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0yMCAwIDI0IDQwIiB3aWR0aD0iMjQiIGhlaWdodD0iNDAiPjxnIHRyYW5zZm9ybT0icm90YXRlKDQ1KSI+PHBhdGggZD0ibTAgMGg1djIzaDIzdjVoLTI4eiIgb3BhY2l0eT0iLjQiLz48cGF0aCBkPSJtMSAxaDN2MjNoMjN2M2gtMjZ6IiBmaWxsPSIjZmZmIi8+PC9nPjwvc3ZnPg==');
1664
-
display: none;
1665
-
bottom: 0;
1666
-
left: 0;
1667
-
}
1668
-
1669
-
.w-lightbox-right {
1670
-
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii00IDAgMjQgNDAiIHdpZHRoPSIyNCIgaGVpZ2h0PSI0MCI+PGcgdHJhbnNmb3JtPSJyb3RhdGUoNDUpIj48cGF0aCBkPSJtMC0waDI4djI4aC01di0yM2gtMjN6IiBvcGFjaXR5PSIuNCIvPjxwYXRoIGQ9Im0xIDFoMjZ2MjZoLTN2LTIzaC0yM3oiIGZpbGw9IiNmZmYiLz48L2c+PC9zdmc+');
1671
-
display: none;
1672
-
bottom: 0;
1673
-
right: 0;
1674
-
}
1675
-
1676
-
.w-lightbox-close {
1677
-
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii00IDAgMTggMTciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxNyI+PGcgdHJhbnNmb3JtPSJyb3RhdGUoNDUpIj48cGF0aCBkPSJtMCAwaDd2LTdoNXY3aDd2NWgtN3Y3aC01di03aC03eiIgb3BhY2l0eT0iLjQiLz48cGF0aCBkPSJtMSAxaDd2LTdoM3Y3aDd2M2gtN3Y3aC0zdi03aC03eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4=');
1678
-
background-size: 18px;
1679
-
height: 2.6em;
1680
-
right: 0;
1681
-
}
1682
-
1683
-
.w-lightbox-strip {
1684
-
white-space: nowrap;
1685
-
padding: 0 1vh;
1686
-
line-height: 0;
1687
-
position: absolute;
1688
-
bottom: 0;
1689
-
left: 0;
1690
-
right: 0;
1691
-
overflow-x: auto;
1692
-
overflow-y: hidden;
1693
-
}
1694
-
1695
-
.w-lightbox-item {
1696
-
box-sizing: content-box;
1697
-
cursor: pointer;
1698
-
width: 10vh;
1699
-
padding: 2vh 1vh;
1700
-
display: inline-block;
1701
-
-webkit-transform: translate3d(0, 0, 0);
1702
-
transform: translate3d(0, 0, 0);
1703
-
}
1704
-
1705
-
.w-lightbox-active {
1706
-
opacity: 0.3;
1707
-
}
1708
-
1709
-
.w-lightbox-thumbnail {
1710
-
background: #222;
1711
-
height: 10vh;
1712
-
position: relative;
1713
-
overflow: hidden;
1714
-
}
1715
-
1716
-
.w-lightbox-thumbnail-image {
1717
-
position: absolute;
1718
-
top: 0;
1719
-
left: 0;
1720
-
}
1721
-
1722
-
.w-lightbox-thumbnail .w-lightbox-tall {
1723
-
width: 100%;
1724
-
top: 50%;
1725
-
transform: translate(0, -50%);
1726
-
}
1727
-
1728
-
.w-lightbox-thumbnail .w-lightbox-wide {
1729
-
height: 100%;
1730
-
left: 50%;
1731
-
transform: translate(-50%);
1732
-
}
1733
-
1734
-
.w-lightbox-spinner {
1735
-
box-sizing: border-box;
1736
-
border: 5px solid rgba(0, 0, 0, 0.4);
1737
-
border-radius: 50%;
1738
-
width: 40px;
1739
-
height: 40px;
1740
-
margin-top: -20px;
1741
-
margin-left: -20px;
1742
-
animation: 0.8s linear infinite spin;
1743
-
position: absolute;
1744
-
top: 50%;
1745
-
left: 50%;
1746
-
}
1747
-
1748
-
.w-lightbox-spinner:after {
1749
-
content: '';
1750
-
border: 3px solid rgba(0, 0, 0, 0);
1751
-
border-bottom-color: #fff;
1752
-
border-radius: 50%;
1753
-
position: absolute;
1754
-
top: -4px;
1755
-
bottom: -4px;
1756
-
left: -4px;
1757
-
right: -4px;
1758
-
}
1759
-
1760
-
.w-lightbox-hide {
1761
-
display: none;
1762
-
}
1763
-
1764
-
.w-lightbox-noscroll {
1765
-
overflow: hidden;
1766
-
}
1767
-
1768
-
@media (min-width: 768px) {
1769
-
.w-lightbox-content {
1770
-
height: 96vh;
1771
-
margin-top: 2vh;
1772
-
}
1773
-
1774
-
.w-lightbox-view,
1775
-
.w-lightbox-view:before {
1776
-
height: 96vh;
1777
-
}
1778
-
1779
-
.w-lightbox-group,
1780
-
.w-lightbox-group .w-lightbox-view,
1781
-
.w-lightbox-group .w-lightbox-view:before {
1782
-
height: 84vh;
1783
-
}
1784
-
1785
-
.w-lightbox-image {
1786
-
max-width: 96vw;
1787
-
max-height: 96vh;
1788
-
}
1789
-
1790
-
.w-lightbox-group .w-lightbox-image {
1791
-
max-width: 82.3vw;
1792
-
max-height: 84vh;
1793
-
}
1794
-
1795
-
.w-lightbox-left,
1796
-
.w-lightbox-right {
1797
-
opacity: 0.5;
1798
-
display: block;
1799
-
}
1800
-
1801
-
.w-lightbox-close {
1802
-
opacity: 0.8;
1803
-
}
1804
-
1805
-
.w-lightbox-control:hover {
1806
-
opacity: 1;
1807
-
}
1808
-
}
1809
-
1810
-
.w-lightbox-inactive,
1811
-
.w-lightbox-inactive:hover {
1812
-
opacity: 0;
1813
-
}
1814
-
1815
-
.w-richtext:before,
1816
-
.w-richtext:after {
1817
-
content: ' ';
1818
-
grid-area: 1 / 1 / 2 / 2;
1819
-
display: table;
1820
-
}
1821
-
1822
-
.w-richtext:after {
1823
-
clear: both;
1824
-
}
1825
-
1826
-
.w-richtext[contenteditable='true']:before,
1827
-
.w-richtext[contenteditable='true']:after {
1828
-
white-space: initial;
1829
-
}
1830
-
1831
-
.w-richtext ol,
1832
-
.w-richtext ul {
1833
-
overflow: hidden;
1834
-
}
1835
-
1836
-
.w-richtext .w-richtext-figure-selected.w-richtext-figure-type-video div:after,
1837
-
.w-richtext .w-richtext-figure-selected[data-rt-type='video'] div:after,
1838
-
.w-richtext .w-richtext-figure-selected.w-richtext-figure-type-image div,
1839
-
.w-richtext .w-richtext-figure-selected[data-rt-type='image'] div {
1840
-
outline: 2px solid #2895f7;
1841
-
}
1842
-
1843
-
.w-richtext figure.w-richtext-figure-type-video > div:after,
1844
-
.w-richtext figure[data-rt-type='video'] > div:after {
1845
-
content: '';
1846
-
display: none;
1847
-
position: absolute;
1848
-
top: 0;
1849
-
bottom: 0;
1850
-
left: 0;
1851
-
right: 0;
1852
-
}
1853
-
1854
-
.w-richtext figure {
1855
-
max-width: 60%;
1856
-
position: relative;
1857
-
}
1858
-
1859
-
.w-richtext figure > div:before {
1860
-
cursor: default !important;
1861
-
}
1862
-
1863
-
.w-richtext figure img {
1864
-
width: 100%;
1865
-
}
1866
-
1867
-
.w-richtext figure figcaption.w-richtext-figcaption-placeholder {
1868
-
opacity: 0.6;
1869
-
}
1870
-
1871
-
.w-richtext figure div {
1872
-
color: rgba(0, 0, 0, 0);
1873
-
font-size: 0;
1874
-
}
1875
-
1876
-
.w-richtext figure.w-richtext-figure-type-image,
1877
-
.w-richtext figure[data-rt-type='image'] {
1878
-
display: table;
1879
-
}
1880
-
1881
-
.w-richtext figure.w-richtext-figure-type-image > div,
1882
-
.w-richtext figure[data-rt-type='image'] > div {
1883
-
display: inline-block;
1884
-
}
1885
-
1886
-
.w-richtext figure.w-richtext-figure-type-image > figcaption,
1887
-
.w-richtext figure[data-rt-type='image'] > figcaption {
1888
-
caption-side: bottom;
1889
-
display: table-caption;
1890
-
}
1891
-
1892
-
.w-richtext figure.w-richtext-figure-type-video,
1893
-
.w-richtext figure[data-rt-type='video'] {
1894
-
width: 60%;
1895
-
height: 0;
1896
-
}
1897
-
1898
-
.w-richtext figure.w-richtext-figure-type-video iframe,
1899
-
.w-richtext figure[data-rt-type='video'] iframe {
1900
-
width: 100%;
1901
-
height: 100%;
1902
-
position: absolute;
1903
-
top: 0;
1904
-
left: 0;
1905
-
}
1906
-
1907
-
.w-richtext figure.w-richtext-figure-type-video > div,
1908
-
.w-richtext figure[data-rt-type='video'] > div {
1909
-
width: 100%;
1910
-
}
1911
-
1912
-
.w-richtext figure.w-richtext-align-center {
1913
-
clear: both;
1914
-
margin-left: auto;
1915
-
margin-right: auto;
1916
-
}
1917
-
1918
-
.w-richtext figure.w-richtext-align-center.w-richtext-figure-type-image > div,
1919
-
.w-richtext figure.w-richtext-align-center[data-rt-type='image'] > div {
1920
-
max-width: 100%;
1921
-
}
1922
-
1923
-
.w-richtext figure.w-richtext-align-normal {
1924
-
clear: both;
1925
-
}
1926
-
1927
-
.w-richtext figure.w-richtext-align-fullwidth {
1928
-
text-align: center;
1929
-
clear: both;
1930
-
width: 100%;
1931
-
max-width: 100%;
1932
-
margin-left: auto;
1933
-
margin-right: auto;
1934
-
display: block;
1935
-
}
1936
-
1937
-
.w-richtext figure.w-richtext-align-fullwidth > div {
1938
-
padding-bottom: inherit;
1939
-
display: inline-block;
1940
-
}
1941
-
1942
-
.w-richtext figure.w-richtext-align-fullwidth > figcaption {
1943
-
display: block;
1944
-
}
1945
-
1946
-
.w-richtext figure.w-richtext-align-floatleft {
1947
-
float: left;
1948
-
clear: none;
1949
-
margin-right: 15px;
1950
-
}
1951
-
1952
-
.w-richtext figure.w-richtext-align-floatright {
1953
-
float: right;
1954
-
clear: none;
1955
-
margin-left: 15px;
1956
-
}
1957
-
1958
-
.w-nav {
1959
-
z-index: 1000;
1960
-
background: #ddd;
1961
-
position: relative;
1962
-
}
1963
-
1964
-
.w-nav:before,
1965
-
.w-nav:after {
1966
-
content: ' ';
1967
-
grid-area: 1 / 1 / 2 / 2;
1968
-
display: table;
1969
-
}
1970
-
1971
-
.w-nav:after {
1972
-
clear: both;
1973
-
}
1974
-
1975
-
.w-nav-brand {
1976
-
float: left;
1977
-
color: #333;
1978
-
text-decoration: none;
1979
-
position: relative;
1980
-
}
1981
-
1982
-
.w-nav-link {
1983
-
vertical-align: top;
1984
-
color: #222;
1985
-
text-align: left;
1986
-
margin-left: auto;
1987
-
margin-right: auto;
1988
-
padding: 20px;
1989
-
text-decoration: none;
1990
-
display: inline-block;
1991
-
position: relative;
1992
-
}
1993
-
1994
-
.w-nav-link.w--current {
1995
-
color: #0082f3;
1996
-
}
1997
-
1998
-
.w-nav-menu {
1999
-
float: right;
2000
-
position: relative;
2001
-
}
2002
-
2003
-
[data-nav-menu-open] {
2004
-
text-align: center;
2005
-
background: #c8c8c8;
2006
-
min-width: 200px;
2007
-
position: absolute;
2008
-
top: 100%;
2009
-
left: 0;
2010
-
right: 0;
2011
-
overflow: visible;
2012
-
display: block !important;
2013
-
}
2014
-
2015
-
.w--nav-link-open {
2016
-
display: block;
2017
-
position: relative;
2018
-
}
2019
-
2020
-
.w-nav-overlay {
2021
-
width: 100%;
2022
-
display: none;
2023
-
position: absolute;
2024
-
top: 100%;
2025
-
left: 0;
2026
-
right: 0;
2027
-
overflow: hidden;
2028
-
}
2029
-
2030
-
.w-nav-overlay [data-nav-menu-open] {
2031
-
top: 0;
2032
-
}
2033
-
2034
-
.w-nav[data-animation='over-left'] .w-nav-overlay {
2035
-
width: auto;
2036
-
}
2037
-
2038
-
.w-nav[data-animation='over-left'] .w-nav-overlay,
2039
-
.w-nav[data-animation='over-left'] [data-nav-menu-open] {
2040
-
z-index: 1;
2041
-
top: 0;
2042
-
right: auto;
2043
-
}
2044
-
2045
-
.w-nav[data-animation='over-right'] .w-nav-overlay {
2046
-
width: auto;
2047
-
}
2048
-
2049
-
.w-nav[data-animation='over-right'] .w-nav-overlay,
2050
-
.w-nav[data-animation='over-right'] [data-nav-menu-open] {
2051
-
z-index: 1;
2052
-
top: 0;
2053
-
left: auto;
2054
-
}
2055
-
2056
-
.w-nav-button {
2057
-
float: right;
2058
-
cursor: pointer;
2059
-
2060
-
-webkit-user-select: none;
2061
-
-ms-user-select: none;
2062
-
user-select: none;
2063
-
padding: 18px;
2064
-
font-size: 24px;
2065
-
display: none;
2066
-
position: relative;
2067
-
}
2068
-
2069
-
.w-nav-button:focus {
2070
-
outline: 0;
2071
-
}
2072
-
2073
-
.w-nav-button.w--open {
2074
-
color: #fff;
2075
-
background-color: #c8c8c8;
2076
-
}
2077
-
2078
-
.w-nav[data-collapse='all'] .w-nav-menu {
2079
-
display: none;
2080
-
}
2081
-
2082
-
.w-nav[data-collapse='all'] .w-nav-button,
2083
-
.w--nav-dropdown-open,
2084
-
.w--nav-dropdown-toggle-open {
2085
-
display: block;
2086
-
}
2087
-
2088
-
.w--nav-dropdown-list-open {
2089
-
position: static;
2090
-
}
2091
-
2092
-
@media screen and (max-width: 991px) {
2093
-
.w-nav[data-collapse='medium'] .w-nav-menu {
2094
-
display: none;
2095
-
}
2096
-
2097
-
.w-nav[data-collapse='medium'] .w-nav-button {
2098
-
display: block;
2099
-
}
2100
-
}
2101
-
2102
-
@media screen and (max-width: 767px) {
2103
-
.w-nav[data-collapse='small'] .w-nav-menu {
2104
-
display: none;
2105
-
}
2106
-
2107
-
.w-nav[data-collapse='small'] .w-nav-button {
2108
-
display: block;
2109
-
}
2110
-
2111
-
.w-nav-brand {
2112
-
padding-left: 10px;
2113
-
}
2114
-
}
2115
-
2116
-
@media screen and (max-width: 479px) {
2117
-
.w-nav[data-collapse='tiny'] .w-nav-menu {
2118
-
display: none;
2119
-
}
2120
-
2121
-
.w-nav[data-collapse='tiny'] .w-nav-button {
2122
-
display: block;
2123
-
}
2124
-
}
2125
-
2126
-
.w-tabs {
2127
-
position: relative;
2128
-
}
2129
-
2130
-
.w-tabs:before,
2131
-
.w-tabs:after {
2132
-
content: ' ';
2133
-
grid-area: 1 / 1 / 2 / 2;
2134
-
display: table;
2135
-
}
2136
-
2137
-
.w-tabs:after {
2138
-
clear: both;
2139
-
}
2140
-
2141
-
.w-tab-menu {
2142
-
position: relative;
2143
-
}
2144
-
2145
-
.w-tab-link {
2146
-
vertical-align: top;
2147
-
text-align: left;
2148
-
cursor: pointer;
2149
-
color: #222;
2150
-
background-color: #ddd;
2151
-
padding: 9px 30px;
2152
-
text-decoration: none;
2153
-
display: inline-block;
2154
-
position: relative;
2155
-
}
2156
-
2157
-
.w-tab-link.w--current {
2158
-
background-color: #c8c8c8;
2159
-
}
2160
-
2161
-
.w-tab-link:focus {
2162
-
outline: 0;
2163
-
}
2164
-
2165
-
.w-tab-content {
2166
-
display: block;
2167
-
position: relative;
2168
-
overflow: hidden;
2169
-
}
2170
-
2171
-
.w-tab-pane {
2172
-
display: none;
2173
-
position: relative;
2174
-
}
2175
-
2176
-
.w--tab-active {
2177
-
display: block;
2178
-
}
2179
-
2180
-
@media screen and (max-width: 479px) {
2181
-
.w-tab-link {
2182
-
display: block;
2183
-
}
2184
-
}
2185
-
2186
-
.w-ix-emptyfix:after {
2187
-
content: '';
2188
-
}
2189
-
2190
-
@keyframes spin {
2191
-
0% {
2192
-
transform: rotate(0);
2193
-
}
2194
-
2195
-
100% {
2196
-
transform: rotate(360deg);
2197
-
}
2198
-
}
2199
-
2200
-
.w-dyn-empty {
2201
-
background-color: #ddd;
2202
-
padding: 10px;
2203
-
}
2204
-
2205
-
.w-dyn-hide,
2206
-
.w-dyn-bind-empty,
2207
-
.w-condition-invisible {
2208
-
display: none !important;
2209
-
}
2210
-
2211
-
.wf-layout-layout {
2212
-
display: grid;
2213
-
}
2214
-
2215
-
.w-code-component > * {
2216
-
width: 100%;
2217
-
height: 100%;
2218
-
position: absolute;
2219
-
top: 0;
2220
-
left: 0;
2221
-
}
2222
-
2223
-
:root {
2224
-
--mono-98: #f5f5ff;
2225
-
--mono-00: #00010d;
2226
-
--act-1: #01f;
2227
-
--hi-1: #fb0;
2228
-
--mono-75: #a9aabf;
2229
-
--act-2: #000cb3;
2230
-
--trans-50: rgba(51, 54, 102, 0.5);
2231
-
--mono-100: white;
2232
-
--mono-35: #000659;
2233
-
--mono-97: #e4e5f7;
2234
-
--mono-95: #d5d7f2;
2235
-
--act-100: #d1d4ff;
2236
-
--hi-2: #ffcd42;
2237
-
--mono-50: #000980;
2238
-
--mono-25: #101340;
2239
-
--act-90: #b3b8ff;
2240
-
--trans-25: rgba(51, 54, 102, 0.25);
2241
-
--err-1: #cc2900;
2242
-
--err-2: #f5c4b8;
2243
-
--mono-92: #ced0eb;
2244
-
--mono-85: #c3c5d9;
2245
-
}
2246
-
2247
-
.w-layout-blockcontainer {
2248
-
max-width: 940px;
2249
-
margin-left: auto;
2250
-
margin-right: auto;
2251
-
display: block;
2252
-
}
2253
-
2254
-
.w-layout-hflex {
2255
-
flex-direction: row;
2256
-
align-items: flex-start;
2257
-
display: flex;
2258
-
}
2259
-
2260
-
.w-layout-grid {
2261
-
grid-row-gap: 16px;
2262
-
grid-column-gap: 16px;
2263
-
grid-template-rows: auto auto;
2264
-
grid-template-columns: 1fr 1fr;
2265
-
grid-auto-columns: 1fr;
2266
-
display: grid;
2267
-
}
2268
-
2269
-
@media screen and (max-width: 991px) {
2270
-
.w-layout-blockcontainer {
2271
-
max-width: 728px;
2272
-
}
2273
-
}
2274
-
2275
-
@media screen and (max-width: 767px) {
2276
-
.w-layout-blockcontainer {
2277
-
max-width: none;
2278
-
}
2279
-
}
2280
-
2281
-
body {
2282
-
background-color: var(--mono-98);
2283
-
color: var(--mono-00);
2284
-
font-family:
2285
-
DM Sans,
2286
-
sans-serif;
2287
-
font-size: 24px;
2288
-
font-weight: 400;
2289
-
line-height: 1.6;
2290
-
transition:
2291
-
flex 0.4s linear,
2292
-
background-color 0.2s linear,
2293
-
color 0.2s linear;
2294
-
}
2295
-
2296
-
h1,
2297
-
h2,
2298
-
h3,
2299
-
h4,
2300
-
h5 {
2301
-
margin-top: 0;
2302
-
margin-bottom: 0;
2303
-
font-size: 1em;
2304
-
font-weight: 400;
2305
-
line-height: 1.6;
2306
-
}
2307
-
2308
-
h6 {
2309
-
margin-top: 0;
2310
-
margin-bottom: 0;
2311
-
font-size: 1em;
2312
-
font-weight: 400;
2313
-
}
2314
-
2315
-
p {
2316
-
margin-bottom: 0;
2317
-
font-size: 1em;
2318
-
}
2319
-
2320
-
/* a {
2321
-
color: var(--act-1);
2322
-
font-size: 1em;
2323
-
font-weight: 400;
2324
-
text-decoration: none;
2325
-
} */
2326
-
2327
-
/* a:hover {
2328
-
text-decoration: underline;
2329
-
} */
2330
-
2331
-
/* a:focus-visible {
2332
-
outline-color: var(--hi-1);
2333
-
outline-offset: 4px;
2334
-
border-radius: 2px;
2335
-
outline-width: 4px;
2336
-
outline-style: solid;
2337
-
} */
2338
-
2339
-
/* a[data-wf-focus-visible] {
2340
-
outline-color: var(--hi-1);
2341
-
outline-offset: 4px;
2342
-
border-radius: 2px;
2343
-
outline-width: 4px;
2344
-
outline-style: solid;
2345
-
} */
2346
-
2347
-
ul,
2348
-
ol {
2349
-
margin-top: 0;
2350
-
margin-bottom: 0;
2351
-
padding-left: 0;
2352
-
font-size: 1em;
2353
-
}
2354
-
2355
-
li {
2356
-
margin-bottom: 0;
2357
-
}
2358
-
2359
-
label {
2360
-
margin-bottom: 5px;
2361
-
font-size: 1.5rem;
2362
-
font-weight: 500;
2363
-
line-height: 2rem;
2364
-
}
2365
-
2366
-
blockquote {
2367
-
border-left: 1px solid var(--mono-75);
2368
-
letter-spacing: 0.5px;
2369
-
margin-top: 0;
2370
-
margin-bottom: 0;
2371
-
padding: 0.25em 1em 0.5em;
2372
-
font-family:
2373
-
DM Serif Text,
2374
-
sans-serif;
2375
-
font-size: 1em;
2376
-
font-style: normal;
2377
-
font-weight: 400;
2378
-
line-height: 1.6;
2379
-
}
2380
-
2381
-
figcaption {
2382
-
color: #2a2f31;
2383
-
text-align: center;
2384
-
margin-top: 6px;
2385
-
font-size: 16px;
2386
-
}
2387
-
2388
-
.bp--text {
2389
-
line-height: 1.6;
2390
-
}
2391
-
2392
-
.bp--text:hover {
2393
-
text-decoration: none;
2394
-
}
2395
-
2396
-
.bp--text.md {
2397
-
font-size: 1.25em;
2398
-
}
2399
-
2400
-
.bp--text.lg {
2401
-
margin-bottom: 0;
2402
-
font-size: 1.5em;
2403
-
}
2404
-
2405
-
.bp--text.sm {
2406
-
margin-bottom: 0;
2407
-
font-size: 0.75em;
2408
-
line-height: 1.8em;
2409
-
}
2410
-
2411
-
.bp--text.xs {
2412
-
font-size: 0.625em;
2413
-
}
2414
-
2415
-
.bp--text.hero-primary {
2416
-
font-family:
2417
-
DM Serif Display,
2418
-
sans-serif;
2419
-
font-size: 2.5em;
2420
-
font-style: italic;
2421
-
font-weight: 700;
2422
-
line-height: 1.25;
2423
-
}
2424
-
2425
-
.bp--text.hero-secondary {
2426
-
font-family:
2427
-
DM Sans,
2428
-
sans-serif;
2429
-
font-size: 1.25em;
2430
-
font-weight: 400;
2431
-
}
2432
-
2433
-
.bp--text.hidden {
2434
-
display: none;
2435
-
}
2436
-
2437
-
.bp--image {
2438
-
display: block;
2439
-
}
2440
-
2441
-
.bp--button {
2442
-
background-color: var(--act-2);
2443
-
box-shadow: 0 0 0 0 var(--trans-50);
2444
-
color: var(--mono-100);
2445
-
text-align: left;
2446
-
border-radius: 2px;
2447
-
padding: 0.375em 0.75em;
2448
-
font-size: 1em;
2449
-
font-weight: 500;
2450
-
transition:
2451
-
box-shadow 80ms ease-in,
2452
-
color 80ms linear,
2453
-
transform 80ms ease-in-out,
2454
-
background-color 80ms linear;
2455
-
}
2456
-
2457
-
.bp--button:hover {
2458
-
background-color: var(--act-1);
2459
-
box-shadow:
2460
-
0 10px 2px -4px rgba(95, 92, 153, 0.24),
2461
-
0 2px 3px -2px var(--trans-50);
2462
-
color: var(--mono-100);
2463
-
text-decoration: none;
2464
-
transform: scale(1.025);
2465
-
}
2466
-
2467
-
.bp--button:active {
2468
-
background-color: var(--mono-35);
2469
-
box-shadow: 0 2px 1px 0 var(--trans-50);
2470
-
transform: scale(1);
2471
-
}
2472
-
2473
-
.bp--button:focus-visible {
2474
-
background-color: var(--act-1);
2475
-
outline-color: var(--hi-1);
2476
-
outline-offset: 2px;
2477
-
border-radius: 1px;
2478
-
outline-width: 3px;
2479
-
outline-style: solid;
2480
-
transform: scale(1.05);
2481
-
}
2482
-
2483
-
.bp--button[data-wf-focus-visible] {
2484
-
background-color: var(--act-1);
2485
-
outline-color: var(--hi-1);
2486
-
outline-offset: 2px;
2487
-
border-radius: 1px;
2488
-
outline-width: 3px;
2489
-
outline-style: solid;
2490
-
transform: scale(1.05);
2491
-
}
2492
-
2493
-
.bp--button.emphasis-low {
2494
-
background-color: var(--mono-97);
2495
-
color: var(--act-2);
2496
-
}
2497
-
2498
-
.bp--button.emphasis-low:hover {
2499
-
background-color: var(--mono-98);
2500
-
color: var(--act-1);
2501
-
}
2502
-
2503
-
.bp--button.emphasis-low:active {
2504
-
background-color: var(--mono-95);
2505
-
color: var(--act-2);
2506
-
}
2507
-
2508
-
.bp--button.emphasis-low.w--current:hover {
2509
-
background-color: var(--act-100);
2510
-
color: var(--act-1);
2511
-
}
2512
-
2513
-
.bp--button.icon {
2514
-
background-color: var(--mono-100);
2515
-
padding-left: 1rem;
2516
-
padding-right: 1rem;
2517
-
}
2518
-
2519
-
.bp--button.icon:hover {
2520
-
background-color: #ced6f5;
2521
-
transform: scale(1.1);
2522
-
}
2523
-
2524
-
.bp--button.primary {
2525
-
background-color: var(--act-1);
2526
-
color: var(--mono-100);
2527
-
}
2528
-
2529
-
.bp--button.primary:hover,
2530
-
.bp--button.primary.w--current {
2531
-
background-color: var(--act-2);
2532
-
}
2533
-
2534
-
.bp--button.primary.mobile {
2535
-
display: none;
2536
-
}
2537
-
2538
-
.bp--button.primary.dropup {
2539
-
grid-column-gap: 16px;
2540
-
grid-row-gap: 16px;
2541
-
grid-template-rows: auto auto;
2542
-
grid-template-columns: 1fr 1fr;
2543
-
grid-auto-columns: 1fr;
2544
-
justify-content: flex-start;
2545
-
position: fixed;
2546
-
bottom: 1em;
2547
-
left: 1em;
2548
-
}
2549
-
2550
-
.bp--button.primary.dropup:hover {
2551
-
transform: none;
2552
-
}
2553
-
2554
-
.bp--button.dropup {
2555
-
display: flex;
2556
-
}
2557
-
2558
-
.bp--button.emphasis-lowest {
2559
-
box-shadow: none;
2560
-
color: var(--mono-00);
2561
-
background-color: rgba(0, 0, 0, 0);
2562
-
}
2563
-
2564
-
.bp--button.emphasis-lowest:hover {
2565
-
background-color: var(--mono-98);
2566
-
box-shadow: none;
2567
-
color: var(--act-1);
2568
-
}
2569
-
2570
-
.bp--button.emphasis-lowest:active {
2571
-
background-color: var(--mono-95);
2572
-
color: var(--act-1);
2573
-
}
2574
-
2575
-
.bp--button.emphasis-lowest:focus-visible {
2576
-
background-color: var(--act-100);
2577
-
color: var(--act-1);
2578
-
}
2579
-
2580
-
.bp--button.emphasis-lowest[data-wf-focus-visible] {
2581
-
background-color: var(--act-100);
2582
-
color: var(--act-1);
2583
-
}
2584
-
2585
-
.bp--button.emphasis-lowest.w--current {
2586
-
background-color: var(--mono-98);
2587
-
color: var(--act-1);
2588
-
}
2589
-
2590
-
.bp--button.emphasis-lowest.w--current:hover {
2591
-
transform: none;
2592
-
}
2593
-
2594
-
.bp--button.emphasis-lowest.icon {
2595
-
justify-content: center;
2596
-
align-items: center;
2597
-
font-family: 'FontAwesome 6', sans-serif;
2598
-
font-weight: 300;
2599
-
display: flex;
2600
-
}
2601
-
2602
-
.bp--button.emphasis-lowest.icon.brand {
2603
-
font-family: 'FontAwesome Brands 6', sans-serif;
2604
-
}
2605
-
2606
-
.bp--button.wide {
2607
-
width: 100%;
2608
-
}
2609
-
2610
-
.bp--section {
2611
-
flex-direction: column;
2612
-
margin-left: auto;
2613
-
margin-right: auto;
2614
-
padding-top: 2em;
2615
-
padding-bottom: 2em;
2616
-
display: flex;
2617
-
}
2618
-
2619
-
.bp--section.hero {
2620
-
background-color: var(--mono-100);
2621
-
}
2622
-
2623
-
.bp--section.nav-bar {
2624
-
z-index: 999998;
2625
-
background-color: var(--mono-100);
2626
-
box-shadow: 0 4px 16px -3px var(--trans-50);
2627
-
padding-top: 0.125rem;
2628
-
padding-bottom: 0.125rem;
2629
-
position: -webkit-sticky;
2630
-
position: sticky;
2631
-
top: 0%;
2632
-
bottom: auto;
2633
-
left: 0%;
2634
-
right: 0%;
2635
-
}
2636
-
2637
-
.bp--section.nav-callout {
2638
-
z-index: 999999;
2639
-
background-color: var(--hi-2);
2640
-
}
2641
-
2642
-
.bp--section.fit {
2643
-
width: 100%;
2644
-
padding-left: 2em;
2645
-
}
2646
-
2647
-
.bp--row {
2648
-
grid-column-gap: 1rem;
2649
-
grid-row-gap: 1rem;
2650
-
border-radius: 2px;
2651
-
flex-direction: column;
2652
-
display: flex;
2653
-
}
2654
-
2655
-
.bp--row.image-caption {
2656
-
grid-column-gap: 0.5em;
2657
-
grid-row-gap: 0.5em;
2658
-
}
2659
-
2660
-
.bp--row.grid {
2661
-
grid-column-gap: 1px;
2662
-
grid-row-gap: 1px;
2663
-
grid-template-rows: auto auto;
2664
-
grid-template-columns: 1fr 1fr;
2665
-
grid-auto-columns: 1fr;
2666
-
display: grid;
2667
-
}
2668
-
2669
-
.bp--row.large-gap {
2670
-
grid-column-gap: 2rem;
2671
-
grid-row-gap: 2rem;
2672
-
}
2673
-
2674
-
.bp--row.small-gap {
2675
-
grid-column-gap: 0.5rem;
2676
-
grid-row-gap: 0.5rem;
2677
-
}
2678
-
2679
-
.bp--row.small-gap.small-text {
2680
-
font-size: 0.75em;
2681
-
}
2682
-
2683
-
.bp--row.no-gap {
2684
-
grid-column-gap: 0rem;
2685
-
grid-row-gap: 0rem;
2686
-
}
2687
-
2688
-
.bp--row.no-gap.align-c {
2689
-
justify-content: center;
2690
-
}
2691
-
2692
-
.bp--row.align-l {
2693
-
align-items: flex-start;
2694
-
}
2695
-
2696
-
.bp--col {
2697
-
grid-column-gap: 1rem;
2698
-
grid-row-gap: 1rem;
2699
-
flex-direction: row;
2700
-
align-items: stretch;
2701
-
display: flex;
2702
-
}
2703
-
2704
-
.bp--col.icons {
2705
-
grid-column-gap: 1rem;
2706
-
grid-row-gap: 1rem;
2707
-
flex-direction: row;
2708
-
grid-template-rows: auto;
2709
-
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
2710
-
grid-auto-columns: 1fr;
2711
-
width: 100%;
2712
-
display: flex;
2713
-
}
2714
-
2715
-
.bp--col.keywords {
2716
-
display: block;
2717
-
}
2718
-
2719
-
.bp--col.menu {
2720
-
grid-column-gap: 0.5em;
2721
-
grid-row-gap: 0.5em;
2722
-
flex-direction: row;
2723
-
flex: 1;
2724
-
grid-template-rows: auto;
2725
-
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
2726
-
grid-auto-columns: 1fr;
2727
-
align-items: center;
2728
-
display: flex;
2729
-
}
2730
-
2731
-
.bp--col.menu.trigger {
2732
-
display: none;
2733
-
}
2734
-
2735
-
.bp--col.menu.right {
2736
-
justify-content: flex-end;
2737
-
}
2738
-
2739
-
.bp--col.menu.left {
2740
-
grid-column-gap: 0.125em;
2741
-
grid-row-gap: 0.125em;
2742
-
flex: 0 auto;
2743
-
}
2744
-
2745
-
.bp--col.large-gap {
2746
-
grid-column-gap: 2rem;
2747
-
grid-row-gap: 2rem;
2748
-
}
2749
-
2750
-
.bp--col.small-gap {
2751
-
grid-column-gap: 0.5rem;
2752
-
grid-row-gap: 0.5rem;
2753
-
}
2754
-
2755
-
.bp--col.no-gap {
2756
-
grid-column-gap: 0rem;
2757
-
grid-row-gap: 0rem;
2758
-
}
2759
-
2760
-
.bp--col.nav-bar {
2761
-
justify-content: space-between;
2762
-
max-width: 100%;
2763
-
padding-top: 0.5em;
2764
-
padding-bottom: 0.5em;
2765
-
font-size: 0.75em;
2766
-
}
2767
-
2768
-
.bp--col.fieldset {
2769
-
flex-direction: row;
2770
-
align-items: stretch;
2771
-
}
2772
-
2773
-
.bp--col.right {
2774
-
justify-content: flex-end;
2775
-
}
2776
-
2777
-
.bp--col.align-m {
2778
-
grid-column-gap: 0.25em;
2779
-
grid-row-gap: 0.25em;
2780
-
align-items: center;
2781
-
}
2782
-
2783
-
.bp--container-2rem {
2784
-
grid-column-gap: 2em;
2785
-
grid-row-gap: 2em;
2786
-
flex-direction: column;
2787
-
grid-template-rows: auto auto;
2788
-
grid-template-columns: 1fr;
2789
-
grid-auto-columns: 1fr;
2790
-
justify-content: space-around;
2791
-
display: flex;
2792
-
}
2793
-
2794
-
.bp--body {
2795
-
color: var(--mono-00);
2796
-
}
2797
-
2798
-
.bp--type-overline {
2799
-
color: var(--mono-50);
2800
-
letter-spacing: 0.1em;
2801
-
text-transform: uppercase;
2802
-
margin-bottom: 0;
2803
-
font-size: 1em;
2804
-
font-weight: 300;
2805
-
line-height: 1.5em;
2806
-
}
2807
-
2808
-
.bp--heading {
2809
-
letter-spacing: 0.375px;
2810
-
font-family:
2811
-
DM Serif Display,
2812
-
sans-serif;
2813
-
font-size: 1em;
2814
-
font-weight: 400;
2815
-
line-height: 1.6em;
2816
-
}
2817
-
2818
-
.bp--heading.overline {
2819
-
letter-spacing: 0.1em;
2820
-
text-transform: uppercase;
2821
-
font-family:
2822
-
DM Sans,
2823
-
sans-serif;
2824
-
font-size: 0.75em;
2825
-
}
2826
-
2827
-
.bp--heading.xxl {
2828
-
font-family:
2829
-
DM Serif Text,
2830
-
sans-serif;
2831
-
font-size: 2em;
2832
-
line-height: 1.3em;
2833
-
}
2834
-
2835
-
.bp--heading.xl {
2836
-
font-size: 2.75rem;
2837
-
line-height: 1.4;
2838
-
}
2839
-
2840
-
.bp--heading.lg {
2841
-
font-size: 1.5em;
2842
-
line-height: 1.3em;
2843
-
}
2844
-
2845
-
.bp--heading.lg.sans {
2846
-
color: var(--mono-00);
2847
-
letter-spacing: -0.375px;
2848
-
font-family:
2849
-
DM Sans,
2850
-
sans-serif;
2851
-
font-size: 1.25em;
2852
-
font-weight: 700;
2853
-
line-height: 1.2;
2854
-
}
2855
-
2856
-
.bp--heading.md {
2857
-
font-size: 1.25em;
2858
-
line-height: 1.4em;
2859
-
}
2860
-
2861
-
.bp--heading.sm {
2862
-
font-size: 0.75em;
2863
-
font-weight: 700;
2864
-
}
2865
-
2866
-
.bp--heading.xs {
2867
-
font-family:
2868
-
DM Serif Text,
2869
-
sans-serif;
2870
-
font-size: 0.625em;
2871
-
font-weight: 400;
2872
-
}
2873
-
2874
-
.bp--quote {
2875
-
font-size: 1em;
2876
-
font-style: italic;
2877
-
}
2878
-
2879
-
.bp--quote.lg {
2880
-
font-size: 1.5em;
2881
-
line-height: 1.4;
2882
-
}
2883
-
2884
-
.bp--quote.md {
2885
-
font-size: 1.25em;
2886
-
}
2887
-
2888
-
.bp--quote.sm {
2889
-
font-size: 0.75em;
2890
-
}
2891
-
2892
-
.bp--list {
2893
-
grid-column-gap: 0.5em;
2894
-
grid-row-gap: 0.5em;
2895
-
flex-direction: column;
2896
-
padding-left: 2rem;
2897
-
display: flex;
2898
-
}
2899
-
2900
-
.bp--list.ul {
2901
-
list-style-type: disc;
2902
-
}
2903
-
2904
-
.bp--list.ol {
2905
-
list-style-type: decimal;
2906
-
}
2907
-
2908
-
.bp--hightlight {
2909
-
background-color: var(--hi-1);
2910
-
padding-left: 0.125rem;
2911
-
padding-right: 0.125rem;
2912
-
font-weight: 400;
2913
-
transition: background-color 0.2s;
2914
-
}
2915
-
2916
-
.bp-type--link {
2917
-
color: var(--act-1);
2918
-
text-decoration: none;
2919
-
transition:
2920
-
transform 0.1s linear,
2921
-
color 0.1s linear;
2922
-
}
2923
-
2924
-
.bp-type--link:hover {
2925
-
color: var(--act-1);
2926
-
text-decoration: underline;
2927
-
}
2928
-
2929
-
.bp-type--link:focus-visible {
2930
-
outline-offset: 4px;
2931
-
border-radius: 0.25rem;
2932
-
outline-width: 4px;
2933
-
text-decoration: underline;
2934
-
}
2935
-
2936
-
.bp-type--link[data-wf-focus-visible] {
2937
-
outline-offset: 4px;
2938
-
border-radius: 0.25rem;
2939
-
outline-width: 4px;
2940
-
text-decoration: underline;
2941
-
}
2942
-
2943
-
.homepage-link {
2944
-
text-decoration: underline;
2945
-
transition: all 0.2s ease;
2946
-
}
2947
-
2948
-
.homepage-link:hover {
2949
-
text-decoration: none;
2950
-
}
2951
-
2952
-
.homepage-link:focus-visible {
2953
-
outline-color: var(--bones-yellow, #fb0);
2954
-
outline-offset: 4px;
2955
-
outline-width: 4px;
2956
-
outline-style: solid;
2957
-
border-radius: 2px;
2958
-
}
2959
-
2960
-
.bp-type--link.icon {
2961
-
text-align: center;
2962
-
width: 1.5em;
2963
-
font-family: 'FontAwesome 6', sans-serif;
2964
-
text-decoration: none;
2965
-
}
2966
-
2967
-
.bp-type--link.icon.brand {
2968
-
font-family: 'FontAwesome Brands 6', sans-serif;
2969
-
font-weight: 400;
2970
-
}
2971
-
2972
-
.bp-type--link.dropup {
2973
-
color: var(--mono-00);
2974
-
flex: none;
2975
-
}
2976
-
2977
-
.bp-type--link.dropup:hover {
2978
-
color: var(--act-1);
2979
-
text-decoration: none;
2980
-
}
2981
-
2982
-
.bp--icon {
2983
-
text-align: center;
2984
-
font-family: 'FontAwesome 6', sans-serif;
2985
-
font-weight: 400;
2986
-
display: inline-block;
2987
-
}
2988
-
2989
-
.bp--icon.brand {
2990
-
font-family: 'FontAwesome Brands 6', sans-serif;
2991
-
}
2992
-
2993
-
.bp--icon.button {
2994
-
min-width: 1.5em;
2995
-
}
2996
-
2997
-
.bp--icon.value {
2998
-
color: var(--act-1);
2999
-
text-align: center;
3000
-
border-radius: 1rem;
3001
-
font-size: 4em;
3002
-
font-weight: 100;
3003
-
line-height: 1.25em;
3004
-
}
3005
-
3006
-
.bp--icon.sm {
3007
-
font-size: 0.75em;
3008
-
font-weight: 400;
3009
-
}
3010
-
3011
-
.sg--icon-holder {
3012
-
background-color: var(--mono-100);
3013
-
color: #04041e;
3014
-
text-align: center;
3015
-
border-radius: 0.5rem;
3016
-
padding: 0.5rem;
3017
-
}
3018
-
3019
-
.bp--keyword {
3020
-
background-color: var(--mono-97);
3021
-
color: var(--mono-25);
3022
-
cursor: default;
3023
-
transform-style: preserve-3d;
3024
-
border-radius: 2px;
3025
-
margin-bottom: 2px;
3026
-
margin-right: 2px;
3027
-
padding: 0.125em 0.5em;
3028
-
font-weight: 400;
3029
-
transition: background-color 0.1s;
3030
-
display: inline-block;
3031
-
}
3032
-
3033
-
.bp--keyword.small {
3034
-
background-color: var(--mono-35);
3035
-
color: var(--mono-100);
3036
-
padding-top: 0;
3037
-
padding-bottom: 0;
3038
-
font-size: 0.75em;
3039
-
}
3040
-
3041
-
.bp--nav-menu {
3042
-
z-index: 9999;
3043
-
grid-column-gap: 0.5em;
3044
-
grid-row-gap: 0.5em;
3045
-
background-color: var(--mono-100);
3046
-
flex-direction: column;
3047
-
align-content: flex-start;
3048
-
justify-content: space-between;
3049
-
align-items: stretch;
3050
-
width: 100svw;
3051
-
height: 100svh;
3052
-
padding: 2em;
3053
-
display: flex;
3054
-
position: fixed;
3055
-
top: 0%;
3056
-
bottom: 0%;
3057
-
left: 0%;
3058
-
right: 0%;
3059
-
}
3060
-
3061
-
.bp--global-icon {
3062
-
padding-left: 0.125rem;
3063
-
padding-right: 0.125rem;
3064
-
font-family: 'FontAwesome 6', sans-serif;
3065
-
font-weight: 300;
3066
-
}
3067
-
3068
-
.bp--dropup {
3069
-
z-index: 9999999;
3070
-
flex-direction: column;
3071
-
position: fixed;
3072
-
bottom: 1em;
3073
-
left: 1em;
3074
-
}
3075
-
3076
-
.bp--dropup-content {
3077
-
grid-column-gap: 1em;
3078
-
grid-row-gap: 1em;
3079
-
background-color: var(--mono-100);
3080
-
flex-direction: column;
3081
-
padding: 2em;
3082
-
font-size: 1em;
3083
-
display: flex;
3084
-
position: fixed;
3085
-
top: 0%;
3086
-
bottom: 0%;
3087
-
left: 0%;
3088
-
right: auto;
3089
-
overflow: hidden;
3090
-
}
3091
-
3092
-
.bp-link:hover {
3093
-
color: var(--act-2);
3094
-
}
3095
-
3096
-
.bp-avatar {
3097
-
background-color: var(--act-90);
3098
-
background-image: url('https://assets-global.website-files.com/63020a6ad41103c31cd7414e/6342ba1ac5c73022236607cb_me--site-hero-colour.jpg');
3099
-
background-position: 50%;
3100
-
background-size: cover;
3101
-
border-radius: 100%;
3102
-
width: 3em;
3103
-
height: 3em;
3104
-
}
3105
-
3106
-
.bp-avatar.large {
3107
-
width: 5em;
3108
-
height: 5em;
3109
-
}
3110
-
3111
-
.bp--plate {
3112
-
grid-column-gap: 1em;
3113
-
grid-row-gap: 1em;
3114
-
border-radius: 0;
3115
-
flex-direction: column;
3116
-
grid-template:
3117
-
'.'
3118
-
/ 1fr 0.5fr;
3119
-
grid-auto-columns: 1fr;
3120
-
grid-auto-flow: row;
3121
-
align-content: end;
3122
-
justify-content: end;
3123
-
height: 28em;
3124
-
display: grid;
3125
-
position: relative;
3126
-
overflow: hidden;
3127
-
}
3128
-
3129
-
.bp--plate:hover {
3130
-
box-shadow: none;
3131
-
text-decoration: none;
3132
-
}
3133
-
3134
-
.bp--plate:focus-visible {
3135
-
background-color: var(--mono-100);
3136
-
outline-color: var(--hi-1);
3137
-
outline-style: solid;
3138
-
}
3139
-
3140
-
.bp--plate[data-wf-focus-visible] {
3141
-
background-color: var(--mono-100);
3142
-
outline-color: var(--hi-1);
3143
-
outline-style: solid;
3144
-
}
3145
-
3146
-
.bp--plate.mini {
3147
-
grid-column-gap: 0em;
3148
-
grid-row-gap: 0em;
3149
-
grid-template-columns: 1fr;
3150
-
}
3151
-
3152
-
.bp--plate-card {
3153
-
grid-column-gap: 0em;
3154
-
grid-row-gap: 0em;
3155
-
background-color: var(--mono-100);
3156
-
box-shadow: 0 0 15px 0 var(--trans-25);
3157
-
border-radius: 0;
3158
-
flex-direction: column;
3159
-
padding: 2em 1.5em;
3160
-
text-decoration: none;
3161
-
display: flex;
3162
-
}
3163
-
3164
-
.bp--plate-card:hover {
3165
-
text-decoration: none;
3166
-
}
3167
-
3168
-
.error-message {
3169
-
background-color: var(--hi-1);
3170
-
}
3171
-
3172
-
.bp-input {
3173
-
border-style: solid;
3174
-
border-width: 1px 0 0;
3175
-
border-color: var(--mono-75) black var(--mono-25);
3176
-
border-radius: 2px;
3177
-
margin-bottom: 0;
3178
-
padding: 1.5em 1em;
3179
-
font-size: 1.25rem;
3180
-
line-height: 1.5;
3181
-
transition:
3182
-
background-color 0.1s linear,
3183
-
color 0.1s linear;
3184
-
}
3185
-
3186
-
.bp-input:hover {
3187
-
border-color: var(--mono-00);
3188
-
}
3189
-
3190
-
.bp-input:focus-visible {
3191
-
border-color: var(--mono-00);
3192
-
outline-color: var(--hi-1);
3193
-
outline-offset: 2px;
3194
-
outline-width: 3px;
3195
-
outline-style: solid;
3196
-
}
3197
-
3198
-
.bp-input[data-wf-focus-visible] {
3199
-
border-color: var(--mono-00);
3200
-
outline-color: var(--hi-1);
3201
-
outline-offset: 2px;
3202
-
outline-width: 3px;
3203
-
outline-style: solid;
3204
-
}
3205
-
3206
-
.bp-input::placeholder {
3207
-
color: var(--mono-50);
3208
-
}
3209
-
3210
-
.fill-mono-00 {
3211
-
background-color: #000;
3212
-
}
3213
-
3214
-
.fill-mono-100 {
3215
-
background-color: var(--mono-100);
3216
-
}
3217
-
3218
-
.fill-mono-50 {
3219
-
background-color: var(--mono-50);
3220
-
}
3221
-
3222
-
.fill-trans {
3223
-
background-color: rgba(0, 0, 0, 0);
3224
-
}
3225
-
3226
-
.fill-trans-50 {
3227
-
background-color: var(--trans-50);
3228
-
}
3229
-
3230
-
.fill-mono-95 {
3231
-
background-color: var(--mono-95);
3232
-
}
3233
-
3234
-
.fill-info {
3235
-
background-color: #fbb800;
3236
-
}
3237
-
3238
-
.fill-error {
3239
-
background-color: var(--err-1);
3240
-
}
3241
-
3242
-
.fill-error-low {
3243
-
background-color: var(--err-2);
3244
-
}
3245
-
3246
-
.bp-pad-1 {
3247
-
padding: 1rem;
3248
-
}
3249
-
3250
-
.bp-pad-2 {
3251
-
padding: 2rem;
3252
-
}
3253
-
3254
-
.bp-pad-4 {
3255
-
padding: 4rem;
3256
-
}
3257
-
3258
-
.bp-pad-0_5 {
3259
-
padding: 0.5rem;
3260
-
}
3261
-
3262
-
.bp-pad-0 {
3263
-
padding: 0;
3264
-
}
3265
-
3266
-
.bp-pad-0_5-v {
3267
-
padding: 0.5rem 0;
3268
-
}
3269
-
3270
-
.bp-pad-1-v {
3271
-
padding: 1rem 0;
3272
-
}
3273
-
3274
-
.bp-pad-2-v {
3275
-
padding: 2rem 0;
3276
-
}
3277
-
3278
-
.bp-pad-4-v {
3279
-
padding: 4rem 0;
3280
-
}
3281
-
3282
-
.tooltip-container {
3283
-
z-index: 9999;
3284
-
background-color: var(--mono-00);
3285
-
opacity: 0;
3286
-
color: var(--mono-100);
3287
-
white-space: nowrap;
3288
-
border-radius: 4px;
3289
-
padding: 0.5rem 0.75rem;
3290
-
font-size: 1rem;
3291
-
font-weight: 500;
3292
-
line-height: 1rem;
3293
-
transition: opacity 0.2s;
3294
-
display: none;
3295
-
position: absolute;
3296
-
}
3297
-
3298
-
.tooltip-container.visible {
3299
-
opacity: 1;
3300
-
display: block;
3301
-
}
3302
-
3303
-
.tooltip {
3304
-
cursor: default;
3305
-
display: inline-block;
3306
-
position: relative;
3307
-
}
3308
-
3309
-
.meta {
3310
-
color: var(--mono-50);
3311
-
}
3312
-
3313
-
.bp--plate-bg {
3314
-
z-index: -1;
3315
-
transform: scale3d(1none, 1none, 1none);
3316
-
object-fit: cover;
3317
-
transform-style: preserve-3d;
3318
-
width: 100%;
3319
-
height: 100%;
3320
-
transition: transform 0.425s;
3321
-
position: absolute;
3322
-
top: 0%;
3323
-
bottom: 0%;
3324
-
left: 0%;
3325
-
right: 0%;
3326
-
}
3327
-
3328
-
.fill-act-4 {
3329
-
background-color: var(--act-100);
3330
-
}
3331
-
3332
-
.fill-act-1 {
3333
-
background-color: #000ed9;
3334
-
}
3335
-
3336
-
.fill-act-3 {
3337
-
background-color: var(--act-90);
3338
-
}
3339
-
3340
-
.fill-act-2 {
3341
-
background-color: var(--act-1);
3342
-
}
3343
-
3344
-
.fill-mono-25 {
3345
-
background-color: var(--mono-25);
3346
-
}
3347
-
3348
-
.bp--section-hero {
3349
-
background-color: var(--mono-100);
3350
-
padding-top: 2em;
3351
-
padding-bottom: 2em;
3352
-
}
3353
-
3354
-
.bp--section-hero.portfolio {
3355
-
background-color: var(--mono-00);
3356
-
color: var(--mono-100);
3357
-
background-image: url('https://d3e54v103j8qbb.cloudfront.net/img/background-image.svg');
3358
-
background-position: 50%;
3359
-
background-repeat: no-repeat;
3360
-
background-size: cover;
3361
-
}
3362
-
3363
-
.bp--job {
3364
-
grid-column-gap: 0.5em;
3365
-
grid-row-gap: 0.5em;
3366
-
background-color: var(--mono-100);
3367
-
text-align: left;
3368
-
flex-direction: column;
3369
-
justify-content: center;
3370
-
padding: 2em;
3371
-
text-decoration: none;
3372
-
display: flex;
3373
-
}
3374
-
3375
-
.bp--job:hover {
3376
-
text-decoration: none;
3377
-
}
3378
-
3379
-
.bp--value {
3380
-
grid-column-gap: 0.5em;
3381
-
grid-row-gap: 0.5em;
3382
-
background-color: var(--mono-100);
3383
-
text-align: center;
3384
-
flex-direction: column;
3385
-
justify-content: center;
3386
-
padding: 2em 1em;
3387
-
text-decoration: none;
3388
-
display: flex;
3389
-
}
3390
-
3391
-
.bp--value:hover {
3392
-
text-decoration: none;
3393
-
}
3394
-
3395
-
.bp-block-inline {
3396
-
display: inline-block;
3397
-
}
3398
-
3399
-
.hopper {
3400
-
height: 1px;
3401
-
}
3402
-
3403
-
.bp--button-group {
3404
-
grid-column-gap: 1rem;
3405
-
grid-row-gap: 1rem;
3406
-
flex-direction: row;
3407
-
align-items: stretch;
3408
-
padding-top: 0.5em;
3409
-
padding-bottom: 0;
3410
-
display: flex;
3411
-
}
3412
-
3413
-
.bp--button-group.mini {
3414
-
font-size: 0.75em;
3415
-
}
3416
-
3417
-
.bp--plate-card-content {
3418
-
grid-column-gap: 0.5em;
3419
-
grid-row-gap: 0.5em;
3420
-
flex-direction: column;
3421
-
display: flex;
3422
-
}
3423
-
3424
-
.bp--identity {
3425
-
grid-column-gap: 1em;
3426
-
grid-row-gap: 1em;
3427
-
align-items: center;
3428
-
display: flex;
3429
-
}
3430
-
3431
-
.bp--container {
3432
-
max-width: 940px;
3433
-
margin-left: auto;
3434
-
margin-right: auto;
3435
-
}
3436
-
3437
-
.bp--card {
3438
-
grid-column-gap: 0.5em;
3439
-
grid-row-gap: 0.5em;
3440
-
background-color: var(--mono-100);
3441
-
text-align: left;
3442
-
flex-direction: column;
3443
-
justify-content: center;
3444
-
padding: 2em;
3445
-
text-decoration: none;
3446
-
display: flex;
3447
-
}
3448
-
3449
-
.bp--card:hover {
3450
-
text-decoration: none;
3451
-
}
3452
-
3453
-
.bp--container-max {
3454
-
grid-column-gap: 1em;
3455
-
grid-row-gap: 1em;
3456
-
border-radius: 2px;
3457
-
flex-direction: column;
3458
-
width: 100%;
3459
-
display: flex;
3460
-
}
3461
-
3462
-
.bp--container-max.hero {
3463
-
max-width: 1024px;
3464
-
}
3465
-
3466
-
.bp--container-full {
3467
-
grid-column-gap: 3em;
3468
-
grid-row-gap: 3em;
3469
-
grid-template-rows: auto;
3470
-
grid-template-columns: 1fr 1fr;
3471
-
grid-auto-columns: 1fr;
3472
-
grid-auto-flow: row;
3473
-
justify-content: space-between;
3474
-
width: 100%;
3475
-
max-width: 100%;
3476
-
margin-left: auto;
3477
-
margin-right: auto;
3478
-
padding-left: 2em;
3479
-
padding-right: 2em;
3480
-
display: grid;
3481
-
}
3482
-
3483
-
.bp--container-collection_source {
3484
-
width: 100%;
3485
-
max-width: 100%;
3486
-
margin-left: auto;
3487
-
margin-right: auto;
3488
-
padding-left: 2em;
3489
-
padding-right: 2em;
3490
-
display: none;
3491
-
}
3492
-
3493
-
.bp--prev {
3494
-
grid-column-gap: 0rem;
3495
-
grid-row-gap: 0rem;
3496
-
border-radius: 2px;
3497
-
flex-direction: column;
3498
-
grid-template-rows: auto auto;
3499
-
grid-template-columns: 1fr;
3500
-
grid-auto-columns: 1fr;
3501
-
justify-content: start;
3502
-
display: grid;
3503
-
}
3504
-
3505
-
.bp--next {
3506
-
grid-column-gap: 0rem;
3507
-
grid-row-gap: 0rem;
3508
-
border-radius: 2px;
3509
-
flex-direction: column;
3510
-
grid-template-rows: auto auto;
3511
-
grid-template-columns: 1fr;
3512
-
grid-auto-columns: 1fr;
3513
-
justify-content: end;
3514
-
display: grid;
3515
-
}
3516
-
3517
-
.bp--prev_next-wrapper {
3518
-
grid-column-gap: 1rem;
3519
-
grid-row-gap: 1rem;
3520
-
border-radius: 2px;
3521
-
flex-direction: column;
3522
-
align-items: stretch;
3523
-
display: block;
3524
-
}
3525
-
3526
-
.bp--footer {
3527
-
background-color: var(--mono-97);
3528
-
border-bottom: 1px solid #e4ebf3;
3529
-
margin-top: 4rem;
3530
-
margin-left: auto;
3531
-
margin-right: auto;
3532
-
position: relative;
3533
-
}
3534
-
3535
-
.bp--footer-col {
3536
-
flex-direction: column;
3537
-
justify-content: flex-start;
3538
-
align-items: flex-start;
3539
-
display: flex;
3540
-
}
3541
-
3542
-
.bp--divider {
3543
-
background-color: var(--mono-75);
3544
-
width: 100%;
3545
-
height: 1px;
3546
-
margin-top: 70px;
3547
-
margin-bottom: 15px;
3548
-
}
3549
-
3550
-
.bp--footer-grid {
3551
-
grid-column-gap: 1em;
3552
-
grid-row-gap: 1em;
3553
-
grid-template-rows: auto;
3554
-
grid-template-columns: 2fr 1fr 1fr;
3555
-
grid-auto-columns: 1fr;
3556
-
display: grid;
3557
-
}
3558
-
3559
-
.bp--badge {
3560
-
background-color: var(--mono-92);
3561
-
color: var(--mono-50);
3562
-
border-radius: 2px;
3563
-
padding-left: 0.5em;
3564
-
padding-right: 0.5em;
3565
-
font-size: 0.75em;
3566
-
line-height: 1.8em;
3567
-
display: inline-block;
3568
-
}
3569
-
3570
-
.bp--empty-state {
3571
-
background-color: var(--mono-92);
3572
-
padding: 1em;
3573
-
}
3574
-
3575
-
.fill-mono-35 {
3576
-
background-color: var(--mono-35);
3577
-
}
3578
-
3579
-
.fill-mono-75 {
3580
-
background-color: var(--mono-75);
3581
-
}
3582
-
3583
-
.fill-mono-85 {
3584
-
background-color: var(--mono-85);
3585
-
}
3586
-
3587
-
.fill-mono-92 {
3588
-
background-color: var(--mono-92);
3589
-
}
3590
-
3591
-
.fill-mono-97 {
3592
-
background-color: var(--mono-97);
3593
-
}
3594
-
3595
-
.bp--main {
3596
-
flex-direction: column;
3597
-
display: flex;
3598
-
}
3599
-
3600
-
.bp--navbar {
3601
-
width: 240px;
3602
-
max-width: 400px;
3603
-
height: 100vh;
3604
-
padding-top: 2em;
3605
-
padding-left: 1em;
3606
-
padding-right: 1em;
3607
-
font-size: 0.75em;
3608
-
position: -webkit-sticky;
3609
-
position: sticky;
3610
-
top: 0%;
3611
-
bottom: 0%;
3612
-
left: 0%;
3613
-
right: auto;
3614
-
}
3615
-
3616
-
.bp--navbar.hero {
3617
-
grid-column-gap: 1px;
3618
-
grid-row-gap: 1px;
3619
-
background-color: var(--mono-100);
3620
-
flex-direction: row;
3621
-
justify-content: space-between;
3622
-
align-items: flex-start;
3623
-
padding-top: 1em;
3624
-
padding-bottom: 1em;
3625
-
display: flex;
3626
-
}
3627
-
3628
-
.bp--navbar.h {
3629
-
z-index: 999998;
3630
-
background-color: var(--mono-100);
3631
-
box-shadow: 0 4px 16px -3px var(--trans-50);
3632
-
padding-top: 0.125rem;
3633
-
padding-bottom: 0.125rem;
3634
-
position: -webkit-sticky;
3635
-
position: sticky;
3636
-
top: 0%;
3637
-
bottom: auto;
3638
-
left: 0%;
3639
-
right: 0%;
3640
-
}
3641
-
3642
-
.bp--navbar.nav-callout {
3643
-
z-index: 999999;
3644
-
background-color: var(--hi-2);
3645
-
}
3646
-
3647
-
.bp--navbar.v {
3648
-
z-index: 999998;
3649
-
background-color: var(--mono-100);
3650
-
box-shadow: 0 4px 16px -3px var(--trans-50);
3651
-
flex-direction: column;
3652
-
padding-top: 0.125rem;
3653
-
padding-bottom: 0.125rem;
3654
-
display: flex;
3655
-
position: -webkit-sticky;
3656
-
position: sticky;
3657
-
top: 0%;
3658
-
bottom: auto;
3659
-
left: 0%;
3660
-
right: 0%;
3661
-
}
3662
-
3663
-
.bp--scaffold {
3664
-
width: 100vw;
3665
-
display: flex;
3666
-
}
3667
-
3668
-
.bp--col-copy {
3669
-
grid-column-gap: 1rem;
3670
-
grid-row-gap: 1rem;
3671
-
flex-direction: row;
3672
-
align-items: stretch;
3673
-
display: flex;
3674
-
}
3675
-
3676
-
.bp--col-copy.icons {
3677
-
grid-column-gap: 1rem;
3678
-
grid-row-gap: 1rem;
3679
-
flex-direction: row;
3680
-
grid-template-rows: auto;
3681
-
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
3682
-
grid-auto-columns: 1fr;
3683
-
width: 100%;
3684
-
display: flex;
3685
-
}
3686
-
3687
-
.bp--col-copy.keywords {
3688
-
display: block;
3689
-
}
3690
-
3691
-
.bp--col-copy.menu {
3692
-
grid-column-gap: 0.5em;
3693
-
grid-row-gap: 0.5em;
3694
-
flex-direction: row;
3695
-
flex: 1;
3696
-
grid-template-rows: auto;
3697
-
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
3698
-
grid-auto-columns: 1fr;
3699
-
align-items: center;
3700
-
display: flex;
3701
-
}
3702
-
3703
-
.bp--col-copy.menu.trigger {
3704
-
display: none;
3705
-
}
3706
-
3707
-
.bp--col-copy.menu.right {
3708
-
justify-content: flex-end;
3709
-
}
3710
-
3711
-
.bp--col-copy.menu.left {
3712
-
grid-column-gap: 0.125em;
3713
-
grid-row-gap: 0.125em;
3714
-
flex: 0 auto;
3715
-
}
3716
-
3717
-
.bp--col-copy.large-gap {
3718
-
grid-column-gap: 2rem;
3719
-
grid-row-gap: 2rem;
3720
-
}
3721
-
3722
-
.bp--col-copy.small-gap {
3723
-
grid-column-gap: 0.5rem;
3724
-
grid-row-gap: 0.5rem;
3725
-
}
3726
-
3727
-
.bp--col-copy.no-gap {
3728
-
grid-column-gap: 0rem;
3729
-
grid-row-gap: 0rem;
3730
-
}
3731
-
3732
-
.bp--col-copy.nav-bar {
3733
-
justify-content: space-between;
3734
-
max-width: 100%;
3735
-
padding-top: 0.5em;
3736
-
padding-bottom: 0.5em;
3737
-
font-size: 0.625em;
3738
-
}
3739
-
3740
-
.bp--col-copy.fieldset {
3741
-
flex-direction: row;
3742
-
align-items: stretch;
3743
-
}
3744
-
3745
-
.bp--col-copy.right {
3746
-
justify-content: flex-end;
3747
-
}
3748
-
3749
-
.bp--col-copy.align-m {
3750
-
grid-column-gap: 0.25em;
3751
-
grid-row-gap: 0.25em;
3752
-
align-items: center;
3753
-
}
3754
-
3755
-
.surface {
3756
-
z-index: 999999;
3757
-
background-color: var(--mono-100);
3758
-
box-shadow: 0 4px 12px 0 var(--trans-25);
3759
-
position: -webkit-sticky;
3760
-
position: sticky;
3761
-
top: 0;
3762
-
}
3763
-
3764
-
@media screen and (min-width: 1280px) {
3765
-
.bp--keyword:hover {
3766
-
transform: scale3d(1none, 1none, 1none);
3767
-
}
3768
-
}
3769
-
3770
-
@media screen and (max-width: 991px) {
3771
-
.bp--container-2rem {
3772
-
grid-column-gap: 2em;
3773
-
grid-row-gap: 2em;
3774
-
}
3775
-
3776
-
.bp--body,
3777
-
.bp--site-body {
3778
-
font-size: 20px;
3779
-
}
3780
-
3781
-
.bp--plate {
3782
-
grid-template-columns: 1fr;
3783
-
grid-auto-columns: 5fr;
3784
-
display: flex;
3785
-
}
3786
-
3787
-
.bp--badge {
3788
-
font-size: 20px;
3789
-
}
3790
-
}
3791
-
3792
-
@media screen and (max-width: 767px) {
3793
-
.bp--button.icon {
3794
-
text-align: center;
3795
-
padding-left: 0.75rem;
3796
-
padding-right: 0.75rem;
3797
-
}
3798
-
3799
-
.bp--section {
3800
-
padding-left: 1em;
3801
-
padding-right: 1em;
3802
-
}
3803
-
3804
-
.bp--container-2rem {
3805
-
grid-column-gap: 1em;
3806
-
grid-row-gap: 1em;
3807
-
padding-left: 1em;
3808
-
padding-right: 1em;
3809
-
}
3810
-
3811
-
.bp--body,
3812
-
.bp--site-body {
3813
-
font-size: 16px;
3814
-
}
3815
-
3816
-
.bp-avatar.large {
3817
-
width: 6rem;
3818
-
height: 6rem;
3819
-
}
3820
-
3821
-
.bp--footer {
3822
-
padding-left: 15px;
3823
-
padding-right: 15px;
3824
-
}
3825
-
3826
-
.bp--footer-col {
3827
-
align-items: center;
3828
-
}
3829
-
3830
-
.bp--divider {
3831
-
margin-top: 60px;
3832
-
}
3833
-
3834
-
.bp--badge {
3835
-
font-size: 16px;
3836
-
}
3837
-
3838
-
.bp--navbar {
3839
-
padding-left: 1em;
3840
-
padding-right: 1em;
3841
-
}
3842
-
}
3843
-
3844
-
@media screen and (max-width: 479px) {
3845
-
.bp--button.primary.mobile {
3846
-
display: block;
3847
-
}
3848
-
3849
-
.bp--button.primary.desktop {
3850
-
display: none;
3851
-
}
3852
-
3853
-
.bp--quote {
3854
-
padding-left: 0.5em;
3855
-
padding-right: 1em;
3856
-
}
3857
-
3858
-
.bp--site-body {
3859
-
font-size: 14px;
3860
-
}
3861
-
3862
-
.bp--dropup {
3863
-
bottom: 4em;
3864
-
right: 1em;
3865
-
}
3866
-
3867
-
.bp-avatar.large {
3868
-
width: 4rem;
3869
-
height: 4rem;
3870
-
}
3871
-
3872
-
.bp--plate {
3873
-
padding: 0 1em 1em;
3874
-
}
3875
-
3876
-
.bp--plate:hover {
3877
-
transform: scale(1.01);
3878
-
}
3879
-
3880
-
.bp--plate-card {
3881
-
padding-top: 1.5em;
3882
-
padding-left: 1.5em;
3883
-
padding-right: 1.5em;
3884
-
}
3885
-
3886
-
.bp-input {
3887
-
font-size: 1.5rem;
3888
-
}
3889
-
3890
-
.bp--job,
3891
-
.bp--value,
3892
-
.bp--card {
3893
-
padding-top: 1.5em;
3894
-
padding-left: 1.5em;
3895
-
padding-right: 1.5em;
3896
-
}
3897
-
}
3898
-
3899
-
#w-node-_399d947d-56b1-e710-5746-a0a5f0039a87-9fd74154,
3900
-
#w-node-_399d947d-56b1-e710-5746-a0a5f0039a82-9fd74154,
3901
-
#w-node-_399d947d-56b1-e710-5746-a0a5f0039a8c-9fd74154,
3902
-
#w-node-_399d947d-56b1-e710-5746-a0a5f0039a91-9fd74154,
3903
-
#w-node-_041de344-4f4b-53e4-1bfd-fb069093f632-9093f623,
3904
-
#w-node-_041de344-4f4b-53e4-1bfd-fb069093f63d-9093f623,
3905
-
#w-node-_4c308b66-d1d3-2bd2-33c3-6d4a8eb213b0-79ded8ce,
3906
-
#w-node-_692c258b-e6b3-6ae4-a71f-374b5ed108ec-22d74155,
3907
-
#w-node-_27d964d7-71bf-8ce5-1e1d-4b972b3ba478-22d74155,
3908
-
#w-node-_7466db0e-81a6-9bbe-98ec-b431b3344db5-22d74155,
3909
-
#w-node-_817dc116-5c27-258c-e786-af444fa08762-22d74155,
3910
-
#w-node-_38676560-e717-595a-5094-877f49e544a0-49e5449f,
3911
-
#w-node-_9b92584f-99b9-23be-771a-34fd65faf4d4-a9745761,
3912
-
#w-node-_6ae5c451-b99c-2666-8289-3a462e35fd71-2e35fd71,
3913
-
#w-node-c87483e8-cc45-6b38-841c-2f17ba12af6d-2e35fd71 {
3914
-
grid-area: span 1 / span 1 / span 1 / span 1;
3915
-
}
3916
-
3917
-
@font-face {
3918
-
font-family: 'FontAwesome 6';
3919
-
src:
3920
-
url('https://uploads-ssl.webflow.com/63020a6ad41103c31cd7414e/63020a6ad41103330ed7417a_fa-regular-400.woff2')
3921
-
format('woff2'),
3922
-
url('https://uploads-ssl.webflow.com/63020a6ad41103c31cd7414e/63020a6ad41103db67d7417e_fa-regular-400.ttf')
3923
-
format('truetype');
3924
-
font-weight: 400;
3925
-
font-style: normal;
3926
-
font-display: auto;
3927
-
}
3928
-
3929
-
@font-face {
3930
-
font-family: 'FontAwesome 6';
3931
-
src:
3932
-
url('https://uploads-ssl.webflow.com/63020a6ad41103c31cd7414e/63020a6ad41103794bd7418c_fa-solid-900.woff2')
3933
-
format('woff2'),
3934
-
url('https://uploads-ssl.webflow.com/63020a6ad41103c31cd7414e/63020a6ad411033fc7d7417d_fa-solid-900.ttf')
3935
-
format('truetype');
3936
-
font-weight: 900;
3937
-
font-style: normal;
3938
-
font-display: auto;
3939
-
}
3940
-
3941
-
@font-face {
3942
-
font-family: 'FontAwesome Brands 6';
3943
-
src:
3944
-
url('https://uploads-ssl.webflow.com/63020a6ad41103c31cd7414e/63020a6ad41103fe94d74178_fa-brands-400.woff2')
3945
-
format('woff2'),
3946
-
url('https://uploads-ssl.webflow.com/63020a6ad41103c31cd7414e/63020a6ad411037fcdd7417c_fa-brands-400.ttf')
3947
-
format('truetype');
3948
-
font-weight: 400;
3949
-
font-style: normal;
3950
-
font-display: auto;
3951
-
}
3952
-
3953
-
@font-face {
3954
-
font-family: 'FontAwesome 6';
3955
-
src:
3956
-
url('https://uploads-ssl.webflow.com/63020a6ad41103c31cd7414e/63020a6ad411039933d74179_fa-thin-100.woff2')
3957
-
format('woff2'),
3958
-
url('https://uploads-ssl.webflow.com/63020a6ad41103c31cd7414e/63020a6ad411038365d7418e_fa-thin-100.ttf')
3959
-
format('truetype');
3960
-
font-weight: 100;
3961
-
font-style: normal;
3962
-
font-display: auto;
3963
-
}
3964
-
3965
-
@font-face {
3966
-
font-family: 'FontAwesome 6';
3967
-
src:
3968
-
url('https://uploads-ssl.webflow.com/63020a6ad41103c31cd7414e/63020a6ad411031febd7417b_fa-light-300.woff2')
3969
-
format('woff2'),
3970
-
url('https://uploads-ssl.webflow.com/63020a6ad41103c31cd7414e/63020a6ad41103530ed7418d_fa-light-300.ttf')
3971
-
format('truetype');
3972
-
font-weight: 300;
3973
-
font-style: normal;
3974
-
font-display: auto;
3975
-
}
-139
src/hooks/atproto/fetchPublications.ts
-139
src/hooks/atproto/fetchPublications.ts
···
1
-
import { useEffect, useState } from 'react';
2
-
import { ATPROTO_COLLECTIONS, buildBlobUrl } from '@/config/atproto';
3
-
import type { ATProtocolDocument, ATProtocolPublication, FetchResult } from '@/types/atproto';
4
-
import { fetchRecords } from './fetchRecords';
5
-
6
-
/**
7
-
* Publication data with resolved metadata
8
-
*/
9
-
export interface Publication {
10
-
uri: string;
11
-
name: string;
12
-
basePath: string;
13
-
icon?: string;
14
-
description?: string;
15
-
}
16
-
17
-
/**
18
-
* Document data with resolved publication metadata
19
-
*/
20
-
export interface Document {
21
-
uri: string;
22
-
slug: string;
23
-
title: string;
24
-
description?: string;
25
-
publishedAt: string;
26
-
articleUrl: string;
27
-
publication: Publication;
28
-
}
29
-
30
-
/**
31
-
* Hook for fetching publications and their associated documents
32
-
* Automatically resolves publication references in documents
33
-
*
34
-
* @returns FetchResult with combined documents and publications data
35
-
*
36
-
* @example
37
-
* ```tsx
38
-
* const { data: posts, loading, error } = fetchPublications();
39
-
*
40
-
* if (loading) return <div>Loading...</div>;
41
-
* if (error) return <div>Error: {error}</div>;
42
-
*
43
-
* return posts?.map(post => (
44
-
* <article key={post.uri}>
45
-
* <h2>{post.title}</h2>
46
-
* <p>{post.publication.name}</p>
47
-
* </article>
48
-
* ));
49
-
* ```
50
-
*/
51
-
export function fetchPublications(): FetchResult<Document[]> {
52
-
const {
53
-
data: publicationsData,
54
-
loading: publicationsLoading,
55
-
error: publicationsError,
56
-
} = fetchRecords<ATProtocolPublication['value']>(ATPROTO_COLLECTIONS.PUBLICATION);
57
-
58
-
const {
59
-
data: documentsData,
60
-
loading: documentsLoading,
61
-
error: documentsError,
62
-
} = fetchRecords<ATProtocolDocument['value']>(ATPROTO_COLLECTIONS.DOCUMENT);
63
-
64
-
const [data, setData] = useState<Document[] | null>(null);
65
-
const [loading, setLoading] = useState(true);
66
-
const [error, setError] = useState<string | null>(null);
67
-
68
-
useEffect(() => {
69
-
// Wait for both fetches to complete
70
-
if (publicationsLoading || documentsLoading) {
71
-
setLoading(true);
72
-
return;
73
-
}
74
-
75
-
// Check for errors
76
-
if (publicationsError || documentsError) {
77
-
setError(publicationsError || documentsError);
78
-
setLoading(false);
79
-
return;
80
-
}
81
-
82
-
// Check if we have data
83
-
if (!publicationsData || !documentsData) {
84
-
setLoading(false);
85
-
return;
86
-
}
87
-
88
-
try {
89
-
// Create publication lookup map
90
-
const publicationMap = new Map<string, Publication>();
91
-
publicationsData.forEach((record) => {
92
-
const iconUrl = record.value.icon ? buildBlobUrl(record.value.icon.ref.$link) : undefined;
93
-
94
-
publicationMap.set(record.uri, {
95
-
uri: record.uri,
96
-
name: record.value.name,
97
-
basePath: record.value.base_path,
98
-
icon: iconUrl,
99
-
description: record.value.description,
100
-
});
101
-
});
102
-
103
-
// Transform documents with resolved publication data
104
-
const documents: Document[] = documentsData
105
-
.map((record) => {
106
-
const slug = record.uri.split('/').pop() || '';
107
-
const publication = publicationMap.get(record.value.publication);
108
-
109
-
if (!publication) {
110
-
console.warn(`Publication not found for document: ${record.uri}`);
111
-
return null;
112
-
}
113
-
114
-
return {
115
-
uri: record.uri,
116
-
slug,
117
-
title: record.value.title,
118
-
description: record.value.description,
119
-
publishedAt: record.value.publishedAt,
120
-
articleUrl: `https://${publication.basePath}/${slug}`,
121
-
publication,
122
-
};
123
-
})
124
-
.filter((doc): doc is Document => doc !== null)
125
-
// Sort by published date, newest first
126
-
.sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime());
127
-
128
-
setData(documents);
129
-
setError(null);
130
-
} catch (err) {
131
-
console.error('Error processing publications and documents:', err);
132
-
setError(err instanceof Error ? err.message : 'An error occurred');
133
-
} finally {
134
-
setLoading(false);
135
-
}
136
-
}, [publicationsData, documentsData, publicationsLoading, documentsLoading, publicationsError, documentsError]);
137
-
138
-
return { data, loading, error };
139
-
}
-51
src/hooks/atproto/fetchRecords.ts
-51
src/hooks/atproto/fetchRecords.ts
···
1
-
import { useEffect, useState } from 'react';
2
-
import { buildRecordsUrl } from '@/config/atproto';
3
-
import type { ATProtocolRecord, FetchResult, ListRecordsResponse } from '@/types/atproto';
4
-
5
-
/**
6
-
* Generic hook for fetching records from any AT Protocol collection
7
-
*
8
-
* @param collection - The collection identifier (e.g., 'pub.leaflet.publication')
9
-
* @param repo - Optional repo DID (defaults to configured DID)
10
-
* @returns FetchResult with records data, loading state, and error
11
-
*
12
-
* @example
13
-
* ```tsx
14
-
* const { data: publications, loading, error } = fetchRecords<PublicationValue>(
15
-
* 'pub.leaflet.publication'
16
-
* );
17
-
* ```
18
-
*/
19
-
export function fetchRecords<T = unknown>(collection: string, repo?: string): FetchResult<ATProtocolRecord<T>[]> {
20
-
const [data, setData] = useState<ATProtocolRecord<T>[] | null>(null);
21
-
const [loading, setLoading] = useState(true);
22
-
const [error, setError] = useState<string | null>(null);
23
-
24
-
useEffect(() => {
25
-
const fetchRecords = async () => {
26
-
try {
27
-
setLoading(true);
28
-
setError(null);
29
-
30
-
const url = buildRecordsUrl(collection, repo);
31
-
const response = await fetch(url);
32
-
33
-
if (!response.ok) {
34
-
throw new Error(`Failed to fetch records from collection: ${collection}`);
35
-
}
36
-
37
-
const data: ListRecordsResponse<T> = await response.json();
38
-
setData(data.records);
39
-
} catch (err) {
40
-
console.error(`Error fetching AT Protocol records from ${collection}:`, err);
41
-
setError(err instanceof Error ? err.message : 'An error occurred');
42
-
} finally {
43
-
setLoading(false);
44
-
}
45
-
};
46
-
47
-
fetchRecords();
48
-
}, [collection, repo]);
49
-
50
-
return { data, loading, error };
51
-
}
+5
-3
src/hooks/atproto/index.ts
+5
-3
src/hooks/atproto/index.ts
···
2
2
* AT Protocol hooks exports
3
3
*/
4
4
5
-
export { fetchPublications } from './fetchPublications';
6
-
export type { Document, Publication } from './fetchPublications';
7
-
export { fetchRecords } from './fetchRecords';
5
+
export { useLeaflet } from './useLeaflet';
6
+
export type { Document, Publication } from './useLeaflet';
7
+
export { useProtopro } from './useProtopro';
8
+
export type { Profile } from './useProtopro';
9
+
export { useRecords } from './useRecords';
+140
src/hooks/atproto/useLeaflet.ts
+140
src/hooks/atproto/useLeaflet.ts
···
1
+
import { useEffect, useState } from 'react';
2
+
import { ATPROTO_COLLECTIONS, buildBlobUrl } from '@/config/atproto';
3
+
import type { ATProtocolDocument, ATProtocolPublication, FetchResult } from '@/types/atproto';
4
+
import { useRecords } from './useRecords';
5
+
6
+
/**
7
+
* Publication data with resolved metadata
8
+
*/
9
+
export interface Publication {
10
+
uri: string;
11
+
name: string;
12
+
basePath: string;
13
+
icon?: string;
14
+
description?: string;
15
+
}
16
+
17
+
/**
18
+
* Document data with resolved publication metadata
19
+
*/
20
+
export interface Document {
21
+
uri: string;
22
+
slug: string;
23
+
title: string;
24
+
description?: string;
25
+
publishedAt: string;
26
+
articleUrl: string;
27
+
publication: Publication;
28
+
}
29
+
30
+
/**
31
+
* Hook for fetching Leaflet publications and their associated documents
32
+
* Fetches from pub.leaflet.publication and pub.leaflet.document collections
33
+
* Automatically resolves publication references in documents
34
+
*
35
+
* @returns FetchResult with combined documents and publications data
36
+
*
37
+
* @example
38
+
* ```tsx
39
+
* const { data: posts, loading, error } = useLeaflet();
40
+
*
41
+
* if (loading) return <div>Loading...</div>;
42
+
* if (error) return <div>Error: {error}</div>;
43
+
*
44
+
* return posts?.map(post => (
45
+
* <article key={post.uri}>
46
+
* <h2>{post.title}</h2>
47
+
* <p>{post.publication.name}</p>
48
+
* </article>
49
+
* ));
50
+
* ```
51
+
*/
52
+
export function useLeaflet(): FetchResult<Document[]> {
53
+
const {
54
+
data: publicationsData,
55
+
loading: publicationsLoading,
56
+
error: publicationsError,
57
+
} = useRecords<ATProtocolPublication['value']>(ATPROTO_COLLECTIONS.PUBLICATION);
58
+
59
+
const {
60
+
data: documentsData,
61
+
loading: documentsLoading,
62
+
error: documentsError,
63
+
} = useRecords<ATProtocolDocument['value']>(ATPROTO_COLLECTIONS.DOCUMENT);
64
+
65
+
const [data, setData] = useState<Document[] | null>(null);
66
+
const [loading, setLoading] = useState(true);
67
+
const [error, setError] = useState<string | null>(null);
68
+
69
+
useEffect(() => {
70
+
// Wait for both fetches to complete
71
+
if (publicationsLoading || documentsLoading) {
72
+
setLoading(true);
73
+
return;
74
+
}
75
+
76
+
// Check for errors
77
+
if (publicationsError || documentsError) {
78
+
setError(publicationsError || documentsError);
79
+
setLoading(false);
80
+
return;
81
+
}
82
+
83
+
// Check if we have data
84
+
if (!publicationsData || !documentsData) {
85
+
setLoading(false);
86
+
return;
87
+
}
88
+
89
+
try {
90
+
// Create publication lookup map
91
+
const publicationMap = new Map<string, Publication>();
92
+
publicationsData.forEach((record) => {
93
+
const iconUrl = record.value.icon ? buildBlobUrl(record.value.icon.ref.$link) : undefined;
94
+
95
+
publicationMap.set(record.uri, {
96
+
uri: record.uri,
97
+
name: record.value.name,
98
+
basePath: record.value.base_path,
99
+
icon: iconUrl,
100
+
description: record.value.description,
101
+
});
102
+
});
103
+
104
+
// Transform documents with resolved publication data
105
+
const documents: Document[] = documentsData
106
+
.map((record) => {
107
+
const slug = record.uri.split('/').pop() || '';
108
+
const publication = publicationMap.get(record.value.publication);
109
+
110
+
if (!publication) {
111
+
console.warn(`Publication not found for document: ${record.uri}`);
112
+
return null;
113
+
}
114
+
115
+
return {
116
+
uri: record.uri,
117
+
slug,
118
+
title: record.value.title,
119
+
description: record.value.description,
120
+
publishedAt: record.value.publishedAt,
121
+
articleUrl: `https://${publication.basePath}/${slug}`,
122
+
publication,
123
+
} as Document;
124
+
})
125
+
.filter((doc): doc is Document => doc !== null)
126
+
// Sort by published date, newest first
127
+
.sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime());
128
+
129
+
setData(documents);
130
+
setError(null);
131
+
} catch (err) {
132
+
console.error('Error processing publications and documents:', err);
133
+
setError(err instanceof Error ? err.message : 'An error occurred');
134
+
} finally {
135
+
setLoading(false);
136
+
}
137
+
}, [publicationsData, documentsData, publicationsLoading, documentsLoading, publicationsError, documentsError]);
138
+
139
+
return { data, loading, error };
140
+
}
+89
src/hooks/atproto/useProtopro.ts
+89
src/hooks/atproto/useProtopro.ts
···
1
+
import { useEffect, useState } from 'react';
2
+
import type { FetchResult, JobHistoryEntry, LanguageProficiency, ProfileValue } from '@/types/atproto';
3
+
import { useRecords } from './useRecords';
4
+
5
+
/**
6
+
* Profile data from Protopro collection
7
+
*/
8
+
export interface Profile {
9
+
overview?: string;
10
+
jobHistory: JobHistoryEntry[];
11
+
languages: LanguageProficiency[];
12
+
skills: string[];
13
+
educationHistory: unknown[];
14
+
}
15
+
16
+
/**
17
+
* Hook for fetching Protopro actor profile from AT Protocol
18
+
* Fetches from blue.protopro.actor.profile collection
19
+
* Returns: overview, jobHistory, languages, skills, educationHistory
20
+
*
21
+
* @returns FetchResult with profile data
22
+
*
23
+
* @example
24
+
* ```tsx
25
+
* const { data: profile, loading, error } = useProtopro();
26
+
*
27
+
* if (loading) return <div>Loading...</div>;
28
+
* if (error) return <div>Error: {error}</div>;
29
+
*
30
+
* return (
31
+
* <div>
32
+
* {profile?.jobHistory.map(job => <JobCard key={job.company} job={job} />)}
33
+
* </div>
34
+
* );
35
+
* ```
36
+
*/
37
+
export function useProtopro(): FetchResult<Profile | null> {
38
+
const {
39
+
data: profileData,
40
+
loading: profileLoading,
41
+
error: profileError,
42
+
} = useRecords<ProfileValue>('blue.protopro.actor.profile');
43
+
44
+
const [data, setData] = useState<Profile | null>(null);
45
+
const [loading, setLoading] = useState(true);
46
+
const [error, setError] = useState<string | null>(null);
47
+
48
+
useEffect(() => {
49
+
if (profileLoading) {
50
+
setLoading(true);
51
+
return;
52
+
}
53
+
54
+
if (profileError) {
55
+
setError(profileError);
56
+
setLoading(false);
57
+
return;
58
+
}
59
+
60
+
if (!profileData || profileData.length === 0) {
61
+
setLoading(false);
62
+
return;
63
+
}
64
+
65
+
try {
66
+
// Get the first (and likely only) profile record
67
+
const profileRecord = profileData[0];
68
+
const profileValue = profileRecord.value;
69
+
70
+
const profile: Profile = {
71
+
overview: profileValue.overview,
72
+
jobHistory: profileValue.jobHistory || [],
73
+
languages: profileValue.languages || [],
74
+
skills: profileValue.skills || [],
75
+
educationHistory: profileValue.educationHistory || [],
76
+
};
77
+
78
+
setData(profile);
79
+
setError(null);
80
+
} catch (err) {
81
+
console.error('Error processing profile data:', err);
82
+
setError(err instanceof Error ? err.message : 'An error occurred');
83
+
} finally {
84
+
setLoading(false);
85
+
}
86
+
}, [profileData, profileLoading, profileError]);
87
+
88
+
return { data, loading, error };
89
+
}
+51
src/hooks/atproto/useRecords.ts
+51
src/hooks/atproto/useRecords.ts
···
1
+
import { useEffect, useState } from 'react';
2
+
import { buildRecordsUrl } from '@/config/atproto';
3
+
import type { ATProtocolRecord, FetchResult, ListRecordsResponse } from '@/types/atproto';
4
+
5
+
/**
6
+
* Generic hook for fetching records from any AT Protocol collection
7
+
*
8
+
* @param collection - The collection identifier (e.g., 'pub.leaflet.publication')
9
+
* @param repo - Optional repo DID (defaults to configured DID)
10
+
* @returns FetchResult with records data, loading state, and error
11
+
*
12
+
* @example
13
+
* ```tsx
14
+
* const { data: publications, loading, error } = useRecords<PublicationValue>(
15
+
* 'pub.leaflet.publication'
16
+
* );
17
+
* ```
18
+
*/
19
+
export function useRecords<T = unknown>(collection: string, repo?: string): FetchResult<ATProtocolRecord<T>[]> {
20
+
const [data, setData] = useState<ATProtocolRecord<T>[] | null>(null);
21
+
const [loading, setLoading] = useState(true);
22
+
const [error, setError] = useState<string | null>(null);
23
+
24
+
useEffect(() => {
25
+
const loadRecords = async () => {
26
+
try {
27
+
setLoading(true);
28
+
setError(null);
29
+
30
+
const url = buildRecordsUrl(collection, repo);
31
+
const response = await fetch(url);
32
+
33
+
if (!response.ok) {
34
+
throw new Error(`Failed to fetch records from collection: ${collection}`);
35
+
}
36
+
37
+
const data: ListRecordsResponse<T> = await response.json();
38
+
setData(data.records);
39
+
} catch (err) {
40
+
console.error(`Error fetching AT Protocol records from ${collection}:`, err);
41
+
setError(err instanceof Error ? err.message : 'An error occurred');
42
+
} finally {
43
+
setLoading(false);
44
+
}
45
+
};
46
+
47
+
loadRecords();
48
+
}, [collection, repo]);
49
+
50
+
return { data, loading, error };
51
+
}
+267
-1
src/index.css
+267
-1
src/index.css
···
1
-
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=DM+Serif+Display:ital@0;1&display=swap');
1
+
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=DM+Serif+Display:ital@0;1&family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap');
2
2
3
3
@tailwind base;
4
4
@tailwind components;
5
5
@tailwind utilities;
6
6
7
+
html {
8
+
-webkit-text-size-adjust: 100%;
9
+
-ms-text-size-adjust: 100%;
10
+
font-family: sans-serif;
11
+
}
12
+
13
+
body {
14
+
margin: 0;
15
+
}
16
+
17
+
/**
18
+
* Typography System
19
+
*
20
+
* A modular, responsive typography system using fluid type scaling.
21
+
* All typography components use these base classes for consistency.
22
+
*
23
+
* Design principles:
24
+
* - Fluid scaling from mobile (360px) to desktop (1200px)
25
+
* - Semantic sizing (sm, md, lg, xl, 2xl)
26
+
* - Optical adjustments for readability
27
+
* - Consistent vertical rhythm
28
+
*/
29
+
30
+
@layer base {
31
+
/* Typography base reset */
32
+
body {
33
+
@apply font-sans antialiased;
34
+
font-weight: 400;
35
+
text-rendering: optimizeLegibility;
36
+
-webkit-font-smoothing: antialiased;
37
+
-moz-osx-font-smoothing: grayscale;
38
+
}
39
+
40
+
/* Prevent layout shift during font load */
41
+
html {
42
+
font-feature-settings:
43
+
'kern' 1,
44
+
'liga' 1,
45
+
'calt' 1;
46
+
}
47
+
}
48
+
49
+
@layer components {
50
+
/**
51
+
* Typography Scale Classes
52
+
* Base sizing classes with optimized line heights
53
+
*/
54
+
55
+
/* sm: Captions, footnotes, labels (14px โ 16px) */
56
+
.type-sm {
57
+
@apply fluid-preset-sm;
58
+
line-height: 1.62;
59
+
}
60
+
61
+
/* base: Default text (16px โ 32px) */
62
+
.type-base {
63
+
@apply fluid-preset-base;
64
+
line-height: 1.5;
65
+
}
66
+
67
+
/* md: Body copy, paragraphs (24px โ 40px) */
68
+
.type-md {
69
+
@apply fluid-preset-md;
70
+
line-height: 1.38;
71
+
}
72
+
73
+
/* lg: Subheadings, lead paragraphs (18px โ 24px) */
74
+
.type-lg {
75
+
@apply fluid-preset-lg;
76
+
line-height: 1.24;
77
+
}
78
+
79
+
/* xl: Headings (24px โ 36px) */
80
+
.type-xl {
81
+
@apply fluid-preset-xl;
82
+
line-height: 1.16;
83
+
}
84
+
85
+
/* 2xl: Display text, hero headings (36px โ 64px) */
86
+
.type-2xl {
87
+
@apply fluid-preset-2xl;
88
+
line-height: 1;
89
+
}
90
+
91
+
/**
92
+
* Heading Presets
93
+
* Always bold, uses Tailwind's native font-sans
94
+
*/
95
+
96
+
.heading-display {
97
+
@apply type-2xl font-bold font-sans;
98
+
}
99
+
100
+
.heading-2xl {
101
+
@apply type-2xl font-semibold font-sans;
102
+
}
103
+
104
+
.heading-xl {
105
+
@apply type-xl font-semibold font-sans;
106
+
}
107
+
108
+
.heading-lg {
109
+
@apply type-lg font-extrabold font-sans;
110
+
}
111
+
112
+
.heading-md {
113
+
@apply type-md font-extrabold font-sans;
114
+
}
115
+
116
+
.heading-base {
117
+
@apply type-base font-black font-sans;
118
+
}
119
+
120
+
.heading-sm {
121
+
@apply type-sm font-black font-sans;
122
+
}
123
+
124
+
/**
125
+
* Paragraph Presets
126
+
* Normal weight (400), uses Tailwind's native font-sans
127
+
*/
128
+
129
+
.paragraph-2xl {
130
+
@apply type-2xl font-semibold font-sans;
131
+
}
132
+
133
+
.paragraph-xl {
134
+
@apply type-xl font-normal font-sans;
135
+
}
136
+
137
+
.paragraph-lg {
138
+
@apply type-lg font-normal font-sans;
139
+
}
140
+
141
+
.paragraph-md {
142
+
@apply type-md font-normal font-sans;
143
+
}
144
+
145
+
.paragraph-base {
146
+
@apply type-base font-normal font-sans;
147
+
}
148
+
149
+
.paragraph-sm {
150
+
@apply type-sm font-normal font-sans;
151
+
}
152
+
153
+
/**
154
+
* Quote Presets
155
+
* Uses Tailwind's native font-serif, italic
156
+
*/
157
+
158
+
.quote-2xl {
159
+
@apply type-2xl font-normal font-sans italic;
160
+
}
161
+
162
+
.quote-xl {
163
+
@apply type-xl font-normal font-sans italic;
164
+
}
165
+
166
+
.quote-lg {
167
+
@apply type-lg font-normal font-serif italic;
168
+
}
169
+
170
+
.quote-md {
171
+
@apply type-md font-normal font-serif italic;
172
+
}
173
+
174
+
/**
175
+
* Code Presets
176
+
* Uses Tailwind's native font-mono
177
+
*/
178
+
179
+
.code-inline {
180
+
@apply type-base font-normal font-mono;
181
+
}
182
+
183
+
.code-block {
184
+
@apply type-base font-normal font-mono;
185
+
line-height: 1.75;
186
+
}
187
+
188
+
/**
189
+
* Caption Preset
190
+
* Small text for image/media captions
191
+
*/
192
+
193
+
.caption {
194
+
@apply type-sm font-normal font-sans;
195
+
}
196
+
}
197
+
7
198
@layer utilities {
199
+
/* Text selection */
8
200
::selection {
9
201
@apply bg-bones-yellow text-bones-black;
10
202
}
203
+
204
+
/* Prevent orphans in headings (last word alone on a line) */
205
+
.balance-text {
206
+
text-wrap: balance;
207
+
}
208
+
209
+
/* Optimize for readability */
210
+
.optimize-legibility {
211
+
text-rendering: optimizeLegibility;
212
+
}
213
+
}
214
+
215
+
/**
216
+
* Icon System - FontAwesome 6
217
+
*/
218
+
219
+
@font-face {
220
+
font-family: 'FontAwesome 6';
221
+
src:
222
+
url('https://uploads-ssl.webflow.com/63020a6ad41103c31cd7414e/63020a6ad41103330ed7417a_fa-regular-400.woff2')
223
+
format('woff2'),
224
+
url('https://uploads-ssl.webflow.com/63020a6ad41103c31cd7414e/63020a6ad41103db67d7417e_fa-regular-400.ttf')
225
+
format('truetype');
226
+
font-weight: 400;
227
+
font-style: normal;
228
+
font-display: auto;
229
+
}
230
+
231
+
@font-face {
232
+
font-family: 'FontAwesome 6';
233
+
src:
234
+
url('https://uploads-ssl.webflow.com/63020a6ad41103c31cd7414e/63020a6ad41103794bd7418c_fa-solid-900.woff2')
235
+
format('woff2'),
236
+
url('https://uploads-ssl.webflow.com/63020a6ad41103c31cd7414e/63020a6ad411033fc7d7417d_fa-solid-900.ttf')
237
+
format('truetype');
238
+
font-weight: 900;
239
+
font-style: normal;
240
+
font-display: auto;
241
+
}
242
+
243
+
@font-face {
244
+
font-family: 'FontAwesome Brands 6';
245
+
src:
246
+
url('https://uploads-ssl.webflow.com/63020a6ad41103c31cd7414e/63020a6ad41103fe94d74178_fa-brands-400.woff2')
247
+
format('woff2'),
248
+
url('https://uploads-ssl.webflow.com/63020a6ad41103c31cd7414e/63020a6ad411037fcdd7417c_fa-brands-400.ttf')
249
+
format('truetype');
250
+
font-weight: 400;
251
+
font-style: normal;
252
+
font-display: auto;
253
+
}
254
+
255
+
@font-face {
256
+
font-family: 'FontAwesome 6';
257
+
src:
258
+
url('https://uploads-ssl.webflow.com/63020a6ad41103c31cd7414e/63020a6ad411039933d74179_fa-thin-100.woff2')
259
+
format('woff2'),
260
+
url('https://uploads-ssl.webflow.com/63020a6ad41103c31cd7414e/63020a6ad411038365d7418e_fa-thin-100.ttf')
261
+
format('truetype');
262
+
font-weight: 100;
263
+
font-style: normal;
264
+
font-display: auto;
265
+
}
266
+
267
+
@font-face {
268
+
font-family: 'FontAwesome 6';
269
+
src:
270
+
url('https://uploads-ssl.webflow.com/63020a6ad41103c31cd7414e/63020a6ad411031febd7417b_fa-light-300.woff2')
271
+
format('woff2'),
272
+
url('https://uploads-ssl.webflow.com/63020a6ad41103c31cd7414e/63020a6ad41103530ed7418d_fa-light-300.ttf')
273
+
format('truetype');
274
+
font-weight: 300;
275
+
font-style: normal;
276
+
font-display: auto;
11
277
}
+13
-1
src/lib/data/getData.ts
+13
-1
src/lib/data/getData.ts
···
2
2
import path from 'path';
3
3
import type { Article, CaseStudy, Company, ContentData, Job, Skill } from '@/types/content';
4
4
5
-
const dataDirectory = path.join(process.cwd(), 'src', 'lib', 'data', 'json');
5
+
const dataDirectory = path.join(process.cwd(), 'src', 'data', 'json');
6
6
7
7
export async function getContent(): Promise<ContentData> {
8
8
const filePath = path.join(dataDirectory, 'content.json');
···
58
58
.sort((a, b) => new Date(b.publishedOn).getTime() - new Date(a.publishedOn).getTime())
59
59
.slice(0, limit);
60
60
}
61
+
62
+
// Helper function to get latest jobs
63
+
export async function getLatestJobs(limit: number = 5): Promise<Job[]> {
64
+
const jobs = await getJobs();
65
+
return jobs
66
+
.sort((a, b) => {
67
+
const dateA = b.endDate ? new Date(b.endDate).getTime() : Date.now();
68
+
const dateB = a.endDate ? new Date(a.endDate).getTime() : Date.now();
69
+
return dateA - dateB;
70
+
})
71
+
.slice(0, limit);
72
+
}
-1
src/main.tsx
-1
src/main.tsx
+192
-97
src/pages/AboutPage.tsx
+192
-97
src/pages/AboutPage.tsx
···
1
+
import { CardRole } from '@/components/CardRole/CardRole';
2
+
import { Divider } from '@/components/Divider/Divider';
1
3
import { Heading } from '@/components/Heading/Heading';
2
4
import { Aside, Layout, Main } from '@/components/Layout/Layout';
3
5
import { Link } from '@/components/Link/Link';
6
+
import { ListItem } from '@/components/ListItem/ListItem';
4
7
import { Paragraph } from '@/components/Paragraph/Paragraph';
8
+
import Section from '@/components/Section/Section';
5
9
import TLDRProfile from '@/components/TLDRProfile/TLDRProfile';
10
+
import { UnorderedList } from '@/components/UnorderedList/UnorderedList';
11
+
import jobsData from '@/data/json/jobs.json';
6
12
import type { JSX } from 'react';
7
13
import { Helmet } from 'react-helmet-async';
8
14
···
12
18
* @returns JSX element with about page content
13
19
*/
14
20
export default function AboutPage(): JSX.Element {
21
+
// Get latest 5 jobs sorted by end date
22
+
const latestJobs = jobsData
23
+
.sort((a, b) => {
24
+
const dateA = b.endDate ? new Date(b.endDate).getTime() : Date.now();
25
+
const dateB = a.endDate ? new Date(a.endDate).getTime() : Date.now();
26
+
return dateA - dateB;
27
+
})
28
+
.slice(0, 5);
29
+
15
30
const jsonLd = {
16
31
'@context': 'https://schema.org',
17
32
'@type': 'AboutPage',
18
33
mainEntity: {
19
34
'@type': 'Person',
20
35
name: 'Barry Prendergast',
21
-
jobTitle: 'Consulting Design Strategist',
36
+
jobTitle: 'Product Designer',
22
37
description:
23
-
'Design strategist helping ambitious organisations get better products to market faster by focusing on the metrics that matter.',
38
+
'Independent product designer helping organisations deliver better products through clear thinking, practical design, and meaningful collaboration.',
24
39
url: 'https://renderg.host/about',
25
40
sameAs: [
26
41
'https://bsky.app/profile/renderg.host',
···
36
51
<title>About | Barry Prendergast</title>
37
52
<meta
38
53
name="description"
39
-
content="Design strategist helping ambitious organisations get better products to market faster through outcome-driven design."
54
+
content="Independent product designer helping organisations deliver better products through clear thinking, practical design, and meaningful collaboration."
40
55
/>
41
56
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
42
57
</Helmet>
43
58
44
59
<Layout theme="default">
45
60
<Main>
46
-
<div className="flex flex-col gap-12">
47
-
<Heading level={1} style="page">
48
-
About Me
61
+
<div className="flex flex-col gap-16">
62
+
{/* Heading */}
63
+
<Heading level={2} size="md">
64
+
about / <Link href="/">renderg.host</Link>
49
65
</Heading>
50
-
<Paragraph size="billboard">
51
-
I help ambitious organisations get better products to market faster by focusing on the metrics that
52
-
matter.
53
-
</Paragraph>
66
+
67
+
{/* Hero Section*/}
68
+
<Section className="flex flex-col">
69
+
<Paragraph size="2xl">
70
+
I'm an independent designer helping organisations deliver better products and services through
71
+
clear thinking, practical design, and meaningful collaboration.
72
+
</Paragraph>
73
+
<Paragraph size="xl">
74
+
I've spent over 15 years building measurably successful things in complex domains, from scientific
75
+
publishing and medical devices, to energy infrastructure and government services, and more.
76
+
</Paragraph>
77
+
</Section>
78
+
79
+
{/* What I Do Section*/}
80
+
<Section className="flex flex-col">
81
+
<Heading level={2} size="xl">
82
+
What I do...
83
+
</Heading>
84
+
85
+
<Heading level={3} size="md">
86
+
For Customers & Users
87
+
</Heading>
88
+
89
+
<UnorderedList bullet="angle">
90
+
<ListItem>Build autonomy-driven products that people can easily master</ListItem>
91
+
<ListItem>Connect people to help them work better, together</ListItem>
92
+
<ListItem>Create interactions that feel obvious and reassuring</ListItem>
93
+
<ListItem>Design efficient usable interfaces that respect people's time</ListItem>
94
+
<ListItem>Simplify complexity so users can get things done, faster</ListItem>
95
+
</UnorderedList>
96
+
97
+
<Heading level={3} size="md">
98
+
For Business
99
+
</Heading>
100
+
101
+
<UnorderedList bullet="angle">
102
+
<ListItem>Connect design decisions to leading metrics that drive growth</ListItem>
103
+
<ListItem>Get to market faster by testing risky product assumptions early</ListItem>
104
+
<ListItem>Increase adoption and retention by simplifying complexity</ListItem>
105
+
<ListItem>Reduce support costs with self-service user experiences</ListItem>
106
+
<ListItem>Streamline engineering overheads with lean iteration</ListItem>
107
+
</UnorderedList>
108
+
109
+
<Heading level={3} size="md">
110
+
For Products & Services
111
+
</Heading>
54
112
55
-
<Paragraph size="billboard">
56
-
I believe that great design comes from <em>bold ideas</em>, timely conversations with the right people,
57
-
and rigorous testing against the right metrics in lean, iterative cycles.
58
-
</Paragraph>
113
+
<UnorderedList bullet="angle">
114
+
<ListItem>Build prototypes that answer key questions before development</ListItem>
115
+
<ListItem>Create scalable, maintainable design systems</ListItem>
116
+
<ListItem>Test designs with real users to guide confident decisions</ListItem>
117
+
<ListItem>Translate business goals into measurable design outcomes</ListItem>
118
+
<ListItem>Turn complex workflows into clear, usable interfaces</ListItem>
119
+
</UnorderedList>
59
120
60
-
<Paragraph size="billboard">
61
-
I'm a fellow of the{' '}
62
-
<Link href="https://thomaskuhnfoundation.org/" target="_blank">
63
-
Thomas Kuhn Foundation
64
-
</Link>
65
-
, supporting new ways to understand scientific knowledge.
66
-
</Paragraph>
121
+
<Heading level={3} size="md">
122
+
For Stakeholders & Teams
123
+
</Heading>
124
+
125
+
<UnorderedList bullet="angle">
126
+
<ListItem>Coach teams to make confident, informed design choices</ListItem>
127
+
<ListItem>Create healthy feedback loops that strengthen outcomes</ListItem>
128
+
<ListItem>Define design processes that support collaboration and progress</ListItem>
129
+
<ListItem>Establish research practices that directly inform decisions</ListItem>
130
+
<ListItem>Write lightweight docs teams refer to every day</ListItem>
131
+
</UnorderedList>
132
+
</Section>
133
+
134
+
<Section>
135
+
<Heading level={2} size="lg">
136
+
Currently
137
+
</Heading>
138
+
139
+
<div className="flex flex-col gap-6">
140
+
<Paragraph size="lg">Freelancing as a ____</Paragraph>
141
+
{/* TODO: Add CTA to /Contact page when ready */}
142
+
143
+
<Paragraph size="lg">
144
+
I'm a fellow of the{' '}
145
+
<Link href="https://thomaskuhnfoundation.org/" target="_blank" rel="noopener noreferrer">
146
+
Thomas Kuhn Foundation
147
+
</Link>
148
+
, supporting new ways to understand scientific knowledge.
149
+
</Paragraph>
150
+
151
+
<Paragraph size="lg">
152
+
<Link href="/writing">Read my Blog</Link>
153
+
</Paragraph>
154
+
155
+
<Paragraph size="lg">I'm building tools for the ATprotocol</Paragraph>
156
+
157
+
<Paragraph size="lg">
158
+
<Link href="/writing">Read my Blog</Link>
159
+
</Paragraph>
160
+
</div>
161
+
</Section>
162
+
<Section>
163
+
<Heading level={2} size="lg">
164
+
Recent Work
165
+
</Heading>
166
+
167
+
<div className="grid grid-cols-1 border-2 border-bones-black-20 dark:border-bones-white-20">
168
+
{latestJobs.map((job, index) => {
169
+
const startYear = new Date(job.startDate).getFullYear();
170
+
const endYear = job.endDate ? new Date(job.endDate).getFullYear() : 'Present';
171
+
const dateRange = startYear === endYear ? `${startYear}` : `${startYear}โ${endYear}`;
67
172
68
-
<Paragraph size="billboard">
69
-
I try to tackle the hardest problems by listening closely, communicating clearly and collaborating openly
70
-
by default.
71
-
</Paragraph>
173
+
return (
174
+
<>
175
+
<CardRole
176
+
key={job.slug}
177
+
role={{
178
+
title: job.title,
179
+
company: job.client,
180
+
subtitle: job.summary,
181
+
date: dateRange,
182
+
// coverImage: job.cover,
183
+
slug: job.slug,
184
+
}}
185
+
/>
186
+
{index < latestJobs.length - 1 && <Divider />}
187
+
</>
188
+
);
189
+
})}
190
+
</div>
72
191
73
-
<Paragraph size="billboard">
74
-
My design approach balances <em>strategic clarity</em> with research and design excellence, while
75
-
responding to the everchanging needs of an agile product team.
76
-
</Paragraph>
192
+
{/* TODO: Add link to Resume when ready */}
193
+
{/* TODO: Add CTA to /Contact page when ready */}
194
+
</Section>
195
+
<Section>
196
+
<Heading level={2} size="lg">
197
+
How I Work
198
+
</Heading>
77
199
78
-
<Paragraph size="billboard">
79
-
I help teams to cut through distractions, to adapt when things don't go to plan, and to stay focused
80
-
on what matters mostโto the teams and their customers alike.
81
-
</Paragraph>
200
+
<Paragraph size="lg">My approach is straightforward and outcome-focused.</Paragraph>
82
201
83
-
<Heading level={2} style="section">
84
-
I <Link href="#">specialise</Link> in three areas.
85
-
</Heading>
202
+
<UnorderedList bullet="disc">
203
+
<ListItem>I start by understanding users and the problems they face</ListItem>
204
+
<ListItem>I design and prototype quickly to validate ideas early</ListItem>
205
+
<ListItem>I measure results and refine based on evidence</ListItem>
206
+
<ListItem>I document decisions to help teams move with clarity</ListItem>
207
+
<ListItem>I build systems that continue to work well beyond my involvement</ListItem>
208
+
</UnorderedList>
86
209
87
-
<Paragraph size="billboard">
88
-
<em>Design Strategy</em>: Aligning key business metrics with unmet user needs. I translate business goals
89
-
into actionable product and design objectives, ensuring every design decision drives meaningful success.
90
-
</Paragraph>
210
+
<Paragraph size="lg">
211
+
<Link href="/writing">Read my Blog</Link>
212
+
</Paragraph>
213
+
</Section>
214
+
<Section>
215
+
<Heading level={2} size="lg">
216
+
What Drives Me
217
+
</Heading>
91
218
92
-
<Paragraph size="billboard">
93
-
<em>Product Design</em>: Hands-on design and prototyping to validate bets and improve user experiences. I
94
-
combine research, usability, and rapid prototyping to de-risk product decisions and refine solutions over
95
-
time through continuous iteration.
96
-
</Paragraph>
219
+
<Paragraph size="lg">
220
+
<strong>At work:</strong> Designing with purpose. Creating products that respect users' time and
221
+
intelligence. Collaborating with teams that value clarity, curiosity, and progress.
222
+
</Paragraph>
97
223
98
-
<Paragraph size="billboard">
99
-
<em>Design Operations</em>: Optimising the tools, rituals, and metrics that nurture great design culture.
100
-
I streamline workflows, remove friction, and embed scalable design practices that empower teams to move
101
-
fast without compromising quality.
102
-
</Paragraph>
224
+
<Paragraph size="lg">
225
+
<strong>Outside work:</strong> Learning continuouslyโcurrently exploring systems thinking and
226
+
information architecture. Making experimental music when the kids are asleep. Reading everything from
227
+
technical manuals to science fiction.
228
+
</Paragraph>
229
+
</Section>
230
+
<Section>
231
+
<Heading level={2} size="lg">
232
+
Work With Me
233
+
</Heading>
103
234
104
-
<Paragraph size="billboard">
105
-
Since 2008, I've worked with organisations including{' '}
106
-
<Link href="https://www.morressier.com/" target="_blank">
107
-
Morressier
108
-
</Link>
109
-
,{' '}
110
-
<Link href="https://www.leo-pharma.com/" target="_blank">
111
-
LEO Pharma
112
-
</Link>
113
-
,{' '}
114
-
<Link href="https://www.edfenergy.com/" target="_blank">
115
-
EDF Energy
116
-
</Link>
117
-
,{' '}
118
-
<Link href="https://www.gov.uk/government/organisations/home-office" target="_blank">
119
-
UK Home Office
120
-
</Link>
121
-
,{' '}
122
-
<Link href="https://www.brandwatch.com/" target="_blank">
123
-
Brandwatch
124
-
</Link>
125
-
, and{' '}
126
-
<Link href="https://mediatonicgames.com/" target="_blank">
127
-
Mediatonic
128
-
</Link>
129
-
โfrom scientific publishing to pharmaceuticals, energy to government, consumer intelligence to gaming.
130
-
</Paragraph>
235
+
<Paragraph size="lg">I collaborate with organisations that:</Paragraph>
131
236
132
-
<Paragraph size="billboard">
133
-
<Link href="https://calendar.app.google/cuYkSrDLca1Wxfqo9" target="_blank" rel="noopener noreferrer">
134
-
Book our first meeting
135
-
</Link>
136
-
, connect on{' '}
137
-
<Link href="https://linkedin.com/in/barryprendergast" target="_blank" rel="noopener noreferrer">
138
-
LinkedIn
139
-
</Link>
140
-
, or message me on{' '}
141
-
<Link
142
-
href="https://signal.me/#eu/XO_aKC1aE1GZYWdMx7WK7HKGSCfrlpNhlxLGNi774dhiL7qr32BAMrH1BqgChaiM"
143
-
target="_blank"
144
-
rel="noopener noreferrer"
145
-
>
146
-
Signal
147
-
</Link>
148
-
.
149
-
</Paragraph>
237
+
<UnorderedList bullet="disc">
238
+
<ListItem>Are tackling meaningful challenges</ListItem>
239
+
<ListItem>Value clarity, purpose, and long-term impact</ListItem>
240
+
<ListItem>See design as a driver of business value</ListItem>
241
+
<ListItem>Want to build better ways of working, not just better interfaces</ListItem>
242
+
</UnorderedList>
150
243
151
-
<Paragraph size="billboard">
152
-
<Link href="/">โ Back to Home</Link>
244
+
{/* TODO: Add CTA to /Contact page when ready */}
245
+
</Section>
246
+
<Paragraph size="lg">
247
+
<Link href="/">โ Back to home</Link>
153
248
</Paragraph>
154
249
</div>
155
250
</Main>
+8
-11
src/pages/HomePage.tsx
+8
-11
src/pages/HomePage.tsx
···
52
52
53
53
<Layout theme="accent">
54
54
<Main>
55
-
<div className="flex flex-col gap-12">
56
-
<Heading level={1} style="billboard">
55
+
<div className="flex flex-col gap-16">
56
+
<Heading level={1} size="2xl">
57
57
Hi! ๐ I'm Barry Prendergast, a design strategist living in Berlin, Germany.
58
58
</Heading>
59
59
60
-
<Paragraph size="billboard">
60
+
<Paragraph size="2xl">
61
61
I <Link href="/about">specialise</Link> in <em>outcome</em>-driven design strategy, practice, and systems
62
62
for digital products and services.
63
63
</Paragraph>
64
64
65
-
<Paragraph size="billboard">
66
-
I{' '}
67
-
<Link href="https://www.linkedin.com/in/barrymprendergast/details/experience/" target="_blank">
68
-
work
69
-
</Link>{' '}
70
-
with nonprofits and startups to ease their growing pains, and to market faster.
65
+
<Paragraph size="2xl">
66
+
I <Link href="/work">work</Link> with nonprofits and startups to ease their growing pains, and to market
67
+
faster.
71
68
</Paragraph>
72
69
73
-
<Paragraph size="billboard">
70
+
<Paragraph size="2xl">
74
71
I <Link href="/writing">write</Link> about about academia, design, product, science, systems, technology &
75
72
the messy in-betweens.
76
73
</Paragraph>
77
74
78
-
<Paragraph size="billboard">
75
+
<Paragraph size="2xl">
79
76
<Link href="https://cal.com/renderghost" target="_blank" rel="noopener noreferrer">
80
77
Book a meeting
81
78
</Link>{' '}
+71
src/pages/PortfolioPage.tsx
+71
src/pages/PortfolioPage.tsx
···
1
+
import { Link } from '@/components/Link/Link';
2
+
import type { JSX } from 'react';
3
+
import { Helmet } from 'react-helmet-async';
4
+
5
+
/**
6
+
* Works page component
7
+
*
8
+
* @returns JSX element with works page content
9
+
*/
10
+
export default function WorksPage(): JSX.Element {
11
+
const jsonLd = {
12
+
'@context': 'https://schema.org',
13
+
'@type': 'CollectionPage',
14
+
name: 'Works by Barry Prendergast',
15
+
description: 'Case studies and projects by Barry Prendergast, design strategist.',
16
+
url: 'https://renderg.host/works',
17
+
};
18
+
19
+
const caseStudies = [
20
+
{ id: 1, title: 'Democratising insightful decision making across organisations', company: 'Brandwatch' },
21
+
{ id: 2, title: 'Building a professional community for scientists', company: 'Morressier' },
22
+
{ id: 3, title: 'Scaling a platform design system', company: 'Morressier' },
23
+
{ id: 4, title: 'Designing dermatology as a digital service', company: 'LEO Pharma' },
24
+
{ id: 5, title: 'Reducing energy waste at home', company: 'EDF Energy' },
25
+
{ id: 6, title: 'Defining the product strategy for eco shopping', company: 'MyGoodPlanet' },
26
+
{ id: 7, title: 'Improving the recruitment experience for design candidates', company: 'Morressier' },
27
+
{ id: 8, title: 'Scaling product design feedback', company: 'Morressier' },
28
+
{ id: 9, title: 'Streamlining immigration services', company: 'UK Home Office' },
29
+
{ id: 10, title: 'Creating data-driven insights for enterprises', company: 'Brandwatch' },
30
+
{ id: 11, title: 'Launching scalable design systems', company: 'Pure360' },
31
+
{ id: 12, title: 'Building inclusive gaming experiences', company: 'Mediatonic' },
32
+
{ id: 13, title: 'Enhancing smart energy platforms', company: 'EDF Energy' },
33
+
{ id: 14, title: 'Designing for scientific publishing', company: 'Morressier' },
34
+
{ id: 15, title: 'Supporting sustainable shopping decisions', company: 'MyGoodPlanet' },
35
+
{ id: 16, title: 'Developing enterprise software solutions', company: 'Schlumberger' },
36
+
];
37
+
38
+
return (
39
+
<>
40
+
<Helmet>
41
+
<title>Works | Barry Prendergast</title>
42
+
<meta
43
+
name="description"
44
+
content="Case studies and projects by Barry Prendergast, showcasing design strategy and product design work."
45
+
/>
46
+
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
47
+
</Helmet>
48
+
49
+
<main className="min-h-screen bg-bones-blue text-bones-white">
50
+
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3">
51
+
{caseStudies.map((study) => (
52
+
<Link
53
+
key={study.id}
54
+
href="#"
55
+
className="flex flex-col justify-start items-start p-8 border border-bones-white/10 hover:bg-bones-white/5 transition-colors overflow-hidden aspect-square"
56
+
>
57
+
<span className="text-5xl font-medium leading-tight">{study.title}</span>
58
+
<span className="text-2xl opacity-60 mt-2">{study.company}</span>
59
+
</Link>
60
+
))}
61
+
<Link
62
+
href="/"
63
+
className="flex flex-col justify-start items-start p-8 border border-bones-white/10 hover:bg-bones-white/5 transition-colors col-span-1 sm:col-span-2 xl:col-span-3"
64
+
>
65
+
<span className="text-6xl font-medium">โ Back to Home</span>
66
+
</Link>
67
+
</div>
68
+
</main>
69
+
</>
70
+
);
71
+
}
+130
src/pages/WorkPage.tsx
+130
src/pages/WorkPage.tsx
···
1
+
import { CardRole } from '@/components/CardRole/CardRole';
2
+
import { Divider } from '@/components/Divider/Divider';
3
+
import { Heading } from '@/components/Heading/Heading';
4
+
import { Aside, Layout, Main } from '@/components/Layout/Layout';
5
+
import { Link } from '@/components/Link/Link';
6
+
import { Paragraph } from '@/components/Paragraph/Paragraph';
7
+
import Section from '@/components/Section/Section';
8
+
import TLDRProfile from '@/components/TLDRProfile/TLDRProfile';
9
+
import { useProtopro } from '@/hooks/atproto';
10
+
import type { JSX } from 'react';
11
+
import { Helmet } from 'react-helmet-async';
12
+
13
+
/**
14
+
* Work page component - displays CV/work history from AT Protocol PDS
15
+
*
16
+
* @returns JSX element with work page content
17
+
*/
18
+
export default function WorkPage(): JSX.Element {
19
+
const { data: profile, loading, error } = useProtopro();
20
+
21
+
// Split jobs into current and past
22
+
const currentJobs = profile?.jobHistory.filter((job) => !job.endDate) || [];
23
+
const pastJobs =
24
+
profile?.jobHistory
25
+
.filter((job) => job.endDate)
26
+
.sort((a, b) => {
27
+
// Sort by end date, newest first
28
+
const dateA = a.endDate ? new Date(a.endDate).getTime() : 0;
29
+
const dateB = b.endDate ? new Date(b.endDate).getTime() : 0;
30
+
return dateB - dateA;
31
+
}) || [];
32
+
33
+
const jsonLd = {
34
+
'@context': 'https://schema.org',
35
+
'@type': 'ProfilePage',
36
+
mainEntity: {
37
+
'@type': 'Person',
38
+
name: 'Barry Prendergast',
39
+
jobTitle: currentJobs[0]?.position || 'Product Designer',
40
+
description: 'Independent product designer and strategist',
41
+
url: 'https://renderg.host/work',
42
+
},
43
+
};
44
+
45
+
return (
46
+
<>
47
+
<Helmet>
48
+
<title>Work | Barry Prendergast</title>
49
+
<meta
50
+
name="description"
51
+
content="Independent product designer helping organisations deliver better products through clear thinking, practical design, and meaningful collaboration."
52
+
/>
53
+
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
54
+
</Helmet>
55
+
56
+
<Layout theme="default">
57
+
<Main>
58
+
<div className="flex flex-col gap-16">
59
+
{/* Heading */}
60
+
<Section>
61
+
<Heading level={2} size="base">
62
+
work / <Link href="/">renderg.host</Link>
63
+
</Heading>
64
+
</Section>
65
+
66
+
{/* Loading/Error States */}
67
+
{loading && (
68
+
<Section>
69
+
<Paragraph size="2xl">Loading profile...</Paragraph>
70
+
</Section>
71
+
)}
72
+
73
+
{error && (
74
+
<Section>
75
+
<Paragraph size="2xl">Error loading profile: {error}</Paragraph>
76
+
</Section>
77
+
)}
78
+
79
+
{/* Current Work Section */}
80
+
{!loading && !error && currentJobs.length > 0 && (
81
+
<Section>
82
+
<Heading level={2} size="lg">
83
+
Current roles
84
+
</Heading>
85
+
86
+
<div className="grid grid-cols-1 border-2 border-bones-black-20 dark:border-bones-white-20">
87
+
{currentJobs.map((job, index) => (
88
+
<>
89
+
<CardRole key={`current-${job.company}-${index}`} role={job} />
90
+
{index < currentJobs.length - 1 && <Divider />}
91
+
</>
92
+
))}
93
+
</div>
94
+
</Section>
95
+
)}
96
+
97
+
{/* Past Work Section */}
98
+
{!loading && !error && pastJobs.length > 0 && (
99
+
<Section>
100
+
<Heading level={2} size="lg">
101
+
Previous roles
102
+
</Heading>
103
+
104
+
<div className="grid grid-cols-1 border-2 border-bones-black-20 dark:border-bones-white-20">
105
+
{pastJobs.map((job, index) => (
106
+
<>
107
+
<CardRole key={`past-${job.company}-${index}`} role={job} />
108
+
{index < pastJobs.length - 1 && <Divider />}
109
+
</>
110
+
))}
111
+
</div>
112
+
</Section>
113
+
)}
114
+
115
+
{/* Exit */}
116
+
<Section>
117
+
<Paragraph size="md">
118
+
Return to <Link href="/">renderg.host</Link>
119
+
</Paragraph>
120
+
</Section>
121
+
</div>
122
+
</Main>
123
+
124
+
<Aside>
125
+
<TLDRProfile />
126
+
</Aside>
127
+
</Layout>
128
+
</>
129
+
);
130
+
}
-71
src/pages/WorksPage.tsx
-71
src/pages/WorksPage.tsx
···
1
-
import { Link } from '@/components/Link/Link';
2
-
import type { JSX } from 'react';
3
-
import { Helmet } from 'react-helmet-async';
4
-
5
-
/**
6
-
* Works page component
7
-
*
8
-
* @returns JSX element with works page content
9
-
*/
10
-
export default function WorksPage(): JSX.Element {
11
-
const jsonLd = {
12
-
'@context': 'https://schema.org',
13
-
'@type': 'CollectionPage',
14
-
name: 'Works by Barry Prendergast',
15
-
description: 'Case studies and projects by Barry Prendergast, design strategist.',
16
-
url: 'https://renderg.host/works',
17
-
};
18
-
19
-
const caseStudies = [
20
-
{ id: 1, title: 'Democratising insightful decision making across organisations', company: 'Brandwatch' },
21
-
{ id: 2, title: 'Building a professional community for scientists', company: 'Morressier' },
22
-
{ id: 3, title: 'Scaling a platform design system', company: 'Morressier' },
23
-
{ id: 4, title: 'Designing dermatology as a digital service', company: 'LEO Pharma' },
24
-
{ id: 5, title: 'Reducing energy waste at home', company: 'EDF Energy' },
25
-
{ id: 6, title: 'Defining the product strategy for eco shopping', company: 'MyGoodPlanet' },
26
-
{ id: 7, title: 'Improving the recruitment experience for design candidates', company: 'Morressier' },
27
-
{ id: 8, title: 'Scaling product design feedback', company: 'Morressier' },
28
-
{ id: 9, title: 'Streamlining immigration services', company: 'UK Home Office' },
29
-
{ id: 10, title: 'Creating data-driven insights for enterprises', company: 'Brandwatch' },
30
-
{ id: 11, title: 'Launching scalable design systems', company: 'Pure360' },
31
-
{ id: 12, title: 'Building inclusive gaming experiences', company: 'Mediatonic' },
32
-
{ id: 13, title: 'Enhancing smart energy platforms', company: 'EDF Energy' },
33
-
{ id: 14, title: 'Designing for scientific publishing', company: 'Morressier' },
34
-
{ id: 15, title: 'Supporting sustainable shopping decisions', company: 'MyGoodPlanet' },
35
-
{ id: 16, title: 'Developing enterprise software solutions', company: 'Schlumberger' },
36
-
];
37
-
38
-
return (
39
-
<>
40
-
<Helmet>
41
-
<title>Works | Barry Prendergast</title>
42
-
<meta
43
-
name="description"
44
-
content="Case studies and projects by Barry Prendergast, showcasing design strategy and product design work."
45
-
/>
46
-
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
47
-
</Helmet>
48
-
49
-
<main className="min-h-screen bg-bones-blue text-bones-white">
50
-
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3">
51
-
{caseStudies.map((study) => (
52
-
<Link
53
-
key={study.id}
54
-
href="#"
55
-
className="flex flex-col justify-start items-start p-8 border border-bones-white/10 hover:bg-bones-white/5 transition-colors overflow-hidden aspect-square"
56
-
>
57
-
<span className="text-5xl font-medium leading-tight">{study.title}</span>
58
-
<span className="text-2xl opacity-60 mt-2">{study.company}</span>
59
-
</Link>
60
-
))}
61
-
<Link
62
-
href="/"
63
-
className="flex flex-col justify-start items-start p-8 border border-bones-white/10 hover:bg-bones-white/5 transition-colors col-span-1 sm:col-span-2 xl:col-span-3"
64
-
>
65
-
<span className="text-6xl font-medium">โ Back to Home</span>
66
-
</Link>
67
-
</div>
68
-
</main>
69
-
</>
70
-
);
71
-
}
+11
-12
src/pages/WritingPage.tsx
+11
-12
src/pages/WritingPage.tsx
···
5
5
import { Link } from '@/components/Link/Link';
6
6
import { Paragraph } from '@/components/Paragraph/Paragraph';
7
7
import TLDRProfile from '@/components/TLDRProfile/TLDRProfile';
8
-
import { fetchPublications } from '@/hooks/atproto';
8
+
import { useLeaflet } from '@/hooks/atproto';
9
9
import type { JSX } from 'react';
10
10
import { Helmet } from 'react-helmet-async';
11
11
···
14
14
*
15
15
* @returns JSX element with writing page content
16
16
*/
17
+
17
18
export default function WritingPage(): JSX.Element {
18
-
const { data: documents, loading, error } = fetchPublications();
19
+
const { data: documents, loading, error } = useLeaflet();
19
20
20
21
const jsonLd = {
21
22
'@context': 'https://schema.org',
···
44
45
<Layout theme="default">
45
46
<Main>
46
47
<div className="flex flex-col gap-12">
47
-
<Heading level={1} style="page">
48
-
Writing
48
+
<Heading level={2} size="md">
49
+
writing / <Link href="/">renderg.host</Link>
49
50
</Heading>
50
51
51
-
<Paragraph size="billboard">
52
-
Thoughts on design strategy, product design, and building better products.
53
-
</Paragraph>
52
+
<Paragraph size="lg">Thoughts on design strategy, product design, and building better products.</Paragraph>
54
53
55
-
{loading && <Paragraph size="billboard">Loading posts...</Paragraph>}
54
+
{loading && <Paragraph size="lg">Loading posts...</Paragraph>}
56
55
57
-
{error && <Paragraph size="billboard">Error loading posts: {error}</Paragraph>}
56
+
{error && <Paragraph size="lg">Error loading posts: {error}</Paragraph>}
58
57
59
58
{!loading && !error && (!documents || documents.length === 0) && (
60
-
<Paragraph size="billboard">No posts found.</Paragraph>
59
+
<Paragraph size="lg">No posts found.</Paragraph>
61
60
)}
62
61
63
62
{!loading && !error && documents && documents.length > 0 && (
64
-
<div className="grid grid-cols-1 border-2 border-bones-white-30">
63
+
<div className="grid grid-cols-1 border-2 border-bones-black-20 dark:border-bones-white-20">
65
64
{documents.map((doc, index) => (
66
65
<>
67
66
<CardArticle
···
82
81
</div>
83
82
)}
84
83
85
-
<Paragraph size="billboard">
84
+
<Paragraph size="lg">
86
85
<Link href="/">โ Back to Home</Link>
87
86
</Paragraph>
88
87
</div>
+43
src/types/atproto/defineBase.ts
+43
src/types/atproto/defineBase.ts
···
1
+
/**
2
+
* Base AT Protocol type definitions
3
+
* Generic types used across all AT Protocol collections
4
+
*/
5
+
6
+
/**
7
+
* AT Protocol Blob reference
8
+
* Used for binary data like images
9
+
*/
10
+
export interface ATProtocolBlob {
11
+
$type: 'blob';
12
+
ref: {
13
+
$link: string;
14
+
};
15
+
mimeType: string;
16
+
size: number;
17
+
}
18
+
19
+
/**
20
+
* Base AT Protocol record structure
21
+
*/
22
+
export interface ATProtocolRecord<T = unknown> {
23
+
uri: string;
24
+
cid: string;
25
+
value: T;
26
+
}
27
+
28
+
/**
29
+
* Response from listRecords XRPC endpoint
30
+
*/
31
+
export interface ListRecordsResponse<T = unknown> {
32
+
records: ATProtocolRecord<T>[];
33
+
cursor?: string;
34
+
}
35
+
36
+
/**
37
+
* Generic fetch result with loading and error states
38
+
*/
39
+
export interface FetchResult<T> {
40
+
data: T | null;
41
+
loading: boolean;
42
+
error: string | null;
43
+
}
+38
src/types/atproto/defineLeaflet.ts
+38
src/types/atproto/defineLeaflet.ts
···
1
+
/**
2
+
* Leaflet collection type definitions
3
+
* Types for pub.leaflet.publication and pub.leaflet.document
4
+
*/
5
+
6
+
import type { ATProtocolBlob, ATProtocolRecord } from './defineBase';
7
+
8
+
/**
9
+
* Publication record value structure
10
+
*/
11
+
export interface PublicationValue {
12
+
name: string;
13
+
base_path: string;
14
+
icon?: ATProtocolBlob;
15
+
description?: string;
16
+
$type: string;
17
+
}
18
+
19
+
/**
20
+
* Document record value structure
21
+
*/
22
+
export interface DocumentValue {
23
+
title: string;
24
+
description?: string;
25
+
publishedAt: string;
26
+
publication: string; // URI reference to the publication
27
+
$type: string;
28
+
}
29
+
30
+
/**
31
+
* Full Publication record
32
+
*/
33
+
export type ATProtocolPublication = ATProtocolRecord<PublicationValue>;
34
+
35
+
/**
36
+
* Full Document record
37
+
*/
38
+
export type ATProtocolDocument = ATProtocolRecord<DocumentValue>;
+45
src/types/atproto/defineProtopro.ts
+45
src/types/atproto/defineProtopro.ts
···
1
+
/**
2
+
* Protopro collection type definitions
3
+
* Types for blue.protopro.actor.profile
4
+
*/
5
+
6
+
import type { ATProtocolRecord } from './defineBase';
7
+
8
+
/**
9
+
* Language proficiency structure
10
+
*/
11
+
export interface LanguageProficiency {
12
+
code: string;
13
+
level: number;
14
+
}
15
+
16
+
/**
17
+
* Job history entry structure
18
+
*/
19
+
export interface JobHistoryEntry {
20
+
company: string;
21
+
position: string;
22
+
startDate: string;
23
+
endDate?: string;
24
+
description?: string;
25
+
}
26
+
27
+
/**
28
+
* Actor Profile record value structure
29
+
*/
30
+
export interface ProfileValue {
31
+
name: string;
32
+
skills: string[];
33
+
overview?: string;
34
+
languages?: LanguageProficiency[];
35
+
jobHistory?: JobHistoryEntry[];
36
+
socialLinks?: string[];
37
+
educationHistory?: unknown[];
38
+
updatedAt?: string;
39
+
$type: string;
40
+
}
41
+
42
+
/**
43
+
* Full Profile record
44
+
*/
45
+
export type ATProtocolProfile = ATProtocolRecord<ProfileValue>;
-66
src/types/atproto/defineRecords.ts
-66
src/types/atproto/defineRecords.ts
···
1
-
/**
2
-
* AT Protocol record type definitions
3
-
* Type-safe interfaces for AT Protocol data structures
4
-
*/
5
-
6
-
/**
7
-
* AT Protocol Blob reference
8
-
* Used for binary data like images
9
-
*/
10
-
export interface ATProtocolBlob {
11
-
$type: 'blob';
12
-
ref: {
13
-
$link: string;
14
-
};
15
-
mimeType: string;
16
-
size: number;
17
-
}
18
-
19
-
/**
20
-
* Base AT Protocol record structure
21
-
*/
22
-
export interface ATProtocolRecord<T = unknown> {
23
-
uri: string;
24
-
cid: string;
25
-
value: T;
26
-
}
27
-
28
-
/**
29
-
* Publication record value structure
30
-
*/
31
-
export interface PublicationValue {
32
-
name: string;
33
-
base_path: string;
34
-
icon?: ATProtocolBlob;
35
-
description?: string;
36
-
$type: string;
37
-
}
38
-
39
-
/**
40
-
* Document record value structure
41
-
*/
42
-
export interface DocumentValue {
43
-
title: string;
44
-
description?: string;
45
-
publishedAt: string;
46
-
publication: string; // URI reference to the publication
47
-
$type: string;
48
-
}
49
-
50
-
/**
51
-
* Full Publication record
52
-
*/
53
-
export type ATProtocolPublication = ATProtocolRecord<PublicationValue>;
54
-
55
-
/**
56
-
* Full Document record
57
-
*/
58
-
export type ATProtocolDocument = ATProtocolRecord<DocumentValue>;
59
-
60
-
/**
61
-
* Response from listRecords XRPC endpoint
62
-
*/
63
-
export interface ListRecordsResponse<T = unknown> {
64
-
records: ATProtocolRecord<T>[];
65
-
cursor?: string;
66
-
}
-12
src/types/atproto/defineResults.ts
-12
src/types/atproto/defineResults.ts
+12
-10
src/types/atproto/index.ts
+12
-10
src/types/atproto/index.ts
···
2
2
* AT Protocol type exports
3
3
*/
4
4
5
-
export type {
6
-
ATProtocolBlob,
7
-
ATProtocolDocument,
8
-
ATProtocolPublication,
9
-
ATProtocolRecord,
10
-
DocumentValue,
11
-
ListRecordsResponse,
12
-
PublicationValue,
13
-
} from './defineRecords';
5
+
// Base types
6
+
export type { ATProtocolBlob, ATProtocolRecord, FetchResult, ListRecordsResponse } from './defineBase';
14
7
15
-
export type { FetchResult } from './defineResults';
8
+
// Leaflet types
9
+
export type { ATProtocolDocument, ATProtocolPublication, DocumentValue, PublicationValue } from './defineLeaflet';
10
+
11
+
// Protopro types
12
+
export type {
13
+
ATProtocolProfile,
14
+
JobHistoryEntry,
15
+
LanguageProficiency,
16
+
ProfileValue,
17
+
} from './defineProtopro';
+1
tailwind.config.ts
+1
tailwind.config.ts
+5
-1
tsconfig.json
+5
-1
tsconfig.json
···
25
25
"types": ["react"],
26
26
"useDefineForClassFields": true
27
27
},
28
-
"include": ["src", "src/components/Sidebar/.Sidebar.tsx.off"],
28
+
"include": [
29
+
"src",
30
+
"src/components/Sidebar/.Sidebar.tsx.off",
31
+
"src/components/CardArticle/CardArticle.tsx"
32
+
],
29
33
"references": [{ "path": "./tsconfig.node.json" }]
30
34
}