diff --git a/.github/workflows/build_preview.yaml b/.github/workflows/build_preview.yaml index 799ad822..7160e508 100644 --- a/.github/workflows/build_preview.yaml +++ b/.github/workflows/build_preview.yaml @@ -67,6 +67,28 @@ jobs: --alias ${{github.event.number}} shell: sh + - name: Accessibility audit + id: a11y + if: github.event_name == 'pull_request' + run: | + url=https://${{github.event.number}}--capro-uio.netlify.app + # the alias is not always live the instant `netlify deploy` returns + for i in $(seq 1 20); do + code=$(curl -s -o /dev/null -w '%{http_code}' "$url/") || code=000 + [ "$code" = "200" ] && break + echo "waiting for $url ($code)"; sleep 5 + done + npm ci --prefix scripts/a11y + BASE=https://${{github.event.number}}--capro-uio.netlify.app \ + node scripts/a11y/run.mjs --report "$GITHUB_WORKSPACE/a11y-report.md" + + - name: Comment PR – accessibility + if: always() && github.event_name == 'pull_request' && hashFiles('a11y-report.md') != '' + uses: thollander/actions-comment-pull-request@v3 + with: + comment-tag: a11y + file-path: a11y-report.md + - name: Comment PR – success if: success() && github.event_name == 'pull_request' uses: thollander/actions-comment-pull-request@v3 @@ -77,9 +99,20 @@ jobs: Please check it looks ok before merging. - name: Comment PR – failure - if: failure() && github.event_name == 'pull_request' + if: failure() && steps.a11y.outcome != 'failure' && github.event_name == 'pull_request' uses: thollander/actions-comment-pull-request@v3 with: comment-tag: preview message: | :x: Preview build failed. [View logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + + # The audit failing means the preview deployed fine but has a11y issues; + # the report is posted separately, so just keep the preview link honest. + - name: Comment PR – deployed but audit failed + if: always() && steps.a11y.outcome == 'failure' && github.event_name == 'pull_request' + uses: thollander/actions-comment-pull-request@v3 + with: + comment-tag: preview + message: | + :white_check_mark: Preview deployed at https://${{github.event.number}}--capro-uio.netlify.app + :warning: The accessibility audit found issues — see the report below. diff --git a/config/_default/hugo.yaml b/config/_default/hugo.yaml index 26cbbb86..b9e7c177 100644 --- a/config/_default/hugo.yaml +++ b/config/_default/hugo.yaml @@ -5,3 +5,9 @@ pagination.pagerSize: "6" summaryLength: "15" enableGitInfo: true defaultContentLanguage: en + +# Monokai's comment colour is 3.03:1 on its own background, below AA for body +# text. github-dark clears it at 6.15:1. +markup: + highlight: + style: github-dark diff --git a/content/funfacts/_index.en.md b/content/funfacts/_index.en.md index d1f63bbe..f930bec5 100644 --- a/content/funfacts/_index.en.md +++ b/content/funfacts/_index.en.md @@ -11,4 +11,7 @@ item: - title : "Cups of Coffee" icon : "fa-solid fa-mug-hot" count : "250" +build: + render: false + list: false --- diff --git a/content/funfacts/_index.no.md b/content/funfacts/_index.no.md index fdbdcae0..b113232f 100644 --- a/content/funfacts/_index.no.md +++ b/content/funfacts/_index.no.md @@ -11,4 +11,7 @@ item: - title : "Kaffekopper" icon : "fa-solid fa-mug-hot" count : "250" +build: + render: false + list: false --- diff --git a/content/news/2025/03-nettskjemar/index.en.md b/content/news/2025/03-nettskjemar/index.en.md index cacf1a33..909ccba4 100644 --- a/content/news/2025/03-nettskjemar/index.en.md +++ b/content/news/2025/03-nettskjemar/index.en.md @@ -23,9 +23,9 @@ The package has extensive documentation [online](/nettskjemar), and we recommend This package is designed to help you effortlessly retrieve, process, and analyze survey data, all while being tidyverse-compatible. Below, we highlight some of the key features of `nettskjemar` and why it might become your go-to tool for working with Nettskjema. -### Key Features of `nettskjemar` +## Key Features of `nettskjemar` -#### **1. Download Form Submissions with Ease** +### **1. Download Form Submissions with Ease** With just a few lines of R code, you can pull survey answers into a tidy dataframe. This allows you to start working with your data immediately without spending time on manual downloads or file conversions. @@ -38,7 +38,7 @@ formid <- 123823 data <- ns_get_data(formid) ``` -#### **2. Raw Data in Long Format** +### **2. Raw Data in Long Format** For those interested in timestamps or a detailed breakdown of how survey respondents interacted with individual questions, the package allows retrieval of **raw data** in long format. This structure is perfect for analyzing response times or other granular aspects, and is only available to researchers through the API. @@ -47,7 +47,7 @@ This structure is perfect for analyzing response times or other granular aspects raw_data <- ns_get_data(formid, type = "long") ``` -#### **3. Checkbox Matrix Handling** +### **3. Checkbox Matrix Handling** Nettskjema includes an advanced checkbox matrix functionality, but the exported data can sometimes be tricky to manage. Fortunately, `nettskjemar` offers tools to simplify and reshape checkbox data into user-friendly formats: such as as lists, delimited strings, or separated rows. @@ -59,7 +59,7 @@ ns_get_data(formid) |> ns_alter_checkbox(to = "character", sep = ";") ``` -#### **4. Tidyverse-Compatible** +### **4. Tidyverse-Compatible** The data retrieved with `nettskjemar` is fully compatible with the tidyverse ecosystem, making it an intuitive choice for R users already familiar with `dplyr`, `tidyr`, or `ggplot2`. You can chain commands and explore your survey data with remarkable ease. @@ -82,14 +82,14 @@ ns_get_data(formid) |> separate_rows(checkbox_matrix.1) ``` -### **Before Getting Started: Codebook Setup** +## **Before Getting Started: Codebook Setup** To ensure smooth analysis in R, it’s recommended to enable and configure the **codebook** feature in the Nettskjema portal. This makes it easier to work with well-labeled, organized data. Head to the Nettskjema Portal’s *General Settings* and activate the "Codebook" option for your form. [Learn more about codebooks here](https://www.uio.no/tjenester/it/adm-app/nettskjema/hjelp/kodebok.html) (content in Norwegian). --- -### Why Use `nettskjemar`? +## Why Use `nettskjemar`? - **Time-Saving**: Skip manual data downloads—seamlessly fetch survey submissions directly into R. - **Customizable**: Tidy data the way you want—reshape checkbox matrices, fetch raw tall data, or stick with wide-form tibbles. diff --git a/content/news/2025/03-nettskjemar/index.no.md b/content/news/2025/03-nettskjemar/index.no.md index 517929c2..7d75955f 100644 --- a/content/news/2025/03-nettskjemar/index.no.md +++ b/content/news/2025/03-nettskjemar/index.no.md @@ -22,9 +22,9 @@ Nedenfor fremhever vi noen av de viktigste funksjonene i `nettskjemar` og hvorfo --- -### Viktige funksjoner i `nettskjemar` +## Viktige funksjoner i `nettskjemar` -#### **1. Last ned skjemabesvarelser med enkelhet** +### **1. Last ned skjemabesvarelser med enkelhet** Med bare noen få linjer R-kode kan du hente svar til et ryddig data format. Dette lar deg starte arbeidet med dataene dine umiddelbart uten å bruke tid på manuelle nedlastinger eller filkonverteringer. @@ -36,7 +36,7 @@ formid <- 123823 data <- ns_get_data(formid) ``` -#### **2. Rådata i langformat** +### **2. Rådata i langformat** For de som er interessert i tidsstempler eller en detaljert oversikt over hvordan respondenter har interagert med individuelle spørsmål, tillater pakken henting av **rådata** i langt format. Denne strukturen er perfekt for å analysere responstider eller andre detaljer, og er kun tilgjengelig for forskere gjennom API-et. @@ -45,7 +45,7 @@ Denne strukturen er perfekt for å analysere responstider eller andre detaljer, raw_data <- ns_get_data(formid, type = "long") ``` -#### **3. Håndtering av avkrysningsmatrise** +### **3. Håndtering av avkrysningsmatrise** Nettskjema inkluderer et avansert avkrysningsmatriseverktøy (checkbox matrix), men de eksporterte dataene kan noen ganger være utfordrende å jobbe med. Heldigvis tilbyr `nettskjemar` verktøy for å forenkle og omstrukturere avkrysningsdata til brukervennlige formater, som lister eller separerte strenger. @@ -57,7 +57,7 @@ ns_get_data(formid) |> ns_alter_checkbox(to = "character", sep = ";") ``` -#### **4. Tidyverse-kompatibel** +### **4. Tidyverse-kompatibel** Dataene hentet med `nettskjemar` er fullt kompatible med tidyverse-økosystemet, noe som gjør det til et intuitivt valg for R-brukere som allerede er kjent med `dplyr`, `tidyr` eller `ggplot2`. Du kan enkelt kjede kommandoer og utforske undersøkelsesdataene dine. ```r @@ -79,7 +79,7 @@ ns_get_data(formid) |> separate_rows(checkbox_matrix.1) ``` -### **Før du starter: Aktivering av kodeboken** +## **Før du starter: Aktivering av kodeboken** For å sikre en god arbeidsflyt i R, anbefales det å aktivere og konfigurere **kodebok**-funksjonen i Nettskjema-portalen. Dette gjør det enklere å jobbe med godt merkede og organiserte data. @@ -88,7 +88,7 @@ Gå til Nettskjema-portalen under *Generelle innstillinger* og aktiver "Kodebok" --- -### Hvorfor bruke `nettskjemar`? +## Hvorfor bruke `nettskjemar`? - **Tidsbesparende**: Hopp over manuelle nedlastinger—hent besvarelser direkte i R. - **Tilpassbart**: Utform dataene slik du ønsker—endre avkrysningsmatriser, jobb med rådata i lange formater eller behold brede dataframes. diff --git a/i18n/en.yaml b/i18n/en.yaml index b95cd3a5..a5fd811a 100644 --- a/i18n/en.yaml +++ b/i18n/en.yaml @@ -6,6 +6,8 @@ - authorsPubs: Publications by - all: All +- skipToContent: Skip to content + # Months - January: January - February: February diff --git a/i18n/no.yaml b/i18n/no.yaml index c4aa443e..65b5a81a 100644 --- a/i18n/no.yaml +++ b/i18n/no.yaml @@ -7,6 +7,8 @@ - all: Alle - contact: Kontakt +- skipToContent: Hopp til innhold + # Months - January: Januar - February: Februar diff --git a/scripts/a11y/README.md b/scripts/a11y/README.md new file mode 100644 index 00000000..0daa3954 --- /dev/null +++ b/scripts/a11y/README.md @@ -0,0 +1,59 @@ +# Accessibility gate + +Runs on every pull request against the deployed Netlify preview, and fails the +job if anything regresses. The report is posted as a PR comment. + +## Running it locally + +```sh +hugo server # in one terminal +npm install --prefix scripts/a11y # once +BASE=http://localhost:1313 npm run audit --prefix scripts/a11y +``` + +Against a preview instead: + +```sh +BASE=https://23--capro-uio.netlify.app npm run audit --prefix scripts/a11y +``` + +## What it checks + +**axe-core** (WCAG 2.0 / 2.1 / 2.2 A + AA, plus best-practice) at desktop and +mobile, including the nav states that only exist after a click. + +By default it scans one page per layout — every language's home page, every +section landing page, and the first child of each section — which catches +layout-level regressions without scanning every news post. `--all` scans every +route in the sitemap and is worth running before a release. + +**Checks axe cannot make:** + +- the skip link is the first tab stop and becomes visible when focused +- no keyboard trap +- a visible focus indicator on every interactive element +- `prefers-reduced-motion` is honoured, and content is still visible when the + scroll observer never fires +- no horizontal scrolling at 320px (400% zoom) or 640px (200% zoom) +- `lang`, `title`, image `alt` and link labels on each page + +## Things that will trip you up + +**Settle animations before measuring.** `.reveal` fades in. Sampling a colour +mid-fade composites it against the background and reports a contrast failure +that does not ship. The script disables transitions before revealing anything. + +**Do not emulate zoom with `body { zoom }`.** Real browser zoom shrinks the CSS +viewport, so media queries respond to it. `zoom` does not, so the desktop nav +stays on screen at double size and reports an overflow that cannot happen. +Resize the viewport instead — that is what the script does. + +**Redirect stubs.** A page with a `` navigates away +mid-scan and axe audits the destination site. The script detects and skips them +before navigating. + +**A clean run is a floor, not a ceiling.** Automated tooling catches roughly a +third of real accessibility problems. It cannot tell you whether alt text is +*meaningful*, whether the copy is understandable, or how the site sounds in a +screen reader. Test with VoiceOver or NVDA before claiming the site is +accessible. diff --git a/scripts/a11y/package-lock.json b/scripts/a11y/package-lock.json new file mode 100644 index 00000000..10d47d28 --- /dev/null +++ b/scripts/a11y/package-lock.json @@ -0,0 +1,373 @@ +{ + "name": "capro-a11y", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "capro-a11y", + "version": "1.0.0", + "dependencies": { + "axe-core": "^4.14.0", + "puppeteer": "^25.14.0" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.5.tgz", + "integrity": "sha512-UoCo3Pynbm37aLEEoydSiG+t24ui7sLD40+07CKxzxNy4PpSowJMkvlLIiV67XjP1lMVynhQVnuwlTABqBH8LQ==", + "license": "Apache-2.0", + "dependencies": { + "modern-tar": "^0.8.5", + "yargs": "^18.0.0" + }, + "bin": { + "browsers": "lib/main-cli.js" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/axe-core": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.14.0.tgz", + "integrity": "sha512-9WTZxEjsZ7b13TH8JPmbV2z8CHbl80/2hm3XPEG4JgNdQLK81IBRXmSxHfMAOkSqQeRxT/0dwNDz2GOm3zzpcQ==", + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/chromium-bidi": { + "version": "157.0.8090-0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-157.0.8090-0.tgz", + "integrity": "sha512-KJw7IQgqXwxpJgJlEIPg5mr2ZoqA+jkTRtc5EABI7sUJ3bu3cpI7NuEBoj4Gn1AsF78CaT9So5WQWspK3/wYag==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^4.6.5" + }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1696802", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1696802.tgz", + "integrity": "sha512-FO5/SENfvc2K8wRnWQflKcwuoTH4dBkXCWTQ9ebIWtuX8JM5+ZvCotaEmkfh21pO7TvIKGTM3hG4xFiq9WTwmg==", + "license": "BSD-3-Clause" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/modern-tar": { + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.6.tgz", + "integrity": "sha512-LYSks0tNVqbtez/9OB0YFP6qLRSqkdAxJWOQpVaR67lNTbj2zNLyYpowkRMia5lXLCZKzxwoxkxH2MbbBR2aEg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/puppeteer": { + "version": "25.14.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.14.0.tgz", + "integrity": "sha512-+s1Iqbl9DsGLOaQdgCQyhtgHZsF9hi4Iu8iuBqxzraxUXoRG1QkYQI0CA1/VsNK1gZWJfs50BL/oOb1L+Yz8Yg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.2.5", + "chromium-bidi": "157.0.8090-0", + "devtools-protocol": "0.0.1696802", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.14.0", + "typed-query-selector": "^2.12.3" + }, + "bin": { + "puppeteer": "lib/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/puppeteer-core": { + "version": "25.14.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.14.0.tgz", + "integrity": "sha512-LQitnes3tBeVbdZ7V/4iwJPLZGgkl9uBVK0W7YYMh68j/3Y9eNuvj4yl+Fdo7zN0SuajOMrhd7aPj3DifK8k6A==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.2.5", + "chromium-bidi": "157.0.8090-0", + "devtools-protocol": "0.0.1696802", + "typed-query-selector": "^2.12.3", + "webdriver-bidi-protocol": "1.0.1", + "ws": "^8.22.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/string-width": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", + "license": "MIT" + }, + "node_modules/webdriver-bidi-protocol": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-1.0.1.tgz", + "integrity": "sha512-WXWs/aXF2a6Al1Z2/V/7Gp2oPZOAAmHjJOTmv4KCXRJ122IMj3tSSMc6a4q3K4QOmhQbse/Ahr+oTVJOU2dUiw==", + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/scripts/a11y/package.json b/scripts/a11y/package.json new file mode 100644 index 00000000..6b5cb1d8 --- /dev/null +++ b/scripts/a11y/package.json @@ -0,0 +1,15 @@ +{ + "name": "capro-a11y", + "private": true, + "version": "1.0.0", + "description": "Accessibility gate: axe-core plus the checks axe cannot make", + "type": "module", + "scripts": { + "audit": "node run.mjs", + "audit:all": "node run.mjs --all" + }, + "dependencies": { + "axe-core": "^4.14.0", + "puppeteer": "^25.14.0" + } +} diff --git a/scripts/a11y/run.mjs b/scripts/a11y/run.mjs new file mode 100644 index 00000000..cffcb752 --- /dev/null +++ b/scripts/a11y/run.mjs @@ -0,0 +1,339 @@ +#!/usr/bin/env node +/** + * Accessibility gate. + * + * Two passes against a running site: + * 1. axe-core (WCAG 2.0/2.1/2.2 A + AA, plus best-practice) over a sample of + * routes at desktop and mobile, including the nav states that only exist + * after a click. + * 2. Checks axe cannot make: skip link, keyboard order, focus indicators, + * reduced motion, reflow at 320/640px, and per-page alt/lang/title. + * + * Usage: + * BASE=https://… node scripts/a11y/run.mjs [--all] [--report out.md] + * + * --all scans every route in the sitemap instead of one page per layout. + * Exits non-zero if anything fails. + */ +import puppeteer from 'puppeteer'; +import axePkg from 'axe-core'; +import { writeFileSync } from 'node:fs'; + +const axeSource = axePkg.source; +const args = process.argv.slice(2); +const BASE = (process.env.BASE || 'http://localhost:1313').replace(/\/$/, ''); +const SCAN_ALL = args.includes('--all'); +const REPORT = args.includes('--report') ? args[args.indexOf('--report') + 1] : null; + +const TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa', 'best-practice']; +const VIEWPORTS = [ + { name: 'desktop', width: 1440, height: 900, isMobile: false }, + { name: 'mobile', width: 390, height: 844, isMobile: true }, +]; + +const violations = []; +const checks = []; +const note = (check, pass, detail = '') => checks.push({ check, pass, detail }); + +/** Every in the sitemap, following sitemap indexes. */ +async function allRoutes() { + const grab = async (u) => { + const r = await fetch(u); + if (!r.ok) return []; + const x = await r.text(); + return [...x.matchAll(/([^<]+)<\/loc>/g)].map((m) => m[1]); + }; + const roots = await grab(`${BASE}/sitemap.xml`); + const locs = []; + for (const l of roots) locs.push(...(l.endsWith('.xml') ? await grab(l) : [l])); + const paths = locs + .map((l) => l.replace(/^https?:\/\/[^/]+/, '') || '/') + .filter((p) => !/\/(categories|tags)\//.test(p)); + return [...new Set(['/', ...paths])].sort(); +} + +/** + * One route per distinct layout: the home page of each language, every + * section landing page, and the first child of each section. Catches + * layout-level regressions without scanning every news post. + */ +function sampleByLayout(routes) { + const keep = new Set(); + const childSeen = new Set(); + for (const r of routes) { + const parts = r.split('/').filter(Boolean); + const lang = parts[0] === 'no' ? 'no' : 'en'; + const rest = lang === 'no' ? parts.slice(1) : parts; + if (rest.length === 0) keep.add(r); // home, per language + else if (rest.length === 1) keep.add(r); // section landing page + else { + const key = `${lang}:${rest[0]}`; // first child of each section + if (!childSeen.has(key)) { childSeen.add(key); keep.add(r); } + } + } + return [...keep].sort(); +} + +/** Meta-refresh stubs navigate away mid-scan; axe would audit the destination. */ +async function isRedirectStub(url) { + const html = await fetch(BASE + url).then((r) => r.text()).catch(() => ''); + return /http-equiv=["']?refresh/i.test(html); +} + +async function settle(page) { + await page.evaluate(() => { + // Kill transitions first, then reveal: otherwise axe samples elements + // mid-fade and composites a darker colour than actually ships. + const s = document.createElement('style'); + s.textContent = '*,*::before,*::after{transition:none !important;animation:none !important}'; + document.head.appendChild(s); + document.querySelectorAll('.reveal').forEach((e) => e.classList.add('visible')); + }); + // Leaflet renders after networkidle2 fires. Without this the map's + // attribution is sometimes absent at scan time, so results flip between + // runs. Wait for it when a map is on the page. + if (await page.$('#contact-map')) { + await page + .waitForFunction(() => !!document.querySelector('.leaflet-control-attribution a'), + { timeout: 8000 }) + .catch(() => {}); + } + await new Promise((r) => setTimeout(r, 350)); +} + +async function axeScan(page, url, viewport, state, prep) { + await page.goto(BASE + url, { waitUntil: 'networkidle2' }); + await settle(page); + if (prep) await prep(page); + await page.evaluate(axeSource); + const res = await page.evaluate( + async (tags) => window.axe.run(document, { runOnly: { type: 'tag', values: tags } }), + TAGS, + ); + for (const v of res.violations) { + violations.push({ + url, viewport: viewport.name, state, id: v.id, impact: v.impact, + help: v.help, helpUrl: v.helpUrl, count: v.nodes.length, + nodes: v.nodes.slice(0, 3).map((n) => ({ + target: n.target.join(' '), + summary: (n.failureSummary || '').replace(/\s+/g, ' ').slice(0, 220), + })), + }); + } +} + +const browser = await puppeteer.launch({ + headless: true, + args: ['--no-sandbox', '--disable-dev-shm-usage'], +}); + +const everything = await allRoutes(); +const routes = []; +for (const r of SCAN_ALL ? everything : sampleByLayout(everything)) { + if (await isRedirectStub(r)) continue; + // A stale sitemap entry would otherwise be audited as the 404 page under + // the wrong URL. The 404 is worth checking, but on purpose (below). + const code = await fetch(BASE + r, { method: 'HEAD' }) + .then((x) => x.status).catch(() => 0); + if (code === 200) routes.push(r); +} +routes.push('/404.html'); // a page real users land on + +// ---------------------------------------------------------------- axe pass +for (const viewport of VIEWPORTS) { + const page = await browser.newPage(); + await page.setViewport(viewport); + for (const url of routes) { + try { + await axeScan(page, url, viewport, 'default'); + } catch (e) { + violations.push({ url, viewport: viewport.name, state: 'default', id: 'SCAN_ERROR', + impact: 'critical', help: String(e).slice(0, 160), count: 1, nodes: [] }); + } + } + // nav states that only exist after a click + const sel = viewport.isMobile + ? 'header button[aria-label="Toggle navigation"]' + : '.nav-group__btn'; + try { + await axeScan(page, '/', viewport, viewport.isMobile ? 'mobile-menu-open' : 'nav-open', + async (p) => { + const el = await p.$(sel); + if (!el) return; // control absent on this branch + await el.click(); + await new Promise((r) => setTimeout(r, 400)); + }); + } catch { /* control not present; nothing to audit */ } + await page.close(); +} + +// ------------------------------------------------------------- manual pass +{ + const page = await browser.newPage(); + await page.setViewport({ width: 1440, height: 900 }); + const first = routes.find((r) => r !== '/') || '/'; + + // skip link + await page.goto(BASE + first, { waitUntil: 'networkidle2' }); + await page.keyboard.press('Tab'); + await new Promise((r) => setTimeout(r, 300)); + const sl = await page.evaluate(() => { + const a = document.activeElement; + return { href: a.getAttribute('href'), top: a.getBoundingClientRect().top }; + }); + note('skip link is the first tab stop', sl.href === '#main', `href=${sl.href}`); + note('skip link is visible when focused', sl.top >= 0, `top=${Math.round(sl.top)}`); + + // keyboard: no trap + await page.goto(BASE + '/', { waitUntil: 'networkidle2' }); + const stops = new Set(); + for (let i = 0; i < 25; i++) { + await page.keyboard.press('Tab'); + const d = await page.evaluate(() => { + const a = document.activeElement; + return !a || a === document.body ? null + : `${a.tagName}.${(a.className || '').toString().split(' ')[0]}:${(a.textContent || '').trim().slice(0, 18)}`; + }); + if (d) stops.add(d); + } + note('no keyboard trap in the first 25 stops', stops.size > 5, `${stops.size} distinct stops`); + + // focus indicators + const fi = await page.evaluate(() => { + const out = []; + for (const s of ['a[href="#main"]', '.btn', 'footer a', '.nav-group__btn', 'header nav a']) { + const el = document.querySelector(s); + if (!el) continue; + el.focus(); + const cs = getComputedStyle(el); + out.push({ s, ok: (parseFloat(cs.outlineWidth) || 0) > 0 && cs.outlineStyle !== 'none', + outline: `${cs.outlineStyle} ${cs.outlineWidth}` }); + } + return out; + }); + for (const f of fi) note(`focus indicator on ${f.s}`, f.ok, f.outline); + await page.close(); +} + +{ + // reduced motion: content must not depend on the scroll observer + const page = await browser.newPage(); + await page.setViewport({ width: 1440, height: 900 }); + await page.emulateMediaFeatures([{ name: 'prefers-reduced-motion', value: 'reduce' }]); + await page.goto(BASE + '/', { waitUntil: 'networkidle2' }); + const r = await page.evaluate(() => { + const el = document.querySelector('.reveal') || document.body; + const cs = getComputedStyle(el); + const reveals = [...document.querySelectorAll('.reveal')]; + return { + transition: cs.transitionDuration, animation: cs.animationDuration, + scroll: getComputedStyle(document.documentElement).scrollBehavior, + allVisible: reveals.length === 0 || reveals.every((e) => parseFloat(getComputedStyle(e).opacity) > 0.9), + }; + }); + const near0 = (v) => v.split(',').every((x) => parseFloat(x) <= 0.02); + note('reduced motion: transitions suppressed', near0(r.transition), r.transition); + note('reduced motion: animations suppressed', near0(r.animation), r.animation); + note('reduced motion: smooth scroll disabled', r.scroll === 'auto', r.scroll); + note('reduced motion: content visible without the observer', r.allVisible); + await page.close(); +} + +{ + // Reflow (WCAG 1.4.10). Browser zoom shrinks the CSS viewport, so media + // queries respond to it; emulating with body{zoom} does not and gives + // false desktop-nav overflow. Resize the viewport instead. + const sample = routes.slice(0, 8); + for (const [label, vp] of [ + ['320px (400% zoom)', { width: 320, height: 800 }], + ['640px (200% zoom)', { width: 640, height: 900 }], + ]) { + const page = await browser.newPage(); + await page.setViewport(vp); + for (const url of sample) { + await page.goto(BASE + url, { waitUntil: 'networkidle2' }); + await new Promise((r) => setTimeout(r, 200)); + const m = await page.evaluate(() => ({ + sw: document.documentElement.scrollWidth, iw: window.innerWidth, + })); + note(`reflow ${label} ${url}`, m.sw <= m.iw + 1, `scrollWidth ${m.sw} vs ${m.iw}`); + } + await page.close(); + } +} + +{ + // per-page basics + const page = await browser.newPage(); + for (const url of routes.slice(0, 10)) { + await page.goto(BASE + url, { waitUntil: 'networkidle2' }); + const r = await page.evaluate(() => ({ + lang: document.documentElement.lang, + title: document.title, + noAlt: [...document.images].filter((i) => !i.hasAttribute('alt')).length, + unlabelled: [...document.querySelectorAll('a')].filter( + (a) => !a.textContent.trim() && !a.getAttribute('aria-label') + && !a.querySelector('img[alt]:not([alt=""])')).length, + })); + note(`lang set ${url}`, !!r.lang, r.lang); + note(`title set ${url}`, !!r.title, r.title); + note(`images have alt ${url}`, r.noAlt === 0, `${r.noAlt} missing`); + note(`links are labelled ${url}`, r.unlabelled === 0, `${r.unlabelled} unlabelled`); + } + await page.close(); +} + +await browser.close(); + +// ------------------------------------------------------------------ report +const byId = new Map(); +for (const v of violations) { + if (!byId.has(v.id)) byId.set(v.id, { ...v, pages: new Set(), instances: 0 }); + const e = byId.get(v.id); + e.pages.add(v.url); + e.instances += v.count; +} +const order = { critical: 0, serious: 1, moderate: 2, minor: 3 }; +const grouped = [...byId.values()].sort((a, b) => (order[a.impact] ?? 9) - (order[b.impact] ?? 9)); +const failed = checks.filter((c) => !c.pass); +const pass = grouped.length === 0 && failed.length === 0; + +const md = []; +md.push(pass ? '## ✅ Accessibility: no issues' : '## ❌ Accessibility issues found'); +md.push(''); +md.push(`axe-core over ${routes.length} route${routes.length === 1 ? '' : 's'} × ${VIEWPORTS.length} viewports${SCAN_ALL ? ' (full sitemap)' : ' (one page per layout)'}, plus ${checks.length} manual checks.`); +md.push(''); +if (grouped.length) { + md.push(`### axe violations — ${grouped.reduce((s, g) => s + g.instances, 0)} instances, ${grouped.length} type${grouped.length === 1 ? '' : 's'}`); + md.push(''); + md.push('| Impact | Rule | Pages | Instances |'); + md.push('|---|---|---|---|'); + for (const g of grouped) { + md.push(`| ${g.impact ?? 'n/a'} | [${g.id}](${g.helpUrl ?? '#'}) — ${g.help} | ${g.pages.size} | ${g.instances} |`); + } + md.push(''); + md.push('
First failing node per rule'); + md.push(''); + for (const g of grouped) { + md.push(`**${g.id}** — \`${[...g.pages][0]}\``); + for (const n of g.nodes.slice(0, 1)) md.push(`- \`${n.target}\`: ${n.summary}`); + md.push(''); + } + md.push('
'); + md.push(''); +} +if (failed.length) { + md.push(`### Manual checks — ${failed.length} of ${checks.length} failing`); + md.push(''); + for (const f of failed) md.push(`- **${f.check}**${f.detail ? ` — ${f.detail}` : ''}`); + md.push(''); +} +if (pass) md.push(`All ${checks.length} manual checks passed.`); +md.push(''); +md.push('Automated tooling catches roughly a third of accessibility problems. A clean run is a floor, not a ceiling.'); + +const out = md.join('\n'); +if (REPORT) writeFileSync(REPORT, out); +console.log(out); + +if (!pass) process.exit(1); diff --git a/themes/capro-tw/assets/css/components/buttons.css b/themes/capro-tw/assets/css/components/buttons.css index 720ff74e..4efa01d4 100644 --- a/themes/capro-tw/assets/css/components/buttons.css +++ b/themes/capro-tw/assets/css/components/buttons.css @@ -22,7 +22,7 @@ .btn-outline:hover { border-color: var(--color-primary); - color: var(--color-primary); + color: var(--color-primary-text); } .btn-hero { diff --git a/themes/capro-tw/assets/css/components/layout.css b/themes/capro-tw/assets/css/components/layout.css index aad48d4f..46da8b42 100644 --- a/themes/capro-tw/assets/css/components/layout.css +++ b/themes/capro-tw/assets/css/components/layout.css @@ -15,7 +15,9 @@ @apply text-center mb-12; } - .section-title h2 { + .section-title h1, + .section-title h2, + .section-title__label { @apply text-4xl font-bold capitalize; color: var(--color-body); } @@ -61,4 +63,50 @@ opacity: 1; transform: translateY(0); } + + .skip-link { + @apply absolute left-4 z-100 px-4 py-3 text-sm font-semibold; + top: -100%; + background-color: var(--color-primary); + color: #fff; + transition: top var(--transition-fast); + } + + .skip-link:focus, + .skip-link:focus-visible { + top: 0.5rem; + color: #fff; + } + + /* Links in running prose must be distinguishable without colour. */ + .prose a { + text-decoration: underline; + text-underline-offset: 3px; + text-decoration-thickness: 1px; + } + + /* .reveal starts hidden and is shown by scroll-reveal.js. If motion is + reduced the animation has no purpose, so show everything immediately + rather than making visibility depend on the observer firing. */ + @media (prefers-reduced-motion: reduce) { + .reveal { + opacity: 1; + transform: none; + } + } + + /* Long code lines must scroll inside their own block, not widen the page + (WCAG 1.4.10 reflow). */ + .prose pre, + .highlight pre, + .highlight { + max-width: 100%; + overflow-x: auto; + } + + .prose table { + display: block; + max-width: 100%; + overflow-x: auto; + } } diff --git a/themes/capro-tw/assets/css/components/nav.css b/themes/capro-tw/assets/css/components/nav.css index 3fcffd78..e1fbc39e 100644 --- a/themes/capro-tw/assets/css/components/nav.css +++ b/themes/capro-tw/assets/css/components/nav.css @@ -19,7 +19,7 @@ .site-nav a:hover, .site-nav a.active { - color: var(--color-primary); + color: var(--color-primary-text); } .lang-select { diff --git a/themes/capro-tw/assets/css/components/pubs.css b/themes/capro-tw/assets/css/components/pubs.css index f439f751..99944681 100644 --- a/themes/capro-tw/assets/css/components/pubs.css +++ b/themes/capro-tw/assets/css/components/pubs.css @@ -32,7 +32,7 @@ } .pubs-table .pub-title a:hover { - color: var(--color-primary); + color: var(--color-primary-text); } .pubs-table .pub-journal { diff --git a/themes/capro-tw/assets/css/components/sections.css b/themes/capro-tw/assets/css/components/sections.css index e8d7df31..529365f8 100644 --- a/themes/capro-tw/assets/css/components/sections.css +++ b/themes/capro-tw/assets/css/components/sections.css @@ -182,7 +182,7 @@ .testimonial-nav:hover { border-color: var(--color-primary); - color: var(--color-primary); + color: var(--color-primary-text); } .testimonial-nav-left { @@ -224,7 +224,7 @@ .contact-icon { @apply mr-4 mt-1; font-size: 20px; - color: var(--color-primary); + color: var(--color-primary-text); } /* Portfolio */ diff --git a/themes/capro-tw/assets/css/components/team.css b/themes/capro-tw/assets/css/components/team.css index 9340cc1c..feeaf5c4 100644 --- a/themes/capro-tw/assets/css/components/team.css +++ b/themes/capro-tw/assets/css/components/team.css @@ -48,7 +48,7 @@ } .social-overlay a:hover { - color: var(--color-primary); + color: var(--color-primary-text); } .team-meta { diff --git a/themes/capro-tw/assets/css/main.css b/themes/capro-tw/assets/css/main.css index bdc5f9bb..2cc9d456 100644 --- a/themes/capro-tw/assets/css/main.css +++ b/themes/capro-tw/assets/css/main.css @@ -123,3 +123,13 @@ @import "./components/team.css"; @import "./components/pubs.css"; @import "./components/footer-waves.css"; + +/* Leaflet's stylesheet is unlayered and loads after ours, so an @layer rule + can never override it however specific. Its attribution links are #0078a8 + on #333 text (2.55:1), under the 3:1 that link-in-text-block requires; + darkening moves them closer to the surrounding text, so underline instead. + Unlayered and two classes deep so it actually wins. */ +.leaflet-container .leaflet-control-attribution a { + text-decoration: underline; + text-underline-offset: 2px; +} diff --git a/themes/capro-tw/assets/css/vendor/tailwind.css b/themes/capro-tw/assets/css/vendor/tailwind.css index a6f2f9fc..7478b465 100644 --- a/themes/capro-tw/assets/css/vendor/tailwind.css +++ b/themes/capro-tw/assets/css/vendor/tailwind.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-tracking:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-sans:"Space Grotesk", sans-serif;--font-serif:"Spectral", serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-7xl:4.5rem;--text-7xl--line-height:1;--text-8xl:6rem;--text-8xl--line-height:1;--font-weight-light:300;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wider:.05em;--tracking-widest:.1em;--leading-relaxed:1.625;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-primary:#c04545;--color-primary-text:#e57575;--color-primary-dark:#b84444;--color-secondary:#85bfbf;--color-dark:#20242b;--color-grey:#2a2f36;--color-light:#8a8e93;--color-body:#d0d2d4;--color-muted:#9a9da0;--color-border:#eceff11f;--transition-fast:.2s ease;--transition-base:.3s ease;--transition-slow:.6s ease}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}@font-face{font-family:Space Grotesk;src:url(/webfonts/google-fonts/space-grotesk/space-grotesk-latin-300-normal.woff2)format("woff2");font-weight:300;font-display:swap}@font-face{font-family:Space Grotesk;src:url(/webfonts/google-fonts/space-grotesk/space-grotesk-latin-400-normal.woff2)format("woff2");font-weight:400;font-display:swap}@font-face{font-family:Space Grotesk;src:url(/webfonts/google-fonts/space-grotesk/space-grotesk-latin-500-normal.woff2)format("woff2");font-weight:500;font-display:swap}@font-face{font-family:Spectral;src:url(/webfonts/google-fonts/spectral/spectral-latin-300-normal.woff2)format("woff2");font-weight:300;font-display:swap}html{scroll-behavior:smooth;background-color:var(--color-grey);color:var(--color-body)}@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,:before,:after{transition-duration:.01ms!important;animation-duration:.01ms!important}}body{font-family:var(--font-sans);-webkit-font-smoothing:antialiased;font-size:18px;line-height:1.6}@media (min-width:768px){body{font-size:20px}}h1,h2,h3,h4,h5,h6{font-family:var(--font-sans);color:var(--color-secondary);line-height:1.2}h1{font-weight:300}h2{font-weight:500}h3,h4,h5,h6{font-weight:400}p{font-family:var(--font-serif);color:var(--color-body);line-height:1.7}a{color:var(--color-secondary);transition:color var(--transition-fast);text-decoration:none}a:hover{color:var(--color-body)}:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}::selection{background:var(--color-primary);color:var(--color-body)}}@layer components{.section{padding-top:6rem;padding-bottom:6rem}@media (max-width:768px){.section{padding-top:3rem;padding-bottom:3rem}}.section-title{margin-bottom:calc(var(--spacing) * 12);text-align:center}.section-title h2{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);text-transform:capitalize;color:var(--color-body)}.section-title .accent{color:var(--color-primary-text)}.border-accent{border-top:2px solid var(--color-border);width:30%;height:1px;margin:15px auto 0;position:relative}.border-accent:before{content:"";background-color:var(--color-primary);width:50px;height:6px;margin-left:-25px;position:absolute;top:-4px;left:50%}.bg-dark{background-color:var(--color-dark)}.bg-grey{background-color:var(--color-grey)}.reveal{opacity:0;transition:opacity var(--transition-slow), transform var(--transition-slow);transform:translateY(24px)}.reveal.visible{opacity:1;transform:translateY(0)}.site-nav{top:calc(var(--spacing) * 0);z-index:50;background-color:var(--color-dark);transition:border-color var(--transition-base), box-shadow var(--transition-base);border-bottom:1px solid #0000;position:sticky}.site-nav.nav-scrolled{border-bottom-color:var(--color-border);box-shadow:0 2px 12px #0000004d}.site-nav a{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider);text-transform:uppercase;color:var(--color-body);transition:color var(--transition-fast)}.site-nav a:hover,.site-nav a.active{color:var(--color-primary)}.lang-select{cursor:pointer;border-style:var(--tw-border-style);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));appearance:none;background:var(--color-dark);color:var(--color-body);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23d0d2d4' stroke-width='2' fill='none'/%3E%3C/svg%3E");background-position:right 6px center;background-repeat:no-repeat;border-width:0;padding:4px 24px 4px 8px}.footer-social-icon{height:calc(var(--spacing) * 12);width:calc(var(--spacing) * 12);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));color:var(--color-light);border:1px solid var(--color-border);transition:all var(--transition-base);border-radius:3.40282e38px;justify-content:center;align-items:center;display:flex}.footer-social-icon:hover{background-color:var(--color-primary);border-color:var(--color-primary);color:#fff}.btn{padding-inline:calc(var(--spacing) * 8);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider);text-transform:uppercase;transition:all var(--transition-fast);display:inline-block}.btn-primary{background-color:var(--color-primary-dark);color:#fff}.btn-primary:hover{background-color:var(--color-primary);color:#fff}.btn-outline{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);color:var(--color-body)}.btn-outline:hover{border-color:var(--color-primary);color:var(--color-primary)}.btn-hero{padding-inline:calc(var(--spacing) * 14);padding-block:calc(var(--spacing) * 5);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest);text-transform:uppercase;background-color:var(--color-primary);border:2px solid var(--color-primary);color:#fff}.btn-hero:hover{color:#fff;background-color:#0000;border-color:#fff}.about-block{background:var(--color-dark);height:100%;transition:border-color var(--transition-base);border-bottom:2px solid #0000;padding:40px 32px}.about-block:hover{border-bottom-color:var(--color-primary)}.about-block:hover .icon-box{transform:translateY(-10px)}.about-block h3{color:var(--color-secondary)}.icon-box{border-left:2px solid var(--color-secondary);border-right:2px solid var(--color-secondary);width:160px;height:92px;color:var(--color-light);transition:transform var(--transition-base);z-index:1;background-color:#0000;justify-content:center;align-items:center;margin:46px auto 60px;padding:20px 0;font-size:36px;display:flex;position:relative}.icon-box:before,.icon-box:after{content:"";z-index:-1;background-color:var(--color-dark);width:113.14px;height:113.14px;position:absolute;left:20.43px;transform:scaleY(.5774)rotate(-45deg)}.icon-box:before{border-top:2px solid var(--color-secondary);border-right:2px solid var(--color-secondary);top:-56.57px}.icon-box:after{border-bottom:2px solid var(--color-secondary);border-left:2px solid var(--color-secondary);bottom:-56.57px}.service-block{text-align:center;background-color:var(--color-dark);border-left:1px solid var(--color-border);border-top:1px solid var(--color-border);height:100%;padding:60px 32px;box-shadow:2px 2px 5px #2c2c2c4d,13px 13px #ffffff1f}.service-block i{margin-bottom:calc(var(--spacing) * 6);color:var(--color-light);font-size:50px;display:block}.service-block h3{margin-bottom:calc(var(--spacing) * 3);font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-secondary)}.progress-bar-track{background-color:var(--color-border);border-radius:3.40282e38px;width:100%;height:6px}.progress-bar-fill{background-color:var(--color-primary);border-radius:3.40282e38px;height:100%;transition:width .8s}.counter-card{background-color:var(--color-dark);flex-direction:column;justify-content:center;align-items:center;width:200px;height:200px;padding:32px 24px;display:flex}.counter-card i{margin-bottom:calc(var(--spacing) * 3);color:var(--color-light);font-size:40px;display:block}.counter-card .counter-value{color:var(--color-secondary);font-size:48px;font-weight:700}.counter-card h3{margin-top:calc(var(--spacing) * 2);color:var(--color-body);font-size:.9rem}.price-card{background-color:var(--color-dark);border:1px solid var(--color-border);height:100%;transition:border-color var(--transition-base);padding:40px 32px}.price-card h3{color:var(--color-secondary);margin-bottom:24px}.price-card table td{padding:6px 8px}.price-card:hover{border-color:var(--color-primary)}.testimonial-scroll{align-items:stretch;gap:calc(var(--spacing) * 6);scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;scrollbar-width:thin;scrollbar-color:var(--color-light) transparent;padding-bottom:16px;display:flex;overflow-x:auto}.testimonial-scroll::-webkit-scrollbar{height:6px}.testimonial-scroll::-webkit-scrollbar-track{background:0 0}.testimonial-scroll::-webkit-scrollbar-thumb{background:var(--color-light);border-radius:3px}.testimonial-nav{z-index:10;height:calc(var(--spacing) * 10);width:calc(var(--spacing) * 10);--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y);background:var(--color-dark);border:1px solid var(--color-border);color:var(--color-body);transition:all var(--transition-fast);cursor:pointer;justify-content:center;align-items:center;display:flex;position:absolute;top:50%}.testimonial-nav:hover{border-color:var(--color-primary);color:var(--color-primary)}.testimonial-nav-left{left:-20px}.testimonial-nav-right{right:-20px}@media (max-width:768px){.testimonial-nav{display:none}}.testimonial-modern{background-color:var(--color-grey);border:1px solid var(--color-border);scroll-snap-align:start;width:360px;transition:border-color var(--transition-base);flex-direction:column;flex-shrink:0;padding:32px;display:flex}.testimonial-modern:hover{border-color:var(--color-primary)}.testimonial-modern blockquote{font-size:1rem;line-height:1.7}.testimonial-modern blockquote p{margin:0}.contact-icon{margin-top:calc(var(--spacing) * 1);margin-right:calc(var(--spacing) * 4);color:var(--color-primary);font-size:20px}.portfolio-card{border:1px solid var(--color-border);background-color:var(--color-dark);transition:border-color var(--transition-base)}.portfolio-card:hover{border-color:var(--color-primary)}.portfolio-card .portfolio-img{background:#fff;justify-content:center;align-items:center;height:16rem;padding:40px;display:flex;position:relative;overflow:hidden}.portfolio-card .portfolio-img img{object-fit:contain;width:auto;max-height:100%}.portfolio-card .portfolio-overlay{inset:calc(var(--spacing) * 0);opacity:0;transition:opacity var(--transition-base);background:#ffffffeb;justify-content:center;align-items:center;display:flex;position:absolute}.portfolio-card:hover .portfolio-overlay{opacity:1}.portfolio-card .portfolio-overlay .search-circle{background-color:var(--color-primary);color:#fff;border-radius:50%;justify-content:center;align-items:center;width:50px;height:50px;font-size:20px;display:flex}.portfolio-card .portfolio-meta{padding:20px}.portfolio-card .portfolio-meta h3{color:var(--color-secondary);font-size:1rem}.portfolio-card .portfolio-meta p{color:var(--color-light);margin-top:8px;font-size:.85rem}.team-card{border:1px solid var(--color-border);height:100%;transition:border-color var(--transition-base);background-color:#0000;flex-direction:column;display:flex;overflow:hidden}.team-card:hover{border-color:var(--color-primary)}.team-photo{position:relative;overflow:hidden}.team-photo img{width:100%;transition:transform var(--transition-base), filter var(--transition-base)}.team-photo:hover img{filter:brightness(1.15);transform:scale(1.05)}@media (prefers-contrast:more){.team-photo:hover img{filter:none}}.team-photo .social-overlay{inset:calc(var(--spacing) * 0);padding-bottom:calc(var(--spacing) * 5);opacity:0;background:linear-gradient(to top, var(--color-dark) 0%, transparent 60%);transition:opacity var(--transition-base);justify-content:center;align-items:flex-end;display:flex;position:absolute}.team-card:hover .social-overlay{opacity:1}.social-overlay a{margin-inline:calc(var(--spacing) * 1);height:calc(var(--spacing) * 9);width:calc(var(--spacing) * 9);text-align:center;font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-leading:calc(var(--spacing) * 9);line-height:calc(var(--spacing) * 9);color:var(--color-body);transition:color var(--transition-fast);display:inline-block}.social-overlay a:hover{color:var(--color-primary)}.team-meta{background-color:var(--color-dark);flex:1;padding:16px}.team-meta h3{margin-bottom:calc(var(--spacing) * 1);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.team-meta span{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-light)}.pubs-table{width:100%;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-body)}.pubs-table th{padding-bottom:calc(var(--spacing) * 3);text-align:left;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider);text-transform:uppercase;color:var(--color-body);border-bottom:1px solid var(--color-border)}.pubs-table td{padding-block:calc(var(--spacing) * 3);vertical-align:top;border-bottom:1px solid var(--color-border)}.pubs-table .pub-year{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);font-family:var(--font-sans);color:var(--color-secondary)}.pubs-table .pub-authors{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);color:var(--color-muted)}.pubs-table .pub-title a{color:var(--color-secondary);transition:color var(--transition-fast)}.pubs-table .pub-title a:hover{color:var(--color-primary)}.pubs-table .pub-journal{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-muted);font-style:italic}.pubs-table tbody tr:hover td{color:var(--color-body)}.year-select{cursor:pointer;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);appearance:none;background:var(--color-dark);color:var(--color-body);border:1px solid var(--color-border);transition:border-color var(--transition-fast);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23d0d2d4' stroke-width='2' fill='none'/%3E%3C/svg%3E");background-position:right 12px center;background-repeat:no-repeat;border-radius:4px;padding:8px 36px 8px 14px}.year-select:hover,.year-select:focus{border-color:var(--color-primary)}.year-select option{background:var(--color-dark);color:var(--color-body)}.footer-waves{background-color:var(--color-dark);height:60px;position:relative;overflow:hidden}.footer-waves svg{width:100%;height:100%;position:absolute;bottom:0;left:0}}@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.top-0{top:calc(var(--spacing) * 0)}.top-16{top:calc(var(--spacing) * 16)}.right-0{right:calc(var(--spacing) * 0)}.right-5{right:calc(var(--spacing) * 5)}.bottom-0{bottom:calc(var(--spacing) * 0)}.bottom-5{bottom:calc(var(--spacing) * 5)}.left-0{left:calc(var(--spacing) * 0)}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.my-6{margin-block:calc(var(--spacing) * 6)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-auto{margin-top:auto}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.mb-10{margin-bottom:calc(var(--spacing) * 10)}.mb-12{margin-bottom:calc(var(--spacing) * 12)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-10{height:calc(var(--spacing) * 10)}.h-12{height:calc(var(--spacing) * 12)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.h-24{height:calc(var(--spacing) * 24)}.h-48{height:calc(var(--spacing) * 48)}.h-full{height:100%}.min-h-\[400px\]{min-height:400px}.min-h-screen{min-height:100vh}.w-10{width:calc(var(--spacing) * 10)}.w-12{width:calc(var(--spacing) * 12)}.w-48{width:calc(var(--spacing) * 48)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-none{max-width:none}.flex-shrink-0{flex-shrink:0}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}.gap-12{gap:calc(var(--spacing) * 12)}.gap-16{gap:calc(var(--spacing) * 16)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-border{border-color:var(--color-border)}.border-primary{border-color:var(--color-primary)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab, var(--color-black) 50%, transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab, red, red)){.bg-black\/60{background-color:color-mix(in oklab, var(--color-black) 60%, transparent)}}.bg-dark{background-color:var(--color-dark)}.bg-grey{background-color:var(--color-grey)}.bg-primary{background-color:var(--color-primary)}.bg-cover{background-size:cover}.bg-center{background-position:50%}.bg-top{background-position:top}.object-cover{object-fit:cover}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-8{padding:calc(var(--spacing) * 8)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-24{padding-block:calc(var(--spacing) * 24)}.py-28{padding-block:calc(var(--spacing) * 28)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pb-12{padding-bottom:calc(var(--spacing) * 12)}.pl-2{padding-left:calc(var(--spacing) * 2)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-serif{font-family:var(--font-serif)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-8xl{font-size:var(--text-8xl);line-height:var(--tw-leading,var(--text-8xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.text-body{color:var(--color-body)}.text-light{color:var(--color-light)}.text-muted{color:var(--color-muted)}.text-primary{color:var(--color-primary)}.text-secondary{color:var(--color-secondary)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.opacity-40{opacity:.4}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:border-primary:hover{border-color:var(--color-primary)}.hover\:bg-primary:hover{background-color:var(--color-primary)}.hover\:text-primary:hover{color:var(--color-primary)}.hover\:underline:hover{text-decoration-line:underline}}@media (min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:48rem){.md\:flex{display:flex}.md\:hidden{display:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:text-7xl{font-size:var(--text-7xl);line-height:var(--tw-leading,var(--text-7xl--line-height))}.md\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}}@media (min-width:64rem){.lg\:w-1\/2{width:50%}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:p-16{padding:calc(var(--spacing) * 16)}.lg\:text-8xl{font-size:var(--text-8xl);line-height:var(--tw-leading,var(--text-8xl--line-height))}}}[x-cloak]{display:none!important}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-tracking:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-sans:"Space Grotesk", sans-serif;--font-serif:"Spectral", serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-7xl:4.5rem;--text-7xl--line-height:1;--text-8xl:6rem;--text-8xl--line-height:1;--font-weight-light:300;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wider:.05em;--tracking-widest:.1em;--leading-relaxed:1.625;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-primary:#c04545;--color-primary-text:#e57575;--color-primary-dark:#b84444;--color-secondary:#85bfbf;--color-dark:#20242b;--color-grey:#2a2f36;--color-light:#8a8e93;--color-body:#d0d2d4;--color-muted:#9a9da0;--color-border:#eceff11f;--transition-fast:.2s ease;--transition-base:.3s ease;--transition-slow:.6s ease}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}@font-face{font-family:Space Grotesk;src:url(/webfonts/google-fonts/space-grotesk/space-grotesk-latin-300-normal.woff2)format("woff2");font-weight:300;font-display:swap}@font-face{font-family:Space Grotesk;src:url(/webfonts/google-fonts/space-grotesk/space-grotesk-latin-400-normal.woff2)format("woff2");font-weight:400;font-display:swap}@font-face{font-family:Space Grotesk;src:url(/webfonts/google-fonts/space-grotesk/space-grotesk-latin-500-normal.woff2)format("woff2");font-weight:500;font-display:swap}@font-face{font-family:Spectral;src:url(/webfonts/google-fonts/spectral/spectral-latin-300-normal.woff2)format("woff2");font-weight:300;font-display:swap}html{scroll-behavior:smooth;background-color:var(--color-grey);color:var(--color-body)}@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,:before,:after{transition-duration:.01ms!important;animation-duration:.01ms!important}}body{font-family:var(--font-sans);-webkit-font-smoothing:antialiased;font-size:18px;line-height:1.6}@media (min-width:768px){body{font-size:20px}}h1,h2,h3,h4,h5,h6{font-family:var(--font-sans);color:var(--color-secondary);line-height:1.2}h1{font-weight:300}h2{font-weight:500}h3,h4,h5,h6{font-weight:400}p{font-family:var(--font-serif);color:var(--color-body);line-height:1.7}a{color:var(--color-secondary);transition:color var(--transition-fast);text-decoration:none}a:hover{color:var(--color-body)}:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}::selection{background:var(--color-primary);color:var(--color-body)}}@layer components{.section{padding-top:6rem;padding-bottom:6rem}@media (max-width:768px){.section{padding-top:3rem;padding-bottom:3rem}}.section-title{margin-bottom:calc(var(--spacing) * 12);text-align:center}.section-title h1,.section-title h2,.section-title__label{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);text-transform:capitalize;color:var(--color-body)}.section-title .accent{color:var(--color-primary-text)}.border-accent{border-top:2px solid var(--color-border);width:30%;height:1px;margin:15px auto 0;position:relative}.border-accent:before{content:"";background-color:var(--color-primary);width:50px;height:6px;margin-left:-25px;position:absolute;top:-4px;left:50%}.bg-dark{background-color:var(--color-dark)}.bg-grey{background-color:var(--color-grey)}.reveal{opacity:0;transition:opacity var(--transition-slow), transform var(--transition-slow);transform:translateY(24px)}.reveal.visible{opacity:1;transform:translateY(0)}.skip-link{left:calc(var(--spacing) * 4);z-index:100;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);background-color:var(--color-primary);color:#fff;transition:top var(--transition-fast);position:absolute;top:-100%}.skip-link:focus,.skip-link:focus-visible{color:#fff;top:.5rem}.prose a{text-underline-offset:3px;text-decoration:underline;text-decoration-thickness:1px}@media (prefers-reduced-motion:reduce){.reveal{opacity:1;transform:none}}.prose pre,.highlight pre,.highlight{max-width:100%;overflow-x:auto}.prose table{max-width:100%;display:block;overflow-x:auto}.site-nav{top:calc(var(--spacing) * 0);z-index:50;background-color:var(--color-dark);transition:border-color var(--transition-base), box-shadow var(--transition-base);border-bottom:1px solid #0000;position:sticky}.site-nav.nav-scrolled{border-bottom-color:var(--color-border);box-shadow:0 2px 12px #0000004d}.site-nav a{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider);text-transform:uppercase;color:var(--color-body);transition:color var(--transition-fast)}.site-nav a:hover,.site-nav a.active{color:var(--color-primary-text)}.lang-select{cursor:pointer;border-style:var(--tw-border-style);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));appearance:none;background:var(--color-dark);color:var(--color-body);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23d0d2d4' stroke-width='2' fill='none'/%3E%3C/svg%3E");background-position:right 6px center;background-repeat:no-repeat;border-width:0;padding:4px 24px 4px 8px}.footer-social-icon{height:calc(var(--spacing) * 12);width:calc(var(--spacing) * 12);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));color:var(--color-light);border:1px solid var(--color-border);transition:all var(--transition-base);border-radius:3.40282e38px;justify-content:center;align-items:center;display:flex}.footer-social-icon:hover{background-color:var(--color-primary);border-color:var(--color-primary);color:#fff}.btn{padding-inline:calc(var(--spacing) * 8);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider);text-transform:uppercase;transition:all var(--transition-fast);display:inline-block}.btn-primary{background-color:var(--color-primary-dark);color:#fff}.btn-primary:hover{background-color:var(--color-primary);color:#fff}.btn-outline{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);color:var(--color-body)}.btn-outline:hover{border-color:var(--color-primary);color:var(--color-primary-text)}.btn-hero{padding-inline:calc(var(--spacing) * 14);padding-block:calc(var(--spacing) * 5);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest);text-transform:uppercase;background-color:var(--color-primary);border:2px solid var(--color-primary);color:#fff}.btn-hero:hover{color:#fff;background-color:#0000;border-color:#fff}.about-block{background:var(--color-dark);height:100%;transition:border-color var(--transition-base);border-bottom:2px solid #0000;padding:40px 32px}.about-block:hover{border-bottom-color:var(--color-primary)}.about-block:hover .icon-box{transform:translateY(-10px)}.about-block h3{color:var(--color-secondary)}.icon-box{border-left:2px solid var(--color-secondary);border-right:2px solid var(--color-secondary);width:160px;height:92px;color:var(--color-light);transition:transform var(--transition-base);z-index:1;background-color:#0000;justify-content:center;align-items:center;margin:46px auto 60px;padding:20px 0;font-size:36px;display:flex;position:relative}.icon-box:before,.icon-box:after{content:"";z-index:-1;background-color:var(--color-dark);width:113.14px;height:113.14px;position:absolute;left:20.43px;transform:scaleY(.5774)rotate(-45deg)}.icon-box:before{border-top:2px solid var(--color-secondary);border-right:2px solid var(--color-secondary);top:-56.57px}.icon-box:after{border-bottom:2px solid var(--color-secondary);border-left:2px solid var(--color-secondary);bottom:-56.57px}.service-block{text-align:center;background-color:var(--color-dark);border-left:1px solid var(--color-border);border-top:1px solid var(--color-border);height:100%;padding:60px 32px;box-shadow:2px 2px 5px #2c2c2c4d,13px 13px #ffffff1f}.service-block i{margin-bottom:calc(var(--spacing) * 6);color:var(--color-light);font-size:50px;display:block}.service-block h3{margin-bottom:calc(var(--spacing) * 3);font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-secondary)}.progress-bar-track{background-color:var(--color-border);border-radius:3.40282e38px;width:100%;height:6px}.progress-bar-fill{background-color:var(--color-primary);border-radius:3.40282e38px;height:100%;transition:width .8s}.counter-card{background-color:var(--color-dark);flex-direction:column;justify-content:center;align-items:center;width:200px;height:200px;padding:32px 24px;display:flex}.counter-card i{margin-bottom:calc(var(--spacing) * 3);color:var(--color-light);font-size:40px;display:block}.counter-card .counter-value{color:var(--color-secondary);font-size:48px;font-weight:700}.counter-card h3{margin-top:calc(var(--spacing) * 2);color:var(--color-body);font-size:.9rem}.price-card{background-color:var(--color-dark);border:1px solid var(--color-border);height:100%;transition:border-color var(--transition-base);padding:40px 32px}.price-card h3{color:var(--color-secondary);margin-bottom:24px}.price-card table td{padding:6px 8px}.price-card:hover{border-color:var(--color-primary)}.testimonial-scroll{align-items:stretch;gap:calc(var(--spacing) * 6);scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;scrollbar-width:thin;scrollbar-color:var(--color-light) transparent;padding-bottom:16px;display:flex;overflow-x:auto}.testimonial-scroll::-webkit-scrollbar{height:6px}.testimonial-scroll::-webkit-scrollbar-track{background:0 0}.testimonial-scroll::-webkit-scrollbar-thumb{background:var(--color-light);border-radius:3px}.testimonial-nav{z-index:10;height:calc(var(--spacing) * 10);width:calc(var(--spacing) * 10);--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y);background:var(--color-dark);border:1px solid var(--color-border);color:var(--color-body);transition:all var(--transition-fast);cursor:pointer;justify-content:center;align-items:center;display:flex;position:absolute;top:50%}.testimonial-nav:hover{border-color:var(--color-primary);color:var(--color-primary-text)}.testimonial-nav-left{left:-20px}.testimonial-nav-right{right:-20px}@media (max-width:768px){.testimonial-nav{display:none}}.testimonial-modern{background-color:var(--color-grey);border:1px solid var(--color-border);scroll-snap-align:start;width:360px;transition:border-color var(--transition-base);flex-direction:column;flex-shrink:0;padding:32px;display:flex}.testimonial-modern:hover{border-color:var(--color-primary)}.testimonial-modern blockquote{font-size:1rem;line-height:1.7}.testimonial-modern blockquote p{margin:0}.contact-icon{margin-top:calc(var(--spacing) * 1);margin-right:calc(var(--spacing) * 4);color:var(--color-primary-text);font-size:20px}.portfolio-card{border:1px solid var(--color-border);background-color:var(--color-dark);transition:border-color var(--transition-base)}.portfolio-card:hover{border-color:var(--color-primary)}.portfolio-card .portfolio-img{background:#fff;justify-content:center;align-items:center;height:16rem;padding:40px;display:flex;position:relative;overflow:hidden}.portfolio-card .portfolio-img img{object-fit:contain;width:auto;max-height:100%}.portfolio-card .portfolio-overlay{inset:calc(var(--spacing) * 0);opacity:0;transition:opacity var(--transition-base);background:#ffffffeb;justify-content:center;align-items:center;display:flex;position:absolute}.portfolio-card:hover .portfolio-overlay{opacity:1}.portfolio-card .portfolio-overlay .search-circle{background-color:var(--color-primary);color:#fff;border-radius:50%;justify-content:center;align-items:center;width:50px;height:50px;font-size:20px;display:flex}.portfolio-card .portfolio-meta{padding:20px}.portfolio-card .portfolio-meta h3{color:var(--color-secondary);font-size:1rem}.portfolio-card .portfolio-meta p{color:var(--color-light);margin-top:8px;font-size:.85rem}.team-card{border:1px solid var(--color-border);height:100%;transition:border-color var(--transition-base);background-color:#0000;flex-direction:column;display:flex;overflow:hidden}.team-card:hover{border-color:var(--color-primary)}.team-photo{position:relative;overflow:hidden}.team-photo img{width:100%;transition:transform var(--transition-base), filter var(--transition-base)}.team-photo:hover img{filter:brightness(1.15);transform:scale(1.05)}@media (prefers-contrast:more){.team-photo:hover img{filter:none}}.team-photo .social-overlay{inset:calc(var(--spacing) * 0);padding-bottom:calc(var(--spacing) * 5);opacity:0;background:linear-gradient(to top, var(--color-dark) 0%, transparent 60%);transition:opacity var(--transition-base);justify-content:center;align-items:flex-end;display:flex;position:absolute}.team-card:hover .social-overlay{opacity:1}.social-overlay a{margin-inline:calc(var(--spacing) * 1);height:calc(var(--spacing) * 9);width:calc(var(--spacing) * 9);text-align:center;font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-leading:calc(var(--spacing) * 9);line-height:calc(var(--spacing) * 9);color:var(--color-body);transition:color var(--transition-fast);display:inline-block}.social-overlay a:hover{color:var(--color-primary-text)}.team-meta{background-color:var(--color-dark);flex:1;padding:16px}.team-meta h3{margin-bottom:calc(var(--spacing) * 1);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.team-meta span{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-light)}.pubs-table{width:100%;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-body)}.pubs-table th{padding-bottom:calc(var(--spacing) * 3);text-align:left;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider);text-transform:uppercase;color:var(--color-body);border-bottom:1px solid var(--color-border)}.pubs-table td{padding-block:calc(var(--spacing) * 3);vertical-align:top;border-bottom:1px solid var(--color-border)}.pubs-table .pub-year{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);font-family:var(--font-sans);color:var(--color-secondary)}.pubs-table .pub-authors{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed);color:var(--color-muted)}.pubs-table .pub-title a{color:var(--color-secondary);transition:color var(--transition-fast)}.pubs-table .pub-title a:hover{color:var(--color-primary-text)}.pubs-table .pub-journal{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-muted);font-style:italic}.pubs-table tbody tr:hover td{color:var(--color-body)}.year-select{cursor:pointer;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);appearance:none;background:var(--color-dark);color:var(--color-body);border:1px solid var(--color-border);transition:border-color var(--transition-fast);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23d0d2d4' stroke-width='2' fill='none'/%3E%3C/svg%3E");background-position:right 12px center;background-repeat:no-repeat;border-radius:4px;padding:8px 36px 8px 14px}.year-select:hover,.year-select:focus{border-color:var(--color-primary)}.year-select option{background:var(--color-dark);color:var(--color-body)}.footer-waves{background-color:var(--color-dark);height:60px;position:relative;overflow:hidden}.footer-waves svg{width:100%;height:100%;position:absolute;bottom:0;left:0}}@layer utilities{.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.top-0{top:calc(var(--spacing) * 0)}.top-16{top:calc(var(--spacing) * 16)}.right-0{right:calc(var(--spacing) * 0)}.right-5{right:calc(var(--spacing) * 5)}.bottom-0{bottom:calc(var(--spacing) * 0)}.bottom-5{bottom:calc(var(--spacing) * 5)}.left-0{left:calc(var(--spacing) * 0)}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.my-6{margin-block:calc(var(--spacing) * 6)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-auto{margin-top:auto}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.mb-10{margin-bottom:calc(var(--spacing) * 10)}.mb-12{margin-bottom:calc(var(--spacing) * 12)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.h-10{height:calc(var(--spacing) * 10)}.h-12{height:calc(var(--spacing) * 12)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.h-24{height:calc(var(--spacing) * 24)}.h-48{height:calc(var(--spacing) * 48)}.h-full{height:100%}.min-h-\[400px\]{min-height:400px}.min-h-screen{min-height:100vh}.w-10{width:calc(var(--spacing) * 10)}.w-12{width:calc(var(--spacing) * 12)}.w-48{width:calc(var(--spacing) * 48)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-none{max-width:none}.flex-shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}.gap-12{gap:calc(var(--spacing) * 12)}.gap-16{gap:calc(var(--spacing) * 16)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-border{border-color:var(--color-border)}.border-primary{border-color:var(--color-primary)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab, var(--color-black) 50%, transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab, red, red)){.bg-black\/60{background-color:color-mix(in oklab, var(--color-black) 60%, transparent)}}.bg-dark{background-color:var(--color-dark)}.bg-grey{background-color:var(--color-grey)}.bg-primary{background-color:var(--color-primary)}.bg-cover{background-size:cover}.bg-center{background-position:50%}.bg-top{background-position:top}.object-cover{object-fit:cover}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-8{padding:calc(var(--spacing) * 8)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-24{padding-block:calc(var(--spacing) * 24)}.py-28{padding-block:calc(var(--spacing) * 28)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pb-12{padding-bottom:calc(var(--spacing) * 12)}.pl-2{padding-left:calc(var(--spacing) * 2)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-serif{font-family:var(--font-serif)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-8xl{font-size:var(--text-8xl);line-height:var(--tw-leading,var(--text-8xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.text-body{color:var(--color-body)}.text-light{color:var(--color-light)}.text-muted{color:var(--color-muted)}.text-primary{color:var(--color-primary)}.text-primary-text{color:var(--color-primary-text)}.text-secondary{color:var(--color-secondary)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.opacity-40{opacity:.4}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:border-primary:hover{border-color:var(--color-primary)}.hover\:bg-primary:hover{background-color:var(--color-primary)}.hover\:text-primary:hover{color:var(--color-primary)}.hover\:underline:hover{text-decoration-line:underline}}@media (min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:48rem){.md\:flex{display:flex}.md\:hidden{display:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:text-7xl{font-size:var(--text-7xl);line-height:var(--tw-leading,var(--text-7xl--line-height))}.md\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}}@media (min-width:64rem){.lg\:w-1\/2{width:50%}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:p-16{padding:calc(var(--spacing) * 16)}.lg\:text-8xl{font-size:var(--text-8xl);line-height:var(--tw-leading,var(--text-8xl--line-height))}}}[x-cloak]{display:none!important}.leaflet-container .leaflet-control-attribution a{text-underline-offset:2px;text-decoration:underline}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0} \ No newline at end of file diff --git a/themes/capro-tw/layouts/404.html b/themes/capro-tw/layouts/404.html index cd282db7..2c36e2bd 100644 --- a/themes/capro-tw/layouts/404.html +++ b/themes/capro-tw/layouts/404.html @@ -1,7 +1,7 @@ {{ define "main" }}
-

404

+

404

Page Not Found

Back to home
diff --git a/themes/capro-tw/layouts/_default/article.html b/themes/capro-tw/layouts/_default/article.html index 974cc31e..6f74a233 100644 --- a/themes/capro-tw/layouts/_default/article.html +++ b/themes/capro-tw/layouts/_default/article.html @@ -5,7 +5,8 @@ {{ $.Title }} {{ end }}
-

{{ .Title }}

+ {{/* Only rendered from _default/list.html, whose section title is the page h1. */}} +

{{ .Title }}

{{ .Summary }}

{{ i18n "readMore" }}
diff --git a/themes/capro-tw/layouts/_default/baseof.html b/themes/capro-tw/layouts/_default/baseof.html index 57445281..12783408 100644 --- a/themes/capro-tw/layouts/_default/baseof.html +++ b/themes/capro-tw/layouts/_default/baseof.html @@ -6,11 +6,12 @@ {{ block "head" . }}{{ end }} + {{ partial "nav.html" . }} - {{ if .IsHome }} - {{ partial "sections/banner.html" . }} - {{ end }}
+ {{ if .IsHome }} + {{ partial "sections/banner.html" . }} + {{ end }} {{ block "main" . }}{{ end }}
{{ partial "footer/footer.html" . }} diff --git a/themes/capro-tw/layouts/_default/list.html b/themes/capro-tw/layouts/_default/list.html index 25008ffc..dbb82640 100644 --- a/themes/capro-tw/layouts/_default/list.html +++ b/themes/capro-tw/layouts/_default/list.html @@ -2,10 +2,13 @@ {{ with .CurrentSection }}
+ {{ with .Title }} + {{ $words := split . " " }}
-

{{ with .Title }}{{ index (split . " ") 0 | safeHTML }}{{ end }} {{ with .Title }}{{ index (split . " ") 1 | safeHTML }}{{ end }}

+

{{ index $words 0 | safeHTML }}{{ if gt (len $words) 1 }} {{ delimit (after 1 $words) " " | safeHTML }}{{ end }}

+ {{ end }}
{{ $paginator := .Paginate .Data.Pages }} {{ range $paginator.Pages }} diff --git a/themes/capro-tw/layouts/_default/single.html b/themes/capro-tw/layouts/_default/single.html index 456a03a9..5947dc00 100644 --- a/themes/capro-tw/layouts/_default/single.html +++ b/themes/capro-tw/layouts/_default/single.html @@ -3,7 +3,7 @@

{{ .Title }}

-
+
{{ range .Params.author }} {{ with $.Site.GetPage "taxonomyTerm" (printf "team/%s" (urlize .)) }} {{ .Title }} diff --git a/themes/capro-tw/layouts/partials/head/css.html b/themes/capro-tw/layouts/partials/head/css.html index fcd9ba5b..2d0de8f3 100644 --- a/themes/capro-tw/layouts/partials/head/css.html +++ b/themes/capro-tw/layouts/partials/head/css.html @@ -27,3 +27,6 @@ media="print" onload="this.media='screen'"> {{ end }} + +{{/* Content must not depend on the scroll observer running. */}} + diff --git a/themes/capro-tw/layouts/partials/sections/testimonial.html b/themes/capro-tw/layouts/partials/sections/testimonial.html index 3174d7e4..2cdc199b 100644 --- a/themes/capro-tw/layouts/partials/sections/testimonial.html +++ b/themes/capro-tw/layouts/partials/sections/testimonial.html @@ -34,7 +34,7 @@

{{ with .Title }}{{ index (split . " ") 0 | safeHTML }}{{ end }} {{ with .Params.author }}

{{ .name }}

-

{{ .position }}

+

{{ .position }}

{{ end }}

{{ with .Params.link }}{{ end }}{{ .Title }}{{ with .Params.link }}{{ end }} diff --git a/themes/capro-tw/layouts/team/single.html b/themes/capro-tw/layouts/team/single.html index 6b007190..12b28740 100644 --- a/themes/capro-tw/layouts/team/single.html +++ b/themes/capro-tw/layouts/team/single.html @@ -3,7 +3,7 @@

{{ with .Site.GetPage "section" "team" }} -

{{ index (split .Params.single " ") 0 | safeHTML }} {{ index (split .Params.single " ") 1 | safeHTML }}

+

{{ index (split .Params.single " ") 0 | safeHTML }} {{ index (split .Params.single " ") 1 | safeHTML }}

{{ end }}
@@ -18,7 +18,7 @@

{{ index (split .Params.single " ") 0 | safeHTML }} {{ {{ end }} {{ end }}
-

{{ .Title }}

+

{{ .Title }}


@@ -69,7 +69,7 @@

{{ i18n "authorsPubs" }} {{ with .Site.GetPage "section

-

Loading publications from OpenAlex...

+

Loading publications from OpenAlex...