Compare changes

Choose any two refs to compare.

Changed files
+1981 -5466
plugins
src
components
hooks
lib
data
pages
types
+3
.gitignore
··· 22 22 *.njsproj 23 23 *.sln 24 24 *.sw? 25 + 26 + # Project Management BS 27 + .documentation/
-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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 1 + import { ReactNode } from 'react'; 2 + 3 + export interface CaptionProps { 4 + children: ReactNode; 5 + className?: string; 6 + }
+2
src/components/Caption/index.ts
··· 1 + export { Caption } from './Caption'; 2 + export type { CaptionProps } from './Caption.types';
+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
··· 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
··· 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
··· 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} &ndash; {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
··· 1 + export interface CardEventProps { 2 + eventType: string; 3 + eventTitle: string; 4 + eventDescription: string; 5 + eventStartYear: string; 6 + eventEndYear: string; 7 + eventAffiliation: string; 8 + className?: string; 9 + }
+3
src/components/CardFact/CardFact.styles.ts
··· 1 + export const cardFactStyles = { 2 + wrapper: 'px-8 py-6 bg-neutral-50 dark:bg-neutral-800 rounded-lg shadow-sm', 3 + };
+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
··· 1 + export interface CardFactProps { 2 + title: string; 3 + subtitle: string; 4 + className?: string; 5 + }
+1 -1
src/components/CardNote/CardNote.tsx
··· 32 32 </div> 33 33 34 34 {/* Title */} 35 - <Heading level={3}>{title}</Heading> 35 + <Heading level={3} size="lg">{title}</Heading> 36 36 {/* Description */} 37 37 {description && <Paragraph>{description}</Paragraph>} 38 38 </div>
+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
··· 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
··· 1 + import type { JobHistoryEntry } from '@/types/atproto'; 2 + 1 3 export interface CardRoleProps { 2 - role: { 3 - title: string; 4 - company: string; 5 - subtitle: string; 6 - date: string; 7 - coverImage?: string; 8 - slug: string; 9 - }; 4 + role: JobHistoryEntry; 10 5 }
+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
··· 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
··· 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
··· 1 + export { Code } from './Code'; 2 + export type { CodeProps, CodeVariant } from './Code.types';
+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
··· 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
··· 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
··· 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} &ndash; {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
··· 1 + export interface EventProps { 2 + eventType: string; 3 + eventTitle: string; 4 + eventDescription: string; 5 + eventStartYear: string; 6 + eventEndYear: string; 7 + eventAffiliation: string; 8 + className?: string; 9 + }
-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
··· 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} &ndash; {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
··· 1 - export interface EventProps { 2 - eventType: string; 3 - eventTitle: string; 4 - eventDescription: string; 5 - eventStartYear: string; 6 - eventEndYear: string; 7 - eventAffiliation: string; 8 - className?: string; 9 - }
+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
··· 1 - import { EventProps } from './Event.types'; 1 + import { CardEventProps } from '@/components/CardEvent/CardEvent.types'; 2 2 3 3 export interface EventsProps { 4 - items: EventProps[]; 4 + items: CardEventProps[]; 5 5 className?: string; 6 6 }
+3
src/components/Fact/Fact.styles.ts
··· 1 + export const factStyles = { 2 + wrapper: 'px-8 py-6 bg-neutral-50 dark:bg-neutral-800 rounded-lg shadow-sm', 3 + };
+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
··· 1 + export interface FactProps { 2 + title: string; 3 + subtitle: string; 4 + className?: string; 5 + }
-3
src/components/Facts/Fact.styles.ts
··· 1 - export const factStyles = { 2 - wrapper: 'px-8 py-6 bg-neutral-50 dark:bg-neutral-800 rounded-lg shadow-sm', 3 - };
-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
··· 1 - export interface FactProps { 2 - title: string; 3 - subtitle: string; 4 - className?: string; 5 - }
+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
··· 1 - import { FactProps } from './Fact.types'; 1 + import { CardFactProps } from '@/components/CardFact/CardFact.types'; 2 2 3 3 export interface FactsProps { 4 - items: FactProps[]; 4 + items: CardFactProps[]; 5 5 className?: string; 6 6 }
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 1 + import { ReactNode } from 'react'; 2 + 3 + export type ListItemSize = 'sm' | 'md' | 'lg'; 4 + 5 + export interface ListItemProps { 6 + children: ReactNode; 7 + size?: ListItemSize; 8 + className?: string; 9 + }
+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
··· 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
··· 1 + import { ReactNode } from 'react'; 2 + 3 + export type MarkSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl'; 4 + 5 + export interface MarkProps { 6 + children: ReactNode; 7 + size?: MarkSize; 8 + className?: string; 9 + }
+2
src/components/Mark/index.ts
··· 1 + export { Mark } from './Mark'; 2 + export type { MarkProps, MarkSize } from './Mark.types';
+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
··· 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
··· 1 + import { ReactNode } from 'react'; 2 + 3 + export type NumberStyle = 'decimal' | 'lower-alpha' | 'upper-alpha' | 'lower-roman' | 'upper-roman'; 4 + 5 + export interface OrderedListProps { 6 + children: ReactNode; 7 + numberStyle?: NumberStyle; 8 + className?: string; 9 + }
+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
··· 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 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
··· 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
··· 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
··· 1 + import { ReactNode } from 'react'; 2 + 3 + export type QuoteSize = 'md' | 'lg' | 'xl'; 4 + 5 + export interface QuoteProps { 6 + children: ReactNode; 7 + size?: QuoteSize; 8 + cite?: string; // Optional citation URL 9 + className?: string; 10 + }
+2
src/components/Quote/index.ts
··· 1 + export { Quote } from './Quote'; 2 + export type { QuoteProps, QuoteSize } from './Quote.types';
+1 -1
src/components/Section/Section.styles.ts
··· 1 1 // Section inherits colors from page theme context 2 - export const section = 'w-full pt-32 pb-48 h-min-screen'; 2 + export const section = 'w-full h-min-screen';
+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
··· 1 + export const skillStyles = { 2 + wrapper: 'grow w-full px-8 pt-8 pb-12 bg-bones-white dark:bg-bones-black flex flex-col gap-8', 3 + } as const;
+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
··· 1 + import { ArtworkName } from '@/components/Artwork/Artwork.types'; 2 + 3 + export interface SkillProps { 4 + skillArtwork: ArtworkName; 5 + skillTitle: string; 6 + skillDescription: string; 7 + skillApproach: string; 8 + skillTags: string; //NEW 9 + className?: string; 10 + }
-3
src/components/Skills/Skill.styles.ts
··· 1 - export const skillStyles = { 2 - wrapper: 'grow w-full px-8 pt-8 pb-12 bg-bones-white dark:bg-bones-black flex flex-col gap-8', 3 - } as const;
-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
··· 1 - import { ArtworkName } from '@/components/Artwork/Artwork.types'; 2 - 3 - export interface SkillProps { 4 - skillArtwork: ArtworkName; 5 - skillTitle: string; 6 - skillDescription: string; 7 - skillApproach: string; 8 - skillTags: string; //NEW 9 - className?: string; 10 - }
+1 -1
src/components/Skills/Skills.tsx
··· 1 1 import { cn } from '@/lib/utils'; 2 2 import React from 'react'; 3 - import { Skill } from './Skill'; 3 + import { Skill } from '@/components/Skill/Skill'; 4 4 import { skillsStyles } from './Skills.styles'; 5 5 import { SkillsProps } from './Skills.types'; 6 6
+1 -1
src/components/Skills/Skills.types.ts
··· 1 - import { SkillProps } from './Skill.types'; 1 + import { SkillProps } from '@/components/Skill/Skill.types'; 2 2 3 3 export interface SkillsProps { 4 4 items: SkillProps[];
+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
··· 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
··· 1 + import { ReactNode } from 'react'; 2 + 3 + export type SpanSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl'; 4 + 5 + export interface SpanProps { 6 + children: ReactNode; 7 + size?: SpanSize; 8 + className?: string; 9 + }
+2
src/components/Span/index.ts
··· 1 + export { Span } from './Span'; 2 + export type { SpanProps, SpanSize } from './Span.types';
+2 -1
src/components/TLDRProfile/TLDRProfile.constants.ts
··· 7 7 } as const; 8 8 9 9 export const SECTIONS = { 10 - specializations: 'Specializations', 10 + skills: 'Skills', 11 + languages: 'Languages', 11 12 connect: 'Connect', 12 13 } as const; 13 14
+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
··· 1 + export const tagStyles = { 2 + base: 'px-2 py-1 font-medium', 3 + } as const;
+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
··· 1 + export interface TagProps { 2 + label: string; 3 + className?: string; 4 + }
-3
src/components/Tags/Tag.styles.ts
··· 1 - export const tagStyles = { 2 - base: 'px-2 py-1 bg-bones-black-5 text-bones-black font-medium dark:bg-bones-white-10 dark:text-bones-white', 3 - } as const;
-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
··· 1 - export interface TagProps { 2 - label: string; 3 - className?: string; 4 - }
+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
··· 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
··· 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
··· 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
··· 1 + import { ReactNode } from 'react'; 2 + 3 + export type BulletStyle = 'disc' | 'circle' | 'square' | 'arrow' | 'dash' | 'angle'; 4 + 5 + export interface UnorderedListProps { 6 + children: ReactNode; 7 + bullet?: BulletStyle; 8 + className?: string; 9 + }
-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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 4 4 import { BrowserRouter as Router } from 'react-router-dom'; 5 5 import App from './App.tsx'; 6 6 import './index.css'; 7 - import './global.css'; 8 7 9 8 ReactDOM.createRoot(document.getElementById('root')!).render( 10 9 <React.StrictMode>
+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&apos;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&apos;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&apos;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&apos;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&apos;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&apos;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&apos;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&apos; 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&apos;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
··· 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&apos;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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 1 - /** 2 - * AT Protocol fetch result types 3 - */ 4 - 5 - /** 6 - * Generic fetch result with loading and error states 7 - */ 8 - export interface FetchResult<T> { 9 - data: T | null; 10 - loading: boolean; 11 - error: string | null; 12 - }
+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
··· 13 13 fontFamily: { 14 14 sans: ['DM Sans', 'sans-serif'], 15 15 serif: ['DM Serif Display', 'serif'], 16 + mono: ['DM Mono', 'monospace'], 16 17 }, 17 18 colors: { 18 19 // Mono
+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 }