From d9c5497462787e64782e0994cf758ffd6dd6e494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Sun, 26 Jul 2026 17:06:19 +0200 Subject: [PATCH 1/3] ci: guard the transcript injection and render docs on pull requests awk getline returns -1 for a missing or unreadable file and the read loop simply does not run, so a lost artifact would have committed an empty code block instead of failing. Check the artifact before injecting. Render the website on pull requests as well, so a broken docs change is caught before it reaches main, and split the Pages upload and deployment into their own job so pull request runs neither deploy nor record a deployment. Fall back to the workflow token when the app key is out of reach on fork pull requests. Hoist the duplicated target file to the job environment and drop the .nojekyll step, which does nothing when Pages is served from an artifact. --- .github/workflows/deploy.yml | 39 +++++++++++++++++++++--------- .github/workflows/devcontainer.yml | 13 ++++++++-- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0232be3..16be87f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -8,6 +8,10 @@ on: paths: - "docs/**" - ".github/workflows/deploy.yml" + pull_request: + paths: + - "docs/**" + - ".github/workflows/deploy.yml" release: types: [ published ] @@ -17,24 +21,22 @@ permissions: id-token: write concurrency: - group: "pages" - cancel-in-progress: false + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: - deploy: + build: runs-on: ubuntu-latest - name: Deploy - Documentation website - - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} + name: Build - Documentation website steps: - name: Setup Git user uses: mcanouil/quarto-workflows/.github/actions/setup-git-user@main id: setup-git-user with: - gh-app-id: ${{ vars.APP_ID }} + # Pull requests from forks cannot read the app key, so fall back to + # the workflow token there; nothing is pushed from this workflow. + gh-app-id: ${{ github.event_name != 'pull_request' && vars.APP_ID || '' }} app-key: ${{ secrets.APP_KEY }} gh-token: ${{ secrets.GITHUB_TOKEN }} @@ -53,17 +55,30 @@ jobs: working-directory: ./docs run: quarto render . - - name: Prepare site for deployment - run: touch docs/_site/.nojekyll - - name: Configure GitHub Pages + if: github.event_name != 'pull_request' uses: actions/configure-pages@v6 - name: Upload Pages artifact + if: github.event_name != 'pull_request' uses: actions/upload-pages-artifact@v5 with: path: "./docs/_site" + deploy: + runs-on: ubuntu-latest + name: Deploy - Documentation website + + needs: + - "build" + + if: github.event_name != 'pull_request' + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v5 diff --git a/.github/workflows/devcontainer.yml b/.github/workflows/devcontainer.yml index 1a34903..8418ee0 100644 --- a/.github/workflows/devcontainer.yml +++ b/.github/workflows/devcontainer.yml @@ -225,6 +225,9 @@ jobs: permissions: contents: write + env: + TARGET_FILE: docs/reference/quarto-check.qmd + steps: - name: Checkout repository uses: actions/checkout@v7 @@ -265,13 +268,20 @@ jobs: env: BUILD_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - TARGET_FILE: docs/reference/quarto-check.qmd run: | update_docs_section() { local version=$1 local output_file=$2 local build_url=$3 + # An empty transcript would be committed as an empty code block, so + # fail loudly when the artifact is missing rather than silently + # publishing a blank section. + if [ ! -s "${output_file}" ]; then + echo "::error::Missing or empty quarto check output: ${output_file}" + return 1 + fi + # Normalise version for marker tags local version_upper version_upper=$(echo "${version}" | tr '[:lower:]' '[:upper:]') @@ -352,7 +362,6 @@ jobs: GH_TOKEN: ${{ steps.app-token.outputs.token }} BRANCH: "ci/update-quarto-check-docs" COMMIT: "ci: update quarto check outputs in documentation" - TARGET_FILE: docs/reference/quarto-check.qmd run: | if git show-ref --quiet refs/heads/${BRANCH}; then echo "Branch ${BRANCH} already exists." From 49ab98bbdd35f849cdd9aa1defe38f88bc0e1af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Sun, 26 Jul 2026 19:08:24 +0200 Subject: [PATCH 2/3] feat: add quarto 1.10 configuration and generate the version lists Adding a version-pinned configuration previously meant editing the build matrix, the configuration table and three prose ranges by hand, so the documentation drifted from .devcontainer/ as soon as one was missed. Derive the build matrix from the .devcontainer/quarto-* directories, and generate the configuration table and the version ranges at render time with a filter that reads the same directories. Adding a configuration is now the only step required. Sort versions numerically so that 1.10 follows 1.9 rather than 1.1. --- .devcontainer/quarto-1.10/devcontainer.json | 36 +++++ .github/workflows/devcontainer.yml | 48 ++++-- README.md | 4 +- docs/_quarto.yml | 1 + .../filters/devcontainer-configurations.lua | 141 ++++++++++++++++++ docs/getting-started/codespaces.qmd | 2 +- docs/reference/configurations.qmd | 21 +-- docs/reference/images.qmd | 6 +- 8 files changed, 221 insertions(+), 38 deletions(-) create mode 100644 .devcontainer/quarto-1.10/devcontainer.json create mode 100644 docs/assets/filters/devcontainer-configurations.lua diff --git a/.devcontainer/quarto-1.10/devcontainer.json b/.devcontainer/quarto-1.10/devcontainer.json new file mode 100644 index 0000000..16a7af7 --- /dev/null +++ b/.devcontainer/quarto-1.10/devcontainer.json @@ -0,0 +1,36 @@ +{ + "name": "1.10 - Quarto Codespaces", + "image": "ghcr.io/mcanouil/quarto-codespaces:latest", + "remoteUser": "vscode", + "features": { + "ghcr.io/rocker-org/devcontainer-features/quarto-cli:1": { + "version": "1.10" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "quarto.quarto", + "mcanouil.quarto-wizard", + "REditorSupport.r", + "ms-python.python", + "julialang.language-julia", + "ms-python.black-formatter", + "ms-python.flake8", + "ms-python.vscode-pylance", + "ms-python.pylint", + "ms-python.isort", + "ms-toolsai.jupyter", + "Posit.air-vscode" + ], + "settings": { + "r.rterm.option": ["--no-save", "--no-restore-data", "--quiet"], + "r.useRenvLibPath": true, + "[r]": { + "editor.defaultFormatter": "Posit.air-vscode", + "editor.formatOnSave": true + } + } + } + } +} diff --git a/.github/workflows/devcontainer.yml b/.github/workflows/devcontainer.yml index 8418ee0..937bab1 100644 --- a/.github/workflows/devcontainer.yml +++ b/.github/workflows/devcontainer.yml @@ -380,10 +380,9 @@ jobs: sleep 2 gh pr merge --auto --squash --delete-branch - build-versions: + list-versions: runs-on: ubuntu-latest - - name: Build - Quarto ${{ matrix.QUARTO_VERSION }} + name: List - Version-pinned configurations needs: - "merge" @@ -396,21 +395,42 @@ jobs: always() && needs.merge.result == 'success' && needs.update-docs.result == 'success' + outputs: + versions: ${{ steps.versions.outputs.versions }} + + steps: + - uses: actions/checkout@v7 + + # Derive the matrix from the repository so that adding a + # .devcontainer/quarto-/ directory is the only step needed to + # build a new version. + - name: Collect version-pinned configurations + id: versions + run: | + VERSIONS=$(find .devcontainer -mindepth 1 -maxdepth 1 -type d -name 'quarto-[0-9]*' \ + -exec basename {} \; | sed 's/^quarto-//' | sort -V | jq -R . | jq -sc .) + + if [ "${VERSIONS}" = "[]" ]; then + echo "::error::No version-pinned configurations found under .devcontainer/" + exit 1 + fi + + echo "Versions: ${VERSIONS}" + echo "versions=${VERSIONS}" >> "${GITHUB_OUTPUT}" + + build-versions: + runs-on: ubuntu-latest + + name: Build - Quarto ${{ matrix.QUARTO_VERSION }} + + needs: + - "list-versions" + strategy: matrix: PLATFORM: - "linux/amd64" - QUARTO_VERSION: - - "1.0" - - "1.1" - - "1.2" - - "1.3" - - "1.4" - - "1.5" - - "1.6" - - "1.7" - - "1.8" - - "1.9" + QUARTO_VERSION: ${{ fromJSON(needs.list-versions.outputs.versions) }} steps: - uses: actions/checkout@v7 diff --git a/README.md b/README.md index a2eea33..20f39fd 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ docker pull ghcr.io/mcanouil/quarto-codespaces:latest | `latest` | stable release | `amd64`, `arm64` | | `release`, `release-noble` | stable release | `amd64`, `arm64` | | `prerelease`, `prerelease-noble` | prerelease | `amd64`, `arm64` | -| `1.0` … `1.9`, `1.0-noble` … `1.9-noble` | that minor version | `amd64` | +| one per Quarto minor version, with a `-noble` variant | that minor version | `amd64` | | any tag with a `-` suffix | as above, pinned to one build | as above | Images are rebuilt every Sunday and scanned with [Trivy](https://trivy.dev/). @@ -47,7 +47,7 @@ Full tag list: [`ghcr.io/mcanouil/quarto-codespaces`](https://github.com/mcanoui } ``` -Configurations for the Quarto prerelease and for every minor version from `1.0` to `1.9` are in [`.devcontainer/`](.devcontainer). +Configurations for the Quarto prerelease and for every supported Quarto minor version are in [`.devcontainer/`](.devcontainer), and listed on the [configurations](https://m.canouil.dev/quarto-codespaces/reference/configurations.html) page. Click **Use this template** to start your own repository from this one. ## Documentation diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 2c2d104..9cfe348 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -50,6 +50,7 @@ format: - assets/stylesheets/theme.scss filters: + - assets/filters/devcontainer-configurations.lua - at: post-quarto path: gitlink diff --git a/docs/assets/filters/devcontainer-configurations.lua b/docs/assets/filters/devcontainer-configurations.lua new file mode 100644 index 0000000..9614f83 --- /dev/null +++ b/docs/assets/filters/devcontainer-configurations.lua @@ -0,0 +1,141 @@ +--[[ +Generate the configuration reference from the repository itself. + +The `.devcontainer/` directory gains a new `quarto-` configuration +every time Quarto releases a minor version, and every place that lists those +versions by hand drifts the moment one is added. This filter reads the +directory at render time and fills in: + + - `::: {#devcontainer-configurations}`, replaced by the configuration table; + - `[]{.version-range}`, replaced by "`` … ``"; + - `[]{.version-latest}`, replaced by the highest version. + +Both spans accept a `suffix` attribute, appended inside the code spans, so +`[]{.version-range suffix="-noble"}` renders "`1.0-noble` … `1.10-noble`". +]] + +local IMAGE = "`quarto-codespaces:latest`" + +--- Compare two dotted version strings numerically, so 1.10 sorts after 1.9. +local function version_less_than(a, b) + local a_major, a_minor = a:match("^(%d+)%.(%d+)$") + local b_major, b_minor = b:match("^(%d+)%.(%d+)$") + if tonumber(a_major) ~= tonumber(b_major) then + return tonumber(a_major) < tonumber(b_major) + end + return tonumber(a_minor) < tonumber(b_minor) +end + +--- Locate `.devcontainer/`. Pandoc runs with the input file's directory as the +--- working directory, so search from the project root and then walk upwards. +local function devcontainer_directory() + local candidates = {} + local project = os.getenv("QUARTO_PROJECT_DIR") + if project then + table.insert(candidates, project .. "/../.devcontainer") + table.insert(candidates, project .. "/.devcontainer") + end + local prefix = "" + for _ = 1, 4 do + table.insert(candidates, prefix .. ".devcontainer") + prefix = prefix .. "../" + end + + for _, candidate in ipairs(candidates) do + local ok, entries = pcall(pandoc.system.list_directory, candidate) + if ok and entries then + return candidate, entries + end + end + return nil, nil +end + +--- Version-pinned configurations, sorted from newest to oldest. +local function pinned_versions() + local _, entries = devcontainer_directory() + if not entries then + quarto.log.warning("[devcontainer-configurations] .devcontainer/ not found; leaving the placeholder in place") + return {} + end + + local versions = {} + for _, entry in ipairs(entries) do + local version = entry:match("^quarto%-(%d+%.%d+)$") + if version then + table.insert(versions, version) + end + end + table.sort(versions, function(a, b) + return version_less_than(b, a) + end) + return versions +end + +local function row(path, name, image, quarto_version) + return string.format("| `%s` | %s | %s | %s |", path, name, image, quarto_version) +end + +local function configuration_table() + local versions = pinned_versions() + if #versions == 0 then + return nil + end + + local rows = { + "| Path | Name | Base | Quarto |", + "| --- | --- | --- | --- |", + row(".devcontainer/devcontainer.json", "Release", IMAGE, "`release`"), + row(".devcontainer/quarto-prerelease/devcontainer.json", "Pre-release", IMAGE, "`prerelease`"), + } + for _, version in ipairs(versions) do + table.insert( + rows, + row( + string.format(".devcontainer/quarto-%s/devcontainer.json", version), + version, + IMAGE, + string.format("`%s`", version) + ) + ) + end + table.insert(rows, row(".devcontainer/universal/devcontainer.json", "Universal", "`devcontainers/universal:latest`", "`release`")) + table.insert(rows, row(".github/.devcontainer/devcontainer.json", "Build recipe", "`buildpack-deps:noble-curl`", "build argument")) + + return pandoc.read(table.concat(rows, "\n"), "markdown").blocks +end + +function Div(div) + if div.identifier ~= "devcontainer-configurations" then + return nil + end + local blocks = configuration_table() + if not blocks then + return nil + end + div.identifier = "" + div.content = blocks + return div +end + +function Span(span) + local is_range = span.classes:includes("version-range") + local is_latest = span.classes:includes("version-latest") + if not (is_range or is_latest) then + return nil + end + + local versions = pinned_versions() + if #versions == 0 then + return nil + end + + local suffix = span.attributes["suffix"] or "" + if is_latest then + return pandoc.Code(versions[1] .. suffix) + end + return pandoc.Inlines({ + pandoc.Code(versions[#versions] .. suffix), + pandoc.Str(" … "), + pandoc.Code(versions[1] .. suffix), + }) +end diff --git a/docs/getting-started/codespaces.qmd b/docs/getting-started/codespaces.qmd index cd6a439..c78680f 100644 --- a/docs/getting-started/codespaces.qmd +++ b/docs/getting-started/codespaces.qmd @@ -17,7 +17,7 @@ The default configuration is `.devcontainer/devcontainer.json`, which pulls the ## Choose a different configuration -The repository provides configurations for the universal base image, the Quarto prerelease, and each Quarto minor version from `1.0` to `1.9`. +The repository provides configurations for the universal base image, the Quarto prerelease, and every Quarto minor version, []{.version-range}. They are listed in full on the [configurations](../reference/configurations.qmd) page. ### From a URL diff --git a/docs/reference/configurations.qmd b/docs/reference/configurations.qmd index 0eafe2b..8f5f4a5 100644 --- a/docs/reference/configurations.qmd +++ b/docs/reference/configurations.qmd @@ -8,24 +8,9 @@ subtitle: "Every `devcontainer.json` in the repository, and how to write your ow Codespaces uses `.devcontainer/devcontainer.json` unless you select another path, either through the `devcontainer_path` URL parameter or the creation dialog. See [GitHub Codespaces](../getting-started/codespaces.qmd) for both routes. -::: {.codespaces-tags} - -| Path | Name | Base | Quarto | -| --- | --- | --- | --- | -| `.devcontainer/devcontainer.json` | Release | `quarto-codespaces:latest` | `release` | -| `.devcontainer/quarto-prerelease/devcontainer.json` | Pre-release | `quarto-codespaces:latest` | `prerelease` | -| `.devcontainer/quarto-1.9/devcontainer.json` | 1.9 | `quarto-codespaces:latest` | `1.9` | -| `.devcontainer/quarto-1.8/devcontainer.json` | 1.8 | `quarto-codespaces:latest` | `1.8` | -| `.devcontainer/quarto-1.7/devcontainer.json` | 1.7 | `quarto-codespaces:latest` | `1.7` | -| `.devcontainer/quarto-1.6/devcontainer.json` | 1.6 | `quarto-codespaces:latest` | `1.6` | -| `.devcontainer/quarto-1.5/devcontainer.json` | 1.5 | `quarto-codespaces:latest` | `1.5` | -| `.devcontainer/quarto-1.4/devcontainer.json` | 1.4 | `quarto-codespaces:latest` | `1.4` | -| `.devcontainer/quarto-1.3/devcontainer.json` | 1.3 | `quarto-codespaces:latest` | `1.3` | -| `.devcontainer/quarto-1.2/devcontainer.json` | 1.2 | `quarto-codespaces:latest` | `1.2` | -| `.devcontainer/quarto-1.1/devcontainer.json` | 1.1 | `quarto-codespaces:latest` | `1.1` | -| `.devcontainer/quarto-1.0/devcontainer.json` | 1.0 | `quarto-codespaces:latest` | `1.0` | -| `.devcontainer/universal/devcontainer.json` | Universal | `devcontainers/universal:latest` | `release` | -| `.github/.devcontainer/devcontainer.json` | Build recipe | `buildpack-deps:noble-curl` | build argument | +::: {#devcontainer-configurations .codespaces-tags} + +This table is generated from `.devcontainer/` when the site is rendered. ::: diff --git a/docs/reference/images.qmd b/docs/reference/images.qmd index 73b6620..de53bed 100644 --- a/docs/reference/images.qmd +++ b/docs/reference/images.qmd @@ -18,8 +18,8 @@ docker pull ghcr.io/mcanouil/quarto-codespaces:latest | `latest` | stable release | `amd64`, `arm64` | | `release`, `release-noble` | stable release | `amd64`, `arm64` | | `prerelease`, `prerelease-noble` | prerelease | `amd64`, `arm64` | -| `1.0` … `1.9` | that minor version | `amd64` | -| `1.0-noble` … `1.9-noble` | that minor version | `amd64` | +| []{.version-range} | that minor version | `amd64` | +| []{.version-range suffix="-noble"} | that minor version | `amd64` | | `release-`, `prerelease-`, `-` | as above, pinned | as above | ::: @@ -44,7 +44,7 @@ Use `latest` to always get the current stable toolchain, and a build-metadata ta `release` and `prerelease` are built on native runners, `ubuntu-latest` for `linux/amd64` and `ubuntu-24.04-arm` for `linux/arm64`, then combined into a multi-architecture manifest. Docker picks the right one automatically, so Apple Silicon and ARM servers run natively. -The version-pinned images (`1.0` through `1.9`) are `linux/amd64` only. +The version-pinned images ([]{.version-range}) are `linux/amd64` only. They still run on ARM hosts through emulation, more slowly. ## Build schedule From cff1e1add11e1ca371722a480adabe681746a36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Canouil?= <8896044+mcanouil@users.noreply.github.com> Date: Sun, 26 Jul 2026 19:11:35 +0200 Subject: [PATCH 3/3] refactor: derive site colours from the brand and share card styles The navbar token overrides repeated the palette hex values already set in _brand.yml, so the two could drift. Reference the generated brand Sass variables instead, add the muted navbar tone to the palette, and drop the unused slate entry. Extract the shared panel and lead-in rules behind placeholders, since the hero, the section cards and the Codespaces entry points were three copies of the same surface with slightly different padding. Replace both marked sections of the quarto check page in a single awk pass; the output is unchanged. --- .github/workflows/devcontainer.yml | 40 +++----- docs/_brand.yml | 2 +- docs/assets/stylesheets/theme.scss | 153 ++++++++++++++++------------- 3 files changed, 98 insertions(+), 97 deletions(-) diff --git a/.github/workflows/devcontainer.yml b/.github/workflows/devcontainer.yml index 937bab1..3147a4d 100644 --- a/.github/workflows/devcontainer.yml +++ b/.github/workflows/devcontainer.yml @@ -296,16 +296,19 @@ jobs: local temp_file temp_file=$(mktemp) - # Update quarto check output section. The transcript is read from - # the file inside awk rather than passed with -v, because a - # multi-line -v value is not portable across awk variants. The - # blank lines around the injected block are required: without them + # Replace both marked sections in a single pass. The transcript is + # read from the file inside awk rather than passed with -v, because + # a multi-line -v value is not portable across awk variants. The + # blank lines around each injected block are required: without them # Pandoc reads the code fence as part of the HTML comment block. - awk -v start="${output_start}" \ - -v end="${output_end}" \ - -v outfile="${output_file}" ' + awk -v output_start="${output_start}" \ + -v output_end="${output_end}" \ + -v url_start="${url_start}" \ + -v url_end="${url_end}" \ + -v outfile="${output_file}" \ + -v url="${build_url}" ' BEGIN { in_block=0 } - index($0, start) { + index($0, output_start) { print $0 print "" print "```txt" @@ -319,20 +322,7 @@ jobs: in_block=1 next } - index($0, end) { - print $0 - in_block=0 - next - } - !in_block { print } - ' "${TARGET_FILE}" > "${temp_file}" - - # Update build URL section - awk -v start="${url_start}" \ - -v end="${url_end}" \ - -v url="${build_url}" ' - BEGIN { in_block=0 } - index($0, start) { + index($0, url_start) { print $0 print "" print "[View build log](" url ")" @@ -340,15 +330,15 @@ jobs: in_block=1 next } - index($0, end) { + index($0, output_end) || index($0, url_end) { print $0 in_block=0 next } !in_block { print } - ' "${temp_file}" > "${TARGET_FILE}" + ' "${TARGET_FILE}" > "${temp_file}" - rm -f "${temp_file}" + mv "${temp_file}" "${TARGET_FILE}" echo "Updated ${version} section in ${TARGET_FILE}" } diff --git a/docs/_brand.yml b/docs/_brand.yml index 6a63a1a..f1cac9f 100644 --- a/docs/_brand.yml +++ b/docs/_brand.yml @@ -10,10 +10,10 @@ color: palette: midnight: "#0B1220" midnight-2: "#111C2E" - slate: "#1B2942" frost: "#F5F7FA" ink: "#16202E" mist: "#DCE3EC" + mist-muted: "#8EA0B8" amber: "#E8A33D" amber-deep: "#A25E0C" background: diff --git a/docs/assets/stylesheets/theme.scss b/docs/assets/stylesheets/theme.scss index fba8bf7..a7d978f 100644 --- a/docs/assets/stylesheets/theme.scss +++ b/docs/assets/stylesheets/theme.scss @@ -1,112 +1,104 @@ /*-- scss:rules --*/ -// Pin the dark navbar and footer surfaces to the midnight/amber palette from -// _brand.yml. Atelier defaults these tokens to a neutral slate and bridges them -// onto the gitlink widget, so overriding them here is enough for both bars. +// Pin the dark navbar and footer surfaces to the palette from _brand.yml. +// Atelier defaults these tokens to a neutral slate and bridges them onto the +// gitlink widget, so overriding them here is enough for both bars. The values +// come from the brand palette rather than repeated literals, so the site has a +// single source of truth for its colours. :root { - --atelier-navbar-bg: #0b1220; - --atelier-navbar-surface: #111c2e; - --atelier-navbar-fg: #dce3ec; - --atelier-navbar-muted: #8ea0b8; - --atelier-navbar-accent: #e8a33d; - --atelier-navbar-accent-soft: rgba(232, 163, 61, 0.16); - --atelier-navbar-border: rgba(220, 227, 236, 0.14); + --atelier-navbar-bg: #{$brand-midnight}; + --atelier-navbar-surface: #{$brand-midnight-2}; + --atelier-navbar-fg: #{$brand-mist}; + --atelier-navbar-muted: #{$brand-mist-muted}; + --atelier-navbar-accent: #{$brand-amber}; + --atelier-navbar-accent-soft: #{rgba($brand-amber, 0.16)}; + --atelier-navbar-border: #{rgba($brand-mist, 0.14)}; // Space Grotesk is geometric and reads better with negative tracking than // the positive default tuned for the fallback stack. --atelier-heading-letter-spacing: -0.012em; } +// =========================================================================== +// SHARED SURFACES +// =========================================================================== +// The hero, the section cards, and the Codespaces entry points are all raised +// panels over the page background; body-mix() keeps them correct in both +// colour schemes. +%codespaces-surface { + padding: 1.1rem 1.25rem; + border: 1px solid body-mix(85%); + border-radius: $border-radius; + background: body-mix(97%); + + p:last-child { + margin-bottom: 0; + } +} + +// Small uppercase monospace lead-in, used above the hero and each entry point. +%codespaces-eyebrow { + font-family: var(--bs-font-monospace); + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; + color: $primary; +} + // =========================================================================== // HOME HERO // =========================================================================== .codespaces-hero { + @extend %codespaces-surface; + margin-block: 0 2.5rem; padding: 2rem 2rem 1.75rem; - border: 1px solid body-mix(80%); - border-radius: $border-radius-lg; background: - radial-gradient(120% 140% at 100% 0%, rgba(232, 163, 61, 0.14), transparent 60%), + radial-gradient(120% 140% at 100% 0%, rgba($brand-amber, 0.14), transparent 60%), body-mix(96%); - p { + > p { max-width: 60ch; font-size: 1.05rem; } - - p:last-child { - margin-bottom: 0; - } } .codespaces-hero-eyebrow { + @extend %codespaces-eyebrow; + display: inline-block; margin-bottom: 0.75rem; padding: 0.2rem 0.6rem; - border: 1px solid rgba(232, 163, 61, 0.4); + border: 1px solid rgba($brand-amber, 0.4); border-radius: 2rem; - background: rgba(232, 163, 61, 0.1); - color: $primary; - font-family: var(--bs-font-monospace); - font-size: 0.75rem; - letter-spacing: 0.06em; - text-transform: uppercase; + background: rgba($brand-amber, 0.1); } -// The two Codespaces badges are identical images, so each one needs its own -// label and caption to be distinguishable; lay them out as side-by-side cards. +// =========================================================================== +// CARD AND ACTION GRIDS +// =========================================================================== +.codespaces-grid, .codespaces-actions { display: grid; - grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: 1rem; - margin-block: 1.5rem 0; -} - -.codespaces-action { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 0.5rem; - padding: 1rem 1.1rem; - border: 1px solid body-mix(85%); - border-radius: $border-radius; - background: body-mix(98%); - - p { - margin: 0; - font-size: 0.9rem; - color: body-mix(35%); - } - - img { - display: block; - } -} - -.codespaces-action-label { - font-family: var(--bs-font-monospace); - font-size: 0.75rem; - font-weight: 600; - letter-spacing: 0.04em; - text-transform: uppercase; - color: $primary; + margin-block: 1.5rem; } -// =========================================================================== -// CARD GRID (section landing pages) -// =========================================================================== .codespaces-grid { - display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); - gap: 1rem; - margin-block: 1.5rem; +} + +// The two Codespaces badges are identical images, so each one needs its own +// label and caption to be distinguishable. +.codespaces-actions { + grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); + margin-bottom: 0; } .codespaces-card { - padding: 1.1rem 1.25rem; - border: 1px solid body-mix(85%); - border-radius: $border-radius; - background: body-mix(97%); + @extend %codespaces-surface; + transition: border-color 0.15s ease-in-out; &:hover, @@ -118,12 +110,31 @@ margin-top: 0; font-size: 1.05rem; } +} - p:last-child { - margin-bottom: 0; +.codespaces-action { + @extend %codespaces-surface; + + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + + p { + margin: 0; + font-size: 0.9rem; + color: body-mix(35%); + } + + img { + display: block; } } +.codespaces-action-label { + @extend %codespaces-eyebrow; +} + // =========================================================================== // TAG AND OPTION TABLES // ===========================================================================