diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index ce2669e..95449d4 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -54,11 +54,6 @@ jobs:
with:
python-version: '3.x'
- # The social plugin renders the share cards through cairo, which cairosvg
- # loads at runtime rather than bundling.
- - name: Install cairo
- run: sudo apt-get install -y --no-install-recommends libcairo2
-
- name: Build site
run: pipx run nox -s docs
diff --git a/README.md b/README.md
index a1e9001..a79efda 100644
--- a/README.md
+++ b/README.md
@@ -35,21 +35,17 @@ Or through nox, which manages the environment for you:
$ nox -s docs-live
```
-Building the share cards needs cairo on the system (`libcairo2` on Debian and
-Ubuntu, `cairo` from Homebrew on macOS). To skip them while writing:
-
-```console
-$ SOCIAL_CARDS=false mkdocs serve
-```
+The site uses [Sumi](https://sumi.mkdocs-ng.org/), the MkDocs NG theme. It
+needs no system libraries and fetches nothing at build time.
## Layout
| Path | Contents |
| --- | --- |
-| `docs/index.md` | Landing page. Renders through `docs/overrides/home.html`. |
+| `docs/index.md` | Landing page. Renders through `docs/overrides/landing.html`. |
| `docs/getting-started/`, `docs/guides/` | Tutorials and how-to guides. |
| `docs/rules.md`, `docs/configuration.md` | Reference. |
-| `docs/blog/` | Blog, published with Material's blog plugin. |
+| `docs/blog/` | Blog, published by Sumi. A post's file name, `YYYY-MM-DD-slug.md`, sets its URL. |
| `docs/overrides/`, `docs/stylesheets/`, `docs/assets/` | Theme, styles, brand assets. |
| `scripts/mkdocs_hooks.py` | Redirect stubs for the URLs the old Sphinx site served. |
diff --git a/docs/assets/extra.css b/docs/assets/extra.css
index 72aa9a9..8f882e6 100644
--- a/docs/assets/extra.css
+++ b/docs/assets/extra.css
@@ -1,10 +1,10 @@
/* Brand palette -----------------------------------------------------------
- Material's "custom" primary/accent read these variables.
+ Sumi reads every colour, font and size from its --sumi-* variables, so the
+ brand is applied by pointing those at the Commit Check palette.
- 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. */
+ Ink is the brand's dark and Signal Blue its accent. Signal Blue is the same
+ #2c9ccd the README badge has always used, so badges already in the wild
+ still match. Paper is the cool grey of the tinted bands. */
:root {
--cc-ink: #0b1620;
--cc-ink-raised: #13212c;
@@ -14,113 +14,162 @@
/* The brand blue only reaches 3.1:1 on white; links need 4.5:1. */
--cc-link: #176b89;
- --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. */
+ /* Literal stacks rather than var()s of each other, so each one can be read
+ on its own. */
--cc-display-font: "Bricolage Grotesque", "Instrument Sans", -apple-system, "Segoe UI", sans-serif;
+ --sumi-font: "Instrument Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", "PingFang SC",
+ "Microsoft YaHei", sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
+ --sumi-font-mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
+
+ --sumi-accent: var(--cc-brand);
+ --sumi-accent-ink: var(--cc-link);
+ --sumi-bg: #ffffff;
+ --sumi-bg-elev: #ffffff;
+ --sumi-bg-soft: #f4f6f7;
+ --sumi-bg-sunken: #eef2f4;
+ --sumi-fg: #0e1b24;
+ --sumi-fg-2: #4f6270;
+ --sumi-fg-3: #6a7c89;
+ --sumi-line: #dce3e8;
+ --sumi-line-2: #c9d3da;
+ --sumi-link: var(--cc-link);
+ --sumi-link-hover: #0f5570;
+ --sumi-header-bg: rgba(255, 255, 255, 0.9);
+ --sumi-code-bg: #f6f8f9;
+ --sumi-code-head: #eef2f4;
+ --sumi-code-inline: #eef2f4;
+
+ /* The page width the site has always had: a 1220px column with the header
+ aligned to it, and both sidebars at the same width. */
+ --cc-page-w: 1220px;
+ --sumi-side-w: 242px;
+ --sumi-rail-w: 242px;
+ --sumi-header-h: 48px;
}
-/* 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-footer-bg-color: #081119;
- --md-footer-bg-color--dark: #060d13;
+[data-theme="dark"] {
+ --sumi-accent: var(--cc-brand);
+ --sumi-accent-ink: var(--cc-brand-light);
+ --sumi-bg: #0e1a23;
+ --sumi-bg-elev: #13212c;
+ --sumi-bg-soft: #13212c;
+ --sumi-bg-sunken: #0b1620;
+ --sumi-fg: #e8eef2;
+ --sumi-fg-2: #b9c7d1;
+ --sumi-fg-3: #8fa3b1;
+ --sumi-line: #22323f;
+ --sumi-line-2: #2e4252;
+ --sumi-link: var(--cc-brand-light);
+ --sumi-link-hover: #7fc6e6;
+ --sumi-header-bg: rgba(11, 22, 32, 0.9);
+ --sumi-code-bg: #0b1620;
+ --sumi-code-head: #13212c;
+ --sumi-code-inline: #13212c;
}
-[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);
+/* Page frame ---------------------------------------------------------------- */
+.sumi-header-inner,
+.sumi-shell,
+.sumi-footer-inner {
+ max-width: var(--cc-page-w);
}
-[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;
+.sumi-shell {
+ column-gap: 32px;
}
-[data-md-color-scheme="default"] .md-header {
- box-shadow: inset 0 -1px 0 #dce3e8;
+/* The footer is Ink on every page and in both schemes, so the docs and the
+ landing page share one frame. */
+.sumi-footer {
+ border-top: 0;
+ background: #060d13;
}
-/* 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;
+.sumi-footer-inner {
+ color: #8fa3b1;
}
-[data-md-color-scheme="default"] .md-search__form:hover {
- background-color: #e4e9ed;
+.sumi-footer a,
+.sumi-footer .sumi-icon-btn {
+ color: #b9c7d1;
}
-/* 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);
+.sumi-footer a:hover,
+.sumi-footer .sumi-icon-btn:hover {
+ color: #ffffff;
+ background: none;
}
-.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);
+/* Text the way the site has always set it: Instrument Sans without the
+ stylistic sets Sumi turns on for its own face, at the same line height, and
+ with no lead paragraph or rule above each section. */
+body {
+ font-feature-settings: normal;
+}
+
+.sumi-prose {
+ line-height: 1.6;
+}
+
+.sumi-prose h1 + p {
+ font-size: inherit;
+ line-height: inherit;
+ color: inherit;
+}
+
+.sumi-prose h2 {
+ padding-top: 0;
+ border-top: 0;
}
/* 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 {
+ stays in the text font. */
+.sumi-prose h1,
+.sumi-prose h2,
+.sumi-brand-name {
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);
+.sumi-brand-name {
+ letter-spacing: -0.01em;
}
-.md-header__title {
- font-family: var(--cc-display-font);
- font-weight: 700;
- letter-spacing: -0.01em;
+/* Buttons -------------------------------------------------------------------
+ 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. */
+.sumi-prose .md-button--primary,
+.sumi-btn--primary {
+ background-color: var(--cc-brand);
+ border-color: var(--cc-brand);
+ color: var(--cc-ink);
+}
+
+.sumi-prose .md-button--primary:focus,
+.sumi-prose .md-button--primary:hover,
+.sumi-btn--primary:hover {
+ background-color: var(--cc-brand-light);
+ border-color: var(--cc-brand-light);
+ color: var(--cc-ink);
}
-/* Feature grid: uses Material's card grid, tightened up a little. */
-.md-typeset .grid.cards > ul > li {
- border-radius: 0.4rem;
+/* Feature grid: the card hover picks up the brand colour. */
+.sumi-prose .grid.cards > ul > li,
+.sumi-prose ul.sumi-cards > li {
transition: border-color 125ms, box-shadow 125ms;
}
-.md-typeset .grid.cards > ul > li:hover {
+.sumi-prose .grid.cards > ul > li:hover,
+.sumi-prose ul.sumi-cards > li:hover {
border-color: var(--cc-brand);
box-shadow: 0 0 0 1px var(--cc-brand);
}
-.md-typeset .grid.cards > ul > li > hr {
- margin: 0.6rem 0;
-}
-
/* Tables -------------------------------------------------------------------
Cells align to the top so short values line up with a wrapped message. */
-.md-typeset table:not([class]) td,
-.md-typeset table:not([class]) th {
+.sumi-prose table:not([class]) td,
+.sumi-prose table:not([class]) th {
vertical-align: top;
}
@@ -129,12 +178,6 @@
break across two lines even on a wide screen. Other tables hold long regexes
and allow-lists that must stay wrappable, hence the wrapper class rather
than a rule for every table. */
-.md-typeset .rules-index td code {
+.sumi-prose .rules-index td code {
white-space: nowrap;
}
-
-/* Sidebar section headings ("Getting started", "Reference", ...) */
-.md-nav__item--section > .md-nav__link {
- font-weight: 700;
- color: var(--md-default-fg-color);
-}
diff --git a/docs/assets/social.png b/docs/assets/social.png
new file mode 100644
index 0000000..02cc592
Binary files /dev/null and b/docs/assets/social.png differ
diff --git a/docs/blog/posts/ai-native-commit-check.md b/docs/blog/posts/2026-06-21-ai-native-json-output-and-a-python-api.md
similarity index 100%
rename from docs/blog/posts/ai-native-commit-check.md
rename to docs/blog/posts/2026-06-21-ai-native-json-output-and-a-python-api.md
diff --git a/docs/blog/posts/configuring-commit-check.md b/docs/blog/posts/2026-06-21-from-zero-config-to-org-wide-policy.md
similarity index 100%
rename from docs/blog/posts/configuring-commit-check.md
rename to docs/blog/posts/2026-06-21-from-zero-config-to-org-wide-policy.md
diff --git a/docs/blog/posts/introducing-commit-check.md b/docs/blog/posts/2026-06-21-one-policy-file-for-your-git-history.md
similarity index 100%
rename from docs/blog/posts/introducing-commit-check.md
rename to docs/blog/posts/2026-06-21-one-policy-file-for-your-git-history.md
diff --git a/docs/blog/posts/ai-attribution-governance.md b/docs/blog/posts/2026-07-06-ai-attribution-governance-enforcing-ai-disclosure-policies-at-the-ci-level.md
similarity index 100%
rename from docs/blog/posts/ai-attribution-governance.md
rename to docs/blog/posts/2026-07-06-ai-attribution-governance-enforcing-ai-disclosure-policies-at-the-ci-level.md
diff --git a/docs/blog/posts/ai-commit-attribution-policy.md b/docs/blog/posts/2026-10-11-ai-commit-attribution-policy.md
similarity index 100%
rename from docs/blog/posts/ai-commit-attribution-policy.md
rename to docs/blog/posts/2026-10-11-ai-commit-attribution-policy.md
diff --git a/docs/blog/posts/no-llm-co-authors.md b/docs/blog/posts/2026-10-24-no-llm-co-authors.md
similarity index 98%
rename from docs/blog/posts/no-llm-co-authors.md
rename to docs/blog/posts/2026-10-24-no-llm-co-authors.md
index adc0d37..05118d5 100644
--- a/docs/blog/posts/no-llm-co-authors.md
+++ b/docs/blog/posts/2026-10-24-no-llm-co-authors.md
@@ -128,5 +128,5 @@ day.
If your project's AI policy says something different, such as "disclose it
with `Assisted-by:`" or "no AI trailers at all",
-[this earlier post](ai-commit-attribution-policy.md) shows the setting for
+[this earlier post](2026-10-11-ai-commit-attribution-policy.md) shows the setting for
each.
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 91f4e8d..17100cb 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -127,9 +127,9 @@ repository root — or in `.github/` — to make the policy explicit:
```toml title="cchk.toml"
[commit]
conventional_commits = true
-subject_imperative = true # (1)!
+subject_imperative = true # (1)
subject_max_length = 72
-allow_wip_commits = false # (2)!
+allow_wip_commits = false # (2)
[branch]
conventional_branch = true
@@ -158,23 +158,23 @@ belongs:
-- :material-git:{ .lg .middle } __Before the commit lands__
+- :lucide-git-commit-horizontal:{ .lg .middle } __Before the commit lands__
---
A pre-commit hook rejects the message as you write it, so nothing bad
reaches the branch in the first place.
- [:octicons-arrow-right-24: Pre-commit hook](guides/integrations.md#as-a-pre-commit-hook)
+ [:lucide-arrow-right: Pre-commit hook](guides/integrations.md#as-a-pre-commit-hook)
-- :material-github:{ .lg .middle } __On every pull request__
+- :brand-github:{ .lg .middle } __On every pull request__
---
A GitHub Action checks every commit in the PR and can comment on the PR
with what needs fixing.
- [:octicons-arrow-right-24: GitHub Actions](guides/integrations.md#in-github-actions)
+ [:lucide-arrow-right: GitHub Actions](guides/integrations.md#in-github-actions)
-{% endblock %}
diff --git a/docs/overrides/landing.html b/docs/overrides/landing.html
index db57b1b..96fe3b3 100644
--- a/docs/overrides/landing.html
+++ b/docs/overrides/landing.html
@@ -1,18 +1,14 @@
{% extends "main.html" %}
{#
- The landing page takes over the content area but deliberately leaves the
- header alone.
+ The landing page is raw HTML laid out by stylesheets/landing.css, so it
+ takes over the whole area below the header: no sidebar, no table of
+ contents, no page actions. The header and footer stay Sumi's, the same as
+ on every other page.
- Instant navigation swaps a fixed list of elements between documents
- (see `bs()` in the theme bundle): announce, container, header-topic,
- outdated, logo, skip. `{% block header %}` renders *outside*
- `[data-md-component=container]`, so a header overridden here would not be
- swapped on navigation: it would follow the reader onto every docs page,
- and the docs header would follow them back here. The landing look is
- applied to the stock header from `landing.css` instead, which is added to
- and removed from `` as the reader arrives and leaves — instant
- navigation diffs the head, so that part does work.
+ The content sits inside `.md-typeset`, the class landing.css scopes its
+ rules to, as well as Sumi's own `.sumi-prose`, so Sumi's styles for the
+ plain Markdown parts (the FAQ) still apply.
#}
{% block extrahead %}
@@ -20,19 +16,17 @@
{% endblock %}
-{#
- `site_nav` is left to the theme. It holds the navigation that the header's
- menu button opens on phones; `hide: navigation` already keeps it out of the
- wide-screen layout. Emptying the block left that button with nothing to
- open.
-#}
+{% block layout %}
+
+
+{# The two animated demos. Only this page has them, and Sumi loads every page
+ in full, so the script is linked here rather than site-wide. #}
+{% block scripts %}
+ {{ super() }}
+
{% endblock %}
diff --git a/docs/overrides/main.html b/docs/overrides/main.html
index 49c6161..3d6b4ac 100644
--- a/docs/overrides/main.html
+++ b/docs/overrides/main.html
@@ -1,24 +1,25 @@
{% extends "base.html" %}
{# The landing page sets `title: Commit Check` so its social card and search
- entry are not labelled "Home". Material appends the site name to any page
- that carries a meta title, which on this one page repeats it.
-
- The site name alone says nothing to someone who has not heard of it, and
- it is what Google shows as the result's headline, so the landing page can
- give its document title separately as `seo_title`. #}
+ entry are not labelled "Home". The site name alone says nothing to someone
+ who has not heard of it, and it is what Google shows as the result's
+ headline, so the landing page can give its document title separately as
+ `seo_title`. #}
{% block htmltitle %}
- {% if page.is_homepage %}
- {{ page.meta.seo_title or config.site_name }}
+ {% if page and page.is_homepage and page.meta.seo_title %}
+ {{ page.meta.seo_title }}
{% else %}
{{ super() }}
{% endif %}
{% 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. #}
-
+ {# The brand faces: Instrument Sans for text, JetBrains Mono for code and
+ Bricolage Grotesque for headings and the site name (see assets/extra.css,
+ which points Sumi's font variables at them). #}
+
+
+
{% endblock %}
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 548f780..7636cc1 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,8 +1,7 @@
-# The community fork of MkDocs and Material, which the site moved to when
+# MkDocs NG, the community fork of MkDocs, which the site moved to when
# upstream MkDocs 2.0 announced it would remove the plugin system this site
-# depends on (social cards, blog) with no migration path.
-#
-# The `imaging` extra pulls in cairosvg and pillow, which the social plugin
-# needs to render the share cards.
+# depends on, and Sumi, its documentation theme. Sumi brings its own blog,
+# search palette and fonts, so nothing else is needed.
mkdocs-ng==1.11.0
-mkdocs-ng-material[imaging]==9.8.0
+mkdocs-sumi==0.3.0
+pymdown-extensions==12.3
diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css
deleted file mode 100644
index e33c3ea..0000000
--- a/docs/stylesheets/extra.css
+++ /dev/null
@@ -1,259 +0,0 @@
-/* Landing-page layout and a few typographic corrections.
-
- The brand palette lives in assets/extra.css, which loads first and is the
- single source for --md-primary-*, --md-accent-* and the link colour. */
-
-th {
- background-color: var(--md-default-fg-color--lightest);
-}
-
-/* Hero --------------------------------------------------------------------
- Rendered by overrides/home.html, which puts it outside the width-capped
- content column so the background reaches the window edges on its own. Moving
- it into the page content caps it at the column and needs a `calc(50% - 50vw)`
- full-bleed hack to escape again; keeping it in the template avoids that.
-
- Its
is the page's visible title; .cc-page-title below is the hidden
- counterpart that keeps Material from injecting a second one. */
-.hero {
- text-align: center;
- padding: 4rem 1rem;
- background: var(--md-primary-fg-color);
- color: var(--md-primary-bg-color);
-}
-
-/* Material renders a title into any page whose content has none, using the nav
- label — "Home" here, competing with the hero above it. The page carries this
- heading purely to suppress that. */
-.md-typeset .cc-page-title {
- display: none;
-}
-
-/* With the title hidden, the "edit this page" pencil is left floating alone in
- the corner. A landing page is not one anybody edits. */
-.md-content__inner:has(> .cc-page-title) .md-content__button {
- display: none;
-}
-
-/* Scales down rather than overflowing the viewport: at a flat 3rem the title
- was wider than a phone screen and clipped at both ends. */
-.hero h1 {
- font-size: clamp(2rem, 8vw, 3rem);
- font-weight: 700;
- margin-bottom: 1rem;
- text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
- text-align: center;
- color: unset;
-}
-
-/* The permalink anchor every heading gets is noise on a hero title. */
-.hero h1 .headerlink {
- display: none;
-}
-
-.hero p {
- font-size: 1.2rem;
- margin-bottom: 2rem;
- opacity: 0.9;
- width: min(96%, 34rem);
- margin-left: auto;
- margin-right: auto;
-}
-
-.hero .twemoji {
- --md-icon-size: 1.125em;
-
- display: inline-flex;
- height: var(--md-icon-size);
- vertical-align: text-top;
-}
-
-.hero .md-button {
- background: var(--cc-hero-button-bg);
- color: black;
- border: none;
- border-radius: unset;
- display: inline-block;
-}
-
-/* The hero's two calls to action. As inline-blocks they carried no gap, so a
- viewport too narrow to hold them side by side stacked them flush against
- each other — two square-cornered boxes of different widths touching, which
- reads as one broken element rather than two buttons. */
-.hero .cc-hero-actions {
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- gap: 0.8rem;
-}
-
-/* Once stacked they are a column, and a column of two different widths still
- looks like a mistake. Give them one width the whole way down. */
-@media screen and (max-width: 32rem) {
- .hero .cc-hero-actions {
- flex-direction: column;
- align-items: center;
- }
-
- .hero .cc-hero-actions .md-button {
- width: 100%;
- max-width: 18rem;
- }
-}
-
-.hero .md-button:hover {
- /* Was --commit-check-logo-color-green, which is not defined anywhere: the
- hover simply did nothing. */
- background: var(--cc-brand-light);
- color: black;
- border: none;
-}
-
-.md-typeset .md-button:hover {
- color: black;
-}
-
-/* Community section -------------------------------------------------------- */
-.community-section {
- background: var(--md-default-fg-color--lightest);
- padding: 3rem 2rem;
- margin: 3rem -1.5rem 0 -1.5rem;
- text-align: center;
- border-radius: 1rem 1rem 0 0;
-}
-
-.community-section h2 {
- color: var(--md-primary-fg-color);
- margin-bottom: 1rem;
-}
-
-/* Code block enhancements */
-.md-typeset pre > code {
- border-radius: 0.5rem;
-}
-
-/* Button styling improvements */
-.md-button {
- border-radius: 2rem;
- padding: 0.5rem 1.5rem;
- font-weight: 500;
- transition: all 0.3s ease;
-}
-
-.md-button:hover {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
-}
-
-/* Trusted-by section ------------------------------------------------------- */
-.trusted-by {
- text-align: center;
- padding: 3rem 0;
- background: var(--md-default-fg-color--lightest);
- margin: 3rem -1.5rem;
- border-radius: 1rem;
-}
-
-.trusted-by p {
- font-size: 1.1rem;
- color: var(--md-default-fg-color--light);
- margin-bottom: 2rem;
-}
-
-.logo-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
- gap: 2rem;
- margin-bottom: 3rem;
- padding: 0 2rem;
-}
-
-.logo-item {
- display: flex;
- flex-direction: column;
- align-items: center;
- transition: all 0.3s ease;
- padding: 1rem;
- border-radius: 0.5rem;
-}
-
-.logo-item:hover {
- transform: translateY(-4px);
- background: var(--md-default-bg-color);
- box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
-}
-
-.logo-item img {
- width: 60px;
- height: 60px;
- border-radius: 50%;
- margin-bottom: 0.5rem;
- filter: grayscale(100%);
- transition: filter 0.3s ease;
-}
-
-.logo-item:hover img {
- filter: grayscale(0%);
-}
-
-.logo-item span {
- font-size: 0.9rem;
- font-weight: 500;
- color: var(--md-default-fg-color--light);
-}
-
-@media screen and (max-width: 768px) {
- .logo-grid {
- grid-template-columns: repeat(2, 1fr);
- gap: 1rem;
- padding: 0 1rem;
- }
-}
-
-/* Both tinted bands above bleed past the content column with a negative
- margin. Material sets the root font to 125%, so -1.5rem is 30px — but below
- its 76.25em breakpoint the column only has a 16px gutter to give back. The
- bands hung 14px off the right edge, which made the whole page scroll
- sideways on every phone and tablet. Match the gutter there instead.
-
- (76.25em is Material's own breakpoint. Media-query em units resolve against
- the initial 16px font size rather than the 125% root, so this is 1220px.) */
-@media screen and (max-width: 76.25em) {
- .trusted-by,
- .community-section {
- margin-left: -0.8rem;
- margin-right: -0.8rem;
- }
-}
-
-/* Mobile navigation drawer -------------------------------------------------
- Material leaves this at its own default indigo, which clashes with the
- header right above it. */
-@media screen and (max-width: 76.2344em) {
- .md-nav--primary .md-nav__title[for="__drawer"] {
- background-color: var(--md-primary-fg-color);
- }
-}
-
-/* "important" admonition, which Material ships without a colour of its own. */
-.md-typeset .admonition.important,
-.md-typeset details.important {
- border-color: #00b8d4;
-}
-
-.md-typeset .important > .admonition-title::before,
-.md-typeset .important > summary::before {
- background-color: #00b8d4;
- -webkit-mask-image: var(--md-admonition-icon--info);
- mask-image: var(--md-admonition-icon--info);
-}
-
-.md-typeset .important > .admonition-title,
-.md-typeset .important > summary {
- background-color: #00b8d41a;
-}
-
-/* Annotation markers are round, matching the numbered badges they pair with. */
-a.md-annotation__index {
- border-radius: 2.2ch;
-}
diff --git a/docs/stylesheets/landing.css b/docs/stylesheets/landing.css
index 95cb5ef..c329b24 100644
--- a/docs/stylesheets/landing.css
+++ b/docs/stylesheets/landing.css
@@ -2,16 +2,13 @@
* Landing page only.
*
* This file is linked from docs/overrides/landing.html rather than listed in
- * 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 .
+ * mkdocs.yml's extra_css, so the documentation pages never load it. Every
+ * page is a full page load, so rules below may safely reach outside the
+ * content area, to the header and the page frame.
*
- * 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.
+ * Sizes are in px, not rem: the landing page is laid out to fixed measures
+ * (the demo window, the five-column row) that should not grow with the
+ * reader's font size.
*
* 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
@@ -47,7 +44,7 @@
--cc-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
-[data-md-color-scheme="slate"] {
+[data-theme="dark"] {
--cc-paper: #0e1a23;
--cc-card: #13212c;
--cc-line: #263744;
@@ -59,49 +56,35 @@
/* ---------------------------------------------------------------- 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;
-}
-
-@media screen and (min-width: 76.25em) {
- .md-header .md-search {
- display: block;
- }
-}
-
-/* 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 {
+/* The hero is a band that meets the header, so the theme's gap above the
+ content goes. The page is one centred column, the same width as the header,
+ and the bands below reach past it to the window edges. */
+.sumi-main.cc-landing {
+ padding: 0;
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. */
+ `clip` rather than `hidden`, because `hidden` would make this a scroll
+ container and break the sticky header and in-page anchors. */
overflow-x: clip;
}
+.cc-landing > .md-typeset {
+ max-width: var(--cc-page-w, 1220px);
+ margin: 0 auto;
+ padding: 0 16px;
+ font-family: var(--sumi-font);
+ font-size: 16px;
+ line-height: 1.6;
+}
+
+/* The cards were laid out with half an em above and below a paragraph inside
+ a list item, which the page's first theme gave them. Component rules below
+ are more specific and still override it. */
+.md-typeset :is(ul, ol) li :is(p, blockquote) {
+ margin: 0.5em 0;
+}
+
/* ---------------------------------------------------------------- resets -- */
/* The theme styles bare elements inside .md-typeset. These resets use
@@ -112,14 +95,14 @@
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) {
+/* :root keeps these ahead of the theme's own list rules. */
+:root .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 {
+:root .md-typeset :where(.cc-band:not(.cc-faq)) :is(ul, ol) > li {
margin: 0;
}
@@ -150,7 +133,7 @@
/* 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
+ Percentages, not viewport units, for the margin: the column is a grid
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 {
@@ -2046,7 +2029,7 @@
color: #5b6d7a;
}
-[data-md-color-scheme="slate"] .md-typeset .cc-users a {
+[data-theme="dark"] .md-typeset .cc-users a {
color: #a3b4c0;
}
@@ -2086,7 +2069,7 @@
background: #f5fafd;
}
-[data-md-color-scheme="slate"] .md-typeset .cc-plan--soon {
+[data-theme="dark"] .md-typeset .cc-plan--soon {
border-color: #2e5467;
background: #10222d;
}
@@ -2139,20 +2122,25 @@
.md-typeset .cc-faq .cc-h2 {
margin: 0 0 28px;
+ padding-top: 0;
+ border-top: 0;
}
.md-typeset .cc-faq details {
margin: 0 0 10px;
border: 1px solid var(--cc-line);
border-radius: 12px;
+ padding: 0;
background: var(--cc-card);
box-shadow: none;
font-size: 15px;
}
.md-typeset .cc-faq details > summary {
- padding: 14px 44px 14px 18px;
+ margin: 0;
+ padding: 14px 18px;
background: none;
+ color: var(--cc-text);
font-size: 16px;
font-weight: 600;
}
@@ -2161,9 +2149,10 @@
display: none;
}
+/* The question admonition's green, on the chevron only. */
.md-typeset .cc-faq details > summary::after {
- top: 16px;
- right: 14px;
+ border-color: #64dd17;
+ opacity: 1;
}
.md-typeset .cc-faq details > p {
@@ -2474,7 +2463,7 @@
/* 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 {
+[data-theme="dark"] .md-typeset .cc-group--ink {
border-color: #22404f;
}
@@ -2485,51 +2474,51 @@
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 {
+[data-theme="light"] .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 {
+[data-theme="light"] .md-typeset .cc-hero .cc-display {
color: var(--cc-text);
}
-[data-md-color-scheme="default"] .md-typeset .cc-hero .cc-accent {
+[data-theme="light"] .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 {
+[data-theme="light"] .md-typeset .cc-hero .cc-lede {
color: var(--cc-muted);
}
-[data-md-color-scheme="default"] .md-typeset .cc-hero .cc-lede code {
+[data-theme="light"] .md-typeset .cc-hero .cc-lede code {
color: var(--cc-text);
}
-[data-md-color-scheme="default"] .md-typeset .cc-hero .cc-pill {
+[data-theme="light"] .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 {
+[data-theme="light"] .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 {
+[data-theme="light"] .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 {
+[data-theme="light"] .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 {
+[data-theme="light"] .md-typeset .cc-hero__graph circle {
fill: #ffffff;
}
diff --git a/mkdocs.yml b/mkdocs.yml
index 713556a..63f041b 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -29,65 +29,26 @@ validation:
anchors: warn
theme:
- name: material
- language: en
+ name: sumi
+ locale: en
custom_dir: docs/overrides
logo: assets/logo.svg
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: 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: light)"
- scheme: default
- primary: custom
- accent: custom
- toggle:
- icon: material/brightness-7
- name: Switch to dark mode
- - media: "(prefers-color-scheme: dark)"
- scheme: slate
- primary: custom
- accent: custom
- toggle:
- icon: material/brightness-4
- name: Switch to light mode
- features:
- # Navigation lives in the left sidebar, grouped into sections — deliberately
- # not navigation.tabs, which moves the top level up into the header.
- - navigation.sections
- - navigation.top
- - navigation.tracking
- - navigation.instant
- - navigation.instant.progress
- - navigation.footer
- - toc.follow
- - search.suggest
- - search.highlight
- - search.share
- - content.code.copy
- - content.code.annotate
- - content.tabs.link
- - content.action.edit
+ # Follows the system on the first visit; the header toggle then switches
+ # between light and dark and remembers the choice.
+ palette: auto
+ # Signal Blue, the brand accent (see assets/extra.css for the full palette).
+ accent: "#2c9ccd"
+ # Navigation lives in the left sidebar, grouped into sections, not in
+ # header tabs.
+ navigation_tabs: false
+ # The preview image for pages that do not set their own `image:`.
+ social_image: assets/social.png
extra_css:
- # Brand colours and typography, then the landing-page-specific layout.
+ # Brand colours and typography. The landing page adds its own layout in
+ # overrides/landing.html.
- 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:
@@ -113,9 +74,6 @@ markdown_extensions:
permalink: true
permalink_title: Link to this section
- pymdownx.details
- - pymdownx.emoji:
- emoji_index: !!python/name:material.extensions.emoji.twemoji
- emoji_generator: !!python/name:material.extensions.emoji.to_svg
- pymdownx.highlight:
anchor_linenums: true
line_spans: __span
@@ -135,18 +93,9 @@ markdown_extensions:
plugins:
- search
- - blog
- # Generates the preview image shown when a page is shared on Slack, X or
- # LinkedIn. Needs cairo and pillow, which the docs requirements pull in.
- #
- # Deploy previews turn this off (see netlify.toml): nobody shares a preview
- # link for its preview image, and skipping the cards keeps those builds fast
- # and free of the system cairo dependency.
- - social:
- enabled: !ENV [SOCIAL_CARDS, true]
- cards_layout_options:
- background_color: "#0b1620"
- color: "#ffffff"
+ # Sumi's plugin: components, the blog (index, tags, Atom feed) under
+ # docs/blog/, and a Markdown copy of each page.
+ - sumi
hooks:
- scripts/mkdocs_hooks.py
diff --git a/netlify.toml b/netlify.toml
index 8478346..006348d 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -20,15 +20,11 @@
# those values are literals, so "${DEPLOY_PRIME_URL}" would reach MkDocs
# unexpanded and be rejected as a URL without a scheme. The build command runs
# in a shell, where the variable actually expands.
-#
-# SOCIAL_CARDS=false because nobody shares a preview link for its preview
-# image, and skipping the cards keeps previews fast and free of the system
-# cairo dependency.
[context.deploy-preview]
- command = "pip install -r docs/requirements.txt && SITE_URL=\"$DEPLOY_PRIME_URL/\" SOCIAL_CARDS=false mkdocs build --strict"
+ command = "pip install -r docs/requirements.txt && SITE_URL=\"$DEPLOY_PRIME_URL/\" mkdocs build --strict"
[context.branch-deploy]
- command = "pip install -r docs/requirements.txt && SITE_URL=\"$DEPLOY_PRIME_URL/\" SOCIAL_CARDS=false mkdocs build --strict"
+ command = "pip install -r docs/requirements.txt && SITE_URL=\"$DEPLOY_PRIME_URL/\" mkdocs build --strict"
# The URLs the original Sphinx site served. The build does not emit redirect
# stubs on Netlify (see scripts/mkdocs_hooks.py): a stub at rules.html would be
diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py
index 19419ff..0c99412 100644
--- a/scripts/mkdocs_hooks.py
+++ b/scripts/mkdocs_hooks.py
@@ -54,6 +54,12 @@
# An orphan: nothing linked to it, it was not in the nav, and it matched no
# key in .authors.yml, so the blog never treated it as an author page.
"blog/author/team": "blog/",
+ # Material's blog plugin wrote an archive page and one page per category.
+ # Sumi's blog index lists every post and filters by tag in place, so these
+ # all land on it.
+ "blog/archive/2026": "blog/",
+ "blog/category/announcements": "blog/",
+ "blog/category/updates": "blog/",
}
# Redirect stubs for the URLs the Sphinx site served.