Compare commits

...

10 Commits

Author SHA1 Message Date
daveadmin c2975f9a89 feat(business): wire BusinessIssue island to live PHP backend
Deploy Astro Frontend / deploy (push) Waiting to run
Render the DB-driven BusinessIssue island on the Business page and let
the Playwright config target a remote BASE_URL for live verification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 06:49:12 +02:00
daveadmin c5464aa0aa test: add Playwright smoke suite (cookie banner, lang switcher, FamilyAtlas, WritingIssue)
34 tests across desktop + mobile (Chromium/Pixel 5). Verifies all client-side
React islands and interactive features that astro build/WebFetch cannot confirm.
API-dependent WritingIssue shows graceful error against local preview; resolves
on the live site where the PHP backend is available.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 19:40:41 +02:00
daveadmin 61503fdac4 fix(pre-release): remove draft text, fix [object Object] on ai-lab, clean slug routing
- ai-lab.astro: wrap venture summaries in LocaleCopy (was rendering [object Object])
- [slug].astro: add writing to exclusion list; remove draft sidebar paragraph
- site.ts: redirect family slug to /family-lab
- cookies.astro, privacy.astro: strip draft-only language from copy
- education.astro: remove unfinished-content bullet from editorial note

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 19:40:22 +02:00
daveadmin 6b509fe052 Add complete Open Graph metadata layer across all 20 pages
- Add og:title, og:description, og:image, og:url, og:type, og:site_name to BaseLayout
- Add og:locale (en_GB) + og:locale:alternate (fr_FR, nb_NO) multilingual signals
- Add article:published_time, article:section, article:author on all 5 article pages
- Add Twitter/X summary_large_image card and canonical link on every page
- Generate 13 Jazz Noir branded 1200x630 PNG OG images (satori + resvg-js)
- Add scripts/generate-og-images.mjs + Special Elite font for future regeneration
- Add public/images/articles/ and public/assets/ which were previously untracked

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 01:38:51 +02:00
daveadmin e38e2bb68e Remove edition console, route tape, frontline ticker, and venture-desk header
- Delete EditionConsole React component and its cover-console wrapper
- Delete route-tape section (city stops list)
- Delete frontline ticker section (field notes)
- Delete venture-desk section-header (redundant description text)
- Clean up unused imports (EditionConsole, routeStops, fieldNotes)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 00:54:22 +02:00
daveadmin d8b44b34ce Locale bar full labels + broader FR/NB translation pass
- Locale bar now shows "EN edition / Cahier FR / NB utgave" (via localeMeta) instead of just EN/FR/NO
- Increase locale bar button size (font-size 0.88rem, padding 0.4/1.1rem, height 46px)
- education.astro: translate hero eyebrow, h1, lede, editorial note, bullets
- writing.astro: translate Boris Vian feature panel (title, subtitle, body, CTA)
- business.astro: translate AI Bubble feature panel (title, subtitle, body, CTA)
- boris-vian-2026.astro: translate eyebrow, sidebar, article kicker, source credits
- kongsberg-jazz-2026.astro: translate eyebrow, sidebar, field report labels, cross-promo, source credits
- ai-bubble-2026.astro: translate eyebrow, sidebar, field report labels, source credits
- Add untracked data files ai-bubble.ts and boris-vian.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 23:58:26 +02:00
daveadmin 2a06888fd6 Add full trilingual EN/FR/NB support across all pages and data
- Convert all data files (site, profile, norway, jazz, projects, cv) to Record<LocaleCode, string> fields
- Update all page templates to use LocaleCopy component for locale-aware rendering
- Fix CSS specificity conflict: .locale-copy .locale-copy__text (spec 20) now beats container span rules (spec 11)
- Update LocaleSwitcher, BaseLayout, and SectionCard for locale prop types
- Design system overhaul: Special Elite font, burgundy #8f2218, DG seal favicon

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 23:23:14 +02:00
daveadmin ffb368670c Fix OpenPanel consent tracking bootstrap 2026-05-14 15:51:06 +02:00
daveadmin 87bd5af809 Add OpenPanel analytics with cookie consent integration
Loads OpenPanel (analytics.bluenotelogic.com, client 6a6d90f8) only when user
accepts analytics cookies via the existing dg:cookie-consent consent event.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 14:41:59 +02:00
daveadmin 39712d8d5e Trigger clean GitHub Actions release 2026-04-07 17:55:32 +02:00
72 changed files with 3351 additions and 952 deletions
+1
View File
@@ -5,5 +5,6 @@ import react from '@astrojs/react';
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
site: 'https://davegilligan.com',
integrations: [react()] integrations: [react()]
}); });
+507
View File
@@ -15,6 +15,11 @@
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4" "react-dom": "^19.2.4"
}, },
"devDependencies": {
"@playwright/test": "^1.60.0",
"@resvg/resvg-js": "^2.6.2",
"satori": "^0.26.0"
},
"engines": { "engines": {
"node": ">=22.12.0" "node": ">=22.12.0"
} }
@@ -1382,6 +1387,250 @@
"integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@playwright/test": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.60.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@resvg/resvg-js": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz",
"integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==",
"dev": true,
"license": "MPL-2.0",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@resvg/resvg-js-android-arm-eabi": "2.6.2",
"@resvg/resvg-js-android-arm64": "2.6.2",
"@resvg/resvg-js-darwin-arm64": "2.6.2",
"@resvg/resvg-js-darwin-x64": "2.6.2",
"@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2",
"@resvg/resvg-js-linux-arm64-gnu": "2.6.2",
"@resvg/resvg-js-linux-arm64-musl": "2.6.2",
"@resvg/resvg-js-linux-x64-gnu": "2.6.2",
"@resvg/resvg-js-linux-x64-musl": "2.6.2",
"@resvg/resvg-js-win32-arm64-msvc": "2.6.2",
"@resvg/resvg-js-win32-ia32-msvc": "2.6.2",
"@resvg/resvg-js-win32-x64-msvc": "2.6.2"
}
},
"node_modules/@resvg/resvg-js-android-arm-eabi": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz",
"integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-android-arm64": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz",
"integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-darwin-arm64": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz",
"integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-darwin-x64": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz",
"integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-linux-arm-gnueabihf": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz",
"integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-linux-arm64-gnu": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz",
"integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-linux-arm64-musl": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz",
"integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-linux-x64-gnu": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz",
"integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-linux-x64-musl": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz",
"integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-win32-arm64-msvc": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz",
"integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-win32-ia32-msvc": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz",
"integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-win32-x64-msvc": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz",
"integrity": "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@rolldown/pluginutils": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.3", "version": "1.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
@@ -1835,6 +2084,23 @@
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@shuding/opentype.js": {
"version": "1.4.0-beta.0",
"resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz",
"integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fflate": "^0.7.3",
"string.prototype.codepointat": "^0.2.1"
},
"bin": {
"ot": "bin/ot"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -2121,6 +2387,16 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/base64-js": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.10.15", "version": "2.10.15",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.15.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.15.tgz",
@@ -2172,6 +2448,16 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/camelize": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001785", "version": "1.0.30001785",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001785.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001785.tgz",
@@ -2271,6 +2557,13 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/comma-separated-tokens": { "node_modules/comma-separated-tokens": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
@@ -2333,6 +2626,40 @@
"uncrypto": "^0.1.3" "uncrypto": "^0.1.3"
} }
}, },
"node_modules/css-background-parser": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz",
"integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==",
"dev": true,
"license": "MIT"
},
"node_modules/css-box-shadow": {
"version": "1.0.0-3",
"resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz",
"integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==",
"dev": true,
"license": "MIT"
},
"node_modules/css-color-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
"integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=4"
}
},
"node_modules/css-gradient-parser": {
"version": "0.0.17",
"resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.17.tgz",
"integrity": "sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=16"
}
},
"node_modules/css-select": { "node_modules/css-select": {
"version": "5.2.2", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
@@ -2349,6 +2676,18 @@
"url": "https://github.com/sponsors/fb55" "url": "https://github.com/sponsors/fb55"
} }
}, },
"node_modules/css-to-react-native": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
"integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"camelize": "^1.0.0",
"css-color-keywords": "^1.0.0",
"postcss-value-parser": "^4.0.2"
}
},
"node_modules/css-tree": { "node_modules/css-tree": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
@@ -2590,6 +2929,16 @@
"integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/emoji-regex-xs": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz",
"integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/entities": { "node_modules/entities": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
@@ -2658,6 +3007,13 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"dev": true,
"license": "MIT"
},
"node_modules/escape-string-regexp": { "node_modules/escape-string-regexp": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
@@ -2729,6 +3085,13 @@
} }
} }
}, },
"node_modules/fflate": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz",
"integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==",
"dev": true,
"license": "MIT"
},
"node_modules/flattie": { "node_modules/flattie": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz",
@@ -2982,6 +3345,19 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/hex-rgb": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz",
"integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/html-escaper": { "node_modules/html-escaper": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
@@ -3115,6 +3491,17 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/linebreak": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"base64-js": "0.0.8",
"unicode-trie": "^2.0.0"
}
},
"node_modules/longest-streak": { "node_modules/longest-streak": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
@@ -4145,6 +4532,24 @@
"integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/pako": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
"dev": true,
"license": "MIT"
},
"node_modules/parse-css-color": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz",
"integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "^1.1.4",
"hex-rgb": "^4.1.0"
}
},
"node_modules/parse-latin": { "node_modules/parse-latin": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz",
@@ -4199,6 +4604,53 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/playwright": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.60.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.8", "version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
@@ -4227,6 +4679,13 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/prismjs": { "node_modules/prismjs": {
"version": "1.30.0", "version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
@@ -4566,6 +5025,29 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/satori": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/satori/-/satori-0.26.0.tgz",
"integrity": "sha512-tkMFrfIs3l2mQ2JEcyW0ADTy3zGggFRFzi6Ef8YozQSFsFKEqaSO1Y8F9wJg4//PJGQauMalHGTUEkPrFwhVPA==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
"@shuding/opentype.js": "1.4.0-beta.0",
"css-background-parser": "^0.1.0",
"css-box-shadow": "1.0.0-3",
"css-gradient-parser": "^0.0.17",
"css-to-react-native": "^3.0.0",
"emoji-regex-xs": "^2.0.1",
"escape-html": "^1.0.3",
"linebreak": "^1.1.0",
"parse-css-color": "^0.2.1",
"postcss-value-parser": "^4.2.0",
"yoga-layout": "^3.2.1"
},
"engines": {
"node": ">=16"
}
},
"node_modules/sax": { "node_modules/sax": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
@@ -4694,6 +5176,13 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/string.prototype.codepointat": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz",
"integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==",
"dev": true,
"license": "MIT"
},
"node_modules/stringify-entities": { "node_modules/stringify-entities": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
@@ -4838,6 +5327,17 @@
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/unicode-trie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"pako": "^0.2.5",
"tiny-inflate": "^1.0.0"
}
},
"node_modules/unified": { "node_modules/unified": {
"version": "11.0.5", "version": "11.0.5",
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
@@ -5304,6 +5804,13 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/yoga-layout": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz",
"integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==",
"dev": true,
"license": "MIT"
},
"node_modules/zod": { "node_modules/zod": {
"version": "4.3.6", "version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+8 -1
View File
@@ -11,7 +11,9 @@
"prebuild": "node scripts/sync-family-lab.mjs", "prebuild": "node scripts/sync-family-lab.mjs",
"build": "astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro" "astro": "astro",
"og": "node scripts/generate-og-images.mjs",
"test": "playwright test"
}, },
"dependencies": { "dependencies": {
"@astrojs/react": "^5.0.2", "@astrojs/react": "^5.0.2",
@@ -20,5 +22,10 @@
"astro": "^6.1.3", "astro": "^6.1.3",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4" "react-dom": "^19.2.4"
},
"devDependencies": {
"@playwright/test": "^1.60.0",
"@resvg/resvg-js": "^2.6.2",
"satori": "^0.26.0"
} }
} }
+22
View File
@@ -0,0 +1,22 @@
import { defineConfig, devices } from "@playwright/test";
const baseURL = process.env.BASE_URL ?? "http://localhost:4321";
const isRemote = Boolean(process.env.BASE_URL);
export default defineConfig({
testDir: "./tests",
timeout: 15_000,
use: { baseURL },
webServer: isRemote
? undefined
: {
command: "npm run preview",
url: "http://localhost:4321",
reuseExistingServer: true,
timeout: 30_000,
},
projects: [
{ name: "desktop", use: { ...devices["Desktop Chrome"] } },
{ name: "mobile", use: { ...devices["Pixel 5"] } },
],
});
+33
View File
@@ -0,0 +1,33 @@
<svg width="1200" height="760" viewBox="0 0 1200 760" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="760" fill="#F7F0E0"></rect>
<rect x="44" y="44" width="1112" height="672" rx="28" fill="#FFF9EF" stroke="#201914" stroke-width="2"></rect>
<rect x="84" y="92" width="312" height="230" rx="24" fill="#151A21"></rect>
<rect x="424" y="92" width="312" height="230" rx="24" fill="#F0E3C9" stroke="#201914" stroke-width="2"></rect>
<rect x="764" y="92" width="312" height="230" rx="24" fill="#D8E5DF" stroke="#201914" stroke-width="2"></rect>
<rect x="116" y="126" width="136" height="14" rx="7" fill="#C28A47"></rect>
<rect x="116" y="164" width="236" height="10" rx="5" fill="#46505A"></rect>
<rect x="116" y="188" width="210" height="10" rx="5" fill="#46505A"></rect>
<rect x="116" y="212" width="228" height="10" rx="5" fill="#46505A"></rect>
<rect x="456" y="126" width="116" height="14" rx="7" fill="#1A5960"></rect>
<rect x="456" y="164" width="238" height="10" rx="5" fill="#B3A68B"></rect>
<rect x="456" y="188" width="214" height="10" rx="5" fill="#B3A68B"></rect>
<rect x="456" y="212" width="198" height="10" rx="5" fill="#B3A68B"></rect>
<rect x="796" y="126" width="134" height="14" rx="7" fill="#5D2F2B"></rect>
<rect x="796" y="164" width="236" height="10" rx="5" fill="#A7BBB5"></rect>
<rect x="796" y="188" width="214" height="10" rx="5" fill="#A7BBB5"></rect>
<rect x="796" y="212" width="198" height="10" rx="5" fill="#A7BBB5"></rect>
<path d="M396 206H424" stroke="#201914" stroke-width="4" stroke-linecap="round"></path>
<path d="M736 206H764" stroke="#201914" stroke-width="4" stroke-linecap="round"></path>
<path d="M412 192L424 206L412 220" stroke="#201914" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M752 192L764 206L752 220" stroke="#201914" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path>
<rect x="84" y="384" width="992" height="276" rx="24" fill="#161B22"></rect>
<rect x="118" y="420" width="184" height="18" rx="9" fill="#C28A47"></rect>
<rect x="118" y="468" width="384" height="10" rx="5" fill="#46505A"></rect>
<rect x="118" y="492" width="338" height="10" rx="5" fill="#46505A"></rect>
<rect x="118" y="516" width="404" height="10" rx="5" fill="#46505A"></rect>
<rect x="118" y="552" width="220" height="76" rx="16" fill="#1A5960"></rect>
<rect x="362" y="552" width="220" height="76" rx="16" fill="#5D2F2B"></rect>
<rect x="606" y="552" width="220" height="76" rx="16" fill="#2B323C"></rect>
<rect x="850" y="552" width="192" height="76" rx="16" fill="#F0E3C9"></rect>
<rect x="878" y="582" width="136" height="12" rx="6" fill="#B39B77"></rect>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+35
View File
@@ -0,0 +1,35 @@
<svg width="1200" height="800" viewBox="0 0 1200 800" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="800" fill="#FBF4E6"></rect>
<rect x="40" y="40" width="1120" height="720" rx="28" fill="#FFF9EF" stroke="#201914" stroke-width="2"></rect>
<rect x="86" y="92" width="192" height="130" rx="22" fill="#F0E3C9" stroke="#201914" stroke-width="2"></rect>
<rect x="324" y="92" width="192" height="130" rx="22" fill="#D8E5DF" stroke="#201914" stroke-width="2"></rect>
<rect x="562" y="92" width="192" height="130" rx="22" fill="#F3D9D2" stroke="#201914" stroke-width="2"></rect>
<rect x="800" y="92" width="312" height="130" rx="22" fill="#161B22" stroke="#201914" stroke-width="2"></rect>
<rect x="112" y="122" width="78" height="12" rx="6" fill="#1A5960"></rect>
<rect x="112" y="154" width="140" height="10" rx="5" fill="#B8AA8E"></rect>
<rect x="112" y="176" width="120" height="10" rx="5" fill="#B8AA8E"></rect>
<rect x="350" y="122" width="82" height="12" rx="6" fill="#5D2F2B"></rect>
<rect x="350" y="154" width="130" height="10" rx="5" fill="#9EAFAB"></rect>
<rect x="350" y="176" width="112" height="10" rx="5" fill="#9EAFAB"></rect>
<rect x="588" y="122" width="96" height="12" rx="6" fill="#C28A47"></rect>
<rect x="588" y="154" width="132" height="10" rx="5" fill="#B8A19D"></rect>
<rect x="588" y="176" width="112" height="10" rx="5" fill="#B8A19D"></rect>
<rect x="828" y="122" width="124" height="12" rx="6" fill="#C28A47"></rect>
<rect x="828" y="154" width="246" height="10" rx="5" fill="#46505A"></rect>
<rect x="828" y="176" width="214" height="10" rx="5" fill="#46505A"></rect>
<circle cx="182" cy="420" r="82" fill="#1A5960"></circle>
<circle cx="418" cy="420" r="82" fill="#C28A47"></circle>
<circle cx="654" cy="420" r="82" fill="#5D2F2B"></circle>
<circle cx="890" cy="420" r="82" fill="#11151C"></circle>
<circle cx="1018" cy="420" r="54" fill="#E7DBC1" stroke="#201914" stroke-width="2"></circle>
<path d="M264 420H336" stroke="#201914" stroke-width="4" stroke-linecap="round"></path>
<path d="M500 420H572" stroke="#201914" stroke-width="4" stroke-linecap="round"></path>
<path d="M736 420H808" stroke="#201914" stroke-width="4" stroke-linecap="round"></path>
<path d="M972 420H964" stroke="#201914" stroke-width="4" stroke-linecap="round"></path>
<rect x="110" y="607" width="980" height="92" rx="22" fill="#201914"></rect>
<rect x="144" y="640" width="168" height="12" rx="6" fill="#C28A47"></rect>
<rect x="344" y="640" width="168" height="12" rx="6" fill="#F7F0E0"></rect>
<rect x="544" y="640" width="168" height="12" rx="6" fill="#1A5960"></rect>
<rect x="744" y="640" width="168" height="12" rx="6" fill="#F7F0E0"></rect>
<rect x="944" y="640" width="112" height="12" rx="6" fill="#C28A47"></rect>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

+32
View File
@@ -0,0 +1,32 @@
<svg width="1200" height="900" viewBox="0 0 1200 900" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="900" fill="#F7F0E0"></rect>
<rect x="36" y="36" width="1128" height="828" rx="28" fill="#FFF9EF" stroke="#201914" stroke-width="2"></rect>
<rect x="72" y="80" width="540" height="420" rx="22" fill="#11151C"></rect>
<rect x="96" y="104" width="492" height="38" rx="10" fill="#202A34"></rect>
<circle cx="118" cy="123" r="6" fill="#D86D56"></circle>
<circle cx="140" cy="123" r="6" fill="#D8B156"></circle>
<circle cx="162" cy="123" r="6" fill="#4E8F8C"></circle>
<rect x="96" y="170" width="186" height="108" rx="18" fill="#1B2F3A"></rect>
<rect x="302" y="170" width="286" height="108" rx="18" fill="#5D2F2B"></rect>
<rect x="96" y="298" width="492" height="178" rx="18" fill="#151A21"></rect>
<path d="M120 400C178 336 223 380 281 329C339 278 393 326 451 274C491 238 530 240 564 262V452H120V400Z" fill="#5A7E76"></path>
<path d="M120 430C168 389 223 413 272 372C321 331 380 370 434 327C486 285 526 292 564 316V452H120V430Z" fill="#C28A47"></path>
<circle cx="736" cy="178" r="102" fill="#EFE2C5" stroke="#201914" stroke-width="2"></circle>
<path d="M694 217C694 181.654 722.654 153 758 153V153C793.346 153 822 181.654 822 217V288H694V217Z" fill="#1A5960"></path>
<rect x="676" y="288" width="164" height="58" rx="16" fill="#201914"></rect>
<rect x="706" y="314" width="104" height="6" rx="3" fill="#F7F0E0"></rect>
<rect x="676" y="404" width="418" height="154" rx="24" fill="#F2E3C7" stroke="#201914" stroke-width="2"></rect>
<rect x="706" y="434" width="124" height="14" rx="7" fill="#1A5960"></rect>
<rect x="706" y="470" width="338" height="14" rx="7" fill="#D3C2A1"></rect>
<rect x="706" y="502" width="296" height="14" rx="7" fill="#D3C2A1"></rect>
<rect x="706" y="534" width="198" height="14" rx="7" fill="#D3C2A1"></rect>
<rect x="676" y="598" width="418" height="190" rx="24" fill="#151A21"></rect>
<rect x="706" y="628" width="160" height="14" rx="7" fill="#C28A47"></rect>
<rect x="706" y="662" width="114" height="84" rx="14" fill="#1A5960"></rect>
<rect x="838" y="662" width="114" height="84" rx="14" fill="#5D2F2B"></rect>
<rect x="970" y="662" width="94" height="84" rx="14" fill="#2C333B"></rect>
<path d="M748 116L780 149" stroke="#201914" stroke-width="3" stroke-linecap="round"></path>
<path d="M780 149L805 125" stroke="#201914" stroke-width="3" stroke-linecap="round"></path>
<path d="M916 122L958 122" stroke="#201914" stroke-width="3" stroke-linecap="round"></path>
<path d="M937 101L937 143" stroke="#201914" stroke-width="3" stroke-linecap="round"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+27
View File
@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none">
<defs>
<path id="rim-top" d="M 100 100 m -82 0 a 82 82 0 0 1 164 0"></path>
<path id="rim-bot" d="M 100 100 m 82 0 a 82 82 0 0 1 -164 0"></path>
</defs>
<circle cx="100" cy="100" r="92" stroke="#8f2218" stroke-width="1.4"></circle>
<circle cx="100" cy="100" r="84" stroke="#8f2218" stroke-width="0.6"></circle>
<g fill="#8f2218" font-family="Special Elite, Courier, monospace" font-size="11" letter-spacing="3">
<text><textPath href="#rim-top" startOffset="50%" text-anchor="middle">DAVE · GILLIGAN</textPath></text>
<text><textPath href="#rim-bot" startOffset="50%" text-anchor="middle">· KONGSBERG · EDITION ·</textPath></text>
</g>
<path d="M 100 70&#xA; C 120 70 130 88 120 102&#xA; C 112 113 96 113 91 102&#xA; C 87 92 96 84 105 86&#xA; C 113 89 113 99 106 102&#xA; C 100 105 95 100 97 95" stroke="#8f2218" stroke-width="1.6" stroke-linecap="round"></path>
<g font-family="Instrument Serif, serif" fill="#0e0c08">
<text x="100" y="142" font-size="32" text-anchor="middle" letter-spacing="-1">DG</text>
</g>
<g fill="#8f2218">
<circle cx="100" cy="20" r="1.8"></circle>
<circle cx="180" cy="100" r="1.8"></circle>
<circle cx="100" cy="180" r="1.8"></circle>
<circle cx="20" cy="100" r="1.8"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+38
View File
@@ -0,0 +1,38 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 64" fill="none">
<g stroke="#0e0c08" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" fill="none">
<g transform="translate(32, 32)">
<circle cx="0" cy="-8" r="1.6" fill="#0e0c08"></circle>
<circle cx="-7" cy="5" r="1.6" fill="#0e0c08"></circle>
<circle cx="7" cy="5" r="1.6" fill="#0e0c08"></circle>
</g>
<g transform="translate(96, 32)">
<path d="M 0 -10 c -6 0 -10 4 -10 10 c 0 -6 4 -10 10 -10 c 6 0 10 4 10 10 c 0 -6 -4 -10 -10 -10 z"></path>
<path d="M -10 0 c 0 6 4 10 10 10 c -6 0 -10 -4 -10 -10 c 0 6 4 10 10 10 c 6 0 10 -4 10 -10"></path>
</g>
<g transform="translate(160, 32)" stroke-width="1.8">
<path d="M 4 -8 c -6 0 -8 4 -8 6 c 0 4 4 4 4 8 c 0 4 -4 4 -4 4 c 0 0 4 2 8 2"></path>
</g>
<g transform="translate(224, 32)">
<path d="M -10 -4 l 14 -2 l 8 6 l -8 6 l -14 -2 z"></path>
<circle cx="-10" cy="0" r="2" fill="#0e0c08"></circle>
</g>
<g transform="translate(288, 32)">
<path d="M -2 -10 c -5 0 -8 3 -8 6 c 0 3 3 6 8 6"></path>
<path d="M 0 -10 v 22 M 5 -10 v 22"></path>
</g>
<g transform="translate(352, 32)">
<path d="M 0 -10 l 3 7 l 7 3 l -7 3 l -3 7 l -3 -7 l -7 -3 l 7 -3 z" fill="#0e0c08"></path>
</g>
<g transform="translate(416, 32)" stroke="#8f2218">
<path d="M 0 -8 c 7 0 11 5 9 11 c -2 5 -8 6 -11 3 c -3 -3 -1 -7 2 -7 c 2 0 3 2 2 4"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none" stroke="#8f2218" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M 32 18&#xA; C 46 18 54 30 50 42&#xA; C 47 51 36 54 28 49&#xA; C 21 44 22 35 28 31&#xA; C 34 28 41 32 41 38&#xA; C 41 43 37 46 33 45&#xA; C 30 44 29 41 31 39"></path>
</svg>

After

Width:  |  Height:  |  Size: 403 B

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 280">
<rect width="480" height="280" rx="28" fill="#68131a"></rect>
<rect x="24" y="24" width="432" height="232" rx="22" fill="none" stroke="#d8bb74" stroke-width="2"></rect>
<text x="240" y="94" fill="#ffffff" font-family="Georgia, serif" font-size="46" font-weight="700" text-anchor="middle">ESCP</text>
<text x="240" y="124" fill="#d8bb74" font-family="Arial, sans-serif" font-size="18" letter-spacing="6" text-anchor="middle">AND</text>
<text x="240" y="170" fill="#ffffff" font-family="Georgia, serif" font-size="36" font-weight="700" text-anchor="middle">DARLA MOORE</text>
<line x1="116" y1="188" x2="364" y2="188" stroke="#d8bb74" stroke-width="1.5"></line>
<text x="240" y="214" fill="#d8bb74" font-family="Arial, sans-serif" font-size="15" letter-spacing="4" text-anchor="middle">DUAL DEGREE PROGRAM</text>
<text x="240" y="236" fill="#d2a1a1" font-family="Arial, sans-serif" font-size="13" letter-spacing="3.5" text-anchor="middle">PARIS AND COLUMBIA</text>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 280">
<rect width="480" height="280" rx="28" fill="#1c3453"></rect>
<rect x="24" y="24" width="432" height="232" rx="22" fill="none" stroke="#6dc1d0" stroke-width="2"></rect>
<text x="240" y="94" fill="#81d6e1" font-family="Georgia, serif" font-size="24" letter-spacing="5" text-anchor="middle">EUROPEAN</text>
<text x="240" y="144" fill="#ffffff" font-family="Georgia, serif" font-size="44" font-weight="700" text-anchor="middle">UNIVERSITY</text>
<line x1="126" y1="164" x2="354" y2="164" stroke="#6dc1d0" stroke-width="1.5"></line>
<text x="240" y="198" fill="#81d6e1" font-family="Arial, sans-serif" font-size="17" letter-spacing="4" text-anchor="middle">INSTITUT COOREMAN</text>
<text x="240" y="224" fill="#93b7d3" font-family="Arial, sans-serif" font-size="14" letter-spacing="4" text-anchor="middle">BRUSSELS</text>
</svg>

After

Width:  |  Height:  |  Size: 901 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 280">
<rect width="480" height="280" rx="28" fill="#7a1718"></rect>
<rect x="24" y="24" width="432" height="232" rx="22" fill="none" stroke="#d8bb74" stroke-width="2"></rect>
<text x="240" y="96" fill="#f1d08d" font-family="Georgia, serif" font-size="28" letter-spacing="5" text-anchor="middle">UNIWERSYTET</text>
<text x="240" y="146" fill="#ffffff" font-family="Georgia, serif" font-size="42" font-weight="700" text-anchor="middle">JAGIELLONIAN</text>
<line x1="122" y1="164" x2="358" y2="164" stroke="#d8bb74" stroke-width="1.5"></line>
<text x="240" y="198" fill="#f1d08d" font-family="Arial, sans-serif" font-size="17" letter-spacing="5" text-anchor="middle">EST. 1364</text>
<text x="240" y="224" fill="#dcb0a8" font-family="Arial, sans-serif" font-size="14" letter-spacing="4" text-anchor="middle">KRAKOW</text>
</svg>

After

Width:  |  Height:  |  Size: 896 B

+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 280">
<rect width="480" height="280" rx="28" fill="#08284b"></rect>
<rect x="24" y="24" width="432" height="232" rx="22" fill="none" stroke="#c8a85b" stroke-width="2"></rect>
<text x="240" y="118" fill="#f7e7bf" font-family="Georgia, serif" font-size="72" font-weight="700" text-anchor="middle">USN</text>
<line x1="132" y1="138" x2="348" y2="138" stroke="#c8a85b" stroke-width="2"></line>
<text x="240" y="174" fill="#ffffff" font-family="Arial, sans-serif" font-size="18" letter-spacing="2.5" text-anchor="middle">UNIVERSITY OF SOUTH-EASTERN NORWAY</text>
<text x="240" y="208" fill="#9eb4d8" font-family="Arial, sans-serif" font-size="15" letter-spacing="5" text-anchor="middle">KONGSBERG</text>
</svg>

After

Width:  |  Height:  |  Size: 774 B

+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 280">
<rect width="480" height="280" rx="28" fill="#123c65"></rect>
<rect x="24" y="24" width="432" height="232" rx="22" fill="none" stroke="#ffffff" stroke-width="2"></rect>
<text x="240" y="114" fill="#ffffff" font-family="Georgia, serif" font-size="52" font-weight="700" text-anchor="middle">VILLANOVA</text>
<line x1="102" y1="136" x2="378" y2="136" stroke="#93c1dd" stroke-width="2"></line>
<text x="240" y="176" fill="#93c1dd" font-family="Arial, sans-serif" font-size="21" letter-spacing="6" text-anchor="middle">UNIVERSITY</text>
<text x="240" y="212" fill="#c1dded" font-family="Georgia, serif" font-size="16" font-style="italic" text-anchor="middle">Veritas, Unitas, Caritas</text>
</svg>

After

Width:  |  Height:  |  Size: 767 B

+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 120" fill="none">
<text x="0" y="74" font-family="Instrument Serif, serif" font-size="84" fill="#120f0a" letter-spacing="-3">Dave Gilligan</text>
<text x="2" y="100" font-family="Special Elite, monospace" font-size="13" fill="#1f5d5f" letter-spacing="5">DAVEGILLIGAN.COM</text>
<g transform="translate(456, 60)" stroke="#8f2218" stroke-width="1.4" fill="none">
<path d="M 0 0 c -6 -6 -6 -14 0 -20 c 6 6 6 14 0 20 z"></path>
<path d="M -10 -10 c 6 -6 14 -6 20 0 c -6 6 -14 6 -20 0 z"></path>
<circle cx="0" cy="-10" r="2.5" fill="#8f2218"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 642 B

+27 -9
View File
@@ -1,9 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" /> <defs>
<style> <path id="rim-top" d="M 100 100 m -82 0 a 82 82 0 0 1 164 0"></path>
path { fill: #000; } <path id="rim-bot" d="M 100 100 m 82 0 a 82 82 0 0 1 -164 0"></path>
@media (prefers-color-scheme: dark) { </defs>
path { fill: #FFF; }
} <circle cx="100" cy="100" r="92" stroke="#8f2218" stroke-width="1.4"></circle>
</style> <circle cx="100" cy="100" r="84" stroke="#8f2218" stroke-width="0.6"></circle>
</svg>
<g fill="#8f2218" font-family="Special Elite, Courier, monospace" font-size="11" letter-spacing="3">
<text><textPath href="#rim-top" startOffset="50%" text-anchor="middle">DAVE · GILLIGAN</textPath></text>
<text><textPath href="#rim-bot" startOffset="50%" text-anchor="middle">· KONGSBERG · EDITION ·</textPath></text>
</g>
<path d="M 100 70&#xA; C 120 70 130 88 120 102&#xA; C 112 113 96 113 91 102&#xA; C 87 92 96 84 105 86&#xA; C 113 89 113 99 106 102&#xA; C 100 105 95 100 97 95" stroke="#8f2218" stroke-width="1.6" stroke-linecap="round"></path>
<g font-family="Instrument Serif, serif" fill="#0e0c08">
<text x="100" y="142" font-size="32" text-anchor="middle" letter-spacing="-1">DG</text>
</g>
<g fill="#8f2218">
<circle cx="100" cy="20" r="1.8"></circle>
<circle cx="180" cy="100" r="1.8"></circle>
<circle cx="100" cy="180" r="1.8"></circle>
<circle cx="20" cy="100" r="1.8"></circle>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 749 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.
+176
View File
@@ -0,0 +1,176 @@
import { readFileSync, mkdirSync, writeFileSync } from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import satori from "satori";
import { Resvg } from "@resvg/resvg-js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const outDir = join(__dirname, "../public/images/og");
const fontPath = join(__dirname, "fonts/SpecialElite-Regular.ttf");
mkdirSync(outDir, { recursive: true });
const fontData = readFileSync(fontPath);
const W = 1200;
const H = 630;
const NAVY = "#0a0f1e";
const GOLD = "#c9a84c";
const CREAM = "#f6f0e1";
const MUTED = "#7a8aaa";
const sections = [
{ slug: "home", title: "Dave Gilligan", strap: "Private AI. Jazz rooms. Civic weather. Pataphysical field notes." },
{ slug: "business", title: "Business", strap: "Operator studies for adults tired of consultant vapor." },
{ slug: "education", title: "Education", strap: "Degrees as cities, schools as chapters, study as migration." },
{ slug: "writing", title: "Writing", strap: "Boris Vian, jazz syntax, and the science of glorious exception." },
{ slug: "jazz-music", title: "Jazz and Music", strap: "Listening notes, Kongsberg nights, Caveau memories." },
{ slug: "ai-lab", title: "AI Lab", strap: "Forward-looking, grounded, open-source friendly." },
{ slug: "norway", title: "Norway", strap: "Field reports from Norwegian civic and family life." },
{ slug: "cv", title: "CV", strap: "Professional, legible, confident." },
{ slug: "family", title: "Family", strap: "The soft archive, still edited like it matters." },
{ slug: "fun-postings",title: "Fun Postings", strap: "Useful nonsense, neatly set." },
{ slug: "languages", title: "Languages", strap: "Polyglot, sly, welcoming." },
{ slug: "projects", title: "Projects", strap: "Builder energy, clean receipts." },
{ slug: "family-lab", title: "Family Lab", strap: "Private archive, atlas, memory room." },
];
function buildNode({ title, strap }) {
return {
type: "div",
props: {
style: {
display: "flex",
flexDirection: "column",
width: W,
height: H,
background: NAVY,
position: "relative",
},
children: [
// top gold rule
{
type: "div",
props: {
style: {
position: "absolute",
top: 0,
left: 0,
width: W,
height: 8,
background: GOLD,
},
},
},
// domain label
{
type: "div",
props: {
style: {
position: "absolute",
top: 28,
left: 64,
fontFamily: "SpecialElite",
fontSize: 16,
color: GOLD,
letterSpacing: "0.18em",
textTransform: "uppercase",
},
children: "DAVEGILLIGAN.COM",
},
},
// left accent line
{
type: "div",
props: {
style: {
position: "absolute",
top: 64,
left: 64,
width: 3,
height: H - 128,
background: GOLD,
opacity: 0.35,
},
},
},
// section title
{
type: "div",
props: {
style: {
position: "absolute",
top: 200,
left: 96,
right: 64,
fontFamily: "SpecialElite",
fontSize: title.length > 12 ? 64 : 80,
color: CREAM,
lineHeight: 1.1,
},
children: title,
},
},
// strap line
{
type: "div",
props: {
style: {
position: "absolute",
top: title.length > 12 ? 330 : 360,
left: 96,
right: 64,
fontFamily: "SpecialElite",
fontSize: 22,
color: MUTED,
lineHeight: 1.5,
},
children: strap,
},
},
// bottom label
{
type: "div",
props: {
style: {
position: "absolute",
bottom: 36,
left: 96,
fontFamily: "SpecialElite",
fontSize: 14,
color: GOLD,
letterSpacing: "0.12em",
textTransform: "uppercase",
},
children: "Blue Note Logic · Kongsberg",
},
},
],
},
};
}
for (const section of sections) {
const svg = await satori(buildNode(section), {
width: W,
height: H,
fonts: [
{
name: "SpecialElite",
data: fontData,
weight: 400,
style: "normal",
},
],
});
const resvg = new Resvg(svg, {
fitTo: { mode: "width", value: W },
});
const png = resvg.render().asPng();
const outPath = join(outDir, `${section.slug}.png`);
writeFileSync(outPath, png);
console.log(`${section.slug}.png (${(png.length / 1024).toFixed(0)} KB)`);
}
console.log(`\nAll ${sections.length} OG images written to public/images/og/`);
+1
View File
@@ -129,6 +129,7 @@ import { cookieBannerCopy } from "../data/locales";
if (existing) { if (existing) {
applyPreference(existing); applyPreference(existing);
closeBanner(); closeBanner();
window.dispatchEvent(new CustomEvent("dg:cookie-consent", { detail: { preference: existing } }));
return; return;
} }
+11 -9
View File
@@ -1,12 +1,14 @@
--- ---
import type { LocaleCode } from "../data/locales";
import LocaleCopy from "./LocaleCopy.astro";
import SectionMark from "./SectionMark.astro"; import SectionMark from "./SectionMark.astro";
interface Props { interface Props {
slug: string; slug: string;
label: string; label: string;
title: string; title: string;
summary: string; summary: Record<LocaleCode, string>;
tone: string; tone: Record<LocaleCode, string>;
href?: string; href?: string;
} }
@@ -59,7 +61,7 @@ const sectionArt: Record<
caption: "French, English, Norwegian, and trouble", caption: "French, English, Norwegian, and trouble",
}, },
"ai-lab": { "ai-lab": {
src: "/images/ai-lab/corpus-grid.svg", src: "/assets/ai-lab/corpus-grid.svg",
alt: "Illustrated knowledge corpus grid.", alt: "Illustrated knowledge corpus grid.",
eyebrow: "Machine room", eyebrow: "Machine room",
caption: "Private memory with cited answers", caption: "Private memory with cited answers",
@@ -71,7 +73,7 @@ const sectionArt: Record<
caption: "Silver city, civic weather, due process", caption: "Silver city, civic weather, due process",
}, },
projects: { projects: {
src: "/images/ai-lab/api-flow.svg", src: "/assets/ai-lab/api-flow.svg",
alt: "Diagram of a connected API workflow.", alt: "Diagram of a connected API workflow.",
eyebrow: "Bench test", eyebrow: "Bench test",
caption: "Products, repairs, and live deployments", caption: "Products, repairs, and live deployments",
@@ -87,13 +89,13 @@ const sectionArt: Record<
const art = sectionArt[slug]; const art = sectionArt[slug];
--- ---
<article class="section-card"> <a href={href} class="section-card">
<div class="section-card__header"> <div class="section-card__header">
<div class="section-card__stamp"> <div class="section-card__stamp">
<SectionMark slug={slug} className="section-mark--card" /> <SectionMark slug={slug} className="section-mark--card" />
<span class="section-card__label">{label}</span> <span class="section-card__label">{label}</span>
</div> </div>
<span class="section-card__tone">{tone}</span> <span class="section-card__tone"><LocaleCopy copy={tone} /></span>
</div> </div>
{art && ( {art && (
<figure class="section-card__art"> <figure class="section-card__art">
@@ -105,6 +107,6 @@ const art = sectionArt[slug];
</figure> </figure>
)} )}
<h3>{title}</h3> <h3>{title}</h3>
<p>{summary}</p> <p><LocaleCopy copy={summary} /></p>
<a href={href} class="section-card__link">Open section</a> <span class="section-card__link">Open section</span>
</article> </a>
+103
View File
@@ -0,0 +1,103 @@
import type { LocaleCode } from "./locales";
export type AiBubbleImage = {
src: string;
alt: string;
credit: string;
license: string;
sourceUrl: string;
sourceLabel: string;
note?: string;
};
export const aiBubbleArticle = {
title: "The Science of Imaginary Revenues",
subtitle: { en: "A Field Study of the AI Valuation Ecosystem", fr: "Une étude de terrain de l'écosystème de valorisation de l'IA", nb: "En feltstudie av økosystemet for AI-verdsettelse" },
publishedAt: "2026-05-17 09:00:00",
excerpt: { en: "From inside the machine that builds the machines: a correspondent's notes on circular capital, NVIDIA's golden shovel, and the dot-com rhyme that the AI industry insists it is not repeating.", fr: "Depuis l'intérieur de la machine qui construit les machines : les notes d'un correspondant sur le capital circulaire, la pelle dorée de NVIDIA et la rime dot-com que l'industrie de l'IA insiste à ne pas répéter.", nb: "Fra innsiden av maskinen som bygger maskinene: en korrespondents notater om sirkulær kapital, NVIDIAs gyldne spade og dot-com-rimet som AI-industrien insisterer på at den ikke gjentar." },
};
export const aiBubbleImages: AiBubbleImage[] = [
{
src: "/images/articles/ai-bubble/eniac.jpg",
alt: "The ENIAC computer at the University of Pennsylvania, 1946.",
credit: "US Army / University of Pennsylvania",
license: "Public Domain",
sourceUrl: "https://commons.wikimedia.org/wiki/File:Glen_Beck_and_Betty_Snyder_program_the_ENIAC_in_building_328_at_the_Ballistic_Research_Laboratory.jpg",
sourceLabel: "Wikimedia Commons",
note: "The ENIAC (1946): built for $400,000 to calculate artillery trajectories. No one imagined it would one day generate funding decks for companies valued at $300 billion.",
},
{
src: "/images/articles/ai-bubble/semper-augustus.jpg",
alt: "The Semper Augustus tulip, painted during the Dutch Golden Age circa 1637.",
credit: "Anonymous / Dutch Golden Age, c. 1637",
license: "Public Domain",
sourceUrl: "https://commons.wikimedia.org/wiki/File:Semper_Augustus_Tulip_17th_century.jpg",
sourceLabel: "Wikimedia Commons",
note: "The Semper Augustus: at its 1637 peak, a single bulb sold for 10,000 guilders — the price of an Amsterdam townhouse. The tulip was real. The economics were not.",
},
{
src: "/images/articles/ai-bubble/data-centre.jpg",
alt: "Row upon row of server hardware in a Wikimedia Foundation data centre.",
credit: "Victorgrigas",
license: "CC BY-SA 3.0",
sourceUrl: "https://commons.wikimedia.org/wiki/File:Wikimedia_Foundation_Servers-8055_35.jpg",
sourceLabel: "Wikimedia Commons",
note: "A data centre: the physical substrate of the imaginary economy. Every inference request, every funding round, every conference keynote ultimately resolves to a room that looks exactly like this one.",
},
{
src: "/images/articles/ai-bubble/boyles-flask.png",
alt: "Diagram of Boyle's self-flowing flask, a proposed perpetual motion machine.",
credit: "Wikimedia Commons",
license: "Public Domain",
sourceUrl: "https://commons.wikimedia.org/wiki/File:Boyle%27sSelfFlowingFlask.png",
sourceLabel: "Wikimedia Commons",
note: "Boyle's self-flowing flask (c. 1685): a siphon-based perpetual motion machine that theorised water could power itself indefinitely. The AI investment cycle operates on similar principles, with venture capital in place of water.",
},
];
export const aiBubbleBody: Record<LocaleCode, string[]> = {
en: [
"There are two kinds of people in the artificial intelligence industry in the year 2026. The first kind has never actually run an inference in anger; they attend conferences, read press releases, and hold opinions about paradigm shifts that exist in their slide decks but nowhere else. The second kind builds with the technology daily — reads the model cards, watches the hallucinations, argues with the tokenizer, patches the context windows, and finds the whole enterprise simultaneously extraordinary and slightly absurd. I belong to the second camp, which is the worse position for peace of mind and the better position for observation.",
"Let us be precise about what is being wagered, since precision is at least a courtesy. OpenAI carries a valuation of approximately three hundred billion dollars as of early 2026, having raised multiples of what it has ever earned in revenue. Anthropic — the company whose model is composing this very sentence — sits at sixty-one billion. xAI, Elon Musk's contribution to the genre, at fifty billion. NVIDIA, which manufactures the hardware that all of these companies rent from the sky, briefly became one of the most valuable companies in human commercial history, its data-centre revenues compounding from fifteen billion dollars in 2022 to north of one hundred billion in 2024. These are not small numbers. These are numbers that imply a certainty about the future that the future has not yet agreed to honour.",
"Here is where the field study becomes genuinely interesting to anyone who has taken a passing interest in the science of imaginary solutions. Microsoft has invested thirteen billion dollars in OpenAI. OpenAI spends heavily on Microsoft Azure. Amazon has committed four billion dollars to Anthropic. Anthropic spends lavishly on Amazon Web Services. Google has invested in Anthropic while also building its own competing models and selling cloud infrastructure to everyone involved. The money moves in a circle. The investor is also the supplier. The supplier is also the customer. The customer is also the investor. Alfred Jarry would have recognised this ecosystem immediately: a machine of singular elegance that generates energy through the mutual validation of all its participants, with the notable property that no external customer is strictly required to keep the wheel turning — at least until the wheel needs to turn faster.",
"The singular winner in this arrangement, so far, has been NVIDIA. In a gold rush the preferred position is not to mine but to sell shovels, and Jensen Huang has been selling shovels at a margin that would make a Venetian merchant weep with admiration. A single H100 graphics processor lists at around thirty thousand dollars. The hyperscalers are ordering them in quantities measured in hundreds of thousands. The irony embedded in this arrangement is considerable: the AI companies that are supposedly threatening to restructure the global economy must first pay vast sums to the chip company whose business model is entirely traditional — make the thing, sell the thing, collect the money. NVIDIA's revenues are real. What is less certain is whether the demand that produced them is itself sustainable, or whether it represents the most expensive collective fear-of-missing-out in commercial history.",
"One is frequently told, in the industry, that comparisons to the dot-com bubble of 2000 reveal a failure of imagination. The technology is different, the argument runs. The infrastructure actually works. The use cases are genuine. All of this is true, and none of it is the point. The dot-com era also had real technology; the fibre-optic cables that went dark in 2001 carried real light at real speeds and eventually became the substrate of a functional internet. What failed was not the technology — what failed was the assumption that technology alone generates revenue, that the build-out would produce paying customers in proportions sufficient to justify the capital deployed. In 2000 the unanswerable question was: who exactly is paying for all this? In 2026 the question deserves asking again.",
"The answer one hears is: enterprise. Corporations, the theory runs, will pay monthly subscriptions to embed artificial intelligence into every function, and the aggregate of those subscriptions will eventually produce the revenues that justify the valuations. The empirical picture is more equivocal. Surveys of enterprise AI adoption show high rates of experimentation and markedly lower rates of production deployment. Many organisations that report 'using AI' mean that their employees have ChatGPT accounts. This is not the same thing as a restructured cost base or a validated revenue model. The gap between pilot and production — between the innovation theatre of the boardroom and the actual operational budget of the finance department — is wider than the valuation models tend to acknowledge.",
"Pataphysics, which Alfred Jarry described as the science of imaginary solutions and the study of laws governing exceptions, offers the most useful frame I have found. A pataphysical reading of the AI investment cycle does not say the technology is fraudulent — it says that the system has found an imaginary solution to the problem of revenue: the promise that Artificial General Intelligence, somewhere on the horizon, will eventually justify the present expenditure. The AGI assumption functions precisely as Jarry's imaginary solution — not a lie exactly, but a story about an exception, the exceptional future moment when scaling laws deliver human-level cognition, that is being used to govern a very present and non-exceptional allocation of capital. The exceptional case is mistaken for the general rule, which is pataphysics applied to investment banking.",
"My own position in this is not comfortable, and I would be dishonest to pretend otherwise. I build with these tools. I use Anthropic's Claude as a programming collaborator, a research assistant, a first-draft machine. I route between models depending on task complexity. I have run local inference on GPU servers and watched temperature dashboards and argued with fine-tuned weights at midnight. The technology is genuinely useful — genuinely extraordinary, even, in moments. But useful and worth three hundred billion dollars are two very different propositions, and I am careful not to confuse the wonder I feel at a well-placed inference with the financial certainty required to sustain a capital structure of this scale. Wonder is not a revenue model. I know because I have the invoices.",
"The dot-com boom did not end the internet. It ended the assumption that every internet company was worth what the market said it was worth during the fever. What survived were the companies with real revenue models, real cost discipline, and technology that solved problems people were willing to pay for — unprompted, in sufficient volume, to cover the bills. The AI industry will almost certainly follow the same arc: a contraction, a shakeout, a reckoning with unit economics, and then — on the other side of that — a more durable, more honest version of the useful technology that will have been there all along. The shovels are real. The gold may or may not be there in the quantities the prospectors have promised their investors. That is not pataphysics. That is just history, arriving in slightly different clothes and asking us to pretend we have never met."
],
fr: [
"Il existe deux types de personnes dans l'industrie de l'intelligence artificielle en l'an 2026. Le premier type n'a jamais réellement exécuté une inférence sous pression ; ils assistent à des conférences, lisent des communiqués de presse et ont des opinions sur des changements de paradigme qui existent dans leurs présentations mais nulle part ailleurs. Le second type construit avec la technologie au quotidien — lit les fiches des modèles, observe les hallucinations, débat avec le tokenizer, ajuste les fenêtres de contexte, et trouve l'ensemble à la fois extraordinaire et légèrement absurde. J'appartiens au second camp, une position moins favorable pour la tranquillité d'esprit mais meilleure pour l'observation.",
"Soyons précis sur ce qui est en jeu, car la précision est au moins une forme de politesse. OpenAI affiche une valorisation d'environ trois cents milliards de dollars début 2026, ayant levé des montants multiples de ce qu'elle a jamais généré en revenus. Anthropic — la société dont le modèle rédige cette phrase même — est valorisée à soixante et un milliards. xAI, la contribution d'Elon Musk au genre, à cinquante milliards. NVIDIA, qui fabrique le matériel que toutes ces entreprises louent depuis le ciel, est brièvement devenue l'une des entreprises les plus précieuses de l'histoire commerciale humaine, ses revenus des centres de données passant de quinze milliards de dollars en 2022 à plus de cent milliards en 2024. Ce ne sont pas des chiffres modestes. Ce sont des chiffres qui impliquent une certitude sur l'avenir que l'avenir n'a pas encore accepté d'honorer.",
"C'est ici que l'étude de terrain devient véritablement intéressante pour quiconque s'est intéressé, même brièvement, à la science des solutions imaginaires. Microsoft a investi treize milliards de dollars dans OpenAI. OpenAI dépense massivement sur Microsoft Azure. Amazon a engagé quatre milliards de dollars dans Anthropic. Anthropic dépense généreusement sur Amazon Web Services. Google a investi dans Anthropic tout en construisant ses propres modèles concurrents et en vendant une infrastructure cloud à tous les acteurs impliqués. L'argent circule en cercle. L'investisseur est aussi le fournisseur. Le fournisseur est aussi le client. Le client est aussi l'investisseur. Alfred Jarry aurait immédiatement reconnu cet écosystème : une machine d'une élégance singulière qui génère de l'énergie grâce à la validation mutuelle de tous ses participants, avec la propriété notable qu'aucun client externe n'est strictement nécessaire pour maintenir la roue en mouvement — du moins jusqu'à ce que la roue doive tourner plus vite.",
"Le gagnant singulier dans cet arrangement, jusqu'à présent, a été NVIDIA. Lors d'une ruée vers l'or, la position préférée n'est pas de miner mais de vendre des pelles, et Jensen Huang a vendu des pelles avec une marge qui ferait pleurer d'admiration un marchand vénitien. Un seul processeur graphique H100 est affiché à environ trente mille dollars. Les hyperscalers les commandent en quantités mesurées par centaines de milliers. L'ironie intégrée dans cet arrangement est considérable : les entreprises d'IA qui sont censées menacer de restructurer l'économie mondiale doivent d'abord payer des sommes colossales à l'entreprise de puces dont le modèle économique est entièrement traditionnel — fabriquer le produit, vendre le produit, encaisser l'argent. Les revenus de NVIDIA sont réels. Ce qui est moins certain, c'est si la demande qui les a produits est elle-même durable, ou si elle représente la peur collective de manquer la plus coûteuse de l'histoire commerciale.",
"On entend fréquemment, dans l'industrie, que les comparaisons avec la bulle dot-com de 2000 révèlent un manque d'imagination. La technologie est différente, dit-on. L'infrastructure fonctionne réellement. Les cas d'utilisation sont authentiques. Tout cela est vrai, et rien de tout cela n'est le point. L'ère dot-com avait également une technologie réelle ; les câbles fibre optique qui se sont éteints en 2001 transportaient une lumière réelle à des vitesses réelles et sont finalement devenus le substrat d'un internet fonctionnel. Ce qui a échoué, ce n'est pas la technologie — ce qui a échoué, c'est l'hypothèse selon laquelle la technologie seule génère des revenus, que le développement produirait des clients payants en proportions suffisantes pour justifier le capital déployé. En 2000, la question sans réponse était : qui paie exactement pour tout cela ? En 2026, la question mérite d'être posée à nouveau.",
"La réponse qu'on entend est : les entreprises. Les entreprises, selon la théorie, paieront des abonnements mensuels pour intégrer l'intelligence artificielle dans chaque fonction, et l'agrégat de ces abonnements produira finalement les revenus qui justifient les valorisations. Le tableau empirique est plus équivoque. Les enquêtes sur l'adoption de l'IA par les entreprises montrent des taux élevés d'expérimentation et des taux nettement plus bas de déploiement en production. De nombreuses organisations qui déclarent 'utiliser l'IA' signifient que leurs employés ont des comptes ChatGPT. Ce n'est pas la même chose qu'une base de coûts restructurée ou un modèle de revenus validé. L'écart entre le pilote et la production — entre le théâtre de l'innovation de la salle de conseil et le budget opérationnel réel du département financier — est plus large que les modèles de valorisation ne tendent à le reconnaître.",
"La pataphysique, qu'Alfred Jarry a décrite comme la science des solutions imaginaires et l'étude des lois régissant les exceptions, offre le cadre le plus utile que j'ai trouvé. Une lecture pataphysique du cycle d'investissement dans l'IA ne dit pas que la technologie est frauduleuse — elle dit que le système a trouvé une solution imaginaire au problème des revenus : la promesse que l'intelligence artificielle générale, quelque part à l'horizon, justifiera finalement les dépenses actuelles. L'hypothèse de l'AGI fonctionne précisément comme la solution imaginaire de Jarry — pas exactement un mensonge, mais une histoire sur une exception, le moment futur exceptionnel où les lois d'échelle produiront une cognition au niveau humain, qui est utilisée pour gouverner une allocation de capital très présente et non exceptionnelle. Le cas exceptionnel est confondu avec la règle générale, ce qui est la pataphysique appliquée à la banque d'investissement.",
"Ma propre position dans tout cela n'est pas confortable, et je serais malhonnête de prétendre le contraire. Je construis avec ces outils. J'utilise Claude d'Anthropic comme collaborateur en programmation, assistant de recherche, machine à brouillons. Je navigue entre les modèles en fonction de la complexité des tâches. J'ai exécuté des inférences locales sur des serveurs GPU, surveillé des tableaux de bord de température et débattu avec des poids ajustés à minuit. La technologie est réellement utile — réellement extraordinaire, même, par moments. Mais utile et valoir trois cents milliards de dollars sont deux propositions très différentes, et je fais attention à ne pas confondre l'émerveillement que je ressens devant une inférence bien placée avec la certitude financière nécessaire pour soutenir une structure de capital de cette échelle. L'émerveillement n'est pas un modèle de revenus. Je le sais parce que j'ai les factures.",
"La bulle dot-com n'a pas mis fin à l'internet. Elle a mis fin à l'hypothèse selon laquelle chaque entreprise internet valait ce que le marché disait qu'elle valait pendant la fièvre. Ce qui a survécu, ce sont les entreprises avec de vrais modèles de revenus, une vraie discipline des coûts et une technologie qui résolvait des problèmes que les gens étaient prêts à payer — spontanément, en volume suffisant, pour couvrir les factures. L'industrie de l'IA suivra presque certainement le même arc : une contraction, une rationalisation, une prise de conscience des économies d'unité, et ensuite — de l'autre côté de cela — une version plus durable, plus honnête de la technologie utile qui aura été là tout le temps. Les pelles sont réelles. L'or peut ou non être là dans les quantités que les prospecteurs ont promises à leurs investisseurs. Ce n'est pas de la pataphysique. C'est juste l'histoire, arrivant dans des vêtements légèrement différents et nous demandant de prétendre que nous ne nous sommes jamais rencontrés."
],
nb: [
"Det finnes to typer mennesker i kunstig intelligens-industrien i året 2026. Den første typen har aldri faktisk kjørt en inferens i sinne; de deltar på konferanser, leser pressemeldinger og har meninger om paradigmeskifter som eksisterer i deres lysbilder, men ingen andre steder. Den andre typen bygger med teknologien daglig — leser modellkortene, ser hallusinasjonene, krangler med tokenizeren, justerer kontekstvinduene og finner hele foretaket samtidig ekstraordinært og litt absurd. Jeg tilhører den andre leiren, som er en dårligere posisjon for sinnsro og en bedre posisjon for observasjon.",
"La oss være presise om hva som står på spill, siden presisjon i det minste er en høflighet. OpenAI har en verdsettelse på omtrent tre hundre milliarder dollar tidlig i 2026, etter å ha samlet inn flere ganger det de noen gang har tjent i inntekter. Anthropic — selskapet hvis modell skriver akkurat denne setningen — ligger på sekstien milliarder. xAI, Elon Musks bidrag til sjangeren, på femti milliarder. NVIDIA, som produserer maskinvaren som alle disse selskapene leier fra himmelen, ble kort en av de mest verdifulle selskapene i menneskets kommersielle historie, med datasenterinntekter som økte fra femten milliarder dollar i 2022 til over hundre milliarder i 2024. Dette er ikke små tall. Dette er tall som antyder en visshet om fremtiden som fremtiden ennå ikke har gått med på å ære.",
"Her blir feltstudien virkelig interessant for alle som har tatt en forbigående interesse i vitenskapen om imaginære løsninger. Microsoft har investert tretten milliarder dollar i OpenAI. OpenAI bruker tungt på Microsoft Azure. Amazon har forpliktet seg til fire milliarder dollar i Anthropic. Anthropic bruker stort på Amazon Web Services. Google har investert i Anthropic samtidig som de bygger sine egne konkurrerende modeller og selger skyinfrastruktur til alle involverte. Pengene beveger seg i en sirkel. Investoren er også leverandøren. Leverandøren er også kunden. Kunden er også investoren. Alfred Jarry ville umiddelbart ha gjenkjent dette økosystemet: en maskin med enestående eleganse som genererer energi gjennom gjensidig validering av alle deltakerne, med den bemerkelsesverdige egenskapen at ingen ekstern kunde strengt tatt er nødvendig for å holde hjulet i gang — i det minste til hjulet må snurre raskere.",
"Den eneste vinneren i denne ordningen, så langt, har vært NVIDIA. I en gullrush er den foretrukne posisjonen ikke å grave, men å selge spader, og Jensen Huang har solgt spader med en margin som ville fått en venetiansk handelsmann til å gråte av beundring. En enkelt H100 grafikkprosessor er priset til rundt tretti tusen dollar. Hyperscalerene bestiller dem i mengder målt i hundretusenvis. Ironien som ligger i denne ordningen er betydelig: AI-selskapene som angivelig truer med å omstrukturere den globale økonomien må først betale enorme summer til chip-selskapet hvis forretningsmodell er helt tradisjonell — lage produktet, selge produktet, samle pengene. NVIDIAs inntekter er reelle. Det som er mindre sikkert, er om etterspørselen som produserte dem er bærekraftig, eller om den representerer den dyreste kollektive frykten for å gå glipp av noe i kommersiell historie.",
"Man blir ofte fortalt, i industrien, at sammenligninger med dot-com-boblen i 2000 avslører en mangel på fantasi. Teknologien er annerledes, argumentet går. Infrastruktur fungerer faktisk. Bruksområdene er ekte. Alt dette er sant, og ingenting av det er poenget. Dot-com-æraen hadde også ekte teknologi; fiberoptiske kabler som ble mørke i 2001 bar ekte lys med ekte hastigheter og ble til slutt substratet for et funksjonelt internett. Det som feilet var ikke teknologien — det som feilet var antagelsen om at teknologi alene genererer inntekter, at utbyggingen ville produsere betalende kunder i proporsjoner som var tilstrekkelige til å rettferdiggjøre kapitalen som ble brukt. I 2000 var det ubesvarte spørsmålet: hvem betaler egentlig for alt dette? I 2026 fortjener spørsmålet å bli stilt igjen.",
"Svaret man hører er: bedrifter. Bedrifter, teorien går, vil betale månedlige abonnementer for å integrere kunstig intelligens i hver funksjon, og aggregatet av disse abonnementene vil til slutt produsere inntektene som rettferdiggjør verdsettelsene. Det empiriske bildet er mer tvetydig. Undersøkelser av bedriftsadopsjon av AI viser høye eksperimenteringsrater og markant lavere produksjonsutplassering. Mange organisasjoner som rapporterer at de 'bruker AI' mener at deres ansatte har ChatGPT-kontoer. Dette er ikke det samme som en restrukturert kostnadsbase eller en validert inntektsmodell. Gapet mellom pilot og produksjon — mellom innovasjonsteateret i styrerommet og det faktiske driftsbudsjettet til finansavdelingen — er bredere enn verdsettelsesmodellene har en tendens til å anerkjenne.",
"Pataphysikk, som Alfred Jarry beskrev som vitenskapen om imaginære løsninger og studiet av lover som styrer unntak, tilbyr den mest nyttige rammen jeg har funnet. En pataphysisk lesning av AI-investeringens syklus sier ikke at teknologien er svindel — den sier at systemet har funnet en imaginær løsning på inntektsproblemet: løftet om at kunstig generell intelligens, et sted i horisonten, til slutt vil rettferdiggjøre dagens utgifter. AGI-antagelsen fungerer nøyaktig som Jarrys imaginære løsning — ikke akkurat en løgn, men en historie om et unntak, det eksepsjonelle fremtidige øyeblikket når skaleringslover leverer menneskelig nivå kognisjon, som brukes til å styre en veldig nåværende og ikke-eksepsjonell kapitalallokering. Det eksepsjonelle tilfellet forveksles med den generelle regelen, som er pataphysikk anvendt på investeringsbank.",
"Min egen posisjon i dette er ikke komfortabel, og jeg ville være uærlig hvis jeg påsto noe annet. Jeg bygger med disse verktøyene. Jeg bruker Anthropics Claude som programmeringspartner, forskningsassistent, førstegangsutkast-maskin. Jeg navigerer mellom modeller avhengig av oppgavens kompleksitet. Jeg har kjørt lokal inferens på GPU-servere, sett temperaturdashbord og diskutert med finjusterte vekter ved midnatt. Teknologien er genuint nyttig — genuint ekstraordinær, til og med, i øyeblikk. Men nyttig og verdt tre hundre milliarder dollar er to veldig forskjellige proposisjoner, og jeg er forsiktig med å ikke forveksle undringen jeg føler ved en godt plassert inferens med den økonomiske vissheten som kreves for å opprettholde en kapitalstruktur av denne skalaen. Undring er ikke en inntektsmodell. Jeg vet det fordi jeg har fakturaene.",
"Dot-com-boomen avsluttet ikke internett. Den avsluttet antagelsen om at hvert internettfirma var verdt det markedet sa det var verdt under feberen. Det som overlevde var selskapene med ekte inntektsmodeller, ekte kostnadsdisiplin og teknologi som løste problemer folk var villige til å betale for — uoppfordret, i tilstrekkelig volum, til å dekke regningene. AI-industrien vil nesten helt sikkert følge samme bue: en sammentrekning, en utrensking, en oppgjør med enhetsøkonomi, og deretter — på den andre siden av det — en mer holdbar, mer ærlig versjon av den nyttige teknologien som alltid har vært der. Spadene er ekte. Gullet kan eller ikke kan være der i de mengdene prospektørene har lovet sine investorer. Det er ikke pataphysikk. Det er bare historie, som ankommer i litt forskjellige klær og ber oss late som vi aldri har møtt den."
],
};
export const aiBubbleSources = [
{ label: "OpenAI — Company valuation and funding history", href: "https://openai.com/" },
{ label: "Anthropic — About and funding rounds", href: "https://www.anthropic.com/" },
{ label: "NVIDIA — Investor Relations, Data Centre Revenue", href: "https://investor.nvidia.com/" },
{ label: "Alfred Jarry — Exploits and Opinions of Doctor Faustroll, Pataphysician (1911)", href: "https://en.wikipedia.org/wiki/Exploits_and_Opinions_of_Doctor_Faustroll,_Pataphysician" },
{ label: "McKinsey Global Institute — The State of AI in 2024", href: "https://www.mckinsey.com/capabilities/quantumblack/our-insights/the-state-of-ai" },
{ label: "Tulip mania — Wikipedia", href: "https://en.wikipedia.org/wiki/Tulip_mania" },
{ label: "Dot-com bubble — Wikipedia", href: "https://en.wikipedia.org/wiki/Dot-com_bubble" },
];
+6 -6
View File
@@ -161,27 +161,27 @@ export const aiLabInfraNotes = [
]; ];
export const aiLabVisuals = { export const aiLabVisuals = {
hero: "/images/ai-lab/hero-lab.svg", hero: "/images/ai-lab/hero-lab.jpg",
corpus: "/images/ai-lab/corpus-grid.svg", corpus: "/images/ai-lab/corpus-grid.jpg",
api: "/images/ai-lab/api-flow.svg", api: "/images/ai-lab/api-flow.jpg",
}; };
export const aiLabDetailedSources = [ export const aiLabDetailedSources = [
{ {
label: "CorpusAI homepage", label: "CorpusAI homepage",
href: "https://ai.bluenotelogic.com/", href: "https://bluenotelogic.com",
note: note:
"Used for the live demo corpus, real-use-case examples, infrastructure notes, and REST API examples.", "Used for the live demo corpus, real-use-case examples, infrastructure notes, and REST API examples.",
}, },
{ {
label: "Blue Note Logic / Corporate Memory Extraction", label: "Blue Note Logic / Corporate Memory Extraction",
href: "https://bluenotelogic.com/service.php?slug=corporate-memory-extraction", href: "https://bluenotelogic.com",
note: note:
"Used for the sovereign model trajectory, isolated telemetry, and the service framing around owned corporate memory.", "Used for the sovereign model trajectory, isolated telemetry, and the service framing around owned corporate memory.",
}, },
{ {
label: "Blue Note Logic / Document Intelligence Consulting", label: "Blue Note Logic / Document Intelligence Consulting",
href: "https://bluenotelogic.com/service.php?slug=document-intelligence-consulting", href: "https://bluenotelogic.com",
note: note:
"Used for the consulting and implementation layer around private document intelligence.", "Used for the consulting and implementation layer around private document intelligence.",
}, },
+103
View File
@@ -0,0 +1,103 @@
import type { LocaleCode } from "./locales";
export type BorisVianImage = {
src: string;
alt: string;
credit: string;
license: string;
sourceUrl: string;
sourceLabel: string;
note?: string;
};
export const borisVianArticle = {
title: "L'Homme Impossible",
subtitle: { en: "A Guide to Boris Vian and the Collège de 'Pataphysique", fr: "Un guide de Boris Vian et du Collège de 'Pataphysique", nb: "En guide til Boris Vian og Collège de 'Pataphysique" },
publishedAt: "2026-05-17 10:00:00",
excerpt: { en: "He was a trained engineer who played trumpet in a cellar with Miles Davis, wrote ten novels before his fortieth birthday, and became a Transcendent Satrap of an institution dedicated to deliberate uselessness. A guide to Boris Vian, for those who haven't met him yet.", fr: "Ingénieur de formation, il jouait de la trompette dans une cave avec Miles Davis, a écrit dix romans avant ses quarante ans, et est devenu Satrape Transcendant d'une institution dédiée à l'inutilité délibérée. Un guide de Boris Vian, pour ceux qui ne l'ont pas encore rencontré.", nb: "Han var en utdannet ingeniør som spilte trompet i en kjeller med Miles Davis, skrev ti romaner før han fylte førti, og ble en Transcendent Satrap for en institusjon dedikert til bevisst ubrukelighet. En guide til Boris Vian, for de som ennå ikke har møtt ham." },
};
export const borisVianImages: BorisVianImage[] = [
{
src: "/images/articles/boris-vian/boris-vian.jpg",
alt: "Boris Vian, photographed in Paris, 1948.",
credit: "Cohérie Boris Vian / TeKaBe",
license: "CC BY-SA 3.0",
sourceUrl: "https://commons.wikimedia.org/wiki/File:Boris_Vian_-_WIKI_retouched.jpg",
sourceLabel: "Wikimedia Commons",
note: "Boris Vian, Paris, 1948 — engineer, novelist, trumpeter, playwright, songwriter, pataphysician. He was 28.",
},
{
src: "/images/articles/boris-vian/jarry.jpg",
alt: "Alfred Jarry, photographed by the Atelier Nadar, c. 1896.",
credit: "Atelier Nadar",
license: "Public Domain",
sourceUrl: "https://commons.wikimedia.org/wiki/File:Alfred_Jarry.jpg",
sourceLabel: "Wikimedia Commons",
note: "Alfred Jarry (18731907), author of Ubu Roi and inventor of pataphysics. He died at 34. The Collège de 'Pataphysique was founded in his honour, 41 years later.",
},
{
src: "/images/articles/boris-vian/ubu.png",
alt: "Jarry's own drawing of Père Ubu, the Véritable portrait de Monsieur Ubu, 1896.",
credit: "Alfred Jarry",
license: "Public Domain",
sourceUrl: "https://commons.wikimedia.org/wiki/File:V%C3%A9ritable_portrait_de_Monsieur_Ubu.png",
sourceLabel: "Wikimedia Commons",
note: "Jarry's 'Véritable portrait de Monsieur Ubu' — the true portrait of Ubu, drawn by the author himself. Père Ubu is the embodiment of the pataphysical principle: a force of pure absurd authority, ungoverned by consequence.",
},
{
src: "/images/articles/boris-vian/le-tabou.jpg",
alt: "Commemorative plaque at the site of Le Tabou jazz club, corner of rue Christine and rue Dauphine, Paris 6e.",
credit: "Celette",
license: "CC BY-SA 4.0",
sourceUrl: "https://commons.wikimedia.org/wiki/File:Plaque_Le_Tabou%2C_angle_rue_Christine_et_rue_Dauphine%2C_Paris_6e.jpg",
sourceLabel: "Wikimedia Commons",
note: "The plaque at Le Tabou, 33 rue Dauphine, Paris 6e — the cave bar where Boris Vian played trumpet while the existentialists wrote upstairs. It opened in 1947. It closed in the mid-1960s. The street is still there.",
},
];
export const borisVianBody: Record<LocaleCode, string[]> = {
en: [
"There is a novel in which a woman has a water lily growing in her lung. Her husband — a man named Colin, who owns a pianocktail that translates the notes he plays into cocktails — spends everything he has trying to save her, filling their apartment with fresh flowers, because flowers are the only thing that slow the lily's growth. As he spends more, their apartment physically shrinks. The rooms contract. The floors grow mould. The ceilings drop. The flowers cost more every week. The lily keeps growing. This is not magical realism, and it is not allegory. It is a precise physical description, following laws that the author has constructed to his own specification, of what love and grief and money actually do to a life. The book is called L'Écume des jours. The man who wrote it was also a trained engineer.",
"Paris, late 1940s. The cave bars of Saint-Germain-des-Prés — Le Tabou, the Caveau des Lorientais, the Club Saint-Germain — are the centre of the world that matters, which is to say the world of jazz arriving from America and being absorbed into the bones of a city that has just emerged from occupation. Upstairs, in the cafés above ground, Sartre and de Beauvoir are inventing existentialism for a post-war generation. Downstairs, in the literal cellars, Boris Vian is playing trumpet. Not listening to jazz — playing it. In the same room as the musicians he writes about. Miles Davis will later describe Vian as one of the few Frenchmen who genuinely understood the music rather than simply loving it as an exotic import. Vian, for his part, is writing jazz criticism under his own name, novels of extraordinary surrealist precision under his own name, and crime thrillers in American slang under the name Vernon Sullivan — a pseudonym he invented because the genre required an American Black author, and Vian was neither.",
"A short list: engineer (École Centrale Paris, class of 1944, a real degree in applied mechanics and thermodynamics). Novelist — ten novels in twelve years. Playwright. Songwriter — more than four hundred songs, including 'Le Déserteur' (1954), an open letter to the President of France informing him that Vian would not be reporting for military service, which was banned from French radio during the Algerian War and went on to become a pacifist anthem that outlived the war by decades. Jazz critic. Screenwriter. Translator of American hard-boiled fiction. Transcendent Satrap of the Collège de 'Pataphysique. None of these were hobbies. He was a working professional in each register simultaneously. The question is not how he managed it — he had a congenital heart condition that had been killing him since his twenties — but what kind of intelligence generates this output, and why the standard categories have no name for it.",
"The Collège de 'Pataphysique was founded in Paris in 1948 by a group of admirers of Alfred Jarry, who felt, correctly, that the science of imaginary solutions had gone unrepresented in institutional form for too long. It has a real address. It publishes real journals — the Dossiers Acénonètes du Collège de 'Pataphysique, among others. It has a hierarchy of ranks: Auditeurs, Régents, Provedditeurs, Vice-Curateurs, Satrapes, and at the apex, the Transcendent Satraps — those who have so thoroughly exceeded the institution that they can only be said to exist beyond it. Among its members have been Eugène Ionesco, Marcel Duchamp, Joan Miró, Raymond Queneau, Jacques Prévert, Max Ernst, and Man Ray. It continues to meet in Paris. Its activities are deliberately and comprehensively useless. This is the point.",
"Alfred Jarry defined his science in the novel about Doctor Faustroll, published posthumously in 1911: 'the science of imaginary solutions, which symbolically attributes the properties of objects, described by their virtuality, to their lineaments.' This sentence is designed to be slightly out of reach, which is the point. Pataphysics does not explain the world. It proposes that the world's actual behaviour is most accurately described not by the general laws that claim to govern it, but by its exceptions, its edge cases, its impossible cases — the cases the law was designed to exclude. The lily that grows in the lung is not a metaphor for grief. It is a pataphysical account of what grief actually does to a body — more accurate than any medical description, more precise about the mechanism, truer to the experience of watching someone you love diminish while the bills keep coming.",
"I used the frame borrowed from Jarry in an earlier piece on this site — 'The Science of Imaginary Revenues,' published on the business desk in May 2026, about the structural fragility of AI valuations and the circular capital arrangements that keep the wheel turning without requiring a paying customer. The argument was that the AI investment cycle operates pataphysically: it governs itself by appeal to a future exception — Artificial General Intelligence, somewhere on the horizon — rather than present evidence. The frame holds because Jarry built it to hold; it is a description of any system that mistakes the exceptional case for the general rule and legislates accordingly. Vian would have found the AI investment cycle entirely comprehensible. The Collège would have appointed it a Regent at minimum.",
"Start with L'Écume des jours. In English translation it is called Foam of the Daze (Brian Harper's version, which is the one to read) or Mood Indigo in the Michel Gondry film adaptation from 2013, which is beautiful and strange and not quite the same thing as the book. It is short — around 160 pages — and it will make you want to read everything else he wrote. After that, find 'Le Déserteur': there are dozens of recorded versions, but the original with Vian himself is the one. A companion reading route, covering the novels in sequence, the Vernon Sullivan books, the plays, and the Collège's own publications, is available separately on the writing desk. I have tried not to make this piece into a reading list, because reading lists are for people who have already decided to be interested. This piece is for the moment before that decision.",
"Boris Vian died on 23 June 1959. He was 39 years old. He had been told at 22 that his congenital heart condition would kill him before 40. He was watching the film adaptation of J'irai cracher sur vos tombes — the Vernon Sullivan novel, the one that had led to his prosecution for obscenity, which was now being filmed without his involvement in a way he found wholly unsatisfactory — when his heart gave out in the cinema. He had produced, in the seventeen years between that diagnosis and that cinema seat: ten novels, four plays, more than four hundred songs, thousands of articles, a body of jazz criticism that is still read, and a pataphysical rank that the Collège continues to hold open, as Transcendent Satraps cannot technically be replaced. The Collège keeps his death date as a feast day on the 'Pataphysical calendar. This seems appropriate. He would have found it slightly absurd, which is also appropriate.",
"In 2026, as a certain class of technology company produces its own version of the impossible man — the figure who claims to be simultaneously rebuilding human cognition, solving climate change, writing code, composing music, answering customer service emails, and approaching general intelligence, all by next Tuesday — there is some comfort in knowing that the science of imaginary solutions has a name, an institution, a membership list that includes Duchamp and Queneau, and a 160-page novel that explains, with complete physical precision, exactly what all of this costs and who pays for it in the end. In L'Écume des jours, the cost is the apartment. The floors. The light. Everything shrinks except the lily. The lily is always real. Read Vian first. The Collège will still be there when you finish."
],
fr: [
"Il existe un roman dans lequel une femme a un nénuphar qui pousse dans son poumon. Son mari — un homme nommé Colin, qui possède un pianocktail traduisant les notes qu'il joue en cocktails — dépense tout ce qu'il a pour essayer de la sauver, remplissant leur appartement de fleurs fraîches, car les fleurs sont la seule chose qui ralentit la croissance du nénuphar. À mesure qu'il dépense, leur appartement rétrécit physiquement. Les pièces se contractent. Les sols moisissent. Les plafonds descendent. Les fleurs coûtent plus cher chaque semaine. Le nénuphar continue de croître. Ce n'est pas du réalisme magique, et ce n'est pas une allégorie. C'est une description physique précise, suivant des lois que l'auteur a construites selon ses propres spécifications, de ce que l'amour, le chagrin et l'argent font réellement à une vie. Le livre s'appelle L'Écume des jours. L'homme qui l'a écrit était aussi un ingénieur de formation.",
"Paris, fin des années 1940. Les caves de Saint-Germain-des-Prés — Le Tabou, le Caveau des Lorientais, le Club Saint-Germain — sont le centre du monde qui compte, c'est-à-dire le monde du jazz arrivant d'Amérique et s'absorbant dans les os d'une ville qui vient de sortir de l'occupation. À l'étage, dans les cafés en surface, Sartre et de Beauvoir inventent l'existentialisme pour une génération d'après-guerre. En bas, dans les caves littérales, Boris Vian joue de la trompette. Pas écouter le jazz — le jouer. Dans la même pièce que les musiciens dont il écrit. Miles Davis décrira plus tard Vian comme l'un des rares Français à comprendre réellement la musique plutôt que de simplement l'aimer comme une importation exotique. Vian, pour sa part, écrit des critiques de jazz sous son propre nom, des romans d'une précision surréaliste extraordinaire sous son propre nom, et des thrillers criminels en argot américain sous le nom de Vernon Sullivan — un pseudonyme qu'il a inventé parce que le genre exigeait un auteur américain noir, et Vian n'était ni l'un ni l'autre.",
"Une liste courte : ingénieur (École Centrale Paris, promotion 1944, un vrai diplôme en mécanique appliquée et thermodynamique). Romancier — dix romans en douze ans. Dramaturge. Auteur-compositeur — plus de quatre cents chansons, dont 'Le Déserteur' (1954), une lettre ouverte au Président de la France l'informant que Vian ne se présenterait pas au service militaire, interdite de radio française pendant la guerre d'Algérie et devenue un hymne pacifiste qui a survécu à la guerre pendant des décennies. Critique de jazz. Scénariste. Traducteur de romans noirs américains. Satrape Transcendant du Collège de 'Pataphysique. Aucun de ceux-ci n'était un passe-temps. Il était un professionnel actif dans chaque registre simultanément. La question n'est pas comment il y est parvenu — il avait une maladie cardiaque congénitale qui le tuait depuis ses vingt ans — mais quel type d'intelligence génère cette production, et pourquoi les catégories standards n'ont pas de nom pour cela.",
"Le Collège de 'Pataphysique a été fondé à Paris en 1948 par un groupe d'admirateurs d'Alfred Jarry, qui estimaient, à juste titre, que la science des solutions imaginaires n'avait pas été représentée sous forme institutionnelle depuis trop longtemps. Il a une adresse réelle. Il publie de vrais journaux — les Dossiers Acénonètes du Collège de 'Pataphysique, entre autres. Il a une hiérarchie de rangs : Auditeurs, Régents, Provedditeurs, Vice-Curateurs, Satrapes, et au sommet, les Satrapes Transcendants — ceux qui ont tellement dépassé l'institution qu'on ne peut que dire qu'ils existent au-delà. Parmi ses membres figurent Eugène Ionesco, Marcel Duchamp, Joan Miró, Raymond Queneau, Jacques Prévert, Max Ernst, et Man Ray. Il continue de se réunir à Paris. Ses activités sont délibérément et complètement inutiles. C'est le but.",
"Alfred Jarry a défini sa science dans le roman sur le Docteur Faustroll, publié à titre posthume en 1911 : 'la science des solutions imaginaires, qui attribue symboliquement les propriétés des objets, décrites par leur virtualité, à leurs linéaments.' Cette phrase est conçue pour être légèrement hors de portée, ce qui est le but. La pataphysique n'explique pas le monde. Elle propose que le comportement réel du monde soit décrit le plus précisément non pas par les lois générales qui prétendent le gouverner, mais par ses exceptions, ses cas limites, ses cas impossibles — les cas que la loi a été conçue pour exclure. Le nénuphar qui pousse dans le poumon n'est pas une métaphore du chagrin. C'est un compte rendu pataphysique de ce que le chagrin fait réellement à un corps — plus précis que toute description médicale, plus précis sur le mécanisme, plus fidèle à l'expérience de voir quelqu'un que vous aimez diminuer tandis que les factures continuent d'arriver.",
"J'ai utilisé le cadre emprunté à Jarry dans un article précédent sur ce site — 'La Science des Revenus Imaginaires,' publié sur le bureau des affaires en mai 2026, à propos de la fragilité structurelle des évaluations de l'IA et des arrangements de capital circulaire qui maintiennent la roue en mouvement sans nécessiter de client payant. L'argument était que le cycle d'investissement dans l'IA fonctionne de manière pataphysique : il se gouverne par appel à une exception future — l'Intelligence Artificielle Générale, quelque part à l'horizon — plutôt que par des preuves présentes. Le cadre tient parce que Jarry l'a construit pour tenir ; c'est une description de tout système qui confond le cas exceptionnel avec la règle générale et légifère en conséquence. Vian aurait trouvé le cycle d'investissement dans l'IA entièrement compréhensible. Le Collège lui aurait au minimum attribué le rang de Régent.",
"Commencez par L'Écume des jours. En traduction anglaise, il s'appelle Foam of the Daze (la version de Brian Harper, qui est celle à lire) ou Mood Indigo dans l'adaptation cinématographique de Michel Gondry en 2013, qui est belle et étrange et pas tout à fait la même chose que le livre. Il est court — environ 160 pages — et il vous donnera envie de lire tout le reste qu'il a écrit. Après cela, trouvez 'Le Déserteur' : il existe des dizaines de versions enregistrées, mais l'original avec Vian lui-même est celle qu'il faut. Un itinéraire de lecture complémentaire, couvrant les romans dans l'ordre, les livres de Vernon Sullivan, les pièces de théâtre, et les publications du Collège, est disponible séparément sur le bureau d'écriture. J'ai essayé de ne pas faire de cet article une liste de lecture, car les listes de lecture sont pour les personnes qui ont déjà décidé d'être intéressées. Cet article est pour le moment avant cette décision.",
"Boris Vian est mort le 23 juin 1959. Il avait 39 ans. On lui avait dit à 22 ans que sa maladie cardiaque congénitale le tuerait avant ses 40 ans. Il regardait l'adaptation cinématographique de J'irai cracher sur vos tombes — le roman de Vernon Sullivan, celui qui avait conduit à sa poursuite pour obscénité, qui était maintenant filmé sans sa participation d'une manière qu'il trouvait totalement insatisfaisante — lorsque son cœur a lâché dans le cinéma. Il avait produit, dans les dix-sept ans entre ce diagnostic et ce siège de cinéma : dix romans, quatre pièces de théâtre, plus de quatre cents chansons, des milliers d'articles, un corpus de critique de jazz qui est encore lu, et un rang pataphysique que le Collège continue de maintenir ouvert, car les Satrapes Transcendants ne peuvent techniquement pas être remplacés. Le Collège garde la date de sa mort comme jour de fête dans le calendrier 'Pataphysique. Cela semble approprié. Il aurait trouvé cela légèrement absurde, ce qui est également approprié.",
"En 2026, alors qu'une certaine classe d'entreprise technologique produit sa propre version de l'homme impossible — la figure qui prétend simultanément reconstruire la cognition humaine, résoudre le changement climatique, écrire du code, composer de la musique, répondre aux e-mails du service client, et atteindre l'intelligence générale, tout cela d'ici mardi prochain — il y a un certain réconfort à savoir que la science des solutions imaginaires a un nom, une institution, une liste de membres qui inclut Duchamp et Queneau, et un roman de 160 pages qui explique, avec une précision physique complète, exactement ce que tout cela coûte et qui le paie à la fin. Dans L'Écume des jours, le coût est l'appartement. Les sols. La lumière. Tout rétrécit sauf le nénuphar. Le nénuphar est toujours réel. Lisez Vian en premier. Le Collège sera toujours là quand vous aurez fini."
],
nb: [
"Det finnes en roman der en kvinne har en vannlilje som vokser i lungen hennes. Hennes ektemann — en mann ved navn Colin, som eier en pianocktail som oversetter tonene han spiller til cocktails — bruker alt han har for å forsøke å redde henne, og fyller leiligheten deres med friske blomster, fordi blomster er det eneste som bremser liljens vekst. Etter hvert som han bruker mer, krymper leiligheten fysisk. Rommene trekker seg sammen. Gulvene blir mugne. Takene synker. Blomstene blir dyrere hver uke. Liljen fortsetter å vokse. Dette er ikke magisk realisme, og det er ikke allegori. Det er en presis fysisk beskrivelse, som følger lover forfatteren har konstruert etter egne spesifikasjoner, av hva kjærlighet, sorg og penger faktisk gjør med et liv. Boken heter L'Écume des jours. Mannen som skrev den var også utdannet ingeniør.",
"Paris, slutten av 1940-tallet. Kjellerbarene i Saint-Germain-des-Prés — Le Tabou, Caveau des Lorientais, Club Saint-Germain — er sentrum for verden som betyr noe, det vil si jazzens verden som kommer fra Amerika og absorberes inn i benene til en by som nettopp har kommet ut av okkupasjon. Ovenpå, i kafeene over bakken, oppfinner Sartre og de Beauvoir eksistensialismen for en etterkrigsgenerasjon. Nede, i de bokstavelige kjellerne, spiller Boris Vian trompet. Ikke lytter til jazz — spiller det. I samme rom som musikerne han skriver om. Miles Davis vil senere beskrive Vian som en av de få franskmennene som virkelig forsto musikken, i stedet for bare å elske den som en eksotisk import. Vian, på sin side, skriver jazzkritikk under sitt eget navn, romaner med ekstraordinær surrealistisk presisjon under sitt eget navn, og krimthrillere på amerikansk slang under navnet Vernon Sullivan — et pseudonym han oppfant fordi sjangeren krevde en amerikansk svart forfatter, og Vian var ingen av delene.",
"En kort liste: ingeniør (École Centrale Paris, klasse 1944, en ekte grad i anvendt mekanikk og termodynamikk). Romanforfatter — ti romaner på tolv år. Dramatiker. Låtskriver — mer enn fire hundre sanger, inkludert 'Le Déserteur' (1954), et åpent brev til Frankrikes president som informerer ham om at Vian ikke vil møte til militærtjeneste, som ble forbudt på fransk radio under Algeriekrigen og ble en pasifistisk hymne som overlevde krigen med tiår. Jazzkritiker. Manusforfatter. Oversetter av amerikansk hardkokt litteratur. Transcendent Satrap for Collège de 'Pataphysique. Ingen av disse var hobbyer. Han var en arbeidende profesjonell i hver register samtidig. Spørsmålet er ikke hvordan han klarte det — han hadde en medfødt hjertesykdom som hadde tatt livet av ham siden han var i tjueårene — men hva slags intelligens som genererer denne produksjonen, og hvorfor standardkategoriene ikke har noe navn for det.",
"Collège de 'Pataphysique ble grunnlagt i Paris i 1948 av en gruppe beundrere av Alfred Jarry, som korrekt mente at vitenskapen om imaginære løsninger hadde vært underrepresentert i institusjonell form altfor lenge. Det har en ekte adresse. Det publiserer ekte tidsskrifter — Dossiers Acénonètes du Collège de 'Pataphysique, blant andre. Det har en hierarki av rangeringer: Auditeurs, Régents, Provedditeurs, Vice-Curateurs, Satrapes, og på toppen, de Transcendente Satrapene — de som har overgått institusjonen så grundig at de bare kan sies å eksistere utenfor den. Blant medlemmene har vært Eugène Ionesco, Marcel Duchamp, Joan Miró, Raymond Queneau, Jacques Prévert, Max Ernst, og Man Ray. Det fortsetter å møtes i Paris. Aktivitetene deres er bevisst og fullstendig ubrukelige. Det er poenget.",
"Alfred Jarry definerte sin vitenskap i romanen om doktor Faustroll, utgitt posthumt i 1911: 'vitenskapen om imaginære løsninger, som symbolsk tilskriver egenskapene til objekter, beskrevet av deres virtualitet, til deres linjer.' Denne setningen er designet for å være litt utenfor rekkevidde, som er poenget. Pataphysikk forklarer ikke verden. Den foreslår at verdens faktiske oppførsel mest nøyaktig beskrives ikke av de generelle lovene som hevder å styre den, men av dens unntak, dens kanttilfeller, dens umulige tilfeller — tilfellene loven ble designet for å ekskludere. Liljen som vokser i lungen er ikke en metafor for sorg. Det er en pataphysisk redegjørelse for hva sorg faktisk gjør med en kropp — mer nøyaktig enn noen medisinsk beskrivelse, mer presis om mekanismen, sannere til opplevelsen av å se noen du elsker forsvinne mens regningene fortsetter å komme.",
"Jeg brukte rammen lånt fra Jarry i en tidligere artikkel på dette nettstedet — 'Vitenskapen om Imaginære Inntekter,' publisert på forretningsseksjonen i mai 2026, om den strukturelle skjørheten til AI-verdsettelse og de sirkulære kapitalordningene som holder hjulet i gang uten å kreve en betalende kunde. Argumentet var at AI-investeringssyklusen opererer pataphysisk: den styrer seg selv ved å appellere til et fremtidig unntak — Kunstig Generell Intelligens, et sted på horisonten — snarere enn nåværende bevis. Rammen holder fordi Jarry bygde den for å holde; det er en beskrivelse av ethvert system som forveksler det eksepsjonelle tilfellet med den generelle regelen og lovgiver deretter. Vian ville ha funnet AI-investeringssyklusen helt forståelig. Collège ville ha utnevnt det til minst en Regent.",
"Begynn med L'Écume des jours. På engelsk oversettelse heter den Foam of the Daze (Brian Harpers versjon, som er den å lese) eller Mood Indigo i Michel Gondrys filmatisering fra 2013, som er vakker og merkelig og ikke helt det samme som boken. Den er kort — rundt 160 sider — og den vil få deg til å ville lese alt annet han skrev. Etter det, finn 'Le Déserteur': det finnes dusinvis av innspilte versjoner, men originalen med Vian selv er den. En ledsagende leserute, som dekker romanene i rekkefølge, Vernon Sullivan-bøkene, skuespillene, og Collèges egne publikasjoner, er tilgjengelig separat på skrivebordet. Jeg har forsøkt å ikke gjøre denne artikkelen til en leseliste, fordi leselister er for folk som allerede har bestemt seg for å være interessert. Denne artikkelen er for øyeblikket før den beslutningen.",
"Boris Vian døde 23. juni 1959. Han var 39 år gammel. Han hadde fått beskjed som 22-åring om at hans medfødte hjertesykdom ville drepe ham før han fylte 40. Han så filmatiseringen av J'irai cracher sur vos tombes — Vernon Sullivan-romanen, den som hadde ført til hans rettsforfølgelse for obskønitet, som nå ble filmet uten hans involvering på en måte han fant helt utilfredsstillende — da hjertet hans sviktet i kinosalen. Han hadde produsert, i de sytten årene mellom den diagnosen og den kinosalen: ti romaner, fire skuespill, mer enn fire hundre sanger, tusenvis av artikler, et korpus av jazzkritikk som fortsatt leses, og en pataphysisk rang som Collège fortsatt holder åpen, da Transcendente Satraper teknisk sett ikke kan erstattes. Collège holder hans dødsdato som en festdag på den 'Pataphysiske kalenderen. Dette virker passende. Han ville ha funnet det litt absurd, som også er passende.",
"I 2026, mens en viss klasse av teknologiselskaper produserer sin egen versjon av den umulige mannen — figuren som hevder å samtidig gjenoppbygge menneskelig kognisjon, løse klimaendringer, skrive kode, komponere musikk, svare på kundeservice-e-poster, og nærme seg generell intelligens, alt innen neste tirsdag — er det en viss trøst i å vite at vitenskapen om imaginære løsninger har et navn, en institusjon, en medlemsliste som inkluderer Duchamp og Queneau, og en 160-siders roman som forklarer, med full fysisk presisjon, akkurat hva alt dette koster og hvem som betaler for det til slutt. I L'Écume des jours er kostnaden leiligheten. Gulvene. Lyset. Alt krymper bortsett fra liljen. Liljen er alltid ekte. Les Vian først. Collège vil fortsatt være der når du er ferdig."
],
};
export const borisVianSources = [
{ label: "Boris Vian — L'Écume des jours (1947)", href: "https://en.wikipedia.org/wiki/Froth_on_the_Daydream" },
{ label: "Alfred Jarry — Exploits and Opinions of Doctor Faustroll, Pataphysician (1911)", href: "https://en.wikipedia.org/wiki/Exploits_and_Opinions_of_Doctor_Faustroll,_Pataphysician" },
{ label: "Collège de 'Pataphysique — Wikipedia", href: "https://en.wikipedia.org/wiki/Coll%C3%A8ge_de_%27Pataphysique" },
{ label: "Boris Vian — Wikipedia", href: "https://en.wikipedia.org/wiki/Boris_Vian" },
{ label: "Le Déserteur — Wikipedia", href: "https://en.wikipedia.org/wiki/Le_D%C3%A9serteur" },
{ label: "Pataphysics — Wikipedia", href: "https://en.wikipedia.org/wiki/Pataphysics" },
{ label: "The Science of Imaginary Revenues — May 2026", href: "/articles/ai-bubble-2026/" },
];
+54 -75
View File
@@ -1,10 +1,12 @@
import type { LocaleCode } from "./locales";
export type CvMandate = { export type CvMandate = {
years: string; years: string;
role: string; role: string;
org: string; org: string;
location: string; location: string;
summary: string; summary: Record<LocaleCode, string>;
detail: string; detail: Record<LocaleCode, string>;
sourceLabel: string; sourceLabel: string;
sourceUrl: string; sourceUrl: string;
}; };
@@ -14,22 +16,38 @@ export type CvRole = {
role: string; role: string;
org: string; org: string;
location: string; location: string;
summary: string; summary: Record<LocaleCode, string>;
bullets: string[]; bullets: Record<LocaleCode, string[]>;
}; };
export type CvSkillTrack = { export type CvSkillTrack = {
label: string; label: Record<LocaleCode, string>;
items: string[]; items: string[];
}; };
export const cvHero = { export const cvHero = {
eyebrow: "Section 11 / curriculum vitae", eyebrow: { en: "Section 11 / curriculum vitae", fr: "Section 11 / curriculum vitae", nb: "Seksjon 11 / curriculum vitae" },
title: "The formal record, but still edited like a cover story.", title: { en: "The formal record, but still edited like a cover story.", fr: "Le dossier formel, mais toujours édité comme une histoire de couverture.", nb: "Den formelle oversikten, men fortsatt redigert som en forsidehistorie." },
lede: lede: { en: "Finance, reinsurance, systems engineering, private AI, multilingual business study, and children's-rights advocacy belong on one page only if the through-line is legible: build the machinery, understand the money, keep the language human.", fr: "Finance, réassurance, ingénierie des systèmes, IA privée, études commerciales multilingues et défense des droits des enfants tiennent sur une seule page seulement si le fil conducteur est lisible : construire la machine, comprendre l'argent, garder le langage humain.", nb: "Finans, reassuranse, systemteknikk, privat AI, flerspråklige forretningsstudier og barns rettighetsarbeid hører hjemme på én side bare hvis den røde tråden er lesbar: bygg maskineriet, forstå pengene, hold språket menneskelig." },
"Finance, reinsurance, systems engineering, private AI, multilingual business study, and children's-rights advocacy belong on one page only if the through-line is legible: build the machinery, understand the money, keep the language human.", note: { en: "This desk combines the standing employment history from the current public profile with the live venture descriptions now used across the new site.", fr: "Ce bureau combine l'historique d'emploi actuel issu du profil public avec les descriptions des projets en cours désormais utilisées sur le nouveau site.", nb: "Denne desken kombinerer den nåværende ansettelseshistorikken fra den offentlige profilen med de levende prosjektbeskrivelsene som nå brukes på det nye nettstedet." },
note: };
"This desk combines the standing employment history from the current public profile with the live venture descriptions now used across the new site.",
export const cvHighlights: Record<LocaleCode, string[]> = {
en: [
"Dual Norwegian-American profile shaped across the United States and Europe.",
"Business education in Philadelphia, Brussels, Columbia, Paris, and Krakow.",
"Current work sits at the overlap of AI systems, civic advocacy, and cultural publishing.",
],
fr: [
"Double profil norvégien-américain façonné entre les États-Unis et l'Europe.",
"Formation en commerce à Philadelphie, Bruxelles, Columbia, Paris et Cracovie.",
"Travail actuel à l'intersection des systèmes d'IA, du plaidoyer civique et de l'édition culturelle."
],
nb: [
"Dobbelt norsk-amerikansk profil formet mellom USA og Europa.",
"Forretningsutdanning i Philadelphia, Brussel, Columbia, Paris og Krakow.",
"Nåværende arbeid ligger i skjæringspunktet mellom AI-systemer, samfunnsadvokatvirksomhet og kulturell publisering."
],
}; };
export const cvMandates: CvMandate[] = [ export const cvMandates: CvMandate[] = [
@@ -38,22 +56,18 @@ export const cvMandates: CvMandate[] = [
role: "Owner", role: "Owner",
org: "Blue Note Logic Inc", org: "Blue Note Logic Inc",
location: "Philadelphia to Paris, with EU infrastructure", location: "Philadelphia to Paris, with EU infrastructure",
summary: summary: { en: "Private AI platforms, document intelligence, and sovereign infrastructure designed to turn working knowledge into owned systems rather than rented dependency.", fr: "Plateformes d'IA privées, intelligence documentaire et infrastructures souveraines conçues pour transformer les connaissances opérationnelles en systèmes possédés plutôt qu'en dépendances louées.", nb: "Private AI-plattformer, dokumentintelligens og suveren infrastruktur designet for å gjøre arbeidskunnskap om til eierskapssystemer i stedet for leieavhengighet." },
"Private AI platforms, document intelligence, and sovereign infrastructure designed to turn working knowledge into owned systems rather than rented dependency.", detail: { en: "Current work centers on CorpusAI, document-grounded retrieval, corporate memory extraction, and EU-hosted deployments that keep data, models, and outcomes under client control.", fr: "Le travail actuel se concentre sur CorpusAI, la récupération basée sur les documents, l'extraction de mémoire d'entreprise et les déploiements hébergés dans l'UE qui maintiennent les données, les modèles et les résultats sous le contrôle des clients.", nb: "Nåværende arbeid fokuserer på CorpusAI, dokumentbasert gjenfinning, bedriftsminneuttrekk og EU-vertsbaserte distribusjoner som holder data, modeller og resultater under klientens kontroll." },
detail:
"Current work centers on CorpusAI, document-grounded retrieval, corporate memory extraction, and EU-hosted deployments that keep data, models, and outcomes under client control.",
sourceLabel: "Blue Note Logic official sites", sourceLabel: "Blue Note Logic official sites",
sourceUrl: "https://ai.bluenotelogic.com/", sourceUrl: "https://bluenotelogic.com",
}, },
{ {
years: "2025 to present", years: "2025 to present",
role: "CEO & Founder", role: "CEO & Founder",
org: "Do Better Norge", org: "Do Better Norge",
location: "Kongsberg, Norway", location: "Kongsberg, Norway",
summary: summary: { en: "A civic and advocacy organization focused on parental rights, children's rights, due process, and the treatment of immigrant and international families in Norway.", fr: "Une organisation civique et de plaidoyer axée sur les droits parentaux, les droits des enfants, le respect des procédures légales et le traitement des familles immigrées et internationales en Norvège.", nb: "En samfunns- og interesseorganisasjon med fokus på foreldrerettigheter, barns rettigheter, rettssikkerhet og behandlingen av innvandrer- og internasjonale familier i Norge." },
"A civic and advocacy organization focused on parental rights, children's rights, due process, and the treatment of immigrant and international families in Norway.", detail: { en: "The public work includes rights explainers, source-based legal materials, media-facing advocacy, and a broader reform effort aimed at family unity and procedural fairness.", fr: "Le travail public inclut des explications sur les droits, des documents juridiques basés sur des sources, un plaidoyer médiatique et un effort de réforme plus large visant à l'unité familiale et à l'équité procédurale.", nb: "Det offentlige arbeidet inkluderer rettighetsforklaringer, kildebaserte juridiske materialer, medieorientert påvirkning og en bredere reforminnsats rettet mot familiegjenforening og prosessrettferdighet." },
detail:
"The public work includes rights explainers, source-based legal materials, media-facing advocacy, and a broader reform effort aimed at family unity and procedural fairness.",
sourceLabel: "Do Better Norge", sourceLabel: "Do Better Norge",
sourceUrl: "https://dobetternorge.no/", sourceUrl: "https://dobetternorge.no/",
}, },
@@ -62,10 +76,8 @@ export const cvMandates: CvMandate[] = [
role: "Owner", role: "Owner",
org: "Gilligan TECH ENK", org: "Gilligan TECH ENK",
location: "Kongsberg, Norway", location: "Kongsberg, Norway",
summary: summary: { en: "AI systems engineering for Nordic SMBs: architectural audits, system builds, and fractional CTO support anchored in practical ROI rather than consultant theatre.", fr: "Ingénierie de systèmes d'IA pour les PME nordiques : audits architecturaux, constructions de systèmes et support CTO fractionné ancré dans un ROI pratique plutôt que dans du théâtre de consultant.", nb: "AI-systemteknikk for nordiske SMB-er: arkitekturgjennomganger, systembygging og delt CTO-støtte forankret i praktisk avkastning på investeringer fremfor konsulentteater." },
"AI systems engineering for Nordic SMBs: architectural audits, system builds, and fractional CTO support anchored in practical ROI rather than consultant theatre.", detail: { en: "The current Gilligan Tech positioning is explicit about sovereign European AI, dedicated infrastructure, and the mix of strategy, SQL, RAG, transcription, and deployment work needed to make AI useful in the real world.", fr: "Le positionnement actuel de Gilligan Tech est explicite sur l'IA européenne souveraine, les infrastructures dédiées et le mélange de stratégie, SQL, RAG, transcription et travail de déploiement nécessaire pour rendre l'IA utile dans le monde réel.", nb: "Den nåværende posisjoneringen til Gilligan Tech er eksplisitt om suveren europeisk AI, dedikert infrastruktur og blandingen av strategi, SQL, RAG, transkripsjon og distribusjonsarbeid som trengs for å gjøre AI nyttig i den virkelige verden." },
detail:
"The current Gilligan Tech positioning is explicit about sovereign European AI, dedicated infrastructure, and the mix of strategy, SQL, RAG, transcription, and deployment work needed to make AI useful in the real world.",
sourceLabel: "Gilligan Tech", sourceLabel: "Gilligan Tech",
sourceUrl: "https://gilligan.tech/", sourceUrl: "https://gilligan.tech/",
}, },
@@ -77,110 +89,77 @@ export const cvTimeline: CvRole[] = [
role: "Technical Consultant - Finance", role: "Technical Consultant - Finance",
org: "Kvaerner", org: "Kvaerner",
location: "Oslo, Norway", location: "Oslo, Norway",
summary: summary: { en: "Worked with corporate finance, HR, purchasing, cost estimating, MS SQL Server, Promineo, and QlikView to make large industrial bid and cost systems more useful to decision-makers.", fr: "Travaillé avec la finance d'entreprise, les RH, les achats, l'estimation des coûts, MS SQL Server, Promineo et QlikView pour rendre les systèmes d'offres et de coûts industriels plus utiles aux décideurs.", nb: "Jobbet med bedriftsfinans, HR, innkjøp, kostnadsestimering, MS SQL Server, Promineo og QlikView for å gjøre store industrielle anbuds- og kostnadssystemer mer nyttige for beslutningstakere." },
"Worked with corporate finance, HR, purchasing, cost estimating, MS SQL Server, Promineo, and QlikView to make large industrial bid and cost systems more useful to decision-makers.", bullets: { en: ["Reported to the Senior Vice President of Corporate Finance.","Helped build the Cost Estimating Project (CEM), a major change in how tenders were issued for offshore platforms and jackets.","Linked finance logic, operational data, and what-if analysis across multiple departments."], fr: ["Rapporté au Vice-Président Senior des Finances d'Entreprise.","A aidé à construire le projet d'estimation des coûts (CEM), un changement majeur dans la manière dont les appels d'offres étaient émis pour les plateformes offshore et les jackets.","Lié la logique financière, les données opérationnelles et l'analyse de scénarios entre plusieurs départements."], nb: ["Rapporterte til Senior Vice President for Corporate Finance.","Bidro til å bygge Cost Estimating Project (CEM), en stor endring i hvordan anbud ble utstedt for offshore-plattformer og -understell.","Koblet finanslogikk, operasjonelle data og hypotetiske analyser på tvers av flere avdelinger."] },
bullets: [
"Reported to the Senior Vice President of Corporate Finance.",
"Helped build the Cost Estimating Project (CEM), a major change in how tenders were issued for offshore platforms and jackets.",
"Linked finance logic, operational data, and what-if analysis across multiple departments.",
],
}, },
{ {
years: "2008 to 2011", years: "2008 to 2011",
role: "Owner", role: "Owner",
org: "Chloe & Colin", org: "Chloe & Colin",
location: "United States", location: "United States",
summary: summary: { en: "Operated a general IT consultancy serving small businesses and organizations across practical technology needs before the current AI era.", fr: "Exploité une société de conseil en informatique générale au service des petites entreprises et organisations pour leurs besoins technologiques pratiques avant l'ère actuelle de l'IA.", nb: "Drevet et generelt IT-konsulentfirma som betjente små bedrifter og organisasjoner med praktiske teknologibehov før den nåværende AI-æraen." },
"Operated a general IT consultancy serving small businesses and organizations across practical technology needs before the current AI era.", bullets: { en: ["Worked as a hands-on generalist across infrastructure, applications, websites, databases, and operational problem-solving.","Helped clients improve reliability, efficiency, and day-to-day use of their systems without unnecessary complexity.","A broad-spectrum owner-operator chapter that fills the gap between larger corporate roles and later venture work."], fr: ["Travaillé comme généraliste pratique sur les infrastructures, applications, sites web, bases de données et résolution de problèmes opérationnels.","A aidé les clients à améliorer la fiabilité, l'efficacité et l'utilisation quotidienne de leurs systèmes sans complexité inutile.","Un chapitre de propriétaire-opérateur à large spectre comblant l'écart entre les rôles d'entreprise plus importants et le travail de capital-risque ultérieur."], nb: ["Jobbet som en praktisk generalist innen infrastruktur, applikasjoner, nettsteder, databaser og operasjonell problemløsning.","Hjalp klienter med å forbedre pålitelighet, effektivitet og daglig bruk av systemene deres uten unødvendig kompleksitet.","En bredspektret eier-operatør-periode som fylte gapet mellom større bedriftsroller og senere venturearbeid."] },
bullets: [
"Worked as a hands-on generalist across infrastructure, applications, websites, databases, and operational problem-solving.",
"Helped clients improve reliability, efficiency, and day-to-day use of their systems without unnecessary complexity.",
"A broad-spectrum owner-operator chapter that fills the gap between larger corporate roles and later venture work.",
],
}, },
{ {
years: "2006 to 2007", years: "2006 to 2007",
role: "Assistant Vice President", role: "Assistant Vice President",
org: "Ariel Re", org: "Ariel Re",
location: "Hamilton, Bermuda", location: "Hamilton, Bermuda",
summary: summary: { en: "Built the IT support structure for AIR and RMS catastrophe risk modeling and ran stochastic analysis on large insured property portfolios.", fr: "Construit la structure de support informatique pour la modélisation des risques catastrophiques AIR et RMS et réalisé des analyses stochastiques sur de grands portefeuilles de propriétés assurées.", nb: "Bygget IT-støttestrukturen for AIR- og RMS-katastroferisikomodellering og utførte stokastiske analyser på store forsikrede eiendomsporteføljer." },
"Built the IT support structure for AIR and RMS catastrophe risk modeling and ran stochastic analysis on large insured property portfolios.", bullets: { en: ["Worked at the junction of underwriters, analysts, and internal software teams.","Supported catastrophe modeling across hurricanes, earthquakes, storms, and other modeled events."], fr: ["Travaillé à la jonction des souscripteurs, analystes et équipes logicielles internes.","Soutenu la modélisation des catastrophes pour les ouragans, tremblements de terre, tempêtes et autres événements modélisés."], nb: ["Jobbet i skjæringspunktet mellom forsikringsgivere, analytikere og interne programvareteam.","Støttet katastrofemodellering for orkaner, jordskjelv, stormer og andre modellerte hendelser."] },
bullets: [
"Worked at the junction of underwriters, analysts, and internal software teams.",
"Supported catastrophe modeling across hurricanes, earthquakes, storms, and other modeled events.",
],
}, },
{ {
years: "2001 to 2006", years: "2001 to 2006",
role: "Assistant Secretary", role: "Assistant Secretary",
org: "Folksamerica / White Mountains Re", org: "Folksamerica / White Mountains Re",
location: "New York City, New York", location: "New York City, New York",
summary: summary: { en: "Analyzed property treaty reinsurance risk while helping design the infrastructure around large-scale actuarial and underwriting work.", fr: "Analysé les risques de réassurance des traités immobiliers tout en aidant à concevoir l'infrastructure autour des travaux actuariels et de souscription à grande échelle.", nb: "Analyserte risikoen for eiendomsgjenforsikring mens jeg bidro til å designe infrastrukturen rundt storskala aktuarmessig og underwriting-arbeid." },
"Analyzed property treaty reinsurance risk while helping design the infrastructure around large-scale actuarial and underwriting work.", bullets: { en: ["Worked closely with senior management, underwriters, and actuarial staff on pricing and risk analysis.","Helped shape a multi-tiered risk management environment with roughly 30 data-analysis engines and 4 MS SQL database servers.","Supported both Windows client and IIS-based global web interfaces."], fr: ["Travaillé en étroite collaboration avec la direction, les souscripteurs et le personnel actuariel sur la tarification et l'analyse des risques.","A aidé à façonner un environnement de gestion des risques à plusieurs niveaux avec environ 30 moteurs d'analyse de données et 4 serveurs de bases de données MS SQL.","Soutenu à la fois les interfaces globales Windows client et IIS basées sur le web."], nb: ["Jobbet tett med toppledelsen, forsikringsgivere og aktuarmessig personale om prising og risikoberegning.","Bidro til å forme et flernivå risikostyringsmiljø med omtrent 30 dataanalyse-motorer og 4 MS SQL-databaseservere.","Støttet både Windows-klient og IIS-baserte globale webgrensesnitt."] },
bullets: [
"Worked closely with senior management, underwriters, and actuarial staff on pricing and risk analysis.",
"Helped shape a multi-tiered risk management environment with roughly 30 data-analysis engines and 4 MS SQL database servers.",
"Supported both Windows client and IIS-based global web interfaces.",
],
}, },
{ {
years: "1998 to 1999", years: "1998 to 1999",
role: "Intern for CFO", role: "Intern for CFO",
org: "Zurich France / Zurich Financial Services", org: "Zurich France / Zurich Financial Services",
location: "Paris, France", location: "Paris, France",
summary: summary: { en: "Graduate internship in the CFO's orbit during the French Franc to Euro transition and the Year 2000 remediation cycle.", fr: "Stage de fin d'études dans l'orbite du CFO pendant la transition du Franc français à l'Euro et le cycle de remédiation de l'An 2000.", nb: "Graduate internship i CFOs sfære under overgangen fra franske franc til euro og år 2000-oppgraderingssyklusen." },
"Graduate internship in the CFO's orbit during the French Franc to Euro transition and the Year 2000 remediation cycle.", bullets: { en: ["Worked on specialist project teams related to Euro conversion and Y2K readiness.","Built the \"Euro 1999\" client and partner sub-site in Lotus Notes."], fr: ["Travaillé sur des équipes de projets spécialisés liés à la conversion à l'Euro et à la préparation au passage à l'An 2000.","Construit le sous-site client et partenaire \"Euro 1999\" dans Lotus Notes."], nb: ["Jobbet i spesialistprosjektteam relatert til euro-konvertering og Y2K-beredskap.","Bygget \"Euro 1999\" klient- og partnerunderside i Lotus Notes."] },
bullets: [
"Worked on specialist project teams related to Euro conversion and Y2K readiness.",
"Built the \"Euro 1999\" client and partner sub-site in Lotus Notes.",
],
}, },
]; ];
export const cvSkillTracks: CvSkillTrack[] = [ export const cvSkillTracks: CvSkillTrack[] = [
{ {
label: "Operating languages", label: { en: "Operating languages", fr: "Langues de travail", nb: "Arbeidsspråk" },
items: ["English", "French", "Norwegian", "SQL", "the language of finance"], items: ["English", "French", "Norwegian", "SQL", "the language of finance"],
}, },
{ {
label: "Technical stack", label: { en: "Technical stack", fr: "Stack technique", nb: "Teknisk stack" },
items: ["MS SQL / MariaDB / MySQL", "PHP", "Python", "JavaScript / React / Astro", "RAG systems"], items: ["MS SQL / MariaDB / MySQL", "PHP", "Python", "JavaScript / React / Astro", "RAG systems"],
}, },
{ {
label: "Working domains", label: { en: "Working domains", fr: "Domaines de travail", nb: "Arbeidsdomener" },
items: ["Global finance", "Reinsurance", "Private AI", "Document intelligence", "Family-rights advocacy"], items: ["Global finance", "Reinsurance", "Private AI", "Document intelligence", "Family-rights advocacy"],
}, },
]; ];
export const cvHighlights = [
"Dual Norwegian-American profile shaped across the United States and Europe.",
"Business education in Philadelphia, Brussels, Columbia, Paris, and Krakow.",
"Current work sits at the overlap of AI systems, civic advocacy, and cultural publishing.",
];
export const cvSourceNotes = [ export const cvSourceNotes = [
{ {
label: "Current public profile API", label: "Current public profile API",
url: "http://davegilligan.local/api/content.php?lang=en", url: "http://davegilligan.local/api/content.php?lang=en",
note: note: "Used for the standing public experience history, headline language, education chronology, and contact references from the current local site.",
"Used for the standing public experience history, headline language, education chronology, and contact references from the current local site.",
}, },
{ {
label: "Gilligan Tech", label: "Gilligan Tech",
url: "https://gilligan.tech/", url: "https://gilligan.tech/",
note: note: "Used for the current Gilligan TECH ENK positioning, Kongsberg-based identity, and the AI Dispatch / Systems Forge / Systems Command model.",
"Used for the current Gilligan TECH ENK positioning, Kongsberg-based identity, and the AI Dispatch / Systems Forge / Systems Command model.",
}, },
{ {
label: "Blue Note Logic / CorpusAI", label: "Blue Note Logic / CorpusAI",
url: "https://ai.bluenotelogic.com/", url: "https://bluenotelogic.com",
note: note: "Used for the current Blue Note Logic mandate around private AI, document intelligence, and source-cited retrieval.",
"Used for the current Blue Note Logic mandate around private AI, document intelligence, and source-cited retrieval.",
}, },
{ {
label: "Do Better Norge", label: "Do Better Norge",
url: "https://dobetternorge.no/", url: "https://dobetternorge.no/",
note: note: "Used for the public advocacy scope around parental rights, children, family unity, and due process in Norway.",
"Used for the public advocacy scope around parental rights, children, family unity, and due process in Norway.",
}, },
]; ];
+55 -100
View File
@@ -1,3 +1,5 @@
import type { LocaleCode } from "./locales";
export type JazzImage = { export type JazzImage = {
src: string; src: string;
alt: string; alt: string;
@@ -11,23 +13,20 @@ export type JazzImage = {
export type JazzVenue = { export type JazzVenue = {
name: string; name: string;
href: string; href: string;
summary: string; summary: Record<LocaleCode, string>;
}; };
export const jazzHero = { export const jazzHero = {
eyebrow: "Section 06 / jazz and music", eyebrow: { en: "Section 06 / jazz and music", fr: undefined, nb: undefined },
title: "Pataphysics by the silver river, with one ear still turned toward the Caveau.", title: { en: "Pataphysics by the silver river, with one ear still turned toward the Caveau.", fr: "Pataphysique au bord de la rivière argentée, avec une oreille encore tournée vers le Caveau.", nb: "Pataphysikk ved den sølvfargede elven, med ett øre fortsatt vendt mot Caveau." },
lede: lede: { en: "The jazz desk now has a proper field note: half Latin Quarter memory, half Kongsberg route map, with a little smoke from rue Saint-Jacques still caught in the coat lining.", fr: "Le bureau jazz a enfin une note de terrain digne de ce nom : moitié souvenir du Quartier Latin, moitié carte des itinéraires de Kongsberg, avec un peu de fumée de la rue Saint-Jacques encore prise dans la doublure du manteau.", nb: "Jazzkontoret har nå en ordentlig feltnotat: halvveis et minne fra Latinerkvarteret, halvveis et kart over Kongsbergs ruter, med litt røyk fra rue Saint-Jacques fortsatt fanget i frakkens fôr." },
"The jazz desk now has a proper field note: half Latin Quarter memory, half Kongsberg route map, with a little smoke from rue Saint-Jacques still caught in the coat lining.", note: { en: "Built around official Kongsberg Jazz Festival sources for 2026, with venue links and openly licensed imagery where possible.", fr: "Construit autour des sources officielles du Kongsberg Jazz Festival pour 2026, avec des liens vers les lieux et des images sous licence libre lorsque cela est possible.", nb: "Bygget rundt offisielle kilder fra Kongsberg Jazzfestival for 2026, med lenker til arenaer og åpent lisensierte bilder der det er mulig." },
note:
"Built around official Kongsberg Jazz Festival sources for 2026, with venue links and openly licensed imagery where possible.",
}; };
export const jazzArticle = { export const jazzArticle = {
title: "A Pataphysical Field Guide to Kongsberg Jazz 2026", title: "A Pataphysical Field Guide to Kongsberg Jazz 2026",
publishedAt: "2026-04-06 08:15:00", publishedAt: "2026-04-06 08:15:00",
excerpt: excerpt: { en: "A personal route from the Caveau de la Huchette to Kongsberg Jazzfestival 2026, with notes on Samara Joy, Snarky Puppy, Mezzoforte, Kurt Rosenwinkel, Jazzbox free shows, and the venues that keep the town musically honest.", fr: "Un itinéraire personnel du Caveau de la Huchette au Kongsberg Jazzfestival 2026, avec des notes sur Samara Joy, Snarky Puppy, Mezzoforte, Kurt Rosenwinkel, les concerts gratuits Jazzbox, et les lieux qui maintiennent l'intégrité musicale de la ville.", nb: "En personlig rute fra Caveau de la Huchette til Kongsberg Jazzfestival 2026, med notater om Samara Joy, Snarky Puppy, Mezzoforte, Kurt Rosenwinkel, gratis Jazzbox-konserter og arenaene som holder byen musikalsk ærlig." },
"A personal route from the Caveau de la Huchette to Kongsberg Jazzfestival 2026, with notes on Samara Joy, Snarky Puppy, Mezzoforte, Kurt Rosenwinkel, Jazzbox free shows, and the venues that keep the town musically honest.",
}; };
export const jazzImages: JazzImage[] = [ export const jazzImages: JazzImage[] = [
@@ -36,11 +35,9 @@ export const jazzImages: JazzImage[] = [
alt: "Concert photograph from Christians Kjeller during Kongsberg Jazzfestival 2018.", alt: "Concert photograph from Christians Kjeller during Kongsberg Jazzfestival 2018.",
credit: "Tore Saetre", credit: "Tore Saetre",
license: "CC BY-SA 4.0", license: "CC BY-SA 4.0",
sourceUrl: sourceUrl: "https://commons.wikimedia.org/wiki/File:Hans_Cato_Kristiansen_Kongsberg_Jazzfestival_2018_(223526).jpg",
"https://commons.wikimedia.org/wiki/File:Hans_Cato_Kristiansen_Kongsberg_Jazzfestival_2018_(223526).jpg",
sourceLabel: "Wikimedia Commons", sourceLabel: "Wikimedia Commons",
note: note: "Used here as an atmospheric Christians Kjeller / Kongsberg Jazz image rather than a 2026 festival still.",
"Used here as an atmospheric Christians Kjeller / Kongsberg Jazz image rather than a 2026 festival still.",
}, },
{ {
src: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Le_Caveau_de_la_Huchette.jpg", src: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Le_Caveau_de_la_Huchette.jpg",
@@ -53,100 +50,25 @@ export const jazzImages: JazzImage[] = [
]; ];
export const jazzPicks = [ export const jazzPicks = [
{ { day: "Wednesday 1 July 2026", artist: "Samara Joy", detail: { en: "Opening concert at Kongsberg Musikkteater, followed later that night by Ghosted at Energimolla.", fr: "Concert d'ouverture au Kongsberg Musikkteater, suivi plus tard dans la soirée par Ghosted à l'Energimolla.", nb: "Åpningskonsert på Kongsberg Musikkteater, etterfulgt senere samme kveld av Ghosted på Energimolla." }, href: "https://kongsbergjazz.no/program/" },
day: "Wednesday 1 July 2026", { day: "Thursday 2 July 2026", artist: "Bobo Stenson Trio / Snarky Puppy / Mezzoforte", detail: { en: "A day that moves from piano intelligence to large-room propulsion and then into late-night club lift at Christians Kjeller.", fr: "Une journée qui passe de l'intelligence du piano à la propulsion des grandes salles, puis à l'élévation nocturne du club Christians Kjeller.", nb: "En dag som beveger seg fra piano-intelligens til storroms-propulsjon og deretter til nattklubb-løft på Christians Kjeller." }, href: "https://kongsbergjazz.no/program/" },
artist: "Samara Joy", { day: "Friday 3 July 2026", artist: "Wesseltoft-Andersen-Nilssen Trio / Wibutee / Matoma", detail: { en: "The sort of Friday where one can begin in church-space seriousness and end outdoors at Gamle Norge with the town in full chorus.", fr: "Le genre de vendredi où l'on peut commencer dans la gravité d'un espace sacré et finir en plein air à Gamle Norge avec la ville en chœur.", nb: "En slags fredag hvor man kan starte med alvor i kirkerommet og avslutte utendørs på Gamle Norge med byen i full kor." }, href: "https://kongsbergjazz.no/program/" },
detail: "Opening concert at Kongsberg Musikkteater, followed later that night by Ghosted at Energimolla.", { day: "Saturday 4 July 2026", artist: "Silya & Kongsberg Storband / Kurt Rosenwinkel / Nils Petter Molvaer / Silje Nergaard", detail: { en: "The last day plays like a designed crescendo: big-band daylight, guitar dusk, electric weather, then midnight vocals.", fr: "Le dernier jour joue comme un crescendo conçu : lumière du jour en big band, crépuscule de guitare, météo électrique, puis voix de minuit.", nb: "Den siste dagen spiller som en designet crescendo: storband i dagslys, gitar i skumringen, elektrisk vær, og deretter midnattssang." }, href: "https://kongsbergjazz.no/program/" },
href: "https://kongsbergjazz.no/program/",
},
{
day: "Thursday 2 July 2026",
artist: "Bobo Stenson Trio / Snarky Puppy / Mezzoforte",
detail: "A day that moves from piano intelligence to large-room propulsion and then into late-night club lift at Christians Kjeller.",
href: "https://kongsbergjazz.no/program/",
},
{
day: "Friday 3 July 2026",
artist: "Wesseltoft-Andersen-Nilssen Trio / Wibutee / Matoma",
detail: "The sort of Friday where one can begin in church-space seriousness and end outdoors at Gamle Norge with the town in full chorus.",
href: "https://kongsbergjazz.no/program/",
},
{
day: "Saturday 4 July 2026",
artist: "Silya & Kongsberg Storband / Kurt Rosenwinkel / Nils Petter Molvaer / Silje Nergaard",
detail: "The last day plays like a designed crescendo: big-band daylight, guitar dusk, electric weather, then midnight vocals.",
href: "https://kongsbergjazz.no/program/",
},
]; ];
export const jazzFreeNotes = [ export const jazzFreeNotes = [
{ { title: { en: "Jazzbox / Jazzboksen", fr: "Jazzbox / Jazzboksen", nb: "Jazzbox / Jazzboksen" }, summary: { en: "Free concerts every day during the festival, with students, semi-professionals and amateurs onstage, no reservation required, and a proper social center in the middle of town.", fr: "Concerts gratuits tous les jours pendant le festival, avec des étudiants, semi-professionnels et amateurs sur scène, sans réservation requise, et un véritable centre social au cœur de la ville.", nb: "Gratis konserter hver dag under festivalen, med studenter, semi-profesjonelle og amatører på scenen, ingen reservasjon nødvendig, og et ordentlig sosialt sentrum midt i byen." }, href: "https://kongsbergjazz.no/en/faq/jazzboksen/" },
title: "Jazzbox / Jazzboksen", { title: { en: "Jazzmine", fr: "Jazzmine", nb: "Jazzmine" }, summary: { en: "A free Friday session in the Jazzbox area that points toward the new Magasinet / Fellesbrukskrysset idea as a future festival midpoint.", fr: "Une session gratuite le vendredi dans la zone Jazzbox qui pointe vers la nouvelle idée Magasinet / Fellesbrukskrysset comme futur centre du festival.", nb: "En gratis fredagssession i Jazzbox-området som peker mot den nye Magasinet / Fellesbrukskrysset-ideen som et fremtidig festivalmidtpunkt." }, href: "https://kongsbergjazz.no/events/jazzmine/" },
summary:
"Free concerts every day during the festival, with students, semi-professionals and amateurs onstage, no reservation required, and a proper social center in the middle of town.",
href: "https://kongsbergjazz.no/en/faq/jazzboksen/",
},
{
title: "Jazzmine",
summary:
"A free Friday session in the Jazzbox area that points toward the new Magasinet / Fellesbrukskrysset idea as a future festival midpoint.",
href: "https://kongsbergjazz.no/events/jazzmine/",
},
]; ];
export const jazzVenues: JazzVenue[] = [ export const jazzVenues: JazzVenue[] = [
{ { name: "Privat Bar", href: "https://www.privatbar.no/", summary: { en: "Privat Bar describes itself as scene, sport, cocktail bar, gastropub and courtyard venue, a place built for after-hours momentum.", fr: "Privat Bar se décrit comme une scène, un bar sportif, un bar à cocktails, un gastropub et un lieu en cour intérieure, un endroit conçu pour le momentum après les heures.", nb: "Privat Bar beskriver seg selv som scene, sportsbar, cocktailbar, gastropub og gårdsplass-venue, et sted bygget for etter-timers momentum." } },
name: "Privat Bar", { name: "Gamle Norge / Folkefestscenen", href: "https://kongsbergjazz.no/spillesteder/folkefestscenen-gamle-norge/", summary: { en: "The festival calls it an outdoor stage between Gamle Norge and Trattoria Madre, designed for familiar names, sing-along lift and party atmosphere.", fr: "Le festival le décrit comme une scène extérieure entre Gamle Norge et Trattoria Madre, conçue pour des noms familiers, une ambiance chantante et une atmosphère festive.", nb: "Festivalen kaller det en utendørsscene mellom Gamle Norge og Trattoria Madre, designet for kjente navn, allsang-løft og feststemning." } },
href: "https://www.privatbar.no/", { name: "Opsahlgarden / Christians Kjeller", href: "https://kongsbergjazz.no/hei-christian-kjeller/", summary: { en: "Christian's Kjeller sits inside Opsahlgarden on Vestsida: restaurant upstairs, gastro/music pub in the basement, brewery in the mix, and a long memory for late sets.", fr: "Christian's Kjeller se trouve à l'intérieur d'Opsahlgarden sur Vestsida : restaurant à l'étage, pub gastro/musical au sous-sol, brasserie dans le mélange, et une longue mémoire pour les sets tardifs.", nb: "Christian's Kjeller ligger inne i Opsahlgarden på Vestsida: restaurant oppe, gastro/musikkpub i kjelleren, bryggeri i miksen, og en lang hukommelse for sene sett." } },
summary:
"Privat Bar describes itself as scene, sport, cocktail bar, gastropub and courtyard venue, a place built for after-hours momentum.",
},
{
name: "Gamle Norge / Folkefestscenen",
href: "https://kongsbergjazz.no/spillesteder/folkefestscenen-gamle-norge/",
summary:
"The festival calls it an outdoor stage between Gamle Norge and Trattoria Madre, designed for familiar names, sing-along lift and party atmosphere.",
},
{
name: "Opsahlgarden / Christians Kjeller",
href: "https://kongsbergjazz.no/hei-christian-kjeller/",
summary:
"Christian's Kjeller sits inside Opsahlgarden on Vestsida: restaurant upstairs, gastro/music pub in the basement, brewery in the mix, and a long memory for late sets.",
},
]; ];
export const jazzSources = [ export const jazzBody: Record<LocaleCode, string[]> = {
{ en: [
label: "Kongsberg Jazzfestival programme 2026",
href: "https://kongsbergjazz.no/program/",
},
{
label: "Kongsberg Jazzfestival / Folkefestscenen Gamle Norge",
href: "https://kongsbergjazz.no/spillesteder/folkefestscenen-gamle-norge/",
},
{
label: "Kongsberg Jazzfestival / Hei Privat Bar",
href: "https://kongsbergjazz.no/hei-privat-bar/",
},
{
label: "Kongsberg Jazzfestival / Hei Christian Kjeller",
href: "https://kongsbergjazz.no/hei-christian-kjeller/",
},
{
label: "Kongsberg Jazzfestival / Jazzbox",
href: "https://kongsbergjazz.no/en/faq/jazzboksen/",
},
{
label: "Kongsberg Jazzfestival / Jazzmine",
href: "https://kongsbergjazz.no/events/jazzmine/",
},
{
label: "Caveau de la Huchette",
href: "https://www.caveaudelahuchette.fr/",
},
];
export const jazzBody = [
"There are people who approach a jazz festival with a spreadsheet, and there are people who approach it the way one enters a cellar in Paris after midnight: not to control the night, but to meet it properly. I belong to the second camp, even when the first camp pays the invoices. So yes, Kongsberg Jazzfestival 2026 deserves practical planning. But it also deserves a little pataphysics: the science of exceptions, the doctrine of beloved detours, the right to follow one brass phrase into another street and call that method.", "There are people who approach a jazz festival with a spreadsheet, and there are people who approach it the way one enters a cellar in Paris after midnight: not to control the night, but to meet it properly. I belong to the second camp, even when the first camp pays the invoices. So yes, Kongsberg Jazzfestival 2026 deserves practical planning. But it also deserves a little pataphysics: the science of exceptions, the doctrine of beloved detours, the right to follow one brass phrase into another street and call that method.",
"My own measuring stick is still French and subterranean. For a time I lived at 23 rue Saint-Jacques, with the Caveau de la Huchette close enough to count as neighborhood weather. The Caveau at 5 rue de la Huchette still presents itself as the temple of swing, open all year, every night, a place where Paris and jazz agree to keep dancing without asking permission. Once you have learned to think of a city through such a room, every later festival is judged by whether it contains at least one staircase into a better mood.", "My own measuring stick is still French and subterranean. For a time I lived at 23 rue Saint-Jacques, with the Caveau de la Huchette close enough to count as neighborhood weather. The Caveau at 5 rue de la Huchette still presents itself as the temple of swing, open all year, every night, a place where Paris and jazz agree to keep dancing without asking permission. Once you have learned to think of a city through such a room, every later festival is judged by whether it contains at least one staircase into a better mood.",
"Kongsberg, fortunately, understands this. The official 2026 programme already reads like a civilised argument between polish and danger. On Wednesday 1 July, Samara Joy opens at Kongsberg Musikkteater, and Ghosted follows at Energimolla. Thursday 2 July offers Bobo Stenson Trio, ganavya, Snarky Puppy and late-night Mezzoforte at Christians Kjeller. Friday 3 July runs from Wesseltoft-Andersen-Nilssen Trio and Wibutee to the larger public-weather of Rotlaus and Matoma at Gamle Norge. Saturday 4 July closes with Silya & Kongsberg Storband, Kurt Rosenwinkel, Nils Petter Molvaer and a midnight Silje Nergaard set. That is not a thin programme. That is a town temporarily edited by rhythm.", "Kongsberg, fortunately, understands this. The official 2026 programme already reads like a civilised argument between polish and danger. On Wednesday 1 July, Samara Joy opens at Kongsberg Musikkteater, and Ghosted follows at Energimolla. Thursday 2 July offers Bobo Stenson Trio, ganavya, Snarky Puppy and late-night Mezzoforte at Christians Kjeller. Friday 3 July runs from Wesseltoft-Andersen-Nilssen Trio and Wibutee to the larger public-weather of Rotlaus and Matoma at Gamle Norge. Saturday 4 July closes with Silya & Kongsberg Storband, Kurt Rosenwinkel, Nils Petter Molvaer and a midnight Silje Nergaard set. That is not a thin programme. That is a town temporarily edited by rhythm.",
@@ -155,5 +77,38 @@ export const jazzBody = [
"Then there is Christians Kjeller, which matters to me more than the poster typography. The festival's portrait of the venue places it inside Opsahlgarden on Vestsida, one of the town's older preserved houses, restored and reopened as restaurant in 2001. The house carries restaurant, brewery and basement music-pub energy all at once. That combination is morally sound. If Gamle Norge is the public square in summer clothes, Christians Kjeller is the lower room where style loosens its tie and means it. Mezzoforte on Thursday night, Poesioasen with Ine Hoem and Edvard Hoem on Saturday afternoon, and Pumpegris later that same night: this is exactly the kind of sequencing one wants from a compact jazz city.", "Then there is Christians Kjeller, which matters to me more than the poster typography. The festival's portrait of the venue places it inside Opsahlgarden on Vestsida, one of the town's older preserved houses, restored and reopened as restaurant in 2001. The house carries restaurant, brewery and basement music-pub energy all at once. That combination is morally sound. If Gamle Norge is the public square in summer clothes, Christians Kjeller is the lower room where style loosens its tie and means it. Mezzoforte on Thursday night, Poesioasen with Ine Hoem and Edvard Hoem on Saturday afternoon, and Pumpegris later that same night: this is exactly the kind of sequencing one wants from a compact jazz city.",
"And then there is the democratic correction: the free programme. Jazzbox / Jazzboksen is still one of the best arguments for the whole festival, because it gives students, semi-professionals and amateurs a real audience rather than an educational corner. The official festival note is clear: free concerts every day, no reservation, just turn up, with room to lounge, drift, listen, dance and discover tomorrow's names before they become brochure adjectives. Jazzmine, listed as a free Friday happening, extends that logic into the new Magasinet / Fellesbrukskrysset center. Every good festival needs paid monuments; every great one also needs zones where curiosity remains inexpensive.", "And then there is the democratic correction: the free programme. Jazzbox / Jazzboksen is still one of the best arguments for the whole festival, because it gives students, semi-professionals and amateurs a real audience rather than an educational corner. The official festival note is clear: free concerts every day, no reservation, just turn up, with room to lounge, drift, listen, dance and discover tomorrow's names before they become brochure adjectives. Jazzmine, listed as a free Friday happening, extends that logic into the new Magasinet / Fellesbrukskrysset center. Every good festival needs paid monuments; every great one also needs zones where curiosity remains inexpensive.",
"So the proper route, at least in my private cosmology, is not to choose between the polished and the unruly. It is to move among them. Begin with shape and listening. Allow Samara Joy or Bobo Stenson to set the grammar. Slip into Privat Bar for the social murmur. Let Gamle Norge provide the public proof that joy also scales. End in Christians Kjeller when the evening starts to remember its own pulse. And if the day needs a reset, take the free path through Jazzbox and Jazzmine, where the future is usually less expensive and often more alive.", "So the proper route, at least in my private cosmology, is not to choose between the polished and the unruly. It is to move among them. Begin with shape and listening. Allow Samara Joy or Bobo Stenson to set the grammar. Slip into Privat Bar for the social murmur. Let Gamle Norge provide the public proof that joy also scales. End in Christians Kjeller when the evening starts to remember its own pulse. And if the day needs a reset, take the free path through Jazzbox and Jazzmine, where the future is usually less expensive and often more alive.",
"Boris Vian would probably recommend a trumpet, a bad idea and a better jacket. Vernon Sullivan would tell you to stay out too late and call it field research. I will settle for a calmer prescription: Kongsberg Jazzfestival, 1 to 4 July 2026, looks like the kind of week that lets a town become multiple versions of itself at once. That is close enough to pataphysics for me, and certainly close enough to jazz.", "Boris Vian would probably recommend a trumpet, a bad idea and a better jacket. Vernon Sullivan would tell you to stay out too late and call it field research. I will settle for a calmer prescription: Kongsberg Jazzfestival, 1 to 4 July 2026, looks like the kind of week that lets a town become multiple versions of itself at once. That is close enough to pataphysics for me, and certainly close enough to jazz."
],
fr: [
"Il y a ceux qui abordent un festival de jazz avec un tableur, et ceux qui y entrent comme on pénètre dans une cave à Paris après minuit : non pas pour contrôler la nuit, mais pour la rencontrer comme il se doit. Je fais partie du deuxième camp, même lorsque le premier camp paie les factures. Alors oui, le Kongsberg Jazzfestival 2026 mérite une planification pratique. Mais il mérite aussi un peu de pataphysique : la science des exceptions, la doctrine des détours bien-aimés, le droit de suivre une phrase de cuivre dans une autre rue et d'appeler cela une méthode.",
"Mon propre étalon reste français et souterrain. Pendant un temps, j'ai vécu au 23 rue Saint-Jacques, avec le Caveau de la Huchette assez proche pour compter comme météo de quartier. Le Caveau au 5 rue de la Huchette se présente toujours comme le temple du swing, ouvert toute l'année, chaque soir, un endroit où Paris et le jazz s'accordent pour continuer à danser sans demander la permission. Une fois que vous avez appris à penser une ville à travers une telle salle, chaque festival ultérieur est jugé par sa capacité à contenir au moins un escalier vers une meilleure humeur.",
"Kongsberg, heureusement, comprend cela. Le programme officiel 2026 ressemble déjà à un argument civilisé entre le poli et le danger. Le mercredi 1er juillet, Samara Joy ouvre au Kongsberg Musikkteater, et Ghosted suit à l'Energimolla. Le jeudi 2 juillet propose Bobo Stenson Trio, ganavya, Snarky Puppy et Mezzoforte en fin de soirée au Christians Kjeller. Le vendredi 3 juillet passe du Wesseltoft-Andersen-Nilssen Trio et Wibutee à la météo publique plus large de Rotlaus et Matoma à Gamle Norge. Le samedi 4 juillet se termine avec Silya & Kongsberg Storband, Kurt Rosenwinkel, Nils Petter Molvaer et un set de minuit de Silje Nergaard. Ce n'est pas un programme maigre. C'est une ville temporairement éditée par le rythme.",
"Si l'on préfère les petites salles, la géographie devient encore plus intéressante. Le JAZZpass du festival nomme explicitement Privat Bar parmi les lieux intérieurs intimes, tout en notant que Gamle Norge et Christians Kjeller échappent à cette logique de pass, ce qui est exactement juste : certains endroits appartiennent au circuit ordonné, d'autres à l'appétit. Privat Bar, dans sa propre voix, est un bar à cocktails, une scène, un bar sportif, une discothèque et une cour appelée Oasen ; le portrait du lieu par le festival le décrit comme un endroit qui prospère grâce à la qualité, au service, à l'atmosphère et à la folkefest. En d'autres termes : pas seulement un bar, mais un accélérateur social.",
"Gamle Norge est la réponse extérieure. Le festival décrit Folkefestscenen / Gamle Norge comme la scène des noms familiers, de la force chantante et de l'excès festif, placée entre Gamle Norge et Trattoria Madre. Cela vous dit presque tout ce que vous devez savoir. On n'y va pas pour prouver son sérieux. On y va parce que chaque festival sérieux a besoin d'une zone où la rue devient chœur. Espen Lind, Rotlaus, Matoma, No. 4 et Stavangerkameratene donnent à cette scène son visage 2026, et aucune de ces programmations ne prétend être austère. Bien. Les festivals ont besoin de largeur s'ils veulent ressembler à des villes plutôt qu'à des cercles fermés.",
"Puis il y a Christians Kjeller, qui compte pour moi plus que la typographie des affiches. Le portrait du lieu par le festival le place à l'intérieur d'Opsahlgarden sur Vestsida, l'une des maisons anciennes préservées de la ville, restaurée et rouverte comme restaurant en 2001. La maison porte à la fois restaurant, brasserie et énergie de pub musical en sous-sol. Cette combinaison est moralement saine. Si Gamle Norge est la place publique en habits d'été, Christians Kjeller est la salle basse où le style desserre sa cravate et le fait avec sincérité. Mezzoforte jeudi soir, Poesioasen avec Ine Hoem et Edvard Hoem samedi après-midi, et Pumpegris plus tard dans la même nuit : c'est exactement le genre de séquençage que l'on attend d'une ville jazz compacte.",
"Et puis il y a la correction démocratique : le programme gratuit. Jazzbox / Jazzboksen reste l'un des meilleurs arguments pour tout le festival, car il offre aux étudiants, semi-professionnels et amateurs un véritable public plutôt qu'un coin éducatif. La note officielle du festival est claire : concerts gratuits tous les jours, sans réservation, il suffit de venir, avec de l'espace pour se détendre, dériver, écouter, danser et découvrir les noms de demain avant qu'ils ne deviennent des adjectifs de brochure. Jazzmine, listé comme un événement gratuit le vendredi, étend cette logique au nouveau centre Magasinet / Fellesbrukskrysset. Tout bon festival a besoin de monuments payants ; tout grand festival a également besoin de zones où la curiosité reste peu coûteuse.",
"Alors, le bon itinéraire, du moins dans ma cosmologie privée, n'est pas de choisir entre le poli et l'indiscipliné. C'est de naviguer entre eux. Commencez par la forme et l'écoute. Laissez Samara Joy ou Bobo Stenson poser la grammaire. Glissez-vous dans Privat Bar pour le murmure social. Laissez Gamle Norge fournir la preuve publique que la joie peut aussi prendre de l'ampleur. Terminez à Christians Kjeller lorsque la soirée commence à se souvenir de son propre pouls. Et si la journée a besoin d'un redémarrage, prenez le chemin gratuit à travers Jazzbox et Jazzmine, où l'avenir est généralement moins cher et souvent plus vivant.",
"Boris Vian recommanderait probablement une trompette, une mauvaise idée et une meilleure veste. Vernon Sullivan vous dirait de rester dehors trop tard et d'appeler cela de la recherche sur le terrain. Je me contenterai d'une prescription plus calme : le Kongsberg Jazzfestival, du 1er au 4 juillet 2026, ressemble au genre de semaine qui permet à une ville de devenir plusieurs versions d'elle-même à la fois. C'est assez proche de la pataphysique pour moi, et certainement assez proche du jazz."
],
nb: [
"Det finnes folk som nærmer seg en jazzfestival med et regneark, og det finnes folk som går inn i den slik man entrer en kjeller i Paris etter midnatt: ikke for å kontrollere natten, men for å møte den ordentlig. Jeg tilhører den andre leiren, selv når den første leiren betaler fakturaene. Så ja, Kongsberg Jazzfestival 2026 fortjener praktisk planlegging. Men den fortjener også litt pataphysikk: unntakenes vitenskap, doktrinen om elskede omveier, retten til å følge en messingfrase inn i en annen gate og kalle det metode.",
"Min egen målestokk er fortsatt fransk og underjordisk. En periode bodde jeg på 23 rue Saint-Jacques, med Caveau de la Huchette nær nok til å telle som nabolagsvær. Caveau på 5 rue de la Huchette presenterer seg fortsatt som swingens tempel, åpent hele året, hver kveld, et sted hvor Paris og jazz er enige om å fortsette å danse uten å be om tillatelse. Når du først har lært å tenke på en by gjennom et slikt rom, blir enhver senere festival vurdert etter om den inneholder minst én trapp ned til en bedre stemning.",
"Kongsberg, heldigvis, forstår dette. Det offisielle 2026-programmet leser allerede som et sivilisert argument mellom polering og fare. Onsdag 1. juli åpner Samara Joy på Kongsberg Musikkteater, og Ghosted følger på Energimolla. Torsdag 2. juli tilbyr Bobo Stenson Trio, ganavya, Snarky Puppy og Mezzoforte sent på kvelden på Christians Kjeller. Fredag 3. juli går fra Wesseltoft-Andersen-Nilssen Trio og Wibutee til den større offentlige stemningen med Rotlaus og Matoma på Gamle Norge. Lørdag 4. juli avsluttes med Silya & Kongsberg Storband, Kurt Rosenwinkel, Nils Petter Molvaer og et midnattsshow med Silje Nergaard. Dette er ikke et tynt program. Dette er en by midlertidig redigert av rytme.",
"Hvis man foretrekker de mindre rommene, blir geografien enda mer interessant. Festivalens JAZZpass nevner eksplisitt Privat Bar blant de intime innendørsarenaene, samtidig som det påpeker at Gamle Norge og Christians Kjeller ligger utenfor denne pass-logikken, noe som er helt riktig: noen steder tilhører den ryddige kretsen, andre tilhører appetitten. Privat Bar, i sin egen stemme, er cocktailbar, scene, sportsbar, nattklubb og gårdsplass kalt Oasen; festivalens eget portrett av arenaen beskriver det som et sted som trives på kvalitet, service, atmosfære og folkefest. Med andre ord: ikke bare en bar, men en sosial akselerator.",
"Gamle Norge er det utendørs svaret. Festivalen beskriver Folkefestscenen / Gamle Norge som scenen for kjente navn, allsangkraft og festlig overflod, plassert mellom Gamle Norge og Trattoria Madre. Det forteller deg nesten alt du trenger å vite. Man går ikke dit for å bevise alvor. Man går dit fordi enhver seriøs festival trenger én sone hvor gaten blir til kor. Espen Lind, Rotlaus, Matoma, No. 4 og Stavangerkameratene gir den scenen sitt 2026-ansikt, og ingen av disse bookingene later som de er strenge. Bra. Festivaler trenger bredde hvis de skal føles som byer snarere enn klikker.",
"Og så er det Christians Kjeller, som betyr mer for meg enn plakatens typografi. Festivalens portrett av arenaen plasserer den inne i Opsahlgarden på Vestsida, et av byens eldre bevarte hus, restaurert og gjenåpnet som restaurant i 2001. Huset bærer både restaurant, bryggeri og kjeller-musikkpub-energi på én gang. Den kombinasjonen er moralsk sunn. Hvis Gamle Norge er det offentlige torget i sommerklær, er Christians Kjeller det lavere rommet hvor stilen løsner slipset og mener det. Mezzoforte torsdag kveld, Poesioasen med Ine Hoem og Edvard Hoem lørdag ettermiddag, og Pumpegris senere samme kveld: dette er akkurat den typen sekvensering man ønsker fra en kompakt jazzby.",
"Og så er det den demokratiske korreksjonen: gratisprogrammet. Jazzbox / Jazzboksen er fortsatt et av de beste argumentene for hele festivalen, fordi det gir studenter, semi-profesjonelle og amatører et ekte publikum snarere enn et utdanningshjørne. Festivalens offisielle notat er klart: gratis konserter hver dag, ingen reservasjon, bare møt opp, med rom for å slappe av, drive, lytte, danse og oppdage morgendagens navn før de blir brosjyreadjektiver. Jazzmine, oppført som en gratis fredagshendelse, utvider den logikken til det nye Magasinet / Fellesbrukskrysset-senteret. Enhver god festival trenger betalte monumenter; enhver stor festival trenger også soner hvor nysgjerrighet forblir rimelig.",
"Så den riktige ruten, i det minste i min private kosmologi, er ikke å velge mellom det polerte og det uregjerlige. Det er å bevege seg mellom dem. Begynn med form og lytting. La Samara Joy eller Bobo Stenson sette grammatikken. Snik deg inn på Privat Bar for den sosiale mumlingen. La Gamle Norge gi det offentlige beviset på at glede også kan skaleres. Avslutt på Christians Kjeller når kvelden begynner å huske sin egen puls. Og hvis dagen trenger en omstart, ta den gratis stien gjennom Jazzbox og Jazzmine, hvor fremtiden vanligvis er billigere og ofte mer levende.",
"Boris Vian ville sannsynligvis anbefale en trompet, en dårlig idé og en bedre jakke. Vernon Sullivan ville fortelle deg å være ute for sent og kalle det feltforskning. Jeg nøyer meg med en roligere resept: Kongsberg Jazzfestival, 1. til 4. juli 2026, ser ut som den typen uke som lar en by bli flere versjoner av seg selv samtidig. Det er nært nok pataphysikk for meg, og absolutt nært nok jazz."
],
};
export const jazzSources = [
{ label: "Kongsberg Jazzfestival programme 2026", href: "https://kongsbergjazz.no/program/" },
{ label: "Kongsberg Jazzfestival / Folkefestscenen Gamle Norge", href: "https://kongsbergjazz.no/spillesteder/folkefestscenen-gamle-norge/" },
{ label: "Kongsberg Jazzfestival / Hei Privat Bar", href: "https://kongsbergjazz.no/hei-privat-bar/" },
{ label: "Kongsberg Jazzfestival / Hei Christian Kjeller", href: "https://kongsbergjazz.no/hei-christian-kjeller/" },
{ label: "Kongsberg Jazzfestival / Jazzbox", href: "https://kongsbergjazz.no/en/faq/jazzboksen/" },
{ label: "Kongsberg Jazzfestival / Jazzmine", href: "https://kongsbergjazz.no/events/jazzmine/" },
{ label: "Caveau de la Huchette", href: "https://www.caveaudelahuchette.fr/" },
]; ];
+41 -109
View File
@@ -1,3 +1,5 @@
import type { LocaleCode } from "./locales";
export type NorwayImage = { export type NorwayImage = {
src: string; src: string;
alt: string; alt: string;
@@ -12,31 +14,28 @@ export type NorwayCase = {
date: string; date: string;
href: string; href: string;
sourceLabel: string; sourceLabel: string;
summary: string; summary: Record<LocaleCode, string>;
significance: string; significance: Record<LocaleCode, string>;
}; };
export type NorwayOrganization = { export type NorwayOrganization = {
name: string; name: string;
href: string; href: string;
strap: string; strap: Record<LocaleCode, string>;
summary: string; summary: Record<LocaleCode, string>;
}; };
export const norwayFeature = { export const norwayFeature = {
eyebrow: "Section 09 / Norway", eyebrow: { en: "Section 09 / Norway", fr: "Section 09 / Norvège", nb: "Seksjon 09 / Norge" },
title: "Family life, fathers, immigration, and the long Norwegian argument over Article 8.", title: { en: "Family life, fathers, immigration, and the long Norwegian argument over Article 8.", fr: "Vie familiale, pères, immigration et le long débat norvégien sur l'Article 8.", nb: "Familieliv, fedre, immigrasjon og den lange norske debatten om artikkel 8." },
lede: lede: { en: "This month's Norway feature sits where advocacy, rights language, and lived family conflict meet: Do Better Norge's campaign line, fathers' rights groups, immigrant-family anxiety, and the official Strasbourg record against Norway.", fr: "Le dossier de ce mois sur la Norvège se situe à l'intersection du plaidoyer, du langage des droits et des conflits familiaux vécus : la campagne de Do Better Norge, les groupes de défense des droits des pères, l'anxiété des familles immigrées et le dossier officiel de Strasbourg contre la Norvège.", nb: "Denne månedens Norge-artikkel befinner seg der aktivisme, rettighetsspråk og levde familiekonflikter møtes: Do Better Norges kampanjelinje, fedres rettighetsgrupper, immigrantfamilienes uro og den offisielle Strasbourg-protokollen mot Norge." },
"This months Norway feature sits where advocacy, rights language, and lived family conflict meet: Do Better Norges campaign line, fathers rights groups, immigrant-family anxiety, and the official Strasbourg record against Norway.", note: { en: "The argument here is deliberately two-track: advocacy claims are identified as advocacy, while the legal spine is anchored to official European Court of Human Rights material.", fr: "L'argument ici est délibérément à deux niveaux : les revendications de plaidoyer sont identifiées comme telles, tandis que la colonne vertébrale juridique est ancrée dans le matériel officiel de la Cour européenne des droits de l'homme.", nb: "Argumentet her er bevisst todelt: aktivistiske påstander identifiseres som aktivisme, mens den juridiske ryggraden er forankret i offisielt materiale fra Den europeiske menneskerettighetsdomstolen." },
note:
"The argument here is deliberately two-track: advocacy claims are identified as advocacy, while the legal spine is anchored to official European Court of Human Rights material.",
}; };
export const norwayArticle = { export const norwayArticle = {
title: "April 2026: Norway's Family-Life Problem Still Has Faces, Fathers, and a Court Record", title: "April 2026: Norway's Family-Life Problem Still Has Faces, Fathers, and a Court Record",
publishedAt: "2026-04-06 10:20:00", publishedAt: "2026-04-06 10:20:00",
excerpt: excerpt: { en: "A Norway dispatch about Do Better Norge, fathers' rights, immigrant families, and the official Article 8 case law showing why the argument over family life has not gone away.", fr: "Un reportage norvégien sur Do Better Norge, les droits des pères, les familles immigrées et la jurisprudence officielle de l'Article 8 montrant pourquoi le débat sur la vie familiale persiste.", nb: "En Norge-rapport om Do Better Norge, fedres rettigheter, immigrantfamilier og den offisielle artikkel 8-rettspraksisen som viser hvorfor debatten om familieliv ikke har forsvunnet." },
"A Norway dispatch about Do Better Norge, fathers' rights, immigrant families, and the official Article 8 case law showing why the argument over family life has not gone away.",
}; };
export const norwayImages: NorwayImage[] = [ export const norwayImages: NorwayImage[] = [
@@ -46,8 +45,7 @@ export const norwayImages: NorwayImage[] = [
credit: "Do Better Norge", credit: "Do Better Norge",
sourceUrl: "https://dobetternorge.no/images/humanRightsCollision.jpeg", sourceUrl: "https://dobetternorge.no/images/humanRightsCollision.jpeg",
sourceLabel: "Do Better Norge", sourceLabel: "Do Better Norge",
note: note: "Used to frame the article's central tension between legal standards, contact rights, and what advocacy groups say happens in practice.",
"Used to frame the article's central tension between legal standards, contact rights, and what advocacy groups say happens in practice.",
}, },
{ {
src: "https://dobetternorge.no/images/echrIntervention.jpeg", src: "https://dobetternorge.no/images/echrIntervention.jpeg",
@@ -55,8 +53,7 @@ export const norwayImages: NorwayImage[] = [
credit: "Do Better Norge", credit: "Do Better Norge",
sourceUrl: "https://dobetternorge.no/images/echrIntervention.jpeg", sourceUrl: "https://dobetternorge.no/images/echrIntervention.jpeg",
sourceLabel: "Do Better Norge", sourceLabel: "Do Better Norge",
note: note: "Used as a visual bridge between Strasbourg case law and Norway's domestic reunification debate.",
"Used as a visual bridge between Strasbourg case law and Norway's domestic reunification debate.",
}, },
{ {
src: "https://dobetternorge.no/uploads/infographics/norwayLaw-vs-ECHR.png", src: "https://dobetternorge.no/uploads/infographics/norwayLaw-vs-ECHR.png",
@@ -64,8 +61,7 @@ export const norwayImages: NorwayImage[] = [
credit: "Do Better Norge", credit: "Do Better Norge",
sourceUrl: "https://dobetternorge.no/uploads/infographics/norwayLaw-vs-ECHR.png", sourceUrl: "https://dobetternorge.no/uploads/infographics/norwayLaw-vs-ECHR.png",
sourceLabel: "Do Better Norge", sourceLabel: "Do Better Norge",
note: note: "Useful for the article's law-versus-practice section because it visualizes the reunification versus stability conflict directly.",
"Useful for the article's law-versus-practice section because it visualizes the reunification versus stability conflict directly.",
}, },
{ {
src: "https://dobetternorge.no/images/theBlackBox.jpeg", src: "https://dobetternorge.no/images/theBlackBox.jpeg",
@@ -73,8 +69,7 @@ export const norwayImages: NorwayImage[] = [
credit: "Do Better Norge", credit: "Do Better Norge",
sourceUrl: "https://dobetternorge.no/images/theBlackBox.jpeg", sourceUrl: "https://dobetternorge.no/images/theBlackBox.jpeg",
sourceLabel: "Do Better Norge", sourceLabel: "Do Better Norge",
note: note: "Selected for the immigrant-family section because it explicitly depicts cultural blindness and interpretive bias as advocacy concerns.",
"Selected for the immigrant-family section because it explicitly depicts cultural blindness and interpretive bias as advocacy concerns.",
}, },
]; ];
@@ -84,50 +79,40 @@ export const norwayCases: NorwayCase[] = [
date: "10 September 2019", date: "10 September 2019",
href: "https://www.echr.coe.int/w/strand-lobben-and-others-v.-norway-no.-37283/13-", href: "https://www.echr.coe.int/w/strand-lobben-and-others-v.-norway-no.-37283/13-",
sourceLabel: "ECHR case page", sourceLabel: "ECHR case page",
summary: summary: { en: "Grand Chamber case concerning forced adoption and the severing of legal ties between mother and child.", fr: "Affaire de la Grande Chambre concernant l'adoption forcée et la rupture des liens juridiques entre une mère et son enfant.", nb: "Storkammer-sak om tvangsadopsjon og brudd på juridiske bånd mellom mor og barn." },
"Grand Chamber case concerning forced adoption and the severing of legal ties between mother and child.", significance: { en: "The core signal was that reunification must remain the ultimate aim where possible and that permanently cutting family ties demands exceptional justification.", fr: "Le signal clé était que la réunification doit rester l'objectif ultime lorsque cela est possible, et que couper définitivement les liens familiaux exige une justification exceptionnelle.", nb: "Kjernesignalet var at gjenforening må forbli det ultimate målet der det er mulig, og at permanent brudd på familiebånd krever en ekstraordinær begrunnelse." },
significance:
"The core signal was that reunification must remain the ultimate aim where possible and that permanently cutting family ties demands exceptional justification.",
}, },
{ {
title: "K.O. and V.M. v. Norway", title: "K.O. and V.M. v. Norway",
date: "19 November 2019", date: "19 November 2019",
href: "https://hudoc.echr.coe.int/app/conversion/pdf?filename=Judgment+K.O.+and+V.M.+v.+Norway+-+placement+in+foster+care+and+contact+rights.pdf&id=003-6566150-8690597&library=ECHR", href: "https://hudoc.echr.coe.int/app/conversion/pdf?filename=Judgment+K.O.+and+V.M.+v.+Norway+-+placement+in+foster+care+and+contact+rights.pdf&id=003-6566150-8690597&library=ECHR",
sourceLabel: "ECHR press release", sourceLabel: "ECHR press release",
summary: summary: { en: "The Court found no Article 8 violation in the placement itself, but did find a violation in the parents' contact regime.", fr: "La Cour n'a pas trouvé de violation de l'Article 8 dans le placement lui-même, mais a constaté une violation dans le régime de contact des parents.", nb: "Domstolen fant ingen brudd på artikkel 8 i selve plasseringen, men fant brudd i foreldrenes kontaktregime." },
"The Court found no Article 8 violation in the placement itself, but did find a violation in the parents' contact regime.", significance: { en: "That split matters: the recurring Norway problem is often not only removal, but low-contact decisions that let separation harden into estrangement.", fr: "Cette distinction est importante : le problème récurrent en Norvège concerne souvent non seulement le retrait, mais aussi les décisions de faible contact qui laissent la séparation se transformer en éloignement.", nb: "Den splittelsen er viktig: det tilbakevendende Norge-problemet handler ofte ikke bare om fjerning, men om lavkontaktavgjørelser som lar separasjon bli til fremmedgjøring." },
significance:
"That split matters: the recurring Norway problem is often not only removal, but low-contact decisions that let separation harden into estrangement.",
}, },
{ {
title: "Abdi Ibrahim v. Norway", title: "Abdi Ibrahim v. Norway",
date: "10 December 2021", date: "10 December 2021",
href: "https://www.echr.coe.int/w/abdi-ibrahim-v.-norway-no.-15379/16-", href: "https://www.echr.coe.int/w/abdi-ibrahim-v.-norway-no.-15379/16-",
sourceLabel: "ECHR case page", sourceLabel: "ECHR case page",
summary: summary: { en: "Grand Chamber case about adoption by a foster family against the wishes of a Somali Muslim mother.", fr: "Affaire de la Grande Chambre concernant l'adoption par une famille d'accueil contre les souhaits d'une mère somalienne musulmane.", nb: "Storkammer-sak om adopsjon av en fosterfamilie mot ønskene til en somalisk muslimsk mor." },
"Grand Chamber case about adoption by a foster family against the wishes of a Somali Muslim mother.", significance: { en: "The Court held that Norway had not given sufficient weight to the family's cultural and religious background, making this one of the clearest immigrant-family Article 8 judgments.", fr: "La Cour a estimé que la Norvège n'avait pas accordé suffisamment de poids au contexte culturel et religieux de la famille, ce qui en fait l'un des jugements les plus clairs concernant les familles immigrées dans le cadre de l'Article 8.", nb: "Domstolen mente at Norge ikke hadde gitt tilstrekkelig vekt til familiens kulturelle og religiøse bakgrunn, noe som gjør dette til en av de tydeligste immigrantfamilie-dommene under artikkel 8." },
significance:
"The Court held that Norway had not given sufficient weight to the family's cultural and religious background, making this one of the clearest immigrant-family Article 8 judgments.",
}, },
{ {
title: "A.L. and Others v. Norway", title: "A.L. and Others v. Norway",
date: "20 January 2022", date: "20 January 2022",
href: "https://hudoc.echr.coe.int/app/conversion/pdf/?filename=Two+judgments+concerning+care+orders+for+children+in+Norway.pdf&id=003-7235635-9843609&library=ECHR", href: "https://hudoc.echr.coe.int/app/conversion/pdf/?filename=Two+judgments+concerning+care+orders+for+children+in+Norway.pdf&id=003-7235635-9843609&library=ECHR",
sourceLabel: "ECHR press release", sourceLabel: "ECHR press release",
summary: summary: { en: "Case involving Norwegian and Slovak family members and the handling of a care order plus restricted parental contact.", fr: "Affaire impliquant des membres de familles norvégiennes et slovaques et la gestion d'une ordonnance de prise en charge avec un contact parental restreint.", nb: "Sak som involverer norske og slovakiske familiemedlemmer og håndtering av en omsorgsordre med begrenset foreldrekontakt." },
"Case involving Norwegian and Slovak family members and the handling of a care order plus restricted parental contact.", significance: { en: "The Court found that the child had effectively been set on a foster-care trajectory without proper consideration of alternatives or genuine reconciliation work.", fr: "La Cour a constaté que l'enfant avait été effectivement placé sur une trajectoire de placement en famille d'accueil sans considération adéquate des alternatives ou de véritables efforts de réconciliation.", nb: "Domstolen fant at barnet i praksis var satt på en fosterhjemsbane uten tilstrekkelig vurdering av alternativer eller genuint forsoningsarbeid." },
significance:
"The Court found that the child had effectively been set on a foster-care trajectory without proper consideration of alternatives or genuine reconciliation work.",
}, },
{ {
title: "Country profile: Norway parental-rights line", title: "Country profile: Norway parental-rights line",
date: "Updated July 2024", date: "Updated July 2024",
href: "https://www.echr.coe.int/documents/d/echr/CP_Norway_ENG", href: "https://www.echr.coe.int/documents/d/echr/CP_Norway_ENG",
sourceLabel: "ECHR country profile", sourceLabel: "ECHR country profile",
summary: summary: { en: "The Court's official Norway profile groups together child-welfare, parental-rights, and immigration-family-life cases.", fr: "Le profil officiel de la Norvège par la Cour regroupe les affaires concernant le bien-être des enfants, les droits parentaux et la vie familiale des immigrés.", nb: "Domstolens offisielle Norge-profil grupperer saker om barnevern, foreldrerettigheter og immigrantfamilieliv." },
"The Court's official Norway profile groups together child-welfare, parental-rights, and immigration-family-life cases.", significance: { en: "It records that on 14 September 2023 the Court dealt with 21 Norway public-care applications, declaring 12 inadmissible and finding Article 8 violations in nine others.", fr: "Il enregistre que, le 14 septembre 2023, la Cour a traité 21 demandes concernant la prise en charge publique en Norvège, déclarant 12 irrecevables et constatant des violations de l'Article 8 dans neuf autres.", nb: "Den registrerer at den 14. september 2023 behandlet domstolen 21 norske offentlige omsorgssøknader, erklærte 12 som inadmissible og fant brudd på artikkel 8 i ni andre." },
significance:
"It records that on 14 September 2023 the Court dealt with 21 Norway public-care applications, declaring 12 inadmissible and finding Article 8 violations in nine others.",
}, },
]; ];
@@ -135,85 +120,32 @@ export const norwayOrganizations: NorwayOrganization[] = [
{ {
name: "Do Better Norge", name: "Do Better Norge",
href: "https://dobetternorge.no/", href: "https://dobetternorge.no/",
strap: "rights education, reform pressure, and family-life advocacy", strap: { en: "rights education, reform pressure, and family-life advocacy", fr: "éducation aux droits, pression pour la réforme et plaidoyer pour la vie familiale", nb: "rettighetsopplæring, reformpress og familielivsaktivisme" },
summary: summary: { en: "The site presents itself as a practical route for families navigating custody, contact, child-welfare process, and Article 8 questions, with guides, infographics, videos, and reform arguments.", fr: "Le site se présente comme une voie pratique pour les familles naviguant dans les questions de garde, de contact, de processus de protection de l'enfance et de l'Article 8, avec des guides, des infographies, des vidéos et des arguments pour la réforme.", nb: "Nettstedet presenterer seg som en praktisk vei for familier som navigerer i spørsmål om omsorg, kontakt, barnevernsprosesser og artikkel 8, med guider, infografikk, videoer og reformargumenter." },
"The site presents itself as a practical route for families navigating custody, contact, child-welfare process, and Article 8 questions, with guides, infographics, videos, and reform arguments.",
}, },
{ {
name: "MannsForum", name: "MannsForum",
href: "https://mannsforum.no/kontaktinformasjon-mannsforum/", href: "https://mannsforum.no/kontaktinformasjon-mannsforum/",
strap: "boys, fathers, and men's equality voice", strap: { en: "boys, fathers, and men's equality voice", fr: "voix pour l'égalité des garçons, des pères et des hommes", nb: "gutters, fedres og menns likestillingsstemme" },
summary: summary: { en: "MannsForum describes itself as Norway's largest membership-based equality organization focused on boys, fathers, and men, which makes it a natural bridge into the fathers' rights side of the debate.", fr: "MannsForum se décrit comme la plus grande organisation d'égalité basée sur l'adhésion en Norvège, axée sur les garçons, les pères et les hommes, ce qui en fait un pont naturel vers le côté des droits des pères du débat.", nb: "MannsForum beskriver seg selv som Norges største medlemsbaserte likestillingsorganisasjon fokusert på gutter, fedre og menn, noe som gjør det til en naturlig bro inn i fedres rettighetssiden av debatten." },
"MannsForum describes itself as Norway's largest membership-based equality organization focused on boys, fathers, and men, which makes it a natural bridge into the fathers' rights side of the debate.",
}, },
{ {
name: "Foreningen 2 Foreldre", name: "Foreningen 2 Foreldre",
href: "https://www.f2f.no/", href: "https://www.f2f.no/",
strap: "children with two homes, shared-parenting culture", strap: { en: "children with two homes, shared-parenting culture", fr: "enfants avec deux foyers, culture de co-parentalité", nb: "barn med to hjem, delt foreldrekultur" },
summary: summary: { en: "F2F's own framing is concise and revealing: safe family conditions for children with two homes, which is almost a thesis statement for the contact-and-continuity side of the article.", fr: "La propre présentation de F2F est concise et révélatrice : des conditions familiales sûres pour les enfants avec deux foyers, ce qui est presque une déclaration de thèse pour le côté contact et continuité de l'article.", nb: "F2Fs egen formulering er kortfattet og avslørende: trygge familieforhold for barn med to hjem, som nesten er en teseerklæring for kontakt- og kontinuitetssiden av artikkelen." },
"F2F's own framing is concise and revealing: safe family conditions for children with two homes, which is almost a thesis statement for the contact-and-continuity side of the article.",
}, },
]; ];
export const norwaySources = [ export const norwaySources = [
{ { label: "ECHR country profile for Norway", href: "https://www.echr.coe.int/documents/d/echr/CP_Norway_ENG", note: "Used for the official summary of Norway parental-rights and immigration-family-life judgments, including the 2023 line on 21 public-care applications." },
label: "ECHR country profile for Norway", { label: "ECHR case page: Strand Lobben and Others v. Norway", href: "https://www.echr.coe.int/w/strand-lobben-and-others-v.-norway-no.-37283/13-", note: "Used for the Grand Chamber adoption and reunification reference point that reshaped the Norway debate." },
href: "https://www.echr.coe.int/documents/d/echr/CP_Norway_ENG", { label: "ECHR case page: Abdi Ibrahim v. Norway", href: "https://www.echr.coe.int/w/abdi-ibrahim-v.-norway-no.-15379/16-", note: "Used for the cultural and religious identity dimension in foster-care and adoption decisions." },
note: { label: "ECHR press release: K.O. and V.M. v. Norway", href: "https://hudoc.echr.coe.int/app/conversion/pdf?filename=Judgment+K.O.+and+V.M.+v.+Norway+-+placement+in+foster+care+and+contact+rights.pdf&id=003-6566150-8690597&library=ECHR", note: "Used for the contact-rights holding and the distinction between the placement decision and the visitation regime." },
"Used for the official summary of Norway parental-rights and immigration-family-life judgments, including the 2023 line on 21 public-care applications.", { label: "ECHR press release: A.L. and Others v. Norway / E.M. and Others v. Norway", href: "https://hudoc.echr.coe.int/app/conversion/pdf/?filename=Two+judgments+concerning+care+orders+for+children+in+Norway.pdf&id=003-7235635-9843609&library=ECHR", note: "Used for the 2022 family-reconciliation and long-term foster-care analysis." },
}, { label: "ECHR press release: 21 applications against Norway concerning children taken into public care", href: "https://hudoc.echr.coe.int/app/conversion/pdf/?filename=21+applications+against+Norway+concerning+children+taken+into+public+care.pdf&id=003-7744629-10718629&library=ECHR", note: "Used for the official 2023 aggregation of admissibility outcomes and Article 8 violations against Norway." },
{ { label: "ECHR factsheet: Parental rights", href: "https://www.echr.coe.int/Documents/FS_Parental_ENG.pdf", note: "Used as the official thematic overview linking Norway decisions into the wider Article 8 parental-rights doctrine." },
label: "ECHR case page: Strand Lobben and Others v. Norway", { label: "Do Better Norge homepage", href: "https://dobetternorge.no/", note: "Used for the site's self-description, emphasis on Article 8, custody, child welfare, and reform work." },
href: "https://www.echr.coe.int/w/strand-lobben-and-others-v.-norway-no.-37283/13-", { label: "MannsForum contact/about page", href: "https://mannsforum.no/kontaktinformasjon-mannsforum/", note: "Used for MannsForum's own description of itself as an organization focused on boys, fathers, and men." },
note: { label: "Foreningen 2 Foreldre homepage", href: "https://www.f2f.no/", note: "Used for F2F's own framing of children with two homes and shared-family stability." },
"Used for the Grand Chamber adoption and reunification reference point that reshaped the Norway debate.",
},
{
label: "ECHR case page: Abdi Ibrahim v. Norway",
href: "https://www.echr.coe.int/w/abdi-ibrahim-v.-norway-no.-15379/16-",
note:
"Used for the cultural and religious identity dimension in foster-care and adoption decisions.",
},
{
label: "ECHR press release: K.O. and V.M. v. Norway",
href: "https://hudoc.echr.coe.int/app/conversion/pdf?filename=Judgment+K.O.+and+V.M.+v.+Norway+-+placement+in+foster+care+and+contact+rights.pdf&id=003-6566150-8690597&library=ECHR",
note:
"Used for the contact-rights holding and the distinction between the placement decision and the visitation regime.",
},
{
label: "ECHR press release: A.L. and Others v. Norway / E.M. and Others v. Norway",
href: "https://hudoc.echr.coe.int/app/conversion/pdf/?filename=Two+judgments+concerning+care+orders+for+children+in+Norway.pdf&id=003-7235635-9843609&library=ECHR",
note:
"Used for the 2022 family-reconciliation and long-term foster-care analysis.",
},
{
label: "ECHR press release: 21 applications against Norway concerning children taken into public care",
href: "https://hudoc.echr.coe.int/app/conversion/pdf/?filename=21+applications+against+Norway+concerning+children+taken+into+public+care.pdf&id=003-7744629-10718629&library=ECHR",
note:
"Used for the official 2023 aggregation of admissibility outcomes and Article 8 violations against Norway.",
},
{
label: "ECHR factsheet: Parental rights",
href: "https://www.echr.coe.int/Documents/FS_Parental_ENG.pdf",
note:
"Used as the official thematic overview linking Norway decisions into the wider Article 8 parental-rights doctrine.",
},
{
label: "Do Better Norge homepage",
href: "https://dobetternorge.no/",
note:
"Used for the site's self-description, emphasis on Article 8, custody, child welfare, and reform work.",
},
{
label: "MannsForum contact/about page",
href: "https://mannsforum.no/kontaktinformasjon-mannsforum/",
note:
"Used for MannsForum's own description of itself as an organization focused on boys, fathers, and men.",
},
{
label: "Foreningen 2 Foreldre homepage",
href: "https://www.f2f.no/",
note:
"Used for F2F's own framing of children with two homes and shared-family stability.",
},
]; ];
+66 -120
View File
@@ -1,6 +1,8 @@
import type { LocaleCode } from "./locales";
export type RouteStop = { export type RouteStop = {
place: string; place: string;
note: string; note: Record<LocaleCode, string>;
}; };
export type SourceCredit = { export type SourceCredit = {
@@ -16,16 +18,16 @@ export type Venture = {
name: string; name: string;
label: string; label: string;
location: string; location: string;
summary: string; summary: Record<LocaleCode, string>;
detail: string; detail: Record<LocaleCode, string>;
highlights: string[]; highlights: Record<LocaleCode, string[]>;
source: SourceCredit; source: SourceCredit;
}; };
export type VentureSignal = { export type VentureSignal = {
name: string; name: string;
strap: string; strap: Record<LocaleCode, string>;
summary: string; summary: Record<LocaleCode, string>;
href: string; href: string;
imageSrc: string; imageSrc: string;
imageAlt: string; imageAlt: string;
@@ -36,12 +38,12 @@ export type VentureSignal = {
export type CollageImage = { export type CollageImage = {
src: string; src: string;
alt: string; alt: string;
caption: string; caption: Record<LocaleCode, string>;
}; };
export type AILabCapability = { export type AILabCapability = {
title: string; title: Record<LocaleCode, string>;
summary: string; summary: Record<LocaleCode, string>;
}; };
export type AILabProgramme = { export type AILabProgramme = {
@@ -51,20 +53,20 @@ export type AILabProgramme = {
}; };
export const routeStops: RouteStop[] = [ export const routeStops: RouteStop[] = [
{ place: "Ringwood, New Jersey", note: "born" }, { place: "Ringwood, New Jersey", note: { en: "born", fr: "naissance", nb: "født" } },
{ place: "Villanova, Pennsylvania", note: "undergraduate chapter" }, { place: "Villanova, Pennsylvania", note: { en: "undergraduate chapter", fr: "chapitre universitaire", nb: "grunnleggende kapittel" } },
{ place: "Brussels, Belgium", note: "trade and multilingual weather" }, { place: "Brussels, Belgium", note: { en: "trade and multilingual weather", fr: "commerce et météo multilingue", nb: "handel og flerspråklig vær" } },
{ place: "New Jersey", note: "return passage" }, { place: "New Jersey", note: { en: "return passage", fr: "retour", nb: "returpassasje" } },
{ place: "Columbia, South Carolina", note: "business school orbit" }, { place: "Columbia, South Carolina", note: { en: "business school orbit", fr: "orbite de l'école de commerce", nb: "handelshøyskolebane" } },
{ place: "Paris, France", note: "international MBA" }, { place: "Paris, France", note: { en: "international MBA", fr: "MBA international", nb: "internasjonal MBA" } },
{ place: "Washington, DC", note: "capital interval" }, { place: "Washington, DC", note: { en: "capital interval", fr: "intervalle capital", nb: "hovedstadsintervall" } },
{ place: "Manhattan, New York", note: "city tempo" }, { place: "Manhattan, New York", note: { en: "city tempo", fr: "tempo urbain", nb: "bytempo" } },
{ place: "Hamilton, Bermuda", note: "Atlantic detour" }, { place: "Hamilton, Bermuda", note: { en: "Atlantic detour", fr: "détour atlantique", nb: "atlantisk omvei" } },
{ place: "Washington, DC", note: "federal coda" }, { place: "Washington, DC", note: { en: "federal coda", fr: "coda fédérale", nb: "føderal koda" } },
{ place: "Brooklyn, New York", note: "borough voltage" }, { place: "Brooklyn, New York", note: { en: "borough voltage", fr: "voltage de quartier", nb: "bydelsspenning" } },
{ place: "Krakow, Poland", note: "EU studies" }, { place: "Krakow, Poland", note: { en: "EU studies", fr: "études européennes", nb: "EU-studier" } },
{ place: "Oslo, Norway", note: "Nordic transition" }, { place: "Oslo, Norway", note: { en: "Nordic transition", fr: "transition nordique", nb: "nordisk overgang" } },
{ place: "Kongsberg, Norway", note: "current desk" }, { place: "Kongsberg, Norway", note: { en: "current desk", fr: "bureau actuel", nb: "nåværende skrivebord" } },
]; ];
export const ventureDesk: Venture[] = [ export const ventureDesk: Venture[] = [
@@ -75,20 +77,13 @@ export const ventureDesk: Venture[] = [
name: "Gilligan TECH ENK", name: "Gilligan TECH ENK",
label: "Norwegian ENK", label: "Norwegian ENK",
location: "Kongsberg, Norway", location: "Kongsberg, Norway",
summary: summary: { en: "Local AI systems consulting for Nordic businesses, with strategy first, delivery second, and infrastructure only when it materially improves the result.", fr: "Consultation en systèmes d'IA locaux pour les entreprises nordiques, avec la stratégie d'abord, la mise en œuvre ensuite, et l'infrastructure uniquement si elle améliore réellement le résultat.", nb: "Konsulenttjenester for lokale AI-systemer for nordiske bedrifter, med strategi først, levering deretter, og infrastruktur kun når det gir en materiell forbedring." },
"Local AI systems consulting for Nordic businesses, with strategy first, delivery second, and infrastructure only when it materially improves the result.", detail: { en: "The Gilligan Tech line is practical and operator-minded: architectural audits, system builds, and fractional CTO guidance for Nordic SMBs that want useful AI without rented buzzwords.", fr: "La ligne Gilligan TECH est pratique et orientée opérateurs : audits architecturaux, constructions de systèmes et conseils CTO fractionnés pour les PME nordiques qui souhaitent une IA utile sans dépendre des mots à la mode.", nb: "Gilligan TECH-linjen er praktisk og operatørfokusert: arkitektoniske revisjoner, systembygging og delt CTO-veiledning for nordiske SMB-er som ønsker nyttig AI uten leide buzzwords." },
detail: highlights: { en: ["AI Dispatch: fixed-fee architectural audit and strike map","Systems Forge: custom RAG, AI portals, and transcription services","Systems Command: fractional CTO support and board-ready AI strategy"], fr: ["AI Dispatch : audit architectural à tarif fixe et carte d'intervention","Systems Forge : RAG personnalisé, portails IA et services de transcription","Systems Command : support CTO fractionné et stratégie IA prête pour le conseil d'administration"], nb: ["AI Dispatch: fastpris arkitektonisk revisjon og handlingskart","Systems Forge: tilpasset RAG, AI-portaler og transkripsjonstjenester","Systems Command: delt CTO-støtte og styreklar AI-strategi"] },
"The Gilligan Tech line is practical and operator-minded: architectural audits, system builds, and fractional CTO guidance for Nordic SMBs that want useful AI without rented buzzwords.",
highlights: [
"AI Dispatch: fixed-fee architectural audit and strike map",
"Systems Forge: custom RAG, AI portals, and transcription services",
"Systems Command: fractional CTO support and board-ready AI strategy",
],
source: { source: {
label: "Gilligan Tech", label: "Gilligan Tech",
url: "https://gilligan.tech/", url: "https://gilligan.tech/",
note: note: "Paraphrased from the official Gilligan Tech site, including the AI Dispatch, Systems Forge, and Systems Command descriptions.",
"Paraphrased from the official Gilligan Tech site, including the AI Dispatch, Systems Forge, and Systems Command descriptions.",
}, },
}, },
{ {
@@ -98,20 +93,13 @@ export const ventureDesk: Venture[] = [
name: "Blue Note Logic Inc", name: "Blue Note Logic Inc",
label: "AI / IT Lab", label: "AI / IT Lab",
location: "Philadelphia to Paris, with EU infrastructure", location: "Philadelphia to Paris, with EU infrastructure",
summary: summary: { en: "Private AI platforms, document intelligence, and production infrastructure designed to turn working knowledge into owned systems rather than vendor dependency.", fr: "Plateformes d'IA privées, intelligence documentaire et infrastructure de production conçues pour transformer le savoir-faire en systèmes propriétaires plutôt qu'en dépendance aux fournisseurs.", nb: "Private AI-plattformer, dokumentintelligens og produksjonsinfrastruktur designet for å gjøre arbeidskunnskap til egne systemer i stedet for leverandøravhengighet." },
"Private AI platforms, document intelligence, and production infrastructure designed to turn working knowledge into owned systems rather than vendor dependency.", detail: { en: "Blue Note Logic is the lab, forge, and infrastructure house behind the AI side of the publication: consultancy, sovereign model work, corpus design, and productized document intelligence with cited answers and European hosting.", fr: "Blue Note Logic est le laboratoire, la forge et la maison d'infrastructure derrière le volet IA de la publication : conseil, travail sur des modèles souverains, conception de corpus et intelligence documentaire produit avec des réponses citées et un hébergement européen.", nb: "Blue Note Logic er laboratoriet, smia og infrastrukturhuset bak AI-delen av publikasjonen: konsulenttjenester, arbeid med suverene modeller, korpusdesign og produktifisert dokumentintelligens med siterte svar og europeisk hosting." },
detail: highlights: { en: ["CorpusAI and CaveauAI for private, source-cited retrieval","Knowledge corpus design, document intelligence, and deployment strategy","Sovereign fine-tuning paths built around owned data and owned outcomes"], fr: ["CorpusAI et CaveauAI pour une récupération privée et sourcée","Conception de corpus de connaissances, intelligence documentaire et stratégie de déploiement","Chemins de fine-tuning souverains basés sur des données et des résultats propriétaires"], nb: ["CorpusAI og CaveauAI for privat, kildebasert gjenfinning","Korpusdesign, dokumentintelligens og distribusjonsstrategi","Suverene finjusteringsveier bygget rundt egne data og egne resultater"] },
"Blue Note Logic is the lab, forge, and infrastructure house behind the AI side of the publication: consultancy, sovereign model work, corpus design, and productized document intelligence with cited answers and European hosting.",
highlights: [
"CorpusAI and CaveauAI for private, source-cited retrieval",
"Knowledge corpus design, document intelligence, and deployment strategy",
"Sovereign fine-tuning paths built around owned data and owned outcomes",
],
source: { source: {
label: "Blue Note Logic official sites", label: "Blue Note Logic official sites",
url: "https://ai.bluenotelogic.com/", url: "https://bluenotelogic.com",
note: note: "Paraphrased from Blue Note Logic and CorpusAI official pages covering private AI, document intelligence, and owned infrastructure.",
"Paraphrased from Blue Note Logic and CorpusAI official pages covering private AI, document intelligence, and owned infrastructure.",
}, },
}, },
]; ];
@@ -119,32 +107,28 @@ export const ventureDesk: Venture[] = [
export const ventureSignals: VentureSignal[] = [ export const ventureSignals: VentureSignal[] = [
{ {
name: "Blue Note Logic", name: "Blue Note Logic",
strap: "private AI, document intelligence, and source-cited memory", strap: { en: "private AI, document intelligence, and source-cited memory", fr: "IA privée, intelligence documentaire et mémoire sourcée", nb: "privat AI, dokumentintelligens og kildebasert hukommelse" },
summary: summary: { en: "The machine room behind the paper: owned infrastructure, private corpora, multilingual controls, and AI that keeps its receipts.", fr: "La salle des machines derrière le papier : infrastructure propriétaire, corpus privés, contrôles multilingues et IA qui garde ses preuves.", nb: "Maskinrommet bak papiret: egen infrastruktur, private korpus, flerspråklige kontroller og AI som holder kvitteringene sine." },
"The machine room behind the paper: owned infrastructure, private corpora, multilingual controls, and AI that keeps its receipts.", href: "https://bluenotelogic.com",
href: "https://ai.bluenotelogic.com/", imageSrc: "/images/ai-lab/bnl-home-card.jpg",
imageSrc: "/images/ai-lab/hero-lab.svg", imageAlt: "Blue Note Logic — private AI server room, jazz-cellar atmosphere.",
imageAlt: "Illustrated AI lab diagram for Blue Note Logic.",
imageNote: "Private corpus / cited answers / EU hosting", imageNote: "Private corpus / cited answers / EU hosting",
external: true, external: true,
}, },
{ {
name: "Trivia & Tunes", name: "Trivia & Tunes",
strap: "live-hosted games, music rounds, and true AI in the loop", strap: { en: "live-hosted games, music rounds, and true AI in the loop", fr: "jeux animés en direct, tours musicaux et vraie IA en boucle", nb: "live-vertede spill, musikkrunder og ekte AI i loopen" },
summary: summary: { en: "A culture product with venue instincts: playlists, game craft, AI grading, and voice features edging toward the microphone.", fr: "Un produit culturel avec des instincts de lieu : playlists, conception de jeux, évaluation par IA et fonctionnalités vocales qui s'approchent du micro.", nb: "Et kulturprodukt med stedssans: spillelister, spilldesign, AI-evaluering og stemmefunksjoner som nærmer seg mikrofonen." },
"A culture product with venue instincts: playlists, game craft, AI grading, and voice features edging toward the microphone.",
href: "https://triviaandtunes.no/", href: "https://triviaandtunes.no/",
imageSrc: imageSrc: "/images/projects/trivia-venue.jpg",
"https://commons.wikimedia.org/wiki/Special:Redirect/file/Level_42_Kongsberg_Jazzfestival_2017_%28214257%29.jpg", imageAlt: "Warmly lit Scandinavian pub interior during quiz night.",
imageAlt: "Crowd-facing stage image from Kongsberg Jazzfestival.",
imageNote: "Live rooms / music energy / quiz-night voltage", imageNote: "Live rooms / music energy / quiz-night voltage",
external: true, external: true,
}, },
{ {
name: "Do Better Norge", name: "Do Better Norge",
strap: "children's rights, due process, and family life protections", strap: { en: "children's rights, due process, and family life protections", fr: "droits des enfants, procédure équitable et protection de la vie familiale", nb: "barns rettigheter, rettferdig prosess og beskyttelse av familieliv" },
summary: summary: { en: "A civic and advocacy desk grounded in primary sources, practical guidance, and a refusal to treat children as procedural debris.", fr: "Un bureau civique et de plaidoyer ancré dans les sources primaires, des conseils pratiques et un refus de traiter les enfants comme des débris procéduraux.", nb: "En samfunns- og advokatdesk forankret i primærkilder, praktisk veiledning og en avvisning av å behandle barn som prosedyremessig avfall." },
"A civic and advocacy desk grounded in primary sources, practical guidance, and a refusal to treat children as procedural debris.",
href: "https://dobetternorge.no/", href: "https://dobetternorge.no/",
imageSrc: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Kongsberg_IMG_0357.JPG", imageSrc: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Kongsberg_IMG_0357.JPG",
imageAlt: "Riverfront view in Kongsberg, Norway.", imageAlt: "Riverfront view in Kongsberg, Norway.",
@@ -153,12 +137,10 @@ export const ventureSignals: VentureSignal[] = [
}, },
{ {
name: "School dossier", name: "School dossier",
strap: "Brussels, Krakow, Paris, Villanova, and the long route north", strap: { en: "Brussels, Krakow, Paris, Villanova, and the long route north", fr: "Bruxelles, Cracovie, Paris, Villanova et la longue route vers le nord", nb: "Brussel, Krakow, Paris, Villanova og den lange veien nordover" },
summary: summary: { en: "The education issue turns institutions into chapters, cities into footnotes, and degrees into a proper migration story.", fr: "Le numéro éducation transforme les institutions en chapitres, les villes en notes de bas de page et les diplômes en une véritable histoire de migration.", nb: "Utdanningsutgaven gjør institusjoner til kapitler, byer til fotnoter og grader til en ordentlig migrasjonshistorie." },
"The education issue turns institutions into chapters, cities into footnotes, and degrees into a proper migration story.",
href: "/education", href: "/education",
imageSrc: imageSrc: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Grand-Place,_Brussels_%2839528722772%29.jpg",
"https://commons.wikimedia.org/wiki/Special:Redirect/file/Grand-Place,_Brussels_%2839528722772%29.jpg",
imageAlt: "Grand-Place in Brussels.", imageAlt: "Grand-Place in Brussels.",
imageNote: "Brussels / multilingual weather / first European chapter", imageNote: "Brussels / multilingual weather / first European chapter",
external: false, external: false,
@@ -169,43 +151,39 @@ export const collageImages: CollageImage[] = [
{ {
src: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Villanova_University_A_panoramic_shot.jpg", src: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Villanova_University_A_panoramic_shot.jpg",
alt: "Panoramic view of Villanova University.", alt: "Panoramic view of Villanova University.",
caption: "Villanova / the first brass section", caption: { en: "Villanova / the first brass section", fr: "Villanova / la première section de cuivres", nb: "Villanova / den første messingseksjonen" },
}, },
{ {
src: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Jagiellonian_University_Collegium_Novum%2C_1882_designed_by_Feliks_Ksi%C4%99%C5%BCarski%2C_24_Go%C5%82%C4%99bia_street%2C_Old_Town%2C_Krak%C3%B3w%2C_Poland_%284%29.jpg", src: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Jagiellonian_University_Collegium_Novum%2C_1882_designed_by_Feliks_Ksi%C4%99%C5%BCarski%2C_24_Go%C5%82%C4%99bia_street%2C_Old_Town%2C_Krak%C3%B3w%2C_Poland_%284%29.jpg",
alt: "Collegium Novum at Jagiellonian University in Krakow.", alt: "Collegium Novum at Jagiellonian University in Krakow.",
caption: "Krakow / bureaucracy and stone", caption: { en: "Krakow / bureaucracy and stone", fr: "Cracovie / bureaucratie et pierre", nb: "Krakow / byråkrati og stein" },
}, },
{ {
src: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Kongsberg_IMG_0357.JPG", src: "https://commons.wikimedia.org/wiki/Special:Redirect/file/Kongsberg_IMG_0357.JPG",
alt: "Kongsberg riverfront in Norway.", alt: "Kongsberg riverfront in Norway.",
caption: "Kongsberg / present tense", caption: { en: "Kongsberg / present tense", fr: "Kongsberg / présent immédiat", nb: "Kongsberg / nåtid" },
}, },
]; ];
export const aiLabCapabilities: AILabCapability[] = [ export const aiLabCapabilities: AILabCapability[] = [
{ {
title: "Private AI on your own corpus", title: { en: "Private AI on your own corpus", fr: "IA privée sur votre propre corpus", nb: "Privat AI på ditt eget korpus" },
summary: summary: { en: "Document intelligence built around a private corpus, with source-cited answers and search modes that stay anchored to the evidence.", fr: "Intelligence documentaire construite autour d'un corpus privé, avec des réponses sourcées et des modes de recherche ancrés dans les preuves.", nb: "Dokumentintelligens bygget rundt et privat korpus, med kildebaserte svar og søkemoduser som forblir forankret i bevisene." },
"Document intelligence built around a private corpus, with source-cited answers and search modes that stay anchored to the evidence.",
}, },
{ {
title: "European infrastructure by design", title: { en: "European infrastructure by design", fr: "Infrastructure européenne par conception", nb: "Europeisk infrastruktur som standard" },
summary: summary: { en: "Dedicated European hosting, isolated telemetry, and a security posture built to reduce cloud leakage and shared-tenancy risk.", fr: "Hébergement européen dédié, télémétrie isolée et posture de sécurité conçue pour réduire les fuites cloud et les risques de cohabitation.", nb: "Dedikert europeisk hosting, isolert telemetri og en sikkerhetsholdning bygget for å redusere sky-lekkasje og risiko for delt leietak." },
"Dedicated European hosting, isolated telemetry, and a security posture built to reduce cloud leakage and shared-tenancy risk.",
}, },
{ {
title: "From retrieval to owned model", title: { en: "From retrieval to owned model", fr: "De la récupération au modèle propriétaire", nb: "Fra gjenfinning til egen modell" },
summary: summary: { en: "A path from daily retrieval work toward domain-tuned models shaped by verified expert interaction instead of disposable prompt theatre.", fr: "Un chemin allant du travail quotidien de récupération vers des modèles adaptés au domaine, façonnés par une interaction experte vérifiée plutôt que par un théâtre de prompts jetables.", nb: "En vei fra daglig gjenfinningsarbeid til domene-tilpassede modeller formet av verifisert ekspertinteraksjon i stedet for engangs prompt-teater." },
"A path from daily retrieval work toward domain-tuned models shaped by verified expert interaction instead of disposable prompt theatre.",
}, },
]; ];
export const aiLabProgrammes: AILabProgramme[] = [ export const aiLabProgrammes: AILabProgramme[] = [
{ {
title: "CorpusAI / CaveauAI", title: "CorpusAI / CaveauAI",
summary: summary: "The product layer for private retrieval, cited answers, and document-grounded search from day one.",
"The product layer for private retrieval, cited answers, and document-grounded search from day one.",
bullets: [ bullets: [
"Vector, keyword, and hybrid search modes", "Vector, keyword, and hybrid search modes",
"Cited answers linked back to source paragraphs", "Cited answers linked back to source paragraphs",
@@ -214,8 +192,7 @@ export const aiLabProgrammes: AILabProgramme[] = [
}, },
{ {
title: "Corporate Memory Extraction", title: "Corporate Memory Extraction",
summary: summary: "A flagship service that turns daily document workflows into a proprietary training asset and an owned model trajectory.",
"A flagship service that turns daily document workflows into a proprietary training asset and an owned model trajectory.",
bullets: [ bullets: [
"EU-hosted RAG as the operational starting point", "EU-hosted RAG as the operational starting point",
"Verified interaction telemetry captured in isolated MariaDB", "Verified interaction telemetry captured in isolated MariaDB",
@@ -224,8 +201,7 @@ export const aiLabProgrammes: AILabProgramme[] = [
}, },
{ {
title: "Related services", title: "Related services",
summary: summary: "The working bench around the core platform: deployment, corpus architecture, and privacy-aware data engineering.",
"The working bench around the core platform: deployment, corpus architecture, and privacy-aware data engineering.",
bullets: [ bullets: [
"Document intelligence consulting", "Document intelligence consulting",
"Knowledge corpus development", "Knowledge corpus development",
@@ -235,40 +211,10 @@ export const aiLabProgrammes: AILabProgramme[] = [
]; ];
export const aiLabSources: SourceCredit[] = [ export const aiLabSources: SourceCredit[] = [
{ { label: "Gilligan Tech", url: "https://gilligan.tech/", note: "Used for the Gilligan Tech positioning, engagement models, and the relationship to the sister platform." },
label: "Gilligan Tech", { label: "Blue Note Logic / CorpusAI", url: "https://bluenotelogic.com", note: "Used for the private AI, document intelligence, and source-cited corpus positioning." },
url: "https://gilligan.tech/", { label: "Blue Note Logic service page", url: "https://bluenotelogic.com", note: "Used for the sovereign model, EU-hosted RAG, isolated telemetry, and related-service descriptions." },
note: { label: "Trivia & Tunes", url: "https://triviaandtunes.no/", note: "Used for the live-hosted trivia and music angle on the homepage culture desk." },
"Used for the Gilligan Tech positioning, engagement models, and the relationship to the sister platform.", { label: "Do Better Norge", url: "https://dobetternorge.no/", note: "Used for the advocacy and children's-rights positioning on the homepage civic desk." },
}, { label: "Wikimedia Commons", url: "https://commons.wikimedia.org/", note: "Used for the public-domain and openly licensed city, campus, and festival images woven through the homepage and section cards." },
{
label: "Blue Note Logic / CorpusAI",
url: "https://ai.bluenotelogic.com/",
note:
"Used for the private AI, document intelligence, and source-cited corpus positioning.",
},
{
label: "Blue Note Logic service page",
url: "https://bluenotelogic.com/service.php?slug=corporate-memory-extraction",
note:
"Used for the sovereign model, EU-hosted RAG, isolated telemetry, and related-service descriptions.",
},
{
label: "Trivia & Tunes",
url: "https://triviaandtunes.no/",
note:
"Used for the live-hosted trivia and music angle on the homepage culture desk.",
},
{
label: "Do Better Norge",
url: "https://dobetternorge.no/",
note:
"Used for the advocacy and children's-rights positioning on the homepage civic desk.",
},
{
label: "Wikimedia Commons",
url: "https://commons.wikimedia.org/",
note:
"Used for the public-domain and openly licensed city, campus, and festival images woven through the homepage and section cards.",
},
]; ];
+72 -93
View File
@@ -1,3 +1,5 @@
import type { LocaleCode } from "./locales";
export type ProjectImage = { export type ProjectImage = {
src: string; src: string;
alt: string; alt: string;
@@ -14,19 +16,16 @@ export type ProjectSource = {
}; };
export const projectsFeature = { export const projectsFeature = {
eyebrow: "Section 10 / projects desk", eyebrow: { en: "Section 10 / projects desk", fr: undefined, nb: undefined },
title: "The April project is a music-trivia machine with a rhinoceros in the judging booth.", title: { en: "The April project is a music-trivia machine with a rhinoceros in the judging booth.", fr: "Le projet d'avril est une machine de quiz musical avec un rhinocéros dans le rôle de juge.", nb: "April-prosjektet er en musikkquiz-maskin med en neshorn i dommerboden." },
lede: lede: { en: "Trivia & Tunes is the first full vibe-coded product in the portfolio: a live trivia platform built for homes, venues, tournaments, player identities, and now a genuinely theatrical AI layer.", fr: "Trivia & Tunes est le premier produit entièrement codé par ambiance du portfolio : une plateforme de quiz en direct conçue pour les maisons, les lieux, les tournois, les identités des joueurs, et désormais une couche IA véritablement théâtrale.", nb: "Trivia & Tunes er det første fullstendig stemningskodede produktet i porteføljen: en live quizplattform bygget for hjem, arenaer, turneringer, spilleridentiteter, og nå et genuint teatralsk AI-lag." },
"Trivia & Tunes is the first full vibe-coded product in the portfolio: a live trivia platform built for homes, venues, tournaments, player identities, and now a genuinely theatrical AI layer.", note: { en: "This desk distinguishes between what Trivia & Tunes says publicly today and what the working codebase already proves is built or actively being staged for release.", fr: "Ce bureau distingue ce que Trivia & Tunes annonce publiquement aujourd'hui de ce que la base de code prouve déjà être construit ou activement en préparation pour la sortie.", nb: "Denne skrivepulten skiller mellom hva Trivia & Tunes sier offentlig i dag og hva kodebasen allerede viser er bygget eller aktivt under oppseiling for lansering." },
note:
"This desk distinguishes between what Trivia & Tunes says publicly today and what the working codebase already proves is built or actively being staged for release.",
}; };
export const projectsArticle = { export const projectsArticle = {
slug: "trivia-and-tunes-april-2026", slug: "trivia-and-tunes-april-2026",
title: "Trivia & Tunes and the Arrival of True AI at the Pub Quiz", title: "Trivia & Tunes and the Arrival of True AI at the Pub Quiz",
excerpt: excerpt: { en: "A spring 2026 project dispatch on music trivia, Blue Note Rhino, multiplayer formats, and the small matter of giving a quiz night an actual machine personality.", fr: "Un rapport de projet du printemps 2026 sur le quiz musical, Blue Note Rhino, les formats multijoueurs, et la petite affaire de donner une personnalité machine à une soirée quiz.", nb: "En vår 2026 prosjektoppdatering om musikkquiz, Blue Note Rhino, flerspillerformater, og den lille saken med å gi en quizkveld en faktisk maskinpersonlighet." },
"A spring 2026 project dispatch on music trivia, Blue Note Rhino, multiplayer formats, and the small matter of giving a quiz night an actual machine personality.",
publishedAt: "2026-04-06 10:15:00", publishedAt: "2026-04-06 10:15:00",
}; };
@@ -37,8 +36,7 @@ export const projectsImages: ProjectImage[] = [
credit: "Original Trivia & Tunes venue image", credit: "Original Trivia & Tunes venue image",
sourceLabel: "Trivia & Tunes live uploads", sourceLabel: "Trivia & Tunes live uploads",
sourceUrl: "https://triviaandtunes.no/uploads/venues/venue_1768237941_ec432572.jpg", sourceUrl: "https://triviaandtunes.no/uploads/venues/venue_1768237941_ec432572.jpg",
note: note: "Used here as project mood art: the right combination of room heat, street cold, and low-lit invitation.",
"Used here as project mood art: the right combination of room heat, street cold, and low-lit invitation.",
}, },
{ {
src: "https://triviaandtunes.no/uploads/teams/seed_team_microphone.png", src: "https://triviaandtunes.no/uploads/teams/seed_team_microphone.png",
@@ -46,8 +44,7 @@ export const projectsImages: ProjectImage[] = [
credit: "Original Trivia & Tunes team artwork", credit: "Original Trivia & Tunes team artwork",
sourceLabel: "Trivia & Tunes live uploads", sourceLabel: "Trivia & Tunes live uploads",
sourceUrl: "https://triviaandtunes.no/uploads/teams/seed_team_microphone.png", sourceUrl: "https://triviaandtunes.no/uploads/teams/seed_team_microphone.png",
note: note: "A good emblem for the product itself: performance, scoring, personality, and room presence.",
"A good emblem for the product itself: performance, scoring, personality, and room presence.",
}, },
{ {
src: "https://triviaandtunes.no/logoNew.png", src: "https://triviaandtunes.no/logoNew.png",
@@ -55,73 +52,57 @@ export const projectsImages: ProjectImage[] = [
credit: "Original Trivia & Tunes logo", credit: "Original Trivia & Tunes logo",
sourceLabel: "Trivia & Tunes live site", sourceLabel: "Trivia & Tunes live site",
sourceUrl: "https://triviaandtunes.no/logoNew.png", sourceUrl: "https://triviaandtunes.no/logoNew.png",
note: note: "The live public brand mark used by the current production site.",
"The live public brand mark used by the current production site.",
}, },
]; ];
export const projectsSignals = [ export const projectsSignals = [
{ { title: { en: "Public truth, right now", fr: "La vérité publique, à ce jour", nb: "Offentlig sannhet, akkurat nå" }, summary: { en: "The live site currently presents Trivia & Tunes as a three-format product: solo host, live display, and pro live, with strong music integrations and venue-facing play.", fr: "Le site en direct présente actuellement Trivia & Tunes comme un produit à trois formats : hôte solo, affichage en direct et pro en direct, avec de fortes intégrations musicales et un jeu orienté vers les lieux.", nb: "Den aktive nettsiden presenterer Trivia & Tunes som et produkt med tre formater: solovert, live display og pro live, med sterke musikkintegrasjoner og spill rettet mot arenaer." } },
title: "Public truth, right now", { title: { en: "The stronger local build", fr: "La version locale renforcée", nb: "Den sterkere lokale bygningen" }, summary: { en: "The working codebase already goes further: AI Home, AI Live, AI Solo, Blue Note Rhino, commentary, wrapups, model selection, and call logging are all visible in the repo on April 6, 2026.", fr: "La base de code en cours va déjà plus loin : AI Home, AI Live, AI Solo, Blue Note Rhino, commentaires, résumés, sélection de modèles et journalisation des appels sont tous visibles dans le dépôt au 6 avril 2026.", nb: "Kodebasen går allerede lenger: AI Home, AI Live, AI Solo, Blue Note Rhino, kommentarer, oppsummeringer, modellvalg og samtalelogger er alle synlige i repoet per 6. april 2026." } },
summary: { title: { en: "Voice is the next turn of the screw", fr: "La voix est le prochain tour de vis", nb: "Stemmen er neste skritt" }, summary: { en: "A dedicated TTS service is already staged for testing, with English, Norwegian, French, and additional voices mapped and ready for use.", fr: "Un service TTS dédié est déjà prêt pour les tests, avec des voix en anglais, norvégien, français et d'autres langues mappées et prêtes à l'emploi.", nb: "En dedikert TTS-tjeneste er allerede klar for testing, med engelsk, norsk, fransk og flere stemmer kartlagt og klare til bruk." } },
"The live site currently presents Trivia & Tunes as a three-format product: solo host, live display, and pro live, with strong music integrations and venue-facing play.",
},
{
title: "The stronger local build",
summary:
"The working codebase already goes further: AI Home, AI Live, AI Solo, Blue Note Rhino, commentary, wrapups, model selection, and call logging are all visible in the repo on April 6, 2026.",
},
{
title: "Voice is the next turn of the screw",
summary:
"A dedicated TTS service is already staged for testing, with English, Norwegian, French, and additional voices mapped and ready for use.",
},
]; ];
export const projectsCapabilities = [ export const projectsCapabilities = [
{ { title: { en: "One game, multiple room geometries", fr: "Un jeu, plusieurs géométries de salle", nb: "Ett spill, flere romgeometrier" }, body: { en: "The product is not just a quiz app. It is a room-design system. One-screen home play, host-plus-display nights, and phone-driven player lanes all exist as distinct social formats rather than accidental layouts.", fr: "Le produit n'est pas juste une application de quiz. C'est un système de conception de salle. Le jeu à un écran pour la maison, les soirées avec hôte et affichage, et les couloirs de joueurs pilotés par téléphone existent tous comme des formats sociaux distincts plutôt que des dispositions accidentelles.", nb: "Produktet er ikke bare en quizapp. Det er et romdesignsystem. Énskjermspill for hjemmet, vert-pluss-display-kvelder og telefonstyrte spillerbaner eksisterer alle som distinkte sosiale formater snarere enn tilfeldige oppsett." } },
title: "One game, multiple room geometries", { title: { en: "Blue Note Rhino as judge and emcee", fr: "Blue Note Rhino comme juge et maître de cérémonie", nb: "Blue Note Rhino som dommer og konferansier" }, body: { en: "The AI layer is not framed as vague assistance. It grades free-text answers, applies strictness levels, generates wrapups, reacts to how a round went, and speaks in a built persona instead of silent utility.", fr: "La couche IA n'est pas présentée comme une assistance vague. Elle évalue les réponses en texte libre, applique des niveaux de rigueur, génère des résumés, réagit à la manière dont une manche s'est déroulée, et parle dans une persona construite plutôt que d'être une utilité silencieuse.", nb: "AI-laget er ikke rammet inn som vag assistanse. Det vurderer fritekstsvar, anvender strenghetsnivåer, genererer oppsummeringer, reagerer på hvordan en runde gikk, og snakker i en bygget persona i stedet for å være en stille nyttefunksjon." } },
body: { title: { en: "Music is treated as infrastructure", fr: "La musique est traitée comme une infrastructure", nb: "Musikk behandles som infrastruktur" }, body: { en: "Spotify, Apple Music, and YouTube are not ornamental add-ons. They sit inside the trivia flow itself, which is why the whole thing feels closer to a hosted night than a generic browser game.", fr: "Spotify, Apple Music et YouTube ne sont pas des ajouts ornementaux. Ils s'insèrent dans le flux de quiz lui-même, ce qui explique pourquoi l'ensemble ressemble plus à une soirée animée qu'à un jeu de navigateur générique.", nb: "Spotify, Apple Music og YouTube er ikke dekorative tillegg. De sitter inne i quizflyten selv, noe som gjør at hele opplevelsen føles nærmere en vertsstyrt kveld enn et generisk nettleserspill." } },
"The product is not just a quiz app. It is a room-design system. One-screen home play, host-plus-display nights, and phone-driven player lanes all exist as distinct social formats rather than accidental layouts.", { title: { en: "A real management surface", fr: "Une vraie surface de gestion", nb: "En ekte administrasjonsoverflate" }, body: { en: "Questions, players, teams, events, tournaments, venue pages, and media links are all treated as editable operating material, not hard-coded brochure text.", fr: "Les questions, les joueurs, les équipes, les événements, les tournois, les pages de lieux et les liens médias sont tous traités comme du matériel opérationnel éditable, et non comme du texte de brochure codé en dur.", nb: "Spørsmål, spillere, lag, arrangementer, turneringer, arena-sider og medielenker behandles som redigerbart operativt materiale, ikke hardkodet brosjyretekst." } },
}, { title: { en: "The first real vibe-coded product", fr: "Le premier vrai produit codé par ambiance", nb: "Det første ekte stemningskodede produktet" }, body: { en: "The internal build notes tell the truth plainly: start with room feel, iterate fast, keep it useful, document everything, and let the software earn its style through use.", fr: "Les notes internes de construction disent la vérité simplement : commencer par l'ambiance de la salle, itérer rapidement, garder cela utile, tout documenter, et laisser le logiciel gagner son style par l'usage.", nb: "De interne byggnotatene sier sannheten enkelt: start med romfølelsen, iterer raskt, hold det nyttig, dokumenter alt, og la programvaren tjene sin stil gjennom bruk." } },
{ { title: { en: "The April turn", fr: "Le tournant d'avril", nb: "April-svingen" }, body: { en: "This month matters because the product crosses a line from 'music trivia with good UX' into 'music trivia with an actual AI performance layer,' and that changes the identity of the whole thing.", fr: "Ce mois compte parce que le produit franchit une ligne entre 'quiz musical avec une bonne UX' et 'quiz musical avec une véritable couche de performance IA', et cela change l'identité de l'ensemble.", nb: "Denne måneden er viktig fordi produktet krysser en linje fra 'musikkquiz med god UX' til 'musikkquiz med et faktisk AI-ytelseslag', og det endrer identiteten til hele opplevelsen." } },
title: "Blue Note Rhino as judge and emcee",
body:
"The AI layer is not framed as vague assistance. It grades free-text answers, applies strictness levels, generates wrapups, reacts to how a round went, and speaks in a built persona instead of silent utility.",
},
{
title: "Music is treated as infrastructure",
body:
"Spotify, Apple Music, and YouTube are not ornamental add-ons. They sit inside the trivia flow itself, which is why the whole thing feels closer to a hosted night than a generic browser game.",
},
{
title: "A real management surface",
body:
"Questions, players, teams, events, tournaments, venue pages, and media links are all treated as editable operating material, not hard-coded brochure text.",
},
{
title: "The first real vibe-coded product",
body:
"The internal build notes tell the truth plainly: start with room feel, iterate fast, keep it useful, document everything, and let the software earn its style through use.",
},
{
title: "The April turn",
body:
"This month matters because the product crosses a line from 'music trivia with good UX' into 'music trivia with an actual AI performance layer,' and that changes the identity of the whole thing.",
},
]; ];
export const projectsDeskBody = [ export const projectsDeskBody: Record<LocaleCode, string[]> = {
en: [
"Trivia nights usually suffer from one of two problems. Either they are dead administrative exercises disguised as fun, or they are charming little messes that collapse as soon as a room gets big, loud, or demanding. Trivia & Tunes is interesting because it refuses that choice. It wants proper room energy and proper system design at the same time.", "Trivia nights usually suffer from one of two problems. Either they are dead administrative exercises disguised as fun, or they are charming little messes that collapse as soon as a room gets big, loud, or demanding. Trivia & Tunes is interesting because it refuses that choice. It wants proper room energy and proper system design at the same time.",
"The live public site still presents the older triptych: solo host, live display, and pro live. Even in that public form, the platform already reads as more than a hobby project. It speaks in formats, roles, display logic, music sources, venues, tournaments, and player identity. That is the vocabulary of an ecosystem, not a one-off game.", "The live public site still presents the older triptych: solo host, live display, and pro live. Even in that public form, the platform already reads as more than a hobby project. It speaks in formats, roles, display logic, music sources, venues, tournaments, and player identity. That is the vocabulary of an ecosystem, not a one-off game.",
"The working April build goes further. In the local codebase reviewed on April 6, 2026, Trivia & Tunes has already developed an AI vocabulary of its own: AI Home, AI Live, AI Solo, model selection, per-question commentary, round wrapups, game wrapups, and a named judging persona called Blue Note Rhino. That is not decorative AI sprayed over an old surface. It is a real change in product character.", "The working April build goes further. In the local codebase reviewed on April 6, 2026, Trivia & Tunes has already developed an AI vocabulary of its own: AI Home, AI Live, AI Solo, model selection, per-question commentary, round wrapups, game wrapups, and a named judging persona called Blue Note Rhino. That is not decorative AI sprayed over an old surface. It is a real change in product character.",
"What makes the Rhino interesting is not merely that it grades. Plenty of systems can call a model and pretend they have solved judgment. Here the ambition is theatrical. The Rhino has strictness levels, a configurable personality, free-text semantic grading, and the beginnings of a running relationship with the room. In other words, the machine is not only a checker. It is becoming part of the night.", "What makes the Rhino interesting is not merely that it grades. Plenty of systems can call a model and pretend they have solved judgment. Here the ambition is theatrical. The Rhino has strictness levels, a configurable personality, free-text semantic grading, and the beginnings of a running relationship with the room. In other words, the machine is not only a checker. It is becoming part of the night.",
"That matters because music trivia is social timing more than raw database retrieval. A good night has pacing, tension, recovery, banter, and a little danger. The AI layer works best when it helps the host keep those qualities alive rather than replacing them. The most promising parts of the local build do exactly that: commentary between questions, wrapups between rounds, and mode-specific flows that respect the room rather than flatten it.", "That matters because music trivia is social timing more than raw database retrieval. A good night has pacing, tension, recovery, banter, and a little danger. The AI layer works best when it helps the host keep those qualities alive rather than replacing them. The most promising parts of the local build do exactly that: commentary between questions, wrapups between rounds, and mode-specific flows that respect the room rather than flatten it.",
"The codebase also reveals another practical truth: this is not a toy AI experiment sitting in isolation. There are management surfaces, player records, team structures, events, tournaments, login gates, model logs, and admin settings. Even voice is already staged, with a TTS service ready for testing across multiple languages. If the GPU and IIS side are not always awake yet, the architecture is nevertheless pointing in the right direction.", "The codebase also reveals another practical truth: this is not a toy AI experiment sitting in isolation. There are management surfaces, player records, team structures, events, tournaments, login gates, model logs, and admin settings. Even voice is already staged, with a TTS service ready for testing across multiple languages. If the GPU and IIS side are not always awake yet, the architecture is nevertheless pointing in the right direction.",
"That is why this April project deserves the Projects desk rather than a casual mention elsewhere. Trivia & Tunes is the first full, opinionated, public-facing vibe-coded product in the portfolio: music-heavy, UX-led, multilingual, operationally serious, and now decisively AI-backed. It has enough room heat to feel alive, and enough systems underneath to survive contact with real users.", "That is why this April project deserves the Projects desk rather than a casual mention elsewhere. Trivia & Tunes is the first full, opinionated, public-facing vibe-coded product in the portfolio: music-heavy, UX-led, multilingual, operationally serious, and now decisively AI-backed. It has enough room heat to feel alive, and enough systems underneath to survive contact with real users."
]; ],
fr: [
"Les soirées quiz souffrent généralement de l'un des deux problèmes. Soit elles sont des exercices administratifs morts déguisés en amusement, soit elles sont de charmants petits désordres qui s'effondrent dès qu'une salle devient grande, bruyante ou exigeante. Trivia & Tunes est intéressant parce qu'il refuse ce choix. Il veut à la fois une vraie énergie de salle et un vrai design de système.",
"Le site public en direct présente encore l'ancien triptyque : hôte solo, affichage en direct et pro en direct. Même sous cette forme publique, la plateforme se lit déjà comme plus qu'un projet de hobby. Elle parle en formats, rôles, logique d'affichage, sources musicales, lieux, tournois et identité des joueurs. C'est le vocabulaire d'un écosystème, pas d'un jeu unique.",
"La version d'avril va plus loin. Dans la base de code locale examinée le 6 avril 2026, Trivia & Tunes a déjà développé un vocabulaire IA propre : AI Home, AI Live, AI Solo, sélection de modèles, commentaires par question, résumés de manche, résumés de jeu, et une persona de juge nommée Blue Note Rhino. Ce n'est pas une IA décorative pulvérisée sur une vieille surface. C'est un vrai changement de caractère du produit.",
"Ce qui rend le Rhino intéressant, ce n'est pas seulement qu'il évalue. Beaucoup de systèmes peuvent appeler un modèle et prétendre avoir résolu le jugement. Ici, l'ambition est théâtrale. Le Rhino a des niveaux de rigueur, une personnalité configurable, une évaluation sémantique des réponses en texte libre, et les débuts d'une relation continue avec la salle. En d'autres termes, la machine n'est pas seulement un vérificateur. Elle devient partie intégrante de la soirée.",
"Cela compte parce que le quiz musical est plus une question de timing social que de récupération brute de base de données. Une bonne soirée a du rythme, de la tension, de la récupération, des plaisanteries et un peu de danger. La couche IA fonctionne mieux lorsqu'elle aide l'hôte à maintenir ces qualités vivantes plutôt que de les remplacer. Les parties les plus prometteuses de la version locale font exactement cela : commentaires entre les questions, résumés entre les manches, et flux spécifiques au mode qui respectent la salle plutôt que de l'aplatir.",
"La base de code révèle également une autre vérité pratique : ce n'est pas une expérience IA jouet isolée. Il y a des surfaces de gestion, des enregistrements de joueurs, des structures d'équipe, des événements, des tournois, des portes de connexion, des journaux de modèles et des paramètres d'administration. Même la voix est déjà prête, avec un service TTS prêt pour les tests dans plusieurs langues. Si le côté GPU et IIS n'est pas toujours éveillé, l'architecture pointe néanmoins dans la bonne direction.",
"C'est pourquoi ce projet d'avril mérite le bureau des projets plutôt qu'une mention occasionnelle ailleurs. Trivia & Tunes est le premier produit entièrement codé par ambiance, public et opiniâtre du portfolio : axé sur la musique, dirigé par l'UX, multilingue, opérationnellement sérieux, et maintenant résolument soutenu par l'IA. Il a assez de chaleur de salle pour se sentir vivant, et assez de systèmes en dessous pour survivre au contact avec de vrais utilisateurs."
],
nb: [
"Quizkvelder lider vanligvis av ett av to problemer. Enten er de døde administrative øvelser forkledd som moro, eller så er de sjarmerende små rot som kollapser så snart et rom blir stort, høyt eller krevende. Trivia & Tunes er interessant fordi det nekter å velge. Det vil ha ekte romenergi og ekte systemdesign samtidig.",
"Den aktive offentlige nettsiden presenterer fortsatt den eldre triptyken: solovert, live display og pro live. Selv i denne offentlige formen leser plattformen allerede som mer enn et hobbyprosjekt. Den snakker i formater, roller, displaylogikk, musikkskilder, arenaer, turneringer og spilleridentitet. Det er vokabularet til et økosystem, ikke et engangsspill.",
"Den lokale aprilbygningen går lenger. I kodebasen som ble gjennomgått 6. april 2026, har Trivia & Tunes allerede utviklet et eget AI-vokabular: AI Home, AI Live, AI Solo, modellvalg, per-spørsmål-kommentarer, rundeoppsummeringer, spilloppsummeringer, og en navngitt dommerpersona kalt Blue Note Rhino. Dette er ikke dekorativ AI sprayet over en gammel overflate. Det er en reell endring i produktkarakter.",
"Det som gjør Rhino interessant er ikke bare at det vurderer. Mange systemer kan kalle en modell og late som de har løst dommerproblemet. Her er ambisjonen teatralsk. Rhino har strenghetsnivåer, en konfigurerbar personlighet, semantisk vurdering av fritekstsvar, og begynnelsen på et løpende forhold til rommet. Med andre ord, maskinen er ikke bare en sjekker. Den blir en del av kvelden.",
"Det betyr noe fordi musikkquiz handler mer om sosial timing enn rå databasehenting. En god kveld har tempo, spenning, gjenoppretting, småprat og litt fare. AI-laget fungerer best når det hjelper verten med å holde disse kvalitetene levende i stedet for å erstatte dem. De mest lovende delene av den lokale bygningen gjør akkurat det: kommentarer mellom spørsmål, oppsummeringer mellom runder, og modusspesifikke flyter som respekterer rommet i stedet for å flate det ut.",
"Kodebasen avslører også en annen praktisk sannhet: dette er ikke et leketøy-AI-eksperiment som sitter isolert. Det finnes administrasjonsoverflater, spillerregistre, lagstrukturer, arrangementer, turneringer, innloggingsporter, modelllogger og admininnstillinger. Selv stemme er allerede klar, med en TTS-tjeneste klar for testing på flere språk. Hvis GPU- og IIS-siden ikke alltid er våken ennå, peker arkitekturen likevel i riktig retning.",
"Det er derfor dette aprilprosjektet fortjener Prosjekt-skrivebordet i stedet for en tilfeldig omtale andre steder. Trivia & Tunes er det første fullstendig stemningskodede, offentlig orienterte produktet i porteføljen: musikkfokusert, UX-ledet, flerspråklig, operativt seriøst, og nå definitivt AI-støttet. Det har nok romvarme til å føles levende, og nok systemer under til å overleve kontakt med ekte brukere."
],
};
export const projectsArticleBody = [ export const projectsArticleBody: Record<LocaleCode, string[]> = {
en: [
"The phrase 'true AI' is abused so often that it usually arrives smelling of stale venture decks. But once in a while a project earns the phrase by changing what the product is allowed to feel like. Trivia & Tunes has reached that threshold this spring.", "The phrase 'true AI' is abused so often that it usually arrives smelling of stale venture decks. But once in a while a project earns the phrase by changing what the product is allowed to feel like. Trivia & Tunes has reached that threshold this spring.",
"On the public site today, Trivia & Tunes already presents itself as a layered trivia platform rather than a simple quiz toy. The live games page offers three recognizable public formats: a solo host edition for home use, a live game with a big-screen display, and a pro live version with mobile answering for players. That is the respectable front room, and it is already more ambitious than most trivia products ever become.", "On the public site today, Trivia & Tunes already presents itself as a layered trivia platform rather than a simple quiz toy. The live games page offers three recognizable public formats: a solo host edition for home use, a live game with a big-screen display, and a pro live version with mobile answering for players. That is the respectable front room, and it is already more ambitious than most trivia products ever become.",
"Behind that public face, the working local build tells a hotter story. The repo reviewed on April 6, 2026 shows a second life already under construction: AI Home, AI Live, AI Solo, model management, call logging, wrapup prompts, per-question commentary, and a fully named judging persona, Blue Note Rhino. In other words, this is no longer only a trivia platform with good screens. It is becoming a performance system with machine timing.", "Behind that public face, the working local build tells a hotter story. The repo reviewed on April 6, 2026 shows a second life already under construction: AI Home, AI Live, AI Solo, model management, call logging, wrapup prompts, per-question commentary, and a fully named judging persona, Blue Note Rhino. In other words, this is no longer only a trivia platform with good screens. It is becoming a performance system with machine timing.",
@@ -129,36 +110,34 @@ export const projectsArticleBody = [
"The clever part is that the underlying architecture still respects the old truths of quiz nights. Music remains central, with Spotify, Apple Music, and YouTube embedded into the experience. Room geometry matters. One-screen living-room play is not treated the same as host-plus-display nights or the more competitive phone-plus-display format. This is where the vibe coding note becomes more than a slogan. The code follows the social feel of the room.", "The clever part is that the underlying architecture still respects the old truths of quiz nights. Music remains central, with Spotify, Apple Music, and YouTube embedded into the experience. Room geometry matters. One-screen living-room play is not treated the same as host-plus-display nights or the more competitive phone-plus-display format. This is where the vibe coding note becomes more than a slogan. The code follows the social feel of the room.",
"Even the build notes are unusually honest. They talk about starting with the desired feeling, iterating quickly, keeping the code organized without fetishizing perfection, and making the product work beautifully for non-technical people. Normally this kind of internal document is too earnest to quote. Here it belongs to the story, because the resulting product really does show the fingerprints of that method.", "Even the build notes are unusually honest. They talk about starting with the desired feeling, iterating quickly, keeping the code organized without fetishizing perfection, and making the product work beautifully for non-technical people. Normally this kind of internal document is too earnest to quote. Here it belongs to the story, because the resulting product really does show the fingerprints of that method.",
"Then there is voice. The TTS service sitting in the local project is not yet a triumphant public launch, and I will not pretend otherwise. But it is real enough to matter: mapped voices for English, Norwegian, French, German, Spanish, Italian, Portuguese, Polish, Dutch, Swedish, Danish, and Finnish, all framed as part of the next testing layer. That means the Rhino is not only a text persona waiting in the shadows. It is edging toward an audible one.", "Then there is voice. The TTS service sitting in the local project is not yet a triumphant public launch, and I will not pretend otherwise. But it is real enough to matter: mapped voices for English, Norwegian, French, German, Spanish, Italian, Portuguese, Polish, Dutch, Swedish, Danish, and Finnish, all framed as part of the next testing layer. That means the Rhino is not only a text persona waiting in the shadows. It is edging toward an audible one.",
"So April's verdict is simple. Trivia & Tunes is the first full vibe-coded product in the broader Dave Gilligan orbit to step across the line from strong interface into living system. It already knows how to run a room. Now it is learning how to talk back.", "So April's verdict is simple. Trivia & Tunes is the first full vibe-coded product in the broader Dave Gilligan orbit to step across the line from strong interface into living system. It already knows how to run a room. Now it is learning how to talk back."
]; ],
fr: [
"L'expression 'véritable IA' est tellement abusée qu'elle arrive généralement avec une odeur de présentations d'investissement périmées. Mais de temps en temps, un projet mérite cette expression en changeant ce que le produit est autorisé à ressentir. Trivia & Tunes a atteint ce seuil ce printemps.",
"Sur le site public aujourd'hui, Trivia & Tunes se présente déjà comme une plateforme de quiz stratifiée plutôt qu'un simple jouet de quiz. La page des jeux en direct propose trois formats publics reconnaissables : une édition hôte solo pour un usage domestique, un jeu en direct avec un grand écran d'affichage, et une version pro en direct avec réponses mobiles pour les joueurs. C'est le salon respectable, et c'est déjà plus ambitieux que la plupart des produits de quiz ne le deviennent jamais.",
"Derrière cette façade publique, la version locale raconte une histoire plus chaude. Le dépôt examiné le 6 avril 2026 montre une seconde vie déjà en construction : AI Home, AI Live, AI Solo, gestion des modèles, journalisation des appels, invites de résumés, commentaires par question, et une persona de juge entièrement nommée, Blue Note Rhino. En d'autres termes, ce n'est plus seulement une plateforme de quiz avec de bons écrans. Cela devient un système de performance avec un timing machine.",
"Blue Note Rhino est le bon type de dépassement. Le guide IA présente le Rhino non pas comme une utilité cachée mais comme un véritable personnage de salle : un évaluateur, un commentateur, et un maître de cérémonie taquin qui comprend les réponses en texte libre de manière sémantique au lieu de forcer les joueurs dans des cases à choix multiples. L'hôte peut ajuster la rigueur. La machine peut générer des résumés de manche et de jeu. Elle peut réagir à la performance des joueurs. C'est une identité de produit, pas seulement une consommation d'API.",
"La partie intelligente est que l'architecture sous-jacente respecte encore les anciennes vérités des soirées quiz. La musique reste centrale, avec Spotify, Apple Music et YouTube intégrés dans l'expérience. La géométrie de la salle compte. Le jeu à un écran dans le salon n'est pas traité de la même manière que les soirées avec hôte et affichage ou le format plus compétitif téléphone-plus-affichage. C'est là que la note de codage par ambiance devient plus qu'un slogan. Le code suit le ressenti social de la salle.",
"Même les notes de construction sont exceptionnellement honnêtes. Elles parlent de commencer par le ressenti désiré, d'itérer rapidement, de garder le code organisé sans fétichiser la perfection, et de faire fonctionner le produit magnifiquement pour les personnes non techniques. Normalement, ce genre de document interne est trop sérieux pour être cité. Ici, il appartient à l'histoire, parce que le produit résultant montre vraiment les empreintes de cette méthode.",
"Et puis il y a la voix. Le service TTS dans le projet local n'est pas encore un lancement public triomphal, et je ne prétendrai pas le contraire. Mais il est suffisamment réel pour compter : des voix mappées pour l'anglais, le norvégien, le français, l'allemand, l'espagnol, l'italien, le portugais, le polonais, le néerlandais, le suédois, le danois et le finnois, toutes encadrées comme partie de la prochaine couche de test. Cela signifie que le Rhino n'est pas seulement une persona textuelle attendant dans l'ombre. Il se rapproche d'une persona audible.",
"Donc le verdict d'avril est simple. Trivia & Tunes est le premier produit entièrement codé par ambiance dans l'orbite élargie de Dave Gilligan à franchir la ligne entre une interface forte et un système vivant. Il sait déjà comment gérer une salle. Maintenant, il apprend à répondre."
],
nb: [
"Uttrykket 'ekte AI' blir misbrukt så ofte at det vanligvis ankommer med en lukt av utdaterte investorpresentasjoner. Men av og til fortjener et prosjekt uttrykket ved å endre hva produktet får lov til å føles som. Trivia & Tunes har nådd den terskelen denne våren.",
"På den offentlige nettsiden i dag presenterer Trivia & Tunes seg allerede som en lagdelt quizplattform snarere enn et enkelt quiz-leketøy. Siden for live spill tilbyr tre gjenkjennelige offentlige formater: en solovert-utgave for hjemmebruk, et live spill med storskjermdisplay, og en pro live-versjon med mobilbesvarelse for spillere. Det er den respektable stuen, og det er allerede mer ambisiøst enn de fleste quizprodukter noen gang blir.",
"Bak denne offentlige fasaden forteller den lokale bygningen en varmere historie. Repoet som ble gjennomgått 6. april 2026 viser et annet liv allerede under konstruksjon: AI Home, AI Live, AI Solo, modelladministrasjon, samtalelogging, oppsummeringsprompt, per-spørsmål-kommentarer, og en fullt navngitt dommerpersona, Blue Note Rhino. Med andre ord, dette er ikke lenger bare en quizplattform med gode skjermer. Det blir et ytelsessystem med maskintiming.",
"Blue Note Rhino er den rette typen overambisjon. AI-guiden rammer inn Rhino ikke som en skjult nyttefunksjon, men som en faktisk romkarakter: en vurderer, kommentator og ertende konferansier som forstår fritekstsvar semantisk i stedet for å tvinge spillere inn i flervalgskasser. Vert kan justere strenghet. Maskinen kan generere runde- og spilloppsummeringer. Den kan reagere på hvordan spillerne presterte. Det er produktidentitet, ikke bare API-forbruk.",
"Det smarte er at den underliggende arkitekturen fortsatt respekterer de gamle sannhetene om quizkvelder. Musikk forblir sentralt, med Spotify, Apple Music og YouTube integrert i opplevelsen. Romgeometri betyr noe. Énskjermspill i stuen behandles ikke på samme måte som vert-pluss-display-kvelder eller det mer konkurransedyktige telefon-pluss-display-formatet. Det er her stemningskoding blir mer enn et slagord. Koden følger den sosiale følelsen av rommet.",
"Selv byggnotatene er uvanlig ærlige. De snakker om å starte med ønsket følelse, iterere raskt, holde koden organisert uten å fetisjere perfeksjon, og få produktet til å fungere vakkert for ikke-tekniske mennesker. Normalt er denne typen interne dokumenter for alvorlige til å sitere. Her hører de til historien, fordi det resulterende produktet virkelig viser fingeravtrykkene av den metoden.",
"Og så er det stemmen. TTS-tjenesten som ligger i det lokale prosjektet er ennå ikke en triumferende offentlig lansering, og jeg vil ikke late som noe annet. Men den er ekte nok til å telle: kartlagte stemmer for engelsk, norsk, fransk, tysk, spansk, italiensk, portugisisk, polsk, nederlandsk, svensk, dansk og finsk, alle rammet inn som en del av neste testlag. Det betyr at Rhino ikke bare er en tekstpersona som venter i skyggene. Den nærmer seg en hørbar en.",
"Så april-dommen er enkel. Trivia & Tunes er det første fullstendig stemningskodede produktet i den bredere Dave Gilligan-sfæren som krysser linjen fra sterk grensesnitt til levende system. Den vet allerede hvordan man styrer et rom. Nå lærer den å svare."
],
};
export const projectsSources: ProjectSource[] = [ export const projectsSources: ProjectSource[] = [
{ { label: "Trivia & Tunes live homepage", href: "https://triviaandtunes.no/", note: "Used for the current public brand, tagline, and confirmation that the live production shell is active." },
label: "Trivia & Tunes live homepage", { label: "Trivia & Tunes games page", href: "https://triviaandtunes.no/games/", note: "Used for the live public format breakdown: solo host, live game with big-screen display, and pro live with mobile answering." },
href: "https://triviaandtunes.no/", { label: "Trivia & Tunes live image assets", href: "https://triviaandtunes.no/uploads/venues/venue_1768237941_ec432572.jpg", note: "Used for original project imagery pulled from the live Trivia & Tunes installation." },
note: { label: "Local Trivia & Tunes working repository", note: "Reviewed locally on April 6, 2026 for AI Home, AI Live, AI Solo, Blue Note Rhino, wrapups, per-question commentary, model management, call logging, and voice-service staging." },
"Used for the current public brand, tagline, and confirmation that the live production shell is active.", { label: "How We Built Trivia & Tunes", note: "Used for the internal product-development narrative around vibe coding, iterative delivery, UX-first decisions, and the explanation of the system for non-technical readers." },
},
{
label: "Trivia & Tunes games page",
href: "https://triviaandtunes.no/games/",
note:
"Used for the live public format breakdown: solo host, live game with big-screen display, and pro live with mobile answering.",
},
{
label: "Trivia & Tunes live image assets",
href: "https://triviaandtunes.no/uploads/venues/venue_1768237941_ec432572.jpg",
note:
"Used for original project imagery pulled from the live Trivia & Tunes installation.",
},
{
label: "Local Trivia & Tunes working repository",
note:
"Reviewed locally on April 6, 2026 for AI Home, AI Live, AI Solo, Blue Note Rhino, wrapups, per-question commentary, model management, call logging, and voice-service staging.",
},
{
label: "How We Built Trivia & Tunes",
note:
"Used for the internal product-development narrative around vibe coding, iterative delivery, UX-first decisions, and the explanation of the system for non-technical readers.",
},
]; ];
+155 -142
View File
@@ -1,13 +1,15 @@
import type { LocaleCode } from "./locales";
export type LaunchSection = { export type LaunchSection = {
slug: string; slug: string;
label: string; label: string;
title: string; title: string;
summary: string; summary: Record<LocaleCode, string>;
tone: string; tone: Record<LocaleCode, string>;
strap: string; strap: Record<LocaleCode, string>;
coverline: string; coverline: Record<LocaleCode, string>;
motif: string; motif: Record<LocaleCode, string>;
samples: string[]; samples: Record<LocaleCode, string[]>;
}; };
export type SchoolDossier = { export type SchoolDossier = {
@@ -17,143 +19,115 @@ export type SchoolDossier = {
program: string; program: string;
city: string; city: string;
logo: string; logo: string;
teaser: string; teaser: Record<LocaleCode, string>;
}; };
export const hero = { export const hero = {
kicker: "Pataphysical bulletin / Kongsberg edition", kicker: { en: "Pataphysical bulletin / Kongsberg edition", fr: "Bulletin pataphysique / Édition Kongsberg", nb: "Pataphysisk bulletin / Kongsberg-utgave" },
title: "A high-tech newsmagazine disguised as one man's improbable paper trail.", title: { en: "A high-tech newsmagazine disguised as one man's improbable paper trail.", fr: "Un magazine d'actualités high-tech déguisé en le parcours improbable d'un homme.", nb: "Et høyteknologisk nyhetsmagasin forkledd som én manns usannsynlige papirspor." },
lede: lede: { en: "Private AI, jazz basements, multilingual weather, family rights, and systems work from Ringwood to Kongsberg, edited with equal parts brass, evidence, and deliberate mischief.", fr: "IA privée, caves de jazz, météo multilingue, droits familiaux et travail systémique de Ringwood à Kongsberg, édité avec autant de cuivres, de preuves et de malice délibérée.", nb: "Privat AI, jazzkjellere, flerspråklig vær, familierettigheter og systemarbeid fra Ringwood til Kongsberg, redigert med like deler messing, bevis og bevisst rampestreker." },
"Private AI, jazz basements, multilingual weather, family rights, and systems work from Ringwood to Kongsberg, edited with equal parts brass, evidence, and deliberate mischief.", sublede: { en: "Inside this issue: Blue Note Logic in the machine room, Gilligan Tech in the field, Do Better Norge in the civic file, Trivia & Tunes in the live-wire culture pages, and school dossiers written like contraband literature.", fr: "Dans ce numéro : Blue Note Logic dans la salle des machines, Gilligan TECH sur le terrain, Do Better Norge dans le dossier civique, Trivia & Tunes dans les pages culturelles sous tension, et des dossiers scolaires écrits comme de la littérature de contrebande.", nb: "I denne utgaven: Blue Note Logic i maskinrommet, Gilligan TECH i felten, Do Better Norge i det sivile arkivet, Trivia & Tunes i de kulturspente sidene, og skoledokumenter skrevet som forbudt litteratur." },
sublede:
"Inside this issue: Blue Note Logic in the machine room, Gilligan Tech in the field, Do Better Norge in the civic file, Trivia & Tunes in the live-wire culture pages, and school dossiers written like contraband literature.",
}; };
export const launchSections: LaunchSection[] = [ export const launchSections: LaunchSection[] = [
{ {
slug: "business", slug: "business", label: "01", title: "Business",
label: "01", summary: { en: "Consulting notes, AI architecture, and operator essays for people who prefer working systems to rented theater.", fr: "Notes de conseil, architectures IA et essais d'opérateurs pour ceux qui préfèrent les systèmes fonctionnels au théâtre en location.", nb: "Konsulentnotater, AI-arkitektur og operatør-essays for folk som foretrekker fungerende systemer fremfor leid teater." },
title: "Business", tone: { en: "Sharp, practical, anti-buzzword.", fr: "Tranchant, pratique, anti-buzzword.", nb: "Skarpt, praktisk, anti-buzzword." },
summary: "Consulting notes, AI architecture, and operator essays for people who prefer working systems to rented theater.", strap: { en: "Operator studies for adults who are tired of consultant vapor.", fr: "Études d'opérateurs pour adultes lassés des consultants vaporeux.", nb: "Operatørstudier for voksne som er lei av konsulentprat." },
tone: "Sharp, practical, anti-buzzword.", coverline: { en: "The anti-buzzword ledger.", fr: "Le registre anti-buzzword.", nb: "Anti-buzzword-regnskapet." },
strap: "Operator studies for adults who are tired of consultant vapor.", motif: { en: "Pricing, systems, execution, and elegant refusal.", fr: "Tarification, systèmes, exécution et refus élégant.", nb: "Prising, systemer, utførelse og elegant avslag." },
coverline: "The anti-buzzword ledger.", samples: { en: ["The one-laptop doctrine","SME systems with teeth","Boardroom notes without cologne"], fr: ["La doctrine du seul ordinateur portable","Systèmes PME avec du mordant","Notes de salle de conseil sans eau de Cologne"], nb: ["Én-laptop-doktrinen","SME-systemer med tenner","Styreromsnotater uten parfyme"] },
motif: "Pricing, systems, execution, and elegant refusal.",
samples: ["The one-laptop doctrine", "SME systems with teeth", "Boardroom notes without cologne"],
}, },
{ {
slug: "education", slug: "education", label: "02", title: "Education",
label: "02", summary: { en: "School dossiers, study notes, intellectual migrations, and academic detours worth keeping.", fr: "Dossiers scolaires, notes d'étude, migrations intellectuelles et détours académiques qui valent la peine d'être conservés.", nb: "Skolemapper, studienotater, intellektuelle migrasjoner og akademiske omveier verdt å beholde." },
title: "Education", tone: { en: "Curious, rigorous, lightly mischievous.", fr: "Curieux, rigoureux, légèrement espiègle.", nb: "Nysgjerrig, grundig, lett rampete." },
summary: "School dossiers, study notes, intellectual migrations, and academic detours worth keeping.", strap: { en: "Degrees as cities, schools as chapters, study as migration.", fr: "Les diplômes comme des villes, les écoles comme des chapitres, l'étude comme une migration.", nb: "Grader som byer, skoler som kapitler, studier som migrasjon." },
tone: "Curious, rigorous, lightly mischievous.", coverline: { en: "The dossier issue.", fr: "Le numéro dossier.", nb: "Dossier-utgaven." },
strap: "Degrees as cities, schools as chapters, study as migration.", motif: { en: "Memory, theory, institutions, and weather.", fr: "Mémoire, théorie, institutions et météo.", nb: "Minne, teori, institusjoner og vær." },
coverline: "The dossier issue.", samples: { en: ["Villanova as overture","Krakow and the bureaucracy sublime","Kongsberg after experience"], fr: ["Villanova comme ouverture","Cracovie et la bureaucratie sublime","Kongsberg après l'expérience"], nb: ["Villanova som ouverture","Krakow og den sublime byråkratien","Kongsberg etter erfaring"] },
motif: "Memory, theory, institutions, and weather.",
samples: ["Villanova as overture", "Krakow and the bureaucracy sublime", "Kongsberg after experience"],
}, },
{ {
slug: "family", slug: "family", label: "03", title: "Family",
label: "03", summary: { en: "A warmer archive for memory, milestones, and the private weather that keeps the machinery worth running.", fr: "Une archive plus chaleureuse pour la mémoire, les jalons et la météo privée qui donne un sens à la machine.", nb: "Et varmere arkiv for minner, milepæler og det private været som gjør maskineriet verdt å holde i gang." },
title: "Family", tone: { en: "Private-minded, generous, alive.", fr: "Intimiste, généreux, vivant.", nb: "Privat, generøs, levende." },
summary: "A warmer archive for memory, milestones, and the private weather that keeps the machinery worth running.", strap: { en: "The soft archive, still edited like it matters.", fr: "L'archive douce, toujours éditée comme si elle comptait.", nb: "Det myke arkivet, fortsatt redigert som om det betyr noe." },
tone: "Private-minded, generous, alive.", coverline: { en: "Domestic front pages.", fr: "Les premières pages domestiques.", nb: "Hjemlige forsider." },
strap: "The soft archive, still edited like it matters.", motif: { en: "Photographs, notes, kinship, and time.", fr: "Photographies, notes, liens familiaux et temps.", nb: "Fotografier, notater, slektskap og tid." },
coverline: "Domestic front pages.", samples: { en: ["Albums with captions worth reading","Milestones without sentimentality","A household in motion"], fr: ["Albums avec légendes qui valent la peine d'être lues","Jalons sans sentimentalisme","Un foyer en mouvement"], nb: ["Album med bildetekster verdt å lese","Milepæler uten sentimentalitet","Et hushold i bevegelse"] },
motif: "Photographs, notes, kinship, and time.",
samples: ["Albums with captions worth reading", "Milestones without sentimentality", "A household in motion"],
}, },
{ {
slug: "fun-postings", slug: "fun-postings", label: "04", title: "Fun Postings",
label: "04", summary: { en: "Odd notices, cultural flyers, side projects, and the sort of elegant nonsense that deserves proper typesetting.", fr: "Avis insolites, flyers culturels, projets parallèles et ce genre de non-sens élégant qui mérite une typographie soignée.", nb: "Merkelige kunngjøringer, kulturelle flyers, sideprosjekter og den typen elegant nonsens som fortjener ordentlig typografi." },
title: "Fun Postings", tone: { en: "Playful, deadpan, collectible.", fr: "Ludique, pince-sans-rire, collectionnable.", nb: "Leken, deadpan, samlerverdig." },
summary: "Odd notices, cultural flyers, side projects, and the sort of elegant nonsense that deserves proper typesetting.", strap: { en: "The classified page gets strange and starts wearing cologne.", fr: "La page des petites annonces devient étrange et commence à porter de l'eau de Cologne.", nb: "Rubrikkannonsen blir rar og begynner å bruke parfyme." },
tone: "Playful, deadpan, collectible.", coverline: { en: "Useful nonsense, neatly set.", fr: "Non-sens utile, soigneusement présenté.", nb: "Nyttig nonsens, pent satt." },
strap: "The classified page gets strange and starts wearing cologne.", motif: { en: "Flyers, jokes, events, absurd notices.", fr: "Flyers, blagues, événements, avis absurdes.", nb: "Flyers, vitser, arrangementer, absurde kunngjøringer." },
coverline: "Useful nonsense, neatly set.", samples: { en: ["Cultural dispatches","Ridiculous but sincere","Posters for improbable evenings"], fr: ["Dépêches culturelles","Ridicule mais sincère","Affiches pour des soirées improbables"], nb: ["Kulturelle utsendelser","Latterlig men oppriktig","Plakater for usannsynlige kvelder"] },
motif: "Flyers, jokes, events, absurd notices.",
samples: ["Cultural dispatches", "Ridiculous but sincere", "Posters for improbable evenings"],
}, },
{ {
slug: "writing", slug: "writing", label: "05", title: "Writing",
label: "05", summary: { en: "This month's writing desk runs on Boris Vian: novels with trapdoors, Vernon Sullivan weather, Saint-Germain smoke, and bibliography arranged like a contraband route.", fr: "Le bureau d'écriture de ce mois-ci fonctionne sur Boris Vian : romans avec trappes, météo Vernon Sullivan, fumée de Saint-Germain et bibliographie arrangée comme une route de contrebande.", nb: "Denne månedens skrivebord drives av Boris Vian: romaner med fallgruver, Vernon Sullivan-vær, Saint-Germain-røyk og bibliografi arrangert som en smuglerute." },
title: "Writing", tone: { en: "Literary, international, smoky.", fr: "Littéraire, international, enfumé.", nb: "Litterær, internasjonal, røykfylt." },
summary: "This month's writing desk runs on Boris Vian: novels with trapdoors, Vernon Sullivan weather, Saint-Germain smoke, and bibliography arranged like a contraband route.", strap: { en: "A low-lit file on Boris Vian, jazz syntax, and the exact science of glorious exception.", fr: "Un dossier en lumière tamisée sur Boris Vian, la syntaxe jazz et la science exacte de l'exception glorieuse.", nb: "En lavmælt fil om Boris Vian, jazzsyntaks og den eksakte vitenskapen om strålende unntak." },
tone: "Literary, international, smoky.", coverline: { en: "Boris Vian in the side door.", fr: "Boris Vian par la porte dérobée.", nb: "Boris Vian via sidedøren." },
strap: "A low-lit file on Boris Vian, jazz syntax, and the exact science of glorious exception.", motif: { en: "Novels, jazz, pataphysics, counterfeit signatures, and Paris after midnight.", fr: "Romans, jazz, pataphysique, signatures contrefaites et Paris après minuit.", nb: "Romaner, jazz, patafysikk, falske signaturer og Paris etter midnatt." },
coverline: "Boris Vian in the side door.", samples: { en: ["The engineer of exceptions","Five doors into Boris Vian","Why Saint-Germain still leaks into the prose"], fr: ["L'ingénieur des exceptions","Cinq portes vers Boris Vian","Pourquoi Saint-Germain s'infiltre encore dans la prose"], nb: ["Unntakenes ingeniør","Fem dører inn til Boris Vian","Hvorfor Saint-Germain fortsatt siver inn i prosaen"] },
motif: "Novels, jazz, pataphysics, counterfeit signatures, and Paris after midnight.",
samples: [
"The engineer of exceptions",
"Five doors into Boris Vian",
"Why Saint-Germain still leaks into the prose",
],
}, },
{ {
slug: "jazz-music", slug: "jazz-music", label: "06", title: "Jazz and Music",
label: "06", summary: { en: "Listening notes, Kongsberg nights, Caveau memories, and the low-lit logic of serious groove.", fr: "Notes d'écoute, nuits à Kongsberg, souvenirs de Caveau et la logique tamisée du groove sérieux.", nb: "Lyttenotater, Kongsberg-netter, Caveau-minner og den lavmælte logikken til seriøs groove." },
title: "Jazz and Music", tone: { en: "Velvet, brassy, precise.", fr: "Velours, cuivré, précis.", nb: "Fløyel, messing, presist." },
summary: "Listening notes, Kongsberg nights, Caveau memories, and the low-lit logic of serious groove.", strap: { en: "For records, rooms, and players who understand that rhythm is governance.", fr: "Pour les disques, les salles et les musiciens qui comprennent que le rythme est une gouvernance.", nb: "For plater, rom og musikere som forstår at rytme er styring." },
tone: "Velvet, brassy, precise.", coverline: { en: "Blue notes and side doors.", fr: "Blue notes et portes dérobées.", nb: "Blue notes og sidedører." },
strap: "For records, rooms, and players who understand that rhythm is governance.", motif: { en: "Listening, memory, improvisation, timing.", fr: "Écoute, mémoire, improvisation, timing.", nb: "Lytting, minne, improvisasjon, timing." },
coverline: "Blue notes and side doors.", samples: { en: ["Records after midnight","Why groove beats branding","A private history of swing"], fr: ["Disques après minuit","Pourquoi le groove l'emporte sur le branding","Une histoire privée du swing"], nb: ["Plater etter midnatt","Hvorfor groove slår merkevarebygging","En privat historie om swing"] },
motif: "Listening, memory, improvisation, timing.",
samples: ["Records after midnight", "Why groove beats branding", "A private history of swing"],
}, },
{ {
slug: "languages", slug: "languages", label: "07", title: "Languages",
label: "07", summary: { en: "English, French, and Norwegian switching places without losing the joke, the seduction, or the filing detail.", fr: "Anglais, français et norvégien échangent leurs places sans perdre la blague, la séduction ou le détail archivistique.", nb: "Engelsk, fransk og norsk bytter plass uten å miste vitsen, forførelsen eller arkivdetaljen." },
title: "Languages", tone: { en: "Polyglot, sly, welcoming.", fr: "Polyglotte, subtil, accueillant.", nb: "Polyglott, lun, imøtekommende." },
summary: "English, French, and Norwegian switching places without losing the joke, the seduction, or the filing detail.", strap: { en: "A section for mistranslation, seduction, and grammatical diplomacy.", fr: "Une section pour les mauvaises traductions, la séduction et la diplomatie grammaticale.", nb: "En seksjon for feiltolkning, forførelse og grammatisk diplomati." },
tone: "Polyglot, sly, welcoming.", coverline: { en: "The multilingual cabinet.", fr: "Le cabinet multilingue.", nb: "Det flerspråklige kabinettet." },
strap: "A section for mistranslation, seduction, and grammatical diplomacy.", motif: { en: "French, Norwegian, English, and elegant trouble.", fr: "Français, norvégien, anglais et troubles élégants.", nb: "Fransk, norsk, engelsk og elegant trøbbel." },
coverline: "The multilingual cabinet.", samples: { en: ["Words that travel badly","Translation as personality","Syntax with a passport"], fr: ["Mots qui voyagent mal","La traduction comme personnalité","Syntaxe avec passeport"], nb: ["Ord som reiser dårlig","Oversettelse som personlighet","Syntaks med pass"] },
motif: "French, Norwegian, English, and elegant trouble.",
samples: ["Words that travel badly", "Translation as personality", "Syntax with a passport"],
}, },
{ {
slug: "ai-lab", slug: "ai-lab", label: "08", title: "AI Lab",
label: "08", summary: { en: "Private corpora, cited answers, multilingual agents, and practical machine intelligence with actual memory.", fr: "Corpus privés, réponses citées, agents multilingues et intelligence artificielle pratique avec une mémoire réelle.", nb: "Private korpora, siterte svar, flerspråklige agenter og praktisk maskinintelligens med faktisk hukommelse." },
title: "AI Lab", tone: { en: "Forward-looking, grounded, open source friendly.", fr: "Visionnaire, ancré, favorable à l'open source.", nb: "Framtidsrettet, jordnær, vennlig mot åpen kildekode." },
summary: "Private corpora, cited answers, multilingual agents, and practical machine intelligence with actual memory.", strap: { en: "Machine intelligence without the conference lanyard.", fr: "Intelligence artificielle sans le badge de conférence.", nb: "Maskinintelligens uten konferansebånd." },
tone: "Forward-looking, grounded, open source friendly.", coverline: { en: "The atelier for useful futures.", fr: "L'atelier des futurs utiles.", nb: "Atelieret for nyttige fremtider." },
strap: "Machine intelligence without the conference lanyard.", motif: { en: "Prompts, tooling, agentic systems, humane interfaces.", fr: "Prompts, outils, systèmes agentiques, interfaces humaines.", nb: "Prompter, verktøy, agentbaserte systemer, humane grensesnitt." },
coverline: "The atelier for useful futures.", samples: { en: ["AI that can write in-house style","Editorial controls, not gimmicks","Open tools with manners"], fr: ["IA capable d'écrire dans un style maison","Contrôles éditoriaux, pas des gadgets","Outils ouverts avec des manières"], nb: ["AI som kan skrive i husstil","Redaksjonelle kontroller, ikke gimmicker","Åpne verktøy med manerer"] },
motif: "Prompts, tooling, agentic systems, humane interfaces.",
samples: ["AI that can write in-house style", "Editorial controls, not gimmicks", "Open tools with manners"],
}, },
{ {
slug: "norway", slug: "norway", label: "09", title: "Norway",
label: "09", summary: { en: "Kongsberg dispatches, civic reporting, immigrant-family realities, and Norwegian life observed without brochure language.", fr: "Dépêches de Kongsberg, reportages civiques, réalités des familles immigrées et vie norvégienne observée sans langage de brochure.", nb: "Kongsberg-utsendelser, samfunnsrapportering, innvandrerfamilie-realiteter og norsk liv observert uten brosjyrespråk." },
title: "Norway", tone: { en: "Observant, civic, place-aware.", fr: "Observateur, civique, conscient du lieu.", nb: "Observant, samfunnsengasjert, stedbevisst." },
summary: "Kongsberg dispatches, civic reporting, immigrant-family realities, and Norwegian life observed without brochure language.", strap: { en: "A local paper for one town and several realities.", fr: "Un journal local pour une ville et plusieurs réalités.", nb: "En lokalavis for én by og flere realiteter." },
tone: "Observant, civic, place-aware.", coverline: { en: "Kongsberg, correctly observed.", fr: "Kongsberg, correctement observé.", nb: "Kongsberg, korrekt observert." },
strap: "A local paper for one town and several realities.", motif: { en: "Place, policy, weather, Nordic texture.", fr: "Lieu, politique, météo, texture nordique.", nb: "Sted, politikk, vær, nordisk tekstur." },
coverline: "Kongsberg, correctly observed.", samples: { en: ["Silver city dispatches","The civic weather report","Norway beyond brochure language"], fr: ["Dépêches de la ville d'argent","Le bulletin météo civique","La Norvège au-delà du langage des brochures"], nb: ["Sølvby-utsendelser","Den samfunnsmessige værmeldingen","Norge uten brosjyrespråk"] },
motif: "Place, policy, weather, Nordic texture.",
samples: ["Silver city dispatches", "The civic weather report", "Norway beyond brochure language"],
}, },
{ {
slug: "projects", slug: "projects", label: "10", title: "Projects",
label: "10", summary: { en: "Things launched, repaired, modernized, or made slightly dangerous across code, content, venues, and data.", fr: "Choses lancées, réparées, modernisées ou rendues légèrement dangereuses à travers le code, le contenu, les lieux et les données.", nb: "Ting lansert, reparert, modernisert eller gjort litt farlige på tvers av kode, innhold, arenaer og data." },
title: "Projects", tone: { en: "Builder energy, clean receipts.", fr: "Énergie de constructeur, reçus propres.", nb: "Byggerenergi, rene kvitteringer." },
summary: "Things launched, repaired, modernized, or made slightly dangerous across code, content, venues, and data.", strap: { en: "The workshop floor, but art directed.", fr: "Le sol de l'atelier, mais dirigé artistiquement.", nb: "Verkstedgulvet, men kunstnerisk regissert." },
tone: "Builder energy, clean receipts.", coverline: { en: "Built, fixed, and shipped.", fr: "Construit, réparé et expédié.", nb: "Bygget, fikset og sendt." },
strap: "The workshop floor, but art directed.", motif: { en: "Case files, before/after, code and consequence.", fr: "Dossiers, avant/après, code et conséquences.", nb: "Saksmapper, før/etter, kode og konsekvens." },
coverline: "Built, fixed, and shipped.", samples: { en: ["From schema to interface","Deployments with fingerprints","Systems that survived contact"], fr: ["Du schéma à l'interface","Déploiements avec empreintes digitales","Systèmes qui ont survécu au contact"], nb: ["Fra skjema til grensesnitt","Utrullinger med fingeravtrykk","Systemer som overlevde kontakt"] },
motif: "Case files, before/after, code and consequence.",
samples: ["From schema to interface", "Deployments with fingerprints", "Systems that survived contact"],
}, },
{ {
slug: "cv", slug: "cv", label: "11", title: "CV",
label: "11", summary: { en: "The formal record, still elegant, still readable, and never trapped in a dusty PDF.", fr: "Le dossier formel, toujours élégant, toujours lisible et jamais enfermé dans un PDF poussiéreux.", nb: "Den formelle opptegnelsen, fortsatt elegant, fortsatt lesbar og aldri fanget i en støvete PDF." },
title: "CV", tone: { en: "Professional, legible, confident.", fr: "Professionnel, lisible, confiant.", nb: "Profesjonell, lesbar, selvsikker." },
summary: "The formal record, still elegant, still readable, and never trapped in a dusty PDF.", strap: { en: "The biography in pressed clothes.", fr: "La biographie en vêtements pressés.", nb: "Biografien i pressede klær." },
tone: "Professional, legible, confident.", coverline: { en: "A resume that behaves like publishing.", fr: "Un CV qui se comporte comme une publication.", nb: "En CV som oppfører seg som publisering." },
strap: "The biography in pressed clothes.", motif: { en: "Timeline, experience, proof, education.", fr: "Chronologie, expérience, preuves, éducation.", nb: "Tidslinje, erfaring, bevis, utdanning." },
coverline: "A resume that behaves like publishing.", samples: { en: ["Careers across countries","A record without dead language","Credentials with pulse"], fr: ["Carrières à travers les pays","Un dossier sans langage mort","Certifications avec du souffle"], nb: ["Karrierer på tvers av land","En opptegnelse uten dødt språk","Kvalifikasjoner med puls"] },
motif: "Timeline, experience, proof, education.",
samples: ["Careers across countries", "A record without dead language", "Credentials with pulse"],
}, },
]; ];
@@ -165,8 +139,7 @@ export const schoolDossiers: SchoolDossier[] = [
program: "MSc, Innovation and Technology Management", program: "MSc, Innovation and Technology Management",
city: "Kongsberg, Norway", city: "Kongsberg, Norway",
logo: "/images/schools/usn.svg", logo: "/images/schools/usn.svg",
teaser: teaser: { en: "A systems laboratory where engineering discipline and lived experience meet over coffee, models, and unfinished questions.", fr: "Un laboratoire de systèmes où la discipline d'ingénierie et l'expérience vécue se rencontrent autour d'un café, de modèles et de questions inachevées.", nb: "Et systemlaboratorium der ingeniørdisiplin og levd erfaring møtes over kaffe, modeller og uferdige spørsmål." },
"A systems laboratory where engineering discipline and lived experience meet over coffee, models, and unfinished questions.",
}, },
{ {
slug: "jagiellonian", slug: "jagiellonian",
@@ -175,8 +148,7 @@ export const schoolDossiers: SchoolDossier[] = [
program: "European Union Studies", program: "European Union Studies",
city: "Krakow, Poland", city: "Krakow, Poland",
logo: "/images/schools/jagiellonian.svg", logo: "/images/schools/jagiellonian.svg",
teaser: teaser: { en: "History, bureaucracy, Europe, and the old civilizational habit of asking larger questions than the room can comfortably hold.", fr: "Histoire, bureaucratie, Europe et l'ancienne habitude civilisationnelle de poser des questions plus grandes que la salle ne peut confortablement contenir.", nb: "Historie, byråkrati, Europa og den gamle sivilisasjonsvanen med å stille større spørsmål enn rommet komfortabelt kan romme." },
"History, bureaucracy, Europe, and the old civilizational habit of asking larger questions than the room can comfortably hold.",
}, },
{ {
slug: "escp-moore", slug: "escp-moore",
@@ -185,8 +157,7 @@ export const schoolDossiers: SchoolDossier[] = [
program: "International MBA and Master of International Business", program: "International MBA and Master of International Business",
city: "Paris and Columbia, South Carolina", city: "Paris and Columbia, South Carolina",
logo: "/images/schools/escp-moore.svg", logo: "/images/schools/escp-moore.svg",
teaser: teaser: { en: "A dual orbit of French polish and American execution, where commerce learned to travel with style and consequence.", fr: "Une double orbite de raffinement français et d'exécution américaine, où le commerce a appris à voyager avec style et conséquence.", nb: "En dobbel bane av fransk eleganse og amerikansk utførelse, der handel lærte å reise med stil og konsekvens." },
"A dual orbit of French polish and American execution, where commerce learned to travel with style and consequence.",
}, },
{ {
slug: "european-university", slug: "european-university",
@@ -195,8 +166,7 @@ export const schoolDossiers: SchoolDossier[] = [
program: "MBA and Master of International Trade", program: "MBA and Master of International Trade",
city: "Brussels, Belgium", city: "Brussels, Belgium",
logo: "/images/schools/european-university.svg", logo: "/images/schools/european-university.svg",
teaser: teaser: { en: "Trade, movement, and continental ambition, written in the language of deals, trains, and multilingual evenings.", fr: "Commerce, mouvement et ambition continentale, écrits dans le langage des affaires, des trains et des soirées multilingues.", nb: "Handel, bevegelse og kontinental ambisjon, skrevet i språket til avtaler, tog og flerspråklige kvelder." },
"Trade, movement, and continental ambition, written in the language of deals, trains, and multilingual evenings.",
}, },
{ {
slug: "villanova", slug: "villanova",
@@ -205,29 +175,72 @@ export const schoolDossiers: SchoolDossier[] = [
program: "BSc, Business Administration", program: "BSc, Business Administration",
city: "Philadelphia, Pennsylvania", city: "Philadelphia, Pennsylvania",
logo: "/images/schools/villanova.svg", logo: "/images/schools/villanova.svg",
teaser: teaser: { en: "The Main Line chapter, where finance, communication, and character first learned to share the same bandstand.", fr: "Le chapitre de Main Line, où la finance, la communication et le caractère ont appris à partager la même scène.", nb: "Main Line-kapittelet, der finans, kommunikasjon og karakter først lærte å dele samme scene." },
"The Main Line chapter, where finance, communication, and character first learned to share the same bandstand.",
}, },
]; ];
export const fieldNotes = [ export const fieldNotes: Record<LocaleCode, string[]> = {
"Blue Note Logic keeps the machine room full of private AI, cited answers, and document intelligence that behaves like evidence instead of theater.", en: [
"Trivia & Tunes is now a living product story: venue-grade quiz nights, real AI in the game loop, and voice layers warming up for live testing.", "Blue Note Logic keeps the machine room full of private AI, cited answers, and document intelligence that behaves like evidence instead of theater.",
"Do Better Norge keeps the civic file open on family life, immigrant fathers, due process, and the legal weather in contemporary Norway.", "Trivia & Tunes is now a living product story: venue-grade quiz nights, real AI in the game loop, and voice layers warming up for live testing.",
]; "Do Better Norge keeps the civic file open on family life, immigrant fathers, due process, and the legal weather in contemporary Norway.",
],
fr: [
"Blue Note Logic garde la salle des machines remplie d'IA privée, de réponses citées et d'intelligence documentaire qui se comporte comme une preuve plutôt qu'un théâtre.",
"Trivia & Tunes est désormais une histoire de produit vivant : soirées quiz dignes de lieux, véritable IA dans la boucle de jeu, et couches vocales prêtes pour des tests en direct.",
"Do Better Norge garde le dossier civique ouvert sur la vie familiale, les pères immigrés, le droit à un procès équitable et la météo juridique dans la Norvège contemporaine."
],
nb: [
"Blue Note Logic holder maskinrommet fullt av privat AI, siterte svar og dokumentintelligens som oppfører seg som bevis i stedet for teater.",
"Trivia & Tunes er nå en levende produktfortelling: quizkvelder på nivå med arenaer, ekte AI i spillsløyfen, og stemmelag som varmer opp for live-testing.",
"Do Better Norge holder det sivile arkivet åpent om familieliv, innvandrerfedre, rettferdig prosess og det juridiske været i dagens Norge."
],
};
export const editorialPromises = [ export const editorialPromises: Record<LocaleCode, string[]> = {
"Keep the paper light, breathable, and a little dangerous around the edges.", en: [
"Make AI visible as a craft tool, a newsroom instrument, and never a plastic gimmick.", "Keep the paper light, breathable, and a little dangerous around the edges.",
"Treat the CV, the jazz notebook, the civic archive, and the family pages with equal design seriousness.", "Make AI visible as a craft tool, a newsroom instrument, and never a plastic gimmick.",
]; "Treat the CV, the jazz notebook, the civic archive, and the family pages with equal design seriousness.",
],
fr: [
"Garder le papier léger, respirable et un peu dangereux sur les bords.",
"Rendre l'IA visible comme un outil artisanal, un instrument de salle de rédaction, et jamais un gadget en plastique.",
"Traiter le CV, le carnet de jazz, l'archive civique et les pages familiales avec le même sérieux de conception."
],
nb: [
"Hold papiret lett, pustende og litt farlig i kantene.",
"Gjøre AI synlig som et håndverksverktøy, et nyhetsromsinstrument, og aldri en plastgimmick.",
"Behandle CV-en, jazznotatboken, det sivile arkivet og familiesidene med samme designalvor."
],
};
export const coverLines = [ export const coverLines: Record<LocaleCode, string[]> = {
"A pataphysical field paper with jazz smoke in the margins and SQL under the floorboards.", en: [
"Five school dossiers, each signed with a clearly counterfeit blessing and a straight face.", "A pataphysical field paper with jazz smoke in the margins and SQL under the floorboards.",
"AI in the machinery, not sprayed on top like fresh conference cologne.", "Five school dossiers, each signed with a clearly counterfeit blessing and a straight face.",
]; "AI in the machinery, not sprayed on top like fresh conference cologne.",
],
fr: [
"Un papier de terrain pataphysique avec de la fumée de jazz dans les marges et du SQL sous les planchers.",
"Cinq dossiers scolaires, chacun signé avec une bénédiction clairement contrefaite et un visage impassible.",
"IA dans la machinerie, pas vaporisée sur le dessus comme une eau de cologne fraîche de conférence."
],
nb: [
"Et pataphysisk feltpapir med jazzrøyk i margen og SQL under gulvbordene.",
"Fem skoledokumenter, hver signert med en åpenbart falsk velsignelse og et rett ansikt.",
"AI i maskineriet, ikke sprayet på toppen som fersk konferanseparfyme."
],
};
export const sectionCardCta: Record<LocaleCode, string> = {
en: "Open section",
fr: "Ouvrir la section",
nb: "Åpne seksjonen",
};
export function getSectionHref(section: LaunchSection) { export function getSectionHref(section: LaunchSection) {
return section.slug === "education" ? "/education" : `/${section.slug}`; if (section.slug === "education") return "/education";
if (section.slug === "family") return "/family-lab";
return `/${section.slug}`;
} }
+65 -2
View File
@@ -5,18 +5,26 @@ import LocaleCopy from "../components/LocaleCopy.astro";
import LocaleSwitcher from "../components/LocaleSwitcher.astro"; import LocaleSwitcher from "../components/LocaleSwitcher.astro";
import SectionMark from "../components/SectionMark.astro"; import SectionMark from "../components/SectionMark.astro";
import { getSectionHref, launchSections } from "../data/site"; import { getSectionHref, launchSections } from "../data/site";
import { footerCallouts, getChromeCopy, localizedSections, policyLinkCopy } from "../data/locales"; import { footerCallouts, getChromeCopy, localeMeta, localizedSections, policyLinkCopy } from "../data/locales";
interface Props { interface Props {
title?: string; title?: string;
description?: string; description?: string;
lang?: string; lang?: string;
ogImage?: string;
ogType?: "website" | "article";
ogArticlePublishedTime?: string;
ogArticleSection?: string;
} }
const { const {
title = "Dave Gilligan | Blue Note Logic", title = "Dave Gilligan | Blue Note Logic",
description = "A literary, jazzy, technically serious online magazine for writing, consulting, education, languages, family, and AI.", description = "A literary, jazzy, technically serious online magazine for writing, consulting, education, languages, family, and AI.",
lang = "en", lang = "en",
ogImage,
ogType,
ogArticlePublishedTime,
ogArticleSection,
} = Astro.props; } = Astro.props;
const now = new Date(); const now = new Date();
@@ -39,6 +47,10 @@ const issueDate = {
}; };
const pathname = Astro.url.pathname.replace(/\/+$/, "") || "/"; const pathname = Astro.url.pathname.replace(/\/+$/, "") || "/";
const siteUrl = "https://davegilligan.com";
const canonicalUrl = siteUrl + (pathname === "/" ? "" : pathname);
const ogImageAbsolute = siteUrl + (ogImage ?? "/images/og/home.png");
const resolvedOgType = ogType ?? "website";
const activeSlug = pathname === "/" ? "home" : pathname.split("/").filter(Boolean)[0]; const activeSlug = pathname === "/" ? "home" : pathname.split("/").filter(Boolean)[0];
const primarySlugs = ["business", "education", "writing", "jazz-music", "ai-lab", "norway"]; const primarySlugs = ["business", "education", "writing", "jazz-music", "ai-lab", "norway"];
const primaryNav = launchSections.filter((section) => primarySlugs.includes(section.slug)); const primaryNav = launchSections.filter((section) => primarySlugs.includes(section.slug));
@@ -61,16 +73,66 @@ const chromeCopy = getChromeCopy({ activeSlug, issueDate, articleKey });
<meta name="description" content={description} /> <meta name="description" content={description} />
<meta name="generator" content={Astro.generator} /> <meta name="generator" content={Astro.generator} />
<meta name="theme-color" content="#f6f0e1" /> <meta name="theme-color" content="#f6f0e1" />
<link rel="canonical" href={canonicalUrl} />
<meta property="og:site_name" content="Dave Gilligan" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalUrl} />
<meta property="og:type" content={resolvedOgType} />
<meta property="og:image" content={ogImageAbsolute} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content={title} />
<meta property="og:locale" content="en_GB" />
<meta property="og:locale:alternate" content="fr_FR" />
<meta property="og:locale:alternate" content="nb_NO" />
{resolvedOgType === "article" && ogArticlePublishedTime && (
<meta property="article:published_time" content={ogArticlePublishedTime} />
)}
{resolvedOgType === "article" && ogArticleSection && (
<meta property="article:section" content={ogArticleSection} />
)}
<meta property="article:author" content="https://davegilligan.com" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImageAbsolute} />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link <link
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Instrument+Serif:ital@0;1&family=Newsreader:opsz,wght@6..72,400;6..72,500;6..72,600&display=swap" href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Newsreader:opsz,ital,wght@6..72,0,400..700;6..72,1,400..600&family=Special+Elite&family=IBM+Plex+Mono:wght@400;500;600&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
<title>{title}</title> <title>{title}</title>
<script is:inline>
window.op=window.op||function(){var n=[];return new Proxy(function(){arguments.length&&n.push([].slice.call(arguments))},{get:function(t,r){return"q"===r?n:function(){n.push([r].concat([].slice.call(arguments)))}},has:function(t,r){return"q"===r}})}();
(function(){
var cfg={apiUrl:'https://analytics.bluenotelogic.com/api',clientId:'6a6d90f8-66ea-413e-9b3d-9b7278ac6c09',trackScreenViews:true,trackOutgoingLinks:true,trackAttributes:true};
var opLoaded=false;
function initOP(){if(opLoaded)return;opLoaded=true;window.op('init',cfg);var s=document.createElement('script');s.src='https://analytics.bluenotelogic.com/op1.js';s.defer=true;s.async=true;document.head.appendChild(s);}
if(document.documentElement.getAttribute('data-cookie-analytics')==='true'){initOP();}
window.addEventListener('dg:cookie-consent',function(e){var p=e.detail&&e.detail.preference||'';if(p==='all'||p.indexOf('analytics')!==-1){initOP();}});
})();
</script>
</head> </head>
<body> <body>
<div class="locale-bar" role="navigation" aria-label="Language selection">
<div class="locale-bar__inner">
<button type="button" class="locale-bar__btn" data-lang-button="en" aria-pressed="true">
🇬🇧 <span>{localeMeta.en.switcher}</span>
</button>
<span class="locale-bar__sep" aria-hidden="true">·</span>
<button type="button" class="locale-bar__btn" data-lang-button="fr" aria-pressed="false">
🇫🇷 <span>{localeMeta.fr.switcher}</span>
</button>
<span class="locale-bar__sep" aria-hidden="true">·</span>
<button type="button" class="locale-bar__btn" data-lang-button="nb" aria-pressed="false">
🇳🇴 <span>{localeMeta.nb.switcher}</span>
</button>
</div>
</div>
<header class="site-ribbon"> <header class="site-ribbon">
<div class="container site-ribbon__row"> <div class="container site-ribbon__row">
<LocaleCopy copy={chromeCopy.ribbonIssue} /> <LocaleCopy copy={chromeCopy.ribbonIssue} />
@@ -128,6 +190,7 @@ const chromeCopy = getChromeCopy({ activeSlug, issueDate, articleKey });
<p>davegilligan.com</p> <p>davegilligan.com</p>
<strong><LocaleCopy copy={chromeCopy.footerHeadline} /></strong> <strong><LocaleCopy copy={chromeCopy.footerHeadline} /></strong>
<span><LocaleCopy copy={chromeCopy.footerBody} /></span> <span><LocaleCopy copy={chromeCopy.footerBody} /></span>
<span style="font-size:0.78rem;color:var(--ink-whisper);font-family:var(--font-mono);letter-spacing:0.1em;text-transform:uppercase;">Filed under the Subcommission for Multilingual Weather</span>
</div> </div>
<div class="footer-links"> <div class="footer-links">
+15 -17
View File
@@ -1,12 +1,13 @@
--- ---
import { getSectionHref, launchSections } from "../data/site"; import { getSectionHref, launchSections } from "../data/site";
import BaseLayout from "../layouts/BaseLayout.astro"; import BaseLayout from "../layouts/BaseLayout.astro";
import LocaleCopy from "../components/LocaleCopy.astro";
export function getStaticPaths() { export function getStaticPaths() {
return launchSections return launchSections
.filter( .filter(
(section) => (section) =>
!["education", "business", "ai-lab", "jazz-music", "norway", "cv", "projects"].includes(section.slug), !["education", "business", "ai-lab", "jazz-music", "norway", "cv", "projects", "writing"].includes(section.slug),
) )
.map((section) => ({ .map((section) => ({
params: { slug: section.slug }, params: { slug: section.slug },
@@ -19,30 +20,31 @@ const { section } = Astro.props;
const relatedSections = launchSections.filter( const relatedSections = launchSections.filter(
(entry) => (entry) =>
entry.slug !== section.slug && entry.slug !== section.slug &&
!["education", "business", "ai-lab", "jazz-music", "norway", "cv", "projects"].includes(entry.slug), !["education", "business", "ai-lab", "jazz-music", "norway", "cv", "projects", "writing"].includes(entry.slug),
); );
--- ---
<BaseLayout <BaseLayout
title={`${section.title} | Dave Gilligan`} title={`${section.title} | Dave Gilligan`}
description={section.summary} description={section.summary.en}
ogImage={`/images/og/${section.slug}.png`}
> >
<main class="section-page"> <main class="section-page">
<section class="container section-page__hero"> <section class="container section-page__hero">
<div class="section-page__intro"> <div class="section-page__intro">
<span class="eyebrow">Section {section.label}</span> <span class="eyebrow">Section {section.label}</span>
<h1>{section.title}</h1> <h1>{section.title}</h1>
<p class="section-page__strap">{section.strap}</p> <p class="section-page__strap"><LocaleCopy copy={section.strap} /></p>
<p class="section-page__lede">{section.summary}</p> <p class="section-page__lede"><LocaleCopy copy={section.summary} /></p>
</div> </div>
<aside class="panel section-page__cover"> <aside class="panel section-page__cover">
<div class="capsule__kicker"> <div class="capsule__kicker">
<span>Cover line</span> <span>Cover line</span>
<span>{section.tone}</span> <span><LocaleCopy copy={section.tone} /></span>
</div> </div>
<h2>{section.coverline}</h2> <h2><LocaleCopy copy={section.coverline} /></h2>
<p>{section.motif}</p> <p><LocaleCopy copy={section.motif} /></p>
</aside> </aside>
</section> </section>
@@ -53,11 +55,11 @@ const relatedSections = launchSections.filter(
<span>Drafting notes</span> <span>Drafting notes</span>
</div> </div>
<ul class="section-page__samples"> <ul class="section-page__samples">
{section.samples.map((sample, index) => ( {section.samples.en.map((_, i) => (
<li> <li>
<span>{String(index + 1).padStart(2, "0")}</span> <span>{String(i + 1).padStart(2, "0")}</span>
<strong>{sample}</strong> <strong><LocaleCopy copy={{ en: section.samples.en[i], fr: section.samples.fr[i], nb: section.samples.nb[i] }} /></strong>
<p>{section.motif}</p> <p><LocaleCopy copy={section.motif} /></p>
</li> </li>
))} ))}
</ul> </ul>
@@ -73,10 +75,6 @@ const relatedSections = launchSections.filter(
dead blocks, more atmosphere, and copy that behaves as though it expects readers with a dead blocks, more atmosphere, and copy that behaves as though it expects readers with a
memory. memory.
</p> </p>
<p>
Eventually this will be fed from PHP and SQL, with roles, member access, and AI-backed
translation controls. For now it is the visual and editorial north star.
</p>
</article> </article>
</section> </section>
@@ -93,7 +91,7 @@ const relatedSections = launchSections.filter(
<a class="section-related-card" href={getSectionHref(entry)}> <a class="section-related-card" href={getSectionHref(entry)}>
<span>{entry.label}</span> <span>{entry.label}</span>
<strong>{entry.title}</strong> <strong>{entry.title}</strong>
<p>{entry.strap}</p> <p><LocaleCopy copy={entry.strap} /></p>
</a> </a>
))} ))}
</div> </div>
+5 -3
View File
@@ -10,6 +10,7 @@ import {
aiLabWorkflow, aiLabWorkflow,
} from "../data/ai-lab"; } from "../data/ai-lab";
import { ventureDesk } from "../data/profile"; import { ventureDesk } from "../data/profile";
import LocaleCopy from "../components/LocaleCopy.astro";
import BaseLayout from "../layouts/BaseLayout.astro"; import BaseLayout from "../layouts/BaseLayout.astro";
const [gilliganTech, blueNoteLogic] = ventureDesk; const [gilliganTech, blueNoteLogic] = ventureDesk;
@@ -18,6 +19,7 @@ const [gilliganTech, blueNoteLogic] = ventureDesk;
<BaseLayout <BaseLayout
title="AI Lab | Dave Gilligan" title="AI Lab | Dave Gilligan"
description="A fully developed AI Lab issue covering CorpusAI, live use cases, European infrastructure, API examples, and Blue Note Logic implementation work." description="A fully developed AI Lab issue covering CorpusAI, live use cases, European infrastructure, API examples, and Blue Note Logic implementation work."
ogImage="/images/og/ai-lab.png"
> >
<main class="ai-lab-page"> <main class="ai-lab-page">
<section class="container ai-lab-hero"> <section class="container ai-lab-hero">
@@ -27,7 +29,7 @@ const [gilliganTech, blueNoteLogic] = ventureDesk;
<p class="ai-lab-hero__lede">{aiLabHero.lede}</p> <p class="ai-lab-hero__lede">{aiLabHero.lede}</p>
<div class="ai-lab-hero__actions"> <div class="ai-lab-hero__actions">
<a class="button button--dark" href="https://ai.bluenotelogic.com/" target="_blank" rel="noreferrer"> <a class="button button--dark" href="https://bluenotelogic.com" target="_blank" rel="noreferrer">
Visit CorpusAI Visit CorpusAI
</a> </a>
<a class="button button--soft" href="#api-examples">See API examples</a> <a class="button button--soft" href="#api-examples">See API examples</a>
@@ -45,10 +47,10 @@ const [gilliganTech, blueNoteLogic] = ventureDesk;
</div> </div>
<p>{aiLabHero.note}</p> <p>{aiLabHero.note}</p>
<p> <p>
<strong>{blueNoteLogic.name}</strong>: {blueNoteLogic.summary} <strong>{blueNoteLogic.name}</strong>: <LocaleCopy copy={blueNoteLogic.summary} />
</p> </p>
<p> <p>
<strong>{gilliganTech.name}</strong>: {gilliganTech.summary} <strong>{gilliganTech.name}</strong>: <LocaleCopy copy={gilliganTech.summary} />
</p> </p>
</article> </article>
</aside> </aside>
+212
View File
@@ -0,0 +1,212 @@
---
import { aiBubbleArticle, aiBubbleBody, aiBubbleImages, aiBubbleSources } from "../../data/ai-bubble";
import BaseLayout from "../../layouts/BaseLayout.astro";
import LocaleCopy from "../../components/LocaleCopy.astro";
const pageCopy = {
eyebrow: {
en: "Article / Business & Technology",
fr: "Article / Commerce et Technologie",
nb: "Artikkel / Næringsliv og Teknologi",
},
published: { en: "Published", fr: "Publié", nb: "Publisert" },
sidebarH2: {
en: "Field Correspondent",
fr: "Correspondant de terrain",
nb: "Feltkorrespondent",
},
sidebarNote: {
en: "Dave Gilligan builds AI systems professionally and writes about what that vantage point reveals about an industry in the process of valuing itself.",
fr: "Dave Gilligan construit des systèmes d'IA à titre professionnel et écrit sur ce que ce point de vue révèle d'une industrie en train de s'évaluer elle-même.",
nb: "Dave Gilligan bygger AI-systemer profesjonelt og skriver om hva det perspektivet avslører om en industri i ferd med å verdsette seg selv.",
},
fieldReport: { en: "Field report", fr: "Reportage de terrain", nb: "Feltrapport" },
location: { en: "AI & Capital Markets", fr: "IA et marchés financiers", nb: "AI og kapitalmarkeder" },
creditsTitle: { en: "Source Credits", fr: "Sources", nb: "Kildehenvisninger" },
creditsMeta: {
en: "Valuation figures are drawn from publicly reported fundraising rounds and financial press. Image attributions are per Wikimedia Commons licensing terms.",
fr: "Les chiffres de valorisation sont tirés des tours de financement publiquement rapportés et de la presse financière. Les attributions d'images sont conformes aux termes de licence Wikimedia Commons.",
nb: "Verdivurderingstall er hentet fra offentlig rapporterte finansieringsrunder og finanspressen. Bildeattribusjon er i henhold til Wikimedia Commons-lisensvilkår.",
},
};
---
<BaseLayout
title={`${aiBubbleArticle.title} | Dave Gilligan`}
description={aiBubbleArticle.excerpt.en}
ogType="article"
ogImage="/images/articles/ai-bubble/data-centre.jpg"
ogArticlePublishedTime={aiBubbleArticle.publishedAt.replace(' ', 'T') + 'Z'}
ogArticleSection="Business"
>
<main class="jazz-page">
<section class="container jazz-hero">
<div class="jazz-hero__copy">
<span class="eyebrow"><LocaleCopy copy={pageCopy.eyebrow} /></span>
<h1>{aiBubbleArticle.title}</h1>
<p class="jazz-hero__lede"><LocaleCopy copy={aiBubbleArticle.subtitle} /></p>
<p class="jazz-hero__lede"><LocaleCopy copy={aiBubbleArticle.excerpt} /></p>
</div>
<aside class="panel jazz-hero__note">
<div class="capsule__kicker">
<span><LocaleCopy copy={pageCopy.published} /></span>
<span>{aiBubbleArticle.publishedAt.slice(0, 10)}</span>
</div>
<h2><LocaleCopy copy={pageCopy.sidebarH2} /></h2>
<p><LocaleCopy copy={pageCopy.sidebarNote} /></p>
</aside>
</section>
<section class="container">
<article class="panel jazz-article article-prose">
<div class="capsule__kicker">
<span><LocaleCopy copy={pageCopy.fieldReport} /></span>
<span><LocaleCopy copy={pageCopy.location} /></span>
</div>
<div class="article-block">
<figure class="article-fig article-fig--right">
<img src={aiBubbleImages[0].src} alt={aiBubbleImages[0].alt} loading="lazy" />
<figcaption>
<span>{aiBubbleImages[0].credit} / {aiBubbleImages[0].license}</span>
<a href={aiBubbleImages[0].sourceUrl} target="_blank" rel="noreferrer">{aiBubbleImages[0].sourceLabel}</a>
</figcaption>
{aiBubbleImages[0].note && <p class="article-fig__note">{aiBubbleImages[0].note}</p>}
</figure>
<p><LocaleCopy copy={{ en: aiBubbleBody.en[0], fr: aiBubbleBody.fr[0], nb: aiBubbleBody.nb[0] }} /></p>
<p><LocaleCopy copy={{ en: aiBubbleBody.en[1], fr: aiBubbleBody.fr[1], nb: aiBubbleBody.nb[1] }} /></p>
</div>
<div class="article-block">
<figure class="article-fig article-fig--left">
<img src={aiBubbleImages[1].src} alt={aiBubbleImages[1].alt} loading="lazy" />
<figcaption>
<span>{aiBubbleImages[1].credit} / {aiBubbleImages[1].license}</span>
<a href={aiBubbleImages[1].sourceUrl} target="_blank" rel="noreferrer">{aiBubbleImages[1].sourceLabel}</a>
</figcaption>
{aiBubbleImages[1].note && <p class="article-fig__note">{aiBubbleImages[1].note}</p>}
</figure>
<p><LocaleCopy copy={{ en: aiBubbleBody.en[2], fr: aiBubbleBody.fr[2], nb: aiBubbleBody.nb[2] }} /></p>
<p><LocaleCopy copy={{ en: aiBubbleBody.en[3], fr: aiBubbleBody.fr[3], nb: aiBubbleBody.nb[3] }} /></p>
</div>
<div class="article-block">
<figure class="article-fig article-fig--right">
<img src={aiBubbleImages[2].src} alt={aiBubbleImages[2].alt} loading="lazy" />
<figcaption>
<span>{aiBubbleImages[2].credit} / {aiBubbleImages[2].license}</span>
<a href={aiBubbleImages[2].sourceUrl} target="_blank" rel="noreferrer">{aiBubbleImages[2].sourceLabel}</a>
</figcaption>
{aiBubbleImages[2].note && <p class="article-fig__note">{aiBubbleImages[2].note}</p>}
</figure>
<p><LocaleCopy copy={{ en: aiBubbleBody.en[4], fr: aiBubbleBody.fr[4], nb: aiBubbleBody.nb[4] }} /></p>
<p><LocaleCopy copy={{ en: aiBubbleBody.en[5], fr: aiBubbleBody.fr[5], nb: aiBubbleBody.nb[5] }} /></p>
</div>
<div class="article-block">
<figure class="article-fig article-fig--left">
<img src={aiBubbleImages[3].src} alt={aiBubbleImages[3].alt} loading="lazy" />
<figcaption>
<span>{aiBubbleImages[3].credit} / {aiBubbleImages[3].license}</span>
<a href={aiBubbleImages[3].sourceUrl} target="_blank" rel="noreferrer">{aiBubbleImages[3].sourceLabel}</a>
</figcaption>
{aiBubbleImages[3].note && <p class="article-fig__note">{aiBubbleImages[3].note}</p>}
</figure>
<p><LocaleCopy copy={{ en: aiBubbleBody.en[6], fr: aiBubbleBody.fr[6], nb: aiBubbleBody.nb[6] }} /></p>
<p><LocaleCopy copy={{ en: aiBubbleBody.en[7], fr: aiBubbleBody.fr[7], nb: aiBubbleBody.nb[7] }} /></p>
<p><LocaleCopy copy={{ en: aiBubbleBody.en[8], fr: aiBubbleBody.fr[8], nb: aiBubbleBody.nb[8] }} /></p>
</div>
</article>
</section>
<section class="container source-block">
<div class="section-header">
<div class="section-header__title"><LocaleCopy copy={pageCopy.creditsTitle} /></div>
<div class="section-header__meta"><LocaleCopy copy={pageCopy.creditsMeta} /></div>
</div>
<div class="source-list">
{aiBubbleSources.map((source) => (
<article>
<a href={source.href} target="_blank" rel="noreferrer">{source.label}</a>
</article>
))}
</div>
</section>
</main>
</BaseLayout>
<style>
.article-prose {
padding: 1.6rem 2rem;
}
.article-block {
display: flow-root;
margin-top: 0.5rem;
}
.article-fig {
margin: 0.3rem 0 1rem;
overflow: hidden;
border-radius: 1.4rem;
border: 1px solid var(--line);
background: rgba(255, 252, 246, 0.84);
box-shadow: var(--shadow-paper);
}
.article-fig--right {
float: right;
width: 44%;
margin-left: 1.6rem;
margin-bottom: 0.8rem;
}
.article-fig--left {
float: left;
width: 44%;
margin-right: 1.6rem;
margin-bottom: 0.8rem;
}
.article-fig img {
width: 100%;
height: clamp(13rem, 24vw, 21rem);
object-fit: cover;
}
.article-fig figcaption {
display: flex;
flex-wrap: wrap;
gap: 0.4rem 0.8rem;
padding: 0.65rem 0.9rem 0;
font-family: var(--font-mono);
font-size: 0.6rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ink-faint);
}
.article-fig figcaption a {
color: var(--teal);
text-decoration: none;
}
.article-fig__note {
margin: 0.35rem 0.9rem 0.9rem;
color: var(--ink-soft);
line-height: 1.5;
font-size: 0.86rem;
font-style: italic;
}
@media (max-width: 760px) {
.article-fig--right,
.article-fig--left {
float: none;
width: 100%;
margin: 1rem 0;
}
}
</style>
+212
View File
@@ -0,0 +1,212 @@
---
import { borisVianArticle, borisVianBody, borisVianImages, borisVianSources } from "../../data/boris-vian";
import BaseLayout from "../../layouts/BaseLayout.astro";
import LocaleCopy from "../../components/LocaleCopy.astro";
const pageCopy = {
eyebrow: {
en: "Article / Writing & Literature",
fr: "Article / Écriture et Littérature",
nb: "Artikkel / Skriving og Litteratur",
},
published: { en: "Published", fr: "Publié", nb: "Publisert" },
sidebarH2: {
en: "On the Writing Desk",
fr: "Au bureau Écriture",
nb: "På skrivedesken",
},
sidebarNote: {
en: "I write about the things that shaped how I think: music, cities, literature, and the gaps between disciplines where the most interesting ideas live.",
fr: "J'écris sur les choses qui ont façonné ma façon de penser : la musique, les villes, la littérature, et les espaces entre les disciplines où vivent les idées les plus intéressantes.",
nb: "Jeg skriver om det som formet måten jeg tenker på: musikk, byer, litteratur og hullene mellom disiplinene der de mest interessante ideene lever.",
},
articleKicker: { en: "Introduction", fr: "Introduction", nb: "Introduksjon" },
articleSubject: { en: "Boris Vian & 'Pataphysique", fr: "Boris Vian & la 'Pataphysique", nb: "Boris Vian & Patafysikk" },
creditsTitle: { en: "Source Credits", fr: "Sources", nb: "Kildehenvisninger" },
creditsMeta: {
en: "Biographical details drawn from standard reference sources. Image attributions per Wikimedia Commons licensing terms.",
fr: "Détails biographiques tirés de sources de référence standard. Attributions d'images selon les termes de licence Wikimedia Commons.",
nb: "Biografiske detaljer hentet fra standardreferansekilder. Bildeattribusjon i henhold til Wikimedia Commons-lisensvilkår.",
},
};
---
<BaseLayout
title={`${borisVianArticle.title} | Dave Gilligan`}
description={borisVianArticle.excerpt.en}
ogType="article"
ogImage="/images/articles/boris-vian/boris-vian.jpg"
ogArticlePublishedTime={borisVianArticle.publishedAt.replace(' ', 'T') + 'Z'}
ogArticleSection="Writing"
>
<main class="jazz-page">
<section class="container jazz-hero">
<div class="jazz-hero__copy">
<span class="eyebrow"><LocaleCopy copy={pageCopy.eyebrow} /></span>
<h1>{borisVianArticle.title}</h1>
<p class="jazz-hero__lede"><LocaleCopy copy={borisVianArticle.subtitle} /></p>
<p class="jazz-hero__lede"><LocaleCopy copy={borisVianArticle.excerpt} /></p>
</div>
<aside class="panel jazz-hero__note">
<div class="capsule__kicker">
<span><LocaleCopy copy={pageCopy.published} /></span>
<span>{borisVianArticle.publishedAt.slice(0, 10)}</span>
</div>
<h2><LocaleCopy copy={pageCopy.sidebarH2} /></h2>
<p><LocaleCopy copy={pageCopy.sidebarNote} /></p>
</aside>
</section>
<section class="container">
<article class="panel jazz-article article-prose">
<div class="capsule__kicker">
<span><LocaleCopy copy={pageCopy.articleKicker} /></span>
<span><LocaleCopy copy={pageCopy.articleSubject} /></span>
</div>
<div class="article-block">
<figure class="article-fig article-fig--right">
<img src={borisVianImages[0].src} alt={borisVianImages[0].alt} loading="lazy" />
<figcaption>
<span>{borisVianImages[0].credit} / {borisVianImages[0].license}</span>
<a href={borisVianImages[0].sourceUrl} target="_blank" rel="noreferrer">{borisVianImages[0].sourceLabel}</a>
</figcaption>
{borisVianImages[0].note && <p class="article-fig__note">{borisVianImages[0].note}</p>}
</figure>
<p><LocaleCopy copy={{ en: borisVianBody.en[0], fr: borisVianBody.fr[0], nb: borisVianBody.nb[0] }} /></p>
<p><LocaleCopy copy={{ en: borisVianBody.en[1], fr: borisVianBody.fr[1], nb: borisVianBody.nb[1] }} /></p>
</div>
<div class="article-block">
<figure class="article-fig article-fig--left">
<img src={borisVianImages[1].src} alt={borisVianImages[1].alt} loading="lazy" />
<figcaption>
<span>{borisVianImages[1].credit} / {borisVianImages[1].license}</span>
<a href={borisVianImages[1].sourceUrl} target="_blank" rel="noreferrer">{borisVianImages[1].sourceLabel}</a>
</figcaption>
{borisVianImages[1].note && <p class="article-fig__note">{borisVianImages[1].note}</p>}
</figure>
<p><LocaleCopy copy={{ en: borisVianBody.en[2], fr: borisVianBody.fr[2], nb: borisVianBody.nb[2] }} /></p>
<p><LocaleCopy copy={{ en: borisVianBody.en[3], fr: borisVianBody.fr[3], nb: borisVianBody.nb[3] }} /></p>
</div>
<div class="article-block">
<figure class="article-fig article-fig--right">
<img src={borisVianImages[2].src} alt={borisVianImages[2].alt} loading="lazy" />
<figcaption>
<span>{borisVianImages[2].credit} / {borisVianImages[2].license}</span>
<a href={borisVianImages[2].sourceUrl} target="_blank" rel="noreferrer">{borisVianImages[2].sourceLabel}</a>
</figcaption>
{borisVianImages[2].note && <p class="article-fig__note">{borisVianImages[2].note}</p>}
</figure>
<p><LocaleCopy copy={{ en: borisVianBody.en[4], fr: borisVianBody.fr[4], nb: borisVianBody.nb[4] }} /></p>
<p><LocaleCopy copy={{ en: borisVianBody.en[5], fr: borisVianBody.fr[5], nb: borisVianBody.nb[5] }} /></p>
</div>
<div class="article-block">
<figure class="article-fig article-fig--left">
<img src={borisVianImages[3].src} alt={borisVianImages[3].alt} loading="lazy" />
<figcaption>
<span>{borisVianImages[3].credit} / {borisVianImages[3].license}</span>
<a href={borisVianImages[3].sourceUrl} target="_blank" rel="noreferrer">{borisVianImages[3].sourceLabel}</a>
</figcaption>
{borisVianImages[3].note && <p class="article-fig__note">{borisVianImages[3].note}</p>}
</figure>
<p><LocaleCopy copy={{ en: borisVianBody.en[6], fr: borisVianBody.fr[6], nb: borisVianBody.nb[6] }} /></p>
<p><LocaleCopy copy={{ en: borisVianBody.en[7], fr: borisVianBody.fr[7], nb: borisVianBody.nb[7] }} /></p>
<p><LocaleCopy copy={{ en: borisVianBody.en[8], fr: borisVianBody.fr[8], nb: borisVianBody.nb[8] }} /></p>
</div>
</article>
</section>
<section class="container source-block">
<div class="section-header">
<div class="section-header__title"><LocaleCopy copy={pageCopy.creditsTitle} /></div>
<div class="section-header__meta"><LocaleCopy copy={pageCopy.creditsMeta} /></div>
</div>
<div class="source-list">
{borisVianSources.map((source) => (
<article>
<a href={source.href} target="_blank" rel="noreferrer">{source.label}</a>
</article>
))}
</div>
</section>
</main>
</BaseLayout>
<style>
.article-prose {
padding: 1.6rem 2rem;
}
.article-block {
display: flow-root;
margin-top: 0.5rem;
}
.article-fig {
margin: 0.3rem 0 1rem;
overflow: hidden;
border-radius: 1.4rem;
border: 1px solid var(--line);
background: rgba(255, 252, 246, 0.84);
box-shadow: var(--shadow-paper);
}
.article-fig--right {
float: right;
width: 44%;
margin-left: 1.6rem;
margin-bottom: 0.8rem;
}
.article-fig--left {
float: left;
width: 44%;
margin-right: 1.6rem;
margin-bottom: 0.8rem;
}
.article-fig img {
width: 100%;
height: clamp(13rem, 24vw, 21rem);
object-fit: cover;
}
.article-fig figcaption {
display: flex;
flex-wrap: wrap;
gap: 0.4rem 0.8rem;
padding: 0.65rem 0.9rem 0;
font-family: var(--font-mono);
font-size: 0.6rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ink-faint);
}
.article-fig figcaption a {
color: var(--teal);
text-decoration: none;
}
.article-fig__note {
margin: 0.35rem 0.9rem 0.9rem;
color: var(--ink-soft);
line-height: 1.5;
font-size: 0.86rem;
font-style: italic;
}
@media (max-width: 760px) {
.article-fig--right,
.article-fig--left {
float: none;
width: 100%;
margin: 1rem 0;
}
}
</style>
+56 -25
View File
@@ -1,30 +1,67 @@
--- ---
import { jazzArticle, jazzBody, jazzImages, jazzSources } from "../../data/jazz"; import { jazzArticle, jazzBody, jazzImages, jazzSources } from "../../data/jazz";
import BaseLayout from "../../layouts/BaseLayout.astro"; import BaseLayout from "../../layouts/BaseLayout.astro";
import LocaleCopy from "../../components/LocaleCopy.astro";
const pageCopy = {
eyebrow: {
en: "Article / Jazz and Music",
fr: "Article / Jazz et Musique",
nb: "Artikkel / Jazz og Musikk",
},
published: { en: "Published", fr: "Publié", nb: "Publisert" },
sidebarH2: {
en: "Direct article link",
fr: "Lien direct vers l'article",
nb: "Direkte artikkellenke",
},
sidebarNote: {
en: "This is the clean standalone version of the Kongsberg Jazz piece, separate from the wider jazz desk.",
fr: "Voici la version autonome épurée de l'article sur le Jazz de Kongsberg, séparée du bureau jazz général.",
nb: "Dette er den rene frittstående versjonen av Kongsberg Jazz-artikkelen, atskilt fra den bredere jazzdesken.",
},
fieldReport: { en: "Field report", fr: "Reportage de terrain", nb: "Feltrapport" },
location: { en: "Kongsberg x Paris", fr: "Kongsberg x Paris", nb: "Kongsberg x Paris" },
alsoRead: { en: "Also read", fr: "À lire aussi", nb: "Les også" },
jazzDesk: { en: "Jazz desk", fr: "Bureau Jazz", nb: "Jazzdesk" },
crossPromo: {
en: "The wider jazz issue includes venue notes, free-show pointers, and the more magazine-like version of this piece.",
fr: "Le numéro jazz plus large comprend des notes de salle, des indications de spectacles gratuits et la version plus magazine de cet article.",
nb: "Det bredere jazznummeret inkluderer stedsnotater, pekere til gratiskonserter og den mer magasinlignende versjonen av denne artikkelen.",
},
crossPromoCta: { en: "Open Jazz and Music", fr: "Ouvrir Jazz et Musique", nb: "Åpne Jazz og Musikk" },
creditsTitle: { en: "Source Credits", fr: "Sources", nb: "Kildehenvisninger" },
creditsMeta: {
en: "Festival details and venue framing are paraphrased from the official festival and venue sites linked below.",
fr: "Les détails du festival et la présentation des salles sont paraphrasés à partir des sites officiels du festival et des salles liés ci-dessous.",
nb: "Festivaldetaljer og stedsramme er omskrevet fra de offisielle festival- og stedssidene lenket nedenfor.",
},
};
--- ---
<BaseLayout <BaseLayout
title={`${jazzArticle.title} | Dave Gilligan`} title={`${jazzArticle.title} | Dave Gilligan`}
description={jazzArticle.excerpt} description={jazzArticle.excerpt.en}
ogType="article"
ogImage="/images/og/jazz-music.png"
ogArticlePublishedTime={jazzArticle.publishedAt.replace(' ', 'T') + 'Z'}
ogArticleSection="Jazz and Music"
> >
<main class="jazz-page"> <main class="jazz-page">
<section class="container jazz-hero"> <section class="container jazz-hero">
<div class="jazz-hero__copy"> <div class="jazz-hero__copy">
<span class="eyebrow">Article / Jazz and Music</span> <span class="eyebrow"><LocaleCopy copy={pageCopy.eyebrow} /></span>
<h1>{jazzArticle.title}</h1> <h1>{jazzArticle.title}</h1>
<p class="jazz-hero__lede">{jazzArticle.excerpt}</p> <p class="jazz-hero__lede"><LocaleCopy copy={jazzArticle.excerpt} /></p>
</div> </div>
<aside class="panel jazz-hero__note"> <aside class="panel jazz-hero__note">
<div class="capsule__kicker"> <div class="capsule__kicker">
<span>Published</span> <span><LocaleCopy copy={pageCopy.published} /></span>
<span>{jazzArticle.publishedAt.slice(0, 10)}</span> <span>{jazzArticle.publishedAt.slice(0, 10)}</span>
</div> </div>
<h2>Direct article link</h2> <h2><LocaleCopy copy={pageCopy.sidebarH2} /></h2>
<p> <p><LocaleCopy copy={pageCopy.sidebarNote} /></p>
This is the clean standalone version of the Kongsberg Jazz piece, separate from the
wider jazz desk.
</p>
</aside> </aside>
</section> </section>
@@ -44,36 +81,30 @@ import BaseLayout from "../../layouts/BaseLayout.astro";
<section class="container jazz-layout"> <section class="container jazz-layout">
<article class="panel jazz-article"> <article class="panel jazz-article">
<div class="capsule__kicker"> <div class="capsule__kicker">
<span>Field report</span> <span><LocaleCopy copy={pageCopy.fieldReport} /></span>
<span>Kongsberg x Paris</span> <span><LocaleCopy copy={pageCopy.location} /></span>
</div> </div>
{jazzBody.map((paragraph) => ( {jazzBody.en.map((_, i) => (
<p>{paragraph}</p> <p><LocaleCopy copy={{ en: jazzBody.en[i], fr: jazzBody.fr[i], nb: jazzBody.nb[i] }} /></p>
))} ))}
</article> </article>
<aside class="jazz-sidebar"> <aside class="jazz-sidebar">
<article class="panel jazz-sidebar__card"> <article class="panel jazz-sidebar__card">
<div class="capsule__kicker"> <div class="capsule__kicker">
<span>Also read</span> <span><LocaleCopy copy={pageCopy.alsoRead} /></span>
<span>Jazz desk</span> <span><LocaleCopy copy={pageCopy.jazzDesk} /></span>
</div> </div>
<p> <p><LocaleCopy copy={pageCopy.crossPromo} /></p>
The wider jazz issue includes venue notes, free-show pointers, and the more <a class="button button--dark" href="/jazz-music"><LocaleCopy copy={pageCopy.crossPromoCta} /></a>
magazine-like version of this piece.
</p>
<a class="button button--dark" href="/jazz-music">Open Jazz and Music</a>
</article> </article>
</aside> </aside>
</section> </section>
<section class="container source-block"> <section class="container source-block">
<div class="section-header"> <div class="section-header">
<div class="section-header__title">Source Credits</div> <div class="section-header__title"><LocaleCopy copy={pageCopy.creditsTitle} /></div>
<div class="section-header__meta"> <div class="section-header__meta"><LocaleCopy copy={pageCopy.creditsMeta} /></div>
Festival details and venue framing are paraphrased from the official festival and venue
sites linked below.
</div>
</div> </div>
<div class="source-list"> <div class="source-list">
+10 -5
View File
@@ -7,18 +7,23 @@ import {
norwaySources, norwaySources,
} from "../../data/norway"; } from "../../data/norway";
import BaseLayout from "../../layouts/BaseLayout.astro"; import BaseLayout from "../../layouts/BaseLayout.astro";
import LocaleCopy from "../../components/LocaleCopy.astro";
--- ---
<BaseLayout <BaseLayout
title={`${norwayArticle.title} | Dave Gilligan`} title={`${norwayArticle.title} | Dave Gilligan`}
description={norwayArticle.excerpt} description={norwayArticle.excerpt.en}
ogType="article"
ogImage="/images/og/norway.png"
ogArticlePublishedTime={norwayArticle.publishedAt.replace(' ', 'T') + 'Z'}
ogArticleSection="Norway"
> >
<main class="norway-page"> <main class="norway-page">
<section class="container norway-hero norway-hero--article"> <section class="container norway-hero norway-hero--article">
<div class="norway-hero__copy"> <div class="norway-hero__copy">
<span class="eyebrow">Article / Norway desk</span> <span class="eyebrow">Article / Norway desk</span>
<h1>{norwayArticle.title}</h1> <h1>{norwayArticle.title}</h1>
<p class="norway-hero__lede">{norwayArticle.excerpt}</p> <p class="norway-hero__lede"><LocaleCopy copy={norwayArticle.excerpt} /></p>
</div> </div>
<aside class="panel norway-hero__note"> <aside class="panel norway-hero__note">
@@ -197,7 +202,7 @@ import BaseLayout from "../../layouts/BaseLayout.astro";
<article class="panel norway-casebook__item"> <article class="panel norway-casebook__item">
<span>{item.date}</span> <span>{item.date}</span>
<h3>{item.title}</h3> <h3>{item.title}</h3>
<p>{item.significance}</p> <p><LocaleCopy copy={item.significance} /></p>
<a href={item.href} target="_blank" rel="noreferrer">{item.sourceLabel}</a> <a href={item.href} target="_blank" rel="noreferrer">{item.sourceLabel}</a>
</article> </article>
))} ))}
@@ -216,9 +221,9 @@ import BaseLayout from "../../layouts/BaseLayout.astro";
<div class="norway-network__grid"> <div class="norway-network__grid">
{norwayOrganizations.map((organization) => ( {norwayOrganizations.map((organization) => (
<a class="norway-network__card" href={organization.href} target="_blank" rel="noreferrer"> <a class="norway-network__card" href={organization.href} target="_blank" rel="noreferrer">
<span>{organization.strap}</span> <span><LocaleCopy copy={organization.strap} /></span>
<strong>{organization.name}</strong> <strong>{organization.name}</strong>
<p>{organization.summary}</p> <p><LocaleCopy copy={organization.summary} /></p>
</a> </a>
))} ))}
</div> </div>
@@ -6,18 +6,23 @@ import {
projectsSources, projectsSources,
} from "../../data/projects"; } from "../../data/projects";
import BaseLayout from "../../layouts/BaseLayout.astro"; import BaseLayout from "../../layouts/BaseLayout.astro";
import LocaleCopy from "../../components/LocaleCopy.astro";
--- ---
<BaseLayout <BaseLayout
title={`${projectsArticle.title} | Dave Gilligan`} title={`${projectsArticle.title} | Dave Gilligan`}
description={projectsArticle.excerpt} description={projectsArticle.excerpt.en}
ogType="article"
ogImage="/images/og/projects.png"
ogArticlePublishedTime={projectsArticle.publishedAt.replace(' ', 'T') + 'Z'}
ogArticleSection="Projects"
> >
<main class="projects-page"> <main class="projects-page">
<section class="container projects-hero"> <section class="container projects-hero">
<div class="projects-hero__copy"> <div class="projects-hero__copy">
<span class="eyebrow">Article / Projects desk</span> <span class="eyebrow">Article / Projects desk</span>
<h1>{projectsArticle.title}</h1> <h1>{projectsArticle.title}</h1>
<p class="projects-hero__lede">{projectsArticle.excerpt}</p> <p class="projects-hero__lede"><LocaleCopy copy={projectsArticle.excerpt} /></p>
</div> </div>
<aside class="panel projects-hero__note"> <aside class="panel projects-hero__note">
@@ -51,8 +56,8 @@ import BaseLayout from "../../layouts/BaseLayout.astro";
<span>April dispatch</span> <span>April dispatch</span>
<span>Music trivia x Blue Note Rhino</span> <span>Music trivia x Blue Note Rhino</span>
</div> </div>
{projectsArticleBody.map((paragraph) => ( {projectsArticleBody.en.map((_, i) => (
<p>{paragraph}</p> <p><LocaleCopy copy={{ en: projectsArticleBody.en[i], fr: projectsArticleBody.fr[i], nb: projectsArticleBody.nb[i] }} /></p>
))} ))}
</article> </article>
+31
View File
@@ -1,13 +1,44 @@
--- ---
import LocaleCopy from "../components/LocaleCopy.astro";
import BusinessIssue from "../components/BusinessIssue.jsx"; import BusinessIssue from "../components/BusinessIssue.jsx";
import BaseLayout from "../layouts/BaseLayout.astro"; import BaseLayout from "../layouts/BaseLayout.astro";
const copy = {
kicker: { en: "Latest dispatch", fr: "Dernière dépêche", nb: "Siste depeche" },
date: { en: "May 2026", fr: "Mai 2026", nb: "Mai 2026" },
title: { en: "The Science of Imaginary Revenues", fr: "La Science des Revenus Imaginaires", nb: "Vitenskapen om Imaginære Inntekter" },
subtitle: {
en: "A Field Study of the AI Valuation Ecosystem",
fr: "Une étude de terrain sur l'écosystème de valorisation de l'IA",
nb: "En feltstudie av AI-vurderingsøkosystemet",
},
body: {
en: "From inside the machine that builds the machines: a correspondent's notes on circular capital, NVIDIA's golden shovel, and the dot-com rhyme that the AI industry insists it is not repeating.",
fr: "De l'intérieur de la machine qui construit les machines : les notes d'un correspondant sur le capital circulaire, la pelle dorée de NVIDIA, et la rime dot-com que l'industrie de l'IA insiste à ne pas répéter.",
nb: "Fra innsiden av maskinen som bygger maskinene: en korrespondents notater om sirkulær kapital, NVIDIAs gylne spade, og dot-com-rimet som AI-industrien insisterer på ikke å gjenta.",
},
cta: { en: "Read the field report", fr: "Lire le rapport de terrain", nb: "Les feltrapporten" },
};
--- ---
<BaseLayout <BaseLayout
title="Business Desk | Dave Gilligan" title="Business Desk | Dave Gilligan"
description="A live business desk fed from the CMS: agentic AI, labour, Queneau, Ionesco, and the anti-buzzword management of imaginary solutions." description="A live business desk fed from the CMS: agentic AI, labour, Queneau, Ionesco, and the anti-buzzword management of imaginary solutions."
ogImage="/images/og/business.png"
> >
<main class="business-page"> <main class="business-page">
<section class="container jazz-layout" style="margin-bottom: 0; padding-bottom: 0;">
<article class="panel jazz-article" style="margin-bottom: var(--space-md, 1.5rem);">
<div class="capsule__kicker">
<span><LocaleCopy copy={copy.kicker} /></span>
<span><LocaleCopy copy={copy.date} /></span>
</div>
<h2 style="margin-top: 0.5rem;"><LocaleCopy copy={copy.title} /></h2>
<p style="font-style: italic; margin-bottom: 0.75rem;"><LocaleCopy copy={copy.subtitle} /></p>
<p><LocaleCopy copy={copy.body} /></p>
<a class="button button--dark" href="/articles/ai-bubble-2026/"><LocaleCopy copy={copy.cta} /></a>
</article>
</section>
<BusinessIssue client:load /> <BusinessIssue client:load />
</main> </main>
</BaseLayout> </BaseLayout>
+6 -12
View File
@@ -5,7 +5,8 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<BaseLayout <BaseLayout
title="Cookie Desk | Dave Gilligan" title="Cookie Desk | Dave Gilligan"
description="Draft cookie notice for the localhost multilingual build, covering essential cookies, optional analytics, and future embedded media consent." description="Cookie notice for davegilligan.com: essential cookies only by default, with optional analytics and embedded media consent on request."
ogImage="/images/og/home.png"
> >
<main class="policy-page"> <main class="policy-page">
<section class="policy-hero container"> <section class="policy-hero container">
@@ -30,9 +31,9 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<p class="policy-hero__lede"> <p class="policy-hero__lede">
<LocaleCopy <LocaleCopy
copy={{ copy={{
en: "The local build now uses a consent pattern meant for a proper EU launch: essential cookies can run immediately, while analytics and third-party embeds stay off until a visitor chooses otherwise.", en: "This site uses a consent-first pattern for EU visitors: essential cookies run immediately, while analytics and third-party embeds stay off until a visitor chooses otherwise.",
fr: "La version locale utilise desormais un schema de consentement pense pour un vrai lancement europeen : les cookies essentiels peuvent fonctionner immediatement, tandis que les analyses et les integrations tierces restent desactivees tant que le visiteur ne choisit pas autre chose.", fr: "Ce site utilise un schema de consentement prioritaire pour les visiteurs europeens : les cookies essentiels fonctionnent immediatement, tandis que les analyses et les integrations tierces restent desactivees tant que le visiteur ne choisit pas autre chose.",
nb: "Den lokale byggen bruker na et samtykkemonster laget for en ordentlig EU-lansering: essensielle cookies kan kjore med en gang, mens analyse og tredjepartsinnbygginger forblir av til en besokende velger noe annet.", nb: "Dette nettstedet bruker et samtykke-forst-monster for EU-besokende: essensielle cookies kjorer med en gang, mens analyse og tredjepartsinnbygginger forblir av til en besokende velger noe annet.",
}} }}
/> />
</p> </p>
@@ -89,14 +90,7 @@ import BaseLayout from "../layouts/BaseLayout.astro";
}} /></p> }} /></p>
</article> </article>
<article class="policy-block policy-block--full">
<h2><LocaleCopy copy={{ en: "Before public launch", fr: "Avant lancement public", nb: "For offentlig lansering" }} /></h2>
<p><LocaleCopy copy={{
en: "This page is still a draft. Before deployment we should review every script, font, embed, analytics tool, and cookie lifetime against the final production stack, then update the policy and banner copy to match the exact live behavior.",
fr: "Cette page reste un brouillon. Avant de deployer, il faudra verifier chaque script, police, integration, outil d'analyse et duree de vie des cookies par rapport a la pile de production finale, puis mettre a jour la politique et la banniere pour correspondre au comportement reel.",
nb: "Denne siden er fortsatt et utkast. For utrulling bor vi ga gjennom hvert script, hver font, hver innbygging, hvert analyseverktoy og hver cookielevetid mot den endelige produksjonsstakken, og deretter oppdatere policyen og bannerteksten slik at de matcher den faktiske adferden.",
}} /></p>
</article>
</section> </section>
</main> </main>
</BaseLayout> </BaseLayout>
+14 -12
View File
@@ -9,6 +9,7 @@ import {
cvTimeline, cvTimeline,
} from "../data/cv"; } from "../data/cv";
import BaseLayout from "../layouts/BaseLayout.astro"; import BaseLayout from "../layouts/BaseLayout.astro";
import LocaleCopy from "../components/LocaleCopy.astro";
const currentMandates = cvMandates.slice(0, 3); const currentMandates = cvMandates.slice(0, 3);
const educationSlice = schoolDossiers.slice(0, 5); const educationSlice = schoolDossiers.slice(0, 5);
@@ -17,13 +18,14 @@ const educationSlice = schoolDossiers.slice(0, 5);
<BaseLayout <BaseLayout
title="CV | Dave Gilligan" title="CV | Dave Gilligan"
description="A custom CV desk covering Dave Gilligan's current ventures, earlier finance and reinsurance work, education route, and the operating themes tying them together." description="A custom CV desk covering Dave Gilligan's current ventures, earlier finance and reinsurance work, education route, and the operating themes tying them together."
ogImage="/images/og/cv.png"
> >
<main class="cv-page"> <main class="cv-page">
<section class="container cv-hero"> <section class="container cv-hero">
<div class="cv-hero__copy"> <div class="cv-hero__copy">
<span class="eyebrow">{cvHero.eyebrow}</span> <span class="eyebrow"><LocaleCopy copy={cvHero.eyebrow} /></span>
<h1>{cvHero.title}</h1> <h1><LocaleCopy copy={cvHero.title} /></h1>
<p class="cv-hero__lede">{cvHero.lede}</p> <p class="cv-hero__lede"><LocaleCopy copy={cvHero.lede} /></p>
<div class="cv-hero__actions"> <div class="cv-hero__actions">
<a class="button button--dark" href="#career-record">Read the chronology</a> <a class="button button--dark" href="#career-record">Read the chronology</a>
@@ -38,13 +40,13 @@ const educationSlice = schoolDossiers.slice(0, 5);
</div> </div>
<p class="cv-poster__eyebrow">Bridgework</p> <p class="cv-poster__eyebrow">Bridgework</p>
<h2>Finance, systems, language, and advocacy on one line.</h2> <h2>Finance, systems, language, and advocacy on one line.</h2>
<p>{cvHero.note}</p> <p><LocaleCopy copy={cvHero.note} /></p>
<div class="cv-poster__grid"> <div class="cv-poster__grid">
{cvHighlights.map((item) => ( {cvHighlights.en.map((_, i) => (
<article> <article>
<span>Signal</span> <span>Signal</span>
<p>{item}</p> <p><LocaleCopy copy={{ en: cvHighlights.en[i], fr: cvHighlights.fr[i], nb: cvHighlights.nb[i] }} /></p>
</article> </article>
))} ))}
</div> </div>
@@ -68,8 +70,8 @@ const educationSlice = schoolDossiers.slice(0, 5);
<span>{mandate.location}</span> <span>{mandate.location}</span>
</div> </div>
<h2>{mandate.org}</h2> <h2>{mandate.org}</h2>
<p class="cv-mandate__summary">{mandate.summary}</p> <p class="cv-mandate__summary"><LocaleCopy copy={mandate.summary} /></p>
<p>{mandate.detail}</p> <p><LocaleCopy copy={mandate.detail} /></p>
<a href={mandate.sourceUrl} target="_blank" rel="noreferrer"> <a href={mandate.sourceUrl} target="_blank" rel="noreferrer">
Source: {mandate.sourceLabel} Source: {mandate.sourceLabel}
</a> </a>
@@ -100,11 +102,11 @@ const educationSlice = schoolDossiers.slice(0, 5);
<strong>{role.role}</strong> <strong>{role.role}</strong>
</div> </div>
<p class="cv-role__summary">{role.summary}</p> <p class="cv-role__summary"><LocaleCopy copy={role.summary} /></p>
<ul> <ul>
{role.bullets.map((bullet) => ( {role.bullets.en.map((_, i) => (
<li>{bullet}</li> <li><LocaleCopy copy={{ en: role.bullets.en[i], fr: role.bullets.fr[i], nb: role.bullets.nb[i] }} /></li>
))} ))}
</ul> </ul>
</div> </div>
@@ -120,7 +122,7 @@ const educationSlice = schoolDossiers.slice(0, 5);
</div> </div>
{cvSkillTracks.map((track) => ( {cvSkillTracks.map((track) => (
<section> <section>
<h3>{track.label}</h3> <h3><LocaleCopy copy={track.label} /></h3>
<ul> <ul>
{track.items.map((item) => ( {track.items.map((item) => (
<li>{item}</li> <li>{item}</li>
+44 -10
View File
@@ -1,34 +1,68 @@
--- ---
import LocaleCopy from "../components/LocaleCopy.astro";
import SchoolDossier from "../components/SchoolDossier.astro"; import SchoolDossier from "../components/SchoolDossier.astro";
import { schoolDossiers } from "../data/education"; import { schoolDossiers } from "../data/education";
import BaseLayout from "../layouts/BaseLayout.astro"; import BaseLayout from "../layouts/BaseLayout.astro";
const copy = {
eyebrow: {
en: "Special issue / school dossiers",
fr: "Numéro spécial / dossiers écoles",
nb: "Spesialnummer / skoledossierer",
},
h1: {
en: "The schools, rewritten as a magazine of migration, trade, weather, and nerve.",
fr: "Les écoles, réécrites comme un magazine de migration, commerce, météo et courage.",
nb: "Skolene, omskrevet som et magasin om migrasjon, handel, vær og nerve.",
},
lede: {
en: "This issue treats each school as a chapter in style, place, and formation. The quotes are invented on purpose, signed in faux Boris or faux Vernon mode, and labeled so nobody mistakes the joke for scholarship.",
fr: "Ce numéro traite chaque école comme un chapitre de style, de lieu et de formation. Les citations sont inventées délibérément, signées en mode Boris ou Vernon apocryphes, et étiquetées pour qu'on ne prenne pas la blague pour de l'érudition.",
nb: "Dette nummeret behandler hver skole som et kapittel i stil, sted og formasjon. Sitatene er bevisst oppfunnet, signert i apokryf Boris- eller Vernon-stil, og merket så ingen tar spøken for kunnskap.",
},
editorialNote: { en: "Editorial note", fr: "Note éditoriale", nb: "Redaksjonell note" },
draft: { en: "Draft 1", fr: "Brouillon 1", nb: "Utkast 1" },
bullets: {
en: [
"All faux signatures are clearly marked as invented.",
"All images are openly licensed or used as contextual illustrations with attribution.",
],
fr: [
"Toutes les fausses signatures sont clairement marquées comme inventées.",
"Toutes les images sont sous licence libre ou utilisées comme illustrations contextuelles avec attribution.",
],
nb: [
"Alle apokryfe signaturer er tydelig merket som oppfunnet.",
"Alle bilder er åpent lisensiert eller brukt som kontekstuelle illustrasjoner med kreditering.",
],
},
};
--- ---
<BaseLayout <BaseLayout
title="Education Dossiers | Dave Gilligan" title="Education Dossiers | Dave Gilligan"
description="Five school dossiers written as a literary magazine issue, with attributed imagery and clearly labeled faux Boris Vian and Vernon Sullivan signatures." description="Five school dossiers written as a literary magazine issue, with attributed imagery and clearly labeled faux Boris Vian and Vernon Sullivan signatures."
ogImage="/images/og/education.png"
> >
<main class="education-page"> <main class="education-page">
<section class="education-hero container"> <section class="education-hero container">
<div class="education-hero__copy"> <div class="education-hero__copy">
<span class="eyebrow">Special issue / school dossiers</span> <span class="eyebrow"><LocaleCopy copy={copy.eyebrow} /></span>
<h1>The schools, rewritten as a magazine of migration, trade, weather, and nerve.</h1> <h1><LocaleCopy copy={copy.h1} /></h1>
<p class="education-hero__lede"> <p class="education-hero__lede">
This issue treats each school as a chapter in style, place, and formation. The quotes <LocaleCopy copy={copy.lede} />
are invented on purpose, signed in faux Boris or faux Vernon mode, and labeled so nobody
mistakes the joke for scholarship.
</p> </p>
</div> </div>
<aside class="education-hero__note panel"> <aside class="education-hero__note panel">
<div class="capsule__kicker"> <div class="capsule__kicker">
<span>Editorial note</span> <span><LocaleCopy copy={copy.editorialNote} /></span>
<span>Draft 1</span> <span><LocaleCopy copy={copy.draft} /></span>
</div> </div>
<ul> <ul>
<li>All faux signatures are clearly marked as invented.</li> {copy.bullets.en.map((_, i) => (
<li>All images are openly licensed or used as contextual stand-ins with attribution.</li> <li><LocaleCopy copy={{ en: copy.bullets.en[i], fr: copy.bullets.fr[i], nb: copy.bullets.nb[i] }} /></li>
<li>Exact campus replacements can be swapped in once you approve or supply them.</li> ))}
</ul> </ul>
</aside> </aside>
</section> </section>
+1
View File
@@ -33,6 +33,7 @@ const namedPhotos = photos.filter((photo) => photo.namedFile).slice(0, 12);
<BaseLayout <BaseLayout
title="Family Laboratory | Dave Gilligan" title="Family Laboratory | Dave Gilligan"
description="A bright pataphysical family-atlas test page built from the Facebook archive, with a custom editorial collage and a full lightbox for the cleaned photo set." description="A bright pataphysical family-atlas test page built from the Facebook archive, with a custom editorial collage and a full lightbox for the cleaned photo set."
ogImage="/images/og/family-lab.png"
> >
<main class="family-lab-page"> <main class="family-lab-page">
<section class="container family-lab-hero"> <section class="container family-lab-hero">
+28 -56
View File
@@ -1,17 +1,15 @@
--- ---
import SectionCard from "../components/SectionCard.astro"; import SectionCard from "../components/SectionCard.astro";
import EditionConsole from "../components/EditionConsole.jsx"; import LocaleCopy from "../components/LocaleCopy.astro";
import { import {
aiLabCapabilities, aiLabCapabilities,
aiLabSources, aiLabSources,
collageImages, collageImages,
routeStops,
ventureDesk, ventureDesk,
ventureSignals, ventureSignals,
} from "../data/profile"; } from "../data/profile";
import { import {
editorialPromises, editorialPromises,
fieldNotes,
getSectionHref, getSectionHref,
hero, hero,
launchSections, launchSections,
@@ -30,16 +28,17 @@ const projects = launchSections.find((section) => section.slug === "projects");
<BaseLayout <BaseLayout
title="Dave Gilligan | Hybrid IT, AI, Jazz, and Literary Magazine" title="Dave Gilligan | Hybrid IT, AI, Jazz, and Literary Magazine"
description="A hybrid hi-tech and retro editorial homepage for Dave Gilligan, weaving AI systems, jazz, writing, languages, advocacy, and lived geography into a magazine-like personal site." description="A hybrid hi-tech and retro editorial homepage for Dave Gilligan, weaving AI systems, jazz, writing, languages, advocacy, and lived geography into a magazine-like personal site."
ogImage="/images/og/home.png"
> >
<main class="page-shell page-shell--cover"> <main class="page-shell page-shell--cover">
<section class="cover-hero"> <section class="cover-hero">
<div class="container cover-hero__inner"> <div class="container cover-hero__inner">
<article class="cover-hero__copy"> <article class="cover-hero__copy">
<span class="eyebrow eyebrow--cover">{hero.kicker}</span> <span class="eyebrow eyebrow--cover"><LocaleCopy copy={hero.kicker} /></span>
<p class="cover-hero__brand">davegilligan.com</p> <p class="cover-hero__brand">davegilligan.com</p>
<h1>{hero.title}</h1> <h1><LocaleCopy copy={hero.title} /></h1>
<p class="cover-hero__lede">{hero.lede}</p> <p class="cover-hero__lede"><LocaleCopy copy={hero.lede} /></p>
<p class="cover-hero__sublede">{hero.sublede}</p> <p class="cover-hero__sublede"><LocaleCopy copy={hero.sublede} /></p>
<div class="cover-hero__actions"> <div class="cover-hero__actions">
<a class="button button--dark" href="#venture-desk">Enter the front page</a> <a class="button button--dark" href="#venture-desk">Enter the front page</a>
@@ -49,13 +48,13 @@ const projects = launchSections.find((section) => section.slug === "projects");
<div class="cover-hero__billboard"> <div class="cover-hero__billboard">
<div> <div>
<span>Writing desk</span> <span>Writing desk</span>
<strong>{writing?.coverline}</strong> {writing && <strong><LocaleCopy copy={writing.coverline} /></strong>}
<p>{writing?.strap}</p> {writing && <p><LocaleCopy copy={writing.strap} /></p>}
</div> </div>
<div> <div>
<span>Jazz desk</span> <span>Jazz desk</span>
<strong>{jazzMusic?.coverline}</strong> {jazzMusic && <strong><LocaleCopy copy={jazzMusic.coverline} /></strong>}
<p>{jazzMusic?.strap}</p> {jazzMusic && <p><LocaleCopy copy={jazzMusic.strap} /></p>}
</div> </div>
</div> </div>
</article> </article>
@@ -65,43 +64,16 @@ const projects = launchSections.find((section) => section.slug === "projects");
{collageImages.map((image, index) => ( {collageImages.map((image, index) => (
<figure class={`cover-collage__frame cover-collage__frame--${index + 1}`}> <figure class={`cover-collage__frame cover-collage__frame--${index + 1}`}>
<img src={image.src} alt={image.alt} loading="lazy" /> <img src={image.src} alt={image.alt} loading="lazy" />
<figcaption>{image.caption}</figcaption> <figcaption><LocaleCopy copy={image.caption} /></figcaption>
</figure> </figure>
))} ))}
</div> </div>
<div class="panel cover-console">
<EditionConsole client:load />
</div>
</aside> </aside>
</div> </div>
</section> </section>
<section class="container route-tape">
{routeStops.map((stop) => (
<span>
<strong>{stop.place}</strong>
<em>{stop.note}</em>
</span>
))}
</section>
<section class="container frontline">
{fieldNotes.map((note) => (
<span>{note}</span>
))}
</section>
<section id="venture-desk" class="container venture-desk"> <section id="venture-desk" class="container venture-desk">
<div class="section-header section-header--tight">
<div class="section-header__title">Four Desks, One Signature</div>
<div class="section-header__meta">
This front page is not a brochure. It is the running file on the real occupations:
private AI, field consulting, jazz-adjacent live products, advocacy, and the long
European afterglow of the schools.
</div>
</div>
<div class="venture-desk__grid"> <div class="venture-desk__grid">
{ventureDesk.map((venture) => ( {ventureDesk.map((venture) => (
<article class="panel venture-story"> <article class="panel venture-story">
@@ -112,11 +84,11 @@ const projects = launchSections.find((section) => section.slug === "projects");
</div> </div>
<p class="venture-story__label">{venture.label}</p> <p class="venture-story__label">{venture.label}</p>
<h2>{venture.name}</h2> <h2>{venture.name}</h2>
<p class="venture-story__summary">{venture.summary}</p> <p class="venture-story__summary"><LocaleCopy copy={venture.summary} /></p>
<p class="venture-story__detail">{venture.detail}</p> <p class="venture-story__detail"><LocaleCopy copy={venture.detail} /></p>
<ul> <ul>
{venture.highlights.map((item) => ( {venture.highlights.en.map((_, i) => (
<li>{item}</li> <li><LocaleCopy copy={{ en: venture.highlights.en[i], fr: venture.highlights.fr[i], nb: venture.highlights.nb[i] }} /></li>
))} ))}
</ul> </ul>
<p class="venture-story__source"> <p class="venture-story__source">
@@ -136,9 +108,9 @@ const projects = launchSections.find((section) => section.slug === "projects");
rel={signal.external ? "noreferrer" : undefined} rel={signal.external ? "noreferrer" : undefined}
> >
<img src={signal.imageSrc} alt={signal.imageAlt} loading="lazy" /> <img src={signal.imageSrc} alt={signal.imageAlt} loading="lazy" />
<span>{signal.strap}</span> <span><LocaleCopy copy={signal.strap} /></span>
<strong>{signal.name}</strong> <strong>{signal.name}</strong>
<p>{signal.summary}</p> <p><LocaleCopy copy={signal.summary} /></p>
<small>{signal.imageNote}</small> <small>{signal.imageNote}</small>
</a> </a>
))} ))}
@@ -167,8 +139,8 @@ const projects = launchSections.find((section) => section.slug === "projects");
<div class="ai-observatory__list"> <div class="ai-observatory__list">
{aiLabCapabilities.map((capability) => ( {aiLabCapabilities.map((capability) => (
<article> <article>
<span>{capability.title}</span> <span><LocaleCopy copy={capability.title} /></span>
<p>{capability.summary}</p> <p><LocaleCopy copy={capability.summary} /></p>
</article> </article>
))} ))}
</div> </div>
@@ -179,7 +151,7 @@ const projects = launchSections.find((section) => section.slug === "projects");
{[languages, norway, projects].filter(Boolean).map((section) => ( {[languages, norway, projects].filter(Boolean).map((section) => (
<a href={getSectionHref(section!)}> <a href={getSectionHref(section!)}>
<span>{section!.title}</span> <span>{section!.title}</span>
<strong>{section!.strap}</strong> <strong><LocaleCopy copy={section!.strap} /></strong>
</a> </a>
))} ))}
</div> </div>
@@ -215,11 +187,11 @@ const projects = launchSections.find((section) => section.slug === "projects");
<span>Education dossier</span> <span>Education dossier</span>
<span>{education?.label}</span> <span>{education?.label}</span>
</div> </div>
<h2>{education?.coverline}</h2> {education && <h2><LocaleCopy copy={education.coverline} /></h2>}
<p>{education?.summary}</p> {education && <p><LocaleCopy copy={education.summary} /></p>}
<ul> <ul>
{education?.samples.map((sample) => ( {education && education.samples.en.map((_, i) => (
<li>{sample}</li> <li><LocaleCopy copy={{ en: education.samples.en[i], fr: education.samples.fr[i], nb: education.samples.nb[i] }} /></li>
))} ))}
</ul> </ul>
<a class="button button--dark" href="/education">Read the school issue</a> <a class="button button--dark" href="/education">Read the school issue</a>
@@ -236,7 +208,7 @@ const projects = launchSections.find((section) => section.slug === "projects");
</div> </div>
<h3>{school.institution}</h3> <h3>{school.institution}</h3>
<p><strong>{school.program}</strong></p> <p><strong>{school.program}</strong></p>
<p>{school.teaser}</p> <p><LocaleCopy copy={school.teaser} /></p>
</div> </div>
</article> </article>
))} ))}
@@ -253,10 +225,10 @@ const projects = launchSections.find((section) => section.slug === "projects");
</div> </div>
<div class="manifesto-grid"> <div class="manifesto-grid">
{editorialPromises.map((item, index) => ( {editorialPromises.en.map((_, i) => (
<article class="manifesto-card"> <article class="manifesto-card">
<span>Promise {index + 1}</span> <span>Promise {i + 1}</span>
<p>{item}</p> <p><LocaleCopy copy={{ en: editorialPromises.en[i], fr: editorialPromises.fr[i], nb: editorialPromises.nb[i] }} /></p>
</article> </article>
))} ))}
</div> </div>
+13 -11
View File
@@ -1,18 +1,20 @@
--- ---
import { jazzArticle, jazzBody, jazzFreeNotes, jazzHero, jazzImages, jazzPicks, jazzSources, jazzVenues } from "../data/jazz"; import { jazzArticle, jazzBody, jazzFreeNotes, jazzHero, jazzImages, jazzPicks, jazzSources, jazzVenues } from "../data/jazz";
import BaseLayout from "../layouts/BaseLayout.astro"; import BaseLayout from "../layouts/BaseLayout.astro";
import LocaleCopy from "../components/LocaleCopy.astro";
--- ---
<BaseLayout <BaseLayout
title="Jazz and Music | Dave Gilligan" title="Jazz and Music | Dave Gilligan"
description={jazzArticle.excerpt} description={jazzArticle.excerpt.en}
ogImage="/images/og/jazz-music.png"
> >
<main class="jazz-page"> <main class="jazz-page">
<section class="container jazz-hero"> <section class="container jazz-hero">
<div class="jazz-hero__copy"> <div class="jazz-hero__copy">
<span class="eyebrow">{jazzHero.eyebrow}</span> <span class="eyebrow"><LocaleCopy copy={jazzHero.eyebrow} /></span>
<h1>{jazzHero.title}</h1> <h1><LocaleCopy copy={jazzHero.title} /></h1>
<p class="jazz-hero__lede">{jazzHero.lede}</p> <p class="jazz-hero__lede"><LocaleCopy copy={jazzHero.lede} /></p>
</div> </div>
<aside class="panel jazz-hero__note"> <aside class="panel jazz-hero__note">
@@ -21,7 +23,7 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<span>{jazzArticle.publishedAt.slice(0, 10)}</span> <span>{jazzArticle.publishedAt.slice(0, 10)}</span>
</div> </div>
<h2>{jazzArticle.title}</h2> <h2>{jazzArticle.title}</h2>
<p>{jazzHero.note}</p> <p><LocaleCopy copy={jazzHero.note} /></p>
<div class="jazz-hero__actions"> <div class="jazz-hero__actions">
<a class="button button--dark" href="/articles/kongsberg-jazz-2026">Read the article</a> <a class="button button--dark" href="/articles/kongsberg-jazz-2026">Read the article</a>
</div> </div>
@@ -47,8 +49,8 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<span>Editorial</span> <span>Editorial</span>
<span>Kongsberg x Paris</span> <span>Kongsberg x Paris</span>
</div> </div>
{jazzBody.map((paragraph) => ( {jazzBody.en.map((_, i) => (
<p>{paragraph}</p> <p><LocaleCopy copy={{ en: jazzBody.en[i], fr: jazzBody.fr[i], nb: jazzBody.nb[i] }} /></p>
))} ))}
</article> </article>
@@ -63,7 +65,7 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<li> <li>
<span>{pick.day}</span> <span>{pick.day}</span>
<strong>{pick.artist}</strong> <strong>{pick.artist}</strong>
<p>{pick.detail}</p> <p><LocaleCopy copy={pick.detail} /></p>
<a href={pick.href} target="_blank" rel="noreferrer">Festival programme</a> <a href={pick.href} target="_blank" rel="noreferrer">Festival programme</a>
</li> </li>
))} ))}
@@ -78,8 +80,8 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<ul class="jazz-list"> <ul class="jazz-list">
{jazzFreeNotes.map((note) => ( {jazzFreeNotes.map((note) => (
<li> <li>
<strong>{note.title}</strong> <strong><LocaleCopy copy={note.title} /></strong>
<p>{note.summary}</p> <p><LocaleCopy copy={note.summary} /></p>
<a href={note.href} target="_blank" rel="noreferrer">Open details</a> <a href={note.href} target="_blank" rel="noreferrer">Open details</a>
</li> </li>
))} ))}
@@ -101,7 +103,7 @@ import BaseLayout from "../layouts/BaseLayout.astro";
{jazzVenues.map((venue) => ( {jazzVenues.map((venue) => (
<article class="jazz-venue-card"> <article class="jazz-venue-card">
<h3>{venue.name}</h3> <h3>{venue.name}</h3>
<p>{venue.summary}</p> <p><LocaleCopy copy={venue.summary} /></p>
<a href={venue.href} target="_blank" rel="noreferrer">Open venue</a> <a href={venue.href} target="_blank" rel="noreferrer">Open venue</a>
</article> </article>
))} ))}
+11 -9
View File
@@ -8,18 +8,20 @@ import {
norwaySources, norwaySources,
} from "../data/norway"; } from "../data/norway";
import BaseLayout from "../layouts/BaseLayout.astro"; import BaseLayout from "../layouts/BaseLayout.astro";
import LocaleCopy from "../components/LocaleCopy.astro";
--- ---
<BaseLayout <BaseLayout
title="Norway Desk | Dave Gilligan" title="Norway Desk | Dave Gilligan"
description="A Norway desk feature on family life, fathers, immigration, and the Article 8 case law surrounding Norway." description="A Norway desk feature on family life, fathers, immigration, and the Article 8 case law surrounding Norway."
ogImage="/images/og/norway.png"
> >
<main class="norway-page"> <main class="norway-page">
<section class="container norway-hero"> <section class="container norway-hero">
<div class="norway-hero__copy"> <div class="norway-hero__copy">
<span class="eyebrow">{norwayFeature.eyebrow}</span> <span class="eyebrow"><LocaleCopy copy={norwayFeature.eyebrow} /></span>
<h1>{norwayFeature.title}</h1> <h1><LocaleCopy copy={norwayFeature.title} /></h1>
<p class="norway-hero__lede">{norwayFeature.lede}</p> <p class="norway-hero__lede"><LocaleCopy copy={norwayFeature.lede} /></p>
<div class="norway-hero__actions"> <div class="norway-hero__actions">
<a class="button button--dark" href="/articles/norway-april-2026">Read the April article</a> <a class="button button--dark" href="/articles/norway-april-2026">Read the April article</a>
@@ -33,8 +35,8 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<span>{norwayArticle.publishedAt.slice(0, 10)}</span> <span>{norwayArticle.publishedAt.slice(0, 10)}</span>
</div> </div>
<h2>{norwayArticle.title}</h2> <h2>{norwayArticle.title}</h2>
<p>{norwayArticle.excerpt}</p> <p><LocaleCopy copy={norwayArticle.excerpt} /></p>
<p>{norwayFeature.note}</p> <p><LocaleCopy copy={norwayFeature.note} /></p>
</aside> </aside>
</section> </section>
@@ -91,8 +93,8 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<article class="panel norway-casebook__item"> <article class="panel norway-casebook__item">
<span>{item.date}</span> <span>{item.date}</span>
<h3>{item.title}</h3> <h3>{item.title}</h3>
<p>{item.summary}</p> <p><LocaleCopy copy={item.summary} /></p>
<p>{item.significance}</p> <p><LocaleCopy copy={item.significance} /></p>
<a href={item.href} target="_blank" rel="noreferrer">{item.sourceLabel}</a> <a href={item.href} target="_blank" rel="noreferrer">{item.sourceLabel}</a>
</article> </article>
))} ))}
@@ -111,9 +113,9 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<div class="norway-network__grid"> <div class="norway-network__grid">
{norwayOrganizations.map((organization) => ( {norwayOrganizations.map((organization) => (
<a class="norway-network__card" href={organization.href} target="_blank" rel="noreferrer"> <a class="norway-network__card" href={organization.href} target="_blank" rel="noreferrer">
<span>{organization.strap}</span> <span><LocaleCopy copy={organization.strap} /></span>
<strong>{organization.name}</strong> <strong>{organization.name}</strong>
<p>{organization.summary}</p> <p><LocaleCopy copy={organization.summary} /></p>
</a> </a>
))} ))}
</div> </div>
+11 -10
View File
@@ -5,16 +5,17 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<BaseLayout <BaseLayout
title="Privacy Desk | Dave Gilligan" title="Privacy Desk | Dave Gilligan"
description="Draft privacy notice for the local multilingual edition, covering accounts, family access, contact handling, and editorial infrastructure." description="Privacy notice for davegilligan.com, covering accounts, family access, contact handling, and editorial infrastructure."
ogImage="/images/og/home.png"
> >
<main class="policy-page"> <main class="policy-page">
<section class="policy-hero container"> <section class="policy-hero container">
<p class="eyebrow eyebrow--cover"> <p class="eyebrow eyebrow--cover">
<LocaleCopy <LocaleCopy
copy={{ copy={{
en: "Privacy desk / draft for local review", en: "Privacy desk / plain language policy",
fr: "Cahier confidentialite / brouillon pour revue locale", fr: "Cahier confidentialite / politique en langage clair",
nb: "Personverndesk / utkast for lokal gjennomgang", nb: "Personverndesk / policy pa klart sprak",
}} }}
/> />
</p> </p>
@@ -30,9 +31,9 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<p class="policy-hero__lede"> <p class="policy-hero__lede">
<LocaleCopy <LocaleCopy
copy={{ copy={{
en: "This is the working privacy notice for the localhost multilingual build. It explains how the site should handle accounts, private family areas, contact flows, and first-party operational data before we ship the public version.", en: "This privacy notice covers how davegilligan.com handles accounts, private family areas, contact flows, and first-party operational data.",
fr: "Voici la notice de confidentialite de travail pour la version multilingue sur localhost. Elle explique comment le site doit gerer les comptes, les espaces familiaux prives, les formulaires de contact et les donnees operationnelles de premiere main avant la mise en ligne publique.", fr: "Cette notice de confidentialite couvre la facon dont davegilligan.com gere les comptes, les espaces familiaux prives, les formulaires de contact et les donnees operationnelles de premiere main.",
nb: "Dette er arbeidsutgaven av personvernerklaeringen for den flerspraklige localhost-byggen. Den forklarer hvordan nettstedet skal handtere kontoer, private familieomrader, kontaktskjemaer og operasjonelle forstepartsdata for offentlig lansering.", nb: "Denne personvernerklaeringen beskriver hvordan davegilligan.com handterer kontoer, private familieomrader, kontaktskjemaer og operasjonelle forstepartsdata.",
}} }}
/> />
</p> </p>
@@ -90,9 +91,9 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<article class="policy-block policy-block--full"> <article class="policy-block policy-block--full">
<h2><LocaleCopy copy={{ en: "Rights and review", fr: "Droits et relecture", nb: "Rettigheter og gjennomgang" }} /></h2> <h2><LocaleCopy copy={{ en: "Rights and review", fr: "Droits et relecture", nb: "Rettigheter og gjennomgang" }} /></h2>
<p><LocaleCopy copy={{ <p><LocaleCopy copy={{
en: "Before public deployment, this draft should be tightened into a formal privacy notice with contact details, retention periods, lawful bases, and a final list of processors. The intended rights model includes access, correction, deletion, restriction, objection, and portability where applicable.", en: "Visitors may request access, correction, deletion, restriction, or portability of their personal data by contacting dave@davegilligan.com. Retention periods and lawful bases follow applicable EU and Norwegian data protection law.",
fr: "Avant tout deploiement public, ce brouillon doit devenir une notice formelle avec coordonnees, durees de conservation, bases legales et liste finale des sous-traitants. Le modele de droits vise comprend l'acces, la rectification, l'effacement, la limitation, l'opposition et la portabilite lorsque cela s'applique.", fr: "Les visiteurs peuvent demander l'acces, la rectification, la suppression, la limitation ou la portabilite de leurs donnees personnelles en contactant dave@davegilligan.com. Les durees de conservation et les bases legales respectent le droit europeen et norvegien applicable en matiere de protection des donnees.",
nb: "For offentlig lansering bor dette utkastet skjerpes til en formell personvernerklaering med kontaktopplysninger, lagringstider, behandlingsgrunnlag og en endelig liste over databehandlere. Den tiltenkte rettighetsmodellen omfatter innsyn, retting, sletting, begrensning, protest og dataportabilitet der det gjelder.", nb: "Besokende kan be om innsyn, retting, sletting, begrensning eller dataportabilitet ved a kontakte dave@davegilligan.com. Lagringstider og behandlingsgrunnlag folger gjeldende europeisk og norsk personvernlovgivning.",
}} /></p> }} /></p>
</article> </article>
</section> </section>
+14 -12
View File
@@ -9,18 +9,20 @@ import {
projectsSources, projectsSources,
} from "../data/projects"; } from "../data/projects";
import BaseLayout from "../layouts/BaseLayout.astro"; import BaseLayout from "../layouts/BaseLayout.astro";
import LocaleCopy from "../components/LocaleCopy.astro";
--- ---
<BaseLayout <BaseLayout
title="Projects Desk | Dave Gilligan" title="Projects Desk | Dave Gilligan"
description="An April 2026 projects desk feature on Trivia & Tunes, Blue Note Rhino, room-scale music trivia, and the first full vibe-coded product in the portfolio." description="An April 2026 projects desk feature on Trivia & Tunes, Blue Note Rhino, room-scale music trivia, and the first full vibe-coded product in the portfolio."
ogImage="/images/og/projects.png"
> >
<main class="projects-page"> <main class="projects-page">
<section class="container projects-hero"> <section class="container projects-hero">
<div class="projects-hero__copy"> <div class="projects-hero__copy">
<span class="eyebrow">{projectsFeature.eyebrow}</span> <span class="eyebrow"><LocaleCopy copy={projectsFeature.eyebrow} /></span>
<h1>{projectsFeature.title}</h1> <h1><LocaleCopy copy={projectsFeature.title} /></h1>
<p class="projects-hero__lede">{projectsFeature.lede}</p> <p class="projects-hero__lede"><LocaleCopy copy={projectsFeature.lede} /></p>
<div class="projects-hero__actions"> <div class="projects-hero__actions">
<a class="button button--dark" href={`/articles/${projectsArticle.slug}`}>Read the April article</a> <a class="button button--dark" href={`/articles/${projectsArticle.slug}`}>Read the April article</a>
@@ -36,8 +38,8 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<span>{projectsArticle.publishedAt.slice(0, 10)}</span> <span>{projectsArticle.publishedAt.slice(0, 10)}</span>
</div> </div>
<h2>{projectsArticle.title}</h2> <h2>{projectsArticle.title}</h2>
<p>{projectsArticle.excerpt}</p> <p><LocaleCopy copy={projectsArticle.excerpt} /></p>
<p>{projectsFeature.note}</p> <p><LocaleCopy copy={projectsFeature.note} /></p>
</aside> </aside>
</section> </section>
@@ -60,16 +62,16 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<span>Project notebook</span> <span>Project notebook</span>
<span>Trivia x AI x room design</span> <span>Trivia x AI x room design</span>
</div> </div>
{projectsDeskBody.map((paragraph) => ( {projectsDeskBody.en.map((_, i) => (
<p>{paragraph}</p> <p><LocaleCopy copy={{ en: projectsDeskBody.en[i], fr: projectsDeskBody.fr[i], nb: projectsDeskBody.nb[i] }} /></p>
))} ))}
</article> </article>
<aside class="projects-aside"> <aside class="projects-aside">
{projectsSignals.map((signal) => ( {projectsSignals.map((signal) => (
<article class="panel projects-artifact"> <article class="panel projects-artifact">
<span>{signal.title}</span> <span><LocaleCopy copy={signal.title} /></span>
<p>{signal.summary}</p> <p><LocaleCopy copy={signal.summary} /></p>
</article> </article>
))} ))}
</aside> </aside>
@@ -86,8 +88,8 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<div class="projects-capabilities__grid"> <div class="projects-capabilities__grid">
{projectsCapabilities.map((capability) => ( {projectsCapabilities.map((capability) => (
<article class="projects-card"> <article class="projects-card">
<h3>{capability.title}</h3> <h3><LocaleCopy copy={capability.title} /></h3>
<p>{capability.body}</p> <p><LocaleCopy copy={capability.body} /></p>
</article> </article>
))} ))}
</div> </div>
@@ -100,7 +102,7 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<span>Direct article</span> <span>Direct article</span>
</div> </div>
<h2>{projectsArticle.title}</h2> <h2>{projectsArticle.title}</h2>
<p>{projectsArticle.excerpt}</p> <p><LocaleCopy copy={projectsArticle.excerpt} /></p>
<a class="button button--dark" href={`/articles/${projectsArticle.slug}`}>Open the article</a> <a class="button button--dark" href={`/articles/${projectsArticle.slug}`}>Open the article</a>
</article> </article>
</section> </section>
+31
View File
@@ -1,13 +1,44 @@
--- ---
import LocaleCopy from "../components/LocaleCopy.astro";
import WritingIssue from "../components/WritingIssue.jsx"; import WritingIssue from "../components/WritingIssue.jsx";
import BaseLayout from "../layouts/BaseLayout.astro"; import BaseLayout from "../layouts/BaseLayout.astro";
const copy = {
kicker: { en: "Latest piece", fr: "Dernier article", nb: "Siste stykke" },
date: { en: "May 2026", fr: "Mai 2026", nb: "Mai 2026" },
title: { en: "L'Homme Impossible", fr: "L'Homme Impossible", nb: "L'Homme Impossible" },
subtitle: {
en: "A Guide to Boris Vian and the Collège de 'Pataphysique",
fr: "Un guide sur Boris Vian et le Collège de 'Pataphysique",
nb: "En guide til Boris Vian og Collège de 'Pataphysique",
},
body: {
en: "He was a trained engineer who played trumpet in a cellar with Miles Davis, wrote ten novels before his fortieth birthday, and became a Transcendent Satrap of an institution dedicated to deliberate uselessness.",
fr: "C'était un ingénieur de formation qui jouait de la trompette dans une cave avec Miles Davis, écrivit dix romans avant ses quarante ans, et devint Satrape Transcendant d'une institution dédiée à l'inutilité délibérée.",
nb: "Han var utdannet ingeniør som spilte trompet i en kjeller med Miles Davis, skrev ti romaner før han ble førti, og ble Transcendent Satrape i en institusjon viet til bevisst ubrukbarhet.",
},
cta: { en: "Read the introduction", fr: "Lire l'introduction", nb: "Les introduksjonen" },
};
--- ---
<BaseLayout <BaseLayout
title="Writing Desk | Dave Gilligan" title="Writing Desk | Dave Gilligan"
description="A live writing desk fed from the PHP archive: Boris Vian, Vernon Sullivan weather, jazz syntax, and pataphysical essays." description="A live writing desk fed from the PHP archive: Boris Vian, Vernon Sullivan weather, jazz syntax, and pataphysical essays."
ogImage="/images/og/writing.png"
> >
<main class="writing-page"> <main class="writing-page">
<section class="container writing-feature" style="margin-bottom: 0; padding-bottom: 0;">
<article class="panel jazz-article" style="margin-bottom: var(--space-md, 1.5rem);">
<div class="capsule__kicker">
<span><LocaleCopy copy={copy.kicker} /></span>
<span><LocaleCopy copy={copy.date} /></span>
</div>
<h2 style="margin-top: 0.5rem;"><LocaleCopy copy={copy.title} /></h2>
<p style="font-style: italic; margin-bottom: 0.75rem;"><LocaleCopy copy={copy.subtitle} /></p>
<p><LocaleCopy copy={copy.body} /></p>
<a class="button button--dark" href="/articles/boris-vian-2026/"><LocaleCopy copy={copy.cta} /></a>
</article>
</section>
<WritingIssue client:load /> <WritingIssue client:load />
</main> </main>
</BaseLayout> </BaseLayout>
+760 -100
View File
@@ -1,22 +1,130 @@
:root { :root {
--paper: #f6f0e1; /* ---------- Surfaces (paper) ---------- */
--paper-deep: #ead9b8; --paper-white: #fbf8f0;
--ink: #181511; --paper: #f6f0e1;
--ink-soft: #5e564b; --paper-warm: #f2ead8;
--ink-faint: #867a6b; --paper-deep: #ead9b8;
--line: rgba(24, 21, 17, 0.16); --paper-shadow: #d9c79e;
--line-strong: rgba(24, 21, 17, 0.32);
--brass: #936c20; /* ---------- Ink (foreground) ---------- */
--burgundy: #73312a; --ink: #120f0a;
--teal: #1f5d5f; --ink-strong: #1a160f;
--card: rgba(255, 252, 245, 0.82); --ink-soft: #5e564b;
--card-strong: rgba(255, 250, 241, 0.94); --ink-faint: #867a6b;
--shadow: 0 18px 55px rgba(50, 36, 20, 0.08); --ink-whisper: #b1a795;
--radius-lg: 28px;
--radius-sm: 999px; /* ---------- Lines & rules ---------- */
--font-display: "Instrument Serif", "Times New Roman", serif; --line: rgba(18, 15, 10, 0.16);
--font-body: "Newsreader", Georgia, serif; --line-strong: rgba(18, 15, 10, 0.32);
--font-mono: "IBM Plex Mono", "Courier New", monospace; --line-heavy: rgba(18, 15, 10, 0.55);
--rule-double: 2px;
/* ---------- Brand inks ---------- */
--burgundy: #8f2218;
--burgundy-deep: #5a1610;
--burgundy-legacy: #73312a;
--brass: #a07614;
--brass-deep: #6a4a08;
--mustard: #c9941a;
--mustard-pale: #e7c777;
--ink-blue: #1b3a5f;
--ink-blue-deep: #0f2440;
--teal: #1f5d5f;
--teal-deep: #143e40;
/* ---------- Section accents ---------- */
--sec-business: #1f5d5f;
--sec-education: #a07614;
--sec-family: #8b4a41;
--sec-fun: #8a5a17;
--sec-writing: #8f2218;
--sec-jazz: #7b3f74;
--sec-languages: #2d5f92;
--sec-ai-lab: #1b3a5f;
--sec-norway: #46613e;
--sec-projects: #6b4e85;
--sec-cv: #5c564c;
/* ---------- Cards & surfaces ---------- */
--card: rgba(255, 252, 245, 0.82);
--card-strong: rgba(255, 250, 241, 0.94);
--card-deep: rgba(247, 239, 222, 0.88);
--card-press: #fffaef;
/* ---------- Radius ---------- */
--radius-xs: 0.5rem;
--radius-sm: 999px;
--radius-md: 1rem;
--radius-lg: 1.4rem;
--radius-xl: 1.8rem;
--radius-2xl: 2rem;
/* ---------- Shadows (warm, never grey) ---------- */
--shadow-paper: 0 18px 55px rgba(50, 36, 20, 0.08);
--shadow-card: 0 10px 35px rgba(40, 28, 12, 0.05);
--shadow-card-hover: 0 18px 35px rgba(40, 28, 12, 0.08);
--shadow-photo: 0 32px 80px rgba(31, 20, 9, 0.16);
--shadow-stamp: inset 0 0 0 1px rgba(143, 34, 24, 0.35),
0 1px 0 rgba(143, 34, 24, 0.18);
/* ---------- Spacing ---------- */
--sp-0: 0;
--sp-1: 0.25rem;
--sp-2: 0.5rem;
--sp-3: 0.75rem;
--sp-4: 1rem;
--sp-5: 1.25rem;
--sp-6: 1.5rem;
--sp-7: 2rem;
--sp-8: 2.5rem;
--sp-9: 3rem;
--sp-10: 4rem;
--gutter: 1rem;
--gutter-tight: 0.75rem;
--column-max: 1180px;
/* ---------- Type scale ---------- */
--t-display-xl: clamp(4.15rem, 11vw, 8.6rem);
--t-display-l: clamp(3.2rem, 8vw, 6.6rem);
--t-display-m: clamp(2.5rem, 6vw, 4.8rem);
--t-display-s: clamp(2rem, 4vw, 3rem);
--t-display-xs: clamp(1.55rem, 3vw, 2rem);
--t-body-xl: 1.3rem;
--t-body-l: 1.14rem;
--t-body: 1rem;
--t-body-s: 0.92rem;
--t-caption: 0.78rem;
--t-micro: 0.68rem;
/* ---------- Tracking ---------- */
--track-display: -0.04em;
--track-display-tight: -0.06em;
--track-body: 0;
--track-mono: 0.14em;
--track-mono-wide: 0.22em;
/* ---------- Line heights ---------- */
--lh-display: 0.9;
--lh-display-tight: 0.86;
--lh-headline: 1.0;
--lh-body: 1.65;
--lh-body-tight: 1.45;
/* ---------- Motion ---------- */
--ease-out: cubic-bezier(0.2, 0.7, 0.2, 1);
--ease-in-out: cubic-bezier(0.5, 0, 0.3, 1);
--dur-fast: 120ms;
--dur-base: 180ms;
--dur-slow: 280ms;
/* ---------- Fonts ---------- */
--font-display: "Instrument Serif", "Times New Roman", serif;
--font-body: "Newsreader", Georgia, "Iowan Old Style", serif;
--font-mono: "Special Elite", "Courier Prime", "IBM Plex Mono", "Courier New", monospace;
--font-mono-tech: "IBM Plex Mono", "Courier New", monospace;
color-scheme: light; color-scheme: light;
} }
@@ -34,10 +142,10 @@ body {
color: var(--ink); color: var(--ink);
font-family: var(--font-body); font-family: var(--font-body);
background: background:
radial-gradient(circle at top left, rgba(197, 165, 105, 0.2), transparent 28%), radial-gradient(circle at top left, rgba(160, 118, 20, 0.18), transparent 28%),
radial-gradient(circle at 85% 20%, rgba(31, 93, 95, 0.18), transparent 26%), radial-gradient(circle at 85% 20%, rgba(31, 93, 95, 0.16), transparent 26%),
radial-gradient(circle at 50% 100%, rgba(115, 49, 42, 0.14), transparent 32%), radial-gradient(circle at 50% 100%, rgba(143, 34, 24, 0.12), transparent 32%),
linear-gradient(180deg, #fbf8f0 0%, #f6f0e1 44%, #f2ead8 100%); linear-gradient(180deg, var(--paper-white) 0%, var(--paper) 44%, var(--paper-warm) 100%);
} }
body::before { body::before {
@@ -81,7 +189,7 @@ img {
height: 34rem; height: 34rem;
top: -10rem; top: -10rem;
left: -9rem; left: -9rem;
background: radial-gradient(circle, rgba(147, 108, 32, 0.22), transparent 65%); background: radial-gradient(circle, rgba(160, 118, 20, 0.22), transparent 65%);
animation: drift 16s ease-in-out infinite; animation: drift 16s ease-in-out infinite;
} }
@@ -194,6 +302,7 @@ img {
font-size: 0.74rem; font-size: 0.74rem;
letter-spacing: 0.2em; letter-spacing: 0.2em;
color: var(--burgundy); color: var(--burgundy);
font-style: italic;
} }
.site-nav { .site-nav {
@@ -277,60 +386,17 @@ img {
height: 1.05rem; height: 1.05rem;
} }
.section-mark--business { .section-mark--business { color: var(--sec-business); background: rgba(31, 93, 95, 0.12); }
color: #1f5d5f; .section-mark--education { color: var(--sec-education); background: rgba(160, 118, 20, 0.12); }
background: rgba(31, 93, 95, 0.12); .section-mark--family { color: var(--sec-family); background: rgba(139, 74, 65, 0.12); }
} .section-mark--fun-postings { color: var(--sec-fun); background: rgba(138, 90, 23, 0.12); }
.section-mark--writing { color: var(--sec-writing); background: rgba(143, 34, 24, 0.12); }
.section-mark--education { .section-mark--jazz-music { color: var(--sec-jazz); background: rgba(123, 63, 116, 0.12); }
color: #936c20; .section-mark--languages { color: var(--sec-languages); background: rgba(45, 95, 146, 0.12); }
background: rgba(147, 108, 32, 0.12); .section-mark--ai-lab { color: var(--sec-ai-lab); background: rgba(27, 58, 95, 0.12); }
} .section-mark--norway { color: var(--sec-norway); background: rgba(70, 97, 62, 0.12); }
.section-mark--projects { color: var(--sec-projects); background: rgba(107, 78, 133, 0.12); }
.section-mark--family { .section-mark--cv { color: var(--sec-cv); background: rgba(92, 86, 76, 0.12); }
color: #8b4a41;
background: rgba(139, 74, 65, 0.12);
}
.section-mark--fun-postings {
color: #8a5a17;
background: rgba(138, 90, 23, 0.12);
}
.section-mark--writing {
color: #73312a;
background: rgba(115, 49, 42, 0.12);
}
.section-mark--jazz-music {
color: #7b3f74;
background: rgba(123, 63, 116, 0.12);
}
.section-mark--languages {
color: #2d5f92;
background: rgba(45, 95, 146, 0.12);
}
.section-mark--ai-lab {
color: #24506f;
background: rgba(36, 80, 111, 0.12);
}
.section-mark--norway {
color: #46613e;
background: rgba(70, 97, 62, 0.12);
}
.section-mark--projects {
color: #6b4e85;
background: rgba(107, 78, 133, 0.12);
}
.section-mark--cv {
color: #5c564c;
background: rgba(92, 86, 76, 0.12);
}
.frontline { .frontline {
display: grid; display: grid;
@@ -365,7 +431,7 @@ img {
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
background: linear-gradient(180deg, var(--card-strong), var(--card)); background: linear-gradient(180deg, var(--card-strong), var(--card));
box-shadow: var(--shadow); box-shadow: var(--shadow-paper);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
} }
@@ -476,10 +542,13 @@ img {
letter-spacing: 0.14em; letter-spacing: 0.14em;
text-transform: uppercase; text-transform: uppercase;
text-decoration: none; text-decoration: none;
color: var(--ink);
background: rgba(255, 255, 255, 0.55);
transition: transition:
transform 180ms ease, transform var(--dur-base) var(--ease-out),
background-color 180ms ease, background-color var(--dur-base) var(--ease-out),
border-color 180ms ease; border-color var(--dur-base) var(--ease-out);
cursor: pointer;
} }
.button:hover { .button:hover {
@@ -488,13 +557,22 @@ img {
.button--dark { .button--dark {
background: var(--ink); background: var(--ink);
color: #fff7eb; color: var(--paper-white);
border-color: var(--ink);
} }
.button--soft { .button--soft {
background: rgba(255, 255, 255, 0.55); background: rgba(255, 255, 255, 0.55);
} }
.button--stamp {
background: transparent;
color: var(--burgundy);
border: 1.5px solid var(--burgundy);
box-shadow: var(--shadow-stamp);
font-style: italic;
}
.hero-side { .hero-side {
display: grid; display: grid;
gap: 1rem; gap: 1rem;
@@ -697,20 +775,23 @@ img {
.section-card { .section-card {
height: 100%; height: 100%;
padding: 1.15rem; padding: 1.15rem;
border-radius: 1.4rem; border-radius: var(--radius-lg);
border: 1px solid var(--line); border: 1px solid var(--line);
background: rgba(255, 253, 249, 0.78); background: rgba(255, 253, 249, 0.78);
box-shadow: 0 10px 35px rgba(40, 28, 12, 0.05); box-shadow: var(--shadow-card);
transition: transition:
transform 180ms ease, transform var(--dur-base) var(--ease-out),
border-color 180ms ease, border-color var(--dur-base) var(--ease-out),
box-shadow 180ms ease; box-shadow var(--dur-base) var(--ease-out);
text-decoration: none;
color: inherit;
display: block;
} }
.section-card:hover { .section-card:hover {
transform: translateY(-4px); transform: translateY(-4px);
border-color: rgba(115, 49, 42, 0.35); border-color: rgba(143, 34, 24, 0.35);
box-shadow: 0 18px 35px rgba(40, 28, 12, 0.08); box-shadow: var(--shadow-card-hover);
} }
.section-card__header { .section-card__header {
@@ -903,9 +984,9 @@ img {
.page-shell--cover { .page-shell--cover {
background: background:
radial-gradient(circle at 10% 10%, rgba(147, 108, 32, 0.16), transparent 24%), radial-gradient(circle at 10% 10%, rgba(160, 118, 20, 0.18), transparent 24%),
radial-gradient(circle at 85% 18%, rgba(31, 93, 95, 0.2), transparent 22%), radial-gradient(circle at 85% 18%, rgba(31, 93, 95, 0.16), transparent 22%),
linear-gradient(180deg, rgba(255, 251, 242, 0.94), rgba(246, 240, 225, 0.82)); linear-gradient(180deg, rgba(251, 248, 240, 0.98), rgba(242, 233, 215, 0.88));
} }
.cover-banner { .cover-banner {
@@ -1009,8 +1090,8 @@ img {
inset: 0 0 auto; inset: 0 0 auto;
height: min(42rem, 92vh); height: min(42rem, 92vh);
background: background:
linear-gradient(125deg, rgba(255, 252, 246, 0.92), rgba(237, 228, 208, 0.76)), linear-gradient(125deg, rgba(255, 252, 246, 0.92), rgba(234, 224, 200, 0.76)),
radial-gradient(circle at top right, rgba(31, 93, 95, 0.18), transparent 30%); radial-gradient(circle at top right, rgba(31, 93, 95, 0.16), transparent 30%);
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
} }
@@ -1124,8 +1205,8 @@ img {
overflow: hidden; overflow: hidden;
background: background:
linear-gradient(160deg, rgba(12, 15, 22, 0.88), rgba(39, 24, 19, 0.72)), linear-gradient(160deg, rgba(12, 15, 22, 0.88), rgba(39, 24, 19, 0.72)),
radial-gradient(circle at top left, rgba(147, 108, 32, 0.26), transparent 28%); radial-gradient(circle at top left, rgba(160, 118, 20, 0.26), transparent 28%);
box-shadow: 0 32px 80px rgba(31, 20, 9, 0.16); box-shadow: var(--shadow-photo);
} }
.cover-collage::after { .cover-collage::after {
@@ -4831,13 +4912,13 @@ img {
} }
} }
.locale-copy__text { .locale-copy .locale-copy__text {
display: none; display: none;
} }
html[data-ui-lang="en"] .locale-copy__text[data-locale-option="en"], html[data-ui-lang="en"] .locale-copy .locale-copy__text[data-locale-option="en"],
html[data-ui-lang="fr"] .locale-copy__text[data-locale-option="fr"], html[data-ui-lang="fr"] .locale-copy .locale-copy__text[data-locale-option="fr"],
html[data-ui-lang="nb"] .locale-copy__text[data-locale-option="nb"] { html[data-ui-lang="nb"] .locale-copy .locale-copy__text[data-locale-option="nb"] {
display: inline; display: inline;
} }
@@ -5076,6 +5157,504 @@ html[data-ui-lang="nb"] .locale-copy__text[data-locale-option="nb"] {
margin-top: 0.25rem; margin-top: 0.25rem;
} }
/* =========================================================================
Design system components (from colors_and_type.css + kit.css)
========================================================================= */
/* Kicker / eyebrow reset (already defined above; keeping for reference) */
.kicker {
display: inline-flex;
align-items: center;
gap: 0.65rem;
font-family: var(--font-mono);
font-size: var(--t-micro);
letter-spacing: var(--track-mono);
text-transform: uppercase;
color: var(--teal);
}
.kicker::before {
content: "";
width: 2.4rem;
height: 1px;
background: currentColor;
}
/* Periodical double rule */
.rule-double {
height: 6px;
border-top: 1px solid var(--ink);
border-bottom: 1px solid var(--ink);
}
/* Stamp (pataphysique easter egg) */
.stamp {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.3rem 0.7rem;
border: 1.5px solid var(--burgundy);
border-radius: 0.2rem;
color: var(--burgundy);
font-family: var(--font-mono);
font-size: var(--t-micro);
letter-spacing: var(--track-mono);
text-transform: uppercase;
background: rgba(255, 252, 245, 0.4);
transform: rotate(-3deg);
box-shadow: var(--shadow-stamp);
opacity: 0.85;
font-style: italic;
}
.stamp--blue { color: var(--ink-blue); border-color: var(--ink-blue); box-shadow: inset 0 0 0 1px rgba(27, 58, 95, 0.35); }
.stamp--brass { color: var(--brass-deep); border-color: var(--brass-deep); }
/* Registration marks (printer's crosshair) */
.reg-mark {
display: inline-block;
width: 1.1rem;
height: 1.1rem;
background:
linear-gradient(currentColor, currentColor) center / 100% 1px no-repeat,
linear-gradient(currentColor, currentColor) center / 1px 100% no-repeat;
color: var(--ink-faint);
position: relative;
}
.reg-mark::after {
content: "";
position: absolute;
inset: 0;
border: 1px solid currentColor;
border-radius: 50%;
}
/* Corner registration marks */
.page-shell .reg-corner {
position: absolute;
width: 16px;
height: 16px;
color: rgba(18, 15, 10, 0.28);
pointer-events: none;
background:
linear-gradient(currentColor, currentColor) center / 100% 1px no-repeat,
linear-gradient(currentColor, currentColor) center / 1px 100% no-repeat;
}
.page-shell .reg-corner::after {
content: "";
position: absolute;
inset: 0;
border: 1px solid currentColor;
border-radius: 50%;
}
.page-shell .reg-corner.tl { top: 12px; left: 12px; }
.page-shell .reg-corner.tr { top: 12px; right: 12px; }
.page-shell .reg-corner.bl { bottom: 12px; left: 12px; }
.page-shell .reg-corner.br { bottom: 12px; right: 12px; }
/* Drop cap (1950s lit style) */
.dropcap::first-letter {
font-family: var(--font-display);
float: left;
font-size: 4.6em;
line-height: 0.82;
padding: 0.04em 0.08em 0 0;
color: var(--burgundy);
}
/* ---- Code blocks ---- */
code, pre, .mono {
font-family: var(--font-mono-tech);
font-size: 0.9em;
}
/* ---- Generic section/article page ---- */
.section-page, .article-page {
padding-bottom: 3rem;
background:
radial-gradient(circle at 84% 16%, rgba(31, 93, 95, 0.14), transparent 18%),
radial-gradient(circle at 14% 12%, rgba(143, 34, 24, 0.12), transparent 18%),
linear-gradient(180deg, rgba(251, 248, 240, 0.98), rgba(244, 235, 219, 0.92));
}
/* ---- Business page (teal accent, React-island page) ---- */
.business-page {
padding-bottom: 3rem;
background:
radial-gradient(circle at 82% 14%, rgba(31, 93, 95, 0.18), transparent 22%),
radial-gradient(circle at 12% 10%, rgba(160, 118, 20, 0.12), transparent 20%),
linear-gradient(180deg, rgba(251, 248, 240, 0.98), rgba(241, 236, 222, 0.94));
}
/* ---- Writing page (burgundy accent) ---- */
.writing-page {
padding-bottom: 3rem;
background:
radial-gradient(circle at 82% 14%, rgba(143, 34, 24, 0.14), transparent 20%),
radial-gradient(circle at 12% 10%, rgba(160, 118, 20, 0.12), transparent 18%),
linear-gradient(180deg, rgba(251, 248, 240, 0.98), rgba(244, 235, 219, 0.92));
}
/* ---- Projects page (purple accent) ---- */
.projects-page {
padding-bottom: 3rem;
background:
radial-gradient(circle at 84% 14%, rgba(107, 78, 133, 0.14), transparent 20%),
radial-gradient(circle at 12% 12%, rgba(160, 118, 20, 0.10), transparent 18%),
linear-gradient(180deg, rgba(251, 248, 240, 0.98), rgba(244, 236, 222, 0.92));
}
/* ---- Family Lab page ---- */
.family-lab-page {
padding-bottom: 3rem;
background:
radial-gradient(circle at 12% 12%, rgba(139, 74, 65, 0.14), transparent 20%),
radial-gradient(circle at 88% 14%, rgba(160, 118, 20, 0.12), transparent 22%),
linear-gradient(180deg, rgba(251, 248, 240, 0.98), rgba(244, 235, 219, 0.94));
}
/* =========================================================================
Issue / section page layouts
========================================================================= */
.issue-page { padding-bottom: 3rem; }
.issue-hero {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(300px, 0.9fr);
gap: 1rem;
padding: 2rem 0 1rem;
}
.issue-hero__copy h1 { margin: 0; max-width: 12ch; font-family: var(--font-display); font-size: clamp(3rem, 8vw, 6rem); line-height: 0.9; letter-spacing: -0.05em; font-weight: 400; }
.issue-hero__lede { max-width: 42rem; margin: 1rem 0 0; font-size: 1.12rem; line-height: 1.7; color: var(--ink-soft); }
.issue-hero__note { align-self: end; padding: 1.2rem; }
.issue-hero__note h2 { margin: 0; font-family: var(--font-display); font-size: clamp(1.6rem, 3.5vw, 2.6rem); line-height: 0.96; font-weight: 400; }
.issue-hero__note p { margin: 0.6rem 0 0; color: var(--ink-soft); line-height: 1.55; }
/* =========================================================================
AI Lab page
========================================================================= */
.ai-lab-page {
position: relative;
padding-bottom: 3rem;
background:
radial-gradient(circle at 82% 15%, rgba(27, 58, 95, 0.14), transparent 20%),
radial-gradient(circle at 14% 12%, rgba(160, 118, 20, 0.14), transparent 18%),
linear-gradient(180deg, rgba(251, 248, 240, 0.98), rgba(242, 233, 215, 0.92));
}
.ai-lab-page::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background-image: linear-gradient(rgba(18, 15, 10, 0.035) 1px, transparent 1px);
background-size: 100% 18px;
opacity: 0.5;
z-index: 0;
}
.ai-lab-hero {
display: grid;
grid-template-columns: minmax(0, 1.02fr) minmax(320px, 0.98fr);
gap: 1rem;
align-items: end;
padding: 1.6rem 0 1rem;
}
.ai-lab-hero__copy h1 { margin: 0; max-width: 12ch; font-family: var(--font-display); font-size: clamp(3.2rem, 8vw, 6rem); line-height: 0.88; letter-spacing: -0.06em; font-weight: 400; }
.ai-lab-hero__lede { max-width: 42rem; margin: 1rem 0 0; font-size: 1.1rem; line-height: 1.75; color: var(--ink-soft); }
.ai-lab-hero__actions { display: flex; flex-wrap: wrap; gap: 0.85rem; margin-top: 1.4rem; }
.ai-lab-metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1rem; padding: 0.4rem 0 1rem; }
.ai-lab-metric { padding: 1rem 1.05rem; border-top: 2px solid var(--ink); background: rgba(255, 251, 244, 0.78); }
.ai-lab-metric strong { display: block; font-family: var(--font-display); font-size: 2.4rem; line-height: 0.9; font-weight: 400; }
.ai-lab-metric span { display: block; margin-top: 0.4rem; font-family: var(--font-mono); font-size: 0.64rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--burgundy); }
.ai-lab-metric p { margin: 0.6rem 0 0; color: var(--ink-soft); line-height: 1.55; font-size: 0.92rem; }
.ai-lab-split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(320px, 0.96fr); gap: 1rem; padding: 0.2rem 0 1rem; }
.ai-lab-poster, .ai-lab-credit, .ai-lab-story, .ai-lab-diagram, .ai-lab-code { padding: 1.2rem; }
.ai-lab-poster img, .ai-lab-diagram img { width: 100%; border-radius: 1.2rem; }
.ai-lab-credit p, .ai-lab-story p { margin: 0.8rem 0 0; color: var(--ink-soft); line-height: 1.7; }
.ai-lab-story h2 { margin: 0; font-family: var(--font-display); font-size: clamp(2rem, 4.5vw, 3.4rem); line-height: 0.95; font-weight: 400; }
.ai-lab-code pre {
margin: 0;
overflow-x: auto;
padding: 1rem;
border-radius: 1rem;
background: #11151c;
color: #f3ead9;
font-family: var(--font-mono-tech);
font-size: 0.78rem;
line-height: 1.6;
}
.ai-lab-code__block p { margin: 0 0 0.5rem; font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--burgundy); }
/* =========================================================================
Jazz desk + Article page
========================================================================= */
.jazz-page, .article-page {
position: relative;
padding-bottom: 3rem;
background:
radial-gradient(circle at 84% 16%, rgba(31, 93, 95, 0.14), transparent 18%),
radial-gradient(circle at 14% 12%, rgba(143, 34, 24, 0.12), transparent 18%),
linear-gradient(180deg, rgba(251, 248, 240, 0.98), rgba(244, 235, 219, 0.92));
}
.jazz-page::before, .article-page::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background: linear-gradient(transparent 0, transparent 97%, rgba(18, 15, 10, 0.03) 97%, rgba(18, 15, 10, 0.03) 100%);
background-size: 100% 10px;
opacity: 0.35;
z-index: 0;
}
.jazz-hero {
display: grid;
grid-template-columns: minmax(0, 1.12fr) minmax(300px, 0.88fr);
gap: 1rem;
padding: 2rem 0 1rem;
}
.jazz-hero__copy h1 { margin: 0; max-width: 11ch; font-family: var(--font-display); font-size: clamp(3rem, 8vw, 6rem); line-height: 0.9; letter-spacing: -0.05em; font-weight: 400; }
.jazz-hero__lede { max-width: 42rem; margin: 1rem 0 0; color: var(--ink-soft); font-size: 1.12rem; line-height: 1.7; }
.jazz-gallery { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; padding: 0.3rem 0 1rem; }
.jazz-gallery__figure {
margin: 0;
overflow: hidden;
border-radius: 1.4rem;
border: 1px solid var(--line);
background: rgba(255, 252, 246, 0.84);
box-shadow: var(--shadow-paper);
}
.jazz-gallery__figure img { width: 100%; height: clamp(16rem, 32vw, 26rem); object-fit: cover; }
.jazz-gallery__figure figcaption {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 0.8rem;
padding: 0.85rem 1rem 0;
font-family: var(--font-mono);
font-size: 0.62rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ink-faint);
}
.jazz-gallery__note { margin: 0.45rem 1rem 1rem; color: var(--ink-soft); line-height: 1.5; font-size: 0.9rem; }
.jazz-layout { display: grid; grid-template-columns: minmax(0, 1.08fr) minmax(320px, 0.92fr); gap: 1rem; padding: 0.2rem 0 1rem; }
.jazz-article, .article-body { padding: 1.4rem 1.6rem; }
.jazz-article p, .article-body p { margin: 0.9rem 0 0; color: var(--ink-strong); font-size: 1.04rem; line-height: 1.85; }
.jazz-article p:first-of-type::first-letter, .article-body .dropcap::first-letter {
font-family: var(--font-display);
float: left;
font-size: 4.6em;
line-height: 0.82;
padding: 0.04em 0.08em 0 0;
color: var(--burgundy);
}
.jazz-sidebar { display: grid; gap: 1rem; }
.jazz-sidebar__card { padding: 1.2rem; }
.jazz-list { list-style: none; margin: 0.9rem 0 0; padding: 0; display: grid; gap: 0.9rem; }
.jazz-list li { padding-top: 0.9rem; border-top: 1px solid var(--line); }
.jazz-list span { display: block; font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--burgundy); }
.jazz-list strong { display: block; margin-top: 0.4rem; font-family: var(--font-display); font-size: 1.4rem; line-height: 1; font-weight: 400; }
.jazz-list p { margin: 0.5rem 0 0; color: var(--ink-soft); line-height: 1.55; font-size: 0.93rem; }
.jazz-list a { display: inline-flex; margin-top: 0.7rem; font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.12em; text-transform: uppercase; text-decoration: none; color: var(--teal); }
.jazz-venues { padding: 0.8rem 0 1rem; }
.jazz-venue-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; }
.jazz-venue-card { padding: 1rem 1.05rem; border-top: 2px solid var(--ink); background: rgba(255, 252, 246, 0.78); }
.jazz-venue-card h3 { margin: 0; font-family: var(--font-display); font-size: 1.6rem; line-height: 1; font-weight: 400; }
.jazz-venue-card p { margin: 0.65rem 0 0; color: var(--ink-soft); line-height: 1.55; font-size: 0.95rem; }
.jazz-venue-card a { display: inline-flex; margin-top: 0.6rem; font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.12em; text-transform: uppercase; text-decoration: none; color: var(--teal); }
/* =========================================================================
Norway page
========================================================================= */
.norway-page {
position: relative;
padding-bottom: 3rem;
background:
radial-gradient(circle at 82% 16%, rgba(70, 97, 62, 0.16), transparent 18%),
radial-gradient(circle at 12% 14%, rgba(45, 95, 146, 0.12), transparent 20%),
linear-gradient(180deg, rgba(251, 248, 240, 0.98), rgba(241, 236, 225, 0.94));
}
.norway-hero { display: grid; grid-template-columns: minmax(0, 1.08fr) minmax(320px, 0.92fr); gap: 1rem; padding: 2rem 0 1rem; }
.norway-hero__copy h1 { margin: 0; max-width: 11ch; font-family: var(--font-display); font-size: clamp(3rem, 7.5vw, 5.6rem); line-height: 0.92; font-weight: 400; letter-spacing: -0.05em; }
.norway-hero__lede { max-width: 38rem; margin: 1.25rem 0 0; font-size: 1.18rem; line-height: 1.58; }
.norway-pullquote {
padding: 1.4rem;
background:
radial-gradient(circle at top right, rgba(70, 97, 62, 0.14), transparent 24%),
linear-gradient(180deg, rgba(255, 253, 247, 0.92), rgba(247, 242, 233, 0.88));
}
.norway-pullquote p { margin: 0; font-family: var(--font-display); font-size: 1.7rem; line-height: 1.2; color: var(--ink); font-style: italic; }
.norway-pullquote cite { display: block; margin-top: 0.8rem; font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--sec-norway); font-style: normal; }
.norway-gallery { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; padding: 0.8rem 0 1rem; }
.norway-gallery__figure { margin: 0; display: grid; gap: 0.5rem; }
.norway-gallery__figure img { width: 100%; aspect-ratio: 1.2; object-fit: cover; border-radius: 1.4rem; border: 1px solid var(--line); box-shadow: var(--shadow-paper); }
.norway-gallery__figure figcaption {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 0.5rem;
font-family: var(--font-mono);
font-size: 0.66rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ink-soft);
}
/* =========================================================================
Education page
========================================================================= */
.education-page {
position: relative;
padding-bottom: 3rem;
background:
radial-gradient(circle at 88% 12%, rgba(160, 118, 20, 0.18), transparent 22%),
linear-gradient(180deg, rgba(251, 248, 240, 0.98), rgba(244, 235, 219, 0.92));
}
/* =========================================================================
CV page
========================================================================= */
.cv-page {
padding-bottom: 3rem;
background:
radial-gradient(circle at 90% 8%, rgba(92, 86, 76, 0.15), transparent 22%),
linear-gradient(180deg, rgba(251, 248, 240, 0.98), rgba(246, 240, 225, 0.94));
}
.cv-stripe {
display: grid;
grid-template-columns: 11rem minmax(0, 1fr);
gap: 1.5rem;
padding: 1.1rem 0;
border-top: 1px solid var(--line);
align-items: start;
}
.cv-stripe__year {
font-family: var(--font-mono);
font-size: 0.74rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--burgundy);
padding-top: 0.2rem;
}
.cv-stripe__year strong {
display: block;
font-family: var(--font-display);
font-size: 1.4rem;
color: var(--ink);
letter-spacing: -0.01em;
font-weight: 400;
margin-bottom: 0.2rem;
}
.cv-stripe__title { font-family: var(--font-display); font-size: 1.8rem; line-height: 1.05; font-weight: 400; margin: 0; }
.cv-stripe__org { display: block; margin-top: 0.4rem; color: var(--ink-soft); font-style: italic; }
.cv-stripe__detail { margin: 0.6rem 0 0; color: var(--ink-soft); line-height: 1.6; max-width: 56ch; }
/* =========================================================================
Family album
========================================================================= */
.family-page {
padding-bottom: 3rem;
background:
radial-gradient(circle at 12% 12%, rgba(139, 74, 65, 0.14), transparent 20%),
radial-gradient(circle at 88% 14%, rgba(160, 118, 20, 0.12), transparent 22%),
linear-gradient(180deg, rgba(251, 248, 240, 0.98), rgba(244, 235, 219, 0.94));
}
.family-album {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 0.85rem;
}
.family-photo {
position: relative;
overflow: hidden;
border-radius: 0.6rem;
border: 1px solid rgba(18, 15, 10, 0.12);
background: var(--paper-white);
box-shadow: var(--shadow-paper);
}
.family-photo img { width: 100%; height: 100%; object-fit: cover; filter: saturate(0.9) contrast(1.05); }
.family-photo figcaption {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 0.6rem 0.75rem;
background: linear-gradient(180deg, transparent, rgba(10, 12, 18, 0.6));
color: #f7f1e3;
font-family: var(--font-mono);
font-size: 0.6rem;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.family-photo--12 { grid-column: span 12; aspect-ratio: 2.5; }
.family-photo--8 { grid-column: span 8; aspect-ratio: 1.6; }
.family-photo--6 { grid-column: span 6; aspect-ratio: 1.4; }
.family-photo--4 { grid-column: span 4; aspect-ratio: 1.0; }
.family-photo--3 { grid-column: span 3; aspect-ratio: 0.9; }
.family-caption {
grid-column: span 12;
padding: 1.2rem;
background: rgba(255, 252, 246, 0.8);
border-radius: 1rem;
border-top: 2px solid var(--ink);
}
/* =========================================================================
Responsive
========================================================================= */
@media (max-width: 1080px) {
.cover-hero__inner,
.venture-desk__grid,
.signal-deck,
.signal-strip,
.frontline,
.manifesto-grid,
.section-marquee__row,
.site-footer__top,
.ai-lab-split,
.ai-lab-metrics,
.ai-lab-hero,
.jazz-layout,
.jazz-hero,
.jazz-gallery,
.jazz-venue-grid,
.norway-hero,
.norway-gallery,
.issue-hero { grid-template-columns: 1fr; }
.section-grid > * { grid-column: span 6; }
.cv-stripe { grid-template-columns: 1fr; gap: 0.5rem; }
.family-album { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.family-photo--12, .family-photo--8 { grid-column: span 6; }
.family-photo--6, .family-photo--4, .family-photo--3 { grid-column: span 3; }
}
@media (max-width: 640px) {
.section-grid > * { grid-column: span 12; }
.footer-links { grid-template-columns: 1fr; }
.norway-gallery { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.jazz-venue-grid { grid-template-columns: 1fr; }
.ai-lab-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (prefers-reduced-motion: reduce) {
.page-shell::before,
.page-shell::after { animation: none; }
* { transition-duration: 0.01ms !important; }
}
.cookie-toggle span { .cookie-toggle span {
display: grid; display: grid;
gap: 0.2rem; gap: 0.2rem;
@@ -5111,3 +5690,84 @@ html[data-ui-lang="nb"] .locale-copy__text[data-locale-option="nb"] {
width: calc(100% - 1.5rem); width: calc(100% - 1.5rem);
} }
} }
/* =====================================================================
LOCALE BAR fixed flag strip at top of every page
===================================================================== */
.locale-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 9999;
height: 46px;
background: var(--burgundy);
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid var(--burgundy-deep);
}
.locale-bar__inner {
display: flex;
align-items: center;
gap: 0.25rem;
}
.locale-bar__btn {
display: inline-flex;
align-items: center;
gap: 0.45rem;
background: none;
border: none;
color: rgba(246, 240, 225, 0.7);
cursor: pointer;
font-family: var(--font-mono);
font-size: 0.88rem;
letter-spacing: 0.12em;
text-transform: uppercase;
padding: 0.4rem 1.1rem;
border-radius: var(--radius-xs);
transition: color var(--dur-fast) var(--ease-out),
background var(--dur-fast) var(--ease-out);
line-height: 1;
}
.locale-bar__btn:hover {
color: var(--paper);
background: rgba(255, 255, 255, 0.12);
}
.locale-bar__btn.is-active {
color: var(--paper-white);
background: rgba(255, 255, 255, 0.22);
font-weight: 600;
}
.locale-bar__btn span {
font-size: 0.86rem;
}
.locale-bar__sep {
color: rgba(246, 240, 225, 0.3);
font-family: var(--font-mono);
font-size: 1rem;
user-select: none;
padding: 0 0.1rem;
}
body {
padding-top: 42px;
}
@media (max-width: 600px) {
.locale-bar__btn span {
display: none;
}
.locale-bar__btn {
font-size: 1.1rem;
padding: 0.3rem 0.6rem;
letter-spacing: 0;
}
}
+171
View File
@@ -0,0 +1,171 @@
import { test, expect, type Page } from "@playwright/test";
async function getCookie(page: Page, name: string): Promise<string | undefined> {
const cookies = await page.context().cookies();
return cookies.find((c) => c.name === name)?.value;
}
// ─── Cookie banner ────────────────────────────────────────────────────────────
test.describe("Cookie banner", () => {
test.beforeEach(async ({ context }) => {
await context.clearCookies();
});
test("appears on first visit", async ({ page }) => {
await page.goto("/");
await expect(page.locator("[data-cookie-banner]")).toBeVisible();
});
test("accept all → banner hidden, cookie = all", async ({ page }) => {
await page.goto("/");
await page.click('[data-cookie-accept="all"]');
await expect(page.locator("[data-cookie-banner]")).toBeHidden();
expect(await getCookie(page, "dg_cookie_preferences")).toBe("all");
});
test("preference persists on reload", async ({ page }) => {
await page.goto("/");
await page.click('[data-cookie-accept="all"]');
await page.reload();
await expect(page.locator("[data-cookie-banner]")).toBeHidden();
});
test("essential only → cookie = essential", async ({ page }) => {
await page.goto("/");
await page.click('[data-cookie-accept="essential"]');
await expect(page.locator("[data-cookie-banner]")).toBeHidden();
expect(await getCookie(page, "dg_cookie_preferences")).toBe("essential");
});
});
// ─── Language switcher ────────────────────────────────────────────────────────
test.describe("Language switcher", () => {
test.beforeEach(async ({ context }) => {
await context.clearCookies();
});
test("defaults to EN", async ({ page }) => {
await page.goto("/");
await expect(page.locator("html")).toHaveAttribute("data-ui-lang", "en");
});
test("switches to NB and persists cookie", async ({ page }) => {
await page.goto("/");
await page.click('[data-lang-button="nb"]');
await expect(page.locator("html")).toHaveAttribute("data-ui-lang", "nb");
expect(await getCookie(page, "dg_ui_lang")).toBe("nb");
});
test("switches to FR", async ({ page }) => {
await page.goto("/");
await page.click('[data-lang-button="fr"]');
await expect(page.locator("html")).toHaveAttribute("data-ui-lang", "fr");
});
});
// ─── /ai-lab bug regression ───────────────────────────────────────────────────
test("no [object Object] on /ai-lab", async ({ page }) => {
await page.goto("/ai-lab");
const content = await page.content();
expect(content).not.toContain("[object Object]");
});
// ─── WritingIssue ─────────────────────────────────────────────────────────────
test("WritingIssue resolves to content or graceful error on /writing", async ({ page }) => {
await page.goto("/writing");
// Wait for the island to mount and finish its API call (succeeds or fails)
await page.waitForSelector(".writing-live, h2:text('The writing issue could not be loaded.')", {
timeout: 10_000,
});
// Must not still be in loading state
await expect(page.locator("h2:has-text('Loading the Boris Vian issue...')")).not.toBeVisible();
// Either resolved content or graceful error must be visible — not blank
const hasContent = await page.locator(".writing-live").isVisible();
const hasError = await page.locator("h2:has-text('The writing issue could not be loaded.')").isVisible();
expect(hasContent || hasError).toBeTruthy();
});
// ─── FamilyAtlas ─────────────────────────────────────────────────────────────
test.describe("FamilyAtlas", () => {
// Pre-set the cookie preference so the banner doesn't intercept clicks
test.beforeEach(async ({ context }) => {
await context.addCookies([
{ name: "dg_cookie_preferences", value: "essential", domain: "localhost", path: "/" },
]);
});
test("renders on /family-lab", async ({ page }) => {
await page.goto("/family-lab");
await page.waitForSelector(".family-atlas", { timeout: 8_000 });
await expect(page.locator(".family-atlas")).toBeVisible();
});
test("filter tab switches without crash", async ({ page }) => {
await page.goto("/family-lab");
await page.waitForSelector(".family-atlas", { timeout: 8_000 });
const tabs = page.locator('[role="tablist"] [role="tab"]');
const count = await tabs.count();
if (count < 2) return; // skip if fewer than 2 tabs
await tabs.nth(1).click();
await expect(page.locator(".family-atlas")).toBeVisible();
});
test("lightbox opens on tile click and closes with Escape", async ({ page }) => {
await page.goto("/family-lab");
await page.waitForSelector(".family-atlas", { timeout: 8_000 });
const tiles = page.locator(".family-atlas__tile");
const tileCount = await tiles.count();
if (tileCount === 0) return; // skip if no photos
await tiles.first().click();
await expect(page.locator('[role="dialog"]')).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.locator('[role="dialog"]')).not.toBeVisible();
});
});
// ─── Console errors ───────────────────────────────────────────────────────────
for (const path of ["/", "/ai-lab", "/writing", "/family-lab"]) {
test(`no unexpected console errors on ${path}`, async ({ page }) => {
const errors: string[] = [];
page.on("console", (msg) => {
if (msg.type() === "error") errors.push(msg.text());
});
page.on("pageerror", (err) => errors.push(err.message));
await page.goto(path);
// Allow React islands to mount and any API calls to settle
await page.waitForTimeout(3_000);
// Filter known benign errors: API 404s on /writing are expected locally
const unexpected = errors.filter(
(e) =>
!e.includes("/api/") &&
!e.includes("Failed to fetch") &&
!e.includes("NetworkError") &&
!e.includes("Failed to load resource"),
);
expect(unexpected).toHaveLength(0);
});
}
// ─── Mobile layout ───────────────────────────────────────────────────────────
test("navigation is visible on mobile", async ({ page }) => {
await page.goto("/");
await expect(page.locator("nav")).toBeVisible();
});