diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000000..34d09448c5 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,73 @@ +name: Project website + +on: + push: + branches: [main] + paths: ["site/**", ".github/workflows/pages.yml"] + pull_request: + paths: ["site/**", ".github/workflows/pages.yml"] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pages-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Check static assets and fragment links + run: | + python3 - <<'PY' + from html.parser import HTMLParser + from pathlib import Path + from urllib.parse import urlsplit + + root = Path('site') + class Page(HTMLParser): + def __init__(self): + super().__init__() + self.ids, self.urls = set(), [] + def handle_starttag(self, tag, attrs): + attrs = dict(attrs) + if 'id' in attrs: + assert attrs['id'] not in self.ids, f"Duplicate ID: {attrs['id']}" + self.ids.add(attrs['id']) + self.urls.extend(attrs[key] for key in ('src', 'href') if key in attrs) + + page = Page() + page.feed((root / 'index.html').read_text()) + for url in page.urls: + parts = urlsplit(url) + if parts.scheme or parts.netloc: + continue + if parts.path: + assert (root / parts.path).exists(), f'Missing asset: {url}' + elif parts.fragment: + assert parts.fragment in page.ids, f'Missing fragment: {url}' + print('Static assets and fragment links passed.') + PY + - uses: actions/upload-pages-artifact@v4 + with: + path: site + + deploy: + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index b664c5a505..cccd80c26b 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,16 @@ -# PALM +# PALM +**Rust tests, guided by program analysis.** + +[![Website](https://img.shields.io/badge/website-PALM-5941bc)](https://cppbear.github.io/PALM/) +[![License: MIT](https://img.shields.io/badge/license-MIT-5941bc)](LICENSE) [![Build and test](https://github.com/cppbear/PALM/actions/workflows/ci.yml/badge.svg)](https://github.com/cppbear/PALM/actions/workflows/ci.yml) PALM generates Rust tests by combining program analysis with large language models. It extracts path constraints and code context, generates candidates, repairs compilation errors, and reports execution results and coverage with test code excluded. This is the maintained implementation of ***PALM: Synergizing Program Analysis and LLMs to Enhance Rust Unit Test Coverage***, published at ASE 2025. The current code includes changes made after the paper's experiments. -[Quick start](#quick-start) · [Examples](examples/README.md) · [CLI reference](utgen/README.md) · [中文技术指南](docs/palm-rust-unit-test-generation.md) · [Paper](https://doi.org/10.1109/ASE63991.2025.00223) · [Preprint](https://arxiv.org/abs/2506.09002) +[Website](https://cppbear.github.io/PALM/) · [Quick start](#quick-start) · [Examples](examples/README.md) · [CLI reference](utgen/README.md) · [中文技术指南](docs/palm-rust-unit-test-generation.md) · [Paper](https://doi.org/10.1109/ASE63991.2025.00223) · [Preprint](https://arxiv.org/abs/2506.09002) ## What PALM does diff --git a/site/README.md b/site/README.md new file mode 100644 index 0000000000..2c936dc242 --- /dev/null +++ b/site/README.md @@ -0,0 +1,13 @@ +# Project website + +The GitHub Pages site is plain HTML, CSS, and JavaScript. Preview from the repository root: + +```sh +python3 -m http.server 8000 --directory site +``` + +Open `http://localhost:8000`. The branch selector is an illustration of `examples/minimal`, not a live model or Rust execution. Keep detailed instructions in the repository and link to them from the site. + +The Pages workflow publishes only this directory after relevant changes reach `main`. In repository **Settings → Pages**, select **GitHub Actions** as the source. Project-relative asset URLs work under `/PALM/`. + +The self-hosted Sora semibold font is from [Google Fonts](https://github.com/google/fonts/tree/main/ofl/sora), licensed under the [SIL Open Font License](assets/OFL-Sora.md). The geometric PALM mark is part of this project's MIT-licensed source. diff --git a/site/assets/OFL-Sora.md b/site/assets/OFL-Sora.md new file mode 100644 index 0000000000..936a84ef40 --- /dev/null +++ b/site/assets/OFL-Sora.md @@ -0,0 +1,93 @@ +Copyright 2019 The Sora Project Authors (https://github.com/sora-xor/sora-font) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/site/assets/mark.svg b/site/assets/mark.svg new file mode 100644 index 0000000000..9ec29a881b --- /dev/null +++ b/site/assets/mark.svg @@ -0,0 +1 @@ + diff --git a/site/assets/sora-semibold.ttf b/site/assets/sora-semibold.ttf new file mode 100644 index 0000000000..66820ae428 Binary files /dev/null and b/site/assets/sora-semibold.ttf differ diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000000..a3471c9f42 --- /dev/null +++ b/site/index.html @@ -0,0 +1,360 @@ + + + + + + + + PALM — Rust tests, guided by program analysis + + + + + + + + +
+
+

+ Rust tests.
Grounded in your code. +

+
+

+ PALM combines program analysis with language models to turn path + constraints and code context into test candidates. +

+ Try the offline example + +
+
+
+

One function. Two paths to test.

+ Explore the fixture + +
+
+
+
+ Rust source classify(value) +
+
pub fn classify(value: i32) -> i32 {
+    if support::positive(value) {
+        1
+    } else {
+        0
+    }
+}
+

+ support::positive checks value > 0. +

+
+
+ + +
+
+ +
+ Path conditionvalue > 0 +
+
+
+ +
+ Example assertionassert_eq!(classify(2), 1); +
+
+
+
+
+

+ Interactive illustration of the minimal fixture. Its offline check + uses fixed local model responses; this page does not run Rust or + call a model. +

+
+
+ Open source · MIT licensedRead the ASE 2025 paper + +
+
+ +
+
+

+ Give the model
more than a signature. +

+

+ PALM brings the function’s paths and dependencies into the prompt, + then checks the generated tests with the Rust toolchain. +

+
+
    +
  1. + +

    Analyze the code

    +

    + Extract representative path constraints, calls, and type + dependencies for the focal function. +

    + Explore code context +
  2. +
  3. + +

    Generate & repair

    +

    + Generate candidates with a language model. Use compiler + diagnostics to repair compilation errors. +

    + Explore the CLI +
  4. +
  5. + +

    Run & measure

    +

    + Inspect compilation, execution, and coverage per function, with + test code excluded from coverage. +

    + Understand the results +
  6. +
+

+ Repair addresses compilation errors. Assertion failures and candidate + timeouts remain failed test outcomes. +

+
+ +
+
+
+

Start small.
See the whole pipeline.

+

+ The minimal example checks analysis, generation, compilation + repair, execution, coverage, and source restoration. No model + credentials needed. +

+

+ First install the + prerequisites: Git, Rust and a C linker, Python 3.9+, the pinned nightly, and + cargo-llvm-cov. Downloads may need network access. +

+
+
+
+ Run from your terminal +
+
git clone https://github.com/cppbear/PALM.git
+cd PALM
+cargo build --workspace --locked
+python3 scripts/check_minimal.py
+

Expected completion

+

+ Minimal analysis/generation/repair/coverage checks passed. +

+

+
+
+

Ready for your own package?

+

+ Install the tools, configure a model, and begin with a small + function list and request budget. Preprocessing changes source and + tests, so use a fresh working copy. +

+ Follow a bounded example + +
+
+
+ +
+
+

Know where it fits.

+

+ Start with one standalone Cargo package and a few functions. Expand + after checking the results. +

+
+
+
+
Unit & integration tests
+
+ Unit tests use the recorded library or binary target. + Function-level integration tests require a library and check + external access. +
+
+
+
One package at a time
+
+ Ordinary lib/bin targets are supported, including custom names and + entry files. The complete workflow does not support multi-package + workspaces. +
+
+
+
Explicit analysis boundaries
+
+ Analysis follows the active build configuration. Definitions + shared by lib and bin prefer lib; this does not cover every binary + variant. +
+
+
+
A pinned Rust toolchain
+
+ Uses nightly-2025-03-19 and cargo-llvm-cov + 0.6.16. Linux CI and local macOS Apple Silicon have + been exercised; Windows has not been validated. +
+
+
+ Full scope and limitations + +
+ +
+
+

+ Built on research.
Open for exploration. +

+

+ PALM: Synergizing Program Analysis and LLMs to Enhance Rust Unit + Test Coverage. +

+

+ Published at ASE 2025. This maintained implementation includes + changes made after the paper’s experiments. +

+
+ +
+
+ + + diff --git a/site/main.js b/site/main.js new file mode 100644 index 0000000000..dcae1bcf27 --- /dev/null +++ b/site/main.js @@ -0,0 +1,32 @@ +const controls = document.querySelector("#path-controls"); +controls.hidden = false; +controls.addEventListener("change", (event) => { + const positive = event.target.value === "positive"; + document + .querySelector("#positive-line") + .classList.toggle("selected", positive); + document + .querySelector("#nonpositive-line") + .classList.toggle("selected", !positive); + document.querySelector("#path-condition").textContent = positive + ? "value > 0" + : "value <= 0"; + document.querySelector("#path-assertion").textContent = positive + ? "assert_eq!(classify(2), 1);" + : "assert_eq!(classify(-1), 0);"; +}); + +const copyButton = document.querySelector("[data-copy]"); +copyButton.hidden = false; +copyButton.addEventListener("click", async () => { + const status = document.querySelector("#copy-status"); + try { + await navigator.clipboard.writeText( + document.getElementById(copyButton.dataset.copy).textContent, + ); + status.textContent = "Commands copied."; + } catch { + status.textContent = + "Copy unavailable. Select the commands above and copy them manually."; + } +}); diff --git a/site/styles.css b/site/styles.css new file mode 100644 index 0000000000..ff95fa1f21 --- /dev/null +++ b/site/styles.css @@ -0,0 +1,834 @@ +@font-face { + font-family: Sora; + src: url("./assets/sora-semibold.ttf") format("truetype"); + font-style: normal; + font-weight: 600; + font-display: swap; +} +:root { + --paper: #f7f7fc; + --ink: #222133; + --muted: #59576b; + --violet: #5941bc; + --violet-dark: #3d287f; + --lavender: #eae5fa; + --line: #d5d2e1; + --white: #fff; + --display: Sora, system-ui, sans-serif; +} +* { + box-sizing: border-box; +} +html { + scroll-behavior: smooth; + scroll-padding-top: 24px; + scrollbar-color: #a49abf var(--paper); +} +body { + margin: 0; + background: var(--paper); + color: var(--ink); + font: + 17px/1.65 system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; +} +::selection { + background: #d6ccff; + color: #251747; +} +a { + color: inherit; + text-underline-offset: 5px; +} +a:hover { + color: var(--violet); +} +button, +input { + font: inherit; +} +button { + cursor: pointer; +} +a:focus-visible, +button:focus-visible, +input:focus-visible + span { + outline: 3px solid var(--violet); + outline-offset: 5px; +} +h1, +h2, +h3, +p { + margin: 0; +} +h1, +h2, +h3 { + font-family: var(--display); + font-weight: 600; + line-height: 1.2; + text-wrap: balance; +} +h1 { + font-size: clamp(2.7rem, 5.3vw, 4.75rem); + letter-spacing: -0.035em; +} +h2 { + font-size: clamp(1.85rem, 3vw, 2.6rem); + letter-spacing: -0.025em; +} +h3 { + font-size: 1.12rem; + letter-spacing: -0.015em; +} +p { + max-width: 70ch; +} +code { + font-family: ui-monospace, SFMono-Regular, Consolas, monospace; + font-size: 0.88em; +} +pre { + margin: 0; + overflow-x: auto; + tab-size: 4; +} +pre code { + font-size: 14px; + line-height: 1.85; +} +.wrap { + width: min(1184px, calc(100% - 80px)); + margin-inline: auto; +} +.site-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-block: 28px; +} +.wordmark { + display: inline-flex; + gap: 10px; + align-items: center; + font: 600 25px/1 var(--display); + letter-spacing: -0.03em; + text-decoration: none; +} +.wordmark img { + display: block; +} +.site-header nav, +.site-footer nav { + display: flex; + gap: 28px; + align-items: center; +} +.site-header nav a { + font-size: 15px; + text-decoration: none; +} +.arrow { + width: 18px; + height: 18px; + fill: none; + stroke: currentColor; + stroke-width: 1.7; + vertical-align: middle; + flex-shrink: 0; +} +.skip-link { + position: absolute; + left: 20px; + top: -100px; + padding: 12px 20px; + background: var(--white); + z-index: 10; +} +.skip-link:focus { + top: 12px; +} +.hero { + padding-top: 45px; +} +.hero h1 span { + color: var(--violet); +} +.hero-intro { + display: flex; + align-items: center; + justify-content: space-between; + gap: 40px; + margin-top: 26px; +} +.hero-intro p { + font-size: 19px; + max-width: 660px; + color: var(--muted); +} +.button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 16px; + padding: 14px 20px; + border-radius: 8px; + font-size: 15px; + font-weight: 600; + text-decoration: none; + min-height: 48px; + flex-shrink: 0; +} +.primary { + background: var(--violet); + color: var(--white); +} +.primary:hover { + background: var(--violet-dark); + color: var(--white); +} +.secondary { + border: 1px solid var(--violet); + color: var(--violet); +} +.secondary:hover { + background: var(--lavender); +} +.path-demo { + margin-top: 42px; + background: var(--lavender); + border-radius: 16px; + overflow: hidden; +} +.demo-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + padding: 24px 32px; + border-bottom: 1px solid #cfc6e8; +} +.demo-heading h2 { + font-size: 19px; + letter-spacing: -0.02em; +} +.demo-heading a { + font-size: 14px; +} +.demo-body { + display: grid; + grid-template-columns: 1fr 1fr; +} +.source { + padding: 25px 32px 22px; + border-right: 1px solid #cfc6e8; + min-width: 0; +} +.source-caption { + display: flex; + justify-content: space-between; + gap: 16px; + margin-bottom: 14px; + font-size: 13px; + color: #554574; +} +.source-caption span { + font-family: ui-monospace, monospace; +} +.source pre { + font-size: 14px; +} +.code-keyword { + color: #644288; +} +.code-type { + color: #37566c; +} +.code-line { + display: inline-block; + min-width: 100%; + border-radius: 3px; + transition: background-color 0.18s ease-out; +} +.code-line.selected { + background: #d6caef; + color: #332063; +} +.source-note { + font-size: 13px; + color: #554574; + margin-top: 13px; +} +.trace { + padding: 24px 32px; + min-width: 0; +} +fieldset { + padding: 0; + border: 0; + margin: 0; +} +legend { + padding: 0; + font-size: 14px; + margin-bottom: 12px; +} +.input-options { + display: flex; + gap: 10px; +} +.input-options label { + position: relative; + cursor: pointer; +} +.input-options input { + position: absolute; + opacity: 0; + width: 1px; + height: 1px; +} +.input-options span { + display: block; + padding: 8px 17px; + background: #f8f5ff; + border: 1px solid #bdb0d8; + border-radius: 6px; + font: + 14px/1.8 ui-monospace, + monospace; +} +.input-options input:checked + span { + background: var(--violet); + border-color: var(--violet); + color: white; +} +.input-options label:hover span { + border-color: var(--violet); +} +.route-result { + margin-top: 24px; +} +.route-step { + position: relative; + display: flex; + gap: 20px; + padding-bottom: 22px; +} +.route-step:first-child:before { + content: ""; + position: absolute; + width: 2px; + background: var(--violet); + left: 6px; + top: 12px; + bottom: -6px; +} +.route-step:last-child { + padding-bottom: 0; +} +.route-dot { + position: relative; + width: 14px; + height: 14px; + border: 2px solid var(--violet); + border-radius: 50%; + background: var(--lavender); + margin-top: 6px; + flex-shrink: 0; +} +.route-dot.end { + background: var(--violet); +} +.route-label { + display: block; + font-size: 13px; + color: #554574; + margin-bottom: 4px; +} +.route-step code { + color: #32205e; + font-size: 15px; + overflow-wrap: anywhere; +} +.demo-note { + max-width: none; + padding: 14px 32px; + border-top: 1px solid #cfc6e8; + font-size: 12px; + color: #554574; +} +.hero-foot { + display: flex; + justify-content: space-between; + gap: 20px; + padding-top: 17px; + color: var(--muted); + font-size: 13px; +} +.section { + padding-block: 104px; +} +.section-intro { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 60px; +} +.section-intro p { + max-width: 455px; + color: var(--muted); + padding-top: 5px; +} +.workflow-steps { + display: grid; + grid-template-columns: repeat(3, 1fr); + list-style: none; + padding: 0; + margin: 46px 0 0; + gap: 40px; +} +.workflow-steps li { + border-top: 2px solid var(--violet); + padding-top: 20px; +} +.step-index { + display: block; + color: var(--violet); + font: 600 24px/1 var(--display); + margin-bottom: 24px; +} +.workflow-steps p { + font-size: 15px; + margin-top: 14px; + color: var(--muted); +} +.workflow-steps a { + display: inline-block; + margin-top: 20px; + font-size: 14px; +} +.workflow-note { + font-size: 13px; + color: var(--muted); + margin-top: 32px; +} +.start-section { + background: #ebe7f6; + padding-block: 76px; +} +.start-grid { + display: grid; + grid-template-columns: 1fr 1.1fr; + gap: 42px 65px; + align-items: start; +} +.start-grid h2 + p { + margin-top: 22px; + color: #504861; +} +.prerequisites { + font-size: 14px; + margin-top: 24px; + color: #504861; +} +.prerequisites a { + font-weight: 600; +} +.terminal { + background: #29243b; + color: #f0eafb; + border-radius: 12px; + padding: 23px 25px; + min-width: 0; +} +.terminal-heading { + display: flex; + justify-content: space-between; + align-items: center; + gap: 15px; + font-size: 13px; + margin-bottom: 25px; + color: #d2c8e7; +} +.copy-button { + border: 1px solid #807397; + background: transparent; + border-radius: 5px; + color: #f0eafb; + padding: 7px 10px; + font-size: 12px; + min-height: 36px; +} +.copy-button:hover { + background: #47385f; +} +.copy-button:focus-visible { + outline-color: #c3b1ff; +} +.terminal pre code { + font-size: 12px; + line-height: 2; +} +.expected { + border-top: 1px solid #544765; + margin-top: 26px; + padding-top: 20px; + font-size: 12px; + color: #d2c8e7; +} +.terminal-result { + font: + 12px/1.8 ui-monospace, + monospace; + color: #bce4cc; + margin-top: 7px; +} +.copy-status { + font-size: 12px; + color: #d2c8e7; +} +.copy-status:not(:empty) { + margin-top: 12px; +} +.next-step { + grid-column: 1/-1; + display: grid; + grid-template-columns: 1fr auto; + gap: 12px 45px; + padding-top: 32px; + border-top: 1px solid #c8bedc; +} +.next-step p { + max-width: 710px; + color: #504861; + font-size: 15px; +} +.next-step .button { + grid-column: 2; + grid-row: 1/3; + align-self: center; +} +.scope .section-intro { + margin-bottom: 38px; +} +.scope-list { + margin: 0; +} +.scope-list > div { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 40px; + padding-block: 23px; + border-top: 1px solid var(--line); +} +.scope-list dt { + font-size: 16px; + font-weight: 600; +} +.scope-list dd { + margin: 0; + color: var(--muted); + font-size: 15px; + max-width: 72ch; +} +.text-link { + display: inline-block; + margin-top: 20px; + font-size: 15px; +} +.research { + display: grid; + grid-template-columns: 1.4fr 1fr; + gap: 90px; + background: var(--violet-dark); + color: #fbf9ff; + padding: 48px; + border-radius: 16px; +} +.research h2 { + font-size: 2rem; +} +.research p { + margin-top: 22px; + font-size: 17px; + max-width: 510px; +} +.research .research-note { + font-size: 13px; + color: #ded3f4; + margin-top: 14px; +} +.research-links { + align-self: center; +} +.research-links a { + display: flex; + justify-content: space-between; + gap: 24px; + padding-block: 14px; + border-bottom: 1px solid #7764a5; + font-size: 15px; + text-decoration: none; +} +.research-links a:hover { + color: #e1d3ff; +} +.research a:focus-visible { + outline-color: #c3b1ff; +} +.site-footer { + display: flex; + align-items: center; + gap: 25px; + padding-block: 50px; +} +.site-footer .wordmark { + font-size: 20px; +} +.site-footer p { + font-size: 12px; + color: var(--muted); + flex: 1; +} +.site-footer nav { + gap: 20px; + font-size: 12px; +} +@media (min-width: 1500px) { + .hero { + padding-top: 65px; + } +} +@media (max-width: 1000px) { + .wrap { + width: calc(100% - 48px); + } + .hero-intro { + align-items: flex-start; + gap: 25px; + } + .hero-intro p { + font-size: 17px; + } + .button { + padding-inline: 16px; + } + .demo-heading, + .source, + .trace { + padding-inline: 24px; + } + .section-intro { + gap: 35px; + } + .section-intro p { + max-width: 42%; + } + .start-grid { + gap: 35px; + } + .terminal { + padding-inline: 20px; + } + .research { + gap: 50px; + padding: 38px; + } + .site-footer { + flex-wrap: wrap; + } + .site-footer nav { + margin-left: auto; + } +} +@media (max-width: 760px) { + .wrap { + width: calc(100% - 40px); + } + .site-header { + padding-block: 23px; + } + .wordmark { + font-size: 22px; + } + .site-header nav { + gap: 18px; + } + .site-header nav a { + font-size: 13px; + } + .site-header nav a:first-child { + display: none; + } + .hero { + padding-top: 27px; + } + .hero-intro { + display: block; + margin-top: 23px; + } + .hero-intro p { + font-size: 17px; + } + .hero-intro .button { + margin-top: 22px; + } + .path-demo { + margin-top: 32px; + } + .demo-heading { + display: block; + padding: 21px; + } + .demo-heading h2 { + font-size: 17px; + } + .demo-heading a { + display: inline-block; + margin-top: 10px; + font-size: 13px; + } + .demo-body { + grid-template-columns: 1fr; + } + .source { + padding: 20px; + border-right: 0; + border-bottom: 1px solid #cfc6e8; + } + .source pre code { + font-size: 12px; + } + .source-caption { + font-size: 12px; + } + .trace { + padding: 22px 20px; + } + .demo-note { + padding: 16px 20px; + line-height: 1.6; + } + .route-step code { + font-size: 13px; + } + .hero-foot { + display: block; + font-size: 12px; + } + .hero-foot a { + display: block; + margin-top: 6px; + } + .section { + padding-block: 67px; + } + .section-intro { + display: block; + } + .section-intro p { + max-width: none; + margin-top: 21px; + padding: 0; + font-size: 16px; + } + .workflow-steps { + grid-template-columns: 1fr; + gap: 29px; + margin-top: 34px; + } + .workflow-steps li { + display: grid; + grid-template-columns: 32px 1fr; + column-gap: 15px; + padding-top: 23px; + } + .step-index { + grid-row: 1/4; + margin: 0; + font-size: 21px; + } + .workflow-steps p { + margin-top: 10px; + } + .workflow-steps a { + margin-top: 14px; + } + .workflow-note { + margin-top: 25px; + } + .start-section { + padding-block: 51px; + } + .start-grid { + grid-template-columns: 1fr; + gap: 30px; + } + .terminal { + padding: 20px; + } + .terminal-heading { + font-size: 12px; + gap: 8px; + } + .terminal pre code { + font-size: 11px; + } + .next-step { + display: block; + padding-top: 28px; + } + .next-step p { + margin-top: 12px; + } + .next-step .button { + margin-top: 23px; + } + .scope-list > div { + grid-template-columns: 1fr; + gap: 9px; + padding-block: 21px; + } + .scope .section-intro { + margin-bottom: 29px; + } + .research { + grid-template-columns: 1fr; + padding: 28px 24px; + gap: 20px; + } + .research h2 { + font-size: 1.6rem; + } + .research p { + font-size: 15px; + } + .site-footer { + gap: 18px; + padding-block: 35px; + } + .site-footer p { + flex-basis: 65%; + font-size: 11px; + } + .site-footer nav { + margin-left: 0; + width: 100%; + gap: 25px; + } + .source-note { + font-size: 12px; + } +} +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + .code-line { + transition: none; + } +}