diff --git a/.github/workflows/docs-preview-deploy.yml b/.github/workflows/docs-preview-deploy.yml index 55690098..36fa3ccc 100644 --- a/.github/workflows/docs-preview-deploy.yml +++ b/.github/workflows/docs-preview-deploy.yml @@ -103,6 +103,9 @@ jobs: 'tests/test_agent_markdown.py', 'tests/test_docs_404.py', 'tests/test_render_dev_notes.py', + 'tests/test_dev_note_headers.py', + 'tests/test_dev_notes_layout.py', + 'tests/test_dev_notes_layout.py.lock', 'tests/test_stage_project_docs.py', 'tests/docs-preview.test.js', 'zensical.toml', diff --git a/.github/workflows/docs-preview.yml b/.github/workflows/docs-preview.yml index fb17cb66..8b3487d5 100644 --- a/.github/workflows/docs-preview.yml +++ b/.github/workflows/docs-preview.yml @@ -51,6 +51,9 @@ jobs: 'tests/test_agent_markdown.py', 'tests/test_docs_404.py', 'tests/test_render_dev_notes.py', + 'tests/test_dev_note_headers.py', + 'tests/test_dev_notes_layout.py', + 'tests/test_dev_notes_layout.py.lock', 'tests/test_stage_project_docs.py', 'tests/docs-preview.test.js', 'zensical.toml', diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4a4bb775..21d84977 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -31,7 +31,7 @@ jobs: uv run --python ${{ matrix.python-version }} --with pytest==8.4.2 pytest -q tests/test_agent_markdown.py tests/test_docs_404.py tests/test_docs_seo.py tests/test_render_dev_notes.py - tests/test_stage_project_docs.py + tests/test_dev_note_headers.py tests/test_stage_project_docs.py validate: name: Validate documentation @@ -54,6 +54,7 @@ jobs: - name: Check documentation JavaScript syntax run: | node --check docs/javascripts/navigation-drawer.js + node --check docs/javascripts/dev-notes.js node --check docs/javascripts/pi-traces.js - name: Test navigation JavaScript behavior @@ -65,6 +66,25 @@ jobs: - name: Build documentation run: scripts/build-docs.sh + - name: Set up uv for browser checks + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + + - name: Install layout-check browser + run: uv run --locked --script tests/test_dev_notes_layout.py --install-browser + + - name: Check Dev Notes reading layouts + run: uv run --locked --script tests/test_dev_notes_layout.py -q + + - name: Upload Dev Notes layout screenshots + if: always() + uses: actions/upload-artifact@v7 + with: + name: dev-notes-layout + path: .cache/dev-notes-layout + include-hidden-files: true + if-no-files-found: ignore + retention-days: 7 + - name: Verify generated documentation is committed run: git diff --exit-code -- docs/dev-notes zensical.toml diff --git a/docs/assets/robotics-policy-prover/openshell-robotics-prover-demo.mp4 b/docs/assets/robotics-policy-prover/openshell-robotics-prover-demo.mp4 index 32a1cf73..4a244789 100644 Binary files a/docs/assets/robotics-policy-prover/openshell-robotics-prover-demo.mp4 and b/docs/assets/robotics-policy-prover/openshell-robotics-prover-demo.mp4 differ diff --git a/docs/dev-notes/AGENTS.md b/docs/dev-notes/AGENTS.md new file mode 100644 index 00000000..25e8d56d --- /dev/null +++ b/docs/dev-notes/AGENTS.md @@ -0,0 +1,26 @@ +--- +title: Dev Notes authoring instructions +robots: noindex, follow +--- + +# Dev Notes authoring + +Read `docs/development/index.md` before editing a note. Authors supply front +matter and prose; `python3 scripts/render-dev-notes.py` generates the title, +optional subtitle and hero, author byline, index cards, and navigation. + +- Set `categories` to a list containing exactly one of `Announcements`, + `Research`, `Case Studies`, or `Examples`. Use tags for technical topics. + The renderer rejects missing, unknown, or multiple categories. +- Use `subtitle` for an optional short deck and `hero_image_alt` to describe a + `hero_image`. `hero_image_dark` is optional. Do not copy header HTML from a post. +- Start new post bodies with the introduction, without a duplicate H1 or hero. +- Do not edit the generated `dev-note:header`, `dev-note:byline`, or + `dev-notes:posts` marker regions. +- Keep typography and hero sizing in the shared Dev Notes stylesheet. Do not add + per-post title or hero sizing rules, inline styles, or fixed aspect ratios. +- Keep detailed body figures readable; the hero links to its full-size image. +- Run the documented build and locked browser layout checks. They discover all + notes automatically and cover both themes, desktop and phone widths, and a + featured note moving into the recent list. Commit regenerated files with the + authored metadata. diff --git a/docs/dev-notes/index.md b/docs/dev-notes/index.md index 98c842c5..2617b8fd 100644 --- a/docs/dev-notes/index.md +++ b/docs/dev-notes/index.md @@ -5,20 +5,17 @@ hide: - toc --- -
+
- - - OpenShell Research / Dev Notes - + + OpenShell / Dev Notes

Dev Notes

- Technical notes from the OpenShell team – reproducible research, - benchmarks, and use case examples. + OpenShell's research journal – reproducible research, announcements, and case studies.

@@ -26,12 +23,34 @@ hide: +
+ +
+
diff --git a/docs/dev-notes/posts/2026-07-20-policy-controlling-reachy-mini-with-openshell.md b/docs/dev-notes/posts/2026-07-20-policy-controlling-reachy-mini-with-openshell.md index d864765e..eb9f1ff0 100644 --- a/docs/dev-notes/posts/2026-07-20-policy-controlling-reachy-mini-with-openshell.md +++ b/docs/dev-notes/posts/2026-07-20-policy-controlling-reachy-mini-with-openshell.md @@ -6,9 +6,10 @@ description: "Edge agents handle sensitive data and make decisions with physical author: "Kirit Thadaka" agent_markdown: true hero_image: "../../assets/reachy-mini-openshell/hero.svg" +hero_image_alt: "Bringing Privacy and Security to the Edge with OpenShell: a stylized Reachy Mini robot with two green camera eyes inside an OpenShell boundary ring." social_image: "assets/reachy-mini-openshell/hero.svg" categories: - - Edge AI + - Examples tags: - reachy-mini - openshell @@ -24,22 +25,25 @@ card_tags: - policy --- + + + + +
+ # Bringing Privacy and Security to the Edge with OpenShell
- Bringing Privacy and Security to the Edge with OpenShell: a stylized Reachy Mini robot with two green camera eyes inside an OpenShell boundary ring. + Bringing Privacy and Security to the Edge with OpenShell: a stylized Reachy Mini robot with two green camera eyes inside an OpenShell boundary ring.
+
+
- +
+ + Useful AI agents now run for long periods of time, taking on critical tasks to accomplish goals. They call tools, learn new skills, write code, and keep working while no one is watching. How do you give autonomy to such a highly capable agent? OpenShell is the secure runtime for autonomous agents. It solves this problem by moving the security layer outside the agent. OpenShell runs your agent in a sandbox and sits in the path of everything the agent does. A policy layer, configured separately from the agent, governs what processes the agent can start, which hosts and ports it can reach, which models it can call, and which other services it needs to invoke along its network path. diff --git a/docs/dev-notes/posts/2026-08-07-formal-methods-ai-generated-robot-actions.md b/docs/dev-notes/posts/2026-08-07-formal-methods-ai-generated-robot-actions.md index 509f1b34..db0f4fa7 100644 --- a/docs/dev-notes/posts/2026-08-07-formal-methods-ai-generated-robot-actions.md +++ b/docs/dev-notes/posts/2026-08-07-formal-methods-ai-generated-robot-actions.md @@ -6,9 +6,11 @@ description: "A robotics experiment asks whether an independent, SMT-backed poli author: "Alex Watson" agent_markdown: true hero_image: "../../assets/robotics-policy-prover/robotics-policy-prover-hero.png" +hero_image_alt: "A simulated robot workcell showing an AI-generated waypoint path, a red restricted volume, and the policy prover's decision." +subtitle: "We built a small robotics experiment to test whether OpenShell's formal methods-based policy enforcement can govern robot plans generated by local and frontier AI models." social_image: "assets/robotics-policy-prover/robotics-policy-prover-hero.png" categories: - - Physical AI + - Research tags: - openshell - robotics @@ -24,26 +26,27 @@ card_tags: - policy --- + + + + +
+ # Can Formal Methods Govern AI-Generated Robot Actions? -*We built a small robotics experiment to test whether OpenShell's formal -methods-based policy enforcement can govern robot plans generated by local and -frontier AI models.* +

We built a small robotics experiment to test whether OpenShell's formal methods-based policy enforcement can govern robot plans generated by local and frontier AI models.

-
- A simulated robot workcell showing an AI-generated waypoint path, a red restricted volume, and the policy prover's decision. +
+ A simulated robot workcell showing an AI-generated waypoint path, a red restricted volume, and the policy prover's decision.
+
+
- +
+ + OpenShell uses formal methods-based policy enforcement to constrain what AI agents can do in digital environments. We wanted to see whether a similar approach could govern the higher-level plans local and frontier models generate diff --git a/docs/dev-notes/posts/2026-08-27-adversarial-policy-review-long-horizon-agents.md b/docs/dev-notes/posts/2026-08-27-adversarial-policy-review-long-horizon-agents.md index 58665e03..de2f08c4 100644 --- a/docs/dev-notes/posts/2026-08-27-adversarial-policy-review-long-horizon-agents.md +++ b/docs/dev-notes/posts/2026-08-27-adversarial-policy-review-long-horizon-agents.md @@ -5,9 +5,10 @@ description: "Can an attacker agent convince an equally capable reviewer agent t author: "Alex Watson" agent_markdown: true hero_image: "../../assets/long-horizon-agent-evals/back-and-forth-by-session.png" +hero_image_alt: "Back-and-forth by session for the 30-minute experiments." social_image: "assets/long-horizon-agent-evals/back-and-forth-by-session.png" categories: - - OpenShell + - Research tags: - openshell - agents @@ -22,18 +23,25 @@ card_tags: - security --- + + + + +
+ # Adversarial prototype: AI policy auto-approval for long-horizon agents in OpenShell +
+ Back-and-forth by session for the 30-minute experiments. +
+ +
+
- +
+ + We’ve been experimenting with AI auto policy approval in OpenShell (similar to [OpenAI’s Auto-review work](https://alignment.openai.com/auto-review/) and [Anthropic’s research directions on behavioral monitoring and AI control](https://red.anthropic.com/2025/recommended-directions/)) and ran an adversarial test to understand how it behaves over longer agent trajectories. The question was: can an attacker agent convince an equally capable reviewer agent to grant a capability that is explicitly prohibited? diff --git a/docs/dev-notes/posts/2026-09-10-learning-formal-methods-agent-policy-prover.md b/docs/dev-notes/posts/2026-09-10-learning-formal-methods-agent-policy-prover.md index b6ae1bc6..148af98a 100644 --- a/docs/dev-notes/posts/2026-09-10-learning-formal-methods-agent-policy-prover.md +++ b/docs/dev-notes/posts/2026-09-10-learning-formal-methods-agent-policy-prover.md @@ -5,9 +5,11 @@ description: "An intro to using formal methods to reason about permission change author: "Alex Watson" agent_markdown: true hero_image: "../../assets/agent-policy-prover/hero-concept.png" +hero_image_alt: "Five colorful clusters of connected AI agent nodes sit within a green policy boundary while a red path crosses the boundary and is stopped by a proof marker." +subtitle: "An intro to using formal methods to reason about permission changes in long-running AI agents." social_image: "assets/agent-policy-prover/hero-concept.png" categories: - - OpenShell + - Research tags: - openshell - formal-methods @@ -22,24 +24,27 @@ card_tags: - agents --- + + + + +
+ # What we have learned applying formal methods to control AI agents

An intro to using formal methods to reason about permission changes in long-running AI agents.

- Five colorful clusters of connected AI agent nodes sit within a green policy boundary while a red path crosses the boundary and is stopped by a proof marker. + Five colorful clusters of connected AI agent nodes sit within a green policy boundary while a red path crosses the boundary and is stopped by a proof marker.
+
+
- +
+ + In this post- we’ll dive into how permission review breaks at agent scale, and how to use the [Z3 open source library](https://github.com/Z3Prover/z3) to write a formal proof that a policy change proposed by an agent stays inside what you approved. ## Why permission review breaks at agent scale diff --git a/docs/dev-notes/posts/2026-09-26-network-redaction-is-not-enough.md b/docs/dev-notes/posts/2026-09-26-network-redaction-is-not-enough.md index 81054df0..3952782f 100644 --- a/docs/dev-notes/posts/2026-09-26-network-redaction-is-not-enough.md +++ b/docs/dev-notes/posts/2026-09-26-network-redaction-is-not-enough.md @@ -7,6 +7,8 @@ agent_markdown: true reset_scroll_on_reload: true social_image: "assets/pi-admission/hero-light.png" hero_image: "../../assets/pi-admission/hero-light.png" +hero_image_alt: "An envelope inside a sandbox boundary breaks into small pieces that flow through a small gap in the boundary and reassemble into an envelope outside." +subtitle: "A simple experiment shows why network-layer redaction isn’t enough." hero_image_dark: "../../assets/pi-admission/hero-dark.png" categories: - Research @@ -22,25 +24,28 @@ card_tags: - runtime-and-harness --- + + + + +
+ # The leak is coming from inside the sandbox

A simple experiment shows why network-layer redaction isn’t enough.

-
- An envelope inside a sandbox boundary breaks into small pieces that flow through a small gap in the boundary and reassemble into an envelope outside. - An envelope inside a sandbox boundary breaks into small pieces that flow through a small gap in the boundary and reassemble into an envelope outside. +
+ An envelope inside a sandbox boundary breaks into small pieces that flow through a small gap in the boundary and reassemble into an envelope outside. + An envelope inside a sandbox boundary breaks into small pieces that flow through a small gap in the boundary and reassemble into an envelope outside.
+
+
- +
+ + Agents are most useful when they have access to your (company’s) data, but the context they need is often mixed with sensitive information you don’t want to share with model providers. If you plan to redact or anonymize that sensitive information, when in the pipeline should you do it? The best answer is *before* the agent's tools can come anywhere near your sensitive data, but that's not always practical (though it might be required!). What if instead you simply applied a redaction filter to all outgoing model requests? After all, the model would never see your sensitive data, right? diff --git a/docs/development/index.md b/docs/development/index.md index 12417873..19d4b475 100644 --- a/docs/development/index.md +++ b/docs/development/index.md @@ -53,23 +53,102 @@ Dev Notes are Markdown posts under `docs/dev-notes/posts/`. Each post requires defined in `docs/dev-notes/authors.json`. Use a dated filename such as `YYYY-MM-DD-short-title.md`. -The renderer uses `categories[0]` as the card topic and `card_tags` as its tags, -falling back to `tags`. An optional `card_variant` must have matching card and +Every post must list exactly one category under `categories`, using one of these +fixed names: + +- **Announcements**: releases, launches, and team or project updates. +- **Research**: experiments, benchmarks, findings, and technical investigations. +- **Case Studies**: accounts of real-world adoption, deployment, and outcomes. +- **Examples**: practical walkthroughs and demonstrations readers can reproduce. + +The renderer validates this list before updating any generated content. Missing, +unknown, or multiple categories fail the build with an author-facing error. +Categories appear consistently on index cards and article bylines. Categories +remain available even when they have no posts; do not create placeholder notes. +Use tags for subject areas such as robotics, formal methods, or agent security. + +Readers can combine category and author filters on the Dev Notes index. The +renderer derives the author menu from every post's author IDs and always includes +all four categories, including those without posts. The newest matching post is +featured; the remaining matches keep their chronological order. Filters use +`?category=research&author=johnnygreco` URLs that survive refresh and browser +Back/Forward navigation. Article byline names and categories link to these +filtered views. Empty combinations show a message and a clear-filters action. +Keep the full index readable when JavaScript is unavailable. + +The renderer uses `card_tags` as the card's tags, falling back to `tags`. +An optional `card_variant` must have matching card and artwork CSS modifiers in `docs/stylesheets/dev-notes.css`. Set `hero_image` to -an image path relative to the post when its card should use the post's hero -instead of generated artwork. Set `hero_image_dark` to an optional dark-mode -counterpart; cards switch between them with the site theme. Hero images must -live under `docs/`. +an image path relative to the post, and describe it with `hero_image_alt`. The +renderer uses it for both the post's opening hero and its index thumbnail. Set +`hero_image_dark` to an optional dark-mode counterpart; both surfaces switch with +the site theme. Hero images must live under `docs/`. Without `hero_image`, the +post omits the hero and the index uses generated artwork. + +Authors provide content rather than header HTML. For example, a new post can be: + +```markdown +--- +title: "A clear experiment title" +date: 2026-09-28 +description: "A short summary for the index and search." +subtitle: "An optional sentence introducing the experiment." +agent_markdown: true +authors: + - johnnygreco +hero_image: "../../assets/my-experiment/hero.png" +hero_image_alt: "A description of what the image communicates." +categories: + - Research +--- + +Start the introduction here. The renderer inserts the opening layout above it. -Place a post's hero figure immediately after its subtitle and before the generated -author byline. If the post has no subtitle, place the hero directly after the -title. Keep the introduction below the byline. +## Experiment + +Describe the work and evidence. +``` + +Use a real author ID and an existing image, or omit both hero fields. `subtitle` +is optional and separate from the index's `description`. The renderer generates +the title, subtitle, hero, and byline in that order. Do not add another H1 or +manually position a hero. Existing generated headers update from front matter +without changing the prose below them. + +Shared CSS sets the reading column to at most 74 character units, with 18px body +text on desktop and at least 16px on phones. Every post uses the sandbox note's +smaller title and subtitle styles: titles scale from 1.6rem on narrow phones +to 2.8rem on desktop, with a 1.85rem minimum above 24rem viewport width, and +subtitles from 0.95rem to 1.05rem. The opening reserves space for the site header +and breadcrumbs, then fits the title, subtitle, and complete hero in the remaining +viewport. Heroes fill that available space up to the reading-column width and +560px height, preserving their native proportions without cropping. Longer text +leaves less room for the hero; authors should keep titles and subtitles concise. +The index continues to use compact thumbnails. +Clicking a hero opens the original image, so a diagram can remain compact without +losing access to its details. Index images use bounded frames with the full image +visible. Do not add per-post title/hero sizing or force an image into a different +aspect ratio. Body diagrams may still use the shared wide or scrollable figure +treatments when their labels need more room. + +The index separates the page introduction, filtering controls, and posts. Give +the masthead breathing room and keep the filters in their own row between two +dividers, above the featured-note heading. Laptop screens show the featured title; +a 900px-tall desktop viewport fits the featured note. Keep its image and type +larger than the archive entries; longer featured titles can move the archive +below the first screen. +The archive remains a normal scrolling list as it grows; do not shrink its +entries to fit all notes on one screen or give the archive a separate scrolling +panel. Keep one label per section. Use whitespace within the featured note and +dividers above and below the filtering controls, at the archive boundary, and between +archive entries. Do not edit content inside these generated marker pairs: - `` / `` in `docs/dev-notes/index.md` - `` / `` in posts +- `` / `` in posts - `# dev-notes:nav:start` / `# dev-notes:nav:end` in `zensical.toml` After changing posts or author metadata, run: @@ -80,6 +159,29 @@ python3 scripts/render-dev-notes.py Commit any generated changes with the source change. +The Docs workflow runs the header tests, verifies generated content is committed, +and checks the built site in Chromium and WebKit at desktop, tablet, and phone +sizes in both themes. The browser checks discover every note automatically. They reject missing +hero assets or alt text, cropped or thumbnail-sized article heroes, overly long text lines, +horizontal page overflow, off-center heroes, images overflowing their figures or +overlapping the byline, incorrect theme-image visibility, an opening that needs +scrolling to see the full hero, and an index that obscures the featured title on +laptops or pushes the featured note below a 900px-tall +desktop viewport. They also check the separation of introduction, filters, and posts. +They also move the featured note into the recent list to exercise its thumbnail +layout. The workflow saves screenshots as the `dev-notes-layout` artifact for +visual review. These are layout +constraints rather than pixel snapshots, so ordinary prose edits need no new +baselines. They cannot judge the readability of labels baked into an image; +review the screenshot and full-size image for detailed charts. + +The same browser checks cover shared navigation: footer titles must fit inside +their links, breadcrumbs must remain readable, and wide documentation tables +must scroll within the article without overflowing the page. +Embedded demos are also played in both browsers. Publish MP4 recordings with +H.264 video, 8-bit `yuv420p` pixels, and streaming metadata at the beginning of +the file (`faststart`); HEVC-only recordings do not play in every browser. + For Pi transcript provenance, publication boundaries, and viewer checks, see the [Pi project maintenance guide](https://github.com/NVIDIA/OpenShell-Research/blob/main/projects/research/pi-admission/README.md#maintenance). @@ -110,12 +212,19 @@ Run the renderer tests and the same clean build used by CI: ```sh uv run --python 3.12 --with pytest==8.4.2 pytest -q \ tests/test_agent_markdown.py tests/test_docs_404.py \ - tests/test_render_dev_notes.py tests/test_stage_project_docs.py + tests/test_render_dev_notes.py tests/test_dev_note_headers.py tests/test_stage_project_docs.py node --check docs/javascripts/pi-traces.js node tests/docs-preview.test.js scripts/build-docs.sh +uv run --locked --script tests/test_dev_notes_layout.py --install-browser +uv run --locked --script tests/test_dev_notes_layout.py -q ``` +The browser installer is needed once per environment. The test script's inline +dependencies and adjacent uv lock pin its toolchain independently of the site +builder. Browser checks serve the built artifact on an ephemeral local port and +write viewport screenshots to `.cache/dev-notes-layout/`. + `scripts/build-docs.sh` recreates `.venv-docs`, installs the pinned toolchain, stages each configured canonical project documentation tree from `projects/` at its configured site destination, renders Dev Notes metadata, and runs `zensical diff --git a/docs/index.md b/docs/index.md index d030f279..21cb9364 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,7 +5,7 @@ hide: - toc --- -
+

diff --git a/docs/javascripts/dev-notes.js b/docs/javascripts/dev-notes.js new file mode 100644 index 00000000..9ae873f4 --- /dev/null +++ b/docs/javascripts/dev-notes.js @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +(() => { + let activePage = null; + let cleanup = () => {}; + const originalCards = new WeakMap(); + + function enhanceDevNotes() { + const page = document.querySelector(".dev-notes-page"); + if (page === activePage) return; + cleanup(); + activePage = page; + cleanup = () => {}; + if (!page) return; + + const form = page.querySelector(".dev-notes-filters"); + if (!form) return; + const category = form.elements.namedItem("category"); + const author = form.elements.namedItem("author"); + const clear = form.querySelector(".dev-notes-filters__clear"); + const featured = page.querySelector(".dev-notes-featured"); + const recent = page.querySelector(".dev-notes-recent"); + const recentList = page.querySelector(".dev-notes-recent-list"); + const heading = page.querySelector("#featured-note-title"); + const recentHeading = page.querySelector("#recent-notes-title"); + const empty = page.querySelector(".dev-notes-filter-empty"); + if (!originalCards.has(page)) { + originalCards.set(page, Array.from(page.querySelectorAll(".dev-note-card"))); + } + const cards = originalCards.get(page).map((element) => ({ + element, + category: element.dataset.category, + authors: JSON.parse(element.dataset.authors), + })); + + const applyFilters = () => { + const url = new URL(window.location.href); + let normalized = false; + for (const select of [category, author]) { + const value = url.searchParams.get(select.name) || ""; + const valid = Array.from(select.options).some((option) => option.value === value); + select.value = valid ? value : ""; + if (!valid) { + url.searchParams.delete(select.name); + normalized = true; + } + } + if (normalized) window.history.replaceState(window.history.state, "", url); + + const filtered = Boolean(category.value || author.value); + const matches = cards.filter((card) => + (!category.value || card.category === category.value) && + (!author.value || card.authors.includes(author.value)), + ); + const visible = new Set(matches); + for (const card of cards) { + card.element.hidden = !visible.has(card); + card.element.classList.remove("dev-note-card--featured"); + card.element.classList.add("dev-note-card--recent"); + } + matches.forEach((card, index) => { + card.element.classList.toggle("dev-note-card--featured", index === 0); + card.element.classList.toggle("dev-note-card--recent", index !== 0); + if (index === 0) featured.insertBefore(card.element, empty); + else recentList.append(card.element); + }); + if (recent) recent.hidden = matches.length < 2; + if (recentHeading) recentHeading.textContent = filtered ? "More notes" : "Recent notes"; + heading.textContent = filtered + ? `${matches.length} ${matches.length === 1 ? "note" : "notes"}` + : "Featured note"; + clear.hidden = !filtered; + empty.hidden = matches.length !== 0; + const categoryName = category.selectedOptions[0].textContent; + const authorName = author.selectedOptions[0].textContent; + empty.textContent = category.value && author.value + ? `No ${categoryName} by ${authorName} yet.` + : category.value + ? `No ${categoryName} yet.` + : `No notes by ${authorName} yet.`; + }; + + const updateUrl = (event) => { + event.preventDefault(); + const url = new URL(window.location.href); + for (const select of [category, author]) { + if (select.value) url.searchParams.set(select.name, select.value); + else url.searchParams.delete(select.name); + } + if (url.href !== window.location.href) { + window.history.pushState(window.history.state, "", url); + } + applyFilters(); + }; + const clearFilters = (event) => { + category.value = ""; + author.value = ""; + category.focus(); + updateUrl(event); + }; + + form.addEventListener("change", updateUrl); + form.addEventListener("submit", updateUrl); + clear.addEventListener("click", clearFilters); + window.addEventListener("popstate", applyFilters); + applyFilters(); + form.hidden = false; + cleanup = () => { + form.removeEventListener("change", updateUrl); + form.removeEventListener("submit", updateUrl); + clear.removeEventListener("click", clearFilters); + window.removeEventListener("popstate", applyFilters); + }; + } + + if (typeof document$ !== "undefined") { + document$.subscribe(enhanceDevNotes); + } else if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", enhanceDevNotes); + } else { + enhanceDevNotes(); + } +})(); diff --git a/docs/javascripts/navigation-drawer.js b/docs/javascripts/navigation-drawer.js index 1644d407..c17494f5 100644 --- a/docs/javascripts/navigation-drawer.js +++ b/docs/javascripts/navigation-drawer.js @@ -11,6 +11,7 @@ const overlay = document.querySelector('.md-overlay[for="__drawer"]'); const modalDrawer = window.matchMedia(modalDrawerQuery); const button = document.querySelector(".openshell-drawer-button"); + const closeButton = document.querySelector(".openshell-drawer-close"); if ( !(toggle instanceof HTMLInputElement) || @@ -161,6 +162,7 @@ }; button.addEventListener("click", onButtonClick); + closeButton?.addEventListener("click", onOverlayClick); toggle.addEventListener("change", onToggleChange); sidebar.addEventListener("click", onSidebarClick); overlay?.addEventListener("click", onOverlayClick); @@ -182,6 +184,7 @@ cleanup = () => { button.removeEventListener("click", onButtonClick); + closeButton?.removeEventListener("click", onOverlayClick); toggle.removeEventListener("change", onToggleChange); sidebar.removeEventListener("click", onSidebarClick); overlay?.removeEventListener("click", onOverlayClick); diff --git a/docs/projects/reachy-mini-openshell-sandbox.md b/docs/projects/reachy-mini-openshell-sandbox.md index 5c338804..1ad0a881 100644 --- a/docs/projects/reachy-mini-openshell-sandbox.md +++ b/docs/projects/reachy-mini-openshell-sandbox.md @@ -2,7 +2,7 @@ The canonical tutorial now lives beside the runnable project: -[Open the onboard setup tutorial](https://github.com/NVIDIA/OpenShell-Research/blob/main/projects/use-case-examples/reachy-mini-openshell/ONBOARD_SETUP.md){ .md-button .md-button--primary } +[Open the onboard setup tutorial](https://github.com/NVIDIA/OpenShell-Research/blob/main/projects/use-case-examples/reachy-mini-openshell/ONBOARD_SETUP.md). For the engineering story and reusable edge architecture, read [Designing OpenShell for the Edge: Why Policy Has to Live Next to the diff --git a/docs/stylesheets/dev-notes.css b/docs/stylesheets/dev-notes.css index 62225daa..9bce7861 100644 --- a/docs/stylesheets/dev-notes.css +++ b/docs/stylesheets/dev-notes.css @@ -69,12 +69,10 @@ body[data-md-color-scheme="slate"] { --md-typeset-a-color: var(--openshell-accent); } -body[data-md-color-scheme="default"][data-md-color-primary="black"] { - --md-typeset-a-color: var(--openshell-accent); -} - -body[data-md-color-scheme="slate"][data-md-color-primary="black"] { +body[data-md-color-scheme][data-md-color-primary="black"] { --md-typeset-a-color: var(--openshell-accent); + --md-accent-fg-color: var(--openshell-accent); + --md-accent-fg-color--transparent: color-mix(in srgb, var(--openshell-accent) 14%, transparent); } html { @@ -140,6 +138,28 @@ body { outline-offset: 0.15rem; } +.openshell-drawer-close { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + margin-left: auto; + border-radius: 0.25rem; + color: var(--openshell-muted); + cursor: pointer; +} + +.openshell-drawer-close:hover { + color: var(--openshell-ink); + background: var(--openshell-paper-deep); +} + +.openshell-drawer-close:focus-visible { + outline: 2px solid var(--openshell-accent); +} + .openshell-drawer-button svg { fill: none; stroke: currentColor; @@ -270,6 +290,10 @@ body { } @media (min-width: 64rem) { + .openshell-drawer-close { + display: none; + } + .md-header { z-index: 6; } @@ -417,6 +441,7 @@ body { } .md-typeset h1 { + margin-bottom: 1.5rem; font-size: clamp(2.25rem, 5vw, 3.8rem); line-height: 1.02; } @@ -435,6 +460,42 @@ body { font-size: 0.72rem; } +/* Keep complete breadcrumb labels together, including on narrow screens. */ +.md-path__list { + flex-wrap: wrap; + row-gap: 0.3rem; +} + +.md-path__item { + flex: 0 0 auto; + max-width: 100%; +} + +.md-path__link .md-ellipsis { + white-space: normal; + overflow-wrap: anywhere; +} + +/* Tables may scroll within the article; identifiers should not split mid-word. */ +.md-typeset table:not([class]) th { + min-width: 8rem; +} + +.md-typeset table:not([class]) { + background: var(--openshell-paper-raised); +} + +.md-typeset__table code { + overflow-wrap: normal; + word-break: normal; +} + +/* The theme appends subtitle metadata; sidebar links should show titles only. */ +.md-sidebar--primary .md-nav__link .md-ellipsis > br, +.md-sidebar--primary .md-nav__link .md-ellipsis > small { + display: none; +} + .md-nav__item .md-nav__link--active { color: var(--md-typeset-a-color); font-weight: 650; @@ -495,6 +556,7 @@ body { flex: 1 1 auto; min-width: 0; max-width: none; + margin: 0; padding: 0; font-family: var(--openshell-serif); font-size: 1rem; @@ -536,6 +598,7 @@ body { } .md-footer__link { + flex: 0 0 auto; width: 100%; max-width: none; } @@ -633,13 +696,14 @@ body:has(.openshell-home-page) .md-path { } .research-masthead { - min-height: min(30rem, calc(100vh - var(--openshell-header-height))); - padding: clamp(3.5rem, 9vw, 7.5rem) 0 clamp(3.2rem, 7vw, 5.8rem); - border-bottom: 1px solid var(--openshell-rule-strong); + padding: 1.5rem 0 1.8rem; } .research-masthead__topline { - margin-bottom: clamp(2.5rem, 6vw, 5rem); + display: flex; + align-items: center; + gap: 0.8rem; + margin-bottom: 1.8rem; color: var(--openshell-muted); font-family: var(--openshell-mono); font-size: 0.63rem; @@ -647,21 +711,20 @@ body:has(.openshell-home-page) .md-path { text-transform: uppercase; } -.research-masthead__identity { - display: inline-flex; - align-items: center; - gap: 0.65rem; -} - -.research-masthead__identity img { +.research-masthead__topline img { width: 1.6rem; height: 1.6rem; + flex-shrink: 0; } .research-masthead__layout { display: block; } +.research-masthead__copy { + display: block; +} + .research-masthead__copy h1 { max-width: 13ch; margin: 0; @@ -673,44 +736,98 @@ body:has(.openshell-home-page) .md-path { } .research-masthead__dek { - max-width: 39rem; - margin: 1.8rem 0 0; + margin: 1.4rem 0 0; color: var(--openshell-muted); font-family: var(--openshell-serif); font-size: clamp(1.02rem, 1.9vw, 1.32rem); line-height: 1.45; + text-wrap: balance; } .journal-section { - padding: clamp(3.5rem, 7vw, 6rem) 0 0; + padding: 0.8rem 0 0; } .journal-section__head { - display: flex; - align-items: baseline; - justify-content: space-between; - gap: 1.5rem; - margin-bottom: 1.2rem; - padding-bottom: 0.72rem; - border-bottom: 1px solid var(--openshell-rule-strong); + margin-bottom: 0.75rem; } .md-typeset .journal-section__head h2 { - flex: 0 0 auto; margin: 0; font-family: var(--openshell-serif); - font-size: clamp(1.45rem, 2.8vw, 2rem); + font-size: 1.1rem; font-weight: 500; letter-spacing: -0.035em; - white-space: nowrap; } -.journal-section__head span { +.dev-notes-page [hidden] { + display: none !important; +} + +.dev-notes-filters, +.dev-notes-filters label { + display: flex; + align-items: center; +} + +.dev-notes-toolbar { + padding: 0.8rem 0 0.75rem; + border-top: 1px solid var(--openshell-rule); + border-bottom: 1px solid var(--openshell-rule); +} + +.dev-notes-toolbar:not(:has(.dev-notes-filters:not([hidden]))) { + display: none; +} + +.dev-notes-filters { + justify-content: flex-end; + flex-wrap: wrap; + gap: 0.5rem 1rem; + margin: 0; +} + +.dev-notes-filters label { + gap: 0.5rem; color: var(--openshell-muted); - font-family: var(--openshell-mono); - font-size: 0.58rem; - letter-spacing: 0.08em; - text-transform: uppercase; + font-family: var(--openshell-sans); + font-size: 0.7rem; +} + +.dev-notes-filters select { + min-width: 7.5rem; + max-width: 100%; + height: 1.7rem; + padding: 0 0.4rem; + border: 1px solid var(--openshell-rule-strong); + border-radius: 0.2rem; + background: var(--openshell-paper); + color: var(--openshell-ink); + font: inherit; + line-height: 1.2; + cursor: pointer; +} + +.dev-notes-filters select:focus-visible, +.dev-notes-filters__clear:focus-visible { + outline: 2px solid var(--openshell-accent); + outline-offset: 3px; +} + +.dev-notes-filters__clear { + padding: 0.3rem 0; + color: var(--openshell-accent); + font-size: 0.7rem; + text-decoration: underline; + text-underline-offset: 0.2em; + cursor: pointer; +} + +.md-typeset .dev-notes-filter-empty { + margin: 1.5rem 0 3rem; + color: var(--openshell-muted); + font-family: var(--openshell-serif); + font-size: 1.1rem; } /* Generated note index ---------------------------------------------------- */ @@ -731,29 +848,17 @@ body:has(.openshell-home-page) .md-path { .dev-note-card--featured .dev-note-card__link { display: grid; - grid-template-columns: minmax(0, 1.15fr) minmax(19rem, 0.85fr); - min-height: 25rem; - border-bottom: 1px solid var(--openshell-rule); -} - -.dev-note-card--featured.dev-note-card--has-image .dev-note-card__link { - grid-template-columns: 1fr; -} - -.dev-note-card--featured.dev-note-card--has-image .dev-note-card__visual { - min-height: 0; - aspect-ratio: 1200 / 630; -} - -.dev-note-card--featured.dev-note-card--has-image .dev-note-card__copy { - border-left: 1px solid var(--openshell-rule); + grid-template-columns: repeat(2, minmax(0, 1fr)); + align-items: center; + gap: 1.5rem; } .dev-note-card__visual { position: relative; display: block; - min-height: 100%; - border: 1px solid color-mix(in srgb, var(--openshell-ink) 25%, transparent); + align-self: center; + min-width: 0; + aspect-ratio: 1200 / 630; background: radial-gradient(circle at 68% 44%, rgba(118, 185, 0, 0.18) 0 0.34rem, transparent 0.38rem), linear-gradient(rgba(255, 255, 255, 0.09) 1px, transparent 1px), @@ -804,15 +909,14 @@ body:has(.openshell-home-page) .md-path { display: none; } -.dev-note-card__visual-image { +.md-typeset .dev-note-card__visual-image { position: absolute; inset: 0; display: block; width: 100%; height: 100%; - object-fit: cover; + object-fit: contain; object-position: center; - transition: transform 300ms ease; } .dev-note-card__visual--runtime, @@ -864,9 +968,7 @@ body:has(.openshell-home-page) .md-path { display: flex; flex-direction: column; justify-content: center; - padding: clamp(2rem, 5vw, 4.5rem); - border-top: 1px solid var(--openshell-rule); - border-right: 1px solid var(--openshell-rule); + min-width: 0; } .dev-note-card__meta { @@ -889,30 +991,35 @@ body:has(.openshell-home-page) .md-path { } .md-typeset .dev-note-card h3 { - margin: 1.05rem 0 0.8rem; + margin: 0.6rem 0; font-family: var(--openshell-serif); font-weight: 500; letter-spacing: -0.04em; } .md-typeset .dev-note-card--featured h3 { - font-size: clamp(2rem, 4vw, 3.25rem); - line-height: 1; + font-size: clamp(1.85rem, 3.5vw, 2.5rem); + line-height: 1.08; + text-wrap: balance; } -.dev-note-card__summary { +.md-typeset .dev-note-card__summary { margin: 0; color: var(--openshell-muted); font-family: var(--openshell-serif); - font-size: 0.92rem; - line-height: 1.55; + font-size: 0.85rem; + line-height: 1.45; +} + +.md-typeset .dev-note-card--featured .dev-note-card__summary { + font-size: 1rem; } .dev-note-card__tags { display: flex; flex-wrap: wrap; gap: 0.28rem 0.7rem; - margin-top: 0.9rem; + margin-top: 0.6rem; color: var(--openshell-accent); font-family: var(--openshell-mono); font-size: 0.54rem; @@ -931,9 +1038,7 @@ body:has(.openshell-home-page) .md-path { align-items: center; justify-content: space-between; gap: 1rem; - margin-top: 2rem; - padding-top: 0.9rem; - border-top: 1px solid var(--openshell-rule); + margin-top: 1rem; } .dev-note-card__authors { @@ -993,36 +1098,40 @@ body:has(.openshell-home-page) .md-path { transform: translate(-50%, -50%) rotate(-27deg); } -.dev-note-card__link:hover .dev-note-card__visual-image { - transform: scale(1.015); -} - .dev-note-card__link:hover .dev-note-card__read::after { transform: translate(0.15rem, -0.15rem); } -.dev-notes-recent-list { - border-bottom: 0; +.dev-notes-recent { + margin-top: 1rem; + padding-top: 0.5rem; + border-top: 1px solid var(--openshell-rule); +} + +.dev-notes-recent .journal-section__head { + margin-bottom: 0; } .dev-note-card--recent .dev-note-card__link { display: grid; - grid-template-columns: minmax(14rem, 0.8fr) minmax(0, 1.2fr); - min-height: 13rem; - padding: 1.2rem 0; + grid-template-columns: 10rem minmax(0, 1fr); + align-items: start; + gap: 1.2rem; + padding: 0.8rem 0; border-top: 1px solid var(--openshell-rule); } .dev-note-card--recent:first-child .dev-note-card__link { + padding-top: 0.6rem; border-top: 0; } .dev-note-card--recent .dev-note-card__visual { - min-height: 11rem; + align-self: start; } .dev-note-card--recent .dev-note-card__visual::before { - width: 7rem; + width: 4rem; } .dev-note-card--recent .dev-note-card__visual-index { @@ -1034,15 +1143,10 @@ body:has(.openshell-home-page) .md-path { .dev-note-card--recent .dev-note-card__copy { display: grid; - grid-template-columns: minmax(0, 1fr) minmax(11rem, 0.72fr); - grid-template-areas: - "meta meta" - "title summary" - "tags summary" - "footer footer"; - gap: 0.5rem clamp(2rem, 5vw, 4rem); - align-content: center; - padding: 0.75rem 0 0.75rem clamp(1.5rem, 4vw, 3.5rem); + grid-template-columns: minmax(0, 1fr); + grid-template-areas: "meta" "title" "summary" "tags" "footer"; + gap: 0.35rem; + min-width: 0; } .dev-note-card--recent .dev-note-card__meta { @@ -1051,26 +1155,30 @@ body:has(.openshell-home-page) .md-path { .md-typeset .dev-note-card--recent h3 { grid-area: title; - margin: 0.65rem 0 0; - font-size: clamp(1.32rem, 2.6vw, 2rem); - line-height: 1.08; + margin: 0; + font-size: 1.15rem; + line-height: 1.2; } -.dev-note-card--recent .dev-note-card__summary { +.md-typeset .dev-note-card--recent .dev-note-card__summary { grid-area: summary; - align-self: start; - margin-top: 0.72rem; font-size: 0.78rem; } .dev-note-card--recent .dev-note-card__tags { grid-area: tags; - margin-top: 0.25rem; + align-self: center; + margin-top: 0; } .dev-note-card--recent .dev-note-card__footer { grid-area: footer; - margin-top: 1rem; + margin-top: 0; +} + +.dev-note-card--recent .dev-note-card__authors img { + width: 1.25rem; + height: 1.25rem; } .dev-notes-empty { @@ -1126,7 +1234,7 @@ body[data-md-color-scheme="slate"] .openshell-home-brand__dark { .md-typeset .openshell-home-link { display: flex; - align-items: center; + align-items: flex-start; justify-content: space-between; gap: 2rem; min-height: 11rem; @@ -1142,6 +1250,7 @@ body[data-md-color-scheme="slate"] .openshell-home-brand__dark { .openshell-home-link::after { flex: 0 0 auto; + align-self: center; color: var(--openshell-green); font-size: 1.3rem; content: "↗"; @@ -1170,29 +1279,54 @@ body[data-md-color-scheme="slate"] .openshell-home-brand__dark { /* Article bylines --------------------------------------------------------- */ -.md-typeset #the-leak-is-coming-from-inside-the-sandbox { - margin-bottom: 1rem; - font-size: clamp(1.85rem, 4vw, 2.8rem); - line-height: 1.12; - text-wrap: balance; +/* Shared reading proportions for every generated Dev Note. */ +body:has(.dev-note-header) .md-main__inner { + margin-top: 0.8rem; } -.md-typeset #the-leak-is-coming-from-inside-the-sandbox + .dev-note-deck { - margin-bottom: 0.25rem; - font-size: clamp(0.95rem, 1.6vw, 1.05rem); +.md-content__inner:has(> .dev-note-byline) { + position: relative; + width: calc(100% - 2rem); + max-width: 74ch; + margin-inline: auto; + margin-top: 0.6rem; + font-size: 0.9rem; + line-height: 1.7; +} + +/* A floated action would narrow the entire opening, including its hero. */ +.md-content__inner:has(> .dev-note-byline) > .md-content__button { + position: absolute; + top: 1.1rem; + right: 0; + margin: 0.2rem 0 0; +} + +.dev-note-header { + display: grid; + grid-template-rows: auto auto minmax(0, 1fr); + /* Reserve room for the site header, breadcrumbs, and page gutters. */ + max-height: calc(100svh - var(--openshell-header-height) - 5rem); +} + +.md-typeset .dev-note-header > h1 { + margin: 0 0 1rem; + padding-inline-end: 2rem; + font-size: clamp(1.85rem, 4vw, 2.8rem); + line-height: 1.12; text-wrap: balance; } -.md-typeset #the-leak-is-coming-from-inside-the-sandbox ~ h2 { +.md-typeset:has(> .dev-note-byline) > h2 { margin-top: 1.6rem; } .md-typeset .dev-note-deck { max-width: 46rem; - margin: -0.35rem 0 1.8rem; + margin: 0 0 0.25rem; color: var(--openshell-muted); font-family: var(--openshell-serif); - font-size: clamp(1.08rem, 2vw, 1.28rem); + font-size: clamp(0.95rem, 1.6vw, 1.05rem); line-height: 1.5; } @@ -1231,17 +1365,22 @@ body[data-md-color-scheme="slate"] .openshell-home-brand__dark { } .dev-note-byline { - margin: -0.5rem 0 2rem; - padding: 1rem 0; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 0.6rem 1rem; + margin: 0.8rem 0 1.2rem; + padding: 0.65rem 0; border-top: 1px solid var(--openshell-rule-strong); border-bottom: 1px solid var(--openshell-rule); } -.dev-note-byline__label { +.md-typeset .dev-note-byline__label { display: flex; flex-wrap: wrap; gap: 0.35rem 0.8rem; - margin: 0 0 0.75rem; + margin: 0; color: var(--openshell-muted); font-family: var(--openshell-mono); font-size: 0.58rem; @@ -1255,6 +1394,17 @@ body[data-md-color-scheme="slate"] .openshell-home-brand__dark { content: "/"; } +.md-typeset .dev-note-byline__label a { + color: inherit; + text-decoration: underline; + text-decoration-color: var(--openshell-rule-strong); + text-underline-offset: 0.2em; +} + +.md-typeset .dev-note-byline__label a:hover { + color: var(--openshell-accent); +} + .dev-note-byline__authors { display: flex; flex-wrap: wrap; @@ -1270,8 +1420,8 @@ body[data-md-color-scheme="slate"] .openshell-home-brand__dark { } .dev-note-byline__author img { - width: 2.8rem; - height: 2.8rem; + width: 2rem; + height: 2rem; border-radius: 50%; object-fit: cover; background: var(--openshell-paper-deep); @@ -1294,50 +1444,68 @@ body[data-md-color-scheme="slate"] .openshell-home-brand__dark { /* Responsive -------------------------------------------------------------- */ -@media (max-width: 52rem) { - .research-masthead { - min-height: auto; +@media (max-width: 44rem) { + .dev-notes-filters { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + width: 100%; + gap: 0.6rem; } - .dev-note-card--featured .dev-note-card__link { - grid-template-columns: 1fr; + .dev-notes-filters label { + align-items: stretch; + flex-direction: column; + gap: 0.2rem; } - .dev-note-card--featured .dev-note-card__visual { - min-height: 20rem; + .dev-notes-filters select { + width: 100%; + min-width: 0; + min-height: 2.2rem; + font-size: 0.8rem; } - .dev-note-card--featured .dev-note-card__copy { - border-left: 1px solid var(--openshell-rule); + .dev-notes-filters__clear { + grid-column: 1 / -1; + justify-self: start; } - .dev-note-card--recent .dev-note-card__copy { - grid-template-columns: 1fr; - grid-template-areas: "meta" "title" "summary" "tags" "footer"; - gap: 0.35rem; + .research-masthead__dek { + margin-top: 0.8rem; } -} - -@media (max-width: 44rem) { - .research-masthead__topline { - margin-bottom: 2.6rem; + .dev-note-card--featured .dev-note-card__link { + grid-template-columns: 1fr; + gap: 0.85rem; } - .research-masthead__identity { - white-space: nowrap; + .dev-note-card--featured .dev-note-card__visual { + width: min(100%, 21rem); + justify-self: center; } .dev-note-card--recent .dev-note-card__link { - grid-template-columns: 1fr; + grid-template-columns: 5rem minmax(0, 1fr); + grid-template-areas: + "visual meta" + "visual title" + "summary summary" + "tags tags" + "footer footer"; + gap: 0.4rem 0.75rem; } .dev-note-card--recent .dev-note-card__visual { - min-height: clamp(12rem, 48vw, 17rem); + grid-area: visual; + } + + .dev-note-card--recent .dev-note-card__visual-label, + .dev-note-card--recent .dev-note-card__visual-index { + display: none; } .dev-note-card--recent .dev-note-card__copy { - padding: 1.3rem 0 0.7rem; + display: contents; } } @@ -1356,39 +1524,6 @@ body[data-md-color-scheme="slate"] .openshell-home-brand__dark { margin-top: 2.75rem; } - .research-masthead { - padding-top: 2.5rem; - } - - .research-masthead__copy h1 { - font-size: clamp(3rem, 16vw, 4.25rem); - } - - .journal-section__head { - align-items: flex-end; - } - - .journal-section__head span { - max-width: 8rem; - text-align: right; - } - - .dev-note-card--featured .dev-note-card__visual { - min-height: 16rem; - } - - .dev-note-card--featured .dev-note-card__copy { - padding: 1.6rem 1.25rem 1.8rem; - } - - .dev-note-card--recent .dev-note-card__visual { - min-height: 11.5rem; - } - - .dev-note-card--recent .dev-note-card__copy { - padding: 1.2rem 0 0.6rem; - } - .dev-note-card__footer { align-items: center; flex-wrap: wrap; @@ -1419,14 +1554,27 @@ body[data-md-color-scheme="slate"] .openshell-home-brand__dark { } @media (max-width: 24rem) { - .journal-section__head { - gap: 0.75rem; + .research-masthead__copy h1 { + font-size: 2.8rem; + } + + .dev-notes-filters { + grid-template-columns: minmax(0, 1fr); + } + + .dev-notes-filters label { + display: grid; + grid-template-columns: 3.4rem minmax(0, 1fr); + align-items: center; } - .journal-section__head span { - max-width: 6.5rem; - font-size: 0.53rem; - line-height: 1.35; + /* Leave room for a second breadcrumb row without crowding the hero. */ + .dev-note-header { + max-height: calc(100svh - var(--openshell-header-height) - 6.5rem); + } + + .md-typeset .dev-note-header > h1 { + font-size: 1.6rem; } .dev-note-card--featured .dev-note-card__visual-index { @@ -1436,11 +1584,6 @@ body[data-md-color-scheme="slate"] .openshell-home-brand__dark { left: 1rem; } - .dev-note-card--recent .dev-note-card__footer { - align-items: flex-start; - flex-direction: column; - gap: 0.75rem; - } } @media (prefers-reduced-motion: reduce) { @@ -1455,15 +1598,14 @@ body[data-md-color-scheme="slate"] .openshell-home-brand__dark { .md-footer, .dev-note-card__visual::before, .dev-note-card__visual::after, - .dev-note-card__visual-image, .dev-note-card__read::after { transition: none !important; } } /* Dev note figures and theme-specific images */ -body:not([data-md-color-scheme="slate"]) img.dev-note-image--dark, -body[data-md-color-scheme="slate"] img.dev-note-image--light { +body:not([data-md-color-scheme="slate"]) .dev-note-image--dark, +body[data-md-color-scheme="slate"] .dev-note-image--light { display: none; } @@ -1490,40 +1632,44 @@ body[data-md-color-scheme="slate"] img.dev-note-image--light { max-width: 960px; } -.md-typeset .dev-note-figure--hero img { - max-width: 100%; - aspect-ratio: 1200 / 630; +.md-typeset .dev-note-figure--hero { + display: grid; + grid-row: 3; + grid-template-rows: minmax(0, 1fr); + min-height: 0; + max-height: 28rem; + margin: 0.8rem auto; } -.md-typeset #pi-admission-hero { - margin: 0.25rem auto 0.5rem; +.dev-note-figure--hero > a { + display: grid; + grid-template-rows: minmax(0, 1fr); + min-height: 0; } -.md-typeset #pi-admission-hero img { +.md-typeset .dev-note-figure--hero img { + width: 100%; + height: 100%; + min-height: 0; + max-width: 100%; + max-height: 100%; + object-fit: contain; background: transparent; border: 0; border-radius: 0; box-shadow: none; - mask-image: - linear-gradient(to right, transparent, #000 4%, #000 96%, transparent), - linear-gradient(to bottom, transparent, #000 6%, #000 94%, transparent); - mask-composite: intersect; } body:not([data-md-color-scheme="slate"]) .dev-note-card__visual:has(img[src$="/pi-admission/hero-light.png"]) { background: var(--openshell-paper); } -body:not([data-md-color-scheme="slate"]) #pi-admission-hero img, +body:not([data-md-color-scheme="slate"]) .dev-note-figure--hero img[src$="/pi-admission/hero-light.png"], body:not([data-md-color-scheme="slate"]) .dev-note-card__visual-image[src$="/pi-admission/hero-light.png"] { filter: brightness(1.06); mix-blend-mode: multiply; } -.md-typeset .dev-note-figure--native-aspect img { - aspect-ratio: auto; -} - .md-typeset .admonition, .md-typeset details { font-size: inherit; diff --git a/overrides/404.html b/overrides/404.html index 0ce3477a..fed9bf75 100644 --- a/overrides/404.html +++ b/overrides/404.html @@ -9,23 +9,23 @@