diff --git a/docs/assets/extra.css b/docs/assets/extra.css index 7b2b300..72aa9a9 100644 --- a/docs/assets/extra.css +++ b/docs/assets/extra.css @@ -1,29 +1,105 @@ /* Brand palette ----------------------------------------------------------- - Material's "custom" primary/accent read these variables. */ + Material's "custom" primary/accent read these variables. + + Ink is the brand's dark and Signal Blue its accent. The header and footer + are Ink on every page, so the docs and the landing page share one frame; + Signal Blue is the same #2c9ccd the README badge has always used, so badges + already in the wild still match. */ :root { + --cc-ink: #0b1620; + --cc-ink-raised: #13212c; --cc-brand: #2c9ccd; --cc-brand-dark: #1e85a8; --cc-brand-light: #5bb3d9; - /* The brand blue only reaches 4.2:1 on white; links need 4.5:1. */ + /* The brand blue only reaches 3.1:1 on white; links need 4.5:1. */ --cc-link: #176b89; - /* The hero sits on brand blue, so its button is white rather than branded. */ - --cc-hero-button-bg: #ffffff; - - --md-primary-fg-color: var(--cc-brand); - --md-primary-fg-color--light: var(--cc-brand-light); - --md-primary-fg-color--dark: var(--cc-brand-dark); + --md-primary-fg-color: var(--cc-ink); + --md-primary-fg-color--light: var(--cc-ink-raised); + --md-primary-fg-color--dark: #060d13; --md-accent-fg-color: var(--cc-brand-dark); + + /* A literal stack: the theme defines --md-text-font-family below :root, + so a var() of it here would resolve to nothing. */ + --cc-display-font: "Bricolage Grotesque", "Instrument Sans", -apple-system, "Segoe UI", sans-serif; } +/* The theme sets the footer colours per scheme, so they are overridden per + scheme too; on :root alone they would lose to the theme's own. */ +[data-md-color-scheme="default"], [data-md-color-scheme="slate"] { - --md-primary-fg-color: var(--cc-brand); + --md-footer-bg-color: #081119; + --md-footer-bg-color--dark: #060d13; +} + +[data-md-color-scheme="slate"] { + /* Material tints every slate grey from one hue; 205 is Ink's. */ + --md-hue: 205; + --md-default-bg-color: #0e1a23; + --md-primary-fg-color: var(--cc-ink); --md-accent-fg-color: var(--cc-brand-light); --md-typeset-a-color: var(--cc-brand-light); } [data-md-color-scheme="default"] { --md-typeset-a-color: var(--cc-link); + /* A light header in the light scheme, Ink in the dark one, so the header + answers the theme toggle like the rest of the page. */ + --md-primary-fg-color: #ffffff; + --md-primary-fg-color--light: #f4f6f7; + --md-primary-fg-color--dark: #eef2f4; + --md-primary-bg-color: var(--cc-ink); + --md-primary-bg-color--light: #4f6270; +} + +[data-md-color-scheme="default"] .md-header { + box-shadow: inset 0 -1px 0 #dce3e8; +} + +/* The theme's search field is a translucent black meant for a coloured + header; on a white one it needs a solid tint instead. */ +[data-md-color-scheme="default"] .md-search__form { + background-color: #eef2f4; +} + +[data-md-color-scheme="default"] .md-search__form:hover { + background-color: #e4e9ed; +} + +/* A primary button is Signal Blue with Ink text: white on this blue is 3.1:1, + under the 4.5:1 a button label needs; Ink on it is 5.9:1. */ +.md-typeset .md-button--primary { + background-color: var(--cc-brand); + border-color: var(--cc-brand); + color: var(--cc-ink); +} + +.md-typeset .md-button--primary:focus, +.md-typeset .md-button--primary:hover { + background-color: var(--cc-brand-light); + border-color: var(--cc-brand-light); + color: var(--cc-ink); +} + +/* Display face for page titles, section headings and the site name. Body text + stays in the theme's text font. */ +.md-typeset h1, +.md-typeset h2 { + font-family: var(--cc-display-font); + font-weight: 700; + letter-spacing: -0.02em; +} + +/* The theme draws page titles in a light grey at weight 300; in the display + face they read as headings only at full strength. */ +.md-typeset h1 { + color: var(--md-default-fg-color); +} + +.md-header__title { + font-family: var(--cc-display-font); + font-weight: 700; + letter-spacing: -0.01em; } /* Feature grid: uses Material's card grid, tightened up a little. */ diff --git a/docs/assets/favicon.svg b/docs/assets/favicon.svg index 896e52a..13f3d13 100644 --- a/docs/assets/favicon.svg +++ b/docs/assets/favicon.svg @@ -1,8 +1,6 @@ - - - - - - - + + + + diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg index a4d1132..1f13a0f 100644 --- a/docs/assets/logo.svg +++ b/docs/assets/logo.svg @@ -1,13 +1,8 @@ - - - - - - - - - - - - + + + + + + diff --git a/docs/guides/github-app.md b/docs/guides/github-app.md index 584e5cd..4241e0b 100644 --- a/docs/guides/github-app.md +++ b/docs/guides/github-app.md @@ -181,13 +181,13 @@ without a wall of red. Public repositories are free on any account, and everything on a personal account is free, and both stay free. Only an organization's private -repositories are paid: +repositories will be paid, on a Team plan that is not on sale yet: | Plan | Price | Covers | |---|---|---| | Open Source | Free | Public repositories, on any account | | Personal | Free | Private repositories on a personal account | -| Team | $19 / month | Private repositories in an organization, 14-day free trial | +| Team | Coming soon | Private repositories in an organization | The Team plan is not tiered by seat count. Twenty of you and twenty thousand of you pay the same, so growing the team never changes the bill. diff --git a/docs/index.md b/docs/index.md index 4a0ae50..c4ca2b5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,320 +4,453 @@ hide: - toc template: landing.html title: Commit Check -description: One config file enforced in your commit-msg hook, in CI, on every pull request and in your AI agent. Rules for commit messages, branch names, author identity and signoff. +description: Catch bad commits before they merge. Messages, branches, authors and AI attribution, checked from your commit hook to every pull request. --- -
-
- -# One config file. Every place your team commits. - -Laptop, CI, pull request, AI agent — the same `cchk.toml`, the same rules, -the same diagnostics, with a fix you can paste. - -[Get started :octicons-arrow-right-24:](getting-started.md){ .md-button .md-button--primary } -[Browse the rules](rules.md){ .md-button } - -
-
- -```console -$ echo 'Fix: add streaming support' | commit-check --message -CC001 message check failed ==> Fix: add streaming support -The commit message should follow Conventional Commits. See https://www.conventionalcommits.org -Suggest: Use "fix: add streaming support" -Docs: https://commit-check.com/rules/#cc001 -``` - -When the correction is unambiguous, it hands you the line. - -
-
- -
- -**Commit Check runs in repositories across these organizations, and in -[many more](https://github.com/commit-check/commit-check-action/network/dependents).** - -
-
- Apache - Apache -
-
- Discovery Unicamp - Discovery Unicamp -
-
- Texas Instruments - Texas Instruments -
-
- OpenCADC - OpenCADC -
-
- Extrawest - Extrawest -
-
- Chainlift - Chainlift -
-
- Mila - Mila -
-
- RLinf - RLinf -
-
- Istio Ecosystem - Istio Ecosystem -
-
- Juniper Networks - Juniper Networks -
-
- French National Parks - French National Parks -
-
- OpenDriveLab - OpenDriveLab -
-
- UT Austin RobIn - UT Austin RobIn -
-
- WorldArena2 - WorldArena2 -
-
- moniqo - moniqo -
-
- elu mobility - elu mobility -
-
- Open Energy Platform - Open Energy Platform -
-
- Collective - Collective -
-
- -
- -## Why check commit metadata at all - -
- -- __Changelog tools have nothing to group by__ - - --- - - `git-cliff` and `semantic-release` read the `type:` prefix on the subject - line to decide what a commit was. Without a consistent subject there is - nothing to read, and the release notes get written by hand from `git log`. - -- __`git bisect` stops at a merge commit__ - - --- - - When the first bad commit is a merge, the change is in one of two parents - or in the conflict resolution. Bisect cannot narrow it any further. - -- __The author is `ec2-user`__ - - --- - - A build box with no `user.name` set writes itself into the history. - `git log --author` finds the commit; there is no person on the other end - of it. - -- __A DCO check fails on a branch you already wrote__ - - --- - - `Signed-off-by` costs one `-s` at commit time. Adding it afterwards means - `git rebase --signoff` across the whole branch and a force-push. - -
- -None of these are caught by a linter, a type checker or a test suite. They are -caught in review — which means inconsistently, and after the work is done. - -The check that runs in CI is the same one that runs in your `commit-msg` hook. -Fixing a subject line at commit time costs a second; fixing it after CI costs a -full run and a force-push. - -## What your team actually sees - -On a pull request, every finding carries a rule ID, the value that failed, and -what to do about it — in the job summary, as annotations on the changed files, -and as a single comment that is edited in place rather than added to. - -| Scope | Checked value | Failed checks | -|---|---|---| -| Commit 2/2 (5584f46) | `bad msg` | CC001 message | -| Branch | `Feature/Add-Login` | CC201 branch | - -```text -Commit message - ✔ PR title (feat: add login page) - ✔ Commit 1/2 (d87faca) (feat: add login page) - ✖ Commit 2/2 (5584f46) (1 failure) - CC001 message - value: bad msg - The commit message should follow Conventional Commits. - Suggest: Use (): -Branch - ✖ Branch (1 failure) - CC201 branch - value: Feature/Add-Login - The branch should follow Conventional Branch. - Suggest: Rename the branch to "feature/Add-Login" (git branch -m feature/Add-Login) - Fix: feature/Add-Login -``` - -And in the merge box, where the decision actually gets made: - -
-![The Commit Check check run in a pull request merge box, reporting 2 of 4 checks failed](assets/merge-box.png){ loading=lazy } -
The hosted GitHub App reports one check run per commit. The title -names what failed, so nobody opens Details to learn whether it was the message, -the branch or an author email.
-
- -
-![A terminal recording: a commit message and a branch name are rejected, then accepted once corrected](assets/demo.gif){ .cc-motion loading=lazy } -![The same terminal at the end of the recording, both checks having run](assets/demo-poster.png){ .cc-still loading=lazy } -
The same engine on the command line. The recording is replaced by a -still frame when your system asks for reduced motion.
+ + +
+ +
+
+

Open source · MIT licensed

+

Catch bad commits before they merge.

+

Commit Check validates commit messages, branch names, authors and sign-offs against one cchk.toml — in your commit hook, in CI, on every pull request and inside your AI agent. When the fix is obvious, it hands you the line.

+
+Add to GitHub Actions +
pip install commit-check
+
+
+ +
+ +
+
❯ git commit -m "Fix: add streaming support"
+
check commit messageFailed
+
- hook id: check-message
+
- exit code: 1
+
Commit rejected by Commit-Check.
+
CC001 message check failed ==> Fix: add streaming support
+
The commit message should follow Conventional Commits. See https://www.conventionalcommits.org
+
Suggest: Use "fix: add streaming support"
+
Docs: https://commit-check.com/rules/#cc001
+
❯ git commit -m "fix: add streaming support"
+
check commit messagePassed
+
[feature/streaming 77663ff] fix: add streaming support
+
- -## Start with two commands - -```console -$ pip install commit-check -$ commit-check --message --branch -``` - -No configuration file needed to start: the defaults check Conventional Commits, -Conventional Branch and subject length, and you tighten them when you are ready. Every release -carries a signed [build provenance attestation](https://docs.github.com/en/actions/concepts/security/artifact-attestations), -so you can verify an artifact came from this repository's pipeline before you -install it. - -## Pick where it runs - -One policy engine, five places to enforce it. Every one of them reads the same -`cchk.toml`. - -
- -- __Command line__ - - --- - - The engine itself. Any forge, any CI, plus a JSON mode and a Python API for - scripts and agents. - - [:octicons-arrow-right-24: Getting started](getting-started.md) - -- __pre-commit hook__ - - --- - - It rejects a bad commit before Git records it. - Opt-in by nature, so pair it with one of the enforced surfaces. - - [:octicons-arrow-right-24: Guide](guides/pre-commit.md) - -- __GitHub Action__ - - --- - - Runs in CI whether or not the hook ran. Make it a required check and a - violation cannot merge, with per-rule outputs later steps can gate on. - - [:octicons-arrow-right-24: Guide](guides/github-actions.md) - -- __GitHub App__ - - --- - - No workflow file and no CI minutes. Install it once and every push and pull - request gets a check run. - - [:octicons-arrow-right-24: Guide](guides/github-app.md) - -- __MCP server__ - - --- - - The validations as structured tools, so an AI coding agent checks its own - commit before it writes it. - - [:octicons-arrow-right-24: Guide](guides/mcp.md) - +
+
    +
  • MIT licensed, no account needed
  • +
  • Python 3.10 – 3.14 on Linux, macOS and Windows
  • +
  • Signed build provenance on every release
  • +
  • CLI and hook work with any Git host
  • +
+
+ +
+ +
+ +
+
+
+

commit-check-action

+

Every pull request, checked and explained.

+

The Action reports where your reviewers already look — a PR comment, the job summary, inline annotations — and exposes a JSON result the next step can gate on.

+
+ +
+
+ + + + +
-## Pricing - -The CLI, the pre-commit hook, the GitHub Action and the MCP server are MIT -licensed — no account, no limits, nothing to buy. The plans below are for the -hosted GitHub App, the one surface we run for you. - -
- -- __Open Source__ · Free - - --- - - Public repositories, on any account. - -- __Personal__ · Free - - --- - - Private repositories on a personal account. - -- __Team__ · $19 / month +
+

One report on the pull request, edited in place on every push.Turn it on with pr-comments: true

+
+ +
+

feat: add login page #128Open

+

alex wants to merge 3 commits into main from feature/add-login

+ +
+
+
+ +
+

github-actionsbot commentededited

+
+

Commit Check

+
+

2 of 5 checks failed

+ + + + + + +
ScopeChecked valueFailed checks
Commit 2/3 (5584f46)bad msgCC001 message
Commit 3/3 (37d6def)Fix: handle empty passwordCC001 message
+

▸ Show all 5 checks

+
+
+

All 5 checks passed

+

▸ Show all 5 checks

+
+

Rules reference

+
+
+
+

alex force-pushed feature/add-login, rewording 2 commits

+

The same comment was updated. No new comment, no noise in the thread.

+
+
+
- --- +
+

The full breakdown lands in the workflow run: each check, the value, the fix.Turn it on with job-summary: true

+
+ +
+ +
+

Triggered via pull_requestalex pushed 5584f46

StatusFailure

Total duration14s

+
+

commit-check summary

+
+

Commit Check

+

2 of 5 checks failed

+

▾ Show all 5 checks

+
+
Commit message
+
✔ PR title (feat: add login page)
+
✔ Commit 1/3 (d87faca) (feat: add login page)
+
✖ Commit 2/3 (5584f46) (1 failure)
+
CC001 message
+
value: bad msg
+
Suggest: Use <type>(<scope>): <description>, where <type> is one of: feat, fix, docs, style, refactor, test, chore, perf, build, ci
+
✖ Commit 3/3 (37d6def) (1 failure)
+
CC001 message
+
value: Fix: handle empty password
+
Fix: fix: handle empty password
+
Branch
+
✔ Branch (feature/add-login)
+
+
+
+
+
+
+
- Private repositories in an organization, however many of you there are. - 14-day free trial. +
+

Squash merges turn the PR title into the commit, so the title is checked too.Turn it on with pr-title: true

+
+ +
+

add login page #128Open

+

feat: add login pageSave

+

feat: add login page #128Open

+

alex wants to merge 3 commits into main from feature/add-login

+ +
+
+
+

Checks

+
+ +

Commit Check / commit-check (pull_request)CC001 message · PR title (add login page)Re-running on the edited title…✔ PR title (feat: add login page)

+
+
+
+

Squash and merge creates this commit on main:

+

feat: add login page (#128)

+
Squash and mergeSquash and mergeA required check has not passedAll checks have passed
+
+
+
+
+
+

Every commit in the pull request, checked against Conventional Commits.On by default: message: true

+
+ +
+

feat: add login page #128Open

+

alex wants to merge 3 commits into main from feature/add-login

+ +
+
+
+

Commit message

+
feat: add login paged87faca
+
bad msg5584f46
+
Fix: handle empty password37d6def
+
+
+

Annotations 2 errors

+

CC001 message

Commit 2/3 (5584f46): The commit message should follow Conventional Commits.
value: bad msg
Suggest: Use <type>(<scope>): <description>, where <type> is one of: feat, fix, docs, …

+

CC001 message

Commit 3/3 (37d6def): The commit message should follow Conventional Commits.
value: Fix: handle empty password
Fix: fix: handle empty password

+
+
+
-[:octicons-arrow-right-24: Install the App](https://github.com/apps/commit-check) +
+

Branch names checked against Conventional Branch, with the rename to run.On by default: branch: true

+
+ +
+

feat: add login page #131Open

+

alex wants to merge 1 commit into main from Feature/Add-Login

+ +
+
+
+
+

BranchFeature/Add-Login

+

CC201 The branch should follow Conventional Branch.

value: Feature/Add-Login
Fix: feature/Add-Login

+
+
+
❯ git branch -m feature/Add-Login
+
❯ commit-check --branch && echo passed
+
passed
+
+
+
+

Conventional Branch types

+
  • feature/
  • bugfix/
  • hotfix/
  • release/
  • chore/
+

main and master always pass. Add your own types in cchk.toml.

+
+
+
+
+
-Nothing is blocked while you try it. Without a config file the App reports its -findings but leaves the check run neutral, and it never rejects a push — the -only way Commit Check blocks a merge is if you make it a required check -yourself. +
+
+

.github/workflows/commit-check.yml

+
on: + pull_request: + types: [opened, synchronize, reopened, edited] +permissions: + contents: read + pull-requests: write +jobs: + commit-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - uses: commit-check/commit-check-action@v2 + with: + pr-title: true + pr-comments: true
+
+
+

Make it a required check

Branch protection does the rest: a violating commit cannot merge, however it was made.

+

Verified before it installs

The Action checks the wheel's signed build provenance and fails the step if it does not match.

+

Try it without blocking anyone

dry-run: true reports every finding as a warning and always exits 0.

+ +
+
+
+
+ +
+
+
+
+

What it checks

+

Rules grouped by what they protect.

+

A few are on by default. Turn on what your project needs, rule by rule, or set any rule to warn instead of fail.

+
+Browse all rules → +
+
    +
  • +

    Commit messagesCC001–CC012

    +

    Conventional Commits, subject length, imperative mood, sign-off, and merge, revert, fixup or WIP commits.

    +

    Rejected: Fix: add streaming support

    +

    Accepted: fix: add streaming support

    +
  • +
  • +

    Branch namesCC201–CC202

    +

    Conventional Branch naming, and whether the branch is rebased onto its target.

    +

    Rejected: my-new-thing

    +

    Accepted: feature/streaming-support

    +
  • +
  • +

    AI attributionCC013–CC016

    +

    Spots the trailers Claude Code, Cursor and others add. Forbid them, or require a disclosure instead.

    +

    Rejected: Co-authored-by: Claude

    +

    Accepted: Assisted-by: Claude

    +
  • +
  • +

    Author identityCC101–CC102

    +

    Names and emails that match the patterns you set — here author_email_pattern = "@acme\.dev$" — so a build box cannot write itself into the history.

    +

    Rejected: ec2-user <root@ip-10-0-0-12>

    +

    Accepted: Jane Doe <jane@acme.dev>

    +
  • +
  • +

    Pushes and filesCC301–CC304

    +

    No force-pushes, no oversized files, no forbidden paths — caught at pre-push, before they leave the laptop.

    +

    Rejected: git push --force origin main

    +

    Rejected: assets/model.bin, 240 MB

    +
  • +
  • +

    Across an organization

    +

    One shared policy for every repository. Each repo inherits it and overrides only what differs.

    +

    inherit_from = "github:acme/.github:cchk.toml"

    +Organization guide → +
  • +
+
+
+ +
+
+
+

Every finding

+

Not just “failed”. What failed, and the fix.

+
    +
  1. A stable rule ID that links to its documentation.
  2. +
  3. The exact value that was checked.
  4. +
  5. Why it failed, in one sentence.
  6. +
  7. A fix you can paste, whenever the correction is unambiguous.
  8. +
+
+ +
+

$ echo "Fix: add streaming support" | commit-check -m

+

1CC001 message check failed ==>

+

2Fix: add streaming support

+

3The commit message should follow Conventional Commits.

+

4Suggest: Use "fix: add streaming support"

+

Docs: https://commit-check.com/rules/#cc001

+
+
+
+ +
+
+
    +
  • 1.6M+downloads on PyPI
  • +
  • 160+repositories run the Action
  • +
  • Apacheallowlists the Action for its projects' CI
  • +
+

In the CI ofApacheTexas InstrumentsMilaIstio EcosystemOpenDriveLaband more →

+
+
+ +
+
+
+

Pricing

+

Free for open source. Team plan, soon.

+

The CLI, the hook, the Action and the MCP server are MIT licensed — nothing to buy. Plans only cover the hosted GitHub App, the one surface we run for you.

+
+ +

Nothing is blocked while you try it: without a config file the App reports its findings but leaves the check run neutral. GitHub's own commit-metadata rules sit behind its Enterprise plan — the arithmetic, and the caveats →

+
+
-GitHub can enforce some of the same policies natively, but the commit-metadata -rules sit behind its Enterprise plan. For a twenty-person team that is the -difference between $4 and $21 a seat — about $340 a month for a regular -expression, which reports a bare mismatch where Commit Check reports a rule ID, -a suggestion and a link. The Team plan here is $19 a month whatever the team -size. -[The arithmetic, and the caveats](compare/github-rules.md). +
+
-## Questions +

Questions

??? question "Does it read my source code?" @@ -376,24 +509,29 @@ size. if the hosted App ever stops, the [GitHub Action](guides/github-actions.md) reads the same config and reports the same rule IDs. -
- -## Questions, bugs, contributions - -**Start a [discussion](https://github.com/commit-check/commit-check/discussions)** -if you are weighing up a policy, are not sure whether something is a bug, or -want to know how other projects have handled it. - -**Open an [issue](https://github.com/commit-check/commit-check/issues)** when -something is broken or missing — include the output of -`commit-check --format json`, which carries the rule ID and the value that -failed. -**Send a pull request** to any of the -[repositories](https://github.com/commit-check). The engine, the Action, the App -and the MCP server are separate projects. - -[Discussions :fontawesome-brands-github:](https://github.com/commit-check/commit-check/discussions){ .md-button .md-button--primary } -[Issues :fontawesome-brands-github:](https://github.com/commit-check/commit-check/issues){ .md-button } +
+
+
+
+
+

Start with two commands.

+

No config file needed. The defaults check Conventional Commits, Conventional Branch and subject length; tighten them when you are ready.

+ +
+

$ pip install commit-check

$ commit-check --message --branch

+
+
+ + diff --git a/docs/javascripts/landing.js b/docs/javascripts/landing.js new file mode 100644 index 0000000..79da01d --- /dev/null +++ b/docs/javascripts/landing.js @@ -0,0 +1,219 @@ +/* + * Landing page demos: the commit-msg terminal in the hero and the tabbed + * GitHub Action window. + * + * Loaded on every page through extra_javascript, and a no-op on every page + * but the home page. Instant navigation swaps the page content without a + * reload, so setup runs from the theme's document$ observable on each page + * view, and whatever the previous view started is stopped first. + * + * The demos are plain HTML: index.md documents the data-* timeline + * attributes, landing.css says what each state looks like, and this file only + * moves the clock. Nothing animates under prefers-reduced-motion, or while a + * demo is scrolled out of view. + */ +(function () { + "use strict"; + + var stops = []; + + function prefersReducedMotion() { + return window.matchMedia("(prefers-reduced-motion: reduce)").matches; + } + + function each(root, selector, fn) { + Array.prototype.forEach.call(root.querySelectorAll(selector), fn); + } + + function prepare(root) { + each(root, "[data-type]", function (el) { + if (el.dataset.full === undefined) el.dataset.full = el.textContent; + }); + } + + /* Put a timeline at `t` milliseconds. */ + function render(root, t) { + each(root, "[data-on]", function (el) { + el.classList.toggle("is-on", t >= Number(el.dataset.on)); + }); + each(root, "[data-from]", function (el) { + el.classList.toggle("is-on", t >= Number(el.dataset.from)); + }); + each(root, "[data-until]", function (el) { + el.classList.toggle("is-gone", t >= Number(el.dataset.until)); + }); + each(root, "[data-hl]", function (el) { + el.classList.toggle("is-hl", t >= Number(el.dataset.hl)); + }); + each(root, "[data-type]", function (el) { + var full = el.dataset.full; + var speed = Number(el.dataset.speed || 50); + var n = Math.floor((t - Number(el.dataset.type)) / speed); + var text = full.slice(0, Math.max(0, Math.min(full.length, n))); + if (el.textContent !== text) el.textContent = text; + }); + } + + /* Back to the final frame, as if the script had never run. */ + function rest(root) { + root.classList.remove("is-live"); + each(root, "[data-type]", function (el) { + el.textContent = el.dataset.full; + }); + } + + /* Runs `onFrame(now)` on every animation frame while `el` is on screen. */ + function whileVisible(el, onShow, onFrame) { + var raf = null; + function frame(now) { + onFrame(now); + raf = requestAnimationFrame(frame); + } + var io = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting && raf === null) { + onShow(performance.now()); + raf = requestAnimationFrame(frame); + } else if (!entry.isIntersecting && raf !== null) { + cancelAnimationFrame(raf); + raf = null; + } + }); + }, { threshold: 0.25 }); + io.observe(el); + return function () { + io.disconnect(); + if (raf !== null) cancelAnimationFrame(raf); + }; + } + + /* A timeline that plays on a loop: the hero terminal. */ + function setupLoop(root) { + var loop = Number(root.dataset.loop); + var start = 0; + prepare(root); + root.classList.add("is-live"); + render(root, 0); + stops.push(whileVisible(root, function (now) { + start = now; + }, function (now) { + render(root, (now - start) % loop); + })); + } + + /* The tabbed Action demo. */ + function setupDemo(demo, motion) { + var tabs = Array.prototype.slice.call(demo.querySelectorAll('[role="tab"]')); + var panels = tabs.map(function (tab) { + return document.getElementById(tab.getAttribute("aria-controls")); + }); + var cycle = Number(demo.dataset.cycle); + var current = 0; + var start = performance.now(); + // Cycles through the tabs until the reader picks one, then stays put. + var autoplay = motion; + var hovering = false; + + function select(index, focus) { + tabs.forEach(function (tab, i) { + var on = i === index; + tab.setAttribute("aria-selected", on ? "true" : "false"); + tab.tabIndex = on ? 0 : -1; + tab.style.setProperty("--p", "0"); + panels[i].hidden = !on; + rest(panels[i]); + }); + current = index; + start = performance.now(); + if (motion) { + panels[index].classList.add("is-live"); + render(panels[index], 0); + } + if (focus) tabs[index].focus(); + } + + function choose(index, focus) { + autoplay = false; + select(index, focus); + } + + tabs.forEach(function (tab, i) { + tab.addEventListener("click", function () { + choose(i, false); + }); + }); + + demo.querySelector('[role="tablist"]').addEventListener("keydown", function (event) { + var last = tabs.length - 1; + var next = { + ArrowRight: current === last ? 0 : current + 1, + ArrowLeft: current === 0 ? last : current - 1, + Home: 0, + End: last + }[event.key]; + if (next === undefined) return; + event.preventDefault(); + choose(next, true); + }); + + demo.addEventListener("pointerenter", function () { hovering = true; }); + demo.addEventListener("pointerleave", function () { hovering = false; }); + demo.addEventListener("focusin", function () { hovering = true; }); + demo.addEventListener("focusout", function () { hovering = false; }); + + panels.forEach(prepare); + demo.classList.add("is-ready"); + select(0, false); + + if (!motion) return; + + stops.push(whileVisible(demo, function (now) { + start = now; + }, function (now) { + var t = now - start; + render(panels[current], t); + if (!autoplay) return; + tabs[current].style.setProperty("--p", String(Math.min(1, t / cycle))); + if (t >= cycle && !hovering) select((current + 1) % tabs.length, false); + })); + } + + function setupCopy(button) { + button.addEventListener("click", function () { + var source = button.dataset.copyFrom + ? document.getElementById(button.dataset.copyFrom).textContent + : button.dataset.copy; + if (!navigator.clipboard) return; + navigator.clipboard.writeText(source).then(function () { + var label = button.getAttribute("aria-label"); + var text = button.textContent; + button.classList.add("is-done"); + if (label) button.setAttribute("aria-label", "Copied"); + if (!label) button.textContent = "Copied"; + setTimeout(function () { + button.classList.remove("is-done"); + if (label) button.setAttribute("aria-label", label); + if (!label) button.textContent = text; + }, 1600); + }); + }); + } + + function init() { + stops.forEach(function (stop) { stop(); }); + stops = []; + + var motion = !prefersReducedMotion() && "IntersectionObserver" in window; + if (motion) each(document, ".cc-term .cc-tl[data-loop]", setupLoop); + each(document, ".cc-demo", function (demo) { setupDemo(demo, motion); }); + each(document, ".cc-copy", setupCopy); + } + + if (typeof document$ !== "undefined") { + document$.subscribe(init); + } else if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})(); diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 4451dce..4b6289a 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -12,6 +12,9 @@ {% endblock %} {% block extrahead %} + {# The display face for headings and the site name (--cc-display-font in + assets/extra.css). The theme loads the text and code faces itself. #} + {% endblock %} diff --git a/docs/static/apple-touch-icon.png b/docs/static/apple-touch-icon.png index a499f93..744be14 100644 Binary files a/docs/static/apple-touch-icon.png and b/docs/static/apple-touch-icon.png differ diff --git a/docs/static/favicon.ico b/docs/static/favicon.ico index ed1872b..6727ead 100644 Binary files a/docs/static/favicon.ico and b/docs/static/favicon.ico differ diff --git a/docs/stylesheets/landing.css b/docs/stylesheets/landing.css index 2cfa79e..95cb5ef 100644 --- a/docs/stylesheets/landing.css +++ b/docs/stylesheets/landing.css @@ -2,23 +2,66 @@ * Landing page only. * * This file is linked from docs/overrides/landing.html rather than listed in - * mkdocs.yml's extra_css, so the twenty-odd documentation pages never load it. - * Instant navigation diffs between documents, so the link is added when - * a reader arrives here and removed when they leave -- which is also why rules - * below may safely reach outside the content area, to .md-header. A body class - * could not do that: instant navigation does not touch . + * mkdocs.yml's extra_css, so the documentation pages never load it. Instant + * navigation diffs between documents, so the link is added when a + * reader arrives here and removed when they leave -- which is also why rules + * below may safely reach outside the content area, to .md-header and + * .md-main. A body class could not do that: instant navigation does not + * touch . + * + * Sizes are in px, not the theme's rem: the theme scales its root font size + * up on wide screens, and the landing page is laid out to fixed measures + * (the demo window, the five-column row) that should not grow with it. + * + * The brand tokens are Ink, Signal Blue and Paper; see the brand kit. The + * demo window is drawn light in both colour schemes on purpose -- it stands + * for a page on github.com, which the reader sees in its own theme. */ -/* ---------------------------------------------------------------- header -- */ +:root { + --cc-ink: #0b1620; + --cc-ink-2: #13212c; + --cc-ink-3: #0f1d28; + --cc-ink-line: #22323f; + --cc-on-ink: #e8eef2; + --cc-on-ink-2: #b9c7d1; + --cc-on-ink-3: #8fa3b1; + --cc-blue: #2c9ccd; + --cc-blue-soft: #7fc6e6; + --cc-blue-deep: #176b89; + --cc-tint: #e3f2f9; + + --cc-paper: #f4f6f7; + --cc-card: #ffffff; + --cc-line: #dce3e8; + --cc-text: #0e1b24; + --cc-muted: #4f6270; + + --cc-pass: #1a7f4b; + --cc-fail: #c8322b; + --cc-wait: #9a6a00; -/* Marc Lou's first rule for a landing page: the brand is small and out of the - way, because nobody arrives caring about it. The stock header already puts - the logo and the name top left; the search box is what competes with the - headline, and a reader who wants search is one click into the docs. */ -.md-header__title .md-header__topic:first-child { - font-size: 0.9rem; + /* Literal stacks: the theme defines its font variables below :root, so a + var() of them here would resolve to nothing. */ + --cc-display: "Bricolage Grotesque", "Instrument Sans", -apple-system, "Segoe UI", sans-serif; + --cc-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } +[data-md-color-scheme="slate"] { + --cc-paper: #0e1a23; + --cc-card: #13212c; + --cc-line: #263744; + --cc-text: #e8eef2; + --cc-muted: #a3b4c0; + --cc-tint: #16303e; + --cc-blue-deep: #7fc6e6; +} + +/* ---------------------------------------------------------------- chrome -- */ + +/* The brand is small and out of the way in the header: nobody arrives caring + about it. The search box is what competes with the headline, and a reader + who wants search is one click into the docs. */ .md-header .md-search { display: none; } @@ -29,250 +72,2464 @@ } } +/* The hero is a dark band that meets the header, so the theme's gap above the + content, and the spacer it draws inside it, go. */ +.md-main__inner { + margin-top: 0; +} + +.cc-landing .md-content__inner { + padding-top: 0; + margin-bottom: 0; +} + +.cc-landing .md-content__inner::before { + display: none; +} + +/* The "edit this page" pencil points at index.md, which is not what a + first-time reader wants to do next. */ +.cc-landing .md-content__button { + display: none; +} + +.md-main { + background: var(--cc-paper); + /* The bands below span the window with `margin-inline: calc(50% - 50vw)`. + 100vw includes a classic scrollbar, so they can end a few pixels wide; + `clip` rather than `hidden`, because `hidden` would make .md-main a + scroll container and break the sticky header and in-page anchors. */ + overflow-x: clip; +} + +/* ---------------------------------------------------------------- resets -- */ + +/* The theme styles bare elements inside .md-typeset. These resets use + :where() so they stay no stronger than the theme's own rules and every + component class below still wins. The FAQ band is left alone: it is + ordinary Markdown and should look like the rest of the docs. */ +.md-typeset :where(.cc-band:not(.cc-faq)) :is(p, ul, ol, figure, h1, h2, h3, table) { + margin: 0; +} + +/* The theme indents lists with [dir=ltr]-prefixed rules, hence [dir] here. */ +[dir] .md-typeset :where(.cc-band:not(.cc-faq)) :is(ul, ol) { + margin: 0; + padding: 0; + list-style: none; +} + +[dir] .md-typeset :where(.cc-band:not(.cc-faq)) :is(ul, ol) > li { + margin: 0; +} + +.md-typeset :where(.cc-band:not(.cc-faq)) code { + padding: 0; + background: none; + border-radius: 0; + box-shadow: none; + color: inherit; + font-size: inherit; + word-break: normal; +} + +.md-typeset :where(.cc-band) svg { + max-width: none; +} + +.cc-sr { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} + +/* ----------------------------------------------------------------- bands -- */ + +/* Each band spans the window while its text stays in the content column: + the negative margin pulls the edges out, the padding puts the text back. + Percentages, not viewport units, for the margin: .md-content is a flex + item, and a `-100vw` margin counts towards its min-content width, which + once stretched the column to 2754px on a 1440px screen. */ +.md-typeset .cc-band { + position: relative; + margin-inline: calc(50% - 50vw); + padding: 104px calc(50vw - 50%); + color: var(--cc-text); + font-size: 16px; + line-height: 1.55; +} + +.md-typeset .cc-band--ink { + background: var(--cc-ink); + color: var(--cc-on-ink); +} + +.md-typeset .cc-band--white { + background: var(--cc-card); +} + +.cc-wrap { + position: relative; + max-width: 1200px; + margin-inline: auto; +} + +/* ------------------------------------------------------------ type scale -- */ + +.md-typeset .cc-display { + font-family: var(--cc-display); + font-size: clamp(42px, 6.2vw, 70px); + line-height: 1; + font-weight: 800; + letter-spacing: -0.035em; + color: #ffffff; + text-wrap: balance; +} + +.md-typeset .cc-accent { + color: var(--cc-blue); +} + +.md-typeset .cc-h2 { + font-family: var(--cc-display); + font-size: clamp(32px, 4.2vw, 50px); + line-height: 1.05; + font-weight: 800; + letter-spacing: -0.03em; + color: inherit; + text-wrap: balance; +} + +.md-typeset .cc-h2--xl { + font-size: clamp(36px, 4.6vw, 54px); +} + +.md-typeset .cc-eyebrow { + font-family: var(--cc-mono); + font-size: 13px; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--cc-blue-deep); +} + +.md-typeset .cc-band--ink .cc-eyebrow { + color: var(--cc-blue-soft); +} + +.md-typeset .cc-sub { + font-size: 18px; + line-height: 1.55; + color: var(--cc-muted); + text-wrap: pretty; +} + +.md-typeset .cc-band--ink .cc-sub { + color: var(--cc-on-ink-2); +} + +.md-typeset .cc-sub code, +.md-typeset .cc-lede code { + font-family: var(--cc-mono); + font-size: 0.88em; +} + +.cc-head { + display: flex; + flex-direction: column; + gap: 16px; + max-width: 760px; +} + +.cc-head--center { + align-items: center; + margin-inline: auto; + text-align: center; +} + +.cc-head--split { + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; + align-items: flex-end; + gap: 24px 40px; + max-width: none; +} + +.cc-head--split > div { + display: flex; + flex-direction: column; + gap: 16px; + max-width: 720px; +} + +.md-typeset .cc-mono { + font-family: var(--cc-mono); +} + +.md-typeset .cc-b { + font-weight: 700; +} + +.md-typeset .cc-dim { + color: var(--cc-muted); +} + +/* Terminal colours, tuned for the Ink background. */ +.md-typeset .cc-t-fail { color: #ff7a6e; } +.md-typeset .cc-t-pass { color: #4fc98b; } +.md-typeset .cc-t-blue { color: var(--cc-blue-soft); } +.md-typeset .cc-t-num { color: #f2b866; } +.md-typeset .cc-t-white { color: #ffffff; } +.md-typeset .cc-t-dim { color: var(--cc-on-ink-3); } + +/* The same meanings, dark enough for the light demo window. */ +.md-typeset .cc-t-okd { color: #177245; } +.md-typeset .cc-t-bad { color: #a62a24; } +.md-typeset .cc-t-wait { color: #7a5500; } + +/* --------------------------------------------------------------- buttons -- */ + +.cc-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; +} + +.md-typeset .cc-btn { + display: inline-flex; + align-items: center; + gap: 10px; + height: 52px; + padding: 0 22px; + border-radius: 11px; + font-size: 16px; + font-weight: 600; + text-decoration: none; + transition: background-color 150ms, border-color 150ms; +} + +.md-typeset .cc-btn svg { + width: 16px; + height: 16px; +} + +/* Ink on Signal Blue is 5.9:1; white on it would be 3.1:1. */ +.md-typeset .cc-btn--primary, +.md-typeset .cc-btn--primary:hover, +.md-typeset .cc-btn--primary:focus { + background: var(--cc-blue); + color: #06131b; +} + +.md-typeset .cc-btn--primary:hover { + background: #5bb8e0; +} + +.md-typeset .cc-btn--ghost { + border: 1px solid #2a3b48; + color: var(--cc-on-ink); +} + +.md-typeset .cc-btn--ghost:hover { + background: rgba(255, 255, 255, 0.06); + color: #ffffff; +} + +.md-typeset .cc-btn--quiet { + height: 48px; + border: 1px solid var(--cc-line); + background: var(--cc-card); + color: var(--cc-text); + font-size: 15px; +} + +.md-typeset .cc-btn--quiet:hover { + border-color: var(--cc-blue); + color: var(--cc-text); +} + +.md-typeset .cc-install { + display: inline-flex; + align-items: center; + gap: 12px; + height: 52px; + box-sizing: border-box; + padding: 0 5px 0 16px; + border: 1px solid #2a3b48; + border-radius: 11px; + background: var(--cc-ink-3); + font-family: var(--cc-mono); + font-size: 14.5px; + color: var(--cc-on-ink); +} + +.cc-install__sigil { + color: var(--cc-blue); +} + +.md-typeset .cc-copy { + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border: 0; + border-radius: 8px; + background: #16252f; + color: #9db0be; + cursor: pointer; +} + +.md-typeset .cc-copy:hover, +.md-typeset .cc-copy:focus-visible { + color: #ffffff; +} + +.md-typeset .cc-copy svg { + width: 16px; + height: 16px; +} + +.md-typeset .cc-copy.is-done { + color: #4fc98b; +} + +.md-typeset .cc-copy--text { + width: auto; + height: 32px; + padding: 0 10px; + background: transparent; + font-family: "Instrument Sans", -apple-system, "Segoe UI", sans-serif; + font-size: 13px; + font-weight: 600; + color: var(--cc-blue-soft); +} + /* ------------------------------------------------------------------ hero -- */ -.cc-hero { +.md-typeset .cc-hero { + padding-top: 72px; + padding-bottom: 0; + overflow: hidden; +} + +.md-typeset .cc-hero__graph { + position: absolute; + top: 0; + right: 0; + width: 480px; + height: 100%; + pointer-events: none; +} + +.cc-hero__grid { display: grid; - gap: 2.4rem; + gap: 48px; align-items: center; - margin: 1.2rem 0 3.2rem; } -@media screen and (min-width: 60em) { - .cc-hero { - grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); - gap: 3.2rem; +@media screen and (min-width: 960px) { + .cc-hero__grid { + grid-template-columns: minmax(0, 1fr) minmax(0, 600px); + gap: 56px; } } -/* The h1 is the promise, so it gets the size budget the site name used to - spend. clamp() keeps it from wrapping to four lines on a phone. */ -.md-typeset .cc-hero__copy h1 { - margin: 0 0 0.8rem; - font-size: clamp(1.7rem, 5.2vw, 2.6rem); - font-weight: 700; - line-height: 1.15; - letter-spacing: -0.01em; - color: var(--md-default-fg-color); +.cc-hero__copy { + display: flex; + flex-direction: column; + gap: 26px; +} + +.md-typeset .cc-pill { + align-self: flex-start; + display: inline-flex; + align-items: center; + gap: 8px; + height: 30px; + padding: 0 12px; + border: 1px solid #22404f; + border-radius: 999px; + background: #102836; + color: var(--cc-blue-soft); + font-size: 13.5px; + font-weight: 500; +} + +.cc-pill__dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--cc-blue); +} + +.md-typeset .cc-lede { + font-size: 19px; + line-height: 1.55; + color: var(--cc-on-ink-2); + text-wrap: pretty; } -.md-typeset .cc-hero__copy > p:first-of-type { - font-size: 0.82rem; - line-height: 1.65; - color: var(--md-default-fg-color--light); +.md-typeset .cc-lede code { + color: var(--cc-on-ink); } -.cc-hero__copy .md-button { - margin: 0.4rem 0.4rem 0 0; +.md-typeset ul.cc-facts { + display: flex; + flex-wrap: wrap; + gap: 8px 32px; + margin-top: 64px; + padding: 22px 0 30px; + border-top: 1px solid #1a2934; + font-size: 14px; + color: var(--cc-on-ink-3); } -/* The terminal sample is the proof, so it carries the visual weight of a - screenshot without being one: real text, selectable, and it recolours with - the palette instead of burning a light theme into a PNG. */ -.md-typeset .cc-hero__demo > .highlight, -.md-typeset .cc-hero__demo pre { +/* -------------------------------------------------------------- terminal -- */ + +.md-typeset .cc-term { + display: flex; + flex-direction: column; + width: 100%; + max-width: none; margin: 0; + border-radius: 14px; + overflow: hidden; + background: var(--cc-ink-3); + box-shadow: 0 30px 70px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.07); + color: #d6e1e8; + font-family: var(--cc-mono); + text-align: left; +} + +.cc-term__bar { + display: flex; + align-items: center; + gap: 14px; + height: 38px; + padding: 0 16px; + background: var(--cc-ink-2); + border-bottom: 1px solid var(--cc-ink-line); + font-size: 12px; + color: var(--cc-on-ink-3); + white-space: nowrap; + overflow: hidden; +} + +.cc-dots { + display: flex; + gap: 7px; } -.md-typeset .cc-hero__demo > .highlight { - border: 1px solid var(--md-default-fg-color--lightest); - border-radius: 0.4rem; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); +.cc-dots i { + width: 11px; + height: 11px; + border-radius: 50%; + background: #2a3b48; } -/* The transcript is real output, so its longest line is 93 characters and - cannot be shortened without making it a fake. In the hero it has to be - readable at a glance rather than scrolled to, so it soft-wraps -- which is - what an 80-column terminal does with that line anyway. The source in - index.md is untouched, so tests/docs_sync_test.py still checks the rule - name against the installed package. +.cc-term__body { + padding: 18px 20px 22px; + font-size: 13px; + line-height: 21px; +} - Not `overflow: hidden` on the wrapper: an earlier version did that and - silently cut the line off at the border. */ -.md-typeset .cc-hero__demo pre > code { - font-size: 0.65rem; +.cc-line { + min-height: 21px; white-space: pre-wrap; overflow-wrap: anywhere; } -.md-typeset .cc-hero__demo > p { - margin: 0.8rem 0 0; - font-size: 0.72rem; - color: var(--md-default-fg-color--light); +.cc-gap { + margin-top: 21px; +} + +.cc-row { + display: flex; + gap: 2px; + white-space: nowrap; +} + +.cc-leader { + flex: 1; + min-width: 24px; + overflow: hidden; } -/* ------------------------------------------------------------ full bleed -- */ +.cc-leader::before { + content: "................................................................................"; + color: var(--cc-on-ink-3); +} -/* .cc-proof and .cc-community are tinted bands that span the whole window - while their text stays in the content column. They do it with the pair - below, and the units matter more than they look: +.cc-sigil { + color: var(--cc-blue); +} - margin-inline: calc(50% - 50vw) pull each edge out to the window - padding-inline: calc(50vw - 50% + gutter) put the text back +.cc-caret { + display: inline-block; + width: 8px; + height: 16px; + margin-left: 1px; + vertical-align: -3px; + background: var(--cc-blue); + animation: cc-blink 1s steps(1) infinite; +} - The percentages are load-bearing. `.md-content` is a flex item, so its - min-content width is the widest thing inside it; a viewport-unit margin - like `-100vw` counts towards that and stretched the column to 2754px on a - 1440px viewport. The document itself did not scroll -- the width went into - the column, which then rendered its own content off the right edge. A - percentage margin resolves against the containing block and contributes - nothing to intrinsic width, so the column stays viewport-sized. +.cc-caret--ink { + width: 2px; + height: 22px; + vertical-align: -4px; + background: #0e1b24; +} - `overflow-x: clip` is the belt to that pair of braces: 100vw includes the - classic scrollbar, so the bands can still end a few pixels wide of the - viewport. `clip` rather than `hidden` because `hidden` would make .md-main - a scroll container and break the sticky header and in-page anchors. */ -.md-main { - overflow-x: clip; +@keyframes cc-blink { + 50% { opacity: 0; } } -/* ---------------------------------------------------- social proof strip -- */ +/* ------------------------------------------------------ timeline states -- */ -.cc-proof { - margin: 0 calc(50% - 50vw) 3.2rem; - padding: 2rem calc(50vw - 50% + 0.8rem); - background: var(--md-code-bg-color); - text-align: center; +/* See the comment at the top of index.md. A timeline (.cc-tl) is `is-live` + only while the script plays it; otherwise it shows its final frame. */ +.cc-tl:not(.is-live) [data-until], +.cc-tl.is-live [data-until].is-gone, +.cc-tl.is-live [data-from]:not(.is-on) { + display: none; +} + +.cc-tl [data-on] { + transition: opacity 0.45s ease, transform 0.45s ease; +} + +.cc-tl.is-live [data-on]:not(.is-on) { + opacity: 0; + transform: translateY(6px); +} + +.cc-tl.is-live .cc-flash.is-on { + animation: cc-flash 1.4s ease; +} + +@keyframes cc-flash { + from { background: var(--cc-tint); } + to { background: transparent; } +} + +/* ------------------------------------------------------ where it runs -- */ + +.md-typeset .cc-config { + width: min(380px, 100%); + margin: 52px auto 0; + border-radius: 14px; + overflow: hidden; + background: var(--cc-ink); + color: #d6e1e8; + box-shadow: 0 16px 40px rgba(11, 22, 32, 0.18); +} + +.md-typeset .cc-config__bar { + display: flex; + align-items: center; + gap: 8px; + height: 34px; + padding: 0 14px; + background: var(--cc-ink-2); + font-family: var(--cc-mono); + font-size: 12.5px; + color: #9db0be; } -.md-typeset .cc-proof > p { - margin: 0 auto 1.6rem; - max-width: 34rem; - font-size: 0.72rem; - color: var(--md-default-fg-color--light); +.cc-config__bar svg { + width: 14px; + height: 14px; + color: var(--cc-blue); } -.cc-proof .logo-grid { +.cc-config__body { + padding: 14px 18px 16px; + font-family: var(--cc-mono); + font-size: 13px; + line-height: 21px; + white-space: pre; +} + +.md-typeset .cc-fan { + display: none; + width: 100%; + height: 64px; + color: #9fc9dc; +} + +.cc-fan path { + stroke: currentColor; + vector-effect: non-scaling-stroke; +} + +.cc-fan .cc-fan__main { + stroke: var(--cc-blue); + stroke-width: 2.4; +} + +.md-typeset ul.cc-surfaces { display: grid; - grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); - gap: 1.2rem 0.8rem; - max-width: 52rem; - margin: 0 auto; + grid-template-columns: repeat(auto-fit, minmax(208px, 1fr)); + gap: 16px; + margin-top: 40px; +} + +@media screen and (min-width: 1100px) { + .md-typeset .cc-fan { + display: block; + } + + .md-typeset ul.cc-surfaces { + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 20px; + margin-top: 0; + } } -.cc-proof .logo-item { +.md-typeset .cc-surface { display: flex; flex-direction: column; + gap: 12px; + height: 100%; + box-sizing: border-box; + padding: 20px; + border: 1px solid var(--cc-line); + border-radius: 14px; + background: var(--cc-card); + color: var(--cc-text); + transition: border-color 150ms, box-shadow 150ms, transform 150ms; +} + +.md-typeset .cc-surface:hover, +.md-typeset .cc-surface:focus-visible { + border-color: var(--cc-blue); + color: var(--cc-text); + transform: translateY(-2px); +} + +.md-typeset .cc-surface--main { + padding: 19px; + border: 2px solid var(--cc-blue); + box-shadow: 0 12px 30px rgba(44, 156, 205, 0.16); +} + +.cc-surface__icon { + display: flex; align-items: center; - gap: 0.4rem; + justify-content: center; + width: 42px; + height: 42px; + border-radius: 11px; + background: var(--cc-ink); + color: var(--cc-blue); } -.cc-proof .logo-item img { - width: 44px; - height: 44px; - border-radius: 50%; - filter: grayscale(1); - opacity: 0.7; - transition: filter 125ms, opacity 125ms; +.cc-surface--main .cc-surface__icon { + background: var(--cc-blue); + color: #06131b; } -.cc-proof .logo-item:hover img { - filter: grayscale(0); - opacity: 1; +.cc-surface__icon svg { + width: 22px; + height: 22px; } -.cc-proof .logo-item span { - font-size: 0.6rem; +/* Sits on the card's top edge, like a tab, so it never crowds the icon. */ +.cc-surface__flag { + position: absolute; + top: -12px; + left: 50%; + transform: translateX(-50%); + white-space: nowrap; + padding: 4px 8px; + border-radius: 999px; + background: var(--cc-tint); + color: var(--cc-blue-deep); + font-size: 11.5px; + font-weight: 700; line-height: 1.3; - color: var(--md-default-fg-color--light); } -/* ----------------------------------------------------------------- cards -- */ - -/* `.md-typeset ul:not([hidden])` in the theme sets `display: flow-root`, and - that `:not()` counts as an attribute selector -- so a plain `.cc-cards > ul` - loses the cascade and every card stacks full width. Matching the theme's - shape wins on specificity instead of relying on load order. */ -.md-typeset .cc-cards > ul:not([hidden]), -.md-typeset .cc-pricing > ul:not([hidden]) { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); - gap: 0.8rem; - padding: 0; - margin: 1.2rem 0 2rem; - list-style: none; +.md-typeset .cc-surface--main { + position: relative; } -.md-typeset .cc-cards > ul > li, -.md-typeset .cc-pricing > ul > li { - margin: 0; - padding: 1rem 1.1rem; - border: 1px solid var(--md-default-fg-color--lightest); - border-radius: 0.4rem; - transition: border-color 125ms, box-shadow 125ms; +.cc-surface__kicker { + font-size: 12.5px; + font-weight: 600; + color: var(--cc-muted); } -.md-typeset .cc-cards > ul > li:hover, -.md-typeset .cc-pricing > ul > li:hover { - border-color: var(--cc-brand, #2c9ccd); - box-shadow: 0 0 0 1px var(--cc-brand, #2c9ccd); +.cc-surface__name { + font-family: var(--cc-display); + font-size: 22px; + font-weight: 700; + letter-spacing: -0.015em; + line-height: 1.15; } -.md-typeset .cc-cards > ul > li > hr, -.md-typeset .cc-pricing > ul > li > hr { - margin: 0.6rem 0; - border-color: var(--md-default-fg-color--lightest); +.cc-surface__desc { + flex-grow: 1; + font-size: 14.5px; + line-height: 1.5; + color: var(--cc-muted); } -.md-typeset .cc-cards > ul > li > p, -.md-typeset .cc-pricing > ul > li > p { - font-size: 0.72rem; - line-height: 1.6; +.md-typeset .cc-surface__code { + padding: 8px; + border-radius: 7px; + background: var(--cc-paper); + font-family: var(--cc-mono); + font-size: 11.5px; + overflow-wrap: anywhere; } -/* ---------------------------------------------------------- screenshots -- */ +.cc-surface__go { + font-size: 14px; + font-weight: 600; + color: var(--cc-blue-deep); +} -.md-typeset .cc-shot { - margin: 1.6rem 0 2rem; +.md-typeset .cc-more-link { + margin-top: 28px; text-align: center; + font-size: 15px; + font-weight: 600; } -.md-typeset .cc-shot img { - max-width: 100%; - height: auto; - border: 1px solid var(--md-default-fg-color--lightest); - border-radius: 0.4rem; +/* On a phone the five cards become a list of rows: the icon, the name and + what it is for. The details are one tap away on the guide. */ +@media screen and (max-width: 599px) { + .md-typeset ul.cc-surfaces { + grid-template-columns: minmax(0, 1fr); + gap: 10px; + margin-top: 28px; + } + + .md-typeset .cc-surface { + display: grid; + grid-template-columns: 40px minmax(0, 1fr); + grid-template-areas: "icon name" "icon kicker"; + column-gap: 14px; + row-gap: 2px; + align-items: center; + padding: 14px 16px; + } + + .md-typeset .cc-surface--main { + padding: 13px 15px; + } + + .cc-surface__icon { grid-area: icon; width: 40px; height: 40px; } + .cc-surface__name { grid-area: name; font-size: 17px; } + .cc-surface__kicker { grid-area: kicker; font-size: 13.5px; font-weight: 500; } + .cc-surface__desc, + .md-typeset .cc-surface__code, + .cc-surface__go, + .cc-surface__flag { display: none; } } -.md-typeset .cc-shot figcaption { - margin-top: 0.6rem; - font-size: 0.66rem; - color: var(--md-default-fg-color--light); +/* ----------------------------------------------------------- Action demo -- */ + +.cc-demo { + display: flex; + flex-direction: column; + align-items: center; + gap: 22px; + margin-top: 52px; } -/* The recording autoplays and cannot be paused, so a reader who has asked for - reduced motion gets the last frame of it instead. */ -.md-typeset .cc-shot .cc-still { +.cc-demo:not(.is-ready) .cc-tabs { display: none; } -@media (prefers-reduced-motion: reduce) { - .md-typeset .cc-shot .cc-motion { - display: none; - } +.cc-tabs { + display: flex; + justify-content: center; + gap: 10px; + max-width: 100%; + overflow-x: auto; + padding: 2px; + scrollbar-width: none; +} - .md-typeset .cc-shot .cc-still { - display: inline-block; - } +.md-typeset .cc-tab { + position: relative; + flex-shrink: 0; + height: 44px; + padding: 0 18px; + overflow: hidden; + border: 1px solid #2a3b48; + border-radius: 999px; + background: transparent; + color: var(--cc-on-ink-2); + font-family: var(--cc-mono); + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: background-color 250ms, color 250ms, border-color 250ms; } -/* ------------------------------------------------------------- community -- */ +.md-typeset .cc-tab:hover { + border-color: var(--cc-blue); + color: #ffffff; +} -.cc-community { - margin: 3.2rem calc(50% - 50vw) 0; - padding: 2rem calc(50vw - 50% + 0.8rem) 2.4rem; - background: var(--md-code-bg-color); +.md-typeset .cc-tab[aria-selected="true"] { + border-color: var(--cc-blue); + background: var(--cc-blue); + color: #06131b; } -.md-typeset .cc-community h2 { - margin-top: 0; +.cc-tab__bar { + position: absolute; + left: 0; + bottom: 0; + height: 3px; + width: calc(var(--p, 0) * 100%); + background: rgba(6, 19, 27, 0.45); } -/* ------------------------------------------------------------------ chrome -- */ +.cc-panel { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + width: 100%; +} -/* The "edit this page" pencil belongs on a documentation page. Here it is the - only control in the hero and it points at index.md, which is not what a - first-time reader wants to do next. */ -.cc-landing .md-content__button { +.cc-panel[hidden] { display: none; } + +/* Without the script every panel is listed, one under the other. */ +.cc-demo:not(.is-ready) .cc-panel + .cc-panel { + margin-top: 48px; +} + +.cc-panel:focus-visible { + outline: 2px solid var(--cc-blue); + outline-offset: 6px; + border-radius: 14px; +} + +.md-typeset .cc-caption { + display: flex; + flex-direction: column; + gap: 6px; + text-align: center; + font-size: 14px; + color: #9db0be; +} + +.md-typeset .cc-caption strong { + font-size: 19px; + font-weight: 600; + color: #f2f6f8; + text-wrap: balance; +} + +.md-typeset .cc-caption code { + padding: 2px 7px; + border-radius: 5px; + background: var(--cc-ink-2); + color: var(--cc-blue-soft); + font-family: var(--cc-mono); + font-size: 13px; +} + +/* The window: a page on github.com, drawn light in either scheme. */ +.cc-window { + --w-text: #0e1b24; + --w-muted: #4f6270; + --w-line: #dce3e8; + --w-soft: #f4f6f7; + display: flex; + flex-direction: column; + width: 100%; + max-width: 1000px; + border-radius: 14px; + overflow: hidden; + background: #ffffff; + color: var(--w-text); + font-size: 14px; + line-height: 1.5; + text-align: left; + box-shadow: 0 30px 80px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.06); +} + +.cc-demo.is-ready .cc-window { + min-height: 600px; +} + +.cc-window .cc-dim { + color: var(--w-muted); +} + +.cc-window__bar { + display: flex; + align-items: center; + gap: 14px; + height: 36px; + flex-shrink: 0; + padding: 0 16px; + background: #eef2f4; + border-bottom: 1px solid var(--w-line); +} + +.cc-window__bar .cc-dots i { + background: #d5dce1; +} + +.cc-url { + flex-grow: 1; + height: 22px; + padding: 0 10px; + border-radius: 6px; + background: #ffffff; + font-family: var(--cc-mono); + font-size: 12px; + line-height: 22px; + color: #5b6d7a; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.cc-pr { + display: flex; + flex-direction: column; + gap: 8px; + padding: 18px 28px 0; + border-bottom: 1px solid var(--w-line); +} + +.md-typeset .cc-pr__title { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 4px 10px; + min-height: 40px; + font-size: 24px; + font-weight: 600; + letter-spacing: -0.01em; + line-height: 1.25; +} + +.cc-pr__num { + font-weight: 400; + color: #6b7c88; +} + +.cc-open { + display: inline-flex; + align-items: center; + height: 26px; + padding: 0 11px; + border-radius: 999px; + background: var(--cc-pass); + color: #ffffff; + font-size: 13px; + font-weight: 600; + letter-spacing: 0; +} + +.cc-editbox { + display: flex; + align-items: center; + flex: 1; + min-width: 0; + height: 40px; + box-sizing: border-box; + padding: 0 12px; + border: 2px solid var(--cc-blue); + border-radius: 8px; + font-size: 21px; + white-space: pre; +} + +.cc-save { + display: inline-flex; + align-items: center; + height: 34px; + padding: 0 14px; + border: 1px solid #c9d3da; + border-radius: 8px; + background: var(--w-soft); + font-size: 14px; +} + +.md-typeset .cc-pr__meta { + font-size: 14px; + color: var(--w-muted); +} + +.md-typeset .cc-pr__meta b { + color: var(--w-text); +} + +.md-typeset .cc-window .cc-ref { + padding: 2px 6px; + border-radius: 5px; + background: var(--cc-tint); + color: #135a74; + font-family: var(--cc-mono); + font-size: 12.5px; +} + +.md-typeset .cc-window .cc-ref--bad { + background: #fce3e0; + color: #a62a24; +} + +.md-typeset .cc-pr__tabs { + display: flex; + gap: 22px; + margin-top: 4px; + font-size: 14px; + color: var(--w-muted); + white-space: nowrap; +} + +.cc-pr__tabs span { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 10px 2px 11px; + border-bottom: 2px solid transparent; +} + +.cc-pr__tabs .is-active { + border-bottom-color: var(--cc-blue); + color: var(--w-text); + font-weight: 600; +} + +.cc-pr__tabs i { + padding: 1px 7px; + border-radius: 999px; + background: #eef2f4; + font-size: 12px; + font-style: normal; + font-weight: 400; +} + +.cc-window__body { + flex-grow: 1; + padding: 22px 28px; + background: #f7f9fa; +} + +/* Status icons: a filled circle with a glyph, drawn in CSS so the markup + stays one empty element per icon. */ +.cc-st { + position: relative; + display: inline-block; + flex-shrink: 0; + width: 18px; + height: 18px; + border-radius: 50%; + vertical-align: -3px; +} + +.cc-st::after { + content: ""; + position: absolute; + inset: 0; + background: #ffffff; + -webkit-mask: var(--glyph) center / 12px no-repeat; + mask: var(--glyph) center / 12px no-repeat; +} + +.cc-st--pass { + background: var(--cc-pass); + --glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.4 6.3l2.3 2.3 4.9-5.2' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); +} + +.cc-st--fail { + background: var(--cc-fail); + --glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3.2 3.2l5.6 5.6M8.8 3.2l-5.6 5.6' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E"); +} + +.cc-st--wait { + border: 2px dashed var(--cc-wait); + box-sizing: border-box; +} + +.cc-st--wait::after { + display: none; +} + +/* pr-comments */ +.cc-conv { + display: flex; + flex-direction: column; + gap: 14px; +} + +.cc-comment { + display: flex; + gap: 14px; +} + +.cc-avatar { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 36px; + height: 36px; + border-radius: 50%; + background: var(--cc-ink); +} + +.cc-avatar svg { + width: 22px; + height: 22px; +} + +.cc-comment__box { + flex-grow: 1; + min-width: 0; + border: 1px solid #d3dce2; + border-radius: 10px; + background: #ffffff; + overflow: hidden; +} + +.md-typeset .cc-comment__head { + display: flex; + align-items: center; + gap: 8px; + height: 40px; + padding: 0 16px; + border-bottom: 1px solid var(--w-line); + background: #eef2f4; + font-size: 13.5px; + color: var(--w-muted); +} + +.cc-comment__head b { + color: var(--w-text); +} + +.cc-chip { + padding: 0 7px; + border: 1px solid #c9d3da; + border-radius: 999px; + font-size: 11.5px; +} + +.cc-chip--end { + margin-left: auto; +} + +.cc-comment__body { + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px 18px 14px; +} + +.cc-comment__body > div { + display: flex; + flex-direction: column; + gap: 12px; + border-radius: 8px; +} + +.md-typeset .cc-report__title { + padding-bottom: 8px; + border-bottom: 1px solid #e3e9ed; + font-size: 19px; + font-weight: 700; +} + +.md-typeset .cc-verdict { + display: flex; + align-items: center; + gap: 8px; + font-size: 15px; + font-weight: 700; +} + +.md-typeset .cc-table { + width: 100%; + border: 1px solid var(--w-line); + border-radius: 8px; + border-collapse: separate; + border-spacing: 0; + overflow: hidden; + font-size: 13.5px; +} + +.md-typeset .cc-table th, +.md-typeset .cc-table td { + padding: 9px 12px; + border-bottom: 1px solid #e8edf0; + text-align: left; + vertical-align: top; +} + +.md-typeset .cc-table th { + background: var(--w-soft); + font-weight: 600; +} + +.md-typeset .cc-table tr:last-child td { + border-bottom: 0; +} + +.md-typeset .cc-table code { + padding: 1px 5px; + border-radius: 4px; + background: #f1f4f6; + font-family: var(--cc-mono); + font-size: 12.5px; +} + +.cc-lnk { + color: var(--cc-blue-deep); +} + +.cc-window .cc-lnk { + color: #176b89; +} + +.md-typeset .cc-fold { + font-size: 13.5px; + color: var(--w-muted); +} + +.md-typeset .cc-report__foot { + font-size: 12.5px; + font-style: italic; + color: #176b89; +} + +.md-typeset .cc-event { + display: flex; + align-items: center; + gap: 10px; + margin-left: 50px; + font-size: 14px; + color: var(--w-muted); +} + +.cc-event svg { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +.md-typeset .cc-event b { + color: var(--w-text); +} + +.md-typeset .cc-event code { + font-family: var(--cc-mono); + font-size: 12.5px; +} + +.md-typeset .cc-note { + margin-left: 50px; + font-size: 13.5px; + font-weight: 600; + color: #176b89; +} + +/* job-summary */ +.cc-run { + display: flex; + padding: 0; +} + +.cc-run__side { + display: flex; + flex-direction: column; + gap: 6px; + width: 200px; + flex-shrink: 0; + box-sizing: border-box; + padding: 20px 14px; + border-right: 1px solid var(--w-line); + background: #ffffff; +} + +.cc-run__side span { + display: flex; + align-items: center; + gap: 8px; + padding: 7px 10px; + border-radius: 7px; +} + +.cc-run__side .is-active { + background: #eef2f4; + font-weight: 600; +} + +.cc-run__side .cc-run__label { + padding-top: 14px; + font-size: 12px; + font-weight: 600; + color: #5b6d7a; +} + +.cc-run__main { + display: flex; + flex-direction: column; + gap: 14px; + flex-grow: 1; + min-width: 0; + padding: 20px 24px; +} + +.cc-run__meta { + display: flex; + flex-wrap: wrap; + gap: 8px 36px; + padding: 12px 16px; + border: 1px solid var(--w-line); + border-radius: 10px; + background: #ffffff; +} + +.md-typeset .cc-run__meta p { + display: flex; + flex-direction: column; + font-size: 13px; + color: #5b6d7a; +} + +.cc-run__meta b { + font-size: 14px; + color: var(--w-text); +} + +.cc-card { + border: 1px solid var(--w-line); + border-radius: 10px; + background: #ffffff; + overflow: hidden; +} + +.md-typeset .cc-card__head { + display: flex; + justify-content: space-between; + padding: 10px 16px; + border-bottom: 1px solid #e3e9ed; + font-size: 14px; + font-weight: 600; +} + +.cc-card__head .cc-dim { + font-weight: 400; +} + +.cc-card__body { + display: flex; + flex-direction: column; + gap: 10px; + padding: 14px 18px; +} + +.cc-card--pad { + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px; +} + +.cc-tree { + padding: 12px 14px; + border-radius: 8px; + background: var(--w-soft); + font-family: var(--cc-mono); + font-size: 12.5px; + line-height: 19px; +} + +.cc-tree > div { + min-height: 19px; + white-space: pre; +} + +/* The one long line, a Suggest: that lists every commit type, wraps with a + hanging indent under its own text instead of scrolling the box. */ +.cc-tree > .cc-hang { + white-space: pre-wrap; + padding-left: 8ch; + text-indent: -8ch; +} + +/* pr-title, message, branch */ +.cc-stack { + display: flex; + flex-direction: column; + gap: 16px; +} + +.cc-check { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; +} + +.md-typeset .cc-check p { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.cc-check .cc-mono { + font-size: 13px; +} + +.md-typeset .cc-squash__label { + font-size: 14px; + color: var(--w-muted); +} + +.md-typeset .cc-squash__label code { + font-family: var(--cc-mono); + font-size: 12.5px; +} + +.md-typeset .cc-squash { + padding: 12px 14px; + border-radius: 8px; + background: var(--w-soft); + font-family: var(--cc-mono); + font-size: 14px; + white-space: pre-wrap; +} + +.cc-merge { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 14px; + font-size: 13px; +} + +.cc-mergebtn { + display: inline-flex; + align-items: center; + height: 38px; + padding: 0 16px; + border-radius: 8px; + background: #e4e9ed; + color: #6b7c88; + font-size: 14px; + font-weight: 600; +} + +.cc-mergebtn--ready { + background: var(--cc-pass); + color: #ffffff; +} + +.cc-commit { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + border-bottom: 1px solid #eef2f4; + font-size: 15px; + font-weight: 600; +} + +.cc-commit:last-child { + border-bottom: 0; +} + +.cc-commit span { + flex-grow: 1; + min-width: 0; +} + +.md-typeset .cc-commit code { + padding: 2px 7px; + border: 1px solid var(--w-line); + border-radius: 6px; + font-family: var(--cc-mono); + font-size: 12.5px; + font-weight: 400; + color: var(--w-muted); +} + +.cc-annot { + display: flex; + flex-direction: column; + gap: 6px; + padding: 12px 16px; + border-bottom: 1px solid #eef2f4; +} + +.cc-annot:last-child { + border-bottom: 0; +} + +.md-typeset .cc-annot__title { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 700; +} + +.md-typeset .cc-annot .cc-mono, +.md-typeset .cc-finding .cc-mono { + font-size: 12.5px; + line-height: 1.6; + color: #3a2320; + overflow-wrap: anywhere; +} + +.cc-annot .cc-mono { + margin-left: 26px; +} + +.cc-split { + display: grid; + grid-template-columns: minmax(0, 1fr) 250px; + gap: 20px; + align-items: start; +} + +.md-typeset .cc-branchrow { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px; + font-size: 14px; +} + +.md-typeset .cc-branchrow code { + font-size: 15px; +} + +.cc-finding { + display: flex; + flex-direction: column; + gap: 6px; + padding: 11px 14px; + border-radius: 8px; + background: #fcefed; +} + +.md-typeset .cc-finding p:first-child { + display: flex; + align-items: center; + gap: 10px; + font-size: 13.5px; +} + +.cc-rule { + padding: 1px 7px; + border-radius: 4px; + background: var(--cc-fail); + color: #ffffff; + font-family: var(--cc-mono); + font-size: 12.5px; + font-weight: 700; +} + +.md-typeset .cc-term--inline { + padding: 14px 16px; + border-radius: 10px; + box-shadow: none; + font-size: 13.5px; + line-height: 22px; +} + +.md-typeset .cc-types { + font-size: 12.5px; +} + +.md-typeset .cc-types__head { + font-size: 13px; + font-weight: 600; + color: #5b6d7a; +} + +.md-typeset .cc-types ul { + display: flex; + flex-direction: column; + gap: 8px; +} + +.md-typeset .cc-types li { + padding: 7px 10px; + border-radius: 7px; + background: var(--w-soft); + font-family: var(--cc-mono); + font-size: 14px; + color: #33444f; + transition: background-color 400ms, color 400ms; +} + +.md-typeset .cc-tl:not(.is-live) .cc-types li[data-hl], +.md-typeset .cc-types li.is-hl { + background: #ddf3e6; + color: #146b3e; + font-weight: 700; +} + +.md-typeset .cc-types code { + font-family: var(--cc-mono); +} + +/* Below the demo: the workflow and what makes it worth running. */ +.cc-action__more { + display: grid; + gap: 24px; + width: 100%; + max-width: 1080px; + margin: 56px auto 0; +} + +@media screen and (min-width: 900px) { + .cc-action__more { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +.cc-yaml { + border: 1px solid #1f2f3b; + border-radius: 14px; + background: var(--cc-ink-3); + overflow: hidden; +} + +.md-typeset .cc-yaml__bar { + display: flex; + align-items: center; + justify-content: space-between; + height: 40px; + padding: 0 8px 0 16px; + background: var(--cc-ink-2); + font-family: var(--cc-mono); + font-size: 12.5px; + color: #9db0be; +} + +.cc-yaml__body { + padding: 16px 18px 18px; + overflow-x: auto; + font-family: var(--cc-mono); + font-size: 13px; + line-height: 21px; + color: #d6e1e8; + white-space: pre; +} + +.cc-yaml__body b { + color: #ffffff; +} + +.cc-points { + display: flex; + flex-direction: column; + gap: 14px; +} + +.cc-point { + display: flex; + flex-direction: column; + gap: 6px; + padding: 20px 22px; + border: 1px solid #1f2f3b; + border-radius: 14px; + font-size: 14.5px; + line-height: 1.5; + color: #9db0be; +} + +.md-typeset .cc-point__title { + font-size: 16px; + font-weight: 600; + color: #ffffff; +} + +.md-typeset .cc-point code { + font-family: var(--cc-mono); + font-size: 13px; + color: #d6e1e8; +} + +.md-typeset .cc-point__links { + display: flex; + flex-wrap: wrap; + gap: 8px 24px; + padding: 6px 4px 0; + font-size: 15px; + font-weight: 600; +} + +.md-typeset .cc-band--ink a:not(.cc-btn) { + color: var(--cc-blue-soft); +} + +.md-typeset .cc-band--ink a:not(.cc-btn):hover { + color: #ffffff; +} + +/* ---------------------------------------------------------------- rules -- */ + +.md-typeset ul.cc-groups { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 24px; + margin-top: 52px; +} + +@media screen and (min-width: 1000px) { + .md-typeset ul.cc-groups { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +.md-typeset .cc-group { + display: flex; + flex-direction: column; + gap: 10px; + padding: 26px; + border: 1px solid var(--cc-line); + border-radius: 16px; + background: var(--cc-card); +} + +.md-typeset .cc-group__head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + font-family: var(--cc-display); + font-size: 23px; + font-weight: 700; + letter-spacing: -0.015em; + line-height: 1.2; +} + +.md-typeset .cc-group__head code { + flex-shrink: 0; + padding: 3px 8px; + border-radius: 6px; + background: var(--cc-tint); + color: var(--cc-blue-deep); + font-family: var(--cc-mono); + font-size: 12px; + font-weight: 500; + letter-spacing: 0; +} + +.md-typeset .cc-group__desc { + flex-grow: 1; + font-size: 14.5px; + line-height: 1.5; + color: var(--cc-muted); +} + +.md-typeset .cc-group__desc code { + font-family: var(--cc-mono); + font-size: 12.5px; + overflow-wrap: anywhere; +} + +.md-typeset .cc-ex { + display: flex; + gap: 10px; + padding: 8px 10px; + border-radius: 7px; + font-family: var(--cc-mono); + font-size: 13px; + overflow-wrap: anywhere; +} + +.md-typeset .cc-ex--bad { + background: #fcefed; + color: #8e231e; +} + +.md-typeset .cc-ex--ok { + background: #e6f4ec; + color: #146b3e; +} + +.md-typeset .cc-ex > span:first-child { + font-weight: 700; +} + +.md-typeset .cc-group--ink { + border-color: var(--cc-ink); + background: var(--cc-ink); + color: var(--cc-on-ink); +} + +.md-typeset .cc-group--ink .cc-group__desc { + color: #9db0be; +} + +.md-typeset .cc-group__code { + padding: 10px 12px; + border-radius: 8px; + background: var(--cc-ink-2); + font-family: var(--cc-mono); + font-size: 12.5px; + line-height: 1.5; + color: #d6e1e8; + overflow-wrap: anywhere; +} + +.md-typeset .cc-group__link, +.md-typeset .cc-group__link:hover { + font-size: 14px; + font-weight: 600; + color: var(--cc-blue-soft); +} + +/* -------------------------------------------------------------- anatomy -- */ + +.cc-anatomy__grid { + display: grid; + gap: 48px; + align-items: center; +} + +@media screen and (min-width: 960px) { + .cc-anatomy__grid { + grid-template-columns: minmax(0, 470px) minmax(0, 1fr); + gap: 72px; + } +} + +.cc-anatomy__grid > div:first-child { + display: flex; + flex-direction: column; + gap: 18px; +} + +.md-typeset ol.cc-steps { + display: flex; + flex-direction: column; + gap: 16px; + margin-top: 10px; + counter-reset: cc-step; +} + +.md-typeset .cc-steps li { + display: flex; + gap: 14px; + font-size: 16px; + line-height: 1.5; + color: var(--cc-muted); + counter-increment: cc-step; +} + +.md-typeset .cc-steps li::before, +.cc-mark { + content: counter(cc-step); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 28px; + height: 28px; + border-radius: 50%; + background: var(--cc-blue); + color: #06131b; + font-family: "Instrument Sans", -apple-system, "Segoe UI", sans-serif; + font-size: 14px; + font-weight: 700; +} + +.md-typeset .cc-steps b { + color: var(--cc-text); +} + +.cc-diag { + display: flex; + flex-direction: column; + padding: 30px 32px; + border-radius: 16px; + background: var(--cc-ink); + box-shadow: 0 24px 60px rgba(11, 22, 32, 0.2); + font-family: var(--cc-mono); + font-size: 14.5px; + line-height: 2.1; + color: #d6e1e8; + overflow-x: auto; +} + +.md-typeset .cc-diag p { + display: flex; + align-items: center; + gap: 12px; + white-space: nowrap; +} + +.cc-diag .cc-mark { + width: 22px; + height: 22px; + font-size: 12px; +} + +.md-typeset .cc-diag__docs { + padding-left: 34px; +} + +/* ---------------------------------------------------------------- proof -- */ + +.md-typeset .cc-proof { + padding-top: 88px; + padding-bottom: 88px; +} + +.md-typeset ul.cc-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 24px; +} + +.md-typeset .cc-stats li { + display: flex; + flex-direction: column; + gap: 6px; + padding-top: 18px; + border-top: 2px solid var(--cc-text); +} + +.cc-stats b { + font-family: var(--cc-display); + font-size: clamp(38px, 4.4vw, 52px); + font-weight: 800; + letter-spacing: -0.03em; + line-height: 1.1; +} + +.cc-stats span { + font-size: 15.5px; + color: var(--cc-muted); +} + +.md-typeset .cc-users { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 12px 40px; + margin-top: 44px; + padding-top: 32px; + border-top: 1px solid var(--cc-line); +} + +.cc-users span { + font-size: 14px; + color: var(--cc-muted); +} + +.md-typeset .cc-users a { + font-family: var(--cc-display); + font-size: 23px; + font-weight: 700; + letter-spacing: -0.015em; + color: #5b6d7a; +} + +[data-md-color-scheme="slate"] .md-typeset .cc-users a { + color: #a3b4c0; +} + +.md-typeset .cc-users a:hover { + color: var(--cc-text); +} + +.md-typeset .cc-users a:last-child { + font-family: "Instrument Sans", -apple-system, "Segoe UI", sans-serif; + font-size: 15px; + font-weight: 600; + letter-spacing: 0; + color: var(--cc-blue-deep); +} + +/* -------------------------------------------------------------- pricing -- */ + +.md-typeset ul.cc-plans { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 24px; + margin-top: 48px; +} + +.md-typeset .cc-plan { + display: flex; + flex-direction: column; + gap: 10px; + padding: 28px; + border: 1px solid var(--cc-line); + border-radius: 16px; +} + +.md-typeset .cc-plan--soon { + padding: 27px; + border: 2px dashed #9fc9dc; + background: #f5fafd; +} + +[data-md-color-scheme="slate"] .md-typeset .cc-plan--soon { + border-color: #2e5467; + background: #10222d; +} + +.md-typeset .cc-plan__name { + font-size: 16px; + font-weight: 600; +} + +.md-typeset .cc-plan__price { + font-family: var(--cc-display); + font-size: 44px; + font-weight: 800; + letter-spacing: -0.03em; + line-height: 1.15; +} + +.md-typeset .cc-plan--soon .cc-plan__price { + color: var(--cc-muted); +} + +.md-typeset .cc-plan__desc { + flex-grow: 1; + font-size: 15px; + color: var(--cc-muted); +} + +.md-typeset .cc-plan a { + font-size: 15px; + font-weight: 600; +} + +.md-typeset .cc-plans__note { + margin-top: 28px; + max-width: 820px; + font-size: 15px; + color: var(--cc-muted); +} + +/* ------------------------------------------------------------------ FAQ -- */ + +.md-typeset .cc-faq { + padding-top: 96px; + padding-bottom: 96px; +} + +.md-typeset .cc-faq .cc-wrap { + max-width: 820px; +} + +.md-typeset .cc-faq .cc-h2 { + margin: 0 0 28px; +} + +.md-typeset .cc-faq details { + margin: 0 0 10px; + border: 1px solid var(--cc-line); + border-radius: 12px; + background: var(--cc-card); + box-shadow: none; + font-size: 15px; +} + +.md-typeset .cc-faq details > summary { + padding: 14px 44px 14px 18px; + background: none; + font-size: 16px; + font-weight: 600; +} + +.md-typeset .cc-faq details > summary::before { + display: none; +} + +.md-typeset .cc-faq details > summary::after { + top: 16px; + right: 14px; +} + +.md-typeset .cc-faq details > p { + margin: 0 18px 14px; +} + +/* ------------------------------------------------------------------ CTA -- */ + +.cc-cta__grid { + display: grid; + gap: 40px; + align-items: center; +} + +@media screen and (min-width: 960px) { + .cc-cta__grid { + grid-template-columns: minmax(0, 1fr) minmax(0, 500px); + gap: 60px; + } +} + +.cc-cta__grid > div:first-child { + display: flex; + flex-direction: column; + gap: 16px; +} + +.cc-cta__grid .cc-actions { + margin-top: 10px; +} + +.md-typeset .cc-h2--xl { + color: #ffffff; +} + +.cc-cta__code { + padding: 26px 28px; + border: 1px solid #1f2f3b; + border-radius: 14px; + background: var(--cc-ink-3); + font-family: var(--cc-mono); + font-size: 16px; + line-height: 2.1; + color: var(--cc-on-ink); + overflow-x: auto; + white-space: nowrap; +} + +/* -------------------------------------------------------------- sitemap -- */ + +.md-typeset .cc-sitemap { + padding-top: 56px; + padding-bottom: 48px; + background: #081119; + border-top: 1px solid #16242f; +} + +.cc-sitemap__grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 28px 24px; + font-size: 14.5px; +} + +.cc-sitemap__grid > div { + display: flex; + flex-direction: column; + gap: 11px; +} + +.md-typeset .cc-sitemap p { + font-weight: 600; + color: #ffffff; +} + +.md-typeset .cc-sitemap a { + color: #9db0be; +} + +.md-typeset .cc-sitemap a:hover { + color: #ffffff; +} + +/* ---------------------------------------------------------- small screens -- */ + +@media screen and (max-width: 759px) { + .md-typeset .cc-band { + padding-top: 64px; + padding-bottom: 64px; + font-size: 15px; + } + + .md-typeset .cc-hero { + padding-top: 40px; + padding-bottom: 0; + } + + .md-typeset .cc-hero__graph { + display: none; + } + + .md-typeset .cc-lede { + font-size: 16.5px; + } + + .cc-actions, + .md-typeset .cc-btn, + .md-typeset .cc-install { + width: 100%; + } + + .md-typeset .cc-btn { + justify-content: center; + } + + .md-typeset .cc-install code { + flex-grow: 1; + } + + .md-typeset ul.cc-facts { + margin-top: 40px; + flex-direction: column; + } + + .cc-term__body { + padding: 14px 16px 18px; + font-size: 12px; + line-height: 19px; + } + + .cc-line { min-height: 19px; } + .cc-gap { margin-top: 19px; } + + .md-typeset .cc-sub { + font-size: 16px; + } + + .cc-head--center { + align-items: flex-start; + text-align: left; + } + + /* The demo keeps its story on a phone, with the chrome pared back: no + sidebar, no avatar, and the tab strip scrolls sideways. */ + .cc-tabs { + justify-content: flex-start; + width: calc(100% + 32px); + margin-inline: -16px; + padding-inline: 16px; + } + + .md-typeset .cc-tab { + padding: 0 15px; + font-size: 13px; + } + + .md-typeset .cc-caption { + text-align: left; + align-self: stretch; + } + + .md-typeset .cc-caption strong { + font-size: 16px; + } + + .cc-demo.is-ready .cc-window { + min-height: 640px; + } + + .cc-pr { + padding: 14px 16px 0; + } + + .md-typeset .cc-pr__title { + font-size: 18px; + min-height: 32px; + } + + .cc-editbox { + height: 34px; + font-size: 16px; + } + + .cc-pr__tabs .cc-wide, + .cc-avatar, + .cc-run__side { + display: none; + } + + .md-typeset .cc-pr__tabs { + gap: 16px; + font-size: 13px; + } + + .cc-window__body { + padding: 16px; + } + + .cc-run__main { + padding: 16px; + } + + .md-typeset .cc-event, + .md-typeset .cc-note { + margin-left: 0; + } + + /* The report table turns into one block per failing scope. */ + .md-typeset .cc-table thead { + display: none; + } + + .md-typeset .cc-table tr { + display: flex; + flex-direction: column; + padding: 8px 10px; + border-bottom: 1px solid #e8edf0; + } + + .md-typeset .cc-table tr:last-child { + border-bottom: 0; + } + + .md-typeset .cc-table td { + padding: 1px 0; + border: 0; + } + + .cc-tree { + font-size: 11.5px; + line-height: 18px; + } + + .cc-tree > div { + white-space: pre-wrap; + overflow-wrap: anywhere; + } + + .cc-split { + grid-template-columns: minmax(0, 1fr); + } + + .md-typeset .cc-types ul { + flex-direction: row; + flex-wrap: wrap; + } + + .cc-annot .cc-mono { + margin-left: 0; + } + + .cc-commit { + font-size: 14px; + padding: 11px 12px; + } + + .cc-diag { + padding: 20px; + font-size: 12.5px; + line-height: 1.9; + } + + .md-typeset .cc-diag p { + white-space: normal; + align-items: flex-start; + } + + .md-typeset .cc-diag__docs { + padding-left: 0; + overflow-wrap: anywhere; + } + + .cc-cta__code { + font-size: 13.5px; + padding: 18px 20px; + } + + .md-typeset .cc-users a { + font-size: 19px; + } + + .cc-sitemap__grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +/* ------------------------------------------------------- reduced motion -- */ + +/* The script never starts a timeline under reduced motion, so every demo + already rests on its final frame; this stops the carets and hover lifts. */ +@media (prefers-reduced-motion: reduce) { + .cc-caret { + animation: none; + } + + .md-typeset .cc-surface, + .md-typeset .cc-surface:hover { + transition: none; + transform: none; + } +} + +/* JetBrains Mono draws `==>` as one arrow. The transcripts must show what the + terminal printed, character for character. */ +.md-typeset .cc-band :is(.cc-term, .cc-diag, .cc-tree, .cc-mono, code, .cc-yaml__body, .cc-config__body, .cc-cta__code, .cc-squash, .cc-install, .cc-tab, .cc-ex, .cc-group__code) { + font-variant-ligatures: none; +} + +/* In the dark scheme the cards are already near Ink; an outline keeps the + organization card from dissolving into its neighbours. */ +[data-md-color-scheme="slate"] .md-typeset .cc-group--ink { + border-color: #22404f; +} + +/* --------------------------------------------------- hero, light scheme -- */ + +/* In the light scheme the hero is light too, so the theme toggle changes what + is on screen at the top of the page. The terminal and the install command + stay dark: they stand for a terminal, which is dark in either theme. The + Action band and the closing band stay Ink in both schemes as a stage for + the demo. */ +[data-md-color-scheme="default"] .md-typeset .cc-hero { + background: #ffffff; + color: var(--cc-text); + box-shadow: inset 0 -1px 0 var(--cc-line); +} + +[data-md-color-scheme="default"] .md-typeset .cc-hero .cc-display { + color: var(--cc-text); +} + +[data-md-color-scheme="default"] .md-typeset .cc-hero .cc-accent { + /* Signal Blue is 3.1:1 on white, enough for display type (3:1 from 24px). */ + color: var(--cc-blue); +} + +[data-md-color-scheme="default"] .md-typeset .cc-hero .cc-lede { + color: var(--cc-muted); +} + +[data-md-color-scheme="default"] .md-typeset .cc-hero .cc-lede code { + color: var(--cc-text); +} + +[data-md-color-scheme="default"] .md-typeset .cc-hero .cc-pill { + border-color: #b9dcec; + background: var(--cc-tint); + color: #135a74; +} + +[data-md-color-scheme="default"] .md-typeset .cc-hero ul.cc-facts { + border-top-color: var(--cc-line); + color: var(--cc-muted); +} + +[data-md-color-scheme="default"] .md-typeset .cc-hero .cc-install { + border-color: var(--cc-ink); + background: var(--cc-ink); +} + +[data-md-color-scheme="default"] .md-typeset .cc-hero .cc-term { + box-shadow: 0 24px 60px rgba(11, 22, 32, 0.22); +} + +/* The history-line motif's nodes are filled with the ground colour so the + lines pass behind them; on the light hero that ground is white. */ +[data-md-color-scheme="default"] .md-typeset .cc-hero__graph circle { + fill: #ffffff; +} diff --git a/mkdocs.yml b/mkdocs.yml index 0b0b75c..5c73338 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -35,14 +35,17 @@ theme: favicon: assets/favicon.svg icon: repo: fontawesome/brands/github + # Instrument Sans for text and JetBrains Mono for code; the display face, + # Bricolage Grotesque, is loaded in overrides/main.html because the theme + # only takes two families. font: - text: Inter + text: Instrument Sans code: JetBrains Mono + # Two states, not three. The first visit still follows the system through + # the media queries; after that every click visibly changes the page. A + # third "follow system" state made one click in three do nothing a reader + # could see, which read as a broken toggle. palette: - - media: "(prefers-color-scheme)" - toggle: - icon: material/brightness-auto - name: Follow system theme - media: "(prefers-color-scheme: light)" scheme: default primary: custom @@ -80,6 +83,11 @@ extra_css: - assets/extra.css - stylesheets/extra.css +# The landing page's two demos. Loaded everywhere, because instant navigation +# does not run scripts that arrive with a page; it does nothing elsewhere. +extra_javascript: + - javascripts/landing.js + extra: social: - icon: fontawesome/brands/github @@ -136,7 +144,8 @@ plugins: - social: enabled: !ENV [SOCIAL_CARDS, true] cards_layout_options: - background_color: "#2c9ccd" + background_color: "#0b1620" + color: "#ffffff" hooks: - scripts/mkdocs_hooks.py